mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-01 05:29:20 +03:00
Remote Synchronization (CardDAV) improvements:
now it supports address book url auto detection (and works with Google contacts, icloud and etc.)
This commit is contained in:
parent
92f2caf700
commit
1fb553e77e
249 changed files with 1338 additions and 1453 deletions
|
|
@ -0,0 +1,526 @@
|
|||
<?php
|
||||
|
||||
namespace SabreForRainLoop\CalDAV;
|
||||
|
||||
use SabreForRainLoop\DAV;
|
||||
|
||||
/**
|
||||
* This plugin implements support for caldav sharing.
|
||||
*
|
||||
* This spec is defined at:
|
||||
* http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt
|
||||
*
|
||||
* See:
|
||||
* SabreForRainLoop\CalDAV\Backend\SharingSupport for all the documentation.
|
||||
*
|
||||
* Note: This feature is experimental, and may change in between different
|
||||
* SabreDAV versions.
|
||||
*
|
||||
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
|
||||
*/
|
||||
class SharingPlugin extends DAV\ServerPlugin {
|
||||
|
||||
/**
|
||||
* These are the various status constants used by sharing-messages.
|
||||
*/
|
||||
const STATUS_ACCEPTED = 1;
|
||||
const STATUS_DECLINED = 2;
|
||||
const STATUS_DELETED = 3;
|
||||
const STATUS_NORESPONSE = 4;
|
||||
const STATUS_INVALID = 5;
|
||||
|
||||
/**
|
||||
* Reference to SabreDAV server object.
|
||||
*
|
||||
* @var SabreForRainLoop\DAV\Server
|
||||
*/
|
||||
protected $server;
|
||||
|
||||
/**
|
||||
* This method should return a list of server-features.
|
||||
*
|
||||
* This is for example 'versioning' and is added to the DAV: header
|
||||
* in an OPTIONS response.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFeatures() {
|
||||
|
||||
return array('calendarserver-sharing');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a plugin name.
|
||||
*
|
||||
* Using this name other plugins will be able to access other plugins
|
||||
* using SabreForRainLoop\DAV\Server::getPlugin
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPluginName() {
|
||||
|
||||
return 'caldav-sharing';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This initializes the plugin.
|
||||
*
|
||||
* This function is called by SabreForRainLoop\DAV\Server, after
|
||||
* addPlugin is called.
|
||||
*
|
||||
* This method should set up the required event subscriptions.
|
||||
*
|
||||
* @param DAV\Server $server
|
||||
* @return void
|
||||
*/
|
||||
public function initialize(DAV\Server $server) {
|
||||
|
||||
$this->server = $server;
|
||||
$server->resourceTypeMapping['SabreForRainLoop\\CalDAV\\ISharedCalendar'] = '{' . Plugin::NS_CALENDARSERVER . '}shared';
|
||||
|
||||
array_push(
|
||||
$this->server->protectedProperties,
|
||||
'{' . Plugin::NS_CALENDARSERVER . '}invite',
|
||||
'{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes',
|
||||
'{' . Plugin::NS_CALENDARSERVER . '}shared-url'
|
||||
);
|
||||
|
||||
$this->server->subscribeEvent('beforeGetProperties', array($this, 'beforeGetProperties'));
|
||||
$this->server->subscribeEvent('afterGetProperties', array($this, 'afterGetProperties'));
|
||||
$this->server->subscribeEvent('updateProperties', array($this, 'updateProperties'));
|
||||
$this->server->subscribeEvent('unknownMethod', array($this,'unknownMethod'));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This event is triggered when properties are requested for a certain
|
||||
* node.
|
||||
*
|
||||
* This allows us to inject any properties early.
|
||||
*
|
||||
* @param string $path
|
||||
* @param DAV\INode $node
|
||||
* @param array $requestedProperties
|
||||
* @param array $returnedProperties
|
||||
* @return void
|
||||
*/
|
||||
public function beforeGetProperties($path, DAV\INode $node, &$requestedProperties, &$returnedProperties) {
|
||||
|
||||
if ($node instanceof IShareableCalendar) {
|
||||
if (($index = array_search('{' . Plugin::NS_CALENDARSERVER . '}invite', $requestedProperties))!==false) {
|
||||
|
||||
unset($requestedProperties[$index]);
|
||||
$returnedProperties[200]['{' . Plugin::NS_CALENDARSERVER . '}invite'] =
|
||||
new Property\Invite(
|
||||
$node->getShares()
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($node instanceof ISharedCalendar) {
|
||||
|
||||
if (($index = array_search('{' . Plugin::NS_CALENDARSERVER . '}shared-url', $requestedProperties))!==false) {
|
||||
|
||||
unset($requestedProperties[$index]);
|
||||
$returnedProperties[200]['{' . Plugin::NS_CALENDARSERVER . '}shared-url'] =
|
||||
new DAV\Property\Href(
|
||||
$node->getSharedUrl()
|
||||
);
|
||||
|
||||
}
|
||||
// The 'invite' property is slightly different for the 'shared'
|
||||
// instance of the calendar, as it also contains the owner
|
||||
// information.
|
||||
if (($index = array_search('{' . Plugin::NS_CALENDARSERVER . '}invite', $requestedProperties))!==false) {
|
||||
|
||||
unset($requestedProperties[$index]);
|
||||
|
||||
// Fetching owner information
|
||||
$props = $this->server->getPropertiesForPath($node->getOwner(), array(
|
||||
'{http://sabredav.org/ns}email-address',
|
||||
'{DAV:}displayname',
|
||||
), 1);
|
||||
|
||||
$ownerInfo = array(
|
||||
'href' => $node->getOwner(),
|
||||
);
|
||||
|
||||
if (isset($props[0][200])) {
|
||||
|
||||
// We're mapping the internal webdav properties to the
|
||||
// elements caldav-sharing expects.
|
||||
if (isset($props[0][200]['{http://sabredav.org/ns}email-address'])) {
|
||||
$ownerInfo['href'] = 'mailto:' . $props[0][200]['{http://sabredav.org/ns}email-address'];
|
||||
}
|
||||
if (isset($props[0][200]['{DAV:}displayname'])) {
|
||||
$ownerInfo['commonName'] = $props[0][200]['{DAV:}displayname'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$returnedProperties[200]['{' . Plugin::NS_CALENDARSERVER . '}invite'] =
|
||||
new Property\Invite(
|
||||
$node->getShares(),
|
||||
$ownerInfo
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is triggered *after* all properties have been retrieved.
|
||||
* This allows us to inject the correct resourcetype for calendars that
|
||||
* have been shared.
|
||||
*
|
||||
* @param string $path
|
||||
* @param array $properties
|
||||
* @param DAV\INode $node
|
||||
* @return void
|
||||
*/
|
||||
public function afterGetProperties($path, &$properties, DAV\INode $node) {
|
||||
|
||||
if ($node instanceof IShareableCalendar) {
|
||||
if (isset($properties[200]['{DAV:}resourcetype'])) {
|
||||
if (count($node->getShares())>0) {
|
||||
$properties[200]['{DAV:}resourcetype']->add(
|
||||
'{' . Plugin::NS_CALENDARSERVER . '}shared-owner'
|
||||
);
|
||||
}
|
||||
}
|
||||
$propName = '{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes';
|
||||
if (array_key_exists($propName, $properties[404])) {
|
||||
unset($properties[404][$propName]);
|
||||
$properties[200][$propName] = new Property\AllowedSharingModes(true,false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is trigged when a user attempts to update a node's
|
||||
* properties.
|
||||
*
|
||||
* A previous draft of the sharing spec stated that it was possible to use
|
||||
* PROPPATCH to remove 'shared-owner' from the resourcetype, thus unsharing
|
||||
* the calendar.
|
||||
*
|
||||
* Even though this is no longer in the current spec, we keep this around
|
||||
* because OS X 10.7 may still make use of this feature.
|
||||
*
|
||||
* @param array $mutations
|
||||
* @param array $result
|
||||
* @param DAV\INode $node
|
||||
* @return void
|
||||
*/
|
||||
public function updateProperties(array &$mutations, array &$result, DAV\INode $node) {
|
||||
|
||||
if (!$node instanceof IShareableCalendar)
|
||||
return;
|
||||
|
||||
if (!isset($mutations['{DAV:}resourcetype'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only doing something if shared-owner is indeed not in the list.
|
||||
if($mutations['{DAV:}resourcetype']->is('{' . Plugin::NS_CALENDARSERVER . '}shared-owner')) return;
|
||||
|
||||
$shares = $node->getShares();
|
||||
$remove = array();
|
||||
foreach($shares as $share) {
|
||||
$remove[] = $share['href'];
|
||||
}
|
||||
$node->updateShares(array(), $remove);
|
||||
|
||||
// We're marking this update as 200 OK
|
||||
$result[200]['{DAV:}resourcetype'] = null;
|
||||
|
||||
// Removing it from the mutations list
|
||||
unset($mutations['{DAV:}resourcetype']);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This event is triggered when the server didn't know how to handle a
|
||||
* certain request.
|
||||
*
|
||||
* We intercept this to handle POST requests on calendars.
|
||||
*
|
||||
* @param string $method
|
||||
* @param string $uri
|
||||
* @return null|bool
|
||||
*/
|
||||
public function unknownMethod($method, $uri) {
|
||||
|
||||
if ($method!=='POST') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handling xml
|
||||
$contentType = $this->server->httpRequest->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($uri);
|
||||
} catch (DAV\Exception\NotFound $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
$requestBody = $this->server->httpRequest->getBody(true);
|
||||
|
||||
// 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.
|
||||
$this->server->httpRequest->setBody($requestBody);
|
||||
|
||||
$dom = DAV\XMLUtil::loadDOMDocument($requestBody);
|
||||
|
||||
$documentType = DAV\XMLUtil::toClarkNotation($dom->firstChild);
|
||||
|
||||
switch($documentType) {
|
||||
|
||||
// Dealing with the 'share' document, which modified invitees on a
|
||||
// calendar.
|
||||
case '{' . Plugin::NS_CALENDARSERVER . '}share' :
|
||||
|
||||
// We can only deal with IShareableCalendar objects
|
||||
if (!$node instanceof IShareableCalendar) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Getting ACL info
|
||||
$acl = $this->server->getPlugin('acl');
|
||||
|
||||
// If there's no ACL support, we allow everything
|
||||
if ($acl) {
|
||||
$acl->checkPrivileges($uri, '{DAV:}write');
|
||||
}
|
||||
|
||||
$mutations = $this->parseShareRequest($dom);
|
||||
|
||||
$node->updateShares($mutations[0], $mutations[1]);
|
||||
|
||||
$this->server->httpResponse->sendStatus(200);
|
||||
// Adding this because sending a response body may cause issues,
|
||||
// and I wanted some type of indicator the response was handled.
|
||||
$this->server->httpResponse->setHeader('X-Sabre-Status', 'everything-went-well');
|
||||
|
||||
// Breaking the event chain
|
||||
return false;
|
||||
|
||||
// The invite-reply document is sent when the user replies to an
|
||||
// invitation of a calendar share.
|
||||
case '{'. Plugin::NS_CALENDARSERVER.'}invite-reply' :
|
||||
|
||||
// This only works on the calendar-home-root node.
|
||||
if (!$node instanceof UserCalendars) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Getting ACL info
|
||||
$acl = $this->server->getPlugin('acl');
|
||||
|
||||
// If there's no ACL support, we allow everything
|
||||
if ($acl) {
|
||||
$acl->checkPrivileges($uri, '{DAV:}write');
|
||||
}
|
||||
|
||||
$message = $this->parseInviteReplyRequest($dom);
|
||||
|
||||
$url = $node->shareReply(
|
||||
$message['href'],
|
||||
$message['status'],
|
||||
$message['calendarUri'],
|
||||
$message['inReplyTo'],
|
||||
$message['summary']
|
||||
);
|
||||
|
||||
$this->server->httpResponse->sendStatus(200);
|
||||
// Adding this because sending a response body may cause issues,
|
||||
// and I wanted some type of indicator the response was handled.
|
||||
$this->server->httpResponse->setHeader('X-Sabre-Status', 'everything-went-well');
|
||||
|
||||
if ($url) {
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$dom->formatOutput = true;
|
||||
|
||||
$root = $dom->createElement('cs:shared-as');
|
||||
foreach($this->server->xmlNamespaces as $namespace => $prefix) {
|
||||
$root->setAttribute('xmlns:' . $prefix, $namespace);
|
||||
}
|
||||
|
||||
$dom->appendChild($root);
|
||||
$href = new DAV\Property\Href($url);
|
||||
|
||||
$href->serialize($this->server, $root);
|
||||
$this->server->httpResponse->setHeader('Content-Type','application/xml');
|
||||
$this->server->httpResponse->sendBody($dom->saveXML());
|
||||
|
||||
}
|
||||
|
||||
// Breaking the event chain
|
||||
return false;
|
||||
|
||||
case '{' . Plugin::NS_CALENDARSERVER . '}publish-calendar' :
|
||||
|
||||
// We can only deal with IShareableCalendar objects
|
||||
if (!$node instanceof IShareableCalendar) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Getting ACL info
|
||||
$acl = $this->server->getPlugin('acl');
|
||||
|
||||
// If there's no ACL support, we allow everything
|
||||
if ($acl) {
|
||||
$acl->checkPrivileges($uri, '{DAV:}write');
|
||||
}
|
||||
|
||||
$node->setPublishStatus(true);
|
||||
|
||||
// iCloud sends back the 202, so we will too.
|
||||
$this->server->httpResponse->sendStatus(202);
|
||||
|
||||
// Adding this because sending a response body may cause issues,
|
||||
// and I wanted some type of indicator the response was handled.
|
||||
$this->server->httpResponse->setHeader('X-Sabre-Status', 'everything-went-well');
|
||||
|
||||
// Breaking the event chain
|
||||
return false;
|
||||
|
||||
case '{' . Plugin::NS_CALENDARSERVER . '}unpublish-calendar' :
|
||||
|
||||
// We can only deal with IShareableCalendar objects
|
||||
if (!$node instanceof IShareableCalendar) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Getting ACL info
|
||||
$acl = $this->server->getPlugin('acl');
|
||||
|
||||
// If there's no ACL support, we allow everything
|
||||
if ($acl) {
|
||||
$acl->checkPrivileges($uri, '{DAV:}write');
|
||||
}
|
||||
|
||||
$node->setPublishStatus(false);
|
||||
|
||||
$this->server->httpResponse->sendStatus(200);
|
||||
|
||||
// Adding this because sending a response body may cause issues,
|
||||
// and I wanted some type of indicator the response was handled.
|
||||
$this->server->httpResponse->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
|
||||
*/
|
||||
protected function parseShareRequest(\DOMDocument $dom) {
|
||||
|
||||
$xpath = new \DOMXPath($dom);
|
||||
$xpath->registerNamespace('cs', Plugin::NS_CALENDARSERVER);
|
||||
$xpath->registerNamespace('d', 'urn:DAV');
|
||||
|
||||
$set = array();
|
||||
$elems = $xpath->query('cs:set');
|
||||
|
||||
for($i=0; $i < $elems->length; $i++) {
|
||||
|
||||
$xset = $elems->item($i);
|
||||
$set[] = array(
|
||||
'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 = array();
|
||||
$elems = $xpath->query('cs:remove');
|
||||
|
||||
for($i=0; $i < $elems->length; $i++) {
|
||||
|
||||
$xremove = $elems->item($i);
|
||||
$remove[] = $xpath->evaluate('string(d:href)', $xremove);
|
||||
|
||||
}
|
||||
|
||||
return array($set, $remove);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the 'invite-reply' POST request.
|
||||
*
|
||||
* This method returns an array, containing the following properties:
|
||||
* * href - The sharee who is replying
|
||||
* * status - One of the self::STATUS_* constants
|
||||
* * calendarUri - The url of the shared calendar
|
||||
* * inReplyTo - The unique id of the share invitation.
|
||||
* * summary - Optional description of the reply.
|
||||
*
|
||||
* @param \DOMDocument $dom
|
||||
* @return array
|
||||
*/
|
||||
protected function parseInviteReplyRequest(\DOMDocument $dom) {
|
||||
|
||||
$xpath = new \DOMXPath($dom);
|
||||
$xpath->registerNamespace('cs', Plugin::NS_CALENDARSERVER);
|
||||
$xpath->registerNamespace('d', 'urn:DAV');
|
||||
|
||||
$hostHref = $xpath->evaluate('string(cs:hosturl/d:href)');
|
||||
if (!$hostHref) {
|
||||
throw new DAV\Exception\BadRequest('The {' . Plugin::NS_CALENDARSERVER . '}hosturl/{DAV:}href element is required');
|
||||
}
|
||||
|
||||
return array(
|
||||
'href' => $xpath->evaluate('string(d:href)'),
|
||||
'calendarUri' => $this->server->calculateUri($hostHref),
|
||||
'inReplyTo' => $xpath->evaluate('string(cs:in-reply-to)'),
|
||||
'summary' => $xpath->evaluate('string(cs:summary)'),
|
||||
'status' => $xpath->evaluate('boolean(cs:invite-accepted)')?self::STATUS_ACCEPTED:self::STATUS_DECLINED
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue