PageRenderTime 46ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Sabre/CardDAV/Plugin.php

https://github.com/agilastic/sabre-dav
PHP | 720 lines | 344 code | 159 blank | 217 comment | 76 complexity | 4b79fdcd50fbaadbeb3d44843dd39386 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. namespace Sabre\CardDAV;
  3. use Sabre\DAV;
  4. use Sabre\DAVACL;
  5. use Sabre\VObject;
  6. /**
  7. * CardDAV plugin
  8. *
  9. * The CardDAV plugin adds CardDAV functionality to the WebDAV server
  10. *
  11. * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
  12. * @author Evert Pot (http://evertpot.com/)
  13. * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
  14. */
  15. class Plugin extends DAV\ServerPlugin {
  16. /**
  17. * Url to the addressbooks
  18. */
  19. const ADDRESSBOOK_ROOT = 'addressbooks';
  20. /**
  21. * xml namespace for CardDAV elements
  22. */
  23. const NS_CARDDAV = 'urn:ietf:params:xml:ns:carddav';
  24. /**
  25. * Add urls to this property to have them automatically exposed as
  26. * 'directories' to the user.
  27. *
  28. * @var array
  29. */
  30. public $directories = array();
  31. /**
  32. * Server class
  33. *
  34. * @var Sabre\DAV\Server
  35. */
  36. protected $server;
  37. /**
  38. * Initializes the plugin
  39. *
  40. * @param DAV\Server $server
  41. * @return void
  42. */
  43. public function initialize(DAV\Server $server) {
  44. /* Events */
  45. $server->on('beforeGetProperties', [$this, 'beforeGetProperties']);
  46. $server->on('afterGetProperties', [$this, 'afterGetProperties']);
  47. $server->on('updateProperties', [$this, 'updateProperties']);
  48. $server->on('report', [$this, 'report']);
  49. $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']);
  50. $server->on('onBrowserPostAction', [$this, 'browserPostAction']);
  51. $server->on('beforeWriteContent', [$this, 'beforeWriteContent']);
  52. $server->on('beforeCreateFile', [$this, 'beforeCreateFile']);
  53. /* Namespaces */
  54. $server->xmlNamespaces[self::NS_CARDDAV] = 'card';
  55. /* Mapping Interfaces to {DAV:}resourcetype values */
  56. $server->resourceTypeMapping['Sabre\\CardDAV\\IAddressBook'] = '{' . self::NS_CARDDAV . '}addressbook';
  57. $server->resourceTypeMapping['Sabre\\CardDAV\\IDirectory'] = '{' . self::NS_CARDDAV . '}directory';
  58. /* Adding properties that may never be changed */
  59. $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}supported-address-data';
  60. $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}max-resource-size';
  61. $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}addressbook-home-set';
  62. $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}supported-collation-set';
  63. $server->propertyMap['{http://calendarserver.org/ns/}me-card'] = 'Sabre\\DAV\\Property\\Href';
  64. $this->server = $server;
  65. }
  66. /**
  67. * Returns a list of supported features.
  68. *
  69. * This is used in the DAV: header in the OPTIONS and PROPFIND requests.
  70. *
  71. * @return array
  72. */
  73. public function getFeatures() {
  74. return ['addressbook'];
  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. public function getSupportedReportSet($uri) {
  87. $node = $this->server->tree->getNodeForPath($uri);
  88. if ($node instanceof IAddressBook || $node instanceof ICard) {
  89. return array(
  90. '{' . self::NS_CARDDAV . '}addressbook-multiget',
  91. '{' . self::NS_CARDDAV . '}addressbook-query',
  92. );
  93. }
  94. return array();
  95. }
  96. /**
  97. * Adds all CardDAV-specific properties
  98. *
  99. * @param string $path
  100. * @param DAV\INode $node
  101. * @param array $requestedProperties
  102. * @param array $returnedProperties
  103. * @return void
  104. */
  105. public function beforeGetProperties($path, DAV\INode $node, array &$requestedProperties, array &$returnedProperties) {
  106. if ($node instanceof DAVACL\IPrincipal) {
  107. // calendar-home-set property
  108. $addHome = '{' . self::NS_CARDDAV . '}addressbook-home-set';
  109. if (in_array($addHome,$requestedProperties)) {
  110. $principalId = $node->getName();
  111. $addressbookHomePath = self::ADDRESSBOOK_ROOT . '/' . $principalId . '/';
  112. unset($requestedProperties[array_search($addHome, $requestedProperties)]);
  113. $returnedProperties[200][$addHome] = new DAV\Property\Href($addressbookHomePath);
  114. }
  115. $directories = '{' . self::NS_CARDDAV . '}directory-gateway';
  116. if ($this->directories && in_array($directories, $requestedProperties)) {
  117. unset($requestedProperties[array_search($directories, $requestedProperties)]);
  118. $returnedProperties[200][$directories] = new DAV\Property\HrefList($this->directories);
  119. }
  120. }
  121. if ($node instanceof ICard) {
  122. // The address-data property is not supposed to be a 'real'
  123. // property, but in large chunks of the spec it does act as such.
  124. // Therefore we simply expose it as a property.
  125. $addressDataProp = '{' . self::NS_CARDDAV . '}address-data';
  126. if (in_array($addressDataProp, $requestedProperties)) {
  127. unset($requestedProperties[$addressDataProp]);
  128. $val = $node->get();
  129. if (is_resource($val))
  130. $val = stream_get_contents($val);
  131. $returnedProperties[200][$addressDataProp] = $val;
  132. }
  133. }
  134. if ($node instanceof UserAddressBooks) {
  135. $meCardProp = '{http://calendarserver.org/ns/}me-card';
  136. if (in_array($meCardProp, $requestedProperties)) {
  137. $props = $this->server->getProperties($node->getOwner(), array('{http://sabredav.org/ns}vcard-url'));
  138. if (isset($props['{http://sabredav.org/ns}vcard-url'])) {
  139. $returnedProperties[200][$meCardProp] = new DAV\Property\Href(
  140. $props['{http://sabredav.org/ns}vcard-url']
  141. );
  142. $pos = array_search($meCardProp, $requestedProperties);
  143. unset($requestedProperties[$pos]);
  144. }
  145. }
  146. }
  147. }
  148. /**
  149. * This event is triggered when a PROPPATCH method is executed
  150. *
  151. * @param array $mutations
  152. * @param array $result
  153. * @param DAV\INode $node
  154. * @return bool
  155. */
  156. public function updateProperties(&$mutations, &$result, DAV\INode $node) {
  157. if (!$node instanceof UserAddressBooks) {
  158. return true;
  159. }
  160. $meCard = '{http://calendarserver.org/ns/}me-card';
  161. // The only property we care about
  162. if (!isset($mutations[$meCard]))
  163. return true;
  164. $value = $mutations[$meCard];
  165. unset($mutations[$meCard]);
  166. if ($value instanceof DAV\Property\IHref) {
  167. $value = $value->getHref();
  168. $value = $this->server->calculateUri($value);
  169. } elseif (!is_null($value)) {
  170. $result[400][$meCard] = null;
  171. return false;
  172. }
  173. $innerResult = $this->server->updateProperties(
  174. $node->getOwner(),
  175. array(
  176. '{http://sabredav.org/ns}vcard-url' => $value,
  177. )
  178. );
  179. $closureResult = false;
  180. foreach($innerResult as $status => $props) {
  181. if (is_array($props) && array_key_exists('{http://sabredav.org/ns}vcard-url', $props)) {
  182. $result[$status][$meCard] = null;
  183. $closureResult = ($status>=200 && $status<300);
  184. }
  185. }
  186. return $result;
  187. }
  188. /**
  189. * This functions handles REPORT requests specific to CardDAV
  190. *
  191. * @param string $reportName
  192. * @param \DOMNode $dom
  193. * @return bool
  194. */
  195. public function report($reportName,$dom) {
  196. switch($reportName) {
  197. case '{'.self::NS_CARDDAV.'}addressbook-multiget' :
  198. $this->server->transactionType = 'report-addressbook-multiget';
  199. $this->addressbookMultiGetReport($dom);
  200. return false;
  201. case '{'.self::NS_CARDDAV.'}addressbook-query' :
  202. $this->server->transactionType = 'report-addressbook-query';
  203. $this->addressBookQueryReport($dom);
  204. return false;
  205. default :
  206. return;
  207. }
  208. }
  209. /**
  210. * This function handles the addressbook-multiget REPORT.
  211. *
  212. * This report is used by the client to fetch the content of a series
  213. * of urls. Effectively avoiding a lot of redundant requests.
  214. *
  215. * @param \DOMNode $dom
  216. * @return void
  217. */
  218. public function addressbookMultiGetReport($dom) {
  219. $properties = array_keys(DAV\XMLUtil::parseProperties($dom->firstChild));
  220. $hrefElems = $dom->getElementsByTagNameNS('urn:DAV','href');
  221. $propertyList = array();
  222. $uris = [];
  223. foreach($hrefElems as $elem) {
  224. $uris[] = $this->server->calculateUri($elem->nodeValue);
  225. }
  226. $propertyList = array_values(
  227. $this->server->getPropertiesForMultiplePaths($uris, $properties)
  228. );
  229. $prefer = $this->server->getHTTPPRefer();
  230. $this->server->httpResponse->setStatus(207);
  231. $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
  232. $this->server->httpResponse->setHeader('Vary','Brief,Prefer');
  233. $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, $prefer['return-minimal']));
  234. }
  235. /**
  236. * This method is triggered before a file gets updated with new content.
  237. *
  238. * This plugin uses this method to ensure that Card nodes receive valid
  239. * vcard data.
  240. *
  241. * @param string $path
  242. * @param DAV\IFile $node
  243. * @param resource $data
  244. * @param bool $modified Should be set to true, if this event handler
  245. * changed &$data.
  246. * @return void
  247. */
  248. public function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified) {
  249. if (!$node instanceof ICard)
  250. return;
  251. $this->validateVCard($data, $modified);
  252. }
  253. /**
  254. * This method is triggered before a new file is created.
  255. *
  256. * This plugin uses this method to ensure that Card nodes receive valid
  257. * vcard data.
  258. *
  259. * @param string $path
  260. * @param resource $data
  261. * @param DAV\ICollection $parentNode
  262. * @param bool $modified Should be set to true, if this event handler
  263. * changed &$data.
  264. * @return void
  265. */
  266. public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified) {
  267. if (!$parentNode instanceof IAddressBook)
  268. return;
  269. $this->validateVCard($data, $modified);
  270. }
  271. /**
  272. * Checks if the submitted iCalendar data is in fact, valid.
  273. *
  274. * An exception is thrown if it's not.
  275. *
  276. * @param resource|string $data
  277. * @param bool $modified Should be set to true, if this event handler
  278. * changed &$data.
  279. * @return void
  280. */
  281. protected function validateVCard(&$data, &$modified) {
  282. // If it's a stream, we convert it to a string first.
  283. if (is_resource($data)) {
  284. $data = stream_get_contents($data);
  285. }
  286. $before = md5($data);
  287. // Converting the data to unicode, if needed.
  288. $data = DAV\StringUtil::ensureUTF8($data);
  289. if (md5($data) !== $before) $modified = true;
  290. try {
  291. $vobj = VObject\Reader::read($data);
  292. } catch (VObject\ParseException $e) {
  293. throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid vcard data. Parse error: ' . $e->getMessage());
  294. }
  295. if ($vobj->name !== 'VCARD') {
  296. throw new DAV\Exception\UnsupportedMediaType('This collection can only support vcard objects.');
  297. }
  298. if (!isset($vobj->UID)) {
  299. throw new DAV\Exception\BadRequest('Every vcard must have a UID.');
  300. }
  301. }
  302. /**
  303. * This function handles the addressbook-query REPORT
  304. *
  305. * This report is used by the client to filter an addressbook based on a
  306. * complex query.
  307. *
  308. * @param \DOMNode $dom
  309. * @return void
  310. */
  311. protected function addressbookQueryReport($dom) {
  312. $query = new AddressBookQueryParser($dom);
  313. $query->parse();
  314. $depth = $this->server->getHTTPDepth(0);
  315. if ($depth==0) {
  316. $candidateNodes = array(
  317. $this->server->tree->getNodeForPath($this->server->getRequestUri())
  318. );
  319. } else {
  320. $candidateNodes = $this->server->tree->getChildren($this->server->getRequestUri());
  321. }
  322. $validNodes = array();
  323. foreach($candidateNodes as $node) {
  324. if (!$node instanceof ICard)
  325. continue;
  326. $blob = $node->get();
  327. if (is_resource($blob)) {
  328. $blob = stream_get_contents($blob);
  329. }
  330. if (!$this->validateFilters($blob, $query->filters, $query->test)) {
  331. continue;
  332. }
  333. $validNodes[] = $node;
  334. if ($query->limit && $query->limit <= count($validNodes)) {
  335. // We hit the maximum number of items, we can stop now.
  336. break;
  337. }
  338. }
  339. $result = array();
  340. foreach($validNodes as $validNode) {
  341. if ($depth==0) {
  342. $href = $this->server->getRequestUri();
  343. } else {
  344. $href = $this->server->getRequestUri() . '/' . $validNode->getName();
  345. }
  346. list($result[]) = $this->server->getPropertiesForPath($href, $query->requestedProperties, 0);
  347. }
  348. $prefer = $this->server->getHTTPPRefer();
  349. $this->server->httpResponse->setStatus(207);
  350. $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
  351. $this->server->httpResponse->setHeader('Vary','Brief,Prefer');
  352. $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, $prefer['return-minimal']));
  353. }
  354. /**
  355. * Validates if a vcard makes it throught a list of filters.
  356. *
  357. * @param string $vcardData
  358. * @param array $filters
  359. * @param string $test anyof or allof (which means OR or AND)
  360. * @return bool
  361. */
  362. public function validateFilters($vcardData, array $filters, $test) {
  363. $vcard = VObject\Reader::read($vcardData);
  364. if (!$filters) return true;
  365. foreach($filters as $filter) {
  366. $isDefined = isset($vcard->{$filter['name']});
  367. if ($filter['is-not-defined']) {
  368. if ($isDefined) {
  369. $success = false;
  370. } else {
  371. $success = true;
  372. }
  373. } elseif ((!$filter['param-filters'] && !$filter['text-matches']) || !$isDefined) {
  374. // We only need to check for existence
  375. $success = $isDefined;
  376. } else {
  377. $vProperties = $vcard->select($filter['name']);
  378. $results = array();
  379. if ($filter['param-filters']) {
  380. $results[] = $this->validateParamFilters($vProperties, $filter['param-filters'], $filter['test']);
  381. }
  382. if ($filter['text-matches']) {
  383. $texts = array();
  384. foreach($vProperties as $vProperty)
  385. $texts[] = $vProperty->getValue();
  386. $results[] = $this->validateTextMatches($texts, $filter['text-matches'], $filter['test']);
  387. }
  388. if (count($results)===1) {
  389. $success = $results[0];
  390. } else {
  391. if ($filter['test'] === 'anyof') {
  392. $success = $results[0] || $results[1];
  393. } else {
  394. $success = $results[0] && $results[1];
  395. }
  396. }
  397. } // else
  398. // There are two conditions where we can already determine whether
  399. // or not this filter succeeds.
  400. if ($test==='anyof' && $success) {
  401. return true;
  402. }
  403. if ($test==='allof' && !$success) {
  404. return false;
  405. }
  406. } // foreach
  407. // If we got all the way here, it means we haven't been able to
  408. // determine early if the test failed or not.
  409. //
  410. // This implies for 'anyof' that the test failed, and for 'allof' that
  411. // we succeeded. Sounds weird, but makes sense.
  412. return $test==='allof';
  413. }
  414. /**
  415. * Validates if a param-filter can be applied to a specific property.
  416. *
  417. * @todo currently we're only validating the first parameter of the passed
  418. * property. Any subsequence parameters with the same name are
  419. * ignored.
  420. * @param array $vProperties
  421. * @param array $filters
  422. * @param string $test
  423. * @return bool
  424. */
  425. protected function validateParamFilters(array $vProperties, array $filters, $test) {
  426. foreach($filters as $filter) {
  427. $isDefined = false;
  428. foreach($vProperties as $vProperty) {
  429. $isDefined = isset($vProperty[$filter['name']]);
  430. if ($isDefined) break;
  431. }
  432. if ($filter['is-not-defined']) {
  433. if ($isDefined) {
  434. $success = false;
  435. } else {
  436. $success = true;
  437. }
  438. // If there's no text-match, we can just check for existence
  439. } elseif (!$filter['text-match'] || !$isDefined) {
  440. $success = $isDefined;
  441. } else {
  442. $success = false;
  443. foreach($vProperties as $vProperty) {
  444. // If we got all the way here, we'll need to validate the
  445. // text-match filter.
  446. $success = DAV\StringUtil::textMatch($vProperty[$filter['name']]->getValue(), $filter['text-match']['value'], $filter['text-match']['collation'], $filter['text-match']['match-type']);
  447. if ($success) break;
  448. }
  449. if ($filter['text-match']['negate-condition']) {
  450. $success = !$success;
  451. }
  452. } // else
  453. // There are two conditions where we can already determine whether
  454. // or not this filter succeeds.
  455. if ($test==='anyof' && $success) {
  456. return true;
  457. }
  458. if ($test==='allof' && !$success) {
  459. return false;
  460. }
  461. }
  462. // If we got all the way here, it means we haven't been able to
  463. // determine early if the test failed or not.
  464. //
  465. // This implies for 'anyof' that the test failed, and for 'allof' that
  466. // we succeeded. Sounds weird, but makes sense.
  467. return $test==='allof';
  468. }
  469. /**
  470. * Validates if a text-filter can be applied to a specific property.
  471. *
  472. * @param array $texts
  473. * @param array $filters
  474. * @param string $test
  475. * @return bool
  476. */
  477. protected function validateTextMatches(array $texts, array $filters, $test) {
  478. foreach($filters as $filter) {
  479. $success = false;
  480. foreach($texts as $haystack) {
  481. $success = DAV\StringUtil::textMatch($haystack, $filter['value'], $filter['collation'], $filter['match-type']);
  482. // Breaking on the first match
  483. if ($success) break;
  484. }
  485. if ($filter['negate-condition']) {
  486. $success = !$success;
  487. }
  488. if ($success && $test==='anyof')
  489. return true;
  490. if (!$success && $test=='allof')
  491. return false;
  492. }
  493. // If we got all the way here, it means we haven't been able to
  494. // determine early if the test failed or not.
  495. //
  496. // This implies for 'anyof' that the test failed, and for 'allof' that
  497. // we succeeded. Sounds weird, but makes sense.
  498. return $test==='allof';
  499. }
  500. /**
  501. * This event is triggered after webdav-properties have been retrieved.
  502. *
  503. * @return bool
  504. */
  505. public function afterGetProperties($uri, &$properties, DAV\INode $node) {
  506. // If the request was made using the SOGO connector, we must rewrite
  507. // the content-type property. By default SabreDAV will send back
  508. // text/x-vcard; charset=utf-8, but for SOGO we must strip that last
  509. // part.
  510. if (!isset($properties[200]['{DAV:}getcontenttype']))
  511. return;
  512. if (strpos($this->server->httpRequest->getHeader('User-Agent'),'Thunderbird')===false) {
  513. return;
  514. }
  515. if (strpos($properties[200]['{DAV:}getcontenttype'],'text/x-vcard')===0) {
  516. $properties[200]['{DAV:}getcontenttype'] = 'text/x-vcard';
  517. }
  518. }
  519. /**
  520. * This method is used to generate HTML output for the
  521. * Sabre\DAV\Browser\Plugin. This allows us to generate an interface users
  522. * can use to create new addressbooks.
  523. *
  524. * @param DAV\INode $node
  525. * @param string $output
  526. * @return bool
  527. */
  528. public function htmlActionsPanel(DAV\INode $node, &$output) {
  529. if (!$node instanceof UserAddressBooks)
  530. return;
  531. $output.= '<tr><td colspan="2"><form method="post" action="">
  532. <h3>Create new address book</h3>
  533. <input type="hidden" name="sabreAction" value="mkaddressbook" />
  534. <label>Name (uri):</label> <input type="text" name="name" /><br />
  535. <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
  536. <input type="submit" value="create" />
  537. </form>
  538. </td></tr>';
  539. return false;
  540. }
  541. /**
  542. * This method allows us to intercept the 'mkaddressbook' sabreAction. This
  543. * action enables the user to create new addressbooks from the browser plugin.
  544. *
  545. * @param string $uri
  546. * @param string $action
  547. * @param array $postVars
  548. * @return bool
  549. */
  550. public function browserPostAction($uri, $action, array $postVars) {
  551. if ($action!=='mkaddressbook')
  552. return;
  553. $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:carddav}addressbook');
  554. $properties = array();
  555. if (isset($postVars['{DAV:}displayname'])) {
  556. $properties['{DAV:}displayname'] = $postVars['{DAV:}displayname'];
  557. }
  558. $this->server->createCollection($uri . '/' . $postVars['name'],$resourceType,$properties);
  559. return false;
  560. }
  561. }