Skip to content

Commit acb55ed

Browse files
committed
feat: allow to provide manual URL
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
1 parent bbb3fee commit acb55ed

File tree

15 files changed

+172
-80
lines changed

15 files changed

+172
-80
lines changed

index.php

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -567,11 +567,9 @@ private function getUpdateServerResponse(): array {
567567
*
568568
* @throws \Exception
569569
*/
570-
public function downloadUpdate(): void {
570+
public function downloadUpdate(?string $url = null): void {
571571
$this->silentLog('[info] downloadUpdate()');
572572

573-
$response = $this->getUpdateServerResponse();
574-
575573
$storageLocation = $this->getUpdateDirectoryLocation() . '/updater-'.$this->getConfigOptionMandatoryString('instanceid') . '/downloads/';
576574
if (file_exists($storageLocation)) {
577575
$this->silentLog('[info] storage location exists');
@@ -582,12 +580,26 @@ public function downloadUpdate(): void {
582580
throw new \Exception('Could not mkdir storage location');
583581
}
584582

585-
if (!isset($response['url']) || !is_string($response['url'])) {
586-
throw new \Exception('Response from update server is missing url');
583+
$downloadURL = '';
584+
if ($url) {
585+
// If a URL is provided, use it directly
586+
$downloadURL = $url;
587+
} else {
588+
// Otherwise, get the download URLs from the update server
589+
$response = $this->getUpdateServerResponse();
590+
591+
if (!isset($response['url']) || !is_string($response['url'])) {
592+
throw new \Exception('Response from update server is missing url');
593+
}
594+
$downloadURL = $response['url'];
595+
}
596+
597+
if (!$downloadURL) {
598+
throw new \Exception('No download URL provided or available from update server');
587599
}
588600

589-
$fp = fopen($storageLocation . basename($response['url']), 'w+');
590-
$ch = curl_init($response['url']);
601+
$fp = fopen($storageLocation . basename($downloadURL), 'w+');
602+
$ch = curl_init($downloadURL);
591603
curl_setopt_array($ch, [
592604
CURLOPT_FILE => $fp,
593605
CURLOPT_USERAGENT => 'Nextcloud Updater',
@@ -629,7 +641,7 @@ public function downloadUpdate(): void {
629641
$message .= ' - curl error message: ' . $curlErrorMessage;
630642
}
631643

632-
$message .= ' - URL: ' . htmlentities($response['url']);
644+
$message .= ' - URL: ' . htmlentities($downloadURL);
633645

634646
throw new \Exception($message);
635647
}
@@ -662,14 +674,19 @@ private function getDownloadedFilePath(): string {
662674
*
663675
* @throws \Exception
664676
*/
665-
public function verifyIntegrity(): void {
677+
public function verifyIntegrity(?string $urlOverride = null): void {
666678
$this->silentLog('[info] verifyIntegrity()');
667679

668680
if ($this->getCurrentReleaseChannel() === 'daily') {
669681
$this->silentLog('[info] current channel is "daily" which is not signed. Skipping verification.');
670682
return;
671683
}
672684

685+
if ($urlOverride) {
686+
$this->silentLog('[info] custom download url provided, cannot verify signature');
687+
return;
688+
}
689+
673690
$response = $this->getUpdateServerResponse();
674691
if (empty($response['signature'])) {
675692
throw new \Exception('No signature specified for defined update');

lib/UpdateCommand.php

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class UpdateCommand extends Command {
3636
protected bool $shouldStop = false;
3737
protected bool $skipBackup = false;
3838
protected bool $skipUpgrade = false;
39+
protected string $urlOverride = '';
3940

4041
/** @var list<string> strings of text for stages of updater */
4142
protected array $checkTexts = [
@@ -60,7 +61,8 @@ protected function configure(): void {
6061
->setDescription('Updates the code of an Nextcloud instance')
6162
->setHelp("This command fetches the latest code that is announced via the updater server and safely replaces the existing code with the new one.")
6263
->addOption('no-backup', null, InputOption::VALUE_NONE, 'Skip backup of current Nextcloud version')
63-
->addOption('no-upgrade', null, InputOption::VALUE_NONE, "Don't automatically run occ upgrade");
64+
->addOption('no-upgrade', null, InputOption::VALUE_NONE, "Don't automatically run occ upgrade")
65+
->addOption('url', null, InputOption::VALUE_OPTIONAL, 'The URL of the Nextcloud release to download');
6466
}
6567

6668
public static function getUpdaterVersion(): string {
@@ -75,6 +77,7 @@ public static function getUpdaterVersion(): string {
7577
protected function execute(InputInterface $input, OutputInterface $output) {
7678
$this->skipBackup = (bool)$input->getOption('no-backup');
7779
$this->skipUpgrade = (bool)$input->getOption('no-upgrade');
80+
$this->urlOverride = (string)$input->getOption('url');
7881

7982
$version = static::getUpdaterVersion();
8083
$output->writeln('Nextcloud Updater - version: ' . $version);
@@ -148,7 +151,12 @@ protected function execute(InputInterface $input, OutputInterface $output) {
148151
$output->writeln('Current version is ' . $this->updater->getCurrentVersion() . '.');
149152

150153
// needs to be called that early because otherwise updateAvailable() returns false
151-
$updateString = $this->updater->checkForUpdate();
154+
if ($this->urlOverride) {
155+
$this->updater->log('[info] Using URL override: ' . $this->urlOverride);
156+
$updateString = 'Update check forced with URL override: ' . $this->urlOverride;
157+
} else {
158+
$updateString = $this->updater->checkForUpdate();
159+
}
152160

153161
$output->writeln('');
154162

@@ -161,9 +169,11 @@ protected function execute(InputInterface $input, OutputInterface $output) {
161169

162170
$output->writeln('');
163171

164-
if (!$this->updater->updateAvailable() && $stepNumber === 0) {
165-
$output->writeln('Nothing to do.');
166-
return 0;
172+
if (!$this->urlOverride) {
173+
if (!$this->updater->updateAvailable() && $stepNumber === 0) {
174+
$output->writeln('Nothing to do.');
175+
return 0;
176+
}
167177
}
168178

169179
$questionText = 'Start update';
@@ -375,10 +385,10 @@ protected function executeStep(int $step): array {
375385
}
376386
break;
377387
case 4:
378-
$this->updater->downloadUpdate();
388+
$this->updater->downloadUpdate($this->urlOverride);
379389
break;
380390
case 5:
381-
$this->updater->verifyIntegrity();
391+
$this->updater->verifyIntegrity($this->urlOverride);
382392
break;
383393
case 6:
384394
$this->updater->extractDownload();

lib/Updater.php

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,7 @@
11
<?php
22
/**
3-
* @copyright Copyright (c) 2016-2017 Lukas Reschke <lukas@statuscode.ch>
4-
* @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de>
5-
* @copyright Copyright (c) 2018 Jonas Sulzer <jonas@violoncello.ch>
6-
*
7-
* @license GNU AGPL version 3 or any later version
8-
*
9-
* This program is free software: you can redistribute it and/or modify
10-
* it under the terms of the GNU Affero General Public License as
11-
* published by the Free Software Foundation, either version 3 of the
12-
* License, or (at your option) any later version.
13-
*
14-
* This program is distributed in the hope that it will be useful,
15-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17-
* GNU Affero General Public License for more details.
18-
*
19-
* You should have received a copy of the GNU Affero General Public License
20-
* along with this program. If not, see <http://www.gnu.org/licenses/>.
21-
*
3+
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
4+
* SPDX-License-Identifier: AGPL-3.0-or-later
225
*/
236

247
namespace NC\Updater;
@@ -529,11 +512,9 @@ private function getUpdateServerResponse(): array {
529512
*
530513
* @throws \Exception
531514
*/
532-
public function downloadUpdate(): void {
515+
public function downloadUpdate(?string $url = null): void {
533516
$this->silentLog('[info] downloadUpdate()');
534517

535-
$response = $this->getUpdateServerResponse();
536-
537518
$storageLocation = $this->getUpdateDirectoryLocation() . '/updater-'.$this->getConfigOptionMandatoryString('instanceid') . '/downloads/';
538519
if (file_exists($storageLocation)) {
539520
$this->silentLog('[info] storage location exists');
@@ -544,12 +525,26 @@ public function downloadUpdate(): void {
544525
throw new \Exception('Could not mkdir storage location');
545526
}
546527

547-
if (!isset($response['url']) || !is_string($response['url'])) {
548-
throw new \Exception('Response from update server is missing url');
528+
$downloadURL = '';
529+
if ($url) {
530+
// If a URL is provided, use it directly
531+
$downloadURL = $url;
532+
} else {
533+
// Otherwise, get the download URLs from the update server
534+
$response = $this->getUpdateServerResponse();
535+
536+
if (!isset($response['url']) || !is_string($response['url'])) {
537+
throw new \Exception('Response from update server is missing url');
538+
}
539+
$downloadURL = $response['url'];
540+
}
541+
542+
if (!$downloadURL) {
543+
throw new \Exception('No download URL provided or available from update server');
549544
}
550545

551-
$fp = fopen($storageLocation . basename($response['url']), 'w+');
552-
$ch = curl_init($response['url']);
546+
$fp = fopen($storageLocation . basename($downloadURL), 'w+');
547+
$ch = curl_init($downloadURL);
553548
curl_setopt_array($ch, [
554549
CURLOPT_FILE => $fp,
555550
CURLOPT_USERAGENT => 'Nextcloud Updater',
@@ -591,7 +586,7 @@ public function downloadUpdate(): void {
591586
$message .= ' - curl error message: ' . $curlErrorMessage;
592587
}
593588

594-
$message .= ' - URL: ' . htmlentities($response['url']);
589+
$message .= ' - URL: ' . htmlentities($downloadURL);
595590

596591
throw new \Exception($message);
597592
}
@@ -624,14 +619,19 @@ private function getDownloadedFilePath(): string {
624619
*
625620
* @throws \Exception
626621
*/
627-
public function verifyIntegrity(): void {
622+
public function verifyIntegrity(?string $urlOverride = null): void {
628623
$this->silentLog('[info] verifyIntegrity()');
629624

630625
if ($this->getCurrentReleaseChannel() === 'daily') {
631626
$this->silentLog('[info] current channel is "daily" which is not signed. Skipping verification.');
632627
return;
633628
}
634629

630+
if ($urlOverride) {
631+
$this->silentLog('[info] custom download url provided, cannot verify signature');
632+
return;
633+
}
634+
635635
$response = $this->getUpdateServerResponse();
636636
if (empty($response['signature'])) {
637637
throw new \Exception('No signature specified for defined update');

updater.phar

1.34 KB
Binary file not shown.

vendor/autoload.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@
1414
echo $err;
1515
}
1616
}
17-
trigger_error(
18-
$err,
19-
E_USER_ERROR
20-
);
17+
throw new RuntimeException($err);
2118
}
2219

2320
require_once __DIR__ . '/composer/autoload_real.php';

vendor/composer/InstalledVersions.php

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,23 @@
2626
*/
2727
class InstalledVersions
2828
{
29+
/**
30+
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
31+
* @internal
32+
*/
33+
private static $selfDir = null;
34+
2935
/**
3036
* @var mixed[]|null
3137
* @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<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
3238
*/
3339
private static $installed;
3440

41+
/**
42+
* @var bool
43+
*/
44+
private static $installedIsLocalDir;
45+
3546
/**
3647
* @var bool|null
3748
*/
@@ -309,6 +320,24 @@ public static function reload($data)
309320
{
310321
self::$installed = $data;
311322
self::$installedByVendor = array();
323+
324+
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
325+
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
326+
// so we have to assume it does not, and that may result in duplicate data being returned when listing
327+
// all installed packages for example
328+
self::$installedIsLocalDir = false;
329+
}
330+
331+
/**
332+
* @return string
333+
*/
334+
private static function getSelfDir()
335+
{
336+
if (self::$selfDir === null) {
337+
self::$selfDir = strtr(__DIR__, '\\', '/');
338+
}
339+
340+
return self::$selfDir;
312341
}
313342

314343
/**
@@ -322,19 +351,27 @@ private static function getInstalled()
322351
}
323352

324353
$installed = array();
354+
$copiedLocalDir = false;
325355

326356
if (self::$canGetVendors) {
357+
$selfDir = self::getSelfDir();
327358
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
359+
$vendorDir = strtr($vendorDir, '\\', '/');
328360
if (isset(self::$installedByVendor[$vendorDir])) {
329361
$installed[] = self::$installedByVendor[$vendorDir];
330362
} elseif (is_file($vendorDir.'/composer/installed.php')) {
331363
/** @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<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
332364
$required = require $vendorDir.'/composer/installed.php';
333-
$installed[] = self::$installedByVendor[$vendorDir] = $required;
334-
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
335-
self::$installed = $installed[count($installed) - 1];
365+
self::$installedByVendor[$vendorDir] = $required;
366+
$installed[] = $required;
367+
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
368+
self::$installed = $required;
369+
self::$installedIsLocalDir = true;
336370
}
337371
}
372+
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
373+
$copiedLocalDir = true;
374+
}
338375
}
339376
}
340377

@@ -350,7 +387,7 @@ private static function getInstalled()
350387
}
351388
}
352389

353-
if (self::$installed !== array()) {
390+
if (self::$installed !== array() && !$copiedLocalDir) {
354391
$installed[] = self::$installed;
355392
}
356393

0 commit comments

Comments
 (0)