diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index ff526a2a..5bef7f3b 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -2,6 +2,9 @@ # # https://github.com/nextcloud/.github # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT name: Static analysis @@ -11,6 +14,9 @@ concurrency: group: psalm-${{ github.head_ref || github.run_id }} cancel-in-progress: true +permissions: + contents: read + jobs: static-analysis: runs-on: ubuntu-latest @@ -18,7 +24,9 @@ jobs: name: static-psalm-analysis steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Set up php8.2 uses: shivammathur/setup-php@6d7209f44a25a59e904b1ee9f3b0c33ab2cd888d # v2 @@ -27,6 +35,8 @@ jobs: extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite coverage: none ini-file: development + # Temporary workaround for missing pcntl_* in PHP 8.3 + ini-values: disable_functions= env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -34,4 +44,4 @@ jobs: run: composer i - name: Run coding standards check - run: composer run psalm + run: composer run psalm -- --threads=1 --monochrome --no-progress --output-format=github diff --git a/index.php b/index.php index 62d7606c..14b6c58e 100644 --- a/index.php +++ b/index.php @@ -567,11 +567,9 @@ private function getUpdateServerResponse(): array { * * @throws \Exception */ - public function downloadUpdate(): void { + public function downloadUpdate(?string $url = null): void { $this->silentLog('[info] downloadUpdate()'); - $response = $this->getUpdateServerResponse(); - $storageLocation = $this->getUpdateDirectoryLocation() . '/updater-'.$this->getConfigOptionMandatoryString('instanceid') . '/downloads/'; if (file_exists($storageLocation)) { $this->silentLog('[info] storage location exists'); @@ -582,12 +580,26 @@ public function downloadUpdate(): void { throw new \Exception('Could not mkdir storage location'); } - if (!isset($response['url']) || !is_string($response['url'])) { - throw new \Exception('Response from update server is missing url'); + $downloadURL = ''; + if ($url) { + // If a URL is provided, use it directly + $downloadURL = $url; + } else { + // Otherwise, get the download URLs from the update server + $response = $this->getUpdateServerResponse(); + + if (!isset($response['url']) || !is_string($response['url'])) { + throw new \Exception('Response from update server is missing url'); + } + $downloadURL = $response['url']; + } + + if (!$downloadURL) { + throw new \Exception('No download URL provided or available from update server'); } - $fp = fopen($storageLocation . basename($response['url']), 'w+'); - $ch = curl_init($response['url']); + $fp = fopen($storageLocation . basename($downloadURL), 'w+'); + $ch = curl_init($downloadURL); curl_setopt_array($ch, [ CURLOPT_FILE => $fp, CURLOPT_USERAGENT => 'Nextcloud Updater', @@ -629,7 +641,7 @@ public function downloadUpdate(): void { $message .= ' - curl error message: ' . $curlErrorMessage; } - $message .= ' - URL: ' . htmlentities($response['url']); + $message .= ' - URL: ' . htmlentities($downloadURL); throw new \Exception($message); } @@ -662,7 +674,7 @@ private function getDownloadedFilePath(): string { * * @throws \Exception */ - public function verifyIntegrity(): void { + public function verifyIntegrity(?string $urlOverride = null): void { $this->silentLog('[info] verifyIntegrity()'); if ($this->getCurrentReleaseChannel() === 'daily') { @@ -670,6 +682,11 @@ public function verifyIntegrity(): 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'); diff --git a/lib/UpdateCommand.php b/lib/UpdateCommand.php index 22cc87d0..8db68550 100644 --- a/lib/UpdateCommand.php +++ b/lib/UpdateCommand.php @@ -36,6 +36,7 @@ class UpdateCommand extends Command { protected bool $shouldStop = false; protected bool $skipBackup = false; protected bool $skipUpgrade = false; + protected string $urlOverride = ''; /** @var list strings of text for stages of updater */ protected array $checkTexts = [ @@ -60,7 +61,8 @@ protected function configure(): void { ->setDescription('Updates the code of an Nextcloud instance') ->setHelp("This command fetches the latest code that is announced via the updater server and safely replaces the existing code with the new one.") ->addOption('no-backup', null, InputOption::VALUE_NONE, 'Skip backup of current Nextcloud version') - ->addOption('no-upgrade', null, InputOption::VALUE_NONE, "Don't automatically run occ upgrade"); + ->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'); } public static function getUpdaterVersion(): string { @@ -75,6 +77,7 @@ public static function getUpdaterVersion(): string { protected function execute(InputInterface $input, OutputInterface $output) { $this->skipBackup = (bool)$input->getOption('no-backup'); $this->skipUpgrade = (bool)$input->getOption('no-upgrade'); + $this->urlOverride = (string)$input->getOption('url'); $version = static::getUpdaterVersion(); $output->writeln('Nextcloud Updater - version: ' . $version); @@ -148,7 +151,12 @@ protected function execute(InputInterface $input, OutputInterface $output) { $output->writeln('Current version is ' . $this->updater->getCurrentVersion() . '.'); // needs to be called that early because otherwise updateAvailable() returns false - $updateString = $this->updater->checkForUpdate(); + if ($this->urlOverride) { + $this->updater->log('[info] Using URL override: ' . $this->urlOverride); + $updateString = 'Update check forced with URL override: ' . $this->urlOverride; + } else { + $updateString = $this->updater->checkForUpdate(); + } $output->writeln(''); @@ -161,9 +169,11 @@ protected function execute(InputInterface $input, OutputInterface $output) { $output->writeln(''); - if (!$this->updater->updateAvailable() && $stepNumber === 0) { - $output->writeln('Nothing to do.'); - return 0; + if (!$this->urlOverride) { + if (!$this->updater->updateAvailable() && $stepNumber === 0) { + $output->writeln('Nothing to do.'); + return 0; + } } $questionText = 'Start update'; @@ -375,10 +385,10 @@ protected function executeStep(int $step): array { } break; case 4: - $this->updater->downloadUpdate(); + $this->updater->downloadUpdate($this->urlOverride); break; case 5: - $this->updater->verifyIntegrity(); + $this->updater->verifyIntegrity($this->urlOverride); break; case 6: $this->updater->extractDownload(); diff --git a/lib/Updater.php b/lib/Updater.php index f1725aa1..9dc1cd85 100644 --- a/lib/Updater.php +++ b/lib/Updater.php @@ -1,24 +1,7 @@ - * @copyright Copyright (c) 2016 Morris Jobke - * @copyright Copyright (c) 2018 Jonas Sulzer - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace NC\Updater; @@ -529,11 +512,9 @@ private function getUpdateServerResponse(): array { * * @throws \Exception */ - public function downloadUpdate(): void { + public function downloadUpdate(?string $url = null): void { $this->silentLog('[info] downloadUpdate()'); - $response = $this->getUpdateServerResponse(); - $storageLocation = $this->getUpdateDirectoryLocation() . '/updater-'.$this->getConfigOptionMandatoryString('instanceid') . '/downloads/'; if (file_exists($storageLocation)) { $this->silentLog('[info] storage location exists'); @@ -544,12 +525,26 @@ public function downloadUpdate(): void { throw new \Exception('Could not mkdir storage location'); } - if (!isset($response['url']) || !is_string($response['url'])) { - throw new \Exception('Response from update server is missing url'); + $downloadURL = ''; + if ($url) { + // If a URL is provided, use it directly + $downloadURL = $url; + } else { + // Otherwise, get the download URLs from the update server + $response = $this->getUpdateServerResponse(); + + if (!isset($response['url']) || !is_string($response['url'])) { + throw new \Exception('Response from update server is missing url'); + } + $downloadURL = $response['url']; + } + + if (!$downloadURL) { + throw new \Exception('No download URL provided or available from update server'); } - $fp = fopen($storageLocation . basename($response['url']), 'w+'); - $ch = curl_init($response['url']); + $fp = fopen($storageLocation . basename($downloadURL), 'w+'); + $ch = curl_init($downloadURL); curl_setopt_array($ch, [ CURLOPT_FILE => $fp, CURLOPT_USERAGENT => 'Nextcloud Updater', @@ -591,7 +586,7 @@ public function downloadUpdate(): void { $message .= ' - curl error message: ' . $curlErrorMessage; } - $message .= ' - URL: ' . htmlentities($response['url']); + $message .= ' - URL: ' . htmlentities($downloadURL); throw new \Exception($message); } @@ -624,7 +619,7 @@ private function getDownloadedFilePath(): string { * * @throws \Exception */ - public function verifyIntegrity(): void { + public function verifyIntegrity(?string $urlOverride = null): void { $this->silentLog('[info] verifyIntegrity()'); if ($this->getCurrentReleaseChannel() === 'daily') { @@ -632,6 +627,11 @@ public function verifyIntegrity(): 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'); diff --git a/updater.phar b/updater.phar index bfb802eb..6d197806 100755 Binary files a/updater.phar and b/updater.phar differ diff --git a/vendor/autoload.php b/vendor/autoload.php index e97f2aed..f52071e9 100644 --- a/vendor/autoload.php +++ b/vendor/autoload.php @@ -14,10 +14,7 @@ echo $err; } } - trigger_error( - $err, - E_USER_ERROR - ); + throw new RuntimeException($err); } require_once __DIR__ . '/composer/autoload_real.php'; diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php index 51e734a7..2052022f 100644 --- a/vendor/composer/InstalledVersions.php +++ b/vendor/composer/InstalledVersions.php @@ -26,12 +26,23 @@ */ class InstalledVersions { + /** + * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to + * @internal + */ + private static $selfDir = null; + /** * @var mixed[]|null * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null */ private static $installed; + /** + * @var bool + */ + private static $installedIsLocalDir; + /** * @var bool|null */ @@ -309,6 +320,24 @@ public static function reload($data) { self::$installed = $data; self::$installedByVendor = array(); + + // when using reload, we disable the duplicate protection to ensure that self::$installed data is + // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not, + // so we have to assume it does not, and that may result in duplicate data being returned when listing + // all installed packages for example + self::$installedIsLocalDir = false; + } + + /** + * @return string + */ + private static function getSelfDir() + { + if (self::$selfDir === null) { + self::$selfDir = strtr(__DIR__, '\\', '/'); + } + + return self::$selfDir; } /** @@ -322,19 +351,27 @@ private static function getInstalled() } $installed = array(); + $copiedLocalDir = false; if (self::$canGetVendors) { + $selfDir = self::getSelfDir(); foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + $vendorDir = strtr($vendorDir, '\\', '/'); if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ $required = require $vendorDir.'/composer/installed.php'; - $installed[] = self::$installedByVendor[$vendorDir] = $required; - if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { - self::$installed = $installed[count($installed) - 1]; + self::$installedByVendor[$vendorDir] = $required; + $installed[] = $required; + if (self::$installed === null && $vendorDir.'/composer' === $selfDir) { + self::$installed = $required; + self::$installedIsLocalDir = true; } } + if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) { + $copiedLocalDir = true; + } } } @@ -350,7 +387,7 @@ private static function getInstalled() } } - if (self::$installed !== array()) { + if (self::$installed !== array() && !$copiedLocalDir) { $installed[] = self::$installed; } diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index a1f1e348..aaf3465d 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => '__root__', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => 'aac9e4b1f9edc88a0d28dcbfca50276c9a2c25ba', + 'reference' => '153ed34eddb3ef3ae21991443dc774c8f7cf88ac', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -13,7 +13,7 @@ '__root__' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => 'aac9e4b1f9edc88a0d28dcbfca50276c9a2c25ba', + 'reference' => '153ed34eddb3ef3ae21991443dc774c8f7cf88ac', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php index 580fa960..d2225c7d 100644 --- a/vendor/composer/platform_check.php +++ b/vendor/composer/platform_check.php @@ -19,8 +19,7 @@ echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; } } - trigger_error( - 'Composer detected issues in your platform: ' . implode(' ', $issues), - E_USER_ERROR + throw new \RuntimeException( + 'Composer detected issues in your platform: ' . implode(' ', $issues) ); }