]> source.dussan.org Git - nextcloud-server.git/commitdiff
Use storage id + appframework for ext storage CRUD
authorVincent Petry <pvince81@owncloud.com>
Fri, 31 Oct 2014 10:41:07 +0000 (11:41 +0100)
committerVincent Petry <pvince81@owncloud.com>
Thu, 12 Mar 2015 17:51:02 +0000 (18:51 +0100)
- 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

29 files changed:
apps/files_external/ajax/addMountPoint.php [deleted file]
apps/files_external/ajax/removeMountPoint.php [deleted file]
apps/files_external/appinfo/app.php
apps/files_external/appinfo/application.php
apps/files_external/appinfo/routes.php
apps/files_external/controller/globalstoragescontroller.php [new file with mode: 0644]
apps/files_external/controller/storagescontroller.php [new file with mode: 0644]
apps/files_external/controller/userstoragescontroller.php [new file with mode: 0644]
apps/files_external/js/dropbox.js
apps/files_external/js/google.js
apps/files_external/js/settings.js
apps/files_external/js/sftp_key.js
apps/files_external/lib/config.php
apps/files_external/lib/notfoundexception.php [new file with mode: 0644]
apps/files_external/lib/storageconfig.php [new file with mode: 0644]
apps/files_external/service/globalstoragesservice.php [new file with mode: 0644]
apps/files_external/service/storagesservice.php [new file with mode: 0644]
apps/files_external/service/userstoragesservice.php [new file with mode: 0644]
apps/files_external/templates/settings.php
apps/files_external/tests/controller/globalstoragescontrollertest.php [new file with mode: 0644]
apps/files_external/tests/controller/storagescontrollertest.php [new file with mode: 0644]
apps/files_external/tests/controller/userstoragescontrollertest.php [new file with mode: 0644]
apps/files_external/tests/js/settingsSpec.js [new file with mode: 0644]
apps/files_external/tests/service/globalstoragesservicetest.php [new file with mode: 0644]
apps/files_external/tests/service/storagesservicetest.php [new file with mode: 0644]
apps/files_external/tests/service/userstoragesservicetest.php [new file with mode: 0644]
apps/files_external/tests/storageconfigtest.php [new file with mode: 0644]
core/js/tests/specHelper.js
tests/karma.config.js

diff --git a/apps/files_external/ajax/addMountPoint.php b/apps/files_external/ajax/addMountPoint.php
deleted file mode 100644 (file)
index 4e27ef9..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-OCP\JSON::checkAppEnabled('files_external');
-OCP\JSON::callCheck();
-
-if ($_POST['isPersonal'] == 'true') {
-       OCP\JSON::checkLoggedIn();
-       $isPersonal = true;
-} else {
-       OCP\JSON::checkAdminUser();
-       $isPersonal = false;
-}
-
-$mountPoint = (string)$_POST['mountPoint'];
-$oldMountPoint = (string)$_POST['oldMountPoint'];
-$class = (string)$_POST['class'];
-$options = (array)$_POST['classOptions'];
-$type = (string)$_POST['mountType'];
-$applicable = (string)$_POST['applicable'];
-
-if ($oldMountPoint and $oldMountPoint !== $mountPoint) {
-       OC_Mount_Config::removeMountPoint($oldMountPoint, $type, $applicable, $isPersonal);
-}
-
-$status = OC_Mount_Config::addMountPoint($mountPoint, $class, $options, $type, $applicable, $isPersonal);
-OCP\JSON::success(array('data' => array('message' => $status)));
diff --git a/apps/files_external/ajax/removeMountPoint.php b/apps/files_external/ajax/removeMountPoint.php
deleted file mode 100644 (file)
index 0870911..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-OCP\JSON::checkAppEnabled('files_external');
-OCP\JSON::callCheck();
-
-if (!isset($_POST['isPersonal']))
-       return;
-if (!isset($_POST['mountPoint']))
-       return;
-if (!isset($_POST['mountType']))
-       return;
-if (!isset($_POST['applicable']))
-       return;
-
-if ($_POST['isPersonal'] == 'true') {
-       OCP\JSON::checkLoggedIn();
-       $isPersonal = true;
-} else {
-       OCP\JSON::checkAdminUser();
-       $isPersonal = false;
-}
-
-OC_Mount_Config::removeMountPoint((string)$_POST['mountPoint'], (string)$_POST['mountType'], (string)$_POST['applicable'], $isPersonal);
index e74ce3594c19640f515bb9c2f251224556be18f6..7ea1e96bf2fb6cb826bf92b02af0802fbcd9845e 100644 (file)
@@ -6,6 +6,8 @@
  * later.
  * See the COPYING-README file.
  */
+$app = new \OCA\Files_external\Appinfo\Application();
+
 $l = \OC::$server->getL10N('files_external');
 
 OC::$CLASSPATH['OC\Files\Storage\StreamWrapper'] = 'files_external/lib/streamwrapper.php';
index b1605bb98a87adaf691d9afbb98c0f02178afa7c..3e6b80ccb48a62dce7abf781d7a05cd3919d4b86 100644 (file)
@@ -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();
 
                /**
index 5c7c4eca9099b49ea76cec713f410fa5d63e40e9..506c9d34e26f9599a9fd2fccc4ac534f43171344 100644 (file)
 
 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 (file)
index 0000000..3aa64f0
--- /dev/null
@@ -0,0 +1,145 @@
+<?php
+/**
+ * ownCloud - files_external
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later. See the COPYING file.
+ *
+ * @author Vincent Petry <pvince81@owncloud.com>
+ * @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 (file)
index 0000000..f047ba3
--- /dev/null
@@ -0,0 +1,157 @@
+<?php
+/**
+ * ownCloud - files_external
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later. See the COPYING file.
+ *
+ * @author Vincent Petry <pvince81@owncloud.com>
+ * @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 (file)
index 0000000..b77cbca
--- /dev/null
@@ -0,0 +1,172 @@
+<?php
+/**
+ * ownCloud - files_external
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later. See the COPYING file.
+ *
+ * @author Vincent Petry <pvince81@owncloud.com>
+ * @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);
+       }
+}
+
index 2880e910f2c68574fbdc75ccf1491ce9cd2481ed..53b5d5d666fb3760742a1d04144e7d8769d5ac29 100644 (file)
@@ -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('<span id="access" style="padding-left:0.5em;">'+t('files_external', 'Access granted')+'</span>');
@@ -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 {
index b9a5e66b800bbf596185b7e31d8ff20ae40f4c83..648538f80287715c7054d92ea4543a0c96ce5846 100644 (file)
@@ -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($('<span/>')
@@ -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 {
index ee3d0b736da01f2863e0b2c93b9c96986e43b9cc..b3567b7ebf53d8507da23a0fb8d3ef144c0e728e 100644 (file)
@@ -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<Object>} 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 = $('<span><div class="avatardiv"/><span>'+escapeHTML(element.displayname)+'</span></span>');
+                       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('<img width="32" height="32" src="'+url+'">');
+                       }
+                       return $result.get(0).outerHTML;
+               },
+               formatSelection: function (element) {
+                       if (element.type === 'group') {
+                               return '<span title="'+escapeHTML(element.name)+'" class="group">'+escapeHTML(element.displayname+' '+t('files_external', '(group)'))+'</span>';
                        } 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 '<span title="'+escapeHTML(element.name)+'" class="user">'+escapeHTML(element.displayname)+'</span>';
                        }
-                       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.<string,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 = $('<span><div class="avatardiv"/><span>'+escapeHTML(element.displayname)+'</span></span>');
-                                       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('<img width="32" height="32" src="'+url+'">');
-                                       }
-                                       return $result.get(0).outerHTML;
-                               },
-                               formatSelection: function (element) {
-                                       if (element.type === 'group') {
-                                               return '<span title="'+escapeHTML(element.name)+'" class="group">'+escapeHTML(element.displayname+' '+t('files_external', '(group)'))+'</span>';
-                                       } else {
-                                               return '<span title="'+escapeHTML(element.name)+'" class="user">'+escapeHTML(element.displayname)+'</span>';
-                                       }
-                               },
-                               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.<string>
+        */
+       applicableUsers: null,
+
+       /**
+        * Applicable groups
+        *
+        * @type Array.<string>
+        */
+       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.<string,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('<span></span>');
                $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;
+
 })();
index 2b39628247c207c54e3e95d99a9583a7c66909a0..55b11b1fac99f2473ff10a71df5bd4812ee93c38 100644 (file)
@@ -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 {
index ddfab43987922933e744984d5ca9834b8d2f2d96..deeedb9855177865b38f0bab6a8575d3c6b6591c 100644 (file)
@@ -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 (file)
index 0000000..d1d1530
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ * 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 (file)
index 0000000..f23b5cd
--- /dev/null
@@ -0,0 +1,243 @@
+<?php
+/**
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ * 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 (file)
index 0000000..257c9bd
--- /dev/null
@@ -0,0 +1,192 @@
+<?php
+/**
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ * 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 (file)
index 0000000..52188b2
--- /dev/null
@@ -0,0 +1,303 @@
+<?php
+/**
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ * 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 (file)
index 0000000..fcf579c
--- /dev/null
@@ -0,0 +1,142 @@
+<?php
+/**
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ * 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);
+               }
+       }
+}
index 79950f30385e09ef413676d28f385597b3cda076..5f7d7cff75222e3c5655f8feba9d39d93f355e7c 100644 (file)
                        </tr>
                </thead>
                <tbody>
-               <?php $_['mounts'] = array_merge($_['mounts'], array('' => array())); ?>
+               <?php $_['mounts'] = array_merge($_['mounts'], array('' => array('id' => ''))); ?>
                <?php foreach ($_['mounts'] as $mount): ?>
-                       <tr <?php print_unescaped(isset($mount['mountpoint']) ? 'class="'.OC_Util::sanitizeHTML($mount['class']).'"' : 'id="addMountPoint"'); ?>>
+                       <tr <?php print_unescaped(isset($mount['mountpoint']) ? 'class="'.OC_Util::sanitizeHTML($mount['class']).'"' : 'id="addMountPoint"'); ?> data-id="<?php p($mount['id']) ?>">
                                <td class="status">
                                <?php if (isset($mount['status'])): ?>
-                                       <span class="<?php p(($mount['status']) ? 'success' : 'error'); ?>"></span>
+                                       <span class="<?php p(($mount['status'] === \OC_Mount_Config::STATUS_SUCCESS) ? 'success' : 'error'); ?>"></span>
                                <?php endif; ?>
                                </td>
                                <td class="mountPoint"><input type="text" name="mountPoint"
@@ -28,7 +28,7 @@
                                </td>
                                <?php if (!isset($mount['mountpoint'])): ?>
                                        <td class="backend">
-                                               <select id="selectBackend" data-configurations='<?php p(json_encode($_['backends'])); ?>'>
+                                               <select id="selectBackend" class="selectBackend" data-configurations='<?php p(json_encode($_['backends'])); ?>'>
                                                        <option value="" disabled selected
                                                                        style="display:none;"><?php p($l->t('Add storage')); ?></option>
                                                        <?php foreach ($_['backends'] as $class => $backend): ?>
diff --git a/apps/files_external/tests/controller/globalstoragescontrollertest.php b/apps/files_external/tests/controller/globalstoragescontrollertest.php
new file mode 100644 (file)
index 0000000..7ba4d16
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+/**
+ * ownCloud
+ *
+ * @author Vincent Petry
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+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 (file)
index 0000000..fefe292
--- /dev/null
@@ -0,0 +1,217 @@
+<?php
+/**
+ * ownCloud
+ *
+ * @author Vincent Petry
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+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 (file)
index 0000000..9d6fbb1
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+/**
+ * ownCloud
+ *
+ * @author Vincent Petry
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+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 (file)
index 0000000..350840e
--- /dev/null
@@ -0,0 +1,164 @@
+/*
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ *
+ * 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(
+                       '<table id="externalStorage" data-admin="true">' +
+                       '<thead></thead>' +
+                       '<tbody>' +
+                       '<tr id="addMountPoint" data-id="">' +
+                       '<td class="status"></td>' +
+                       '<td class="mountPoint"><input type="text" name="mountPoint"/></td>' +
+                       '<td class="backend">' +
+                       '<select class="selectBackend">' +
+                       '<option disable selected>Add storage</option>' +
+                       '<option value="\\OC\\TestBackend">Test Backend</option>' +
+                       '<option value="\\OC\\AnotherTestBackend">Another Test Backend</option>' +
+                       '</select>' +
+                       '</td>' +
+                       '<td class="configuration"></td>' +
+                       '<td class="applicable">' +
+                       '<input type="hidden" class="applicableUsers">' +
+                       '</td>' +
+                       '<td><img alt="Delete" title="Delete" class="svg action"/></td>' +
+                       '</tr>' +
+                       '</tbody>' +
+                       '</table>'
+               );
+               // 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 (file)
index 0000000..6286865
--- /dev/null
@@ -0,0 +1,705 @@
+<?php
+/**
+ * ownCloud
+ *
+ * @author Vincent Petry
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+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 (file)
index 0000000..1e338b3
--- /dev/null
@@ -0,0 +1,180 @@
+<?php
+/**
+ * ownCloud
+ *
+ * @author Vincent Petry
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+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 (file)
index 0000000..64d59dc
--- /dev/null
@@ -0,0 +1,200 @@
+<?php
+/**
+ * ownCloud
+ *
+ * @author Vincent Petry
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+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 (file)
index 0000000..473dc20
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+/**
+ * ownCloud
+ *
+ * @author Vincent Petry
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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']);
+       }
+
+}
index 59c2a99645f2815e1929879d3c9b6a450679b367..29293e89bcb7030d7d98bee8212c7771daad6328 100644 (file)
@@ -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);
        });
 })();
 
index e5febb15aaabddae860c2832065e11bff35df3e5..997da4bcb26e67c8bce572c8d9e3eb697b6b2c5a 100644 (file)
@@ -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']
                        },