From 32cf661215fb3926789054a3953b465fc2665330 Mon Sep 17 00:00:00 2001 From: Lukas Reschke Date: Thu, 27 Oct 2016 17:41:15 +0200 Subject: [PATCH] Use new appstore API This change introduces the new appstore API in Nextcloud. Signed-off-by: Lukas Reschke --- .../lib/Controller/AppsController.php | 9 +- .../tests/Controller/AppsControllerTest.php | 20 +- config/config.sample.php | 14 - lib/composer/composer/autoload_classmap.php | 6 +- lib/composer/composer/autoload_static.php | 6 +- .../App/AppStore/Fetcher/AppFetcher.php | 52 + .../App/AppStore/Fetcher/CategoryFetcher.php | 45 + lib/private/App/AppStore/Fetcher/Fetcher.php | 92 ++ lib/private/App/AppStore/Version/Version.php | 52 + .../App/AppStore/Version/VersionParser.php | 64 + lib/private/Installer.php | 230 ++-- lib/private/OCSClient.php | 351 ----- lib/private/Server.php | 7 - lib/private/legacy/app.php | 242 +--- settings/Application.php | 23 + settings/Controller/AppSettingsController.php | 360 +++--- settings/ajax/enableapp.php | 6 +- settings/ajax/installapp.php | 11 +- settings/css/settings.css | 11 - settings/js/apps.js | 6 +- settings/routes.php | 1 - settings/templates/apps.php | 19 - .../Controller/AppSettingsControllerTest.php | 231 ++-- .../App/AppStore/Fetcher/AppFetcherTest.php | 39 + .../AppStore/Fetcher/CategoryFetcherTest.php | 38 + .../lib/App/AppStore/Fetcher/FetcherBase.php | 246 ++++ .../AppStore/Version/VersionParserTest.php | 84 ++ .../lib/App/AppStore/Version/VersionTest.php | 37 + tests/lib/OCSClientTest.php | 1132 ----------------- tests/lib/ServerTest.php | 2 - 30 files changed, 1243 insertions(+), 2193 deletions(-) create mode 100644 lib/private/App/AppStore/Fetcher/AppFetcher.php create mode 100644 lib/private/App/AppStore/Fetcher/CategoryFetcher.php create mode 100644 lib/private/App/AppStore/Fetcher/Fetcher.php create mode 100644 lib/private/App/AppStore/Version/Version.php create mode 100644 lib/private/App/AppStore/Version/VersionParser.php delete mode 100644 lib/private/OCSClient.php create mode 100644 tests/lib/App/AppStore/Fetcher/AppFetcherTest.php create mode 100644 tests/lib/App/AppStore/Fetcher/CategoryFetcherTest.php create mode 100644 tests/lib/App/AppStore/Fetcher/FetcherBase.php create mode 100644 tests/lib/App/AppStore/Version/VersionParserTest.php create mode 100644 tests/lib/App/AppStore/Version/VersionTest.php delete mode 100644 tests/lib/OCSClientTest.php diff --git a/apps/provisioning_api/lib/Controller/AppsController.php b/apps/provisioning_api/lib/Controller/AppsController.php index 3821fc343ad..7d11d92b55a 100644 --- a/apps/provisioning_api/lib/Controller/AppsController.php +++ b/apps/provisioning_api/lib/Controller/AppsController.php @@ -37,25 +37,20 @@ use OCP\IRequest; class AppsController extends OCSController { /** @var \OCP\App\IAppManager */ private $appManager; - /** @var OCSClient */ - private $ocsClient; /** * @param string $appName * @param IRequest $request * @param IAppManager $appManager - * @param OCSClient $ocsClient */ public function __construct( $appName, IRequest $request, - IAppManager $appManager, - OCSClient $ocsClient + IAppManager $appManager ) { parent::__construct($appName, $request); $this->appManager = $appManager; - $this->ocsClient = $ocsClient; } /** @@ -64,7 +59,7 @@ class AppsController extends OCSController { * @throws OCSException */ public function getApps($filter = null) { - $apps = OC_App::listAllApps(false, true, $this->ocsClient); + $apps = (new OC_App())->listAllApps(); $list = []; foreach($apps as $app) { $list[] = $app['id']; diff --git a/apps/provisioning_api/tests/Controller/AppsControllerTest.php b/apps/provisioning_api/tests/Controller/AppsControllerTest.php index 9ac4a8290e4..c891433258f 100644 --- a/apps/provisioning_api/tests/Controller/AppsControllerTest.php +++ b/apps/provisioning_api/tests/Controller/AppsControllerTest.php @@ -48,8 +48,6 @@ class AppsControllerTest extends \OCA\Provisioning_API\Tests\TestCase { private $api; /** @var IUserSession */ private $userSession; - /** @var OCSClient|\PHPUnit_Framework_MockObject_MockObject */ - private $ocsClient; protected function setUp() { parent::setUp(); @@ -57,9 +55,6 @@ class AppsControllerTest extends \OCA\Provisioning_API\Tests\TestCase { $this->appManager = \OC::$server->getAppManager(); $this->groupManager = \OC::$server->getGroupManager(); $this->userSession = \OC::$server->getUserSession(); - $this->ocsClient = $this->getMockBuilder('OC\OCSClient') - ->disableOriginalConstructor() - ->getMock(); $request = $this->getMockBuilder('OCP\IRequest') ->disableOriginalConstructor() @@ -68,8 +63,7 @@ class AppsControllerTest extends \OCA\Provisioning_API\Tests\TestCase { $this->api = new AppsController( 'provisioning_api', $request, - $this->appManager, - $this->ocsClient + $this->appManager ); } @@ -88,10 +82,6 @@ class AppsControllerTest extends \OCA\Provisioning_API\Tests\TestCase { } public function testGetApps() { - $this->ocsClient - ->expects($this->any()) - ->method($this->anything()) - ->will($this->returnValue(null)); $user = $this->generateUsers(); $this->groupManager->get('admin')->addUser($user); $this->userSession->setUser($user); @@ -99,7 +89,7 @@ class AppsControllerTest extends \OCA\Provisioning_API\Tests\TestCase { $result = $this->api->getApps(); $data = $result->getData(); - $this->assertEquals(count(\OC_App::listAllApps(false, true, $this->ocsClient)), count($data['apps'])); + $this->assertEquals(count((new \OC_App())->listAllApps()), count($data['apps'])); } public function testGetAppsEnabled() { @@ -109,13 +99,9 @@ class AppsControllerTest extends \OCA\Provisioning_API\Tests\TestCase { } public function testGetAppsDisabled() { - $this->ocsClient - ->expects($this->any()) - ->method($this->anything()) - ->will($this->returnValue(null)); $result = $this->api->getApps('disabled'); $data = $result->getData(); - $apps = \OC_App::listAllApps(false, true, $this->ocsClient); + $apps = (new \OC_App)->listAllApps(); $list = array(); foreach($apps as $app) { $list[] = $app['id']; diff --git a/config/config.sample.php b/config/config.sample.php index 7f4b3345642..fc52edbc778 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -673,20 +673,6 @@ $CONFIG = array( */ 'appstoreenabled' => true, -/** - * The URL of the appstore to use. - */ -'appstoreurl' => 'https://api.owncloud.com/v1', - -/** - * Whether to show experimental apps in the appstore interface - * - * Experimental apps are not checked for security issues and are new or known - * to be unstable and under heavy development. Installing these can cause data - * loss or security breaches. - */ -'appstore.experimental.enabled' => false, - /** * Use the ``apps_paths`` parameter to set the location of the Apps directory, * which should be scanned for available apps, and where user-specific apps diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 532a6f39848..ddd531868d4 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -279,6 +279,11 @@ return array( 'OC\\AppFramework\\Utility\\TimeFactory' => $baseDir . '/lib/private/AppFramework/Utility/TimeFactory.php', 'OC\\AppHelper' => $baseDir . '/lib/private/AppHelper.php', 'OC\\App\\AppManager' => $baseDir . '/lib/private/App/AppManager.php', + 'OC\\App\\AppStore\\Fetcher\\AppFetcher' => $baseDir . '/lib/private/App/AppStore/Fetcher/AppFetcher.php', + 'OC\\App\\AppStore\\Fetcher\\CategoryFetcher' => $baseDir . '/lib/private/App/AppStore/Fetcher/CategoryFetcher.php', + 'OC\\App\\AppStore\\Fetcher\\Fetcher' => $baseDir . '/lib/private/App/AppStore/Fetcher/Fetcher.php', + 'OC\\App\\AppStore\\Version\\Version' => $baseDir . '/lib/private/App/AppStore/Version/Version.php', + 'OC\\App\\AppStore\\Version\\VersionParser' => $baseDir . '/lib/private/App/AppStore/Version/VersionParser.php', 'OC\\App\\CodeChecker\\AbstractCheck' => $baseDir . '/lib/private/App/CodeChecker/AbstractCheck.php', 'OC\\App\\CodeChecker\\CodeChecker' => $baseDir . '/lib/private/App/CodeChecker/CodeChecker.php', 'OC\\App\\CodeChecker\\DeprecationCheck' => $baseDir . '/lib/private/App/CodeChecker/DeprecationCheck.php', @@ -602,7 +607,6 @@ return array( 'OC\\Notification\\Action' => $baseDir . '/lib/private/Notification/Action.php', 'OC\\Notification\\Manager' => $baseDir . '/lib/private/Notification/Manager.php', 'OC\\Notification\\Notification' => $baseDir . '/lib/private/Notification/Notification.php', - 'OC\\OCSClient' => $baseDir . '/lib/private/OCSClient.php', 'OC\\OCS\\CoreCapabilities' => $baseDir . '/lib/private/OCS/CoreCapabilities.php', 'OC\\OCS\\Exception' => $baseDir . '/lib/private/OCS/Exception.php', 'OC\\OCS\\Person' => $baseDir . '/lib/private/OCS/Person.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index c0a3e9b50c6..99a3c3d540e 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -309,6 +309,11 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\AppFramework\\Utility\\TimeFactory' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Utility/TimeFactory.php', 'OC\\AppHelper' => __DIR__ . '/../../..' . '/lib/private/AppHelper.php', 'OC\\App\\AppManager' => __DIR__ . '/../../..' . '/lib/private/App/AppManager.php', + 'OC\\App\\AppStore\\Fetcher\\AppFetcher' => __DIR__ . '/../../..' . '/lib/private/App/AppStore/Fetcher/AppFetcher.php', + 'OC\\App\\AppStore\\Fetcher\\CategoryFetcher' => __DIR__ . '/../../..' . '/lib/private/App/AppStore/Fetcher/CategoryFetcher.php', + 'OC\\App\\AppStore\\Fetcher\\Fetcher' => __DIR__ . '/../../..' . '/lib/private/App/AppStore/Fetcher/Fetcher.php', + 'OC\\App\\AppStore\\Version\\Version' => __DIR__ . '/../../..' . '/lib/private/App/AppStore/Version/Version.php', + 'OC\\App\\AppStore\\Version\\VersionParser' => __DIR__ . '/../../..' . '/lib/private/App/AppStore/Version/VersionParser.php', 'OC\\App\\CodeChecker\\AbstractCheck' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/AbstractCheck.php', 'OC\\App\\CodeChecker\\CodeChecker' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/CodeChecker.php', 'OC\\App\\CodeChecker\\DeprecationCheck' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/DeprecationCheck.php', @@ -632,7 +637,6 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Notification\\Action' => __DIR__ . '/../../..' . '/lib/private/Notification/Action.php', 'OC\\Notification\\Manager' => __DIR__ . '/../../..' . '/lib/private/Notification/Manager.php', 'OC\\Notification\\Notification' => __DIR__ . '/../../..' . '/lib/private/Notification/Notification.php', - 'OC\\OCSClient' => __DIR__ . '/../../..' . '/lib/private/OCSClient.php', 'OC\\OCS\\CoreCapabilities' => __DIR__ . '/../../..' . '/lib/private/OCS/CoreCapabilities.php', 'OC\\OCS\\Exception' => __DIR__ . '/../../..' . '/lib/private/OCS/Exception.php', 'OC\\OCS\\Person' => __DIR__ . '/../../..' . '/lib/private/OCS/Person.php', diff --git a/lib/private/App/AppStore/Fetcher/AppFetcher.php b/lib/private/App/AppStore/Fetcher/AppFetcher.php new file mode 100644 index 00000000000..2a39b047a5a --- /dev/null +++ b/lib/private/App/AppStore/Fetcher/AppFetcher.php @@ -0,0 +1,52 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OC\App\AppStore\Fetcher; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Files\IAppData; +use OCP\Http\Client\IClientService; +use OCP\IConfig; + +class AppFetcher extends Fetcher { + /** + * @param IAppData $appData + * @param IClientService $clientService + * @param ITimeFactory $timeFactory + * @param IConfig $config; + */ + public function __construct(IAppData $appData, + IClientService $clientService, + ITimeFactory $timeFactory, + IConfig $config) { + parent::__construct( + $appData, + $clientService, + $timeFactory + ); + + $this->fileName = 'apps.json'; + $this->endpointUrl = sprintf( + 'https://apps.nextcloud.com/api/v1/platform/%s/apps.json', + substr(implode(\OC_Util::getVersion(), '.'), 0, 5) + ); + } +} diff --git a/lib/private/App/AppStore/Fetcher/CategoryFetcher.php b/lib/private/App/AppStore/Fetcher/CategoryFetcher.php new file mode 100644 index 00000000000..74201ec3737 --- /dev/null +++ b/lib/private/App/AppStore/Fetcher/CategoryFetcher.php @@ -0,0 +1,45 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OC\App\AppStore\Fetcher; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Files\IAppData; +use OCP\Http\Client\IClientService; + +class CategoryFetcher extends Fetcher { + /** + * @param IAppData $appData + * @param IClientService $clientService + * @param ITimeFactory $timeFactory + */ + public function __construct(IAppData $appData, + IClientService $clientService, + ITimeFactory $timeFactory) { + parent::__construct( + $appData, + $clientService, + $timeFactory + ); + $this->fileName = 'categories.json'; + $this->endpointUrl = 'https://apps.nextcloud.com/api/v1/categories.json'; + } +} diff --git a/lib/private/App/AppStore/Fetcher/Fetcher.php b/lib/private/App/AppStore/Fetcher/Fetcher.php new file mode 100644 index 00000000000..cffff9176e2 --- /dev/null +++ b/lib/private/App/AppStore/Fetcher/Fetcher.php @@ -0,0 +1,92 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OC\App\AppStore\Fetcher; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\Http\Client\IClientService; + +abstract class Fetcher { + const INVALIDATE_AFTER_SECONDS = 300; + + /** @var IAppData */ + private $appData; + /** @var IClientService */ + private $clientService; + /** @var ITimeFactory */ + private $timeFactory; + /** @var string */ + protected $fileName; + /** @var string */ + protected $endpointUrl; + + /** + * @param IAppData $appData + * @param IClientService $clientService + * @param ITimeFactory $timeFactory + */ + public function __construct(IAppData $appData, + IClientService $clientService, + ITimeFactory $timeFactory) { + $this->appData = $appData; + $this->clientService = $clientService; + $this->timeFactory = $timeFactory; + } + + /** + * Returns the array with the categories on the appstore server + * + * @return array + */ + public function get() { + $rootFolder = $this->appData->getFolder('/'); + + try { + // File does already exists + $file = $rootFolder->getFile($this->fileName); + $jsonBlob = json_decode($file->getContent(), true); + if(is_array($jsonBlob)) { + // If the timestamp is older than 300 seconds request the files new + if((int)$jsonBlob['timestamp'] > ($this->timeFactory->getTime() - self::INVALIDATE_AFTER_SECONDS)) { + return $jsonBlob['data']; + } + } + } catch (NotFoundException $e) { + // File does not already exists + $file = $rootFolder->newFile($this->fileName); + } + + // Refresh the file content + $client = $this->clientService->newClient(); + try { + $response = $client->get($this->endpointUrl); + $responseJson = []; + $responseJson['data'] = json_decode($response->getBody(), true); + $responseJson['timestamp'] = $this->timeFactory->getTime(); + $file->putContent(json_encode($responseJson)); + return json_decode($file->getContent(), true)['data']; + } catch (\Exception $e) { + return []; + } + } +} diff --git a/lib/private/App/AppStore/Version/Version.php b/lib/private/App/AppStore/Version/Version.php new file mode 100644 index 00000000000..ca182ae078b --- /dev/null +++ b/lib/private/App/AppStore/Version/Version.php @@ -0,0 +1,52 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OC\App\AppStore\Version; + +class Version { + /** @var string */ + private $minVersion; + /** @var string */ + private $maxVersion; + + /** + * @param string $minVersion + * @param string $maxVersion + */ + public function __construct($minVersion, $maxVersion) { + $this->minVersion = $minVersion; + $this->maxVersion = $maxVersion; + } + + /** + * @return string + */ + public function getMinimumVersion() { + return $this->minVersion; + } + + /** + * @return string + */ + public function getMaximumVersion() { + return $this->maxVersion; + } +} diff --git a/lib/private/App/AppStore/Version/VersionParser.php b/lib/private/App/AppStore/Version/VersionParser.php new file mode 100644 index 00000000000..1058a8bc0fa --- /dev/null +++ b/lib/private/App/AppStore/Version/VersionParser.php @@ -0,0 +1,64 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OC\App\AppStore\Version; + +/** + * Class VersionParser parses the versions as sent by the Nextcloud app store + * + * @package OC\App\AppStore + */ +class VersionParser { + /** + * Returns the version for a version string + * + * @param string $versionSpec + * @return Version + * @throws \Exception If the version cannot be parsed + */ + public function getVersion($versionSpec) { + // * indicates that the version is compatible with all versions + if($versionSpec === '*') { + return new Version('', ''); + } + + // Count the amount of =, if it is one then it's either maximum or minimum + // version. If it is two then it is maximum and minimum. + if (preg_match_all('/(?:>|<)(?:=|)[0-9.]+/', $versionSpec, $matches)) { + switch(count($matches[0])) { + case 1: + if(substr($matches[0][0], 0, 1) === '>') { + return new Version(substr($matches[0][0], 2), ''); + } else { + return new Version('', substr($matches[0][0], 2)); + } + break; + case 2: + return new Version(substr($matches[0][0], 2), substr($matches[0][1], 2)); + break; + default: + throw new \Exception('Version cannot be parsed'); + } + } + + throw new \Exception('Version cannot be parsed'); + } +} diff --git a/lib/private/Installer.php b/lib/private/Installer.php index 009df790585..02dbd670a0a 100644 --- a/lib/private/Installer.php +++ b/lib/private/Installer.php @@ -40,12 +40,18 @@ namespace OC; +use OC\App\AppStore\Fetcher\AppFetcher; use OC\App\CodeChecker\CodeChecker; use OC\App\CodeChecker\EmptyCheck; use OC\App\CodeChecker\PrivateCheck; +use OC\Archive\Archive; use OC_App; use OC_DB; use OC_Helper; +use OCP\Http\Client\IClientService; +use OCP\ILogger; +use OCP\ITempManager; +use phpseclib\File\X509; /** * This class provides the functionality needed to install, update and remove plugins/apps @@ -81,49 +87,13 @@ class Installer { * needed to get the app working. * * Installs an app - * @param array $data with all information + * @param string $appId App to install * @throws \Exception * @return integer */ - public static function installApp( $data = array()) { - $l = \OC::$server->getL10N('lib'); - - list($extractDir, $path) = self::downloadApp($data); - - $info = self::checkAppsIntegrity($data, $extractDir, $path); - $appId = OC_App::cleanAppId($info['id']); + public function installApp($appId) { $basedir = OC_App::getInstallPath().'/'.$appId; - //check if the destination directory already exists - if(is_dir($basedir)) { - OC_Helper::rmdirr($extractDir); - if($data['source']=='http') { - unlink($path); - } - throw new \Exception($l->t("App directory already exists")); - } - - if(!empty($data['pretent'])) { - return false; - } - - //copy the app to the correct place - if(@!mkdir($basedir)) { - OC_Helper::rmdirr($extractDir); - if($data['source']=='http') { - unlink($path); - } - throw new \Exception($l->t("Can't create app folder. Please fix permissions. %s", array($basedir))); - } - - $extractDir .= '/' . $info['id']; - if(!file_exists($extractDir)) { - OC_Helper::rmdirr($basedir); - throw new \Exception($l->t("Archive does not contain a directory named %s", $info['id'])); - } - OC_Helper::copyr($extractDir, $basedir); - - //remove temporary files - OC_Helper::rmdirr($extractDir); + $info = OC_App::getAppInfo($basedir.'/appinfo/info.xml', true); //install the database if(is_file($basedir.'/appinfo/database.xml')) { @@ -168,7 +138,7 @@ class Installer { * * Checks whether or not an app is installed, i.e. registered in apps table. */ - public static function isInstalled( $app ) { + public static function isInstalled( $app ) { return (\OC::$server->getConfig()->getAppValue($app, "installed_version", null) !== null); } @@ -265,58 +235,148 @@ class Installer { } /** - * @param array $data - * @return array + * Downloads an app and puts it into the app directory + * + * @param string $appId + * @param AppFetcher $appFetcher + * @param IClientService $clientService + * @param ITempManager $tempManager + * @param ILogger $logger + * + * @return bool Whether the installation was successful or not * @throws \Exception */ - public static function downloadApp($data = array()) { - $l = \OC::$server->getL10N('lib'); - - if(!isset($data['source'])) { - throw new \Exception($l->t("No source specified when installing app")); - } + public function downloadApp($appId, + AppFetcher $appFetcher, + IClientService $clientService, + ITempManager $tempManager, + ILogger $logger) { + $appId = strtolower($appId); + + $apps = $appFetcher->get(); + foreach($apps as $app) { + if($app['id'] === $appId) { + // Verify if the certificate has been issued by the Nextcloud Code Authority CA + $x509 = new X509(); + $x509->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt')); + $x509->loadX509($app['certificate']); + if($x509->validateSignature() !== true) { + $logger->error( + sprintf( + 'App with id %s has a certificate not issued by a trusted Code Signing Authority', + $appId + ), + [ + 'app' => 'core', + ] + ); + return false; + } - //download the file if necessary - if($data['source']=='http') { - $pathInfo = pathinfo($data['href']); - $extension = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : ''; - $path = \OC::$server->getTempManager()->getTemporaryFile($extension); - if(!isset($data['href'])) { - throw new \Exception($l->t("No href specified when installing app from http")); - } - $client = \OC::$server->getHTTPClientService()->newClient(); - $client->get($data['href'], ['save_to' => $path]); - } else { - if(!isset($data['path'])) { - throw new \Exception($l->t("No path specified when installing app from local file")); - } - $path=$data['path']; - } + // Verify if the certificate is issued for the requested app id + $certInfo = openssl_x509_parse($app['certificate']); + if(!isset($certInfo['subject']['CN'])) { + $logger->error( + sprintf( + 'App with id %s has a cert with no CN', + $appId + ), + [ + 'app' => 'core', + ] + ); + return false; + } + if($certInfo['subject']['CN'] !== $appId) { + $logger->error( + sprintf( + 'App with id %s has a cert issued to %s', + $appId, + $certInfo['subject']['CN'] + ), + [ + 'app' => 'core', + ] + ); + return false; + } - //detect the archive type - $mime = \OC::$server->getMimeTypeDetector()->detect($path); - if ($mime !=='application/zip' && $mime !== 'application/x-gzip' && $mime !== 'application/x-bzip2') { - throw new \Exception($l->t("Archives of type %s are not supported", array($mime))); - } + // Download the release + $tempFile = $tempManager->getTemporaryFile('.tar.gz'); + $client = $clientService->newClient(); + // FIXME: Proper way to determine what the latest release is + $client->get($app['releases'][0]['download'], ['save_to' => $tempFile]); + + // 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); + openssl_free_key($certificate); + + if($verified === true) { + // Seems to match, let's proceed + $extractDir = $tempManager->getTemporaryFolder(); + $archive = Archive::open($tempFile); + + if($archive) { + $archive->extract($extractDir); + + // Check if appinfo/info.xml has the same app ID as well + $loadEntities = libxml_disable_entity_loader(false); + $xml = simplexml_load_file($extractDir . '/' . $appId . '/appinfo/info.xml'); + libxml_disable_entity_loader($loadEntities); + if((string)$xml->id !== $appId) { + $logger->error( + sprintf( + 'App for id %s has a wrong app ID in info.xml: %s', + $appId, + (string)$xml->id + ), + [ + 'app' => 'core', + ] + ); + return false; + } - //extract the archive in a temporary folder - $extractDir = \OC::$server->getTempManager()->getTemporaryFolder(); - OC_Helper::rmdirr($extractDir); - mkdir($extractDir); - if($archive=\OC\Archive\Archive::open($path)) { - $archive->extract($extractDir); - } else { - OC_Helper::rmdirr($extractDir); - if($data['source']=='http') { - unlink($path); + // Move to app folder + $baseDir = OC_App::getInstallPath().'/'.$appId; + //copy the app to the correct place + if(@mkdir($baseDir)) { + $extractDir .= '/' . $appId; + OC_Helper::copyr($extractDir, $baseDir); + } + OC_Helper::copyr($extractDir, $baseDir); + OC_Helper::rmdirr($extractDir); + return true; + } else { + $logger->error( + sprintf( + 'Could not extract app with ID %s to %s', + $appId, + $extractDir + ), + [ + 'app' => 'core', + ] + ); + return false; + } + } else { + // Signature does not match + $logger->error( + sprintf( + 'App with id %s has invalid signature', + $appId + ), + [ + 'app' => 'core', + ] + ); + } } - throw new \Exception($l->t("Failed to open archive when installing app")); } - return array( - $extractDir, - $path - ); + return false; } /** @@ -466,7 +526,7 @@ class Installer { * * The function will check if the app is already downloaded in the apps repository */ - public static function isDownloaded( $name ) { + public function isDownloaded($name) { foreach(\OC::$APPSROOTS as $dir) { $dirToTest = $dir['path']; $dirToTest .= '/'; diff --git a/lib/private/OCSClient.php b/lib/private/OCSClient.php deleted file mode 100644 index 76c0b136c06..00000000000 --- a/lib/private/OCSClient.php +++ /dev/null @@ -1,351 +0,0 @@ - - * @author Brice Maron - * @author Felix Moeller - * @author Frank Karlitschek - * @author Jarrett - * @author Joas Schilling - * @author Jörn Friedrich Dreyer - * @author Kamil Domanski - * @author Lukas Reschke - * @author Morris Jobke - * @author Robin McCorkell - * @author Sam Tuke - * @author Thomas Müller - * - * @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 - * - */ - -namespace OC; - -use OCP\Http\Client\IClientService; -use OCP\IConfig; -use OCP\ILogger; - -/** - * Class OCSClient is a class for communication with the ownCloud appstore - * - * @package OC - */ -class OCSClient { - /** @var IClientService */ - private $httpClientService; - /** @var IConfig */ - private $config; - /** @var ILogger */ - private $logger; - - /** - * @param IClientService $httpClientService - * @param IConfig $config - * @param ILogger $logger - */ - public function __construct(IClientService $httpClientService, - IConfig $config, - ILogger $logger) { - $this->httpClientService = $httpClientService; - $this->config = $config; - $this->logger = $logger; - } - - /** - * Returns whether the AppStore is enabled (i.e. because the AppStore is disabled for EE) - * - * @return bool - */ - public function isAppStoreEnabled() { - return $this->config->getSystemValue('appstoreenabled', true) === true; - } - - /** - * Get the url of the OCS AppStore server. - * - * @return string of the AppStore server - */ - private function getAppStoreUrl() { - return $this->config->getSystemValue('appstoreurl', 'https://api.owncloud.com/v1'); - } - - /** - * @param string $body - * @param string $action - * @return null|\SimpleXMLElement - */ - private function loadData($body, $action) { - $loadEntities = libxml_disable_entity_loader(true); - $data = @simplexml_load_string($body); - libxml_disable_entity_loader($loadEntities); - - if($data === false) { - libxml_clear_errors(); - $this->logger->error( - sprintf('Could not get %s, content was no valid XML', $action), - [ - 'app' => 'core', - ] - ); - return null; - } - - return $data; - } - - /** - * Get all the categories from the OCS server - * - * @param array $targetVersion The target ownCloud version - * @return array|null an array of category ids or null - * @note returns NULL if config value appstoreenabled is set to false - * This function returns a list of all the application categories on the OCS server - */ - public function getCategories(array $targetVersion) { - if (!$this->isAppStoreEnabled()) { - return null; - } - - $client = $this->httpClientService->newClient(); - try { - $response = $client->get( - $this->getAppStoreUrl() . '/content/categories', - [ - 'timeout' => 20, - 'query' => [ - 'version' => implode('x', $targetVersion), - ], - ] - ); - } catch(\Exception $e) { - $this->logger->error( - sprintf('Could not get categories: %s', $e->getMessage()), - [ - 'app' => 'core', - ] - ); - return null; - } - - $data = $this->loadData($response->getBody(), 'categories'); - if($data === null) { - return null; - } - - $tmp = $data->data; - $cats = []; - - foreach ($tmp->category as $value) { - $id = (int)$value->id; - $name = (string)$value->name; - $cats[$id] = $name; - } - - return $cats; - } - - /** - * Get all the applications from the OCS server - * @param array $categories - * @param int $page - * @param string $filter - * @param array $targetVersion The target ownCloud version - * @return array An array of application data - */ - public function getApplications(array $categories, $page, $filter, array $targetVersion) { - if (!$this->isAppStoreEnabled()) { - return []; - } - - $client = $this->httpClientService->newClient(); - try { - $response = $client->get( - $this->getAppStoreUrl() . '/content/data', - [ - 'timeout' => 20, - 'query' => [ - 'version' => implode('x', $targetVersion), - 'filter' => $filter, - 'categories' => implode('x', $categories), - 'sortmode' => 'new', - 'page' => $page, - 'pagesize' => 100, - 'approved' => $filter - ], - ] - ); - } catch(\Exception $e) { - $this->logger->error( - sprintf('Could not get applications: %s', $e->getMessage()), - [ - 'app' => 'core', - ] - ); - return []; - } - - $data = $this->loadData($response->getBody(), 'applications'); - if($data === null) { - return []; - } - - $tmp = $data->data->content; - $tmpCount = count($tmp); - - $apps = []; - for ($i = 0; $i < $tmpCount; $i++) { - $app = []; - $app['id'] = (string)$tmp[$i]->id; - $app['name'] = (string)$tmp[$i]->name; - $app['label'] = (string)$tmp[$i]->label; - $app['version'] = (string)$tmp[$i]->version; - $app['type'] = (string)$tmp[$i]->typeid; - $app['typename'] = (string)$tmp[$i]->typename; - $app['personid'] = (string)$tmp[$i]->personid; - $app['profilepage'] = (string)$tmp[$i]->profilepage; - $app['license'] = (string)$tmp[$i]->license; - $app['detailpage'] = (string)$tmp[$i]->detailpage; - $app['preview'] = (string)$tmp[$i]->smallpreviewpic1; - $app['preview-full'] = (string)$tmp[$i]->previewpic1; - $app['changed'] = strtotime($tmp[$i]->changed); - $app['description'] = (string)$tmp[$i]->description; - $app['score'] = (string)$tmp[$i]->score; - $app['downloads'] = (int)$tmp[$i]->downloads; - $app['level'] = (int)$tmp[$i]->approved; - - $apps[] = $app; - } - - return $apps; - } - - - /** - * Get an the applications from the OCS server - * - * @param string $id - * @param array $targetVersion The target ownCloud version - * @return array|null an array of application data or null - * - * This function returns an applications from the OCS server - */ - public function getApplication($id, array $targetVersion) { - if (!$this->isAppStoreEnabled()) { - return null; - } - - $client = $this->httpClientService->newClient(); - try { - $response = $client->get( - $this->getAppStoreUrl() . '/content/data/' . urlencode($id), - [ - 'timeout' => 20, - 'query' => [ - 'version' => implode('x', $targetVersion), - ], - ] - ); - } catch(\Exception $e) { - $this->logger->error( - sprintf('Could not get application: %s', $e->getMessage()), - [ - 'app' => 'core', - ] - ); - return null; - } - - $data = $this->loadData($response->getBody(), 'application'); - if($data === null) { - return null; - } - - $tmp = $data->data->content; - if (is_null($tmp)) { - \OCP\Util::writeLog('core', 'No update found at the ownCloud appstore for app ' . $id, \OCP\Util::DEBUG); - return null; - } - - $app = []; - $app['id'] = (int)$id; - $app['name'] = (string)$tmp->name; - $app['version'] = (string)$tmp->version; - $app['type'] = (string)$tmp->typeid; - $app['label'] = (string)$tmp->label; - $app['typename'] = (string)$tmp->typename; - $app['personid'] = (string)$tmp->personid; - $app['profilepage'] = (string)$tmp->profilepage; - $app['detailpage'] = (string)$tmp->detailpage; - $app['preview1'] = (string)$tmp->smallpreviewpic1; - $app['preview2'] = (string)$tmp->smallpreviewpic2; - $app['preview3'] = (string)$tmp->smallpreviewpic3; - $app['changed'] = strtotime($tmp->changed); - $app['description'] = (string)$tmp->description; - $app['detailpage'] = (string)$tmp->detailpage; - $app['score'] = (int)$tmp->score; - $app['level'] = (int)$tmp->approved; - - return $app; - } - - /** - * Get the download url for an application from the OCS server - * @param string $id - * @param array $targetVersion The target ownCloud version - * @return array|null an array of application data or null - */ - public function getApplicationDownload($id, array $targetVersion) { - if (!$this->isAppStoreEnabled()) { - return null; - } - $url = $this->getAppStoreUrl() . '/content/download/' . urlencode($id) . '/1'; - $client = $this->httpClientService->newClient(); - try { - $response = $client->get( - $url, - [ - 'timeout' => 20, - 'query' => [ - 'version' => implode('x', $targetVersion), - ], - ] - ); - } catch(\Exception $e) { - $this->logger->error( - sprintf('Could not get application download URL: %s', $e->getMessage()), - [ - 'app' => 'core', - ] - ); - return null; - } - - $data = $this->loadData($response->getBody(), 'application download URL'); - if($data === null) { - return null; - } - - $tmp = $data->data->content; - $app = []; - if (isset($tmp->downloadlink)) { - $app['downloadlink'] = (string)$tmp->downloadlink; - } else { - $app['downloadlink'] = ''; - } - return $app; - } - -} diff --git a/lib/private/Server.php b/lib/private/Server.php index 21ec311401d..39905dcf7ce 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -580,13 +580,6 @@ class Server extends ServerContainer implements IServerContainer { $c->getThemingDefaults() ); }); - $this->registerService('OcsClient', function (Server $c) { - return new OCSClient( - $this->getHTTPClientService(), - $this->getConfig(), - $this->getLogger() - ); - }); $this->registerService('LDAPProvider', function(Server $c) { $config = $c->getConfig(); $factoryClass = $config->getSystemValue('ldapProviderFactory', null); diff --git a/lib/private/legacy/app.php b/lib/private/legacy/app.php index d25534aa822..1db1ce74cdc 100644 --- a/lib/private/legacy/app.php +++ b/lib/private/legacy/app.php @@ -1,6 +1,7 @@ * * @author Arthur Schiwon * @author Bart Visscher @@ -326,24 +327,59 @@ class OC_App { /** * enables an app * - * @param mixed $app app + * @param string $appId * @param array $groups (optional) when set, only these groups will have access to the app * @throws \Exception * @return void * * This function set an app as enabled in appconfig. */ - public static function enable($app, $groups = null) { + public function enable($appId, + $groups = null) { self::$enabledAppsCache = []; // flush - if (!Installer::isInstalled($app)) { - $app = self::installApp($app); + $l = \OC::$server->getL10N('core'); + $config = \OC::$server->getConfig(); + + // Check if app is already downloaded + $installer = new Installer(); + $isDownloaded = $installer->isDownloaded($appId); + + if(!$isDownloaded) { + $state = $installer->downloadApp( + $appId, + new \OC\App\AppStore\Fetcher\AppFetcher( + \OC::$server->getAppDataDir('appstore'), + \OC::$server->getHTTPClientService(), + new \OC\AppFramework\Utility\TimeFactory(), + $config + ), + \OC::$server->getHTTPClientService(), + \OC::$server->getTempManager(), + \OC::$server->getLogger() + ); + + if($state !== true) { + throw new \Exception( + sprintf( + 'Could not download app with id: %s', + $appId + ) + ); + } + } + + if (!Installer::isInstalled($appId)) { + $appId = self::installApp( + $appId, + $config, + $l + ); + $installer->installApp($appId); } else { // check for required dependencies - $config = \OC::$server->getConfig(); - $l = \OC::$server->getL10N('core'); - $info = self::getAppInfo($app); - + $info = self::getAppInfo($appId); self::checkAppDependencies($config, $l, $info); + $installer->installApp($appId); } $appManager = \OC::$server->getAppManager(); @@ -356,40 +392,19 @@ class OC_App { $groupsList[] = $groupManager->get($group); } } - $appManager->enableAppForGroups($app, $groupsList); + $appManager->enableAppForGroups($appId, $groupsList); } else { - $appManager->enableApp($app); + $appManager->enableApp($appId); } - $info = self::getAppInfo($app); + $info = self::getAppInfo($appId); if(isset($info['settings']) && is_array($info['settings'])) { - $appPath = self::getAppPath($app); - self::registerAutoloading($app, $appPath); + $appPath = self::getAppPath($appId); + self::registerAutoloading($appId, $appPath); \OC::$server->getSettingsManager()->setupSettings($info['settings']); } } - /** - * @param string $app - * @return int - */ - private static function downloadApp($app) { - $ocsClient = new OCSClient( - \OC::$server->getHTTPClientService(), - \OC::$server->getConfig(), - \OC::$server->getLogger() - ); - $appData = $ocsClient->getApplication($app, \OCP\Util::getVersion()); - $download = $ocsClient->getApplicationDownload($app, \OCP\Util::getVersion()); - if(isset($download['downloadlink']) and $download['downloadlink']!='') { - // Replace spaces in download link without encoding entire URL - $download['downloadlink'] = str_replace(' ', '%20', $download['downloadlink']); - $info = array('source' => 'http', 'href' => $download['downloadlink'], 'appdata' => $appData); - $app = Installer::installApp($info); - } - return $app; - } - /** * @param string $app * @return bool @@ -409,11 +424,6 @@ class OC_App { * @throws Exception */ public static function disable($app) { - // Convert OCS ID to regular application identifier - if(self::getInternalAppIdByOcs($app) !== false) { - $app = self::getInternalAppIdByOcs($app); - } - // flush self::$enabledAppsCache = array(); @@ -613,18 +623,6 @@ class OC_App { return false; } - - /** - * check if an app's directory is writable - * - * @param string $appId - * @return bool - */ - public static function isAppDirWritable($appId) { - $path = self::getAppPath($appId); - return ($path !== false) ? is_writable($path) : false; - } - /** * Get the path for the given app on the access * If the app is defined in multiple directories, the first one is taken. (false if not found) @@ -837,20 +835,11 @@ class OC_App { /** * List all apps, this is used in apps.php * - * @param bool $onlyLocal - * @param bool $includeUpdateInfo Should we check whether there is an update - * in the app store? - * @param OCSClient $ocsClient * @return array */ - public static function listAllApps($onlyLocal = false, - $includeUpdateInfo = true, - OCSClient $ocsClient) { + public function listAllApps() { $installedApps = OC_App::getAllApps(); - //TODO which apps do we want to blacklist and how do we integrate - // blacklisting with the multi apps folder feature? - //we don't want to show configuration for these $blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps(); $appList = array(); @@ -893,8 +882,6 @@ class OC_App { $info['removable'] = true; } - $info['update'] = ($includeUpdateInfo) ? Installer::isUpdateAvailable($app) : null; - $appPath = self::getAppPath($app); if($appPath !== false) { $appIcon = $appPath . '/img/' . $app . '.svg'; @@ -926,29 +913,8 @@ class OC_App { $appList[] = $info; } } - if ($onlyLocal) { - $remoteApps = []; - } else { - $remoteApps = OC_App::getAppstoreApps('approved', null, $ocsClient); - } - if ($remoteApps) { - // Remove duplicates - foreach ($appList as $app) { - foreach ($remoteApps AS $key => $remote) { - if ($app['name'] === $remote['name'] || - (isset($app['ocsid']) && - $app['ocsid'] === $remote['id']) - ) { - unset($remoteApps[$key]); - } - } - } - $combinedApps = array_merge($appList, $remoteApps); - } else { - $combinedApps = $appList; - } - return $combinedApps; + return $appList; } /** @@ -966,70 +932,6 @@ class OC_App { return false; } - /** - * Get a list of all apps on the appstore - * @param string $filter - * @param string|null $category - * @param OCSClient $ocsClient - * @return array|bool multi-dimensional array of apps. - * Keys: id, name, type, typename, personid, license, detailpage, preview, changed, description - */ - public static function getAppstoreApps($filter = 'approved', - $category = null, - OCSClient $ocsClient) { - $categories = [$category]; - - if (is_null($category)) { - $categoryNames = $ocsClient->getCategories(\OCP\Util::getVersion()); - if (is_array($categoryNames)) { - // Check that categories of apps were retrieved correctly - if (!$categories = array_keys($categoryNames)) { - return false; - } - } else { - return false; - } - } - - $page = 0; - $remoteApps = $ocsClient->getApplications($categories, $page, $filter, \OCP\Util::getVersion()); - $apps = []; - $i = 0; - $l = \OC::$server->getL10N('core'); - foreach ($remoteApps as $app) { - $potentialCleanId = self::getInternalAppIdByOcs($app['id']); - // enhance app info (for example the description) - $apps[$i] = OC_App::parseAppInfo($app); - $apps[$i]['author'] = $app['personid']; - $apps[$i]['ocs_id'] = $app['id']; - $apps[$i]['internal'] = 0; - $apps[$i]['active'] = ($potentialCleanId !== false) ? self::isEnabled($potentialCleanId) : false; - $apps[$i]['update'] = false; - $apps[$i]['groups'] = false; - $apps[$i]['score'] = $app['score']; - $apps[$i]['removable'] = false; - if ($app['label'] == 'recommended') { - $apps[$i]['internallabel'] = (string)$l->t('Recommended'); - $apps[$i]['internalclass'] = 'recommendedapp'; - } - - // Apps from the appstore are always assumed to be compatible with the - // the current release as the initial filtering is done on the appstore - $apps[$i]['dependencies']['owncloud']['@attributes']['min-version'] = implode('.', \OCP\Util::getVersion()); - $apps[$i]['dependencies']['owncloud']['@attributes']['max-version'] = implode('.', \OCP\Util::getVersion()); - - $i++; - } - - - - if (empty($apps)) { - return false; - } else { - return $apps; - } - } - public static function shouldUpgrade($app) { $versions = self::getAppVersions(); $currentVersion = OC_App::getAppVersion($app); @@ -1132,46 +1034,16 @@ class OC_App { /** * @param string $app + * @param \OCP\IConfig $config + * @param \OCP\IL10N $l * @return bool + * * @throws Exception if app is not compatible with this version of ownCloud * @throws Exception if no app-name was specified */ - public static function installApp($app) { - $appName = $app; // $app will be overwritten, preserve name for error logging - $l = \OC::$server->getL10N('core'); - $config = \OC::$server->getConfig(); - $ocsClient = new OCSClient( - \OC::$server->getHTTPClientService(), - $config, - \OC::$server->getLogger() - ); - $appData = $ocsClient->getApplication($app, \OCP\Util::getVersion()); - - // check if app is a shipped app or not. OCS apps have an integer as id, shipped apps use a string - if (!is_numeric($app)) { - $shippedVersion = self::getAppVersion($app); - if ($appData && version_compare($shippedVersion, $appData['version'], '<')) { - $app = self::downloadApp($app); - } else { - $app = Installer::installShippedApp($app); - } - } else { - // Maybe the app is already installed - compare the version in this - // case and use the local already installed one. - // FIXME: This is a horrible hack. I feel sad. The god of code cleanness may forgive me. - $internalAppId = self::getInternalAppIdByOcs($app); - if($internalAppId !== false) { - if($appData && version_compare(\OC_App::getAppVersion($internalAppId), $appData['version'], '<')) { - $app = self::downloadApp($app); - } else { - self::enable($internalAppId); - $app = $internalAppId; - } - } else { - $app = self::downloadApp($app); - } - } - + public function installApp($app, + \OCP\IConfig $config, + \OCP\IL10N $l) { if ($app !== false) { // check if the app is compatible with this version of ownCloud $info = self::getAppInfo($app); diff --git a/settings/Application.php b/settings/Application.php index dd237e40c9d..3dbf9acc524 100644 --- a/settings/Application.php +++ b/settings/Application.php @@ -30,7 +30,11 @@ namespace OC\Settings; +use OC\App\AppStore\Fetcher\AppFetcher; +use OC\App\AppStore\Fetcher\CategoryFetcher; +use OC\AppFramework\Utility\TimeFactory; use OC\Authentication\Token\IProvider; +use OC\Server; use OC\Settings\Middleware\SubadminMiddleware; use OCP\AppFramework\App; use OCP\IContainer; @@ -86,5 +90,24 @@ class Application extends App { $container->registerService(IManager::class, function (IContainer $c) { return $c->query('ServerContainer')->getSettingsManager(); }); + $container->registerService(AppFetcher::class, function (IContainer $c) { + /** @var Server $server */ + $server = $c->query('ServerContainer'); + return new AppFetcher( + $server->getAppDataDir('appstore'), + $server->getHTTPClientService(), + new TimeFactory(), + $server->getConfig() + ); + }); + $container->registerService(CategoryFetcher::class, function (IContainer $c) { + /** @var Server $server */ + $server = $c->query('ServerContainer'); + return new CategoryFetcher( + $server->getAppDataDir('appstore'), + $server->getHTTPClientService(), + new TimeFactory() + ); + }); } } diff --git a/settings/Controller/AppSettingsController.php b/settings/Controller/AppSettingsController.php index 2efd3b8a847..16d4780c5f9 100644 --- a/settings/Controller/AppSettingsController.php +++ b/settings/Controller/AppSettingsController.php @@ -1,6 +1,7 @@ * * @author Christoph Wurst * @author Joas Schilling @@ -26,19 +27,22 @@ namespace OC\Settings\Controller; +use OC\App\AppStore\Fetcher\AppFetcher; +use OC\App\AppStore\Fetcher\CategoryFetcher; +use OC\App\AppStore\Version\VersionParser; use OC\App\DependencyAnalyzer; use OC\App\Platform; -use OC\OCSClient; use OCP\App\IAppManager; use \OCP\AppFramework\Controller; use OCP\AppFramework\Http\ContentSecurityPolicy; -use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\TemplateResponse; use OCP\ICacheFactory; use OCP\INavigationManager; use OCP\IRequest; use OCP\IL10N; use OCP\IConfig; +use OCP\L10N\IFactory; /** * @package OC\Settings\Controller @@ -57,8 +61,12 @@ class AppSettingsController extends Controller { private $navigationManager; /** @var IAppManager */ private $appManager; - /** @var OCSClient */ - private $ocsClient; + /** @var CategoryFetcher */ + private $categoryFetcher; + /** @var AppFetcher */ + private $appFetcher; + /** @var IFactory */ + private $l10nFactory; /** * @param string $appName @@ -68,7 +76,9 @@ class AppSettingsController extends Controller { * @param ICacheFactory $cache * @param INavigationManager $navigationManager * @param IAppManager $appManager - * @param OCSClient $ocsClient + * @param CategoryFetcher $categoryFetcher + * @param AppFetcher $appFetcher + * @param IFactory $l10nFactory */ public function __construct($appName, IRequest $request, @@ -77,69 +87,39 @@ class AppSettingsController extends Controller { ICacheFactory $cache, INavigationManager $navigationManager, IAppManager $appManager, - OCSClient $ocsClient) { + CategoryFetcher $categoryFetcher, + AppFetcher $appFetcher, + IFactory $l10nFactory) { parent::__construct($appName, $request); $this->l10n = $l10n; $this->config = $config; $this->cache = $cache->create($appName); $this->navigationManager = $navigationManager; $this->appManager = $appManager; - $this->ocsClient = $ocsClient; - } - - /** - * Enables or disables the display of experimental apps - * @param bool $state - * @return DataResponse - */ - public function changeExperimentalConfigState($state) { - $this->config->setSystemValue('appstore.experimental.enabled', $state); - $this->appManager->clearAppsCache(); - return new DataResponse(); - } - - /** - * @param string|int $category - * @return int - */ - protected function getCategory($category) { - if (is_string($category)) { - foreach ($this->listCategories() as $cat) { - if (isset($cat['ident']) && $cat['ident'] === $category) { - $category = (int) $cat['id']; - break; - } - } - - // Didn't find the category, falling back to enabled - if (is_string($category)) { - $category = self::CAT_ENABLED; - } - } - return (int) $category; + $this->categoryFetcher = $categoryFetcher; + $this->appFetcher = $appFetcher; + $this->l10nFactory = $l10nFactory; } /** * @NoCSRFRequired + * * @param string $category * @return TemplateResponse */ public function viewApps($category = '') { - $categoryId = $this->getCategory($category); - if ($categoryId === self::CAT_ENABLED) { - // Do not use an arbitrary input string, because we put the category in html + if ($category === '') { $category = 'enabled'; } $params = []; - $params['experimentalEnabled'] = $this->config->getSystemValue('appstore.experimental.enabled', false); $params['category'] = $category; $params['appstoreEnabled'] = $this->config->getSystemValue('appstoreenabled', true) === true; $this->navigationManager->setActiveEntry('core_apps'); $templateResponse = new TemplateResponse($this->appName, 'apps', $params, 'user'); $policy = new ContentSecurityPolicy(); - $policy->addAllowedImageDomain('https://apps.owncloud.com'); + $policy->addAllowedImageDomain('*'); $templateResponse->setContentSecurityPolicy($policy); return $templateResponse; @@ -147,139 +127,171 @@ class AppSettingsController extends Controller { /** * Get all available categories - * @return array + * + * @return JSONResponse */ public function listCategories() { + $currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2); - if(!is_null($this->cache->get('listCategories'))) { - return $this->cache->get('listCategories'); - } - $categories = [ + $formattedCategories = [ ['id' => self::CAT_ENABLED, 'ident' => 'enabled', 'displayName' => (string)$this->l10n->t('Enabled')], ['id' => self::CAT_DISABLED, 'ident' => 'disabled', 'displayName' => (string)$this->l10n->t('Not enabled')], ]; + $categories = $this->categoryFetcher->get(); + foreach($categories as $category) { + $formattedCategories[] = [ + 'id' => $category['id'], + 'ident' => $category['id'], + 'displayName' => isset($category['translations'][$currentLanguage]['name']) ? $category['translations'][$currentLanguage]['name'] : $category['translations']['en']['name'], + ]; + } - if($this->ocsClient->isAppStoreEnabled()) { - // apps from external repo via OCS - $ocs = $this->ocsClient->getCategories(\OCP\Util::getVersion()); - if ($ocs) { - foreach($ocs as $k => $v) { - $name = str_replace('ownCloud ', '', $v); - $ident = str_replace(' ', '-', urlencode(strtolower($name))); - $categories[] = [ - 'id' => $k, - 'ident' => $ident, - 'displayName' => $name, - ]; + return new JSONResponse($formattedCategories); + } + + /** + * Get all apps for a category + * + * @param string $requestedCategory + * @return array + */ + private function getAppsForCategory($requestedCategory) { + $versionParser = new VersionParser(); + $formattedApps = []; + $apps = $this->appFetcher->get(); + foreach($apps as $app) { + + // Skip all apps not in the requested category + $isInCategory = false; + foreach($app['categories'] as $category) { + if($category === $requestedCategory) { + $isInCategory = true; + } + } + if(!$isInCategory) { + continue; + } + + $nextCloudVersion = $versionParser->getVersion($app['releases'][0]['rawPlatformVersionSpec']); + $nextCloudVersionDependencies = []; + if($nextCloudVersion->getMinimumVersion() !== '') { + $nextCloudVersionDependencies['owncloud']['@attributes']['min-version'] = $nextCloudVersion->getMinimumVersion(); + } + if($nextCloudVersion->getMaximumVersion() !== '') { + $nextCloudVersionDependencies['owncloud']['@attributes']['max-version'] = $nextCloudVersion->getMaximumVersion(); + } + $phpVersion = $versionParser->getVersion($app['releases'][0]['rawPhpVersionSpec']); + $existsLocally = (\OC_App::getAppPath($app['id']) !== false) ? true : false; + $phpDependencies = []; + if($phpVersion->getMinimumVersion() !== '') { + $phpDependencies['php']['@attributes']['min-version'] = $phpVersion->getMinimumVersion(); + } + if($phpVersion->getMaximumVersion() !== '') { + $phpDependencies['php']['@attributes']['max-version'] = $phpVersion->getMaximumVersion(); + } + if(isset($app['releases'][0]['minIntSize'])) { + $phpDependencies['php']['@attributes']['min-int-size'] = $app['releases'][0]['minIntSize']; + } + $authors = ''; + foreach($app['authors'] as $key => $author) { + $authors .= $author['name']; + if($key !== count($app['authors']) - 1) { + $authors .= ', '; } } - } - $this->cache->set('listCategories', $categories, 3600); + $currentLanguage = substr(\OC::$server->getL10NFactory()->findLanguage(), 0, 2); + + $formattedApps[] = [ + 'id' => $app['id'], + 'name' => isset($app['translations'][$currentLanguage]['name']) ? $app['translations'][$currentLanguage]['name'] : $app['translations']['en']['name'], + 'description' => isset($app['translations'][$currentLanguage]['description']) ? $app['translations'][$currentLanguage]['description'] : $app['translations']['en']['description'], + 'license' => $app['releases'][0]['licenses'], + 'author' => $authors, + 'shipped' => false, + 'version' => $app['releases'][0]['version'], + 'default_enable' => '', + 'types' => [], + 'documentation' => [ + 'admin' => $app['adminDocs'], + 'user' => $app['userDocs'], + 'developer' => $app['developerDocs'] + ], + 'website' => $app['website'], + 'bugs' => $app['issueTracker'], + 'detailpage' => $app['website'], + 'dependencies' => array_merge( + $nextCloudVersionDependencies, + $phpDependencies + ), + 'level' => ($app['featured'] === true) ? 200 : 100, + 'missingMaxOwnCloudVersion' => false, + 'missingMinOwnCloudVersion' => false, + 'canInstall' => true, + 'preview' => $app['screenshots'][0]['url'], + 'score' => $app['ratingOverall'], + 'removable' => $existsLocally, + 'active' => $this->appManager->isEnabledForUser($app['id']), + 'needsDownload' => !$existsLocally, + ]; + } - return $categories; + return $formattedApps; } /** * Get all available apps in a category * * @param string $category - * @param bool $includeUpdateInfo Should we check whether there is an update - * in the app store? - * @return array + * @return JSONResponse */ - public function listApps($category = '', $includeUpdateInfo = true) { - $category = $this->getCategory($category); - $cacheName = 'listApps-' . $category . '-' . (int) $includeUpdateInfo; - - if(!is_null($this->cache->get($cacheName))) { - $apps = $this->cache->get($cacheName); - } else { - switch ($category) { - // installed apps - case 0: - $apps = $this->getInstalledApps($includeUpdateInfo); - usort($apps, function ($a, $b) { - $a = (string)$a['name']; - $b = (string)$b['name']; - if ($a === $b) { - return 0; - } - return ($a < $b) ? -1 : 1; - }); - $version = \OCP\Util::getVersion(); - foreach($apps as $key => $app) { - if(!array_key_exists('level', $app) && array_key_exists('ocsid', $app)) { - $remoteAppEntry = $this->ocsClient->getApplication($app['ocsid'], $version); - - if(is_array($remoteAppEntry) && array_key_exists('level', $remoteAppEntry)) { - $apps[$key]['level'] = $remoteAppEntry['level']; - } - } + public function listApps($category = '') { + $appClass = new \OC_App(); + + switch ($category) { + // installed apps + case 'enabled': + $apps = $appClass->listAllApps(); + $apps = array_filter($apps, function ($app) { + return $app['active']; + }); + usort($apps, function ($a, $b) { + $a = (string)$a['name']; + $b = (string)$b['name']; + if ($a === $b) { + return 0; } - break; - // not-installed apps - case 1: - $apps = \OC_App::listAllApps(true, $includeUpdateInfo, $this->ocsClient); - $apps = array_filter($apps, function ($app) { - return !$app['active']; - }); - $version = \OCP\Util::getVersion(); - foreach($apps as $key => $app) { - if(!array_key_exists('level', $app) && array_key_exists('ocsid', $app)) { - $remoteAppEntry = $this->ocsClient->getApplication($app['ocsid'], $version); - - if(is_array($remoteAppEntry) && array_key_exists('level', $remoteAppEntry)) { - $apps[$key]['level'] = $remoteAppEntry['level']; - } - } + return ($a < $b) ? -1 : 1; + }); + break; + // disabled apps + case 'disabled': + $apps = $appClass->listAllApps(); + $apps = array_filter($apps, function ($app) { + return !$app['active']; + }); + usort($apps, function ($a, $b) { + $a = (string)$a['name']; + $b = (string)$b['name']; + if ($a === $b) { + return 0; } - usort($apps, function ($a, $b) { - $a = (string)$a['name']; - $b = (string)$b['name']; - if ($a === $b) { - return 0; - } - return ($a < $b) ? -1 : 1; - }); - break; - default: - $filter = $this->config->getSystemValue('appstore.experimental.enabled', false) ? 'all' : 'approved'; - - $apps = \OC_App::getAppstoreApps($filter, $category, $this->ocsClient); - if (!$apps) { - $apps = array(); - } else { - // don't list installed apps - $installedApps = $this->getInstalledApps(false); - $installedApps = array_map(function ($app) { - if (isset($app['ocsid'])) { - return $app['ocsid']; - } - return $app['id']; - }, $installedApps); - $apps = array_filter($apps, function ($app) use ($installedApps) { - return !in_array($app['id'], $installedApps); - }); - - // show tooltip if app is downloaded from remote server - $inactiveApps = $this->getInactiveApps(); - foreach ($apps as &$app) { - $app['needsDownload'] = !in_array($app['id'], $inactiveApps); - } + return ($a < $b) ? -1 : 1; + }); + break; + default: + $apps = $this->getAppsForCategory($category); + + // sort by score + usort($apps, function ($a, $b) { + $a = (int)$a['score']; + $b = (int)$b['score']; + if ($a === $b) { + return 0; } - - // sort by score - usort($apps, function ($a, $b) { - $a = (int)$a['score']; - $b = (int)$b['score']; - if ($a === $b) { - return 0; - } - return ($a > $b) ? -1 : 1; - }); - break; - } + return ($a > $b) ? -1 : 1; + }); + break; } // fix groups to be an array @@ -310,40 +322,6 @@ class AppSettingsController extends Controller { return $app; }, $apps); - $this->cache->set($cacheName, $apps, 300); - - return ['apps' => $apps, 'status' => 'success']; - } - - /** - * @param bool $includeUpdateInfo Should we check whether there is an update - * in the app store? - * @return array - */ - private function getInstalledApps($includeUpdateInfo = true) { - $apps = \OC_App::listAllApps(true, $includeUpdateInfo, $this->ocsClient); - $apps = array_filter($apps, function ($app) { - return $app['active']; - }); - return $apps; - } - - /** - * @return array - */ - private function getInactiveApps() { - $inactiveApps = \OC_App::listAllApps(true, false, $this->ocsClient); - $inactiveApps = array_filter($inactiveApps, - function ($app) { - return !$app['active']; - }); - $inactiveApps = array_map(function($app) { - if (isset($app['ocsid'])) { - return $app['ocsid']; - } - return $app['id']; - }, $inactiveApps); - return $inactiveApps; + return new JSONResponse(['apps' => $apps, 'status' => 'success']); } - } diff --git a/settings/ajax/enableapp.php b/settings/ajax/enableapp.php index db4503f20e7..b378b3c918d 100644 --- a/settings/ajax/enableapp.php +++ b/settings/ajax/enableapp.php @@ -31,8 +31,10 @@ OCP\JSON::callCheck(); $groups = isset($_POST['groups']) ? (array)$_POST['groups'] : null; try { - $app = OC_App::cleanAppId((string)$_POST['appid']); - OC_App::enable($app, $groups); + $app = new OC_App(); + $appId = (string)$_POST['appid']; + $appId = OC_App::cleanAppId($appId); + $app->enable($appId, $groups); OC_JSON::success(['data' => ['update_required' => \OC_App::shouldUpgrade($app)]]); } catch (Exception $e) { \OCP\Util::writeLog('core', $e->getMessage(), \OCP\Util::ERROR); diff --git a/settings/ajax/installapp.php b/settings/ajax/installapp.php index 8831305e223..75f3fea83b7 100644 --- a/settings/ajax/installapp.php +++ b/settings/ajax/installapp.php @@ -29,14 +29,15 @@ if (!array_key_exists('appid', $_POST)) { exit; } +$app = new OC_App(); $appId = (string)$_POST['appid']; $appId = OC_App::cleanAppId($appId); - -$result = OC_App::installApp($appId); +$result = $app->installApp( + $appId, + \OC::$server->getConfig(), + \OC::$server->getL10N('core') +); if($result !== false) { - // FIXME: Clear the cache - move that into some sane helper method - \OC::$server->getMemCacheFactory()->create('settings')->remove('listApps-0'); - \OC::$server->getMemCacheFactory()->create('settings')->remove('listApps-1'); OC_JSON::success(array('data' => array('appid' => $appId))); } else { $l = \OC::$server->getL10N('settings'); diff --git a/settings/css/settings.css b/settings/css/settings.css index 0dadf401c04..fe0e40cb273 100644 --- a/settings/css/settings.css +++ b/settings/css/settings.css @@ -415,17 +415,6 @@ span.version { background-position: 5px center; padding-left: 25px; } -.app-level .approved { - border-color: #0082c9; -} -.app-level .experimental { - background-color: #ce3702; - border-color: #ce3702; - color: #fff; -} -.apps-experimental { - color: #ce3702; -} .app-score { position: relative; diff --git a/settings/js/apps.js b/settings/js/apps.js index 5fc366c4921..ecd7543c8ce 100644 --- a/settings/js/apps.js +++ b/settings/js/apps.js @@ -2,7 +2,7 @@ Handlebars.registerHelper('score', function() { if(this.score) { - var score = Math.round( this.score / 10 ); + var score = Math.round( this.score * 10 ); var imageName = 'rating/s' + score + '.svg'; return new Handlebars.SafeString(''); @@ -13,10 +13,6 @@ Handlebars.registerHelper('level', function() { if(typeof this.level !== 'undefined') { if(this.level === 200) { return new Handlebars.SafeString('' + t('settings', 'Official') + ''); - } else if(this.level === 100) { - return new Handlebars.SafeString('' + t('settings', 'Approved') + ''); - } else { - return new Handlebars.SafeString('' + t('settings', 'Experimental') + ''); } } }); diff --git a/settings/routes.php b/settings/routes.php index 64c4e549681..829474ce2bb 100644 --- a/settings/routes.php +++ b/settings/routes.php @@ -49,7 +49,6 @@ $application->registerRoutes($this, [ ['name' => 'AppSettings#listCategories', 'url' => '/settings/apps/categories', 'verb' => 'GET'], ['name' => 'AppSettings#viewApps', 'url' => '/settings/apps', 'verb' => 'GET'], ['name' => 'AppSettings#listApps', 'url' => '/settings/apps/list', 'verb' => 'GET'], - ['name' => 'AppSettings#changeExperimentalConfigState', 'url' => '/settings/apps/experimental', 'verb' => 'POST'], ['name' => 'SecuritySettings#trustedDomains', 'url' => '/settings/admin/security/trustedDomains', 'verb' => 'POST'], ['name' => 'Users#setDisplayName', 'url' => '/settings/users/{username}/displayName', 'verb' => 'POST'], ['name' => 'Users#setMailAddress', 'url' => '/settings/users/{id}/mailAddress', 'verb' => 'PUT'], diff --git a/settings/templates/apps.php b/settings/templates/apps.php index 46fd5bd0e40..36064f0981c 100644 --- a/settings/templates/apps.php +++ b/settings/templates/apps.php @@ -30,15 +30,6 @@ script(