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