Merge pull request #9565 from nextcloud/feature/noid/app-settings

Migrate apps management to Vue.js
This commit is contained in:
Morris Jobke 2018-06-06 18:05:04 +02:00 committed by GitHub
commit b49f8e43bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 5149 additions and 2000 deletions

View File

@ -323,6 +323,7 @@ class AppManager implements IAppManager {
public function clearAppsCache() {
$settingsMemCache = $this->memCacheFactory->createDistributed('settings');
$settingsMemCache->clear('listApps');
$this->appInfos = [];
}
/**

View File

@ -887,6 +887,7 @@ class OC_App {
}
self::registerAutoloading($appId, $appPath);
\OC::$server->getAppManager()->clearAppsCache();
$appData = self::getAppInfo($appId);
self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
@ -900,6 +901,7 @@ class OC_App {
self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']);
// update appversion in app manager
\OC::$server->getAppManager()->clearAppsCache();
\OC::$server->getAppManager()->getAppVersion($appId, false);
// run upgrade code

View File

@ -100,6 +100,7 @@ interface IAppManager {
*
* @param string $appId
* @param \OCP\IGroup[] $groups
* @throws \Exception
* @since 8.0.0
*/
public function enableAppForGroups($appId, $groups);

View File

@ -38,11 +38,14 @@ use OC\App\AppStore\Version\VersionParser;
use OC\App\DependencyAnalyzer;
use OC\App\Platform;
use OC\Installer;
use OC_App;
use OCP\App\IAppManager;
use \OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\ILogger;
use OCP\INavigationManager;
use OCP\IRequest;
use OCP\IL10N;
@ -54,11 +57,6 @@ use OCP\L10N\IFactory;
* @package OC\Settings\Controller
*/
class AppSettingsController extends Controller {
const CAT_ENABLED = 0;
const CAT_DISABLED = 1;
const CAT_ALL_INSTALLED = 2;
const CAT_APP_BUNDLES = 3;
const CAT_UPDATES = 4;
/** @var \OCP\IL10N */
private $l10n;
@ -80,6 +78,11 @@ class AppSettingsController extends Controller {
private $installer;
/** @var IURLGenerator */
private $urlGenerator;
/** @var ILogger */
private $logger;
/** @var array */
private $allApps = [];
/**
* @param string $appName
@ -94,6 +97,7 @@ class AppSettingsController extends Controller {
* @param BundleFetcher $bundleFetcher
* @param Installer $installer
* @param IURLGenerator $urlGenerator
* @param ILogger $logger
*/
public function __construct(string $appName,
IRequest $request,
@ -106,7 +110,8 @@ class AppSettingsController extends Controller {
IFactory $l10nFactory,
BundleFetcher $bundleFetcher,
Installer $installer,
IURLGenerator $urlGenerator) {
IURLGenerator $urlGenerator,
ILogger $logger) {
parent::__construct($appName, $request);
$this->l10n = $l10n;
$this->config = $config;
@ -118,26 +123,25 @@ class AppSettingsController extends Controller {
$this->bundleFetcher = $bundleFetcher;
$this->installer = $installer;
$this->urlGenerator = $urlGenerator;
$this->logger = $logger;
}
/**
* @NoCSRFRequired
*
* @param string $category
* @return TemplateResponse
*/
public function viewApps($category = '') {
if ($category === '') {
$category = 'installed';
}
public function viewApps(): TemplateResponse {
\OC_Util::addScript('settings', 'apps');
\OC_Util::addVendorScript('core', 'marked/marked.min');
$params = [];
$params['category'] = $category;
$params['appstoreEnabled'] = $this->config->getSystemValue('appstoreenabled', true) === true;
$params['urlGenerator'] = $this->urlGenerator;
$params['updateCount'] = count($this->getAppsWithUpdates());
$params['developerDocumentation'] = $this->urlGenerator->linkToDocs('developer-manual');
$params['bundles'] = $this->getBundles();
$this->navigationManager->setActiveEntry('core_apps');
$templateResponse = new TemplateResponse($this->appName, 'apps', $params, 'user');
$templateResponse = new TemplateResponse('settings', 'settings', ['serverData' => $params]);
$policy = new ContentSecurityPolicy();
$policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com');
$templateResponse->setContentSecurityPolicy($policy);
@ -145,17 +149,45 @@ class AppSettingsController extends Controller {
return $templateResponse;
}
private function getAppsWithUpdates() {
$appClass = new \OC_App();
$apps = $appClass->listAllApps();
foreach($apps as $key => $app) {
$newVersion = $this->installer->isUpdateAvailable($app['id']);
if($newVersion === false) {
unset($apps[$key]);
}
}
return $apps;
}
private function getBundles() {
$result = [];
$bundles = $this->bundleFetcher->getBundles();
foreach ($bundles as $bundle) {
$result[] = [
'name' => $bundle->getName(),
'id' => $bundle->getIdentifier(),
'appIdentifiers' => $bundle->getAppIdentifiers()
];
}
return $result;
}
/**
* Get all available categories
*
* @return JSONResponse
*/
public function listCategories(): JSONResponse {
return new JSONResponse($this->getAllCategories());
}
private function getAllCategories() {
$currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2);
$updateCount = count($this->getAppsWithUpdates());
$formattedCategories = [
['id' => self::CAT_ALL_INSTALLED, 'ident' => 'installed', 'displayName' => (string)$this->l10n->t('Your apps')],
['id' => self::CAT_UPDATES, 'ident' => 'updates', 'displayName' => (string)$this->l10n->t('Updates'), 'counter' => $updateCount],
['id' => self::CAT_ENABLED, 'ident' => 'enabled', 'displayName' => (string)$this->l10n->t('Enabled apps')],
['id' => self::CAT_DISABLED, 'ident' => 'disabled', 'displayName' => (string)$this->l10n->t('Disabled apps')],
['id' => self::CAT_APP_BUNDLES, 'ident' => 'app-bundles', 'displayName' => (string)$this->l10n->t('App bundles')],
];
$formattedCategories = [];
$categories = $this->categoryFetcher->get();
foreach($categories as $category) {
$formattedCategories[] = [
@ -168,39 +200,117 @@ class AppSettingsController extends Controller {
return $formattedCategories;
}
private function fetchApps() {
$appClass = new \OC_App();
$apps = $appClass->listAllApps();
foreach ($apps as $app) {
$app['installed'] = true;
$this->allApps[$app['id']] = $app;
}
$apps = $this->getAppsForCategory('');
foreach ($apps as $app) {
$app['appstore'] = true;
if (!array_key_exists($app['id'], $this->allApps)) {
$this->allApps[$app['id']] = $app;
} else {
$this->allApps[$app['id']] = array_merge($this->allApps[$app['id']], $app);
}
}
// add bundle information
$bundles = $this->bundleFetcher->getBundles();
foreach($bundles as $bundle) {
foreach($bundle->getAppIdentifiers() as $identifier) {
foreach($this->allApps as &$app) {
if($app['id'] === $identifier) {
$app['bundleId'] = $bundle->getIdentifier();
continue;
}
}
}
}
}
private function getAllApps() {
return $this->allApps;
}
/**
* Get all available categories
* Get all available apps in a category
*
* @param string $category
* @return JSONResponse
* @throws \Exception
*/
public function listCategories() {
return new JSONResponse($this->getAllCategories());
public function listApps(): JSONResponse {
$this->fetchApps();
$apps = $this->getAllApps();
$dependencyAnalyzer = new DependencyAnalyzer(new Platform($this->config), $this->l10n);
// Extend existing app details
$apps = array_map(function($appData) use ($dependencyAnalyzer) {
$appstoreData = $appData['appstoreData'];
$appData['screenshot'] = isset($appstoreData['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/'.base64_encode($appstoreData['screenshots'][0]['url']) : '';
$newVersion = $this->installer->isUpdateAvailable($appData['id']);
if($newVersion && $this->appManager->isInstalled($appData['id'])) {
$appData['update'] = $newVersion;
}
// fix groups to be an array
$groups = array();
if (is_string($appData['groups'])) {
$groups = json_decode($appData['groups']);
}
$appData['groups'] = $groups;
$appData['canUnInstall'] = !$appData['active'] && $appData['removable'];
// fix licence vs license
if (isset($appData['license']) && !isset($appData['licence'])) {
$appData['licence'] = $appData['license'];
}
// analyse dependencies
$missing = $dependencyAnalyzer->analyze($appData);
$appData['canInstall'] = empty($missing);
$appData['missingDependencies'] = $missing;
$appData['missingMinOwnCloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['min-version']);
$appData['missingMaxOwnCloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['max-version']);
return $appData;
}, $apps);
usort($apps, [$this, 'sortApps']);
return new JSONResponse(['apps' => $apps, 'status' => 'success']);
}
/**
* Get all apps for a category
* Get all apps for a category from the app store
*
* @param string $requestedCategory
* @return array
* @throws \Exception
*/
private function getAppsForCategory($requestedCategory) {
private function getAppsForCategory($requestedCategory = ''): array {
$versionParser = new VersionParser();
$formattedApps = [];
$apps = $this->appFetcher->get();
foreach($apps as $app) {
if (isset($app['isFeatured'])) {
$app['featured'] = $app['isFeatured'];
}
// Skip all apps not in the requested category
$isInCategory = false;
foreach($app['categories'] as $category) {
if($category === $requestedCategory) {
$isInCategory = true;
if ($requestedCategory !== '') {
$isInCategory = false;
foreach($app['categories'] as $category) {
if($category === $requestedCategory) {
$isInCategory = true;
}
}
if(!$isInCategory) {
continue;
}
}
if(!$isInCategory) {
continue;
}
$nextCloudVersion = $versionParser->getVersion($app['releases'][0]['rawPlatformVersionSpec']);
@ -240,7 +350,7 @@ class AppSettingsController extends Controller {
$currentVersion = '';
if($this->appManager->isInstalled($app['id'])) {
$currentVersion = \OC_App::getAppVersion($app['id']);
$currentVersion = $this->appManager->getAppVersion($app['id']);
} else {
$currentLanguage = $app['releases'][0]['version'];
}
@ -249,6 +359,7 @@ class AppSettingsController extends Controller {
'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'],
'summary' => isset($app['translations'][$currentLanguage]['summary']) ? $app['translations'][$currentLanguage]['summary'] : $app['translations']['en']['summary'],
'license' => $app['releases'][0]['licenses'],
'author' => $authors,
'shipped' => false,
@ -267,208 +378,168 @@ class AppSettingsController extends Controller {
$nextCloudVersionDependencies,
$phpDependencies
),
'level' => ($app['featured'] === true) ? 200 : 100,
'level' => ($app['isFeatured'] === true) ? 200 : 100,
'missingMaxOwnCloudVersion' => false,
'missingMinOwnCloudVersion' => false,
'canInstall' => true,
'preview' => isset($app['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/'.base64_encode($app['screenshots'][0]['url']) : '',
'screenshot' => isset($app['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/'.base64_encode($app['screenshots'][0]['url']) : '',
'score' => $app['ratingOverall'],
'ratingNumOverall' => $app['ratingNumOverall'],
'ratingNumThresholdReached' => $app['ratingNumOverall'] > 5 ? true : false,
'ratingNumThresholdReached' => $app['ratingNumOverall'] > 5,
'removable' => $existsLocally,
'active' => $this->appManager->isEnabledForUser($app['id']),
'needsDownload' => !$existsLocally,
'groups' => $groups,
'fromAppStore' => true,
'appstoreData' => $app,
];
$newVersion = $this->installer->isUpdateAvailable($app['id']);
if($newVersion && $this->appManager->isInstalled($app['id'])) {
$formattedApps[count($formattedApps)-1]['update'] = $newVersion;
}
}
return $formattedApps;
}
private function getAppsWithUpdates() {
$appClass = new \OC_App();
$apps = $appClass->listAllApps();
foreach($apps as $key => $app) {
$newVersion = $this->installer->isUpdateAvailable($app['id']);
if($newVersion !== false) {
$apps[$key]['update'] = $newVersion;
} else {
unset($apps[$key]);
}
}
usort($apps, function ($a, $b) {
$a = (string)$a['name'];
$b = (string)$b['name'];
if ($a === $b) {
return 0;
}
return ($a < $b) ? -1 : 1;
});
return $apps;
/**
* @PasswordConfirmationRequired
*
* @param string $appId
* @param array $groups
* @return JSONResponse
*/
public function enableApp(string $appId, array $groups = []): JSONResponse {
return $this->enableApps([$appId], $groups);
}
/**
* Get all available apps in a category
* Enable one or more apps
*
* @param string $category
* apps will be enabled for specific groups only if $groups is defined
*
* @PasswordConfirmationRequired
* @param array $appIds
* @param array $groups
* @return JSONResponse
*/
public function listApps($category = '') {
$appClass = new \OC_App();
public function enableApps(array $appIds, array $groups = []): JSONResponse {
try {
$updateRequired = false;
switch ($category) {
// installed apps
case 'installed':
$apps = $appClass->listAllApps();
foreach ($appIds as $appId) {
$appId = OC_App::cleanAppId($appId);
foreach($apps as $key => $app) {
$newVersion = $this->installer->isUpdateAvailable($app['id']);
$apps[$key]['update'] = $newVersion;
// Check if app is already downloaded
/** @var Installer $installer */
$installer = \OC::$server->query(Installer::class);
$isDownloaded = $installer->isDownloaded($appId);
if(!$isDownloaded) {
$installer->downloadApp($appId);
}
usort($apps, function ($a, $b) {
$a = (string)$a['name'];
$b = (string)$b['name'];
if ($a === $b) {
return 0;
}
return ($a < $b) ? -1 : 1;
});
break;
// updates
case 'updates':
$apps = $this->getAppsWithUpdates();
break;
// enabled apps
case 'enabled':
$apps = $appClass->listAllApps();
$apps = array_filter($apps, function ($app) {
return $app['active'];
});
$installer->installApp($appId);
foreach($apps as $key => $app) {
$newVersion = $this->installer->isUpdateAvailable($app['id']);
$apps[$key]['update'] = $newVersion;
if (count($groups) > 0) {
$this->appManager->enableAppForGroups($appId, $this->getGroupList($groups));
} else {
$this->appManager->enableApp($appId);
}
usort($apps, function ($a, $b) {
$a = (string)$a['name'];
$b = (string)$b['name'];
if ($a === $b) {
return 0;
}
return ($a < $b) ? -1 : 1;
});
break;
// disabled apps
case 'disabled':
$apps = $appClass->listAllApps();
$apps = array_filter($apps, function ($app) {
return !$app['active'];
});
$apps = array_map(function ($app) {
$newVersion = $this->installer->isUpdateAvailable($app['id']);
if ($newVersion !== false) {
$app['update'] = $newVersion;
}
return $app;
}, $apps);
usort($apps, function ($a, $b) {
$a = (string)$a['name'];
$b = (string)$b['name'];
if ($a === $b) {
return 0;
}
return ($a < $b) ? -1 : 1;
});
break;
case 'app-bundles':
$bundles = $this->bundleFetcher->getBundles();
$apps = [];
foreach($bundles as $bundle) {
$newCategory = true;
$allApps = $appClass->listAllApps();
$categories = $this->getAllCategories();
foreach($categories as $singleCategory) {
$newApps = $this->getAppsForCategory($singleCategory['id']);
foreach($allApps as $app) {
foreach($newApps as $key => $newApp) {
if($app['id'] === $newApp['id']) {
unset($newApps[$key]);
}
}
}
$allApps = array_merge($allApps, $newApps);
}
foreach($bundle->getAppIdentifiers() as $identifier) {
foreach($allApps as $app) {
if($app['id'] === $identifier) {
if($newCategory) {
$app['newCategory'] = true;
$app['categoryName'] = $bundle->getName();
}
$app['bundleId'] = $bundle->getIdentifier();
$newCategory = false;
$apps[] = $app;
continue;
}
}
}
if (\OC_App::shouldUpgrade($appId)) {
$updateRequired = true;
}
break;
default:
$apps = $this->getAppsForCategory($category);
}
return new JSONResponse(['data' => ['update_required' => $updateRequired]]);
// 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;
} catch (\Exception $e) {
$this->logger->logException($e);
return new JSONResponse(['data' => ['message' => $e->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
private function getGroupList(array $groups) {
$groupManager = \OC::$server->getGroupManager();
$groupsList = [];
foreach ($groups as $group) {
$groupItem = $groupManager->get($group);
if ($groupItem instanceof \OCP\IGroup) {
$groupsList[] = $groupManager->get($group);
}
}
return $groupsList;
}
/**
* @PasswordConfirmationRequired
*
* @param string $appId
* @return JSONResponse
*/
public function disableApp(string $appId): JSONResponse {
return $this->disableApps([$appId]);
}
/**
* @PasswordConfirmationRequired
*
* @param array $appIds
* @return JSONResponse
*/
public function disableApps(array $appIds): JSONResponse {
try {
foreach ($appIds as $appId) {
$appId = OC_App::cleanAppId($appId);
$this->appManager->disableApp($appId);
}
return new JSONResponse([]);
} catch (\Exception $e) {
$this->logger->logException($e);
return new JSONResponse(['data' => ['message' => $e->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
/**
* @PasswordConfirmationRequired
*
* @param string $appId
* @return JSONResponse
*/
public function uninstallApp(string $appId): JSONResponse {
$appId = OC_App::cleanAppId($appId);
$result = $this->installer->removeApp($appId);
if($result !== false) {
$this->appManager->clearAppsCache();
return new JSONResponse(['data' => ['appid' => $appId]]);
}
return new JSONResponse(['data' => ['message' => $this->l10n->t('Couldn\'t remove app.')]], Http::STATUS_INTERNAL_SERVER_ERROR);
}
/**
* @param string $appId
* @return JSONResponse
*/
public function updateApp(string $appId): JSONResponse {
$appId = OC_App::cleanAppId($appId);
$this->config->setSystemValue('maintenance', true);
try {
$result = $this->installer->updateAppstoreApp($appId);
$this->config->setSystemValue('maintenance', false);
} catch (\Exception $ex) {
$this->config->setSystemValue('maintenance', false);
return new JSONResponse(['data' => ['message' => $ex->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR);
}
// fix groups to be an array
$dependencyAnalyzer = new DependencyAnalyzer(new Platform($this->config), $this->l10n);
$apps = array_map(function($app) use ($dependencyAnalyzer) {
// fix groups
$groups = array();
if (is_string($app['groups'])) {
$groups = json_decode($app['groups']);
}
$app['groups'] = $groups;
$app['canUnInstall'] = !$app['active'] && $app['removable'];
// fix licence vs license
if (isset($app['license']) && !isset($app['licence'])) {
$app['licence'] = $app['license'];
}
// analyse dependencies
$missing = $dependencyAnalyzer->analyze($app);
$app['canInstall'] = empty($missing);
$app['missingDependencies'] = $missing;
$app['missingMinOwnCloudVersion'] = !isset($app['dependencies']['nextcloud']['@attributes']['min-version']);
$app['missingMaxOwnCloudVersion'] = !isset($app['dependencies']['nextcloud']['@attributes']['max-version']);
return $app;
}, $apps);
return new JSONResponse(['apps' => $apps, 'status' => 'success']);
if ($result !== false) {
return new JSONResponse(['data' => ['appid' => $appId]]);
}
return new JSONResponse(['data' => ['message' => $this->l10n->t('Couldn\'t update app.')]], Http::STATUS_INTERNAL_SERVER_ERROR);
}
private function sortApps($a, $b) {
$a = (string)$a['name'];
$b = (string)$b['name'];
if ($a === $b) {
return 0;
}
return ($a < $b) ? -1 : 1;
}
}

View File

@ -1,44 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Kamil Domanski <kdomanski@kdemail.net>
* @author Lukas Reschke <lukas@statuscode.ch>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
\OC_JSON::checkAdminUser();
\OC_JSON::callCheck();
$lastConfirm = (int) \OC::$server->getSession()->get('last-password-confirm');
if ($lastConfirm < (time() - 30 * 60 + 15)) { // allow 15 seconds delay
$l = \OC::$server->getL10N('core');
OC_JSON::error(array( 'data' => array( 'message' => $l->t('Password confirmation is required'))));
exit();
}
if (!array_key_exists('appid', $_POST)) {
OC_JSON::error();
exit;
}
$appIds = (array)$_POST['appid'];
foreach($appIds as $appId) {
$appId = OC_App::cleanAppId($appId);
\OC::$server->getAppManager()->disableApp($appId);
}
OC_JSON::success();

View File

@ -1,61 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christopher Schäpers <kondou@ts.unde.re>
* @author Kamil Domanski <kdomanski@kdemail.net>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Robin Appelman <robin@icewind.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
use OCP\ILogger;
OC_JSON::checkAdminUser();
\OC_JSON::callCheck();
$lastConfirm = (int) \OC::$server->getSession()->get('last-password-confirm');
if ($lastConfirm < (time() - 30 * 60 + 15)) { // allow 15 seconds delay
$l = \OC::$server->getL10N('core');
OC_JSON::error(array( 'data' => array( 'message' => $l->t('Password confirmation is required'))));
exit();
}
$groups = isset($_POST['groups']) ? (array)$_POST['groups'] : [];
$appIds = isset($_POST['appIds']) ? (array)$_POST['appIds'] : [];
try {
$updateRequired = false;
foreach($appIds as $appId) {
$app = new OC_App();
$appId = OC_App::cleanAppId($appId);
$app->enable($appId, $groups);
if(\OC_App::shouldUpgrade($appId)) {
$updateRequired = true;
}
}
OC_JSON::success(['data' => ['update_required' => $updateRequired]]);
} catch (Exception $e) {
\OC::$server->getLogger()->logException($e, [
'level' => ILogger::DEBUG,
'app' => 'core',
]);
OC_JSON::error(array("data" => array("message" => $e->getMessage()) ));
}

View File

@ -1,55 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
\OC_JSON::checkAdminUser();
\OC_JSON::callCheck();
$lastConfirm = (int) \OC::$server->getSession()->get('last-password-confirm');
if ($lastConfirm < (time() - 30 * 60 + 15)) { // allow 15 seconds delay
$l = \OC::$server->getL10N('core');
OC_JSON::error(array( 'data' => array( 'message' => $l->t('Password confirmation is required'))));
exit();
}
if (!array_key_exists('appid', $_POST)) {
OC_JSON::error();
exit;
}
$appId = (string)$_POST['appid'];
$appId = OC_App::cleanAppId($appId);
// FIXME: move to controller
/** @var \OC\Installer $installer */
$installer = \OC::$server->query(\OC\Installer::class);
$result = $installer->removeApp($app);
if($result !== false) {
// FIXME: Clear the cache - move that into some sane helper method
\OC::$server->getMemCacheFactory()->createDistributed('settings')->remove('listApps-0');
\OC::$server->getMemCacheFactory()->createDistributed('settings')->remove('listApps-1');
OC_JSON::success(array('data' => array('appid' => $appId)));
} else {
$l = \OC::$server->getL10N('settings');
OC_JSON::error(array('data' => array( 'message' => $l->t("Couldn't remove app.") )));
}

View File

@ -1,58 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christopher Schäpers <kondou@ts.unde.re>
* @author Frank Karlitschek <frank@karlitschek.de>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
\OC_JSON::checkAdminUser();
\OC_JSON::callCheck();
if (!array_key_exists('appid', $_POST)) {
\OC_JSON::error(array(
'message' => 'No AppId given!'
));
return;
}
$appId = (string)$_POST['appid'];
$appId = OC_App::cleanAppId($appId);
$config = \OC::$server->getConfig();
$config->setSystemValue('maintenance', true);
try {
$installer = \OC::$server->query(\OC\Installer::class);
$result = $installer->updateAppstoreApp($appId);
$config->setSystemValue('maintenance', false);
} catch(Exception $ex) {
$config->setSystemValue('maintenance', false);
OC_JSON::error(array('data' => array( 'message' => $ex->getMessage() )));
return;
}
if($result !== false) {
OC_JSON::success(array('data' => array('appid' => $appId)));
} else {
$l = \OC::$server->getL10N('settings');
OC_JSON::error(array('data' => array( 'message' => $l->t("Couldn't update app.") )));
}

View File

@ -756,38 +756,118 @@ span.version {
opacity: .5;
}
@media (min-width: 1601px) {
#apps-list .section {
width: 22%;
box-sizing: border-box;
&:nth-child(4n) {
margin-right: 20px;
}
.app-settings-content {
#searchresults {
display: none;
}
}
#apps-list.store {
.section {
border: 0;
}
.app-name {
display: block;
margin: 5px 0;
}
.app-name, .app-image * {
cursor: pointer;
}
.app-summary {
opacity: .7;
}
.app-image-icon .icon-settings-dark {
width: 100%;
height: 150px;
background-size: 45px;
opacity: 0.5;
}
.app-score-image {
height: 14px;
}
.actions {
margin-top: 10px;
}
}
@media (min-width: 1201px) and (max-width: 1600px) {
#apps-list .section {
width: 30%;
box-sizing: border-box;
&:nth-child(3n) {
margin-right: 20px;
#app-sidebar #app-details-view {
h2 {
.icon-settings-dark,
svg {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 10px;
opacity: .7;
}
}
.app-level {
margin-bottom: 10px;
}
.app-author, .app-licence {
color: $color-text-details;
}
.app-dependencies {
margin: 10px 0;
}
.app-description p {
margin: 10px 0;
}
.close {
position: absolute;
top: 0;
right: 0;
padding: 14px;
opacity: 0.5;
z-index: 1;
}.
.actions {
display: flex;
}
}
@media (min-width: 901px) and (max-width: 1200px), (min-width: 601px) and (max-width: 770px) {
#apps-list .section {
width: 40%;
box-sizing: border-box;
&:nth-child(2n) {
margin-right: 20px;
}
@media only screen and (min-width: 1601px) {
.store .section {
width: 25%;
}
.with-app-sidebar .store .section {
width: 33%;
}
}
@media only screen and (max-width: 1600px) {
.store .section {
width: 25%;
}
.with-app-sidebar .store .section {
width: 33%;
}
}
@media only screen and (max-width: 1400px) {
.store .section {
width: 33%;
}
.with-app-sidebar .store .section {
width: 50%;
}
}
@media only screen and (max-width: 900px) {
.store .section {
width: 50%;
}
.with-app-sidebar .store .section {
width: 100%;
}
}
/* hide app version and level on narrower screens */
@media only screen and (max-width: 768px) {
.store .section {
width: 50%;
}
}
/* hide app version and level on narrower screens */
@media only screen and (max-width: 900px) {
#apps-list.installed {
.app-version, .app-level {
display: none !important;
@ -795,7 +875,7 @@ span.version {
}
}
@media only screen and (max-width: 700px) {
@media only screen and (max-width: 500px) {
#apps-list.installed .app-groups {
display: none !important;
}
@ -877,30 +957,15 @@ span.version {
list-style-position: inside;
}
/* Transition to complete width! */
.app {
&:hover, &:active {
max-width: inherit;
}
}
.appslink {
text-decoration: underline;
}
.score {
color: #666;
font-weight: bold;
font-size: 0.8em;
}
.appinfo .documentation {
margin-top: 1em;
margin-bottom: 1em;
}
#apps-list {
#apps-list, #apps-list-search {
.section {
cursor: pointer;
}
&.installed {
display: table;
width: 100%;
@ -919,6 +984,9 @@ span.version {
padding: 6px;
box-sizing: border-box;
}
&.selected {
background-color: nc-darken($color-main-background, 3%);
}
}
.groups-enable {
margin-top: 0;
@ -931,11 +999,22 @@ span.version {
height: auto;
text-align: right;
}
.app-image-icon svg {
.app-image-icon svg,
.app-image-icon .icon-settings-dark {
margin-top: 5px;
width: 20px;
height: 20px;
opacity: .5;
background-size: cover;
display: inline-block;
}
.actions {
text-align: right;
.icon-loading-small {
display: inline-block;
top: 4px;
margin-right: 10px;
}
}
}
&:not(.installed) .app-image-icon svg {
@ -946,8 +1025,6 @@ span.version {
height: 64px;
opacity: .1;
}
position: relative;
height: 100%;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
@ -957,10 +1034,7 @@ span.version {
.section {
position: relative;
flex: 0 0 auto;
margin-left: 20px;
&.apps-experimental {
flex-basis: 90%;
}
h2.app-name {
display: block;
margin: 8px 0;
@ -1016,10 +1090,6 @@ span.version {
}
}
.installed .actions {
text-align: right;
}
/* LOG */
#log {
white-space: normal;

View File

@ -1,610 +1,6 @@
/* global Handlebars */
Handlebars.registerHelper('score', function() {
if(this.score) {
var score = Math.round( this.score * 10 );
var imageName = 'rating/s' + score + '.svg';
return new Handlebars.SafeString('<img src="' + OC.imagePath('core', imageName) + '">');
}
return new Handlebars.SafeString('');
});
Handlebars.registerHelper('level', function() {
if(typeof this.level !== 'undefined') {
if(this.level === 200) {
return new Handlebars.SafeString('<span class="official icon-checkmark">' + t('settings', 'Official') + '</span>');
}
}
});
OC.Settings = OC.Settings || {};
OC.Settings.Apps = OC.Settings.Apps || {
markedOptions: {},
setupGroupsSelect: function($elements) {
OC.Settings.setupGroupsSelect($elements, {
placeholder: t('core', 'All')
});
},
State: {
currentCategory: null,
currentCategoryElements: null,
apps: null,
$updateNotification: null,
availableUpdates: 0
},
loadCategories: function() {
if (this._loadCategoriesCall) {
this._loadCategoriesCall.abort();
}
var categories = [
{displayName: t('settings', 'Your apps'), ident: 'installed', id: '0'},
{displayName: t('settings', 'Enabled apps'), ident: 'enabled', id: '1'},
{displayName: t('settings', 'Disabled apps'), ident: 'disabled', id: '2'}
];
var source = $("#categories-template").html();
var template = Handlebars.compile(source);
var html = template(categories);
$('#apps-categories').html(html);
OC.Settings.Apps.loadCategory($('#app-navigation').attr('data-category'));
this._loadCategoriesCall = $.ajax(OC.generateUrl('settings/apps/categories'), {
data:{},
type:'GET',
success:function (jsondata) {
var html = template(jsondata);
var updateCategory = $.grep(jsondata, function(element, index) {
return element.ident === 'updates'
});
$('#apps-categories').html(html);
$('#app-category-' + OC.Settings.Apps.State.currentCategory).addClass('active');
if (updateCategory.length === 1) {
OC.Settings.Apps.State.availableUpdates = updateCategory[0].counter;
OC.Settings.Apps.refreshUpdateCounter();
}
},
complete: function() {
$('#app-navigation').removeClass('icon-loading');
}
});
},
loadCategory: function(categoryId) {
if (OC.Settings.Apps.State.currentCategory === categoryId) {
return;
}
if (this._loadCategoryCall) {
this._loadCategoryCall.abort();
}
$('#app-content').addClass('icon-loading');
$('#apps-list')
.removeClass('hidden')
.html('');
$('#apps-list-empty').addClass('hidden');
$('#app-category-' + OC.Settings.Apps.State.currentCategory).removeClass('active');
$('#app-category-' + categoryId).addClass('active');
OC.Settings.Apps.State.currentCategory = categoryId;
this._loadCategoryCall = $.ajax(OC.generateUrl('settings/apps/list?category={categoryId}', {
categoryId: categoryId
}), {
type:'GET',
success: function (apps) {
OC.Settings.Apps.State.currentCategoryElements = apps.apps;
var appListWithIndex = _.indexBy(apps.apps, 'id');
OC.Settings.Apps.State.apps = appListWithIndex;
var appList = _.map(appListWithIndex, function(app) {
// default values for missing fields
return _.extend({level: 0}, app);
});
var source;
if (categoryId === 'enabled' || categoryId === 'updates' || categoryId === 'disabled' || categoryId === 'installed' || categoryId === 'app-bundles') {
source = $("#app-template-installed").html();
$('#apps-list').addClass('installed');
} else {
source = $("#app-template").html();
$('#apps-list').removeClass('installed');
}
var template = Handlebars.compile(source);
if (appList.length) {
if(categoryId !== 'app-bundles') {
appList.sort(function (a, b) {
if (a.active !== b.active) {
return (a.active ? -1 : 1)
}
if (a.update !== b.update) {
return (a.update ? -1 : 1)
}
return OC.Util.naturalSortCompare(a.name, b.name);
});
}
var firstExperimental = false;
var hasNewUpdates = false;
_.each(appList, function(app) {
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);
}
if (app.update) {
hasNewUpdates = true;
var $update = $('#app-' + app.id + ' .update');
$update.removeClass('hidden');
$update.val(t('settings', 'Update to %s').replace(/%s/g, app.update));
}
});
// reload updates if a list with new updates is loaded
if (hasNewUpdates) {
OC.Settings.Apps.reloadUpdates();
} else {
// hide update category after all updates are installed
// and the user is switching away from the empty updates view
OC.Settings.Apps.refreshUpdateCounter();
}
} else {
if (categoryId === 'updates') {
OC.Settings.Apps.showEmptyUpdates();
} else {
$('#apps-list').addClass('hidden');
$('#apps-list-empty').removeClass('hidden').find('h2').text(t('settings', 'No apps found for your version'));
$('#app-list-empty-icon').addClass('icon-search').removeClass('icon-download');
}
}
$('.enable.needs-download').tooltip({
title: t('settings', 'The app will be downloaded from the app store'),
placement: 'bottom',
container: 'body'
});
$('.app-level .official').tooltip({
title: t('settings', 'Official apps are developed by and within the community. They offer central functionality and are ready for production use.'),
placement: 'bottom',
container: 'body'
});
$('.app-level .approved').tooltip({
title: t('settings', '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.'),
placement: 'bottom',
container: 'body'
});
$('.app-level .experimental').tooltip({
title: t('settings', 'This app is not checked for security issues and is new or known to be unstable. Install at your own risk.'),
placement: 'bottom',
container: 'body'
});
},
complete: function() {
$('#app-content').removeClass('icon-loading');
}
});
},
renderApp: function(app, template, selector, firstExperimental) {
if (!template) {
var source = $("#app-template").html();
template = Handlebars.compile(source);
}
if (typeof app === 'string') {
app = OC.Settings.Apps.State.apps[app];
}
app.firstExperimental = firstExperimental;
if (!app.preview) {
app.preview = OC.imagePath('core', 'places/default-app-icon');
app.previewAsIcon = true;
}
if (_.isArray(app.author)) {
var authors = [];
_.each(app.author, function (author) {
if (typeof author === 'string') {
authors.push(author);
} else {
authors.push(author['@value']);
}
});
app.author = authors.join(', ');
} else if (typeof app.author !== 'string') {
app.author = app.author['@value'];
}
// Parse markdown in app description
app.description = DOMPurify.sanitize(
marked(app.description.trim(), OC.Settings.Apps.markedOptions),
{
SAFE_FOR_JQUERY: true,
ALLOWED_TAGS: [
'strong',
'p',
'a',
'ul',
'ol',
'li',
'em',
'del',
'blockquote'
]
}
);
var html = template(app);
if (selector) {
selector.html(html);
} else {
$('#apps-list').append(html);
}
var page = $('#app-' + app.id);
if (app.preview) {
var currentImage = new Image();
currentImage.src = app.preview;
currentImage.onload = function() {
/* Trigger color inversion for placeholder image too */
if(app.previewAsIcon) {
page.find('.app-image')
.append(OC.Settings.Apps.imageUrl(app.preview, false))
.removeClass('icon-loading');
} else {
page.find('.app-image')
.append(OC.Settings.Apps.imageUrl(app.preview, app.fromAppStore))
.removeClass('icon-loading');
}
};
}
// set group select properly
if(OC.Settings.Apps.isType(app, 'filesystem') || OC.Settings.Apps.isType(app, 'prelogin') ||
OC.Settings.Apps.isType(app, 'authentication') || OC.Settings.Apps.isType(app, 'logging') ||
OC.Settings.Apps.isType(app, 'prevent_group_restriction')) {
page.find(".groups-enable").hide();
page.find(".groups-enable__checkbox").prop('checked', false);
} else {
page.find('.group_select').val((app.groups || []).join('|'));
if (app.active) {
if (app.groups.length) {
OC.Settings.Apps.setupGroupsSelect(page.find('.group_select'));
page.find(".groups-enable__checkbox").prop('checked', true);
} else {
page.find(".groups-enable__checkbox").prop('checked', false);
}
page.find(".groups-enable").show();
} else {
page.find(".groups-enable").hide();
}
}
},
/**
* Returns the image for apps listing
* url : the url of the image
* appfromstore: bool to check whether the app is fetched from store or not.
*/
imageUrl : function (url, appfromstore) {
var img;
if (appfromstore) {
img = '<svg viewBox="0 0 72 72">';
img += '<image x="0" y="0" width="72" height="72" preserveAspectRatio="xMinYMin meet" xlink:href="' + url + '" class="app-icon" /></svg>';
} else {
var rnd = Math.floor((Math.random() * 100 )) + new Date().getSeconds() + new Date().getMilliseconds();
img = '<svg width="32" height="32" viewBox="0 0 32 32">';
img += '<defs><filter id="invertIconApps-' + rnd + '"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"></feColorMatrix></filter></defs>'
img += '<image x="0" y="0" width="32" height="32" preserveAspectRatio="xMinYMin meet" filter="url(#invertIconApps-' + rnd + ')" xlink:href="' + url + '?v=' + oc_config.version + '" class="app-icon"></image></svg>';
}
return img;
},
isType: function(app, type){
return app.types && app.types.indexOf(type) !== -1;
},
/**
* Checks the server health.
*
* If the promise fails, the server is broken.
*
* @return {Promise} promise
*/
_checkServerHealth: function() {
return $.get(OC.generateUrl('apps/files'));
},
enableAppBundle:function(bundleId, active, element, groups) {
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) {
OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(this.enableAppBundle, this, bundleId, active, element, groups), {
text: t('settings', 'Installing apps requires you to confirm your password'),
confirm: t('settings', 'Install app bundle'),
});
return;
}
var apps = OC.Settings.Apps.State.currentCategoryElements;
var appsToEnable = [];
apps.forEach(function(app) {
if(app['bundleId'] === bundleId) {
if(app['active'] === false) {
appsToEnable.push(app['id']);
}
}
});
OC.Settings.Apps.enableApp(appsToEnable, false, groups);
},
/**
* @param {string[]} appId
* @param {boolean} active
* @param {array} groups
*/
enableApp:function(appId, active, groups) {
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) {
OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(this.enableApp, this, appId, active, groups), {
text: ( active ? t('settings', 'Disabling apps requires you to confirm your password') : t('settings', 'Enabling apps requires you to confirm your password') ),
confirm: ( active ? t('settings', 'Disable app') : t('settings', 'Enable app') ),
});
return;
}
var elements = [];
appId.forEach(function(appId) {
elements.push($('#app-'+appId+' .enable'));
});
var self = this;
appId.forEach(function(appId) {
OC.Settings.Apps.hideErrorMessage(appId);
});
groups = groups || [];
var appItems = [];
appId.forEach(function(appId) {
appItems.push($('div#app-'+appId+''));
});
if(active && !groups.length) {
elements.forEach(function(element) {
element.val(t('settings','Disabling app …'));
});
$.post(OC.filePath('settings','ajax','disableapp.php'),{appid:appId},function(result) {
if(!result || result.status !== 'success') {
if (result.data && result.data.message) {
OC.Settings.Apps.showErrorMessage(appId, result.data.message);
appItems.forEach(function(appItem) {
appItem.data('errormsg', result.data.message);
})
} else {
OC.Settings.Apps.showErrorMessage(appId, t('settings', 'Error while disabling app'));
appItems.forEach(function(appItem) {
appItem.data('errormsg', t('settings', 'Error while disabling app'));
});
}
elements.forEach(function(element) {
element.val(t('settings','Disable'));
});
appItems.forEach(function(appItem) {
appItem.addClass('appwarning');
});
} else {
OC.Settings.Apps.rebuildNavigation();
appItems.forEach(function(appItem) {
appItem.data('active', false);
appItem.data('groups', '');
});
elements.forEach(function(element) {
element.data('active', false);
});
appItems.forEach(function(appItem) {
appItem.removeClass('active');
});
elements.forEach(function(element) {
element.val(t('settings', 'Enable'));
element.parent().find(".groups-enable").hide();
element.parent().find('.group_select').hide().val(null);
});
OC.Settings.Apps.State.apps[appId].active = false;
}
},'json');
} else {
// TODO: display message to admin to not refresh the page!
// TODO: lock UI to prevent further operations
elements.forEach(function(element) {
element.val(t('settings', 'Enabling app …'));
});
var appIdArray = [];
if( typeof appId === 'string' ) {
appIdArray = [appId];
} else {
appIdArray = appId;
}
$.post(OC.filePath('settings','ajax','enableapp.php'),{appIds: appIdArray, groups: groups},function(result) {
if(!result || result.status !== 'success') {
if (result.data && result.data.message) {
OC.Settings.Apps.showErrorMessage(appId, result.data.message);
appItems.forEach(function(appItem) {
appItem.data('errormsg', result.data.message);
});
} else {
OC.Settings.Apps.showErrorMessage(appId, t('settings', 'Error while enabling app'));
appItems.forEach(function(appItem) {
appItem.data('errormsg', t('settings', 'Error while disabling app'));
});
}
elements.forEach(function(element) {
element.val(t('settings', 'Enable'));
});
appItems.forEach(function(appItem) {
appItem.addClass('appwarning');
});
} else {
self._checkServerHealth().done(function() {
if (result.data.update_required) {
OC.Settings.Apps.showReloadMessage();
setTimeout(function() {
location.reload();
}, 5000);
}
OC.Settings.Apps.rebuildNavigation();
appItems.forEach(function(appItem) {
appItem.data('active', true);
});
elements.forEach(function(element) {
element.data('active', true);
});
appItems.forEach(function(appItem) {
appItem.addClass('active');
});
elements.forEach(function(element) {
element.val(t('settings', 'Disable'));
});
var app = OC.Settings.Apps.State.apps[appId];
app.active = true;
if (OC.Settings.Apps.isType(app, 'filesystem') || OC.Settings.Apps.isType(app, 'prelogin') ||
OC.Settings.Apps.isType(app, 'authentication') || OC.Settings.Apps.isType(app, 'logging')) {
elements.forEach(function(element) {
element.parent().find(".groups-enable").prop('checked', true);
element.parent().find(".groups-enable").hide();
element.parent().find('.group_select').hide().val(null);
});
} else {
elements.forEach(function(element) {
element.parent().find("#groups-enable").show();
});
if (groups) {
appItems.forEach(function(appItem) {
appItem.data('groups', JSON.stringify(groups));
});
} else {
appItems.forEach(function(appItem) {
appItem.data('groups', '');
});
}
}
}).fail(function() {
// server borked, emergency disable app
$.post(OC.webroot + '/index.php/disableapp', {appid: appId}, function() {
OC.Settings.Apps.showErrorMessage(
appId,
t('settings', 'Error: This app can not be enabled because it makes the server unstable')
);
appItems.forEach(function(appItem) {
appItem.data('errormsg', t('settings', 'Error while enabling app'));
});
elements.forEach(function(element) {
element.val(t('settings', 'Enable'));
});
appItems.forEach(function(appItem) {
appItem.addClass('appwarning');
});
}).fail(function() {
OC.Settings.Apps.showErrorMessage(
appId,
t('settings', 'Error: Could not disable broken app')
);
appItems.forEach(function(appItem) {
appItem.data('errormsg', t('settings', 'Error while disabling broken app'));
});
elements.forEach(function(element) {
element.val(t('settings', 'Enable'));
});
});
});
}
},'json')
.fail(function() {
OC.Settings.Apps.showErrorMessage(appId, t('settings', 'Error while enabling app'));
appItems.forEach(function(appItem) {
appItem.data('errormsg', t('settings', 'Error while enabling app'));
appItem.data('active', false);
appItem.addClass('appwarning');
});
elements.forEach(function(element) {
element.val(t('settings', 'Enable'));
});
});
}
},
showEmptyUpdates: function() {
$('#apps-list').addClass('hidden');
$('#apps-list-empty').removeClass('hidden').find('h2').text(t('settings', 'App up to date'));
$('#app-list-empty-icon').removeClass('icon-search').addClass('icon-download');
},
updateApp:function(appId, element) {
var oldButtonText = element.val();
element.val(t('settings','Updating …'));
OC.Settings.Apps.hideErrorMessage(appId);
$.post(OC.filePath('settings','ajax','updateapp.php'),{appid:appId},function(result) {
if(!result || result.status !== 'success') {
if (result.data && result.data.message) {
OC.Settings.Apps.showErrorMessage(appId, result.data.message);
} else {
OC.Settings.Apps.showErrorMessage(appId, t('settings','Could not update app'));
}
element.val(oldButtonText);
}
else {
element.val(t('settings','Updated'));
element.hide();
var $update = $('#app-' + appId + ' .update');
$update.addClass('hidden');
var $version = $('#app-' + appId + ' .app-version');
$version.text(OC.Settings.Apps.State.apps[appId]['update']);
OC.Settings.Apps.State.availableUpdates--;
OC.Settings.Apps.refreshUpdateCounter();
if (OC.Settings.Apps.State.currentCategory === 'updates') {
$('#app-' + appId).remove();
if (OC.Settings.Apps.State.availableUpdates === 0) {
OC.Settings.Apps.showEmptyUpdates();
}
}
}
},'json');
},
uninstallApp:function(appId, element) {
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) {
OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(this.uninstallApp, this, appId, element), {
text: t('settings', 'Uninstalling apps requires you to confirm your password'),
confirm: t('settings', 'Uninstall')
});
return;
}
OC.Settings.Apps.hideErrorMessage(appId);
element.val(t('settings','Removing …'));
$.post(OC.filePath('settings','ajax','uninstallapp.php'),{appid:appId},function(result) {
if(!result || result.status !== 'success') {
OC.Settings.Apps.showErrorMessage(appId, t('settings','Could not remove app'));
element.val(t('settings','Remove'));
} else {
OC.Settings.Apps.rebuildNavigation();
element.parents('#apps-list > .section').fadeOut(function() {
this.remove();
});
}
},'json');
},
rebuildNavigation: function() {
$.getJSON(OC.linkToOCS('core/navigation', 2) + 'apps?format=json').done(function(response){
if(response.ocs.meta.status === 'ok') {
@ -691,334 +87,5 @@ OC.Settings.Apps = OC.Settings.Apps || {
$(window).trigger('resize');
}
});
},
reloadUpdates: function() {
if (this._loadUpdatesCall) {
this._loadUpdatesCall.abort();
}
this._loadUpdatesCall = $.ajax(OC.generateUrl('settings/apps/list?category=updates'), {
type:'GET',
success: function (apps) {
OC.Settings.Apps.State.availableUpdates = apps.apps.length;
OC.Settings.Apps.refreshUpdateCounter();
}
});
},
refreshUpdateCounter: function() {
var $appCategoryUpdates = $('#app-category-updates');
var $updateCount = $appCategoryUpdates.find('.app-navigation-entry-utils-counter');
if (OC.Settings.Apps.State.availableUpdates > 0) {
$updateCount.html(OC.Settings.Apps.State.availableUpdates);
$appCategoryUpdates.show();
} else {
$updateCount.empty();
if (OC.Settings.Apps.State.currentCategory !== 'updates') {
$appCategoryUpdates.hide();
}
}
},
showErrorMessage: function(appId, message) {
$('div#app-'+appId+' .warning')
.show()
.text(message);
},
hideErrorMessage: function(appId) {
$('div#app-'+appId+' .warning')
.hide()
.text('');
},
showReloadMessage: function() {
OC.dialogs.info(
t(
'settings',
'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.'
),
t('settings','App update'),
function () {
window.location.reload();
},
true
);
},
/**
* Splits the query by spaces and tries to find all substring in the app
* @param {string} string
* @param {string} query
* @returns {boolean}
*/
_search: function(string, query) {
var keywords = query.split(' '),
stringLower = string.toLowerCase(),
found = true;
_.each(keywords, function(keyword) {
found = found && stringLower.indexOf(keyword) !== -1;
});
return found;
},
filter: function(query) {
var $appList = $('#apps-list'),
$emptyList = $('#apps-list-empty');
$('#app-list-empty-icon').addClass('icon-search').removeClass('icon-download');
$appList.removeClass('hidden');
$appList.find('.section').removeClass('hidden');
$emptyList.addClass('hidden');
if (query === '') {
return;
}
query = query.toLowerCase();
$appList.find('.section').addClass('hidden');
// App Name
var apps = _.filter(OC.Settings.Apps.State.apps, function (app) {
return OC.Settings.Apps._search(app.name, query);
});
// App ID
apps = apps.concat(_.filter(OC.Settings.Apps.State.apps, function (app) {
return OC.Settings.Apps._search(app.id, query);
}));
// App Description
apps = apps.concat(_.filter(OC.Settings.Apps.State.apps, function (app) {
return OC.Settings.Apps._search(app.description, query);
}));
// Author Name
apps = apps.concat(_.filter(OC.Settings.Apps.State.apps, function (app) {
var authors = [];
if (_.isArray(app.author)) {
_.each(app.author, function (author) {
if (typeof author === 'string') {
authors.push(author);
} else {
authors.push(author['@value']);
if (!_.isUndefined(author['@attributes']['homepage'])) {
authors.push(author['@attributes']['homepage']);
}
if (!_.isUndefined(author['@attributes']['mail'])) {
authors.push(author['@attributes']['mail']);
}
}
});
return OC.Settings.Apps._search(authors.join(' '), query);
} else if (typeof app.author !== 'string') {
authors.push(app.author['@value']);
if (!_.isUndefined(app.author['@attributes']['homepage'])) {
authors.push(app.author['@attributes']['homepage']);
}
if (!_.isUndefined(app.author['@attributes']['mail'])) {
authors.push(app.author['@attributes']['mail']);
}
return OC.Settings.Apps._search(authors.join(' '), query);
}
return OC.Settings.Apps._search(app.author, query);
}));
// App status
if (t('settings', 'Official').toLowerCase().indexOf(query) !== -1) {
apps = apps.concat(_.filter(OC.Settings.Apps.State.apps, function (app) {
return app.level === 200;
}));
}
if (t('settings', 'Approved').toLowerCase().indexOf(query) !== -1) {
apps = apps.concat(_.filter(OC.Settings.Apps.State.apps, function (app) {
return app.level === 100;
}));
}
if (t('settings', 'Experimental').toLowerCase().indexOf(query) !== -1) {
apps = apps.concat(_.filter(OC.Settings.Apps.State.apps, function (app) {
return app.level !== 100 && app.level !== 200;
}));
}
apps = _.uniq(apps, function(app){return app.id;});
if (apps.length === 0) {
$appList.addClass('hidden');
$emptyList.removeClass('hidden');
$emptyList.removeClass('hidden').find('h2').text(t('settings', 'No apps found for {query}', {
query: query
}));
} else {
_.each(apps, function (app) {
$('#app-' + app.id).removeClass('hidden');
});
$('#searchresults').hide();
}
},
_onPopState: function(params) {
params = _.extend({
category: 'enabled'
}, params);
OC.Settings.Apps.loadCategory(params.category);
},
/**
* Initializes the apps list
*/
initialize: function($el) {
var renderer = new marked.Renderer();
renderer.link = function(href, title, text) {
try {
var prot = decodeURIComponent(unescape(href))
.replace(/[^\w:]/g, '')
.toLowerCase();
} catch (e) {
return '';
}
if (prot.indexOf('http:') !== 0 && prot.indexOf('https:') !== 0) {
return '';
}
var out = '<a href="' + href + '" rel="noreferrer noopener"';
if (title) {
out += ' title="' + title + '"';
}
out += '>' + text + '</a>';
return out;
};
renderer.image = function(href, title, text) {
if (text) {
return text;
}
return title;
};
renderer.blockquote = function(quote) {
return quote;
};
OC.Settings.Apps.markedOptions = {
renderer: renderer,
gfm: false,
highlight: false,
tables: false,
breaks: false,
pedantic: false,
sanitize: true,
smartLists: true,
smartypants: false
};
OC.Plugins.register('OCA.Search', OC.Settings.Apps.Search);
OC.Settings.Apps.loadCategories();
OC.Util.History.addOnPopStateHandler(_.bind(this._onPopState, this));
$(document).on('click', 'ul#apps-categories li', function () {
var categoryId = $(this).data('categoryId');
OC.Settings.Apps.loadCategory(categoryId);
OC.Util.History.pushState({
category: categoryId
});
$('#searchbox').val('');
});
$(document).on('click', '.app-description-toggle-show', function () {
$(this).addClass('hidden');
$(this).siblings('.app-description-toggle-hide').removeClass('hidden');
$(this).siblings('.app-description-container').slideDown();
});
$(document).on('click', '.app-description-toggle-hide', function () {
$(this).addClass('hidden');
$(this).siblings('.app-description-toggle-show').removeClass('hidden');
$(this).siblings('.app-description-container').slideUp();
});
$(document).on('click', '#apps-list input.enable', function () {
var appId = $(this).data('appid');
var bundleId = $(this).data('bundleid');
var element = $(this);
var active = $(this).data('active');
var category = $('#app-navigation').attr('data-category');
if(bundleId) {
OC.Settings.Apps.enableAppBundle(bundleId, active, element);
element.val(t('settings', 'Enable all'));
} else {
OC.Settings.Apps.enableApp([appId], active);
}
});
$(document).on('click', '#apps-list input.uninstall', function () {
var appId = $(this).data('appid');
var element = $(this);
OC.Settings.Apps.uninstallApp(appId, element);
});
$(document).on('click', '#apps-list input.update', function () {
var appId = $(this).data('appid');
var element = $(this);
OC.Settings.Apps.updateApp(appId, element);
});
$(document).on('change', '.group_select', function() {
var element = $(this).closest('.section').find('input.enable');
var groups = $(this).val();
if (groups && groups !== '') {
groups = groups.split('|');
} else {
groups = [];
}
var appId = element.data('appid');
if (appId) {
OC.Settings.Apps.enableApp([appId], false, groups);
OC.Settings.Apps.State.apps[appId].groups = groups;
}
});
$(document).on('change', ".groups-enable__checkbox", function() {
var $select = $(this).closest('.section').find('.group_select');
$select.val('');
if (this.checked) {
OC.Settings.Apps.setupGroupsSelect($select);
} else {
$select.select2('destroy');
}
$select.change();
});
$(document).on('click', '#enable-experimental-apps', function () {
var state = $(this).prop('checked');
$.ajax(OC.generateUrl('settings/apps/experimental'), {
data: {state: state},
type: 'POST',
success:function () {
location.reload();
}
});
});
}
};
OC.Settings.Apps.Search = {
attach: function (search) {
search.setFilter('settings', OC.Settings.Apps.filter);
}
};
$(document).ready(function () {
// HACK: FIXME: use plugin approach
if (!window.TESTING) {
OC.Settings.Apps.initialize($('#apps-list'));
}
});
};

File diff suppressed because one or more lines are too long

View File

@ -46,9 +46,22 @@ $application->registerRoutes($this, [
['name' => 'MailSettings#storeCredentials', 'url' => '/settings/admin/mailsettings/credentials', 'verb' => 'POST'],
['name' => 'MailSettings#sendTestMail', 'url' => '/settings/admin/mailtest', 'verb' => 'POST'],
['name' => 'Encryption#startMigration', 'url' => '/settings/admin/startmigration', '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#enableApp', 'url' => '/settings/apps/enable/{appId}', 'verb' => 'GET'],
['name' => 'AppSettings#enableApp', 'url' => '/settings/apps/enable/{appId}', 'verb' => 'POST'],
['name' => 'AppSettings#enableApps', 'url' => '/settings/apps/enable', 'verb' => 'POST'],
['name' => 'AppSettings#disableApp', 'url' => '/settings/apps/disable/{appId}', 'verb' => 'GET'],
['name' => 'AppSettings#disableApps', 'url' => '/settings/apps/disable', 'verb' => 'POST'],
['name' => 'AppSettings#updateApp', 'url' => '/settings/apps/update/{appId}', 'verb' => 'GET'],
['name' => 'AppSettings#uninstallApp', 'url' => '/settings/apps/uninstall/{appId}', 'verb' => 'GET'],
['name' => 'AppSettings#viewApps', 'url' => '/settings/apps/{category}', 'verb' => 'GET', 'defaults' => ['category' => '']],
['name' => 'AppSettings#viewApps', 'url' => '/settings/apps/{category}/{id}', 'verb' => 'GET', 'defaults' => ['category' => '', 'id' => '']],
['name' => 'Users#setDisplayName', 'url' => '/settings/users/{username}/displayName', 'verb' => 'POST'],
['name' => 'Users#setEMailAddress', 'url' => '/settings/users/{id}/mailAddress', 'verb' => 'PUT'],
['name' => 'Users#setUserSettings', 'url' => '/settings/users/{username}/settings', 'verb' => 'PUT'],
['name' => 'Users#getVerificationCode', 'url' => '/settings/users/{account}/verify', 'verb' => 'GET'],
['name' => 'Users#usersList', 'url' => '/settings/users', 'verb' => 'GET'],
@ -76,13 +89,4 @@ $application->registerRoutes($this, [
// Settings pages
$this->create('settings_help', '/settings/help')
->actionInclude('settings/help.php');
// Settings ajax actions
// apps
$this->create('settings_ajax_enableapp', '/settings/ajax/enableapp.php')
->actionInclude('settings/ajax/enableapp.php');
$this->create('settings_ajax_disableapp', '/settings/ajax/disableapp.php')
->actionInclude('settings/ajax/disableapp.php');
$this->create('settings_ajax_updateapp', '/settings/ajax/updateapp.php')
->actionInclude('settings/ajax/updateapp.php');
$this->create('settings_ajax_uninstallapp', '/settings/ajax/uninstallapp.php')
->actionInclude('settings/ajax/uninstallapp.php');

View File

@ -0,0 +1,224 @@
<!--
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
-
- @author Julius Härtl <jus@bitgrid.net>
-
- @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 <http://www.gnu.org/licenses/>.
-
-->
<template>
<div id="app-details-view" style="padding: 20px;">
<a class="close icon-close" href="#" v-on:click="hideAppDetails"><span class="hidden-visually">Close</span></a>
<h2>
<div v-if="!app.preview" class="icon-settings-dark"></div>
<svg v-if="app.previewAsIcon && app.preview" width="32" height="32" viewBox="0 0 32 32">
<defs><filter :id="filterId"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"></feColorMatrix></filter></defs>
<image x="0" y="0" width="32" height="32" preserveAspectRatio="xMinYMin meet" :filter="filterUrl" :xlink:href="app.preview" class="app-icon"></image>
</svg>
{{ app.name }}</h2>
<img v-if="app.screenshot" :src="app.screenshot" width="100%" />
<div class="app-level" v-if="app.level === 200 || hasRating">
<span class="official icon-checkmark" v-if="app.level === 200"
v-tooltip.auto="t('settings', 'Official apps are developed by and within the community. They offer central functionality and are ready for production use.')">
{{ t('settings', 'Official') }}</span>
<app-score v-if="hasRating" :score="app.appstoreData.ratingOverall"></app-score>
</div>
<div class="app-author" v-if="author">
{{ t('settings', 'by') }}
<span v-for="(a, index) in author">
<a v-if="a['@attributes'] && a['@attributes']['homepage']" :href="a['@attributes']['homepage']">{{ a['@value'] }}</a><span v-else-if="a['@value']">{{ a['@value'] }}</span><span v-else>{{ a }}</span><span v-if="index+1 < author.length">, </span>
</span>
</div>
<div class="app-licence" v-if="licence">{{ licence }}</div>
<div class="actions">
<div class="actions-buttons">
<input v-if="app.update" class="update" type="button" :value="t('settings', 'Update to {version}', {version: app.update})" :disabled="installing || loading(app.id)"/>
<input v-if="app.canUnInstall" class="uninstall" type="button" :value="t('settings', 'Remove')" :disabled="installing || loading(app.id)"/>
<input v-if="app.active" class="enable" type="button" :value="t('settings','Disable')" v-on:click="disable(app.id)" :disabled="installing || loading(app.id)" />
<input v-if="!app.active" class="enable" type="button" :value="enableButtonText" v-on:click="enable(app.id)" v-tooltip.auto="enableButtonTooltip" :disabled="!app.canInstall || installing || loading(app.id)" />
</div>
<div class="app-groups">
<div class="groups-enable" v-if="app.active && canLimitToGroups(app)">
<input type="checkbox" :value="app.id" v-model="groupCheckedAppsData" v-on:change="setGroupLimit" class="groups-enable__checkbox checkbox" :id="prefix('groups_enable', app.id)">
<label :for="prefix('groups_enable', app.id)">Auf Gruppen beschränken</label>
<input type="hidden" class="group_select" title="Alle" value="">
<multiselect v-if="isLimitedToGroups(app)" :options="groups" :value="appGroups" @select="addGroupLimitation" @remove="removeGroupLimitation" :options-limit="5"
:placeholder="t('settings', 'Limit app usage to groups')"
label="name" track-by="id" class="multiselect-vue"
:multiple="true" :close-on-select="false">
<span slot="noResult">{{t('settings', 'No results')}}</span>
</multiselect>
</div>
</div>
</div>
<p class="documentation">
<a class="appslink" :href="appstoreUrl" v-if="!app.internal" target="_blank" rel="noreferrer noopener">{{ t('settings', 'View in store')}} </a>
<a class="appslink" v-if="app.website" :href="app.website" target="_blank" rel="noreferrer noopener">{{ t('settings', 'Visit website') }} </a>
<a class="appslink" v-if="app.bugs" :href="app.bugs" target="_blank" rel="noreferrer noopener">{{ t('settings', 'Report a bug') }} </a>
<a class="appslink" v-if="app.documentation && app.documentation.user" :href="app.documentation.user" target="_blank" rel="noreferrer noopener">{{ t('settings', 'User documentation') }} </a>
<a class="appslink" v-if="app.documentation && app.documentation.admin" :href="app.documentation.admin" target="_blank" rel="noreferrer noopener">{{ t('settings', 'Admin documentation') }} </a>
<a class="appslink" v-if="app.documentation && app.documentation.developer" :href="app.documentation.developer" target="_blank" rel="noreferrer noopener">{{ t('settings', 'Developer documentation') }} </a>
</p>
<ul class="app-dependencies">
<li v-if="app.missingMinOwnCloudVersion">{{ t('settings', 'This app has no minimum Nextcloud version assigned. This will be an error in the future.') }}</li>
<li v-if="app.missingMaxOwnCloudVersion">{{ t('settings', 'This app has no maximum Nextcloud version assigned. This will be an error in the future.') }}</li>
<li v-if="!app.canInstall">
{{ t('settings', 'This app cannot be installed because the following dependencies are not fulfilled:') }}
<ul class="missing-dependencies">
<li v-for="dep in app.missingDependencies">{{ dep }}</li>
</ul>
</li>
</ul>
<div class="app-description" v-html="renderMarkdown"></div>
</div>
</template>
<script>
import Multiselect from 'vue-multiselect';
import AppScore from './appList/appScore';
import AppManagement from './appManagement';
import prefix from './prefixMixin';
import SvgFilterMixin from './svgFilterMixin';
export default {
mixins: [AppManagement, prefix, SvgFilterMixin],
name: 'appDetails',
props: ['category', 'app'],
components: {
Multiselect,
AppScore
},
data() {
return {
groupCheckedAppsData: false,
}
},
mounted() {
if (this.app.groups.length > 0) {
this.groupCheckedAppsData = true;
}
},
methods: {
hideAppDetails() {
this.$router.push({
name: 'apps-category',
params: {category: this.category}
});
},
},
computed: {
appstoreUrl() {
return `https://apps.nextcloud.com/apps/${this.app.id}`;
},
licence() {
if (this.app.licence)
return ('' + this.app.licence).toUpperCase() + t('settings', '-licensed');
return null;
},
hasRating() {
return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5;
},
author() {
if (typeof this.app.author === 'string') {
return [
{
'@value': this.app.author
}
]
}
if (this.app.author['@value']) {
return [this.app.author];
}
return this.app.author;
},
appGroups() {
return this.app.groups.map(group => {return {id: group, name: group}});
},
groups() {
return this.$store.getters.getGroups
.filter(group => group.id !== 'disabled')
.sort((a, b) => a.name.localeCompare(b.name));
},
renderMarkdown() {
// TODO: bundle marked as well
var renderer = new window.marked.Renderer();
renderer.link = function(href, title, text) {
try {
var prot = decodeURIComponent(unescape(href))
.replace(/[^\w:]/g, '')
.toLowerCase();
} catch (e) {
return '';
}
if (prot.indexOf('http:') !== 0 && prot.indexOf('https:') !== 0) {
return '';
}
var out = '<a href="' + href + '" rel="noreferrer noopener"';
if (title) {
out += ' title="' + title + '"';
}
out += '>' + text + '</a>';
return out;
};
renderer.image = function(href, title, text) {
if (text) {
return text;
}
return title;
};
renderer.blockquote = function(quote) {
return quote;
};
return DOMPurify.sanitize(
window.marked(this.app.description.trim(), {
renderer: renderer,
gfm: false,
highlight: false,
tables: false,
breaks: false,
pedantic: false,
sanitize: true,
smartLists: true,
smartypants: false
}),
{
SAFE_FOR_JQUERY: true,
ALLOWED_TAGS: [
'strong',
'p',
'a',
'ul',
'ol',
'li',
'em',
'del',
'blockquote'
]
}
);
}
}
}
</script>

View File

@ -0,0 +1,181 @@
<!--
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
-
- @author Julius Härtl <jus@bitgrid.net>
-
- @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 <http://www.gnu.org/licenses/>.
-
-->
<template>
<div id="app-content-inner">
<div id="apps-list" :class="{installed: (useBundleView || useListView), store: useAppStoreView}">
<template v-if="useListView">
<app-item v-for="app in apps" :key="app.id" :app="app" :category="category" />
</template>
<template v-for="bundle in bundles" v-if="useBundleView && bundleApps(bundle.id).length > 0">
<div class="apps-header">
<div class="app-image"></div>
<h2>{{ bundle.name }} <input type="button" :value="bundleToggleText(bundle.id)" v-on:click="toggleBundle(bundle.id)"></h2>
<div class="app-version"></div>
<div class="app-level"></div>
<div class="app-groups"></div>
<div class="actions">&nbsp;</div>
</div>
<app-item v-for="app in bundleApps(bundle.id)" :key="bundle.id + app.id" :app="app" :category="category"/>
</template>
<template v-if="useAppStoreView">
<app-item v-for="app in apps" :key="app.id" :app="app" :category="category" :list-view="false" />
</template>
</div>
<div id="apps-list-search" class="installed">
<template v-if="search !== '' && searchApps.length > 0">
<div class="section">
<div></div>
<h2>{{ t('settings', 'Results from other categories') }}</h2>
</div>
<app-item v-for="app in searchApps" :key="app.id" :app="app" :category="category" :list-view="true" />
</template>
</div>
<div id="apps-list-empty" class="emptycontent emptycontent-search" v-if="!loading && searchApps.length === 0 && apps.length === 0">
<div id="app-list-empty-icon" class="icon-settings-dark"></div>
<h2>{{ t('settings', 'No apps found for your versoin')}}</h2>
</div>
<div id="searchresults"></div>
</div>
</template>
<script>
import appItem from './appList/appItem';
import Multiselect from 'vue-multiselect';
import prefix from './prefixMixin';
export default {
name: 'appList',
mixins: [prefix],
props: ['category', 'app', 'search'],
components: {
Multiselect,
appItem
},
computed: {
loading() {
return this.$store.getters.loading('list');
},
apps() {
let apps = this.$store.getters.getAllApps
.filter(app => app.name.toLowerCase().search(this.search.toLowerCase()) !== -1)
.sort(function (a, b) {
if (a.active !== b.active) {
return (a.active ? -1 : 1)
}
if (a.update !== b.update) {
return (a.update ? -1 : 1)
}
return OC.Util.naturalSortCompare(a.name, b.name);
});
if (this.category === 'installed') {
return apps.filter(app => app.installed);
}
if (this.category === 'enabled') {
return apps.filter(app => app.active);
}
if (this.category === 'disabled') {
return apps.filter(app => !app.active);
}
if (this.category === 'app-bundles') {
return apps.filter(app => app.bundles);
}
if (this.category === 'updates') {
return apps.filter(app => app.update);
}
// filter app store categories
return apps.filter(app => {
return app.appstore && app.category !== undefined &&
(app.category === this.category || app.category.indexOf(this.category) > -1);
});
},
bundles() {
return this.$store.getters.getServerData.bundles;
},
bundleApps() {
return function(bundle) {
return this.$store.getters.getAllApps
.filter(app => app.bundleId === bundle);
}
},
searchApps() {
if (this.search === '') {
return [];
}
return this.$store.getters.getAllApps
.filter(app => {
if (app.name.toLowerCase().search(this.search.toLowerCase()) !== -1) {
return (!this.apps.find(_app => _app.id === app.id));
}
return false;
});
},
useAppStoreView() {
return !this.useListView && !this.useBundleView;
},
useListView() {
return (this.category === 'installed' || this.category === 'enabled' || this.category === 'disabled' || this.category === 'updates');
},
useBundleView() {
return (this.category === 'app-bundles');
},
allBundlesEnabled() {
let self = this;
return function(id) {
return self.bundleApps(id).filter(app => !app.active).length === 0;
}
},
bundleToggleText() {
let self = this;
return function(id) {
if (self.allBundlesEnabled(id)) {
return t('settings', 'Disable all');
}
return t('settings', 'Enable all');
}
}
},
methods: {
toggleBundle(id) {
if (this.allBundlesEnabled(id)) {
return this.disableBundle(id);
}
return this.enableBundle(id);
},
enableBundle(id) {
let apps = this.bundleApps(id).map(app => app.id);
this.$store.dispatch('enableApp', { appId: apps, groups: [] })
.catch((error) => { console.log(error); OC.Notification.show(error)});
},
disableBundle(id) {
let apps = this.bundleApps(id).map(app => app.id);
this.$store.dispatch('disableApp', { appId: apps, groups: [] })
.catch((error) => { OC.Notification.show(error)});
}
},
}
</script>

View File

@ -0,0 +1,118 @@
<!--
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
-
- @author Julius Härtl <jus@bitgrid.net>
-
- @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 <http://www.gnu.org/licenses/>.
-
-->
<template>
<div class="section" v-bind:class="{ selected: isSelected }" v-on:click="showAppDetails">
<div class="app-image app-image-icon" v-on:click="showAppDetails">
<div v-if="(listView && !app.preview) || (!listView && !app.screenshot)" class="icon-settings-dark"></div>
<svg v-if="listView && app.preview" width="32" height="32" viewBox="0 0 32 32">
<defs><filter :id="filterId"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"></feColorMatrix></filter></defs>
<image x="0" y="0" width="32" height="32" preserveAspectRatio="xMinYMin meet" :filter="filterUrl" :xlink:href="app.preview" class="app-icon"></image>
</svg>
<img v-if="!listView && app.screenshot" :src="app.screenshot" width="100%" />
</div>
<div class="app-name" v-on:click="showAppDetails">
{{ app.name }}
</div>
<div class="app-summary" v-if="!listView">{{ app.summary }}</div>
<div class="app-version" v-if="listView">
<span v-if="app.version">{{ app.version }}</span>
<span v-else-if="app.appstoreData.releases[0].version">{{ app.appstoreData.releases[0].version }}</span>
</div>
<div class="app-level">
<span class="official icon-checkmark" v-if="app.level === 200"
v-tooltip.auto="t('settings', 'Official apps are developed by and within the community. They offer central functionality and are ready for production use.')">
{{ t('settings', 'Official') }}</span>
<app-score v-if="!listView" :score="app.score"></app-score>
</div>
<div class="actions">
<div class="warning" v-if="app.error">{{ app.error }}</div>
<div class="icon icon-loading-small" v-if="loading(app.id)"></div>
<input v-if="app.update" class="update" type="button" :value="t('settings', 'Update to {update}', {update:app.update})" v-on:click="update(app.id)" :disabled="installing || loading(app.id)" />
<input v-if="app.canUnInstall" class="uninstall" type="button" :value="t('settings', 'Remove')" v-on:click="remove(app.id)" :disabled="installing || loading(app.id)" />
<input v-if="app.active" class="enable" type="button" :value="t('settings','Disable')" v-on:click="disable(app.id)" :disabled="installing || loading(app.id)" />
<input v-if="!app.active" class="enable" type="button" :value="enableButtonText" v-on:click="enable(app.id)" v-tooltip.auto="enableButtonTooltip" :disabled="!app.canInstall || installing || loading(app.id)" />
</div>
</div>
</template>
<script>
import Multiselect from 'vue-multiselect';
import AppScore from './appScore';
import AppManagement from '../appManagement';
import SvgFilterMixin from '../svgFilterMixin';
export default {
name: 'appItem',
mixins: [AppManagement, SvgFilterMixin],
props: {
app: {},
category: {},
listView: {
type: Boolean,
default: true,
}
},
watch: {
'$route.params.id': function (id) {
this.isSelected = (this.app.id === id);
}
},
components: {
Multiselect,
AppScore,
},
data() {
return {
isSelected: false,
scrolled: false,
};
},
mounted() {
this.isSelected = (this.app.id === this.$route.params.id);
},
computed: {
},
watchers: {
},
methods: {
showAppDetails(event) {
if (event.currentTarget.tagName === 'INPUT' || event.currentTarget.tagName === 'A') {
return;
}
this.$router.push({
name: 'apps-details',
params: {category: this.category, id: this.app.id}
});
},
prefix(prefix, content) {
return prefix + '_' + content;
},
}
}
</script>

View File

@ -0,0 +1,38 @@
<!--
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
-
- @author Julius Härtl <jus@bitgrid.net>
-
- @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 <http://www.gnu.org/licenses/>.
-
-->
<template>
<img :src="scoreImage" class="app-score-image" />
</template>
<script>
export default {
name: 'appScore',
props: ['score'],
computed: {
scoreImage() {
let score = Math.round( this.score * 10 );
let imageName = 'rating/s' + score + '.svg';
return OC.imagePath('core', imageName);
}
}
};
</script>

View File

@ -0,0 +1,117 @@
<!--
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
-
- @author Julius Härtl <jus@bitgrid.net>
-
- @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 <http://www.gnu.org/licenses/>.
-
-->
<script>
export default {
mounted() {
if (this.app.groups.length > 0) {
this.groupCheckedAppsData = true;
}
},
computed: {
appGroups() {
return this.app.groups.map(group => {return {id: group, name: group}});
},
loading() {
let self = this;
return function(id) {
return self.$store.getters.loading(id);
}
},
installing() {
return this.$store.getters.loading('install');
},
enableButtonText() {
if (this.app.needsDownload) {
return t('settings','Download and enable');
}
return t('settings','Enable');
},
enableButtonTooltip() {
if (this.app.needsDownload) {
return t('settings','The app will be downloaded from the app store');
}
return false;
}
},
methods: {
isLimitedToGroups(app) {
if (this.app.groups.length || this.groupCheckedAppsData) {
return true;
}
return false;
},
setGroupLimit: function() {
if (!this.groupCheckedAppsData) {
this.$store.dispatch('enableApp', {appId: this.app.id, groups: []});
}
},
canLimitToGroups(app) {
if (app.types && app.types.includes('filesystem')
|| app.types.includes('prelogin')
|| app.types.includes('authentication')
|| app.types.includes('logging')
|| app.types.includes('prevent_group_restriction')) {
return false;
}
return true;
},
addGroupLimitation(group) {
let groups = this.app.groups.concat([]).concat([group.id]);
this.$store.dispatch('enableApp', { appId: this.app.id, groups: groups});
},
removeGroupLimitation(group) {
let currentGroups = this.app.groups.concat([]);
let index = currentGroups.indexOf(group.id);
if (index > -1) {
currentGroups.splice(index, 1);
}
this.$store.dispatch('enableApp', { appId: this.app.id, groups: currentGroups});
},
enable(appId) {
this.$store.dispatch('enableApp', { appId: appId, groups: [] })
.then((response) => { OC.Settings.Apps.rebuildNavigation(); })
.catch((error) => { OC.Notification.show(error)});
},
disable(appId) {
this.$store.dispatch('disableApp', { appId: appId })
.then((response) => { OC.Settings.Apps.rebuildNavigation(); })
.catch((error) => { OC.Notification.show(error)});
},
remove(appId) {
this.$store.dispatch('uninstallApp', { appId: appId })
.then((response) => { OC.Settings.Apps.rebuildNavigation(); })
.catch((error) => { OC.Notification.show(error)});
},
install(appId) {
this.$store.dispatch('enableApp', { appId: appId })
.then((response) => { OC.Settings.Apps.rebuildNavigation(); })
.catch((error) => { OC.Notification.show(error)});
},
update(appId) {
this.$store.dispatch('updateApp', { appId: appId })
.then((response) => { OC.Settings.Apps.rebuildNavigation(); })
.catch((error) => { OC.Notification.show(error)});
}
}
}
</script>

View File

@ -6,7 +6,7 @@
<ul :id="menu.id">
<navigation-item v-for="item in menu.items" :item="item" :key="item.key" />
</ul>
<div id="app-settings">
<div id="app-settings" v-if="!!$slots['settings-content']">
<div id="app-settings-header">
<button class="settings-button"
data-apps-slide-toggle="#app-settings-content"

View File

@ -0,0 +1,32 @@
<!--
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
-
- @author Julius Härtl <jus@bitgrid.net>
-
- @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 <http://www.gnu.org/licenses/>.
-
-->
<script>
export default {
name: 'prefixMixin',
methods: {
prefix (prefix, content) {
return prefix + '_' + content;
},
}
}
</script>

View File

@ -1,6 +1,7 @@
import Vue from 'vue';
import Router from 'vue-router';
import Users from './views/Users';
import Apps from './views/Apps';
Vue.use(Router);
@ -32,6 +33,26 @@ export default new Router({
component: Users
}
]
},
{
path: '/:index(index.php/)?settings/apps',
component: Apps,
props: true,
name: 'apps',
children: [
{
path: ':category',
name: 'apps-category',
component: Apps,
children: [
{
path: ':id',
name: 'apps-details',
component: Apps
}
]
}
]
}
]
});

293
settings/src/store/apps.js Normal file
View File

@ -0,0 +1,293 @@
/*
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
import api from './api';
import axios from 'axios/index';
import Vue from 'vue';
const state = {
apps: [],
categories: [],
updateCount: 0,
loading: {},
loadingList: false,
};
const mutations = {
APPS_API_FAILURE(state, error) {
OC.Notification.showHtml(t('settings','An error occured during the request. Unable to proceed.')+'<br>'+error.error.response.data.data.message, {timeout: 7});
console.log(state, error);
},
initCategories(state, {categories, updateCount}) {
state.categories = categories;
state.updateCount = updateCount;
},
setUpdateCount(state, updateCount) {
state.updateCount = updateCount;
},
addCategory(state, category) {
state.categories.push(category);
},
appendCategories(state, categoriesArray) {
// convert obj to array
state.categories = categoriesArray;
},
setAllApps(state, apps) {
state.apps = apps;
},
setError(state, {appId, error}) {
let app = state.apps.find(app => app.id === appId);
app.error = error;
},
clearError(state, {appId, error}) {
let app = state.apps.find(app => app.id === appId);
app.error = null;
},
enableApp(state, {appId, groups}) {
let app = state.apps.find(app => app.id === appId);
app.active = true;
app.groups = groups;
},
disableApp(state, appId) {
let app = state.apps.find(app => app.id === appId);
app.active = false;
app.groups = [];
if (app.removable) {
app.canUnInstall = true;
}
},
uninstallApp(state, appId) {
state.apps.find(app => app.id === appId).active = false;
state.apps.find(app => app.id === appId).groups = [];
state.apps.find(app => app.id === appId).needsDownload = true;
state.apps.find(app => app.id === appId).canUnInstall = false;
state.apps.find(app => app.id === appId).canInstall = true;
},
updateApp(state, appId) {
let app = state.apps.find(app => app.id === appId);
let version = app.update;
app.update = null;
app.version = version;
state.updateCount--;
},
resetApps(state) {
state.apps = [];
},
reset(state) {
state.apps = [];
state.categories = [];
state.updateCount = 0;
},
startLoading(state, id) {
if (Array.isArray(id)) {
id.forEach((_id) => {
Vue.set(state.loading, _id, true);
})
} else {
Vue.set(state.loading, id, true);
}
},
stopLoading(state, id) {
if (Array.isArray(id)) {
id.forEach((_id) => {
Vue.set(state.loading, _id, false);
})
} else {
Vue.set(state.loading, id, false);
}
},
};
const getters = {
loading(state) {
return function(id) {
return state.loading[id];
}
},
getCategories(state) {
return state.categories;
},
getAllApps(state) {
return state.apps;
},
getUpdateCount(state) {
return state.updateCount;
}
};
const actions = {
enableApp(context, { appId, groups }) {
let apps;
if (Array.isArray(appId)) {
apps = appId;
} else {
apps = [appId];
}
return api.requireAdmin().then((response) => {
context.commit('startLoading', apps);
context.commit('startLoading', 'install');
return api.post(OC.generateUrl(`settings/apps/enable`), {appIds: apps, groups: groups})
.then((response) => {
context.commit('stopLoading', apps);
context.commit('stopLoading', 'install');
apps.forEach(_appId => {
context.commit('enableApp', {appId: _appId, groups: groups});
});
// check for server health
return api.get(OC.generateUrl('apps/files'))
.then(() => {
if (response.data.update_required) {
OC.dialogs.info(
t(
'settings',
'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.'
),
t('settings','App update'),
function () {
window.location.reload();
},
true
);
setTimeout(function() {
location.reload();
}, 5000);
}
})
.catch((error) => {
if (!Array.isArray(appId)) {
context.commit('setError', {
appId: apps,
error: t('settings', 'Error: This app can not be enabled because it makes the server unstable')
});
}
});
})
.catch((error) => {
context.commit('setError', {appId: apps, error: t('settings', 'Error while enabling app')});
context.commit('stopLoading', apps);
context.commit('stopLoading', 'install');
context.commit('APPS_API_FAILURE', { appId, error })
})
}).catch((error) => context.commit('API_FAILURE', { appId, error }));
},
disableApp(context, { appId }) {
let apps;
if (Array.isArray(appId)) {
apps = appId;
} else {
apps = [appId];
}
return api.requireAdmin().then((response) => {
context.commit('startLoading', apps);
return api.post(OC.generateUrl(`settings/apps/disable`), {appIds: apps})
.then((response) => {
context.commit('stopLoading', apps);
apps.forEach(_appId => {
context.commit('disableApp', _appId);
});
return true;
})
.catch((error) => {
context.commit('stopLoading', apps);
context.commit('APPS_API_FAILURE', { appId, error })
})
}).catch((error) => context.commit('API_FAILURE', { appId, error }));
},
uninstallApp(context, { appId }) {
return api.requireAdmin().then((response) => {
context.commit('startLoading', appId);
return api.get(OC.generateUrl(`settings/apps/uninstall/${appId}`))
.then((response) => {
context.commit('stopLoading', appId);
context.commit('uninstallApp', appId);
return true;
})
.catch((error) => {
context.commit('stopLoading', appId);
context.commit('APPS_API_FAILURE', { appId, error })
})
}).catch((error) => context.commit('API_FAILURE', { appId, error }));
},
updateApp(context, { appId }) {
return api.requireAdmin().then((response) => {
context.commit('startLoading', appId);
context.commit('startLoading', 'install');
return api.get(OC.generateUrl(`settings/apps/update/${appId}`))
.then((response) => {
context.commit('stopLoading', 'install');
context.commit('stopLoading', appId);
context.commit('updateApp', appId);
return true;
})
.catch((error) => {
context.commit('stopLoading', appId);
context.commit('stopLoading', 'install');
context.commit('APPS_API_FAILURE', { appId, error })
})
}).catch((error) => context.commit('API_FAILURE', { appId, error }));
},
getAllApps(context) {
context.commit('startLoading', 'list');
return api.get(OC.generateUrl(`settings/apps/list`))
.then((response) => {
context.commit('setAllApps', response.data.apps);
context.commit('stopLoading', 'list');
return true;
})
.catch((error) => context.commit('API_FAILURE', error))
},
getCategories(context) {
context.commit('startLoading', 'categories');
return api.get(OC.generateUrl('settings/apps/categories'))
.then((response) => {
if (response.data.length > 0) {
context.commit('appendCategories', response.data);
context.commit('stopLoading', 'categories');
return true;
}
return false;
})
.catch((error) => context.commit('API_FAILURE', error));
},
};
export default { state, mutations, getters, actions };

View File

@ -1,6 +1,7 @@
import Vue from 'vue';
import Vuex from 'vuex';
import users from './users';
import apps from './apps';
import settings from './settings';
import oc from './oc';
@ -23,6 +24,7 @@ const mutations = {
export default new Vuex.Store({
modules: {
users,
apps,
settings,
oc
},

View File

@ -48,12 +48,12 @@ const mutations = {
state.groups = orderGroups(state.groups, state.orderBy);
},
addGroup(state, gid) {
addGroup(state, {gid, displayName}) {
try {
// extend group to default values
let group = Object.assign({}, defaults.group, {
id: gid,
name: gid
name: displayName,
});
state.groups.push(group);
state.groups = orderGroups(state.groups, state.orderBy);
@ -197,6 +197,21 @@ const actions = {
.catch((error) => context.commit('API_FAILURE', error));
},
getGroups(context, { offset, limit, search }) {
search = typeof search === 'string' ? search : '';
return api.get(OC.linkToOCS(`cloud/groups?offset=${offset}&limit=${limit}&search=${search}`, 2))
.then((response) => {
if (Object.keys(response.data.ocs.data.groups).length > 0) {
response.data.ocs.data.groups.forEach(function(group) {
context.commit('addGroup', {gid: group, displayName: group});
});
return true;
}
return false;
})
.catch((error) => context.commit('API_FAILURE', error));
},
/**
* Get all users with full details
*
@ -253,7 +268,7 @@ const actions = {
addGroup(context, gid) {
return api.requireAdmin().then((response) => {
return api.post(OC.linkToOCS(`cloud/groups`, 2), {groupid: gid})
.then((response) => context.commit('addGroup', gid))
.then((response) => context.commit('addGroup', {gid: gid, displayName: gid}))
.catch((error) => {throw error;});
}).catch((error) => {
context.commit('API_FAILURE', { gid, error });
@ -450,4 +465,4 @@ const actions = {
}
};
export default { state, mutations, getters, actions };
export default { state, mutations, getters, actions };

215
settings/src/views/Apps.vue Normal file
View File

@ -0,0 +1,215 @@
<!--
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
-
- @author Julius Härtl <jus@bitgrid.net>
-
- @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 <http://www.gnu.org/licenses/>.
-
-->
<template>
<div id="app">
<app-navigation :menu="menu" />
<div id="app-content" class="app-settings-content" :class="{ 'with-app-sidebar': currentApp, 'icon-loading': loadingList }">
<app-list :category="category" :app="currentApp" :search="search"></app-list>
<div id="app-sidebar" v-if="id && currentApp">
<app-details :category="category" :app="currentApp"></app-details>
</div>
</div>
</div>
</template>
<script>
import appNavigation from '../components/appNavigation';
import appList from '../components/appList';
import Vue from 'vue';
import VueLocalStorage from 'vue-localstorage'
import Multiselect from 'vue-multiselect';
import api from '../store/api';
import AppDetails from '../components/appDetails';
Vue.use(VueLocalStorage)
Vue.use(VueLocalStorage)
export default {
name: 'Apps',
props: {
category: {
type: String,
default: 'installed',
},
id: {
type: String,
default: '',
}
},
components: {
AppDetails,
appNavigation,
appList,
},
methods: {
setSearch(search) {
this.search = search;
}
},
beforeMount() {
this.$store.dispatch('getCategories');
this.$store.dispatch('getAllApps');
this.$store.dispatch('getGroups', {offset: 0, limit: -1});
this.$store.commit('setUpdateCount', this.$store.getters.getServerData.updateCount)
},
mounted() {
// TODO: remove jQuery once we have a proper standardisation of the search
$('#searchbox').show();
let self = this;
$('#searchbox').change(function(e) {
self.setSearch($('#searchbox').val());
});
},
data() {
return {
search: ''
}
},
watch: {
category: function (val, old) {
this.setSearch('');
}
},
computed: {
loading() {
return this.$store.getters.loading('categories');
},
loadingList() {
return this.$store.getters.loading('list');
},
currentApp() {
return this.apps.find(app => app.id === this.id );
},
categories() {
return this.$store.getters.getCategories;
},
apps() {
return this.$store.getters.getAllApps;
},
updateCount() {
return this.$store.getters.getUpdateCount;
},
settings() {
return this.$store.getters.getServerData;
},
// BUILD APP NAVIGATION MENU OBJECT
menu() {
// Data provided php side
let categories = this.$store.getters.getCategories;
categories = Array.isArray(categories) ? categories : [];
// Map groups
categories = categories.map(category => {
let item = {};
item.id = 'app-category-' + category.ident;
item.icon = 'icon-category-' + category.ident;
item.classes = []; // empty classes, active will be set later
item.router = { // router link to
name: 'apps-category',
params: {category: category.ident}
};
item.text = category.displayName;
return item;
});
// Add everyone group
let defaultCategories = [
{
id: 'app-category-your-apps',
classes: [],
router: {name: 'apps'},
icon: 'icon-category-installed',
text: t('settings', 'Your apps'),
},
{
id: 'app-category-enabled',
classes: [],
icon: 'icon-category-enabled',
router: {name: 'apps-category', params: {category: 'enabled'}},
text: t('settings', 'Active apps'),
}, {
id: 'app-category-disabled',
classes: [],
icon: 'icon-category-disabled',
router: {name: 'apps-category', params: {category: 'disabled'}},
text: t('settings', 'Disabled apps'),
}
];
if (!this.settings.appstoreEnabled) {
return {
id: 'appscategories',
items: defaultCategories,
}
}
if (this.$store.getters.getUpdateCount > 0) {
defaultCategories.push({
id: 'app-category-updates',
classes: [],
icon: 'icon-download',
router: {name: 'apps-category', params: {category: 'updates'}},
text: t('settings', 'Updates'),
utils: {counter: this.$store.getters.getUpdateCount}
});
}
defaultCategories.push({
id: 'app-category-app-bundles',
classes: [],
icon: 'icon-category-app-bundles',
router: {name: 'apps-category', params: {category: 'app-bundles'}},
text: t('settings', 'App bundles'),
});
categories = defaultCategories.concat(categories);
// Set current group as active
let activeGroup = categories.findIndex(group => group.id === 'app-category-' + this.category);
if (activeGroup >= 0) {
categories[activeGroup].classes.push('active');
} else {
categories[0].classes.push('active');
}
categories.push({
id: 'app-developer-docs',
classes: [],
href: this.settings.developerDocumentation,
text: t('settings', 'Developer documentation') + ' ↗',
});
// Return
return {
id: 'appscategories',
items: categories,
loading: this.loading
}
},
}
}
</script>

View File

@ -1,213 +0,0 @@
<?php
style('settings', 'settings');
vendor_script(
'core',
[
'marked/marked.min',
]
);
script(
'settings',
[
'settings',
'apps',
]
);
/** @var array $_ */
/** @var \OCP\IURLGenerator $urlGenerator */
$urlGenerator = $_['urlGenerator'];
?>
<script id="categories-template" type="text/x-handlebars-template">
{{#each this}}
<li id="app-category-{{ident}}" data-category-id="{{ident}}" tabindex="0">
<a href="#" class="icon-category-{{ident}}">{{displayName}}</a>
<div class="app-navigation-entry-utils">
<ul>
<li class="app-navigation-entry-utils-counter">{{ counter }}</li>
</ul>
</div>
</li>
{{/each}}
<?php if($_['appstoreEnabled']): ?>
<li>
<a class="app-external icon-info" target="_blank" rel="noreferrer noopener" href="<?php p($urlGenerator->linkToDocs('developer-manual')); ?>"><?php p($l->t('Developer documentation'));?> ↗</a>
</li>
<?php endif; ?>
</script>
<script id="app-template-installed" type="text/x-handlebars">
{{#if newCategory}}
<div class="apps-header">
<div class="app-image"></div>
<h2>{{categoryName}} <input class="enable" type="submit" data-bundleid="{{bundleId}}" data-active="true" value="<?php p($l->t('Enable all'));?>"/></h2>
<div class="app-version"></div>
<div class="app-level"></div>
<div class="app-groups"></div>
<div class="actions">&nbsp;</div>
</div>
{{/if}}
<div class="section" id="app-{{id}}">
<div class="app-image app-image-icon"></div>
<div class="app-name">
{{#if detailpage}}
<a href="{{detailpage}}" target="_blank" rel="noreferrer noopener">{{name}}</a>
{{else}}
{{name}}
{{/if}}
</div>
<div class="app-version">{{version}}</div>
<div class="app-level">
{{{level}}}{{#unless internal}}<a href="https://apps.nextcloud.com/apps/{{id}}" target="_blank"><?php p($l->t('View in store'));?> ↗</a>{{/unless}}
</div>
<div class="app-groups">
{{#if active}}
<div class="groups-enable">
<input type="checkbox" class="groups-enable__checkbox checkbox" id="groups_enable-{{id}}"/>
<label for="groups_enable-{{id}}"><?php p($l->t('Limit to groups')); ?></label>
<input type="hidden" class="group_select" title="<?php p($l->t('All')); ?>">
</div>
{{/if}}
</div>
<div class="actions">
<div class="warning hidden"></div>
<input class="update hidden" type="submit" value="<?php p($l->t('Update to %s', array('{{update}}'))); ?>" data-appid="{{id}}" />
{{#if canUnInstall}}
<input class="uninstall" type="submit" value="<?php p($l->t('Remove')); ?>" data-appid="{{id}}" />
{{/if}}
{{#if active}}
<input class="enable" type="submit" data-appid="{{id}}" data-active="true" value="<?php p($l->t("Disable"));?>"/>
{{else}}
<input class="enable{{#if needsDownload}} needs-download{{/if}}" type="submit" data-appid="{{id}}" data-active="false" {{#unless canInstall}}disabled="disabled"{{/unless}} value="<?php p($l->t("Enable"));?>"/>
{{/if}}
</div>
</div>
</script>
<script id="app-template" type="text/x-handlebars">
<div class="section" id="app-{{id}}">
{{#if preview}}
<div class="app-image{{#if previewAsIcon}} app-image-icon{{/if}} icon-loading">
</div>
{{/if}}
<h2 class="app-name">
{{#if detailpage}}
<a href="{{detailpage}}" target="_blank" rel="noreferrer noopener">{{name}}</a>
{{else}}
{{name}}
{{/if}}
</h2>
<div class="app-level">
{{{level}}}
</div>
{{#if ratingNumThresholdReached }}
<div class="app-score">{{{score}}}</div>
{{/if}}
<div class="app-detailpage"></div>
<div class="app-description-container hidden">
<div class="app-version">{{version}}</div>
{{#if profilepage}}<a href="{{profilepage}}" target="_blank" rel="noreferrer noopener">{{/if}}
<div class="app-author"><?php p($l->t('by %s', ['{{author}}']));?>
{{#if licence}}
(<?php p($l->t('%s-licensed', ['{{licence}}'])); ?>)
{{/if}}
</div>
{{#if profilepage}}</a>{{/if}}
<div class="app-description">{{{description}}}</div>
<!--<div class="app-changed">{{changed}}</div>-->
{{#if documentation}}
<p class="documentation">
<?php p($l->t("Documentation:"));?>
{{#if documentation.user}}
<span class="userDocumentation">
<a id="userDocumentation" class="appslink" href="{{documentation.user}}" target="_blank" rel="noreferrer noopener"><?php p($l->t('User documentation'));?> ↗</a>
</span>
{{/if}}
{{#if documentation.admin}}
<span class="adminDocumentation">
<a id="adminDocumentation" class="appslink" href="{{documentation.admin}}" target="_blank" rel="noreferrer noopener"><?php p($l->t('Admin documentation'));?> ↗</a>
</span>
{{/if}}
{{#if documentation.developer}}
<span class="developerDocumentation">
<a id="developerDocumentation" class="appslink" href="{{documentation.developer}}" target="_blank" rel="noreferrer noopener"><?php p($l->t('Developer documentation'));?> ↗</a>
</span>
{{/if}}
</p>
{{/if}}
{{#if website}}
<a id="userDocumentation" class="appslink" href="{{website}}" target="_blank" rel="noreferrer noopener"><?php p($l->t('Visit website'));?> ↗</a>
{{/if}}
{{#if bugs}}
<a id="adminDocumentation" class="appslink" href="{{bugs}}" target="_blank" rel="noreferrer noopener"><?php p($l->t('Report a bug'));?> ↗</a>
{{/if}}
</div><!-- end app-description-container -->
<div class="app-description-toggle-show" role="link"><?php p($l->t("Show description …"));?></div>
<div class="app-description-toggle-hide hidden" role="link"><?php p($l->t("Hide description …"));?></div>
<div class="app-dependencies update hidden">
<p><?php p($l->t('This app has an update available.')); ?></p>
</div>
{{#if missingMinOwnCloudVersion}}
<div class="app-dependencies">
<p><?php p($l->t('This app has no minimum Nextcloud version assigned. This will be an error in the future.')); ?></p>
</div>
{{else}}
{{#if missingMaxOwnCloudVersion}}
<div class="app-dependencies">
<p><?php p($l->t('This app has no maximum Nextcloud version assigned. This will be an error in the future.')); ?></p>
</div>
{{/if}}
{{/if}}
{{#unless canInstall}}
<div class="app-dependencies">
<p><?php p($l->t('This app cannot be installed because the following dependencies are not fulfilled:')); ?></p>
<ul class="missing-dependencies">
{{#each missingDependencies}}
<li>{{this}}</li>
{{/each}}
</ul>
</div>
{{/unless}}
<input class="update hidden" type="submit" value="<?php p($l->t('Update to %s', array('{{update}}'))); ?>" data-appid="{{id}}" />
{{#if active}}
<input class="enable" type="submit" data-appid="{{id}}" data-active="true" value="<?php p($l->t("Disable"));?>"/>
<div class="groups-enable">
<input type="checkbox" class="groups-enable__checkbox checkbox" id="groups_enable-{{id}}"/>
<label for="groups_enable-{{id}}"><?php p($l->t('Enable only for specific groups')); ?></label>
</div>
<input type="hidden" class="group_select" title="<?php p($l->t('All')); ?>" style="width: 200px">
{{else}}
<input class="enable{{#if needsDownload}} needs-download{{/if}}" type="submit" data-appid="{{id}}" data-active="false" {{#unless canInstall}}disabled="disabled"{{/unless}} value="<?php p($l->t("Enable"));?>"/>
{{/if}}
{{#if canUnInstall}}
<input class="uninstall" type="submit" value="<?php p($l->t('Remove')); ?>" data-appid="{{id}}" />
{{/if}}
<div class="warning hidden"></div>
</div>
</script>
<div id="app-navigation" class="icon-loading" data-category="<?php p($_['category']);?>">
<ul id="apps-categories">
</ul>
</div>
<div id="app-content" class="icon-loading">
<div id="apps-list"></div>
<div id="apps-list-empty" class="hidden emptycontent emptycontent-search">
<div id="app-list-empty-icon" class="icon-search"></div>
<h2><?php p($l->t('No apps found for your version')) ?></h2>
</div>
</div>

View File

@ -1,266 +0,0 @@
/**
* ownCloud
*
* @author Vincent Petry
* @copyright 2015 Vincent Petry <pvince81@owncloud.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*
*/
describe('OC.Settings.Apps tests', function() {
var Apps;
beforeEach(function() {
var $el = $('<div id="apps-list"></div>' +
'<div id="apps-list-empty" class="hidden"></div>' +
'<div id="app-template">' +
// dummy template for testing
'<div id="app-{{id}}" data-id="{{id}}" class="section">{{name}}</div>' +
'</div>'
);
$('#testArea').append($el);
Apps = OC.Settings.Apps;
});
afterEach(function() {
Apps.State.apps = null;
Apps.State.currentCategory = null;
});
describe('Filtering apps', function() {
var oldApps;
function loadApps(appList) {
Apps.State.apps = appList;
_.each(appList, function(appSpec) {
Apps.renderApp(appSpec);
});
}
function getResultsFromDom() {
var results = [];
$('#apps-list .section:not(.hidden)').each(function() {
results.push($(this).attr('data-id'));
});
return results;
}
beforeEach(function() {
loadApps([
{id: 'appone', name: 'App One', description: 'The first app', author: 'author1', level: 200},
{id: 'apptwo', name: 'App Two', description: 'The second app', author: 'author2', level: 100},
{id: 'appthree', name: 'App Three', description: 'Third app', author: 'author3', level: 0},
{id: 'somestuff', name: 'Some Stuff', description: 'whatever', author: 'author4', level: 0}
]);
});
it('returns no results when query does not match anything', function() {
expect(getResultsFromDom().length).toEqual(4);
expect($('#apps-list:not(.hidden)').length).toEqual(1);
expect($('#apps-list-empty:not(.hidden)').length).toEqual(0);
Apps.filter('absurdity');
expect(getResultsFromDom().length).toEqual(0);
expect($('#apps-list:not(.hidden)').length).toEqual(0);
expect($('#apps-list-empty:not(.hidden)').length).toEqual(1);
Apps.filter('');
expect(getResultsFromDom().length).toEqual(4);
expect($('#apps-list:not(.hidden)').length).toEqual(1);
expect($('#apps-list-empty:not(.hidden)').length).toEqual(0);
expect(getResultsFromDom().length).toEqual(4);
});
it('returns relevant results when query matches name', function() {
expect($('#apps-list:not(.hidden)').length).toEqual(1);
expect($('#apps-list-empty:not(.hidden)').length).toEqual(0);
var results;
Apps.filter('app');
results = getResultsFromDom();
expect(results.length).toEqual(3);
expect(results[0]).toEqual('appone');
expect(results[1]).toEqual('apptwo');
expect(results[2]).toEqual('appthree');
expect($('#apps-list:not(.hidden)').length).toEqual(1);
expect($('#apps-list-empty:not(.hidden)').length).toEqual(0);
});
it('returns relevant result when query matches name', function() {
var results;
Apps.filter('TWO');
results = getResultsFromDom();
expect(results.length).toEqual(1);
expect(results[0]).toEqual('apptwo');
});
it('returns relevant result when query matches description', function() {
var results;
Apps.filter('ever');
results = getResultsFromDom();
expect(results.length).toEqual(1);
expect(results[0]).toEqual('somestuff');
});
it('returns relevant results when query matches author name', function() {
var results;
Apps.filter('author');
results = getResultsFromDom();
expect(results.length).toEqual(4);
expect(results[0]).toEqual('appone');
expect(results[1]).toEqual('apptwo');
expect(results[2]).toEqual('appthree');
expect(results[3]).toEqual('somestuff');
});
it('returns relevant result when query matches author name', function() {
var results;
Apps.filter('thor3');
results = getResultsFromDom();
expect(results.length).toEqual(1);
expect(results[0]).toEqual('appthree');
});
it('returns relevant result when query matches level name', function() {
var results;
Apps.filter('Offic');
results = getResultsFromDom();
expect(results.length).toEqual(1);
expect(results[0]).toEqual('appone');
});
it('returns relevant result when query matches level name', function() {
var results;
Apps.filter('Appro');
results = getResultsFromDom();
expect(results.length).toEqual(1);
expect(results[0]).toEqual('apptwo');
});
it('returns relevant result when query matches level name', function() {
var results;
Apps.filter('Exper');
results = getResultsFromDom();
expect(results.length).toEqual(2);
expect(results[0]).toEqual('appthree');
expect(results[1]).toEqual('somestuff');
});
});
describe('loading categories', function() {
var suite = this;
beforeEach( function(){
suite.server = sinon.fakeServer.create();
});
afterEach( function(){
suite.server.restore();
});
function getResultsFromDom() {
var results = [];
$('#apps-list .section:not(.hidden)').each(function() {
results.push($(this).attr('data-id'));
});
return results;
}
it('does not sort applications using the level', function() {
Apps.loadCategory('TestId');
suite.server.requests[0].respond(
200,
{
'Content-Type': 'application/json'
},
JSON.stringify({
apps: [
{
id: 'foo',
name: 'Foo app',
description: 'Hello',
level: 0,
author: 'foo'
},
{
id: 'alpha',
name: 'Alpha app',
description: 'Hello',
level: 300,
author: ['alpha', 'beta']
},
{
id: 'nolevel',
name: 'No level',
description: 'Hello',
author: 'bar'
},
{
id: 'zork',
name: 'Some famous adventure game',
description: 'Hello',
level: 200,
author: 'baz'
},
{
id: 'delta',
name: 'Mathematical symbol',
description: 'Hello',
level: 200,
author: 'foobar'
}
]
})
);
var results = getResultsFromDom();
expect(results.length).toEqual(5);
expect(results).toEqual(['alpha', 'foo', 'delta', 'nolevel', 'zork']);
expect(OC.Settings.Apps.State.apps).toEqual({
'foo': {
id: 'foo',
name: 'Foo app',
description: 'Hello',
level: 0,
author: 'foo'
},
'alpha': {
id: 'alpha',
name: 'Alpha app',
description: 'Hello',
level: 300,
author: ['alpha', 'beta']
},
'nolevel': {
id: 'nolevel',
name: 'No level',
description: 'Hello',
author: 'bar'
},
'zork': {
id: 'zork',
name: 'Some famous adventure game',
description: 'Hello',
level: 200,
author: 'baz',
},
'delta': {
id: 'delta',
name: 'Mathematical symbol',
description: 'Hello',
level: 200,
author: 'foobar'
}
});
});
});
});

View File

@ -30,6 +30,7 @@ use OC\Settings\Controller\AppSettingsController;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\ILogger;
use OCP\IURLGenerator;
use OCP\L10N\IFactory;
use Test\TestCase;
@ -69,6 +70,8 @@ class AppSettingsControllerTest extends TestCase {
private $installer;
/** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject */
private $urlGenerator;
/** @var ILogger|\PHPUnit_Framework_MockObject_MockObject */
private $logger;
public function setUp() {
parent::setUp();
@ -87,6 +90,7 @@ class AppSettingsControllerTest extends TestCase {
$this->bundleFetcher = $this->createMock(BundleFetcher::class);
$this->installer = $this->createMock(Installer::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->logger = $this->createMock(ILogger::class);
$this->appSettingsController = new AppSettingsController(
'settings',
@ -100,7 +104,8 @@ class AppSettingsControllerTest extends TestCase {
$this->l10nFactory,
$this->bundleFetcher,
$this->installer,
$this->urlGenerator
$this->urlGenerator,
$this->logger
);
}
@ -109,32 +114,6 @@ class AppSettingsControllerTest extends TestCase {
->method('isUpdateAvailable')
->willReturn(false);
$expected = new JSONResponse([
[
'id' => 2,
'ident' => 'installed',
'displayName' => 'Your apps',
],
[
'id' => 4,
'ident' => 'updates',
'displayName' => 'Updates',
'counter' => 0,
],
[
'id' => 0,
'ident' => 'enabled',
'displayName' => 'Enabled apps',
],
[
'id' => 1,
'ident' => 'disabled',
'displayName' => 'Disabled apps',
],
[
'id' => 3,
'ident' => 'app-bundles',
'displayName' => 'App bundles',
],
[
'id' => 'auth',
'ident' => 'auth',
@ -196,6 +175,10 @@ class AppSettingsControllerTest extends TestCase {
}
public function testViewApps() {
$this->bundleFetcher->expects($this->once())->method('getBundles')->willReturn([]);
$this->installer->expects($this->any())
->method('isUpdateAvailable')
->willReturn(false);
$this->config
->expects($this->once())
->method('getSystemValue')
@ -210,11 +193,14 @@ class AppSettingsControllerTest extends TestCase {
$policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com');
$expected = new TemplateResponse('settings',
'apps',
'settings',
[
'category' => 'installed',
'appstoreEnabled' => true,
'urlGenerator' => $this->urlGenerator,
'serverData' => [
'updateCount' => 0,
'appstoreEnabled' => true,
'bundles' => [],
'developerDocumentation' => ''
]
],
'user');
$expected->setContentSecurityPolicy($policy);
@ -223,6 +209,10 @@ class AppSettingsControllerTest extends TestCase {
}
public function testViewAppsAppstoreNotEnabled() {
$this->installer->expects($this->any())
->method('isUpdateAvailable')
->willReturn(false);
$this->bundleFetcher->expects($this->once())->method('getBundles')->willReturn([]);
$this->config
->expects($this->once())
->method('getSystemValue')
@ -237,11 +227,14 @@ class AppSettingsControllerTest extends TestCase {
$policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com');
$expected = new TemplateResponse('settings',
'apps',
'settings',
[
'category' => 'installed',
'appstoreEnabled' => false,
'urlGenerator' => $this->urlGenerator,
'serverData' => [
'updateCount' => 0,
'appstoreEnabled' => false,
'bundles' => [],
'developerDocumentation' => ''
]
],
'user');
$expected->setContentSecurityPolicy($policy);