Skip to content

Commit 8b2e07b

Browse files
makoehrskjnldsv
authored andcommitted
feat: allow to extend .user.ini on update
When running nextcloud with a web hoster it might be necessary to extend .user.ini after each update (e.g. adding memory_limit). To automate this step, an additional config entry may be provided in config.php that specifies the lines to be added to .user.ini. If the config option 'user_ini_additional_lines' exists, the provided value (string or array of strings) will be added to .user.ini. Signed-off-by: Mathias Koehrer <koehrer08@koehrer-mail.de>
1 parent b3c0bcf commit 8b2e07b

4 files changed

Lines changed: 162 additions & 240 deletions

File tree

index.php

Lines changed: 80 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ class LogException extends \Exception {
2727
}
2828

2929

30-
use CurlHandle;
31-
3230
class Updater {
3331
private string $nextcloudDir;
3432
private array $configValues = [];
@@ -280,7 +278,6 @@ private function getExpectedElementsList(): array {
280278
'COPYING-AGPL',
281279
'occ',
282280
'db_structure.xml',
283-
'REUSE.toml',
284281
];
285282
return array_merge($expected, $this->getAppDirectories());
286283
}
@@ -520,7 +517,20 @@ private function getUpdateServerResponse(): array {
520517
$this->silentLog('[info] updateURL: ' . $updateURL);
521518

522519
// Download update response
523-
$curl = $this->getCurl($updateURL);
520+
$curl = curl_init();
521+
curl_setopt_array($curl, [
522+
CURLOPT_RETURNTRANSFER => 1,
523+
CURLOPT_URL => $updateURL,
524+
CURLOPT_USERAGENT => 'Nextcloud Updater',
525+
]);
526+
527+
if ($this->getConfigOption('proxy') !== null) {
528+
curl_setopt_array($curl, [
529+
CURLOPT_PROXY => $this->getConfigOptionString('proxy'),
530+
CURLOPT_PROXYUSERPWD => $this->getConfigOptionString('proxyuserpwd'),
531+
CURLOPT_HTTPPROXYTUNNEL => $this->getConfigOption('proxy') ? 1 : 0,
532+
]);
533+
}
524534

525535
/** @var false|string $response */
526536
$response = curl_exec($curl);
@@ -555,20 +565,19 @@ private function getUpdateServerResponse(): array {
555565
*
556566
* @throws \Exception
557567
*/
558-
public function downloadUpdate(?string $url = null): void {
568+
public function downloadUpdate(): void {
559569
$this->silentLog('[info] downloadUpdate()');
560570

561-
if ($url) {
562-
// If a URL is provided, use it directly
563-
$downloadURLs = [$url];
564-
} else {
565-
// Otherwise, get the download URLs from the update server
566-
$downloadURLs = $this->getDownloadURLs();
571+
$response = $this->getUpdateServerResponse();
572+
if (!isset($response['url']) || !is_string($response['url'])) {
573+
throw new \Exception('Response from update server is missing url');
567574
}
568575

569-
$this->silentLog('[info] will try to download archive from: ' . implode(', ', $downloadURLs));
570-
571576
$storageLocation = $this->getUpdateDirectoryLocation() . '/updater-' . $this->getConfigOptionMandatoryString('instanceid') . '/downloads/';
577+
$saveLocation = $storageLocation . basename($response['url']);
578+
$this->previousProgress = 0;
579+
580+
$ch = curl_init($response['url']);
572581

573582
if (!file_exists($storageLocation)) {
574583
$state = mkdir($storageLocation, 0750, true);
@@ -583,64 +592,22 @@ public function downloadUpdate(?string $url = null): void {
583592
$this->silentLog('[info] extracted Archive location exists');
584593
$this->recursiveDelete($storageLocation . 'nextcloud/');
585594
}
586-
}
587-
588-
foreach ($downloadURLs as $url) {
589-
$this->previousProgress = 0;
590-
$saveLocation = $storageLocation . basename($url);
591-
if ($this->downloadArchive($url, $saveLocation)) {
592-
return;
593-
}
594-
}
595-
596-
throw new \Exception('All downloads failed. See updater logs for more information.');
597-
}
598-
599-
private function getDownloadURLs(): array {
600-
$response = $this->getUpdateServerResponse();
601-
$downloadURLs = [];
602-
if (!isset($response['downloads']) || !is_array($response['downloads'])) {
603-
if (isset($response['url']) && is_string($response['url'])) {
604-
// Compatibility with previous verison of updater_server
605-
$ext = pathinfo($response['url'], PATHINFO_EXTENSION);
606-
$response['downloads'] = [
607-
$ext => [$response['url']]
608-
];
609-
} else {
610-
throw new \Exception('Response from update server is missing download URLs');
611-
}
612-
}
613-
foreach ($response['downloads'] as $format => $urls) {
614-
if (!$this->isAbleToDecompress($format)) {
615-
continue;
616-
}
617-
foreach ($urls as $url) {
618-
if (!is_string($url)) {
619-
continue;
620-
}
621-
$downloadURLs[] = $url;
595+
// see if there's an existing incomplete download to resume
596+
if (is_file($saveLocation)) {
597+
$size = filesize($saveLocation);
598+
$range = $size . '-';
599+
curl_setopt($ch, CURLOPT_RANGE, $range);
600+
$this->silentLog('[info] previous download found; resuming from ' . $this->formatBytes($size));
622601
}
623602
}
624603

625-
if (empty($downloadURLs)) {
626-
throw new \Exception('Your PHP install is not able to decompress any archive. Try to install modules like zip or bzip.');
627-
}
628-
629-
return array_unique($downloadURLs);
630-
631-
}
632-
633-
private function getCurl(string $url): CurlHandle {
634-
$ch = curl_init($url);
635-
if ($ch === false) {
636-
throw new \Exception('Fail to open cUrl handler');
637-
}
638-
604+
$fp = fopen($saveLocation, 'a');
639605
curl_setopt_array($ch, [
640606
CURLOPT_RETURNTRANSFER => true,
607+
CURLOPT_NOPROGRESS => false,
608+
CURLOPT_PROGRESSFUNCTION => [$this, 'downloadProgressCallback'],
609+
CURLOPT_FILE => $fp,
641610
CURLOPT_USERAGENT => 'Nextcloud Updater',
642-
CURLOPT_FOLLOWLOCATION => 1,
643-
CURLOPT_MAXREDIRS => 2,
644611
]);
645612

646613
if ($this->getConfigOption('proxy') !== null) {
@@ -651,61 +618,47 @@ private function getCurl(string $url): CurlHandle {
651618
]);
652619
}
653620

654-
return $ch;
655-
}
656-
657-
private function downloadArchive(string $fromUrl, string $toLocation): bool {
658-
$ch = $this->getCurl($fromUrl);
659-
660-
// see if there's an existing incomplete download to resume
661-
if (is_file($toLocation)) {
662-
$size = (int)filesize($toLocation);
663-
$range = $size . '-';
664-
curl_setopt($ch, CURLOPT_RANGE, $range);
665-
$this->silentLog('[info] previous download found; resuming from ' . $this->formatBytes($size));
666-
}
667-
668-
$fp = fopen($toLocation, 'ab');
669-
if ($fp === false) {
670-
throw new \Exception('Fail to open file in ' . $toLocation);
671-
}
672-
673-
curl_setopt_array($ch, [
674-
CURLOPT_NOPROGRESS => false,
675-
CURLOPT_PROGRESSFUNCTION => [$this, 'downloadProgressCallback'],
676-
CURLOPT_FILE => $fp,
677-
]);
678-
679621
if (curl_exec($ch) === false) {
680622
throw new \Exception('Curl error: ' . curl_error($ch));
681623
}
682-
683624
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
684625
if ($httpCode !== 200 && $httpCode !== 206) {
685-
fclose($fp);
686-
unlink($toLocation);
687-
$this->silentLog('[warn] fail to download archive from ' . $fromUrl . '. Error: ' . $httpCode . ' ' . curl_error($ch));
688-
curl_close($ch);
626+
$statusCodes = [
627+
400 => 'Bad request',
628+
401 => 'Unauthorized',
629+
403 => 'Forbidden',
630+
404 => 'Not Found',
631+
500 => 'Internal Server Error',
632+
502 => 'Bad Gateway',
633+
503 => 'Service Unavailable',
634+
504 => 'Gateway Timeout',
635+
];
636+
637+
$message = 'Download failed';
638+
if (is_int($httpCode) && isset($statusCodes[$httpCode])) {
639+
$message .= ' - ' . $statusCodes[$httpCode] . ' (HTTP ' . $httpCode . ')';
640+
} else {
641+
$message .= ' - HTTP status code: ' . (string)$httpCode;
642+
}
689643

690-
return false;
644+
$curlErrorMessage = curl_error($ch);
645+
if (!empty($curlErrorMessage)) {
646+
$message .= ' - curl error message: ' . $curlErrorMessage;
647+
}
648+
649+
$message .= ' - URL: ' . htmlentities($response['url']);
650+
651+
throw new \Exception($message);
652+
} else {
653+
// download succeeded
654+
$info = curl_getinfo($ch);
655+
$this->silentLog('[info] download stats: size=' . $this->formatBytes((int)$info['size_download']) . ' bytes; total_time=' . round($info['total_time'], 2) . ' secs; avg speed=' . $this->formatBytes((int)$info['speed_download']) . '/sec');
691656
}
692-
// download succeeded
693-
$info = curl_getinfo($ch);
694-
$this->silentLog('[info] download stats: size=' . $this->formatBytes((int)$info['size_download']) . ' bytes; total_time=' . round($info['total_time'], 2) . ' secs; avg speed=' . $this->formatBytes((int)$info['speed_download']) . '/sec');
695657

696658
curl_close($ch);
697659
fclose($fp);
698660

699661
$this->silentLog('[info] end of downloadUpdate()');
700-
return true;
701-
}
702-
703-
/**
704-
* Check if PHP is able to decompress archive format
705-
*/
706-
private function isAbleToDecompress(string $ext): bool {
707-
// Only zip is supported for now
708-
return $ext === 'zip' && extension_loaded($ext);
709662
}
710663

711664
private function downloadProgressCallback(\CurlHandle $resource, int $download_size, int $downloaded, int $upload_size, int $uploaded): void {
@@ -722,7 +675,7 @@ private function downloadProgressCallback(\CurlHandle $resource, int $download_s
722675
}
723676

724677
private function formatBytes(int $bytes, int $precision = 2): string {
725-
$units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
678+
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
726679

727680
$bytes = max($bytes, 0);
728681
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
@@ -744,34 +697,28 @@ private function getDownloadedFilePath(): string {
744697

745698
$filesInStorageLocation = scandir($storageLocation);
746699
$files = array_values(array_filter($filesInStorageLocation, function (string $path) {
747-
// Match files with - in the name and extension (*-*.*)
748-
return preg_match('/^.*-.*\..*$/i', $path);
700+
return $path !== '.' && $path !== '..';
749701
}));
750702
// only the downloaded archive
751703
if (count($files) !== 1) {
752704
throw new \Exception('There are more files than the downloaded archive in the downloads/ folder.');
753705
}
754-
return $storageLocation . $files[0];
706+
return $storageLocation . '/' . $files[0];
755707
}
756708

757709
/**
758710
* Verifies the integrity of the downloaded file
759711
*
760712
* @throws \Exception
761713
*/
762-
public function verifyIntegrity(?string $urlOverride = null): void {
714+
public function verifyIntegrity(): void {
763715
$this->silentLog('[info] verifyIntegrity()');
764716

765717
if ($this->getCurrentReleaseChannel() === 'daily') {
766718
$this->silentLog('[info] current channel is "daily" which is not signed. Skipping verification.');
767719
return;
768720
}
769721

770-
if ($urlOverride) {
771-
$this->silentLog('[info] custom download url provided, cannot verify signature');
772-
return;
773-
}
774-
775722
$response = $this->getUpdateServerResponse();
776723
if (empty($response['signature'])) {
777724
throw new \Exception('No signature specified for defined update');
@@ -1059,7 +1006,7 @@ private function moveWithExclusions(string $dataLocation, array $excludedElement
10591006
throw new \Exception('Could not mkdir ' . $this->nextcloudDir . '/' . dirname($fileName));
10601007
}
10611008
}
1062-
$state = @rename($path, $this->nextcloudDir . '/' . $fileName);
1009+
$state = rename($path, $this->nextcloudDir . '/' . $fileName);
10631010
if ($state === false) {
10641011
throw new \Exception(
10651012
sprintf(
@@ -1129,6 +1076,20 @@ public function finalize(): void {
11291076
throw new \Exception('Could not rmdir .step');
11301077
}
11311078

1079+
/* Check if there is the need to extend .user.ini */
1080+
$user_ini_additional_lines = $this->getConfigOption('user_ini_additional_lines');
1081+
if ($user_ini_additional_lines) {
1082+
$this->silentLog('[info] Extend .user.ini');
1083+
if (is_array($user_ini_additional_lines)) {
1084+
$user_ini_additional_lines = implode(PHP_EOL, $user_ini_additional_lines);
1085+
}
1086+
1087+
$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);
1088+
if ($result === false) {
1089+
throw new \Exception('Could not append to .user.ini');
1090+
}
1091+
}
1092+
11321093
if (function_exists('opcache_reset')) {
11331094
$this->silentLog('[info] call opcache_reset()');
11341095
opcache_reset();

0 commit comments

Comments
 (0)