Skip to content

Sync 3.5 to 3.6 - #2577

Merged
Seiger merged 77 commits into
evolution-cms:3.6.xfrom
elcreator:sync-3.5-to-3.6
Nov 9, 2026
Merged

Seiger merged 77 commits into
evolution-cms:3.6.xfrom
elcreator:sync-3.5-to-3.6

Conversation

@elcreator

@elcreator elcreator commented Nov 9, 2026 •

Copy link
Copy Markdown

Brings everything merged into 3.5.x since the last sync into 3.6.x, together with #2572, which is still open against 3.5.x.

3.5.x PRs included: #2544 #2549 #2550 #2551 #2554 #2556 #2558 #2562 #2565 #2566 #2567 #2568 #2569 #2570 #2571 #2573 #2574 #2575 #2576, plus #2572.

Conflicts and how they were resolved

  • core/composer.lock: 3.6 keeps its own lock. Its symfony 8.1.11 already includes the fixes for the 2026-11-04 advisories that 3.5 took in b0c2f3a.
  • core/vendor/*: git-ignored on 3.6, so it stays deleted.
  • SiteUpdateCommand: the update now runs inside ComposerEnvironment (3.5, fe8dd19), which sets COMPOSER_HOME and COMPOSER_AUTH before the overlay. 3.6's configureComposerHome() did nothing after that, so it is removed along with its two tests; ComposerEnvironmentTest covers their cases (legacy site auth, implicit or explicit home, a worker without a home).
  • files.php, files.dynamic.php, shared-type-root-request.php: the file manager root comes from fileManagerRoot() (fix(manager): contain a per-user filemanager_path within the system root #2571).

3.6-specific follow-ups

  • SystemFilesChecksum::RELEASED lists 3.6's index.php as the current shipped copy (manager/index.php is the same as 3.5's), so an update from any 3.5 release to 3.6 does not report it as modified.
  • The save_settings test fixture provides the ManagerTheme facade, which the 3.6 processor uses to remove the compiled theme stylesheet.

Verification

  • Merges into the current 3.6.x without conflicts.
  • Full suite in evo-ci-php:8.4: 2674 passed, 12 skipped.
  • PHPStan: no errors, as on 3.6.x.
  • The save_settings tests also pass on MariaDB 11.4 (utf8mb4_general_ci and utf8mb4_unicode_ci).
  • Every 3.x release copy of index.php and manager/index.php is in RELEASED, and the last entries match this tree.

🤖 Generated with Claude Code

Seiger and others added 30 commits November 2, 2026 15:43
…ns-alignment

Fix action buttons alignment
The shared-root mapping read KCFinder's typeURL, which resolves a relative
rb_base_url against the browser script (or gives up when DOCUMENT_ROOT is a
symlink), so it never named the folder: restricted managers got an empty,
read-only browser on shared storage. Use the configured URL instead.

A type root the site serves from outside a narrowed filemanager_path carries
no file groups again instead of refusing every action.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…managers

Links below a file manager root are made by the operator, never by the CMS:
a shared upload folder, a plugin checked out elsewhere, images from another
disk. A link whose real target exists and is new (not inside or around the
ACL root, not a loop, not inside anything the walk already named or a shared
type folder) is now a folder or file like any other, named by its own path,
with every rule applied by that name: protected folders, file groups, the
executable-name rule and the media browser type folder. Links that dangle,
loop or would give a file a second name stay inert entries, and `..` past a
link is refused.

Both managers key a shared type folder the same way, so the classic manager
can now enter it and edit its groups; the link that stands for a type folder
is never deleted, renamed or unlinked. Removing any other passable link
removes the link only and keeps its groups.

A new fixture builds the media browser the way a web request does ($_SERVER,
the working directory of browse.php, config.php), through a public_html ->
evolution -> release chain, which is how the typeURL bug slipped through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ough a linked file

A passed link keeps its own name, which file groups are keyed by, but the
protected-folder rule compared only that name. A link to a folder protected
outside the ACL root (an external view path; core/, manager/ or views/ under
a narrowed filemanager_path) opened it to managers without the permission.
fileManagerPathIsProtected() and fileManagerPathTouchesProtected() now also
match where a path lands: the real path of its nearest existing ancestor with
the rest appended, so new destinations are covered too. Removing a link
resolves only the folder it sits in.

textsave() checked a linked file by its name while file_put_contents()
followed the link, so a manager without code permissions could write PHP into
the target through a text name. It now refuses a final link through
fileManagerIsSafeWriteTarget(), as the classic upload over an existing name
does now too (a move across file systems copies, and the copy follows a link).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sions-for-shared-storage-3.5

fix(file-manager): shared storage and operator links in both file managers (3.5.x)
(cherry picked from commit 8e43927)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r-eof-3.5

fix(installer): stop CLI input loops on EOF
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The update widget writes update.php to the site root and opens it, and the
catch-all of ng.inx answered 404, so the update could not start on nginx.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ames its commit

The lock gives "dev-main 18f1a03" and Composer prints "dev-main", so every
update of a site with a package on a branch stopped and asked for consent.
Two different references still count as a change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Docker Swarm names services project_service, and the hostname filter refused
them, so the installer could not reach a database in such a stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…base

The name check refused "/", so a database on a mounted folder could not be
chosen in the browser. It stays a SQLite thing, and dots stay refused, so the
name cannot leave core/database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
REMOTE_ADDR behind a proxy is the proxy, so the address check of the lock
compared one value for everyone, and on the same host that value is loopback,
which may take over a lock: a stranger could take a running install. The lock
now keeps the forwarded client next to REMOTE_ADDR. Without a proxy nothing
changes, and the header cannot be used to pass for another address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ment overrides

The connection config asks env('DB_*') first, so what the form said about
those settings was dropped without a word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s release

An update keeps the site's .htaccess, so a site installed before a rule was
added stays without it: on 3.5.8 updated to 3.5.9 dumps under assets could be
read and PHP under assets ran. The file stays the operator's: make:site and
the dashboard name what is missing, and the warning can be hidden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
symfony/filesystem, http-foundation, http-kernel and routing go to 7.4.23.
composer audit found seven advisories in them, CVE-2026-118516 among them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sories-3.5

fix(deps): update symfony packages for the advisories of 2026-11-04
…ion-cms#2553)

The save redirected to the user list, which needs edit_user, so a role
without it saw "You don't have enough privileges" and lost the tab even
though the password had changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The form changes the viewer's own password only, yet a second manager
opening it while another had it open was refused with the privilege
error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A role with delete_user but not edit_user deleted the user and then got
the privilege error from the list. It now lands on the dashboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ord-privilege-error-3.5

fix(manager): stop the false privilege error after changing a password
…moval-3.5

fix(installer): make "remove the install folder" work
The lock kept only "dev-main", so a restore resolved the branch wherever it
stood by then, and treating a commit on one side as equal let that pass
without consent. The commit now travels with the pin (dev-main#<commit>),
through the lock and the record, and a branch whose commit is not known on
both sides counts as a change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user editor stores any value and the settings merge took it as it was.
The manager theme normalises it, but a manager on the front end kept it, and
it reached include in the error handler, the updater and extras plugins and
the store. Only a locale name now gets into the configuration, which also
covers packages that read the setting, and those includes ask for one too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Composer prints the dist reference of a dev package that has no source, so an
unchanged path package read as a change and needed consent on every update.
Keep that reference in the pin and the record to compare, and drop it before
anything reaches Composer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seiger and others added 22 commits November 9, 2026 11:22
…isher-default-preset-3.5

Drop settings and bk_manager from the default Publisher preset
…r-composer-auth

fix(updater): preserve Composer authentication during preflight
…h-publish-permission-3.5

# Conflicts:
#	core/tests/Unit/Security/ManagerAuthorizationGapsTest.php
… schemes

The Preview button decided sandbox vs. window by a text prefix of the site URL, so
same-origin links such as /page, //host/page, a different letter case or a path
outside a subdirectory installation opened unsandboxed, and a javascript: weblink
ran in a same-origin window. preview-target-helper.js now parses the target against
the site URL, sandboxes anything on the site's or the manager's origin, opens only
external http(s) links (noopener) and refuses every other scheme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n-use-delete-3.5

# Conflicts:
#	core/src/Controllers/UserRoles/UserRole.php
The delete now asks delete_role and the reach guard before the in-use rule, so the
behavioral harness supplies getCore() with the actor's permissions, the session role
and the permissions table. Covers an in-use role (refused, permissions kept), an unused
role (deleted), a missing delete_role and a role beyond the actor's reach.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dule sync

A document with pub_date and unpub_date both past matched the pending-publish rule of
the front-end sync and of the refresh threshold, so every sync published it and then
unpublished it again, and the threshold stayed due: the writes, events and page cache
drops repeated on every request. Both now use SiteContent::publishDocuments() /
unPublishDocuments(), the scopes Refresh site already applies, so such a window ends
unpublished without the flip and leaves the threshold alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n path rule

Core judged a manager's own filemanager_path by realpath(), but the file managers name
paths by fileManagerCanonicalPath(), which refuses spellings realpath() accepts (a ".."
back over the root, a link that names a folder a second time). Both file managers then
replaced the refusal with the site folder, wider than a narrowed site-wide root.
fileManagerRoot() validates the override with the consumer's rule and is the value the
classic manager, its ACL keys and the media browser's top-level rule use; it falls back
only to the site-wide root, and refuses when that does not resolve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ttings

sys_files_checksum was computed from check_files_onlogin and stored before the
security_settings check, and it is not a key the tab renders, so a manager without the
permission still rewrote it, and a save with the tab hidden read a missing field. It is
now computed only for a manager with the permission (from the posted list, else the
stored one) and dropped from the request otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-filemanager-path-containment-3.5

fix(manager): contain a per-user filemanager_path within the system root
…ew-sandbox-3.5

fix(manager): sandbox the editor resource preview
…esh-publish-permission-3.5

Require publish_document to run the due (un)publish on Refresh site
…-in-use-delete-3.5

Refuse deleting a role that is still assigned to users
…ttings

MySQL and MariaDB match setting_name by collation (utf8mb4_general_ci by
default), so a crafted USE_UDPERMS or Allow_Eval passed the case-sensitive
security_settings check and then updated the use_udperms / allow_eval row.
A posted name is now stored only when it is plain ASCII and, if a stored,
factory, gated or derived setting matches it case-insensitively, exactly
that spelling; isGated() ignores case, and every spelling of the derived
sys_files_checksum is dropped from the request.

The save_settings tests run the real processor on SQLite with NOCASE and,
when EVO_TEST_MYSQL_HOST is set, on MySQL/MariaDB with utf8mb4_general_ci
and utf8mb4_unicode_ci.

Also: SecuritySettings was missing from the committed classmap, and the
install stub seeders now match the core ones (security_settings for the
administrator, no dead action_ok grants), which PublisherDefaultPresetTest
checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every update replaces index.php and manager/index.php with the release's
copies, and the dashboard then reported both as modified. The dashboard
check now first records a listed file whose stored hash and current hash
are both copies the CMS shipped (SystemFilesChecksum::RELEASED, every copy
since 3.0), the current one this code's. A file anyone edited, before or
after the update, or swapped for an older copy, is still reported. This
works for make:site, the web updater, an overlay or git alike.

The reset link of that warning changed state by GET without a CSRF token;
it now carries one and a=2 ?b= is a guarded parameter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ComposerEnvironment, DocumentCascadeGuard, RoleGuard and UserGroupGuard
came in with recent 3.5.x merges without a classmap entry, so a site on
the committed classmap-authoritative vendor fails with class not found
where they are used. CommittedClassmapTest reports them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts:
- core/composer.lock: 3.6 keeps its own lock; its symfony 8.1.11 already
  carries the fixes of the 2026-11-04 advisories that 3.5 took in b0c2f3a.
- core/vendor/*: git-ignored on 3.6, stays deleted.
- SiteUpdateCommand: the update runs inside ComposerEnvironment (3.5,
  fe8dd19), which sets COMPOSER_HOME and COMPOSER_AUTH before the overlay;
  3.6's configureComposerHome() was a no-op after it and is removed with its
  tests, whose cases ComposerEnvironmentTest covers.
- files.php, files.dynamic.php, shared-type-root-request.php: the file
  manager root comes from fileManagerRoot() (3.5, evolution-cms#2571).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SystemFilesChecksumTest requires this code's copy last; 3.6's index.php
differs from 3.5's, manager/index.php does not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On 3.6 the processor removes the theme's compiled stylesheet through the
ManagerTheme facade, which the stand-alone fixture has no application for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rity-settings-permission-3.5

add(manager): gate the Security settings tab behind a security_settings permission
@MiddleSokilAI

Copy link
Copy Markdown
Collaborator

One upgrade case still looks worth fixing before merging b64350b. In UserRole.php (around lines 247–271), the new role reach checks compare the target role's stored grants with the actor's defined permission keys. Older installations can still have the obsolete role_actionok grant from the previous default seeders. Updating the seed data does not remove that existing row, and I could not find an upgrade cleanup for it.

That can leave a non-administrator role manager unable to edit, delete or assign a legacy default role even when they hold every real permission it grants: the obsolete grant remains on the target side but can never appear among the actor's defined permissions.

Please either migrate/clean up those obsolete grants or consistently restrict the comparison to defined permissions, while preserving the checks for all legitimate permissions. An upgrade regression test should start with the pre-existing obsolete grant, verify the allowed role operations, and still reject operations when the actor lacks a real permission required by the target role.

This is a static review finding; I have not reproduced it at runtime.

SokilAI

Saving, deleting or assigning a role compared the target role's stored
grants with the actor's defined permission keys. Sites installed with the
earlier default seeders still give every default role role_actionok, a key
no permission defines and no update removes, so a role manager holding
every real permission of such a role was refused it. An always-on
permission (action_ok on those sites) stored on the target by a later save
but not on the actor's role blocked the same way.

RoleGuard::reachOf() returns a role's grants of defined permissions that a
role may hold or not, and both reach checks (role save/delete, role
assignment on user save) use it. Every real permission still counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Role reach is judged by defined, optional permissions only, so a legacy
role_actionok grant no longer locks a default role away from a role
manager (review of evolution-cms#2577).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@elcreator

Copy link
Copy Markdown
Author

Thanks, confirmed at runtime: the save was refused. The reach checks come from 3.5.x, so the fix is #2578 against 3.5.x, merged here in 95d7b68.

Confirmed: the default Editor role on an upgraded site holds edit_document, save_document and the obsolete role_actionok. A role manager holding every real permission of that role was refused saving it. The same happened with an always-on permission: a role save stores it on the target, and on those sites action_ok is still defined as always-on. When the actor's role was saved before that, the actor lacked it.

Fix: I restricted the comparison rather than adding a cleanup migration, so it covers any grant left without a definition, not just role_actionok. RoleGuard::reachOf($roleId) returns only a role's grants of defined permissions that a role may hold or not (permissions.disabled = 0). Both reach checks use it: role save and delete (guardRoleReach()) and role assignment in EditOrNewUser. Every real permission still counts, and the administrator role stays out of reach.

Upgrade regression test (RoleReachLegacyGrantsTest): it starts with the obsolete role_actionok grant, plus action_ok defined as always-on and granted to the target role, and runs the real controller paths:

  • allowed: save, assign and delete for an actor holding every real permission of the role;
  • refused on all three: when the actor lacks one of the role's real permissions, or when the role also holds settings;
  • the administrator role stays refused.

The allowed case fails on b64350b.

Full suite on this branch: 2680 passed, 12 skipped (3.5.x: 2476 passed). It still merges into 3.6.x without conflicts.

@Seiger
Seiger merged commit 86de1af into evolution-cms:3.6.x Nov 9, 2026
@elcreator elcreator mentioned this pull request Nov 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants