PageRenderTime 47ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/Sabre/DAV/Sync/Plugin.php

https://github.com/KOLANICH/SabreDAV
PHP | 342 lines | 159 code | 73 blank | 110 comment | 22 complexity | e24403490a0e26e5c257652ebf1b7f53 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. namespace Sabre\DAV\Sync;
  3. use Sabre\DAV;
  4. /**
  5. * This plugin all WebDAV-sync capabilities to the Server.
  6. *
  7. * WebDAV-sync is defined by rfc6578
  8. *
  9. * The sync capabilities only work with collections that implement
  10. * Sabreu\DAV\Sync\ISyncCollection.
  11. *
  12. * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
  13. * @author Evert Pot (http://evertpot.com/)
  14. * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
  15. */
  16. class Plugin extends DAV\ServerPlugin {
  17. /**
  18. * Reference to server object
  19. *
  20. * @var DAV\Server
  21. */
  22. protected $server;
  23. const SYNCTOKEN_PREFIX = 'http://sabredav.org/ns/sync/';
  24. /**
  25. * Returns a plugin name.
  26. *
  27. * Using this name other plugins will be able to access other plugins
  28. * using \Sabre\DAV\Server::getPlugin
  29. *
  30. * @return string
  31. */
  32. public function getPluginName() {
  33. return 'sync';
  34. }
  35. /**
  36. * Initializes the plugin.
  37. *
  38. * This is when the plugin registers it's hooks.
  39. *
  40. * @param DAV\Server $server
  41. * @return void
  42. */
  43. public function initialize(DAV\Server $server) {
  44. $this->server = $server;
  45. $self = $this;
  46. $server->subscribeEvent('report', function($reportName, $dom, $uri) use ($self) {
  47. if ($reportName === '{DAV:}sync-collection') {
  48. $this->server->transactionType = 'report-sync-collection';
  49. $self->syncCollection($uri, $dom);
  50. return false;
  51. }
  52. });
  53. $server->subscribeEvent('beforeGetProperties', array($this, 'beforeGetProperties'));
  54. $server->subscribeEvent('validateTokens', array($this, 'validateTokens'));
  55. }
  56. /**
  57. * Returns a list of reports this plugin supports.
  58. *
  59. * This will be used in the {DAV:}supported-report-set property.
  60. * Note that you still need to subscribe to the 'report' event to actually
  61. * implement them
  62. *
  63. * @param string $uri
  64. * @return array
  65. */
  66. public function getSupportedReportSet($uri) {
  67. $node = $this->server->tree->getNodeForPath($uri);
  68. if ($node instanceof ISyncCollection && $node->getSyncToken()) {
  69. return array(
  70. '{DAV:}sync-collection',
  71. );
  72. }
  73. return array();
  74. }
  75. /**
  76. * This method handles the {DAV:}sync-collection HTTP REPORT.
  77. *
  78. * @param string $uri
  79. * @param \DOMDocument $dom
  80. * @return void
  81. */
  82. public function syncCollection($uri, \DOMDocument $dom) {
  83. // rfc3253 specifies 0 is the default value for Depth:
  84. $depth = $this->server->getHTTPDepth(0);
  85. list(
  86. $syncToken,
  87. $syncLevel,
  88. $limit,
  89. $properties
  90. ) = $this->parseSyncCollectionRequest($dom, $depth);
  91. // Getting the data
  92. $node = $this->server->tree->getNodeForPath($uri);
  93. if (!$node instanceof ISyncCollection) {
  94. throw new DAV\Exception\ReportNotSupported('The {DAV:}sync-collection REPORT is not supported on this url.');
  95. }
  96. $token = $node->getSyncToken();
  97. if (!$token) {
  98. throw new DAV\Exception\ReportNotSupported('No sync information is available at this node');
  99. }
  100. if (!is_null($syncToken)) {
  101. // Sync-token must start with our prefix
  102. if (substr($syncToken, 0, strlen(self::SYNCTOKEN_PREFIX)) !== self::SYNCTOKEN_PREFIX) {
  103. throw new DAV\Exception\InvalidSyncToken('Invalid or unknown sync token');
  104. }
  105. $syncToken = substr($syncToken, strlen(self::SYNCTOKEN_PREFIX));
  106. }
  107. $changeInfo = $node->getChanges($syncToken, $syncLevel, $limit);
  108. if (is_null($changeInfo)) {
  109. throw new DAV\Exception\InvalidSyncToken('Invalid or unknown sync token');
  110. }
  111. // Encoding the response
  112. $this->sendSyncCollectionResponse(
  113. $changeInfo['syncToken'],
  114. $uri,
  115. $changeInfo['added'],
  116. $changeInfo['modified'],
  117. $changeInfo['deleted'],
  118. $properties
  119. );
  120. }
  121. /**
  122. * Parses the {DAV:}sync-collection REPORT request body.
  123. *
  124. * This method returns an array with 3 values:
  125. * 0 - the value of the {DAV:}sync-token element
  126. * 1 - the value of the {DAV:}sync-level element
  127. * 2 - The value of the {DAV:}limit element
  128. * 3 - A list of requested properties
  129. *
  130. * @param \DOMDocument $dom
  131. * @param int $depth
  132. * @return void
  133. */
  134. protected function parseSyncCollectionRequest(\DOMDocument $dom, $depth) {
  135. $xpath = new \DOMXPath($dom);
  136. $xpath->registerNamespace('d','urn:DAV');
  137. $syncToken = $xpath->query("//d:sync-token");
  138. if ($syncToken->length !== 1) {
  139. throw new DAV\Exception\BadRequest('You must specify a {DAV:}sync-token element, and it must appear exactly once');
  140. }
  141. $syncToken = $syncToken->item(0)->nodeValue;
  142. // Initial sync
  143. if (!$syncToken) $syncToken = null;
  144. $syncLevel = $xpath->query("//d:sync-level");
  145. if ($syncLevel->length === 0) {
  146. // In case there was no sync-level, it could mean that we're dealing
  147. // with an old client. For these we must use the depth header
  148. // instead.
  149. $syncLevel = $depth;
  150. } else {
  151. $syncLevel = $syncLevel->item(0)->nodeValue;
  152. if ($syncLevel === 'infinite') {
  153. $syncLevel = DAV\Server::DEPTH_INFINITY;
  154. }
  155. }
  156. $limit = $xpath->query("//d:limit/d:nresults");
  157. if ($limit->length === 0) {
  158. $limit = null;
  159. } else {
  160. $limit = $limit->item(0)->nodeValue;
  161. }
  162. $prop = $xpath->query('d:prop');
  163. if ($prop->length !== 1) {
  164. throw new DAV\Exception\BadRequest('The {DAV:}sync-collection must contain extactly 1 {DAV:}prop');
  165. }
  166. $properties = array_keys(
  167. DAV\XMLUtil::parseProperties($dom->documentElement)
  168. );
  169. return array(
  170. $syncToken,
  171. $syncLevel,
  172. $limit,
  173. $properties,
  174. );
  175. }
  176. /**
  177. * Sends the response to a sync-collection request.
  178. *
  179. * @param string $syncToken
  180. * @param string $collectionUrl
  181. * @param array $added
  182. * @param array $modified
  183. * @param array $deleted
  184. * @param array $properties
  185. * @return void
  186. */
  187. protected function sendSyncCollectionResponse($syncToken, $collectionUrl, array $added, array $modified, array $deleted, array $properties) {
  188. $dom = new \DOMDocument('1.0','utf-8');
  189. $dom->formatOutput = true;
  190. $multiStatus = $dom->createElement('d:multistatus');
  191. $dom->appendChild($multiStatus);
  192. // Adding in default namespaces
  193. foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
  194. $multiStatus->setAttribute('xmlns:' . $prefix,$namespace);
  195. }
  196. $fullPaths = [];
  197. // Pre-fetching children, if this is possible.
  198. foreach(array_merge($added, $modified) as $item) {
  199. $fullPath = $collectionUrl . '/' . $item;
  200. $fullPaths[] = $fullPath;
  201. }
  202. foreach($this->server->getPropertiesForMultiplePaths($fullPaths, $properties) as $fullPath => $props) {
  203. // The 'Property_Response' class is responsible for generating a
  204. // single {DAV:}response xml element.
  205. $response = new DAV\Property\Response($fullPath, $props);
  206. $response->serialize($this->server, $multiStatus);
  207. }
  208. // Deleted items also show up as 'responses'. They have no properties,
  209. // and a single {DAV:}status element set as 'HTTP/1.1 404 Not Found'.
  210. foreach($deleted as $item) {
  211. $fullPath = $collectionUrl . '/' . $item;
  212. $response = new DAV\Property\Response($fullPath, array(), 404);
  213. $response->serialize($this->server, $multiStatus);
  214. }
  215. $syncToken = $dom->createElement('d:sync-token', self::SYNCTOKEN_PREFIX . $syncToken);
  216. $multiStatus->appendChild($syncToken);
  217. $this->server->httpResponse->sendStatus(207);
  218. $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
  219. $this->server->httpResponse->sendBody($dom->saveXML());
  220. }
  221. /**
  222. * This method is triggered whenever properties are requested for a node.
  223. * We intercept this to see if we can must return a {DAV:}sync-token.
  224. *
  225. * @param string $path
  226. * @param DAV\INode $node
  227. * @param array $requestedProperties
  228. * @param array $returnedProperties
  229. * @return void
  230. */
  231. public function beforeGetProperties($path, DAV\INode $node, array &$requestedProperties, array &$returnedProperties) {
  232. if (!in_array('{DAV:}sync-token', $requestedProperties)) {
  233. return;
  234. }
  235. if ($node instanceof ISyncCollection && $token = $node->getSyncToken()) {
  236. // Unsetting the property from requested properties.
  237. $index = array_search('{DAV:}sync-token', $requestedProperties);
  238. unset($requestedProperties[$index]);
  239. $returnedProperties[200]['{DAV:}sync-token'] = self::SYNCTOKEN_PREFIX . $token;
  240. }
  241. }
  242. /**
  243. * The validateTokens event is triggered before every request.
  244. *
  245. * It's a moment where this plugin can check all the supplied lock tokens
  246. * in the If: header, and check if they are valid.
  247. *
  248. * @param mixed $conditions
  249. * @return void
  250. */
  251. public function validateTokens( &$conditions ) {
  252. foreach($conditions as $kk=>$condition) {
  253. foreach($condition['tokens'] as $ii=>$token) {
  254. // Sync-tokens must always start with our designated prefix.
  255. if (substr($token['token'], 0, strlen(self::SYNCTOKEN_PREFIX)) !== self::SYNCTOKEN_PREFIX) {
  256. continue;
  257. }
  258. // Checking if the token is a match.
  259. $node = $this->server->tree->getNodeForPath($condition['uri']);
  260. if (
  261. $node instanceof ISyncCollection &&
  262. $node->getSyncToken() == substr($token['token'], strlen(self::SYNCTOKEN_PREFIX))
  263. ) {
  264. $conditions[$kk]['tokens'][$ii]['validToken'] = true;
  265. }
  266. }
  267. }
  268. }
  269. }