summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/dav/lib/Connector/PublicAuth.php2
-rw-r--r--apps/dav/lib/Server.php5
-rw-r--r--apps/files_external/appinfo/register_command.php3
-rw-r--r--apps/files_external/lib/Command/Notify.php173
-rw-r--r--apps/files_external/lib/Config/ConfigAdapter.php3
-rw-r--r--apps/files_external/lib/Lib/Storage/SFTP.php2
-rw-r--r--apps/files_external/lib/Lib/Storage/SMB.php59
-rwxr-xr-xapps/files_external/tests/env/start-swift-ceph.sh2
-rw-r--r--apps/files_sharing/css/sharetabview.css9
-rw-r--r--bower.json3
-rw-r--r--core/Command/Group/AddUser.php77
-rw-r--r--core/Command/Group/ListCommand.php89
-rw-r--r--core/Command/Group/RemoveUser.php77
-rw-r--r--core/Command/User/Info.php88
-rw-r--r--core/Command/User/ListCommand.php87
-rw-r--r--core/css/icons.css8
-rw-r--r--core/img/actions/clippy.svg3
-rw-r--r--core/js/core.json3
-rw-r--r--core/js/sharedialoglinkshareview.js35
-rw-r--r--core/js/sharedialogmailview.js8
-rw-r--r--core/js/sharedialogshareelistview.js5
-rw-r--r--core/js/sharedialogview.js2
-rw-r--r--core/js/tests/specs/sharedialogviewSpec.js4
-rw-r--r--core/register_command.php6
-rw-r--r--core/vendor/.gitignore4
-rw-r--r--core/vendor/clipboard/dist/clipboard.js742
-rw-r--r--db_structure.xml14
-rw-r--r--lib/private/AppFramework/DependencyInjection/DIContainer.php8
-rw-r--r--lib/private/AppFramework/Middleware/OCSMiddleware.php80
-rw-r--r--lib/private/Files/Config/CachedMountInfo.php19
-rw-r--r--lib/private/Files/Config/LazyStorageMountInfo.php4
-rw-r--r--lib/private/Files/Config/UserMountCache.php44
-rw-r--r--lib/private/Files/Mount/MountPoint.php12
-rw-r--r--lib/private/legacy/app.php7
-rw-r--r--lib/private/legacy/files.php2
-rw-r--r--lib/public/API.php2
-rw-r--r--lib/public/AppFramework/OCS/OCSBadRequestException.php45
-rw-r--r--lib/public/AppFramework/OCS/OCSException.php32
-rw-r--r--lib/public/AppFramework/OCS/OCSForbiddenException.php44
-rw-r--r--lib/public/AppFramework/OCS/OCSNotFoundException.php44
-rw-r--r--lib/public/Files/Config/ICachedMountInfo.php8
-rw-r--r--lib/public/Files/Mount/IMountPoint.php8
-rw-r--r--lib/public/Files/Storage/INotifyStorage.php51
-rw-r--r--settings/css/settings.css7
-rw-r--r--settings/js/authtoken_collection.js2
-rw-r--r--settings/js/authtoken_view.js12
-rw-r--r--settings/templates/personal.php4
-rw-r--r--tests/lib/AppFramework/Middleware/OCSMiddlewareTest.php108
-rw-r--r--tests/lib/Files/Config/UserMountCacheTest.php27
-rw-r--r--tests/lib/Files/Storage/Storage.php26
-rw-r--r--version.php2
51 files changed, 2059 insertions, 52 deletions
diff --git a/apps/dav/lib/Connector/PublicAuth.php b/apps/dav/lib/Connector/PublicAuth.php
index 9b386c7609d..38d91f086c7 100644
--- a/apps/dav/lib/Connector/PublicAuth.php
+++ b/apps/dav/lib/Connector/PublicAuth.php
@@ -98,7 +98,7 @@ class PublicAuth extends AbstractBasic {
if ($this->shareManager->checkPassword($share, $password)) {
return true;
} else if ($this->session->exists('public_link_authenticated')
- && $this->session->get('public_link_authenticated') === $share->getId()) {
+ && $this->session->get('public_link_authenticated') === (string)$share->getId()) {
return true;
} else {
if (in_array('XMLHttpRequest', explode(',', $this->request->getHeader('X-Requested-With')))) {
diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php
index 0715d39049c..73a07072d3c 100644
--- a/apps/dav/lib/Server.php
+++ b/apps/dav/lib/Server.php
@@ -72,13 +72,16 @@ class Server {
$this->server->setBaseUri($this->baseUri);
$this->server->addPlugin(new BlockLegacyClientPlugin(\OC::$server->getConfig()));
- $authPlugin = new Plugin($authBackend, 'ownCloud');
+ $authPlugin = new Plugin();
$this->server->addPlugin($authPlugin);
// allow setup of additional auth backends
$event = new SabrePluginEvent($this->server);
$dispatcher->dispatch('OCA\DAV\Connector\Sabre::authInit', $event);
+ // because we are throwing exceptions this plugin has to be the last one
+ $authPlugin->addBackend($authBackend);
+
// debugging
if(\OC::$server->getConfig()->getSystemValue('debug', false)) {
$this->server->addPlugin(new \Sabre\DAV\Browser\Plugin());
diff --git a/apps/files_external/appinfo/register_command.php b/apps/files_external/appinfo/register_command.php
index a5163f96d54..629970722de 100644
--- a/apps/files_external/appinfo/register_command.php
+++ b/apps/files_external/appinfo/register_command.php
@@ -31,6 +31,7 @@ use OCA\Files_External\Command\Delete;
use OCA\Files_External\Command\Create;
use OCA\Files_External\Command\Backends;
use OCA\Files_External\Command\Verify;
+use OCA\Files_External\Command\Notify;
$userManager = OC::$server->getUserManager();
$userSession = OC::$server->getUserSession();
@@ -42,6 +43,7 @@ $globalStorageService = $app->getContainer()->query('\OCA\Files_External\Service
$userStorageService = $app->getContainer()->query('\OCA\Files_External\Service\UserStoragesService');
$importLegacyStorageService = $app->getContainer()->query('\OCA\Files_External\Service\ImportLegacyStoragesService');
$backendService = $app->getContainer()->query('OCA\Files_External\Service\BackendService');
+$connection = $app->getContainer()->getServer()->getDatabaseConnection();
/** @var Symfony\Component\Console\Application $application */
$application->add(new ListCommand($globalStorageService, $userStorageService, $userSession, $userManager));
@@ -54,3 +56,4 @@ $application->add(new Delete($globalStorageService, $userStorageService, $userSe
$application->add(new Create($globalStorageService, $userStorageService, $userManager, $userSession, $backendService));
$application->add(new Backends($backendService));
$application->add(new Verify($globalStorageService));
+$application->add(new Notify($globalStorageService, $connection));
diff --git a/apps/files_external/lib/Command/Notify.php b/apps/files_external/lib/Command/Notify.php
new file mode 100644
index 00000000000..63d027b6778
--- /dev/null
+++ b/apps/files_external/lib/Command/Notify.php
@@ -0,0 +1,173 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Files_External\Command;
+
+use OC\Core\Command\Base;
+use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\Service\GlobalStoragesService;
+use OCP\Files\Storage\INotifyStorage;
+use OCP\Files\StorageNotAvailableException;
+use OCP\IDBConnection;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Notify extends Base {
+ /** @var GlobalStoragesService */
+ private $globalService;
+ /** @var IDBConnection */
+ private $connection;
+ /** @var \OCP\DB\QueryBuilder\IQueryBuilder */
+ private $updateQuery;
+
+ function __construct(GlobalStoragesService $globalService, IDBConnection $connection) {
+ parent::__construct();
+ $this->globalService = $globalService;
+ $this->connection = $connection;
+ // the query builder doesn't really like subqueries with parameters
+ $this->updateQuery = $this->connection->prepare(
+ 'UPDATE *PREFIX*filecache SET size = -1
+ WHERE `path` = ?
+ AND `storage` IN (SELECT storage_id FROM *PREFIX*mounts WHERE mount_id = ?)'
+ );
+ }
+
+ protected function configure() {
+ $this
+ ->setName('files_external:notify')
+ ->setDescription('Listen for active update notifications for a configured external mount')
+ ->addArgument(
+ 'mount_id',
+ InputArgument::REQUIRED,
+ 'the mount id of the mount to listen to'
+ )->addOption(
+ 'user',
+ 'u',
+ InputOption::VALUE_REQUIRED,
+ 'The username for the remote mount (required only for some mount configuration that don\'t store credentials)'
+ )->addOption(
+ 'password',
+ 'p',
+ InputOption::VALUE_REQUIRED,
+ 'The password for the remote mount (required only for some mount configuration that don\'t store credentials)'
+ )->addOption(
+ 'path',
+ null,
+ InputOption::VALUE_REQUIRED,
+ 'The directory in the storage to listen for updates in',
+ '/'
+ );
+ parent::configure();
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $mount = $this->globalService->getStorage($input->getArgument('mount_id'));
+ if (is_null($mount)) {
+ $output->writeln('<error>Mount not found</error>');
+ return 1;
+ }
+ $noAuth = false;
+ try {
+ $authBackend = $mount->getAuthMechanism();
+ $authBackend->manipulateStorageConfig($mount);
+ } catch (InsufficientDataForMeaningfulAnswerException $e) {
+ $noAuth = true;
+ } catch (StorageNotAvailableException $e) {
+ $noAuth = true;
+ }
+
+ if ($input->getOption('user')) {
+ $mount->setBackendOption('user', $input->getOption('user'));
+ }
+ if ($input->getOption('password')) {
+ $mount->setBackendOption('password', $input->getOption('password'));
+ }
+
+ try {
+ $storage = $this->createStorage($mount);
+ } catch (\Exception $e) {
+ $output->writeln('<error>Error while trying to create storage</error>');
+ if ($noAuth) {
+ $output->writeln('<error>Username and/or password required</error>');
+ }
+ return 1;
+ }
+ if (!$storage instanceof INotifyStorage) {
+ $output->writeln('<error>Mount of type "' . $mount->getBackend()->getText() . '" does not support active update notifications</error>');
+ return 1;
+ }
+
+ $verbose = $input->getOption('verbose');
+
+ $path = trim($input->getOption('path'), '/');
+ $storage->listen($path, function ($type, $path, $renameTarget) use ($mount, $verbose, $output) {
+ if ($verbose) {
+ $this->logUpdate($type, $path, $renameTarget, $output);
+ }
+ if ($type == INotifyStorage::NOTIFY_RENAMED) {
+ $this->markParentAsOutdated($mount->getId(), $renameTarget);
+ }
+ $this->markParentAsOutdated($mount->getId(), $path);
+ });
+ }
+
+ private function createStorage(StorageConfig $mount) {
+ $class = $mount->getBackend()->getStorageClass();
+ return new $class($mount->getBackendOptions());
+ }
+
+ private function markParentAsOutdated($mountId, $path) {
+ $parent = dirname($path);
+ if ($parent === '.') {
+ $parent = '';
+ }
+ $this->updateQuery->execute([$parent, $mountId]);
+ }
+
+ private function logUpdate($type, $path, $renameTarget, OutputInterface $output) {
+ switch ($type) {
+ case INotifyStorage::NOTIFY_ADDED:
+ $text = 'added';
+ break;
+ case INotifyStorage::NOTIFY_MODIFIED:
+ $text = 'modified';
+ break;
+ case INotifyStorage::NOTIFY_REMOVED:
+ $text = 'removed';
+ break;
+ case INotifyStorage::NOTIFY_RENAMED:
+ $text = 'renamed';
+ break;
+ default:
+ return;
+ }
+
+ $text .= ' ' . $path;
+ if ($type === INotifyStorage::NOTIFY_RENAMED) {
+ $text .= ' to ' . $renameTarget;
+ }
+
+ $output->writeln($text);
+ }
+}
diff --git a/apps/files_external/lib/Config/ConfigAdapter.php b/apps/files_external/lib/Config/ConfigAdapter.php
index 48a521a76f3..9375ff74c56 100644
--- a/apps/files_external/lib/Config/ConfigAdapter.php
+++ b/apps/files_external/lib/Config/ConfigAdapter.php
@@ -151,7 +151,8 @@ class ConfigAdapter implements IMountProvider {
'/' . $user->getUID() . '/files' . $storage->getMountPoint(),
null,
$loader,
- $storage->getMountOptions()
+ $storage->getMountOptions(),
+ $storage->getId()
);
$mounts[$storage->getMountPoint()] = $mount;
}
diff --git a/apps/files_external/lib/Lib/Storage/SFTP.php b/apps/files_external/lib/Lib/Storage/SFTP.php
index 3be7a801229..4d4fe6945aa 100644
--- a/apps/files_external/lib/Lib/Storage/SFTP.php
+++ b/apps/files_external/lib/Lib/Storage/SFTP.php
@@ -426,7 +426,7 @@ class SFTP extends \OC\Files\Storage\Common {
*/
public function rename($source, $target) {
try {
- if (!$this->is_dir($target) && $this->file_exists($target)) {
+ if ($this->file_exists($target)) {
$this->unlink($target);
}
return $this->getConnection()->rename(
diff --git a/apps/files_external/lib/Lib/Storage/SMB.php b/apps/files_external/lib/Lib/Storage/SMB.php
index e677f8c9eba..6a24c6b17d9 100644
--- a/apps/files_external/lib/Lib/Storage/SMB.php
+++ b/apps/files_external/lib/Lib/Storage/SMB.php
@@ -34,15 +34,18 @@ use Icewind\SMB\Exception\ConnectException;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\ForbiddenException;
use Icewind\SMB\Exception\NotFoundException;
+use Icewind\SMB\IShare;
use Icewind\SMB\NativeServer;
use Icewind\SMB\Server;
use Icewind\Streams\CallbackWrapper;
use Icewind\Streams\IteratorDirectory;
use OC\Cache\CappedMemoryCache;
use OC\Files\Filesystem;
+use OC\Files\Storage\Common;
+use OCP\Files\Storage\INotifyStorage;
use OCP\Files\StorageNotAvailableException;
-class SMB extends \OC\Files\Storage\Common {
+class SMB extends Common implements INotifyStorage {
/**
* @var \Icewind\SMB\Server
*/
@@ -103,6 +106,16 @@ class SMB extends \OC\Files\Storage\Common {
return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
}
+ protected function relativePath($fullPath) {
+ if ($fullPath === $this->root) {
+ return '';
+ } else if (substr($fullPath, 0, strlen($this->root)) === $this->root) {
+ return substr($fullPath, strlen($this->root));
+ } else {
+ return null;
+ }
+ }
+
/**
* @param string $path
* @return \Icewind\SMB\IFileInfo
@@ -413,4 +426,48 @@ class SMB extends \OC\Files\Storage\Common {
return false;
}
}
+
+ public function listen($path, callable $callback) {
+ $fullPath = $this->buildPath($path);
+ $oldRenamePath = null;
+ $this->share->notify($fullPath, function ($smbType, $fullPath) use (&$oldRenamePath, $callback) {
+ $path = $this->relativePath($fullPath);
+ if (is_null($path)) {
+ return true;
+ }
+ if ($smbType === IShare::NOTIFY_RENAMED_OLD) {
+ $oldRenamePath = $path;
+ return true;
+ }
+ $type = $this->mapNotifyType($smbType);
+ if (is_null($type)) {
+ return true;
+ }
+ if ($type === INotifyStorage::NOTIFY_RENAMED && !is_null($oldRenamePath)) {
+ $result = $callback($type, $path, $oldRenamePath);
+ $oldRenamePath = null;
+ } else {
+ $result = $callback($type, $path);
+ }
+ return $result;
+ });
+ }
+
+ private function mapNotifyType($smbType) {
+ switch ($smbType) {
+ case IShare::NOTIFY_ADDED:
+ return INotifyStorage::NOTIFY_ADDED;
+ case IShare::NOTIFY_REMOVED:
+ return INotifyStorage::NOTIFY_REMOVED;
+ case IShare::NOTIFY_MODIFIED:
+ case IShare::NOTIFY_ADDED_STREAM:
+ case IShare::NOTIFY_MODIFIED_STREAM:
+ case IShare::NOTIFY_REMOVED_STREAM:
+ return INotifyStorage::NOTIFY_MODIFIED;
+ case IShare::NOTIFY_RENAMED_NEW:
+ return INotifyStorage::NOTIFY_RENAMED;
+ default:
+ return null;
+ }
+ }
}
diff --git a/apps/files_external/tests/env/start-swift-ceph.sh b/apps/files_external/tests/env/start-swift-ceph.sh
index b73fa899a6d..3a299a6fa85 100755
--- a/apps/files_external/tests/env/start-swift-ceph.sh
+++ b/apps/files_external/tests/env/start-swift-ceph.sh
@@ -80,7 +80,7 @@ if ! "$thisFolder"/env/wait-for-connection ${host} 80 600; then
exit 1
fi
echo "Waiting another 15 seconds"
-sleep 15
+sleep 15
cat > $thisFolder/config.swift.php <<DELIM
<?php
diff --git a/apps/files_sharing/css/sharetabview.css b/apps/files_sharing/css/sharetabview.css
index 04338820881..e048b7564ac 100644
--- a/apps/files_sharing/css/sharetabview.css
+++ b/apps/files_sharing/css/sharetabview.css
@@ -10,8 +10,9 @@
top: 2px;
}
-.shareTabView .shareWithRemoteInfo {
- padding: 11px 20px;
+.shareTabView .shareWithRemoteInfo,
+.shareTabView .clipboardButton {
+ padding-left: 10px;
}
.shareTabView label {
@@ -28,7 +29,9 @@
width: 94%;
margin-left: 0;
}
-.shareTabView input[type="text"].shareWithField {
+.shareTabView input[type="text"].shareWithField,
+.shareTabView input[type="text"].emailField,
+.shareTabView input[type="text"].linkText {
width: 80%;
}
diff --git a/bower.json b/bower.json
index 74c4be96a21..3aa2cd4b3b2 100644
--- a/bower.json
+++ b/bower.json
@@ -30,6 +30,7 @@
"backbone": "~1.2.3",
"davclient.js": "https://github.com/evert/davclient.js.git",
"es6-promise": "https://github.com/jakearchibald/es6-promise.git#~2.3.0",
- "base64": "~0.3.0"
+ "base64": "~0.3.0",
+ "clipboard": "^1.5.12"
}
}
diff --git a/core/Command/Group/AddUser.php b/core/Command/Group/AddUser.php
new file mode 100644
index 00000000000..23aa193fbc0
--- /dev/null
+++ b/core/Command/Group/AddUser.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Core\Command\Group;
+
+use OC\Core\Command\Base;
+use OCP\IGroupManager;
+use OCP\IUserManager;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class AddUser extends Base {
+ /** @var IUserManager */
+ protected $userManager;
+ /** @var IGroupManager */
+ protected $groupManager;
+
+ /**
+ * @param IUserManager $userManager
+ * @param IGroupManager $groupManager
+ */
+ public function __construct(IUserManager $userManager, IGroupManager $groupManager) {
+ $this->userManager = $userManager;
+ $this->groupManager = $groupManager;
+ parent::__construct();
+ }
+
+ protected function configure() {
+ $this
+ ->setName('group:adduser')
+ ->setDescription('add a user to a group')
+ ->addArgument(
+ 'group',
+ InputArgument::REQUIRED,
+ 'group to add the user to'
+ )->addArgument(
+ 'user',
+ InputArgument::REQUIRED,
+ 'user to add to the group'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $group = $this->groupManager->get($input->getArgument('group'));
+ if (is_null($group)) {
+ $output->writeln('<error>group not found</error>');
+ return 1;
+ }
+ $user = $this->userManager->get($input->getArgument('user'));
+ if (is_null($user)) {
+ $output->writeln('<error>user not found</error>');
+ return 1;
+ }
+ $group->addUser($user);
+ }
+}
diff --git a/core/Command/Group/ListCommand.php b/core/Command/Group/ListCommand.php
new file mode 100644
index 00000000000..d0c0adacd3a
--- /dev/null
+++ b/core/Command/Group/ListCommand.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Core\Command\Group;
+
+use OC\Core\Command\Base;
+use OCP\IGroup;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserManager;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ListCommand extends Base {
+ /** @var IGroupManager */
+ protected $groupManager;
+
+ /**
+ * @param IGroupManager $groupManager
+ */
+ public function __construct(IGroupManager $groupManager) {
+ $this->groupManager = $groupManager;
+ parent::__construct();
+ }
+
+ protected function configure() {
+ $this
+ ->setName('group:list')
+ ->setDescription('list configured groups')
+ ->addOption(
+ 'limit',
+ 'l',
+ InputOption::VALUE_OPTIONAL,
+ 'Number of groups to retrieve',
+ 500
+ )->addOption(
+ 'offset',
+ 'o',
+ InputOption::VALUE_OPTIONAL,
+ 'Offset for retrieving groups',
+ 0
+ )->addOption(
+ 'output',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'Output format (plain, json or json_pretty, default is plain)',
+ $this->defaultOutputFormat
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $groups = $this->groupManager->search('', (int)$input->getOption('limit'), (int)$input->getOption('offset'));
+ $this->writeArrayInOutputFormat($input, $output, $this->formatGroups($groups));
+ }
+
+ /**
+ * @param IGroup[] $groups
+ * @return array
+ */
+ private function formatGroups(array $groups) {
+ $keys = array_map(function (IGroup $group) {
+ return $group->getGID();
+ }, $groups);
+ $values = array_map(function (IGroup $group) {
+ return array_keys($group->getUsers());
+ }, $groups);
+ return array_combine($keys, $values);
+ }
+}
diff --git a/core/Command/Group/RemoveUser.php b/core/Command/Group/RemoveUser.php
new file mode 100644
index 00000000000..f579468a74d
--- /dev/null
+++ b/core/Command/Group/RemoveUser.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Core\Command\Group;
+
+use OC\Core\Command\Base;
+use OCP\IGroupManager;
+use OCP\IUserManager;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class RemoveUser extends Base {
+ /** @var IUserManager */
+ protected $userManager;
+ /** @var IGroupManager */
+ protected $groupManager;
+
+ /**
+ * @param IUserManager $userManager
+ * @param IGroupManager $groupManager
+ */
+ public function __construct(IUserManager $userManager, IGroupManager $groupManager) {
+ $this->userManager = $userManager;
+ $this->groupManager = $groupManager;
+ parent::__construct();
+ }
+
+ protected function configure() {
+ $this
+ ->setName('group:removeuser')
+ ->setDescription('remove a user from a group')
+ ->addArgument(
+ 'group',
+ InputArgument::REQUIRED,
+ 'group to remove the user from'
+ )->addArgument(
+ 'user',
+ InputArgument::REQUIRED,
+ 'user to remove from the group'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $group = $this->groupManager->get($input->getArgument('group'));
+ if (is_null($group)) {
+ $output->writeln('<error>group not found</error>');
+ return 1;
+ }
+ $user = $this->userManager->get($input->getArgument('user'));
+ if (is_null($user)) {
+ $output->writeln('<error>user not found</error>');
+ return 1;
+ }
+ $group->removeUser($user);
+ }
+}
diff --git a/core/Command/User/Info.php b/core/Command/User/Info.php
new file mode 100644
index 00000000000..1888e5cc644
--- /dev/null
+++ b/core/Command/User/Info.php
@@ -0,0 +1,88 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Core\Command\User;
+
+use OC\Core\Command\Base;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserManager;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Info extends Base {
+ /** @var IUserManager */
+ protected $userManager;
+ /** @var IGroupManager */
+ protected $groupManager;
+
+ /**
+ * @param IUserManager $userManager
+ * @param IGroupManager $groupManager
+ */
+ public function __construct(IUserManager $userManager, IGroupManager $groupManager) {
+ $this->userManager = $userManager;
+ $this->groupManager = $groupManager;
+ parent::__construct();
+ }
+
+ protected function configure() {
+ $this
+ ->setName('user:info')
+ ->setDescription('show user info')
+ ->addArgument(
+ 'user',
+ InputArgument::REQUIRED,
+ 'user to show'
+ )->addOption(
+ 'output',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'Output format (plain, json or json_pretty, default is plain)',
+ $this->defaultOutputFormat
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $user = $this->userManager->get($input->getArgument('user'));
+ if (is_null($user)) {
+ $output->writeln('<error>user not found</error>');
+ return 1;
+ }
+ $groups = $this->groupManager->getUserGroupIds($user);
+ $data = [
+ 'user_id' => $user->getUID(),
+ 'display_name' => $user->getDisplayName(),
+ 'email' => ($user->getEMailAddress()) ? $user->getEMailAddress() : '',
+ 'cloud_id' => $user->getCloudId(),
+ 'enabled' => $user->isEnabled(),
+ 'groups' => $groups,
+ 'quota' => $user->getQuota(),
+ 'last_seen' => date(\DateTime::ATOM, $user->getLastLogin()), // ISO-8601
+ 'user_directory' => $user->getHome(),
+ 'backend' => $user->getBackendClassName()
+ ];
+ $this->writeArrayInOutputFormat($input, $output, $data);
+ }
+}
diff --git a/core/Command/User/ListCommand.php b/core/Command/User/ListCommand.php
new file mode 100644
index 00000000000..b9e10f6a31c
--- /dev/null
+++ b/core/Command/User/ListCommand.php
@@ -0,0 +1,87 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Core\Command\User;
+
+use OC\Core\Command\Base;
+use OCP\IUser;
+use OCP\IUserManager;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ListCommand extends Base {
+ /** @var IUserManager */
+ protected $userManager;
+
+ /**
+ * @param IUserManager $userManager
+ */
+ public function __construct(IUserManager $userManager) {
+ $this->userManager = $userManager;
+ parent::__construct();
+ }
+
+ protected function configure() {
+ $this
+ ->setName('user:list')
+ ->setDescription('list configured users')
+ ->addOption(
+ 'limit',
+ 'l',
+ InputOption::VALUE_OPTIONAL,
+ 'Number of users to retrieve',
+ 500
+ )->addOption(
+ 'offset',
+ 'o',
+ InputOption::VALUE_OPTIONAL,
+ 'Offset for retrieving users',
+ 0
+ )->addOption(
+ 'output',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'Output format (plain, json or json_pretty, default is plain)',
+ $this->defaultOutputFormat
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $users = $this->userManager->search('', (int)$input->getOption('limit'), (int)$input->getOption('offset'));
+ $this->writeArrayInOutputFormat($input, $output, $this->formatUsers($users));
+ }
+
+ /**
+ * @param IUser[] $users
+ * @return array
+ */
+ private function formatUsers(array $users) {
+ $keys = array_map(function (IUser $user) {
+ return $user->getUID();
+ }, $users);
+ $values = array_map(function (IUser $user) {
+ return $user->getDisplayName();
+ }, $users);
+ return array_combine($keys, $values);
+ }
+}
diff --git a/core/css/icons.css b/core/css/icons.css
index 22b699b97ec..1a632fdd58b 100644
--- a/core/css/icons.css
+++ b/core/css/icons.css
@@ -204,6 +204,10 @@ img.icon-loading-small-dark, object.icon-loading-small-dark, video.icon-loading-
background-image: url('../img/actions/mail.svg');
}
+.icon-mail-grey {
+ background-image: url('../img/actions/mail-grey.svg');
+}
+
.icon-menu {
background-image: url('../img/actions/menu.svg');
}
@@ -372,3 +376,7 @@ img.icon-loading-small-dark, object.icon-loading-small-dark, video.icon-loading-
.icon-picture {
background-image: url('../img/places/picture.svg');
}
+
+.icon-clippy {
+ background-image: url('../img/actions/clippy.svg');
+}
diff --git a/core/img/actions/clippy.svg b/core/img/actions/clippy.svg
new file mode 100644
index 00000000000..8fa5c89c349
--- /dev/null
+++ b/core/img/actions/clippy.svg
@@ -0,0 +1,3 @@
+<svg height="1024" width="896" xmlns="http://www.w3.org/2000/svg">
+ <path opacity=".5" d="M704 896h-640v-576h640v192h64v-320c0-35-29-64-64-64h-192c0-71-57-128-128-128s-128 57-128 128h-192c-35 0-64 29-64 64v704c0 35 29 64 64 64h640c35 0 64-29 64-64v-128h-64v128z m-512-704c29 0 29 0 64 0s64-29 64-64 29-64 64-64 64 29 64 64 32 64 64 64 33 0 64 0 64 29 64 64h-512c0-39 28-64 64-64z m-64 512h128v-64h-128v64z m448-128v-128l-256 192 256 192v-128h320v-128h-320z m-448 256h192v-64h-192v64z m320-448h-320v64h320v-64z m-192 128h-128v64h128v-64z" />
+</svg>
diff --git a/core/js/core.json b/core/js/core.json
index 03c72e9b3ff..5254e7b3d03 100644
--- a/core/js/core.json
+++ b/core/js/core.json
@@ -10,7 +10,8 @@
"bootstrap/js/tooltip.js",
"backbone/backbone.js",
"es6-promise/dist/es6-promise.js",
- "davclient.js/lib/client.js"
+ "davclient.js/lib/client.js",
+ "clipboard/dist/clipboard.js"
],
"libraries": [
"jquery-showpassword.js",
diff --git a/core/js/sharedialoglinkshareview.js b/core/js/sharedialoglinkshareview.js
index 457a788d589..8ad2e270099 100644
--- a/core/js/sharedialoglinkshareview.js
+++ b/core/js/sharedialoglinkshareview.js
@@ -22,8 +22,11 @@
'<input type="checkbox" name="linkCheckbox" id="linkCheckbox-{{cid}}" class="checkbox linkCheckbox" value="1" {{#if isLinkShare}}checked="checked"{{/if}} />' +
'<label for="linkCheckbox-{{cid}}">{{linkShareLabel}}</label>' +
'<br />' +
+ '<div class="oneline">' +
'<label for="linkText-{{cid}}" class="hidden-visually">{{urlLabel}}</label>' +
'<input id="linkText-{{cid}}" class="linkText {{#unless isLinkShare}}hidden{{/unless}}" type="text" readonly="readonly" value="{{shareLinkURL}}" />' +
+ '<a class="{{#unless isLinkShare}}hidden-visually{{/unless}} clipboardButton icon icon-clippy" data-clipboard-target="#linkText-{{cid}}"></a>' +
+ '</div>' +
' {{#if publicUpload}}' +
'<div id="allowPublicUploadWrapper">' +
' <span class="icon-loading-small hidden"></span>' +
@@ -125,6 +128,38 @@
'onHideFileListChange',
'onAllowPublicUploadChange'
);
+
+ var clipboard = new Clipboard('.clipboardButton');
+ clipboard.on('success', function(e) {
+ $input = $(e.trigger);
+ $input.tooltip({placement: 'bottom', trigger: 'manual', title: t('core', 'Copied!')});
+ $input.tooltip('show');
+ _.delay(function() {
+ $input.tooltip('hide');
+ }, 3000);
+ });
+ clipboard.on('error', function (e) {
+ $input = $(e.trigger);
+ var actionMsg = '';
+ if (/iPhone|iPad/i.test(navigator.userAgent)) {
+ actionMsg = t('core', 'Not supported!');
+ } else if (/Mac/i.test(navigator.userAgent)) {
+ actionMsg = t('core', 'Press ⌘-C to copy.');
+ } else {
+ actionMsg = t('core', 'Press Ctrl-C to copy.');
+ }
+
+ $input.tooltip({
+ placement: 'bottom',
+ trigger: 'manual',
+ title: actionMsg
+ });
+ $input.tooltip('show');
+ _.delay(function () {
+ $input.tooltip('hide');
+ }, 3000);
+ });
+
},
onLinkCheckBoxChange: function() {
diff --git a/core/js/sharedialogmailview.js b/core/js/sharedialogmailview.js
index 84e3f3242ad..79741e92ac5 100644
--- a/core/js/sharedialogmailview.js
+++ b/core/js/sharedialogmailview.js
@@ -16,9 +16,9 @@
var TEMPLATE =
'{{#if shareAllowed}}' +
' {{#if mailPublicNotificationEnabled}}' +
- '<form id="emailPrivateLink" class="emailPrivateLinkForm">' +
+ '<form id="emailPrivateLink" class="emailPrivateLinkForm oneline">' +
' <input id="email" class="emailField" value="{{email}}" placeholder="{{mailPrivatePlaceholder}}" type="text" />' +
- ' <input id="emailButton" class="emailButton" type="submit" value="{{mailButtonText}}" />' +
+ ' <a id="emailButton" class="icon icon-mail-grey" />' +
'</form>' +
' {{/if}}' +
'{{/if}}'
@@ -48,7 +48,7 @@
showLink: true,
events: {
- 'submit .emailPrivateLinkForm': '_onEmailPrivateLink'
+ 'click #emailButton': '_onEmailPrivateLink'
},
initialize: function(options) {
@@ -173,4 +173,4 @@
OC.Share.ShareDialogMailView = ShareDialogMailView;
-})(); \ No newline at end of file
+})();
diff --git a/core/js/sharedialogshareelistview.js b/core/js/sharedialogshareelistview.js
index 85dee978987..d156d30cecd 100644
--- a/core/js/sharedialogshareelistview.js
+++ b/core/js/sharedialogshareelistview.js
@@ -38,7 +38,9 @@
'<span class="shareOption">' +
'<input id="canEdit-{{cid}}-{{shareWith}}" type="checkbox" name="edit" class="permissions checkbox" {{#if hasEditPermission}}checked="checked"{{/if}} />' +
'<label for="canEdit-{{cid}}-{{shareWith}}">{{canEditLabel}}</label>' +
+ '{{#if isFolder}}' +
'<a href="#" class="showCruds"><img alt="{{crudsLabel}}" src="{{triangleSImage}}"/></a>' +
+ '{{/if}}' +
'</span>' +
'{{/if}}' +
'<div class="cruds hidden">' +
@@ -162,7 +164,8 @@
sharePermission: OC.PERMISSION_SHARE,
createPermission: OC.PERMISSION_CREATE,
updatePermission: OC.PERMISSION_UPDATE,
- deletePermission: OC.PERMISSION_DELETE
+ deletePermission: OC.PERMISSION_DELETE,
+ isFolder: this.model.isFolder()
};
if(!this.model.hasUserShares()) {
diff --git a/core/js/sharedialogview.js b/core/js/sharedialogview.js
index c17da94bab3..5637ffc3a0a 100644
--- a/core/js/sharedialogview.js
+++ b/core/js/sharedialogview.js
@@ -30,7 +30,7 @@
'<div class="loading hidden" style="height: 50px"></div>';
var TEMPLATE_REMOTE_SHARE_INFO =
- '<a target="_blank" class="icon-info shareWithRemoteInfo hasTooltip" href="{{docLink}}" ' +
+ '<a target="_blank" class="icon icon-info shareWithRemoteInfo hasTooltip" href="{{docLink}}" ' +
'title="{{tooltip}}"></a>';
/**
diff --git a/core/js/tests/specs/sharedialogviewSpec.js b/core/js/tests/specs/sharedialogviewSpec.js
index 23214a7fe86..f0e027d9fb9 100644
--- a/core/js/tests/specs/sharedialogviewSpec.js
+++ b/core/js/tests/specs/sharedialogviewSpec.js
@@ -444,7 +444,7 @@ describe('OC.Share.ShareDialogView', function() {
dialog.render();
dialog.$el.find('.emailPrivateLinkForm .emailField').val('a@b.c');
- dialog.$el.find('.emailPrivateLinkForm').trigger('submit');
+ dialog.$el.find('#emailButton').trigger('click');
expect(sendEmailPrivateLinkStub.callCount).toEqual(1);
expect(dialog.$el.find('.emailPrivateLinkForm .emailField').val()).toEqual('Sending ...');
@@ -463,7 +463,7 @@ describe('OC.Share.ShareDialogView', function() {
dialog.render();
dialog.$el.find('.emailPrivateLinkForm .emailField').val('a@b.c');
- dialog.$el.find('.emailPrivateLinkForm').trigger('submit');
+ dialog.$el.find('#emailButton').trigger('click');
expect(sendEmailPrivateLinkStub.callCount).toEqual(1);
expect(dialog.$el.find('.emailPrivateLinkForm .emailField').val()).toEqual('Sending ...');
diff --git a/core/register_command.php b/core/register_command.php
index 91b00df20f1..70a1d7ed42e 100644
--- a/core/register_command.php
+++ b/core/register_command.php
@@ -137,6 +137,12 @@ if (\OC::$server->getConfig()->getSystemValue('installed', false)) {
$application->add(new OC\Core\Command\User\Report(\OC::$server->getUserManager()));
$application->add(new OC\Core\Command\User\ResetPassword(\OC::$server->getUserManager()));
$application->add(new OC\Core\Command\User\Setting(\OC::$server->getUserManager(), \OC::$server->getConfig(), \OC::$server->getDatabaseConnection()));
+ $application->add(new OC\Core\Command\User\ListCommand(\OC::$server->getUserManager()));
+ $application->add(new OC\Core\Command\User\Info(\OC::$server->getUserManager(), \OC::$server->getGroupManager()));
+
+ $application->add(new OC\Core\Command\Group\ListCommand(\OC::$server->getGroupManager()));
+ $application->add(new OC\Core\Command\Group\AddUser(\OC::$server->getUserManager(), \OC::$server->getGroupManager()));
+ $application->add(new OC\Core\Command\Group\RemoveUser(\OC::$server->getUserManager(), \OC::$server->getGroupManager()));
$application->add(new OC\Core\Command\Security\ListCertificates(\OC::$server->getCertificateManager(null), \OC::$server->getL10N('core')));
$application->add(new OC\Core\Command\Security\ImportCertificate(\OC::$server->getCertificateManager(null)));
diff --git a/core/vendor/.gitignore b/core/vendor/.gitignore
index 58a231c4bb4..5160c2a7f97 100644
--- a/core/vendor/.gitignore
+++ b/core/vendor/.gitignore
@@ -137,3 +137,7 @@ es6-promise/dist/*
# base64
base64/*min.js
+
+# clipboard
+clipboard/**
+!clipboard/dist/clipboard.js
diff --git a/core/vendor/clipboard/dist/clipboard.js b/core/vendor/clipboard/dist/clipboard.js
new file mode 100644
index 00000000000..040c5e005c3
--- /dev/null
+++ b/core/vendor/clipboard/dist/clipboard.js
@@ -0,0 +1,742 @@
+/*!
+ * clipboard.js v1.5.12
+ * https://zenorocha.github.io/clipboard.js
+ *
+ * Licensed MIT © Zeno Rocha
+ */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Clipboard = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+var matches = require('matches-selector')
+
+module.exports = function (element, selector, checkYoSelf) {
+ var parent = checkYoSelf ? element : element.parentNode
+
+ while (parent && parent !== document) {
+ if (matches(parent, selector)) return parent;
+ parent = parent.parentNode
+ }
+}
+
+},{"matches-selector":5}],2:[function(require,module,exports){
+var closest = require('closest');
+
+/**
+ * Delegates event to a selector.
+ *
+ * @param {Element} element
+ * @param {String} selector
+ * @param {String} type
+ * @param {Function} callback
+ * @param {Boolean} useCapture
+ * @return {Object}
+ */
+function delegate(element, selector, type, callback, useCapture) {
+ var listenerFn = listener.apply(this, arguments);
+
+ element.addEventListener(type, listenerFn, useCapture);
+
+ return {
+ destroy: function() {
+ element.removeEventListener(type, listenerFn, useCapture);
+ }
+ }
+}
+
+/**
+ * Finds closest match and invokes callback.
+ *
+ * @param {Element} element
+ * @param {String} selector
+ * @param {String} type
+ * @param {Function} callback
+ * @return {Function}
+ */
+function listener(element, selector, type, callback) {
+ return function(e) {
+ e.delegateTarget = closest(e.target, selector, true);
+
+ if (e.delegateTarget) {
+ callback.call(element, e);
+ }
+ }
+}
+
+module.exports = delegate;
+
+},{"closest":1}],3:[function(require,module,exports){
+/**
+ * Check if argument is a HTML element.
+ *
+ * @param {Object} value
+ * @return {Boolean}
+ */
+exports.node = function(value) {
+ return value !== undefined
+ && value instanceof HTMLElement
+ && value.nodeType === 1;
+};
+
+/**
+ * Check if argument is a list of HTML elements.
+ *
+ * @param {Object} value
+ * @return {Boolean}
+ */
+exports.nodeList = function(value) {
+ var type = Object.prototype.toString.call(value);
+
+ return value !== undefined
+ && (type === '[object NodeList]' || type === '[object HTMLCollection]')
+ && ('length' in value)
+ && (value.length === 0 || exports.node(value[0]));
+};
+
+/**
+ * Check if argument is a string.
+ *
+ * @param {Object} value
+ * @return {Boolean}
+ */
+exports.string = function(value) {
+ return typeof value === 'string'
+ || value instanceof String;
+};
+
+/**
+ * Check if argument is a function.
+ *
+ * @param {Object} value
+ * @return {Boolean}
+ */
+exports.fn = function(value) {
+ var type = Object.prototype.toString.call(value);
+
+ return type === '[object Function]';
+};
+
+},{}],4:[function(require,module,exports){
+var is = require('./is');
+var delegate = require('delegate');
+
+/**
+ * Validates all params and calls the right
+ * listener function based on its target type.
+ *
+ * @param {String|HTMLElement|HTMLCollection|NodeList} target
+ * @param {String} type
+ * @param {Function} callback
+ * @return {Object}
+ */
+function listen(target, type, callback) {
+ if (!target && !type && !callback) {
+ throw new Error('Missing required arguments');
+ }
+
+ if (!is.string(type)) {
+ throw new TypeError('Second argument must be a String');
+ }
+
+ if (!is.fn(callback)) {
+ throw new TypeError('Third argument must be a Function');
+ }
+
+ if (is.node(target)) {
+ return listenNode(target, type, callback);
+ }
+ else if (is.nodeList(target)) {
+ return listenNodeList(target, type, callback);
+ }
+ else if (is.string(target)) {
+ return listenSelector(target, type, callback);
+ }
+ else {
+ throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');
+ }
+}
+
+/**
+ * Adds an event listener to a HTML element
+ * and returns a remove listener function.
+ *
+ * @param {HTMLElement} node
+ * @param {String} type
+ * @param {Function} callback
+ * @return {Object}
+ */
+function listenNode(node, type, callback) {
+ node.addEventListener(type, callback);
+
+ return {
+ destroy: function() {
+ node.removeEventListener(type, callback);
+ }
+ }
+}
+
+/**
+ * Add an event listener to a list of HTML elements
+ * and returns a remove listener function.
+ *
+ * @param {NodeList|HTMLCollection} nodeList
+ * @param {String} type
+ * @param {Function} callback
+ * @return {Object}
+ */
+function listenNodeList(nodeList, type, callback) {
+ Array.prototype.forEach.call(nodeList, function(node) {
+ node.addEventListener(type, callback);
+ });
+
+ return {
+ destroy: function() {
+ Array.prototype.forEach.call(nodeList, function(node) {
+ node.removeEventListener(type, callback);
+ });
+ }
+ }
+}
+
+/**
+ * Add an event listener to a selector
+ * and returns a remove listener function.
+ *
+ * @param {String} selector
+ * @param {String} type
+ * @param {Function} callback
+ * @return {Object}
+ */
+function listenSelector(selector, type, callback) {
+ return delegate(document.body, selector, type, callback);
+}
+
+module.exports = listen;
+
+},{"./is":3,"delegate":2}],5:[function(require,module,exports){
+
+/**
+ * Element prototype.
+ */
+
+var proto = Element.prototype;
+
+/**
+ * Vendor function.
+ */
+
+var vendor = proto.matchesSelector
+ || proto.webkitMatchesSelector
+ || proto.mozMatchesSelector
+ || proto.msMatchesSelector
+ || proto.oMatchesSelector;
+
+/**
+ * Expose `match()`.
+ */
+
+module.exports = match;
+
+/**
+ * Match `el` to `selector`.
+ *
+ * @param {Element} el
+ * @param {String} selector
+ * @return {Boolean}
+ * @api public
+ */
+
+function match(el, selector) {
+ if (vendor) return vendor.call(el, selector);
+ var nodes = el.parentNode.querySelectorAll(selector);
+ for (var i = 0; i < nodes.length; ++i) {
+ if (nodes[i] == el) return true;
+ }
+ return false;
+}
+},{}],6:[function(require,module,exports){
+function select(element) {
+ var selectedText;
+
+ if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
+ element.focus();
+ element.setSelectionRange(0, element.value.length);
+
+ selectedText = element.value;
+ }
+ else {
+ if (element.hasAttribute('contenteditable')) {
+ element.focus();
+ }
+
+ var selection = window.getSelection();
+ var range = document.createRange();
+
+ range.selectNodeContents(element);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ selectedText = selection.toString();
+ }
+
+ return selectedText;
+}
+
+module.exports = select;
+
+},{}],7:[function(require,module,exports){
+function E () {
+ // Keep this empty so it's easier to inherit from
+ // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
+}
+
+E.prototype = {
+ on: function (name, callback, ctx) {
+ var e = this.e || (this.e = {});
+
+ (e[name] || (e[name] = [])).push({
+ fn: callback,
+ ctx: ctx
+ });
+
+ return this;
+ },
+
+ once: function (name, callback, ctx) {
+ var self = this;
+ function listener () {
+ self.off(name, listener);
+ callback.apply(ctx, arguments);
+ };
+
+ listener._ = callback
+ return this.on(name, listener, ctx);
+ },
+
+ emit: function (name) {
+ var data = [].slice.call(arguments, 1);
+ var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
+ var i = 0;
+ var len = evtArr.length;
+
+ for (i; i < len; i++) {
+ evtArr[i].fn.apply(evtArr[i].ctx, data);
+ }
+
+ return this;
+ },
+
+ off: function (name, callback) {
+ var e = this.e || (this.e = {});
+ var evts = e[name];
+ var liveEvents = [];
+
+ if (evts && callback) {
+ for (var i = 0, len = evts.length; i < len; i++) {
+ if (evts[i].fn !== callback && evts[i].fn._ !== callback)
+ liveEvents.push(evts[i]);
+ }
+ }
+
+ // Remove event from queue to prevent memory leak
+ // Suggested by https://github.com/lazd
+ // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
+
+ (liveEvents.length)
+ ? e[name] = liveEvents
+ : delete e[name];
+
+ return this;
+ }
+};
+
+module.exports = E;
+
+},{}],8:[function(require,module,exports){
+(function (global, factory) {
+ if (typeof define === "function" && define.amd) {
+ define(['module', 'select'], factory);
+ } else if (typeof exports !== "undefined") {
+ factory(module, require('select'));
+ } else {
+ var mod = {
+ exports: {}
+ };
+ factory(mod, global.select);
+ global.clipboardAction = mod.exports;
+ }
+})(this, function (module, _select) {
+ 'use strict';
+
+ var _select2 = _interopRequireDefault(_select);
+
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+
+ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
+ return typeof obj;
+ } : function (obj) {
+ return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj;
+ };
+
+ function _classCallCheck(instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+ }
+
+ var _createClass = function () {
+ function defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+
+ return function (Constructor, protoProps, staticProps) {
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) defineProperties(Constructor, staticProps);
+ return Constructor;
+ };
+ }();
+
+ var ClipboardAction = function () {
+ /**
+ * @param {Object} options
+ */
+
+ function ClipboardAction(options) {
+ _classCallCheck(this, ClipboardAction);
+
+ this.resolveOptions(options);
+ this.initSelection();
+ }
+
+ /**
+ * Defines base properties passed from constructor.
+ * @param {Object} options
+ */
+
+
+ ClipboardAction.prototype.resolveOptions = function resolveOptions() {
+ var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+
+ this.action = options.action;
+ this.emitter = options.emitter;
+ this.target = options.target;
+ this.text = options.text;
+ this.trigger = options.trigger;
+
+ this.selectedText = '';
+ };
+
+ ClipboardAction.prototype.initSelection = function initSelection() {
+ if (this.text) {
+ this.selectFake();
+ } else if (this.target) {
+ this.selectTarget();
+ }
+ };
+
+ ClipboardAction.prototype.selectFake = function selectFake() {
+ var _this = this;
+
+ var isRTL = document.documentElement.getAttribute('dir') == 'rtl';
+
+ this.removeFake();
+
+ this.fakeHandlerCallback = function () {
+ return _this.removeFake();
+ };
+ this.fakeHandler = document.body.addEventListener('click', this.fakeHandlerCallback) || true;
+
+ this.fakeElem = document.createElement('textarea');
+ // Prevent zooming on iOS
+ this.fakeElem.style.fontSize = '12pt';
+ // Reset box model
+ this.fakeElem.style.border = '0';
+ this.fakeElem.style.padding = '0';
+ this.fakeElem.style.margin = '0';
+ // Move element out of screen horizontally
+ this.fakeElem.style.position = 'absolute';
+ this.fakeElem.style[isRTL ? 'right' : 'left'] = '-9999px';
+ // Move element to the same position vertically
+ this.fakeElem.style.top = (window.pageYOffset || document.documentElement.scrollTop) + 'px';
+ this.fakeElem.setAttribute('readonly', '');
+ this.fakeElem.value = this.text;
+
+ document.body.appendChild(this.fakeElem);
+
+ this.selectedText = (0, _select2.default)(this.fakeElem);
+ this.copyText();
+ };
+
+ ClipboardAction.prototype.removeFake = function removeFake() {
+ if (this.fakeHandler) {
+ document.body.removeEventListener('click', this.fakeHandlerCallback);
+ this.fakeHandler = null;
+ this.fakeHandlerCallback = null;
+ }
+
+ if (this.fakeElem) {
+ document.body.removeChild(this.fakeElem);
+ this.fakeElem = null;
+ }
+ };
+
+ ClipboardAction.prototype.selectTarget = function selectTarget() {
+ this.selectedText = (0, _select2.default)(this.target);
+ this.copyText();
+ };
+
+ ClipboardAction.prototype.copyText = function copyText() {
+ var succeeded = undefined;
+
+ try {
+ succeeded = document.execCommand(this.action);
+ } catch (err) {
+ succeeded = false;
+ }
+
+ this.handleResult(succeeded);
+ };
+
+ ClipboardAction.prototype.handleResult = function handleResult(succeeded) {
+ if (succeeded) {
+ this.emitter.emit('success', {
+ action: this.action,
+ text: this.selectedText,
+ trigger: this.trigger,
+ clearSelection: this.clearSelection.bind(this)
+ });
+ } else {
+ this.emitter.emit('error', {
+ action: this.action,
+ trigger: this.trigger,
+ clearSelection: this.clearSelection.bind(this)
+ });
+ }
+ };
+
+ ClipboardAction.prototype.clearSelection = function clearSelection() {
+ if (this.target) {
+ this.target.blur();
+ }
+
+ window.getSelection().removeAllRanges();
+ };
+
+ ClipboardAction.prototype.destroy = function destroy() {
+ this.removeFake();
+ };
+
+ _createClass(ClipboardAction, [{
+ key: 'action',
+ set: function set() {
+ var action = arguments.length <= 0 || arguments[0] === undefined ? 'copy' : arguments[0];
+
+ this._action = action;
+
+ if (this._action !== 'copy' && this._action !== 'cut') {
+ throw new Error('Invalid "action" value, use either "copy" or "cut"');
+ }
+ },
+ get: function get() {
+ return this._action;
+ }
+ }, {
+ key: 'target',
+ set: function set(target) {
+ if (target !== undefined) {
+ if (target && (typeof target === 'undefined' ? 'undefined' : _typeof(target)) === 'object' && target.nodeType === 1) {
+ if (this.action === 'copy' && target.hasAttribute('disabled')) {
+ throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');
+ }
+
+ if (this.action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {
+ throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');
+ }
+
+ this._target = target;
+ } else {
+ throw new Error('Invalid "target" value, use a valid Element');
+ }
+ }
+ },
+ get: function get() {
+ return this._target;
+ }
+ }]);
+
+ return ClipboardAction;
+ }();
+
+ module.exports = ClipboardAction;
+});
+
+},{"select":6}],9:[function(require,module,exports){
+(function (global, factory) {
+ if (typeof define === "function" && define.amd) {
+ define(['module', './clipboard-action', 'tiny-emitter', 'good-listener'], factory);
+ } else if (typeof exports !== "undefined") {
+ factory(module, require('./clipboard-action'), require('tiny-emitter'), require('good-listener'));
+ } else {
+ var mod = {
+ exports: {}
+ };
+ factory(mod, global.clipboardAction, global.tinyEmitter, global.goodListener);
+ global.clipboard = mod.exports;
+ }
+})(this, function (module, _clipboardAction, _tinyEmitter, _goodListener) {
+ 'use strict';
+
+ var _clipboardAction2 = _interopRequireDefault(_clipboardAction);
+
+ var _tinyEmitter2 = _interopRequireDefault(_tinyEmitter);
+
+ var _goodListener2 = _interopRequireDefault(_goodListener);
+
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+
+ function _classCallCheck(instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+ }
+
+ function _possibleConstructorReturn(self, call) {
+ if (!self) {
+ throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
+ }
+
+ return call && (typeof call === "object" || typeof call === "function") ? call : self;
+ }
+
+ function _inherits(subClass, superClass) {
+ if (typeof superClass !== "function" && superClass !== null) {
+ throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
+ }
+
+ subClass.prototype = Object.create(superClass && superClass.prototype, {
+ constructor: {
+ value: subClass,
+ enumerable: false,
+ writable: true,
+ configurable: true
+ }
+ });
+ if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
+ }
+
+ var Clipboard = function (_Emitter) {
+ _inherits(Clipboard, _Emitter);
+
+ /**
+ * @param {String|HTMLElement|HTMLCollection|NodeList} trigger
+ * @param {Object} options
+ */
+
+ function Clipboard(trigger, options) {
+ _classCallCheck(this, Clipboard);
+
+ var _this = _possibleConstructorReturn(this, _Emitter.call(this));
+
+ _this.resolveOptions(options);
+ _this.listenClick(trigger);
+ return _this;
+ }
+
+ /**
+ * Defines if attributes would be resolved using internal setter functions
+ * or custom functions that were passed in the constructor.
+ * @param {Object} options
+ */
+
+
+ Clipboard.prototype.resolveOptions = function resolveOptions() {
+ var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+
+ this.action = typeof options.action === 'function' ? options.action : this.defaultAction;
+ this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;
+ this.text = typeof options.text === 'function' ? options.text : this.defaultText;
+ };
+
+ Clipboard.prototype.listenClick = function listenClick(trigger) {
+ var _this2 = this;
+
+ this.listener = (0, _goodListener2.default)(trigger, 'click', function (e) {
+ return _this2.onClick(e);
+ });
+ };
+
+ Clipboard.prototype.onClick = function onClick(e) {
+ var trigger = e.delegateTarget || e.currentTarget;
+
+ if (this.clipboardAction) {
+ this.clipboardAction = null;
+ }
+
+ this.clipboardAction = new _clipboardAction2.default({
+ action: this.action(trigger),
+ target: this.target(trigger),
+ text: this.text(trigger),
+ trigger: trigger,
+ emitter: this
+ });
+ };
+
+ Clipboard.prototype.defaultAction = function defaultAction(trigger) {
+ return getAttributeValue('action', trigger);
+ };
+
+ Clipboard.prototype.defaultTarget = function defaultTarget(trigger) {
+ var selector = getAttributeValue('target', trigger);
+
+ if (selector) {
+ return document.querySelector(selector);
+ }
+ };
+
+ Clipboard.prototype.defaultText = function defaultText(trigger) {
+ return getAttributeValue('text', trigger);
+ };
+
+ Clipboard.prototype.destroy = function destroy() {
+ this.listener.destroy();
+
+ if (this.clipboardAction) {
+ this.clipboardAction.destroy();
+ this.clipboardAction = null;
+ }
+ };
+
+ return Clipboard;
+ }(_tinyEmitter2.default);
+
+ /**
+ * Helper function to retrieve attribute value.
+ * @param {String} suffix
+ * @param {Element} element
+ */
+ function getAttributeValue(suffix, element) {
+ var attribute = 'data-clipboard-' + suffix;
+
+ if (!element.hasAttribute(attribute)) {
+ return;
+ }
+
+ return element.getAttribute(attribute);
+ }
+
+ module.exports = Clipboard;
+});
+
+},{"./clipboard-action":8,"good-listener":4,"tiny-emitter":7}]},{},[9])(9)
+}); \ No newline at end of file
diff --git a/db_structure.xml b/db_structure.xml
index 6b91c3c4c5d..1127f0d82d4 100644
--- a/db_structure.xml
+++ b/db_structure.xml
@@ -170,6 +170,11 @@
<length>4000</length>
</field>
+ <field>
+ <name>mount_id</name>
+ <type>integer</type>
+ </field>
+
<index>
<name>mounts_user_index</name>
<unique>false</unique>
@@ -198,6 +203,15 @@
</index>
<index>
+ <name>mounts_mount_id_index</name>
+ <unique>false</unique>
+ <field>
+ <name>mount_id</name>
+ <sorting>ascending</sorting>
+ </field>
+ </index>
+
+ <index>
<name>mounts_user_root_index</name>
<unique>true</unique>
<field>
diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php
index f21b34a6b4a..32a85606abf 100644
--- a/lib/private/AppFramework/DependencyInjection/DIContainer.php
+++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php
@@ -38,6 +38,7 @@ use OC\AppFramework\Http\Dispatcher;
use OC\AppFramework\Http\Output;
use OC\AppFramework\Middleware\MiddlewareDispatcher;
use OC\AppFramework\Middleware\Security\CORSMiddleware;
+use OC\AppFramework\Middleware\OCSMiddleware;
use OC\AppFramework\Middleware\Security\SecurityMiddleware;
use OC\AppFramework\Middleware\SessionMiddleware;
use OC\AppFramework\Utility\SimpleContainer;
@@ -373,6 +374,12 @@ class DIContainer extends SimpleContainer implements IAppContainer {
return new TwoFactorMiddleware($twoFactorManager, $userSession, $session, $urlGenerator, $reflector, $request);
});
+ $this->registerService('OCSMiddleware', function (SimpleContainer $c) {
+ return new OCSMiddleware(
+ $c['Request']
+ );
+ });
+
$middleWares = &$this->middleWares;
$this->registerService('MiddlewareDispatcher', function($c) use (&$middleWares) {
$dispatcher = new MiddlewareDispatcher();
@@ -385,6 +392,7 @@ class DIContainer extends SimpleContainer implements IAppContainer {
}
$dispatcher->registerMiddleware($c['SessionMiddleware']);
+ $dispatcher->registerMiddleware($c['OCSMiddleware']);
return $dispatcher;
});
diff --git a/lib/private/AppFramework/Middleware/OCSMiddleware.php b/lib/private/AppFramework/Middleware/OCSMiddleware.php
new file mode 100644
index 00000000000..2c7d1167e7c
--- /dev/null
+++ b/lib/private/AppFramework/Middleware/OCSMiddleware.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OC\AppFramework\Middleware;
+
+use OC\AppFramework\Http;
+use OCP\AppFramework\Http\OCSResponse;
+use OCP\AppFramework\OCS\OCSException;
+use OCP\AppFramework\OCSController;
+use OCP\IRequest;
+use OCP\AppFramework\Middleware;
+
+class OCSMiddleware extends Middleware {
+
+ /** @var IRequest */
+ private $request;
+
+ /**
+ * @param IRequest $request
+ */
+ public function __construct(IRequest $request) {
+ $this->request = $request;
+ }
+
+ /**
+ * @param \OCP\AppFramework\Controller $controller
+ * @param string $methodName
+ * @param \Exception $exception
+ * @throws \Exception
+ * @return OCSResponse
+ */
+ public function afterException($controller, $methodName, \Exception $exception) {
+ if ($controller instanceof OCSController && $exception instanceof OCSException) {
+ $format = $this->getFormat($controller);
+
+ $code = $exception->getCode();
+ if ($code === 0) {
+ $code = Http::STATUS_INTERNAL_SERVER_ERROR;
+ }
+ return new OCSResponse($format, $code, $exception->getMessage());
+ }
+
+ throw $exception;
+ }
+
+ /**
+ * @param \OCP\AppFramework\Controller $controller
+ * @return string
+ */
+ private function getFormat($controller) {
+ // get format from the url format or request format parameter
+ $format = $this->request->getParam('format');
+
+ // if none is given try the first Accept header
+ if($format === null) {
+ $headers = $this->request->getHeader('Accept');
+ $format = $controller->getResponderByHTTPHeader($headers);
+ }
+
+ return $format;
+ }
+}
diff --git a/lib/private/Files/Config/CachedMountInfo.php b/lib/private/Files/Config/CachedMountInfo.php
index ce75cb66896..b81cd11a1c1 100644
--- a/lib/private/Files/Config/CachedMountInfo.php
+++ b/lib/private/Files/Config/CachedMountInfo.php
@@ -48,18 +48,25 @@ class CachedMountInfo implements ICachedMountInfo {
protected $mountPoint;
/**
+ * @var int|null
+ */
+ protected $mountId;
+
+ /**
* CachedMountInfo constructor.
*
* @param IUser $user
* @param int $storageId
* @param int $rootId
* @param string $mountPoint
+ * @param int|null $mountId
*/
- public function __construct(IUser $user, $storageId, $rootId, $mountPoint) {
+ public function __construct(IUser $user, $storageId, $rootId, $mountPoint, $mountId = null) {
$this->user = $user;
$this->storageId = $storageId;
$this->rootId = $rootId;
$this->mountPoint = $mountPoint;
+ $this->mountId = $mountId;
}
/**
@@ -104,4 +111,14 @@ class CachedMountInfo implements ICachedMountInfo {
public function getMountPoint() {
return $this->mountPoint;
}
+
+ /**
+ * Get the id of the configured mount
+ *
+ * @return int|null mount id or null if not applicable
+ * @since 9.1.0
+ */
+ public function getMountId() {
+ return $this->mountId;
+ }
}
diff --git a/lib/private/Files/Config/LazyStorageMountInfo.php b/lib/private/Files/Config/LazyStorageMountInfo.php
index 5df04c4b78e..58f288135e6 100644
--- a/lib/private/Files/Config/LazyStorageMountInfo.php
+++ b/lib/private/Files/Config/LazyStorageMountInfo.php
@@ -75,4 +75,8 @@ class LazyStorageMountInfo extends CachedMountInfo {
}
return parent::getMountPoint();
}
+
+ public function getMountId() {
+ return $this->mount->getMountId();
+ }
}
diff --git a/lib/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php
index bc6ad1b34f0..d9b538a002a 100644
--- a/lib/private/Files/Config/UserMountCache.php
+++ b/lib/private/Files/Config/UserMountCache.php
@@ -112,13 +112,7 @@ class UserMountCache implements IUserMountCache {
/** @var ICachedMountInfo[] $removedMounts */
$removedMounts = array_udiff($cachedMounts, $newMounts, $mountDiff);
- $changedMounts = array_uintersect($newMounts, $cachedMounts, function (ICachedMountInfo $mount1, ICachedMountInfo $mount2) {
- // filter mounts with the same root id and different mountpoints
- if ($mount1->getRootId() !== $mount2->getRootId()) {
- return -1;
- }
- return ($mount1->getMountPoint() !== $mount2->getMountPoint()) ? 0 : 1;
- });
+ $changedMounts = $this->findChangedMounts($newMounts, $cachedMounts);
foreach ($addedMounts as $mount) {
$this->addToCache($mount);
@@ -130,8 +124,28 @@ class UserMountCache implements IUserMountCache {
unset($this->mountsForUsers[$user->getUID()][$index]);
}
foreach ($changedMounts as $mount) {
- $this->setMountPoint($mount);
+ $this->updateCachedMount($mount);
+ }
+ }
+
+ /**
+ * @param ICachedMountInfo[] $newMounts
+ * @param ICachedMountInfo[] $cachedMounts
+ * @return ICachedMountInfo[]
+ */
+ private function findChangedMounts(array $newMounts, array $cachedMounts) {
+ $changed = [];
+ foreach ($newMounts as $newMount) {
+ foreach ($cachedMounts as $cachedMount) {
+ if (
+ $newMount->getRootId() === $cachedMount->getRootId() &&
+ ($newMount->getMountPoint() !== $cachedMount->getMountPoint() || $newMount->getMountId() !== $cachedMount->getMountId())
+ ) {
+ $changed[] = $newMount;
+ }
+ }
}
+ return $changed;
}
private function addToCache(ICachedMountInfo $mount) {
@@ -140,18 +154,20 @@ class UserMountCache implements IUserMountCache {
'storage_id' => $mount->getStorageId(),
'root_id' => $mount->getRootId(),
'user_id' => $mount->getUser()->getUID(),
- 'mount_point' => $mount->getMountPoint()
+ 'mount_point' => $mount->getMountPoint(),
+ 'mount_id' => $mount->getMountId()
], ['root_id', 'user_id']);
} else {
$this->logger->error('Error getting storage info for mount at ' . $mount->getMountPoint());
}
}
- private function setMountPoint(ICachedMountInfo $mount) {
+ private function updateCachedMount(ICachedMountInfo $mount) {
$builder = $this->connection->getQueryBuilder();
$query = $builder->update('mounts')
->set('mount_point', $builder->createNamedParameter($mount->getMountPoint()))
+ ->set('mount_id', $builder->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT))
->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)));
@@ -169,7 +185,7 @@ class UserMountCache implements IUserMountCache {
private function dbRowToMountInfo(array $row) {
$user = $this->userManager->get($row['user_id']);
- return new CachedMountInfo($user, (int)$row['storage_id'], (int)$row['root_id'], $row['mount_point']);
+ return new CachedMountInfo($user, (int)$row['storage_id'], (int)$row['root_id'], $row['mount_point'], $row['mount_id']);
}
/**
@@ -179,7 +195,7 @@ class UserMountCache implements IUserMountCache {
public function getMountsForUser(IUser $user) {
if (!isset($this->mountsForUsers[$user->getUID()])) {
$builder = $this->connection->getQueryBuilder();
- $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point')
+ $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id')
->from('mounts')
->where($builder->expr()->eq('user_id', $builder->createPositionalParameter($user->getUID())));
@@ -196,7 +212,7 @@ class UserMountCache implements IUserMountCache {
*/
public function getMountsForStorageId($numericStorageId) {
$builder = $this->connection->getQueryBuilder();
- $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point')
+ $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id')
->from('mounts')
->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($numericStorageId, IQueryBuilder::PARAM_INT)));
@@ -211,7 +227,7 @@ class UserMountCache implements IUserMountCache {
*/
public function getMountsForRootId($rootFileId) {
$builder = $this->connection->getQueryBuilder();
- $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point')
+ $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id')
->from('mounts')
->where($builder->expr()->eq('root_id', $builder->createPositionalParameter($rootFileId, IQueryBuilder::PARAM_INT)));
diff --git a/lib/private/Files/Mount/MountPoint.php b/lib/private/Files/Mount/MountPoint.php
index e11da9e5c74..f76e8151059 100644
--- a/lib/private/Files/Mount/MountPoint.php
+++ b/lib/private/Files/Mount/MountPoint.php
@@ -68,14 +68,19 @@ class MountPoint implements IMountPoint {
*/
private $invalidStorage = false;
+ /** @var int|null */
+ protected $mountId;
+
/**
* @param string|\OC\Files\Storage\Storage $storage
* @param string $mountpoint
* @param array $arguments (optional) configuration for the storage backend
* @param \OCP\Files\Storage\IStorageFactory $loader
* @param array $mountOptions mount specific options
+ * @param int|null $mountId
+ * @throws \Exception
*/
- public function __construct($storage, $mountpoint, $arguments = null, $loader = null, $mountOptions = null) {
+ public function __construct($storage, $mountpoint, $arguments = null, $loader = null, $mountOptions = null, $mountId = null) {
if (is_null($arguments)) {
$arguments = array();
}
@@ -102,6 +107,7 @@ class MountPoint implements IMountPoint {
$this->class = $storage;
$this->arguments = $arguments;
}
+ $this->mountId = $mountId;
}
/**
@@ -249,4 +255,8 @@ class MountPoint implements IMountPoint {
public function getStorageRootId() {
return (int)$this->getStorage()->getCache()->getId('');
}
+
+ public function getMountId() {
+ return $this->mountId;
+ }
}
diff --git a/lib/private/legacy/app.php b/lib/private/legacy/app.php
index 87553cca805..37ceea35ac0 100644
--- a/lib/private/legacy/app.php
+++ b/lib/private/legacy/app.php
@@ -1091,6 +1091,7 @@ class OC_App {
* @throws Exception if no app-name was specified
*/
public static function installApp($app) {
+ $appName = $app; // $app will be overwritten, preserve name for error logging
$l = \OC::$server->getL10N('core');
$config = \OC::$server->getConfig();
$ocsClient = new OCSClient(
@@ -1163,7 +1164,11 @@ class OC_App {
}
\OC_Hook::emit('OC_App', 'post_enable', array('app' => $app));
} else {
- throw new \Exception($l->t("No app name specified"));
+ if(empty($appName) ) {
+ throw new \Exception($l->t("No app name specified"));
+ } else {
+ throw new \Exception($l->t("App '%s' could not be installed!", $appName));
+ }
}
return $app;
diff --git a/lib/private/legacy/files.php b/lib/private/legacy/files.php
index 8cf98322223..cb8dc35aa5c 100644
--- a/lib/private/legacy/files.php
+++ b/lib/private/legacy/files.php
@@ -192,7 +192,7 @@ class OC_Files {
* @return array $rangeArray ('from'=>int,'to'=>int), ...
*/
private static function parseHttpRangeHeader($rangeHeaderPos, $fileSize) {
- $rArray=split(',', $rangeHeaderPos);
+ $rArray=explode(',', $rangeHeaderPos);
$minOffset = 0;
$ind = 0;
diff --git a/lib/public/API.php b/lib/public/API.php
index 4d68bef6f29..d5c08f43347 100644
--- a/lib/public/API.php
+++ b/lib/public/API.php
@@ -35,6 +35,7 @@ namespace OCP;
/**
* This class provides functions to manage apps in ownCloud
* @since 5.0.0
+ * @deprecated 9.1.0 Use the AppFramework
*/
class API {
@@ -66,6 +67,7 @@ class API {
* @param array $defaults
* @param array $requirements
* @since 5.0.0
+ * @deprecated 9.1.0 Use the AppFramework
*/
public static function register($method, $url, $action, $app, $authLevel = self::USER_AUTH,
$defaults = array(), $requirements = array()){
diff --git a/lib/public/AppFramework/OCS/OCSBadRequestException.php b/lib/public/AppFramework/OCS/OCSBadRequestException.php
new file mode 100644
index 00000000000..0f4278fddc4
--- /dev/null
+++ b/lib/public/AppFramework/OCS/OCSBadRequestException.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OCP\AppFramework\OCS;
+
+use Exception;
+use OCP\AppFramework\Http;
+
+/**
+ * Class OCSBadRequestException
+ *
+ * @package OCP\AppFramework
+ * @since 9.1.0
+ */
+class OCSBadRequestException extends OCSException {
+ /**
+ * OCSBadRequestException constructor.
+ *
+ * @param string $message
+ * @param Exception|null $previous
+ * @since 9.1.0
+ */
+ public function __construct($message = '', Exception $previous = null) {
+ parent::__construct($message, Http::STATUS_BAD_REQUEST, $previous);
+ }
+
+}
diff --git a/lib/public/AppFramework/OCS/OCSException.php b/lib/public/AppFramework/OCS/OCSException.php
new file mode 100644
index 00000000000..f95b5a16844
--- /dev/null
+++ b/lib/public/AppFramework/OCS/OCSException.php
@@ -0,0 +1,32 @@
+<?php
+/**
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OCP\AppFramework\OCS;
+
+use Exception;
+
+/**
+ * Class OCSException
+ *
+ * @package OCP\AppFramework
+ * @since 9.1.0
+ */
+class OCSException extends Exception {}
diff --git a/lib/public/AppFramework/OCS/OCSForbiddenException.php b/lib/public/AppFramework/OCS/OCSForbiddenException.php
new file mode 100644
index 00000000000..0c792722d9a
--- /dev/null
+++ b/lib/public/AppFramework/OCS/OCSForbiddenException.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OCP\AppFramework\OCS;
+
+use Exception;
+use OCP\AppFramework\Http;
+
+/**
+ * Class OCSForbiddenException
+ *
+ * @package OCP\AppFramework
+ * @since 9.1.0
+ */
+class OCSForbiddenException extends OCSException {
+ /**
+ * OCSForbiddenException constructor.
+ *
+ * @param string $message
+ * @param Exception|null $previous
+ * @since 9.1.0
+ */
+ public function __construct($message = '', Exception $previous = null) {
+ parent::__construct($message, Http::STATUS_FORBIDDEN, $previous);
+ }
+}
diff --git a/lib/public/AppFramework/OCS/OCSNotFoundException.php b/lib/public/AppFramework/OCS/OCSNotFoundException.php
new file mode 100644
index 00000000000..aaef36af1c7
--- /dev/null
+++ b/lib/public/AppFramework/OCS/OCSNotFoundException.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OCP\AppFramework\OCS;
+
+use Exception;
+use OCP\AppFramework\Http;
+
+/**
+ * Class OCSNotFoundException
+ *
+ * @package OCP\AppFramework
+ * @since 9.1.0
+ */
+class OCSNotFoundException extends OCSException {
+ /**
+ * OCSNotFoundException constructor.
+ *
+ * @param string $message
+ * @param Exception|null $previous
+ * @since 9.1.0
+ */
+ public function __construct($message = '', Exception $previous = null) {
+ parent::__construct($message, Http::STATUS_NOT_FOUND, $previous);
+ }
+}
diff --git a/lib/public/Files/Config/ICachedMountInfo.php b/lib/public/Files/Config/ICachedMountInfo.php
index e09c1a7f014..24c09654212 100644
--- a/lib/public/Files/Config/ICachedMountInfo.php
+++ b/lib/public/Files/Config/ICachedMountInfo.php
@@ -59,4 +59,12 @@ interface ICachedMountInfo {
* @since 9.0.0
*/
public function getMountPoint();
+
+ /**
+ * Get the id of the configured mount
+ *
+ * @return int|null mount id or null if not applicable
+ * @since 9.1.0
+ */
+ public function getMountId();
}
diff --git a/lib/public/Files/Mount/IMountPoint.php b/lib/public/Files/Mount/IMountPoint.php
index bc7bf81709f..824b60a1024 100644
--- a/lib/public/Files/Mount/IMountPoint.php
+++ b/lib/public/Files/Mount/IMountPoint.php
@@ -102,4 +102,12 @@ interface IMountPoint {
* @since 9.1.0
*/
public function getStorageRootId();
+
+ /**
+ * Get the id of the configured mount
+ *
+ * @return int|null mount id or null if not applicable
+ * @since 9.1.0
+ */
+ public function getMountId();
}
diff --git a/lib/public/Files/Storage/INotifyStorage.php b/lib/public/Files/Storage/INotifyStorage.php
new file mode 100644
index 00000000000..0ebfd689b87
--- /dev/null
+++ b/lib/public/Files/Storage/INotifyStorage.php
@@ -0,0 +1,51 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCP\Files\Storage;
+
+/**
+ * Storage backend that support active notifications
+ *
+ * @since 9.1.0
+ */
+interface INotifyStorage {
+ const NOTIFY_ADDED = 1;
+ const NOTIFY_REMOVED = 2;
+ const NOTIFY_MODIFIED = 3;
+ const NOTIFY_RENAMED = 4;
+
+ /**
+ * Start listening for update notifications
+ *
+ * The provided callback will be called for every incoming notification with the following parameters
+ * - int $type the type of update, one of the INotifyStorage::NOTIFY_* constants
+ * - string $path the path of the update
+ * - string $renameTarget the target of the rename operation, only provided for rename updates
+ *
+ * Note that this call is blocking and will not exit on it's own, to stop listening for notifications return `false` from the callback
+ *
+ * @param string $path
+ * @param callable $callback
+ *
+ * @since 9.1.0
+ */
+ public function listen($path, callable $callback);
+}
diff --git a/settings/css/settings.css b/settings/css/settings.css
index a190cd91b00..4cd85598443 100644
--- a/settings/css/settings.css
+++ b/settings/css/settings.css
@@ -102,7 +102,7 @@ table.nostyle td { padding: 0.2em 0; }
#sessions table,
#apppasswords table {
width: 100%;
- min-height: 150px;
+ min-height: 50px;
padding-top: 5px;
max-width: 580px;
}
@@ -125,6 +125,11 @@ table.nostyle td { padding: 0.2em 0; }
white-space: nowrap;
overflow: hidden;
}
+
+#sessions tr *:nth-child(2),
+#apppasswords tr *:nth-child(2) {
+ text-align: right;
+}
#sessions .token-list td a.icon-delete,
#apppasswords .token-list td a.icon-delete {
display: block;
diff --git a/settings/js/authtoken_collection.js b/settings/js/authtoken_collection.js
index ab7f7d5804a..d1ffc25a599 100644
--- a/settings/js/authtoken_collection.js
+++ b/settings/js/authtoken_collection.js
@@ -39,7 +39,7 @@
comparator: function (t1, t2) {
var ts1 = parseInt(t1.get('lastActivity'), 10);
var ts2 = parseInt(t2.get('lastActivity'), 10);
- return ts1 < ts2;
+ return ts2 - ts1;
},
tokenType: null,
diff --git a/settings/js/authtoken_view.js b/settings/js/authtoken_view.js
index 2ebedb4131c..354173341bc 100644
--- a/settings/js/authtoken_view.js
+++ b/settings/js/authtoken_view.js
@@ -114,14 +114,18 @@
// Chrome User Agent from https://developer.chrome.com/multidevice/user-agent
chrome: /^Mozilla\/5\.0 \([^)]*(Windows|OS X|Linux)[^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/(\d+)[0-9.]+ (?:Mobile Safari|Safari)\/[0-9.]+$/,
// Safari User Agent from http://www.useragentstring.com/pages/Safari/
- safari: /^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/([0-9]+)[0-9.]+ Safari\/[0-9.A-Z]+$/,
+ safari: /^Mozilla\/5\.0 \([^)]*(Windows|OS X)[^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\)(?: Version\/([0-9]+)[0-9.]+)? Safari\/[0-9.A-Z]+$/,
// Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent
androidChrome: /Android.*(?:; (.*) Build\/).*Chrome\/(\d+)[0-9.]+/,
iphone: / *CPU +iPhone +OS +(\d+)_\d+ +like +Mac +OS +X */,
iosClient: /^Mozilla\/5\.0 \(iOS\) ownCloud\-iOS.*$/,
androidClient:/^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/,
// DAVdroid/1.2 (2016/07/03; dav4android; okhttp3) Android/6.0.1
- davDroid: /DAVdroid\/([0-9.]+)/
+ davDroid: /DAVdroid\/([0-9.]+)/,
+ // Mozilla/5.0 (U; Linux; Maemo; Jolla; Sailfish; like Android 4.3) AppleWebKit/538.1 (KHTML, like Gecko) WebPirate/2.0 like Mobile Safari/538.1 (compatible)
+ webPirate: /(Sailfish).*WebPirate\/(\d+)/,
+ // Mozilla/5.0 (Maemo; Linux; U; Jolla; Sailfish; Mobile; rv:31.0) Gecko/31.0 Firefox/31.0 SailfishBrowser/1.0
+ sailfishBrowser: /(Sailfish).*SailfishBrowser\/(\d+)/
};
var nameMap = {
ie: t('setting', 'Internet Explorer'),
@@ -133,7 +137,9 @@
iphone: t('setting', 'iPhone'),
iosClient: t('setting', 'iOS Client'),
androidClient: t('setting', 'Android Client'),
- davDroid: 'DAVdroid'
+ davDroid: 'DAVdroid',
+ webPirate: 'WebPirate',
+ sailfishBrowser: 'SailfishBrowser'
};
if (matches) {
diff --git a/settings/templates/personal.php b/settings/templates/personal.php
index e3164eb5b98..4f580e41287 100644
--- a/settings/templates/personal.php
+++ b/settings/templates/personal.php
@@ -172,7 +172,7 @@ if($_['passwordChangeSupported']) {
<thead class="token-list-header">
<tr>
<th><?php p($l->t('Device'));?></th>
- <th><?php p($l->t('Recent activity'));?></th>
+ <th><?php p($l->t('Last activity'));?></th>
<th></th>
</tr>
</thead>
@@ -188,7 +188,7 @@ if($_['passwordChangeSupported']) {
<thead class="hidden-when-empty">
<tr>
<th><?php p($l->t('Name'));?></th>
- <th><?php p($l->t('Recent activity'));?></th>
+ <th><?php p($l->t('Last activity'));?></th>
<th></th>
</tr>
</thead>
diff --git a/tests/lib/AppFramework/Middleware/OCSMiddlewareTest.php b/tests/lib/AppFramework/Middleware/OCSMiddlewareTest.php
new file mode 100644
index 00000000000..66131aa4b25
--- /dev/null
+++ b/tests/lib/AppFramework/Middleware/OCSMiddlewareTest.php
@@ -0,0 +1,108 @@
+<?php
+/**
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace Test\AppFramework\Middleware;
+
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\OCS\OCSBadRequestException;
+use OCP\AppFramework\OCS\OCSException;
+use OCP\AppFramework\OCS\OCSForbiddenException;
+use OCP\AppFramework\OCS\OCSNotFoundException;
+use OC\AppFramework\Http\Request;
+use OC\AppFramework\Middleware\OCSMiddleware;
+
+
+class OCSMiddlewareTest extends \Test\TestCase {
+
+ /**
+ * @var Request
+ */
+ private $request;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->request = $this->getMockBuilder('OCP\IRequest')
+ ->getMock();
+
+ }
+
+ public function dataAfterException() {
+ $OCSController = $this->getMockBuilder('OCP\AppFramework\OCSController')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $controller = $this->getMockBuilder('OCP\AppFramework\Controller')
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ return [
+ [$OCSController, new \Exception(), true],
+ [$OCSController, new OCSException(), false, '', Http::STATUS_INTERNAL_SERVER_ERROR],
+ [$OCSController, new OCSException('foo'), false, 'foo', Http::STATUS_INTERNAL_SERVER_ERROR],
+ [$OCSController, new OCSException('foo', Http::STATUS_IM_A_TEAPOT), false, 'foo', Http::STATUS_IM_A_TEAPOT],
+ [$OCSController, new OCSBadRequestException(), false, '', Http::STATUS_BAD_REQUEST],
+ [$OCSController, new OCSBadRequestException('foo'), false, 'foo', Http::STATUS_BAD_REQUEST],
+ [$OCSController, new OCSForbiddenException(), false, '', Http::STATUS_FORBIDDEN],
+ [$OCSController, new OCSForbiddenException('foo'), false, 'foo', Http::STATUS_FORBIDDEN],
+ [$OCSController, new OCSNotFoundException(), false, '', Http::STATUS_NOT_FOUND],
+ [$OCSController, new OCSNotFoundException('foo'), false, 'foo', Http::STATUS_NOT_FOUND],
+
+ [$controller, new \Exception(), true],
+ [$controller, new OCSException(), true],
+ [$controller, new OCSException('foo'), true],
+ [$controller, new OCSException('foo', Http::STATUS_IM_A_TEAPOT), true],
+ [$controller, new OCSBadRequestException(), true],
+ [$controller, new OCSBadRequestException('foo'), true],
+ [$controller, new OCSForbiddenException(), true],
+ [$controller, new OCSForbiddenException('foo'), true],
+ [$controller, new OCSNotFoundException(), true],
+ [$controller, new OCSNotFoundException('foo'), true],
+ ];
+ }
+
+ /**
+ * @dataProvider dataAfterException
+ *
+ * @param Controller $controller
+ * @param \Exception $exception
+ * @param bool $forward
+ * @param string $message
+ * @param int $code
+ */
+ public function testAfterException($controller, $exception, $forward, $message = '', $code = 0) {
+ $OCSMiddleware = new OCSMiddleware($this->request);
+
+ try {
+ $result = $OCSMiddleware->afterException($controller, 'method', $exception);
+ $this->assertFalse($forward);
+
+ $this->assertInstanceOf('OCP\AppFramework\Http\OCSResponse', $result);
+
+ $this->assertSame($message, $this->invokePrivate($result, 'message'));
+ $this->assertSame($code, $this->invokePrivate($result, 'statuscode'));
+ } catch (\Exception $e) {
+ $this->assertTrue($forward);
+ $this->assertEquals($exception, $e);
+ }
+ }
+
+}
diff --git a/tests/lib/Files/Config/UserMountCacheTest.php b/tests/lib/Files/Config/UserMountCacheTest.php
index e7554fc36d9..b9e09687c95 100644
--- a/tests/lib/Files/Config/UserMountCacheTest.php
+++ b/tests/lib/Files/Config/UserMountCacheTest.php
@@ -163,12 +163,14 @@ class UserMountCacheTest extends TestCase {
$user = $this->userManager->get('u1');
$storage = $this->getStorage(10, 20);
- $mount = new MountPoint($storage, '/foo/');
+ $mount = new MountPoint($storage, '/bar/');
$this->cache->registerMounts($user, [$mount]);
$this->clearCache();
+ $mount = new MountPoint($storage, '/foo/');
+
$this->cache->registerMounts($user, [$mount]);
$this->clearCache();
@@ -180,6 +182,29 @@ class UserMountCacheTest extends TestCase {
$this->assertEquals('/foo/', $cachedMount->getMountPoint());
}
+ public function testChangeMountId() {
+ $user = $this->userManager->get('u1');
+
+ $storage = $this->getStorage(10, 20);
+ $mount = new MountPoint($storage, '/foo/', null, null, null, null);
+
+ $this->cache->registerMounts($user, [$mount]);
+
+ $this->clearCache();
+
+ $mount = new MountPoint($storage, '/foo/', null, null, null, 1);
+
+ $this->cache->registerMounts($user, [$mount]);
+
+ $this->clearCache();
+
+ $cachedMounts = $this->cache->getMountsForUser($user);
+
+ $this->assertCount(1, $cachedMounts);
+ $cachedMount = $cachedMounts[0];
+ $this->assertEquals(1, $cachedMount->getMountId());
+ }
+
public function testGetMountsForUser() {
$user1 = $this->userManager->get('u1');
$user2 = $this->userManager->get('u2');
diff --git a/tests/lib/Files/Storage/Storage.php b/tests/lib/Files/Storage/Storage.php
index ed2ea87f9d9..04aafece2e3 100644
--- a/tests/lib/Files/Storage/Storage.php
+++ b/tests/lib/Files/Storage/Storage.php
@@ -105,6 +105,17 @@ abstract class Storage extends \Test\TestCase {
$this->assertEquals(array(), $content);
}
+ public function fileNameProvider() {
+ return [
+ ['file.txt'],
+ [' file.txt'],
+ ['folder .txt'],
+ ['file with space.txt'],
+ ['spéciäl fäile'],
+ ['test single\'quote.txt'],
+ ];
+ }
+
public function directoryProvider() {
return [
['folder'],
@@ -336,22 +347,25 @@ abstract class Storage extends \Test\TestCase {
$this->assertFalse($this->instance->file_exists('/lorem.txt'));
}
- public function testFOpen() {
+ /**
+ * @dataProvider fileNameProvider
+ */
+ public function testFOpen($fileName) {
$textFile = \OC::$SERVERROOT . '/tests/data/lorem.txt';
- $fh = @$this->instance->fopen('foo', 'r');
+ $fh = @$this->instance->fopen($fileName, 'r');
if ($fh) {
fclose($fh);
}
$this->assertFalse($fh);
- $this->assertFalse($this->instance->file_exists('foo'));
+ $this->assertFalse($this->instance->file_exists($fileName));
- $fh = $this->instance->fopen('foo', 'w');
+ $fh = $this->instance->fopen($fileName, 'w');
fwrite($fh, file_get_contents($textFile));
fclose($fh);
- $this->assertTrue($this->instance->file_exists('foo'));
+ $this->assertTrue($this->instance->file_exists($fileName));
- $fh = $this->instance->fopen('foo', 'r');
+ $fh = $this->instance->fopen($fileName, 'r');
$content = stream_get_contents($fh);
$this->assertEquals(file_get_contents($textFile), $content);
}
diff --git a/version.php b/version.php
index 86fb9bbf54c..15400c4f4e5 100644
--- a/version.php
+++ b/version.php
@@ -25,7 +25,7 @@
// We only can count up. The 4. digit is only for the internal patchlevel to trigger DB upgrades
// between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel
// when updating major/minor version number.
-$OC_Version = array(9, 1, 0, 11);
+$OC_Version = array(9, 1, 0, 12);
// The human readable string
$OC_VersionString = '9.1.0 RC1';