Официальный сайт SLAED CMS
Журнал изменений
All four batches of docs/QUICK-EDIT-2026.md have landed. The protocol, the stamp rule, the status table, the limits and how to add a kind are in docs/ARCHITECTURE.md ("Quick edit"); the Node writer and limits.edit in docs/NODE.md; the release note in docs/VERSIONS.md; the attachment check the forum quick edit still needs is carried by docs/FILES-2026.md, batch 4.
Core changes:
- Plan (docs/QUICK-EDIT-2026.md):
- Deleted; its final, audited version stays in the history of the previous commit
Technical notes:
- Documentation only, no code change
The two separate inline edits of comments and forum posts become one protocol, QuickEdit, with strict GET and POST routes, a stamp of the stored row and a fixed order under the lock of each kind: existence, the right, an equal text saved without a write, and only then a conflict. Node gains the quick edit of the intro and the body on the public page for its moderator and, inside limits.edit, for the signed-in author; editor assets load once per page, so a fragment no longer leaves the editor hidden or runs an engine twice.
Core changes:
- Protocol (core/classes/quick.php, core/system.php, index.php, templates/lite/fragments/quick-edit.html):
- QuickEdit: closed forms of kind, id, field and stamp, closed result codes, QuickEdit::getStamp() for texts without a version
- getQuickService() builds one adapter per kind; each source renders its editor with the literal store of its column
getQuickEdit (GET) and updateQuickEdit (POST) check the method first, then the token from the X-CSRF-TOKEN header alone
- 405 / 403 / 404 / 409 / 422 / 500 / 503 per outcome; a conflict answers the current text with its fresh stamp
- A save answers the region with the edited mark out of band and a note as the event header sl-quick-note
- One fragment holds the form, the conflict answer and the always-printed mark wrapper
- Comments (core/classes/comment.php, core/user.php):
- Comment::updateComment() takes the stamp, decides existence, right, window, equality and stamp under the row lock
- setWriteBegin() answers blocked for a closed guard and storage for a failed BEGIN
- getCommentBody() renders the body and the mark for the page and the quick edit alike
- The route op=updateComment and its handler are gone
- Forum (core/user.php, modules/forum/index.php, index.php):
updateForumBody() writes under the write guard: topic row, then post row, the place and the right read again
- A topic closed or a post moved after the editor opened refuses the author; the page cache moves only after a write
- getForumSource() and getForumBody() serve the page and the quick edit
- updatePost(), its route, its epoch bump in the router and getTplAjaxTextarea() are gone
- Node (core/classes/node/.php, modules/node/, config/node.php, update.php):
NodeService::updateNodeText() and getTextSource(): intro or body of a type without an extension
- Moderator in every state; author of a pending or published material inside limits.edit from creation
- An author edit of a published material goes back to Pending in the same write unless the author publishes directly
- NodeStatus gains Published -> Pending; NodeException gains BLOCKED (7, HTTP 503) for a closed write guard
- setNodeWrite() reports a closed guard as BLOCKED and moves no cache generation for a work that wrote nothing
- limits.edit (default 600, 0 off) on the limits tab; update.php adds it to an existing config/node.php
- The view partials wrap intro and body in regions; the public page offers the quick edit before the full editor
- A material the visitor may neither edit nor read answers 404, as Node answers a missing and a closed one alike
- Editor assets (core/classes/editor.php, plugins/editors/, plugins/system/.js, core/helpers.php):
- Editor::getAssetTags(): plain tags on a page load, the client loader SlaedEditors on an htmx fragment
- Editor::getInitScript(): the init waits for its engine, checks its node and registers its teardown
- setQuickEdit in slaed.js: Cancel without a request, Escape and Ctrl+Enter, the warning toast, the conflict question
- A region swapped away destroys its editors and clears both Toast UI registries
- Tests, language and help:
- QuickEditTest drives both routes over real HTTP for comments, forum posts and Node materials
- NodeServiceTest covers updateNodeText(); comment, forum and node tests follow the new contracts
- Seven site constants, _SAVEBUSY and the panel label _NODE_AEDIT in all six locales
- Help of comments, forum, Node, categories and settings; docs/ARCHITECTURE.md, NODE.md, VERSIONS.md, FILES-2026.md
Benefits:
- A refusal or an expired session no longer swaps an alert over the typed text
- Two editors of one item can no longer overwrite each other unnoticed; a repeated save writes nothing twice
- One protocol, one fragment and one client handler instead of two inline paths and HTML built in PHP
Technical notes:
- Breaking: Comment::updateComment(int $id, string $body, string $stamp) answers a result code
- Breaking: NodeException::BLOCKED is new; setNodeWrite() reported a closed guard as STORAGE
- New configuration key node.limits.edit; a limits section missing it makes every Node type invalid until update.php runs
- No schema change
Site mail is built for deliverability: every mailing carries a signed one-click unsubscribe (RFC 8058) in its body and headers, notices are marked auto-generated, and group and activity audiences keep to subscribers. Delivery reports are read over POP3 from a dedicated bounce mailbox in every maildrain run, so an address that no longer exists leaves mailings without any server setup. The registry of undeliverable addresses is written at last: its upsert repeated a placeholder that native prepares refuse, so it had never stored a single row.
Core changes:
- Unsubscribe (core/classes/mail.php, core/system.php, modules/account/index.php):
getUnsubKey() signs the address with getSecret('unsub'); the drain appends the link of each recipient to the body
- List-Unsubscribe, List-Unsubscribe-Post, List-Id in the site domain and Precedence: bulk on mailings
- Auto-Submitted: auto-generated on every other message
op=unsub of the account module: GET shows the question and a form, POST sets newslet = 0 for that address
- The one-click POST of a mail provider carries no session; the signed key authorizes it instead of a token
- The address is read raw, checked by the key and escaped on output
- Audiences (core/system.php, admin/modules/newsletter.php):
- group and active select only accounts with newslet = 1; all stays the forced send an administrator chooses
The days field of the activity audience shows again, and an edited campaign keeps its audience and window
- The option value, its count and the show rule are built from the same number of days
- Delivery reports and the registry (core/classes/mail.php, core/system.php, admin/modules/config.php):
- A bounce address goes into the envelope of every transport; From is unchanged
Message-ID carries a signature bound to the recipient; addBounce() believes only a failed 5.x.x report quoting it
- A report naming another address, a soft failure, a wrong signature or an auto-reply marks nobody
updateBounce() reads up to 50 messages over POP3 (none, STARTTLS, SSL) in each maildrain run
- Verdicts are written only after QUIT committed the deletions, so a broken session counts nothing twice
- A mailbox that cannot be read is named in the job result and never stops delivery
- addDeadMail() names every placeholder once; getDeadList() and deleteDeadMail() serve the new screen
- The settings carry the bounce address and the POP3 host, port, encryption, user and password
- Admin screen (admin/modules/newsletter.php):
- New tab Undeliverable addresses: address, failures, code, date and a button to send to the address again
- Text part and parser (core/classes/mail.php, core/classes/parser.php):
- The plain-text part starts every list item on its own line with a dash
- A line opening with <br> no longer starts a raw HTML block, so Markdown after a list in plain-editor text is parsed
- The breaks format also recognizes <br/> and <br /> at a line end
- A blank configured sender name falls back to the site name
- Languages (lang/.php, modules/account/lang/.php, admin/lang/*.php):
- _MAIL_UNSUB; _ACCOUNT_UNSUBASK, _ACCOUNT_UNSUBBAD, _ACCOUNT_UNSUBOK
- _MAIL_BOUNCE, _MAIL_BOUNCEI, _MAIL_POP*, _MAIL_DEAD, _MAIL_FAILS, _MAIL_REVIVE
- Tests and docs (tests/, admin/info/, docs/PERFORMANCE.md, config/mail.php):
- A probe POP3 mailbox; the mail probe covers the envelope, signed ids, forged reports, the registry and POP3 runs
- Header, plain-text, config and parser fixture tests for every change above
- The newsletter and configuration help describe the unsubscribe, the bounce mailbox and the new tab
Benefits:
- Mail providers offer their own unsubscribe button instead of the spam button
- Dead mailboxes stop receiving mailings on any hosting, with nothing to configure on the server
- The registry of undeliverable addresses finally records what it was built for
Technical notes:
- No schema change; config/mail.php gains bounce, pophost, poppass, popport, popsecure and popuser
- New route index.php?name=account&op=unsub, new admin ops dead and revive
- Unsubscribe links and report signatures depend on a stable site secret in config/security.php
The last two batches of the installer plan land: the probes walk the new setup.php end to end, the help moves into the permanent references and the plan file is deleted. The installer stops deleting itself on an installed site, which a development copy needs, and its CSRF token moves from storage/install.php into the session. Site mail is sent as multipart/alternative from the site address, with Date and Message-ID on every transport.
Core changes:
- Installer (setup.php, templates/admin/pages/setup.html, templates/admin/assets/css/theme.css):
setSetupShut() answers an installed site with the refusal only: no form, no write and no delete
- The installer deletes itself only on the closing stop of its own installation
- A file that stays keeps the _DELSETUP warning of the panel
The CSRF token is a random value in the session of the installing browser
- storage/install.php, getSetupToken() and its two unlink() calls are gone
- A token of another browser is refused like none
Zip and Zlib are optional: a missing one is a warning row (is_warn, .sl-is-warn) that lets the server stop pass
- mbstring, PDO MySQL and JSON still hold the stop
- Mail (core/classes/mail.php, core/system.php, core/user.php, modules/account, modules/forum, admin/modules/config.php):
- Every message is multipart/alternative: a plain-text part derived from the HTML, then the HTML part
- Date and Message-ID are written for every transport; X-Priority stays normal
- From is the configured identity, else the site address; a form visitor becomes Reply-To, never the sender
- getMailFrame() wraps a text into the plain-text mtemp frame; every caller goes through it
- addQueue() stores the body through getOutputHtml()
- Languages (admin/lang/*.php):
- _SETUP_NOOPT for a missing optional extension
- _SETUP_ZIP and _SETUP_ZLIB name the ZIP and GZ archives they serve
- Tests (tests/Support/, tests/Unit/):
The install probe walks the seven stops over HTTP, checks the refusals with the own and a foreign token, and expects a shut installer to stay in place
- Mail tests cover the frame, the queued body format, the sender, Reply-To, Date and Message-ID
Docs and housekeeping (docs/*, README.md, SECURITY.md, UPGRADING.md, CONTRIBUTING.md, .htaccess, robots.txt, admin/modules/editor.php, core/classes/filemanager.php, .gitignore, demo/*):
docs/SETUP-2026.md is deleted; the installer lives in docs/ARCHITECTURE.md "Installation", the 6.3 update in docs/NODE.md "The 6.3 update"
- UPGRADING.md states that the release does not update a 6.2 site
- The setup/ directory rules and the setup.php entry of the critical files of the file manager are dropped
Benefits:
- A development copy of an installed site keeps its installer
- The installer writes nothing outside config/ before the run and leaves no token file behind
- A server without Zip or Zlib installs
- Site mail passes SPF and DMARC of the site domain and carries the headers filters score
Technical notes:
- No schema change
- storage/install.php is no longer written; an existing one is unused
- An installed site with setup.php left in the root shows the panel warning until the file is deleted
The XML sitemap listed every forum topic and category, including those whose read right is closed to guests, so the map advertised addresses a search engine could only answer with a refusal.
Core changes:
- Sitemap task (core/system.php, addSitemapTask()):
- A forum topic enters the map only when the read right of its category opens to a guest
- A forum category enters the map only when its view right opens to a guest
- The guest branch follows is_acess(): level 0, no group, and an empty right stays closed
- Configuration and output (config/sitemap.php, sitemap.xml):
- The modules of the map include forum
- sitemap.xml is generated again on this rule: 3385 addresses, 2221 of them in the forum
Benefits:
- The map names only pages a crawler can read
Technical notes:
- Node types keep their own sitemap integration, read as a guest of the site language
The help tab of the materials section grew from one page of notes into a full operator guide, written as the current state of the system and checked against modules/node/admin/index.php, the Node classes and docs/NODE.md.
Core changes:
- Help (modules/node/admin/info/ru.md):
- Terms: type, material, resource and its role, extension, profile
The list: filters, the five states and the allowed moves, the trash, what a permanent deletion removes, the version check
The material form: fields by the features of the type, deferred publication and the nodepublish job, editor attachments [attach=...], concurrent editing, reports on resources
Types: life of a type, the nginx rule for uploads/<type>, records of removed sections, every setting of a type with tables of the seven display modes, the twelve features and the six display modes of a resource role
The ten shipped types, the support extension (queue, working card, states, mail) and the sync extension (source, period, manual check, back-off after errors), the four limits with their defaults, the rights
14 callouts of the five kinds the parser knows; the nginx rule stands under its callout, because a fence inside a callout is dropped
Benefits:
- An operator finds the answer on the tab instead of in the developer reference
Technical notes:
- Russian only, as every help page of the panel; renders cleanly on the stand
A call of the scheduler endpoint ran only the first due job. nodepublish is due every minute, so on a site called once a minute the jobs further down the priority list hardly ever got a turn: on the stand monitor had not run for twenty days, nodesync never, and maildrain ran only now and then although all mail of the site goes through it.
Core changes:
- Runner (core/system.php):
addSchedulerBatch() walks the jobs in priority order and runs each due one at most once
- A job another process holds is passed over, the rest of the call still runs
- No further job starts once the call has spent 60 per cent of max_execution_time, or 120 s without a limit
- Returns status done or idle with the list of jobs it ran
- Endpoint (index.php):
- A cron or pseudo call without job= runs addSchedulerBatch(); with job= it still runs that one job
- Tests (tests/Support/scheduler_probe.php, tests/Unit/SchedulerLockTest.php):
oneCallRunsEveryDueJobOnce: two due jobs run in one call in priority order, a second call finds nothing, and a job held by a second process is passed over while the other one runs
- Help (admin/info/scheduler/ru.md):
- All nine system jobs with key, schedule and priority, including nodepublish, nodesync and monitor
How a call runs, real cron each minute, the pseudo-cron and when it steps aside, the six top-level settings of config/scheduler.php, the form and its limits, the labels as the screen shows them
Benefits:
- Every job runs on its own schedule
- The work per unit of time is the configured one; the runner itself adds a few file reads per call
Technical notes:
- Manual runs from the panel are unchanged
- The JSON answer of a call without job= carries jobs[] instead of a single job key; nothing reads it
The help tab op=info borrowed _DOCS, which is also the title of the Node type docs. With the type named "Документация" every help tab of the panel said so too. The tab gets a constant of its own, and the two places that used _DOCS for something else get labels that say what they do.
Core changes:
- Constant (admin/lang/*.php, lang/ru.php, lang/uk.php):
- _MANUAL in all six admin dictionaries: Help, Hilfe, Aide, Pomoc, Справка, Довідка
- _DOCS reads Документация in ru and Документація in uk, the title of the docs type in every locale
- pl _NO_SICHT reads Niewidoczny w bloku modułów instead of Niewiem w bloku modułów
- Help tabs (admin/index.php, admin/modules/.php, modules//admin/index.php):
- Every op=info tab and link and the help item of a module menu read _MANUAL
- Two tab calls of admin/modules/groups.php wrap to stay inside 180 characters
- Labels (admin/modules/security.php, admin/modules/modules.php):
- The log view of the security section reads _SHOW
- The module list tip reads _STATUS: Invisible in the block of modules
- The title of a module name carries the plain name instead of the markup of its tip
- Usage audit (tests/LanguageConstantsUsageTest.php):
Constants named by a module profile modules//profiles/.json count as used, so _DOCS, _NODE_CAST and the other field titles of the Node profiles leave the list of unused constants
Benefits:
- One constant, one meaning: help is help, the docs type is documentation
- The unused count of the audit names only what is really unused
Technical notes:
- No constant removed; _MANUAL is new in admin/lang only, where every user of it runs
- LanguageValidation, LanguageConstantsUsage and ModuleStructure are green
Work ahead of batch 6 of docs/SETUP-2026.md: the tests that still read the old installer lose what only it had, and two tests that could never fail leave the suite.
Core changes:
- Installer contract (tests/Unit/NodeProfileTest.php):
- onlyANewInstallationLeavesTheMark is gone, it grepped the update branches of setup/index.php
theUnlockedInstallerRefusesBeforeItWrites becomes theInstallerRefusesBeforeItWrites without the step and code rows; its other rows stay the contract setup.php must meet
- Tree walk (tests/Unit/PointOwnersTest.php):
- setup/ leaves the directories the point owners are searched in
- Route probe (tests/Support/route_probe.php):
The built-in server runs with opcache.revalidate_freq=0, because the probe rewrites the scratch configuration between requests and a file cached two seconds longer served old values to NodeGuardTest and NodeIntegrityTest
- Placeholders (tests/Unit/ExampleTest.php, tests/LanguageValidationTest.php, docs/TESTS.md):
ExampleTest asserted only that PHPUnit runs; testNoUnusedConstants was skipped unconditionally since the usage audit of LanguageConstantsUsageTest replaced it
Benefits:
- No test fails on a directory that no longer exists
- Every remaining test can fail
Technical notes:
- NodeProfileTest stays red until batch 6 drives the new stops of setup.php
Batch 4 of docs/SETUP-2026.md: the new installer is one file in the site root on the admin theme. It installs a new site only, keeps its answers in the session, writes the configuration, the tables, the data and the administrator as separate requests, and removes itself and its token with the closing stop. Batch 5 is dropped: the empty-table recovery form of admin.php stays as the way back into a panel that lost its last administrator.
Core changes:
- Installer (setup.php):
- Stops: language, server checks, database, site, administrator, run, done
- Boots without the core; every function carries a name the core does not declare
An installed site - config/db.php names a database holding an _admins row or not answering - is refused and the file tries to delete itself; only the browser that started the run passes
- A one-time token in storage/install.php guards every form; the answers live in the session under {user_c}-setup
The run: configuration, table.sql in groups of seven, every statement of insert.sql alone, the administrator; a part is marked busy before it runs, so a repeated request fails the run instead of running it twice
Only the administrator part boots the core; it creates the account, imports the Node profiles and sets the core language cookie
- Panel (admin/index.php, core/admin.php, templates/admin/partials/auth-form.html):
- addNodeProfiles() moves to core/admin.php, where setup.php can load it; admin/index.php keeps calling it
An administrator password is hashed as typed; the login checks it as typed and falls back to the old form of the login, so an older hash still opens the panel
- The recovery form carries a token of scope add_admin
- Driver (templates/admin/assets/js/admin-ui.js):
- Posts go=part until the run reports no more parts or a reply breaks, then submits go=next for the closing stop
- Repository (.gitignore, docs/SETUP-2026.md):
- storage/install.php is ignored; the plan records batches 3 and 4 as landed and batch 5 as dropped
Benefits:
- One file to upload, no setup/ directory, no installer left on a live site
- A failed part is reported with its reason and resumes from the database stop
Technical notes:
- The release carries a new installation only; updating a 6.3 site is update.php, which is not shipped
- Batch 6 (installer tests) is next; NodeProfileTest stays red until it lands