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