From 0816cf91422346313d44cba5f017125899afbf2d Mon Sep 17 00:00:00 2001 From: Lukas Reschke Date: Mon, 30 Mar 2015 15:58:20 +0200 Subject: [PATCH] Add experimental applications switch Allows administrators to disable or enabled experimental applications as well as show the trust level. --- config/config.sample.php | 9 + core/templates/layout.user.php | 2 +- lib/private/app.php | 106 +- lib/private/app/appmanager.php | 2 +- lib/private/installer.php | 17 +- lib/private/ocsclient.php | 274 ++++-- lib/private/server.php | 14 + lib/private/templatelayout.php | 2 +- lib/public/app/iappmanager.php | 5 + settings/application.php | 14 +- settings/apps.php | 42 - settings/controller/appsettingscontroller.php | 75 +- settings/css/settings.css | 21 +- settings/js/apps.js | 41 +- settings/routes.php | 40 +- settings/templates/apps.php | 57 ++ tests/lib/OCSClientTest.php | 931 ++++++++++++++++++ .../controller/AppSettingsControllerTest.php | 231 +++++ 18 files changed, 1635 insertions(+), 248 deletions(-) delete mode 100644 settings/apps.php create mode 100644 tests/lib/OCSClientTest.php create mode 100644 tests/settings/controller/AppSettingsControllerTest.php diff --git a/config/config.sample.php b/config/config.sample.php index 60932ab7d9..f17473b7d8 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -576,6 +576,15 @@ $CONFIG = array( */ 'appstoreurl' => 'https://api.owncloud.com/v1', +/** + * Whether to 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/core/templates/layout.user.php b/core/templates/layout.user.php index 880a276c72..87a6a9216d 100644 --- a/core/templates/layout.user.php +++ b/core/templates/layout.user.php @@ -123,7 +123,7 @@ if(OC_User::isAdminUser(OC_User::getUser())): ?>
  • - class="active"> diff --git a/lib/private/app.php b/lib/private/app.php index 84bc23608f..ee92f8f5ad 100644 --- a/lib/private/app.php +++ b/lib/private/app.php @@ -61,6 +61,7 @@ class OC_App { static private $loadedApps = array(); static private $altLogin = array(); private static $shippedApps = null; + const officialApp = 200; /** * clean the appId @@ -306,8 +307,13 @@ class OC_App { * @return int */ public static function downloadApp($app) { - $appData= OCSClient::getApplication($app); - $download= OCSClient::getApplicationDownload($app, 1); + $ocsClient = new OCSClient( + \OC::$server->getHTTPClientService(), + \OC::$server->getConfig(), + \OC::$server->getLogger() + ); + $appData = $ocsClient->getApplication($app); + $download= $ocsClient->getApplicationDownload($app); if(isset($download['downloadlink']) and $download['downloadlink']!='') { // Replace spaces in download link without encoding entire URL $download['downloadlink'] = str_replace(' ', '%20', $download['downloadlink']); @@ -780,8 +786,9 @@ class OC_App { } /** - * Lists all apps, this is used in apps.php + * List all apps, this is used in apps.php * + * @param bool $onlyLocal * @return array */ public static function listAllApps($onlyLocal = false) { @@ -819,8 +826,7 @@ class OC_App { if (isset($info['shipped']) and ($info['shipped'] == 'true')) { $info['internal'] = true; - $info['internallabel'] = (string)$l->t('Recommended'); - $info['internalclass'] = 'recommendedapp'; + $info['level'] = self::officialApp; $info['removable'] = false; } else { $info['internal'] = false; @@ -845,7 +851,7 @@ class OC_App { } } if ($onlyLocal) { - $remoteApps = array(); + $remoteApps = []; } else { $remoteApps = OC_App::getAppstoreApps(); } @@ -865,34 +871,6 @@ class OC_App { } else { $combinedApps = $appList; } - // bring the apps into the right order with a custom sort function - usort($combinedApps, function ($a, $b) { - - // priority 1: active - if ($a['active'] != $b['active']) { - return $b['active'] - $a['active']; - } - - // priority 2: shipped - $aShipped = (array_key_exists('shipped', $a) && $a['shipped'] === 'true') ? 1 : 0; - $bShipped = (array_key_exists('shipped', $b) && $b['shipped'] === 'true') ? 1 : 0; - if ($aShipped !== $bShipped) { - return ($bShipped - $aShipped); - } - - // priority 3: recommended - $internalClassA = isset($a['internalclass']) ? $a['internalclass'] : ''; - $internalClassB = isset($b['internalclass']) ? $b['internalclass'] : ''; - if ($internalClassA != $internalClassB) { - $aTemp = ($internalClassA == 'recommendedapp' ? 1 : 0); - $bTemp = ($internalClassB == 'recommendedapp' ? 1 : 0); - return ($bTemp - $aTemp); - } - - // priority 4: alphabetical - return strcasecmp($a['name'], $b['name']); - - }); return $combinedApps; } @@ -913,15 +891,24 @@ class OC_App { } /** - * get a list of all apps on apps.owncloud.com - * - * @return array|false multi-dimensional array of apps. - * Keys: id, name, type, typename, personid, license, detailpage, preview, changed, description + * Get a list of all apps on the appstore + * @param string $filter + * @param string $category + * @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) { - $categories = array($category); + $categories = [$category]; + + $ocsClient = new OCSClient( + \OC::$server->getHTTPClientService(), + \OC::$server->getConfig(), + \OC::$server->getLogger() + ); + + if (is_null($category)) { - $categoryNames = OCSClient::getCategories(); + $categoryNames = $ocsClient->getCategories(); if (is_array($categoryNames)) { // Check that categories of apps were retrieved correctly if (!$categories = array_keys($categoryNames)) { @@ -933,34 +920,36 @@ class OC_App { } $page = 0; - $remoteApps = OCSClient::getApplications($categories, $page, $filter); - $app1 = array(); + $remoteApps = $ocsClient->getApplications($categories, $page, $filter); + $apps = []; $i = 0; $l = \OC::$server->getL10N('core'); foreach ($remoteApps as $app) { $potentialCleanId = self::getInternalAppIdByOcs($app['id']); // enhance app info (for example the description) - $app1[$i] = OC_App::parseAppInfo($app); - $app1[$i]['author'] = $app['personid']; - $app1[$i]['ocs_id'] = $app['id']; - $app1[$i]['internal'] = 0; - $app1[$i]['active'] = ($potentialCleanId !== false) ? self::isEnabled($potentialCleanId) : false; - $app1[$i]['update'] = false; - $app1[$i]['groups'] = false; - $app1[$i]['score'] = $app['score']; - $app1[$i]['removable'] = false; + $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') { - $app1[$i]['internallabel'] = (string)$l->t('Recommended'); - $app1[$i]['internalclass'] = 'recommendedapp'; + $apps[$i]['internallabel'] = (string)$l->t('Recommended'); + $apps[$i]['internalclass'] = 'recommendedapp'; } $i++; } - if (empty($app1)) { + + + if (empty($apps)) { return false; } else { - return $app1; + return $apps; } } @@ -1084,7 +1073,12 @@ class OC_App { public static function installApp($app) { $l = \OC::$server->getL10N('core'); $config = \OC::$server->getConfig(); - $appData=OCSClient::getApplication($app); + $ocsClient = new OCSClient( + \OC::$server->getHTTPClientService(), + $config, + \OC::$server->getLogger() + ); + $appData = $ocsClient->getApplication($app); // 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)) { diff --git a/lib/private/app/appmanager.php b/lib/private/app/appmanager.php index 2a147d4de6..c9d4a777c4 100644 --- a/lib/private/app/appmanager.php +++ b/lib/private/app/appmanager.php @@ -203,7 +203,7 @@ class AppManager implements IAppManager { /** * Clear the cached list of apps when enabling/disabling an app */ - protected function clearAppsCache() { + public function clearAppsCache() { $settingsMemCache = $this->memCacheFactory->create('settings'); $settingsMemCache->clear('listApps'); } diff --git a/lib/private/installer.php b/lib/private/installer.php index e30344b1b1..41f13f0f5f 100644 --- a/lib/private/installer.php +++ b/lib/private/installer.php @@ -222,8 +222,13 @@ class OC_Installer{ * @throws Exception */ public static function updateAppByOCSId($ocsId) { - $appData = OCSClient::getApplication($ocsId); - $download = OCSClient::getApplicationDownload($ocsId, 1); + $ocsClient = new OCSClient( + \OC::$server->getHTTPClientService(), + \OC::$server->getConfig(), + \OC::$server->getLogger() + ); + $appData = $ocsClient->getApplication($ocsId); + $download = $ocsClient->getApplicationDownload($ocsId); if (isset($download['downloadlink']) && trim($download['downloadlink']) !== '') { $download['downloadlink'] = str_replace(' ', '%20', $download['downloadlink']); @@ -385,8 +390,12 @@ class OC_Installer{ $ocsid=OC_Appconfig::getValue( $app, 'ocsid', ''); if($ocsid<>'') { - - $ocsdata=OCSClient::getApplication($ocsid); + $ocsClient = new OCSClient( + \OC::$server->getHTTPClientService(), + \OC::$server->getConfig(), + \OC::$server->getLogger() + ); + $ocsdata = $ocsClient->getApplication($ocsid); $ocsversion= (string) $ocsdata['version']; $currentversion=OC_App::getAppVersion($app); if (version_compare($ocsversion, $currentversion, '>')) { diff --git a/lib/private/ocsclient.php b/lib/private/ocsclient.php index f69426ddaf..30747c0d5f 100644 --- a/lib/private/ocsclient.php +++ b/lib/private/ocsclient.php @@ -32,36 +32,52 @@ namespace OC; -/** - * This class provides an easy way for apps to store config values in the - * database. - */ +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 static function isAppStoreEnabled() { - if (\OC::$server->getConfig()->getSystemValue('appstoreenabled', true) === false ) { - return false; - } - - return true; + public function isAppStoreEnabled() { + return $this->config->getSystemValue('appstoreenabled', true) === true; } /** * Get the url of the OCS AppStore server. * * @return string of the AppStore server - * - * This function returns the url of the OCS AppStore server. It´s possible - * to set it in the config file or it will fallback to the default */ - private static function getAppStoreURL() { - return \OC::$server->getConfig()->getSystemValue('appstoreurl', 'https://api.owncloud.com/v1'); + private function getAppStoreUrl() { + return $this->config->getSystemValue('appstoreurl', 'https://api.owncloud.com/v1'); } /** @@ -71,36 +87,50 @@ class OCSClient { * @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 static function getCategories() { - if (!self::isAppStoreEnabled()) { + public function getCategories() { + if (!$this->isAppStoreEnabled()) { return null; } - $url = self::getAppStoreURL() . '/content/categories'; - $client = \OC::$server->getHTTPClientService()->newClient(); + $client = $this->httpClientService->newClient(); try { - $response = $client->get($url, ['timeout' => 5]); + $response = $client->get( + $this->getAppStoreUrl() . '/content/categories', + [ + 'timeout' => 5, + ] + ); } catch(\Exception $e) { - return null; - } - - if($response->getStatusCode() !== 200) { + $this->logger->error( + sprintf('Could not get categories: %s', $e->getMessage()), + [ + 'app' => 'core', + ] + ); return null; } $loadEntities = libxml_disable_entity_loader(true); - $data = simplexml_load_string($response->getBody()); + $data = @simplexml_load_string($response->getBody()); libxml_disable_entity_loader($loadEntities); + if($data === false) { + $this->logger->error( + 'Could not get categories, content was no valid XML', + [ + 'app' => 'core', + ] + ); + return null; + } + $tmp = $data->data; $cats = []; foreach ($tmp->category as $value) { - $id = (int)$value->id; $name = (string)$value->name; $cats[$id] = $name; - } return $cats; @@ -108,50 +138,63 @@ class OCSClient { /** * Get all the applications from the OCS server - * - * @return array|null an array of application data or null - * - * This function returns a list of all the applications on the OCS server - * @param array|string $categories + * @param array $categories * @param int $page * @param string $filter + * @return array An array of application data */ - public static function getApplications($categories, $page, $filter) { - if (!self::isAppStoreEnabled()) { - return (array()); + public function getApplications(array $categories, $page, $filter) { + if (!$this->isAppStoreEnabled()) { + return []; } - if (is_array($categories)) { - $categoriesString = implode('x', $categories); - } else { - $categoriesString = $categories; - } - - $version = '&version=' . implode('x', \OC_Util::getVersion()); - $filterUrl = '&filter=' . urlencode($filter); - $url = self::getAppStoreURL() . '/content/data?categories=' . urlencode($categoriesString) - . '&sortmode=new&page=' . urlencode($page) . '&pagesize=100' . $filterUrl . $version; - $apps = []; - - $client = \OC::$server->getHTTPClientService()->newClient(); + $client = $this->httpClientService->newClient(); try { - $response = $client->get($url, ['timeout' => 5]); + $response = $client->get( + $this->getAppStoreUrl() . '/content/data', + [ + 'timeout' => 5, + 'query' => [ + 'version' => implode('x', \OC_Util::getVersion()), + 'filter' => $filter, + 'categories' => implode('x', $categories), + 'sortmode' => 'new', + 'page' => $page, + 'pagesize' => 100, + 'approved' => $filter + ], + ] + ); } catch(\Exception $e) { - return null; - } - - if($response->getStatusCode() !== 200) { - return null; + $this->logger->error( + sprintf('Could not get applications: %s', $e->getMessage()), + [ + 'app' => 'core', + ] + ); + return []; } $loadEntities = libxml_disable_entity_loader(true); - $data = simplexml_load_string($response->getBody()); + $data = @simplexml_load_string($response->getBody()); libxml_disable_entity_loader($loadEntities); + if($data === false) { + $this->logger->error( + 'Could not get applications, content was no valid XML', + [ + 'app' => 'core', + ] + ); + return []; + } + $tmp = $data->data->content; $tmpCount = count($tmp); + + $apps = []; for ($i = 0; $i < $tmpCount; $i++) { - $app = array(); + $app = []; $app['id'] = (string)$tmp[$i]->id; $app['name'] = (string)$tmp[$i]->name; $app['label'] = (string)$tmp[$i]->label; @@ -167,9 +210,11 @@ class OCSClient { $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; } @@ -182,84 +227,111 @@ class OCSClient { * * This function returns an applications from the OCS server */ - public static function getApplication($id) { - if (!self::isAppStoreEnabled()) { - return null; - } - $url = self::getAppStoreURL() . '/content/data/' . urlencode($id); - $client = \OC::$server->getHTTPClientService()->newClient(); - try { - $response = $client->get($url, ['timeout' => 5]); - } catch(\Exception $e) { + public function getApplication($id) { + if (!$this->isAppStoreEnabled()) { return null; } - if($response->getStatusCode() !== 200) { + $client = $this->httpClientService->newClient(); + try { + $response = $client->get( + $this->getAppStoreUrl() . '/content/data/' . urlencode($id), + [ + 'timeout' => 5, + ] + ); + } catch(\Exception $e) { + $this->logger->error( + sprintf('Could not get application: %s', $e->getMessage()), + [ + 'app' => 'core', + ] + ); return null; } $loadEntities = libxml_disable_entity_loader(true); - $data = simplexml_load_string($response->getBody()); + $data = @simplexml_load_string($response->getBody()); libxml_disable_entity_loader($loadEntities); - $tmp = $data->data->content; - if (is_null($tmp)) { - \OC_Log::write('core', 'Invalid OCS content returned for app ' . $id, \OC_Log::FATAL); + if($data === false) { + $this->logger->error( + 'Could not get application, content was no valid XML', + [ + 'app' => 'core', + ] + ); return null; } + + $tmp = $data->data->content; + $app = []; - $app['id'] = $tmp->id; - $app['name'] = $tmp->name; - $app['version'] = $tmp->version; - $app['type'] = $tmp->typeid; - $app['label'] = $tmp->label; - $app['typename'] = $tmp->typename; - $app['personid'] = $tmp->personid; - $app['detailpage'] = $tmp->detailpage; - $app['preview1'] = $tmp->smallpreviewpic1; - $app['preview2'] = $tmp->smallpreviewpic2; - $app['preview3'] = $tmp->smallpreviewpic3; + $app['id'] = (int)$tmp->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['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'] = $tmp->description; - $app['detailpage'] = $tmp->detailpage; - $app['score'] = $tmp->score; + $app['description'] = (string)$tmp->description; + $app['detailpage'] = (string)$tmp->detailpage; + $app['score'] = (int)$tmp->score; return $app; } /** * Get the download url for an application from the OCS server - * + * @param $id * @return array|null an array of application data or null - * - * This function returns an download url for an applications from the OCS server - * @param string $id - * @param integer $item */ - public static function getApplicationDownload($id, $item) { - if (!self::isAppStoreEnabled()) { + public function getApplicationDownload($id) { + if (!$this->isAppStoreEnabled()) { return null; } - $url = self::getAppStoreURL() . '/content/download/' . urlencode($id) . '/' . urlencode($item); - $client = \OC::$server->getHTTPClientService()->newClient(); + $url = $this->getAppStoreUrl() . '/content/download/' . urlencode($id) . '/1'; + $client = $this->httpClientService->newClient(); try { - $response = $client->get($url, ['timeout' => 5]); + $response = $client->get( + $url, + [ + 'timeout' => 5, + ] + ); } catch(\Exception $e) { - return null; - } - - if($response->getStatusCode() !== 200) { + $this->logger->error( + sprintf('Could not get application download URL: %s', $e->getMessage()), + [ + 'app' => 'core', + ] + ); return null; } $loadEntities = libxml_disable_entity_loader(true); - $data = simplexml_load_string($response->getBody()); + $data = @simplexml_load_string($response->getBody()); libxml_disable_entity_loader($loadEntities); + if($data === false) { + $this->logger->error( + 'Could not get application download URL, content was no valid XML', + [ + 'app' => 'core', + ] + ); + return null; + } + $tmp = $data->data->content; - $app = array(); + $app = []; if (isset($tmp->downloadlink)) { - $app['downloadlink'] = $tmp->downloadlink; + $app['downloadlink'] = (string)$tmp->downloadlink; } else { $app['downloadlink'] = ''; } diff --git a/lib/private/server.php b/lib/private/server.php index 8c5169f229..cfdbd800a7 100644 --- a/lib/private/server.php +++ b/lib/private/server.php @@ -391,6 +391,13 @@ class Server extends SimpleContainer implements IServerContainer { new \OC_Defaults() ); }); + $this->registerService('OcsClient', function(Server $c) { + return new OCSClient( + $this->getHTTPClientService(), + $this->getConfig(), + $this->getLogger() + ); + }); } /** @@ -836,6 +843,13 @@ class Server extends SimpleContainer implements IServerContainer { return $this->webRoot; } + /** + * @return \OC\OCSClient + */ + public function getOcsClient() { + return $this->query('OcsClient'); + } + /** * @return \OCP\IDateTimeZone */ diff --git a/lib/private/templatelayout.php b/lib/private/templatelayout.php index ee1412fba7..448276ca7f 100644 --- a/lib/private/templatelayout.php +++ b/lib/private/templatelayout.php @@ -107,7 +107,7 @@ class OC_TemplateLayout extends OC_Template { $userDisplayName = OC_User::getDisplayName(); $this->assign('user_displayname', $userDisplayName); $this->assign('user_uid', OC_User::getUser()); - $this->assign('appsmanagement_active', strpos(\OC::$server->getRequest()->getRequestUri(), OC_Helper::linkToRoute('settings_apps')) === 0 ); + $this->assign('appsmanagement_active', strpos(\OC::$server->getRequest()->getRequestUri(), \OC::$server->getURLGenerator()->linkToRoute('settings.AppSettings.viewApps')) === 0 ); $this->assign('enableAvatars', $this->config->getSystemValue('enable_avatars', true)); $this->assign('userAvatarSet', \OC_Helper::userAvatarSet(OC_User::getUser())); } else if ($renderAs == 'error') { diff --git a/lib/public/app/iappmanager.php b/lib/public/app/iappmanager.php index f50a7f6417..69b8c335d6 100644 --- a/lib/public/app/iappmanager.php +++ b/lib/public/app/iappmanager.php @@ -78,4 +78,9 @@ interface IAppManager { * @return string[] */ public function getInstalledApps(); + + /** + * Clear the cached list of apps when enabling/disabling an app + */ + public function clearAppsCache(); } diff --git a/settings/application.php b/settings/application.php index b459603796..07a458d249 100644 --- a/settings/application.php +++ b/settings/application.php @@ -71,7 +71,10 @@ class Application extends App { $c->query('Request'), $c->query('L10N'), $c->query('Config'), - $c->query('ICacheFactory') + $c->query('ICacheFactory'), + $c->query('INavigationManager'), + $c->query('IAppManager'), + $c->query('OcsClient') ); }); $container->registerService('SecuritySettingsController', function(IContainer $c) { @@ -191,6 +194,15 @@ class Application extends App { $container->registerService('ClientService', function(IContainer $c) { return $c->query('ServerContainer')->getHTTPClientService(); }); + $container->registerService('INavigationManager', function(IContainer $c) { + return $c->query('ServerContainer')->getNavigationManager(); + }); + $container->registerService('IAppManager', function(IContainer $c) { + return $c->query('ServerContainer')->getAppManager(); + }); + $container->registerService('OcsClient', function(IContainer $c) { + return $c->query('ServerContainer')->getOcsClient(); + }); $container->registerService('Util', function(IContainer $c) { return new \OC_Util(); }); diff --git a/settings/apps.php b/settings/apps.php deleted file mode 100644 index 7245b6610e..0000000000 --- a/settings/apps.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @author Frank Karlitschek - * @author Jan-Christoph Borchardt - * @author Lukas Reschke - * @author Morris Jobke - * @author Robin Appelman - * @author Thomas Müller - * - * @copyright Copyright (c) 2015, ownCloud, Inc. - * @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 - * - */ - -OC_Util::checkAdminUser(); -\OC::$server->getSession()->close(); - -// Load the files we need -\OC_Util::addVendorScript('handlebars/handlebars'); -\OCP\Util::addScript("settings", "settings"); -\OCP\Util::addStyle("settings", "settings"); -\OC_Util::addVendorScript('select2/select2'); -\OC_Util::addVendorStyle('select2/select2'); -\OCP\Util::addScript("settings", "apps"); -\OC_App::setActiveNavigationEntry( "core_apps" ); - -$tmpl = new OC_Template( "settings", "apps", "user" ); -$tmpl->printPage(); - diff --git a/settings/controller/appsettingscontroller.php b/settings/controller/appsettingscontroller.php index 9a85f6d3b9..f1b62bb1d3 100644 --- a/settings/controller/appsettingscontroller.php +++ b/settings/controller/appsettingscontroller.php @@ -27,8 +27,13 @@ namespace OC\Settings\Controller; 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\TemplateResponse; use OCP\ICacheFactory; +use OCP\INavigationManager; use OCP\IRequest; use OCP\IL10N; use OCP\IConfig; @@ -44,6 +49,12 @@ class AppSettingsController extends Controller { private $config; /** @var \OCP\ICache */ private $cache; + /** @var INavigationManager */ + private $navigationManager; + /** @var IAppManager */ + private $appManager; + /** @var OCSClient */ + private $ocsClient; /** * @param string $appName @@ -51,16 +62,53 @@ class AppSettingsController extends Controller { * @param IL10N $l10n * @param IConfig $config * @param ICacheFactory $cache + * @param INavigationManager $navigationManager + * @param IAppManager $appManager + * @param OCSClient $ocsClient */ public function __construct($appName, IRequest $request, IL10N $l10n, IConfig $config, - ICacheFactory $cache) { + ICacheFactory $cache, + INavigationManager $navigationManager, + IAppManager $appManager, + OCSClient $ocsClient) { 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(); + } + + /** + * @NoCSRFRequired + * @return TemplateResponse + */ + public function viewApps() { + $params = []; + $params['experimentalEnabled'] = $this->config->getSystemValue('appstore.experimental.enabled', false); + $this->navigationManager->setActiveEntry('core_apps'); + + $templateResponse = new TemplateResponse($this->appName, 'apps', $params, 'user'); + $policy = new ContentSecurityPolicy(); + $policy->addAllowedImageDomain('https://apps.owncloud.com'); + $templateResponse->setContentSecurityPolicy($policy); + + return $templateResponse; } /** @@ -77,16 +125,15 @@ class AppSettingsController extends Controller { ['id' => 1, 'displayName' => (string)$this->l10n->t('Not enabled')], ]; - if(OCSClient::isAppStoreEnabled()) { - $categories[] = ['id' => 2, 'displayName' => (string)$this->l10n->t('Recommended')]; + if($this->ocsClient->isAppStoreEnabled()) { // apps from external repo via OCS - $ocs = OCSClient::getCategories(); + $ocs = $this->ocsClient->getCategories(); if ($ocs) { foreach($ocs as $k => $v) { - $categories[] = array( + $categories[] = [ 'id' => $k, 'displayName' => str_replace('ownCloud ', '', $v) - ); + ]; } } } @@ -97,7 +144,8 @@ class AppSettingsController extends Controller { } /** - * Get all available categories + * Get all available apps in a category + * * @param int $category * @return array */ @@ -134,16 +182,9 @@ class AppSettingsController extends Controller { }); break; default: - if ($category === 2) { - $apps = \OC_App::getAppstoreApps('approved'); - if ($apps) { - $apps = array_filter($apps, function ($app) { - return isset($app['internalclass']) && $app['internalclass'] === 'recommendedapp'; - }); - } - } else { - $apps = \OC_App::getAppstoreApps('approved', $category); - } + $filter = $this->config->getSystemValue('appstore.experimental.enabled', false) ? 'all' : 'approved'; + + $apps = \OC_App::getAppstoreApps($filter, $category); if (!$apps) { $apps = array(); } else { diff --git a/settings/css/settings.css b/settings/css/settings.css index c619bd7b9b..eb6b0f5405 100644 --- a/settings/css/settings.css +++ b/settings/css/settings.css @@ -210,6 +210,24 @@ span.version { margin-left:1em; margin-right:1em; color:#555; } opacity: .5; } +.app-level { + color: white; +} + +.app-level .official, .app-level .approved { + background-color: #E8C805; + border-radius: 2px; + margin-left: 5px; + padding: 3px; +} + +.app-level .experimental { + background-color: #F02405; + border-radius: 2px; + margin-left: 5px; + padding: 3px; +} + #apps-list { position: relative; height: 100%; @@ -236,6 +254,7 @@ span.version { margin-left:1em; margin-right:1em; color:#555; } .app-name, .app-version, .app-score, +.app-level, .recommendedapp { display: inline-block; } @@ -261,7 +280,7 @@ span.version { margin-left:1em; margin-right:1em; color:#555; } white-space: pre-line; } -#app-category-2 { +#app-category-1 { border-bottom: 1px solid #e8e8e8; } diff --git a/settings/js/apps.js b/settings/js/apps.js index 3db84e8acd..f54611369b 100644 --- a/settings/js/apps.js +++ b/settings/js/apps.js @@ -9,6 +9,17 @@ Handlebars.registerHelper('score', function() { } return new Handlebars.SafeString(''); }); +Handlebars.registerHelper('level', function() { + if(typeof this.level !== 'undefined') { + if(this.level === 200) { + return new Handlebars.SafeString('Official'); + } else if(this.level === 100) { + return new Handlebars.SafeString('Approved'); + } else { + return new Handlebars.SafeString('Experimental'); + } + } +}); OC.Settings = OC.Settings || {}; OC.Settings.Apps = OC.Settings.Apps || { @@ -73,7 +84,6 @@ OC.Settings.Apps = OC.Settings.Apps || { this._loadCategoryCall = $.ajax(OC.generateUrl('settings/apps/list?category={categoryId}', { categoryId: categoryId }), { - data:{}, type:'GET', success: function (apps) { OC.Settings.Apps.State.apps = _.indexBy(apps.apps, 'id'); @@ -81,13 +91,27 @@ OC.Settings.Apps = OC.Settings.Apps || { var template = Handlebars.compile(source); if (apps.apps.length) { + apps.apps.sort(function(a,b) { + return b.level - a.level; + }); + + var firstExperimental = false; _.each(apps.apps, function(app) { - OC.Settings.Apps.renderApp(app, template, null); + if(app.level === 0 && firstExperimental === false) { + firstExperimental = true; + OC.Settings.Apps.renderApp(app, template, null, true); + } else { + OC.Settings.Apps.renderApp(app, template, null, false); + } }); } else { $('#apps-list').addClass('hidden'); $('#apps-list-empty').removeClass('hidden'); } + + $('.app-level .official').tipsy({fallback: t('core', 'Official apps are developed by and within the ownCloud community and its GitHub repository and offer functionality central to ownCloud. They are ready for serious use.')}); + $('.app-level .approved').tipsy({fallback: t('core', 'Approved apps are developed by trusted developers and have passed a cursory security check. They are actively maintained in an open code repository and their maintainers deem them to be stable for casual to normal use.')}); + $('.app-level .experimental').tipsy({fallback: t('core', 'This app is not checked for security issues and is new or known to be unstable. Install on your own risk.')}); }, complete: function() { $('#apps-list').removeClass('icon-loading'); @@ -95,7 +119,7 @@ OC.Settings.Apps = OC.Settings.Apps || { }); }, - renderApp: function(app, template, selector) { + renderApp: function(app, template, selector, firstExperimental) { if (!template) { var source = $("#app-template").html(); template = Handlebars.compile(source); @@ -103,6 +127,7 @@ OC.Settings.Apps = OC.Settings.Apps || { if (typeof app === 'string') { app = OC.Settings.Apps.State.apps[app]; } + app.firstExperimental = firstExperimental; var html = template(app); if (selector) { @@ -438,6 +463,16 @@ OC.Settings.Apps = OC.Settings.Apps || { $select.change(); }); + $(document).on('click', '#enable-experimental-apps', function () { + var state = $('#enable-experimental-apps').prop('checked'); + $.ajax(OC.generateUrl('settings/apps/experimental'), { + data: {state: state}, + type: 'POST', + success:function () { + location.reload(); + } + }); + }); } }; diff --git a/settings/routes.php b/settings/routes.php index 5a069e5a1c..86b7fa2375 100644 --- a/settings/routes.php +++ b/settings/routes.php @@ -33,25 +33,27 @@ namespace OC\Settings; $application = new Application(); -$application->registerRoutes($this, array( - 'resources' => array( - 'groups' => array('url' => '/settings/users/groups'), - 'users' => array('url' => '/settings/users/users') - ), - 'routes' => array( - array('name' => 'MailSettings#setMailSettings', 'url' => '/settings/admin/mailsettings', 'verb' => 'POST'), - array('name' => 'MailSettings#storeCredentials', 'url' => '/settings/admin/mailsettings/credentials', 'verb' => 'POST'), - array('name' => 'MailSettings#sendTestMail', 'url' => '/settings/admin/mailtest', 'verb' => 'POST'), - array('name' => 'AppSettings#listCategories', 'url' => '/settings/apps/categories', 'verb' => 'GET'), - array('name' => 'AppSettings#listApps', 'url' => '/settings/apps/list', 'verb' => 'GET'), - array('name' => 'SecuritySettings#trustedDomains', 'url' => '/settings/admin/security/trustedDomains', 'verb' => 'POST'), - array('name' => 'Users#setMailAddress', 'url' => '/settings/users/{id}/mailAddress', 'verb' => 'PUT'), - array('name' => 'LogSettings#setLogLevel', 'url' => '/settings/admin/log/level', 'verb' => 'POST'), - array('name' => 'LogSettings#getEntries', 'url' => '/settings/admin/log/entries', 'verb' => 'GET'), - array('name' => 'LogSettings#download', 'url' => '/settings/admin/log/download', 'verb' => 'GET'), +$application->registerRoutes($this, [ + 'resources' => [ + 'groups' => ['url' => '/settings/users/groups'], + 'users' => ['url' => '/settings/users/users'] + ], + 'routes' => [ + ['name' => 'MailSettings#setMailSettings', 'url' => '/settings/admin/mailsettings', 'verb' => 'POST'], + ['name' => 'MailSettings#storeCredentials', 'url' => '/settings/admin/mailsettings/credentials', 'verb' => 'POST'], + ['name' => 'MailSettings#sendTestMail', 'url' => '/settings/admin/mailtest', 'verb' => 'POST'], + ['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#setMailAddress', 'url' => '/settings/users/{id}/mailAddress', 'verb' => 'PUT'], + ['name' => 'LogSettings#setLogLevel', 'url' => '/settings/admin/log/level', 'verb' => 'POST'], + ['name' => 'LogSettings#getEntries', 'url' => '/settings/admin/log/entries', 'verb' => 'GET'], + ['name' => 'LogSettings#download', 'url' => '/settings/admin/log/download', 'verb' => 'GET'], ['name' => 'CheckSetup#check', 'url' => '/settings/ajax/checksetup', 'verb' => 'GET'], - ) -)); + ] +]); /** @var $this \OCP\Route\IRouter */ @@ -62,8 +64,6 @@ $this->create('settings_personal', '/settings/personal') ->actionInclude('settings/personal.php'); $this->create('settings_users', '/settings/users') ->actionInclude('settings/users.php'); -$this->create('settings_apps', '/settings/apps') - ->actionInclude('settings/apps.php'); $this->create('settings_admin', '/settings/admin') ->actionInclude('settings/admin.php'); // Settings ajax actions diff --git a/settings/templates/apps.php b/settings/templates/apps.php index a2fe5d9b63..f930ce6d44 100644 --- a/settings/templates/apps.php +++ b/settings/templates/apps.php @@ -1,3 +1,27 @@ +