Allow to have multiple links per public calendar

Adding a public link is still the old way:

```xml
<o:publish-calendar xmlns:o="http://calendarserver.org/ns/"/>
```

Removing all public links is just like we used to unpublish
```xml
<o:unpublish-calendar xmlns:o="http://calendarserver.org/ns/"/>
```

We now have the following for unpublishing a specific link
```xml
<o:unpublish-calendar xmlns:o="http://nextcloud.com/ns/">urltounpublish</o:unpublish-calendar>
```

The public URLs are exposed this way:
```xml
<x1:publish-urls xmlns:d="DAV:" xmlns:x1="http://nextcloud.com/ns/">
	<d:href>urltopublish</d:href>
	<d:href>secondurltopublish</d:href>
</x1:publish-urls>
```

`publish-url` is still available and give the first URL
```xml
<x1:publish-url xmlns:d="DAV:" xmlns:x1="http://calendarserver.org/ns/">
	<d:href>urltopublish</d:href>
</x1:publish-url>
```

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2020-03-28 18:25:20 +01:00
parent df463f90be
commit 02e4b05644
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
8 changed files with 254 additions and 127 deletions

View File

@ -2339,47 +2339,82 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
* @param boolean $value
* @param \OCA\DAV\CalDAV\Calendar $calendar
* @return string|null
* @param Calendar $calendar
* @return string
*/
public function setPublishStatus($value, $calendar) {
public function addPublicLink(Calendar $calendar): string {
$calendarId = $calendar->getResourceId();
$publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent(
'\OCA\DAV\CalDAV\CalDavBackend::updateShares',
'\OCA\DAV\CalDAV\CalDavBackend::addPublicLink',
[
'calendarId' => $calendarId,
'calendarData' => $this->getCalendarById($calendarId),
'public' => $value,
'publicuri' => $publicUri,
]));
$query = $this->db->getQueryBuilder();
if ($value) {
$publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
$query->insert('dav_shares')
->values([
'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
'type' => $query->createNamedParameter('calendar'),
'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
'publicuri' => $query->createNamedParameter($publicUri)
]);
$query->execute();
return $publicUri;
}
$query->delete('dav_shares')
->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
$query->insert('dav_shares')
->values([
'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
'type' => $query->createNamedParameter('calendar'),
'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
'publicuri' => $query->createNamedParameter($publicUri)
]);
$query->execute();
return null;
return $publicUri;
}
/**
* @param \OCA\DAV\CalDAV\Calendar $calendar
* @return mixed
* @param Calendar $calendar
* @param string $publicUri
*/
public function getPublishStatus($calendar) {
public function removePublicLink(Calendar $calendar, string $publicUri) {
$calendarId = $calendar->getResourceId();
$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent(
'\OCA\DAV\CalDAV\CalDavBackend::removePublicLinks',
[
'calendarId' => $calendarId,
'calendarData' => $this->getCalendarById($calendarId),
'publicuri' => $publicUri
]));
$query = $this->db->getQueryBuilder();
$query->delete('dav_shares')
->where($query->expr()->eq('publicuri', $query->createNamedParameter($publicUri)))
->andWhere($query->expr()->eq('resourceid', $query->createNamedParameter($calendarId)))
->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
$query->execute();
}
/**
* @param Calendar $calendar
*/
public function removeAllPublicLinks(Calendar $calendar) {
$calendarId = $calendar->getResourceId();
$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent(
'\OCA\DAV\CalDAV\CalDavBackend::removeAllPublicLinks',
[
'calendarId' => $calendarId,
'calendarData' => $this->getCalendarById($calendarId),
]));
$query = $this->db->getQueryBuilder();
$query->delete('dav_shares')
->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendarId)))
->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
$query->execute();
}
/**
* @param Calendar $calendar
* @return array
*/
public function getPublicURIs($calendar): array {
$query = $this->db->getQueryBuilder();
$result = $query->select('publicuri')
->from('dav_shares')
@ -2387,9 +2422,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
->execute();
$row = $result->fetch();
$result->closeCursor();
return $row ? reset($row) : false;
return $result->fetchAll(\PDO::FETCH_ASSOC);
}
/**

View File

@ -350,21 +350,32 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IShareable {
return $uris;
}
/**
* @param boolean $value
* @return string|null
*/
public function setPublishStatus($value) {
$publicUri = $this->caldavBackend->setPublishStatus($value, $this);
$this->calendarInfo['publicuri'] = $publicUri;
public function addPublicLink() {
$publicUri = $this->caldavBackend->addPublicLink($this);
$this->calendarInfo['publicuris'][] = $publicUri;
return $publicUri;
}
public function removePublicLink(string $publicUri) {
$this->caldavBackend->removePublicLink($this, $publicUri);
}
public function removeAllPublicLinks() {
$this->caldavBackend->removeAllPublicLinks($this);
}
/**
* @return mixed $value
* @return bool $value
*/
public function getPublishStatus() {
return $this->caldavBackend->getPublishStatus($this);
public function getPublishStatus(): bool {
return count($this->getPublicURIs()) > 0;
}
/**
* @return mixed
*/
public function getPublicURIs() : array {
return array_map(function ($publicURI) { return $publicURI['publicuri']; }, $this->caldavBackend->getPublicURIs($this));
}
public function canWrite() {

View File

@ -41,6 +41,7 @@ use Sabre\HTTP\ResponseInterface;
class PublishPlugin extends ServerPlugin {
const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
const NS_NEXTCLOUD = 'http://nextcloud.com/ns/';
/**
* Reference to SabreDAV server object.
@ -121,10 +122,22 @@ class PublishPlugin extends ServerPlugin {
$propFind->handle('{'.self::NS_CALENDARSERVER.'}publish-url', function () use ($node) {
if ($node->getPublishStatus()) {
// We return the publish-url only if the calendar is published.
$token = $node->getPublishStatus();
$token = reset($node->getPublicURIs());
$publishUrl = $this->urlGenerator->getAbsoluteURL($this->server->getBaseUri().'public-calendars/').$token;
return new Publisher($publishUrl, true);
return new Publisher([$publishUrl => true]);
}
});
$propFind->handle('{'.self::NS_NEXTCLOUD.'}publish-urls', function () use ($node) {
if ($node->getPublishStatus()) {
// We return the publish-url only if the calendar is published.
$tokens = $node->getPublicURIs();
$publishUrls = array_map(function ($token) {
return [$this->urlGenerator->getAbsoluteURL($this->server->getBaseUri().'public-calendars/').$token => true];
}, $tokens);
return new Publisher($publishUrls);
}
});
@ -172,65 +185,90 @@ class PublishPlugin extends ServerPlugin {
// re-populated the request body with the existing data.
$request->setBody($requestBody);
$this->server->xml->parse($requestBody, $request->getUrl(), $documentType);
$message = $this->server->xml->parse($requestBody, $request->getUrl(), $documentType);
switch ($documentType) {
case '{'.self::NS_CALENDARSERVER.'}publish-calendar' :
// We can only deal with IShareableCalendar objects
if (!$node instanceof Calendar) {
return;
}
$this->server->transactionType = 'post-publish-calendar';
// We can only deal with IShareableCalendar objects
if (!$node instanceof Calendar) {
return;
}
$this->server->transactionType = 'post-publish-calendar';
// Getting ACL info
$acl = $this->server->getPlugin('acl');
// Getting ACL info
$acl = $this->server->getPlugin('acl');
// If there's no ACL support, we allow everything
if ($acl) {
$acl->checkPrivileges($path, '{DAV:}write');
}
// If there's no ACL support, we allow everything
if ($acl) {
$acl->checkPrivileges($path, '{DAV:}write');
}
$node->setPublishStatus(true);
$node->addPublicLink();
// iCloud sends back the 202, so we will too.
$response->setStatus(202);
// iCloud sends back the 202, so we will too.
$response->setStatus(202);
// 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');
// 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;
// Breaking the event chain
return false;
case '{'.self::NS_CALENDARSERVER.'}unpublish-calendar' :
// We can only deal with IShareableCalendar objects
if (!$node instanceof Calendar) {
return;
}
$this->server->transactionType = 'post-unpublish-calendar';
// We can only deal with IShareableCalendar objects
if (!$node instanceof Calendar) {
return;
}
$this->server->transactionType = 'post-unpublish-all-calendars';
// Getting ACL info
$acl = $this->server->getPlugin('acl');
// Getting ACL info
$acl = $this->server->getPlugin('acl');
// If there's no ACL support, we allow everything
if ($acl) {
$acl->checkPrivileges($path, '{DAV:}write');
}
// If there's no ACL support, we allow everything
if ($acl) {
$acl->checkPrivileges($path, '{DAV:}write');
}
$node->setPublishStatus(false);
$node->removeAllPublicLinks();
$response->setStatus(200);
$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');
// 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;
// Breaking the event chain
return false;
case '{'.self::NS_NEXTCLOUD.'}unpublish-calendar' :
// We can only deal with IShareableCalendar objects
if (!$node instanceof Calendar) {
return;
}
$this->server->transactionType = 'post-unpublish-calendar';
// Getting ACL info
$acl = $this->server->getPlugin('acl');
// If there's no ACL support, we allow everything
if ($acl) {
$acl->checkPrivileges($path, '{DAV:}write');
}
$node->removePublicLink($message);
$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;
}
}
}

View File

@ -30,33 +30,26 @@ use Sabre\Xml\XmlSerializable;
class Publisher implements XmlSerializable {
/**
* @var string $publishUrl
* @var string $publishUrls
*/
protected $publishUrl;
protected $publishUrls;
/**
* @var boolean $isPublished
* @param array $publishUrls
*/
protected $isPublished;
/**
* @param string $publishUrl
* @param boolean $isPublished
*/
function __construct($publishUrl, $isPublished) {
$this->publishUrl = $publishUrl;
$this->isPublished = $isPublished;
function __construct(array $publishUrls) {
$this->publishUrls = $publishUrls;
}
/**
* @return string
* @return array
*/
function getValue() {
return $this->publishUrl;
function getValue(): array {
return array_keys($this->publishUrls);
}
/**
* The xmlSerialize metod is called during xml writing.
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
@ -75,12 +68,13 @@ class Publisher implements XmlSerializable {
* @return void
*/
function xmlSerialize(Writer $writer) {
if (!$this->isPublished) {
foreach ($this->publishUrls as $publishUrl => $isPublished)
if (!$isPublished) {
// for pre-publish-url
$writer->write($this->publishUrl);
$writer->write($publishUrl);
} else {
// for publish-url
$writer->writeElement('{DAV:}href', $this->publishUrl);
$writer->writeElement('{DAV:}href', $publishUrl);
}
}
}

View File

@ -34,6 +34,7 @@ use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Calendar;
use OCP\IConfig;
use OCP\IL10N;
use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\PropPatch;
use Sabre\DAV\Xml\Property\Href;
@ -280,7 +281,7 @@ EOD;
$this->assertCount(0, $calendarObjects);
}
public function testMultipleCalendarObjectsWithSameUID() {
$this->expectException(\Sabre\DAV\Exception\BadRequest::class);
$this->expectExceptionMessage('Calendar object with uid already exists in this calendar collection.');
@ -508,12 +509,12 @@ EOD;
$calendarInfo = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER)[0];
/** @var IL10N|\PHPUnit_Framework_MockObject_MockObject $l10n */
/** @var IL10N|MockObject $l10n */
$l10n = $this->createMock(IL10N::class);
$config = $this->createMock(IConfig::class);
$calendar = new Calendar($this->backend, $calendarInfo, $l10n, $config);
$calendar->setPublishStatus(true);
$calendar->addPublicLink();
$this->assertNotEquals(false, $calendar->getPublishStatus());
$publicCalendars = $this->backend->getPublicCalendars();
@ -525,11 +526,24 @@ EOD;
$publicCalendar = $this->backend->getPublicCalendar($publicCalendarURI);
$this->assertEquals(true, $publicCalendar['{http://owncloud.org/ns}public']);
$calendar->setPublishStatus(false);
$calendar->addPublicLink();
$publicCalendarURIs = $this->backend->getPublicURIs($calendar);
$this->assertCount(2, $publicCalendarURIs);
$publicCalendarURI2 = $publicCalendarURIs[1]['publicuri'];
$publicCalendar2 = $this->backend->getPublicCalendar($publicCalendarURI2);
$this->assertEquals(true, $publicCalendar2['{http://owncloud.org/ns}public']);
$calendar->removePublicLink($publicCalendarURI2);
$publicCalendarURIs = $this->backend->getPublicURIs($calendar);
$this->assertCount(1, $publicCalendarURIs);
$calendar->removeAllPublicLinks();
$this->assertEquals(false, $calendar->getPublishStatus());
$this->expectException(NotFound::class);
$this->backend->getPublicCalendar($publicCalendarURI);
$this->expectException(NotFound::class);
$this->backend->getPublicCalendar($publicCalendarURI2);
}
public function testSubscriptions() {
@ -607,21 +621,21 @@ DTSTART;TZID=Europe/Warsaw:20170325T150000
DTEND;TZID=Europe/Warsaw:20170325T160000
TRANSP:OPAQUE
DESCRIPTION:Magiczna treść uzyskana za pomocą magicznego proszku.\n\nę
żźćńłóÓŻŹĆŁĘ€śśśŚŚ\n \,\,))))))))\;\,\n
żźćńłóÓŻŹĆŁĘ€śśśŚŚ\n \,\,))))))))\;\,\n
__))))))))))))))\,\n \\|/ -\\(((((''''((((((((.\n -*-==///
///(('' . `))))))\,\n /|\\ ))| o \;-. '(((((
\,(\,\n ( `| / ) \;))))'
///(('' . `))))))\,\n /|\\ ))| o \;-. '(((((
\,(\,\n ( `| / ) \;))))'
\,_))^\;(~\n | | | \,))((((_ _____-
-----~~~-. %\,\;(\;(>'\;'~\n o_)\; \; )))(((` ~---
~ `:: \\ %%~~)(v\;(`('~\n \; ''''````
`: `:::|\\\,__\,%% )\;`'\; ~\n | _
) / `:|`----' `-'\n ______/\\/~ |
`: `:::|\\\,__\,%% )\;`'\; ~\n | _
) / `:|`----' `-'\n ______/\\/~ |
/ /\n /~\;\;.____/\;\;' / ___--\
,-( `\;\;\;/\n / // _\;______\;'------~~~~~ /\;\;/\\ /\n
// | | / \; \\\;\;\,\\\n (<_ | \;
/'\,/-----' _>\n \\_| ||_
//~\;~~~~~~~~~\n `\\_| (\,~~ -Tua Xiong\n
\\~\\\n
,-( `\;\;\;/\n / // _\;______\;'------~~~~~ /\;\;/\\ /\n
// | | / \; \\\;\;\,\\\n (<_ | \;
/'\,/-----' _>\n \\_| ||_
//~\;~~~~~~~~~\n `\\_| (\,~~ -Tua Xiong\n
\\~\\\n
~~\n\n
SEQUENCE:1
X-MOZ-GENERATION:1

View File

@ -7,7 +7,7 @@
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Citharel <tcit@tcit.fr>
* @author Thomas Citharel <nextcloud@tcit.fr>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
*
@ -41,6 +41,9 @@ use OCP\IL10N;
use OCP\ILogger;
use OCP\IUserManager;
use OCP\Security\ISecureRandom;
use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\Exception;
use Sabre\DAV\Exception\NotFound;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Test\TestCase;
@ -60,11 +63,11 @@ class PublicCalendarRootTest extends TestCase {
private $publicCalendarRoot;
/** @var IL10N */
private $l10n;
/** @var Principal|\PHPUnit_Framework_MockObject_MockObject */
/** @var Principal| MockObject */
private $principal;
/** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */
/** @var IUserManager| MockObject */
protected $userManager;
/** @var IGroupManager|\PHPUnit_Framework_MockObject_MockObject */
/** @var IGroupManager| MockObject */
protected $groupManager;
/** @var IConfig */
protected $config;
@ -157,13 +160,15 @@ class PublicCalendarRootTest extends TestCase {
/**
* @return Calendar
* @throws Exception
* @throws NotFound
*/
protected function createPublicCalendar() {
$this->backend->createCalendar(self::UNIT_TEST_USER, 'Example', []);
$calendarInfo = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER)[0];
$calendar = new PublicCalendar($this->backend, $calendarInfo, $this->l10n, $this->config);
$publicUri = $calendar->setPublishStatus(true);
$publicUri = $calendar->addPublicLink();
$calendarInfo = $this->backend->getPublicCalendar($publicUri);
$calendar = new PublicCalendar($this->backend, $calendarInfo, $this->l10n, $this->config);

View File

@ -33,15 +33,16 @@ use Test\TestCase;
class PublisherTest extends TestCase {
const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
const NS_NEXTCLOUD = 'http://nextcloud.com/ns/';
public function testSerializePublished() {
$publish = new Publisher('urltopublish', true);
$publish = new Publisher(['urltopublish' => true]);
$xml = $this->write([
'{' . self::NS_CALENDARSERVER . '}publish-url' => $publish,
]);
$this->assertEquals('urltopublish', $publish->getValue());
$this->assertEquals(['urltopublish'], $publish->getValue());
$this->assertXmlStringEqualsXmlString(
'<?xml version="1.0"?>
@ -50,14 +51,31 @@ class PublisherTest extends TestCase {
</x1:publish-url>', $xml);
}
public function testSerializeMultiplePublished() {
$publish = new Publisher(['urltopublish' => true, 'secondurltopublish' => true]);
$xml = $this->write([
'{' . self::NS_NEXTCLOUD . '}publish-urls' => $publish,
]);
$this->assertEquals(['urltopublish', 'secondurltopublish'], $publish->getValue());
$this->assertXmlStringEqualsXmlString(
'<?xml version="1.0"?>
<x1:publish-urls xmlns:d="DAV:" xmlns:x1="' . self::NS_NEXTCLOUD . '">
<d:href>urltopublish</d:href>
<d:href>secondurltopublish</d:href>
</x1:publish-urls>', $xml);
}
public function testSerializeNotPublished() {
$publish = new Publisher('urltopublish', false);
$publish = new Publisher(['urltopublish' => false]);
$xml = $this->write([
'{' . self::NS_CALENDARSERVER . '}pre-publish-url' => $publish,
]);
$this->assertEquals('urltopublish', $publish->getValue());
$this->assertEquals(['urltopublish'], $publish->getValue());
$this->assertXmlStringEqualsXmlString(
'<?xml version="1.0"?>

View File

@ -30,6 +30,7 @@ use OCA\DAV\CalDAV\Publishing\PublishPlugin;
use OCP\IConfig;
use OCP\IRequest;
use OCP\IURLGenerator;
use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\Server;
use Sabre\DAV\SimpleCollection;
use Sabre\HTTP\Request;
@ -42,11 +43,11 @@ class PluginTest extends TestCase {
private $plugin;
/** @var Server */
private $server;
/** @var Calendar | \PHPUnit_Framework_MockObject_MockObject */
/** @var Calendar | MockObject */
private $book;
/** @var IConfig | \PHPUnit_Framework_MockObject_MockObject */
/** @var IConfig | MockObject */
private $config;
/** @var IURLGenerator | \PHPUnit_Framework_MockObject_MockObject */
/** @var IURLGenerator | MockObject */
private $urlGenerator;
protected function setUp(): void {
@ -79,7 +80,7 @@ class PluginTest extends TestCase {
public function testPublishing() {
$this->book->expects($this->once())->method('setPublishStatus')->with(true);
$this->book->expects($this->once())->method('addPublicLink')->with();
// setup request
$request = new Request();
@ -92,7 +93,7 @@ class PluginTest extends TestCase {
public function testUnPublishing() {
$this->book->expects($this->once())->method('setPublishStatus')->with(false);
$this->book->expects($this->once())->method('removeAllPublicLinks')->with();
// setup request
$request = new Request();
@ -102,4 +103,17 @@ class PluginTest extends TestCase {
$response = new Response();
$this->plugin->httpPost($request, $response);
}
public function testUnPublishingALink() {
$this->book->expects($this->once())->method('removePublicLink')->with('urltounpublish');
// setup request
$request = new Request();
$request->addHeader('Content-Type', 'application/xml');
$request->setUrl('cal1');
$request->setBody('<o:unpublish-calendar xmlns:o="http://nextcloud.com/ns/">urltounpublish</o:unpublish-calendar>');
$response = new Response();
$this->plugin->httpPost($request, $response);
}
}