diff options
58 files changed, 2605 insertions, 169 deletions
diff --git a/apps/dav/appinfo/application.php b/apps/dav/appinfo/application.php index 28b9a833456..7a201e1dd78 100644 --- a/apps/dav/appinfo/application.php +++ b/apps/dav/appinfo/application.php @@ -20,6 +20,7 @@ */ namespace OCA\Dav\AppInfo; +use OCA\DAV\CalDAV\BirthdayService; use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CardDAV\CardDavBackend; use OCA\DAV\CardDAV\ContactsManager; @@ -34,6 +35,7 @@ use \OCP\AppFramework\App; use OCP\AppFramework\IAppContainer; use OCP\Contacts\IManager; use OCP\IUser; +use Symfony\Component\EventDispatcher\GenericEvent; class Application extends App { @@ -74,12 +76,12 @@ class Application extends App { $container->registerService('CardDavBackend', function($c) { /** @var IAppContainer $c */ $db = $c->getServer()->getDatabaseConnection(); - $logger = $c->getServer()->getLogger(); + $dispatcher = $c->getServer()->getEventDispatcher(); $principal = new \OCA\DAV\Connector\Sabre\Principal( $c->getServer()->getUserManager(), $c->getServer()->getGroupManager() ); - return new CardDavBackend($db, $principal, $logger); + return new CardDavBackend($db, $principal, $dispatcher); }); $container->registerService('CalDavBackend', function($c) { @@ -109,6 +111,15 @@ class Application extends App { $c->query('CalDavBackend') ); }); + + $container->registerService('BirthdayService', function($c) { + /** @var IAppContainer $c */ + return new BirthdayService( + $c->query('CalDavBackend'), + $c->query('CardDavBackend') + ); + + }); } /** @@ -125,6 +136,30 @@ class Application extends App { /** @var HookManager $hm */ $hm = $this->getContainer()->query('HookManager'); $hm->setup(); + + $listener = function($event) { + if ($event instanceof GenericEvent) { + $b = $this->getContainer()->query('BirthdayService'); + $b->onCardChanged( + $event->getArgument('addressBookId'), + $event->getArgument('cardUri'), + $event->getArgument('cardData') + ); + } + }; + + $dispatcher = $this->getContainer()->getServer()->getEventDispatcher(); + $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::createCard', $listener); + $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::updateCard', $listener); + $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', function($event) { + if ($event instanceof GenericEvent) { + $b = $this->getContainer()->query('BirthdayService'); + $b->onCardDeleted( + $event->getArgument('addressBookId'), + $event->getArgument('cardUri') + ); + } + }); } public function getSyncService() { diff --git a/apps/dav/appinfo/register_command.php b/apps/dav/appinfo/register_command.php index 4981cab9264..e07f6b4a25b 100644 --- a/apps/dav/appinfo/register_command.php +++ b/apps/dav/appinfo/register_command.php @@ -24,21 +24,21 @@ use OCA\DAV\Command\CreateAddressBook; use OCA\DAV\Command\CreateCalendar; use OCA\Dav\Command\MigrateAddressbooks; use OCA\Dav\Command\MigrateCalendars; +use OCA\DAV\Command\SyncBirthdayCalendar; use OCA\DAV\Command\SyncSystemAddressBook; -$config = \OC::$server->getConfig(); $dbConnection = \OC::$server->getDatabaseConnection(); $userManager = OC::$server->getUserManager(); $groupManager = OC::$server->getGroupManager(); $config = \OC::$server->getConfig(); -$logger = \OC::$server->getLogger(); $app = new Application(); /** @var Symfony\Component\Console\Application $application */ -$application->add(new CreateAddressBook($userManager, $groupManager, $dbConnection, $logger)); $application->add(new CreateCalendar($userManager, $groupManager, $dbConnection)); +$application->add(new CreateAddressBook($userManager, $app->getContainer()->query('CardDavBackend'))); $application->add(new SyncSystemAddressBook($app->getSyncService())); +$application->add(new SyncBirthdayCalendar($userManager, $app->getContainer()->query('BirthdayService'))); // the occ tool is *for now* only available in debug mode for developers to test if ($config->getSystemValue('debug', false)){ diff --git a/apps/dav/appinfo/v1/caldav.php b/apps/dav/appinfo/v1/caldav.php index 333e8bbb3c4..3e56e3d0e81 100644 --- a/apps/dav/appinfo/v1/caldav.php +++ b/apps/dav/appinfo/v1/caldav.php @@ -62,7 +62,10 @@ $server->setBaseUri($baseuri); $server->addPlugin(new MaintenancePlugin()); $server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend, 'ownCloud')); $server->addPlugin(new \Sabre\CalDAV\Plugin()); -$server->addPlugin(new \Sabre\DAVACL\Plugin()); + +$acl = new \OCA\DAV\Connector\LegacyDAVACL(); +$server->addPlugin($acl); + $server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin()); $server->addPlugin(new ExceptionLoggerPlugin('caldav', \OC::$server->getLogger())); diff --git a/apps/dav/appinfo/v1/carddav.php b/apps/dav/appinfo/v1/carddav.php index 54f0d259bb9..4a3f98475cd 100644 --- a/apps/dav/appinfo/v1/carddav.php +++ b/apps/dav/appinfo/v1/carddav.php @@ -22,7 +22,6 @@ // Backends use OCA\DAV\CardDAV\AddressBookRoot; use OCA\DAV\CardDAV\CardDavBackend; -use OCA\DAV\Connector\Sabre\AppEnabledPlugin; use OCA\DAV\Connector\Sabre\Auth; use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin; use OCA\DAV\Connector\Sabre\MaintenancePlugin; @@ -63,7 +62,10 @@ $server->setBaseUri($baseuri); $server->addPlugin(new MaintenancePlugin()); $server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend, 'ownCloud')); $server->addPlugin(new Plugin()); -$server->addPlugin(new \Sabre\DAVACL\Plugin()); + +$acl = new \OCA\DAV\Connector\LegacyDAVACL(); +$server->addPlugin($acl); + $server->addPlugin(new \Sabre\CardDAV\VCFExportPlugin()); $server->addPlugin(new ExceptionLoggerPlugin('carddav', \OC::$server->getLogger())); diff --git a/apps/dav/command/createaddressbook.php b/apps/dav/command/createaddressbook.php index 3d99afd4ba3..48302a2b439 100644 --- a/apps/dav/command/createaddressbook.php +++ b/apps/dav/command/createaddressbook.php @@ -36,33 +36,21 @@ use Symfony\Component\Console\Output\OutputInterface; class CreateAddressBook extends Command { /** @var IUserManager */ - protected $userManager; + private $userManager; - /** @var \OCP\IDBConnection */ - protected $dbConnection; - - /** @var ILogger */ - private $logger; - - /** @var IGroupManager $groupManager */ - private $groupManager; + /** @var CardDavBackend */ + private $cardDavBackend; /** * @param IUserManager $userManager - * @param IDBConnection $dbConnection - * @param IConfig $config - * @param ILogger $logger + * @param CardDavBackend $cardDavBackend */ function __construct(IUserManager $userManager, - IGroupManager $groupManager, - IDBConnection $dbConnection, - ILogger $logger + CardDavBackend $cardDavBackend ) { parent::__construct(); $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->dbConnection = $dbConnection; - $this->logger = $logger; + $this->cardDavBackend = $cardDavBackend; } protected function configure() { @@ -82,13 +70,8 @@ class CreateAddressBook extends Command { if (!$this->userManager->userExists($user)) { throw new \InvalidArgumentException("User <$user> in unknown."); } - $principalBackend = new Principal( - $this->userManager, - $this->groupManager - ); $name = $input->getArgument('name'); - $carddav = new CardDavBackend($this->dbConnection, $principalBackend, $this->logger); - $carddav->createAddressBook("principals/users/$user", $name, []); + $this->cardDavBackend->createAddressBook("principals/users/$user", $name, []); } } diff --git a/apps/dav/command/syncbirthdaycalendar.php b/apps/dav/command/syncbirthdaycalendar.php new file mode 100644 index 00000000000..66ab540b9ad --- /dev/null +++ b/apps/dav/command/syncbirthdaycalendar.php @@ -0,0 +1,85 @@ +<?php +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\BirthdayService; +use OCP\IUser; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class SyncBirthdayCalendar extends Command { + + /** @var BirthdayService */ + private $birthdayService; + + /** @var IUserManager */ + private $userManager; + + /** + * @param IUserManager $userManager + * @param BirthdayService $birthdayService + */ + function __construct(IUserManager $userManager, BirthdayService $birthdayService) { + parent::__construct(); + $this->birthdayService = $birthdayService; + $this->userManager = $userManager; + } + + protected function configure() { + $this + ->setName('dav:sync-birthday-calendar') + ->setDescription('Synchronizes the birthday calendar') + ->addArgument('user', + InputArgument::OPTIONAL, + 'User for whom the birthday calendar will be synchronized'); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + */ + protected function execute(InputInterface $input, OutputInterface $output) { + if ($input->hasArgument('user')) { + $user = $input->getArgument('user'); + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User <$user> in unknown."); + } + $output->writeln("Start birthday calendar sync for $user"); + $this->birthdayService->syncUser($user); + return; + } + $output->writeln("Start birthday calendar sync for all users ..."); + $p = new ProgressBar($output); + $p->start(); + $this->userManager->callForAllUsers(function($user) use ($p) { + $p->advance(); + /** @var IUser $user */ + $this->birthdayService->syncUser($user->getUID()); + }); + + $p->finish(); + $output->writeln(''); + } +} diff --git a/apps/dav/command/syncsystemaddressbook.php b/apps/dav/command/syncsystemaddressbook.php index b83a37131c3..b62a42d7b90 100644 --- a/apps/dav/command/syncsystemaddressbook.php +++ b/apps/dav/command/syncsystemaddressbook.php @@ -34,7 +34,6 @@ class SyncSystemAddressBook extends Command { private $syncService; /** - * @param IUserManager $userManager * @param SyncService $syncService */ function __construct(SyncService $syncService) { diff --git a/apps/dav/lib/caldav/birthdayservice.php b/apps/dav/lib/caldav/birthdayservice.php new file mode 100644 index 00000000000..3b0a2a10e1c --- /dev/null +++ b/apps/dav/lib/caldav/birthdayservice.php @@ -0,0 +1,187 @@ +<?php +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\DAV\CalDAV; + +use Exception; +use OCA\DAV\CardDAV\CardDavBackend; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Reader; + +class BirthdayService { + + const BIRTHDAY_CALENDAR_URI = 'contact_birthdays'; + + /** + * BirthdayService constructor. + * + * @param CalDavBackend $calDavBackEnd + * @param CardDavBackend $cardDavBackEnd + */ + public function __construct($calDavBackEnd, $cardDavBackEnd) { + $this->calDavBackEnd = $calDavBackEnd; + $this->cardDavBackEnd = $cardDavBackEnd; + } + + /** + * @param int $addressBookId + * @param string $cardUri + * @param string $cardData + */ + public function onCardChanged($addressBookId, $cardUri, $cardData) { + + $book = $this->cardDavBackEnd->getAddressBookById($addressBookId); + $principalUri = $book['principaluri']; + $calendarUri = self::BIRTHDAY_CALENDAR_URI; + $calendar = $this->ensureCalendarExists($principalUri, $calendarUri, []); + $objectUri = $book['uri'] . '-' . $cardUri. '.ics'; + $calendarData = $this->buildBirthdayFromContact($cardData); + $existing = $this->calDavBackEnd->getCalendarObject($calendar['id'], $objectUri); + if (is_null($calendarData)) { + if (!is_null($existing)) { + $this->calDavBackEnd->deleteCalendarObject($calendar['id'], $objectUri); + } + } else { + if (is_null($existing)) { + $this->calDavBackEnd->createCalendarObject($calendar['id'], $objectUri, $calendarData->serialize()); + } else { + if ($this->birthdayEvenChanged($existing['calendardata'], $calendarData)) { + $this->calDavBackEnd->updateCalendarObject($calendar['id'], $objectUri, $calendarData->serialize()); + } + } + } + } + + /** + * @param int $addressBookId + * @param string $cardUri + */ + public function onCardDeleted($addressBookId, $cardUri) { + $book = $this->cardDavBackEnd->getAddressBookById($addressBookId); + $principalUri = $book['principaluri']; + $calendarUri = self::BIRTHDAY_CALENDAR_URI; + $calendar = $this->ensureCalendarExists($principalUri, $calendarUri, []); + $objectUri = $book['uri'] . '-' . $cardUri. '.ics'; + $this->calDavBackEnd->deleteCalendarObject($calendar['id'], $objectUri); + } + + /** + * @param string $principal + * @param string $id + * @param array $properties + * @return array|null + * @throws \Sabre\DAV\Exception\BadRequest + */ + public function ensureCalendarExists($principal, $id, $properties) { + $book = $this->calDavBackEnd->getCalendarByUri($principal, $id); + if (!is_null($book)) { + return $book; + } + $this->calDavBackEnd->createCalendar($principal, $id, $properties); + + return $this->calDavBackEnd->getCalendarByUri($principal, $id); + } + + /** + * @param string $cardData + * @return null|VCalendar + */ + public function buildBirthdayFromContact($cardData) { + if (empty($cardData)) { + return null; + } + try { + $doc = Reader::read($cardData); + } catch (Exception $e) { + return null; + } + + if (!isset($doc->BDAY)) { + return null; + } + $birthday = $doc->BDAY; + if (!(string)$birthday) { + return null; + } + $title = str_replace('{name}', + strtr((string)$doc->FN, array('\,' => ',', '\;' => ';')), + '{name}\'s Birthday' + ); + try { + $date = new \DateTime($birthday); + } catch (Exception $e) { + return null; + } + $vCal = new VCalendar(); + $vCal->VERSION = '2.0'; + $vEvent = $vCal->createComponent('VEVENT'); + $vEvent->add('DTSTART'); + $vEvent->DTSTART->setDateTime( + $date + ); + $vEvent->DTSTART['VALUE'] = 'DATE'; + $vEvent->add('DTEND'); + $date->add(new \DateInterval('P1D')); + $vEvent->DTEND->setDateTime( + $date + ); + $vEvent->DTEND['VALUE'] = 'DATE'; + $vEvent->{'UID'} = $doc->UID; + $vEvent->{'RRULE'} = 'FREQ=YEARLY'; + $vEvent->{'SUMMARY'} = $title . ' (' . $date->format('Y') . ')'; + $vEvent->{'TRANSP'} = 'TRANSPARENT'; + $vCal->add($vEvent); + return $vCal; + } + + /** + * @param string $user + */ + public function syncUser($user) { + $books = $this->cardDavBackEnd->getAddressBooksForUser('principals/users/'.$user); + foreach($books as $book) { + $cards = $this->cardDavBackEnd->getCards($book['id']); + foreach($cards as $card) { + $this->onCardChanged($book['id'], $card['uri'], $card['carddata']); + } + } + } + + /** + * @param string $existingCalendarData + * @param VCalendar $newCalendarData + * @return bool + */ + public function birthdayEvenChanged($existingCalendarData, $newCalendarData) { + try { + $existingBirthday = Reader::read($existingCalendarData); + } catch (Exception $ex) { + return true; + } + if ($newCalendarData->VEVENT->DTSTART->getValue() !== $existingBirthday->VEVENT->DTSTART->getValue() || + $newCalendarData->VEVENT->SUMMARY->getValue() !== $existingBirthday->VEVENT->SUMMARY->getValue() + ) { + return true; + } + return false; + } + +} diff --git a/apps/dav/lib/caldav/caldavbackend.php b/apps/dav/lib/caldav/caldavbackend.php index 775612487f9..7f6810fb1e2 100644 --- a/apps/dav/lib/caldav/caldavbackend.php +++ b/apps/dav/lib/caldav/caldavbackend.php @@ -138,6 +138,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @return array */ function getCalendarsForUser($principalUri) { + $principalUri = $this->convertPrincipal($principalUri, true); $fields = array_values($this->propertyMap); $fields[] = 'id'; $fields[] = 'uri'; @@ -164,7 +165,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendar = [ 'id' => $row['id'], 'uri' => $row['uri'], - 'principaluri' => $row['principaluri'], + 'principaluri' => $this->convertPrincipal($row['principaluri'], false), '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), @@ -235,6 +236,11 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return array_values($calendars); } + /** + * @param string $principal + * @param string $uri + * @return array|null + */ public function getCalendarByUri($principal, $uri) { $fields = array_values($this->propertyMap); $fields[] = 'id'; @@ -1088,7 +1094,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $newValues = []; foreach($mutations as $propertyName=>$propertyValue) { - if ($propertyName === '{http://calendarserver.org/ns/}source') { + if ($propertyName === '{http://calendarserver.org/ns/}source') { $newValues['source'] = $propertyValue->getHref(); } else { $fieldName = $this->subscriptionPropertyMap[$propertyName]; @@ -1367,4 +1373,15 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription public function applyShareAcl($resourceId, $acl) { return $this->sharingBackend->applyShareAcl($resourceId, $acl); } + + private function convertPrincipal($principalUri, $toV2) { + if ($this->principalBackend->getPrincipalPrefix() === 'principals') { + list(, $name) = URLUtil::splitPath($principalUri); + if ($toV2 === true) { + return "principals/users/$name"; + } + return "principals/$name"; + } + return $principalUri; + } } diff --git a/apps/dav/lib/caldav/calendar.php b/apps/dav/lib/caldav/calendar.php index 8ed5b6563d0..6b34d570eb3 100644 --- a/apps/dav/lib/caldav/calendar.php +++ b/apps/dav/lib/caldav/calendar.php @@ -80,6 +80,10 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IShareable { } function delete() { + if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI) { + throw new Forbidden(); + } + if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) { $principal = 'principal:' . parent::getOwner(); $shares = $this->getShares(); diff --git a/apps/dav/lib/carddav/carddavbackend.php b/apps/dav/lib/carddav/carddavbackend.php index 78706ae6bff..56fa652d798 100644 --- a/apps/dav/lib/carddav/carddavbackend.php +++ b/apps/dav/lib/carddav/carddavbackend.php @@ -37,6 +37,8 @@ use Sabre\DAV\Exception\BadRequest; use Sabre\HTTP\URLUtil; use Sabre\VObject\Component\VCard; use Sabre\VObject\Reader; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\GenericEvent; class CardDavBackend implements BackendInterface, SyncSupport { @@ -64,15 +66,22 @@ class CardDavBackend implements BackendInterface, SyncSupport { const ACCESS_READ_WRITE = 2; const ACCESS_READ = 3; + /** @var EventDispatcherInterface */ + private $dispatcher; + /** * CardDavBackend constructor. * * @param IDBConnection $db * @param Principal $principalBackend + * @param EventDispatcherInterface $dispatcher */ - public function __construct(IDBConnection $db, Principal $principalBackend) { + public function __construct(IDBConnection $db, + Principal $principalBackend, + $dispatcher ) { $this->db = $db; $this->principalBackend = $principalBackend; + $this->dispatcher = $dispatcher; $this->sharingBackend = new Backend($this->db, $principalBackend, 'addressbook'); } @@ -492,6 +501,14 @@ class CardDavBackend implements BackendInterface, SyncSupport { $this->addChange($addressBookId, $cardUri, 1); $this->updateProperties($addressBookId, $cardUri, $cardData); + if (!is_null($this->dispatcher)) { + $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard', + new GenericEvent(null, [ + 'addressBookId' => $addressBookId, + 'cardUri' => $cardUri, + 'cardData' => $cardData])); + } + return '"' . $etag . '"'; } @@ -536,6 +553,14 @@ class CardDavBackend implements BackendInterface, SyncSupport { $this->addChange($addressBookId, $cardUri, 2); $this->updateProperties($addressBookId, $cardUri, $cardData); + if (!is_null($this->dispatcher)) { + $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard', + new GenericEvent(null, [ + 'addressBookId' => $addressBookId, + 'cardUri' => $cardUri, + 'cardData' => $cardData])); + } + return '"' . $etag . '"'; } @@ -560,6 +585,13 @@ class CardDavBackend implements BackendInterface, SyncSupport { $this->addChange($addressBookId, $cardUri, 3); + if (!is_null($this->dispatcher)) { + $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', + new GenericEvent(null, [ + 'addressBookId' => $addressBookId, + 'cardUri' => $cardUri])); + } + if ($ret === 1) { if ($cardId !== null) { $this->purgeProperties($addressBookId, $cardId); diff --git a/apps/dav/lib/connector/legacydavacl.php b/apps/dav/lib/connector/legacydavacl.php new file mode 100644 index 00000000000..149bd85e4be --- /dev/null +++ b/apps/dav/lib/connector/legacydavacl.php @@ -0,0 +1,69 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\DAV\Connector; + + +use Sabre\HTTP\URLUtil; + +class LegacyDAVACL extends \Sabre\DAVACL\Plugin { + + /** + * Converts the v1 principal `principal/<username>` to the new v2 + * `principal/users/<username>` which is required for permission checks + * + * @inheritdoc + */ + function getCurrentUserPrincipal() { + $principalV1 = parent::getCurrentUserPrincipal(); + if (is_null($principalV1)) { + return $principalV1; + } + return $this->convertPrincipal($principalV1, true); + } + + + /** + * @inheritdoc + */ + function getCurrentUserPrincipals() { + $principalV2 = $this->getCurrentUserPrincipal(); + + if (is_null($principalV2)) return []; + + $principalV1 = $this->convertPrincipal($principalV2, false); + return array_merge( + [ + $principalV2, + $principalV1 + ], + $this->getPrincipalMembership($principalV1) + ); + } + + private function convertPrincipal($principal, $toV2) { + list(, $name) = URLUtil::splitPath($principal); + if ($toV2) { + return "principals/users/$name"; + } + return "principals/$name"; + } +} diff --git a/apps/dav/lib/connector/sabre/exceptionloggerplugin.php b/apps/dav/lib/connector/sabre/exceptionloggerplugin.php index b514a917556..1052c56e968 100644 --- a/apps/dav/lib/connector/sabre/exceptionloggerplugin.php +++ b/apps/dav/lib/connector/sabre/exceptionloggerplugin.php @@ -94,6 +94,8 @@ class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin { $message = "HTTP/1.1 {$ex->getHTTPCode()} $message"; } + $user = \OC_User::getUser(); + $exception = [ 'Message' => $message, 'Exception' => $exceptionClass, @@ -101,6 +103,7 @@ class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin { 'Trace' => $ex->getTraceAsString(), 'File' => $ex->getFile(), 'Line' => $ex->getLine(), + 'User' => $user, ]; $this->logger->log($level, 'Exception: ' . json_encode($exception), ['app' => $this->appName]); } diff --git a/apps/dav/lib/connector/sabre/filesplugin.php b/apps/dav/lib/connector/sabre/filesplugin.php index 2e913ee1077..eb9116d219b 100644 --- a/apps/dav/lib/connector/sabre/filesplugin.php +++ b/apps/dav/lib/connector/sabre/filesplugin.php @@ -193,11 +193,13 @@ class FilesPlugin extends \Sabre\DAV\ServerPlugin { // adds a 'Content-Disposition: attachment' header $response->addHeader('Content-Disposition', 'attachment'); - //Add OC-Checksum header - /** @var $node File */ - $checksum = $node->getChecksum(); - if ($checksum !== null) { - $response->addHeader('OC-Checksum', $checksum); + if ($node instanceof \OCA\DAV\Connector\Sabre\File) { + //Add OC-Checksum header + /** @var $node File */ + $checksum = $node->getChecksum(); + if ($checksum !== null) { + $response->addHeader('OC-Checksum', $checksum); + } } } diff --git a/apps/dav/lib/rootcollection.php b/apps/dav/lib/rootcollection.php index 2a8f63a2270..b3648e7ba38 100644 --- a/apps/dav/lib/rootcollection.php +++ b/apps/dav/lib/rootcollection.php @@ -36,6 +36,7 @@ class RootCollection extends SimpleCollection { public function __construct() { $config = \OC::$server->getConfig(); $db = \OC::$server->getDatabaseConnection(); + $dispatcher = \OC::$server->getEventDispatcher(); $userPrincipalBackend = new Principal( \OC::$server->getUserManager(), \OC::$server->getGroupManager() @@ -79,11 +80,11 @@ class RootCollection extends SimpleCollection { \OC::$server->getLogger() ); - $usersCardDavBackend = new CardDavBackend($db, $userPrincipalBackend); + $usersCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, $dispatcher); $usersAddressBookRoot = new AddressBookRoot($userPrincipalBackend, $usersCardDavBackend, 'principals/users'); $usersAddressBookRoot->disableListing = $disableListing; - $systemCardDavBackend = new CardDavBackend($db, $userPrincipalBackend); + $systemCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, $dispatcher); $systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, 'principals/system'); $systemAddressBookRoot->disableListing = $disableListing; diff --git a/apps/dav/lib/server.php b/apps/dav/lib/server.php index ed1ee684049..74be318fe5e 100644 --- a/apps/dav/lib/server.php +++ b/apps/dav/lib/server.php @@ -26,6 +26,7 @@ use OCA\DAV\CalDAV\Schedule\IMipPlugin; use OCA\DAV\Connector\FedAuth; use OCA\DAV\Connector\Sabre\Auth; use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin; +use OCA\DAV\Connector\Sabre\FilesPlugin; use OCA\DAV\Files\CustomPropertiesBackend; use OCP\IRequest; use OCP\SabrePluginEvent; @@ -92,7 +93,11 @@ class Server { $this->server->addPlugin(new \OCA\DAV\CardDAV\Plugin()); // system tags plugins - $this->server->addPlugin(new \OCA\DAV\SystemTag\SystemTagPlugin(\OC::$server->getSystemTagManager())); + $this->server->addPlugin(new \OCA\DAV\SystemTag\SystemTagPlugin( + \OC::$server->getSystemTagManager(), + \OC::$server->getGroupManager(), + \OC::$server->getUserSession() + )); // comments plugin $this->server->addPlugin(new \OCA\DAV\Comments\CommentsPlugin( @@ -127,6 +132,9 @@ class Server { // custom properties plugin must be the last one $user = \OC::$server->getUserSession()->getUser(); if (!is_null($user)) { + $view = \OC\Files\Filesystem::getView(); + $this->server->addPlugin(new FilesPlugin($this->server->tree, $view)); + $this->server->addPlugin( new \Sabre\DAV\PropertyStorage\Plugin( new CustomPropertiesBackend( diff --git a/apps/dav/lib/systemtag/systemtagnode.php b/apps/dav/lib/systemtag/systemtagnode.php index ecdb39a762c..7a47a752ad0 100644 --- a/apps/dav/lib/systemtag/systemtagnode.php +++ b/apps/dav/lib/systemtag/systemtagnode.php @@ -103,6 +103,7 @@ class SystemTagNode implements \Sabre\DAV\INode { * @param bool $userVisible user visible * @param bool $userAssignable user assignable * @throws NotFound whenever the given tag id does not exist + * @throws Forbidden whenever there is no permission to update said tag * @throws Conflict whenever a tag already exists with the given attributes */ public function update($name, $userVisible, $userAssignable) { diff --git a/apps/dav/lib/systemtag/systemtagplugin.php b/apps/dav/lib/systemtag/systemtagplugin.php index 3348b431c47..7da24ba7cf8 100644 --- a/apps/dav/lib/systemtag/systemtagplugin.php +++ b/apps/dav/lib/systemtag/systemtagplugin.php @@ -21,6 +21,8 @@ */ namespace OCA\DAV\SystemTag; +use OCP\IGroupManager; +use OCP\IUserSession; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\PropFind; use Sabre\DAV\PropPatch; @@ -61,12 +63,26 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { protected $tagManager; /** - * System tags plugin - * + * @var IUserSession + */ + protected $userSession; + + /** + * @var IGroupManager + */ + protected $groupManager; + + /** * @param ISystemTagManager $tagManager tag manager + * @param IGroupManager $groupManager + * @param IUserSession $userSession */ - public function __construct(ISystemTagManager $tagManager) { + public function __construct(ISystemTagManager $tagManager, + IGroupManager $groupManager, + IUserSession $userSession) { $this->tagManager = $tagManager; + $this->userSession = $userSession; + $this->groupManager = $groupManager; } /** @@ -163,6 +179,13 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { if (isset($data['userAssignable'])) { $userAssignable = (bool)$data['userAssignable']; } + + if($userVisible === false || $userAssignable === false) { + if(!$this->userSession->isLoggedIn() || !$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) { + throw new BadRequest('Not sufficient permissions'); + } + } + try { return $this->tagManager->createTag($tagName, $userVisible, $userAssignable); } catch (TagAlreadyExistsException $e) { diff --git a/apps/dav/tests/travis/caldav/script.sh b/apps/dav/tests/travis/caldav/script.sh index aa5fc732922..7259372567c 100644 --- a/apps/dav/tests/travis/caldav/script.sh +++ b/apps/dav/tests/travis/caldav/script.sh @@ -10,6 +10,8 @@ sleep 30 # run the tests cd "$SCRIPTPATH/CalDAVTester" PYTHONPATH="$SCRIPTPATH/pycalendar/src" python testcaldav.py --print-details-onfail --basedir "$SCRIPTPATH/../caldavtest/" -o cdt.txt \ + "CalDAV/current-user-principal.xml" \ + "CalDAV/sync-report.xml" \ "CalDAV/sharing-calendars.xml" RESULT=$? diff --git a/apps/dav/tests/unit/caldav/calendartest.php b/apps/dav/tests/unit/caldav/calendartest.php index 93b3f4bff8c..4a3c94e8aba 100644 --- a/apps/dav/tests/unit/caldav/calendartest.php +++ b/apps/dav/tests/unit/caldav/calendartest.php @@ -37,7 +37,8 @@ class CalendarTest extends TestCase { $calendarInfo = [ '{http://owncloud.org/ns}owner-principal' => 'user1', 'principaluri' => 'user2', - 'id' => 666 + 'id' => 666, + 'uri' => 'cal', ]; $c = new Calendar($backend, $calendarInfo); $c->delete(); @@ -56,7 +57,8 @@ class CalendarTest extends TestCase { $calendarInfo = [ '{http://owncloud.org/ns}owner-principal' => 'user1', 'principaluri' => 'user2', - 'id' => 666 + 'id' => 666, + 'uri' => 'cal', ]; $c = new Calendar($backend, $calendarInfo); $c->delete(); diff --git a/apps/dav/tests/unit/carddav/birthdayservicetest.php b/apps/dav/tests/unit/carddav/birthdayservicetest.php new file mode 100644 index 00000000000..faf42d9e1b8 --- /dev/null +++ b/apps/dav/tests/unit/carddav/birthdayservicetest.php @@ -0,0 +1,171 @@ +<?php +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\DAV\Tests\Unit\CardDAV; + +use OCA\DAV\CalDAV\BirthdayService; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CardDAV\CardDavBackend; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Reader; +use Test\TestCase; + +class BirthdayServiceTest extends TestCase { + + /** @var BirthdayService */ + private $service; + /** @var CalDavBackend | \PHPUnit_Framework_MockObject_MockObject */ + private $calDav; + /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject */ + private $cardDav; + + public function setUp() { + parent::setUp(); + + $this->calDav = $this->getMockBuilder('OCA\DAV\CalDAV\CalDavBackend')->disableOriginalConstructor()->getMock(); + $this->cardDav = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')->disableOriginalConstructor()->getMock(); + + $this->service = new BirthdayService($this->calDav, $this->cardDav); + } + + /** + * @dataProvider providesVCards + * @param boolean $nullExpected + * @param string | null $data + */ + public function testBuildBirthdayFromContact($nullExpected, $data) { + $cal = $this->service->buildBirthdayFromContact($data); + if ($nullExpected) { + $this->assertNull($cal); + } else { + $this->assertInstanceOf('Sabre\VObject\Component\VCalendar', $cal); + $this->assertTrue(isset($cal->VEVENT)); + $this->assertEquals('FREQ=YEARLY', $cal->VEVENT->RRULE->getValue()); + $this->assertEquals('12345\'s Birthday (1900)', $cal->VEVENT->SUMMARY->getValue()); + $this->assertEquals('TRANSPARENT', $cal->VEVENT->TRANSP->getValue()); + } + } + + public function testOnCardDeleted() { + $this->cardDav->expects($this->once())->method('getAddressBookById') + ->with(666) + ->willReturn([ + 'principaluri' => 'principals/users/user01', + 'uri' => 'default' + ]); + $this->calDav->expects($this->once())->method('getCalendarByUri') + ->with('principals/users/user01', 'contact_birthdays') + ->willReturn([ + 'id' => 1234 + ]); + $this->calDav->expects($this->once())->method('deleteCalendarObject')->with(1234, 'default-gump.vcf.ics'); + + $this->service->onCardDeleted(666, 'gump.vcf'); + } + + /** + * @dataProvider providesCardChanges + */ + public function testOnCardChanged($expectedOp) { + $this->cardDav->expects($this->once())->method('getAddressBookById') + ->with(666) + ->willReturn([ + 'principaluri' => 'principals/users/user01', + 'uri' => 'default' + ]); + $this->calDav->expects($this->once())->method('getCalendarByUri') + ->with('principals/users/user01', 'contact_birthdays') + ->willReturn([ + 'id' => 1234 + ]); + + /** @var BirthdayService | \PHPUnit_Framework_MockObject_MockObject $service */ + $service = $this->getMock('\OCA\DAV\CalDAV\BirthdayService', + ['buildBirthdayFromContact', 'birthdayEvenChanged'], [$this->calDav, $this->cardDav]); + + if ($expectedOp === 'delete') { + $this->calDav->expects($this->once())->method('getCalendarObject')->willReturn(''); + $service->expects($this->once())->method('buildBirthdayFromContact')->willReturn(null); + $this->calDav->expects($this->once())->method('deleteCalendarObject')->with(1234, 'default-gump.vcf.ics'); + } + if ($expectedOp === 'create') { + $service->expects($this->once())->method('buildBirthdayFromContact')->willReturn(new VCalendar()); + $this->calDav->expects($this->once())->method('createCalendarObject')->with(1234, 'default-gump.vcf.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nEND:VCALENDAR\r\n"); + } + if ($expectedOp === 'update') { + $service->expects($this->once())->method('buildBirthdayFromContact')->willReturn(new VCalendar()); + $service->expects($this->once())->method('birthdayEvenChanged')->willReturn(true); + $this->calDav->expects($this->once())->method('getCalendarObject')->willReturn([ + 'calendardata' => '']); + $this->calDav->expects($this->once())->method('updateCalendarObject')->with(1234, 'default-gump.vcf.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nEND:VCALENDAR\r\n"); + } + + $service->onCardChanged(666, 'gump.vcf', ''); + } + + /** + * @dataProvider providesBirthday + * @param $expected + * @param $old + * @param $new + */ + public function testBirthdayEvenChanged($expected, $old, $new) { + $new = Reader::read($new); + $this->assertEquals($expected, $this->service->birthdayEvenChanged($old, $new)); + } + + public function providesBirthday() { + return [ + [true, + '', + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"], + [false, + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"], + [true, + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:4567's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"], + [true, + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000102\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"] + ]; + } + + public function providesCardChanges(){ + return[ + ['delete'], + ['create'], + ['update'] + ]; + } + + public function providesVCards() { + return [ + [true, null], + [true, ''], + [true, 'yasfewf'], + [true, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", "Dr. Foo Bar"], + [true, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:\r\nEND:VCARD\r\n", "Dr. Foo Bar"], + [true, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:someday\r\nEND:VCARD\r\n", "Dr. Foo Bar"], + [false, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:1900-01-01\r\nEND:VCARD\r\n", "Dr. Foo Bar"], + ]; + } +} diff --git a/apps/dav/tests/unit/carddav/carddavbackendtest.php b/apps/dav/tests/unit/carddav/carddavbackendtest.php index 3b5395fb09e..f920eb47b6e 100644 --- a/apps/dav/tests/unit/carddav/carddavbackendtest.php +++ b/apps/dav/tests/unit/carddav/carddavbackendtest.php @@ -77,7 +77,7 @@ class CardDavBackendTest extends TestCase { $this->db = \OC::$server->getDatabaseConnection(); - $this->backend = new CardDavBackend($this->db, $this->principal); + $this->backend = new CardDavBackend($this->db, $this->principal, null); // start every test with a empty cards_properties and cards table $query = $this->db->getQueryBuilder(); @@ -155,7 +155,7 @@ class CardDavBackendTest extends TestCase { /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject $backend */ $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend') - ->setConstructorArgs([$this->db, $this->principal]) + ->setConstructorArgs([$this->db, $this->principal, null]) ->setMethods(['updateProperties', 'purgeProperties'])->getMock(); // create a new address book @@ -201,7 +201,7 @@ class CardDavBackendTest extends TestCase { public function testMultiCard() { $this->backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend') - ->setConstructorArgs([$this->db, $this->principal]) + ->setConstructorArgs([$this->db, $this->principal, null]) ->setMethods(['updateProperties'])->getMock(); // create a new address book @@ -248,7 +248,7 @@ class CardDavBackendTest extends TestCase { public function testDeleteWithoutCard() { $this->backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend') - ->setConstructorArgs([$this->db, $this->principal]) + ->setConstructorArgs([$this->db, $this->principal, null]) ->setMethods([ 'getCardId', 'addChange', @@ -289,7 +289,7 @@ class CardDavBackendTest extends TestCase { public function testSyncSupport() { $this->backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend') - ->setConstructorArgs([$this->db, $this->principal]) + ->setConstructorArgs([$this->db, $this->principal, null]) ->setMethods(['updateProperties'])->getMock(); // create a new address book @@ -347,7 +347,7 @@ class CardDavBackendTest extends TestCase { $cardId = 2; $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend') - ->setConstructorArgs([$this->db, $this->principal]) + ->setConstructorArgs([$this->db, $this->principal, null]) ->setMethods(['getCardId'])->getMock(); $backend->expects($this->any())->method('getCardId')->willReturn($cardId); diff --git a/apps/dav/tests/unit/systemtag/systemtagplugin.php b/apps/dav/tests/unit/systemtag/systemtagplugin.php index b026451701f..b945223e668 100644 --- a/apps/dav/tests/unit/systemtag/systemtagplugin.php +++ b/apps/dav/tests/unit/systemtag/systemtagplugin.php @@ -22,6 +22,8 @@ namespace OCA\DAV\Tests\Unit\SystemTag; use OC\SystemTag\SystemTag; +use OCP\IGroupManager; +use OCP\IUserSession; use OCP\SystemTag\TagAlreadyExistsException; class SystemTagPlugin extends \Test\TestCase { @@ -47,6 +49,16 @@ class SystemTagPlugin extends \Test\TestCase { private $tagManager; /** + * @var IGroupManager + */ + private $groupManager; + + /** + * @var IUserSession + */ + private $userSession; + + /** * @var \OCA\DAV\SystemTag\SystemTagPlugin */ private $plugin; @@ -60,8 +72,14 @@ class SystemTagPlugin extends \Test\TestCase { $this->server = new \Sabre\DAV\Server($this->tree); $this->tagManager = $this->getMock('\OCP\SystemTag\ISystemTagManager'); + $this->groupManager = $this->getMock('\OCP\IGroupManager'); + $this->userSession = $this->getMock('\OCP\IUserSession'); - $this->plugin = new \OCA\DAV\SystemTag\SystemTagPlugin($this->tagManager); + $this->plugin = new \OCA\DAV\SystemTag\SystemTagPlugin( + $this->tagManager, + $this->groupManager, + $this->userSession + ); $this->plugin->initialize($this->server); } @@ -153,7 +171,204 @@ class SystemTagPlugin extends \Test\TestCase { $this->assertEquals(200, $result[self::USERVISIBLE_PROPERTYNAME]); } + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + * @expectedExceptionMessage Not sufficient permissions + */ + public function testCreateNotAssignableTagAsRegularUser() { + $user = $this->getMock('\OCP\IUser'); + $user->expects($this->once()) + ->method('getUID') + ->willReturn('admin'); + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->willReturn($user); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('admin') + ->willReturn(false); + + $requestData = json_encode([ + 'name' => 'Test', + 'userVisible' => true, + 'userAssignable' => false, + ]); + + $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagsByIdCollection') + ->disableOriginalConstructor() + ->getMock(); + $this->tagManager->expects($this->never()) + ->method('createTag'); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('/systemtags') + ->will($this->returnValue($node)); + + $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') + ->disableOriginalConstructor() + ->getMock(); + $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') + ->disableOriginalConstructor() + ->getMock(); + + $request->expects($this->once()) + ->method('getPath') + ->will($this->returnValue('/systemtags')); + + $request->expects($this->once()) + ->method('getBodyAsString') + ->will($this->returnValue($requestData)); + + $request->expects($this->once()) + ->method('getHeader') + ->with('Content-Type') + ->will($this->returnValue('application/json')); + + $this->plugin->httpPost($request, $response); + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + * @expectedExceptionMessage Not sufficient permissions + */ + public function testCreateInvisibleTagAsRegularUser() { + $user = $this->getMock('\OCP\IUser'); + $user->expects($this->once()) + ->method('getUID') + ->willReturn('admin'); + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->willReturn($user); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('admin') + ->willReturn(false); + + $requestData = json_encode([ + 'name' => 'Test', + 'userVisible' => false, + 'userAssignable' => true, + ]); + + $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagsByIdCollection') + ->disableOriginalConstructor() + ->getMock(); + $this->tagManager->expects($this->never()) + ->method('createTag'); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('/systemtags') + ->will($this->returnValue($node)); + + $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') + ->disableOriginalConstructor() + ->getMock(); + $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') + ->disableOriginalConstructor() + ->getMock(); + + $request->expects($this->once()) + ->method('getPath') + ->will($this->returnValue('/systemtags')); + + $request->expects($this->once()) + ->method('getBodyAsString') + ->will($this->returnValue($requestData)); + + $request->expects($this->once()) + ->method('getHeader') + ->with('Content-Type') + ->will($this->returnValue('application/json')); + + $this->plugin->httpPost($request, $response); + } + + public function testCreateTagInByIdCollectionAsRegularUser() { + $systemTag = new SystemTag(1, 'Test', true, false); + + $requestData = json_encode([ + 'name' => 'Test', + 'userVisible' => true, + 'userAssignable' => true, + ]); + + $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagsByIdCollection') + ->disableOriginalConstructor() + ->getMock(); + $this->tagManager->expects($this->once()) + ->method('createTag') + ->with('Test', true, true) + ->will($this->returnValue($systemTag)); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('/systemtags') + ->will($this->returnValue($node)); + + $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') + ->disableOriginalConstructor() + ->getMock(); + $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') + ->disableOriginalConstructor() + ->getMock(); + + $request->expects($this->once()) + ->method('getPath') + ->will($this->returnValue('/systemtags')); + + $request->expects($this->once()) + ->method('getBodyAsString') + ->will($this->returnValue($requestData)); + + $request->expects($this->once()) + ->method('getHeader') + ->with('Content-Type') + ->will($this->returnValue('application/json')); + + $request->expects($this->once()) + ->method('getUrl') + ->will($this->returnValue('http://example.com/dav/systemtags')); + + $response->expects($this->once()) + ->method('setHeader') + ->with('Content-Location', 'http://example.com/dav/systemtags/1'); + + $this->plugin->httpPost($request, $response); + } + public function testCreateTagInByIdCollection() { + $user = $this->getMock('\OCP\IUser'); + $user->expects($this->once()) + ->method('getUID') + ->willReturn('admin'); + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->willReturn($user); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('admin') + ->willReturn(true); + $systemTag = new SystemTag(1, 'Test', true, false); $requestData = json_encode([ @@ -214,6 +429,24 @@ class SystemTagPlugin extends \Test\TestCase { } public function testCreateTagInMappingCollection() { + $user = $this->getMock('\OCP\IUser'); + $user->expects($this->once()) + ->method('getUID') + ->willReturn('admin'); + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->willReturn($user); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('admin') + ->willReturn(true); + $systemTag = new SystemTag(1, 'Test', true, false); $requestData = json_encode([ @@ -307,9 +540,27 @@ class SystemTagPlugin extends \Test\TestCase { /** * @dataProvider nodeClassProvider - * @expectedException Sabre\DAV\Exception\Conflict + * @expectedException \Sabre\DAV\Exception\Conflict */ public function testCreateTagConflict($nodeClass) { + $user = $this->getMock('\OCP\IUser'); + $user->expects($this->once()) + ->method('getUID') + ->willReturn('admin'); + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->willReturn($user); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('admin') + ->willReturn(true); + $requestData = json_encode([ 'name' => 'Test', 'userVisible' => true, diff --git a/apps/files_sharing/css/public.css b/apps/files_sharing/css/public.css index bd8e98e966d..d09947dab26 100644 --- a/apps/files_sharing/css/public.css +++ b/apps/files_sharing/css/public.css @@ -1,7 +1,11 @@ #content { height: initial; min-height: calc(100vh - 120px); - overflow: hidden; +} + +/* force layout to make sure the content element's height matches its contents' height */ +.ie #content { + display: inline-block; } #preview { diff --git a/apps/files_versions/lib/storage.php b/apps/files_versions/lib/storage.php index da736c868fc..4eac476ed66 100644 --- a/apps/files_versions/lib/storage.php +++ b/apps/files_versions/lib/storage.php @@ -104,6 +104,9 @@ class Storage { $ownerView = new View('/'.$uid.'/files'); try { $filename = $ownerView->getPath($info['fileid']); + // make sure that the file name doesn't end with a trailing slash + // can for example happen single files shared across servers + $filename = rtrim($filename, '/'); } catch (NotFoundException $e) { $filename = null; } diff --git a/build/integration/config/behat.yml b/build/integration/config/behat.yml index 42f5f88ac8a..a1f9d610c68 100644 --- a/build/integration/config/behat.yml +++ b/build/integration/config/behat.yml @@ -12,6 +12,10 @@ default: - admin - admin regular_user_password: 123456 + - CommentsContext: + baseUrl: http://localhost:8080 + - TagsContext: + baseUrl: http://localhost:8080 federation: paths: - %paths.base%/../federation_features diff --git a/build/integration/features/bootstrap/CommentsContext.php b/build/integration/features/bootstrap/CommentsContext.php new file mode 100644 index 00000000000..d720bb8dcd6 --- /dev/null +++ b/build/integration/features/bootstrap/CommentsContext.php @@ -0,0 +1,263 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +require __DIR__ . '/../../vendor/autoload.php'; + +class CommentsContext implements \Behat\Behat\Context\Context { + /** @var string */ + private $baseUrl; + /** @var array */ + private $response; + /** @var int */ + private $commentId; + /** @var int */ + private $fileId; + + /** + * @param string $baseUrl + */ + public function __construct($baseUrl) { + $this->baseUrl = $baseUrl; + + // in case of ci deployment we take the server url from the environment + $testServerUrl = getenv('TEST_SERVER_URL'); + if ($testServerUrl !== false) { + $this->baseUrl = substr($testServerUrl, 0, -5); + } + } + + /** @AfterScenario */ + public function teardownScenario(\Behat\Behat\Hook\Scope\AfterScenarioScope $scope) { + $client = new \GuzzleHttp\Client(); + try { + $client->delete( + $this->baseUrl.'/remote.php/webdav/myFileToComment.txt', + [ + 'auth' => [ + 'user0', + '123456', + ], + 'headers' => [ + 'Content-Type' => 'application/json', + ], + ] + ); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $e->getResponse(); + } + } + + /** + * @return int + */ + private function getFileIdForPath($path) { + $url = $this->baseUrl.'/remote.php/webdav/'.$path; + $context = stream_context_create(array( + 'http' => array( + 'method' => 'PROPFIND', + 'header' => "Authorization: Basic dXNlcjA6MTIzNDU2\r\nContent-Type: application/x-www-form-urlencoded", + 'content' => '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns"> + <d:prop> + <oc:fileid /> + </d:prop> +</d:propfind>' + ) + )); + + $response = file_get_contents($url, false, $context); + preg_match_all('/\<oc:fileid\>(.*)\<\/oc:fileid\>/', $response, $matches); + return (int)$matches[1][0]; + } + + /** + * @When :user posts a comment with content :content on the file named :fileName it should return :statusCode + */ + public function postsACommentWithContentOnTheFileNamedItShouldReturn($user, $content, $fileName, $statusCode) { + $fileId = $this->getFileIdForPath($fileName); + $this->fileId = (int)$fileId; + $url = $this->baseUrl.'/remote.php/dav/comments/files/'.$fileId.'/'; + + $client = new \GuzzleHttp\Client(); + try { + $res = $client->post( + $url, + [ + 'body' => '{"actorId":"user0","actorDisplayName":"user0","actorType":"users","verb":"comment","message":"' . $content . '","creationDateTime":"Thu, 18 Feb 2016 17:04:18 GMT","objectType":"files"}', + 'auth' => [ + $user, + '123456', + ], + 'headers' => [ + 'Content-Type' => 'application/json', + ], + ] + ); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $res = $e->getResponse(); + } + + if($res->getStatusCode() !== (int)$statusCode) { + throw new \Exception("Response status code was not $statusCode (".$res->getStatusCode().")"); + } + } + + + /** + * @Then As :user load all the comments of the file named :fileName it should return :statusCode + */ + public function asLoadloadAllTheCommentsOfTheFileNamedItShouldReturn($user, $fileName, $statusCode) { + $fileId = $this->getFileIdForPath($fileName); + $url = $this->baseUrl.'/remote.php/dav/comments/files/'.$fileId.'/'; + + try { + $client = new \GuzzleHttp\Client(); + $res = $client->createRequest( + 'REPORT', + $url, + [ + 'body' => '<?xml version="1.0" encoding="utf-8" ?> +<oc:filter-comments xmlns:D="DAV:" xmlns:oc="http://owncloud.org/ns"> + <oc:limit>200</oc:limit> + <oc:offset>0</oc:offset> +</oc:filter-comments> +', + 'auth' => [ + $user, + '123456', + ], + 'headers' => [ + 'Content-Type' => 'application/json', + ], + ] + ); + $res = $client->send($res); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $res = $e->getResponse(); + } + + if($res->getStatusCode() !== (int)$statusCode) { + throw new \Exception("Response status code was not $statusCode (".$res->getStatusCode().")"); + } + + if($res->getStatusCode() === 207) { + $service = new Sabre\Xml\Service(); + $this->response = $service->parse($res->getBody()->getContents()); + $this->commentId = (int)$this->response[0]['value'][2]['value'][0]['value'][0]['value']; + } + } + + /** + * @Given As :user sending :verb to :url with + */ + public function asUserSendingToWith($user, $verb, $url, \Behat\Gherkin\Node\TableNode $body) { + $client = new \GuzzleHttp\Client(); + $options = []; + $options['auth'] = [$user, '123456']; + $fd = $body->getRowsHash(); + $options['body'] = $fd; + $client->send($client->createRequest($verb, $this->baseUrl.'/ocs/v1.php/'.$url, $options)); + } + + /** + * @Then As :user delete the created comment it should return :statusCode + */ + public function asDeleteTheCreatedCommentItShouldReturn($user, $statusCode) { + $url = $this->baseUrl.'/remote.php/dav/comments/files/'.$this->fileId.'/'.$this->commentId; + + $client = new \GuzzleHttp\Client(); + try { + $res = $client->delete( + $url, + [ + 'auth' => [ + $user, + '123456', + ], + 'headers' => [ + 'Content-Type' => 'application/json', + ], + ] + ); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $res = $e->getResponse(); + } + + if($res->getStatusCode() !== (int)$statusCode) { + throw new \Exception("Response status code was not $statusCode (".$res->getStatusCode().")"); + } + } + + /** + * @Then the response should contain a property :key with value :value + */ + public function theResponseShouldContainAPropertyWithValue($key, $value) { + $keys = $this->response[0]['value'][2]['value'][0]['value']; + $found = false; + foreach($keys as $singleKey) { + if($singleKey['name'] === '{http://owncloud.org/ns}'.substr($key, 3)) { + if($singleKey['value'] === $value) { + $found = true; + } + } + } + if($found === false) { + throw new \Exception("Cannot find property $key with $value"); + } + } + + /** + * @Then the response should contain only :number comments + */ + public function theResponseShouldContainOnlyComments($number) { + if(count($this->response) !== (int)$number) { + throw new \Exception("Found more comments than $number (".count($this->response).")"); + } + } + + /** + * @Then As :user edit the last created comment and set text to :text it should return :statusCode + */ + public function asEditTheLastCreatedCommentAndSetTextToItShouldReturn($user, $text, $statusCode) { + $client = new \GuzzleHttp\Client(); + $options = []; + $options['auth'] = [$user, '123456']; + $options['body'] = '<?xml version="1.0"?> +<d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns"> + <d:set> + <d:prop> + <oc:message>'.$text.'</oc:message> + </d:prop> + </d:set> +</d:propertyupdate>'; + try { + $res = $client->send($client->createRequest('PROPPATCH', $this->baseUrl.'/remote.php/dav/comments/files/' . $this->fileId . '/' . $this->commentId, $options)); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $res = $e->getResponse(); + } + + if($res->getStatusCode() !== (int)$statusCode) { + throw new \Exception("Response status code was not $statusCode (".$res->getStatusCode().")"); + } + } + + +} diff --git a/build/integration/features/bootstrap/Sharing.php b/build/integration/features/bootstrap/Sharing.php index 5103b4af508..faf8e0bf507 100644 --- a/build/integration/features/bootstrap/Sharing.php +++ b/build/integration/features/bootstrap/Sharing.php @@ -370,5 +370,49 @@ trait Sharing{ } } + /** + * @Then As :user remove all shares from the file named :fileName + */ + public function asRemoveAllSharesFromTheFileNamed($user, $fileName) { + $url = $this->baseUrl.'v2.php/apps/files_sharing/api/v1/shares?format=json'; + $client = new \GuzzleHttp\Client(); + $res = $client->get( + $url, + [ + 'auth' => [ + $user, + '123456', + ], + 'headers' => [ + 'Content-Type' => 'application/json', + ], + ] + ); + $json = json_decode($res->getBody()->getContents(), true); + $deleted = false; + foreach($json['ocs']['data'] as $data) { + if (stripslashes($data['path']) === $fileName) { + $id = $data['id']; + $client->delete( + $this->baseUrl.'v2.php/apps/files_sharing/api/v1/shares/'.$id, + [ + 'auth' => [ + $user, + '123456', + ], + 'headers' => [ + 'Content-Type' => 'application/json', + ], + ] + ); + $deleted = true; + } + } + + if($deleted === false) { + throw new \Exception("Could not delete file $fileName"); + } + } + } diff --git a/build/integration/features/bootstrap/TagsContext.php b/build/integration/features/bootstrap/TagsContext.php new file mode 100644 index 00000000000..5e1f62ba838 --- /dev/null +++ b/build/integration/features/bootstrap/TagsContext.php @@ -0,0 +1,482 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +require __DIR__ . '/../../vendor/autoload.php'; + +use Behat\Gherkin\Node\TableNode; +use GuzzleHttp\Client; +use GuzzleHttp\Message\ResponseInterface; + +class TagsContext implements \Behat\Behat\Context\Context { + /** @var string */ + private $baseUrl; + /** @var Client */ + private $client; + /** @var ResponseInterface */ + private $response; + + /** + * @param string $baseUrl + */ + public function __construct($baseUrl) { + $this->baseUrl = $baseUrl; + + // in case of ci deployment we take the server url from the environment + $testServerUrl = getenv('TEST_SERVER_URL'); + if ($testServerUrl !== false) { + $this->baseUrl = substr($testServerUrl, 0, -5); + } + } + + /** @BeforeScenario */ + public function tearUpScenario() { + $this->client = new Client(); + } + + /** @AfterScenario */ + public function tearDownScenario() { + $user = 'admin'; + $tags = $this->requestTagsForUser($user); + foreach($tags as $tagId => $tag) { + $this->response = $this->client->delete( + $this->baseUrl . '/remote.php/dav/systemtags/'.$tagId, + [ + 'auth' => [ + $user, + $this->getPasswordForUser($user), + ], + 'headers' => [ + 'Content-Type' => 'application/json', + ], + ] + ); + } + try { + $this->client->delete( + $this->baseUrl . '/remote.php/webdav/myFileToTag.txt', + [ + 'auth' => [ + 'user0', + '123456', + ], + 'headers' => [ + 'Content-Type' => 'application/json', + ], + ] + ); + } catch (\GuzzleHttp\Exception\ClientException $e) {} + } + + /** + * @param string $userName + * @return string + */ + private function getPasswordForUser($userName) { + if($userName === 'admin') { + return 'admin'; + } + return '123456'; + } + + /** + * @When :user creates a :type tag with name :name + */ + public function createsATagWithName($user, $type, $name) { + $userVisible = 'true'; + $userAssignable = 'true'; + switch ($type) { + case 'normal': + break; + case 'not user-assignable': + $userAssignable = 'false'; + break; + case 'not user-visible': + $userVisible = 'false'; + break; + default: + throw new \Exception('Unsupported type'); + } + + try { + $this->response = $this->client->post( + $this->baseUrl . '/remote.php/dav/systemtags/', + [ + 'auth' => [ + $user, + $this->getPasswordForUser($user), + ], + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => '{"name":"'.$name.'","userVisible":'.$userVisible.',"userAssignable":'.$userAssignable.'}', + ] + ); + } catch (\GuzzleHttp\Exception\ClientException $e){ + $this->response = $e->getResponse(); + } + } + + /** + * @Then The response should have a status code :statusCode + */ + public function theResponseShouldHaveAStatusCode($statusCode) { + if((int)$statusCode !== $this->response->getStatusCode()) { + throw new \Exception("Expected $statusCode, got ".$this->response->getStatusCode()); + } + } + + /** + * Returns all tags for a given user + * + * @param string $user + * @return array + */ + private function requestTagsForUser($user) { + try { + $request = $this->client->createRequest( + 'PROPFIND', + $this->baseUrl . '/remote.php/dav/systemtags/', + [ + 'body' => '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns"> + <d:prop> + <oc:id /> + <oc:display-name /> + <oc:user-visible /> + <oc:user-assignable /> + </d:prop> +</d:propfind>', + 'auth' => [ + $user, + $this->getPasswordForUser($user), + ], + 'headers' => [ + 'Content-Type' => 'application/json', + ], + ] + ); + $this->response = $this->client->send($request); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + + $tags = []; + $service = new Sabre\Xml\Service(); + $parsed = $service->parse($this->response->getBody()->getContents()); + foreach($parsed as $entry) { + $singleEntry = $entry['value'][1]['value'][0]['value']; + if(empty($singleEntry[0]['value'])) { + continue; + } + + $tags[$singleEntry[0]['value']] = [ + 'display-name' => $singleEntry[1]['value'], + 'user-visible' => $singleEntry[2]['value'], + 'user-assignable' => $singleEntry[3]['value'], + ]; + } + + return $tags; + } + + /** + * @Then The following tags should exist for :user + */ + public function theFollowingTagsShouldExistFor($user, TableNode $table) { + $tags = $this->requestTagsForUser($user); + + if(count($table->getRows()) !== count($tags)) { + throw new \Exception( + sprintf( + "Expected %s tags, got %s.", + count($table->getRows()), + count($tags) + ) + ); + } + + foreach($table->getRowsHash() as $rowDisplayName => $row) { + foreach($tags as $key => $tag) { + if( + $tag['display-name'] === $rowDisplayName && + $tag['user-visible'] === $row[0] && + $tag['user-assignable'] === $row[1] + ) { + unset($tags[$key]); + } + } + } + if(count($tags) !== 0) { + throw new \Exception('Not expected response'); + } + } + + /** + * @Then :count tags should exist for :user + */ + public function tagsShouldExistFor($count, $user) { + if((int)$count !== count($this->requestTagsForUser($user))) { + throw new \Exception("Expected $count tags, got ".count($this->requestTagsForUser($user))); + } + } + + /** + * @param string $name + * @return int + */ + private function findTagIdByName($name) { + $tags = $this->requestTagsForUser('admin'); + $tagId = 0; + foreach($tags as $id => $tag) { + if($tag['display-name'] === $name) { + $tagId = $id; + break; + } + } + return (int)$tagId; + } + + /** + * @When :user edits the tag with name :oldNmae and sets its name to :newName + */ + public function editsTheTagWithNameAndSetsItsNameTo($user, $oldName, $newName) { + $tagId = $this->findTagIdByName($oldName); + if($tagId === 0) { + throw new \Exception('Could not find tag to rename'); + } + + try { + $request = $this->client->createRequest( + 'PROPPATCH', + $this->baseUrl . '/remote.php/dav/systemtags/' . $tagId, + [ + 'body' => '<?xml version="1.0"?> +<d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns"> + <d:set> + <d:prop> + <oc:display-name>' . $newName . '</oc:display-name> + </d:prop> + </d:set> +</d:propertyupdate>', + 'auth' => [ + $user, + $this->getPasswordForUser($user), + ], + ] + ); + $this->response = $this->client->send($request); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + } + + /** + * @When :user deletes the tag with name :name + */ + public function deletesTheTagWithName($user, $name) { + $tagId = $this->findTagIdByName($name); + try { + $this->response = $this->client->delete( + $this->baseUrl . '/remote.php/dav/systemtags/' . $tagId, + [ + 'auth' => [ + $user, + $this->getPasswordForUser($user), + ], + 'headers' => [ + 'Content-Type' => 'application/json', + ], + ] + ); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + } + + /** + * @param string $path + * @param string $user + * @return int + */ + private function getFileIdForPath($path, $user) { + $url = $this->baseUrl.'/remote.php/webdav/'.$path; + $credentials = base64_encode($user .':'.$this->getPasswordForUser($user)); + $context = stream_context_create(array( + 'http' => array( + 'method' => 'PROPFIND', + 'header' => "Authorization: Basic $credentials\r\nContent-Type: application/x-www-form-urlencoded", + 'content' => '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns"> + <d:prop> + <oc:fileid /> + </d:prop> +</d:propfind>' + ) + )); + $response = file_get_contents($url, false, $context); + preg_match_all('/\<oc:fileid\>(.*)\<\/oc:fileid\>/', $response, $matches); + return (int)$matches[1][0]; + } + + /** + * @When :taggingUser adds the tag :tagName to :fileName shared by :sharingUser + */ + public function addsTheTagToSharedBy($taggingUser, $tagName, $fileName, $sharingUser) { + $fileId = $this->getFileIdForPath($fileName, $sharingUser); + $tagId = $this->findTagIdByName($tagName); + + try { + $this->response = $this->client->put( + $this->baseUrl.'/remote.php/dav/systemtags-relations/files/'.$fileId.'/'.$tagId, + [ + 'auth' => [ + $taggingUser, + $this->getPasswordForUser($taggingUser), + ] + ] + ); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + } + + /** + * @Then :fileName shared by :sharingUser has the following tags + */ + public function sharedByHasTheFollowingTags($fileName, $sharingUser, TableNode $table) { + $loadedExpectedTags = $table->getTable(); + $expectedTags = []; + foreach($loadedExpectedTags as $expected) { + $expectedTags[] = $expected[0]; + } + + // Get the real tags + $request = $this->client->createRequest( + 'PROPFIND', + $this->baseUrl.'/remote.php/dav/systemtags-relations/files/'.$this->getFileIdForPath($fileName, $sharingUser), + [ + 'auth' => [ + $sharingUser, + $this->getPasswordForUser($sharingUser), + ], + 'body' => '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns"> + <d:prop> + <oc:id /> + <oc:display-name /> + <oc:user-visible /> + <oc:user-assignable /> + </d:prop> +</d:propfind>', + ] + ); + $response = $this->client->send($request)->getBody()->getContents(); + preg_match_all('/\<oc:display-name\>(.*)\<\/oc:display-name\>/', $response, $realTags); + + foreach($expectedTags as $key => $row) { + foreach($realTags as $tag) { + if($tag[0] === $row) { + unset($expectedTags[$key]); + } + } + } + + if(count($expectedTags) !== 0) { + throw new \Exception('Not all tags found.'); + } + } + + /** + * @Then :fileName shared by :sharingUser has the following tags for :user + */ + public function sharedByHasTheFollowingTagsFor($fileName, $sharingUser, $user, TableNode $table) { + $loadedExpectedTags = $table->getTable(); + $expectedTags = []; + foreach($loadedExpectedTags as $expected) { + $expectedTags[] = $expected[0]; + } + + // Get the real tags + try { + $request = $this->client->createRequest( + 'PROPFIND', + $this->baseUrl . '/remote.php/dav/systemtags-relations/files/' . $this->getFileIdForPath($fileName, $sharingUser), + [ + 'auth' => [ + $user, + $this->getPasswordForUser($user), + ], + 'body' => '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns"> + <d:prop> + <oc:id /> + <oc:display-name /> + <oc:user-visible /> + <oc:user-assignable /> + </d:prop> +</d:propfind>', + ] + ); + $this->response = $this->client->send($request)->getBody()->getContents(); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + preg_match_all('/\<oc:display-name\>(.*)\<\/oc:display-name\>/', $this->response, $realTags); + $realTags = array_filter($realTags); + $expectedTags = array_filter($expectedTags); + + foreach($expectedTags as $key => $row) { + foreach($realTags as $tag) { + foreach($tag as $index => $foo) { + if($tag[$index] === $row) { + unset($expectedTags[$key]); + } + } + } + } + + if(count($expectedTags) !== 0) { + throw new \Exception('Not all tags found.'); + } + } + + /** + * @When :user removes the tag :tagName from :fileName shared by :shareUser + */ + public function removesTheTagFromSharedBy($user, $tagName, $fileName, $shareUser) { + $tagId = $this->findTagIdByName($tagName); + $fileId = $this->getFileIdForPath($fileName, $shareUser); + + try { + $this->response = $this->client->delete( + $this->baseUrl.'/remote.php/dav/systemtags-relations/files/'.$fileId.'/'.$tagId, + [ + 'auth' => [ + $user, + $this->getPasswordForUser($user), + ], + ] + ); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + } +} diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index 49cd565cf26..58fdfed1711 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -9,8 +9,7 @@ use Sabre\DAV\Client as SClient; require __DIR__ . '/../../vendor/autoload.php'; -trait WebDav{ - +trait WebDav { /** @var string*/ private $davPath = "remote.php/webdav"; @@ -163,6 +162,18 @@ trait WebDav{ } /** + * @When User :user deletes file :file + */ + public function userDeletesFile($user, $file) { + try { + $this->response = $this->makeDavRequest($user, 'DELETE', $file, []); + } catch (\GuzzleHttp\Exception\ServerException $e) { + // 4xx and 5xx responses cause an exception + $this->response = $e->getResponse(); + } + } + + /** * @Given User :user created a folder :destination */ public function userCreatedAFolder($user, $destination){ diff --git a/build/integration/features/comments.feature b/build/integration/features/comments.feature new file mode 100644 index 00000000000..135bb016527 --- /dev/null +++ b/build/integration/features/comments.feature @@ -0,0 +1,209 @@ +Feature: comments + Scenario: Creating a comment on a file belonging to myself + Given user "user0" exists + Given As an "user0" + Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" + When "user0" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "201" + Then As "user0" load all the comments of the file named "/myFileToComment.txt" it should return "207" + And the response should contain a property "oc:parentId" with value "0" + And the response should contain a property "oc:childrenCount" with value "0" + And the response should contain a property "oc:verb" with value "comment" + And the response should contain a property "oc:actorType" with value "users" + And the response should contain a property "oc:objectType" with value "files" + And the response should contain a property "oc:message" with value "My first comment" + And the response should contain a property "oc:actorDisplayName" with value "user0" + And the response should contain only "1" comments + + Scenario: Creating a comment on a shared file belonging to another user + Given user "user0" exists + Given user "user1" exists + Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToComment.txt | + | shareWith | user1 | + | shareType | 0 | + When "user1" posts a comment with content "A comment from another user" on the file named "/myFileToComment.txt" it should return "201" + Then As "user1" load all the comments of the file named "/myFileToComment.txt" it should return "207" + And the response should contain a property "oc:parentId" with value "0" + And the response should contain a property "oc:childrenCount" with value "0" + And the response should contain a property "oc:verb" with value "comment" + And the response should contain a property "oc:actorType" with value "users" + And the response should contain a property "oc:objectType" with value "files" + And the response should contain a property "oc:message" with value "A comment from another user" + And the response should contain a property "oc:actorDisplayName" with value "user1" + And the response should contain only "1" comments + + Scenario: Creating a comment on a non-shared file belonging to another user + Given user "user0" exists + Given user "user1" exists + Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" + Then "user1" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "404" + + Scenario: Reading comments on a non-shared file belonging to another user + Given user "user0" exists + Given user "user1" exists + Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" + Then As "user1" load all the comments of the file named "/myFileToComment.txt" it should return "404" + + Scenario: Deleting my own comments on a file belonging to myself + Given user "user0" exists + Given As an "user0" + Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" + Given "user0" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "201" + When As "user0" load all the comments of the file named "/myFileToComment.txt" it should return "207" + Then the response should contain a property "oc:parentId" with value "0" + Then the response should contain a property "oc:childrenCount" with value "0" + And the response should contain a property "oc:verb" with value "comment" + And the response should contain a property "oc:actorType" with value "users" + And the response should contain a property "oc:objectType" with value "files" + And the response should contain a property "oc:message" with value "My first comment" + And the response should contain a property "oc:actorDisplayName" with value "user0" + And the response should contain only "1" comments + And As "user0" delete the created comment it should return "204" + And As "user0" load all the comments of the file named "/myFileToComment.txt" it should return "207" + And the response should contain only "0" comments + + Scenario: Deleting my own comments on a file shared by somebody else + Given user "user0" exists + Given user "user1" exists + Given As an "user0" + Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToComment.txt | + | shareWith | user1 | + | shareType | 0 | + Given "user1" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "201" + When As "user1" load all the comments of the file named "/myFileToComment.txt" it should return "207" + Then the response should contain a property "oc:parentId" with value "0" + And the response should contain a property "oc:childrenCount" with value "0" + And the response should contain a property "oc:verb" with value "comment" + And the response should contain a property "oc:actorType" with value "users" + And the response should contain a property "oc:objectType" with value "files" + And the response should contain a property "oc:message" with value "My first comment" + And the response should contain a property "oc:actorDisplayName" with value "user1" + And the response should contain only "1" comments + And As "user1" delete the created comment it should return "204" + And As "user1" load all the comments of the file named "/myFileToComment.txt" it should return "207" + And the response should contain only "0" comments + + Scenario: Deleting my own comments on a file unshared by someone else + Given user "user0" exists + Given user "user1" exists + Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToComment.txt | + | shareWith | user1 | + | shareType | 0 | + Given "user1" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "201" + When As "user1" load all the comments of the file named "/myFileToComment.txt" it should return "207" + Then the response should contain a property "oc:parentId" with value "0" + And the response should contain a property "oc:childrenCount" with value "0" + And the response should contain a property "oc:verb" with value "comment" + And the response should contain a property "oc:actorType" with value "users" + And the response should contain a property "oc:objectType" with value "files" + And the response should contain a property "oc:message" with value "My first comment" + And the response should contain a property "oc:actorDisplayName" with value "user1" + And the response should contain only "1" comments + And As "user0" remove all shares from the file named "/myFileToComment.txt" + And As "user1" delete the created comment it should return "404" + And As "user1" load all the comments of the file named "/myFileToComment.txt" it should return "404" + + Scenario: Edit my own comments on a file belonging to myself + Given user "user0" exists + Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" + Given "user0" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "201" + When As "user0" load all the comments of the file named "/myFileToComment.txt" it should return "207" + Then the response should contain a property "oc:parentId" with value "0" + And the response should contain a property "oc:childrenCount" with value "0" + And the response should contain a property "oc:verb" with value "comment" + And the response should contain a property "oc:actorType" with value "users" + And the response should contain a property "oc:objectType" with value "files" + And the response should contain a property "oc:message" with value "My first comment" + And the response should contain a property "oc:actorDisplayName" with value "user0" + And the response should contain only "1" comments + When As "user0" edit the last created comment and set text to "My edited comment" it should return "207" + Then As "user0" load all the comments of the file named "/myFileToComment.txt" it should return "207" + And the response should contain a property "oc:parentId" with value "0" + And the response should contain a property "oc:childrenCount" with value "0" + And the response should contain a property "oc:verb" with value "comment" + And the response should contain a property "oc:actorType" with value "users" + And the response should contain a property "oc:objectType" with value "files" + And the response should contain a property "oc:message" with value "My edited comment" + And the response should contain a property "oc:actorDisplayName" with value "user0" + + Scenario: Edit my own comments on a file shared by someone with me + Given user "user0" exists + Given user "user1" exists + Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToComment.txt | + | shareWith | user1 | + | shareType | 0 | + Given "user1" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "201" + When As "user0" load all the comments of the file named "/myFileToComment.txt" it should return "207" + Then the response should contain a property "oc:parentId" with value "0" + And the response should contain a property "oc:childrenCount" with value "0" + And the response should contain a property "oc:verb" with value "comment" + And the response should contain a property "oc:actorType" with value "users" + And the response should contain a property "oc:objectType" with value "files" + And the response should contain a property "oc:message" with value "My first comment" + And the response should contain a property "oc:actorDisplayName" with value "user1" + And the response should contain only "1" comments + Given As "user1" edit the last created comment and set text to "My edited comment" it should return "207" + Then As "user1" load all the comments of the file named "/myFileToComment.txt" it should return "207" + And the response should contain a property "oc:parentId" with value "0" + And the response should contain a property "oc:childrenCount" with value "0" + And the response should contain a property "oc:verb" with value "comment" + And the response should contain a property "oc:actorType" with value "users" + And the response should contain a property "oc:objectType" with value "files" + And the response should contain a property "oc:message" with value "My edited comment" + And the response should contain a property "oc:actorDisplayName" with value "user1" + + Scenario: Edit my own comments on a file unshared by someone with me + Given user "user0" exists + Given user "user1" exists + Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToComment.txt | + | shareWith | user1 | + | shareType | 0 | + When "user1" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "201" + Then As "user0" load all the comments of the file named "/myFileToComment.txt" it should return "207" + And the response should contain a property "oc:parentId" with value "0" + And the response should contain a property "oc:childrenCount" with value "0" + And the response should contain a property "oc:verb" with value "comment" + And the response should contain a property "oc:actorType" with value "users" + And the response should contain a property "oc:objectType" with value "files" + And the response should contain a property "oc:message" with value "My first comment" + And the response should contain a property "oc:actorDisplayName" with value "user1" + And the response should contain only "1" comments + And As "user0" remove all shares from the file named "/myFileToComment.txt" + When As "user1" edit the last created comment and set text to "My edited comment" it should return "404" + Then As "user0" load all the comments of the file named "/myFileToComment.txt" it should return "207" + And the response should contain a property "oc:parentId" with value "0" + And the response should contain a property "oc:childrenCount" with value "0" + And the response should contain a property "oc:verb" with value "comment" + And the response should contain a property "oc:actorType" with value "users" + And the response should contain a property "oc:objectType" with value "files" + And the response should contain a property "oc:message" with value "My first comment" + And the response should contain a property "oc:actorDisplayName" with value "user1" + + Scenario: Edit comments of other users should not be possible + Given user "user0" exists + Given user "user1" exists + Given User "user0" uploads file "data/textfile.txt" to "/myFileToComment.txt" + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToComment.txt | + | shareWith | user1 | + | shareType | 0 | + Given "user1" posts a comment with content "My first comment" on the file named "/myFileToComment.txt" it should return "201" + When As "user0" load all the comments of the file named "/myFileToComment.txt" it should return "207" + Then the response should contain a property "oc:parentId" with value "0" + And the response should contain a property "oc:childrenCount" with value "0" + And the response should contain a property "oc:verb" with value "comment" + And the response should contain a property "oc:actorType" with value "users" + And the response should contain a property "oc:objectType" with value "files" + And the response should contain a property "oc:message" with value "My first comment" + And the response should contain a property "oc:actorDisplayName" with value "user1" + And the response should contain only "1" comments + Then As "user0" edit the last created comment and set text to "My edited comment" it should return "403"
\ No newline at end of file diff --git a/build/integration/features/tags.feature b/build/integration/features/tags.feature new file mode 100644 index 00000000000..286fb62bf42 --- /dev/null +++ b/build/integration/features/tags.feature @@ -0,0 +1,370 @@ +Feature: tags + + Scenario: Creating a normal tag as regular user should work + Given user "user0" exists + When "user0" creates a "normal" tag with name "MySuperAwesomeTagName" + Then The response should have a status code "201" + And The following tags should exist for "admin" + |MySuperAwesomeTagName|true|true| + And The following tags should exist for "user0" + |MySuperAwesomeTagName|true|true| + + Scenario: Creating a not user-assignable tag as regular user should fail + Given user "user0" exists + When "user0" creates a "not user-assignable" tag with name "MySuperAwesomeTagName" + Then The response should have a status code "400" + And "0" tags should exist for "admin" + + Scenario: Creating a not user-visible tag as regular user should fail + Given user "user0" exists + When "user0" creates a "not user-visible" tag with name "MySuperAwesomeTagName" + Then The response should have a status code "400" + And "0" tags should exist for "admin" + + Scenario: Renaming a normal tag as regular user should work + Given user "user0" exists + Given "admin" creates a "normal" tag with name "MySuperAwesomeTagName" + When "user0" edits the tag with name "MySuperAwesomeTagName" and sets its name to "AnotherTagName" + Then The response should have a status code "207" + And The following tags should exist for "admin" + |AnotherTagName|true|true| + + Scenario: Renaming a not user-assignable tag as regular user should fail + Given user "user0" exists + Given "admin" creates a "not user-assignable" tag with name "MySuperAwesomeTagName" + When "user0" edits the tag with name "MySuperAwesomeTagName" and sets its name to "AnotherTagName" + Then The response should have a status code "403" + And The following tags should exist for "admin" + |MySuperAwesomeTagName|true|false| + + Scenario: Renaming a not user-visible tag as regular user should fail + Given user "user0" exists + Given "admin" creates a "not user-visible" tag with name "MySuperAwesomeTagName" + When "user0" edits the tag with name "MySuperAwesomeTagName" and sets its name to "AnotherTagName" + Then The response should have a status code "404" + And The following tags should exist for "admin" + |MySuperAwesomeTagName|false|true| + + Scenario: Deleting a normal tag as regular user should work + Given user "user0" exists + Given "admin" creates a "normal" tag with name "MySuperAwesomeTagName" + When "user0" deletes the tag with name "MySuperAwesomeTagName" + Then The response should have a status code "204" + And "0" tags should exist for "admin" + + Scenario: Deleting a not user-assignable tag as regular user should fail + Given user "user0" exists + Given "admin" creates a "not user-assignable" tag with name "MySuperAwesomeTagName" + When "user0" deletes the tag with name "MySuperAwesomeTagName" + Then The response should have a status code "403" + And The following tags should exist for "admin" + |MySuperAwesomeTagName|true|false| + + Scenario: Deleting a not user-visible tag as regular user should fail + Given user "user0" exists + Given "admin" creates a "not user-visible" tag with name "MySuperAwesomeTagName" + When "user0" deletes the tag with name "MySuperAwesomeTagName" + Then The response should have a status code "404" + And The following tags should exist for "admin" + |MySuperAwesomeTagName|false|true| + + Scenario: Deleting a not user-assignable tag as admin should work + Given "admin" creates a "not user-assignable" tag with name "MySuperAwesomeTagName" + When "admin" deletes the tag with name "MySuperAwesomeTagName" + Then The response should have a status code "204" + And "0" tags should exist for "admin" + + Scenario: Deleting a not user-visible tag as admin should work + Given "admin" creates a "not user-visible" tag with name "MySuperAwesomeTagName" + When "admin" deletes the tag with name "MySuperAwesomeTagName" + Then The response should have a status code "204" + And "0" tags should exist for "admin" + + Scenario: Assigning a normal tag to a file shared by someone else as regular user should work + Given user "user0" exists + Given user "user1" exists + Given "admin" creates a "normal" tag with name "MySuperAwesomeTagName" + Given user "user0" uploads file "data/textfile.txt" to "/myFileToTag.txt" + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToTag.txt | + | shareWith | user1 | + | shareType | 0 | + When "user1" adds the tag "MySuperAwesomeTagName" to "/myFileToTag.txt" shared by "user0" + Then The response should have a status code "201" + And "/myFileToTag.txt" shared by "user0" has the following tags + |MySuperAwesomeTagName| + + Scenario: Assigning a normal tag to a file belonging to someone else as regular user should fail + Given user "user0" exists + Given user "user1" exists + Given "admin" creates a "normal" tag with name "MyFirstTag" + Given "admin" creates a "normal" tag with name "MySecondTag" + Given user "user0" uploads file "data/textfile.txt" to "/myFileToTag.txt" + When "user0" adds the tag "MyFirstTag" to "/myFileToTag.txt" shared by "user0" + When "user1" adds the tag "MySecondTag" to "/myFileToTag.txt" shared by "user0" + Then The response should have a status code "404" + And "/myFileToTag.txt" shared by "user0" has the following tags + |MyFirstTag| + + Scenario: Assigning a not user-assignable tag to a file shared by someone else as regular user should fail + Given user "user0" exists + Given user "user1" exists + Given "admin" creates a "normal" tag with name "MyFirstTag" + Given "admin" creates a "not user-assignable" tag with name "MySecondTag" + Given user "user0" uploads file "data/textfile.txt" to "/myFileToTag.txt" + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToTag.txt | + | shareWith | user1 | + | shareType | 0 | + When "user0" adds the tag "MyFirstTag" to "/myFileToTag.txt" shared by "user0" + When "user1" adds the tag "MySecondTag" to "/myFileToTag.txt" shared by "user0" + Then The response should have a status code "403" + And "/myFileToTag.txt" shared by "user0" has the following tags + |MyFirstTag| + + Scenario: Assigning a not user-visible tag to a file shared by someone else as regular user should fail + Given user "user0" exists + Given user "user1" exists + Given "admin" creates a "normal" tag with name "MyFirstTag" + Given "admin" creates a "not user-visible" tag with name "MySecondTag" + Given user "user0" uploads file "data/textfile.txt" to "/myFileToTag.txt" + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToTag.txt | + | shareWith | user1 | + | shareType | 0 | + When "user0" adds the tag "MyFirstTag" to "/myFileToTag.txt" shared by "user0" + When "user1" adds the tag "MySecondTag" to "/myFileToTag.txt" shared by "user0" + Then The response should have a status code "412" + And "/myFileToTag.txt" shared by "user0" has the following tags + |MyFirstTag| + + Scenario: Assigning a not user-visible tag to a file shared by someone else as admin user should work + Given user "user0" exists + Given "admin" creates a "normal" tag with name "MyFirstTag" + Given "admin" creates a "not user-visible" tag with name "MySecondTag" + Given user "user0" uploads file "data/textfile.txt" to "/myFileToTag.txt" + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToTag.txt | + | shareWith | admin | + | shareType | 0 | + When "user0" adds the tag "MyFirstTag" to "/myFileToTag.txt" shared by "user0" + When "admin" adds the tag "MySecondTag" to "/myFileToTag.txt" shared by "user0" + Then The response should have a status code "201" + And "/myFileToTag.txt" shared by "user0" has the following tags for "admin" + |MyFirstTag| + |MySecondTag| + And "/myFileToTag.txt" shared by "user0" has the following tags for "user0" + |MyFirstTag| + + Scenario: Assigning a not user-assignable tag to a file shared by someone else as admin user should worj + Given user "user0" exists + Given "admin" creates a "normal" tag with name "MyFirstTag" + Given "admin" creates a "not user-assignable" tag with name "MySecondTag" + Given user "user0" uploads file "data/textfile.txt" to "/myFileToTag.txt" + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToTag.txt | + | shareWith | admin | + | shareType | 0 | + When "user0" adds the tag "MyFirstTag" to "/myFileToTag.txt" shared by "user0" + When "admin" adds the tag "MySecondTag" to "/myFileToTag.txt" shared by "user0" + Then The response should have a status code "201" + And "/myFileToTag.txt" shared by "user0" has the following tags for "admin" + |MyFirstTag| + |MySecondTag| + And "/myFileToTag.txt" shared by "user0" has the following tags for "user0" + |MyFirstTag| + |MySecondTag| + + Scenario: Unassigning a normal tag from a file shared by someone else as regular user should work + Given user "user0" exists + Given user "user1" exists + Given "admin" creates a "normal" tag with name "MyFirstTag" + Given "admin" creates a "normal" tag with name "MySecondTag" + Given user "user0" uploads file "data/textfile.txt" to "/myFileToTag.txt" + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToTag.txt | + | shareWith | user1 | + | shareType | 0 | + Given "user0" adds the tag "MyFirstTag" to "/myFileToTag.txt" shared by "user0" + Given "user0" adds the tag "MySecondTag" to "/myFileToTag.txt" shared by "user0" + When "user1" removes the tag "MyFirstTag" from "/myFileToTag.txt" shared by "user0" + Then The response should have a status code "204" + And "/myFileToTag.txt" shared by "user0" has the following tags for "user0" + |MySecondTag| + + Scenario: Unassigning a normal tag from a file unshared by someone else as regular user should fail + Given user "user0" exists + Given user "user1" exists + Given "admin" creates a "normal" tag with name "MyFirstTag" + Given "admin" creates a "normal" tag with name "MySecondTag" + Given user "user0" uploads file "data/textfile.txt" to "/myFileToTag.txt" + Given "user0" adds the tag "MyFirstTag" to "/myFileToTag.txt" shared by "user0" + Given "user0" adds the tag "MySecondTag" to "/myFileToTag.txt" shared by "user0" + When "user1" removes the tag "MyFirstTag" from "/myFileToTag.txt" shared by "user0" + Then The response should have a status code "404" + And "/myFileToTag.txt" shared by "user0" has the following tags for "user0" + |MyFirstTag| + |MySecondTag| + + Scenario: Unassigning a not user-visible tag from a file shared by someone else as regular user should fail + Given user "user0" exists + Given user "user1" exists + Given "admin" creates a "not user-visible" tag with name "MyFirstTag" + Given "admin" creates a "normal" tag with name "MySecondTag" + Given user "user0" uploads file "data/textfile.txt" to "/myFileToTag.txt" + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToTag.txt | + | shareWith | user1 | + | shareType | 0 | + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToTag.txt | + | shareWith | admin | + | shareType | 0 | + Given "admin" adds the tag "MyFirstTag" to "/myFileToTag.txt" shared by "user0" + Given "user0" adds the tag "MySecondTag" to "/myFileToTag.txt" shared by "user0" + When "user1" removes the tag "MyFirstTag" from "/myFileToTag.txt" shared by "user0" + Then The response should have a status code "404" + And "/myFileToTag.txt" shared by "user0" has the following tags for "user0" + |MySecondTag| + And "/myFileToTag.txt" shared by "user0" has the following tags for "admin" + |MyFirstTag| + |MySecondTag| + + Scenario: Unassigning a not user-visible tag from a file shared by someone else as admin should work + Given user "user0" exists + Given user "user1" exists + Given "admin" creates a "not user-visible" tag with name "MyFirstTag" + Given "admin" creates a "normal" tag with name "MySecondTag" + Given user "user0" uploads file "data/textfile.txt" to "/myFileToTag.txt" + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToTag.txt | + | shareWith | user1 | + | shareType | 0 | + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToTag.txt | + | shareWith | admin | + | shareType | 0 | + Given "admin" adds the tag "MyFirstTag" to "/myFileToTag.txt" shared by "user0" + Given "user0" adds the tag "MySecondTag" to "/myFileToTag.txt" shared by "user0" + When "admin" removes the tag "MyFirstTag" from "/myFileToTag.txt" shared by "user0" + Then The response should have a status code "204" + And "/myFileToTag.txt" shared by "user0" has the following tags for "user0" + |MySecondTag| + And "/myFileToTag.txt" shared by "user0" has the following tags for "admin" + |MySecondTag| + + Scenario: Unassigning a not user-visible tag from a file unshared by someone else should fail + Given user "user0" exists + Given user "user1" exists + Given "admin" creates a "not user-visible" tag with name "MyFirstTag" + Given "admin" creates a "normal" tag with name "MySecondTag" + Given user "user0" uploads file "data/textfile.txt" to "/myFileToTag.txt" + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToTag.txt | + | shareWith | user1 | + | shareType | 0 | + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToTag.txt | + | shareWith | admin | + | shareType | 0 | + Given "admin" adds the tag "MyFirstTag" to "/myFileToTag.txt" shared by "user0" + Given "user0" adds the tag "MySecondTag" to "/myFileToTag.txt" shared by "user0" + Given As "user0" remove all shares from the file named "/myFileToTag.txt" + When "admin" removes the tag "MyFirstTag" from "/myFileToTag.txt" shared by "user0" + Then The response should have a status code "404" + + Scenario: Unassigning a not user-assignable tag from a file shared by someone else as regular user should fail + Given user "user0" exists + Given user "user1" exists + Given "admin" creates a "not user-assignable" tag with name "MyFirstTag" + Given "admin" creates a "normal" tag with name "MySecondTag" + Given user "user0" uploads file "data/textfile.txt" to "/myFileToTag.txt" + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToTag.txt | + | shareWith | user1 | + | shareType | 0 | + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToTag.txt | + | shareWith | admin | + | shareType | 0 | + Given "admin" adds the tag "MyFirstTag" to "/myFileToTag.txt" shared by "user0" + Given "user0" adds the tag "MySecondTag" to "/myFileToTag.txt" shared by "user0" + When "user1" removes the tag "MyFirstTag" from "/myFileToTag.txt" shared by "user0" + Then The response should have a status code "403" + And "/myFileToTag.txt" shared by "user0" has the following tags for "user0" + |MyFirstTag| + |MySecondTag| + And "/myFileToTag.txt" shared by "user0" has the following tags for "admin" + |MyFirstTag| + |MySecondTag| + + Scenario: Unassigning a not user-assignable tag from a file shared by someone else as admin should work + Given user "user0" exists + Given user "user1" exists + Given "admin" creates a "not user-assignable" tag with name "MyFirstTag" + Given "admin" creates a "normal" tag with name "MySecondTag" + Given user "user0" uploads file "data/textfile.txt" to "/myFileToTag.txt" + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToTag.txt | + | shareWith | user1 | + | shareType | 0 | + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToTag.txt | + | shareWith | admin | + | shareType | 0 | + Given "admin" adds the tag "MyFirstTag" to "/myFileToTag.txt" shared by "user0" + Given "user0" adds the tag "MySecondTag" to "/myFileToTag.txt" shared by "user0" + When "admin" removes the tag "MyFirstTag" from "/myFileToTag.txt" shared by "user0" + Then The response should have a status code "204" + And "/myFileToTag.txt" shared by "user0" has the following tags for "user0" + |MySecondTag| + And "/myFileToTag.txt" shared by "user0" has the following tags for "admin" + |MySecondTag| + + Scenario: Unassigning a not user-assignable tag from a file unshared by someone else should fail + Given user "user0" exists + Given user "user1" exists + Given "admin" creates a "not user-assignable" tag with name "MyFirstTag" + Given "admin" creates a "normal" tag with name "MySecondTag" + Given user "user0" uploads file "data/textfile.txt" to "/myFileToTag.txt" + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToTag.txt | + | shareWith | user1 | + | shareType | 0 | + Given As "user0" sending "POST" to "/apps/files_sharing/api/v1/shares" with + | path | myFileToTag.txt | + | shareWith | admin | + | shareType | 0 | + Given "admin" adds the tag "MyFirstTag" to "/myFileToTag.txt" shared by "user0" + Given "user0" adds the tag "MySecondTag" to "/myFileToTag.txt" shared by "user0" + Given As "user0" remove all shares from the file named "/myFileToTag.txt" + When "admin" removes the tag "MyFirstTag" from "/myFileToTag.txt" shared by "user0" + Then The response should have a status code "404" + + Scenario: Overwriting existing normal tags should fail + Given user "user0" exists + Given "user0" creates a "normal" tag with name "MyFirstTag" + When "user0" creates a "normal" tag with name "MyFirstTag" + Then The response should have a status code "409" + + Scenario: Overwriting existing not user-assignable tags should fail + Given "admin" creates a "not user-assignable" tag with name "MyFirstTag" + When "admin" creates a "not user-assignable" tag with name "MyFirstTag" + Then The response should have a status code "409" + + Scenario: Overwriting existing not user-visible tags should fail + Given "admin" creates a "not user-visible" tag with name "MyFirstTag" + When "admin" creates a "not user-visible" tag with name "MyFirstTag" + Then The response should have a status code "409" + + Scenario: Getting tags only works with access to the file + Given user "user0" exists + Given user "user1" exists + Given "admin" creates a "normal" tag with name "MyFirstTag" + Given user "user0" uploads file "data/textfile.txt" to "/myFileToTag.txt" + When "user0" adds the tag "MyFirstTag" to "/myFileToTag.txt" shared by "user0" + And "/myFileToTag.txt" shared by "user0" has the following tags for "user0" + |MyFirstTag| + And "/myFileToTag.txt" shared by "user0" has the following tags for "user1" + || + And The response should have a status code "404" diff --git a/console.php b/console.php index d8c23d4ce00..eb6c84c3cf8 100644 --- a/console.php +++ b/console.php @@ -27,6 +27,7 @@ */ use OC\Console\Application; +use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Output\ConsoleOutput; define('OC_CONSOLE', 1); @@ -81,7 +82,7 @@ try { } $application = new Application(\OC::$server->getConfig(), \OC::$server->getEventDispatcher(), \OC::$server->getRequest()); - $application->loadCommands(new ConsoleOutput()); + $application->loadCommands(new ArgvInput(), new ConsoleOutput()); $application->run(); } catch (Exception $ex) { echo "An unhandled exception has been thrown:" . PHP_EOL; diff --git a/core/css/systemtags.css b/core/css/systemtags.css index 40f93011b6a..22e41ea53ca 100644 --- a/core/css/systemtags.css +++ b/core/css/systemtags.css @@ -13,9 +13,12 @@ } .systemtags-select2-dropdown .select2-highlighted, .systemtags-select2-dropdown .select2-selected.select2-highlighted { - background: #3875d7; + background: #f8f8f8; } +.select2-result { + position: relative; +} .systemtags-select2-dropdown .select2-highlighted { color: #000000; } @@ -33,10 +36,16 @@ .systemtags-select2-dropdown .select2-result-label .icon { display: inline-block; + opacity: .5; +} +.systemtags-select2-dropdown .select2-result-label .icon.rename { + padding: 4px; } .systemtags-select2-dropdown .systemtags-actions { - float: right; + position: absolute; + right: 0; + top: 3px; } .systemtags-select2-dropdown .systemtags-rename-form { @@ -44,7 +53,7 @@ margin-left: 10px; } -.systemtags-select2-container { +.systemtags-select2-container { width: 100%; } @@ -57,7 +66,7 @@ border-radius: 3px; border: 1px solid #ddd; margin: 3px 3px 3px 0; - padding: 7px 6px 5px; + padding: 2px 0; min-height: auto; } @@ -88,3 +97,22 @@ display: none; } +.select2-container-multi .select2-choices .select2-search-choice { + background-color: rgba(240,240,240,.9); + border-color: rgba(240,240,240,.9); + box-shadow: none; + background-image: none; +} +.select2-results .select2-highlighted { + background-color: rgba(240,240,240,.9); + color: #000; +} + +.select2-container-multi.select2-container-active .select2-choices, +.select2-drop-active { + border-color: #ddd; +} +.select2-container-multi.select2-container-active .select2-choices { + -webkit-box-shadow: none; + box-shadow: none; +} diff --git a/core/js/setupchecks.js b/core/js/setupchecks.js index 1819b5a9c1e..c29c4137c58 100644 --- a/core/js/setupchecks.js +++ b/core/js/setupchecks.js @@ -276,7 +276,7 @@ var minimumSeconds = 15768000; if(isNaN(transportSecurityValidity) || transportSecurityValidity <= (minimumSeconds - 1)) { messages.push({ - msg: t('core', 'The "Strict-Transport-Security" HTTP header is not configured to least "{seconds}" seconds. For enhanced security we recommend enabling HSTS as described in our <a href="{docUrl}">security tips</a>.', {'seconds': minimumSeconds, docUrl: '#admin-tips'}), + msg: t('core', 'The "Strict-Transport-Security" HTTP header is not configured to at least "{seconds}" seconds. For enhanced security we recommend enabling HSTS as described in our <a href="{docUrl}">security tips</a>.', {'seconds': minimumSeconds, docUrl: '#admin-tips'}), type: OC.SetupChecks.MESSAGE_TYPE_WARNING }); } diff --git a/core/js/tests/specs/setupchecksSpec.js b/core/js/tests/specs/setupchecksSpec.js index 59df3a58746..fa0974c90f9 100644 --- a/core/js/tests/specs/setupchecksSpec.js +++ b/core/js/tests/specs/setupchecksSpec.js @@ -542,7 +542,7 @@ describe('OC.SetupChecks tests', function() { async.done(function( data, s, x ){ expect(data).toEqual([{ - msg: 'The "Strict-Transport-Security" HTTP header is not configured to least "15768000" seconds. For enhanced security we recommend enabling HSTS as described in our <a href="#admin-tips">security tips</a>.', + msg: 'The "Strict-Transport-Security" HTTP header is not configured to at least "15768000" seconds. For enhanced security we recommend enabling HSTS as described in our <a href="#admin-tips">security tips</a>.', type: OC.SetupChecks.MESSAGE_TYPE_WARNING }]); done(); @@ -567,7 +567,7 @@ describe('OC.SetupChecks tests', function() { async.done(function( data, s, x ){ expect(data).toEqual([{ - msg: 'The "Strict-Transport-Security" HTTP header is not configured to least "15768000" seconds. For enhanced security we recommend enabling HSTS as described in our <a href="#admin-tips">security tips</a>.', + msg: 'The "Strict-Transport-Security" HTTP header is not configured to at least "15768000" seconds. For enhanced security we recommend enabling HSTS as described in our <a href="#admin-tips">security tips</a>.', type: OC.SetupChecks.MESSAGE_TYPE_WARNING }]); done(); @@ -592,7 +592,7 @@ describe('OC.SetupChecks tests', function() { async.done(function( data, s, x ){ expect(data).toEqual([{ - msg: 'The "Strict-Transport-Security" HTTP header is not configured to least "15768000" seconds. For enhanced security we recommend enabling HSTS as described in our <a href="#admin-tips">security tips</a>.', + msg: 'The "Strict-Transport-Security" HTTP header is not configured to at least "15768000" seconds. For enhanced security we recommend enabling HSTS as described in our <a href="#admin-tips">security tips</a>.', type: OC.SetupChecks.MESSAGE_TYPE_WARNING }]); done(); diff --git a/issue_template.md b/issue_template.md index 424f6c062a9..e19583977ef 100644 --- a/issue_template.md +++ b/issue_template.md @@ -1,3 +1,10 @@ +<!-- +Thanks for reporting issues back to ownCloud! This is the issue tracker of ownCloud, if you have any support question please check out https://owncloud.org/support + +This is the bug tracker for the Server component. Find other components at https://github.com/owncloud/core/blob/master/CONTRIBUTING.md#guidelines + +To make it possible for us to help you please fill out below information carefully. +--> ### Steps to reproduce 1. 2. diff --git a/lib/private/api.php b/lib/private/api.php index 6c6be233c9d..87f2aa9b118 100644 --- a/lib/private/api.php +++ b/lib/private/api.php @@ -188,7 +188,7 @@ class OC_API { /** * merge the returned result objects into one response * @param array $responses - * @return array|\OC_OCS_Result + * @return OC_OCS_Result */ public static function mergeResponses($responses) { // Sort into shipped and third-party @@ -442,6 +442,7 @@ class OC_API { /** * Based on the requested format the response content type is set + * @param string $format */ public static function setContentType($format = null) { $format = is_null($format) ? self::requestedFormat() : $format; diff --git a/lib/private/avatarmanager.php b/lib/private/avatarmanager.php index b39f5495122..b2d3e6eb3dd 100644 --- a/lib/private/avatarmanager.php +++ b/lib/private/avatarmanager.php @@ -26,6 +26,8 @@ namespace OC; +use OCP\Files\Folder; +use OCP\Files\NotFoundException; use OCP\IAvatarManager; use OCP\IUserManager; use OCP\Files\IRootFolder; @@ -45,6 +47,13 @@ class AvatarManager implements IAvatarManager { /** @var IL10N */ private $l; + /** + * AvatarManager constructor. + * + * @param IUserManager $userManager + * @param IRootFolder $rootFolder + * @param IL10N $l + */ public function __construct( IUserManager $userManager, IRootFolder $rootFolder, @@ -57,15 +66,26 @@ class AvatarManager implements IAvatarManager { /** * return a user specific instance of \OCP\IAvatar * @see \OCP\IAvatar - * @param string $user the ownCloud user id + * @param string $userId the ownCloud user id * @return \OCP\IAvatar * @throws \Exception In case the username is potentially dangerous + * @throws NotFoundException In case there is no user folder yet */ public function getAvatar($userId) { $user = $this->userManager->get($userId); if (is_null($user)) { throw new \Exception('user does not exist'); } - return new Avatar($this->rootFolder->getUserFolder($userId)->getParent(), $this->l, $user); + + /* + * Fix for #22119 + * Basically we do not want to copy the skeleton folder + */ + \OC\Files\Filesystem::initMountPoints($userId); + $dir = '/' . $userId; + /** @var Folder $folder */ + $folder = $this->rootFolder->get($dir); + + return new Avatar($folder, $this->l, $user); } } diff --git a/lib/private/console/application.php b/lib/private/console/application.php index 10ff69b1c80..0895f1788af 100644 --- a/lib/private/console/application.php +++ b/lib/private/console/application.php @@ -31,6 +31,7 @@ use OCP\IRequest; use Symfony\Component\Console\Application as SymfonyApplication; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -56,12 +57,31 @@ class Application { } /** + * @param InputInterface $input * @param OutputInterface $output * @throws \Exception */ - public function loadCommands(OutputInterface $output) { + public function loadCommands(InputInterface $input, OutputInterface $output) { // $application is required to be defined in the register_command scripts $application = $this->application; + $inputDefinition = $application->getDefinition(); + $inputDefinition->addOption( + new InputOption( + 'no-warnings', + null, + InputOption::VALUE_NONE, + 'Skip global warnings, show command output only', + null + ) + ); + try { + $input->bind($inputDefinition); + } catch (\RuntimeException $e) { + //expected if there are extra options + } + if ($input->getOption('no-warnings')) { + $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); + } require_once __DIR__ . '/../../../core/register_command.php'; if ($this->config->getSystemValue('installed', false)) { if (\OCP\Util::needUpgrade()) { diff --git a/lib/private/db/adaptersqlsrv.php b/lib/private/db/adaptersqlsrv.php deleted file mode 100644 index f208b2ba787..00000000000 --- a/lib/private/db/adaptersqlsrv.php +++ /dev/null @@ -1,37 +0,0 @@ -<?php -/** - * @author Bart Visscher <bartv@thisnet.nl> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - - -namespace OC\DB; - -class AdapterSQLSrv extends Adapter { - public function fixupStatement($statement) { - $statement = str_replace(' ILIKE ', ' COLLATE Latin1_General_CI_AS LIKE ', $statement); - $statement = preg_replace( "/\`(.*?)`/", "[$1]", $statement ); - $statement = str_ireplace( 'NOW()', 'CURRENT_TIMESTAMP', $statement ); - $statement = str_replace( 'LENGTH(', 'LEN(', $statement ); - $statement = str_replace( 'SUBSTR(', 'SUBSTRING(', $statement ); - $statement = str_ireplace( 'UNIX_TIMESTAMP()', 'DATEDIFF(second,{d \'1970-01-01\'},GETDATE())', $statement ); - return $statement; - } -} diff --git a/lib/private/files/storage/common.php b/lib/private/files/storage/common.php index edc570c967d..1d4801e5b97 100644 --- a/lib/private/files/storage/common.php +++ b/lib/private/files/storage/common.php @@ -613,6 +613,10 @@ abstract class Common implements Storage, ILockingStorage { return $this->rename($sourceInternalPath, $targetInternalPath); } + if (!$sourceStorage->isDeletable($sourceInternalPath)) { + return false; + } + $result = $this->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, true); if ($result) { if ($sourceStorage->is_dir($sourceInternalPath)) { diff --git a/lib/private/files/storage/wrapper/encryption.php b/lib/private/files/storage/wrapper/encryption.php index 26905dfb388..11c6084d00c 100644 --- a/lib/private/files/storage/wrapper/encryption.php +++ b/lib/private/files/storage/wrapper/encryption.php @@ -459,6 +459,10 @@ class Encryption extends Wrapper { // - copy the copyKeys() call from $this->copyBetweenStorage to this method // - remove $this->copyBetweenStorage + if (!$sourceStorage->isDeletable($sourceInternalPath)) { + return false; + } + $result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true); if ($result) { if ($sourceStorage->is_dir($sourceInternalPath)) { diff --git a/lib/private/files/stream/encryption.php b/lib/private/files/stream/encryption.php index 63949035b5a..37a1d75519d 100644 --- a/lib/private/files/stream/encryption.php +++ b/lib/private/files/stream/encryption.php @@ -183,7 +183,7 @@ class Encryption extends Wrapper { * * @param resource $source * @param string $mode - * @param array $context + * @param resource $context * @param string $protocol * @param string $class * @return resource diff --git a/lib/private/group/dummy.php b/lib/private/group/dummy.php index c0d206a34e1..97f00385954 100644 --- a/lib/private/group/dummy.php +++ b/lib/private/group/dummy.php @@ -114,6 +114,7 @@ class OC_Group_Dummy extends OC_Group_Backend { if(isset($this->groups[$gid])) { if(($index=array_search($uid, $this->groups[$gid]))!==false) { unset($this->groups[$gid][$index]); + return true; }else{ return false; } diff --git a/lib/private/group/manager.php b/lib/private/group/manager.php index 98e5551bcc5..7eca249c701 100644 --- a/lib/private/group/manager.php +++ b/lib/private/group/manager.php @@ -150,6 +150,10 @@ class Manager extends PublicEmitter implements IGroupManager { return $this->getGroupObject($gid); } + /** + * @param string $gid + * @return \OCP\IGroup + */ protected function getGroupObject($gid) { $backends = array(); foreach ($this->backends as $backend) { diff --git a/lib/private/memcache/factory.php b/lib/private/memcache/factory.php index 21149d8b6bf..204ded7d5ab 100644 --- a/lib/private/memcache/factory.php +++ b/lib/private/memcache/factory.php @@ -172,7 +172,7 @@ class Factory implements ICacheFactory { /** * @see \OC\Memcache\Factory::createLocal() * @param string $prefix - * @return \OC\Memcache\Cache|null + * @return Cache */ public function createLowLatency($prefix = '') { return $this->createLocal($prefix); diff --git a/lib/private/preview.php b/lib/private/preview.php index df6eeceddcb..4fca56dd984 100644 --- a/lib/private/preview.php +++ b/lib/private/preview.php @@ -202,7 +202,7 @@ class Preview { /** * returns the max width set in ownCloud's config * - * @return string + * @return integer */ public function getConfigMaxX() { return $this->configMaxWidth; @@ -211,7 +211,7 @@ class Preview { /** * returns the max height set in ownCloud's config * - * @return string + * @return integer */ public function getConfigMaxY() { return $this->configMaxHeight; @@ -546,7 +546,7 @@ class Preview { /** * Determines the size of the preview we should be looking for in the cache * - * @return int[] + * @return integer[] */ private function simulatePreviewDimensions() { $askedWidth = $this->getMaxX(); @@ -570,7 +570,7 @@ class Preview { * * @param int $originalWidth * @param int $originalHeight - * @return \int[] + * @return integer[] */ private function applyAspectRatio($askedWidth, $askedHeight, $originalWidth = 0, $originalHeight = 0) { if(!$originalWidth){ @@ -602,7 +602,7 @@ class Preview { * @param int $askedHeight * @param int $previewWidth * @param int $previewHeight - * @return \int[] + * @return integer[] */ private function applyCover($askedWidth, $askedHeight, $previewWidth, $previewHeight) { $originalRatio = $previewWidth / $previewHeight; @@ -628,7 +628,7 @@ class Preview { * @param int $askedWidth * @param int $askedHeight * - * @return \int[] + * @return integer[] */ private function fixSize($askedWidth, $askedHeight) { if ($this->scalingUp) { @@ -921,7 +921,7 @@ class Preview { * @param int $askedWidth * @param int $askedHeight * @param int $previewWidth - * @param null $previewHeight + * @param int $previewHeight * * @return int[] */ @@ -971,7 +971,7 @@ class Preview { * @param int $askedWidth * @param int $askedHeight * @param int $previewWidth - * @param null $previewHeight + * @param int $previewHeight */ private function crop($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight = null) { $cropX = floor(abs($askedWidth - $previewWidth) * 0.5); @@ -990,7 +990,7 @@ class Preview { * @param int $askedWidth * @param int $askedHeight * @param int $previewWidth - * @param null $previewHeight + * @param int $previewHeight */ private function cropAndFill($image, $askedWidth, $askedHeight, $previewWidth, $previewHeight) { if ($previewWidth > $askedWidth) { @@ -1218,7 +1218,7 @@ class Preview { * @param int $maxDim * @param string $dimName * - * @return mixed + * @return integer */ private function limitMaxDim($dim, $maxDim, $dimName) { if (!is_null($maxDim)) { diff --git a/lib/private/preview/movie.php b/lib/private/preview/movie.php index ee56f017229..43a8d674fc9 100644 --- a/lib/private/preview/movie.php +++ b/lib/private/preview/movie.php @@ -83,9 +83,9 @@ class Movie extends Provider { $tmpPath = \OC::$server->getTempManager()->getTemporaryFile(); if (self::$avconvBinary) { - $cmd = self::$avconvBinary . ' -an -y -ss ' . escapeshellarg($second) . + $cmd = self::$avconvBinary . ' -y -ss ' . escapeshellarg($second) . ' -i ' . escapeshellarg($absPath) . - ' -f mjpeg -vframes 1 -vsync 1 ' . escapeshellarg($tmpPath) . + ' -an -f mjpeg -vframes 1 -vsync 1 ' . escapeshellarg($tmpPath) . ' > /dev/null 2>&1'; } else { $cmd = self::$ffmpegBinary . ' -y -ss ' . escapeshellarg($second) . diff --git a/lib/private/share20/manager.php b/lib/private/share20/manager.php index 4cff3dc818b..9b33e947557 100644 --- a/lib/private/share20/manager.php +++ b/lib/private/share20/manager.php @@ -517,8 +517,20 @@ class Manager implements IManager { // Verify if there are any issues with the path $this->pathCreateChecks($share->getNode()); - // On creation of a share the owner is always the owner of the path - $share->setShareOwner($share->getNode()->getOwner()->getUID()); + /* + * On creation of a share the owner is always the owner of the path + * Except for mounted federated shares. + */ + $storage = $share->getNode()->getStorage(); + if ($storage->instanceOfStorage('OCA\Files_Sharing\External\Storage')) { + $parent = $share->getNode()->getParent(); + while($parent->getStorage()->instanceOfStorage('OCA\Files_Sharing\External\Storage')) { + $parent = $parent->getParent(); + } + $share->setShareOwner($parent->getOwner()->getUID()); + } else { + $share->setShareOwner($share->getNode()->getOwner()->getUID()); + } // Cannot share with the owner if ($share->getShareType() === \OCP\Share::SHARE_TYPE_USER && diff --git a/lib/public/appframework/utility/icontrollermethodreflector.php b/lib/public/appframework/utility/icontrollermethodreflector.php index b2f91fdb170..7bf422aa567 100644 --- a/lib/public/appframework/utility/icontrollermethodreflector.php +++ b/lib/public/appframework/utility/icontrollermethodreflector.php @@ -35,6 +35,7 @@ interface IControllerMethodReflector { /** * @param object $object an object or classname * @param string $method the method which we want to inspect + * @return void * @since 8.0.0 */ public function reflect($object, $method); diff --git a/lib/public/iavatarmanager.php b/lib/public/iavatarmanager.php index 264c4fcf051..cb63ccaf6fd 100644 --- a/lib/public/iavatarmanager.php +++ b/lib/public/iavatarmanager.php @@ -36,6 +36,8 @@ interface IAvatarManager { * @see \OCP\IAvatar * @param string $user the ownCloud user id * @return \OCP\IAvatar + * @throws \Exception In case the username is potentially dangerous + * @throws \OCP\Files\NotFoundException In case there is no user folder yet * @since 6.0.0 */ public function getAvatar($user); diff --git a/lib/public/search/pagedprovider.php b/lib/public/search/pagedprovider.php index 93289a1bde4..c8530626e59 100644 --- a/lib/public/search/pagedprovider.php +++ b/lib/public/search/pagedprovider.php @@ -58,7 +58,7 @@ abstract class PagedProvider extends Provider { * Search for $query * @param string $query * @param int $page pages start at page 1 - * @param int $size, 0 = SIZE_ALL + * @param int $size 0 = SIZE_ALL * @return array An array of OCP\Search\Result's * @since 8.0.0 */ diff --git a/settings/controller/userscontroller.php b/settings/controller/userscontroller.php index 3e5455751ab..0abcabed11c 100644 --- a/settings/controller/userscontroller.php +++ b/settings/controller/userscontroller.php @@ -176,7 +176,11 @@ class UsersController extends Controller { $avatarAvailable = false; if ($this->config->getSystemValue('enable_avatars', true) === true) { - $avatarAvailable = $this->avatarManager->getAvatar($user->getUID())->exists(); + try { + $avatarAvailable = $this->avatarManager->getAvatar($user->getUID())->exists(); + } catch (\Exception $e) { + //No avatar yet + } } return [ diff --git a/settings/templates/admin.php b/settings/templates/admin.php index 539e4b94b8b..a51b9aa16e2 100644 --- a/settings/templates/admin.php +++ b/settings/templates/admin.php @@ -110,7 +110,7 @@ if ($_['WindowsWarning']) { foreach ($_['OutdatedCacheWarning'] as $php_module => $data) { ?> <li> - <?php p($l->t('%1$s below version %2$s is installed, for stability and performance reasons we recommend to update to a newer %1$s version.', $data)); ?> + <?php p($l->t('%1$s below version %2$s is installed, for stability and performance reasons we recommend updating to a newer %1$s version.', $data)); ?> </li> <?php } diff --git a/tests/lib/avatarmanagertest.php b/tests/lib/avatarmanagertest.php index cb9068c46a6..f5cdd99176d 100644 --- a/tests/lib/avatarmanagertest.php +++ b/tests/lib/avatarmanagertest.php @@ -19,30 +19,32 @@ * */ use OC\AvatarManager; -use OCP\Files\IRootFolder; -use OCP\IUserManager; +use Test\Traits\UserTrait; +use Test\Traits\MountProviderTrait; +/** + * Class AvatarManagerTest + * @group DB + */ class AvatarManagerTest extends \Test\TestCase { - /** @var IRootFolder */ - private $rootFolder; + use UserTrait; + use MountProviderTrait; - /** @var AvatarManager */ + /** @var AvatarManager */ private $avatarManager; - /** @var IUserManager */ - private $userManager; + /** @var \OC\Files\Storage\Temporary */ + private $storage; public function setUp() { parent::setUp(); - $this->rootFolder = $this->getMock('\OCP\Files\IRootFolder'); - $this->userManager = $this->getMock('\OCP\IUserManager'); - $l = $this->getMock('\OCP\IL10N'); - $l->method('t')->will($this->returnArgument(0)); - $this->avatarManager = new \OC\AvatarManager( - $this->userManager, - $this->rootFolder, - $l);; + $this->createUser('valid-user', 'valid-user'); + + $this->storage = new \OC\Files\Storage\Temporary(); + $this->registerMount('valid-user', $this->storage, '/valid-user/'); + + $this->avatarManager = \OC::$server->getAvatarManager(); } /** @@ -54,23 +56,10 @@ class AvatarManagerTest extends \Test\TestCase { } public function testGetAvatarValidUser() { - $this->userManager->expects($this->once()) - ->method('get') - ->with('validUser') - ->willReturn(true); - - $folder = $this->getMock('\OCP\Files\Folder'); - $this->rootFolder->expects($this->once()) - ->method('getUserFolder') - ->with('validUser') - ->willReturn($folder); - - $folder->expects($this->once()) - ->method('getParent') - ->will($this->returnSelf()); - - $this->avatarManager->getAvatar('validUser'); + $avatar = $this->avatarManager->getAvatar('valid-user'); + $this->assertInstanceOf('\OCP\IAvatar', $avatar); + $this->assertFalse($this->storage->file_exists('files')); } } diff --git a/tests/lib/share20/managertest.php b/tests/lib/share20/managertest.php index fe94b72c4e6..c41f0754396 100644 --- a/tests/lib/share20/managertest.php +++ b/tests/lib/share20/managertest.php @@ -1429,9 +1429,11 @@ class ManagerTest extends \Test\TestCase { $shareOwner = $this->getMock('\OCP\IUser'); $shareOwner->method('getUID')->willReturn('shareOwner'); + $storage = $this->getMock('\OCP\Files\Storage'); $path = $this->getMock('\OCP\Files\File'); $path->method('getOwner')->willReturn($shareOwner); $path->method('getName')->willReturn('target'); + $path->method('getStorage')->willReturn($storage); $share = $this->createShare( null, @@ -1480,9 +1482,11 @@ class ManagerTest extends \Test\TestCase { $shareOwner = $this->getMock('\OCP\IUser'); $shareOwner->method('getUID')->willReturn('shareOwner'); + $storage = $this->getMock('\OCP\Files\Storage'); $path = $this->getMock('\OCP\Files\File'); $path->method('getOwner')->willReturn($shareOwner); $path->method('getName')->willReturn('target'); + $path->method('getStorage')->willReturn($storage); $share = $this->createShare( null, @@ -1539,10 +1543,12 @@ class ManagerTest extends \Test\TestCase { $shareOwner = $this->getMock('\OCP\IUser'); $shareOwner->method('getUID')->willReturn('shareOwner'); + $storage = $this->getMock('\OCP\Files\Storage'); $path = $this->getMock('\OCP\Files\File'); $path->method('getOwner')->willReturn($shareOwner); $path->method('getName')->willReturn('target'); $path->method('getId')->willReturn(1); + $path->method('getStorage')->willReturn($storage); $date = new \DateTime(); @@ -1663,9 +1669,11 @@ class ManagerTest extends \Test\TestCase { $shareOwner = $this->getMock('\OCP\IUser'); $shareOwner->method('getUID')->willReturn('shareOwner'); + $storage = $this->getMock('\OCP\Files\Storage'); $path = $this->getMock('\OCP\Files\File'); $path->method('getOwner')->willReturn($shareOwner); $path->method('getName')->willReturn('target'); + $path->method('getStorage')->willReturn($storage); $share = $this->createShare( null, @@ -1697,8 +1705,86 @@ class ManagerTest extends \Test\TestCase { ->method('setTarget') ->with('/target'); - $dummy = new DummyCreate(); - \OCP\Util::connectHook('OCP\Share', 'pre_shared', $dummy, 'listner'); + $hookListner = $this->getMockBuilder('Dummy')->setMethods(['pre'])->getMock(); + \OCP\Util::connectHook('OCP\Share', 'pre_shared', $hookListner, 'pre'); + $hookListner->expects($this->once()) + ->method('pre') + ->will($this->returnCallback(function (array $data) { + $data['run'] = false; + $data['error'] = 'I won\'t let you share!'; + })); + + $manager->createShare($share); + } + + public function testCreateShareOfIncommingFederatedShare() { + $manager = $this->createManagerMock() + ->setMethods(['canShare', 'generalCreateChecks', 'userCreateChecks', 'pathCreateChecks']) + ->getMock(); + + $shareOwner = $this->getMock('\OCP\IUser'); + $shareOwner->method('getUID')->willReturn('shareOwner'); + + $storage = $this->getMock('\OCP\Files\Storage'); + $storage->method('instanceOfStorage') + ->with('OCA\Files_Sharing\External\Storage') + ->willReturn(true); + + $storage2 = $this->getMock('\OCP\Files\Storage'); + $storage2->method('instanceOfStorage') + ->with('OCA\Files_Sharing\External\Storage') + ->willReturn(false); + + $path = $this->getMock('\OCP\Files\File'); + $path->expects($this->never())->method('getOwner'); + $path->method('getName')->willReturn('target'); + $path->method('getStorage')->willReturn($storage); + + $parent = $this->getMock('\OCP\Files\Folder'); + $parent->method('getStorage')->willReturn($storage); + + $parentParent = $this->getMock('\OCP\Files\Folder'); + $parentParent->method('getStorage')->willReturn($storage2); + $parentParent->method('getOwner')->willReturn($shareOwner); + + $path->method('getParent')->willReturn($parent); + $parent->method('getParent')->willReturn($parentParent); + + $share = $this->createShare( + null, + \OCP\Share::SHARE_TYPE_USER, + $path, + 'sharedWith', + 'sharedBy', + null, + \OCP\Constants::PERMISSION_ALL); + + $manager->expects($this->once()) + ->method('canShare') + ->with($share) + ->willReturn(true); + $manager->expects($this->once()) + ->method('generalCreateChecks') + ->with($share);; + $manager->expects($this->once()) + ->method('userCreateChecks') + ->with($share);; + $manager->expects($this->once()) + ->method('pathCreateChecks') + ->with($path); + + $this->defaultProvider + ->expects($this->once()) + ->method('create') + ->with($share) + ->will($this->returnArgument(0)); + + $share->expects($this->once()) + ->method('setShareOwner') + ->with('shareOwner'); + $share->expects($this->once()) + ->method('setTarget') + ->with('/target'); $manager->createShare($share); } @@ -2269,13 +2355,6 @@ class DummyPassword { } } -class DummyCreate { - public function listner($array) { - $array['run'] = false; - $array['error'] = 'I won\'t let you share!'; - } -} - class DummyFactory implements IProviderFactory { /** @var IShareProvider */ diff --git a/tests/settings/controller/userscontrollertest.php b/tests/settings/controller/userscontrollertest.php index 947540fa67b..6f07f34ba8d 100644 --- a/tests/settings/controller/userscontrollertest.php +++ b/tests/settings/controller/userscontrollertest.php @@ -1696,6 +1696,32 @@ class UsersControllerTest extends \Test\TestCase { $this->assertEquals($expectedResult, $result); } + public function testNoAvatar() { + $this->container['IsAdmin'] = true; + + list($user, $expectedResult) = $this->mockUser(); + + $subadmin = $this->getMockBuilder('\OC\SubAdmin') + ->disableOriginalConstructor() + ->getMock(); + $subadmin->expects($this->once()) + ->method('getSubAdminsGroups') + ->with($user) + ->will($this->returnValue([])); + $this->container['GroupManager'] + ->expects($this->any()) + ->method('getSubAdmin') + ->will($this->returnValue($subadmin)); + + $this->container['OCP\\IAvatarManager'] + ->method('getAvatar') + ->will($this->throwException(new \OCP\Files\NotFoundException())); + $expectedResult['isAvatarAvailable'] = false; + + $result = self::invokePrivate($this->container['UsersController'], 'formatUserForIndex', [$user]); + $this->assertEquals($expectedResult, $result); + } + /** * @return array */ |