1<?php
2
3namespace Sabre\CardDAV;
4
5use Sabre\DAV;
6use Sabre\DAVACL;
7
8/**
9 * The AddressBook class represents a CardDAV addressbook, owned by a specific user
10 *
11 * The AddressBook can contain multiple vcards
12 *
13 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
14 * @author Evert Pot (http://evertpot.com/)
15 * @license http://sabre.io/license/ Modified BSD License
16 */
17class AddressBook extends DAV\Collection implements IAddressBook, DAV\IProperties, DAVACL\IACL, DAV\Sync\ISyncCollection, DAV\IMultiGet {
18
19    use DAVACL\ACLTrait;
20
21    /**
22     * This is an array with addressbook information
23     *
24     * @var array
25     */
26    protected $addressBookInfo;
27
28    /**
29     * CardDAV backend
30     *
31     * @var Backend\BackendInterface
32     */
33    protected $carddavBackend;
34
35    /**
36     * Constructor
37     *
38     * @param Backend\BackendInterface $carddavBackend
39     * @param array $addressBookInfo
40     */
41    function __construct(Backend\BackendInterface $carddavBackend, array $addressBookInfo) {
42
43        $this->carddavBackend = $carddavBackend;
44        $this->addressBookInfo = $addressBookInfo;
45
46    }
47
48    /**
49     * Returns the name of the addressbook
50     *
51     * @return string
52     */
53    function getName() {
54
55        return $this->addressBookInfo['uri'];
56
57    }
58
59    /**
60     * Returns a card
61     *
62     * @param string $name
63     * @return Card
64     */
65    function getChild($name) {
66
67        $obj = $this->carddavBackend->getCard($this->addressBookInfo['id'], $name);
68        if (!$obj) throw new DAV\Exception\NotFound('Card not found');
69        return new Card($this->carddavBackend, $this->addressBookInfo, $obj);
70
71    }
72
73    /**
74     * Returns the full list of cards
75     *
76     * @return array
77     */
78    function getChildren() {
79
80        $objs = $this->carddavBackend->getCards($this->addressBookInfo['id']);
81        $children = [];
82        foreach ($objs as $obj) {
83            $obj['acl'] = $this->getChildACL();
84            $children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj);
85        }
86        return $children;
87
88    }
89
90    /**
91     * This method receives a list of paths in it's first argument.
92     * It must return an array with Node objects.
93     *
94     * If any children are not found, you do not have to return them.
95     *
96     * @param string[] $paths
97     * @return array
98     */
99    function getMultipleChildren(array $paths) {
100
101        $objs = $this->carddavBackend->getMultipleCards($this->addressBookInfo['id'], $paths);
102        $children = [];
103        foreach ($objs as $obj) {
104            $obj['acl'] = $this->getChildACL();
105            $children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj);
106        }
107        return $children;
108
109    }
110
111    /**
112     * Creates a new directory
113     *
114     * We actually block this, as subdirectories are not allowed in addressbooks.
115     *
116     * @param string $name
117     * @return void
118     */
119    function createDirectory($name) {
120
121        throw new DAV\Exception\MethodNotAllowed('Creating collections in addressbooks is not allowed');
122
123    }
124
125    /**
126     * Creates a new file
127     *
128     * The contents of the new file must be a valid VCARD.
129     *
130     * This method may return an ETag.
131     *
132     * @param string $name
133     * @param resource $vcardData
134     * @return string|null
135     */
136    function createFile($name, $vcardData = null) {
137
138        if (is_resource($vcardData)) {
139            $vcardData = stream_get_contents($vcardData);
140        }
141        // Converting to UTF-8, if needed
142        $vcardData = DAV\StringUtil::ensureUTF8($vcardData);
143
144        return $this->carddavBackend->createCard($this->addressBookInfo['id'], $name, $vcardData);
145
146    }
147
148    /**
149     * Deletes the entire addressbook.
150     *
151     * @return void
152     */
153    function delete() {
154
155        $this->carddavBackend->deleteAddressBook($this->addressBookInfo['id']);
156
157    }
158
159    /**
160     * Renames the addressbook
161     *
162     * @param string $newName
163     * @return void
164     */
165    function setName($newName) {
166
167        throw new DAV\Exception\MethodNotAllowed('Renaming addressbooks is not yet supported');
168
169    }
170
171    /**
172     * Returns the last modification date as a unix timestamp.
173     *
174     * @return void
175     */
176    function getLastModified() {
177
178        return null;
179
180    }
181
182    /**
183     * Updates properties on this node.
184     *
185     * This method received a PropPatch object, which contains all the
186     * information about the update.
187     *
188     * To update specific properties, call the 'handle' method on this object.
189     * Read the PropPatch documentation for more information.
190     *
191     * @param DAV\PropPatch $propPatch
192     * @return void
193     */
194    function propPatch(DAV\PropPatch $propPatch) {
195
196        return $this->carddavBackend->updateAddressBook($this->addressBookInfo['id'], $propPatch);
197
198    }
199
200    /**
201     * Returns a list of properties for this nodes.
202     *
203     * The properties list is a list of propertynames the client requested,
204     * encoded in clark-notation {xmlnamespace}tagname
205     *
206     * If the array is empty, it means 'all properties' were requested.
207     *
208     * @param array $properties
209     * @return array
210     */
211    function getProperties($properties) {
212
213        $response = [];
214        foreach ($properties as $propertyName) {
215
216            if (isset($this->addressBookInfo[$propertyName])) {
217
218                $response[$propertyName] = $this->addressBookInfo[$propertyName];
219
220            }
221
222        }
223
224        return $response;
225
226    }
227
228    /**
229     * Returns the owner principal
230     *
231     * This must be a url to a principal, or null if there's no owner
232     *
233     * @return string|null
234     */
235    function getOwner() {
236
237        return $this->addressBookInfo['principaluri'];
238
239    }
240
241
242    /**
243     * This method returns the ACL's for card nodes in this address book.
244     * The result of this method automatically gets passed to the
245     * card nodes in this address book.
246     *
247     * @return array
248     */
249    function getChildACL() {
250
251        return [
252            [
253                'privilege' => '{DAV:}all',
254                'principal' => $this->getOwner(),
255                'protected' => true,
256            ],
257        ];
258
259    }
260
261
262    /**
263     * This method returns the current sync-token for this collection.
264     * This can be any string.
265     *
266     * If null is returned from this function, the plugin assumes there's no
267     * sync information available.
268     *
269     * @return string|null
270     */
271    function getSyncToken() {
272
273        if (
274            $this->carddavBackend instanceof Backend\SyncSupport &&
275            isset($this->addressBookInfo['{DAV:}sync-token'])
276        ) {
277            return $this->addressBookInfo['{DAV:}sync-token'];
278        }
279        if (
280            $this->carddavBackend instanceof Backend\SyncSupport &&
281            isset($this->addressBookInfo['{http://sabredav.org/ns}sync-token'])
282        ) {
283            return $this->addressBookInfo['{http://sabredav.org/ns}sync-token'];
284        }
285
286    }
287
288    /**
289     * The getChanges method returns all the changes that have happened, since
290     * the specified syncToken and the current collection.
291     *
292     * This function should return an array, such as the following:
293     *
294     * [
295     *   'syncToken' => 'The current synctoken',
296     *   'added'   => [
297     *      'new.txt',
298     *   ],
299     *   'modified'   => [
300     *      'modified.txt',
301     *   ],
302     *   'deleted' => [
303     *      'foo.php.bak',
304     *      'old.txt'
305     *   ]
306     * ];
307     *
308     * The syncToken property should reflect the *current* syncToken of the
309     * collection, as reported getSyncToken(). This is needed here too, to
310     * ensure the operation is atomic.
311     *
312     * If the syncToken is specified as null, this is an initial sync, and all
313     * members should be reported.
314     *
315     * The modified property is an array of nodenames that have changed since
316     * the last token.
317     *
318     * The deleted property is an array with nodenames, that have been deleted
319     * from collection.
320     *
321     * The second argument is basically the 'depth' of the report. If it's 1,
322     * you only have to report changes that happened only directly in immediate
323     * descendants. If it's 2, it should also include changes from the nodes
324     * below the child collections. (grandchildren)
325     *
326     * The third (optional) argument allows a client to specify how many
327     * results should be returned at most. If the limit is not specified, it
328     * should be treated as infinite.
329     *
330     * If the limit (infinite or not) is higher than you're willing to return,
331     * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
332     *
333     * If the syncToken is expired (due to data cleanup) or unknown, you must
334     * return null.
335     *
336     * The limit is 'suggestive'. You are free to ignore it.
337     *
338     * @param string $syncToken
339     * @param int $syncLevel
340     * @param int $limit
341     * @return array
342     */
343    function getChanges($syncToken, $syncLevel, $limit = null) {
344
345        if (!$this->carddavBackend instanceof Backend\SyncSupport) {
346            return null;
347        }
348
349        return $this->carddavBackend->getChangesForAddressBook(
350            $this->addressBookInfo['id'],
351            $syncToken,
352            $syncLevel,
353            $limit
354        );
355
356    }
357}
358