diff options
29 files changed, 2123 insertions, 55 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/filesplugin.php b/apps/dav/lib/connector/sabre/filesplugin.php index 00d5d2cd725..d68397dcaa3 100644 --- a/apps/dav/lib/connector/sabre/filesplugin.php +++ b/apps/dav/lib/connector/sabre/filesplugin.php @@ -37,6 +37,7 @@ 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'; @@ -98,6 +99,7 @@ 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; @@ -175,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) { 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 ae7dd51fc94..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() { 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/filesplugin.php b/apps/dav/tests/unit/connector/sabre/filesplugin.php index 55c8dd49e17..2e3338fefa1 100644 --- a/apps/dav/tests/unit/connector/sabre/filesplugin.php +++ b/apps/dav/tests/unit/connector/sabre/filesplugin.php @@ -11,6 +11,7 @@ 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; @@ -69,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"')); @@ -90,6 +94,7 @@ 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, @@ -125,7 +130,8 @@ 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)); @@ -186,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)); 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_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/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/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/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/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/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"> |