From: Vincent Petry Date: Fri, 31 Oct 2014 10:41:07 +0000 (+0100) Subject: Use storage id + appframework for ext storage CRUD X-Git-Tag: v8.1.0alpha1~233^2~8 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=ce94a998dd5a5801beef7874dd13752095e35de0;p=nextcloud-server.git 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 --- diff --git a/apps/files_external/ajax/addMountPoint.php b/apps/files_external/ajax/addMountPoint.php deleted file mode 100644 index 4e27ef98756..00000000000 --- 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 0870911544b..00000000000 --- 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 b1605bb98a8..3e6b80ccb48 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 5c7c4eca909..506c9d34e26 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 00000000000..3aa64f0d85d --- /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 00000000000..f047ba34b50 --- /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 00000000000..b77cbca59fd --- /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 2880e910f2c..53b5d5d666f 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 b9a5e66b800..648538f8028 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 ee3d0b736da..b3567b7ebf5 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 } - } else { - classOptions[$(input).data('parameter')] = $(input).val(); } - }); - if ($('#externalStorage').data('admin') === true) { - var multiselect = getSelection($tr); - } - 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 + }, + 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 { - 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); - } - } - }); + return ''+escapeHTML(element.displayname)+''; } - return status; - } - } + }, + 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); + } + }); + }); +} + +/** + * @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, -$(document).ready(function() { - var $externalStorage = $('#externalStorage'); - - //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(',')); + /** + * 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}); + } + + $.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; } - }); + $.ajax({ + type: 'GET', + url: OC.generateUrl(this._url + '/{id}', {id: this.id}), + success: options.success, + error: options.error + }); + }, - 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') { - - 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]); - } + /** + * 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 + }); + }, - $.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); - } - }) - }); + /** + * 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; } - 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.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', + + /** + * Applicable users + * + * @type Array. + */ + applicableUsers: null, + + /** + * Applicable groups + * + * @type Array. + */ + applicableGroups: null, + + /** + * 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, + }); + } +}); + +/** + * @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); + }, + + /** + * 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; + }, - function suggestMountPoint(defaultMountPoint) { + /** + * 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 2b39628247c..55b11b1fac9 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 ddfab439879..deeedb98551 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 00000000000..d1d15309d5b --- /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 00000000000..f23b5cd86a9 --- /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 00000000000..257c9bd4679 --- /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 00000000000..52188b23a39 --- /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 00000000000..fcf579c5d43 --- /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 79950f30385..5f7d7cff752 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 00000000000..7ba4d16a7e9 --- /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 00000000000..fefe2928d76 --- /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 00000000000..9d6fbb15e23 --- /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 00000000000..350840e542c --- /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 00000000000..6286865bf43 --- /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 00000000000..1e338b3948d --- /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 00000000000..64d59dc7d03 --- /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 00000000000..473dc20b387 --- /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 59c2a99645f..29293e89bcb 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 e5febb15aaa..997da4bcb26 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'] },