aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Installer.php
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Installer.php')
-rw-r--r--lib/private/Installer.php165
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);
+ }
+ }
+ }
}