diff options
Diffstat (limited to 'apps')
112 files changed, 5393 insertions, 532 deletions
diff --git a/apps/dav/appinfo/database.xml b/apps/dav/appinfo/database.xml index f3fd5079949..48641c2be6f 100644 --- a/apps/dav/appinfo/database.xml +++ b/apps/dav/appinfo/database.xml @@ -183,4 +183,442 @@ CREATE TABLE addressbookchanges ( </declaration> </table> + +<!-- +CREATE TABLE calendarobjects ( + id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + calendardata MEDIUMBLOB, + uri VARBINARY(200), + calendarid INTEGER UNSIGNED NOT NULL, + lastmodified INT(11) UNSIGNED, + etag VARBINARY(32), + size INT(11) UNSIGNED NOT NULL, + componenttype VARBINARY(8), + firstoccurence INT(11) UNSIGNED, + lastoccurence INT(11) UNSIGNED, + uid VARBINARY(200), + UNIQUE(calendarid, uri) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +--> +<table> + <name>*dbprefix*calendarobjects</name> + <declaration> + <field> + <name>id</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <autoincrement>1</autoincrement> + <unsigned>true</unsigned> + <length>11</length> + </field> + <field> + <name>calendardata</name> + <type>blob</type> + </field> + <field> + <name>uri</name> + <type>text</type> + </field> + <field> + <name>calendarid</name> + <type>integer</type> + <unsigned>true</unsigned> + <notnull>true</notnull> + </field> + <field> + <name>lastmodified</name> + <type>integer</type> + <unsigned>true</unsigned> + </field> + <field> + <name>etag</name> + <type>text</type> + <length>32</length> + </field> + <field> + <name>size</name> + <type>integer</type> + <notnull>true</notnull> + <unsigned>true</unsigned> + <length>11</length> + </field> + <field> + <name>componenttype</name> + <type>text</type> + </field> + <field> + <name>firstoccurence</name> + <type>integer</type> + <unsigned>true</unsigned> + </field> + <field> + <name>lastoccurence</name> + <type>integer</type> + <unsigned>true</unsigned> + </field> + <field> + <name>uid</name> + <type>text</type> + </field> + <index> + <name>calobjects_index</name> + <unique>true</unique> + <field> + <name>calendarid</name> + </field> + <field> + <name>uri</name> + </field> + </index> + </declaration> +</table> + <!-- + CREATE TABLE calendars ( + id INTEGER UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + principaluri VARBINARY(100), + displayname VARCHAR(100), + uri VARBINARY(200), + synctoken INTEGER UNSIGNED NOT NULL DEFAULT '1', + description TEXT, + calendarorder INT(11) UNSIGNED NOT NULL DEFAULT '0', + calendarcolor VARBINARY(10), + timezone TEXT, + components VARBINARY(20), + transparent TINYINT(1) NOT NULL DEFAULT '0', + UNIQUE(principaluri, uri) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + --> +<table> + <name>*dbprefix*calendars</name> + <declaration> + <field> + <name>id</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <autoincrement>1</autoincrement> + <unsigned>true</unsigned> + <length>11</length> + </field> + <field> + <name>principaluri</name> + <type>text</type> + </field> + <field> + <name>displayname</name> + <type>text</type> + </field> + <field> + <name>uri</name> + <type>text</type> + </field> + <field> + <name>synctoken</name> + <type>integer</type> + <default>1</default> + <notnull>true</notnull> + <unsigned>true</unsigned> + </field> + <field> + <name>description</name> + <type>text</type> + </field> + <field> + <name>calendarorder</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <unsigned>true</unsigned> + </field> + <field> + <name>calendarcolor</name> + <type>text</type> + </field> + <field> + <name>timezone</name> + <type>text</type> + </field> + <field> + <name>components</name> + <type>text</type> + </field> + <field> + <name>transparent</name> + <type>integer</type> + <length>1</length> + <notnull>true</notnull> + <default>0</default> + </field> + <index> + <name>calendars_index</name> + <unique>true</unique> + <field> + <name>principaluri</name> + </field> + <field> + <name>uri</name> + </field> + </index> + </declaration> +</table> + <!-- + CREATE TABLE calendarchanges ( + id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + uri VARBINARY(200) NOT NULL, + synctoken INT(11) UNSIGNED NOT NULL, + calendarid INT(11) UNSIGNED NOT NULL, + operation TINYINT(1) NOT NULL, + INDEX calendarid_synctoken (calendarid, synctoken) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + --> + <table> + <name>*dbprefix*calendarchanges</name> + <declaration> + <field> + <name>id</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <autoincrement>1</autoincrement> + <unsigned>true</unsigned> + <length>11</length> + </field> + <field> + <name>uri</name> + <type>text</type> + </field> + <field> + <name>synctoken</name> + <type>integer</type> + <default>1</default> + <notnull>true</notnull> + <unsigned>true</unsigned> + </field> + <field> + <name>calendarid</name> + <type>integer</type> + <notnull>true</notnull> + </field> + <field> + <name>operation</name> + <type>integer</type> + <notnull>true</notnull> + <length>1</length> + </field> + + <index> + <name>calendarid_synctoken</name> + <field> + <name>calendarid</name> + </field> + <field> + <name>synctoken</name> + </field> + </index> + + </declaration> + </table> + + <!-- + CREATE TABLE calendarsubscriptions ( + id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + uri VARBINARY(200) NOT NULL, + principaluri VARBINARY(100) NOT NULL, + source TEXT, + displayname VARCHAR(100), + refreshrate VARCHAR(10), + calendarorder INT(11) UNSIGNED NOT NULL DEFAULT '0', + calendarcolor VARBINARY(10), + striptodos TINYINT(1) NULL, + stripalarms TINYINT(1) NULL, + stripattachments TINYINT(1) NULL, + lastmodified INT(11) UNSIGNED, + UNIQUE(principaluri, uri) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + --> +<table> + <name>*dbprefix*calendarsubscriptions</name> + <declaration> + <field> + <name>id</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <autoincrement>1</autoincrement> + <unsigned>true</unsigned> + <length>11</length> + </field> + <field> + <name>uri</name> + <type>text</type> + </field> + <field> + <name>principaluri</name> + <type>text</type> + </field> + <field> + <name>source</name> + <type>text</type> + </field> + <field> + <name>displayname</name> + <type>text</type> + <length>100</length> + </field> + <field> + <name>refreshrate</name> + <type>text</type> + <length>10</length> + </field> + <field> + <name>calendarorder</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <unsigned>true</unsigned> + </field> + <field> + <name>calendarcolor</name> + <type>text</type> + </field> + <field> + <name>striptodos</name> + <type>integer</type> + <length>1</length> + </field> + <field> + <name>stripalarms</name> + <type>integer</type> + <length>1</length> + </field> + <field> + <name>stripattachments</name> + <type>integer</type> + <length>1</length> + </field> + <field> + <name>lastmodified</name> + <type>integer</type> + <unsigned>true</unsigned> + </field> + <index> + <name>calsub_index</name> + <unique>true</unique> + <field> + <name>principaluri</name> + </field> + <field> + <name>uri</name> + </field> + </index> + </declaration> +</table> + <!-- + CREATE TABLE schedulingobjects ( + id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + principaluri VARBINARY(255), + calendardata MEDIUMBLOB, + uri VARBINARY(200), + lastmodified INT(11) UNSIGNED, + etag VARBINARY(32), + size INT(11) UNSIGNED NOT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + --> + + <table> + <name>*dbprefix*schedulingobjects</name> + <declaration> + <field> + <name>id</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <autoincrement>1</autoincrement> + <unsigned>true</unsigned> + <length>11</length> + </field> + <field> + <name>principaluri</name> + <type>text</type> + </field> + <field> + <name>calendardata</name> + <type>blob</type> + </field> + <field> + <name>uri</name> + <type>text</type> + </field> + <field> + <name>lastmodified</name> + <type>integer</type> + <unsigned>true</unsigned> + </field> + <field> + <name>etag</name> + <type>text</type> + <length>32</length> + </field> + <field> + <name>size</name> + <type>integer</type> + <notnull>true</notnull> + <unsigned>true</unsigned> + <length>11</length> + </field> + + </declaration> + </table> + + <table> + <name>*dbprefix*dav_shares</name> + <declaration> + <field> + <name>id</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <autoincrement>1</autoincrement> + <unsigned>true</unsigned> + <length>11</length> + </field> + <field> + <name>uri</name> + <type>text</type> + </field> + <field> + <name>principaluri</name> + <type>text</type> + </field> + <field> + <name>type</name> + <type>text</type> + </field> + <field> + <name>access</name> + <type>integer</type> + <length>1</length> + </field> + <field> + <name>resourceid</name> + <type>integer</type> + <notnull>true</notnull> + <unsigned>true</unsigned> + </field> + <index> + <name>dav_shares_index</name> + <unique>true</unique> + <field> + <name>principaluri</name> + </field> + <field> + <name>uri</name> + </field> + <field> + <name>type</name> + </field> + </index> + </declaration> + </table> </database> diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index 11025115691..5f681e784fc 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -5,7 +5,7 @@ <description>ownCloud WebDAV endpoint</description> <licence>AGPL</licence> <author>owncloud.org</author> - <version>0.1.2</version> + <version>0.1.3</version> <requiremin>9.0</requiremin> <shipped>true</shipped> <standalone/> diff --git a/apps/dav/appinfo/register_command.php b/apps/dav/appinfo/register_command.php index c996dd44063..7d57b944fb2 100644 --- a/apps/dav/appinfo/register_command.php +++ b/apps/dav/appinfo/register_command.php @@ -1,8 +1,10 @@ <?php use OCA\DAV\Command\CreateAddressBook; +use OCA\DAV\Command\CreateCalendar; $dbConnection = \OC::$server->getDatabaseConnection(); $userManager = OC::$server->getUserManager(); /** @var Symfony\Component\Console\Application $application */ $application->add(new CreateAddressBook($userManager, $dbConnection)); +$application->add(new CreateCalendar($userManager, $dbConnection)); diff --git a/apps/dav/appinfo/v1/publicwebdav.php b/apps/dav/appinfo/v1/publicwebdav.php index 5bdfd94e658..cf0488038d3 100644 --- a/apps/dav/appinfo/v1/publicwebdav.php +++ b/apps/dav/appinfo/v1/publicwebdav.php @@ -39,7 +39,8 @@ $serverFactory = new OCA\DAV\Connector\Sabre\ServerFactory( \OC::$server->getUserSession(), \OC::$server->getMountManager(), \OC::$server->getTagManager(), - \OC::$server->getEventDispatcher() + \OC::$server->getEventDispatcher(), + \OC::$server->getRequest() ); $requestUri = \OC::$server->getRequest()->getRequestUri(); diff --git a/apps/dav/appinfo/v1/webdav.php b/apps/dav/appinfo/v1/webdav.php index f28736f1f01..8324f962b8e 100644 --- a/apps/dav/appinfo/v1/webdav.php +++ b/apps/dav/appinfo/v1/webdav.php @@ -40,7 +40,8 @@ $serverFactory = new \OCA\DAV\Connector\Sabre\ServerFactory( \OC::$server->getUserSession(), \OC::$server->getMountManager(), \OC::$server->getTagManager(), - \OC::$server->getEventDispatcher() + \OC::$server->getEventDispatcher(), + \OC::$server->getRequest() ); // Backends diff --git a/apps/dav/command/createcalendar.php b/apps/dav/command/createcalendar.php new file mode 100644 index 00000000000..da4f248e51d --- /dev/null +++ b/apps/dav/command/createcalendar.php @@ -0,0 +1,52 @@ +<?php + +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCP\IDBConnection; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class CreateCalendar extends Command { + + /** @var IUserManager */ + protected $userManager; + + /** @var \OCP\IDBConnection */ + protected $dbConnection; + + /** + * @param IUserManager $userManager + * @param IDBConnection $dbConnection + */ + function __construct(IUserManager $userManager, IDBConnection $dbConnection) { + parent::__construct(); + $this->userManager = $userManager; + $this->dbConnection = $dbConnection; + } + + protected function configure() { + $this + ->setName('dav:create-calendar') + ->setDescription('Create a dav calendar') + ->addArgument('user', + InputArgument::REQUIRED, + 'User for whom the calendar will be created') + ->addArgument('name', + InputArgument::REQUIRED, + 'Name of the calendar'); + } + + protected function execute(InputInterface $input, OutputInterface $output) { + $user = $input->getArgument('user'); + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User <$user> in unknown."); + } + $name = $input->getArgument('name'); + $caldav = new CalDavBackend($this->dbConnection); + $caldav->createCalendar("principals/$user", $name, []); + } +} diff --git a/apps/dav/lib/caldav/caldavbackend.php b/apps/dav/lib/caldav/caldavbackend.php new file mode 100644 index 00000000000..99338650793 --- /dev/null +++ b/apps/dav/lib/caldav/caldavbackend.php @@ -0,0 +1,1174 @@ +<?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\DAV\CalDAV; + +use Sabre\CalDAV\Backend\AbstractBackend; +use Sabre\CalDAV\Backend\SchedulingSupport; +use Sabre\CalDAV\Backend\SubscriptionSupport; +use Sabre\CalDAV\Backend\SyncSupport; +use Sabre\CalDAV\Plugin; +use Sabre\CalDAV\Property\ScheduleCalendarTransp; +use Sabre\CalDAV\Property\SupportedCalendarComponentSet; +use Sabre\DAV; +use Sabre\DAV\Exception\Forbidden; +use Sabre\VObject\DateTimeParser; +use Sabre\VObject\Reader; +use Sabre\VObject\RecurrenceIterator; + +/** + * Class CalDavBackend + * + * Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php + * + * @package OCA\DAV\CalDAV + */ +class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport { + + /** + * We need to specify a max date, because we need to stop *somewhere* + * + * On 32 bit system the maximum for a signed integer is 2147483647, so + * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results + * in 2038-01-19 to avoid problems when the date is converted + * to a unix timestamp. + */ + const MAX_DATE = '2038-01-01'; + + /** + * List of CalDAV properties, and how they map to database fieldnames + * Add your own properties by simply adding on to this array. + * + * Note that only string-based properties are supported here. + * + * @var array + */ + public $propertyMap = [ + '{DAV:}displayname' => 'displayname', + '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description', + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone', + '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', + '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', + ]; + + /** + * List of subscription properties, and how they map to database fieldnames. + * + * @var array + */ + public $subscriptionPropertyMap = [ + '{DAV:}displayname' => 'displayname', + '{http://apple.com/ns/ical/}refreshrate' => 'refreshrate', + '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', + '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', + '{http://calendarserver.org/ns/}subscribed-strip-todos' => 'striptodos', + '{http://calendarserver.org/ns/}subscribed-strip-alarms' => 'stripalarms', + '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments', + ]; + + public function __construct(\OCP\IDBConnection $db) { + $this->db = $db; + } + + /** + * Returns a list of calendars for a principal. + * + * Every project is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * calendar. This can be the same as the uri or a database key. + * * uri, which the basename of the uri with which the calendar is + * accessed. + * * principaluri. The owner of the calendar. Almost always the same as + * principalUri passed to this method. + * + * Furthermore it can contain webdav properties in clark notation. A very + * common one is '{DAV:}displayname'. + * + * Many clients also require: + * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * For this property, you can just return an instance of + * Sabre\CalDAV\Property\SupportedCalendarComponentSet. + * + * If you return {http://sabredav.org/ns}read-only and set the value to 1, + * ACL will automatically be put in read-only mode. + * + * @param string $principalUri + * @return array + */ + function getCalendarsForUser($principalUri) { + $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('principaluri', $query->createNamedParameter($principalUri))) + ->orderBy('calendarorder', 'ASC'); + $stmt = $query->execute(); + + $calendars = []; + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + + $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]; + } + + $calendars[] = $calendar; + } + + return $calendars; + } + + /** + * Creates a new calendar for a principal. + * + * If the creation was a success, an id must be returned that can be used to reference + * this calendar in other methods, such as updateCalendar. + * + * @param string $principalUri + * @param string $calendarUri + * @param array $properties + * @return void + */ + function createCalendar($principalUri, $calendarUri, array $properties) { + $values = [ + 'principaluri' => $principalUri, + 'uri' => $calendarUri, + 'synctoken' => 1, + 'transparent' => 0, + 'components' => 'VEVENT,VTODO', + 'displayname' => $calendarUri + ]; + + // Default value + $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; + if (isset($properties[$sccs])) { + if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) { + throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet'); + } + $values['components'] = implode(',',$properties[$sccs]->getValue()); + } + $transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp'; + if (isset($properties[$transp])) { + $values['transparent'] = $properties[$transp]->getValue()==='transparent'; + } + + foreach($this->propertyMap as $xmlName=>$dbName) { + if (isset($properties[$xmlName])) { + $values[$dbName] = $properties[$xmlName]; + } + } + + $query = $this->db->getQueryBuilder(); + $query->insert('calendars'); + foreach($values as $column => $value) { + $query->setValue($column, $query->createNamedParameter($value)); + } + $query->execute(); + } + + /** + * Updates properties for a calendar. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch) { + $supportedProperties = array_keys($this->propertyMap); + $supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp'; + + $propPatch->handle($supportedProperties, function($mutations) use ($calendarId) { + $newValues = []; + foreach ($mutations as $propertyName => $propertyValue) { + + switch ($propertyName) { + case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' : + $fieldName = 'transparent'; + $newValues[$fieldName] = $propertyValue->getValue() === 'transparent'; + break; + default : + $fieldName = $this->propertyMap[$propertyName]; + $newValues[$fieldName] = $propertyValue; + break; + } + + } + $query = $this->db->getQueryBuilder(); + $query->update('calendars'); + foreach ($newValues as $fieldName => $value) { + $query->set($fieldName, $query->createNamedParameter($value)); + } + $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))); + $query->execute(); + + $this->addChange($calendarId, "", 2); + + return true; + }); + } + + /** + * Delete a calendar and all it's objects + * + * @param mixed $calendarId + * @return void + */ + function deleteCalendar($calendarId) { + $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ?'); + $stmt->execute([$calendarId]); + } + + /** + * Returns all calendar objects within a calendar. + * + * Every item contains an array with the following keys: + * * calendardata - The iCalendar-compatible calendar data + * * uri - a unique key which will be used to construct the uri. This can + * be any arbitrary string, but making sure it ends with '.ics' is a + * good idea. This is only the basename, or filename, not the full + * path. + * * lastmodified - a timestamp of the last modification time + * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: + * '"abcdef"') + * * size - The size of the calendar objects, in bytes. + * * component - optional, a string containing the type of object, such + * as 'vevent' or 'vtodo'. If specified, this will be used to populate + * the Content-Type header. + * + * Note that the etag is optional, but it's highly encouraged to return for + * speed reasons. + * + * The calendardata is also optional. If it's not returned + * 'getCalendarObject' will be called later, which *is* expected to return + * calendardata. + * + * If neither etag or size are specified, the calendardata will be + * used/fetched to determine these numbers. If both are specified the + * amount of times this is needed is reduced by a great degree. + * + * @param mixed $calendarId + * @return array + */ + function getCalendarObjects($calendarId) { + $query = $this->db->getQueryBuilder(); + $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype']) + ->from('calendarobjects') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))); + $stmt = $query->execute(); + + $result = []; + foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { + $result[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'calendarid' => $row['calendarid'], + 'size' => (int)$row['size'], + 'component' => strtolower($row['componenttype']), + ]; + } + + return $result; + } + + /** + * Returns information from a single calendar object, based on it's object + * uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * The returned array must have the same keys as getCalendarObjects. The + * 'calendardata' object is required here though, while it's not required + * for getCalendarObjects. + * + * This method must return null if the object did not exist. + * + * @param mixed $calendarId + * @param string $objectUri + * @return array|null + */ + function getCalendarObject($calendarId, $objectUri) { + + $query = $this->db->getQueryBuilder(); + $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype']) + ->from('calendarobjects') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))); + $stmt = $query->execute(); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if(!$row) return null; + + return [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'calendarid' => $row['calendarid'], + 'size' => (int)$row['size'], + 'calendardata' => $this->readBlob($row['calendardata']), + 'component' => strtolower($row['componenttype']), + ]; + } + + /** + * Returns a list of calendar objects. + * + * This method should work identical to getCalendarObject, but instead + * return all the calendar objects in the list as an array. + * + * If the backend supports this, it may allow for some speed-ups. + * + * @param mixed $calendarId + * @param string[] $uris + * @return array + */ + function getMultipleCalendarObjects($calendarId, array $uris) { + $query = $this->db->getQueryBuilder(); + $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype']) + ->from('calendarobjects') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) + ->andWhere($query->expr()->in('uri', $query->createParameter('uri'))) + ->setParameter('uri', $uris, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY); + + $stmt = $query->execute(); + + $result = []; + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + + $result[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'calendarid' => $row['calendarid'], + 'size' => (int)$row['size'], + 'calendardata' => $this->readBlob($row['calendardata']), + 'component' => strtolower($row['componenttype']), + ]; + + } + return $result; + } + + /** + * Creates a new calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * @return string + */ + function createCalendarObject($calendarId, $objectUri, $calendarData) { + $extraData = $this->getDenormalizedData($calendarData); + + $query = $this->db->getQueryBuilder(); + $query->insert('calendarobjects') + ->values([ + 'calendarid' => $query->createNamedParameter($calendarId), + 'uri' => $query->createNamedParameter($objectUri), + 'calendardata' => $query->createNamedParameter($calendarData, \PDO::PARAM_LOB), + 'lastmodified' => $query->createNamedParameter(time()), + 'etag' => $query->createNamedParameter($extraData['etag']), + 'size' => $query->createNamedParameter($extraData['size']), + 'componenttype' => $query->createNamedParameter($extraData['componentType']), + 'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']), + 'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']), + 'uid' => $query->createNamedParameter($extraData['uid']), + ]) + ->execute(); + + $this->addChange($calendarId, $objectUri, 1); + + return '"' . $extraData['etag'] . '"'; + } + + /** + * Updates an existing calendarobject, based on it's uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * @return string + */ + function updateCalendarObject($calendarId, $objectUri, $calendarData) { + $extraData = $this->getDenormalizedData($calendarData); + + $query = $this->db->getQueryBuilder(); + $query->update('calendarobjects') + ->set('calendardata', $query->createNamedParameter($calendarData, \PDO::PARAM_LOB)) + ->set('lastmodified', $query->createNamedParameter(time())) + ->set('etag', $query->createNamedParameter($extraData['etag'])) + ->set('size', $query->createNamedParameter($extraData['size'])) + ->set('componenttype', $query->createNamedParameter($extraData['componentType'])) + ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence'])) + ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence'])) + ->set('uid', $query->createNamedParameter($extraData['uid'])) + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) + ->execute(); + + $this->addChange($calendarId, $objectUri, 2); + + return '"' . $extraData['etag'] . '"'; + } + + /** + * Deletes an existing calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * @param mixed $calendarId + * @param string $objectUri + * @return void + */ + function deleteCalendarObject($calendarId, $objectUri) { + $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ?'); + $stmt->execute([$calendarId, $objectUri]); + + $this->addChange($calendarId, $objectUri, 3); + } + + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by Sabre\CalDAV\CalendarQueryParser. + * + * Note that it is extremely likely that getCalendarObject for every path + * returned from this method will be called almost immediately after. You + * may want to anticipate this to speed up these requests. + * + * This method provides a default implementation, which parses *all* the + * iCalendar objects in the specified calendar. + * + * This default may well be good enough for personal use, and calendars + * that aren't very large. But if you anticipate high usage, big calendars + * or high loads, you are strongly adviced to optimize certain paths. + * + * The best way to do so is override this method and to optimize + * specifically for 'common filters'. + * + * Requests that are extremely common are: + * * requests for just VEVENTS + * * requests for just VTODO + * * requests with a time-range-filter on either VEVENT or VTODO. + * + * ..and combinations of these requests. It may not be worth it to try to + * handle every possible situation and just rely on the (relatively + * easy to use) CalendarQueryValidator to handle the rest. + * + * Note that especially time-range-filters may be difficult to parse. A + * time-range filter specified on a VEVENT must for instance also handle + * recurrence rules correctly. + * A good example of how to interprete all these filters can also simply + * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct + * as possible, so it gives you a good idea on what type of stuff you need + * to think of. + * + * @param mixed $calendarId + * @param array $filters + * @return array + */ + function calendarQuery($calendarId, array $filters) { + $componentType = null; + $requirePostFilter = true; + $timeRange = null; + + // if no filters were specified, we don't need to filter after a query + if (!$filters['prop-filters'] && !$filters['comp-filters']) { + $requirePostFilter = false; + } + + // Figuring out if there's a component filter + if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) { + $componentType = $filters['comp-filters'][0]['name']; + + // Checking if we need post-filters + if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) { + $requirePostFilter = false; + } + // There was a time-range filter + if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) { + $timeRange = $filters['comp-filters'][0]['time-range']; + + // If start time OR the end time is not specified, we can do a + // 100% accurate mysql query. + if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) { + $requirePostFilter = false; + } + } + + } + $columns = ['uri']; + if ($requirePostFilter) { + $columns = ['uri', 'calendardata']; + } + $query = $this->db->getQueryBuilder(); + $query->select($columns) + ->from('calendarobjects') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))); + + if ($componentType) { + $query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType))); + } + + if ($timeRange && $timeRange['start']) { + $query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp()))); + } + if ($timeRange && $timeRange['end']) { + $query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp()))); + } + + $stmt = $query->execute(); + + $result = []; + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + if ($requirePostFilter) { + if (!$this->validateFilterForObject($row, $filters)) { + continue; + } + } + $result[] = $row['uri']; + } + + return $result; + } + + /** + * Searches through all of a users calendars and calendar objects to find + * an object with a specific UID. + * + * This method should return the path to this object, relative to the + * calendar home, so this path usually only contains two parts: + * + * calendarpath/objectpath.ics + * + * If the uid is not found, return null. + * + * This method should only consider * objects that the principal owns, so + * any calendars owned by other principals that also appear in this + * collection should be ignored. + * + * @param string $principalUri + * @param string $uid + * @return string|null + */ + function getCalendarObjectByUID($principalUri, $uid) { + + $query = $this->db->getQueryBuilder(); + $query->select([$query->createFunction('c.`uri` AS `calendaruri`'), $query->createFunction('co.`uri` AS `objecturi`')]) + ->from('calendarobjects', 'co') + ->leftJoin('co', 'calendars', 'c', 'co.`calendarid` = c.`id`') + ->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri))) + ->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid))); + + $stmt = $query->execute(); + + if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return $row['calendaruri'] . '/' . $row['objecturi']; + } + + return null; + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken in the specified calendar. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ); + * + * The returned syncToken property should reflect the *current* syncToken + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token + * property This is * needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $calendarId + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * @return array + */ + function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) { + // Current synctoken + $stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*calendars` WHERE `id` = ?'); + $stmt->execute([ $calendarId ]); + $currentToken = $stmt->fetchColumn(0); + + if (is_null($currentToken)) { + return null; + } + + $result = [ + 'syncToken' => $currentToken, + 'added' => [], + 'modified' => [], + 'deleted' => [], + ]; + + if ($syncToken) { + + $query = "SELECT `uri`, `operation` FROM `*PREFIX*calendarchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `calendarid` = ? ORDER BY `synctoken`"; + if ($limit>0) { + $query.= " `LIMIT` " . (int)$limit; + } + + // Fetching all changes + $stmt = $this->db->prepare($query); + $stmt->execute([$syncToken, $currentToken, $calendarId]); + + $changes = []; + + // This loop ensures that any duplicates are overwritten, only the + // last change on a node is relevant. + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + + $changes[$row['uri']] = $row['operation']; + + } + + foreach($changes as $uri => $operation) { + + switch($operation) { + case 1 : + $result['added'][] = $uri; + break; + case 2 : + $result['modified'][] = $uri; + break; + case 3 : + $result['deleted'][] = $uri; + break; + } + + } + } else { + // No synctoken supplied, this is the initial sync. + $query = "SELECT `uri` FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?"; + $stmt = $this->db->prepare($query); + $stmt->execute([$calendarId]); + + $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); + } + return $result; + + } + + /** + * Returns a list of subscriptions for a principal. + * + * Every subscription is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * subscription. This can be the same as the uri or a database key. + * * uri. This is just the 'base uri' or 'filename' of the subscription. + * * principaluri. The owner of the subscription. Almost always the same as + * principalUri passed to this method. + * + * Furthermore, all the subscription info must be returned too: + * + * 1. {DAV:}displayname + * 2. {http://apple.com/ns/ical/}refreshrate + * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos + * should not be stripped). + * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms + * should not be stripped). + * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if + * attachments should not be stripped). + * 6. {http://calendarserver.org/ns/}source (Must be a + * Sabre\DAV\Property\Href). + * 7. {http://apple.com/ns/ical/}calendar-color + * 8. {http://apple.com/ns/ical/}calendar-order + * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * (should just be an instance of + * Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of + * default components). + * + * @param string $principalUri + * @return array + */ + function getSubscriptionsForUser($principalUri) { + $fields = array_values($this->subscriptionPropertyMap); + $fields[] = 'id'; + $fields[] = 'uri'; + $fields[] = 'source'; + $fields[] = 'principaluri'; + $fields[] = 'lastmodified'; + + $query = $this->db->getQueryBuilder(); + $query->select($fields) + ->from('calendarsubscriptions') + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) + ->orderBy('calendarorder', 'asc'); + $stmt =$query->execute(); + + $subscriptions = []; + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + + $subscription = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + 'source' => $row['source'], + 'lastmodified' => $row['lastmodified'], + + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']), + ]; + + foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) { + if (!is_null($row[$dbName])) { + $subscription[$xmlName] = $row[$dbName]; + } + } + + $subscriptions[] = $subscription; + + } + + return $subscriptions; + } + + /** + * Creates a new subscription for a principal. + * + * If the creation was a success, an id must be returned that can be used to reference + * this subscription in other methods, such as updateSubscription. + * + * @param string $principalUri + * @param string $uri + * @param array $properties + * @return mixed + */ + function createSubscription($principalUri, $uri, array $properties) { + + if (!isset($properties['{http://calendarserver.org/ns/}source'])) { + throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions'); + } + + $values = [ + 'principaluri' => $principalUri, + 'uri' => $uri, + 'source' => $properties['{http://calendarserver.org/ns/}source']->getHref(), + 'lastmodified' => time(), + ]; + + foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) { + if (isset($properties[$xmlName])) { + + $values[$dbName] = $properties[$xmlName]; + $fieldNames[] = $dbName; + } + } + + $query = $this->db->getQueryBuilder(); + $query->insert('calendarsubscriptions') + ->values([ + 'principaluri' => $query->createNamedParameter($values['principaluri']), + 'uri' => $query->createNamedParameter($values['uri']), + 'source' => $query->createNamedParameter($values['source']), + 'lastmodified' => $query->createNamedParameter($values['lastmodified']), + ]) + ->execute(); + + return $this->db->lastInsertId('*PREFIX*calendarsubscriptions'); + } + + /** + * Updates a subscription + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param mixed $subscriptionId + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updateSubscription($subscriptionId, DAV\PropPatch $propPatch) { + $supportedProperties = array_keys($this->subscriptionPropertyMap); + $supportedProperties[] = '{http://calendarserver.org/ns/}source'; + + $propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) { + + $newValues = []; + + foreach($mutations as $propertyName=>$propertyValue) { + if ($propertyName === '{http://calendarserver.org/ns/}source') { + $newValues['source'] = $propertyValue->getHref(); + } else { + $fieldName = $this->subscriptionPropertyMap[$propertyName]; + $newValues[$fieldName] = $propertyValue; + } + } + + $query = $this->db->getQueryBuilder(); + $query->update('calendarsubscriptions') + ->set('lastmodified', $query->createNamedParameter(time())); + foreach($newValues as $fieldName=>$value) { + $query->set($fieldName, $query->createNamedParameter($value)); + } + $query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId))) + ->execute(); + + return true; + + }); + } + + /** + * Deletes a subscription. + * + * @param mixed $subscriptionId + * @return void + */ + function deleteSubscription($subscriptionId) { + $query = $this->db->getQueryBuilder(); + $query->delete('calendarsubscriptions') + ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId))) + ->execute(); + } + + /** + * Returns a single scheduling object for the inbox collection. + * + * The returned array should contain the following elements: + * * uri - A unique basename for the object. This will be used to + * construct a full uri. + * * calendardata - The iCalendar object + * * lastmodified - The last modification date. Can be an int for a unix + * timestamp, or a PHP DateTime object. + * * etag - A unique token that must change if the object changed. + * * size - The size of the object, in bytes. + * + * @param string $principalUri + * @param string $objectUri + * @return array + */ + function getSchedulingObject($principalUri, $objectUri) { + $query = $this->db->getQueryBuilder(); + $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size']) + ->from('schedulingobjects') + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) + ->execute(); + + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if(!$row) { + return null; + } + + return [ + 'uri' => $row['uri'], + 'calendardata' => $row['calendardata'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'size' => (int)$row['size'], + ]; + } + + /** + * Returns all scheduling objects for the inbox collection. + * + * These objects should be returned as an array. Every item in the array + * should follow the same structure as returned from getSchedulingObject. + * + * The main difference is that 'calendardata' is optional. + * + * @param string $principalUri + * @return array + */ + function getSchedulingObjects($principalUri) { + $query = $this->db->getQueryBuilder(); + $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size']) + ->from('schedulingobjects') + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) + ->execute(); + + $result = []; + foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { + $result[] = [ + 'calendardata' => $row['calendardata'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'size' => (int)$row['size'], + ]; + } + + return $result; + } + + /** + * Deletes a scheduling object from the inbox collection. + * + * @param string $principalUri + * @param string $objectUri + * @return void + */ + function deleteSchedulingObject($principalUri, $objectUri) { + $query = $this->db->getQueryBuilder(); + $query->delete('schedulingobjects') + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) + ->execute(); + } + + /** + * Creates a new scheduling object. This should land in a users' inbox. + * + * @param string $principalUri + * @param string $objectUri + * @param string $objectData + * @return void + */ + function createSchedulingObject($principalUri, $objectUri, $objectData) { + $query = $this->db->getQueryBuilder(); + $query->insert('schedulingobjects') + ->values([ + 'principaluri' => $query->createNamedParameter($principalUri), + 'calendardata' => $query->createNamedParameter($objectData), + 'uri' => $query->createNamedParameter($objectUri), + 'lastmodified' => $query->createNamedParameter(time()), + 'etag' => $query->createNamedParameter(md5($objectData)), + 'size' => $query->createNamedParameter(strlen($objectData)) + ]) + ->execute(); + } + + /** + * Adds a change record to the calendarchanges table. + * + * @param mixed $calendarId + * @param string $objectUri + * @param int $operation 1 = add, 2 = modify, 3 = delete. + * @return void + */ + protected function addChange($calendarId, $objectUri, $operation) { + + $stmt = $this->db->prepare('INSERT INTO `*PREFIX*calendarchanges` (`uri`, `synctoken`, `calendarid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*calendars` WHERE `id` = ?'); + $stmt->execute([ + $objectUri, + $calendarId, + $operation, + $calendarId + ]); + $stmt = $this->db->prepare('UPDATE `*PREFIX*calendars` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?'); + $stmt->execute([ + $calendarId + ]); + + } + + /** + * Parses some information from calendar objects, used for optimized + * calendar-queries. + * + * Returns an array with the following keys: + * * etag - An md5 checksum of the object without the quotes. + * * size - Size of the object in bytes + * * componentType - VEVENT, VTODO or VJOURNAL + * * firstOccurence + * * lastOccurence + * * uid - value of the UID property + * + * @param string $calendarData + * @return array + */ + protected function getDenormalizedData($calendarData) { + + $vObject = Reader::read($calendarData); + $componentType = null; + $component = null; + $firstOccurence = null; + $lastOccurence = null; + $uid = null; + foreach($vObject->getComponents() as $component) { + if ($component->name!=='VTIMEZONE') { + $componentType = $component->name; + $uid = (string)$component->UID; + break; + } + } + if (!$componentType) { + throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component'); + } + if ($componentType === 'VEVENT') { + $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp(); + // Finding the last occurence is a bit harder + if (!isset($component->RRULE)) { + if (isset($component->DTEND)) { + $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp(); + } elseif (isset($component->DURATION)) { + $endDate = clone $component->DTSTART->getDateTime(); + $endDate->add(DateTimeParser::parse($component->DURATION->getValue())); + $lastOccurence = $endDate->getTimeStamp(); + } elseif (!$component->DTSTART->hasTime()) { + $endDate = clone $component->DTSTART->getDateTime(); + $endDate->modify('+1 day'); + $lastOccurence = $endDate->getTimeStamp(); + } else { + $lastOccurence = $firstOccurence; + } + } else { + $it = new RecurrenceIterator($vObject, (string)$component->UID); + $maxDate = new \DateTime(self::MAX_DATE); + if ($it->isInfinite()) { + $lastOccurence = $maxDate->getTimeStamp(); + } else { + $end = $it->getDtEnd(); + while($it->valid() && $end < $maxDate) { + $end = $it->getDtEnd(); + $it->next(); + + } + $lastOccurence = $end->getTimeStamp(); + } + + } + } + + return [ + 'etag' => md5($calendarData), + 'size' => strlen($calendarData), + 'componentType' => $componentType, + 'firstOccurence' => $firstOccurence, + 'lastOccurence' => $lastOccurence, + 'uid' => $uid, + ]; + + } + + private function readBlob($cardData) { + if (is_resource($cardData)) { + return stream_get_contents($cardData); + } + + return $cardData; + } +} diff --git a/apps/dav/lib/carddav/addressbook.php b/apps/dav/lib/carddav/addressbook.php new file mode 100644 index 00000000000..e50f6f4adf6 --- /dev/null +++ b/apps/dav/lib/carddav/addressbook.php @@ -0,0 +1,58 @@ +<?php + +namespace OCA\DAV\CardDAV; + +use OCA\DAV\CardDAV\Sharing\IShareableAddressBook; +use OCP\IUserManager; + +class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareableAddressBook { + + /** @var IUserManager */ + private $userManager; + + public function __construct(CardDavBackend $carddavBackend, array $addressBookInfo) { + parent::__construct($carddavBackend, $addressBookInfo); + } + + /** + * Updates the list of shares. + * + * The first array is a list of people that are to be added to the + * addressbook. + * + * Every element in the add array has the following properties: + * * href - A url. Usually a mailto: address + * * commonName - Usually a first and last name, or false + * * summary - A description of the share, can also be false + * * readOnly - A boolean value + * + * Every element in the remove array is just the address string. + * + * @param array $add + * @param array $remove + * @return void + */ + function updateShares(array $add, array $remove) { + /** @var CardDavBackend $carddavBackend */ + $carddavBackend = $this->carddavBackend; + $carddavBackend->updateShares($this->getName(), $add, $remove); + } + + /** + * Returns the list of people whom this addressbook is shared with. + * + * Every element in this array should have the following properties: + * * href - Often a mailto: address + * * commonName - Optional, for example a first + last name + * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. + * * readOnly - boolean + * * summary - Optional, a description for the share + * + * @return array + */ + function getShares() { + /** @var CardDavBackend $carddavBackend */ + $carddavBackend = $this->carddavBackend; + $carddavBackend->getShares($this->getName()); + } +}
\ No newline at end of file diff --git a/apps/dav/lib/carddav/addressbookroot.php b/apps/dav/lib/carddav/addressbookroot.php new file mode 100644 index 00000000000..ee99ac8d798 --- /dev/null +++ b/apps/dav/lib/carddav/addressbookroot.php @@ -0,0 +1,23 @@ +<?php + +namespace OCA\DAV\CardDAV; + +class AddressBookRoot extends \Sabre\CardDAV\AddressBookRoot { + + /** + * This method returns a node for a principal. + * + * The passed array contains principal information, and is guaranteed to + * at least contain a uri item. Other properties may or may not be + * supplied by the authentication backend. + * + * @param array $principal + * @return \Sabre\DAV\INode + */ + function getChildForPrincipal(array $principal) { + + return new UserAddressBooks($this->carddavBackend, $principal['uri']); + + } + +}
\ No newline at end of file diff --git a/apps/dav/lib/carddav/carddavbackend.php b/apps/dav/lib/carddav/carddavbackend.php index b2597baedc6..daa31725fa1 100644 --- a/apps/dav/lib/carddav/carddavbackend.php +++ b/apps/dav/lib/carddav/carddavbackend.php @@ -22,6 +22,7 @@ namespace OCA\DAV\CardDAV; +use OCA\DAV\Connector\Sabre\Principal; use Sabre\CardDAV\Backend\BackendInterface; use Sabre\CardDAV\Backend\SyncSupport; use Sabre\CardDAV\Plugin; @@ -29,8 +30,12 @@ use Sabre\DAV\Exception\BadRequest; class CardDavBackend implements BackendInterface, SyncSupport { - public function __construct(\OCP\IDBConnection $db) { + /** @var Principal */ + private $principalBackend; + + public function __construct(\OCP\IDBConnection $db, Principal $principalBackend) { $this->db = $db; + $this->principalBackend = $principalBackend; } /** @@ -73,9 +78,61 @@ class CardDavBackend implements BackendInterface, SyncSupport { } $result->closeCursor(); + // query for shared calendars + $query = $this->db->getQueryBuilder(); + $query2 = $this->db->getQueryBuilder(); + $query2->select(['resourceid']) + ->from('dav_shares') + ->where($query2->expr()->eq('principaluri', $query2->createParameter('principaluri'))) + ->andWhere($query2->expr()->eq('type', $query2->createParameter('type'))); + $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) + ->from('addressbooks') + ->where($query->expr()->in('id', $query->createFunction($query2->getSQL()))) + ->setParameter('type', 'addressbook') + ->setParameter('principaluri', $principalUri) + ->execute(); + + while($row = $result->fetch()) { + $addressBooks[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + '{DAV:}displayname' => $row['displayname'], + '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], + '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + ]; + } + $result->closeCursor(); + return $addressBooks; } + private function getAddressBooksByUri($addressBookUri) { + $query = $this->db->getQueryBuilder(); + $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) + ->from('addressbooks') + ->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri))) + ->setMaxResults(1) + ->execute(); + + $row = $result->fetch(); + if (is_null($row)) { + return null; + } + $result->closeCursor(); + + return [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + '{DAV:}displayname' => $row['displayname'], + '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], + '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + ]; + } + /** * Updates properties for an address book. * @@ -86,7 +143,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { * Calling the handle method is like telling the PropPatch object "I * promise I can handle updating this property". * - * Read the PropPatch documenation for more info and examples. + * Read the PropPatch documentation for more info and examples. * * @param string $addressBookId * @param \Sabre\DAV\PropPatch $propPatch @@ -201,6 +258,11 @@ class CardDavBackend implements BackendInterface, SyncSupport { ->where($query->expr()->eq('id', $query->createParameter('id'))) ->setParameter('id', $addressBookId) ->execute(); + + $query->delete('dav_shares') + ->where($query->expr()->eq('resourceid', $query->createNamedParameter($addressBookId))) + ->andWhere($query->expr()->eq('type', $query->createNamedParameter('addressbook'))) + ->execute(); } /** @@ -281,7 +343,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { * If the backend supports this, it may allow for some speed-ups. * * @param mixed $addressBookId - * @param array $uris + * @param string[] $uris * @return array */ function getMultipleCards($addressBookId, array $uris) { @@ -328,7 +390,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @param mixed $addressBookId * @param string $cardUri * @param string $cardData - * @return string|null + * @return string */ function createCard($addressBookId, $cardUri, $cardData) { $etag = md5($cardData); @@ -373,7 +435,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @param mixed $addressBookId * @param string $cardUri * @param string $cardData - * @return string|null + * @return string */ function updateCard($addressBookId, $cardUri, $cardData) { @@ -561,4 +623,115 @@ class CardDavBackend implements BackendInterface, SyncSupport { return $cardData; } + /** + * @param string $path + * @param string[] $add + * @param string[] $remove + */ + public function updateShares($path, $add, $remove) { + foreach($add as $element) { + $this->shareWith($path, $element); + } + foreach($remove as $element) { + $this->unshare($path, $element); + } + } + + /** + * @param string $addressBookUri + * @param string $element + */ + private function shareWith($addressBookUri, $element) { + $user = $element['href']; + $parts = explode(':', $user, 2); + if ($parts[0] !== 'principal') { + return; + } + $p = $this->principalBackend->getPrincipalByPath($parts[1]); + if (is_null($p)) { + return; + } + + $addressBook = $this->getAddressBooksByUri($addressBookUri); + if (is_null($addressBook)) { + return; + } + + // remove the share if it already exists + $this->unshare($addressBookUri, $element); + + $query = $this->db->getQueryBuilder(); + $query->insert('dav_shares') + ->values([ + 'principaluri' => $query->createNamedParameter($parts[1]), + 'uri' => $query->createNamedParameter($addressBookUri), + 'type' => $query->createNamedParameter('addressbook'), + 'access' => $query->createNamedParameter(0), + 'resourceid' => $query->createNamedParameter($addressBook['id']) + ]); + $query->execute(); + } + + /** + * @param string $addressBookUri + * @param string $element + */ + private function unshare($addressBookUri, $element) { + $user = $element['href']; + $parts = explode(':', $user, 2); + if ($parts[0] !== 'principal') { + return; + } + $p = $this->principalBackend->getPrincipalByPath($parts[1]); + if (is_null($p)) { + return; + } + + $addressBook = $this->getAddressBooksByUri($addressBookUri); + if (is_null($addressBook)) { + return; + } + + $query = $this->db->getQueryBuilder(); + $query->delete('dav_shares') + ->where($query->expr()->eq('resourceid', $query->createNamedParameter($addressBook['id']))) + ->andWhere($query->expr()->eq('type', $query->createNamedParameter('addressbook'))) + ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($parts[1]))) + ; + $query->execute(); + } + + /** + * Returns the list of people whom this address book is shared with. + * + * Every element in this array should have the following properties: + * * href - Often a mailto: address + * * commonName - Optional, for example a first + last name + * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. + * * readOnly - boolean + * * summary - Optional, a description for the share + * + * @return array + */ + public function getShares($addressBookUri) { + $query = $this->db->getQueryBuilder(); + $result = $query->select(['principaluri', 'access']) + ->from('dav_shares') + ->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri))) + ->andWhere($query->expr()->eq('type', $query->createNamedParameter('addressbook'))) + ->execute(); + + $shares = []; + while($row = $result->fetch()) { + $p = $this->principalBackend->getPrincipalByPath($row['principaluri']); + $shares[]= [ + 'href' => "principal:${p['uri']}", + 'commonName' => isset($p['{DAV:}displayname']) ? $p['{DAV:}displayname'] : '', + 'status' => 1, + 'readOnly' => ($row['access'] === 1) + ]; + } + + return $shares; + } } diff --git a/apps/dav/lib/carddav/sharing/ishareableaddressbook.php b/apps/dav/lib/carddav/sharing/ishareableaddressbook.php new file mode 100644 index 00000000000..856a9ed18e6 --- /dev/null +++ b/apps/dav/lib/carddav/sharing/ishareableaddressbook.php @@ -0,0 +1,46 @@ +<?php + +namespace OCA\DAV\CardDAV\Sharing; +use Sabre\CardDAV\IAddressBook; + +/** + * This interface represents a Calendar that can be shared with other users. + * + */ +interface IShareableAddressBook extends IAddressBook { + + /** + * Updates the list of shares. + * + * The first array is a list of people that are to be added to the + * addressbook. + * + * Every element in the add array has the following properties: + * * href - A url. Usually a mailto: address + * * commonName - Usually a first and last name, or false + * * summary - A description of the share, can also be false + * * readOnly - A boolean value + * + * Every element in the remove array is just the address string. + * + * @param array $add + * @param array $remove + * @return void + */ + function updateShares(array $add, array $remove); + + /** + * Returns the list of people whom this addressbook is shared with. + * + * Every element in this array should have the following properties: + * * href - Often a mailto: address + * * commonName - Optional, for example a first + last name + * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. + * * readOnly - boolean + * * summary - Optional, a description for the share + * + * @return array + */ + function getShares(); + +}
\ No newline at end of file diff --git a/apps/dav/lib/carddav/sharing/plugin.php b/apps/dav/lib/carddav/sharing/plugin.php new file mode 100644 index 00000000000..eeb5abc6d23 --- /dev/null +++ b/apps/dav/lib/carddav/sharing/plugin.php @@ -0,0 +1,218 @@ +<?php + +namespace OCA\DAV\CardDAV\Sharing; + +use OCA\DAV\Connector\Sabre\Auth; +use OCP\IRequest; +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\DAV\XMLUtil; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +class Plugin extends ServerPlugin { + + public function __construct(Auth $authBackEnd, IRequest $request) { + $this->auth = $authBackEnd; + $this->request = $request; + } + + /** + * Reference to SabreDAV server object. + * + * @var \Sabre\DAV\Server + */ + protected $server; + + /** + * This method should return a list of server-features. + * + * This is for example 'versioning' and is added to the DAV: header + * in an OPTIONS response. + * + * @return array + */ + function getFeatures() { + + return ['oc-addressbook-sharing']; + + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using Sabre\DAV\Server::getPlugin + * + * @return string + */ + function getPluginName() { + + return 'carddav-sharing'; + + } + + /** + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param Server $server + * @return void + */ + function initialize(Server $server) { + $this->server = $server; + $server->resourceTypeMapping['OCA\\DAV\CardDAV\\ISharedAddressbook'] = '{' . \Sabre\CardDAV\Plugin::NS_CARDDAV . '}shared'; + + $this->server->on('method:POST', [$this, 'httpPost']); + } + + /** + * We intercept this to handle POST requests on calendars. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return null|bool + */ + function httpPost(RequestInterface $request, ResponseInterface $response) { + + $path = $request->getPath(); + + // Only handling xml + $contentType = $request->getHeader('Content-Type'); + if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false) + return; + + // Making sure the node exists + try { + $node = $this->server->tree->getNodeForPath($path); + } catch (NotFound $e) { + return; + } + + // CSRF protection + $this->protectAgainstCSRF(); + + $requestBody = $request->getBodyAsString(); + + // If this request handler could not deal with this POST request, it + // will return 'null' and other plugins get a chance to handle the + // request. + // + // However, we already requested the full body. This is a problem, + // because a body can only be read once. This is why we preemptively + // re-populated the request body with the existing data. + $request->setBody($requestBody); + + $dom = XMLUtil::loadDOMDocument($requestBody); + + $documentType = XMLUtil::toClarkNotation($dom->firstChild); + + switch ($documentType) { + + // Dealing with the 'share' document, which modified invitees on a + // calendar. + case '{' . \Sabre\CardDAV\Plugin::NS_CARDDAV . '}share' : + + // We can only deal with IShareableCalendar objects + if (!$node instanceof IShareableAddressBook) { + return; + } + + $this->server->transactionType = 'post-calendar-share'; + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($path, '{DAV:}write'); + } + + $mutations = $this->parseShareRequest($dom); + + $node->updateShares($mutations[0], $mutations[1]); + + $response->setStatus(200); + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $response->setHeader('X-Sabre-Status', 'everything-went-well'); + + // Breaking the event chain + return false; + } + } + + /** + * Parses the 'share' POST request. + * + * This method returns an array, containing two arrays. + * The first array is a list of new sharees. Every element is a struct + * containing a: + * * href element. (usually a mailto: address) + * * commonName element (often a first and lastname, but can also be + * false) + * * readOnly (true or false) + * * summary (A description of the share, can also be false) + * + * The second array is a list of sharees that are to be removed. This is + * just a simple array with 'hrefs'. + * + * @param \DOMDocument $dom + * @return array + */ + function parseShareRequest(\DOMDocument $dom) { + + $xpath = new \DOMXPath($dom); + $xpath->registerNamespace('cs', \Sabre\CardDAV\Plugin::NS_CARDDAV); + $xpath->registerNamespace('d', 'urn:DAV'); + + $set = []; + $elems = $xpath->query('cs:set'); + + for ($i = 0; $i < $elems->length; $i++) { + + $xset = $elems->item($i); + $set[] = [ + 'href' => $xpath->evaluate('string(d:href)', $xset), + 'commonName' => $xpath->evaluate('string(cs:common-name)', $xset), + 'summary' => $xpath->evaluate('string(cs:summary)', $xset), + 'readOnly' => $xpath->evaluate('boolean(cs:read)', $xset) !== false + ]; + + } + + $remove = []; + $elems = $xpath->query('cs:remove'); + + for ($i = 0; $i < $elems->length; $i++) { + + $xremove = $elems->item($i); + $remove[] = $xpath->evaluate('string(d:href)', $xremove); + + } + + return [$set, $remove]; + + } + + private function protectAgainstCSRF() { + $user = $this->auth->getCurrentUser(); + if ($this->auth->isDavAuthenticated($user)) { + return true; + } + + if ($this->request->passesCSRFCheck()) { + return true; + } + + throw new BadRequest(); + } + + +} diff --git a/apps/dav/lib/carddav/useraddressbooks.php b/apps/dav/lib/carddav/useraddressbooks.php new file mode 100644 index 00000000000..adbb0292fa7 --- /dev/null +++ b/apps/dav/lib/carddav/useraddressbooks.php @@ -0,0 +1,23 @@ +<?php + +namespace OCA\DAV\CardDAV; + +class UserAddressBooks extends \Sabre\CardDAV\UserAddressBooks { + + /** + * Returns a list of addressbooks + * + * @return array + */ + function getChildren() { + + $addressbooks = $this->carddavBackend->getAddressBooksForUser($this->principalUri); + $objs = []; + foreach($addressbooks as $addressbook) { + $objs[] = new AddressBook($this->carddavBackend, $addressbook); + } + return $objs; + + } + +} diff --git a/apps/dav/lib/connector/sabre/auth.php b/apps/dav/lib/connector/sabre/auth.php index 39a7df31b7f..f9a39799046 100644 --- a/apps/dav/lib/connector/sabre/auth.php +++ b/apps/dav/lib/connector/sabre/auth.php @@ -65,7 +65,7 @@ class Auth extends AbstractBasic { * @param string $username * @return bool */ - protected function isDavAuthenticated($username) { + public function isDavAuthenticated($username) { return !is_null($this->session->get(self::DAV_AUTHENTICATED)) && $this->session->get(self::DAV_AUTHENTICATED) === $username; } @@ -150,7 +150,7 @@ class Auth extends AbstractBasic { /** * @param \Sabre\DAV\Server $server - * @param $realm + * @param string $realm * @return bool */ private function auth(\Sabre\DAV\Server $server, $realm) { diff --git a/apps/dav/lib/connector/sabre/directory.php b/apps/dav/lib/connector/sabre/directory.php index 8c736ea0108..b602dd2f7b1 100644 --- a/apps/dav/lib/connector/sabre/directory.php +++ b/apps/dav/lib/connector/sabre/directory.php @@ -28,8 +28,10 @@ */ namespace OCA\DAV\Connector\Sabre; +use OCA\DAV\Connector\Sabre\Exception\Forbidden; use OCA\DAV\Connector\Sabre\Exception\InvalidPath; use OCA\DAV\Connector\Sabre\Exception\FileLocked; +use OCP\Files\ForbiddenException; use OCP\Lock\ILockingProvider; use OCP\Lock\LockedException; use Sabre\DAV\Exception\Locked; @@ -117,6 +119,8 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); } catch (\OCP\Files\InvalidPathException $ex) { throw new InvalidPath($ex->getMessage()); + } catch (ForbiddenException $ex) { + throw new Forbidden($ex->getMessage(), $ex->getRetry()); } catch (LockedException $e) { throw new FileLocked($e->getMessage(), $e->getCode(), $e); } @@ -146,6 +150,8 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); } catch (\OCP\Files\InvalidPathException $ex) { throw new InvalidPath($ex->getMessage()); + } catch (ForbiddenException $ex) { + throw new Forbidden($ex->getMessage(), $ex->getRetry()); } catch (LockedException $e) { throw new FileLocked($e->getMessage(), $e->getCode(), $e); } @@ -247,6 +253,8 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node // assume it wasn't possible to remove due to permission issue throw new \Sabre\DAV\Exception\Forbidden(); } + } catch (ForbiddenException $ex) { + throw new Forbidden($ex->getMessage(), $ex->getRetry()); } catch (LockedException $e) { throw new FileLocked($e->getMessage(), $e->getCode(), $e); } diff --git a/apps/dav/lib/connector/sabre/exception/forbidden.php b/apps/dav/lib/connector/sabre/exception/forbidden.php new file mode 100644 index 00000000000..673958349f3 --- /dev/null +++ b/apps/dav/lib/connector/sabre/exception/forbidden.php @@ -0,0 +1,64 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @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\DAV\Connector\Sabre\Exception; + +class Forbidden extends \Sabre\DAV\Exception\Forbidden { + + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + + /** + * @var bool + */ + private $retry; + + /** + * @param string $message + * @param bool $retry + * @param \Exception $previous + */ + public function __construct($message, $retry = false, \Exception $previous = null) { + parent::__construct($message, 0, $previous); + $this->retry = $retry; + } + + /** + * This method allows the exception to include additional information + * into the WebDAV error response + * + * @param \Sabre\DAV\Server $server + * @param \DOMElement $errorNode + * @return void + */ + public function serialize(\Sabre\DAV\Server $server,\DOMElement $errorNode) { + + // set ownCloud namespace + $errorNode->setAttribute('xmlns:o', self::NS_OWNCLOUD); + + // adding the retry node + $error = $errorNode->ownerDocument->createElementNS('o:','o:retry', var_export($this->retry, true)); + $errorNode->appendChild($error); + + // adding the message node + $error = $errorNode->ownerDocument->createElementNS('o:','o:reason', $this->getMessage()); + $errorNode->appendChild($error); + } +} diff --git a/apps/dav/lib/connector/sabre/fakelockerplugin.php b/apps/dav/lib/connector/sabre/fakelockerplugin.php new file mode 100644 index 00000000000..b9d1a30a041 --- /dev/null +++ b/apps/dav/lib/connector/sabre/fakelockerplugin.php @@ -0,0 +1,159 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * + * @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\DAV\Connector\Sabre; + +use Sabre\DAV\Locks\LockInfo; +use Sabre\DAV\Property\LockDiscovery; +use Sabre\DAV\Property\SupportedLock; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Sabre\DAV\PropFind; +use Sabre\DAV\INode; + +/** + * Class FakeLockerPlugin is a plugin only used when connections come in from + * OS X via Finder. The fake locking plugin does emulate Class 2 WebDAV support + * (locking of files) which allows Finder to access the storage in write mode as + * well. + * + * No real locking is performed, instead the plugin just returns always positive + * responses. + * + * @see https://github.com/owncloud/core/issues/17732 + * @package OCA\DAV\Connector\Sabre + */ +class FakeLockerPlugin extends ServerPlugin { + /** @var \Sabre\DAV\Server */ + private $server; + + /** {@inheritDoc} */ + public function initialize(\Sabre\DAV\Server $server) { + $this->server = $server; + $this->server->on('method:LOCK', [$this, 'fakeLockProvider'], 1); + $this->server->on('method:UNLOCK', [$this, 'fakeUnlockProvider'], 1); + $server->on('propFind', [$this, 'propFind']); + $server->on('validateTokens', [$this, 'validateTokens']); + } + + /** + * Indicate that we support LOCK and UNLOCK + * + * @param string $path + * @return string[] + */ + public function getHTTPMethods($path) { + return [ + 'LOCK', + 'UNLOCK', + ]; + } + + /** + * Indicate that we support locking + * + * @return integer[] + */ + function getFeatures() { + return [2]; + } + + /** + * Return some dummy response for PROPFIND requests with regard to locking + * + * @param PropFind $propFind + * @param INode $node + * @return void + */ + function propFind(PropFind $propFind, INode $node) { + $propFind->handle('{DAV:}supportedlock', function() { + return new SupportedLock(true); + }); + $propFind->handle('{DAV:}lockdiscovery', function() use ($propFind) { + return new LockDiscovery([]); + }); + } + + /** + * Mark a locking token always as valid + * + * @param RequestInterface $request + * @param array $conditions + */ + public function validateTokens(RequestInterface $request, &$conditions) { + foreach($conditions as &$fileCondition) { + if(isset($fileCondition['tokens'])) { + foreach($fileCondition['tokens'] as &$token) { + if(isset($token['token'])) { + if(substr($token['token'], 0, 16) === 'opaquelocktoken:') { + $token['validToken'] = true; + } + } + } + } + } + } + + /** + * Fakes a successful LOCK + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + public function fakeLockProvider(RequestInterface $request, + ResponseInterface $response) { + $dom = new \DOMDocument('1.0', 'utf-8'); + $prop = $dom->createElementNS('DAV:', 'd:prop'); + $dom->appendChild($prop); + + $lockDiscovery = $dom->createElementNS('DAV:', 'd:lockdiscovery'); + $prop->appendChild($lockDiscovery); + + $lockInfo = new LockInfo(); + $lockInfo->token = md5($request->getPath()); + $lockInfo->uri = $request->getPath(); + $lockInfo->depth = \Sabre\DAV\Server::DEPTH_INFINITY; + $lockInfo->timeout = 1800; + + $lockObj = new LockDiscovery([$lockInfo]); + $lockObj->serialize($this->server, $lockDiscovery); + + $response->setBody($dom->saveXML()); + + return false; + } + + /** + * Fakes a successful LOCK + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + public function fakeUnlockProvider(RequestInterface $request, + ResponseInterface $response) { + $response->setStatus(204); + $response->setHeader('Content-Length', '0'); + return false; + } +} diff --git a/apps/dav/lib/connector/sabre/file.php b/apps/dav/lib/connector/sabre/file.php index 961532daf50..ef7b9891dc9 100644 --- a/apps/dav/lib/connector/sabre/file.php +++ b/apps/dav/lib/connector/sabre/file.php @@ -35,9 +35,11 @@ namespace OCA\DAV\Connector\Sabre; use OC\Files\Filesystem; use OCA\DAV\Connector\Sabre\Exception\EntityTooLarge; use OCA\DAV\Connector\Sabre\Exception\FileLocked; +use OCA\DAV\Connector\Sabre\Exception\Forbidden as DAVForbiddenException; use OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType; use OCP\Encryption\Exceptions\GenericEncryptionException; use OCP\Files\EntityTooLargeException; +use OCP\Files\ForbiddenException; use OCP\Files\InvalidContentException; use OCP\Files\InvalidPathException; use OCP\Files\LockNotAcquiredException; @@ -175,6 +177,8 @@ class File extends Node implements IFile { \OCP\Util::writeLog('webdav', 'renaming part file to final file failed', \OCP\Util::ERROR); throw new Exception('Could not rename part file to final file'); } + } catch (ForbiddenException $ex) { + throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); } catch (\Exception $e) { $partStorage->unlink($internalPartPath); $this->convertToSabreException($e); @@ -209,6 +213,9 @@ class File extends Node implements IFile { return '"' . $this->info->getEtag() . '"'; } + /** + * @param string $path + */ private function emitPreHooks($exists, $path = null) { if (is_null($path)) { $path = $this->path; @@ -234,6 +241,9 @@ class File extends Node implements IFile { return $run; } + /** + * @param string $path + */ private function emitPostHooks($exists, $path = null) { if (is_null($path)) { $path = $this->path; @@ -256,7 +266,7 @@ class File extends Node implements IFile { /** * Returns the data * - * @return string|resource + * @return resource * @throws Forbidden * @throws ServiceUnavailable */ @@ -273,6 +283,8 @@ class File extends Node implements IFile { throw new ServiceUnavailable("Encryption not ready: " . $e->getMessage()); } catch (StorageNotAvailableException $e) { throw new ServiceUnavailable("Failed to open file: " . $e->getMessage()); + } catch (ForbiddenException $ex) { + throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); } catch (LockedException $e) { throw new FileLocked($e->getMessage(), $e->getCode(), $e); } @@ -296,6 +308,8 @@ class File extends Node implements IFile { } } catch (StorageNotAvailableException $e) { throw new ServiceUnavailable("Failed to unlink: " . $e->getMessage()); + } catch (ForbiddenException $ex) { + throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); } catch (LockedException $e) { throw new FileLocked($e->getMessage(), $e->getCode(), $e); } @@ -306,7 +320,7 @@ class File extends Node implements IFile { * * If null is returned, we'll assume application/octet-stream * - * @return mixed + * @return string */ public function getContentType() { $mimeType = $this->info->getMimetype(); @@ -474,6 +488,10 @@ class File extends Node implements IFile { // a more general case - due to whatever reason the content could not be written throw new Forbidden($e->getMessage(), 0, $e); } + if ($e instanceof ForbiddenException) { + // the path for the file was forbidden + throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e); + } if ($e instanceof EntityTooLargeException) { // the file is too big to be stored throw new EntityTooLarge($e->getMessage(), 0, $e); diff --git a/apps/dav/lib/connector/sabre/filesplugin.php b/apps/dav/lib/connector/sabre/filesplugin.php index 61b5360cac1..d68397dcaa3 100644 --- a/apps/dav/lib/connector/sabre/filesplugin.php +++ b/apps/dav/lib/connector/sabre/filesplugin.php @@ -37,11 +37,14 @@ class FilesPlugin extends \Sabre\DAV\ServerPlugin { // namespace const NS_OWNCLOUD = 'http://owncloud.org/ns'; const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id'; + const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid'; const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions'; const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL'; const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size'; const GETETAG_PROPERTYNAME = '{DAV:}getetag'; const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified'; + const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id'; + const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name'; /** * Reference to main server object @@ -96,9 +99,12 @@ class FilesPlugin extends \Sabre\DAV\ServerPlugin { $server->xmlNamespaces[self::NS_OWNCLOUD] = 'oc'; $server->protectedProperties[] = self::FILEID_PROPERTYNAME; + $server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME; $server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME; $server->protectedProperties[] = self::SIZE_PROPERTYNAME; $server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME; + $server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME; + $server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME; // normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH $allowedProperties = ['{DAV:}getetag']; @@ -171,6 +177,10 @@ class FilesPlugin extends \Sabre\DAV\ServerPlugin { return $node->getFileId(); }); + $propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function() use ($node) { + return $node->getInternalFileId(); + }); + $propFind->handle(self::PERMISSIONS_PROPERTYNAME, function() use ($node) { $perms = $node->getDavPermissions(); if ($this->isPublic) { @@ -201,6 +211,16 @@ class FilesPlugin extends \Sabre\DAV\ServerPlugin { return $node->getSize(); }); } + + $propFind->handle(self::OWNER_ID_PROPERTYNAME, function() use ($node) { + $owner = $node->getOwner(); + return $owner->getUID(); + }); + $propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function() use ($node) { + $owner = $node->getOwner(); + $displayName = $owner->getDisplayName(); + return $displayName; + }); } /** diff --git a/apps/dav/lib/connector/sabre/lockplugin.php b/apps/dav/lib/connector/sabre/lockplugin.php index 5840e59854c..d770b141eb9 100644 --- a/apps/dav/lib/connector/sabre/lockplugin.php +++ b/apps/dav/lib/connector/sabre/lockplugin.php @@ -27,7 +27,6 @@ use OCP\Lock\ILockingProvider; use OCP\Lock\LockedException; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\ServerPlugin; -use Sabre\DAV\Tree; use Sabre\HTTP\RequestInterface; class LockPlugin extends ServerPlugin { @@ -39,18 +38,6 @@ class LockPlugin extends ServerPlugin { private $server; /** - * @var \Sabre\DAV\Tree - */ - private $tree; - - /** - * @param \Sabre\DAV\Tree $tree tree - */ - public function __construct(Tree $tree) { - $this->tree = $tree; - } - - /** * {@inheritdoc} */ public function initialize(\Sabre\DAV\Server $server) { @@ -66,7 +53,7 @@ class LockPlugin extends ServerPlugin { return; } try { - $node = $this->tree->getNodeForPath($request->getPath()); + $node = $this->server->tree->getNodeForPath($request->getPath()); } catch (NotFound $e) { return; } @@ -84,7 +71,7 @@ class LockPlugin extends ServerPlugin { return; } try { - $node = $this->tree->getNodeForPath($request->getPath()); + $node = $this->server->tree->getNodeForPath($request->getPath()); } catch (NotFound $e) { return; } diff --git a/apps/dav/lib/connector/sabre/node.php b/apps/dav/lib/connector/sabre/node.php index 814aaceb077..c4e0614077f 100644 --- a/apps/dav/lib/connector/sabre/node.php +++ b/apps/dav/lib/connector/sabre/node.php @@ -178,7 +178,7 @@ abstract class Node implements \Sabre\DAV\INode { /** * Returns the size of the node, in bytes * - * @return int|float + * @return integer */ public function getSize() { return $this->info->getSize(); @@ -207,7 +207,14 @@ abstract class Node implements \Sabre\DAV\INode { } /** - * @return string|null + * @return integer + */ + public function getInternalFileId() { + return $this->info->getId(); + } + + /** + * @return string */ public function getDavPermissions() { $p = ''; @@ -238,6 +245,10 @@ abstract class Node implements \Sabre\DAV\INode { return $p; } + public function getOwner() { + return $this->info->getOwner(); + } + protected function verifyPath() { try { $fileName = basename($this->info->getPath()); diff --git a/apps/dav/lib/connector/sabre/objecttree.php b/apps/dav/lib/connector/sabre/objecttree.php index 80c0ef74610..2e9c1b9916c 100644 --- a/apps/dav/lib/connector/sabre/objecttree.php +++ b/apps/dav/lib/connector/sabre/objecttree.php @@ -25,10 +25,12 @@ namespace OCA\DAV\Connector\Sabre; +use OCA\DAV\Connector\Sabre\Exception\Forbidden; use OCA\DAV\Connector\Sabre\Exception\InvalidPath; use OCA\DAV\Connector\Sabre\Exception\FileLocked; use OC\Files\FileInfo; use OC\Files\Mount\MoveableMount; +use OCP\Files\ForbiddenException; use OCP\Files\StorageInvalidException; use OCP\Files\StorageNotAvailableException; use OCP\Lock\LockedException; @@ -235,6 +237,8 @@ class ObjectTree extends \Sabre\DAV\Tree { } } catch (StorageNotAvailableException $e) { throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); + } catch (ForbiddenException $ex) { + throw new Forbidden($ex->getMessage(), $ex->getRetry()); } catch (LockedException $e) { throw new FileLocked($e->getMessage(), $e->getCode(), $e); } @@ -274,6 +278,8 @@ class ObjectTree extends \Sabre\DAV\Tree { $this->fileView->copy($source, $destination); } catch (StorageNotAvailableException $e) { throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); + } catch (ForbiddenException $ex) { + throw new Forbidden($ex->getMessage(), $ex->getRetry()); } catch (LockedException $e) { throw new FileLocked($e->getMessage(), $e->getCode(), $e); } diff --git a/apps/dav/lib/connector/sabre/principal.php b/apps/dav/lib/connector/sabre/principal.php index 35215e1f63c..7fb14c031f9 100644 --- a/apps/dav/lib/connector/sabre/principal.php +++ b/apps/dav/lib/connector/sabre/principal.php @@ -168,7 +168,7 @@ class Principal implements \Sabre\DAVACL\PrincipalBackend\BackendInterface { * The principals should be passed as a list of uri's. * * @param string $principal - * @param array $members + * @param string[] $members * @throws \Sabre\DAV\Exception */ public function setGroupMemberSet($principal, array $members) { diff --git a/apps/dav/lib/connector/sabre/serverfactory.php b/apps/dav/lib/connector/sabre/serverfactory.php index f67e949e802..0f0377e96bd 100644 --- a/apps/dav/lib/connector/sabre/serverfactory.php +++ b/apps/dav/lib/connector/sabre/serverfactory.php @@ -26,12 +26,41 @@ use OCP\Files\Mount\IMountManager; use OCP\IConfig; use OCP\IDBConnection; use OCP\ILogger; +use OCP\IRequest; use OCP\ITagManager; use OCP\IUserSession; use Sabre\DAV\Auth\Backend\BackendInterface; +use Sabre\DAV\Locks\Plugin; use Symfony\Component\EventDispatcher\EventDispatcherInterface; class ServerFactory { + /** @var IConfig */ + private $config; + /** @var ILogger */ + private $logger; + /** @var IDBConnection */ + private $databaseConnection; + /** @var IUserSession */ + private $userSession; + /** @var IMountManager */ + private $mountManager; + /** @var ITagManager */ + private $tagManager; + /** @var EventDispatcherInterface */ + private $dispatcher; + /** @var IRequest */ + private $request; + + /** + * @param IConfig $config + * @param ILogger $logger + * @param IDBConnection $databaseConnection + * @param IUserSession $userSession + * @param IMountManager $mountManager + * @param ITagManager $tagManager + * @param EventDispatcherInterface $dispatcher + * @param IRequest $request + */ public function __construct( IConfig $config, ILogger $logger, @@ -39,7 +68,8 @@ class ServerFactory { IUserSession $userSession, IMountManager $mountManager, ITagManager $tagManager, - EventDispatcherInterface $dispatcher + EventDispatcherInterface $dispatcher, + IRequest $request ) { $this->config = $config; $this->logger = $logger; @@ -48,6 +78,7 @@ class ServerFactory { $this->mountManager = $mountManager; $this->tagManager = $tagManager; $this->dispatcher = $dispatcher; + $this->request = $request; } /** @@ -57,7 +88,10 @@ class ServerFactory { * @param callable $viewCallBack callback that should return the view for the dav endpoint * @return Server */ - public function createServer($baseUri, $requestUri, BackendInterface $authBackend, callable $viewCallBack) { + public function createServer($baseUri, + $requestUri, + BackendInterface $authBackend, + callable $viewCallBack) { // Fire up server $objectTree = new \OCA\DAV\Connector\Sabre\ObjectTree(); $server = new \OCA\DAV\Connector\Sabre\Server($objectTree); @@ -73,8 +107,13 @@ class ServerFactory { // FIXME: The following line is a workaround for legacy components relying on being able to send a GET to / $server->addPlugin(new \OCA\DAV\Connector\Sabre\DummyGetResponsePlugin()); $server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $this->logger)); - $server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin($objectTree)); + $server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin()); $server->addPlugin(new \OCA\DAV\Connector\Sabre\ListenerPlugin($this->dispatcher)); + // Finder on OS X requires Class 2 WebDAV support (locking), since we do + // not provide locking we emulate it using a fake locking plugin. + if($this->request->isUserAgent(['/WebDAVFS/'])) { + $server->addPlugin(new \OCA\DAV\Connector\Sabre\FakeLockerPlugin()); + } // wait with registering these until auth is handled and the filesystem is setup $server->on('beforeMethod', function () use ($server, $objectTree, $viewCallBack) { diff --git a/apps/dav/lib/rootcollection.php b/apps/dav/lib/rootcollection.php index 850180d8481..672e0a98684 100644 --- a/apps/dav/lib/rootcollection.php +++ b/apps/dav/lib/rootcollection.php @@ -2,19 +2,22 @@ namespace OCA\DAV; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CardDAV\AddressBookRoot; use OCA\DAV\CardDAV\CardDavBackend; use OCA\DAV\Connector\Sabre\Principal; +use Sabre\CalDAV\CalendarRoot; use Sabre\CalDAV\Principal\Collection; -use Sabre\CardDAV\AddressBookRoot; use Sabre\DAV\SimpleCollection; class RootCollection extends SimpleCollection { public function __construct() { $config = \OC::$server->getConfig(); + $db = \OC::$server->getDatabaseConnection(); $principalBackend = new Principal( - $config, - \OC::$server->getUserManager() + $config, + \OC::$server->getUserManager() ); // as soon as debug mode is enabled we allow listing of principals $disableListing = !$config->getSystemValue('debug', false); @@ -24,14 +27,20 @@ class RootCollection extends SimpleCollection { $principalCollection->disableListing = $disableListing; $filesCollection = new Files\RootCollection($principalBackend); $filesCollection->disableListing = $disableListing; - $cardDavBackend = new CardDavBackend(\OC::$server->getDatabaseConnection()); + $caldavBackend = new CalDavBackend($db); + $calendarRoot = new CalendarRoot($principalBackend, $caldavBackend); + $calendarRoot->disableListing = $disableListing; + + $cardDavBackend = new CardDavBackend(\OC::$server->getDatabaseConnection(), $principalBackend); + $addressBookRoot = new AddressBookRoot($principalBackend, $cardDavBackend); $addressBookRoot->disableListing = $disableListing; $children = [ - $principalCollection, - $filesCollection, - $addressBookRoot, + $principalCollection, + $filesCollection, + $calendarRoot, + $addressBookRoot, ]; parent::__construct('root', $children); diff --git a/apps/dav/lib/server.php b/apps/dav/lib/server.php index a92c9980f54..44afcf23df6 100644 --- a/apps/dav/lib/server.php +++ b/apps/dav/lib/server.php @@ -17,6 +17,9 @@ class Server { public function __construct(IRequest $request, $baseUri) { $this->request = $request; $this->baseUri = $baseUri; + $logger = \OC::$server->getLogger(); + $dispatcher = \OC::$server->getEventDispatcher(); + $root = new RootCollection(); $this->server = new \OCA\DAV\Connector\Sabre\Server($root); @@ -32,11 +35,32 @@ class Server { $this->server->addPlugin(new BlockLegacyClientPlugin(\OC::$server->getConfig())); $this->server->addPlugin(new Plugin($authBackend, 'ownCloud')); + $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\DummyGetResponsePlugin()); + $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $logger)); + $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin()); + $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\ListenerPlugin($dispatcher)); + // calendar plugins + $this->server->addPlugin(new \Sabre\CalDAV\Plugin()); $this->server->addPlugin(new \Sabre\DAVACL\Plugin()); + $this->server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin()); + $senderEmail = \OCP\Util::getDefaultEmailAddress('no-reply'); + $this->server->addPlugin(new \Sabre\CalDAV\Schedule\Plugin()); + $this->server->addPlugin(new \Sabre\CalDAV\Schedule\IMipPlugin($senderEmail)); + $this->server->addPlugin(new \Sabre\CalDAV\SharingPlugin()); + $this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin()); + $this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin()); + $this->server->addPlugin(new CardDAV\Sharing\Plugin($authBackend, \OC::$server->getRequest())); + // addressbook plugins $this->server->addPlugin(new \Sabre\CardDAV\Plugin()); + // Finder on OS X requires Class 2 WebDAV support (locking), since we do + // not provide locking we emulate it using a fake locking plugin. + if($request->isUserAgent(['/WebDAVFS/'])) { + $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\FakeLockerPlugin()); + } + // wait with registering these until auth is handled and the filesystem is setup $this->server->on('beforeMethod', function () { // custom properties plugin must be the last one diff --git a/apps/dav/tests/misc/sharing.xml b/apps/dav/tests/misc/sharing.xml new file mode 100644 index 00000000000..8771256ce79 --- /dev/null +++ b/apps/dav/tests/misc/sharing.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8" ?> + <CS:share xmlns:D="DAV:" xmlns:CS="urn:ietf:params:xml:ns:carddav"> + <CS:set> + <D:href>principal:principals/admin</D:href> + <CS:read-write /> + </CS:set> + </CS:share> diff --git a/apps/dav/tests/unit/caldav/caldavbackendtest.php b/apps/dav/tests/unit/caldav/caldavbackendtest.php new file mode 100644 index 00000000000..258c5627ad9 --- /dev/null +++ b/apps/dav/tests/unit/caldav/caldavbackendtest.php @@ -0,0 +1,348 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * + * @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 Tests\Connector\Sabre; + +use DateTime; +use DateTimeZone; +use OCA\DAV\CalDAV\CalDavBackend; +use Sabre\CalDAV\Property\SupportedCalendarComponentSet; +use Sabre\DAV\Property\Href; +use Sabre\DAV\PropPatch; +use Test\TestCase; + +/** + * Class CalDavBackendTest + * + * @group DB + * + * @package Tests\Connector\Sabre + */ +class CalDavBackendTest extends TestCase { + + /** @var CalDavBackend */ + private $backend; + + const UNIT_TEST_USER = 'caldav-unit-test'; + + + public function setUp() { + parent::setUp(); + + $db = \OC::$server->getDatabaseConnection(); + $this->backend = new CalDavBackend($db); + + $this->tearDown(); + } + + public function tearDown() { + parent::tearDown(); + + if (is_null($this->backend)) { + return; + } + $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); + foreach ($books as $book) { + $this->backend->deleteCalendar($book['id']); + } + $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER); + foreach ($subscriptions as $subscription) { + $this->backend->deleteSubscription($subscription['id']); + } + } + + public function testCalendarOperations() { + + $calendarId = $this->createTestCalendar(); + + // update it's display name + $patch = new PropPatch([ + '{DAV:}displayname' => 'Unit test', + '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'Calendar used for unit testing' + ]); + $this->backend->updateCalendar($calendarId, $patch); + $patch->commit(); + $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($books)); + $this->assertEquals('Unit test', $books[0]['{DAV:}displayname']); + $this->assertEquals('Calendar used for unit testing', $books[0]['{urn:ietf:params:xml:ns:caldav}calendar-description']); + + // delete the address book + $this->backend->deleteCalendar($books[0]['id']); + $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); + $this->assertEquals(0, count($books)); + } + + public function testCalendarObjectsOperations() { + + $calendarId = $this->createTestCalendar(); + + // create a card + $uri = $this->getUniqueID('calobj'); + $calData = <<<'EOD' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + + $this->backend->createCalendarObject($calendarId, $uri, $calData); + + // get all the cards + $calendarObjects = $this->backend->getCalendarObjects($calendarId); + $this->assertEquals(1, count($calendarObjects)); + $this->assertEquals($calendarId, $calendarObjects[0]['calendarid']); + + // get the cards + $calendarObject = $this->backend->getCalendarObject($calendarId, $uri); + $this->assertNotNull($calendarObject); + $this->assertArrayHasKey('id', $calendarObject); + $this->assertArrayHasKey('uri', $calendarObject); + $this->assertArrayHasKey('lastmodified', $calendarObject); + $this->assertArrayHasKey('etag', $calendarObject); + $this->assertArrayHasKey('size', $calendarObject); + $this->assertEquals($calData, $calendarObject['calendardata']); + + // update the card + $calData = <<<'EOD' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +END:VEVENT +END:VCALENDAR +EOD; + $this->backend->updateCalendarObject($calendarId, $uri, $calData); + $calendarObject = $this->backend->getCalendarObject($calendarId, $uri); + $this->assertEquals($calData, $calendarObject['calendardata']); + + // delete the card + $this->backend->deleteCalendarObject($calendarId, $uri); + $calendarObjects = $this->backend->getCalendarObjects($calendarId); + $this->assertEquals(0, count($calendarObjects)); + } + + public function testMultiCalendarObjects() { + + $calendarId = $this->createTestCalendar(); + + // create an event + $calData = <<<'EOD' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + $uri0 = $this->getUniqueID('card'); + $this->backend->createCalendarObject($calendarId, $uri0, $calData); + $uri1 = $this->getUniqueID('card'); + $this->backend->createCalendarObject($calendarId, $uri1, $calData); + $uri2 = $this->getUniqueID('card'); + $this->backend->createCalendarObject($calendarId, $uri2, $calData); + + // get all the cards + $calendarObjects = $this->backend->getCalendarObjects($calendarId); + $this->assertEquals(3, count($calendarObjects)); + + // get the cards + $calendarObjects = $this->backend->getMultipleCalendarObjects($calendarId, [$uri1, $uri2]); + $this->assertEquals(2, count($calendarObjects)); + foreach($calendarObjects as $card) { + $this->assertArrayHasKey('id', $card); + $this->assertArrayHasKey('uri', $card); + $this->assertArrayHasKey('lastmodified', $card); + $this->assertArrayHasKey('etag', $card); + $this->assertArrayHasKey('size', $card); + $this->assertEquals($calData, $card['calendardata']); + } + + // delete the card + $this->backend->deleteCalendarObject($calendarId, $uri0); + $this->backend->deleteCalendarObject($calendarId, $uri1); + $this->backend->deleteCalendarObject($calendarId, $uri2); + $calendarObjects = $this->backend->getCalendarObjects($calendarId); + $this->assertEquals(0, count($calendarObjects)); + } + + /** + * @dataProvider providesCalendarQueryParameters + */ + public function testCalendarQuery($expectedEventsInResult, $propFilters, $compFilter) { + $calendarId = $this->createTestCalendar(); + $events = []; + $events[0] = $this->createEvent($calendarId, '20130912T130000Z', '20130912T140000Z'); + $events[1] = $this->createEvent($calendarId, '20130912T150000Z', '20130912T170000Z'); + $events[2] = $this->createEvent($calendarId, '20130912T173000Z', '20130912T220000Z'); + + $result = $this->backend->calendarQuery($calendarId, [ + 'name' => '', + 'prop-filters' => $propFilters, + 'comp-filters' => $compFilter + ]); + + $expectedEventsInResult = array_map(function($index) use($events) { + return $events[$index]; + }, $expectedEventsInResult); + $this->assertEquals($expectedEventsInResult, $result, '', 0.0, 10, true); + } + + public function testGetCalendarObjectByUID() { + $calendarId = $this->createTestCalendar(); + $this->createEvent($calendarId, '20130912T130000Z', '20130912T140000Z'); + + $co = $this->backend->getCalendarObjectByUID(self::UNIT_TEST_USER, '47d15e3ec8'); + $this->assertNotNull($co); + } + + public function providesCalendarQueryParameters() { + return [ + 'all' => [[0, 1, 2], [], []], + 'only-todos' => [[], ['name' => 'VTODO'], []], + 'only-events' => [[0, 1, 2], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => null, 'end' => null], 'prop-filters' => []]],], + 'start' => [[1, 2], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => new DateTime('2013-09-12 14:00:00', new DateTimeZone('UTC')), 'end' => null], 'prop-filters' => []]],], + 'end' => [[0], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => null, 'end' => new DateTime('2013-09-12 14:00:00', new DateTimeZone('UTC'))], 'prop-filters' => []]],], + ]; + } + + private function createTestCalendar() { + $this->backend->createCalendar(self::UNIT_TEST_USER, 'Example', [ + '{http://apple.com/ns/ical/}calendar-color' => '#1C4587FF' + ]); + $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($calendars)); + $this->assertEquals(self::UNIT_TEST_USER, $calendars[0]['principaluri']); + /** @var SupportedCalendarComponentSet $components */ + $components = $calendars[0]['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set']; + $this->assertEquals(['VEVENT','VTODO'], $components->getValue()); + $color = $calendars[0]['{http://apple.com/ns/ical/}calendar-color']; + $this->assertEquals('#1C4587FF', $color); + $this->assertEquals('Example', $calendars[0]['uri']); + $this->assertEquals('Example', $calendars[0]['{DAV:}displayname']); + $calendarId = $calendars[0]['id']; + + return $calendarId; + } + + private function createEvent($calendarId, $start = '20130912T130000Z', $end = '20130912T140000Z') { + + $calData = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:$start +DTEND;VALUE=DATE-TIME:$end +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + $uri0 = $this->getUniqueID('event'); + $this->backend->createCalendarObject($calendarId, $uri0, $calData); + + return $uri0; + } + + public function testSyncSupport() { + $calendarId = $this->createTestCalendar(); + + // fist call without synctoken + $changes = $this->backend->getChangesForCalendar($calendarId, '', 1); + $syncToken = $changes['syncToken']; + + // add a change + $event = $this->createEvent($calendarId, '20130912T130000Z', '20130912T140000Z'); + + // look for changes + $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 1); + $this->assertEquals($event, $changes['added'][0]); + } + + public function testSubscriptions() { + $id = $this->backend->createSubscription(self::UNIT_TEST_USER, 'Subscription', [ + '{http://calendarserver.org/ns/}source' => new Href('test-source') + ]); + + $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($subscriptions)); + $this->assertEquals($id, $subscriptions[0]['id']); + + $patch = new PropPatch([ + '{DAV:}displayname' => 'Unit test', + ]); + $this->backend->updateSubscription($id, $patch); + $patch->commit(); + + $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($subscriptions)); + $this->assertEquals($id, $subscriptions[0]['id']); + $this->assertEquals('Unit test', $subscriptions[0]['{DAV:}displayname']); + + $this->backend->deleteSubscription($id); + $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER); + $this->assertEquals(0, count($subscriptions)); + } + + public function testScheduling() { + $this->backend->createSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule', ''); + + $sos = $this->backend->getSchedulingObjects(self::UNIT_TEST_USER); + $this->assertEquals(1, count($sos)); + + $so = $this->backend->getSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule'); + $this->assertNotNull($so); + + $this->backend->deleteSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule'); + + $sos = $this->backend->getSchedulingObjects(self::UNIT_TEST_USER); + $this->assertEquals(0, count($sos)); + } +} diff --git a/apps/dav/tests/unit/carddav/carddavbackendtest.php b/apps/dav/tests/unit/carddav/carddavbackendtest.php index 79ef36d8097..dd5e205242a 100644 --- a/apps/dav/tests/unit/carddav/carddavbackendtest.php +++ b/apps/dav/tests/unit/carddav/carddavbackendtest.php @@ -24,6 +24,13 @@ use OCA\DAV\CardDAV\CardDavBackend; use Sabre\DAV\PropPatch; use Test\TestCase; +/** + * Class CardDavBackendTest + * + * @group DB + * + * @package OCA\DAV\Tests\Unit\CardDAV + */ class CardDavBackendTest extends TestCase { /** @var CardDavBackend */ @@ -31,12 +38,20 @@ class CardDavBackendTest extends TestCase { const UNIT_TEST_USER = 'carddav-unit-test'; - public function setUp() { parent::setUp(); + $principal = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Principal') + ->disableOriginalConstructor() + ->setMethods(['getPrincipalByPath']) + ->getMock(); + $principal->method('getPrincipalByPath') + ->willReturn([ + 'uri' => 'principals/best-friend' + ]); + $db = \OC::$server->getDatabaseConnection(); - $this->backend = new CardDavBackend($db); + $this->backend = new CardDavBackend($db, $principal); $this->tearDown(); } @@ -178,4 +193,32 @@ class CardDavBackendTest extends TestCase { $changes = $this->backend->getChangesForAddressBook($bookId, $syncToken, 1); $this->assertEquals($uri0, $changes['added'][0]); } + + public function testSharing() { + $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($books)); + + $this->backend->updateShares('Example', [['href' => 'principal:principals/best-friend']], []); + + $shares = $this->backend->getShares('Example'); + $this->assertEquals(1, count($shares)); + + // adding the same sharee again has no effect + $this->backend->updateShares('Example', [['href' => 'principal:principals/best-friend']], []); + + $shares = $this->backend->getShares('Example'); + $this->assertEquals(1, count($shares)); + + $books = $this->backend->getAddressBooksForUser('principals/best-friend'); + $this->assertEquals(1, count($books)); + + $this->backend->updateShares('Example', [], [['href' => 'principal:principals/best-friend']]); + + $shares = $this->backend->getShares('Example'); + $this->assertEquals(0, count($shares)); + + $books = $this->backend->getAddressBooksForUser('principals/best-friend'); + $this->assertEquals(0, count($books)); + } } diff --git a/apps/dav/tests/unit/connector/sabre/FakeLockerPluginTest.php b/apps/dav/tests/unit/connector/sabre/FakeLockerPluginTest.php new file mode 100644 index 00000000000..dfe8cc220a3 --- /dev/null +++ b/apps/dav/tests/unit/connector/sabre/FakeLockerPluginTest.php @@ -0,0 +1,173 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * + * @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\DAV\Tests\Unit\Connector\Sabre; + +use OCA\DAV\Connector\Sabre\FakeLockerPlugin; +use Test\TestCase; + +/** + * Class FakeLockerPluginTest + * + * @package OCA\DAV\Tests\Unit\Connector\Sabre + */ +class FakeLockerPluginTest extends TestCase { + /** @var FakeLockerPlugin */ + private $fakeLockerPlugin; + + public function setUp() { + parent::setUp(); + $this->fakeLockerPlugin = new FakeLockerPlugin(); + } + + public function testInitialize() { + /** @var \Sabre\DAV\Server $server */ + $server = $this->getMock('\Sabre\DAV\Server'); + $server + ->expects($this->at(0)) + ->method('on') + ->with('method:LOCK', [$this->fakeLockerPlugin, 'fakeLockProvider'], 1); + $server + ->expects($this->at(1)) + ->method('on') + ->with('method:UNLOCK', [$this->fakeLockerPlugin, 'fakeUnlockProvider'], 1); + $server + ->expects($this->at(2)) + ->method('on') + ->with('propFind', [$this->fakeLockerPlugin, 'propFind']); + $server + ->expects($this->at(3)) + ->method('on') + ->with('validateTokens', [$this->fakeLockerPlugin, 'validateTokens']); + + $this->fakeLockerPlugin->initialize($server); + } + + public function testGetHTTPMethods() { + $expected = [ + 'LOCK', + 'UNLOCK', + ]; + $this->assertSame($expected, $this->fakeLockerPlugin->getHTTPMethods('Test')); + } + + public function testGetFeatures() { + $expected = [ + 2, + ]; + $this->assertSame($expected, $this->fakeLockerPlugin->getFeatures()); + } + + public function testPropFind() { + $propFind = $this->getMockBuilder('\Sabre\DAV\PropFind') + ->disableOriginalConstructor() + ->getMock(); + $node = $this->getMock('\Sabre\DAV\INode'); + + $propFind->expects($this->at(0)) + ->method('handle') + ->with('{DAV:}supportedlock'); + $propFind->expects($this->at(1)) + ->method('handle') + ->with('{DAV:}lockdiscovery'); + + $this->fakeLockerPlugin->propFind($propFind, $node); + } + + public function tokenDataProvider() { + return [ + [ + [ + [ + 'tokens' => [ + [ + 'token' => 'aToken', + 'validToken' => false, + ], + [], + [ + 'token' => 'opaquelocktoken:asdf', + 'validToken' => false, + ] + ], + ] + ], + [ + [ + 'tokens' => [ + [ + 'token' => 'aToken', + 'validToken' => false, + ], + [], + [ + 'token' => 'opaquelocktoken:asdf', + 'validToken' => true, + ] + ], + ] + ], + ] + ]; + } + + /** + * @dataProvider tokenDataProvider + * @param array $input + * @param array $expected + */ + public function testValidateTokens(array $input, array $expected) { + $request = $this->getMock('\Sabre\HTTP\RequestInterface'); + $this->fakeLockerPlugin->validateTokens($request, $input); + $this->assertSame($expected, $input); + } + + public function testFakeLockProvider() { + $request = $this->getMock('\Sabre\HTTP\RequestInterface'); + $response = $this->getMock('\Sabre\HTTP\ResponseInterface'); + $server = $this->getMock('\Sabre\DAV\Server'); + $this->fakeLockerPlugin->initialize($server); + + $request->expects($this->exactly(2)) + ->method('getPath') + ->will($this->returnValue('MyPath')); + $response->expects($this->once()) + ->method('setBody') + ->with('<?xml version="1.0" encoding="utf-8"?> +<d:prop xmlns:d="DAV:"><d:lockdiscovery><d:activelock><d:lockscope><d:exclusive/></d:lockscope><d:locktype><d:write/></d:locktype><d:lockroot><d:href>MyPath</d:href></d:lockroot><d:depth>infinity</d:depth><d:timeout>Second-1800</d:timeout><d:locktoken><d:href>opaquelocktoken:fe4f7f2437b151fbcb4e9f5c8118c6b1</d:href></d:locktoken><d:owner/></d:activelock></d:lockdiscovery></d:prop> +'); + + $this->assertSame(false, $this->fakeLockerPlugin->fakeLockProvider($request, $response)); + } + + public function testFakeUnlockProvider() { + $request = $this->getMock('\Sabre\HTTP\RequestInterface'); + $response = $this->getMock('\Sabre\HTTP\ResponseInterface'); + + $response->expects($this->once()) + ->method('setStatus') + ->with('204'); + $response->expects($this->once()) + ->method('setHeader') + ->with('Content-Length', '0'); + + $this->assertSame(false, $this->fakeLockerPlugin->fakeUnlockProvider($request, $response)); + } +} diff --git a/apps/dav/tests/unit/connector/sabre/directory.php b/apps/dav/tests/unit/connector/sabre/directory.php index 148a91d26db..75c4828641b 100644 --- a/apps/dav/tests/unit/connector/sabre/directory.php +++ b/apps/dav/tests/unit/connector/sabre/directory.php @@ -9,6 +9,8 @@ namespace OCA\DAV\Tests\Unit\Connector\Sabre; +use OCP\Files\ForbiddenException; + class Directory extends \Test\TestCase { /** @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject */ @@ -49,6 +51,25 @@ class Directory extends \Test\TestCase { } /** + * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden + */ + public function testDeleteForbidden() { + // deletion allowed + $this->info->expects($this->once()) + ->method('isDeletable') + ->will($this->returnValue(true)); + + // but fails + $this->view->expects($this->once()) + ->method('rmdir') + ->with('sub') + ->willThrowException(new ForbiddenException('', true)); + + $dir = $this->getDir('sub'); + $dir->delete(); + } + + /** * */ public function testDeleteFolderWhenAllowed() { diff --git a/apps/dav/tests/unit/connector/sabre/exception/forbiddentest.php b/apps/dav/tests/unit/connector/sabre/exception/forbiddentest.php new file mode 100644 index 00000000000..19799c71b9e --- /dev/null +++ b/apps/dav/tests/unit/connector/sabre/exception/forbiddentest.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright (c) 2015 Thomas Müller <deepdiver@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OCA\DAV\Tests\Unit\Connector\Sabre\Exception; + +use OCA\DAV\Connector\Sabre\Exception\Forbidden; + +class ForbiddenTest extends \Test\TestCase { + + public function testSerialization() { + + // create xml doc + $DOM = new \DOMDocument('1.0','utf-8'); + $DOM->formatOutput = true; + $error = $DOM->createElementNS('DAV:','d:error'); + $error->setAttribute('xmlns:s', \Sabre\DAV\Server::NS_SABREDAV); + $DOM->appendChild($error); + + // serialize the exception + $message = "1234567890"; + $retry = false; + $expectedXml = <<<EOD +<?xml version="1.0" encoding="utf-8"?> +<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:o="http://owncloud.org/ns"> + <o:retry xmlns:o="o:">false</o:retry> + <o:reason xmlns:o="o:">1234567890</o:reason> +</d:error> + +EOD; + + $ex = new Forbidden($message, $retry); + $server = $this->getMock('Sabre\DAV\Server'); + $ex->serialize($server, $error); + + // assert + $xml = $DOM->saveXML(); + $this->assertEquals($expectedXml, $xml); + } +} diff --git a/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php b/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php index 19e82320d55..4296a4d5618 100644 --- a/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php +++ b/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php @@ -1,15 +1,15 @@ <?php - -namespace OCA\DAV\Tests\Unit\Connector\Sabre\Exception; - -use OCA\DAV\Connector\Sabre\Exception\InvalidPath; - /** * Copyright (c) 2015 Thomas Müller <deepdiver@owncloud.com> * This file is licensed under the Affero General Public License version 3 or * later. * See the COPYING-README file. */ + +namespace OCA\DAV\Tests\Unit\Connector\Sabre\Exception; + +use OCA\DAV\Connector\Sabre\Exception\InvalidPath; + class InvalidPathTest extends \Test\TestCase { public function testSerialization() { diff --git a/apps/dav/tests/unit/connector/sabre/file.php b/apps/dav/tests/unit/connector/sabre/file.php index 94dadf88fe4..0a52299cec7 100644 --- a/apps/dav/tests/unit/connector/sabre/file.php +++ b/apps/dav/tests/unit/connector/sabre/file.php @@ -9,6 +9,7 @@ namespace OCA\DAV\Tests\Unit\Connector\Sabre; use OC\Files\Storage\Local; +use OCP\Files\ForbiddenException; use Test\HookHelper; use OC\Files\Filesystem; use OCP\Lock\ILockingProvider; @@ -40,6 +41,9 @@ class File extends \Test\TestCase { parent::tearDown(); } + /** + * @param string $string + */ private function getStream($string) { $stream = fopen('php://temp', 'r+'); fwrite($stream, $string); @@ -73,6 +77,10 @@ class File extends \Test\TestCase { 'Sabre\DAV\Exception\Forbidden' ], [ + new \OCP\Files\ForbiddenException('', true), + 'OCA\DAV\Connector\Sabre\Exception\Forbidden' + ], + [ new \OCP\Files\LockNotAcquiredException('/test.txt', 1), 'OCA\DAV\Connector\Sabre\Exception\FileLocked' ], @@ -234,7 +242,7 @@ class File extends \Test\TestCase { * @param string $path path to put the file into * @param string $viewRoot root to use for the view * - * @return result of the PUT operaiton which is usually the etag + * @return null|string of the PUT operaiton which is usually the etag */ private function doPut($path, $viewRoot = null) { $view = \OC\Files\Filesystem::getView(); @@ -691,6 +699,29 @@ class File extends \Test\TestCase { } /** + * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden + */ + public function testDeleteThrowsWhenDeletionThrows() { + // setup + $view = $this->getMock('\OC\Files\View', + array()); + + // but fails + $view->expects($this->once()) + ->method('unlink') + ->willThrowException(new ForbiddenException('', true)); + + $info = new \OC\Files\FileInfo('/test.txt', null, null, array( + 'permissions' => \OCP\Constants::PERMISSION_ALL + ), null); + + $file = new \OCA\DAV\Connector\Sabre\File($view, $info); + + // action + $file->delete(); + } + + /** * Asserts hook call * * @param array $callData hook call data to check @@ -835,4 +866,22 @@ class File extends \Test\TestCase { $file->get(); } + + /** + * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden + */ + public function testGetFopenThrows() { + $view = $this->getMock('\OC\Files\View', ['fopen'], array()); + $view->expects($this->atLeastOnce()) + ->method('fopen') + ->willThrowException(new ForbiddenException('', true)); + + $info = new \OC\Files\FileInfo('/test.txt', null, null, array( + 'permissions' => \OCP\Constants::PERMISSION_ALL + ), null); + + $file = new \OCA\DAV\Connector\Sabre\File($view, $info); + + $file->get(); + } } diff --git a/apps/dav/tests/unit/connector/sabre/filesplugin.php b/apps/dav/tests/unit/connector/sabre/filesplugin.php index f3c862941c0..b33c8340f72 100644 --- a/apps/dav/tests/unit/connector/sabre/filesplugin.php +++ b/apps/dav/tests/unit/connector/sabre/filesplugin.php @@ -11,10 +11,13 @@ namespace OCA\DAV\Tests\Unit\Connector\Sabre; class FilesPlugin extends \Test\TestCase { const GETETAG_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::GETETAG_PROPERTYNAME; const FILEID_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::FILEID_PROPERTYNAME; + const INTERNAL_FILEID_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::INTERNAL_FILEID_PROPERTYNAME; const SIZE_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::SIZE_PROPERTYNAME; const PERMISSIONS_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::PERMISSIONS_PROPERTYNAME; const LASTMODIFIED_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::LASTMODIFIED_PROPERTYNAME; const DOWNLOADURL_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::DOWNLOADURL_PROPERTYNAME; + const OWNER_ID_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::OWNER_ID_PROPERTYNAME; + const OWNER_DISPLAY_NAME_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME; /** * @var \Sabre\DAV\Server @@ -52,6 +55,9 @@ class FilesPlugin extends \Test\TestCase { $this->plugin->initialize($this->server); } + /** + * @param string $class + */ private function createTestNode($class) { $node = $this->getMockBuilder($class) ->disableOriginalConstructor() @@ -67,7 +73,10 @@ class FilesPlugin extends \Test\TestCase { $node->expects($this->any()) ->method('getFileId') - ->will($this->returnValue(123)); + ->will($this->returnValue('00000123instanceid')); + $node->expects($this->any()) + ->method('getInternalFileId') + ->will($this->returnValue('123')); $node->expects($this->any()) ->method('getEtag') ->will($this->returnValue('"abc"')); @@ -88,16 +97,33 @@ class FilesPlugin extends \Test\TestCase { array( self::GETETAG_PROPERTYNAME, self::FILEID_PROPERTYNAME, + self::INTERNAL_FILEID_PROPERTYNAME, self::SIZE_PROPERTYNAME, self::PERMISSIONS_PROPERTYNAME, self::DOWNLOADURL_PROPERTYNAME, + self::OWNER_ID_PROPERTYNAME, + self::OWNER_DISPLAY_NAME_PROPERTYNAME ), 0 ); + $user = $this->getMockBuilder('\OC\User\User') + ->disableOriginalConstructor()->getMock(); + $user + ->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('foo')); + $user + ->expects($this->once()) + ->method('getDisplayName') + ->will($this->returnValue('M. Foo')); + $node->expects($this->once()) ->method('getDirectDownload') ->will($this->returnValue(array('url' => 'http://example.com/'))); + $node->expects($this->exactly(2)) + ->method('getOwner') + ->will($this->returnValue($user)); $node->expects($this->never()) ->method('getSize'); @@ -107,10 +133,13 @@ class FilesPlugin extends \Test\TestCase { ); $this->assertEquals('"abc"', $propFind->get(self::GETETAG_PROPERTYNAME)); - $this->assertEquals(123, $propFind->get(self::FILEID_PROPERTYNAME)); + $this->assertEquals('00000123instanceid', $propFind->get(self::FILEID_PROPERTYNAME)); + $this->assertEquals('123', $propFind->get(self::INTERNAL_FILEID_PROPERTYNAME)); $this->assertEquals(null, $propFind->get(self::SIZE_PROPERTYNAME)); $this->assertEquals('DWCKMSR', $propFind->get(self::PERMISSIONS_PROPERTYNAME)); $this->assertEquals('http://example.com/', $propFind->get(self::DOWNLOADURL_PROPERTYNAME)); + $this->assertEquals('foo', $propFind->get(self::OWNER_ID_PROPERTYNAME)); + $this->assertEquals('M. Foo', $propFind->get(self::OWNER_DISPLAY_NAME_PROPERTYNAME)); $this->assertEquals(array(self::SIZE_PROPERTYNAME), $propFind->get404Properties()); } @@ -166,7 +195,7 @@ class FilesPlugin extends \Test\TestCase { ); $this->assertEquals('"abc"', $propFind->get(self::GETETAG_PROPERTYNAME)); - $this->assertEquals(123, $propFind->get(self::FILEID_PROPERTYNAME)); + $this->assertEquals('00000123instanceid', $propFind->get(self::FILEID_PROPERTYNAME)); $this->assertEquals(1025, $propFind->get(self::SIZE_PROPERTYNAME)); $this->assertEquals('DWCKMSR', $propFind->get(self::PERMISSIONS_PROPERTYNAME)); $this->assertEquals(null, $propFind->get(self::DOWNLOADURL_PROPERTYNAME)); @@ -207,6 +236,36 @@ class FilesPlugin extends \Test\TestCase { $this->assertEquals(200, $result[self::GETETAG_PROPERTYNAME]); } + public function testUpdatePropsForbidden() { + $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File'); + + $propPatch = new \Sabre\DAV\PropPatch(array( + self::OWNER_ID_PROPERTYNAME => 'user2', + self::OWNER_DISPLAY_NAME_PROPERTYNAME => 'User Two', + self::FILEID_PROPERTYNAME => 12345, + self::PERMISSIONS_PROPERTYNAME => 'C', + self::SIZE_PROPERTYNAME => 123, + self::DOWNLOADURL_PROPERTYNAME => 'http://example.com/', + )); + + $this->plugin->handleUpdateProperties( + '/dummypath', + $propPatch + ); + + $propPatch->commit(); + + $this->assertEmpty($propPatch->getRemainingMutations()); + + $result = $propPatch->getResult(); + $this->assertEquals(403, $result[self::OWNER_ID_PROPERTYNAME]); + $this->assertEquals(403, $result[self::OWNER_DISPLAY_NAME_PROPERTYNAME]); + $this->assertEquals(403, $result[self::FILEID_PROPERTYNAME]); + $this->assertEquals(403, $result[self::PERMISSIONS_PROPERTYNAME]); + $this->assertEquals(403, $result[self::SIZE_PROPERTYNAME]); + $this->assertEquals(403, $result[self::DOWNLOADURL_PROPERTYNAME]); + } + /** * Testcase from https://github.com/owncloud/core/issues/5251 * diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/auth.php b/apps/dav/tests/unit/connector/sabre/requesttest/auth.php index 41b554d11db..02b64ab070b 100644 --- a/apps/dav/tests/unit/connector/sabre/requesttest/auth.php +++ b/apps/dav/tests/unit/connector/sabre/requesttest/auth.php @@ -41,7 +41,7 @@ class Auth implements BackendInterface { * * @param \Sabre\DAV\Server $server * @param string $realm - * @return bool + * @return boolean|null */ function authenticate(\Sabre\DAV\Server $server, $realm) { $userSession = \OC::$server->getUserSession(); @@ -61,7 +61,7 @@ class Auth implements BackendInterface { * * If nobody is currently logged in, this method should return null. * - * @return string|null + * @return string */ function getCurrentUser() { return $this->user; diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/requesttest.php b/apps/dav/tests/unit/connector/sabre/requesttest/requesttest.php index d90cf6e19bc..a83f25c1585 100644 --- a/apps/dav/tests/unit/connector/sabre/requesttest/requesttest.php +++ b/apps/dav/tests/unit/connector/sabre/requesttest/requesttest.php @@ -46,7 +46,8 @@ abstract class RequestTest extends TestCase { \OC::$server->getUserSession(), \OC::$server->getMountManager(), \OC::$server->getTagManager(), - \OC::$server->getEventDispatcher() + \OC::$server->getEventDispatcher(), + $this->getMock('\OCP\IRequest') ); } @@ -67,6 +68,7 @@ abstract class RequestTest extends TestCase { * @param resource|string|null $body * @param array|null $headers * @return \Sabre\HTTP\Response + * @throws \Exception */ protected function request($view, $user, $password, $method, $url, $body = null, $headers = null) { if (is_string($body)) { diff --git a/apps/encryption/l10n/tr.js b/apps/encryption/l10n/tr.js index e1fcb526d06..5abbf87b813 100644 --- a/apps/encryption/l10n/tr.js +++ b/apps/encryption/l10n/tr.js @@ -32,6 +32,8 @@ OC.L10N.register( "The share will expire on %s." : "Bu paylaşım %s tarihinde sona erecek.", "Cheers!" : "Hoşçakalın!", "Hey there,<br><br>the admin enabled server-side-encryption. Your files were encrypted using the password <strong>%s</strong>.<br><br>Please login to the web interface, go to the section \"ownCloud basic encryption module\" of your personal settings and update your encryption password by entering this password into the \"old log-in password\" field and your current login-password.<br><br>" : "Selam,<br><br>Sistem yöneticisi sunucu tarafında şifrelemeyi etkinleştirdi. Dosyalarınız <strong>%s</strong> parolası kullanılarak şifrelendi.<br><br>Lütfen web arayüzünde oturum açın ve kişisel ayarlarınızdan 'ownCloud temel şifreleme modülü'ne giderek 'eski oturum parolası' alanına bu parolayı girdikten sonra şifreleme parolanızı ve mevcut oturum açma parolanızı güncelleyin.<br><br>", + "Encrypt the home storage" : "Yerel depolamayı şifrele", + "Enabling this option encrypts all files stored on the main storage, otherwise only files on external storage will be encrypted" : "Bu seçeneği etkinleştirmek ana depolamadaki bütün dosyaları şifreler, aksi takdirde sadece harici depolamadaki dosyalar şifrelenir", "Enable recovery key" : "Kurtarma anahtarını etkinleştir", "Disable recovery key" : "Kurtarma anahtarını devre dışı bırak", "The recovery key is an extra encryption key that is used to encrypt files. It allows recovery of a user's files if the user forgets his or her password." : "Kurtarma anahtarı, dosyaların şifrelenmesi için daha fazla \nşifreleme sunar. Bu kullanıcının dosyasının şifresini unuttuğunda kurtarmasına imkan verir.", diff --git a/apps/encryption/l10n/tr.json b/apps/encryption/l10n/tr.json index 743d3e7d15c..31824461d84 100644 --- a/apps/encryption/l10n/tr.json +++ b/apps/encryption/l10n/tr.json @@ -30,6 +30,8 @@ "The share will expire on %s." : "Bu paylaşım %s tarihinde sona erecek.", "Cheers!" : "Hoşçakalın!", "Hey there,<br><br>the admin enabled server-side-encryption. Your files were encrypted using the password <strong>%s</strong>.<br><br>Please login to the web interface, go to the section \"ownCloud basic encryption module\" of your personal settings and update your encryption password by entering this password into the \"old log-in password\" field and your current login-password.<br><br>" : "Selam,<br><br>Sistem yöneticisi sunucu tarafında şifrelemeyi etkinleştirdi. Dosyalarınız <strong>%s</strong> parolası kullanılarak şifrelendi.<br><br>Lütfen web arayüzünde oturum açın ve kişisel ayarlarınızdan 'ownCloud temel şifreleme modülü'ne giderek 'eski oturum parolası' alanına bu parolayı girdikten sonra şifreleme parolanızı ve mevcut oturum açma parolanızı güncelleyin.<br><br>", + "Encrypt the home storage" : "Yerel depolamayı şifrele", + "Enabling this option encrypts all files stored on the main storage, otherwise only files on external storage will be encrypted" : "Bu seçeneği etkinleştirmek ana depolamadaki bütün dosyaları şifreler, aksi takdirde sadece harici depolamadaki dosyalar şifrelenir", "Enable recovery key" : "Kurtarma anahtarını etkinleştir", "Disable recovery key" : "Kurtarma anahtarını devre dışı bırak", "The recovery key is an extra encryption key that is used to encrypt files. It allows recovery of a user's files if the user forgets his or her password." : "Kurtarma anahtarı, dosyaların şifrelenmesi için daha fazla \nşifreleme sunar. Bu kullanıcının dosyasının şifresini unuttuğunda kurtarmasına imkan verir.", diff --git a/apps/files/appinfo/app.php b/apps/files/appinfo/app.php index 40b194ab882..c752b5e7d72 100644 --- a/apps/files/appinfo/app.php +++ b/apps/files/appinfo/app.php @@ -25,12 +25,14 @@ */ \OCP\App::registerAdmin('files', 'admin'); + \OC::$server->getNavigationManager()->add(function () { + $urlGenerator = \OC::$server->getURLGenerator(); $l = \OC::$server->getL10N('files'); return [ 'id' => 'files_index', 'order' => 0, - 'href' => \OCP\Util::linkTo('files', 'index.php'), + 'href' => $urlGenerator->linkToRoute('files.view.index'), 'icon' => \OCP\Util::imagePath('core', 'places/files.svg'), 'name' => $l->t('Files'), ]; diff --git a/apps/files/appinfo/routes.php b/apps/files/appinfo/routes.php index d52dfaab21c..2bb913c30a6 100644 --- a/apps/files/appinfo/routes.php +++ b/apps/files/appinfo/routes.php @@ -48,14 +48,17 @@ $application->registerRoutes( 'verb' => 'GET', 'requirements' => array('tagName' => '.+'), ), + [ + 'name' => 'view#index', + 'url' => '/', + 'verb' => 'GET', + ], ) ) ); /** @var $this \OC\Route\Router */ -$this->create('files_index', '/') - ->actionInclude('files/index.php'); $this->create('files_ajax_delete', 'ajax/delete.php') ->actionInclude('files/ajax/delete.php'); $this->create('files_ajax_download', 'ajax/download.php') diff --git a/apps/files/controller/viewcontroller.php b/apps/files/controller/viewcontroller.php new file mode 100644 index 00000000000..c274680e525 --- /dev/null +++ b/apps/files/controller/viewcontroller.php @@ -0,0 +1,224 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * + * @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\Controller; + +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IL10N; +use OCP\INavigationManager; +use OCP\IRequest; +use OCP\IURLGenerator; +use OCP\IConfig; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; + +/** + * Class ViewController + * + * @package OCA\Files\Controller + */ +class ViewController extends Controller { + /** @var string */ + protected $appName; + /** @var IRequest */ + protected $request; + /** @var IURLGenerator */ + protected $urlGenerator; + /** @var INavigationManager */ + protected $navigationManager; + /** @var IL10N */ + protected $l10n; + /** @var IConfig */ + protected $config; + /** @var EventDispatcherInterface */ + protected $eventDispatcher; + + /** + * @param string $appName + * @param IRequest $request + * @param IURLGenerator $urlGenerator + * @param INavigationManager $navigationManager + * @param IL10N $l10n + * @param IConfig $config + * @param EventDispatcherInterface $eventDispatcherInterface + */ + public function __construct($appName, + IRequest $request, + IURLGenerator $urlGenerator, + INavigationManager $navigationManager, + IL10N $l10n, + IConfig $config, + EventDispatcherInterface $eventDispatcherInterface) { + parent::__construct($appName, $request); + $this->appName = $appName; + $this->request = $request; + $this->urlGenerator = $urlGenerator; + $this->navigationManager = $navigationManager; + $this->l10n = $l10n; + $this->config = $config; + $this->eventDispatcher = $eventDispatcherInterface; + } + + /** + * @param string $appName + * @param string $scriptName + * @return string + */ + protected function renderScript($appName, $scriptName) { + $content = ''; + $appPath = \OC_App::getAppPath($appName); + $scriptPath = $appPath . '/' . $scriptName; + if (file_exists($scriptPath)) { + // TODO: sanitize path / script name ? + ob_start(); + include $scriptPath; + $content = ob_get_contents(); + @ob_end_clean(); + } + return $content; + } + + /** + * FIXME: Replace with non static code + * + * @return array + * @throws \OCP\Files\NotFoundException + */ + protected function getStorageInfo() { + $dirInfo = \OC\Files\Filesystem::getFileInfo('/', false); + return \OC_Helper::getStorageInfo('/', $dirInfo); + } + + /** + * @NoCSRFRequired + * @NoAdminRequired + * + * @param string $dir + * @param string $view + * @return TemplateResponse + * @throws \OCP\Files\NotFoundException + */ + public function index($dir = '', $view = '') { + // Load the files we need + \OCP\Util::addStyle('files', 'files'); + \OCP\Util::addStyle('files', 'upload'); + \OCP\Util::addStyle('files', 'mobile'); + \OCP\Util::addscript('files', 'app'); + \OCP\Util::addscript('files', 'file-upload'); + \OCP\Util::addscript('files', 'newfilemenu'); + \OCP\Util::addscript('files', 'jquery.iframe-transport'); + \OCP\Util::addscript('files', 'jquery.fileupload'); + \OCP\Util::addscript('files', 'jquery-visibility'); + \OCP\Util::addscript('files', 'fileinfomodel'); + \OCP\Util::addscript('files', 'filesummary'); + \OCP\Util::addscript('files', 'breadcrumb'); + \OCP\Util::addscript('files', 'filelist'); + \OCP\Util::addscript('files', 'search'); + + \OCP\Util::addScript('files', 'favoritesfilelist'); + \OCP\Util::addScript('files', 'tagsplugin'); + \OCP\Util::addScript('files', 'favoritesplugin'); + + \OCP\Util::addScript('files', 'detailfileinfoview'); + \OCP\Util::addScript('files', 'detailtabview'); + \OCP\Util::addScript('files', 'mainfileinfodetailview'); + \OCP\Util::addScript('files', 'detailsview'); + \OCP\Util::addStyle('files', 'detailsView'); + + \OC_Util::addVendorScript('core', 'handlebars/handlebars'); + + \OCP\Util::addscript('files', 'fileactions'); + \OCP\Util::addscript('files', 'fileactionsmenu'); + \OCP\Util::addscript('files', 'files'); + \OCP\Util::addscript('files', 'keyboardshortcuts'); + \OCP\Util::addscript('files', 'navigation'); + + // if IE8 and "?dir=path&view=someview" was specified, reformat the URL to use a hash like "#?dir=path&view=someview" + $isIE8 = $this->request->isUserAgent([Request::USER_AGENT_IE_8]); + if ($isIE8 && ($dir !== '' || $view !== '')) { + $dir = !empty($dir) ? $dir : '/'; + $view = !empty($view) ? $view : 'files'; + $hash = '#?dir=' . \OCP\Util::encodePath($dir); + if ($view !== 'files') { + $hash .= '&view=' . urlencode($view); + } + return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index') . $hash); + } + + // mostly for the home storage's free space + // FIXME: Make non static + $storageInfo = $this->getStorageInfo(); + + $nav = new \OCP\Template('files', 'appnavigation', ''); + + \OCA\Files\App::getNavigationManager()->add( + [ + 'id' => 'favorites', + 'appname' => 'files', + 'script' => 'simplelist.php', + 'order' => 5, + 'name' => $this->l10n->t('Favorites') + ] + ); + + $navItems = \OCA\Files\App::getNavigationManager()->getAll(); + usort($navItems, function($item1, $item2) { + return $item1['order'] - $item2['order']; + }); + $nav->assign('navigationItems', $navItems); + + $contentItems = []; + + // render the container content for every navigation item + foreach ($navItems as $item) { + $content = ''; + if (isset($item['script'])) { + $content = $this->renderScript($item['appname'], $item['script']); + } + $contentItem = []; + $contentItem['id'] = $item['id']; + $contentItem['content'] = $content; + $contentItems[] = $contentItem; + } + + $this->eventDispatcher->dispatch('OCA\Files::loadAdditionalScripts'); + + $params = []; + $params['usedSpacePercent'] = (int)$storageInfo['relative']; + $params['owner'] = $storageInfo['owner']; + $params['ownerDisplayName'] = $storageInfo['ownerDisplayName']; + $params['isPublic'] = false; + $params['mailNotificationEnabled'] = $this->config->getAppValue('core', 'shareapi_allow_mail_notification', 'no'); + $params['mailPublicNotificationEnabled'] = $this->config->getAppValue('core', 'shareapi_allow_public_notification', 'no'); + $params['allowShareWithLink'] = $this->config->getAppValue('core', 'shareapi_allow_links', 'yes'); + $params['appNavigation'] = $nav; + $params['appContents'] = $contentItems; + $this->navigationManager->setActiveEntry('files_index'); + + return new TemplateResponse( + $this->appName, + 'index', + $params + ); + } +} diff --git a/apps/files/index.php b/apps/files/index.php deleted file mode 100644 index cc007ebdb07..00000000000 --- a/apps/files/index.php +++ /dev/null @@ -1,161 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * @author Frank Karlitschek <frank@owncloud.org> - * @author Jakob Sack <mail@jakobsack.de> - * @author Jan-Christoph Borchardt <hey@jancborchardt.net> - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Roman Geber <rgeber@owncloudapps.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @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/> - * - */ - -// Check if we are a user -OCP\User::checkLoggedIn(); - -// Load the files we need -OCP\Util::addStyle('files', 'files'); -OCP\Util::addStyle('files', 'upload'); -OCP\Util::addStyle('files', 'mobile'); -OCP\Util::addscript('files', 'app'); -OCP\Util::addscript('files', 'file-upload'); -OCP\Util::addscript('files', 'newfilemenu'); -OCP\Util::addscript('files', 'jquery.iframe-transport'); -OCP\Util::addscript('files', 'jquery.fileupload'); -OCP\Util::addscript('files', 'jquery-visibility'); -OCP\Util::addscript('files', 'fileinfomodel'); -OCP\Util::addscript('files', 'filesummary'); -OCP\Util::addscript('files', 'breadcrumb'); -OCP\Util::addscript('files', 'filelist'); -OCP\Util::addscript('files', 'search'); - -\OCP\Util::addScript('files', 'favoritesfilelist'); -\OCP\Util::addScript('files', 'tagsplugin'); -\OCP\Util::addScript('files', 'favoritesplugin'); - -\OCP\Util::addScript('files', 'detailfileinfoview'); -\OCP\Util::addScript('files', 'detailtabview'); -\OCP\Util::addScript('files', 'mainfileinfodetailview'); -\OCP\Util::addScript('files', 'detailsview'); -\OCP\Util::addStyle('files', 'detailsView'); - -\OC_Util::addVendorScript('core', 'handlebars/handlebars'); - -OCP\App::setActiveNavigationEntry('files_index'); - -$l = \OC::$server->getL10N('files'); - -$isIE8 = false; -preg_match('/MSIE (.*?);/', $_SERVER['HTTP_USER_AGENT'], $matches); -if (count($matches) > 0 && $matches[1] <= 9) { - $isIE8 = true; -} - -// if IE8 and "?dir=path&view=someview" was specified, reformat the URL to use a hash like "#?dir=path&view=someview" -if ($isIE8 && (isset($_GET['dir']) || isset($_GET['view']))) { - $hash = '#?'; - $dir = isset($_GET['dir']) ? $_GET['dir'] : '/'; - $view = isset($_GET['view']) ? $_GET['view'] : 'files'; - $hash = '#?dir=' . \OCP\Util::encodePath($dir); - if ($view !== 'files') { - $hash .= '&view=' . urlencode($view); - } - header('Location: ' . OCP\Util::linkTo('files', 'index.php') . $hash); - exit(); -} - -$user = OC_User::getUser(); - -$config = \OC::$server->getConfig(); - -// mostly for the home storage's free space -$dirInfo = \OC\Files\Filesystem::getFileInfo('/', false); -$storageInfo=OC_Helper::getStorageInfo('/', $dirInfo); - -$nav = new OCP\Template('files', 'appnavigation', ''); - -function sortNavigationItems($item1, $item2) { - return $item1['order'] - $item2['order']; -} - -\OCA\Files\App::getNavigationManager()->add( - array( - 'id' => 'favorites', - 'appname' => 'files', - 'script' => 'simplelist.php', - 'order' => 5, - 'name' => $l->t('Favorites') - ) -); - -$navItems = \OCA\Files\App::getNavigationManager()->getAll(); -usort($navItems, 'sortNavigationItems'); -$nav->assign('navigationItems', $navItems); - -$contentItems = array(); - -function renderScript($appName, $scriptName) { - $content = ''; - $appPath = OC_App::getAppPath($appName); - $scriptPath = $appPath . '/' . $scriptName; - if (file_exists($scriptPath)) { - // TODO: sanitize path / script name ? - ob_start(); - include $scriptPath; - $content = ob_get_contents(); - @ob_end_clean(); - } - return $content; -} - -// render the container content for every navigation item -foreach ($navItems as $item) { - $content = ''; - if (isset($item['script'])) { - $content = renderScript($item['appname'], $item['script']); - } - $contentItem = array(); - $contentItem['id'] = $item['id']; - $contentItem['content'] = $content; - $contentItems[] = $contentItem; -} - -OCP\Util::addscript('files', 'fileactions'); -OCP\Util::addscript('files', 'fileactionsmenu'); -OCP\Util::addscript('files', 'files'); -OCP\Util::addscript('files', 'navigation'); -OCP\Util::addscript('files', 'keyboardshortcuts'); - -\OC::$server->getEventDispatcher()->dispatch('OCA\Files::loadAdditionalScripts'); - -$tmpl = new OCP\Template('files', 'index', 'user'); -$tmpl->assign('usedSpacePercent', (int)$storageInfo['relative']); -$tmpl->assign('owner', $storageInfo['owner']); -$tmpl->assign('ownerDisplayName', $storageInfo['ownerDisplayName']); -$tmpl->assign('isPublic', false); -$tmpl->assign("mailNotificationEnabled", $config->getAppValue('core', 'shareapi_allow_mail_notification', 'no')); -$tmpl->assign("mailPublicNotificationEnabled", $config->getAppValue('core', 'shareapi_allow_public_notification', 'no')); -$tmpl->assign("allowShareWithLink", $config->getAppValue('core', 'shareapi_allow_links', 'yes')); -$tmpl->assign('appNavigation', $nav); -$tmpl->assign('appContents', $contentItems); - -$tmpl->printPage(); diff --git a/apps/files/js/newfilemenu.js b/apps/files/js/newfilemenu.js index 175eb1d1a75..be7dcc40b6e 100644 --- a/apps/files/js/newfilemenu.js +++ b/apps/files/js/newfilemenu.js @@ -44,6 +44,11 @@ 'click .menuitem': '_onClickAction' }, + /** + * @type OCA.Files.FileList + */ + fileList: null, + initialize: function(options) { var self = this; var $uploadEl = $('#file_upload_start'); @@ -55,25 +60,16 @@ console.warn('Missing upload element "file_upload_start"'); } - this._fileList = options && options.fileList; + this.fileList = options && options.fileList; this._menuItems = [{ - id: 'file', - displayName: t('files', 'Text file'), - templateName: t('files', 'New text file.txt'), - iconClass: 'icon-filetype-text', - fileType: 'file', - actionHandler: function(name) { - self._fileList.createFile(name); - } - }, { id: 'folder', displayName: t('files', 'Folder'), templateName: t('files', 'New folder'), iconClass: 'icon-folder', fileType: 'folder', actionHandler: function(name) { - self._fileList.createDirectory(name); + self.fileList.createDirectory(name); } }]; @@ -149,7 +145,7 @@ try { if (!Files.isFileNameValid(filename)) { // Files.isFileNameValid(filename) throws an exception itself - } else if (self._fileList.inList(filename)) { + } else if (self.fileList.inList(filename)) { throw t('files', '{newname} already exists', {newname: filename}); } else { return true; diff --git a/apps/files/l10n/hu_HU.js b/apps/files/l10n/hu_HU.js index 48e9bfb168b..3c12e4bd486 100644 --- a/apps/files/l10n/hu_HU.js +++ b/apps/files/l10n/hu_HU.js @@ -106,6 +106,8 @@ OC.L10N.register( "Maximum upload size" : "Maximális feltölthető fájlméret", "max. possible: " : "max. lehetséges: ", "Save" : "Mentés", + "With PHP-FPM it might take 5 minutes for changes to be applied." : "PHP-FPM-mel akár 5 percbe is telhet, míg ez a beállítás érvénybe lép.", + "Missing permissions to edit from here." : "Innen nem lehet szerkeszteni hiányzó jogosultság miatt.", "Settings" : "Beállítások", "WebDAV" : "WebDAV", "Use this address to <a href=\"%s\" target=\"_blank\">access your Files via WebDAV</a>" : "Ezt a címet használja, ha <a href=\"%s\" target=\"_blank\">WebDAV-on keresztül szeretné elérni a fájljait</a>", diff --git a/apps/files/l10n/hu_HU.json b/apps/files/l10n/hu_HU.json index c72dfa0d48b..b1bde8b7d51 100644 --- a/apps/files/l10n/hu_HU.json +++ b/apps/files/l10n/hu_HU.json @@ -104,6 +104,8 @@ "Maximum upload size" : "Maximális feltölthető fájlméret", "max. possible: " : "max. lehetséges: ", "Save" : "Mentés", + "With PHP-FPM it might take 5 minutes for changes to be applied." : "PHP-FPM-mel akár 5 percbe is telhet, míg ez a beállítás érvénybe lép.", + "Missing permissions to edit from here." : "Innen nem lehet szerkeszteni hiányzó jogosultság miatt.", "Settings" : "Beállítások", "WebDAV" : "WebDAV", "Use this address to <a href=\"%s\" target=\"_blank\">access your Files via WebDAV</a>" : "Ezt a címet használja, ha <a href=\"%s\" target=\"_blank\">WebDAV-on keresztül szeretné elérni a fájljait</a>", diff --git a/apps/files/l10n/hy.js b/apps/files/l10n/hy.js index 62bc84d6c7b..ae06fed4cd6 100644 --- a/apps/files/l10n/hy.js +++ b/apps/files/l10n/hy.js @@ -2,17 +2,29 @@ OC.L10N.register( "files", { "Files" : "Ֆայլեր", + "All files" : "Բոլոր ֆայլերը", "Close" : "Փակել", "Download" : "Բեռնել", "Rename" : "Վերանվանել", "Delete" : "Ջնջել", "Select" : "Նշել", + "Error" : "Սխալ", + "Could not rename file" : "Չկարողացա վերանվանել ֆայլը", + "Could not create file" : "Չկարողացա ստեղծել ֆայլը", + "Could not create folder" : "Չկարողացա ստեղծել պանակը", "Name" : "Անուն", "Size" : "Չափս", + "Modified" : "Փոփոխված", + "_%n folder_::_%n folders_" : ["%n պանակ","%n պանակ"], + "_%n file_::_%n files_" : ["%n ֆայլ","%n ֆայլ"], + "{dirs} and {files}" : "{dirs} և {files}", "New" : "Նոր", + "_%n byte_::_%n bytes_" : ["%n բայտ","%n բայտ"], "Folder" : "Պանակ", "New folder" : "Նոր պանակ", "Save" : "Պահպանել", - "Select all" : "Նշել բոլորը" + "Select all" : "Նշել բոլորը", + "Text file" : "Տեքստ ֆայլ", + "New text file.txt" : "Նոր տեքստ ֆայլ.txt" }, "nplurals=2; plural=(n != 1);"); diff --git a/apps/files/l10n/hy.json b/apps/files/l10n/hy.json index c5afd306272..4874c47b3a4 100644 --- a/apps/files/l10n/hy.json +++ b/apps/files/l10n/hy.json @@ -1,16 +1,28 @@ { "translations": { "Files" : "Ֆայլեր", + "All files" : "Բոլոր ֆայլերը", "Close" : "Փակել", "Download" : "Բեռնել", "Rename" : "Վերանվանել", "Delete" : "Ջնջել", "Select" : "Նշել", + "Error" : "Սխալ", + "Could not rename file" : "Չկարողացա վերանվանել ֆայլը", + "Could not create file" : "Չկարողացա ստեղծել ֆայլը", + "Could not create folder" : "Չկարողացա ստեղծել պանակը", "Name" : "Անուն", "Size" : "Չափս", + "Modified" : "Փոփոխված", + "_%n folder_::_%n folders_" : ["%n պանակ","%n պանակ"], + "_%n file_::_%n files_" : ["%n ֆայլ","%n ֆայլ"], + "{dirs} and {files}" : "{dirs} և {files}", "New" : "Նոր", + "_%n byte_::_%n bytes_" : ["%n բայտ","%n բայտ"], "Folder" : "Պանակ", "New folder" : "Նոր պանակ", "Save" : "Պահպանել", - "Select all" : "Նշել բոլորը" + "Select all" : "Նշել բոլորը", + "Text file" : "Տեքստ ֆայլ", + "New text file.txt" : "Նոր տեքստ ֆայլ.txt" },"pluralForm" :"nplurals=2; plural=(n != 1);" }
\ No newline at end of file diff --git a/apps/files/l10n/lt_LT.js b/apps/files/l10n/lt_LT.js index ac7084cfc2d..13e7a03e45b 100644 --- a/apps/files/l10n/lt_LT.js +++ b/apps/files/l10n/lt_LT.js @@ -75,8 +75,6 @@ OC.L10N.register( "_%n byte_::_%n bytes_" : ["%n baitas","%n baitai","%n baitų"], "Favorited" : "Pažymėta mėgstamu", "Favorite" : "Mėgiamas", - "Text file" : "Teksto failas", - "New text file.txt" : "Naujas tekstas file.txt", "Folder" : "Katalogas", "New folder" : "Naujas aplankas", "{newname} already exists" : "{newname} jau egzistuoja", @@ -106,6 +104,8 @@ OC.L10N.register( "Maximum upload size" : "Maksimalus įkeliamo failo dydis", "max. possible: " : "maks. galima:", "Save" : "Išsaugoti", + "With PHP-FPM it might take 5 minutes for changes to be applied." : "Su PHP-FPM atnaujinimai gali užtrukti apie 5min.", + "Missing permissions to edit from here." : "Draudžiama iš čia redaguoti", "Settings" : "Nustatymai", "WebDAV" : "WebDAV", "Use this address to <a href=\"%s\" target=\"_blank\">access your Files via WebDAV</a>" : "Naudokite šį adresą, kad <a href=\"%s\" target=\"_blank\">pasiektumėte savo failus per WebDAV</a>", @@ -119,6 +119,8 @@ OC.L10N.register( "Files are being scanned, please wait." : "Skenuojami failai, prašome palaukti.", "Currently scanning" : "Šiuo metu skenuojama", "No favorites" : "Nėra mėgstamiausių", - "Files and folders you mark as favorite will show up here" : "Failai ir aplankai, kuriuos pažymite mėgstamais, atsiras čia" + "Files and folders you mark as favorite will show up here" : "Failai ir aplankai, kuriuos pažymite mėgstamais, atsiras čia", + "Text file" : "Teksto failas", + "New text file.txt" : "Naujas tekstas file.txt" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);"); diff --git a/apps/files/l10n/lt_LT.json b/apps/files/l10n/lt_LT.json index aef54469542..c6e4a35c815 100644 --- a/apps/files/l10n/lt_LT.json +++ b/apps/files/l10n/lt_LT.json @@ -73,8 +73,6 @@ "_%n byte_::_%n bytes_" : ["%n baitas","%n baitai","%n baitų"], "Favorited" : "Pažymėta mėgstamu", "Favorite" : "Mėgiamas", - "Text file" : "Teksto failas", - "New text file.txt" : "Naujas tekstas file.txt", "Folder" : "Katalogas", "New folder" : "Naujas aplankas", "{newname} already exists" : "{newname} jau egzistuoja", @@ -104,6 +102,8 @@ "Maximum upload size" : "Maksimalus įkeliamo failo dydis", "max. possible: " : "maks. galima:", "Save" : "Išsaugoti", + "With PHP-FPM it might take 5 minutes for changes to be applied." : "Su PHP-FPM atnaujinimai gali užtrukti apie 5min.", + "Missing permissions to edit from here." : "Draudžiama iš čia redaguoti", "Settings" : "Nustatymai", "WebDAV" : "WebDAV", "Use this address to <a href=\"%s\" target=\"_blank\">access your Files via WebDAV</a>" : "Naudokite šį adresą, kad <a href=\"%s\" target=\"_blank\">pasiektumėte savo failus per WebDAV</a>", @@ -117,6 +117,8 @@ "Files are being scanned, please wait." : "Skenuojami failai, prašome palaukti.", "Currently scanning" : "Šiuo metu skenuojama", "No favorites" : "Nėra mėgstamiausių", - "Files and folders you mark as favorite will show up here" : "Failai ir aplankai, kuriuos pažymite mėgstamais, atsiras čia" + "Files and folders you mark as favorite will show up here" : "Failai ir aplankai, kuriuos pažymite mėgstamais, atsiras čia", + "Text file" : "Teksto failas", + "New text file.txt" : "Naujas tekstas file.txt" },"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);" }
\ No newline at end of file diff --git a/apps/files/l10n/nds.js b/apps/files/l10n/nds.js index 9b28220ae43..37043dbe6c1 100644 --- a/apps/files/l10n/nds.js +++ b/apps/files/l10n/nds.js @@ -3,7 +3,11 @@ OC.L10N.register( { "Files" : "Dateien", "Delete" : "Löschen", + "Details" : "Details", + "Error" : "Fehler", "Name" : "Name", + "New folder" : "Neuer Ordner", + "Upload" : "Hochladen", "Settings" : "Einstellungen", "WebDAV" : "WebDAV" }, diff --git a/apps/files/l10n/nds.json b/apps/files/l10n/nds.json index 4ab8de68b35..dbd6bad9573 100644 --- a/apps/files/l10n/nds.json +++ b/apps/files/l10n/nds.json @@ -1,7 +1,11 @@ { "translations": { "Files" : "Dateien", "Delete" : "Löschen", + "Details" : "Details", + "Error" : "Fehler", "Name" : "Name", + "New folder" : "Neuer Ordner", + "Upload" : "Hochladen", "Settings" : "Einstellungen", "WebDAV" : "WebDAV" },"pluralForm" :"nplurals=2; plural=(n != 1);" diff --git a/apps/files/l10n/nl.js b/apps/files/l10n/nl.js index d60598ca147..88eb77c02a5 100644 --- a/apps/files/l10n/nl.js +++ b/apps/files/l10n/nl.js @@ -105,6 +105,8 @@ OC.L10N.register( "Maximum upload size" : "Maximale bestandsgrootte voor uploads", "max. possible: " : "max. mogelijk: ", "Save" : "Bewaren", + "With PHP-FPM it might take 5 minutes for changes to be applied." : "Met PHP-FPM kan het 5 minuten duren voordat wijzigingen zijn doorgevoerd.", + "Missing permissions to edit from here." : "Ontbrekende rechten om vanaf hier te bewerken.", "Settings" : "Instellingen", "WebDAV" : "WebDAV", "Use this address to <a href=\"%s\" target=\"_blank\">access your Files via WebDAV</a>" : "Gebruik deze link <a href=\"%s\" target=\"_blank\">om uw bestanden via WebDAV te benaderen</a>", diff --git a/apps/files/l10n/nl.json b/apps/files/l10n/nl.json index 28f80c09af7..0eca44aaa0d 100644 --- a/apps/files/l10n/nl.json +++ b/apps/files/l10n/nl.json @@ -103,6 +103,8 @@ "Maximum upload size" : "Maximale bestandsgrootte voor uploads", "max. possible: " : "max. mogelijk: ", "Save" : "Bewaren", + "With PHP-FPM it might take 5 minutes for changes to be applied." : "Met PHP-FPM kan het 5 minuten duren voordat wijzigingen zijn doorgevoerd.", + "Missing permissions to edit from here." : "Ontbrekende rechten om vanaf hier te bewerken.", "Settings" : "Instellingen", "WebDAV" : "WebDAV", "Use this address to <a href=\"%s\" target=\"_blank\">access your Files via WebDAV</a>" : "Gebruik deze link <a href=\"%s\" target=\"_blank\">om uw bestanden via WebDAV te benaderen</a>", diff --git a/apps/files/l10n/ru.js b/apps/files/l10n/ru.js index 254e701a17c..29f46270af6 100644 --- a/apps/files/l10n/ru.js +++ b/apps/files/l10n/ru.js @@ -75,8 +75,6 @@ OC.L10N.register( "_%n byte_::_%n bytes_" : ["%n байт","%n байта","%n байтов","%n байта(ов)"], "Favorited" : "Избранное", "Favorite" : "Избранное", - "Text file" : "Текстовый файл", - "New text file.txt" : "Новый текстовый документ.txt", "Folder" : "Каталог", "New folder" : "Новый каталог", "{newname} already exists" : "{newname} уже существует", @@ -119,6 +117,8 @@ OC.L10N.register( "Files are being scanned, please wait." : "Идет сканирование файлов. Пожалуйста подождите.", "Currently scanning" : "В настоящее время сканируется", "No favorites" : "Нет избранного", - "Files and folders you mark as favorite will show up here" : "Здесь появятся файлы и каталоги, отмеченные как избранные" + "Files and folders you mark as favorite will show up here" : "Здесь появятся файлы и каталоги, отмеченные как избранные", + "Text file" : "Текстовый файл", + "New text file.txt" : "Новый текстовый документ.txt" }, "nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);"); diff --git a/apps/files/l10n/ru.json b/apps/files/l10n/ru.json index 7666ebb75d4..19999734621 100644 --- a/apps/files/l10n/ru.json +++ b/apps/files/l10n/ru.json @@ -73,8 +73,6 @@ "_%n byte_::_%n bytes_" : ["%n байт","%n байта","%n байтов","%n байта(ов)"], "Favorited" : "Избранное", "Favorite" : "Избранное", - "Text file" : "Текстовый файл", - "New text file.txt" : "Новый текстовый документ.txt", "Folder" : "Каталог", "New folder" : "Новый каталог", "{newname} already exists" : "{newname} уже существует", @@ -117,6 +115,8 @@ "Files are being scanned, please wait." : "Идет сканирование файлов. Пожалуйста подождите.", "Currently scanning" : "В настоящее время сканируется", "No favorites" : "Нет избранного", - "Files and folders you mark as favorite will show up here" : "Здесь появятся файлы и каталоги, отмеченные как избранные" + "Files and folders you mark as favorite will show up here" : "Здесь появятся файлы и каталоги, отмеченные как избранные", + "Text file" : "Текстовый файл", + "New text file.txt" : "Новый текстовый документ.txt" },"pluralForm" :"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);" }
\ No newline at end of file diff --git a/apps/files/l10n/th_TH.js b/apps/files/l10n/th_TH.js index 1194c11ced1..79747b68ee9 100644 --- a/apps/files/l10n/th_TH.js +++ b/apps/files/l10n/th_TH.js @@ -75,8 +75,6 @@ OC.L10N.register( "_%n byte_::_%n bytes_" : ["%n ไบต์"], "Favorited" : "รายการโปรด", "Favorite" : "รายการโปรด", - "Text file" : "ไฟล์ข้อความ", - "New text file.txt" : "ไฟล์ข้อความใหม่ .txt", "Folder" : "แฟ้มเอกสาร", "New folder" : "โฟลเดอร์ใหม่", "{newname} already exists" : "{newname} ถูกใช้ไปแล้ว", @@ -106,6 +104,8 @@ OC.L10N.register( "Maximum upload size" : "ขนาดไฟล์สูงสุดที่อัพโหลดได้", "max. possible: " : "จำนวนสูงสุดที่สามารถทำได้: ", "Save" : "บันทึก", + "With PHP-FPM it might take 5 minutes for changes to be applied." : "กับ PHP-FPM อาจใช้เวลา 5 นาทีสำหรับการเปลี่ยนแปลงที่ถูกนำมาใช้", + "Missing permissions to edit from here." : "สิทธิ์ในการแก้ไขส่วนนี้หายไป", "Settings" : "ตั้งค่า", "WebDAV" : "WebDAV", "Use this address to <a href=\"%s\" target=\"_blank\">access your Files via WebDAV</a>" : "ใช้ที่อยู่นี้เพื่อ <a href=\"%s\" target=\"_blank\">เข้าถึงไฟล์ของคุณผ่าน WebDAV</a>", @@ -119,6 +119,8 @@ OC.L10N.register( "Files are being scanned, please wait." : "ไฟล์กำลังอยู่ระหว่างการสแกน, กรุณารอสักครู่.", "Currently scanning" : "ปัจจุบันกำลังสแกน", "No favorites" : "ไม่มีรายการโปรด", - "Files and folders you mark as favorite will show up here" : "ไฟล์และโฟลเดอร์ที่คุณทำเครื่องหมายเป็นรายการโปรดจะปรากฏขึ้นที่นี่" + "Files and folders you mark as favorite will show up here" : "ไฟล์และโฟลเดอร์ที่คุณทำเครื่องหมายเป็นรายการโปรดจะปรากฏขึ้นที่นี่", + "Text file" : "ไฟล์ข้อความ", + "New text file.txt" : "ไฟล์ข้อความใหม่ .txt" }, "nplurals=1; plural=0;"); diff --git a/apps/files/l10n/th_TH.json b/apps/files/l10n/th_TH.json index 80f80e54ddf..327e2d9eb82 100644 --- a/apps/files/l10n/th_TH.json +++ b/apps/files/l10n/th_TH.json @@ -73,8 +73,6 @@ "_%n byte_::_%n bytes_" : ["%n ไบต์"], "Favorited" : "รายการโปรด", "Favorite" : "รายการโปรด", - "Text file" : "ไฟล์ข้อความ", - "New text file.txt" : "ไฟล์ข้อความใหม่ .txt", "Folder" : "แฟ้มเอกสาร", "New folder" : "โฟลเดอร์ใหม่", "{newname} already exists" : "{newname} ถูกใช้ไปแล้ว", @@ -104,6 +102,8 @@ "Maximum upload size" : "ขนาดไฟล์สูงสุดที่อัพโหลดได้", "max. possible: " : "จำนวนสูงสุดที่สามารถทำได้: ", "Save" : "บันทึก", + "With PHP-FPM it might take 5 minutes for changes to be applied." : "กับ PHP-FPM อาจใช้เวลา 5 นาทีสำหรับการเปลี่ยนแปลงที่ถูกนำมาใช้", + "Missing permissions to edit from here." : "สิทธิ์ในการแก้ไขส่วนนี้หายไป", "Settings" : "ตั้งค่า", "WebDAV" : "WebDAV", "Use this address to <a href=\"%s\" target=\"_blank\">access your Files via WebDAV</a>" : "ใช้ที่อยู่นี้เพื่อ <a href=\"%s\" target=\"_blank\">เข้าถึงไฟล์ของคุณผ่าน WebDAV</a>", @@ -117,6 +117,8 @@ "Files are being scanned, please wait." : "ไฟล์กำลังอยู่ระหว่างการสแกน, กรุณารอสักครู่.", "Currently scanning" : "ปัจจุบันกำลังสแกน", "No favorites" : "ไม่มีรายการโปรด", - "Files and folders you mark as favorite will show up here" : "ไฟล์และโฟลเดอร์ที่คุณทำเครื่องหมายเป็นรายการโปรดจะปรากฏขึ้นที่นี่" + "Files and folders you mark as favorite will show up here" : "ไฟล์และโฟลเดอร์ที่คุณทำเครื่องหมายเป็นรายการโปรดจะปรากฏขึ้นที่นี่", + "Text file" : "ไฟล์ข้อความ", + "New text file.txt" : "ไฟล์ข้อความใหม่ .txt" },"pluralForm" :"nplurals=1; plural=0;" }
\ No newline at end of file diff --git a/apps/files/l10n/tr.js b/apps/files/l10n/tr.js index b2c4fd56df9..e95f458b304 100644 --- a/apps/files/l10n/tr.js +++ b/apps/files/l10n/tr.js @@ -75,8 +75,6 @@ OC.L10N.register( "_%n byte_::_%n bytes_" : ["%n bayt","%n bayt"], "Favorited" : "Sık kullanılanlara eklendi", "Favorite" : "Sık kullanılan", - "Text file" : "Metin dosyası", - "New text file.txt" : "Yeni metin dosyası.txt", "Folder" : "Klasör", "New folder" : "Yeni klasör", "{newname} already exists" : "{newname} zaten mevcut", @@ -106,6 +104,8 @@ OC.L10N.register( "Maximum upload size" : "Azami yükleme boyutu", "max. possible: " : "mümkün olan en fazla: ", "Save" : "Kaydet", + "With PHP-FPM it might take 5 minutes for changes to be applied." : "PHP-FPM ile değişikliklerin uygulanması 5 dakika sürebilir.", + "Missing permissions to edit from here." : "Buradan düzenleme için eksik yetki.", "Settings" : "Ayarlar", "WebDAV" : "WebDAV", "Use this address to <a href=\"%s\" target=\"_blank\">access your Files via WebDAV</a>" : "<a href=\"%s\" target=\"_blank\">Dosyalarınıza WebDAV aracılığıyla erişmek için</a> bu adresi kullanın", @@ -119,6 +119,8 @@ OC.L10N.register( "Files are being scanned, please wait." : "Dosyalar taranıyor, lütfen bekleyin.", "Currently scanning" : "Şu anda taranan", "No favorites" : "Sık kullanılan öge yok.", - "Files and folders you mark as favorite will show up here" : "Sık kullanılan olarak işaretlediğiniz dosya ve klasörler burada gösterilecek" + "Files and folders you mark as favorite will show up here" : "Sık kullanılan olarak işaretlediğiniz dosya ve klasörler burada gösterilecek", + "Text file" : "Metin dosyası", + "New text file.txt" : "Yeni metin dosyası.txt" }, "nplurals=2; plural=(n > 1);"); diff --git a/apps/files/l10n/tr.json b/apps/files/l10n/tr.json index fcb87d74fa1..1bcb92501b9 100644 --- a/apps/files/l10n/tr.json +++ b/apps/files/l10n/tr.json @@ -73,8 +73,6 @@ "_%n byte_::_%n bytes_" : ["%n bayt","%n bayt"], "Favorited" : "Sık kullanılanlara eklendi", "Favorite" : "Sık kullanılan", - "Text file" : "Metin dosyası", - "New text file.txt" : "Yeni metin dosyası.txt", "Folder" : "Klasör", "New folder" : "Yeni klasör", "{newname} already exists" : "{newname} zaten mevcut", @@ -104,6 +102,8 @@ "Maximum upload size" : "Azami yükleme boyutu", "max. possible: " : "mümkün olan en fazla: ", "Save" : "Kaydet", + "With PHP-FPM it might take 5 minutes for changes to be applied." : "PHP-FPM ile değişikliklerin uygulanması 5 dakika sürebilir.", + "Missing permissions to edit from here." : "Buradan düzenleme için eksik yetki.", "Settings" : "Ayarlar", "WebDAV" : "WebDAV", "Use this address to <a href=\"%s\" target=\"_blank\">access your Files via WebDAV</a>" : "<a href=\"%s\" target=\"_blank\">Dosyalarınıza WebDAV aracılığıyla erişmek için</a> bu adresi kullanın", @@ -117,6 +117,8 @@ "Files are being scanned, please wait." : "Dosyalar taranıyor, lütfen bekleyin.", "Currently scanning" : "Şu anda taranan", "No favorites" : "Sık kullanılan öge yok.", - "Files and folders you mark as favorite will show up here" : "Sık kullanılan olarak işaretlediğiniz dosya ve klasörler burada gösterilecek" + "Files and folders you mark as favorite will show up here" : "Sık kullanılan olarak işaretlediğiniz dosya ve klasörler burada gösterilecek", + "Text file" : "Metin dosyası", + "New text file.txt" : "Yeni metin dosyası.txt" },"pluralForm" :"nplurals=2; plural=(n > 1);" }
\ No newline at end of file diff --git a/apps/files/templates/appnavigation.php b/apps/files/templates/appnavigation.php index 512300e3a58..d05a02ee7db 100644 --- a/apps/files/templates/appnavigation.php +++ b/apps/files/templates/appnavigation.php @@ -16,9 +16,7 @@ </button> </div> <div id="app-settings-content"> - <h2> - <label for="webdavurl"><?php p($l->t('WebDAV'));?></label> - </h2> + <label for="webdavurl"><?php p($l->t('WebDAV'));?></label> <input id="webdavurl" type="text" readonly="readonly" value="<?php p(\OCP\Util::linkToRemote('webdav')); ?>" /> <em><?php print_unescaped($l->t('Use this address to <a href="%s" target="_blank">access your Files via WebDAV</a>', array(link_to_docs('user-webdav'))));?></em> </div> diff --git a/apps/files/tests/controller/ViewControllerTest.php b/apps/files/tests/controller/ViewControllerTest.php new file mode 100644 index 00000000000..028dfce8c58 --- /dev/null +++ b/apps/files/tests/controller/ViewControllerTest.php @@ -0,0 +1,250 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * + * @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\Tests\Controller; + +use OCA\Files\Controller\ViewController; +use OCP\AppFramework\Http; +use OCP\Template; +use Test\TestCase; +use OCP\IRequest; +use OCP\IURLGenerator; +use OCP\AppFramework\Http\RedirectResponse; +use OCP\INavigationManager; +use OCP\IL10N; +use OCP\IConfig; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; + +/** + * Class ViewControllerTest + * + * @package OCA\Files\Tests\Controller + */ +class ViewControllerTest extends TestCase { + /** @var IRequest */ + private $request; + /** @var IURLGenerator */ + private $urlGenerator; + /** @var INavigationManager */ + private $navigationManager; + /** @var IL10N */ + private $l10n; + /** @var IConfig */ + private $config; + /** @var EventDispatcherInterface */ + private $eventDispatcher; + /** @var ViewController */ + private $viewController; + + public function setUp() { + parent::setUp(); + $this->request = $this->getMock('\OCP\IRequest'); + $this->urlGenerator = $this->getMock('\OCP\IURLGenerator'); + $this->navigationManager = $this->getMock('\OCP\INavigationManager'); + $this->l10n = $this->getMock('\OCP\IL10N'); + $this->config = $this->getMock('\OCP\IConfig'); + $this->eventDispatcher = $this->getMock('\Symfony\Component\EventDispatcher\EventDispatcherInterface'); + $this->viewController = $this->getMockBuilder('\OCA\Files\Controller\ViewController') + ->setConstructorArgs([ + 'files', + $this->request, + $this->urlGenerator, + $this->navigationManager, + $this->l10n, + $this->config, + $this->eventDispatcher + ]) + ->setMethods([ + 'getStorageInfo', + 'renderScript' + ]) + ->getMock(); + } + + public function testIndexWithIE8RedirectAndDirDefined() { + $this->request + ->expects($this->once()) + ->method('isUserAgent') + ->with(['/MSIE 8.0/']) + ->will($this->returnValue(true)); + $this->urlGenerator + ->expects($this->once()) + ->method('linkToRoute') + ->with('files.view.index') + ->will($this->returnValue('/apps/files/')); + + $expected = new Http\RedirectResponse('/apps/files/#?dir=MyDir'); + $this->assertEquals($expected, $this->viewController->index('MyDir')); + } + + public function testIndexWithIE8RedirectAndViewDefined() { + $this->request + ->expects($this->once()) + ->method('isUserAgent') + ->with(['/MSIE 8.0/']) + ->will($this->returnValue(true)); + $this->urlGenerator + ->expects($this->once()) + ->method('linkToRoute') + ->with('files.view.index') + ->will($this->returnValue('/apps/files/')); + + $expected = new Http\RedirectResponse('/apps/files/#?dir=/&view=MyView'); + $this->assertEquals($expected, $this->viewController->index('', 'MyView')); + } + + public function testIndexWithIE8RedirectAndViewAndDirDefined() { + $this->request + ->expects($this->once()) + ->method('isUserAgent') + ->with(['/MSIE 8.0/']) + ->will($this->returnValue(true)); + $this->urlGenerator + ->expects($this->once()) + ->method('linkToRoute') + ->with('files.view.index') + ->will($this->returnValue('/apps/files/')); + + $expected = new RedirectResponse('/apps/files/#?dir=MyDir&view=MyView'); + $this->assertEquals($expected, $this->viewController->index('MyDir', 'MyView')); + } + + public function testIndexWithRegularBrowser() { + $this->request + ->expects($this->once()) + ->method('isUserAgent') + ->with(['/MSIE 8.0/']) + ->will($this->returnValue(false)); + $this->viewController + ->expects($this->once()) + ->method('getStorageInfo') + ->will($this->returnValue([ + 'relative' => 123, + 'owner' => 'MyName', + 'ownerDisplayName' => 'MyDisplayName', + ])); + + $this->config + ->expects($this->any()) + ->method('getAppValue') + ->will($this->returnArgument(2)); + + $nav = new Template('files', 'appnavigation'); + $nav->assign('navigationItems', [ + 0 => [ + 'id' => 'files', + 'appname' => 'files', + 'script' => 'list.php', + 'order' => 0, + 'name' => new \OC_L10N_String(new \OC_L10N('files'), 'All files', []), + 'active' => false, + 'icon' => '', + ], + 1 => [ + 'id' => 'favorites', + 'appname' => 'files', + 'script' => 'simplelist.php', + 'order' => 5, + 'name' => null, + 'active' => false, + 'icon' => '', + ], + 2 => [ + 'id' => 'sharingin', + 'appname' => 'files_sharing', + 'script' => 'list.php', + 'order' => 10, + 'name' => new \OC_L10N_String(new \OC_L10N('files_sharing'), 'Shared with you', []), + 'active' => false, + 'icon' => '', + ], + 3 => [ + 'id' => 'sharingout', + 'appname' => 'files_sharing', + 'script' => 'list.php', + 'order' => 15, + 'name' => new \OC_L10N_String(new \OC_L10N('files_sharing'), 'Shared with others', []), + 'active' => false, + 'icon' => '', + ], + 4 => [ + 'id' => 'sharinglinks', + 'appname' => 'files_sharing', + 'script' => 'list.php', + 'order' => 20, + 'name' => new \OC_L10N_String(new \OC_L10N('files_sharing'), 'Shared by link', []), + 'active' => false, + 'icon' => '', + ], + 5 => [ + 'id' => 'trashbin', + 'appname' => 'files_trashbin', + 'script' => 'list.php', + 'order' => 50, + 'name' => new \OC_L10N_String(new \OC_L10N('files_trashbin'), 'Deleted files', []), + 'active' => false, + 'icon' => '', + ], + ]); + + $expected = new Http\TemplateResponse( + 'files', + 'index', + [ + 'usedSpacePercent' => 123, + 'owner' => 'MyName', + 'ownerDisplayName' => 'MyDisplayName', + 'isPublic' => false, + 'mailNotificationEnabled' => 'no', + 'mailPublicNotificationEnabled' => 'no', + 'allowShareWithLink' => 'yes', + 'appNavigation' => $nav, + 'appContents' => [ + 0 => [ + 'id' => 'files', + 'content' => null, + ], + 1 => [ + 'id' => 'favorites', + 'content' => null, + ], + 2 => [ + 'id' => 'sharingin', + 'content' => null, + ], + 3 => [ + 'id' => 'sharingout', + 'content' => null, + ], + 4 => [ + 'id' => 'sharinglinks', + 'content' => null, + ], + 5 => [ + 'id' => 'trashbin', + 'content' => null, + ], + ], + ] + ); + $this->assertEquals($expected, $this->viewController->index('MyDir', 'MyView')); + } +} diff --git a/apps/files/tests/js/newfilemenuSpec.js b/apps/files/tests/js/newfilemenuSpec.js index 3d89a997eb2..20f617d24d6 100644 --- a/apps/files/tests/js/newfilemenuSpec.js +++ b/apps/files/tests/js/newfilemenuSpec.js @@ -46,7 +46,7 @@ describe('OCA.Files.NewFileMenu', function() { describe('rendering', function() { it('renders menu items', function() { var $items = menu.$el.find('.menuitem'); - expect($items.length).toEqual(3); + expect($items.length).toEqual(2); // label points to the file_upload_start item var $item = $items.eq(0); expect($item.is('label')).toEqual(true); @@ -55,39 +55,26 @@ describe('OCA.Files.NewFileMenu', function() { }); describe('New file/folder', function() { var $input; - var createFileStub; var createDirectoryStub; beforeEach(function() { - createFileStub = sinon.stub(FileList.prototype, 'createFile'); createDirectoryStub = sinon.stub(FileList.prototype, 'createDirectory'); menu.$el.find('.menuitem').eq(1).click(); $input = menu.$el.find('form.filenameform input'); }); afterEach(function() { - createFileStub.restore(); createDirectoryStub.restore(); }); it('sets default text in field', function() { expect($input.length).toEqual(1); - expect($input.val()).toEqual('New text file.txt'); - }); - it('creates file when enter is pressed', function() { - $input.val('somefile.txt'); - $input.trigger(new $.Event('keyup', {keyCode: 13})); - $input.parent('form').submit(); - - expect(createFileStub.calledOnce).toEqual(true); - expect(createFileStub.getCall(0).args[0]).toEqual('somefile.txt'); - expect(createDirectoryStub.notCalled).toEqual(true); + expect($input.val()).toEqual('New folder'); }); it('prevents entering invalid file names', function() { $input.val('..'); $input.trigger(new $.Event('keyup', {keyCode: 13})); $input.closest('form').submit(); - expect(createFileStub.notCalled).toEqual(true); expect(createDirectoryStub.notCalled).toEqual(true); }); it('prevents entering file names that already exist', function() { @@ -96,16 +83,10 @@ describe('OCA.Files.NewFileMenu', function() { $input.trigger(new $.Event('keyup', {keyCode: 13})); $input.closest('form').submit(); - expect(createFileStub.notCalled).toEqual(true); expect(createDirectoryStub.notCalled).toEqual(true); inListStub.restore(); }); - it('switching fields removes the previous form', function() { - menu.$el.find('.menuitem').eq(2).click(); - expect(menu.$el.find('form').length).toEqual(1); - }); it('creates directory when clicking on create directory field', function() { - menu.$el.find('.menuitem').eq(2).click(); $input = menu.$el.find('form.filenameform input'); $input.val('some folder'); $input.trigger(new $.Event('keyup', {keyCode: 13})); @@ -113,7 +94,55 @@ describe('OCA.Files.NewFileMenu', function() { expect(createDirectoryStub.calledOnce).toEqual(true); expect(createDirectoryStub.getCall(0).args[0]).toEqual('some folder'); - expect(createFileStub.notCalled).toEqual(true); + }); + }); + describe('custom entries', function() { + var oldPlugins; + var plugin; + var actionStub; + + beforeEach(function() { + oldPlugins = _.extend({}, OC.Plugins._plugins); + actionStub = sinon.stub(); + plugin = { + attach: function(menu) { + menu.addMenuEntry({ + id: 'file', + displayName: t('files_texteditor', 'Text file'), + templateName: t('files_texteditor', 'New text file.txt'), + iconClass: 'icon-filetype-text', + fileType: 'file', + actionHandler: actionStub + }); + } + }; + + OC.Plugins.register('OCA.Files.NewFileMenu', plugin); + menu = new OCA.Files.NewFileMenu({ + fileList: fileList + }); + menu.showAt($trigger); + }); + afterEach(function() { + OC.Plugins._plugins = oldPlugins; + }); + it('renders custom menu items', function() { + expect(menu.$el.find('.menuitem').length).toEqual(3); + expect(menu.$el.find('.menuitem[data-action=file]').length).toEqual(1); + }); + it('calls action handler when clicking on custom item', function() { + menu.$el.find('.menuitem').eq(2).click(); + var $input = menu.$el.find('form.filenameform input'); + $input.val('some name'); + $input.trigger(new $.Event('keyup', {keyCode: 13})); + $input.closest('form').submit(); + + expect(actionStub.calledOnce).toEqual(true); + expect(actionStub.getCall(0).args[0]).toEqual('some name'); + }); + it('switching fields removes the previous form', function() { + menu.$el.find('.menuitem').eq(2).click(); + expect(menu.$el.find('form').length).toEqual(1); }); }); }); diff --git a/apps/files_external/l10n/de.js b/apps/files_external/l10n/de.js index c26322495d6..a51cc52a247 100644 --- a/apps/files_external/l10n/de.js +++ b/apps/files_external/l10n/de.js @@ -87,6 +87,6 @@ OC.L10N.register( "Advanced settings" : "Erweiterte Einstellungen", "Delete" : "Löschen", "Add storage" : "Speicher hinzufügen", - "Allow users to mount the following external storage" : "Erlaube es Benutzern, den folgenden externen Speicher einzubinden" + "Allow users to mount the following external storage" : "Benutzern erlauben, den oder die folgenden externen Speicher einzubinden:" }, "nplurals=2; plural=(n != 1);"); diff --git a/apps/files_external/l10n/de.json b/apps/files_external/l10n/de.json index adec32d072b..41ecaad2594 100644 --- a/apps/files_external/l10n/de.json +++ b/apps/files_external/l10n/de.json @@ -85,6 +85,6 @@ "Advanced settings" : "Erweiterte Einstellungen", "Delete" : "Löschen", "Add storage" : "Speicher hinzufügen", - "Allow users to mount the following external storage" : "Erlaube es Benutzern, den folgenden externen Speicher einzubinden" + "Allow users to mount the following external storage" : "Benutzern erlauben, den oder die folgenden externen Speicher einzubinden:" },"pluralForm" :"nplurals=2; plural=(n != 1);" }
\ No newline at end of file diff --git a/apps/files_external/l10n/de_DE.js b/apps/files_external/l10n/de_DE.js index c30fcb95919..610f737a4a8 100644 --- a/apps/files_external/l10n/de_DE.js +++ b/apps/files_external/l10n/de_DE.js @@ -70,6 +70,6 @@ OC.L10N.register( "Advanced settings" : "Erweiterte Einstellungen", "Delete" : "Löschen", "Add storage" : "Speicher hinzufügen", - "Allow users to mount the following external storage" : "Erlauben Sie Benutzern, folgende externe Speicher einzubinden" + "Allow users to mount the following external storage" : "Benutzern erlauben, den oder die folgenden externen Speicher einzubinden:" }, "nplurals=2; plural=(n != 1);"); diff --git a/apps/files_external/l10n/de_DE.json b/apps/files_external/l10n/de_DE.json index 7fec06bf90f..fa21fea8f4a 100644 --- a/apps/files_external/l10n/de_DE.json +++ b/apps/files_external/l10n/de_DE.json @@ -68,6 +68,6 @@ "Advanced settings" : "Erweiterte Einstellungen", "Delete" : "Löschen", "Add storage" : "Speicher hinzufügen", - "Allow users to mount the following external storage" : "Erlauben Sie Benutzern, folgende externe Speicher einzubinden" + "Allow users to mount the following external storage" : "Benutzern erlauben, den oder die folgenden externen Speicher einzubinden:" },"pluralForm" :"nplurals=2; plural=(n != 1);" }
\ No newline at end of file diff --git a/apps/files_external/l10n/hy.js b/apps/files_external/l10n/hy.js index 9996681f88b..1092d48d575 100644 --- a/apps/files_external/l10n/hy.js +++ b/apps/files_external/l10n/hy.js @@ -2,11 +2,14 @@ OC.L10N.register( "files_external", { "Personal" : "Անձնական", + "Never" : "Երբեք", + "Username" : "Օգտանուն", "Password" : "Գաղտնաբառ", "URL" : "URL", "Dropbox" : "Dropbox", "Share" : "Կիսվել", "Name" : "Անուն", + "Folder name" : "Պանակի անուն", "Delete" : "Ջնջել" }, "nplurals=2; plural=(n != 1);"); diff --git a/apps/files_external/l10n/hy.json b/apps/files_external/l10n/hy.json index ac5a0edf013..1fecd4f3da1 100644 --- a/apps/files_external/l10n/hy.json +++ b/apps/files_external/l10n/hy.json @@ -1,10 +1,13 @@ { "translations": { "Personal" : "Անձնական", + "Never" : "Երբեք", + "Username" : "Օգտանուն", "Password" : "Գաղտնաբառ", "URL" : "URL", "Dropbox" : "Dropbox", "Share" : "Կիսվել", "Name" : "Անուն", + "Folder name" : "Պանակի անուն", "Delete" : "Ջնջել" },"pluralForm" :"nplurals=2; plural=(n != 1);" }
\ No newline at end of file diff --git a/apps/files_external/l10n/lt_LT.js b/apps/files_external/l10n/lt_LT.js index b6d334d792d..78d03a865f2 100644 --- a/apps/files_external/l10n/lt_LT.js +++ b/apps/files_external/l10n/lt_LT.js @@ -1,6 +1,7 @@ OC.L10N.register( "files_external", { + "Fetching request tokens failed. Verify that your app key and secret are correct." : "Nepavyko atsiųsti užklausos žymės. Patikrinkite savo programos raktą ir paslaptį.", "External storage" : "Išorinė saugykla", "Personal" : "Asmeniniai", "Grant access" : "Suteikti priėjimą", diff --git a/apps/files_external/l10n/lt_LT.json b/apps/files_external/l10n/lt_LT.json index f1c46b145ee..fcb1f1f39bd 100644 --- a/apps/files_external/l10n/lt_LT.json +++ b/apps/files_external/l10n/lt_LT.json @@ -1,4 +1,5 @@ { "translations": { + "Fetching request tokens failed. Verify that your app key and secret are correct." : "Nepavyko atsiųsti užklausos žymės. Patikrinkite savo programos raktą ir paslaptį.", "External storage" : "Išorinė saugykla", "Personal" : "Asmeniniai", "Grant access" : "Suteikti priėjimą", diff --git a/apps/files_external/l10n/tr.js b/apps/files_external/l10n/tr.js index 619e5975ed8..9d46012454b 100644 --- a/apps/files_external/l10n/tr.js +++ b/apps/files_external/l10n/tr.js @@ -16,6 +16,7 @@ OC.L10N.register( "Not permitted to use authentication mechanism \"%s\"" : "\"%s\" kimlik doğrulama mekanizmasına izin verilmiyor", "Unsatisfied backend parameters" : "Yetersiz arka uç parametreleri", "Unsatisfied authentication mechanism parameters" : "Yetersiz kimlik doğrulama mekanizması parametreleri", + "Insufficient data: %s" : "Yetersiz veri: %s", "Personal" : "Kişisel", "System" : "Sistem", "Grant access" : "Erişimi sağla", @@ -101,6 +102,7 @@ OC.L10N.register( "Advanced settings" : "Gelişmiş ayarlar", "Delete" : "Sil", "Add storage" : "Depo ekle", + "Allow users to mount external storage" : "Kullanıcılara harici depolama bağlama izin ver", "Allow users to mount the following external storage" : "Kullanıcıların aşağıdaki harici depolamayı bağlamalarına izin ver" }, "nplurals=2; plural=(n > 1);"); diff --git a/apps/files_external/l10n/tr.json b/apps/files_external/l10n/tr.json index cb315a333c2..17c76b1916e 100644 --- a/apps/files_external/l10n/tr.json +++ b/apps/files_external/l10n/tr.json @@ -14,6 +14,7 @@ "Not permitted to use authentication mechanism \"%s\"" : "\"%s\" kimlik doğrulama mekanizmasına izin verilmiyor", "Unsatisfied backend parameters" : "Yetersiz arka uç parametreleri", "Unsatisfied authentication mechanism parameters" : "Yetersiz kimlik doğrulama mekanizması parametreleri", + "Insufficient data: %s" : "Yetersiz veri: %s", "Personal" : "Kişisel", "System" : "Sistem", "Grant access" : "Erişimi sağla", @@ -99,6 +100,7 @@ "Advanced settings" : "Gelişmiş ayarlar", "Delete" : "Sil", "Add storage" : "Depo ekle", + "Allow users to mount external storage" : "Kullanıcılara harici depolama bağlama izin ver", "Allow users to mount the following external storage" : "Kullanıcıların aşağıdaki harici depolamayı bağlamalarına izin ver" },"pluralForm" :"nplurals=2; plural=(n > 1);" }
\ No newline at end of file diff --git a/apps/files_external/lib/sftp.php b/apps/files_external/lib/sftp.php index 5dcc7686ca3..f8651727fd2 100644 --- a/apps/files_external/lib/sftp.php +++ b/apps/files_external/lib/sftp.php @@ -52,27 +52,37 @@ class SFTP extends \OC\Files\Storage\Common { protected $client; /** + * @param string $host protocol://server:port + * @return array [$server, $port] + */ + private function splitHost($host) { + $input = $host; + if (strpos($host, '://') === false) { + // add a protocol to fix parse_url behavior with ipv6 + $host = 'http://' . $host; + } + + $parsed = parse_url($host); + if(is_array($parsed) && isset($parsed['port'])) { + return [$parsed['host'], $parsed['port']]; + } else if (is_array($parsed)) { + return [$parsed['host'], 22]; + } else { + return [$input, 22]; + } + } + + /** * {@inheritdoc} */ public function __construct($params) { // Register sftp:// Stream::register(); - $this->host = $params['host']; + $parsedHost = $this->splitHost($params['host']); - //deals with sftp://server example - $proto = strpos($this->host, '://'); - if ($proto != false) { - $this->host = substr($this->host, $proto+3); - } - - //deals with server:port - $hasPort = strpos($this->host,':'); - if($hasPort != false) { - $pieces = explode(":", $this->host); - $this->host = $pieces[0]; - $this->port = $pieces[1]; - } + $this->host = $parsedHost[0]; + $this->port = $parsedHost[1]; $this->user = $params['user']; diff --git a/apps/files_external/service/storagesservice.php b/apps/files_external/service/storagesservice.php index 41bb0ca9b80..3446ed0dab3 100644 --- a/apps/files_external/service/storagesservice.php +++ b/apps/files_external/service/storagesservice.php @@ -221,17 +221,26 @@ abstract class StoragesService { $currentStorage->setMountPoint($relativeMountPath); } - $this->populateStorageConfigWithLegacyOptions( - $currentStorage, - $mountType, - $applicable, - $storageOptions - ); - - if ($hasId) { - $storages[$configId] = $currentStorage; - } else { - $storagesWithConfigHash[$configId] = $currentStorage; + try { + $this->populateStorageConfigWithLegacyOptions( + $currentStorage, + $mountType, + $applicable, + $storageOptions + ); + + if ($hasId) { + $storages[$configId] = $currentStorage; + } else { + $storagesWithConfigHash[$configId] = $currentStorage; + } + } catch (\UnexpectedValueException $e) { + // dont die if a storage backend doesn't exist + \OCP\Util::writeLog( + 'files_external', + 'Could not load storage: "' . $e->getMessage() . '"', + \OCP\Util::ERROR + ); } } } diff --git a/apps/files_external/tests/backends/sftp.php b/apps/files_external/tests/backends/sftp.php index da2c0ac6ba2..aaed2b3460a 100644 --- a/apps/files_external/tests/backends/sftp.php +++ b/apps/files_external/tests/backends/sftp.php @@ -26,6 +26,11 @@ namespace Test\Files\Storage; class SFTP extends Storage { + /** + * @var \OC\Files\Storage\SFTP instance + */ + protected $instance; + private $config; protected function setUp() { @@ -103,6 +108,39 @@ class SFTP extends Storage { ], 'sftp::someuser@somehost:8822//remotedir/subdir/', ], + [ + // ipv6 with port + [ + 'run' => true, + 'host' => 'FE80:0000:0000:0000:0202:B3FF:FE1E:8329', + 'user' => 'someuser', + 'password' => 'somepassword', + 'root' => 'remotedir/subdir/', + ], + 'sftp::someuser@FE80:0000:0000:0000:0202:B3FF:FE1E:8329//remotedir/subdir/', + ], + [ + // ipv6 without port + [ + 'run' => true, + 'host' => 'FE80:0000:0000:0000:0202:B3FF:FE1E:8329:8822', + 'user' => 'someuser', + 'password' => 'somepassword', + 'root' => 'remotedir/subdir/', + ], + 'sftp::someuser@FE80:0000:0000:0000:0202:B3FF:FE1E:8329:8822//remotedir/subdir/', + ], + [ + // collapsed ipv6 with port + [ + 'run' => true, + 'host' => 'FE80::0202:B3FF:FE1E:8329:8822', + 'user' => 'someuser', + 'password' => 'somepassword', + 'root' => 'remotedir/subdir/', + ], + 'sftp::someuser@FE80::0202:B3FF:FE1E:8329:8822//remotedir/subdir/', + ], ]; } } diff --git a/apps/files_external/tests/backends/swift.php b/apps/files_external/tests/backends/swift.php index 2e6670f84f8..07ee36043b1 100644 --- a/apps/files_external/tests/backends/swift.php +++ b/apps/files_external/tests/backends/swift.php @@ -32,28 +32,35 @@ class Swift extends Storage { protected function setUp() { parent::setUp(); - $this->config = include('files_external/tests/config.php'); - if (!is_array($this->config) or !isset($this->config['swift']) - or !$this->config['swift']['run']) { + $this->config = include('files_external/tests/config.swift.php'); + if (!is_array($this->config) or !$this->config['run']) { $this->markTestSkipped('OpenStack Object Storage backend not configured'); } - $this->instance = new \OC\Files\Storage\Swift($this->config['swift']); + $this->instance = new \OC\Files\Storage\Swift($this->config); } protected function tearDown() { if ($this->instance) { - $connection = $this->instance->getConnection(); - $container = $connection->getContainer($this->config['swift']['bucket']); + try { + $connection = $this->instance->getConnection(); + $container = $connection->getContainer($this->config['bucket']); - $objects = $container->objectList(); - while($object = $objects->next()) { - $object->setName(str_replace('#','%23',$object->getName())); - $object->delete(); - } + $objects = $container->objectList(); + while($object = $objects->next()) { + $object->setName(str_replace('#','%23',$object->getName())); + $object->delete(); + } - $container->delete(); + $container->delete(); + } catch (\Guzzle\Http\Exception\ClientErrorResponseException $e) { + // container didn't exist, so we don't need to delete it + } } parent::tearDown(); } + + public function testStat() { + $this->markTestSkipped('Swift doesn\'t update the parents folder mtime'); + } } diff --git a/apps/files_external/tests/env/start-amazons3-ceph.sh b/apps/files_external/tests/env/start-amazons3-ceph.sh index 3b4a15da92a..b40d28f1ec6 100755 --- a/apps/files_external/tests/env/start-amazons3-ceph.sh +++ b/apps/files_external/tests/env/start-amazons3-ceph.sh @@ -49,9 +49,21 @@ echo "${docker_image} container: $container" # put container IDs into a file to drop them after the test run (keep in mind that multiple tests run in parallel on the same host) echo $container >> $thisFolder/dockerContainerCeph.$EXECUTOR_NUMBER.amazons3 -# TODO find a way to determine the successful initialization inside the docker container -echo "Waiting 20 seconds for ceph initialization ... " -sleep 20 +echo -n "Waiting for ceph initialization" +starttime=$(date +%s) +# support for GNU netcat and BSD netcat +while ! (nc -c -w 1 ${host} ${port} </dev/null >&/dev/null \ + || nc -w 1 ${host} ${port} </dev/null >&/dev/null); do + sleep 1 + echo -n '.' + if (( $(date +%s) > starttime + 60 )); then + echo + echo "[ERROR] Waited 60 seconds, no response" >&2 + exit 1 + fi +done +echo +sleep 1 echo "Create ceph user" docker exec $container radosgw-admin user create \ diff --git a/apps/files_external/tests/env/start-ftp-morrisjobke.sh b/apps/files_external/tests/env/start-ftp-morrisjobke.sh index f1ab7f69952..3c6cc62bce8 100755 --- a/apps/files_external/tests/env/start-ftp-morrisjobke.sh +++ b/apps/files_external/tests/env/start-ftp-morrisjobke.sh @@ -54,12 +54,25 @@ echo "ftp container: $container" # put container IDs into a file to drop them after the test run (keep in mind that multiple tests run in parallel on the same host) echo $container >> $thisFolder/dockerContainerMorrisJobke.$EXECUTOR_NUMBER.ftp +echo -n "Waiting for ftp initialization" +starttime=$(date +%s) +# support for GNU netcat and BSD netcat +while ! (nc -c -w 1 ${host} 21 </dev/null >&/dev/null \ + || nc -w 1 ${host} 21 </dev/null >&/dev/null); do + sleep 1 + echo -n '.' + if (( $(date +%s) > starttime + 60 )); then + echo + echo "[ERROR] Waited 60 seconds, no response" >&2 + exit 1 + fi +done +echo +sleep 1 + if [ -n "$DEBUG" ]; then cat $thisFolder/config.ftp.php cat $thisFolder/dockerContainerMorrisJobke.$EXECUTOR_NUMBER.ftp fi -# TODO find a way to determine the successful initialization inside the docker container -echo "Waiting 5 seconds for ftp initialization ... " -sleep 5 diff --git a/apps/files_external/tests/env/start-sftp-atmoz.sh b/apps/files_external/tests/env/start-sftp-atmoz.sh index bebc7289250..3e0616f03d2 100755 --- a/apps/files_external/tests/env/start-sftp-atmoz.sh +++ b/apps/files_external/tests/env/start-sftp-atmoz.sh @@ -54,15 +54,27 @@ echo "sftp container: $container" # put container IDs into a file to drop them after the test run (keep in mind that multiple tests run in parallel on the same host) echo $container >> $thisFolder/dockerContainerAtmoz.$EXECUTOR_NUMBER.sftp +echo -n "Waiting for sftp initialization" +starttime=$(date +%s) +# support for GNU netcat and BSD netcat +while ! (nc -c -w 1 ${host} 22 </dev/null >&/dev/null \ + || nc -w 1 ${host} 22 </dev/null >&/dev/null); do + sleep 1 + echo -n '.' + if (( $(date +%s) > starttime + 60 )); then + echo + echo "[ERROR] Waited 60 seconds, no response" >&2 + exit 1 + fi +done +echo +sleep 1 + if [ -n "$DEBUG" ]; then cat $thisFolder/config.sftp.php cat $thisFolder/dockerContainerAtmoz.$EXECUTOR_NUMBER.sftp fi -# TODO find a way to determine the successful initialization inside the docker container -echo "Waiting 5 seconds for sftp initialization ... " -sleep 5 - # create folder "upload" with correct permissions docker exec $container bash -c "mkdir /home/$user/upload && chown $user:users /home/$user/upload" diff --git a/apps/files_external/tests/env/start-smb-silvershell.sh b/apps/files_external/tests/env/start-smb-silvershell.sh index 41ba3b11a70..31e3da44646 100755 --- a/apps/files_external/tests/env/start-smb-silvershell.sh +++ b/apps/files_external/tests/env/start-smb-silvershell.sh @@ -52,12 +52,25 @@ echo "samba container: $container" # put container IDs into a file to drop them after the test run (keep in mind that multiple tests run in parallel on the same host) echo $container >> $thisFolder/dockerContainerSilvershell.$EXECUTOR_NUMBER.smb +echo -n "Waiting for samba initialization" +starttime=$(date +%s) +# support for GNU netcat and BSD netcat +while ! (nc -c -w 1 ${host} 445 </dev/null >&/dev/null \ + || nc -w 1 ${host} 445 </dev/null >&/dev/null); do + sleep 1 + echo -n '.' + if (( $(date +%s) > starttime + 60 )); then + echo + echo "[ERROR] Waited 60 seconds, no response" >&2 + exit 1 + fi +done +echo +sleep 1 + if [ -n "$DEBUG" ]; then cat $thisFolder/config.smb.php cat $thisFolder/dockerContainerSilvershell.$EXECUTOR_NUMBER.smb fi -# TODO find a way to determine the successful initialization inside the docker container -echo "Waiting 5 seconds for smbd initialization ... " -sleep 5 diff --git a/apps/files_external/tests/env/start-smb-windows.sh b/apps/files_external/tests/env/start-smb-windows.sh index 2143d7e7499..6779cdb2d56 100755 --- a/apps/files_external/tests/env/start-smb-windows.sh +++ b/apps/files_external/tests/env/start-smb-windows.sh @@ -19,6 +19,12 @@ user=smb-test password=!owncloud123 host=WIN-9GTFAS08C15 +if ! (nc -c -w 1 ${host} 445 </dev/null >&/dev/null \ + || nc -w 1 ${host} 445 </dev/null >&/dev/null); then + echo "[ERROR] Server not reachable" >&2 + exit 1 +fi + cat > $thisFolder/config.smb.php <<DELIM <?php diff --git a/apps/files_external/tests/env/start-swift-ceph.sh b/apps/files_external/tests/env/start-swift-ceph.sh new file mode 100755 index 00000000000..ea16e167af9 --- /dev/null +++ b/apps/files_external/tests/env/start-swift-ceph.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# +# ownCloud +# +# This script start a docker container to test the files_external tests +# against. It will also change the files_external config to use the docker +# container as testing environment. This is reverted in the stop step.W +# +# Set environment variable DEBUG to print config file +# +# @author Morris Jobke +# @author Robin McCorkell +# @copyright 2015 ownCloud + +if ! command -v docker >/dev/null 2>&1; then + echo "No docker executable found - skipped docker setup" + exit 0; +fi + +echo "Docker executable found - setup docker" + +docker_image=xenopathic/ceph-keystone + +echo "Fetch recent ${docker_image} docker image" +docker pull ${docker_image} + +# retrieve current folder to place the config in the parent folder +thisFolder=`echo $0 | replace "env/start-swift-ceph.sh" ""` + +if [ -z "$thisFolder" ]; then + thisFolder="." +fi; + +port=5001 + +user=test +pass=testing +tenant=testenant +region=testregion +service=testceph + +container=`docker run -d \ + -e KEYSTONE_PUBLIC_PORT=${port} \ + -e KEYSTONE_ADMIN_USER=${user} \ + -e KEYSTONE_ADMIN_PASS=${pass} \ + -e KEYSTONE_ADMIN_TENANT=${tenant} \ + -e KEYSTONE_ENDPOINT_REGION=${region} \ + -e KEYSTONE_SERVICE=${service} \ + ${docker_image}` + +host=`docker inspect --format="{{.NetworkSettings.IPAddress}}" $container` + + +echo "${docker_image} container: $container" + +# put container IDs into a file to drop them after the test run (keep in mind that multiple tests run in parallel on the same host) +echo $container >> $thisFolder/dockerContainerCeph.$EXECUTOR_NUMBER.swift + +echo -n "Waiting for ceph initialization" +starttime=$(date +%s) +# support for GNU netcat and BSD netcat +while ! (nc -c -w 1 ${host} 80 </dev/null >&/dev/null \ + || nc -w 1 ${host} 80 </dev/null >&/dev/null); do + sleep 1 + echo -n '.' + if (( $(date +%s) > starttime + 60 )); then + echo + echo "[ERROR] Waited 60 seconds, no response" >&2 + exit 1 + fi +done +echo +sleep 1 + +cat > $thisFolder/config.swift.php <<DELIM +<?php + +return array( + 'run'=>true, + 'url'=>'http://$host:$port/v2.0', + 'user'=>'$user', + 'tenant'=>'$tenant', + 'password'=>'$pass', + 'service_name'=>'$service', + 'bucket'=>'swift', + 'region' => '$region', +); + +DELIM + +if [ -n "$DEBUG" ]; then + cat $thisFolder/config.swift.php + cat $thisFolder/dockerContainerCeph.$EXECUTOR_NUMBER.swift +fi diff --git a/apps/files_external/tests/env/start-webdav-ownCloud.sh b/apps/files_external/tests/env/start-webdav-ownCloud.sh index 6bf9142ee53..6e3904f2bad 100755 --- a/apps/files_external/tests/env/start-webdav-ownCloud.sh +++ b/apps/files_external/tests/env/start-webdav-ownCloud.sh @@ -46,20 +46,30 @@ fi container=`docker run -P $parameter -d -e ADMINLOGIN=test -e ADMINPWD=test morrisjobke/owncloud` -# TODO find a way to determine the successful initialization inside the docker container -echo "Waiting 30 seconds for ownCloud initialization ... " -sleep 30 - -# get mapped port on host for internal port 80 - output is IP:PORT - we need to extract the port with 'cut' -port=`docker port $container 80 | cut -f 2 -d :` - +host=`docker inspect $container | grep IPAddress | cut -d '"' -f 4` + +echo -n "Waiting for ownCloud initialization" +starttime=$(date +%s) +# support for GNU netcat and BSD netcat +while ! (nc -c -w 1 ${host} 80 </dev/null >&/dev/null \ + || nc -w 1 ${host} 80 </dev/null >&/dev/null); do + sleep 1 + echo -n '.' + if (( $(date +%s) > starttime + 60 )); then + echo + echo "[ERROR] Waited 60 seconds, no response" >&2 + exit 1 + fi +done +echo +sleep 1 cat > $thisFolder/config.webdav.php <<DELIM <?php return array( 'run'=>true, - 'host'=>'localhost:$port/owncloud/remote.php/webdav/', + 'host'=>'${host}:80/owncloud/remote.php/webdav/', 'user'=>'test', 'password'=>'test', 'root'=>'', diff --git a/apps/files_external/tests/env/stop-swift-ceph.sh b/apps/files_external/tests/env/stop-swift-ceph.sh new file mode 100755 index 00000000000..edac1389a78 --- /dev/null +++ b/apps/files_external/tests/env/stop-swift-ceph.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# +# ownCloud +# +# This script stops the docker container the files_external tests were run +# against. It will also revert the config changes done in start step. +# +# @author Morris Jobke +# @author Robin McCorkell +# @copyright 2015 ownCloud + +if ! command -v docker >/dev/null 2>&1; then + echo "No docker executable found - skipped docker stop" + exit 0; +fi + +echo "Docker executable found - stop and remove docker containers" + +# retrieve current folder to remove the config from the parent folder +thisFolder=`echo $0 | replace "env/stop-swift-ceph.sh" ""` + +if [ -z "$thisFolder" ]; then + thisFolder="." +fi; + +# stopping and removing docker containers +for container in `cat $thisFolder/dockerContainerCeph.$EXECUTOR_NUMBER.swift`; do + echo "Stopping and removing docker container $container" + # kills running container and removes it + docker rm -f $container +done; + +# cleanup +rm $thisFolder/config.swift.php +rm $thisFolder/dockerContainerCeph.$EXECUTOR_NUMBER.swift + diff --git a/apps/files_sharing/api/local.php b/apps/files_sharing/api/local.php index bb5136a0c99..aaafafb269f 100644 --- a/apps/files_sharing/api/local.php +++ b/apps/files_sharing/api/local.php @@ -233,6 +233,7 @@ class Local { if (\OC::$server->getPreviewManager()->isMimeSupported($share['mimetype'])) { $share['isPreviewAvailable'] = true; } + unset($share['path']); } } $result = new \OC_OCS_Result($shares); diff --git a/apps/files_sharing/api/ocssharewrapper.php b/apps/files_sharing/api/ocssharewrapper.php index 8c0d8f7d150..3ce2901dfb4 100644 --- a/apps/files_sharing/api/ocssharewrapper.php +++ b/apps/files_sharing/api/ocssharewrapper.php @@ -35,15 +35,16 @@ class OCSShareWrapper { \OC::$server->getUserFolder(), new \OC\Share20\DefaultShareProvider( \OC::$server->getDatabaseConnection(), - \OC::$server->getUserManager(), - \OC::$server->getGroupManager(), - \OC::$server->getUserFolder() + \OC::$server->getUserManager(), + \OC::$server->getGroupManager(), + \OC::$server->getUserFolder() ) ), \OC::$server->getGroupManager(), \OC::$server->getUserManager(), \OC::$server->getRequest(), - \OC::$server->getUserFolder()); + \OC::$server->getUserFolder(), + \OC::$server->getURLGenerator()); } public function getAllShares($params) { @@ -55,7 +56,8 @@ class OCSShareWrapper { } public function getShare($params) { - return \OCA\Files_Sharing\API\Local::getShare($params); + $id = $params['id']; + return $this->getShare20OCS()->getShare($id); } public function updateShare($params) { @@ -63,7 +65,7 @@ class OCSShareWrapper { } public function deleteShare($params) { - $id = (int)$params['id']; + $id = $params['id']; return $this->getShare20OCS()->deleteShare($id); } } diff --git a/apps/files_sharing/api/remote.php b/apps/files_sharing/api/remote.php index 41ebb6e2eab..fb692f8a9a6 100644 --- a/apps/files_sharing/api/remote.php +++ b/apps/files_sharing/api/remote.php @@ -98,7 +98,7 @@ class Remote { */ private static function extendShareInfo($share) { $view = new \OC\Files\View('/' . \OC_User::getUser() . '/files/'); - $info = $view->getFileInfo($shares['mountpoint']); + $info = $view->getFileInfo($share['mountpoint']); $share['mimetype'] = $info->getMimetype(); $share['mtime'] = $info->getMtime(); diff --git a/apps/files_sharing/api/share20ocs.php b/apps/files_sharing/api/share20ocs.php index 8a7f90c0023..aaf5a3c72b6 100644 --- a/apps/files_sharing/api/share20ocs.php +++ b/apps/files_sharing/api/share20ocs.php @@ -20,39 +20,125 @@ */ namespace OCA\Files_Sharing\API; +use OC\Share20\IShare; + class Share20OCS { - /** @var OC\Share20\Manager */ + /** @var \OC\Share20\Manager */ private $shareManager; - /** @var OCP\IGroupManager */ + /** @var \OCP\IGroupManager */ private $groupManager; - /** @var OCP\IUserManager */ + /** @var \OCP\IUserManager */ private $userManager; - /** @var OCP\IRequest */ + /** @var \OCP\IRequest */ private $request; - /** @var OCP\Files\Folder */ + /** @var \OCP\Files\Folder */ private $userFolder; public function __construct(\OC\Share20\Manager $shareManager, \OCP\IGroupManager $groupManager, \OCP\IUserManager $userManager, \OCP\IRequest $request, - \OCP\Files\Folder $userFolder) { + \OCP\Files\Folder $userFolder, + \OCP\IURLGenerator $urlGenerator) { $this->shareManager = $shareManager; $this->userManager = $userManager; $this->groupManager = $groupManager; $this->request = $request; $this->userFolder = $userFolder; + $this->urlGenerator = $urlGenerator; + } + + /** + * Convert an IShare to an array for OCS output + * + * @param IShare $share + * @return array + */ + protected function formatShare($share) { + $result = [ + 'id' => $share->getId(), + 'share_type' => $share->getShareType(), + 'uid_owner' => $share->getSharedBy()->getUID(), + 'displayname_owner' => $share->getSharedBy()->getDisplayName(), + 'permissions' => $share->getPermissions(), + 'stime' => $share->getShareTime(), + 'parent' => $share->getParent(), + 'expiration' => null, + 'token' => null, + ]; + + $path = $share->getPath(); + $result['path'] = $this->userFolder->getRelativePath($path->getPath()); + if ($path instanceOf \OCP\Files\Folder) { + $result['item_type'] = 'folder'; + } else { + $result['item_type'] = 'file'; + } + $result['storage_id'] = $path->getStorage()->getId(); + $result['storage'] = \OC\Files\Cache\Storage::getNumericStorageId($path->getStorage()->getId()); + $result['item_source'] = $path->getId(); + $result['file_source'] = $path->getId(); + $result['file_parent'] = $path->getParent()->getId(); + $result['file_target'] = $share->getTarget(); + + if ($share->getShareType() === \OCP\Share::SHARE_TYPE_USER) { + $sharedWith = $share->getSharedWith(); + $result['share_with'] = $sharedWith->getUID(); + $result['share_with_displayname'] = $sharedWith->getDisplayName(); + } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_GROUP) { + $sharedWith = $share->getSharedWith(); + $result['share_with'] = $sharedWith->getGID(); + $result['share_with_displayname'] = $sharedWith->getGID(); + } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK) { + + $result['share_with'] = $share->getPassword(); + $result['share_with_displayname'] = $share->getPassword(); + + $result['token'] = $share->getToken(); + $result['url'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $share->getToken()]); + + $expiration = $share->getExpirationDate(); + if ($expiration !== null) { + $result['expiration'] = $expiration->format('Y-m-d 00:00:00'); + } + + } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_REMOTE) { + $result['share_with'] = $share->getSharedWith(); + $result['share_with_displayname'] = $share->getSharedWith(); + $result['token'] = $share->getToken(); + } + + $result['mail_send'] = $share->getMailSend() ? 1 : 0; + + return $result; + } + + /** + * Get a specific share by id + * + * @param string $id + * @return \OC_OCS_Result + */ + public function getShare($id) { + try { + $share = $this->shareManager->getShareById($id); + } catch (\OC\Share20\Exception\ShareNotFound $e) { + return new \OC_OCS_Result(null, 404, 'wrong share ID, share doesn\'t exist.'); + } + + $share = $this->formatShare($share); + return new \OC_OCS_Result($share); } /** * Delete a share * - * @param int $id + * @param string $id * @return \OC_OCS_Result */ public function deleteShare($id) { diff --git a/apps/files_sharing/lib/external/storage.php b/apps/files_sharing/lib/external/storage.php index 270d8b6d1b8..2a0d827e064 100644 --- a/apps/files_sharing/lib/external/storage.php +++ b/apps/files_sharing/lib/external/storage.php @@ -250,7 +250,7 @@ class Storage extends DAV implements ISharedStorage { $response = $client->post($url, ['body' => ['password' => $password]]); } catch (\GuzzleHttp\Exception\RequestException $e) { if ($e->getCode() === 401 || $e->getCode() === 403) { - throw new ForbiddenException(); + throw new ForbiddenException(); } // throw this to be on the safe side: the share will still be visible // in the UI in case the failure is intermittent, and the user will @@ -260,4 +260,9 @@ class Storage extends DAV implements ISharedStorage { return json_decode($response->getBody(), true); } + + public function getOwner($path) { + list(, $remote) = explode('://', $this->remote, 2); + return $this->remoteUser . '@' . $remote; + } } diff --git a/apps/files_sharing/tests/api/share20ocstest.php b/apps/files_sharing/tests/api/share20ocstest.php index 9c4377a2a7f..f74585eb47d 100644 --- a/apps/files_sharing/tests/api/share20ocstest.php +++ b/apps/files_sharing/tests/api/share20ocstest.php @@ -39,6 +39,9 @@ class Share20OCSTest extends \Test\TestCase { /** @var OCP\Files\Folder */ private $userFolder; + /** @var OCP\IURLGenerator */ + private $urlGenerator; + /** @var OCS */ private $ocs; @@ -46,24 +49,18 @@ class Share20OCSTest extends \Test\TestCase { $this->shareManager = $this->getMockBuilder('OC\Share20\Manager') ->disableOriginalConstructor() ->getMock(); - $this->groupManager = $this->getMockBuilder('OCP\IGroupManager') - ->disableOriginalConstructor() - ->getMock(); - $this->userManager = $this->getMockBuilder('OCP\IUserManager') - ->disableOriginalConstructor() - ->getMock(); - $this->request = $this->getMockBuilder('OCP\IRequest') - ->disableOriginalConstructor() - ->getMock(); - $this->userFolder = $this->getMockBuilder('OCP\Files\Folder') - ->disableOriginalConstructor() - ->getMock(); + $this->groupManager = $this->getMock('OCP\IGroupManager'); + $this->userManager = $this->getMock('OCP\IUserManager'); + $this->request = $this->getMock('OCP\IRequest'); + $this->userFolder = $this->getMock('OCP\Files\Folder'); + $this->urlGenerator = $this->getMock('OCP\IURLGenerator'); $this->ocs = new Share20OCS($this->shareManager, $this->groupManager, $this->userManager, $this->request, - $this->userFolder); + $this->userFolder, + $this->urlGenerator); } public function testDeleteShareShareNotFound() { @@ -110,4 +107,240 @@ class Share20OCSTest extends \Test\TestCase { $expected = new \OC_OCS_Result(); $this->assertEquals($expected, $this->ocs->deleteShare(42)); } + + public function testGetGetShareNotExists() { + $this->shareManager + ->expects($this->once()) + ->method('getShareById') + ->with(42) + ->will($this->throwException(new \OC\Share20\Exception\ShareNotFound())); + + $expected = new \OC_OCS_Result(null, 404, 'wrong share ID, share doesn\'t exist.'); + $this->assertEquals($expected, $this->ocs->getShare(42)); + } + + public function createShare($id, $shareType, $sharedWith, $sharedBy, $path, $permissions, + $shareTime, $expiration, $parent, $target, $mail_send, $token=null, + $password=null) { + $share = $this->getMock('OC\Share20\IShare'); + $share->method('getId')->willReturn($id); + $share->method('getShareType')->willReturn($shareType); + $share->method('getSharedWith')->willReturn($sharedWith); + $share->method('getSharedBy')->willReturn($sharedBy); + $share->method('getPath')->willReturn($path); + $share->method('getPermissions')->willReturn($permissions); + $share->method('getShareTime')->willReturn($shareTime); + $share->method('getExpirationDate')->willReturn($expiration); + $share->method('getParent')->willReturn($parent); + $share->method('getTarget')->willReturn($target); + $share->method('getMailSend')->willReturn($mail_send); + $share->method('getToken')->willReturn($token); + $share->method('getPassword')->willReturn($password); + + return $share; + } + + public function dataGetShare() { + $data = []; + + $owner = $this->getMock('OCP\IUser'); + $owner->method('getUID')->willReturn('ownerId'); + $owner->method('getDisplayName')->willReturn('ownerDisplay'); + + $user = $this->getMock('OCP\IUser'); + $user->method('getUID')->willReturn('userId'); + $user->method('getDisplayName')->willReturn('userDisplay'); + + $group = $this->getMock('OCP\IGroup'); + $group->method('getGID')->willReturn('groupId'); + + $storage = $this->getMock('OCP\Files\Storage'); + $storage->method('getId')->willReturn('STORAGE'); + + $parentFolder = $this->getMock('OCP\Files\Folder'); + $parentFolder->method('getId')->willReturn(3); + + $file = $this->getMock('OCP\Files\File'); + $file->method('getId')->willReturn(1); + $file->method('getPath')->willReturn('file'); + $file->method('getStorage')->willReturn($storage); + $file->method('getParent')->willReturn($parentFolder); + + $folder = $this->getMock('OCP\Files\Folder'); + $folder->method('getId')->willReturn(2); + $folder->method('getPath')->willReturn('folder'); + $folder->method('getStorage')->willReturn($storage); + $folder->method('getParent')->willReturn($parentFolder); + + // File shared with user + $share = $this->createShare(100, + \OCP\Share::SHARE_TYPE_USER, + $user, + $owner, + $file, + 4, + 5, + null, + 6, + 'target', + 0); + $expected = [ + 'id' => 100, + 'share_type' => \OCP\Share::SHARE_TYPE_USER, + 'share_with' => 'userId', + 'share_with_displayname' => 'userDisplay', + 'uid_owner' => 'ownerId', + 'displayname_owner' => 'ownerDisplay', + 'item_type' => 'file', + 'item_source' => 1, + 'file_source' => 1, + 'file_target' => 'target', + 'file_parent' => 3, + 'token' => null, + 'expiration' => null, + 'permissions' => 4, + 'stime' => 5, + 'parent' => 6, + 'storage_id' => 'STORAGE', + 'path' => 'file', + 'storage' => null, // HACK around static function + 'mail_send' => 0, + ]; + $data[] = [$share, $expected]; + + // Folder shared with group + $share = $this->createShare(101, + \OCP\Share::SHARE_TYPE_GROUP, + $group, + $owner, + $folder, + 4, + 5, + null, + 6, + 'target', + 0); + $expected = [ + 'id' => 101, + 'share_type' => \OCP\Share::SHARE_TYPE_GROUP, + 'share_with' => 'groupId', + 'share_with_displayname' => 'groupId', + 'uid_owner' => 'ownerId', + 'displayname_owner' => 'ownerDisplay', + 'item_type' => 'folder', + 'item_source' => 2, + 'file_source' => 2, + 'file_target' => 'target', + 'file_parent' => 3, + 'token' => null, + 'expiration' => null, + 'permissions' => 4, + 'stime' => 5, + 'parent' => 6, + 'storage_id' => 'STORAGE', + 'path' => 'folder', + 'storage' => null, // HACK around static function + 'mail_send' => 0, + ]; + $data[] = [$share, $expected]; + + // Folder shared with remote + $share = $this->createShare(101, + \OCP\Share::SHARE_TYPE_REMOTE, + 'user@remote.com', + $owner, + $folder, + 4, + 5, + null, + 6, + 'target', + 0); + $expected = [ + 'id' => 101, + 'share_type' => \OCP\Share::SHARE_TYPE_REMOTE, + 'share_with' => 'user@remote.com', + 'share_with_displayname' => 'user@remote.com', + 'uid_owner' => 'ownerId', + 'displayname_owner' => 'ownerDisplay', + 'item_type' => 'folder', + 'item_source' => 2, + 'file_source' => 2, + 'file_target' => 'target', + 'file_parent' => 3, + 'token' => null, + 'expiration' => null, + 'permissions' => 4, + 'stime' => 5, + 'parent' => 6, + 'storage_id' => 'STORAGE', + 'path' => 'folder', + 'storage' => null, // HACK around static function + 'mail_send' => 0, + ]; + $data[] = [$share, $expected]; + + // File shared by link with Expire + $expire = \DateTime::createFromFormat('Y-m-d h:i:s', '2000-01-02 01:02:03'); + $share = $this->createShare(101, + \OCP\Share::SHARE_TYPE_LINK, + null, + $owner, + $folder, + 4, + 5, + $expire, + 6, + 'target', + 0, + 'token', + 'password'); + $expected = [ + 'id' => 101, + 'share_type' => \OCP\Share::SHARE_TYPE_LINK, + 'share_with' => 'password', + 'share_with_displayname' => 'password', + 'uid_owner' => 'ownerId', + 'displayname_owner' => 'ownerDisplay', + 'item_type' => 'folder', + 'item_source' => 2, + 'file_source' => 2, + 'file_target' => 'target', + 'file_parent' => 3, + 'token' => 'token', + 'expiration' => '2000-01-02 00:00:00', + 'permissions' => 4, + 'stime' => 5, + 'parent' => 6, + 'storage_id' => 'STORAGE', + 'path' => 'folder', + 'storage' => null, // HACK around static function + 'mail_send' => 0, + 'url' => 'url', + ]; + $data[] = [$share, $expected]; + + return $data; + } + + /** + * @dataProvider dataGetShare + */ + public function testGetShare(\OC\Share20\IShare $share, array $result) { + $this->shareManager + ->expects($this->once()) + ->method('getShareById') + ->with($share->getId()) + ->willReturn($share); + + $this->userFolder + ->method('getRelativePath') + ->will($this->returnArgument(0)); + + $this->urlGenerator + ->method('linkToRouteAbsolute') + ->willReturn('url'); + + $expected = new \OC_OCS_Result($result); + $this->assertEquals($expected->getData(), $this->ocs->getShare($share->getId())->getData()); } } diff --git a/apps/files_trashbin/l10n/hy.js b/apps/files_trashbin/l10n/hy.js index 3cc05b39236..39e49b47ae1 100644 --- a/apps/files_trashbin/l10n/hy.js +++ b/apps/files_trashbin/l10n/hy.js @@ -5,8 +5,10 @@ OC.L10N.register( "Restore" : "Վերականգնել", "Delete" : "Ջնջել", "Delete permanently" : "Ջնջել ընդմիշտ", + "Error" : "Սխալ", "No deleted files" : "Ջնջված ֆայլեր չկան", "Select all" : "Նշել բոլորը", - "Name" : "Անուն" + "Name" : "Անուն", + "Deleted" : "Ջնջված" }, "nplurals=2; plural=(n != 1);"); diff --git a/apps/files_trashbin/l10n/hy.json b/apps/files_trashbin/l10n/hy.json index 994d6afaa58..4bd34056b3f 100644 --- a/apps/files_trashbin/l10n/hy.json +++ b/apps/files_trashbin/l10n/hy.json @@ -3,8 +3,10 @@ "Restore" : "Վերականգնել", "Delete" : "Ջնջել", "Delete permanently" : "Ջնջել ընդմիշտ", + "Error" : "Սխալ", "No deleted files" : "Ջնջված ֆայլեր չկան", "Select all" : "Նշել բոլորը", - "Name" : "Անուն" + "Name" : "Անուն", + "Deleted" : "Ջնջված" },"pluralForm" :"nplurals=2; plural=(n != 1);" }
\ No newline at end of file diff --git a/apps/files_trashbin/l10n/nds.js b/apps/files_trashbin/l10n/nds.js index b2dd9ff4ca6..8212e5210a9 100644 --- a/apps/files_trashbin/l10n/nds.js +++ b/apps/files_trashbin/l10n/nds.js @@ -2,6 +2,7 @@ OC.L10N.register( "files_trashbin", { "Delete" : "Löschen", + "Error" : "Fehler", "Name" : "Name" }, "nplurals=2; plural=(n != 1);"); diff --git a/apps/files_trashbin/l10n/nds.json b/apps/files_trashbin/l10n/nds.json index 45b0caa74ac..d15a7c583ac 100644 --- a/apps/files_trashbin/l10n/nds.json +++ b/apps/files_trashbin/l10n/nds.json @@ -1,5 +1,6 @@ { "translations": { "Delete" : "Löschen", + "Error" : "Fehler", "Name" : "Name" },"pluralForm" :"nplurals=2; plural=(n != 1);" }
\ No newline at end of file diff --git a/apps/files_trashbin/l10n/ru.js b/apps/files_trashbin/l10n/ru.js index b51486b6eb0..ef39fee4511 100644 --- a/apps/files_trashbin/l10n/ru.js +++ b/apps/files_trashbin/l10n/ru.js @@ -5,6 +5,7 @@ OC.L10N.register( "Couldn't restore %s" : "%s не может быть восстановлен", "Deleted files" : "Удалённые файлы", "Restore" : "Восстановить", + "Delete" : "Удалить", "Delete permanently" : "Удалить окончательно", "Error" : "Ошибка", "restored" : "восстановлен", @@ -13,7 +14,6 @@ OC.L10N.register( "No entries found in this folder" : "Нет элементов в этом каталоге", "Select all" : "Выделить все", "Name" : "Имя", - "Deleted" : "Удалён", - "Delete" : "Удалить" + "Deleted" : "Удалён" }, "nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);"); diff --git a/apps/files_trashbin/l10n/ru.json b/apps/files_trashbin/l10n/ru.json index 9fecb3a9641..2fe089b7ace 100644 --- a/apps/files_trashbin/l10n/ru.json +++ b/apps/files_trashbin/l10n/ru.json @@ -3,6 +3,7 @@ "Couldn't restore %s" : "%s не может быть восстановлен", "Deleted files" : "Удалённые файлы", "Restore" : "Восстановить", + "Delete" : "Удалить", "Delete permanently" : "Удалить окончательно", "Error" : "Ошибка", "restored" : "восстановлен", @@ -11,7 +12,6 @@ "No entries found in this folder" : "Нет элементов в этом каталоге", "Select all" : "Выделить все", "Name" : "Имя", - "Deleted" : "Удалён", - "Delete" : "Удалить" + "Deleted" : "Удалён" },"pluralForm" :"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);" }
\ No newline at end of file diff --git a/apps/files_versions/l10n/hy.js b/apps/files_versions/l10n/hy.js index 3924810e149..d48487a9a84 100644 --- a/apps/files_versions/l10n/hy.js +++ b/apps/files_versions/l10n/hy.js @@ -2,6 +2,7 @@ OC.L10N.register( "files_versions", { "Versions" : "Տարբերակներ", - "Restore" : "Վերականգնել" + "Restore" : "Վերականգնել", + "No other versions available" : "Այլ տարբերակներ չկան" }, "nplurals=2; plural=(n != 1);"); diff --git a/apps/files_versions/l10n/hy.json b/apps/files_versions/l10n/hy.json index 7f61c267c7c..579b9240310 100644 --- a/apps/files_versions/l10n/hy.json +++ b/apps/files_versions/l10n/hy.json @@ -1,5 +1,6 @@ { "translations": { "Versions" : "Տարբերակներ", - "Restore" : "Վերականգնել" + "Restore" : "Վերականգնել", + "No other versions available" : "Այլ տարբերակներ չկան" },"pluralForm" :"nplurals=2; plural=(n != 1);" }
\ No newline at end of file diff --git a/apps/provisioning_api/appinfo/routes.php b/apps/provisioning_api/appinfo/routes.php index dcf18e0e53b..22a923f5c20 100644 --- a/apps/provisioning_api/appinfo/routes.php +++ b/apps/provisioning_api/appinfo/routes.php @@ -37,7 +37,7 @@ $users = new \OCA\Provisioning_API\Users( \OC::$server->getLogger() ); API::register('get', '/cloud/users', [$users, 'getUsers'], 'provisioning_api', API::SUBADMIN_AUTH); -API::register('post', '/cloud/users', [$users, 'addUser'], 'provisioning_api', API::ADMIN_AUTH); +API::register('post', '/cloud/users', [$users, 'addUser'], 'provisioning_api', API::SUBADMIN_AUTH); API::register('get', '/cloud/users/{userid}', [$users, 'getUser'], 'provisioning_api', API::USER_AUTH); API::register('put', '/cloud/users/{userid}', [$users, 'editUser'], 'provisioning_api', API::USER_AUTH); API::register('delete', '/cloud/users/{userid}', [$users, 'deleteUser'], 'provisioning_api', API::SUBADMIN_AUTH); @@ -51,7 +51,8 @@ API::register('get', '/cloud/users/{userid}/subadmins', [$users, 'getUserSubAdmi // Groups $groups = new \OCA\Provisioning_API\Groups( \OC::$server->getGroupManager(), - \OC::$server->getUserSession() + \OC::$server->getUserSession(), + \OC::$server->getRequest() ); API::register('get', '/cloud/groups', [$groups, 'getGroups'], 'provisioning_api', API::SUBADMIN_AUTH); API::register('post', '/cloud/groups', [$groups, 'addGroup'], 'provisioning_api', API::SUBADMIN_AUTH); diff --git a/apps/provisioning_api/lib/groups.php b/apps/provisioning_api/lib/groups.php index c28db35972f..7a6e6150782 100644 --- a/apps/provisioning_api/lib/groups.php +++ b/apps/provisioning_api/lib/groups.php @@ -37,14 +37,20 @@ class Groups{ /** @var \OCP\IUserSession */ private $userSession; + /** @var \OCP\IRequest */ + private $request; + /** * @param \OCP\IGroupManager $groupManager * @param \OCP\IUserSession $userSession + * @param \OCP\IRequest $request */ public function __construct(\OCP\IGroupManager $groupManager, - \OCP\IUserSession $userSession) { + \OCP\IUserSession $userSession, + \OCP\IRequest $request) { $this->groupManager = $groupManager; $this->userSession = $userSession; + $this->request = $request; } /** @@ -54,9 +60,16 @@ class Groups{ * @return OC_OCS_Result */ public function getGroups($parameters) { - $search = !empty($_GET['search']) ? $_GET['search'] : ''; - $limit = !empty($_GET['limit']) ? $_GET['limit'] : null; - $offset = !empty($_GET['offset']) ? $_GET['offset'] : null; + $search = $this->request->getParam('search', ''); + $limit = $this->request->getParam('limit'); + $offset = $this->request->getParam('offset'); + + if ($limit !== null) { + $limit = (int)$limit; + } + if ($offset !== null) { + $offset = (int)$offset; + } $groups = $this->groupManager->search($search, $limit, $offset); $groups = array_map(function($group) { @@ -80,21 +93,23 @@ class Groups{ return new OC_OCS_Result(null, \OCP\API::RESPOND_UNAUTHORISED); } + $groupId = $parameters['groupid']; + // Check the group exists - if(!$this->groupManager->groupExists($parameters['groupid'])) { + if(!$this->groupManager->groupExists($groupId)) { return new OC_OCS_Result(null, \OCP\API::RESPOND_NOT_FOUND, 'The requested group could not be found'); } $isSubadminOfGroup = false; - $targetGroupObject =$this->groupManager->get($parameters['groupid']); - if($targetGroupObject !== null) { - $isSubadminOfGroup =$this->groupManager->getSubAdmin()->isSubAdminofGroup($user, $targetGroupObject); + $group = $this->groupManager->get($groupId); + if ($group !== null) { + $isSubadminOfGroup =$this->groupManager->getSubAdmin()->isSubAdminofGroup($user, $group); } // Check subadmin has access to this group if($this->groupManager->isAdmin($user->getUID()) || $isSubadminOfGroup) { - $users = $this->groupManager->get($parameters['groupid'])->getUsers(); + $users = $this->groupManager->get($groupId)->getUsers(); $users = array_map(function($user) { /** @var IUser $user */ return $user->getUID(); @@ -114,7 +129,7 @@ class Groups{ */ public function addGroup($parameters) { // Validate name - $groupId = isset($_POST['groupid']) ? $_POST['groupid'] : ''; + $groupId = $this->request->getParam('groupid', ''); if( preg_match( '/[^a-zA-Z0-9 _\.@\-]/', $groupId ) || empty($groupId)){ \OCP\Util::writeLog('provisioning_api', 'Attempt made to create group using invalid characters.', \OCP\Util::ERROR); return new OC_OCS_Result(null, 101, 'Invalid group name'); @@ -161,14 +176,8 @@ class Groups{ foreach ($subadmins as $user) { $uids[] = $user->getUID(); } - $subadmins = $uids; - // Go - if(!$subadmins) { - return new OC_OCS_Result(null, 102, 'Unknown error occured'); - } else { - return new OC_OCS_Result($subadmins); - } + return new OC_OCS_Result($uids); } } diff --git a/apps/provisioning_api/lib/users.php b/apps/provisioning_api/lib/users.php index 304fe901cfd..a2568425d0f 100644 --- a/apps/provisioning_api/lib/users.php +++ b/apps/provisioning_api/lib/users.php @@ -117,19 +117,50 @@ class Users { public function addUser() { $userId = isset($_POST['userid']) ? $_POST['userid'] : null; $password = isset($_POST['password']) ? $_POST['password'] : null; + $groups = isset($_POST['groups']) ? $_POST['groups'] : null; + $user = $this->userSession->getUser(); + $isAdmin = $this->groupManager->isAdmin($user->getUID()); + $subAdminManager = $this->groupManager->getSubAdmin(); + + if (!$isAdmin && !$subAdminManager->isSubAdmin($user)) { + return new OC_OCS_Result(null, \OCP\API::RESPOND_UNAUTHORISED); + } + if($this->userManager->userExists($userId)) { $this->logger->error('Failed addUser attempt: User already exists.', ['app' => 'ocs_api']); return new OC_OCS_Result(null, 102, 'User already exists'); + } + + if(is_array($groups)) { + foreach ($groups as $group) { + if(!$this->groupManager->groupExists($group)){ + return new OC_OCS_Result(null, 104, 'group '.$group.' does not exist'); + } + if(!$isAdmin && !$subAdminManager->isSubAdminofGroup($user, $this->groupManager->get($group))) { + return new OC_OCS_Result(null, 105, 'insufficient privileges for group '. $group); + } + } } else { - try { - $this->userManager->createUser($userId, $password); - $this->logger->info('Successful addUser call with userid: '.$_POST['userid'], ['app' => 'ocs_api']); - return new OC_OCS_Result(null, 100); - } catch (\Exception $e) { - $this->logger->error('Failed addUser attempt with exception: '.$e->getMessage(), ['app' => 'ocs_api']); - return new OC_OCS_Result(null, 101, 'Bad request'); + if(!$isAdmin) { + return new OC_OCS_Result(null, 106, 'no group specified (required for subadmins)'); } } + + try { + $newUser = $this->userManager->createUser($userId, $password); + $this->logger->info('Successful addUser call with userid: '.$userId, ['app' => 'ocs_api']); + + if (is_array($groups)) { + foreach ($groups as $group) { + $this->groupManager->get($group)->addUser($newUser); + $this->logger->info('Added userid '.$userId.' to group '.$group, ['app' => 'ocs_api']); + } + } + return new OC_OCS_Result(null, 100); + } catch (\Exception $e) { + $this->logger->error('Failed addUser attempt with exception: '.$e->getMessage(), ['app' => 'ocs_api']); + return new OC_OCS_Result(null, 101, 'Bad request'); + } } /** diff --git a/apps/provisioning_api/tests/groupstest.php b/apps/provisioning_api/tests/groupstest.php index f67ed1c36ae..d37f4412e20 100644 --- a/apps/provisioning_api/tests/groupstest.php +++ b/apps/provisioning_api/tests/groupstest.php @@ -25,79 +25,131 @@ namespace OCA\Provisioning_API\Tests; -use OCP\IUserManager; use OCP\IGroupManager; use OCP\IUserSession; +use OCP\IRequest; -class GroupsTest extends TestCase { - /** @var IUserManager */ - protected $userManager; +class GroupsTest extends \Test\TestCase { /** @var IGroupManager */ protected $groupManager; /** @var IUserSession */ protected $userSession; + /** @var IRequest */ + protected $request; + /** @var \OC\SubAdmin */ + protected $subAdminManager; /** @var \OCA\Provisioning_API\Groups */ protected $api; protected function setup() { - parent::setup(); + $this->subAdminManager = $this->getMockBuilder('OC\SubAdmin')->disableOriginalConstructor()->getMock(); - $this->userManager = \OC::$server->getUserManager(); - $this->groupManager = \OC::$server->getGroupManager(); - $this->userSession = \OC::$server->getUserSession(); + $this->groupManager = $this->getMockBuilder('OC\Group\Manager')->disableOriginalConstructor()->getMock(); + $this->groupManager + ->method('getSubAdmin') + ->willReturn($this->subAdminManager); + + $this->userSession = $this->getMock('OCP\IUserSession'); + $this->request = $this->getMock('OCP\IRequest'); $this->api = new \OCA\Provisioning_API\Groups( $this->groupManager, - $this->userSession + $this->userSession, + $this->request ); } - public function testGetGroups() { - $groups = []; - $id = $this->getUniqueID(); + private function createGroup($gid) { + $group = $this->getMock('OCP\IGroup'); + $group + ->method('getGID') + ->willReturn($gid); + return $group; + } - for ($i=0; $i < 10; $i++) { - $groups[] = $this->groupManager->createGroup($id . '_' . $i); - } + private function createUser($uid) { + $user = $this->getMock('OCP\IUser'); + $user + ->method('getUID') + ->willReturn($uid); + return $user; + } - $_GET = []; - $result = $this->api->getGroups([]); - $this->assertInstanceOf('OC_OCS_Result', $result); - $this->assertTrue($result->succeeded()); - $this->assertCount(count($this->groupManager->search('')), $result->getData()['groups']); - $this->assertContains('admin', $result->getData()['groups']); - foreach ($groups as $group) { - $this->assertContains($group->getGID(), $result->getData()['groups']); - } - - $_GET = [ - 'search' => $id, - 'limit' => 5, - 'offset' => 2 + private function asUser() { + $user = $this->createUser('user'); + $this->userSession + ->method('getUser') + ->willReturn($user); + } + + private function asAdmin() { + $user = $this->createUser('admin'); + $this->userSession + ->method('getUser') + ->willReturn($user); + + $this->groupManager + ->method('isAdmin') + ->with('admin') + ->willReturn(true); + } + + private function asSubAdminOfGroup($group) { + $user = $this->createUser('subAdmin'); + $this->userSession + ->method('getUser') + ->willReturn($user); + + $this->subAdminManager + ->method('isSubAdminOfGroup') + ->will($this->returnCallback(function($_user, $_group) use ($user, $group) { + if ($_user === $user && $_group === $group) { + return true; + } + return false; + })); + } + + public function dataGetGroups() { + return [ + [null, null, null], + ['foo', null, null], + [null, 1, null], + [null, null, 2], + ['foo', 1, 2], ]; + } + + /** + * @dataProvider dataGetGroups + */ + public function testGetGroups($search, $limit, $offset) { + $this->request + ->expects($this->exactly(3)) + ->method('getParam') + ->will($this->returnValueMap([ + ['search', '', $search], + ['limit', null, $limit], + ['offset', null, $offset], + ])); + + $groups = [$this->createGroup('group1'), $this->createGroup('group2')]; + + $search = $search === null ? '' : $search; + + $this->groupManager + ->expects($this->once()) + ->method('search') + ->with($search, $limit, $offset) + ->willReturn($groups); + $result = $this->api->getGroups([]); $this->assertInstanceOf('OC_OCS_Result', $result); $this->assertTrue($result->succeeded()); - $this->assertCount(5, $result->getData()['groups']); - foreach (array_splice($groups, 2, 5) as $group) { - $this->assertContains($group->getGID(), $result->getData()['groups']); - } - - foreach ($groups as $group) { - $group->delete(); - } + $this->assertEquals(['group1', 'group2'], $result->getData()['groups']); } public function testGetGroupAsUser() { - - $users = $this->generateUsers(2); - $this->userSession->setUser($users[0]); - - $group = $this->groupManager->createGroup($this->getUniqueID()); - $group->addUser($users[1]); - - $result = $this->api->getGroup(array( - 'groupid' => $group->getGID(), - )); + $result = $this->api->getGroup([]); $this->assertInstanceOf('OC_OCS_Result', $result); $this->assertFalse($result->succeeded()); @@ -106,80 +158,91 @@ class GroupsTest extends TestCase { } public function testGetGroupAsSubadmin() { - - $users = $this->generateUsers(2); - $this->userSession->setUser($users[0]); - - $group = $this->groupManager->createGroup($this->getUniqueID()); - $group->addUser($users[0]); - $group->addUser($users[1]); - - $this->groupManager->getSubAdmin()->createSubAdmin($users[0], $group); + $group = $this->createGroup('group'); + $this->asSubAdminOfGroup($group); + + $this->groupManager + ->method('get') + ->with('group') + ->willReturn($group); + $this->groupManager + ->method('groupExists') + ->with('group') + ->willReturn(true); + $group + ->method('getUsers') + ->willReturn([ + $this->createUser('user1'), + $this->createUser('user2') + ]); $result = $this->api->getGroup([ - 'groupid' => $group->getGID(), + 'groupid' => 'group', ]); $this->assertInstanceOf('OC_OCS_Result', $result); $this->assertTrue($result->succeeded()); $this->assertEquals(1, sizeof($result->getData()), 'Asserting the result data array only has the "users" key'); $this->assertArrayHasKey('users', $result->getData()); - $resultData = $result->getData(); - $resultData = $resultData['users']; - - $users = array_map(function($user) { - return $user->getUID(); - }, $users); - - sort($users); - sort($resultData); - $this->assertEquals($users, $resultData); - + $this->assertEquals(['user1', 'user2'], $result->getData()['users']); } public function testGetGroupAsIrrelevantSubadmin() { - - $users = $this->generateUsers(2); - $this->userSession->setUser($users[0]); - - $group1 = $this->groupManager->createGroup($this->getUniqueID()); - $group2 = $this->groupManager->createGroup($this->getUniqueID()); - $group1->addUser($users[1]); - $group2->addUser($users[0]); - - $this->groupManager->getSubAdmin()->createSubAdmin($users[0], $group2); + $group = $this->createGroup('group'); + $otherGroup = $this->createGroup('otherGroup'); + $this->asSubAdminOfGroup($otherGroup); + + $this->groupManager + ->method('get') + ->with('group') + ->willReturn($group); + $this->groupManager + ->method('groupExists') + ->with('group') + ->willReturn(true); $result = $this->api->getGroup([ - 'groupid' => $group1->getGID(), + 'groupid' => 'group', ]); $this->assertInstanceOf('OC_OCS_Result', $result); $this->assertFalse($result->succeeded()); $this->assertEquals(\OCP\API::RESPOND_UNAUTHORISED, $result->getStatusCode()); - } public function testGetGroupAsAdmin() { - - $users = $this->generateUsers(2); - $this->userSession->setUser($users[0]); - - $group = $this->groupManager->createGroup($this->getUniqueID()); - - $group->addUser($users[1]); - $this->groupManager->get('admin')->addUser($users[0]); + $group = $this->createGroup('group'); + $this->asAdmin(); + + $this->groupManager + ->method('get') + ->with('group') + ->willReturn($group); + $this->groupManager + ->method('groupExists') + ->with('group') + ->willReturn(true); + $group + ->method('getUsers') + ->willReturn([ + $this->createUser('user1'), + $this->createUser('user2') + ]); $result = $this->api->getGroup([ - 'groupid' => $group->getGID(), + 'groupid' => 'group', ]); $this->assertInstanceOf('OC_OCS_Result', $result); $this->assertTrue($result->succeeded()); - $this->assertEquals(['users' => [$users[1]->getUID()]], $result->getData()); - + $this->assertEquals(1, sizeof($result->getData()), 'Asserting the result data array only has the "users" key'); + $this->assertArrayHasKey('users', $result->getData()); + $this->assertEquals(['user1', 'user2'], $result->getData()['users']); } public function testGetGroupNonExisting() { + $this->asUser(); + $result = $this->api->getGroup([ 'groupid' => $this->getUniqueId() ]); @@ -190,35 +253,71 @@ class GroupsTest extends TestCase { $this->assertEquals('The requested group could not be found', $result->getMeta()['message']); } + public function testGetSubAdminsOfGroupsNotExists() { + $result = $this->api->getSubAdminsOfGroup([ + 'groupid' => 'NonExistingGroup', + ]); + + $this->assertInstanceOf('OC_OCS_Result', $result); + $this->assertFalse($result->succeeded()); + $this->assertEquals(101, $result->getStatusCode()); + $this->assertEquals('Group does not exist', $result->getMeta()['message']); + } + public function testGetSubAdminsOfGroup() { - $user1 = $this->generateUsers(); - $user2 = $this->generateUsers(); - $this->userSession->setUser($user1); - $this->groupManager->get('admin')->addUser($user1); - $group1 = $this->groupManager->createGroup($this->getUniqueID()); - $this->groupManager->getSubAdmin()->createSubAdmin($user2, $group1); + $group = $this->createGroup('GroupWithSubAdmins'); + $this->groupManager + ->method('get') + ->with('GroupWithSubAdmins') + ->willReturn($group); + + $this->subAdminManager + ->expects($this->once()) + ->method('getGroupsSubAdmins') + ->with($group) + ->willReturn([ + $this->createUser('SubAdmin1'), + $this->createUser('SubAdmin2'), + ]); + $result = $this->api->getSubAdminsOfGroup([ - 'groupid' => $group1->getGID(), + 'groupid' => 'GroupWithSubAdmins', ]); + $this->assertInstanceOf('OC_OCS_Result', $result); $this->assertTrue($result->succeeded()); - $data = $result->getData(); - $this->assertEquals($user2->getUID(), reset($data)); - $group1->delete(); + $this->assertEquals(['SubAdmin1', 'SubAdmin2'], $result->getData()); + } + + public function testGetSubAdminsOfGroupEmptyList() { + $group = $this->createGroup('GroupWithOutSubAdmins'); + $this->groupManager + ->method('get') + ->with('GroupWithOutSubAdmins') + ->willReturn($group); + + $this->subAdminManager + ->expects($this->once()) + ->method('getGroupsSubAdmins') + ->with($group) + ->willReturn([ + ]); - $user1 = $this->generateUsers(); - $this->userSession->setUser($user1); - $this->groupManager->get('admin')->addUser($user1); $result = $this->api->getSubAdminsOfGroup([ - 'groupid' => $this->getUniqueID(), + 'groupid' => 'GroupWithOutSubAdmins', ]); + $this->assertInstanceOf('OC_OCS_Result', $result); - $this->assertFalse($result->succeeded()); - $this->assertEquals(101, $result->getStatusCode()); + $this->assertTrue($result->succeeded()); + $this->assertEquals([], $result->getData()); } public function testAddGroupEmptyGroup() { - $_POST = []; + $this->request + ->method('getParam') + ->with('groupid') + ->willReturn(''); + $result = $this->api->addGroup([]); $this->assertInstanceOf('OC_OCS_Result', $result); @@ -228,40 +327,47 @@ class GroupsTest extends TestCase { } public function testAddGroupExistingGroup() { - $group = $this->groupManager->createGroup($this->getUniqueID()); + $this->request + ->method('getParam') + ->with('groupid') + ->willReturn('ExistingGroup'); + + $this->groupManager + ->method('groupExists') + ->with('ExistingGroup') + ->willReturn(true); - $_POST = [ - 'groupid' => $group->getGID() - ]; $result = $this->api->addGroup([]); $this->assertInstanceOf('OC_OCS_Result', $result); $this->assertFalse($result->succeeded()); $this->assertEquals(102, $result->getStatusCode()); - - $group->delete(); } public function testAddGroup() { - $group = $this->getUniqueId(); + $this->request + ->method('getParam') + ->with('groupid') + ->willReturn('NewGroup'); - $_POST = [ - 'groupid' => $group - ]; + $this->groupManager + ->method('groupExists') + ->with('NewGroup') + ->willReturn(false); + + $this->groupManager + ->expects($this->once()) + ->method('createGroup') + ->with('NewGroup'); $result = $this->api->addGroup([]); $this->assertInstanceOf('OC_OCS_Result', $result); $this->assertTrue($result->succeeded()); - $this->assertTrue($this->groupManager->groupExists($group)); - - $this->groupManager->get($group)->delete(); } public function testDeleteGroupNonExisting() { - $group = $this->getUniqueId(); - $result = $this->api->deleteGroup([ - 'groupid' => $group + 'groupid' => 'NonExistingGroup' ]); $this->assertInstanceOf('OC_OCS_Result', $result); $this->assertFalse($result->succeeded()); @@ -269,6 +375,11 @@ class GroupsTest extends TestCase { } public function testDeleteAdminGroup() { + $this->groupManager + ->method('groupExists') + ->with('admin') + ->willReturn('true'); + $result = $this->api->deleteGroup([ 'groupid' => 'admin' ]); @@ -278,13 +389,25 @@ class GroupsTest extends TestCase { } public function testDeleteGroup() { - $group = $this->groupManager->createGroup($this->getUniqueId()); + $this->groupManager + ->method('groupExists') + ->with('ExistingGroup') + ->willReturn('true'); + + $group = $this->createGroup('ExistingGroup'); + $this->groupManager + ->method('get') + ->with('ExistingGroup') + ->willReturn($group); + $group + ->expects($this->once()) + ->method('delete') + ->willReturn(true); $result = $this->api->deleteGroup([ - 'groupid' => $group->getGID() + 'groupid' => 'ExistingGroup', ]); $this->assertInstanceOf('OC_OCS_Result', $result); $this->assertTrue($result->succeeded()); - $this->assertFalse($this->groupManager->groupExists($group->getGID())); } } diff --git a/apps/provisioning_api/tests/userstest.php b/apps/provisioning_api/tests/userstest.php index ba4ed8a2e2f..63180eb3472 100644 --- a/apps/provisioning_api/tests/userstest.php +++ b/apps/provisioning_api/tests/userstest.php @@ -218,11 +218,95 @@ class UsersTest extends OriginalTest { ->expects($this->once()) ->method('error') ->with('Failed addUser attempt: User already exists.', ['app' => 'ocs_api']); + $loggedInUser = $this->getMock('\OCP\IUser'); + $loggedInUser + ->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('adminUser')); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($loggedInUser)); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('adminUser') + ->willReturn(true); $expected = new \OC_OCS_Result(null, 102, 'User already exists'); $this->assertEquals($expected, $this->api->addUser()); } + public function testAddUserNonExistingGroup() { + $_POST['userid'] = 'NewUser'; + $_POST['groups'] = ['NonExistingGroup']; + $this->userManager + ->expects($this->once()) + ->method('userExists') + ->with('NewUser') + ->willReturn(false); + $loggedInUser = $this->getMock('\OCP\IUser'); + $loggedInUser + ->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('adminUser')); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($loggedInUser)); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('adminUser') + ->willReturn(true); + $this->groupManager + ->expects($this->once()) + ->method('groupExists') + ->with('NonExistingGroup') + ->willReturn(false); + + $expected = new \OC_OCS_Result(null, 104, 'group NonExistingGroup does not exist'); + $this->assertEquals($expected, $this->api->addUser()); + } + + public function testAddUserExistingGroupNonExistingGroup() { + $_POST['userid'] = 'NewUser'; + $_POST['groups'] = ['ExistingGroup', 'NonExistingGroup']; + $this->userManager + ->expects($this->once()) + ->method('userExists') + ->with('NewUser') + ->willReturn(false); + $loggedInUser = $this->getMock('\OCP\IUser'); + $loggedInUser + ->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('adminUser')); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($loggedInUser)); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('adminUser') + ->willReturn(true); + $this->groupManager + ->expects($this->exactly(2)) + ->method('groupExists') + ->withConsecutive( + ['ExistingGroup'], + ['NonExistingGroup'] + ) + ->will($this->returnValueMap([ + ['ExistingGroup', true], + ['NonExistingGroup', false] + ])); + + $expected = new \OC_OCS_Result(null, 104, 'group NonExistingGroup does not exist'); + $this->assertEquals($expected, $this->api->addUser()); + } + public function testAddUserSuccessful() { $_POST['userid'] = 'NewUser'; $_POST['password'] = 'PasswordOfTheNewUser'; @@ -239,6 +323,76 @@ class UsersTest extends OriginalTest { ->expects($this->once()) ->method('info') ->with('Successful addUser call with userid: NewUser', ['app' => 'ocs_api']); + $loggedInUser = $this->getMock('\OCP\IUser'); + $loggedInUser + ->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('adminUser')); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($loggedInUser)); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('adminUser') + ->willReturn(true); + + $expected = new \OC_OCS_Result(null, 100); + $this->assertEquals($expected, $this->api->addUser()); + } + + public function testAddUserExistingGroup() { + $_POST['userid'] = 'NewUser'; + $_POST['password'] = 'PasswordOfTheNewUser'; + $_POST['groups'] = ['ExistingGroup']; + $this->userManager + ->expects($this->once()) + ->method('userExists') + ->with('NewUser') + ->willReturn(false); + $loggedInUser = $this->getMock('\OCP\IUser'); + $loggedInUser + ->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('adminUser')); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($loggedInUser)); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('adminUser') + ->willReturn(true); + $this->groupManager + ->expects($this->once()) + ->method('groupExists') + ->with('ExistingGroup') + ->willReturn(true); + $user = $this->getMock('\OCP\IUser'); + $this->userManager + ->expects($this->once()) + ->method('createUser') + ->with('NewUser', 'PasswordOfTheNewUser') + ->willReturn($user); + $group = $this->getMock('\OCP\IGroup'); + $group + ->expects($this->once()) + ->method('addUser') + ->with($user); + $this->groupManager + ->expects($this->once()) + ->method('get') + ->with('ExistingGroup') + ->willReturn($group); + $this->logger + ->expects($this->exactly(2)) + ->method('info') + ->withConsecutive( + ['Successful addUser call with userid: NewUser', ['app' => 'ocs_api']], + ['Added userid NewUser to group ExistingGroup', ['app' => 'ocs_api']] + ); $expected = new \OC_OCS_Result(null, 100); $this->assertEquals($expected, $this->api->addUser()); @@ -261,11 +415,238 @@ class UsersTest extends OriginalTest { ->expects($this->once()) ->method('error') ->with('Failed addUser attempt with exception: User backend not found.', ['app' => 'ocs_api']); + $loggedInUser = $this->getMock('\OCP\IUser'); + $loggedInUser + ->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('adminUser')); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($loggedInUser)); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('adminUser') + ->willReturn(true); $expected = new \OC_OCS_Result(null, 101, 'Bad request'); $this->assertEquals($expected, $this->api->addUser()); } + public function testAddUserAsRegularUser() { + $_POST['userid'] = 'NewUser'; + $_POST['password'] = 'PasswordOfTheNewUser'; + $loggedInUser = $this->getMock('\OCP\IUser'); + $loggedInUser + ->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('regularUser')); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($loggedInUser)); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('regularUser') + ->willReturn(false); + $subAdminManager = $this->getMockBuilder('\OC\Subadmin') + ->disableOriginalConstructor()->getMock(); + $subAdminManager + ->expects($this->once()) + ->method('isSubAdmin') + ->with($loggedInUser) + ->willReturn(false); + $this->groupManager + ->expects($this->once()) + ->method('getSubAdmin') + ->with() + ->willReturn($subAdminManager); + + $expected = new \OC_OCS_Result(null, \OCP\API::RESPOND_UNAUTHORISED); + $this->assertEquals($expected, $this->api->addUser()); + } + + public function testAddUserAsSubAdminNoGroup() { + $_POST['userid'] = 'NewUser'; + $_POST['password'] = 'PasswordOfTheNewUser'; + $loggedInUser = $this->getMock('\OCP\IUser'); + $loggedInUser + ->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('regularUser')); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($loggedInUser)); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('regularUser') + ->willReturn(false); + $subAdminManager = $this->getMockBuilder('\OC\Subadmin') + ->disableOriginalConstructor()->getMock(); + $subAdminManager + ->expects($this->once()) + ->method('isSubAdmin') + ->with($loggedInUser) + ->willReturn(true); + $this->groupManager + ->expects($this->once()) + ->method('getSubAdmin') + ->with() + ->willReturn($subAdminManager); + + $expected = new \OC_OCS_Result(null, 106, 'no group specified (required for subadmins)'); + $this->assertEquals($expected, $this->api->addUser()); + } + + public function testAddUserAsSubAdminValidGroupNotSubAdmin() { + $_POST['userid'] = 'NewUser'; + $_POST['password'] = 'PasswordOfTheNewUser'; + $_POST['groups'] = ['ExistingGroup']; + $loggedInUser = $this->getMock('\OCP\IUser'); + $loggedInUser + ->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('regularUser')); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($loggedInUser)); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('regularUser') + ->willReturn(false); + $existingGroup = $this->getMock('\OCP\IGroup'); + $this->groupManager + ->expects($this->once()) + ->method('get') + ->with('ExistingGroup') + ->willReturn($existingGroup); + $subAdminManager = $this->getMockBuilder('\OC\Subadmin') + ->disableOriginalConstructor()->getMock(); + $subAdminManager + ->expects($this->once()) + ->method('isSubAdmin') + ->with($loggedInUser) + ->willReturn(true); + $subAdminManager + ->expects($this->once()) + ->method('isSubAdminOfGroup') + ->with($loggedInUser, $existingGroup) + ->wilLReturn(false); + $this->groupManager + ->expects($this->once()) + ->method('getSubAdmin') + ->with() + ->willReturn($subAdminManager); + $this->groupManager + ->expects($this->once()) + ->method('groupExists') + ->with('ExistingGroup') + ->willReturn(true); + + $expected = new \OC_OCS_Result(null, 105, 'insufficient privileges for group ExistingGroup'); + $this->assertEquals($expected, $this->api->addUser()); + } + + public function testAddUserAsSubAdminExistingGroups() { + $_POST['userid'] = 'NewUser'; + $_POST['password'] = 'PasswordOfTheNewUser'; + $_POST['groups'] = ['ExistingGroup1', 'ExistingGroup2']; + $this->userManager + ->expects($this->once()) + ->method('userExists') + ->with('NewUser') + ->willReturn(false); + $loggedInUser = $this->getMock('\OCP\IUser'); + $loggedInUser + ->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('subAdminUser')); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($loggedInUser)); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('subAdminUser') + ->willReturn(false); + $this->groupManager + ->expects($this->exactly(2)) + ->method('groupExists') + ->withConsecutive( + ['ExistingGroup1'], + ['ExistingGroup2'] + ) + ->willReturn(true); + $user = $this->getMock('\OCP\IUser'); + $this->userManager + ->expects($this->once()) + ->method('createUser') + ->with('NewUser', 'PasswordOfTheNewUser') + ->willReturn($user); + $existingGroup1 = $this->getMock('\OCP\IGroup'); + $existingGroup2 = $this->getMock('\OCP\IGroup'); + $existingGroup1 + ->expects($this->once()) + ->method('addUser') + ->with($user); + $existingGroup2 + ->expects($this->once()) + ->method('addUser') + ->with($user); + $this->groupManager + ->expects($this->exactly(4)) + ->method('get') + ->withConsecutive( + ['ExistingGroup1'], + ['ExistingGroup2'], + ['ExistingGroup1'], + ['ExistingGroup2'] + ) + ->will($this->returnValueMap([ + ['ExistingGroup1', $existingGroup1], + ['ExistingGroup2', $existingGroup2] + ])); + $this->logger + ->expects($this->exactly(3)) + ->method('info') + ->withConsecutive( + ['Successful addUser call with userid: NewUser', ['app' => 'ocs_api']], + ['Added userid NewUser to group ExistingGroup1', ['app' => 'ocs_api']], + ['Added userid NewUser to group ExistingGroup2', ['app' => 'ocs_api']] + ); + $subAdminManager = $this->getMockBuilder('\OC\Subadmin') + ->disableOriginalConstructor()->getMock(); + $this->groupManager + ->expects($this->once()) + ->method('getSubAdmin') + ->willReturn($subAdminManager); + $subAdminManager + ->expects($this->once()) + ->method('isSubAdmin') + ->with($loggedInUser) + ->willReturn(true); + $subAdminManager + ->expects($this->exactly(2)) + ->method('isSubAdminOfGroup') + ->withConsecutive( + [$loggedInUser, $existingGroup1], + [$loggedInUser, $existingGroup2] + ) + ->wilLReturn(true); + + + $expected = new \OC_OCS_Result(null, 100); + $this->assertEquals($expected, $this->api->addUser()); + } + + public function testGetUserNotLoggedIn() { $this->userSession ->expects($this->once()) diff --git a/apps/user_ldap/l10n/es.js b/apps/user_ldap/l10n/es.js index bfe48c28f99..e457978bcd7 100644 --- a/apps/user_ldap/l10n/es.js +++ b/apps/user_ldap/l10n/es.js @@ -24,6 +24,7 @@ OC.L10N.register( "Could not detect Base DN, please enter it manually." : "No se pudo detectar Base DN, por favor ingrésela manualmente.", "{nthServer}. Server" : "{nthServer}. servidor", "No object found in the given Base DN. Please revise." : "No se encuentra ningún objeto en la Base DN dada. Por favor revisar.", + "More than 1,000 directory entries available." : "Más de 1.000 entradas de directorios disponibles.", " entries available within the provided Base DN" : "entradas disponibles dentro de la BaseDN provista", "An error occurred. Please check the Base DN, as well as connection settings and credentials." : "Un error ocurrió. Por favor revise la Base DN, también como la configuración de la conexión y credenciales.", "Do you really want to delete the current Server Configuration?" : "¿Realmente desea eliminar la configuración actual del servidor?", diff --git a/apps/user_ldap/l10n/es.json b/apps/user_ldap/l10n/es.json index c8d64f6b9d6..ebce66270f1 100644 --- a/apps/user_ldap/l10n/es.json +++ b/apps/user_ldap/l10n/es.json @@ -22,6 +22,7 @@ "Could not detect Base DN, please enter it manually." : "No se pudo detectar Base DN, por favor ingrésela manualmente.", "{nthServer}. Server" : "{nthServer}. servidor", "No object found in the given Base DN. Please revise." : "No se encuentra ningún objeto en la Base DN dada. Por favor revisar.", + "More than 1,000 directory entries available." : "Más de 1.000 entradas de directorios disponibles.", " entries available within the provided Base DN" : "entradas disponibles dentro de la BaseDN provista", "An error occurred. Please check the Base DN, as well as connection settings and credentials." : "Un error ocurrió. Por favor revise la Base DN, también como la configuración de la conexión y credenciales.", "Do you really want to delete the current Server Configuration?" : "¿Realmente desea eliminar la configuración actual del servidor?", diff --git a/apps/user_ldap/l10n/lt_LT.js b/apps/user_ldap/l10n/lt_LT.js index e9429c0de2a..a5b85499804 100644 --- a/apps/user_ldap/l10n/lt_LT.js +++ b/apps/user_ldap/l10n/lt_LT.js @@ -4,9 +4,12 @@ OC.L10N.register( "Failed to clear the mappings." : "Nepavyko išvalyti sąsajų.", "Failed to delete the server configuration" : "Nepavyko pašalinti serverio konfigūracijos", "The configuration is valid and the connection could be established!" : "Konfigūracija yra tinkama bei prisijungta sėkmingai!", + "No action specified" : "Nepasirinktas veiksmas", "Select groups" : "Pasirinkti grupes", "Do you really want to delete the current Server Configuration?" : "Ar tikrai norite ištrinti dabartinę serverio konfigūraciją?", "Confirm Deletion" : "Patvirtinkite trynimą", + "_%s group found_::_%s groups found_" : ["Rasta %s grupė","Rastos %s grupės","Rastos %s grupės"], + "_%s user found_::_%s users found_" : ["Rastas %s vartotojas","Rasti %s vartotojai","Rasti %s vartotojai"], "Server" : "Serveris", "Users" : "Vartotojai", "Groups" : "Grupės", diff --git a/apps/user_ldap/l10n/lt_LT.json b/apps/user_ldap/l10n/lt_LT.json index 19ca042b62f..68dfbc38b9c 100644 --- a/apps/user_ldap/l10n/lt_LT.json +++ b/apps/user_ldap/l10n/lt_LT.json @@ -2,9 +2,12 @@ "Failed to clear the mappings." : "Nepavyko išvalyti sąsajų.", "Failed to delete the server configuration" : "Nepavyko pašalinti serverio konfigūracijos", "The configuration is valid and the connection could be established!" : "Konfigūracija yra tinkama bei prisijungta sėkmingai!", + "No action specified" : "Nepasirinktas veiksmas", "Select groups" : "Pasirinkti grupes", "Do you really want to delete the current Server Configuration?" : "Ar tikrai norite ištrinti dabartinę serverio konfigūraciją?", "Confirm Deletion" : "Patvirtinkite trynimą", + "_%s group found_::_%s groups found_" : ["Rasta %s grupė","Rastos %s grupės","Rastos %s grupės"], + "_%s user found_::_%s users found_" : ["Rastas %s vartotojas","Rasti %s vartotojai","Rasti %s vartotojai"], "Server" : "Serveris", "Users" : "Vartotojai", "Groups" : "Grupės", diff --git a/apps/user_ldap/l10n/th_TH.js b/apps/user_ldap/l10n/th_TH.js index c848d137464..8b155dcfe23 100644 --- a/apps/user_ldap/l10n/th_TH.js +++ b/apps/user_ldap/l10n/th_TH.js @@ -24,6 +24,7 @@ OC.L10N.register( "Could not detect Base DN, please enter it manually." : "ไม่สามารถตรวจสอบ Base DN โปรดเลือกด้วยตนเอง", "{nthServer}. Server" : "เซิร์ฟเวอร์ {nthServer}", "No object found in the given Base DN. Please revise." : "ไม่พบวัตถุที่กำหนดใน Base DN กรุณาแก้ไข", + "More than 1,000 directory entries available." : "ไดเรกทอรีมีอยู่มากกว่า 1,000 รายการ", " entries available within the provided Base DN" : "รายการที่มีอยู่ใน Base DN", "An error occurred. Please check the Base DN, as well as connection settings and credentials." : "เกิดข้อผิดพลาด กรุณาตรวจสอบ Base DN เช่นเดียวกับการตั้งค่าการเชื่อมต่อและข้อมูลที่สำคัญ", "Do you really want to delete the current Server Configuration?" : "คุณแน่ใจแล้วหรือว่าต้องการลบการกำหนดค่าเซิร์ฟเวอร์ปัจจุบันทิ้งไป?", diff --git a/apps/user_ldap/l10n/th_TH.json b/apps/user_ldap/l10n/th_TH.json index 6afa6f2bbc4..9787ba10037 100644 --- a/apps/user_ldap/l10n/th_TH.json +++ b/apps/user_ldap/l10n/th_TH.json @@ -22,6 +22,7 @@ "Could not detect Base DN, please enter it manually." : "ไม่สามารถตรวจสอบ Base DN โปรดเลือกด้วยตนเอง", "{nthServer}. Server" : "เซิร์ฟเวอร์ {nthServer}", "No object found in the given Base DN. Please revise." : "ไม่พบวัตถุที่กำหนดใน Base DN กรุณาแก้ไข", + "More than 1,000 directory entries available." : "ไดเรกทอรีมีอยู่มากกว่า 1,000 รายการ", " entries available within the provided Base DN" : "รายการที่มีอยู่ใน Base DN", "An error occurred. Please check the Base DN, as well as connection settings and credentials." : "เกิดข้อผิดพลาด กรุณาตรวจสอบ Base DN เช่นเดียวกับการตั้งค่าการเชื่อมต่อและข้อมูลที่สำคัญ", "Do you really want to delete the current Server Configuration?" : "คุณแน่ใจแล้วหรือว่าต้องการลบการกำหนดค่าเซิร์ฟเวอร์ปัจจุบันทิ้งไป?", diff --git a/apps/user_ldap/l10n/tr.js b/apps/user_ldap/l10n/tr.js index cbe95a194c5..c9798c5382d 100644 --- a/apps/user_ldap/l10n/tr.js +++ b/apps/user_ldap/l10n/tr.js @@ -24,6 +24,7 @@ OC.L10N.register( "Could not detect Base DN, please enter it manually." : "Base DN tespit edilemedi, lütfen elle girin.", "{nthServer}. Server" : "{nthServer}. Sunucu", "No object found in the given Base DN. Please revise." : "Girilen Base DN içerisinde nesne bulunamadı. Lütfen gözden geçirin.", + "More than 1,000 directory entries available." : "1000'den fazla dizin şuan müsait durumdadır.", " entries available within the provided Base DN" : " girdi sağlanan Base DN içerisinde mevcut", "An error occurred. Please check the Base DN, as well as connection settings and credentials." : "Bir hata oluştu. Lütfen Base DN ile birlikte bağlantı ayarlarını ve kimlik bilgilerini denetleyin.", "Do you really want to delete the current Server Configuration?" : "Şu anki sunucu yapılandırmasını silmek istediğinizden emin misiniz?", diff --git a/apps/user_ldap/l10n/tr.json b/apps/user_ldap/l10n/tr.json index 2bd0bd59907..c60842078a7 100644 --- a/apps/user_ldap/l10n/tr.json +++ b/apps/user_ldap/l10n/tr.json @@ -22,6 +22,7 @@ "Could not detect Base DN, please enter it manually." : "Base DN tespit edilemedi, lütfen elle girin.", "{nthServer}. Server" : "{nthServer}. Sunucu", "No object found in the given Base DN. Please revise." : "Girilen Base DN içerisinde nesne bulunamadı. Lütfen gözden geçirin.", + "More than 1,000 directory entries available." : "1000'den fazla dizin şuan müsait durumdadır.", " entries available within the provided Base DN" : " girdi sağlanan Base DN içerisinde mevcut", "An error occurred. Please check the Base DN, as well as connection settings and credentials." : "Bir hata oluştu. Lütfen Base DN ile birlikte bağlantı ayarlarını ve kimlik bilgilerini denetleyin.", "Do you really want to delete the current Server Configuration?" : "Şu anki sunucu yapılandırmasını silmek istediğinizden emin misiniz?", |