From ce94a998dd5a5801beef7874dd13752095e35de0 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Fri, 31 Oct 2014 11:41:07 +0100 Subject: [PATCH 1/9] Use storage id + appframework for ext storage CRUD - Added StorageConfig class to replace ugly arrays - Implemented StorageService and StorageController for Global and User storages - Async status checking for storages (from Xenopathic) - Auto-generate id for external storage configs (not the same as storage_id) - Refactor JS classes for external storage settings, this mostly moves/encapsulated existing global event handlers into the MountConfigListView class. - Added some JS unit tests for the external storage UI --- apps/files_external/ajax/addMountPoint.php | 26 - apps/files_external/ajax/removeMountPoint.php | 23 - apps/files_external/appinfo/app.php | 2 + apps/files_external/appinfo/application.php | 7 +- apps/files_external/appinfo/routes.php | 38 +- .../controller/globalstoragescontroller.php | 145 +++ .../controller/storagescontroller.php | 157 +++ .../controller/userstoragescontroller.php | 172 +++ apps/files_external/js/dropbox.js | 4 +- apps/files_external/js/google.js | 4 +- apps/files_external/js/settings.js | 1047 +++++++++++------ apps/files_external/js/sftp_key.js | 2 +- apps/files_external/lib/config.php | 51 +- apps/files_external/lib/notfoundexception.php | 15 + apps/files_external/lib/storageconfig.php | 243 ++++ .../service/globalstoragesservice.php | 192 +++ .../service/storagesservice.php | 303 +++++ .../service/userstoragesservice.php | 142 +++ apps/files_external/templates/settings.php | 8 +- .../globalstoragescontrollertest.php | 41 + .../controller/storagescontrollertest.php | 217 ++++ .../controller/userstoragescontrollertest.php | 112 ++ apps/files_external/tests/js/settingsSpec.js | 164 +++ .../service/globalstoragesservicetest.php | 705 +++++++++++ .../tests/service/storagesservicetest.php | 180 +++ .../tests/service/userstoragesservicetest.php | 200 ++++ .../tests/storageconfigtest.php | 50 + core/js/tests/specHelper.js | 5 + tests/karma.config.js | 3 +- 29 files changed, 3803 insertions(+), 455 deletions(-) delete mode 100644 apps/files_external/ajax/addMountPoint.php delete mode 100644 apps/files_external/ajax/removeMountPoint.php create mode 100644 apps/files_external/controller/globalstoragescontroller.php create mode 100644 apps/files_external/controller/storagescontroller.php create mode 100644 apps/files_external/controller/userstoragescontroller.php create mode 100644 apps/files_external/lib/notfoundexception.php create mode 100644 apps/files_external/lib/storageconfig.php create mode 100644 apps/files_external/service/globalstoragesservice.php create mode 100644 apps/files_external/service/storagesservice.php create mode 100644 apps/files_external/service/userstoragesservice.php create mode 100644 apps/files_external/tests/controller/globalstoragescontrollertest.php create mode 100644 apps/files_external/tests/controller/storagescontrollertest.php create mode 100644 apps/files_external/tests/controller/userstoragescontrollertest.php create mode 100644 apps/files_external/tests/js/settingsSpec.js create mode 100644 apps/files_external/tests/service/globalstoragesservicetest.php create mode 100644 apps/files_external/tests/service/storagesservicetest.php create mode 100644 apps/files_external/tests/service/userstoragesservicetest.php create mode 100644 apps/files_external/tests/storageconfigtest.php diff --git a/apps/files_external/ajax/addMountPoint.php b/apps/files_external/ajax/addMountPoint.php deleted file mode 100644 index 4e27ef9875..0000000000 --- a/apps/files_external/ajax/addMountPoint.php +++ /dev/null @@ -1,26 +0,0 @@ - array('message' => $status))); diff --git a/apps/files_external/ajax/removeMountPoint.php b/apps/files_external/ajax/removeMountPoint.php deleted file mode 100644 index 0870911544..0000000000 --- a/apps/files_external/ajax/removeMountPoint.php +++ /dev/null @@ -1,23 +0,0 @@ -getL10N('files_external'); OC::$CLASSPATH['OC\Files\Storage\StreamWrapper'] = 'files_external/lib/streamwrapper.php'; diff --git a/apps/files_external/appinfo/application.php b/apps/files_external/appinfo/application.php index b1605bb98a..3e6b80ccb4 100644 --- a/apps/files_external/appinfo/application.php +++ b/apps/files_external/appinfo/application.php @@ -12,12 +12,13 @@ use \OCA\Files_External\Controller\AjaxController; use \OCP\AppFramework\App; use \OCP\IContainer; - /** - * @package OCA\Files_External\Appinfo - */ +/** + * @package OCA\Files_External\Appinfo + */ class Application extends App { public function __construct(array $urlParams=array()) { parent::__construct('files_external', $urlParams); + $container = $this->getContainer(); /** diff --git a/apps/files_external/appinfo/routes.php b/apps/files_external/appinfo/routes.php index 5c7c4eca90..506c9d34e2 100644 --- a/apps/files_external/appinfo/routes.php +++ b/apps/files_external/appinfo/routes.php @@ -22,28 +22,30 @@ namespace OCA\Files_External\Appinfo; +/** + * @var $this OC\Route\Router + **/ + $application = new Application(); $application->registerRoutes( - $this, - array( - 'routes' => array( - array( - 'name' => 'Ajax#getSshKeys', - 'url' => '/ajax/sftp_key.php', - 'verb' => 'POST', - 'requirements' => array() - ) - ) - ) + $this, + array( + 'resources' => array( + 'global_storages' => array('url' => '/globalstorages'), + 'user_storages' => array('url' => '/userstorages'), + ), + 'routes' => array( + array( + 'name' => 'Ajax#getSshKeys', + 'url' => '/ajax/sftp_key.php', + 'verb' => 'POST', + 'requirements' => array() + ) + ) + ) ); -/** @var $this OC\Route\Router */ - -$this->create('files_external_add_mountpoint', 'ajax/addMountPoint.php') - ->actionInclude('files_external/ajax/addMountPoint.php'); -$this->create('files_external_remove_mountpoint', 'ajax/removeMountPoint.php') - ->actionInclude('files_external/ajax/removeMountPoint.php'); - +// TODO: move these to app framework $this->create('files_external_add_root_certificate', 'ajax/addRootCertificate.php') ->actionInclude('files_external/ajax/addRootCertificate.php'); $this->create('files_external_remove_root_certificate', 'ajax/removeRootCertificate.php') diff --git a/apps/files_external/controller/globalstoragescontroller.php b/apps/files_external/controller/globalstoragescontroller.php new file mode 100644 index 0000000000..3aa64f0d85 --- /dev/null +++ b/apps/files_external/controller/globalstoragescontroller.php @@ -0,0 +1,145 @@ + + * @copyright Vincent Petry 2015 + */ + +namespace OCA\Files_External\Controller; + + +use \OCP\IConfig; +use \OCP\IUserSession; +use \OCP\IRequest; +use \OCP\AppFramework\Http\DataResponse; +use \OCP\AppFramework\Controller; +use \OCP\AppFramework\Http; +use \OCA\Files_external\Service\GlobalStoragesService; +use \OCA\Files_external\NotFoundException; +use \OCA\Files_external\Lib\StorageConfig; + +class GlobalStoragesController extends StoragesController { + /** + * @param string $appName + * @param IRequest $request + * @param IConfig $config + * @param GlobalStoragesService $globalStoragesService + */ + public function __construct( + $AppName, + IRequest $request, + \OCP\IL10N $l10n, + GlobalStoragesService $globalStoragesService + ){ + parent::__construct( + $AppName, + $request, + $l10n, + $globalStoragesService + ); + } + + /** + * Create an external storage entry. + * + * @param string $mountPoint storage mount point + * @param string $backendClass backend class name + * @param array $backendOptions backend-specific options + * @param array $applicableUsers users for which to mount the storage + * @param array $applicableGroups groups for which to mount the storage + * @param int $priority priority + * + * @return DataResponse + */ + public function create( + $mountPoint, + $backendClass, + $backendOptions, + $applicableUsers, + $applicableGroups, + $priority + ) { + $newStorage = new StorageConfig(); + $newStorage->setMountPoint($mountPoint); + $newStorage->setBackendClass($backendClass); + $newStorage->setBackendOptions($backendOptions); + $newStorage->setApplicableUsers($applicableUsers); + $newStorage->setApplicableGroups($applicableGroups); + $newStorage->setPriority($priority); + + $response = $this->validate($newStorage); + if (!empty($response)) { + return $response; + } + + $newStorage = $this->service->addStorage($newStorage); + + $this->updateStorageStatus($newStorage); + + return new DataResponse( + $newStorage, + Http::STATUS_CREATED + ); + } + + /** + * Update an external storage entry. + * + * @param int $id storage id + * @param string $mountPoint storage mount point + * @param string $backendClass backend class name + * @param array $backendOptions backend-specific options + * @param array $applicableUsers users for which to mount the storage + * @param array $applicableGroups groups for which to mount the storage + * @param int $priority priority + * + * @return DataResponse + */ + public function update( + $id, + $mountPoint, + $backendClass, + $backendOptions, + $applicableUsers, + $applicableGroups, + $priority + ) { + $storage = new StorageConfig($id); + $storage->setMountPoint($mountPoint); + $storage->setBackendClass($backendClass); + $storage->setBackendOptions($backendOptions); + $storage->setApplicableUsers($applicableUsers); + $storage->setApplicableGroups($applicableGroups); + $storage->setPriority($priority); + + $response = $this->validate($storage); + if (!empty($response)) { + return $response; + } + + try { + $storage = $this->service->updateStorage($storage); + } catch (NotFoundException $e) { + return new DataResponse( + [ + 'message' => (string)$this->l10n->t('Storage with id "%i" not found', array($id)) + ], + Http::STATUS_NOT_FOUND + ); + } + + $this->updateStorageStatus($storage); + + return new DataResponse( + $storage, + Http::STATUS_OK + ); + + } + +} + diff --git a/apps/files_external/controller/storagescontroller.php b/apps/files_external/controller/storagescontroller.php new file mode 100644 index 0000000000..f047ba34b5 --- /dev/null +++ b/apps/files_external/controller/storagescontroller.php @@ -0,0 +1,157 @@ + + * @copyright Vincent Petry 2014 + */ + +namespace OCA\Files_External\Controller; + + +use \OCP\IConfig; +use \OCP\IUserSession; +use \OCP\IRequest; +use \OCP\AppFramework\Http\DataResponse; +use \OCP\AppFramework\Controller; +use \OCP\AppFramework\Http; +use \OCA\Files_external\Service\StoragesService; +use \OCA\Files_external\NotFoundException; +use \OCA\Files_external\Lib\StorageConfig; + +abstract class StoragesController extends Controller { + + /** + * @var \OCP\IL10N + */ + protected $l10n; + + /** + * @var StoragesService + */ + protected $service; + + /** + * @param string $appName + * @param IRequest $request + * @param IConfig $config + * @param StoragesService $storagesService + */ + public function __construct( + $AppName, + IRequest $request, + \OCP\IL10N $l10n, + StoragesService $storagesService + ){ + parent::__construct($AppName, $request); + $this->l10n = $l10n; + $this->service = $storagesService; + } + + /** + * Validate storage config + * + * @param StorageConfig $storage storage config + * + * @return DataResponse|null returns response in case of validation error + */ + protected function validate(StorageConfig $storage) { + $mountPoint = $storage->getMountPoint(); + if ($mountPoint === '' || $mountPoint === '/') { + return new DataResponse( + array( + 'message' => (string)$this->l10n->t('Invalid mount point') + ), + Http::STATUS_UNPROCESSABLE_ENTITY + ); + } + + // TODO: validate that other attrs are set + + $backends = \OC_Mount_Config::getBackends(); + if (!isset($backends[$storage->getBackendClass()])) { + // invalid backend + return new DataResponse( + array( + 'message' => (string)$this->l10n->t('Invalid storage backend "%s"', array($storage->getBackendClass())) + ), + Http::STATUS_UNPROCESSABLE_ENTITY + ); + } + + return null; + } + + /** + * Check whether the given storage is available / valid. + * + * Note that this operation can be time consuming depending + * on whether the remote storage is available or not. + * + * @param StorageConfig $storage + */ + protected function updateStorageStatus(StorageConfig &$storage) { + // update status (can be time-consuming) + $storage->setStatus( + \OC_Mount_Config::getBackendStatus( + $storage->getBackendClass(), + $storage->getBackendOptions(), + false + ) + ); + } + + /** + * Get an external storage entry. + * + * @param int $id storage id + * + * @return DataResponse + */ + public function show($id) { + try { + $storage = $this->service->getStorage($id); + + $this->updateStorageStatus($storage); + } catch (NotFoundException $e) { + return new DataResponse( + [ + 'message' => (string)$this->l10n->t('Storage with id "%i" not found', array($id)) + ], + Http::STATUS_NOT_FOUND + ); + } + + return new DataResponse( + $storage, + Http::STATUS_OK + ); + } + + /** + * Deletes the storage with the given id. + * + * @param int $id storage id + * + * @return DataResponse + */ + public function destroy($id) { + try { + $this->service->removeStorage($id); + } catch (NotFoundException $e) { + return new DataResponse( + [ + 'message' => (string)$this->l10n->t('Storage with id "%i" not found', array($id)) + ], + Http::STATUS_NOT_FOUND + ); + } + + return new DataResponse([], Http::STATUS_NO_CONTENT); + } + +} + diff --git a/apps/files_external/controller/userstoragescontroller.php b/apps/files_external/controller/userstoragescontroller.php new file mode 100644 index 0000000000..b77cbca59f --- /dev/null +++ b/apps/files_external/controller/userstoragescontroller.php @@ -0,0 +1,172 @@ + + * @copyright Vincent Petry 2015 + */ + +namespace OCA\Files_External\Controller; + + +use \OCP\IConfig; +use \OCP\IUserSession; +use \OCP\IRequest; +use \OCP\AppFramework\Http\DataResponse; +use \OCP\AppFramework\Controller; +use \OCP\AppFramework\Http; +use \OCA\Files_external\Service\UserStoragesService; +use \OCA\Files_external\NotFoundException; +use \OCA\Files_external\Lib\StorageConfig; + +class UserStoragesController extends StoragesController { + /** + * @param string $appName + * @param IRequest $request + * @param IConfig $config + * @param UserStoragesService $userStoragesService + */ + public function __construct( + $AppName, + IRequest $request, + \OCP\IL10N $l10n, + UserStoragesService $userStoragesService + ){ + parent::__construct( + $AppName, + $request, + $l10n, + $userStoragesService + ); + } + + /** + * Validate storage config + * + * @param StorageConfig $storage storage config + * + * @return DataResponse|null returns response in case of validation error + */ + protected function validate(StorageConfig $storage) { + $result = parent::validate($storage); + + if ($result != null) { + return $result; + } + + // Verify that the mount point applies for the current user + // Prevent non-admin users from mounting local storage and other disabled backends + $allowedBackends = \OC_Mount_Config::getPersonalBackends(); + if (!isset($allowedBackends[$storage->getBackendClass()])) { + return new DataResponse( + array( + 'message' => (string)$this->l10n->t('Invalid storage backend "%s"', array($storage->getBackendClass())) + ), + Http::STATUS_UNPROCESSABLE_ENTITY + ); + } + + return null; + } + + /** + * @NoAdminRequired + * @{inheritdoc} + */ + public function show($id) { + return parent::show($id); + } + + /** + * Create an external storage entry. + * + * @param string $mountPoint storage mount point + * @param string $backendClass backend class name + * @param array $backendOptions backend-specific options + * + * @return DataResponse + * + * @NoAdminRequired + */ + public function create( + $mountPoint, + $backendClass, + $backendOptions + ) { + $newStorage = new StorageConfig(); + $newStorage->setMountPoint($mountPoint); + $newStorage->setBackendClass($backendClass); + $newStorage->setBackendOptions($backendOptions); + + $response = $this->validate($newStorage); + if (!empty($response)) { + return $response; + } + + $newStorage = $this->service->addStorage($newStorage); + $this->updateStorageStatus($newStorage); + + return new DataResponse( + $newStorage, + Http::STATUS_CREATED + ); + } + + /** + * Update an external storage entry. + * + * @param int $id storage id + * @param string $mountPoint storage mount point + * @param string $backendClass backend class name + * @param array $backendOptions backend-specific options + * + * @return DataResponse + */ + public function update( + $id, + $mountPoint, + $backendClass, + $backendOptions + ) { + $storage = new StorageConfig($id); + $storage->setMountPoint($mountPoint); + $storage->setBackendClass($backendClass); + $storage->setBackendOptions($backendOptions); + + $response = $this->validate($storage); + if (!empty($response)) { + return $response; + } + + try { + $storage = $this->service->updateStorage($storage); + } catch (NotFoundException $e) { + return new DataResponse( + [ + 'message' => (string)$this->l10n->t('Storage with id "%i" not found', array($id)) + ], + Http::STATUS_NOT_FOUND + ); + } + + $this->updateStorageStatus($storage); + + return new DataResponse( + $storage, + Http::STATUS_OK + ); + + } + + /** + * {@inheritdoc} + * @NoAdminRequired + */ + public function destroy($id) { + return parent::destroy($id); + } +} + diff --git a/apps/files_external/js/dropbox.js b/apps/files_external/js/dropbox.js index 2880e910f2..53b5d5d666 100644 --- a/apps/files_external/js/dropbox.js +++ b/apps/files_external/js/dropbox.js @@ -23,7 +23,7 @@ $(document).ready(function() { $(token).val(result.access_token); $(token_secret).val(result.access_token_secret); $(configured).val('true'); - OC.MountConfig.saveStorage(tr, function(status) { + OCA.External.Settings.mountConfig.saveStorageConfig(tr, function(status) { if (status) { $(tr).find('.configuration input').attr('disabled', 'disabled'); $(tr).find('.configuration').append(''+t('files_external', 'Access granted')+''); @@ -93,7 +93,7 @@ $(document).ready(function() { $(configured).val('false'); $(token).val(result.data.request_token); $(token_secret).val(result.data.request_token_secret); - OC.MountConfig.saveStorage(tr, function() { + OCA.External.Settings.mountConfig.saveStorageConfig(tr, function() { window.location = result.data.url; }); } else { diff --git a/apps/files_external/js/google.js b/apps/files_external/js/google.js index b9a5e66b80..648538f802 100644 --- a/apps/files_external/js/google.js +++ b/apps/files_external/js/google.js @@ -32,7 +32,7 @@ $(document).ready(function() { if (result && result.status == 'success') { $(token).val(result.data.token); $(configured).val('true'); - OC.MountConfig.saveStorage(tr, function(status) { + OCA.External.Settings.mountConfig.saveStorageConfig(tr, function(status) { if (status) { $(tr).find('.configuration input').attr('disabled', 'disabled'); $(tr).find('.configuration').append($('') @@ -115,7 +115,7 @@ $(document).ready(function() { if (result && result.status == 'success') { $(configured).val('false'); $(token).val('false'); - OC.MountConfig.saveStorage(tr, function(status) { + OCA.External.Settings.mountConfig.saveStorageConfig(tr, function(status) { window.location = result.data.url; }); } else { diff --git a/apps/files_external/js/settings.js b/apps/files_external/js/settings.js index ee3d0b736d..b3567b7ebf 100644 --- a/apps/files_external/js/settings.js +++ b/apps/files_external/js/settings.js @@ -1,20 +1,24 @@ +/* + * Copyright (c) 2014 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ (function(){ -function updateStatus(statusEl, result){ - statusEl.removeClass('success error loading-small'); - if (result && result.status === 'success' && result.data.message) { - statusEl.addClass('success'); - return true; - } else { - statusEl.addClass('error'); - return false; - } -} - +/** + * Returns the selection of applicable users in the given configuration row + * + * @param $row configuration row + * @return array array of user names + */ function getSelection($row) { var values = $row.find('.applicableUsers').select2('val'); if (!values || values.length === 0) { - values = ['all']; + values = []; } return values; } @@ -31,298 +35,493 @@ function highlightInput($input) { } } -OC.MountConfig={ - saveStorage:function($tr, callback) { - var mountPoint = $tr.find('.mountPoint input').val(); - var oldMountPoint = $tr.find('.mountPoint input').data('mountpoint'); - if (mountPoint === '') { - return false; - } - var statusSpan = $tr.find('.status span'); - var backendClass = $tr.find('.backend').data('class'); - var configuration = $tr.find('.configuration input'); - var addMountPoint = true; - if (configuration.length < 1) { - return false; - } - var classOptions = {}; - $.each(configuration, function(index, input) { - if ($(input).val() === '' && !$(input).hasClass('optional')) { - addMountPoint = false; - return false; - } - if ($(input).is(':checkbox')) { - if ($(input).is(':checked')) { - classOptions[$(input).data('parameter')] = true; +/** + * Initialize select2 plugin on the given elements + * + * @param {Array} array of jQuery elements + * @param {int} userListLimit page size for result list + */ +function addSelect2 ($elements, userListLimit) { + if (!$elements.length) { + return; + } + $elements.select2({ + placeholder: t('files_external', 'All users. Type to select user or group.'), + allowClear: true, + multiple: true, + //minimumInputLength: 1, + ajax: { + url: OC.generateUrl('apps/files_external/applicable'), + dataType: 'json', + quietMillis: 100, + data: function (term, page) { // page is the one-based page number tracked by Select2 + return { + pattern: term, //search term + limit: userListLimit, // page size + offset: userListLimit*(page-1) // page number starts with 0 + }; + }, + results: function (data) { + if (data.status === 'success') { + + var results = []; + var userCount = 0; // users is an object + + // add groups + $.each(data.groups, function(i, group) { + results.push({name:group+'(group)', displayname:group, type:'group' }); + }); + // add users + $.each(data.users, function(id, user) { + userCount++; + results.push({name:id, displayname:user, type:'user' }); + }); + + + var more = (userCount >= userListLimit) || (data.groups.length >= userListLimit); + return {results: results, more: more}; } else { - classOptions[$(input).data('parameter')] = false; + //FIXME add error handling } + } + }, + initSelection: function(element, callback) { + var users = {}; + users['users'] = []; + var toSplit = element.val().split(","); + for (var i = 0; i < toSplit.length; i++) { + users['users'].push(toSplit[i]); + } + + $.ajax(OC.generateUrl('displaynames'), { + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(users), + dataType: 'json' + }).done(function(data) { + var results = []; + if (data.status === 'success') { + $.each(data.users, function(user, displayname) { + if (displayname !== false) { + results.push({name:user, displayname:displayname, type:'user'}); + } + }); + callback(results); + } else { + //FIXME add error handling + } + }); + }, + id: function(element) { + return element.name; + }, + formatResult: function (element) { + var $result = $('
'+escapeHTML(element.displayname)+''); + var $div = $result.find('.avatardiv') + .attr('data-type', element.type) + .attr('data-name', element.name) + .attr('data-displayname', element.displayname); + if (element.type === 'group') { + var url = OC.imagePath('core','places/contacts-dark'); // TODO better group icon + $div.html(''); + } + return $result.get(0).outerHTML; + }, + formatSelection: function (element) { + if (element.type === 'group') { + return ''+escapeHTML(element.displayname+' '+t('files_external', '(group)'))+''; } else { - classOptions[$(input).data('parameter')] = $(input).val(); + return ''+escapeHTML(element.displayname)+''; + } + }, + escapeMarkup: function (m) { return m; } // we escape the markup in formatResult and formatSelection + }).on('select2-loaded', function() { + $.each($('.avatardiv'), function(i, div) { + var $div = $(div); + if ($div.data('type') === 'user') { + $div.avatar($div.data('name'),32); } }); - if ($('#externalStorage').data('admin') === true) { - var multiselect = getSelection($tr); + }); +} + +/** + * @class OCA.External.Settings.StorageConfig + * + * @classdesc External storage config + */ +var StorageConfig = function(id) { + this.id = id; + this.backendOptions = {}; +}; +// Keep this in sync with \OC_Mount_Config::STATUS_* +StorageConfig.Status = { + IN_PROGRESS: -1, + SUCCESS: 0, + ERROR: 1 +}; +/** + * @memberof OCA.External.Settings + */ +StorageConfig.prototype = { + _url: null, + + /** + * Storage id + * + * @type int + */ + id: null, + + /** + * Mount point + * + * @type string + */ + mountPoint: '', + + /** + * Backend class name + * + * @type string + */ + backendClass: null, + + /** + * Backend-specific configuration + * + * @type Object. + */ + backendOptions: null, + + /** + * Creates or saves the storage. + * + * @param {Function} [options.success] success callback, receives result as argument + * @param {Function} [options.error] error callback + */ + save: function(options) { + var self = this; + var url = OC.generateUrl(this._url); + var method = 'POST'; + if (_.isNumber(this.id)) { + method = 'PUT'; + url = OC.generateUrl(this._url + '/{id}', {id: this.id}); } - if (addMountPoint) { - var status = false; - if ($('#externalStorage').data('admin') === true) { - var isPersonal = false; - var oldGroups = $tr.find('.applicable').data('applicable-groups'); - var oldUsers = $tr.find('.applicable').data('applicable-users'); - var groups = []; - var users = []; - $.each(multiselect, function(index, value) { - var pos = value.indexOf('(group)'); - if (pos != -1) { - var mountType = 'group'; - var applicable = value.substr(0, pos); - if ($.inArray(applicable, oldGroups) != -1) { - oldGroups.splice($.inArray(applicable, oldGroups), 1); - } - groups.push(applicable); - } else { - var mountType = 'user'; - var applicable = value; - if ($.inArray(applicable, oldUsers) != -1) { - oldUsers.splice($.inArray(applicable, oldUsers), 1); - } - users.push(applicable); - } - statusSpan.addClass('loading-small').removeClass('error success'); - $.ajax({type: 'POST', - url: OC.filePath('files_external', 'ajax', 'addMountPoint.php'), - data: { - mountPoint: mountPoint, - 'class': backendClass, - classOptions: classOptions, - mountType: mountType, - applicable: applicable, - isPersonal: isPersonal, - oldMountPoint: oldMountPoint - }, - success: function(result) { - $tr.find('.mountPoint input').data('mountpoint', mountPoint); - status = updateStatus(statusSpan, result); - if (callback) { - callback(status); - } - }, - error: function(result){ - status = updateStatus(statusSpan, result); - if (callback) { - callback(status); - } - } - }); - }); - $tr.find('.applicable').data('applicable-groups', groups); - $tr.find('.applicable').data('applicable-users', users); - var mountType = 'group'; - $.each(oldGroups, function(index, applicable) { - $.ajax({type: 'POST', - url: OC.filePath('files_external', 'ajax', 'removeMountPoint.php'), - data: { - mountPoint: mountPoint, - 'class': backendClass, - classOptions: classOptions, - mountType: mountType, - applicable: applicable, - isPersonal: isPersonal - } - }); - }); - var mountType = 'user'; - $.each(oldUsers, function(index, applicable) { - $.ajax({type: 'POST', - url: OC.filePath('files_external', 'ajax', 'removeMountPoint.php'), - data: { - mountPoint: mountPoint, - 'class': backendClass, - classOptions: classOptions, - mountType: mountType, - applicable: applicable, - isPersonal: isPersonal - } - }); - }); - } else { - var isPersonal = true; - var mountType = 'user'; - var applicable = OC.currentUser; - statusSpan.addClass('loading-small').removeClass('error success'); - $.ajax({type: 'POST', - url: OC.filePath('files_external', 'ajax', 'addMountPoint.php'), - data: { - mountPoint: mountPoint, - 'class': backendClass, - classOptions: classOptions, - mountType: mountType, - applicable: applicable, - isPersonal: isPersonal, - oldMountPoint: oldMountPoint - }, - success: function(result) { - $tr.find('.mountPoint input').data('mountpoint', mountPoint); - status = updateStatus(statusSpan, result); - if (callback) { - callback(status); - } - }, - error: function(result){ - status = updateStatus(statusSpan, result); - if (callback) { - callback(status); - } - } - }); + + $.ajax({ + type: method, + url: url, + data: this.getData(), + success: function(result) { + self.id = result.id; + if (_.isFunction(options.success)) { + options.success(result); + } + }, + error: options.error + }); + }, + + /** + * Returns the data from this object + * + * @return {Array} JSON array of the data + */ + getData: function() { + var data = { + mountPoint: this.mountPoint, + backendClass: this.backendClass, + backendOptions: this.backendOptions + }; + if (this.id) { + data.id = this.id; + } + return data; + }, + + /** + * Recheck the storage + * + * @param {Function} [options.success] success callback, receives result as argument + * @param {Function} [options.error] error callback + */ + recheck: function(options) { + if (!_.isNumber(this.id)) { + if (_.isFunction(options.error)) { + options.error(); } - return status; + return; } + $.ajax({ + type: 'GET', + url: OC.generateUrl(this._url + '/{id}', {id: this.id}), + success: options.success, + error: options.error + }); + }, + + /** + * Deletes the storage + * + * @param {Function} [options.success] success callback + * @param {Function} [options.error] error callback + */ + destroy: function(options) { + if (!_.isNumber(this.id)) { + // the storage hasn't even been created => success + if (_.isFunction(options.success)) { + options.success(); + } + return; + } + var self = this; + $.ajax({ + type: 'DELETE', + url: OC.generateUrl(this._url + '/{id}', {id: this.id}), + success: options.success, + error: options.error + }); + }, + + /** + * Validate this model + * + * @return {boolean} false if errors exist, true otherwise + */ + validate: function() { + if (this.mountPoint === '') { + return false; + } + if (this.errors) { + return false; + } + return true; } }; -$(document).ready(function() { - var $externalStorage = $('#externalStorage'); +/** + * @class OCA.External.Settings.GlobalStorageConfig + * @augments OCA.External.Settings.StorageConfig + * + * @classdesc Global external storage config + */ +var GlobalStorageConfig = function(id) { + this.id = id; + this.applicableUsers = []; + this.applicableGroups = []; +}; +/** + * @memberOf OCA.External.Settings + */ +GlobalStorageConfig.prototype = _.extend({}, StorageConfig.prototype, + /** @lends OCA.External.Settings.GlobalStorageConfig.prototype */ { + _url: 'apps/files_external/globalstorages', - //initialize hidden input field with list of users and groups - $externalStorage.find('tr:not(#addMountPoint)').each(function(i,tr) { - var $tr = $(tr); - var $applicable = $tr.find('.applicable'); - if ($applicable.length > 0) { - var groups = $applicable.data('applicable-groups'); - var groupsId = []; - $.each(groups, function () { - groupsId.push(this + '(group)'); - }); - var users = $applicable.data('applicable-users'); - if (users.indexOf('all') > -1) { - $tr.find('.applicableUsers').val(''); - } else { - $tr.find('.applicableUsers').val(groupsId.concat(users).join(',')); - } - } - }); + /** + * Applicable users + * + * @type Array. + */ + applicableUsers: null, - var userListLimit = 30; - function addSelect2 ($elements) { - if ($elements.length > 0) { - $elements.select2({ - placeholder: t('files_external', 'All users. Type to select user or group.'), - allowClear: true, - multiple: true, - //minimumInputLength: 1, - ajax: { - url: OC.generateUrl('apps/files_external/applicable'), - dataType: 'json', - quietMillis: 100, - data: function (term, page) { // page is the one-based page number tracked by Select2 - return { - pattern: term, //search term - limit: userListLimit, // page size - offset: userListLimit*(page-1) // page number starts with 0 - }; - }, - results: function (data, page) { - if (data.status === 'success') { + /** + * Applicable groups + * + * @type Array. + */ + applicableGroups: null, - var results = []; - var userCount = 0; // users is an object - - // add groups - $.each(data.groups, function(i, group) { - results.push({name:group+'(group)', displayname:group, type:'group' }); - }); - // add users - $.each(data.users, function(id, user) { - userCount++; - results.push({name:id, displayname:user, type:'user' }); - }); - - - var more = (userCount >= userListLimit) || (data.groups.length >= userListLimit); - return {results: results, more: more}; - } else { - //FIXME add error handling - } - } - }, - initSelection: function(element, callback) { - var users = {}; - users['users'] = []; - var toSplit = element.val().split(","); - for (var i = 0; i < toSplit.length; i++) { - users['users'].push(toSplit[i]); - } - - $.ajax(OC.generateUrl('displaynames'), { - type: 'POST', - contentType: 'application/json', - data: JSON.stringify(users), - dataType: 'json' - }).done(function(data) { - var results = []; - if (data.status === 'success') { - $.each(data.users, function(user, displayname) { - if (displayname !== false) { - results.push({name:user, displayname:displayname, type:'user'}); - } - }); - callback(results); - } else { - //FIXME add error handling - } - }); - }, - id: function(element) { - return element.name; - }, - formatResult: function (element) { - var $result = $('
'+escapeHTML(element.displayname)+''); - var $div = $result.find('.avatardiv') - .attr('data-type', element.type) - .attr('data-name', element.name) - .attr('data-displayname', element.displayname); - if (element.type === 'group') { - var url = OC.imagePath('core','places/contacts-dark'); // TODO better group icon - $div.html(''); - } - return $result.get(0).outerHTML; - }, - formatSelection: function (element) { - if (element.type === 'group') { - return ''+escapeHTML(element.displayname+' '+t('files_external', '(group)'))+''; - } else { - return ''+escapeHTML(element.displayname)+''; - } - }, - escapeMarkup: function (m) { return m; } // we escape the markup in formatResult and formatSelection - }).on('select2-loaded', function() { - $.each($('.avatardiv'), function(i, div) { - var $div = $(div); - if ($div.data('type') === 'user') { - $div.avatar($div.data('name'),32); - } - }) - }); - } + /** + * Returns the data from this object + * + * @return {Array} JSON array of the data + */ + getData: function() { + var data = StorageConfig.prototype.getData.apply(this, arguments); + return _.extend(data, { + applicableUsers: this.applicableUsers, + applicableGroups: this.applicableGroups, + }); } - addSelect2($('tr:not(#addMountPoint) .applicableUsers')); - - $externalStorage.on('change', '#selectBackend', function() { - var $tr = $(this).closest('tr'); - $externalStorage.find('tbody').append($tr.clone()); - $externalStorage.find('tbody tr').last().find('.mountPoint input').val(''); - var selected = $(this).find('option:selected').text(); - var backendClass = $(this).val(); +}); + +/** + * @class OCA.External.Settings.UserStorageConfig + * @augments OCA.External.Settings.StorageConfig + * + * @classdesc User external storage config + */ +var UserStorageConfig = function(id) { + this.id = id; +}; +UserStorageConfig.prototype = _.extend({}, StorageConfig.prototype, + /** @lends OCA.External.Settings.UserStorageConfig.prototype */ { + _url: 'apps/files_external/userstorages' +}); + +/** + * @class OCA.External.Settings.MountConfigListView + * + * @classdesc Mount configuration list view + * + * @param {Object} $el DOM object containing the list + * @param {Object} [options] + * @param {int} [options.userListLimit] page size in applicable users dropdown + */ +var MountConfigListView = function($el, options) { + this.initialize($el, options); +}; +/** + * @memberOf OCA.External.Settings + */ +MountConfigListView.prototype = { + + /** + * jQuery element containing the config list + * + * @type Object + */ + $el: null, + + /** + * Storage config class + * + * @type Class + */ + _storageConfigClass: null, + + /** + * Flag whether the list is about user storage configs (true) + * or global storage configs (false) + * + * @type bool + */ + _isPersonal: false, + + /** + * Page size in applicable users dropdown + * + * @type int + */ + _userListLimit: 30, + + /** + * List of supported backends + * + * @type Object. + */ + _allBackends: null, + + /** + * @param {Object} $el DOM object containing the list + * @param {Object} [options] + * @param {int} [options.userListLimit] page size in applicable users dropdown + */ + initialize: function($el, options) { + this.$el = $el; + this._isPersonal = ($el.data('admin') !== true); + if (this._isPersonal) { + this._storageConfigClass = OCA.External.Settings.UserStorageConfig; + } else { + this._storageConfigClass = OCA.External.Settings.GlobalStorageConfig; + } + + if (options && !_.isUndefined(options.userListLimit)) { + this._userListLimit = options.userListLimit; + } + + // read the backend config that was carefully crammed + // into the data-configurations attribute of the select + this._allBackends = this.$el.find('.selectBackend').data('configurations'); + + //initialize hidden input field with list of users and groups + this.$el.find('tr:not(#addMountPoint)').each(function(i,tr) { + var $tr = $(tr); + var $applicable = $tr.find('.applicable'); + if ($applicable.length > 0) { + var groups = $applicable.data('applicable-groups'); + var groupsId = []; + $.each(groups, function () { + groupsId.push(this + '(group)'); + }); + var users = $applicable.data('applicable-users'); + if (users.indexOf('all') > -1 || users === '') { + $tr.find('.applicableUsers').val(''); + } else { + $tr.find('.applicableUsers').val(groupsId.concat(users).join(',')); + } + } + }); + + addSelect2(this.$el.find('tr:not(#addMountPoint) .applicableUsers'), this._userListLimit); + + this._initEvents(); + }, + + /** + * Initialize DOM event handlers + */ + _initEvents: function() { + var self = this; + + this.$el.on('paste', 'td input', function() { + var $me = $(this); + var $tr = $me.closest('tr'); + setTimeout(function() { + highlightInput($me); + self.saveStorageConfig($tr); + }, 20); + }); + + var timer; + + this.$el.on('keyup', 'td input', function() { + clearTimeout(timer); + var $tr = $(this).closest('tr'); + highlightInput($(this)); + if ($(this).val) { + timer = setTimeout(function() { + self.saveStorageConfig($tr); + }, 2000); + } + }); + + this.$el.on('change', 'td input:checkbox', function() { + self.saveStorageConfig($(this).closest('tr')); + }); + + this.$el.on('change', '.applicable', function() { + self.saveStorageConfig($(this).closest('tr')); + }); + + this.$el.on('click', '.status>span', function() { + self.recheckStorageConfig($(this).closest('tr')); + }); + + this.$el.on('click', 'td.remove>img', function() { + self.deleteStorageConfig($(this).closest('tr')); + }); + + this.$el.on('change', '.selectBackend', _.bind(this._onSelectBackend, this)); + }, + + _onSelectBackend: function(event) { + var $target = $(event.target); + var $el = this.$el; + var $tr = $target.closest('tr'); + $el.find('tbody').append($tr.clone()); + $el.find('tbody tr').last().find('.mountPoint input').val(''); + var selected = $target.find('option:selected').text(); + var backendClass = $target.val(); $tr.find('.backend').text(selected); if ($tr.find('.mountPoint input').val() === '') { - $tr.find('.mountPoint input').val(suggestMountPoint(selected)); + $tr.find('.mountPoint input').val(this._suggestMountPoint(selected)); } $tr.addClass(backendClass); $tr.find('.status').append(''); $tr.find('.backend').data('class', backendClass); - var configurations = $(this).data('configurations'); + var configurations = this._allBackends; var $td = $tr.find('td.configuration'); $.each(configurations, function(backend, parameters) { if (backend === backendClass) { @@ -347,7 +546,7 @@ $(document).ready(function() { highlightInput(newElement); $td.append(newElement); }); - if (parameters['custom'] && $externalStorage.find('tbody tr.'+backendClass.replace(/\\/g, '\\\\')).length === 1) { + if (parameters['custom'] && $el.find('tbody tr.'+backendClass.replace(/\\/g, '\\\\')).length === 1) { OC.addScript('files_external', parameters['custom']); } $td.children().not('[type=hidden]').first().focus(); @@ -357,11 +556,190 @@ $(document).ready(function() { $tr.find('td').last().attr('class', 'remove'); $tr.find('td').last().removeAttr('style'); $tr.removeAttr('id'); - $(this).remove(); - addSelect2($tr.find('.applicableUsers')); - }); + $target.remove(); + addSelect2($tr.find('.applicableUsers'), this._userListLimit); + }, - function suggestMountPoint(defaultMountPoint) { + /** + * Gets the storage model from the given row + * + * @param $tr row element + * @return {OCA.External.StorageConfig} storage model instance + */ + getStorageConfig: function($tr) { + var storageId = parseInt($tr.attr('data-id'), 10); + if (!storageId) { + // new entry + storageId = null; + } + var storage = new this._storageConfigClass(storageId); + storage.mountPoint = $tr.find('.mountPoint input').val(); + storage.backendClass = $tr.find('.backend').data('class'); + + var classOptions = {}; + var configuration = $tr.find('.configuration input'); + var missingOptions = []; + $.each(configuration, function(index, input) { + var $input = $(input); + var parameter = $input.data('parameter'); + if ($input.attr('type') === 'button') { + return; + } + if ($input.val() === '' && !$input.hasClass('optional')) { + missingOptions.push(parameter); + return; + } + if ($(input).is(':checkbox')) { + if ($(input).is(':checked')) { + classOptions[parameter] = true; + } else { + classOptions[parameter] = false; + } + } else { + classOptions[parameter] = $(input).val(); + } + }); + + storage.backendOptions = classOptions; + if (missingOptions.length) { + storage.errors = { + backendOptions: missingOptions + }; + } + + // gather selected users and groups + if (!this._isPersonal) { + var groups = []; + var users = []; + var multiselect = getSelection($tr); + $.each(multiselect, function(index, value) { + var pos = value.indexOf('(group)'); + if (pos !== -1) { + groups.push(value.substr(0, pos)); + } else { + users.push(value); + } + }); + // FIXME: this should be done in the multiselect change event instead + $tr.find('.applicable') + .data('applicable-groups', groups) + .data('applicable-users', users); + + storage.applicableUsers = users; + storage.applicableGroups = groups; + } + + return storage; + }, + + /** + * Deletes the storage from the given tr + * + * @param $tr storage row + * @param Function callback callback to call after save + */ + deleteStorageConfig: function($tr) { + var self = this; + var configId = $tr.data('id'); + if (!_.isNumber(configId)) { + // deleting unsaved storage + $tr.remove(); + return; + } + var storage = new this._storageConfigClass(configId); + this.updateStatus($tr, StorageConfig.Status.IN_PROGRESS); + + storage.destroy({ + success: function() { + $tr.remove(); + }, + error: function() { + self.updateStatus($tr, StorageConfig.Status.ERROR); + } + }); + }, + + /** + * Saves the storage from the given tr + * + * @param $tr storage row + * @param Function callback callback to call after save + */ + saveStorageConfig:function($tr, callback) { + var self = this; + var storage = this.getStorageConfig($tr); + if (!storage.validate()) { + return false; + } + + this.updateStatus($tr, StorageConfig.Status.IN_PROGRESS); + storage.save({ + success: function(result) { + self.updateStatus($tr, result.status); + $tr.attr('data-id', result.id); + + if (_.isFunction(callback)) { + callback(storage); + } + }, + error: function() { + self.updateStatus($tr, StorageConfig.Status.ERROR); + } + }); + }, + + /** + * Recheck storage availability + * + * @param {jQuery} $tr storage row + * @return {boolean} success + */ + recheckStorageConfig: function($tr) { + var self = this; + var storage = this.getStorageConfig($tr); + if (!storage.validate()) { + return false; + } + + this.updateStatus($tr, StorageConfig.Status.IN_PROGRESS); + storage.recheck({ + success: function(result) { + self.updateStatus($tr, result.status); + }, + error: function() { + self.updateStatus($tr, StorageConfig.Status.ERROR); + } + }); + }, + + /** + * Update status display + * + * @param {jQuery} $tr + * @param {int} status + */ + updateStatus: function($tr, status) { + var $statusSpan = $tr.find('.status span'); + $statusSpan.removeClass('success error loading-small'); + switch (status) { + case StorageConfig.Status.IN_PROGRESS: + $statusSpan.addClass('loading-small'); + break; + case StorageConfig.Status.SUCCESS: + $statusSpan.addClass('success'); + break; + default: + $statusSpan.addClass('error'); + } + }, + + /** + * Suggest mount point name that doesn't conflict with the existing names in the list + * + * @param {string} defaultMountPoint default name + */ + _suggestMountPoint: function(defaultMountPoint) { + var $el = this.$el; var pos = defaultMountPoint.indexOf('/'); if (pos !== -1) { defaultMountPoint = defaultMountPoint.substring(0, pos); @@ -372,7 +750,7 @@ $(document).ready(function() { var match = true; while (match && i < 20) { match = false; - $externalStorage.find('tbody td.mountPoint input').each(function(index, mountPoint) { + $el.find('tbody td.mountPoint input').each(function(index, mountPoint) { if ($(mountPoint).val() === defaultMountPoint+append) { match = true; return false; @@ -385,42 +763,12 @@ $(document).ready(function() { break; } } - return defaultMountPoint+append; + return defaultMountPoint + append; } +}; - $externalStorage.on('paste', 'td input', function() { - var $me = $(this); - var $tr = $me.closest('tr'); - setTimeout(function() { - highlightInput($me); - OC.MountConfig.saveStorage($tr); - }, 20); - }); - - var timer; - - $externalStorage.on('keyup', 'td input', function() { - clearTimeout(timer); - var $tr = $(this).closest('tr'); - highlightInput($(this)); - if ($(this).val) { - timer = setTimeout(function() { - OC.MountConfig.saveStorage($tr); - }, 2000); - } - }); - - $externalStorage.on('change', 'td input:checkbox', function() { - OC.MountConfig.saveStorage($(this).closest('tr')); - }); - - $externalStorage.on('change', '.applicable', function() { - OC.MountConfig.saveStorage($(this).closest('tr')); - }); - - $externalStorage.on('click', '.status>span', function() { - OC.MountConfig.saveStorage($(this).closest('tr')); - }); +$(document).ready(function() { + var mountConfigListView = new MountConfigListView($('#externalStorage')); $('#sslCertificate').on('click', 'td.remove>img', function() { var $tr = $(this).closest('tr'); @@ -429,33 +777,7 @@ $(document).ready(function() { return true; }); - $externalStorage.on('click', 'td.remove>img', function() { - var $tr = $(this).closest('tr'); - var mountPoint = $tr.find('.mountPoint input').val(); - - if ($externalStorage.data('admin') === true) { - var isPersonal = false; - var multiselect = getSelection($tr); - $.each(multiselect, function(index, value) { - var pos = value.indexOf('(group)'); - if (pos != -1) { - var mountType = 'group'; - var applicable = value.substr(0, pos); - } else { - var mountType = 'user'; - var applicable = value; - } - $.post(OC.filePath('files_external', 'ajax', 'removeMountPoint.php'), { mountPoint: mountPoint, mountType: mountType, applicable: applicable, isPersonal: isPersonal }); - }); - } else { - var mountType = 'user'; - var applicable = OC.currentUser; - var isPersonal = true; - $.post(OC.filePath('files_external', 'ajax', 'removeMountPoint.php'), { mountPoint: mountPoint, mountType: mountType, applicable: applicable, isPersonal: isPersonal }); - } - $tr.remove(); - }); - + // TODO: move this into its own View class var $allowUserMounting = $('#allowUserMounting'); $allowUserMounting.bind('change', function() { OC.msg.startSaving('#userMountingMsg'); @@ -484,6 +806,31 @@ $(document).ready(function() { } }); + + // global instance + OCA.External.Settings.mountConfig = mountConfigListView; + + /** + * Legacy + * + * @namespace + * @deprecated use OCA.External.Settings.mountConfig instead + */ + OC.MountConfig = { + saveStorage: _.bind(mountConfigListView.saveStorageConfig, mountConfigListView) + }; }); +// export + +OCA.External = OCA.External || {}; +/** + * @namespace + */ +OCA.External.Settings = OCA.External.Settings || {}; + +OCA.External.Settings.GlobalStorageConfig = GlobalStorageConfig; +OCA.External.Settings.UserStorageConfig = UserStorageConfig; +OCA.External.Settings.MountConfigListView = MountConfigListView; + })(); diff --git a/apps/files_external/js/sftp_key.js b/apps/files_external/js/sftp_key.js index 2b39628247..55b11b1fac 100644 --- a/apps/files_external/js/sftp_key.js +++ b/apps/files_external/js/sftp_key.js @@ -42,7 +42,7 @@ $(document).ready(function() { if (result && result.status === 'success') { $(config).find('[data-parameter="public_key"]').val(result.data.public_key); $(config).find('[data-parameter="private_key"]').val(result.data.private_key); - OC.MountConfig.saveStorage(tr, function() { + OCA.External.mountConfig.saveStorageConfig(tr, function() { // Nothing to do }); } else { diff --git a/apps/files_external/lib/config.php b/apps/files_external/lib/config.php index ddfab43987..deeedb9855 100644 --- a/apps/files_external/lib/config.php +++ b/apps/files_external/lib/config.php @@ -32,6 +32,10 @@ class OC_Mount_Config { const MOUNT_TYPE_USER = 'user'; const MOUNT_TYPE_PERSONAL = 'personal'; + // getBackendStatus return types + const STATUS_SUCCESS = 0; + const STATUS_ERROR = 1; + // whether to skip backend test (for unit tests, as this static class is not mockable) public static $skipTest = false; @@ -143,15 +147,9 @@ class OC_Mount_Config { $mountPoints = array(); $datadir = \OC_Config::getValue("datadirectory", \OC::$SERVERROOT . "/data"); - $mount_file = \OC_Config::getValue("mount_file", $datadir . "/mount.json"); $backends = self::getBackends(); - //move config file to it's new position - if (is_file(\OC::$SERVERROOT . '/config/mount.json')) { - rename(\OC::$SERVERROOT . '/config/mount.json', $mount_file); - } - // Load system mount points $mountConfig = self::readData(); @@ -349,6 +347,8 @@ class OC_Mount_Config { $mountPoint = substr($mountPoint, 13); $config = array( + 'id' => (int) $mount['id'], + 'storage_id' => (int) $mount['storage_id'], 'class' => $mount['class'], 'mountpoint' => $mountPoint, 'backend' => $backends[$mount['class']]['backend'], @@ -383,6 +383,8 @@ class OC_Mount_Config { // Remove '/$user/files/' from mount point $mountPoint = substr($mountPoint, 13); $config = array( + 'id' => (int) $mount['id'], + 'storage_id' => (int) $mount['storage_id'], 'class' => $mount['class'], 'mountpoint' => $mountPoint, 'backend' => $backends[$mount['class']]['backend'], @@ -425,6 +427,8 @@ class OC_Mount_Config { } $mount['options'] = self::decryptPasswords($mount['options']); $personal[] = array( + 'id' => (int) $mount['id'], + 'storage_id' => (int) $mount['storage_id'], 'class' => $mount['class'], // Remove '/uid/files/' from mount point 'mountpoint' => substr($mountPoint, strlen($uid) + 8), @@ -442,11 +446,11 @@ class OC_Mount_Config { * * @param string $class backend class name * @param array $options backend configuration options - * @return bool true if the connection succeeded, false otherwise + * @return int see self::STATUS_* */ - private static function getBackendStatus($class, $options, $isPersonal) { + public static function getBackendStatus($class, $options, $isPersonal) { if (self::$skipTest) { - return true; + return self::STATUS_SUCCESS; } foreach ($options as &$option) { $option = self::setUserVars(OCP\User::getUser(), $option); @@ -454,13 +458,14 @@ class OC_Mount_Config { if (class_exists($class)) { try { $storage = new $class($options); - return $storage->test($isPersonal); + if ($storage->test($isPersonal)) { + return self::STATUS_SUCCESS; + } } catch (Exception $exception) { \OCP\Util::logException('files_external', $exception); - return false; } } - return false; + return self::STATUS_ERROR; } /** @@ -474,6 +479,8 @@ class OC_Mount_Config { * @param bool $isPersonal Personal or system mount point i.e. is this being called from the personal or admin page * @param int|null $priority Mount point priority, null for default * @return boolean + * + * @deprecated use StoragesService#addStorage() instead */ public static function addMountPoint($mountPoint, $class, @@ -537,7 +544,7 @@ class OC_Mount_Config { self::writeData($isPersonal ? OCP\User::getUser() : null, $mountPoints); $result = self::getBackendStatus($class, $classOptions, $isPersonal); - if ($result && $isNew) { + if ($result === self::STATUS_SUCCESS && $isNew) { \OC_Hook::emit( \OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create_mount, @@ -558,6 +565,8 @@ class OC_Mount_Config { * @param string $applicable User or group to remove mount from * @param bool $isPersonal Personal or system mount point * @return bool + * + * @deprecated use StoragesService#removeStorage() instead */ public static function removeMountPoint($mountPoint, $mountType, $applicable, $isPersonal = false) { // Verify that the mount point applies for the current user @@ -622,13 +631,10 @@ class OC_Mount_Config { * @param string|null $user If not null, personal for $user, otherwise system * @return array */ - private static function readData($user = null) { - $parser = new \OC\ArrayParser(); + public static function readData($user = null) { if (isset($user)) { - $phpFile = OC_User::getHome($user) . '/mount.php'; $jsonFile = OC_User::getHome($user) . '/mount.json'; } else { - $phpFile = OC::$SERVERROOT . '/config/mount.php'; $datadir = \OC_Config::getValue('datadirectory', \OC::$SERVERROOT . '/data/'); $jsonFile = \OC_Config::getValue('mount_file', $datadir . '/mount.json'); } @@ -637,11 +643,6 @@ class OC_Mount_Config { if (is_array($mountPoints)) { return $mountPoints; } - } elseif (is_file($phpFile)) { - $mountPoints = $parser->parsePHP(file_get_contents($phpFile)); - if (is_array($mountPoints)) { - return $mountPoints; - } } return array(); } @@ -652,7 +653,7 @@ class OC_Mount_Config { * @param string|null $user If not null, personal for $user, otherwise system * @param array $data Mount points */ - private static function writeData($user, $data) { + public static function writeData($user, $data) { if (isset($user)) { $file = OC_User::getHome($user) . '/mount.json'; } else { @@ -769,7 +770,7 @@ class OC_Mount_Config { * @param array $options mount options * @return array updated options */ - private static function encryptPasswords($options) { + public static function encryptPasswords($options) { if (isset($options['password'])) { $options['password_encrypted'] = self::encryptPassword($options['password']); // do not unset the password, we want to keep the keys order @@ -785,7 +786,7 @@ class OC_Mount_Config { * @param array $options mount options * @return array updated options */ - private static function decryptPasswords($options) { + public static function decryptPasswords($options) { // note: legacy options might still have the unencrypted password in the "password" field if (isset($options['password_encrypted'])) { $options['password'] = self::decryptPassword($options['password_encrypted']); diff --git a/apps/files_external/lib/notfoundexception.php b/apps/files_external/lib/notfoundexception.php new file mode 100644 index 0000000000..d1d15309d5 --- /dev/null +++ b/apps/files_external/lib/notfoundexception.php @@ -0,0 +1,15 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OCA\Files_external; + +/** + * Storage is not found + */ +class NotFoundException extends \Exception { +} diff --git a/apps/files_external/lib/storageconfig.php b/apps/files_external/lib/storageconfig.php new file mode 100644 index 0000000000..f23b5cd86a --- /dev/null +++ b/apps/files_external/lib/storageconfig.php @@ -0,0 +1,243 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OCA\Files_external\Lib; + +/** + * External storage configuration + */ +class StorageConfig implements \JsonSerializable { + + /** + * @var int + */ + private $id; + + /** + * @var string + */ + private $backendClass; + + /** + * @var array + */ + private $backendOptions = []; + + /** + * @var string + */ + private $mountPoint; + + /** + * @var int + */ + private $status; + + /** + * @var int + */ + private $priority; + + /** + * @var array + */ + private $applicableUsers = []; + + /** + * @var array + */ + private $applicableGroups = []; + + /** + * @param int|null $id config id or null for a new config + */ + public function __construct($id = null) { + $this->id = $id; + } + + /** + * Returns the configuration id + * + * @return int + */ + public function getId() { + return $this->id; + } + + /** + * Sets the configuration id + * + * @param int configuration id + */ + public function setId($id) { + $this->id = $id; + } + + /** + * Returns mount point path relative to the user's + * "files" folder. + * + * @return string path + */ + public function getMountPoint() { + return $this->mountPoint; + } + + /** + * Sets mount point path relative to the user's + * "files" folder. + * The path will be normalized. + * + * @param string path + */ + public function setMountPoint($mountPoint) { + $this->mountPoint = \OC\Files\Filesystem::normalizePath($mountPoint); + } + + /** + * Returns the external storage backend class name + * + * @return string external storage backend class name + */ + public function getBackendClass() { + return $this->backendClass; + } + + /** + * Sets the external storage backend class name + * + * @param string external storage backend class name + */ + public function setBackendClass($backendClass) { + $this->backendClass = $backendClass; + } + + /** + * Returns the external storage backend-specific options + * + * @return array backend options + */ + public function getBackendOptions() { + return $this->backendOptions; + } + + /** + * Sets the external storage backend-specific options + * + * @param array backend options + */ + public function setBackendOptions($backendOptions) { + $this->backendOptions = $backendOptions; + } + + /** + * Returns the mount priority + * + * @return int priority + */ + public function getPriority() { + return $this->priority; + } + + /** + * Sets the mount priotity + * + * @param int priority + */ + public function setPriority($priority) { + $this->priority = $priority; + } + + /** + * Returns the users for which to mount this storage + * + * @return array applicable users + */ + public function getApplicableUsers() { + return $this->applicableUsers; + } + + /** + * Sets the users for which to mount this storage + * + * @param array applicable users + */ + public function setApplicableUsers($applicableUsers) { + if (is_null($applicableUsers)) { + $applicableUsers = []; + } + $this->applicableUsers = $applicableUsers; + } + + /** + * Returns the groups for which to mount this storage + * + * @return array applicable groups + */ + public function getApplicableGroups() { + return $this->applicableGroups; + } + + /** + * Sets the groups for which to mount this storage + * + * @param array applicable groups + */ + public function setApplicableGroups($applicableGroups) { + if (is_null($applicableGroups)) { + $applicableGroups = []; + } + $this->applicableGroups = $applicableGroups; + } + + /** + * Sets the storage status, whether the config worked last time + * + * @return int $status status + */ + public function getStatus() { + return $this->status; + } + + /** + * Sets the storage status, whether the config worked last time + * + * @param int $status status + */ + public function setStatus($status) { + $this->status = $status; + } + + /** + * Serialize config to JSON + * + * @return array + */ + public function jsonSerialize() { + $result = []; + if (!is_null($this->id)) { + $result['id'] = $this->id; + } + $result['mountPoint'] = $this->mountPoint; + $result['backendClass'] = $this->backendClass; + $result['backendOptions'] = $this->backendOptions; + if (!is_null($this->priority)) { + $result['priority'] = $this->priority; + } + if (!empty($this->applicableUsers)) { + $result['applicableUsers'] = $this->applicableUsers; + } + if (!empty($this->applicableGroups)) { + $result['applicableGroups'] = $this->applicableGroups; + } + if (!is_null($this->status)) { + $result['status'] = $this->status; + } + return $result; + } +} diff --git a/apps/files_external/service/globalstoragesservice.php b/apps/files_external/service/globalstoragesservice.php new file mode 100644 index 0000000000..257c9bd467 --- /dev/null +++ b/apps/files_external/service/globalstoragesservice.php @@ -0,0 +1,192 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OCA\Files_external\Service; + +use \OCP\IUserSession; +use \OC\Files\Filesystem; + +use \OCA\Files_external\Lib\StorageConfig; +use \OCA\Files_external\NotFoundException; + +/** + * Service class to manage global external storages + */ +class GlobalStoragesService extends StoragesService { + + /** + * Write the storages to the configuration. + * + * @param string $user user or null for global config + * @param array $storages map of storage id to storage config + */ + public function writeConfig($storages) { + // let the horror begin + $mountPoints = []; + foreach ($storages as $storageConfig) { + $mountPoint = $storageConfig->getMountPoint(); + $oldBackendOptions = $storageConfig->getBackendOptions(); + $storageConfig->setBackendOptions( + \OC_Mount_Config::encryptPasswords( + $oldBackendOptions + ) + ); + + // system mount + $rootMountPoint = '/$user/files/' . ltrim($mountPoint, '/'); + + $applicableUsers = $storageConfig->getApplicableUsers(); + $applicableGroups = $storageConfig->getApplicableGroups(); + foreach ($applicableUsers as $applicable) { + $this->addMountPoint( + $mountPoints, + \OC_Mount_Config::MOUNT_TYPE_USER, + $applicable, + $rootMountPoint, + $storageConfig + ); + } + + foreach ($applicableGroups as $applicable) { + $this->addMountPoint( + $mountPoints, + \OC_Mount_Config::MOUNT_TYPE_GROUP, + $applicable, + $rootMountPoint, + $storageConfig + ); + } + + // if neither "applicableGroups" or "applicableUsers" were set, use "all" user + if (empty($applicableUsers) && empty($applicableGroups)) { + $this->addMountPoint( + $mountPoints, + \OC_Mount_Config::MOUNT_TYPE_USER, + 'all', + $rootMountPoint, + $storageConfig + ); + } + + // restore old backend options where the password was not encrypted, + // because we don't want to change the state of the original object + $storageConfig->setBackendOptions($oldBackendOptions); + } + + \OC_Mount_Config::writeData(null, $mountPoints); + } + + /** + * Triggers $signal for all applicable users of the given + * storage + * + * @param StorageConfig $storage storage data + * @param string $signal signal to trigger + */ + protected function triggerHooks(StorageConfig $storage, $signal) { + $applicableUsers = $storage->getApplicableUsers(); + $applicableGroups = $storage->getApplicableGroups(); + if (empty($applicableUsers) && empty($applicableGroups)) { + // raise for user "all" + $this->triggerApplicableHooks( + $signal, + $storage->getMountPoint(), + \OC_Mount_Config::MOUNT_TYPE_USER, + ['all'] + ); + return; + } + + $this->triggerApplicableHooks( + $signal, + $storage->getMountPoint(), + \OC_Mount_Config::MOUNT_TYPE_USER, + $applicableUsers + ); + $this->triggerApplicableHooks( + $signal, + $storage->getMountPoint(), + \OC_Mount_Config::MOUNT_TYPE_GROUP, + $applicableGroups + ); + } + + /** + * Triggers signal_create_mount or signal_delete_mount to + * accomodate for additions/deletions in applicableUsers + * and applicableGroups fields. + * + * @param StorageConfig $oldStorage old storage data + * @param StorageConfig $newStorage new storage data + */ + protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage) { + // if mount point changed, it's like a deletion + creation + if ($oldStorage->getMountPoint() !== $newStorage->getMountPoint()) { + $this->triggerHooks($oldStorage, Filesystem::signal_delete_mount); + $this->triggerHooks($newStorage, Filesystem::signal_create_mount); + return; + } + + $userAdditions = array_diff($newStorage->getApplicableUsers(), $oldStorage->getApplicableUsers()); + $userDeletions = array_diff($oldStorage->getApplicableUsers(), $newStorage->getApplicableUsers()); + $groupAdditions = array_diff($newStorage->getApplicableGroups(), $oldStorage->getApplicableGroups()); + $groupDeletions = array_diff($oldStorage->getApplicableGroups(), $newStorage->getApplicableGroups()); + + // if no applicable were set, raise a signal for "all" + if (empty($oldStorage->getApplicableUsers()) && empty($oldStorage->getApplicableGroups())) { + $this->triggerApplicableHooks( + Filesystem::signal_delete_mount, + $oldStorage->getMountPoint(), + \OC_Mount_Config::MOUNT_TYPE_USER, + ['all'] + ); + } + + // trigger delete for removed users + $this->triggerApplicableHooks( + Filesystem::signal_delete_mount, + $oldStorage->getMountPoint(), + \OC_Mount_Config::MOUNT_TYPE_USER, + $userDeletions + ); + + // trigger delete for removed groups + $this->triggerApplicableHooks( + Filesystem::signal_delete_mount, + $oldStorage->getMountPoint(), + \OC_Mount_Config::MOUNT_TYPE_GROUP, + $groupDeletions + ); + + // and now add the new users + $this->triggerApplicableHooks( + Filesystem::signal_create_mount, + $newStorage->getMountPoint(), + \OC_Mount_Config::MOUNT_TYPE_USER, + $userAdditions + ); + + // and now add the new groups + $this->triggerApplicableHooks( + Filesystem::signal_create_mount, + $newStorage->getMountPoint(), + \OC_Mount_Config::MOUNT_TYPE_GROUP, + $groupAdditions + ); + + // if no applicable, raise a signal for "all" + if (empty($newStorage->getApplicableUsers()) && empty($newStorage->getApplicableGroups())) { + $this->triggerApplicableHooks( + Filesystem::signal_create_mount, + $newStorage->getMountPoint(), + \OC_Mount_Config::MOUNT_TYPE_USER, + ['all'] + ); + } + } +} diff --git a/apps/files_external/service/storagesservice.php b/apps/files_external/service/storagesservice.php new file mode 100644 index 0000000000..52188b23a3 --- /dev/null +++ b/apps/files_external/service/storagesservice.php @@ -0,0 +1,303 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OCA\Files_external\Service; + +use \OCP\IUserSession; +use \OC\Files\Filesystem; + +use \OCA\Files_external\Lib\StorageConfig; +use \OCA\Files_external\NotFoundException; + +/** + * Service class to manage external storages + */ +abstract class StoragesService { + + /** + * Read legacy config data + * + * @return array list of mount configs + */ + protected function readLegacyConfig() { + // read global config + return \OC_Mount_Config::readData(); + } + + /** + * Read the external storages config + * + * @return array map of storage id to storage config + */ + protected function readConfig() { + $mountPoints = $this->readLegacyConfig(); + + /** + * Here is the how the horribly messy mount point array looks like + * from the mount.json file: + * + * $storageOptions = $mountPoints[$mountType][$applicable][$mountPath] + * + * - $mountType is either "user" or "group" + * - $applicable is the name of a user or group (or the current user for personal mounts) + * - $mountPath is the mount point path (where the storage must be mounted) + * - $storageOptions is a map of storage options: + * - "priority": storage priority + * - "backend": backend class name + * - "options": backend-specific options + */ + + // group by storage id + $storages = []; + foreach ($mountPoints as $mountType => $applicables) { + foreach ($applicables as $applicable => $mountPaths) { + foreach ($mountPaths as $rootMountPath => $storageOptions) { + // the root mount point is in the format "/$user/files/the/mount/point" + // we remove the "/$user/files" prefix + $parts = explode('/', trim($rootMountPath, '/'), 3); + if (count($parts) < 3) { + // something went wrong, skip + \OCP\Util::writeLog( + 'files_external', + 'Could not parse mount point "' . $rootMountPath . '"', + \OCP\Util::ERROR + ); + continue; + } + + $relativeMountPath = $parts[2]; + + $configId = (int)$storageOptions['id']; + if (isset($storages[$configId])) { + $currentStorage = $storages[$configId]; + } else { + $currentStorage = new StorageConfig($configId); + $currentStorage->setMountPoint($relativeMountPath); + } + + $currentStorage->setBackendClass($storageOptions['class']); + $currentStorage->setBackendOptions($storageOptions['options']); + if (isset($storageOptions['priority'])) { + $currentStorage->setPriority($storageOptions['priority']); + } + + if ($mountType === \OC_Mount_Config::MOUNT_TYPE_USER) { + $applicableUsers = $currentStorage->getApplicableUsers(); + if ($applicable !== 'all') { + $applicableUsers[] = $applicable; + $currentStorage->setApplicableUsers($applicableUsers); + } + } else if ($mountType === \OC_Mount_Config::MOUNT_TYPE_GROUP) { + $applicableGroups = $currentStorage->getApplicableGroups(); + $applicableGroups[] = $applicable; + $currentStorage->setApplicableGroups($applicableGroups); + } + $storages[$configId] = $currentStorage; + } + } + } + + // decrypt passwords + foreach ($storages as &$storage) { + $storage->setBackendOptions( + \OC_Mount_Config::decryptPasswords( + $storage->getBackendOptions() + ) + ); + } + + return $storages; + } + + /** + * Add mount point into the messy mount point structure + * + * @param array $mountPoints messy array of mount points + * @param string $mountType mount type + * @param string $applicable single applicable user or group + * @param string $rootMountPoint root mount point to use + * @param array $storageConfig storage config to set to the mount point + */ + protected function addMountPoint(&$mountPoints, $mountType, $applicable, $rootMountPoint, $storageConfig) { + if (!isset($mountPoints[$mountType])) { + $mountPoints[$mountType] = []; + } + + if (!isset($mountPoints[$mountType][$applicable])) { + $mountPoints[$mountType][$applicable] = []; + } + + $options = [ + 'id' => $storageConfig->getId(), + 'class' => $storageConfig->getBackendClass(), + 'options' => $storageConfig->getBackendOptions(), + ]; + + if (!is_null($storageConfig->getPriority())) { + $options['priority'] = $storageConfig->getPriority(); + } + + $mountPoints[$mountType][$applicable][$rootMountPoint] = $options; + } + + /** + * Write the storages to the configuration. + * + * @param array $storages map of storage id to storage config + */ + abstract protected function writeConfig($storages); + + /** + * Get a storage with status + * + * @param int $id + * + * @return StorageConfig + */ + public function getStorage($id) { + $allStorages = $this->readConfig(); + + if (!isset($allStorages[$id])) { + throw new NotFoundException('Storage with id "' . $id . '" not found'); + } + + return $allStorages[$id]; + } + + /** + * Add new storage to the configuration + * + * @param array $newStorage storage attributes + * + * @return StorageConfig storage config, with added id + */ + public function addStorage(StorageConfig $newStorage) { + $allStorages = $this->readConfig(); + + $configId = $this->generateNextId($allStorages); + $newStorage->setId($configId); + + // add new storage + $allStorages[$configId] = $newStorage; + + $this->writeConfig($allStorages); + + $this->triggerHooks($newStorage, Filesystem::signal_create_mount); + + $newStorage->setStatus(\OC_Mount_Config::STATUS_SUCCESS); + return $newStorage; + } + + /** + * Triggers the given hook signal for all the applicables given + * + * @param string $signal signal + * @param string $mountPoint hook mount pount param + * @param string $mountType hook mount type param + * @param array $applicableArray array of applicable users/groups for which to trigger the hook + */ + protected function triggerApplicableHooks($signal, $mountPoint, $mountType, $applicableArray) { + foreach ($applicableArray as $applicable) { + \OC_Hook::emit( + Filesystem::CLASSNAME, + $signal, + [ + Filesystem::signal_param_path => $mountPoint, + Filesystem::signal_param_mount_type => $mountType, + Filesystem::signal_param_users => $applicable, + ] + ); + } + } + + /** + * Triggers $signal for all applicable users of the given + * storage + * + * @param StorageConfig $storage storage data + * @param string $signal signal to trigger + */ + abstract protected function triggerHooks(StorageConfig $storage, $signal); + + /** + * Triggers signal_create_mount or signal_delete_mount to + * accomodate for additions/deletions in applicableUsers + * and applicableGroups fields. + * + * @param StorageConfig $oldStorage old storage data + * @param StorageConfig $newStorage new storage data + */ + abstract protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage); + + /** + * Update storage to the configuration + * + * @param StorageConfig $updatedStorage storage attributes + * + * @return StorageConfig storage config + * @throws NotFoundException + */ + public function updateStorage(StorageConfig $updatedStorage) { + $allStorages = $this->readConfig(); + + $id = $updatedStorage->getId(); + if (!isset($allStorages[$id])) { + throw new NotFoundException('Storage with id "' . $id . '" not found'); + } + + $oldStorage = $allStorages[$id]; + $allStorages[$id] = $updatedStorage; + + $this->writeConfig($allStorages); + + $this->triggerChangeHooks($oldStorage, $updatedStorage); + + return $this->getStorage($id); + } + + /** + * Delete the storage with the given id. + * + * @param int $id storage id + * + * @throws NotFoundException + */ + public function removeStorage($id) { + $allStorages = $this->readConfig(); + + if (!isset($allStorages[$id])) { + throw new NotFoundException('Storage with id "' . $id . '" not found'); + } + + $deletedStorage = $allStorages[$id]; + unset($allStorages[$id]); + + $this->writeConfig($allStorages); + + $this->triggerHooks($deletedStorage, Filesystem::signal_delete_mount); + } + + /** + * Generates a configuration id to use for a new configuration entry. + * + * @param array $allStorages array of all storage configs + * + * @return int id + */ + protected function generateNextId($allStorages) { + if (empty($allStorages)) { + return 1; + } + // note: this will mess up with with concurrency, + // but so did the mount.json. This horribly hack + // will disappear once we move to DB tables to + // store the config + return max(array_keys($allStorages)) + 1; + } + +} diff --git a/apps/files_external/service/userstoragesservice.php b/apps/files_external/service/userstoragesservice.php new file mode 100644 index 0000000000..fcf579c5d4 --- /dev/null +++ b/apps/files_external/service/userstoragesservice.php @@ -0,0 +1,142 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OCA\Files_external\Service; + +use \OCP\IUserSession; +use \OC\Files\Filesystem; + +use \OCA\Files_external\Lib\StorageConfig; +use \OCA\Files_external\NotFoundException; + +/** + * Service class to manage user external storages + * (aka personal storages) + */ +class UserStoragesService extends StoragesService { + /** + * @var IUserSession + */ + private $userSession; + + public function __construct( + IUserSession $userSession + ) { + $this->userSession = $userSession; + } + + /** + * Read legacy config data + * + * @return array list of storage configs + */ + protected function readLegacyConfig() { + // read user config + $user = $this->userSession->getUser()->getUID(); + return \OC_Mount_Config::readData($user); + } + + /** + * Read the external storages config + * + * @return array map of storage id to storage config + */ + protected function readConfig() { + $user = $this->userSession->getUser()->getUID(); + // TODO: in the future don't rely on the global config reading code + $storages = parent::readConfig(); + + $filteredStorages = []; + foreach ($storages as $configId => $storage) { + // filter out all bogus storages that aren't for the current user + if (!in_array($user, $storage->getApplicableUsers())) { + continue; + } + + // clear applicable users, should not be used + $storage->setApplicableUsers([]); + + // strip out unneeded applicableUser fields + $filteredStorages[$configId] = $storage; + } + + return $filteredStorages; + } + + /** + * Write the storages to the user's configuration. + * + * @param array $storages map of storage id to storage config + */ + public function writeConfig($storages) { + $user = $this->userSession->getUser()->getUID(); + + // let the horror begin + $mountPoints = []; + foreach ($storages as $storageConfig) { + $mountPoint = $storageConfig->getMountPoint(); + $oldBackendOptions = $storageConfig->getBackendOptions(); + $storageConfig->setBackendOptions( + \OC_Mount_Config::encryptPasswords( + $oldBackendOptions + ) + ); + + $rootMountPoint = '/' . $user . '/files/' . ltrim($mountPoint, '/'); + + $this->addMountPoint( + $mountPoints, + \OC_Mount_Config::MOUNT_TYPE_USER, + $user, + $rootMountPoint, + $storageConfig + ); + + // restore old backend options where the password was not encrypted, + // because we don't want to change the state of the original object + $storageConfig->setBackendOptions($oldBackendOptions); + } + + \OC_Mount_Config::writeData($user, $mountPoints); + } + + /** + * Triggers $signal for all applicable users of the given + * storage + * + * @param StorageConfig $storage storage data + * @param string $signal signal to trigger + */ + protected function triggerHooks(StorageConfig $storage, $signal) { + $user = $this->userSession->getUser()->getUID(); + + // trigger hook for the current user + $this->triggerApplicableHooks( + $signal, + $storage->getMountPoint(), + \OC_Mount_Config::MOUNT_TYPE_USER, + [$user] + ); + } + + /** + * Triggers signal_create_mount or signal_delete_mount to + * accomodate for additions/deletions in applicableUsers + * and applicableGroups fields. + * + * @param StorageConfig $oldStorage old storage data + * @param StorageConfig $newStorage new storage data + */ + protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage) { + // if mount point changed, it's like a deletion + creation + if ($oldStorage->getMountPoint() !== $newStorage->getMountPoint()) { + $this->triggerHooks($oldStorage, Filesystem::signal_delete_mount); + $this->triggerHooks($newStorage, Filesystem::signal_create_mount); + } + } +} diff --git a/apps/files_external/templates/settings.php b/apps/files_external/templates/settings.php index 79950f3038..5f7d7cff75 100644 --- a/apps/files_external/templates/settings.php +++ b/apps/files_external/templates/settings.php @@ -13,12 +13,12 @@ - array())); ?> + array('id' => ''))); ?> - > + data-id=""> - + - '> $backend): ?> diff --git a/apps/files_external/tests/controller/globalstoragescontrollertest.php b/apps/files_external/tests/controller/globalstoragescontrollertest.php new file mode 100644 index 0000000000..7ba4d16a7e --- /dev/null +++ b/apps/files_external/tests/controller/globalstoragescontrollertest.php @@ -0,0 +1,41 @@ + + * + * 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 . + * + */ +namespace OCA\Files_external\Tests\Controller; + +use \OCA\Files_external\Controller\GlobalStoragesController; +use \OCA\Files_external\Service\GlobalStoragesService; +use \OCP\AppFramework\Http; +use \OCA\Files_external\NotFoundException; + +class GlobalStoragesControllerTest extends StoragesControllerTest { + public function setUp() { + parent::setUp(); + $this->service = $this->getMock('\OCA\Files_external\Service\GlobalStoragesService'); + + $this->controller = new GlobalStoragesController( + 'files_external', + $this->getMock('\OCP\IRequest'), + $this->getMock('\OCP\IL10N'), + $this->service + ); + } +} diff --git a/apps/files_external/tests/controller/storagescontrollertest.php b/apps/files_external/tests/controller/storagescontrollertest.php new file mode 100644 index 0000000000..fefe2928d7 --- /dev/null +++ b/apps/files_external/tests/controller/storagescontrollertest.php @@ -0,0 +1,217 @@ + + * + * 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 . + * + */ +namespace OCA\Files_external\Tests\Controller; + +use \OCP\AppFramework\Http; + +use \OCA\Files_external\Controller\GlobalStoragesController; +use \OCA\Files_external\Service\GlobalStoragesService; +use \OCA\Files_external\Lib\StorageConfig; +use \OCA\Files_external\NotFoundException; + +abstract class StoragesControllerTest extends \Test\TestCase { + + /** + * @var GlobalStoragesController + */ + protected $controller; + + /** + * @var GlobalStoragesService + */ + protected $service; + + public function setUp() { + \OC_Mount_Config::$skipTest = true; + } + + public function tearDown() { + \OC_Mount_Config::$skipTest = false; + } + + public function testAddStorage() { + $storageConfig = new StorageConfig(1); + $storageConfig->setMountPoint('mount'); + + $this->service->expects($this->once()) + ->method('addStorage') + ->will($this->returnValue($storageConfig)); + + $response = $this->controller->create( + 'mount', + '\OC\Files\Storage\SMB', + array(), + [], + [], + null + ); + + $data = $response->getData(); + $this->assertEquals($storageConfig, $data); + $this->assertEquals(Http::STATUS_CREATED, $response->getStatus()); + } + + public function testUpdateStorage() { + $storageConfig = new StorageConfig(1); + $storageConfig->setMountPoint('mount'); + + $this->service->expects($this->once()) + ->method('updateStorage') + ->will($this->returnValue($storageConfig)); + + $response = $this->controller->update( + 1, + 'mount', + '\OC\Files\Storage\SMB', + array(), + [], + [], + null + ); + + $data = $response->getData(); + $this->assertEquals($storageConfig, $data); + $this->assertEquals(Http::STATUS_OK, $response->getStatus()); + } + + function mountPointNamesProvider() { + return array( + array(''), + array('/'), + array('//'), + ); + } + + /** + * @dataProvider mountPointNamesProvider + */ + public function testAddOrUpdateStorageInvalidMountPoint($mountPoint) { + $this->service->expects($this->never()) + ->method('addStorage'); + $this->service->expects($this->never()) + ->method('updateStorage'); + + $response = $this->controller->create( + $mountPoint, + '\OC\Files\Storage\SMB', + array(), + [], + [], + null + ); + + $this->assertEquals(Http::STATUS_UNPROCESSABLE_ENTITY, $response->getStatus()); + + $response = $this->controller->update( + 1, + $mountPoint, + '\OC\Files\Storage\SMB', + array(), + [], + [], + null + ); + + $this->assertEquals(Http::STATUS_UNPROCESSABLE_ENTITY, $response->getStatus()); + } + + public function testAddOrUpdateStorageInvalidBackend() { + $this->service->expects($this->never()) + ->method('addStorage'); + $this->service->expects($this->never()) + ->method('updateStorage'); + + $response = $this->controller->create( + 'mount', + '\OC\Files\Storage\InvalidStorage', + array(), + [], + [], + null + ); + + $this->assertEquals(Http::STATUS_UNPROCESSABLE_ENTITY, $response->getStatus()); + + $response = $this->controller->update( + 1, + 'mount', + '\OC\Files\Storage\InvalidStorage', + array(), + [], + [], + null + ); + + $this->assertEquals(Http::STATUS_UNPROCESSABLE_ENTITY, $response->getStatus()); + } + + public function testUpdateStorageNonExisting() { + $this->service->expects($this->once()) + ->method('updateStorage') + ->will($this->throwException(new NotFoundException())); + + $response = $this->controller->update( + 255, + 'mount', + '\OC\Files\Storage\SMB', + array(), + [], + [], + null + ); + + $this->assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus()); + } + + public function testDeleteStorage() { + $this->service->expects($this->once()) + ->method('removeStorage'); + + $response = $this->controller->destroy(1); + $this->assertEquals(Http::STATUS_NO_CONTENT, $response->getStatus()); + } + + public function testDeleteStorageNonExisting() { + $this->service->expects($this->once()) + ->method('removeStorage') + ->will($this->throwException(new NotFoundException())); + + $response = $this->controller->destroy(255); + $this->assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus()); + } + + public function testGetStorage() { + $storageConfig = new StorageConfig(1); + $storageConfig->setMountPoint('test'); + $storageConfig->setBackendClass('\OC\Files\Storage\SMB'); + $storageConfig->setBackendOptions(['user' => 'test', 'password', 'password123']); + + $this->service->expects($this->once()) + ->method('getStorage') + ->with(1) + ->will($this->returnValue($storageConfig)); + $response = $this->controller->show(1); + + $this->assertEquals(Http::STATUS_OK, $response->getStatus()); + $this->assertEquals($storageConfig, $response->getData()); + } +} diff --git a/apps/files_external/tests/controller/userstoragescontrollertest.php b/apps/files_external/tests/controller/userstoragescontrollertest.php new file mode 100644 index 0000000000..9d6fbb15e2 --- /dev/null +++ b/apps/files_external/tests/controller/userstoragescontrollertest.php @@ -0,0 +1,112 @@ + + * + * 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 . + * + */ +namespace OCA\Files_external\Tests\Controller; + +use \OCA\Files_external\Controller\UserStoragesController; +use \OCA\Files_external\Service\UserStoragesService; +use \OCP\AppFramework\Http; +use \OCA\Files_external\NotFoundException; + +class UserStoragesControllerTest extends StoragesControllerTest { + + /** + * @var array + */ + private $oldAllowedBackends; + + public function setUp() { + parent::setUp(); + $this->service = $this->getMockBuilder('\OCA\Files_external\Service\UserStoragesService') + ->disableOriginalConstructor() + ->getMock(); + + $this->controller = new UserStoragesController( + 'files_external', + $this->getMock('\OCP\IRequest'), + $this->getMock('\OCP\IL10N'), + $this->service + ); + + $config = \OC::$server->getConfig(); + + $this->oldAllowedBackends = $config->getAppValue( + 'files_external', + 'user_mounting_backends', + '' + ); + $config->setAppValue( + 'files_external', + 'user_mounting_backends', + '\OC\Files\Storage\SMB' + ); + } + + public function tearDown() { + $config = \OC::$server->getConfig(); + $config->setAppValue( + 'files_external', + 'user_mounting_backends', + $this->oldAllowedBackends + ); + parent::tearDown(); + } + + function disallowedBackendClassProvider() { + return array( + array('\OC\Files\Storage\Local'), + array('\OC\Files\Storage\FTP'), + ); + } + /** + * @dataProvider disallowedBackendClassProvider + */ + public function testAddOrUpdateStorageDisallowedBackend($backendClass) { + $this->service->expects($this->never()) + ->method('addStorage'); + $this->service->expects($this->never()) + ->method('updateStorage'); + + $response = $this->controller->create( + 'mount', + $backendClass, + array(), + [], + [], + null + ); + + $this->assertEquals(Http::STATUS_UNPROCESSABLE_ENTITY, $response->getStatus()); + + $response = $this->controller->update( + 1, + 'mount', + $backendClass, + array(), + [], + [], + null + ); + + $this->assertEquals(Http::STATUS_UNPROCESSABLE_ENTITY, $response->getStatus()); + } + +} diff --git a/apps/files_external/tests/js/settingsSpec.js b/apps/files_external/tests/js/settingsSpec.js new file mode 100644 index 0000000000..350840e542 --- /dev/null +++ b/apps/files_external/tests/js/settingsSpec.js @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2015 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +describe('OCA.External.Settings tests', function() { + var clock; + var select2Stub; + var select2ApplicableUsers; + + beforeEach(function() { + clock = sinon.useFakeTimers(); + select2Stub = sinon.stub($.fn, 'select2', function(args) { + if (args === 'val') { + return select2ApplicableUsers; + } + return { + on: function() {} + }; + }); + + // view still requires an existing DOM table + $('#testArea').append( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '' + + '' + + '' + + 'Delete
' + ); + // these are usually appended into the data attribute + // within the DOM by the server template + $('#externalStorage .selectBackend:first').data('configurations', { + '\\OC\\TestBackend': { + 'backend': 'Test Backend Name', + 'configuration': { + 'field1': 'Display Name 1', + 'field2': '&Display Name 2' + } + }, + '\\OC\\AnotherTestBackend': { + 'backend': 'Another Test Backend Name', + 'configuration': { + 'field1': 'Display Name 1', + 'field2': '&Display Name 2' + } + } + } + ); + }); + afterEach(function() { + select2Stub.restore(); + clock.restore(); + }); + + describe('storage configuration', function() { + var view; + + function selectBackend(backendName) { + view.$el.find('.selectBackend:first').val('\\OC\\TestBackend').trigger('change'); + } + + beforeEach(function() { + var $el = $('#externalStorage'); + view = new OCA.External.Settings.MountConfigListView($el); + }); + afterEach(function() { + view = null; + }); + describe('selecting backend', function() { + it('populates the row and creates a new empty one', function() { + var $firstRow = view.$el.find('tr:first'); + selectBackend('\\OC\\TestBackend'); + expect($firstRow.find('.backend').text()).toEqual('Test Backend'); + expect($firstRow.find('.selectBackend').length).toEqual(0); + + // TODO: check "remove" button visibility + + // the suggested mount point name + expect($firstRow.find('[name=mountPoint]').val()).toEqual('TestBackend'); + + // TODO: check that the options have been created + + // TODO: check select2 call on the ".applicableUsers" element + + var $emptyRow = $firstRow.next('tr'); + expect($emptyRow.length).toEqual(1); + expect($emptyRow.find('.selectBackend').length).toEqual(1); + expect($emptyRow.find('.applicable select').length).toEqual(0); + + // TODO: check "remove" button visibility + }); + // TODO: test with personal mounts (no applicable fields) + // TODO: test suggested mount point logic + }); + describe('saving storages', function() { + it('saves storage after editing config', function() { + var $tr = view.$el.find('tr:first'); + selectBackend('\\OC\\TestBackend'); + + var $field1 = $tr.find('input[data-parameter=field1]'); + expect($field1.length).toEqual(1); + $field1.val('test'); + $field1.trigger(new $.Event('keyup', {keyCode: 97})); + + clock.tick(4000); + + expect(fakeServer.requests.length).toEqual(1); + var request = fakeServer.requests[0]; + expect(request.url).toEqual(OC.webroot + '/index.php/apps/files_external/globalstorages'); + expect(OC.parseQueryString(request.requestBody)).toEqual({ + backendClass: '\\OC\\TestBackend', + 'backendOptions[field1]': 'test', + 'backendOptions[field2]': '', + mountPoint: 'TestBackend' + }); + + // TODO: respond and check data-id + }); + // TODO: tests with "applicableUsers" and "applicableGroups" + // TODO: test with non-optional config parameters + // TODO: test with missing mount point value + // TODO: test with personal mounts (no applicable fields) + // TODO: test save triggers: paste, keyup, checkbox + // TODO: test "custom" field with addScript + // TODO: status indicator + }); + describe('update storage', function() { + // TODO + }); + describe('delete storage', function() { + // TODO + }); + describe('recheck storages', function() { + // TODO + }); + }); + describe('applicable user list', function() { + // TODO: test select2 retrieval logic + }); + describe('allow user mounts section', function() { + // TODO: test allowUserMounting section + }); +}); diff --git a/apps/files_external/tests/service/globalstoragesservicetest.php b/apps/files_external/tests/service/globalstoragesservicetest.php new file mode 100644 index 0000000000..6286865bf4 --- /dev/null +++ b/apps/files_external/tests/service/globalstoragesservicetest.php @@ -0,0 +1,705 @@ + + * + * 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 . + * + */ +namespace OCA\Files_external\Tests\Service; + +use \OC\Files\Filesystem; + +use \OCA\Files_external\Service\GlobalStoragesService; +use \OCA\Files_external\NotFoundException; +use \OCA\Files_external\Lib\StorageConfig; + +class GlobalStoragesServiceTest extends StoragesServiceTest { + public function setUp() { + parent::setUp(); + $this->service = new GlobalStoragesService(); + } + + public function tearDown() { + @unlink($this->dataDir . '/mount.json'); + parent::tearDown(); + } + + protected function makeTestStorageData() { + return $this->makeStorageConfig([ + 'mountPoint' => 'mountpoint', + 'backendClass' => '\OC\Files\Storage\SMB', + 'backendOptions' => [ + 'option1' => 'value1', + 'option2' => 'value2', + 'password' => 'testPassword', + ], + 'applicableUsers' => [], + 'applicableGroups' => [], + 'priority' => 15, + ]); + } + + function storageDataProvider() { + return [ + // all users + [ + $this->makeStorageConfig([ + 'mountPoint' => 'mountpoint', + 'backendClass' => '\OC\Files\Storage\SMB', + 'backendOptions' => [ + 'option1' => 'value1', + 'option2' => 'value2', + 'password' => 'testPassword', + ], + 'applicableUsers' => [], + 'applicableGroups' => [], + 'priority' => 15, + ]), + ], + // some users + [ + $this->makeStorageConfig([ + 'mountPoint' => 'mountpoint', + 'backendClass' => '\OC\Files\Storage\SMB', + 'backendOptions' => [ + 'option1' => 'value1', + 'option2' => 'value2', + 'password' => 'testPassword', + ], + 'applicableUsers' => ['user1', 'user2'], + 'applicableGroups' => [], + 'priority' => 15, + ]), + ], + // some groups + [ + $this->makeStorageConfig([ + 'mountPoint' => 'mountpoint', + 'backendClass' => '\OC\Files\Storage\SMB', + 'backendOptions' => [ + 'option1' => 'value1', + 'option2' => 'value2', + 'password' => 'testPassword', + ], + 'applicableUsers' => [], + 'applicableGroups' => ['group1', 'group2'], + 'priority' => 15, + ]), + ], + // both users and groups + [ + $this->makeStorageConfig([ + 'mountPoint' => 'mountpoint', + 'backendClass' => '\OC\Files\Storage\SMB', + 'backendOptions' => [ + 'option1' => 'value1', + 'option2' => 'value2', + 'password' => 'testPassword', + ], + 'applicableUsers' => ['user1', 'user2'], + 'applicableGroups' => ['group1', 'group2'], + 'priority' => 15, + ]), + ], + ]; + } + + /** + * @dataProvider storageDataProvider + */ + public function testAddStorage($storage) { + $newStorage = $this->service->addStorage($storage); + + $this->assertEquals(1, $newStorage->getId()); + + + $newStorage = $this->service->getStorage(1); + + $this->assertEquals($storage->getMountPoint(), $newStorage->getMountPoint()); + $this->assertEquals($storage->getBackendClass(), $newStorage->getBackendClass()); + $this->assertEquals($storage->getBackendOptions(), $newStorage->getBackendOptions()); + $this->assertEquals($storage->getApplicableUsers(), $newStorage->getApplicableUsers()); + $this->assertEquals($storage->getApplicableGroups(), $newStorage->getApplicableGroups()); + $this->assertEquals($storage->getPriority(), $newStorage->getPriority()); + $this->assertEquals(1, $newStorage->getId()); + $this->assertEquals(0, $newStorage->getStatus()); + + // next one gets id 2 + $nextStorage = $this->service->addStorage($storage); + $this->assertEquals(2, $nextStorage->getId()); + } + + /** + * @dataProvider storageDataProvider + */ + public function testUpdateStorage($updatedStorage) { + $storage = $this->makeStorageConfig([ + 'mountPoint' => 'mountpoint', + 'backendClass' => '\OC\Files\Storage\SMB', + 'backendOptions' => [ + 'option1' => 'value1', + 'option2' => 'value2', + 'password' => 'testPassword', + ], + 'applicableUsers' => [], + 'applicableGroups' => [], + 'priority' => 15, + ]); + + $newStorage = $this->service->addStorage($storage); + $this->assertEquals(1, $newStorage->getId()); + + $updatedStorage->setId(1); + + $this->service->updateStorage($updatedStorage); + $newStorage = $this->service->getStorage(1); + + $this->assertEquals($updatedStorage->getMountPoint(), $newStorage->getMountPoint()); + $this->assertEquals($updatedStorage->getBackendOptions()['password'], $newStorage->getBackendOptions()['password']); + $this->assertEquals($updatedStorage->getApplicableUsers(), $newStorage->getApplicableUsers()); + $this->assertEquals($updatedStorage->getApplicableGroups(), $newStorage->getApplicableGroups()); + $this->assertEquals($updatedStorage->getPriority(), $newStorage->getPriority()); + $this->assertEquals(1, $newStorage->getId()); + $this->assertEquals(0, $newStorage->getStatus()); + } + + function hooksAddStorageDataProvider() { + return [ + // applicable all + [ + [], + [], + // expected hook calls + [ + [ + Filesystem::signal_create_mount, + \OC_Mount_Config::MOUNT_TYPE_USER, + 'all' + ], + ], + ], + // single user + [ + ['user1'], + [], + // expected hook calls + [ + [ + Filesystem::signal_create_mount, + \OC_Mount_Config::MOUNT_TYPE_USER, + 'user1', + ], + ], + ], + // single group + [ + [], + ['group1'], + // expected hook calls + [ + [ + Filesystem::signal_create_mount, + \OC_Mount_Config::MOUNT_TYPE_GROUP, + 'group1', + ], + ], + ], + // multiple users + [ + ['user1', 'user2'], + [], + [ + [ + Filesystem::signal_create_mount, + \OC_Mount_Config::MOUNT_TYPE_USER, + 'user1', + ], + [ + Filesystem::signal_create_mount, + \OC_Mount_Config::MOUNT_TYPE_USER, + 'user2', + ], + ], + ], + // multiple groups + [ + [], + ['group1', 'group2'], + // expected hook calls + [ + [ + Filesystem::signal_create_mount, + \OC_Mount_Config::MOUNT_TYPE_GROUP, + 'group1' + ], + [ + Filesystem::signal_create_mount, + \OC_Mount_Config::MOUNT_TYPE_GROUP, + 'group2' + ], + ], + ], + // mixed groups and users + [ + ['user1', 'user2'], + ['group1', 'group2'], + // expected hook calls + [ + [ + Filesystem::signal_create_mount, + \OC_Mount_Config::MOUNT_TYPE_USER, + 'user1', + ], + [ + Filesystem::signal_create_mount, + \OC_Mount_Config::MOUNT_TYPE_USER, + 'user2', + ], + [ + Filesystem::signal_create_mount, + \OC_Mount_Config::MOUNT_TYPE_GROUP, + 'group1' + ], + [ + Filesystem::signal_create_mount, + \OC_Mount_Config::MOUNT_TYPE_GROUP, + 'group2' + ], + ], + ], + ]; + } + + /** + * @dataProvider hooksAddStorageDataProvider + */ + public function testHooksAddStorage($applicableUsers, $applicableGroups, $expectedCalls) { + $storage = $this->makeTestStorageData(); + $storage->setApplicableUsers($applicableUsers); + $storage->setApplicableGroups($applicableGroups); + $this->service->addStorage($storage); + + $this->assertCount(count($expectedCalls), self::$hookCalls); + + foreach ($expectedCalls as $index => $call) { + $this->assertHookCall( + self::$hookCalls[$index], + $call[0], + $storage->getMountPoint(), + $call[1], + $call[2] + ); + } + } + + function hooksUpdateStorageDataProvider() { + return [ + [ + // nothing to multiple users and groups + [], + [], + ['user1', 'user2'], + ['group1', 'group2'], + // expected hook calls + [ + // delete the "all entry" + [ + Filesystem::signal_delete_mount, + \OC_Mount_Config::MOUNT_TYPE_USER, + 'all', + ], + [ + Filesystem::signal_create_mount, + \OC_Mount_Config::MOUNT_TYPE_USER, + 'user1', + ], + [ + Filesystem::signal_create_mount, + \OC_Mount_Config::MOUNT_TYPE_USER, + 'user2', + ], + [ + Filesystem::signal_create_mount, + \OC_Mount_Config::MOUNT_TYPE_GROUP, + 'group1' + ], + [ + Filesystem::signal_create_mount, + \OC_Mount_Config::MOUNT_TYPE_GROUP, + 'group2' + ], + ], + ], + [ + // adding a user and a group + ['user1'], + ['group1'], + ['user1', 'user2'], + ['group1', 'group2'], + // expected hook calls + [ + [ + Filesystem::signal_create_mount, + \OC_Mount_Config::MOUNT_TYPE_USER, + 'user2', + ], + [ + Filesystem::signal_create_mount, + \OC_Mount_Config::MOUNT_TYPE_GROUP, + 'group2' + ], + ], + ], + [ + // removing a user and a group + ['user1', 'user2'], + ['group1', 'group2'], + ['user1'], + ['group1'], + // expected hook calls + [ + [ + Filesystem::signal_delete_mount, + \OC_Mount_Config::MOUNT_TYPE_USER, + 'user2', + ], + [ + Filesystem::signal_delete_mount, + \OC_Mount_Config::MOUNT_TYPE_GROUP, + 'group2' + ], + ], + ], + [ + // removing all + ['user1'], + ['group1'], + [], + [], + // expected hook calls + [ + [ + Filesystem::signal_delete_mount, + \OC_Mount_Config::MOUNT_TYPE_USER, + 'user1', + ], + [ + Filesystem::signal_delete_mount, + \OC_Mount_Config::MOUNT_TYPE_GROUP, + 'group1' + ], + // create the "all" entry + [ + Filesystem::signal_create_mount, + \OC_Mount_Config::MOUNT_TYPE_USER, + 'all' + ], + ], + ], + [ + // no changes + ['user1'], + ['group1'], + ['user1'], + ['group1'], + // no hook calls + [] + ] + ]; + } + + /** + * @dataProvider hooksUpdateStorageDataProvider + */ + public function testHooksUpdateStorage( + $sourceApplicableUsers, + $sourceApplicableGroups, + $updatedApplicableUsers, + $updatedApplicableGroups, + $expectedCalls) { + + $storage = $this->makeTestStorageData(); + $storage->setApplicableUsers($sourceApplicableUsers); + $storage->setApplicableGroups($sourceApplicableGroups); + $storage = $this->service->addStorage($storage); + + $storage->setapplicableUsers($updatedApplicableUsers); + $storage->setapplicableGroups($updatedApplicableGroups); + + // reset calls + self::$hookCalls = []; + + $this->service->updateStorage($storage); + + $this->assertCount(count($expectedCalls), self::$hookCalls); + + foreach ($expectedCalls as $index => $call) { + $this->assertHookCall( + self::$hookCalls[$index], + $call[0], + '/mountpoint', + $call[1], + $call[2] + ); + } + } + + /** + */ + public function testHooksRenameMountPoint() { + $storage = $this->makeTestStorageData(); + $storage->setApplicableUsers(['user1', 'user2']); + $storage->setApplicableGroups(['group1', 'group2']); + $storage = $this->service->addStorage($storage); + + $storage->setMountPoint('renamedMountpoint'); + + // reset calls + self::$hookCalls = []; + + $this->service->updateStorage($storage); + + $expectedCalls = [ + // deletes old mount + [ + Filesystem::signal_delete_mount, + '/mountpoint', + \OC_Mount_Config::MOUNT_TYPE_USER, + 'user1', + ], + [ + Filesystem::signal_delete_mount, + '/mountpoint', + \OC_Mount_Config::MOUNT_TYPE_USER, + 'user2', + ], + [ + Filesystem::signal_delete_mount, + '/mountpoint', + \OC_Mount_Config::MOUNT_TYPE_GROUP, + 'group1', + ], + [ + Filesystem::signal_delete_mount, + '/mountpoint', + \OC_Mount_Config::MOUNT_TYPE_GROUP, + 'group2', + ], + // creates new one + [ + Filesystem::signal_create_mount, + '/renamedMountpoint', + \OC_Mount_Config::MOUNT_TYPE_USER, + 'user1', + ], + [ + Filesystem::signal_create_mount, + '/renamedMountpoint', + \OC_Mount_Config::MOUNT_TYPE_USER, + 'user2', + ], + [ + Filesystem::signal_create_mount, + '/renamedMountpoint', + \OC_Mount_Config::MOUNT_TYPE_GROUP, + 'group1', + ], + [ + Filesystem::signal_create_mount, + '/renamedMountpoint', + \OC_Mount_Config::MOUNT_TYPE_GROUP, + 'group2', + ], + ]; + + $this->assertCount(count($expectedCalls), self::$hookCalls); + + foreach ($expectedCalls as $index => $call) { + $this->assertHookCall( + self::$hookCalls[$index], + $call[0], + $call[1], + $call[2], + $call[3] + ); + } + } + + function hooksDeleteStorageDataProvider() { + return [ + [ + ['user1', 'user2'], + ['group1', 'group2'], + // expected hook calls + [ + [ + Filesystem::signal_delete_mount, + \OC_Mount_Config::MOUNT_TYPE_USER, + 'user1', + ], + [ + Filesystem::signal_delete_mount, + \OC_Mount_Config::MOUNT_TYPE_USER, + 'user2', + ], + [ + Filesystem::signal_delete_mount, + \OC_Mount_Config::MOUNT_TYPE_GROUP, + 'group1' + ], + [ + Filesystem::signal_delete_mount, + \OC_Mount_Config::MOUNT_TYPE_GROUP, + 'group2' + ], + ], + ], + [ + // deleting "all" entry + [], + [], + [ + [ + Filesystem::signal_delete_mount, + \OC_Mount_Config::MOUNT_TYPE_USER, + 'all', + ], + ], + ], + ]; + } + + /** + * @dataProvider hooksDeleteStorageDataProvider + */ + public function testHooksDeleteStorage( + $sourceApplicableUsers, + $sourceApplicableGroups, + $expectedCalls) { + + $storage = $this->makeTestStorageData(); + $storage->setApplicableUsers($sourceApplicableUsers); + $storage->setApplicableGroups($sourceApplicableGroups); + $storage = $this->service->addStorage($storage); + + // reset calls + self::$hookCalls = []; + + $this->service->removeStorage($storage->getId()); + + $this->assertCount(count($expectedCalls), self::$hookCalls); + + foreach ($expectedCalls as $index => $call) { + $this->assertHookCall( + self::$hookCalls[$index], + $call[0], + '/mountpoint', + $call[1], + $call[2] + ); + } + } + + /** + * Make sure it uses the correct format when reading/writing + * the legacy config + */ + public function testLegacyConfigConversionApplicableAll() { + $configFile = $this->dataDir . '/mount.json'; + + $storage = $this->makeTestStorageData(); + $storage = $this->service->addStorage($storage); + + $json = json_decode(file_get_contents($configFile), true); + + $this->assertCount(1, $json); + + $this->assertEquals([\OC_Mount_Config::MOUNT_TYPE_USER], array_keys($json)); + $this->assertEquals(['all'], array_keys($json[\OC_Mount_config::MOUNT_TYPE_USER])); + + $mountPointData = $json[\OC_Mount_config::MOUNT_TYPE_USER]['all']; + $this->assertEquals(['/$user/files/mountpoint'], array_keys($mountPointData)); + + $mountPointOptions = current($mountPointData); + $this->assertEquals(1, $mountPointOptions['id']); + $this->assertEquals('\OC\Files\Storage\SMB', $mountPointOptions['class']); + $this->assertEquals(15, $mountPointOptions['priority']); + + $backendOptions = $mountPointOptions['options']; + $this->assertEquals('value1', $backendOptions['option1']); + $this->assertEquals('value2', $backendOptions['option2']); + $this->assertEquals('', $backendOptions['password']); + $this->assertNotEmpty($backendOptions['password_encrypted']); + } + + /** + * Make sure it uses the correct format when reading/writing + * the legacy config + */ + public function testLegacyConfigConversionApplicableUserAndGroup() { + $configFile = $this->dataDir . '/mount.json'; + + $storage = $this->makeTestStorageData(); + $storage->setApplicableUsers(['user1', 'user2']); + $storage->setApplicableGroups(['group1', 'group2']); + + $storage = $this->service->addStorage($storage); + + $json = json_decode(file_get_contents($configFile), true); + + $this->assertCount(2, $json); + + $this->assertTrue(isset($json[\OC_Mount_Config::MOUNT_TYPE_USER])); + $this->assertTrue(isset($json[\OC_Mount_Config::MOUNT_TYPE_GROUP])); + $this->assertEquals(['user1', 'user2'], array_keys($json[\OC_Mount_config::MOUNT_TYPE_USER])); + $this->assertEquals(['group1', 'group2'], array_keys($json[\OC_Mount_config::MOUNT_TYPE_GROUP])); + + // check that all options are the same for both users and both groups + foreach ($json[\OC_Mount_Config::MOUNT_TYPE_USER] as $mountPointData) { + $this->assertEquals(['/$user/files/mountpoint'], array_keys($mountPointData)); + + $mountPointOptions = current($mountPointData); + + $this->assertEquals(1, $mountPointOptions['id']); + $this->assertEquals('\OC\Files\Storage\SMB', $mountPointOptions['class']); + $this->assertEquals(15, $mountPointOptions['priority']); + + $backendOptions = $mountPointOptions['options']; + $this->assertEquals('value1', $backendOptions['option1']); + $this->assertEquals('value2', $backendOptions['option2']); + $this->assertEquals('', $backendOptions['password']); + $this->assertNotEmpty($backendOptions['password_encrypted']); + } + + foreach ($json[\OC_Mount_Config::MOUNT_TYPE_GROUP] as $mountPointData) { + $this->assertEquals(['/$user/files/mountpoint'], array_keys($mountPointData)); + + $mountPointOptions = current($mountPointData); + + $this->assertEquals(1, $mountPointOptions['id']); + $this->assertEquals('\OC\Files\Storage\SMB', $mountPointOptions['class']); + $this->assertEquals(15, $mountPointOptions['priority']); + + $backendOptions = $mountPointOptions['options']; + $this->assertEquals('value1', $backendOptions['option1']); + $this->assertEquals('value2', $backendOptions['option2']); + $this->assertEquals('', $backendOptions['password']); + $this->assertNotEmpty($backendOptions['password_encrypted']); + } + } + +} diff --git a/apps/files_external/tests/service/storagesservicetest.php b/apps/files_external/tests/service/storagesservicetest.php new file mode 100644 index 0000000000..1e338b3948 --- /dev/null +++ b/apps/files_external/tests/service/storagesservicetest.php @@ -0,0 +1,180 @@ + + * + * 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 . + * + */ +namespace OCA\Files_external\Tests\Service; + +use \OC\Files\Filesystem; + +use \OCA\Files_external\NotFoundException; +use \OCA\Files_external\Lib\StorageConfig; + +abstract class StoragesServiceTest extends \Test\TestCase { + + /** + * @var StoragesService + */ + protected $service; + + /** + * Data directory + * + * @var string + */ + protected $dataDir; + + /** + * Hook calls + * + * @var array + */ + protected static $hookCalls; + + public function setUp() { + self::$hookCalls = array(); + $config = \OC::$server->getConfig(); + $this->dataDir = $config->getSystemValue( + 'datadirectory', + \OC::$SERVERROOT . '/data/' + ); + \OC_Mount_Config::$skipTest = true; + + \OCP\Util::connectHook( + Filesystem::CLASSNAME, + Filesystem::signal_create_mount, + get_class($this), 'createHookCallback'); + \OCP\Util::connectHook( + Filesystem::CLASSNAME, + Filesystem::signal_delete_mount, + get_class($this), 'deleteHookCallback'); + + } + + public function tearDown() { + \OC_Mount_Config::$skipTest = false; + self::$hookCalls = array(); + } + + /** + * Creates a StorageConfig instance based on array data + * + * @param array data + * + * @return StorageConfig storage config instance + */ + protected function makeStorageConfig($data) { + $storage = new StorageConfig(); + if (isset($data['id'])) { + $storage->setId($data['id']); + } + $storage->setMountPoint($data['mountPoint']); + $storage->setBackendClass($data['backendClass']); + $storage->setBackendOptions($data['backendOptions']); + if (isset($data['applicableUsers'])) { + $storage->setApplicableUsers($data['applicableUsers']); + } + if (isset($data['applicableGroups'])) { + $storage->setApplicableGroups($data['applicableGroups']); + } + if (isset($data['priority'])) { + $storage->setPriority($data['priority']); + } + return $storage; + } + + + /** + * @expectedException \OCA\Files_external\NotFoundException + */ + public function testNonExistingStorage() { + $storage = new StorageConfig(255); + $storage->setMountPoint('mountpoint'); + $storage->setBackendClass('\OC\Files\Storage\SMB'); + $this->service->updateStorage($storage); + } + + public function testDeleteStorage() { + $storage = new StorageConfig(255); + $storage->setMountPoint('mountpoint'); + $storage->setBackendClass('\OC\Files\Storage\SMB'); + $storage->setBackendOptions(['password' => 'testPassword']); + + $newStorage = $this->service->addStorage($storage); + $this->assertEquals(1, $newStorage->getId()); + + $newStorage = $this->service->removeStorage(1); + + $caught = false; + try { + $this->service->getStorage(1); + } catch (NotFoundException $e) { + $caught = true; + } + + $this->assertTrue($caught); + } + + /** + * @expectedException \OCA\Files_external\NotFoundException + */ + public function testDeleteUnexistingStorage() { + $this->service->removeStorage(255); + } + + public static function createHookCallback($params) { + self::$hookCalls[] = array( + 'signal' => Filesystem::signal_create_mount, + 'params' => $params + ); + } + + public static function deleteHookCallback($params) { + self::$hookCalls[] = array( + 'signal' => Filesystem::signal_delete_mount, + 'params' => $params + ); + } + + /** + * Asserts hook call + * + * @param array $callData hook call data to check + * @param string $signal signal name + * @param string $mountPath mount path + * @param string $mountType mount type + * @param string $applicable applicable users + */ + protected function assertHookCall($callData, $signal, $mountPath, $mountType, $applicable) { + $this->assertEquals($signal, $callData['signal']); + $params = $callData['params']; + $this->assertEquals( + $mountPath, + $params[Filesystem::signal_param_path] + ); + $this->assertEquals( + $mountType, + $params[Filesystem::signal_param_mount_type] + ); + $this->assertEquals( + $applicable, + $params[Filesystem::signal_param_users] + ); + } +} diff --git a/apps/files_external/tests/service/userstoragesservicetest.php b/apps/files_external/tests/service/userstoragesservicetest.php new file mode 100644 index 0000000000..64d59dc7d0 --- /dev/null +++ b/apps/files_external/tests/service/userstoragesservicetest.php @@ -0,0 +1,200 @@ + + * + * 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 . + * + */ +namespace OCA\Files_external\Tests\Service; + +use \OC\Files\Filesystem; + +use \OCA\Files_external\Service\UserStoragesService; +use \OCA\Files_external\NotFoundException; +use \OCA\Files_external\Lib\StorageConfig; + +class UserStoragesServiceTest extends StoragesServiceTest { + + public function setUp() { + parent::setUp(); + + $this->userId = $this->getUniqueID('user_'); + + $this->user = new \OC\User\User($this->userId, null); + $userSession = $this->getMock('\OCP\IUserSession'); + $userSession + ->expects($this->any()) + ->method('getUser') + ->will($this->returnValue($this->user)); + + $this->service = new UserStoragesService($userSession); + + // create home folder + mkdir($this->dataDir . '/' . $this->userId . '/'); + } + + public function tearDown() { + @unlink($this->dataDir . '/' . $this->userId . '/mount.json'); + parent::tearDown(); + } + + private function makeTestStorageData() { + return $this->makeStorageConfig([ + 'mountPoint' => 'mountpoint', + 'backendClass' => '\OC\Files\Storage\SMB', + 'backendOptions' => [ + 'option1' => 'value1', + 'option2' => 'value2', + 'password' => 'testPassword', + ], + ]); + } + + public function testAddStorage() { + $storage = $this->makeTestStorageData(); + + $newStorage = $this->service->addStorage($storage); + + $this->assertEquals(1, $newStorage->getId()); + + $newStorage = $this->service->getStorage(1); + + $this->assertEquals($storage->getMountPoint(), $newStorage->getMountPoint()); + $this->assertEquals($storage->getBackendClass(), $newStorage->getBackendClass()); + $this->assertEquals($storage->getBackendOptions(), $newStorage->getBackendOptions()); + $this->assertEquals(1, $newStorage->getId()); + $this->assertEquals(0, $newStorage->getStatus()); + + // hook called once for user + $this->assertHookCall( + current(self::$hookCalls), + Filesystem::signal_create_mount, + $storage->getMountPoint(), + \OC_Mount_Config::MOUNT_TYPE_USER, + $this->userId + ); + + // next one gets id 2 + $nextStorage = $this->service->addStorage($storage); + $this->assertEquals(2, $nextStorage->getId()); + } + + public function testUpdateStorage() { + $storage = $this->makeStorageConfig([ + 'mountPoint' => 'mountpoint', + 'backendClass' => '\OC\Files\Storage\SMB', + 'backendOptions' => [ + 'option1' => 'value1', + 'option2' => 'value2', + 'password' => 'testPassword', + ], + ]); + + $newStorage = $this->service->addStorage($storage); + $this->assertEquals(1, $newStorage->getId()); + + $backendOptions = $newStorage->getBackendOptions(); + $backendOptions['password'] = 'anotherPassword'; + $newStorage->setBackendOptions($backendOptions); + + self::$hookCalls = []; + + $newStorage = $this->service->updateStorage($newStorage); + + $this->assertEquals('anotherPassword', $newStorage->getBackendOptions()['password']); + // these attributes are unused for user storages + $this->assertEmpty($newStorage->getApplicableUsers()); + $this->assertEmpty($newStorage->getApplicableGroups()); + $this->assertEquals(1, $newStorage->getId()); + $this->assertEquals(0, $newStorage->getStatus()); + + // no hook calls + $this->assertEmpty(self::$hookCalls); + } + + public function testDeleteStorage() { + parent::testDeleteStorage(); + + // hook called once for user (first one was during test creation) + $this->assertHookCall( + self::$hookCalls[1], + Filesystem::signal_delete_mount, + '/mountpoint', + \OC_Mount_Config::MOUNT_TYPE_USER, + $this->userId + ); + } + + public function testHooksRenameMountPoint() { + $storage = $this->makeTestStorageData(); + $storage = $this->service->addStorage($storage); + + $storage->setMountPoint('renamedMountpoint'); + + // reset calls + self::$hookCalls = []; + + $this->service->updateStorage($storage); + + // hook called twice + $this->assertHookCall( + self::$hookCalls[0], + Filesystem::signal_delete_mount, + '/mountpoint', + \OC_Mount_Config::MOUNT_TYPE_USER, + $this->userId + ); + $this->assertHookCall( + self::$hookCalls[1], + Filesystem::signal_create_mount, + '/renamedMountpoint', + \OC_Mount_Config::MOUNT_TYPE_USER, + $this->userId + ); + } + + /** + * Make sure it uses the correct format when reading/writing + * the legacy config + */ + public function testLegacyConfigConversion() { + $configFile = $this->dataDir . '/' . $this->userId . '/mount.json'; + + $storage = $this->makeTestStorageData(); + $storage = $this->service->addStorage($storage); + + $json = json_decode(file_get_contents($configFile), true); + + $this->assertCount(1, $json); + + $this->assertEquals([\OC_Mount_Config::MOUNT_TYPE_USER], array_keys($json)); + $this->assertEquals([$this->userId], array_keys($json[\OC_Mount_config::MOUNT_TYPE_USER])); + + $mountPointData = $json[\OC_Mount_config::MOUNT_TYPE_USER][$this->userId]; + $this->assertEquals(['/' . $this->userId . '/files/mountpoint'], array_keys($mountPointData)); + + $mountPointOptions = current($mountPointData); + $this->assertEquals(1, $mountPointOptions['id']); + $this->assertEquals('\OC\Files\Storage\SMB', $mountPointOptions['class']); + + $backendOptions = $mountPointOptions['options']; + $this->assertEquals('value1', $backendOptions['option1']); + $this->assertEquals('value2', $backendOptions['option2']); + $this->assertEquals('', $backendOptions['password']); + $this->assertNotEmpty($backendOptions['password_encrypted']); + } +} diff --git a/apps/files_external/tests/storageconfigtest.php b/apps/files_external/tests/storageconfigtest.php new file mode 100644 index 0000000000..473dc20b38 --- /dev/null +++ b/apps/files_external/tests/storageconfigtest.php @@ -0,0 +1,50 @@ + + * + * 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 . + * + */ + +namespace OCA\Files_external\Tests; + +use \OCA\Files_external\Lib\StorageConfig; + +class StorageConfigTest extends \Test\TestCase { + + public function testJsonSerialization() { + $storageConfig = new StorageConfig(1); + $storageConfig->setMountPoint('test'); + $storageConfig->setBackendClass('\OC\Files\Storage\SMB'); + $storageConfig->setBackendOptions(['user' => 'test', 'password' => 'password123']); + $storageConfig->setPriority(128); + $storageConfig->setApplicableUsers(['user1', 'user2']); + $storageConfig->setApplicableGroups(['group1', 'group2']); + + $json = $storageConfig->jsonSerialize(); + + $this->assertEquals(1, $json['id']); + $this->assertEquals('/test', $json['mountPoint']); + $this->assertEquals('\OC\Files\Storage\SMB', $json['backendClass']); + $this->assertEquals('test', $json['backendOptions']['user']); + $this->assertEquals('password123', $json['backendOptions']['password']); + $this->assertEquals(128, $json['priority']); + $this->assertEquals(['user1', 'user2'], $json['applicableUsers']); + $this->assertEquals(['group1', 'group2'], $json['applicableGroups']); + } + +} diff --git a/core/js/tests/specHelper.js b/core/js/tests/specHelper.js index 59c2a99645..29293e89bc 100644 --- a/core/js/tests/specHelper.js +++ b/core/js/tests/specHelper.js @@ -123,6 +123,9 @@ window.isPhantom = /phantom/i.test(navigator.userAgent); // reset plugins OC.Plugins._plugins = []; + + // dummy select2 (which isn't loaded during the tests) + $.fn.select2 = function() {}; }); afterEach(function() { @@ -131,6 +134,8 @@ window.isPhantom = /phantom/i.test(navigator.userAgent); fakeServer.restore(); $testArea.remove(); + + delete($.fn.select2); }); })(); diff --git a/tests/karma.config.js b/tests/karma.config.js index e5febb15aa..997da4bcb2 100644 --- a/tests/karma.config.js +++ b/tests/karma.config.js @@ -64,7 +64,8 @@ module.exports = function(config) { // only test these files, others are not ready and mess // up with the global namespace/classes/state 'apps/files_external/js/app.js', - 'apps/files_external/js/mountsfilelist.js' + 'apps/files_external/js/mountsfilelist.js', + 'apps/files_external/js/settings.js' ], testFiles: ['apps/files_external/tests/js/*.js'] }, From 680ec056dc8c4fb84b8300967bcc12db2a57f225 Mon Sep 17 00:00:00 2001 From: Robin McCorkell Date: Thu, 12 Mar 2015 16:27:31 +0000 Subject: [PATCH 2/9] Implement priority on client side in hidden input --- apps/files_external/js/settings.js | 12 ++++++++++++ apps/files_external/templates/settings.php | 3 +++ 2 files changed, 15 insertions(+) diff --git a/apps/files_external/js/settings.js b/apps/files_external/js/settings.js index b3567b7ebf..44488db4bc 100644 --- a/apps/files_external/js/settings.js +++ b/apps/files_external/js/settings.js @@ -332,6 +332,13 @@ GlobalStorageConfig.prototype = _.extend({}, StorageConfig.prototype, */ applicableGroups: null, + /** + * Storage priority + * + * @type int + */ + priority: null, + /** * Returns the data from this object * @@ -342,6 +349,7 @@ GlobalStorageConfig.prototype = _.extend({}, StorageConfig.prototype, return _.extend(data, { applicableUsers: this.applicableUsers, applicableGroups: this.applicableGroups, + priority: this.priority, }); } }); @@ -546,6 +554,8 @@ MountConfigListView.prototype = { highlightInput(newElement); $td.append(newElement); }); + var priorityEl = $(''); + $tr.append(priorityEl); if (parameters['custom'] && $el.find('tbody tr.'+backendClass.replace(/\\/g, '\\\\')).length === 1) { OC.addScript('files_external', parameters['custom']); } @@ -627,6 +637,8 @@ MountConfigListView.prototype = { storage.applicableUsers = users; storage.applicableGroups = groups; + + storage.priority = $tr.find('input.priority').val(); } return storage; diff --git a/apps/files_external/templates/settings.php b/apps/files_external/templates/settings.php index 5f7d7cff75..4c54d4069b 100644 --- a/apps/files_external/templates/settings.php +++ b/apps/files_external/templates/settings.php @@ -90,6 +90,9 @@ print_unescaped(json_encode($mount['applicable']['users'])); ?>'> + + + class="remove" style="visibility:hidden;" From 34c8b1ac7732b4502beebea0dfeeaa8bac225550 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Fri, 13 Mar 2015 12:49:11 +0100 Subject: [PATCH 3/9] Mount options for ext storage are now passed around The mount options are now passed to the UI and stored in a hidden field. The ext storage controllers and services also know how to load/save them from the legacy config. --- .../controller/globalstoragescontroller.php | 6 ++++ .../controller/userstoragescontroller.php | 10 +++++-- apps/files_external/js/settings.js | 15 ++++++++++ apps/files_external/lib/config.php | 12 +++++++- apps/files_external/lib/storageconfig.php | 29 +++++++++++++++++++ .../service/storagesservice.php | 7 +++++ apps/files_external/templates/settings.php | 11 +++++-- .../controller/storagescontrollertest.php | 8 +++++ .../controller/userstoragescontrollertest.php | 2 ++ .../service/globalstoragesservicetest.php | 6 ++++ .../tests/service/storagesservicetest.php | 3 ++ .../tests/service/userstoragesservicetest.php | 4 +++ .../tests/storageconfigtest.php | 2 ++ 13 files changed, 109 insertions(+), 6 deletions(-) diff --git a/apps/files_external/controller/globalstoragescontroller.php b/apps/files_external/controller/globalstoragescontroller.php index 3aa64f0d85..e5aff4f95a 100644 --- a/apps/files_external/controller/globalstoragescontroller.php +++ b/apps/files_external/controller/globalstoragescontroller.php @@ -49,6 +49,7 @@ class GlobalStoragesController extends StoragesController { * @param string $mountPoint storage mount point * @param string $backendClass backend class name * @param array $backendOptions backend-specific options + * @param array $mountOptions mount-specific options * @param array $applicableUsers users for which to mount the storage * @param array $applicableGroups groups for which to mount the storage * @param int $priority priority @@ -59,6 +60,7 @@ class GlobalStoragesController extends StoragesController { $mountPoint, $backendClass, $backendOptions, + $mountOptions, $applicableUsers, $applicableGroups, $priority @@ -67,6 +69,7 @@ class GlobalStoragesController extends StoragesController { $newStorage->setMountPoint($mountPoint); $newStorage->setBackendClass($backendClass); $newStorage->setBackendOptions($backendOptions); + $newStorage->setMountOptions($mountOptions); $newStorage->setApplicableUsers($applicableUsers); $newStorage->setApplicableGroups($applicableGroups); $newStorage->setPriority($priority); @@ -93,6 +96,7 @@ class GlobalStoragesController extends StoragesController { * @param string $mountPoint storage mount point * @param string $backendClass backend class name * @param array $backendOptions backend-specific options + * @param array $mountOptions mount-specific options * @param array $applicableUsers users for which to mount the storage * @param array $applicableGroups groups for which to mount the storage * @param int $priority priority @@ -104,6 +108,7 @@ class GlobalStoragesController extends StoragesController { $mountPoint, $backendClass, $backendOptions, + $mountOptions, $applicableUsers, $applicableGroups, $priority @@ -112,6 +117,7 @@ class GlobalStoragesController extends StoragesController { $storage->setMountPoint($mountPoint); $storage->setBackendClass($backendClass); $storage->setBackendOptions($backendOptions); + $storage->setMountOptions($mountOptions); $storage->setApplicableUsers($applicableUsers); $storage->setApplicableGroups($applicableGroups); $storage->setPriority($priority); diff --git a/apps/files_external/controller/userstoragescontroller.php b/apps/files_external/controller/userstoragescontroller.php index b77cbca59f..64202b5e54 100644 --- a/apps/files_external/controller/userstoragescontroller.php +++ b/apps/files_external/controller/userstoragescontroller.php @@ -86,6 +86,7 @@ class UserStoragesController extends StoragesController { * @param string $mountPoint storage mount point * @param string $backendClass backend class name * @param array $backendOptions backend-specific options + * @param array $mountOptions backend-specific mount options * * @return DataResponse * @@ -94,12 +95,14 @@ class UserStoragesController extends StoragesController { public function create( $mountPoint, $backendClass, - $backendOptions + $backendOptions, + $mountOptions ) { $newStorage = new StorageConfig(); $newStorage->setMountPoint($mountPoint); $newStorage->setBackendClass($backendClass); $newStorage->setBackendOptions($backendOptions); + $newStorage->setMountOptions($mountOptions); $response = $this->validate($newStorage); if (!empty($response)) { @@ -122,6 +125,7 @@ class UserStoragesController extends StoragesController { * @param string $mountPoint storage mount point * @param string $backendClass backend class name * @param array $backendOptions backend-specific options + * @param array $mountOptions backend-specific mount options * * @return DataResponse */ @@ -129,12 +133,14 @@ class UserStoragesController extends StoragesController { $id, $mountPoint, $backendClass, - $backendOptions + $backendOptions, + $mountOptions ) { $storage = new StorageConfig($id); $storage->setMountPoint($mountPoint); $storage->setBackendClass($backendClass); $storage->setBackendOptions($backendOptions); + $storage->setMountOptions($mountOptions); $response = $this->validate($storage); if (!empty($response)) { diff --git a/apps/files_external/js/settings.js b/apps/files_external/js/settings.js index 44488db4bc..ef64d08788 100644 --- a/apps/files_external/js/settings.js +++ b/apps/files_external/js/settings.js @@ -194,6 +194,13 @@ StorageConfig.prototype = { */ backendOptions: null, + /** + * Mount-specific options + * + * @type Object. + */ + mountOptions: null, + /** * Creates or saves the storage. * @@ -237,6 +244,9 @@ StorageConfig.prototype = { if (this.id) { data.id = this.id; } + if (this.mountOptions) { + data.mountOptions = this.mountOptions; + } return data; }, @@ -641,6 +651,11 @@ MountConfigListView.prototype = { storage.priority = $tr.find('input.priority').val(); } + var mountOptions = $tr.find('input.mountOptions').val(); + if (mountOptions) { + storage.mountOptions = JSON.parse(mountOptions); + } + return storage; }, diff --git a/apps/files_external/lib/config.php b/apps/files_external/lib/config.php index deeedb9855..378bedfa8d 100644 --- a/apps/files_external/lib/config.php +++ b/apps/files_external/lib/config.php @@ -357,6 +357,9 @@ class OC_Mount_Config { 'applicable' => array('groups' => array($group), 'users' => array()), 'status' => self::getBackendStatus($mount['class'], $mount['options'], false) ); + if (isset($mount['mountOptions'])) { + $config['mountOptions'] = $mount['mountOptions']; + } $hash = self::makeConfigHash($config); // If an existing config exists (with same class, mountpoint and options) if (isset($system[$hash])) { @@ -393,6 +396,9 @@ class OC_Mount_Config { 'applicable' => array('groups' => array(), 'users' => array($user)), 'status' => self::getBackendStatus($mount['class'], $mount['options'], false) ); + if (isset($mount['mountOptions'])) { + $config['mountOptions'] = $mount['mountOptions']; + } $hash = self::makeConfigHash($config); // If an existing config exists (with same class, mountpoint and options) if (isset($system[$hash])) { @@ -426,7 +432,7 @@ class OC_Mount_Config { $mount['class'] = '\OC\Files\Storage\\' . substr($mount['class'], 15); } $mount['options'] = self::decryptPasswords($mount['options']); - $personal[] = array( + $config = array( 'id' => (int) $mount['id'], 'storage_id' => (int) $mount['storage_id'], 'class' => $mount['class'], @@ -436,6 +442,10 @@ class OC_Mount_Config { 'options' => $mount['options'], 'status' => self::getBackendStatus($mount['class'], $mount['options'], true) ); + if (isset($mount['mountOptions'])) { + $config['mountOptions'] = $mount['mountOptions']; + } + $personal[] = $config; } } return $personal; diff --git a/apps/files_external/lib/storageconfig.php b/apps/files_external/lib/storageconfig.php index f23b5cd86a..a45321516d 100644 --- a/apps/files_external/lib/storageconfig.php +++ b/apps/files_external/lib/storageconfig.php @@ -53,6 +53,11 @@ class StorageConfig implements \JsonSerializable { */ private $applicableGroups = []; + /** + * @var array + */ + private $mountOptions = []; + /** * @param int|null $id config id or null for a new config */ @@ -195,6 +200,27 @@ class StorageConfig implements \JsonSerializable { $this->applicableGroups = $applicableGroups; } + /** + * Returns the mount-specific options + * + * @return array mount specific options + */ + public function getMountOptions() { + return $this->mountOptions; + } + + /** + * Sets the mount-specific options + * + * @param array applicable groups + */ + public function setMountOptions($mountOptions) { + if (is_null($mountOptions)) { + $mountOptions = []; + } + $this->mountOptions = $mountOptions; + } + /** * Sets the storage status, whether the config worked last time * @@ -235,6 +261,9 @@ class StorageConfig implements \JsonSerializable { if (!empty($this->applicableGroups)) { $result['applicableGroups'] = $this->applicableGroups; } + if (!empty($this->mountOptions)) { + $result['mountOptions'] = $this->mountOptions; + } if (!is_null($this->status)) { $result['status'] = $this->status; } diff --git a/apps/files_external/service/storagesservice.php b/apps/files_external/service/storagesservice.php index 52188b23a3..46a485a169 100644 --- a/apps/files_external/service/storagesservice.php +++ b/apps/files_external/service/storagesservice.php @@ -50,6 +50,7 @@ abstract class StoragesService { * - "priority": storage priority * - "backend": backend class name * - "options": backend-specific options + * - "mountOptions": mount-specific options (ex: disable previews, scanner, etc) */ // group by storage id @@ -82,6 +83,9 @@ abstract class StoragesService { $currentStorage->setBackendClass($storageOptions['class']); $currentStorage->setBackendOptions($storageOptions['options']); + if (isset($storageOptions['mountOptions'])) { + $currentStorage->setMountOptions($storageOptions['mountOptions']); + } if (isset($storageOptions['priority'])) { $currentStorage->setPriority($storageOptions['priority']); } @@ -141,6 +145,9 @@ abstract class StoragesService { if (!is_null($storageConfig->getPriority())) { $options['priority'] = $storageConfig->getPriority(); } + if (!empty($storageConfig->getMountOptions())) { + $options['mountOptions'] = $storageConfig->getMountOptions(); + } $mountPoints[$mountType][$applicable][$rootMountPoint] = $options; } diff --git a/apps/files_external/templates/settings.php b/apps/files_external/templates/settings.php index 4c54d4069b..3368b96a12 100644 --- a/apps/files_external/templates/settings.php +++ b/apps/files_external/templates/settings.php @@ -80,6 +80,14 @@ + + + + + + + + '> - - - class="remove" style="visibility:hidden;" diff --git a/apps/files_external/tests/controller/storagescontrollertest.php b/apps/files_external/tests/controller/storagescontrollertest.php index fefe2928d7..853b4a86f0 100644 --- a/apps/files_external/tests/controller/storagescontrollertest.php +++ b/apps/files_external/tests/controller/storagescontrollertest.php @@ -62,6 +62,7 @@ abstract class StoragesControllerTest extends \Test\TestCase { array(), [], [], + [], null ); @@ -85,6 +86,7 @@ abstract class StoragesControllerTest extends \Test\TestCase { array(), [], [], + [], null ); @@ -116,6 +118,7 @@ abstract class StoragesControllerTest extends \Test\TestCase { array(), [], [], + [], null ); @@ -128,6 +131,7 @@ abstract class StoragesControllerTest extends \Test\TestCase { array(), [], [], + [], null ); @@ -146,6 +150,7 @@ abstract class StoragesControllerTest extends \Test\TestCase { array(), [], [], + [], null ); @@ -158,6 +163,7 @@ abstract class StoragesControllerTest extends \Test\TestCase { array(), [], [], + [], null ); @@ -176,6 +182,7 @@ abstract class StoragesControllerTest extends \Test\TestCase { array(), [], [], + [], null ); @@ -204,6 +211,7 @@ abstract class StoragesControllerTest extends \Test\TestCase { $storageConfig->setMountPoint('test'); $storageConfig->setBackendClass('\OC\Files\Storage\SMB'); $storageConfig->setBackendOptions(['user' => 'test', 'password', 'password123']); + $storageConfig->setMountOptions(['priority' => false]); $this->service->expects($this->once()) ->method('getStorage') diff --git a/apps/files_external/tests/controller/userstoragescontrollertest.php b/apps/files_external/tests/controller/userstoragescontrollertest.php index 9d6fbb15e2..0ba413f695 100644 --- a/apps/files_external/tests/controller/userstoragescontrollertest.php +++ b/apps/files_external/tests/controller/userstoragescontrollertest.php @@ -91,6 +91,7 @@ class UserStoragesControllerTest extends StoragesControllerTest { array(), [], [], + [], null ); @@ -103,6 +104,7 @@ class UserStoragesControllerTest extends StoragesControllerTest { array(), [], [], + [], null ); diff --git a/apps/files_external/tests/service/globalstoragesservicetest.php b/apps/files_external/tests/service/globalstoragesservicetest.php index 6286865bf4..f5cdcfa390 100644 --- a/apps/files_external/tests/service/globalstoragesservicetest.php +++ b/apps/files_external/tests/service/globalstoragesservicetest.php @@ -50,6 +50,9 @@ class GlobalStoragesServiceTest extends StoragesServiceTest { 'applicableUsers' => [], 'applicableGroups' => [], 'priority' => 15, + 'mountOptions' => [ + 'preview' => false, + ] ]); } @@ -638,6 +641,7 @@ class GlobalStoragesServiceTest extends StoragesServiceTest { $this->assertEquals(1, $mountPointOptions['id']); $this->assertEquals('\OC\Files\Storage\SMB', $mountPointOptions['class']); $this->assertEquals(15, $mountPointOptions['priority']); + $this->assertEquals(false, $mountPointOptions['mountOptions']['preview']); $backendOptions = $mountPointOptions['options']; $this->assertEquals('value1', $backendOptions['option1']); @@ -677,6 +681,7 @@ class GlobalStoragesServiceTest extends StoragesServiceTest { $this->assertEquals(1, $mountPointOptions['id']); $this->assertEquals('\OC\Files\Storage\SMB', $mountPointOptions['class']); $this->assertEquals(15, $mountPointOptions['priority']); + $this->assertEquals(false, $mountPointOptions['mountOptions']['preview']); $backendOptions = $mountPointOptions['options']; $this->assertEquals('value1', $backendOptions['option1']); @@ -693,6 +698,7 @@ class GlobalStoragesServiceTest extends StoragesServiceTest { $this->assertEquals(1, $mountPointOptions['id']); $this->assertEquals('\OC\Files\Storage\SMB', $mountPointOptions['class']); $this->assertEquals(15, $mountPointOptions['priority']); + $this->assertEquals(false, $mountPointOptions['mountOptions']['preview']); $backendOptions = $mountPointOptions['options']; $this->assertEquals('value1', $backendOptions['option1']); diff --git a/apps/files_external/tests/service/storagesservicetest.php b/apps/files_external/tests/service/storagesservicetest.php index 1e338b3948..445e86d411 100644 --- a/apps/files_external/tests/service/storagesservicetest.php +++ b/apps/files_external/tests/service/storagesservicetest.php @@ -96,6 +96,9 @@ abstract class StoragesServiceTest extends \Test\TestCase { if (isset($data['priority'])) { $storage->setPriority($data['priority']); } + if (isset($data['mountOptions'])) { + $storage->setMountOptions($data['mountOptions']); + } return $storage; } diff --git a/apps/files_external/tests/service/userstoragesservicetest.php b/apps/files_external/tests/service/userstoragesservicetest.php index 64d59dc7d0..77b3842b31 100644 --- a/apps/files_external/tests/service/userstoragesservicetest.php +++ b/apps/files_external/tests/service/userstoragesservicetest.php @@ -61,6 +61,9 @@ class UserStoragesServiceTest extends StoragesServiceTest { 'option2' => 'value2', 'password' => 'testPassword', ], + 'mountOptions' => [ + 'preview' => false, + ] ]); } @@ -190,6 +193,7 @@ class UserStoragesServiceTest extends StoragesServiceTest { $mountPointOptions = current($mountPointData); $this->assertEquals(1, $mountPointOptions['id']); $this->assertEquals('\OC\Files\Storage\SMB', $mountPointOptions['class']); + $this->assertEquals(false, $mountPointOptions['mountOptions']['preview']); $backendOptions = $mountPointOptions['options']; $this->assertEquals('value1', $backendOptions['option1']); diff --git a/apps/files_external/tests/storageconfigtest.php b/apps/files_external/tests/storageconfigtest.php index 473dc20b38..ec79b1bf30 100644 --- a/apps/files_external/tests/storageconfigtest.php +++ b/apps/files_external/tests/storageconfigtest.php @@ -34,6 +34,7 @@ class StorageConfigTest extends \Test\TestCase { $storageConfig->setPriority(128); $storageConfig->setApplicableUsers(['user1', 'user2']); $storageConfig->setApplicableGroups(['group1', 'group2']); + $storageConfig->setMountOptions(['preview' => false]); $json = $storageConfig->jsonSerialize(); @@ -45,6 +46,7 @@ class StorageConfigTest extends \Test\TestCase { $this->assertEquals(128, $json['priority']); $this->assertEquals(['user1', 'user2'], $json['applicableUsers']); $this->assertEquals(['group1', 'group2'], $json['applicableGroups']); + $this->assertEquals(['preview' => false], $json['mountOptions']); } } From fb4cf532533d99abfd1b4f40ef6319d484f6ef85 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Fri, 13 Mar 2015 17:43:38 +0100 Subject: [PATCH 4/9] Fix mount config unit tests Fix expected result now that it returns the status. Added isset for some properties that are not always present. --- apps/files_external/lib/config.php | 24 +++++++--- apps/files_external/tests/mountconfig.php | 58 +++++++++++++++-------- 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/apps/files_external/lib/config.php b/apps/files_external/lib/config.php index 378bedfa8d..23750dbb3f 100644 --- a/apps/files_external/lib/config.php +++ b/apps/files_external/lib/config.php @@ -347,8 +347,6 @@ class OC_Mount_Config { $mountPoint = substr($mountPoint, 13); $config = array( - 'id' => (int) $mount['id'], - 'storage_id' => (int) $mount['storage_id'], 'class' => $mount['class'], 'mountpoint' => $mountPoint, 'backend' => $backends[$mount['class']]['backend'], @@ -357,6 +355,12 @@ class OC_Mount_Config { 'applicable' => array('groups' => array($group), 'users' => array()), 'status' => self::getBackendStatus($mount['class'], $mount['options'], false) ); + if (isset($mount['id'])) { + $config['id'] = (int)$mount['id']; + } + if (isset($mount['storage_id'])) { + $config['storage_id'] = (int)$mount['storage_id']; + } if (isset($mount['mountOptions'])) { $config['mountOptions'] = $mount['mountOptions']; } @@ -386,8 +390,6 @@ class OC_Mount_Config { // Remove '/$user/files/' from mount point $mountPoint = substr($mountPoint, 13); $config = array( - 'id' => (int) $mount['id'], - 'storage_id' => (int) $mount['storage_id'], 'class' => $mount['class'], 'mountpoint' => $mountPoint, 'backend' => $backends[$mount['class']]['backend'], @@ -396,6 +398,12 @@ class OC_Mount_Config { 'applicable' => array('groups' => array(), 'users' => array($user)), 'status' => self::getBackendStatus($mount['class'], $mount['options'], false) ); + if (isset($mount['id'])) { + $config['id'] = (int)$mount['id']; + } + if (isset($mount['storage_id'])) { + $config['storage_id'] = (int)$mount['storage_id']; + } if (isset($mount['mountOptions'])) { $config['mountOptions'] = $mount['mountOptions']; } @@ -433,8 +441,6 @@ class OC_Mount_Config { } $mount['options'] = self::decryptPasswords($mount['options']); $config = array( - 'id' => (int) $mount['id'], - 'storage_id' => (int) $mount['storage_id'], 'class' => $mount['class'], // Remove '/uid/files/' from mount point 'mountpoint' => substr($mountPoint, strlen($uid) + 8), @@ -442,6 +448,12 @@ class OC_Mount_Config { 'options' => $mount['options'], 'status' => self::getBackendStatus($mount['class'], $mount['options'], true) ); + if (isset($mount['id'])) { + $config['id'] = (int)$mount['id']; + } + if (isset($mount['storage_id'])) { + $config['storage_id'] = (int)$mount['storage_id']; + } if (isset($mount['mountOptions'])) { $config['mountOptions'] = $mount['mountOptions']; } diff --git a/apps/files_external/tests/mountconfig.php b/apps/files_external/tests/mountconfig.php index f00812c567..645f0c64e1 100644 --- a/apps/files_external/tests/mountconfig.php +++ b/apps/files_external/tests/mountconfig.php @@ -252,7 +252,7 @@ class Test_Mount_Config extends \Test\TestCase { 'password' => '12345', ); - $this->assertEquals(true, OC_Mount_Config::addMountPoint('/ext', 'Test_Mount_Config_Dummy_Storage', $storageOptions, $mountType, $applicable, $isPersonal)); + $this->assertEquals(0, OC_Mount_Config::addMountPoint('/ext', 'Test_Mount_Config_Dummy_Storage', $storageOptions, $mountType, $applicable, $isPersonal)); $config = $this->readGlobalConfig(); $this->assertEquals(1, count($config)); @@ -279,7 +279,7 @@ class Test_Mount_Config extends \Test\TestCase { 'password' => '12345', ); - $this->assertEquals(true, OC_Mount_Config::addMountPoint('/ext', 'Test_Mount_Config_Dummy_Storage', $storageOptions, $mountType, $applicable, $isPersonal)); + $this->assertEquals(0, OC_Mount_Config::addMountPoint('/ext', 'Test_Mount_Config_Dummy_Storage', $storageOptions, $mountType, $applicable, $isPersonal)); $config = $this->readUserConfig(); $this->assertEquals(1, count($config)); @@ -382,7 +382,8 @@ class Test_Mount_Config extends \Test\TestCase { ); // write config - $this->assertTrue( + $this->assertEquals( + 0, OC_Mount_Config::addMountPoint( '/ext', 'Test_Mount_Config_Dummy_Storage', @@ -422,7 +423,8 @@ class Test_Mount_Config extends \Test\TestCase { ); // write config - $this->assertTrue( + $this->assertEquals( + 0, OC_Mount_Config::addMountPoint( '/ext', 'Test_Mount_Config_Dummy_Storage', @@ -459,7 +461,8 @@ class Test_Mount_Config extends \Test\TestCase { ); // write config - $this->assertTrue( + $this->assertEquals( + 0, OC_Mount_Config::addMountPoint( $mountPoint, 'Test_Mount_Config_Dummy_Storage', @@ -492,7 +495,8 @@ class Test_Mount_Config extends \Test\TestCase { // edit $mountConfig['host'] = 'anothersmbhost'; - $this->assertTrue( + $this->assertEquals( + 0, OC_Mount_Config::addMountPoint( $mountPoint, 'Test_Mount_Config_Dummy_Storage', @@ -557,7 +561,8 @@ class Test_Mount_Config extends \Test\TestCase { ); // write config - $this->assertTrue( + $this->assertEquals( + 0, OC_Mount_Config::addMountPoint( '/ext', 'Test_Mount_Config_Dummy_Storage', @@ -598,7 +603,8 @@ class Test_Mount_Config extends \Test\TestCase { ); // write config - $this->assertTrue( + $this->assertEquals( + 0, OC_Mount_Config::addMountPoint( '/ext', 'Test_Mount_Config_Dummy_Storage', @@ -707,7 +713,8 @@ class Test_Mount_Config extends \Test\TestCase { ); // add mount point as "test" user - $this->assertTrue( + $this->assertEquals( + 0, OC_Mount_Config::addMountPoint( '/ext', 'Test_Mount_Config_Dummy_Storage', @@ -750,7 +757,8 @@ class Test_Mount_Config extends \Test\TestCase { ); // write config - $this->assertTrue( + $this->assertEquals( + 0, OC_Mount_Config::addMountPoint( '/ext', 'Test_Mount_Config_Dummy_Storage', @@ -761,7 +769,8 @@ class Test_Mount_Config extends \Test\TestCase { ) ); - $this->assertTrue( + $this->assertEquals( + 0, OC_Mount_Config::addMountPoint( '/ext', 'Test_Mount_Config_Dummy_Storage', @@ -772,7 +781,8 @@ class Test_Mount_Config extends \Test\TestCase { ) ); - $this->assertTrue( + $this->assertEquals( + 0, OC_Mount_Config::addMountPoint( '/ext', 'Test_Mount_Config_Dummy_Storage', @@ -783,7 +793,8 @@ class Test_Mount_Config extends \Test\TestCase { ) ); - $this->assertTrue( + $this->assertEquals( + 0, OC_Mount_Config::addMountPoint( '/ext', 'Test_Mount_Config_Dummy_Storage', @@ -821,7 +832,8 @@ class Test_Mount_Config extends \Test\TestCase { ); // write config - $this->assertTrue( + $this->assertEquals( + 0, OC_Mount_Config::addMountPoint( '/ext', 'Test_Mount_Config_Dummy_Storage', @@ -839,7 +851,8 @@ class Test_Mount_Config extends \Test\TestCase { 'share' => 'anothersmbshare', 'root' => 'anothersmbroot' ); - $this->assertTrue( + $this->assertEquals( + 0, OC_Mount_Config::addMountPoint( '/ext', 'Test_Mount_Config_Dummy_Storage', @@ -952,7 +965,8 @@ class Test_Mount_Config extends \Test\TestCase { // Add mount points foreach($mounts as $i => $mount) { - $this->assertTrue( + $this->assertEquals( + 0, OC_Mount_Config::addMountPoint( '/ext', 'Test_Mount_Config_Dummy_Storage', @@ -987,7 +1001,8 @@ class Test_Mount_Config extends \Test\TestCase { 'share' => '', ); - $this->assertTrue( + $this->assertEquals( + 0, OC_Mount_Config::addMountPoint( '/ext', $class, @@ -1005,7 +1020,8 @@ class Test_Mount_Config extends \Test\TestCase { $mountPoints['/'.self::TEST_USER1.'/files/ext']['priority']); // Simulate changed mount options (without priority set) - $this->assertTrue( + $this->assertEquals( + 0, OC_Mount_Config::addMountPoint( '/ext', $class, @@ -1035,7 +1051,8 @@ class Test_Mount_Config extends \Test\TestCase { ); // Create personal mount point - $this->assertTrue( + $this->assertEquals( + 0, OC_Mount_Config::addMountPoint( '/ext', 'Test_Mount_Config_Dummy_Storage', @@ -1066,7 +1083,8 @@ class Test_Mount_Config extends \Test\TestCase { $applicable = 'all'; $isPersonal = false; - $this->assertTrue( + $this->assertEquals( + 0, OC_Mount_Config::addMountPoint( '/ext', $storageClass, From 72632ad402bd905107db836ed0f9bfae825d6c52 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Mon, 16 Mar 2015 12:18:01 +0100 Subject: [PATCH 5/9] Generate storage config ids when missing When reading in old mount.json files, they do not contain config ids. Since these are needed to be able to use the UI and the new service classes, these will be generated automatically. The config grouping is based on a config hash. --- .../controller/globalstoragescontroller.php | 13 +- .../controller/userstoragescontroller.php | 13 +- apps/files_external/lib/config.php | 6 +- apps/files_external/personal.php | 22 ++- .../service/storagesservice.php | 130 ++++++++++++++---- apps/files_external/settings.php | 22 ++- .../service/globalstoragesservicetest.php | 104 ++++++++++++++ .../tests/service/userstoragesservicetest.php | 50 +++++++ 8 files changed, 317 insertions(+), 43 deletions(-) diff --git a/apps/files_external/controller/globalstoragescontroller.php b/apps/files_external/controller/globalstoragescontroller.php index e5aff4f95a..815f24ee2b 100644 --- a/apps/files_external/controller/globalstoragescontroller.php +++ b/apps/files_external/controller/globalstoragescontroller.php @@ -15,6 +15,7 @@ namespace OCA\Files_External\Controller; use \OCP\IConfig; use \OCP\IUserSession; use \OCP\IRequest; +use \OCP\IL10N; use \OCP\AppFramework\Http\DataResponse; use \OCP\AppFramework\Controller; use \OCP\AppFramework\Http; @@ -24,15 +25,17 @@ use \OCA\Files_external\Lib\StorageConfig; class GlobalStoragesController extends StoragesController { /** - * @param string $appName - * @param IRequest $request - * @param IConfig $config - * @param GlobalStoragesService $globalStoragesService + * Creates a new global storages controller. + * + * @param string $AppName application name + * @param IRequest $request request object + * @param IL10N $l10n l10n service + * @param GlobalStoragesService $globalStoragesService storage service */ public function __construct( $AppName, IRequest $request, - \OCP\IL10N $l10n, + IL10N $l10n, GlobalStoragesService $globalStoragesService ){ parent::__construct( diff --git a/apps/files_external/controller/userstoragescontroller.php b/apps/files_external/controller/userstoragescontroller.php index 64202b5e54..ed7ec453cc 100644 --- a/apps/files_external/controller/userstoragescontroller.php +++ b/apps/files_external/controller/userstoragescontroller.php @@ -15,6 +15,7 @@ namespace OCA\Files_External\Controller; use \OCP\IConfig; use \OCP\IUserSession; use \OCP\IRequest; +use \OCP\IL10N; use \OCP\AppFramework\Http\DataResponse; use \OCP\AppFramework\Controller; use \OCP\AppFramework\Http; @@ -24,15 +25,17 @@ use \OCA\Files_external\Lib\StorageConfig; class UserStoragesController extends StoragesController { /** - * @param string $appName - * @param IRequest $request - * @param IConfig $config - * @param UserStoragesService $userStoragesService + * Creates a new user storages controller. + * + * @param string $AppName application name + * @param IRequest $request request object + * @param IL10N $l10n l10n service + * @param UserStoragesService $userStoragesService storage service */ public function __construct( $AppName, IRequest $request, - \OCP\IL10N $l10n, + IL10N $l10n, UserStoragesService $userStoragesService ){ parent::__construct( diff --git a/apps/files_external/lib/config.php b/apps/files_external/lib/config.php index 23750dbb3f..884e596bdd 100644 --- a/apps/files_external/lib/config.php +++ b/apps/files_external/lib/config.php @@ -894,12 +894,14 @@ class OC_Mount_Config { * This is mostly used to find out whether configurations * are the same. */ - private static function makeConfigHash($config) { + public static function makeConfigHash($config) { $data = json_encode( array( 'c' => $config['class'], 'm' => $config['mountpoint'], - 'o' => $config['options'] + 'o' => $config['options'], + 'p' => isset($config['priority']) ? $config['priority'] : -1, + 'mo' => isset($config['mountOptions']) ? $config['mountOptions'] : [], ) ); return hash('md5', $data); diff --git a/apps/files_external/personal.php b/apps/files_external/personal.php index a279163ff7..8b43544225 100644 --- a/apps/files_external/personal.php +++ b/apps/files_external/personal.php @@ -24,9 +24,29 @@ OCP\Util::addScript('files_external', 'settings'); OCP\Util::addStyle('files_external', 'settings'); $backends = OC_Mount_Config::getPersonalBackends(); +$mounts = OC_Mount_Config::getPersonalMountPoints(); +$hasId = true; +foreach ($mounts as $mount) { + if (!isset($mount['id'])) { + // some mount points are missing ids + $hasId = false; + break; + } +} + +if (!$hasId) { + $service = new \OCA\Files_external\Service\UserStoragesService(\OC::$server->getUserSession()); + // this will trigger the new storage code which will automatically + // generate storage config ids + $service->getAllStorages(); + // re-read updated config + $mounts = OC_Mount_Config::getPersonalMountPoints(); + // TODO: use the new storage config format in the template +} + $tmpl = new OCP\Template('files_external', 'settings'); $tmpl->assign('isAdminPage', false); -$tmpl->assign('mounts', OC_Mount_Config::getPersonalMountPoints()); +$tmpl->assign('mounts', $mounts); $tmpl->assign('dependencies', OC_Mount_Config::checkDependencies()); $tmpl->assign('backends', $backends); return $tmpl->fetchPage(); diff --git a/apps/files_external/service/storagesservice.php b/apps/files_external/service/storagesservice.php index 46a485a169..73a0ae7647 100644 --- a/apps/files_external/service/storagesservice.php +++ b/apps/files_external/service/storagesservice.php @@ -29,6 +29,39 @@ abstract class StoragesService { return \OC_Mount_Config::readData(); } + /** + * Copy legacy storage options into the given storage config object. + * + * @param StorageConfig $storageConfig storage config to populate + * @param string $mountType mount type + * @param string $applicable applicable user or group + * @param array $storageOptions legacy storage options + * @return StorageConfig populated storage config + */ + protected function populateStorageConfigWithLegacyOptions(&$storageConfig, $mountType, $applicable, $storageOptions) { + $storageConfig->setBackendClass($storageOptions['class']); + $storageConfig->setBackendOptions($storageOptions['options']); + if (isset($storageOptions['mountOptions'])) { + $storageConfig->setMountOptions($storageOptions['mountOptions']); + } + if (isset($storageOptions['priority'])) { + $storageConfig->setPriority($storageOptions['priority']); + } + + if ($mountType === \OC_Mount_Config::MOUNT_TYPE_USER) { + $applicableUsers = $storageConfig->getApplicableUsers(); + if ($applicable !== 'all') { + $applicableUsers[] = $applicable; + $storageConfig->setApplicableUsers($applicableUsers); + } + } else if ($mountType === \OC_Mount_Config::MOUNT_TYPE_GROUP) { + $applicableGroups = $storageConfig->getApplicableGroups(); + $applicableGroups[] = $applicable; + $storageConfig->setApplicableGroups($applicableGroups); + } + return $storageConfig; + } + /** * Read the external storages config * @@ -55,9 +88,25 @@ abstract class StoragesService { // group by storage id $storages = []; + + // for storages without id (legacy), group by config hash for + // later processing + $storagesWithConfigHash = []; + foreach ($mountPoints as $mountType => $applicables) { foreach ($applicables as $applicable => $mountPaths) { foreach ($mountPaths as $rootMountPath => $storageOptions) { + $currentStorage = null; + + /** + * Flag whether the config that was read already has an id. + * If not, it will use a config hash instead and generate + * a proper id later + * + * @var boolean + */ + $hasId = false; + // the root mount point is in the format "/$user/files/the/mount/point" // we remove the "/$user/files" prefix $parts = explode('/', trim($rootMountPath, '/'), 3); @@ -73,46 +122,60 @@ abstract class StoragesService { $relativeMountPath = $parts[2]; - $configId = (int)$storageOptions['id']; - if (isset($storages[$configId])) { - $currentStorage = $storages[$configId]; + // note: we cannot do this after the loop because the decrypted config + // options might be needed for the config hash + $storageOptions['options'] = \OC_Mount_Config::decryptPasswords($storageOptions['options']); + + if (isset($storageOptions['id'])) { + $configId = (int)$storageOptions['id']; + if (isset($storages[$configId])) { + $currentStorage = $storages[$configId]; + } + $hasId = true; } else { + // missing id in legacy config, need to generate + // but at this point we don't know the max-id, so use + // first group it by config hash + $storageOptions['mountpoint'] = $rootMountPath; + $configId = \OC_Mount_Config::makeConfigHash($storageOptions); + if (isset($storagesWithConfigHash[$configId])) { + $currentStorage = $storagesWithConfigHash[$configId]; + } + } + + if (is_null($currentStorage)) { + // create new $currentStorage = new StorageConfig($configId); $currentStorage->setMountPoint($relativeMountPath); } - $currentStorage->setBackendClass($storageOptions['class']); - $currentStorage->setBackendOptions($storageOptions['options']); - if (isset($storageOptions['mountOptions'])) { - $currentStorage->setMountOptions($storageOptions['mountOptions']); - } - if (isset($storageOptions['priority'])) { - $currentStorage->setPriority($storageOptions['priority']); - } + $this->populateStorageConfigWithLegacyOptions( + $currentStorage, + $mountType, + $applicable, + $storageOptions + ); - if ($mountType === \OC_Mount_Config::MOUNT_TYPE_USER) { - $applicableUsers = $currentStorage->getApplicableUsers(); - if ($applicable !== 'all') { - $applicableUsers[] = $applicable; - $currentStorage->setApplicableUsers($applicableUsers); - } - } else if ($mountType === \OC_Mount_Config::MOUNT_TYPE_GROUP) { - $applicableGroups = $currentStorage->getApplicableGroups(); - $applicableGroups[] = $applicable; - $currentStorage->setApplicableGroups($applicableGroups); + if ($hasId) { + $storages[$configId] = $currentStorage; + } else { + $storagesWithConfigHash[$configId] = $currentStorage; } - $storages[$configId] = $currentStorage; } } } - // decrypt passwords - foreach ($storages as &$storage) { - $storage->setBackendOptions( - \OC_Mount_Config::decryptPasswords( - $storage->getBackendOptions() - ) - ); + // process storages with config hash, they must get a real id + if (!empty($storagesWithConfigHash)) { + $nextId = $this->generateNextId($storages); + foreach ($storagesWithConfigHash as $storage) { + $storage->setId($nextId); + $storages[$nextId] = $storage; + $nextId++; + } + + // re-save the config with the generated ids + $this->writeConfig($storages); } return $storages; @@ -176,6 +239,15 @@ abstract class StoragesService { return $allStorages[$id]; } + /** + * Gets all storages + * + * @return array array of storage configs + */ + public function getAllStorages() { + return $this->readConfig(); + } + /** * Add new storage to the configuration * diff --git a/apps/files_external/settings.php b/apps/files_external/settings.php index dec329e82a..3144e1a2ab 100644 --- a/apps/files_external/settings.php +++ b/apps/files_external/settings.php @@ -42,9 +42,29 @@ foreach ($backends as $class => $backend) } } +$mounts = OC_Mount_Config::getSystemMountPoints(); +$hasId = true; +foreach ($mounts as $mount) { + if (!isset($mount['id'])) { + // some mount points are missing ids + $hasId = false; + break; + } +} + +if (!$hasId) { + $service = new \OCA\Files_external\Service\GlobalStoragesService(); + // this will trigger the new storage code which will automatically + // generate storage config ids + $service->getAllStorages(); + // re-read updated config + $mounts = OC_Mount_Config::getSystemMountPoints(); + // TODO: use the new storage config format in the template +} + $tmpl = new OCP\Template('files_external', 'settings'); $tmpl->assign('isAdminPage', true); -$tmpl->assign('mounts', OC_Mount_Config::getSystemMountPoints()); +$tmpl->assign('mounts', $mounts); $tmpl->assign('backends', $backends); $tmpl->assign('personal_backends', $personal_backends); $tmpl->assign('dependencies', OC_Mount_Config::checkDependencies()); diff --git a/apps/files_external/tests/service/globalstoragesservicetest.php b/apps/files_external/tests/service/globalstoragesservicetest.php index f5cdcfa390..4c038bc489 100644 --- a/apps/files_external/tests/service/globalstoragesservicetest.php +++ b/apps/files_external/tests/service/globalstoragesservicetest.php @@ -708,4 +708,108 @@ class GlobalStoragesServiceTest extends StoragesServiceTest { } } + /** + * Test reading in a legacy config and generating config ids. + */ + public function testReadLegacyConfigAndGenerateConfigId() { + $configFile = $this->dataDir . '/mount.json'; + + $legacyBackendOptions = [ + 'user' => 'someuser', + 'password' => 'somepassword', + ]; + $legacyBackendOptions = \OC_Mount_Config::encryptPasswords($legacyBackendOptions); + + $legacyConfig = [ + 'class' => '\OC\Files\Storage\SMB', + 'options' => $legacyBackendOptions, + 'mountOptions' => ['preview' => false], + ]; + // different mount options + $legacyConfig2 = [ + 'class' => '\OC\Files\Storage\SMB', + 'options' => $legacyBackendOptions, + 'mountOptions' => ['preview' => true], + ]; + + $legacyBackendOptions2 = $legacyBackendOptions; + $legacyBackendOptions2 = ['user' => 'someuser2', 'password' => 'somepassword2']; + $legacyBackendOptions2 = \OC_Mount_Config::encryptPasswords($legacyBackendOptions2); + + // different config + $legacyConfig3 = [ + 'class' => '\OC\Files\Storage\SMB', + 'options' => $legacyBackendOptions2, + 'mountOptions' => ['preview' => true], + ]; + + $json = [ + 'user' => [ + 'user1' => [ + '/$user/files/somemount' => $legacyConfig, + ], + // same config + 'user2' => [ + '/$user/files/somemount' => $legacyConfig, + ], + // different mountOptions + 'user3' => [ + '/$user/files/somemount' => $legacyConfig2, + ], + // different mount point + 'user4' => [ + '/$user/files/anothermount' => $legacyConfig, + ], + // different storage config + 'user5' => [ + '/$user/files/somemount' => $legacyConfig3, + ], + ], + 'group' => [ + 'group1' => [ + // will get grouped with user configs + '/$user/files/somemount' => $legacyConfig, + ], + ], + ]; + + file_put_contents($configFile, json_encode($json)); + + $allStorages = $this->service->getAllStorages(); + + $this->assertCount(4, $allStorages); + + $storage1 = $allStorages[1]; + $storage2 = $allStorages[2]; + $storage3 = $allStorages[3]; + $storage4 = $allStorages[4]; + + $this->assertEquals('/somemount', $storage1->getMountPoint()); + $this->assertEquals('someuser', $storage1->getBackendOptions()['user']); + $this->assertEquals('somepassword', $storage1->getBackendOptions()['password']); + $this->assertEquals(['user1', 'user2'], $storage1->getApplicableUsers()); + $this->assertEquals(['group1'], $storage1->getApplicableGroups()); + $this->assertEquals(['preview' => false], $storage1->getMountOptions()); + + $this->assertEquals('/somemount', $storage2->getMountPoint()); + $this->assertEquals('someuser', $storage2->getBackendOptions()['user']); + $this->assertEquals('somepassword', $storage2->getBackendOptions()['password']); + $this->assertEquals(['user3'], $storage2->getApplicableUsers()); + $this->assertEquals([], $storage2->getApplicableGroups()); + $this->assertEquals(['preview' => true], $storage2->getMountOptions()); + + $this->assertEquals('/anothermount', $storage3->getMountPoint()); + $this->assertEquals('someuser', $storage3->getBackendOptions()['user']); + $this->assertEquals('somepassword', $storage3->getBackendOptions()['password']); + $this->assertEquals(['user4'], $storage3->getApplicableUsers()); + $this->assertEquals([], $storage3->getApplicableGroups()); + $this->assertEquals(['preview' => false], $storage3->getMountOptions()); + + $this->assertEquals('/somemount', $storage4->getMountPoint()); + $this->assertEquals('someuser2', $storage4->getBackendOptions()['user']); + $this->assertEquals('somepassword2', $storage4->getBackendOptions()['password']); + $this->assertEquals(['user5'], $storage4->getApplicableUsers()); + $this->assertEquals([], $storage4->getApplicableGroups()); + $this->assertEquals(['preview' => true], $storage4->getMountOptions()); + } } diff --git a/apps/files_external/tests/service/userstoragesservicetest.php b/apps/files_external/tests/service/userstoragesservicetest.php index 77b3842b31..dd3f9e1b92 100644 --- a/apps/files_external/tests/service/userstoragesservicetest.php +++ b/apps/files_external/tests/service/userstoragesservicetest.php @@ -201,4 +201,54 @@ class UserStoragesServiceTest extends StoragesServiceTest { $this->assertEquals('', $backendOptions['password']); $this->assertNotEmpty($backendOptions['password_encrypted']); } + + /** + * Test reading in a legacy config and generating config ids. + */ + public function testReadLegacyConfigAndGenerateConfigId() { + $configFile = $this->dataDir . '/' . $this->userId . '/mount.json'; + + $legacyBackendOptions = [ + 'user' => 'someuser', + 'password' => 'somepassword', + ]; + $legacyBackendOptions = \OC_Mount_Config::encryptPasswords($legacyBackendOptions); + + $legacyConfig = [ + 'class' => '\OC\Files\Storage\SMB', + 'options' => $legacyBackendOptions, + 'mountOptions' => ['preview' => false], + ]; + // different mount options + $legacyConfig2 = [ + 'class' => '\OC\Files\Storage\SMB', + 'options' => $legacyBackendOptions, + 'mountOptions' => ['preview' => true], + ]; + + $json = ['user' => []]; + $json['user'][$this->userId] = [ + '/$user/files/somemount' => $legacyConfig, + '/$user/files/anothermount' => $legacyConfig2, + ]; + + file_put_contents($configFile, json_encode($json)); + + $allStorages = $this->service->getAllStorages(); + + $this->assertCount(2, $allStorages); + + $storage1 = $allStorages[1]; + $storage2 = $allStorages[2]; + + $this->assertEquals('/somemount', $storage1->getMountPoint()); + $this->assertEquals('someuser', $storage1->getBackendOptions()['user']); + $this->assertEquals('somepassword', $storage1->getBackendOptions()['password']); + $this->assertEquals(['preview' => false], $storage1->getMountOptions()); + + $this->assertEquals('/anothermount', $storage2->getMountPoint()); + $this->assertEquals('someuser', $storage2->getBackendOptions()['user']); + $this->assertEquals('somepassword', $storage2->getBackendOptions()['password']); + $this->assertEquals(['preview' => true], $storage2->getMountOptions()); + } } From 06448170cfc66ad481ab999926252fbab2c1824c Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Mon, 16 Mar 2015 14:39:48 +0100 Subject: [PATCH 6/9] Fix PHPDoc for StoragesController --- .../controller/storagescontroller.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/files_external/controller/storagescontroller.php b/apps/files_external/controller/storagescontroller.php index f047ba34b5..5cdbfe735e 100644 --- a/apps/files_external/controller/storagescontroller.php +++ b/apps/files_external/controller/storagescontroller.php @@ -15,6 +15,7 @@ namespace OCA\Files_External\Controller; use \OCP\IConfig; use \OCP\IUserSession; use \OCP\IRequest; +use \OCP\IL10N; use \OCP\AppFramework\Http\DataResponse; use \OCP\AppFramework\Controller; use \OCP\AppFramework\Http; @@ -25,7 +26,7 @@ use \OCA\Files_external\Lib\StorageConfig; abstract class StoragesController extends Controller { /** - * @var \OCP\IL10N + * @var IL10N */ protected $l10n; @@ -35,15 +36,17 @@ abstract class StoragesController extends Controller { protected $service; /** - * @param string $appName - * @param IRequest $request - * @param IConfig $config - * @param StoragesService $storagesService + * Creates a new storages controller. + * + * @param string $AppName application name + * @param IRequest $request request object + * @param IL10N $l10n l10n service + * @param StoragesService $storagesService storage service */ public function __construct( $AppName, IRequest $request, - \OCP\IL10N $l10n, + IL10N $l10n, StoragesService $storagesService ){ parent::__construct($AppName, $request); From 847880803e9bdbaae332762e197ac13ffe552233 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Mon, 16 Mar 2015 15:07:38 +0100 Subject: [PATCH 7/9] Fix storage priority in JS unit test --- apps/files_external/tests/js/settingsSpec.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/files_external/tests/js/settingsSpec.js b/apps/files_external/tests/js/settingsSpec.js index 350840e542..5a3ee2cb5f 100644 --- a/apps/files_external/tests/js/settingsSpec.js +++ b/apps/files_external/tests/js/settingsSpec.js @@ -56,14 +56,16 @@ describe('OCA.External.Settings tests', function() { 'configuration': { 'field1': 'Display Name 1', 'field2': '&Display Name 2' - } + }, + 'priority': 11 }, '\\OC\\AnotherTestBackend': { 'backend': 'Another Test Backend Name', 'configuration': { 'field1': 'Display Name 1', 'field2': '&Display Name 2' - } + }, + 'priority': 12 } } ); @@ -132,7 +134,8 @@ describe('OCA.External.Settings tests', function() { backendClass: '\\OC\\TestBackend', 'backendOptions[field1]': 'test', 'backendOptions[field2]': '', - mountPoint: 'TestBackend' + mountPoint: 'TestBackend', + priority: '11' }); // TODO: respond and check data-id From e5e30924b14c22cc68b2fe0c47144f06c4a997aa Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Tue, 17 Mar 2015 11:42:52 +0100 Subject: [PATCH 8/9] Fix PHPDoc in files_external, add missing tag Fix various PHPDoc issues in external storage app. Added missing NoAdminRequired tag --- apps/files_external/appinfo/routes.php | 3 +- .../controller/globalstoragescontroller.php | 6 ++-- .../controller/storagescontroller.php | 11 ++++-- .../controller/userstoragescontroller.php | 18 +++++++--- apps/files_external/lib/storageconfig.php | 34 +++++++++++++++---- .../service/globalstoragesservice.php | 5 ++- .../service/storagesservice.php | 17 +++++++--- .../service/userstoragesservice.php | 8 +++++ 8 files changed, 77 insertions(+), 25 deletions(-) diff --git a/apps/files_external/appinfo/routes.php b/apps/files_external/appinfo/routes.php index 506c9d34e2..a090fca20e 100644 --- a/apps/files_external/appinfo/routes.php +++ b/apps/files_external/appinfo/routes.php @@ -23,9 +23,8 @@ namespace OCA\Files_External\Appinfo; /** - * @var $this OC\Route\Router + * @var $this \OC\Route\Router **/ - $application = new Application(); $application->registerRoutes( $this, diff --git a/apps/files_external/controller/globalstoragescontroller.php b/apps/files_external/controller/globalstoragescontroller.php index 815f24ee2b..819cccdbde 100644 --- a/apps/files_external/controller/globalstoragescontroller.php +++ b/apps/files_external/controller/globalstoragescontroller.php @@ -23,6 +23,9 @@ use \OCA\Files_external\Service\GlobalStoragesService; use \OCA\Files_external\NotFoundException; use \OCA\Files_external\Lib\StorageConfig; +/** + * Global storages controller + */ class GlobalStoragesController extends StoragesController { /** * Creates a new global storages controller. @@ -37,7 +40,7 @@ class GlobalStoragesController extends StoragesController { IRequest $request, IL10N $l10n, GlobalStoragesService $globalStoragesService - ){ + ) { parent::__construct( $AppName, $request, @@ -151,4 +154,3 @@ class GlobalStoragesController extends StoragesController { } } - diff --git a/apps/files_external/controller/storagescontroller.php b/apps/files_external/controller/storagescontroller.php index 5cdbfe735e..b454dff702 100644 --- a/apps/files_external/controller/storagescontroller.php +++ b/apps/files_external/controller/storagescontroller.php @@ -23,14 +23,21 @@ use \OCA\Files_external\Service\StoragesService; use \OCA\Files_external\NotFoundException; use \OCA\Files_external\Lib\StorageConfig; +/** + * Base class for storages controllers + */ abstract class StoragesController extends Controller { /** + * L10N service + * * @var IL10N */ protected $l10n; /** + * Storages service + * * @var StoragesService */ protected $service; @@ -48,7 +55,7 @@ abstract class StoragesController extends Controller { IRequest $request, IL10N $l10n, StoragesService $storagesService - ){ + ) { parent::__construct($AppName, $request); $this->l10n = $l10n; $this->service = $storagesService; @@ -94,7 +101,7 @@ abstract class StoragesController extends Controller { * Note that this operation can be time consuming depending * on whether the remote storage is available or not. * - * @param StorageConfig $storage + * @param StorageConfig $storage storage configuration */ protected function updateStorageStatus(StorageConfig &$storage) { // update status (can be time-consuming) diff --git a/apps/files_external/controller/userstoragescontroller.php b/apps/files_external/controller/userstoragescontroller.php index ed7ec453cc..86557988ea 100644 --- a/apps/files_external/controller/userstoragescontroller.php +++ b/apps/files_external/controller/userstoragescontroller.php @@ -23,6 +23,9 @@ use \OCA\Files_external\Service\UserStoragesService; use \OCA\Files_external\NotFoundException; use \OCA\Files_external\Lib\StorageConfig; +/** + * User storages controller + */ class UserStoragesController extends StoragesController { /** * Creates a new user storages controller. @@ -37,7 +40,7 @@ class UserStoragesController extends StoragesController { IRequest $request, IL10N $l10n, UserStoragesService $userStoragesService - ){ + ) { parent::__construct( $AppName, $request, @@ -76,8 +79,11 @@ class UserStoragesController extends StoragesController { } /** + * Return storage + * * @NoAdminRequired - * @{inheritdoc} + * + * {@inheritdoc} */ public function show($id) { return parent::show($id); @@ -131,6 +137,8 @@ class UserStoragesController extends StoragesController { * @param array $mountOptions backend-specific mount options * * @return DataResponse + * + * @NoAdminRequired */ public function update( $id, @@ -171,11 +179,13 @@ class UserStoragesController extends StoragesController { } /** - * {@inheritdoc} + * Delete storage + * * @NoAdminRequired + * + * {@inheritdoc} */ public function destroy($id) { return parent::destroy($id); } } - diff --git a/apps/files_external/lib/storageconfig.php b/apps/files_external/lib/storageconfig.php index a45321516d..80d0152db8 100644 --- a/apps/files_external/lib/storageconfig.php +++ b/apps/files_external/lib/storageconfig.php @@ -14,51 +14,71 @@ namespace OCA\Files_external\Lib; class StorageConfig implements \JsonSerializable { /** + * Storage config id + * * @var int */ private $id; /** + * Backend class name + * * @var string */ private $backendClass; /** + * Backend options + * * @var array */ private $backendOptions = []; /** + * Mount point path, relative to the user's "files" folder + * * @var string */ private $mountPoint; /** + * Storage status + * * @var int */ private $status; /** + * Priority + * * @var int */ private $priority; /** + * List of users who have access to this storage + * * @var array */ private $applicableUsers = []; /** + * List of groups that have access to this storage + * * @var array */ private $applicableGroups = []; /** + * Mount-specific options + * * @var array */ private $mountOptions = []; /** + * Creates a storage config + * * @param int|null $id config id or null for a new config */ public function __construct($id = null) { @@ -77,7 +97,7 @@ class StorageConfig implements \JsonSerializable { /** * Sets the configuration id * - * @param int configuration id + * @param int $id configuration id */ public function setId($id) { $this->id = $id; @@ -98,7 +118,7 @@ class StorageConfig implements \JsonSerializable { * "files" folder. * The path will be normalized. * - * @param string path + * @param string $mountPoint path */ public function setMountPoint($mountPoint) { $this->mountPoint = \OC\Files\Filesystem::normalizePath($mountPoint); @@ -134,7 +154,7 @@ class StorageConfig implements \JsonSerializable { /** * Sets the external storage backend-specific options * - * @param array backend options + * @param array $backendOptions backend options */ public function setBackendOptions($backendOptions) { $this->backendOptions = $backendOptions; @@ -152,7 +172,7 @@ class StorageConfig implements \JsonSerializable { /** * Sets the mount priotity * - * @param int priority + * @param int $priority priority */ public function setPriority($priority) { $this->priority = $priority; @@ -170,7 +190,7 @@ class StorageConfig implements \JsonSerializable { /** * Sets the users for which to mount this storage * - * @param array applicable users + * @param array|null $applicableUsers applicable users */ public function setApplicableUsers($applicableUsers) { if (is_null($applicableUsers)) { @@ -191,7 +211,7 @@ class StorageConfig implements \JsonSerializable { /** * Sets the groups for which to mount this storage * - * @param array applicable groups + * @param array|null $applicableGroups applicable groups */ public function setApplicableGroups($applicableGroups) { if (is_null($applicableGroups)) { @@ -212,7 +232,7 @@ class StorageConfig implements \JsonSerializable { /** * Sets the mount-specific options * - * @param array applicable groups + * @param array $mountOptions applicable groups */ public function setMountOptions($mountOptions) { if (is_null($mountOptions)) { diff --git a/apps/files_external/service/globalstoragesservice.php b/apps/files_external/service/globalstoragesservice.php index 257c9bd467..b024824c46 100644 --- a/apps/files_external/service/globalstoragesservice.php +++ b/apps/files_external/service/globalstoragesservice.php @@ -22,7 +22,6 @@ class GlobalStoragesService extends StoragesService { /** * Write the storages to the configuration. * - * @param string $user user or null for global config * @param array $storages map of storage id to storage config */ public function writeConfig($storages) { @@ -121,8 +120,8 @@ class GlobalStoragesService extends StoragesService { * accomodate for additions/deletions in applicableUsers * and applicableGroups fields. * - * @param StorageConfig $oldStorage old storage data - * @param StorageConfig $newStorage new storage data + * @param StorageConfig $oldStorage old storage config + * @param StorageConfig $newStorage new storage config */ protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage) { // if mount point changed, it's like a deletion + creation diff --git a/apps/files_external/service/storagesservice.php b/apps/files_external/service/storagesservice.php index 73a0ae7647..6800474126 100644 --- a/apps/files_external/service/storagesservice.php +++ b/apps/files_external/service/storagesservice.php @@ -36,9 +36,15 @@ abstract class StoragesService { * @param string $mountType mount type * @param string $applicable applicable user or group * @param array $storageOptions legacy storage options + * * @return StorageConfig populated storage config */ - protected function populateStorageConfigWithLegacyOptions(&$storageConfig, $mountType, $applicable, $storageOptions) { + protected function populateStorageConfigWithLegacyOptions( + &$storageConfig, + $mountType, + $applicable, + $storageOptions + ) { $storageConfig->setBackendClass($storageOptions['class']); $storageConfig->setBackendOptions($storageOptions['options']); if (isset($storageOptions['mountOptions'])) { @@ -225,9 +231,10 @@ abstract class StoragesService { /** * Get a storage with status * - * @param int $id + * @param int $id storage id * * @return StorageConfig + * @throws NotFoundException if the storage with the given id was not found */ public function getStorage($id) { $allStorages = $this->readConfig(); @@ -319,7 +326,7 @@ abstract class StoragesService { * @param StorageConfig $updatedStorage storage attributes * * @return StorageConfig storage config - * @throws NotFoundException + * @throws NotFoundException if the given storage does not exist in the config */ public function updateStorage(StorageConfig $updatedStorage) { $allStorages = $this->readConfig(); @@ -344,7 +351,7 @@ abstract class StoragesService { * * @param int $id storage id * - * @throws NotFoundException + * @throws NotFoundException if no storage was found with the given id */ public function removeStorage($id) { $allStorages = $this->readConfig(); @@ -376,7 +383,7 @@ abstract class StoragesService { // but so did the mount.json. This horribly hack // will disappear once we move to DB tables to // store the config - return max(array_keys($allStorages)) + 1; + return (max(array_keys($allStorages)) + 1); } } diff --git a/apps/files_external/service/userstoragesservice.php b/apps/files_external/service/userstoragesservice.php index fcf579c5d4..df452a4812 100644 --- a/apps/files_external/service/userstoragesservice.php +++ b/apps/files_external/service/userstoragesservice.php @@ -19,11 +19,19 @@ use \OCA\Files_external\NotFoundException; * (aka personal storages) */ class UserStoragesService extends StoragesService { + /** + * User session + * * @var IUserSession */ private $userSession; + /** + * Create a user storages service + * + * @param IUserSession $userSession user session + */ public function __construct( IUserSession $userSession ) { From c37913b1d506c0a4afe035de0e4c4fd5a80bb6ea Mon Sep 17 00:00:00 2001 From: Robin McCorkell Date: Fri, 20 Mar 2015 09:48:14 +0000 Subject: [PATCH 9/9] Introduce async status checking --- apps/files_external/js/settings.js | 6 +++++- apps/files_external/lib/config.php | 9 +++------ apps/files_external/templates/settings.php | 4 +--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/files_external/js/settings.js b/apps/files_external/js/settings.js index ef64d08788..9e4d026980 100644 --- a/apps/files_external/js/settings.js +++ b/apps/files_external/js/settings.js @@ -437,6 +437,7 @@ MountConfigListView.prototype = { * @param {int} [options.userListLimit] page size in applicable users dropdown */ initialize: function($el, options) { + var self = this; this.$el = $el; this._isPersonal = ($el.data('admin') !== true); if (this._isPersonal) { @@ -474,6 +475,10 @@ MountConfigListView.prototype = { addSelect2(this.$el.find('tr:not(#addMountPoint) .applicableUsers'), this._userListLimit); + this.$el.find('tr:not(#addMountPoint)').each(function(i, tr) { + self.recheckStorageConfig($(tr)); + }); + this._initEvents(); }, @@ -537,7 +542,6 @@ MountConfigListView.prototype = { $tr.find('.mountPoint input').val(this._suggestMountPoint(selected)); } $tr.addClass(backendClass); - $tr.find('.status').append(''); $tr.find('.backend').data('class', backendClass); var configurations = this._allBackends; var $td = $tr.find('td.configuration'); diff --git a/apps/files_external/lib/config.php b/apps/files_external/lib/config.php index 884e596bdd..85a93a779a 100644 --- a/apps/files_external/lib/config.php +++ b/apps/files_external/lib/config.php @@ -352,8 +352,7 @@ class OC_Mount_Config { 'backend' => $backends[$mount['class']]['backend'], 'priority' => $mount['priority'], 'options' => $mount['options'], - 'applicable' => array('groups' => array($group), 'users' => array()), - 'status' => self::getBackendStatus($mount['class'], $mount['options'], false) + 'applicable' => array('groups' => array($group), 'users' => array()) ); if (isset($mount['id'])) { $config['id'] = (int)$mount['id']; @@ -395,8 +394,7 @@ class OC_Mount_Config { 'backend' => $backends[$mount['class']]['backend'], 'priority' => $mount['priority'], 'options' => $mount['options'], - 'applicable' => array('groups' => array(), 'users' => array($user)), - 'status' => self::getBackendStatus($mount['class'], $mount['options'], false) + 'applicable' => array('groups' => array(), 'users' => array($user)) ); if (isset($mount['id'])) { $config['id'] = (int)$mount['id']; @@ -445,8 +443,7 @@ class OC_Mount_Config { // Remove '/uid/files/' from mount point 'mountpoint' => substr($mountPoint, strlen($uid) + 8), 'backend' => $backEnds[$mount['class']]['backend'], - 'options' => $mount['options'], - 'status' => self::getBackendStatus($mount['class'], $mount['options'], true) + 'options' => $mount['options'] ); if (isset($mount['id'])) { $config['id'] = (int)$mount['id']; diff --git a/apps/files_external/templates/settings.php b/apps/files_external/templates/settings.php index 3368b96a12..4866d17763 100644 --- a/apps/files_external/templates/settings.php +++ b/apps/files_external/templates/settings.php @@ -17,9 +17,7 @@ data-id=""> - - - +