diff options
Diffstat (limited to 'apps')
-rw-r--r-- | apps/dav/appinfo/v1/caldav.php | 4 | ||||
-rw-r--r-- | apps/dav/lib/AppInfo/Application.php | 63 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Activity/Backend.php | 466 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Activity/Extension.php | 378 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/CalDavBackend.php | 89 | ||||
-rw-r--r-- | apps/dav/lib/Command/CreateCalendar.php | 4 | ||||
-rw-r--r-- | apps/dav/lib/Connector/Sabre/FilesPlugin.php | 50 | ||||
-rw-r--r-- | apps/dav/lib/RootCollection.php | 2 | ||||
-rw-r--r-- | apps/dav/tests/unit/CalDAV/AbstractCalDavBackendTest.php | 51 | ||||
-rw-r--r-- | apps/dav/tests/unit/CalDAV/Activity/BackendTest.php | 332 | ||||
-rw-r--r-- | apps/dav/tests/unit/CalDAV/CalDavBackendTest.php | 50 | ||||
-rw-r--r-- | apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php | 22 | ||||
-rw-r--r-- | apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php | 81 | ||||
-rw-r--r-- | apps/files/js/file-upload.js | 85 | ||||
-rw-r--r-- | apps/files/js/templates/detailsview.handlebars.js | 28 | ||||
-rw-r--r-- | apps/files/tests/js/fileUploadSpec.js | 61 | ||||
-rw-r--r-- | apps/sharebymail/img/app.svg | 1 |
17 files changed, 1550 insertions, 217 deletions
diff --git a/apps/dav/appinfo/v1/caldav.php b/apps/dav/appinfo/v1/caldav.php index ba49f2961f4..9f12a92f4cd 100644 --- a/apps/dav/appinfo/v1/caldav.php +++ b/apps/dav/appinfo/v1/caldav.php @@ -46,10 +46,10 @@ $principalBackend = new Principal( 'principals/' ); $db = \OC::$server->getDatabaseConnection(); -$config = \OC::$server->getConfig(); $userManager = \OC::$server->getUserManager(); $random = \OC::$server->getSecureRandom(); -$calDavBackend = new CalDavBackend($db, $principalBackend, $userManager, $config, $random); +$dispatcher = \OC::$server->getEventDispatcher(); +$calDavBackend = new CalDavBackend($db, $principalBackend, $userManager, $random, $dispatcher); $debugging = \OC::$server->getConfig()->getSystemValue('debug', false); diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index 2bdcff9cf84..c777f5e5a35 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -24,6 +24,8 @@ */ namespace OCA\DAV\AppInfo; +use OCA\DAV\CalDAV\Activity\Backend; +use OCA\DAV\CalDAV\Activity\Extension; use OCA\DAV\CalDAV\BirthdayService; use OCA\DAV\Capabilities; use OCA\DAV\CardDAV\ContactsManager; @@ -88,6 +90,67 @@ class Application extends App { ); } }); + + $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::createCalendar', function(GenericEvent $event) { + /** @var Backend $backend */ + $backend = $this->getContainer()->query(Backend::class); + $backend->onCalendarAdd( + $event->getArgument('calendarData') + ); + }); + $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::updateCalendar', function(GenericEvent $event) { + /** @var Backend $backend */ + $backend = $this->getContainer()->query(Backend::class); + $backend->onCalendarUpdate( + $event->getArgument('calendarData'), + $event->getArgument('shares'), + $event->getArgument('propertyMutations') + ); + }); + $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar', function(GenericEvent $event) { + /** @var Backend $backend */ + $backend = $this->getContainer()->query(Backend::class); + $backend->onCalendarDelete( + $event->getArgument('calendarData'), + $event->getArgument('shares') + ); + }); + $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::updateShares', function(GenericEvent $event) { + /** @var Backend $backend */ + $backend = $this->getContainer()->query(Backend::class); + $backend->onCalendarUpdateShares( + $event->getArgument('calendarData'), + $event->getArgument('shares'), + $event->getArgument('add'), + $event->getArgument('remove') + ); + }); + + $listener = function(GenericEvent $event, $eventName) { + /** @var Backend $backend */ + $backend = $this->getContainer()->query(Backend::class); + + $subject = Extension::SUBJECT_OBJECT_ADD; + if ($eventName === '\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject') { + $subject = Extension::SUBJECT_OBJECT_UPDATE; + } else if ($eventName === '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject') { + $subject = Extension::SUBJECT_OBJECT_DELETE; + } + $backend->onTouchCalendarObject( + $subject, + $event->getArgument('calendarData'), + $event->getArgument('shares'), + $event->getArgument('objectData') + ); + }; + $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', $listener); + $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', $listener); + $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', $listener); + + $aM = $this->getContainer()->getServer()->getActivityManager(); + $aM->registerExtension(function() { + return $this->getContainer()->query(Extension::class); + }); } public function getSyncService() { diff --git a/apps/dav/lib/CalDAV/Activity/Backend.php b/apps/dav/lib/CalDAV/Activity/Backend.php new file mode 100644 index 00000000000..6cf09f6de46 --- /dev/null +++ b/apps/dav/lib/CalDAV/Activity/Backend.php @@ -0,0 +1,466 @@ +<?php +/** + * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\DAV\CalDAV\Activity; + + +use OCP\Activity\IEvent; +use OCP\Activity\IManager as IActivityManager; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserSession; +use Sabre\VObject\Reader; + +/** + * Class Backend + * + * @package OCA\DAV\CalDAV\Activity + */ +class Backend { + + /** @var IActivityManager */ + protected $activityManager; + + /** @var IGroupManager */ + protected $groupManager; + + /** @var IUserSession */ + protected $userSession; + + /** + * @param IActivityManager $activityManager + * @param IGroupManager $groupManager + * @param IUserSession $userSession + */ + public function __construct(IActivityManager $activityManager, IGroupManager $groupManager, IUserSession $userSession) { + $this->activityManager = $activityManager; + $this->groupManager = $groupManager; + $this->userSession = $userSession; + } + + /** + * Creates activities when a calendar was creates + * + * @param array $calendarData + */ + public function onCalendarAdd(array $calendarData) { + $this->triggerCalendarActivity(Extension::SUBJECT_ADD, $calendarData); + } + + /** + * Creates activities when a calendar was updated + * + * @param array $calendarData + * @param array $shares + * @param array $properties + */ + public function onCalendarUpdate(array $calendarData, array $shares, array $properties) { + $this->triggerCalendarActivity(Extension::SUBJECT_UPDATE, $calendarData, $shares, $properties); + } + + /** + * Creates activities when a calendar was deleted + * + * @param array $calendarData + * @param array $shares + */ + public function onCalendarDelete(array $calendarData, array $shares) { + $this->triggerCalendarActivity(Extension::SUBJECT_DELETE, $calendarData, $shares); + } + + /** + * Creates activities for all related users when a calendar was touched + * + * @param string $action + * @param array $calendarData + * @param array $shares + * @param array $changedProperties + */ + protected function triggerCalendarActivity($action, array $calendarData, array $shares = [], array $changedProperties = []) { + if (!isset($calendarData['principaluri'])) { + return; + } + + $principal = explode('/', $calendarData['principaluri']); + $owner = $principal[2]; + + $currentUser = $this->userSession->getUser(); + if ($currentUser instanceof IUser) { + $currentUser = $currentUser->getUID(); + } else { + $currentUser = $owner; + } + + $event = $this->activityManager->generateEvent(); + $event->setApp('dav') + ->setObject(Extension::CALENDAR, $calendarData['id']) + ->setType(Extension::CALENDAR) + ->setAuthor($currentUser); + + $changedVisibleInformation = array_intersect([ + '{DAV:}displayname', + '{http://apple.com/ns/ical/}calendar-color' + ], array_keys($changedProperties)); + + if (empty($shares) || ($action === Extension::SUBJECT_UPDATE && empty($changedVisibleInformation))) { + $users = [$owner]; + } else { + $users = $this->getUsersForShares($shares); + $users[] = $owner; + } + + foreach ($users as $user) { + $event->setAffectedUser($user) + ->setSubject( + $user === $currentUser ? $action . '_self' : $action, + [ + $currentUser, + $calendarData['{DAV:}displayname'], + ] + ); + $this->activityManager->publish($event); + } + } + + /** + * Creates activities for all related users when a calendar was (un-)shared + * + * @param array $calendarData + * @param array $shares + * @param array $add + * @param array $remove + */ + public function onCalendarUpdateShares(array $calendarData, array $shares, array $add, array $remove) { + $principal = explode('/', $calendarData['principaluri']); + $owner = $principal[2]; + + $currentUser = $this->userSession->getUser(); + if ($currentUser instanceof IUser) { + $currentUser = $currentUser->getUID(); + } else { + $currentUser = $owner; + } + + $event = $this->activityManager->generateEvent(); + $event->setApp('dav') + ->setObject(Extension::CALENDAR, $calendarData['id']) + ->setType(Extension::CALENDAR) + ->setAuthor($currentUser); + + foreach ($remove as $principal) { + // principal:principals/users/test + $parts = explode(':', $principal, 2); + if ($parts[0] !== 'principal') { + continue; + } + $principal = explode('/', $parts[1]); + + if ($principal[1] === 'users') { + $this->triggerActivityUser( + $principal[2], + $event, + $calendarData, + Extension::SUBJECT_UNSHARE_USER, + Extension::SUBJECT_DELETE . '_self' + ); + + if ($owner !== $principal[2]) { + $parameters = [ + $principal[2], + $calendarData['{DAV:}displayname'], + ]; + + if ($owner === $event->getAuthor()) { + $subject = Extension::SUBJECT_UNSHARE_USER . '_you'; + } else if ($principal[2] === $event->getAuthor()) { + $subject = Extension::SUBJECT_UNSHARE_USER . '_self'; + } else { + $event->setAffectedUser($event->getAuthor()) + ->setSubject(Extension::SUBJECT_UNSHARE_USER . '_you', $parameters); + $this->activityManager->publish($event); + + $subject = Extension::SUBJECT_UNSHARE_USER . '_by'; + $parameters[] = $event->getAuthor(); + } + + $event->setAffectedUser($owner) + ->setSubject($subject, $parameters); + $this->activityManager->publish($event); + } + } else if ($principal[1] === 'groups') { + $this->triggerActivityGroup($principal[2], $event, $calendarData, Extension::SUBJECT_UNSHARE_USER); + + $parameters = [ + $principal[2], + $calendarData['{DAV:}displayname'], + ]; + + if ($owner === $event->getAuthor()) { + $subject = Extension::SUBJECT_UNSHARE_GROUP . '_you'; + } else { + $event->setAffectedUser($event->getAuthor()) + ->setSubject(Extension::SUBJECT_UNSHARE_GROUP . '_you', $parameters); + $this->activityManager->publish($event); + + $subject = Extension::SUBJECT_UNSHARE_GROUP . '_by'; + $parameters[] = $event->getAuthor(); + } + + $event->setAffectedUser($owner) + ->setSubject($subject, $parameters); + $this->activityManager->publish($event); + } + } + + foreach ($add as $share) { + if ($this->isAlreadyShared($share['href'], $shares)) { + continue; + } + + // principal:principals/users/test + $parts = explode(':', $share['href'], 2); + if ($parts[0] !== 'principal') { + continue; + } + $principal = explode('/', $parts[1]); + + if ($principal[1] === 'users') { + $this->triggerActivityUser($principal[2], $event, $calendarData, Extension::SUBJECT_SHARE_USER); + + if ($owner !== $principal[2]) { + $parameters = [ + $principal[2], + $calendarData['{DAV:}displayname'], + ]; + + if ($owner === $event->getAuthor()) { + $subject = Extension::SUBJECT_SHARE_USER . '_you'; + } else { + $event->setAffectedUser($event->getAuthor()) + ->setSubject(Extension::SUBJECT_SHARE_USER . '_you', $parameters); + $this->activityManager->publish($event); + + $subject = Extension::SUBJECT_SHARE_USER . '_by'; + $parameters[] = $event->getAuthor(); + } + + $event->setAffectedUser($owner) + ->setSubject($subject, $parameters); + $this->activityManager->publish($event); + } + } else if ($principal[1] === 'groups') { + $this->triggerActivityGroup($principal[2], $event, $calendarData, Extension::SUBJECT_SHARE_USER); + + $parameters = [ + $principal[2], + $calendarData['{DAV:}displayname'], + ]; + + if ($owner === $event->getAuthor()) { + $subject = Extension::SUBJECT_SHARE_GROUP . '_you'; + } else { + $event->setAffectedUser($event->getAuthor()) + ->setSubject(Extension::SUBJECT_SHARE_GROUP . '_you', $parameters); + $this->activityManager->publish($event); + + $subject = Extension::SUBJECT_SHARE_GROUP . '_by'; + $parameters[] = $event->getAuthor(); + } + + $event->setAffectedUser($owner) + ->setSubject($subject, $parameters); + $this->activityManager->publish($event); + } + } + } + + /** + * Checks if a calendar is already shared with a principal + * + * @param string $principal + * @param array[] $shares + * @return bool + */ + protected function isAlreadyShared($principal, $shares) { + foreach ($shares as $share) { + if ($principal === $share['href']) { + return true; + } + } + + return false; + } + + /** + * Creates the given activity for all members of the given group + * + * @param string $gid + * @param IEvent $event + * @param array $properties + * @param string $subject + */ + protected function triggerActivityGroup($gid, IEvent $event, array $properties, $subject) { + $group = $this->groupManager->get($gid); + + if ($group instanceof IGroup) { + foreach ($group->getUsers() as $user) { + // Exclude current user + if ($user->getUID() !== $event->getAuthor()) { + $this->triggerActivityUser($user->getUID(), $event, $properties, $subject); + } + } + } + } + + /** + * Creates the given activity for the given user + * + * @param string $user + * @param IEvent $event + * @param array $properties + * @param string $subject + * @param string $subjectSelf + */ + protected function triggerActivityUser($user, IEvent $event, array $properties, $subject, $subjectSelf = '') { + $event->setAffectedUser($user) + ->setSubject( + $user === $event->getAuthor() && $subjectSelf ? $subjectSelf : $subject, + [ + $event->getAuthor(), + $properties['{DAV:}displayname'], + ] + ); + + $this->activityManager->publish($event); + } + + /** + * Creates activities when a calendar object was created/updated/deleted + * + * @param string $action + * @param array $calendarData + * @param array $shares + * @param array $objectData + */ + public function onTouchCalendarObject($action, array $calendarData, array $shares, array $objectData) { + if (!isset($calendarData['principaluri'])) { + return; + } + + $principal = explode('/', $calendarData['principaluri']); + $owner = $principal[2]; + + $currentUser = $this->userSession->getUser(); + if ($currentUser instanceof IUser) { + $currentUser = $currentUser->getUID(); + } else { + $currentUser = $owner; + } + + $object = $this->getObjectNameAndType($objectData); + $action = $action . '_' . $object['type']; + + if ($object['type'] === 'todo' && strpos($action, Extension::SUBJECT_OBJECT_UPDATE) === 0 && $object['status'] === 'COMPLETED') { + $action .= '_completed'; + } else if ($object['type'] === 'todo' && strpos($action, Extension::SUBJECT_OBJECT_UPDATE) === 0 && $object['status'] === 'NEEDS-ACTION') { + $action .= '_needs_action'; + } + + $event = $this->activityManager->generateEvent(); + $event->setApp('dav') + ->setObject(Extension::CALENDAR, $calendarData['id']) + ->setType($object['type'] === 'event' ? Extension::CALENDAR_EVENT : Extension::CALENDAR_TODO) + ->setAuthor($currentUser); + + $users = $this->getUsersForShares($shares); + $users[] = $owner; + + foreach ($users as $user) { + $event->setAffectedUser($user) + ->setSubject( + $user === $currentUser ? $action . '_self' : $action, + [ + $currentUser, + $calendarData['{DAV:}displayname'], + $object['name'], + ] + ); + $this->activityManager->publish($event); + } + } + + /** + * @param array $objectData + * @return string[]|bool + */ + protected function getObjectNameAndType(array $objectData) { + $vObject = Reader::read($objectData['calendardata']); + $component = $componentType = null; + foreach($vObject->getComponents() as $component) { + if (in_array($component->name, ['VEVENT', 'VTODO'])) { + $componentType = $component->name; + break; + } + } + + if (!$componentType) { + // Calendar objects must have a VEVENT or VTODO component + return false; + } + + if ($componentType === 'VEVENT') { + return ['name' => (string) $component->SUMMARY, 'type' => 'event']; + } + return ['name' => (string) $component->SUMMARY, 'type' => 'todo', 'status' => (string) $component->STATUS]; + } + + /** + * Get all users that have access to a given calendar + * + * @param array $shares + * @return string[] + */ + protected function getUsersForShares(array $shares) { + $users = $groups = []; + foreach ($shares as $share) { + $prinical = explode('/', $share['{http://owncloud.org/ns}principal']); + if ($prinical[1] === 'users') { + $users[] = $prinical[2]; + } else if ($prinical[1] === 'groups') { + $groups[] = $prinical[2]; + } + } + + if (!empty($groups)) { + foreach ($groups as $gid) { + $group = $this->groupManager->get($gid); + if ($group instanceof IGroup) { + foreach ($group->getUsers() as $user) { + $users[] = $user->getUID(); + } + } + } + } + + return array_unique($users); + } +} diff --git a/apps/dav/lib/CalDAV/Activity/Extension.php b/apps/dav/lib/CalDAV/Activity/Extension.php new file mode 100644 index 00000000000..29222b90ce8 --- /dev/null +++ b/apps/dav/lib/CalDAV/Activity/Extension.php @@ -0,0 +1,378 @@ +<?php +/** + * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\DAV\CalDAV\Activity; + +use OCP\Activity\IExtension; +use OCP\IURLGenerator; +use OCP\L10N\IFactory; + +class Extension implements IExtension { + const APP = 'dav'; + /** + * Filter with all sharing related activities + */ + const CALENDAR = 'calendar'; + const CALENDAR_EVENT = 'calendar_event'; + const CALENDAR_TODO = 'calendar_todo'; + + const SUBJECT_ADD = 'calendar_add'; + const SUBJECT_UPDATE = 'calendar_update'; + const SUBJECT_DELETE = 'calendar_delete'; + const SUBJECT_SHARE_USER = 'calendar_user_share'; + const SUBJECT_SHARE_GROUP = 'calendar_group_share'; + const SUBJECT_UNSHARE_USER = 'calendar_user_unshare'; + const SUBJECT_UNSHARE_GROUP = 'calendar_group_unshare'; + + const SUBJECT_OBJECT_ADD = 'object_add'; + const SUBJECT_OBJECT_UPDATE = 'object_update'; + const SUBJECT_OBJECT_DELETE = 'object_delete'; + + /** @var IFactory */ + protected $languageFactory; + + /** @var IURLGenerator */ + protected $URLGenerator; + + /** + * @param IFactory $languageFactory + * @param IURLGenerator $URLGenerator + */ + public function __construct(IFactory $languageFactory, IURLGenerator $URLGenerator) { + $this->languageFactory = $languageFactory; + $this->URLGenerator = $URLGenerator; + } + + protected function getL10N($languageCode = null) { + return $this->languageFactory->get(self::APP, $languageCode); + } + + /** + * The extension can return an array of additional notification types. + * If no additional types are to be added false is to be returned + * + * @param string $languageCode + * @return array|false + */ + public function getNotificationTypes($languageCode) { + $l = $this->getL10N($languageCode); + + return array( + self::CALENDAR => (string) $l->t('A <strong>calendar</strong> was modified'), + self::CALENDAR_EVENT => (string) $l->t('A calendar <strong>event</strong> was modified'), + self::CALENDAR_TODO => (string) $l->t('A calendar <strong>todo</strong> was modified'), + ); + } + + /** + * For a given method additional types to be displayed in the settings can be returned. + * In case no additional types are to be added false is to be returned. + * + * @param string $method + * @return array|false + */ + public function getDefaultTypes($method) { + $defaultTypes = []; + if ($method === self::METHOD_STREAM) { + $defaultTypes[] = self::CALENDAR; + $defaultTypes[] = self::CALENDAR_EVENT; + $defaultTypes[] = self::CALENDAR_TODO; + } + + return $defaultTypes; + } + + /** + * A string naming the css class for the icon to be used can be returned. + * If no icon is known for the given type false is to be returned. + * + * @param string $type + * @return string|false + */ + public function getTypeIcon($type) { + switch ($type) { + case self::CALENDAR: + case self::CALENDAR_EVENT: + return 'icon-calendar-dark'; + case self::CALENDAR_TODO: + return 'icon-checkmark'; + } + + return false; + } + + /** + * The extension can translate a given message to the requested languages. + * If no translation is available false is to be returned. + * + * @param string $app + * @param string $text + * @param array $params + * @param boolean $stripPath + * @param boolean $highlightParams + * @param string $languageCode + * @return string|false + */ + public function translate($app, $text, $params, $stripPath, $highlightParams, $languageCode) { + if ($app !== self::APP) { + return false; + } + + $l = $this->getL10N($languageCode); + + switch ($text) { + case self::SUBJECT_ADD: + return (string) $l->t('%1$s created calendar %2$s', $params); + case self::SUBJECT_ADD . '_self': + return (string) $l->t('You created calendar %2$s', $params); + case self::SUBJECT_DELETE: + return (string) $l->t('%1$s deleted calendar %2$s', $params); + case self::SUBJECT_DELETE . '_self': + return (string) $l->t('You deleted calendar %2$s', $params); + case self::SUBJECT_UPDATE: + return (string) $l->t('%1$s updated calendar %2$s', $params); + case self::SUBJECT_UPDATE . '_self': + return (string) $l->t('You updated calendar %2$s', $params); + + case self::SUBJECT_SHARE_USER: + return (string) $l->t('%1$s shared calendar %2$s with you', $params); + case self::SUBJECT_SHARE_USER . '_you': + return (string) $l->t('You shared calendar %2$s with %1$s', $params); + case self::SUBJECT_SHARE_USER . '_by': + return (string) $l->t('%3$s shared calendar %2$s with %1$s', $params); + case self::SUBJECT_UNSHARE_USER: + return (string) $l->t('%1$s unshared calendar %2$s from you', $params); + case self::SUBJECT_UNSHARE_USER . '_you': + return (string) $l->t('You unshared calendar %2$s from %1$s', $params); + case self::SUBJECT_UNSHARE_USER . '_by': + return (string) $l->t('%3$s unshared calendar %2$s from %1$s', $params); + case self::SUBJECT_UNSHARE_USER . '_self': + return (string) $l->t('%1$s unshared calendar %2$s from themselves', $params); + + case self::SUBJECT_SHARE_GROUP . '_you': + return (string) $l->t('You shared calendar %2$s with group %1$s', $params); + case self::SUBJECT_SHARE_GROUP . '_by': + return (string) $l->t('%3$s shared calendar %2$s with group %1$s', $params); + case self::SUBJECT_UNSHARE_GROUP . '_you': + return (string) $l->t('You unshared calendar %2$s from group %1$s', $params); + case self::SUBJECT_UNSHARE_GROUP . '_by': + return (string) $l->t('%3$s unshared calendar %2$s from group %1$s', $params); + + case self::SUBJECT_OBJECT_ADD . '_event': + return (string) $l->t('%1$s created event %3$s in calendar %2$s', $params); + case self::SUBJECT_OBJECT_ADD . '_event_self': + return (string) $l->t('You created event %3$s in calendar %2$s', $params); + case self::SUBJECT_OBJECT_DELETE . '_event': + return (string) $l->t('%1$s deleted event %3$s from calendar %2$s', $params); + case self::SUBJECT_OBJECT_DELETE . '_event_self': + return (string) $l->t('You deleted event %3$s from calendar %2$s', $params); + case self::SUBJECT_OBJECT_UPDATE . '_event': + return (string) $l->t('%1$s updated event %3$s in calendar %2$s', $params); + case self::SUBJECT_OBJECT_UPDATE . '_event_self': + return (string) $l->t('You updated event %3$s in calendar %2$s', $params); + + case self::SUBJECT_OBJECT_ADD . '_todo': + return (string) $l->t('%1$s created todo %3$s in list %2$s', $params); + case self::SUBJECT_OBJECT_ADD . '_todo_self': + return (string) $l->t('You created todo %3$s in list %2$s', $params); + case self::SUBJECT_OBJECT_DELETE . '_todo': + return (string) $l->t('%1$s deleted todo %3$s from list %2$s', $params); + case self::SUBJECT_OBJECT_DELETE . '_todo_self': + return (string) $l->t('You deleted todo %3$s from list %2$s', $params); + case self::SUBJECT_OBJECT_UPDATE . '_todo': + return (string) $l->t('%1$s updated todo %3$s in list %2$s', $params); + case self::SUBJECT_OBJECT_UPDATE . '_todo_self': + return (string) $l->t('You updated todo %3$s in list %2$s', $params); + + case self::SUBJECT_OBJECT_UPDATE . '_todo_completed': + return (string) $l->t('%1$s solved todo %3$s in list %2$s', $params); + case self::SUBJECT_OBJECT_UPDATE . '_todo_completed_self': + return (string) $l->t('You solved todo %3$s in list %2$s', $params); + case self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action': + return (string) $l->t('%1$s reopened todo %3$s in list %2$s', $params); + case self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action_self': + return (string) $l->t('You reopened todo %3$s in list %2$s', $params); + + } + + return false; + } + + /** + * The extension can define the type of parameters for translation + * + * Currently known types are: + * * file => will strip away the path of the file and add a tooltip with it + * * username => will add the avatar of the user + * + * @param string $app + * @param string $text + * @return array|false + */ + public function getSpecialParameterList($app, $text) { + if ($app === self::APP) { + switch ($text) { + case self::SUBJECT_ADD: + case self::SUBJECT_ADD . '_self': + case self::SUBJECT_DELETE: + case self::SUBJECT_DELETE . '_self': + case self::SUBJECT_UPDATE: + case self::SUBJECT_UPDATE . '_self': + case self::SUBJECT_SHARE_USER: + case self::SUBJECT_SHARE_USER . '_you': + case self::SUBJECT_UNSHARE_USER: + case self::SUBJECT_UNSHARE_USER . '_you': + case self::SUBJECT_UNSHARE_USER . '_self': + return [ + 0 => 'username', + //1 => 'calendar', + ]; + case self::SUBJECT_SHARE_USER . '_by': + case self::SUBJECT_UNSHARE_USER . '_by': + return [ + 0 => 'username', + //1 => 'calendar', + 2 => 'username', + ]; + case self::SUBJECT_SHARE_GROUP . '_you': + case self::SUBJECT_UNSHARE_GROUP . '_you': + return [ + //0 => 'group', + //1 => 'calendar', + ]; + case self::SUBJECT_SHARE_GROUP . '_by': + case self::SUBJECT_UNSHARE_GROUP . '_by': + return [ + //0 => 'group', + //1 => 'calendar', + 2 => 'username', + ]; + + case self::SUBJECT_OBJECT_ADD . '_event': + case self::SUBJECT_OBJECT_ADD . '_event_self': + case self::SUBJECT_OBJECT_DELETE . '_event': + case self::SUBJECT_OBJECT_DELETE . '_event_self': + case self::SUBJECT_OBJECT_UPDATE . '_event': + case self::SUBJECT_OBJECT_UPDATE . '_event_self': + + case self::SUBJECT_OBJECT_ADD . '_todo': + case self::SUBJECT_OBJECT_ADD . '_todo_self': + case self::SUBJECT_OBJECT_DELETE . '_todo': + case self::SUBJECT_OBJECT_DELETE . '_todo_self': + case self::SUBJECT_OBJECT_UPDATE . '_todo': + case self::SUBJECT_OBJECT_UPDATE . '_todo_self': + + case self::SUBJECT_OBJECT_UPDATE . '_todo_completed': + case self::SUBJECT_OBJECT_UPDATE . '_todo_completed_self': + case self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action': + case self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action_self': + return [ + 0 => 'username', + //1 => 'calendar', + //2 => 'object', + ]; + } + } + + return false; + } + + /** + * The extension can define the parameter grouping by returning the index as integer. + * In case no grouping is required false is to be returned. + * + * @param array $activity + * @return integer|false + */ + public function getGroupParameter($activity) { + return false; + } + + /** + * The extension can define additional navigation entries. The array returned has to contain two keys 'top' + * and 'apps' which hold arrays with the relevant entries. + * If no further entries are to be added false is no be returned. + * + * @return array|false + */ + public function getNavigation() { + $l = $this->getL10N(); + return [ + 'apps' => [ + self::CALENDAR => [ + 'id' => self::CALENDAR, + 'icon' => 'icon-calendar-dark', + 'name' => (string) $l->t('Calendar'), + 'url' => $this->URLGenerator->linkToRoute('activity.Activities.showList', ['filter' => self::CALENDAR]), + ], + self::CALENDAR_TODO => [ + 'id' => self::CALENDAR_TODO, + 'icon' => 'icon-checkmark', + 'name' => (string) $l->t('Todos'), + 'url' => $this->URLGenerator->linkToRoute('activity.Activities.showList', ['filter' => self::CALENDAR_TODO]), + ], + ], + 'top' => [], + ]; + } + + /** + * The extension can check if a custom filter (given by a query string like filter=abc) is valid or not. + * + * @param string $filterValue + * @return boolean + */ + public function isFilterValid($filterValue) { + return in_array($filterValue, [self::CALENDAR, self::CALENDAR_TODO]); + } + + /** + * The extension can filter the types based on the filter if required. + * In case no filter is to be applied false is to be returned unchanged. + * + * @param array $types + * @param string $filter + * @return array|false + */ + public function filterNotificationTypes($types, $filter) { + switch ($filter) { + case self::CALENDAR: + return array_intersect([self::CALENDAR, self::CALENDAR_EVENT], $types); + case self::CALENDAR_TODO: + return array_intersect([self::CALENDAR_TODO], $types); + } + return false; + } + + /** + * For a given filter the extension can specify the sql query conditions including parameters for that query. + * In case the extension does not know the filter false is to be returned. + * The query condition and the parameters are to be returned as array with two elements. + * E.g. return array('`app` = ? and `message` like ?', array('mail', 'ownCloud%')); + * + * @param string $filter + * @return array|false + */ + public function getQueryForFilter($filter) { + return false; + } + +} diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index 07b61285eca..fb608c2009d 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -29,7 +29,6 @@ use OCA\DAV\DAV\Sharing\IShareable; use OCP\DB\QueryBuilder\IQueryBuilder; use OCA\DAV\Connector\Sabre\Principal; use OCA\DAV\DAV\Sharing\Backend; -use OCP\IConfig; use OCP\IDBConnection; use OCP\IUser; use OCP\IUserManager; @@ -49,6 +48,8 @@ use Sabre\HTTP\URLUtil; use Sabre\VObject\DateTimeParser; use Sabre\VObject\Reader; use Sabre\VObject\Recur\EventIterator; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\GenericEvent; /** * Class CalDavBackend @@ -124,33 +125,33 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription /** @var IUserManager */ private $userManager; - - /** @var IConfig */ - private $config; /** @var ISecureRandom */ private $random; + /** @var EventDispatcherInterface */ + private $dispatcher; + /** * CalDavBackend constructor. * * @param IDBConnection $db * @param Principal $principalBackend * @param IUserManager $userManager - * @param IConfig $config * @param ISecureRandom $random + * @param EventDispatcherInterface $dispatcher */ public function __construct(IDBConnection $db, Principal $principalBackend, IUserManager $userManager, - IConfig $config, - ISecureRandom $random) { + ISecureRandom $random, + EventDispatcherInterface $dispatcher) { $this->db = $db; $this->principalBackend = $principalBackend; $this->userManager = $userManager; $this->sharingBackend = new Backend($this->db, $principalBackend, 'calendar'); - $this->config = $config; $this->random = $random; + $this->dispatcher = $dispatcher; } /** @@ -613,7 +614,16 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $query->setValue($column, $query->createNamedParameter($value)); } $query->execute(); - return $query->getLastInsertId(); + $calendarId = $query->getLastInsertId(); + + $this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendar', new GenericEvent( + '\OCA\DAV\CalDAV\CalDavBackend::createCalendar', + [ + 'calendarId' => $calendarId, + 'calendarData' => $this->getCalendarById($calendarId), + ])); + + return $calendarId; } /** @@ -661,6 +671,15 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $this->addChange($calendarId, "", 2); + $this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendar', new GenericEvent( + '\OCA\DAV\CalDAV\CalDavBackend::updateCalendar', + [ + 'calendarId' => $calendarId, + 'calendarData' => $this->getCalendarById($calendarId), + 'shares' => $this->getShares($calendarId), + 'propertyMutations' => $mutations, + ])); + return true; }); } @@ -672,6 +691,14 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @return void */ function deleteCalendar($calendarId) { + $this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar', new GenericEvent( + '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar', + [ + 'calendarId' => $calendarId, + 'calendarData' => $this->getCalendarById($calendarId), + 'shares' => $this->getShares($calendarId), + ])); + $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?'); $stmt->execute([$calendarId]); @@ -876,6 +903,15 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ]) ->execute(); + $this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', new GenericEvent( + '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', + [ + 'calendarId' => $calendarId, + 'calendarData' => $this->getCalendarById($calendarId), + 'shares' => $this->getShares($calendarId), + 'objectData' => $this->getCalendarObject($calendarId, $objectUri), + ] + )); $this->addChange($calendarId, $objectUri, 1); return '"' . $extraData['etag'] . '"'; @@ -917,6 +953,18 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) ->execute(); + $data = $this->getCalendarObject($calendarId, $objectUri); + if (is_array($data)) { + $this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', new GenericEvent( + '\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', + [ + 'calendarId' => $calendarId, + 'calendarData' => $this->getCalendarById($calendarId), + 'shares' => $this->getShares($calendarId), + 'objectData' => $data, + ] + )); + } $this->addChange($calendarId, $objectUri, 2); return '"' . $extraData['etag'] . '"'; @@ -949,6 +997,19 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @return void */ function deleteCalendarObject($calendarId, $objectUri) { + $data = $this->getCalendarObject($calendarId, $objectUri); + if (is_array($data)) { + $this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', new GenericEvent( + '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', + [ + 'calendarId' => $calendarId, + 'calendarData' => $this->getCalendarById($calendarId), + 'shares' => $this->getShares($calendarId), + 'objectData' => $data, + ] + )); + } + $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ?'); $stmt->execute([$calendarId, $objectUri]); @@ -1648,6 +1709,16 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @param array $remove */ public function updateShares($shareable, $add, $remove) { + $calendarId = $shareable->getResourceId(); + $this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent( + '\OCA\DAV\CalDAV\CalDavBackend::updateShares', + [ + 'calendarId' => $calendarId, + 'calendarData' => $this->getCalendarById($calendarId), + 'shares' => $this->getShares($calendarId), + 'add' => $add, + 'remove' => $remove, + ])); $this->sharingBackend->updateShares($shareable, $add, $remove); } diff --git a/apps/dav/lib/Command/CreateCalendar.php b/apps/dav/lib/Command/CreateCalendar.php index da1f706a8b8..24990352fab 100644 --- a/apps/dav/lib/Command/CreateCalendar.php +++ b/apps/dav/lib/Command/CreateCalendar.php @@ -75,11 +75,11 @@ class CreateCalendar extends Command { $this->userManager, $this->groupManager ); - $config = \OC::$server->getConfig(); $random = \OC::$server->getSecureRandom(); + $dispatcher = \OC::$server->getEventDispatcher(); $name = $input->getArgument('name'); - $caldav = new CalDavBackend($this->dbConnection, $principalBackend, $this->userManager, $config, $random); + $caldav = new CalDavBackend($this->dbConnection, $principalBackend, $this->userManager, $random, $dispatcher); $caldav->createCalendar("principals/users/$user", $name, []); } } diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index 39d15e0c6e9..539e22296f2 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -172,8 +172,6 @@ class FilesPlugin extends ServerPlugin { $this->server = $server; $this->server->on('propFind', array($this, 'handleGetProperties')); $this->server->on('propPatch', array($this, 'handleUpdateProperties')); - // RFC5995 to add file to the collection with a suggested name - $this->server->on('method:POST', [$this, 'httpPost']); $this->server->on('afterBind', array($this, 'sendFileIdHeader')); $this->server->on('afterWriteContent', array($this, 'sendFileIdHeader')); $this->server->on('afterMethod:GET', [$this,'httpGet']); @@ -435,52 +433,4 @@ class FilesPlugin extends ServerPlugin { } } } - - /** - * POST operation on directories to create a new file - * with suggested name - * - * @param RequestInterface $request request object - * @param ResponseInterface $response response object - * @return null|false - */ - public function httpPost(RequestInterface $request, ResponseInterface $response) { - // TODO: move this to another plugin ? - if (!\OC::$CLI && !\OC::$server->getRequest()->passesCSRFCheck()) { - throw new BadRequest('Invalid CSRF token'); - } - - list($parentPath, $name) = \Sabre\HTTP\URLUtil::splitPath($request->getPath()); - - // Making sure the parent node exists and is a directory - $node = $this->tree->getNodeForPath($parentPath); - - if ($node instanceof Directory) { - // no Add-Member found - if (empty($name) || $name[0] !== '&') { - // suggested name required - throw new BadRequest('Missing suggested file name'); - } - - $name = substr($name, 1); - - if (empty($name)) { - // suggested name required - throw new BadRequest('Missing suggested file name'); - } - - // make sure the name is unique - $name = basename(\OC_Helper::buildNotExistingFileNameForView($parentPath, $name, $this->fileView)); - - $node->createFile($name, $request->getBodyAsStream()); - - list($parentUrl, ) = \Sabre\HTTP\URLUtil::splitPath($request->getUrl()); - - $response->setHeader('Content-Location', $parentUrl . '/' . rawurlencode($name)); - - // created - $response->setStatus(201); - return false; - } - } } diff --git a/apps/dav/lib/RootCollection.php b/apps/dav/lib/RootCollection.php index 4c76dc30c3f..478f0929c20 100644 --- a/apps/dav/lib/RootCollection.php +++ b/apps/dav/lib/RootCollection.php @@ -62,7 +62,7 @@ class RootCollection extends SimpleCollection { $systemPrincipals->disableListing = $disableListing; $filesCollection = new Files\RootCollection($userPrincipalBackend, 'principals/users'); $filesCollection->disableListing = $disableListing; - $caldavBackend = new CalDavBackend($db, $userPrincipalBackend, $userManager, $config, $random); + $caldavBackend = new CalDavBackend($db, $userPrincipalBackend, $userManager, $random, $dispatcher); $calendarRoot = new CalendarRoot($userPrincipalBackend, $caldavBackend, 'principals/users'); $calendarRoot->disableListing = $disableListing; $publicCalendarRoot = new PublicCalendarRoot($caldavBackend); diff --git a/apps/dav/tests/unit/CalDAV/AbstractCalDavBackendTest.php b/apps/dav/tests/unit/CalDAV/AbstractCalDavBackendTest.php index 2559ecbbf89..d15be72c77b 100644 --- a/apps/dav/tests/unit/CalDAV/AbstractCalDavBackendTest.php +++ b/apps/dav/tests/unit/CalDAV/AbstractCalDavBackendTest.php @@ -22,18 +22,12 @@ namespace OCA\DAV\Tests\unit\CalDAV; -use DateTime; -use DateTimeZone; use OCA\DAV\CalDAV\CalDavBackend; -use OCA\DAV\CalDAV\Calendar; use OCA\DAV\Connector\Sabre\Principal; -use OCP\IL10N; -use OCP\IConfig; +use OCP\IUserManager; use OCP\Security\ISecureRandom; use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; -use Sabre\DAV\PropPatch; -use Sabre\DAV\Xml\Property\Href; -use Sabre\DAVACL\IACL; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Test\TestCase; /** @@ -50,12 +44,10 @@ abstract class AbstractCalDavBackendTest extends TestCase { /** @var Principal | \PHPUnit_Framework_MockObject_MockObject */ protected $principal; - - /** @var \OCP\IUserManager|\PHPUnit_Framework_MockObject_MockObject */ + /** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */ protected $userManager; - - /** var OCP\IConfig */ - protected $config; + /** @var EventDispatcherInterface|\PHPUnit_Framework_MockObject_MockObject */ + protected $dispatcher; /** @var ISecureRandom */ private $random; @@ -67,9 +59,8 @@ abstract class AbstractCalDavBackendTest extends TestCase { public function setUp() { parent::setUp(); - $this->userManager = $this->getMockBuilder('OCP\IUserManager') - ->disableOriginalConstructor() - ->getMock(); + $this->userManager = $this->createMock(IUserManager::class); + $this->dispatcher = $this->createMock(EventDispatcherInterface::class); $this->principal = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Principal') ->disableOriginalConstructor() ->setMethods(['getPrincipalByPath', 'getGroupMembership']) @@ -83,21 +74,28 @@ abstract class AbstractCalDavBackendTest extends TestCase { ->willReturn([self::UNIT_TEST_GROUP]); $db = \OC::$server->getDatabaseConnection(); - $this->config = \OC::$server->getConfig(); $this->random = \OC::$server->getSecureRandom(); - $this->backend = new CalDavBackend($db, $this->principal, $this->userManager, $this->config, $this->random); - $this->tearDown(); + $this->backend = new CalDavBackend($db, $this->principal, $this->userManager, $this->random, $this->dispatcher); + + $this->cleanUpBackend(); } public function tearDown() { + $this->cleanUpBackend(); parent::tearDown(); + } + public function cleanUpBackend() { if (is_null($this->backend)) { return; } - $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); - foreach ($books as $book) { - $this->backend->deleteCalendar($book['id']); + $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); + foreach ($calendars as $calendar) { + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar'); + + $this->backend->deleteCalendar($calendar['id']); } $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER); foreach ($subscriptions as $subscription) { @@ -106,6 +104,10 @@ abstract class AbstractCalDavBackendTest extends TestCase { } protected function createTestCalendar() { + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('\OCA\DAV\CalDAV\CalDavBackend::createCalendar'); + $this->backend->createCalendar(self::UNIT_TEST_USER, 'Example', [ '{http://apple.com/ns/ical/}calendar-color' => '#1C4587FF' ]); @@ -143,6 +145,11 @@ END:VEVENT END:VCALENDAR EOD; $uri0 = $this->getUniqueID('event'); + + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject'); + $this->backend->createCalendarObject($calendarId, $uri0, $calData); return $uri0; diff --git a/apps/dav/tests/unit/CalDAV/Activity/BackendTest.php b/apps/dav/tests/unit/CalDAV/Activity/BackendTest.php new file mode 100644 index 00000000000..3585d69bad3 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Activity/BackendTest.php @@ -0,0 +1,332 @@ +<?php +/** + * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\DAV\Tests\unit\CalDAV\Activity; + +use OCA\DAV\CalDAV\Activity\Backend; +use OCA\DAV\CalDAV\Activity\Extension; +use OCP\Activity\IEvent; +use OCP\Activity\IManager; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserSession; +use Test\TestCase; + +class BackendTest extends TestCase { + + /** @var IManager|\PHPUnit_Framework_MockObject_MockObject */ + protected $activityManager; + + /** @var IGroupManager|\PHPUnit_Framework_MockObject_MockObject */ + protected $groupManager; + + /** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */ + protected $userSession; + + protected function setUp() { + parent::setUp(); + $this->activityManager = $this->createMock(IManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->userSession = $this->createMock(IUserSession::class); + } + + /** + * @param array $methods + * @return Backend|\PHPUnit_Framework_MockObject_MockObject + */ + protected function getBackend(array $methods = []) { + if (empty($methods)) { + return new Backend( + $this->activityManager, + $this->groupManager, + $this->userSession + ); + } else { + return $this->getMockBuilder(Backend::class) + ->setConstructorArgs([ + $this->activityManager, + $this->groupManager, + $this->userSession, + ]) + ->setMethods($methods) + ->getMock(); + } + } + + public function dataCallTriggerCalendarActivity() { + return [ + ['onCalendarAdd', [['data']], Extension::SUBJECT_ADD, [['data'], [], []]], + ['onCalendarUpdate', [['data'], ['shares'], ['changed-properties']], Extension::SUBJECT_UPDATE, [['data'], ['shares'], ['changed-properties']]], + ['onCalendarDelete', [['data'], ['shares']], Extension::SUBJECT_DELETE, [['data'], ['shares'], []]], + ]; + } + + /** + * @dataProvider dataCallTriggerCalendarActivity + * + * @param string $method + * @param array $payload + * @param string $expectedSubject + * @param array $expectedPayload + */ + public function testCallTriggerCalendarActivity($method, array $payload, $expectedSubject, array $expectedPayload) { + $backend = $this->getBackend(['triggerCalendarActivity']); + $backend->expects($this->once()) + ->method('triggerCalendarActivity') + ->willReturnCallback(function() use($expectedPayload, $expectedSubject) { + $arguments = func_get_args(); + $this->assertSame($expectedSubject, array_shift($arguments)); + $this->assertEquals($expectedPayload, $arguments); + }); + + call_user_func_array([$backend, $method], $payload); + } + + public function dataTriggerCalendarActivity() { + return [ + // Add calendar + [Extension::SUBJECT_ADD, [], [], [], '', '', null, []], + [Extension::SUBJECT_ADD, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + '{DAV:}displayname' => 'Name of calendar', + ], [], [], '', 'admin', null, ['admin']], + [Extension::SUBJECT_ADD, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + '{DAV:}displayname' => 'Name of calendar', + ], [], [], 'test2', 'test2', null, ['admin']], + + // Update calendar + [Extension::SUBJECT_UPDATE, [], [], [], '', '', null, []], + // No visible change - owner only + [Extension::SUBJECT_UPDATE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + '{DAV:}displayname' => 'Name of calendar', + ], ['shares'], [], '', 'admin', null, ['admin']], + // Visible change + [Extension::SUBJECT_UPDATE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + '{DAV:}displayname' => 'Name of calendar', + ], ['shares'], ['{DAV:}displayname' => 'Name'], '', 'admin', ['user1'], ['user1', 'admin']], + [Extension::SUBJECT_UPDATE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + '{DAV:}displayname' => 'Name of calendar', + ], ['shares'], ['{DAV:}displayname' => 'Name'], 'test2', 'test2', ['user1'], ['user1', 'admin']], + + // Delete calendar + [Extension::SUBJECT_DELETE, [], [], [], '', '', null, []], + [Extension::SUBJECT_DELETE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + '{DAV:}displayname' => 'Name of calendar', + ], ['shares'], [], '', 'admin', [], ['admin']], + [Extension::SUBJECT_DELETE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + '{DAV:}displayname' => 'Name of calendar', + ], ['shares'], [], '', 'admin', ['user1'], ['user1', 'admin']], + [Extension::SUBJECT_DELETE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + '{DAV:}displayname' => 'Name of calendar', + ], ['shares'], [], 'test2', 'test2', ['user1'], ['user1', 'admin']], + ]; + } + + /** + * @dataProvider dataTriggerCalendarActivity + * @param string $action + * @param array $data + * @param array $shares + * @param array $changedProperties + * @param string $currentUser + * @param string $author + * @param string[]|null $shareUsers + * @param string[] $users + */ + public function testTriggerCalendarActivity($action, array $data, array $shares, array $changedProperties, $currentUser, $author, $shareUsers, array $users) { + $backend = $this->getBackend(['getUsersForShares']); + + if ($shareUsers === null) { + $backend->expects($this->never()) + ->method('getUsersForShares'); + } else { + $backend->expects($this->once()) + ->method('getUsersForShares') + ->with($shares) + ->willReturn($shareUsers); + } + + if ($author !== '') { + if ($currentUser !== '') { + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn($this->getUserMock($currentUser)); + } else { + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn(null); + } + + $event = $this->createMock(IEvent::class); + $this->activityManager->expects($this->once()) + ->method('generateEvent') + ->willReturn($event); + + $event->expects($this->once()) + ->method('setApp') + ->with('dav') + ->willReturnSelf(); + $event->expects($this->once()) + ->method('setObject') + ->with(Extension::CALENDAR, $data['id']) + ->willReturnSelf(); + $event->expects($this->once()) + ->method('setType') + ->with(Extension::CALENDAR) + ->willReturnSelf(); + $event->expects($this->once()) + ->method('setAuthor') + ->with($author) + ->willReturnSelf(); + + $event->expects($this->exactly(sizeof($users))) + ->method('setAffectedUser') + ->willReturnSelf(); + $event->expects($this->exactly(sizeof($users))) + ->method('setSubject') + ->willReturnSelf(); + $this->activityManager->expects($this->exactly(sizeof($users))) + ->method('publish') + ->with($event); + } else { + $this->activityManager->expects($this->never()) + ->method('generateEvent'); + } + + $this->invokePrivate($backend, 'triggerCalendarActivity', [$action, $data, $shares, $changedProperties]); + } + + public function dataGetUsersForShares() { + return [ + [ + [], + [], + [], + ], + [ + [ + ['{http://owncloud.org/ns}principal' => 'principal/users/user1'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user3'], + ], + [], + ['user1', 'user2', 'user3'], + ], + [ + [ + ['{http://owncloud.org/ns}principal' => 'principal/users/user1'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/groups/group2'], + ['{http://owncloud.org/ns}principal' => 'principal/groups/group3'], + ], + ['group2' => null, 'group3' => null], + ['user1', 'user2'], + ], + [ + [ + ['{http://owncloud.org/ns}principal' => 'principal/users/user1'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/groups/group2'], + ['{http://owncloud.org/ns}principal' => 'principal/groups/group3'], + ], + ['group2' => ['user1', 'user2', 'user3'], 'group3' => ['user2', 'user3', 'user4']], + ['user1', 'user2', 'user3', 'user4'], + ], + ]; + } + + /** + * @dataProvider dataGetUsersForShares + * @param array $shares + * @param array $groups + * @param array $expected + */ + public function testGetUsersForShares(array $shares, array $groups, array $expected) { + $backend = $this->getBackend(); + + $getGroups = []; + foreach ($groups as $gid => $members) { + if ($members === null) { + $getGroups[] = [$gid, null]; + continue; + } + + $group = $this->createMock(IGroup::class); + $group->expects($this->once()) + ->method('getUsers') + ->willReturn($this->getUsers($members)); + + $getGroups[] = [$gid, $group]; + } + + $this->groupManager->expects($this->exactly(sizeof($getGroups))) + ->method('get') + ->willReturnMap($getGroups); + + $users = $this->invokePrivate($backend, 'getUsersForShares', [$shares]); + sort($users); + $this->assertEquals($expected, $users); + } + + /** + * @param string[] $users + * @return IUser[]|\PHPUnit_Framework_MockObject_MockObject[] + */ + protected function getUsers(array $users) { + $list = []; + foreach ($users as $user) { + $list[] = $this->getUserMock($user); + } + return $list; + } + + /** + * @param string $uid + * @return IUser|\PHPUnit_Framework_MockObject_MockObject + */ + protected function getUserMock($uid) { + $user = $this->createMock(IUser::class); + $user->expects($this->once()) + ->method('getUID') + ->willReturn($uid); + return $user; + } +} diff --git a/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php b/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php index 8349d98cd94..b5e700e8bc4 100644 --- a/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php +++ b/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php @@ -51,6 +51,9 @@ class CalDavBackendTest extends AbstractCalDavBackendTest { '{DAV:}displayname' => 'Unit test', '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'Calendar used for unit testing' ]); + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('\OCA\DAV\CalDAV\CalDavBackend::updateCalendar'); $this->backend->updateCalendar($calendarId, $patch); $patch->commit(); $this->assertEquals(1, $this->backend->getCalendarsForUserCount(self::UNIT_TEST_USER)); @@ -60,6 +63,9 @@ class CalDavBackendTest extends AbstractCalDavBackendTest { $this->assertEquals('Calendar used for unit testing', $books[0]['{urn:ietf:params:xml:ns:caldav}calendar-description']); // delete the address book + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar'); $this->backend->deleteCalendar($books[0]['id']); $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); $this->assertEquals(0, count($books)); @@ -106,6 +112,9 @@ class CalDavBackendTest extends AbstractCalDavBackendTest { $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); $this->assertEquals(1, count($books)); $calendar = new Calendar($this->backend, $books[0], $l10n); + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('\OCA\DAV\CalDAV\CalDavBackend::updateShares'); $this->backend->updateShares($calendar, $add, []); $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER1); $this->assertEquals(1, count($books)); @@ -138,6 +147,9 @@ END:VEVENT END:VCALENDAR EOD; + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject'); $this->backend->createCalendarObject($calendarId, $uri, $calData); /** @var IACL $child */ @@ -151,6 +163,9 @@ EOD; $this->assertAccess($groupCanWrite, self::UNIT_TEST_GROUP, '{DAV:}write', $acl); // delete the address book + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar'); $this->backend->deleteCalendar($books[0]['id']); $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); $this->assertEquals(0, count($books)); @@ -179,6 +194,9 @@ END:VEVENT END:VCALENDAR EOD; + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject'); $this->backend->createCalendarObject($calendarId, $uri, $calData); // get all the cards @@ -214,11 +232,17 @@ DTEND;VALUE=DATE-TIME:20130912T140000Z END:VEVENT END:VCALENDAR EOD; + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject'); $this->backend->updateCalendarObject($calendarId, $uri, $calData); $calendarObject = $this->backend->getCalendarObject($calendarId, $uri); $this->assertEquals($calData, $calendarObject['calendardata']); // delete the card + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject'); $this->backend->deleteCalendarObject($calendarId, $uri); $calendarObjects = $this->backend->getCalendarObjects($calendarId); $this->assertEquals(0, count($calendarObjects)); @@ -246,10 +270,19 @@ END:VEVENT END:VCALENDAR EOD; $uri0 = $this->getUniqueID('card'); + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject'); $this->backend->createCalendarObject($calendarId, $uri0, $calData); $uri1 = $this->getUniqueID('card'); + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject'); $this->backend->createCalendarObject($calendarId, $uri1, $calData); $uri2 = $this->getUniqueID('card'); + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject'); $this->backend->createCalendarObject($calendarId, $uri2, $calData); // get all the cards @@ -270,8 +303,17 @@ EOD; } // delete the card + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject'); $this->backend->deleteCalendarObject($calendarId, $uri0); + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject'); $this->backend->deleteCalendarObject($calendarId, $uri1); + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject'); $this->backend->deleteCalendarObject($calendarId, $uri2); $calendarObjects = $this->backend->getCalendarObjects($calendarId); $this->assertEquals(0, count($calendarObjects)); @@ -335,6 +377,10 @@ EOD; } public function testPublications() { + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('\OCA\DAV\CalDAV\CalDavBackend::createCalendar'); + $this->backend->createCalendar(self::UNIT_TEST_USER, 'Example', []); $calendarInfo = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER)[0]; @@ -357,10 +403,8 @@ EOD; $calendar->setPublishStatus(false); $this->assertEquals(false, $calendar->getPublishStatus()); - $publicCalendarURI = md5($this->config->getSystemValue('secret', '') . $calendar->getResourceId()); $this->setExpectedException('Sabre\DAV\Exception\NotFound'); - $publicCalendar = $this->backend->getPublicCalendar($publicCalendarURI); - + $this->backend->getPublicCalendar($publicCalendarURI); } public function testSubscriptions() { diff --git a/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php b/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php index 6dfec6d7e1f..59fa4747a93 100644 --- a/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php +++ b/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php @@ -9,6 +9,7 @@ use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\PublicCalendarRoot; use OCP\IUserManager; use OCP\Security\ISecureRandom; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Test\TestCase; /** @@ -27,12 +28,11 @@ class PublicCalendarRootTest extends TestCase { private $publicCalendarRoot; /** @var IL10N */ private $l10n; - /** @var IUserManager */ - private $userManager; - /** @var Principal */ + /** @var Principal|\PHPUnit_Framework_MockObject_MockObject */ private $principal; - /** var IConfig */ - protected $config; + /** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */ + protected $userManager; + /** @var ISecureRandom */ private $random; @@ -40,19 +40,17 @@ class PublicCalendarRootTest extends TestCase { parent::setUp(); $db = \OC::$server->getDatabaseConnection(); - $this->principal = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Principal') - ->disableOriginalConstructor() - ->getMock(); - $this->config = \OC::$server->getConfig(); - $this->userManager = $this->getMockBuilder('\OCP\IUserManager')->getMock(); + $this->principal = $this->createMock('OCA\DAV\Connector\Sabre\Principal'); + $this->userManager = $this->createMock(IUserManager::class); $this->random = \OC::$server->getSecureRandom(); + $dispatcher = $this->createMock(EventDispatcherInterface::class); $this->backend = new CalDavBackend( $db, $this->principal, $this->userManager, - $this->config, - $this->random + $this->random, + $dispatcher ); $this->publicCalendarRoot = new PublicCalendarRoot($this->backend); diff --git a/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php index 43ca119abff..d39f709493e 100644 --- a/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php @@ -547,85 +547,4 @@ class FilesPluginTest extends TestCase { $this->assertEquals("false", $propFind->get(self::HAS_PREVIEW_PROPERTYNAME)); } - - public function postCreateFileProvider() { - $baseUrl = 'http://example.com/owncloud/remote.php/webdav/subdir/'; - return [ - ['test.txt', 'some file.txt', 'some file.txt', $baseUrl . 'some%20file.txt'], - ['some file.txt', 'some file.txt', 'some file (2).txt', $baseUrl . 'some%20file%20%282%29.txt'], - ]; - } - - /** - * @dataProvider postCreateFileProvider - */ - public function testPostWithAddMember($existingFile, $wantedName, $deduplicatedName, $expectedLocation) { - $request = $this->getMock('Sabre\HTTP\RequestInterface'); - $response = $this->getMock('Sabre\HTTP\ResponseInterface'); - - $request->expects($this->any()) - ->method('getUrl') - ->will($this->returnValue('http://example.com/owncloud/remote.php/webdav/subdir/&' . $wantedName)); - - $request->expects($this->any()) - ->method('getPath') - ->will($this->returnValue('/subdir/&' . $wantedName)); - - $request->expects($this->once()) - ->method('getBodyAsStream') - ->will($this->returnValue(fopen('data://text/plain,hello', 'r'))); - - $this->view->expects($this->any()) - ->method('file_exists') - ->will($this->returnCallback(function($path) use ($existingFile) { - return ($path === '/subdir/' . $existingFile); - })); - - $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\Directory', '/subdir'); - - $node->expects($this->once()) - ->method('createFile') - ->with($deduplicatedName, $this->isType('resource')); - - $response->expects($this->once()) - ->method('setStatus') - ->with(201); - $response->expects($this->once()) - ->method('setHeader') - ->with('Content-Location', $expectedLocation); - - $this->assertFalse($this->plugin->httpPost($request, $response)); - } - - public function testPostOnNonDirectory() { - $request = $this->getMock('Sabre\HTTP\RequestInterface'); - $response = $this->getMock('Sabre\HTTP\ResponseInterface'); - - $request->expects($this->any()) - ->method('getPath') - ->will($this->returnValue('/subdir/test.txt/&abc')); - - $this->createTestNode('\OCA\DAV\Connector\Sabre\File', '/subdir/test.txt'); - - $this->assertNull($this->plugin->httpPost($request, $response)); - } - - /** - * @expectedException \Sabre\DAV\Exception\BadRequest - */ - public function testPostWithoutAddMember() { - $request = $this->getMock('Sabre\HTTP\RequestInterface'); - $response = $this->getMock('Sabre\HTTP\ResponseInterface'); - - $request->expects($this->any()) - ->method('getPath') - ->will($this->returnValue('/subdir/&')); - - $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\Directory', '/subdir'); - - $node->expects($this->never()) - ->method('createFile'); - - $this->plugin->httpPost($request, $response); - } } diff --git a/apps/files/js/file-upload.js b/apps/files/js/file-upload.js index 18d0d973586..8bcaa2e24aa 100644 --- a/apps/files/js/file-upload.js +++ b/apps/files/js/file-upload.js @@ -100,26 +100,14 @@ OC.FileUpload.prototype = { /** * Return the final filename. - * Either this is the original file name or the file name - * after an autorename. * * @return {String} file name */ getFileName: function() { - // in case of autorename + // autorenamed name if (this._newName) { return this._newName; } - - if (this._conflictMode === OC.FileUpload.CONFLICT_MODE_AUTORENAME) { - - var locationUrl = this.getResponseHeader('Content-Location'); - if (locationUrl) { - this._newName = decodeURIComponent(OC.basename(locationUrl)); - return this._newName; - } - } - return this.getFile().name; }, @@ -142,8 +130,19 @@ OC.FileUpload.prototype = { }, /** + * Returns conflict resolution mode. + * + * @return {int} conflict mode + */ + getConflictMode: function() { + return this._conflictMode || OC.FileUpload.CONFLICT_MODE_DETECT; + }, + + /** * Set conflict resolution mode. * See CONFLICT_MODE_* constants. + * + * @param {int} mode conflict mode */ setConflictMode: function(mode) { this._conflictMode = mode; @@ -163,6 +162,30 @@ OC.FileUpload.prototype = { }, /** + * Trigger autorename and append "(2)". + * Multiple calls will increment the appended number. + */ + autoRename: function() { + var name = this.getFile().name; + if (!this._renameAttempt) { + this._renameAttempt = 1; + } + + var dotPos = name.lastIndexOf('.'); + var extPart = ''; + if (dotPos > 0) { + this._newName = name.substr(0, dotPos); + extPart = name.substr(dotPos); + } else { + this._newName = name; + } + + // generate new name + this._renameAttempt++; + this._newName = this._newName + ' (' + this._renameAttempt + ')' + extPart; + }, + + /** * Submit the upload */ submit: function() { @@ -179,7 +202,7 @@ OC.FileUpload.prototype = { } if (this.uploader.fileList) { - this.data.url = this.uploader.fileList.getUploadUrl(file.name, this.getFullPath()); + this.data.url = this.uploader.fileList.getUploadUrl(this.getFileName(), this.getFullPath()); } if (!this.data.headers) { @@ -191,12 +214,9 @@ OC.FileUpload.prototype = { this.data.type = 'PUT'; delete this.data.headers['If-None-Match']; - if (this._conflictMode === OC.FileUpload.CONFLICT_MODE_DETECT) { + if (this._conflictMode === OC.FileUpload.CONFLICT_MODE_DETECT + || this._conflictMode === OC.FileUpload.CONFLICT_MODE_AUTORENAME) { this.data.headers['If-None-Match'] = '*'; - } else if (this._conflictMode === OC.FileUpload.CONFLICT_MODE_AUTORENAME) { - // POST to parent folder, with slug - this.data.type = 'POST'; - this.data.url = this.uploader.fileList.getUploadUrl('&' + file.name, this.getFullPath()); } if (file.lastModified) { @@ -212,18 +232,6 @@ OC.FileUpload.prototype = { 'Basic ' + btoa(userName + ':' + (password || '')); } - if (!this.uploader.isXHRUpload()) { - data.formData = []; - - // pass headers as parameters - data.formData.push({name: 'headers', value: JSON.stringify(this.data.headers)}); - data.formData.push({name: 'requesttoken', value: OC.requestToken}); - if (data.type === 'POST') { - // still add the method to the URL - data.url += '?_method=POST'; - } - } - var chunkFolderPromise; if ($.support.blobSlice && this.uploader.fileUploadParam.maxChunkSize @@ -491,6 +499,14 @@ OC.Uploader.prototype = _.extend({ //show "file already exists" dialog var self = this; var file = fileUpload.getFile(); + // already attempted autorename but the server said the file exists ? (concurrently added) + if (fileUpload.getConflictMode() === OC.FileUpload.CONFLICT_MODE_AUTORENAME) { + // attempt another autorename, defer to let the current callback finish + _.defer(function() { + self.onAutorename(fileUpload); + }); + return; + } // retrieve more info about this file this.filesClient.getFileInfo(fileUpload.getFullPath()).then(function(status, fileInfo) { var original = fileInfo; @@ -603,6 +619,13 @@ OC.Uploader.prototype = _.extend({ onAutorename:function(upload) { this.log('autorename', null, upload); upload.setConflictMode(OC.FileUpload.CONFLICT_MODE_AUTORENAME); + + do { + upload.autoRename(); + // if file known to exist on the client side, retry + } while (this.fileList && this.fileList.inList(upload.getFileName())); + + // resubmit upload this.submitUploads([upload]); }, _trace:false, //TODO implement log handler for JS per class? diff --git a/apps/files/js/templates/detailsview.handlebars.js b/apps/files/js/templates/detailsview.handlebars.js new file mode 100644 index 00000000000..c109da77a63 --- /dev/null +++ b/apps/files/js/templates/detailsview.handlebars.js @@ -0,0 +1,28 @@ +(function() { + var template = Handlebars.template, templates = OCA.Files.Templates = OCA.Files.Templates || {}; +templates['detailsview'] = template({"1":function(container,depth0,helpers,partials,data) { + var stack1; + + return "<ul class=\"tabHeaders\">\n" + + ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.tabHeaders : depth0),{"name":"each","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + "</ul>\n"; +},"2":function(container,depth0,helpers,partials,data) { + var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; + + return " <li class=\"tabHeader\" data-tabid=\"" + + alias4(((helper = (helper = helpers.tabId || (depth0 != null ? depth0.tabId : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"tabId","hash":{},"data":data}) : helper))) + + "\" data-tabindex=\"" + + alias4(((helper = (helper = helpers.tabIndex || (depth0 != null ? depth0.tabIndex : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"tabIndex","hash":{},"data":data}) : helper))) + + "\">\n <a href=\"#\">" + + alias4(((helper = (helper = helpers.label || (depth0 != null ? depth0.label : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"label","hash":{},"data":data}) : helper))) + + "</a>\n </li>\n"; +},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { + var stack1, helper, alias1=depth0 != null ? depth0 : {}; + + return "<div class=\"detailFileInfoContainer\"></div>\n" + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.tabHeaders : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + "<div class=\"tabsContainer\">\n</div>\n<a class=\"close icon-close\" href=\"#\" alt=\"" + + container.escapeExpression(((helper = (helper = helpers.closeLabel || (depth0 != null ? depth0.closeLabel : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(alias1,{"name":"closeLabel","hash":{},"data":data}) : helper))) + + "\"></a>\n"; +},"useData":true}); +})(); diff --git a/apps/files/tests/js/fileUploadSpec.js b/apps/files/tests/js/fileUploadSpec.js index fa686dbf3e2..81a0a2df610 100644 --- a/apps/files/tests/js/fileUploadSpec.js +++ b/apps/files/tests/js/fileUploadSpec.js @@ -144,6 +144,10 @@ describe('OC.Upload tests', function() { uploader = new OC.Uploader($dummyUploader, { fileList: fileList }); + + var deferred = $.Deferred(); + conflictDialogStub.returns(deferred.promise()); + deferred.resolve(); }); afterEach(function() { conflictDialogStub.restore(); @@ -158,10 +162,6 @@ describe('OC.Upload tests', function() { expect(result[1].submit.calledOnce).toEqual(true); }); it('shows conflict dialog when no client side conflict', function() { - var deferred = $.Deferred(); - conflictDialogStub.returns(deferred.promise()); - deferred.resolve(); - var result = addFiles(uploader, [ {name: 'conflict.txt'}, {name: 'conflict2.txt'}, @@ -185,5 +185,58 @@ describe('OC.Upload tests', function() { expect(result[1].submit.calledOnce).toEqual(false); expect(result[2].submit.calledOnce).toEqual(true); }); + it('cancels upload when skipping file in conflict mode', function() { + var fileData = {name: 'conflict.txt'}; + var uploadData = addFiles(uploader, [ + fileData + ]); + + var upload = new OC.FileUpload(uploader, uploadData[0]); + var deleteStub = sinon.stub(upload, 'deleteUpload'); + + uploader.onSkip(upload); + expect(deleteStub.calledOnce).toEqual(true); + }); + it('overwrites file when choosing replace in conflict mode', function() { + var fileData = {name: 'conflict.txt'}; + var uploadData = addFiles(uploader, [ + fileData + ]); + + expect(uploadData[0].submit.notCalled).toEqual(true); + + var upload = new OC.FileUpload(uploader, uploadData[0]); + + uploader.onReplace(upload); + expect(upload.getConflictMode()).toEqual(OC.FileUpload.CONFLICT_MODE_OVERWRITE); + expect(uploadData[0].submit.calledOnce).toEqual(true); + }); + it('autorenames file when choosing replace in conflict mode', function() { + // needed for _.defer call + var clock = sinon.useFakeTimers(); + var fileData = {name: 'conflict.txt'}; + var uploadData = addFiles(uploader, [ + fileData + ]); + + expect(uploadData[0].submit.notCalled).toEqual(true); + + var upload = new OC.FileUpload(uploader, uploadData[0]); + var getResponseStatusStub = sinon.stub(upload, 'getResponseStatus'); + + uploader.onAutorename(upload); + expect(upload.getConflictMode()).toEqual(OC.FileUpload.CONFLICT_MODE_AUTORENAME); + expect(upload.getFileName()).toEqual('conflict (2).txt'); + expect(uploadData[0].submit.calledOnce).toEqual(true); + + // in case of server-side conflict, tries to rename again + getResponseStatusStub.returns(412); + uploader.fileUploadParam.fail.call($dummyUploader[0], {}, uploadData[0]); + clock.tick(500); + expect(upload.getFileName()).toEqual('conflict (3).txt'); + expect(uploadData[0].submit.calledTwice).toEqual(true); + + clock.restore(); + }); }); }); diff --git a/apps/sharebymail/img/app.svg b/apps/sharebymail/img/app.svg new file mode 100644 index 00000000000..869373adde6 --- /dev/null +++ b/apps/sharebymail/img/app.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 16 16"><path d="M12.228 1a2.457 2.457 0 0 0-2.46 2.454c0 .075.01.15.016.224L5.05 6.092a2.445 2.445 0 0 0-1.596-.586A2.453 2.453 0 0 0 1 7.96a2.453 2.453 0 0 0 2.454 2.455 2.45 2.45 0 0 0 1.46-.477l4.865 2.474c-.004.044-.01.09-.01.134a2.457 2.457 0 1 0 .804-1.818l-4.696-2.4c.02-.123.035-.25.035-.378 0-.072-.01-.144-.015-.214l4.74-2.414A2.457 2.457 0 1 0 12.228.99z" fill="#fff"/></svg>
\ No newline at end of file |