diff --git a/.github/docker/ci/web-update.php b/.github/docker/ci/web-update.php new file mode 100644 index 0000000000..6a5ce3f10e --- /dev/null +++ b/.github/docker/ci/web-update.php @@ -0,0 +1,149 @@ +} */ +function request(string $url, string $jar, ?array $post = null): array +{ + $redirects = []; + for ($hop = 0; $hop < 10; $hop++) { + $curl = curl_init($url); + curl_setopt_array($curl, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_COOKIEJAR => $jar, + CURLOPT_COOKIEFILE => $jar, + CURLOPT_HTTPHEADER => ['Accept-Language: en'], + // Same-origin, as a browser on the site sends it: the Manager refuses a login without one. + CURLOPT_REFERER => $url, + CURLOPT_TIMEOUT => 900, + CURLOPT_HEADER => true, + ]); + if ($post !== null) { + curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($post)); + } + $response = (string) curl_exec($curl); + $status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE); + $headerSize = (int) curl_getinfo($curl, CURLINFO_HEADER_SIZE); + $location = (string) curl_getinfo($curl, CURLINFO_REDIRECT_URL); + curl_close($curl); + if ($status < 300 || $status >= 400 || $location === '') { + return ['status' => $status, 'url' => $url, 'body' => substr($response, $headerSize), 'redirects' => $redirects]; + } + $redirects[] = [$status, $location]; + // A redirect after a form post is followed with a GET, as browsers do for 302 and 303. + [$url, $post] = [$location, null]; + } + throw new RuntimeException("too many redirects from $url"); +} + +/** The fields form#install_form submits as it stands, and where it posts them. */ +function installForm(string $html, string $pageUrl): ?array +{ + $document = new DOMDocument(); + @$document->loadHTML($html); + $form = $document->getElementById('install_form'); + if (!$form instanceof DOMElement) { + return null; + } + $fields = []; + foreach ((new DOMXPath($document))->query('.//input|.//select|.//textarea', $form) as $field) { + $name = $field->getAttribute('name'); + if ($name === '' || $field->hasAttribute('disabled')) { + continue; + } + $type = strtolower($field->getAttribute('type')); + if (in_array($type, ['checkbox', 'radio'], true) && !$field->hasAttribute('checked')) { + continue; + } + if ($field->nodeName === 'select') { + $value = ''; + foreach ($field->getElementsByTagName('option') as $index => $option) { + if ($index === 0 || $option->hasAttribute('selected')) { + $value = $option->hasAttribute('value') ? $option->getAttribute('value') : $option->textContent; + } + } + } else { + $value = $field->nodeName === 'textarea' ? $field->textContent : $field->getAttribute('value'); + } + if (str_ends_with($name, '[]')) { + $fields[substr($name, 0, -2)][] = $value; + } else { + $fields[$name] = $value; + } + } + $action = $form->getAttribute('action'); + $target = preg_match('~^https?://~', $action) ? $action : substr($pageUrl, 0, (int) strrpos(strtok($pageUrl, '?'), '/') + 1) . $action; + + return ['action' => $target, 'fields' => $fields]; +} + +function say(string $line): void +{ + fwrite(STDOUT, $line . "\n"); +} + +if ($command === 'grant') { + [$admin, $password] = [$argv[3], $argv[4]]; + request("$base/manager/", $jar); + $login = request("$base/manager/index.php?a=0", $jar, ['username' => $admin, 'password' => $password]); + request("$base/manager/index.php?a=2", $jar); + $granted = str_contains((string) @file_get_contents($jar), 'evo_installer_grant'); + say("manager login: {$login['status']}, installer grant: " . ($granted ? 'held' : 'missing')); + exit($granted ? 0 : 1); +} + +$page = request("$base/install/index.php?action=mode", $jar); +foreach ($page['redirects'] as [$status, $location]) { + say("installer redirected ($status) to $location"); +} +say("installer opened: {$page['status']} " . $page['url']); + +// Each step posts to the next; "update" and the licence are what an administrator chooses. +$choices = ['installmode' => '1', 'chkagree' => '1', 'installdata' => '0', 'language' => 'en']; +for ($step = 0; $step < 8; $step++) { + if ($page['status'] !== 200) { + fwrite(STDERR, substr(strip_tags($page['body']), 0, 2000) . "\n"); + throw new RuntimeException("the installer answered {$page['status']} at {$page['url']}"); + } + if (str_contains($page['body'], 'remove_installer.processor.php')) { + break; + } + $form = installForm($page['body'], $page['url']); + if ($form === null) { + fwrite(STDERR, substr(strip_tags($page['body']), 0, 2000) . "\n"); + throw new RuntimeException("no installer form at {$page['url']}"); + } + $fields = array_replace($form['fields'], array_intersect_key($choices, $form['fields']) + ['installmode' => '1']); + if (str_contains($form['action'], 'action=summary') || str_contains($form['action'], 'action=install')) { + $fields['chkagree'] = '1'; + } + say('submitting ' . $form['action']); + $page = request($form['action'], $jar, $fields); +} + +$text = preg_replace('/\s+/', ' ', strip_tags($page['body'])); +$incomplete = stripos($text, 'Update incomplete') !== false || stripos($text, 'FAILED') !== false; +say('results page: ' . ($incomplete ? 'INCOMPLETE' : 'complete')); +if ($incomplete || !str_contains($page['body'], 'remove_installer.processor.php')) { + fwrite(STDERR, substr($text, 0, 3000) . "\n"); + exit(1); +} diff --git a/.github/docker/ci/web-upgrade-and-smoke.sh b/.github/docker/ci/web-upgrade-and-smoke.sh new file mode 100644 index 0000000000..a78c8ef4cb --- /dev/null +++ b/.github/docker/ci/web-upgrade-and-smoke.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# Updates an older Evolution CMS to this tree the way the manager's updater does, and proves the +# web installer gets the site back on its feet. +# +# The updater lays a source archive over the site - this tree's tracked files, no vendor/ - and +# keeps the site's core/vendor, with the packages and the classmap-authoritative autoloader of the +# version it ran. The new core cannot boot on that: classes it added are missing from the classmap, +# and packages its lock moved are still at their old versions. The web installer has to bring +# core/vendor to the new lock before any step boots the new core (install/index.php, +# installerDependenciesStale()), then finish the update through its own forms. +# +# EVO_FROM_DIR=/path/to/older/tree EVO_NEW_DIR=/path/to/this/tree ./web-upgrade-and-smoke.sh +# +# EVO_CUSTOM_PACKAGE=1 also give the older site a package of core/custom/composer.json +# (a path repository, so no network is needed for it) +# COMPOSER=... how to run Composer (default: composer) +# +# The older tree is installed into in place and is left dirty, so hand it a checkout that can be +# thrown away, never a working tree. sqlite only: the subject is Composer, not the database. + +set -euo pipefail + +NEW_DIR=${EVO_NEW_DIR:-$(cd "$(dirname "$0")/../../.." && pwd)} +APP_DIR=${EVO_FROM_DIR:?EVO_FROM_DIR must point at the older tree to update} +COMPOSER=${COMPOSER:-composer} +CUSTOM_PACKAGE=${EVO_CUSTOM_PACKAGE:-0} +PORT=${EVO_HTTP_PORT:-8898} +BASE="http://127.0.0.1:${PORT}" + +export EVO_DB_TYPE=sqlite +export EVO_DB_NAME=${EVO_DB_NAME:-$APP_DIR/core/database/evo-web-upgrade.sqlite} +export EVO_DB_PREFIX=${EVO_DB_PREFIX:-evo_} +export EVO_ADMIN=${EVO_ADMIN:-admin} +export EVO_ADMIN_EMAIL=${EVO_ADMIN_EMAIL:-admin@evo.local} +export EVO_ADMIN_PASSWORD=${EVO_ADMIN_PASSWORD:-Passw0rd123} +export EVO_LANGUAGE=${EVO_LANGUAGE:-en} + +# shellcheck source=lib.sh +. "$(dirname "$0")/lib.sh" + +DRIVER="$NEW_DIR/.github/docker/ci/web-update.php" +SMOKE="$NEW_DIR/.github/docker/ci/smoke.php" +export EVO_SMOKE_BASELINE=/tmp/evo-web-upgrade-baseline.json +rm -f "$EVO_SMOKE_BASELINE" +JAR=$(mktemp) +BIN=$(mktemp -d) + +# The web installer finds Composer on the PATH of the server process, as on a real host. +printf '#!/bin/sh\nexec %s "$@"\n' "$COMPOSER" > "$BIN/composer" +chmod +x "$BIN/composer" + +# What installerDependenciesStale() says about a core directory: the new installer's own verdict. +stale_reason() { + php -r 'require $argv[1] . "/core/vendor/autoload.php"; require $argv[2] . "/install/src/functions.php"; + echo installerDependenciesStale($argv[1] . "/core/") ?? "";' "$APP_DIR" "$APP_DIR" +} + +from_version=$(php -r 'echo (include $argv[1])["version"] ?? "unknown";' "$APP_DIR/core/factory/version.php") +to_version=$(php -r 'echo (include $argv[1])["version"] ?? "unknown";' "$NEW_DIR/core/factory/version.php") + +say "Building the ${from_version} vendor, classmap-authoritative" +(cd "$APP_DIR/core" && "$COMPOSER" install --no-dev --classmap-authoritative --no-interaction --no-progress --no-scripts) +grep -q 'setClassMapAuthoritative(true)' "$APP_DIR/core/vendor/composer/autoload_real.php" \ + || fail "the ${from_version} vendor is not classmap-authoritative" + +say "Installing ${from_version} (sqlite)" +mapfile -t db_args < <(installer_db_args) +(cd "$APP_DIR/install" && php cli-install.php --typeInstall=1 "${db_args[@]}" \ + --cmsAdmin="$EVO_ADMIN" --cmsAdminEmail="$EVO_ADMIN_EMAIL" --cmsPassword="$EVO_ADMIN_PASSWORD" \ + --language="$EVO_LANGUAGE" --removeInstall=n --skipComposer=y < /dev/null > /tmp/web-upgrade-source.log 2>&1) \ + || { tail -30 /tmp/web-upgrade-source.log; fail "the ${from_version} installer failed"; } + +if [ "$CUSTOM_PACKAGE" = 1 ]; then + say "Adding a custom package to ${from_version}" + probe="$APP_DIR/core/custom/packages/ci-probe" + mkdir -p "$probe/src" + printf '{"name": "evo-ci/probe", "version": "1.0.0", "autoload": {"psr-4": {"EvoCi\\\\Probe\\\\": "src/"}}}\n' > "$probe/composer.json" + printf ' "$probe/src/Probe.php" + # The merge plugin resolves path repositories of core/custom/composer.json against core/custom. + printf '{"repositories": [{"type": "path", "url": "packages/ci-probe", "options": {"symlink": false}}], "require": {}}\n' \ + > "$APP_DIR/core/custom/composer.json" + (cd "$APP_DIR/core" && PATH="$BIN:$PATH" php artisan package:installrequire evo-ci/probe '*') \ + || fail "the custom package could not be installed on ${from_version}" + [ -d "$APP_DIR/core/vendor/evo-ci/probe" ] || fail "the custom package is not in the ${from_version} vendor" +fi + +say "Recording what ${from_version} left in the database" +php "$SMOKE" --mode=baseline "$APP_DIR" || fail "the ${from_version} install is not usable as an update source" + +say "Serving the site; the administrator takes the installer grant in the ${from_version} Manager" +PATH="$BIN:$PATH" php -S "127.0.0.1:${PORT}" -t "$APP_DIR" > /tmp/web-upgrade-server.log 2>&1 & +server=$! +trap 'kill "$server" 2>/dev/null || true' EXIT +for _ in $(seq 1 30); do curl -s -o /dev/null "$BASE/" && break; sleep 1; done +php "$DRIVER" grant "$BASE" "$EVO_ADMIN" "$EVO_ADMIN_PASSWORD" "$JAR" || fail "no installer grant from the ${from_version} Manager" + +say "Laying the ${to_version} source over ${from_version}, keeping its core/vendor" +# A source archive: the files of the tree (tracked, or new and not ignored), no vendor. The site keeps its connection and its custom files. +if git -C "$NEW_DIR" rev-parse --git-dir > /dev/null 2>&1; then + git -C "$NEW_DIR" ls-files -co --exclude-standard -z | grep -zv '^core/vendor/' | tar -C "$NEW_DIR" --null -T - -cf - | tar -C "$APP_DIR" -xf - +else + tar -C "$NEW_DIR" --exclude=./.git --exclude=./core/vendor --exclude=./core/custom \ + --exclude=./core/config/database/connections/default.php --exclude='./core/database/*.sqlite' -cf - . \ + | tar -C "$APP_DIR" -xf - +fi +reason=$(stale_reason) +[ -n "$reason" ] || fail "the ${from_version} vendor already fits ${to_version}: nothing under test" +echo "before the update: ${reason}" + +say "Running the web update" +php "$DRIVER" update "$BASE" "$JAR" || { tail -40 /tmp/web-upgrade-server.log >&2; fail "the web update did not complete"; } + +say "Checking core/vendor" +reason=$(stale_reason) +[ -z "$reason" ] || fail "core/vendor still does not fit ${to_version}: ${reason}" +grep -q 'setClassMapAuthoritative(true)' "$APP_DIR/core/vendor/composer/autoload_real.php" \ + || fail "the rebuilt autoloader is not classmap-authoritative" +if [ "$CUSTOM_PACKAGE" = 1 ]; then + grep -q "EvoCi\\\\\\\\Probe\\\\\\\\Probe" "$APP_DIR/core/vendor/composer/autoload_classmap.php" \ + || fail "the custom package did not survive the update" +fi + +kill "$server" 2>/dev/null || true +trap - EXIT + +say "Checking the updated site" +php "$SMOKE" --mode=upgrade "$APP_DIR" || fail "the web update from ${from_version} did not produce a sound site" +http_check "$APP_DIR" "" + +say "OK: ${from_version} -> ${to_version} updated through the web installer$([ "$CUSTOM_PACKAGE" = 1 ] && echo ', custom package kept')" diff --git a/AGENTS.md b/AGENTS.md index 190d9db4af..e06fb0102c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -208,6 +208,26 @@ php artisan vendor:publish --provider="Vendor\\Package\\PackageServiceProvider" php artisan migrate ``` +### Classmap-authoritative autoloader + +`core/composer.json` sets `config.classmap-authoritative`, so every `composer install`, `update`, +`dump-autoload` (and `composer optimize`, which installs without dev packages in one step) writes an +autoloader that never looks for a class outside `vendor/composer/autoload_classmap.php`. Whatever puts new +PHP classes on a site must rebuild it after the files are in place: Composer and the package commands do, +`make:site` does after `moveFiles()`, and so does the web installer after an update through the manager's +updater (`installerInstallComposerDependencies()`, after the custom packages), with the same +`CustomComposerPackages::INSTALL_ARGUMENTS` / `DUMP_AUTOLOAD_ARGUMENTS`; a failure leaves the update incomplete. A class copied in by hand, git or FTP (`core/src`, +`core/custom/packages/*/src`, a package under `vendor/`) does not exist until `composer dump-autoload`. +Do not register namespaces on the Composer loader at runtime: an authoritative loader ignores them. +`tests/` stays out of the classmap; `tests/Pest.php` loads the `Tests\` classes itself, and a script a test +runs in its own process requires the ones it uses. + +A folder a package publishes into (`assets/plugins/`, `assets/modules/`, `assets/snippets/`, +as its provider's `publishes()` declares) is rebuilt from `core/vendor` and stays out of the site's repository: +every artisan run gives it a `.gitignore` of `*` when it has none (`EvolutionCMS\Support\PackageAssetFolders`, +called from `EvolutionCMS\Console` once all providers have booted). Declare such assets through `publishes()`, +even when the package also copies them itself while it boots. + --- ## Customisation Points (core/custom/) @@ -258,6 +278,14 @@ There are ~30 service providers in `core/src/Providers/`. Notable ones: - `TracyServiceProvider` — Tracy debug bar integration - `ModifiersServiceProvider` — output modifiers +A bundled package that serves the manager (EvoUI with Livewire, dTui editor, dDocs, sGallery, sTask) is +declared in `app.on_demand_providers` (`core/config/app.php`) next to its `app.providers` entry: a frontend +request registers it only on its `services` (facade accessors), a class of its namespace or its `events` +(`EvolutionCMS\Support\OnDemandProviders`); a request to any declared route `paths` registers all of them, +because their manager pages need EvoUI and Livewire booted before routing. Manager, console, installer and kept +worker applications register everything. Booting such packages on every page cost the 3.6 frontend about 17 ms, so +declare a new one there too. Test: `core/tests/Unit/OnDemandProvidersTest.php`. + --- ## CMS Concepts for Agents diff --git a/core/composer.json b/core/composer.json index fea320c10f..0e5bfafb20 100644 --- a/core/composer.json +++ b/core/composer.json @@ -79,7 +79,7 @@ "rosell-dk/exec-with-fallback": "*", "rosell-dk/webp-convert": "*", "secondnetwork/blade-tabler-icons": "^3.46", - "seiger/sgallery": "^1.7", + "seiger/sgallery": "^1.8.1", "seiger/stask": "^2.2.1", "symfony/process": "^8.1", "tracy/tracy": "^2.12.2", @@ -100,6 +100,7 @@ }, "config": { "optimize-autoloader": true, + "classmap-authoritative": true, "preferred-install": "dist", "prestissimo": { "maxConnections": 6, @@ -161,8 +162,8 @@ "scripts": { "sync-replace": "@php functions/sync-replace-version.php", "test": "vendor/bin/pest", - "optimize": "@composer install --no-dev && @composer dump-autoload -o --no-dev --classmap-authoritative", - "optimize-dev": "@composer install && @composer dump-autoload -o --classmap-authoritative", + "optimize": "@composer install --no-dev --classmap-authoritative", + "optimize-dev": "@composer install --classmap-authoritative", "upd": [ "@sync-replace", "@composer update --lock --ansi" diff --git a/core/composer.lock b/core/composer.lock index a4e409c8d7..634d6b2f93 100644 --- a/core/composer.lock +++ b/core/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "83121b757dfb51b1dc7f32e2f1326bd1", + "content-hash": "6a76de8270c879b4cf32f8fa217ea3cc", "packages": [ { "name": "blade-ui-kit/blade-icons", @@ -6162,25 +6162,26 @@ }, { "name": "seiger/sgallery", - "version": "v1.7.0", + "version": "v1.8.1", "source": { "type": "git", "url": "https://github.com/Seiger/sGallery.git", - "reference": "e82d8680ea1d24e83363c90382c82cc7ac890a38" + "reference": "18f1a0308a246a8e372cc060625afe6e1112637c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seiger/sGallery/zipball/e82d8680ea1d24e83363c90382c82cc7ac890a38", - "reference": "e82d8680ea1d24e83363c90382c82cc7ac890a38", + "url": "https://api.github.com/repos/Seiger/sGallery/zipball/18f1a0308a246a8e372cc060625afe6e1112637c", + "reference": "18f1a0308a246a8e372cc060625afe6e1112637c", "shasum": "" }, "require": { "evolution-cms/evolution": "^3.3", - "php": "^8.3", - "spatie/image": "3.*" + "php": "^8.3" }, "suggest": { - "seiger/stask": "Optional background image optimization; requires the heartbeat API and an active runner" + "ext-exif": "Automatic camera-photo orientation with the GD image driver", + "seiger/stask": "Optional background image optimization; requires the heartbeat API and an active runner", + "spatie/image": "Install ^3 for image resizing, cropping, format conversion and watermarks; otherwise original files are served" }, "type": "evolutioncms-plugin", "extra": { @@ -6216,7 +6217,7 @@ ], "support": { "issues": "https://github.com/Seiger/sGallery/issues", - "source": "https://github.com/Seiger/sGallery/tree/v1.7.0" + "source": "https://github.com/Seiger/sGallery/tree/v1.8.1" }, "funding": [ { @@ -6224,7 +6225,8 @@ "type": "ko_fi" } ], - "time": "2026-10-15T16:07:38+00:00" + "time": "2026-10-31T17:54:55+00:00", + "published-time": "2026-11-01T19:41:39+00:00" }, { "name": "seiger/stask", @@ -6462,197 +6464,6 @@ }, "time": "2023-09-03T09:24:00+00:00" }, - { - "name": "spatie/image", - "version": "3.9.9", - "source": { - "type": "git", - "url": "https://github.com/spatie/image.git", - "reference": "929c00fdab6c0c929e69524fffd1a658167cbcc2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/image/zipball/929c00fdab6c0c929e69524fffd1a658167cbcc2", - "reference": "929c00fdab6c0c929e69524fffd1a658167cbcc2", - "shasum": "" - }, - "require": { - "ext-exif": "*", - "ext-json": "*", - "ext-mbstring": "*", - "php": "^8.4", - "spatie/image-optimizer": "^1.7.5", - "spatie/temporary-directory": "^2.2", - "symfony/process": "^6.4|^7.0|^8.0" - }, - "require-dev": { - "ext-ffi": "*", - "ext-gd": "*", - "ext-imagick": "*", - "jcupitt/vips": "^2.4", - "laravel/sail": "^1.34", - "pestphp/pest": "^5.0", - "phpstan/phpstan": "^1.10.50", - "spatie/pest-plugin-snapshots": "^2.4", - "spatie/pixelmatch-php": "^1.0", - "spatie/ray": "^1.40.1", - "symfony/var-dumper": "^6.4|^7.0|^8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Spatie\\Image\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://spatie.be", - "role": "Developer" - } - ], - "description": "Manipulate images with an expressive API", - "homepage": "https://github.com/spatie/image", - "keywords": [ - "image", - "spatie" - ], - "support": { - "source": "https://github.com/spatie/image/tree/3.9.9" - }, - "funding": [ - { - "url": "https://spatie.be/open-source/support-us", - "type": "custom" - }, - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2026-10-20T06:00:43+00:00" - }, - { - "name": "spatie/image-optimizer", - "version": "1.10.0", - "source": { - "type": "git", - "url": "https://github.com/spatie/image-optimizer.git", - "reference": "333c03952289dc2df0a91874636a0dffeb5b6aec" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/image-optimizer/zipball/333c03952289dc2df0a91874636a0dffeb5b6aec", - "reference": "333c03952289dc2df0a91874636a0dffeb5b6aec", - "shasum": "" - }, - "require": { - "ext-fileinfo": "*", - "php": "^7.4|^8.0", - "psr/log": "^1.0 | ^2.0 | ^3.0", - "symfony/process": "^4.2|^5.0|^6.0|^7.0|^8.0" - }, - "require-dev": { - "pestphp/pest": "^1.21|^2.0|^3.0|^4.0", - "phpunit/phpunit": "^8.5.21|^9.4.4|^10.0|^11.0|^12.0", - "symfony/var-dumper": "^4.2|^5.0|^6.0|^7.0|^8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Spatie\\ImageOptimizer\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://spatie.be", - "role": "Developer" - } - ], - "description": "Easily optimize images using PHP", - "homepage": "https://github.com/spatie/image-optimizer", - "keywords": [ - "image-optimizer", - "spatie" - ], - "support": { - "issues": "https://github.com/spatie/image-optimizer/issues", - "source": "https://github.com/spatie/image-optimizer/tree/1.10.0" - }, - "time": "2026-06-29T08:28:30+00:00" - }, - { - "name": "spatie/temporary-directory", - "version": "2.4.0", - "source": { - "type": "git", - "url": "https://github.com/spatie/temporary-directory.git", - "reference": "32cbb9645b28839cf4f476708e99a2c70e6802c9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/32cbb9645b28839cf4f476708e99a2c70e6802c9", - "reference": "32cbb9645b28839cf4f476708e99a2c70e6802c9", - "shasum": "" - }, - "require": { - "php": "^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Spatie\\TemporaryDirectory\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Alex Vanderbist", - "email": "alex@spatie.be", - "homepage": "https://spatie.be", - "role": "Developer" - } - ], - "description": "Easily create, use and destroy temporary directories", - "homepage": "https://github.com/spatie/temporary-directory", - "keywords": [ - "php", - "spatie", - "temporary-directory" - ], - "support": { - "issues": "https://github.com/spatie/temporary-directory/issues", - "source": "https://github.com/spatie/temporary-directory/tree/2.4.0" - }, - "funding": [ - { - "url": "https://spatie.be/open-source/support-us", - "type": "custom" - }, - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2026-06-22T07:55:44+00:00" - }, { "name": "symfony/clock", "version": "v8.1.9", @@ -13628,5 +13439,5 @@ "ext-zip": "*" }, "platform-dev": {}, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.11.0" } diff --git a/core/config/app.php b/core/config/app.php index 4b1cd204f2..51adeece2f 100644 --- a/core/config/app.php +++ b/core/config/app.php @@ -82,6 +82,25 @@ 'Evolution_sTask' => Seiger\sTask\sTaskServiceProvider::class, ], + /** + * Manager packages that a frontend request registers only when it uses them, keyed like `providers`: + * on the first resolution of one of their `services` or of a class of their namespace (facades included), + * or when one of their `events` is invoked. A request to any of their `paths` registers them all. + * Return false from custom/config/app/on_demand_providers/.php to register one on every request. + * Livewire comes with EvoUI and stays off on the frontend: a site that renders Livewire components there + * returns false from custom/config/app/on_demand_providers/Evolution_EvoUI.php. + */ + 'on_demand_providers' => [ + 'Evolution_EvoUI' => ['services' => ['EvoUI']], + 'Evolution_DTuiEditor' => [ + 'paths' => ['dtui-plantuml', 'dtui-evo-link-search', 'dtui-image-upload'], + 'events' => ['OnRichTextEditorRegister', 'OnRichTextEditorInit'], + ], + 'Evolution_dDocs' => ['paths' => ['ddocs', 'dtui-plantuml'], 'services' => ['dDocs']], + 'Evolution_sGallery' => ['paths' => ['sgallery'], 'services' => ['sGallery']], + 'Evolution_sTask' => ['paths' => ['stask'], 'services' => ['sTask']], + ], + 'aliases' => [ 'Arr' => Illuminate\Support\Arr::class, 'Artisan' => Illuminate\Support\Facades\Artisan::class, diff --git a/core/src/AbstractLaravel.php b/core/src/AbstractLaravel.php index 6d72451c90..a79f0038c7 100644 --- a/core/src/AbstractLaravel.php +++ b/core/src/AbstractLaravel.php @@ -112,6 +112,11 @@ abstract class AbstractLaravel extends Container implements ApplicationContract */ protected $deferredServices = []; + /** + * Configured providers this frontend request has not registered yet, or null when none wait. + */ + protected ?Support\OnDemandProviders $onDemandProviders = null; + /** * The array of booting callbacks. * @@ -438,17 +443,103 @@ public function resolveProvider($provider) * * Provider availability is checked when rebuilding the service manifest or * registering an eager provider, leaving deferred classes unloaded on warm requests. + * On a frontend request the providers of `app.on_demand_providers` wait until used. */ public function registerConfiguredProviders() { - $providers = Collection::make($this['config']->get('app.providers')) + $configured = (array) $this['config']->get('app.providers', []); + $providers = Collection::make($configured) ->partition(function ($provider) { // The core cache adapter keeps the framework provider's original slot. return Str::startsWith($provider, 'Illuminate\\') || $provider === Providers\CacheServiceProvider::class; }); + $onDemand = null; + if ($this->registersProvidersOnDemand()) { + $onDemand = new Support\OnDemandProviders( + $configured, + (array) $this['config']->get('app.on_demand_providers', []), + $this->onDemandRequestPath() + ); + } + $this->onDemandProviders = $onDemand === null || $onDemand->isEmpty() ? null : $onDemand; + // The manifest keeps the full list: frontend and manager requests share it. (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath())) - ->load($providers->collapse()->toArray()); + ->load($providers->collapse()->toArray(), $this->onDemandProviders?->pending() ?? []); + } + + /** + * Whether this request may leave `app.on_demand_providers` unregistered until used: + * a web request outside the manager and installer, in a process that does not keep its application. + * + * @since 3.6.0 + */ + protected function registersProvidersOnDemand(): bool + { + if ($this->runningInConsole() + || !defined('IN_MANAGER_MODE') || IN_MANAGER_MODE + || (defined('IN_INSTALL_MODE') && IN_INSTALL_MODE)) { + return false; + } + // A kept worker application boots once and serves every later request: it loads them all. + return !(class_exists(Runtime\WorkerRuntime::class, false) && Runtime\WorkerRuntime::keepsApplication()); + } + + /** + * Request path relative to the site root, matched against the `paths` of on-demand providers. + * + * @since 3.6.0 + */ + protected function onDemandRequestPath(): string + { + return Support\OnDemandProviders::requestPath( + (string) ($_SERVER['REQUEST_URI'] ?? '/'), + defined('EVO_BASE_URL') ? (string) EVO_BASE_URL : '/' + ); + } + + /** + * Register the waiting provider that binds a service or owns a class. + * + * @return bool Whether a provider was registered. + * @since 3.6.0 + */ + public function loadOnDemandProviderFor(string $abstract): bool + { + $provider = $this->onDemandProviders?->forService($abstract); + if ($provider === null) { + return false; + } + $this->loadOnDemandProvider($provider); + return true; + } + + /** + * Register the waiting providers whose plugins answer an Evolution event. + * + * @since 3.6.0 + */ + public function loadOnDemandProvidersForEvent(string $event): void + { + foreach ($this->onDemandProviders?->forEvent($event) ?? [] as $provider) { + $this->loadOnDemandProvider($provider); + } + } + + /** + * @since 3.6.0 + */ + protected function loadOnDemandProvider(string $provider): void + { + $this->onDemandProviders?->forget($provider); + if ($this->onDemandProviders?->isEmpty()) { + $this->onDemandProviders = null; + } + if (class_exists($provider)) { + $this->register($provider); + } else { + error_log('[EvolutionCMS] Skipped missing service provider "' . $provider . '"'); + } } /** @@ -630,6 +721,11 @@ public function make($abstract, array $parameters = []) if (isset($this->deferredServices[$abstract]) && !isset($this->instances[$abstract])) { $this->loadDeferredProvider($abstract); } + if ($this->onDemandProviders !== null && !isset($this->instances[$abstract]) + && !isset($this->bindings[$abstract]) && $this->loadOnDemandProviderFor($abstract)) { + // The provider may have aliased the key it was asked for. + $abstract = $this->getAlias($abstract); + } return parent::make($abstract, $parameters); } @@ -643,7 +739,8 @@ public function make($abstract, array $parameters = []) */ public function bound($abstract) { - return isset($this->deferredServices[$abstract]) || parent::bound($abstract); + return isset($this->deferredServices[$abstract]) || parent::bound($abstract) + || ($this->onDemandProviders?->declaresService($abstract) ?? false); } /** diff --git a/core/src/AliasLoader.php b/core/src/AliasLoader.php index ef6dde9abf..bc86a7d971 100644 --- a/core/src/AliasLoader.php +++ b/core/src/AliasLoader.php @@ -66,6 +66,12 @@ public function load($alias) return true; } if (isset($this->aliases[$alias])) { + $app = \Illuminate\Container\Container::getInstance(); + // A waiting frontend provider may declare this alias itself while it boots. + if ($app instanceof AbstractLaravel && $app->loadOnDemandProviderFor($this->aliases[$alias]) + && class_exists($alias, false)) { + return true; + } return class_alias($this->aliases[$alias], $alias); } } diff --git a/core/src/Console.php b/core/src/Console.php index 0ce9e1c401..982e6b3735 100644 --- a/core/src/Console.php +++ b/core/src/Console.php @@ -24,6 +24,10 @@ public function __construct(Container $laravel, Dispatcher $events, $version) $laravel->loadDeferredProviders(); + // Every provider has booted, so every publish target is known: the folders packages publish into stay + // out of the site's repository (make:site, the installers and vendor:publish all run through here). + Support\PackageAssetFolders::ignore(\Illuminate\Support\ServiceProvider::pathsToPublish(), public_path('assets')); + parent::bootstrap(); } diff --git a/core/src/Console/Packages/InstallPackageRequireCommand.php b/core/src/Console/Packages/InstallPackageRequireCommand.php index e633e8d0ab..c96353a3b0 100644 --- a/core/src/Console/Packages/InstallPackageRequireCommand.php +++ b/core/src/Console/Packages/InstallPackageRequireCommand.php @@ -16,7 +16,7 @@ class InstallPackageRequireCommand extends Command /** * Composer release that introduced `update --minimal-changes`. */ - public const MINIMAL_CHANGES_SINCE = '2.7.0'; + public const MINIMAL_CHANGES_SINCE = CustomComposerPackages::MINIMAL_CHANGES_SINCE; /** * The name and signature of the console command. diff --git a/core/src/Console/SiteUpdateCommand.php b/core/src/Console/SiteUpdateCommand.php index 81873a367b..13efd7402c 100644 --- a/core/src/Console/SiteUpdateCommand.php +++ b/core/src/Console/SiteUpdateCommand.php @@ -1244,12 +1244,12 @@ protected function customComposerPackages(): CustomComposerPackages protected function composerInstallCommand(): string { - return $this->composerBinaryCommand() . ' install --no-dev --no-interaction --prefer-dist --optimize-autoloader --classmap-authoritative --no-scripts'; + return $this->composerBinaryCommand() . ' ' . CustomComposerPackages::INSTALL_ARGUMENTS; } protected function composerDumpAutoloadCommand(): string { - return $this->composerBinaryCommand() . ' dump-autoload -o --no-dev --classmap-authoritative --no-scripts'; + return $this->composerBinaryCommand() . ' ' . CustomComposerPackages::DUMP_AUTOLOAD_ARGUMENTS; } protected function normalizeUpdateRepository(string $repository): string diff --git a/core/src/Core.php b/core/src/Core.php index efd40adf6c..884173167e 100644 --- a/core/src/Core.php +++ b/core/src/Core.php @@ -6171,6 +6171,10 @@ public function invokeEvent($evtName, $extParams = []) return false; } + if ($this->onDemandProviders !== null) { + $this->loadOnDemandProvidersForEvent((string) $evtName); + } + $out = $this['events']->dispatch('evolution.' . $evtName, [$extParams]); if ($out === false) { return false; diff --git a/core/src/ProviderRepository.php b/core/src/ProviderRepository.php index 6c6a8c7659..73958d88be 100644 --- a/core/src/ProviderRepository.php +++ b/core/src/ProviderRepository.php @@ -82,9 +82,10 @@ public function __construct(ApplicationContract $app, Filesystem $files, $manife * Existing eager classes load at their registration point, not in a global preflight. * * @param array $providers + * @param array $waiting Eager providers the application registers later, on demand. * @return void */ - public function load(array $providers) + public function load(array $providers, array $waiting = []) { $manifest = $this->loadManifest(); // First we will load the service manifest, which contains information on all @@ -104,10 +105,15 @@ public function load(array $providers) // application so their services can be registered with the application as // a provided service. Then we will set the deferred service list on it. foreach ($manifest['eager'] as $provider) { + if (isset($waiting[$provider])) { + continue; + } if (!class_exists($provider)) { // Rare out-of-band removal: repair the manifest and skip stale declarations. $manifest = $this->compileManifest($providers); - foreach ($manifest['eager'] as $available) $this->app->register($available); + foreach ($manifest['eager'] as $available) { + if (!isset($waiting[$available])) $this->app->register($available); + } break; } $this->app->register($provider); @@ -208,7 +214,6 @@ protected function compileManifest($providers) } else { $manifest['missing'][] = $provider; error_log('[EvolutionCMS] Skipped missing service provider "' . $provider . '"'); - error_log('[EvolutionCMS] Skipped missing service provider "' . $provider . '"'); } } return $this->writeManifest($manifest); diff --git a/core/src/Providers/ManagerThemeServiceProvider.php b/core/src/Providers/ManagerThemeServiceProvider.php index 8deec8b86d..709b631b9d 100644 --- a/core/src/Providers/ManagerThemeServiceProvider.php +++ b/core/src/Providers/ManagerThemeServiceProvider.php @@ -8,10 +8,11 @@ class ManagerThemeServiceProvider extends ServiceProvider { public function boot(): void { - $this->app->make(\EvoUI\EvoUI::class)->registerComponent( - 'evo-core.system-info', - \EvolutionCMS\Livewire\SystemInfo::class - ); + // This provider boots on every request ($defer is not honoured); resolving EvoUI here would load + // EvoUI and Livewire on frontend pages that never use them. + $this->callAfterResolving(\EvoUI\EvoUI::class, static function (\EvoUI\EvoUI $evoUi): void { + $evoUi->registerComponent('evo-core.system-info', \EvolutionCMS\Livewire\SystemInfo::class); + }); } /** diff --git a/core/src/Services/CustomComposerPackages.php b/core/src/Services/CustomComposerPackages.php index c600b89c62..61a3ece40e 100644 --- a/core/src/Services/CustomComposerPackages.php +++ b/core/src/Services/CustomComposerPackages.php @@ -32,6 +32,27 @@ final class CustomComposerPackages */ public const RECORD_FILE = 'custom/composer.installed.json'; + /** + * After the files of an update are in place, make:site and the web installer build core/vendor + * from the release's lock with these, then rebuild the classmap-authoritative autoloader. + * + * @since 3.6.0 + */ + public const INSTALL_ARGUMENTS = 'install --no-dev --no-interaction --prefer-dist --optimize-autoloader --classmap-authoritative --no-scripts'; + + /** + * @since 3.6.0 + */ + public const DUMP_AUTOLOAD_ARGUMENTS = 'dump-autoload -o --no-dev --classmap-authoritative --no-scripts'; + + /** + * Composer version that knows --minimal-changes. Here, not in a console command, because the web + * installer reads it before the new application can boot. + * + * @since 3.6.0 + */ + public const MINIMAL_CHANGES_SINCE = '2.7.0'; + public function __construct(private string $corePath) { $this->corePath = rtrim($corePath, '/\\') . '/'; diff --git a/core/src/Support/OnDemandProviders.php b/core/src/Support/OnDemandProviders.php new file mode 100644 index 0000000000..3fedb9058b --- /dev/null +++ b/core/src/Support/OnDemandProviders.php @@ -0,0 +1,153 @@ + provider class => namespace prefix */ + private array $namespaces = []; + + /** @var array service => provider class */ + private array $services = []; + + /** @var array> event => provider classes */ + private array $events = []; + + /** + * @param array $providers `app.providers` + * @param array $declarations `app.on_demand_providers` + * @param string $path Request path relative to the site root, without leading or trailing slashes. + */ + public function __construct(array $providers, array $declarations, string $path) + { + foreach ($declarations as $declaration) { + if (is_array($declaration) && self::matchesPath($path, (array) ($declaration['paths'] ?? []))) { + return; + } + } + foreach ($declarations as $key => $declaration) { + $provider = $providers[$key] ?? null; + if (!is_array($declaration) || !is_string($provider) || $provider === '') { + continue; + } + $namespace = strrpos($provider, '\\'); + $this->namespaces[$provider] = $namespace === false ? '' : substr($provider, 0, $namespace + 1); + foreach ((array) ($declaration['services'] ?? []) as $service) { + $this->services[(string) $service] = $provider; + } + foreach ((array) ($declaration['events'] ?? []) as $event) { + $this->events[(string) $event][] = $provider; + } + } + } + + /** + * @return array Providers that are not registered yet. + */ + public function pending(): array + { + return array_fill_keys(array_keys($this->namespaces), true); + } + + public function isEmpty(): bool + { + return $this->namespaces === []; + } + + /** + * Whether a pending provider declares this service. + */ + public function declaresService(string $service): bool + { + return isset($this->services[$service]); + } + + /** + * The pending provider that binds a service or owns a class, if any. + */ + public function forService(string $abstract): ?string + { + if (isset($this->services[$abstract])) { + return $this->services[$abstract]; + } + if (str_contains($abstract, '\\')) { + $abstract = ltrim($abstract, '\\'); + foreach ($this->namespaces as $provider => $namespace) { + if ($namespace !== '' && str_starts_with($abstract, $namespace)) { + return $provider; + } + } + } + return null; + } + + /** + * @return list Pending providers whose plugins answer the event. + */ + public function forEvent(string $event): array + { + return $this->events[$event] ?? []; + } + + /** + * Drop a provider once it is registered. + */ + public function forget(string $provider): void + { + unset($this->namespaces[$provider]); + $this->services = array_filter($this->services, static fn (string $owner): bool => $owner !== $provider); + foreach ($this->events as $event => $owners) { + $owners = array_values(array_diff($owners, [$provider])); + if ($owners === []) { + unset($this->events[$event]); + } else { + $this->events[$event] = $owners; + } + } + } + + /** + * A prefix matches the whole path or its leading segments; letter case is ignored. + * + * @param list $prefixes + */ + public static function matchesPath(string $path, array $prefixes): bool + { + foreach ($prefixes as $prefix) { + $prefix = trim((string) $prefix, '/'); + $length = strlen($prefix); + if ($prefix !== '' && strncasecmp($path, $prefix, $length) === 0 + && (strlen($path) === $length || $path[$length] === '/')) { + return true; + } + } + return false; + } + + /** + * Path of a request URI relative to the site base URL. + */ + public static function requestPath(string $uri, string $baseUrl): string + { + $path = rawurldecode((string) parse_url($uri, PHP_URL_PATH)); + $base = '/' . trim($baseUrl, '/'); + if ($base !== '/' && strncasecmp($path, $base, strlen($base)) === 0 + && (strlen($path) === strlen($base) || $path[strlen($base)] === '/')) { + $path = substr($path, strlen($base)); + } + return trim($path, '/'); + } +} diff --git a/core/src/Support/PackageAssetFolders.php b/core/src/Support/PackageAssetFolders.php new file mode 100644 index 0000000000..18c73b7c42 --- /dev/null +++ b/core/src/Support/PackageAssetFolders.php @@ -0,0 +1,88 @@ +, assets/modules/, assets/snippets/. + * + * Their content is rebuilt from core/vendor (vendor:publish, or the package itself while it boots, as the dTui + * editor and EvoUI do), so it is no part of a site's repository. Each such folder gets a .gitignore that ignores + * the whole folder, unless it has one already; the folder is created for it, so that a package filling it later + * still lands in an ignored folder. A site's own plugins are never touched: only publish targets count, and a + * folder that is a link is left alone. + * + * @since 3.6.0 + */ +final class PackageAssetFolders +{ + public const TYPES = ['plugins', 'modules', 'snippets']; + + public const GITIGNORE = "# Published by a Composer package (vendor:publish or the package itself): rebuilt from core/vendor.\n*\n"; + + /** + * Package folders among publish targets. + * + * @param array $publishes Source => target, as ServiceProvider::pathsToPublish() returns it. + * @param string $assetsPath The site's assets folder. + * @return list Absolute folder paths, with forward slashes. + */ + public static function folders(array $publishes, string $assetsPath): array + { + $assets = rtrim(str_replace('\\', '/', $assetsPath), '/') . '/'; + $folders = []; + foreach ($publishes as $source => $target) { + if (!is_string($target)) { + continue; + } + $path = str_replace('\\', '/', $target); + if (!str_starts_with($path, $assets)) { + continue; + } + $parts = explode('/', trim(substr($path, strlen($assets)), '/')); + if (count($parts) < 2 || !in_array($parts[0], self::TYPES, true) || in_array($parts[1], ['', '.', '..'], true)) { + continue; + } + // assets/plugins/ itself is a folder only when a folder is published there, not a file. + if (count($parts) === 2 && !is_dir(self::sourcePath((string) $source))) { + continue; + } + $folders[$assets . $parts[0] . '/' . $parts[1]] = true; + } + + return array_keys($folders); + } + + /** + * Give every package folder among the publish targets its .gitignore. + * + * @param array $publishes Source => target, as ServiceProvider::pathsToPublish() returns it. + * @param string $assetsPath The site's assets folder. + * @return list The .gitignore files written. + */ + public static function ignore(array $publishes, string $assetsPath): array + { + $written = []; + foreach (self::folders($publishes, $assetsPath) as $folder) { + $file = $folder . '/.gitignore'; + if (is_link($folder) || file_exists($file)) { + continue; + } + if (!is_dir($folder) && !@mkdir($folder, 0775, true) && !is_dir($folder)) { + continue; + } + if (@file_put_contents($file, self::GITIGNORE) !== false) { + $written[] = $file; + } + } + + return $written; + } + + /** + * A source path without the "symlink:" marker of VendorPublishCommand. + */ + private static function sourcePath(string $source): string + { + return str_starts_with($source, 'symlink:') ? substr($source, strlen('symlink:')) : $source; + } +} diff --git a/core/tests/Fixtures/on-demand-providers.php b/core/tests/Fixtures/on-demand-providers.php new file mode 100644 index 0000000000..5f12cb0808 --- /dev/null +++ b/core/tests/Fixtures/on-demand-providers.php @@ -0,0 +1,67 @@ +app->bound('gallery.registered') ? $this->app->make('gallery.registered') : 0; + $this->app->instance('gallery.registered', $count + 1); + } + + public function boot(): void + { + $this->app->singleton('gallery', fn () => new Gallery()); + $this->app->alias('gallery', Gallery::class); + // Like sGallery: the provider declares its facade alias itself, unconditionally. + $alias = $this->app['config']->get('gallery.alias'); + if (is_string($alias)) { + class_alias(Facades\GalleryFacade::class, $alias); + } + } + } + + final class Gallery + { + public function name(): string + { + return 'gallery'; + } + } +} + +namespace OnDemandFixture\Gallery\Facades { + + final class GalleryFacade + { + } +} + +namespace OnDemandFixture\Editor { + + use Illuminate\Support\ServiceProvider; + + final class EditorServiceProvider extends ServiceProvider + { + public function register(): void + { + $this->app['events']->listen('evolution.OnRichTextEditorInit', fn () => 'editor'); + } + } +} + +namespace OnDemandFixture\Always { + + use Illuminate\Support\ServiceProvider; + + final class AlwaysServiceProvider extends ServiceProvider + { + public function register(): void + { + $this->app->instance('always.registered', true); + } + } +} diff --git a/core/tests/Fixtures/on-demand-request-mode.php b/core/tests/Fixtures/on-demand-request-mode.php new file mode 100644 index 0000000000..6dc9798a66 --- /dev/null +++ b/core/tests/Fixtures/on-demand-request-mode.php @@ -0,0 +1,31 @@ +setValue(null, true); +} + +$app = (new ReflectionClass(new class extends EvolutionCMS\Core { + public function __construct() + { + } + + public function runningInConsole() + { + return false; + } +}))->newInstanceWithoutConstructor(); + +echo (new ReflectionMethod($app, 'registersProvidersOnDemand'))->invoke($app) ? 'on-demand' : 'all'; diff --git a/core/tests/Mocks/cache_refresh_worker.php b/core/tests/Mocks/cache_refresh_worker.php index 96eeb6e3e7..a2cf9bedfc 100644 --- a/core/tests/Mocks/cache_refresh_worker.php +++ b/core/tests/Mocks/cache_refresh_worker.php @@ -3,6 +3,8 @@ // argv: [, $database, $dir, $alias] = $argv; require dirname(__DIR__, 2) . '/vendor/autoload.php'; +// tests/ is not in the classmap of the authoritative autoloader. +require_once __DIR__ . '/CacheRefreshHarness.php'; use EvolutionCMS\Legacy\Cache; use EvolutionCMS\Models\SiteContent; diff --git a/core/tests/Pest.php b/core/tests/Pest.php index ce811791af..03e32e273a 100644 --- a/core/tests/Pest.php +++ b/core/tests/Pest.php @@ -11,6 +11,17 @@ | */ +// The autoloader is classmap-authoritative (core/composer.json) and tests/ stays out of the classmap, +// so Composer never falls back to the Tests\ PSR-4 mapping: test runs load those classes here. +spl_autoload_register(static function (string $class): void { + if (str_starts_with($class, 'Tests\\')) { + $file = __DIR__ . '/' . str_replace('\\', '/', substr($class, strlen('Tests\\'))) . '.php'; + if (is_file($file)) { + require_once $file; + } + } +}); + pest()->extend(Tests\TestCase::class)->in('Feature'); // errors silenced with @ stay out of the results, see the trait (Tests\TestCase, and with it diff --git a/core/tests/Unit/Console/SiteUpdateCommandTest.php b/core/tests/Unit/Console/SiteUpdateCommandTest.php index 2a53d17807..98793b0154 100644 --- a/core/tests/Unit/Console/SiteUpdateCommandTest.php +++ b/core/tests/Unit/Console/SiteUpdateCommandTest.php @@ -123,7 +123,6 @@ function withoutBundledComposerOnPath(callable $test): mixed ->toContain('buildCustomComposerUpdateCommand') ->toContain('$this->composerDumpAutoloadCommand()') ->toContain("runCoreShellCommand('php artisan package:discover')") - ->toContain('--no-scripts') ->toContain('return self::FAILURE') ->toContain('catch (\Throwable $exception)') ->not->toContain('new Application()') diff --git a/core/tests/Unit/Install/WebInstallComposerDependenciesTest.php b/core/tests/Unit/Install/WebInstallComposerDependenciesTest.php new file mode 100644 index 0000000000..ab4a89184f --- /dev/null +++ b/core/tests/Unit/Install/WebInstallComposerDependenciesTest.php @@ -0,0 +1,142 @@ + $answers Exit code and output by the first word after the Composer command. + * @return array{result: array, commands: list, discovered: bool} + */ +function webComposerInstallRun(string $root, string $core, bool $standalone, array $answers = []): array +{ + $probe = tempnam(sys_get_temp_dir(), 'evo_web_composer_install_'); + file_put_contents($probe, ' "composer", "standalone" => ' . var_export($standalone, true) . ']);' + . ' echo json_encode(["result" => $result, "commands" => $commands, "discovered" => $discovered]);'); + $out = trim(evoRunPhp($probe)); + unlink($probe); + + return json_decode($out, true, flags: JSON_THROW_ON_ERROR); +} + +function webComposerInstallCore(): string +{ + $core = sys_get_temp_dir() . '/evo-web-composer-install-' . getmypid() . '-' . uniqid() . '/'; + mkdir($core . 'vendor/composer', 0777, true); + // composer/composer runs on symfony/console: the Composer of core/vendor must not replace it. + file_put_contents($core . 'vendor/composer/installed.json', json_encode(['packages' => [ + ['name' => 'composer/composer', 'require' => ['symfony/console' => '^8.0']], + ['name' => 'symfony/console', 'require' => []], + ]])); + + return $core; +} + +test('a standalone Composer installs from the lock, rebuilds the autoloader and discovers the packages', function () use ($root) { + $core = webComposerInstallCore(); + $dir = ' --working-dir=' . escapeshellarg(rtrim($core, '/\\')) . ' 2>&1'; + + try { + $run = webComposerInstallRun($root, $core, true); + + expect($run['result'])->toBe(['installed' => true, 'command' => 'cd core && composer install --no-dev --classmap-authoritative', 'output' => '']) + ->and($run['commands'])->toBe([ + 'composer ' . CustomComposerPackages::INSTALL_ARGUMENTS . $dir, + 'composer ' . CustomComposerPackages::DUMP_AUTOLOAD_ARGUMENTS . $dir, + ]) + ->and($run['discovered'])->toBeTrue() + ->and(CustomComposerPackages::INSTALL_ARGUMENTS)->toContain('--no-dev')->toContain('--classmap-authoritative') + ->and(CustomComposerPackages::DUMP_AUTOLOAD_ARGUMENTS)->toContain('--classmap-authoritative'); + } finally { + (new Illuminate\Filesystem\Filesystem())->deleteDirectory($core); + } +}); + +test('the Composer of core/vendor proves with a dry run that it keeps its own packages', function () use ($root) { + $core = webComposerInstallCore(); + + try { + $refused = webComposerInstallRun($root, $core, false, [ + 'install' => [0, "Package operations: 0 installs, 1 update, 0 removals\n - Upgrading symfony/console (v8.0.1 => v8.1.0)"], + ]); + $allowed = webComposerInstallRun($root, $core, false, [ + 'install' => [0, "Package operations: 0 installs, 1 update, 0 removals\n - Upgrading seiger/sgallery (v1.7.0 => v1.8.1)"], + ]); + + expect($refused['result']['installed'])->toBeFalse() + ->and($refused['result']['output'])->toContain('symfony/console')->toContain('standalone Composer') + ->and($refused['commands'])->toHaveCount(1) + ->and($refused['commands'][0])->toContain(' --dry-run') + ->and($refused['discovered'])->toBeFalse() + ->and($allowed['result']['installed'])->toBeTrue() + ->and($allowed['commands'])->toHaveCount(3) + ->and($allowed['commands'][0])->toContain(' --dry-run'); + } finally { + (new Illuminate\Filesystem\Filesystem())->deleteDirectory($core); + } +}); + +test('a failed install leaves the command to run and its output, and discovers nothing', function () use ($root) { + $core = webComposerInstallCore(); + + try { + $run = webComposerInstallRun($root, $core, true, ['install' => [2, "Loading composer repositories\nCould not reach packagist.org"]]); + + expect($run['result']['installed'])->toBeFalse() + ->and($run['result']['command'])->toBe('cd core && composer install --no-dev --classmap-authoritative') + ->and($run['result']['output'])->toContain('Could not reach packagist.org') + ->and($run['commands'])->toHaveCount(1) + ->and($run['discovered'])->toBeFalse(); + } finally { + (new Illuminate\Filesystem\Filesystem())->deleteDirectory($core); + } +}); + +test('make:site and the web installer share the Composer arguments', function () use ($root) { + $command = (string) file_get_contents($root . '/core/src/Console/SiteUpdateCommand.php'); + + expect($command)->toContain("' ' . CustomComposerPackages::INSTALL_ARGUMENTS;") + ->and($command)->toContain("' ' . CustomComposerPackages::DUMP_AUTOLOAD_ARGUMENTS;"); +}); + +test('Composer runs before the new core boots; the booted update step only discovers the packages', function () use ($root) { + $controller = (string) file_get_contents($root . '/install/src/controllers/install.php'); + $repairPage = (string) file_get_contents($root . '/install/src/template/actions/dependencies.php'); + $lang = (string) file_get_contents($root . '/install/src/lang/en.inc.php'); + $update = substr($controller, strpos($controller, "seed('update');")); + + // Rebuilding core/vendor in this request would not reach the Composer loader it already runs. + expect($controller)->not->toContain('installerInstallComposerDependencies(') + ->and(strpos($update, 'restoreCustomComposerPackages()'))->toBeLessThan(strpos($update, "Console::call('package:discover');")) + ->and($repairPage)->toContain("htmlspecialchars(\$failed['command'], ENT_QUOTES, 'UTF-8')") + ->and($repairPage)->toContain("htmlspecialchars(\$failed['output'], ENT_QUOTES, 'UTF-8')"); + foreach (['composer_dependencies_not_installed', 'dependencies_check_again', 'dependencies_note', 'dependencies_retry', 'dependencies_title'] as $key) { + expect($lang)->toContain("'$key' =>"); + } +}); diff --git a/core/tests/Unit/Install/WebInstallStaleVendorTest.php b/core/tests/Unit/Install/WebInstallStaleVendorTest.php new file mode 100644 index 0000000000..f3ab8fc2b9 --- /dev/null +++ b/core/tests/Unit/Install/WebInstallStaleVendorTest.php @@ -0,0 +1,167 @@ + $locked])); + file_put_contents($core . 'vendor/composer/installed.json', json_encode(['packages' => $installed])); + file_put_contents($core . 'vendor/composer/autoload_real.php', 'setClassMapAuthoritative(true);' : '') . "\n"); + file_put_contents($core . 'vendor/composer/autoload_classmap.php', ' ['name' => 'seiger/sgallery', 'version' => $version]; + +test('a vendor that matches the lock and lists every class boots the new core', function () use ($root, $sgallery) { + $core = staleVendorCore([$sgallery('v1.8.1')], [$sgallery('v1.8.1')], true, + ['EvolutionCMS\\Core', 'EvolutionCMS\\Support\\OnDemandProviders'], ['Core', 'Support/OnDemandProviders']); + + try { + expect(staleVendorCheck($root, $core))->toBeNull(); + } finally { + (new Illuminate\Filesystem\Filesystem())->deleteDirectory($core); + } +}); + +test('an authoritative classmap without a class of the new core is stale', function () use ($root, $sgallery) { + $core = staleVendorCore([$sgallery('v1.8.1')], [$sgallery('v1.8.1')], true, + ['EvolutionCMS\\Core'], ['Core', 'Support/OnDemandProviders']); + + try { + expect(staleVendorCheck($root, $core))->toBe('The classmap of core/vendor does not list EvolutionCMS\\Support\\OnDemandProviders.'); + } finally { + (new Illuminate\Filesystem\Filesystem())->deleteDirectory($core); + } +}); + +test('a classmap that is not authoritative finds new classes itself', function () use ($root, $sgallery) { + $core = staleVendorCore([$sgallery('v1.8.1')], [$sgallery('v1.8.1')], false, + ['EvolutionCMS\\Core'], ['Core', 'Support/OnDemandProviders']); + + try { + expect(staleVendorCheck($root, $core))->toBeNull(); + } finally { + (new Illuminate\Filesystem\Filesystem())->deleteDirectory($core); + } +}); + +test('a package at another version than the lock, or missing, is stale', function () use ($root, $sgallery) { + $moved = staleVendorCore([$sgallery('v1.8.1')], [$sgallery('v1.7.0')], true, [], []); + $missing = staleVendorCore([$sgallery('v1.8.1')], [], true, [], []); + $branch = staleVendorCore( + [['name' => 'acme/tool', 'version' => 'dev-main', 'source' => ['reference' => 'b2']]], + [['name' => 'acme/tool', 'version' => 'dev-main', 'source' => ['reference' => 'a1']]], + true, [], [] + ); + + try { + expect(staleVendorCheck($root, $moved))->toBe('core/vendor has seiger/sgallery v1.7.0, core/composer.lock wants v1.8.1.') + ->and(staleVendorCheck($root, $missing))->toBe('core/vendor has seiger/sgallery (not installed), core/composer.lock wants v1.8.1.') + ->and(staleVendorCheck($root, $branch))->toBe('core/vendor has acme/tool dev-main@a1, core/composer.lock wants dev-main@b2.'); + } finally { + foreach ([$moved, $missing, $branch] as $core) { + (new Illuminate\Filesystem\Filesystem())->deleteDirectory($core); + } + } +}); + +test('a site without core/vendor has to install it', function () use ($root) { + $core = sys_get_temp_dir() . '/evo-stale-vendor-' . getmypid() . '-' . uniqid() . '/'; + mkdir($core, 0777, true); + + try { + expect(staleVendorCheck($root, $core))->toBe('core/vendor is missing.'); + } finally { + (new Illuminate\Filesystem\Filesystem())->deleteDirectory($core); + } +}); + +test('the repair runs on an autoloader that cannot load any class of the new core', function () use ($root) { + $core = sys_get_temp_dir() . '/evo-stale-repair-' . getmypid() . '-' . uniqid() . '/'; + mkdir($core . 'custom', 0777, true); + file_put_contents($core . 'custom/composer.json', json_encode(['require' => ['php' => '^8.3']])); + $probe = tempnam(sys_get_temp_dir(), 'evo_stale_repair_'); + // The vendor autoloader, then a loader in front of it that refuses the whole EvolutionCMS namespace: + // what a stale authoritative classmap amounts to for classes the new core added or reshaped. + file_put_contents($probe, ' "composer", "standalone" => true]);' + . ' echo json_encode(["repair" => $repair, "commands" => $commands]);'); + + try { + $out = json_decode(trim(evoRunPhp($probe)), true, flags: JSON_THROW_ON_ERROR); + + expect($out['repair']['customPackages']['restored'])->toBeTrue() + ->and($out['repair']['composerDependencies']['installed'])->toBeTrue() + ->and($out['commands'])->toHaveCount(2) + ->and($out['commands'][0])->toStartWith('composer ' . EvolutionCMS\Services\CustomComposerPackages::INSTALL_ARGUMENTS) + ->and($out['commands'][1])->toStartWith('composer ' . EvolutionCMS\Services\CustomComposerPackages::DUMP_AUTOLOAD_ARGUMENTS); + } finally { + unlink($probe); + (new Illuminate\Filesystem\Filesystem())->deleteDirectory($core); + } +}); + +test('every installer page asks about core/vendor before its step can boot the new core', function () use ($root) { + $index = (string) file_get_contents($root . '/install/index.php'); + $controller = (string) file_get_contents($root . '/install/src/controllers/dependencies.php'); + $page = substr($index, strpos($index, "if (empty(\$_GET['s'])) {")); + + expect($page)->toContain('$vendorStale = installerDependenciesStale(EVO_CORE_PATH);') + ->and(strpos($page, '$vendorStale = installerDependenciesStale(EVO_CORE_PATH);'))->toBeLessThan(strpos($page, 'require $controller;')) + // Admission first: an installer shut to this browser runs nothing. + ->and(strpos($index, 'if ($decision === InstallerCompletion::DENY) {'))->toBeLessThan(strpos($index, 'installerDependenciesStale(')) + ->and($controller)->toContain('$repair = installerRepairDependencies();') + // A fresh request after the repair: this one still runs the previous autoloader. + ->and($controller)->toContain("header('Location: index.php?action=' . rawurlencode(\$resume), true, 303);") + ->and($controller)->toContain('installerDependenciesStale(EVO_CORE_PATH)'); +}); diff --git a/core/tests/Unit/ManagerThemeServiceProviderTest.php b/core/tests/Unit/ManagerThemeServiceProviderTest.php new file mode 100644 index 0000000000..dfd0b9c98f --- /dev/null +++ b/core/tests/Unit/ManagerThemeServiceProviderTest.php @@ -0,0 +1,31 @@ +getValue($evoUi); +} + +test('booting the manager theme provider does not resolve EvoUI', function () { + $app = new Container(); + $app->singleton(EvoUI::class); + + (new ManagerThemeServiceProvider($app))->boot(); + + expect($app->resolved(EvoUI::class))->toBeFalse(); + expect(managerThemeComponents($app->make(EvoUI::class))) + ->toHaveKey('evo-core.system-info', EvolutionCMS\Livewire\SystemInfo::class); +}); + +test('an EvoUI resolved before the manager theme provider boots still gets the system info component', function () { + $app = new Container(); + $app->singleton(EvoUI::class); + $evoUi = $app->make(EvoUI::class); + + (new ManagerThemeServiceProvider($app))->boot(); + + expect(managerThemeComponents($evoUi))->toHaveKey('evo-core.system-info'); +}); diff --git a/core/tests/Unit/OnDemandProvidersTest.php b/core/tests/Unit/OnDemandProvidersTest.php new file mode 100644 index 0000000000..9adcc78691 --- /dev/null +++ b/core/tests/Unit/OnDemandProvidersTest.php @@ -0,0 +1,248 @@ +instance('events', new Dispatcher($this)); + $this->instance('config', new Repository(['app' => [ + 'providers' => $providers, + 'on_demand_providers' => $onDemand, + ]])); + } + + public function getCachedServicesPath() + { + return $this->manifestFile; + } + + protected function registersProvidersOnDemand(): bool + { + return $this->frontend; + } + + protected function onDemandRequestPath(): string + { + return $this->path; + } +} + +function onDemandProviders(): array +{ + return [ + 'Gallery' => GalleryServiceProvider::class, + 'Editor' => EditorServiceProvider::class, + 'Always' => AlwaysServiceProvider::class, + ]; +} + +function onDemandDeclarations(): array +{ + return [ + 'Gallery' => ['paths' => ['gallery'], 'services' => ['gallery']], + 'Editor' => ['paths' => ['editor-upload'], 'events' => ['OnRichTextEditorInit']], + 'Always' => false, + ]; +} + +function onDemandApp(string $path = 'articles/category-042', bool $frontend = true): OnDemandRuntimeCore +{ + $app = new OnDemandRuntimeCore(tempnam(sys_get_temp_dir(), 'evo-on-demand-'), onDemandProviders(), onDemandDeclarations(), $frontend, $path); + $app->registerConfiguredProviders(); + $app->boot(); + return $app; +} + +afterEach(function () { + foreach (glob(sys_get_temp_dir() . '/evo-on-demand-*') ?: [] as $file) { + @unlink($file); + } +}); + +test('request paths are taken relative to the site base URL', function (string $uri, string $base, string $path) { + expect(OnDemandProviders::requestPath($uri, $base))->toBe($path); +})->with([ + 'root site' => ['/sgallery/upload?x=1', '/', 'sgallery/upload'], + 'subdirectory site' => ['/shop/sgallery/upload', '/shop/', 'sgallery/upload'], + 'subdirectory letter case' => ['/Shop/stask/', '/shop/', 'stask'], + 'outside the base' => ['/other/sgallery', '/shop/', 'other/sgallery'], + 'base prefix of a longer segment' => ['/shopping/sgallery', '/shop/', 'shopping/sgallery'], + 'encoded' => ['/dtui%2Dplantuml', '/', 'dtui-plantuml'], + 'home page' => ['/', '/', ''], +]); + +test('a path prefix matches whole leading segments in any letter case', function (string $path, bool $matches) { + expect(OnDemandProviders::matchesPath($path, ['sgallery', '/ddocs/']))->toBe($matches); +})->with([ + ['sgallery', true], + ['sgallery/upload-file', true], + ['SGallery/upload', true], + ['ddocs/health', true], + ['sgallery-news', false], + ['articles/sgallery', false], + ['', false], +]); + +test('declarations name what loads a waiting provider', function () { + $providers = onDemandProviders() + ['Unknown' => 'Vendor\\Missing\\Provider']; + $declarations = onDemandDeclarations() + ['Undeclared' => ['services' => ['x']]]; + $onDemand = new OnDemandProviders($providers, $declarations, 'articles'); + + expect($onDemand->pending())->toBe([GalleryServiceProvider::class => true, EditorServiceProvider::class => true]) + ->and($onDemand->forService('gallery'))->toBe(GalleryServiceProvider::class) + ->and($onDemand->forService(Gallery::class))->toBe(GalleryServiceProvider::class) + ->and($onDemand->forService('\\' . Gallery::class))->toBe(GalleryServiceProvider::class) + ->and($onDemand->forService('OnDemandFixture\\GalleryOther\\Thing'))->toBeNull() + ->and($onDemand->forService('view'))->toBeNull() + ->and($onDemand->forEvent('OnRichTextEditorInit'))->toBe([EditorServiceProvider::class]) + ->and($onDemand->declaresService('gallery'))->toBeTrue(); + + $onDemand->forget(GalleryServiceProvider::class); + expect($onDemand->forService('gallery'))->toBeNull() + ->and($onDemand->declaresService('gallery'))->toBeFalse() + ->and($onDemand->isEmpty())->toBeFalse(); + $onDemand->forget(EditorServiceProvider::class); + expect($onDemand->forEvent('OnRichTextEditorInit'))->toBe([])->and($onDemand->isEmpty())->toBeTrue(); +}); + +test('a frontend request leaves declared providers unregistered and keeps the shared manifest', function () { + $app = onDemandApp(); + + expect($app->getProviders(GalleryServiceProvider::class))->toBe([]) + ->and($app->getProviders(EditorServiceProvider::class))->toBe([]) + ->and($app->make('always.registered'))->toBeTrue(); + + $manifest = require $app->manifestFile; + $repository = new ProviderRepository($app, new Filesystem, $app->manifestFile); + // A manager request passes the same list, so the two kinds of request never rewrite each other's manifest. + expect($manifest['providers'])->toBe(onDemandProviders()) + ->and($repository->shouldRecompile($manifest, onDemandProviders()))->toBeFalse(); +}); + +test('resolving a declared service registers and boots its provider once', function () { + $app = onDemandApp(); + + expect($app->bound('gallery'))->toBeTrue() + ->and($app->make('gallery'))->toBeInstanceOf(Gallery::class) + ->and($app->make('gallery.registered'))->toBe(1) + ->and($app->make('gallery'))->toBe($app->make(Gallery::class)) + ->and($app->getProviders(GalleryServiceProvider::class))->toHaveCount(1) + ->and($app->getProviders(EditorServiceProvider::class))->toBe([]); +}); + +test('resolving a class of the provider namespace registers it', function () { + $app = onDemandApp(); + + expect($app->make(Gallery::class))->toBeInstanceOf(Gallery::class) + ->and($app->make('gallery.registered'))->toBe(1); +}); + +test('a request to any declared path registers every declared provider up front', function () { + $app = onDemandApp('gallery/upload-file'); + + expect($app->make('gallery.registered'))->toBe(1) + ->and($app->getProviders(EditorServiceProvider::class))->toHaveCount(1); +}); + +test('invoking a declared event registers the provider before its listeners run', function () { + $app = onDemandApp(); + $app->pluginEvent = []; + + expect($app->invokeEvent('OnDocFormRender'))->toBeFalse() + ->and($app->getProviders(EditorServiceProvider::class))->toBe([]) + ->and($app->invokeEvent('OnRichTextEditorInit'))->toBe(['editor']) + ->and($app->getProviders(EditorServiceProvider::class))->toHaveCount(1); +}); + +test('requests outside the frontend register every provider', function () { + $app = onDemandApp(frontend: false); + + expect($app->make('gallery.registered'))->toBe(1) + ->and($app->getProviders(EditorServiceProvider::class))->toHaveCount(1) + ->and($app->bound('gallery'))->toBeTrue(); +}); + +test('only frontend web requests of a process that does not keep its application wait', function (string $mode, string $expected) { + $output = evoRunPhp(dirname(__DIR__) . '/Fixtures/on-demand-request-mode.php', [$mode], $status); + + expect($status)->toBe(0, $output)->and(trim($output))->toBe($expected); +})->with([ + ['frontend', 'on-demand'], + ['manager', 'all'], + ['install', 'all'], + ['kept-worker', 'all'], +]); + +test('the console is never treated as a frontend request', function () { + $app = (new ReflectionClass(Core::class))->newInstanceWithoutConstructor(); + $method = new ReflectionMethod($app, 'registersProvidersOnDemand'); + + expect($method->invoke($app))->toBeFalse(); +}); + +test('an alias whose provider declares it while booting is not declared twice', function () { + $app = onDemandApp(); + $alias = 'OnDemandFixtureGallery' . bin2hex(random_bytes(4)); + $app['config']->set('gallery.alias', $alias); + $previous = Container::getInstance(); + Container::setInstance($app); + $loader = AliasLoader::getInstance(); + $aliases = $loader->getAliases(); + $loader->setAliases([$alias => \OnDemandFixture\Gallery\Facades\GalleryFacade::class]); + $warnings = []; + set_error_handler(static function (int $level, string $message) use (&$warnings): bool { + $warnings[] = $message; + return true; + }); + try { + expect($loader->load($alias))->toBeTrue() + ->and($warnings)->toBe([]) + ->and(class_exists($alias, false))->toBeTrue() + ->and($app->make('gallery.registered'))->toBe(1); + } finally { + restore_error_handler(); + $loader->setAliases($aliases); + Container::setInstance($previous); + } +}); + +test('the bundled manager packages wait on the frontend and Livewire stays off there', function () { + $config = require dirname(__DIR__, 2) . '/config/app.php'; + $onDemand = new OnDemandProviders($config['providers'], $config['on_demand_providers'], 'articles/category-042'); + + expect(array_keys($onDemand->pending()))->toBe([ + EvoUI\EvoUIServiceProvider::class, + EvolutionCMS\dTuiEditor\DTuiEditorServiceProvider::class, + Dmi3yy\dDocs\dDocsServiceProvider::class, + Seiger\sGallery\sGalleryServiceProvider::class, + Seiger\sTask\sTaskServiceProvider::class, + ]) + ->and($onDemand->forService('livewire'))->toBeNull() + ->and($onDemand->forService('sGallery'))->toBe(Seiger\sGallery\sGalleryServiceProvider::class) + // sTask's module page renders EvoUI's Livewire panel: its URL needs EvoUI registered before routing. + ->and((new OnDemandProviders($config['providers'], $config['on_demand_providers'], 'stask'))->pending()) + ->toBe([]); +}); diff --git a/core/tests/Unit/PackageAssetFoldersTest.php b/core/tests/Unit/PackageAssetFoldersTest.php new file mode 100644 index 0000000000..d8af9195e3 --- /dev/null +++ b/core/tests/Unit/PackageAssetFoldersTest.php @@ -0,0 +1,96 @@ + $assets . '/plugins/acme.editor/editor.js', + $site . '/vendor/acme/editor/public/theme/a.css' => $assets . '/plugins/acme.editor/theme/a.css', + 'symlink:' . $site . '/vendor/acme/ui/ui.css' => $assets . '/modules/acme-ui/ui.css', + $site . '/vendor/acme/editor/public' => $assets . '/snippets/acme-snippet', + $site . '/vendor/acme/editor/noimage.png' => $assets . '/plugins/noimage.png', + $site . '/vendor/acme/gallery/noimage.png' => $assets . '/site/noimage.png', + $site . '/vendor/acme/editor/config.php' => $site . '/core/custom/config/acme.php', + $site . '/vendor/acme/editor/evil.js' => $assets . '/plugins/../evil.js', + $site . '/vendor/acme/editor/x.js' => str_replace('/', '\\', $assets . '/plugins/acme.windows/x.js'), + ], $assets); + + expect($folders)->toBe([ + $assets . '/plugins/acme.editor', + $assets . '/modules/acme-ui', + $assets . '/snippets/acme-snippet', + $assets . '/plugins/acme.windows', + ]); + } finally { + (new Filesystem())->deleteDirectory($site); + } +}); + +test('each package folder gets a .gitignore that ignores it, created along with the folder', function () { + $site = packageAssetsSite(); + $assets = $site . '/assets'; + + try { + $written = PackageAssetFolders::ignore([ + $site . '/vendor/acme/editor/public/editor.js' => $assets . '/plugins/acme.editor/editor.js', + 'symlink:' . $site . '/vendor/acme/ui/ui.css' => $assets . '/modules/acme-ui/ui.css', + ], $assets); + + expect($written)->toBe([$assets . '/plugins/acme.editor/.gitignore', $assets . '/modules/acme-ui/.gitignore']) + ->and(file_get_contents($assets . '/plugins/acme.editor/.gitignore'))->toBe(PackageAssetFolders::GITIGNORE) + ->and(PackageAssetFolders::GITIGNORE)->toEndWith("\n*\n") + ->and(is_dir($assets . '/modules/acme-ui'))->toBeTrue(); + } finally { + (new Filesystem())->deleteDirectory($site); + } +}); + +test('a folder that has a .gitignore keeps it, and a linked folder is left alone', function () { + $site = packageAssetsSite(); + $assets = $site . '/assets'; + mkdir($assets . '/plugins/acme.editor'); + file_put_contents($assets . '/plugins/acme.editor/.gitignore', "custom\n"); + mkdir($site . '/elsewhere'); + $linked = @symlink($site . '/elsewhere', $assets . '/plugins/acme.linked'); + + try { + $written = PackageAssetFolders::ignore([ + $site . '/vendor/acme/editor/public/editor.js' => $assets . '/plugins/acme.editor/editor.js', + $site . '/vendor/acme/linked/a.js' => $assets . '/plugins/acme.linked/a.js', + ], $assets); + + expect($written)->toBe([]) + ->and(file_get_contents($assets . '/plugins/acme.editor/.gitignore'))->toBe("custom\n"); + if ($linked) { + expect(file_exists($site . '/elsewhere/.gitignore'))->toBeFalse(); + } + } finally { + if ($linked) { + @unlink($assets . '/plugins/acme.linked'); + } + (new Filesystem())->deleteDirectory($site); + } +}); + +test('every artisan run ignores the package folders once all providers have booted', function () { + $console = (string) file_get_contents(dirname(__DIR__, 2) . '/src/Console.php'); + $hook = 'Support\PackageAssetFolders::ignore(\Illuminate\Support\ServiceProvider::pathsToPublish(), public_path(\'assets\'));'; + + expect($console)->toContain($hook) + ->and(strpos($console, '$laravel->loadDeferredProviders();'))->toBeLessThan(strpos($console, $hook)) + ->and(strpos($console, $hook))->toBeLessThan(strpos($console, 'parent::bootstrap();')); +}); diff --git a/core/tests/Unit/ProviderManifestRuntimeTest.php b/core/tests/Unit/ProviderManifestRuntimeTest.php index 12fd781c57..2438934cf6 100644 --- a/core/tests/Unit/ProviderManifestRuntimeTest.php +++ b/core/tests/Unit/ProviderManifestRuntimeTest.php @@ -116,6 +116,21 @@ function manifestRuntimeWarm(ManifestRuntimeCore $app, array $providers, array $ } }); +test('a missing provider is reported once when the manifest is compiled', function () { + $path = tempnam(sys_get_temp_dir(), 'evo-manifest-'); + $log = tempnam(sys_get_temp_dir(), 'evo-manifest-log-'); + $missing = 'ManifestMissing'.bin2hex(random_bytes(4)); + $previous = ini_set('error_log', $log); + try { + (new ManifestRuntimeCore($path, [ManifestRuntimeEager::class, $missing]))->registerConfiguredProviders(); + expect(substr_count((string) file_get_contents($log), 'Skipped missing service provider "'.$missing.'"'))->toBe(1); + } finally { + ini_set('error_log', (string) $previous); + @unlink($path); + @unlink($log); + } +}); + test('cold manifests skip missing packages and detect providers that become available', function () { $path = tempnam(sys_get_temp_dir(), 'evo-manifest-'); $missing = 'ManifestMissing'.bin2hex(random_bytes(4)); diff --git a/install/index.php b/install/index.php index 5f08eb74f2..3444e616b3 100644 --- a/install/index.php +++ b/install/index.php @@ -153,6 +153,13 @@ ob_start(); $action = isset($_GET['action']) ? preg_replace('/[^a-z\/]/', '', $_GET['action']) : 'language'; + // The manager's updater lays a source archive over the site and keeps its core/vendor: before any step + // boots the new core on it, Composer brings it to the new lock (controllers/dependencies.php). + $vendorStale = installerDependenciesStale(EVO_CORE_PATH); + if ($vendorStale !== null) { + $resumeAction = $action; + $action = 'dependencies'; + } $controller = __DIR__ . '/src/controllers/' . $action . '.php'; if (! file_exists($controller)) { die("Invalid install action attempted. [action={$action}]"); diff --git a/install/src/controllers/dependencies.php b/install/src/controllers/dependencies.php new file mode 100644 index 0000000000..7b4ab22dcc --- /dev/null +++ b/install/src/controllers/dependencies.php @@ -0,0 +1,20 @@ + 0) { + ob_end_clean(); + } + header('Location: index.php?action=' . rawurlencode($resume), true, 303); + exit; +} + +include dirname(__DIR__) . '/template/actions/dependencies.php'; diff --git a/install/src/controllers/install.php b/install/src/controllers/install.php index 5506a16393..6e5e9433b2 100644 --- a/install/src/controllers/install.php +++ b/install/src/controllers/install.php @@ -252,6 +252,11 @@ // Not restored: the update is incomplete, and the installer stays for the operator. if (!$customPackages['restored']) { $errors += 1; + } else { + // make:site's Composer install and autoload rebuild ran before this request booted, whenever + // core/vendor did not match this version (controllers/dependencies.php); what is left is the + // discovery it ends with. A matching vendor, uploaded or built offline, needs no Composer. + Console::call('package:discover'); } } diff --git a/install/src/functions.php b/install/src/functions.php index 2bb8633e1a..2057b98efd 100644 --- a/install/src/functions.php +++ b/install/src/functions.php @@ -1178,14 +1178,11 @@ function restoreCustomComposerPackages(?callable $run = null, ?callable $discove return $result; } - // A web request rarely has HOME, which Composer needs; make:site uses the same folder. - if (getenv('COMPOSER_HOME') === false && getenv('HOME') === false) { - putenv('COMPOSER_HOME=' . EVO_CORE_PATH . 'composer'); - } + installerComposerHome(); [, $version] = $run($composer['command'] . ' --version --no-ansi 2>&1'); $minimalChanges = preg_match('/Composer (?:version )?(\d+\.\d+\.\d+)/', (string) $version, $match) === 1 - && version_compare($match[1], \EvolutionCMS\Console\Packages\InstallPackageRequireCommand::MINIMAL_CHANGES_SINCE, '>='); + && version_compare($match[1], \EvolutionCMS\Services\CustomComposerPackages::MINIMAL_CHANGES_SINCE, '>='); $plan = $packages->plan( static fn (string $command): array => $run($command), @@ -1223,6 +1220,202 @@ function restoreCustomComposerPackages(?callable $run = null, ?callable $discove return $result; } +/** + * What make:site does once the files of an update are in place: core/vendor from the new lock, + * the classmap-authoritative autoloader rebuilt, the packages discovered. + * + * The manager's updater lays a source archive over the site, which carries no vendor directory: without + * this the site keeps the dependencies and the classmap of the version it ran, and finds neither the new + * packages nor the new classes of the core. A Composer that runs from core/vendor first proves with a dry + * run that it replaces none of its own packages (CustomComposerPackages::planInstall()), as make:site does. + * + * @since 3.6.0 + * @param callable|null $run Runs a shell command: array{0: int, 1: string} exit code and output. + * @param callable|null $discover Registers the providers of the installed packages. + * @param array{command: string, standalone: bool}|null $composer The Composer to use; found when null. + * @return array{installed: bool, command: string, output: string} command is what to run by hand when not installed. + */ +function installerInstallComposerDependencies(?callable $run = null, ?callable $discover = null, ?array $composer = null): array +{ + $result = ['installed' => false, 'command' => 'cd core && composer install --no-dev --classmap-authoritative', 'output' => '']; + $run ??= 'installerRunCommand'; + $composer ??= installerComposerCommand(); + if ($composer === null) { + $result['output'] = 'No Composer and no PHP command line binary to run the one in core/vendor were found.'; + + return $result; + } + + installerComposerHome(); + $workingDir = ' --working-dir=' . escapeshellarg(rtrim(EVO_CORE_PATH, '/\\')); + if (!$composer['standalone']) { + $plan = (new \EvolutionCMS\Services\CustomComposerPackages(EVO_CORE_PATH)) + ->planInstall(static fn (string $command): array => $run($command), $composer['command'], true); + if (!$plan['ok']) { + $result['output'] = \EvolutionCMS\Services\CustomComposerPackages::describeRefusal($plan); + + return $result; + } + } + + foreach ([\EvolutionCMS\Services\CustomComposerPackages::INSTALL_ARGUMENTS, \EvolutionCMS\Services\CustomComposerPackages::DUMP_AUTOLOAD_ARGUMENTS] as $arguments) { + [$exitCode, $output] = $run($composer['command'] . ' ' . $arguments . $workingDir . ' 2>&1'); + if ((int) $exitCode !== 0) { + $result['output'] = installerOutputTail((string) $output); + + return $result; + } + } + + $discover ??= static function (): void { + \EvolutionCMS\Facades\Console::call('package:discover'); + }; + $discover(); + $result['installed'] = true; + + return $result; +} + +/** + * Why core/vendor cannot boot the code laid over it, or null when it can. + * + * The manager's updater lays a source archive over the site and keeps core/vendor: until Composer runs, the + * site has the packages and the autoloader of the version it ran. An authoritative classmap finds no class the + * new core added, and a package the new lock moved is still at its old version, so the new application cannot + * boot. Read before it does, from the files alone. + * + * @since 3.6.0 + */ +function installerDependenciesStale(string $corePath): ?string +{ + $composerDir = $corePath . 'vendor/composer/'; + if (!is_file($corePath . 'vendor/autoload.php') || !is_file($composerDir . 'installed.json')) { + return 'core/vendor is missing.'; + } + + $installed = json_decode((string) file_get_contents($composerDir . 'installed.json'), true); + $versions = []; + foreach ((array) ($installed['packages'] ?? $installed ?? []) as $package) { + if (is_array($package) && isset($package['name'])) { + $versions[strtolower((string) $package['name'])] = installerPackageVersion($package); + } + } + $lock = json_decode((string) @file_get_contents($corePath . 'composer.lock'), true); + foreach ((array) ($lock['packages'] ?? []) as $package) { + if (!is_array($package) || !isset($package['name'])) { + continue; + } + $name = strtolower((string) $package['name']); + $locked = installerPackageVersion($package); + if (($versions[$name] ?? null) !== $locked) { + return sprintf('core/vendor has %s %s, core/composer.lock wants %s.', $name, $versions[$name] ?? '(not installed)', $locked); + } + } + + // A classmap that is not authoritative still finds new classes through PSR-4. + if (str_contains((string) @file_get_contents($composerDir . 'autoload_real.php'), 'setClassMapAuthoritative(true)')) { + $classmap = (static fn (string $file) => include $file)($composerDir . 'autoload_classmap.php'); + foreach (installerCoreClasses($corePath . 'src') as $class) { + if (!isset($classmap[$class])) { + return "The classmap of core/vendor does not list $class."; + } + } + } + + return null; +} + +/** + * A package's version as Composer installed it; a development branch moves with its commit. + * + * @since 3.6.0 + */ +function installerPackageVersion(array $package): string +{ + $version = (string) ($package['version'] ?? ''); + if (str_starts_with($version, 'dev-') || str_ends_with($version, '-dev')) { + $version .= '@' . ($package['source']['reference'] ?? $package['dist']['reference'] ?? ''); + } + + return $version; +} + +/** + * The classes of core/src (PSR-4 under EvolutionCMS\), as the classmap of a matching vendor lists them. + * + * @since 3.6.0 + * @return list + */ +function installerCoreClasses(string $srcPath): array +{ + $srcPath = rtrim(str_replace('\\', '/', $srcPath), '/') . '/'; + $classes = []; + $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($srcPath, \FilesystemIterator::SKIP_DOTS)); + foreach ($files as $file) { + if ($file->getExtension() !== 'php' + || !preg_match('/^\s*(?:(?:final|abstract|readonly)\s+)*(?:class|interface|trait|enum)\s+\w+/m', (string) file_get_contents($file->getPathname()))) { + continue; + } + $relative = substr(str_replace('\\', '/', $file->getPathname()), strlen($srcPath), -4); + $classes[] = 'EvolutionCMS\\' . str_replace('/', '\\', $relative); + } + sort($classes); + + return $classes; +} + +/** + * The Composer helpers of core/src, loaded from the new source: the stale classmap of a site whose vendor + * installerDependenciesStale() rejects may not list them, or list an older shape. + * + * @since 3.6.0 + */ +function installerRequireComposerHelpers(): void +{ + if (!trait_exists(\EvolutionCMS\Traits\RunsComposerShell::class, false)) { + require_once EVO_CORE_PATH . 'src/Traits/RunsComposerShell.php'; + } + if (!class_exists(\EvolutionCMS\Services\CustomComposerPackages::class, false)) { + require_once EVO_CORE_PATH . 'src/Services/CustomComposerPackages.php'; + } +} + +/** + * The Composer repair of a stale core/vendor, before the new application boots: the custom packages, then + * the install and autoload rebuild of make:site. Nothing here loads the new core beyond the two Composer + * helpers; discovery waits for the install step, which boots in a fresh request on the rebuilt autoloader. + * + * @since 3.6.0 + * @return array{customPackages: array, composerDependencies: array|null} composerDependencies is null when + * the custom packages were not restored, so the install did not run. + */ +function installerRepairDependencies(?callable $run = null, ?array $composer = null): array +{ + installerRequireComposerHelpers(); + $noDiscovery = static function (): void { + }; + $customPackages = restoreCustomComposerPackages($run, $noDiscovery, $composer); + + return [ + 'customPackages' => $customPackages, + 'composerDependencies' => $customPackages['restored'] + ? installerInstallComposerDependencies($run, $noDiscovery, $composer) + : null, + ]; +} + +/** + * A web request rarely has HOME, which Composer needs; make:site uses the same folder. + * + * @since 3.6.0 + */ +function installerComposerHome(): void +{ + if (getenv('COMPOSER_HOME') === false && getenv('HOME') === false) { + putenv('COMPOSER_HOME=' . EVO_CORE_PATH . 'composer'); + } +} + /** * The restoring Composer command, as an operator types it in a terminal. * diff --git a/install/src/lang/az.inc.php b/install/src/lang/az.inc.php index 63ed8547ef..c670a83e64 100644 --- a/install/src/lang/az.inc.php +++ b/install/src/lang/az.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Cədvəl prefiksini yoxlayır `', 'choose_language' => 'Dil seçin', 'chunks' => 'Chunklar', + 'composer_dependencies_not_installed' => 'Bu versiyanın Composer asılılıqları quraşdırıla bilmədi, buna görə core/vendor hələ də əvvəlki versiyanın asılılıqlarını saxlayır:', 'config_permissions_note' => 'Yeni Linux/Unix quraşdırmaları üçün, xahiş edirik config.inc.php adlı boş bir fayl yaradın və /[+MGR_DIR+]/includes/ qovluğunda fayl icazələrini 0666 olaraq təyin edin.', 'connection_screen_collation' => 'Collation:', 'connection_screen_database_connection_information' => 'Məlumat bazası məlumatları', @@ -89,6 +90,10 @@ 'default_language_description' => 'Bu, Evolution CMS Meneceri idarəetmə panelində istifadə ediləcək defolt dildir.', 'depedency_create' => 'Asılılıq yaradıldı', 'depedency_update' => 'Asılılıq yeniləndi', + 'dependencies_check_again' => 'Yenidən yoxla', + 'dependencies_note' => 'Bu versiya saytın üzərinə öz core/vendor qovluğu olmadan köçürülüb, core/vendor isə hələ də əvvəlki versiyanın Composer asılılıqlarını saxlayır. Yeniləmənin davam etməsi üçün onlar quraşdırılmalıdır:', + 'dependencies_retry' => 'Yuxarıda göstərilən əmri terminalda saytın kök qovluğundan icra edin, sonra yenidən yoxlayın.', + 'dependencies_title' => 'Composer asılılıqları tələb olunur', 'during_execution_of_sql' => ' SQL ifadəsinin icrası zamanı ', 'encoding' => 'iso-8859-1', 'error' => 'xəta', diff --git a/install/src/lang/be.inc.php b/install/src/lang/be.inc.php index 3d1e1c931a..36ab2cb869 100644 --- a/install/src/lang/be.inc.php +++ b/install/src/lang/be.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Checking table prefix `', 'choose_language' => 'Choose language', 'chunks' => 'Chunks', + 'composer_dependencies_not_installed' => 'Не ўдалося ўсталяваць залежнасці Composer гэтай версіі, таму ў core/vendor па-ранейшаму залежнасці папярэдняй версіі:', 'config_permissions_note' => 'For new Linux/Unix installs, please create a blank file named config.inc.php in the /[+MGR_DIR+]/includes/ directory with file permissions set to 0666.', 'connection_screen_collation' => 'Collation:', 'connection_screen_database_connection_information' => 'Database information', @@ -89,6 +90,10 @@ 'default_language_description' => 'This is the default language that will be used in the Evolution CMS Manager back end control panel.', 'depedency_create' => 'Dependency created', 'depedency_update' => 'Dependency updated', + 'dependencies_check_again' => 'Праверыць зноў', + 'dependencies_note' => 'Гэтая версія была скапіявана паверх сайта без свайго core/vendor, у якім па-ранейшаму залежнасці Composer папярэдняй версіі. Іх трэба ўсталяваць, каб абнаўленне магло працягнуцца:', + 'dependencies_retry' => 'Выканайце паказаную вышэй каманду ў тэрмінале з каранёвага каталога сайта, затым праверце зноў.', + 'dependencies_title' => 'Патрабуюцца залежнасці Composer', 'during_execution_of_sql' => ' during the execution of SQL statement ', 'encoding' => 'iso-8859-1', 'error' => 'error', diff --git a/install/src/lang/bg.inc.php b/install/src/lang/bg.inc.php index 9c1550c212..36de6483c0 100644 --- a/install/src/lang/bg.inc.php +++ b/install/src/lang/bg.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Проверка на префикса на таблицата `', 'choose_language' => 'Изберете език', 'chunks' => 'Чънкове', + 'composer_dependencies_not_installed' => 'Зависимостите на Composer за тази версия не можаха да бъдат инсталирани, затова core/vendor все още съдържа тези от предишната версия:', 'config_permissions_note' => 'За нови Linux/Unix инсталации, моля, създайте празен файл с име config.inc.php в директорията [+MGR_DIR+]/includes/ и задайте права на файла 0666.', 'connection_screen_collation' => 'Колация:', 'connection_screen_database_connection_information' => 'Информация за БД', @@ -89,6 +90,10 @@ 'default_language_description' => 'Това е езикът по подразбиране, който ще се използва в контролния панел на Evolution CMS Manager.', 'depedency_create' => 'Depedency created', 'depedency_update' => 'Depedency updated', + 'dependencies_check_again' => 'Провери отново', + 'dependencies_note' => 'Тази версия е копирана върху сайта без своята директория core/vendor, която все още съдържа зависимостите на Composer от предишната версия. Те трябва да бъдат инсталирани, преди обновяването да може да продължи:', + 'dependencies_retry' => 'Изпълнете показаната по-горе команда в терминал от основната директория на сайта, след което проверете отново.', + 'dependencies_title' => 'Необходими са зависимости на Composer', 'during_execution_of_sql' => ' по време на изпълнение на SQL условие ', 'encoding' => 'utf-8', 'error' => 'грешка', diff --git a/install/src/lang/cs.inc.php b/install/src/lang/cs.inc.php index 1892c5c884..2d9e956076 100644 --- a/install/src/lang/cs.inc.php +++ b/install/src/lang/cs.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Kontrola prefixů tabulek `', 'choose_language' => 'Zvolte jazyk', 'chunks' => 'Chunky', + 'composer_dependencies_not_installed' => 'Závislosti Composer této verze se nepodařilo nainstalovat, proto core/vendor stále obsahuje závislosti předchozí verze:', 'config_permissions_note' => 'Pro nové instalace na Linux/Unix vytvořte prosím prázdný soubor s názvem config.inc.php v adresáři /[+MGR_DIR+]/includes/ a nastavte oprávnění souboru na 0666.', 'connection_screen_collation' => 'Porovnání:', 'connection_screen_database_connection_information' => 'Informace o databázi', @@ -89,6 +90,10 @@ 'default_language_description' => 'Toto bude výchozí jazyk, který bude použit v Evolution CMS správci obsahu.', 'depedency_create' => 'Depedency created', 'depedency_update' => 'Depedency updated', + 'dependencies_check_again' => 'Zkontrolovat znovu', + 'dependencies_note' => 'Tato verze byla nahrána přes web bez své složky core/vendor, která stále obsahuje závislosti Composer předchozí verze. Před pokračováním aktualizace je nutné je nainstalovat:', + 'dependencies_retry' => 'Spusťte výše uvedený příkaz v terminálu z kořenové složky webu a poté zkontrolujte znovu.', + 'dependencies_title' => 'Jsou vyžadovány závislosti Composer', 'during_execution_of_sql' => ' při provádění SQL ', 'encoding' => 'utf-8', 'error' => 'chyba', diff --git a/install/src/lang/da.inc.php b/install/src/lang/da.inc.php index 5151703b5a..dee26d6e58 100644 --- a/install/src/lang/da.inc.php +++ b/install/src/lang/da.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Kontrollerer database tabellernes præfiks `', 'choose_language' => 'Vælg sprog', 'chunks' => 'Chunks', + 'composer_dependencies_not_installed' => 'Composer-afhængighederne for denne version kunne ikke installeres, så core/vendor indeholder stadig dem fra den forrige version:', 'config_permissions_note' => 'Ved nyinstallation under Linux eller Unix, skal du oprette en tom fil, med navnet config.inc.php i /[+MGR_DIR+]/includes/ mappen, med filrettighederne indstillet til 0666.', 'connection_screen_collation' => 'Collation:', 'connection_screen_database_connection_information' => 'Database information', @@ -89,6 +90,10 @@ 'default_language_description' => 'Dette er standard sproget der vil blive brugt i CMS\'et.', 'depedency_create' => 'Depedency created', 'depedency_update' => 'Depedency updated', + 'dependencies_check_again' => 'Tjek igen', + 'dependencies_note' => 'Denne version blev lagt over sitet uden sin core/vendor, som stadig indeholder Composer-afhængighederne fra den forrige version. De skal installeres, før opdateringen kan fortsætte:', + 'dependencies_retry' => 'Kør kommandoen ovenfor i en terminal fra sitets rodmappe, og tjek derefter igen.', + 'dependencies_title' => 'Composer-afhængigheder er påkrævet', 'during_execution_of_sql' => ' under eksekvering af SQL kommandoen ', 'encoding' => 'utf-8', 'error' => 'fejl', diff --git a/install/src/lang/de.inc.php b/install/src/lang/de.inc.php index a9f0b2fa4a..23c9ae5b0d 100644 --- a/install/src/lang/de.inc.php +++ b/install/src/lang/de.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Überprüfe Tabellen-Präfix `', 'choose_language' => 'Sprache auswählen', 'chunks' => 'Chunks', + 'composer_dependencies_not_installed' => 'Die Composer-Abhängigkeiten dieser Version konnten nicht installiert werden, daher enthält core/vendor noch die der vorherigen Version:', 'config_permissions_note' => 'Für neue Linux/Unix-Installationen erstellen Sie bitte eine leere Datei namens config.inc.php im Verzeichnis /[+MGR_DIR+]/includes/ mit auf 0666 gesetzten Dateiberechtigungen.', 'connection_screen_collation' => 'Kollation:', 'connection_screen_database_connection_information' => 'Datenbankinformationen', @@ -89,6 +90,10 @@ 'default_language_description' => 'Das ist die voreingestellte Sprache die im Evolution CMS-Manager (dem Evolution CMS-Adminbereich) verwendet wird.', 'depedency_create' => 'Abhängigkeit erstellt', 'depedency_update' => 'Abhängigkeit aktualisiert', + 'dependencies_check_again' => 'Erneut prüfen', + 'dependencies_note' => 'Diese Version wurde ohne ihr core/vendor über die Website kopiert; core/vendor enthält noch die Composer-Abhängigkeiten der vorherigen Version. Sie müssen installiert werden, bevor die Aktualisierung fortgesetzt werden kann:', + 'dependencies_retry' => 'Führen Sie den oben angezeigten Befehl in einem Terminal im Stammverzeichnis der Website aus und prüfen Sie anschließend erneut.', + 'dependencies_title' => 'Composer-Abhängigkeiten erforderlich', 'during_execution_of_sql' => ' während des Ausführens des SQL-Statements ', 'encoding' => 'utf-8', 'error' => 'Fehler', diff --git a/install/src/lang/en.inc.php b/install/src/lang/en.inc.php index 9b199543a7..83e560219a 100644 --- a/install/src/lang/en.inc.php +++ b/install/src/lang/en.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Checking table prefix `', 'choose_language' => 'Choose language', 'chunks' => 'Chunks', + 'composer_dependencies_not_installed' => 'The Composer dependencies of this version could not be installed, so core/vendor still holds those of the previous version:', 'config_permissions_note' => 'For new Linux/Unix installs, please create a blank file named config.inc.php in the /[+MGR_DIR+]/includes/ directory with file permissions set to 0666.', 'connection_screen_collation' => 'Collation:', 'connection_screen_database_connection_information' => 'Database information', @@ -89,6 +90,10 @@ 'default_language_description' => 'This is the default language that will be used in the Evolution CMS Manager back end control panel.', 'depedency_create' => 'Dependency created', 'depedency_update' => 'Dependency updated', + 'dependencies_check_again' => 'Check again', + 'dependencies_note' => 'This version was laid over the site without its core/vendor, which still holds the Composer dependencies of the previous version. They have to be installed before the update can go on:', + 'dependencies_retry' => 'Run the command shown above in a terminal from the site root, then check again.', + 'dependencies_title' => 'Composer dependencies required', 'during_execution_of_sql' => ' during the execution of SQL statement ', 'encoding' => 'iso-8859-1', 'error' => 'error', diff --git a/install/src/lang/es.inc.php b/install/src/lang/es.inc.php index eeba187565..96b67e5454 100644 --- a/install/src/lang/es.inc.php +++ b/install/src/lang/es.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Checando el prefijo de las tablas `', 'choose_language' => 'Elige Lengua', 'chunks' => 'Chunks', + 'composer_dependencies_not_installed' => 'No se pudieron instalar las dependencias de Composer de esta versión, por lo que core/vendor aún contiene las de la versión anterior:', 'config_permissions_note' => 'Para instalaciones nuevas en servidores Linux/Unix, favpr de crear un archivo nuevo en blanco llamado config.inc.php en el directorio [+MGR_DIR+]/includes/ con los permisos de archivo configurados a 0666.', 'connection_screen_collation' => 'Compaginación:', 'connection_screen_database_connection_information' => 'Información de la base de datos', @@ -89,6 +90,10 @@ 'default_language_description' => 'Este es el idioma predeterminado que se utilizará en el panel de control back-end del Administrador Evolution CMS.', 'depedency_create' => 'Dependencia creada', 'depedency_update' => 'Dependencia actualizada', + 'dependencies_check_again' => 'Comprobar de nuevo', + 'dependencies_note' => 'Esta versión se copió sobre el sitio sin su core/vendor, que aún contiene las dependencias de Composer de la versión anterior. Deben instalarse antes de que la actualización pueda continuar:', + 'dependencies_retry' => 'Ejecute el comando mostrado arriba en una terminal desde la raíz del sitio y luego compruebe de nuevo.', + 'dependencies_title' => 'Se requieren dependencias de Composer', 'during_execution_of_sql' => ' durante la ejecución del comando SQL ', 'encoding' => 'UTF-8', 'error' => 'error', diff --git a/install/src/lang/et.inc.php b/install/src/lang/et.inc.php index 5fe8076946..02a10026f8 100644 --- a/install/src/lang/et.inc.php +++ b/install/src/lang/et.inc.php @@ -58,6 +58,7 @@ 'checking_table_prefix' => 'Tabeli eesliite kontroll `', 'choose_language' => 'Vali keel', 'chunks' => 'HTML-plokid', + 'composer_dependencies_not_installed' => 'Selle versiooni Composeri sõltuvusi ei saanud paigaldada, mistõttu core/vendor sisaldab endiselt eelmise versiooni omasid:', 'config_permissions_note' => 'Uue Linuxi/Unixi paigalduse korral looge kataloogi /[+MGR_DIR+]/includes/ tühi fail nimega config.inc.php ja määrake selle õigusteks 0666.', 'connection_screen_collation' => 'Sortimisreeglistik:', 'connection_screen_database_connection_information' => 'Andmebaasi teave', @@ -95,6 +96,10 @@ 'default_language_description' => 'See on Evolution CMS-i halduspaneelis vaikimisi kasutatav keel.', 'depedency_create' => 'Sõltuvus loodud', 'depedency_update' => 'Sõltuvus uuendatud', + 'dependencies_check_again' => 'Kontrolli uuesti', + 'dependencies_note' => 'See versioon kopeeriti saidi peale ilma oma core/vendor kaustata, mis sisaldab endiselt eelmise versiooni Composeri sõltuvusi. Need tuleb paigaldada, enne kui uuendamine saab jätkuda:', + 'dependencies_retry' => 'Käivitage ülal näidatud käsk terminalis saidi juurkataloogist ja seejärel kontrollige uuesti.', + 'dependencies_title' => 'Vajalikud on Composeri sõltuvused', 'during_execution_of_sql' => ' SQL-lause täitmisel ', 'encoding' => 'UTF-8', 'error' => 'viga', diff --git a/install/src/lang/fa.inc.php b/install/src/lang/fa.inc.php index ba95a1068a..256c779723 100644 --- a/install/src/lang/fa.inc.php +++ b/install/src/lang/fa.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'مرور پیشوند جدول `', 'choose_language' => 'انتخاب زبان', 'chunks' => 'چانکها', + 'composer_dependencies_not_installed' => 'وابستگی‌های Composer این نسخه نصب نشدند، بنابراین core/vendor هنوز وابستگی‌های نسخه قبلی را در خود دارد:', 'config_permissions_note' => 'برای نصب جدید روی لینوکس/یونیکس, لطفا یک فایل خالی به نام config.inc.php در دایرکتوری [+MGR_DIR+]/includes/ ایجاد کنید که سطح دسترسی به آن 0666 تنظیم شده باشد.', 'connection_screen_collation' => 'تطبیق(Collation):', 'connection_screen_database_connection_information' => 'اطلاعات پایگاه داده', @@ -89,6 +90,10 @@ 'default_language_description' => 'این زبان پیش فرضی است که در پنل کنترل مدیریت Evolution CMS استفاده خواهد شد.', 'depedency_create' => 'Depedency created', 'depedency_update' => 'Depedency updated', + 'dependencies_check_again' => 'بررسی دوباره', + 'dependencies_note' => 'این نسخه بدون core/vendor خود روی سایت کپی شده است و core/vendor هنوز وابستگی‌های Composer نسخه قبلی را در خود دارد. پیش از ادامه به‌روزرسانی باید آن‌ها نصب شوند:', + 'dependencies_retry' => 'فرمان نشان‌داده‌شده در بالا را در ترمینال از پوشه ریشه سایت اجرا کنید، سپس دوباره بررسی کنید.', + 'dependencies_title' => 'وابستگی‌های Composer لازم است', 'during_execution_of_sql' => ' به هنگام اجراي فرامين SQL ', 'encoding' => 'utf-8', 'error' => 'خطا', diff --git a/install/src/lang/fi.inc.php b/install/src/lang/fi.inc.php index 08048e8561..8b28c6b8dc 100644 --- a/install/src/lang/fi.inc.php +++ b/install/src/lang/fi.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Tarkistetaan tietokannan taulujen etuliite `', 'choose_language' => 'Valitse kieli', 'chunks' => 'HTML-palaset', + 'composer_dependencies_not_installed' => 'Tämän version Composer-riippuvuuksia ei voitu asentaa, joten core/vendor sisältää yhä edellisen version riippuvuudet:', 'config_permissions_note' => 'Jos olet asentamassa järjestelmää Linux / Unix palvelimeen, luo kansioon [+MGR_DIR+]/includes/ tyhjä tiedosto nimeltä config.inc.php ja aseta tiedostoon 0666 oikeudet.', 'connection_screen_collation' => 'Merkistö:', 'connection_screen_database_connection_information' => 'Tietokannan yhteysasetukset', @@ -89,6 +90,10 @@ 'default_language_description' => 'Tämä asetetaan Evolution CMS ylläpidon oletus kieleksi.', 'depedency_create' => 'Riippuvuus luotu', 'depedency_update' => 'Riippuvuus päivitetty', + 'dependencies_check_again' => 'Tarkista uudelleen', + 'dependencies_note' => 'Tämä versio kopioitiin sivuston päälle ilman omaa core/vendor-kansiotaan, joka sisältää yhä edellisen version Composer-riippuvuudet. Ne on asennettava ennen kuin päivitystä voidaan jatkaa:', + 'dependencies_retry' => 'Suorita yllä näkyvä komento päätteessä sivuston juurihakemistosta ja tarkista sitten uudelleen.', + 'dependencies_title' => 'Composer-riippuvuudet vaaditaan', 'during_execution_of_sql' => ' SQL lauseen suorituksen aikana ', 'encoding' => 'UTF-8', 'error' => 'virhe', diff --git a/install/src/lang/fr.inc.php b/install/src/lang/fr.inc.php index 545650a2fb..680cde1c01 100644 --- a/install/src/lang/fr.inc.php +++ b/install/src/lang/fr.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Vérification du préfixe de table `', 'choose_language' => 'Choisissez la langue', 'chunks' => 'Chunks', + 'composer_dependencies_not_installed' => 'Les dépendances Composer de cette version n\'ont pas pu être installées, core/vendor contient donc toujours celles de la version précédente :', 'config_permissions_note' => 'Lors des installations Linux/Unix, veuillez créer un nouveau fichier nommé config.inc.php dans le répertoire [+MGR_DIR+]/includes/ avec les droits d\'accès 0666.', 'connection_screen_collation' => 'Collation:', 'connection_screen_database_connection_information' => 'Informations base de données', @@ -89,6 +90,10 @@ 'default_language_description' => 'Ceci est la langue par défaut qui sera utilisée dans l\'interface d\'administration de votre Gestionnaire de Contenu Evolution CMS.', 'depedency_create' => 'Dépendance créée', 'depedency_update' => 'Dépendance mise à jour', + 'dependencies_check_again' => 'Vérifier à nouveau', + 'dependencies_note' => 'Cette version a été copiée sur le site sans son core/vendor, qui contient toujours les dépendances Composer de la version précédente. Elles doivent être installées avant que la mise à jour puisse continuer :', + 'dependencies_retry' => 'Exécutez la commande ci-dessus dans un terminal depuis la racine du site, puis vérifiez à nouveau.', + 'dependencies_title' => 'Dépendances Composer requises', 'during_execution_of_sql' => ' lors de l\'exécution de la requête SQL ', 'encoding' => 'utf-8', 'error' => 'erreur', diff --git a/install/src/lang/he.inc.php b/install/src/lang/he.inc.php index 54d7dad109..c776b3e609 100644 --- a/install/src/lang/he.inc.php +++ b/install/src/lang/he.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'בדיקת קידומת טבלה `', 'choose_language' => 'בחר שפה', 'chunks' => 'מיני תבניות', + 'composer_dependencies_not_installed' => 'לא ניתן היה להתקין את תלויות Composer של גרסה זו, ולכן core/vendor עדיין מכיל את אלה של הגרסה הקודמת:', 'config_permissions_note' => 'עבור התקנות לינוקס/יוניקס חדשות, אנא צרו קובץ ריק בשם config.inc.php בספרייה [+MGR_DIR+]/includes/ עם הרשאות 0666.', 'connection_screen_collation' => 'אוסף נתונים:', 'connection_screen_database_connection_information' => 'מידע אודות מסד הנתונים', @@ -89,6 +90,10 @@ 'default_language_description' => 'זוהי השפה שתוגדר כברירת מחדל עבור ממשק הניהול של Evolution CMS.', 'depedency_create' => 'Depedency created', 'depedency_update' => 'Depedency updated', + 'dependencies_check_again' => 'בדיקה חוזרת', + 'dependencies_note' => 'גרסה זו הועתקה על גבי האתר ללא core/vendor שלה, שעדיין מכיל את תלויות Composer של הגרסה הקודמת. יש להתקין אותן לפני שהעדכון יוכל להמשיך:', + 'dependencies_retry' => 'הריצו את הפקודה המוצגת למעלה במסוף מתיקיית השורש של האתר, ולאחר מכן בדקו שוב.', + 'dependencies_title' => 'נדרשות תלויות Composer', 'during_execution_of_sql' => ' במהלך ביצוע משפט SQL ', 'encoding' => 'UTF-8', 'error' => 'שגיאה', diff --git a/install/src/lang/hy.inc.php b/install/src/lang/hy.inc.php index 11869136da..5f7ec7e237 100644 --- a/install/src/lang/hy.inc.php +++ b/install/src/lang/hy.inc.php @@ -58,6 +58,7 @@ 'checking_table_prefix' => 'Ստուգվում է աղյուսակների նախածանցը՝ `', 'choose_language' => 'Ընտրեք լեզուն', 'chunks' => 'Հատվածներ', + 'composer_dependencies_not_installed' => 'Այս տարբերակի Composer-ի կախվածությունները չհաջողվեց տեղադրել, ուստի core/vendor-ը դեռ պարունակում է նախորդ տարբերակի կախվածությունները՝', 'config_permissions_note' => 'Linux/Unix-ում նոր տեղադրումների համար ստեղծեք config.inc.php անունով դատարկ ֆայլ /[+MGR_DIR+]/includes/ պանակում և դրա թույլտվությունները սահմանեք 0666։', 'connection_screen_collation' => 'Համադրման կարգ՝', 'connection_screen_database_connection_information' => 'Տվյալների բազայի տեղեկություններ', @@ -95,6 +96,10 @@ 'default_language_description' => 'Սա Evolution CMS-ի կառավարման վահանակում օգտագործվող լռելյայն լեզուն է։', 'depedency_create' => 'Կախվածությունը ստեղծված է', 'depedency_update' => 'Կախվածությունը թարմացված է', + 'dependencies_check_again' => 'Ստուգել կրկին', + 'dependencies_note' => 'Այս տարբերակը պատճենվել է կայքի վրա առանց իր core/vendor-ի, որը դեռ պարունակում է նախորդ տարբերակի Composer-ի կախվածությունները։ Դրանք պետք է տեղադրվեն, որպեսզի թարմացումը շարունակվի՝', + 'dependencies_retry' => 'Կայքի արմատային պանակից տերմինալում գործարկեք վերևում նշված հրամանը, ապա կրկին ստուգեք։', + 'dependencies_title' => 'Անհրաժեշտ են Composer-ի կախվածությունները', 'during_execution_of_sql' => ' SQL հրամանի կատարման ընթացքում՝ ', 'encoding' => 'UTF-8', 'error' => 'սխալ', diff --git a/install/src/lang/it.inc.php b/install/src/lang/it.inc.php index 4ac8479b4c..1fc8565692 100644 --- a/install/src/lang/it.inc.php +++ b/install/src/lang/it.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Controllo il prefisso per le tabelle `', 'choose_language' => 'Scegli la lingua', 'chunks' => 'Chunks', + 'composer_dependencies_not_installed' => 'Non è stato possibile installare le dipendenze Composer di questa versione, quindi core/vendor contiene ancora quelle della versione precedente:', 'config_permissions_note' => 'Per nuove installazioni su Linux/Unix, vi preghiamo di creare un file vuoto config.inc.php nella directory /[+MGR_DIR+]/includes/ con i permessi di scrittura a 0666.', 'connection_screen_collation' => 'Collation:', 'connection_screen_database_connection_information' => 'Informazioni sul Database', @@ -89,6 +90,10 @@ 'default_language_description' => 'Questa è la lingua che userà di default nell\'area di amministrazione di Evolution CMS.', 'depedency_create' => 'Dipendenza creata', 'depedency_update' => 'Dipendenza aggiornata', + 'dependencies_check_again' => 'Controlla di nuovo', + 'dependencies_note' => 'Questa versione è stata copiata sul sito senza la propria core/vendor, che contiene ancora le dipendenze Composer della versione precedente. Devono essere installate prima che l\'aggiornamento possa proseguire:', + 'dependencies_retry' => 'Esegui il comando mostrato sopra in un terminale dalla cartella principale del sito, quindi controlla di nuovo.', + 'dependencies_title' => 'Dipendenze Composer richieste', 'during_execution_of_sql' => ' durante l\'esecuzione del comando SQL ', 'encoding' => 'iso-8859-1', 'error' => 'errore', diff --git a/install/src/lang/ja.inc.php b/install/src/lang/ja.inc.php index a9418e63fa..0fdbdbe0f0 100644 --- a/install/src/lang/ja.inc.php +++ b/install/src/lang/ja.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Tableプリフィックスの設定 `', 'choose_language' => '言語を選択してください', 'chunks' => 'チャンク', + 'composer_dependencies_not_installed' => 'このバージョンの Composer 依存関係をインストールできなかったため、core/vendor には以前のバージョンのものが残っています:', 'config_permissions_note' => 'Linux / Unixを新規にインストールする場合は、/[+MGR_DIR+]/includes/ ディレクトリ内にconfig.inc.php という名前の空のファイルを作成してください。ファイルのパーミッションは0666に設定してください。', 'connection_screen_collation' => '照合順序(エンコード指定含む):
※utf8_general_ciを
おすすめします', 'connection_screen_database_connection_information' => 'データベース設定', @@ -89,6 +90,10 @@ 'default_language_description' => '管理画面で使用する言語を選択してください。(※Japanese-eucを選択するとプリインストールされるサンプルコンテンツやアドオンの説明文は文字化けしますのでご注意ください。アドオンの機能は利用できます。)', 'depedency_create' => '依存関係が作成されました', 'depedency_update' => '依存関係が更新されました', + 'dependencies_check_again' => '再確認', + 'dependencies_note' => 'このバージョンは core/vendor なしでサイトに上書きされたため、core/vendor には以前のバージョンの Composer 依存関係が残っています。更新を続行するには、それらをインストールする必要があります:', + 'dependencies_retry' => '上に表示されたコマンドをサイトのルートディレクトリからターミナルで実行してから、再確認してください。', + 'dependencies_title' => 'Composer 依存関係が必要です', 'during_execution_of_sql' => ' SQL文の実行中 ', 'encoding' => 'utf-8', 'error' => 'エラー', diff --git a/install/src/lang/ka.inc.php b/install/src/lang/ka.inc.php index 06cc58040c..cc08ff6114 100644 --- a/install/src/lang/ka.inc.php +++ b/install/src/lang/ka.inc.php @@ -58,6 +58,7 @@ 'checking_table_prefix' => 'მოწმდება ცხრილის პრეფიქსი `', 'choose_language' => 'აირჩიეთ ენა', 'chunks' => 'ჩანკები', + 'composer_dependencies_not_installed' => 'ამ ვერსიის Composer-ის დამოკიდებულებები ვერ დაინსტალირდა, ამიტომ core/vendor კვლავ წინა ვერსიის დამოკიდებულებებს შეიცავს:', 'config_permissions_note' => 'Linux/Unix-ზე ახალი ინსტალაციისას შექმენით ცარიელი ფაილი config.inc.php კატალოგში /[+MGR_DIR+]/includes/ და მიანიჭეთ უფლებები 0666.', 'connection_screen_collation' => 'შედარების წესი:', 'connection_screen_database_connection_information' => 'მონაცემთა ბაზის ინფორმაცია', @@ -95,6 +96,10 @@ 'default_language_description' => 'ეს არის ნაგულისხმევი ენა, რომელიც Evolution CMS-ის ადმინისტრირების პანელში გამოიყენება.', 'depedency_create' => 'დამოკიდებულება შექმნილია', 'depedency_update' => 'დამოკიდებულება განახლებულია', + 'dependencies_check_again' => 'ხელახლა შემოწმება', + 'dependencies_note' => 'ეს ვერსია საიტზე გადაწერილია მისი core/vendor-ის გარეშე, რომელიც კვლავ წინა ვერსიის Composer-ის დამოკიდებულებებს შეიცავს. განახლების გასაგრძელებლად ისინი უნდა დაინსტალირდეს:', + 'dependencies_retry' => 'გაუშვით ზემოთ ნაჩვენები ბრძანება ტერმინალში საიტის ძირეული კატალოგიდან, შემდეგ ხელახლა შეამოწმეთ.', + 'dependencies_title' => 'საჭიროა Composer-ის დამოკიდებულებები', 'during_execution_of_sql' => ' SQL ბრძანების შესრულებისას ', 'encoding' => 'UTF-8', 'error' => 'შეცდომა', diff --git a/install/src/lang/kk.inc.php b/install/src/lang/kk.inc.php index 7d617ff9f9..3b33a603a5 100644 --- a/install/src/lang/kk.inc.php +++ b/install/src/lang/kk.inc.php @@ -58,6 +58,7 @@ 'checking_table_prefix' => 'Кесте префиксін тексеру `', 'choose_language' => 'Тілді таңдаңыз', 'chunks' => 'Чанктар', + 'composer_dependencies_not_installed' => 'Осы нұсқаның Composer тәуелділіктерін орнату мүмкін болмады, сондықтан core/vendor әлі де алдыңғы нұсқаның тәуелділіктерін қамтиды:', 'config_permissions_note' => 'Linux/Unix жүйесіне жаңадан орнатқанда /[+MGR_DIR+]/includes/ каталогында config.inc.php атты бос файл жасап, оған 0666 рұқсатын беріңіз.', 'connection_screen_collation' => 'Салыстыру тәртібі:', 'connection_screen_database_connection_information' => 'Дерекқор туралы ақпарат', @@ -95,6 +96,10 @@ 'default_language_description' => 'Бұл Evolution CMS басқару тақтасында қолданылатын әдепкі тіл.', 'depedency_create' => 'Тәуелділік жасалды', 'depedency_update' => 'Тәуелділік жаңартылды', + 'dependencies_check_again' => 'Қайта тексеру', + 'dependencies_note' => 'Бұл нұсқа сайттың үстіне өзінің core/vendor каталогынсыз көшірілді, ал core/vendor әлі де алдыңғы нұсқаның Composer тәуелділіктерін қамтиды. Жаңартуды жалғастыру үшін оларды орнату қажет:', + 'dependencies_retry' => 'Жоғарыда көрсетілген пәрменді сайттың түбірлік каталогынан терминалда орындаңыз, содан кейін қайта тексеріңіз.', + 'dependencies_title' => 'Composer тәуелділіктері қажет', 'during_execution_of_sql' => ' SQL өрнегін орындау кезінде ', 'encoding' => 'UTF-8', 'error' => 'қате', diff --git a/install/src/lang/ky.inc.php b/install/src/lang/ky.inc.php index 8f00782e25..736eba0535 100644 --- a/install/src/lang/ky.inc.php +++ b/install/src/lang/ky.inc.php @@ -58,6 +58,7 @@ 'checking_table_prefix' => 'Таблицанын префикси текшерилүүдө `', 'choose_language' => 'Тилди тандаңыз', 'chunks' => 'Чанктар', + 'composer_dependencies_not_installed' => 'Ушул версиянын Composer көз карандылыктарын орнотуу мүмкүн болгон жок, ошондуктан core/vendor дагы эле мурунку версиянын көз карандылыктарын камтыйт:', 'config_permissions_note' => 'Linux/Unix тутумуна жаңы орнотууда /[+MGR_DIR+]/includes/ папкасында config.inc.php аттуу бош файл түзүп, анын укуктарын 0666 кылыңыз.', 'connection_screen_collation' => 'Салыштыруу тартиби:', 'connection_screen_database_connection_information' => 'Маалымат базасы жөнүндө маалымат', @@ -95,6 +96,10 @@ 'default_language_description' => 'Evolution CMS башкаруу панелинде демейки боюнча колдонула турган тил.', 'depedency_create' => 'Көз карандылык түзүлдү', 'depedency_update' => 'Көз карандылык жаңыртылды', + 'dependencies_check_again' => 'Кайра текшерүү', + 'dependencies_note' => 'Бул версия сайттын үстүнө өзүнүн core/vendor папкасысыз көчүрүлгөн, ал core/vendor дагы эле мурунку версиянын Composer көз карандылыктарын камтыйт. Жаңыртуу улантылышы үчүн аларды орнотуу керек:', + 'dependencies_retry' => 'Жогоруда көрсөтүлгөн буйрукту сайттын түпкү папкасынан терминалда аткарып, андан соң кайра текшериңиз.', + 'dependencies_title' => 'Composer көз карандылыктары талап кылынат', 'during_execution_of_sql' => ' SQL буйругун аткаруу учурунда ', 'encoding' => 'UTF-8', 'error' => 'ката', diff --git a/install/src/lang/nl.inc.php b/install/src/lang/nl.inc.php index 97f678bbd1..80d9410df9 100644 --- a/install/src/lang/nl.inc.php +++ b/install/src/lang/nl.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Controleren tabel voorvoegsel `', 'choose_language' => 'Selecteer een taal', 'chunks' => 'Chunks', + 'composer_dependencies_not_installed' => 'De Composer-afhankelijkheden van deze versie konden niet worden geïnstalleerd, dus core/vendor bevat nog die van de vorige versie:', 'config_permissions_note' => 'Voor nieuwe Linux / Unix installaties, maak een leeg bestand met de naam config.inc.php in de /[+MGR_DIR+]/includes/ map met permissie ingesteld op 0666.', 'connection_screen_collation' => 'Collatie:', 'connection_screen_database_connection_information' => 'Database informatie', @@ -89,6 +90,10 @@ 'default_language_description' => 'Dit is de standaardtaal die wordt gebruikt in de Manager.', 'depedency_create' => 'Afhankelijkheid gecreëerd', 'depedency_update' => 'Afhankelijkheid geupdate', + 'dependencies_check_again' => 'Opnieuw controleren', + 'dependencies_note' => 'Deze versie is over de site gekopieerd zonder haar eigen core/vendor, dat nog de Composer-afhankelijkheden van de vorige versie bevat. Ze moeten worden geïnstalleerd voordat de update kan doorgaan:', + 'dependencies_retry' => 'Voer de hierboven getoonde opdracht uit in een terminal vanuit de hoofdmap van de site en controleer daarna opnieuw.', + 'dependencies_title' => 'Composer-afhankelijkheden vereist', 'during_execution_of_sql' => 'tijdens de uitvoering van de SQL-statement', 'encoding' => 'iso-8859-1', 'error' => 'fout', diff --git a/install/src/lang/nn.inc.php b/install/src/lang/nn.inc.php index 420a99aec3..f915775276 100644 --- a/install/src/lang/nn.inc.php +++ b/install/src/lang/nn.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Kontrollerer tabellprefixet `', 'choose_language' => 'Velg språk', 'chunks' => 'Chunks', + 'composer_dependencies_not_installed' => 'Composer-avhengnadene i denne versjonen kunne ikkje installerast, så core/vendor inneheld framleis dei frå den førre versjonen:', 'config_permissions_note' => 'For nye installasjoner i Linux/Unix-miljø må en tom fil med navnet config.inc.php opprettes i katalogen [+MGR_DIR+]/includes/ med skriverrettighetene satt til 666.', 'connection_screen_collation' => 'Kollasjonering:', 'connection_screen_database_connection_information' => 'Databaseopplysninger', @@ -89,6 +90,10 @@ 'default_language_description' => 'This is the default language that will be used in the Evolution CMS Manager back end control panel.', 'depedency_create' => 'Depedency created', 'depedency_update' => 'Depedency updated', + 'dependencies_check_again' => 'Sjekk på nytt', + 'dependencies_note' => 'Denne versjonen vart lagd over nettstaden utan sin eigen core/vendor, som framleis inneheld Composer-avhengnadene frå den førre versjonen. Dei må installerast før oppdateringa kan halde fram:', + 'dependencies_retry' => 'Køyr kommandoen ovanfor i ein terminal frå rotmappa til nettstaden, og sjekk deretter på nytt.', + 'dependencies_title' => 'Composer-avhengnader er påkravde', 'during_execution_of_sql' => ' under kjøringen av SQL-spørringen ', 'encoding' => 'utf-8', 'error' => 'feil', diff --git a/install/src/lang/pl.inc.php b/install/src/lang/pl.inc.php index 3bdccec27e..3d0174d2d0 100644 --- a/install/src/lang/pl.inc.php +++ b/install/src/lang/pl.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Sprawdzanie prefiksu tabeli `', 'choose_language' => 'Wybierz język', 'chunks' => 'Chunki', + 'composer_dependencies_not_installed' => 'Nie udało się zainstalować zależności Composer tej wersji, więc core/vendor nadal zawiera zależności poprzedniej wersji:', 'config_permissions_note' => 'Dla nowych instalacji na systemach Linux/Unix utwórz pusty plik o nazwie config.inc.php w folderze /[+MGR_DIR+]/includes/ i ustaw dla niego prawa dostępu na 0666.', 'connection_screen_collation' => 'System porównań (collation): ', 'connection_screen_database_connection_information' => 'Informacje o bazie danych', @@ -89,6 +90,10 @@ 'default_language_description' => 'Domyślny język dla EVO Manager.', 'depedency_create' => 'Zależność utworzona', 'depedency_update' => 'Zależność uaktualniona', + 'dependencies_check_again' => 'Sprawdź ponownie', + 'dependencies_note' => 'Ta wersja została skopiowana na witrynę bez własnego katalogu core/vendor, który nadal zawiera zależności Composer poprzedniej wersji. Należy je zainstalować, zanim aktualizacja będzie mogła być kontynuowana:', + 'dependencies_retry' => 'Uruchom powyższe polecenie w terminalu z katalogu głównego witryny, a następnie sprawdź ponownie.', + 'dependencies_title' => 'Wymagane zależności Composer', 'during_execution_of_sql' => ' podczas wykonywania zapytania SQL ', 'encoding' => 'utf-8', 'error' => 'błąd', diff --git a/install/src/lang/pt.inc.php b/install/src/lang/pt.inc.php index d848a933d5..ac8f750bee 100644 --- a/install/src/lang/pt.inc.php +++ b/install/src/lang/pt.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Verificando Prefixo de Tabela `', 'choose_language' => 'Escolha o idioma', 'chunks' => 'Chunks', + 'composer_dependencies_not_installed' => 'Não foi possível instalar as dependências Composer desta versão, por isso core/vendor ainda contém as da versão anterior:', 'config_permissions_note' => 'Para novas instalações de Linux/Unix, crie um arquivo vazio chamado config.inc.php no diretório [+MGR_DIR+]/includes/ com as permissões de 0666.', 'connection_screen_collation' => 'Collation:', 'connection_screen_database_connection_information' => 'Informação da Base de Dados.', @@ -89,6 +90,10 @@ 'default_language_description' => 'Este é o idioma padrão que será usado no painel de controle do Evolution CMS.', 'depedency_create' => 'Depedency created', 'depedency_update' => 'Depedency updated', + 'dependencies_check_again' => 'Verificar novamente', + 'dependencies_note' => 'Esta versão foi copiada sobre o site sem o seu core/vendor, que ainda contém as dependências Composer da versão anterior. Têm de ser instaladas antes de a atualização poder continuar:', + 'dependencies_retry' => 'Execute o comando apresentado acima num terminal a partir da raiz do site e, em seguida, verifique novamente.', + 'dependencies_title' => 'Dependências Composer necessárias', 'during_execution_of_sql' => ' durante uma execução do SQL statement ', 'encoding' => 'iso-8859-1', 'error' => 'erro', diff --git a/install/src/lang/sk.inc.php b/install/src/lang/sk.inc.php index 9290552317..ef7bf5a3cf 100644 --- a/install/src/lang/sk.inc.php +++ b/install/src/lang/sk.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Kontrola predpony tabuliek `', 'choose_language' => 'Vyberte jazyk', 'chunks' => 'Chunky', + 'composer_dependencies_not_installed' => 'Závislosti Composer tejto verzie sa nepodarilo nainštalovať, preto core/vendor stále obsahuje závislosti predchádzajúcej verzie:', 'config_permissions_note' => 'Pri novej inštalácii na Linux/Unix vytvorte prázdny súbor config.inc.php v priečinku /[+MGR_DIR+]/includes/ s právami 0666.', 'connection_screen_collation' => 'Porovnávanie (collation):', 'connection_screen_database_connection_information' => 'Parametre databázy', @@ -89,6 +90,10 @@ 'default_language_description' => 'Toto je jazyk, ktorý bude predvolene používaný v administračnom rozhraní Evolution CMS.', 'depedency_create' => 'Závislosti vytvorené ', 'depedency_update' => 'Závislosti aktualizované', + 'dependencies_check_again' => 'Skontrolovať znova', + 'dependencies_note' => 'Táto verzia bola nahratá cez stránku bez svojho priečinka core/vendor, ktorý stále obsahuje závislosti Composer predchádzajúcej verzie. Pred pokračovaním aktualizácie ich treba nainštalovať:', + 'dependencies_retry' => 'Spustite vyššie uvedený príkaz v termináli z koreňového priečinka stránky a potom skontrolujte znova.', + 'dependencies_title' => 'Vyžadujú sa závislosti Composer', 'during_execution_of_sql' => ' počas vykonávania SQL dopytu ', 'encoding' => 'utf-8', 'error' => 'chyby', diff --git a/install/src/lang/sv.inc.php b/install/src/lang/sv.inc.php index 85771e1894..e4ee880d4b 100644 --- a/install/src/lang/sv.inc.php +++ b/install/src/lang/sv.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Kontrollerar tabellprefixet `', 'choose_language' => 'Välj språk', 'chunks' => 'Chunks', + 'composer_dependencies_not_installed' => 'Composer-beroendena för den här versionen kunde inte installeras, så core/vendor innehåller fortfarande den föregående versionens:', 'config_permissions_note' => 'För nya installationer i Linux/Unix-miljö måste en tom fil med namnet config.inc.php skapas i katalogen /[+MGR_DIR+]/includes/ med åtkomsträttigheterna satta till 0666.', 'connection_screen_collation' => 'Kollationering:', 'connection_screen_database_connection_information' => 'Databasuppgifter', @@ -89,6 +90,10 @@ 'default_language_description' => 'Det här är det standardspråk som kommer att användas i Evolution CMS hanterare.', 'depedency_create' => 'Beroende skapat', 'depedency_update' => 'Beroende uppdaterat', + 'dependencies_check_again' => 'Kontrollera igen', + 'dependencies_note' => 'Den här versionen lades över webbplatsen utan sin core/vendor, som fortfarande innehåller den föregående versionens Composer-beroenden. De måste installeras innan uppdateringen kan fortsätta:', + 'dependencies_retry' => 'Kör kommandot ovan i en terminal från webbplatsens rotkatalog och kontrollera sedan igen.', + 'dependencies_title' => 'Composer-beroenden krävs', 'during_execution_of_sql' => ' under körningen av SQL-frågan ', 'encoding' => 'utf-8', 'error' => 'fel', diff --git a/install/src/lang/tg.inc.php b/install/src/lang/tg.inc.php index 0b91843543..62e5c3304d 100644 --- a/install/src/lang/tg.inc.php +++ b/install/src/lang/tg.inc.php @@ -58,6 +58,7 @@ 'checking_table_prefix' => 'Санҷиши пешванди ҷадвал `', 'choose_language' => 'Интихоби забон', 'chunks' => 'Чанкҳо', + 'composer_dependencies_not_installed' => 'Вобастагиҳои Composer-и ин версия насб нашуданд, бинобар ин core/vendor ҳанӯз вобастагиҳои версияи қаблиро дар бар мегирад:', 'config_permissions_note' => 'Барои насби нави Linux/Unix дар ҷузвдони /[+MGR_DIR+]/includes/ файли холии config.inc.php-ро бо иҷозати 0666 созед.', 'connection_screen_collation' => 'Муқоисаи аломатҳо:', 'connection_screen_database_connection_information' => 'Маълумоти пойгоҳи додаҳо', @@ -95,6 +96,10 @@ 'default_language_description' => 'Ин забони пешфарз дар панели идоракунии Evolution CMS истифода мешавад.', 'depedency_create' => 'Вобастагӣ сохта шуд', 'depedency_update' => 'Вобастагӣ навсозӣ шуд', + 'dependencies_check_again' => 'Аз нав санҷидан', + 'dependencies_note' => 'Ин версия бе core/vendor-и худ болои сомона нусхабардорӣ шудааст, core/vendor бошад ҳанӯз вобастагиҳои Composer-и версияи қаблиро дар бар мегирад. Пеш аз идомаи навсозӣ онҳоро насб кардан лозим аст:', + 'dependencies_retry' => 'Фармони дар боло нишондодашударо аз решаи сомона дар терминал иҷро кунед, сипас аз нав санҷед.', + 'dependencies_title' => 'Вобастагиҳои Composer лозиманд', 'during_execution_of_sql' => ' ҳангоми иҷрои фармони SQL ', 'encoding' => 'UTF-8', 'error' => 'хато', diff --git a/install/src/lang/tk.inc.php b/install/src/lang/tk.inc.php index 8fb255d117..570fcfa39b 100644 --- a/install/src/lang/tk.inc.php +++ b/install/src/lang/tk.inc.php @@ -58,6 +58,7 @@ 'checking_table_prefix' => 'Tablisa prefiksi barlanýar `', 'choose_language' => 'Dili saýlaň', 'chunks' => 'Çanklar', + 'composer_dependencies_not_installed' => 'Bu wersiýanyň Composer baglylyklaryny gurnap bolmady, şonuň üçin core/vendor henizem öňki wersiýanyň baglylyklaryny saklaýar:', 'config_permissions_note' => 'Täze Linux/Unix gurnamalary üçin /[+MGR_DIR+]/includes/ katalogynda config.inc.php atly boş faýl dörediň we onuň rugsatlaryny 0666 edip belläň.', 'connection_screen_collation' => 'Deňeşdirme tertibi:', 'connection_screen_database_connection_information' => 'Maglumat binýady barada maglumat', @@ -95,6 +96,10 @@ 'default_language_description' => 'Bu Evolution CMS dolandyryş panelinde ulanyljak deslapky dildir.', 'depedency_create' => 'Baglylyk döredildi', 'depedency_update' => 'Baglylyk täzelendi', + 'dependencies_check_again' => 'Täzeden barla', + 'dependencies_note' => 'Bu wersiýa öz core/vendor bukjasy bolmazdan saýtyň üstüne göçürildi, core/vendor bolsa henizem öňki wersiýanyň Composer baglylyklaryny saklaýar. Täzeleme dowam etmezden öň olary gurnamaly:', + 'dependencies_retry' => 'Ýokarda görkezilen buýrugy saýtyň kök katalogyndan terminalda ýerine ýetiriň, soňra täzeden barlaň.', + 'dependencies_title' => 'Composer baglylyklary gerek', 'during_execution_of_sql' => ' SQL buýrugy ýerine ýetirilýärkä ', 'encoding' => 'UTF-8', 'error' => 'ýalňyşlyk', diff --git a/install/src/lang/uk.inc.php b/install/src/lang/uk.inc.php index 8fe2fd6331..784f95813a 100644 --- a/install/src/lang/uk.inc.php +++ b/install/src/lang/uk.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Перевірка префіксу таблиць `', 'choose_language' => 'Оберіть мову', 'chunks' => 'Чанки', + 'composer_dependencies_not_installed' => 'Не вдалося встановити залежності Composer цієї версії, тому core/vendor досі містить залежності попередньої версії:', 'config_permissions_note' => 'Для нової Linux/Unix‑інсталяції створіть порожній файл config.inc.php у папці /[+MGR_DIR+]/includes/ з правами 0666.', 'connection_screen_collation' => 'Порівняння:', 'connection_screen_database_connection_information' => 'Параметри бази даних', @@ -89,6 +90,10 @@ 'default_language_description' => 'Це мова, яка за умовчанням використовуватиметься менеджером в адміністративній панелі Evolution CMS.', 'depedency_create' => 'Залежності створено ', 'depedency_update' => 'Залежності оновлено', + 'dependencies_check_again' => 'Перевірити ще раз', + 'dependencies_note' => 'Цю версію скопійовано поверх сайту без її core/vendor, тож core/vendor досі містить залежності Composer попередньої версії. Їх потрібно встановити, перш ніж оновлення зможе продовжитися:', + 'dependencies_retry' => 'Виконайте наведену вище команду в терміналі з кореневої теки сайту, а потім перевірте ще раз.', + 'dependencies_title' => 'Потрібні залежності Composer', 'during_execution_of_sql' => ' під час виконання SQL‑запиту ', 'encoding' => 'utf-8', 'error' => 'помилка', diff --git a/install/src/lang/zh.inc.php b/install/src/lang/zh.inc.php index b83f255c95..521938b537 100644 --- a/install/src/lang/zh.inc.php +++ b/install/src/lang/zh.inc.php @@ -52,6 +52,7 @@ 'checking_table_prefix' => 'Checking table prefix `', 'choose_language' => 'Choose language', 'chunks' => 'Chunks', + 'composer_dependencies_not_installed' => '无法安装此版本的 Composer 依赖,因此 core/vendor 中仍是上一版本的依赖:', 'config_permissions_note' => 'For new Linux/Unix installs, please create a blank file named config.inc.php in the /[+MGR_DIR+]/includes/ directory with file permissions set to 0666.', 'connection_screen_collation' => 'Collation:', 'connection_screen_database_connection_information' => 'Database information', @@ -89,6 +90,10 @@ 'default_language_description' => 'This is the default language that will be used in the Evolution CMS Manager back end control panel.', 'depedency_create' => 'Depedency created', 'depedency_update' => 'Depedency updated', + 'dependencies_check_again' => '重新检查', + 'dependencies_note' => '此版本是在不含其 core/vendor 的情况下覆盖到网站上的,core/vendor 中仍是上一版本的 Composer 依赖。必须先安装这些依赖,更新才能继续:', + 'dependencies_retry' => '请在终端中从网站根目录运行上方显示的命令,然后重新检查。', + 'dependencies_title' => '需要 Composer 依赖', 'during_execution_of_sql' => ' during the execution of SQL statement ', 'encoding' => 'iso-8859-1', 'error' => 'error', diff --git a/install/src/template/actions/dependencies.php b/install/src/template/actions/dependencies.php new file mode 100644 index 0000000000..949c02ea8a --- /dev/null +++ b/install/src/template/actions/dependencies.php @@ -0,0 +1,41 @@ + +

+

+
+ + +

+ + +

+ +

+ + +

+ +
+ + + +

+

+ + +
+ + +

+