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); |
||||
|
0 ignored issues
–
show
Unused Code
introduced
by
Loading history...
|
|||||
| 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) { |
||||
|
0 ignored issues
–
show
|
|||||
| 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
The expression
return false returns the type false which is incompatible with the return type mandated by Sabre\CardDAV\Backend\BackendInterface::getCard() of array.
In the issue above, the returned value is violating the contract defined by the mentioned interface. Let's take a look at an example: interface HasName {
/** @return string */
public function getName();
}
class Name {
public $name;
}
class User implements HasName {
/** @return string|Name */
public function getName() {
return new Name('foo'); // This is a violation of the ``HasName`` interface
// which only allows a string value to be returned.
}
}
Loading history...
|
|||||
| 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, []); |
||||
|
0 ignored issues
–
show
The function
mapi_mapitovcf was not found. Maybe you did not declare it correctly or list all dependencies?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
| 167 | $props = mapi_getprops($mapimessage, [PR_LAST_MODIFICATION_TIME]); |
||||
|
0 ignored issues
–
show
|
|||||
| 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); |
||||
|
0 ignored issues
–
show
The function
mapi_vcftomapi was not found. Maybe you did not declare it correctly or list all dependencies?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
| 266 | if ($ok) { |
||||
| 267 | mapi_savechanges($mapimessage); |
||||
| 268 | $props = mapi_getprops($mapimessage); |
||||
| 269 | |||||
| 270 | return '"' . $props[PR_LAST_MODIFICATION_TIME] . '"'; |
||||
|
0 ignored issues
–
show
|
|||||
| 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); |
||||
|
0 ignored issues
–
show
|
|||||
| 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]); |
||||
|
0 ignored issues
–
show
|
|||||
| 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 |