diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 00000000..db50b9b8 --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,66 @@ +# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: PHPUnit + +on: + pull_request: + push: + branches: + - main + - master + - stable* + +permissions: + contents: read + +concurrency: + group: phpunit-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + phpunit: + runs-on: ubuntu-latest + strategy: + matrix: + php-versions: ["8.2", "8.3", "8.4"] + + name: PHPUnit PHP ${{ matrix.php-versions }} + + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - name: Set up php ${{ matrix.php-versions }} + uses: shivammathur/setup-php@ec406be512d7077f68eed36e63f4d91bc006edc4 # v2.35.4 + with: + php-version: ${{ matrix.php-versions }} + extensions: ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib + coverage: none + ini-file: development + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + run: | + composer i + composer bin phpunit install + + - name: PHPUnit + run: composer run test:unit + + summary: + permissions: + contents: none + runs-on: ubuntu-latest-low + needs: phpunit + + if: always() + + name: phpunit-summary + + steps: + - name: Summary status + run: if ${{ needs.phpunit.result != 'success' && needs.phpunit.result != 'skipped' }}; then exit 1; fi diff --git a/.gitignore b/.gitignore index 75a13176..4eacecc9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ /vendor/symfony/service-contracts/Test /vendor-bin/**/vendor .php-cs-fixer.cache +.phpunit.result.cache diff --git a/Makefile b/Makefile index 273f7e7c..2a6503c7 100644 --- a/Makefile +++ b/Makefile @@ -21,9 +21,15 @@ index.php: lib/UpdateException.php lib/LogException.php lib/Updater.php index.we test/vendor: composer bin tests install +vendor/bin/phpunit: + composer bin phpunit install + test: updater.phar test/vendor cd tests && ../vendor/bin/behat +test-unit: vendor/bin/phpunit + composer run test:unit + test-cli: updater.phar test/vendor cd tests && ../vendor/bin/behat features/cli.feature diff --git a/README.md b/README.md index c518e93c..4c23de2e 100644 --- a/README.md +++ b/README.md @@ -192,29 +192,47 @@ Described here are both relevant aspects of the Updater itself as well as surrou ### Updater components -For each mode the Updater supports - Web and command line - a dedicated artifact is generated. However, all common operations are located in shared code. Since the code is not shared in all cases at runtime, it's important to understand where various changes should go during development so that they end up in the appropriate places at build or check-in time. +For each mode the Updater supports - Web and command line - a dedicated artifact is generated. However, all common operations are located +in shared code. Since the code is not shared in all cases at runtime, it's important to understand where various changes should go during +development so that they end up in the appropriate places at build or check-in time. Changes should be made to the following places in the `updater` repo: * Shared aspects of the Updater: - - `/Makefile` - - `make updater.phar` - - `make index.php` - `/lib/LogException.php` - - `/lib/RecursiveDirectoryIteratorWithoutDate.php` - `/lib/UpdateException.php` - `/lib/Updater.php` <-- core of the Updater functionality for both Web and CLI modes is implemented here * Aspects specific to the Web Updater: - `/index.web.php` - - `/Makefile` - - `make index.php` * Aspects specific to the CLI Updater: - `/updater.php` - `/buildVersionFile.php` - `/lib/CommandApplication.php` - `/lib/UpdateCommand.php` - - `/Makefile` - - `make updater.phar` + +#### Build system (`/Makefile`) + +The Makefile contains targets for both modes. The relevant targets are: + +* `make updater.phar` — builds the CLI Updater artifact +* `make index.php` — builds the Web Updater artifact +* `make check-same-code-base` — verifies that `index.php` contains the same shared library code as the individual files in `/lib/` <-- checked by CI + +**How `index.php` is built:** The Web Updater runs as a single self-contained PHP file. `make index.php` produces it by *concatenating* `index.web.php` +with the shared library files (`UpdateException.php`, `LogException.php`, `Updater.php`), stripping `namespace`/`use` statements and duplicate PHP +opening tags via `awk`/`grep`. This means that after changing any shared `/lib/*.php` file or `index.web.php`, you must re-run `make index.php` and +check in the resulting `/index.php`. + +**How `updater.phar` is built:** `make updater.phar` generates a transient `/lib/Version.php` (via `buildVersionFile.php`), runs `composer dump-autoload`, +then uses [box](https://github.com/box-project/box) (configured in `/box.json`) to package `/updater.php`, `/lib/*.php`, and `/vendor/*.php` into the +phar. The transient `Version.php` is deleted after the build. After changing any CLI-specific or shared file, you must re-run `make updater.phar` and +check in the resulting `/updater.phar`. + +#### Build configuration files + +* `/box.json` — Configures which files are packaged into `updater.phar` (the `lib/` directory, vendor PHP files, with `updater.php` as the main entry point) +* `/composer.json` / `/composer.lock` — Manage PHP dependencies; the CLI Updater bootstraps via `vendor/autoload.php` +* `/vendor/` — Checked in to the repo (used at runtime by the CLI Updater and during the phar build) #### Server components @@ -239,15 +257,35 @@ Keep in mind that for the update/upgrade process there are some additional compo - Handles database upgrade migrations - Handles app updates (i.e. for compatibility with a new major version of Server) -### Dependences needed for building +### Dependencies needed for building #### box Install box: https://github.com/box-project/box/blob/main/doc/installation.md#composer +#### Composer + +A working `composer` install is required both for dependency management and for building. See "Testing" below for version requirements. + #### Tests -If you want to run the tests locally, you'll need to run them in an environment that has Nextcloud's required PHP modules installed. The various test scenarios are all available via the `make test*` (see Makefile for specifics). +If you want to run the tests locally, you'll need to run them in an environment that has Nextcloud's required PHP modules installed. +Tests use [Behat](https://docs.behat.org/) (a BDD framework); feature files live under `tests/features/`. Test data (gitignored) is +generated under `tests/data/`. + +The available test targets are: + +| Target | Description | +|---|---| +| `make test` | Runs all Behat feature tests | +| `make test-cli` | Runs only the CLI updater tests (`features/cli.feature`) | +| `make test-stable26` | Tests update path for stable26 | +| `make test-stable27` | Tests update path for stable27 | +| `make test-stable28` | Tests update path for stable28 | +| `make test-master` | Tests update path for master | +| `make test-user.ini` | Tests `.user.ini` handling (`features/user.ini.feature`) | +| `make check-same-code-base` | Verifies `/index.php` is in sync with `/lib/*.php` + `/index.web.php` | +| `make build-and-local-test` | Builds the phar then runs a local updater smoke test | ### Build artifacts / What to check in @@ -269,16 +307,23 @@ Plus whatever has been changed in the implementation in: #### Transient -Used during the build process but not checked in: +Used during the build process but not checked in (gitignored): * Specific to the CLI Updater: - - `/lib/Version.php` + - `/lib/Version.php` — auto-generated by `buildVersionFile.php`, deleted after `make updater.phar` completes ### Testing -#### Check same code base test keeps failing +#### `check-same-code-base` test keeps failing + +This test (`make check-same-code-base`) runs `tests/checkSameCodeBase.php`, which verifies that the contents of each shared library file in `/lib/` +(excluding CLI-only files `CommandApplication.php` and `UpdateCommand.php`) are present verbatim inside the built `/index.php`. If it fails, it +usually means: -If it keeps failing on your PR, confirm your local version of `composer` is the same version in-use in the workflow runner. You can check the details of the test run and find the version currently being used (and therefore required locally) under "Setup Tools". (Hint: distro versions are typically too outdated. Remove that version and see https://getcomposer.org/download/ to install your own version). +1. **You changed a file in `/lib/` but didn't rebuild `index.php`.** Run `make index.php` and commit the result. +2. **Your local `composer` version differs from CI.** Confirm your local version of `composer` is the same version in-use in the workflow runner. + You can check the details of the test run and find the version currently being used (and therefore required locally) under "Setup Tools". (Hint: + distro versions are typically too outdated. Remove that version and see https://getcomposer.org/download/ to install your own version). #### CI diff --git a/REUSE.toml b/REUSE.toml index ccd18bd4..6a863295 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -24,7 +24,7 @@ SPDX-FileCopyrightText = "2020 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "MIT" [[annotations]] -path = ["vendor-bin/box/**", "vendor-bin/psalm/**", "vendor-bin/rector/**"] +path = ["vendor-bin/box/**", "vendor-bin/phpunit/**", "vendor-bin/psalm/**", "vendor-bin/rector/**"] precedence = "aggregate" SPDX-FileCopyrightText = "2023 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" diff --git a/composer.json b/composer.json index d05b7108..85dba635 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,8 @@ "psalm": "psalm --threads=$(nproc)", "psalm:ci": "psalm --threads=1", "psalm:fix": "- --issues=InvalidReturnType,InvalidNullableReturnType,MissingParamType,InvalidFalsableReturnType", - "rector": "rector && composer run cs:fix" + "rector": "rector && composer run cs:fix", + "test:unit": "phpunit -c phpunit.xml" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8" diff --git a/index.php b/index.php index 277bfa7e..44259871 100644 --- a/index.php +++ b/index.php @@ -81,7 +81,12 @@ public function __construct( throw new \Exception('Could not read data directory from config.php.'); } - $versionFileName = $this->nextcloudDir . '/version.php'; + $versionFileName = $this->buildPath('version.php'); + // Invalidate version.php OPcache entry to assure file_exists() returns + // false after files were removed while opcache.enable_file_override is set. + if (function_exists('opcache_invalidate')) { + opcache_invalidate($versionFileName, true); + } if (!file_exists($versionFileName)) { // fallback to version in config.php $version = $this->getConfigOptionString('version'); @@ -111,6 +116,17 @@ public function __construct( $this->buildTime = $buildTime; } + /** + * Builds an absolute path by joining the Nextcloud root directory with the provided relative path. + * Handles leading and trailing slashes to prevent double-slash issues. + * + * @param string $suffix Relative path to append (with or without leading slash) + * @return string The absolute path + */ + public function buildPath(string $suffix): string { + return rtrim($this->nextcloudDir, '/') . '/' . ltrim($suffix, '/'); + } + /** * @return array{array, string} */ @@ -121,7 +137,7 @@ private function readConfigFile(): array { throw new \Exception('Configuration not found in ' . $dir); } } else { - $configFileName = $this->nextcloudDir . '/config/config.php'; + $configFileName = $this->buildPath('config/config.php'); } if (!file_exists($configFileName)) { @@ -335,8 +351,8 @@ private function getAppDirectories(): array { throw new \Exception('Invalid configuration in apps_paths configuration key'); } - if (str_starts_with($appsPath['path'], $this->nextcloudDir . '/')) { - $relativePath = substr($appsPath['path'], strlen($this->nextcloudDir . '/')); + if (str_starts_with($appsPath['path'], $this->buildPath(''))) { + $relativePath = substr($appsPath['path'], strlen($this->buildPath(''))); if ($relativePath !== 'apps') { $expected[] = $relativePath; } @@ -436,13 +452,13 @@ public function checkWritePermissions(): void { } // Special handling for included default theme - foreach ($this->getRecursiveDirectoryIterator($this->nextcloudDir . '/themes/example', $excludedElements) as $fileInfo) { + foreach ($this->getRecursiveDirectoryIterator($this->buildPath('themes/example'), $excludedElements) as $fileInfo) { if (!$fileInfo->isWritable()) { $notWritablePaths[] = $fileInfo->getFilename(); } } - $themesReadmeFileInfo = new \SplFileInfo($this->nextcloudDir . '/themes/README'); + $themesReadmeFileInfo = new \SplFileInfo($this->buildPath('themes/README')); if (!$themesReadmeFileInfo->isWritable()) { $notWritablePaths[] = $themesReadmeFileInfo->getFilename(); } @@ -502,14 +518,14 @@ public function createBackup(): void { } foreach ($this->getRecursiveDirectoryIterator($this->nextcloudDir, $excludedElements) as $absolutePath => $fileInfo) { - $relativePath = explode($this->nextcloudDir, $absolutePath)[1]; + $relativePath = ltrim(substr($absolutePath, strlen($this->nextcloudDir)), '/'); $relativeDirectory = dirname($relativePath); // Create folder if it doesn't exist - if (!file_exists($backupFolderLocation . '/' . $relativeDirectory)) { - $state = mkdir($backupFolderLocation . '/' . $relativeDirectory, 0750, true); + if (!file_exists($backupFolderLocation . $relativeDirectory)) { + $state = mkdir($backupFolderLocation . $relativeDirectory, 0750, true); if ($state === false) { - throw new \Exception('Could not create folder: ' . $backupFolderLocation . '/' . $relativeDirectory); + throw new \Exception('Could not create folder: ' . $backupFolderLocation . $relativeDirectory); } } @@ -574,28 +590,64 @@ private function getUpdateServerResponse(): array { $updateURL = $updaterServer . '?version=' . str_replace('.', 'x', $this->getConfigOptionMandatoryString('version')) . 'xxx' . $releaseChannel . 'xx' . urlencode($this->buildTime) . 'x' . PHP_MAJOR_VERSION . 'x' . PHP_MINOR_VERSION . 'x' . PHP_RELEASE_VERSION; $this->silentLog('[info] updateURL: ' . $updateURL); + $maxRetries = 2; + + for ($attempt = 1; $attempt <= $maxRetries; $attempt++) { + try { + return $this->fetchUpdateServerResponse($updateURL); + } catch (\Exception $e) { + $lastException = $e; + $this->silentLog('[warn] attempt ' . $attempt . '/' . $maxRetries . ' failed: ' . $e->getMessage()); + if ($attempt < $maxRetries) { + sleep(1); + } + } + } + + throw $lastException; + } + + /** + * @throws \Exception + */ + private function fetchUpdateServerResponse(string $updateURL): array { // Download update response $curl = $this->getCurl($updateURL); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10); + curl_setopt($curl, CURLOPT_TIMEOUT, 30); /** @var false|string $response */ $response = curl_exec($curl); + if ($response === false) { - throw new \Exception('Could not do request to updater server: ' . curl_error($curl)); + $curlError = curl_error($curl); + curl_close($curl); + throw new \Exception('Could not do request to updater server: ' . $curlError); } + $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl); + if ($httpCode !== 200 && $httpCode !== 204) { + $this->silentLog('[warn] update server returned HTTP ' . $httpCode); + throw new \Exception('Update server returned unexpected HTTP status ' . $httpCode); + } + // Response can be empty when no update is available if ($response === '') { return []; } libxml_use_internal_errors(true); - $xml = simplexml_load_string($response); - if ($xml === false) { - $content = strlen($response) > 200 ? substr($response, 0, 200) . '…' : $response; - $errors = implode("\n", array_map(fn ($error) => $error->message, libxml_get_errors())); - throw new \Exception('Could not parse updater server XML response: ' . $content . "\nErrors:\n" . $errors); + try { + $xml = simplexml_load_string($response); + if ($xml === false) { + $content = strlen($response) > 200 ? substr($response, 0, 200) . '…' : $response; + $errors = implode("\n", array_map(fn ($error) => $error->message, libxml_get_errors())); + throw new \Exception('Could not parse updater server XML response: ' . $content . "\nErrors:\n" . $errors); + } + } finally { + libxml_clear_errors(); } $response = get_object_vars($xml); @@ -615,11 +667,11 @@ private function getUpdateServerResponse(): array { * * @throws \Exception */ - public function downloadUpdate(string $url = '', ?Closure $downloadProgress = null): void { + public function downloadUpdate(string $urlOverride = '', ?Closure $downloadProgress = null): void { $this->silentLog('[info] downloadUpdate()'); $this->downloadProgress = $downloadProgress; - $downloadURLs = $url !== '' ? [$url] : $this->getDownloadURLs(); + $downloadURLs = $urlOverride !== '' ? [$urlOverride] : $this->getDownloadURLs(); $this->silentLog('[info] will try to download archive from: ' . implode(', ', $downloadURLs)); @@ -641,10 +693,10 @@ public function downloadUpdate(string $url = '', ?Closure $downloadProgress = nu } } - foreach ($downloadURLs as $url) { + foreach ($downloadURLs as $urlOverride) { $this->previousProgress = 0; - $saveLocation = $storageLocation . basename((string)$url); - if ($this->downloadArchive($url, $saveLocation)) { + $saveLocation = $storageLocation . basename((string)$urlOverride); + if ($this->downloadArchive($urlOverride, $saveLocation)) { return; } } @@ -830,7 +882,7 @@ private function getDownloadedFilePath(): string { * * @throws \Exception */ - public function verifyIntegrity(string $urlOverride = ''): void { + public function verifyIntegrity(string $urlOverride = '', string $signature = ''): void { $this->silentLog('[info] verifyIntegrity()'); if ($this->getCurrentReleaseChannel() === 'daily') { @@ -838,18 +890,13 @@ public function verifyIntegrity(string $urlOverride = ''): void { return; } - if ($urlOverride !== '') { - $this->silentLog('[info] custom download url provided, cannot verify signature'); - return; - } - - $response = $this->getUpdateServerResponse(); - if (empty($response['signature'])) { - throw new \Exception('No signature specified for defined update'); - } - - if (!is_string($response['signature'])) { - throw new \Exception('Signature specified for defined update should be a string'); + if ($signature === '') { + if ($urlOverride !== '') { + throw new \Exception( + 'Custom download url provided. You need to provide a signature with --signature or skip integrity check with --no-verify.' + ); + } + $signature = $this->getSignatureFromUpdater(); } $certificate = <<getDownloadedFilePath()), - base64_decode($response['signature']), + base64_decode($signature), $certificate, OPENSSL_ALGO_SHA512 ) === 1; @@ -896,6 +943,19 @@ public function verifyIntegrity(string $urlOverride = ''): void { $this->silentLog('[info] end of verifyIntegrity()'); } + private function getSignatureFromUpdater(): string { + $response = $this->getUpdateServerResponse(); + if (empty($response['signature'])) { + throw new \Exception('No signature specified for defined update'); + } + + if (!is_string($response['signature'])) { + throw new \Exception('Signature specified for defined update should be a string'); + } + + return $response['signature']; + } + /** * Gets the version as declared in $versionFile * @@ -949,7 +1009,7 @@ public function extractDownload(): void { // Ensure that the downloaded version is not lower $downloadedVersion = $this->getVersionByVersionFile(dirname($downloadedFilePath) . '/nextcloud/version.php'); - $currentVersion = $this->getVersionByVersionFile($this->nextcloudDir . '/version.php'); + $currentVersion = $this->getVersionByVersionFile($this->buildPath('version.php')); if (version_compare($downloadedVersion, $currentVersion, '<')) { throw new \Exception('Downloaded version is lower than installed version'); } @@ -977,7 +1037,7 @@ public function replaceEntryPoints(): void { $content = "silentLog('[info] replace ' . $file); - $parentDir = dirname($this->nextcloudDir . '/' . $file); + $parentDir = dirname($this->buildPath($file)); if (!file_exists($parentDir)) { $r = mkdir($parentDir); if (!$r) { @@ -985,7 +1045,7 @@ public function replaceEntryPoints(): void { } } - $state = file_put_contents($this->nextcloudDir . '/' . $file, $content); + $state = file_put_contents($this->buildPath($file), $content); if ($state === false) { throw new \Exception("Can't replace entry point: " . $file); } @@ -1028,7 +1088,7 @@ private function recursiveDelete(string $folder): void { public function deleteOldFiles(): void { $this->silentLog('[info] deleteOldFiles()'); - $shippedAppsFile = $this->nextcloudDir . '/core/shipped.json'; + $shippedAppsFile = $this->buildPath('core/shipped.json'); $shippedAppsFileContent = file_get_contents($shippedAppsFile); if ($shippedAppsFileContent === false) { throw new \Exception('core/shipped.json is not available'); @@ -1056,10 +1116,10 @@ public function deleteOldFiles(): void { $shippedApps = array_merge($shippedApps, $newShippedApps); /** @var string $app */ foreach ($shippedApps as $app) { - $this->recursiveDelete($this->nextcloudDir . '/apps/' . $app); + $this->recursiveDelete($this->buildPath('apps/' . $app)); } - $configSampleFile = $this->nextcloudDir . '/config/config.sample.php'; + $configSampleFile = $this->buildPath('config/config.sample.php'); if (file_exists($configSampleFile)) { $this->silentLog('[info] config sample exists'); @@ -1070,7 +1130,7 @@ public function deleteOldFiles(): void { } } - $themesReadme = $this->nextcloudDir . '/themes/README'; + $themesReadme = $this->buildPath('themes/README'); if (file_exists($themesReadme)) { $this->silentLog('[info] themes README exists'); @@ -1081,7 +1141,7 @@ public function deleteOldFiles(): void { } } - $this->recursiveDelete($this->nextcloudDir . '/themes/example/'); + $this->recursiveDelete($this->buildPath('themes/example/')); // Delete the rest $excludedElements = [ @@ -1128,23 +1188,23 @@ private function moveWithExclusions(string $dataLocation, array $excludedElement } - $fileName = explode($dataLocation, $path)[1]; + $fileName = substr($path, strlen($dataLocation)); if ($fileInfo->isFile()) { - if (!file_exists($this->nextcloudDir . '/' . dirname($fileName))) { - $state = mkdir($this->nextcloudDir . '/' . dirname($fileName), 0755, true); + if (!file_exists($this->buildPath(dirname($fileName)))) { + $state = mkdir($this->buildPath(dirname($fileName)), 0755, true); if ($state === false) { - throw new \Exception('Could not mkdir ' . $this->nextcloudDir . '/' . dirname($fileName)); + throw new \Exception('Could not mkdir ' . $this->buildPath(dirname($fileName))); } } - $state = @rename($path, $this->nextcloudDir . '/' . $fileName); + $state = @rename($path, $this->buildPath($fileName)); if ($state === false) { throw new \Exception( sprintf( 'Could not rename %s to %s', $path, - $this->nextcloudDir . '/' . $fileName + $this->buildPath($fileName) ) ); } @@ -1217,7 +1277,7 @@ public function finalize(): void { $user_ini_additional_lines = implode(PHP_EOL, $user_ini_additional_lines); } - $result = file_put_contents($this->nextcloudDir . '/.user.ini', PHP_EOL . '; Additional settings from config.php:' . PHP_EOL . $user_ini_additional_lines . PHP_EOL, FILE_APPEND); + $result = file_put_contents($this->buildPath('.user.ini'), PHP_EOL . '; Additional settings from config.php:' . PHP_EOL . $user_ini_additional_lines . PHP_EOL, FILE_APPEND); if ($result === false) { throw new \Exception('Could not append to .user.ini'); } @@ -1987,7 +2047,15 @@ public function isAuthenticated(): bool {

To login you need to provide the unhashed value of "updater.secret" in your config file.

If you don't know that value, you can access this updater directly via the Nextcloud admin screen or generate your own secret:

- php -r '$password = trim(shell_exec("openssl rand -base64 48"));if(strlen($password) === 64) {$hash = password_hash($password, PASSWORD_DEFAULT) . "\n"; echo "Insert as \"updater.secret\": ".$hash; echo "The plaintext value is: ".$password."\n";}else{echo "Could not execute OpenSSL.\n";};' + php -r '$password = trim(shell_exec("openssl rand -base64 48")); + if (strlen($password) === 64) { + $hash = password_hash($password, PASSWORD_DEFAULT); + echo "Insert this line in your config.php:\n\n"; + echo "'\''updater.secret'\'' => '\''" . $hash . "'\'',\n\n"; + echo "The plaintext value is: " . $password . "\n"; + } else { + echo "Could not execute OpenSSL.\n"; + }'
To login you need to provide the unhashed value of "updater.secret" in your config file.

If you don't know that value, you can access this updater directly via the Nextcloud admin screen or generate your own secret:

- php -r '$password = trim(shell_exec("openssl rand -base64 48"));if(strlen($password) === 64) {$hash = password_hash($password, PASSWORD_DEFAULT) . "\n"; echo "Insert as \"updater.secret\": ".$hash; echo "The plaintext value is: ".$password."\n";}else{echo "Could not execute OpenSSL.\n";};' + php -r '$password = trim(shell_exec("openssl rand -base64 48")); + if (strlen($password) === 64) { + $hash = password_hash($password, PASSWORD_DEFAULT); + echo "Insert this line in your config.php:\n\n"; + echo "'\''updater.secret'\'' => '\''" . $hash . "'\'',\n\n"; + echo "The plaintext value is: " . $password . "\n"; + } else { + echo "Could not execute OpenSSL.\n"; + }'
addOption('no-upgrade', null, InputOption::VALUE_NONE, "Don't automatically run occ upgrade") ->addOption('url', null, InputOption::VALUE_OPTIONAL, 'The URL of the Nextcloud release to download') ->addOption('ignore-state', null, InputOption::VALUE_NONE, 'Ignore known state from .step file, do a complete update') + ->addOption('no-verify', null, InputOption::VALUE_NONE, 'Skip integrity verification of the downloaded file') + ->addOption('signature', null, InputOption::VALUE_OPTIONAL, 'Base64 signature of the archive (use it in combination with --url option)') ; } @@ -73,7 +78,9 @@ public static function getUpdaterVersion(): string { protected function execute(InputInterface $input, OutputInterface $output): int { $this->skipBackup = (bool)$input->getOption('no-backup'); $this->skipUpgrade = (bool)$input->getOption('no-upgrade'); + $this->skipIntegrityCheck = (bool)$input->getOption('no-verify'); $this->urlOverride = (string)$input->getOption('url'); + $this->signature = (string)$input->getOption('signature'); $this->ignoreState = (bool)$input->getOption('ignore-state'); $version = static::getUpdaterVersion(); @@ -166,6 +173,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $updateString = $this->updater->checkForUpdate(); } + if ($this->skipIntegrityCheck) { + $this->updater->log('[warn] Integrity check of the downloaded file will be skipped'); + $output->writeln('Integrity check of the downloaded file will be skipped.'); + } + $output->writeln(''); $lines = explode('
', $updateString); @@ -450,7 +462,11 @@ protected function executeStep(int $step, OutputInterface $output): array { }); break; case 5: - $this->updater->verifyIntegrity($this->urlOverride); + if ($this->skipIntegrityCheck) { + $this->updater->silentLog('[info] Skipping integrity check as requested'); + break; + } + $this->updater->verifyIntegrity($this->urlOverride, $this->signature); break; case 6: $this->updater->extractDownload(); diff --git a/lib/Updater.php b/lib/Updater.php index 9a915277..cc03054a 100644 --- a/lib/Updater.php +++ b/lib/Updater.php @@ -65,7 +65,12 @@ public function __construct( throw new \Exception('Could not read data directory from config.php.'); } - $versionFileName = $this->nextcloudDir . '/version.php'; + $versionFileName = $this->buildPath('version.php'); + // Invalidate version.php OPcache entry to assure file_exists() returns + // false after files were removed while opcache.enable_file_override is set. + if (function_exists('opcache_invalidate')) { + opcache_invalidate($versionFileName, true); + } if (!file_exists($versionFileName)) { // fallback to version in config.php $version = $this->getConfigOptionString('version'); @@ -95,6 +100,17 @@ public function __construct( $this->buildTime = $buildTime; } + /** + * Builds an absolute path by joining the Nextcloud root directory with the provided relative path. + * Handles leading and trailing slashes to prevent double-slash issues. + * + * @param string $suffix Relative path to append (with or without leading slash) + * @return string The absolute path + */ + public function buildPath(string $suffix): string { + return rtrim($this->nextcloudDir, '/') . '/' . ltrim($suffix, '/'); + } + /** * @return array{array, string} */ @@ -105,7 +121,7 @@ private function readConfigFile(): array { throw new \Exception('Configuration not found in ' . $dir); } } else { - $configFileName = $this->nextcloudDir . '/config/config.php'; + $configFileName = $this->buildPath('config/config.php'); } if (!file_exists($configFileName)) { @@ -319,8 +335,8 @@ private function getAppDirectories(): array { throw new \Exception('Invalid configuration in apps_paths configuration key'); } - if (str_starts_with($appsPath['path'], $this->nextcloudDir . '/')) { - $relativePath = substr($appsPath['path'], strlen($this->nextcloudDir . '/')); + if (str_starts_with($appsPath['path'], $this->buildPath(''))) { + $relativePath = substr($appsPath['path'], strlen($this->buildPath(''))); if ($relativePath !== 'apps') { $expected[] = $relativePath; } @@ -420,13 +436,13 @@ public function checkWritePermissions(): void { } // Special handling for included default theme - foreach ($this->getRecursiveDirectoryIterator($this->nextcloudDir . '/themes/example', $excludedElements) as $fileInfo) { + foreach ($this->getRecursiveDirectoryIterator($this->buildPath('themes/example'), $excludedElements) as $fileInfo) { if (!$fileInfo->isWritable()) { $notWritablePaths[] = $fileInfo->getFilename(); } } - $themesReadmeFileInfo = new \SplFileInfo($this->nextcloudDir . '/themes/README'); + $themesReadmeFileInfo = new \SplFileInfo($this->buildPath('themes/README')); if (!$themesReadmeFileInfo->isWritable()) { $notWritablePaths[] = $themesReadmeFileInfo->getFilename(); } @@ -486,14 +502,14 @@ public function createBackup(): void { } foreach ($this->getRecursiveDirectoryIterator($this->nextcloudDir, $excludedElements) as $absolutePath => $fileInfo) { - $relativePath = explode($this->nextcloudDir, $absolutePath)[1]; + $relativePath = ltrim(substr($absolutePath, strlen($this->nextcloudDir)), '/'); $relativeDirectory = dirname($relativePath); // Create folder if it doesn't exist - if (!file_exists($backupFolderLocation . '/' . $relativeDirectory)) { - $state = mkdir($backupFolderLocation . '/' . $relativeDirectory, 0750, true); + if (!file_exists($backupFolderLocation . $relativeDirectory)) { + $state = mkdir($backupFolderLocation . $relativeDirectory, 0750, true); if ($state === false) { - throw new \Exception('Could not create folder: ' . $backupFolderLocation . '/' . $relativeDirectory); + throw new \Exception('Could not create folder: ' . $backupFolderLocation . $relativeDirectory); } } @@ -558,28 +574,64 @@ private function getUpdateServerResponse(): array { $updateURL = $updaterServer . '?version=' . str_replace('.', 'x', $this->getConfigOptionMandatoryString('version')) . 'xxx' . $releaseChannel . 'xx' . urlencode($this->buildTime) . 'x' . PHP_MAJOR_VERSION . 'x' . PHP_MINOR_VERSION . 'x' . PHP_RELEASE_VERSION; $this->silentLog('[info] updateURL: ' . $updateURL); + $maxRetries = 2; + + for ($attempt = 1; $attempt <= $maxRetries; $attempt++) { + try { + return $this->fetchUpdateServerResponse($updateURL); + } catch (\Exception $e) { + $lastException = $e; + $this->silentLog('[warn] attempt ' . $attempt . '/' . $maxRetries . ' failed: ' . $e->getMessage()); + if ($attempt < $maxRetries) { + sleep(1); + } + } + } + + throw $lastException; + } + + /** + * @throws \Exception + */ + private function fetchUpdateServerResponse(string $updateURL): array { // Download update response $curl = $this->getCurl($updateURL); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10); + curl_setopt($curl, CURLOPT_TIMEOUT, 30); /** @var false|string $response */ $response = curl_exec($curl); + if ($response === false) { - throw new \Exception('Could not do request to updater server: ' . curl_error($curl)); + $curlError = curl_error($curl); + curl_close($curl); + throw new \Exception('Could not do request to updater server: ' . $curlError); } + $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl); + if ($httpCode !== 200 && $httpCode !== 204) { + $this->silentLog('[warn] update server returned HTTP ' . $httpCode); + throw new \Exception('Update server returned unexpected HTTP status ' . $httpCode); + } + // Response can be empty when no update is available if ($response === '') { return []; } libxml_use_internal_errors(true); - $xml = simplexml_load_string($response); - if ($xml === false) { - $content = strlen($response) > 200 ? substr($response, 0, 200) . '…' : $response; - $errors = implode("\n", array_map(fn ($error) => $error->message, libxml_get_errors())); - throw new \Exception('Could not parse updater server XML response: ' . $content . "\nErrors:\n" . $errors); + try { + $xml = simplexml_load_string($response); + if ($xml === false) { + $content = strlen($response) > 200 ? substr($response, 0, 200) . '…' : $response; + $errors = implode("\n", array_map(fn ($error) => $error->message, libxml_get_errors())); + throw new \Exception('Could not parse updater server XML response: ' . $content . "\nErrors:\n" . $errors); + } + } finally { + libxml_clear_errors(); } $response = get_object_vars($xml); @@ -599,11 +651,11 @@ private function getUpdateServerResponse(): array { * * @throws \Exception */ - public function downloadUpdate(string $url = '', ?Closure $downloadProgress = null): void { + public function downloadUpdate(string $urlOverride = '', ?Closure $downloadProgress = null): void { $this->silentLog('[info] downloadUpdate()'); $this->downloadProgress = $downloadProgress; - $downloadURLs = $url !== '' ? [$url] : $this->getDownloadURLs(); + $downloadURLs = $urlOverride !== '' ? [$urlOverride] : $this->getDownloadURLs(); $this->silentLog('[info] will try to download archive from: ' . implode(', ', $downloadURLs)); @@ -625,10 +677,10 @@ public function downloadUpdate(string $url = '', ?Closure $downloadProgress = nu } } - foreach ($downloadURLs as $url) { + foreach ($downloadURLs as $urlOverride) { $this->previousProgress = 0; - $saveLocation = $storageLocation . basename((string)$url); - if ($this->downloadArchive($url, $saveLocation)) { + $saveLocation = $storageLocation . basename((string)$urlOverride); + if ($this->downloadArchive($urlOverride, $saveLocation)) { return; } } @@ -814,7 +866,7 @@ private function getDownloadedFilePath(): string { * * @throws \Exception */ - public function verifyIntegrity(string $urlOverride = ''): void { + public function verifyIntegrity(string $urlOverride = '', string $signature = ''): void { $this->silentLog('[info] verifyIntegrity()'); if ($this->getCurrentReleaseChannel() === 'daily') { @@ -822,18 +874,13 @@ public function verifyIntegrity(string $urlOverride = ''): void { return; } - if ($urlOverride !== '') { - $this->silentLog('[info] custom download url provided, cannot verify signature'); - return; - } - - $response = $this->getUpdateServerResponse(); - if (empty($response['signature'])) { - throw new \Exception('No signature specified for defined update'); - } - - if (!is_string($response['signature'])) { - throw new \Exception('Signature specified for defined update should be a string'); + if ($signature === '') { + if ($urlOverride !== '') { + throw new \Exception( + 'Custom download url provided. You need to provide a signature with --signature or skip integrity check with --no-verify.' + ); + } + $signature = $this->getSignatureFromUpdater(); } $certificate = <<getDownloadedFilePath()), - base64_decode($response['signature']), + base64_decode($signature), $certificate, OPENSSL_ALGO_SHA512 ) === 1; @@ -880,6 +927,19 @@ public function verifyIntegrity(string $urlOverride = ''): void { $this->silentLog('[info] end of verifyIntegrity()'); } + private function getSignatureFromUpdater(): string { + $response = $this->getUpdateServerResponse(); + if (empty($response['signature'])) { + throw new \Exception('No signature specified for defined update'); + } + + if (!is_string($response['signature'])) { + throw new \Exception('Signature specified for defined update should be a string'); + } + + return $response['signature']; + } + /** * Gets the version as declared in $versionFile * @@ -933,7 +993,7 @@ public function extractDownload(): void { // Ensure that the downloaded version is not lower $downloadedVersion = $this->getVersionByVersionFile(dirname($downloadedFilePath) . '/nextcloud/version.php'); - $currentVersion = $this->getVersionByVersionFile($this->nextcloudDir . '/version.php'); + $currentVersion = $this->getVersionByVersionFile($this->buildPath('version.php')); if (version_compare($downloadedVersion, $currentVersion, '<')) { throw new \Exception('Downloaded version is lower than installed version'); } @@ -961,7 +1021,7 @@ public function replaceEntryPoints(): void { $content = "silentLog('[info] replace ' . $file); - $parentDir = dirname($this->nextcloudDir . '/' . $file); + $parentDir = dirname($this->buildPath($file)); if (!file_exists($parentDir)) { $r = mkdir($parentDir); if (!$r) { @@ -969,7 +1029,7 @@ public function replaceEntryPoints(): void { } } - $state = file_put_contents($this->nextcloudDir . '/' . $file, $content); + $state = file_put_contents($this->buildPath($file), $content); if ($state === false) { throw new \Exception("Can't replace entry point: " . $file); } @@ -1012,7 +1072,7 @@ private function recursiveDelete(string $folder): void { public function deleteOldFiles(): void { $this->silentLog('[info] deleteOldFiles()'); - $shippedAppsFile = $this->nextcloudDir . '/core/shipped.json'; + $shippedAppsFile = $this->buildPath('core/shipped.json'); $shippedAppsFileContent = file_get_contents($shippedAppsFile); if ($shippedAppsFileContent === false) { throw new \Exception('core/shipped.json is not available'); @@ -1040,10 +1100,10 @@ public function deleteOldFiles(): void { $shippedApps = array_merge($shippedApps, $newShippedApps); /** @var string $app */ foreach ($shippedApps as $app) { - $this->recursiveDelete($this->nextcloudDir . '/apps/' . $app); + $this->recursiveDelete($this->buildPath('apps/' . $app)); } - $configSampleFile = $this->nextcloudDir . '/config/config.sample.php'; + $configSampleFile = $this->buildPath('config/config.sample.php'); if (file_exists($configSampleFile)) { $this->silentLog('[info] config sample exists'); @@ -1054,7 +1114,7 @@ public function deleteOldFiles(): void { } } - $themesReadme = $this->nextcloudDir . '/themes/README'; + $themesReadme = $this->buildPath('themes/README'); if (file_exists($themesReadme)) { $this->silentLog('[info] themes README exists'); @@ -1065,7 +1125,7 @@ public function deleteOldFiles(): void { } } - $this->recursiveDelete($this->nextcloudDir . '/themes/example/'); + $this->recursiveDelete($this->buildPath('themes/example/')); // Delete the rest $excludedElements = [ @@ -1112,23 +1172,23 @@ private function moveWithExclusions(string $dataLocation, array $excludedElement } - $fileName = explode($dataLocation, $path)[1]; + $fileName = substr($path, strlen($dataLocation)); if ($fileInfo->isFile()) { - if (!file_exists($this->nextcloudDir . '/' . dirname($fileName))) { - $state = mkdir($this->nextcloudDir . '/' . dirname($fileName), 0755, true); + if (!file_exists($this->buildPath(dirname($fileName)))) { + $state = mkdir($this->buildPath(dirname($fileName)), 0755, true); if ($state === false) { - throw new \Exception('Could not mkdir ' . $this->nextcloudDir . '/' . dirname($fileName)); + throw new \Exception('Could not mkdir ' . $this->buildPath(dirname($fileName))); } } - $state = @rename($path, $this->nextcloudDir . '/' . $fileName); + $state = @rename($path, $this->buildPath($fileName)); if ($state === false) { throw new \Exception( sprintf( 'Could not rename %s to %s', $path, - $this->nextcloudDir . '/' . $fileName + $this->buildPath($fileName) ) ); } @@ -1201,7 +1261,7 @@ public function finalize(): void { $user_ini_additional_lines = implode(PHP_EOL, $user_ini_additional_lines); } - $result = file_put_contents($this->nextcloudDir . '/.user.ini', PHP_EOL . '; Additional settings from config.php:' . PHP_EOL . $user_ini_additional_lines . PHP_EOL, FILE_APPEND); + $result = file_put_contents($this->buildPath('.user.ini'), PHP_EOL . '; Additional settings from config.php:' . PHP_EOL . $user_ini_additional_lines . PHP_EOL, FILE_APPEND); if ($result === false) { throw new \Exception('Could not append to .user.ini'); } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..8e2a09a2 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,15 @@ + + + + + + tests/unit + + + diff --git a/tests/features/bootstrap/FeatureContext.php b/tests/features/bootstrap/FeatureContext.php index 34d036de..f8560815 100644 --- a/tests/features/bootstrap/FeatureContext.php +++ b/tests/features/bootstrap/FeatureContext.php @@ -204,7 +204,7 @@ public function theCliUpdaterIsRun() { copy($this->buildDir . 'updater.phar', $this->serverDir . 'nextcloud/updater/updater'); chdir($this->serverDir . 'nextcloud/updater'); chmod($this->serverDir . 'nextcloud/updater/updater', 0755); - exec('./updater -n', $output, $returnCode); + exec('./updater -n 2>&1', $output, $returnCode); // sleep to let the opcache do it's work and invalidate the status.php sleep(5); @@ -564,4 +564,53 @@ public function thereIsAConfigForASecondaryAppsDirectoryCalled($name) { public function phpIsAtLeastInVersion($version) { $this->skipIt = in_array(version_compare($version, PHP_VERSION, '<'), [0, false], true); } + + /** + * @Given the update server returns HTTP status :statusCode + */ + public function theUpdateServerReturnsHttpStatus(int $statusCode) { + if ($this->skipIt) { + return; + } + + $this->runUpdateServer(); + + $content = 'updateServerDir . 'index.php', $content); + } + + /** + * @Given the update server returns invalid XML + */ + public function theUpdateServerReturnsInvalidXml() { + if ($this->skipIt) { + return; + } + + $this->runUpdateServer(); + + $content = '<><";'; + file_put_contents($this->updateServerDir . 'index.php', $content); + } + + /** + * @Given the update server is unreachable + */ + public function theUpdateServerIsUnreachable() { + if ($this->skipIt) { + return; + } + + // Point updater.server.url at a port with nothing listening + $configFile = $this->serverDir . 'nextcloud/config/config.php'; + $content = file_get_contents($configFile); + $content = preg_replace( + '!\$CONFIG\s*=\s*array\s*\(!', + "\$CONFIG = array(\n 'updater.server.url' => 'http://localhost:8871/',", + $content + ); + file_put_contents($configFile, $content); + + // Intentionally do NOT start any server on port 8871 + } } diff --git a/tests/features/cli.feature b/tests/features/cli.feature index 9555710c..9cd1db17 100644 --- a/tests/features/cli.feature +++ b/tests/features/cli.feature @@ -84,3 +84,33 @@ Feature: CLI updater And the installed version should be 26.0.0 And maintenance mode should be off And upgrade is not required + + Scenario: Update server returns HTTP 500 - 26.0.0 + Given the current installed version is 26.0.0 + And the update server returns HTTP status 500 + When the CLI updater is run + Then the return code should not be 0 + And the output should contain "Update server returned unexpected HTTP status 500" + And the installed version should be 26.0.0 + And maintenance mode should be off + And upgrade is not required + + Scenario: Update server returns invalid XML - 26.0.0 + Given the current installed version is 26.0.0 + And the update server returns invalid XML + When the CLI updater is run + Then the return code should not be 0 + And the output should contain "Could not parse updater server XML response" + And the installed version should be 26.0.0 + And maintenance mode should be off + And upgrade is not required + + Scenario: Update server is unreachable - 26.0.0 + Given the current installed version is 26.0.0 + And the update server is unreachable + When the CLI updater is run + Then the return code should not be 0 + And the output should contain "Could not do request to updater server" + And the installed version should be 26.0.0 + And maintenance mode should be off + And upgrade is not required diff --git a/tests/unit/UpdaterTest.php b/tests/unit/UpdaterTest.php new file mode 100644 index 00000000..a89a5427 --- /dev/null +++ b/tests/unit/UpdaterTest.php @@ -0,0 +1,166 @@ +tempDir = sys_get_temp_dir() . '/nextcloud-updater-test-' . uniqid(); + $this->dataDir = sys_get_temp_dir() . '/nextcloud-updater-data-' . uniqid(); + $this->configDir = sys_get_temp_dir() . '/nextcloud-config-test-' . uniqid(); + + mkdir($this->tempDir . '/updater', 0755, true); + mkdir($this->dataDir, 0755, true); + mkdir($this->configDir, 0755, true); + + // Write a minimal config.php with required keys + file_put_contents($this->configDir . '/config.php', ' "' . $this->dataDir . '",' + . '"version" => "30.0.0.1",' + . '"instanceid" => "testid",' + . '];'); + + putenv('NEXTCLOUD_CONFIG_DIR=' . $this->configDir); + } + + protected function tearDown(): void { + putenv('NEXTCLOUD_CONFIG_DIR='); + + $this->removeDirectory($this->configDir); + $this->removeDirectory($this->dataDir); + $this->removeDirectory($this->tempDir); + } + + private function removeDirectory(string $dir): void { + if ($dir === '' || !is_dir($dir)) { + return; + } + $items = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + foreach ($items as $item) { + if ($item->isDir()) { + rmdir($item->getRealPath()); + } else { + unlink($item->getRealPath()); + } + } + rmdir($dir); + } + + private function makeUpdater(): Updater { + return new Updater($this->tempDir . '/updater'); + } + + // ------------------------------------------------------------------------- + // buildPath() unit tests + // ------------------------------------------------------------------------- + + public function testBuildPathWithSimpleSuffix(): void { + $updater = $this->makeUpdater(); + $this->assertSame($this->tempDir . '/config/config.php', $updater->buildPath('config/config.php')); + } + + public function testBuildPathWithLeadingSlash(): void { + $updater = $this->makeUpdater(); + // A suffix with a leading slash must produce the same result as without + $this->assertSame( + $updater->buildPath('config/config.php'), + $updater->buildPath('/config/config.php') + ); + } + + public function testBuildPathWithMultipleLeadingSlashes(): void { + $updater = $this->makeUpdater(); + // Multiple leading slashes must all be stripped + $this->assertSame( + $updater->buildPath('config/config.php'), + $updater->buildPath('///config/config.php') + ); + } + + public function testBuildPathResultHasNoDoubleSlash(): void { + $updater = $this->makeUpdater(); + $this->assertStringNotContainsString('//', $updater->buildPath('/some/path')); + } + + public function testBuildPathPreservesRelativePathStructure(): void { + $updater = $this->makeUpdater(); + $this->assertSame($this->tempDir . '/apps/myapp', $updater->buildPath('apps/myapp')); + } + + // ------------------------------------------------------------------------- + // Constructor validation tests + // ------------------------------------------------------------------------- + + public function testConstructorThrowsOnEmptyBaseDir(): void { + putenv('NEXTCLOUD_CONFIG_DIR='); + $this->expectException(\Exception::class); + new Updater(''); + } + + public function testConstructorThrowsOnInvalidBaseDir(): void { + $this->expectException(\Exception::class); + // Use a path whose parent doesn't exist + new Updater('/nonexistent/path/that/does/not/exist/updater'); + } + + // ------------------------------------------------------------------------- + // Regression tests for issue #711 + // https://github.com/nextcloud/updater/issues/711 + // + // The bug: createBackup() used explode($nextcloudDir, $absolutePath)[1] + // to extract the relative path. When the install-directory name appears + // anywhere inside the file path (e.g. Nextcloud installed at /nextcloud + // with a file called nextcloud.html), explode() splits on EVERY occurrence + // of the delimiter, returning a directory instead of the full file path. + // PHP's copy() then fails with "cannot be a directory". + // + // The fix: ltrim(substr($absolutePath, strlen($nextcloudDir)), '/') + // ------------------------------------------------------------------------- + + /** + * Integration regression: createBackup() must copy a deeply nested file + * that has the install-directory name as part of its own name, without + * throwing and without producing a double-slash in the destination path. + * + * @see https://github.com/nextcloud/updater/issues/711 + */ + public function testCreateBackupCopiesNestedFilesCorrectly(): void { + // Reproduce the directory structure from the bug report: + // core/doc/admin/configuration_files/external_storage/nextcloud.html + $nestedDir = $this->tempDir . '/core/doc/admin/configuration_files/external_storage'; + mkdir($nestedDir, 0755, true); + file_put_contents($nestedDir . '/nextcloud.html', 'nextcloud docs'); + + $updater = $this->makeUpdater(); + // Must not throw + $updater->createBackup(); + + // Find the backup dir (contains a timestamp so we use glob) + $backupPattern = $this->dataDir . '/updater-testid/backups/nextcloud-30.0.0.1-*'; + $backupDirs = glob($backupPattern, GLOB_ONLYDIR); + $this->assertCount(1, $backupDirs, 'Expected exactly one backup directory'); + + // Backup folder ends with '/' by design; build the expected file path + $backedUpFile = rtrim($backupDirs[0], '/') . '/core/doc/admin/configuration_files/external_storage/nextcloud.html'; + + $this->assertFileExists($backedUpFile, 'Backed-up file must exist at the correct nested path'); + $this->assertStringNotContainsString('//', $backedUpFile, 'Backup path must not contain double slashes'); + $this->assertSame('nextcloud docs', file_get_contents($backedUpFile)); + } +} diff --git a/updater.phar b/updater.phar index 52060e44..3d00cc69 100755 Binary files a/updater.phar and b/updater.phar differ diff --git a/vendor-bin/phpunit/composer.json b/vendor-bin/phpunit/composer.json new file mode 100644 index 00000000..d0c6fafa --- /dev/null +++ b/vendor-bin/phpunit/composer.json @@ -0,0 +1,10 @@ +{ + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "config": { + "platform": { + "php": "8.2" + } + } +} diff --git a/vendor-bin/phpunit/composer.lock b/vendor-bin/phpunit/composer.lock new file mode 100644 index 00000000..d63f5698 --- /dev/null +++ b/vendor-bin/phpunit/composer.lock @@ -0,0 +1,1803 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "2d32e5d858a15b0462bdf724a01645a5", + "packages": [], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.1", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.3.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.46" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:01:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T13:52:54+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.55", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-02-18T12:37:06+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:26:40+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:12:51+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:42:22+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "platform-overrides": { + "php": "8.2" + }, + "plugin-api-version": "2.9.0" +} diff --git a/vendor-bin/tests/composer.lock b/vendor-bin/tests/composer.lock index 81b94f54..61c64899 100644 --- a/vendor-bin/tests/composer.lock +++ b/vendor-bin/tests/composer.lock @@ -9,37 +9,37 @@ "packages-dev": [ { "name": "behat/behat", - "version": "v3.27.0", + "version": "v3.30.0", "source": { "type": "git", "url": "https://github.com/Behat/Behat.git", - "reference": "3282ad774358e4eaf533855e9a1f48559894d1b5" + "reference": "be4af8c803a1ed589409a8a2eed01f9fb858e11d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Behat/zipball/3282ad774358e4eaf533855e9a1f48559894d1b5", - "reference": "3282ad774358e4eaf533855e9a1f48559894d1b5", + "url": "https://api.github.com/repos/Behat/Behat/zipball/be4af8c803a1ed589409a8a2eed01f9fb858e11d", + "reference": "be4af8c803a1ed589409a8a2eed01f9fb858e11d", "shasum": "" }, "require": { - "behat/gherkin": "^4.12.0", + "behat/gherkin": "^4.15.0", "composer-runtime-api": "^2.2", "composer/xdebug-handler": "^1.4 || ^2.0 || ^3.0", "ext-mbstring": "*", "nikic/php-parser": "^4.19.2 || ^5.2", - "php": ">=8.1 <8.6", + "php": ">=8.2 <8.6", "psr/container": "^1.0 || ^2.0", "symfony/config": "^5.4 || ^6.4 || ^7.0", - "symfony/console": "^5.4 || ^6.4 || ^7.0", + "symfony/console": "^5.4.9 || ^6.4 || ^7.0", "symfony/dependency-injection": "^5.4 || ^6.4 || ^7.0", "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0", "symfony/translation": "^5.4 || ^6.4 || ^7.0", "symfony/yaml": "^5.4 || ^6.4 || ^7.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.68", "opis/json-schema": "^2.5", - "phpstan/phpstan": "^2.0", + "php-cs-fixer/shim": "^3.89", + "phpstan/phpstan": "2.1.18", "phpunit/phpunit": "^9.6", "rector/rector": "2.1.7", "sebastian/diff": "^4.0", @@ -54,11 +54,6 @@ "bin/behat" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "autoload": { "psr-4": { "Behat\\Hook\\": "src/Behat/Hook/", @@ -98,22 +93,36 @@ ], "support": { "issues": "https://github.com/Behat/Behat/issues", - "source": "https://github.com/Behat/Behat/tree/v3.27.0" + "source": "https://github.com/Behat/Behat/tree/v3.30.0" }, - "time": "2025-11-23T12:12:41+00:00" + "funding": [ + { + "url": "https://github.com/acoulton", + "type": "github" + }, + { + "url": "https://github.com/carlos-granados", + "type": "github" + }, + { + "url": "https://github.com/stof", + "type": "github" + } + ], + "time": "2026-03-26T17:26:12+00:00" }, { "name": "behat/gherkin", - "version": "v4.15.0", + "version": "v4.16.1", "source": { "type": "git", "url": "https://github.com/Behat/Gherkin.git", - "reference": "05a7459283e8e6af0d46ec25b8bb5960ca3cfa7b" + "reference": "e26037937dfd48528746764dd870bc5d0836665f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/05a7459283e8e6af0d46ec25b8bb5960ca3cfa7b", - "reference": "05a7459283e8e6af0d46ec25b8bb5960ca3cfa7b", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/e26037937dfd48528746764dd870bc5d0836665f", + "reference": "e26037937dfd48528746764dd870bc5d0836665f", "shasum": "" }, "require": { @@ -121,7 +130,7 @@ "php": ">=8.1 <8.6" }, "require-dev": { - "cucumber/gherkin-monorepo": "dev-gherkin-v36.0.0", + "cucumber/gherkin-monorepo": "dev-gherkin-v37.0.0", "friendsofphp/php-cs-fixer": "^3.77", "mikey179/vfsstream": "^1.6", "phpstan/extension-installer": "^1", @@ -167,9 +176,23 @@ ], "support": { "issues": "https://github.com/Behat/Gherkin/issues", - "source": "https://github.com/Behat/Gherkin/tree/v4.15.0" + "source": "https://github.com/Behat/Gherkin/tree/v4.16.1" }, - "time": "2025-11-05T15:34:04+00:00" + "funding": [ + { + "url": "https://github.com/acoulton", + "type": "github" + }, + { + "url": "https://github.com/carlos-granados", + "type": "github" + }, + { + "url": "https://github.com/stof", + "type": "github" + } + ], + "time": "2025-12-08T16:12:58+00:00" }, { "name": "composer/pcre", @@ -318,16 +341,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -370,9 +393,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "psr/container", @@ -529,16 +552,16 @@ }, { "name": "symfony/config", - "version": "v7.4.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "f76c74e93bce2b9285f2dad7fbd06fa8182a7a41" + "reference": "2d19dde43fa2ff720b9a40763ace7226594f503b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/f76c74e93bce2b9285f2dad7fbd06fa8182a7a41", - "reference": "f76c74e93bce2b9285f2dad7fbd06fa8182a7a41", + "url": "https://api.github.com/repos/symfony/config/zipball/2d19dde43fa2ff720b9a40763ace7226594f503b", + "reference": "2d19dde43fa2ff720b9a40763ace7226594f503b", "shasum": "" }, "require": { @@ -584,7 +607,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v7.4.0" + "source": "https://github.com/symfony/config/tree/v7.4.8" }, "funding": [ { @@ -604,20 +627,20 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/console", - "version": "v7.4.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8" + "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", - "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", + "url": "https://api.github.com/repos/symfony/console/zipball/1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", + "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", "shasum": "" }, "require": { @@ -682,7 +705,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.0" + "source": "https://github.com/symfony/console/tree/v7.4.8" }, "funding": [ { @@ -702,20 +725,20 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2026-03-30T13:54:39+00:00" }, { "name": "symfony/dependency-injection", - "version": "v7.4.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "3972ca7bbd649467b21a54870721b9e9f3652f9b" + "reference": "f7025fd7b687c240426562f86ada06a93b1e771d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/3972ca7bbd649467b21a54870721b9e9f3652f9b", - "reference": "3972ca7bbd649467b21a54870721b9e9f3652f9b", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f7025fd7b687c240426562f86ada06a93b1e771d", + "reference": "f7025fd7b687c240426562f86ada06a93b1e771d", "shasum": "" }, "require": { @@ -766,7 +789,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.4.0" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.8" }, "funding": [ { @@ -786,7 +809,7 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2026-03-31T06:50:29+00:00" }, { "name": "symfony/deprecation-contracts", @@ -857,16 +880,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v7.4.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d" + "reference": "f57b899fa736fd71121168ef268f23c206083f0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9dddcddff1ef974ad87b3708e4b442dc38b2261d", - "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f57b899fa736fd71121168ef268f23c206083f0a", + "reference": "f57b899fa736fd71121168ef268f23c206083f0a", "shasum": "" }, "require": { @@ -918,7 +941,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.8" }, "funding": [ { @@ -938,7 +961,7 @@ "type": "tidelift" } ], - "time": "2025-10-28T09:38:46+00:00" + "time": "2026-03-30T13:54:39+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -1018,16 +1041,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.4.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "d551b38811096d0be9c4691d406991b47c0c630a" + "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", - "reference": "d551b38811096d0be9c4691d406991b47c0c630a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/58b9790d12f9670b7f53a1c1738febd3108970a5", + "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5", "shasum": "" }, "require": { @@ -1064,7 +1087,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.0" + "source": "https://github.com/symfony/filesystem/tree/v7.4.8" }, "funding": [ { @@ -1084,7 +1107,7 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/polyfill-ctype", @@ -1510,16 +1533,16 @@ }, { "name": "symfony/string", - "version": "v7.4.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003" + "reference": "114ac57257d75df748eda23dd003878080b8e688" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/d50e862cb0a0e0886f73ca1f31b865efbb795003", - "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003", + "url": "https://api.github.com/repos/symfony/string/zipball/114ac57257d75df748eda23dd003878080b8e688", + "reference": "114ac57257d75df748eda23dd003878080b8e688", "shasum": "" }, "require": { @@ -1577,7 +1600,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.4.0" + "source": "https://github.com/symfony/string/tree/v7.4.8" }, "funding": [ { @@ -1597,20 +1620,20 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/translation", - "version": "v7.4.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68" + "reference": "33600f8489485425bfcddd0d983391038d3422e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/2d01ca0da3f092f91eeedb46f24aa30d2fca8f68", - "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68", + "url": "https://api.github.com/repos/symfony/translation/zipball/33600f8489485425bfcddd0d983391038d3422e7", + "reference": "33600f8489485425bfcddd0d983391038d3422e7", "shasum": "" }, "require": { @@ -1677,7 +1700,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.4.0" + "source": "https://github.com/symfony/translation/tree/v7.4.8" }, "funding": [ { @@ -1697,7 +1720,7 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/translation-contracts", @@ -1783,16 +1806,16 @@ }, { "name": "symfony/var-exporter", - "version": "v7.4.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" + "reference": "398907e89a2a56fe426f7955c6fa943ec0c77225" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/398907e89a2a56fe426f7955c6fa943ec0c77225", + "reference": "398907e89a2a56fe426f7955c6fa943ec0c77225", "shasum": "" }, "require": { @@ -1840,7 +1863,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" + "source": "https://github.com/symfony/var-exporter/tree/v7.4.8" }, "funding": [ { @@ -1860,20 +1883,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:15:23+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/yaml", - "version": "v7.4.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810" + "reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/6c84a4b55aee4cd02034d1c528e83f69ddf63810", - "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810", + "url": "https://api.github.com/repos/symfony/yaml/zipball/c58fdf7b3d6c2995368264c49e4e8b05bcff2883", + "reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883", "shasum": "" }, "require": { @@ -1916,7 +1939,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.0" + "source": "https://github.com/symfony/yaml/tree/v7.4.8" }, "funding": [ { @@ -1936,7 +1959,7 @@ "type": "tidelift" } ], - "time": "2025-11-16T10:14:42+00:00" + "time": "2026-03-24T13:12:05+00:00" } ], "aliases": [], @@ -1949,5 +1972,5 @@ "platform-overrides": { "php": "8.2" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/vendor/bamarni/composer-bin-plugin/.github/workflows/phpstan.yml b/vendor/bamarni/composer-bin-plugin/.github/workflows/phpstan.yml deleted file mode 100644 index ebb57672..00000000 --- a/vendor/bamarni/composer-bin-plugin/.github/workflows/phpstan.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: "Static analysis" - -on: - push: - branches: - - "main" - - "master" - pull_request: null - -jobs: - static-analysis: - runs-on: "ubuntu-latest" - name: "PHPStan on PHP ${{ matrix.php }}" - strategy: - fail-fast: false - matrix: - php: - - "8.1" - steps: - - name: "Check out repository code" - uses: "actions/checkout@v2" - - - name: "Setup PHP" - uses: "shivammathur/setup-php@v2" - with: - php-version: "${{ matrix.php }}" - tools: "composer" - - - name: "Install Composer dependencies" - uses: "ramsey/composer-install@v2" - with: - dependency-versions: "highest" - - - name: "Perform static analysis" - run: "make phpstan" diff --git a/vendor/bamarni/composer-bin-plugin/.github/workflows/tests.yaml b/vendor/bamarni/composer-bin-plugin/.github/workflows/tests.yaml deleted file mode 100644 index 5bec38a4..00000000 --- a/vendor/bamarni/composer-bin-plugin/.github/workflows/tests.yaml +++ /dev/null @@ -1,80 +0,0 @@ -name: "Tests" - -on: - push: - branches: - - "main" - - "master" - pull_request: null - -jobs: - unit-tests: - runs-on: "ubuntu-latest" - name: "Unit Tests on PHP ${{ matrix.php }} and ${{ matrix.tools }}" - strategy: - fail-fast: false - matrix: - php: - - "7.2" - - "7.3" - - "7.4" - - "8.0" - - "8.1" - tools: [ "composer" ] - dependency-versions: [ "highest" ] - include: - - php: "7.2" - tools: "composer:v2.0" - dependency-versions: "lowest" - - steps: - - name: "Check out repository code" - uses: "actions/checkout@v2" - - - name: "Setup PHP" - uses: "shivammathur/setup-php@v2" - with: - php-version: "${{ matrix.php }}" - tools: "${{ matrix.tools }}" - - - name: "Install Composer dependencies" - uses: "ramsey/composer-install@v2" - with: - dependency-versions: "${{ matrix.dependency-versions }}" - - - name: "Validate composer.json" - run: "composer validate --strict --no-check-lock" - - - name: "Run tests" - run: "vendor/bin/phpunit --group default" - - e2e-tests: - runs-on: "ubuntu-latest" - name: "E2E Tests on PHP ${{ matrix.php }}" - strategy: - fail-fast: false - matrix: - php: - - "8.1" - - steps: - - name: "Check out repository code" - uses: "actions/checkout@v2" - - - name: "Setup PHP" - uses: "shivammathur/setup-php@v2" - with: - php-version: "${{ matrix.php }}" - tools: "composer" - - - name: "Correct bin plugin version for e2e scenarios (PR-only)" - if: github.event_name == 'pull_request' - run: find e2e -maxdepth 1 -mindepth 1 -type d -exec bash -c "cd {} && composer require --dev bamarni/composer-bin-plugin:dev-${GITHUB_SHA} --no-update" \; - - - name: "Install Composer dependencies" - uses: "ramsey/composer-install@v2" - with: - dependency-versions: "highest" - - - name: "Run tests" - run: "vendor/bin/phpunit --group e2e" diff --git a/vendor/bamarni/composer-bin-plugin/.makefile/touch.sh b/vendor/bamarni/composer-bin-plugin/.makefile/touch.sh deleted file mode 100755 index 5c0a7759..00000000 --- a/vendor/bamarni/composer-bin-plugin/.makefile/touch.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash -# -# Takes a given string, e.g. 'bin/console' or 'docker-compose exec php bin/console' -# and split it by words. For each words, if the target is a file, it is touched. -# -# This allows to implement a similar rule to: -# -# ```Makefile -# bin/php-cs-fixer: vendor -# touch $@ -# ``` -# -# Indeed when the rule `bin/php-cs-fixer` is replaced with a docker-compose -# equivalent, it will not play out as nicely. -# -# Arguments: -# $1 - {string} Command potentially containing a file -# - -set -Eeuo pipefail; - - -readonly ERROR_COLOR="\e[41m"; -readonly NO_COLOR="\e[0m"; - - -if [ $# -ne 1 ]; then - printf "${ERROR_COLOR}Illegal number of parameters.${NO_COLOR}\n"; - - exit 1; -fi - - -readonly FILES="$1"; - - -####################################### -# Touch the given file path if the target is a file and do not create the file -# if does not exist. -# -# Globals: -# None -# -# Arguments: -# $1 - {string} File path -# -# Returns: -# None -####################################### -touch_file() { - local file="$1"; - - if [ -e ${file} ]; then - touch -c ${file}; - fi -} - -for file in ${FILES} -do - touch_file ${file}; -done diff --git a/vendor/bamarni/composer-bin-plugin/.phive/phars.xml b/vendor/bamarni/composer-bin-plugin/.phive/phars.xml deleted file mode 100644 index 335086e3..00000000 --- a/vendor/bamarni/composer-bin-plugin/.phive/phars.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/vendor/bamarni/composer-bin-plugin/composer.json b/vendor/bamarni/composer-bin-plugin/composer.json index 5b3809ba..4b76ff1b 100644 --- a/vendor/bamarni/composer-bin-plugin/composer.json +++ b/vendor/bamarni/composer-bin-plugin/composer.json @@ -17,11 +17,11 @@ }, "require-dev": { "ext-json": "*", - "composer/composer": "^2.0", + "composer/composer": "^2.2.26", "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^8.5 || ^9.5", + "phpstan/phpstan": "^1.8 || ^2.0", + "phpstan/phpstan-phpunit": "^1.1 || ^2.0", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.0", "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0" @@ -38,9 +38,9 @@ }, "config": { "allow-plugins": { - "phpstan/extension-installer": true, "ergebnis/composer-normalize": true, - "infection/extension-installer": true + "infection/extension-installer": true, + "phpstan/extension-installer": true }, "sort-packages": true }, diff --git a/vendor/bamarni/composer-bin-plugin/src/BamarniBinPlugin.php b/vendor/bamarni/composer-bin-plugin/src/BamarniBinPlugin.php index 1d21c93b..19e36e0a 100644 --- a/vendor/bamarni/composer-bin-plugin/src/BamarniBinPlugin.php +++ b/vendor/bamarni/composer-bin-plugin/src/BamarniBinPlugin.php @@ -24,6 +24,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Throwable; + use function count; use function in_array; use function sprintf; diff --git a/vendor/bamarni/composer-bin-plugin/src/Command/BinCommand.php b/vendor/bamarni/composer-bin-plugin/src/Command/BinCommand.php index 9edb4c3e..69c4aa99 100644 --- a/vendor/bamarni/composer-bin-plugin/src/Command/BinCommand.php +++ b/vendor/bamarni/composer-bin-plugin/src/Command/BinCommand.php @@ -19,6 +19,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Throwable; + use function chdir; use function count; use function file_exists; @@ -30,6 +31,7 @@ use function mkdir; use function putenv; use function sprintf; + use const GLOB_ONLYDIR; /** @@ -96,6 +98,7 @@ public function isProxyCommand(): bool public function execute(InputInterface $input, OutputInterface $output): int { // Switch to requireComposer() once Composer 2.3 is set as the minimum + // @phpstan-ignore function.alreadyNarrowedType $composer = method_exists($this, 'requireComposer') ? $this->requireComposer() : $this->getComposer(); diff --git a/vendor/bamarni/composer-bin-plugin/src/Command/CouldNotCreateNamespaceDir.php b/vendor/bamarni/composer-bin-plugin/src/Command/CouldNotCreateNamespaceDir.php index 33145078..8985a7cd 100644 --- a/vendor/bamarni/composer-bin-plugin/src/Command/CouldNotCreateNamespaceDir.php +++ b/vendor/bamarni/composer-bin-plugin/src/Command/CouldNotCreateNamespaceDir.php @@ -5,6 +5,7 @@ namespace Bamarni\Composer\Bin\Command; use RuntimeException; + use function sprintf; final class CouldNotCreateNamespaceDir extends RuntimeException diff --git a/vendor/bamarni/composer-bin-plugin/src/Config/Config.php b/vendor/bamarni/composer-bin-plugin/src/Config/Config.php index fa62b3a1..629ab868 100644 --- a/vendor/bamarni/composer-bin-plugin/src/Config/Config.php +++ b/vendor/bamarni/composer-bin-plugin/src/Config/Config.php @@ -5,6 +5,7 @@ namespace Bamarni\Composer\Bin\Config; use Composer\Composer; + use function array_key_exists; use function array_merge; use function function_exists; diff --git a/vendor/bamarni/composer-bin-plugin/src/Input/BinInputFactory.php b/vendor/bamarni/composer-bin-plugin/src/Input/BinInputFactory.php index dab96f68..105afae9 100644 --- a/vendor/bamarni/composer-bin-plugin/src/Input/BinInputFactory.php +++ b/vendor/bamarni/composer-bin-plugin/src/Input/BinInputFactory.php @@ -6,6 +6,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\StringInput; + use function array_filter; use function array_map; use function implode; @@ -61,7 +62,7 @@ public static function createInput( public static function createNamespaceInput(InputInterface $previousInput): InputInterface { $matchResult = preg_match( - '/^(.+?\s?)(--(?: .+)?)?$/', + '/^([^\s]+)(.*?\s?)(--(?: .+)?)?$/', $previousInput->__toString(), $matches ); @@ -74,9 +75,10 @@ public static function createNamespaceInput(InputInterface $previousInput): Inpu array_map( 'trim', [ - $matches[1], '--working-dir=.', - $matches[2] ?? '', + $matches[1], + $matches[2], + $matches[3] ?? '', ] ) ); diff --git a/vendor/bamarni/composer-bin-plugin/src/Input/InvalidBinInput.php b/vendor/bamarni/composer-bin-plugin/src/Input/InvalidBinInput.php index 2418d9c8..7f44ff5e 100644 --- a/vendor/bamarni/composer-bin-plugin/src/Input/InvalidBinInput.php +++ b/vendor/bamarni/composer-bin-plugin/src/Input/InvalidBinInput.php @@ -6,6 +6,7 @@ use RuntimeException; use Symfony\Component\Console\Input\InputInterface; + use function sprintf; final class InvalidBinInput extends RuntimeException diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 00778138..3ae12af1 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -25,7 +25,6 @@ 'NC\\Updater\\UpdateCommand' => $baseDir . '/lib/UpdateCommand.php', 'NC\\Updater\\UpdateException' => $baseDir . '/lib/UpdateException.php', 'NC\\Updater\\Updater' => $baseDir . '/lib/Updater.php', - 'NC\\Updater\\Version' => $baseDir . '/lib/Version.php', 'Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php', 'Psr\\Container\\ContainerExceptionInterface' => $vendorDir . '/psr/container/src/ContainerExceptionInterface.php', 'Psr\\Container\\ContainerInterface' => $vendorDir . '/psr/container/src/ContainerInterface.php', diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 173bf8d5..fe175374 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -103,7 +103,6 @@ class ComposerStaticInitcbbead1010db4afef500f7adc2b6cac3 'NC\\Updater\\UpdateCommand' => __DIR__ . '/../..' . '/lib/UpdateCommand.php', 'NC\\Updater\\UpdateException' => __DIR__ . '/../..' . '/lib/UpdateException.php', 'NC\\Updater\\Updater' => __DIR__ . '/../..' . '/lib/Updater.php', - 'NC\\Updater\\Version' => __DIR__ . '/../..' . '/lib/Version.php', 'Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php', 'Psr\\Container\\ContainerExceptionInterface' => __DIR__ . '/..' . '/psr/container/src/ContainerExceptionInterface.php', 'Psr\\Container\\ContainerInterface' => __DIR__ . '/..' . '/psr/container/src/ContainerInterface.php', diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 55853f22..d44e57cc 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -2,17 +2,17 @@ "packages": [ { "name": "bamarni/composer-bin-plugin", - "version": "1.8.2", - "version_normalized": "1.8.2.0", + "version": "1.9.1", + "version_normalized": "1.9.1.0", "source": { "type": "git", "url": "https://github.com/bamarni/composer-bin-plugin.git", - "reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880" + "reference": "641d0663f5ac270b1aeec4337b7856f76204df47" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bamarni/composer-bin-plugin/zipball/92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880", - "reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880", + "url": "https://api.github.com/repos/bamarni/composer-bin-plugin/zipball/641d0663f5ac270b1aeec4337b7856f76204df47", + "reference": "641d0663f5ac270b1aeec4337b7856f76204df47", "shasum": "" }, "require": { @@ -20,17 +20,17 @@ "php": "^7.2.5 || ^8.0" }, "require-dev": { - "composer/composer": "^2.0", + "composer/composer": "^2.2.26", "ext-json": "*", "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^8.5 || ^9.5", + "phpstan/phpstan": "^1.8 || ^2.0", + "phpstan/phpstan-phpunit": "^1.1 || ^2.0", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.0", "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0" }, - "time": "2022-10-31T08:38:03+00:00", + "time": "2026-02-04T10:18:12+00:00", "type": "composer-plugin", "extra": { "class": "Bamarni\\Composer\\Bin\\BamarniBinPlugin" @@ -56,7 +56,7 @@ ], "support": { "issues": "https://github.com/bamarni/composer-bin-plugin/issues", - "source": "https://github.com/bamarni/composer-bin-plugin/tree/1.8.2" + "source": "https://github.com/bamarni/composer-bin-plugin/tree/1.9.1" }, "install-path": "../bamarni/composer-bin-plugin" }, diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index a121f657..3217a893 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -1,9 +1,9 @@ array( 'name' => '__root__', - 'pretty_version' => 'dev-master', - 'version' => 'dev-master', - 'reference' => '0deb35c1f4fd23fea55c03db40f07c72603adc43', + 'pretty_version' => 'dev-copilot/add-string-concat-method', + 'version' => 'dev-copilot/add-string-concat-method', + 'reference' => '851f344fad579276efb35eef23b70f76d7ce71e9', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -11,18 +11,18 @@ ), 'versions' => array( '__root__' => array( - 'pretty_version' => 'dev-master', - 'version' => 'dev-master', - 'reference' => '0deb35c1f4fd23fea55c03db40f07c72603adc43', + 'pretty_version' => 'dev-copilot/add-string-concat-method', + 'version' => 'dev-copilot/add-string-concat-method', + 'reference' => '851f344fad579276efb35eef23b70f76d7ce71e9', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => false, ), 'bamarni/composer-bin-plugin' => array( - 'pretty_version' => '1.8.2', - 'version' => '1.8.2.0', - 'reference' => '92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880', + 'pretty_version' => '1.9.1', + 'version' => '1.9.1.0', + 'reference' => '641d0663f5ac270b1aeec4337b7856f76204df47', 'type' => 'composer-plugin', 'install_path' => __DIR__ . '/../bamarni/composer-bin-plugin', 'aliases' => array(),