+ *dbprefix*dav_shares
+
+
+ id
+ integer
+ 0
+ true
+ 1
+ true
+ 11
+
+
+ uri
+ text
+
+
+ principaluri
+ text
+
+
+ type
+ text
+
+
+ access
+ integer
+ 1
+
+
+ resourceid
+ integer
+ true
+ true
+
+
+ dav_shares_index
+ true
+
+ principaluri
+
+
+ uri
+
+
+ type
+
+
+
+
diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml
index 1102511569..5f681e784f 100644
--- a/apps/dav/appinfo/info.xml
+++ b/apps/dav/appinfo/info.xml
@@ -5,7 +5,7 @@
ownCloud WebDAV endpointAGPLowncloud.org
- 0.1.2
+ 0.1.39.0true
diff --git a/apps/dav/appinfo/register_command.php b/apps/dav/appinfo/register_command.php
index c996dd4406..af41036cdd 100644
--- a/apps/dav/appinfo/register_command.php
+++ b/apps/dav/appinfo/register_command.php
@@ -1,8 +1,15 @@
getConfig();
$dbConnection = \OC::$server->getDatabaseConnection();
$userManager = OC::$server->getUserManager();
+$config = \OC::$server->getConfig();
+
/** @var Symfony\Component\Console\Application $application */
-$application->add(new CreateAddressBook($userManager, $dbConnection));
+$application->add(new CreateAddressBook($userManager, $dbConnection, $config));
+$application->add(new CreateCalendar($userManager, $dbConnection));
+$application->add(new SyncSystemAddressBook($userManager, $dbConnection, $config));
diff --git a/apps/dav/appinfo/v1/publicwebdav.php b/apps/dav/appinfo/v1/publicwebdav.php
index 5bdfd94e65..cf0488038d 100644
--- a/apps/dav/appinfo/v1/publicwebdav.php
+++ b/apps/dav/appinfo/v1/publicwebdav.php
@@ -39,7 +39,8 @@ $serverFactory = new OCA\DAV\Connector\Sabre\ServerFactory(
\OC::$server->getUserSession(),
\OC::$server->getMountManager(),
\OC::$server->getTagManager(),
- \OC::$server->getEventDispatcher()
+ \OC::$server->getEventDispatcher(),
+ \OC::$server->getRequest()
);
$requestUri = \OC::$server->getRequest()->getRequestUri();
diff --git a/apps/dav/appinfo/v1/webdav.php b/apps/dav/appinfo/v1/webdav.php
index f28736f1f0..8324f962b8 100644
--- a/apps/dav/appinfo/v1/webdav.php
+++ b/apps/dav/appinfo/v1/webdav.php
@@ -40,7 +40,8 @@ $serverFactory = new \OCA\DAV\Connector\Sabre\ServerFactory(
\OC::$server->getUserSession(),
\OC::$server->getMountManager(),
\OC::$server->getTagManager(),
- \OC::$server->getEventDispatcher()
+ \OC::$server->getEventDispatcher(),
+ \OC::$server->getRequest()
);
// Backends
diff --git a/apps/dav/command/createaddressbook.php b/apps/dav/command/createaddressbook.php
index 286871b39e..ea89e7aa0a 100644
--- a/apps/dav/command/createaddressbook.php
+++ b/apps/dav/command/createaddressbook.php
@@ -3,6 +3,8 @@
namespace OCA\DAV\Command;
use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\Connector\Sabre\Principal;
+use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IUserManager;
use Symfony\Component\Console\Command\Command;
@@ -18,26 +20,30 @@ class CreateAddressBook extends Command {
/** @var \OCP\IDBConnection */
protected $dbConnection;
+ /** @var IConfig */
+ private $config;
+
/**
* @param IUserManager $userManager
* @param IDBConnection $dbConnection
*/
- function __construct(IUserManager $userManager, IDBConnection $dbConnection) {
+ function __construct(IUserManager $userManager, IDBConnection $dbConnection, IConfig $config) {
parent::__construct();
$this->userManager = $userManager;
$this->dbConnection = $dbConnection;
+ $this->config = $config;
}
protected function configure() {
$this
- ->setName('dav:create-addressbook')
- ->setDescription('Create a dav addressbook')
- ->addArgument('user',
- InputArgument::REQUIRED,
- 'User for whom the addressbook will be created')
- ->addArgument('name',
- InputArgument::REQUIRED,
- 'Name of the addressbook');
+ ->setName('dav:create-addressbook')
+ ->setDescription('Create a dav addressbook')
+ ->addArgument('user',
+ InputArgument::REQUIRED,
+ 'User for whom the addressbook will be created')
+ ->addArgument('name',
+ InputArgument::REQUIRED,
+ 'Name of the addressbook');
}
protected function execute(InputInterface $input, OutputInterface $output) {
@@ -45,8 +51,13 @@ class CreateAddressBook extends Command {
if (!$this->userManager->userExists($user)) {
throw new \InvalidArgumentException("User <$user> in unknown.");
}
+ $principalBackend = new Principal(
+ $this->config,
+ $this->userManager
+ );
+
$name = $input->getArgument('name');
- $carddav = new CardDavBackend($this->dbConnection);
- $carddav->createAddressBook("principals/$user", $name, []);
+ $carddav = new CardDavBackend($this->dbConnection, $principalBackend);
+ $carddav->createAddressBook("principals/users/$user", $name, []);
}
}
diff --git a/apps/dav/command/createcalendar.php b/apps/dav/command/createcalendar.php
new file mode 100644
index 0000000000..5e7b17dae6
--- /dev/null
+++ b/apps/dav/command/createcalendar.php
@@ -0,0 +1,52 @@
+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/users/$user", $name, []);
+ }
+}
diff --git a/apps/dav/command/syncsystemaddressbook.php b/apps/dav/command/syncsystemaddressbook.php
new file mode 100644
index 0000000000..bb2896abc6
--- /dev/null
+++ b/apps/dav/command/syncsystemaddressbook.php
@@ -0,0 +1,107 @@
+userManager = $userManager;
+ $this->dbConnection = $dbConnection;
+ $this->config = $config;
+ }
+
+ protected function configure() {
+ $this
+ ->setName('dav:sync-system-addressbook')
+ ->setDescription('Synchronizes users to the system addressbook');
+ }
+
+ /**
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ */
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $principalBackend = new Principal(
+ $this->config,
+ $this->userManager
+ );
+
+ $this->backend = new CardDavBackend($this->dbConnection, $principalBackend);
+
+ // ensure system addressbook exists
+ $systemAddressBook = $this->ensureSystemAddressBookExists();
+ $converter = new Converter();
+
+ $output->writeln('Syncing users ...');
+ $progress = new ProgressBar($output);
+ $progress->start();
+ $this->userManager->callForAllUsers(function($user) use ($systemAddressBook, $converter, $progress) {
+ /** @var IUser $user */
+ $name = $user->getBackendClassName();
+ $userId = $user->getUID();
+
+ $cardId = "$name:$userId.vcf";
+ $card = $this->backend->getCard($systemAddressBook['id'], $cardId);
+ if ($card === false) {
+ $vCard = $converter->createCardFromUser($user);
+ $this->backend->createCard($systemAddressBook['id'], $cardId, $vCard->serialize());
+ } else {
+ $vCard = Reader::read($card['carddata']);
+ if ($converter->updateCard($vCard, $user)) {
+ $this->backend->updateCard($systemAddressBook['id'], $cardId, $vCard->serialize());
+ }
+ }
+ $progress->advance();
+ });
+ $progress->finish();
+ $output->writeln('');
+ }
+
+ protected function ensureSystemAddressBookExists() {
+ $book = $this->backend->getAddressBooksByUri('system');
+ if (!is_null($book)) {
+ return $book;
+ }
+ $systemPrincipal = "principals/system/system";
+ $this->backend->createAddressBook($systemPrincipal, 'system', [
+ '{' . Plugin::NS_CARDDAV . '}addressbook-description' => 'System addressbook which holds all users of this instance'
+ ]);
+
+ return $this->backend->getAddressBooksByUri('system');
+ }
+}
diff --git a/apps/dav/lib/caldav/caldavbackend.php b/apps/dav/lib/caldav/caldavbackend.php
new file mode 100644
index 0000000000..d912f209d4
--- /dev/null
+++ b/apps/dav/lib/caldav/caldavbackend.php
@@ -0,0 +1,1174 @@
+
+ *
+ * @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
+ *
+ */
+
+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\Xml\Property\ScheduleCalendarTransp;
+use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
+use Sabre\DAV;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\VObject\DateTimeParser;
+use Sabre\VObject\Reader;
+use Sabre\VObject\RecurrenceIterator;
+
+/**
+ * Class CalDavBackend
+ *
+ * Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php
+ *
+ * @package OCA\DAV\CalDAV
+ */
+class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
+
+ /**
+ * We need to specify a max date, because we need to stop *somewhere*
+ *
+ * On 32 bit system the maximum for a signed integer is 2147483647, so
+ * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
+ * in 2038-01-19 to avoid problems when the date is converted
+ * to a unix timestamp.
+ */
+ const MAX_DATE = '2038-01-01';
+
+ /**
+ * List of CalDAV properties, and how they map to database fieldnames
+ * Add your own properties by simply adding on to this array.
+ *
+ * Note that only string-based properties are supported here.
+ *
+ * @var array
+ */
+ public $propertyMap = [
+ '{DAV:}displayname' => 'displayname',
+ '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone',
+ '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder',
+ '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor',
+ ];
+
+ /**
+ * List of subscription properties, and how they map to database fieldnames.
+ *
+ * @var array
+ */
+ public $subscriptionPropertyMap = [
+ '{DAV:}displayname' => 'displayname',
+ '{http://apple.com/ns/ical/}refreshrate' => 'refreshrate',
+ '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder',
+ '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor',
+ '{http://calendarserver.org/ns/}subscribed-strip-todos' => 'striptodos',
+ '{http://calendarserver.org/ns/}subscribed-strip-alarms' => 'stripalarms',
+ '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments',
+ ];
+
+ public function __construct(\OCP\IDBConnection $db) {
+ $this->db = $db;
+ }
+
+ /**
+ * Returns a list of calendars for a principal.
+ *
+ * Every project is an array with the following keys:
+ * * id, a unique id that will be used by other functions to modify the
+ * calendar. This can be the same as the uri or a database key.
+ * * uri, which the basename of the uri with which the calendar is
+ * accessed.
+ * * principaluri. The owner of the calendar. Almost always the same as
+ * principalUri passed to this method.
+ *
+ * Furthermore it can contain webdav properties in clark notation. A very
+ * common one is '{DAV:}displayname'.
+ *
+ * Many clients also require:
+ * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
+ * For this property, you can just return an instance of
+ * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
+ *
+ * If you return {http://sabredav.org/ns}read-only and set the value to 1,
+ * ACL will automatically be put in read-only mode.
+ *
+ * @param string $principalUri
+ * @return array
+ */
+ function getCalendarsForUser($principalUri) {
+ $fields = array_values($this->propertyMap);
+ $fields[] = 'id';
+ $fields[] = 'uri';
+ $fields[] = 'synctoken';
+ $fields[] = 'components';
+ $fields[] = 'principaluri';
+ $fields[] = 'transparent';
+
+ // Making fields a comma-delimited list
+ $query = $this->db->getQueryBuilder();
+ $query->select($fields)->from('calendars')
+ ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
+ ->orderBy('calendarorder', 'ASC');
+ $stmt = $query->execute();
+
+ $calendars = [];
+ while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ $components = [];
+ if ($row['components']) {
+ $components = explode(',',$row['components']);
+ }
+
+ $calendar = [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'principaluri' => $row['principaluri'],
+ '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
+ '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
+ ];
+
+ foreach($this->propertyMap as $xmlName=>$dbName) {
+ $calendar[$xmlName] = $row[$dbName];
+ }
+
+ $calendars[] = $calendar;
+ }
+
+ return $calendars;
+ }
+
+ /**
+ * Creates a new calendar for a principal.
+ *
+ * If the creation was a success, an id must be returned that can be used to reference
+ * this calendar in other methods, such as updateCalendar.
+ *
+ * @param string $principalUri
+ * @param string $calendarUri
+ * @param array $properties
+ * @return void
+ */
+ function createCalendar($principalUri, $calendarUri, array $properties) {
+ $values = [
+ 'principaluri' => $principalUri,
+ 'uri' => $calendarUri,
+ 'synctoken' => 1,
+ 'transparent' => 0,
+ 'components' => 'VEVENT,VTODO',
+ 'displayname' => $calendarUri
+ ];
+
+ // Default value
+ $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
+ if (isset($properties[$sccs])) {
+ if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
+ throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
+ }
+ $values['components'] = implode(',',$properties[$sccs]->getValue());
+ }
+ $transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
+ if (isset($properties[$transp])) {
+ $values['transparent'] = $properties[$transp]->getValue()==='transparent';
+ }
+
+ foreach($this->propertyMap as $xmlName=>$dbName) {
+ if (isset($properties[$xmlName])) {
+ $values[$dbName] = $properties[$xmlName];
+ }
+ }
+
+ $query = $this->db->getQueryBuilder();
+ $query->insert('calendars');
+ foreach($values as $column => $value) {
+ $query->setValue($column, $query->createNamedParameter($value));
+ }
+ $query->execute();
+ }
+
+ /**
+ * Updates properties for a calendar.
+ *
+ * The list of mutations is stored in a Sabre\DAV\PropPatch object.
+ * To do the actual updates, you must tell this object which properties
+ * you're going to process with the handle() method.
+ *
+ * Calling the handle method is like telling the PropPatch object "I
+ * promise I can handle updating this property".
+ *
+ * Read the PropPatch documentation for more info and examples.
+ *
+ * @param \Sabre\DAV\PropPatch $propPatch
+ * @return void
+ */
+ function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch) {
+ $supportedProperties = array_keys($this->propertyMap);
+ $supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
+
+ $propPatch->handle($supportedProperties, function($mutations) use ($calendarId) {
+ $newValues = [];
+ foreach ($mutations as $propertyName => $propertyValue) {
+
+ switch ($propertyName) {
+ case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' :
+ $fieldName = 'transparent';
+ $newValues[$fieldName] = $propertyValue->getValue() === 'transparent';
+ break;
+ default :
+ $fieldName = $this->propertyMap[$propertyName];
+ $newValues[$fieldName] = $propertyValue;
+ break;
+ }
+
+ }
+ $query = $this->db->getQueryBuilder();
+ $query->update('calendars');
+ foreach ($newValues as $fieldName => $value) {
+ $query->set($fieldName, $query->createNamedParameter($value));
+ }
+ $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
+ $query->execute();
+
+ $this->addChange($calendarId, "", 2);
+
+ return true;
+ });
+ }
+
+ /**
+ * Delete a calendar and all it's objects
+ *
+ * @param mixed $calendarId
+ * @return void
+ */
+ function deleteCalendar($calendarId) {
+ $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?');
+ $stmt->execute([$calendarId]);
+
+ $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?');
+ $stmt->execute([$calendarId]);
+
+ $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ?');
+ $stmt->execute([$calendarId]);
+ }
+
+ /**
+ * Returns all calendar objects within a calendar.
+ *
+ * Every item contains an array with the following keys:
+ * * calendardata - The iCalendar-compatible calendar data
+ * * uri - a unique key which will be used to construct the uri. This can
+ * be any arbitrary string, but making sure it ends with '.ics' is a
+ * good idea. This is only the basename, or filename, not the full
+ * path.
+ * * lastmodified - a timestamp of the last modification time
+ * * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
+ * '"abcdef"')
+ * * size - The size of the calendar objects, in bytes.
+ * * component - optional, a string containing the type of object, such
+ * as 'vevent' or 'vtodo'. If specified, this will be used to populate
+ * the Content-Type header.
+ *
+ * Note that the etag is optional, but it's highly encouraged to return for
+ * speed reasons.
+ *
+ * The calendardata is also optional. If it's not returned
+ * 'getCalendarObject' will be called later, which *is* expected to return
+ * calendardata.
+ *
+ * If neither etag or size are specified, the calendardata will be
+ * used/fetched to determine these numbers. If both are specified the
+ * amount of times this is needed is reduced by a great degree.
+ *
+ * @param mixed $calendarId
+ * @return array
+ */
+ function getCalendarObjects($calendarId) {
+ $query = $this->db->getQueryBuilder();
+ $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype'])
+ ->from('calendarobjects')
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
+ $stmt = $query->execute();
+
+ $result = [];
+ foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
+ $result[] = [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'lastmodified' => $row['lastmodified'],
+ 'etag' => '"' . $row['etag'] . '"',
+ 'calendarid' => $row['calendarid'],
+ 'size' => (int)$row['size'],
+ 'component' => strtolower($row['componenttype']),
+ ];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns information from a single calendar object, based on it's object
+ * uri.
+ *
+ * The object uri is only the basename, or filename and not a full path.
+ *
+ * The returned array must have the same keys as getCalendarObjects. The
+ * 'calendardata' object is required here though, while it's not required
+ * for getCalendarObjects.
+ *
+ * This method must return null if the object did not exist.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @return array|null
+ */
+ function getCalendarObject($calendarId, $objectUri) {
+
+ $query = $this->db->getQueryBuilder();
+ $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype'])
+ ->from('calendarobjects')
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
+ ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)));
+ $stmt = $query->execute();
+ $row = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+ if(!$row) return null;
+
+ return [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'lastmodified' => $row['lastmodified'],
+ 'etag' => '"' . $row['etag'] . '"',
+ 'calendarid' => $row['calendarid'],
+ 'size' => (int)$row['size'],
+ 'calendardata' => $this->readBlob($row['calendardata']),
+ 'component' => strtolower($row['componenttype']),
+ ];
+ }
+
+ /**
+ * Returns a list of calendar objects.
+ *
+ * This method should work identical to getCalendarObject, but instead
+ * return all the calendar objects in the list as an array.
+ *
+ * If the backend supports this, it may allow for some speed-ups.
+ *
+ * @param mixed $calendarId
+ * @param string[] $uris
+ * @return array
+ */
+ function getMultipleCalendarObjects($calendarId, array $uris) {
+ $query = $this->db->getQueryBuilder();
+ $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype'])
+ ->from('calendarobjects')
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
+ ->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
+ ->setParameter('uri', $uris, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY);
+
+ $stmt = $query->execute();
+
+ $result = [];
+ while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ $result[] = [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'lastmodified' => $row['lastmodified'],
+ 'etag' => '"' . $row['etag'] . '"',
+ 'calendarid' => $row['calendarid'],
+ 'size' => (int)$row['size'],
+ 'calendardata' => $this->readBlob($row['calendardata']),
+ 'component' => strtolower($row['componenttype']),
+ ];
+
+ }
+ return $result;
+ }
+
+ /**
+ * Creates a new calendar object.
+ *
+ * The object uri is only the basename, or filename and not a full path.
+ *
+ * It is possible return an etag from this function, which will be used in
+ * the response to this PUT request. Note that the ETag must be surrounded
+ * by double-quotes.
+ *
+ * However, you should only really return this ETag if you don't mangle the
+ * calendar-data. If the result of a subsequent GET to this object is not
+ * the exact same as this request body, you should omit the ETag.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @param string $calendarData
+ * @return string
+ */
+ function createCalendarObject($calendarId, $objectUri, $calendarData) {
+ $extraData = $this->getDenormalizedData($calendarData);
+
+ $query = $this->db->getQueryBuilder();
+ $query->insert('calendarobjects')
+ ->values([
+ 'calendarid' => $query->createNamedParameter($calendarId),
+ 'uri' => $query->createNamedParameter($objectUri),
+ 'calendardata' => $query->createNamedParameter($calendarData, \PDO::PARAM_LOB),
+ 'lastmodified' => $query->createNamedParameter(time()),
+ 'etag' => $query->createNamedParameter($extraData['etag']),
+ 'size' => $query->createNamedParameter($extraData['size']),
+ 'componenttype' => $query->createNamedParameter($extraData['componentType']),
+ 'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
+ 'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
+ 'uid' => $query->createNamedParameter($extraData['uid']),
+ ])
+ ->execute();
+
+ $this->addChange($calendarId, $objectUri, 1);
+
+ return '"' . $extraData['etag'] . '"';
+ }
+
+ /**
+ * Updates an existing calendarobject, based on it's uri.
+ *
+ * The object uri is only the basename, or filename and not a full path.
+ *
+ * It is possible return an etag from this function, which will be used in
+ * the response to this PUT request. Note that the ETag must be surrounded
+ * by double-quotes.
+ *
+ * However, you should only really return this ETag if you don't mangle the
+ * calendar-data. If the result of a subsequent GET to this object is not
+ * the exact same as this request body, you should omit the ETag.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @param string $calendarData
+ * @return string
+ */
+ function updateCalendarObject($calendarId, $objectUri, $calendarData) {
+ $extraData = $this->getDenormalizedData($calendarData);
+
+ $query = $this->db->getQueryBuilder();
+ $query->update('calendarobjects')
+ ->set('calendardata', $query->createNamedParameter($calendarData, \PDO::PARAM_LOB))
+ ->set('lastmodified', $query->createNamedParameter(time()))
+ ->set('etag', $query->createNamedParameter($extraData['etag']))
+ ->set('size', $query->createNamedParameter($extraData['size']))
+ ->set('componenttype', $query->createNamedParameter($extraData['componentType']))
+ ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
+ ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
+ ->set('uid', $query->createNamedParameter($extraData['uid']))
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
+ ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
+ ->execute();
+
+ $this->addChange($calendarId, $objectUri, 2);
+
+ return '"' . $extraData['etag'] . '"';
+ }
+
+ /**
+ * Deletes an existing calendar object.
+ *
+ * The object uri is only the basename, or filename and not a full path.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @return void
+ */
+ function deleteCalendarObject($calendarId, $objectUri) {
+ $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ?');
+ $stmt->execute([$calendarId, $objectUri]);
+
+ $this->addChange($calendarId, $objectUri, 3);
+ }
+
+ /**
+ * Performs a calendar-query on the contents of this calendar.
+ *
+ * The calendar-query is defined in RFC4791 : CalDAV. Using the
+ * calendar-query it is possible for a client to request a specific set of
+ * object, based on contents of iCalendar properties, date-ranges and
+ * iCalendar component types (VTODO, VEVENT).
+ *
+ * This method should just return a list of (relative) urls that match this
+ * query.
+ *
+ * The list of filters are specified as an array. The exact array is
+ * documented by Sabre\CalDAV\CalendarQueryParser.
+ *
+ * Note that it is extremely likely that getCalendarObject for every path
+ * returned from this method will be called almost immediately after. You
+ * may want to anticipate this to speed up these requests.
+ *
+ * This method provides a default implementation, which parses *all* the
+ * iCalendar objects in the specified calendar.
+ *
+ * This default may well be good enough for personal use, and calendars
+ * that aren't very large. But if you anticipate high usage, big calendars
+ * or high loads, you are strongly adviced to optimize certain paths.
+ *
+ * The best way to do so is override this method and to optimize
+ * specifically for 'common filters'.
+ *
+ * Requests that are extremely common are:
+ * * requests for just VEVENTS
+ * * requests for just VTODO
+ * * requests with a time-range-filter on either VEVENT or VTODO.
+ *
+ * ..and combinations of these requests. It may not be worth it to try to
+ * handle every possible situation and just rely on the (relatively
+ * easy to use) CalendarQueryValidator to handle the rest.
+ *
+ * Note that especially time-range-filters may be difficult to parse. A
+ * time-range filter specified on a VEVENT must for instance also handle
+ * recurrence rules correctly.
+ * A good example of how to interprete all these filters can also simply
+ * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
+ * as possible, so it gives you a good idea on what type of stuff you need
+ * to think of.
+ *
+ * @param mixed $calendarId
+ * @param array $filters
+ * @return array
+ */
+ function calendarQuery($calendarId, array $filters) {
+ $componentType = null;
+ $requirePostFilter = true;
+ $timeRange = null;
+
+ // if no filters were specified, we don't need to filter after a query
+ if (!$filters['prop-filters'] && !$filters['comp-filters']) {
+ $requirePostFilter = false;
+ }
+
+ // Figuring out if there's a component filter
+ if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
+ $componentType = $filters['comp-filters'][0]['name'];
+
+ // Checking if we need post-filters
+ if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
+ $requirePostFilter = false;
+ }
+ // There was a time-range filter
+ if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) {
+ $timeRange = $filters['comp-filters'][0]['time-range'];
+
+ // If start time OR the end time is not specified, we can do a
+ // 100% accurate mysql query.
+ if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
+ $requirePostFilter = false;
+ }
+ }
+
+ }
+ $columns = ['uri'];
+ if ($requirePostFilter) {
+ $columns = ['uri', 'calendardata'];
+ }
+ $query = $this->db->getQueryBuilder();
+ $query->select($columns)
+ ->from('calendarobjects')
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
+
+ if ($componentType) {
+ $query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
+ }
+
+ if ($timeRange && $timeRange['start']) {
+ $query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
+ }
+ if ($timeRange && $timeRange['end']) {
+ $query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
+ }
+
+ $stmt = $query->execute();
+
+ $result = [];
+ while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if ($requirePostFilter) {
+ if (!$this->validateFilterForObject($row, $filters)) {
+ continue;
+ }
+ }
+ $result[] = $row['uri'];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Searches through all of a users calendars and calendar objects to find
+ * an object with a specific UID.
+ *
+ * This method should return the path to this object, relative to the
+ * calendar home, so this path usually only contains two parts:
+ *
+ * calendarpath/objectpath.ics
+ *
+ * If the uid is not found, return null.
+ *
+ * This method should only consider * objects that the principal owns, so
+ * any calendars owned by other principals that also appear in this
+ * collection should be ignored.
+ *
+ * @param string $principalUri
+ * @param string $uid
+ * @return string|null
+ */
+ function getCalendarObjectByUID($principalUri, $uid) {
+
+ $query = $this->db->getQueryBuilder();
+ $query->select([$query->createFunction('c.`uri` AS `calendaruri`'), $query->createFunction('co.`uri` AS `objecturi`')])
+ ->from('calendarobjects', 'co')
+ ->leftJoin('co', 'calendars', 'c', 'co.`calendarid` = c.`id`')
+ ->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
+ ->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)));
+
+ $stmt = $query->execute();
+
+ if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ return $row['calendaruri'] . '/' . $row['objecturi'];
+ }
+
+ return null;
+ }
+
+ /**
+ * The getChanges method returns all the changes that have happened, since
+ * the specified syncToken in the specified calendar.
+ *
+ * This function should return an array, such as the following:
+ *
+ * [
+ * 'syncToken' => 'The current synctoken',
+ * 'added' => [
+ * 'new.txt',
+ * ],
+ * 'modified' => [
+ * 'modified.txt',
+ * ],
+ * 'deleted' => [
+ * 'foo.php.bak',
+ * 'old.txt'
+ * ]
+ * );
+ *
+ * The returned syncToken property should reflect the *current* syncToken
+ * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
+ * property This is * needed here too, to ensure the operation is atomic.
+ *
+ * If the $syncToken argument is specified as null, this is an initial
+ * sync, and all members should be reported.
+ *
+ * The modified property is an array of nodenames that have changed since
+ * the last token.
+ *
+ * The deleted property is an array with nodenames, that have been deleted
+ * from collection.
+ *
+ * The $syncLevel argument is basically the 'depth' of the report. If it's
+ * 1, you only have to report changes that happened only directly in
+ * immediate descendants. If it's 2, it should also include changes from
+ * the nodes below the child collections. (grandchildren)
+ *
+ * The $limit argument allows a client to specify how many results should
+ * be returned at most. If the limit is not specified, it should be treated
+ * as infinite.
+ *
+ * If the limit (infinite or not) is higher than you're willing to return,
+ * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
+ *
+ * If the syncToken is expired (due to data cleanup) or unknown, you must
+ * return null.
+ *
+ * The limit is 'suggestive'. You are free to ignore it.
+ *
+ * @param string $calendarId
+ * @param string $syncToken
+ * @param int $syncLevel
+ * @param int $limit
+ * @return array
+ */
+ function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) {
+ // Current synctoken
+ $stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*calendars` WHERE `id` = ?');
+ $stmt->execute([ $calendarId ]);
+ $currentToken = $stmt->fetchColumn(0);
+
+ if (is_null($currentToken)) {
+ return null;
+ }
+
+ $result = [
+ 'syncToken' => $currentToken,
+ 'added' => [],
+ 'modified' => [],
+ 'deleted' => [],
+ ];
+
+ if ($syncToken) {
+
+ $query = "SELECT `uri`, `operation` FROM `*PREFIX*calendarchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `calendarid` = ? ORDER BY `synctoken`";
+ if ($limit>0) {
+ $query.= " `LIMIT` " . (int)$limit;
+ }
+
+ // Fetching all changes
+ $stmt = $this->db->prepare($query);
+ $stmt->execute([$syncToken, $currentToken, $calendarId]);
+
+ $changes = [];
+
+ // This loop ensures that any duplicates are overwritten, only the
+ // last change on a node is relevant.
+ while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ $changes[$row['uri']] = $row['operation'];
+
+ }
+
+ foreach($changes as $uri => $operation) {
+
+ switch($operation) {
+ case 1 :
+ $result['added'][] = $uri;
+ break;
+ case 2 :
+ $result['modified'][] = $uri;
+ break;
+ case 3 :
+ $result['deleted'][] = $uri;
+ break;
+ }
+
+ }
+ } else {
+ // No synctoken supplied, this is the initial sync.
+ $query = "SELECT `uri` FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?";
+ $stmt = $this->db->prepare($query);
+ $stmt->execute([$calendarId]);
+
+ $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
+ }
+ return $result;
+
+ }
+
+ /**
+ * Returns a list of subscriptions for a principal.
+ *
+ * Every subscription is an array with the following keys:
+ * * id, a unique id that will be used by other functions to modify the
+ * subscription. This can be the same as the uri or a database key.
+ * * uri. This is just the 'base uri' or 'filename' of the subscription.
+ * * principaluri. The owner of the subscription. Almost always the same as
+ * principalUri passed to this method.
+ *
+ * Furthermore, all the subscription info must be returned too:
+ *
+ * 1. {DAV:}displayname
+ * 2. {http://apple.com/ns/ical/}refreshrate
+ * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
+ * should not be stripped).
+ * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
+ * should not be stripped).
+ * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
+ * attachments should not be stripped).
+ * 6. {http://calendarserver.org/ns/}source (Must be a
+ * Sabre\DAV\Property\Href).
+ * 7. {http://apple.com/ns/ical/}calendar-color
+ * 8. {http://apple.com/ns/ical/}calendar-order
+ * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
+ * (should just be an instance of
+ * Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
+ * default components).
+ *
+ * @param string $principalUri
+ * @return array
+ */
+ function getSubscriptionsForUser($principalUri) {
+ $fields = array_values($this->subscriptionPropertyMap);
+ $fields[] = 'id';
+ $fields[] = 'uri';
+ $fields[] = 'source';
+ $fields[] = 'principaluri';
+ $fields[] = 'lastmodified';
+
+ $query = $this->db->getQueryBuilder();
+ $query->select($fields)
+ ->from('calendarsubscriptions')
+ ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
+ ->orderBy('calendarorder', 'asc');
+ $stmt =$query->execute();
+
+ $subscriptions = [];
+ while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ $subscription = [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'principaluri' => $row['principaluri'],
+ 'source' => $row['source'],
+ 'lastmodified' => $row['lastmodified'],
+
+ '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
+ ];
+
+ foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
+ if (!is_null($row[$dbName])) {
+ $subscription[$xmlName] = $row[$dbName];
+ }
+ }
+
+ $subscriptions[] = $subscription;
+
+ }
+
+ return $subscriptions;
+ }
+
+ /**
+ * Creates a new subscription for a principal.
+ *
+ * If the creation was a success, an id must be returned that can be used to reference
+ * this subscription in other methods, such as updateSubscription.
+ *
+ * @param string $principalUri
+ * @param string $uri
+ * @param array $properties
+ * @return mixed
+ */
+ function createSubscription($principalUri, $uri, array $properties) {
+
+ if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
+ throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
+ }
+
+ $values = [
+ 'principaluri' => $principalUri,
+ 'uri' => $uri,
+ 'source' => $properties['{http://calendarserver.org/ns/}source']->getHref(),
+ 'lastmodified' => time(),
+ ];
+
+ foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
+ if (isset($properties[$xmlName])) {
+
+ $values[$dbName] = $properties[$xmlName];
+ $fieldNames[] = $dbName;
+ }
+ }
+
+ $query = $this->db->getQueryBuilder();
+ $query->insert('calendarsubscriptions')
+ ->values([
+ 'principaluri' => $query->createNamedParameter($values['principaluri']),
+ 'uri' => $query->createNamedParameter($values['uri']),
+ 'source' => $query->createNamedParameter($values['source']),
+ 'lastmodified' => $query->createNamedParameter($values['lastmodified']),
+ ])
+ ->execute();
+
+ return $this->db->lastInsertId('*PREFIX*calendarsubscriptions');
+ }
+
+ /**
+ * Updates a subscription
+ *
+ * The list of mutations is stored in a Sabre\DAV\PropPatch object.
+ * To do the actual updates, you must tell this object which properties
+ * you're going to process with the handle() method.
+ *
+ * Calling the handle method is like telling the PropPatch object "I
+ * promise I can handle updating this property".
+ *
+ * Read the PropPatch documentation for more info and examples.
+ *
+ * @param mixed $subscriptionId
+ * @param \Sabre\DAV\PropPatch $propPatch
+ * @return void
+ */
+ function updateSubscription($subscriptionId, DAV\PropPatch $propPatch) {
+ $supportedProperties = array_keys($this->subscriptionPropertyMap);
+ $supportedProperties[] = '{http://calendarserver.org/ns/}source';
+
+ $propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) {
+
+ $newValues = [];
+
+ foreach($mutations as $propertyName=>$propertyValue) {
+ if ($propertyName === '{http://calendarserver.org/ns/}source') {
+ $newValues['source'] = $propertyValue->getHref();
+ } else {
+ $fieldName = $this->subscriptionPropertyMap[$propertyName];
+ $newValues[$fieldName] = $propertyValue;
+ }
+ }
+
+ $query = $this->db->getQueryBuilder();
+ $query->update('calendarsubscriptions')
+ ->set('lastmodified', $query->createNamedParameter(time()));
+ foreach($newValues as $fieldName=>$value) {
+ $query->set($fieldName, $query->createNamedParameter($value));
+ }
+ $query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
+ ->execute();
+
+ return true;
+
+ });
+ }
+
+ /**
+ * Deletes a subscription.
+ *
+ * @param mixed $subscriptionId
+ * @return void
+ */
+ function deleteSubscription($subscriptionId) {
+ $query = $this->db->getQueryBuilder();
+ $query->delete('calendarsubscriptions')
+ ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
+ ->execute();
+ }
+
+ /**
+ * Returns a single scheduling object for the inbox collection.
+ *
+ * The returned array should contain the following elements:
+ * * uri - A unique basename for the object. This will be used to
+ * construct a full uri.
+ * * calendardata - The iCalendar object
+ * * lastmodified - The last modification date. Can be an int for a unix
+ * timestamp, or a PHP DateTime object.
+ * * etag - A unique token that must change if the object changed.
+ * * size - The size of the object, in bytes.
+ *
+ * @param string $principalUri
+ * @param string $objectUri
+ * @return array
+ */
+ function getSchedulingObject($principalUri, $objectUri) {
+ $query = $this->db->getQueryBuilder();
+ $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
+ ->from('schedulingobjects')
+ ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
+ ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
+ ->execute();
+
+ $row = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+ if(!$row) {
+ return null;
+ }
+
+ return [
+ 'uri' => $row['uri'],
+ 'calendardata' => $row['calendardata'],
+ 'lastmodified' => $row['lastmodified'],
+ 'etag' => '"' . $row['etag'] . '"',
+ 'size' => (int)$row['size'],
+ ];
+ }
+
+ /**
+ * Returns all scheduling objects for the inbox collection.
+ *
+ * These objects should be returned as an array. Every item in the array
+ * should follow the same structure as returned from getSchedulingObject.
+ *
+ * The main difference is that 'calendardata' is optional.
+ *
+ * @param string $principalUri
+ * @return array
+ */
+ function getSchedulingObjects($principalUri) {
+ $query = $this->db->getQueryBuilder();
+ $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
+ ->from('schedulingobjects')
+ ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
+ ->execute();
+
+ $result = [];
+ foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
+ $result[] = [
+ 'calendardata' => $row['calendardata'],
+ 'uri' => $row['uri'],
+ 'lastmodified' => $row['lastmodified'],
+ 'etag' => '"' . $row['etag'] . '"',
+ 'size' => (int)$row['size'],
+ ];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Deletes a scheduling object from the inbox collection.
+ *
+ * @param string $principalUri
+ * @param string $objectUri
+ * @return void
+ */
+ function deleteSchedulingObject($principalUri, $objectUri) {
+ $query = $this->db->getQueryBuilder();
+ $query->delete('schedulingobjects')
+ ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
+ ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
+ ->execute();
+ }
+
+ /**
+ * Creates a new scheduling object. This should land in a users' inbox.
+ *
+ * @param string $principalUri
+ * @param string $objectUri
+ * @param string $objectData
+ * @return void
+ */
+ function createSchedulingObject($principalUri, $objectUri, $objectData) {
+ $query = $this->db->getQueryBuilder();
+ $query->insert('schedulingobjects')
+ ->values([
+ 'principaluri' => $query->createNamedParameter($principalUri),
+ 'calendardata' => $query->createNamedParameter($objectData),
+ 'uri' => $query->createNamedParameter($objectUri),
+ 'lastmodified' => $query->createNamedParameter(time()),
+ 'etag' => $query->createNamedParameter(md5($objectData)),
+ 'size' => $query->createNamedParameter(strlen($objectData))
+ ])
+ ->execute();
+ }
+
+ /**
+ * Adds a change record to the calendarchanges table.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @param int $operation 1 = add, 2 = modify, 3 = delete.
+ * @return void
+ */
+ protected function addChange($calendarId, $objectUri, $operation) {
+
+ $stmt = $this->db->prepare('INSERT INTO `*PREFIX*calendarchanges` (`uri`, `synctoken`, `calendarid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*calendars` WHERE `id` = ?');
+ $stmt->execute([
+ $objectUri,
+ $calendarId,
+ $operation,
+ $calendarId
+ ]);
+ $stmt = $this->db->prepare('UPDATE `*PREFIX*calendars` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
+ $stmt->execute([
+ $calendarId
+ ]);
+
+ }
+
+ /**
+ * Parses some information from calendar objects, used for optimized
+ * calendar-queries.
+ *
+ * Returns an array with the following keys:
+ * * etag - An md5 checksum of the object without the quotes.
+ * * size - Size of the object in bytes
+ * * componentType - VEVENT, VTODO or VJOURNAL
+ * * firstOccurence
+ * * lastOccurence
+ * * uid - value of the UID property
+ *
+ * @param string $calendarData
+ * @return array
+ */
+ protected function getDenormalizedData($calendarData) {
+
+ $vObject = Reader::read($calendarData);
+ $componentType = null;
+ $component = null;
+ $firstOccurence = null;
+ $lastOccurence = null;
+ $uid = null;
+ foreach($vObject->getComponents() as $component) {
+ if ($component->name!=='VTIMEZONE') {
+ $componentType = $component->name;
+ $uid = (string)$component->UID;
+ break;
+ }
+ }
+ if (!$componentType) {
+ throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
+ }
+ if ($componentType === 'VEVENT') {
+ $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp();
+ // Finding the last occurence is a bit harder
+ if (!isset($component->RRULE)) {
+ if (isset($component->DTEND)) {
+ $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp();
+ } elseif (isset($component->DURATION)) {
+ $endDate = clone $component->DTSTART->getDateTime();
+ $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
+ $lastOccurence = $endDate->getTimeStamp();
+ } elseif (!$component->DTSTART->hasTime()) {
+ $endDate = clone $component->DTSTART->getDateTime();
+ $endDate->modify('+1 day');
+ $lastOccurence = $endDate->getTimeStamp();
+ } else {
+ $lastOccurence = $firstOccurence;
+ }
+ } else {
+ $it = new RecurrenceIterator($vObject, (string)$component->UID);
+ $maxDate = new \DateTime(self::MAX_DATE);
+ if ($it->isInfinite()) {
+ $lastOccurence = $maxDate->getTimeStamp();
+ } else {
+ $end = $it->getDtEnd();
+ while($it->valid() && $end < $maxDate) {
+ $end = $it->getDtEnd();
+ $it->next();
+
+ }
+ $lastOccurence = $end->getTimeStamp();
+ }
+
+ }
+ }
+
+ return [
+ 'etag' => md5($calendarData),
+ 'size' => strlen($calendarData),
+ 'componentType' => $componentType,
+ 'firstOccurence' => $firstOccurence,
+ 'lastOccurence' => $lastOccurence,
+ 'uid' => $uid,
+ ];
+
+ }
+
+ private function readBlob($cardData) {
+ if (is_resource($cardData)) {
+ return stream_get_contents($cardData);
+ }
+
+ return $cardData;
+ }
+}
diff --git a/apps/dav/lib/carddav/addressbook.php b/apps/dav/lib/carddav/addressbook.php
new file mode 100644
index 0000000000..507657e968
--- /dev/null
+++ b/apps/dav/lib/carddav/addressbook.php
@@ -0,0 +1,90 @@
+carddavBackend;
+ $carddavBackend->updateShares($this->getName(), $add, $remove);
+ }
+
+ /**
+ * Returns the list of people whom this addressbook is shared with.
+ *
+ * Every element in this array should have the following properties:
+ * * href - Often a mailto: address
+ * * commonName - Optional, for example a first + last name
+ * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
+ * * readOnly - boolean
+ * * summary - Optional, a description for the share
+ *
+ * @return array
+ */
+ function getShares() {
+ /** @var CardDavBackend $carddavBackend */
+ $carddavBackend = $this->carddavBackend;
+ $carddavBackend->getShares($this->getName());
+ }
+
+ function getACL() {
+ $acl = parent::getACL();
+ if ($this->getOwner() === 'principals/system/system') {
+ $acl[] = [
+ 'privilege' => '{DAV:}read',
+ 'principal' => '{DAV:}authenticated',
+ 'protected' => true,
+ ];
+ }
+
+ return $acl;
+ }
+
+ function getChildACL() {
+ $acl = parent::getChildACL();
+ if ($this->getOwner() === 'principals/system/system') {
+ $acl[] = [
+ 'privilege' => '{DAV:}read',
+ 'principal' => '{DAV:}authenticated',
+ 'protected' => true,
+ ];
+ }
+
+ return $acl;
+ }
+
+ function getChild($name) {
+ $obj = $this->carddavBackend->getCard($this->addressBookInfo['id'], $name);
+ if (!$obj) {
+ throw new NotFound('Card not found');
+ }
+ return new Card($this->carddavBackend, $this->addressBookInfo, $obj);
+ }
+
+}
diff --git a/apps/dav/lib/carddav/addressbookroot.php b/apps/dav/lib/carddav/addressbookroot.php
new file mode 100644
index 0000000000..8c78d02455
--- /dev/null
+++ b/apps/dav/lib/carddav/addressbookroot.php
@@ -0,0 +1,33 @@
+carddavBackend, $principal['uri']);
+
+ }
+
+ function getName() {
+
+ // Grabbing all the components of the principal path.
+ $parts = explode('/', $this->principalPrefix);
+
+ // We are only interested in the second part.
+ return $parts[1];
+
+ }
+
+}
diff --git a/apps/dav/lib/carddav/card.php b/apps/dav/lib/carddav/card.php
new file mode 100644
index 0000000000..cea0b1e41c
--- /dev/null
+++ b/apps/dav/lib/carddav/card.php
@@ -0,0 +1,39 @@
+
+ *
+ * @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
+ *
+ */
+
+namespace OCA\DAV\CardDAV;
+
+class Card extends \Sabre\CardDAV\Card {
+
+ function getACL() {
+ $acl = parent::getACL();
+ if ($this->getOwner() === 'principals/system/system') {
+ $acl[] = [
+ 'privilege' => '{DAV:}read',
+ 'principal' => '{DAV:}authenticated',
+ 'protected' => true,
+ ];
+ }
+
+ return $acl;
+ }
+
+}
diff --git a/apps/dav/lib/carddav/carddavbackend.php b/apps/dav/lib/carddav/carddavbackend.php
index b2597baedc..29b056672b 100644
--- a/apps/dav/lib/carddav/carddavbackend.php
+++ b/apps/dav/lib/carddav/carddavbackend.php
@@ -22,6 +22,7 @@
namespace OCA\DAV\CardDAV;
+use OCA\DAV\Connector\Sabre\Principal;
use Sabre\CardDAV\Backend\BackendInterface;
use Sabre\CardDAV\Backend\SyncSupport;
use Sabre\CardDAV\Plugin;
@@ -29,8 +30,12 @@ use Sabre\DAV\Exception\BadRequest;
class CardDavBackend implements BackendInterface, SyncSupport {
- public function __construct(\OCP\IDBConnection $db) {
+ /** @var Principal */
+ private $principalBackend;
+
+ public function __construct(\OCP\IDBConnection $db, Principal $principalBackend) {
$this->db = $db;
+ $this->principalBackend = $principalBackend;
}
/**
@@ -73,9 +78,61 @@ class CardDavBackend implements BackendInterface, SyncSupport {
}
$result->closeCursor();
+ // query for shared calendars
+ $query = $this->db->getQueryBuilder();
+ $query2 = $this->db->getQueryBuilder();
+ $query2->select(['resourceid'])
+ ->from('dav_shares')
+ ->where($query2->expr()->eq('principaluri', $query2->createParameter('principaluri')))
+ ->andWhere($query2->expr()->eq('type', $query2->createParameter('type')));
+ $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
+ ->from('addressbooks')
+ ->where($query->expr()->in('id', $query->createFunction($query2->getSQL())))
+ ->setParameter('type', 'addressbook')
+ ->setParameter('principaluri', $principalUri)
+ ->execute();
+
+ while($row = $result->fetch()) {
+ $addressBooks[] = [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'principaluri' => $row['principaluri'],
+ '{DAV:}displayname' => $row['displayname'],
+ '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
+ '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ ];
+ }
+ $result->closeCursor();
+
return $addressBooks;
}
+ public function getAddressBooksByUri($addressBookUri) {
+ $query = $this->db->getQueryBuilder();
+ $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
+ ->from('addressbooks')
+ ->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
+ ->setMaxResults(1)
+ ->execute();
+
+ $row = $result->fetch();
+ $result->closeCursor();
+ if ($row === false) {
+ return null;
+ }
+
+ return [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'principaluri' => $row['principaluri'],
+ '{DAV:}displayname' => $row['displayname'],
+ '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
+ '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ ];
+ }
+
/**
* Updates properties for an address book.
*
@@ -86,7 +143,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* Calling the handle method is like telling the PropPatch object "I
* promise I can handle updating this property".
*
- * Read the PropPatch documenation for more info and examples.
+ * Read the PropPatch documentation for more info and examples.
*
* @param string $addressBookId
* @param \Sabre\DAV\PropPatch $propPatch
@@ -201,6 +258,11 @@ class CardDavBackend implements BackendInterface, SyncSupport {
->where($query->expr()->eq('id', $query->createParameter('id')))
->setParameter('id', $addressBookId)
->execute();
+
+ $query->delete('dav_shares')
+ ->where($query->expr()->eq('resourceid', $query->createNamedParameter($addressBookId)))
+ ->andWhere($query->expr()->eq('type', $query->createNamedParameter('addressbook')))
+ ->execute();
}
/**
@@ -281,7 +343,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* If the backend supports this, it may allow for some speed-ups.
*
* @param mixed $addressBookId
- * @param array $uris
+ * @param string[] $uris
* @return array
*/
function getMultipleCards($addressBookId, array $uris) {
@@ -328,7 +390,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @param mixed $addressBookId
* @param string $cardUri
* @param string $cardData
- * @return string|null
+ * @return string
*/
function createCard($addressBookId, $cardUri, $cardData) {
$etag = md5($cardData);
@@ -373,7 +435,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @param mixed $addressBookId
* @param string $cardUri
* @param string $cardData
- * @return string|null
+ * @return string
*/
function updateCard($addressBookId, $cardUri, $cardData) {
@@ -561,4 +623,115 @@ class CardDavBackend implements BackendInterface, SyncSupport {
return $cardData;
}
+ /**
+ * @param string $path
+ * @param string[] $add
+ * @param string[] $remove
+ */
+ public function updateShares($path, $add, $remove) {
+ foreach($add as $element) {
+ $this->shareWith($path, $element);
+ }
+ foreach($remove as $element) {
+ $this->unshare($path, $element);
+ }
+ }
+
+ /**
+ * @param string $addressBookUri
+ * @param string $element
+ */
+ private function shareWith($addressBookUri, $element) {
+ $user = $element['href'];
+ $parts = explode(':', $user, 2);
+ if ($parts[0] !== 'principal') {
+ return;
+ }
+ $p = $this->principalBackend->getPrincipalByPath($parts[1]);
+ if (is_null($p)) {
+ return;
+ }
+
+ $addressBook = $this->getAddressBooksByUri($addressBookUri);
+ if (is_null($addressBook)) {
+ return;
+ }
+
+ // remove the share if it already exists
+ $this->unshare($addressBookUri, $element);
+
+ $query = $this->db->getQueryBuilder();
+ $query->insert('dav_shares')
+ ->values([
+ 'principaluri' => $query->createNamedParameter($parts[1]),
+ 'uri' => $query->createNamedParameter($addressBookUri),
+ 'type' => $query->createNamedParameter('addressbook'),
+ 'access' => $query->createNamedParameter(0),
+ 'resourceid' => $query->createNamedParameter($addressBook['id'])
+ ]);
+ $query->execute();
+ }
+
+ /**
+ * @param string $addressBookUri
+ * @param string $element
+ */
+ private function unshare($addressBookUri, $element) {
+ $user = $element['href'];
+ $parts = explode(':', $user, 2);
+ if ($parts[0] !== 'principal') {
+ return;
+ }
+ $p = $this->principalBackend->getPrincipalByPath($parts[1]);
+ if (is_null($p)) {
+ return;
+ }
+
+ $addressBook = $this->getAddressBooksByUri($addressBookUri);
+ if (is_null($addressBook)) {
+ return;
+ }
+
+ $query = $this->db->getQueryBuilder();
+ $query->delete('dav_shares')
+ ->where($query->expr()->eq('resourceid', $query->createNamedParameter($addressBook['id'])))
+ ->andWhere($query->expr()->eq('type', $query->createNamedParameter('addressbook')))
+ ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($parts[1])))
+ ;
+ $query->execute();
+ }
+
+ /**
+ * Returns the list of people whom this address book is shared with.
+ *
+ * Every element in this array should have the following properties:
+ * * href - Often a mailto: address
+ * * commonName - Optional, for example a first + last name
+ * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
+ * * readOnly - boolean
+ * * summary - Optional, a description for the share
+ *
+ * @return array
+ */
+ public function getShares($addressBookUri) {
+ $query = $this->db->getQueryBuilder();
+ $result = $query->select(['principaluri', 'access'])
+ ->from('dav_shares')
+ ->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
+ ->andWhere($query->expr()->eq('type', $query->createNamedParameter('addressbook')))
+ ->execute();
+
+ $shares = [];
+ while($row = $result->fetch()) {
+ $p = $this->principalBackend->getPrincipalByPath($row['principaluri']);
+ $shares[]= [
+ 'href' => "principal:${p['uri']}",
+ 'commonName' => isset($p['{DAV:}displayname']) ? $p['{DAV:}displayname'] : '',
+ 'status' => 1,
+ 'readOnly' => ($row['access'] === 1)
+ ];
+ }
+
+ return $shares;
+ }
}
diff --git a/apps/dav/lib/carddav/converter.php b/apps/dav/lib/carddav/converter.php
new file mode 100644
index 0000000000..56b73eba4c
--- /dev/null
+++ b/apps/dav/lib/carddav/converter.php
@@ -0,0 +1,158 @@
+
+ *
+ * @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
+ *
+ */
+
+namespace OCA\DAV\CardDAV;
+
+use OCP\IImage;
+use OCP\IUser;
+use Sabre\VObject\Component\VCard;
+use Sabre\VObject\Property\Text;
+
+class Converter {
+
+ /**
+ * @param IUser $user
+ * @return VCard
+ */
+ public function createCardFromUser(IUser $user) {
+
+ $uid = $user->getUID();
+ $displayName = $user->getDisplayName();
+ $displayName = empty($displayName ) ? $uid : $displayName;
+ $emailAddress = $user->getEMailAddress();
+ $cloudId = $user->getCloudId();
+ $image = $user->getAvatarImage(-1);
+
+ $vCard = new VCard();
+ $vCard->add(new Text($vCard, 'UID', $uid));
+ if (!empty($displayName)) {
+ $vCard->add(new Text($vCard, 'FN', $displayName));
+ $vCard->add(new Text($vCard, 'N', $this->splitFullName($displayName)));
+ }
+ if (!empty($emailAddress)) {
+ $vCard->add(new Text($vCard, 'EMAIL', $emailAddress, ['TYPE' => 'OTHER']));
+ }
+ if (!empty($cloudId)) {
+ $vCard->add(new Text($vCard, 'CLOUD', $cloudId));
+ }
+ if ($image) {
+ $vCard->add('PHOTO', $image->data(), ['ENCODING' => 'b', 'TYPE' => $image->mimeType()]);
+ }
+ $vCard->validate();
+
+ return $vCard;
+ }
+
+ /**
+ * @param VCard $vCard
+ * @param IUser $user
+ * @return bool
+ */
+ public function updateCard(VCard $vCard, IUser $user) {
+ $uid = $user->getUID();
+ $displayName = $user->getDisplayName();
+ $displayName = empty($displayName ) ? $uid : $displayName;
+ $emailAddress = $user->getEMailAddress();
+ $cloudId = $user->getCloudId();
+ $image = $user->getAvatarImage(-1);
+
+ $updated = false;
+ if($this->propertyNeedsUpdate($vCard, 'FN', $displayName)) {
+ $vCard->FN = new Text($vCard, 'FN', $displayName);
+ unset($vCard->N);
+ $vCard->add(new Text($vCard, 'N', $this->splitFullName($displayName)));
+ $updated = true;
+ }
+ if($this->propertyNeedsUpdate($vCard, 'EMAIL', $emailAddress)) {
+ $vCard->EMAIL = new Text($vCard, 'EMAIL', $emailAddress);
+ $updated = true;
+ }
+ if($this->propertyNeedsUpdate($vCard, 'CLOUD', $cloudId)) {
+ $vCard->CLOUD = new Text($vCard, 'CLOUD', $cloudId);
+ $updated = true;
+ }
+
+ if($this->propertyNeedsUpdate($vCard, 'PHOTO', $image)) {
+ $vCard->add('PHOTO', $image->data(), ['ENCODING' => 'b', 'TYPE' => $image->mimeType()]);
+ $updated = true;
+ }
+
+ if (empty($emailAddress) && !is_null($vCard->EMAIL)) {
+ unset($vCard->EMAIL);
+ $updated = true;
+ }
+ if (empty($cloudId) && !is_null($vCard->CLOUD)) {
+ unset($vCard->CLOUD);
+ $updated = true;
+ }
+ if (empty($image) && !is_null($vCard->PHOTO)) {
+ unset($vCard->PHOTO);
+ $updated = true;
+ }
+
+ return $updated;
+ }
+
+ /**
+ * @param VCard $vCard
+ * @param string $name
+ * @param string|IImage $newValue
+ * @return bool
+ */
+ private function propertyNeedsUpdate(VCard $vCard, $name, $newValue) {
+ if (is_null($newValue)) {
+ return false;
+ }
+ $value = $vCard->__get($name);
+ if (!is_null($value)) {
+ $value = $value->getValue();
+ $newValue = $newValue instanceof IImage ? $newValue->data() : $newValue;
+
+ return $value !== $newValue;
+ }
+ return true;
+ }
+
+ /**
+ * @param string $fullName
+ * @return string[]
+ */
+ public function splitFullName($fullName) {
+ // Very basic western style parsing. I'm not gonna implement
+ // https://github.com/android/platform_packages_providers_contactsprovider/blob/master/src/com/android/providers/contacts/NameSplitter.java ;)
+
+ $elements = explode(' ', $fullName);
+ $result = ['', '', '', '', ''];
+ if (count($elements) > 2) {
+ $result[0] = implode(' ', array_slice($elements, count($elements)-1));
+ $result[1] = $elements[0];
+ $result[2] = implode(' ', array_slice($elements, 1, count($elements)-2));
+ } elseif (count($elements) === 2) {
+ $result[0] = $elements[1];
+ $result[1] = $elements[0];
+ } else {
+ $result[0] = $elements[0];
+ }
+
+ return $result;
+ }
+
+}
diff --git a/apps/dav/lib/carddav/plugin.php b/apps/dav/lib/carddav/plugin.php
new file mode 100644
index 0000000000..f2b3dcbfda
--- /dev/null
+++ b/apps/dav/lib/carddav/plugin.php
@@ -0,0 +1,47 @@
+
+ *
+ * @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
+ *
+ */
+
+namespace OCA\DAV\CardDAV;
+
+use Sabre\HTTP\URLUtil;
+
+class Plugin extends \Sabre\CardDAV\Plugin {
+
+ /**
+ * Returns the addressbook home for a given principal
+ *
+ * @param string $principal
+ * @return string
+ */
+ protected function getAddressbookHomeForPrincipal($principal) {
+
+ if (strrpos($principal, 'principals/users', -strlen($principal)) !== FALSE) {
+ list(, $principalId) = URLUtil::splitPath($principal);
+ return self::ADDRESSBOOK_ROOT . '/users/' . $principalId;
+ }
+ if (strrpos($principal, 'principals/system', -strlen($principal)) !== FALSE) {
+ list(, $principalId) = URLUtil::splitPath($principal);
+ return self::ADDRESSBOOK_ROOT . '/system/' . $principalId;
+ }
+
+ throw new \LogicException('This is not supposed to happen');
+ }
+}
diff --git a/apps/dav/lib/carddav/sharing/ishareableaddressbook.php b/apps/dav/lib/carddav/sharing/ishareableaddressbook.php
new file mode 100644
index 0000000000..856a9ed18e
--- /dev/null
+++ b/apps/dav/lib/carddav/sharing/ishareableaddressbook.php
@@ -0,0 +1,46 @@
+auth = $authBackEnd;
+ $this->request = $request;
+ }
+
+ /**
+ * Reference to SabreDAV server object.
+ *
+ * @var \Sabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * This method should return a list of server-features.
+ *
+ * This is for example 'versioning' and is added to the DAV: header
+ * in an OPTIONS response.
+ *
+ * @return string[]
+ */
+ function getFeatures() {
+
+ return ['oc-addressbook-sharing'];
+
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using Sabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ function getPluginName() {
+
+ return 'carddav-sharing';
+
+ }
+
+ /**
+ * This initializes the plugin.
+ *
+ * This function is called by Sabre\DAV\Server, after
+ * addPlugin is called.
+ *
+ * This method should set up the required event subscriptions.
+ *
+ * @param Server $server
+ * @return void
+ */
+ function initialize(Server $server) {
+ $this->server = $server;
+ $server->resourceTypeMapping['OCA\\DAV\CardDAV\\ISharedAddressbook'] = '{' . \Sabre\CardDAV\Plugin::NS_CARDDAV . '}shared';
+
+ $this->server->on('method:POST', [$this, 'httpPost']);
+ }
+
+ /**
+ * We intercept this to handle POST requests on calendars.
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return null|false
+ */
+ function httpPost(RequestInterface $request, ResponseInterface $response) {
+
+ $path = $request->getPath();
+
+ // Only handling xml
+ $contentType = $request->getHeader('Content-Type');
+ if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false)
+ return;
+
+ // Making sure the node exists
+ try {
+ $node = $this->server->tree->getNodeForPath($path);
+ } catch (NotFound $e) {
+ return;
+ }
+
+ // CSRF protection
+ $this->protectAgainstCSRF();
+
+ $requestBody = $request->getBodyAsString();
+
+ // If this request handler could not deal with this POST request, it
+ // will return 'null' and other plugins get a chance to handle the
+ // request.
+ //
+ // However, we already requested the full body. This is a problem,
+ // because a body can only be read once. This is why we preemptively
+ // re-populated the request body with the existing data.
+ $request->setBody($requestBody);
+
+ $dom = XMLUtil::loadDOMDocument($requestBody);
+
+ $documentType = XMLUtil::toClarkNotation($dom->firstChild);
+
+ switch ($documentType) {
+
+ // Dealing with the 'share' document, which modified invitees on a
+ // calendar.
+ case '{' . \Sabre\CardDAV\Plugin::NS_CARDDAV . '}share' :
+
+ // We can only deal with IShareableCalendar objects
+ if (!$node instanceof IShareableAddressBook) {
+ return;
+ }
+
+ $this->server->transactionType = 'post-calendar-share';
+
+ // Getting ACL info
+ $acl = $this->server->getPlugin('acl');
+
+ // If there's no ACL support, we allow everything
+ if ($acl) {
+ $acl->checkPrivileges($path, '{DAV:}write');
+ }
+
+ $mutations = $this->parseShareRequest($dom);
+
+ $node->updateShares($mutations[0], $mutations[1]);
+
+ $response->setStatus(200);
+ // Adding this because sending a response body may cause issues,
+ // and I wanted some type of indicator the response was handled.
+ $response->setHeader('X-Sabre-Status', 'everything-went-well');
+
+ // Breaking the event chain
+ return false;
+ }
+ }
+
+ /**
+ * Parses the 'share' POST request.
+ *
+ * This method returns an array, containing two arrays.
+ * The first array is a list of new sharees. Every element is a struct
+ * containing a:
+ * * href element. (usually a mailto: address)
+ * * commonName element (often a first and lastname, but can also be
+ * false)
+ * * readOnly (true or false)
+ * * summary (A description of the share, can also be false)
+ *
+ * The second array is a list of sharees that are to be removed. This is
+ * just a simple array with 'hrefs'.
+ *
+ * @param \DOMDocument $dom
+ * @return array
+ */
+ function parseShareRequest(\DOMDocument $dom) {
+
+ $xpath = new \DOMXPath($dom);
+ $xpath->registerNamespace('cs', \Sabre\CardDAV\Plugin::NS_CARDDAV);
+ $xpath->registerNamespace('d', 'urn:DAV');
+
+ $set = [];
+ $elems = $xpath->query('cs:set');
+
+ for ($i = 0; $i < $elems->length; $i++) {
+
+ $xset = $elems->item($i);
+ $set[] = [
+ 'href' => $xpath->evaluate('string(d:href)', $xset),
+ 'commonName' => $xpath->evaluate('string(cs:common-name)', $xset),
+ 'summary' => $xpath->evaluate('string(cs:summary)', $xset),
+ 'readOnly' => $xpath->evaluate('boolean(cs:read)', $xset) !== false
+ ];
+
+ }
+
+ $remove = [];
+ $elems = $xpath->query('cs:remove');
+
+ for ($i = 0; $i < $elems->length; $i++) {
+
+ $xremove = $elems->item($i);
+ $remove[] = $xpath->evaluate('string(d:href)', $xremove);
+
+ }
+
+ return [$set, $remove];
+
+ }
+
+ private function protectAgainstCSRF() {
+ $user = $this->auth->getCurrentUser();
+ if ($this->auth->isDavAuthenticated($user)) {
+ return true;
+ }
+
+ if ($this->request->passesCSRFCheck()) {
+ return true;
+ }
+
+ throw new BadRequest();
+ }
+
+
+}
diff --git a/apps/dav/lib/carddav/useraddressbooks.php b/apps/dav/lib/carddav/useraddressbooks.php
new file mode 100644
index 0000000000..093cee0e1b
--- /dev/null
+++ b/apps/dav/lib/carddav/useraddressbooks.php
@@ -0,0 +1,49 @@
+carddavBackend->getAddressBooksForUser($this->principalUri);
+ $objects = [];
+ foreach($addressBooks as $addressBook) {
+ $objects[] = new AddressBook($this->carddavBackend, $addressBook);
+ }
+ return $objects;
+
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ function getACL() {
+
+ $acl = parent::getACL();
+ if ($this->principalUri === 'principals/system/system') {
+ $acl[] = [
+ 'privilege' => '{DAV:}read',
+ 'principal' => '{DAV:}authenticated',
+ 'protected' => true,
+ ];
+ }
+
+ return $acl;
+ }
+
+}
diff --git a/apps/dav/lib/connector/sabre/auth.php b/apps/dav/lib/connector/sabre/auth.php
index 39a7df31b7..4f31977023 100644
--- a/apps/dav/lib/connector/sabre/auth.php
+++ b/apps/dav/lib/connector/sabre/auth.php
@@ -35,6 +35,8 @@ use OCP\IUserSession;
use Sabre\DAV\Auth\Backend\AbstractBasic;
use Sabre\DAV\Exception\NotAuthenticated;
use Sabre\DAV\Exception\ServiceUnavailable;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
class Auth extends AbstractBasic {
const DAV_AUTHENTICATED = 'AUTHENTICATED_TO_DAV_BACKEND';
@@ -52,6 +54,7 @@ class Auth extends AbstractBasic {
IUserSession $userSession) {
$this->session = $session;
$this->userSession = $userSession;
+ $this->principalPrefix = 'principals/users/';
}
/**
@@ -65,7 +68,7 @@ class Auth extends AbstractBasic {
* @param string $username
* @return bool
*/
- protected function isDavAuthenticated($username) {
+ public function isDavAuthenticated($username) {
return !is_null($this->session->get(self::DAV_AUTHENTICATED)) &&
$this->session->get(self::DAV_AUTHENTICATED) === $username;
}
@@ -122,22 +125,15 @@ class Auth extends AbstractBasic {
}
/**
- * Override function here. We want to cache authentication cookies
- * in the syncing client to avoid HTTP-401 roundtrips.
- * If the sync client supplies the cookies, then OC_User::isLoggedIn()
- * will return true and we can see this WebDAV request as already authenticated,
- * even if there are no HTTP Basic Auth headers.
- * In other case, just fallback to the parent implementation.
- *
- * @param \Sabre\DAV\Server $server
- * @param string $realm
- * @return bool
- * @throws ServiceUnavailable
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return array
* @throws NotAuthenticated
+ * @throws ServiceUnavailable
*/
- public function authenticate(\Sabre\DAV\Server $server, $realm) {
+ function check(RequestInterface $request, ResponseInterface $response) {
try {
- $result = $this->auth($server, $realm);
+ $result = $this->auth($request, $response);
return $result;
} catch (NotAuthenticated $e) {
throw $e;
@@ -149,11 +145,11 @@ class Auth extends AbstractBasic {
}
/**
- * @param \Sabre\DAV\Server $server
- * @param $realm
- * @return bool
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return array
*/
- private function auth(\Sabre\DAV\Server $server, $realm) {
+ private function auth(RequestInterface $request, ResponseInterface $response) {
if (\OC_User::handleApacheAuth() ||
($this->userSession->isLoggedIn() && is_null($this->session->get(self::DAV_AUTHENTICATED)))
) {
@@ -161,9 +157,16 @@ class Auth extends AbstractBasic {
\OC_Util::setupFS($user);
$this->currentUser = $user;
$this->session->close();
- return true;
+ return [true, $this->principalPrefix . $user];
}
- return parent::authenticate($server, $realm);
+ if (!$this->userSession->isLoggedIn() && $request->getHeader('X-Requested-With') === 'XMLHttpRequest') {
+ // do not re-authenticate over ajax, use dummy auth name to prevent browser popup
+ $response->addHeader('WWW-Authenticate','DummyBasic realm="' . $this->realm . '"');
+ $response->setStatus(401);
+ throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls');
+ }
+
+ return parent::check($request, $response);
}
}
diff --git a/apps/dav/lib/connector/sabre/directory.php b/apps/dav/lib/connector/sabre/directory.php
index 8c736ea010..b602dd2f7b 100644
--- a/apps/dav/lib/connector/sabre/directory.php
+++ b/apps/dav/lib/connector/sabre/directory.php
@@ -28,8 +28,10 @@
*/
namespace OCA\DAV\Connector\Sabre;
+use OCA\DAV\Connector\Sabre\Exception\Forbidden;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
+use OCP\Files\ForbiddenException;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
use Sabre\DAV\Exception\Locked;
@@ -117,6 +119,8 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node
throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
} catch (\OCP\Files\InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage());
+ } catch (ForbiddenException $ex) {
+ throw new Forbidden($ex->getMessage(), $ex->getRetry());
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
@@ -146,6 +150,8 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node
throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
} catch (\OCP\Files\InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage());
+ } catch (ForbiddenException $ex) {
+ throw new Forbidden($ex->getMessage(), $ex->getRetry());
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
@@ -247,6 +253,8 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node
// assume it wasn't possible to remove due to permission issue
throw new \Sabre\DAV\Exception\Forbidden();
}
+ } catch (ForbiddenException $ex) {
+ throw new Forbidden($ex->getMessage(), $ex->getRetry());
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
diff --git a/apps/dav/lib/connector/sabre/exception/forbidden.php b/apps/dav/lib/connector/sabre/exception/forbidden.php
new file mode 100644
index 0000000000..673958349f
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/exception/forbidden.php
@@ -0,0 +1,64 @@
+
+ *
+ * @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
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre\Exception;
+
+class Forbidden extends \Sabre\DAV\Exception\Forbidden {
+
+ const NS_OWNCLOUD = 'http://owncloud.org/ns';
+
+ /**
+ * @var bool
+ */
+ private $retry;
+
+ /**
+ * @param string $message
+ * @param bool $retry
+ * @param \Exception $previous
+ */
+ public function __construct($message, $retry = false, \Exception $previous = null) {
+ parent::__construct($message, 0, $previous);
+ $this->retry = $retry;
+ }
+
+ /**
+ * This method allows the exception to include additional information
+ * into the WebDAV error response
+ *
+ * @param \Sabre\DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(\Sabre\DAV\Server $server,\DOMElement $errorNode) {
+
+ // set ownCloud namespace
+ $errorNode->setAttribute('xmlns:o', self::NS_OWNCLOUD);
+
+ // adding the retry node
+ $error = $errorNode->ownerDocument->createElementNS('o:','o:retry', var_export($this->retry, true));
+ $errorNode->appendChild($error);
+
+ // adding the message node
+ $error = $errorNode->ownerDocument->createElementNS('o:','o:reason', $this->getMessage());
+ $errorNode->appendChild($error);
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/fakelockerplugin.php b/apps/dav/lib/connector/sabre/fakelockerplugin.php
new file mode 100644
index 0000000000..b75e7f137d
--- /dev/null
+++ b/apps/dav/lib/connector/sabre/fakelockerplugin.php
@@ -0,0 +1,155 @@
+
+ *
+ * @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
+ *
+ */
+
+namespace OCA\DAV\Connector\Sabre;
+
+use Sabre\DAV\Locks\LockInfo;
+use Sabre\DAV\ServerPlugin;
+use Sabre\DAV\Xml\Property\LockDiscovery;
+use Sabre\DAV\Xml\Property\SupportedLock;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Sabre\DAV\PropFind;
+use Sabre\DAV\INode;
+
+/**
+ * Class FakeLockerPlugin is a plugin only used when connections come in from
+ * OS X via Finder. The fake locking plugin does emulate Class 2 WebDAV support
+ * (locking of files) which allows Finder to access the storage in write mode as
+ * well.
+ *
+ * No real locking is performed, instead the plugin just returns always positive
+ * responses.
+ *
+ * @see https://github.com/owncloud/core/issues/17732
+ * @package OCA\DAV\Connector\Sabre
+ */
+class FakeLockerPlugin extends ServerPlugin {
+ /** @var \Sabre\DAV\Server */
+ private $server;
+
+ /** {@inheritDoc} */
+ public function initialize(\Sabre\DAV\Server $server) {
+ $this->server = $server;
+ $this->server->on('method:LOCK', [$this, 'fakeLockProvider'], 1);
+ $this->server->on('method:UNLOCK', [$this, 'fakeUnlockProvider'], 1);
+ $server->on('propFind', [$this, 'propFind']);
+ $server->on('validateTokens', [$this, 'validateTokens']);
+ }
+
+ /**
+ * Indicate that we support LOCK and UNLOCK
+ *
+ * @param string $path
+ * @return string[]
+ */
+ public function getHTTPMethods($path) {
+ return [
+ 'LOCK',
+ 'UNLOCK',
+ ];
+ }
+
+ /**
+ * Indicate that we support locking
+ *
+ * @return integer[]
+ */
+ function getFeatures() {
+ return [2];
+ }
+
+ /**
+ * Return some dummy response for PROPFIND requests with regard to locking
+ *
+ * @param PropFind $propFind
+ * @param INode $node
+ * @return void
+ */
+ function propFind(PropFind $propFind, INode $node) {
+ $propFind->handle('{DAV:}supportedlock', function() {
+ return new SupportedLock(true);
+ });
+ $propFind->handle('{DAV:}lockdiscovery', function() use ($propFind) {
+ return new LockDiscovery([]);
+ });
+ }
+
+ /**
+ * Mark a locking token always as valid
+ *
+ * @param RequestInterface $request
+ * @param array $conditions
+ */
+ public function validateTokens(RequestInterface $request, &$conditions) {
+ foreach($conditions as &$fileCondition) {
+ if(isset($fileCondition['tokens'])) {
+ foreach($fileCondition['tokens'] as &$token) {
+ if(isset($token['token'])) {
+ if(substr($token['token'], 0, 16) === 'opaquelocktoken:') {
+ $token['validToken'] = true;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Fakes a successful LOCK
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return bool
+ */
+ public function fakeLockProvider(RequestInterface $request,
+ ResponseInterface $response) {
+
+ $lockInfo = new LockInfo();
+ $lockInfo->token = md5($request->getPath());
+ $lockInfo->uri = $request->getPath();
+ $lockInfo->depth = \Sabre\DAV\Server::DEPTH_INFINITY;
+ $lockInfo->timeout = 1800;
+
+ $body = $this->server->xml->write('{DAV:}prop', [
+ '{DAV:}lockdiscovery' =>
+ new LockDiscovery([$lockInfo])
+ ]);
+
+ $response->setBody($body);
+
+ return false;
+ }
+
+ /**
+ * Fakes a successful LOCK
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return bool
+ */
+ public function fakeUnlockProvider(RequestInterface $request,
+ ResponseInterface $response) {
+ $response->setStatus(204);
+ $response->setHeader('Content-Length', '0');
+ return false;
+ }
+}
diff --git a/apps/dav/lib/connector/sabre/file.php b/apps/dav/lib/connector/sabre/file.php
index 961532daf5..c66f627c0a 100644
--- a/apps/dav/lib/connector/sabre/file.php
+++ b/apps/dav/lib/connector/sabre/file.php
@@ -35,9 +35,11 @@ namespace OCA\DAV\Connector\Sabre;
use OC\Files\Filesystem;
use OCA\DAV\Connector\Sabre\Exception\EntityTooLarge;
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
+use OCA\DAV\Connector\Sabre\Exception\Forbidden as DAVForbiddenException;
use OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType;
use OCP\Encryption\Exceptions\GenericEncryptionException;
use OCP\Files\EntityTooLargeException;
+use OCP\Files\ForbiddenException;
use OCP\Files\InvalidContentException;
use OCP\Files\InvalidPathException;
use OCP\Files\LockNotAcquiredException;
@@ -175,6 +177,8 @@ class File extends Node implements IFile {
\OCP\Util::writeLog('webdav', 'renaming part file to final file failed', \OCP\Util::ERROR);
throw new Exception('Could not rename part file to final file');
}
+ } catch (ForbiddenException $ex) {
+ throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
} catch (\Exception $e) {
$partStorage->unlink($internalPartPath);
$this->convertToSabreException($e);
@@ -188,7 +192,7 @@ class File extends Node implements IFile {
}
// since we skipped the view we need to scan and emit the hooks ourselves
- $this->fileView->getUpdater()->update($this->path);
+ $storage->getUpdater()->update($internalPath);
if ($view) {
$this->emitPostHooks($exists);
@@ -209,6 +213,9 @@ class File extends Node implements IFile {
return '"' . $this->info->getEtag() . '"';
}
+ /**
+ * @param string $path
+ */
private function emitPreHooks($exists, $path = null) {
if (is_null($path)) {
$path = $this->path;
@@ -234,6 +241,9 @@ class File extends Node implements IFile {
return $run;
}
+ /**
+ * @param string $path
+ */
private function emitPostHooks($exists, $path = null) {
if (is_null($path)) {
$path = $this->path;
@@ -256,7 +266,7 @@ class File extends Node implements IFile {
/**
* Returns the data
*
- * @return string|resource
+ * @return resource
* @throws Forbidden
* @throws ServiceUnavailable
*/
@@ -273,6 +283,8 @@ class File extends Node implements IFile {
throw new ServiceUnavailable("Encryption not ready: " . $e->getMessage());
} catch (StorageNotAvailableException $e) {
throw new ServiceUnavailable("Failed to open file: " . $e->getMessage());
+ } catch (ForbiddenException $ex) {
+ throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
@@ -296,6 +308,8 @@ class File extends Node implements IFile {
}
} catch (StorageNotAvailableException $e) {
throw new ServiceUnavailable("Failed to unlink: " . $e->getMessage());
+ } catch (ForbiddenException $ex) {
+ throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
@@ -306,7 +320,7 @@ class File extends Node implements IFile {
*
* If null is returned, we'll assume application/octet-stream
*
- * @return mixed
+ * @return string
*/
public function getContentType() {
$mimeType = $this->info->getMimetype();
@@ -424,7 +438,7 @@ class File extends Node implements IFile {
$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
// since we skipped the view we need to scan and emit the hooks ourselves
- $this->fileView->getUpdater()->update($targetPath);
+ $targetStorage->getUpdater()->update($targetInternalPath);
$this->emitPostHooks($exists, $targetPath);
@@ -474,6 +488,10 @@ class File extends Node implements IFile {
// a more general case - due to whatever reason the content could not be written
throw new Forbidden($e->getMessage(), 0, $e);
}
+ if ($e instanceof ForbiddenException) {
+ // the path for the file was forbidden
+ throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e);
+ }
if ($e instanceof EntityTooLargeException) {
// the file is too big to be stored
throw new EntityTooLarge($e->getMessage(), 0, $e);
diff --git a/apps/dav/lib/connector/sabre/filesplugin.php b/apps/dav/lib/connector/sabre/filesplugin.php
index 61b5360cac..e85a67a875 100644
--- a/apps/dav/lib/connector/sabre/filesplugin.php
+++ b/apps/dav/lib/connector/sabre/filesplugin.php
@@ -37,11 +37,14 @@ class FilesPlugin extends \Sabre\DAV\ServerPlugin {
// namespace
const NS_OWNCLOUD = 'http://owncloud.org/ns';
const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id';
+ const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid';
const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL';
const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size';
const GETETAG_PROPERTYNAME = '{DAV:}getetag';
const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified';
+ const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id';
+ const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name';
/**
* Reference to main server object
@@ -96,9 +99,12 @@ class FilesPlugin extends \Sabre\DAV\ServerPlugin {
$server->xmlNamespaces[self::NS_OWNCLOUD] = 'oc';
$server->protectedProperties[] = self::FILEID_PROPERTYNAME;
+ $server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME;
$server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME;
$server->protectedProperties[] = self::SIZE_PROPERTYNAME;
$server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME;
+ $server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME;
+ $server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME;
// normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH
$allowedProperties = ['{DAV:}getetag'];
@@ -110,6 +116,7 @@ class FilesPlugin extends \Sabre\DAV\ServerPlugin {
$this->server->on('afterBind', array($this, 'sendFileIdHeader'));
$this->server->on('afterWriteContent', array($this, 'sendFileIdHeader'));
$this->server->on('afterMethod:GET', [$this,'httpGet']);
+ $this->server->on('afterMethod:GET', array($this, 'handleDownloadToken'));
$this->server->on('afterResponse', function($request, ResponseInterface $response) {
$body = $response->getBody();
if (is_resource($body)) {
@@ -142,6 +149,32 @@ class FilesPlugin extends \Sabre\DAV\ServerPlugin {
}
}
+ /**
+ * This sets a cookie to be able to recognize the start of the download
+ * the content must not be longer than 32 characters and must only contain
+ * alphanumeric characters
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ */
+ function handleDownloadToken(RequestInterface $request, ResponseInterface $response) {
+ $queryParams = $request->getQueryParameters();
+
+ /**
+ * this sets a cookie to be able to recognize the start of the download
+ * the content must not be longer than 32 characters and must only contain
+ * alphanumeric characters
+ */
+ if (isset($queryParams['downloadStartSecret'])) {
+ $token = $queryParams['downloadStartSecret'];
+ if (!isset($token[32])
+ && preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) {
+ // FIXME: use $response->setHeader() instead
+ setcookie('ocDownloadStarted', $token, time() + 20, '/');
+ }
+ }
+ }
+
/**
* Plugin that adds a 'Content-Disposition: attachment' header to all files
* delivered by SabreDAV.
@@ -171,6 +204,10 @@ class FilesPlugin extends \Sabre\DAV\ServerPlugin {
return $node->getFileId();
});
+ $propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function() use ($node) {
+ return $node->getInternalFileId();
+ });
+
$propFind->handle(self::PERMISSIONS_PROPERTYNAME, function() use ($node) {
$perms = $node->getDavPermissions();
if ($this->isPublic) {
@@ -201,6 +238,16 @@ class FilesPlugin extends \Sabre\DAV\ServerPlugin {
return $node->getSize();
});
}
+
+ $propFind->handle(self::OWNER_ID_PROPERTYNAME, function() use ($node) {
+ $owner = $node->getOwner();
+ return $owner->getUID();
+ });
+ $propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function() use ($node) {
+ $owner = $node->getOwner();
+ $displayName = $owner->getDisplayName();
+ return $displayName;
+ });
}
/**
diff --git a/apps/dav/lib/connector/sabre/lockplugin.php b/apps/dav/lib/connector/sabre/lockplugin.php
index 5840e59854..d770b141eb 100644
--- a/apps/dav/lib/connector/sabre/lockplugin.php
+++ b/apps/dav/lib/connector/sabre/lockplugin.php
@@ -27,7 +27,6 @@ use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\ServerPlugin;
-use Sabre\DAV\Tree;
use Sabre\HTTP\RequestInterface;
class LockPlugin extends ServerPlugin {
@@ -38,18 +37,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}
*/
@@ -66,7 +53,7 @@ class LockPlugin extends ServerPlugin {
return;
}
try {
- $node = $this->tree->getNodeForPath($request->getPath());
+ $node = $this->server->tree->getNodeForPath($request->getPath());
} catch (NotFound $e) {
return;
}
@@ -84,7 +71,7 @@ class LockPlugin extends ServerPlugin {
return;
}
try {
- $node = $this->tree->getNodeForPath($request->getPath());
+ $node = $this->server->tree->getNodeForPath($request->getPath());
} catch (NotFound $e) {
return;
}
diff --git a/apps/dav/lib/connector/sabre/node.php b/apps/dav/lib/connector/sabre/node.php
index 814aaceb07..c4e0614077 100644
--- a/apps/dav/lib/connector/sabre/node.php
+++ b/apps/dav/lib/connector/sabre/node.php
@@ -178,7 +178,7 @@ abstract class Node implements \Sabre\DAV\INode {
/**
* Returns the size of the node, in bytes
*
- * @return int|float
+ * @return integer
*/
public function getSize() {
return $this->info->getSize();
@@ -207,7 +207,14 @@ abstract class Node implements \Sabre\DAV\INode {
}
/**
- * @return string|null
+ * @return integer
+ */
+ public function getInternalFileId() {
+ return $this->info->getId();
+ }
+
+ /**
+ * @return string
*/
public function getDavPermissions() {
$p = '';
@@ -238,6 +245,10 @@ abstract class Node implements \Sabre\DAV\INode {
return $p;
}
+ public function getOwner() {
+ return $this->info->getOwner();
+ }
+
protected function verifyPath() {
try {
$fileName = basename($this->info->getPath());
diff --git a/apps/dav/lib/connector/sabre/objecttree.php b/apps/dav/lib/connector/sabre/objecttree.php
index 80c0ef7461..2e9c1b9916 100644
--- a/apps/dav/lib/connector/sabre/objecttree.php
+++ b/apps/dav/lib/connector/sabre/objecttree.php
@@ -25,10 +25,12 @@
namespace OCA\DAV\Connector\Sabre;
+use OCA\DAV\Connector\Sabre\Exception\Forbidden;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
use OC\Files\FileInfo;
use OC\Files\Mount\MoveableMount;
+use OCP\Files\ForbiddenException;
use OCP\Files\StorageInvalidException;
use OCP\Files\StorageNotAvailableException;
use OCP\Lock\LockedException;
@@ -235,6 +237,8 @@ class ObjectTree extends \Sabre\DAV\Tree {
}
} catch (StorageNotAvailableException $e) {
throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
+ } catch (ForbiddenException $ex) {
+ throw new Forbidden($ex->getMessage(), $ex->getRetry());
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
@@ -274,6 +278,8 @@ class ObjectTree extends \Sabre\DAV\Tree {
$this->fileView->copy($source, $destination);
} catch (StorageNotAvailableException $e) {
throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
+ } catch (ForbiddenException $ex) {
+ throw new Forbidden($ex->getMessage(), $ex->getRetry());
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
diff --git a/apps/dav/lib/connector/sabre/principal.php b/apps/dav/lib/connector/sabre/principal.php
index 35215e1f63..cc9c1c40d5 100644
--- a/apps/dav/lib/connector/sabre/principal.php
+++ b/apps/dav/lib/connector/sabre/principal.php
@@ -30,9 +30,11 @@
namespace OCA\DAV\Connector\Sabre;
+use OCP\IUser;
use OCP\IUserManager;
use OCP\IConfig;
use \Sabre\DAV\PropPatch;
+use Sabre\HTTP\URLUtil;
class Principal implements \Sabre\DAVACL\PrincipalBackend\BackendInterface {
/** @var IConfig */
@@ -66,20 +68,9 @@ class Principal implements \Sabre\DAVACL\PrincipalBackend\BackendInterface {
public function getPrincipalsByPrefix($prefixPath) {
$principals = [];
- if ($prefixPath === 'principals') {
+ if ($prefixPath === 'principals/users') {
foreach($this->userManager->search('') as $user) {
-
- $principal = [
- 'uri' => 'principals/' . $user->getUID(),
- '{DAV:}displayname' => $user->getUID(),
- ];
-
- $email = $this->config->getUserValue($user->getUID(), 'settings', 'email');
- if(!empty($email)) {
- $principal['{http://sabredav.org/ns}email-address'] = $email;
- }
-
- $principals[] = $principal;
+ $principals[] = $this->userToPrincipal($user);
}
}
@@ -95,21 +86,18 @@ class Principal implements \Sabre\DAVACL\PrincipalBackend\BackendInterface {
* @return array
*/
public function getPrincipalByPath($path) {
- list($prefix, $name) = explode('/', $path);
+ $elements = explode('/', $path);
+ if ($elements[0] !== 'principals') {
+ return null;
+ }
+ if ($elements[1] !== 'users') {
+ return null;
+ }
+ $name = $elements[2];
$user = $this->userManager->get($name);
- if ($prefix === 'principals' && !is_null($user)) {
- $principal = [
- 'uri' => 'principals/' . $user->getUID(),
- '{DAV:}displayname' => $user->getUID(),
- ];
-
- $email = $this->config->getUserValue($user->getUID(), 'settings', 'email');
- if($email) {
- $principal['{http://sabredav.org/ns}email-address'] = $email;
- }
-
- return $principal;
+ if (!is_null($user)) {
+ return $this->userToPrincipal($user);
}
return null;
@@ -140,10 +128,10 @@ class Principal implements \Sabre\DAVACL\PrincipalBackend\BackendInterface {
* @throws \Sabre\DAV\Exception
*/
public function getGroupMembership($principal) {
- list($prefix, $name) = \Sabre\HTTP\URLUtil::splitPath($principal);
+ list($prefix, $name) = URLUtil::splitPath($principal);
$group_membership = array();
- if ($prefix === 'principals') {
+ if ($prefix === 'principals/users') {
$principal = $this->getPrincipalByPath($principal);
if (!$principal) {
throw new \Sabre\DAV\Exception('Principal not found');
@@ -151,8 +139,8 @@ class Principal implements \Sabre\DAVACL\PrincipalBackend\BackendInterface {
// TODO: for now the user principal has only its own groups
return array(
- 'principals/'.$name.'/calendar-proxy-read',
- 'principals/'.$name.'/calendar-proxy-write',
+ 'principals/users/'.$name.'/calendar-proxy-read',
+ 'principals/users/'.$name.'/calendar-proxy-write',
// The addressbook groups are not supported in Sabre,
// see http://groups.google.com/group/sabredav-discuss/browse_thread/thread/ef2fa9759d55f8c#msg_5720afc11602e753
//'principals/'.$name.'/addressbook-proxy-read',
@@ -168,7 +156,7 @@ class Principal implements \Sabre\DAVACL\PrincipalBackend\BackendInterface {
* The principals should be passed as a list of uri's.
*
* @param string $principal
- * @param array $members
+ * @param string[] $members
* @throws \Sabre\DAV\Exception
*/
public function setGroupMemberSet($principal, array $members) {
@@ -202,4 +190,24 @@ class Principal implements \Sabre\DAVACL\PrincipalBackend\BackendInterface {
function findByUri($uri, $principalPrefix) {
return '';
}
+
+ /**
+ * @param IUser $user
+ * @return array
+ */
+ protected function userToPrincipal($user) {
+ $userId = $user->getUID();
+ $displayName = $user->getDisplayName();
+ $principal = [
+ 'uri' => "principals/users/$userId",
+ '{DAV:}displayname' => is_null($displayName) ? $userId : $displayName,
+ ];
+
+ $email = $user->getEMailAddress();
+ if (!empty($email)) {
+ $principal['{http://sabredav.org/ns}email-address'] = $email;
+ return $principal;
+ }
+ return $principal;
+ }
}
diff --git a/apps/dav/lib/connector/sabre/serverfactory.php b/apps/dav/lib/connector/sabre/serverfactory.php
index f67e949e80..0f0377e96b 100644
--- a/apps/dav/lib/connector/sabre/serverfactory.php
+++ b/apps/dav/lib/connector/sabre/serverfactory.php
@@ -26,12 +26,41 @@ use OCP\Files\Mount\IMountManager;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\ILogger;
+use OCP\IRequest;
use OCP\ITagManager;
use OCP\IUserSession;
use Sabre\DAV\Auth\Backend\BackendInterface;
+use Sabre\DAV\Locks\Plugin;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class ServerFactory {
+ /** @var IConfig */
+ private $config;
+ /** @var ILogger */
+ private $logger;
+ /** @var IDBConnection */
+ private $databaseConnection;
+ /** @var IUserSession */
+ private $userSession;
+ /** @var IMountManager */
+ private $mountManager;
+ /** @var ITagManager */
+ private $tagManager;
+ /** @var EventDispatcherInterface */
+ private $dispatcher;
+ /** @var IRequest */
+ private $request;
+
+ /**
+ * @param IConfig $config
+ * @param ILogger $logger
+ * @param IDBConnection $databaseConnection
+ * @param IUserSession $userSession
+ * @param IMountManager $mountManager
+ * @param ITagManager $tagManager
+ * @param EventDispatcherInterface $dispatcher
+ * @param IRequest $request
+ */
public function __construct(
IConfig $config,
ILogger $logger,
@@ -39,7 +68,8 @@ class ServerFactory {
IUserSession $userSession,
IMountManager $mountManager,
ITagManager $tagManager,
- EventDispatcherInterface $dispatcher
+ EventDispatcherInterface $dispatcher,
+ IRequest $request
) {
$this->config = $config;
$this->logger = $logger;
@@ -48,6 +78,7 @@ class ServerFactory {
$this->mountManager = $mountManager;
$this->tagManager = $tagManager;
$this->dispatcher = $dispatcher;
+ $this->request = $request;
}
/**
@@ -57,7 +88,10 @@ class ServerFactory {
* @param callable $viewCallBack callback that should return the view for the dav endpoint
* @return Server
*/
- public function createServer($baseUri, $requestUri, BackendInterface $authBackend, callable $viewCallBack) {
+ public function createServer($baseUri,
+ $requestUri,
+ BackendInterface $authBackend,
+ callable $viewCallBack) {
// Fire up server
$objectTree = new \OCA\DAV\Connector\Sabre\ObjectTree();
$server = new \OCA\DAV\Connector\Sabre\Server($objectTree);
@@ -73,8 +107,13 @@ class ServerFactory {
// FIXME: The following line is a workaround for legacy components relying on being able to send a GET to /
$server->addPlugin(new \OCA\DAV\Connector\Sabre\DummyGetResponsePlugin());
$server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $this->logger));
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin($objectTree));
+ $server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin());
$server->addPlugin(new \OCA\DAV\Connector\Sabre\ListenerPlugin($this->dispatcher));
+ // Finder on OS X requires Class 2 WebDAV support (locking), since we do
+ // not provide locking we emulate it using a fake locking plugin.
+ if($this->request->isUserAgent(['/WebDAVFS/'])) {
+ $server->addPlugin(new \OCA\DAV\Connector\Sabre\FakeLockerPlugin());
+ }
// wait with registering these until auth is handled and the filesystem is setup
$server->on('beforeMethod', function () use ($server, $objectTree, $viewCallBack) {
diff --git a/apps/dav/lib/connector/sabre/taglist.php b/apps/dav/lib/connector/sabre/taglist.php
index 177cc23e80..1b32d4b104 100644
--- a/apps/dav/lib/connector/sabre/taglist.php
+++ b/apps/dav/lib/connector/sabre/taglist.php
@@ -22,82 +22,100 @@
namespace OCA\DAV\Connector\Sabre;
-use Sabre\DAV;
+use Sabre\Xml\Element;
+use Sabre\Xml\Reader;
+use Sabre\Xml\Writer;
/**
* TagList property
*
* This property contains multiple "tag" elements, each containing a tag name.
*/
-class TagList extends DAV\Property {
+class TagList implements Element {
const NS_OWNCLOUD = 'http://owncloud.org/ns';
- /**
- * tags
- *
- * @var array
- */
- private $tags;
+ /**
+ * tags
+ *
+ * @var array
+ */
+ private $tags;
- /**
- * @param array $tags
- */
- public function __construct(array $tags) {
- $this->tags = $tags;
- }
+ /**
+ * @param array $tags
+ */
+ public function __construct(array $tags) {
+ $this->tags = $tags;
+ }
- /**
- * Returns the tags
- *
- * @return array
- */
- public function getTags() {
+ /**
+ * Returns the tags
+ *
+ * @return array
+ */
+ public function getTags() {
- return $this->tags;
+ return $this->tags;
- }
+ }
- /**
- * Serializes this property.
- *
- * @param DAV\Server $server
- * @param \DOMElement $dom
- * @return void
- */
- public function serialize(DAV\Server $server,\DOMElement $dom) {
+ /**
+ * The deserialize method is called during xml parsing.
+ *
+ * This method is called statictly, this is because in theory this method
+ * may be used as a type of constructor, or factory method.
+ *
+ * Often you want to return an instance of the current class, but you are
+ * free to return other data as well.
+ *
+ * You are responsible for advancing the reader to the next element. Not
+ * doing anything will result in a never-ending loop.
+ *
+ * If you just want to skip parsing for this element altogether, you can
+ * just call $reader->next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @param Reader $reader
+ * @return mixed
+ */
+ static function xmlDeserialize(Reader $reader) {
+ $tags = [];
- $prefix = $server->xmlNamespaces[self::NS_OWNCLOUD];
+ foreach ($reader->parseInnerTree() as $elem) {
+ if ($elem['name'] === '{' . self::NS_OWNCLOUD . '}tag') {
+ $tags[] = $elem['value'];
+ }
+ }
+ return new self($tags);
+ }
- foreach($this->tags as $tag) {
-
- $elem = $dom->ownerDocument->createElement($prefix . ':tag');
- $elem->appendChild($dom->ownerDocument->createTextNode($tag));
-
- $dom->appendChild($elem);
- }
-
- }
-
- /**
- * Unserializes this property from a DOM Element
- *
- * This method returns an instance of this class.
- * It will only decode tag values.
- *
- * @param \DOMElement $dom
- * @param array $propertyMap
- * @return \OCA\DAV\Connector\Sabre\TagList
- */
- static function unserialize(\DOMElement $dom, array $propertyMap) {
-
- $tags = array();
- foreach($dom->childNodes as $child) {
- if (DAV\XMLUtil::toClarkNotation($child)==='{' . self::NS_OWNCLOUD . '}tag') {
- $tags[] = $child->textContent;
- }
- }
- return new self($tags);
-
- }
+ /**
+ * The xmlSerialize metod is called during xml writing.
+ *
+ * Use the $writer argument to write its own xml serialization.
+ *
+ * An important note: do _not_ create a parent element. Any element
+ * implementing XmlSerializble should only ever write what's considered
+ * its 'inner xml'.
+ *
+ * The parent of the current element is responsible for writing a
+ * containing element.
+ *
+ * This allows serializers to be re-used for different element names.
+ *
+ * If you are opening new elements, you must also close them again.
+ *
+ * @param Writer $writer
+ * @return void
+ */
+ function xmlSerialize(Writer $writer) {
+ foreach ($this->tags as $tag) {
+ $writer->startElement(self::NS_OWNCLOUD . ':tag');
+ $writer->writeElement($tag);
+ $writer->endElement();
+ }
+ }
}
diff --git a/apps/dav/lib/dav/systemprincipalbackend.php b/apps/dav/lib/dav/systemprincipalbackend.php
new file mode 100644
index 0000000000..2c2049ace6
--- /dev/null
+++ b/apps/dav/lib/dav/systemprincipalbackend.php
@@ -0,0 +1,183 @@
+
+ *
+ * @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
+ *
+ */
+
+namespace OCA\DAV\DAV;
+
+use Sabre\DAVACL\PrincipalBackend\AbstractBackend;
+use Sabre\HTTP\URLUtil;
+
+class SystemPrincipalBackend extends AbstractBackend {
+
+ /**
+ * Returns a list of principals based on a prefix.
+ *
+ * This prefix will often contain something like 'principals'. You are only
+ * expected to return principals that are in this base path.
+ *
+ * You are expected to return at least a 'uri' for every user, you can
+ * return any additional properties if you wish so. Common properties are:
+ * {DAV:}displayname
+ * {http://sabredav.org/ns}email-address - This is a custom SabreDAV
+ * field that's actually injected in a number of other properties. If
+ * you have an email address, use this property.
+ *
+ * @param string $prefixPath
+ * @return array
+ */
+ function getPrincipalsByPrefix($prefixPath) {
+ $principals = [];
+
+ if ($prefixPath === 'principals/system') {
+ $principals[] = [
+ 'uri' => 'principals/system/system',
+ '{DAV:}displayname' => 'system',
+ ];
+ }
+
+ return $principals;
+ }
+
+ /**
+ * Returns a specific principal, specified by it's path.
+ * The returned structure should be the exact same as from
+ * getPrincipalsByPrefix.
+ *
+ * @param string $path
+ * @return array
+ */
+ function getPrincipalByPath($path) {
+
+ $elements = explode('/', $path);
+ if ($elements[0] !== 'principals') {
+ return null;
+ }
+ if ($elements[1] === 'system') {
+ $principal = [
+ 'uri' => 'principals/system/system',
+ '{DAV:}displayname' => 'system',
+ ];
+ return $principal;
+ }
+
+ return null;
+ }
+
+ /**
+ * Updates one ore more webdav properties on a principal.
+ *
+ * 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 updatePrincipal($path, \Sabre\DAV\PropPatch $propPatch) {
+ }
+
+ /**
+ * This method is used to search for principals matching a set of
+ * properties.
+ *
+ * This search is specifically used by RFC3744's principal-property-search
+ * REPORT.
+ *
+ * The actual search should be a unicode-non-case-sensitive search. The
+ * keys in searchProperties are the WebDAV property names, while the values
+ * are the property values to search on.
+ *
+ * By default, if multiple properties are submitted to this method, the
+ * various properties should be combined with 'AND'. If $test is set to
+ * 'anyof', it should be combined using 'OR'.
+ *
+ * This method should simply return an array with full principal uri's.
+ *
+ * If somebody attempted to search on a property the backend does not
+ * support, you should simply return 0 results.
+ *
+ * You can also just return 0 results if you choose to not support
+ * searching at all, but keep in mind that this may stop certain features
+ * from working.
+ *
+ * @param string $prefixPath
+ * @param array $searchProperties
+ * @param string $test
+ * @return array
+ */
+ function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
+ return [];
+ }
+
+ /**
+ * Returns the list of members for a group-principal
+ *
+ * @param string $principal
+ * @return array
+ */
+ function getGroupMemberSet($principal) {
+ // TODO: for now the group principal has only one member, the user itself
+ $principal = $this->getPrincipalByPath($principal);
+ if (!$principal) {
+ throw new \Sabre\DAV\Exception('Principal not found');
+ }
+
+ return [$principal['uri']];
+ }
+
+ /**
+ * Returns the list of groups a principal is a member of
+ *
+ * @param string $principal
+ * @return array
+ */
+ function getGroupMembership($principal) {
+ list($prefix, $name) = URLUtil::splitPath($principal);
+
+ if ($prefix === 'principals/system') {
+ $principal = $this->getPrincipalByPath($principal);
+ if (!$principal) {
+ throw new \Sabre\DAV\Exception('Principal not found');
+ }
+
+ return [];
+ }
+ return [];
+ }
+
+ /**
+ * Updates the list of group members for a group principal.
+ *
+ * The principals should be passed as a list of uri's.
+ *
+ * @param string $principal
+ * @param array $members
+ * @return void
+ */
+ function setGroupMemberSet($principal, array $members) {
+ throw new \Sabre\DAV\Exception('Setting members of the group is not supported yet');
+ }
+}
diff --git a/apps/dav/lib/rootcollection.php b/apps/dav/lib/rootcollection.php
index 850180d848..c1635c9cde 100644
--- a/apps/dav/lib/rootcollection.php
+++ b/apps/dav/lib/rootcollection.php
@@ -2,36 +2,55 @@
namespace OCA\DAV;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CardDAV\AddressBookRoot;
use OCA\DAV\CardDAV\CardDavBackend;
use OCA\DAV\Connector\Sabre\Principal;
+use OCA\DAV\DAV\SystemPrincipalBackend;
+use Sabre\CalDAV\CalendarRoot;
use Sabre\CalDAV\Principal\Collection;
-use Sabre\CardDAV\AddressBookRoot;
use Sabre\DAV\SimpleCollection;
class RootCollection extends SimpleCollection {
public function __construct() {
$config = \OC::$server->getConfig();
+ $db = \OC::$server->getDatabaseConnection();
$principalBackend = new Principal(
- $config,
- \OC::$server->getUserManager()
+ $config,
+ \OC::$server->getUserManager()
);
// as soon as debug mode is enabled we allow listing of principals
$disableListing = !$config->getSystemValue('debug', false);
// setup the first level of the dav tree
- $principalCollection = new Collection($principalBackend);
- $principalCollection->disableListing = $disableListing;
- $filesCollection = new Files\RootCollection($principalBackend);
+ $userPrincipals = new Collection($principalBackend, 'principals/users');
+ $userPrincipals->disableListing = $disableListing;
+ $systemPrincipals = new Collection(new SystemPrincipalBackend(), 'principals/system');
+ $systemPrincipals->disableListing = $disableListing;
+ $filesCollection = new Files\RootCollection($principalBackend, 'principals/users');
$filesCollection->disableListing = $disableListing;
- $cardDavBackend = new CardDavBackend(\OC::$server->getDatabaseConnection());
- $addressBookRoot = new AddressBookRoot($principalBackend, $cardDavBackend);
- $addressBookRoot->disableListing = $disableListing;
+ $caldavBackend = new CalDavBackend($db);
+ $calendarRoot = new CalendarRoot($principalBackend, $caldavBackend, 'principals/users');
+ $calendarRoot->disableListing = $disableListing;
+
+ $usersCardDavBackend = new CardDavBackend($db, $principalBackend);
+ $usersAddressBookRoot = new AddressBookRoot($principalBackend, $usersCardDavBackend, 'principals/users');
+ $usersAddressBookRoot->disableListing = $disableListing;
+
+ $systemCardDavBackend = new CardDavBackend($db, $principalBackend);
+ $systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, 'principals/system');
+ $systemAddressBookRoot->disableListing = $disableListing;
$children = [
- $principalCollection,
- $filesCollection,
- $addressBookRoot,
+ new SimpleCollection('principals', [
+ $userPrincipals,
+ $systemPrincipals]),
+ $filesCollection,
+ $calendarRoot,
+ new SimpleCollection('addressbooks', [
+ $usersAddressBookRoot,
+ $systemAddressBookRoot]),
];
parent::__construct('root', $children);
diff --git a/apps/dav/lib/server.php b/apps/dav/lib/server.php
index a92c9980f5..a031f2c442 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,10 +35,36 @@ 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));
+ $this->server->addPlugin(new \Sabre\DAV\Sync\Plugin());
- $this->server->addPlugin(new \Sabre\DAVACL\Plugin());
+ // acl
+ $acl = new \Sabre\DAVACL\Plugin();
+ $acl->defaultUsernamePath = 'principals/users';
+ $this->server->addPlugin($acl);
- $this->server->addPlugin(new \Sabre\CardDAV\Plugin());
+ // calendar plugins
+ $this->server->addPlugin(new \Sabre\CalDAV\Plugin());
+ $this->server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin());
+ $senderEmail = \OCP\Util::getDefaultEmailAddress('no-reply');
+ $this->server->addPlugin(new \Sabre\CalDAV\Schedule\Plugin());
+ $this->server->addPlugin(new \Sabre\CalDAV\Schedule\IMipPlugin($senderEmail));
+ $this->server->addPlugin(new \Sabre\CalDAV\SharingPlugin());
+ $this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin());
+ $this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin());
+ $this->server->addPlugin(new CardDAV\Sharing\Plugin($authBackend, \OC::$server->getRequest()));
+
+ // addressbook plugins
+ $this->server->addPlugin(new \OCA\DAV\CardDAV\Plugin());
+
+ // Finder on OS X requires Class 2 WebDAV support (locking), since we do
+ // not provide locking we emulate it using a fake locking plugin.
+ if($request->isUserAgent(['/WebDAVFS/'])) {
+ $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\FakeLockerPlugin());
+ }
// wait with registering these until auth is handled and the filesystem is setup
$this->server->on('beforeMethod', function () {
diff --git a/apps/dav/tests/misc/sharing.xml b/apps/dav/tests/misc/sharing.xml
new file mode 100644
index 0000000000..8771256ce7
--- /dev/null
+++ b/apps/dav/tests/misc/sharing.xml
@@ -0,0 +1,7 @@
+
+
+
+ principal:principals/admin
+
+
+
diff --git a/apps/dav/tests/travis/caldavtest/config/serverinfo.xml b/apps/dav/tests/travis/caldavtest/config/serverinfo.xml
index b85a8639e4..c80e47f948 100644
--- a/apps/dav/tests/travis/caldavtest/config/serverinfo.xml
+++ b/apps/dav/tests/travis/caldavtest/config/serverinfo.xml
@@ -180,7 +180,7 @@
$principalcollection:
- $root:principals/
+ $root:principals/users/
@@ -569,7 +569,7 @@
$addressbookhome%d:
- $addressbooks_uids:$userguid%d:
+ $addressbooks:users/$userid%d:
diff --git a/apps/dav/tests/travis/caldavtest/tests/CardDAV/sync-report.xml b/apps/dav/tests/travis/caldavtest/tests/CardDAV/sync-report.xml
new file mode 100644
index 0000000000..ffa6662981
--- /dev/null
+++ b/apps/dav/tests/travis/caldavtest/tests/CardDAV/sync-report.xml
@@ -0,0 +1,1602 @@
+
+
+
+
+
+
+
+
+ carddav
+ sync-report
+
+
+
+
+ PUT
+ $addressbookpath1:/1.vcf
+
+ text/vcard; charset=utf-8
+ Resource/CardDAV/vreports/put/1.vcf
+
+
+
+ PUT
+ $addressbookpath1:/2.vcf
+
+ text/vcard; charset=utf-8
+ Resource/CardDAV/vreports/put/2.vcf
+
+
+
+
+
+
+ Not on addressbooks
+
+ PROPFIND
+ $addressbooks:/
+
+ Depth
+ 0
+
+
+ text/xml; charset=utf-8
+ Resource/CardDAV/vreports/sync/1.xml
+
+
+ xmlElementMatch
+
+ notexists
+ $verify-property-prefix:/{DAV:}supported-report-set/{DAV:}supported-report/{DAV:}report/{DAV:}sync-collection
+
+
+
+ propfindItems
+
+ okprops
+ {DAV:}supported-report-set
+
+
+ badprops
+ {DAV:}sync-token
+
+
+
+
+
+
+ On addressbook
+
+ PROPFIND
+ $addressbookpath1:/
+
+ Depth
+ 0
+
+
+ text/xml; charset=utf-8
+ Resource/CardDAV/vreports/sync/1.xml
+
+
+ xmlElementMatch
+
+ exists
+ $verify-property-prefix:/{DAV:}supported-report-set/{DAV:}supported-report/{DAV:}report/{DAV:}sync-collection
+
+
+
+
+
+ propfindItems
+
+ okprops
+ {DAV:}supported-report-set
+ {DAV:}sync-token
+
+
+
+
+
+
+
+
+ initial query - addressbook depth:1
+
+ REPORT
+ $addressbookpath1:/
+
+ Depth
+ 1
+
+
+ text/xml; charset=utf-8
+ Resource/CardDAV/vreports/sync/2.xml
+
+
+ multistatusItems
+
+ okhrefs
+
+
+ 1.vcf
+ 2.vcf
+
+
+
+
+
+
+ add new resource
+
+ PUT
+ $addressbookpath1:/3.vcf
+
+ text/vcard; charset=utf-8
+ Resource/CardDAV/vreports/put/3.vcf
+
+
+ statusCode
+
+
+
+
+ new resource - addressbook depth:1
+
+ REPORT
+ $addressbookpath1:/
+
+ Depth
+ 1
+
+
+ text/xml; charset=utf-8
+ Resource/CardDAV/vreports/sync/2.xml
+
+
+ multistatusItems
+
+ okhrefs
+
+
+ 1.vcf
+ 2.vcf
+ 3.vcf
+
+
+
+
+
+
+ remove new resource
+
+ DELETE
+ $addressbookpath1:/3.vcf
+
+ statusCode
+
+
+
+
+ remove new resource - addressbook depth:1
+
+ REPORT
+ $addressbookpath1:/
+
+ Depth
+ 1
+
+
+ text/xml; charset=utf-8
+ Resource/CardDAV/vreports/sync/2.xml
+
+
+ multistatusItems
+
+ okhrefs
+
+
+ 1.vcf
+ 2.vcf
+
+
+
+
+
+
+
+
+
+
+
+ DELETEALL
+ $addressbookhome1:/
+ $addressbookhome2:/
+ $notificationpath1:/
+ $notificationpath2:/
+
+
+
+
diff --git a/apps/dav/tests/travis/carddavtester.sh b/apps/dav/tests/travis/carddavtester.sh
index a128872f42..17f7e8eb4a 100644
--- a/apps/dav/tests/travis/carddavtester.sh
+++ b/apps/dav/tests/travis/carddavtester.sh
@@ -18,11 +18,18 @@ fi
# create test user
cd "$SCRIPTPATH/../../../../"
OC_PASS=user01 php occ user:add --password-from-env user01
+php occ dav:create-addressbook user01 addressbook
OC_PASS=user02 php occ user:add --password-from-env user02
+php occ dav:create-addressbook user02 addressbook
cd "$SCRIPTPATH/../../../../"
# run the tests
cd "$SCRIPTPATH/CalDAVTester"
PYTHONPATH="$SCRIPTPATH/pycalendar/src" python testcaldav.py --print-details-onfail -s "$SCRIPTPATH/caldavtest/config/serverinfo.xml" -o cdt.txt \
- "$SCRIPTPATH/caldavtest/tests/CardDAV/current-user-principal.xml"
+ "$SCRIPTPATH/caldavtest/tests/CardDAV/current-user-principal.xml" \
+ "$SCRIPTPATH/caldavtest/tests/CardDAV/sync-report.xml"
+RESULT=$?
+tail "$SCRIPTPATH/../../../../data-autotest/owncloud.log"
+
+exit $RESULT
diff --git a/apps/dav/tests/unit/bootstrap.php b/apps/dav/tests/unit/bootstrap.php
index 28f6b971de..b6ea48ec90 100644
--- a/apps/dav/tests/unit/bootstrap.php
+++ b/apps/dav/tests/unit/bootstrap.php
@@ -1,6 +1,8 @@
+ *
+ * @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
+ *
+ */
+namespace Tests\Connector\Sabre;
+
+use DateTime;
+use DateTimeZone;
+use OCA\DAV\CalDAV\CalDavBackend;
+use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
+use Sabre\DAV\PropPatch;
+use Sabre\DAV\Xml\Property\Href;
+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 = <<getUniqueID('event');
+ $this->backend->createCalendarObject($calendarId, $uri0, $calData);
+
+ return $uri0;
+ }
+
+ public function testSyncSupport() {
+ $calendarId = $this->createTestCalendar();
+
+ // fist call without synctoken
+ $changes = $this->backend->getChangesForCalendar($calendarId, '', 1);
+ $syncToken = $changes['syncToken'];
+
+ // add a change
+ $event = $this->createEvent($calendarId, '20130912T130000Z', '20130912T140000Z');
+
+ // look for changes
+ $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 1);
+ $this->assertEquals($event, $changes['added'][0]);
+ }
+
+ public function testSubscriptions() {
+ $id = $this->backend->createSubscription(self::UNIT_TEST_USER, 'Subscription', [
+ '{http://calendarserver.org/ns/}source' => new Href('test-source')
+ ]);
+
+ $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($subscriptions));
+ $this->assertEquals($id, $subscriptions[0]['id']);
+
+ $patch = new PropPatch([
+ '{DAV:}displayname' => 'Unit test',
+ ]);
+ $this->backend->updateSubscription($id, $patch);
+ $patch->commit();
+
+ $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($subscriptions));
+ $this->assertEquals($id, $subscriptions[0]['id']);
+ $this->assertEquals('Unit test', $subscriptions[0]['{DAV:}displayname']);
+
+ $this->backend->deleteSubscription($id);
+ $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(0, count($subscriptions));
+ }
+
+ public function testScheduling() {
+ $this->backend->createSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule', '');
+
+ $sos = $this->backend->getSchedulingObjects(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($sos));
+
+ $so = $this->backend->getSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule');
+ $this->assertNotNull($so);
+
+ $this->backend->deleteSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule');
+
+ $sos = $this->backend->getSchedulingObjects(self::UNIT_TEST_USER);
+ $this->assertEquals(0, count($sos));
+ }
+}
diff --git a/apps/dav/tests/unit/carddav/carddavbackendtest.php b/apps/dav/tests/unit/carddav/carddavbackendtest.php
index 79ef36d809..dd5e205242 100644
--- a/apps/dav/tests/unit/carddav/carddavbackendtest.php
+++ b/apps/dav/tests/unit/carddav/carddavbackendtest.php
@@ -24,6 +24,13 @@ use OCA\DAV\CardDAV\CardDavBackend;
use Sabre\DAV\PropPatch;
use Test\TestCase;
+/**
+ * Class CardDavBackendTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\Unit\CardDAV
+ */
class CardDavBackendTest extends TestCase {
/** @var CardDavBackend */
@@ -31,12 +38,20 @@ class CardDavBackendTest extends TestCase {
const UNIT_TEST_USER = 'carddav-unit-test';
-
public function setUp() {
parent::setUp();
+ $principal = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Principal')
+ ->disableOriginalConstructor()
+ ->setMethods(['getPrincipalByPath'])
+ ->getMock();
+ $principal->method('getPrincipalByPath')
+ ->willReturn([
+ 'uri' => 'principals/best-friend'
+ ]);
+
$db = \OC::$server->getDatabaseConnection();
- $this->backend = new CardDavBackend($db);
+ $this->backend = new CardDavBackend($db, $principal);
$this->tearDown();
}
@@ -178,4 +193,32 @@ class CardDavBackendTest extends TestCase {
$changes = $this->backend->getChangesForAddressBook($bookId, $syncToken, 1);
$this->assertEquals($uri0, $changes['added'][0]);
}
+
+ public function testSharing() {
+ $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($books));
+
+ $this->backend->updateShares('Example', [['href' => 'principal:principals/best-friend']], []);
+
+ $shares = $this->backend->getShares('Example');
+ $this->assertEquals(1, count($shares));
+
+ // adding the same sharee again has no effect
+ $this->backend->updateShares('Example', [['href' => 'principal:principals/best-friend']], []);
+
+ $shares = $this->backend->getShares('Example');
+ $this->assertEquals(1, count($shares));
+
+ $books = $this->backend->getAddressBooksForUser('principals/best-friend');
+ $this->assertEquals(1, count($books));
+
+ $this->backend->updateShares('Example', [], [['href' => 'principal:principals/best-friend']]);
+
+ $shares = $this->backend->getShares('Example');
+ $this->assertEquals(0, count($shares));
+
+ $books = $this->backend->getAddressBooksForUser('principals/best-friend');
+ $this->assertEquals(0, count($books));
+ }
}
diff --git a/apps/dav/tests/unit/carddav/convertertest.php b/apps/dav/tests/unit/carddav/convertertest.php
new file mode 100644
index 0000000000..f4e2ea3f00
--- /dev/null
+++ b/apps/dav/tests/unit/carddav/convertertest.php
@@ -0,0 +1,136 @@
+
+ *
+ * @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
+ *
+ */
+
+namespace OCA\DAV\Tests\Unit;
+
+use OCA\DAV\CardDAV\Converter;
+use Test\TestCase;
+
+class ConverterTests extends TestCase {
+
+ /**
+ * @dataProvider providesNewUsers
+ */
+ public function testCreation($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null) {
+ $user = $this->getUserMock($displayName, $eMailAddress, $cloudId);
+
+ $converter = new Converter();
+ $vCard = $converter->createCardFromUser($user);
+ $cardData = $vCard->serialize();
+
+ $this->assertEquals($expectedVCard, $cardData);
+ }
+
+ public function providesNewUsers() {
+ return [
+ ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.7//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nPHOTO;ENCODING=b;TYPE=JPEG:MTIzNDU2Nzg5\r\nEND:VCARD\r\n"],
+ ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.7//EN\r\nUID:12345\r\nFN:Dr. Foo Bar\r\nN:Bar;Dr.;Foo;;\r\nPHOTO;ENCODING=b;TYPE=JPEG:MTIzNDU2Nzg5\r\nEND:VCARD\r\n", "Dr. Foo Bar"],
+ ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.7//EN\r\nUID:12345\r\nFN:Dr. Foo Bar\r\nN:Bar;Dr.;Foo;;\r\nEMAIL;TYPE=OTHER:foo@bar.net\r\nPHOTO;ENCODING=b;TYPE=JPEG:MTIzNDU2Nzg5\r\nEND:VCARD\r\n", "Dr. Foo Bar", "foo@bar.net"],
+ ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.7//EN\r\nUID:12345\r\nFN:Dr. Foo Bar\r\nN:Bar;Dr.;Foo;;\r\nCLOUD:foo@bar.net\r\nPHOTO;ENCODING=b;TYPE=JPEG:MTIzNDU2Nzg5\r\nEND:VCARD\r\n", "Dr. Foo Bar", null, "foo@bar.net"],
+ ];
+ }
+
+ /**
+ * @dataProvider providesNewUsers
+ */
+ public function testUpdateOfUnchangedUser($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null) {
+ $user = $this->getUserMock($displayName, $eMailAddress, $cloudId);
+
+ $converter = new Converter();
+ $vCard = $converter->createCardFromUser($user);
+ $updated = $converter->updateCard($vCard, $user);
+ $this->assertFalse($updated);
+ $cardData = $vCard->serialize();
+
+ $this->assertEquals($expectedVCard, $cardData);
+ }
+
+ /**
+ * @dataProvider providesUsersForUpdateOfRemovedElement
+ */
+ public function testUpdateOfRemovedElement($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null) {
+ $user = $this->getUserMock($displayName, $eMailAddress, $cloudId);
+
+ $converter = new Converter();
+ $vCard = $converter->createCardFromUser($user);
+
+ $user1 = $this->getMockBuilder('OCP\IUser')->disableOriginalConstructor()->getMock();
+ $user1->method('getUID')->willReturn('12345');
+ $user1->method('getDisplayName')->willReturn(null);
+ $user1->method('getEMailAddress')->willReturn(null);
+ $user1->method('getCloudId')->willReturn(null);
+ $user1->method('getAvatarImage')->willReturn(null);
+
+ $updated = $converter->updateCard($vCard, $user1);
+ $this->assertTrue($updated);
+ $cardData = $vCard->serialize();
+
+ $this->assertEquals($expectedVCard, $cardData);
+ }
+
+ public function providesUsersForUpdateOfRemovedElement() {
+ return [
+ ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.7//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", "Dr. Foo Bar"],
+ ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.7//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", "Dr. Foo Bar", "foo@bar.net"],
+ ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.7//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", "Dr. Foo Bar", null, "foo@bar.net"],
+ ];
+ }
+
+ /**
+ * @dataProvider providesNames
+ * @param $expected
+ * @param $fullName
+ */
+ public function testNameSplitter($expected, $fullName) {
+
+ $converter = new Converter();
+ $r = $converter->splitFullName($fullName);
+ $r = implode(';', $r);
+ $this->assertEquals($expected, $r);
+ }
+
+ public function providesNames() {
+ return [
+ ['Sauron;;;;', 'Sauron'],
+ ['Baggins;Bilbo;;;', 'Bilbo Baggins'],
+ ['Tolkien;John;Ronald Reuel;;', 'John Ronald Reuel Tolkien'],
+ ];
+ }
+
+ /**
+ * @param $displayName
+ * @param $eMailAddress
+ * @param $cloudId
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ protected function getUserMock($displayName, $eMailAddress, $cloudId) {
+ $image0 = $this->getMockBuilder('OCP\IImage')->disableOriginalConstructor()->getMock();
+ $image0->method('mimeType')->willReturn('JPEG');
+ $image0->method('data')->willReturn('123456789');
+ $user = $this->getMockBuilder('OCP\IUser')->disableOriginalConstructor()->getMock();
+ $user->method('getUID')->willReturn('12345');
+ $user->method('getDisplayName')->willReturn($displayName);
+ $user->method('getEMailAddress')->willReturn($eMailAddress);
+ $user->method('getCloudId')->willReturn($cloudId);
+ $user->method('getAvatarImage')->willReturn($image0);
+ return $user;
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/FakeLockerPluginTest.php b/apps/dav/tests/unit/connector/sabre/FakeLockerPluginTest.php
new file mode 100644
index 0000000000..8539e9c06e
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/FakeLockerPluginTest.php
@@ -0,0 +1,173 @@
+
+ *
+ * @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
+ *
+ */
+namespace OCA\DAV\Tests\Unit\Connector\Sabre;
+
+use OCA\DAV\Connector\Sabre\FakeLockerPlugin;
+use Sabre\HTTP\Response;
+use Test\TestCase;
+
+/**
+ * Class FakeLockerPluginTest
+ *
+ * @package OCA\DAV\Tests\Unit\Connector\Sabre
+ */
+class FakeLockerPluginTest extends TestCase {
+ /** @var FakeLockerPlugin */
+ private $fakeLockerPlugin;
+
+ public function setUp() {
+ parent::setUp();
+ $this->fakeLockerPlugin = new FakeLockerPlugin();
+ }
+
+ public function testInitialize() {
+ /** @var \Sabre\DAV\Server $server */
+ $server = $this->getMock('\Sabre\DAV\Server');
+ $server
+ ->expects($this->at(0))
+ ->method('on')
+ ->with('method:LOCK', [$this->fakeLockerPlugin, 'fakeLockProvider'], 1);
+ $server
+ ->expects($this->at(1))
+ ->method('on')
+ ->with('method:UNLOCK', [$this->fakeLockerPlugin, 'fakeUnlockProvider'], 1);
+ $server
+ ->expects($this->at(2))
+ ->method('on')
+ ->with('propFind', [$this->fakeLockerPlugin, 'propFind']);
+ $server
+ ->expects($this->at(3))
+ ->method('on')
+ ->with('validateTokens', [$this->fakeLockerPlugin, 'validateTokens']);
+
+ $this->fakeLockerPlugin->initialize($server);
+ }
+
+ public function testGetHTTPMethods() {
+ $expected = [
+ 'LOCK',
+ 'UNLOCK',
+ ];
+ $this->assertSame($expected, $this->fakeLockerPlugin->getHTTPMethods('Test'));
+ }
+
+ public function testGetFeatures() {
+ $expected = [
+ 2,
+ ];
+ $this->assertSame($expected, $this->fakeLockerPlugin->getFeatures());
+ }
+
+ public function testPropFind() {
+ $propFind = $this->getMockBuilder('\Sabre\DAV\PropFind')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $node = $this->getMock('\Sabre\DAV\INode');
+
+ $propFind->expects($this->at(0))
+ ->method('handle')
+ ->with('{DAV:}supportedlock');
+ $propFind->expects($this->at(1))
+ ->method('handle')
+ ->with('{DAV:}lockdiscovery');
+
+ $this->fakeLockerPlugin->propFind($propFind, $node);
+ }
+
+ public function tokenDataProvider() {
+ return [
+ [
+ [
+ [
+ 'tokens' => [
+ [
+ 'token' => 'aToken',
+ 'validToken' => false,
+ ],
+ [],
+ [
+ 'token' => 'opaquelocktoken:asdf',
+ 'validToken' => false,
+ ]
+ ],
+ ]
+ ],
+ [
+ [
+ 'tokens' => [
+ [
+ 'token' => 'aToken',
+ 'validToken' => false,
+ ],
+ [],
+ [
+ 'token' => 'opaquelocktoken:asdf',
+ 'validToken' => true,
+ ]
+ ],
+ ]
+ ],
+ ]
+ ];
+ }
+
+ /**
+ * @dataProvider tokenDataProvider
+ * @param array $input
+ * @param array $expected
+ */
+ public function testValidateTokens(array $input, array $expected) {
+ $request = $this->getMock('\Sabre\HTTP\RequestInterface');
+ $this->fakeLockerPlugin->validateTokens($request, $input);
+ $this->assertSame($expected, $input);
+ }
+
+ public function testFakeLockProvider() {
+ $request = $this->getMock('\Sabre\HTTP\RequestInterface');
+ $response = new Response();
+ $server = $this->getMock('\Sabre\DAV\Server');
+ $this->fakeLockerPlugin->initialize($server);
+
+ $request->expects($this->exactly(2))
+ ->method('getPath')
+ ->will($this->returnValue('MyPath'));
+
+ $this->assertSame(false, $this->fakeLockerPlugin->fakeLockProvider($request, $response));
+
+ $expectedXml = 'MyPathinfinitySecond-1800opaquelocktoken:fe4f7f2437b151fbcb4e9f5c8118c6b1';
+
+ $this->assertXmlStringEqualsXmlString($expectedXml, $response->getBody());
+ }
+
+ public function testFakeUnlockProvider() {
+ $request = $this->getMock('\Sabre\HTTP\RequestInterface');
+ $response = $this->getMock('\Sabre\HTTP\ResponseInterface');
+
+ $response->expects($this->once())
+ ->method('setStatus')
+ ->with('204');
+ $response->expects($this->once())
+ ->method('setHeader')
+ ->with('Content-Length', '0');
+
+ $this->assertSame(false, $this->fakeLockerPlugin->fakeUnlockProvider($request, $response));
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/auth.php b/apps/dav/tests/unit/connector/sabre/auth.php
index d18747d732..217ff5fc3f 100644
--- a/apps/dav/tests/unit/connector/sabre/auth.php
+++ b/apps/dav/tests/unit/connector/sabre/auth.php
@@ -249,9 +249,12 @@ class Auth extends TestCase {
}
public function testAuthenticateAlreadyLoggedIn() {
- $server = $this->getMockBuilder('\Sabre\DAV\Server')
- ->disableOriginalConstructor()
- ->getMock();
+ $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')
+ ->disableOriginalConstructor()
+ ->getMock();
$this->userSession
->expects($this->once())
->method('isLoggedIn')
@@ -275,13 +278,10 @@ class Auth extends TestCase {
->expects($this->once())
->method('close');
- $this->assertTrue($this->auth->authenticate($server, 'TestRealm'));
+ $response = $this->auth->check($request, $response);
+ $this->assertEquals([true, 'principals/users/MyWrongDavUser'], $response);
}
- /**
- * @expectedException \Sabre\DAV\Exception\NotAuthenticated
- * @expectedExceptionMessage No basic authentication headers were found
- */
public function testAuthenticateNoBasicAuthenticateHeadersProvided() {
$server = $this->getMockBuilder('\Sabre\DAV\Server')
->disableOriginalConstructor()
@@ -292,7 +292,59 @@ class Auth extends TestCase {
$server->httpResponse = $this->getMockBuilder('\Sabre\HTTP\ResponseInterface')
->disableOriginalConstructor()
->getMock();
- $this->auth->authenticate($server, 'TestRealm');
+ $response = $this->auth->check($server->httpRequest, $server->httpResponse);
+ $this->assertEquals([false, 'No \'Authorization: Basic\' header found. Either the client didn\'t send one, or the server is mis-configured'], $response);
+ }
+
+ /**
+ * @expectedException \Sabre\DAV\Exception\NotAuthenticated
+ * @expectedExceptionMessage Cannot authenticate over ajax calls
+ */
+ public function testAuthenticateNoBasicAuthenticateHeadersProvidedWithAjax() {
+ /** @var \Sabre\HTTP\RequestInterface $httpRequest */
+ $httpRequest = $this->getMockBuilder('\Sabre\HTTP\RequestInterface')
+ ->disableOriginalConstructor()
+ ->getMock();
+ /** @var \Sabre\HTTP\ResponseInterface $httpResponse */
+ $httpResponse = $this->getMockBuilder('\Sabre\HTTP\ResponseInterface')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->userSession
+ ->expects($this->any())
+ ->method('isLoggedIn')
+ ->will($this->returnValue(false));
+ $httpRequest
+ ->expects($this->once())
+ ->method('getHeader')
+ ->with('X-Requested-With')
+ ->will($this->returnValue('XMLHttpRequest'));
+ $this->auth->check($httpRequest, $httpResponse);
+ }
+
+ public function testAuthenticateNoBasicAuthenticateHeadersProvidedWithAjaxButUserIsStillLoggedIn() {
+ /** @var \Sabre\HTTP\RequestInterface $httpRequest */
+ $httpRequest = $this->getMockBuilder('\Sabre\HTTP\RequestInterface')
+ ->disableOriginalConstructor()
+ ->getMock();
+ /** @var \Sabre\HTTP\ResponseInterface $httpResponse */
+ $httpResponse = $this->getMockBuilder('\Sabre\HTTP\ResponseInterface')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->userSession
+ ->expects($this->any())
+ ->method('isLoggedIn')
+ ->will($this->returnValue(true));
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('AUTHENTICATED_TO_DAV_BACKEND')
+ ->will($this->returnValue('MyTestUser'));
+ $httpRequest
+ ->expects($this->once())
+ ->method('getHeader')
+ ->with('Authorization')
+ ->will($this->returnValue(null));
+ $this->auth->check($httpRequest, $httpResponse);
}
public function testAuthenticateValidCredentials() {
@@ -303,7 +355,12 @@ class Auth extends TestCase {
->disableOriginalConstructor()
->getMock();
$server->httpRequest
- ->expects($this->once())
+ ->expects($this->at(0))
+ ->method('getHeader')
+ ->with('X-Requested-With')
+ ->will($this->returnValue(null));
+ $server->httpRequest
+ ->expects($this->at(1))
->method('getHeader')
->with('Authorization')
->will($this->returnValue('basic dXNlcm5hbWU6cGFzc3dvcmQ='));
@@ -325,13 +382,10 @@ class Auth extends TestCase {
->expects($this->exactly(2))
->method('getUser')
->will($this->returnValue($user));
- $this->assertTrue($this->auth->authenticate($server, 'TestRealm'));
+ $response = $this->auth->check($server->httpRequest, $server->httpResponse);
+ $this->assertEquals([true, 'principals/users/username'], $response);
}
- /**
- * @expectedException \Sabre\DAV\Exception\NotAuthenticated
- * @expectedExceptionMessage Username or password does not match
- */
public function testAuthenticateInvalidCredentials() {
$server = $this->getMockBuilder('\Sabre\DAV\Server')
->disableOriginalConstructor()
@@ -340,7 +394,12 @@ class Auth extends TestCase {
->disableOriginalConstructor()
->getMock();
$server->httpRequest
- ->expects($this->once())
+ ->expects($this->at(0))
+ ->method('getHeader')
+ ->with('X-Requested-With')
+ ->will($this->returnValue(null));
+ $server->httpRequest
+ ->expects($this->at(1))
->method('getHeader')
->with('Authorization')
->will($this->returnValue('basic dXNlcm5hbWU6cGFzc3dvcmQ='));
@@ -352,6 +411,7 @@ class Auth extends TestCase {
->method('login')
->with('username', 'password')
->will($this->returnValue(false));
- $this->auth->authenticate($server, 'TestRealm');
+ $response = $this->auth->check($server->httpRequest, $server->httpResponse);
+ $this->assertEquals([false, 'Username or password was incorrect'], $response);
}
}
diff --git a/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php b/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php
index e1bcc99690..1a973a28ed 100644
--- a/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php
+++ b/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php
@@ -8,6 +8,14 @@ namespace OCA\DAV\Tests\Unit\Connector\Sabre;
* later.
* See the COPYING-README file.
*/
+
+/**
+ * Class CustomPropertiesBackend
+ *
+ * @group DB
+ *
+ * @package Tests\Connector\Sabre
+ */
class CustomPropertiesBackend extends \Test\TestCase {
/**
diff --git a/apps/dav/tests/unit/connector/sabre/directory.php b/apps/dav/tests/unit/connector/sabre/directory.php
index 148a91d26d..75c4828641 100644
--- a/apps/dav/tests/unit/connector/sabre/directory.php
+++ b/apps/dav/tests/unit/connector/sabre/directory.php
@@ -9,6 +9,8 @@
namespace OCA\DAV\Tests\Unit\Connector\Sabre;
+use OCP\Files\ForbiddenException;
+
class Directory extends \Test\TestCase {
/** @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject */
@@ -48,6 +50,25 @@ class Directory extends \Test\TestCase {
$dir->delete();
}
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ */
+ public function testDeleteForbidden() {
+ // deletion allowed
+ $this->info->expects($this->once())
+ ->method('isDeletable')
+ ->will($this->returnValue(true));
+
+ // but fails
+ $this->view->expects($this->once())
+ ->method('rmdir')
+ ->with('sub')
+ ->willThrowException(new ForbiddenException('', true));
+
+ $dir = $this->getDir('sub');
+ $dir->delete();
+ }
+
/**
*
*/
diff --git a/apps/dav/tests/unit/connector/sabre/exception/forbiddentest.php b/apps/dav/tests/unit/connector/sabre/exception/forbiddentest.php
new file mode 100644
index 0000000000..19799c71b9
--- /dev/null
+++ b/apps/dav/tests/unit/connector/sabre/exception/forbiddentest.php
@@ -0,0 +1,44 @@
+
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+namespace OCA\DAV\Tests\Unit\Connector\Sabre\Exception;
+
+use OCA\DAV\Connector\Sabre\Exception\Forbidden;
+
+class ForbiddenTest extends \Test\TestCase {
+
+ public function testSerialization() {
+
+ // create xml doc
+ $DOM = new \DOMDocument('1.0','utf-8');
+ $DOM->formatOutput = true;
+ $error = $DOM->createElementNS('DAV:','d:error');
+ $error->setAttribute('xmlns:s', \Sabre\DAV\Server::NS_SABREDAV);
+ $DOM->appendChild($error);
+
+ // serialize the exception
+ $message = "1234567890";
+ $retry = false;
+ $expectedXml = <<
+
+ false
+ 1234567890
+
+
+EOD;
+
+ $ex = new Forbidden($message, $retry);
+ $server = $this->getMock('Sabre\DAV\Server');
+ $ex->serialize($server, $error);
+
+ // assert
+ $xml = $DOM->saveXML();
+ $this->assertEquals($expectedXml, $xml);
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php b/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php
index 19e82320d5..4296a4d561 100644
--- a/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php
+++ b/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php
@@ -1,15 +1,15 @@
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/
+
+namespace OCA\DAV\Tests\Unit\Connector\Sabre\Exception;
+
+use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+
class InvalidPathTest extends \Test\TestCase {
public function testSerialization() {
diff --git a/apps/dav/tests/unit/connector/sabre/file.php b/apps/dav/tests/unit/connector/sabre/file.php
index 94dadf88fe..2a6cf46ef1 100644
--- a/apps/dav/tests/unit/connector/sabre/file.php
+++ b/apps/dav/tests/unit/connector/sabre/file.php
@@ -9,10 +9,18 @@
namespace OCA\DAV\Tests\Unit\Connector\Sabre;
use OC\Files\Storage\Local;
+use OCP\Files\ForbiddenException;
use Test\HookHelper;
use OC\Files\Filesystem;
use OCP\Lock\ILockingProvider;
+/**
+ * Class File
+ *
+ * @group DB
+ *
+ * @package Test\Connector\Sabre
+ */
class File extends \Test\TestCase {
/**
@@ -40,6 +48,9 @@ class File extends \Test\TestCase {
parent::tearDown();
}
+ /**
+ * @param string $string
+ */
private function getStream($string) {
$stream = fopen('php://temp', 'r+');
fwrite($stream, $string);
@@ -72,6 +83,10 @@ class File extends \Test\TestCase {
new \OCP\Files\InvalidPathException(),
'Sabre\DAV\Exception\Forbidden'
],
+ [
+ new \OCP\Files\ForbiddenException('', true),
+ 'OCA\DAV\Connector\Sabre\Exception\Forbidden'
+ ],
[
new \OCP\Files\LockNotAcquiredException('/test.txt', 1),
'OCA\DAV\Connector\Sabre\Exception\FileLocked'
@@ -234,7 +249,7 @@ class File extends \Test\TestCase {
* @param string $path path to put the file into
* @param string $viewRoot root to use for the view
*
- * @return result of the PUT operaiton which is usually the etag
+ * @return null|string of the PUT operaiton which is usually the etag
*/
private function doPut($path, $viewRoot = null) {
$view = \OC\Files\Filesystem::getView();
@@ -690,6 +705,29 @@ class File extends \Test\TestCase {
$file->delete();
}
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ */
+ public function testDeleteThrowsWhenDeletionThrows() {
+ // setup
+ $view = $this->getMock('\OC\Files\View',
+ array());
+
+ // but fails
+ $view->expects($this->once())
+ ->method('unlink')
+ ->willThrowException(new ForbiddenException('', true));
+
+ $info = new \OC\Files\FileInfo('/test.txt', null, null, array(
+ 'permissions' => \OCP\Constants::PERMISSION_ALL
+ ), null);
+
+ $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+
+ // action
+ $file->delete();
+ }
+
/**
* Asserts hook call
*
@@ -835,4 +873,22 @@ class File extends \Test\TestCase {
$file->get();
}
+
+ /**
+ * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ */
+ public function testGetFopenThrows() {
+ $view = $this->getMock('\OC\Files\View', ['fopen'], array());
+ $view->expects($this->atLeastOnce())
+ ->method('fopen')
+ ->willThrowException(new ForbiddenException('', true));
+
+ $info = new \OC\Files\FileInfo('/test.txt', null, null, array(
+ 'permissions' => \OCP\Constants::PERMISSION_ALL
+ ), null);
+
+ $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
+
+ $file->get();
+ }
}
diff --git a/apps/dav/tests/unit/connector/sabre/filesplugin.php b/apps/dav/tests/unit/connector/sabre/filesplugin.php
index f3c862941c..b33c8340f7 100644
--- a/apps/dav/tests/unit/connector/sabre/filesplugin.php
+++ b/apps/dav/tests/unit/connector/sabre/filesplugin.php
@@ -11,10 +11,13 @@ namespace OCA\DAV\Tests\Unit\Connector\Sabre;
class FilesPlugin extends \Test\TestCase {
const GETETAG_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::GETETAG_PROPERTYNAME;
const FILEID_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::FILEID_PROPERTYNAME;
+ const INTERNAL_FILEID_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::INTERNAL_FILEID_PROPERTYNAME;
const SIZE_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::SIZE_PROPERTYNAME;
const PERMISSIONS_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::PERMISSIONS_PROPERTYNAME;
const LASTMODIFIED_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::LASTMODIFIED_PROPERTYNAME;
const DOWNLOADURL_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::DOWNLOADURL_PROPERTYNAME;
+ const OWNER_ID_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::OWNER_ID_PROPERTYNAME;
+ const OWNER_DISPLAY_NAME_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME;
/**
* @var \Sabre\DAV\Server
@@ -52,6 +55,9 @@ class FilesPlugin extends \Test\TestCase {
$this->plugin->initialize($this->server);
}
+ /**
+ * @param string $class
+ */
private function createTestNode($class) {
$node = $this->getMockBuilder($class)
->disableOriginalConstructor()
@@ -67,7 +73,10 @@ class FilesPlugin extends \Test\TestCase {
$node->expects($this->any())
->method('getFileId')
- ->will($this->returnValue(123));
+ ->will($this->returnValue('00000123instanceid'));
+ $node->expects($this->any())
+ ->method('getInternalFileId')
+ ->will($this->returnValue('123'));
$node->expects($this->any())
->method('getEtag')
->will($this->returnValue('"abc"'));
@@ -88,16 +97,33 @@ class FilesPlugin extends \Test\TestCase {
array(
self::GETETAG_PROPERTYNAME,
self::FILEID_PROPERTYNAME,
+ self::INTERNAL_FILEID_PROPERTYNAME,
self::SIZE_PROPERTYNAME,
self::PERMISSIONS_PROPERTYNAME,
self::DOWNLOADURL_PROPERTYNAME,
+ self::OWNER_ID_PROPERTYNAME,
+ self::OWNER_DISPLAY_NAME_PROPERTYNAME
),
0
);
+ $user = $this->getMockBuilder('\OC\User\User')
+ ->disableOriginalConstructor()->getMock();
+ $user
+ ->expects($this->once())
+ ->method('getUID')
+ ->will($this->returnValue('foo'));
+ $user
+ ->expects($this->once())
+ ->method('getDisplayName')
+ ->will($this->returnValue('M. Foo'));
+
$node->expects($this->once())
->method('getDirectDownload')
->will($this->returnValue(array('url' => 'http://example.com/')));
+ $node->expects($this->exactly(2))
+ ->method('getOwner')
+ ->will($this->returnValue($user));
$node->expects($this->never())
->method('getSize');
@@ -107,10 +133,13 @@ class FilesPlugin extends \Test\TestCase {
);
$this->assertEquals('"abc"', $propFind->get(self::GETETAG_PROPERTYNAME));
- $this->assertEquals(123, $propFind->get(self::FILEID_PROPERTYNAME));
+ $this->assertEquals('00000123instanceid', $propFind->get(self::FILEID_PROPERTYNAME));
+ $this->assertEquals('123', $propFind->get(self::INTERNAL_FILEID_PROPERTYNAME));
$this->assertEquals(null, $propFind->get(self::SIZE_PROPERTYNAME));
$this->assertEquals('DWCKMSR', $propFind->get(self::PERMISSIONS_PROPERTYNAME));
$this->assertEquals('http://example.com/', $propFind->get(self::DOWNLOADURL_PROPERTYNAME));
+ $this->assertEquals('foo', $propFind->get(self::OWNER_ID_PROPERTYNAME));
+ $this->assertEquals('M. Foo', $propFind->get(self::OWNER_DISPLAY_NAME_PROPERTYNAME));
$this->assertEquals(array(self::SIZE_PROPERTYNAME), $propFind->get404Properties());
}
@@ -166,7 +195,7 @@ class FilesPlugin extends \Test\TestCase {
);
$this->assertEquals('"abc"', $propFind->get(self::GETETAG_PROPERTYNAME));
- $this->assertEquals(123, $propFind->get(self::FILEID_PROPERTYNAME));
+ $this->assertEquals('00000123instanceid', $propFind->get(self::FILEID_PROPERTYNAME));
$this->assertEquals(1025, $propFind->get(self::SIZE_PROPERTYNAME));
$this->assertEquals('DWCKMSR', $propFind->get(self::PERMISSIONS_PROPERTYNAME));
$this->assertEquals(null, $propFind->get(self::DOWNLOADURL_PROPERTYNAME));
@@ -207,6 +236,36 @@ class FilesPlugin extends \Test\TestCase {
$this->assertEquals(200, $result[self::GETETAG_PROPERTYNAME]);
}
+ public function testUpdatePropsForbidden() {
+ $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
+
+ $propPatch = new \Sabre\DAV\PropPatch(array(
+ self::OWNER_ID_PROPERTYNAME => 'user2',
+ self::OWNER_DISPLAY_NAME_PROPERTYNAME => 'User Two',
+ self::FILEID_PROPERTYNAME => 12345,
+ self::PERMISSIONS_PROPERTYNAME => 'C',
+ self::SIZE_PROPERTYNAME => 123,
+ self::DOWNLOADURL_PROPERTYNAME => 'http://example.com/',
+ ));
+
+ $this->plugin->handleUpdateProperties(
+ '/dummypath',
+ $propPatch
+ );
+
+ $propPatch->commit();
+
+ $this->assertEmpty($propPatch->getRemainingMutations());
+
+ $result = $propPatch->getResult();
+ $this->assertEquals(403, $result[self::OWNER_ID_PROPERTYNAME]);
+ $this->assertEquals(403, $result[self::OWNER_DISPLAY_NAME_PROPERTYNAME]);
+ $this->assertEquals(403, $result[self::FILEID_PROPERTYNAME]);
+ $this->assertEquals(403, $result[self::PERMISSIONS_PROPERTYNAME]);
+ $this->assertEquals(403, $result[self::SIZE_PROPERTYNAME]);
+ $this->assertEquals(403, $result[self::DOWNLOADURL_PROPERTYNAME]);
+ }
+
/**
* Testcase from https://github.com/owncloud/core/issues/5251
*
diff --git a/apps/dav/tests/unit/connector/sabre/objecttree.php b/apps/dav/tests/unit/connector/sabre/objecttree.php
index 3a56404e55..1cea4ff0b6 100644
--- a/apps/dav/tests/unit/connector/sabre/objecttree.php
+++ b/apps/dav/tests/unit/connector/sabre/objecttree.php
@@ -41,6 +41,13 @@ class TestDoubleFileView extends \OC\Files\View {
}
}
+/**
+ * Class ObjectTree
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\Unit\Connector\Sabre
+ */
class ObjectTree extends \Test\TestCase {
/**
diff --git a/apps/dav/tests/unit/connector/sabre/principal.php b/apps/dav/tests/unit/connector/sabre/principal.php
index 2fbab124fb..9a6ae545b5 100644
--- a/apps/dav/tests/unit/connector/sabre/principal.php
+++ b/apps/dav/tests/unit/connector/sabre/principal.php
@@ -41,43 +41,45 @@ class Principal extends \Test\TestCase {
$fooUser = $this->getMockBuilder('\OC\User\User')
->disableOriginalConstructor()->getMock();
$fooUser
- ->expects($this->exactly(3))
- ->method('getUID')
- ->will($this->returnValue('foo'));
+ ->expects($this->exactly(1))
+ ->method('getUID')
+ ->will($this->returnValue('foo'));
+ $fooUser
+ ->expects($this->exactly(1))
+ ->method('getDisplayName')
+ ->will($this->returnValue('Dr. Foo-Bar'));
+ $fooUser
+ ->expects($this->exactly(1))
+ ->method('getEMailAddress')
+ ->will($this->returnValue(''));
$barUser = $this->getMockBuilder('\OC\User\User')
->disableOriginalConstructor()->getMock();
$barUser
- ->expects($this->exactly(3))
+ ->expects($this->exactly(1))
->method('getUID')
->will($this->returnValue('bar'));
+ $barUser
+ ->expects($this->exactly(1))
+ ->method('getEMailAddress')
+ ->will($this->returnValue('bar@owncloud.org'));
$this->userManager
->expects($this->once())
->method('search')
->with('')
->will($this->returnValue([$fooUser, $barUser]));
- $this->config
- ->expects($this->at(0))
- ->method('getUserValue')
- ->with('foo', 'settings', 'email')
- ->will($this->returnValue(''));
- $this->config
- ->expects($this->at(1))
- ->method('getUserValue')
- ->with('bar', 'settings', 'email')
- ->will($this->returnValue('bar@owncloud.org'));
$expectedResponse = [
0 => [
- 'uri' => 'principals/foo',
- '{DAV:}displayname' => 'foo'
+ 'uri' => 'principals/users/foo',
+ '{DAV:}displayname' => 'Dr. Foo-Bar'
],
1 => [
- 'uri' => 'principals/bar',
+ 'uri' => 'principals/users/bar',
'{DAV:}displayname' => 'bar',
'{http://sabredav.org/ns}email-address' => 'bar@owncloud.org'
]
];
- $response = $this->connector->getPrincipalsByPrefix('principals');
+ $response = $this->connector->getPrincipalsByPrefix('principals/users');
$this->assertSame($expectedResponse, $response);
}
@@ -88,7 +90,7 @@ class Principal extends \Test\TestCase {
->with('')
->will($this->returnValue([]));
- $response = $this->connector->getPrincipalsByPrefix('principals');
+ $response = $this->connector->getPrincipalsByPrefix('principals/users');
$this->assertSame([], $response);
}
@@ -96,7 +98,7 @@ class Principal extends \Test\TestCase {
$fooUser = $this->getMockBuilder('\OC\User\User')
->disableOriginalConstructor()->getMock();
$fooUser
- ->expects($this->exactly(3))
+ ->expects($this->exactly(1))
->method('getUID')
->will($this->returnValue('foo'));
$this->userManager
@@ -104,17 +106,12 @@ class Principal extends \Test\TestCase {
->method('get')
->with('foo')
->will($this->returnValue($fooUser));
- $this->config
- ->expects($this->once())
- ->method('getUserValue')
- ->with('foo', 'settings', 'email')
- ->will($this->returnValue(''));
$expectedResponse = [
- 'uri' => 'principals/foo',
+ 'uri' => 'principals/users/foo',
'{DAV:}displayname' => 'foo'
];
- $response = $this->connector->getPrincipalByPath('principals/foo');
+ $response = $this->connector->getPrincipalByPath('principals/users/foo');
$this->assertSame($expectedResponse, $response);
}
@@ -122,26 +119,25 @@ class Principal extends \Test\TestCase {
$fooUser = $this->getMockBuilder('\OC\User\User')
->disableOriginalConstructor()->getMock();
$fooUser
- ->expects($this->exactly(3))
- ->method('getUID')
- ->will($this->returnValue('foo'));
+ ->expects($this->exactly(1))
+ ->method('getEMailAddress')
+ ->will($this->returnValue('foo@owncloud.org'));
+ $fooUser
+ ->expects($this->exactly(1))
+ ->method('getUID')
+ ->will($this->returnValue('foo'));
$this->userManager
->expects($this->once())
->method('get')
->with('foo')
->will($this->returnValue($fooUser));
- $this->config
- ->expects($this->once())
- ->method('getUserValue')
- ->with('foo', 'settings', 'email')
- ->will($this->returnValue('foo@owncloud.org'));
$expectedResponse = [
- 'uri' => 'principals/foo',
+ 'uri' => 'principals/users/foo',
'{DAV:}displayname' => 'foo',
'{http://sabredav.org/ns}email-address' => 'foo@owncloud.org'
];
- $response = $this->connector->getPrincipalByPath('principals/foo');
+ $response = $this->connector->getPrincipalByPath('principals/users/foo');
$this->assertSame($expectedResponse, $response);
}
@@ -152,7 +148,7 @@ class Principal extends \Test\TestCase {
->with('foo')
->will($this->returnValue(null));
- $response = $this->connector->getPrincipalByPath('principals/foo');
+ $response = $this->connector->getPrincipalByPath('principals/users/foo');
$this->assertSame(null, $response);
}
@@ -160,7 +156,7 @@ class Principal extends \Test\TestCase {
$fooUser = $this->getMockBuilder('\OC\User\User')
->disableOriginalConstructor()->getMock();
$fooUser
- ->expects($this->exactly(3))
+ ->expects($this->exactly(1))
->method('getUID')
->will($this->returnValue('foo'));
$this->userManager
@@ -168,14 +164,9 @@ class Principal extends \Test\TestCase {
->method('get')
->with('foo')
->will($this->returnValue($fooUser));
- $this->config
- ->expects($this->once())
- ->method('getUserValue')
- ->with('foo', 'settings', 'email')
- ->will($this->returnValue('foo@owncloud.org'));
- $response = $this->connector->getGroupMemberSet('principals/foo');
- $this->assertSame(['principals/foo'], $response);
+ $response = $this->connector->getGroupMemberSet('principals/users/foo');
+ $this->assertSame(['principals/users/foo'], $response);
}
/**
@@ -189,14 +180,14 @@ class Principal extends \Test\TestCase {
->with('foo')
->will($this->returnValue(null));
- $this->connector->getGroupMemberSet('principals/foo');
+ $this->connector->getGroupMemberSet('principals/users/foo');
}
public function testGetGroupMembership() {
$fooUser = $this->getMockBuilder('\OC\User\User')
->disableOriginalConstructor()->getMock();
$fooUser
- ->expects($this->exactly(3))
+ ->expects($this->exactly(1))
->method('getUID')
->will($this->returnValue('foo'));
$this->userManager
@@ -204,17 +195,12 @@ class Principal extends \Test\TestCase {
->method('get')
->with('foo')
->will($this->returnValue($fooUser));
- $this->config
- ->expects($this->once())
- ->method('getUserValue')
- ->with('foo', 'settings', 'email')
- ->will($this->returnValue('foo@owncloud.org'));
$expectedResponse = [
- 'principals/foo/calendar-proxy-read',
- 'principals/foo/calendar-proxy-write'
+ 'principals/users/foo/calendar-proxy-read',
+ 'principals/users/foo/calendar-proxy-write'
];
- $response = $this->connector->getGroupMembership('principals/foo');
+ $response = $this->connector->getGroupMembership('principals/users/foo');
$this->assertSame($expectedResponse, $response);
}
@@ -229,7 +215,7 @@ class Principal extends \Test\TestCase {
->with('foo')
->will($this->returnValue(null));
- $this->connector->getGroupMembership('principals/foo');
+ $this->connector->getGroupMembership('principals/users/foo');
}
/**
@@ -237,7 +223,7 @@ class Principal extends \Test\TestCase {
* @expectedExceptionMessage Setting members of the group is not supported yet
*/
public function testSetGroupMembership() {
- $this->connector->setGroupMemberSet('principals/foo', ['foo']);
+ $this->connector->setGroupMemberSet('principals/users/foo', ['foo']);
}
public function testUpdatePrincipal() {
@@ -245,6 +231,6 @@ class Principal extends \Test\TestCase {
}
public function testSearchPrincipals() {
- $this->assertSame([], $this->connector->searchPrincipals('principals', []));
+ $this->assertSame([], $this->connector->searchPrincipals('principals/users', []));
}
}
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/auth.php b/apps/dav/tests/unit/connector/sabre/requesttest/auth.php
index 41b554d11d..3caa019af8 100644
--- a/apps/dav/tests/unit/connector/sabre/requesttest/auth.php
+++ b/apps/dav/tests/unit/connector/sabre/requesttest/auth.php
@@ -9,6 +9,8 @@
namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest;
use Sabre\DAV\Auth\Backend\BackendInterface;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
class Auth implements BackendInterface {
/**
@@ -32,18 +34,35 @@ class Auth implements BackendInterface {
$this->password = $password;
}
-
/**
- * Authenticates the user based on the current request.
+ * When this method is called, the backend must check if authentication was
+ * successful.
*
- * If authentication is successful, true must be returned.
- * If authentication fails, an exception must be thrown.
+ * The returned value must be one of the following
*
- * @param \Sabre\DAV\Server $server
- * @param string $realm
- * @return bool
+ * [true, "principals/username"]
+ * [false, "reason for failure"]
+ *
+ * If authentication was successful, it's expected that the authentication
+ * backend returns a so-called principal url.
+ *
+ * Examples of a principal url:
+ *
+ * principals/admin
+ * principals/user1
+ * principals/users/joe
+ * principals/uid/123457
+ *
+ * If you don't use WebDAV ACL (RFC3744) we recommend that you simply
+ * return a string such as:
+ *
+ * principals/users/[username]
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return array
*/
- function authenticate(\Sabre\DAV\Server $server, $realm) {
+ function check(RequestInterface $request, ResponseInterface $response) {
$userSession = \OC::$server->getUserSession();
$result = $userSession->login($this->user, $this->password);
if ($result) {
@@ -52,18 +71,33 @@ class Auth implements BackendInterface {
\OC_Util::setupFS($user);
//trigger creation of user home and /files folder
\OC::$server->getUserFolder($user);
+ return [true, "principals/$user"];
}
- return $result;
+ return [false, "login failed"];
}
/**
- * Returns information about the currently logged in username.
+ * This method is called when a user could not be authenticated, and
+ * authentication was required for the current request.
*
- * If nobody is currently logged in, this method should return null.
+ * This gives you the opportunity to set authentication headers. The 401
+ * status code will already be set.
*
- * @return string|null
+ * In this case of Basic Auth, this would for example mean that the
+ * following header needs to be set:
+ *
+ * $response->addHeader('WWW-Authenticate', 'Basic realm=SabreDAV');
+ *
+ * Keep in mind that in the case of multiple authentication backends, other
+ * WWW-Authenticate headers may already have been set, and you'll want to
+ * append your own WWW-Authenticate header instead of overwriting the
+ * existing one.
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return void
*/
- function getCurrentUser() {
- return $this->user;
+ function challenge(RequestInterface $request, ResponseInterface $response) {
+ // TODO: Implement challenge() method.
}
}
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/downloadtest.php b/apps/dav/tests/unit/connector/sabre/requesttest/downloadtest.php
index 245deff3b3..29454c3821 100644
--- a/apps/dav/tests/unit/connector/sabre/requesttest/downloadtest.php
+++ b/apps/dav/tests/unit/connector/sabre/requesttest/downloadtest.php
@@ -11,6 +11,13 @@ namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest;
use OCP\AppFramework\Http;
use OCP\Lock\ILockingProvider;
+/**
+ * Class DownloadTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest
+ */
class DownloadTest extends RequestTest {
public function testDownload() {
$user = $this->getUniqueID();
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/encryptionuploadtest.php b/apps/dav/tests/unit/connector/sabre/requesttest/encryptionuploadtest.php
index ed1d6046d7..b79dfa0c26 100644
--- a/apps/dav/tests/unit/connector/sabre/requesttest/encryptionuploadtest.php
+++ b/apps/dav/tests/unit/connector/sabre/requesttest/encryptionuploadtest.php
@@ -11,6 +11,13 @@ namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest;
use OC\Files\View;
use Test\Traits\EncryptionTrait;
+/**
+ * Class EncryptionUploadTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest
+ */
class EncryptionUploadTest extends UploadTest {
use EncryptionTrait;
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/requesttest.php b/apps/dav/tests/unit/connector/sabre/requesttest/requesttest.php
index d90cf6e19b..a83f25c158 100644
--- a/apps/dav/tests/unit/connector/sabre/requesttest/requesttest.php
+++ b/apps/dav/tests/unit/connector/sabre/requesttest/requesttest.php
@@ -46,7 +46,8 @@ abstract class RequestTest extends TestCase {
\OC::$server->getUserSession(),
\OC::$server->getMountManager(),
\OC::$server->getTagManager(),
- \OC::$server->getEventDispatcher()
+ \OC::$server->getEventDispatcher(),
+ $this->getMock('\OCP\IRequest')
);
}
@@ -67,6 +68,7 @@ abstract class RequestTest extends TestCase {
* @param resource|string|null $body
* @param array|null $headers
* @return \Sabre\HTTP\Response
+ * @throws \Exception
*/
protected function request($view, $user, $password, $method, $url, $body = null, $headers = null) {
if (is_string($body)) {
diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/uploadtest.php b/apps/dav/tests/unit/connector/sabre/requesttest/uploadtest.php
index a2a8326f4f..c1876a7f29 100644
--- a/apps/dav/tests/unit/connector/sabre/requesttest/uploadtest.php
+++ b/apps/dav/tests/unit/connector/sabre/requesttest/uploadtest.php
@@ -12,6 +12,13 @@ use OC\Connector\Sabre\Exception\FileLocked;
use OCP\AppFramework\Http;
use OCP\Lock\ILockingProvider;
+/**
+ * Class UploadTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest
+ */
class UploadTest extends RequestTest {
public function testBasicUpload() {
$user = $this->getUniqueID();
diff --git a/apps/encryption/hooks/userhooks.php b/apps/encryption/hooks/userhooks.php
index 5bd5e39f3c..4a5f1198fc 100644
--- a/apps/encryption/hooks/userhooks.php
+++ b/apps/encryption/hooks/userhooks.php
@@ -141,7 +141,7 @@ class UserHooks implements IHook {
*
* @note This method should never be called for users using client side encryption
* @param array $params
- * @return bool
+ * @return boolean|null
*/
public function login($params) {
@@ -199,7 +199,7 @@ class UserHooks implements IHook {
* If the password can't be changed within ownCloud, than update the key password in advance.
*
* @param array $params : uid, password
- * @return bool
+ * @return boolean|null
*/
public function preSetPassphrase($params) {
if (App::isEnabled('encryption')) {
@@ -216,7 +216,7 @@ class UserHooks implements IHook {
* Change a user's encryption passphrase
*
* @param array $params keys: uid, password
- * @return bool
+ * @return boolean|null
*/
public function setPassphrase($params) {
diff --git a/apps/encryption/l10n/es_AR.js b/apps/encryption/l10n/es_AR.js
index bff5b7c593..3f2fbb5de3 100644
--- a/apps/encryption/l10n/es_AR.js
+++ b/apps/encryption/l10n/es_AR.js
@@ -1,6 +1,7 @@
OC.L10N.register(
"encryption",
{
+ "Missing recovery key password" : "Falta contraseña de recuperación",
"Recovery key successfully enabled" : "Se habilitó la recuperación de archivos",
"Could not enable recovery key. Please check your recovery key password!" : "No se pudo habilitar la clave de recuperación. Por favor, comprobá tu contraseña.",
"Recovery key successfully disabled" : "Clave de recuperación deshabilitada",
diff --git a/apps/encryption/l10n/es_AR.json b/apps/encryption/l10n/es_AR.json
index 0cdcd9cd12..225ca5139f 100644
--- a/apps/encryption/l10n/es_AR.json
+++ b/apps/encryption/l10n/es_AR.json
@@ -1,4 +1,5 @@
{ "translations": {
+ "Missing recovery key password" : "Falta contraseña de recuperación",
"Recovery key successfully enabled" : "Se habilitó la recuperación de archivos",
"Could not enable recovery key. Please check your recovery key password!" : "No se pudo habilitar la clave de recuperación. Por favor, comprobá tu contraseña.",
"Recovery key successfully disabled" : "Clave de recuperación deshabilitada",
diff --git a/apps/encryption/l10n/tr.js b/apps/encryption/l10n/tr.js
index e1fcb526d0..5abbf87b81 100644
--- a/apps/encryption/l10n/tr.js
+++ b/apps/encryption/l10n/tr.js
@@ -32,6 +32,8 @@ OC.L10N.register(
"The share will expire on %s." : "Bu paylaşım %s tarihinde sona erecek.",
"Cheers!" : "Hoşçakalın!",
"Hey there,
the admin enabled server-side-encryption. Your files were encrypted using the password %s.
Please login to the web interface, go to the section \"ownCloud basic encryption module\" of your personal settings and update your encryption password by entering this password into the \"old log-in password\" field and your current login-password.
" : "Selam,
Sistem yöneticisi sunucu tarafında şifrelemeyi etkinleştirdi. Dosyalarınız %s parolası kullanılarak şifrelendi.
Lütfen web arayüzünde oturum açın ve kişisel ayarlarınızdan 'ownCloud temel şifreleme modülü'ne giderek 'eski oturum parolası' alanına bu parolayı girdikten sonra şifreleme parolanızı ve mevcut oturum açma parolanızı güncelleyin.
",
+ "Encrypt the home storage" : "Yerel depolamayı şifrele",
+ "Enabling this option encrypts all files stored on the main storage, otherwise only files on external storage will be encrypted" : "Bu seçeneği etkinleştirmek ana depolamadaki bütün dosyaları şifreler, aksi takdirde sadece harici depolamadaki dosyalar şifrelenir",
"Enable recovery key" : "Kurtarma anahtarını etkinleştir",
"Disable recovery key" : "Kurtarma anahtarını devre dışı bırak",
"The recovery key is an extra encryption key that is used to encrypt files. It allows recovery of a user's files if the user forgets his or her password." : "Kurtarma anahtarı, dosyaların şifrelenmesi için daha fazla \nşifreleme sunar. Bu kullanıcının dosyasının şifresini unuttuğunda kurtarmasına imkan verir.",
diff --git a/apps/encryption/l10n/tr.json b/apps/encryption/l10n/tr.json
index 743d3e7d15..31824461d8 100644
--- a/apps/encryption/l10n/tr.json
+++ b/apps/encryption/l10n/tr.json
@@ -30,6 +30,8 @@
"The share will expire on %s." : "Bu paylaşım %s tarihinde sona erecek.",
"Cheers!" : "Hoşçakalın!",
"Hey there,
the admin enabled server-side-encryption. Your files were encrypted using the password %s.
Please login to the web interface, go to the section \"ownCloud basic encryption module\" of your personal settings and update your encryption password by entering this password into the \"old log-in password\" field and your current login-password.
" : "Selam,
Sistem yöneticisi sunucu tarafında şifrelemeyi etkinleştirdi. Dosyalarınız %s parolası kullanılarak şifrelendi.
Lütfen web arayüzünde oturum açın ve kişisel ayarlarınızdan 'ownCloud temel şifreleme modülü'ne giderek 'eski oturum parolası' alanına bu parolayı girdikten sonra şifreleme parolanızı ve mevcut oturum açma parolanızı güncelleyin.
",
+ "Encrypt the home storage" : "Yerel depolamayı şifrele",
+ "Enabling this option encrypts all files stored on the main storage, otherwise only files on external storage will be encrypted" : "Bu seçeneği etkinleştirmek ana depolamadaki bütün dosyaları şifreler, aksi takdirde sadece harici depolamadaki dosyalar şifrelenir",
"Enable recovery key" : "Kurtarma anahtarını etkinleştir",
"Disable recovery key" : "Kurtarma anahtarını devre dışı bırak",
"The recovery key is an extra encryption key that is used to encrypt files. It allows recovery of a user's files if the user forgets his or her password." : "Kurtarma anahtarı, dosyaların şifrelenmesi için daha fazla \nşifreleme sunar. Bu kullanıcının dosyasının şifresini unuttuğunda kurtarmasına imkan verir.",
diff --git a/apps/encryption/lib/crypto/crypt.php b/apps/encryption/lib/crypto/crypt.php
index c0dcc936bd..dbc0364a15 100644
--- a/apps/encryption/lib/crypto/crypt.php
+++ b/apps/encryption/lib/crypto/crypt.php
@@ -34,7 +34,6 @@ use OCA\Encryption\Vendor\PBKDF2Fallback;
use OCP\Encryption\Exceptions\GenericEncryptionException;
use OCP\IConfig;
use OCP\ILogger;
-use OCP\IUser;
use OCP\IUserSession;
class Crypt {
@@ -146,7 +145,7 @@ class Crypt {
/**
* @param string $plainContent
* @param string $passPhrase
- * @return bool|string
+ * @return false|string
* @throws GenericEncryptionException
*/
public function symmetricEncryptFileContent($plainContent, $passPhrase) {
@@ -273,7 +272,7 @@ class Crypt {
}
/**
- * @param $data
+ * @param string $data
* @return string
*/
private function addPadding($data) {
@@ -326,7 +325,7 @@ class Crypt {
* @param string $privateKey
* @param string $password
* @param string $uid for regular users, empty for system keys
- * @return bool|string
+ * @return false|string
*/
public function encryptPrivateKey($privateKey, $password, $uid = '') {
$cipher = $this->getCipher();
@@ -343,7 +342,7 @@ class Crypt {
* @param string $privateKey
* @param string $password
* @param string $uid for regular users, empty for system keys
- * @return bool|string
+ * @return false|string
*/
public function decryptPrivateKey($privateKey, $password = '', $uid = '') {
@@ -386,7 +385,7 @@ class Crypt {
/**
* check if it is a valid private key
*
- * @param $plainKey
+ * @param string $plainKey
* @return bool
*/
protected function isValidPrivateKey($plainKey) {
@@ -402,7 +401,7 @@ class Crypt {
}
/**
- * @param $keyFileContents
+ * @param string $keyFileContents
* @param string $passPhrase
* @param string $cipher
* @return string
@@ -424,7 +423,7 @@ class Crypt {
* remove padding
*
* @param $padded
- * @return bool|string
+ * @return string|false
*/
private function removePadding($padded) {
if (substr($padded, -2) === 'xx') {
@@ -436,8 +435,8 @@ class Crypt {
/**
* split iv from encrypted content
*
- * @param $catFile
- * @return array
+ * @param string|false $catFile
+ * @return string
*/
private function splitIv($catFile) {
// Fetch encryption metadata from end of file
@@ -457,8 +456,8 @@ class Crypt {
}
/**
- * @param $encryptedContent
- * @param $iv
+ * @param string $encryptedContent
+ * @param string $iv
* @param string $passPhrase
* @param string $cipher
* @return string
@@ -479,7 +478,7 @@ class Crypt {
}
/**
- * @param $data
+ * @param string $data
* @return array
*/
protected function parseHeader($data) {
@@ -551,7 +550,7 @@ class Crypt {
* @param $encKeyFile
* @param $shareKey
* @param $privateKey
- * @return mixed
+ * @return string
* @throws MultiKeyDecryptException
*/
public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) {
diff --git a/apps/encryption/lib/crypto/encryptall.php b/apps/encryption/lib/crypto/encryptall.php
index 8e97fe341b..ef67523d7e 100644
--- a/apps/encryption/lib/crypto/encryptall.php
+++ b/apps/encryption/lib/crypto/encryptall.php
@@ -31,6 +31,7 @@ use OCP\IL10N;
use OCP\IUserManager;
use OCP\Mail\IMailer;
use OCP\Security\ISecureRandom;
+use OCP\Util;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Helper\Table;
@@ -358,14 +359,15 @@ class EncryptAll {
$progress = new ProgressBar($this->output, count($this->userPasswords));
$progress->start();
- foreach ($this->userPasswords as $recipient => $password) {
+ foreach ($this->userPasswords as $uid => $password) {
$progress->advance();
if (!empty($password)) {
- $recipientDisplayName = $this->userManager->get($recipient)->getDisplayName();
- $to = $this->config->getUserValue($recipient, 'settings', 'email', '');
+ $recipient = $this->userManager->get($uid);
+ $recipientDisplayName = $recipient->getDisplayName();
+ $to = $recipient->getEMailAddress();
if ($to === '') {
- $noMail[] = $recipient;
+ $noMail[] = $uid;
continue;
}
@@ -380,12 +382,12 @@ class EncryptAll {
$message->setHtmlBody($htmlBody);
$message->setPlainBody($textBody);
$message->setFrom([
- \OCP\Util::getDefaultEmailAddress('admin-noreply')
+ Util::getDefaultEmailAddress('admin-noreply')
]);
$this->mailer->send($message);
} catch (\Exception $e) {
- $noMail[] = $recipient;
+ $noMail[] = $uid;
}
}
}
diff --git a/apps/encryption/lib/crypto/encryption.php b/apps/encryption/lib/crypto/encryption.php
index d1140ce7cd..ea6c05c338 100644
--- a/apps/encryption/lib/crypto/encryption.php
+++ b/apps/encryption/lib/crypto/encryption.php
@@ -247,7 +247,7 @@ class Encryption implements IEncryptionModule {
* encrypt data
*
* @param string $data you want to encrypt
- * @return mixed encrypted data
+ * @return string encrypted data
*/
public function encrypt($data) {
@@ -312,7 +312,7 @@ class Encryption implements IEncryptionModule {
* decrypt data
*
* @param string $data you want to decrypt
- * @return mixed decrypted data
+ * @return string decrypted data
* @throws DecryptionFailedException
*/
public function decrypt($data) {
diff --git a/apps/encryption/lib/keymanager.php b/apps/encryption/lib/keymanager.php
index c450722887..0c8418c67a 100644
--- a/apps/encryption/lib/keymanager.php
+++ b/apps/encryption/lib/keymanager.php
@@ -280,7 +280,7 @@ class KeyManager {
/**
* @param $userId
- * @param $key
+ * @param string $key
* @return bool
*/
public function setPrivateKey($userId, $key) {
@@ -365,7 +365,7 @@ class KeyManager {
/**
* @param $userId
- * @return mixed
+ * @return string
* @throws PrivateKeyMissingException
*/
public function getPrivateKey($userId) {
@@ -379,7 +379,7 @@ class KeyManager {
}
/**
- * @param $path
+ * @param string $path
* @param $uid
* @return string
*/
@@ -412,7 +412,7 @@ class KeyManager {
/**
* get the encrypted file key
*
- * @param $path
+ * @param string $path
* @return string
*/
public function getEncryptedFileKey($path) {
@@ -508,7 +508,7 @@ class KeyManager {
}
/**
- * @param $purpose
+ * @param string $purpose
* @param bool $timestamp
* @param bool $includeUserKeys
*/
@@ -534,13 +534,16 @@ class KeyManager {
}
/**
- * @param $uid
+ * @param string $uid
* @return bool
*/
private function deletePrivateKey($uid) {
return $this->keyStorage->deleteUserKey($uid, $this->privateKeyId, Encryption::ID);
}
+ /**
+ * @param string $path
+ */
public function deleteAllFileKeys($path) {
return $this->keyStorage->deleteAllFileKeys($path);
}
diff --git a/apps/encryption/lib/migration.php b/apps/encryption/lib/migration.php
index 1a7c2e9877..7bc399ddcb 100644
--- a/apps/encryption/lib/migration.php
+++ b/apps/encryption/lib/migration.php
@@ -50,7 +50,7 @@ class Migration {
*/
public function __construct(IConfig $config, View $view, IDBConnection $connection, ILogger $logger) {
$this->view = $view;
- $this->view->getUpdater()->disable();
+ $this->view->disableCacheUpdate();
$this->connection = $connection;
$this->moduleId = \OCA\Encryption\Crypto\Encryption::ID;
$this->config = $config;
@@ -237,7 +237,7 @@ class Migration {
/**
* rename system wide public key
*
- * @param $privateKey private key for which we want to rename the corresponding public key
+ * @param string $privateKey private key for which we want to rename the corresponding public key
*/
private function renameSystemPublicKey($privateKey) {
$publicKey = substr($privateKey,0 , strrpos($privateKey, '.privateKey')) . '.publicKey';
diff --git a/apps/encryption/lib/recovery.php b/apps/encryption/lib/recovery.php
index e7b20e2c4a..cffa641f51 100644
--- a/apps/encryption/lib/recovery.php
+++ b/apps/encryption/lib/recovery.php
@@ -103,7 +103,7 @@ class Recovery {
/**
* @param $recoveryKeyId
- * @param $password
+ * @param string $password
* @return bool
*/
public function enableAdminRecovery($password) {
@@ -144,7 +144,7 @@ class Recovery {
}
/**
- * @param $recoveryPassword
+ * @param string $recoveryPassword
* @return bool
*/
public function disableAdminRecovery($recoveryPassword) {
@@ -212,6 +212,7 @@ class Recovery {
/**
* add recovery key to all encrypted files
+ * @param string $path
*/
private function addRecoveryKeys($path) {
$dirContent = $this->view->getDirectoryContent($path);
@@ -239,6 +240,7 @@ class Recovery {
/**
* remove recovery key to all encrypted files
+ * @param string $path
*/
private function removeRecoveryKeys($path) {
$dirContent = $this->view->getDirectoryContent($path);
diff --git a/apps/encryption/tests/lib/MigrationTest.php b/apps/encryption/tests/lib/MigrationTest.php
index 65fefa262a..fc3d014345 100644
--- a/apps/encryption/tests/lib/MigrationTest.php
+++ b/apps/encryption/tests/lib/MigrationTest.php
@@ -62,6 +62,9 @@ class MigrationTest extends \Test\TestCase {
$this->moduleId = \OCA\Encryption\Crypto\Encryption::ID;
}
+ /**
+ * @param string $uid
+ */
protected function createDummyShareKeys($uid) {
$this->loginAsUser($uid);
@@ -89,6 +92,9 @@ class MigrationTest extends \Test\TestCase {
}
}
+ /**
+ * @param string $uid
+ */
protected function createDummyUserKeys($uid) {
$this->loginAsUser($uid);
@@ -98,6 +104,9 @@ class MigrationTest extends \Test\TestCase {
$this->view->file_put_contents('/files_encryption/public_keys/' . $uid . '.publicKey', 'publicKey');
}
+ /**
+ * @param string $uid
+ */
protected function createDummyFileKeys($uid) {
$this->loginAsUser($uid);
@@ -111,6 +120,9 @@ class MigrationTest extends \Test\TestCase {
$this->view->file_put_contents($uid . '/files_encryption/keys/folder2/file.2.1/fileKey' , 'data');
}
+ /**
+ * @param string $uid
+ */
protected function createDummyFiles($uid) {
$this->loginAsUser($uid);
@@ -124,6 +136,9 @@ class MigrationTest extends \Test\TestCase {
$this->view->file_put_contents($uid . '/files/folder2/file.2.1/fileKey' , 'data');
}
+ /**
+ * @param string $uid
+ */
protected function createDummyFilesInTrash($uid) {
$this->loginAsUser($uid);
@@ -239,6 +254,9 @@ class MigrationTest extends \Test\TestCase {
}
+ /**
+ * @param string $uid
+ */
protected function verifyFilesInTrash($uid) {
$this->loginAsUser($uid);
@@ -266,6 +284,9 @@ class MigrationTest extends \Test\TestCase {
);
}
+ /**
+ * @param string $uid
+ */
protected function verifyNewKeyPath($uid) {
// private key
if ($uid !== '') {
@@ -394,6 +415,11 @@ class MigrationTest extends \Test\TestCase {
}
+ /**
+ * @param string $table
+ * @param string $appid
+ * @param integer $expected
+ */
public function verifyDB($table, $appid, $expected) {
/** @var \OCP\IDBConnection $connection */
$connection = \OC::$server->getDatabaseConnection();
diff --git a/apps/federation/api/ocsauthapi.php b/apps/federation/api/ocsauthapi.php
new file mode 100644
index 0000000000..42d7113820
--- /dev/null
+++ b/apps/federation/api/ocsauthapi.php
@@ -0,0 +1,145 @@
+
+ *
+ * @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
+ *
+ */
+
+
+namespace OCA\Federation\API;
+
+use OCA\Federation\DbHandler;
+use OCA\Federation\TrustedServers;
+use OCP\AppFramework\Http;
+use OCP\BackgroundJob\IJobList;
+use OCP\IRequest;
+use OCP\Security\ISecureRandom;
+use OCP\Security\StringUtils;
+
+/**
+ * Class OCSAuthAPI
+ *
+ * OCS API end-points to exchange shared secret between two connected ownClouds
+ *
+ * @package OCA\Federation\API
+ */
+class OCSAuthAPI {
+
+ /** @var IRequest */
+ private $request;
+
+ /** @var ISecureRandom */
+ private $secureRandom;
+
+ /** @var IJobList */
+ private $jobList;
+
+ /** @var TrustedServers */
+ private $trustedServers;
+
+ /** @var DbHandler */
+ private $dbHandler;
+
+ /**
+ * OCSAuthAPI constructor.
+ *
+ * @param IRequest $request
+ * @param ISecureRandom $secureRandom
+ * @param IJobList $jobList
+ * @param TrustedServers $trustedServers
+ * @param DbHandler $dbHandler
+ */
+ public function __construct(
+ IRequest $request,
+ ISecureRandom $secureRandom,
+ IJobList $jobList,
+ TrustedServers $trustedServers,
+ DbHandler $dbHandler
+ ) {
+ $this->request = $request;
+ $this->secureRandom = $secureRandom;
+ $this->jobList = $jobList;
+ $this->trustedServers = $trustedServers;
+ $this->dbHandler = $dbHandler;
+ }
+
+ /**
+ * request received to ask remote server for a shared secret
+ *
+ * @return \OC_OCS_Result
+ */
+ public function requestSharedSecret() {
+
+ $url = $this->request->getParam('url');
+ $token = $this->request->getParam('token');
+
+ if ($this->trustedServers->isTrustedServer($url) === false) {
+ return new \OC_OCS_Result(null, HTTP::STATUS_FORBIDDEN);
+ }
+
+ // if both server initiated the exchange of the shared secret the greater
+ // token wins
+ $localToken = $this->dbHandler->getToken($url);
+ if (strcmp($localToken, $token) > 0) {
+ return new \OC_OCS_Result(null, HTTP::STATUS_FORBIDDEN);
+ }
+
+ $this->jobList->add(
+ 'OCA\Federation\BackgroundJob\GetSharedSecret',
+ [
+ 'url' => $url,
+ 'token' => $token,
+ ]
+ );
+
+ return new \OC_OCS_Result(null, Http::STATUS_OK);
+
+ }
+
+ /**
+ * create shared secret and return it
+ *
+ * @return \OC_OCS_Result
+ */
+ public function getSharedSecret() {
+
+ $url = $this->request->getParam('url');
+ $token = $this->request->getParam('token');
+
+ if (
+ $this->trustedServers->isTrustedServer($url) === false
+ || $this->isValidToken($url, $token) === false
+ ) {
+ return new \OC_OCS_Result(null, HTTP::STATUS_FORBIDDEN);
+ }
+
+ $sharedSecret = $this->secureRandom->getMediumStrengthGenerator()->generate(32);
+
+ $this->trustedServers->addSharedSecret($url, $sharedSecret);
+ // reset token after the exchange of the shared secret was successful
+ $this->dbHandler->addToken($url, '');
+
+ return new \OC_OCS_Result(['sharedSecret' => $sharedSecret], Http::STATUS_OK);
+
+ }
+
+ protected function isValidToken($url, $token) {
+ $storedToken = $this->dbHandler->getToken($url);
+ return StringUtils::equals($storedToken, $token);
+ }
+
+}
diff --git a/apps/federation/appinfo/app.php b/apps/federation/appinfo/app.php
new file mode 100644
index 0000000000..8cc77885d6
--- /dev/null
+++ b/apps/federation/appinfo/app.php
@@ -0,0 +1,26 @@
+
+ *
+ * @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
+ *
+ */
+
+namespace OCA\Federation\AppInfo;
+
+$app = new Application();
+$app->registerSettings();
+$app->registerHooks();
diff --git a/apps/federation/appinfo/application.php b/apps/federation/appinfo/application.php
new file mode 100644
index 0000000000..172283536b
--- /dev/null
+++ b/apps/federation/appinfo/application.php
@@ -0,0 +1,148 @@
+
+ *
+ * @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
+ *
+ */
+
+namespace OCA\Federation\AppInfo;
+
+use OCA\Federation\API\OCSAuthAPI;
+use OCA\Federation\Controller\SettingsController;
+use OCA\Federation\DbHandler;
+use OCA\Federation\Hooks;
+use OCA\Federation\Middleware\AddServerMiddleware;
+use OCA\Federation\TrustedServers;
+use OCP\API;
+use OCP\App;
+use OCP\AppFramework\IAppContainer;
+use OCP\Util;
+
+class Application extends \OCP\AppFramework\App {
+
+ /**
+ * @param array $urlParams
+ */
+ public function __construct($urlParams = array()) {
+ parent::__construct('federation', $urlParams);
+ $this->registerService();
+ $this->registerMiddleware();
+ }
+
+ /**
+ * register setting scripts
+ */
+ public function registerSettings() {
+ App::registerAdmin('federation', 'settings/settings-admin');
+ }
+
+ private function registerService() {
+ $container = $this->getContainer();
+
+ $container->registerService('addServerMiddleware', function(IAppContainer $c) {
+ return new AddServerMiddleware(
+ $c->getAppName(),
+ \OC::$server->getL10N($c->getAppName()),
+ \OC::$server->getLogger()
+ );
+ });
+
+ $container->registerService('DbHandler', function(IAppContainer $c) {
+ return new DbHandler(
+ \OC::$server->getDatabaseConnection(),
+ \OC::$server->getL10N($c->getAppName())
+ );
+ });
+
+ $container->registerService('TrustedServers', function(IAppContainer $c) {
+ return new TrustedServers(
+ $c->query('DbHandler'),
+ \OC::$server->getHTTPClientService(),
+ \OC::$server->getLogger(),
+ \OC::$server->getJobList(),
+ \OC::$server->getSecureRandom(),
+ \OC::$server->getConfig()
+ );
+ });
+
+ $container->registerService('SettingsController', function (IAppContainer $c) {
+ $server = $c->getServer();
+ return new SettingsController(
+ $c->getAppName(),
+ $server->getRequest(),
+ $server->getL10N($c->getAppName()),
+ $c->query('TrustedServers')
+ );
+ });
+ }
+
+ private function registerMiddleware() {
+ $container = $this->getContainer();
+ $container->registerMiddleware('addServerMiddleware');
+ }
+
+ /**
+ * register OCS API Calls
+ */
+ public function registerOCSApi() {
+
+ $container = $this->getContainer();
+ $server = $container->getServer();
+
+ $auth = new OCSAuthAPI(
+ $server->getRequest(),
+ $server->getSecureRandom(),
+ $server->getJobList(),
+ $container->query('TrustedServers'),
+ $container->query('DbHandler')
+
+ );
+
+ API::register('get',
+ '/apps/federation/api/v1/shared-secret',
+ array($auth, 'getSharedSecret'),
+ 'federation',
+ API::GUEST_AUTH
+ );
+
+ API::register('post',
+ '/apps/federation/api/v1/request-shared-secret',
+ array($auth, 'requestSharedSecret'),
+ 'federation',
+ API::GUEST_AUTH
+ );
+
+ }
+
+ /**
+ * listen to federated_share_added hooks to auto-add new servers to the
+ * list of trusted servers.
+ */
+ public function registerHooks() {
+
+ $container = $this->getContainer();
+ $hooksManager = new Hooks($container->query('TrustedServers'));
+
+ Util::connectHook(
+ 'OCP\Share',
+ 'federated_share_added',
+ $hooksManager,
+ 'addServerHook'
+ );
+ }
+
+}
diff --git a/apps/federation/appinfo/database.xml b/apps/federation/appinfo/database.xml
new file mode 100644
index 0000000000..e0bb241918
--- /dev/null
+++ b/apps/federation/appinfo/database.xml
@@ -0,0 +1,63 @@
+
+
+ *dbname*
+ true
+ false
+ utf8
+
+ *dbprefix*trusted_servers
+
+
+ id
+ integer
+ 0
+ true
+ 1
+ 4
+
+
+ url
+ text
+ true
+ 512
+ Url of trusted server
+
+
+ url_hash
+ text
+
+ true
+ 32
+ md5 hash of the url without the protocol
+
+
+ token
+ text
+ 128
+ toke used to exchange the shared secret
+
+
+ shared_secret
+ text
+ 256
+ shared secret used to authenticate
+
+
+ status
+ integer
+ 4
+ true
+ 2
+ current status of the connection
+
+
+ url_hash
+ true
+
+ url_hash
+ ascending
+
+
+
+
+
diff --git a/apps/federation/appinfo/info.xml b/apps/federation/appinfo/info.xml
new file mode 100644
index 0000000000..53b2926ba5
--- /dev/null
+++ b/apps/federation/appinfo/info.xml
@@ -0,0 +1,14 @@
+
+
+ federation
+ Federation
+ ownCloud Federation allows you to connect with other trusted ownClouds to exchange the user directory. For example this will be used to auto-complete external users for federated sharing.
+ AGPL
+ Bjoern Schiessle
+ 0.0.1
+ Federation
+ other
+
+
+
+
diff --git a/apps/federation/appinfo/routes.php b/apps/federation/appinfo/routes.php
new file mode 100644
index 0000000000..8c1629a4fc
--- /dev/null
+++ b/apps/federation/appinfo/routes.php
@@ -0,0 +1,47 @@
+
+ *
+ * @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
+ *
+ */
+
+$application = new \OCA\Federation\AppInfo\Application();
+
+$application->registerRoutes(
+ $this,
+ [
+ 'routes' => [
+ [
+ 'name' => 'Settings#addServer',
+ 'url' => '/trusted-servers',
+ 'verb' => 'POST'
+ ],
+ [
+ 'name' => 'Settings#removeServer',
+ 'url' => '/trusted-servers/{id}',
+ 'verb' => 'DELETE'
+ ],
+ [
+ 'name' => 'Settings#autoAddServers',
+ 'url' => '/auto-add-servers',
+ 'verb' => 'POST'
+ ],
+ ]
+ ]
+);
+
+$application->registerOCSApi();
diff --git a/apps/federation/backgroundjob/getsharedsecret.php b/apps/federation/backgroundjob/getsharedsecret.php
new file mode 100644
index 0000000000..eb55fa2d6a
--- /dev/null
+++ b/apps/federation/backgroundjob/getsharedsecret.php
@@ -0,0 +1,185 @@
+
+ *
+ * @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
+ *
+ */
+
+
+namespace OCA\Federation\BackgroundJob;
+
+use GuzzleHttp\Exception\ClientException;
+use OC\BackgroundJob\JobList;
+use OC\BackgroundJob\QueuedJob;
+use OCA\Federation\DbHandler;
+use OCA\Federation\TrustedServers;
+use OCP\AppFramework\Http;
+use OCP\BackgroundJob\IJobList;
+use OCP\Http\Client\IClient;
+use OCP\ILogger;
+use OCP\IURLGenerator;
+
+/**
+ * Class GetSharedSecret
+ *
+ * request shared secret from remote ownCloud
+ *
+ * @package OCA\Federation\Backgroundjob
+ */
+class GetSharedSecret extends QueuedJob{
+
+ /** @var IClient */
+ private $httpClient;
+
+ /** @var IJobList */
+ private $jobList;
+
+ /** @var IURLGenerator */
+ private $urlGenerator;
+
+ /** @var TrustedServers */
+ private $trustedServers;
+
+ /** @var DbHandler */
+ private $dbHandler;
+
+ /** @var ILogger */
+ private $logger;
+
+ private $endPoint = '/ocs/v2.php/apps/federation/api/v1/shared-secret?format=json';
+
+ /**
+ * RequestSharedSecret constructor.
+ *
+ * @param IClient $httpClient
+ * @param IURLGenerator $urlGenerator
+ * @param IJobList $jobList
+ * @param TrustedServers $trustedServers
+ * @param ILogger $logger
+ * @param DbHandler $dbHandler
+ */
+ public function __construct(
+ IClient $httpClient = null,
+ IURLGenerator $urlGenerator = null,
+ IJobList $jobList = null,
+ TrustedServers $trustedServers = null,
+ ILogger $logger = null,
+ dbHandler $dbHandler = null
+ ) {
+ $this->logger = $logger ? $logger : \OC::$server->getLogger();
+ $this->httpClient = $httpClient ? $httpClient : \OC::$server->getHTTPClientService()->newClient();
+ $this->jobList = $jobList ? $jobList : \OC::$server->getJobList();
+ $this->urlGenerator = $urlGenerator ? $urlGenerator : \OC::$server->getURLGenerator();
+ $this->dbHandler = $dbHandler ? $dbHandler : new DbHandler(\OC::$server->getDatabaseConnection(), \OC::$server->getL10N('federation'));
+ if ($trustedServers) {
+ $this->trustedServers = $trustedServers;
+ } else {
+ $this->trustedServers = new TrustedServers(
+ $this->dbHandler,
+ \OC::$server->getHTTPClientService(),
+ \OC::$server->getLogger(),
+ $this->jobList,
+ \OC::$server->getSecureRandom(),
+ \OC::$server->getConfig()
+ );
+ }
+ }
+
+ /**
+ * run the job, then remove it from the joblist
+ *
+ * @param JobList $jobList
+ * @param ILogger $logger
+ */
+ public function execute($jobList, ILogger $logger = null) {
+ $jobList->remove($this, $this->argument);
+ $target = $this->argument['url'];
+ // only execute if target is still in the list of trusted domains
+ if ($this->trustedServers->isTrustedServer($target)) {
+ $this->parentExecute($jobList, $logger);
+ }
+ }
+
+ /**
+ * call execute() method of parent
+ *
+ * @param JobList $jobList
+ * @param ILogger $logger
+ */
+ protected function parentExecute($jobList, $logger) {
+ parent::execute($jobList, $logger);
+ }
+
+ protected function run($argument) {
+ $target = $argument['url'];
+ $source = $this->urlGenerator->getAbsoluteURL('/');
+ $source = rtrim($source, '/');
+ $token = $argument['token'];
+
+ try {
+ $result = $this->httpClient->get(
+ $target . $this->endPoint,
+ [
+ 'query' =>
+ [
+ 'url' => $source,
+ 'token' => $token
+ ],
+ 'timeout' => 3,
+ 'connect_timeout' => 3,
+ ]
+ );
+
+ $status = $result->getStatusCode();
+
+ } catch (ClientException $e) {
+ $status = $e->getCode();
+ }
+
+ // if we received a unexpected response we try again later
+ if (
+ $status !== Http::STATUS_OK
+ && $status !== Http::STATUS_FORBIDDEN
+ ) {
+ $this->jobList->add(
+ 'OCA\Federation\BackgroundJob\GetSharedSecret',
+ $argument
+ );
+ } else {
+ // reset token if we received a valid response
+ $this->dbHandler->addToken($target, '');
+ }
+
+ if ($status === Http::STATUS_OK) {
+ $body = $result->getBody();
+ $result = json_decode($body, true);
+ if (isset($result['ocs']['data']['sharedSecret'])) {
+ $this->trustedServers->addSharedSecret(
+ $target,
+ $result['ocs']['data']['sharedSecret']
+ );
+ } else {
+ $this->logger->error(
+ 'remote server "' . $target . '"" does not return a valid shared secret',
+ ['app' => 'federation']
+ );
+ $this->trustedServers->setServerStatus($target, TrustedServers::STATUS_FAILURE);
+ }
+ }
+
+ }
+}
diff --git a/apps/federation/backgroundjob/requestsharedsecret.php b/apps/federation/backgroundjob/requestsharedsecret.php
new file mode 100644
index 0000000000..24d8adada1
--- /dev/null
+++ b/apps/federation/backgroundjob/requestsharedsecret.php
@@ -0,0 +1,164 @@
+
+ *
+ * @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
+ *
+ */
+
+
+namespace OCA\Federation\BackgroundJob;
+
+
+use GuzzleHttp\Exception\ClientException;
+use OC\BackgroundJob\JobList;
+use OC\BackgroundJob\QueuedJob;
+use OCA\Federation\DbHandler;
+use OCA\Federation\TrustedServers;
+use OCP\AppFramework\Http;
+use OCP\BackgroundJob\IJobList;
+use OCP\Http\Client\IClient;
+use OCP\ILogger;
+use OCP\IURLGenerator;
+
+/**
+ * Class RequestSharedSecret
+ *
+ * Ask remote ownCloud to request a sharedSecret from this server
+ *
+ * @package OCA\Federation\Backgroundjob
+ */
+class RequestSharedSecret extends QueuedJob {
+
+ /** @var IClient */
+ private $httpClient;
+
+ /** @var IJobList */
+ private $jobList;
+
+ /** @var IURLGenerator */
+ private $urlGenerator;
+
+ /** @var DbHandler */
+ private $dbHandler;
+
+ /** @var TrustedServers */
+ private $trustedServers;
+
+ private $endPoint = '/ocs/v2.php/apps/federation/api/v1/request-shared-secret?format=json';
+
+ /**
+ * RequestSharedSecret constructor.
+ *
+ * @param IClient $httpClient
+ * @param IURLGenerator $urlGenerator
+ * @param IJobList $jobList
+ * @param TrustedServers $trustedServers
+ * @param DbHandler $dbHandler
+ */
+ public function __construct(
+ IClient $httpClient = null,
+ IURLGenerator $urlGenerator = null,
+ IJobList $jobList = null,
+ TrustedServers $trustedServers = null,
+ dbHandler $dbHandler = null
+ ) {
+ $this->httpClient = $httpClient ? $httpClient : \OC::$server->getHTTPClientService()->newClient();
+ $this->jobList = $jobList ? $jobList : \OC::$server->getJobList();
+ $this->urlGenerator = $urlGenerator ? $urlGenerator : \OC::$server->getURLGenerator();
+ $this->dbHandler = $dbHandler ? $dbHandler : new DbHandler(\OC::$server->getDatabaseConnection(), \OC::$server->getL10N('federation'));
+ if ($trustedServers) {
+ $this->trustedServers = $trustedServers;
+ } else {
+ $this->trustedServers = new TrustedServers(
+ $this->dbHandler,
+ \OC::$server->getHTTPClientService(),
+ \OC::$server->getLogger(),
+ $this->jobList,
+ \OC::$server->getSecureRandom(),
+ \OC::$server->getConfig()
+ );
+ }
+ }
+
+
+ /**
+ * run the job, then remove it from the joblist
+ *
+ * @param JobList $jobList
+ * @param ILogger $logger
+ */
+ public function execute($jobList, ILogger $logger = null) {
+ $jobList->remove($this, $this->argument);
+ $target = $this->argument['url'];
+ // only execute if target is still in the list of trusted domains
+ if ($this->trustedServers->isTrustedServer($target)) {
+ $this->parentExecute($jobList, $logger);
+ }
+ }
+
+ /**
+ * @param JobList $jobList
+ * @param ILogger $logger
+ */
+ protected function parentExecute($jobList, $logger) {
+ parent::execute($jobList, $logger);
+ }
+
+ protected function run($argument) {
+
+ $target = $argument['url'];
+ $source = $this->urlGenerator->getAbsoluteURL('/');
+ $source = rtrim($source, '/');
+ $token = $argument['token'];
+
+ try {
+ $result = $this->httpClient->post(
+ $target . $this->endPoint,
+ [
+ 'body' => [
+ 'url' => $source,
+ 'token' => $token,
+ ],
+ 'timeout' => 3,
+ 'connect_timeout' => 3,
+ ]
+ );
+
+ $status = $result->getStatusCode();
+
+ } catch (ClientException $e) {
+ $status = $e->getCode();
+ }
+
+ // if we received a unexpected response we try again later
+ if (
+ $status !== Http::STATUS_OK
+ && $status !== Http::STATUS_FORBIDDEN
+ ) {
+ $this->jobList->add(
+ 'OCA\Federation\BackgroundJob\RequestSharedSecret',
+ $argument
+ );
+ }
+
+ if ($status === Http::STATUS_FORBIDDEN) {
+ // clear token if remote server refuses to ask for shared secret
+ $this->dbHandler->addToken($target, '');
+ }
+
+ }
+}
diff --git a/apps/federation/controller/settingscontroller.php b/apps/federation/controller/settingscontroller.php
new file mode 100644
index 0000000000..2e28cd60cf
--- /dev/null
+++ b/apps/federation/controller/settingscontroller.php
@@ -0,0 +1,123 @@
+
+ *
+ * @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
+ *
+ */
+
+namespace OCA\Federation\Controller;
+
+use OC\HintException;
+use OCA\Federation\TrustedServers;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\IConfig;
+use OCP\IL10N;
+use OCP\IRequest;
+
+
+class SettingsController extends Controller {
+
+ /** @var IL10N */
+ private $l;
+
+ /** @var TrustedServers */
+ private $trustedServers;
+
+ /**
+ * @param string $AppName
+ * @param IRequest $request
+ * @param IL10N $l10n
+ * @param TrustedServers $trustedServers
+ */
+ public function __construct($AppName,
+ IRequest $request,
+ IL10N $l10n,
+ TrustedServers $trustedServers
+ ) {
+ parent::__construct($AppName, $request);
+ $this->l = $l10n;
+ $this->trustedServers = $trustedServers;
+ }
+
+
+ /**
+ * add server to the list of trusted ownClouds
+ *
+ * @param string $url
+ * @return DataResponse
+ * @throws HintException
+ */
+ public function addServer($url) {
+ $this->checkServer($url);
+ $id = $this->trustedServers->addServer($url);
+
+ return new DataResponse(
+ [
+ 'url' => $url,
+ 'id' => $id,
+ 'message' => (string) $this->l->t('Server added to the list of trusted ownClouds')
+ ]
+ );
+ }
+
+ /**
+ * add server to the list of trusted ownClouds
+ *
+ * @param int $id
+ * @return DataResponse
+ */
+ public function removeServer($id) {
+ $this->trustedServers->removeServer($id);
+ return new DataResponse();
+ }
+
+ /**
+ * enable/disable to automatically add servers to the list of trusted servers
+ * once a federated share was created and accepted successfully
+ *
+ * @param bool $autoAddServers
+ */
+ public function autoAddServers($autoAddServers) {
+ $this->trustedServers->setAutoAddServers($autoAddServers);
+ }
+
+ /**
+ * check if the server should be added to the list of trusted servers or not
+ *
+ * @param string $url
+ * @return bool
+ * @throws HintException
+ */
+ protected function checkServer($url) {
+ if ($this->trustedServers->isTrustedServer($url) === true) {
+ $message = 'Server is already in the list of trusted servers.';
+ $hint = $this->l->t('Server is already in the list of trusted servers.');
+ throw new HintException($message, $hint);
+ }
+
+ if ($this->trustedServers->isOwnCloudServer($url) === false) {
+ $message = 'No ownCloud server found';
+ $hint = $this->l->t('No ownCloud server found');
+ throw new HintException($message, $hint);
+ }
+
+ return true;
+ }
+
+}
diff --git a/apps/federation/css/settings-admin.css b/apps/federation/css/settings-admin.css
new file mode 100644
index 0000000000..55b1dd64d1
--- /dev/null
+++ b/apps/federation/css/settings-admin.css
@@ -0,0 +1,26 @@
+#ocFederationSettings p {
+ padding-top: 10px;
+}
+
+#listOfTrustedServers li {
+ padding-top: 10px;
+ padding-left: 20px;
+}
+
+.removeTrustedServer {
+ display: none;
+ vertical-align:middle;
+ padding-left: 10px;
+}
+
+#ocFederationAddServerButton {
+ cursor: pointer;
+}
+
+#listOfTrustedServers li:hover {
+ cursor: pointer;
+}
+
+#listOfTrustedServers .status {
+ margin-right: 10px;
+}
diff --git a/apps/federation/img/app.svg b/apps/federation/img/app.svg
new file mode 100644
index 0000000000..b6ae35211a
--- /dev/null
+++ b/apps/federation/img/app.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/apps/federation/js/settings-admin.js b/apps/federation/js/settings-admin.js
new file mode 100644
index 0000000000..7d531b39d8
--- /dev/null
+++ b/apps/federation/js/settings-admin.js
@@ -0,0 +1,82 @@
+/**
+ * @author Björn Schießle
+ *
+ * @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
+ *
+ */
+
+$(document).ready(function () {
+
+ // show input field to add a new trusted server
+ $("#ocFederationAddServer").on('click', function() {
+ $('#ocFederationAddServerButton').addClass('hidden');
+ $("#serverUrl").removeClass('hidden');
+ $("#serverUrl").focus();
+ });
+
+ // add new trusted server
+ $("#serverUrl").keyup(function (e) {
+ if (e.keyCode === 13) { // add server on "enter"
+ var url = $('#serverUrl').val();
+ OC.msg.startSaving('#ocFederationAddServer .msg');
+ $.post(
+ OC.generateUrl('/apps/federation/trusted-servers'),
+ {
+ url: url
+ }
+ ).done(function (data) {
+ $('#serverUrl').attr('value', '');
+ $('ul#listOfTrustedServers').prepend(
+ $('
')
+ .attr('id', data.id)
+ .attr('class', 'icon-delete')
+ .html('' + data.url)
+ );
+ OC.msg.finishedSuccess('#ocFederationAddServer .msg', data.message);
+ })
+ .fail(function (jqXHR) {
+ OC.msg.finishedError('#ocFederationAddServer .msg', JSON.parse(jqXHR.responseText).message);
+ });
+ } else if (e.keyCode === 27) { // hide input filed again in ESC
+ $('#ocFederationAddServerButton').toggleClass('hidden');
+ $("#serverUrl").toggleClass('hidden');
+ }
+ });
+
+ // remove trusted server from list
+ $( "#listOfTrustedServers" ).on('click', 'li', function() {
+ var id = $(this).attr('id');
+ var $this = $(this);
+ $.ajax({
+ url: OC.generateUrl('/apps/federation/trusted-servers/' + id),
+ type: 'DELETE',
+ success: function(response) {
+ $this.remove();
+ }
+ });
+
+ });
+
+ $("#ocFederationSettings #autoAddServers").change(function() {
+ $.post(
+ OC.generateUrl('/apps/federation/auto-add-servers'),
+ {
+ autoAddServers: $(this).is(":checked")
+ }
+ );
+ });
+
+});
diff --git a/apps/federation/lib/dbhandler.php b/apps/federation/lib/dbhandler.php
new file mode 100644
index 0000000000..7606593f78
--- /dev/null
+++ b/apps/federation/lib/dbhandler.php
@@ -0,0 +1,270 @@
+
+ *
+ * @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
+ *
+ */
+
+
+namespace OCA\Federation;
+
+
+use OC\Files\Filesystem;
+use OC\HintException;
+use OCP\IDBConnection;
+use OCP\IL10N;
+
+/**
+ * Class DbHandler
+ *
+ * handles all database calls for the federation app
+ *
+ * @group DB
+ * @package OCA\Federation
+ */
+class DbHandler {
+
+ /** @var IDBConnection */
+ private $connection;
+
+ /** @var IL10N */
+ private $l;
+
+ /** @var string */
+ private $dbTable = 'trusted_servers';
+
+ /**
+ * @param IDBConnection $connection
+ * @param IL10N $il10n
+ */
+ public function __construct(
+ IDBConnection $connection,
+ IL10N $il10n
+ ) {
+ $this->connection = $connection;
+ $this->IL10N = $il10n;
+ }
+
+ /**
+ * add server to the list of trusted ownCloud servers
+ *
+ * @param string $url
+ * @return int
+ * @throws HintException
+ */
+ public function addServer($url) {
+ $hash = $this->hash($url);
+ $url = rtrim($url, '/');
+ $query = $this->connection->getQueryBuilder();
+ $query->insert($this->dbTable)
+ ->values(
+ [
+ 'url' => $query->createParameter('url'),
+ 'url_hash' => $query->createParameter('url_hash'),
+ ]
+ )
+ ->setParameter('url', $url)
+ ->setParameter('url_hash', $hash);
+
+ $result = $query->execute();
+
+ if ($result) {
+ return (int)$this->connection->lastInsertId('*PREFIX*'.$this->dbTable);
+ } else {
+ $message = 'Internal failure, Could not add ownCloud as trusted server: ' . $url;
+ $message_t = $this->l->t('Could not add server');
+ throw new HintException($message, $message_t);
+ }
+ }
+
+ /**
+ * remove server from the list of trusted ownCloud servers
+ *
+ * @param int $id
+ */
+ public function removeServer($id) {
+ $query = $this->connection->getQueryBuilder();
+ $query->delete($this->dbTable)
+ ->where($query->expr()->eq('id', $query->createParameter('id')))
+ ->setParameter('id', $id);
+ $query->execute();
+ }
+
+ /**
+ * get all trusted servers
+ *
+ * @return array
+ */
+ public function getAllServer() {
+ $query = $this->connection->getQueryBuilder();
+ $query->select(['url', 'id', 'status'])->from($this->dbTable);
+ $result = $query->execute()->fetchAll();
+ return $result;
+ }
+
+ /**
+ * check if server already exists in the database table
+ *
+ * @param string $url
+ * @return bool
+ */
+ public function serverExists($url) {
+ $hash = $this->hash($url);
+ $query = $this->connection->getQueryBuilder();
+ $query->select('url')->from($this->dbTable)
+ ->where($query->expr()->eq('url_hash', $query->createParameter('url_hash')))
+ ->setParameter('url_hash', $hash);
+ $result = $query->execute()->fetchAll();
+
+ return !empty($result);
+ }
+
+ /**
+ * write token to database. Token is used to exchange the secret
+ *
+ * @param string $url
+ * @param string $token
+ */
+ public function addToken($url, $token) {
+ $hash = $this->hash($url);
+ $query = $this->connection->getQueryBuilder();
+ $query->update($this->dbTable)
+ ->set('token', $query->createParameter('token'))
+ ->where($query->expr()->eq('url_hash', $query->createParameter('url_hash')))
+ ->setParameter('url_hash', $hash)
+ ->setParameter('token', $token);
+ $query->execute();
+ }
+
+ /**
+ * get token stored in database
+ *
+ * @param string $url
+ * @return string
+ */
+ public function getToken($url) {
+ $hash = $this->hash($url);
+ $query = $this->connection->getQueryBuilder();
+ $query->select('token')->from($this->dbTable)
+ ->where($query->expr()->eq('url_hash', $query->createParameter('url_hash')))
+ ->setParameter('url_hash', $hash);
+
+ $result = $query->execute()->fetch();
+ return $result['token'];
+ }
+
+ /**
+ * add shared Secret to database
+ *
+ * @param string $url
+ * @param string $sharedSecret
+ */
+ public function addSharedSecret($url, $sharedSecret) {
+ $hash = $this->hash($url);
+ $query = $this->connection->getQueryBuilder();
+ $query->update($this->dbTable)
+ ->set('shared_secret', $query->createParameter('sharedSecret'))
+ ->where($query->expr()->eq('url_hash', $query->createParameter('url_hash')))
+ ->setParameter('url_hash', $hash)
+ ->setParameter('sharedSecret', $sharedSecret);
+ $query->execute();
+ }
+
+ /**
+ * get shared secret from database
+ *
+ * @param string $url
+ * @return string
+ */
+ public function getSharedSecret($url) {
+ $hash = $this->hash($url);
+ $query = $this->connection->getQueryBuilder();
+ $query->select('shared_secret')->from($this->dbTable)
+ ->where($query->expr()->eq('url_hash', $query->createParameter('url_hash')))
+ ->setParameter('url_hash', $hash);
+
+ $result = $query->execute()->fetch();
+ return $result['shared_secret'];
+ }
+
+ /**
+ * set server status
+ *
+ * @param string $url
+ * @param int $status
+ */
+ public function setServerStatus($url, $status) {
+ $hash = $this->hash($url);
+ $query = $this->connection->getQueryBuilder();
+ $query->update($this->dbTable)
+ ->set('status', $query->createParameter('status'))
+ ->where($query->expr()->eq('url_hash', $query->createParameter('url_hash')))
+ ->setParameter('url_hash', $hash)
+ ->setParameter('status', $status);
+ $query->execute();
+ }
+
+ /**
+ * get server status
+ *
+ * @param string $url
+ * @return int
+ */
+ public function getServerStatus($url) {
+ $hash = $this->hash($url);
+ $query = $this->connection->getQueryBuilder();
+ $query->select('status')->from($this->dbTable)
+ ->where($query->expr()->eq('url_hash', $query->createParameter('url_hash')))
+ ->setParameter('url_hash', $hash);
+
+ $result = $query->execute()->fetch();
+ return (int)$result['status'];
+ }
+
+ /**
+ * create hash from URL
+ *
+ * @param string $url
+ * @return string
+ */
+ protected function hash($url) {
+ $normalized = $this->normalizeUrl($url);
+ return md5($normalized);
+ }
+
+ /**
+ * normalize URL, used to create the md5 hash
+ *
+ * @param string $url
+ * @return string
+ */
+ protected function normalizeUrl($url) {
+ $normalized = $url;
+
+ if (strpos($url, 'https://') === 0) {
+ $normalized = substr($url, strlen('https://'));
+ } else if (strpos($url, 'http://') === 0) {
+ $normalized = substr($url, strlen('http://'));
+ }
+
+ $normalized = Filesystem::normalizePath($normalized);
+ $normalized = trim($normalized, '/');
+
+ return $normalized;
+ }
+
+}
diff --git a/apps/federation/lib/hooks.php b/apps/federation/lib/hooks.php
new file mode 100644
index 0000000000..4bf5be4e5b
--- /dev/null
+++ b/apps/federation/lib/hooks.php
@@ -0,0 +1,50 @@
+
+ *
+ * @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
+ *
+ */
+
+
+namespace OCA\Federation;
+
+
+
+class Hooks {
+
+ /** @var TrustedServers */
+ private $trustedServers;
+
+ public function __construct(TrustedServers $trustedServers) {
+ $this->trustedServers = $trustedServers;
+ }
+
+ /**
+ * add servers to the list of trusted servers once a federated share was established
+ *
+ * @param array $params
+ */
+ public function addServerHook($params) {
+ if (
+ $this->trustedServers->getAutoAddServers() === true &&
+ $this->trustedServers->isTrustedServer($params['server']) === false
+ ) {
+ $this->trustedServers->addServer($params['server']);
+ }
+ }
+
+}
diff --git a/apps/federation/lib/trustedservers.php b/apps/federation/lib/trustedservers.php
new file mode 100644
index 0000000000..96a2917807
--- /dev/null
+++ b/apps/federation/lib/trustedservers.php
@@ -0,0 +1,254 @@
+
+ *
+ * @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
+ *
+ */
+
+
+namespace OCA\Federation;
+
+use OCP\AppFramework\Http;
+use OCP\BackgroundJob\IJobList;
+use OCP\Http\Client\IClientService;
+use OCP\IConfig;
+use OCP\ILogger;
+use OCP\Security\ISecureRandom;
+
+class TrustedServers {
+
+ /** after a user list was exchanged at least once successfully */
+ const STATUS_OK = 1;
+ /** waiting for shared secret or initial user list exchange */
+ const STATUS_PENDING = 2;
+ /** something went wrong, misconfigured server, software bug,... user interaction needed */
+ const STATUS_FAILURE = 3;
+
+ /** @var dbHandler */
+ private $dbHandler;
+
+ /** @var IClientService */
+ private $httpClientService;
+
+ /** @var ILogger */
+ private $logger;
+
+ /** @var IJobList */
+ private $jobList;
+
+ /** @var ISecureRandom */
+ private $secureRandom;
+
+ /** @var IConfig */
+ private $config;
+
+ /**
+ * @param DbHandler $dbHandler
+ * @param IClientService $httpClientService
+ * @param ILogger $logger
+ * @param IJobList $jobList
+ * @param ISecureRandom $secureRandom
+ * @param IConfig $config
+ */
+ public function __construct(
+ DbHandler $dbHandler,
+ IClientService $httpClientService,
+ ILogger $logger,
+ IJobList $jobList,
+ ISecureRandom $secureRandom,
+ IConfig $config
+ ) {
+ $this->dbHandler = $dbHandler;
+ $this->httpClientService = $httpClientService;
+ $this->logger = $logger;
+ $this->jobList = $jobList;
+ $this->secureRandom = $secureRandom;
+ $this->config = $config;
+ }
+
+ /**
+ * add server to the list of trusted ownCloud servers
+ *
+ * @param $url
+ * @return int server id
+ */
+ public function addServer($url) {
+ $url = $this->updateProtocol($url);
+ $result = $this->dbHandler->addServer($url);
+ if ($result) {
+ $token = $this->secureRandom->getMediumStrengthGenerator()->generate(16);
+ $this->dbHandler->addToken($url, $token);
+ $this->jobList->add(
+ 'OCA\Federation\BackgroundJob\RequestSharedSecret',
+ [
+ 'url' => $url,
+ 'token' => $token
+ ]
+ );
+ }
+
+ return $result;
+ }
+
+ /**
+ * enable/disable to automatically add servers to the list of trusted servers
+ * once a federated share was created and accepted successfully
+ *
+ * @param bool $status
+ */
+ public function setAutoAddServers($status) {
+ $value = $status ? '1' : '0';
+ $this->config->setAppValue('federation', 'autoAddServers', $value);
+ }
+
+ /**
+ * return if we automatically add servers to the list of trusted servers
+ * once a federated share was created and accepted successfully
+ *
+ * @return bool
+ */
+ public function getAutoAddServers() {
+ $value = $this->config->getAppValue('federation', 'autoAddServers', '1');
+ return $value === '1';
+ }
+
+ /**
+ * get shared secret for the given server
+ *
+ * @param string $url
+ * @return string
+ */
+ public function getSharedSecret($url) {
+ return $this->dbHandler->getSharedSecret($url);
+ }
+
+ /**
+ * add shared secret for the given server
+ *
+ * @param string $url
+ * @param $sharedSecret
+ */
+ public function addSharedSecret($url, $sharedSecret) {
+ $this->dbHandler->addSharedSecret($url, $sharedSecret);
+ }
+
+ /**
+ * remove server from the list of trusted ownCloud servers
+ *
+ * @param int $id
+ */
+ public function removeServer($id) {
+ $this->dbHandler->removeServer($id);
+ }
+
+ /**
+ * get all trusted servers
+ *
+ * @return array
+ */
+ public function getServers() {
+ return $this->dbHandler->getAllServer();
+ }
+
+ /**
+ * check if given server is a trusted ownCloud server
+ *
+ * @param string $url
+ * @return bool
+ */
+ public function isTrustedServer($url) {
+ return $this->dbHandler->serverExists($url);
+ }
+
+ /**
+ * set server status
+ *
+ * @param string $url
+ * @param int $status
+ */
+ public function setServerStatus($url, $status) {
+ $this->dbHandler->setServerStatus($url, $status);
+ }
+
+ /**
+ * @param string $url
+ * @return int
+ */
+ public function getServerStatus($url) {
+ return $this->dbHandler->getServerStatus($url);
+ }
+
+ /**
+ * check if URL point to a ownCloud server
+ *
+ * @param string $url
+ * @return bool
+ */
+ public function isOwnCloudServer($url) {
+ $isValidOwnCloud = false;
+ $client = $this->httpClientService->newClient();
+ try {
+ $result = $client->get(
+ $url . '/status.php',
+ [
+ 'timeout' => 3,
+ 'connect_timeout' => 3,
+ ]
+ );
+ if ($result->getStatusCode() === Http::STATUS_OK) {
+ $isValidOwnCloud = $this->checkOwnCloudVersion($result->getBody());
+ }
+ } catch (\Exception $e) {
+ $this->logger->error($e->getMessage(), ['app' => 'federation']);
+ return false;
+ }
+ return $isValidOwnCloud;
+ }
+
+ /**
+ * check if ownCloud version is >= 9.0
+ *
+ * @param $statusphp
+ * @return bool
+ */
+ protected function checkOwnCloudVersion($statusphp) {
+ $decoded = json_decode($statusphp, true);
+ if (!empty($decoded) && isset($decoded['version'])) {
+ return version_compare($decoded['version'], '9.0.0', '>=');
+ }
+ return false;
+ }
+
+ /**
+ * check if the URL contain a protocol, if not add https
+ *
+ * @param string $url
+ * @return string
+ */
+ protected function updateProtocol($url) {
+ if (
+ strpos($url, 'https://') === 0
+ || strpos($url, 'http://') === 0
+ ) {
+
+ return $url;
+
+ }
+
+ return 'https://' . $url;
+ }
+}
diff --git a/apps/federation/middleware/addservermiddleware.php b/apps/federation/middleware/addservermiddleware.php
new file mode 100644
index 0000000000..56552021dc
--- /dev/null
+++ b/apps/federation/middleware/addservermiddleware.php
@@ -0,0 +1,71 @@
+
+ *
+ * @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
+ *
+ */
+
+namespace OCA\Federation\Middleware ;
+
+use OC\HintException;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\AppFramework\Middleware;
+use OCP\IL10N;
+use OCP\ILogger;
+
+class AddServerMiddleware extends Middleware {
+
+ /** @var string */
+ protected $appName;
+
+ /** @var IL10N */
+ protected $l;
+
+ /** @var ILogger */
+ protected $logger;
+
+ public function __construct($appName, IL10N $l, ILogger $logger) {
+ $this->appName = $appName;
+ $this->l = $l;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Log error message and return a response which can be displayed to the user
+ *
+ * @param \OCP\AppFramework\Controller $controller
+ * @param string $methodName
+ * @param \Exception $exception
+ * @return JSONResponse
+ */
+ public function afterException($controller, $methodName, \Exception $exception) {
+ $this->logger->error($exception->getMessage(), ['app' => $this->appName]);
+ if ($exception instanceof HintException) {
+ $message = $exception->getHint();
+ } else {
+ $message = $this->l->t('Unknown error');
+ }
+
+ return new JSONResponse(
+ ['message' => $message],
+ Http::STATUS_BAD_REQUEST
+ );
+
+ }
+
+}
diff --git a/apps/federation/settings/settings-admin.php b/apps/federation/settings/settings-admin.php
new file mode 100644
index 0000000000..76ae0c3b6e
--- /dev/null
+++ b/apps/federation/settings/settings-admin.php
@@ -0,0 +1,43 @@
+
+ *
+ * @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
+ *
+ */
+
+\OC_Util::checkAdminUser();
+
+$template = new OCP\Template('federation', 'settings-admin');
+
+$dbHandler = new \OCA\Federation\DbHandler(
+ \OC::$server->getDatabaseConnection(),
+ \OC::$server->getL10N('federation')
+);
+
+$trustedServers = new \OCA\Federation\TrustedServers(
+ $dbHandler,
+ \OC::$server->getHTTPClientService(),
+ \OC::$server->getLogger(),
+ \OC::$server->getJobList(),
+ \OC::$server->getSecureRandom(),
+ \OC::$server->getConfig()
+);
+
+$template->assign('trustedServers', $trustedServers->getServers());
+$template->assign('autoAddServers', $trustedServers->getAutoAddServers());
+
+return $template->fetchPage();
diff --git a/apps/federation/templates/settings-admin.php b/apps/federation/templates/settings-admin.php
new file mode 100644
index 0000000000..854bb74417
--- /dev/null
+++ b/apps/federation/templates/settings-admin.php
@@ -0,0 +1,40 @@
+
+
+
t('Federation')); ?>
+ t('ownCloud Federation allows you to connect with other trusted ownClouds to exchange the user directory. For example this will be used to auto-complete external users for federated sharing.')); ?>
+
+
+ />
+
+
+
+
Trusted ownCloud Servers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/federation/tests/api/ocsauthapitest.php b/apps/federation/tests/api/ocsauthapitest.php
new file mode 100644
index 0000000000..a334686c24
--- /dev/null
+++ b/apps/federation/tests/api/ocsauthapitest.php
@@ -0,0 +1,184 @@
+
+ *
+ * @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
+ *
+ */
+
+
+namespace OCA\Federation\Tests\API;
+
+
+use OC\BackgroundJob\JobList;
+use OCA\Federation\API\OCSAuthAPI;
+use OCA\Federation\DbHandler;
+use OCA\Federation\TrustedServers;
+use OCP\AppFramework\Http;
+use OCP\IRequest;
+use OCP\Security\ISecureRandom;
+use Test\TestCase;
+
+class OCSAuthAPITest extends TestCase {
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | IRequest */
+ private $request;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | ISecureRandom */
+ private $secureRandom;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | JobList */
+ private $jobList;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | TrustedServers */
+ private $trustedServers;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | DbHandler */
+ private $dbHandler;
+
+ /** @var OCSAuthApi */
+ private $ocsAuthApi;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->request = $this->getMock('OCP\IRequest');
+ $this->secureRandom = $this->getMock('OCP\Security\ISecureRandom');
+ $this->trustedServers = $this->getMockBuilder('OCA\Federation\TrustedServers')
+ ->disableOriginalConstructor()->getMock();
+ $this->dbHandler = $this->getMockBuilder('OCA\Federation\DbHandler')
+ ->disableOriginalConstructor()->getMock();
+ $this->jobList = $this->getMockBuilder('OC\BackgroundJob\JobList')
+ ->disableOriginalConstructor()->getMock();
+
+ $this->ocsAuthApi = new OCSAuthAPI(
+ $this->request,
+ $this->secureRandom,
+ $this->jobList,
+ $this->trustedServers,
+ $this->dbHandler
+ );
+
+ }
+
+ /**
+ * @dataProvider dataTestRequestSharedSecret
+ *
+ * @param string $token
+ * @param string $localToken
+ * @param bool $isTrustedServer
+ * @param int $expected
+ */
+ public function testRequestSharedSecret($token, $localToken, $isTrustedServer, $expected) {
+
+ $url = 'url';
+
+ $this->request->expects($this->at(0))->method('getParam')->with('url')->willReturn($url);
+ $this->request->expects($this->at(1))->method('getParam')->with('token')->willReturn($token);
+ $this->trustedServers
+ ->expects($this->once())
+ ->method('isTrustedServer')->with($url)->willReturn($isTrustedServer);
+ $this->dbHandler->expects($this->any())
+ ->method('getToken')->with($url)->willReturn($localToken);
+
+ if ($expected === Http::STATUS_OK) {
+ $this->jobList->expects($this->once())->method('add')
+ ->with('OCA\Federation\BackgroundJob\GetSharedSecret', ['url' => $url, 'token' => $token]);
+ } else {
+ $this->jobList->expects($this->never())->method('add');
+ }
+
+ $result = $this->ocsAuthApi->requestSharedSecret();
+ $this->assertSame($expected, $result->getStatusCode());
+ }
+
+ public function dataTestRequestSharedSecret() {
+ return [
+ ['token2', 'token1', true, Http::STATUS_OK],
+ ['token1', 'token2', false, Http::STATUS_FORBIDDEN],
+ ['token1', 'token2', true, Http::STATUS_FORBIDDEN],
+ ];
+ }
+
+ /**
+ * @dataProvider dataTestGetSharedSecret
+ *
+ * @param bool $isTrustedServer
+ * @param bool $isValidToken
+ * @param int $expected
+ */
+ public function testGetSharedSecret($isTrustedServer, $isValidToken, $expected) {
+
+ $url = 'url';
+ $token = 'token';
+
+ $this->request->expects($this->at(0))->method('getParam')->with('url')->willReturn($url);
+ $this->request->expects($this->at(1))->method('getParam')->with('token')->willReturn($token);
+
+ /** @var OCSAuthAPI | \PHPUnit_Framework_MockObject_MockObject $ocsAuthApi */
+ $ocsAuthApi = $this->getMockBuilder('OCA\Federation\API\OCSAuthAPI')
+ ->setConstructorArgs(
+ [
+ $this->request,
+ $this->secureRandom,
+ $this->jobList,
+ $this->trustedServers,
+ $this->dbHandler
+ ]
+ )->setMethods(['isValidToken'])->getMock();
+
+ $this->trustedServers
+ ->expects($this->any())
+ ->method('isTrustedServer')->with($url)->willReturn($isTrustedServer);
+ $ocsAuthApi->expects($this->any())
+ ->method('isValidToken')->with($url, $token)->willReturn($isValidToken);
+
+ if($expected === Http::STATUS_OK) {
+ $this->secureRandom->expects($this->once())->method('getMediumStrengthGenerator')
+ ->willReturn($this->secureRandom);
+ $this->secureRandom->expects($this->once())->method('generate')->with(32)
+ ->willReturn('secret');
+ $this->trustedServers->expects($this->once())
+ ->method('addSharedSecret')->willReturn($url, 'secret');
+ $this->dbHandler->expects($this->once())
+ ->method('addToken')->with($url, '');
+ } else {
+ $this->secureRandom->expects($this->never())->method('getMediumStrengthGenerator');
+ $this->secureRandom->expects($this->never())->method('generate');
+ $this->trustedServers->expects($this->never())->method('addSharedSecret');
+ $this->dbHandler->expects($this->never())->method('addToken');
+ }
+
+ $result = $ocsAuthApi->getSharedSecret();
+
+ $this->assertSame($expected, $result->getStatusCode());
+
+ if ($expected === Http::STATUS_OK) {
+ $data = $result->getData();
+ $this->assertSame('secret', $data['sharedSecret']);
+ }
+ }
+
+ public function dataTestGetSharedSecret() {
+ return [
+ [true, true, Http::STATUS_OK],
+ [false, true, Http::STATUS_FORBIDDEN],
+ [true, false, Http::STATUS_FORBIDDEN],
+ [false, false, Http::STATUS_FORBIDDEN],
+ ];
+ }
+
+}
diff --git a/apps/federation/tests/backgroundjob/getsharedsecrettest.php b/apps/federation/tests/backgroundjob/getsharedsecrettest.php
new file mode 100644
index 0000000000..cb3a294713
--- /dev/null
+++ b/apps/federation/tests/backgroundjob/getsharedsecrettest.php
@@ -0,0 +1,197 @@
+
+ *
+ * @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
+ *
+ */
+
+
+namespace OCA\Federation\Tests\BackgroundJob;
+
+
+use OCA\Federation\BackgroundJob\GetSharedSecret;
+use OCA\Files_Sharing\Tests\TestCase;
+use OCA\Federation\DbHandler;
+use OCA\Federation\TrustedServers;
+use OCP\AppFramework\Http;
+use OCP\BackgroundJob\IJobList;
+use OCP\Http\Client\IClient;
+use OCP\Http\Client\IResponse;
+use OCP\ILogger;
+use OCP\IURLGenerator;
+
+/**
+ * Class GetSharedSecretTest
+ *
+ * @group DB
+ *
+ * @package OCA\Federation\Tests\BackgroundJob
+ */
+class GetSharedSecretTest extends TestCase {
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | IClient */
+ private $httpClient;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | IJobList */
+ private $jobList;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | IURLGenerator */
+ private $urlGenerator;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | TrustedServers */
+ private $trustedServers;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | DbHandler */
+ private $dbHandler;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | ILogger */
+ private $logger;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | IResponse */
+ private $response;
+
+ /** @var GetSharedSecret */
+ private $getSharedSecret;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->httpClient = $this->getMock('OCP\Http\Client\IClient');
+ $this->jobList = $this->getMock('OCP\BackgroundJob\IJobList');
+ $this->urlGenerator = $this->getMock('OCP\IURLGenerator');
+ $this->trustedServers = $this->getMockBuilder('OCA\Federation\TrustedServers')
+ ->disableOriginalConstructor()->getMock();
+ $this->dbHandler = $this->getMockBuilder('OCA\Federation\DbHandler')
+ ->disableOriginalConstructor()->getMock();
+ $this->logger = $this->getMock('OCP\ILogger');
+ $this->response = $this->getMock('OCP\Http\Client\IResponse');
+
+ $this->getSharedSecret = new GetSharedSecret(
+ $this->httpClient,
+ $this->urlGenerator,
+ $this->jobList,
+ $this->trustedServers,
+ $this->logger,
+ $this->dbHandler
+ );
+ }
+
+ /**
+ * @dataProvider dataTestExecute
+ *
+ * @param bool $isTrustedServer
+ */
+ public function testExecute($isTrustedServer) {
+ /** @var GetSharedSecret |\PHPUnit_Framework_MockObject_MockObject $getSharedSecret */
+ $getSharedSecret = $this->getMockBuilder('OCA\Federation\BackgroundJob\GetSharedSecret')
+ ->setConstructorArgs(
+ [
+ $this->httpClient,
+ $this->urlGenerator,
+ $this->jobList,
+ $this->trustedServers,
+ $this->logger,
+ $this->dbHandler
+ ]
+ )->setMethods(['parentExecute'])->getMock();
+ $this->invokePrivate($getSharedSecret, 'argument', [['url' => 'url']]);
+
+ $this->jobList->expects($this->once())->method('remove');
+ $this->trustedServers->expects($this->once())->method('isTrustedServer')
+ ->with('url')->willReturn($isTrustedServer);
+ if ($isTrustedServer) {
+ $getSharedSecret->expects($this->once())->method('parentExecute');
+ } else {
+ $getSharedSecret->expects($this->never())->method('parentExecute');
+ }
+
+ $getSharedSecret->execute($this->jobList);
+
+ }
+
+ public function dataTestExecute() {
+ return [
+ [true],
+ [false]
+ ];
+ }
+
+ /**
+ * @dataProvider dataTestRun
+ *
+ * @param int $statusCode
+ */
+ public function testRun($statusCode) {
+
+ $target = 'targetURL';
+ $source = 'sourceURL';
+ $token = 'token';
+
+ $argument = ['url' => $target, 'token' => $token];
+
+ $this->urlGenerator->expects($this->once())->method('getAbsoluteURL')->with('/')
+ ->willReturn($source);
+ $this->httpClient->expects($this->once())->method('get')
+ ->with(
+ $target . '/ocs/v2.php/apps/federation/api/v1/shared-secret?format=json',
+ [
+ 'query' =>
+ [
+ 'url' => $source,
+ 'token' => $token
+ ],
+ 'timeout' => 3,
+ 'connect_timeout' => 3,
+ ]
+ )->willReturn($this->response);
+
+ $this->response->expects($this->once())->method('getStatusCode')
+ ->willReturn($statusCode);
+
+ if (
+ $statusCode !== Http::STATUS_OK
+ && $statusCode !== Http::STATUS_FORBIDDEN
+ ) {
+ $this->jobList->expects($this->once())->method('add')
+ ->with('OCA\Federation\BackgroundJob\GetSharedSecret', $argument);
+ $this->dbHandler->expects($this->never())->method('addToken');
+ } else {
+ $this->dbHandler->expects($this->once())->method('addToken')->with($target, '');
+ $this->jobList->expects($this->never())->method('add');
+ }
+
+ if ($statusCode === Http::STATUS_OK) {
+ $this->response->expects($this->once())->method('getBody')
+ ->willReturn('{"ocs":{"data":{"sharedSecret":"secret"}}}');
+ $this->trustedServers->expects($this->once())->method('addSharedSecret')
+ ->with($target, 'secret');
+ } else {
+ $this->trustedServers->expects($this->never())->method('addSharedSecret');
+ }
+
+ $this->invokePrivate($this->getSharedSecret, 'run', [$argument]);
+ }
+
+ public function dataTestRun() {
+ return [
+ [Http::STATUS_OK],
+ [Http::STATUS_FORBIDDEN],
+ [Http::STATUS_CONFLICT],
+ ];
+ }
+
+}
diff --git a/apps/federation/tests/backgroundjob/requestsharedsecrettest.php b/apps/federation/tests/backgroundjob/requestsharedsecrettest.php
new file mode 100644
index 0000000000..df81113c68
--- /dev/null
+++ b/apps/federation/tests/backgroundjob/requestsharedsecrettest.php
@@ -0,0 +1,169 @@
+
+ *
+ * @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
+ *
+ */
+
+
+namespace OCA\Federation\Tests\BackgroundJob;
+
+
+use OCA\Federation\BackgroundJob\RequestSharedSecret;
+use OCP\AppFramework\Http;
+use Test\TestCase;
+
+class RequestSharedSecretTest extends TestCase {
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | IClient */
+ private $httpClient;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | IJobList */
+ private $jobList;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | IURLGenerator */
+ private $urlGenerator;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | DbHandler */
+ private $dbHandler;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | TrustedServers */
+ private $trustedServers;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | IResponse */
+ private $response;
+
+ /** @var RequestSharedSecret */
+ private $requestSharedSecret;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->httpClient = $this->getMock('OCP\Http\Client\IClient');
+ $this->jobList = $this->getMock('OCP\BackgroundJob\IJobList');
+ $this->urlGenerator = $this->getMock('OCP\IURLGenerator');
+ $this->trustedServers = $this->getMockBuilder('OCA\Federation\TrustedServers')
+ ->disableOriginalConstructor()->getMock();
+ $this->dbHandler = $this->getMockBuilder('OCA\Federation\DbHandler')
+ ->disableOriginalConstructor()->getMock();
+ $this->response = $this->getMock('OCP\Http\Client\IResponse');
+
+ $this->requestSharedSecret = new RequestSharedSecret(
+ $this->httpClient,
+ $this->urlGenerator,
+ $this->jobList,
+ $this->trustedServers,
+ $this->dbHandler
+ );
+ }
+
+ /**
+ * @dataProvider dataTestExecute
+ *
+ * @param bool $isTrustedServer
+ */
+ public function testExecute($isTrustedServer) {
+ /** @var RequestSharedSecret |\PHPUnit_Framework_MockObject_MockObject $requestSharedSecret */
+ $requestSharedSecret = $this->getMockBuilder('OCA\Federation\BackgroundJob\RequestSharedSecret')
+ ->setConstructorArgs(
+ [
+ $this->httpClient,
+ $this->urlGenerator,
+ $this->jobList,
+ $this->trustedServers,
+ $this->dbHandler
+ ]
+ )->setMethods(['parentExecute'])->getMock();
+ $this->invokePrivate($requestSharedSecret, 'argument', [['url' => 'url']]);
+
+ $this->jobList->expects($this->once())->method('remove');
+ $this->trustedServers->expects($this->once())->method('isTrustedServer')
+ ->with('url')->willReturn($isTrustedServer);
+ if ($isTrustedServer) {
+ $requestSharedSecret->expects($this->once())->method('parentExecute');
+ } else {
+ $requestSharedSecret->expects($this->never())->method('parentExecute');
+ }
+
+ $requestSharedSecret->execute($this->jobList);
+
+ }
+
+ public function dataTestExecute() {
+ return [
+ [true],
+ [false]
+ ];
+ }
+
+ /**
+ * @dataProvider dataTestRun
+ *
+ * @param int $statusCode
+ */
+ public function testRun($statusCode) {
+
+ $target = 'targetURL';
+ $source = 'sourceURL';
+ $token = 'token';
+
+ $argument = ['url' => $target, 'token' => $token];
+
+ $this->urlGenerator->expects($this->once())->method('getAbsoluteURL')->with('/')
+ ->willReturn($source);
+ $this->httpClient->expects($this->once())->method('post')
+ ->with(
+ $target . '/ocs/v2.php/apps/federation/api/v1/request-shared-secret?format=json',
+ [
+ 'body' =>
+ [
+ 'url' => $source,
+ 'token' => $token
+ ],
+ 'timeout' => 3,
+ 'connect_timeout' => 3,
+ ]
+ )->willReturn($this->response);
+
+ $this->response->expects($this->once())->method('getStatusCode')
+ ->willReturn($statusCode);
+
+ if (
+ $statusCode !== Http::STATUS_OK
+ && $statusCode !== Http::STATUS_FORBIDDEN
+ ) {
+ $this->jobList->expects($this->once())->method('add')
+ ->with('OCA\Federation\BackgroundJob\RequestSharedSecret', $argument);
+ $this->dbHandler->expects($this->never())->method('addToken');
+ }
+
+ if ($statusCode === Http::STATUS_FORBIDDEN) {
+ $this->jobList->expects($this->never())->method('add');
+ $this->dbHandler->expects($this->once())->method('addToken')->with($target, '');
+ }
+
+ $this->invokePrivate($this->requestSharedSecret, 'run', [$argument]);
+ }
+
+ public function dataTestRun() {
+ return [
+ [Http::STATUS_OK],
+ [Http::STATUS_FORBIDDEN],
+ [Http::STATUS_CONFLICT],
+ ];
+ }
+}
diff --git a/apps/federation/tests/controller/settingscontrollertest.php b/apps/federation/tests/controller/settingscontrollertest.php
new file mode 100644
index 0000000000..efbc6911c5
--- /dev/null
+++ b/apps/federation/tests/controller/settingscontrollertest.php
@@ -0,0 +1,166 @@
+
+ *
+ * @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
+ *
+ */
+
+
+namespace OCA\Federation\Tests\Controller;
+
+
+use OCA\Federation\Controller\SettingsController;
+use OCP\AppFramework\Http\DataResponse;
+use Test\TestCase;
+
+class SettingsControllerTest extends TestCase {
+
+ /** @var SettingsController */
+ private $controller;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | \OCP\IRequest */
+ private $request;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | \OCP\IL10N */
+ private $l10n;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | \OCA\Federation\TrustedServers */
+ private $trustedServers;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->request = $this->getMock('OCP\IRequest');
+ $this->l10n = $this->getMock('OCP\IL10N');
+ $this->trustedServers = $this->getMockBuilder('OCA\Federation\TrustedServers')
+ ->disableOriginalConstructor()->getMock();
+
+ $this->controller = new SettingsController(
+ 'SettingsControllerTest',
+ $this->request,
+ $this->l10n,
+ $this->trustedServers
+ );
+ }
+
+ public function testAddServer() {
+ $this->trustedServers
+ ->expects($this->once())
+ ->method('isTrustedServer')
+ ->with('url')
+ ->willReturn(false);
+ $this->trustedServers
+ ->expects($this->once())
+ ->method('isOwnCloudServer')
+ ->with('url')
+ ->willReturn(true);
+
+ $result = $this->controller->addServer('url');
+ $this->assertTrue($result instanceof DataResponse);
+
+ $data = $result->getData();
+ $this->assertSame(200, $result->getStatus());
+ $this->assertSame('url', $data['url']);
+ $this->assertArrayHasKey('id', $data);
+ }
+
+ /**
+ * @dataProvider checkServerFails
+ * @expectedException \OC\HintException
+ *
+ * @param bool $isTrustedServer
+ * @param bool $isOwnCloud
+ */
+ public function testAddServerFail($isTrustedServer, $isOwnCloud) {
+ $this->trustedServers
+ ->expects($this->any())
+ ->method('isTrustedServer')
+ ->with('url')
+ ->willReturn($isTrustedServer);
+ $this->trustedServers
+ ->expects($this->any())
+ ->method('isOwnCloudServer')
+ ->with('url')
+ ->willReturn($isOwnCloud);
+
+ $this->controller->addServer('url');
+ }
+
+ public function testRemoveServer() {
+ $this->trustedServers->expects($this->once())->method('removeServer')
+ ->with('url');
+ $result = $this->controller->removeServer('url');
+ $this->assertTrue($result instanceof DataResponse);
+ $this->assertSame(200, $result->getStatus());
+ }
+
+ public function testCheckServer() {
+ $this->trustedServers
+ ->expects($this->once())
+ ->method('isTrustedServer')
+ ->with('url')
+ ->willReturn(false);
+ $this->trustedServers
+ ->expects($this->once())
+ ->method('isOwnCloudServer')
+ ->with('url')
+ ->willReturn(true);
+
+ $this->assertTrue(
+ $this->invokePrivate($this->controller, 'checkServer', ['url'])
+ );
+
+ }
+
+ /**
+ * @dataProvider checkServerFails
+ * @expectedException \OC\HintException
+ *
+ * @param bool $isTrustedServer
+ * @param bool $isOwnCloud
+ */
+ public function testCheckServerFail($isTrustedServer, $isOwnCloud) {
+ $this->trustedServers
+ ->expects($this->any())
+ ->method('isTrustedServer')
+ ->with('url')
+ ->willReturn($isTrustedServer);
+ $this->trustedServers
+ ->expects($this->any())
+ ->method('isOwnCloudServer')
+ ->with('url')
+ ->willReturn($isOwnCloud);
+
+ $this->assertTrue(
+ $this->invokePrivate($this->controller, 'checkServer', ['url'])
+ );
+
+ }
+
+ /**
+ * data to simulate checkServer fails
+ *
+ * @return array
+ */
+ public function checkServerFails() {
+ return [
+ [true, true],
+ [false, false]
+ ];
+ }
+
+}
diff --git a/apps/federation/tests/lib/dbhandlertest.php b/apps/federation/tests/lib/dbhandlertest.php
new file mode 100644
index 0000000000..123eaaee45
--- /dev/null
+++ b/apps/federation/tests/lib/dbhandlertest.php
@@ -0,0 +1,259 @@
+
+ *
+ * @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
+ *
+ */
+
+
+namespace OCA\Federation\Tests\lib;
+
+
+use OCA\Federation\DbHandler;
+use OCA\Federation\TrustedServers;
+use OCP\IDBConnection;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class DbHandlerTest extends TestCase {
+
+ /** @var DbHandler */
+ private $dbHandler;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject */
+ private $il10n;
+
+ /** @var IDBConnection */
+ private $connection;
+
+ /** @var string */
+ private $dbTable = 'trusted_servers';
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->connection = \OC::$server->getDatabaseConnection();
+ $this->il10n = $this->getMock('OCP\IL10N');
+
+ $this->dbHandler = new DbHandler(
+ $this->connection,
+ $this->il10n
+ );
+
+ $query = $this->connection->getQueryBuilder()->select('*')->from($this->dbTable);
+ $result = $query->execute()->fetchAll();
+ $this->assertEmpty($result, 'we need to start with a empty trusted_servers table');
+ }
+
+ public function tearDown() {
+ parent::tearDown();
+ $query = $this->connection->getQueryBuilder()->delete($this->dbTable);
+ $query->execute();
+ }
+
+ /**
+ * @dataProvider dataTestAddServer
+ *
+ * @param string $url passed to the method
+ * @param string $expectedUrl the url we expect to be written to the db
+ * @param string $expectedHash the hash value we expect to be written to the db
+ */
+ public function testAddServer($url, $expectedUrl, $expectedHash) {
+ $id = $this->dbHandler->addServer($url);
+
+ $query = $this->connection->getQueryBuilder()->select('*')->from($this->dbTable);
+ $result = $query->execute()->fetchAll();
+ $this->assertSame(1, count($result));
+ $this->assertSame($expectedUrl, $result[0]['url']);
+ $this->assertSame($id, (int)$result[0]['id']);
+ $this->assertSame($expectedHash, $result[0]['url_hash']);
+ $this->assertSame(TrustedServers::STATUS_PENDING, (int)$result[0]['status']);
+ }
+
+ public function dataTestAddServer() {
+ return [
+ ['http://owncloud.org', 'http://owncloud.org', md5('owncloud.org')],
+ ['https://owncloud.org', 'https://owncloud.org', md5('owncloud.org')],
+ ['http://owncloud.org/', 'http://owncloud.org', md5('owncloud.org')],
+ ];
+ }
+
+ public function testRemove() {
+ $id1 = $this->dbHandler->addServer('server1');
+ $id2 = $this->dbHandler->addServer('server2');
+
+ $query = $this->connection->getQueryBuilder()->select('*')->from($this->dbTable);
+ $result = $query->execute()->fetchAll();
+ $this->assertSame(2, count($result));
+ $this->assertSame('server1', $result[0]['url']);
+ $this->assertSame('server2', $result[1]['url']);
+ $this->assertSame($id1, (int)$result[0]['id']);
+ $this->assertSame($id2, (int)$result[1]['id']);
+
+ $this->dbHandler->removeServer($id2);
+ $query = $this->connection->getQueryBuilder()->select('*')->from($this->dbTable);
+ $result = $query->execute()->fetchAll();
+ $this->assertSame(1, count($result));
+ $this->assertSame('server1', $result[0]['url']);
+ $this->assertSame($id1, (int)$result[0]['id']);
+ }
+
+ public function testGetAll() {
+ $id1 = $this->dbHandler->addServer('server1');
+ $id2 = $this->dbHandler->addServer('server2');
+
+ $result = $this->dbHandler->getAllServer();
+ $this->assertSame(2, count($result));
+ $this->assertSame('server1', $result[0]['url']);
+ $this->assertSame('server2', $result[1]['url']);
+ $this->assertSame($id1, (int)$result[0]['id']);
+ $this->assertSame($id2, (int)$result[1]['id']);
+ }
+
+ /**
+ * @dataProvider dataTestServerExists
+ *
+ * @param string $serverInTable
+ * @param string $checkForServer
+ * @param bool $expected
+ */
+ public function testServerExists($serverInTable, $checkForServer, $expected) {
+ $this->dbHandler->addServer($serverInTable);
+ $this->assertSame($expected,
+ $this->dbHandler->serverExists($checkForServer)
+ );
+ }
+
+ public function dataTestServerExists() {
+ return [
+ ['server1', 'server1', true],
+ ['server1', 'http://server1', true],
+ ['server1', 'server2', false]
+ ];
+ }
+
+ public function testAddToken() {
+ $this->dbHandler->addServer('server1');
+ $query = $this->connection->getQueryBuilder()->select('*')->from($this->dbTable);
+ $result = $query->execute()->fetchAll();
+ $this->assertSame(1, count($result));
+ $this->assertSame(null, $result[0]['token']);
+ $this->dbHandler->addToken('http://server1', 'token');
+ $query = $this->connection->getQueryBuilder()->select('*')->from($this->dbTable);
+ $result = $query->execute()->fetchAll();
+ $this->assertSame(1, count($result));
+ $this->assertSame('token', $result[0]['token']);
+ }
+
+ public function testGetToken() {
+ $this->dbHandler->addServer('server1');
+ $this->dbHandler->addToken('http://server1', 'token');
+ $this->assertSame('token',
+ $this->dbHandler->getToken('https://server1')
+ );
+ }
+
+ public function testAddSharedSecret() {
+ $this->dbHandler->addServer('server1');
+ $query = $this->connection->getQueryBuilder()->select('*')->from($this->dbTable);
+ $result = $query->execute()->fetchAll();
+ $this->assertSame(1, count($result));
+ $this->assertSame(null, $result[0]['shared_secret']);
+ $this->dbHandler->addSharedSecret('http://server1', 'secret');
+ $query = $this->connection->getQueryBuilder()->select('*')->from($this->dbTable);
+ $result = $query->execute()->fetchAll();
+ $this->assertSame(1, count($result));
+ $this->assertSame('secret', $result[0]['shared_secret']);
+ }
+
+ public function testGetSharedSecret() {
+ $this->dbHandler->addServer('server1');
+ $this->dbHandler->addSharedSecret('http://server1', 'secret');
+ $this->assertSame('secret',
+ $this->dbHandler->getSharedSecret('https://server1')
+ );
+ }
+
+ public function testSetServerStatus() {
+ $this->dbHandler->addServer('server1');
+ $query = $this->connection->getQueryBuilder()->select('*')->from($this->dbTable);
+ $result = $query->execute()->fetchAll();
+ $this->assertSame(1, count($result));
+ $this->assertSame(TrustedServers::STATUS_PENDING, (int)$result[0]['status']);
+ $this->dbHandler->setServerStatus('http://server1', TrustedServers::STATUS_OK);
+ $query = $this->connection->getQueryBuilder()->select('*')->from($this->dbTable);
+ $result = $query->execute()->fetchAll();
+ $this->assertSame(1, count($result));
+ $this->assertSame(TrustedServers::STATUS_OK, (int)$result[0]['status']);
+ }
+
+ public function testGetServerStatus() {
+ $this->dbHandler->addServer('server1');
+ $this->dbHandler->setServerStatus('http://server1', TrustedServers::STATUS_OK);
+ $this->assertSame(TrustedServers::STATUS_OK,
+ $this->dbHandler->getServerStatus('https://server1')
+ );
+ }
+
+ /**
+ * hash should always be computed with the normalized URL
+ *
+ * @dataProvider dataTestHash
+ *
+ * @param string $url
+ * @param string $expected
+ */
+ public function testHash($url, $expected) {
+ $this->assertSame($expected,
+ $this->invokePrivate($this->dbHandler, 'hash', [$url])
+ );
+ }
+
+ public function dataTestHash() {
+ return [
+ ['server1', md5('server1')],
+ ['http://server1', md5('server1')],
+ ['https://server1', md5('server1')],
+ ['http://server1/', md5('server1')],
+ ];
+ }
+
+ /**
+ * @dataProvider dataTestNormalizeUrl
+ *
+ * @param string $url
+ * @param string $expected
+ */
+ public function testNormalizeUrl($url, $expected) {
+ $this->assertSame($expected,
+ $this->invokePrivate($this->dbHandler, 'normalizeUrl', [$url])
+ );
+ }
+
+ public function dataTestNormalizeUrl() {
+ return [
+ ['owncloud.org', 'owncloud.org'],
+ ['http://owncloud.org', 'owncloud.org'],
+ ['https://owncloud.org', 'owncloud.org'],
+ ['https://owncloud.org//mycloud', 'owncloud.org/mycloud'],
+ ['https://owncloud.org/mycloud/', 'owncloud.org/mycloud'],
+ ];
+ }
+
+}
diff --git a/apps/federation/tests/lib/hookstest.php b/apps/federation/tests/lib/hookstest.php
new file mode 100644
index 0000000000..5b19c16745
--- /dev/null
+++ b/apps/federation/tests/lib/hookstest.php
@@ -0,0 +1,79 @@
+
+ *
+ * @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
+ *
+ */
+
+
+namespace OCA\Federation\Tests\lib;
+
+
+use OCA\Federation\Hooks;
+use OCA\Federation\TrustedServers;
+use Test\TestCase;
+
+class HooksTest extends TestCase {
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | TrustedServers */
+ private $trustedServers;
+
+ /** @var Hooks */
+ private $hooks;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->trustedServers = $this->getMockBuilder('OCA\Federation\TrustedServers')
+ ->disableOriginalConstructor()->getMock();
+
+ $this->hooks = new Hooks($this->trustedServers);
+ }
+
+ /**
+ * @dataProvider dataTestAddServerHook
+ *
+ * @param bool $autoAddEnabled is auto-add enabled
+ * @param bool $isTrustedServer is the server already in the list of trusted servers
+ * @param bool $addServer should the server be added
+ */
+ public function testAddServerHook($autoAddEnabled, $isTrustedServer, $addServer) {
+ $this->trustedServers->expects($this->any())->method('getAutoAddServers')
+ ->willReturn($autoAddEnabled);
+ $this->trustedServers->expects($this->any())->method('isTrustedServer')
+ ->with('url')->willReturn($isTrustedServer);
+
+ if ($addServer) {
+ $this->trustedServers->expects($this->once())->method('addServer')
+ ->with('url');
+ } else {
+ $this->trustedServers->expects($this->never())->method('addServer');
+ }
+
+ $this->hooks->addServerHook(['server' => 'url']);
+
+ }
+
+ public function dataTestAddServerHook() {
+ return [
+ [true, true, false],
+ [false, true, false],
+ [true, false, true],
+ [false, false, false],
+ ];
+ }
+}
diff --git a/apps/federation/tests/lib/trustedserverstest.php b/apps/federation/tests/lib/trustedserverstest.php
new file mode 100644
index 0000000000..d067cd1c18
--- /dev/null
+++ b/apps/federation/tests/lib/trustedserverstest.php
@@ -0,0 +1,344 @@
+
+ *
+ * @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
+ *
+ */
+
+
+namespace OCA\Federation\Tests\lib;
+
+
+use OCA\Federation\DbHandler;
+use OCA\Federation\TrustedServers;
+use OCP\BackgroundJob\IJobList;
+use OCP\Http\Client\IClient;
+use OCP\Http\Client\IClientService;
+use OCP\Http\Client\IResponse;
+use OCP\IConfig;
+use OCP\ILogger;
+use OCP\Security\ISecureRandom;
+use Test\TestCase;
+
+class TrustedServersTest extends TestCase {
+
+ /** @var TrustedServers */
+ private $trustedServers;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | DbHandler */
+ private $dbHandler;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | IClientService */
+ private $httpClientService;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | IClient */
+ private $httpClient;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | IResponse */
+ private $response;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | ILogger */
+ private $logger;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | IJobList */
+ private $jobList;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | ISecureRandom */
+ private $secureRandom;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | IConfig */
+ private $config;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->dbHandler = $this->getMockBuilder('\OCA\Federation\DbHandler')
+ ->disableOriginalConstructor()->getMock();
+ $this->httpClientService = $this->getMock('OCP\Http\Client\IClientService');
+ $this->httpClient = $this->getMock('OCP\Http\Client\IClient');
+ $this->response = $this->getMock('OCP\Http\Client\IResponse');
+ $this->logger = $this->getMock('OCP\ILogger');
+ $this->jobList = $this->getMock('OCP\BackgroundJob\IJobList');
+ $this->secureRandom = $this->getMock('OCP\Security\ISecureRandom');
+ $this->config = $this->getMock('OCP\IConfig');
+
+ $this->trustedServers = new TrustedServers(
+ $this->dbHandler,
+ $this->httpClientService,
+ $this->logger,
+ $this->jobList,
+ $this->secureRandom,
+ $this->config
+ );
+
+ }
+
+ /**
+ * @dataProvider dataTrueFalse
+ *
+ * @param bool $success
+ */
+ public function testAddServer($success) {
+ /** @var \PHPUnit_Framework_MockObject_MockObject | TrustedServers $trustedServer */
+ $trustedServers = $this->getMockBuilder('OCA\Federation\TrustedServers')
+ ->setConstructorArgs(
+ [
+ $this->dbHandler,
+ $this->httpClientService,
+ $this->logger,
+ $this->jobList,
+ $this->secureRandom,
+ $this->config
+ ]
+ )
+ ->setMethods(['normalizeUrl', 'updateProtocol'])
+ ->getMock();
+ $trustedServers->expects($this->once())->method('updateProtocol')
+ ->with('url')->willReturn('https://url');
+ $this->dbHandler->expects($this->once())->method('addServer')->with('https://url')
+ ->willReturn($success);
+
+ if ($success) {
+ $this->secureRandom->expects($this->once())->method('getMediumStrengthGenerator')
+ ->willReturn($this->secureRandom);
+ $this->secureRandom->expects($this->once())->method('generate')
+ ->willReturn('token');
+ $this->dbHandler->expects($this->once())->method('addToken')->with('https://url', 'token');
+ $this->jobList->expects($this->once())->method('add')
+ ->with('OCA\Federation\BackgroundJob\RequestSharedSecret',
+ ['url' => 'https://url', 'token' => 'token']);
+ } else {
+ $this->jobList->expects($this->never())->method('add');
+ }
+
+ $this->assertSame($success,
+ $trustedServers->addServer('url')
+ );
+ }
+
+ public function dataTrueFalse() {
+ return [
+ [true],
+ [false]
+ ];
+ }
+
+ /**
+ * @dataProvider dataTrueFalse
+ *
+ * @param bool $status
+ */
+ public function testSetAutoAddServers($status) {
+ if ($status) {
+ $this->config->expects($this->once())->method('setAppValue')
+ ->with('federation', 'autoAddServers', '1');
+ } else {
+ $this->config->expects($this->once())->method('setAppValue')
+ ->with('federation', 'autoAddServers', '0');
+ }
+
+ $this->trustedServers->setAutoAddServers($status);
+ }
+
+ /**
+ * @dataProvider dataTestGetAutoAddServers
+ *
+ * @param string $status
+ * @param bool $expected
+ */
+ public function testGetAutoAddServers($status, $expected) {
+ $this->config->expects($this->once())->method('getAppValue')
+ ->with('federation', 'autoAddServers', '1')->willReturn($status);
+
+ $this->assertSame($expected,
+ $this->trustedServers->getAutoAddServers($status)
+ );
+ }
+
+ public function dataTestGetAutoAddServers() {
+ return [
+ ['1', true],
+ ['0', false]
+ ];
+ }
+
+ public function testAddSharedSecret() {
+ $this->dbHandler->expects($this->once())->method('addSharedSecret')
+ ->with('url', 'secret');
+ $this->trustedServers->addSharedSecret('url', 'secret');
+ }
+
+ public function testGetSharedSecret() {
+ $this->dbHandler->expects($this->once())->method('getSharedSecret')
+ ->with('url')->willReturn(true);
+ $this->assertTrue(
+ $this->trustedServers->getSharedSecret('url')
+ );
+ }
+
+ public function testRemoveServer() {
+ $id = 42;
+ $this->dbHandler->expects($this->once())->method('removeServer')->with($id);
+ $this->trustedServers->removeServer($id);
+ }
+
+ public function testGetServers() {
+ $this->dbHandler->expects($this->once())->method('getAllServer')->willReturn(true);
+
+ $this->assertTrue(
+ $this->trustedServers->getServers()
+ );
+ }
+
+
+ public function testIsTrustedServer() {
+ $this->dbHandler->expects($this->once())->method('serverExists')->with('url')
+ ->willReturn(true);
+
+ $this->assertTrue(
+ $this->trustedServers->isTrustedServer('url')
+ );
+ }
+
+ public function testSetServerStatus() {
+ $this->dbHandler->expects($this->once())->method('setServerStatus')
+ ->with('url', 'status');
+ $this->trustedServers->setServerStatus('url', 'status');
+ }
+
+ public function testGetServerStatus() {
+ $this->dbHandler->expects($this->once())->method('getServerStatus')
+ ->with('url')->willReturn(true);
+ $this->assertTrue(
+ $this->trustedServers->getServerStatus('url')
+ );
+ }
+
+ /**
+ * @dataProvider dataTestIsOwnCloudServer
+ *
+ * @param int $statusCode
+ * @param bool $isValidOwnCloudVersion
+ * @param bool $expected
+ */
+ public function testIsOwnCloudServer($statusCode, $isValidOwnCloudVersion, $expected) {
+
+ $server = 'server1';
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | TrustedServers $trustedServer */
+ $trustedServers = $this->getMockBuilder('OCA\Federation\TrustedServers')
+ ->setConstructorArgs(
+ [
+ $this->dbHandler,
+ $this->httpClientService,
+ $this->logger,
+ $this->jobList,
+ $this->secureRandom,
+ $this->config
+ ]
+ )
+ ->setMethods(['checkOwnCloudVersion'])
+ ->getMock();
+
+ $this->httpClientService->expects($this->once())->method('newClient')
+ ->willReturn($this->httpClient);
+
+ $this->httpClient->expects($this->once())->method('get')->with($server . '/status.php')
+ ->willReturn($this->response);
+
+ $this->response->expects($this->once())->method('getStatusCode')
+ ->willReturn($statusCode);
+
+ if ($statusCode === 200) {
+ $trustedServers->expects($this->once())->method('checkOwnCloudVersion')
+ ->willReturn($isValidOwnCloudVersion);
+ } else {
+ $trustedServers->expects($this->never())->method('checkOwnCloudVersion');
+ }
+
+ $this->assertSame($expected,
+ $trustedServers->isOwnCloudServer($server)
+ );
+
+ }
+
+ public function dataTestIsOwnCloudServer() {
+ return [
+ [200, true, true],
+ [200, false, false],
+ [404, true, false],
+ ];
+ }
+
+ public function testIsOwnCloudServerFail() {
+ $server = 'server1';
+
+ $this->httpClientService->expects($this->once())->method('newClient')
+ ->willReturn($this->httpClient);
+
+ $this->logger->expects($this->once())->method('error')
+ ->with('simulated exception', ['app' => 'federation']);
+
+ $this->httpClient->expects($this->once())->method('get')->with($server . '/status.php')
+ ->willReturnCallback(function () {
+ throw new \Exception('simulated exception');
+ });
+
+ $this->assertFalse($this->trustedServers->isOwnCloudServer($server));
+
+ }
+
+ /**
+ * @dataProvider dataTestCheckOwnCloudVersion
+ *
+ * @param $statusphp
+ * @param $expected
+ */
+ public function testCheckOwnCloudVersion($statusphp, $expected) {
+ $this->assertSame($expected,
+ $this->invokePrivate($this->trustedServers, 'checkOwnCloudVersion', [$statusphp])
+ );
+ }
+
+ public function dataTestCheckOwnCloudVersion() {
+ return [
+ ['{"version":"8.4.0"}', false],
+ ['{"version":"9.0.0"}', true],
+ ['{"version":"9.1.0"}', true]
+ ];
+ }
+
+ /**
+ * @dataProvider dataTestUpdateProtocol
+ * @param string $url
+ * @param string $expected
+ */
+ public function testUpdateProtocol($url, $expected) {
+ $this->assertSame($expected,
+ $this->invokePrivate($this->trustedServers, 'updateProtocol', [$url])
+ );
+ }
+
+ public function dataTestUpdateProtocol() {
+ return [
+ ['http://owncloud.org', 'http://owncloud.org'],
+ ['https://owncloud.org', 'https://owncloud.org'],
+ ['owncloud.org', 'https://owncloud.org'],
+ ['httpserver', 'https://httpserver'],
+ ];
+ }
+}
diff --git a/apps/federation/tests/middleware/addservermiddlewaretest.php b/apps/federation/tests/middleware/addservermiddlewaretest.php
new file mode 100644
index 0000000000..1be5a34228
--- /dev/null
+++ b/apps/federation/tests/middleware/addservermiddlewaretest.php
@@ -0,0 +1,100 @@
+
+ *
+ * @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
+ *
+ */
+
+
+namespace OCA\Federation\Tests\Middleware;
+
+
+use OC\HintException;
+use OCA\Federation\Middleware\AddServerMiddleware;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http;
+use Test\TestCase;
+
+class AddServerMiddlewareTest extends TestCase {
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | ILogger */
+ private $logger;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | \OCP\IL10N */
+ private $l10n;
+
+ /** @var AddServerMiddleware */
+ private $middleware;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject | Controller */
+ private $controller;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->logger = $this->getMock('OCP\ILogger');
+ $this->l10n = $this->getMock('OCP\IL10N');
+ $this->controller = $this->getMockBuilder('OCP\AppFramework\Controller')
+ ->disableOriginalConstructor()->getMock();
+
+ $this->middleware = new AddServerMiddleware(
+ 'AddServerMiddlewareTest',
+ $this->l10n,
+ $this->logger
+ );
+ }
+
+ /**
+ * @dataProvider dataTestAfterException
+ *
+ * @param \Exception $exception
+ * @param string $message
+ * @param string $hint
+ */
+ public function testAfterException($exception, $message, $hint) {
+
+ $this->logger->expects($this->once())->method('error')
+ ->with($message, ['app' => 'AddServerMiddlewareTest']);
+
+ $this->l10n->expects($this->any())->method('t')
+ ->willReturnCallback(
+ function($message) {
+ return $message;
+ }
+ );
+
+ $result = $this->middleware->afterException($this->controller, 'method', $exception);
+
+ $this->assertSame(Http::STATUS_BAD_REQUEST,
+ $result->getStatus()
+ );
+
+ $data = $result->getData();
+
+ $this->assertSame($hint,
+ $data['message']
+ );
+ }
+
+ public function dataTestAfterException() {
+ return [
+ [new HintException('message', 'hint'), 'message', 'hint'],
+ [new \Exception('message'), 'message', 'Unknown error'],
+ ];
+ }
+
+}
diff --git a/apps/files/ajax/delete.php b/apps/files/ajax/delete.php
deleted file mode 100644
index 2d02869df1..0000000000
--- a/apps/files/ajax/delete.php
+++ /dev/null
@@ -1,81 +0,0 @@
-
- * @author Frank Karlitschek
- * @author Jakob Sack
- * @author Joas Schilling
- * @author Jörn Friedrich Dreyer
- * @author Lukas Reschke
- * @author Robin Appelman
- * @author Thomas Müller
- * @author Vincent Petry
- *
- * @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
- *
- */
-OCP\JSON::checkLoggedIn();
-OCP\JSON::callCheck();
-\OC::$server->getSession()->close();
-
-
-// Get data
-$dir = isset($_POST['dir']) ? (string)$_POST['dir'] : '';
-$allFiles = isset($_POST["allfiles"]) ? (string)$_POST["allfiles"] : false;
-
-// delete all files in dir ?
-if ($allFiles === 'true') {
- $files = array();
- $fileList = \OC\Files\Filesystem::getDirectoryContent($dir);
- foreach ($fileList as $fileInfo) {
- $files[] = $fileInfo['name'];
- }
-} else {
- $files = isset($_POST["file"]) ? (string)$_POST["file"] : (string)$_POST["files"];
- $files = json_decode($files);
-}
-$filesWithError = '';
-
-$success = true;
-
-//Now delete
-foreach ($files as $file) {
- try {
- if (\OC\Files\Filesystem::file_exists($dir . '/' . $file) &&
- !(\OC\Files\Filesystem::isDeletable($dir . '/' . $file) &&
- \OC\Files\Filesystem::unlink($dir . '/' . $file))
- ) {
- $filesWithError .= $file . "\n";
- $success = false;
- }
- } catch (\Exception $e) {
- $filesWithError .= $file . "\n";
- $success = false;
- }
-}
-
-// get array with updated storage stats (e.g. max file size) after upload
-try {
- $storageStats = \OCA\Files\Helper::buildFileStorageStatistics($dir);
-} catch(\OCP\Files\NotFoundException $e) {
- OCP\JSON::error(['data' => ['message' => 'File not found']]);
- return;
-}
-
-if ($success) {
- OCP\JSON::success(array("data" => array_merge(array("dir" => $dir, "files" => $files), $storageStats)));
-} else {
- OCP\JSON::error(array("data" => array_merge(array("message" => "Could not delete:\n" . $filesWithError), $storageStats)));
-}
diff --git a/apps/files/ajax/move.php b/apps/files/ajax/move.php
deleted file mode 100644
index 0961636a11..0000000000
--- a/apps/files/ajax/move.php
+++ /dev/null
@@ -1,59 +0,0 @@
-
- * @author Frank Karlitschek
- * @author Georg Ehrke
- * @author Jörn Friedrich Dreyer
- * @author Lukas Reschke
- * @author Robin Appelman
- * @author Vincent Petry
- *
- * @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
- *
- */
-OCP\JSON::checkLoggedIn();
-OCP\JSON::callCheck();
-\OC::$server->getSession()->close();
-
-// Get data
-$dir = isset($_POST['dir']) ? (string)$_POST['dir'] : '';
-$file = isset($_POST['file']) ? (string)$_POST['file'] : '';
-$target = isset($_POST['target']) ? rawurldecode((string)$_POST['target']) : '';
-
-$l = \OC::$server->getL10N('files');
-
-if(\OC\Files\Filesystem::file_exists($target . '/' . $file)) {
- OCP\JSON::error(array("data" => array( "message" => $l->t("Could not move %s - File with this name already exists", array($file)) )));
- exit;
-}
-
-if ($target != '' || strtolower($file) != 'shared') {
- $targetFile = \OC\Files\Filesystem::normalizePath($target . '/' . $file);
- $sourceFile = \OC\Files\Filesystem::normalizePath($dir . '/' . $file);
- try {
- if(\OC\Files\Filesystem::rename($sourceFile, $targetFile)) {
- OCP\JSON::success(array("data" => array( "dir" => $dir, "files" => $file )));
- } else {
- OCP\JSON::error(array("data" => array( "message" => $l->t("Could not move %s", array($file)) )));
- }
- } catch (\OCP\Files\NotPermittedException $e) {
- OCP\JSON::error(array("data" => array( "message" => $l->t("Permission denied") )));
- } catch (\Exception $e) {
- OCP\JSON::error(array("data" => array( "message" => $e->getMessage())));
- }
-}else{
- OCP\JSON::error(array("data" => array( "message" => $l->t("Could not move %s", array($file)) )));
-}
diff --git a/apps/files/ajax/newfile.php b/apps/files/ajax/newfile.php
deleted file mode 100644
index be09b288d4..0000000000
--- a/apps/files/ajax/newfile.php
+++ /dev/null
@@ -1,103 +0,0 @@
-
- * @author Georg Ehrke
- * @author Jörn Friedrich Dreyer
- * @author Lukas Reschke
- * @author Robin Appelman
- * @author Thomas Müller
- * @author Vincent Petry
- *
- * @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
- *
- */
-// Init owncloud
-global $eventSource;
-
-\OCP\JSON::checkLoggedIn();
-\OCP\JSON::callCheck();
-
-\OC::$server->getSession()->close();
-
-// Get the params
-$dir = isset( $_REQUEST['dir'] ) ? '/'.trim((string)$_REQUEST['dir'], '/\\') : '';
-$fileName = isset( $_REQUEST['filename'] ) ? trim((string)$_REQUEST['filename'], '/\\') : '';
-
-$l10n = \OC::$server->getL10N('files');
-
-$result = array(
- 'success' => false,
- 'data' => NULL
-);
-
-try {
- \OC\Files\Filesystem::getView()->verifyPath($dir, $fileName);
-} catch (\OCP\Files\InvalidPathException $ex) {
- $result['data'] = [
- 'message' => $ex->getMessage()];
- OCP\JSON::error($result);
- return;
-}
-
-if (!\OC\Files\Filesystem::file_exists($dir . '/')) {
- $result['data'] = array('message' => (string)$l10n->t(
- 'The target folder has been moved or deleted.'),
- 'code' => 'targetnotfound'
- );
- OCP\JSON::error($result);
- exit();
-}
-
-$target = $dir.'/'.$fileName;
-
-if (\OC\Files\Filesystem::file_exists($target)) {
- $result['data'] = array('message' => (string)$l10n->t(
- 'The name %s is already used in the folder %s. Please choose a different name.',
- array($fileName, $dir))
- );
- OCP\JSON::error($result);
- exit();
-}
-
-$success = false;
-$templateManager = OC_Helper::getFileTemplateManager();
-$mimeType = OC_Helper::getMimetypeDetector()->detectPath($target);
-$content = $templateManager->getTemplate($mimeType);
-
-try {
- if($content) {
- $success = \OC\Files\Filesystem::file_put_contents($target, $content);
- } else {
- $success = \OC\Files\Filesystem::touch($target);
- }
-} catch (\Exception $e) {
- $result = [
- 'success' => false,
- 'data' => [
- 'message' => $e->getMessage()
- ]
- ];
- OCP\JSON::error($result);
- exit();
-}
-
-if($success) {
- $meta = \OC\Files\Filesystem::getFileInfo($target);
- OCP\JSON::success(array('data' => \OCA\Files\Helper::formatFileInfo($meta)));
- return;
-}
-
-OCP\JSON::error(array('data' => array( 'message' => $l10n->t('Error when creating the file') )));
diff --git a/apps/files/ajax/newfolder.php b/apps/files/ajax/newfolder.php
deleted file mode 100644
index a2897dd437..0000000000
--- a/apps/files/ajax/newfolder.php
+++ /dev/null
@@ -1,99 +0,0 @@
-
- * @author Björn Schießle
- * @author Frank Karlitschek
- * @author Georg Ehrke
- * @author Jörn Friedrich Dreyer
- * @author Lukas Reschke
- * @author Robin Appelman
- * @author Thomas Müller
- * @author Vincent Petry
- *
- * @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
- *
- */
-// Init owncloud
-
-
-OCP\JSON::checkLoggedIn();
-OCP\JSON::callCheck();
-\OC::$server->getSession()->close();
-
-// Get the params
-$dir = isset($_POST['dir']) ? (string)$_POST['dir'] : '';
-$folderName = isset($_POST['foldername']) ?(string) $_POST['foldername'] : '';
-
-$l10n = \OC::$server->getL10N('files');
-
-$result = array(
- 'success' => false,
- 'data' => NULL
- );
-
-try {
- \OC\Files\Filesystem::getView()->verifyPath($dir, $folderName);
-} catch (\OCP\Files\InvalidPathException $ex) {
- $result['data'] = [
- 'message' => $ex->getMessage()];
- OCP\JSON::error($result);
- return;
-}
-
-if (!\OC\Files\Filesystem::file_exists($dir . '/')) {
- $result['data'] = array('message' => (string)$l10n->t(
- 'The target folder has been moved or deleted.'),
- 'code' => 'targetnotfound'
- );
- OCP\JSON::error($result);
- exit();
-}
-
-$target = $dir . '/' . $folderName;
-
-if (\OC\Files\Filesystem::file_exists($target)) {
- $result['data'] = array('message' => $l10n->t(
- 'The name %s is already used in the folder %s. Please choose a different name.',
- array($folderName, $dir))
- );
- OCP\JSON::error($result);
- exit();
-}
-
-try {
- if(\OC\Files\Filesystem::mkdir($target)) {
- if ( $dir !== '/') {
- $path = $dir.'/'.$folderName;
- } else {
- $path = '/'.$folderName;
- }
- $meta = \OC\Files\Filesystem::getFileInfo($path);
- $meta['type'] = 'dir'; // missing ?!
- OCP\JSON::success(array('data' => \OCA\Files\Helper::formatFileInfo($meta)));
- exit();
- }
-} catch (\Exception $e) {
- $result = [
- 'success' => false,
- 'data' => [
- 'message' => $e->getMessage()
- ]
- ];
- OCP\JSON::error($result);
- exit();
-}
-
-OCP\JSON::error(array('data' => array( 'message' => $l10n->t('Error when creating the folder') )));
diff --git a/apps/files/ajax/rename.php b/apps/files/ajax/rename.php
deleted file mode 100644
index a24a57b104..0000000000
--- a/apps/files/ajax/rename.php
+++ /dev/null
@@ -1,58 +0,0 @@
-
- * @author Frank Karlitschek
- * @author Jakob Sack
- * @author Jörn Friedrich Dreyer
- * @author Lukas Reschke
- * @author Morris Jobke
- * @author Robin Appelman
- * @author Vincent Petry
- *
- * @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
- *
- */
-
-OCP\JSON::checkLoggedIn();
-OCP\JSON::callCheck();
-\OC::$server->getSession()->close();
-
-$l10n = \OC::$server->getL10N('files');
-
-$files = new \OCA\Files\App(
- \OC\Files\Filesystem::getView(),
- \OC::$server->getL10N('files')
-);
-try {
- $result = $files->rename(
- isset($_GET['dir']) ? (string)$_GET['dir'] : '',
- isset($_GET['file']) ? (string)$_GET['file'] : '',
- isset($_GET['newname']) ? (string)$_GET['newname'] : ''
- );
-} catch (\Exception $e) {
- $result = [
- 'success' => false,
- 'data' => [
- 'message' => $e->getMessage()
- ]
- ];
-}
-
-if($result['success'] === true){
- OCP\JSON::success(['data' => $result['data']]);
-} else {
- OCP\JSON::error(['data' => $result['data']]);
-}
diff --git a/apps/files/ajax/scan.php b/apps/files/ajax/scan.php
index 491adaa9b8..7710a28a8c 100644
--- a/apps/files/ajax/scan.php
+++ b/apps/files/ajax/scan.php
@@ -47,9 +47,8 @@ $listener = new ScanListener($eventSource);
foreach ($users as $user) {
$eventSource->send('user', $user);
- $scanner = new \OC\Files\Utils\Scanner($user, \OC::$server->getDatabaseConnection());
+ $scanner = new \OC\Files\Utils\Scanner($user, \OC::$server->getDatabaseConnection(), \OC::$server->getLogger());
$scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', array($listener, 'file'));
- $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', array($listener, 'folder'));
try {
if ($force) {
$scanner->scan($dir);
@@ -81,13 +80,6 @@ class ScanListener {
$this->eventSource = $eventSource;
}
- /**
- * @param string $path
- */
- public function folder($path) {
- $this->eventSource->send('folder', $path);
- }
-
public function file() {
$this->fileCount++;
if ($this->fileCount > $this->lastCount + 20) { //send a count update every 20 files
diff --git a/apps/files/ajax/upload.php b/apps/files/ajax/upload.php
index a784642728..18e9cfe611 100644
--- a/apps/files/ajax/upload.php
+++ b/apps/files/ajax/upload.php
@@ -41,7 +41,6 @@ OCP\JSON::setContentTypeHeader('text/plain');
// If not, check the login.
// If no token is sent along, rely on login only
-$allowedPermissions = \OCP\Constants::PERMISSION_ALL;
$errorCode = null;
$l = \OC::$server->getL10N('files');
@@ -60,8 +59,6 @@ if (empty($_POST['dirToken'])) {
\OC_User::setIncognitoMode(true);
- // return only read permissions for public upload
- $allowedPermissions = \OCP\Constants::PERMISSION_READ;
$publicDirectory = !empty($_POST['subdir']) ? (string)$_POST['subdir'] : '/';
$linkItem = OCP\Share::getShareByToken((string)$_POST['dirToken']);
@@ -207,7 +204,7 @@ if (\OC\Files\Filesystem::isValidPath($dir) === true) {
$data['originalname'] = $files['name'][$i];
$data['uploadMaxFilesize'] = $maxUploadFileSize;
$data['maxHumanFilesize'] = $maxHumanFileSize;
- $data['permissions'] = $meta['permissions'] & $allowedPermissions;
+ $data['permissions'] = $meta['permissions'];
$data['directory'] = $returnedDir;
$result[] = $data;
}
@@ -234,7 +231,7 @@ if (\OC\Files\Filesystem::isValidPath($dir) === true) {
$data['originalname'] = $files['name'][$i];
$data['uploadMaxFilesize'] = $maxUploadFileSize;
$data['maxHumanFilesize'] = $maxHumanFileSize;
- $data['permissions'] = $meta['permissions'] & $allowedPermissions;
+ $data['permissions'] = $meta['permissions'];
$data['directory'] = $returnedDir;
$result[] = $data;
}
diff --git a/apps/files/appinfo/app.php b/apps/files/appinfo/app.php
index 40b194ab88..61ff6d748f 100644
--- a/apps/files/appinfo/app.php
+++ b/apps/files/appinfo/app.php
@@ -25,12 +25,14 @@
*/
\OCP\App::registerAdmin('files', 'admin');
+
\OC::$server->getNavigationManager()->add(function () {
+ $urlGenerator = \OC::$server->getURLGenerator();
$l = \OC::$server->getL10N('files');
return [
'id' => 'files_index',
'order' => 0,
- 'href' => \OCP\Util::linkTo('files', 'index.php'),
+ 'href' => $urlGenerator->linkToRoute('files.view.index'),
'icon' => \OCP\Util::imagePath('core', 'places/files.svg'),
'name' => $l->t('Files'),
];
@@ -63,6 +65,7 @@ $templateManager->registerTemplate('application/vnd.oasis.opendocument.spreadshe
new \OCA\Files\ActivityHelper(
\OC::$server->getTagManager()
),
+ \OC::$server->getDatabaseConnection(),
\OC::$server->getConfig()
);
});
diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml
index ba8bb62494..4ab226f396 100644
--- a/apps/files/appinfo/info.xml
+++ b/apps/files/appinfo/info.xml
@@ -8,7 +8,7 @@
true
- 1.3.0
+ 1.4.0
diff --git a/apps/files/appinfo/routes.php b/apps/files/appinfo/routes.php
index d52dfaab21..2bb913c30a 100644
--- a/apps/files/appinfo/routes.php
+++ b/apps/files/appinfo/routes.php
@@ -48,14 +48,17 @@ $application->registerRoutes(
'verb' => 'GET',
'requirements' => array('tagName' => '.+'),
),
+ [
+ 'name' => 'view#index',
+ 'url' => '/',
+ 'verb' => 'GET',
+ ],
)
)
);
/** @var $this \OC\Route\Router */
-$this->create('files_index', '/')
- ->actionInclude('files/index.php');
$this->create('files_ajax_delete', 'ajax/delete.php')
->actionInclude('files/ajax/delete.php');
$this->create('files_ajax_download', 'ajax/download.php')
diff --git a/apps/files/command/scan.php b/apps/files/command/scan.php
index 99ce64e09c..31ae555e04 100644
--- a/apps/files/command/scan.php
+++ b/apps/files/command/scan.php
@@ -26,6 +26,7 @@
namespace OCA\Files\Command;
use OC\ForbiddenException;
+use OCP\Files\StorageNotAvailableException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -74,7 +75,7 @@ class Scan extends Command {
}
protected function scanFiles($user, $path, $quiet, OutputInterface $output) {
- $scanner = new \OC\Files\Utils\Scanner($user, \OC::$server->getDatabaseConnection());
+ $scanner = new \OC\Files\Utils\Scanner($user, \OC::$server->getDatabaseConnection(), \OC::$server->getLogger());
if (!$quiet) {
$scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) {
$output->writeln("Scanning file $path");
@@ -82,6 +83,9 @@ class Scan extends Command {
$scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) {
$output->writeln("Scanning folder $path");
});
+ $scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output) {
+ $output->writeln("Error while scanning, storage not available (" . $e->getMessage() . ")");
+ });
}
try {
$scanner->scan($path);
diff --git a/apps/files/controller/apicontroller.php b/apps/files/controller/apicontroller.php
index 1ecd5294c6..ee54db6319 100644
--- a/apps/files/controller/apicontroller.php
+++ b/apps/files/controller/apicontroller.php
@@ -127,7 +127,7 @@ class ApiController extends Controller {
*
* @NoAdminRequired
*
- * @param array|string $tagName tag name to filter by
+ * @param string $tagName tag name to filter by
* @return DataResponse
*/
public function getFilesByTag($tagName) {
diff --git a/apps/files/controller/viewcontroller.php b/apps/files/controller/viewcontroller.php
new file mode 100644
index 0000000000..d9c5959286
--- /dev/null
+++ b/apps/files/controller/viewcontroller.php
@@ -0,0 +1,230 @@
+
+ *
+ * @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
+ *
+ */
+
+namespace OCA\Files\Controller;
+
+use OC\AppFramework\Http\Request;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\ContentSecurityPolicy;
+use OCP\AppFramework\Http\RedirectResponse;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\IL10N;
+use OCP\INavigationManager;
+use OCP\IRequest;
+use OCP\IURLGenerator;
+use OCP\IConfig;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+
+/**
+ * Class ViewController
+ *
+ * @package OCA\Files\Controller
+ */
+class ViewController extends Controller {
+ /** @var string */
+ protected $appName;
+ /** @var IRequest */
+ protected $request;
+ /** @var IURLGenerator */
+ protected $urlGenerator;
+ /** @var INavigationManager */
+ protected $navigationManager;
+ /** @var IL10N */
+ protected $l10n;
+ /** @var IConfig */
+ protected $config;
+ /** @var EventDispatcherInterface */
+ protected $eventDispatcher;
+
+ /**
+ * @param string $appName
+ * @param IRequest $request
+ * @param IURLGenerator $urlGenerator
+ * @param INavigationManager $navigationManager
+ * @param IL10N $l10n
+ * @param IConfig $config
+ * @param EventDispatcherInterface $eventDispatcherInterface
+ */
+ public function __construct($appName,
+ IRequest $request,
+ IURLGenerator $urlGenerator,
+ INavigationManager $navigationManager,
+ IL10N $l10n,
+ IConfig $config,
+ EventDispatcherInterface $eventDispatcherInterface) {
+ parent::__construct($appName, $request);
+ $this->appName = $appName;
+ $this->request = $request;
+ $this->urlGenerator = $urlGenerator;
+ $this->navigationManager = $navigationManager;
+ $this->l10n = $l10n;
+ $this->config = $config;
+ $this->eventDispatcher = $eventDispatcherInterface;
+ }
+
+ /**
+ * @param string $appName
+ * @param string $scriptName
+ * @return string
+ */
+ protected function renderScript($appName, $scriptName) {
+ $content = '';
+ $appPath = \OC_App::getAppPath($appName);
+ $scriptPath = $appPath . '/' . $scriptName;
+ if (file_exists($scriptPath)) {
+ // TODO: sanitize path / script name ?
+ ob_start();
+ include $scriptPath;
+ $content = ob_get_contents();
+ @ob_end_clean();
+ }
+ return $content;
+ }
+
+ /**
+ * FIXME: Replace with non static code
+ *
+ * @return array
+ * @throws \OCP\Files\NotFoundException
+ */
+ protected function getStorageInfo() {
+ $dirInfo = \OC\Files\Filesystem::getFileInfo('/', false);
+ return \OC_Helper::getStorageInfo('/', $dirInfo);
+ }
+
+ /**
+ * @NoCSRFRequired
+ * @NoAdminRequired
+ *
+ * @param string $dir
+ * @param string $view
+ * @return TemplateResponse
+ * @throws \OCP\Files\NotFoundException
+ */
+ public function index($dir = '', $view = '') {
+ $nav = new \OCP\Template('files', 'appnavigation', '');
+
+ // Load the files we need
+ \OCP\Util::addStyle('files', 'files');
+ \OCP\Util::addStyle('files', 'upload');
+ \OCP\Util::addStyle('files', 'mobile');
+ \OCP\Util::addscript('files', 'app');
+ \OCP\Util::addscript('files', 'file-upload');
+ \OCP\Util::addscript('files', 'newfilemenu');
+ \OCP\Util::addscript('files', 'jquery.iframe-transport');
+ \OCP\Util::addscript('files', 'jquery.fileupload');
+ \OCP\Util::addscript('files', 'jquery-visibility');
+ \OCP\Util::addscript('files', 'fileinfomodel');
+ \OCP\Util::addscript('files', 'filesummary');
+ \OCP\Util::addscript('files', 'breadcrumb');
+ \OCP\Util::addscript('files', 'filelist');
+ \OCP\Util::addscript('files', 'search');
+
+ \OCP\Util::addScript('files', 'favoritesfilelist');
+ \OCP\Util::addScript('files', 'tagsplugin');
+ \OCP\Util::addScript('files', 'favoritesplugin');
+
+ \OCP\Util::addScript('files', 'detailfileinfoview');
+ \OCP\Util::addScript('files', 'detailtabview');
+ \OCP\Util::addScript('files', 'mainfileinfodetailview');
+ \OCP\Util::addScript('files', 'detailsview');
+ \OCP\Util::addStyle('files', 'detailsView');
+
+ \OC_Util::addVendorScript('core', 'handlebars/handlebars');
+
+ \OCP\Util::addscript('files', 'fileactions');
+ \OCP\Util::addscript('files', 'fileactionsmenu');
+ \OCP\Util::addscript('files', 'files');
+ \OCP\Util::addscript('files', 'keyboardshortcuts');
+ \OCP\Util::addscript('files', 'navigation');
+
+ // if IE8 and "?dir=path&view=someview" was specified, reformat the URL to use a hash like "#?dir=path&view=someview"
+ $isIE8 = $this->request->isUserAgent([Request::USER_AGENT_IE_8]);
+ if ($isIE8 && ($dir !== '' || $view !== '')) {
+ $dir = !empty($dir) ? $dir : '/';
+ $view = !empty($view) ? $view : 'files';
+ $hash = '#?dir=' . \OCP\Util::encodePath($dir);
+ if ($view !== 'files') {
+ $hash .= '&view=' . urlencode($view);
+ }
+ return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index') . $hash);
+ }
+
+ // mostly for the home storage's free space
+ // FIXME: Make non static
+ $storageInfo = $this->getStorageInfo();
+
+ \OCA\Files\App::getNavigationManager()->add(
+ [
+ 'id' => 'favorites',
+ 'appname' => 'files',
+ 'script' => 'simplelist.php',
+ 'order' => 5,
+ 'name' => $this->l10n->t('Favorites')
+ ]
+ );
+
+ $navItems = \OCA\Files\App::getNavigationManager()->getAll();
+ usort($navItems, function($item1, $item2) {
+ return $item1['order'] - $item2['order'];
+ });
+ $nav->assign('navigationItems', $navItems);
+
+ $contentItems = [];
+
+ // render the container content for every navigation item
+ foreach ($navItems as $item) {
+ $content = '';
+ if (isset($item['script'])) {
+ $content = $this->renderScript($item['appname'], $item['script']);
+ }
+ $contentItem = [];
+ $contentItem['id'] = $item['id'];
+ $contentItem['content'] = $content;
+ $contentItems[] = $contentItem;
+ }
+
+ $this->eventDispatcher->dispatch('OCA\Files::loadAdditionalScripts');
+
+ $params = [];
+ $params['usedSpacePercent'] = (int)$storageInfo['relative'];
+ $params['owner'] = $storageInfo['owner'];
+ $params['ownerDisplayName'] = $storageInfo['ownerDisplayName'];
+ $params['isPublic'] = false;
+ $params['mailNotificationEnabled'] = $this->config->getAppValue('core', 'shareapi_allow_mail_notification', 'no');
+ $params['mailPublicNotificationEnabled'] = $this->config->getAppValue('core', 'shareapi_allow_public_notification', 'no');
+ $params['allowShareWithLink'] = $this->config->getAppValue('core', 'shareapi_allow_links', 'yes');
+ $params['appNavigation'] = $nav;
+ $params['appContents'] = $contentItems;
+ $this->navigationManager->setActiveEntry('files_index');
+
+ $response = new TemplateResponse(
+ $this->appName,
+ 'index',
+ $params
+ );
+ $policy = new ContentSecurityPolicy();
+ $policy->addAllowedFrameDomain('\'self\'');
+ $response->setContentSecurityPolicy($policy);
+
+ return $response;
+ }
+}
diff --git a/apps/files/index.php b/apps/files/index.php
deleted file mode 100644
index cc007ebdb0..0000000000
--- a/apps/files/index.php
+++ /dev/null
@@ -1,161 +0,0 @@
-
- * @author Frank Karlitschek
- * @author Jakob Sack
- * @author Jan-Christoph Borchardt
- * @author Joas Schilling
- * @author Jörn Friedrich Dreyer
- * @author Morris Jobke
- * @author Robin Appelman
- * @author Roman Geber
- * @author Thomas Müller
- * @author Vincent Petry
- *
- * @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
- *
- */
-
-// Check if we are a user
-OCP\User::checkLoggedIn();
-
-// Load the files we need
-OCP\Util::addStyle('files', 'files');
-OCP\Util::addStyle('files', 'upload');
-OCP\Util::addStyle('files', 'mobile');
-OCP\Util::addscript('files', 'app');
-OCP\Util::addscript('files', 'file-upload');
-OCP\Util::addscript('files', 'newfilemenu');
-OCP\Util::addscript('files', 'jquery.iframe-transport');
-OCP\Util::addscript('files', 'jquery.fileupload');
-OCP\Util::addscript('files', 'jquery-visibility');
-OCP\Util::addscript('files', 'fileinfomodel');
-OCP\Util::addscript('files', 'filesummary');
-OCP\Util::addscript('files', 'breadcrumb');
-OCP\Util::addscript('files', 'filelist');
-OCP\Util::addscript('files', 'search');
-
-\OCP\Util::addScript('files', 'favoritesfilelist');
-\OCP\Util::addScript('files', 'tagsplugin');
-\OCP\Util::addScript('files', 'favoritesplugin');
-
-\OCP\Util::addScript('files', 'detailfileinfoview');
-\OCP\Util::addScript('files', 'detailtabview');
-\OCP\Util::addScript('files', 'mainfileinfodetailview');
-\OCP\Util::addScript('files', 'detailsview');
-\OCP\Util::addStyle('files', 'detailsView');
-
-\OC_Util::addVendorScript('core', 'handlebars/handlebars');
-
-OCP\App::setActiveNavigationEntry('files_index');
-
-$l = \OC::$server->getL10N('files');
-
-$isIE8 = false;
-preg_match('/MSIE (.*?);/', $_SERVER['HTTP_USER_AGENT'], $matches);
-if (count($matches) > 0 && $matches[1] <= 9) {
- $isIE8 = true;
-}
-
-// if IE8 and "?dir=path&view=someview" was specified, reformat the URL to use a hash like "#?dir=path&view=someview"
-if ($isIE8 && (isset($_GET['dir']) || isset($_GET['view']))) {
- $hash = '#?';
- $dir = isset($_GET['dir']) ? $_GET['dir'] : '/';
- $view = isset($_GET['view']) ? $_GET['view'] : 'files';
- $hash = '#?dir=' . \OCP\Util::encodePath($dir);
- if ($view !== 'files') {
- $hash .= '&view=' . urlencode($view);
- }
- header('Location: ' . OCP\Util::linkTo('files', 'index.php') . $hash);
- exit();
-}
-
-$user = OC_User::getUser();
-
-$config = \OC::$server->getConfig();
-
-// mostly for the home storage's free space
-$dirInfo = \OC\Files\Filesystem::getFileInfo('/', false);
-$storageInfo=OC_Helper::getStorageInfo('/', $dirInfo);
-
-$nav = new OCP\Template('files', 'appnavigation', '');
-
-function sortNavigationItems($item1, $item2) {
- return $item1['order'] - $item2['order'];
-}
-
-\OCA\Files\App::getNavigationManager()->add(
- array(
- 'id' => 'favorites',
- 'appname' => 'files',
- 'script' => 'simplelist.php',
- 'order' => 5,
- 'name' => $l->t('Favorites')
- )
-);
-
-$navItems = \OCA\Files\App::getNavigationManager()->getAll();
-usort($navItems, 'sortNavigationItems');
-$nav->assign('navigationItems', $navItems);
-
-$contentItems = array();
-
-function renderScript($appName, $scriptName) {
- $content = '';
- $appPath = OC_App::getAppPath($appName);
- $scriptPath = $appPath . '/' . $scriptName;
- if (file_exists($scriptPath)) {
- // TODO: sanitize path / script name ?
- ob_start();
- include $scriptPath;
- $content = ob_get_contents();
- @ob_end_clean();
- }
- return $content;
-}
-
-// render the container content for every navigation item
-foreach ($navItems as $item) {
- $content = '';
- if (isset($item['script'])) {
- $content = renderScript($item['appname'], $item['script']);
- }
- $contentItem = array();
- $contentItem['id'] = $item['id'];
- $contentItem['content'] = $content;
- $contentItems[] = $contentItem;
-}
-
-OCP\Util::addscript('files', 'fileactions');
-OCP\Util::addscript('files', 'fileactionsmenu');
-OCP\Util::addscript('files', 'files');
-OCP\Util::addscript('files', 'navigation');
-OCP\Util::addscript('files', 'keyboardshortcuts');
-
-\OC::$server->getEventDispatcher()->dispatch('OCA\Files::loadAdditionalScripts');
-
-$tmpl = new OCP\Template('files', 'index', 'user');
-$tmpl->assign('usedSpacePercent', (int)$storageInfo['relative']);
-$tmpl->assign('owner', $storageInfo['owner']);
-$tmpl->assign('ownerDisplayName', $storageInfo['ownerDisplayName']);
-$tmpl->assign('isPublic', false);
-$tmpl->assign("mailNotificationEnabled", $config->getAppValue('core', 'shareapi_allow_mail_notification', 'no'));
-$tmpl->assign("mailPublicNotificationEnabled", $config->getAppValue('core', 'shareapi_allow_public_notification', 'no'));
-$tmpl->assign("allowShareWithLink", $config->getAppValue('core', 'shareapi_allow_links', 'yes'));
-$tmpl->assign('appNavigation', $nav);
-$tmpl->assign('appContents', $contentItems);
-
-$tmpl->printPage();
diff --git a/apps/files/js/app.js b/apps/files/js/app.js
index f31770466f..ff505d417f 100644
--- a/apps/files/js/app.js
+++ b/apps/files/js/app.js
@@ -71,7 +71,8 @@
folderDropOptions: folderDropOptions,
fileActions: fileActions,
allowLegacyActions: true,
- scrollTo: urlParams.scrollto
+ scrollTo: urlParams.scrollto,
+ filesClient: OC.Files.getClient()
}
);
this.files.initialize();
diff --git a/apps/files/js/favoritesplugin.js b/apps/files/js/favoritesplugin.js
index 417a32ef80..454a505c7b 100644
--- a/apps/files/js/favoritesplugin.js
+++ b/apps/files/js/favoritesplugin.js
@@ -92,7 +92,7 @@
// folder in the files app instead of opening it directly
fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
OCA.Files.App.setActiveView('files', {silent: true});
- OCA.Files.App.fileList.changeDirectory(context.$file.attr('data-path') + '/' + filename, true, true);
+ OCA.Files.App.fileList.changeDirectory(OC.joinPaths(context.$file.attr('data-path'), filename), true, true);
});
fileActions.setDefault('dir', 'Open');
return fileActions;
diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js
index 6a767d48a2..871a2149c8 100644
--- a/apps/files/js/fileactions.js
+++ b/apps/files/js/fileactions.js
@@ -575,7 +575,8 @@
},
actionHandler: function (filename, context) {
var dir = context.dir || context.fileList.getCurrentDirectory();
- var url = context.fileList.getDownloadUrl(filename, dir);
+ var isDir = context.$file.attr('data-type') === 'dir';
+ var url = context.fileList.getDownloadUrl(filename, dir, isDir);
var downloadFileaction = $(context.$file).find('.fileactions .action-download');
@@ -611,10 +612,7 @@
this.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
var dir = context.$file.attr('data-path') || context.fileList.getCurrentDirectory();
- if (dir !== '/') {
- dir = dir + '/';
- }
- context.fileList.changeDirectory(dir + filename);
+ context.fileList.changeDirectory(OC.joinPaths(dir, filename));
});
this.registerAction({
diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js
index d1f68d98ea..672c39a8bb 100644
--- a/apps/files/js/filelist.js
+++ b/apps/files/js/filelist.js
@@ -22,11 +22,12 @@
*
* @param $el container element with existing markup for the #controls
* and a table
- * @param [options] map of options, see other parameters
- * @param [options.scrollContainer] scrollable container, defaults to $(window)
- * @param [options.dragOptions] drag options, disabled by default
- * @param [options.folderDropOptions] folder drop options, disabled by default
- * @param [options.detailsViewEnabled=true] whether to enable details view
+ * @param {Object} [options] map of options, see other parameters
+ * @param {Object} [options.scrollContainer] scrollable container, defaults to $(window)
+ * @param {Object} [options.dragOptions] drag options, disabled by default
+ * @param {Object} [options.folderDropOptions] folder drop options, disabled by default
+ * @param {boolean} [options.detailsViewEnabled=true] whether to enable details view
+ * @param {OC.Files.Client} [options.filesClient] files client to use
*/
var FileList = function($el, options) {
this.initialize($el, options);
@@ -73,6 +74,13 @@
*/
_detailsView: null,
+ /**
+ * Files client instance
+ *
+ * @type OC.Files.Client
+ */
+ filesClient: null,
+
/**
* Whether the file list was initialized already.
* @type boolean
@@ -92,10 +100,17 @@
* Array of files in the current folder.
* The entries are of file data.
*
- * @type Array.