diff options
Diffstat (limited to 'apps')
36 files changed, 2111 insertions, 51 deletions
diff --git a/apps/dav/appinfo/application.php b/apps/dav/appinfo/application.php index 07905db7368..11ab384d75d 100644 --- a/apps/dav/appinfo/application.php +++ b/apps/dav/appinfo/application.php @@ -20,12 +20,16 @@ */ namespace OCA\Dav\AppInfo; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CardDAV\CardDavBackend; use OCA\DAV\CardDAV\ContactsManager; use OCA\DAV\CardDAV\SyncJob; use OCA\DAV\CardDAV\SyncService; use OCA\DAV\HookManager; use OCA\Dav\Migration\AddressBookAdapter; +use OCA\Dav\Migration\CalendarAdapter; use OCA\Dav\Migration\MigrateAddressbooks; +use OCA\Dav\Migration\MigrateCalendars; use \OCP\AppFramework\App; use OCP\AppFramework\IAppContainer; use OCP\Contacts\IManager; @@ -73,7 +77,17 @@ class Application extends App { $c->getServer()->getUserManager(), $c->getServer()->getGroupManager() ); - return new \OCA\DAV\CardDAV\CardDavBackend($db, $principal, $logger); + return new CardDavBackend($db, $principal, $logger); + }); + + $container->registerService('CalDavBackend', function($c) { + /** @var IAppContainer $c */ + $db = $c->getServer()->getDatabaseConnection(); + $principal = new \OCA\DAV\Connector\Sabre\Principal( + $c->getServer()->getUserManager(), + $c->getServer()->getGroupManager() + ); + return new CalDavBackend($db, $principal); }); $container->registerService('MigrateAddressbooks', function($c) { @@ -84,6 +98,15 @@ class Application extends App { $c->query('CardDavBackend') ); }); + + $container->registerService('MigrateCalendars', function($c) { + /** @var IAppContainer $c */ + $db = $c->getServer()->getDatabaseConnection(); + return new MigrateCalendars( + new CalendarAdapter($db), + $c->query('CalDavBackend') + ); + }); } /** @@ -112,8 +135,8 @@ class Application extends App { } public function migrateAddressbooks() { - try { + /** @var MigrateAddressbooks $migration */ $migration = $this->getContainer()->query('MigrateAddressbooks'); $migration->setup(); $userManager = $this->getContainer()->getServer()->getUserManager(); @@ -127,4 +150,19 @@ class Application extends App { } } + public function migrateCalendars() { + try { + /** @var MigrateCalendars $migration */ + $migration = $this->getContainer()->query('MigrateCalendars'); + $migration->setup(); + $userManager = $this->getContainer()->getServer()->getUserManager(); + + $userManager->callForAllUsers(function($user) use($migration) { + /** @var IUser $user */ + $migration->migrateForUser($user->getUID()); + }); + } catch (\Exception $ex) { + $this->getContainer()->getServer()->getLogger()->logException($ex); + } + } } diff --git a/apps/dav/appinfo/install.php b/apps/dav/appinfo/install.php index f6ef533958e..a7a3220b90f 100644 --- a/apps/dav/appinfo/install.php +++ b/apps/dav/appinfo/install.php @@ -24,3 +24,4 @@ use OCA\Dav\AppInfo\Application; $app = new Application(); $app->setupCron(); $app->migrateAddressbooks(); +$app->migrateCalendars(); diff --git a/apps/dav/appinfo/register_command.php b/apps/dav/appinfo/register_command.php index e8ca370f84f..4981cab9264 100644 --- a/apps/dav/appinfo/register_command.php +++ b/apps/dav/appinfo/register_command.php @@ -23,6 +23,7 @@ use OCA\Dav\AppInfo\Application; use OCA\DAV\Command\CreateAddressBook; use OCA\DAV\Command\CreateCalendar; use OCA\Dav\Command\MigrateAddressbooks; +use OCA\Dav\Command\MigrateCalendars; use OCA\DAV\Command\SyncSystemAddressBook; $config = \OC::$server->getConfig(); @@ -44,4 +45,6 @@ if ($config->getSystemValue('debug', false)){ $app = new \OCA\Dav\AppInfo\Application(); $migration = $app->getContainer()->query('MigrateAddressbooks'); $application->add(new MigrateAddressbooks($userManager, $migration)); + $migration = $app->getContainer()->query('MigrateCalendars'); + $application->add(new MigrateCalendars($userManager, $migration)); } diff --git a/apps/dav/command/migrateaddressbooks.php b/apps/dav/command/migrateaddressbooks.php index 2ab7113ab1f..f37c29e7ab3 100644 --- a/apps/dav/command/migrateaddressbooks.php +++ b/apps/dav/command/migrateaddressbooks.php @@ -68,6 +68,7 @@ class MigrateAddressbooks extends Command { } $output->writeln("Start migration for $user"); $this->service->migrateForUser($user); + return; } $output->writeln("Start migration of all known users ..."); $p = new ProgressBar($output); diff --git a/apps/dav/command/migratecalendars.php b/apps/dav/command/migratecalendars.php new file mode 100644 index 00000000000..eda4f5fb417 --- /dev/null +++ b/apps/dav/command/migratecalendars.php @@ -0,0 +1,66 @@ +<?php + +namespace OCA\Dav\Command; + +use OCP\IUser; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class MigrateCalendars extends Command { + + /** @var IUserManager */ + protected $userManager; + + /** @var \OCA\Dav\Migration\MigrateCalendars */ + private $service; + + /** + * @param IUserManager $userManager + * @param \OCA\Dav\Migration\MigrateCalendars $service + */ + function __construct(IUserManager $userManager, + \OCA\Dav\Migration\MigrateCalendars $service + ) { + parent::__construct(); + $this->userManager = $userManager; + $this->service = $service; + } + + protected function configure() { + $this + ->setName('dav:migrate-calendars') + ->setDescription('Migrate calendars from the calendar app to core') + ->addArgument('user', + InputArgument::OPTIONAL, + 'User for whom all calendars will be migrated'); + } + + protected function execute(InputInterface $input, OutputInterface $output) { + $this->service->setup(); + + if ($input->hasArgument('user')) { + $user = $input->getArgument('user'); + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User <$user> in unknown."); + } + $output->writeln("Start migration for $user"); + $this->service->migrateForUser($user); + return; + } + $output->writeln("Start migration of all known users ..."); + $p = new ProgressBar($output); + $p->start(); + $this->userManager->callForAllUsers(function($user) use ($p) { + $p->advance(); + /** @var IUser $user */ + $this->service->migrateForUser($user->getUID()); + }); + + $p->finish(); + $output->writeln(''); + } +} diff --git a/apps/dav/lib/caldav/caldavbackend.php b/apps/dav/lib/caldav/caldavbackend.php index 52b4812b05b..775612487f9 100644 --- a/apps/dav/lib/caldav/caldavbackend.php +++ b/apps/dav/lib/caldav/caldavbackend.php @@ -22,9 +22,11 @@ namespace OCA\DAV\CalDAV; +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\IDBConnection; use Sabre\CalDAV\Backend\AbstractBackend; use Sabre\CalDAV\Backend\SchedulingSupport; use Sabre\CalDAV\Backend\SubscriptionSupport; @@ -59,7 +61,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription const MAX_DATE = '2038-01-01'; /** - * List of CalDAV properties, and how they map to database fieldnames + * List of CalDAV properties, and how they map to database field names * Add your own properties by simply adding on to this array. * * Note that only string-based properties are supported here. @@ -75,7 +77,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ]; /** - * List of subscription properties, and how they map to database fieldnames. + * List of subscription properties, and how they map to database field names. * * @var array */ @@ -89,7 +91,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments', ]; - /** @var \OCP\IDBConnection */ + /** @var IDBConnection */ private $db; /** @var Backend */ @@ -101,9 +103,10 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription /** * CalDavBackend constructor. * - * @param \OCP\IDBConnection $db + * @param IDBConnection $db + * @param Principal $principalBackend */ - public function __construct(\OCP\IDBConnection $db, Principal $principalBackend) { + public function __construct(IDBConnection $db, Principal $principalBackend) { $this->db = $db; $this->principalBackend = $principalBackend; $this->sharingBackend = new Backend($this->db, $principalBackend, 'calendar'); @@ -232,6 +235,95 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return array_values($calendars); } + public function getCalendarByUri($principal, $uri) { + $fields = array_values($this->propertyMap); + $fields[] = 'id'; + $fields[] = 'uri'; + $fields[] = 'synctoken'; + $fields[] = 'components'; + $fields[] = 'principaluri'; + $fields[] = 'transparent'; + + // Making fields a comma-delimited list + $query = $this->db->getQueryBuilder(); + $query->select($fields)->from('calendars') + ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) + ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal))) + ->setMaxResults(1); + $stmt = $query->execute(); + + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + $stmt->closeCursor(); + if ($row === false) { + return null; + } + + $components = []; + if ($row['components']) { + $components = explode(',',$row['components']); + } + + $calendar = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), + '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), + ]; + + foreach($this->propertyMap as $xmlName=>$dbName) { + $calendar[$xmlName] = $row[$dbName]; + } + + return $calendar; + } + + public function getCalendarById($calendarId) { + $fields = array_values($this->propertyMap); + $fields[] = 'id'; + $fields[] = 'uri'; + $fields[] = 'synctoken'; + $fields[] = 'components'; + $fields[] = 'principaluri'; + $fields[] = 'transparent'; + + // Making fields a comma-delimited list + $query = $this->db->getQueryBuilder(); + $query->select($fields)->from('calendars') + ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))) + ->setMaxResults(1); + $stmt = $query->execute(); + + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + $stmt->closeCursor(); + if ($row === false) { + return null; + } + + $components = []; + if ($row['components']) { + $components = explode(',',$row['components']); + } + + $calendar = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), + '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), + ]; + + foreach($this->propertyMap as $xmlName=>$dbName) { + $calendar[$xmlName] = $row[$dbName]; + } + + return $calendar; + } + /** * Creates a new calendar for a principal. * @@ -241,7 +333,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @param string $principalUri * @param string $calendarUri * @param array $properties - * @return void + * @return int */ function createCalendar($principalUri, $calendarUri, array $properties) { $values = [ @@ -278,6 +370,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $query->setValue($column, $query->createNamedParameter($value)); } $query->execute(); + return $query->getLastInsertId(); } /** @@ -1249,16 +1342,29 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return $cardData; } + /** + * @param IShareable $shareable + * @param array $add + * @param array $remove + */ public function updateShares($shareable, $add, $remove) { $this->sharingBackend->updateShares($shareable, $add, $remove); } + /** + * @param int $resourceId + * @return array + */ public function getShares($resourceId) { return $this->sharingBackend->getShares($resourceId); } - public function applyShareAcl($addressBookId, $acl) { - return $this->sharingBackend->applyShareAcl($addressBookId, $acl); + /** + * @param int $resourceId + * @param array $acl + * @return array + */ + public function applyShareAcl($resourceId, $acl) { + return $this->sharingBackend->applyShareAcl($resourceId, $acl); } - } diff --git a/apps/dav/lib/connector/sabre/filesreportplugin.php b/apps/dav/lib/connector/sabre/filesreportplugin.php index 5bdd7a71ddc..141b684360e 100644 --- a/apps/dav/lib/connector/sabre/filesreportplugin.php +++ b/apps/dav/lib/connector/sabre/filesreportplugin.php @@ -211,7 +211,7 @@ class FilesReportPlugin extends ServerPlugin { */ public function processFilterRules($filterRules) { $ns = '{' . $this::NS_OWNCLOUD . '}'; - $resultFileIds = []; + $resultFileIds = null; $systemTagIds = []; foreach ($filterRules as $filterRule) { if ($filterRule['name'] === $ns . 'systemtag') { @@ -239,11 +239,22 @@ class FilesReportPlugin extends ServerPlugin { foreach ($systemTagIds as $systemTagId) { $fileIds = $this->tagMapper->getObjectIdsForTags($systemTagId, 'files'); - if (empty($resultFileIds)) { + if (empty($fileIds)) { + // This tag has no files, nothing can ever show up + return []; + } + + // first run ? + if ($resultFileIds === null) { $resultFileIds = $fileIds; } else { $resultFileIds = array_intersect($resultFileIds, $fileIds); } + + if (empty($resultFileIds)) { + // Empty intersection, nothing can show up anymore + return []; + } } return $resultFileIds; } diff --git a/apps/dav/lib/migration/calendaradapter.php b/apps/dav/lib/migration/calendaradapter.php new file mode 100644 index 00000000000..39faf2cec37 --- /dev/null +++ b/apps/dav/lib/migration/calendaradapter.php @@ -0,0 +1,83 @@ +<?php + +namespace OCA\Dav\Migration; + +use OCP\IDBConnection; + +class CalendarAdapter { + + /** @var \OCP\IDBConnection */ + protected $dbConnection; + + /** @var string */ + private $sourceCalendarTable; + + /** @var string */ + private $sourceCalObjTable; + + /** + * @param IDBConnection $dbConnection + * @param string $sourceCalendarTable + * @param string $sourceCalObjTable + */ + function __construct(IDBConnection $dbConnection, + $sourceCalendarTable = 'clndr_calendars', + $sourceCalObjTable = 'clndr_objects') { + $this->dbConnection = $dbConnection; + $this->sourceCalendarTable = $sourceCalendarTable; + $this->sourceCalObjTable = $sourceCalObjTable; + } + + /** + * @param string $user + * @param \Closure $callBack + */ + public function foreachCalendar($user, \Closure $callBack) { + // get all calendars of that user + $query = $this->dbConnection->getQueryBuilder(); + $stmt = $query->select('*')->from($this->sourceCalendarTable) + ->where($query->expr()->eq('userid', $query->createNamedParameter($user))) + ->execute(); + + while($row = $stmt->fetch()) { + $callBack($row); + } + } + + public function setup() { + if (!$this->dbConnection->tableExists($this->sourceCalendarTable)) { + throw new \DomainException('Calendar tables are missing. Nothing to do.'); + } + } + + /** + * @param int $calendarId + * @param \Closure $callBack + */ + public function foreachCalendarObject($calendarId, \Closure $callBack) { + $query = $this->dbConnection->getQueryBuilder(); + $stmt = $query->select('*')->from($this->sourceCalObjTable) + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) + ->execute(); + + while($row = $stmt->fetch()) { + $callBack($row); + } + } + + /** + * @param int $addressBookId + * @return array + */ + public function getShares($addressBookId) { + $query = $this->dbConnection->getQueryBuilder(); + $shares = $query->select('*')->from('share') + ->where($query->expr()->eq('item_source', $query->createNamedParameter($addressBookId))) + ->andWhere($query->expr()->eq('item_type', $query->expr()->literal('calendar'))) + ->andWhere($query->expr()->in('share_type', [ $query->expr()->literal(0), $query->expr()->literal(1)])) + ->execute() + ->fetchAll(); + + return $shares; + } +} diff --git a/apps/dav/lib/migration/migratecalendars.php b/apps/dav/lib/migration/migratecalendars.php new file mode 100644 index 00000000000..33f8a105180 --- /dev/null +++ b/apps/dav/lib/migration/migratecalendars.php @@ -0,0 +1,94 @@ +<?php + +namespace OCA\Dav\Migration; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\Calendar; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class MigrateCalendars { + + /** @var CalendarAdapter */ + protected $adapter; + + /** @var CalDavBackend */ + private $backend; + + /** + * @param CalendarAdapter $adapter + * @param CalDavBackend $backend + */ + function __construct(CalendarAdapter $adapter, + CalDavBackend $backend + ) { + $this->adapter = $adapter; + $this->backend = $backend; + } + + /** + * @param string $user + */ + public function migrateForUser($user) { + + $this->adapter->foreachCalendar($user, function($calendar) use ($user) { + $principal = "principals/users/$user"; + $calendarByUri = $this->backend->getCalendarByUri($principal, $calendar['uri']); + if (!is_null($calendarByUri)) { + return; + } + + $newId = $this->backend->createCalendar($principal, $calendar['uri'], [ + '{DAV:}displayname' => $calendar['displayname'], + '{urn:ietf:params:xml:ns:caldav}calendar-description' => $calendar['displayname'], + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => $calendar['timezone'], + '{http://apple.com/ns/ical/}calendar-order' => $calendar['calendarorder'], + '{http://apple.com/ns/ical/}calendar-color' => $calendar['calendarcolor'], + ]); + + $this->migrateCalendar($calendar['id'], $newId); + $this->migrateShares($calendar['id'], $newId); + }); + } + + public function setup() { + $this->adapter->setup(); + } + + /** + * @param int $calendarId + * @param int $newCalendarId + */ + private function migrateCalendar($calendarId, $newCalendarId) { + $this->adapter->foreachCalendarObject($calendarId, function($calObject) use ($newCalendarId) { + $this->backend->createCalendarObject($newCalendarId, $calObject['uri'], $calObject['calendardata']); + }); + } + + /** + * @param int $calendarId + * @param int $newCalendarId + */ + private function migrateShares($calendarId, $newCalendarId) { + $shares =$this->adapter->getShares($calendarId); + if (empty($shares)) { + return; + } + + $add = array_map(function($s) { + $prefix = 'principal:principals/users/'; + if ($s['share_type'] === 1) { + $prefix = 'principal:principals/groups/'; + } + return [ + 'href' => $prefix . $s['share_with'] + ]; + }, $shares); + + $newCalendar = $this->backend->getCalendarById($newCalendarId); + $calendar = new Calendar($this->backend, $newCalendar); + $this->backend->updateShares($calendar, $add, []); + } +} diff --git a/apps/dav/tests/unit/connector/sabre/filesreportplugin.php b/apps/dav/tests/unit/connector/sabre/filesreportplugin.php index 853e52f5039..b528e2d2427 100644 --- a/apps/dav/tests/unit/connector/sabre/filesreportplugin.php +++ b/apps/dav/tests/unit/connector/sabre/filesreportplugin.php @@ -30,16 +30,16 @@ use OCP\IGroupManager; use OCP\SystemTag\ISystemTagManager; class FilesReportPlugin extends \Test\TestCase { - /** @var \Sabre\DAV\Server */ + /** @var \Sabre\DAV\Server|\PHPUnit_Framework_MockObject_MockObject */ private $server; - /** @var \Sabre\DAV\Tree */ + /** @var \Sabre\DAV\Tree|\PHPUnit_Framework_MockObject_MockObject */ private $tree; - /** @var ISystemTagObjectMapper */ + /** @var ISystemTagObjectMapper|\PHPUnit_Framework_MockObject_MockObject */ private $tagMapper; - /** @var ISystemTagManager */ + /** @var ISystemTagManager|\PHPUnit_Framework_MockObject_MockObject */ private $tagManager; /** @var \OCP\IUserSession */ @@ -48,13 +48,13 @@ class FilesReportPlugin extends \Test\TestCase { /** @var FilesReportPluginImplementation */ private $plugin; - /** @var View **/ + /** @var View|\PHPUnit_Framework_MockObject_MockObject **/ private $view; - /** @var IGroupManager **/ + /** @var IGroupManager|\PHPUnit_Framework_MockObject_MockObject **/ private $groupManager; - /** @var Folder **/ + /** @var Folder|\PHPUnit_Framework_MockObject_MockObject **/ private $userFolder; public function setUp() { @@ -254,6 +254,7 @@ class FilesReportPlugin extends \Test\TestCase { ->with('222') ->will($this->returnValue([$filesNode2])); + /** @var \OCA\DAV\Connector\Sabre\Directory|\PHPUnit_Framework_MockObject_MockObject $reportTargetNode */ $result = $this->plugin->findNodesByFileIds($reportTargetNode, ['111', '222']); $this->assertCount(2, $result); @@ -304,6 +305,7 @@ class FilesReportPlugin extends \Test\TestCase { ->with('222') ->will($this->returnValue([$filesNode2])); + /** @var \OCA\DAV\Connector\Sabre\Directory|\PHPUnit_Framework_MockObject_MockObject $reportTargetNode */ $result = $this->plugin->findNodesByFileIds($reportTargetNode, ['111', '222']); $this->assertCount(2, $result); @@ -361,10 +363,14 @@ class FilesReportPlugin extends \Test\TestCase { ->method('isAdmin') ->will($this->returnValue(true)); - $this->tagMapper->expects($this->once()) + $this->tagMapper->expects($this->exactly(1)) ->method('getObjectIdsForTags') - ->with('123') - ->will($this->returnValue(['111', '222'])); + ->withConsecutive( + ['123', 'files'] + ) + ->willReturnMap([ + ['123', 'files', ['111', '222']], + ]); $rules = [ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], @@ -378,14 +384,16 @@ class FilesReportPlugin extends \Test\TestCase { ->method('isAdmin') ->will($this->returnValue(true)); - $this->tagMapper->expects($this->at(0)) - ->method('getObjectIdsForTags') - ->with('123') - ->will($this->returnValue(['111', '222'])); - $this->tagMapper->expects($this->at(1)) + $this->tagMapper->expects($this->exactly(2)) ->method('getObjectIdsForTags') - ->with('456') - ->will($this->returnValue(['222', '333'])); + ->withConsecutive( + ['123', 'files'], + ['456', 'files'] + ) + ->willReturnMap([ + ['123', 'files', ['111', '222']], + ['456', 'files', ['222', '333']], + ]); $rules = [ ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], @@ -395,6 +403,81 @@ class FilesReportPlugin extends \Test\TestCase { $this->assertEquals(['222'], array_values($this->plugin->processFilterRules($rules))); } + public function testProcessFilterRulesAndConditionWithOneEmptyResult() { + $this->groupManager->expects($this->any()) + ->method('isAdmin') + ->will($this->returnValue(true)); + + $this->tagMapper->expects($this->exactly(2)) + ->method('getObjectIdsForTags') + ->withConsecutive( + ['123', 'files'], + ['456', 'files'] + ) + ->willReturnMap([ + ['123', 'files', ['111', '222']], + ['456', 'files', []], + ]); + + $rules = [ + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], + ]; + + $this->assertEquals([], array_values($this->plugin->processFilterRules($rules))); + } + + public function testProcessFilterRulesAndConditionWithFirstEmptyResult() { + $this->groupManager->expects($this->any()) + ->method('isAdmin') + ->will($this->returnValue(true)); + + $this->tagMapper->expects($this->exactly(1)) + ->method('getObjectIdsForTags') + ->withConsecutive( + ['123', 'files'], + ['456', 'files'] + ) + ->willReturnMap([ + ['123', 'files', []], + ['456', 'files', ['111', '222']], + ]); + + $rules = [ + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], + ]; + + $this->assertEquals([], array_values($this->plugin->processFilterRules($rules))); + } + + public function testProcessFilterRulesAndConditionWithEmptyMidResult() { + $this->groupManager->expects($this->any()) + ->method('isAdmin') + ->will($this->returnValue(true)); + + $this->tagMapper->expects($this->exactly(2)) + ->method('getObjectIdsForTags') + ->withConsecutive( + ['123', 'files'], + ['456', 'files'], + ['789', 'files'] + ) + ->willReturnMap([ + ['123', 'files', ['111', '222']], + ['456', 'files', ['333']], + ['789', 'files', ['111', '222']], + ]); + + $rules = [ + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '789'], + ]; + + $this->assertEquals([], array_values($this->plugin->processFilterRules($rules))); + } + public function testProcessFilterRulesInvisibleTagAsAdmin() { $this->groupManager->expects($this->any()) ->method('isAdmin') diff --git a/apps/dav/tests/unit/migration/calendar_schema.xml b/apps/dav/tests/unit/migration/calendar_schema.xml new file mode 100644 index 00000000000..6c88b596a3f --- /dev/null +++ b/apps/dav/tests/unit/migration/calendar_schema.xml @@ -0,0 +1,191 @@ +<?xml version="1.0" encoding="ISO-8859-1" ?> +<database> + + <name>*dbname*</name> + <create>true</create> + <overwrite>false</overwrite> + + <charset>utf8</charset> + + <table> + + <name>*dbprefix*clndr_objects</name> + + <declaration> + + <field> + <name>id</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <autoincrement>1</autoincrement> + <unsigned>true</unsigned> + <length>4</length> + </field> + + <field> + <name>calendarid</name> + <type>integer</type> + <default></default> + <notnull>true</notnull> + <unsigned>true</unsigned> + <length>4</length> + </field> + + <field> + <name>objecttype</name> + <type>text</type> + <default></default> + <notnull>true</notnull> + <length>40</length> + </field> + + <field> + <name>startdate</name> + <type>timestamp</type> + <default>1970-01-01 00:00:00</default> + <notnull>false</notnull> + </field> + + <field> + <name>enddate</name> + <type>timestamp</type> + <default>1970-01-01 00:00:00</default> + <notnull>false</notnull> + </field> + + <field> + <name>repeating</name> + <type>integer</type> + <default></default> + <notnull>false</notnull> + <length>4</length> + </field> + + <field> + <name>summary</name> + <type>text</type> + <default></default> + <notnull>false</notnull> + <length>255</length> + </field> + + <field> + <name>calendardata</name> + <type>clob</type> + <notnull>false</notnull> + </field> + + <field> + <name>uri</name> + <type>text</type> + <default></default> + <notnull>false</notnull> + <length>255</length> + </field> + + <field> + <name>lastmodified</name> + <type>integer</type> + <default></default> + <notnull>false</notnull> + <length>4</length> + </field> + + </declaration> + + </table> + + <table> + + <name>*dbprefix*clndr_calendars</name> + + <declaration> + + <field> + <name>id</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <autoincrement>1</autoincrement> + <unsigned>true</unsigned> + <length>4</length> + </field> + + <field> + <name>userid</name> + <type>text</type> + <default></default> + <notnull>false</notnull> + <length>255</length> + </field> + + <field> + <name>displayname</name> + <type>text</type> + <default></default> + <notnull>false</notnull> + <length>100</length> + </field> + + <field> + <name>uri</name> + <type>text</type> + <default></default> + <notnull>false</notnull> + <length>255</length> + </field> + + <field> + <name>active</name> + <type>integer</type> + <default>1</default> + <notnull>true</notnull> + <length>4</length> + </field> + + <field> + <name>ctag</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <unsigned>true</unsigned> + <length>4</length> + </field> + + <field> + <name>calendarorder</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <unsigned>true</unsigned> + <length>4</length> + </field> + + <field> + <name>calendarcolor</name> + <type>text</type> + <default></default> + <notnull>false</notnull> + <length>10</length> + </field> + + <field> + <name>timezone</name> + <type>clob</type> + <notnull>false</notnull> + </field> + + <field> + <name>components</name> + <type>text</type> + <default></default> + <notnull>false</notnull> + <length>100</length> + </field> + + </declaration> + + </table> + +</database> diff --git a/apps/dav/tests/unit/migration/calendaradaptertest.php b/apps/dav/tests/unit/migration/calendaradaptertest.php new file mode 100644 index 00000000000..f92774ef6ad --- /dev/null +++ b/apps/dav/tests/unit/migration/calendaradaptertest.php @@ -0,0 +1,131 @@ +<?php +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OCA\DAV\Tests\Unit\Migration; + +use DomainException; +use OCA\Dav\Migration\AddressBookAdapter; +use OCA\Dav\Migration\CalendarAdapter; +use OCP\IDBConnection; +use Test\TestCase; + +/** + * Class CalendarAdapterTest + * + * @group DB + * + * @package OCA\DAV\Tests\Unit\Migration + */ +class CalendarAdapterTest extends TestCase { + + /** @var IDBConnection */ + private $db; + /** @var CalendarAdapter */ + private $adapter; + /** @var array */ + private $cals = []; + /** @var array */ + private $calObjs = []; + + public function setUp() { + parent::setUp(); + $this->db = \OC::$server->getDatabaseConnection(); + + $manager = new \OC\DB\MDB2SchemaManager($this->db); + $manager->createDbFromStructure(__DIR__ . '/calendar_schema.xml'); + + $this->adapter = new CalendarAdapter($this->db); + } + + public function tearDown() { + $this->db->dropTable('clndr_calendars'); + $this->db->dropTable('clndr_objects'); + parent::tearDown(); + } + + /** + * @expectedException DomainException + */ + public function testOldTablesDoNotExist() { + $adapter = new AddressBookAdapter(\OC::$server->getDatabaseConnection(), 'crazy_table_that_does_no_exist'); + $adapter->setup(); + } + + public function test() { + + // insert test data + $builder = $this->db->getQueryBuilder(); + $builder->insert('clndr_calendars') + ->values([ + 'userid' => $builder->createNamedParameter('test-user-666'), + 'displayname' => $builder->createNamedParameter('Display Name'), + 'uri' => $builder->createNamedParameter('events'), + 'ctag' => $builder->createNamedParameter('112233'), + 'active' => $builder->createNamedParameter('1') + ]) + ->execute(); + $builder = $this->db->getQueryBuilder(); + $builder->insert('clndr_objects') + ->values([ + 'calendarid' => $builder->createNamedParameter(6666), + 'objecttype' => $builder->createNamedParameter('VEVENT'), + 'startdate' => $builder->createNamedParameter(new \DateTime(), 'datetime'), + 'enddate' => $builder->createNamedParameter(new \DateTime(), 'datetime'), + 'repeating' => $builder->createNamedParameter(0), + 'summary' => $builder->createNamedParameter('Something crazy will happen'), + 'uri' => $builder->createNamedParameter('event.ics'), + 'lastmodified' => $builder->createNamedParameter('112233'), + ]) + ->execute(); + $builder = $this->db->getQueryBuilder(); + $builder->insert('share') + ->values([ + 'share_type' => $builder->createNamedParameter(1), + 'share_with' => $builder->createNamedParameter('user01'), + 'uid_owner' => $builder->createNamedParameter('user02'), + 'item_type' => $builder->createNamedParameter('calendar'), + 'item_source' => $builder->createNamedParameter(6666), + 'item_target' => $builder->createNamedParameter('Contacts (user02)'), + ]) + ->execute(); + + // test the adapter + $this->adapter->foreachCalendar('test-user-666', function($row) { + $this->cals[] = $row; + }); + $this->assertArrayHasKey('id', $this->cals[0]); + $this->assertEquals('test-user-666', $this->cals[0]['userid']); + $this->assertEquals('Display Name', $this->cals[0]['displayname']); + $this->assertEquals('events', $this->cals[0]['uri']); + $this->assertEquals('112233', $this->cals[0]['ctag']); + + $this->adapter->foreachCalendarObject(6666, function($row) { + $this->calObjs[]= $row; + }); + $this->assertArrayHasKey('id', $this->calObjs[0]); + $this->assertEquals(6666, $this->calObjs[0]['calendarid']); + + // test getShares + $shares = $this->adapter->getShares(6666); + $this->assertEquals(1, count($shares)); + + } + +} diff --git a/apps/dav/tests/unit/migration/migratecalendartest.php b/apps/dav/tests/unit/migration/migratecalendartest.php new file mode 100644 index 00000000000..1058773ffff --- /dev/null +++ b/apps/dav/tests/unit/migration/migratecalendartest.php @@ -0,0 +1,76 @@ +<?php +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OCA\DAV\Tests\Unit\Migration; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\Dav\Migration\CalendarAdapter; +use Test\TestCase; + +class MigrateCalendarTest extends TestCase { + + public function testMigration() { + /** @var CalendarAdapter | \PHPUnit_Framework_MockObject_MockObject $adapter */ + $adapter = $this->mockAdapter(); + + /** @var CalDavBackend | \PHPUnit_Framework_MockObject_MockObject $cardDav */ + $cardDav = $this->getMockBuilder('\OCA\Dav\CalDAV\CalDAVBackend')->disableOriginalConstructor()->getMock(); + $cardDav->method('createCalendar')->willReturn(666); + $cardDav->expects($this->once())->method('createCalendar')->with('principals/users/test01', 'test_contacts'); + $cardDav->expects($this->once())->method('createCalendarObject')->with(666, '63f0dd6c-39d5-44be-9d34-34e7a7441fc2.ics', 'BEGIN:VCARD'); + + $m = new \OCA\Dav\Migration\MigrateCalendars($adapter, $cardDav); + $m->migrateForUser('test01'); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function mockAdapter($shares = []) { + $adapter = $this->getMockBuilder('\OCA\Dav\Migration\CalendarAdapter') + ->disableOriginalConstructor() + ->getMock(); + $adapter->method('foreachCalendar')->willReturnCallback(function ($user, \Closure $callBack) { + $callBack([ + // calendarorder | calendarcolor | timezone | components + 'id' => 0, + 'userid' => $user, + 'displayname' => 'Test Contacts', + 'uri' => 'test_contacts', + 'ctag' => 1234567890, + 'active' => 1, + 'calendarorder' => '0', + 'calendarcolor' => '#b3dc6c', + 'timezone' => null, + 'components' => 'VEVENT,VTODO,VJOURNAL' + ]); + }); + $adapter->method('foreachCalendarObject')->willReturnCallback(function ($addressBookId, \Closure $callBack) { + $callBack([ + 'userid' => $addressBookId, + 'uri' => '63f0dd6c-39d5-44be-9d34-34e7a7441fc2.ics', + 'calendardata' => 'BEGIN:VCARD' + ]); + }); + $adapter->method('getShares')->willReturn($shares); + return $adapter; + } + +} diff --git a/apps/federatedfilesharing/lib/federatedshareprovider.php b/apps/federatedfilesharing/lib/federatedshareprovider.php index 0825a0e69bc..0e6089bde07 100644 --- a/apps/federatedfilesharing/lib/federatedshareprovider.php +++ b/apps/federatedfilesharing/lib/federatedshareprovider.php @@ -220,6 +220,8 @@ class FederatedShareProvider implements IShareProvider { $qb->update('share') ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) ->set('permissions', $qb->createNamedParameter($share->getPermissions())) + ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) + ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) ->execute(); return $share; diff --git a/apps/files/appinfo/register_command.php b/apps/files/appinfo/register_command.php index 4aaf49df9e2..0ec2fe1a584 100644 --- a/apps/files/appinfo/register_command.php +++ b/apps/files/appinfo/register_command.php @@ -21,5 +21,11 @@ * */ -$application->add(new OCA\Files\Command\Scan(\OC::$server->getUserManager())); -$application->add(new OCA\Files\Command\DeleteOrphanedFiles(\OC::$server->getDatabaseConnection())); +$dbConnection = \OC::$server->getDatabaseConnection(); +$userManager = OC::$server->getUserManager(); +$shareManager = \OC::$server->getShareManager(); + +/** @var Symfony\Component\Console\Application $application */ +$application->add(new OCA\Files\Command\Scan($userManager)); +$application->add(new OCA\Files\Command\DeleteOrphanedFiles($dbConnection)); +$application->add(new OCA\Files\Command\TransferOwnership($userManager, $shareManager)); diff --git a/apps/files/command/transferownership.php b/apps/files/command/transferownership.php new file mode 100644 index 00000000000..4cc2f34c3a3 --- /dev/null +++ b/apps/files/command/transferownership.php @@ -0,0 +1,225 @@ +<?php +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Files\Command; + +use OC\Files\Filesystem; +use OC\Files\View; +use OCP\Files\FileInfo; +use OCP\Files\Folder; +use OCP\IUserManager; +use OCP\Share\IManager; +use OCP\Share\IShare; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class TransferOwnership extends Command { + + /** @var IUserManager $userManager */ + private $userManager; + + /** @var IManager */ + private $shareManager; + + /** @var FileInfo[] */ + private $allFiles = []; + + /** @var FileInfo[] */ + private $encryptedFiles = []; + + /** @var IShare[] */ + private $shares = []; + + /** @var string */ + private $sourceUser; + + /** @var string */ + private $destinationUser; + + /** @var string */ + private $finalTarget; + + public function __construct(IUserManager $userManager, IManager $shareManager) { + $this->userManager = $userManager; + $this->shareManager = $shareManager; + parent::__construct(); + } + + protected function configure() { + $this + ->setName('files:transfer-ownership') + ->setDescription('All files and folders are move to another user - shares are moved as well.') + ->addArgument( + 'source-user', + InputArgument::REQUIRED, + 'owner of files which shall be moved' + ) + ->addArgument( + 'destination-user', + InputArgument::REQUIRED, + 'user who will be the new owner of the files' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) { + $this->sourceUser = $input->getArgument('source-user'); + $this->destinationUser = $input->getArgument('destination-user'); + if (!$this->userManager->userExists($this->sourceUser)) { + $output->writeln("<error>Unknown source user $this->sourceUser</error>"); + return; + } + if (!$this->userManager->userExists($this->destinationUser)) { + $output->writeln("<error>Unknown destination user $this->destinationUser</error>"); + return; + } + + $date = date('c'); + $this->finalTarget = "$this->destinationUser/files/transferred from $this->sourceUser on $date"; + + // setup filesystem + Filesystem::initMountPoints($this->sourceUser); + Filesystem::initMountPoints($this->destinationUser); + + // analyse source folder + $this->analyse($output); + + // collect all the shares + $this->collectUsersShares($output); + + // transfer the files + $this->transfer($output); + + // restore the shares + $this->restoreShares($output); + } + + private function walkFiles(View $view, $path, \Closure $callBack) { + foreach ($view->getDirectoryContent($path) as $fileInfo) { + if (!$callBack($fileInfo)) { + return; + } + if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) { + $this->walkFiles($view, $fileInfo->getPath(), $callBack); + } + } + } + + /** + * @param OutputInterface $output + * @throws \Exception + */ + protected function analyse(OutputInterface $output) { + $view = new View(); + $output->writeln("Analysing files of $this->sourceUser ..."); + $progress = new ProgressBar($output); + $progress->start(); + $self = $this; + $this->walkFiles($view, "$this->sourceUser/files", + function (FileInfo $fileInfo) use ($progress, $self) { + if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) { + return true; + } + $progress->advance(); + $this->allFiles[] = $fileInfo; + if ($fileInfo->isEncrypted()) { + $this->encryptedFiles[] = $fileInfo; + } + return true; + }); + $progress->finish(); + $output->writeln(''); + + // no file is allowed to be encrypted + if (!empty($this->encryptedFiles)) { + $output->writeln("<error>Some files are encrypted - please decrypt them first</error>"); + foreach($this->encryptedFiles as $encryptedFile) { + /** @var FileInfo $encryptedFile */ + $output->writeln(" " . $encryptedFile->getPath()); + } + throw new \Exception('Execution terminated.'); + } + + } + + /** + * @param OutputInterface $output + */ + private function collectUsersShares(OutputInterface $output) { + $output->writeln("Collecting all share information for files and folder of $this->sourceUser ..."); + + $progress = new ProgressBar($output, count($this->shares)); + foreach([\OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_GROUP, \OCP\Share::SHARE_TYPE_LINK, \OCP\Share::SHARE_TYPE_REMOTE] as $shareType) { + $offset = 0; + while (true) { + $sharePage = $this->shareManager->getSharesBy($this->sourceUser, $shareType, null, true, 50, $offset); + $progress->advance(count($sharePage)); + if (empty($sharePage)) { + break; + } + $this->shares = array_merge($this->shares, $sharePage); + $offset += 50; + } + } + + $progress->finish(); + $output->writeln(''); + } + + /** + * @param OutputInterface $output + */ + protected function transfer(OutputInterface $output) { + $view = new View(); + $output->writeln("Transferring files to $this->finalTarget ..."); + $view->rename("$this->sourceUser/files", $this->finalTarget); + // because the files folder is moved away we need to recreate it + $view->mkdir("$this->sourceUser/files"); + } + + /** + * @param OutputInterface $output + */ + private function restoreShares(OutputInterface $output) { + $output->writeln("Restoring shares ..."); + $progress = new ProgressBar($output, count($this->shares)); + + foreach($this->shares as $share) { + if ($share->getSharedWith() === $this->destinationUser) { + $this->shareManager->deleteShare($share); + } else { + if ($share->getShareOwner() === $this->sourceUser) { + $share->setShareOwner($this->destinationUser); + } + if ($share->getSharedBy() === $this->sourceUser) { + $share->setSharedBy($this->destinationUser); + } + + $this->shareManager->updateShare($share); + } + $progress->advance(); + } + $progress->finish(); + $output->writeln(''); + } +} diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 35999b5d0ee..1a6f38d3d7c 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -815,6 +815,10 @@ if (mountType) { data.mountType = mountType; } + var path = $el.attr('data-path'); + if (path) { + data.path = path; + } return data; }, diff --git a/apps/files/tests/controller/ViewControllerTest.php b/apps/files/tests/controller/ViewControllerTest.php index b5df3cfc904..28e2f0c2c9a 100644 --- a/apps/files/tests/controller/ViewControllerTest.php +++ b/apps/files/tests/controller/ViewControllerTest.php @@ -168,6 +168,15 @@ class ViewControllerTest extends TestCase { 'icon' => '', ], 2 => [ + 'id' => 'systemtagsfilter', + 'appname' => 'systemtags', + 'script' => 'list.php', + 'order' => 9, + 'name' => new \OC_L10N_String(new \OC_L10N('systemtags'), 'Tags', []), + 'active' => false, + 'icon' => '', + ], + 3 => [ 'id' => 'sharingin', 'appname' => 'files_sharing', 'script' => 'list.php', @@ -176,7 +185,7 @@ class ViewControllerTest extends TestCase { 'active' => false, 'icon' => '', ], - 3 => [ + 4 => [ 'id' => 'sharingout', 'appname' => 'files_sharing', 'script' => 'list.php', @@ -185,7 +194,7 @@ class ViewControllerTest extends TestCase { 'active' => false, 'icon' => '', ], - 4 => [ + 5 => [ 'id' => 'sharinglinks', 'appname' => 'files_sharing', 'script' => 'list.php', @@ -194,7 +203,7 @@ class ViewControllerTest extends TestCase { 'active' => false, 'icon' => '', ], - 5 => [ + 6 => [ 'id' => 'trashbin', 'appname' => 'files_trashbin', 'script' => 'list.php', @@ -227,18 +236,22 @@ class ViewControllerTest extends TestCase { 'content' => null, ], 2 => [ - 'id' => 'sharingin', + 'id' => 'systemtagsfilter', 'content' => null, ], 3 => [ - 'id' => 'sharingout', + 'id' => 'sharingin', 'content' => null, ], 4 => [ - 'id' => 'sharinglinks', + 'id' => 'sharingout', 'content' => null, ], 5 => [ + 'id' => 'sharinglinks', + 'content' => null, + ], + 6 => [ 'id' => 'trashbin', 'content' => null, ], diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index 1b2dd12213b..0091a9ee6e4 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -2521,6 +2521,12 @@ describe('OCA.Files.FileList tests', function() { expect(fileInfo.size).toEqual(12); expect(fileInfo.mimetype).toEqual('text/plain'); expect(fileInfo.type).toEqual('file'); + expect(fileInfo.path).not.toBeDefined(); + }); + it('adds path attribute if available', function() { + $tr.attr('data-path', '/subdir'); + var fileInfo = fileList.elementToFile($tr); + expect(fileInfo.path).toEqual('/subdir'); }); }); describe('new file menu', function() { diff --git a/apps/files_external/command/import.php b/apps/files_external/command/import.php index fe27051359c..28032c207b5 100644 --- a/apps/files_external/command/import.php +++ b/apps/files_external/command/import.php @@ -194,8 +194,8 @@ class Import extends Base { $mount = new StorageConfig($data['mount_id']); $mount->setMountPoint($data['mount_point']); $mount->setBackend($this->getBackendByClass($data['storage'])); - $authBackends = $this->backendService->getAuthMechanismsByScheme([$data['authentication_type']]); - $mount->setAuthMechanism(current($authBackends)); + $authBackend = $this->backendService->getAuthMechanism($data['authentication_type']); + $mount->setAuthMechanism($authBackend); $mount->setBackendOptions($data['configuration']); $mount->setMountOptions($data['options']); $mount->setApplicableUsers(isset($data['applicable_users']) ? $data['applicable_users'] : []); diff --git a/apps/files_external/command/listcommand.php b/apps/files_external/command/listcommand.php index c978ae5cfcb..5a0794be4c5 100644 --- a/apps/files_external/command/listcommand.php +++ b/apps/files_external/command/listcommand.php @@ -146,7 +146,7 @@ class ListCommand extends Base { $config->getId(), $config->getMountPoint(), $config->getBackend()->getStorageClass(), - $config->getAuthMechanism()->getScheme(), + $config->getAuthMechanism()->getIdentifier(), $config->getBackendOptions(), $config->getMountOptions() ]; diff --git a/apps/files_external/lib/auth/password/globalauth.php b/apps/files_external/lib/auth/password/globalauth.php index b1e52fb53ab..c6faee06109 100644 --- a/apps/files_external/lib/auth/password/globalauth.php +++ b/apps/files_external/lib/auth/password/globalauth.php @@ -21,8 +21,6 @@ namespace OCA\Files_External\Lib\Auth\Password; -use OCA\Files_External\Lib\Auth\IUserProvided; -use OCA\Files_External\Lib\DefinitionParameter; use OCA\Files_External\Service\BackendService; use OCP\IL10N; use OCP\IUser; @@ -74,6 +72,8 @@ class GlobalAuth extends AuthMechanism { public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) { if ($storage->getType() === StorageConfig::MOUNT_TYPE_ADMIN) { $uid = ''; + } elseif (is_null($user)) { + throw new InsufficientDataForMeaningfulAnswerException('No credentials saved'); } else { $uid = $user->getUID(); } diff --git a/apps/files_external/tests/auth/password/globalauth.php b/apps/files_external/tests/auth/password/globalauth.php new file mode 100644 index 00000000000..912bfd1574d --- /dev/null +++ b/apps/files_external/tests/auth/password/globalauth.php @@ -0,0 +1,117 @@ +<?php +/** + * @author Robin Appelman <icewind@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Files_External\Tests\Auth\Password; + +use OCA\Files_External\Lib\Auth\Password\GlobalAuth; +use OCA\Files_external\Lib\StorageConfig; +use Test\TestCase; + +class GlobalAuthTest extends TestCase { + /** + * @var \OCP\IL10N|\PHPUnit_Framework_MockObject_MockObject + */ + private $l10n; + + /** + * @var \OCP\Security\ICredentialsManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $credentialsManager; + + /** + * @var GlobalAuth + */ + private $instance; + + protected function setUp() { + parent::setUp(); + $this->l10n = $this->getMock('\OCP\IL10N'); + $this->credentialsManager = $this->getMock('\OCP\Security\ICredentialsManager'); + $this->instance = new GlobalAuth($this->l10n, $this->credentialsManager); + } + + private function getStorageConfig($type, $config = []) { + /** @var \OCA\Files_External\Lib\StorageConfig|\PHPUnit_Framework_MockObject_MockObject $storageConfig */ + $storageConfig = $this->getMock('\OCA\Files_External\Lib\StorageConfig'); + $storageConfig->expects($this->any()) + ->method('getType') + ->will($this->returnValue($type)); + $storageConfig->expects($this->any()) + ->method('getBackendOptions') + ->will($this->returnCallback(function () use (&$config) { + return $config; + })); + $storageConfig->expects($this->any()) + ->method('getBackendOption') + ->will($this->returnCallback(function ($key) use (&$config) { + return $config[$key]; + })); + $storageConfig->expects($this->any()) + ->method('setBackendOption') + ->will($this->returnCallback(function ($key, $value) use (&$config) { + $config[$key] = $value; + })); + + return $storageConfig; + } + + public function testNoCredentials() { + $this->credentialsManager->expects($this->once()) + ->method('retrieve') + ->will($this->returnValue(null)); + + $storage = $this->getStorageConfig(StorageConfig::MOUNT_TYPE_ADMIN); + + $this->instance->manipulateStorageConfig($storage); + $this->assertEquals([], $storage->getBackendOptions()); + } + + public function testSavedCredentials() { + $this->credentialsManager->expects($this->once()) + ->method('retrieve') + ->will($this->returnValue([ + 'user' => 'a', + 'password' => 'b' + ])); + + $storage = $this->getStorageConfig(StorageConfig::MOUNT_TYPE_ADMIN); + + $this->instance->manipulateStorageConfig($storage); + $this->assertEquals([ + 'user' => 'a', + 'password' => 'b' + ], $storage->getBackendOptions()); + } + + /** + * @expectedException \OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException + */ + public function testNoCredentialsPersonal() { + $this->credentialsManager->expects($this->never()) + ->method('retrieve'); + + $storage = $this->getStorageConfig(StorageConfig::MOUNT_TYPE_PERSONAl); + + $this->instance->manipulateStorageConfig($storage); + $this->assertEquals([], $storage->getBackendOptions()); + } + +} diff --git a/apps/files_external/tests/command/listcommandtest.php b/apps/files_external/tests/command/listcommandtest.php new file mode 100644 index 00000000000..338ddb7593e --- /dev/null +++ b/apps/files_external/tests/command/listcommandtest.php @@ -0,0 +1,69 @@ +<?php +/** + * @author Robin Appelman <icewind@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Files_External\Tests\Command; + +use OCA\Files_External\Command\ListCommand; +use OCA\Files_External\Lib\Auth\NullMechanism; +use OCA\Files_External\Lib\Auth\Password\Password; +use OCA\Files_External\Lib\Auth\Password\UserProvided; +use OCA\Files_External\Lib\Backend\Local; +use OCA\Files_external\Lib\StorageConfig; +use Symfony\Component\Console\Output\BufferedOutput; + +class ListCommandTest extends CommandTest { + /** + * @return \OCA\Files_External\Command\ListCommand|\PHPUnit_Framework_MockObject_MockObject + */ + private function getInstance() { + /** @var \OCA\Files_external\Service\GlobalStoragesService|\PHPUnit_Framework_MockObject_MockObject $globalService */ + $globalService = $this->getMock('\OCA\Files_external\Service\GlobalStoragesService', null, [], '', false); + /** @var \OCA\Files_external\Service\UserStoragesService|\PHPUnit_Framework_MockObject_MockObject $userService */ + $userService = $this->getMock('\OCA\Files_external\Service\UserStoragesService', null, [], '', false); + /** @var \OCP\IUserManager|\PHPUnit_Framework_MockObject_MockObject $userManager */ + $userManager = $this->getMock('\OCP\IUserManager'); + /** @var \OCP\IUserSession|\PHPUnit_Framework_MockObject_MockObject $userSession */ + $userSession = $this->getMock('\OCP\IUserSession'); + + return new ListCommand($globalService, $userService, $userSession, $userManager); + } + + public function testListAuthIdentifier() { + $l10n = $this->getMock('\OC_L10N', null, [], '', false); + $credentialsManager = $this->getMock('\OCP\Security\ICredentialsManager'); + $instance = $this->getInstance(); + $mount1 = new StorageConfig(); + $mount1->setAuthMechanism(new Password($l10n)); + $mount1->setBackend(new Local($l10n, new NullMechanism($l10n))); + $mount2 = new StorageConfig(); + $mount2->setAuthMechanism(new UserProvided($l10n, $credentialsManager)); + $mount2->setBackend(new Local($l10n, new NullMechanism($l10n))); + $input = $this->getInput($instance, [], [ + 'output' => 'json' + ]); + $output = new BufferedOutput(); + + $instance->listMounts('', [$mount1, $mount2], $input, $output); + $output = json_decode($output->fetch(), true); + + $this->assertNotEquals($output[0]['authentication_type'], $output[1]['authentication_type']); + } +} diff --git a/apps/files_sharing/lib/controllers/sharecontroller.php b/apps/files_sharing/lib/controllers/sharecontroller.php index bbe68096b52..dae61a3537b 100644 --- a/apps/files_sharing/lib/controllers/sharecontroller.php +++ b/apps/files_sharing/lib/controllers/sharecontroller.php @@ -30,7 +30,6 @@ namespace OCA\Files_Sharing\Controllers; use OC; -use OC\Files\Filesystem; use OC_Files; use OC_Util; use OCP; @@ -71,7 +70,7 @@ class ShareController extends Controller { protected $logger; /** @var OCP\Activity\IManager */ protected $activityManager; - /** @var OC\Share20\Manager */ + /** @var OCP\Share\IManager */ protected $shareManager; /** @var ISession */ protected $session; @@ -88,7 +87,7 @@ class ShareController extends Controller { * @param IUserManager $userManager * @param ILogger $logger * @param OCP\Activity\IManager $activityManager - * @param \OC\Share20\Manager $shareManager + * @param \OCP\Share\IManager $shareManager * @param ISession $session * @param IPreview $previewManager * @param IRootFolder $rootFolder @@ -100,7 +99,7 @@ class ShareController extends Controller { IUserManager $userManager, ILogger $logger, \OCP\Activity\IManager $activityManager, - \OC\Share20\Manager $shareManager, + \OCP\Share\IManager $shareManager, ISession $session, IPreview $previewManager, IRootFolder $rootFolder) { @@ -177,6 +176,7 @@ class ShareController extends Controller { if ($this->shareManager->checkPassword($share, $password)) { $this->session->set('public_link_authenticated', (string)$share->getId()); } else { + $this->emitAccessShareHook($share, 403, 'Wrong password'); return false; } } else { @@ -190,6 +190,44 @@ class ShareController extends Controller { } /** + * throws hooks when a share is attempted to be accessed + * + * @param \OCP\Share\IShare|string $share the Share instance if available, + * otherwise token + * @param int $errorCode + * @param string $errorMessage + * @throws OC\HintException + * @throws OC\ServerNotAvailableException + */ + protected function emitAccessShareHook($share, $errorCode = 200, $errorMessage = '') { + $itemType = $itemSource = $uidOwner = ''; + $token = $share; + $exception = null; + if($share instanceof \OCP\Share\IShare) { + try { + $token = $share->getToken(); + $uidOwner = $share->getSharedBy(); + $itemType = $share->getNodeType(); + $itemSource = $share->getNodeId(); + } catch (\Exception $e) { + // we log what we know and pass on the exception afterwards + $exception = $e; + } + } + \OC_Hook::emit('OCP\Share', 'share_link_access', [ + 'itemType' => $itemType, + 'itemSource' => $itemSource, + 'uidOwner' => $uidOwner, + 'token' => $token, + 'errorCode' => $errorCode, + 'errorMessage' => $errorMessage, + ]); + if(!is_null($exception)) { + throw $exception; + } + } + + /** * @PublicPage * @NoCSRFRequired * @@ -205,6 +243,7 @@ class ShareController extends Controller { try { $share = $this->shareManager->getShareByToken($token); } catch (ShareNotFound $e) { + $this->emitAccessShareHook($token, 404, 'Share not found'); return new NotFoundResponse(); } @@ -215,8 +254,14 @@ class ShareController extends Controller { } // We can't get the path of a file share - if ($share->getNode() instanceof \OCP\Files\File && $path !== '') { - throw new NotFoundException(); + try { + if ($share->getNode() instanceof \OCP\Files\File && $path !== '') { + $this->emitAccessShareHook($share, 404, 'Share not found'); + throw new NotFoundException(); + } + } catch (\Exception $e) { + $this->emitAccessShareHook($share, 404, 'Share not found'); + throw $e; } $rootFolder = null; @@ -227,6 +272,7 @@ class ShareController extends Controller { try { $path = $rootFolder->get($path); } catch (\OCP\Files\NotFoundException $e) { + $this->emitAccessShareHook($share, 404, 'Share not found'); throw new NotFoundException(); } } @@ -287,6 +333,8 @@ class ShareController extends Controller { $response = new TemplateResponse($this->appName, 'public', $shareTmpl, 'base'); $response->setContentSecurityPolicy($csp); + $this->emitAccessShareHook($share); + return $response; } @@ -344,6 +392,7 @@ class ShareController extends Controller { try { $node = $node->get($path); } catch (NotFoundException $e) { + $this->emitAccessShareHook($share, 404, 'Share not found'); return new NotFoundResponse(); } } @@ -409,6 +458,8 @@ class ShareController extends Controller { setcookie('ocDownloadStarted', $downloadStartSecret, time() + 20, '/'); } + $this->emitAccessShareHook($share); + // download selected files if (!is_null($files)) { // FIXME: The exit is required here because otherwise the AppFramework is trying to add headers as well diff --git a/apps/files_sharing/tests/controller/sharecontroller.php b/apps/files_sharing/tests/controller/sharecontroller.php index 22e15972cd6..11dc082390c 100644 --- a/apps/files_sharing/tests/controller/sharecontroller.php +++ b/apps/files_sharing/tests/controller/sharecontroller.php @@ -219,7 +219,11 @@ class ShareControllerTest extends \Test\TestCase { public function testAuthenticateInvalidPassword() { $share = \OC::$server->getShareManager()->newShare(); - $share->setId(42); + $share->setNodeId(100) + ->setNodeType('file') + ->setToken('token') + ->setSharedBy('initiator') + ->setId(42); $this->shareManager ->expects($this->once()) @@ -237,6 +241,20 @@ class ShareControllerTest extends \Test\TestCase { ->expects($this->never()) ->method('set'); + $hookListner = $this->getMockBuilder('Dummy')->setMethods(['access'])->getMock(); + \OCP\Util::connectHook('OCP\Share', 'share_link_access', $hookListner, 'access'); + + $hookListner->expects($this->once()) + ->method('access') + ->with($this->callback(function(array $data) { + return $data['itemType'] === 'file' && + $data['itemSource'] === 100 && + $data['uidOwner'] === 'initiator' && + $data['token'] === 'token' && + $data['errorCode'] === 403 && + $data['errorMessage'] === 'Wrong password'; + })); + $response = $this->shareController->authenticate('token', 'invalidpassword'); $expectedResponse = new TemplateResponse($this->appName, 'authenticate', array('wrongpw' => true), 'guest'); $this->assertEquals($expectedResponse, $response); diff --git a/apps/systemtags/appinfo/app.php b/apps/systemtags/appinfo/app.php index 0bb57e1227b..00663d5082b 100644 --- a/apps/systemtags/appinfo/app.php +++ b/apps/systemtags/appinfo/app.php @@ -39,9 +39,11 @@ $eventDispatcher->addListener( \OCP\Util::addScript('systemtags/systemtagscollection'); \OCP\Util::addScript('systemtags/systemtagsinputfield'); \OCP\Util::addScript('systemtags', 'app'); + \OCP\Util::addScript('systemtags', 'systemtagsfilelist'); \OCP\Util::addScript('systemtags', 'filesplugin'); \OCP\Util::addScript('systemtags', 'systemtagsinfoview'); \OCP\Util::addStyle('systemtags'); + \OCP\Util::addStyle('systemtags', 'systemtagsfilelist'); } ); @@ -73,3 +75,15 @@ $mapperListener = function(MapperEvent $event) use ($activityManager) { $eventDispatcher->addListener(MapperEvent::EVENT_ASSIGN, $mapperListener); $eventDispatcher->addListener(MapperEvent::EVENT_UNASSIGN, $mapperListener); + +$l = \OC::$server->getL10N('systemtags'); + +\OCA\Files\App::getNavigationManager()->add( + array( + 'id' => 'systemtagsfilter', + 'appname' => 'systemtags', + 'script' => 'list.php', + 'order' => 9, + 'name' => $l->t('Tags') + ) +); diff --git a/apps/systemtags/css/systemtagsfilelist.css b/apps/systemtags/css/systemtagsfilelist.css new file mode 100644 index 00000000000..e8fb665e26b --- /dev/null +++ b/apps/systemtags/css/systemtagsfilelist.css @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2016 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ +#app-content-systemtagsfilter .select2-container { + width: 30%; +} + +#app-content-systemtagsfilter .select2-choices { + white-space: nowrap; + text-overflow: ellipsis; + background: #fff; + color: #555; + box-sizing: content-box; + border-radius: 3px; + border: 1px solid #ddd; + margin: 3px 3px 3px 0; + padding: 0; + min-height: auto; +} + +.nav-icon-systemtagsfilter { + background-image: url('../img/tag.svg'); +} diff --git a/apps/systemtags/img/tag.png b/apps/systemtags/img/tag.png Binary files differnew file mode 100644 index 00000000000..5f4767a6f46 --- /dev/null +++ b/apps/systemtags/img/tag.png diff --git a/apps/systemtags/img/tag.svg b/apps/systemtags/img/tag.svg new file mode 100644 index 00000000000..6024607dd0a --- /dev/null +++ b/apps/systemtags/img/tag.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> + <rect style="color:#000000" fill-opacity="0" height="97.986" width="163.31" y="-32.993" x="-62.897"/> + <path opacity=".5" style="color:#000000" d="m6 1c-2.7614 0-5 2.2386-5 5s2.2386 5 5 5c0.98478 0 1.8823-0.28967 2.6562-0.78125l4.4688 4.625c0.09558 0.10527 0.22619 0.16452 0.375 0.15625 0.14882-0.0083 0.3031-0.07119 0.40625-0.1875l0.9375-1.0625c0.19194-0.22089 0.19549-0.53592 0-0.71875l-4.594-4.406c0.478-0.7663 0.75-1.6555 0.75-2.625 0-2.7614-2.2386-5-5-5zm0 2c1.6569 0 3 1.3431 3 3s-1.3431 3-3 3-3-1.3431-3-3 1.3431-3 3-3z"/> +</svg> diff --git a/apps/systemtags/js/app.js b/apps/systemtags/js/app.js index f55aa5c9a6e..d28514358c1 100644 --- a/apps/systemtags/js/app.js +++ b/apps/systemtags/js/app.js @@ -16,5 +16,92 @@ OCA.SystemTags = {}; } + OCA.SystemTags.App = { + + initFileList: function($el) { + if (this._fileList) { + return this._fileList; + } + + this._fileList = new OCA.SystemTags.FileList( + $el, + { + id: 'systemtags', + scrollContainer: $('#app-content'), + fileActions: this._createFileActions() + } + ); + + this._fileList.appName = t('systemtags', 'Tags'); + return this._fileList; + }, + + removeFileList: function() { + if (this._fileList) { + this._fileList.$fileList.empty(); + } + }, + + _createFileActions: function() { + // inherit file actions from the files app + var fileActions = new OCA.Files.FileActions(); + // note: not merging the legacy actions because legacy apps are not + // compatible with the sharing overview and need to be adapted first + fileActions.registerDefaultActions(); + fileActions.merge(OCA.Files.fileActions); + + if (!this._globalActionsInitialized) { + // in case actions are registered later + this._onActionsUpdated = _.bind(this._onActionsUpdated, this); + OCA.Files.fileActions.on('setDefault.app-systemtags', this._onActionsUpdated); + OCA.Files.fileActions.on('registerAction.app-systemtags', this._onActionsUpdated); + this._globalActionsInitialized = true; + } + + // when the user clicks on a folder, redirect to the corresponding + // folder in the files app instead of opening it directly + fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) { + OCA.Files.App.setActiveView('files', {silent: true}); + OCA.Files.App.fileList.changeDirectory(OC.joinPaths(context.$file.attr('data-path'), filename), true, true); + }); + fileActions.setDefault('dir', 'Open'); + return fileActions; + }, + + _onActionsUpdated: function(ev) { + if (!this._fileList) { + return; + } + + if (ev.action) { + this._fileList.fileActions.registerAction(ev.action); + } else if (ev.defaultAction) { + this._fileList.fileActions.setDefault( + ev.defaultAction.mime, + ev.defaultAction.name + ); + } + }, + + /** + * Destroy the app + */ + destroy: function() { + OCA.Files.fileActions.off('setDefault.app-systemtags', this._onActionsUpdated); + OCA.Files.fileActions.off('registerAction.app-systemtags', this._onActionsUpdated); + this.removeFileList(); + this._fileList = null; + delete this._globalActionsInitialized; + } + }; + })(); +$(document).ready(function() { + $('#app-content-systemtagsfilter').on('show', function(e) { + OCA.SystemTags.App.initFileList($(e.target)); + }); + $('#app-content-systemtagsfilter').on('hide', function() { + OCA.SystemTags.App.removeFileList(); + }); +}); diff --git a/apps/systemtags/js/filesplugin.js b/apps/systemtags/js/filesplugin.js index 471440c2e09..588037455ae 100644 --- a/apps/systemtags/js/filesplugin.js +++ b/apps/systemtags/js/filesplugin.js @@ -23,7 +23,8 @@ OCA.SystemTags.FilesPlugin = { allowedLists: [ 'files', - 'favorites' + 'favorites', + 'systemtagsfilter' ], attach: function(fileList) { diff --git a/apps/systemtags/js/systemtagsfilelist.js b/apps/systemtags/js/systemtagsfilelist.js new file mode 100644 index 00000000000..56838018a2c --- /dev/null +++ b/apps/systemtags/js/systemtagsfilelist.js @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2016 Vincent Petry <pvince81@owncloud.com> + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ +(function() { + /** + * @class OCA.SystemTags.FileList + * @augments OCA.Files.FileList + * + * @classdesc SystemTags file list. + * Contains a list of files filtered by system tags. + * + * @param $el container element with existing markup for the #controls + * and a table + * @param [options] map of options, see other parameters + * @param {Array.<string>} [options.systemTagIds] array of system tag ids to + * filter by + */ + var FileList = function($el, options) { + this.initialize($el, options); + }; + FileList.prototype = _.extend({}, OCA.Files.FileList.prototype, + /** @lends OCA.SystemTags.FileList.prototype */ { + id: 'systemtagsfilter', + appName: t('systemtags', 'Tagged files'), + + /** + * Array of system tag ids to filter by + * + * @type Array.<string> + */ + _systemTagIds: [], + + _clientSideSort: true, + _allowSelection: false, + + _filterField: null, + + /** + * @private + */ + initialize: function($el, options) { + OCA.Files.FileList.prototype.initialize.apply(this, arguments); + if (this.initialized) { + return; + } + + if (options && options.systemTagIds) { + this._systemTagIds = options.systemTagIds; + } + + OC.Plugins.attach('OCA.SystemTags.FileList', this); + + var $controls = this.$el.find('#controls').empty(); + + this._initFilterField($controls); + }, + + destroy: function() { + this.$filterField.remove(); + + OCA.Files.FileList.prototype.destroy.apply(this, arguments); + }, + + _initFilterField: function($container) { + this.$filterField = $('<input type="hidden" name="tags"/>'); + $container.append(this.$filterField); + this.$filterField.select2({ + placeholder: t('systemtags', 'Select tags to filter by'), + allowClear: false, + multiple: true, + separator: ',', + query: _.bind(this._queryTagsAutocomplete, this), + + id: function(tag) { + return tag.id; + }, + + initSelection: function(element, callback) { + var val = $(element).val().trim(); + if (val) { + var tagIds = val.split(','), + tags = []; + + OC.SystemTags.collection.fetch({ + success: function() { + _.each(tagIds, function(tagId) { + var tag = OC.SystemTags.collection.get(tagId); + if (!_.isUndefined(tag)) { + tags.push(tag.toJSON()); + } + }); + + callback(tags); + } + }); + } else { + callback([]); + } + }, + + formatResult: function (tag) { + return OC.SystemTags.getDescriptiveTag(tag); + }, + + formatSelection: function (tag) { + return OC.SystemTags.getDescriptiveTag(tag)[0].outerHTML; + }, + + escapeMarkup: function(m) { + // prevent double markup escape + return m; + } + }); + this.$filterField.on('change', _.bind(this._onTagsChanged, this)); + return this.$filterField; + }, + + /** + * Autocomplete function for dropdown results + * + * @param {Object} query select2 query object + */ + _queryTagsAutocomplete: function(query) { + OC.SystemTags.collection.fetch({ + success: function() { + var results = OC.SystemTags.collection.filterByName(query.term); + + query.callback({ + results: _.invoke(results, 'toJSON') + }); + } + }); + }, + + /** + * Event handler for when the URL changed + */ + _onUrlChanged: function(e) { + if (e.dir) { + var tags = _.filter(e.dir.split('/'), function(val) { return val.trim() !== ''; }); + this.$filterField.select2('val', tags || []); + this._systemTagIds = tags; + this.reload(); + } + }, + + _onTagsChanged: function(ev) { + var val = $(ev.target).val().trim(); + if (val !== '') { + this._systemTagIds = val.split(','); + } else { + this._systemTagIds = []; + } + + this.$el.trigger(jQuery.Event('changeDirectory', { + dir: this._systemTagIds.join('/') + })); + this.reload(); + }, + + updateEmptyContent: function() { + var dir = this.getCurrentDirectory(); + if (dir === '/') { + // root has special permissions + if (!this._systemTagIds.length) { + // no tags selected + this.$el.find('#emptycontent').html('<div class="icon-systemtags"></div>' + + '<h2>' + t('systemtags', 'Please select tags to filter by') + '</h2>'); + } else { + // tags selected but no results + this.$el.find('#emptycontent').html('<div class="icon-systemtags"></div>' + + '<h2>' + t('systemtags', 'No files found for the selected tags') + '</h2>'); + } + this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty); + this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty); + } + else { + OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments); + } + }, + + getDirectoryPermissions: function() { + return OC.PERMISSION_READ | OC.PERMISSION_DELETE; + }, + + updateStorageStatistics: function() { + // no op because it doesn't have + // storage info like free space / used space + }, + + reload: function() { + if (!this._systemTagIds.length) { + // don't reload + this.updateEmptyContent(); + this.setFiles([]); + return $.Deferred().resolve(); + } + + this._selectedFiles = {}; + this._selectionSummary.clear(); + if (this._currentFileModel) { + this._currentFileModel.off(); + } + this._currentFileModel = null; + this.$el.find('.select-all').prop('checked', false); + this.showMask(); + this._reloadCall = this.filesClient.getFilteredFiles( + { + systemTagIds: this._systemTagIds + }, + { + properties: this._getWebdavProperties() + } + ); + if (this._detailsView) { + // close sidebar + this._updateDetailsView(null); + } + var callBack = this.reloadCallback.bind(this); + return this._reloadCall.then(callBack, callBack); + }, + + reloadCallback: function(status, result) { + if (result) { + // prepend empty dir info because original handler + result.unshift({}); + } + + return OCA.Files.FileList.prototype.reloadCallback.call(this, status, result); + } + }); + + OCA.SystemTags.FileList = FileList; +})(); diff --git a/apps/systemtags/list.php b/apps/systemtags/list.php new file mode 100644 index 00000000000..dd4fe01e767 --- /dev/null +++ b/apps/systemtags/list.php @@ -0,0 +1,25 @@ +<?php +/** + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +// Check if we are a user +OCP\User::checkLoggedIn(); + +$tmpl = new OCP\Template('systemtags', 'list', ''); +$tmpl->printPage(); diff --git a/apps/systemtags/templates/list.php b/apps/systemtags/templates/list.php new file mode 100644 index 00000000000..841ce7b5b6d --- /dev/null +++ b/apps/systemtags/templates/list.php @@ -0,0 +1,38 @@ +<div id="controls"> +</div> + +<div id="emptycontent" class="hidden"> + <div class="icon-folder"></div> + <h2><?php p($l->t('No files in here')); ?></h2> + <p class="uploadmessage hidden"></p> +</div> + +<div class="nofilterresults emptycontent hidden"> + <div class="icon-search"></div> + <h2><?php p($l->t('No entries found in this folder')); ?></h2> + <p></p> +</div> + +<table id="filestable" data-preview-x="32" data-preview-y="32"> + <thead> + <tr> + <th id='headerName' class="hidden column-name"> + <div id="headerName-container"> + <a class="name sort columntitle" data-sort="name"><span><?php p($l->t( 'Name' )); ?></span><span class="sort-indicator"></span></a> + </div> + </th> + <th id="headerSize" class="hidden column-size"> + <a class="size sort columntitle" data-sort="size"><span><?php p($l->t('Size')); ?></span><span class="sort-indicator"></span></a> + </th> + <th id="headerDate" class="hidden column-mtime"> + <a id="modified" class="columntitle" data-sort="mtime"><span><?php p($l->t( 'Modified' )); ?></span><span class="sort-indicator"></span></a> + </th> + </tr> + </thead> + <tbody id="fileList"> + </tbody> + <tfoot> + </tfoot> +</table> +<input type="hidden" name="dir" id="dir" value="" /> + diff --git a/apps/systemtags/tests/js/systemtagsfilelistSpec.js b/apps/systemtags/tests/js/systemtagsfilelistSpec.js new file mode 100644 index 00000000000..ba41d347ca4 --- /dev/null +++ b/apps/systemtags/tests/js/systemtagsfilelistSpec.js @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2016 Vincent Petry <pvince81@owncloud.com> + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +describe('OCA.SystemTags.FileList tests', function() { + var FileInfo = OC.Files.FileInfo; + var fileList; + + beforeEach(function() { + // init parameters and test table elements + $('#testArea').append( + '<div id="app-content-container">' + + // init horrible parameters + '<input type="hidden" id="dir" value="/"></input>' + + '<input type="hidden" id="permissions" value="31"></input>' + + '<div id="controls"></div>' + + // dummy table + // TODO: at some point this will be rendered by the fileList class itself! + '<table id="filestable">' + + '<thead><tr>' + + '<th id="headerName" class="hidden column-name">' + + '<input type="checkbox" id="select_all_files" class="select-all">' + + '<a class="name columntitle" data-sort="name"><span>Name</span><span class="sort-indicator"></span></a>' + + '<span class="selectedActions hidden">' + + '</th>' + + '<th class="hidden column-mtime">' + + '<a class="columntitle" data-sort="mtime"><span class="sort-indicator"></span></a>' + + '</th>' + + '</tr></thead>' + + '<tbody id="fileList"></tbody>' + + '<tfoot></tfoot>' + + '</table>' + + '<div id="emptycontent">Empty content message</div>' + + '</div>' + ); + }); + afterEach(function() { + fileList.destroy(); + fileList = undefined; + }); + + describe('filter field', function() { + var select2Stub, oldCollection, fetchTagsStub; + var $tagsField; + + beforeEach(function() { + fetchTagsStub = sinon.stub(OC.SystemTags.SystemTagsCollection.prototype, 'fetch'); + select2Stub = sinon.stub($.fn, 'select2'); + oldCollection = OC.SystemTags.collection; + OC.SystemTags.collection = new OC.SystemTags.SystemTagsCollection([ + { + id: '123', + name: 'abc' + }, + { + id: '456', + name: 'def' + } + ]); + + fileList = new OCA.SystemTags.FileList( + $('#app-content-container'), { + systemTagIds: [] + } + ); + $tagsField = fileList.$el.find('[name=tags]'); + }); + afterEach(function() { + select2Stub.restore(); + fetchTagsStub.restore(); + OC.SystemTags.collection = oldCollection; + }); + it('inits select2 on filter field', function() { + expect(select2Stub.calledOnce).toEqual(true); + }); + it('uses global system tags collection', function() { + var callback = sinon.stub(); + var opts = select2Stub.firstCall.args[0]; + + $tagsField.val('123'); + + opts.initSelection($tagsField, callback); + + expect(callback.notCalled).toEqual(true); + expect(fetchTagsStub.calledOnce).toEqual(true); + + fetchTagsStub.yieldTo('success', fetchTagsStub.thisValues[0]); + + expect(callback.calledOnce).toEqual(true); + expect(callback.lastCall.args[0]).toEqual([ + OC.SystemTags.collection.get('123').toJSON() + ]); + }); + it('fetches tag list from the global collection', function() { + var callback = sinon.stub(); + var opts = select2Stub.firstCall.args[0]; + + $tagsField.val('123'); + + opts.query({ + term: 'de', + callback: callback + }); + + expect(fetchTagsStub.calledOnce).toEqual(true); + expect(callback.notCalled).toEqual(true); + fetchTagsStub.yieldTo('success', fetchTagsStub.thisValues[0]); + + expect(callback.calledOnce).toEqual(true); + expect(callback.lastCall.args[0]).toEqual({ + results: [ + OC.SystemTags.collection.get('456').toJSON() + ] + }); + }); + it('reloads file list after selection', function() { + var reloadStub = sinon.stub(fileList, 'reload'); + $tagsField.val('456,123').change(); + expect(reloadStub.calledOnce).toEqual(true); + reloadStub.restore(); + }); + it('updates URL after selection', function() { + var handler = sinon.stub(); + fileList.$el.on('changeDirectory', handler); + $tagsField.val('456,123').change(); + + expect(handler.calledOnce).toEqual(true); + expect(handler.lastCall.args[0].dir).toEqual('456/123'); + }); + it('updates tag selection when url changed', function() { + fileList.$el.trigger(new $.Event('urlChanged', {dir: '456/123'})); + + expect(select2Stub.lastCall.args[0]).toEqual('val'); + expect(select2Stub.lastCall.args[1]).toEqual(['456', '123']); + }); + }); + + describe('loading results', function() { + var getFilteredFilesSpec, requestDeferred; + + beforeEach(function() { + requestDeferred = new $.Deferred(); + getFilteredFilesSpec = sinon.stub(OC.Files.Client.prototype, 'getFilteredFiles') + .returns(requestDeferred.promise()); + }); + afterEach(function() { + getFilteredFilesSpec.restore(); + }); + + it('renders empty message when no tags were set', function() { + fileList = new OCA.SystemTags.FileList( + $('#app-content-container'), { + systemTagIds: [] + } + ); + + fileList.reload(); + + expect(fileList.$el.find('#emptycontent').hasClass('hidden')).toEqual(false); + + expect(getFilteredFilesSpec.notCalled).toEqual(true); + }); + + it('render files', function() { + fileList = new OCA.SystemTags.FileList( + $('#app-content-container'), { + systemTagIds: ['123', '456'] + } + ); + + fileList.reload(); + + expect(getFilteredFilesSpec.calledOnce).toEqual(true); + expect(getFilteredFilesSpec.lastCall.args[0].systemTagIds).toEqual(['123', '456']); + + var testFiles = [new FileInfo({ + id: 1, + type: 'file', + name: 'One.txt', + mimetype: 'text/plain', + mtime: 123456789, + size: 12, + etag: 'abc', + permissions: OC.PERMISSION_ALL + }), new FileInfo({ + id: 2, + type: 'file', + name: 'Two.jpg', + mimetype: 'image/jpeg', + mtime: 234567890, + size: 12049, + etag: 'def', + permissions: OC.PERMISSION_ALL + }), new FileInfo({ + id: 3, + type: 'file', + name: 'Three.pdf', + mimetype: 'application/pdf', + mtime: 234560000, + size: 58009, + etag: '123', + permissions: OC.PERMISSION_ALL + }), new FileInfo({ + id: 4, + type: 'dir', + name: 'somedir', + mimetype: 'httpd/unix-directory', + mtime: 134560000, + size: 250, + etag: '456', + permissions: OC.PERMISSION_ALL + })]; + + requestDeferred.resolve(207, testFiles); + + expect(fileList.$el.find('#emptycontent').hasClass('hidden')).toEqual(true); + expect(fileList.$el.find('tbody>tr').length).toEqual(4); + }); + }); +}); |