PageRenderTime 47ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Sabre/CardDAV/Backend/PDO.php

https://github.com/KOLANICH/SabreDAV
PHP | 524 lines | 195 code | 93 blank | 236 comment | 7 complexity | 8dad879b2ec626cb89dda1bf68ac53b4 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. namespace Sabre\CardDAV\Backend;
  3. use Sabre\CardDAV;
  4. use Sabre\DAV;
  5. /**
  6. * PDO CardDAV backend
  7. *
  8. * This CardDAV backend uses PDO to store addressbooks
  9. *
  10. * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
  11. * @author Evert Pot (http://evertpot.com/)
  12. * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
  13. */
  14. class PDO extends AbstractBackend implements SyncSupport {
  15. /**
  16. * PDO connection
  17. *
  18. * @var PDO
  19. */
  20. protected $pdo;
  21. /**
  22. * The PDO table name used to store addressbooks
  23. */
  24. protected $addressBooksTableName;
  25. /**
  26. * The PDO table name used to store cards
  27. */
  28. protected $cardsTableName;
  29. /**
  30. * The table name that will be used for tracking changes in address books.
  31. *
  32. * @var string
  33. */
  34. protected $addressBookChangesTableName;
  35. /**
  36. * Sets up the object
  37. *
  38. * @param \PDO $pdo
  39. * @param string $addressBooksTableName
  40. * @param string $cardsTableName
  41. */
  42. public function __construct(\PDO $pdo, $addressBooksTableName = 'addressbooks', $cardsTableName = 'cards', $addressBookChangesTableName = 'addressbookchanges') {
  43. $this->pdo = $pdo;
  44. $this->addressBooksTableName = $addressBooksTableName;
  45. $this->cardsTableName = $cardsTableName;
  46. $this->addressBookChangesTableName = $addressBookChangesTableName;
  47. }
  48. /**
  49. * Returns the list of addressbooks for a specific user.
  50. *
  51. * @param string $principalUri
  52. * @return array
  53. */
  54. public function getAddressBooksForUser($principalUri) {
  55. $stmt = $this->pdo->prepare('SELECT id, uri, displayname, principaluri, description, synctoken FROM '.$this->addressBooksTableName.' WHERE principaluri = ?');
  56. $stmt->execute(array($principalUri));
  57. $addressBooks = array();
  58. foreach($stmt->fetchAll() as $row) {
  59. $addressBooks[] = array(
  60. 'id' => $row['id'],
  61. 'uri' => $row['uri'],
  62. 'principaluri' => $row['principaluri'],
  63. '{DAV:}displayname' => $row['displayname'],
  64. '{' . CardDAV\Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
  65. '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
  66. '{' . CardDAV\Plugin::NS_CARDDAV . '}supported-address-data' =>
  67. new CardDAV\Property\SupportedAddressData(),
  68. '{DAV:}sync-token' => $row['synctoken']?$row['synctoken']:'0',
  69. );
  70. }
  71. return $addressBooks;
  72. }
  73. /**
  74. * Updates an addressbook's properties
  75. *
  76. * See Sabre\DAV\IProperties for a description of the mutations array, as
  77. * well as the return value.
  78. *
  79. * @param mixed $addressBookId
  80. * @param array $mutations
  81. * @see Sabre\DAV\IProperties::updateProperties
  82. * @return bool|array
  83. */
  84. public function updateAddressBook($addressBookId, array $mutations) {
  85. $updates = array();
  86. foreach($mutations as $property=>$newValue) {
  87. switch($property) {
  88. case '{DAV:}displayname' :
  89. $updates['displayname'] = $newValue;
  90. break;
  91. case '{' . CardDAV\Plugin::NS_CARDDAV . '}addressbook-description' :
  92. $updates['description'] = $newValue;
  93. break;
  94. default :
  95. // If any unsupported values were being updated, we must
  96. // let the entire request fail.
  97. return false;
  98. }
  99. }
  100. // No values are being updated?
  101. if (!$updates) {
  102. return false;
  103. }
  104. $query = 'UPDATE ' . $this->addressBooksTableName . ' SET ';
  105. $first = true;
  106. foreach($updates as $key=>$value) {
  107. if ($first) {
  108. $first = false;
  109. } else {
  110. $query.=', ';
  111. }
  112. $query.=' `' . $key . '` = :' . $key . ' ';
  113. }
  114. $query.=' WHERE id = :addressbookid';
  115. $stmt = $this->pdo->prepare($query);
  116. $updates['addressbookid'] = $addressBookId;
  117. $stmt->execute($updates);
  118. $this->addChange($addressBookId, "", 2);
  119. return true;
  120. }
  121. /**
  122. * Creates a new address book
  123. *
  124. * @param string $principalUri
  125. * @param string $url Just the 'basename' of the url.
  126. * @param array $properties
  127. * @return void
  128. */
  129. public function createAddressBook($principalUri, $url, array $properties) {
  130. $values = array(
  131. 'displayname' => null,
  132. 'description' => null,
  133. 'principaluri' => $principalUri,
  134. 'uri' => $url,
  135. );
  136. foreach($properties as $property=>$newValue) {
  137. switch($property) {
  138. case '{DAV:}displayname' :
  139. $values['displayname'] = $newValue;
  140. break;
  141. case '{' . CardDAV\Plugin::NS_CARDDAV . '}addressbook-description' :
  142. $values['description'] = $newValue;
  143. break;
  144. default :
  145. throw new DAV\Exception\BadRequest('Unknown property: ' . $property);
  146. }
  147. }
  148. $query = 'INSERT INTO ' . $this->addressBooksTableName . ' (uri, displayname, description, principaluri, synctoken) VALUES (:uri, :displayname, :description, :principaluri, 1)';
  149. $stmt = $this->pdo->prepare($query);
  150. $stmt->execute($values);
  151. return $this->pdo->lastInsertId();
  152. }
  153. /**
  154. * Deletes an entire addressbook and all its contents
  155. *
  156. * @param int $addressBookId
  157. * @return void
  158. */
  159. public function deleteAddressBook($addressBookId) {
  160. $stmt = $this->pdo->prepare('DELETE FROM ' . $this->cardsTableName . ' WHERE addressbookid = ?');
  161. $stmt->execute([$addressBookId]);
  162. $stmt = $this->pdo->prepare('DELETE FROM ' . $this->addressBooksTableName . ' WHERE id = ?');
  163. $stmt->execute([$addressBookId]);
  164. $stmt = $this->pdo->prepare('DELETE FROM '.$this->addressBookChangesTableName.' WHERE id = ?');
  165. $stmt->execute([$addressBookId]);
  166. }
  167. /**
  168. * Returns all cards for a specific addressbook id.
  169. *
  170. * This method should return the following properties for each card:
  171. * * carddata - raw vcard data
  172. * * uri - Some unique url
  173. * * lastmodified - A unix timestamp
  174. *
  175. * It's recommended to also return the following properties:
  176. * * etag - A unique etag. This must change every time the card changes.
  177. * * size - The size of the card in bytes.
  178. *
  179. * If these last two properties are provided, less time will be spent
  180. * calculating them. If they are specified, you can also ommit carddata.
  181. * This may speed up certain requests, especially with large cards.
  182. *
  183. * @param mixed $addressbookId
  184. * @return array
  185. */
  186. public function getCards($addressbookId) {
  187. $stmt = $this->pdo->prepare('SELECT id, carddata, uri, lastmodified FROM ' . $this->cardsTableName . ' WHERE addressbookid = ?');
  188. $stmt->execute(array($addressbookId));
  189. return $stmt->fetchAll(\PDO::FETCH_ASSOC);
  190. }
  191. /**
  192. * Returns a specfic card.
  193. *
  194. * The same set of properties must be returned as with getCards. The only
  195. * exception is that 'carddata' is absolutely required.
  196. *
  197. * If the card does not exist, you must return false.
  198. *
  199. * @param mixed $addressBookId
  200. * @param string $cardUri
  201. * @return array
  202. */
  203. public function getCard($addressBookId, $cardUri) {
  204. $stmt = $this->pdo->prepare('SELECT id, carddata, uri, lastmodified FROM ' . $this->cardsTableName . ' WHERE addressbookid = ? AND uri = ? LIMIT 1');
  205. $stmt->execute(array($addressBookId, $cardUri));
  206. $result = $stmt->fetch(\PDO::FETCH_ASSOC);
  207. return $result?$result:false;
  208. }
  209. /**
  210. * Returns a list of cards.
  211. *
  212. * This method should work identical to getCard, but instead return all the
  213. * cards in the list as an array.
  214. *
  215. * If the backend supports this, it may allow for some speed-ups.
  216. *
  217. * @param mixed $addressBookId
  218. * @param array $uris
  219. * @return array
  220. */
  221. public function getMultipleCards($addressBookId, array $uris) {
  222. return array_map(function($uri) use ($addressBookId) {
  223. return $this->getCard($addressBookId, $uri);
  224. }, $uris);
  225. $query = 'SELECT id, carddata, uri, lastmodified FROM ' . $this->cardsTableName . ' WHERE addressbookid = ? AND uri = IN (';
  226. // Inserting a whole bunch of question marks
  227. $query.=implode(',', array_fill(0, count($uris), '?'));
  228. $query.=')';
  229. $stmt = $this->pdo->prepare($query);
  230. $stmt->execute(array_merge([$addressBookId], $uris));
  231. return $stmt->fetchAll(\PDO::FETCH_ASSOC);
  232. }
  233. /**
  234. * Creates a new card.
  235. *
  236. * The addressbook id will be passed as the first argument. This is the
  237. * same id as it is returned from the getAddressbooksForUser method.
  238. *
  239. * The cardUri is a base uri, and doesn't include the full path. The
  240. * cardData argument is the vcard body, and is passed as a string.
  241. *
  242. * It is possible to return an ETag from this method. This ETag is for the
  243. * newly created resource, and must be enclosed with double quotes (that
  244. * is, the string itself must contain the double quotes).
  245. *
  246. * You should only return the ETag if you store the carddata as-is. If a
  247. * subsequent GET request on the same card does not have the same body,
  248. * byte-by-byte and you did return an ETag here, clients tend to get
  249. * confused.
  250. *
  251. * If you don't return an ETag, you can just return null.
  252. *
  253. * @param mixed $addressBookId
  254. * @param string $cardUri
  255. * @param string $cardData
  256. * @return string|null
  257. */
  258. public function createCard($addressBookId, $cardUri, $cardData) {
  259. $stmt = $this->pdo->prepare('INSERT INTO ' . $this->cardsTableName . ' (carddata, uri, lastmodified, addressbookid) VALUES (?, ?, ?, ?)');
  260. $result = $stmt->execute(array($cardData, $cardUri, time(), $addressBookId));
  261. $this->addChange($addressBookId, $cardUri, 1);
  262. return '"' . md5($cardData) . '"';
  263. }
  264. /**
  265. * Updates a card.
  266. *
  267. * The addressbook id will be passed as the first argument. This is the
  268. * same id as it is returned from the getAddressbooksForUser method.
  269. *
  270. * The cardUri is a base uri, and doesn't include the full path. The
  271. * cardData argument is the vcard body, and is passed as a string.
  272. *
  273. * It is possible to return an ETag from this method. This ETag should
  274. * match that of the updated resource, and must be enclosed with double
  275. * quotes (that is: the string itself must contain the actual quotes).
  276. *
  277. * You should only return the ETag if you store the carddata as-is. If a
  278. * subsequent GET request on the same card does not have the same body,
  279. * byte-by-byte and you did return an ETag here, clients tend to get
  280. * confused.
  281. *
  282. * If you don't return an ETag, you can just return null.
  283. *
  284. * @param mixed $addressBookId
  285. * @param string $cardUri
  286. * @param string $cardData
  287. * @return string|null
  288. */
  289. public function updateCard($addressBookId, $cardUri, $cardData) {
  290. $stmt = $this->pdo->prepare('UPDATE ' . $this->cardsTableName . ' SET carddata = ?, lastmodified = ? WHERE uri = ? AND addressbookid =?');
  291. $stmt->execute(array($cardData, time(), $cardUri, $addressBookId));
  292. $this->addChange($addressBookId, $cardUri, 2);
  293. return '"' . md5($cardData) . '"';
  294. }
  295. /**
  296. * Deletes a card
  297. *
  298. * @param mixed $addressBookId
  299. * @param string $cardUri
  300. * @return bool
  301. */
  302. public function deleteCard($addressBookId, $cardUri) {
  303. $stmt = $this->pdo->prepare('DELETE FROM ' . $this->cardsTableName . ' WHERE addressbookid = ? AND uri = ?');
  304. $stmt->execute(array($addressBookId, $cardUri));
  305. $this->addChange($addressBookId, $cardUri, 3);
  306. return $stmt->rowCount()===1;
  307. }
  308. /**
  309. * The getChanges method returns all the changes that have happened, since
  310. * the specified syncToken in the specified address book.
  311. *
  312. * This function should return an array, such as the following:
  313. *
  314. * [
  315. * 'syncToken' => 'The current synctoken',
  316. * 'added' => [
  317. * 'new.txt',
  318. * ],
  319. * 'modified' => [
  320. * 'updated.txt',
  321. * ],
  322. * 'deleted' => [
  323. * 'foo.php.bak',
  324. * 'old.txt'
  325. * ]
  326. * ];
  327. *
  328. * The returned syncToken property should reflect the *current* syncToken
  329. * of the addressbook, as reported in the {DAV:}sync-token property This is
  330. * needed here too, to ensure the operation is atomic.
  331. *
  332. * If the $syncToken argument is specified as null, this is an initial
  333. * sync, and all members should be reported.
  334. *
  335. * The modified property is an array of nodenames that have changed since
  336. * the last token.
  337. *
  338. * The deleted property is an array with nodenames, that have been deleted
  339. * from collection.
  340. *
  341. * The $syncLevel argument is basically the 'depth' of the report. If it's
  342. * 1, you only have to report changes that happened only directly in
  343. * immediate descendants. If it's 2, it should also include changes from
  344. * the nodes below the child collections. (grandchildren)
  345. *
  346. * The $limit argument allows a client to specify how many results should
  347. * be returned at most. If the limit is not specified, it should be treated
  348. * as infinite.
  349. *
  350. * If the limit (infinite or not) is higher than you're willing to return,
  351. * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
  352. *
  353. * If the syncToken is expired (due to data cleanup) or unknown, you must
  354. * return null.
  355. *
  356. * The limit is 'suggestive'. You are free to ignore it.
  357. *
  358. * @param string $addressBookId
  359. * @param string $syncToken
  360. * @param int $syncLevel
  361. * @param int $limit
  362. * @return array
  363. */
  364. public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
  365. // Current synctoken
  366. $stmt = $this->pdo->prepare('SELECT synctoken FROM addressbooks WHERE id = ?');
  367. $stmt->execute([ $addressBookId ]);
  368. $currentToken = $stmt->fetchColumn(0);
  369. if (is_null($currentToken)) return null;
  370. $result = [
  371. 'syncToken' => $currentToken,
  372. 'added' => [],
  373. 'modified' => [],
  374. 'deleted' => [],
  375. ];
  376. if ($syncToken) {
  377. $query = "SELECT uri, operation FROM " . $this->addressBookChangesTableName . " WHERE synctoken >= ? AND synctoken < ? AND addressbookid = ? ORDER BY synctoken";
  378. if ($limit>0) $query.= " LIMIT " . (int)$limit;
  379. // Fetching all changes
  380. $stmt = $this->pdo->prepare($query);
  381. $stmt->execute([$syncToken, $currentToken, $addressBookId]);
  382. $changes = [];
  383. // This loop ensures that any duplicates are overwritten, only the
  384. // last change on a node is relevant.
  385. while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
  386. $changes[$row['uri']] = $row['operation'];
  387. }
  388. foreach($changes as $uri => $operation) {
  389. switch($operation) {
  390. case 1:
  391. $result['added'][] = $uri;
  392. break;
  393. case 2:
  394. $result['modified'][] = $uri;
  395. break;
  396. case 3:
  397. $result['deleted'][] = $uri;
  398. break;
  399. }
  400. }
  401. } else {
  402. // No synctoken supplied, this is the initial sync.
  403. $query = "SELECT uri FROM cards WHERE addressbookid = ?";
  404. $stmt = $this->pdo->prepare($query);
  405. $stmt->execute([$addressBookId]);
  406. $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
  407. }
  408. return $result;
  409. }
  410. /**
  411. * Adds a change record to the addressbookchanges table.
  412. *
  413. * @param mixed $addressBookId
  414. * @param string $objectUri
  415. * @param int $operation 1 = add, 2 = modify, 3 = delete/
  416. * @return void
  417. */
  418. protected function addChange($addressBookId, $objectUri, $operation) {
  419. $stmt = $this->pdo->prepare('INSERT INTO ' . $this->addressBookChangesTableName .' (uri, synctoken, addressbookid, operation) SELECT ?, synctoken, ?, ? FROM addressbooks WHERE id = ?');
  420. $stmt->execute([
  421. $objectUri,
  422. $addressBookId,
  423. $operation,
  424. $addressBookId
  425. ]);
  426. $stmt = $this->pdo->prepare('UPDATE ' . $this->addressBooksTableName . ' SET synctoken = synctoken + 1 WHERE id = ?');
  427. $stmt->execute([
  428. $addressBookId
  429. ]);
  430. }
  431. }