2011-05-15 10:31:30 -04:00
< ? php
2024-01-29 09:12:53 -05:00
declare ( strict_types = 1 );
2011-05-15 10:31:30 -04:00
/**
2024-05-23 03:26:56 -04:00
* SPDX - FileCopyrightText : 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX - FileCopyrightText : 2016 ownCloud , Inc .
* SPDX - License - Identifier : AGPL - 3.0 - only
2011-05-15 10:31:30 -04:00
*/
2016-04-28 09:15:34 -04:00
namespace OC ;
2016-11-09 03:10:32 -05:00
use Doctrine\DBAL\Exception\TableExistsException ;
2025-06-05 10:37:10 -04:00
use OC\App\AppStore\AppNotFoundException ;
2017-04-23 16:10:17 -04:00
use OC\App\AppStore\Bundles\Bundle ;
2016-10-27 11:41:15 -04:00
use OC\App\AppStore\Fetcher\AppFetcher ;
2020-11-16 13:41:22 -05:00
use OC\AppFramework\Bootstrap\Coordinator ;
2016-10-31 13:42:19 -04:00
use OC\Archive\TAR ;
2021-01-03 09:28:31 -05:00
use OC\DB\Connection ;
2020-06-30 15:47:31 -04:00
use OC\DB\MigrationService ;
2025-05-16 10:44:38 -04:00
use OC\Files\FilenameValidator ;
2025-07-10 11:17:17 -04:00
use OCP\App\AppPathNotFoundException ;
2023-05-31 15:13:58 -04:00
use OCP\App\IAppManager ;
2025-07-29 11:53:16 -04:00
use OCP\BackgroundJob\IJobList ;
2025-05-14 05:52:35 -04:00
use OCP\Files ;
2021-06-29 19:20:33 -04:00
use OCP\HintException ;
2016-10-27 11:41:15 -04:00
use OCP\Http\Client\IClientService ;
2017-04-23 16:10:17 -04:00
use OCP\IConfig ;
2016-10-27 11:41:15 -04:00
use OCP\ITempManager ;
2025-07-10 11:17:17 -04:00
use OCP\L10N\IFactory ;
2023-10-31 07:06:09 -04:00
use OCP\Migration\IOutput ;
2025-05-16 10:44:38 -04:00
use OCP\Server ;
2016-10-27 11:41:15 -04:00
use phpseclib\File\X509 ;
2021-04-16 08:26:43 -04:00
use Psr\Log\LoggerInterface ;
2015-02-09 16:48:27 -05:00
/**
2016-10-31 06:07:54 -04:00
* This class provides the functionality needed to install , update and remove apps
2015-02-09 16:48:27 -05:00
*/
2016-04-28 09:15:34 -04:00
class Installer {
2024-01-29 10:10:31 -05:00
private ? bool $isInstanceReadyForUpdates = null ;
private ? array $apps = null ;
2014-05-21 06:14:10 -04:00
2020-02-12 07:31:11 -05:00
public function __construct (
2024-01-29 10:10:31 -05:00
private AppFetcher $appFetcher ,
private IClientService $clientService ,
private ITempManager $tempManager ,
private LoggerInterface $logger ,
private IConfig $config ,
2025-07-10 10:04:46 -04:00
private IAppManager $appManager ,
2025-07-10 11:17:17 -04:00
private IFactory $l10nFactory ,
2024-01-29 10:10:31 -05:00
private bool $isCLI ,
2020-02-12 07:31:11 -05:00
) {
2016-10-31 06:07:54 -04:00
}
/**
* Installs an app that is located in one of the app folders already
2011-05-15 10:31:30 -04:00
*
2016-10-27 11:41:15 -04:00
* @ param string $appId App to install
2019-09-05 06:55:24 -04:00
* @ param bool $forceEnable
2014-02-08 05:47:55 -05:00
* @ throws \Exception
2017-05-15 01:03:35 -04:00
* @ return string app ID
2011-05-15 10:31:30 -04:00
*/
2019-09-05 06:55:24 -04:00
public function installApp ( string $appId , bool $forceEnable = false ) : string {
2025-07-10 11:17:17 -04:00
$appPath = $this -> appManager -> getAppPath ( $appId , true );
2020-06-30 15:47:31 -04:00
2025-07-10 11:17:17 -04:00
$l = $this -> l10nFactory -> get ( 'core' );
$info = $this -> appManager -> getAppInfoByPath ( $appPath . '/appinfo/info.xml' , $l -> getLanguageCode ());
2017-05-15 01:03:35 -04:00
2025-07-29 10:50:51 -04:00
if ( ! is_array ( $info ) || $info [ 'id' ] !== $appId ) {
2017-05-15 01:03:35 -04:00
throw new \Exception (
$l -> t ( 'App "%s" cannot be installed because appinfo file cannot be read.' ,
2018-02-22 08:43:56 -05:00
[ $appId ]
2017-05-15 01:03:35 -04:00
)
);
}
2019-03-06 13:59:15 -05:00
$ignoreMaxApps = $this -> config -> getSystemValue ( 'app_install_overwrite' , []);
2019-09-05 06:55:24 -04:00
$ignoreMax = $forceEnable || in_array ( $appId , $ignoreMaxApps , true );
2019-03-06 13:59:15 -05:00
2018-02-17 09:25:24 -05:00
$version = implode ( '.' , \OCP\Util :: getVersion ());
2025-07-29 10:50:51 -04:00
if ( ! $this -> appManager -> isAppCompatible ( $version , $info , $ignoreMax )) {
2017-05-15 01:03:35 -04:00
throw new \Exception (
$l -> t ( 'App "%s" cannot be installed because it is not compatible with this version of the server.' ,
[ $info [ 'name' ]]
)
);
}
// check for required dependencies
2019-03-06 13:59:15 -05:00
\OC_App :: checkAppDependencies ( $this -> config , $l , $info , $ignoreMax );
2025-07-29 10:50:51 -04:00
$coordinator = Server :: get ( Coordinator :: class );
2020-11-16 13:41:22 -05:00
$coordinator -> runLazyRegistration ( $appId );
2017-05-15 01:03:35 -04:00
2025-07-29 10:50:51 -04:00
return $this -> installAppLastSteps ( $appPath , $info , null , 'no' );
2014-05-21 06:14:10 -04:00
}
/**
2016-10-31 06:07:54 -04:00
* Updates the specified app from the appstore
2015-03-11 04:59:56 -04:00
*
2024-01-29 10:10:31 -05:00
* @ param bool $allowUnstable Allow unstable releases
2014-06-04 10:29:41 -04:00
*/
2024-01-29 10:10:31 -05:00
public function updateAppstoreApp ( string $appId , bool $allowUnstable = false ) : bool {
2025-07-29 11:29:30 -04:00
if ( $this -> isUpdateAvailable ( $appId , $allowUnstable ) !== false ) {
2016-10-31 06:07:54 -04:00
try {
2020-04-30 03:43:33 -04:00
$this -> downloadApp ( $appId , $allowUnstable );
2016-10-31 06:07:54 -04:00
} catch ( \Exception $e ) {
2021-04-16 08:26:43 -04:00
$this -> logger -> error ( $e -> getMessage (), [
'exception' => $e ,
2018-01-17 09:21:56 -05:00
]);
2016-10-31 06:07:54 -04:00
return false ;
}
2025-07-29 11:29:30 -04:00
return $this -> appManager -> upgradeApp ( $appId );
2014-06-04 10:29:41 -04:00
}
2016-10-31 06:07:54 -04:00
return false ;
2014-06-04 10:29:41 -04:00
}
2014-05-21 06:14:10 -04:00
2021-01-20 04:46:06 -05:00
/**
* Split the certificate file in individual certs
*
* @ param string $cert
* @ return string []
*/
private function splitCerts ( string $cert ) : array {
preg_match_all ( '([\-]{3,}[\S\ ]+?[\-]{3,}[\S\s]+?[\-]{3,}[\S\ ]+?[\-]{3,})' , $cert , $matches );
return $matches [ 0 ];
}
2025-07-10 03:39:58 -04:00
/**
* Get the path where to install apps
2025-07-10 05:31:59 -04:00
*
* @ throws \RuntimeException if an app folder is marked as writable but is missing permissions
2025-07-10 03:39:58 -04:00
*/
public function getInstallPath () : ? string {
foreach ( \OC :: $APPSROOTS as $dir ) {
if ( isset ( $dir [ 'writable' ]) && $dir [ 'writable' ] === true ) {
2025-07-10 05:31:59 -04:00
// 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.'
);
}
2025-07-10 03:39:58 -04:00
return $dir [ 'path' ];
}
}
return null ;
}
2014-05-21 06:14:10 -04:00
/**
2016-10-27 11:41:15 -04:00
* Downloads an app and puts it into the app directory
*
* @ param string $appId
2020-04-30 03:43:33 -04:00
* @ param bool [ $allowUnstable ]
2016-10-27 11:41:15 -04:00
*
2025-06-05 10:37:10 -04:00
* @ throws AppNotFoundException If the app is not found on the appstore
2016-10-28 06:36:44 -04:00
* @ throws \Exception If the installation was not successful
2014-05-21 06:14:10 -04:00
*/
2024-01-29 10:10:31 -05:00
public function downloadApp ( string $appId , bool $allowUnstable = false ) : void {
2016-10-27 11:41:15 -04:00
$appId = strtolower ( $appId );
2025-07-10 03:39:58 -04:00
$installPath = $this -> getInstallPath ();
if ( $installPath === null ) {
throw new \Exception ( 'No application directories are marked as writable.' );
}
2020-04-30 03:43:33 -04:00
$apps = $this -> appFetcher -> get ( $allowUnstable );
2016-10-27 11:41:15 -04:00
foreach ( $apps as $app ) {
if ( $app [ 'id' ] === $appId ) {
2016-10-28 06:46:02 -04:00
// Load the certificate
$certificate = new X509 ();
2021-01-20 04:46:06 -05:00
$rootCrt = file_get_contents ( __DIR__ . '/../../resources/codesigning/root.crt' );
$rootCrts = $this -> splitCerts ( $rootCrt );
foreach ( $rootCrts as $rootCrt ) {
$certificate -> loadCA ( $rootCrt );
}
2016-10-28 06:46:02 -04:00
$loadedCertificate = $certificate -> loadX509 ( $app [ 'certificate' ]);
// Verify if the certificate has been revoked
$crl = new X509 ();
2021-01-20 04:46:06 -05:00
foreach ( $rootCrts as $rootCrt ) {
$crl -> loadCA ( $rootCrt );
}
2016-10-28 06:46:02 -04:00
$crl -> loadCRL ( file_get_contents ( __DIR__ . '/../../resources/codesigning/root.crl' ));
if ( $crl -> validateSignature () !== true ) {
throw new \Exception ( 'Could not validate CRL signature' );
}
$csn = $loadedCertificate [ 'tbsCertificate' ][ 'serialNumber' ] -> toString ();
$revoked = $crl -> getRevoked ( $csn );
if ( $revoked !== false ) {
throw new \Exception (
sprintf (
'Certificate "%s" has been revoked' ,
$csn
)
);
}
2016-10-27 11:41:15 -04:00
// Verify if the certificate has been issued by the Nextcloud Code Authority CA
2016-10-28 06:46:02 -04:00
if ( $certificate -> validateSignature () !== true ) {
2016-10-28 06:36:44 -04:00
throw new \Exception (
2016-10-27 11:41:15 -04:00
sprintf (
'App with id %s has a certificate not issued by a trusted Code Signing Authority' ,
$appId
2016-10-31 14:39:35 -04:00
)
2016-10-27 11:41:15 -04:00
);
}
2012-08-04 15:25:03 -04:00
2016-10-27 11:41:15 -04:00
// Verify if the certificate is issued for the requested app id
$certInfo = openssl_x509_parse ( $app [ 'certificate' ]);
if ( ! isset ( $certInfo [ 'subject' ][ 'CN' ])) {
2016-10-28 06:36:44 -04:00
throw new \Exception (
2016-10-27 11:41:15 -04:00
sprintf (
'App with id %s has a cert with no CN' ,
$appId
2016-10-31 14:39:35 -04:00
)
2016-10-27 11:41:15 -04:00
);
}
if ( $certInfo [ 'subject' ][ 'CN' ] !== $appId ) {
2016-10-28 06:36:44 -04:00
throw new \Exception (
2016-10-27 11:41:15 -04:00
sprintf (
'App with id %s has a cert issued to %s' ,
$appId ,
$certInfo [ 'subject' ][ 'CN' ]
2016-10-31 14:39:35 -04:00
)
2016-10-27 11:41:15 -04:00
);
}
2012-08-04 15:25:03 -04:00
2016-10-27 11:41:15 -04:00
// Download the release
2016-10-31 06:07:54 -04:00
$tempFile = $this -> tempManager -> getTemporaryFile ( '.tar.gz' );
2025-05-16 10:44:38 -04:00
if ( $tempFile === false ) {
throw new \RuntimeException ( 'Could not create temporary file for downloading app archive.' );
}
2020-02-12 07:31:11 -05:00
$timeout = $this -> isCLI ? 0 : 120 ;
2016-10-31 06:07:54 -04:00
$client = $this -> clientService -> newClient ();
2021-01-12 04:10:08 -05:00
$client -> get ( $app [ 'releases' ][ 0 ][ 'download' ], [ 'sink' => $tempFile , 'timeout' => $timeout ]);
2016-10-27 11:41:15 -04:00
// Check if the signature actually matches the downloaded content
$certificate = openssl_get_publickey ( $app [ 'certificate' ]);
2024-05-15 04:11:31 -04:00
$verified = openssl_verify ( file_get_contents ( $tempFile ), base64_decode ( $app [ 'releases' ][ 0 ][ 'signature' ]), $certificate , OPENSSL_ALGO_SHA512 ) === 1 ;
2016-10-27 11:41:15 -04:00
if ( $verified === true ) {
// Seems to match, let's proceed
2016-10-31 06:07:54 -04:00
$extractDir = $this -> tempManager -> getTemporaryFolder ();
2025-05-16 10:44:38 -04:00
if ( $extractDir === false ) {
throw new \RuntimeException ( 'Could not create temporary directory for unpacking app.' );
}
2016-10-27 11:41:15 -04:00
2025-05-16 10:44:38 -04:00
$archive = new TAR ( $tempFile );
2022-01-06 10:57:32 -05:00
if ( ! $archive -> extract ( $extractDir )) {
$errorMessage = 'Could not extract app ' . $appId ;
2020-10-21 10:03:40 -04:00
2022-01-06 10:57:32 -05:00
$archiveError = $archive -> getError ();
if ( $archiveError instanceof \PEAR_Error ) {
$errorMessage .= ': ' . $archiveError -> getMessage ();
2016-10-31 14:39:35 -04:00
}
2016-10-27 11:41:15 -04:00
2022-01-06 10:57:32 -05:00
throw new \Exception ( $errorMessage );
}
$allFiles = scandir ( $extractDir );
$folders = array_diff ( $allFiles , [ '.' , '..' ]);
$folders = array_values ( $folders );
2012-08-04 15:25:03 -04:00
2024-04-22 10:55:42 -04:00
if ( count ( $folders ) < 1 ) {
throw new \Exception (
sprintf (
'Extracted app %s has no folders' ,
$appId
)
);
}
2022-01-06 10:57:32 -05:00
if ( count ( $folders ) > 1 ) {
throw new \Exception (
sprintf (
'Extracted app %s has more than 1 folder' ,
$appId
)
);
}
2016-11-11 12:53:26 -05:00
2022-01-06 10:57:32 -05:00
// Check if appinfo/info.xml has the same app ID as well
2024-04-22 10:55:42 -04:00
$xml = simplexml_load_string ( file_get_contents ( $extractDir . '/' . $folders [ 0 ] . '/appinfo/info.xml' ));
if ( $xml === false ) {
throw new \Exception (
sprintf (
'Failed to load info.xml for app id %s' ,
$appId ,
)
);
2022-01-06 10:57:32 -05:00
}
2024-04-22 10:55:42 -04:00
2022-01-06 10:57:32 -05:00
if (( string ) $xml -> id !== $appId ) {
2016-10-28 06:36:44 -04:00
throw new \Exception (
2016-10-27 11:41:15 -04:00
sprintf (
2022-01-06 10:57:32 -05:00
'App for id %s has a wrong app ID in info.xml: %s' ,
2016-10-27 11:41:15 -04:00
$appId ,
2022-01-06 10:57:32 -05:00
( string ) $xml -> id
2016-10-31 14:39:35 -04:00
)
2016-10-27 11:41:15 -04:00
);
}
2022-01-06 10:57:32 -05:00
// Check if the version is lower than before
2025-07-10 10:04:46 -04:00
$currentVersion = $this -> appManager -> getAppVersion ( $appId , true );
2022-01-06 10:57:32 -05:00
$newVersion = ( string ) $xml -> version ;
if ( version_compare ( $currentVersion , $newVersion ) === 1 ) {
throw new \Exception (
sprintf (
'App for id %s has version %s and tried to update to lower version %s' ,
$appId ,
$currentVersion ,
$newVersion
)
);
}
2025-07-10 03:39:58 -04:00
$baseDir = $installPath . '/' . $appId ;
2022-01-06 10:57:32 -05:00
// Remove old app with the ID if existent
2025-05-14 05:52:35 -04:00
Files :: rmdirr ( $baseDir );
2022-01-06 10:57:32 -05:00
// Move to app folder
if ( @ mkdir ( $baseDir )) {
$extractDir .= '/' . $folders [ 0 ];
}
2025-05-16 10:44:38 -04:00
// otherwise we just copy the outer directory
$this -> copyRecursive ( $extractDir , $baseDir );
2025-05-14 05:52:35 -04:00
Files :: rmdirr ( $extractDir );
2025-05-30 12:00:16 -04:00
if ( function_exists ( 'opcache_reset' )) {
opcache_reset ();
}
2022-01-06 10:57:32 -05:00
return ;
2016-10-27 11:41:15 -04:00
}
2022-01-06 10:57:32 -05:00
// Signature does not match
throw new \Exception (
sprintf (
'App with id %s has invalid signature' ,
$appId
)
);
2011-05-28 11:33:25 -04:00
}
}
2016-10-31 14:39:35 -04:00
2025-06-05 10:37:10 -04:00
throw new AppNotFoundException (
2016-10-31 14:39:35 -04:00
sprintf (
2025-06-05 10:37:10 -04:00
'Could not download app %s, it was not found on the appstore' ,
2016-10-31 14:39:35 -04:00
$appId
)
);
2014-05-21 06:14:10 -04:00
}
2013-01-31 04:30:13 -05:00
/**
2014-05-21 06:14:10 -04:00
* Check if an update for the app is available
2013-01-31 04:30:13 -05:00
*
2016-10-31 06:07:54 -04:00
* @ param string $appId
2020-04-30 03:43:33 -04:00
* @ param bool $allowUnstable
2016-10-31 06:07:54 -04:00
* @ return string | false false or the version number of the update
2013-01-31 04:30:13 -05:00
*/
2024-01-29 10:10:31 -05:00
public function isUpdateAvailable ( $appId , $allowUnstable = false ) : string | false {
2017-11-24 04:41:51 -05:00
if ( $this -> isInstanceReadyForUpdates === null ) {
2025-07-10 03:39:58 -04:00
$installPath = $this -> getInstallPath ();
2024-01-29 10:10:31 -05:00
if ( $installPath === null ) {
2017-11-24 04:41:51 -05:00
$this -> isInstanceReadyForUpdates = false ;
2014-06-10 12:38:21 -04:00
} else {
2017-11-24 04:41:51 -05:00
$this -> isInstanceReadyForUpdates = true ;
2014-06-10 12:38:21 -04:00
}
}
2017-11-24 04:41:51 -05:00
if ( $this -> isInstanceReadyForUpdates === false ) {
2014-06-10 12:38:21 -04:00
return false ;
}
2017-12-14 03:50:31 -05:00
if ( $this -> isInstalledFromGit ( $appId ) === true ) {
return false ;
}
2017-11-24 04:41:51 -05:00
if ( $this -> apps === null ) {
2020-04-30 03:43:33 -04:00
$this -> apps = $this -> appFetcher -> get ( $allowUnstable );
2017-11-23 09:04:51 -05:00
}
2017-12-14 03:50:17 -05:00
foreach ( $this -> apps as $app ) {
2016-10-31 06:07:54 -04:00
if ( $app [ 'id' ] === $appId ) {
2025-07-10 10:04:46 -04:00
$currentVersion = $this -> appManager -> getAppVersion ( $appId , true );
2019-03-22 11:54:35 -04:00
if ( ! isset ( $app [ 'releases' ][ 0 ][ 'version' ])) {
return false ;
}
2016-10-31 06:07:54 -04:00
$newestVersion = $app [ 'releases' ][ 0 ][ 'version' ];
2018-08-02 08:27:16 -04:00
if ( $currentVersion !== '0' && version_compare ( $newestVersion , $currentVersion , '>' )) {
2016-10-31 06:07:54 -04:00
return $newestVersion ;
} else {
return false ;
}
2013-01-30 06:08:14 -05:00
}
2013-01-21 14:40:23 -05:00
}
2016-10-31 06:07:54 -04:00
return false ;
2013-01-31 04:30:13 -05:00
}
2013-01-21 14:40:23 -05:00
2017-12-14 03:50:31 -05:00
/**
* Check if app has been installed from git
*
* The function will check if the path contains a . git folder
*/
2024-01-29 10:10:31 -05:00
private function isInstalledFromGit ( string $appId ) : bool {
2025-07-10 11:17:17 -04:00
try {
$appPath = $this -> appManager -> getAppPath ( $appId );
return file_exists ( $appPath . '/.git/' );
} catch ( AppPathNotFoundException ) {
2017-12-14 03:50:31 -05:00
return false ;
}
}
2013-01-31 04:30:13 -05:00
/**
2014-05-19 11:50:53 -04:00
* Check if app is already downloaded
2013-01-31 04:30:13 -05:00
*
* The function will check if the app is already downloaded in the apps repository
*/
2024-01-29 10:10:31 -05:00
public function isDownloaded ( string $name ) : bool {
2016-04-28 09:15:34 -04:00
foreach ( \OC :: $APPSROOTS as $dir ) {
2020-10-05 09:12:57 -04:00
$dirToTest = $dir [ 'path' ];
2014-05-31 11:50:39 -04:00
$dirToTest .= '/' ;
$dirToTest .= $name ;
$dirToTest .= '/' ;
if ( is_dir ( $dirToTest )) {
return true ;
}
2013-01-21 14:40:23 -05:00
}
2014-05-31 11:50:39 -04:00
return false ;
2013-01-31 04:30:13 -05:00
}
2013-01-21 14:40:23 -05:00
2011-05-15 10:31:30 -04:00
/**
2014-05-19 11:50:53 -04:00
* Removes an app
2011-05-15 10:31:30 -04:00
*
* This function works as follows
2016-04-28 04:07:29 -04:00
* - # call uninstall repair steps
2011-05-15 10:31:30 -04:00
* - # removing the files
*
* The function will not delete preferences , tables and the configuration ,
* this has to be done by the function oc_app_uninstall () .
*/
2024-01-29 10:10:31 -05:00
public function removeApp ( string $appId ) : bool {
2020-04-09 10:07:47 -04:00
if ( $this -> isDownloaded ( $appId )) {
2025-07-10 10:04:46 -04:00
if ( $this -> appManager -> isShipped ( $appId )) {
2018-01-29 07:41:00 -05:00
return false ;
}
2025-07-10 03:39:58 -04:00
$installPath = $this -> getInstallPath ();
if ( $installPath === null ) {
$this -> logger -> error ( 'No application directories are marked as writable.' , [ 'app' => 'core' ]);
return false ;
}
$appDir = $installPath . '/' . $appId ;
2025-05-14 05:52:35 -04:00
Files :: rmdirr ( $appDir );
2014-05-31 11:50:39 -04:00
return true ;
2013-01-21 14:40:23 -05:00
} else {
2022-03-30 04:55:41 -04:00
$this -> logger -> error ( 'can\'t remove app ' . $appId . '. It is not installed.' );
2013-01-21 14:40:23 -05:00
2014-05-31 11:50:39 -04:00
return false ;
2013-01-21 14:40:23 -05:00
}
2011-05-15 10:31:30 -04:00
}
2011-06-21 16:16:41 -04:00
2017-04-23 16:10:17 -04:00
/**
* Installs the app within the bundle and marks the bundle as installed
*
* @ throws \Exception If app could not get installed
*/
2024-01-29 10:10:31 -05:00
public function installAppBundle ( Bundle $bundle ) : void {
2017-04-23 16:10:17 -04:00
$appIds = $bundle -> getAppIdentifiers ();
foreach ( $appIds as $appId ) {
if ( ! $this -> isDownloaded ( $appId )) {
$this -> downloadApp ( $appId );
}
$this -> installApp ( $appId );
2025-07-29 11:29:30 -04:00
$this -> appManager -> enableApp ( $appId );
2017-04-23 16:10:17 -04:00
}
$bundles = json_decode ( $this -> config -> getAppValue ( 'core' , 'installed.bundles' , json_encode ([])), true );
$bundles [] = $bundle -> getIdentifier ();
$this -> config -> setAppValue ( 'core' , 'installed.bundles' , json_encode ( $bundles ));
}
2011-06-21 16:16:41 -04:00
/**
2014-05-19 11:50:53 -04:00
* Installs shipped apps
2011-06-21 16:16:41 -04:00
*
2012-04-29 08:38:56 -04:00
* This function installs all apps found in the 'apps' directory that should be enabled by default ;
2016-03-21 11:31:59 -04:00
* @ param bool $softErrors When updating we ignore errors and simply log them , better to have a
* working ownCloud at the end instead of an aborted update .
* @ return array Array of error messages ( appid => Exception )
2011-06-21 16:16:41 -04:00
*/
2025-07-10 11:17:17 -04:00
public function installShippedApps ( bool $softErrors = false , ? IOutput $output = null ) : array {
2023-10-31 07:06:09 -04:00
if ( $output instanceof IOutput ) {
$output -> debug ( 'Installing shipped apps' );
}
2016-03-21 11:31:59 -04:00
$errors = [];
2016-04-28 09:15:34 -04:00
foreach ( \OC :: $APPSROOTS as $app_dir ) {
2020-04-09 10:07:47 -04:00
if ( $dir = opendir ( $app_dir [ 'path' ])) {
while ( false !== ( $filename = readdir ( $dir ))) {
if ( $filename [ 0 ] !== '.' && is_dir ( $app_dir [ 'path' ] . " / $filename " )) {
if ( file_exists ( $app_dir [ 'path' ] . " / $filename /appinfo/info.xml " )) {
2025-07-10 11:17:17 -04:00
if ( $this -> config -> getAppValue ( $filename , 'installed_version' ) === '' ) {
$enabled = $this -> appManager -> isDefaultEnabled ( $filename );
if (( $enabled || in_array ( $filename , $this -> appManager -> getAlwaysEnabledApps ()))
&& $this -> config -> getAppValue ( $filename , 'enabled' ) !== 'no' ) {
2016-03-21 11:31:59 -04:00
if ( $softErrors ) {
try {
2025-07-10 11:17:17 -04:00
$this -> installShippedApp ( $filename , $output );
2016-11-09 04:29:25 -05:00
} catch ( HintException $e ) {
if ( $e -> getPrevious () instanceof TableExistsException ) {
$errors [ $filename ] = $e ;
continue ;
}
throw $e ;
2016-03-21 11:31:59 -04:00
}
} else {
2025-07-10 11:17:17 -04:00
$this -> installShippedApp ( $filename , $output );
2016-03-21 11:31:59 -04:00
}
2025-07-10 11:17:17 -04:00
$this -> config -> setAppValue ( $filename , 'enabled' , 'yes' );
2012-06-23 10:17:59 -04:00
}
2012-06-04 16:37:00 -04:00
}
2011-06-21 16:16:41 -04:00
}
}
}
2020-04-09 10:07:47 -04:00
closedir ( $dir );
2011-06-21 16:16:41 -04:00
}
}
2016-03-21 11:31:59 -04:00
return $errors ;
2011-06-21 16:16:41 -04:00
}
2011-08-22 08:17:38 -04:00
2025-07-29 10:50:51 -04:00
private function installAppLastSteps ( string $appPath , array $info , ? IOutput $output = null , string $enabled = 'no' ) : string {
\OC_App :: registerAutoloading ( $info [ 'id' ], $appPath );
2024-03-06 09:58:19 -05:00
2025-07-29 10:50:51 -04:00
$previousVersion = $this -> config -> getAppValue ( $info [ 'id' ], 'installed_version' , '' );
$ms = new MigrationService ( $info [ 'id' ], Server :: get ( Connection :: class ));
2023-10-31 07:06:09 -04:00
if ( $output instanceof IOutput ) {
$ms -> setOutput ( $output );
}
2025-07-29 10:50:51 -04:00
if ( $previousVersion !== '' ) {
\OC_App :: executeRepairSteps ( $info [ 'id' ], $info [ 'repair-steps' ][ 'pre-migration' ]);
}
2011-08-22 08:17:38 -04:00
2025-07-29 10:50:51 -04:00
$ms -> migrate ( 'latest' , $previousVersion === '' );
2016-01-14 11:26:30 -05:00
2025-07-29 10:50:51 -04:00
if ( $previousVersion !== '' ) {
\OC_App :: executeRepairSteps ( $info [ 'id' ], $info [ 'repair-steps' ][ 'post-migration' ]);
2014-01-15 11:11:29 -05:00
}
2025-07-29 10:50:51 -04:00
2023-10-31 07:06:09 -04:00
if ( $output instanceof IOutput ) {
2025-07-29 10:50:51 -04:00
$output -> debug ( 'Registering tasks of ' . $info [ 'id' ]);
2023-10-31 07:06:09 -04:00
}
2025-07-29 10:50:51 -04:00
2025-07-29 11:53:16 -04:00
// Setup background jobs
$queue = Server :: get ( IJobList :: class );
foreach ( $info [ 'background-jobs' ] as $job ) {
$queue -> add ( $job );
}
2015-12-02 09:56:59 -05:00
2025-08-18 11:25:07 -04:00
// Run deprecated appinfo/install.php if any
$appInstallScriptPath = $appPath . '/appinfo/install.php' ;
if ( file_exists ( $appInstallScriptPath )) {
$this -> logger -> warning ( 'Using an appinfo/install.php file is deprecated. Application "{app}" still uses one.' , [
'app' => $info [ 'id' ],
]);
self :: includeAppScript ( $appInstallScriptPath );
}
2025-07-29 10:50:51 -04:00
\OC_App :: executeRepairSteps ( $info [ 'id' ], $info [ 'repair-steps' ][ 'install' ]);
2016-04-28 04:07:29 -04:00
2025-07-29 10:50:51 -04:00
// Set the installed version
$this -> config -> setAppValue ( $info [ 'id' ], 'installed_version' , $this -> appManager -> getAppVersion ( $info [ 'id' ], false ));
$this -> config -> setAppValue ( $info [ 'id' ], 'enabled' , $enabled );
2012-08-04 15:25:03 -04:00
2025-07-29 10:50:51 -04:00
// Set remote/public handlers
2020-10-05 09:12:57 -04:00
foreach ( $info [ 'remote' ] as $name => $path ) {
2025-07-29 10:50:51 -04:00
$this -> config -> setAppValue ( 'core' , 'remote_' . $name , $info [ 'id' ] . '/' . $path );
2012-05-11 14:58:23 -04:00
}
2020-10-05 09:12:57 -04:00
foreach ( $info [ 'public' ] as $name => $path ) {
2025-07-29 10:50:51 -04:00
$this -> config -> setAppValue ( 'core' , 'public_' . $name , $info [ 'id' ] . '/' . $path );
2012-05-11 14:58:23 -04:00
}
2012-08-04 15:25:03 -04:00
2025-07-10 11:17:17 -04:00
\OC_App :: setAppTypes ( $info [ 'id' ]);
2012-08-04 15:25:03 -04:00
2012-08-04 19:40:19 -04:00
return $info [ 'id' ];
2011-08-22 08:17:38 -04:00
}
2012-04-21 16:47:56 -04:00
2025-07-29 10:50:51 -04:00
/**
* install an app already placed in the app folder
*/
public function installShippedApp ( string $app , ? IOutput $output = null ) : string | false {
if ( $output instanceof IOutput ) {
$output -> debug ( 'Installing ' . $app );
}
$info = $this -> appManager -> getAppInfo ( $app );
if ( is_null ( $info ) || $info [ 'id' ] !== $app ) {
return false ;
}
$appPath = $this -> appManager -> getAppPath ( $app );
return $this -> installAppLastSteps ( $appPath , $info , $output , 'yes' );
}
2024-01-29 10:10:31 -05:00
private static function includeAppScript ( string $script ) : void {
2020-04-09 10:07:47 -04:00
if ( file_exists ( $script )) {
2016-01-14 11:26:30 -05:00
include $script ;
}
}
2025-05-16 10:44:38 -04:00
/**
* 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 );
}
}
}
2011-05-15 10:31:30 -04:00
}