Issues (171)

lib/GrommunioCardDavBackend.php (3 issues)

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