1*a1a3b679SAndreas Boehler<?php 2*a1a3b679SAndreas Boehler 3*a1a3b679SAndreas Boehlernamespace Sabre\DAV\Sync; 4*a1a3b679SAndreas Boehler 5*a1a3b679SAndreas Boehleruse Sabre\DAV; 6*a1a3b679SAndreas Boehleruse Sabre\HTTP\RequestInterface; 7*a1a3b679SAndreas Boehleruse Sabre\DAV\Xml\Request\SyncCollectionReport; 8*a1a3b679SAndreas Boehler 9*a1a3b679SAndreas Boehler/** 10*a1a3b679SAndreas Boehler * This plugin all WebDAV-sync capabilities to the Server. 11*a1a3b679SAndreas Boehler * 12*a1a3b679SAndreas Boehler * WebDAV-sync is defined by rfc6578 13*a1a3b679SAndreas Boehler * 14*a1a3b679SAndreas Boehler * The sync capabilities only work with collections that implement 15*a1a3b679SAndreas Boehler * Sabre\DAV\Sync\ISyncCollection. 16*a1a3b679SAndreas Boehler * 17*a1a3b679SAndreas Boehler * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/). 18*a1a3b679SAndreas Boehler * @author Evert Pot (http://evertpot.com/) 19*a1a3b679SAndreas Boehler * @license http://sabre.io/license/ Modified BSD License 20*a1a3b679SAndreas Boehler */ 21*a1a3b679SAndreas Boehlerclass Plugin extends DAV\ServerPlugin { 22*a1a3b679SAndreas Boehler 23*a1a3b679SAndreas Boehler /** 24*a1a3b679SAndreas Boehler * Reference to server object 25*a1a3b679SAndreas Boehler * 26*a1a3b679SAndreas Boehler * @var DAV\Server 27*a1a3b679SAndreas Boehler */ 28*a1a3b679SAndreas Boehler protected $server; 29*a1a3b679SAndreas Boehler 30*a1a3b679SAndreas Boehler const SYNCTOKEN_PREFIX = 'http://sabre.io/ns/sync/'; 31*a1a3b679SAndreas Boehler 32*a1a3b679SAndreas Boehler /** 33*a1a3b679SAndreas Boehler * Returns a plugin name. 34*a1a3b679SAndreas Boehler * 35*a1a3b679SAndreas Boehler * Using this name other plugins will be able to access other plugins 36*a1a3b679SAndreas Boehler * using \Sabre\DAV\Server::getPlugin 37*a1a3b679SAndreas Boehler * 38*a1a3b679SAndreas Boehler * @return string 39*a1a3b679SAndreas Boehler */ 40*a1a3b679SAndreas Boehler function getPluginName() { 41*a1a3b679SAndreas Boehler 42*a1a3b679SAndreas Boehler return 'sync'; 43*a1a3b679SAndreas Boehler 44*a1a3b679SAndreas Boehler } 45*a1a3b679SAndreas Boehler 46*a1a3b679SAndreas Boehler /** 47*a1a3b679SAndreas Boehler * Initializes the plugin. 48*a1a3b679SAndreas Boehler * 49*a1a3b679SAndreas Boehler * This is when the plugin registers it's hooks. 50*a1a3b679SAndreas Boehler * 51*a1a3b679SAndreas Boehler * @param DAV\Server $server 52*a1a3b679SAndreas Boehler * @return void 53*a1a3b679SAndreas Boehler */ 54*a1a3b679SAndreas Boehler function initialize(DAV\Server $server) { 55*a1a3b679SAndreas Boehler 56*a1a3b679SAndreas Boehler $this->server = $server; 57*a1a3b679SAndreas Boehler $server->xml->elementMap['{DAV:}sync-collection'] = 'Sabre\\DAV\\Xml\\Request\\SyncCollectionReport'; 58*a1a3b679SAndreas Boehler 59*a1a3b679SAndreas Boehler $self = $this; 60*a1a3b679SAndreas Boehler 61*a1a3b679SAndreas Boehler $server->on('report', function($reportName, $dom, $uri) use ($self) { 62*a1a3b679SAndreas Boehler 63*a1a3b679SAndreas Boehler if ($reportName === '{DAV:}sync-collection') { 64*a1a3b679SAndreas Boehler $this->server->transactionType = 'report-sync-collection'; 65*a1a3b679SAndreas Boehler $self->syncCollection($uri, $dom); 66*a1a3b679SAndreas Boehler return false; 67*a1a3b679SAndreas Boehler } 68*a1a3b679SAndreas Boehler 69*a1a3b679SAndreas Boehler }); 70*a1a3b679SAndreas Boehler 71*a1a3b679SAndreas Boehler $server->on('propFind', [$this, 'propFind']); 72*a1a3b679SAndreas Boehler $server->on('validateTokens', [$this, 'validateTokens']); 73*a1a3b679SAndreas Boehler 74*a1a3b679SAndreas Boehler } 75*a1a3b679SAndreas Boehler 76*a1a3b679SAndreas Boehler /** 77*a1a3b679SAndreas Boehler * Returns a list of reports this plugin supports. 78*a1a3b679SAndreas Boehler * 79*a1a3b679SAndreas Boehler * This will be used in the {DAV:}supported-report-set property. 80*a1a3b679SAndreas Boehler * Note that you still need to subscribe to the 'report' event to actually 81*a1a3b679SAndreas Boehler * implement them 82*a1a3b679SAndreas Boehler * 83*a1a3b679SAndreas Boehler * @param string $uri 84*a1a3b679SAndreas Boehler * @return array 85*a1a3b679SAndreas Boehler */ 86*a1a3b679SAndreas Boehler function getSupportedReportSet($uri) { 87*a1a3b679SAndreas Boehler 88*a1a3b679SAndreas Boehler $node = $this->server->tree->getNodeForPath($uri); 89*a1a3b679SAndreas Boehler if ($node instanceof ISyncCollection && $node->getSyncToken()) { 90*a1a3b679SAndreas Boehler return [ 91*a1a3b679SAndreas Boehler '{DAV:}sync-collection', 92*a1a3b679SAndreas Boehler ]; 93*a1a3b679SAndreas Boehler } 94*a1a3b679SAndreas Boehler 95*a1a3b679SAndreas Boehler return []; 96*a1a3b679SAndreas Boehler 97*a1a3b679SAndreas Boehler } 98*a1a3b679SAndreas Boehler 99*a1a3b679SAndreas Boehler 100*a1a3b679SAndreas Boehler /** 101*a1a3b679SAndreas Boehler * This method handles the {DAV:}sync-collection HTTP REPORT. 102*a1a3b679SAndreas Boehler * 103*a1a3b679SAndreas Boehler * @param string $uri 104*a1a3b679SAndreas Boehler * @param SyncCollectionReport $report 105*a1a3b679SAndreas Boehler * @return void 106*a1a3b679SAndreas Boehler */ 107*a1a3b679SAndreas Boehler function syncCollection($uri, SyncCollectionReport $report) { 108*a1a3b679SAndreas Boehler 109*a1a3b679SAndreas Boehler // Getting the data 110*a1a3b679SAndreas Boehler $node = $this->server->tree->getNodeForPath($uri); 111*a1a3b679SAndreas Boehler if (!$node instanceof ISyncCollection) { 112*a1a3b679SAndreas Boehler throw new DAV\Exception\ReportNotSupported('The {DAV:}sync-collection REPORT is not supported on this url.'); 113*a1a3b679SAndreas Boehler } 114*a1a3b679SAndreas Boehler $token = $node->getSyncToken(); 115*a1a3b679SAndreas Boehler if (!$token) { 116*a1a3b679SAndreas Boehler throw new DAV\Exception\ReportNotSupported('No sync information is available at this node'); 117*a1a3b679SAndreas Boehler } 118*a1a3b679SAndreas Boehler 119*a1a3b679SAndreas Boehler $syncToken = $report->syncToken; 120*a1a3b679SAndreas Boehler if (!is_null($syncToken)) { 121*a1a3b679SAndreas Boehler // Sync-token must start with our prefix 122*a1a3b679SAndreas Boehler if (substr($syncToken, 0, strlen(self::SYNCTOKEN_PREFIX)) !== self::SYNCTOKEN_PREFIX) { 123*a1a3b679SAndreas Boehler throw new DAV\Exception\InvalidSyncToken('Invalid or unknown sync token'); 124*a1a3b679SAndreas Boehler } 125*a1a3b679SAndreas Boehler 126*a1a3b679SAndreas Boehler $syncToken = substr($syncToken, strlen(self::SYNCTOKEN_PREFIX)); 127*a1a3b679SAndreas Boehler 128*a1a3b679SAndreas Boehler } 129*a1a3b679SAndreas Boehler $changeInfo = $node->getChanges($syncToken, $report->syncLevel, $report->limit); 130*a1a3b679SAndreas Boehler 131*a1a3b679SAndreas Boehler if (is_null($changeInfo)) { 132*a1a3b679SAndreas Boehler 133*a1a3b679SAndreas Boehler throw new DAV\Exception\InvalidSyncToken('Invalid or unknown sync token'); 134*a1a3b679SAndreas Boehler 135*a1a3b679SAndreas Boehler } 136*a1a3b679SAndreas Boehler 137*a1a3b679SAndreas Boehler // Encoding the response 138*a1a3b679SAndreas Boehler $this->sendSyncCollectionResponse( 139*a1a3b679SAndreas Boehler $changeInfo['syncToken'], 140*a1a3b679SAndreas Boehler $uri, 141*a1a3b679SAndreas Boehler $changeInfo['added'], 142*a1a3b679SAndreas Boehler $changeInfo['modified'], 143*a1a3b679SAndreas Boehler $changeInfo['deleted'], 144*a1a3b679SAndreas Boehler $report->properties 145*a1a3b679SAndreas Boehler ); 146*a1a3b679SAndreas Boehler 147*a1a3b679SAndreas Boehler } 148*a1a3b679SAndreas Boehler 149*a1a3b679SAndreas Boehler /** 150*a1a3b679SAndreas Boehler * Sends the response to a sync-collection request. 151*a1a3b679SAndreas Boehler * 152*a1a3b679SAndreas Boehler * @param string $syncToken 153*a1a3b679SAndreas Boehler * @param string $collectionUrl 154*a1a3b679SAndreas Boehler * @param array $added 155*a1a3b679SAndreas Boehler * @param array $modified 156*a1a3b679SAndreas Boehler * @param array $deleted 157*a1a3b679SAndreas Boehler * @param array $properties 158*a1a3b679SAndreas Boehler * @return void 159*a1a3b679SAndreas Boehler */ 160*a1a3b679SAndreas Boehler protected function sendSyncCollectionResponse($syncToken, $collectionUrl, array $added, array $modified, array $deleted, array $properties) { 161*a1a3b679SAndreas Boehler 162*a1a3b679SAndreas Boehler 163*a1a3b679SAndreas Boehler $fullPaths = []; 164*a1a3b679SAndreas Boehler 165*a1a3b679SAndreas Boehler // Pre-fetching children, if this is possible. 166*a1a3b679SAndreas Boehler foreach (array_merge($added, $modified) as $item) { 167*a1a3b679SAndreas Boehler $fullPath = $collectionUrl . '/' . $item; 168*a1a3b679SAndreas Boehler $fullPaths[] = $fullPath; 169*a1a3b679SAndreas Boehler } 170*a1a3b679SAndreas Boehler 171*a1a3b679SAndreas Boehler $responses = []; 172*a1a3b679SAndreas Boehler foreach ($this->server->getPropertiesForMultiplePaths($fullPaths, $properties) as $fullPath => $props) { 173*a1a3b679SAndreas Boehler 174*a1a3b679SAndreas Boehler // The 'Property_Response' class is responsible for generating a 175*a1a3b679SAndreas Boehler // single {DAV:}response xml element. 176*a1a3b679SAndreas Boehler $responses[] = new DAV\Xml\Element\Response($fullPath, $props); 177*a1a3b679SAndreas Boehler 178*a1a3b679SAndreas Boehler } 179*a1a3b679SAndreas Boehler 180*a1a3b679SAndreas Boehler 181*a1a3b679SAndreas Boehler 182*a1a3b679SAndreas Boehler // Deleted items also show up as 'responses'. They have no properties, 183*a1a3b679SAndreas Boehler // and a single {DAV:}status element set as 'HTTP/1.1 404 Not Found'. 184*a1a3b679SAndreas Boehler foreach ($deleted as $item) { 185*a1a3b679SAndreas Boehler 186*a1a3b679SAndreas Boehler $fullPath = $collectionUrl . '/' . $item; 187*a1a3b679SAndreas Boehler $responses[] = new DAV\Xml\Element\Response($fullPath, [], 404); 188*a1a3b679SAndreas Boehler 189*a1a3b679SAndreas Boehler } 190*a1a3b679SAndreas Boehler $multiStatus = new DAV\Xml\Response\MultiStatus($responses, self::SYNCTOKEN_PREFIX . $syncToken); 191*a1a3b679SAndreas Boehler 192*a1a3b679SAndreas Boehler $this->server->httpResponse->setStatus(207); 193*a1a3b679SAndreas Boehler $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); 194*a1a3b679SAndreas Boehler $this->server->httpResponse->setBody( 195*a1a3b679SAndreas Boehler $this->server->xml->write('{DAV:}multistatus', $multiStatus, $this->server->getBaseUri()) 196*a1a3b679SAndreas Boehler ); 197*a1a3b679SAndreas Boehler 198*a1a3b679SAndreas Boehler } 199*a1a3b679SAndreas Boehler 200*a1a3b679SAndreas Boehler /** 201*a1a3b679SAndreas Boehler * This method is triggered whenever properties are requested for a node. 202*a1a3b679SAndreas Boehler * We intercept this to see if we must return a {DAV:}sync-token. 203*a1a3b679SAndreas Boehler * 204*a1a3b679SAndreas Boehler * @param DAV\PropFind $propFind 205*a1a3b679SAndreas Boehler * @param DAV\INode $node 206*a1a3b679SAndreas Boehler * @return void 207*a1a3b679SAndreas Boehler */ 208*a1a3b679SAndreas Boehler function propFind(DAV\PropFind $propFind, DAV\INode $node) { 209*a1a3b679SAndreas Boehler 210*a1a3b679SAndreas Boehler $propFind->handle('{DAV:}sync-token', function() use ($node) { 211*a1a3b679SAndreas Boehler if (!$node instanceof ISyncCollection || !$token = $node->getSyncToken()) { 212*a1a3b679SAndreas Boehler return; 213*a1a3b679SAndreas Boehler } 214*a1a3b679SAndreas Boehler return self::SYNCTOKEN_PREFIX . $token; 215*a1a3b679SAndreas Boehler }); 216*a1a3b679SAndreas Boehler 217*a1a3b679SAndreas Boehler } 218*a1a3b679SAndreas Boehler 219*a1a3b679SAndreas Boehler /** 220*a1a3b679SAndreas Boehler * The validateTokens event is triggered before every request. 221*a1a3b679SAndreas Boehler * 222*a1a3b679SAndreas Boehler * It's a moment where this plugin can check all the supplied lock tokens 223*a1a3b679SAndreas Boehler * in the If: header, and check if they are valid. 224*a1a3b679SAndreas Boehler * 225*a1a3b679SAndreas Boehler * @param RequestInterface $request 226*a1a3b679SAndreas Boehler * @param array $conditions 227*a1a3b679SAndreas Boehler * @return void 228*a1a3b679SAndreas Boehler */ 229*a1a3b679SAndreas Boehler function validateTokens(RequestInterface $request, &$conditions) { 230*a1a3b679SAndreas Boehler 231*a1a3b679SAndreas Boehler foreach ($conditions as $kk => $condition) { 232*a1a3b679SAndreas Boehler 233*a1a3b679SAndreas Boehler foreach ($condition['tokens'] as $ii => $token) { 234*a1a3b679SAndreas Boehler 235*a1a3b679SAndreas Boehler // Sync-tokens must always start with our designated prefix. 236*a1a3b679SAndreas Boehler if (substr($token['token'], 0, strlen(self::SYNCTOKEN_PREFIX)) !== self::SYNCTOKEN_PREFIX) { 237*a1a3b679SAndreas Boehler continue; 238*a1a3b679SAndreas Boehler } 239*a1a3b679SAndreas Boehler 240*a1a3b679SAndreas Boehler // Checking if the token is a match. 241*a1a3b679SAndreas Boehler $node = $this->server->tree->getNodeForPath($condition['uri']); 242*a1a3b679SAndreas Boehler 243*a1a3b679SAndreas Boehler if ( 244*a1a3b679SAndreas Boehler $node instanceof ISyncCollection && 245*a1a3b679SAndreas Boehler $node->getSyncToken() == substr($token['token'], strlen(self::SYNCTOKEN_PREFIX)) 246*a1a3b679SAndreas Boehler ) { 247*a1a3b679SAndreas Boehler $conditions[$kk]['tokens'][$ii]['validToken'] = true; 248*a1a3b679SAndreas Boehler } 249*a1a3b679SAndreas Boehler 250*a1a3b679SAndreas Boehler } 251*a1a3b679SAndreas Boehler 252*a1a3b679SAndreas Boehler } 253*a1a3b679SAndreas Boehler 254*a1a3b679SAndreas Boehler } 255*a1a3b679SAndreas Boehler 256*a1a3b679SAndreas Boehler /** 257*a1a3b679SAndreas Boehler * Returns a bunch of meta-data about the plugin. 258*a1a3b679SAndreas Boehler * 259*a1a3b679SAndreas Boehler * Providing this information is optional, and is mainly displayed by the 260*a1a3b679SAndreas Boehler * Browser plugin. 261*a1a3b679SAndreas Boehler * 262*a1a3b679SAndreas Boehler * The description key in the returned array may contain html and will not 263*a1a3b679SAndreas Boehler * be sanitized. 264*a1a3b679SAndreas Boehler * 265*a1a3b679SAndreas Boehler * @return array 266*a1a3b679SAndreas Boehler */ 267*a1a3b679SAndreas Boehler function getPluginInfo() { 268*a1a3b679SAndreas Boehler 269*a1a3b679SAndreas Boehler return [ 270*a1a3b679SAndreas Boehler 'name' => $this->getPluginName(), 271*a1a3b679SAndreas Boehler 'description' => 'Adds support for WebDAV Collection Sync (rfc6578)', 272*a1a3b679SAndreas Boehler 'link' => 'http://sabre.io/dav/sync/', 273*a1a3b679SAndreas Boehler ]; 274*a1a3b679SAndreas Boehler 275*a1a3b679SAndreas Boehler } 276*a1a3b679SAndreas Boehler 277*a1a3b679SAndreas Boehler} 278