GrommunioCardDavBackend   A
last analyzed

Complexity

Total Complexity 15

Size/Duplication

Total Lines 342
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 66
dl 0
loc 342
rs 10
c 4
b 0
f 0
wmc 15

12 Methods

Rating   Name   Duplication   Size   Complexity  
A createCard() 0 7 1
A updateCard() 0 6 1
A updateAddressBook() 0 3 1
A getAddressBooksForUser() 0 4 1
A deleteCard() 0 12 1
A setData() 0 13 2
A getCard() 0 34 3
A getCards() 0 5 1
A getChangesForAddressBook() 0 4 1
A createAddressBook() 0 5 1
A __construct() 0 3 1
A deleteAddressBook() 0 3 1
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
The assignment to $success is dead and can be removed.
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
introduced by
$mapifolder is of type null|resource, thus it always evaluated to false.
Loading history...
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
Bug Best Practice introduced by
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
Bug introduced by
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 ignore-call  annotation

166
		$vcf = /** @scrutinizer ignore-call */ mapi_mapitovcf($session, $ab, $mapimessage, []);
Loading history...
167
		$props = mapi_getprops($mapimessage, [PR_LAST_MODIFICATION_TIME]);
0 ignored issues
show
Bug introduced by
The constant grommunio\DAV\PR_LAST_MODIFICATION_TIME was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
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
Bug introduced by
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 ignore-call  annotation

265
		$ok = /** @scrutinizer ignore-call */ mapi_vcftomapi($session, $store, $mapimessage, $vcf);
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
Bug introduced by
The constant grommunio\DAV\PR_LAST_MODIFICATION_TIME was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
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
Unused Code introduced by
The assignment to $objectId is dead and can be removed.
Loading history...
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
Bug introduced by
The constant grommunio\DAV\PR_ENTRYID was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
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