1<?php 2/** 3 * @author Arthur Schiwon <blizzz@arthur-schiwon.de> 4 * @author Bart Visscher <bartv@thisnet.nl> 5 * @author Brice Maron <brice@bmaron.net> 6 * @author Christian Weiske <cweiske@cweiske.de> 7 * @author Christopher Schäpers <kondou@ts.unde.re> 8 * @author Frank Karlitschek <frank@karlitschek.de> 9 * @author Georg Ehrke <georg@owncloud.com> 10 * @author Jakob Sack <mail@jakobsack.de> 11 * @author Joas Schilling <coding@schilljs.com> 12 * @author Jörn Friedrich Dreyer <jfd@butonic.de> 13 * @author Kamil Domanski <kdomanski@kdemail.net> 14 * @author Lukas Reschke <lukas@statuscode.ch> 15 * @author michag86 <micha_g@arcor.de> 16 * @author Morris Jobke <hey@morrisjobke.de> 17 * @author Robin Appelman <icewind@owncloud.com> 18 * @author Roeland Jago Douma <rullzer@owncloud.com> 19 * @author Thomas Müller <thomas.mueller@tmit.eu> 20 * @author Thomas Tanghus <thomas@tanghus.net> 21 * 22 * @copyright Copyright (c) 2018, ownCloud GmbH 23 * @license AGPL-3.0 24 * 25 * This code is free software: you can redistribute it and/or modify 26 * it under the terms of the GNU Affero General Public License, version 3, 27 * as published by the Free Software Foundation. 28 * 29 * This program is distributed in the hope that it will be useful, 30 * but WITHOUT ANY WARRANTY; without even the implied warranty of 31 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 32 * GNU Affero General Public License for more details. 33 * 34 * You should have received a copy of the GNU Affero General Public License, version 3, 35 * along with this program. If not, see <http://www.gnu.org/licenses/> 36 * 37 */ 38 39namespace OC; 40 41use Doctrine\DBAL\Exception\TableExistsException; 42use OC\App\CodeChecker\CodeChecker; 43use OC\App\CodeChecker\EmptyCheck; 44use OC\App\CodeChecker\PrivateCheck; 45use OC\DB\MigrationService; 46use OC_App; 47use OC_DB; 48use OC_Helper; 49use OCP\App\AppAlreadyInstalledException; 50 51/** 52 * This class provides the functionality needed to install, update and remove plugins/apps 53 */ 54class Installer { 55 56 /** 57 * 58 * This function installs an app. All information needed are passed in the 59 * associative array $data. 60 * The following keys are required: 61 * - source: string, can be "path" or "http" 62 * 63 * One of the following keys is required: 64 * - path: path to the file containing the app 65 * - href: link to the downloadable file containing the app 66 * 67 * The following keys are optional: 68 * - pretend: boolean, if set true the system won't do anything 69 * - noinstall: boolean, if true appinfo/install.php won't be loaded 70 * - inactive: boolean, if set true the appconfig/app.sample.php won't be 71 * renamed 72 * 73 * This function works as follows 74 * -# fetching the file 75 * -# unzipping it 76 * -# check the code 77 * -# installing the database at appinfo/database.xml 78 * -# including appinfo/install.php 79 * -# setting the installed version 80 * 81 * It is the task of oc_app_install to create the tables and do whatever is 82 * needed to get the app working. 83 * 84 * Installs an app 85 * @param array $data with all information 86 * @throws \Exception 87 * @return integer 88 */ 89 public static function installApp($data = []) { 90 $l = \OC::$server->getL10N('lib'); 91 92 list($extractDir, $path) = self::downloadApp($data); 93 94 $info = self::checkAppsIntegrity($data, $extractDir, $path); 95 $appId = OC_App::cleanAppId($info['id']); 96 $appsFolder = OC_App::getInstallPath(); 97 98 if ($appsFolder === null || !\is_writable($appsFolder)) { 99 throw new \Exception('Apps folder is not writable'); 100 } 101 $basedir = "$appsFolder/$appId"; 102 //check if the destination directory already exists 103 if (\is_dir($basedir)) { 104 OC_Helper::rmdirr($extractDir); 105 if ($data['source']=='http') { 106 \unlink($path); 107 } 108 throw new \Exception($l->t("App directory already exists")); 109 } 110 111 if (!empty($data['pretent'])) { 112 return false; 113 } 114 115 //copy the app to the correct place 116 if (@!\mkdir($basedir)) { 117 OC_Helper::rmdirr($extractDir); 118 if ($data['source']=='http') { 119 \unlink($path); 120 } 121 throw new \Exception($l->t("Can't create app folder. Please fix permissions. %s", [$basedir])); 122 } 123 124 $extractDir .= '/' . $info['id']; 125 if (!\file_exists($extractDir)) { 126 OC_Helper::rmdirr($basedir); 127 throw new \Exception($l->t("Archive does not contain a directory named %s", $info['id'])); 128 } 129 OC_Helper::copyr($extractDir, $basedir); 130 131 //remove temporary files 132 OC_Helper::rmdirr($extractDir); 133 134 //install the database 135 if (isset($info['use-migrations']) && $info['use-migrations'] === 'true') { 136 $ms = new \OC\DB\MigrationService($appId, \OC::$server->getDatabaseConnection()); 137 $ms->migrate(); 138 } else { 139 if (\is_file($basedir.'/appinfo/database.xml')) { 140 if (\OC::$server->getAppConfig()->getValue($info['id'], 'installed_version') === null) { 141 OC_DB::createDbFromStructure($basedir . '/appinfo/database.xml'); 142 } else { 143 OC_DB::updateDbFromStructure($basedir . '/appinfo/database.xml'); 144 } 145 } 146 } 147 148 \OC_App::setupBackgroundJobs($info['background-jobs']); 149 150 //run appinfo/install.php 151 if ((!isset($data['noinstall']) or $data['noinstall']==false)) { 152 self::includeAppScript($basedir . '/appinfo/install.php'); 153 } 154 155 $appData = OC_App::getAppInfo($appId); 156 OC_App::executeRepairSteps($appId, $appData['repair-steps']['install']); 157 158 //set the installed version 159 \OC::$server->getConfig()->setAppValue($info['id'], 'installed_version', OC_App::getAppVersion($info['id'])); 160 \OC::$server->getConfig()->setAppValue($info['id'], 'enabled', 'no'); 161 162 //set remote/public handlers 163 foreach ($info['remote'] as $name=>$path) { 164 \OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path); 165 } 166 foreach ($info['public'] as $name=>$path) { 167 \OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $info['id'].'/'.$path); 168 } 169 170 OC_App::setAppTypes($info['id']); 171 172 return $info['id']; 173 } 174 175 /** 176 * @brief checks whether or not an app is installed 177 * @param string $app app 178 * @return bool 179 * 180 * Checks whether or not an app is installed, i.e. registered in apps table. 181 */ 182 public static function isInstalled($app) { 183 return (\OC::$server->getConfig()->getAppValue($app, "installed_version", null) !== null); 184 } 185 186 /** 187 * @brief Update an application 188 * @param array $info 189 * @param bool $isShipped 190 * @throws \Exception 191 * @return bool 192 * 193 * This function could work like described below, but currently it disables and then 194 * enables the app again. This does result in an updated app. 195 * 196 * 197 * This function installs an app. All information needed are passed in the 198 * associative array $info. 199 * The following keys are required: 200 * - source: string, can be "path" or "http" 201 * 202 * One of the following keys is required: 203 * - path: path to the file containing the app 204 * - href: link to the downloadable file containing the app 205 * 206 * The following keys are optional: 207 * - pretend: boolean, if set true the system won't do anything 208 * - noupgrade: boolean, if true appinfo/upgrade.php won't be loaded 209 * 210 * This function works as follows 211 * -# fetching the file 212 * -# removing the old files 213 * -# unzipping new file 214 * -# including appinfo/upgrade.php 215 * -# setting the installed version 216 * 217 * upgrade.php can determine the current installed version of the app using 218 * "\OC::$server->getAppConfig()->getValue($appid, 'installed_version')" 219 */ 220 public static function updateApp($info= [], $isShipped=false) { 221 list($extractDir, $path) = self::downloadApp($info); 222 $info = self::checkAppsIntegrity($info, $extractDir, $path, $isShipped); 223 224 $currentDir = OC_App::getAppPath($info['id']); 225 if (\is_dir("$currentDir/.git")) { 226 throw new AppAlreadyInstalledException("App <{$info['id']}> is a git clone - it will not be updated."); 227 } 228 229 $basedir = OC_App::getInstallPath(); 230 $basedir .= '/'; 231 $basedir .= $info['id']; 232 233 if ($currentDir !== false && OC_App::isAppDirWritable($info['id'])) { 234 $basedir = $currentDir; 235 } 236 237 if (\is_dir($basedir)) { 238 OC_Helper::rmdirr($basedir); 239 } 240 241 $appInExtractDir = $extractDir; 242 if (\substr($extractDir, -1) !== '/') { 243 $appInExtractDir .= '/'; 244 } 245 246 $appInExtractDir .= $info['id']; 247 OC_Helper::copyr($appInExtractDir, $basedir); 248 OC_Helper::rmdirr($extractDir); 249 250 return OC_App::updateApp($info['id']); 251 } 252 253 /** 254 * @param array $data 255 * @return array 256 * @throws \Exception 257 */ 258 public static function downloadApp($data = []) { 259 $l = \OC::$server->getL10N('lib'); 260 261 if (!isset($data['source'])) { 262 throw new \Exception($l->t("No source specified when installing app")); 263 } 264 265 //download the file if necessary 266 if ($data['source']=='http') { 267 $pathInfo = \pathinfo($data['href']); 268 $extension = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : ''; 269 $path = \OC::$server->getTempManager()->getTemporaryFile($extension); 270 if (!isset($data['href'])) { 271 throw new \Exception($l->t("No href specified when installing app from http")); 272 } 273 $client = \OC::$server->getHTTPClientService()->newClient(); 274 $client->get($data['href'], ['save_to' => $path]); 275 } else { 276 if (!isset($data['path'])) { 277 throw new \Exception($l->t("No path specified when installing app from local file")); 278 } 279 $path=$data['path']; 280 } 281 282 //detect the archive type 283 $mime = \OC::$server->getMimeTypeDetector()->detect($path); 284 if ($mime !=='application/zip' && $mime !== 'application/x-gzip' && $mime !== 'application/gzip' && $mime !== 'application/x-bzip2') { 285 throw new \Exception($l->t("Archives of type %s are not supported", [$mime])); 286 } 287 288 //extract the archive in a temporary folder 289 $extractDir = \OC::$server->getTempManager()->getTemporaryFolder(); 290 OC_Helper::rmdirr($extractDir); 291 \mkdir($extractDir); 292 if ($archive=\OC\Archive\Archive::open($path)) { 293 $archive->extract($extractDir); 294 } else { 295 OC_Helper::rmdirr($extractDir); 296 if ($data['source']=='http') { 297 \unlink($path); 298 } 299 throw new \Exception($l->t("Failed to open archive when installing app")); 300 } 301 302 return [ 303 $extractDir, 304 $path 305 ]; 306 } 307 308 /** 309 * check an app's integrity 310 * @param array $data 311 * @param string $extractDir 312 * @param string $path 313 * @param bool $isShipped 314 * @return array 315 * @throws \Exception 316 */ 317 public static function checkAppsIntegrity($data, $extractDir, $path, $isShipped = false) { 318 $l = \OC::$server->getL10N('lib'); 319 //load the info.xml file of the app 320 if (!\is_file($extractDir.'/appinfo/info.xml')) { 321 //try to find it in a subdir 322 $dh=\opendir($extractDir); 323 if (\is_resource($dh)) { 324 while (($folder = \readdir($dh)) !== false) { 325 if ($folder[0]!='.' and \is_dir($extractDir.'/'.$folder)) { 326 if (\is_file($extractDir.'/'.$folder.'/appinfo/info.xml')) { 327 $extractDir.='/'.$folder; 328 } 329 } 330 } 331 } 332 } 333 if (!\is_file($extractDir.'/appinfo/info.xml')) { 334 OC_Helper::rmdirr($extractDir); 335 if ($data['source'] === 'http') { 336 \unlink($path); 337 } 338 throw new \Exception($l->t("App does not provide an info.xml file")); 339 } 340 341 $info = OC_App::getAppInfo($extractDir.'/appinfo/info.xml', true); 342 if (!\is_array($info)) { 343 throw new \Exception($l->t('App cannot be installed because appinfo file cannot be read.')); 344 } 345 346 // We can't trust the parsed info.xml file as it may have been tampered 347 // with by an attacker and thus we need to use the local data to check 348 // whether the application needs to be signed. 349 if (\file_exists("$extractDir/appinfo/signature.json")) { 350 \OC::$server->getConfig()->setAppValue($info['id'], 'signed', 'true'); 351 $integrityResult = \OC::$server->getIntegrityCodeChecker() 352 ->verifyAppSignature( 353 $info['id'], 354 $extractDir 355 ); 356 if ($integrityResult !== []) { 357 $e = new \Exception( 358 $l->t( 359 'Signature could not get checked. Please contact the app developer and check your admin screen.' 360 ) 361 ); 362 throw $e; 363 } 364 } 365 366 // check if the app is compatible with this version of ownCloud 367 if (!OC_App::isAppCompatible(\OCP\Util::getVersion(), $info)) { 368 OC_Helper::rmdirr($extractDir); 369 throw new \Exception($l->t("App can't be installed because it is not compatible with this version of ownCloud")); 370 } 371 372 // check if shipped tag is set which is only allowed for apps that are shipped with ownCloud 373 if (!$isShipped && isset($info['shipped']) && ($info['shipped']=='true')) { 374 OC_Helper::rmdirr($extractDir); 375 throw new \Exception($l->t("App can't be installed because it contains the <shipped>true</shipped> tag which is not allowed for non shipped apps")); 376 } 377 378 // check if the ocs version is the same as the version in info.xml/version 379 $version = \trim($info['version']); 380 381 if (isset($data['appdata']['version']) && $version<>\trim($data['appdata']['version'])) { 382 OC_Helper::rmdirr($extractDir); 383 throw new \Exception($l->t("App can't be installed because the version in info.xml is not the same as the version reported from the app store")); 384 } 385 386 return $info; 387 } 388 389 /** 390 * Check if app is already downloaded 391 * @param string $name name of the application to remove 392 * @return boolean 393 * 394 * The function will check if the app is already downloaded in the apps repository 395 */ 396 public static function isDownloaded($name) { 397 foreach (\OC::$APPSROOTS as $dir) { 398 $dirToTest = $dir['path']; 399 $dirToTest .= '/'; 400 $dirToTest .= $name; 401 $dirToTest .= '/'; 402 403 if (\is_dir($dirToTest)) { 404 return true; 405 } 406 } 407 408 return false; 409 } 410 411 /** 412 * Removes an app 413 * @param string $appId name of the application to remove 414 * @return boolean 415 * @throws AppAlreadyInstalledException 416 * 417 * 418 * This function works as follows 419 * -# call uninstall repair steps 420 * -# removing the files 421 * 422 * The function will not delete preferences, tables and the configuration, 423 * this has to be done by the function oc_app_uninstall(). 424 */ 425 public static function removeApp($appId) { 426 if (Installer::isDownloaded($appId)) { 427 $appDir = OC_App::getAppPath($appId); 428 if ($appDir === false) { 429 return false; 430 } 431 if (\is_dir("$appDir/.git")) { 432 throw new AppAlreadyInstalledException("App <$appId> is a git clone - it will not be deleted."); 433 } 434 435 OC_Helper::rmdirr($appDir); 436 437 return true; 438 } 439 \OCP\Util::writeLog('core', 'can\'t remove app '.$appId.'. It is not installed.', \OCP\Util::ERROR); 440 441 return false; 442 } 443 444 protected static function getShippedApps() { 445 $shippedApps = []; 446 foreach (\OC::$APPSROOTS as $app_dir) { 447 if ($dir = \opendir($app_dir['path'])) { 448 $nodes = \scandir($app_dir['path']); 449 foreach ($nodes as $filename) { 450 // Since core 10.5.0, enterprise_key app is no longer used 451 // Be sure not to accidentally enable it if it has been 452 // left behind in an apps dir. 453 if ($filename === 'enterprise_key') { 454 continue; 455 } 456 if (\substr($filename, 0, 1) != '.' and \is_dir($app_dir['path']."/$filename")) { 457 if (\file_exists($app_dir['path']."/$filename/appinfo/info.xml")) { 458 if (!Installer::isInstalled($filename)) { 459 $info=OC_App::getAppInfo($filename); 460 $enabled = isset($info['default_enable']); 461 if (($enabled || \in_array($filename, \OC::$server->getAppManager()->getAlwaysEnabledApps())) 462 && \OC::$server->getConfig()->getAppValue($filename, 'enabled') !== 'no') { 463 $shippedApps[] = $filename; 464 } 465 } 466 } 467 } 468 } 469 \closedir($dir); 470 } 471 } 472 473 // Fix the order - make files first 474 $shippedApps = \array_diff($shippedApps, ['files', 'dav']); 475 \array_unshift($shippedApps, 'dav'); 476 \array_unshift($shippedApps, 'files'); 477 return $shippedApps; 478 } 479 480 /** 481 * Installs shipped apps 482 * 483 * This function installs all apps found in the 'apps' directory that should be enabled by default; 484 * @param bool $softErrors When updating we ignore errors and simply log them, better to have a 485 * working ownCloud at the end instead of an aborted update. 486 * @return array Array of error messages (appid => Exception) 487 */ 488 public static function installShippedApps($softErrors = false) { 489 $errors = []; 490 $appsToInstall = Installer::getShippedApps(); 491 492 foreach ($appsToInstall as $appToInstall) { 493 if (!Installer::isInstalled($appToInstall)) { 494 if ($softErrors) { 495 try { 496 Installer::installShippedApp($appToInstall); 497 } catch (TableExistsException $e) { 498 \OC::$server->getLogger()->logException($e, ['app' => __CLASS__]); 499 $errors[$appToInstall] = $e; 500 continue; 501 } 502 } else { 503 Installer::installShippedApp($appToInstall); 504 } 505 \OC::$server->getConfig()->setAppValue($appToInstall, 'enabled', 'yes'); 506 } 507 } 508 509 return $errors; 510 } 511 512 /** 513 * install an app already placed in the app folder 514 * @param string $app id of the app to install 515 * @return integer|false 516 */ 517 public static function installShippedApp($app) { 518 \OC::$server->getLogger()->info('Attempting to install shipped app: '.$app); 519 520 $info = OC_App::getAppInfo($app); 521 if ($info === null) { 522 return false; 523 } 524 525 //install the database 526 $appPath = OC_App::getAppPath($app); 527 if (isset($info['use-migrations']) && $info['use-migrations'] === 'true') { 528 \OC::$server->getLogger()->debug('Running app database migrations'); 529 $ms = new MigrationService($app, \OC::$server->getDatabaseConnection()); 530 $ms->migrate(); 531 } else { 532 if (\is_file($appPath.'/appinfo/database.xml')) { 533 \OC::$server->getLogger()->debug('Create app database from schema file'); 534 OC_DB::createDbFromStructure($appPath . '/appinfo/database.xml'); 535 } 536 } 537 538 //run appinfo/install.php 539 \OC_App::registerAutoloading($app, $appPath); 540 541 \OC::$server->getLogger()->debug('Running app install script'); 542 self::includeAppScript("$appPath/appinfo/install.php"); 543 544 \OC_App::setupBackgroundJobs($info['background-jobs']); 545 546 \OC::$server->getLogger()->debug('Running app install repair steps'); 547 OC_App::executeRepairSteps($app, $info['repair-steps']['install']); 548 549 $config = \OC::$server->getConfig(); 550 551 $config->setAppValue($app, 'installed_version', OC_App::getAppVersion($app)); 552 553 //set remote/public handlers 554 foreach ($info['remote'] as $name=>$path) { 555 $config->setAppValue('core', 'remote_'.$name, $app.'/'.$path); 556 } 557 foreach ($info['public'] as $name=>$path) { 558 $config->setAppValue('core', 'public_'.$name, $app.'/'.$path); 559 } 560 561 OC_App::setAppTypes($info['id']); 562 563 return $info['id']; 564 } 565 566 /** 567 * @param $script 568 */ 569 private static function includeAppScript($script) { 570 if (\file_exists($script)) { 571 include $script; 572 } 573 } 574} 575