Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ under one policy. Do not add a restore path that skips it:
- Versions: what the site ran (`previousVersions()`: the lock while it still lists them, else
`core/custom/composer.installed.json`, written by every restore and by `package:installrequire`),
pinned by `pins()`; a package the release ships or the core requires keeps the release's version.
A branch from git or hg is pinned at its commit (`dev-main#<commit>`). A branch without a source keeps
the dist reference Composer prints for it (`dev-main <reference>`, a path repository's hash or HEAD), to
compare only: `composerVersion()` drops it before anything reaches Composer. A branch whose reference is
not known on both sides counts as a change, so it needs consent.
- Arguments: `updateArguments()`, `--with-dependencies` (`--minimal-changes` from Composer 2.7), never
`--with-all-dependencies`, which moves the core's own packages too.
- `plan()` dry run first (`planInstall()`, a `composer install --dry-run`, when nothing needs restoring:
Expand Down
2 changes: 1 addition & 1 deletion assets/modules/store/installer/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
$_lang = array();
$_params = array();
$lang = evo()->config['manager_language'];
if (file_exists($modulePath . '/lang/' . $lang . '.inc.php')){
if (\EvolutionCMS\Support\ManagerLanguage::isName($lang) && file_exists($modulePath . '/lang/' . $lang . '.inc.php')){
include_once($modulePath . '/lang/' . $lang . '.inc.php');
} else {
include_once($modulePath . '/lang/en.inc.php');
Expand Down
2 changes: 1 addition & 1 deletion assets/plugins/extrascheck/CheckOutdated.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ public function makeConfigButton($title)
public function setLang($lexicon = [])
{
$lang = $this->modx->getConfig('manager_language');
if (file_exists( __DIR__ . '/lang/'.$lang.'/core.inc.php')){
if (\EvolutionCMS\Support\ManagerLanguage::isName($lang) && file_exists( __DIR__ . '/lang/'.$lang.'/core.inc.php')){
include_once(__DIR__ . '/lang/'.$lang.'/core.inc.php');
} else {
include_once(__DIR__ . '/lang/english/core.inc.php');
Expand Down
5 changes: 3 additions & 2 deletions assets/plugins/updater/plugin.updater.php
Original file line number Diff line number Diff line change
Expand Up @@ -852,8 +852,9 @@ function poll() {
$_lang = [];
$plugin_path = EVO_BASE_PATH . "assets/plugins/updater/";
include($plugin_path . 'lang/en.php');
if (file_exists($plugin_path . 'lang/' . $modx->config['manager_language'] . '.php')) {
include($plugin_path . 'lang/' . $modx->config['manager_language'] . '.php');
$language = $modx->config['manager_language'];
if (\EvolutionCMS\Support\ManagerLanguage::isName($language) && file_exists($plugin_path . 'lang/' . $language . '.php')) {
include($plugin_path . 'lang/' . $language . '.php');
}

$e = &$modx->Event;
Expand Down
19 changes: 19 additions & 0 deletions core/src/Core.php
Original file line number Diff line number Diff line change
Expand Up @@ -6822,6 +6822,7 @@ public function getSettings()
$this->config = $this->configCompatibility();

$this->loadConfig();
$this->dropManagerLanguagePath();

// Apply the system timezone before user settings and OnLoadSettings hooks.
$siteTimezone = Support\SiteTimezone::apply(
Expand All @@ -6841,6 +6842,19 @@ public function getSettings()
$this->syncManagerTheme();
}

/**
* manager_language names a file to include, here and in packages that read it as it
* stands, and a stored value may be anything: only a locale name gets into the
* configuration. A deleted locale passes, it reads as English where it is used.
*/
private function dropManagerLanguagePath(): void
{
$language = $this->config['manager_language'] ?? '';
if ($language !== '' && !Support\ManagerLanguage::isName($language)) {
$this->config['manager_language'] = Support\ManagerLanguage::DEFAULT;
}
}

/**
* Realign the already resolved ManagerTheme with the current manager_theme and
* manager_language. A different theme needs a new instance (the theme name drives the
Expand Down Expand Up @@ -6934,6 +6948,11 @@ public function getUserSettings()
}
}

// The same rule as dropManagerLanguagePath(); the system language stays.
if (isset($usrSettings['manager_language']) && !Support\ManagerLanguage::isName($usrSettings['manager_language'])) {
unset($usrSettings['manager_language']);
}

if (empty($usrSettings)) {
return [];
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/Legacy/ErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public function includeLang($context = 'common')
// @phpstan-ignore-next-line includeOnce.fileNotFound
include_once($lang_path . 'en/global.php');
$manager_language = $modx->getConfig('manager_language');
if (is_file($lang_path.$manager_language."/global.php")) {
if (\EvolutionCMS\Support\ManagerLanguage::isName($manager_language) && is_file($lang_path.$manager_language."/global.php")) {
include_once($lang_path.$manager_language."/global.php");
}

Expand Down
66 changes: 53 additions & 13 deletions core/src/Services/CustomComposerPackages.php
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ public function installed(array $packages): bool
/**
* Arguments of the "composer update" that restores the packages.
*
* @param array<string, string> $pins Package name => version ('' leaves the version to Composer).
* @param array<string, string> $pins Package name => pin ('' leaves the version to Composer).
* @param bool $minimalChanges Whether the Composer that runs them knows --minimal-changes (2.7.0+).
* @return string Quoted package list followed by the update options.
*/
Expand All @@ -229,7 +229,7 @@ public function updateArguments(array $pins, bool $minimalChanges): string
}

$package = strtolower((string) $package);
$version = trim((string) $version);
$version = self::composerVersion((string) $version);
$packages[$package] = $version !== '' ? $package . ':' . $version : $package;
}

Expand Down Expand Up @@ -687,14 +687,16 @@ public static function unexpectedChanges(array $operations, array $pins, array $
{
$custom = array_fill_keys(array_map('strtolower', $packages), true);
$sameVersion = static function (string $a, string $b): bool {
// A branch is "dev-main 18f1a03" in one source and "dev-main" in another: a reference
// on one side only says nothing, two different ones mean the branch moved.
[$versionA, $referenceA] = array_pad(explode(' ', strtolower(ltrim(trim($a), 'vV')), 2), 2, '');
[$versionB, $referenceB] = array_pad(explode(' ', strtolower(ltrim(trim($b), 'vV')), 2), 2, '');

return $versionA === $versionB
&& ($referenceA === '' || $referenceB === ''
|| str_starts_with($referenceA, $referenceB) || str_starts_with($referenceB, $referenceA));
// A branch is "dev-main#18f1a03..." or "dev-main 18f1a03..." as a pin and "dev-main 18f1a03" as Composer prints it.
// A reference on one side only cannot show the branch stayed where it was: that needs consent.
$parts = static fn (string $version): array => array_pad(preg_split('/[ #]/', strtolower(ltrim(trim($version), 'vV')), 2), 2, '');
[$versionA, $referenceA] = $parts($a);
[$versionB, $referenceB] = $parts($b);
if ($versionA !== $versionB || ($referenceA === '') !== ($referenceB === '')) {
return false;
}

return $referenceA === '' || str_starts_with($referenceA, $referenceB) || str_starts_with($referenceB, $referenceA);
};

$changes = [];
Expand All @@ -714,6 +716,14 @@ public static function unexpectedChanges(array $operations, array $pins, array $
return $changes;
}

/**
* The part of a pin Composer is given: what follows a space is only compared (pinnedVersion()).
*/
public static function composerVersion(string $pin): string
{
return explode(' ', trim($pin), 2)[0];
}

public static function isPackageName(string $package): bool
{
return preg_match('~^[a-z0-9][a-z0-9_.-]*/[a-z0-9][a-z0-9_.-]*$~i', $package) === 1;
Expand All @@ -738,7 +748,7 @@ private static function satisfies(string $version, string $constraint): bool
/**
* Packages of core/composer.lock with their versions and requirements.
*
* @return array<string, array{version: string, require: list<string>}>
* @return array<string, array{version: string, pin: string, require: list<string>}>
*/
private function lockPackages(): array
{
Expand All @@ -755,6 +765,7 @@ private function lockPackages(): array
if ($name !== '') {
$packages[$name] = [
'version' => (string) ($package['version'] ?? ''),
'pin' => self::pinnedVersion($package),
'require' => array_map('strtolower', array_keys((array) ($package['require'] ?? []))),
];
}
Expand All @@ -764,6 +775,35 @@ private function lockPackages(): array
return $packages;
}

/**
* The version to bring a locked package back at. "dev-main" alone is wherever the branch
* stands by then, so a branch from git or hg keeps its commit: "dev-main#18f1a03...".
*
* A branch without a source (a path repository) cannot be asked for a commit, but Composer
* prints its dist reference: "dev-main 18f1a03...", kept the same way to compare with what
* a dry run reports and never handed to Composer (composerVersion()). Any other source
* prints no reference, so there is nothing to compare.
*
* @param array<string, mixed> $package Entry of composer.lock.
*/
private static function pinnedVersion(array $package): string
{
$version = (string) ($package['version'] ?? '');
if (!str_starts_with($version, 'dev-') && !str_ends_with($version, '-dev')) {
return $version;
}

$source = (array) ($package['source'] ?? []);
$reference = (string) ($source['reference'] ?? '');
if (in_array($source['type'] ?? '', ['git', 'hg'], true) && $reference !== '') {
return $version . '#' . $reference;
}

$distReference = (string) (((array) ($package['dist'] ?? []))['reference'] ?? '');

return (string) ($source['type'] ?? '') === '' && $distReference !== '' ? $version . ' ' . $distReference : $version;
}

/**
* Packages core/composer.json requires itself; they keep the core's version.
*
Expand All @@ -785,7 +825,7 @@ private function coreRequirements(): array
/**
* A package and what it requires in the lock, without the core's own requirements.
*
* @param array<string, array{version: string, require: list<string>}> $lock
* @param array<string, array{version: string, pin: string, require: list<string>}> $lock
* @param string $root Package name.
* @param array<string, true> $core
* @return array<string, string> Package name => version.
Expand All @@ -799,7 +839,7 @@ private function dependencyClosure(array $lock, string $root, array $core): arra
if (isset($versions[$name]) || !isset($lock[$name]) || ($name !== $root && isset($core[$name]))) {
continue;
}
$versions[$name] = $lock[$name]['version'];
$versions[$name] = $lock[$name]['pin'];
foreach ($lock[$name]['require'] as $dependency) {
if (self::isPackageName($dependency)) {
$queue[] = $dependency;
Expand Down
3 changes: 2 additions & 1 deletion core/src/Services/Store/StoreContextService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use EvolutionCMS\Models\RolePermissions;
use EvolutionCMS\Models\UserRole;
use EvolutionCMS\Support\ManagerLanguage;

class StoreContextService
{
Expand All @@ -16,7 +17,7 @@ public function loadLanguage(string $modulePath, string $managerLanguage): array
if (file_exists($fallbackFile)) {
include $fallbackFile;
}
if ($languageFile !== $fallbackFile && file_exists($languageFile)) {
if ($languageFile !== $fallbackFile && ManagerLanguage::isName($managerLanguage) && file_exists($languageFile)) {
include $languageFile;
}

Expand Down
Loading