Skip to content

CoreX v0.46.0

Choose a tag to compare

@MustafaShaaban MustafaShaaban released this 13 Oct 23:41
· 39 commits to main since this release
17cb68a

Everything merged since v0.45.2: two days of fixes found by using CoreX on the first client sites, the rest of the reply email's sender, and a boot that asks the database less.

A site with a form that has a file field should take this release. A form's file field is answered by the file the request carried and by nothing in its text; see Security, and the first entries of Client impact for what a site looks at in what it has stored.

Replies. A site names who its replies are from. A reply is kept as a draft, starts with a subject, is sent in the conversation the person already has or as a new email, and can be sent again when it did not go. An email is drawn, and worded, in the site's language, whoever's request sends it.

The account block. A forgotten password is reset on the account page. The block says what happened under the button that was pressed, says so when a request gets no answer, names the session in use, and its fields tell the browser what they are for.

Forms and the inbox. A form built in Forms & Flows is guarded by its trap field and the site's challenge. A form defined in code raises a notification. What a visitor writes is kept with its backslashes. On a site without pretty permalinks the inbox and seven other admin requests are answered.

Speed. CoreX's boot asks the database for nothing it does not need on a front-end request: a setting with no override costs no query, three options read on every request are stored loaded, and the schema check takes its lock only when a site is behind.

Rebuild plugins/corex-config after merging: the admin bundle changed. Read Client impact before updating a site that has code of its own against CoreX's classes or routes; it is long because the list of small changes is long, and most entries end "nothing to do".

Added

  • A site names who its replies are from (spec 106, slice 5). Three settings under CoreX → Settings → Mail: Reply sender name, Reply sender address and Replies are answered to. A reply from the Submissions inbox, and one sent again, leaves from them; left blank, each falls back to the site's general sender as before. The Mail settings warn when the reply address is on another domain than the site's, as they do for the general one (DECISIONS #357).
  • The page that shows a new preview link has a button that copies it (spec 101, FR-011a; the owner's ask). The link is in a field of its own, labelled, with Copy link beside it: one press puts the whole link on the clipboard and the page says "Copied.", to a screen reader as well. With scripts off there is no button and the link is copied from the field by hand. On a site not served over HTTPS, where a browser gives a page no clipboard, the button copies by selecting the field. The link is still shown once and kept nowhere (DECISIONS #327).
  • A reply that was begun and not sent is kept as a draft (spec 106, slice 3). It is written down when writing pauses, and is in the box again the next time that person opens that submission, on any machine; nobody else is shown it. Sending the reply ends it; Discard draft removes it; a reply that could not be sent stays a draft. It is kept with the submission, so it goes to the trash with it and is removed when the submission is deleted or anonymized (DECISIONS #320).
  • A reply that did not go can be sent again (spec 106, slice 5). A reply listed as "Not sent" has a Send again button, for anybody who may answer the submission. It sends the reply as it was written and adds what happened to that reply, which then says how many times sending it was tried. Two people pressing at once send it once: the reply is written down as being sent before the email is handed over (DECISIONS #328).
  • A reply starts with a subject (spec 106, slice 9): "Re: " and the name of the form the person filled in, to keep or to change. A form defined in code is named as the inbox's own filter names it. The subject started empty and had to be written each time. A subject nobody wrote is not kept as a draft, and the box goes back to it when a reply has gone (DECISIONS #323).
  • A reply is sent in the conversation the person already has, or as a new email (spec 106, slice 10). Where CoreX has a record of an email it sent the person for that submission (the confirmation, or an earlier reply), the reply box offers both, with the conversation chosen and the subject that answers it. The reply then names the email it answers, which is what a mail program reads to put it under that one. Each reply is listed with the way it went, and the preview says which it will be. Where there is no earlier email on record the reply is a new email, and the box says so. A draft keeps the way it was to be sent. Emails sent before this release cannot be continued: they left under an id nothing kept (DECISIONS #325).
  • An email CoreX's mail engine sends is sent under an id CoreX chose, made of the site's host, so that a later email can say it answers it: an email Email Studio sends, and one a site sends through Corex\Mail\Mailer. Not yet checked through a sending service that may write its own id, such as the Gmail connection of an SMTP plugin: there a reply would arrive and stand alone.

Changed

  • An admin page, an admin-ajax.php call and a cron run no longer write to the options table to check the schema. The check that brings a site's CoreX tables up to date took its lock before it knew whether there was anything to do: twelve statements a request, four of them writes, on every admin and cron request of a site that needed nothing, and an admin screen left open asks admin-ajax.php on a timer. It now reads the version of every component in one statement, and takes the lock only when one of them is behind. Measured on a development install: such a request's boot went from 11.1 ms to 7.4 ms and from 23 statements to 12. A site that is behind is brought up to date as before, under the lock, and announced once (DECISIONS #360).
  • A setting with no override costs no query, and an override is an option WordPress loads with the site's options (spec 001, FR-011a). On a site with no persistent object cache, CoreX's boot asked the database for eleven options that were not there, on every request: a third of a fresh site's front page. The config engine now looks for a setting's override among the options WordPress has already loaded, and so does the read of an add-on's feature flag, which leaves one of the eleven (the login protection settings, which are not a setting's override). The front page of a fresh install runs 23 queries where it ran 33. An override saved by the Settings screen, by update_option() or by wp option update is read as before, and so is one a pre_option_ filter answers. An override stored with autoload off is no longer read; wp option set-autoload <name> on makes it one again, and wp option list --search='corex_*' --autoload=off lists what a site has stored that way, which on an install nobody has changed by hand is only CoreX's own records (DECISIONS #361).
  • The standard email's header is part of the card, and a logo of any shape fits it (spec 106, slice 8). The site's logo, or its name, is now the first band of the card that holds the message, under the brand's edge. It stood above the card, on the canvas, limited to 180 pixels of width and nothing of height: a square logo was drawn 180 by 180 over a narrow message. A logo from the site's library is now written at the size that fits inside 180 by 48 pixels, as the image's own width and height, which is what Outlook on Windows reads. A square mark has the site's name beside it as text, so the email still says who it is from where images are not shown; a logo that is the name written out stands alone. A vector logo is passed over, since Gmail and Outlook draw nothing for one. wp corex make:email-design writes the same header (DECISIONS #322).
  • The list of open access requests is answered to the people who decide them (spec 079, FR-011a). GET corex/v1/access/requests needs a REST nonce and manage_options, which is what the Access & Abilities screen and the decision route need. The route's two methods were registered together under the check that suits asking for access; each now has its own. Asking for access (POST on the same route) is open to anybody signed in, as it was, and the screen shows its queue as it did (DECISIONS #346).
  • Three options CoreX reads on every request cost no query (spec 001, FR-011b). The login protection settings, the operations mode and the version of the Email Studio's installed defaults were each saved with autoload off and read on every request: one query each, stored or not, on a site with no persistent object cache. They are now stored where WordPress loads them with the site's options. On the first cron or admin request after a site takes the release, the rows it already holds are turned over, and a site that never saved its login protection or declared a mode is given a row that says so. Until then each is read as before, with the same answer: a hidden login address stays hidden and keeps its slug. Nothing reads these options differently. The front page of a fresh install runs three fewer queries (DECISIONS #366).

Fixed

  • A careers application is accepted on a Windows host. On a server that runs Windows, every application sent to POST corex/v1/careers/apply was refused (422, cv_move_failed), whatever the CV. PHP names an uploaded file's temporary copy with backslashes there (C:\Windows\Temp\php1A2B.tmp), and the route passed the whole description of the file through wp_unslash(), which removed them: the path handed on named no file. Measured with a real upload on Windows: refused before, stored after, the file kept byte for byte. The route now reads the file as the forms route does, from the request, cleans only the name and the type the browser gave, and hands the path on as PHP made it. A request that sends several files under the one name (cv[]) is answered as one with no CV, where it logged PHP warnings. On a Linux host the path has no backslash and nothing changes.

  • A form built in Forms & Flows is guarded by its trap field and by the site's challenge. Found by reading and then measured, 2026-10-13. The stage that judges a submission's trap field and its challenge token ran after the stage that keeps only the answers of the flow's fields, and read both from what that stage handed on, where neither was left. So a filled trap field stopped nothing, and on a site with a challenge provider configured every submission of such a form was refused, a real visitor's included, because the provider was handed an empty token. It has been so since the stages were written (v0.34.0). The stage now reads both from the request as it was received. Nothing got past a provider because of this. A form defined in code was never concerned (spec 068, FR-042a; DECISIONS #358).

  • On a site without pretty permalinks, CoreX → Submissions opens, and seven other things in the admin answer. Such a site, which is what a fresh install is until its permalinks are set, spells a REST route as a query (/index.php?rest_route=/corex/v1/submissions). Eight admin clients wrote ? and their own query after it, which made that query part of the route, and WordPress answered 404 "No route was found". The inbox opened on "The submissions could not be loaded". The same happened to a search or a state filter on Forms & Flows, a model's records and its table changes on Data, a template's health checks in Email Studio, the email log of a submission, Refresh on Blog Pro, and the plan step of the Setup Wizard. Each now adds its query as the site spells the address. A site with pretty permalinks is asked nothing differently. The browser suite covers it on CI's site, which has pretty permalinks, through a fixture that hands a page the other spelling (DECISIONS #359).

  • The update guides name the .env variable that is read: UPDATES_ENDPOINT. The deployment guide, the docs site's update guide and the core README said COREX_UPDATES_ENDPOINT. Nothing reads that: a config key is written in .env in capitals with _ for each . and no prefix, as the configuration guide says. A site that followed them had no update source, and a missing source is a silent no-op, so it was offered no update. The guides also name the option, corex_updates_endpoint. Three more names that nothing reads are corrected: COREX_ENV in the secrets guide (it is APP_ENV), and COREX_MAIL_DRIVER with its host, port and credentials in the mail guide and .env.example, where the five MAIL_* keys the mail add-on reads now are; MWP_I18N_DRIVER is gone from .env.example. The update source's empty default moves from config/app.php, where its key was app.updates.endpoint and nothing read it, to config/updates.php (DECISIONS #363).

  • The integration suite leaves no post on the install it runs against, and a test that would leave one fails. One run on a fresh install left three logged emails and a submission, written by listeners that two test files did not know of. The integration test case now watches every post a test inserts; one still stored after the test's own clean-up is deleted and the test fails, naming it. No transaction is used: a developer's tables may be MyISAM. Posts by type are the same before and after 1,200 tests. Rows the suite leaves in CoreX's own tables (notifications, activity, queued jobs) are listed in the decision and not fixed. A unit test of Email Studio that passed or failed by the order the files ran in now stubs what it calls (DECISIONS #364).

  • CoreX → Data no longer says a site has something to finish because it named no update source. "What this site can do" listed "No update source is configured" under Needs finishing on every site, since none is what CoreX ships with, and its Open Settings button opened a Settings screen that has no field for it: nine tabs and 39 controls, read on a fresh install, none for an update source. The list is for what was chosen and left half done, and a notice nobody could close kept it from ever being empty. The notice is removed. A site with nothing half done now reads "Nothing is half-configured."; a captcha provider with no keys is still listed. The updater is not changed, and the Settings screen gets no field for the source: a site that sets one, as the update guide says, is offered updates as before (spec 074, FR-5.4; the owner's decision; DECISIONS #365).

  • A CI job no longer stops on wp: command not found when setup-php could not fetch WP-CLI. Four jobs in three runs did on 2026-10-13, each before a test had run, and each passed when a person ran it again. setup-php asks for WP-CLI once, with no retry, and reports a tool it could not fetch in its log only, so its step ended green and the next one had no wp. The provisioning action now asks wp for its version after setup-php. Where there is none it installs WP-CLI 2.12.0 from that release's own download, trying again when the connection fails, and refuses a file whose SHA-256 is not the one written in the action. The run then carries a warning saying so. setup-php is still asked first and still provides the latest WP-CLI; WordPress is still installed at latest. Nothing a site runs changes (DECISIONS #354).

  • An email is in the site's language, whoever's request draws it (spec 106, FR-011a). WordPress answers which way text runs, the page's language and every translated string for the request, and in the admin a request is in the signed-in person's own profile language. So on an English site, a reply sent by an administrator who reads the admin in Arabic went out marked as Arabic, laid out right to left and signed off in Arabic; on an Arabic site an administrator who reads it in English sent replies left to right. It reached whatever such a person triggered from CoreX's admin screens, which ask their routes with _locale=user: a reply, its preview, Send again, a resent notification and a flow's test send; and the newsletter's "new article" email when they published a post. A visitor's form and anything sent from cron were the site's already. An email is now drawn inside WordPress's own switch to the site's language, and the person's screen stays in theirs. The operator's own words keep their own direction, paragraph by paragraph, as before. Words made before the mailer is handed them (a code-defined form's subject, the Guides support request) are the next entry's (DECISIONS #356).

  • The words CoreX writes into an email are in the site's language too (spec 106, FR-011b). Three kinds of word were still in the language of whoever triggered the email, on a site that has CoreX's strings translated. The notification of a form defined in code: its subject, the line that says where a file is opened and its link, and "The submission contained no fields."; and a flow's notification on a site without CoreX Mail. A Guides support message: what it is about, "a signed-in user", "no address given", and on a site without CoreX Mail the subject and every line. And what Email Studio stores the first time CoreX Mail runs: the names of its layouts and partials and the words of "Unsubscribe", "Preferences" and "Privacy", which were in the language of whichever administrator's page happened to be first. All are now written in the site's language. What a person is told on screen about a send stays in their own (DECISIONS #362).

  • The settings screen says when it did not save a field. An address, a web address, a colour or a choice that was not one was dropped in silence under "Settings saved", and the field came back as it had been. The screen now names the field, quotes what was typed and says why it was not taken, in a message that stays until it is dismissed; the field is marked as not taken. Everything else in the form is saved, as before.

  • An email address is judged as it was typed. The settings screen cleaned an address before checking it, so one WordPress could "repair" was saved as another address: sales@münchen.de as sales@mnchen.de. It is now refused, and said.

  • A From name is sent when the site names no From address. With a name set and no address, no From header was written and the email left as "WordPress". The name is now sent over the address WordPress itself sends from, unless a plugin has chosen a name.

  • A From name is cleaned as a header, not as text on a page. "Tom & Jerry <3" was sent as "Tom & Jerry <3". A name is now one line with <, > and " taken out, at most 100 characters. A name that held something shaped like a tag keeps its words: "Acme " was sent as "Acme" and is now "Acme Sales".

  • The browser job in CI runs on Ubuntu 26.04 as well as on 24.04. GitHub moves the ubuntu-latest label to Ubuntu 26.04 between 2026-10-19 and 2026-11-19, and a run lands on either image until it is done. Tried on the new image on 2026-10-13, every job passed but the browser job, which stopped on E: Unable to locate package php8.3-fpm before a spec had run: Ubuntu 26.04's archive has php8.5-fpm only. The package was not needed. Once setup-php has set up PHP 8.3 a PHP-FPM for it is on the runner, running, on both images, and the job now asks apt for php8.3-fpm only on a runner that has none. runs-on stays ubuntu-latest. Nothing a site runs changes (DECISIONS #367).

  • A new submission of a form defined in code raises a notification. Reported on 2026-10-13 from a client site: dozens of submissions stored and no notification for any of them. The notification centre listened for the event of a form built in Forms & Flows, and a form registered in code never raised it, so a site whose forms are all in code was told of nothing, since the notification centre first shipped (v0.35.0). A stored submission of a form defined in code now raises the same New submission notification, for whoever manages submissions: one item per form that counts its submissions, named as the inbox's filter names the form (a form built in Forms & Flows is named so too, where it was called by its slug), with a link that opens the inbox on that form's rows. A submission that was not stored raises none. When the form's own notification email, the one CoreX sends, could not be sent, the Submission notification email failed notification is raised for it as well (DECISIONS #352).

  • Every request of a site with WP_DEBUG on logged "Translation loading for the corex domain was triggered too early", and an email on a right-to-left site was drawn left to right. Since v0.45.1. The link that opens a file sent with a form was hooked by an object made on every request. Making it made the inbox's reader and the mail stack, and the email layout read the site's brand as it was made: the theme's settings, which WordPress translated before init, and the direction of the site's text, asked before WordPress knew it. The contact notification was dir="ltr" on a site in Arabic, and an Email Studio template that has a layout is drawn by the same object; a reply was not affected. What answers the link is now made when the link is followed, and the layout asks for the brand when it draws an email. While CoreX boots a request also runs four fewer database queries. A test loads WordPress in a process of its own and fails if a string of the corex domain is read before init (DECISIONS #351).

  • The account block says which of a person's sessions is the one they are using (spec 068, FR-161h). Under "Active sessions" the session of the browser that opened the page is meant to end with " — this device". No row ever did, on any site: the list was looked up by a key WordPress does not hand over. The server now knows the session by the record WordPress holds for it, and marks that row and no other. A request made with an application password has no session and marks none. Two sign-ins in the same second from one address and browser read the same, and one of the two is marked. Sign out other sessions did not rely on the mark and is not changed (DECISIONS #349).

  • A row of the account block's two lists is written for a person (spec 068, FR-161g). Under "Recent activity" a row was an internal name and a machine's date, profile.updated · 2026-10-13T13:23:23+00:00; it is now "Your profile was updated." and, under it, "October 13, 2026 at 4:23 pm", in the date format, time format and timezone set for the site. Under "Active sessions" a row was the browser's whole identification string, three lines of it on a phone; it is now "Chrome on Windows" and, under it, "Signed in October 13, 2026 at 4:23 pm". The IP address is not shown. Recent activity lists what the block itself records, a saved profile and signing out the other sessions: an administrator was also listed what they did in the admin, by its internal name. And a saved profile is listed at once: recent activity was asked for only as the page opened. The rows have no bullets and start where their heading starts (DECISIONS #348).

  • In the account block, a list that did not load says so, and an empty one says it is empty (spec 068, FR-161f). A signed-in person's "Active sessions" and "Recent activity" were a heading with nothing under it in both cases: when the request for the list did not arrive, was answered with an error page, or was refused, and when there was nothing to list. Each now shows its rows, or "No activity yet." (for sessions, "No sessions are recorded for this account."), or, in the box of a refused form, "Your recent activity could not be loaded. Load this page again to see it." (for sessions, "Your sessions could not be loaded. Load this page again to see them."). The sentence stands where the list would: it is not written as the answer to a button, so the page is not moved and no other message is taken away. Rows shown earlier are not kept when a later request for the list fails. Sign out everywhere now loads the page again, which shows the sign-in form: it left the signed-out person on the member view, with the list emptied. The two session buttons stand one spacing step under the list, which they touched (DECISIONS #345).

  • The account block says so when a request gets no answer, and when scripts are off. Two cases in which pressing a button of the corex/account block did nothing that could be seen. A request that did not arrive (the visitor offline, the request blocked), or that was answered with a page and not with JSON (a server's error page, a proxy's), or refused with no message, wrote nothing: the form now says "The site could not be reached, or its answer could not be read. Please try again." in its own line, under its button, and so do the two session buttons. And with scripting off a form was sent to the page, which came back with the empty forms: a browser that runs no scripts is now shown a note above the forms saying that they need JavaScript. The forms still do not work without scripts, and the note names no other place to sign in, because on a site with login protection on WordPress's own address is the hidden one (spec 068, FR-161e and FR-161d; DECISIONS #339).

  • A member who forgot their password resets it on the account page (spec 068, FR-158a to FR-158c; the owner's decision). The account block asked WordPress for the reset email and WordPress wrote its own link into it. Measured on WordPress 7.1.3: the member chose the new password on wp-login.php, its "Log in" link led to WordPress's sign-in screen, and signing in there ended at wp-admin/profile.php; the account page was not seen again, and had no form that takes a reset key, though the route for one existed. Where the Security Center moves the login, the email's link was the moved address. Now the email's link is to the page the reset was asked from. That page takes the key out of its address, shows a Choose a new password form and no other, and once the password is saved says so above the sign-in form. A link that was used, or that WordPress no longer takes, is answered "This password reset link is invalid or has expired. Please ask for a new one." above the forms, one of which asks for one. The email is still WordPress's and is still sent by WordPress: one address in it is changed, and only for a reset asked through the account block (DECISIONS #341).

  • Asking twice for the same access makes one request (spec 079, FR-008a; DECISIONS #343). POST corex/v1/access/requests stored a new request each time the same signed-in person asked again for the same ability or area. Measured on WordPress 7.1.3 over HTTP: two posts, two requests in the queue, two "created" entries in the audit log, two notifications for whoever manages access and, on a site with an email route for access.request.created, two emails. The form on a denied screen asked first whether a request was open; the route did not, and nothing asked about a request for an area. The question is now part of storing the request, so no caller can leave it out. A repeat is answered 200 with state: blocked and the code request_already_pending, and nothing is stored, audited or sent. Two asks at the same moment make one request as well: 36 pairs posted to two server processes at once stored one each. A request for something else, from somebody else, or made after the first was decided or ran out, is a new request. Requests a site already holds twice are left as they are.

  • A long email address registers without a username, and one too long for an account is refused with a reason. The registration form's Username field is optional, and without one the login is the email address. WordPress holds a login in 60 characters and an address in 100. Measured on WordPress 7.1.3: an address of 61 characters or more with no username was answered "Username may not be longer than 60 characters.", to somebody who had typed none; and an address of 101 with a username was answered "Your account has been created. You can now sign in." with no account stored, because WordPress reports no error for an address its database does not take. Now an address of 61 to 100 characters gets a login made from the part before the @ (what a login can hold of it, 60 characters at most, with -2 or the next free number when an account has it), and its owner signs in with the address as everybody with no username does. The account is named by the part of the address before the @, as any account with no username is. When no login can be made, the form asks for a username (username_required). An address of more than 100 characters is refused at registration and on the Profile form, which failed silently in the same way, with a message that says how long one may be (email_too_long). An address of 60 characters or fewer is the login, as before. For a developer: AuthGateway has a new method, loginForAddress(), and createUser() takes the account's name as a fourth argument; a site's own gateway has to add both (DECISIONS #338).

  • A new account with no username is no longer named by its email address. The registration form's Username field is optional, and an account made without one has its address as its login. The account add-on handed that login to WordPress as the display name, and WordPress made the nickname and the address of the author page from it too. Measured on WordPress 7.1.3, for a visitor who is not signed in: a comment by dalia.hassan@example.com was signed dalia.hassan@example.com on the post's page, in wp/v2/comments and in the comments feed, and its markup had the class comment-author-dalia-hassanexample-com; once a post was published in that account's name, wp/v2/users/<id> gave the address as the name and dalia-hassanexample-com as the slug, and the post's page and the posts feed named the address. Such an account is now named by the part of its address before the @: the display name and the nickname are dalia.hassan, and the author address is dalia-hassan. Its login is still the address, and its owner signs in with the address as before. An email address typed into the Username field is treated the same way. An account with a username that is not an email address is named by it, as before (DECISIONS #335).

  • An action is recorded, and its request answered, for a person whose display name is longer than 191 characters. The activity record holds the name of who did something, the name of what it was done to and that thing's id in 191 characters each, and WordPress holds a display name in 250. A record with a longer name was refused by its table, and the request that had already done its work answered 500. Measured over HTTP: a member who saved a display name of 192 to 250 characters on the account block was answered 500, with the name saved; from then on every save of that profile was, and so was "sign out other sessions", which had signed them out. WordPress's own profile screen takes such a name from any member (a nickname chosen as the display name), and a person so named who asked for access to a part of CoreX was answered 500 with the request stored, once for each try. So for every action CoreX records under a person's display name: a role's abilities changed, an access request made or decided, a change applied on CoreX → Data. A name, or an id, longer than the record holds is now cut to 190 characters and an ellipsis in the one place every record is made (Corex\Activity\ActivityService::record()), and the record is written. The account keeps its whole name (DECISIONS #340).

  • A display name too long for an account is refused on the Profile form, and no longer answered "Your profile has been saved." WordPress holds a display name in 250 characters and does not refuse a longer one: the account kept the name it had, nothing else of the account's row was saved, a first name sent with it was, and the form said the profile was saved. The same happened to 250 characters with an & among them, because WordPress keeps & as &amp;. The account block now refuses a name WordPress would keep in more than 250 characters, with a message that says how long one may be, or that a character such as & is kept as several when that is the reason, and saves nothing (display_name_too_long). A name of up to 250 characters is saved, in any script: the count is of characters (DECISIONS #340).

  • The account block says what happened under the button that was pressed. The corex/account block had one message line, above its forms, for everything the server answers: a refused sign-in, a refused registration, "a reset link is on its way", a saved profile. With the button in the window the line was often outside it: 292 to 340 pixels above a desktop window, and up to 521 above a phone's. A screen reader was told; somebody looking at the button saw nothing happen. Each form now has a message line of its own under its button, and so have the two session buttons. If the line would be outside the window the page moves by the least that shows it, at once for a visitor who asked their system for reduced motion. One answer is on the page at a time, written in one place, so it is announced once; focus stays on the button. The line is as wide as the fields above it: those reached 16 pixels past the form's inner edge on one side, and no longer do (spec 068, FR-161a; DECISIONS #336).

  • In the account block, every label belongs to the field under it. A field's id was made from its name alone, and the signed-out block prints up to three forms on one page: the sign-in form and the forgot-password form each ask for "Username or email", and the sign-in form and the registration form each ask for "Password". The second field of each pair had the first one's id. Pressing the forgot-password form's label, or the registration form's "Password", put the cursor in the sign-in form; in the browser's accessibility tree those two fields had no name and the sign-in form's two had theirs twice. An id now holds the block, the form and the field's name, so it is on the page once, also on a page that holds the block twice (spec 068, FR-161b; DECISIONS #337).

  • A field of the account block tells the browser what it is for. No field of the corex/account block had an autocomplete value, which WCAG 2.2 asks for on a field that collects something about the person (1.3.5) and which a browser and a password manager read. Measured in Chromium: it read the empty registration form as a form that changes a password, an old one and a new one, did not connect the forgot-password form to an account, and listed four fields as missing the attribute. The sign-in fields are now username and current-password, registration's email email and its two passwords new-password, the forgot-password field username, the two passwords of the form a reset link leads to new-password, and the profile's fields nickname (the display name), given-name, family-name and email. Chromium now reads registration as a new account and the forgot-password form as one with a username. It offered to save the password after sign-in before, and still does. Registration's optional username is username too, with the account rule of the next entry: with the value alone Chromium wrote the person's saved email address into it, and an address with a + was then refused as a username where the same person registered before (spec 068, FR-161c; DECISIONS #342 and #347).

  • A username that is the person's own email address is taken as no username. The registration form's Username field is optional, and with none the login is made from the address. Somebody whose address was in both fields was judged by the one in Username: name+shop@example.com was refused with "A username can have letters without accents, numbers, spaces and the characters _ . - @ and no others.", and an address of 61 characters or more with WordPress's "Username may not be longer than 60 characters."; the same person registered with the field left empty. A browser puts the address there: Chromium writes a saved address into a field marked username. The account service now takes the person's own address in Username, in any capitals, as no username, so the account is the one the address gets with the field empty: the same login, the same name, the same answer. Any other username, another address among them, is judged as before. Measured in Chromium 153 with the field marked username: a registration filled from a saved address with a + in it had the address in both fields and was refused before the change, and is made after it (spec 068, FR-158d; DECISIONS #347).

  • An Email Studio template's subject is sent as it was written. A template's subject was stripped of markup when the email was made, and a subject is an email's header, which is plain text. A template whose subject is "Price <500 EGP" was sent as "Price"; "Sam sam@example.com asked" as "Sam asked". A value merged into a subject was stripped the same way: with the name "Sam x & co", "Hello {{ user.name }}" was sent as "Hello Sam x & co". Both are now kept as they are, by the rule a reply's subject is read by (Corex\Mail\SubjectLine). The stripping did not take a line break out of a merged value: a name or a message with a break in it reached the email's subject, and the copy Email Studio keeps, with the break. WordPress's own mailer removes one from a subject as it writes it; what another mailer does with one was not measured. The merged subject is now one line, with a space where the break was. This is every email sent from a template through its route (a form's notification, an access request's emails) and one sent again from a submission (DECISIONS #330).

  • What a visitor writes in a form is kept with its backslashes. WordPress takes one level of slashes off a post's fields and off a meta value on their way in, and a submission's answers were handed over as they were. A visitor who wrote "C:\offers\new" in a contact form was read in the inbox, on CoreX → Data and in an export as having written "C:offersnew"; "\server\share" as "\servershare"; a backslash at the end of a message was gone. The same was true of everything stored the same way, and each is now handed over with a level of slashes on it and read back as it was given (DECISIONS #331): - a submission's answers, its sender's name, the name of its form and what came with it (the campaign, the hidden values, the consent wording), for a form defined in code and for a flow; - a flow: its name, its fields' wording, and its rules. A rule pattern:^\d{2,4}$ was stored as pattern:^d{2,4}$, which no longer asks for digits, so a field held to a pattern in the builder refused what it should have taken; - a note on a submission, the history of a submission, and the key of the team a submission is given to. The notes and the history are each written again whole when one is added, so an earlier note lost one more level each time; - the title, the content and the fields of any model kept through Corex\Repositories\PostRepository, and any value written through Corex\Fields\MetaFieldDriver. The inbox's search and a query by a field now find a text with a backslash in it; they found nothing while it was stored without.

    Quotes, ampersands, Arabic and accented text were never changed, and a number or a switch is stored as it was. The email a form sends is built from what the visitor sent and not from what was stored (read in the listener; not sent to check): what was wrong is what was kept.

  • A comment, an editorial note, a profile's names, an import's rows and a kit's page are kept with their backslashes. The rest of the same defect, each write measured before it was changed (DECISIONS #332): - a comment edited or answered from Blog Pro was published without them: a reply saying "C:\offers\new" read "C:offersnew"; - a post's editorial notes lost one more level each time the post's workflow was saved; - a display name, a first name and a last name saved on the account page: "CORP\salma.h" was saved as "CORPsalma.h"; - CoreX → Data: an import keeps the rows of its file in its run and commits them from there, so a row was imported without its backslashes. A migration is run from the plan kept in its run, where \' in a statement had become '. A Data export and a submissions export kept the search they were asked for without them; - a page a kit creates, fills or replaces: a block's attributes are JSON, in which WordPress writes & as \u0026, and "Tom \u0026 Jerry" was stored as "Tom u0026 Jerry". The pages CoreX ships name a pattern each and hold no backslash, so none of them was affected; a kit of a site's own with a block's attributes in a page was; - the record of a WebP file names the picture and its WebP by their paths. On a Windows host every separator of a path is a backslash, so the record named two files that are not there, and wp corex media reset-webp deletes the file the record names. A Linux host's paths have none and its records were right.

    The notification preferences were measured too and lose nothing: they hold a switch for each category. A password is not touched by any of this.

  • The inbox no longer fails to load when a submission is deleted at that moment. WordPress finds the ids of a page of submissions and then reads each one; a submission deleted between the two, by a colleague or by retention, came back as nothing, and the list answered "The submissions could not be loaded" for whoever was opening it. It is now a page with one row fewer. The same in the Data screen's list of submissions.

  • A reply no longer keeps another plugin's mail settings from being applied. CoreX gives WordPress's mailer a reply's plain-text version through the phpmailer_init hook, and took itself off that hook as it ran. WordPress then skips whatever listens at the next priority, where something else listens at an earlier one. On a site whose mail server is set up by a listener at a later priority (a snippet at 20 or 99, say) behind another plugin's at an earlier one, a reply sent since v0.45.0 could leave without those settings. It now does its work once and is taken off when the email has gone (DECISIONS #324).

  • A reply's subject reaches the customer as it was typed. The reply and its preview cleaned the subject as the text of an HTML form, and a subject is an email's header, which is plain text. "Price <500 EGP" was sent as "Price <500 EGP", and a mail program shows that as written; "Sam sam@example.com asked" as "Sam asked"; "Save 50%de today" as "Save 50 today". A draft had kept the subject as typed since it was built, so a draft gave back the right subject and sending it changed it. A reply, its preview and its draft now read a subject by one rule, Corex\Mail\SubjectLine: the text as typed, on one line of at most 300 characters, with a space where a line break or another control character was, so that nothing typed after a break becomes a header of its own. Email Studio's Test send and Resend changed a subject the same way and read it by the same rule now. Every place a subject is printed was read first: each escapes it where it is printed (DECISIONS #326).

  • Email Studio keeps a backslash. Its store handed its records to WordPress as they were, and WordPress takes one level of slashes off a post's title and off a meta value on their way in. A backslash was gone from everything the store kept. An email with the subject "C:\offers\new" was listed, among the captured emails and the attempts, as "C:offersnew", and its body was kept without them too. A template, a layout or a partial written with content:"\201C" was stored as content:"201C", and an email sent by a route is built from what is stored. A name with two backslashes lost one more each time its record was saved again. A template version's checksum, taken before the store, was not the checksum of what was read back. The store now puts a level of slashes on a record's name and on its content before handing them over, as the store of a submission's replies does, and each is read back as it was given. A reply's own email always left with its backslashes: what was wrong was Email Studio's copy of an email, and what it keeps to build emails from. Records kept before this release are not repaired (DECISIONS #329).

  • A username the account form cannot keep is refused, and no longer changed and accepted. WordPress does not refuse a username it does not accept: it removes what it does not accept and stores the rest. Somebody who registered on the account block as "CORP\salma" was told the account was created, was stored as "CORPsalma", and was then refused at the block's sign-in and at wp-login.php with the name they had chosen. The same with an apostrophe, an exclamation mark, a plus, or a name in Arabic letters, of which only the Latin letters and digits beside it were kept. The registration now answers that a username can have letters without accents, numbers, spaces and the characters _ . - @, and asks for another, by WordPress's own rule for a username (validate_username()). Spaces before and after a username are taken off, not refused. Somebody who types no username is registered as before, with a login made from the address, and signs in with the address (DECISIONS #333).

  • A request the Access routes cannot read is answered 400 and says which parameter, not 500 (spec 079, FR-002a). POST corex/v1/access/requests with no reason, a reason over 2,000 characters, an ability or an area CoreX does not have, both, or neither, was answered "There has been a critical error on this website" and wrote a fatal error to the PHP log each time; any signed-in person could send one. The role routes (access/roles/<role>, /preview, /apply) did the same for an administrator who sent an ability or an effect CoreX does not have, a role that is not a role key, or a confirmation that could not be read. Measured over HTTP on WordPress 7.1.3: 21 such requests were 500. Each is now 400 with WordPress's rest_invalid_param body: the parameter, a message, and a code a script can branch on. Nothing is stored, audited or notified. A reason sent as a list was stored as the word "Array" and is now refused. A request that can be read and is refused for how things stand (a decision on a request no longer pending, a change the safety policy stops, a confirmation for another change) is still 200 with a blocked result (DECISIONS #350).

  • A password with a quote or a backslash in it is one password at the account block and at WordPress's own screens. WordPress's screens keep and check a password as the text of a form, with the request's slashes on it; the account add-on handed over the password as it was typed. A member who chose "it's-my-password" on the account block was refused at wp-login.php with it, and a member who chose theirs on WordPress's own reset screen was refused by the account block. The add-on now hands a password over the way a WordPress screen does, at registration, at a reset and at sign-in. An account that chose such a password through the add-on before this release still signs in at the account block, and is moved to WordPress's form as it does, after which wp-login.php accepts it too. A wrong password is counted as one failed sign-in, as before. The owner's decision (DECISIONS #334).

  • The theme keeps a gutter beside what a visitor reads. On a phone the title, the text, the header and the footer of every template began at the edge of the window, and a wide block, such as the grid of posts, did the same in a window 1024 pixels wide. The theme now has a side padding of its own, from a spacing token, the same on both sides: 16 pixels. It is set once, in theme.json, and a site that runs the theme changes it in its brand.json. Measured on every template and every pattern, at 375 and 320 pixels, left to right and right to left, before and after a kit (spec 068, FR-157a; DECISIONS #344).

  • A block set to full width in a page's content is the window's width. In a page, a post or a project it was held to the text column, 768 pixels, on a desktop: a hero pattern placed in a page was a band in the middle of the window. A block set to wide was held to the same. Both are now what they say, with the text of a full-width block inside the gutter (FR-157b).

  • A form fits a narrow phone. The corex/form block could not be narrower than 347 pixels, so a page that held one scrolled sideways in a window 320 wide: the front page, the contact and form templates, and a search that finds nothing. In a window 600 pixels wide or less its fields are now one column. Wider than that, nothing changed.

  • The docs header's search field stays inside the header in a window 320 wide, where its button reached the window's edge once the header had a gutter.

  • The guides say which directory the .env is read from. It is the directory two levels above the real location of corex-core, and no other. Where the plugin is linked into WordPress from a repository, as in a development checkout and the VM recipes, that is the repository or the release directory, outside the web root. Where it is copied into wp-content/plugins/, as in the shared-host package, that is wp-content/, inside the web root, and a .env beside wp-config.php or above the web root is not read. The secrets guide has a new section that says so, with what to do on each kind of install; the configuration guide, the core README, the two update guides, the shared-host guide and .env.example point at it. The secrets guide also said a VM's .env is "root-only": a file the user PHP runs as cannot read is treated as absent, with nothing logged. And it listed the database password and SMTP credentials among what CoreX's config reads, which it does not. No code a site runs is changed (DECISIONS #368).

Security

  • A form's file field is answered by the file the request carried, and by nothing in its text (spec 081, FR-002a). When a request sent no file for a file field, what its body said under the field's name was kept as the field's answer, and an answer to a file field is read as the number of a stored file: a submission could name a file it was not sent with. A file field with no file now has no answer. required says the file is missing, and an optional one is stored without it. The same holds for a file question of a flow built in Forms & Flows, whose route takes no file. The mime and max_size rules refuse text. Reported on 2026-10-13 from a client site (DECISIONS #353).
  • The attachment store reads no file it was not sent (spec 081, FR-004a). It asks whether PHP received the file as an upload before it reads it. WordPress asked the same when the file was moved, and nothing was stored; the store had read the file by then.
  • A submission's files are removed with it only where they are its own. A file is taken as a submission's by the answer to the field it came through, and not by any answer that holds its number. A file that two submissions name stays until the last of them is deleted.
  • The Data import reads the CSV a request carried as a file, and no other. For a person who may manage data: what a request's text said under file was read as the upload. Found by the review of the change above.

Client impact

  • Three new settings, mail.reply.from_name, mail.reply.from_address and mail.reply.reply_to (options corex_mail_reply_from_name, corex_mail_reply_from_address, corex_mail_reply_reply_to). Blank on every site until set. A site whose mail relay sends each address through its own connection has to add the reply address there before naming it here. Corex\Email\Message\EmailMessage gains a last, optional constructor argument, fromName, and a new class, Corex\Email\Message\SenderName, holds the rule for one. A site with its own MailDriver that wants the name has to read it; the reply's arrives cleaned. The general From name may read differently: see Fixed.
  • A form built in Forms & Flows refuses a submission whose trap field is filled, and takes a challenge token. Nothing to set. A site that has such a form and a challenge provider configured (reCAPTCHA, Turnstile, hCaptcha or its own) had every submission of that form refused; a submission whose token the provider accepts is now stored. A site that worked round that by setting the form to Never protect this form can set it back once it has seen a submission go through on its own pages: that was not run in a browser against a real provider here. The builder's test run still carries no token, so on such a site it stops at the protection stage as before. SubmissionPipelineContext has a new public property, received, and a seventh constructor argument that may be left out (DECISIONS #358).
  • A site's own admin script that adds a query to a CoreX REST address by hand does not work on a site without pretty permalinks. CoreX's own screens are fixed (DECISIONS #359) and a site has nothing to set. A script of the site's that writes `${ restUrl }?name=value` asks for a route WordPress does not know there. Set the parameters on a URL instead (new URL( restUrl ), then searchParams.set()), as the Setup Wizard does: CoreX's helper is inside its admin bundle and is not something a site can import.
  • Nothing to do for the schema check. Nothing stored changes: a component's version option stays where it is, with autoload off. A site that binds its own Corex\Database\Schema\SchemaVersionStore has two methods to add, getMany() and forget(). Nothing in the framework binds another.
  • A site whose .env has COREX_UPDATES_ENDPOINT renames the line to UPDATES_ENDPOINT. Until it does it is offered no update. A site that set the option corex_updates_endpoint changes nothing. COREX_ENV, COREX_MAIL_DRIVER, COREX_MAIL_HOST, COREX_MAIL_PORT, COREX_MAIL_USERNAME, COREX_MAIL_PASSWORD and MWP_I18N_DRIVER in a .env did nothing and can be deleted (DECISIONS #363).
  • A client repository's CI may show a framework integration test red where it was green. The suite now fails a test that leaves a post on the install. If a client's own plugin is active there and writes a post while a framework test runs, that test fails and names the post. Not seen on a client site; the remedy is in the framework's test (DECISIONS #364).
  • Three rows of a site's options table change on its first cron or admin request after this release, and there is nothing to do. corex_login_protection_settings, corex_operations_mode and corex_email_studio_defaults_version are stored with autoload on; a site that never saved the first or declared the second gains the row, holding an empty array and an empty string. Code of a site's own that reads get_option('corex_operations_mode') and takes false for "not declared" gets '' now: ask OperationsModeStore::isDeclared(). A site's plugin with an option of its own that every request reads can keep it loaded the same way, with Corex\Support\Options\LoadedOptions (DECISIONS #366).
  • A site with a form that has a file field should take this release, and look at what it already stores. Three things show an answer that was not a file the submission was sent with: text where the inbox shows a file's name, a file answer the inbox shows as a missing file (which is also what a file removed since looks like), and one file named by more than one submission under the same field. Nothing stored is changed by the update.
  • A site's own listener that opens a form's uploaded file asks Corex\Security\Upload\FormUploads::cameThrough($field, $attachmentId). The two post meta keys an upload is marked with (_corex_protected, _corex_upload_context) are the framework's to change and are not a contract; a listener that reads them, as MUVA's SendBriefEmails does, changes to the call. From this release the value a listener is handed for a file field in FormSubmittedEvent is the number of the file this submission stored, or the key is absent: it is never text.
  • Three answers change. A request that sends text under a file field's name gets a response with no value for that field, where it got the text back. A flow with a required file question refuses every submission, where it accepted text; a flow has never taken a file. AttachmentStore::store() answers not_uploaded for a file PHP did not receive as an upload, where it gave another reason; the careers form's answer for it is cv_not_uploaded.
  • An email follows the site's language and no longer the profile language of whoever triggered it. Nothing changes for a site whose staff read the admin in the site's language. Where somebody's profile language differs, the replies they send, and the other emails listed under Fixed, change direction and lang to the site's, and CoreX's own words in them are the site's translation. Nothing stored changes, and an email already sent is as it was sent. A client's own email design and its corex_email_brand filter are called inside the switch, so a string they translate is in the site's language too. In PHP, the constructors of Corex\Email\Design\ReplyComposer and Corex\Email\Template\TemplateRenderer take a Corex\Mail\EmailLanguage as their last argument; the container gives it, and only code that makes either with new has to pass one.
  • A form's notification, a Guides support message and Email Studio's defaults follow the site's language as well. Nothing changes on a site that does not translate CoreX's own strings: CoreX ships no translations of them, so there they are English before and after. On a site that does translate them, those emails stop following the profile language of whoever triggered them. Email Studio's defaults already stored on a site are not rewritten: they stay in the language they were first stored in, and can be edited in Email Studio. For code that calls these classes itself: Corex\Forms\Submission\NotificationDispatcher::dispatch() takes, as its third argument, a closure that returns the Corex\Mail\MailRequest (it took the request), and its constructor takes a Corex\Mail\EmailLanguage first; Corex\Guides\Support\SupportMailer takes one first too; Corex\Guides\Support\SupportMessage takes category, a key, where it took categoryLabel; SupportRequestController::categories() is SupportCategory::labels(). The container makes all of them.
  • A site whose forms are registered in code starts to get the New submission notification. Nothing to set. It is raised for submissions stored from now on: submissions already in the inbox get none. Its key is submission.new:<form slug>, the slug the form is registered under, the same shape a form built in Forms & Flows has always had; a site that publishes a notification of its own for the same form under that key adds to the same item, and under any other key makes a second one. The failed-email notification's key is submission.email_failure:<form slug>. A site whose form lists a listener of its own for the email, in place of SendEmailListener, is told of a failed email only if that listener says how it went: $event->notifiedAs(NotificationDelivery::fromResult($mailer->attempt($request))), where the mailer is an AttemptingMailer (the one the mail add-on binds is). Without that line nothing is raised for the email, as before. That line is for a listener that sends while the request runs. A site that queues the email and sends it afterwards (Muva does, from WP-Cron) has no result to report then, and nothing in this change takes one later: its failed notification emails raise no notification, as before. In PHP: FormSubmittedEvent gains notifiedAs() and delivery(); Corex\Forms\Submission\SubmissionProcessedEvent is now also dispatched for a form defined in code, with flowId 0 (SubmissionProcessedEvent::NO_FLOW) and ownerId null, so a listener a site has on it hears of those submissions too and must not take the id for a flow's; NotificationDelivery gains failed(); FlowFilterOptions gains nameOf().
  • A right-to-left site on v0.45.1 or v0.45.2 sends the emails of the shared layout right-to-left again. Those are the contact notification and an Email Studio template that has a layout. They have been left-to-right since the site took either release. Nothing stored changes, and an email already sent is as it was sent. In PHP, SubmissionFileDelivery::register() is gone: the provider hooks the action. The constructor of Corex\Email\Template\Layout also takes a closure that answers with the brand; an array is taken as before.
  • A site whose theme is its own is not moved by the theme's new gutter. The side padding and the template changes are in the Corex theme. Muva's, Perego's and Mizzey's themes are themes of their own, not children of it, with their own theme.json (read on this machine, 2026-10-13), so none of their pages moves. A site that does run the Corex theme gets the gutter on every page, and a block set to full width or to wide in a page's content becomes that wide on a desktop, where it was held to the text column: look at pages that hold one. A template such a site saved a copy of in the Site Editor keeps the old markup; there, set its Content block to full width, or reset the template. What does reach every site is the form: in a window 600 pixels wide or less a corex/form is one column (grid-template-columns on .corex-form, and a field is grid-column: 1 / -1 where it was span 12). A site that restyles the form's grid at that width should look at it on a phone. The form's stylesheet is one of the framework's built files: the npm run build of a site's update steps makes it, and nothing of the site's own has to be rebuilt. Neither Muva's nor Perego's theme sets a root padding (read the same day); whether their pages keep a gutter on a phone is theirs to measure, and tests/e2e/theme-gutter.spec.js shows how.
  • The account block's two lists changed, in the page and in the routes. Only for a site that has Corex Profile on. GET corex/v1/account/notifications answers with the newest ten events of two kinds, profile.updated and sessions.revoked_others, and no longer with every event the person is the actor of: a site that read that route for a person's whole activity reads less. Each event gained label and when, and each session of GET corex/v1/account/sessions gained device and signedIn; nothing was taken out of either. In the page a row is <li class="corex-account__entry"> holding two <span>s, where it was an <li> of text, and the two <ul>s have role="list" and no bullets: a site that styles .corex-account__session-list li or .corex-account__notification-list li itself has to look at it again. Six strings are new in the corex text domain. Read on this machine on 2026-10-13: nothing under sites/ in the Muva or Perego checkouts names the block, its lists or the two routes. Nothing to rebuild: the block's script and stylesheet are not built.
  • A script that reads GET corex/v1/access/requests has to be signed in as somebody who may open CoreX Access & Abilities. Anybody else is answered 403. Nothing in CoreX read the list through the route: the screen is handed it with the page. In PHP, AccessController::requests() is gone; the route's two methods call pendingRequests() and createRequest().
  • The account block has a new element and a new sentence to translate. Only for a site that has Corex Profile on. <noscript><p class="corex-account__noscript"> is the first child of .corex-account, and the second on the two page loads that say what became of a password reset, where the reset's message line stays first: a site that styles the block itself, or selects its first child, sees it with scripting off only. Two strings are new in the corex text domain: the note's, and "The site could not be reached, or its answer could not be read. Please try again." Nothing to rebuild: the block's script and stylesheet are not built.
  • The password-reset email of a site that uses the account block links to the account page, and a visitor chooses the new password there. It applies to a site with the Corex Profile add-on active and the corex/account block on a page; no other site sees a change. The email's subject, wording and sender are unchanged: they are WordPress's. What a site has to check: a page cache must pass a request with a query string to WordPress (?corex_reset_login=…&corex_reset_key=…, then ?corex_reset=1), which is what caches do unless they were told to ignore query strings; and a site with code of its own that calls POST corex/v1/account/reset-request keeps WordPress's link until it sends the forgot-password form's hidden page field with the request. A plugin that rewrites the whole reset email and leaves no address with the key in it is left alone. A reset asked at wp-login.php is unchanged. For a site that styles the block itself: on the two page loads that say what became of a reset, the block's first child is a .corex-account__status again, with its data-state. Nothing to rebuild: no script or style changed. Which client installs have the add-on on is in the entries below, and was not read again for this one.
  • A site with an access request store of its own has one method to rename, and a script that asks for access has one field to read. Corex\Access\AccessRequestStore::create() is now createUnlessPending(): it returns null when the requester already has an open request for the same ability or area, and has to ask and store as one step. AccessService::pendingRequestFor() is removed; requestAccess() answers the question itself, with a blocked result. A script that posts to corex/v1/access/requests is answered 200 for a repeat, as it was, and has to read data.result.state to know that nothing was created (DECISIONS #343). MUVA's and Perego's own code under sites/ was searched on 2026-10-13 and uses none of the three; mizzey was not read. Nothing to rebuild: no script or style changed.
  • The account block's message line moved, and its fields are 16 pixels narrower. Only for a site that has Corex Profile on and styles the block itself. The one .corex-account__status that was the first child of .corex-account is gone; there is one inside each .corex-account__form, after its button, and one inside .corex-account__sessions. It no longer has the hidden attribute: an empty line is in the page with no box, so a rule written for .corex-account__status[hidden] matches nothing, and one for .corex-account__status now matches several elements. The fields (.corex-account__row input) are sized with their padding and border inside their width. Read on this machine: nothing under sites/ in the Muva or Perego checkouts names the block. Nothing to rebuild: the block's script and stylesheet are not built.
  • An account registered through the account block before this release, with no username, keeps its email address as its display name and nickname, and keeps the address, without its punctuation, in the address of its author page. Nothing rewrites them: a display name may by now be one its owner chose, and a changed author address breaks the links to it. wp user list --fields=ID,user_login,user_nicename,display_name,user_email shows them: such an account's login is its address. Its owner can change the display name on the account block's Profile form. That does not change the nickname or the author address, and, as measured, it does not change the comments already written: WordPress copies the name into each comment when it is posted. From this release, on a site with the add-on active and registration open, a new member who typed no username is shown "Signed in as" the first part of their address, and their comments are signed with it. Nothing to rebuild: no screen, script, style, route or interface changed. The add-on is switched off on Muva by its own decision record, and is inactive, with registration closed, on the Perego and Mizzey installs of the development machine, as read on 2026-10-13; their live hosts were not read.
  • A site with a gateway of its own for the account add-on has one method to add. Corex\Profile\Account\AuthGateway has a new method, displayNameAsKept(): the display name as the account would hold it, which the Profile form's length rule is asked of. A gateway that keeps a name as it is handed over returns it. Neither the Muva nor the Perego site's own code implements the interface, as read in both checkouts on 2026-10-13.
  • A name or an id longer than 191 characters is cut on the activity record. Code of your own that calls Corex\Activity\ActivityService::record() may hand it an actor's label, a target's label and a target's id of any length: each is kept to 190 characters and an ellipsis. Records already written are as they were, and none of them is longer: the table refused those. No table changed and there is nothing to rebuild.
  • The ids of the account block's fields changed. Only for a site that has Corex Profile on and selects a field of the block by id in its own stylesheet or script. corex-account-login, corex-account-password, corex-account-email and the others are gone. An id is now corex-account-, the block's number on the page, the form and the field's name (corex-account-1-login-password), and the number is not promised to stay the same: select a field by its form and its name ([data-corex-account-form="login"] input[name="password"]). The fields' names, the forms' data-corex-account-form values and every class are as they were. Read on this machine on 2026-10-13: nothing under sites/ in the Muva or Perego checkouts names the block or an id of it. Nothing to rebuild.
  • The account block's fields gain an autocomplete attribute. Only for a site that has Corex Profile on. Every labelled field of the block has one now; the profile guide lists the values. A visitor's browser may offer more than it did: Chromium takes the sign-in and the forgot-password field for an email address as well as a username, and the registration form's passwords for new ones. A site that prints its own sign-in form beside the block, or sets autocomplete on these fields from its own script, should check that the two agree. No id, name, class or route changed. Read on this machine on 2026-10-13: nothing under sites/ in the Muva or Perego checkouts names the block. Nothing to rebuild.
  • A registration with the person's own address as the username can make a different account than it did. Only for a site that has Corex Profile on and registration open. Where the address was typed into Username in other capitals than into Email (Kim@Example.com beside kim@example.com), the login was the username as typed and the account was named Kim; the login is now the address as it is in Email, and the name kim. Where the address was refused as a username (a + in it, or over 60 characters) the account is now made. The same address in both fields in the same capitals makes the account it made before. No method was added to AuthGateway, so a site's own gateway needs no change. Read on this machine on 2026-10-13: nothing under sites/ in the Muva or Perego checkouts names the block, the account service, the gateway or the register route. Nothing to rebuild.
  • A template, a layout or a partial saved in Email Studio before this release, with a backslash in it, is still stored without it. Nothing rewrites it: what was dropped is not in the database to put back. Open it, write the backslash again and save. The captured emails and the attempts already listed keep the subject and the body they were stored with. Nothing to rebuild: no screen changed, and no class or route did.
  • A submission, a note or a flow stored before this release with a backslash in it is still stored without it. Nothing rewrites it: what was dropped is not in the database to put back, and a stored "C:offersnew" cannot be told from one that was typed so. A submission already received stays as it is listed. A flow whose field has a pattern: rule with a backslash in it (\d, \s, \.) is not checking what was written: open the flow in the builder, write the rule again, save and publish. Whether a site holds such a flow was not read.
  • Code of your own that keeps text through Corex\Repositories\PostRepository or Corex\Fields\MetaFieldDriver hands it over as it is, without slashes, which is what both always asked for. Text read from $_POST goes through wp_unslash() first, as WordPress asks; text that was handed over still slashed was stored right by accident and is now stored with its slashes. Code that put slashes on to get round the loss takes them off. Neither the Muva nor the Perego site's own code calls either class, as read in both checkouts on 2026-10-13. Nothing to rebuild: no screen changed, and no class or route did.
  • Rows imported on CoreX → Data before this release, from a file with a backslash in it, were imported without it, and comments, editorial notes and names saved before it are stored without theirs. Nothing repairs them: a name or a note is written again, and an imported row is corrected where it was imported to.
  • A site on a Windows host has WebP records that name no file. wp corex media reset-webp finds nothing to delete for a picture converted before this release, and deleting such a picture leaves its .webp beside where it was. Read in the command, not run on such a site. A site on a Linux host is not affected.
  • A kit of your own that hands BlueprintActivator::seedPages() a page hands it the title and the markup as they are. A page it stored before this release with a block's attributes in it is stored without their backslashes: apply the kit again and choose to replace the page.
  • POST corex/v1/account/register answers 422 with the code invalid_username for a username it used to change and accept: one with an accent, markup, a backslash, a quote, or any character other than unaccented letters, digits, spaces and _ . - @. A page of your own that posts to the route shows the message it is given, as the account block does. An account registered before this release under a changed username keeps it: its owner signs in with the address, or with the username as it was stored.
  • A member whose password has a quote or a backslash in it, and who chose it on the account block, is signed out of their other browsers once: the next time they sign in on the account block, when their account is moved (DECISIONS #334). WordPress makes a browser's cookie from the stored password, so it does the same after any password change; the add-on ends those sessions, so that the account's list shows only ones that work. A password-reset link that was waiting for that account stops working, and whatever listens on WordPress's wp_set_password action is told, as read in WordPress. Nobody has to do anything. A member with such a password who does not sign in on the account block again is not moved, and wp-login.php goes on refusing them until they reset it.
  • Code of your own that listens on authenticate is handed a password by the account block's sign-in with a level of slashes on it, as it always was by wp-login.php. Read, not run with such code.
  • Corex\Profile\Account\AuthGateway has a new method, acceptsUsername(). A class of your own that implements the interface has to add it. Nothing under sites/ in the Muva, Perego or Mizzey checkouts names the interface, as read on 2026-10-13. Nothing to rebuild: no screen, script or style changed.
  • A subject can hold <, > and & together now, in a reply and in an email sent from Email Studio's Test send and Resend. A design of your own registered on corex_email_design is handed the subject as plain text, as it always was told ($content->subject: "Escape it before you print it"), and a design made by wp corex make:email-design escapes it. One that prints the subject without escaping it has to escape it: until now a subject with a < in it arrived written as entities, or without what stood between the angle brackets, and no longer does. Code of your own that reads a reply's subject, an attempt's or a captured email's, and prints it into a page escapes it there too.
  • An email made from an Email Studio template can have <, > and markup in its subject now, from the template's own subject or from a value merged into it. Until now both were stripped. Code of your own that prints such an email's subject into a page (an attempt's, a captured email's) escapes it there, as above. A template whose subject merges a value that was written as markup, a field kept as HTML say, now shows the tags in the subject: merge a plain value there. A merged subject longer than 300 characters is cut at 300; it was sent at any length.
  • New class Corex\Mail\SubjectLine (SubjectLine::of($typed)), for code of your own that takes a subject somebody typed. Nothing to rebuild: no screen changed.
  • A reply's email looks different at the top: the header is inside the card, the logo is at most 48 pixels tall, and a square logo has the site's name beside it. A site whose logo is an SVG now shows its site icon or its name there: set a PNG under CoreX → Settings → Mail → Email logo to show a logo. A site that registered its own design on corex_email_design is untouched. A design generated before this release keeps the header it was written with; to take the new one, copy the header from a newly generated file.
  • Corex\Email\Design\EmailBrand gains logoWidth, logoHeight, withLogoSize(), logoBox() and logoIsItsName(), and two optional last constructor arguments. A brand built in a corex_email_brand filter says its logo's size with withLogoSize(). A design of your own may draw its logo from logoBox(); one that does not is unaffected.
  • New route: POST corex/v1/submissions/{id}/replies/{reply}/send, with no body. It answers as the reply route does, with draft_ended beside recorded. Each of a submission's replies gains can_send_again. SubmissionReplyStore gains replace(int $submissionId, SubmissionReplyRecord $was, SubmissionReplyRecord $now): void: a site that implements that store itself has to add it. A reply being sent again has, for as long as that takes, a last attempt in state sending with an empty attempt_id.
  • Corex\Mail\SubmissionEmailGateway::reply() takes a fourth, optional argument, a Corex\Mail\ReplyConversation, and the interface gains conversation(string $recipient, array $attemptIds): ReplyConversation. A site that implements this seam itself has to add both. A site that uses CoreX's email add-on, or none, has nothing to do.
  • A site that sends its own confirmation email through Corex\Mail\Mailer can have replies continue from it: where it sends it, call SubmissionConversation::began($submissionId, $request) with the MailRequest it sent. It records an email only where the submission's sender is one of the request's recipients. The guide "Mail" shows the listener. Without it, replies to that site's submissions are new emails until the first reply, which the next one then continues. What it records is post meta on the submission, corex_submission_conversation: code of your own that copies or removes a submission's personal data has to include it.
  • POST corex/v1/submissions/{id}/reply takes thread, conversation or new; left out, the reply is a new email, as before. A submission's detail gains reply_conversation ({ available, subject }), and each of its replies a thread.
  • An email's Message-ID is now CoreX's, <10 to 20 characters@your site's host>. A mail provider that replaces it with its own (some sending services do) sends the email as before, and replies to it cannot join it.
  • CoreX's own listener on phpmailer_init now runs at priority 9999, after the listeners that set a mail server up, where it ran at 10. A listener of your own at a later priority that changes how an email is sent is not seen by it.
  • An email CoreX sends with a plain-text version or an id carries a header X-Corex-Send as far as WordPress's mailer, where CoreX takes it off. A mail plugin that sends without firing phpmailer_init sends that header with the email; it holds a value made for that one email and nothing else.
  • A submission's detail gains reply_subject, and SubmissionReplyView::of() takes the submission's record where it took its id.
  • New routes: POST and DELETE corex/v1/submissions/{id}/reply/draft, and a submission's detail gains reply_draft ({ subject, body, saved_at, thread } or null) for the person asking; the route takes thread too. A draft is post meta on the submission, corex_reply_draft_{user id}. Code of your own that copies or removes a submission's personal data has to include it, as it does corex_submission_replies.
  • SubmissionControllerServices takes a SubmissionReplyDrafts, and SubmissionEmailService an optional SubmissionReplyDraftStore; SubmissionReplyView::of() also takes the id of the person asking. Code that takes them from the container is unaffected.
  • The admin bundle changed: rebuild plugins/corex-config after merging.
  • The page that shows a new preview link is marked up differently. The link is the value of the field #corex-preview-link, where it was the text of a paragraph in .corex-standalone__detail. A browser test or a script of your own that read it there has to read the field. Two strings in the corex text domain are new: "Copied." and "It could not be copied for you. It is selected: copy it from the keyboard." The field's label and the button use "Preview link" and "Copy link", which the domain already had. Nothing to set and nothing to rebuild for it: the page carries its own script.
  • A script that calls corex/v1/access/... is answered 400 where it was answered 500. One that treats any status outside 2xx as "not done" needs no change. One that retries on 5xx stops retrying a request that could never succeed. The codes are listed under "What the REST routes answer" in docs/en/03-operations/security.md. A site's own PHP that catches InvalidArgumentException from AccessService or AccessPolicy still catches: they now throw Corex\Access\InvalidAccessInput, which extends it. Read on 2026-10-13: nothing under sites/ in the Muva or Perego checkouts names the routes or either class (DECISIONS #350).
  • A site installed from the shared-host package should have no wp-content/.env. That is the only place such a site's .env is read from, and a web server gives the file to anybody who asks for it unless it was told not to. If one is there, move its values to options and delete it. Nothing in this release changes where the file is read from (DECISIONS #368).