Официальный сайт SLAED CMS
Журнал изменений
Roadmap step 15, the rehearsal of docs/1-FILES-2026.md: update.php of the tree ran both stages over HTTP on a copy of the production dump of 2026-09-30 with the production files, in a scratch tree outside the stand. It found four faults, each fixed here, and that a clean 6.2 site is not migrated; every file address of the stored texts was compared before and after, and the pages that show them were crawled.
Core changes:
- First stage (public/update.php):
The first stage defines UPLOADS_DIR, which setUpdateAttach() reads for the forum and the private messages
- Without it the first forum post with a direct address stopped the update with a fatal error
setUpdateConfig() drops a 6.2 global key that names a configuration area (forum, newsletter, search)
- getConfig() merges the files in name order, so 'forum' => '0' of global.php replaced the whole forum area and
every topic answered a TypeError
- Foreign addresses (public/update.php):
- getMigrateForeign() takes the owner of the file route
setMigrateForeign() points a direct address of uploads/forum/ in any text but a forum post at the go=file address of the first published post that carries the name
- The news #468 and the private message #1665 of production reach their forum files through posts #6235 and #14824
- Order of the operator (UPGRADING.md, docs/NODE.md, lang/*.php):
The upload folders move into uploads/node/<type>/ after the migration and before the site opens
- addNodeType() refuses a type whose folder already holds a file, so a folder moved before the run stopped it
- _NODE_MIGINFO says so in the six locales
NODE.md records the foreign address step, the path constants of the first stage, the dropped global keys and that the migration reads the 6.3 shape of the module tables, which a clean 6.2 site does not have
Tests and probes (tests/Unit/UpdateSiteTest.php, tests/Unit/UpdateConfigTest.php, tests/Unit/UpdateAttachTest.php, tests/Support/install_probe.php, tests/Support/update_probe.php, tests/Fixtures/update62/site.sql):
The update62 fixture seeds a forum post and a private message with direct addresses; the probe reports both texts
- The forum post is one more rating target, so the expected count is 4
- The configuration probe ships forum.php and carries the 6.2 key forum, which must not reach global.php
- UpdateAttachTest covers the forum owner of getMigrateForeign()
- Documentation and plans (docs/1-FILES-2026.md, docs/ROADMAP-2026.md):
The files plan records the rehearsal and the decisions "The rehearsal data", "The folders move after the migration" and "A forum file outside the forum"; the roadmap ticks step 15
Benefits:
- update.php runs through on the production data of slaed.net without a stop
Of 721 file addresses in the stored texts none is lost; the guest crawl of 231 pages saw 1049 file references, all answering 200, and the 12 addresses no guest reads answered 200 to the panel
Technical notes:
- Breaking for the operator: the folder moves of UPGRADING.md now come after update.php, not before
- A clean 6.2 site with sid, hometext and bodytext is not migrated; update.php serves slaed.net alone
- The stand database still carries the two forum addresses of news #3895 and private message #1665 unconverted
- No schema change
Batch 8 of docs/1-FILES-2026.md (step 14 of docs/ROADMAP-2026.md). uploads/archive/ leaves the public list of the light path, and update.php points a direct address a text keeps into the 6.2 folder of a type it does not belong to at the go=file address of a published material of that type which carries the name: one owner and one route per file.
Core changes:
- The archive retired (core/stream.php):
- archive is off getUploadPublic(), so every address under uploads/archive/ answers 410, an existing file included
- Addresses of another owner (public/update.php):
getMigrateForeign() rewrites uploads/<dir>/<name> and its thumb into ./index.php?go=file&own=node&id=<id>&key=<name>
- [code] and [php] keep their examples, another host and a name outside the attachment grammar stay
- A name no material carries keeps its address, which answers 410
setMigrateForeign() runs after the data step on every run of the migration over Node texts, comments, the forum, private messages, site messages, newsletters, blocks, signatures, own blocks and polls
- The material is the first published one of the type whose text, [usehtml] address or published comment carries
the name; the own folder of a material and of its comments stays with batch 3
- A text the longer address would push past its column keeps its addresses and is named in the notes of the run
Tests and probes (tests/Unit/UpdateAttachTest.php, tests/Unit/PublicTreeTest.php, tests/Unit/UploadFormatTest.php, tests/Support/format_probe.php):
- UpdateAttachTest covers the pointing, the thumb, the kept example, the other host and the name no material carries
- PublicTreeTest refuses uploads/archive/ and gives it no address; the format probe renders through presentation
- Documentation and plans (docs/*, UPGRADING.md):
The files plan records batch 8, the stand run and the decisions "A file of another owner" and "The stand texts of batch 3"; the roadmap ticks step 14
- UPGRADING.md drops archive from the public folders and describes the pointing of foreign addresses
Benefits:
- No file of the archive is readable by its address any more
A forum post, a message or a newsletter that showed a file of a news or files material keeps showing it to the reader of that material, without a copy
Technical notes:
- Breaking: uploads/archive/<name> answers 410
On the stand the conversion of batch 3 ran again (122 materials, 4 comments) and the pointing reached message #16, newsletter #11 and forum post #2791; no stored text addresses uploads/archive/
- No schema change
Batch 7 of docs/1-FILES-2026.md (step 13 of docs/ROADMAP-2026.md). uploads/voting/ leaves the public list of the light path and the owner comment gets its adapter in FileAccess: a comment grants the [attach] names its body carries to a reader of its poll or profile while it is published, and the comment layer refuses a new name the writer may not bind on every target, Node included.
Core changes:
- The comment adapter (core/stream.php, core/system.php, core/classes/access.php, core/user.php):
- voting is off getUploadPublic(); getUploadOwner() names comment for it
getFileService() wires comment: its target is the id of the comment, its folder the one the comment form of its target uploads into (getCommentPlace(): uploads/voting/, uploads/profile/)
checkCommentFile() serves a name of a published comment whose module page is open to the reader and whose target shows its discussion (Comment::getTargetMode()); a moderator of the module reads every comment
- A comment of a Node material is refused here and keeps its route through the material
- FileAccess::PREVIEW gains comment; own=comment&name=voting serves the own upload of the poll comments
- getCommentBody() renders a comment in ['comment', $id] against the folder of getCommentPlace()
- The profile owner (core/user.php):
checkProfileFile() grants the names of the own block to its owner alone, beside the signature for every reader of the profile
- Writers of a comment (core/classes/comment.php):
Comment::checkAttachNames() refuses the first new [attach] name that is no own upload of the folder, no file for a moderator of it and no name the same target already serves
- A published comment of the target, and for Node NodeService::getNodeFile() of the material
- The new comment, the author edit and the moderator edit (updateBody()) ask it
- Tests and probes (tests/Unit/FileAccessTest.php, tests/Unit/PublicTreeTest.php, tests/Support/route_probe.php):
getRouteComment() covers a published and a pending comment of a poll and of a profile, a hidden poll, a guest, the author, a moderator, closed profiles, the preview, both pages, the own block and the three writers of a poll, a profile and a Node comment
- FileAccessTest the preview address of comment; PublicTreeTest the closed folder of voting
- Documentation and plans (docs/*, UPGRADING.md):
The files plan records batch 7 and the decisions "The poll folder" and "A name in a comment"; the roadmap ticks step 13
- UPGRADING.md and PARSER.md describe the closed poll folder and the file context ['comment', $id]
Benefits:
- A file of a hidden poll or of an unpublished comment is no longer readable by anyone who knows its address
- A comment can no longer publish a file of another account's own block or of an unpublished Node material
Technical notes:
Breaking: uploads/voting/<name> answers 410; no comment of a poll or a profile carries a file on the stand, so nothing was converted
A comment on a profile of a 6.2 site with an [attach] of a file in uploads/account/ needs that file copied into uploads/profile/ (UPGRADING.md)
- No schema change
Batch 6 of docs/1-FILES-2026.md (step 12 of docs/ROADMAP-2026.md). uploads/account/ leaves the public list of the light path and holds the files of the private messages alone; the signature, the own block and the comments on a profile upload into the new closed folder uploads/profile/, and every page renders a signature in the file context of its account.
Core changes:
The private message adapter (core/stream.php, core/system.php, core/classes/access.php, core/classes/privat.php, core/user.php, core/admin.php):
account is off getUploadPublic(); getFileService() wires privat to uploads/account/ and checkPrivatFile()
- A name a message carries goes to a side that still holds it (Privat::getMessageBody())
- A moderator of account reads every message; the panel list renders a body in ['privat', $id]
checkPrivatNames() lets a writer bind an own upload or a name a message they still read carries, so a reply quotes and a forward carries its files; addPrivateMessage() refuses any other name (Privat::getAttachBodies())
- The profile owner (config/uploads.php, core/system.php, core/user.php, modules/account/*):
- A new owner profile with its rule in config/uploads.php, moderated as account through checkUploadModer()
checkProfileFile() grants the names of a signature to whoever may open the profile; checkProfileNames() refuses a foreign name in the signature and the own block on savehome
- The signature, the own block and the comments on a profile upload into profile (getCommentPlace())
getUserSign() renders a signature in ['profile', $uid] on the forum, the profile, the private message and the comments, which printed the stored source unparsed until now; getUserBlock() renders in ['profile', $uid]
- The mail texts of the panel render against the public folder all
- One set of upload helpers (core/system.php, core/user.php, core/classes/node/service.php):
- getUploadOwner() names the closed owner of an upload module for the editor window and every renderer
- checkUploadPreview() and checkUploadNames() replace the copies inside the forum adapter and serve all three owners
- FileAccess::PREVIEW gains privat and profile
The grant of a stored text no longer asks the extensions of the upload rule, Node included: the rule decides the upload, the preview and a new binding, not whether a stored name is served
- Migration (public/update.php):
setUpdateAttach() replaces setUpdateForum() and runs over the forum and the private messages
- The stand ran it once over the messages: the three addresses of brandbook.pdf (#1690, #1691, #1694) became [attach]
- Tests and probes (tests/Unit/*, tests/Support/route_probe.php):
FileAccessTest and getRouteAccount() cover both sides, a deleted side, a third account, a guest, a moderator, the previews, a forward, a foreign and a deleted name, the signature on three pages, closed profiles and the profile form
NodeServiceTest the moderator of profile; PublicTreeTest the two closed folders; UpdateAttachTest a labelled link of a private message
- Browser tooling (package.json, tools/browser-audit.mjs):
- playwright ^1.63.0; chrome-remote-interface leaves the dev dependencies
- browser-audit.mjs prefers the Chromium build pinned by the installed playwright over older revisions on disk
- Documentation and plans (docs/*, UPGRADING.md):
The files plan records batch 6 and the decisions "The profile folder", "A name in a private message", "The administrator and private files" and "The rule governs the upload"; the roadmap ticks step 12
UPGRADING.md, ARCHITECTURE.md and PARSER.md describe the closed account folder, the profile folder and the file contexts ['privat', $id] and ['profile', $uid]
Benefits:
- A file sent in a private message is no longer readable by anyone who knows its address
- Narrowing an upload rule no longer cuts off what was uploaded under it
Technical notes:
Breaking: uploads/account/<name> and uploads/profile/<name> answer 410; update.php rewrites the addresses of the private messages to [attach]
config/uploads.php gains the rule profile; a 6.2 signature, own block or mail text with an [attach] of a file in uploads/account/ needs that file copied into uploads/profile/ or uploads/all/ (UPGRADING.md)
- Until batch 7 an attachment of the own block or of a profile comment has no address; neither carries one today
- No schema change
Batch 5 of docs/1-FILES-2026.md (step 11 of docs/ROADMAP-2026.md). uploads/forum/ leaves the public list of the light path and gets its own adapter in FileAccess: a post grants the [attach] names its text carries to whom its topic page shows it, the preview form grants the visitor's own upload, and a writer can bind only a name they may carry.
Core changes:
- The forum adapter (core/stream.php, core/system.php, core/classes/access.php, core/user.php):
forum is off getUploadPublic(); getFileService() wires its folder and checkForumFile() as its grant
- The gate of the module page first, then the reader of the category while the post and its topic are published
- A moderator of the forum reads every post, a moderator of the category the published posts of any topic
- FileAccess::PREVIEW lists the owners with a preview form; the forum joins Node with name=forum
- setFileRoute() and getFileUrl() accept the preview of every owner on that list
- getEditorFileData() gives a file of the forum the preview address and inserts it as [attach] alone
- Writers of a post (core/user.php, modules/forum/index.php, lang/*):
checkForumNames() refuses a new [attach] name that is no own upload, no file for a forum moderator and no name a published post of the same topic carries; the full form and the quick edit ask it
- _FILE_FOREIGN in all six locales names the refused file
- Rendering in the file context (core/user.php, core/helpers.php, core/classes/parser.php, modules/forum/index.php):
- getForumBody() renders a post in ['forum', $id], the preview of an unsaved post in ['forum', 0]
- The parser accepts target 0 as an unsaved text; getTplPreviewContent() passes own on
- getImgText() takes a file context, so the og:image of a topic is the file route of its first post
- The signature on a forum page renders against account, the folder its editor uploads into
- Migration (public/update.php):
- setUpdateForum() turns a direct address of a file uploads/forum/ holds into [attach] of the same name
getMigrateAttach() also reads a Markdown image and drops a link label that is its own address
- The stand ran it once: 121 of 124 posts; three images inside a foreign link and one example inside [code] stay
- Tests and probes (tests/Unit/*, tests/Support/route_probe.php):
FileAccessTest and the route probe cover a closed category, a hidden topic, an unpublished reply, a guest, a member, a moderator, the preview and the three writers
- QuickEditTest the refused foreign name; UpdateAttachTest the Markdown image and the labelled link
- ParserFixturesTest the preview address of an unsaved text; PublicTreeTest and FileManagerPathTest the closed folder
- Documentation and plans (docs/*, UPGRADING.md):
The files plan records batch 5 and the decisions "A preview of the forum", "A quoted attachment" and "The signature on the forum"; the roadmap ticks step 11
- UPGRADING.md and PARSER.md describe the closed forum folder and the file context of a post
Benefits:
- A file attached in a closed category is no longer readable by anyone who knows its address
- A quote carries an attachment within its topic without opening a file of another category
Technical notes:
- Breaking: uploads/forum/<name> answers 410; update.php rewrites the addresses of posts to [attach]
- The signature keeps the public account folder until batch 6 closes it
- No schema change
Roadmap steps 7 to 10 land together: item 1 of the production findings, and batches 2, 3 and 4 of the files plan. FileAccess now owns the delivery decision of every file through one adapter per owner, the parser asks it for each address instead of building one, and go=file is the single file address of the site, Node included.
Core changes:
- One route for every file (core/classes/access.php, core/system.php, public/index.php, modules/node/index.php):
FileAccess takes one adapter per owner of the closed set FileAccess::OWNERS as two closures, folder and grant
- getFilePath() adds the shared checks: a bare name of a supported type, the file inside the folder of its owner,
a thumb only when its copy exists; every refusal is an empty answer
- getFileUrl() answers the go=file address of a closed owner, the Node preview of a type, the direct link of a
public folder
getFileService() wires node (grant through NodeService::getNodeFile(), the type of a material read from its row, nothing while a configuration journal holds the type) and public (no route)
setFileRoute() serves go=file: the query is exactly an owner with its target or the preview of a Node type, GET and HEAD only, every refusal the same 404
- op=attach and setNodeAttach() are retired; getNodeQuery() moved to the core as getStrictQuery()
- setErrorOut() treats go=file as a non-HTML answer
- Parser file context (core/classes/parser.php, core/user.php, core/classes/node/view.php, modules/search/index.php):
- filterContent() and filterDoc() take array $own (['node', $nid]) in place of int $nid; the cache key carries it
- filterAttach() asks FileAccess for the folder and every address, and makes a thumb only where the owner has a folder
- The editor file list and the preview of an unsaved resource take their address from getFileUrl()
- Inline attachment (config/filetype.php, admin/modules/uploads.php, window-body-insert.html, filemanager.js, lang/*):
[attach ... size=full] renders the template full at the own size of the image and makes no thumb, falling back to the template of its extension when full is empty
- The uploads screen edits full beside the extensions; the insert window offers Thumbnail or Full size
- Node texts off the archive (public/update.php, core/classes/node/service.php, core/classes/parser.php):
update.php moves, copies and renames no file; getMigrateAttach() turns a direct address of the own folder into [attach] with the name unchanged, and a source inside [usehtml] into the go=file address of its material
- getNodeFile() grants the names such an address carries and no longer needs a managed name for a stored material
- checkImageSource() keeps the query of a local source, so an attachment image no longer renders as index.php
- Retired addresses (core/security.php, core/stream.php, nginx.conf.example):
- A path that is no address answers 404, a missing upload 410, and /index.php/... a 301 to the clean address
- Tests and probes (tests/Unit/, tests/Support/, tools/*):
- FileAccessTest: closed owners, every address form, a carried name whatever its form, refusals of every other name
- UpdateAttachTest for the migration of 6.2 texts; ParserFixturesTest for the file context and the full-size form
- The route probes follow go=file: 20 crafted queries refused, POST 405, the retired op=attach 404
- Documentation and plans (docs/*, UPGRADING.md, README.md, admin and module help):
- NODE.md, PARSER.md, ARCHITECTURE.md describe the file route and the file context
- The files plan records batches 2 to 4 and the decision "One file address"; the roadmap ticks steps 7 to 10
- docs/4-EDITOR-2026.md, the plan of the editor work, joins the roadmap
- Design demos (public/demo/*):
The editor variants ed-plain-01..08 and ed-code-01..09 with their CodeMirror bundle, and the Node header variants nh-01..10
Benefits:
One decision and one sender for every uploaded file, so closing the forum, private messages and comments in the next batches is one adapter each
- No address of a file is spelled outside FileAccess
Technical notes:
Breaking: op=attach answers 404; the stand rewrote its 184 [usehtml] addresses to go=file, and production never carried op=attach
- Breaking: Parser::filterContent() and filterDoc() take array $own instead of int $nid as the sixth argument
- No closed owner but Node is wired yet; forum follows in batch 5
Batch 4 of docs/0-PRIVATE-DATA-2026.md (step 6 of docs/ROADMAP-2026.md). setup_old/ was the last folder of the project that carried its own .htaccess and index.html guards and kept ignored .sql files beside them; the new installer in public/setup.php replaced it, so the folder goes whole and the tree test stops exempting it.
Core changes:
- The old installer (setup_old/*):
setup_old/ is deleted with its language files, templates, images and guards
- Its ignored .sql files went with it; they stay in history before b2973ad4, the schema lives in storage/update/sql
- Tests (tests/Unit/PublicTreeTest.php):
- The check for guard files outside the document root no longer skips setup_old/
- Documentation (docs/0-PRIVATE-DATA-2026.md, docs/ROADMAP-2026.md):
- The plan records batch 4 as done; the roadmap ticks step 6
Benefits:
- No guard file is left that a server misconfiguration could rely on outside public/
- One installer remains, the one that is tested and shipped
Technical notes:
- No schema change, no address change
- nginx.conf.example, the other half of the batch, landed with step 4
Batch 3 of docs/0-PRIVATE-DATA-2026.md (step 5 of docs/ROADMAP-2026.md). A random marker check.txt lies in the project and in storage/, config/, uploads/ and admin/info; an hourly scheduler job asks each at the address it would have under the site address and judges by the body, never by the status. The panel warns when a root is open, unknown or not checked within a day, and switching a Node type on asks the same check for the upload root.
Core changes:
- The self-check (core/system.php, config/scheduler.php, .gitignore):
- getPrivateRoots() names the project and the four folders that hold the secrets
- setPrivateMark() keeps a random 32-hex marker per folder, written again under a lock when missing or damaged
checkPrivateRoots() asks every marker over getSchedulerFetch() with the transport as a seam for tests
- open: the marker is in the body, whatever the status; closed: any other answer; unknown: no answer or no http(s) site address
addSelfCheckTask() is the system job selfcheck, every hour at minute 20; it keeps the verdict of every root in its scheduler state and fails on a root that is not closed
- The markers are ignored by git, so a shipped copy cannot let a copy of the delivery pass for the site
- Panel (admin/index.php, admin/modules/security.php, admin/lang/*.php):
getSelfCheckAlert() warns on the home of the panel about an open or unknown root, with its addresses, and about a verdict older than a day; the security section adds the all-clear with the time of the last run
- _SEC_CHECK_DONE, _SEC_CHECK_NONE, _SEC_CHECK_OPEN and _SEC_CHECK_UNK in all six locales
- Node types (core/classes/node/service.php):
checkTypeGuard() asks checkPrivateRoots() for the upload root alone instead of a request to the type folder; only closed lets a type go public
- Installer and update (public/setup.php, public/update.php):
- The last part of setup.php, the one that runs with the core, writes the markers
- update.php adds the job selfcheck to a scheduler that does not carry it; two long comments are shortened
- Documentation and help (docs/*, admin/info/scheduler/ru.md, modules/node/admin/info/ru.md, .gitmessage):
- The plan records batch 3 and the project as the fifth watched root; the roadmap ticks step 5
- NODE.md, the scheduler help and the Node help describe the check behind switching a type on
- .gitmessage states one task, one commit
- Tests (tests/Unit/SelfCheckTest.php, tests/Support/check_probe.php, tests/Unit/NodeConfigTest.php, tests/Support/node_probe.php):
New SelfCheckTest: every root at its own address with its own marker, three verdicts, the body decides and not the status, a missing marker written again, the job state shown by the panel, a stale verdict counted as none
- The gate of the Node probe runs on the marker of the upload root instead of a served file of the type folder
Benefits:
- An operator learns from the panel, not from a leak, that a document root or a server alias exposes the project
- One check serves the panel, the scheduler and the switch of a Node type
Technical notes:
- New scheduler job selfcheck; existing sites gain it through update.php
- check.txt markers are written per installation and never shipped
- No schema change
Batch 2 of docs/0-PRIVATE-DATA-2026.md (step 4 of docs/ROADMAP-2026.md), with the server half of batch 4. The browser now reaches public/ and nothing else: the code, config/, storage/ and the whole uploads/ lie at the project level, so no server misconfiguration can serve a journal, a backup or a closed upload folder. Public upload addresses keep working through a light path of the front controller that answers before the core boots.
Core changes:
- The public/ tree (public/*, .htaccess, nginx.conf.example):
- templates/, plugins/, sound/, demo/, .htaccess, favicon.ico, robots.txt, error.html and sitemap.xml move into public/
index.php, setup.php and update.php move into public/ whole and define BASE_DIR and PUBLIC_DIR themselves; public/admin.php stays the short file that runs admin/index.php
public/.htaccess drops RewriteBase and refuses the PHP and the markup of the themes; a project-level .htaccess rewrites every request into public/ and refuses everything without mod_rewrite
- nginx.conf.example: root on public/, location ^~ /uploads/ to index.php, the theme refusals and the error pages
- One upload root and the light path (core/stream.php, core/system.php, core/classes/*):
core/stream.php: UPLOADS_DIR, getUploadPublic(), getUploadUrl(), getUploadRequest(), setUploadStream() and getFileStream()
- The light path serves a public folder with nosniff, a sandbox policy, a day of public cache, ETag, 304 and ranges
- Every other folder, a dot segment, a traversal or a missing file answers 404 without loading the core
- getFileStream() takes a cache mode (none, private, public) and sends the sandbox policy on every answer
No caller spells 'uploads/' into an address: the parser, getImgText(), the upload rules, avatars, presentation and the file layer ask getUploadUrl(); a private folder has no direct address
- The upload service creates a missing folder of its owner on the first write; a missing folder is no refusal of a rule
- addEditorUpload() builds the file context after the write, so the first upload into a new folder reports its file
- Guards and paths (core/, admin/, modules/, blocks/):
Every .htaccess and guard index.html outside public/ goes, with their writers in FileManager, NodeService, CaptchaStore and update.php; the counters create storage/counter themselves
Every path that relied on the working directory reads BASE_DIR or PUBLIC_DIR: language files, module scans, theme assets, logos, ranks, flags, the sitemap, the file scan, the template editor and the .htaccess and robots.txt editor
- The critical files of the file manager follow PUBLIC_DIR, so a document root named public_html keeps them
The installer records the mode of the document root as webroot in config/global.php and shows it as a check row; the avatar folder ships as avatars below uploads/ with its own settings label
- Documentation and help (README.md, UPGRADING.md, docs/, admin/info/, modules/*/admin/info/ru.md):
README and the help of the security section explain both modes, Apache, LiteSpeed, nginx and three variants for shared hosting; UPGRADING.md and the help point at nginx.conf.example
- The plans record the decisions of the batch, the real entries in public/ and the writer that creates a folder
- Tests and tools (tests/, tools/):
New PublicTreeTest: the light path over real HTTP without the core, nothing of the project on public/, both .htaccess files, getUploadUrl() and no guard outside public/
New NginxConfigTest: nginx.conf.example refuses exactly what public/.htaccess refuses, file by file, and passes nginx -t where a binary is set in SLAED_NGINX
- The probe servers serve public/ and run the real light path; the probes write no guards
- ui-contract, ui-audit and the pre-commit hook watch public/templates and public/plugins
Benefits:
- No server rule is needed to keep config/, storage/ or a closed upload folder private
- One sender and one list decide every upload address and its delivery; closing an owner is one line of the list
- nginx gets a tested server block instead of rules copied from the documentation
Technical notes:
Breaking change: the document root of a site must point at public/, or the project-level .htaccess must rewrite into it on Apache or LiteSpeed; a server without .htaccess support needs nginx.conf.example
- Upload addresses uploads/<folder>/<name> of public folders do not change; files of Node types have no direct address
NodeProfileTest fails 11 of 14 on the stand as on the clean HEAD before this change; the installer of the probe does not reach the database there
Batches 0 and 1 of docs/1-FILES-2026.md. Node types no longer share the upload root with the modules: they live below one root of their own, so a type can only meet another type there, and every builder of an upload path asks one function instead of spelling 'uploads/'.$mod. The plan carries the owner's decisions and the inventory of the stand database that the later batches rely on.
Core changes:
- One root for the types (core/system.php, core/classes/parser.php, modules/node/index.php, update.php):
NODE_DIR is UPLOADS_DIR.'/node'; getUploadFolder($mod, $node) answers node/<type> for a type, the own name for a module
- The flag names a type the registry does not carry yet or no longer, as type operations and update.php do
- getUploadRuleData(), getUploadPlaceRule() (store), getImgText(), Parser::filterAttach() and getNodeAssetPath() ask it
- Node service and locks (core/classes/node/service.php, core/classes/filemanager.php):
setTypeRoot(), checkTypeGuard(), setTypeWrite(), updateNodeTypeStatus(), deleteNodeType(), checkNodeFiles(), setNodeWrite() and getNodeFile() build the type folder through getUploadFolder()
- checkNewName() drops its scan of the upload root; a module name and a key of config/uploads.php stay refused
- FileManager::getPathLock() takes uploads/node/<type> as the root of an upload area, so two types never wait for each other
- Administration (core/admin.php, admin/modules/uploads.php):
- getAdminUploadRule() reads the type out of a path below uploads/node and hands the owner on as mod
getUploadsFolders() lists the folders of the upload root with the type folders below uploads/node
- The default folder setting keeps an owner name, its label shows the path
- The tree and the documents (uploads/, UPGRADING.md, docs/, modules/node/admin/info/ru.md, admin/info/config/ru.md):
- uploads/{content,docs,faq,files,help,links,news} move to uploads/node/, which carries index.html and .htaccess
- UPGRADING.md gives the operator the folder move; NODE.md and the help name uploads/node/<type>
- docs/1-FILES-2026.md records the decisions of batches 0 and 1 and the inventory; docs/ROADMAP-2026.md ticks steps 2 and 3
- Tests and tools (tests/*, tools/node-profile.php, tools/upload-route-check.php):
- The probe routers follow the nginx rule below a folder of the upload root instead of one level only
- Route, node, install, upload and image probes build their type folders below uploads/node
- New: the folder of a type through every rule builder, two types as two lock areas, names a folder of the root carries
Benefits:
- A type can no longer collide with a module folder, and the type check no longer scans the upload root
- The place of the types is decided once, which batch 2 of 0-PRIVATE-DATA-2026.md needs to take uploads/node out of public/
Technical notes:
- Breaking: a site whose type folders sit at uploads/<type>/ moves each to uploads/node/<type>/ before the new files serve
- No public address changes: texts reach type files through op=attach and op=asset by name, no stored row carries the folder
- NodeProfileTest fails 11 of 14 as on a clean HEAD (the installer copy never gets config/db.php); not caused by this change