diff options
20 files changed, 956 insertions, 56 deletions
diff --git a/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php b/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php index ab0b7734258..124ed6184c1 100644 --- a/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php +++ b/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php @@ -192,7 +192,7 @@ class SystemTagsObjectMappingCollection implements ICollection { * * @param ISystemTag $tag * - * @return SystemTagNode + * @return SystemTagMappingNode */ private function makeNode(ISystemTag $tag) { return new SystemTagMappingNode( diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml index 1992b94a03c..35e58b7202e 100644 --- a/apps/files/appinfo/info.xml +++ b/apps/files/appinfo/info.xml @@ -19,6 +19,7 @@ <activity> <settings> + <setting>OCA\Files\Activity\Settings\FavoriteAction</setting> <setting>OCA\Files\Activity\Settings\FileChanged</setting> <setting>OCA\Files\Activity\Settings\FileCreated</setting> <setting>OCA\Files\Activity\Settings\FileDeleted</setting> @@ -32,6 +33,7 @@ </filters> <providers> + <provider>OCA\Files\Activity\FavoriteProvider</provider> <provider>OCA\Files\Activity\Provider</provider> </providers> </activity> diff --git a/apps/files/css/upload.css b/apps/files/css/upload.css index abc09c3e99c..6f337a27578 100644 --- a/apps/files/css/upload.css +++ b/apps/files/css/upload.css @@ -118,7 +118,7 @@ margin-right: 3px; } .oc-dialog .fileexists th:first-child { - width: 230px; + width: 225px; } .oc-dialog .fileexists th label { font-weight: normal; @@ -147,16 +147,14 @@ background-size: 64px 64px; float: left; } + +.oc-dialog .fileexists .original, .oc-dialog .fileexists .replacement { float: left; - width: 230px; -} -.oc-dialog .fileexists .original { - float: left; - width: 230px; + width: 225px; } .oc-dialog .fileexists .conflicts { - overflow-y:scroll; + overflow-y: auto; max-height: 225px; } .oc-dialog .fileexists .conflict input[type='checkbox'] { diff --git a/apps/files/lib/Activity/FavoriteProvider.php b/apps/files/lib/Activity/FavoriteProvider.php new file mode 100644 index 00000000000..8047eb1319e --- /dev/null +++ b/apps/files/lib/Activity/FavoriteProvider.php @@ -0,0 +1,151 @@ +<?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\Files\Activity; + +use OCP\Activity\IEvent; +use OCP\Activity\IEventMerger; +use OCP\Activity\IManager; +use OCP\Activity\IProvider; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\L10N\IFactory; + +class FavoriteProvider implements IProvider { + + const SUBJECT_ADDED = 'added_favorite'; + const SUBJECT_REMOVED = 'removed_favorite'; + + /** @var IFactory */ + protected $languageFactory; + + /** @var IL10N */ + protected $l; + + /** @var IURLGenerator */ + protected $url; + + /** @var IManager */ + protected $activityManager; + + /** @var IEventMerger */ + protected $eventMerger; + + /** + * @param IFactory $languageFactory + * @param IURLGenerator $url + * @param IManager $activityManager + * @param IEventMerger $eventMerger + */ + public function __construct(IFactory $languageFactory, IURLGenerator $url, IManager $activityManager, IEventMerger $eventMerger) { + $this->languageFactory = $languageFactory; + $this->url = $url; + $this->activityManager = $activityManager; + $this->eventMerger = $eventMerger; + } + + /** + * @param string $language + * @param IEvent $event + * @param IEvent|null $previousEvent + * @return IEvent + * @throws \InvalidArgumentException + * @since 11.0.0 + */ + public function parse($language, IEvent $event, IEvent $previousEvent = null) { + if ($event->getApp() !== 'files' || $event->getType() !== 'favorite') { + throw new \InvalidArgumentException(); + } + + $this->l = $this->languageFactory->get('files', $language); + + if ($this->activityManager->isFormattingFilteredObject()) { + try { + return $this->parseShortVersion($event); + } catch (\InvalidArgumentException $e) { + // Ignore and simply use the long version... + } + } + + return $this->parseLongVersion($event, $previousEvent); + } + + /** + * @param IEvent $event + * @return IEvent + * @throws \InvalidArgumentException + * @since 11.0.0 + */ + public function parseShortVersion(IEvent $event) { + + if ($event->getSubject() === self::SUBJECT_ADDED) { + $event->setParsedSubject($this->l->t('Added to favorites')) + ->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/starred.svg'))); + } else if ($event->getSubject() === self::SUBJECT_REMOVED) { + $event->setParsedSubject($this->l->t('Removed from favorites')) + ->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/star.svg'))); + } else { + throw new \InvalidArgumentException(); + } + + return $event; + } + + /** + * @param IEvent $event + * @param IEvent|null $previousEvent + * @return IEvent + * @throws \InvalidArgumentException + * @since 11.0.0 + */ + public function parseLongVersion(IEvent $event, IEvent $previousEvent = null) { + + if ($event->getSubject() === self::SUBJECT_ADDED) { + $subject = $this->l->t('You added {file} to your favorites'); + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/starred.svg'))); + } else if ($event->getSubject() === self::SUBJECT_REMOVED) { + $subject = $this->l->t('You removed {file} from your favorites'); + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/star.svg'))); + } else { + throw new \InvalidArgumentException(); + } + + $this->setSubjects($event, $subject); + $event = $this->eventMerger->mergeEvents('file', $event, $previousEvent); + return $event; + } + + /** + * @param IEvent $event + * @param string $subject + */ + protected function setSubjects(IEvent $event, $subject) { + $parameter = [ + 'type' => 'file', + 'id' => $event->getObjectId(), + 'name' => basename($event->getObjectName()), + 'path' => $event->getObjectName(), + ]; + + $event->setParsedSubject(str_replace('{file}', trim($parameter['path'], '/'), $subject)) + ->setRichSubject($subject, ['file' => $parameter]); + } +} diff --git a/apps/files/lib/Activity/Filter/FileChanges.php b/apps/files/lib/Activity/Filter/FileChanges.php index dc7daf96bac..d8d1a698816 100644 --- a/apps/files/lib/Activity/Filter/FileChanges.php +++ b/apps/files/lib/Activity/Filter/FileChanges.php @@ -34,6 +34,10 @@ class FileChanges implements IFilter { /** @var IURLGenerator */ protected $url; + /** + * @param IL10N $l + * @param IURLGenerator $url + */ public function __construct(IL10N $l, IURLGenerator $url) { $this->l = $l; $this->url = $url; diff --git a/apps/files/lib/Activity/Settings/FavoriteAction.php b/apps/files/lib/Activity/Settings/FavoriteAction.php new file mode 100644 index 00000000000..509c0883e1e --- /dev/null +++ b/apps/files/lib/Activity/Settings/FavoriteAction.php @@ -0,0 +1,98 @@ +<?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\Files\Activity\Settings; + + +use OCP\Activity\ISetting; +use OCP\IL10N; + +class FavoriteAction implements ISetting { + + /** @var IL10N */ + protected $l; + + /** + * @param IL10N $l + */ + public function __construct(IL10N $l) { + $this->l = $l; + } + + /** + * @return string Lowercase a-z and underscore only identifier + * @since 11.0.0 + */ + public function getIdentifier() { + return 'favorite'; + } + + /** + * @return string A translated string + * @since 11.0.0 + */ + public function getName() { + return $this->l->t('A file has been added to or removed from your <strong>favorites</strong>'); + } + + /** + * @return int whether the filter should be rather on the top or bottom of + * the admin section. The filters are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. + * @since 11.0.0 + */ + public function getPriority() { + return 5; + } + + /** + * @return bool True when the option can be changed for the stream + * @since 11.0.0 + */ + public function canChangeStream() { + return true; + } + + /** + * @return bool True when the option can be changed for the stream + * @since 11.0.0 + */ + public function isDefaultEnabledStream() { + return true; + } + + /** + * @return bool True when the option can be changed for the mail + * @since 11.0.0 + */ + public function canChangeMail() { + return true; + } + + /** + * @return bool True when the option can be changed for the stream + * @since 11.0.0 + */ + public function isDefaultEnabledMail() { + return false; + } +} + diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php index fc91e05ba7e..4e5ec03eecf 100644 --- a/apps/files/lib/AppInfo/Application.php +++ b/apps/files/lib/AppInfo/Application.php @@ -84,6 +84,7 @@ class Application extends App { $homeFolder = $c->query('ServerContainer')->getUserFolder(); return new TagService( $c->query('ServerContainer')->getUserSession(), + $c->query('ServerContainer')->getActivityManager(), $c->query('Tagger'), $homeFolder ); diff --git a/apps/files/lib/Service/TagService.php b/apps/files/lib/Service/TagService.php index 4482fb45371..cf80d780eaf 100644 --- a/apps/files/lib/Service/TagService.php +++ b/apps/files/lib/Service/TagService.php @@ -25,35 +25,42 @@ namespace OCA\Files\Service; -use OC\Files\FileInfo; -use OCP\Files\Node; +use OC\Tags; +use OCA\Files\Activity\FavoriteProvider; +use OCP\Activity\IManager; +use OCP\Files\Folder; +use OCP\ITags; +use OCP\IUser; +use OCP\IUserSession; /** * Service class to manage tags on files. */ class TagService { - /** - * @var \OCP\IUserSession - */ + /** @var IUserSession */ private $userSession; - - /** - * @var \OCP\ITags - */ + /** @var IManager */ + private $activityManager; + /** @var ITags */ private $tagger; + /** @var Folder */ + private $homeFolder; /** - * @var \OCP\Files\Folder + * @param IUserSession $userSession + * @param IManager $activityManager + * @param ITags $tagger + * @param Folder $homeFolder */ - private $homeFolder; - public function __construct( - \OCP\IUserSession $userSession, - \OCP\ITags $tagger, - \OCP\Files\Folder $homeFolder + IUserSession $userSession, + IManager $activityManager, + ITags $tagger, + Folder $homeFolder ) { $this->userSession = $userSession; + $this->activityManager = $activityManager; $this->tagger = $tagger; $this->homeFolder = $homeFolder; } @@ -79,10 +86,16 @@ class TagService { $newTags = array_diff($tags, $currentTags); foreach ($newTags as $tag) { + if ($tag === Tags::TAG_FAVORITE) { + $this->addActivity(true, $fileId, $path); + } $this->tagger->tagAs($fileId, $tag); } $deletedTags = array_diff($currentTags, $tags); foreach ($deletedTags as $tag) { + if ($tag === Tags::TAG_FAVORITE) { + $this->addActivity(false, $fileId, $path); + } $this->tagger->unTag($fileId, $tag); } @@ -90,5 +103,27 @@ class TagService { // list is up to date, in case of concurrent changes ? return $tags; } + + /** + * @param bool $addToFavorite + * @param int $fileId + * @param string $path + */ + protected function addActivity($addToFavorite, $fileId, $path) { + $user = $this->userSession->getUser(); + if (!$user instanceof IUser) { + return; + } + + $event = $this->activityManager->generateEvent(); + $event->setApp('files') + ->setObject('files', $fileId, $path) + ->setType('favorite') + ->setAuthor($user->getUID()) + ->setAffectedUser($user->getUID()) + ->setTimestamp(time()) + ->setSubject($addToFavorite ? FavoriteProvider::SUBJECT_ADDED : FavoriteProvider::SUBJECT_REMOVED); + $this->activityManager->publish($event); + } } diff --git a/apps/files/tests/Activity/Filter/GenericTest.php b/apps/files/tests/Activity/Filter/GenericTest.php new file mode 100644 index 00000000000..3788126dd94 --- /dev/null +++ b/apps/files/tests/Activity/Filter/GenericTest.php @@ -0,0 +1,117 @@ +<?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\Files\Tests\Activity\Filter; + + +use OCA\Files\Activity\Filter\Favorites; +use OCA\Files\Activity\Filter\FileChanges; +use OCP\Activity\IFilter; +use Test\TestCase; + +/** + * Class GenericTest + * + * @package OCA\Files\Tests\Activity\Filter + * @group DB + */ +class GenericTest extends TestCase { + + public function dataFilters() { + return [ + [Favorites::class], + [FileChanges::class], + ]; + } + + /** + * @dataProvider dataFilters + * @param string $filterClass + */ + public function testImplementsInterface($filterClass) { + $filter = \OC::$server->query($filterClass); + $this->assertInstanceOf(IFilter::class, $filter); + } + + /** + * @dataProvider dataFilters + * @param string $filterClass + */ + public function testGetIdentifier($filterClass) { + /** @var IFilter $filter */ + $filter = \OC::$server->query($filterClass); + $this->assertInternalType('string', $filter->getIdentifier()); + } + + /** + * @dataProvider dataFilters + * @param string $filterClass + */ + public function testGetName($filterClass) { + /** @var IFilter $filter */ + $filter = \OC::$server->query($filterClass); + $this->assertInternalType('string', $filter->getName()); + } + + /** + * @dataProvider dataFilters + * @param string $filterClass + */ + public function testGetPriority($filterClass) { + /** @var IFilter $filter */ + $filter = \OC::$server->query($filterClass); + $priority = $filter->getPriority(); + $this->assertInternalType('int', $filter->getPriority()); + $this->assertGreaterThanOrEqual(0, $priority); + $this->assertLessThanOrEqual(100, $priority); + } + + /** + * @dataProvider dataFilters + * @param string $filterClass + */ + public function testGetIcon($filterClass) { + /** @var IFilter $filter */ + $filter = \OC::$server->query($filterClass); + $this->assertInternalType('string', $filter->getIcon()); + $this->assertStringStartsWith('http', $filter->getIcon()); + } + + /** + * @dataProvider dataFilters + * @param string $filterClass + */ + public function testFilterTypes($filterClass) { + /** @var IFilter $filter */ + $filter = \OC::$server->query($filterClass); + $this->assertInternalType('array', $filter->filterTypes([])); + } + + /** + * @dataProvider dataFilters + * @param string $filterClass + */ + public function testAllowedApps($filterClass) { + /** @var IFilter $filter */ + $filter = \OC::$server->query($filterClass); + $this->assertInternalType('array', $filter->allowedApps()); + } +} diff --git a/apps/files/tests/Activity/Setting/GenericTest.php b/apps/files/tests/Activity/Setting/GenericTest.php new file mode 100644 index 00000000000..5ae15f02a02 --- /dev/null +++ b/apps/files/tests/Activity/Setting/GenericTest.php @@ -0,0 +1,127 @@ +<?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\Files\Tests\Activity\Setting; + +use OCA\Files\Activity\Settings\FavoriteAction; +use OCA\Files\Activity\Settings\FileChanged; +use OCA\Files\Activity\Settings\FileCreated; +use OCA\Files\Activity\Settings\FileDeleted; +use OCA\Files\Activity\Settings\FileFavorite; +use OCA\Files\Activity\Settings\FileRestored; +use OCP\Activity\ISetting; +use Test\TestCase; + +class GenericTest extends TestCase { + + public function dataSettings() { + return [ + [FavoriteAction::class], + [FileChanged::class], + [FileCreated::class], + [FileDeleted::class], + [FileFavorite::class], + [FileRestored::class], + ]; + } + + /** + * @dataProvider dataSettings + * @param string $settingClass + */ + public function testImplementsInterface($settingClass) { + $setting = \OC::$server->query($settingClass); + $this->assertInstanceOf(ISetting::class, $setting); + } + + /** + * @dataProvider dataSettings + * @param string $settingClass + */ + public function testGetIdentifier($settingClass) { + /** @var ISetting $setting */ + $setting = \OC::$server->query($settingClass); + $this->assertInternalType('string', $setting->getIdentifier()); + } + + /** + * @dataProvider dataSettings + * @param string $settingClass + */ + public function testGetName($settingClass) { + /** @var ISetting $setting */ + $setting = \OC::$server->query($settingClass); + $this->assertInternalType('string', $setting->getName()); + } + + /** + * @dataProvider dataSettings + * @param string $settingClass + */ + public function testGetPriority($settingClass) { + /** @var ISetting $setting */ + $setting = \OC::$server->query($settingClass); + $priority = $setting->getPriority(); + $this->assertInternalType('int', $setting->getPriority()); + $this->assertGreaterThanOrEqual(0, $priority); + $this->assertLessThanOrEqual(100, $priority); + } + + /** + * @dataProvider dataSettings + * @param string $settingClass + */ + public function testCanChangeStream($settingClass) { + /** @var ISetting $setting */ + $setting = \OC::$server->query($settingClass); + $this->assertInternalType('bool', $setting->canChangeStream()); + } + + /** + * @dataProvider dataSettings + * @param string $settingClass + */ + public function testIsDefaultEnabledStream($settingClass) { + /** @var ISetting $setting */ + $setting = \OC::$server->query($settingClass); + $this->assertInternalType('bool', $setting->isDefaultEnabledStream()); + } + + /** + * @dataProvider dataSettings + * @param string $settingClass + */ + public function testCanChangeMail($settingClass) { + /** @var ISetting $setting */ + $setting = \OC::$server->query($settingClass); + $this->assertInternalType('bool', $setting->canChangeMail()); + } + + /** + * @dataProvider dataSettings + * @param string $settingClass + */ + public function testIsDefaultEnabledMail($settingClass) { + /** @var ISetting $setting */ + $setting = \OC::$server->query($settingClass); + $this->assertInternalType('bool', $setting->isDefaultEnabledMail()); + } +} diff --git a/apps/files/tests/Service/TagServiceTest.php b/apps/files/tests/Service/TagServiceTest.php index b8d36487585..1c4ac2328ec 100644 --- a/apps/files/tests/Service/TagServiceTest.php +++ b/apps/files/tests/Service/TagServiceTest.php @@ -24,7 +24,9 @@ */ namespace OCA\Files\Tests\Service; +use OC\Tags; use OCA\Files\Service\TagService; +use OCP\Activity\IManager; use OCP\IUserSession; /** @@ -41,13 +43,19 @@ class TagServiceTest extends \Test\TestCase { */ private $user; + /** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */ + private $userSession; + + /** @var IManager|\PHPUnit_Framework_MockObject_MockObject */ + private $activityManager; + /** * @var \OCP\Files\Folder */ private $root; /** - * @var \OCA\Files\Service\TagService + * @var \OCA\Files\Service\TagService|\PHPUnit_Framework_MockObject_MockObject */ private $tagService; @@ -59,6 +67,7 @@ class TagServiceTest extends \Test\TestCase { protected function setUp() { parent::setUp(); $this->user = $this->getUniqueID('user'); + $this->activityManager = $this->createMock(IManager::class); \OC::$server->getUserManager()->createUser($this->user, 'test'); \OC_User::setUserId($this->user); \OC_Util::setupFS($this->user); @@ -67,8 +76,8 @@ class TagServiceTest extends \Test\TestCase { /** * @var \OCP\IUserSession */ - $userSession = $this->createMock(IUserSession::class); - $userSession->expects($this->any()) + $this->userSession = $this->createMock(IUserSession::class); + $this->userSession->expects($this->any()) ->method('getUser') ->withAnyParameters() ->will($this->returnValue($user)); @@ -76,11 +85,24 @@ class TagServiceTest extends \Test\TestCase { $this->root = \OC::$server->getUserFolder(); $this->tagger = \OC::$server->getTagManager()->load('files'); - $this->tagService = new TagService( - $userSession, - $this->tagger, - $this->root - ); + $this->tagService = $this->getTagService(['addActivity']); + } + + /** + * @param array $methods + * @return TagService|\PHPUnit_Framework_MockObject_MockObject + */ + protected function getTagService(array $methods = []) { + return $this->getMockBuilder(TagService::class) + ->setConstructorArgs([ + $this->userSession, + $this->activityManager, + $this->tagger, + $this->root, + ]) + ->setMethods($methods) + ->getMock(); + } protected function tearDown() { @@ -93,6 +115,9 @@ class TagServiceTest extends \Test\TestCase { $tag1 = 'tag1'; $tag2 = 'tag2'; + $this->tagService->expects($this->never()) + ->method('addActivity'); + $subdir = $this->root->newFolder('subdir'); $testFile = $subdir->newFile('test.txt'); $testFile->putContent('test contents'); @@ -126,5 +151,27 @@ class TagServiceTest extends \Test\TestCase { $subdir->delete(); } + + public function testFavoriteActivity() { + + $subdir = $this->root->newFolder('subdir'); + $file = $subdir->newFile('test.txt'); + + $this->tagService->expects($this->exactly(2)) + ->method('addActivity') + ->withConsecutive( + [true, $file->getId(), 'subdir/test.txt'], + [false, $file->getId(), 'subdir/test.txt'] + ); + + // set tags + $this->tagService->updateFileTags('subdir/test.txt', [Tags::TAG_FAVORITE]); + + // remove tag + $this->tagService->updateFileTags('subdir/test.txt', []); + + + $subdir->delete(); + } } diff --git a/apps/systemtags/appinfo/routes.php b/apps/systemtags/appinfo/routes.php new file mode 100644 index 00000000000..f39c6cdc49c --- /dev/null +++ b/apps/systemtags/appinfo/routes.php @@ -0,0 +1,28 @@ +<?php +/** + * @copyright Copyright (c) 2016, Joas Schilling <coding@schilljs.com> + * + * @author 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/>. + * + */ + +return [ + 'routes' => [ + ['name' => 'LastUsed#getLastUsedTagIds', 'url' => '/lastused', 'verb' => 'GET'], + ] +]; diff --git a/apps/systemtags/js/admin.js b/apps/systemtags/js/admin.js index ed21f82f3ba..1388d5b2d7c 100644 --- a/apps/systemtags/js/admin.js +++ b/apps/systemtags/js/admin.js @@ -153,6 +153,12 @@ }, escapeMarkup: function(m) { return m; + }, + sortResults: function(results) { + results.sort(function(a, b) { + return OC.Util.naturalSortCompare(a.get('name'), b.get('name')); + }); + return results; } } }; diff --git a/apps/systemtags/js/systemtagsfilelist.js b/apps/systemtags/js/systemtagsfilelist.js index 25f377785b7..c2a6f09fd9e 100644 --- a/apps/systemtags/js/systemtagsfilelist.js +++ b/apps/systemtags/js/systemtagsfilelist.js @@ -35,6 +35,7 @@ * @type Array.<string> */ _systemTagIds: [], + _lastUsedTags: [], _clientSideSort: true, _allowSelection: false, @@ -58,6 +59,7 @@ var $controls = this.$el.find('#controls').empty(); + _.defer(_.bind(this._getLastUsedTags, this)); this._initFilterField($controls); }, @@ -67,7 +69,19 @@ OCA.Files.FileList.prototype.destroy.apply(this, arguments); }, + _getLastUsedTags: function() { + var self = this; + $.ajax({ + type: 'GET', + url: OC.generateUrl('/apps/systemtags/lastused'), + success: function (response) { + self._lastUsedTags = response; + } + }); + }, + _initFilterField: function($container) { + var self = this; this.$filterField = $('<input type="hidden" name="tags"/>'); $container.append(this.$filterField); this.$filterField.select2({ @@ -112,6 +126,27 @@ return OC.SystemTags.getDescriptiveTag(tag)[0].outerHTML; }, + sortResults: function(results) { + results.sort(function(a, b) { + var aLastUsed = self._lastUsedTags.indexOf(a.id); + var bLastUsed = self._lastUsedTags.indexOf(b.id); + + if (aLastUsed !== bLastUsed) { + if (bLastUsed === -1) { + return -1; + } + if (aLastUsed === -1) { + return 1; + } + return aLastUsed < bLastUsed ? -1 : 1; + } + + // Both not found + return OC.Util.naturalSortCompare(a.name, b.name); + }); + return results; + }, + escapeMarkup: function(m) { // prevent double markup escape return m; diff --git a/apps/systemtags/lib/Activity/Listener.php b/apps/systemtags/lib/Activity/Listener.php index dbddf3c38bc..152608f7cce 100644 --- a/apps/systemtags/lib/Activity/Listener.php +++ b/apps/systemtags/lib/Activity/Listener.php @@ -28,6 +28,7 @@ use OCP\App\IAppManager; use OCP\Files\Config\IMountProviderCollection; use OCP\Files\IRootFolder; use OCP\Files\Node; +use OCP\IConfig; use OCP\IGroup; use OCP\IGroupManager; use OCP\IUser; @@ -46,6 +47,8 @@ class Listener { protected $activityManager; /** @var IUserSession */ protected $session; + /** @var IConfig */ + protected $config; /** @var \OCP\SystemTag\ISystemTagManager */ protected $tagManager; /** @var \OCP\App\IAppManager */ @@ -61,6 +64,7 @@ class Listener { * @param IGroupManager $groupManager * @param IManager $activityManager * @param IUserSession $session + * @param IConfig $config * @param ISystemTagManager $tagManager * @param IAppManager $appManager * @param IMountProviderCollection $mountCollection @@ -69,6 +73,7 @@ class Listener { public function __construct(IGroupManager $groupManager, IManager $activityManager, IUserSession $session, + IConfig $config, ISystemTagManager $tagManager, IAppManager $appManager, IMountProviderCollection $mountCollection, @@ -76,6 +81,7 @@ class Listener { $this->groupManager = $groupManager; $this->activityManager = $activityManager; $this->session = $session; + $this->config = $config; $this->tagManager = $tagManager; $this->appManager = $appManager; $this->mountCollection = $mountCollection; @@ -126,6 +132,11 @@ class Listener { $this->activityManager->publish($activity); } } + + + if ($actor !== '' && ($event->getEvent() === ManagerEvent::EVENT_CREATE || $event->getEvent() === ManagerEvent::EVENT_UPDATE)) { + $this->updateLastUsedTags($actor, $event->getTag()); + } } /** @@ -213,6 +224,27 @@ class Listener { $this->activityManager->publish($activity); } } + + if ($actor !== '' && $event->getEvent() === MapperEvent::EVENT_ASSIGN) { + foreach ($tags as $tag) { + $this->updateLastUsedTags($actor, $tag); + } + } + } + + /** + * @param string $actor + * @param ISystemTag $tag + */ + protected function updateLastUsedTags($actor, ISystemTag $tag) { + $lastUsedTags = $this->config->getUserValue($actor, 'systemtags', 'last_used', '[]'); + $lastUsedTags = json_decode($lastUsedTags, true); + + array_unshift($lastUsedTags, $tag->getId()); + array_unique($lastUsedTags); + $lastUsedTags = array_slice($lastUsedTags, 0, 10); + + $this->config->setUserValue($actor, 'systemtags', 'last_used', json_encode($lastUsedTags)); } /** diff --git a/apps/systemtags/lib/Controller/LastUsedController.php b/apps/systemtags/lib/Controller/LastUsedController.php new file mode 100644 index 00000000000..cbd149d75fb --- /dev/null +++ b/apps/systemtags/lib/Controller/LastUsedController.php @@ -0,0 +1,59 @@ +<?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\SystemTags\Controller; + + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\DataResponse; +use OCP\IConfig; +use OCP\IRequest; +use OCP\IUserSession; + +class LastUsedController extends Controller { + + /** @var IConfig */ + protected $config; + + /** @var IUserSession */ + protected $userSession; + + /** + * @param string $appName + * @param IRequest $request + * @param IConfig $config + * @param IUserSession $userSession + */ + public function __construct($appName, IRequest $request, IConfig $config, IUserSession $userSession) { + parent::__construct($appName, $request); + $this->config = $config; + $this->userSession = $userSession; + } + + /** + * @NoAdminRequired + */ + public function getLastUsedTagIds() { + $lastUsed = $this->config->getUserValue($this->userSession->getUser()->getUID(), 'systemtags', 'last_used', '[]'); + $tagIds = json_decode($lastUsed, true); + return new DataResponse(array_map(function($id) { return (string) $id; }, $tagIds)); + } +} diff --git a/core/js/systemtags/systemtagsinputfield.js b/core/js/systemtags/systemtagsinputfield.js index 5d986d17290..690525c0ebb 100644 --- a/core/js/systemtags/systemtagsinputfield.js +++ b/core/js/systemtags/systemtagsinputfield.js @@ -57,6 +57,8 @@ _newTag: null, + _lastUsedTags: [], + className: 'systemTagsInputFieldContainer', template: function(data) { @@ -97,6 +99,8 @@ _.defer(self._refreshSelection); }); + _.defer(_.bind(this._getLastUsedTags, this)); + _.bindAll( this, '_refreshSelection', @@ -108,6 +112,17 @@ ); }, + _getLastUsedTags: function() { + var self = this; + $.ajax({ + type: 'GET', + url: OC.generateUrl('/apps/systemtags/lastused'), + success: function (response) { + self._lastUsedTags = response; + } + }); + }, + /** * Refreshes the selection, triggering a call to * select2's initSelection @@ -211,6 +226,7 @@ }, { success: function(model) { self._addToSelect2Selection(model.toJSON()); + self._lastUsedTags.unshift(model.id); self.trigger('select', model); }, error: function(model, xhr) { @@ -238,6 +254,7 @@ return false; } else { tag = this.collection.get(e.object.id); + this._lastUsedTags.unshift(tag.id); } this._newTag = null; this.trigger('select', tag); @@ -400,6 +417,20 @@ var aSelected = selectedItems.indexOf(a.id) >= 0; var bSelected = selectedItems.indexOf(b.id) >= 0; if (aSelected === bSelected) { + var aLastUsed = self._lastUsedTags.indexOf(a.id); + var bLastUsed = self._lastUsedTags.indexOf(b.id); + + if (aLastUsed !== bLastUsed) { + if (bLastUsed === -1) { + return -1; + } + if (aLastUsed === -1) { + return 1; + } + return aLastUsed < bLastUsed ? -1 : 1; + } + + // Both not found return OC.Util.naturalSortCompare(a.name, b.name); } if (aSelected && !bSelected) { diff --git a/lib/private/Files/Storage/Wrapper/Encryption.php b/lib/private/Files/Storage/Wrapper/Encryption.php index 8531ccc0dd0..c0ccd22d147 100644 --- a/lib/private/Files/Storage/Wrapper/Encryption.php +++ b/lib/private/Files/Storage/Wrapper/Encryption.php @@ -444,7 +444,7 @@ class Encryption extends Wrapper { } // encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt - if (!$encryptionEnabled || !$this->mount->getOption('encrypt', true)) { + if (!$encryptionEnabled || !$this->shouldEncrypt($path)) { if (!$targetExists || !$targetIsEncrypted) { $shouldEncrypt = false; } @@ -651,7 +651,7 @@ class Encryption extends Wrapper { * @param bool $isRename */ private function updateEncryptedVersion(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename) { - $isEncrypted = $this->encryptionManager->isEnabled() && $this->mount->getOption('encrypt', true) ? 1 : 0; + $isEncrypted = $this->encryptionManager->isEnabled() && $this->shouldEncrypt($targetInternalPath) ? 1 : 0; $cacheInformation = [ 'encrypted' => (bool)$isEncrypted, ]; @@ -954,6 +954,7 @@ class Encryption extends Wrapper { throw $e; } } + return $encryptionModule; } @@ -991,4 +992,31 @@ class Encryption extends Wrapper { return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/'; } + /** + * check if the given storage should be encrypted or not + * + * @param $path + * @return bool + */ + protected function shouldEncrypt($path) { + $fullPath = $this->getFullPath($path); + $mountPointConfig = $this->mount->getOption('encrypt', true); + if ($mountPointConfig === false) { + return false; + } + + try { + $encryptionModule = $this->getEncryptionModule($fullPath); + } catch (ModuleDoesNotExistsException $e) { + return false; + } + + if ($encryptionModule === null) { + $encryptionModule = $this->encryptionManager->getEncryptionModule(); + } + + return $encryptionModule->shouldEncrypt($fullPath); + + } + } diff --git a/settings/js/apps.js b/settings/js/apps.js index 451becc67a0..b52fb3d11ab 100644 --- a/settings/js/apps.js +++ b/settings/js/apps.js @@ -27,7 +27,9 @@ OC.Settings.Apps = OC.Settings.Apps || { State: { currentCategory: null, - apps: null + apps: null, + $updateNotification: null, + availableUpdates: 0 }, loadCategories: function() { @@ -77,8 +79,9 @@ OC.Settings.Apps = OC.Settings.Apps || { $('#app-category-' + OC.Settings.Apps.State.currentCategory).removeClass('active'); $('#app-category-' + categoryId).addClass('active'); OC.Settings.Apps.State.currentCategory = categoryId; + OC.Settings.Apps.State.availableUpdates = 0; - this._loadCategoryCall = $.ajax(OC.generateUrl('settings/apps/list?category={categoryId}&includeUpdateInfo=0', { + this._loadCategoryCall = $.ajax(OC.generateUrl('settings/apps/list?category={categoryId}', { categoryId: categoryId }), { type:'GET', @@ -109,7 +112,18 @@ OC.Settings.Apps = OC.Settings.Apps || { } else { OC.Settings.Apps.renderApp(app, template, null, false); } + + if (app.update) { + var $update = $('#app-' + app.id + ' .update'); + $update.removeClass('hidden'); + $update.val(t('settings', 'Update to %s').replace(/%s/g, app.update)); + OC.Settings.Apps.State.availableUpdates++; + } }); + + if (OC.Settings.Apps.State.availableUpdates > 0) { + OC.Settings.Apps.State.$updateNotification = OC.Notification.show(n('settings', 'You have %n app update pending', 'You have %n app updates pending', OC.Settings.Apps.State.availableUpdates)); + } } else { $('#apps-list').addClass('hidden'); $('#apps-list-empty').removeClass('hidden').find('h2').text(t('settings', 'No apps found for your version')); @@ -138,28 +152,7 @@ OC.Settings.Apps = OC.Settings.Apps || { }); }, complete: function() { - var availableUpdates = 0; $('#apps-list').removeClass('icon-loading'); - $.ajax(OC.generateUrl('settings/apps/list?category={categoryId}&includeUpdateInfo=1', { - categoryId: categoryId - }), { - type: 'GET', - success: function (apps) { - _.each(apps.apps, function(app) { - if (app.update) { - var $update = $('#app-' + app.id + ' .update'); - $update.removeClass('hidden'); - $update.val(t('settings', 'Update to %s').replace(/%s/g, app.update)); - availableUpdates++; - OC.Settings.Apps.State.apps[app.id].update = true; - } - }); - - if (availableUpdates > 0) { - OC.Notification.show(n('settings', 'You have %n app update pending', 'You have %n app updates pending', availableUpdates)); - } - } - }); } }); }, @@ -390,6 +383,20 @@ OC.Settings.Apps = OC.Settings.Apps || { else { element.val(t('settings','Updated')); element.hide(); + + var $update = $('#app-' + appId + ' .update'); + $update.addClass('hidden'); + var $version = $('#app-' + appId + ' .app-version'); + $version.text(OC.Settings.Apps.State.apps[appId]['update']); + + if (OC.Settings.Apps.State.$updateNotification) { + OC.Notification.hide(OC.Settings.Apps.State.$updateNotification); + } + + OC.Settings.Apps.State.availableUpdates--; + if (OC.Settings.Apps.State.availableUpdates > 0) { + OC.Settings.Apps.State.$updateNotification = OC.Notification.show(n('settings', 'You have %n app update pending', 'You have %n app updates pending', OC.Settings.Apps.State.availableUpdates)); + } } },'json'); }, diff --git a/tests/lib/Files/Storage/Wrapper/EncryptionTest.php b/tests/lib/Files/Storage/Wrapper/EncryptionTest.php index 245f39f5e2e..fb3b463e43b 100644 --- a/tests/lib/Files/Storage/Wrapper/EncryptionTest.php +++ b/tests/lib/Files/Storage/Wrapper/EncryptionTest.php @@ -2,13 +2,21 @@ namespace Test\Files\Storage\Wrapper; +use OC\Encryption\Exceptions\ModuleDoesNotExistsException; +use OC\Encryption\Update; use OC\Encryption\Util; use OC\Files\Storage\Temporary; +use OC\Files\Storage\Wrapper\Encryption; use OC\Files\View; use OC\Log; use OC\Memcache\ArrayCache; use OC\User\Manager; +use OCP\Encryption\IEncryptionModule; +use OCP\Encryption\IFile; +use OCP\Encryption\Keys\IStorage; use OCP\Files\Cache\ICache; +use OCP\Files\Mount\IMountPoint; +use OCP\ILogger; use Test\Files\Storage\Storage; class EncryptionTest extends Storage { @@ -926,4 +934,90 @@ class EncryptionTest extends Storage { ]; } + /** + * @dataProvider dataTestShouldEncrypt + * + * @param bool $encryptMountPoint + * @param \PHPUnit_Framework_MockObject_MockObject | IEncryptionModule $encryptionModule + * @param bool $encryptionModuleShouldEncrypt + * @param bool $expected + */ + public function testShouldEncrypt( + $encryptMountPoint, + $encryptionModule, + $encryptionModuleShouldEncrypt, + $expected + ) { + $encryptionManager = $this->createMock(\OC\Encryption\Manager::class); + $util = $this->createMock(Util::class); + $logger = $this->createMock(ILogger::class); + $fileHelper = $this->createMock(IFile::class); + $uid = null; + $keyStorage = $this->createMock(IStorage::class); + $update = $this->createMock(Update::class); + $mountManager = $this->createMock(\OC\Files\Mount\Manager::class); + $mount = $this->createMock(IMountPoint::class); + $arrayCache = $this->createMock(ArrayCache::class); + $path = '/welcome.txt'; + $fullPath = 'admin/files/welcome.txt'; + $defaultEncryptionModule = $this->createMock(IEncryptionModule::class); + + $wrapper = $this->getMockBuilder(Encryption::class) + ->setConstructorArgs( + [ + ['mountPoint' => '', 'mount' => $mount, 'storage' => ''], + $encryptionManager, + $util, + $logger, + $fileHelper, + $uid, + $keyStorage, + $update, + $mountManager, + $arrayCache + ] + ) + ->setMethods(['getFullPath', 'getEncryptionModule']) + ->getMock(); + + $wrapper->method('getFullPath')->with($path)->willReturn($fullPath); + $wrapper->method('getEncryptionModule')->with($fullPath) + ->willReturnCallback( + function() use ($encryptionModule) { + if ($encryptionModule === false) { + throw new ModuleDoesNotExistsException(); + } + return $encryptionModule; + } + ); + $mount->expects($this->once())->method('getOption')->with('encrypt', true) + ->willReturn($encryptMountPoint); + + if ($encryptionModule !== null && $encryptionModule !== false) { + $encryptionModule->method('shouldEncrypt')->with($fullPath) + ->willReturn($encryptionModuleShouldEncrypt); + } + + if ($encryptionModule === null) { + $encryptionManager->expects($this->once())->method('getEncryptionModule') + ->willReturn($defaultEncryptionModule); + } + $defaultEncryptionModule->method('shouldEncrypt')->willReturn(true); + + $result = $this->invokePrivate($wrapper, 'shouldEncrypt', [$path]); + + $this->assertSame($expected, $result); + } + + public function dataTestShouldEncrypt() { + $encryptionModule = $this->createMock(IEncryptionModule::class); + return [ + [false, false, false, false], + [true, false, false, false], + [true, $encryptionModule, false, false], + [true, $encryptionModule, true, true], + [true, null, false, true], + ]; + } + } |