diff options
Diffstat (limited to 'lib/private/Installer.php')
-rw-r--r-- | lib/private/Installer.php | 165 |
1 files changed, 104 insertions, 61 deletions
diff --git a/lib/private/Installer.php b/lib/private/Installer.php index c4df7768d9e..91d20a129ae 100644 --- a/lib/private/Installer.php +++ b/lib/private/Installer.php @@ -3,60 +3,30 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @copyright Copyright (c) 2016, Lukas Reschke <lukas@statuscode.ch> - * - * @author acsfer <carlos@reendex.com> - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Brice Maron <brice@bmaron.net> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Frank Karlitschek <frank@karlitschek.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Kamil Domanski <kdomanski@kdemail.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author root "root@oc.(none)" - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Thomas Tanghus <thomas@tanghus.net> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC; use Doctrine\DBAL\Exception\TableExistsException; +use OC\App\AppStore\AppNotFoundException; use OC\App\AppStore\Bundles\Bundle; use OC\App\AppStore\Fetcher\AppFetcher; use OC\AppFramework\Bootstrap\Coordinator; use OC\Archive\TAR; use OC\DB\Connection; use OC\DB\MigrationService; +use OC\Files\FilenameValidator; use OC_App; -use OC_Helper; use OCP\App\IAppManager; +use OCP\Files; use OCP\HintException; use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\ITempManager; use OCP\Migration\IOutput; +use OCP\Server; use phpseclib\File\X509; use Psr\Log\LoggerInterface; @@ -91,14 +61,14 @@ class Installer { throw new \Exception('App not found in any app directory'); } - $basedir = $app['path'].'/'.$appId; + $basedir = $app['path'] . '/' . $appId; if (is_file($basedir . '/appinfo/database.xml')) { throw new \Exception('The appinfo/database.xml file is not longer supported. Used in ' . $appId); } $l = \OCP\Util::getL10N('core'); - $info = \OCP\Server::get(IAppManager::class)->getAppInfo($basedir . '/appinfo/info.xml', true, $l->getLanguageCode()); + $info = \OCP\Server::get(IAppManager::class)->getAppInfoByPath($basedir . '/appinfo/info.xml', $l->getLanguageCode()); if (!is_array($info)) { throw new \Exception( @@ -155,10 +125,10 @@ class Installer { //set remote/public handlers foreach ($info['remote'] as $name => $path) { - $config->setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path); + $config->setAppValue('core', 'remote_' . $name, $info['id'] . '/' . $path); } foreach ($info['public'] as $name => $path) { - $config->setAppValue('core', 'public_'.$name, $info['id'].'/'.$path); + $config->setAppValue('core', 'public_' . $name, $info['id'] . '/' . $path); } OC_App::setAppTypes($info['id']); @@ -200,16 +170,44 @@ class Installer { } /** + * Get the path where to install apps + * + * @throws \RuntimeException if an app folder is marked as writable but is missing permissions + */ + public function getInstallPath(): ?string { + foreach (\OC::$APPSROOTS as $dir) { + if (isset($dir['writable']) && $dir['writable'] === true) { + // Check if there is a writable install folder. + if (!is_writable($dir['path']) + || !is_readable($dir['path']) + ) { + throw new \RuntimeException( + 'Cannot write into "apps" directory. This can usually be fixed by giving the web server write access to the apps directory or disabling the App Store in the config file.' + ); + } + return $dir['path']; + } + } + return null; + } + + /** * Downloads an app and puts it into the app directory * * @param string $appId * @param bool [$allowUnstable] * + * @throws AppNotFoundException If the app is not found on the appstore * @throws \Exception If the installation was not successful */ public function downloadApp(string $appId, bool $allowUnstable = false): void { $appId = strtolower($appId); + $installPath = $this->getInstallPath(); + if ($installPath === null) { + throw new \Exception('No application directories are marked as writable.'); + } + $apps = $this->appFetcher->get($allowUnstable); foreach ($apps as $app) { if ($app['id'] === $appId) { @@ -274,19 +272,26 @@ class Installer { // Download the release $tempFile = $this->tempManager->getTemporaryFile('.tar.gz'); + if ($tempFile === false) { + throw new \RuntimeException('Could not create temporary file for downloading app archive.'); + } + $timeout = $this->isCLI ? 0 : 120; $client = $this->clientService->newClient(); $client->get($app['releases'][0]['download'], ['sink' => $tempFile, 'timeout' => $timeout]); // Check if the signature actually matches the downloaded content $certificate = openssl_get_publickey($app['certificate']); - $verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512); + $verified = openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512) === 1; if ($verified === true) { // Seems to match, let's proceed $extractDir = $this->tempManager->getTemporaryFolder(); - $archive = new TAR($tempFile); + if ($extractDir === false) { + throw new \RuntimeException('Could not create temporary directory for unpacking app.'); + } + $archive = new TAR($tempFile); if (!$archive->extract($extractDir)) { $errorMessage = 'Could not extract app ' . $appId; @@ -355,16 +360,19 @@ class Installer { ); } - $baseDir = OC_App::getInstallPath() . '/' . $appId; + $baseDir = $installPath . '/' . $appId; // Remove old app with the ID if existent - OC_Helper::rmdirr($baseDir); + Files::rmdirr($baseDir); // Move to app folder if (@mkdir($baseDir)) { $extractDir .= '/' . $folders[0]; - OC_Helper::copyr($extractDir, $baseDir); } - OC_Helper::copyr($extractDir, $baseDir); - OC_Helper::rmdirr($extractDir); + // otherwise we just copy the outer directory + $this->copyRecursive($extractDir, $baseDir); + Files::rmdirr($extractDir); + if (function_exists('opcache_reset')) { + opcache_reset(); + } return; } // Signature does not match @@ -377,9 +385,9 @@ class Installer { } } - throw new \Exception( + throw new AppNotFoundException( sprintf( - 'Could not download app %s', + 'Could not download app %s, it was not found on the appstore', $appId ) ); @@ -394,7 +402,7 @@ class Installer { */ public function isUpdateAvailable($appId, $allowUnstable = false): string|false { if ($this->isInstanceReadyForUpdates === null) { - $installPath = OC_App::getInstallPath(); + $installPath = $this->getInstallPath(); if ($installPath === null) { $this->isInstanceReadyForUpdates = false; } else { @@ -443,8 +451,8 @@ class Installer { if ($app === false) { return false; } - $basedir = $app['path'].'/'.$appId; - return file_exists($basedir.'/.git/'); + $basedir = $app['path'] . '/' . $appId; + return file_exists($basedir . '/.git/'); } /** @@ -482,11 +490,17 @@ class Installer { if (\OCP\Server::get(IAppManager::class)->isShipped($appId)) { return false; } - $appDir = OC_App::getInstallPath() . '/' . $appId; - OC_Helper::rmdirr($appDir); + + $installPath = $this->getInstallPath(); + if ($installPath === null) { + $this->logger->error('No application directories are marked as writable.', ['app' => 'core']); + return false; + } + $appDir = $installPath . '/' . $appId; + Files::rmdirr($appDir); return true; } else { - $this->logger->error('can\'t remove app '.$appId.'. It is not installed.'); + $this->logger->error('can\'t remove app ' . $appId . '. It is not installed.'); return false; } @@ -530,9 +544,9 @@ class Installer { foreach (\OC::$APPSROOTS as $app_dir) { if ($dir = opendir($app_dir['path'])) { while (false !== ($filename = readdir($dir))) { - if ($filename[0] !== '.' and is_dir($app_dir['path']."/$filename")) { - if (file_exists($app_dir['path']."/$filename/appinfo/info.xml")) { - if ($config->getAppValue($filename, "installed_version", null) === null) { + if ($filename[0] !== '.' and is_dir($app_dir['path'] . "/$filename")) { + if (file_exists($app_dir['path'] . "/$filename/appinfo/info.xml")) { + if ($config->getAppValue($filename, 'installed_version', null) === null) { $enabled = $appManager->isDefaultEnabled($filename); if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps())) && $config->getAppValue($filename, 'enabled') !== 'no') { @@ -604,10 +618,10 @@ class Installer { //set remote/public handlers foreach ($info['remote'] as $name => $path) { - $config->setAppValue('core', 'remote_'.$name, $app.'/'.$path); + $config->setAppValue('core', 'remote_' . $name, $app . '/' . $path); } foreach ($info['public'] as $name => $path) { - $config->setAppValue('core', 'public_'.$name, $app.'/'.$path); + $config->setAppValue('core', 'public_' . $name, $app . '/' . $path); } OC_App::setAppTypes($info['id']); @@ -620,4 +634,33 @@ class Installer { include $script; } } + + /** + * Recursive copying of local folders. + * + * @param string $src source folder + * @param string $dest target folder + */ + private function copyRecursive(string $src, string $dest): void { + if (!file_exists($src)) { + return; + } + + if (is_dir($src)) { + if (!is_dir($dest)) { + mkdir($dest); + } + $files = scandir($src); + foreach ($files as $file) { + if ($file != '.' && $file != '..') { + $this->copyRecursive("$src/$file", "$dest/$file"); + } + } + } else { + $validator = Server::get(FilenameValidator::class); + if (!$validator->isForbidden($src)) { + copy($src, $dest); + } + } + } } |