grommunio /
grommunio-dav
| 1 | <?php |
||
| 2 | |||
| 3 | /* |
||
| 4 | * SPDX-License-Identifier: AGPL-3.0-only |
||
| 5 | * SPDX-FileCopyrightText: Copyright 2016 - 2018 Kopano b.v. |
||
| 6 | * SPDX-FileCopyrightText: Copyright 2020 - 2024 grommunio GmbH |
||
| 7 | * |
||
| 8 | * grommunio Card DAV backend class which handles contact related activities. |
||
| 9 | */ |
||
| 10 | |||
| 11 | namespace grommunio\DAV; |
||
| 12 | |||
| 13 | use Sabre\CardDAV\Backend\AbstractBackend; |
||
| 14 | use Sabre\CardDAV\Backend\SyncSupport; |
||
| 15 | use Sabre\DAV\PropPatch; |
||
| 16 | |||
| 17 | class GrommunioCardDavBackend extends AbstractBackend implements SyncSupport { |
||
| 18 | private $logger; |
||
| 19 | protected $gDavBackend; |
||
| 20 | |||
| 21 | public const FILE_EXTENSION = '.vcf'; |
||
| 22 | public const MESSAGE_CLASSES = ['IPM.Contact']; |
||
| 23 | public const CONTAINER_CLASS = 'IPF.Contact'; |
||
| 24 | public const CONTAINER_CLASSES = ['IPF.Contact']; |
||
| 25 | |||
| 26 | /** |
||
| 27 | * Constructor. |
||
| 28 | */ |
||
| 29 | public function __construct(GrommunioDavBackend $gDavBackend, GLogger $glogger) { |
||
| 30 | $this->gDavBackend = $gDavBackend; |
||
| 31 | $this->logger = $glogger; |
||
| 32 | } |
||
| 33 | |||
| 34 | /** |
||
| 35 | * Returns the list of addressbooks for a specific user. |
||
| 36 | * |
||
| 37 | * Every addressbook should have the following properties: |
||
| 38 | * id - an arbitrary unique id |
||
| 39 | * uri - the 'basename' part of the url |
||
| 40 | * principaluri - Same as the passed parameter |
||
| 41 | * |
||
| 42 | * Any additional clark-notation property may be passed besides this. Some |
||
| 43 | * common ones are : |
||
| 44 | * {DAV:}displayname |
||
| 45 | * {urn:ietf:params:xml:ns:carddav}addressbook-description |
||
| 46 | * {http://calendarserver.org/ns/}getctag |
||
| 47 | * |
||
| 48 | * @param string $principalUri |
||
| 49 | * |
||
| 50 | * @return array |
||
| 51 | */ |
||
| 52 | public function getAddressBooksForUser($principalUri) { |
||
| 53 | $this->logger->trace("principalUri: %s", $principalUri); |
||
| 54 | |||
| 55 | return $this->gDavBackend->GetFolders($principalUri, static::CONTAINER_CLASSES); |
||
| 56 | } |
||
| 57 | |||
| 58 | /** |
||
| 59 | * Updates properties for an address book. |
||
| 60 | * |
||
| 61 | * The list of mutations is stored in a Sabre\DAV\PropPatch object. |
||
| 62 | * To do the actual updates, you must tell this object which properties |
||
| 63 | * you're going to process with the handle() method. |
||
| 64 | * |
||
| 65 | * Calling the handle method is like telling the PropPatch object "I |
||
| 66 | * promise I can handle updating this property". |
||
| 67 | * |
||
| 68 | * Read the PropPatch documentation for more info and examples. |
||
| 69 | * |
||
| 70 | * @param string $addressBookId |
||
| 71 | */ |
||
| 72 | public function updateAddressBook($addressBookId, PropPatch $propPatch) { |
||
| 73 | // TODO is our logger able to log this object? It probably needs to be adapted. |
||
| 74 | $this->logger->trace("addressBookId: %s - proppatch: %s", $addressBookId, $propPatch); |
||
| 75 | } |
||
| 76 | |||
| 77 | /** |
||
| 78 | * Creates a new address book. |
||
| 79 | * |
||
| 80 | * This method should return the id of the new address book. The id can be |
||
| 81 | * in any format, including ints, strings, arrays or objects. |
||
| 82 | * |
||
| 83 | * @param string $principalUri |
||
| 84 | * @param string $url just the 'basename' of the url |
||
| 85 | * |
||
| 86 | * @return mixed |
||
| 87 | */ |
||
| 88 | public function createAddressBook($principalUri, $url, array $properties) { |
||
| 89 | $this->logger->trace("principalUri: %s - url: %s - properties: %s", $principalUri, $url, $properties); |
||
| 90 | |||
| 91 | // TODO Add displayname |
||
| 92 | return $this->gDavBackend->CreateFolder($principalUri, $url, static::CONTAINER_CLASS, ""); |
||
| 93 | } |
||
| 94 | |||
| 95 | /** |
||
| 96 | * Deletes an entire addressbook and all its contents. |
||
| 97 | * |
||
| 98 | * @param mixed $addressBookId |
||
| 99 | */ |
||
| 100 | public function deleteAddressBook($addressBookId) { |
||
| 101 | $this->logger->trace("addressBookId: %s", $addressBookId); |
||
| 102 | $success = $this->gDavBackend->DeleteFolder($addressBookId); |
||
| 103 | // TODO evaluate $success |
||
| 104 | } |
||
| 105 | |||
| 106 | /** |
||
| 107 | * Returns all cards for a specific addressbook id. |
||
| 108 | * |
||
| 109 | * This method should return the following properties for each card: |
||
| 110 | * * carddata - raw vcard data |
||
| 111 | * * uri - Some unique url |
||
| 112 | * * lastmodified - A unix timestamp |
||
| 113 | * |
||
| 114 | * It's recommended to also return the following properties: |
||
| 115 | * * etag - A unique etag. This must change every time the card changes. |
||
| 116 | * * size - The size of the card in bytes. |
||
| 117 | * |
||
| 118 | * If these last two properties are provided, less time will be spent |
||
| 119 | * calculating them. If they are specified, you can also omit carddata. |
||
| 120 | * This may speed up certain requests, especially with large cards. |
||
| 121 | * |
||
| 122 | * @param mixed $addressbookId |
||
| 123 | * |
||
| 124 | * @return array |
||
| 125 | */ |
||
| 126 | public function getCards($addressbookId) { |
||
| 127 | $result = $this->gDavBackend->GetObjects($addressbookId, static::FILE_EXTENSION, ['types' => static::MESSAGE_CLASSES]); |
||
| 128 | $this->logger->trace("addressbookId: %s found %d objects", $addressbookId, count($result)); |
||
| 129 | |||
| 130 | return $result; |
||
| 131 | } |
||
| 132 | |||
| 133 | /** |
||
| 134 | * Returns a specific card. |
||
| 135 | * |
||
| 136 | * The same set of properties must be returned as with getCards. The only |
||
| 137 | * exception is that 'carddata' is absolutely required. |
||
| 138 | * |
||
| 139 | * If the card does not exist, you must return false. |
||
| 140 | * |
||
| 141 | * @param mixed $addressBookId |
||
| 142 | * @param string $cardUri |
||
| 143 | * @param resource $mapifolder optional mapifolder resource, used if available |
||
| 144 | * |
||
| 145 | * @return array|bool |
||
| 146 | */ |
||
| 147 | public function getCard($addressBookId, $cardUri, $mapifolder = null) { |
||
| 148 | $this->logger->trace("addressBookId: %s - cardUri: %s", $addressBookId, $cardUri); |
||
| 149 | |||
| 150 | if (!$mapifolder) { |
||
| 151 | $mapifolder = $this->gDavBackend->GetMapiFolder($addressBookId); |
||
| 152 | } |
||
| 153 | |||
| 154 | $mapimessage = $this->gDavBackend->GetMapiMessageForId($addressBookId, $cardUri, $mapifolder, static::FILE_EXTENSION); |
||
| 155 | if (!$mapimessage) { |
||
| 156 | $this->logger->debug("Object NOT FOUND"); |
||
| 157 | |||
| 158 | return false; |
||
|
0 ignored issues
–
show
|
|||
| 159 | } |
||
| 160 | |||
| 161 | $realId = $this->gDavBackend->GetIdOfMapiMessage($addressBookId, $mapimessage); |
||
| 162 | |||
| 163 | $session = $this->gDavBackend->GetSession(); |
||
| 164 | $ab = $this->gDavBackend->GetAddressBook(); |
||
| 165 | |||
| 166 | $vcf = mapi_mapitovcf($session, $ab, $mapimessage, []); |
||
| 167 | $props = mapi_getprops($mapimessage, [PR_LAST_MODIFICATION_TIME]); |
||
| 168 | $r = [ |
||
| 169 | 'id' => $realId, |
||
| 170 | 'uri' => $realId . static::FILE_EXTENSION, |
||
| 171 | 'etag' => '"' . $props[PR_LAST_MODIFICATION_TIME] . '"', |
||
| 172 | 'lastmodified' => $props[PR_LAST_MODIFICATION_TIME], |
||
| 173 | 'carddata' => $vcf, |
||
| 174 | 'size' => strlen($vcf), |
||
| 175 | 'addressbookid' => $addressBookId, |
||
| 176 | ]; |
||
| 177 | |||
| 178 | $this->logger->trace("returned data id: %s - size: %d - etag: %s", $r['id'], $r['size'], $r['etag']); |
||
| 179 | |||
| 180 | return $r; |
||
| 181 | } |
||
| 182 | |||
| 183 | /** |
||
| 184 | * Creates a new card. |
||
| 185 | * |
||
| 186 | * The addressbook id will be passed as the first argument. This is the |
||
| 187 | * same id as it is returned from the getAddressBooksForUser method. |
||
| 188 | * |
||
| 189 | * The cardUri is a base uri, and doesn't include the full path. The |
||
| 190 | * cardData argument is the vcard body, and is passed as a string. |
||
| 191 | * |
||
| 192 | * It is possible to return an ETag from this method. This ETag is for the |
||
| 193 | * newly created resource, and must be enclosed with double quotes (that |
||
| 194 | * is, the string itself must contain the double quotes). |
||
| 195 | * |
||
| 196 | * You should only return the ETag if you store the carddata as-is. If a |
||
| 197 | * subsequent GET request on the same card does not have the same body, |
||
| 198 | * byte-by-byte and you did return an ETag here, clients tend to get |
||
| 199 | * confused. |
||
| 200 | * |
||
| 201 | * If you don't return an ETag, you can just return null. |
||
| 202 | * |
||
| 203 | * @param mixed $addressBookId |
||
| 204 | * @param string $cardUri |
||
| 205 | * @param string $cardData |
||
| 206 | * |
||
| 207 | * @return null|string |
||
| 208 | */ |
||
| 209 | public function createCard($addressBookId, $cardUri, $cardData) { |
||
| 210 | $this->logger->trace("addressBookId: %s - cardUri: %s", $addressBookId, $cardUri); |
||
| 211 | $objectId = $this->gDavBackend->GetObjectIdFromObjectUri($cardUri, static::FILE_EXTENSION); |
||
| 212 | $folder = $this->gDavBackend->GetMapiFolder($addressBookId); |
||
| 213 | $mapimessage = $this->gDavBackend->CreateObject($addressBookId, $folder, $objectId); |
||
| 214 | |||
| 215 | return $this->setData($addressBookId, $mapimessage, $cardData); |
||
| 216 | } |
||
| 217 | |||
| 218 | /** |
||
| 219 | * Updates a card. |
||
| 220 | * |
||
| 221 | * The addressbook id will be passed as the first argument. This is the |
||
| 222 | * same id as it is returned from the getAddressBooksForUser method. |
||
| 223 | * |
||
| 224 | * The cardUri is a base uri, and doesn't include the full path. The |
||
| 225 | * cardData argument is the vcard body, and is passed as a string. |
||
| 226 | * |
||
| 227 | * It is possible to return an ETag from this method. This ETag should |
||
| 228 | * match that of the updated resource, and must be enclosed with double |
||
| 229 | * quotes (that is: the string itself must contain the actual quotes). |
||
| 230 | * |
||
| 231 | * You should only return the ETag if you store the carddata as-is. If a |
||
| 232 | * subsequent GET request on the same card does not have the same body, |
||
| 233 | * byte-by-byte and you did return an ETag here, clients tend to get |
||
| 234 | * confused. |
||
| 235 | * |
||
| 236 | * If you don't return an ETag, you can just return null. |
||
| 237 | * |
||
| 238 | * @param mixed $addressBookId |
||
| 239 | * @param string $cardUri |
||
| 240 | * @param string $cardData |
||
| 241 | * |
||
| 242 | * @return null|string |
||
| 243 | */ |
||
| 244 | public function updateCard($addressBookId, $cardUri, $cardData) { |
||
| 245 | $this->logger->trace("addressBookId: %s - cardUri: %s", $addressBookId, $cardUri); |
||
| 246 | |||
| 247 | $mapimessage = $this->gDavBackend->GetMapiMessageForId($addressBookId, $cardUri, null, static::FILE_EXTENSION); |
||
| 248 | |||
| 249 | return $this->setData($addressBookId, $mapimessage, $cardData); |
||
| 250 | } |
||
| 251 | |||
| 252 | /** |
||
| 253 | * Sets data for a contact. |
||
| 254 | * |
||
| 255 | * @param mixed $addressBookId |
||
| 256 | * @param mixed $mapimessage |
||
| 257 | * @param string $vcf |
||
| 258 | * |
||
| 259 | * @return null|string |
||
| 260 | */ |
||
| 261 | private function setData($addressBookId, $mapimessage, $vcf) { |
||
| 262 | $store = $this->gDavBackend->GetStoreById($addressBookId); |
||
| 263 | $session = $this->gDavBackend->GetSession(); |
||
| 264 | |||
| 265 | $ok = mapi_vcftomapi($session, $store, $mapimessage, $vcf); |
||
| 266 | if ($ok) { |
||
| 267 | mapi_savechanges($mapimessage); |
||
| 268 | $props = mapi_getprops($mapimessage); |
||
| 269 | |||
| 270 | return '"' . $props[PR_LAST_MODIFICATION_TIME] . '"'; |
||
| 271 | } |
||
| 272 | |||
| 273 | return null; |
||
| 274 | } |
||
| 275 | |||
| 276 | /** |
||
| 277 | * Deletes a card. |
||
| 278 | * |
||
| 279 | * @param mixed $addressBookId |
||
| 280 | * @param string $cardUri |
||
| 281 | * |
||
| 282 | * @return bool |
||
| 283 | */ |
||
| 284 | public function deleteCard($addressBookId, $cardUri) { |
||
| 285 | $this->logger->trace("addressBookId: %s - cardUri: %s", $addressBookId, $cardUri); |
||
| 286 | $mapifolder = $this->gDavBackend->GetMapiFolder($addressBookId); |
||
| 287 | $objectId = $this->gDavBackend->GetObjectIdFromObjectUri($cardUri, static::FILE_EXTENSION); |
||
| 288 | |||
| 289 | // to delete we need the PR_ENTRYID of the message |
||
| 290 | // TODO move this part to GrommunioDavBackend |
||
| 291 | $mapimessage = $this->gDavBackend->GetMapiMessageForId($addressBookId, $cardUri, $mapifolder, static::FILE_EXTENSION); |
||
| 292 | $props = mapi_getprops($mapimessage, [PR_ENTRYID]); |
||
| 293 | mapi_folder_deletemessages($mapifolder, [$props[PR_ENTRYID]]); |
||
| 294 | |||
| 295 | return true; |
||
| 296 | } |
||
| 297 | |||
| 298 | /** |
||
| 299 | * The getChanges method returns all the changes that have happened, since |
||
| 300 | * the specified syncToken in the specified address book. |
||
| 301 | * |
||
| 302 | * This function should return an array, such as the following: |
||
| 303 | * |
||
| 304 | * [ |
||
| 305 | * 'syncToken' => 'The current synctoken', |
||
| 306 | * 'added' => [ |
||
| 307 | * 'new.txt', |
||
| 308 | * ], |
||
| 309 | * 'modified' => [ |
||
| 310 | * 'modified.txt', |
||
| 311 | * ], |
||
| 312 | * 'deleted' => [ |
||
| 313 | * 'foo.php.bak', |
||
| 314 | * 'old.txt' |
||
| 315 | * ] |
||
| 316 | * ]; |
||
| 317 | * |
||
| 318 | * The returned syncToken property should reflect the *current* syncToken |
||
| 319 | * of the calendar, as reported in the {http://sabredav.org/ns}sync-token |
||
| 320 | * property. This is needed here too, to ensure the operation is atomic. |
||
| 321 | * |
||
| 322 | * If the $syncToken argument is specified as null, this is an initial |
||
| 323 | * sync, and all members should be reported. |
||
| 324 | * |
||
| 325 | * The modified property is an array of nodenames that have changed since |
||
| 326 | * the last token. |
||
| 327 | * |
||
| 328 | * The deleted property is an array with nodenames, that have been deleted |
||
| 329 | * from collection. |
||
| 330 | * |
||
| 331 | * The $syncLevel argument is basically the 'depth' of the report. If it's |
||
| 332 | * 1, you only have to report changes that happened only directly in |
||
| 333 | * immediate descendants. If it's 2, it should also include changes from |
||
| 334 | * the nodes below the child collections. (grandchildren) |
||
| 335 | * |
||
| 336 | * The $limit argument allows a client to specify how many results should |
||
| 337 | * be returned at most. If the limit is not specified, it should be treated |
||
| 338 | * as infinite. |
||
| 339 | * |
||
| 340 | * If the limit (infinite or not) is higher than you're willing to return, |
||
| 341 | * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. |
||
| 342 | * |
||
| 343 | * If the syncToken is expired (due to data cleanup) or unknown, you must |
||
| 344 | * return null. |
||
| 345 | * |
||
| 346 | * The limit is 'suggestive'. You are free to ignore it. |
||
| 347 | * |
||
| 348 | * @param string $addressBookId |
||
| 349 | * @param string $syncToken |
||
| 350 | * @param int $syncLevel |
||
| 351 | * @param int $limit |
||
| 352 | * |
||
| 353 | * @return array |
||
| 354 | */ |
||
| 355 | public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) { |
||
| 356 | $this->logger->trace("addressBookId: %s - syncToken: %s - syncLevel: %d - limit: %d", $addressBookId, $syncToken, $syncLevel, $limit); |
||
| 357 | |||
| 358 | return $this->gDavBackend->Sync($addressBookId, $syncToken, static::FILE_EXTENSION, $limit, ['types' => static::MESSAGE_CLASSES]); |
||
| 359 | } |
||
| 360 | } |
||
| 361 |
In the issue above, the returned value is violating the contract defined by the mentioned interface.
Let's take a look at an example: