Официальный сайт SLAED CMS
Журнал изменений
Stages 4 through 7 of docs/FILE-MANAGER-CONCEPT-2026.md close together: the system area can be edited and rewritten under one lock protocol, the upload catalogue and the editor window are both drawn from the same descriptor, and every changing action of either screen travels by POST with a token and leaves an entry in the journal behind it.
Core changes:
- Source editor of the system area (core/classes/filemanager.php, admin/modules/uploads.php):
getFileBody() and setFileBody() read and replace the body of one text file
- the version is the digest of the bytes on disk and is compared inside the directory lock
- a mismatch answers 409, writes nothing and hands the fresh version back to the author
- the line separator is taken from the file, so a save never rewrites every line of it
- fmedit() and fmsave() put the editor in the place of the list while the tree stays
- the guard of an unsaved edit reaches htmx swaps, links and beforeunload alike
- Writing operations of the system area (core/classes/filemanager.php, admin/modules/uploads.php):
addFileEntry(), addDirectory(), updateFileName(), addFileCopy(), updateFilePath(), deleteFileEntry(), addFileArchive() and addFilesArchive()
- both sides of a compound path are asked of the policy separately
- two directories are locked in sorted order, so two writers never wait on each other
- a name already taken is refused inside the lock, where the answer is still current
- one form of the browser carries the argument of a rename, a copy and a move
- Catalogue of the administration (core/admin.php, admin/modules/uploads.php, templates/admin/*):
the whole upload tree is browsed with the rights of an administrator: tree, crumbs, rows and tiles, properties, fan, marks, bulk bar, gallery, upload queue and pager
- deletion and archiving left their GET links for POST routes of the module
- the server path moved out of the heading of the section into a property of the object
- Window of the editor (plugins/editors/toastui/, templates/lite|admin/partials/):
- editorFiles answers the descriptor of the file layer together with capabilities and quota
- getEditorRouteRule() asks the three questions every route of go=4 asks
- editorDelete and editorArchive hand a module moderator the two operations section 14 named
the window follows the approved mock-up: a rail of insertion ways, sections that state the rights they were given, compact and expanded catalogue, filter, fan, marks, gallery and an upload queue, with the five states of the process each carrying its own words
- Language constants (lang/{locale}.php, admin/lang/{locale}.php):
- EDITOR for the window and UPLOADS for the catalogue, in all six locales
- _RETRY becomes a global constant and its scoped copy is dropped
Benefits:
- one file layer answers three screens, so a path, a name and a lock are decided in one place
- a permission is read off the descriptor of the object instead of a role derived in the client
- every changing action is a POST with a token and an entry in storage/logs/error_file.log
- the two themes carry the same components under the same names
Technical notes:
new routes: index.php go=4 editorDelete and editorArchive; the contract of editorUpload and editorFiles is otherwise unchanged and their token now travels in the body
the JSON of the editor never carries an absolute server path; only the administrative context receives realpath, and it is shown in the properties of the selected object
getEditorImageData() is gone together with the row it used to build; the one list of embeddable types is read from Parser::EMBEDIMG by the editor driver
- _UPLOADS_RETRY is removed in favour of the global _RETRY
- no schema change, no configuration change, backward compatible
Stages 1, 2a, 2b and 3 of docs/FILE-MANAGER-CONCEPT-2026.md: FileManager reads one filesystem area below a server-chosen root, the managed name format and the lock protocol leave Upload for it, and a System files tab browses BASE_DIR and writes nothing.
Core changes:
- File layer (core/classes/filemanager.php):
Three contexts over one root the server chooses: uploads, editor, system
- Canonicalization refuses traversal, absolute paths, stream wrappers and symlink escape
- One policy method answers list, read, download, write and delete for every path
- The descriptor carries the absolute path in administrative contexts and never in the editor
The managed name format lives here alone
- SALTLEN, checkFileName() and getFileOwner() replace the two former regular expressions
getPathLock() and deletePathLock() are the one lock protocol of the project
- The key is the directory, because the upload service draws a free name below it
- Upload service (core/classes/upload.php, core/system.php):
- Upload requires the file layer and takes the shared lock when it publishes and when it deletes
- The name pattern and the salt length come from FileManager, and the constructor drops its lock argument
- getEditorFileJson() reads the owner through FileManager::getFileOwner()
- System files tab (admin/modules/uploads.php, core/admin.php, index.php):
- sysfiles() draws the browser: tree along the open path, crumbs, list, tiles, properties
Four go=5 routes: getAdminFileList, getAdminFileData, getAdminFilePreview, getAdminFileDownload
- Every one of them asks isAdmin(true) and checkFileAccess() again before it answers
- A download is always application/octet-stream, because a guessed type executes in the admin origin
- An image preview takes its type from a closed table and answers under a sandbox policy
- The tab carries no upload directory selector and no writing action
- Templates and theme (templates/admin/*):
- New partials file-browser, file-browser-tree, file-browser-list
- New fragments file-browser-row, file-browser-tile, file-browser-props
- The theme gets the three columns, the tree, the tiles, the properties panel and the empty states
- admin-ui.js holds the view, the current object, the way back and the filter of the open directory
- Language constants (admin/lang/, lang/):
- 29 UPLOADS* names in six locales, plus _FRESET and _DOWNLOAD in the site scope
- Mockups, tests and plan (demo/, tests/Unit/, docs/FILE-MANAGER-CONCEPT-2026.md):
- Both mockups show the states of the process: queue, skeleton, empty, failed, busy and rename
- FileManagerPathTest covers the path list of the plan, the policy rows and the capability tables
- FileManagerLockTest proves that two writers serialize on one key through one protocol
- The plan marks stages 1, 2a, 2b and 3 closed and names the routes that exist
Benefits:
- One filesystem layer serves the administration, the editor window and the upload service
- Path traversal, symlink escape and closed paths are refused in one place instead of per route
- The system area is readable through the browser without a link that would execute a file
- Two writers of the same directory wait for each other, because there is only one lock protocol
The file manager concept becomes an implementation plan: every stage carries its own reading map, acceptance rule and stop condition, so one session opens the document, reads its stage and works. The mockups both screens are built from are committed beside it, and the theme gaps found while building them are closed in the themes rather than patched in the mockups.
Core changes:
- Implementation plan (docs/FILE-MANAGER-CONCEPT-2026.md):
The document turns from a concept into a plan of ten stages
- Every stage names why it comes now, what to do, how to verify, what to
commit and when to stop and ask
- Every stage carries a reading map: the plan sections, the source files with
line numbers and the tests it needs, so a session reads about 500 lines
instead of the whole document
- Stage 2 is split: 2a builds the class, 2b moves the managed name and the
lock out of Upload and migrates both callers
The FileManager contract is renamed to the project rule
- getFileList, getFileData, getFileBody, setFileBody, addFileEntry,
addDirectory, deleteFileEntry, updateFileName, addFileCopy, updateFilePath,
addFileArchive, getFileOwner, checkFileAccess, isFileEditable
- list, stat, read and write are refused as names: none of them passes the
verb plus noun rule and half are taken by PHP itself
Three mechanisms are described once and shared by both screens: the sl-dial action fan, the sl-modal preview gallery, and the checkbox selection with its bulk bar
The save protocol takes the lock around the whole compare and replace, because a version check beside the lock protects nothing
The path policy answers per operation instead of hiding entries from a listing, and rename, copy and move check both sides of the path
System files gain their own download route: a direct link would let the server execute index.php instead of handing it over
Language constants go to UPLOADS and EDITOR, each matching its own configuration namespace
- Editor uploads plan (docs/EDITOR-UPLOADS-2026.md):
The plan is closed: the editor mode of tools/upload-route-check.php was walked against a live stand with every row passing
- The access matrix of four settings combinations against three roles
- The guest isolation of two sessions
- The write guard over real HTTP
The walk needs two accounts and not four: is_moder() reads the admin session only, so one account answers as moderator in one cookie jar and as a member in the other
- Admin theme (templates/admin/assets/css/theme.css):
sl-but-mini drops the browser chrome that drew a second border over its round fill, as the lite theme already did
sl-is-muted, the disabled state, sl-bulk-bar and sl-bulk-count arrive under the names they already carry in lite
- sl-modal-wide and sl-modal-image arrive so a gallery has a frame to open in
- A selected table row is now distinguishable from a hovered one
- Lite theme (templates/lite/assets/css/theme.css):
- sl-bulk-count joins sl-bulk-bar under the name the admin theme uses
- Approved mockups (demo/filemanager-*.html):
filemanager-v2-rail.html is the editor window: a side rail of three insert ways, the module storage, four permission scenarios, the action fan, the gallery and multiple selection
filemanager-admin-2026.html is the admin full mode: file catalogue and system files on one shell, built from existing theme components
- Eight earlier variants are kept as the record of why the rail was chosen
Benefits:
- A session can work on one stage without reading the whole plan
The mockups are a working reference, not a picture: they use production classes, so moving them into code is mechanical
The two themes now carry the same component set under the same names, which removes the need to redefine anything locally
Technical notes:
- No PHP was touched; the file manager itself is not implemented yet
The theme additions are new rules only, no existing selector changed its meaning, verified against the live admin and front pages
- The next stage is 1: the loading and error states in the mockups
The content editor no longer keeps rules of its own beside the module upload settings. Access is answered by one function, the width of the target column decides whether a field may carry an embedded image, and one guard holds every write to the room its column has instead of letting the database answer with a lost post. Batches 1 to 6 of docs/EDITOR-UPLOADS-2026.md land together.
Core changes:
- Settings and ownership (core/system.php, core/classes/upload.php, admin/modules/uploads.php):
The module rule gains the key guestfiles, appended so a stored rule keeps working
- A rule written before this release normalises on its first write and keeps every value it carried
- The new key falls back to userfiles when its position is absent, because zero means no limit
A guest owns the own uploads by a token derived from the session, never by the shared zero
- The owner segment of a stored name widened from an integer to alphanumerics
- The listing compares it as a string, so one guest can no longer match another
The guest guard on the listing route is gone; checkEditorUploadAccess() alone decides
- The one role rule left is its first line, a moderator of the module
- Which of the three listing limits applies is role, what each is worth is settings
- The storage contract (core/helpers.php, setup/sql/*.sql, 18 modules):
- Every one of the 54 editor call sites declares where its text is stored
getEditorRoomData() turns that name into a column type, and the type into bytes
- Seventeen body columns are MEDIUMTEXT, so an embed at the cap fits
- Summary columns and signatures stay TEXT, which is how a field says it holds no data URI
- auto_links.intro and users.sig leave VARCHAR, where a rich editor never belonged
checkEditorTextRoom() refuses before the query and answers a ready message
- Length is measured in bytes, because bytes are what a column bounds
- Embedded weight and media type are measured beside it
- The content feed writer keeps the stored body and logs the reason instead of failing
- One window and one bounded embed path (plugins/editors/toastui/, templates//partials/*):
The image dialog and the file catalogue became one window behind one icon
- setImageChrome() and setPopupChrome() are gone with the popup they measured
- The icon is there for every visitor, because the address field inside it always is
The blob hook is registered unconditionally
- A denied upload no longer falls through to the vendor default, which embedded with no bound at all
- Picked, dropped and pasted files reach one guarded path that checks room, type and size
- The cap and the list of embeddable types reach the editor from PHP
- The proof (tests/Unit/EditorRoomTest.php, tests/Support/editor_probe.php, tools/upload-route-check.php):
The room table is held against setup/sql/table.sql in both directions
- No store may resolve to a column that is neither TEXT nor MEDIUMTEXT
- Every call site names a storage the table carries
- The write guard runs against the real core in an isolated process
The tool gained an editor mode for what only exists as a request handler
- The access matrix, the guest isolation and the write guard over real HTTP
Benefits:
- A setting the administrator changes is the setting the editor obeys
- A refusal reaches the author at the moment they act instead of as ERROR 1406
- One definition each for the cap, the embeddable types and the room a field has
- A wrong column name fails a test rather than costing an author a post
Technical notes:
- Storage: seventeen columns widened to MEDIUMTEXT, auto_links.intro and users.sig to TEXT, in all three schema paths
- Config: one key appended to the upload rule; no migration needed and no value reordered
- Files stored under the old name pattern keep resolving; no migration of stored files
- The editor mode of tools/upload-route-check.php needs a stand and four credentials and is not run by the suite
The parser decides escaping from a trust boundary instead of a stored flag, comments and private messages gain their own runtime, and every column an author writes into is wide enough for what the editor is allowed to produce. Three schema paths — fresh install, 6.2 upgrade and the deployed patch — now agree column for column and index for index.
Core changes:
- Trust boundary and tag policy (core/security.php, core/classes/parser.php):
filterTrustedTags() becomes the single authority over [usehtml] and [usephp]
- both tags are reserved for the super administrator and stripped for everyone else
- filterText() and filterHtml() pass the capability, comments and messages never do
the pipeline runs filterCode() before filterBbBlocks(), so a documented example is quoted rather than executed
- a breaks format renders a single newline as a line break without touching authored text
the persistent parse cache moves from the news module into the parser and keys on the configuration hash
- Comment runtime (core/classes/comment.php, core/user.php):
format, iphash and the materialised path are gone; branches resolve through recursive CTEs over pid, and tombstones survive by an upward keep walk
reqkey is BINARY(16) under a unique index, so a repeated submit returns the first row instead of writing a second
- the flood window reads (ip, time, id) and the edit mark reaches the rendered view
- Private messages (core/classes/privat.php):
four independent states replace one overloaded status column, with a single owner per side and no shared flag between inbox and outbox
- Room for embedded images (setup/sql/*.sql):
fifteen columns that hold an authored text widen to MEDIUMTEXT
- EMBEDMAX allows 65536 bytes of binary, which is 87384 characters of base64 before the
data URI prefix, while TEXT holds 65535
- with STRICT_TRANS_TABLES the overflow is a lost post, not a lost image
the summary columns stay TEXT on purpose, because a list query draws twenty of them onto one page and an image referenced by address still fits there in a few dozen characters
users.sig moves from VARCHAR(255) to TEXT: of 828 stored signatures the longest is exactly 255 and 95 sit above 240
- Deployment and tooling (setup/sql/update6_3_patch.sql, setup/sql/update6_3_text_once.sql):
one idempotent patch carries a deployed 6.3 to full parity, including the mail queue and the campaign columns it never received
a one-time text repair normalises stored break markup and entities in comment and message bodies, ordered so that authored text is never mistaken for writer output
the address policy, the upload rule reader and getImgText() stop guessing the module and take it explicitly
Benefits:
an author capability decides what may be written, and the escaping policy decides how it is rendered; neither can be reached through the other
- a repeated submit, a lost connection or a double click can no longer duplicate a comment
- a branch and its tombstones are answered by one query instead of a maintained path column
- an embedded image can no longer end a post with ERROR 1406
- fresh install, upgrade and patch produce the same 38 tables, 494 columns and 172 indexes
Technical notes:
- reqkey is stored as raw bytes; the query interpolator renders non-UTF-8 values as 0x hex
comment.body, privat.body and thirteen further columns are MEDIUMTEXT; intro columns, users.block and order.info stay TEXT
- update6_3_patch.sql and update6_3_text_once.sql are both idempotent and safe to re-run
deployment order: files, update6_3_patch.sql, update6_3_text_once.sql, then clear storage/cache/pages
- no breaking change for stored content: every existing body renders unchanged
Stage 2 of docs/PRIVAT-2026.md: private messages now store the source the author wrote and render it safely by format, replacing the write-time filterHtml() encoding and the admin-side getDecodedText() compensation. Closes steps 11 and 12 of the plan.
Core changes:
- Safe rendering onto the format contract (core/classes/privat.php):
- format joins the field list and every row a read answers
addMessage() normalizes both fields and resolves the format once before the first attempt, so a retry rewrites nothing
- getBodyFormat() refuses an html editor and keeps the body as markdown source
- filterMessageText() keeps the trusted-html tokens, autolinker and censor,
drops only the write-time escape
- addMessageRow() stores format with the row
- Frontend and admin adapters (core/user.php, core/admin.php):
- detail view renders filterContent($body, true, ..., $view['format'])
- the send hands both fields over as submitted; the compose form refills unescaped
- getDecodedText() removed; the admin popover renders the same safe call
- Migration parity fix (setup/sql/table_update6_3.sql):
the bare MODIFY on _privat id/title/body becomes modcol(), so a table already on the final definition is not rewritten
- Documentation:
docs/VERSIONS.md: stage 2 entry with the deployment sequence, the mandatory tool run, the gate, and the maintenance-window requirement
- docs/PRIVAT-2026.md, docs/PARSER.md: stage 2 recorded
docs/CONTENT-CONTRACT-2026.md: handover note carrying the "without format" decision and a separately-tracked unauthenticated preview RCE finding
- Tests (tests/Support/privat_class_probe.php, tests/Unit/PrivatClassTest.php):
what a send stores per editor format, what the renderer answers, what the template does to the title, and what the writer still filters
Benefits:
No stored private-message value is trusted HTML; both frontend and admin render through Parser::filterContent() with safe = true
- The title is plain source escaped at the template boundary
- The upgrade no longer rebuilds a table that already carries the final schema
Technical notes:
Storage is source; format names plain or markdown; the '' state exists only inside the stage 2 maintenance window and the gate clears it before code is live
- tools/privat-migrate.php is mandatory and needs a closed site and a rehearsed restore
Verified: composer analyse, composer test (698 tests), php-cs-fixer --dry-run, 47 Privat tests, and a 48-check authenticated HTTPS route matrix, all passing
Steps 9 and 10 of docs/PRIVAT-2026.md still recorded themselves as uncommitted after cfa91cb8 shipped them. The progress table is what a session reads to decide where the work stands, so a stale evidence line there is a wrong answer to the next question asked of it.
Core changes:
- Progress table (docs/PRIVAT-2026.md):
rows 9 and 10 open with the commit that carries them instead of "Not committed"
- row 9 is the format column in all three update channels
- row 10 is tools/privat-migrate.php with its probe and its test
Benefits:
the plan reads the same on every machine that pulls it, which is what makes the migration resumable across sessions
Technical notes:
- documentation only, no code, schema or behavior touched
Stage 2 of docs/PRIVAT-2026.md opens: the message table gains the format column in all three update channels, and tools/privat-migrate.php is what computes the verdict per body and rewrites the stored source. No runtime code reads format yet, so this release changes nothing a visitor sees.
Core changes:
Format column in all three update channels (setup/sql/table.sql, setup/sql/table_update6_3.sql, setup/sql/update6_3_patch.sql):
format VARCHAR(20) NOT NULL DEFAULT '' as the last column of _privat
- the 6.2 channel appends it in Batch K, after the three state columns and
before the backfill, so an upgraded table keeps the column order of a fresh
one
- the patch channel gets its own section 6, because an installation that
already applied section 5 never re-runs it
the column ships empty on purpose and no statement backfills it: the verdict is per body, not per column
- Migration tool (tools/privat-migrate.php):
five modes and no sixth: report, classify, convert, title, sample
- classify writes format and the ledger storage/migrate/privat-format.json
and rewrites nothing, so the verdict is reviewed before any message changes
- convert and title are two independent ledger-driven passes that store the
value they replaced and rewrite nothing twice
- --db and --prefix run the whole thing against a restored copy first
the body classifier is the one tools/comment-migrate.php carries, because the writer is literally the same filterHtml()
the title has no column to keep its verdict in, so the ledger keeps it: a title was written by the html branch when its own row was
classify refuses a ledger whose rows a pass has already rewritten, counting the rows rather than reading a completion stamp, because an interrupted pass leaves finished rows behind and stamps nothing
Migration and tool tests (tests/Support/privat_probe.php, tests/Unit/PrivatMigrationTest.php, tests/Support/privat_format_probe.php, tests/Unit/PrivatFormatTest.php):
state E joins the probe: the table stage 1 shipped, with only format missing, which is the only shape the patch channel will ever meet in the field
the format probe boots the real core, so a fixture that names an editor is encoded by the writer an installation really runs
10 fixture messages cover both entity maps, both break signatures, the ampersand-last rule and an interrupted pass
Benefits:
the irreversible half of the content contract is rehearsable: dump, restore, classify, read the report, and only then convert
- an interrupted maintenance window resumes instead of reversing a body twice
- all three channels still end on one byte-identical table definition
Technical notes:
storage: format is '' only inside the stage 2 maintenance window, between the schema section and the gate; the gate is what makes it a state the release cannot end in
the tag map runs on the legacy branch alone, because the plain entity map turns an authored <a> back into a real tag that must stay text
- backward compatibility: unchanged, no runtime path reads or writes format yet
Stage 1 of docs/PRIVAT-2026.md. The procedural private-message subsystem becomes one Privat class, and the single status column both participants shared becomes four independent state columns, so what one of them saves or deletes no longer rewrites what the other one sees. Every frontend, block, profile and admin caller moves in the same change; no compatibility wrapper, schema detection or dual schema fallback is left behind.
Core changes:
- Storage model in all three update channels (setup/sql/):
status becomes viewed, and saved, delin and delout join it as own columns
- the backfill reads status through the new runifcol() guard before rencol()
consumes it, so the step disappears by itself once the rename has happened
- modcol() forces viewed onto TINYINT UNSIGNED NOT NULL DEFAULT 0, which a bare
rename cannot do: it keeps the BOOLEAN a column storing 2 was declared as
the single-column keys uidin, uidout and status give way to five composites
- in_box, in_new and out_box serve the mailboxes, their counts and their quotas
- out_new and flood were added on measured plans, not on expectation: the
outgoing unread badge was a full table scan and the send interval a filesort
update6_3_patch.sql gains section 5, so an installation already running 6.3 is not left behind by a release whose runtime code reads the new columns
- the stale addidx on status is deleted: the same file created and then dropped it
- The subsystem (core/classes/privat.php, core/system.php):
final class Privat owns every read and write of the table, built beside Comment
- every mailbox predicate is written once, so a list, its counter and its quota
can never disagree, and no caller restates a state column
- every mutation is transactional and authorizes itself through the detail
predicate of the side that asked; one foreign id refuses the whole batch
addMessage() answers ['id' => int, 'error' => string] from a closed code set
- the id is getSqlLastId(), not the newest row of the table, which under a
concurrent send put a foreign message id in the notification link
- both accounts are locked in one statement ascending by id, and the interval
and both quotas are read behind that lock
- the recipient is resolved before the transaction opens, because the first
plain read of a transaction fixes the snapshot every later one answers from
- a deadlock or a lock timeout is retried once, and only for a send that owns
its transaction
Mailboxes and routes (core/user.php, index.php, modules/account/index.php, blocks/user_info.php):
reads are GET routes that carry no token, mutations are POST with the token in the body; save and delete no longer travel as credentials in an address
- opening a message is what marks it read, so that one is POST and CSRF checked
one mutation route serves a row action and a bulk action alike; inbox, saved and outbox gained read, unread, save and delete over a selection
- only the opening mailbox is rendered, the other tabs are fetched on click
the detail view resolves the counterpart, so the outbox shows the sender the recipient's profile instead of their own
- the session counter cache is gone: both sidebar badges are counted per render
Administrator panel and account deletion (core/admin.php, admin/modules/ privat.php, admin/index.php, modules/account/admin/index.php, core/system.php):
the admin list reads the class, pages from the same numbers its rows came from and renders the five derived states as labelled chips
the duplicate go=5 delete route is deleted rather than re-secured; one POST handler remains behind checkAdminPost('privat') and the existing isAdmin(true)
both paths that delete a user row now clean the mailboxes in the same transaction, and the counterpart keeps a readable copy of an anonymous account
- the private-message branch of ad_status() is gone with the column it rendered
- Templates and language (templates/lite/, lang/):
table, table-row, content-list, block-content and inline-badge gained the check column, the bulk slot and the state flags; theme.css owns their styling
- six new PRIVAT constants in all six locales, none of them a scoped duplicate
- Verification (tests/):
PrivatMigrationTest drives both upgrade channels through states A to D in a disposable schema and holds every one of them to the fresh definition
PrivatClassTest holds the predicates, the limits, the independence of the two sides, the bounded bulk actions and two real concurrent sends
- SchemaUpdateValidationTest reads modcol declarations and both channels
Benefits:
- one owner of the table, one place a mailbox rule can be wrong
sender and recipient states are independent, which is what the old model could not express at all
- concurrent sends cannot both take the last free place of a mailbox
fresh install, 6.2 upgrade and 6.3 patch end on the same table, and each is safe to run twice
Technical notes:
the schema section and the runtime code of this release are deployed together: either half alone answers an SQL error on every private-message page
behavior changes, each intended: a sender may delete a message already read, a saved message stays in the sender's outbox, one participant's delete leaves the other's copy, read and unread are two actions, and a send is refused when the recipient's saved folder is full and not only their inbox
the notification preference is psmail, the setting the profile form has always offered and which had no effect while the code read the forum preference fsmail
- docs/VERSIONS.md carries the dated entry with the channel table
- docs/PRIVAT-2026.md stays: stage 2, the content contract, is not started
- stored titles and bodies keep their current encoding contract until then
Six defects found by re-verifying the finished upload migration, plus the consolidation they exposed: three copies of the SQL statement splitter became one, and the copy the module installer used was corrupting every script it ran.
Core changes:
- Remote address policy (core/classes/upload.php):
IPv6 became an allowlist of the prefixes IANA delegated to a regional registry
- 2000::/3 is far from fully delegated, so reserved space is refused by absence
- the deny-list let fec0::/10, 3ffe::/16 and everything outside 2000::/3 through
a refusal answers its own code and names the address in error_file.log
- a list behind the registry no longer looks like a host that is simply down
- Admin files module (modules/files/admin/index.php):
a delete with no valid token no longer removes the file and its rows
- save() dispatched posttype=delete outside its own token guard
- delete() disabled its check whenever a row id was given
- every handler authorizes through checkAdminPost('files') and the dial posts
- an update matching no row counts as a failed write and compensates the file
- SQL statement splitting (core/admin.php, admin/modules/*, setup/index.php):
getSqlbatch(), getSqlclean() and getSqlinfo() moved to core/admin.php
- the Inquiry tab, the module installer and setup/index.php share one splitter
- setup/index.php loads it by name and the guard of that file admits SETUP_FILE
stripslashes() over SQL is gone from both places that ran it
- it turned an escaped empty default into an unparsable column definition
the module installer fills every placeholder, not only {prefix}
- a table.sql declaring {engine} and {collate} could never install
- a comment in front of a statement is no longer part of the statement
- Smaller fixes (core/classes/parser.php, modules/files/index.php):
- a thumbnail that could not be produced renders the full size image
- a guest submitting a file binds 0 into the uid column instead of an empty string
- Verification (tests/, tools/, admin/info/):
- tools/ipv6-registry-check.php compares the shipped prefixes with the IANA CSV
- DatabaseBatchTest holds the splitter, the placeholders and the installer contract
- the uploads help page describes current behaviour and carries no changelog
Benefits:
- one address policy, one splitter, one place to correct each of them
- a refused address and a refused script are both diagnosable from the log
- the module installer and the system installer run scripts the same way
Technical notes:
behavior changes: the files admin panel is POST-only, a remote address outside a delegated prefix is refused, and the install report names statement types
- a prefix IANA delegates after this release is refused until it joins ALLOWSIX
- docs/UPLOAD-2026.md is removed: the migration it planned is finished