summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/dav/lib/Connector/PublicAuth.php2
-rw-r--r--apps/dav/lib/Connector/Sabre/CustomPropertiesBackend.php2
-rw-r--r--apps/dav/lib/Server.php5
-rw-r--r--apps/files/lib/Controller/ApiController.php1
-rw-r--r--apps/files_external/lib/Lib/Storage/SFTP.php2
-rwxr-xr-xapps/files_external/tests/env/start-swift-ceph.sh2
-rw-r--r--apps/files_sharing/css/sharetabview.css9
-rw-r--r--apps/user_ldap/lib/Access.php2
-rw-r--r--apps/user_ldap/lib/Mapping/AbstractMapping.php6
-rw-r--r--apps/user_ldap/tests/Mapping/AbstractMappingTest.php2
-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/config.php4
-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--lib/base.php80
-rw-r--r--lib/private/AppFramework/DependencyInjection/DIContainer.php8
-rw-r--r--lib/private/AppFramework/Http/Request.php40
-rw-r--r--lib/private/AppFramework/Middleware/OCSMiddleware.php80
-rw-r--r--lib/private/AppFramework/Middleware/Security/Exceptions/StrictCookieMissingException.php36
-rw-r--r--lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php11
-rw-r--r--lib/private/Group/Database.php4
-rw-r--r--lib/private/Repair/RepairLegacyStorages.php2
-rw-r--r--lib/private/legacy/eventsource.php4
-rw-r--r--lib/private/legacy/files.php2
-rw-r--r--lib/private/legacy/json.php5
-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/IRequest.php18
-rw-r--r--lib/public/Util.php5
-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/Http/RequestTest.php307
-rw-r--r--tests/lib/AppFramework/Middleware/OCSMiddlewareTest.php108
-rw-r--r--tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php218
-rw-r--r--tests/lib/Files/Storage/Storage.php26
54 files changed, 2290 insertions, 134 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/Connector/Sabre/CustomPropertiesBackend.php b/apps/dav/lib/Connector/Sabre/CustomPropertiesBackend.php
index d0d348e170e..b319350c7f0 100644
--- a/apps/dav/lib/Connector/Sabre/CustomPropertiesBackend.php
+++ b/apps/dav/lib/Connector/Sabre/CustomPropertiesBackend.php
@@ -327,7 +327,7 @@ class CustomPropertiesBackend implements BackendInterface {
$result = $this->connection->executeQuery(
$sql,
- array($this->user, rtrim($path, '/') . '/%', $requestedProperties),
+ array($this->user, $this->connection->escapeLikeParameter(rtrim($path, '/')) . '/%', $requestedProperties),
array(null, null, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY)
);
diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php
index c0cb5ecd62d..982d299c951 100644
--- a/apps/dav/lib/Server.php
+++ b/apps/dav/lib/Server.php
@@ -73,13 +73,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/lib/Controller/ApiController.php b/apps/files/lib/Controller/ApiController.php
index 9a46cdd0fef..a442d7ea909 100644
--- a/apps/files/lib/Controller/ApiController.php
+++ b/apps/files/lib/Controller/ApiController.php
@@ -86,6 +86,7 @@ class ApiController extends Controller {
*
* @NoAdminRequired
* @NoCSRFRequired
+ * @StrictCookieRequired
*
* @param int $x
* @param int $y
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/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/apps/user_ldap/lib/Access.php b/apps/user_ldap/lib/Access.php
index 4d0753696ff..cdf12331477 100644
--- a/apps/user_ldap/lib/Access.php
+++ b/apps/user_ldap/lib/Access.php
@@ -623,7 +623,7 @@ class Access extends LDAPUtility implements IUserTools {
* "Developers"
*/
private function _createAltInternalOwnCloudNameForGroups($name) {
- $usedNames = $this->groupMapper->getNamesBySearch($name.'_%');
+ $usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%');
if(!($usedNames) || count($usedNames) === 0) {
$lastNo = 1; //will become name_2
} else {
diff --git a/apps/user_ldap/lib/Mapping/AbstractMapping.php b/apps/user_ldap/lib/Mapping/AbstractMapping.php
index 54fe7db366f..67fbd9fe851 100644
--- a/apps/user_ldap/lib/Mapping/AbstractMapping.php
+++ b/apps/user_ldap/lib/Mapping/AbstractMapping.php
@@ -138,16 +138,18 @@ abstract class AbstractMapping {
/**
* Searches mapped names by the giving string in the name column
* @param string $search
+ * @param string $prefixMatch
+ * @param string $postfixMatch
* @return string[]
*/
- public function getNamesBySearch($search) {
+ public function getNamesBySearch($search, $prefixMatch = "", $postfixMatch = "") {
$query = $this->dbc->prepare('
SELECT `owncloud_name`
FROM `'. $this->getTableName() .'`
WHERE `owncloud_name` LIKE ?
');
- $res = $query->execute(array($search));
+ $res = $query->execute(array($prefixMatch.$this->dbc->escapeLikeParameter($search).$postfixMatch));
$names = array();
if($res !== false) {
while($row = $query->fetch()) {
diff --git a/apps/user_ldap/tests/Mapping/AbstractMappingTest.php b/apps/user_ldap/tests/Mapping/AbstractMappingTest.php
index ddd99d31709..a2e9f850913 100644
--- a/apps/user_ldap/tests/Mapping/AbstractMappingTest.php
+++ b/apps/user_ldap/tests/Mapping/AbstractMappingTest.php
@@ -164,7 +164,7 @@ abstract class AbstractMappingTest extends \Test\TestCase {
public function testSearch() {
list($mapper,) = $this->initTest();
- $names = $mapper->getNamesBySearch('%oo%');
+ $names = $mapper->getNamesBySearch('oo', '%', '%');
$this->assertTrue(is_array($names));
$this->assertSame(2, count($names));
$this->assertTrue(in_array('Foobar', $names));
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/config.php b/core/js/config.php
index 197047ed8b8..c2e6213e8f7 100644
--- a/core/js/config.php
+++ b/core/js/config.php
@@ -32,6 +32,10 @@
*
*/
+if(!\OC::$server->getRequest()->passesStrictCookieCheck()) {
+ die();
+}
+
// Set the content type to Javascript
header("Content-type: text/javascript");
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/lib/base.php b/lib/base.php
index a5e9d9716b7..b2ac0bab7d8 100644
--- a/lib/base.php
+++ b/lib/base.php
@@ -469,6 +469,84 @@ class OC {
@ini_set('gd.jpeg_ignore_warning', 1);
}
+ /**
+ * Send the same site cookies
+ */
+ private static function sendSameSiteCookies() {
+ $cookieParams = session_get_cookie_params();
+ $secureCookie = ($cookieParams['secure'] === true) ? 'secure; ' : '';
+ $policies = [
+ 'lax',
+ 'strict',
+ ];
+ foreach($policies as $policy) {
+ header(
+ sprintf(
+ 'Set-Cookie: nc_sameSiteCookie%s=true; path=%s; httponly;' . $secureCookie . 'expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=%s',
+ $policy,
+ $cookieParams['path'],
+ $policy
+ ),
+ false
+ );
+ }
+ }
+
+ /**
+ * Same Site cookie to further mitigate CSRF attacks. This cookie has to
+ * be set in every request if cookies are sent to add a second level of
+ * defense against CSRF.
+ *
+ * If the cookie is not sent this will set the cookie and reload the page.
+ * We use an additional cookie since we want to protect logout CSRF and
+ * also we can't directly interfere with PHP's session mechanism.
+ */
+ private static function performSameSiteCookieProtection() {
+ if(count($_COOKIE) > 0) {
+ $request = \OC::$server->getRequest();
+ $requestUri = $request->getScriptName();
+ $processingScript = explode('/', $requestUri);
+ $processingScript = $processingScript[count($processingScript)-1];
+ // FIXME: In a SAML scenario we don't get any strict or lax cookie
+ // send for the ACS endpoint. Since we have some legacy code in Nextcloud
+ // (direct PHP files) the enforcement of lax cookies is performed here
+ // instead of the middleware.
+ //
+ // This means we cannot exclude some routes from the cookie validation,
+ // which normally is not a problem but is a little bit cumbersome for
+ // this use-case.
+ // Once the old legacy PHP endpoints have been removed we can move
+ // the verification into a middleware and also adds some exemptions.
+ //
+ // Questions about this code? Ask Lukas ;-)
+ $currentUrl = substr(explode('?',$request->getRequestUri(), 2)[0], strlen(\OC::$WEBROOT));
+ if($currentUrl === '/index.php/apps/user_saml/saml/acs') {
+ return;
+ }
+ // For the "index.php" endpoint only a lax cookie is required.
+ if($processingScript === 'index.php') {
+ if(!$request->passesLaxCookieCheck()) {
+ self::sendSameSiteCookies();
+ header('Location: '.$_SERVER['REQUEST_URI']);
+ exit();
+ }
+ } else {
+ // All other endpoints require the lax and the strict cookie
+ if(!$request->passesStrictCookieCheck()) {
+ self::sendSameSiteCookies();
+ // Debug mode gets access to the resources without strict cookie
+ // due to the fact that the SabreDAV browser also lives there.
+ if(!\OC::$server->getConfig()->getSystemValue('debug', false)) {
+ http_response_code(\OCP\AppFramework\Http::STATUS_SERVICE_UNAVAILABLE);
+ exit();
+ }
+ }
+ }
+ } elseif(!isset($_COOKIE['nc_sameSiteCookielax']) || !isset($_COOKIE['nc_sameSiteCookiestrict'])) {
+ self::sendSameSiteCookies();
+ }
+ }
+
public static function init() {
// calculate the root directories
OC::$SERVERROOT = str_replace("\\", '/', substr(__DIR__, 0, -4));
@@ -572,6 +650,8 @@ class OC {
ini_set('session.cookie_secure', true);
}
+ self::performSameSiteCookieProtection();
+
if (!defined('OC_CONSOLE')) {
$errors = OC_Util::checkServer(\OC::$server->getConfig());
if (count($errors) > 0) {
diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php
index 1684ff8027b..893d6cb9aa6 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;
@@ -374,6 +375,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();
@@ -386,6 +393,7 @@ class DIContainer extends SimpleContainer implements IAppContainer {
}
$dispatcher->registerMiddleware($c['SessionMiddleware']);
+ $dispatcher->registerMiddleware($c['OCSMiddleware']);
return $dispatcher;
});
diff --git a/lib/private/AppFramework/Http/Request.php b/lib/private/AppFramework/Http/Request.php
index d9cf1919252..8fc99f125b2 100644
--- a/lib/private/AppFramework/Http/Request.php
+++ b/lib/private/AppFramework/Http/Request.php
@@ -465,6 +465,10 @@ class Request implements \ArrayAccess, \Countable, IRequest {
return false;
}
+ if(!$this->passesStrictCookieCheck()) {
+ return false;
+ }
+
if (isset($this->items['get']['requesttoken'])) {
$token = $this->items['get']['requesttoken'];
} elseif (isset($this->items['post']['requesttoken'])) {
@@ -481,6 +485,42 @@ class Request implements \ArrayAccess, \Countable, IRequest {
}
/**
+ * Checks if the strict cookie has been sent with the request if the request
+ * is including any cookies.
+ *
+ * @return bool
+ * @since 9.1.0
+ */
+ public function passesStrictCookieCheck() {
+ if(count($this->cookies) === 0) {
+ return true;
+ }
+ if($this->getCookie('nc_sameSiteCookiestrict') === 'true'
+ && $this->passesLaxCookieCheck()) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Checks if the lax cookie has been sent with the request if the request
+ * is including any cookies.
+ *
+ * @return bool
+ * @since 9.1.0
+ */
+ public function passesLaxCookieCheck() {
+ if(count($this->cookies) === 0) {
+ return true;
+ }
+ if($this->getCookie('nc_sameSiteCookielax') === 'true') {
+ return true;
+ }
+ return false;
+ }
+
+
+ /**
* Returns an ID for the request, value is not guaranteed to be unique and is mostly meant for logging
* If `mod_unique_id` is installed this value will be taken.
* @return string
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/AppFramework/Middleware/Security/Exceptions/StrictCookieMissingException.php b/lib/private/AppFramework/Middleware/Security/Exceptions/StrictCookieMissingException.php
new file mode 100644
index 00000000000..9ccaed4566f
--- /dev/null
+++ b/lib/private/AppFramework/Middleware/Security/Exceptions/StrictCookieMissingException.php
@@ -0,0 +1,36 @@
+<?php
+/**
+ * @author Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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\Security\Exceptions;
+
+use OCP\AppFramework\Http;
+
+/**
+ * Class StrictCookieMissingException is thrown when the strict cookie has not
+ * been sent with the request but is required.
+ *
+ * @package OC\AppFramework\Middleware\Security\Exceptions
+ */
+class StrictCookieMissingException extends SecurityException {
+ public function __construct() {
+ parent::__construct('Strict Cookie has not been found in request.', Http::STATUS_PRECONDITION_FAILED);
+ }
+}
diff --git a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php
index a3ece262e10..98117751e21 100644
--- a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php
+++ b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php
@@ -32,6 +32,7 @@ use OC\AppFramework\Middleware\Security\Exceptions\AppNotEnabledException;
use OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException;
use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException;
use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException;
+use OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException;
use OC\AppFramework\Utility\ControllerMethodReflector;
use OC\Security\CSP\ContentSecurityPolicyManager;
use OCP\AppFramework\Http\ContentSecurityPolicy;
@@ -134,6 +135,12 @@ class SecurityMiddleware extends Middleware {
}
}
+ // Check for strict cookie requirement
+ if($this->reflector->hasAnnotation('StrictCookieRequired') || !$this->reflector->hasAnnotation('NoCSRFRequired')) {
+ if(!$this->request->passesStrictCookieCheck()) {
+ throw new StrictCookieMissingException();
+ }
+ }
// CSRF check - also registers the CSRF token since the session may be closed later
Util::callRegister();
if(!$this->reflector->hasAnnotation('NoCSRFRequired')) {
@@ -186,7 +193,9 @@ class SecurityMiddleware extends Middleware {
*/
public function afterException($controller, $methodName, \Exception $exception) {
if($exception instanceof SecurityException) {
-
+ if($exception instanceof StrictCookieMissingException) {
+ return new RedirectResponse(\OC::$WEBROOT);
+ }
if (stripos($this->request->getHeader('Accept'),'html') === false) {
$response = new JSONResponse(
array('message' => $exception->getMessage()),
diff --git a/lib/private/Group/Database.php b/lib/private/Group/Database.php
index 36d19f74cc6..64f249d4d25 100644
--- a/lib/private/Group/Database.php
+++ b/lib/private/Group/Database.php
@@ -285,7 +285,7 @@ class Database extends \OC\Group\Backend {
$parameters = [$gid];
$searchLike = '';
if ($search !== '') {
- $parameters[] = '%' . $search . '%';
+ $parameters[] = '%' . $this->dbConn->escapeLikeParameter($search) . '%';
$searchLike = ' AND `uid` LIKE ?';
}
@@ -311,7 +311,7 @@ class Database extends \OC\Group\Backend {
$parameters = [$gid];
$searchLike = '';
if ($search !== '') {
- $parameters[] = '%' . $search . '%';
+ $parameters[] = '%' . $this->dbConn->escapeLikeParameter($search) . '%';
$searchLike = ' AND `uid` LIKE ?';
}
diff --git a/lib/private/Repair/RepairLegacyStorages.php b/lib/private/Repair/RepairLegacyStorages.php
index 8ef20cdf3c2..096300f51c2 100644
--- a/lib/private/Repair/RepairLegacyStorages.php
+++ b/lib/private/Repair/RepairLegacyStorages.php
@@ -172,7 +172,7 @@ class RepairLegacyStorages implements IRepairStep{
$sql = 'SELECT `id`, `numeric_id` FROM `*PREFIX*storages`'
. ' WHERE `id` LIKE ?'
. ' ORDER BY `id`';
- $result = $this->connection->executeQuery($sql, array($dataDirId . '%'));
+ $result = $this->connection->executeQuery($sql, array($this->connection->escapeLikeParameter($dataDirId) . '%'));
while ($row = $result->fetch()) {
$currentId = $row['id'];
diff --git a/lib/private/legacy/eventsource.php b/lib/private/legacy/eventsource.php
index 51040e7be7d..70e9847d237 100644
--- a/lib/private/legacy/eventsource.php
+++ b/lib/private/legacy/eventsource.php
@@ -76,6 +76,10 @@ class OC_EventSource implements \OCP\IEventSource {
} else {
header("Content-Type: text/event-stream");
}
+ if(!\OC::$server->getRequest()->passesStrictCookieCheck()) {
+ header('Location: '.\OC::$WEBROOT);
+ exit();
+ }
if (!(\OC::$server->getRequest()->passesCSRFCheck())) {
$this->send('error', 'Possible CSRF attack. Connection will be closed.');
$this->close();
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/private/legacy/json.php b/lib/private/legacy/json.php
index 1dde63602b1..557e1d77012 100644
--- a/lib/private/legacy/json.php
+++ b/lib/private/legacy/json.php
@@ -79,6 +79,11 @@ class OC_JSON{
* @deprecated Use annotation based CSRF checks from the AppFramework instead
*/
public static function callCheck() {
+ if(!\OC::$server->getRequest()->passesStrictCookieCheck()) {
+ header('Location: '.\OC::$WEBROOT);
+ exit();
+ }
+
if( !(\OC::$server->getRequest()->passesCSRFCheck())) {
$l = \OC::$server->getL10N('lib');
self::error(array( 'data' => array( 'message' => $l->t('Token expired. Please reload page.'), 'error' => 'token_expired' )));
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/IRequest.php b/lib/public/IRequest.php
index 4db1c18b9c1..46e67d1415f 100644
--- a/lib/public/IRequest.php
+++ b/lib/public/IRequest.php
@@ -158,6 +158,24 @@ interface IRequest {
public function passesCSRFCheck();
/**
+ * Checks if the strict cookie has been sent with the request if the request
+ * is including any cookies.
+ *
+ * @return bool
+ * @since 9.0.0
+ */
+ public function passesStrictCookieCheck();
+
+ /**
+ * Checks if the lax cookie has been sent with the request if the request
+ * is including any cookies.
+ *
+ * @return bool
+ * @since 9.0.0
+ */
+ public function passesLaxCookieCheck();
+
+ /**
* Returns an ID for the request, value is not guaranteed to be unique and is mostly meant for logging
* If `mod_unique_id` is installed this value will be taken.
*
diff --git a/lib/public/Util.php b/lib/public/Util.php
index 687f4e78f69..9422dbac66a 100644
--- a/lib/public/Util.php
+++ b/lib/public/Util.php
@@ -513,6 +513,11 @@ class Util {
* @deprecated 9.0.0 Use annotations based on the app framework.
*/
public static function callCheck() {
+ if(!\OC::$server->getRequest()->passesStrictCookieCheck()) {
+ header('Location: '.\OC::$WEBROOT);
+ exit();
+ }
+
if (!(\OC::$server->getRequest()->passesCSRFCheck())) {
exit();
}
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/Http/RequestTest.php b/tests/lib/AppFramework/Http/RequestTest.php
index ddc2403d866..8df81afeb3b 100644
--- a/tests/lib/AppFramework/Http/RequestTest.php
+++ b/tests/lib/AppFramework/Http/RequestTest.php
@@ -1,7 +1,7 @@
<?php
/**
* @copyright 2013 Thomas Tanghus (thomas@tanghus.net)
- * @copyright 2015 Lukas Reschke lukas@owncloud.com
+ * @copyright 2016 Lukas Reschke lukas@owncloud.com
*
* This file is licensed under the Affero General Public License version 3 or
* later.
@@ -740,15 +740,15 @@ class RequestTest extends \Test\TestCase {
*/
public function testUserAgent($testAgent, $userAgent, $matches) {
$request = new Request(
- [
- 'server' => [
- 'HTTP_USER_AGENT' => $testAgent,
- ]
- ],
- $this->secureRandom,
- $this->config,
- $this->csrfTokenManager,
- $this->stream
+ [
+ 'server' => [
+ 'HTTP_USER_AGENT' => $testAgent,
+ ]
+ ],
+ $this->secureRandom,
+ $this->config,
+ $this->csrfTokenManager,
+ $this->stream
);
$this->assertSame($matches, $request->isUserAgent($userAgent));
@@ -762,11 +762,11 @@ class RequestTest extends \Test\TestCase {
*/
public function testUndefinedUserAgent($testAgent, $userAgent, $matches) {
$request = new Request(
- [],
- $this->secureRandom,
- $this->config,
- $this->csrfTokenManager,
- $this->stream
+ [],
+ $this->secureRandom,
+ $this->config,
+ $this->csrfTokenManager,
+ $this->stream
);
$this->assertFalse($request->isUserAgent($userAgent));
@@ -1322,6 +1322,10 @@ class RequestTest extends \Test\TestCase {
'get' => [
'requesttoken' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds',
],
+ 'cookies' => [
+ 'nc_sameSiteCookiestrict' => 'true',
+ 'nc_sameSiteCookielax' => 'true',
+ ],
],
$this->secureRandom,
$this->config,
@@ -1348,6 +1352,10 @@ class RequestTest extends \Test\TestCase {
'post' => [
'requesttoken' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds',
],
+ 'cookies' => [
+ 'nc_sameSiteCookiestrict' => 'true',
+ 'nc_sameSiteCookielax' => 'true',
+ ],
],
$this->secureRandom,
$this->config,
@@ -1357,10 +1365,10 @@ class RequestTest extends \Test\TestCase {
->getMock();
$token = new CsrfToken('AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds');
$this->csrfTokenManager
- ->expects($this->once())
- ->method('isTokenValid')
- ->with($token)
- ->willReturn(true);
+ ->expects($this->once())
+ ->method('isTokenValid')
+ ->with($token)
+ ->willReturn(true);
$this->assertTrue($request->passesCSRFCheck());
}
@@ -1374,6 +1382,10 @@ class RequestTest extends \Test\TestCase {
'server' => [
'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds',
],
+ 'cookies' => [
+ 'nc_sameSiteCookiestrict' => 'true',
+ 'nc_sameSiteCookielax' => 'true',
+ ],
],
$this->secureRandom,
$this->config,
@@ -1383,14 +1395,254 @@ class RequestTest extends \Test\TestCase {
->getMock();
$token = new CsrfToken('AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds');
$this->csrfTokenManager
- ->expects($this->once())
- ->method('isTokenValid')
- ->with($token)
- ->willReturn(true);
+ ->expects($this->once())
+ ->method('isTokenValid')
+ ->with($token)
+ ->willReturn(true);
+
+ $this->assertTrue($request->passesCSRFCheck());
+ }
+
+ public function testPassesCSRFCheckWithGetAndWithoutCookies() {
+ /** @var Request $request */
+ $request = $this->getMockBuilder('\OC\AppFramework\Http\Request')
+ ->setMethods(['getScriptName'])
+ ->setConstructorArgs([
+ [
+ 'get' => [
+ 'requesttoken' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds',
+ ],
+ ],
+ $this->secureRandom,
+ $this->config,
+ $this->csrfTokenManager,
+ $this->stream
+ ])
+ ->getMock();
+ $this->csrfTokenManager
+ ->expects($this->once())
+ ->method('isTokenValid')
+ ->willReturn(true);
+
+ $this->assertTrue($request->passesCSRFCheck());
+ }
+
+ public function testPassesCSRFCheckWithPostAndWithoutCookies() {
+ /** @var Request $request */
+ $request = $this->getMockBuilder('\OC\AppFramework\Http\Request')
+ ->setMethods(['getScriptName'])
+ ->setConstructorArgs([
+ [
+ 'post' => [
+ 'requesttoken' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds',
+ ],
+ ],
+ $this->secureRandom,
+ $this->config,
+ $this->csrfTokenManager,
+ $this->stream
+ ])
+ ->getMock();
+ $this->csrfTokenManager
+ ->expects($this->once())
+ ->method('isTokenValid')
+ ->willReturn(true);
$this->assertTrue($request->passesCSRFCheck());
}
+ public function testPassesCSRFCheckWithHeaderAndWithoutCookies() {
+ /** @var Request $request */
+ $request = $this->getMockBuilder('\OC\AppFramework\Http\Request')
+ ->setMethods(['getScriptName'])
+ ->setConstructorArgs([
+ [
+ 'server' => [
+ 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds',
+ ],
+ ],
+ $this->secureRandom,
+ $this->config,
+ $this->csrfTokenManager,
+ $this->stream
+ ])
+ ->getMock();
+ $this->csrfTokenManager
+ ->expects($this->once())
+ ->method('isTokenValid')
+ ->willReturn(true);
+
+ $this->assertTrue($request->passesCSRFCheck());
+ }
+
+ public function testFailsCSRFCheckWithHeaderAndNotAllChecksPassing() {
+ /** @var Request $request */
+ $request = $this->getMockBuilder('\OC\AppFramework\Http\Request')
+ ->setMethods(['getScriptName'])
+ ->setConstructorArgs([
+ [
+ 'server' => [
+ 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds',
+ ],
+ 'cookies' => [
+ 'nc_sameSiteCookiestrict' => 'true',
+ ],
+ ],
+ $this->secureRandom,
+ $this->config,
+ $this->csrfTokenManager,
+ $this->stream
+ ])
+ ->getMock();
+ $this->csrfTokenManager
+ ->expects($this->never())
+ ->method('isTokenValid');
+
+ $this->assertFalse($request->passesCSRFCheck());
+ }
+
+ public function testPassesStrictCookieCheckWithAllCookies() {
+ /** @var Request $request */
+ $request = $this->getMockBuilder('\OC\AppFramework\Http\Request')
+ ->setMethods(['getScriptName'])
+ ->setConstructorArgs([
+ [
+ 'server' => [
+ 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds',
+ ],
+ 'cookies' => [
+ 'nc_sameSiteCookiestrict' => 'true',
+ 'nc_sameSiteCookielax' => 'true',
+ ],
+ ],
+ $this->secureRandom,
+ $this->config,
+ $this->csrfTokenManager,
+ $this->stream
+ ])
+ ->getMock();
+
+ $this->assertTrue($request->passesStrictCookieCheck());
+ }
+
+ public function testFailsSRFCheckWithPostAndWithCookies() {
+ /** @var Request $request */
+ $request = $this->getMockBuilder('\OC\AppFramework\Http\Request')
+ ->setMethods(['getScriptName'])
+ ->setConstructorArgs([
+ [
+ 'post' => [
+ 'requesttoken' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds',
+ ],
+ 'cookies' => [
+ 'foo' => 'bar',
+ ],
+ ],
+ $this->secureRandom,
+ $this->config,
+ $this->csrfTokenManager,
+ $this->stream
+ ])
+ ->getMock();
+ $this->csrfTokenManager
+ ->expects($this->never())
+ ->method('isTokenValid');
+
+ $this->assertFalse($request->passesCSRFCheck());
+ }
+
+ public function testFailStrictCookieCheckWithOnlyLaxCookie() {
+ /** @var Request $request */
+ $request = $this->getMockBuilder('\OC\AppFramework\Http\Request')
+ ->setMethods(['getScriptName'])
+ ->setConstructorArgs([
+ [
+ 'server' => [
+ 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds',
+ ],
+ 'cookies' => [
+ 'nc_sameSiteCookielax' => 'true',
+ ],
+ ],
+ $this->secureRandom,
+ $this->config,
+ $this->csrfTokenManager,
+ $this->stream
+ ])
+ ->getMock();
+
+ $this->assertFalse($request->passesStrictCookieCheck());
+ }
+
+ public function testFailStrictCookieCheckWithOnlyStrictCookie() {
+ /** @var Request $request */
+ $request = $this->getMockBuilder('\OC\AppFramework\Http\Request')
+ ->setMethods(['getScriptName'])
+ ->setConstructorArgs([
+ [
+ 'server' => [
+ 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds',
+ ],
+ 'cookies' => [
+ 'nc_sameSiteCookiestrict' => 'true',
+ ],
+ ],
+ $this->secureRandom,
+ $this->config,
+ $this->csrfTokenManager,
+ $this->stream
+ ])
+ ->getMock();
+
+ $this->assertFalse($request->passesStrictCookieCheck());
+ }
+
+ public function testPassesLaxCookieCheck() {
+ /** @var Request $request */
+ $request = $this->getMockBuilder('\OC\AppFramework\Http\Request')
+ ->setMethods(['getScriptName'])
+ ->setConstructorArgs([
+ [
+ 'server' => [
+ 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds',
+ ],
+ 'cookies' => [
+ 'nc_sameSiteCookielax' => 'true',
+ ],
+ ],
+ $this->secureRandom,
+ $this->config,
+ $this->csrfTokenManager,
+ $this->stream
+ ])
+ ->getMock();
+
+ $this->assertTrue($request->passesLaxCookieCheck());
+ }
+
+ public function testFailsLaxCookieCheckWithOnlyStrictCookie() {
+ /** @var Request $request */
+ $request = $this->getMockBuilder('\OC\AppFramework\Http\Request')
+ ->setMethods(['getScriptName'])
+ ->setConstructorArgs([
+ [
+ 'server' => [
+ 'HTTP_REQUESTTOKEN' => 'AAAHGxsTCTc3BgMQESAcNR0OAR0=:MyTotalSecretShareds',
+ ],
+ 'cookies' => [
+ 'nc_sameSiteCookiestrict' => 'true',
+ ],
+ ],
+ $this->secureRandom,
+ $this->config,
+ $this->csrfTokenManager,
+ $this->stream
+ ])
+ ->getMock();
+
+ $this->assertFalse($request->passesLaxCookieCheck());
+ }
+
/**
* @return array
*/
@@ -1426,10 +1678,10 @@ class RequestTest extends \Test\TestCase {
$token = new CsrfToken($invalidToken);
$this->csrfTokenManager
- ->expects($this->any())
- ->method('isTokenValid')
- ->with($token)
- ->willReturn(false);
+ ->expects($this->any())
+ ->method('isTokenValid')
+ ->with($token)
+ ->willReturn(false);
$this->assertFalse($request->passesCSRFCheck());
}
@@ -1449,5 +1701,4 @@ class RequestTest extends \Test\TestCase {
$this->assertFalse($request->passesCSRFCheck());
}
-
}
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/AppFramework/Middleware/Security/SecurityMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php
index a4f203bacd7..487b83c0bef 100644
--- a/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php
+++ b/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php
@@ -31,6 +31,7 @@ use OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryExcept
use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException;
use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException;
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
+use OC\Appframework\Middleware\Security\Exceptions\StrictCookieMissingException;
use OC\AppFramework\Middleware\Security\SecurityMiddleware;
use OC\AppFramework\Utility\ControllerMethodReflector;
use OC\Security\CSP\ContentSecurityPolicy;
@@ -57,28 +58,28 @@ class SecurityMiddlewareTest extends \Test\TestCase {
$this->controller = $this->getMockBuilder('OCP\AppFramework\Controller')
->disableOriginalConstructor()
- ->getMock();
+ ->getMock();
$this->reader = new ControllerMethodReflector();
$this->logger = $this->getMockBuilder(
- 'OCP\ILogger')
- ->disableOriginalConstructor()
- ->getMock();
+ 'OCP\ILogger')
+ ->disableOriginalConstructor()
+ ->getMock();
$this->navigationManager = $this->getMockBuilder(
- 'OCP\INavigationManager')
- ->disableOriginalConstructor()
- ->getMock();
+ 'OCP\INavigationManager')
+ ->disableOriginalConstructor()
+ ->getMock();
$this->urlGenerator = $this->getMockBuilder(
- 'OCP\IURLGenerator')
- ->disableOriginalConstructor()
- ->getMock();
+ 'OCP\IURLGenerator')
+ ->disableOriginalConstructor()
+ ->getMock();
$this->request = $this->getMockBuilder(
- 'OCP\IRequest')
- ->disableOriginalConstructor()
- ->getMock();
+ 'OCP\IRequest')
+ ->disableOriginalConstructor()
+ ->getMock();
$this->contentSecurityPolicyManager = $this->getMockBuilder(
- 'OC\Security\CSP\ContentSecurityPolicyManager')
- ->disableOriginalConstructor()
- ->getMock();
+ 'OC\Security\CSP\ContentSecurityPolicyManager')
+ ->disableOriginalConstructor()
+ ->getMock();
$this->middleware = $this->getMiddleware(true, true);
$this->secException = new SecurityException('hey', false);
$this->secAjaxException = new SecurityException('hey', true);
@@ -211,8 +212,8 @@ class SecurityMiddlewareTest extends \Test\TestCase {
*/
public function testNoChecks(){
$this->request->expects($this->never())
- ->method('passesCSRFCheck')
- ->will($this->returnValue(false));
+ ->method('passesCSRFCheck')
+ ->will($this->returnValue(false));
$sec = $this->getMiddleware(false, false);
@@ -256,7 +257,9 @@ class SecurityMiddlewareTest extends \Test\TestCase {
$this->request->expects($this->once())
->method('passesCSRFCheck')
->will($this->returnValue(false));
-
+ $this->request->expects($this->once())
+ ->method('passesStrictCookieCheck')
+ ->will($this->returnValue(true));
$this->reader->reflect(__CLASS__, __FUNCTION__);
$this->middleware->beforeController(__CLASS__, __FUNCTION__);
}
@@ -275,19 +278,81 @@ class SecurityMiddlewareTest extends \Test\TestCase {
$this->middleware->beforeController(__CLASS__, __FUNCTION__);
}
+ /**
+ * @PublicPage
+ */
+ public function testPassesCsrfCheck(){
+ $this->request->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->will($this->returnValue(true));
+ $this->request->expects($this->once())
+ ->method('passesStrictCookieCheck')
+ ->will($this->returnValue(true));
+
+ $this->reader->reflect(__CLASS__, __FUNCTION__);
+ $this->middleware->beforeController(__CLASS__, __FUNCTION__);
+ }
/**
* @PublicPage
+ * @expectedException \OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException
*/
public function testFailCsrfCheck(){
$this->request->expects($this->once())
->method('passesCSRFCheck')
+ ->will($this->returnValue(false));
+ $this->request->expects($this->once())
+ ->method('passesStrictCookieCheck')
->will($this->returnValue(true));
$this->reader->reflect(__CLASS__, __FUNCTION__);
$this->middleware->beforeController(__CLASS__, __FUNCTION__);
}
+ /**
+ * @PublicPage
+ * @StrictCookieRequired
+ * @expectedException \OC\Appframework\Middleware\Security\Exceptions\StrictCookieMissingException
+ */
+ public function testStrictCookieRequiredCheck() {
+ $this->request->expects($this->never())
+ ->method('passesCSRFCheck');
+ $this->request->expects($this->once())
+ ->method('passesStrictCookieCheck')
+ ->will($this->returnValue(false));
+
+ $this->reader->reflect(__CLASS__, __FUNCTION__);
+ $this->middleware->beforeController(__CLASS__, __FUNCTION__);
+ }
+
+
+ /**
+ * @PublicPage
+ * @NoCSRFRequired
+ */
+ public function testNoStrictCookieRequiredCheck() {
+ $this->request->expects($this->never())
+ ->method('passesStrictCookieCheck')
+ ->will($this->returnValue(false));
+
+ $this->reader->reflect(__CLASS__, __FUNCTION__);
+ $this->middleware->beforeController(__CLASS__, __FUNCTION__);
+ }
+
+ /**
+ * @PublicPage
+ * @NoCSRFRequired
+ * @StrictCookieRequired
+ */
+ public function testPassesStrictCookieRequiredCheck() {
+ $this->request
+ ->expects($this->once())
+ ->method('passesStrictCookieCheck')
+ ->willReturn(true);
+
+ $this->reader->reflect(__CLASS__, __FUNCTION__);
+ $this->middleware->beforeController(__CLASS__, __FUNCTION__);
+ }
/**
* @NoCSRFRequired
@@ -331,41 +396,64 @@ class SecurityMiddlewareTest extends \Test\TestCase {
public function testAfterExceptionReturnsRedirectForNotLoggedInUser() {
$this->request = new Request(
- [
- 'server' =>
- [
- 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
- 'REQUEST_URI' => 'owncloud/index.php/apps/specialapp'
- ]
- ],
- $this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(),
- $this->getMockBuilder('\OCP\IConfig')->getMock()
+ [
+ 'server' =>
+ [
+ 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ 'REQUEST_URI' => 'owncloud/index.php/apps/specialapp'
+ ]
+ ],
+ $this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(),
+ $this->getMockBuilder('\OCP\IConfig')->getMock()
);
$this->middleware = $this->getMiddleware(false, false);
$this->urlGenerator
- ->expects($this->once())
- ->method('linkToRoute')
- ->with(
- 'core.login.showLoginForm',
- [
- 'redirect_url' => 'owncloud%2Findex.php%2Fapps%2Fspecialapp',
- ]
- )
- ->will($this->returnValue('http://localhost/index.php/login?redirect_url=owncloud%2Findex.php%2Fapps%2Fspecialapp'));
+ ->expects($this->once())
+ ->method('linkToRoute')
+ ->with(
+ 'core.login.showLoginForm',
+ [
+ 'redirect_url' => 'owncloud%2Findex.php%2Fapps%2Fspecialapp',
+ ]
+ )
+ ->will($this->returnValue('http://localhost/index.php/login?redirect_url=owncloud%2Findex.php%2Fapps%2Fspecialapp'));
$this->logger
- ->expects($this->once())
- ->method('debug')
- ->with('Current user is not logged in');
+ ->expects($this->once())
+ ->method('debug')
+ ->with('Current user is not logged in');
$response = $this->middleware->afterException(
- $this->controller,
- 'test',
- new NotLoggedInException()
+ $this->controller,
+ 'test',
+ new NotLoggedInException()
);
-
$expected = new RedirectResponse('http://localhost/index.php/login?redirect_url=owncloud%2Findex.php%2Fapps%2Fspecialapp');
$this->assertEquals($expected , $response);
}
+ public function testAfterExceptionRedirectsToWebRootAfterStrictCookieFail() {
+ $this->request = new Request(
+ [
+ 'server' => [
+ 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ 'REQUEST_URI' => 'owncloud/index.php/apps/specialapp',
+ ],
+ ],
+ $this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(),
+ $this->getMockBuilder('\OCP\IConfig')->getMock()
+ );
+
+ $this->middleware = $this->getMiddleware(false, false);
+ $response = $this->middleware->afterException(
+ $this->controller,
+ 'test',
+ new StrictCookieMissingException()
+ );
+
+ $expected = new RedirectResponse(\OC::$WEBROOT);
+ $this->assertEquals($expected , $response);
+ }
+
+
/**
* @return array
*/
@@ -389,36 +477,34 @@ class SecurityMiddlewareTest extends \Test\TestCase {
*/
public function testAfterExceptionReturnsTemplateResponse(SecurityException $exception) {
$this->request = new Request(
- [
- 'server' =>
- [
- 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
- 'REQUEST_URI' => 'owncloud/index.php/apps/specialapp'
- ]
- ],
- $this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(),
- $this->getMockBuilder('\OCP\IConfig')->getMock()
+ [
+ 'server' =>
+ [
+ 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ 'REQUEST_URI' => 'owncloud/index.php/apps/specialapp'
+ ]
+ ],
+ $this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(),
+ $this->getMockBuilder('\OCP\IConfig')->getMock()
);
$this->middleware = $this->getMiddleware(false, false);
$this->logger
- ->expects($this->once())
- ->method('debug')
- ->with($exception->getMessage());
+ ->expects($this->once())
+ ->method('debug')
+ ->with($exception->getMessage());
$response = $this->middleware->afterException(
- $this->controller,
- 'test',
- $exception
+ $this->controller,
+ 'test',
+ $exception
);
-
$expected = new TemplateResponse('core', '403', ['file' => $exception->getMessage()], 'guest');
$expected->setStatus($exception->getCode());
$this->assertEquals($expected , $response);
}
-
public function testAfterAjaxExceptionReturnsJSONError(){
$response = $this->middleware->afterException($this->controller, 'test',
- $this->secAjaxException);
+ $this->secAjaxException);
$this->assertTrue($response instanceof JSONResponse);
}
@@ -440,10 +526,10 @@ class SecurityMiddlewareTest extends \Test\TestCase {
->method('getDefaultPolicy')
->willReturn($defaultPolicy);
$this->contentSecurityPolicyManager
- ->expects($this->once())
- ->method('mergePolicies')
- ->with($defaultPolicy, $currentPolicy)
- ->willReturn($mergedPolicy);
+ ->expects($this->once())
+ ->method('mergePolicies')
+ ->with($defaultPolicy, $currentPolicy)
+ ->willReturn($mergedPolicy);
$response->expects($this->once())
->method('setContentSecurityPolicy')
->with($mergedPolicy);
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);
}