diff options
70 files changed, 3458 insertions, 556 deletions
diff --git a/.htaccess b/.htaccess index 918fcbd18e8..615f5e7fc96 100644 --- a/.htaccess +++ b/.htaccess @@ -38,8 +38,8 @@ RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}] RewriteRule ^\.well-known/host-meta /public.php?service=host-meta [QSA,L] RewriteRule ^\.well-known/host-meta\.json /public.php?service=host-meta-json [QSA,L] - RewriteRule ^\.well-known/carddav /remote.php/carddav/ [R=301,L] - RewriteRule ^\.well-known/caldav /remote.php/caldav/ [R=301,L] + RewriteRule ^\.well-known/carddav /remote.php/dav/ [R=301,L] + RewriteRule ^\.well-known/caldav /remote.php/dav/ [R=301,L] RewriteRule ^remote/(.*) remote.php [QSA,L] RewriteRule ^(build|tests|config|lib|3rdparty|templates)/.* - [R=404,L] RewriteRule ^(\.|autotest|occ|issue|indie|db_|console).* - [R=404,L] diff --git a/apps/dav/appinfo/database.xml b/apps/dav/appinfo/database.xml index f3fd5079949..5e2dad097e4 100644 --- a/apps/dav/appinfo/database.xml +++ b/apps/dav/appinfo/database.xml @@ -183,4 +183,391 @@ 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> </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/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..08a2a70c56d --- /dev/null +++ b/apps/dav/lib/caldav/caldavbackend.php @@ -0,0 +1,1175 @@ +<?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 string $path + * @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 array $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|null + */ + 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|null + */ + 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/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/file.php b/apps/dav/lib/connector/sabre/file.php index 961532daf50..a0c35fb2baf 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); @@ -273,6 +277,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 +302,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); } @@ -474,6 +482,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..8032d2b3fbf 100644 --- a/apps/dav/lib/connector/sabre/lockplugin.php +++ b/apps/dav/lib/connector/sabre/lockplugin.php @@ -39,18 +39,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 +54,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 +72,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..daf82ba6f0d 100644 --- a/apps/dav/lib/connector/sabre/node.php +++ b/apps/dav/lib/connector/sabre/node.php @@ -207,6 +207,13 @@ abstract class Node implements \Sabre\DAV\INode { } /** + * @return string + */ + public function getInternalFileId() { + return $this->info->getId(); + } + + /** * @return string|null */ public function getDavPermissions() { @@ -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/serverfactory.php b/apps/dav/lib/connector/sabre/serverfactory.php index a33acc9f00b..0f0377e96bd 100644 --- a/apps/dav/lib/connector/sabre/serverfactory.php +++ b/apps/dav/lib/connector/sabre/serverfactory.php @@ -107,7 +107,7 @@ 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. diff --git a/apps/dav/lib/rootcollection.php b/apps/dav/lib/rootcollection.php index 850180d8481..10baff072cc 100644 --- a/apps/dav/lib/rootcollection.php +++ b/apps/dav/lib/rootcollection.php @@ -2,8 +2,10 @@ namespace OCA\DAV; +use OCA\DAV\CalDAV\CalDavBackend; 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; @@ -12,9 +14,10 @@ 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,18 @@ 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($db); $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 395544761ab..229f33858d9 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,9 +35,23 @@ 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()); + // addressbook plugins $this->server->addPlugin(new \Sabre\CardDAV\Plugin()); // Finder on OS X requires Class 2 WebDAV support (locking), since we do 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/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..399634f8bee 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; @@ -73,6 +74,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' ], @@ -691,6 +696,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 +863,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..2e3338fefa1 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 @@ -67,7 +70,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 +94,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 +130,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 +192,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 +233,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/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/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/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/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_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/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/provisioning_api/appinfo/routes.php b/apps/provisioning_api/appinfo/routes.php index d2cba549846..22a923f5c20 100644 --- a/apps/provisioning_api/appinfo/routes.php +++ b/apps/provisioning_api/appinfo/routes.php @@ -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/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/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?", diff --git a/build/integration/features/bootstrap/FeatureContext.php b/build/integration/features/bootstrap/FeatureContext.php index 46c86f1300b..3d579f52810 100644 --- a/build/integration/features/bootstrap/FeatureContext.php +++ b/build/integration/features/bootstrap/FeatureContext.php @@ -60,6 +60,8 @@ class FeatureContext implements Context, SnippetAcceptingContext { /** * Parses the xml answer to get ocs response which doesn't match with * http one in v1 of the api. + * @param ResponseInterface $response + * @return string */ public function getOCSResponse($response) { return $response->xml()->meta[0]->statuscode; @@ -67,6 +69,8 @@ class FeatureContext implements Context, SnippetAcceptingContext { /** * Parses the xml answer to get the array of users returned. + * @param ResponseInterface $resp + * @return array */ public function getArrayOfUsersResponded($resp) { $listCheckedElements = $resp->xml()->data[0]->users[0]->element; @@ -76,6 +80,8 @@ class FeatureContext implements Context, SnippetAcceptingContext { /** * Parses the xml answer to get the array of groups returned. + * @param ResponseInterface $resp + * @return array */ public function getArrayOfGroupsResponded($resp) { $listCheckedElements = $resp->xml()->data[0]->groups[0]->element; @@ -85,6 +91,8 @@ class FeatureContext implements Context, SnippetAcceptingContext { /** * Parses the xml answer to get the array of subadmins returned. + * @param ResponseInterface $resp + * @return array */ public function getArrayOfSubadminsResponded($resp) { $listCheckedElements = $resp->xml()->data[0]->element; @@ -94,6 +102,8 @@ class FeatureContext implements Context, SnippetAcceptingContext { /** * Parses the xml answer to get the array of apps returned. + * @param ResponseInterface $resp + * @return array */ public function getArrayOfAppsResponded($resp) { $listCheckedElements = $resp->xml()->data[0]->apps[0]->element; @@ -108,10 +118,10 @@ class FeatureContext implements Context, SnippetAcceptingContext { $a = array_map(function($subArray) { return $subArray[0]; }, $arrayOfArrays); return $a; } - + /** * @Then /^users returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $formData + * @param \Behat\Gherkin\Node\TableNode|null $usersList */ public function theUsersShouldBe($usersList) { if ($usersList instanceof \Behat\Gherkin\Node\TableNode) { @@ -125,7 +135,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { /** * @Then /^groups returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $formData + * @param \Behat\Gherkin\Node\TableNode|null $groupsList */ public function theGroupsShouldBe($groupsList) { if ($groupsList instanceof \Behat\Gherkin\Node\TableNode) { @@ -139,7 +149,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { /** * @Then /^subadmin groups returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $formData + * @param \Behat\Gherkin\Node\TableNode|null $groupsList */ public function theSubadminGroupsShouldBe($groupsList) { if ($groupsList instanceof \Behat\Gherkin\Node\TableNode) { @@ -153,7 +163,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { /** * @Then /^subadmin users returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $formData + * @param \Behat\Gherkin\Node\TableNode|null $groupsList */ public function theSubadminUsersShouldBe($groupsList) { $this->theSubadminGroupsShouldBe($groupsList); @@ -161,7 +171,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { /** * @Then /^apps returned are$/ - * @param \Behat\Gherkin\Node\TableNode|null $formData + * @param \Behat\Gherkin\Node\TableNode|null $appList */ public function theAppsShouldBe($appList) { if ($appList instanceof \Behat\Gherkin\Node\TableNode) { @@ -206,7 +216,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { */ public function assureUserExists($user) { try { - $this->userExists($user); + $this->userExists($user); } catch (\GuzzleHttp\Exception\ClientException $ex) { $previous_user = $this->currentUser; $this->currentUser = "admin"; @@ -276,7 +286,6 @@ class FeatureContext implements Context, SnippetAcceptingContext { } $this->response = $client->get($fullUrl, $options); - $subadmins = array($user); $respondedArray = $this->getArrayOfSubadminsResponded($this->response); sort($respondedArray); PHPUnit_Framework_Assert::assertContains($user, $respondedArray); @@ -295,7 +304,6 @@ class FeatureContext implements Context, SnippetAcceptingContext { } $this->response = $client->get($fullUrl, $options); - $subadmins = array($user); $respondedArray = $this->getArrayOfSubadminsResponded($this->response); sort($respondedArray); PHPUnit_Framework_Assert::assertNotContains($user, $respondedArray); @@ -498,7 +506,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { */ public function assureGroupExists($group) { try { - $this->groupExists($group); + $this->groupExists($group); } catch (\GuzzleHttp\Exception\ClientException $ex) { $previous_user = $this->currentUser; $this->currentUser = "admin"; @@ -608,8 +616,8 @@ class FeatureContext implements Context, SnippetAcceptingContext { $fileinfo = $finfo->file("./$filename", FILEINFO_MIME_TYPE); PHPUnit_Framework_Assert::assertEquals($fileinfo, "text/plain"); if (file_exists("./$filename")) { - unlink("./$filename"); - } + unlink("./$filename"); + } } /** @@ -624,7 +632,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { else{ $token = $this->lastShareData->data->token; } - + $fullUrl = substr($this->baseUrl, 0, -4) . "public.php/webdav"; $options['auth'] = [$token, $password]; $options['save_to'] = "./$filename"; @@ -633,8 +641,8 @@ class FeatureContext implements Context, SnippetAcceptingContext { $fileinfo = $finfo->file("./$filename", FILEINFO_MIME_TYPE); PHPUnit_Framework_Assert::assertEquals($fileinfo, "text/plain"); if (file_exists("./$filename")) { - unlink("./$filename"); - } + unlink("./$filename"); + } } /** @@ -658,7 +666,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { /** * @When /^Updating last share with$/ - * @param \Behat\Gherkin\Node\TableNode|null $formData + * @param \Behat\Gherkin\Node\TableNode|null $body */ public function updatingLastShare($body) { $share_id = $this->lastShareData->data[0]->id; @@ -691,11 +699,11 @@ class FeatureContext implements Context, SnippetAcceptingContext { public function createShare($user, - $path = null, - $shareType = null, - $shareWith = null, - $publicUpload = null, - $password = null, + $path = null, + $shareType = null, + $shareWith = null, + $publicUpload = null, + $password = null, $permissions = null){ $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/apps/files_sharing/api/v{$this->sharingApiVersion}/shares"; $client = new Client(); @@ -708,26 +716,26 @@ class FeatureContext implements Context, SnippetAcceptingContext { } $fd = []; if (!is_null($path)){ - $fd['path'] = $path; + $fd['path'] = $path; } if (!is_null($shareType)){ - $fd['shareType'] = $shareType; + $fd['shareType'] = $shareType; } if (!is_null($shareWith)){ - $fd['shareWith'] = $shareWith; + $fd['shareWith'] = $shareWith; } if (!is_null($publicUpload)){ - $fd['publicUpload'] = $publicUpload; + $fd['publicUpload'] = $publicUpload; } if (!is_null($password)){ - $fd['password'] = $password; + $fd['password'] = $password; } if (!is_null($permissions)){ - $fd['permissions'] = $permissions; + $fd['permissions'] = $permissions; } $options['body'] = $fd; - + try { $this->response = $client->send($client->createRequest("POST", $fullUrl, $options)); $this->lastShareData = $this->response->xml(); @@ -740,7 +748,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { public function isExpectedUrl($possibleUrl, $finalPart){ $baseUrlChopped = substr($this->baseUrl, 0, -4); $endCharacter = strlen($baseUrlChopped) + strlen($finalPart); - return (substr($possibleUrl,0,$endCharacter) == "$baseUrlChopped" . "$finalPart"); + return (substr($possibleUrl,0,$endCharacter) == "$baseUrlChopped" . "$finalPart"); } public function isFieldInResponse($field, $contentExpected){ @@ -877,7 +885,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { public static function removeFile($path, $filename){ if (file_exists("$path" . "$filename")) { unlink("$path" . "$filename"); - } + } } /** @@ -888,7 +896,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { file_put_contents("../../core/skeleton/" . "textfile" . "$i" . ".txt", "ownCloud test text file\n"); } if (!file_exists("../../core/skeleton/FOLDER")) { - mkdir("../../core/skeleton/FOLDER", 0777, true); + mkdir("../../core/skeleton/FOLDER", 0777, true); } } @@ -899,9 +907,9 @@ class FeatureContext implements Context, SnippetAcceptingContext { public static function removeFilesFromSkeleton(){ for ($i=0; $i<5; $i++){ self::removeFile("../../core/skeleton/", "textfile" . "$i" . ".txt"); - } - if (!is_dir("../../core/skeleton/FOLDER")) { - rmdir("../../core/skeleton/FOLDER"); + } + if (is_dir("../../core/skeleton/FOLDER")) { + rmdir("../../core/skeleton/FOLDER"); } } diff --git a/core/l10n/hu_HU.js b/core/l10n/hu_HU.js index 0feea423eb6..3d0276c1700 100644 --- a/core/l10n/hu_HU.js +++ b/core/l10n/hu_HU.js @@ -269,6 +269,7 @@ OC.L10N.register( "Contact your system administrator if this message persists or appeared unexpectedly." : "Ha ez az üzenet ismételten vagy indokolatlanul megjelenik, akkor keresse a rendszergazda segítségét!", "Thank you for your patience." : "Köszönjük a türelmét.", "You are accessing the server from an untrusted domain." : "A kiszolgálót nem megbízható tartományból éri el.", + "Please contact your administrator. If you are an administrator of this instance, configure the \"trusted_domains\" setting in config/config.php. An example configuration is provided in config/config.sample.php." : "Kérjük keresse fel a rendszergazdát! Ha ennek a telepítésnek Ön a rendszergazdája, akkor állítsa be a config/config.php állományban a \"trusted_domain\" paramétert! A config/config.sample.php állományban talál példát a beállításra.", "Depending on your configuration, as an administrator you might also be able to use the button below to trust this domain." : "A beállításoktól függően, rendszergazdaként lehetséges, hogy az alábbi gombot is használhatja a tartomány megbízhatóvá tételéhez.", "Add \"%s\" as trusted domain" : "Adjuk hozzá \"%s\"-t a megbízható tartományokhoz!", "App update required" : "Alkalmazás frissítés szükséges", diff --git a/core/l10n/hu_HU.json b/core/l10n/hu_HU.json index 313be232295..b1aa5f4c8ad 100644 --- a/core/l10n/hu_HU.json +++ b/core/l10n/hu_HU.json @@ -267,6 +267,7 @@ "Contact your system administrator if this message persists or appeared unexpectedly." : "Ha ez az üzenet ismételten vagy indokolatlanul megjelenik, akkor keresse a rendszergazda segítségét!", "Thank you for your patience." : "Köszönjük a türelmét.", "You are accessing the server from an untrusted domain." : "A kiszolgálót nem megbízható tartományból éri el.", + "Please contact your administrator. If you are an administrator of this instance, configure the \"trusted_domains\" setting in config/config.php. An example configuration is provided in config/config.sample.php." : "Kérjük keresse fel a rendszergazdát! Ha ennek a telepítésnek Ön a rendszergazdája, akkor állítsa be a config/config.php állományban a \"trusted_domain\" paramétert! A config/config.sample.php állományban talál példát a beállításra.", "Depending on your configuration, as an administrator you might also be able to use the button below to trust this domain." : "A beállításoktól függően, rendszergazdaként lehetséges, hogy az alábbi gombot is használhatja a tartomány megbízhatóvá tételéhez.", "Add \"%s\" as trusted domain" : "Adjuk hozzá \"%s\"-t a megbízható tartományokhoz!", "App update required" : "Alkalmazás frissítés szükséges", diff --git a/core/l10n/nl.js b/core/l10n/nl.js index f4702077004..fe5f45c95ea 100644 --- a/core/l10n/nl.js +++ b/core/l10n/nl.js @@ -269,6 +269,7 @@ OC.L10N.register( "Contact your system administrator if this message persists or appeared unexpectedly." : "Neem contact op met uw systeembeheerder als deze melding aanhoudt of onverwacht verscheen.", "Thank you for your patience." : "Bedankt voor uw geduld.", "You are accessing the server from an untrusted domain." : "U benadert de server vanaf een niet vertrouwd domein.", + "Please contact your administrator. If you are an administrator of this instance, configure the \"trusted_domains\" setting in config/config.php. An example configuration is provided in config/config.sample.php." : "Neem contact op met uw beheerder. Als u de beheerder van deze service bent, configureer dan de \"trusted_domains\" instelling in config/config.php. Een voorbeeldconfiguratie is gegeven in config/config.sample.php.", "Depending on your configuration, as an administrator you might also be able to use the button below to trust this domain." : "Afhankelijk van uw configuratie zou u als beheerder ook de onderstaande knop kunnen gebruiken om dit domein te vertrouwen.", "Add \"%s\" as trusted domain" : "\"%s\" toevoegen als vertrouwd domein", "App update required" : "Bijwerken App vereist", diff --git a/core/l10n/nl.json b/core/l10n/nl.json index a86bbe3d4af..117070e8399 100644 --- a/core/l10n/nl.json +++ b/core/l10n/nl.json @@ -267,6 +267,7 @@ "Contact your system administrator if this message persists or appeared unexpectedly." : "Neem contact op met uw systeembeheerder als deze melding aanhoudt of onverwacht verscheen.", "Thank you for your patience." : "Bedankt voor uw geduld.", "You are accessing the server from an untrusted domain." : "U benadert de server vanaf een niet vertrouwd domein.", + "Please contact your administrator. If you are an administrator of this instance, configure the \"trusted_domains\" setting in config/config.php. An example configuration is provided in config/config.sample.php." : "Neem contact op met uw beheerder. Als u de beheerder van deze service bent, configureer dan de \"trusted_domains\" instelling in config/config.php. Een voorbeeldconfiguratie is gegeven in config/config.sample.php.", "Depending on your configuration, as an administrator you might also be able to use the button below to trust this domain." : "Afhankelijk van uw configuratie zou u als beheerder ook de onderstaande knop kunnen gebruiken om dit domein te vertrouwen.", "Add \"%s\" as trusted domain" : "\"%s\" toevoegen als vertrouwd domein", "App update required" : "Bijwerken App vereist", diff --git a/core/l10n/ru.js b/core/l10n/ru.js index 2df910a6aa4..39302e9a18e 100644 --- a/core/l10n/ru.js +++ b/core/l10n/ru.js @@ -177,6 +177,7 @@ OC.L10N.register( "Hello {name}" : "Здравствуйте {name}", "_download %n file_::_download %n files_" : ["скачать %n файл","скачать %n файла","скачать %n файлов","скачать %n файлов"], "{version} is available. Get more information on how to update." : "Доступна версия {version}. Получить дополнительную информацию о порядке обновления.", + "The upgrade is in progress, leaving this page might interrupt the process in some environments." : "Идет обновление. Покидая эту страницу, вы можете прервать процесс обновления.", "Updating {productName} to version {version}, this may take a while." : "Идет обновление {productName} до версии {version}, пожалуйста, подождите.", "An error occurred." : "Произошла ошибка.", "Please reload the page." : "Обновите страницу.", @@ -268,6 +269,7 @@ OC.L10N.register( "Contact your system administrator if this message persists or appeared unexpectedly." : "Обратитесь к вашему системному администратору если это сообщение не исчезает или появляется неожиданно.", "Thank you for your patience." : "Спасибо за терпение.", "You are accessing the server from an untrusted domain." : "Вы пытаетесь получить доступ к серверу с неподтверждённого домена.", + "Please contact your administrator. If you are an administrator of this instance, configure the \"trusted_domains\" setting in config/config.php. An example configuration is provided in config/config.sample.php." : "Пожалуйста, обратитесь к администратору. Если вы являетесь администратором этого экземпляра, настроить параметры \"trusted_domains\" можно в config/config.php. Пример настройки можно найти в config/config.sample.php.", "Depending on your configuration, as an administrator you might also be able to use the button below to trust this domain." : "В зависимости от конфигурации, как администратор вы можете также внести домен в доверенные с помощью кнопки ниже.", "Add \"%s\" as trusted domain" : "Добавить \"%s\" как доверенный домен", "App update required" : "Требуется обновление приложения", diff --git a/core/l10n/ru.json b/core/l10n/ru.json index 66d3b463899..a24ee0831a0 100644 --- a/core/l10n/ru.json +++ b/core/l10n/ru.json @@ -175,6 +175,7 @@ "Hello {name}" : "Здравствуйте {name}", "_download %n file_::_download %n files_" : ["скачать %n файл","скачать %n файла","скачать %n файлов","скачать %n файлов"], "{version} is available. Get more information on how to update." : "Доступна версия {version}. Получить дополнительную информацию о порядке обновления.", + "The upgrade is in progress, leaving this page might interrupt the process in some environments." : "Идет обновление. Покидая эту страницу, вы можете прервать процесс обновления.", "Updating {productName} to version {version}, this may take a while." : "Идет обновление {productName} до версии {version}, пожалуйста, подождите.", "An error occurred." : "Произошла ошибка.", "Please reload the page." : "Обновите страницу.", @@ -266,6 +267,7 @@ "Contact your system administrator if this message persists or appeared unexpectedly." : "Обратитесь к вашему системному администратору если это сообщение не исчезает или появляется неожиданно.", "Thank you for your patience." : "Спасибо за терпение.", "You are accessing the server from an untrusted domain." : "Вы пытаетесь получить доступ к серверу с неподтверждённого домена.", + "Please contact your administrator. If you are an administrator of this instance, configure the \"trusted_domains\" setting in config/config.php. An example configuration is provided in config/config.sample.php." : "Пожалуйста, обратитесь к администратору. Если вы являетесь администратором этого экземпляра, настроить параметры \"trusted_domains\" можно в config/config.php. Пример настройки можно найти в config/config.sample.php.", "Depending on your configuration, as an administrator you might also be able to use the button below to trust this domain." : "В зависимости от конфигурации, как администратор вы можете также внести домен в доверенные с помощью кнопки ниже.", "Add \"%s\" as trusted domain" : "Добавить \"%s\" как доверенный домен", "App update required" : "Требуется обновление приложения", diff --git a/lib/l10n/fr.js b/lib/l10n/fr.js index e0ac89cbec8..305dea06792 100644 --- a/lib/l10n/fr.js +++ b/lib/l10n/fr.js @@ -141,7 +141,7 @@ OC.L10N.register( "Please upgrade your database version" : "Veuillez mettre à jour votre gestionnaire de base de données", "Error occurred while checking PostgreSQL version" : "Une erreur s’est produite pendant la récupération du numéro de version de PostgreSQL", "Please make sure you have PostgreSQL >= 9 or check the logs for more information about the error" : "Veuillez vérifier que vous utilisez PostgreSQL >= 9 , ou regardez dans le journal d’erreur pour plus d’informations sur ce problème", - "Please change the permissions to 0770 so that the directory cannot be listed by other users." : "Veuillez changer les permissions du répertoire en mode 0770 afin que son contenu puisse être listé par les autres utilisateurs.", + "Please change the permissions to 0770 so that the directory cannot be listed by other users." : "Veuillez changer les permissions du répertoire en mode 0770 afin que son contenu ne puisse pas être listé par les autres utilisateurs.", "Data directory (%s) is readable by other users" : "Le répertoire de données (%s) est lisible par les autres utilisateurs", "Data directory (%s) must be an absolute path" : "Le chemin du dossier de données (%s) doit être absolu", "Check the value of \"datadirectory\" in your configuration" : "Verifiez la valeur de \"datadirectory\" dans votre configuration", diff --git a/lib/l10n/fr.json b/lib/l10n/fr.json index 2d74c986a50..9ca7bce8d10 100644 --- a/lib/l10n/fr.json +++ b/lib/l10n/fr.json @@ -139,7 +139,7 @@ "Please upgrade your database version" : "Veuillez mettre à jour votre gestionnaire de base de données", "Error occurred while checking PostgreSQL version" : "Une erreur s’est produite pendant la récupération du numéro de version de PostgreSQL", "Please make sure you have PostgreSQL >= 9 or check the logs for more information about the error" : "Veuillez vérifier que vous utilisez PostgreSQL >= 9 , ou regardez dans le journal d’erreur pour plus d’informations sur ce problème", - "Please change the permissions to 0770 so that the directory cannot be listed by other users." : "Veuillez changer les permissions du répertoire en mode 0770 afin que son contenu puisse être listé par les autres utilisateurs.", + "Please change the permissions to 0770 so that the directory cannot be listed by other users." : "Veuillez changer les permissions du répertoire en mode 0770 afin que son contenu ne puisse pas être listé par les autres utilisateurs.", "Data directory (%s) is readable by other users" : "Le répertoire de données (%s) est lisible par les autres utilisateurs", "Data directory (%s) must be an absolute path" : "Le chemin du dossier de données (%s) doit être absolu", "Check the value of \"datadirectory\" in your configuration" : "Verifiez la valeur de \"datadirectory\" dans votre configuration", diff --git a/lib/l10n/hu_HU.js b/lib/l10n/hu_HU.js index d065c509460..df162844485 100644 --- a/lib/l10n/hu_HU.js +++ b/lib/l10n/hu_HU.js @@ -11,6 +11,7 @@ OC.L10N.register( "PHP with a version lower than %s is required." : "Ennél régebbi PHP szükséges: %s.", "Following databases are supported: %s" : "A következő adatbázis nem támogatott: %s", "The library %s is not available." : "A könyvtár %s nem áll rendelkezésre.", + "Following platforms are supported: %s" : "Ezek a platformok támogatottak: %s", "ownCloud %s or higher is required." : "ownCoud %s vagy ennél újabb szükséges.", "ownCloud %s or lower is required." : "ownCoud %s vagy ennél régebbi szükséges.", "Help" : "Súgó", @@ -36,7 +37,9 @@ OC.L10N.register( "Dot files are not allowed" : "Pontozott fájlok nem engedétlyezettek", "4-byte characters are not supported in file names" : "4-byte karakterek nem támogatottak a fájl nevekben.", "File name is a reserved word" : "A fajl neve egy rezervált szó", + "File name contains at least one invalid character" : "A fájlnév legalább egy érvénytelen karaktert tartalmaz!", "File name is too long" : "A fájlnév túl hosszú!", + "File is currently busy, please try again later" : "A fájl jelenleg elfoglalt, kérjük próbáld újra később!", "Can't read file" : "Nem olvasható a fájl", "App directory already exists" : "Az alkalmazás mappája már létezik", "Can't create app folder. Please fix permissions. %s" : "Nem lehetett létrehozni az alkalmazás mappáját. Kérem ellenőrizze a jogosultságokat. %s", @@ -70,25 +73,32 @@ OC.L10N.register( "Set an admin username." : "Állítson be egy felhasználói nevet az adminisztrációhoz.", "Set an admin password." : "Állítson be egy jelszót az adminisztrációhoz.", "Can't create or write into the data directory %s" : "Nem sikerült létrehozni vagy irni a \"data\" könyvtárba %s", + "Invalid Federated Cloud ID" : "Érvénytelen Egyesített Felhő Azonosító", "%s shared »%s« with you" : "%s megosztotta Önnel ezt: »%s«", "%s via %s" : "%s über %s", + "Sharing %s failed, because the backend does not allow shares from type %i" : "%s megosztása sikertelen, mert a megosztási alrendszer nem engedi a %l típus megosztását", "Sharing %s failed, because the file does not exist" : "%s megosztása sikertelen, mert a fájl nem létezik", "You are not allowed to share %s" : "Önnek nincs jogosultsága %s megosztására", + "Sharing %s failed, because you can not share with yourself" : "%s megosztása sikertelen, mert magaddal nem oszthatod meg", "Sharing %s failed, because the user %s does not exist" : "%s megosztása nem sikerült, mert %s felhasználó nem létezik", "Sharing %s failed, because the user %s is not a member of any groups that %s is a member of" : "%s megosztása nem sikerült, mert %s felhasználó nem tagja egyik olyan csoportnak sem, aminek %s tagja", "Sharing %s failed, because this item is already shared with %s" : "%s megosztása nem sikerült, mert ez már meg van osztva %s-vel", + "Sharing %s failed, because this item is already shared with user %s" : "%s megosztása sikertelen, mert már meg van osztva %s felhasználóval", "Sharing %s failed, because the group %s does not exist" : "%s megosztása nem sikerült, mert %s csoport nem létezik", "Sharing %s failed, because %s is not a member of the group %s" : "%s megosztása nem sikerült, mert %s felhasználó nem tagja a %s csoportnak", "You need to provide a password to create a public link, only protected links are allowed" : "Meg kell adnia egy jelszót is, mert a nyilvános linkek csak jelszóval védetten használhatók", "Sharing %s failed, because sharing with links is not allowed" : "%s megosztása nem sikerült, mert a linkekkel történő megosztás nincs engedélyezve", + "Sharing %s failed, could not find %s, maybe the server is currently unreachable." : "%s megosztása sikertelen, mert %s nem található, talán a szerver jelenleg nem elérhető.", "Share type %s is not valid for %s" : "A %s megosztási típus nem érvényes %s-re", "Setting permissions for %s failed, because the permissions exceed permissions granted to %s" : "Nem sikerült %s-re beállítani az elérési jogosultságokat, mert a megadottak túllépik a %s-re érvényes jogosultságokat", "Setting permissions for %s failed, because the item was not found" : "Nem sikerült %s-re beállítani az elérési jogosultságokat, mert a kérdéses állomány nem található", "Cannot set expiration date. Shares cannot expire later than %s after they have been shared" : "Nem lehet beállítani a lejárati időt. A megosztások legfeljebb ennyi idővel járhatnak le a létrehozásukat követően: %s", "Cannot set expiration date. Expiration date is in the past" : "Nem lehet beállítani a lejárati időt, mivel a megadott lejárati időpont már elmúlt.", + "Cannot clear expiration date. Shares are required to have an expiration date." : "Nem lehet beállítani a lejárati időt. A megosztásoknak kötelező megadni lejárati időt!", "Sharing backend %s must implement the interface OCP\\Share_Backend" : "Az %s megosztási alrendszernek támogatnia kell az OCP\\Share_Backend interface-t", "Sharing backend %s not found" : "A %s megosztási alrendszer nem található", "Sharing backend for %s not found" : "%s megosztási alrendszere nem található", + "Sharing failed, because the user %s is the original sharer" : "Megosztás sikertelen, mert %s felhasználó az eredeti megosztó", "Sharing %s failed, because the permissions exceed permissions granted to %s" : "%s megosztása nem sikerült, mert a jogosultságok túllépik azt, ami %s rendelkezésére áll", "Sharing %s failed, because resharing is not allowed" : "%s megosztása nem sikerült, mert a megosztás továbbadása nincs engedélyezve", "Sharing %s failed, because the sharing backend for %s could not find its source" : "%s megosztása nem sikerült, mert %s megosztási alrendszere nem találja", @@ -111,6 +121,7 @@ OC.L10N.register( "Please install one of these locales on your system and restart your webserver." : "Kérjük állítsa be a következő lokalizációk valamelyikét a rendszeren és indítsa újra a webszervert!", "Please ask your server administrator to install the module." : "Kérje meg a rendszergazdát, hogy telepítse a modult!", "PHP module %s not installed." : "A %s PHP modul nincs telepítve.", + "PHP setting \"%s\" is not set to \"%s\"." : "%s PHP beállítás nincs \"%s\"-re állítva.", "This is probably caused by a cache/accelerator such as Zend OPcache or eAccelerator." : "Ezt valószínűleg egy gyorsítótár ill. kódgyorsító, mint pl, a Zend, OPcache vagy eAccelererator okozza.", "PHP modules have been installed, but they are still listed as missing?" : "A PHP modulok telepítve vannak, de a listában mégsincsenek felsorolva?", "Please ask your server administrator to restart the web server." : "Kérje meg a rendszergazdát, hogy indítsa újra a webszervert!", @@ -120,6 +131,7 @@ OC.L10N.register( "Please make sure you have PostgreSQL >= 9 or check the logs for more information about the error" : "Kérjük gondoskodjon róla, hogy a PostgreSQL legalább 9-es verziójú legyen, vagy ellenőrizze a naplófájlokat, hogy mi okozta a hibát!", "Please change the permissions to 0770 so that the directory cannot be listed by other users." : "Kérjük módosítsa a könyvtár elérhetőségi engedélybeállítását 0770-re, hogy a tartalmát más felhasználó ne listázhassa!", "Data directory (%s) is readable by other users" : "Az adatkönyvtár (%s) más felhasználók számára is olvasható ", + "Data directory (%s) must be an absolute path" : "Az adatkönyvtárnak (%s) abszolút elérési útnak kell lennie", "Data directory (%s) is invalid" : "Érvénytelen a megadott adatkönyvtár (%s) ", "Please check that the data directory contains a file \".ocdata\" in its root." : "Kérjük ellenőrizze, hogy az adatkönyvtár tartalmaz a gyökerében egy \".ocdata\" nevű állományt!", "Could not obtain lock type %d on \"%s\"." : "Nem sikerült %d típusú zárolást elérni itt: \"%s\".", diff --git a/lib/l10n/hu_HU.json b/lib/l10n/hu_HU.json index 4f864c1d75d..a94321be0b6 100644 --- a/lib/l10n/hu_HU.json +++ b/lib/l10n/hu_HU.json @@ -9,6 +9,7 @@ "PHP with a version lower than %s is required." : "Ennél régebbi PHP szükséges: %s.", "Following databases are supported: %s" : "A következő adatbázis nem támogatott: %s", "The library %s is not available." : "A könyvtár %s nem áll rendelkezésre.", + "Following platforms are supported: %s" : "Ezek a platformok támogatottak: %s", "ownCloud %s or higher is required." : "ownCoud %s vagy ennél újabb szükséges.", "ownCloud %s or lower is required." : "ownCoud %s vagy ennél régebbi szükséges.", "Help" : "Súgó", @@ -34,7 +35,9 @@ "Dot files are not allowed" : "Pontozott fájlok nem engedétlyezettek", "4-byte characters are not supported in file names" : "4-byte karakterek nem támogatottak a fájl nevekben.", "File name is a reserved word" : "A fajl neve egy rezervált szó", + "File name contains at least one invalid character" : "A fájlnév legalább egy érvénytelen karaktert tartalmaz!", "File name is too long" : "A fájlnév túl hosszú!", + "File is currently busy, please try again later" : "A fájl jelenleg elfoglalt, kérjük próbáld újra később!", "Can't read file" : "Nem olvasható a fájl", "App directory already exists" : "Az alkalmazás mappája már létezik", "Can't create app folder. Please fix permissions. %s" : "Nem lehetett létrehozni az alkalmazás mappáját. Kérem ellenőrizze a jogosultságokat. %s", @@ -68,25 +71,32 @@ "Set an admin username." : "Állítson be egy felhasználói nevet az adminisztrációhoz.", "Set an admin password." : "Állítson be egy jelszót az adminisztrációhoz.", "Can't create or write into the data directory %s" : "Nem sikerült létrehozni vagy irni a \"data\" könyvtárba %s", + "Invalid Federated Cloud ID" : "Érvénytelen Egyesített Felhő Azonosító", "%s shared »%s« with you" : "%s megosztotta Önnel ezt: »%s«", "%s via %s" : "%s über %s", + "Sharing %s failed, because the backend does not allow shares from type %i" : "%s megosztása sikertelen, mert a megosztási alrendszer nem engedi a %l típus megosztását", "Sharing %s failed, because the file does not exist" : "%s megosztása sikertelen, mert a fájl nem létezik", "You are not allowed to share %s" : "Önnek nincs jogosultsága %s megosztására", + "Sharing %s failed, because you can not share with yourself" : "%s megosztása sikertelen, mert magaddal nem oszthatod meg", "Sharing %s failed, because the user %s does not exist" : "%s megosztása nem sikerült, mert %s felhasználó nem létezik", "Sharing %s failed, because the user %s is not a member of any groups that %s is a member of" : "%s megosztása nem sikerült, mert %s felhasználó nem tagja egyik olyan csoportnak sem, aminek %s tagja", "Sharing %s failed, because this item is already shared with %s" : "%s megosztása nem sikerült, mert ez már meg van osztva %s-vel", + "Sharing %s failed, because this item is already shared with user %s" : "%s megosztása sikertelen, mert már meg van osztva %s felhasználóval", "Sharing %s failed, because the group %s does not exist" : "%s megosztása nem sikerült, mert %s csoport nem létezik", "Sharing %s failed, because %s is not a member of the group %s" : "%s megosztása nem sikerült, mert %s felhasználó nem tagja a %s csoportnak", "You need to provide a password to create a public link, only protected links are allowed" : "Meg kell adnia egy jelszót is, mert a nyilvános linkek csak jelszóval védetten használhatók", "Sharing %s failed, because sharing with links is not allowed" : "%s megosztása nem sikerült, mert a linkekkel történő megosztás nincs engedélyezve", + "Sharing %s failed, could not find %s, maybe the server is currently unreachable." : "%s megosztása sikertelen, mert %s nem található, talán a szerver jelenleg nem elérhető.", "Share type %s is not valid for %s" : "A %s megosztási típus nem érvényes %s-re", "Setting permissions for %s failed, because the permissions exceed permissions granted to %s" : "Nem sikerült %s-re beállítani az elérési jogosultságokat, mert a megadottak túllépik a %s-re érvényes jogosultságokat", "Setting permissions for %s failed, because the item was not found" : "Nem sikerült %s-re beállítani az elérési jogosultságokat, mert a kérdéses állomány nem található", "Cannot set expiration date. Shares cannot expire later than %s after they have been shared" : "Nem lehet beállítani a lejárati időt. A megosztások legfeljebb ennyi idővel járhatnak le a létrehozásukat követően: %s", "Cannot set expiration date. Expiration date is in the past" : "Nem lehet beállítani a lejárati időt, mivel a megadott lejárati időpont már elmúlt.", + "Cannot clear expiration date. Shares are required to have an expiration date." : "Nem lehet beállítani a lejárati időt. A megosztásoknak kötelező megadni lejárati időt!", "Sharing backend %s must implement the interface OCP\\Share_Backend" : "Az %s megosztási alrendszernek támogatnia kell az OCP\\Share_Backend interface-t", "Sharing backend %s not found" : "A %s megosztási alrendszer nem található", "Sharing backend for %s not found" : "%s megosztási alrendszere nem található", + "Sharing failed, because the user %s is the original sharer" : "Megosztás sikertelen, mert %s felhasználó az eredeti megosztó", "Sharing %s failed, because the permissions exceed permissions granted to %s" : "%s megosztása nem sikerült, mert a jogosultságok túllépik azt, ami %s rendelkezésére áll", "Sharing %s failed, because resharing is not allowed" : "%s megosztása nem sikerült, mert a megosztás továbbadása nincs engedélyezve", "Sharing %s failed, because the sharing backend for %s could not find its source" : "%s megosztása nem sikerült, mert %s megosztási alrendszere nem találja", @@ -109,6 +119,7 @@ "Please install one of these locales on your system and restart your webserver." : "Kérjük állítsa be a következő lokalizációk valamelyikét a rendszeren és indítsa újra a webszervert!", "Please ask your server administrator to install the module." : "Kérje meg a rendszergazdát, hogy telepítse a modult!", "PHP module %s not installed." : "A %s PHP modul nincs telepítve.", + "PHP setting \"%s\" is not set to \"%s\"." : "%s PHP beállítás nincs \"%s\"-re állítva.", "This is probably caused by a cache/accelerator such as Zend OPcache or eAccelerator." : "Ezt valószínűleg egy gyorsítótár ill. kódgyorsító, mint pl, a Zend, OPcache vagy eAccelererator okozza.", "PHP modules have been installed, but they are still listed as missing?" : "A PHP modulok telepítve vannak, de a listában mégsincsenek felsorolva?", "Please ask your server administrator to restart the web server." : "Kérje meg a rendszergazdát, hogy indítsa újra a webszervert!", @@ -118,6 +129,7 @@ "Please make sure you have PostgreSQL >= 9 or check the logs for more information about the error" : "Kérjük gondoskodjon róla, hogy a PostgreSQL legalább 9-es verziójú legyen, vagy ellenőrizze a naplófájlokat, hogy mi okozta a hibát!", "Please change the permissions to 0770 so that the directory cannot be listed by other users." : "Kérjük módosítsa a könyvtár elérhetőségi engedélybeállítását 0770-re, hogy a tartalmát más felhasználó ne listázhassa!", "Data directory (%s) is readable by other users" : "Az adatkönyvtár (%s) más felhasználók számára is olvasható ", + "Data directory (%s) must be an absolute path" : "Az adatkönyvtárnak (%s) abszolút elérési útnak kell lennie", "Data directory (%s) is invalid" : "Érvénytelen a megadott adatkönyvtár (%s) ", "Please check that the data directory contains a file \".ocdata\" in its root." : "Kérjük ellenőrizze, hogy az adatkönyvtár tartalmaz a gyökerében egy \".ocdata\" nevű állományt!", "Could not obtain lock type %d on \"%s\"." : "Nem sikerült %d típusú zárolást elérni itt: \"%s\".", diff --git a/lib/private/appframework/dependencyinjection/dicontainer.php b/lib/private/appframework/dependencyinjection/dicontainer.php index b0be4045541..a32d32fefc5 100644 --- a/lib/private/appframework/dependencyinjection/dicontainer.php +++ b/lib/private/appframework/dependencyinjection/dicontainer.php @@ -226,6 +226,10 @@ class DIContainer extends SimpleContainer implements IAppContainer { return $this->getServer(); }); + $this->registerService('Symfony\Component\EventDispatcher\EventDispatcherInterface', function ($c) { + return $this->getServer()->getEventDispatcher(); + }); + $this->registerService('OCP\\AppFramework\\IAppContainer', function ($c) { return $c; }); diff --git a/lib/private/cache/file.php b/lib/private/cache/file.php index 1cda05f28e5..31d4718d18a 100644 --- a/lib/private/cache/file.php +++ b/lib/private/cache/file.php @@ -185,6 +185,8 @@ class File implements ICache { } catch (\OCP\Lock\LockedException $e) { // ignore locked chunks \OC::$server->getLogger()->debug('Could not cleanup locked chunk "' . $file . '"', array('app' => 'core')); + } catch (\OCP\Files\ForbiddenException $e) { + \OC::$server->getLogger()->debug('Could not cleanup forbidden chunk "' . $file . '"', array('app' => 'core')); } catch (\OCP\Files\LockNotAcquiredException $e) { \OC::$server->getLogger()->debug('Could not cleanup locked chunk "' . $file . '"', array('app' => 'core')); } diff --git a/lib/private/files.php b/lib/private/files.php index 9be5fc9a12f..af10f3e1e32 100644 --- a/lib/private/files.php +++ b/lib/private/files.php @@ -142,6 +142,11 @@ class OC_Files { $l = \OC::$server->getL10N('core'); $hint = method_exists($ex, 'getHint') ? $ex->getHint() : ''; \OC_Template::printErrorPage($l->t('File is currently busy, please try again later'), $hint); + } catch (\OCP\Files\ForbiddenException $ex) { + self::unlockAllTheFiles($dir, $files, $getType, $view, $filename); + OC::$server->getLogger()->logException($ex); + $l = \OC::$server->getL10N('core'); + \OC_Template::printErrorPage($l->t('Can\'t read file'), $ex->getMessage()); } catch (\Exception $ex) { self::unlockAllTheFiles($dir, $files, $getType, $view, $filename); OC::$server->getLogger()->logException($ex); diff --git a/lib/private/files/cache/scanner.php b/lib/private/files/cache/scanner.php index 35043029dc3..983e12d7639 100644 --- a/lib/private/files/cache/scanner.php +++ b/lib/private/files/cache/scanner.php @@ -432,6 +432,8 @@ class Scanner extends BasicEmitter { // skip unavailable storages } catch (\OCP\Files\StorageNotAvailableException $e) { // skip unavailable storages + } catch (\OCP\Files\ForbiddenException $e) { + // skip forbidden storages } catch (\OCP\Lock\LockedException $e) { // skip unavailable storages } diff --git a/lib/private/files/view.php b/lib/private/files/view.php index 7dd83588ec6..cee4b182425 100644 --- a/lib/private/files/view.php +++ b/lib/private/files/view.php @@ -46,11 +46,13 @@ namespace OC\Files; use Icewind\Streams\CallbackWrapper; use OC\Files\Cache\Updater; use OC\Files\Mount\MoveableMount; +use OC\User\User; use OCP\Files\FileNameTooLongException; use OCP\Files\InvalidCharacterInPathException; use OCP\Files\InvalidPathException; use OCP\Files\NotFoundException; use OCP\Files\ReservedWordException; +use OCP\IUser; use OCP\Lock\ILockingProvider; use OCP\Lock\LockedException; @@ -687,14 +689,14 @@ class View { } else { $result = false; } - // moving a file/folder within the same mount point + // moving a file/folder within the same mount point } elseif ($storage1 == $storage2) { if ($storage1) { $result = $storage1->rename($internalPath1, $internalPath2); } else { $result = false; } - // moving a file/folder between storages (from $storage1 to $storage2) + // moving a file/folder between storages (from $storage1 to $storage2) } else { $result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2); } @@ -1164,6 +1166,19 @@ class View { } /** + * @param string $ownerId + * @return \OC\User\User + */ + private function getUserObjectForOwner($ownerId) { + $owner = \OC::$server->getUserManager()->get($ownerId); + if ($owner instanceof IUser) { + return $owner; + } else { + return new User($ownerId, null); + } + } + + /** * get the filesystem info * * @param string $path @@ -1250,7 +1265,7 @@ class View { $data['permissions'] |= \OCP\Constants::PERMISSION_DELETE; } - $owner = \OC::$server->getUserManager()->get($storage->getOwner($internalPath)); + $owner = $this->getUserObjectForOwner($storage->getOwner($internalPath)); return new FileInfo($path, $storage, $internalPath, $data, $mount, $owner); } @@ -1317,7 +1332,7 @@ class View { if (\OCP\Util::isSharingDisabledForUser()) { $content['permissions'] = $content['permissions'] & ~\OCP\Constants::PERMISSION_SHARE; } - $owner = \OC::$server->getUserManager()->get($storage->getOwner($content['path'])); + $owner = $this->getUserObjectForOwner($storage->getOwner($content['path'])); $files[] = new FileInfo($path . '/' . $content['name'], $storage, $content['path'], $content, $mount, $owner); } @@ -1387,7 +1402,7 @@ class View { $rootEntry['permissions'] = $rootEntry['permissions'] & ~\OCP\Constants::PERMISSION_SHARE; } - $owner = \OC::$server->getUserManager()->get($subStorage->getOwner('')); + $owner = $this->getUserObjectForOwner($subStorage->getOwner('')); $files[] = new FileInfo($path . '/' . $rootEntry['name'], $subStorage, '', $rootEntry, $mount, $owner); } } diff --git a/lib/private/log/owncloud.php b/lib/private/log/owncloud.php index 1af989588d1..721bc6002fa 100644 --- a/lib/private/log/owncloud.php +++ b/lib/private/log/owncloud.php @@ -66,13 +66,16 @@ class OC_Log_Owncloud { // default to ISO8601 $format = $config->getValue('logdateformat', 'c'); - $logtimezone = $config->getValue( "logtimezone", 'UTC' ); + $logTimeZone = $config->getValue( "logtimezone", 'UTC' ); try { - $timezone = new DateTimeZone($logtimezone); + $timezone = new DateTimeZone($logTimeZone); } catch (Exception $e) { $timezone = new DateTimeZone('UTC'); } $time = DateTime::createFromFormat("U.u", microtime(true), $timezone); + if ($time === false) { + $time = new DateTime(null, $timezone); + } $request = \OC::$server->getRequest(); $reqId = $request->getId(); $remoteAddr = $request->getRemoteAddress(); diff --git a/lib/private/memcache/memcached.php b/lib/private/memcache/memcached.php index ce7c6fa9577..22b54f7bc95 100644 --- a/lib/private/memcache/memcached.php +++ b/lib/private/memcache/memcached.php @@ -41,9 +41,9 @@ class Memcached extends Cache implements IMemcache { parent::__construct($prefix); if (is_null(self::$cache)) { self::$cache = new \Memcached(); - $servers = \OC_Config::getValue('memcached_servers'); + $servers = \OC::$server->getSystemConfig()->getValue('memcached_servers'); if (!$servers) { - $server = \OC_Config::getValue('memcached_server'); + $server = \OC::$server->getSystemConfig()->getValue('memcached_server'); if ($server) { $servers = array($server); } else { @@ -72,10 +72,12 @@ class Memcached extends Cache implements IMemcache { public function set($key, $value, $ttl = 0) { if ($ttl > 0) { - return self::$cache->set($this->getNamespace() . $key, $value, $ttl); + $result = self::$cache->set($this->getNamespace() . $key, $value, $ttl); } else { - return self::$cache->set($this->getNamespace() . $key, $value); + $result = self::$cache->set($this->getNamespace() . $key, $value); } + $this->verifyReturnCode(); + return $result; } public function hasKey($key) { @@ -84,7 +86,9 @@ class Memcached extends Cache implements IMemcache { } public function remove($key) { - return self::$cache->delete($this->getNamespace() . $key); + $result= self::$cache->delete($this->getNamespace() . $key); + $this->verifyReturnCode(); + return $result; } public function clear($prefix = '') { @@ -121,7 +125,9 @@ class Memcached extends Cache implements IMemcache { * @return bool */ public function add($key, $value, $ttl = 0) { - return self::$cache->add($this->getPrefix() . $key, $value, $ttl); + $result = self::$cache->add($this->getPrefix() . $key, $value, $ttl); + $this->verifyReturnCode(); + return $result; } /** @@ -133,7 +139,9 @@ class Memcached extends Cache implements IMemcache { */ public function inc($key, $step = 1) { $this->add($key, 0); - return self::$cache->increment($this->getPrefix() . $key, $step); + $result = self::$cache->increment($this->getPrefix() . $key, $step); + $this->verifyReturnCode(); + return $result; } /** @@ -144,10 +152,24 @@ class Memcached extends Cache implements IMemcache { * @return int | bool */ public function dec($key, $step = 1) { - return self::$cache->decrement($this->getPrefix() . $key, $step); + $result = self::$cache->decrement($this->getPrefix() . $key, $step); + $this->verifyReturnCode(); + return $result; } static public function isAvailable() { return extension_loaded('memcached'); } + + /** + * @throws \Exception + */ + private function verifyReturnCode() { + $code = self::$cache->getResultCode(); + if ($code === \Memcached::RES_SUCCESS) { + return; + } + $message = self::$cache->getResultMessage(); + throw new \Exception("Error $code interacting with memcached : $message"); + } } diff --git a/lib/private/notification/action.php b/lib/private/notification/action.php index 6de8a1a4bbc..8eade697025 100644 --- a/lib/private/notification/action.php +++ b/lib/private/notification/action.php @@ -39,6 +39,9 @@ class Action implements IAction { /** @var string */ protected $icon; + /** @var bool */ + protected $primary; + /** * Constructor */ @@ -47,7 +50,7 @@ class Action implements IAction { $this->labelParsed = ''; $this->link = ''; $this->requestType = ''; - $this->icon = ''; + $this->primary = false; } /** @@ -95,6 +98,29 @@ class Action implements IAction { } /** + * @param $primary bool + * @return $this + * @throws \InvalidArgumentException if $primary is invalid + * @since 9.0.0 + */ + public function setPrimary($primary) { + if (!is_bool($primary)) { + throw new \InvalidArgumentException('The given primary option is invalid'); + } + + $this->primary = $primary; + return $this; + } + + /** + * @return bool + * @since 9.0.0 + */ + public function isPrimary() { + return $this->primary; + } + + /** * @param string $link * @param string $requestType * @return $this @@ -130,28 +156,6 @@ class Action implements IAction { } /** - * @param string $icon - * @return $this - * @throws \InvalidArgumentException if the icon is invalid - * @since 8.2.0 - */ - public function setIcon($icon) { - if (!is_string($icon) || $icon === '' || isset($icon[64])) { - throw new \InvalidArgumentException('The given icon is invalid'); - } - $this->icon = $icon; - return $this; - } - - /** - * @return string - * @since 8.2.0 - */ - public function getIcon() { - return $this->icon; - } - - /** * @return bool */ public function isValid() { diff --git a/lib/private/notification/iaction.php b/lib/private/notification/iaction.php index da6728f5c52..f7366dd4388 100644 --- a/lib/private/notification/iaction.php +++ b/lib/private/notification/iaction.php @@ -61,6 +61,20 @@ interface IAction { public function getParsedLabel(); /** + * @param $primary bool + * @return $this + * @throws \InvalidArgumentException if $primary is invalid + * @since 9.0.0 + */ + public function setPrimary($primary); + + /** + * @return bool + * @since 9.0.0 + */ + public function isPrimary(); + + /** * @param string $link * @param string $requestType * @return $this @@ -82,20 +96,6 @@ interface IAction { public function getRequestType(); /** - * @param string $icon - * @return $this - * @throws \InvalidArgumentException if the icon is invalid - * @since 8.2.0 - */ - public function setIcon($icon); - - /** - * @return string - * @since 8.2.0 - */ - public function getIcon(); - - /** * @return bool * @since 8.2.0 */ diff --git a/lib/private/notification/inotification.php b/lib/private/notification/inotification.php index faf5db1d24c..a8bf5b110ab 100644 --- a/lib/private/notification/inotification.php +++ b/lib/private/notification/inotification.php @@ -180,20 +180,6 @@ interface INotification { public function getLink(); /** - * @param string $icon - * @return $this - * @throws \InvalidArgumentException if the icon are invalid - * @since 8.2.0 - */ - public function setIcon($icon); - - /** - * @return string - * @since 8.2.0 - */ - public function getIcon(); - - /** * @return IAction * @since 8.2.0 */ diff --git a/lib/private/notification/notification.php b/lib/private/notification/notification.php index 40fe39a956e..01df659d4a1 100644 --- a/lib/private/notification/notification.php +++ b/lib/private/notification/notification.php @@ -68,6 +68,12 @@ class Notification implements INotification { /** @var array */ protected $actionsParsed; + /** @var bool */ + protected $hasPrimaryAction; + + /** @var bool */ + protected $hasPrimaryParsedAction; + /** * Constructor */ @@ -330,28 +336,6 @@ class Notification implements INotification { } /** - * @param string $icon - * @return $this - * @throws \InvalidArgumentException if the icon are invalid - * @since 8.2.0 - */ - public function setIcon($icon) { - if (!is_string($icon) || $icon === '' || isset($icon[64])) { - throw new \InvalidArgumentException('The given icon is invalid'); - } - $this->icon = $icon; - return $this; - } - - /** - * @return string - * @since 8.2.0 - */ - public function getIcon() { - return $this->icon; - } - - /** * @return IAction * @since 8.2.0 */ @@ -369,6 +353,15 @@ class Notification implements INotification { if (!$action->isValid()) { throw new \InvalidArgumentException('The given action is invalid'); } + + if ($action->isPrimary()) { + if ($this->hasPrimaryAction) { + throw new \InvalidArgumentException('The notification already has a primary action'); + } + + $this->hasPrimaryAction = true; + } + $this->actions[] = $action; return $this; } @@ -391,6 +384,15 @@ class Notification implements INotification { if (!$action->isValidParsed()) { throw new \InvalidArgumentException('The given parsed action is invalid'); } + + if ($action->isPrimary()) { + if ($this->hasPrimaryParsedAction) { + throw new \InvalidArgumentException('The notification already has a primary action'); + } + + $this->hasPrimaryParsedAction = true; + } + $this->actionsParsed[] = $action; return $this; } diff --git a/lib/public/files/forbiddenexception.php b/lib/public/files/forbiddenexception.php new file mode 100644 index 00000000000..13490c6eae3 --- /dev/null +++ b/lib/public/files/forbiddenexception.php @@ -0,0 +1,55 @@ +<?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/> + * + */ + +// use OCP namespace for all classes that are considered public. +// This means that they should be used by apps instead of the internal ownCloud classes +namespace OCP\Files; + +/** + * Class ForbiddenException + * + * @package OCP\Files + * @since 9.0.0 + */ +class ForbiddenException extends \Exception { + + /** @var bool */ + private $retry; + + /** + * @param string $message + * @param bool $retry + * @param \Exception $previous previous exception for cascading + * @since 9.0.0 + */ + public function __construct($message, $retry, \Exception $previous = null) { + parent::__construct($message, 0, $previous); + $this->retry = $retry; + } + + /** + * @return bool + * @since 9.0.0 + */ + public function getRetry() { + return (bool) $this->retry; + } +} diff --git a/lib/public/files/node.php b/lib/public/files/node.php index aa1115f8c28..866b9b6f61f 100644 --- a/lib/public/files/node.php +++ b/lib/public/files/node.php @@ -225,4 +225,12 @@ interface Node extends FileInfo { * @since 6.0.0 */ public function getName(); + + /** + * Get the file owner + * + * @since 9.0.0 + * @return string + */ + public function getOwner(); } diff --git a/lib/public/files/storage.php b/lib/public/files/storage.php index 1c520026777..c42a2d05bc3 100644 --- a/lib/public/files/storage.php +++ b/lib/public/files/storage.php @@ -462,4 +462,10 @@ interface Storage { * @param bool $isAvailable */ public function setAvailability($isAvailable); + + /** + * @param $path path for which to retrieve the owner + * @since 9.0.0 + */ + public function getOwner($path); } diff --git a/settings/l10n/zh_TW.js b/settings/l10n/zh_TW.js index caa85e2b443..b59605da676 100644 --- a/settings/l10n/zh_TW.js +++ b/settings/l10n/zh_TW.js @@ -229,7 +229,7 @@ OC.L10N.register( "iOS app" : "iOS 應用程式", "If you want to support the project\n\t\t<a href=\"https://owncloud.org/contribute\"\n\t\t\ttarget=\"_blank\" rel=\"noreferrer\">join development</a>\n\t\tor\n\t\t<a href=\"https://owncloud.org/promote\"\n\t\t\ttarget=\"_blank\" rel=\"noreferrer\">spread the word</a>!" : "若您想支援這個計畫\n\t\t<a href=\"https://owncloud.org/contribute\"\n\t\t\ttarget=\"_blank\" rel=\"noreferrer\">加入開發者</a>\n\t\t或\n\t\t<a href=\"https://owncloud.org/promote\"\n\t\t\ttarget=\"_blank\" rel=\"noreferrer\">替我們宣傳</a>!", "Show First Run Wizard again" : "再次顯示首次使用精靈", - "You have used <strong>%s</strong> of the available <strong>%s</strong>" : "您已經使用了 <strong>%s</strong> ,目前可用空間為 <strong>%s</strong>", + "You have used <strong>%s</strong> of the available <strong>%s</strong>" : "您已經使用了 <strong>%s</strong> ,總共可用空間為 <strong>%s</strong>", "Password" : "密碼", "Unable to change your password" : "無法變更您的密碼", "Current password" : "目前密碼", diff --git a/settings/l10n/zh_TW.json b/settings/l10n/zh_TW.json index 2d7951b8930..9e1e42455df 100644 --- a/settings/l10n/zh_TW.json +++ b/settings/l10n/zh_TW.json @@ -227,7 +227,7 @@ "iOS app" : "iOS 應用程式", "If you want to support the project\n\t\t<a href=\"https://owncloud.org/contribute\"\n\t\t\ttarget=\"_blank\" rel=\"noreferrer\">join development</a>\n\t\tor\n\t\t<a href=\"https://owncloud.org/promote\"\n\t\t\ttarget=\"_blank\" rel=\"noreferrer\">spread the word</a>!" : "若您想支援這個計畫\n\t\t<a href=\"https://owncloud.org/contribute\"\n\t\t\ttarget=\"_blank\" rel=\"noreferrer\">加入開發者</a>\n\t\t或\n\t\t<a href=\"https://owncloud.org/promote\"\n\t\t\ttarget=\"_blank\" rel=\"noreferrer\">替我們宣傳</a>!", "Show First Run Wizard again" : "再次顯示首次使用精靈", - "You have used <strong>%s</strong> of the available <strong>%s</strong>" : "您已經使用了 <strong>%s</strong> ,目前可用空間為 <strong>%s</strong>", + "You have used <strong>%s</strong> of the available <strong>%s</strong>" : "您已經使用了 <strong>%s</strong> ,總共可用空間為 <strong>%s</strong>", "Password" : "密碼", "Unable to change your password" : "無法變更您的密碼", "Current password" : "目前密碼", diff --git a/settings/templates/admin.php b/settings/templates/admin.php index 27785c26dfe..24af4964248 100644 --- a/settings/templates/admin.php +++ b/settings/templates/admin.php @@ -491,15 +491,6 @@ if ($_['cronErrors']) { <div class="section" id="log-section"> <h2><?php p($l->t('Log'));?></h2> - <?php p($l->t('Log level'));?> <select name='loglevel' id='loglevel'> -<?php for ($i = 0; $i < 5; $i++): - $selected = ''; - if ($i == $_['loglevel']): - $selected = 'selected="selected"'; - endif; ?> - <option value='<?php p($i)?>' <?php p($selected) ?>><?php p($levelLabels[$i])?></option> -<?php endfor;?> -</select> <?php if ($_['showLog'] && $_['doesLogFileExist']): ?> <table id="log" class="grid"> <?php foreach ($_['entries'] as $entry): ?> @@ -537,6 +528,16 @@ if ($_['cronErrors']) { </em> <?php endif; ?> <?php endif; ?> + + <p><?php p($l->t('What to log'));?> <select name='loglevel' id='loglevel'> + <?php for ($i = 0; $i < 5; $i++): + $selected = ''; + if ($i == $_['loglevel']): + $selected = 'selected="selected"'; + endif; ?> + <option value='<?php p($i)?>' <?php p($selected) ?>><?php p($levelLabels[$i])?></option> + <?php endfor;?> + </select></p> </div> <div class="section" id="admin-tips"> diff --git a/tests/lib/helper.php b/tests/lib/helper.php index bd527de160d..ca3e0e46d82 100644 --- a/tests/lib/helper.php +++ b/tests/lib/helper.php @@ -353,18 +353,24 @@ class Test_Helper extends \Test\TestCase { $this->assertEquals($expectedResult, $result); } + /** + * @return array + */ public function provideDocRootAppUrlParts() { return array( - array('files', 'index.php', array(), '/index.php/apps/files'), - array('files', 'index.php', array('trut' => 'trat', 'dut' => 'dat'), '/index.php/apps/files?trut=trat&dut=dat'), + array('files', 'ajax/list.php', array(), '/index.php/apps/files/ajax/list.php'), + array('files', 'ajax/list.php', array('trut' => 'trat', 'dut' => 'dat'), '/index.php/apps/files/ajax/list.php?trut=trat&dut=dat'), array('', 'index.php', array('trut' => 'trat', 'dut' => 'dat'), '/index.php?trut=trat&dut=dat'), ); } + /** + * @return array + */ public function provideSubDirAppUrlParts() { return array( - array('files', 'index.php', array(), '/owncloud/index.php/apps/files'), - array('files', 'index.php', array('trut' => 'trat', 'dut' => 'dat'), '/owncloud/index.php/apps/files?trut=trat&dut=dat'), + array('files', 'ajax/list.php', array(), '/owncloud/index.php/apps/files/ajax/list.php'), + array('files', 'ajax/list.php', array('trut' => 'trat', 'dut' => 'dat'), '/owncloud/index.php/apps/files/ajax/list.php?trut=trat&dut=dat'), array('', 'index.php', array('trut' => 'trat', 'dut' => 'dat'), '/owncloud/index.php?trut=trat&dut=dat'), ); } @@ -393,18 +399,24 @@ class Test_Helper extends \Test\TestCase { $this->assertEquals($expectedResult, $result); } + /** + * @return array + */ public function provideDocRootAppAbsoluteUrlParts() { return array( - array('files', 'index.php', array(), 'http://localhost/index.php/apps/files'), - array('files', 'index.php', array('trut' => 'trat', 'dut' => 'dat'), 'http://localhost/index.php/apps/files?trut=trat&dut=dat'), + array('files', 'ajax/list.php', array(), 'http://localhost/index.php/apps/files/ajax/list.php'), + array('files', 'ajax/list.php', array('trut' => 'trat', 'dut' => 'dat'), 'http://localhost/index.php/apps/files/ajax/list.php?trut=trat&dut=dat'), array('', 'index.php', array('trut' => 'trat', 'dut' => 'dat'), 'http://localhost/index.php?trut=trat&dut=dat'), ); } + /** + * @return array + */ public function provideSubDirAppAbsoluteUrlParts() { return array( - array('files', 'index.php', array(), 'http://localhost/owncloud/index.php/apps/files'), - array('files', 'index.php', array('trut' => 'trat', 'dut' => 'dat'), 'http://localhost/owncloud/index.php/apps/files?trut=trat&dut=dat'), + array('files', 'ajax/list.php', array(), 'http://localhost/owncloud/index.php/apps/files/ajax/list.php'), + array('files', 'ajax/list.php', array('trut' => 'trat', 'dut' => 'dat'), 'http://localhost/owncloud/index.php/apps/files/ajax/list.php?trut=trat&dut=dat'), array('', 'index.php', array('trut' => 'trat', 'dut' => 'dat'), 'http://localhost/owncloud/index.php?trut=trat&dut=dat'), ); } diff --git a/tests/lib/notification/actiontest.php b/tests/lib/notification/actiontest.php index e319c250cc7..0044df82cd1 100644 --- a/tests/lib/notification/actiontest.php +++ b/tests/lib/notification/actiontest.php @@ -49,7 +49,7 @@ class ActionTest extends TestCase { */ public function testSetLabel($label) { $this->assertSame('', $this->action->getLabel()); - $this->action->setLabel($label); + $this->assertSame($this->action, $this->action->setLabel($label)); $this->assertSame($label, $this->action->getLabel()); } @@ -68,7 +68,7 @@ class ActionTest extends TestCase { /** * @dataProvider dataSetLabelInvalid - * @param string $label + * @param mixed $label * * @expectedException \InvalidArgumentException */ @@ -90,7 +90,7 @@ class ActionTest extends TestCase { */ public function testSetParsedLabel($label) { $this->assertSame('', $this->action->getParsedLabel()); - $this->action->setParsedLabel($label); + $this->assertSame($this->action, $this->action->setParsedLabel($label)); $this->assertSame($label, $this->action->getParsedLabel()); } @@ -108,7 +108,7 @@ class ActionTest extends TestCase { /** * @dataProvider dataSetParsedLabelInvalid - * @param string $label + * @param mixed $label * * @expectedException \InvalidArgumentException */ @@ -132,7 +132,7 @@ class ActionTest extends TestCase { */ public function testSetLink($link, $type) { $this->assertSame('', $this->action->getLink()); - $this->action->setLink($link, $type); + $this->assertSame($this->action, $this->action->setLink($link, $type)); $this->assertSame($link, $this->action->getLink()); $this->assertSame($type, $this->action->getRequestType()); } @@ -162,8 +162,8 @@ class ActionTest extends TestCase { /** * @dataProvider dataSetLinkInvalid - * @param string $link - * @param string $type + * @param mixed $link + * @param mixed $type * * @expectedException \InvalidArgumentException */ @@ -171,45 +171,42 @@ class ActionTest extends TestCase { $this->action->setLink($link, $type); } - public function dataSetIcon() { + public function dataSetPrimary() { return [ - ['test1'], - [str_repeat('a', 1)], - [str_repeat('a', 64)], + [true], + [false], ]; } /** - * @dataProvider dataSetIcon - * @param string $icon + * @dataProvider dataSetPrimary + * @param bool $primary */ - public function testSetIcon($icon) { - $this->assertSame('', $this->action->getIcon()); - $this->action->setIcon($icon); - $this->assertSame($icon, $this->action->getIcon()); + public function testSetPrimary($primary) { + $this->assertSame(false, $this->action->isPrimary()); + $this->assertSame($this->action, $this->action->setPrimary($primary)); + $this->assertSame($primary, $this->action->isPrimary()); } - public function dataSetIconInvalid() { + public function dataSetPrimaryInvalid() { return [ - [true], - [false], [0], [1], [''], - [str_repeat('a', 65)], + [str_repeat('a', 257)], [[]], - [[str_repeat('a', 65)]], + [[str_repeat('a', 257)]], ]; } /** - * @dataProvider dataSetIconInvalid - * @param string $icon + * @dataProvider dataSetPrimaryInvalid + * @param mixed $primary * * @expectedException \InvalidArgumentException */ - public function testSetIconInvalid($icon) { - $this->action->setIcon($icon); + public function testSetPrimaryInvalid($primary) { + $this->action->setPrimary($primary); } public function testIsValid() { diff --git a/tests/lib/notification/notificationtest.php b/tests/lib/notification/notificationtest.php index a790a53eaa7..662dc5a6176 100644 --- a/tests/lib/notification/notificationtest.php +++ b/tests/lib/notification/notificationtest.php @@ -93,7 +93,7 @@ class NotificationTest extends TestCase { */ public function testSetApp($app) { $this->assertSame('', $this->notification->getApp()); - $this->notification->setApp($app); + $this->assertSame($this->notification, $this->notification->setApp($app)); $this->assertSame($app, $this->notification->getApp()); } @@ -121,7 +121,7 @@ class NotificationTest extends TestCase { */ public function testSetUser($user) { $this->assertSame('', $this->notification->getUser()); - $this->notification->setUser($user); + $this->assertSame($this->notification, $this->notification->setUser($user)); $this->assertSame($user, $this->notification->getUser()); } @@ -149,7 +149,7 @@ class NotificationTest extends TestCase { */ public function testSetTimestamp($timestamp) { $this->assertSame(0, $this->notification->getTimestamp()); - $this->notification->setTimestamp($timestamp); + $this->assertSame($this->notification, $this->notification->setTimestamp($timestamp)); $this->assertSame($timestamp, $this->notification->getTimestamp()); } @@ -182,7 +182,7 @@ class NotificationTest extends TestCase { public function testSetObject($type, $id) { $this->assertSame('', $this->notification->getObjectType()); $this->assertSame(0, $this->notification->getObjectId()); - $this->notification->setObject($type, $id); + $this->assertSame($this->notification, $this->notification->setObject($type, $id)); $this->assertSame($type, $this->notification->getObjectType()); $this->assertSame($id, $this->notification->getObjectId()); } @@ -233,7 +233,7 @@ class NotificationTest extends TestCase { public function testSetSubject($subject, $parameters) { $this->assertSame('', $this->notification->getSubject()); $this->assertSame([], $this->notification->getSubjectParameters()); - $this->notification->setSubject($subject, $parameters); + $this->assertSame($this->notification, $this->notification->setSubject($subject, $parameters)); $this->assertSame($subject, $this->notification->getSubject()); $this->assertSame($parameters, $this->notification->getSubjectParameters()); } @@ -262,7 +262,7 @@ class NotificationTest extends TestCase { */ public function testSetParsedSubject($subject) { $this->assertSame('', $this->notification->getParsedSubject()); - $this->notification->setParsedSubject($subject); + $this->assertSame($this->notification, $this->notification->setParsedSubject($subject)); $this->assertSame($subject, $this->notification->getParsedSubject()); } @@ -296,7 +296,7 @@ class NotificationTest extends TestCase { public function testSetMessage($message, $parameters) { $this->assertSame('', $this->notification->getMessage()); $this->assertSame([], $this->notification->getMessageParameters()); - $this->notification->setMessage($message, $parameters); + $this->assertSame($this->notification, $this->notification->setMessage($message, $parameters)); $this->assertSame($message, $this->notification->getMessage()); $this->assertSame($parameters, $this->notification->getMessageParameters()); } @@ -325,7 +325,7 @@ class NotificationTest extends TestCase { */ public function testSetParsedMessage($message) { $this->assertSame('', $this->notification->getParsedMessage()); - $this->notification->setParsedMessage($message); + $this->assertSame($this->notification, $this->notification->setParsedMessage($message)); $this->assertSame($message, $this->notification->getParsedMessage()); } @@ -353,7 +353,7 @@ class NotificationTest extends TestCase { */ public function testSetLink($link) { $this->assertSame('', $this->notification->getLink()); - $this->notification->setLink($link); + $this->assertSame($this->notification, $this->notification->setLink($link)); $this->assertSame($link, $this->notification->getLink()); } @@ -371,34 +371,6 @@ class NotificationTest extends TestCase { $this->notification->setLink($link); } - public function dataSetIcon() { - return $this->dataValidString(64); - } - - /** - * @dataProvider dataSetIcon - * @param string $icon - */ - public function testSetIcon($icon) { - $this->assertSame('', $this->notification->getIcon()); - $this->notification->setIcon($icon); - $this->assertSame($icon, $this->notification->getIcon()); - } - - public function dataSetIconInvalid() { - return $this->dataInvalidString(64); - } - - /** - * @dataProvider dataSetIconInvalid - * @param mixed $icon - * - * @expectedException \InvalidArgumentException - */ - public function testSetIconInvalid($icon) { - $this->notification->setIcon($icon); - } - public function testCreateAction() { $action = $this->notification->createAction(); $this->assertInstanceOf('OC\Notification\IAction', $action); @@ -415,7 +387,7 @@ class NotificationTest extends TestCase { $action->expects($this->never()) ->method('isValidParsed'); - $this->notification->addAction($action); + $this->assertSame($this->notification, $this->notification->addAction($action)); $this->assertEquals([$action], $this->notification->getActions()); $this->assertEquals([], $this->notification->getParsedActions()); @@ -438,6 +410,24 @@ class NotificationTest extends TestCase { $this->notification->addAction($action); } + public function testAddActionSecondPrimary() { + /** @var \OC\Notification\IAction|\PHPUnit_Framework_MockObject_MockObject $action */ + $action = $this->getMockBuilder('OC\Notification\IAction') + ->disableOriginalConstructor() + ->getMock(); + $action->expects($this->exactly(2)) + ->method('isValid') + ->willReturn(true); + $action->expects($this->exactly(2)) + ->method('isPrimary') + ->willReturn(true); + + $this->assertSame($this->notification, $this->notification->addAction($action)); + + $this->setExpectedException('\InvalidArgumentException'); + $this->notification->addAction($action); + } + public function testAddParsedAction() { /** @var \OC\Notification\IAction|\PHPUnit_Framework_MockObject_MockObject $action */ $action = $this->getMockBuilder('OC\Notification\IAction') @@ -449,7 +439,7 @@ class NotificationTest extends TestCase { $action->expects($this->never()) ->method('isValid'); - $this->notification->addParsedAction($action); + $this->assertSame($this->notification, $this->notification->addParsedAction($action)); $this->assertEquals([$action], $this->notification->getParsedActions()); $this->assertEquals([], $this->notification->getActions()); @@ -472,6 +462,24 @@ class NotificationTest extends TestCase { $this->notification->addParsedAction($action); } + public function testAddActionSecondParsedPrimary() { + /** @var \OC\Notification\IAction|\PHPUnit_Framework_MockObject_MockObject $action */ + $action = $this->getMockBuilder('OC\Notification\IAction') + ->disableOriginalConstructor() + ->getMock(); + $action->expects($this->exactly(2)) + ->method('isValidParsed') + ->willReturn(true); + $action->expects($this->exactly(2)) + ->method('isPrimary') + ->willReturn(true); + + $this->assertSame($this->notification, $this->notification->addParsedAction($action)); + + $this->setExpectedException('\InvalidArgumentException'); + $this->notification->addParsedAction($action); + } + public function dataIsValid() { return [ [false, '', false], diff --git a/tests/lib/urlgenerator.php b/tests/lib/urlgenerator.php index 60e1a86f16a..a5ab483109f 100644 --- a/tests/lib/urlgenerator.php +++ b/tests/lib/urlgenerator.php @@ -53,23 +53,23 @@ class Test_Urlgenerator extends \Test\TestCase { public function provideRoutes() { return array( - array('files_index', 'http://localhost/owncloud/index.php/apps/files/'), + array('files_ajax_list', 'http://localhost/owncloud/index.php/apps/files/ajax/list.php'), array('core_ajax_preview', 'http://localhost/owncloud/index.php/core/preview.png'), ); } public function provideDocRootAppUrlParts() { return array( - array('files', 'index.php', array(), '/index.php/apps/files'), - array('files', 'index.php', array('trut' => 'trat', 'dut' => 'dat'), '/index.php/apps/files?trut=trat&dut=dat'), + array('files', 'ajax/list.php', array(), '/index.php/apps/files/ajax/list.php'), + array('files', 'ajax/list.php', array('trut' => 'trat', 'dut' => 'dat'), '/index.php/apps/files/ajax/list.php?trut=trat&dut=dat'), array('', 'index.php', array('trut' => 'trat', 'dut' => 'dat'), '/index.php?trut=trat&dut=dat'), ); } public function provideSubDirAppUrlParts() { return array( - array('files', 'index.php', array(), '/owncloud/index.php/apps/files'), - array('files', 'index.php', array('trut' => 'trat', 'dut' => 'dat'), '/owncloud/index.php/apps/files?trut=trat&dut=dat'), + array('files', 'ajax/list.php', array(), '/owncloud/index.php/apps/files/ajax/list.php'), + array('files', 'ajax/list.php', array('trut' => 'trat', 'dut' => 'dat'), '/owncloud/index.php/apps/files/ajax/list.php?trut=trat&dut=dat'), array('', 'index.php', array('trut' => 'trat', 'dut' => 'dat'), '/owncloud/index.php?trut=trat&dut=dat'), ); } |