diff options
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'; |