Completed
Branch develop (fa72bb)
by
unknown
26:08
created
htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/Outbox.php 1 patch
Indentation   +89 added lines, -89 removed lines patch added patch discarded remove patch
@@ -21,99 +21,99 @@
 block discarded – undo
21 21
  */
22 22
 class Outbox extends DAV\Collection implements IOutbox
23 23
 {
24
-    use DAVACL\ACLTrait;
24
+	use DAVACL\ACLTrait;
25 25
 
26
-    /**
27
-     * The principal Uri.
28
-     *
29
-     * @var string
30
-     */
31
-    protected $principalUri;
26
+	/**
27
+	 * The principal Uri.
28
+	 *
29
+	 * @var string
30
+	 */
31
+	protected $principalUri;
32 32
 
33
-    /**
34
-     * Constructor.
35
-     *
36
-     * @param string $principalUri
37
-     */
38
-    public function __construct($principalUri)
39
-    {
40
-        $this->principalUri = $principalUri;
41
-    }
33
+	/**
34
+	 * Constructor.
35
+	 *
36
+	 * @param string $principalUri
37
+	 */
38
+	public function __construct($principalUri)
39
+	{
40
+		$this->principalUri = $principalUri;
41
+	}
42 42
 
43
-    /**
44
-     * Returns the name of the node.
45
-     *
46
-     * This is used to generate the url.
47
-     *
48
-     * @return string
49
-     */
50
-    public function getName()
51
-    {
52
-        return 'outbox';
53
-    }
43
+	/**
44
+	 * Returns the name of the node.
45
+	 *
46
+	 * This is used to generate the url.
47
+	 *
48
+	 * @return string
49
+	 */
50
+	public function getName()
51
+	{
52
+		return 'outbox';
53
+	}
54 54
 
55
-    /**
56
-     * Returns an array with all the child nodes.
57
-     *
58
-     * @return \Sabre\DAV\INode[]
59
-     */
60
-    public function getChildren()
61
-    {
62
-        return [];
63
-    }
55
+	/**
56
+	 * Returns an array with all the child nodes.
57
+	 *
58
+	 * @return \Sabre\DAV\INode[]
59
+	 */
60
+	public function getChildren()
61
+	{
62
+		return [];
63
+	}
64 64
 
65
-    /**
66
-     * Returns the owner principal.
67
-     *
68
-     * This must be a url to a principal, or null if there's no owner
69
-     *
70
-     * @return string|null
71
-     */
72
-    public function getOwner()
73
-    {
74
-        return $this->principalUri;
75
-    }
65
+	/**
66
+	 * Returns the owner principal.
67
+	 *
68
+	 * This must be a url to a principal, or null if there's no owner
69
+	 *
70
+	 * @return string|null
71
+	 */
72
+	public function getOwner()
73
+	{
74
+		return $this->principalUri;
75
+	}
76 76
 
77
-    /**
78
-     * Returns a list of ACE's for this node.
79
-     *
80
-     * Each ACE has the following properties:
81
-     *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
82
-     *     currently the only supported privileges
83
-     *   * 'principal', a url to the principal who owns the node
84
-     *   * 'protected' (optional), indicating that this ACE is not allowed to
85
-     *      be updated.
86
-     *
87
-     * @return array
88
-     */
89
-    public function getACL()
90
-    {
91
-        return [
92
-            [
93
-                'privilege' => '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-send',
94
-                'principal' => $this->getOwner(),
95
-                'protected' => true,
96
-            ],
97
-            [
98
-                'privilege' => '{DAV:}read',
99
-                'principal' => $this->getOwner(),
100
-                'protected' => true,
101
-            ],
102
-            [
103
-                'privilege' => '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-send',
104
-                'principal' => $this->getOwner().'/calendar-proxy-write',
105
-                'protected' => true,
106
-            ],
107
-            [
108
-                'privilege' => '{DAV:}read',
109
-                'principal' => $this->getOwner().'/calendar-proxy-read',
110
-                'protected' => true,
111
-            ],
112
-            [
113
-                'privilege' => '{DAV:}read',
114
-                'principal' => $this->getOwner().'/calendar-proxy-write',
115
-                'protected' => true,
116
-            ],
117
-        ];
118
-    }
77
+	/**
78
+	 * Returns a list of ACE's for this node.
79
+	 *
80
+	 * Each ACE has the following properties:
81
+	 *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
82
+	 *     currently the only supported privileges
83
+	 *   * 'principal', a url to the principal who owns the node
84
+	 *   * 'protected' (optional), indicating that this ACE is not allowed to
85
+	 *      be updated.
86
+	 *
87
+	 * @return array
88
+	 */
89
+	public function getACL()
90
+	{
91
+		return [
92
+			[
93
+				'privilege' => '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-send',
94
+				'principal' => $this->getOwner(),
95
+				'protected' => true,
96
+			],
97
+			[
98
+				'privilege' => '{DAV:}read',
99
+				'principal' => $this->getOwner(),
100
+				'protected' => true,
101
+			],
102
+			[
103
+				'privilege' => '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-send',
104
+				'principal' => $this->getOwner().'/calendar-proxy-write',
105
+				'protected' => true,
106
+			],
107
+			[
108
+				'privilege' => '{DAV:}read',
109
+				'principal' => $this->getOwner().'/calendar-proxy-read',
110
+				'protected' => true,
111
+			],
112
+			[
113
+				'privilege' => '{DAV:}read',
114
+				'principal' => $this->getOwner().'/calendar-proxy-write',
115
+				'protected' => true,
116
+			],
117
+		];
118
+	}
119 119
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/Plugin.php 2 patches
Spacing   +7 added lines, -7 removed lines patch added patch discarded remove patch
@@ -198,7 +198,7 @@  discard block
 block discarded – undo
198 198
             $principalUrl = $node->getPrincipalUrl();
199 199
 
200 200
             // schedule-outbox-URL property
201
-            $propFind->handle('{'.self::NS_CALDAV.'}schedule-outbox-URL', function () use ($principalUrl, $caldavPlugin) {
201
+            $propFind->handle('{'.self::NS_CALDAV.'}schedule-outbox-URL', function() use ($principalUrl, $caldavPlugin) {
202 202
                 $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
203 203
                 if (!$calendarHomePath) {
204 204
                     return null;
@@ -208,7 +208,7 @@  discard block
 block discarded – undo
208 208
                 return new LocalHref($outboxPath);
209 209
             });
210 210
             // schedule-inbox-URL property
211
-            $propFind->handle('{'.self::NS_CALDAV.'}schedule-inbox-URL', function () use ($principalUrl, $caldavPlugin) {
211
+            $propFind->handle('{'.self::NS_CALDAV.'}schedule-inbox-URL', function() use ($principalUrl, $caldavPlugin) {
212 212
                 $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
213 213
                 if (!$calendarHomePath) {
214 214
                     return null;
@@ -218,7 +218,7 @@  discard block
 block discarded – undo
218 218
                 return new LocalHref($inboxPath);
219 219
             });
220 220
 
221
-            $propFind->handle('{'.self::NS_CALDAV.'}schedule-default-calendar-URL', function () use ($principalUrl, $caldavPlugin) {
221
+            $propFind->handle('{'.self::NS_CALDAV.'}schedule-default-calendar-URL', function() use ($principalUrl, $caldavPlugin) {
222 222
                 // We don't support customizing this property yet, so in the
223 223
                 // meantime we just grab the first calendar in the home-set.
224 224
                 $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
@@ -258,13 +258,13 @@  discard block
 block discarded – undo
258 258
 
259 259
             // The server currently reports every principal to be of type
260 260
             // 'INDIVIDUAL'
261
-            $propFind->handle('{'.self::NS_CALDAV.'}calendar-user-type', function () {
261
+            $propFind->handle('{'.self::NS_CALDAV.'}calendar-user-type', function() {
262 262
                 return 'INDIVIDUAL';
263 263
             });
264 264
         }
265 265
 
266 266
         // Mapping the old property to the new property.
267
-        $propFind->handle('{http://calendarserver.org/ns/}calendar-availability', function () use ($propFind, $node) {
267
+        $propFind->handle('{http://calendarserver.org/ns/}calendar-availability', function() use ($propFind, $node) {
268 268
             // In case it wasn't clear, the only difference is that we map the
269 269
             // old property to a different namespace.
270 270
             $availProp = '{'.self::NS_CALDAV.'}calendar-availability';
@@ -294,7 +294,7 @@  discard block
 block discarded – undo
294 294
     public function propPatch($path, PropPatch $propPatch)
295 295
     {
296 296
         // Mapping the old property to the new property.
297
-        $propPatch->handle('{http://calendarserver.org/ns/}calendar-availability', function ($value) use ($path) {
297
+        $propPatch->handle('{http://calendarserver.org/ns/}calendar-availability', function($value) use ($path) {
298 298
             $availProp = '{'.self::NS_CALDAV.'}calendar-availability';
299 299
             $subPropPatch = new PropPatch([$availProp => $value]);
300 300
             $this->server->emit('propPatch', [$path, $subPropPatch]);
@@ -919,7 +919,7 @@  discard block
 block discarded – undo
919 919
                 'time-range' => null,
920 920
             ]);
921 921
 
922
-            $calObjects = array_map(function ($url) use ($node) {
922
+            $calObjects = array_map(function($url) use ($node) {
923 923
                 $obj = $node->getChild($url)->get();
924 924
 
925 925
                 return $obj;
Please login to merge, or discard this patch.
Indentation   +931 added lines, -931 removed lines patch added patch discarded remove patch
@@ -57,941 +57,941 @@
 block discarded – undo
57 57
  */
58 58
 class Plugin extends ServerPlugin
59 59
 {
60
-    /**
61
-     * This is the official CalDAV namespace.
62
-     */
63
-    const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
64
-
65
-    /**
66
-     * Reference to main Server object.
67
-     *
68
-     * @var Server
69
-     */
70
-    protected $server;
71
-
72
-    /**
73
-     * Returns a list of features for the DAV: HTTP header.
74
-     *
75
-     * @return array
76
-     */
77
-    public function getFeatures()
78
-    {
79
-        return ['calendar-auto-schedule', 'calendar-availability'];
80
-    }
81
-
82
-    /**
83
-     * Returns the name of the plugin.
84
-     *
85
-     * Using this name other plugins will be able to access other plugins
86
-     * using Server::getPlugin
87
-     *
88
-     * @return string
89
-     */
90
-    public function getPluginName()
91
-    {
92
-        return 'caldav-schedule';
93
-    }
94
-
95
-    /**
96
-     * Initializes the plugin.
97
-     */
98
-    public function initialize(Server $server)
99
-    {
100
-        $this->server = $server;
101
-        $server->on('method:POST', [$this, 'httpPost']);
102
-        $server->on('propFind', [$this, 'propFind']);
103
-        $server->on('propPatch', [$this, 'propPatch']);
104
-        $server->on('calendarObjectChange', [$this, 'calendarObjectChange']);
105
-        $server->on('beforeUnbind', [$this, 'beforeUnbind']);
106
-        $server->on('schedule', [$this, 'scheduleLocalDelivery']);
107
-        $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']);
108
-
109
-        $ns = '{'.self::NS_CALDAV.'}';
110
-
111
-        /*
60
+	/**
61
+	 * This is the official CalDAV namespace.
62
+	 */
63
+	const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
64
+
65
+	/**
66
+	 * Reference to main Server object.
67
+	 *
68
+	 * @var Server
69
+	 */
70
+	protected $server;
71
+
72
+	/**
73
+	 * Returns a list of features for the DAV: HTTP header.
74
+	 *
75
+	 * @return array
76
+	 */
77
+	public function getFeatures()
78
+	{
79
+		return ['calendar-auto-schedule', 'calendar-availability'];
80
+	}
81
+
82
+	/**
83
+	 * Returns the name of the plugin.
84
+	 *
85
+	 * Using this name other plugins will be able to access other plugins
86
+	 * using Server::getPlugin
87
+	 *
88
+	 * @return string
89
+	 */
90
+	public function getPluginName()
91
+	{
92
+		return 'caldav-schedule';
93
+	}
94
+
95
+	/**
96
+	 * Initializes the plugin.
97
+	 */
98
+	public function initialize(Server $server)
99
+	{
100
+		$this->server = $server;
101
+		$server->on('method:POST', [$this, 'httpPost']);
102
+		$server->on('propFind', [$this, 'propFind']);
103
+		$server->on('propPatch', [$this, 'propPatch']);
104
+		$server->on('calendarObjectChange', [$this, 'calendarObjectChange']);
105
+		$server->on('beforeUnbind', [$this, 'beforeUnbind']);
106
+		$server->on('schedule', [$this, 'scheduleLocalDelivery']);
107
+		$server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']);
108
+
109
+		$ns = '{'.self::NS_CALDAV.'}';
110
+
111
+		/*
112 112
          * This information ensures that the {DAV:}resourcetype property has
113 113
          * the correct values.
114 114
          */
115
-        $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IOutbox'] = $ns.'schedule-outbox';
116
-        $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IInbox'] = $ns.'schedule-inbox';
115
+		$server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IOutbox'] = $ns.'schedule-outbox';
116
+		$server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IInbox'] = $ns.'schedule-inbox';
117 117
 
118
-        /*
118
+		/*
119 119
          * Properties we protect are made read-only by the server.
120 120
          */
121
-        array_push($server->protectedProperties,
122
-            $ns.'schedule-inbox-URL',
123
-            $ns.'schedule-outbox-URL',
124
-            $ns.'calendar-user-address-set',
125
-            $ns.'calendar-user-type',
126
-            $ns.'schedule-default-calendar-URL'
127
-        );
128
-    }
129
-
130
-    /**
131
-     * Use this method to tell the server this plugin defines additional
132
-     * HTTP methods.
133
-     *
134
-     * This method is passed a uri. It should only return HTTP methods that are
135
-     * available for the specified uri.
136
-     *
137
-     * @param string $uri
138
-     *
139
-     * @return array
140
-     */
141
-    public function getHTTPMethods($uri)
142
-    {
143
-        try {
144
-            $node = $this->server->tree->getNodeForPath($uri);
145
-        } catch (NotFound $e) {
146
-            return [];
147
-        }
148
-
149
-        if ($node instanceof IOutbox) {
150
-            return ['POST'];
151
-        }
152
-
153
-        return [];
154
-    }
155
-
156
-    /**
157
-     * This method handles POST request for the outbox.
158
-     *
159
-     * @return bool
160
-     */
161
-    public function httpPost(RequestInterface $request, ResponseInterface $response)
162
-    {
163
-        // Checking if this is a text/calendar content type
164
-        $contentType = $request->getHeader('Content-Type');
165
-        if (!$contentType || 0 !== strpos($contentType, 'text/calendar')) {
166
-            return;
167
-        }
168
-
169
-        $path = $request->getPath();
170
-
171
-        // Checking if we're talking to an outbox
172
-        try {
173
-            $node = $this->server->tree->getNodeForPath($path);
174
-        } catch (NotFound $e) {
175
-            return;
176
-        }
177
-        if (!$node instanceof IOutbox) {
178
-            return;
179
-        }
180
-
181
-        $this->server->transactionType = 'post-caldav-outbox';
182
-        $this->outboxRequest($node, $request, $response);
183
-
184
-        // Returning false breaks the event chain and tells the server we've
185
-        // handled the request.
186
-        return false;
187
-    }
188
-
189
-    /**
190
-     * This method handler is invoked during fetching of properties.
191
-     *
192
-     * We use this event to add calendar-auto-schedule-specific properties.
193
-     */
194
-    public function propFind(PropFind $propFind, INode $node)
195
-    {
196
-        if ($node instanceof DAVACL\IPrincipal) {
197
-            $caldavPlugin = $this->server->getPlugin('caldav');
198
-            $principalUrl = $node->getPrincipalUrl();
199
-
200
-            // schedule-outbox-URL property
201
-            $propFind->handle('{'.self::NS_CALDAV.'}schedule-outbox-URL', function () use ($principalUrl, $caldavPlugin) {
202
-                $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
203
-                if (!$calendarHomePath) {
204
-                    return null;
205
-                }
206
-                $outboxPath = $calendarHomePath.'/outbox/';
207
-
208
-                return new LocalHref($outboxPath);
209
-            });
210
-            // schedule-inbox-URL property
211
-            $propFind->handle('{'.self::NS_CALDAV.'}schedule-inbox-URL', function () use ($principalUrl, $caldavPlugin) {
212
-                $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
213
-                if (!$calendarHomePath) {
214
-                    return null;
215
-                }
216
-                $inboxPath = $calendarHomePath.'/inbox/';
217
-
218
-                return new LocalHref($inboxPath);
219
-            });
220
-
221
-            $propFind->handle('{'.self::NS_CALDAV.'}schedule-default-calendar-URL', function () use ($principalUrl, $caldavPlugin) {
222
-                // We don't support customizing this property yet, so in the
223
-                // meantime we just grab the first calendar in the home-set.
224
-                $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
225
-
226
-                if (!$calendarHomePath) {
227
-                    return null;
228
-                }
229
-
230
-                $sccs = '{'.self::NS_CALDAV.'}supported-calendar-component-set';
231
-
232
-                $result = $this->server->getPropertiesForPath($calendarHomePath, [
233
-                    '{DAV:}resourcetype',
234
-                    '{DAV:}share-access',
235
-                    $sccs,
236
-                ], 1);
237
-
238
-                foreach ($result as $child) {
239
-                    if (!isset($child[200]['{DAV:}resourcetype']) || !$child[200]['{DAV:}resourcetype']->is('{'.self::NS_CALDAV.'}calendar')) {
240
-                        // Node is either not a calendar
241
-                        continue;
242
-                    }
243
-                    if (isset($child[200]['{DAV:}share-access'])) {
244
-                        $shareAccess = $child[200]['{DAV:}share-access']->getValue();
245
-                        if (Sharing\Plugin::ACCESS_NOTSHARED !== $shareAccess && Sharing\Plugin::ACCESS_SHAREDOWNER !== $shareAccess) {
246
-                            // Node is a shared node, not owned by the relevant
247
-                            // user.
248
-                            continue;
249
-                        }
250
-                    }
251
-                    if (!isset($child[200][$sccs]) || in_array('VEVENT', $child[200][$sccs]->getValue())) {
252
-                        // Either there is no supported-calendar-component-set
253
-                        // (which is fine) or we found one that supports VEVENT.
254
-                        return new LocalHref($child['href']);
255
-                    }
256
-                }
257
-            });
258
-
259
-            // The server currently reports every principal to be of type
260
-            // 'INDIVIDUAL'
261
-            $propFind->handle('{'.self::NS_CALDAV.'}calendar-user-type', function () {
262
-                return 'INDIVIDUAL';
263
-            });
264
-        }
265
-
266
-        // Mapping the old property to the new property.
267
-        $propFind->handle('{http://calendarserver.org/ns/}calendar-availability', function () use ($propFind, $node) {
268
-            // In case it wasn't clear, the only difference is that we map the
269
-            // old property to a different namespace.
270
-            $availProp = '{'.self::NS_CALDAV.'}calendar-availability';
271
-            $subPropFind = new PropFind(
272
-                 $propFind->getPath(),
273
-                 [$availProp]
274
-             );
275
-
276
-            $this->server->getPropertiesByNode(
277
-                 $subPropFind,
278
-                 $node
279
-             );
280
-
281
-            $propFind->set(
282
-                 '{http://calendarserver.org/ns/}calendar-availability',
283
-                 $subPropFind->get($availProp),
284
-                 $subPropFind->getStatus($availProp)
285
-             );
286
-        });
287
-    }
288
-
289
-    /**
290
-     * This method is called during property updates.
291
-     *
292
-     * @param string $path
293
-     */
294
-    public function propPatch($path, PropPatch $propPatch)
295
-    {
296
-        // Mapping the old property to the new property.
297
-        $propPatch->handle('{http://calendarserver.org/ns/}calendar-availability', function ($value) use ($path) {
298
-            $availProp = '{'.self::NS_CALDAV.'}calendar-availability';
299
-            $subPropPatch = new PropPatch([$availProp => $value]);
300
-            $this->server->emit('propPatch', [$path, $subPropPatch]);
301
-            $subPropPatch->commit();
302
-
303
-            return $subPropPatch->getResult()[$availProp];
304
-        });
305
-    }
306
-
307
-    /**
308
-     * This method is triggered whenever there was a calendar object gets
309
-     * created or updated.
310
-     *
311
-     * @param RequestInterface  $request      HTTP request
312
-     * @param ResponseInterface $response     HTTP Response
313
-     * @param VCalendar         $vCal         Parsed iCalendar object
314
-     * @param mixed             $calendarPath Path to calendar collection
315
-     * @param mixed             $modified     the iCalendar object has been touched
316
-     * @param mixed             $isNew        Whether this was a new item or we're updating one
317
-     */
318
-    public function calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew)
319
-    {
320
-        if (!$this->scheduleReply($this->server->httpRequest)) {
321
-            return;
322
-        }
323
-
324
-        $calendarNode = $this->server->tree->getNodeForPath($calendarPath);
325
-
326
-        $addresses = $this->getAddressesForPrincipal(
327
-            $calendarNode->getOwner()
328
-        );
329
-
330
-        if (!$isNew) {
331
-            $node = $this->server->tree->getNodeForPath($request->getPath());
332
-            $oldObj = Reader::read($node->get());
333
-        } else {
334
-            $oldObj = null;
335
-        }
336
-
337
-        $this->processICalendarChange($oldObj, $vCal, $addresses, [], $modified);
338
-
339
-        if ($oldObj) {
340
-            // Destroy circular references so PHP will GC the object.
341
-            $oldObj->destroy();
342
-        }
343
-    }
344
-
345
-    /**
346
-     * This method is responsible for delivering the ITip message.
347
-     */
348
-    public function deliver(ITip\Message $iTipMessage)
349
-    {
350
-        $this->server->emit('schedule', [$iTipMessage]);
351
-        if (!$iTipMessage->scheduleStatus) {
352
-            $iTipMessage->scheduleStatus = '5.2;There was no system capable of delivering the scheduling message';
353
-        }
354
-        // In case the change was considered 'insignificant', we are going to
355
-        // remove any error statuses, if any. See ticket #525.
356
-        list($baseCode) = explode('.', $iTipMessage->scheduleStatus);
357
-        if (!$iTipMessage->significantChange && in_array($baseCode, ['3', '5'])) {
358
-            $iTipMessage->scheduleStatus = null;
359
-        }
360
-    }
361
-
362
-    /**
363
-     * This method is triggered before a file gets deleted.
364
-     *
365
-     * We use this event to make sure that when this happens, attendees get
366
-     * cancellations, and organizers get 'DECLINED' statuses.
367
-     *
368
-     * @param string $path
369
-     */
370
-    public function beforeUnbind($path)
371
-    {
372
-        // FIXME: We shouldn't trigger this functionality when we're issuing a
373
-        // MOVE. This is a hack.
374
-        if ('MOVE' === $this->server->httpRequest->getMethod()) {
375
-            return;
376
-        }
377
-
378
-        $node = $this->server->tree->getNodeForPath($path);
379
-
380
-        if (!$node instanceof ICalendarObject || $node instanceof ISchedulingObject) {
381
-            return;
382
-        }
383
-
384
-        if (!$this->scheduleReply($this->server->httpRequest)) {
385
-            return;
386
-        }
387
-
388
-        $addresses = $this->getAddressesForPrincipal(
389
-            $node->getOwner()
390
-        );
391
-
392
-        $broker = new ITip\Broker();
393
-        $messages = $broker->parseEvent(null, $addresses, $node->get());
394
-
395
-        foreach ($messages as $message) {
396
-            $this->deliver($message);
397
-        }
398
-    }
399
-
400
-    /**
401
-     * Event handler for the 'schedule' event.
402
-     *
403
-     * This handler attempts to look at local accounts to deliver the
404
-     * scheduling object.
405
-     */
406
-    public function scheduleLocalDelivery(ITip\Message $iTipMessage)
407
-    {
408
-        $aclPlugin = $this->server->getPlugin('acl');
409
-
410
-        // Local delivery is not available if the ACL plugin is not loaded.
411
-        if (!$aclPlugin) {
412
-            return;
413
-        }
414
-
415
-        $caldavNS = '{'.self::NS_CALDAV.'}';
416
-
417
-        $principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient);
418
-        if (!$principalUri) {
419
-            $iTipMessage->scheduleStatus = '3.7;Could not find principal.';
420
-
421
-            return;
422
-        }
423
-
424
-        // We found a principal URL, now we need to find its inbox.
425
-        // Unfortunately we may not have sufficient privileges to find this, so
426
-        // we are temporarily turning off ACL to let this come through.
427
-        //
428
-        // Once we support PHP 5.5, this should be wrapped in a try..finally
429
-        // block so we can ensure that this privilege gets added again after.
430
-        $this->server->removeListener('propFind', [$aclPlugin, 'propFind']);
431
-
432
-        $result = $this->server->getProperties(
433
-            $principalUri,
434
-            [
435
-                '{DAV:}principal-URL',
436
-                 $caldavNS.'calendar-home-set',
437
-                 $caldavNS.'schedule-inbox-URL',
438
-                 $caldavNS.'schedule-default-calendar-URL',
439
-                '{http://sabredav.org/ns}email-address',
440
-            ]
441
-        );
442
-
443
-        // Re-registering the ACL event
444
-        $this->server->on('propFind', [$aclPlugin, 'propFind'], 20);
445
-
446
-        if (!isset($result[$caldavNS.'schedule-inbox-URL'])) {
447
-            $iTipMessage->scheduleStatus = '5.2;Could not find local inbox';
448
-
449
-            return;
450
-        }
451
-        if (!isset($result[$caldavNS.'calendar-home-set'])) {
452
-            $iTipMessage->scheduleStatus = '5.2;Could not locate a calendar-home-set';
453
-
454
-            return;
455
-        }
456
-        if (!isset($result[$caldavNS.'schedule-default-calendar-URL'])) {
457
-            $iTipMessage->scheduleStatus = '5.2;Could not find a schedule-default-calendar-URL property';
458
-
459
-            return;
460
-        }
461
-
462
-        $calendarPath = $result[$caldavNS.'schedule-default-calendar-URL']->getHref();
463
-        $homePath = $result[$caldavNS.'calendar-home-set']->getHref();
464
-        $inboxPath = $result[$caldavNS.'schedule-inbox-URL']->getHref();
465
-
466
-        if ('REPLY' === $iTipMessage->method) {
467
-            $privilege = 'schedule-deliver-reply';
468
-        } else {
469
-            $privilege = 'schedule-deliver-invite';
470
-        }
471
-
472
-        if (!$aclPlugin->checkPrivileges($inboxPath, $caldavNS.$privilege, DAVACL\Plugin::R_PARENT, false)) {
473
-            $iTipMessage->scheduleStatus = '3.8;insufficient privileges: '.$privilege.' is required on the recipient schedule inbox.';
474
-
475
-            return;
476
-        }
477
-
478
-        // Next, we're going to find out if the item already exits in one of
479
-        // the users' calendars.
480
-        $uid = $iTipMessage->uid;
481
-
482
-        $newFileName = 'sabredav-'.\Sabre\DAV\UUIDUtil::getUUID().'.ics';
483
-
484
-        $home = $this->server->tree->getNodeForPath($homePath);
485
-        $inbox = $this->server->tree->getNodeForPath($inboxPath);
486
-
487
-        $currentObject = null;
488
-        $objectNode = null;
489
-        $oldICalendarData = null;
490
-        $isNewNode = false;
491
-
492
-        $result = $home->getCalendarObjectByUID($uid);
493
-        if ($result) {
494
-            // There was an existing object, we need to update probably.
495
-            $objectPath = $homePath.'/'.$result;
496
-            $objectNode = $this->server->tree->getNodeForPath($objectPath);
497
-            $oldICalendarData = $objectNode->get();
498
-            $currentObject = Reader::read($oldICalendarData);
499
-        } else {
500
-            $isNewNode = true;
501
-        }
502
-
503
-        $broker = new ITip\Broker();
504
-        $newObject = $broker->processMessage($iTipMessage, $currentObject);
505
-
506
-        $inbox->createFile($newFileName, $iTipMessage->message->serialize());
507
-
508
-        if (!$newObject) {
509
-            // We received an iTip message referring to a UID that we don't
510
-            // have in any calendars yet, and processMessage did not give us a
511
-            // calendarobject back.
512
-            //
513
-            // The implication is that processMessage did not understand the
514
-            // iTip message.
515
-            $iTipMessage->scheduleStatus = '5.0;iTip message was not processed by the server, likely because we didn\'t understand it.';
516
-
517
-            return;
518
-        }
519
-
520
-        // Note that we are bypassing ACL on purpose by calling this directly.
521
-        // We may need to look a bit deeper into this later. Supporting ACL
522
-        // here would be nice.
523
-        if ($isNewNode) {
524
-            $calendar = $this->server->tree->getNodeForPath($calendarPath);
525
-            $calendar->createFile($newFileName, $newObject->serialize());
526
-        } else {
527
-            // If the message was a reply, we may have to inform other
528
-            // attendees of this attendees status. Therefore we're shooting off
529
-            // another itipMessage.
530
-            if ('REPLY' === $iTipMessage->method) {
531
-                $this->processICalendarChange(
532
-                    $oldICalendarData,
533
-                    $newObject,
534
-                    [$iTipMessage->recipient],
535
-                    [$iTipMessage->sender]
536
-                );
537
-            }
538
-            $objectNode->put($newObject->serialize());
539
-        }
540
-        $iTipMessage->scheduleStatus = '1.2;Message delivered locally';
541
-    }
542
-
543
-    /**
544
-     * This method is triggered whenever a subsystem requests the privileges
545
-     * that are supported on a particular node.
546
-     *
547
-     * We need to add a number of privileges for scheduling purposes.
548
-     */
549
-    public function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet)
550
-    {
551
-        $ns = '{'.self::NS_CALDAV.'}';
552
-        if ($node instanceof IOutbox) {
553
-            $supportedPrivilegeSet[$ns.'schedule-send'] = [
554
-                'abstract' => false,
555
-                'aggregates' => [
556
-                    $ns.'schedule-send-invite' => [
557
-                        'abstract' => false,
558
-                        'aggregates' => [],
559
-                    ],
560
-                    $ns.'schedule-send-reply' => [
561
-                        'abstract' => false,
562
-                        'aggregates' => [],
563
-                    ],
564
-                    $ns.'schedule-send-freebusy' => [
565
-                        'abstract' => false,
566
-                        'aggregates' => [],
567
-                    ],
568
-                    // Privilege from an earlier scheduling draft, but still
569
-                    // used by some clients.
570
-                    $ns.'schedule-post-vevent' => [
571
-                        'abstract' => false,
572
-                        'aggregates' => [],
573
-                    ],
574
-                ],
575
-            ];
576
-        }
577
-        if ($node instanceof IInbox) {
578
-            $supportedPrivilegeSet[$ns.'schedule-deliver'] = [
579
-                'abstract' => false,
580
-                'aggregates' => [
581
-                    $ns.'schedule-deliver-invite' => [
582
-                        'abstract' => false,
583
-                        'aggregates' => [],
584
-                    ],
585
-                    $ns.'schedule-deliver-reply' => [
586
-                        'abstract' => false,
587
-                        'aggregates' => [],
588
-                    ],
589
-                    $ns.'schedule-query-freebusy' => [
590
-                        'abstract' => false,
591
-                        'aggregates' => [],
592
-                    ],
593
-                ],
594
-            ];
595
-        }
596
-    }
597
-
598
-    /**
599
-     * This method looks at an old iCalendar object, a new iCalendar object and
600
-     * starts sending scheduling messages based on the changes.
601
-     *
602
-     * A list of addresses needs to be specified, so the system knows who made
603
-     * the update, because the behavior may be different based on if it's an
604
-     * attendee or an organizer.
605
-     *
606
-     * This method may update $newObject to add any status changes.
607
-     *
608
-     * @param VCalendar|string|null $oldObject
609
-     * @param array                 $ignore    any addresses to not send messages to
610
-     * @param bool                  $modified  a marker to indicate that the original object modified by this process
611
-     */
612
-    protected function processICalendarChange($oldObject, VCalendar $newObject, array $addresses, array $ignore = [], &$modified = false)
613
-    {
614
-        $broker = new ITip\Broker();
615
-        $messages = $broker->parseEvent($newObject, $addresses, $oldObject);
616
-
617
-        if ($messages) {
618
-            $modified = true;
619
-        }
620
-
621
-        foreach ($messages as $message) {
622
-            if (in_array($message->recipient, $ignore)) {
623
-                continue;
624
-            }
625
-
626
-            $this->deliver($message);
627
-
628
-            if (isset($newObject->VEVENT->ORGANIZER) && ($newObject->VEVENT->ORGANIZER->getNormalizedValue() === $message->recipient)) {
629
-                if ($message->scheduleStatus) {
630
-                    $newObject->VEVENT->ORGANIZER['SCHEDULE-STATUS'] = $message->getScheduleStatus();
631
-                }
632
-                unset($newObject->VEVENT->ORGANIZER['SCHEDULE-FORCE-SEND']);
633
-            } else {
634
-                if (isset($newObject->VEVENT->ATTENDEE)) {
635
-                    foreach ($newObject->VEVENT->ATTENDEE as $attendee) {
636
-                        if ($attendee->getNormalizedValue() === $message->recipient) {
637
-                            if ($message->scheduleStatus) {
638
-                                $attendee['SCHEDULE-STATUS'] = $message->getScheduleStatus();
639
-                            }
640
-                            unset($attendee['SCHEDULE-FORCE-SEND']);
641
-                            break;
642
-                        }
643
-                    }
644
-                }
645
-            }
646
-        }
647
-    }
648
-
649
-    /**
650
-     * Returns a list of addresses that are associated with a principal.
651
-     *
652
-     * @param string $principal
653
-     *
654
-     * @return array
655
-     */
656
-    protected function getAddressesForPrincipal($principal)
657
-    {
658
-        $CUAS = '{'.self::NS_CALDAV.'}calendar-user-address-set';
659
-
660
-        $properties = $this->server->getProperties(
661
-            $principal,
662
-            [$CUAS]
663
-        );
664
-
665
-        // If we can't find this information, we'll stop processing
666
-        if (!isset($properties[$CUAS])) {
667
-            return [];
668
-        }
669
-
670
-        $addresses = $properties[$CUAS]->getHrefs();
671
-
672
-        return $addresses;
673
-    }
674
-
675
-    /**
676
-     * This method handles POST requests to the schedule-outbox.
677
-     *
678
-     * Currently, two types of requests are supported:
679
-     *   * FREEBUSY requests from RFC 6638
680
-     *   * Simple iTIP messages from draft-desruisseaux-caldav-sched-04
681
-     *
682
-     * The latter is from an expired early draft of the CalDAV scheduling
683
-     * extensions, but iCal depends on a feature from that spec, so we
684
-     * implement it.
685
-     */
686
-    public function outboxRequest(IOutbox $outboxNode, RequestInterface $request, ResponseInterface $response)
687
-    {
688
-        $outboxPath = $request->getPath();
689
-
690
-        // Parsing the request body
691
-        try {
692
-            $vObject = VObject\Reader::read($request->getBody());
693
-        } catch (VObject\ParseException $e) {
694
-            throw new BadRequest('The request body must be a valid iCalendar object. Parse error: '.$e->getMessage());
695
-        }
696
-
697
-        // The incoming iCalendar object must have a METHOD property, and a
698
-        // component. The combination of both determines what type of request
699
-        // this is.
700
-        $componentType = null;
701
-        foreach ($vObject->getComponents() as $component) {
702
-            if ('VTIMEZONE' !== $component->name) {
703
-                $componentType = $component->name;
704
-                break;
705
-            }
706
-        }
707
-        if (is_null($componentType)) {
708
-            throw new BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component');
709
-        }
710
-
711
-        // Validating the METHOD
712
-        $method = strtoupper((string) $vObject->METHOD);
713
-        if (!$method) {
714
-            throw new BadRequest('A METHOD property must be specified in iTIP messages');
715
-        }
716
-
717
-        // So we support one type of request:
718
-        //
719
-        // REQUEST with a VFREEBUSY component
720
-
721
-        $acl = $this->server->getPlugin('acl');
722
-
723
-        if ('VFREEBUSY' === $componentType && 'REQUEST' === $method) {
724
-            $acl && $acl->checkPrivileges($outboxPath, '{'.self::NS_CALDAV.'}schedule-send-freebusy');
725
-            $this->handleFreeBusyRequest($outboxNode, $vObject, $request, $response);
726
-
727
-            // Destroy circular references so PHP can GC the object.
728
-            $vObject->destroy();
729
-            unset($vObject);
730
-        } else {
731
-            throw new NotImplemented('We only support VFREEBUSY (REQUEST) on this endpoint');
732
-        }
733
-    }
734
-
735
-    /**
736
-     * This method is responsible for parsing a free-busy query request and
737
-     * returning its result in $response.
738
-     */
739
-    protected function handleFreeBusyRequest(IOutbox $outbox, VObject\Component $vObject, RequestInterface $request, ResponseInterface $response)
740
-    {
741
-        $vFreeBusy = $vObject->VFREEBUSY;
742
-        $organizer = $vFreeBusy->ORGANIZER;
743
-
744
-        $organizer = (string) $organizer;
745
-
746
-        // Validating if the organizer matches the owner of the inbox.
747
-        $owner = $outbox->getOwner();
748
-
749
-        $caldavNS = '{'.self::NS_CALDAV.'}';
750
-
751
-        $uas = $caldavNS.'calendar-user-address-set';
752
-        $props = $this->server->getProperties($owner, [$uas]);
753
-
754
-        if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) {
755
-            throw new Forbidden('The organizer in the request did not match any of the addresses for the owner of this inbox');
756
-        }
757
-
758
-        if (!isset($vFreeBusy->ATTENDEE)) {
759
-            throw new BadRequest('You must at least specify 1 attendee');
760
-        }
761
-
762
-        $attendees = [];
763
-        foreach ($vFreeBusy->ATTENDEE as $attendee) {
764
-            $attendees[] = (string) $attendee;
765
-        }
766
-
767
-        if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) {
768
-            throw new BadRequest('DTSTART and DTEND must both be specified');
769
-        }
770
-
771
-        $startRange = $vFreeBusy->DTSTART->getDateTime();
772
-        $endRange = $vFreeBusy->DTEND->getDateTime();
773
-
774
-        $results = [];
775
-        foreach ($attendees as $attendee) {
776
-            $results[] = $this->getFreeBusyForEmail($attendee, $startRange, $endRange, $vObject);
777
-        }
778
-
779
-        $dom = new \DOMDocument('1.0', 'utf-8');
780
-        $dom->formatOutput = true;
781
-        $scheduleResponse = $dom->createElement('cal:schedule-response');
782
-        foreach ($this->server->xml->namespaceMap as $namespace => $prefix) {
783
-            $scheduleResponse->setAttribute('xmlns:'.$prefix, $namespace);
784
-        }
785
-        $dom->appendChild($scheduleResponse);
786
-
787
-        foreach ($results as $result) {
788
-            $xresponse = $dom->createElement('cal:response');
789
-
790
-            $recipient = $dom->createElement('cal:recipient');
791
-            $recipientHref = $dom->createElement('d:href');
792
-
793
-            $recipientHref->appendChild($dom->createTextNode($result['href']));
794
-            $recipient->appendChild($recipientHref);
795
-            $xresponse->appendChild($recipient);
796
-
797
-            $reqStatus = $dom->createElement('cal:request-status');
798
-            $reqStatus->appendChild($dom->createTextNode($result['request-status']));
799
-            $xresponse->appendChild($reqStatus);
800
-
801
-            if (isset($result['calendar-data'])) {
802
-                $calendardata = $dom->createElement('cal:calendar-data');
803
-                $calendardata->appendChild($dom->createTextNode(str_replace("\r\n", "\n", $result['calendar-data']->serialize())));
804
-                $xresponse->appendChild($calendardata);
805
-            }
806
-            $scheduleResponse->appendChild($xresponse);
807
-        }
808
-
809
-        $response->setStatus(200);
810
-        $response->setHeader('Content-Type', 'application/xml');
811
-        $response->setBody($dom->saveXML());
812
-    }
813
-
814
-    /**
815
-     * Returns free-busy information for a specific address. The returned
816
-     * data is an array containing the following properties:.
817
-     *
818
-     * calendar-data : A VFREEBUSY VObject
819
-     * request-status : an iTip status code.
820
-     * href: The principal's email address, as requested
821
-     *
822
-     * The following request status codes may be returned:
823
-     *   * 2.0;description
824
-     *   * 3.7;description
825
-     *
826
-     * @param string $email address
827
-     *
828
-     * @return array
829
-     */
830
-    protected function getFreeBusyForEmail($email, \DateTimeInterface $start, \DateTimeInterface $end, VObject\Component $request)
831
-    {
832
-        $caldavNS = '{'.self::NS_CALDAV.'}';
833
-
834
-        $aclPlugin = $this->server->getPlugin('acl');
835
-        if ('mailto:' === substr($email, 0, 7)) {
836
-            $email = substr($email, 7);
837
-        }
838
-
839
-        $result = $aclPlugin->principalSearch(
840
-            ['{http://sabredav.org/ns}email-address' => $email],
841
-            [
842
-                '{DAV:}principal-URL',
843
-                $caldavNS.'calendar-home-set',
844
-                $caldavNS.'schedule-inbox-URL',
845
-                '{http://sabredav.org/ns}email-address',
846
-            ]
847
-        );
848
-
849
-        if (!count($result)) {
850
-            return [
851
-                'request-status' => '3.7;Could not find principal',
852
-                'href' => 'mailto:'.$email,
853
-            ];
854
-        }
855
-
856
-        if (!isset($result[0][200][$caldavNS.'calendar-home-set'])) {
857
-            return [
858
-                'request-status' => '3.7;No calendar-home-set property found',
859
-                'href' => 'mailto:'.$email,
860
-            ];
861
-        }
862
-        if (!isset($result[0][200][$caldavNS.'schedule-inbox-URL'])) {
863
-            return [
864
-                'request-status' => '3.7;No schedule-inbox-URL property found',
865
-                'href' => 'mailto:'.$email,
866
-            ];
867
-        }
868
-        $homeSet = $result[0][200][$caldavNS.'calendar-home-set']->getHref();
869
-        $inboxUrl = $result[0][200][$caldavNS.'schedule-inbox-URL']->getHref();
870
-
871
-        // Do we have permission?
872
-        $aclPlugin->checkPrivileges($inboxUrl, $caldavNS.'schedule-query-freebusy');
873
-
874
-        // Grabbing the calendar list
875
-        $objects = [];
876
-        $calendarTimeZone = new DateTimeZone('UTC');
877
-
878
-        foreach ($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) {
879
-            if (!$node instanceof ICalendar) {
880
-                continue;
881
-            }
882
-
883
-            $sct = $caldavNS.'schedule-calendar-transp';
884
-            $ctz = $caldavNS.'calendar-timezone';
885
-            $props = $node->getProperties([$sct, $ctz]);
886
-
887
-            if (isset($props[$sct]) && ScheduleCalendarTransp::TRANSPARENT == $props[$sct]->getValue()) {
888
-                // If a calendar is marked as 'transparent', it means we must
889
-                // ignore it for free-busy purposes.
890
-                continue;
891
-            }
892
-
893
-            if (isset($props[$ctz])) {
894
-                $vtimezoneObj = VObject\Reader::read($props[$ctz]);
895
-                $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
896
-
897
-                // Destroy circular references so PHP can garbage collect the object.
898
-                $vtimezoneObj->destroy();
899
-            }
900
-
901
-            // Getting the list of object uris within the time-range
902
-            $urls = $node->calendarQuery([
903
-                'name' => 'VCALENDAR',
904
-                'comp-filters' => [
905
-                    [
906
-                        'name' => 'VEVENT',
907
-                        'comp-filters' => [],
908
-                        'prop-filters' => [],
909
-                        'is-not-defined' => false,
910
-                        'time-range' => [
911
-                            'start' => $start,
912
-                            'end' => $end,
913
-                        ],
914
-                    ],
915
-                ],
916
-                'prop-filters' => [],
917
-                'is-not-defined' => false,
918
-                'time-range' => null,
919
-            ]);
920
-
921
-            $calObjects = array_map(function ($url) use ($node) {
922
-                $obj = $node->getChild($url)->get();
923
-
924
-                return $obj;
925
-            }, $urls);
926
-
927
-            $objects = array_merge($objects, $calObjects);
928
-        }
929
-
930
-        $inboxProps = $this->server->getProperties(
931
-            $inboxUrl,
932
-            $caldavNS.'calendar-availability'
933
-        );
934
-
935
-        $vcalendar = new VObject\Component\VCalendar();
936
-        $vcalendar->METHOD = 'REPLY';
937
-
938
-        $generator = new VObject\FreeBusyGenerator();
939
-        $generator->setObjects($objects);
940
-        $generator->setTimeRange($start, $end);
941
-        $generator->setBaseObject($vcalendar);
942
-        $generator->setTimeZone($calendarTimeZone);
943
-
944
-        if ($inboxProps) {
945
-            $generator->setVAvailability(
946
-                VObject\Reader::read(
947
-                    $inboxProps[$caldavNS.'calendar-availability']
948
-                )
949
-            );
950
-        }
951
-
952
-        $result = $generator->getResult();
953
-
954
-        $vcalendar->VFREEBUSY->ATTENDEE = 'mailto:'.$email;
955
-        $vcalendar->VFREEBUSY->UID = (string) $request->VFREEBUSY->UID;
956
-        $vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER;
957
-
958
-        return [
959
-            'calendar-data' => $result,
960
-            'request-status' => '2.0;Success',
961
-            'href' => 'mailto:'.$email,
962
-        ];
963
-    }
964
-
965
-    /**
966
-     * This method checks the 'Schedule-Reply' header
967
-     * and returns false if it's 'F', otherwise true.
968
-     *
969
-     * @return bool
970
-     */
971
-    protected function scheduleReply(RequestInterface $request)
972
-    {
973
-        $scheduleReply = $request->getHeader('Schedule-Reply');
974
-
975
-        return 'F' !== $scheduleReply;
976
-    }
977
-
978
-    /**
979
-     * Returns a bunch of meta-data about the plugin.
980
-     *
981
-     * Providing this information is optional, and is mainly displayed by the
982
-     * Browser plugin.
983
-     *
984
-     * The description key in the returned array may contain html and will not
985
-     * be sanitized.
986
-     *
987
-     * @return array
988
-     */
989
-    public function getPluginInfo()
990
-    {
991
-        return [
992
-            'name' => $this->getPluginName(),
993
-            'description' => 'Adds calendar-auto-schedule, as defined in rfc6638',
994
-            'link' => 'http://sabre.io/dav/scheduling/',
995
-        ];
996
-    }
121
+		array_push($server->protectedProperties,
122
+			$ns.'schedule-inbox-URL',
123
+			$ns.'schedule-outbox-URL',
124
+			$ns.'calendar-user-address-set',
125
+			$ns.'calendar-user-type',
126
+			$ns.'schedule-default-calendar-URL'
127
+		);
128
+	}
129
+
130
+	/**
131
+	 * Use this method to tell the server this plugin defines additional
132
+	 * HTTP methods.
133
+	 *
134
+	 * This method is passed a uri. It should only return HTTP methods that are
135
+	 * available for the specified uri.
136
+	 *
137
+	 * @param string $uri
138
+	 *
139
+	 * @return array
140
+	 */
141
+	public function getHTTPMethods($uri)
142
+	{
143
+		try {
144
+			$node = $this->server->tree->getNodeForPath($uri);
145
+		} catch (NotFound $e) {
146
+			return [];
147
+		}
148
+
149
+		if ($node instanceof IOutbox) {
150
+			return ['POST'];
151
+		}
152
+
153
+		return [];
154
+	}
155
+
156
+	/**
157
+	 * This method handles POST request for the outbox.
158
+	 *
159
+	 * @return bool
160
+	 */
161
+	public function httpPost(RequestInterface $request, ResponseInterface $response)
162
+	{
163
+		// Checking if this is a text/calendar content type
164
+		$contentType = $request->getHeader('Content-Type');
165
+		if (!$contentType || 0 !== strpos($contentType, 'text/calendar')) {
166
+			return;
167
+		}
168
+
169
+		$path = $request->getPath();
170
+
171
+		// Checking if we're talking to an outbox
172
+		try {
173
+			$node = $this->server->tree->getNodeForPath($path);
174
+		} catch (NotFound $e) {
175
+			return;
176
+		}
177
+		if (!$node instanceof IOutbox) {
178
+			return;
179
+		}
180
+
181
+		$this->server->transactionType = 'post-caldav-outbox';
182
+		$this->outboxRequest($node, $request, $response);
183
+
184
+		// Returning false breaks the event chain and tells the server we've
185
+		// handled the request.
186
+		return false;
187
+	}
188
+
189
+	/**
190
+	 * This method handler is invoked during fetching of properties.
191
+	 *
192
+	 * We use this event to add calendar-auto-schedule-specific properties.
193
+	 */
194
+	public function propFind(PropFind $propFind, INode $node)
195
+	{
196
+		if ($node instanceof DAVACL\IPrincipal) {
197
+			$caldavPlugin = $this->server->getPlugin('caldav');
198
+			$principalUrl = $node->getPrincipalUrl();
199
+
200
+			// schedule-outbox-URL property
201
+			$propFind->handle('{'.self::NS_CALDAV.'}schedule-outbox-URL', function () use ($principalUrl, $caldavPlugin) {
202
+				$calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
203
+				if (!$calendarHomePath) {
204
+					return null;
205
+				}
206
+				$outboxPath = $calendarHomePath.'/outbox/';
207
+
208
+				return new LocalHref($outboxPath);
209
+			});
210
+			// schedule-inbox-URL property
211
+			$propFind->handle('{'.self::NS_CALDAV.'}schedule-inbox-URL', function () use ($principalUrl, $caldavPlugin) {
212
+				$calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
213
+				if (!$calendarHomePath) {
214
+					return null;
215
+				}
216
+				$inboxPath = $calendarHomePath.'/inbox/';
217
+
218
+				return new LocalHref($inboxPath);
219
+			});
220
+
221
+			$propFind->handle('{'.self::NS_CALDAV.'}schedule-default-calendar-URL', function () use ($principalUrl, $caldavPlugin) {
222
+				// We don't support customizing this property yet, so in the
223
+				// meantime we just grab the first calendar in the home-set.
224
+				$calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
225
+
226
+				if (!$calendarHomePath) {
227
+					return null;
228
+				}
229
+
230
+				$sccs = '{'.self::NS_CALDAV.'}supported-calendar-component-set';
231
+
232
+				$result = $this->server->getPropertiesForPath($calendarHomePath, [
233
+					'{DAV:}resourcetype',
234
+					'{DAV:}share-access',
235
+					$sccs,
236
+				], 1);
237
+
238
+				foreach ($result as $child) {
239
+					if (!isset($child[200]['{DAV:}resourcetype']) || !$child[200]['{DAV:}resourcetype']->is('{'.self::NS_CALDAV.'}calendar')) {
240
+						// Node is either not a calendar
241
+						continue;
242
+					}
243
+					if (isset($child[200]['{DAV:}share-access'])) {
244
+						$shareAccess = $child[200]['{DAV:}share-access']->getValue();
245
+						if (Sharing\Plugin::ACCESS_NOTSHARED !== $shareAccess && Sharing\Plugin::ACCESS_SHAREDOWNER !== $shareAccess) {
246
+							// Node is a shared node, not owned by the relevant
247
+							// user.
248
+							continue;
249
+						}
250
+					}
251
+					if (!isset($child[200][$sccs]) || in_array('VEVENT', $child[200][$sccs]->getValue())) {
252
+						// Either there is no supported-calendar-component-set
253
+						// (which is fine) or we found one that supports VEVENT.
254
+						return new LocalHref($child['href']);
255
+					}
256
+				}
257
+			});
258
+
259
+			// The server currently reports every principal to be of type
260
+			// 'INDIVIDUAL'
261
+			$propFind->handle('{'.self::NS_CALDAV.'}calendar-user-type', function () {
262
+				return 'INDIVIDUAL';
263
+			});
264
+		}
265
+
266
+		// Mapping the old property to the new property.
267
+		$propFind->handle('{http://calendarserver.org/ns/}calendar-availability', function () use ($propFind, $node) {
268
+			// In case it wasn't clear, the only difference is that we map the
269
+			// old property to a different namespace.
270
+			$availProp = '{'.self::NS_CALDAV.'}calendar-availability';
271
+			$subPropFind = new PropFind(
272
+				 $propFind->getPath(),
273
+				 [$availProp]
274
+			 );
275
+
276
+			$this->server->getPropertiesByNode(
277
+				 $subPropFind,
278
+				 $node
279
+			 );
280
+
281
+			$propFind->set(
282
+				 '{http://calendarserver.org/ns/}calendar-availability',
283
+				 $subPropFind->get($availProp),
284
+				 $subPropFind->getStatus($availProp)
285
+			 );
286
+		});
287
+	}
288
+
289
+	/**
290
+	 * This method is called during property updates.
291
+	 *
292
+	 * @param string $path
293
+	 */
294
+	public function propPatch($path, PropPatch $propPatch)
295
+	{
296
+		// Mapping the old property to the new property.
297
+		$propPatch->handle('{http://calendarserver.org/ns/}calendar-availability', function ($value) use ($path) {
298
+			$availProp = '{'.self::NS_CALDAV.'}calendar-availability';
299
+			$subPropPatch = new PropPatch([$availProp => $value]);
300
+			$this->server->emit('propPatch', [$path, $subPropPatch]);
301
+			$subPropPatch->commit();
302
+
303
+			return $subPropPatch->getResult()[$availProp];
304
+		});
305
+	}
306
+
307
+	/**
308
+	 * This method is triggered whenever there was a calendar object gets
309
+	 * created or updated.
310
+	 *
311
+	 * @param RequestInterface  $request      HTTP request
312
+	 * @param ResponseInterface $response     HTTP Response
313
+	 * @param VCalendar         $vCal         Parsed iCalendar object
314
+	 * @param mixed             $calendarPath Path to calendar collection
315
+	 * @param mixed             $modified     the iCalendar object has been touched
316
+	 * @param mixed             $isNew        Whether this was a new item or we're updating one
317
+	 */
318
+	public function calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew)
319
+	{
320
+		if (!$this->scheduleReply($this->server->httpRequest)) {
321
+			return;
322
+		}
323
+
324
+		$calendarNode = $this->server->tree->getNodeForPath($calendarPath);
325
+
326
+		$addresses = $this->getAddressesForPrincipal(
327
+			$calendarNode->getOwner()
328
+		);
329
+
330
+		if (!$isNew) {
331
+			$node = $this->server->tree->getNodeForPath($request->getPath());
332
+			$oldObj = Reader::read($node->get());
333
+		} else {
334
+			$oldObj = null;
335
+		}
336
+
337
+		$this->processICalendarChange($oldObj, $vCal, $addresses, [], $modified);
338
+
339
+		if ($oldObj) {
340
+			// Destroy circular references so PHP will GC the object.
341
+			$oldObj->destroy();
342
+		}
343
+	}
344
+
345
+	/**
346
+	 * This method is responsible for delivering the ITip message.
347
+	 */
348
+	public function deliver(ITip\Message $iTipMessage)
349
+	{
350
+		$this->server->emit('schedule', [$iTipMessage]);
351
+		if (!$iTipMessage->scheduleStatus) {
352
+			$iTipMessage->scheduleStatus = '5.2;There was no system capable of delivering the scheduling message';
353
+		}
354
+		// In case the change was considered 'insignificant', we are going to
355
+		// remove any error statuses, if any. See ticket #525.
356
+		list($baseCode) = explode('.', $iTipMessage->scheduleStatus);
357
+		if (!$iTipMessage->significantChange && in_array($baseCode, ['3', '5'])) {
358
+			$iTipMessage->scheduleStatus = null;
359
+		}
360
+	}
361
+
362
+	/**
363
+	 * This method is triggered before a file gets deleted.
364
+	 *
365
+	 * We use this event to make sure that when this happens, attendees get
366
+	 * cancellations, and organizers get 'DECLINED' statuses.
367
+	 *
368
+	 * @param string $path
369
+	 */
370
+	public function beforeUnbind($path)
371
+	{
372
+		// FIXME: We shouldn't trigger this functionality when we're issuing a
373
+		// MOVE. This is a hack.
374
+		if ('MOVE' === $this->server->httpRequest->getMethod()) {
375
+			return;
376
+		}
377
+
378
+		$node = $this->server->tree->getNodeForPath($path);
379
+
380
+		if (!$node instanceof ICalendarObject || $node instanceof ISchedulingObject) {
381
+			return;
382
+		}
383
+
384
+		if (!$this->scheduleReply($this->server->httpRequest)) {
385
+			return;
386
+		}
387
+
388
+		$addresses = $this->getAddressesForPrincipal(
389
+			$node->getOwner()
390
+		);
391
+
392
+		$broker = new ITip\Broker();
393
+		$messages = $broker->parseEvent(null, $addresses, $node->get());
394
+
395
+		foreach ($messages as $message) {
396
+			$this->deliver($message);
397
+		}
398
+	}
399
+
400
+	/**
401
+	 * Event handler for the 'schedule' event.
402
+	 *
403
+	 * This handler attempts to look at local accounts to deliver the
404
+	 * scheduling object.
405
+	 */
406
+	public function scheduleLocalDelivery(ITip\Message $iTipMessage)
407
+	{
408
+		$aclPlugin = $this->server->getPlugin('acl');
409
+
410
+		// Local delivery is not available if the ACL plugin is not loaded.
411
+		if (!$aclPlugin) {
412
+			return;
413
+		}
414
+
415
+		$caldavNS = '{'.self::NS_CALDAV.'}';
416
+
417
+		$principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient);
418
+		if (!$principalUri) {
419
+			$iTipMessage->scheduleStatus = '3.7;Could not find principal.';
420
+
421
+			return;
422
+		}
423
+
424
+		// We found a principal URL, now we need to find its inbox.
425
+		// Unfortunately we may not have sufficient privileges to find this, so
426
+		// we are temporarily turning off ACL to let this come through.
427
+		//
428
+		// Once we support PHP 5.5, this should be wrapped in a try..finally
429
+		// block so we can ensure that this privilege gets added again after.
430
+		$this->server->removeListener('propFind', [$aclPlugin, 'propFind']);
431
+
432
+		$result = $this->server->getProperties(
433
+			$principalUri,
434
+			[
435
+				'{DAV:}principal-URL',
436
+				 $caldavNS.'calendar-home-set',
437
+				 $caldavNS.'schedule-inbox-URL',
438
+				 $caldavNS.'schedule-default-calendar-URL',
439
+				'{http://sabredav.org/ns}email-address',
440
+			]
441
+		);
442
+
443
+		// Re-registering the ACL event
444
+		$this->server->on('propFind', [$aclPlugin, 'propFind'], 20);
445
+
446
+		if (!isset($result[$caldavNS.'schedule-inbox-URL'])) {
447
+			$iTipMessage->scheduleStatus = '5.2;Could not find local inbox';
448
+
449
+			return;
450
+		}
451
+		if (!isset($result[$caldavNS.'calendar-home-set'])) {
452
+			$iTipMessage->scheduleStatus = '5.2;Could not locate a calendar-home-set';
453
+
454
+			return;
455
+		}
456
+		if (!isset($result[$caldavNS.'schedule-default-calendar-URL'])) {
457
+			$iTipMessage->scheduleStatus = '5.2;Could not find a schedule-default-calendar-URL property';
458
+
459
+			return;
460
+		}
461
+
462
+		$calendarPath = $result[$caldavNS.'schedule-default-calendar-URL']->getHref();
463
+		$homePath = $result[$caldavNS.'calendar-home-set']->getHref();
464
+		$inboxPath = $result[$caldavNS.'schedule-inbox-URL']->getHref();
465
+
466
+		if ('REPLY' === $iTipMessage->method) {
467
+			$privilege = 'schedule-deliver-reply';
468
+		} else {
469
+			$privilege = 'schedule-deliver-invite';
470
+		}
471
+
472
+		if (!$aclPlugin->checkPrivileges($inboxPath, $caldavNS.$privilege, DAVACL\Plugin::R_PARENT, false)) {
473
+			$iTipMessage->scheduleStatus = '3.8;insufficient privileges: '.$privilege.' is required on the recipient schedule inbox.';
474
+
475
+			return;
476
+		}
477
+
478
+		// Next, we're going to find out if the item already exits in one of
479
+		// the users' calendars.
480
+		$uid = $iTipMessage->uid;
481
+
482
+		$newFileName = 'sabredav-'.\Sabre\DAV\UUIDUtil::getUUID().'.ics';
483
+
484
+		$home = $this->server->tree->getNodeForPath($homePath);
485
+		$inbox = $this->server->tree->getNodeForPath($inboxPath);
486
+
487
+		$currentObject = null;
488
+		$objectNode = null;
489
+		$oldICalendarData = null;
490
+		$isNewNode = false;
491
+
492
+		$result = $home->getCalendarObjectByUID($uid);
493
+		if ($result) {
494
+			// There was an existing object, we need to update probably.
495
+			$objectPath = $homePath.'/'.$result;
496
+			$objectNode = $this->server->tree->getNodeForPath($objectPath);
497
+			$oldICalendarData = $objectNode->get();
498
+			$currentObject = Reader::read($oldICalendarData);
499
+		} else {
500
+			$isNewNode = true;
501
+		}
502
+
503
+		$broker = new ITip\Broker();
504
+		$newObject = $broker->processMessage($iTipMessage, $currentObject);
505
+
506
+		$inbox->createFile($newFileName, $iTipMessage->message->serialize());
507
+
508
+		if (!$newObject) {
509
+			// We received an iTip message referring to a UID that we don't
510
+			// have in any calendars yet, and processMessage did not give us a
511
+			// calendarobject back.
512
+			//
513
+			// The implication is that processMessage did not understand the
514
+			// iTip message.
515
+			$iTipMessage->scheduleStatus = '5.0;iTip message was not processed by the server, likely because we didn\'t understand it.';
516
+
517
+			return;
518
+		}
519
+
520
+		// Note that we are bypassing ACL on purpose by calling this directly.
521
+		// We may need to look a bit deeper into this later. Supporting ACL
522
+		// here would be nice.
523
+		if ($isNewNode) {
524
+			$calendar = $this->server->tree->getNodeForPath($calendarPath);
525
+			$calendar->createFile($newFileName, $newObject->serialize());
526
+		} else {
527
+			// If the message was a reply, we may have to inform other
528
+			// attendees of this attendees status. Therefore we're shooting off
529
+			// another itipMessage.
530
+			if ('REPLY' === $iTipMessage->method) {
531
+				$this->processICalendarChange(
532
+					$oldICalendarData,
533
+					$newObject,
534
+					[$iTipMessage->recipient],
535
+					[$iTipMessage->sender]
536
+				);
537
+			}
538
+			$objectNode->put($newObject->serialize());
539
+		}
540
+		$iTipMessage->scheduleStatus = '1.2;Message delivered locally';
541
+	}
542
+
543
+	/**
544
+	 * This method is triggered whenever a subsystem requests the privileges
545
+	 * that are supported on a particular node.
546
+	 *
547
+	 * We need to add a number of privileges for scheduling purposes.
548
+	 */
549
+	public function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet)
550
+	{
551
+		$ns = '{'.self::NS_CALDAV.'}';
552
+		if ($node instanceof IOutbox) {
553
+			$supportedPrivilegeSet[$ns.'schedule-send'] = [
554
+				'abstract' => false,
555
+				'aggregates' => [
556
+					$ns.'schedule-send-invite' => [
557
+						'abstract' => false,
558
+						'aggregates' => [],
559
+					],
560
+					$ns.'schedule-send-reply' => [
561
+						'abstract' => false,
562
+						'aggregates' => [],
563
+					],
564
+					$ns.'schedule-send-freebusy' => [
565
+						'abstract' => false,
566
+						'aggregates' => [],
567
+					],
568
+					// Privilege from an earlier scheduling draft, but still
569
+					// used by some clients.
570
+					$ns.'schedule-post-vevent' => [
571
+						'abstract' => false,
572
+						'aggregates' => [],
573
+					],
574
+				],
575
+			];
576
+		}
577
+		if ($node instanceof IInbox) {
578
+			$supportedPrivilegeSet[$ns.'schedule-deliver'] = [
579
+				'abstract' => false,
580
+				'aggregates' => [
581
+					$ns.'schedule-deliver-invite' => [
582
+						'abstract' => false,
583
+						'aggregates' => [],
584
+					],
585
+					$ns.'schedule-deliver-reply' => [
586
+						'abstract' => false,
587
+						'aggregates' => [],
588
+					],
589
+					$ns.'schedule-query-freebusy' => [
590
+						'abstract' => false,
591
+						'aggregates' => [],
592
+					],
593
+				],
594
+			];
595
+		}
596
+	}
597
+
598
+	/**
599
+	 * This method looks at an old iCalendar object, a new iCalendar object and
600
+	 * starts sending scheduling messages based on the changes.
601
+	 *
602
+	 * A list of addresses needs to be specified, so the system knows who made
603
+	 * the update, because the behavior may be different based on if it's an
604
+	 * attendee or an organizer.
605
+	 *
606
+	 * This method may update $newObject to add any status changes.
607
+	 *
608
+	 * @param VCalendar|string|null $oldObject
609
+	 * @param array                 $ignore    any addresses to not send messages to
610
+	 * @param bool                  $modified  a marker to indicate that the original object modified by this process
611
+	 */
612
+	protected function processICalendarChange($oldObject, VCalendar $newObject, array $addresses, array $ignore = [], &$modified = false)
613
+	{
614
+		$broker = new ITip\Broker();
615
+		$messages = $broker->parseEvent($newObject, $addresses, $oldObject);
616
+
617
+		if ($messages) {
618
+			$modified = true;
619
+		}
620
+
621
+		foreach ($messages as $message) {
622
+			if (in_array($message->recipient, $ignore)) {
623
+				continue;
624
+			}
625
+
626
+			$this->deliver($message);
627
+
628
+			if (isset($newObject->VEVENT->ORGANIZER) && ($newObject->VEVENT->ORGANIZER->getNormalizedValue() === $message->recipient)) {
629
+				if ($message->scheduleStatus) {
630
+					$newObject->VEVENT->ORGANIZER['SCHEDULE-STATUS'] = $message->getScheduleStatus();
631
+				}
632
+				unset($newObject->VEVENT->ORGANIZER['SCHEDULE-FORCE-SEND']);
633
+			} else {
634
+				if (isset($newObject->VEVENT->ATTENDEE)) {
635
+					foreach ($newObject->VEVENT->ATTENDEE as $attendee) {
636
+						if ($attendee->getNormalizedValue() === $message->recipient) {
637
+							if ($message->scheduleStatus) {
638
+								$attendee['SCHEDULE-STATUS'] = $message->getScheduleStatus();
639
+							}
640
+							unset($attendee['SCHEDULE-FORCE-SEND']);
641
+							break;
642
+						}
643
+					}
644
+				}
645
+			}
646
+		}
647
+	}
648
+
649
+	/**
650
+	 * Returns a list of addresses that are associated with a principal.
651
+	 *
652
+	 * @param string $principal
653
+	 *
654
+	 * @return array
655
+	 */
656
+	protected function getAddressesForPrincipal($principal)
657
+	{
658
+		$CUAS = '{'.self::NS_CALDAV.'}calendar-user-address-set';
659
+
660
+		$properties = $this->server->getProperties(
661
+			$principal,
662
+			[$CUAS]
663
+		);
664
+
665
+		// If we can't find this information, we'll stop processing
666
+		if (!isset($properties[$CUAS])) {
667
+			return [];
668
+		}
669
+
670
+		$addresses = $properties[$CUAS]->getHrefs();
671
+
672
+		return $addresses;
673
+	}
674
+
675
+	/**
676
+	 * This method handles POST requests to the schedule-outbox.
677
+	 *
678
+	 * Currently, two types of requests are supported:
679
+	 *   * FREEBUSY requests from RFC 6638
680
+	 *   * Simple iTIP messages from draft-desruisseaux-caldav-sched-04
681
+	 *
682
+	 * The latter is from an expired early draft of the CalDAV scheduling
683
+	 * extensions, but iCal depends on a feature from that spec, so we
684
+	 * implement it.
685
+	 */
686
+	public function outboxRequest(IOutbox $outboxNode, RequestInterface $request, ResponseInterface $response)
687
+	{
688
+		$outboxPath = $request->getPath();
689
+
690
+		// Parsing the request body
691
+		try {
692
+			$vObject = VObject\Reader::read($request->getBody());
693
+		} catch (VObject\ParseException $e) {
694
+			throw new BadRequest('The request body must be a valid iCalendar object. Parse error: '.$e->getMessage());
695
+		}
696
+
697
+		// The incoming iCalendar object must have a METHOD property, and a
698
+		// component. The combination of both determines what type of request
699
+		// this is.
700
+		$componentType = null;
701
+		foreach ($vObject->getComponents() as $component) {
702
+			if ('VTIMEZONE' !== $component->name) {
703
+				$componentType = $component->name;
704
+				break;
705
+			}
706
+		}
707
+		if (is_null($componentType)) {
708
+			throw new BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component');
709
+		}
710
+
711
+		// Validating the METHOD
712
+		$method = strtoupper((string) $vObject->METHOD);
713
+		if (!$method) {
714
+			throw new BadRequest('A METHOD property must be specified in iTIP messages');
715
+		}
716
+
717
+		// So we support one type of request:
718
+		//
719
+		// REQUEST with a VFREEBUSY component
720
+
721
+		$acl = $this->server->getPlugin('acl');
722
+
723
+		if ('VFREEBUSY' === $componentType && 'REQUEST' === $method) {
724
+			$acl && $acl->checkPrivileges($outboxPath, '{'.self::NS_CALDAV.'}schedule-send-freebusy');
725
+			$this->handleFreeBusyRequest($outboxNode, $vObject, $request, $response);
726
+
727
+			// Destroy circular references so PHP can GC the object.
728
+			$vObject->destroy();
729
+			unset($vObject);
730
+		} else {
731
+			throw new NotImplemented('We only support VFREEBUSY (REQUEST) on this endpoint');
732
+		}
733
+	}
734
+
735
+	/**
736
+	 * This method is responsible for parsing a free-busy query request and
737
+	 * returning its result in $response.
738
+	 */
739
+	protected function handleFreeBusyRequest(IOutbox $outbox, VObject\Component $vObject, RequestInterface $request, ResponseInterface $response)
740
+	{
741
+		$vFreeBusy = $vObject->VFREEBUSY;
742
+		$organizer = $vFreeBusy->ORGANIZER;
743
+
744
+		$organizer = (string) $organizer;
745
+
746
+		// Validating if the organizer matches the owner of the inbox.
747
+		$owner = $outbox->getOwner();
748
+
749
+		$caldavNS = '{'.self::NS_CALDAV.'}';
750
+
751
+		$uas = $caldavNS.'calendar-user-address-set';
752
+		$props = $this->server->getProperties($owner, [$uas]);
753
+
754
+		if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) {
755
+			throw new Forbidden('The organizer in the request did not match any of the addresses for the owner of this inbox');
756
+		}
757
+
758
+		if (!isset($vFreeBusy->ATTENDEE)) {
759
+			throw new BadRequest('You must at least specify 1 attendee');
760
+		}
761
+
762
+		$attendees = [];
763
+		foreach ($vFreeBusy->ATTENDEE as $attendee) {
764
+			$attendees[] = (string) $attendee;
765
+		}
766
+
767
+		if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) {
768
+			throw new BadRequest('DTSTART and DTEND must both be specified');
769
+		}
770
+
771
+		$startRange = $vFreeBusy->DTSTART->getDateTime();
772
+		$endRange = $vFreeBusy->DTEND->getDateTime();
773
+
774
+		$results = [];
775
+		foreach ($attendees as $attendee) {
776
+			$results[] = $this->getFreeBusyForEmail($attendee, $startRange, $endRange, $vObject);
777
+		}
778
+
779
+		$dom = new \DOMDocument('1.0', 'utf-8');
780
+		$dom->formatOutput = true;
781
+		$scheduleResponse = $dom->createElement('cal:schedule-response');
782
+		foreach ($this->server->xml->namespaceMap as $namespace => $prefix) {
783
+			$scheduleResponse->setAttribute('xmlns:'.$prefix, $namespace);
784
+		}
785
+		$dom->appendChild($scheduleResponse);
786
+
787
+		foreach ($results as $result) {
788
+			$xresponse = $dom->createElement('cal:response');
789
+
790
+			$recipient = $dom->createElement('cal:recipient');
791
+			$recipientHref = $dom->createElement('d:href');
792
+
793
+			$recipientHref->appendChild($dom->createTextNode($result['href']));
794
+			$recipient->appendChild($recipientHref);
795
+			$xresponse->appendChild($recipient);
796
+
797
+			$reqStatus = $dom->createElement('cal:request-status');
798
+			$reqStatus->appendChild($dom->createTextNode($result['request-status']));
799
+			$xresponse->appendChild($reqStatus);
800
+
801
+			if (isset($result['calendar-data'])) {
802
+				$calendardata = $dom->createElement('cal:calendar-data');
803
+				$calendardata->appendChild($dom->createTextNode(str_replace("\r\n", "\n", $result['calendar-data']->serialize())));
804
+				$xresponse->appendChild($calendardata);
805
+			}
806
+			$scheduleResponse->appendChild($xresponse);
807
+		}
808
+
809
+		$response->setStatus(200);
810
+		$response->setHeader('Content-Type', 'application/xml');
811
+		$response->setBody($dom->saveXML());
812
+	}
813
+
814
+	/**
815
+	 * Returns free-busy information for a specific address. The returned
816
+	 * data is an array containing the following properties:.
817
+	 *
818
+	 * calendar-data : A VFREEBUSY VObject
819
+	 * request-status : an iTip status code.
820
+	 * href: The principal's email address, as requested
821
+	 *
822
+	 * The following request status codes may be returned:
823
+	 *   * 2.0;description
824
+	 *   * 3.7;description
825
+	 *
826
+	 * @param string $email address
827
+	 *
828
+	 * @return array
829
+	 */
830
+	protected function getFreeBusyForEmail($email, \DateTimeInterface $start, \DateTimeInterface $end, VObject\Component $request)
831
+	{
832
+		$caldavNS = '{'.self::NS_CALDAV.'}';
833
+
834
+		$aclPlugin = $this->server->getPlugin('acl');
835
+		if ('mailto:' === substr($email, 0, 7)) {
836
+			$email = substr($email, 7);
837
+		}
838
+
839
+		$result = $aclPlugin->principalSearch(
840
+			['{http://sabredav.org/ns}email-address' => $email],
841
+			[
842
+				'{DAV:}principal-URL',
843
+				$caldavNS.'calendar-home-set',
844
+				$caldavNS.'schedule-inbox-URL',
845
+				'{http://sabredav.org/ns}email-address',
846
+			]
847
+		);
848
+
849
+		if (!count($result)) {
850
+			return [
851
+				'request-status' => '3.7;Could not find principal',
852
+				'href' => 'mailto:'.$email,
853
+			];
854
+		}
855
+
856
+		if (!isset($result[0][200][$caldavNS.'calendar-home-set'])) {
857
+			return [
858
+				'request-status' => '3.7;No calendar-home-set property found',
859
+				'href' => 'mailto:'.$email,
860
+			];
861
+		}
862
+		if (!isset($result[0][200][$caldavNS.'schedule-inbox-URL'])) {
863
+			return [
864
+				'request-status' => '3.7;No schedule-inbox-URL property found',
865
+				'href' => 'mailto:'.$email,
866
+			];
867
+		}
868
+		$homeSet = $result[0][200][$caldavNS.'calendar-home-set']->getHref();
869
+		$inboxUrl = $result[0][200][$caldavNS.'schedule-inbox-URL']->getHref();
870
+
871
+		// Do we have permission?
872
+		$aclPlugin->checkPrivileges($inboxUrl, $caldavNS.'schedule-query-freebusy');
873
+
874
+		// Grabbing the calendar list
875
+		$objects = [];
876
+		$calendarTimeZone = new DateTimeZone('UTC');
877
+
878
+		foreach ($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) {
879
+			if (!$node instanceof ICalendar) {
880
+				continue;
881
+			}
882
+
883
+			$sct = $caldavNS.'schedule-calendar-transp';
884
+			$ctz = $caldavNS.'calendar-timezone';
885
+			$props = $node->getProperties([$sct, $ctz]);
886
+
887
+			if (isset($props[$sct]) && ScheduleCalendarTransp::TRANSPARENT == $props[$sct]->getValue()) {
888
+				// If a calendar is marked as 'transparent', it means we must
889
+				// ignore it for free-busy purposes.
890
+				continue;
891
+			}
892
+
893
+			if (isset($props[$ctz])) {
894
+				$vtimezoneObj = VObject\Reader::read($props[$ctz]);
895
+				$calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
896
+
897
+				// Destroy circular references so PHP can garbage collect the object.
898
+				$vtimezoneObj->destroy();
899
+			}
900
+
901
+			// Getting the list of object uris within the time-range
902
+			$urls = $node->calendarQuery([
903
+				'name' => 'VCALENDAR',
904
+				'comp-filters' => [
905
+					[
906
+						'name' => 'VEVENT',
907
+						'comp-filters' => [],
908
+						'prop-filters' => [],
909
+						'is-not-defined' => false,
910
+						'time-range' => [
911
+							'start' => $start,
912
+							'end' => $end,
913
+						],
914
+					],
915
+				],
916
+				'prop-filters' => [],
917
+				'is-not-defined' => false,
918
+				'time-range' => null,
919
+			]);
920
+
921
+			$calObjects = array_map(function ($url) use ($node) {
922
+				$obj = $node->getChild($url)->get();
923
+
924
+				return $obj;
925
+			}, $urls);
926
+
927
+			$objects = array_merge($objects, $calObjects);
928
+		}
929
+
930
+		$inboxProps = $this->server->getProperties(
931
+			$inboxUrl,
932
+			$caldavNS.'calendar-availability'
933
+		);
934
+
935
+		$vcalendar = new VObject\Component\VCalendar();
936
+		$vcalendar->METHOD = 'REPLY';
937
+
938
+		$generator = new VObject\FreeBusyGenerator();
939
+		$generator->setObjects($objects);
940
+		$generator->setTimeRange($start, $end);
941
+		$generator->setBaseObject($vcalendar);
942
+		$generator->setTimeZone($calendarTimeZone);
943
+
944
+		if ($inboxProps) {
945
+			$generator->setVAvailability(
946
+				VObject\Reader::read(
947
+					$inboxProps[$caldavNS.'calendar-availability']
948
+				)
949
+			);
950
+		}
951
+
952
+		$result = $generator->getResult();
953
+
954
+		$vcalendar->VFREEBUSY->ATTENDEE = 'mailto:'.$email;
955
+		$vcalendar->VFREEBUSY->UID = (string) $request->VFREEBUSY->UID;
956
+		$vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER;
957
+
958
+		return [
959
+			'calendar-data' => $result,
960
+			'request-status' => '2.0;Success',
961
+			'href' => 'mailto:'.$email,
962
+		];
963
+	}
964
+
965
+	/**
966
+	 * This method checks the 'Schedule-Reply' header
967
+	 * and returns false if it's 'F', otherwise true.
968
+	 *
969
+	 * @return bool
970
+	 */
971
+	protected function scheduleReply(RequestInterface $request)
972
+	{
973
+		$scheduleReply = $request->getHeader('Schedule-Reply');
974
+
975
+		return 'F' !== $scheduleReply;
976
+	}
977
+
978
+	/**
979
+	 * Returns a bunch of meta-data about the plugin.
980
+	 *
981
+	 * Providing this information is optional, and is mainly displayed by the
982
+	 * Browser plugin.
983
+	 *
984
+	 * The description key in the returned array may contain html and will not
985
+	 * be sanitized.
986
+	 *
987
+	 * @return array
988
+	 */
989
+	public function getPluginInfo()
990
+	{
991
+		return [
992
+			'name' => $this->getPluginName(),
993
+			'description' => 'Adds calendar-auto-schedule, as defined in rfc6638',
994
+			'link' => 'http://sabre.io/dav/scheduling/',
995
+		];
996
+	}
997 997
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/Inbox.php 1 patch
Indentation   +176 added lines, -176 removed lines patch added patch discarded remove patch
@@ -19,180 +19,180 @@
 block discarded – undo
19 19
  */
20 20
 class Inbox extends DAV\Collection implements IInbox
21 21
 {
22
-    use DAVACL\ACLTrait;
23
-
24
-    /**
25
-     * CalDAV backend.
26
-     *
27
-     * @var Backend\BackendInterface
28
-     */
29
-    protected $caldavBackend;
30
-
31
-    /**
32
-     * The principal Uri.
33
-     *
34
-     * @var string
35
-     */
36
-    protected $principalUri;
37
-
38
-    /**
39
-     * Constructor.
40
-     *
41
-     * @param string $principalUri
42
-     */
43
-    public function __construct(Backend\SchedulingSupport $caldavBackend, $principalUri)
44
-    {
45
-        $this->caldavBackend = $caldavBackend;
46
-        $this->principalUri = $principalUri;
47
-    }
48
-
49
-    /**
50
-     * Returns the name of the node.
51
-     *
52
-     * This is used to generate the url.
53
-     *
54
-     * @return string
55
-     */
56
-    public function getName()
57
-    {
58
-        return 'inbox';
59
-    }
60
-
61
-    /**
62
-     * Returns an array with all the child nodes.
63
-     *
64
-     * @return \Sabre\DAV\INode[]
65
-     */
66
-    public function getChildren()
67
-    {
68
-        $objs = $this->caldavBackend->getSchedulingObjects($this->principalUri);
69
-        $children = [];
70
-        foreach ($objs as $obj) {
71
-            //$obj['acl'] = $this->getACL();
72
-            $obj['principaluri'] = $this->principalUri;
73
-            $children[] = new SchedulingObject($this->caldavBackend, $obj);
74
-        }
75
-
76
-        return $children;
77
-    }
78
-
79
-    /**
80
-     * Creates a new file in the directory.
81
-     *
82
-     * Data will either be supplied as a stream resource, or in certain cases
83
-     * as a string. Keep in mind that you may have to support either.
84
-     *
85
-     * After successful creation of the file, you may choose to return the ETag
86
-     * of the new file here.
87
-     *
88
-     * The returned ETag must be surrounded by double-quotes (The quotes should
89
-     * be part of the actual string).
90
-     *
91
-     * If you cannot accurately determine the ETag, you should not return it.
92
-     * If you don't store the file exactly as-is (you're transforming it
93
-     * somehow) you should also not return an ETag.
94
-     *
95
-     * This means that if a subsequent GET to this new file does not exactly
96
-     * return the same contents of what was submitted here, you are strongly
97
-     * recommended to omit the ETag.
98
-     *
99
-     * @param string          $name Name of the file
100
-     * @param resource|string $data Initial payload
101
-     *
102
-     * @return string|null
103
-     */
104
-    public function createFile($name, $data = null)
105
-    {
106
-        $this->caldavBackend->createSchedulingObject($this->principalUri, $name, $data);
107
-    }
108
-
109
-    /**
110
-     * Returns the owner principal.
111
-     *
112
-     * This must be a url to a principal, or null if there's no owner
113
-     *
114
-     * @return string|null
115
-     */
116
-    public function getOwner()
117
-    {
118
-        return $this->principalUri;
119
-    }
120
-
121
-    /**
122
-     * Returns a list of ACE's for this node.
123
-     *
124
-     * Each ACE has the following properties:
125
-     *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
126
-     *     currently the only supported privileges
127
-     *   * 'principal', a url to the principal who owns the node
128
-     *   * 'protected' (optional), indicating that this ACE is not allowed to
129
-     *      be updated.
130
-     *
131
-     * @return array
132
-     */
133
-    public function getACL()
134
-    {
135
-        return [
136
-            [
137
-                'privilege' => '{DAV:}read',
138
-                'principal' => '{DAV:}authenticated',
139
-                'protected' => true,
140
-            ],
141
-            [
142
-                'privilege' => '{DAV:}write-properties',
143
-                'principal' => $this->getOwner(),
144
-                'protected' => true,
145
-            ],
146
-            [
147
-                'privilege' => '{DAV:}unbind',
148
-                'principal' => $this->getOwner(),
149
-                'protected' => true,
150
-            ],
151
-            [
152
-                'privilege' => '{DAV:}unbind',
153
-                'principal' => $this->getOwner().'/calendar-proxy-write',
154
-                'protected' => true,
155
-            ],
156
-            [
157
-                'privilege' => '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-deliver',
158
-                'principal' => '{DAV:}authenticated',
159
-                'protected' => true,
160
-            ],
161
-        ];
162
-    }
163
-
164
-    /**
165
-     * Performs a calendar-query on the contents of this calendar.
166
-     *
167
-     * The calendar-query is defined in RFC4791 : CalDAV. Using the
168
-     * calendar-query it is possible for a client to request a specific set of
169
-     * object, based on contents of iCalendar properties, date-ranges and
170
-     * iCalendar component types (VTODO, VEVENT).
171
-     *
172
-     * This method should just return a list of (relative) urls that match this
173
-     * query.
174
-     *
175
-     * The list of filters are specified as an array. The exact array is
176
-     * documented by \Sabre\CalDAV\CalendarQueryParser.
177
-     *
178
-     * @return array
179
-     */
180
-    public function calendarQuery(array $filters)
181
-    {
182
-        $result = [];
183
-        $validator = new CalDAV\CalendarQueryValidator();
184
-
185
-        $objects = $this->caldavBackend->getSchedulingObjects($this->principalUri);
186
-        foreach ($objects as $object) {
187
-            $vObject = VObject\Reader::read($object['calendardata']);
188
-            if ($validator->validate($vObject, $filters)) {
189
-                $result[] = $object['uri'];
190
-            }
191
-
192
-            // Destroy circular references to PHP will GC the object.
193
-            $vObject->destroy();
194
-        }
195
-
196
-        return $result;
197
-    }
22
+	use DAVACL\ACLTrait;
23
+
24
+	/**
25
+	 * CalDAV backend.
26
+	 *
27
+	 * @var Backend\BackendInterface
28
+	 */
29
+	protected $caldavBackend;
30
+
31
+	/**
32
+	 * The principal Uri.
33
+	 *
34
+	 * @var string
35
+	 */
36
+	protected $principalUri;
37
+
38
+	/**
39
+	 * Constructor.
40
+	 *
41
+	 * @param string $principalUri
42
+	 */
43
+	public function __construct(Backend\SchedulingSupport $caldavBackend, $principalUri)
44
+	{
45
+		$this->caldavBackend = $caldavBackend;
46
+		$this->principalUri = $principalUri;
47
+	}
48
+
49
+	/**
50
+	 * Returns the name of the node.
51
+	 *
52
+	 * This is used to generate the url.
53
+	 *
54
+	 * @return string
55
+	 */
56
+	public function getName()
57
+	{
58
+		return 'inbox';
59
+	}
60
+
61
+	/**
62
+	 * Returns an array with all the child nodes.
63
+	 *
64
+	 * @return \Sabre\DAV\INode[]
65
+	 */
66
+	public function getChildren()
67
+	{
68
+		$objs = $this->caldavBackend->getSchedulingObjects($this->principalUri);
69
+		$children = [];
70
+		foreach ($objs as $obj) {
71
+			//$obj['acl'] = $this->getACL();
72
+			$obj['principaluri'] = $this->principalUri;
73
+			$children[] = new SchedulingObject($this->caldavBackend, $obj);
74
+		}
75
+
76
+		return $children;
77
+	}
78
+
79
+	/**
80
+	 * Creates a new file in the directory.
81
+	 *
82
+	 * Data will either be supplied as a stream resource, or in certain cases
83
+	 * as a string. Keep in mind that you may have to support either.
84
+	 *
85
+	 * After successful creation of the file, you may choose to return the ETag
86
+	 * of the new file here.
87
+	 *
88
+	 * The returned ETag must be surrounded by double-quotes (The quotes should
89
+	 * be part of the actual string).
90
+	 *
91
+	 * If you cannot accurately determine the ETag, you should not return it.
92
+	 * If you don't store the file exactly as-is (you're transforming it
93
+	 * somehow) you should also not return an ETag.
94
+	 *
95
+	 * This means that if a subsequent GET to this new file does not exactly
96
+	 * return the same contents of what was submitted here, you are strongly
97
+	 * recommended to omit the ETag.
98
+	 *
99
+	 * @param string          $name Name of the file
100
+	 * @param resource|string $data Initial payload
101
+	 *
102
+	 * @return string|null
103
+	 */
104
+	public function createFile($name, $data = null)
105
+	{
106
+		$this->caldavBackend->createSchedulingObject($this->principalUri, $name, $data);
107
+	}
108
+
109
+	/**
110
+	 * Returns the owner principal.
111
+	 *
112
+	 * This must be a url to a principal, or null if there's no owner
113
+	 *
114
+	 * @return string|null
115
+	 */
116
+	public function getOwner()
117
+	{
118
+		return $this->principalUri;
119
+	}
120
+
121
+	/**
122
+	 * Returns a list of ACE's for this node.
123
+	 *
124
+	 * Each ACE has the following properties:
125
+	 *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
126
+	 *     currently the only supported privileges
127
+	 *   * 'principal', a url to the principal who owns the node
128
+	 *   * 'protected' (optional), indicating that this ACE is not allowed to
129
+	 *      be updated.
130
+	 *
131
+	 * @return array
132
+	 */
133
+	public function getACL()
134
+	{
135
+		return [
136
+			[
137
+				'privilege' => '{DAV:}read',
138
+				'principal' => '{DAV:}authenticated',
139
+				'protected' => true,
140
+			],
141
+			[
142
+				'privilege' => '{DAV:}write-properties',
143
+				'principal' => $this->getOwner(),
144
+				'protected' => true,
145
+			],
146
+			[
147
+				'privilege' => '{DAV:}unbind',
148
+				'principal' => $this->getOwner(),
149
+				'protected' => true,
150
+			],
151
+			[
152
+				'privilege' => '{DAV:}unbind',
153
+				'principal' => $this->getOwner().'/calendar-proxy-write',
154
+				'protected' => true,
155
+			],
156
+			[
157
+				'privilege' => '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-deliver',
158
+				'principal' => '{DAV:}authenticated',
159
+				'protected' => true,
160
+			],
161
+		];
162
+	}
163
+
164
+	/**
165
+	 * Performs a calendar-query on the contents of this calendar.
166
+	 *
167
+	 * The calendar-query is defined in RFC4791 : CalDAV. Using the
168
+	 * calendar-query it is possible for a client to request a specific set of
169
+	 * object, based on contents of iCalendar properties, date-ranges and
170
+	 * iCalendar component types (VTODO, VEVENT).
171
+	 *
172
+	 * This method should just return a list of (relative) urls that match this
173
+	 * query.
174
+	 *
175
+	 * The list of filters are specified as an array. The exact array is
176
+	 * documented by \Sabre\CalDAV\CalendarQueryParser.
177
+	 *
178
+	 * @return array
179
+	 */
180
+	public function calendarQuery(array $filters)
181
+	{
182
+		$result = [];
183
+		$validator = new CalDAV\CalendarQueryValidator();
184
+
185
+		$objects = $this->caldavBackend->getSchedulingObjects($this->principalUri);
186
+		foreach ($objects as $object) {
187
+			$vObject = VObject\Reader::read($object['calendardata']);
188
+			if ($validator->validate($vObject, $filters)) {
189
+				$result[] = $object['uri'];
190
+			}
191
+
192
+			// Destroy circular references to PHP will GC the object.
193
+			$vObject->destroy();
194
+		}
195
+
196
+		return $result;
197
+	}
198 198
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php 1 patch
Indentation   +102 added lines, -102 removed lines patch added patch discarded remove patch
@@ -16,115 +16,115 @@
 block discarded – undo
16 16
  */
17 17
 class SchedulingObject extends \Sabre\CalDAV\CalendarObject implements ISchedulingObject
18 18
 {
19
-    /**
20
-     * Constructor.
21
-     *
22
-     * The following properties may be passed within $objectData:
23
-     *
24
-     *   * uri - A unique uri. Only the 'basename' must be passed.
25
-     *   * principaluri - the principal that owns the object.
26
-     *   * calendardata (optional) - The iCalendar data
27
-     *   * etag - (optional) The etag for this object, MUST be encloded with
28
-     *            double-quotes.
29
-     *   * size - (optional) The size of the data in bytes.
30
-     *   * lastmodified - (optional) format as a unix timestamp.
31
-     *   * acl - (optional) Use this to override the default ACL for the node.
32
-     */
33
-    public function __construct(Backend\SchedulingSupport $caldavBackend, array $objectData)
34
-    {
35
-        parent::__construct($caldavBackend, [], $objectData);
19
+	/**
20
+	 * Constructor.
21
+	 *
22
+	 * The following properties may be passed within $objectData:
23
+	 *
24
+	 *   * uri - A unique uri. Only the 'basename' must be passed.
25
+	 *   * principaluri - the principal that owns the object.
26
+	 *   * calendardata (optional) - The iCalendar data
27
+	 *   * etag - (optional) The etag for this object, MUST be encloded with
28
+	 *            double-quotes.
29
+	 *   * size - (optional) The size of the data in bytes.
30
+	 *   * lastmodified - (optional) format as a unix timestamp.
31
+	 *   * acl - (optional) Use this to override the default ACL for the node.
32
+	 */
33
+	public function __construct(Backend\SchedulingSupport $caldavBackend, array $objectData)
34
+	{
35
+		parent::__construct($caldavBackend, [], $objectData);
36 36
 
37
-        if (!isset($objectData['uri'])) {
38
-            throw new \InvalidArgumentException('The objectData argument must contain an \'uri\' property');
39
-        }
40
-    }
37
+		if (!isset($objectData['uri'])) {
38
+			throw new \InvalidArgumentException('The objectData argument must contain an \'uri\' property');
39
+		}
40
+	}
41 41
 
42
-    /**
43
-     * Returns the ICalendar-formatted object.
44
-     *
45
-     * @return string
46
-     */
47
-    public function get()
48
-    {
49
-        // Pre-populating the 'calendardata' is optional, if we don't have it
50
-        // already we fetch it from the backend.
51
-        if (!isset($this->objectData['calendardata'])) {
52
-            $this->objectData = $this->caldavBackend->getSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']);
53
-        }
42
+	/**
43
+	 * Returns the ICalendar-formatted object.
44
+	 *
45
+	 * @return string
46
+	 */
47
+	public function get()
48
+	{
49
+		// Pre-populating the 'calendardata' is optional, if we don't have it
50
+		// already we fetch it from the backend.
51
+		if (!isset($this->objectData['calendardata'])) {
52
+			$this->objectData = $this->caldavBackend->getSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']);
53
+		}
54 54
 
55
-        return $this->objectData['calendardata'];
56
-    }
55
+		return $this->objectData['calendardata'];
56
+	}
57 57
 
58
-    /**
59
-     * Updates the ICalendar-formatted object.
60
-     *
61
-     * @param string|resource $calendarData
62
-     *
63
-     * @return string
64
-     */
65
-    public function put($calendarData)
66
-    {
67
-        throw new MethodNotAllowed('Updating scheduling objects is not supported');
68
-    }
58
+	/**
59
+	 * Updates the ICalendar-formatted object.
60
+	 *
61
+	 * @param string|resource $calendarData
62
+	 *
63
+	 * @return string
64
+	 */
65
+	public function put($calendarData)
66
+	{
67
+		throw new MethodNotAllowed('Updating scheduling objects is not supported');
68
+	}
69 69
 
70
-    /**
71
-     * Deletes the scheduling message.
72
-     */
73
-    public function delete()
74
-    {
75
-        $this->caldavBackend->deleteSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']);
76
-    }
70
+	/**
71
+	 * Deletes the scheduling message.
72
+	 */
73
+	public function delete()
74
+	{
75
+		$this->caldavBackend->deleteSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']);
76
+	}
77 77
 
78
-    /**
79
-     * Returns the owner principal.
80
-     *
81
-     * This must be a url to a principal, or null if there's no owner
82
-     *
83
-     * @return string|null
84
-     */
85
-    public function getOwner()
86
-    {
87
-        return $this->objectData['principaluri'];
88
-    }
78
+	/**
79
+	 * Returns the owner principal.
80
+	 *
81
+	 * This must be a url to a principal, or null if there's no owner
82
+	 *
83
+	 * @return string|null
84
+	 */
85
+	public function getOwner()
86
+	{
87
+		return $this->objectData['principaluri'];
88
+	}
89 89
 
90
-    /**
91
-     * Returns a list of ACE's for this node.
92
-     *
93
-     * Each ACE has the following properties:
94
-     *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
95
-     *     currently the only supported privileges
96
-     *   * 'principal', a url to the principal who owns the node
97
-     *   * 'protected' (optional), indicating that this ACE is not allowed to
98
-     *      be updated.
99
-     *
100
-     * @return array
101
-     */
102
-    public function getACL()
103
-    {
104
-        // An alternative acl may be specified in the object data.
105
-        //
90
+	/**
91
+	 * Returns a list of ACE's for this node.
92
+	 *
93
+	 * Each ACE has the following properties:
94
+	 *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
95
+	 *     currently the only supported privileges
96
+	 *   * 'principal', a url to the principal who owns the node
97
+	 *   * 'protected' (optional), indicating that this ACE is not allowed to
98
+	 *      be updated.
99
+	 *
100
+	 * @return array
101
+	 */
102
+	public function getACL()
103
+	{
104
+		// An alternative acl may be specified in the object data.
105
+		//
106 106
 
107
-        if (isset($this->objectData['acl'])) {
108
-            return $this->objectData['acl'];
109
-        }
107
+		if (isset($this->objectData['acl'])) {
108
+			return $this->objectData['acl'];
109
+		}
110 110
 
111
-        // The default ACL
112
-        return [
113
-            [
114
-                'privilege' => '{DAV:}all',
115
-                'principal' => '{DAV:}owner',
116
-                'protected' => true,
117
-            ],
118
-            [
119
-                'privilege' => '{DAV:}all',
120
-                'principal' => $this->objectData['principaluri'].'/calendar-proxy-write',
121
-                'protected' => true,
122
-            ],
123
-            [
124
-                'privilege' => '{DAV:}read',
125
-                'principal' => $this->objectData['principaluri'].'/calendar-proxy-read',
126
-                'protected' => true,
127
-            ],
128
-        ];
129
-    }
111
+		// The default ACL
112
+		return [
113
+			[
114
+				'privilege' => '{DAV:}all',
115
+				'principal' => '{DAV:}owner',
116
+				'protected' => true,
117
+			],
118
+			[
119
+				'privilege' => '{DAV:}all',
120
+				'principal' => $this->objectData['principaluri'].'/calendar-proxy-write',
121
+				'protected' => true,
122
+			],
123
+			[
124
+				'privilege' => '{DAV:}read',
125
+				'principal' => $this->objectData['principaluri'].'/calendar-proxy-read',
126
+				'protected' => true,
127
+			],
128
+		];
129
+	}
130 130
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/dav/lib/CardDAV/Plugin.php 2 patches
Indentation   +849 added lines, -849 removed lines patch added patch discarded remove patch
@@ -25,683 +25,683 @@  discard block
 block discarded – undo
25 25
  */
26 26
 class Plugin extends DAV\ServerPlugin
27 27
 {
28
-    /**
29
-     * Url to the addressbooks.
30
-     */
31
-    const ADDRESSBOOK_ROOT = 'addressbooks';
32
-
33
-    /**
34
-     * xml namespace for CardDAV elements.
35
-     */
36
-    const NS_CARDDAV = 'urn:ietf:params:xml:ns:carddav';
37
-
38
-    /**
39
-     * Add urls to this property to have them automatically exposed as
40
-     * 'directories' to the user.
41
-     *
42
-     * @var array
43
-     */
44
-    public $directories = [];
45
-
46
-    /**
47
-     * Server class.
48
-     *
49
-     * @var DAV\Server
50
-     */
51
-    protected $server;
52
-
53
-    /**
54
-     * The default PDO storage uses a MySQL MEDIUMBLOB for iCalendar data,
55
-     * which can hold up to 2^24 = 16777216 bytes. This is plenty. We're
56
-     * capping it to 10M here.
57
-     */
58
-    protected $maxResourceSize = 10000000;
59
-
60
-    /**
61
-     * Initializes the plugin.
62
-     */
63
-    public function initialize(DAV\Server $server)
64
-    {
65
-        /* Events */
66
-        $server->on('propFind', [$this, 'propFindEarly']);
67
-        $server->on('propFind', [$this, 'propFindLate'], 150);
68
-        $server->on('report', [$this, 'report']);
69
-        $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']);
70
-        $server->on('beforeWriteContent', [$this, 'beforeWriteContent']);
71
-        $server->on('beforeCreateFile', [$this, 'beforeCreateFile']);
72
-        $server->on('afterMethod:GET', [$this, 'httpAfterGet']);
73
-
74
-        $server->xml->namespaceMap[self::NS_CARDDAV] = 'card';
75
-
76
-        $server->xml->elementMap['{'.self::NS_CARDDAV.'}addressbook-query'] = 'Sabre\\CardDAV\\Xml\\Request\\AddressBookQueryReport';
77
-        $server->xml->elementMap['{'.self::NS_CARDDAV.'}addressbook-multiget'] = 'Sabre\\CardDAV\\Xml\\Request\\AddressBookMultiGetReport';
78
-
79
-        /* Mapping Interfaces to {DAV:}resourcetype values */
80
-        $server->resourceTypeMapping['Sabre\\CardDAV\\IAddressBook'] = '{'.self::NS_CARDDAV.'}addressbook';
81
-        $server->resourceTypeMapping['Sabre\\CardDAV\\IDirectory'] = '{'.self::NS_CARDDAV.'}directory';
82
-
83
-        /* Adding properties that may never be changed */
84
-        $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}supported-address-data';
85
-        $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}max-resource-size';
86
-        $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}addressbook-home-set';
87
-        $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}supported-collation-set';
88
-
89
-        $server->xml->elementMap['{http://calendarserver.org/ns/}me-card'] = 'Sabre\\DAV\\Xml\\Property\\Href';
90
-
91
-        $this->server = $server;
92
-    }
93
-
94
-    /**
95
-     * Returns a list of supported features.
96
-     *
97
-     * This is used in the DAV: header in the OPTIONS and PROPFIND requests.
98
-     *
99
-     * @return array
100
-     */
101
-    public function getFeatures()
102
-    {
103
-        return ['addressbook'];
104
-    }
105
-
106
-    /**
107
-     * Returns a list of reports this plugin supports.
108
-     *
109
-     * This will be used in the {DAV:}supported-report-set property.
110
-     * Note that you still need to subscribe to the 'report' event to actually
111
-     * implement them
112
-     *
113
-     * @param string $uri
114
-     *
115
-     * @return array
116
-     */
117
-    public function getSupportedReportSet($uri)
118
-    {
119
-        $node = $this->server->tree->getNodeForPath($uri);
120
-        if ($node instanceof IAddressBook || $node instanceof ICard) {
121
-            return [
122
-                 '{'.self::NS_CARDDAV.'}addressbook-multiget',
123
-                 '{'.self::NS_CARDDAV.'}addressbook-query',
124
-            ];
125
-        }
126
-
127
-        return [];
128
-    }
129
-
130
-    /**
131
-     * Adds all CardDAV-specific properties.
132
-     */
133
-    public function propFindEarly(DAV\PropFind $propFind, DAV\INode $node)
134
-    {
135
-        $ns = '{'.self::NS_CARDDAV.'}';
136
-
137
-        if ($node instanceof IAddressBook) {
138
-            $propFind->handle($ns.'max-resource-size', $this->maxResourceSize);
139
-            $propFind->handle($ns.'supported-address-data', function () {
140
-                return new Xml\Property\SupportedAddressData();
141
-            });
142
-            $propFind->handle($ns.'supported-collation-set', function () {
143
-                return new Xml\Property\SupportedCollationSet();
144
-            });
145
-        }
146
-        if ($node instanceof DAVACL\IPrincipal) {
147
-            $path = $propFind->getPath();
148
-
149
-            $propFind->handle('{'.self::NS_CARDDAV.'}addressbook-home-set', function () use ($path) {
150
-                return new LocalHref($this->getAddressBookHomeForPrincipal($path).'/');
151
-            });
152
-
153
-            if ($this->directories) {
154
-                $propFind->handle('{'.self::NS_CARDDAV.'}directory-gateway', function () {
155
-                    return new LocalHref($this->directories);
156
-                });
157
-            }
158
-        }
159
-
160
-        if ($node instanceof ICard) {
161
-            // The address-data property is not supposed to be a 'real'
162
-            // property, but in large chunks of the spec it does act as such.
163
-            // Therefore we simply expose it as a property.
164
-            $propFind->handle('{'.self::NS_CARDDAV.'}address-data', function () use ($node) {
165
-                $val = $node->get();
166
-                if (is_resource($val)) {
167
-                    $val = stream_get_contents($val);
168
-                }
169
-
170
-                return $val;
171
-            });
172
-        }
173
-    }
174
-
175
-    /**
176
-     * This functions handles REPORT requests specific to CardDAV.
177
-     *
178
-     * @param string   $reportName
179
-     * @param \DOMNode $dom
180
-     * @param mixed    $path
181
-     *
182
-     * @return bool
183
-     */
184
-    public function report($reportName, $dom, $path)
185
-    {
186
-        switch ($reportName) {
187
-            case '{'.self::NS_CARDDAV.'}addressbook-multiget':
188
-                $this->server->transactionType = 'report-addressbook-multiget';
189
-                $this->addressbookMultiGetReport($dom);
190
-
191
-                return false;
192
-            case '{'.self::NS_CARDDAV.'}addressbook-query':
193
-                $this->server->transactionType = 'report-addressbook-query';
194
-                $this->addressBookQueryReport($dom);
195
-
196
-                return false;
197
-            default:
198
-                return;
199
-        }
200
-    }
201
-
202
-    /**
203
-     * Returns the addressbook home for a given principal.
204
-     *
205
-     * @param string $principal
206
-     *
207
-     * @return string
208
-     */
209
-    protected function getAddressbookHomeForPrincipal($principal)
210
-    {
211
-        list(, $principalId) = Uri\split($principal);
212
-
213
-        return self::ADDRESSBOOK_ROOT.'/'.$principalId;
214
-    }
215
-
216
-    /**
217
-     * This function handles the addressbook-multiget REPORT.
218
-     *
219
-     * This report is used by the client to fetch the content of a series
220
-     * of urls. Effectively avoiding a lot of redundant requests.
221
-     *
222
-     * @param Xml\Request\AddressBookMultiGetReport $report
223
-     */
224
-    public function addressbookMultiGetReport($report)
225
-    {
226
-        $contentType = $report->contentType;
227
-        $version = $report->version;
228
-        if ($version) {
229
-            $contentType .= '; version='.$version;
230
-        }
231
-
232
-        $vcardType = $this->negotiateVCard(
233
-            $contentType
234
-        );
235
-
236
-        $propertyList = [];
237
-        $paths = array_map(
238
-            [$this->server, 'calculateUri'],
239
-            $report->hrefs
240
-        );
241
-        foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $props) {
242
-            if (isset($props['200']['{'.self::NS_CARDDAV.'}address-data'])) {
243
-                $props['200']['{'.self::NS_CARDDAV.'}address-data'] = $this->convertVCard(
244
-                    $props[200]['{'.self::NS_CARDDAV.'}address-data'],
245
-                    $vcardType
246
-                );
247
-            }
248
-            $propertyList[] = $props;
249
-        }
250
-
251
-        $prefer = $this->server->getHTTPPrefer();
252
-
253
-        $this->server->httpResponse->setStatus(207);
254
-        $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
255
-        $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
256
-        $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, 'minimal' === $prefer['return']));
257
-    }
258
-
259
-    /**
260
-     * This method is triggered before a file gets updated with new content.
261
-     *
262
-     * This plugin uses this method to ensure that Card nodes receive valid
263
-     * vcard data.
264
-     *
265
-     * @param string   $path
266
-     * @param resource $data
267
-     * @param bool     $modified should be set to true, if this event handler
268
-     *                           changed &$data
269
-     */
270
-    public function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified)
271
-    {
272
-        if (!$node instanceof ICard) {
273
-            return;
274
-        }
275
-
276
-        $this->validateVCard($data, $modified);
277
-    }
278
-
279
-    /**
280
-     * This method is triggered before a new file is created.
281
-     *
282
-     * This plugin uses this method to ensure that Card nodes receive valid
283
-     * vcard data.
284
-     *
285
-     * @param string   $path
286
-     * @param resource $data
287
-     * @param bool     $modified should be set to true, if this event handler
288
-     *                           changed &$data
289
-     */
290
-    public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified)
291
-    {
292
-        if (!$parentNode instanceof IAddressBook) {
293
-            return;
294
-        }
295
-
296
-        $this->validateVCard($data, $modified);
297
-    }
298
-
299
-    /**
300
-     * Checks if the submitted iCalendar data is in fact, valid.
301
-     *
302
-     * An exception is thrown if it's not.
303
-     *
304
-     * @param resource|string $data
305
-     * @param bool            $modified should be set to true, if this event handler
306
-     *                                  changed &$data
307
-     */
308
-    protected function validateVCard(&$data, &$modified)
309
-    {
310
-        // If it's a stream, we convert it to a string first.
311
-        if (is_resource($data)) {
312
-            $data = stream_get_contents($data);
313
-        }
314
-
315
-        $before = $data;
316
-
317
-        try {
318
-            // If the data starts with a [, we can reasonably assume we're dealing
319
-            // with a jCal object.
320
-            if ('[' === substr($data, 0, 1)) {
321
-                $vobj = VObject\Reader::readJson($data);
322
-
323
-                // Converting $data back to iCalendar, as that's what we
324
-                // technically support everywhere.
325
-                $data = $vobj->serialize();
326
-                $modified = true;
327
-            } else {
328
-                $vobj = VObject\Reader::read($data);
329
-            }
330
-        } catch (VObject\ParseException $e) {
331
-            throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid vCard or jCard data. Parse error: '.$e->getMessage());
332
-        }
333
-
334
-        if ('VCARD' !== $vobj->name) {
335
-            throw new DAV\Exception\UnsupportedMediaType('This collection can only support vcard objects.');
336
-        }
337
-
338
-        $options = VObject\Node::PROFILE_CARDDAV;
339
-        $prefer = $this->server->getHTTPPrefer();
340
-
341
-        if ('strict' !== $prefer['handling']) {
342
-            $options |= VObject\Node::REPAIR;
343
-        }
344
-
345
-        $messages = $vobj->validate($options);
346
-
347
-        $highestLevel = 0;
348
-        $warningMessage = null;
349
-
350
-        // $messages contains a list of problems with the vcard, along with
351
-        // their severity.
352
-        foreach ($messages as $message) {
353
-            if ($message['level'] > $highestLevel) {
354
-                // Recording the highest reported error level.
355
-                $highestLevel = $message['level'];
356
-                $warningMessage = $message['message'];
357
-            }
358
-
359
-            switch ($message['level']) {
360
-                case 1:
361
-                    // Level 1 means that there was a problem, but it was repaired.
362
-                    $modified = true;
363
-                    break;
364
-                case 2:
365
-                    // Level 2 means a warning, but not critical
366
-                    break;
367
-                case 3:
368
-                    // Level 3 means a critical error
369
-                    throw new DAV\Exception\UnsupportedMediaType('Validation error in vCard: '.$message['message']);
370
-            }
371
-        }
372
-        if ($warningMessage) {
373
-            $this->server->httpResponse->setHeader(
374
-                'X-Sabre-Ew-Gross',
375
-                'vCard validation warning: '.$warningMessage
376
-            );
377
-
378
-            // Re-serializing object.
379
-            $data = $vobj->serialize();
380
-            if (!$modified && 0 !== strcmp($data, $before)) {
381
-                // This ensures that the system does not send an ETag back.
382
-                $modified = true;
383
-            }
384
-        }
385
-
386
-        // Destroy circular references to PHP will GC the object.
387
-        $vobj->destroy();
388
-    }
389
-
390
-    /**
391
-     * This function handles the addressbook-query REPORT.
392
-     *
393
-     * This report is used by the client to filter an addressbook based on a
394
-     * complex query.
395
-     *
396
-     * @param Xml\Request\AddressBookQueryReport $report
397
-     */
398
-    protected function addressbookQueryReport($report)
399
-    {
400
-        $depth = $this->server->getHTTPDepth(0);
401
-
402
-        if (0 == $depth) {
403
-            $candidateNodes = [
404
-                $this->server->tree->getNodeForPath($this->server->getRequestUri()),
405
-            ];
406
-            if (!$candidateNodes[0] instanceof ICard) {
407
-                throw new ReportNotSupported('The addressbook-query report is not supported on this url with Depth: 0');
408
-            }
409
-        } else {
410
-            $candidateNodes = $this->server->tree->getChildren($this->server->getRequestUri());
411
-        }
412
-
413
-        $contentType = $report->contentType;
414
-        if ($report->version) {
415
-            $contentType .= '; version='.$report->version;
416
-        }
417
-
418
-        $vcardType = $this->negotiateVCard(
419
-            $contentType
420
-        );
421
-
422
-        $validNodes = [];
423
-        foreach ($candidateNodes as $node) {
424
-            if (!$node instanceof ICard) {
425
-                continue;
426
-            }
427
-
428
-            $blob = $node->get();
429
-            if (is_resource($blob)) {
430
-                $blob = stream_get_contents($blob);
431
-            }
432
-
433
-            if (!$this->validateFilters($blob, $report->filters, $report->test)) {
434
-                continue;
435
-            }
436
-
437
-            $validNodes[] = $node;
438
-
439
-            if ($report->limit && $report->limit <= count($validNodes)) {
440
-                // We hit the maximum number of items, we can stop now.
441
-                break;
442
-            }
443
-        }
444
-
445
-        $result = [];
446
-        foreach ($validNodes as $validNode) {
447
-            if (0 == $depth) {
448
-                $href = $this->server->getRequestUri();
449
-            } else {
450
-                $href = $this->server->getRequestUri().'/'.$validNode->getName();
451
-            }
452
-
453
-            list($props) = $this->server->getPropertiesForPath($href, $report->properties, 0);
454
-
455
-            if (isset($props[200]['{'.self::NS_CARDDAV.'}address-data'])) {
456
-                $props[200]['{'.self::NS_CARDDAV.'}address-data'] = $this->convertVCard(
457
-                    $props[200]['{'.self::NS_CARDDAV.'}address-data'],
458
-                    $vcardType,
459
-                    $report->addressDataProperties
460
-                );
461
-            }
462
-            $result[] = $props;
463
-        }
464
-
465
-        $prefer = $this->server->getHTTPPrefer();
466
-
467
-        $this->server->httpResponse->setStatus(207);
468
-        $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
469
-        $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
470
-        $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, 'minimal' === $prefer['return']));
471
-    }
472
-
473
-    /**
474
-     * Validates if a vcard makes it throught a list of filters.
475
-     *
476
-     * @param string $vcardData
477
-     * @param string $test      anyof or allof (which means OR or AND)
478
-     *
479
-     * @return bool
480
-     */
481
-    public function validateFilters($vcardData, array $filters, $test)
482
-    {
483
-        if (!$filters) {
484
-            return true;
485
-        }
486
-        $vcard = VObject\Reader::read($vcardData);
487
-
488
-        foreach ($filters as $filter) {
489
-            $isDefined = isset($vcard->{$filter['name']});
490
-            if ($filter['is-not-defined']) {
491
-                if ($isDefined) {
492
-                    $success = false;
493
-                } else {
494
-                    $success = true;
495
-                }
496
-            } elseif ((!$filter['param-filters'] && !$filter['text-matches']) || !$isDefined) {
497
-                // We only need to check for existence
498
-                $success = $isDefined;
499
-            } else {
500
-                $vProperties = $vcard->select($filter['name']);
501
-
502
-                $results = [];
503
-                if ($filter['param-filters']) {
504
-                    $results[] = $this->validateParamFilters($vProperties, $filter['param-filters'], $filter['test']);
505
-                }
506
-                if ($filter['text-matches']) {
507
-                    $texts = [];
508
-                    foreach ($vProperties as $vProperty) {
509
-                        $texts[] = $vProperty->getValue();
510
-                    }
511
-
512
-                    $results[] = $this->validateTextMatches($texts, $filter['text-matches'], $filter['test']);
513
-                }
514
-
515
-                if (1 === count($results)) {
516
-                    $success = $results[0];
517
-                } else {
518
-                    if ('anyof' === $filter['test']) {
519
-                        $success = $results[0] || $results[1];
520
-                    } else {
521
-                        $success = $results[0] && $results[1];
522
-                    }
523
-                }
524
-            } // else
525
-
526
-            // There are two conditions where we can already determine whether
527
-            // or not this filter succeeds.
528
-            if ('anyof' === $test && $success) {
529
-                // Destroy circular references to PHP will GC the object.
530
-                $vcard->destroy();
531
-
532
-                return true;
533
-            }
534
-            if ('allof' === $test && !$success) {
535
-                // Destroy circular references to PHP will GC the object.
536
-                $vcard->destroy();
537
-
538
-                return false;
539
-            }
540
-        } // foreach
541
-
542
-        // Destroy circular references to PHP will GC the object.
543
-        $vcard->destroy();
544
-
545
-        // If we got all the way here, it means we haven't been able to
546
-        // determine early if the test failed or not.
547
-        //
548
-        // This implies for 'anyof' that the test failed, and for 'allof' that
549
-        // we succeeded. Sounds weird, but makes sense.
550
-        return 'allof' === $test;
551
-    }
552
-
553
-    /**
554
-     * Validates if a param-filter can be applied to a specific property.
555
-     *
556
-     * @todo currently we're only validating the first parameter of the passed
557
-     *       property. Any subsequence parameters with the same name are
558
-     *       ignored.
559
-     *
560
-     * @param string $test
561
-     *
562
-     * @return bool
563
-     */
564
-    protected function validateParamFilters(array $vProperties, array $filters, $test)
565
-    {
566
-        foreach ($filters as $filter) {
567
-            $isDefined = false;
568
-            foreach ($vProperties as $vProperty) {
569
-                $isDefined = isset($vProperty[$filter['name']]);
570
-                if ($isDefined) {
571
-                    break;
572
-                }
573
-            }
574
-
575
-            if ($filter['is-not-defined']) {
576
-                if ($isDefined) {
577
-                    $success = false;
578
-                } else {
579
-                    $success = true;
580
-                }
581
-
582
-                // If there's no text-match, we can just check for existence
583
-            } elseif (!$filter['text-match'] || !$isDefined) {
584
-                $success = $isDefined;
585
-            } else {
586
-                $success = false;
587
-                foreach ($vProperties as $vProperty) {
588
-                    // If we got all the way here, we'll need to validate the
589
-                    // text-match filter.
590
-                    if (isset($vProperty[$filter['name']])) {
591
-                        $success = DAV\StringUtil::textMatch(
592
-                            $vProperty[$filter['name']]->getValue(),
593
-                            $filter['text-match']['value'],
594
-                            $filter['text-match']['collation'],
595
-                            $filter['text-match']['match-type']
596
-                        );
597
-                        if ($filter['text-match']['negate-condition']) {
598
-                            $success = !$success;
599
-                        }
600
-                    }
601
-                    if ($success) {
602
-                        break;
603
-                    }
604
-                }
605
-            } // else
606
-
607
-            // There are two conditions where we can already determine whether
608
-            // or not this filter succeeds.
609
-            if ('anyof' === $test && $success) {
610
-                return true;
611
-            }
612
-            if ('allof' === $test && !$success) {
613
-                return false;
614
-            }
615
-        }
616
-
617
-        // If we got all the way here, it means we haven't been able to
618
-        // determine early if the test failed or not.
619
-        //
620
-        // This implies for 'anyof' that the test failed, and for 'allof' that
621
-        // we succeeded. Sounds weird, but makes sense.
622
-        return 'allof' === $test;
623
-    }
624
-
625
-    /**
626
-     * Validates if a text-filter can be applied to a specific property.
627
-     *
628
-     * @param string $test
629
-     *
630
-     * @return bool
631
-     */
632
-    protected function validateTextMatches(array $texts, array $filters, $test)
633
-    {
634
-        foreach ($filters as $filter) {
635
-            $success = false;
636
-            foreach ($texts as $haystack) {
637
-                $success = DAV\StringUtil::textMatch($haystack, $filter['value'], $filter['collation'], $filter['match-type']);
638
-                if ($filter['negate-condition']) {
639
-                    $success = !$success;
640
-                }
641
-
642
-                // Breaking on the first match
643
-                if ($success) {
644
-                    break;
645
-                }
646
-            }
647
-
648
-            if ($success && 'anyof' === $test) {
649
-                return true;
650
-            }
651
-
652
-            if (!$success && 'allof' == $test) {
653
-                return false;
654
-            }
655
-        }
656
-
657
-        // If we got all the way here, it means we haven't been able to
658
-        // determine early if the test failed or not.
659
-        //
660
-        // This implies for 'anyof' that the test failed, and for 'allof' that
661
-        // we succeeded. Sounds weird, but makes sense.
662
-        return 'allof' === $test;
663
-    }
664
-
665
-    /**
666
-     * This event is triggered when fetching properties.
667
-     *
668
-     * This event is scheduled late in the process, after most work for
669
-     * propfind has been done.
670
-     */
671
-    public function propFindLate(DAV\PropFind $propFind, DAV\INode $node)
672
-    {
673
-        // If the request was made using the SOGO connector, we must rewrite
674
-        // the content-type property. By default SabreDAV will send back
675
-        // text/x-vcard; charset=utf-8, but for SOGO we must strip that last
676
-        // part.
677
-        if (false === strpos((string) $this->server->httpRequest->getHeader('User-Agent'), 'Thunderbird')) {
678
-            return;
679
-        }
680
-        $contentType = $propFind->get('{DAV:}getcontenttype');
681
-        if (null !== $contentType) {
682
-            list($part) = explode(';', $contentType);
683
-            if ('text/x-vcard' === $part || 'text/vcard' === $part) {
684
-                $propFind->set('{DAV:}getcontenttype', 'text/x-vcard');
685
-            }
686
-        }
687
-    }
688
-
689
-    /**
690
-     * This method is used to generate HTML output for the
691
-     * Sabre\DAV\Browser\Plugin. This allows us to generate an interface users
692
-     * can use to create new addressbooks.
693
-     *
694
-     * @param string $output
695
-     *
696
-     * @return bool
697
-     */
698
-    public function htmlActionsPanel(DAV\INode $node, &$output)
699
-    {
700
-        if (!$node instanceof AddressBookHome) {
701
-            return;
702
-        }
703
-
704
-        $output .= '<tr><td colspan="2"><form method="post" action="">
28
+	/**
29
+	 * Url to the addressbooks.
30
+	 */
31
+	const ADDRESSBOOK_ROOT = 'addressbooks';
32
+
33
+	/**
34
+	 * xml namespace for CardDAV elements.
35
+	 */
36
+	const NS_CARDDAV = 'urn:ietf:params:xml:ns:carddav';
37
+
38
+	/**
39
+	 * Add urls to this property to have them automatically exposed as
40
+	 * 'directories' to the user.
41
+	 *
42
+	 * @var array
43
+	 */
44
+	public $directories = [];
45
+
46
+	/**
47
+	 * Server class.
48
+	 *
49
+	 * @var DAV\Server
50
+	 */
51
+	protected $server;
52
+
53
+	/**
54
+	 * The default PDO storage uses a MySQL MEDIUMBLOB for iCalendar data,
55
+	 * which can hold up to 2^24 = 16777216 bytes. This is plenty. We're
56
+	 * capping it to 10M here.
57
+	 */
58
+	protected $maxResourceSize = 10000000;
59
+
60
+	/**
61
+	 * Initializes the plugin.
62
+	 */
63
+	public function initialize(DAV\Server $server)
64
+	{
65
+		/* Events */
66
+		$server->on('propFind', [$this, 'propFindEarly']);
67
+		$server->on('propFind', [$this, 'propFindLate'], 150);
68
+		$server->on('report', [$this, 'report']);
69
+		$server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']);
70
+		$server->on('beforeWriteContent', [$this, 'beforeWriteContent']);
71
+		$server->on('beforeCreateFile', [$this, 'beforeCreateFile']);
72
+		$server->on('afterMethod:GET', [$this, 'httpAfterGet']);
73
+
74
+		$server->xml->namespaceMap[self::NS_CARDDAV] = 'card';
75
+
76
+		$server->xml->elementMap['{'.self::NS_CARDDAV.'}addressbook-query'] = 'Sabre\\CardDAV\\Xml\\Request\\AddressBookQueryReport';
77
+		$server->xml->elementMap['{'.self::NS_CARDDAV.'}addressbook-multiget'] = 'Sabre\\CardDAV\\Xml\\Request\\AddressBookMultiGetReport';
78
+
79
+		/* Mapping Interfaces to {DAV:}resourcetype values */
80
+		$server->resourceTypeMapping['Sabre\\CardDAV\\IAddressBook'] = '{'.self::NS_CARDDAV.'}addressbook';
81
+		$server->resourceTypeMapping['Sabre\\CardDAV\\IDirectory'] = '{'.self::NS_CARDDAV.'}directory';
82
+
83
+		/* Adding properties that may never be changed */
84
+		$server->protectedProperties[] = '{'.self::NS_CARDDAV.'}supported-address-data';
85
+		$server->protectedProperties[] = '{'.self::NS_CARDDAV.'}max-resource-size';
86
+		$server->protectedProperties[] = '{'.self::NS_CARDDAV.'}addressbook-home-set';
87
+		$server->protectedProperties[] = '{'.self::NS_CARDDAV.'}supported-collation-set';
88
+
89
+		$server->xml->elementMap['{http://calendarserver.org/ns/}me-card'] = 'Sabre\\DAV\\Xml\\Property\\Href';
90
+
91
+		$this->server = $server;
92
+	}
93
+
94
+	/**
95
+	 * Returns a list of supported features.
96
+	 *
97
+	 * This is used in the DAV: header in the OPTIONS and PROPFIND requests.
98
+	 *
99
+	 * @return array
100
+	 */
101
+	public function getFeatures()
102
+	{
103
+		return ['addressbook'];
104
+	}
105
+
106
+	/**
107
+	 * Returns a list of reports this plugin supports.
108
+	 *
109
+	 * This will be used in the {DAV:}supported-report-set property.
110
+	 * Note that you still need to subscribe to the 'report' event to actually
111
+	 * implement them
112
+	 *
113
+	 * @param string $uri
114
+	 *
115
+	 * @return array
116
+	 */
117
+	public function getSupportedReportSet($uri)
118
+	{
119
+		$node = $this->server->tree->getNodeForPath($uri);
120
+		if ($node instanceof IAddressBook || $node instanceof ICard) {
121
+			return [
122
+				 '{'.self::NS_CARDDAV.'}addressbook-multiget',
123
+				 '{'.self::NS_CARDDAV.'}addressbook-query',
124
+			];
125
+		}
126
+
127
+		return [];
128
+	}
129
+
130
+	/**
131
+	 * Adds all CardDAV-specific properties.
132
+	 */
133
+	public function propFindEarly(DAV\PropFind $propFind, DAV\INode $node)
134
+	{
135
+		$ns = '{'.self::NS_CARDDAV.'}';
136
+
137
+		if ($node instanceof IAddressBook) {
138
+			$propFind->handle($ns.'max-resource-size', $this->maxResourceSize);
139
+			$propFind->handle($ns.'supported-address-data', function () {
140
+				return new Xml\Property\SupportedAddressData();
141
+			});
142
+			$propFind->handle($ns.'supported-collation-set', function () {
143
+				return new Xml\Property\SupportedCollationSet();
144
+			});
145
+		}
146
+		if ($node instanceof DAVACL\IPrincipal) {
147
+			$path = $propFind->getPath();
148
+
149
+			$propFind->handle('{'.self::NS_CARDDAV.'}addressbook-home-set', function () use ($path) {
150
+				return new LocalHref($this->getAddressBookHomeForPrincipal($path).'/');
151
+			});
152
+
153
+			if ($this->directories) {
154
+				$propFind->handle('{'.self::NS_CARDDAV.'}directory-gateway', function () {
155
+					return new LocalHref($this->directories);
156
+				});
157
+			}
158
+		}
159
+
160
+		if ($node instanceof ICard) {
161
+			// The address-data property is not supposed to be a 'real'
162
+			// property, but in large chunks of the spec it does act as such.
163
+			// Therefore we simply expose it as a property.
164
+			$propFind->handle('{'.self::NS_CARDDAV.'}address-data', function () use ($node) {
165
+				$val = $node->get();
166
+				if (is_resource($val)) {
167
+					$val = stream_get_contents($val);
168
+				}
169
+
170
+				return $val;
171
+			});
172
+		}
173
+	}
174
+
175
+	/**
176
+	 * This functions handles REPORT requests specific to CardDAV.
177
+	 *
178
+	 * @param string   $reportName
179
+	 * @param \DOMNode $dom
180
+	 * @param mixed    $path
181
+	 *
182
+	 * @return bool
183
+	 */
184
+	public function report($reportName, $dom, $path)
185
+	{
186
+		switch ($reportName) {
187
+			case '{'.self::NS_CARDDAV.'}addressbook-multiget':
188
+				$this->server->transactionType = 'report-addressbook-multiget';
189
+				$this->addressbookMultiGetReport($dom);
190
+
191
+				return false;
192
+			case '{'.self::NS_CARDDAV.'}addressbook-query':
193
+				$this->server->transactionType = 'report-addressbook-query';
194
+				$this->addressBookQueryReport($dom);
195
+
196
+				return false;
197
+			default:
198
+				return;
199
+		}
200
+	}
201
+
202
+	/**
203
+	 * Returns the addressbook home for a given principal.
204
+	 *
205
+	 * @param string $principal
206
+	 *
207
+	 * @return string
208
+	 */
209
+	protected function getAddressbookHomeForPrincipal($principal)
210
+	{
211
+		list(, $principalId) = Uri\split($principal);
212
+
213
+		return self::ADDRESSBOOK_ROOT.'/'.$principalId;
214
+	}
215
+
216
+	/**
217
+	 * This function handles the addressbook-multiget REPORT.
218
+	 *
219
+	 * This report is used by the client to fetch the content of a series
220
+	 * of urls. Effectively avoiding a lot of redundant requests.
221
+	 *
222
+	 * @param Xml\Request\AddressBookMultiGetReport $report
223
+	 */
224
+	public function addressbookMultiGetReport($report)
225
+	{
226
+		$contentType = $report->contentType;
227
+		$version = $report->version;
228
+		if ($version) {
229
+			$contentType .= '; version='.$version;
230
+		}
231
+
232
+		$vcardType = $this->negotiateVCard(
233
+			$contentType
234
+		);
235
+
236
+		$propertyList = [];
237
+		$paths = array_map(
238
+			[$this->server, 'calculateUri'],
239
+			$report->hrefs
240
+		);
241
+		foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $props) {
242
+			if (isset($props['200']['{'.self::NS_CARDDAV.'}address-data'])) {
243
+				$props['200']['{'.self::NS_CARDDAV.'}address-data'] = $this->convertVCard(
244
+					$props[200]['{'.self::NS_CARDDAV.'}address-data'],
245
+					$vcardType
246
+				);
247
+			}
248
+			$propertyList[] = $props;
249
+		}
250
+
251
+		$prefer = $this->server->getHTTPPrefer();
252
+
253
+		$this->server->httpResponse->setStatus(207);
254
+		$this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
255
+		$this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
256
+		$this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, 'minimal' === $prefer['return']));
257
+	}
258
+
259
+	/**
260
+	 * This method is triggered before a file gets updated with new content.
261
+	 *
262
+	 * This plugin uses this method to ensure that Card nodes receive valid
263
+	 * vcard data.
264
+	 *
265
+	 * @param string   $path
266
+	 * @param resource $data
267
+	 * @param bool     $modified should be set to true, if this event handler
268
+	 *                           changed &$data
269
+	 */
270
+	public function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified)
271
+	{
272
+		if (!$node instanceof ICard) {
273
+			return;
274
+		}
275
+
276
+		$this->validateVCard($data, $modified);
277
+	}
278
+
279
+	/**
280
+	 * This method is triggered before a new file is created.
281
+	 *
282
+	 * This plugin uses this method to ensure that Card nodes receive valid
283
+	 * vcard data.
284
+	 *
285
+	 * @param string   $path
286
+	 * @param resource $data
287
+	 * @param bool     $modified should be set to true, if this event handler
288
+	 *                           changed &$data
289
+	 */
290
+	public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified)
291
+	{
292
+		if (!$parentNode instanceof IAddressBook) {
293
+			return;
294
+		}
295
+
296
+		$this->validateVCard($data, $modified);
297
+	}
298
+
299
+	/**
300
+	 * Checks if the submitted iCalendar data is in fact, valid.
301
+	 *
302
+	 * An exception is thrown if it's not.
303
+	 *
304
+	 * @param resource|string $data
305
+	 * @param bool            $modified should be set to true, if this event handler
306
+	 *                                  changed &$data
307
+	 */
308
+	protected function validateVCard(&$data, &$modified)
309
+	{
310
+		// If it's a stream, we convert it to a string first.
311
+		if (is_resource($data)) {
312
+			$data = stream_get_contents($data);
313
+		}
314
+
315
+		$before = $data;
316
+
317
+		try {
318
+			// If the data starts with a [, we can reasonably assume we're dealing
319
+			// with a jCal object.
320
+			if ('[' === substr($data, 0, 1)) {
321
+				$vobj = VObject\Reader::readJson($data);
322
+
323
+				// Converting $data back to iCalendar, as that's what we
324
+				// technically support everywhere.
325
+				$data = $vobj->serialize();
326
+				$modified = true;
327
+			} else {
328
+				$vobj = VObject\Reader::read($data);
329
+			}
330
+		} catch (VObject\ParseException $e) {
331
+			throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid vCard or jCard data. Parse error: '.$e->getMessage());
332
+		}
333
+
334
+		if ('VCARD' !== $vobj->name) {
335
+			throw new DAV\Exception\UnsupportedMediaType('This collection can only support vcard objects.');
336
+		}
337
+
338
+		$options = VObject\Node::PROFILE_CARDDAV;
339
+		$prefer = $this->server->getHTTPPrefer();
340
+
341
+		if ('strict' !== $prefer['handling']) {
342
+			$options |= VObject\Node::REPAIR;
343
+		}
344
+
345
+		$messages = $vobj->validate($options);
346
+
347
+		$highestLevel = 0;
348
+		$warningMessage = null;
349
+
350
+		// $messages contains a list of problems with the vcard, along with
351
+		// their severity.
352
+		foreach ($messages as $message) {
353
+			if ($message['level'] > $highestLevel) {
354
+				// Recording the highest reported error level.
355
+				$highestLevel = $message['level'];
356
+				$warningMessage = $message['message'];
357
+			}
358
+
359
+			switch ($message['level']) {
360
+				case 1:
361
+					// Level 1 means that there was a problem, but it was repaired.
362
+					$modified = true;
363
+					break;
364
+				case 2:
365
+					// Level 2 means a warning, but not critical
366
+					break;
367
+				case 3:
368
+					// Level 3 means a critical error
369
+					throw new DAV\Exception\UnsupportedMediaType('Validation error in vCard: '.$message['message']);
370
+			}
371
+		}
372
+		if ($warningMessage) {
373
+			$this->server->httpResponse->setHeader(
374
+				'X-Sabre-Ew-Gross',
375
+				'vCard validation warning: '.$warningMessage
376
+			);
377
+
378
+			// Re-serializing object.
379
+			$data = $vobj->serialize();
380
+			if (!$modified && 0 !== strcmp($data, $before)) {
381
+				// This ensures that the system does not send an ETag back.
382
+				$modified = true;
383
+			}
384
+		}
385
+
386
+		// Destroy circular references to PHP will GC the object.
387
+		$vobj->destroy();
388
+	}
389
+
390
+	/**
391
+	 * This function handles the addressbook-query REPORT.
392
+	 *
393
+	 * This report is used by the client to filter an addressbook based on a
394
+	 * complex query.
395
+	 *
396
+	 * @param Xml\Request\AddressBookQueryReport $report
397
+	 */
398
+	protected function addressbookQueryReport($report)
399
+	{
400
+		$depth = $this->server->getHTTPDepth(0);
401
+
402
+		if (0 == $depth) {
403
+			$candidateNodes = [
404
+				$this->server->tree->getNodeForPath($this->server->getRequestUri()),
405
+			];
406
+			if (!$candidateNodes[0] instanceof ICard) {
407
+				throw new ReportNotSupported('The addressbook-query report is not supported on this url with Depth: 0');
408
+			}
409
+		} else {
410
+			$candidateNodes = $this->server->tree->getChildren($this->server->getRequestUri());
411
+		}
412
+
413
+		$contentType = $report->contentType;
414
+		if ($report->version) {
415
+			$contentType .= '; version='.$report->version;
416
+		}
417
+
418
+		$vcardType = $this->negotiateVCard(
419
+			$contentType
420
+		);
421
+
422
+		$validNodes = [];
423
+		foreach ($candidateNodes as $node) {
424
+			if (!$node instanceof ICard) {
425
+				continue;
426
+			}
427
+
428
+			$blob = $node->get();
429
+			if (is_resource($blob)) {
430
+				$blob = stream_get_contents($blob);
431
+			}
432
+
433
+			if (!$this->validateFilters($blob, $report->filters, $report->test)) {
434
+				continue;
435
+			}
436
+
437
+			$validNodes[] = $node;
438
+
439
+			if ($report->limit && $report->limit <= count($validNodes)) {
440
+				// We hit the maximum number of items, we can stop now.
441
+				break;
442
+			}
443
+		}
444
+
445
+		$result = [];
446
+		foreach ($validNodes as $validNode) {
447
+			if (0 == $depth) {
448
+				$href = $this->server->getRequestUri();
449
+			} else {
450
+				$href = $this->server->getRequestUri().'/'.$validNode->getName();
451
+			}
452
+
453
+			list($props) = $this->server->getPropertiesForPath($href, $report->properties, 0);
454
+
455
+			if (isset($props[200]['{'.self::NS_CARDDAV.'}address-data'])) {
456
+				$props[200]['{'.self::NS_CARDDAV.'}address-data'] = $this->convertVCard(
457
+					$props[200]['{'.self::NS_CARDDAV.'}address-data'],
458
+					$vcardType,
459
+					$report->addressDataProperties
460
+				);
461
+			}
462
+			$result[] = $props;
463
+		}
464
+
465
+		$prefer = $this->server->getHTTPPrefer();
466
+
467
+		$this->server->httpResponse->setStatus(207);
468
+		$this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
469
+		$this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
470
+		$this->server->httpResponse->setBody($this->server->generateMultiStatus($result, 'minimal' === $prefer['return']));
471
+	}
472
+
473
+	/**
474
+	 * Validates if a vcard makes it throught a list of filters.
475
+	 *
476
+	 * @param string $vcardData
477
+	 * @param string $test      anyof or allof (which means OR or AND)
478
+	 *
479
+	 * @return bool
480
+	 */
481
+	public function validateFilters($vcardData, array $filters, $test)
482
+	{
483
+		if (!$filters) {
484
+			return true;
485
+		}
486
+		$vcard = VObject\Reader::read($vcardData);
487
+
488
+		foreach ($filters as $filter) {
489
+			$isDefined = isset($vcard->{$filter['name']});
490
+			if ($filter['is-not-defined']) {
491
+				if ($isDefined) {
492
+					$success = false;
493
+				} else {
494
+					$success = true;
495
+				}
496
+			} elseif ((!$filter['param-filters'] && !$filter['text-matches']) || !$isDefined) {
497
+				// We only need to check for existence
498
+				$success = $isDefined;
499
+			} else {
500
+				$vProperties = $vcard->select($filter['name']);
501
+
502
+				$results = [];
503
+				if ($filter['param-filters']) {
504
+					$results[] = $this->validateParamFilters($vProperties, $filter['param-filters'], $filter['test']);
505
+				}
506
+				if ($filter['text-matches']) {
507
+					$texts = [];
508
+					foreach ($vProperties as $vProperty) {
509
+						$texts[] = $vProperty->getValue();
510
+					}
511
+
512
+					$results[] = $this->validateTextMatches($texts, $filter['text-matches'], $filter['test']);
513
+				}
514
+
515
+				if (1 === count($results)) {
516
+					$success = $results[0];
517
+				} else {
518
+					if ('anyof' === $filter['test']) {
519
+						$success = $results[0] || $results[1];
520
+					} else {
521
+						$success = $results[0] && $results[1];
522
+					}
523
+				}
524
+			} // else
525
+
526
+			// There are two conditions where we can already determine whether
527
+			// or not this filter succeeds.
528
+			if ('anyof' === $test && $success) {
529
+				// Destroy circular references to PHP will GC the object.
530
+				$vcard->destroy();
531
+
532
+				return true;
533
+			}
534
+			if ('allof' === $test && !$success) {
535
+				// Destroy circular references to PHP will GC the object.
536
+				$vcard->destroy();
537
+
538
+				return false;
539
+			}
540
+		} // foreach
541
+
542
+		// Destroy circular references to PHP will GC the object.
543
+		$vcard->destroy();
544
+
545
+		// If we got all the way here, it means we haven't been able to
546
+		// determine early if the test failed or not.
547
+		//
548
+		// This implies for 'anyof' that the test failed, and for 'allof' that
549
+		// we succeeded. Sounds weird, but makes sense.
550
+		return 'allof' === $test;
551
+	}
552
+
553
+	/**
554
+	 * Validates if a param-filter can be applied to a specific property.
555
+	 *
556
+	 * @todo currently we're only validating the first parameter of the passed
557
+	 *       property. Any subsequence parameters with the same name are
558
+	 *       ignored.
559
+	 *
560
+	 * @param string $test
561
+	 *
562
+	 * @return bool
563
+	 */
564
+	protected function validateParamFilters(array $vProperties, array $filters, $test)
565
+	{
566
+		foreach ($filters as $filter) {
567
+			$isDefined = false;
568
+			foreach ($vProperties as $vProperty) {
569
+				$isDefined = isset($vProperty[$filter['name']]);
570
+				if ($isDefined) {
571
+					break;
572
+				}
573
+			}
574
+
575
+			if ($filter['is-not-defined']) {
576
+				if ($isDefined) {
577
+					$success = false;
578
+				} else {
579
+					$success = true;
580
+				}
581
+
582
+				// If there's no text-match, we can just check for existence
583
+			} elseif (!$filter['text-match'] || !$isDefined) {
584
+				$success = $isDefined;
585
+			} else {
586
+				$success = false;
587
+				foreach ($vProperties as $vProperty) {
588
+					// If we got all the way here, we'll need to validate the
589
+					// text-match filter.
590
+					if (isset($vProperty[$filter['name']])) {
591
+						$success = DAV\StringUtil::textMatch(
592
+							$vProperty[$filter['name']]->getValue(),
593
+							$filter['text-match']['value'],
594
+							$filter['text-match']['collation'],
595
+							$filter['text-match']['match-type']
596
+						);
597
+						if ($filter['text-match']['negate-condition']) {
598
+							$success = !$success;
599
+						}
600
+					}
601
+					if ($success) {
602
+						break;
603
+					}
604
+				}
605
+			} // else
606
+
607
+			// There are two conditions where we can already determine whether
608
+			// or not this filter succeeds.
609
+			if ('anyof' === $test && $success) {
610
+				return true;
611
+			}
612
+			if ('allof' === $test && !$success) {
613
+				return false;
614
+			}
615
+		}
616
+
617
+		// If we got all the way here, it means we haven't been able to
618
+		// determine early if the test failed or not.
619
+		//
620
+		// This implies for 'anyof' that the test failed, and for 'allof' that
621
+		// we succeeded. Sounds weird, but makes sense.
622
+		return 'allof' === $test;
623
+	}
624
+
625
+	/**
626
+	 * Validates if a text-filter can be applied to a specific property.
627
+	 *
628
+	 * @param string $test
629
+	 *
630
+	 * @return bool
631
+	 */
632
+	protected function validateTextMatches(array $texts, array $filters, $test)
633
+	{
634
+		foreach ($filters as $filter) {
635
+			$success = false;
636
+			foreach ($texts as $haystack) {
637
+				$success = DAV\StringUtil::textMatch($haystack, $filter['value'], $filter['collation'], $filter['match-type']);
638
+				if ($filter['negate-condition']) {
639
+					$success = !$success;
640
+				}
641
+
642
+				// Breaking on the first match
643
+				if ($success) {
644
+					break;
645
+				}
646
+			}
647
+
648
+			if ($success && 'anyof' === $test) {
649
+				return true;
650
+			}
651
+
652
+			if (!$success && 'allof' == $test) {
653
+				return false;
654
+			}
655
+		}
656
+
657
+		// If we got all the way here, it means we haven't been able to
658
+		// determine early if the test failed or not.
659
+		//
660
+		// This implies for 'anyof' that the test failed, and for 'allof' that
661
+		// we succeeded. Sounds weird, but makes sense.
662
+		return 'allof' === $test;
663
+	}
664
+
665
+	/**
666
+	 * This event is triggered when fetching properties.
667
+	 *
668
+	 * This event is scheduled late in the process, after most work for
669
+	 * propfind has been done.
670
+	 */
671
+	public function propFindLate(DAV\PropFind $propFind, DAV\INode $node)
672
+	{
673
+		// If the request was made using the SOGO connector, we must rewrite
674
+		// the content-type property. By default SabreDAV will send back
675
+		// text/x-vcard; charset=utf-8, but for SOGO we must strip that last
676
+		// part.
677
+		if (false === strpos((string) $this->server->httpRequest->getHeader('User-Agent'), 'Thunderbird')) {
678
+			return;
679
+		}
680
+		$contentType = $propFind->get('{DAV:}getcontenttype');
681
+		if (null !== $contentType) {
682
+			list($part) = explode(';', $contentType);
683
+			if ('text/x-vcard' === $part || 'text/vcard' === $part) {
684
+				$propFind->set('{DAV:}getcontenttype', 'text/x-vcard');
685
+			}
686
+		}
687
+	}
688
+
689
+	/**
690
+	 * This method is used to generate HTML output for the
691
+	 * Sabre\DAV\Browser\Plugin. This allows us to generate an interface users
692
+	 * can use to create new addressbooks.
693
+	 *
694
+	 * @param string $output
695
+	 *
696
+	 * @return bool
697
+	 */
698
+	public function htmlActionsPanel(DAV\INode $node, &$output)
699
+	{
700
+		if (!$node instanceof AddressBookHome) {
701
+			return;
702
+		}
703
+
704
+		$output .= '<tr><td colspan="2"><form method="post" action="">
705 705
             <h3>Create new address book</h3>
706 706
             <input type="hidden" name="sabreAction" value="mkcol" />
707 707
             <input type="hidden" name="resourceType" value="{DAV:}collection,{'.self::NS_CARDDAV.'}addressbook" />
@@ -711,176 +711,176 @@  discard block
 block discarded – undo
711 711
             </form>
712 712
             </td></tr>';
713 713
 
714
-        return false;
715
-    }
716
-
717
-    /**
718
-     * This event is triggered after GET requests.
719
-     *
720
-     * This is used to transform data into jCal, if this was requested.
721
-     */
722
-    public function httpAfterGet(RequestInterface $request, ResponseInterface $response)
723
-    {
724
-        $contentType = $response->getHeader('Content-Type');
725
-        if (null === $contentType || false === strpos($contentType, 'text/vcard')) {
726
-            return;
727
-        }
728
-
729
-        $target = $this->negotiateVCard($request->getHeader('Accept'), $mimeType);
730
-
731
-        $newBody = $this->convertVCard(
732
-            $response->getBody(),
733
-            $target
734
-        );
735
-
736
-        $response->setBody($newBody);
737
-        $response->setHeader('Content-Type', $mimeType.'; charset=utf-8');
738
-        $response->setHeader('Content-Length', strlen($newBody));
739
-    }
740
-
741
-    /**
742
-     * This helper function performs the content-type negotiation for vcards.
743
-     *
744
-     * It will return one of the following strings:
745
-     * 1. vcard3
746
-     * 2. vcard4
747
-     * 3. jcard
748
-     *
749
-     * It defaults to vcard3.
750
-     *
751
-     * @param string $input
752
-     * @param string $mimeType
753
-     *
754
-     * @return string
755
-     */
756
-    protected function negotiateVCard($input, &$mimeType = null)
757
-    {
758
-        $result = HTTP\negotiateContentType(
759
-            $input,
760
-            [
761
-                // Most often used mime-type. Version 3
762
-                'text/x-vcard',
763
-                // The correct standard mime-type. Defaults to version 3 as
764
-                // well.
765
-                'text/vcard',
766
-                // vCard 4
767
-                'text/vcard; version=4.0',
768
-                // vCard 3
769
-                'text/vcard; version=3.0',
770
-                // jCard
771
-                'application/vcard+json',
772
-            ]
773
-        );
774
-
775
-        $mimeType = $result;
776
-        switch ($result) {
777
-            default:
778
-            case 'text/x-vcard':
779
-            case 'text/vcard':
780
-            case 'text/vcard; version=3.0':
781
-                $mimeType = 'text/vcard';
782
-
783
-                return 'vcard3';
784
-            case 'text/vcard; version=4.0':
785
-                return 'vcard4';
786
-            case 'application/vcard+json':
787
-                return 'jcard';
788
-
789
-        // @codeCoverageIgnoreStart
790
-        }
791
-        // @codeCoverageIgnoreEnd
792
-    }
793
-
794
-    /**
795
-     * Converts a vcard blob to a different version, or jcard.
796
-     *
797
-     * @param string|resource $data
798
-     * @param string          $target
799
-     * @param array           $propertiesFilter
800
-     *
801
-     * @return string
802
-     */
803
-    protected function convertVCard($data, $target, array $propertiesFilter = null)
804
-    {
805
-        if (is_resource($data)) {
806
-            $data = stream_get_contents($data);
807
-        }
808
-        $input = VObject\Reader::read($data);
809
-        if (!empty($propertiesFilter)) {
810
-            $propertiesFilter = array_merge(['UID', 'VERSION', 'FN'], $propertiesFilter);
811
-            $keys = array_unique(array_map(function ($child) {
812
-                return $child->name;
813
-            }, $input->children()));
814
-            $keys = array_diff($keys, $propertiesFilter);
815
-            foreach ($keys as $key) {
816
-                unset($input->$key);
817
-            }
818
-            $data = $input->serialize();
819
-        }
820
-        $output = null;
821
-        try {
822
-            switch ($target) {
823
-                default:
824
-                case 'vcard3':
825
-                    if (VObject\Document::VCARD30 === $input->getDocumentType()) {
826
-                        // Do nothing
827
-                        return $data;
828
-                    }
829
-                    $output = $input->convert(VObject\Document::VCARD30);
830
-
831
-                    return $output->serialize();
832
-                case 'vcard4':
833
-                    if (VObject\Document::VCARD40 === $input->getDocumentType()) {
834
-                        // Do nothing
835
-                        return $data;
836
-                    }
837
-                    $output = $input->convert(VObject\Document::VCARD40);
838
-
839
-                    return $output->serialize();
840
-                case 'jcard':
841
-                    $output = $input->convert(VObject\Document::VCARD40);
842
-
843
-                    return json_encode($output);
844
-            }
845
-        } finally {
846
-            // Destroy circular references to PHP will GC the object.
847
-            $input->destroy();
848
-            if (!is_null($output)) {
849
-                $output->destroy();
850
-            }
851
-        }
852
-    }
853
-
854
-    /**
855
-     * Returns a plugin name.
856
-     *
857
-     * Using this name other plugins will be able to access other plugins
858
-     * using DAV\Server::getPlugin
859
-     *
860
-     * @return string
861
-     */
862
-    public function getPluginName()
863
-    {
864
-        return 'carddav';
865
-    }
866
-
867
-    /**
868
-     * Returns a bunch of meta-data about the plugin.
869
-     *
870
-     * Providing this information is optional, and is mainly displayed by the
871
-     * Browser plugin.
872
-     *
873
-     * The description key in the returned array may contain html and will not
874
-     * be sanitized.
875
-     *
876
-     * @return array
877
-     */
878
-    public function getPluginInfo()
879
-    {
880
-        return [
881
-            'name' => $this->getPluginName(),
882
-            'description' => 'Adds support for CardDAV (rfc6352)',
883
-            'link' => 'http://sabre.io/dav/carddav/',
884
-        ];
885
-    }
714
+		return false;
715
+	}
716
+
717
+	/**
718
+	 * This event is triggered after GET requests.
719
+	 *
720
+	 * This is used to transform data into jCal, if this was requested.
721
+	 */
722
+	public function httpAfterGet(RequestInterface $request, ResponseInterface $response)
723
+	{
724
+		$contentType = $response->getHeader('Content-Type');
725
+		if (null === $contentType || false === strpos($contentType, 'text/vcard')) {
726
+			return;
727
+		}
728
+
729
+		$target = $this->negotiateVCard($request->getHeader('Accept'), $mimeType);
730
+
731
+		$newBody = $this->convertVCard(
732
+			$response->getBody(),
733
+			$target
734
+		);
735
+
736
+		$response->setBody($newBody);
737
+		$response->setHeader('Content-Type', $mimeType.'; charset=utf-8');
738
+		$response->setHeader('Content-Length', strlen($newBody));
739
+	}
740
+
741
+	/**
742
+	 * This helper function performs the content-type negotiation for vcards.
743
+	 *
744
+	 * It will return one of the following strings:
745
+	 * 1. vcard3
746
+	 * 2. vcard4
747
+	 * 3. jcard
748
+	 *
749
+	 * It defaults to vcard3.
750
+	 *
751
+	 * @param string $input
752
+	 * @param string $mimeType
753
+	 *
754
+	 * @return string
755
+	 */
756
+	protected function negotiateVCard($input, &$mimeType = null)
757
+	{
758
+		$result = HTTP\negotiateContentType(
759
+			$input,
760
+			[
761
+				// Most often used mime-type. Version 3
762
+				'text/x-vcard',
763
+				// The correct standard mime-type. Defaults to version 3 as
764
+				// well.
765
+				'text/vcard',
766
+				// vCard 4
767
+				'text/vcard; version=4.0',
768
+				// vCard 3
769
+				'text/vcard; version=3.0',
770
+				// jCard
771
+				'application/vcard+json',
772
+			]
773
+		);
774
+
775
+		$mimeType = $result;
776
+		switch ($result) {
777
+			default:
778
+			case 'text/x-vcard':
779
+			case 'text/vcard':
780
+			case 'text/vcard; version=3.0':
781
+				$mimeType = 'text/vcard';
782
+
783
+				return 'vcard3';
784
+			case 'text/vcard; version=4.0':
785
+				return 'vcard4';
786
+			case 'application/vcard+json':
787
+				return 'jcard';
788
+
789
+		// @codeCoverageIgnoreStart
790
+		}
791
+		// @codeCoverageIgnoreEnd
792
+	}
793
+
794
+	/**
795
+	 * Converts a vcard blob to a different version, or jcard.
796
+	 *
797
+	 * @param string|resource $data
798
+	 * @param string          $target
799
+	 * @param array           $propertiesFilter
800
+	 *
801
+	 * @return string
802
+	 */
803
+	protected function convertVCard($data, $target, array $propertiesFilter = null)
804
+	{
805
+		if (is_resource($data)) {
806
+			$data = stream_get_contents($data);
807
+		}
808
+		$input = VObject\Reader::read($data);
809
+		if (!empty($propertiesFilter)) {
810
+			$propertiesFilter = array_merge(['UID', 'VERSION', 'FN'], $propertiesFilter);
811
+			$keys = array_unique(array_map(function ($child) {
812
+				return $child->name;
813
+			}, $input->children()));
814
+			$keys = array_diff($keys, $propertiesFilter);
815
+			foreach ($keys as $key) {
816
+				unset($input->$key);
817
+			}
818
+			$data = $input->serialize();
819
+		}
820
+		$output = null;
821
+		try {
822
+			switch ($target) {
823
+				default:
824
+				case 'vcard3':
825
+					if (VObject\Document::VCARD30 === $input->getDocumentType()) {
826
+						// Do nothing
827
+						return $data;
828
+					}
829
+					$output = $input->convert(VObject\Document::VCARD30);
830
+
831
+					return $output->serialize();
832
+				case 'vcard4':
833
+					if (VObject\Document::VCARD40 === $input->getDocumentType()) {
834
+						// Do nothing
835
+						return $data;
836
+					}
837
+					$output = $input->convert(VObject\Document::VCARD40);
838
+
839
+					return $output->serialize();
840
+				case 'jcard':
841
+					$output = $input->convert(VObject\Document::VCARD40);
842
+
843
+					return json_encode($output);
844
+			}
845
+		} finally {
846
+			// Destroy circular references to PHP will GC the object.
847
+			$input->destroy();
848
+			if (!is_null($output)) {
849
+				$output->destroy();
850
+			}
851
+		}
852
+	}
853
+
854
+	/**
855
+	 * Returns a plugin name.
856
+	 *
857
+	 * Using this name other plugins will be able to access other plugins
858
+	 * using DAV\Server::getPlugin
859
+	 *
860
+	 * @return string
861
+	 */
862
+	public function getPluginName()
863
+	{
864
+		return 'carddav';
865
+	}
866
+
867
+	/**
868
+	 * Returns a bunch of meta-data about the plugin.
869
+	 *
870
+	 * Providing this information is optional, and is mainly displayed by the
871
+	 * Browser plugin.
872
+	 *
873
+	 * The description key in the returned array may contain html and will not
874
+	 * be sanitized.
875
+	 *
876
+	 * @return array
877
+	 */
878
+	public function getPluginInfo()
879
+	{
880
+		return [
881
+			'name' => $this->getPluginName(),
882
+			'description' => 'Adds support for CardDAV (rfc6352)',
883
+			'link' => 'http://sabre.io/dav/carddav/',
884
+		];
885
+	}
886 886
 }
Please login to merge, or discard this patch.
Spacing   +6 added lines, -6 removed lines patch added patch discarded remove patch
@@ -136,22 +136,22 @@  discard block
 block discarded – undo
136 136
 
137 137
         if ($node instanceof IAddressBook) {
138 138
             $propFind->handle($ns.'max-resource-size', $this->maxResourceSize);
139
-            $propFind->handle($ns.'supported-address-data', function () {
139
+            $propFind->handle($ns.'supported-address-data', function() {
140 140
                 return new Xml\Property\SupportedAddressData();
141 141
             });
142
-            $propFind->handle($ns.'supported-collation-set', function () {
142
+            $propFind->handle($ns.'supported-collation-set', function() {
143 143
                 return new Xml\Property\SupportedCollationSet();
144 144
             });
145 145
         }
146 146
         if ($node instanceof DAVACL\IPrincipal) {
147 147
             $path = $propFind->getPath();
148 148
 
149
-            $propFind->handle('{'.self::NS_CARDDAV.'}addressbook-home-set', function () use ($path) {
149
+            $propFind->handle('{'.self::NS_CARDDAV.'}addressbook-home-set', function() use ($path) {
150 150
                 return new LocalHref($this->getAddressBookHomeForPrincipal($path).'/');
151 151
             });
152 152
 
153 153
             if ($this->directories) {
154
-                $propFind->handle('{'.self::NS_CARDDAV.'}directory-gateway', function () {
154
+                $propFind->handle('{'.self::NS_CARDDAV.'}directory-gateway', function() {
155 155
                     return new LocalHref($this->directories);
156 156
                 });
157 157
             }
@@ -161,7 +161,7 @@  discard block
 block discarded – undo
161 161
             // The address-data property is not supposed to be a 'real'
162 162
             // property, but in large chunks of the spec it does act as such.
163 163
             // Therefore we simply expose it as a property.
164
-            $propFind->handle('{'.self::NS_CARDDAV.'}address-data', function () use ($node) {
164
+            $propFind->handle('{'.self::NS_CARDDAV.'}address-data', function() use ($node) {
165 165
                 $val = $node->get();
166 166
                 if (is_resource($val)) {
167 167
                     $val = stream_get_contents($val);
@@ -808,7 +808,7 @@  discard block
 block discarded – undo
808 808
         $input = VObject\Reader::read($data);
809 809
         if (!empty($propertiesFilter)) {
810 810
             $propertiesFilter = array_merge(['UID', 'VERSION', 'FN'], $propertiesFilter);
811
-            $keys = array_unique(array_map(function ($child) {
811
+            $keys = array_unique(array_map(function($child) {
812 812
                 return $child->name;
813 813
             }, $input->children()));
814 814
             $keys = array_diff($keys, $propertiesFilter);
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/dav/lib/CardDAV/Backend/BackendInterface.php 1 patch
Indentation   +163 added lines, -163 removed lines patch added patch discarded remove patch
@@ -19,176 +19,176 @@
 block discarded – undo
19 19
  */
20 20
 interface BackendInterface
21 21
 {
22
-    /**
23
-     * Returns the list of addressbooks for a specific user.
24
-     *
25
-     * Every addressbook should have the following properties:
26
-     *   id - an arbitrary unique id
27
-     *   uri - the 'basename' part of the url
28
-     *   principaluri - Same as the passed parameter
29
-     *
30
-     * Any additional clark-notation property may be passed besides this. Some
31
-     * common ones are :
32
-     *   {DAV:}displayname
33
-     *   {urn:ietf:params:xml:ns:carddav}addressbook-description
34
-     *   {http://calendarserver.org/ns/}getctag
35
-     *
36
-     * @param string $principalUri
37
-     *
38
-     * @return array
39
-     */
40
-    public function getAddressBooksForUser($principalUri);
22
+	/**
23
+	 * Returns the list of addressbooks for a specific user.
24
+	 *
25
+	 * Every addressbook should have the following properties:
26
+	 *   id - an arbitrary unique id
27
+	 *   uri - the 'basename' part of the url
28
+	 *   principaluri - Same as the passed parameter
29
+	 *
30
+	 * Any additional clark-notation property may be passed besides this. Some
31
+	 * common ones are :
32
+	 *   {DAV:}displayname
33
+	 *   {urn:ietf:params:xml:ns:carddav}addressbook-description
34
+	 *   {http://calendarserver.org/ns/}getctag
35
+	 *
36
+	 * @param string $principalUri
37
+	 *
38
+	 * @return array
39
+	 */
40
+	public function getAddressBooksForUser($principalUri);
41 41
 
42
-    /**
43
-     * Updates properties for an address book.
44
-     *
45
-     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
46
-     * To do the actual updates, you must tell this object which properties
47
-     * you're going to process with the handle() method.
48
-     *
49
-     * Calling the handle method is like telling the PropPatch object "I
50
-     * promise I can handle updating this property".
51
-     *
52
-     * Read the PropPatch documentation for more info and examples.
53
-     *
54
-     * @param string $addressBookId
55
-     */
56
-    public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch);
42
+	/**
43
+	 * Updates properties for an address book.
44
+	 *
45
+	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
46
+	 * To do the actual updates, you must tell this object which properties
47
+	 * you're going to process with the handle() method.
48
+	 *
49
+	 * Calling the handle method is like telling the PropPatch object "I
50
+	 * promise I can handle updating this property".
51
+	 *
52
+	 * Read the PropPatch documentation for more info and examples.
53
+	 *
54
+	 * @param string $addressBookId
55
+	 */
56
+	public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch);
57 57
 
58
-    /**
59
-     * Creates a new address book.
60
-     *
61
-     * This method should return the id of the new address book. The id can be
62
-     * in any format, including ints, strings, arrays or objects.
63
-     *
64
-     * @param string $principalUri
65
-     * @param string $url          just the 'basename' of the url
66
-     *
67
-     * @return mixed
68
-     */
69
-    public function createAddressBook($principalUri, $url, array $properties);
58
+	/**
59
+	 * Creates a new address book.
60
+	 *
61
+	 * This method should return the id of the new address book. The id can be
62
+	 * in any format, including ints, strings, arrays or objects.
63
+	 *
64
+	 * @param string $principalUri
65
+	 * @param string $url          just the 'basename' of the url
66
+	 *
67
+	 * @return mixed
68
+	 */
69
+	public function createAddressBook($principalUri, $url, array $properties);
70 70
 
71
-    /**
72
-     * Deletes an entire addressbook and all its contents.
73
-     *
74
-     * @param mixed $addressBookId
75
-     */
76
-    public function deleteAddressBook($addressBookId);
71
+	/**
72
+	 * Deletes an entire addressbook and all its contents.
73
+	 *
74
+	 * @param mixed $addressBookId
75
+	 */
76
+	public function deleteAddressBook($addressBookId);
77 77
 
78
-    /**
79
-     * Returns all cards for a specific addressbook id.
80
-     *
81
-     * This method should return the following properties for each card:
82
-     *   * carddata - raw vcard data
83
-     *   * uri - Some unique url
84
-     *   * lastmodified - A unix timestamp
85
-     *
86
-     * It's recommended to also return the following properties:
87
-     *   * etag - A unique etag. This must change every time the card changes.
88
-     *   * size - The size of the card in bytes.
89
-     *
90
-     * If these last two properties are provided, less time will be spent
91
-     * calculating them. If they are specified, you can also ommit carddata.
92
-     * This may speed up certain requests, especially with large cards.
93
-     *
94
-     * @param mixed $addressbookId
95
-     *
96
-     * @return array
97
-     */
98
-    public function getCards($addressbookId);
78
+	/**
79
+	 * Returns all cards for a specific addressbook id.
80
+	 *
81
+	 * This method should return the following properties for each card:
82
+	 *   * carddata - raw vcard data
83
+	 *   * uri - Some unique url
84
+	 *   * lastmodified - A unix timestamp
85
+	 *
86
+	 * It's recommended to also return the following properties:
87
+	 *   * etag - A unique etag. This must change every time the card changes.
88
+	 *   * size - The size of the card in bytes.
89
+	 *
90
+	 * If these last two properties are provided, less time will be spent
91
+	 * calculating them. If they are specified, you can also ommit carddata.
92
+	 * This may speed up certain requests, especially with large cards.
93
+	 *
94
+	 * @param mixed $addressbookId
95
+	 *
96
+	 * @return array
97
+	 */
98
+	public function getCards($addressbookId);
99 99
 
100
-    /**
101
-     * Returns a specfic card.
102
-     *
103
-     * The same set of properties must be returned as with getCards. The only
104
-     * exception is that 'carddata' is absolutely required.
105
-     *
106
-     * If the card does not exist, you must return false.
107
-     *
108
-     * @param mixed  $addressBookId
109
-     * @param string $cardUri
110
-     *
111
-     * @return array
112
-     */
113
-    public function getCard($addressBookId, $cardUri);
100
+	/**
101
+	 * Returns a specfic card.
102
+	 *
103
+	 * The same set of properties must be returned as with getCards. The only
104
+	 * exception is that 'carddata' is absolutely required.
105
+	 *
106
+	 * If the card does not exist, you must return false.
107
+	 *
108
+	 * @param mixed  $addressBookId
109
+	 * @param string $cardUri
110
+	 *
111
+	 * @return array
112
+	 */
113
+	public function getCard($addressBookId, $cardUri);
114 114
 
115
-    /**
116
-     * Returns a list of cards.
117
-     *
118
-     * This method should work identical to getCard, but instead return all the
119
-     * cards in the list as an array.
120
-     *
121
-     * If the backend supports this, it may allow for some speed-ups.
122
-     *
123
-     * @param mixed $addressBookId
124
-     *
125
-     * @return array
126
-     */
127
-    public function getMultipleCards($addressBookId, array $uris);
115
+	/**
116
+	 * Returns a list of cards.
117
+	 *
118
+	 * This method should work identical to getCard, but instead return all the
119
+	 * cards in the list as an array.
120
+	 *
121
+	 * If the backend supports this, it may allow for some speed-ups.
122
+	 *
123
+	 * @param mixed $addressBookId
124
+	 *
125
+	 * @return array
126
+	 */
127
+	public function getMultipleCards($addressBookId, array $uris);
128 128
 
129
-    /**
130
-     * Creates a new card.
131
-     *
132
-     * The addressbook id will be passed as the first argument. This is the
133
-     * same id as it is returned from the getAddressBooksForUser method.
134
-     *
135
-     * The cardUri is a base uri, and doesn't include the full path. The
136
-     * cardData argument is the vcard body, and is passed as a string.
137
-     *
138
-     * It is possible to return an ETag from this method. This ETag is for the
139
-     * newly created resource, and must be enclosed with double quotes (that
140
-     * is, the string itself must contain the double quotes).
141
-     *
142
-     * You should only return the ETag if you store the carddata as-is. If a
143
-     * subsequent GET request on the same card does not have the same body,
144
-     * byte-by-byte and you did return an ETag here, clients tend to get
145
-     * confused.
146
-     *
147
-     * If you don't return an ETag, you can just return null.
148
-     *
149
-     * @param mixed  $addressBookId
150
-     * @param string $cardUri
151
-     * @param string $cardData
152
-     *
153
-     * @return string|null
154
-     */
155
-    public function createCard($addressBookId, $cardUri, $cardData);
129
+	/**
130
+	 * Creates a new card.
131
+	 *
132
+	 * The addressbook id will be passed as the first argument. This is the
133
+	 * same id as it is returned from the getAddressBooksForUser method.
134
+	 *
135
+	 * The cardUri is a base uri, and doesn't include the full path. The
136
+	 * cardData argument is the vcard body, and is passed as a string.
137
+	 *
138
+	 * It is possible to return an ETag from this method. This ETag is for the
139
+	 * newly created resource, and must be enclosed with double quotes (that
140
+	 * is, the string itself must contain the double quotes).
141
+	 *
142
+	 * You should only return the ETag if you store the carddata as-is. If a
143
+	 * subsequent GET request on the same card does not have the same body,
144
+	 * byte-by-byte and you did return an ETag here, clients tend to get
145
+	 * confused.
146
+	 *
147
+	 * If you don't return an ETag, you can just return null.
148
+	 *
149
+	 * @param mixed  $addressBookId
150
+	 * @param string $cardUri
151
+	 * @param string $cardData
152
+	 *
153
+	 * @return string|null
154
+	 */
155
+	public function createCard($addressBookId, $cardUri, $cardData);
156 156
 
157
-    /**
158
-     * Updates a card.
159
-     *
160
-     * The addressbook id will be passed as the first argument. This is the
161
-     * same id as it is returned from the getAddressBooksForUser method.
162
-     *
163
-     * The cardUri is a base uri, and doesn't include the full path. The
164
-     * cardData argument is the vcard body, and is passed as a string.
165
-     *
166
-     * It is possible to return an ETag from this method. This ETag should
167
-     * match that of the updated resource, and must be enclosed with double
168
-     * quotes (that is: the string itself must contain the actual quotes).
169
-     *
170
-     * You should only return the ETag if you store the carddata as-is. If a
171
-     * subsequent GET request on the same card does not have the same body,
172
-     * byte-by-byte and you did return an ETag here, clients tend to get
173
-     * confused.
174
-     *
175
-     * If you don't return an ETag, you can just return null.
176
-     *
177
-     * @param mixed  $addressBookId
178
-     * @param string $cardUri
179
-     * @param string $cardData
180
-     *
181
-     * @return string|null
182
-     */
183
-    public function updateCard($addressBookId, $cardUri, $cardData);
157
+	/**
158
+	 * Updates a card.
159
+	 *
160
+	 * The addressbook id will be passed as the first argument. This is the
161
+	 * same id as it is returned from the getAddressBooksForUser method.
162
+	 *
163
+	 * The cardUri is a base uri, and doesn't include the full path. The
164
+	 * cardData argument is the vcard body, and is passed as a string.
165
+	 *
166
+	 * It is possible to return an ETag from this method. This ETag should
167
+	 * match that of the updated resource, and must be enclosed with double
168
+	 * quotes (that is: the string itself must contain the actual quotes).
169
+	 *
170
+	 * You should only return the ETag if you store the carddata as-is. If a
171
+	 * subsequent GET request on the same card does not have the same body,
172
+	 * byte-by-byte and you did return an ETag here, clients tend to get
173
+	 * confused.
174
+	 *
175
+	 * If you don't return an ETag, you can just return null.
176
+	 *
177
+	 * @param mixed  $addressBookId
178
+	 * @param string $cardUri
179
+	 * @param string $cardData
180
+	 *
181
+	 * @return string|null
182
+	 */
183
+	public function updateCard($addressBookId, $cardUri, $cardData);
184 184
 
185
-    /**
186
-     * Deletes a card.
187
-     *
188
-     * @param mixed  $addressBookId
189
-     * @param string $cardUri
190
-     *
191
-     * @return bool
192
-     */
193
-    public function deleteCard($addressBookId, $cardUri);
185
+	/**
186
+	 * Deletes a card.
187
+	 *
188
+	 * @param mixed  $addressBookId
189
+	 * @param string $cardUri
190
+	 *
191
+	 * @return bool
192
+	 */
193
+	public function deleteCard($addressBookId, $cardUri);
194 194
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php 2 patches
Indentation   +18 added lines, -18 removed lines patch added patch discarded remove patch
@@ -17,22 +17,22 @@
 block discarded – undo
17 17
  */
18 18
 abstract class AbstractBackend implements BackendInterface
19 19
 {
20
-    /**
21
-     * Returns a list of cards.
22
-     *
23
-     * This method should work identical to getCard, but instead return all the
24
-     * cards in the list as an array.
25
-     *
26
-     * If the backend supports this, it may allow for some speed-ups.
27
-     *
28
-     * @param mixed $addressBookId
29
-     *
30
-     * @return array
31
-     */
32
-    public function getMultipleCards($addressBookId, array $uris)
33
-    {
34
-        return array_map(function ($uri) use ($addressBookId) {
35
-            return $this->getCard($addressBookId, $uri);
36
-        }, $uris);
37
-    }
20
+	/**
21
+	 * Returns a list of cards.
22
+	 *
23
+	 * This method should work identical to getCard, but instead return all the
24
+	 * cards in the list as an array.
25
+	 *
26
+	 * If the backend supports this, it may allow for some speed-ups.
27
+	 *
28
+	 * @param mixed $addressBookId
29
+	 *
30
+	 * @return array
31
+	 */
32
+	public function getMultipleCards($addressBookId, array $uris)
33
+	{
34
+		return array_map(function ($uri) use ($addressBookId) {
35
+			return $this->getCard($addressBookId, $uri);
36
+		}, $uris);
37
+	}
38 38
 }
Please login to merge, or discard this patch.
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -31,7 +31,7 @@
 block discarded – undo
31 31
      */
32 32
     public function getMultipleCards($addressBookId, array $uris)
33 33
     {
34
-        return array_map(function ($uri) use ($addressBookId) {
34
+        return array_map(function($uri) use ($addressBookId) {
35 35
             return $this->getCard($addressBookId, $uri);
36 36
         }, $uris);
37 37
     }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/dav/lib/CardDAV/Backend/PDO.php 2 patches
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -101,7 +101,7 @@
 block discarded – undo
101 101
             '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description',
102 102
         ];
103 103
 
104
-        $propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
104
+        $propPatch->handle($supportedProperties, function($mutations) use ($addressBookId) {
105 105
             $updates = [];
106 106
             foreach ($mutations as $property => $newValue) {
107 107
                 switch ($property) {
Please login to merge, or discard this patch.
Indentation   +517 added lines, -517 removed lines patch added patch discarded remove patch
@@ -19,521 +19,521 @@
 block discarded – undo
19 19
  */
20 20
 class PDO extends AbstractBackend implements SyncSupport
21 21
 {
22
-    /**
23
-     * PDO connection.
24
-     *
25
-     * @var PDO
26
-     */
27
-    protected $pdo;
28
-
29
-    /**
30
-     * The PDO table name used to store addressbooks.
31
-     */
32
-    public $addressBooksTableName = 'addressbooks';
33
-
34
-    /**
35
-     * The PDO table name used to store cards.
36
-     */
37
-    public $cardsTableName = 'cards';
38
-
39
-    /**
40
-     * The table name that will be used for tracking changes in address books.
41
-     *
42
-     * @var string
43
-     */
44
-    public $addressBookChangesTableName = 'addressbookchanges';
45
-
46
-    /**
47
-     * Sets up the object.
48
-     */
49
-    public function __construct(\PDO $pdo)
50
-    {
51
-        $this->pdo = $pdo;
52
-    }
53
-
54
-    /**
55
-     * Returns the list of addressbooks for a specific user.
56
-     *
57
-     * @param string $principalUri
58
-     *
59
-     * @return array
60
-     */
61
-    public function getAddressBooksForUser($principalUri)
62
-    {
63
-        $stmt = $this->pdo->prepare('SELECT id, uri, displayname, principaluri, description, synctoken FROM '.$this->addressBooksTableName.' WHERE principaluri = ?');
64
-        $stmt->execute([$principalUri]);
65
-
66
-        $addressBooks = [];
67
-
68
-        foreach ($stmt->fetchAll() as $row) {
69
-            $addressBooks[] = [
70
-                'id' => $row['id'],
71
-                'uri' => $row['uri'],
72
-                'principaluri' => $row['principaluri'],
73
-                '{DAV:}displayname' => $row['displayname'],
74
-                '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'],
75
-                '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
76
-                '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
77
-            ];
78
-        }
79
-
80
-        return $addressBooks;
81
-    }
82
-
83
-    /**
84
-     * Updates properties for an address book.
85
-     *
86
-     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
87
-     * To do the actual updates, you must tell this object which properties
88
-     * you're going to process with the handle() method.
89
-     *
90
-     * Calling the handle method is like telling the PropPatch object "I
91
-     * promise I can handle updating this property".
92
-     *
93
-     * Read the PropPatch documentation for more info and examples.
94
-     *
95
-     * @param string $addressBookId
96
-     */
97
-    public function updateAddressBook($addressBookId, PropPatch $propPatch)
98
-    {
99
-        $supportedProperties = [
100
-            '{DAV:}displayname',
101
-            '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description',
102
-        ];
103
-
104
-        $propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
105
-            $updates = [];
106
-            foreach ($mutations as $property => $newValue) {
107
-                switch ($property) {
108
-                    case '{DAV:}displayname':
109
-                        $updates['displayname'] = $newValue;
110
-                        break;
111
-                    case '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description':
112
-                        $updates['description'] = $newValue;
113
-                        break;
114
-                }
115
-            }
116
-            $query = 'UPDATE '.$this->addressBooksTableName.' SET ';
117
-            $first = true;
118
-            foreach ($updates as $key => $value) {
119
-                if ($first) {
120
-                    $first = false;
121
-                } else {
122
-                    $query .= ', ';
123
-                }
124
-                $query .= ' '.$key.' = :'.$key.' ';
125
-            }
126
-            $query .= ' WHERE id = :addressbookid';
127
-
128
-            $stmt = $this->pdo->prepare($query);
129
-            $updates['addressbookid'] = $addressBookId;
130
-
131
-            $stmt->execute($updates);
132
-
133
-            $this->addChange($addressBookId, '', 2);
134
-
135
-            return true;
136
-        });
137
-    }
138
-
139
-    /**
140
-     * Creates a new address book.
141
-     *
142
-     * @param string $principalUri
143
-     * @param string $url          just the 'basename' of the url
144
-     *
145
-     * @return int Last insert id
146
-     */
147
-    public function createAddressBook($principalUri, $url, array $properties)
148
-    {
149
-        $values = [
150
-            'displayname' => null,
151
-            'description' => null,
152
-            'principaluri' => $principalUri,
153
-            'uri' => $url,
154
-        ];
155
-
156
-        foreach ($properties as $property => $newValue) {
157
-            switch ($property) {
158
-                case '{DAV:}displayname':
159
-                    $values['displayname'] = $newValue;
160
-                    break;
161
-                case '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description':
162
-                    $values['description'] = $newValue;
163
-                    break;
164
-                default:
165
-                    throw new DAV\Exception\BadRequest('Unknown property: '.$property);
166
-            }
167
-        }
168
-
169
-        $query = 'INSERT INTO '.$this->addressBooksTableName.' (uri, displayname, description, principaluri, synctoken) VALUES (:uri, :displayname, :description, :principaluri, 1)';
170
-        $stmt = $this->pdo->prepare($query);
171
-        $stmt->execute($values);
172
-
173
-        return $this->pdo->lastInsertId(
174
-            $this->addressBooksTableName.'_id_seq'
175
-        );
176
-    }
177
-
178
-    /**
179
-     * Deletes an entire addressbook and all its contents.
180
-     *
181
-     * @param int $addressBookId
182
-     */
183
-    public function deleteAddressBook($addressBookId)
184
-    {
185
-        $stmt = $this->pdo->prepare('DELETE FROM '.$this->cardsTableName.' WHERE addressbookid = ?');
186
-        $stmt->execute([$addressBookId]);
187
-
188
-        $stmt = $this->pdo->prepare('DELETE FROM '.$this->addressBooksTableName.' WHERE id = ?');
189
-        $stmt->execute([$addressBookId]);
190
-
191
-        $stmt = $this->pdo->prepare('DELETE FROM '.$this->addressBookChangesTableName.' WHERE addressbookid = ?');
192
-        $stmt->execute([$addressBookId]);
193
-    }
194
-
195
-    /**
196
-     * Returns all cards for a specific addressbook id.
197
-     *
198
-     * This method should return the following properties for each card:
199
-     *   * carddata - raw vcard data
200
-     *   * uri - Some unique url
201
-     *   * lastmodified - A unix timestamp
202
-     *
203
-     * It's recommended to also return the following properties:
204
-     *   * etag - A unique etag. This must change every time the card changes.
205
-     *   * size - The size of the card in bytes.
206
-     *
207
-     * If these last two properties are provided, less time will be spent
208
-     * calculating them. If they are specified, you can also ommit carddata.
209
-     * This may speed up certain requests, especially with large cards.
210
-     *
211
-     * @param mixed $addressbookId
212
-     *
213
-     * @return array
214
-     */
215
-    public function getCards($addressbookId)
216
-    {
217
-        $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, size FROM '.$this->cardsTableName.' WHERE addressbookid = ?');
218
-        $stmt->execute([$addressbookId]);
219
-
220
-        $result = [];
221
-        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
222
-            $row['etag'] = '"'.$row['etag'].'"';
223
-            $row['lastmodified'] = (int) $row['lastmodified'];
224
-            $result[] = $row;
225
-        }
226
-
227
-        return $result;
228
-    }
229
-
230
-    /**
231
-     * Returns a specific card.
232
-     *
233
-     * The same set of properties must be returned as with getCards. The only
234
-     * exception is that 'carddata' is absolutely required.
235
-     *
236
-     * If the card does not exist, you must return false.
237
-     *
238
-     * @param mixed  $addressBookId
239
-     * @param string $cardUri
240
-     *
241
-     * @return array
242
-     */
243
-    public function getCard($addressBookId, $cardUri)
244
-    {
245
-        $stmt = $this->pdo->prepare('SELECT id, carddata, uri, lastmodified, etag, size FROM '.$this->cardsTableName.' WHERE addressbookid = ? AND uri = ? LIMIT 1');
246
-        $stmt->execute([$addressBookId, $cardUri]);
247
-
248
-        $result = $stmt->fetch(\PDO::FETCH_ASSOC);
249
-
250
-        if (!$result) {
251
-            return false;
252
-        }
253
-
254
-        $result['etag'] = '"'.$result['etag'].'"';
255
-        $result['lastmodified'] = (int) $result['lastmodified'];
256
-
257
-        return $result;
258
-    }
259
-
260
-    /**
261
-     * Returns a list of cards.
262
-     *
263
-     * This method should work identical to getCard, but instead return all the
264
-     * cards in the list as an array.
265
-     *
266
-     * If the backend supports this, it may allow for some speed-ups.
267
-     *
268
-     * @param mixed $addressBookId
269
-     *
270
-     * @return array
271
-     */
272
-    public function getMultipleCards($addressBookId, array $uris)
273
-    {
274
-        $query = 'SELECT id, uri, lastmodified, etag, size, carddata FROM '.$this->cardsTableName.' WHERE addressbookid = ? AND uri IN (';
275
-        // Inserting a whole bunch of question marks
276
-        $query .= implode(',', array_fill(0, count($uris), '?'));
277
-        $query .= ')';
278
-
279
-        $stmt = $this->pdo->prepare($query);
280
-        $stmt->execute(array_merge([$addressBookId], $uris));
281
-        $result = [];
282
-        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
283
-            $row['etag'] = '"'.$row['etag'].'"';
284
-            $row['lastmodified'] = (int) $row['lastmodified'];
285
-            $result[] = $row;
286
-        }
287
-
288
-        return $result;
289
-    }
290
-
291
-    /**
292
-     * Creates a new card.
293
-     *
294
-     * The addressbook id will be passed as the first argument. This is the
295
-     * same id as it is returned from the getAddressBooksForUser method.
296
-     *
297
-     * The cardUri is a base uri, and doesn't include the full path. The
298
-     * cardData argument is the vcard body, and is passed as a string.
299
-     *
300
-     * It is possible to return an ETag from this method. This ETag is for the
301
-     * newly created resource, and must be enclosed with double quotes (that
302
-     * is, the string itself must contain the double quotes).
303
-     *
304
-     * You should only return the ETag if you store the carddata as-is. If a
305
-     * subsequent GET request on the same card does not have the same body,
306
-     * byte-by-byte and you did return an ETag here, clients tend to get
307
-     * confused.
308
-     *
309
-     * If you don't return an ETag, you can just return null.
310
-     *
311
-     * @param mixed  $addressBookId
312
-     * @param string $cardUri
313
-     * @param string $cardData
314
-     *
315
-     * @return string|null
316
-     */
317
-    public function createCard($addressBookId, $cardUri, $cardData)
318
-    {
319
-        $stmt = $this->pdo->prepare('INSERT INTO '.$this->cardsTableName.' (carddata, uri, lastmodified, addressbookid, size, etag) VALUES (?, ?, ?, ?, ?, ?)');
320
-
321
-        $etag = md5($cardData);
322
-
323
-        $stmt->execute([
324
-            $cardData,
325
-            $cardUri,
326
-            time(),
327
-            $addressBookId,
328
-            strlen($cardData),
329
-            $etag,
330
-        ]);
331
-
332
-        $this->addChange($addressBookId, $cardUri, 1);
333
-
334
-        return '"'.$etag.'"';
335
-    }
336
-
337
-    /**
338
-     * Updates a card.
339
-     *
340
-     * The addressbook id will be passed as the first argument. This is the
341
-     * same id as it is returned from the getAddressBooksForUser method.
342
-     *
343
-     * The cardUri is a base uri, and doesn't include the full path. The
344
-     * cardData argument is the vcard body, and is passed as a string.
345
-     *
346
-     * It is possible to return an ETag from this method. This ETag should
347
-     * match that of the updated resource, and must be enclosed with double
348
-     * quotes (that is: the string itself must contain the actual quotes).
349
-     *
350
-     * You should only return the ETag if you store the carddata as-is. If a
351
-     * subsequent GET request on the same card does not have the same body,
352
-     * byte-by-byte and you did return an ETag here, clients tend to get
353
-     * confused.
354
-     *
355
-     * If you don't return an ETag, you can just return null.
356
-     *
357
-     * @param mixed  $addressBookId
358
-     * @param string $cardUri
359
-     * @param string $cardData
360
-     *
361
-     * @return string|null
362
-     */
363
-    public function updateCard($addressBookId, $cardUri, $cardData)
364
-    {
365
-        $stmt = $this->pdo->prepare('UPDATE '.$this->cardsTableName.' SET carddata = ?, lastmodified = ?, size = ?, etag = ? WHERE uri = ? AND addressbookid =?');
366
-
367
-        $etag = md5($cardData);
368
-        $stmt->execute([
369
-            $cardData,
370
-            time(),
371
-            strlen($cardData),
372
-            $etag,
373
-            $cardUri,
374
-            $addressBookId,
375
-        ]);
376
-
377
-        $this->addChange($addressBookId, $cardUri, 2);
378
-
379
-        return '"'.$etag.'"';
380
-    }
381
-
382
-    /**
383
-     * Deletes a card.
384
-     *
385
-     * @param mixed  $addressBookId
386
-     * @param string $cardUri
387
-     *
388
-     * @return bool
389
-     */
390
-    public function deleteCard($addressBookId, $cardUri)
391
-    {
392
-        $stmt = $this->pdo->prepare('DELETE FROM '.$this->cardsTableName.' WHERE addressbookid = ? AND uri = ?');
393
-        $stmt->execute([$addressBookId, $cardUri]);
394
-
395
-        $this->addChange($addressBookId, $cardUri, 3);
396
-
397
-        return 1 === $stmt->rowCount();
398
-    }
399
-
400
-    /**
401
-     * The getChanges method returns all the changes that have happened, since
402
-     * the specified syncToken in the specified address book.
403
-     *
404
-     * This function should return an array, such as the following:
405
-     *
406
-     * [
407
-     *   'syncToken' => 'The current synctoken',
408
-     *   'added'   => [
409
-     *      'new.txt',
410
-     *   ],
411
-     *   'modified'   => [
412
-     *      'updated.txt',
413
-     *   ],
414
-     *   'deleted' => [
415
-     *      'foo.php.bak',
416
-     *      'old.txt'
417
-     *   ]
418
-     * ];
419
-     *
420
-     * The returned syncToken property should reflect the *current* syncToken
421
-     * of the addressbook, as reported in the {http://sabredav.org/ns}sync-token
422
-     * property. This is needed here too, to ensure the operation is atomic.
423
-     *
424
-     * If the $syncToken argument is specified as null, this is an initial
425
-     * sync, and all members should be reported.
426
-     *
427
-     * The modified property is an array of nodenames that have changed since
428
-     * the last token.
429
-     *
430
-     * The deleted property is an array with nodenames, that have been deleted
431
-     * from collection.
432
-     *
433
-     * The $syncLevel argument is basically the 'depth' of the report. If it's
434
-     * 1, you only have to report changes that happened only directly in
435
-     * immediate descendants. If it's 2, it should also include changes from
436
-     * the nodes below the child collections. (grandchildren)
437
-     *
438
-     * The $limit argument allows a client to specify how many results should
439
-     * be returned at most. If the limit is not specified, it should be treated
440
-     * as infinite.
441
-     *
442
-     * If the limit (infinite or not) is higher than you're willing to return,
443
-     * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
444
-     *
445
-     * If the syncToken is expired (due to data cleanup) or unknown, you must
446
-     * return null.
447
-     *
448
-     * The limit is 'suggestive'. You are free to ignore it.
449
-     *
450
-     * @param string $addressBookId
451
-     * @param string $syncToken
452
-     * @param int    $syncLevel
453
-     * @param int    $limit
454
-     *
455
-     * @return array|null
456
-     */
457
-    public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null)
458
-    {
459
-        // Current synctoken
460
-        $stmt = $this->pdo->prepare('SELECT synctoken FROM '.$this->addressBooksTableName.' WHERE id = ?');
461
-        $stmt->execute([$addressBookId]);
462
-        $currentToken = $stmt->fetchColumn(0);
463
-
464
-        if (is_null($currentToken)) {
465
-            return null;
466
-        }
467
-
468
-        $result = [
469
-            'syncToken' => $currentToken,
470
-            'added' => [],
471
-            'modified' => [],
472
-            'deleted' => [],
473
-        ];
474
-
475
-        if ($syncToken) {
476
-            $query = 'SELECT uri, operation FROM '.$this->addressBookChangesTableName.' WHERE synctoken >= ? AND synctoken < ? AND addressbookid = ? ORDER BY synctoken';
477
-            if ($limit > 0) {
478
-                $query .= ' LIMIT '.(int) $limit;
479
-            }
480
-
481
-            // Fetching all changes
482
-            $stmt = $this->pdo->prepare($query);
483
-            $stmt->execute([$syncToken, $currentToken, $addressBookId]);
484
-
485
-            $changes = [];
486
-
487
-            // This loop ensures that any duplicates are overwritten, only the
488
-            // last change on a node is relevant.
489
-            while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
490
-                $changes[$row['uri']] = $row['operation'];
491
-            }
492
-
493
-            foreach ($changes as $uri => $operation) {
494
-                switch ($operation) {
495
-                    case 1:
496
-                        $result['added'][] = $uri;
497
-                        break;
498
-                    case 2:
499
-                        $result['modified'][] = $uri;
500
-                        break;
501
-                    case 3:
502
-                        $result['deleted'][] = $uri;
503
-                        break;
504
-                }
505
-            }
506
-        } else {
507
-            // No synctoken supplied, this is the initial sync.
508
-            $query = 'SELECT uri FROM '.$this->cardsTableName.' WHERE addressbookid = ?';
509
-            $stmt = $this->pdo->prepare($query);
510
-            $stmt->execute([$addressBookId]);
511
-
512
-            $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
513
-        }
514
-
515
-        return $result;
516
-    }
517
-
518
-    /**
519
-     * Adds a change record to the addressbookchanges table.
520
-     *
521
-     * @param mixed  $addressBookId
522
-     * @param string $objectUri
523
-     * @param int    $operation     1 = add, 2 = modify, 3 = delete
524
-     */
525
-    protected function addChange($addressBookId, $objectUri, $operation)
526
-    {
527
-        $stmt = $this->pdo->prepare('INSERT INTO '.$this->addressBookChangesTableName.' (uri, synctoken, addressbookid, operation) SELECT ?, synctoken, ?, ? FROM '.$this->addressBooksTableName.' WHERE id = ?');
528
-        $stmt->execute([
529
-            $objectUri,
530
-            $addressBookId,
531
-            $operation,
532
-            $addressBookId,
533
-        ]);
534
-        $stmt = $this->pdo->prepare('UPDATE '.$this->addressBooksTableName.' SET synctoken = synctoken + 1 WHERE id = ?');
535
-        $stmt->execute([
536
-            $addressBookId,
537
-        ]);
538
-    }
22
+	/**
23
+	 * PDO connection.
24
+	 *
25
+	 * @var PDO
26
+	 */
27
+	protected $pdo;
28
+
29
+	/**
30
+	 * The PDO table name used to store addressbooks.
31
+	 */
32
+	public $addressBooksTableName = 'addressbooks';
33
+
34
+	/**
35
+	 * The PDO table name used to store cards.
36
+	 */
37
+	public $cardsTableName = 'cards';
38
+
39
+	/**
40
+	 * The table name that will be used for tracking changes in address books.
41
+	 *
42
+	 * @var string
43
+	 */
44
+	public $addressBookChangesTableName = 'addressbookchanges';
45
+
46
+	/**
47
+	 * Sets up the object.
48
+	 */
49
+	public function __construct(\PDO $pdo)
50
+	{
51
+		$this->pdo = $pdo;
52
+	}
53
+
54
+	/**
55
+	 * Returns the list of addressbooks for a specific user.
56
+	 *
57
+	 * @param string $principalUri
58
+	 *
59
+	 * @return array
60
+	 */
61
+	public function getAddressBooksForUser($principalUri)
62
+	{
63
+		$stmt = $this->pdo->prepare('SELECT id, uri, displayname, principaluri, description, synctoken FROM '.$this->addressBooksTableName.' WHERE principaluri = ?');
64
+		$stmt->execute([$principalUri]);
65
+
66
+		$addressBooks = [];
67
+
68
+		foreach ($stmt->fetchAll() as $row) {
69
+			$addressBooks[] = [
70
+				'id' => $row['id'],
71
+				'uri' => $row['uri'],
72
+				'principaluri' => $row['principaluri'],
73
+				'{DAV:}displayname' => $row['displayname'],
74
+				'{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'],
75
+				'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
76
+				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
77
+			];
78
+		}
79
+
80
+		return $addressBooks;
81
+	}
82
+
83
+	/**
84
+	 * Updates properties for an address book.
85
+	 *
86
+	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
87
+	 * To do the actual updates, you must tell this object which properties
88
+	 * you're going to process with the handle() method.
89
+	 *
90
+	 * Calling the handle method is like telling the PropPatch object "I
91
+	 * promise I can handle updating this property".
92
+	 *
93
+	 * Read the PropPatch documentation for more info and examples.
94
+	 *
95
+	 * @param string $addressBookId
96
+	 */
97
+	public function updateAddressBook($addressBookId, PropPatch $propPatch)
98
+	{
99
+		$supportedProperties = [
100
+			'{DAV:}displayname',
101
+			'{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description',
102
+		];
103
+
104
+		$propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
105
+			$updates = [];
106
+			foreach ($mutations as $property => $newValue) {
107
+				switch ($property) {
108
+					case '{DAV:}displayname':
109
+						$updates['displayname'] = $newValue;
110
+						break;
111
+					case '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description':
112
+						$updates['description'] = $newValue;
113
+						break;
114
+				}
115
+			}
116
+			$query = 'UPDATE '.$this->addressBooksTableName.' SET ';
117
+			$first = true;
118
+			foreach ($updates as $key => $value) {
119
+				if ($first) {
120
+					$first = false;
121
+				} else {
122
+					$query .= ', ';
123
+				}
124
+				$query .= ' '.$key.' = :'.$key.' ';
125
+			}
126
+			$query .= ' WHERE id = :addressbookid';
127
+
128
+			$stmt = $this->pdo->prepare($query);
129
+			$updates['addressbookid'] = $addressBookId;
130
+
131
+			$stmt->execute($updates);
132
+
133
+			$this->addChange($addressBookId, '', 2);
134
+
135
+			return true;
136
+		});
137
+	}
138
+
139
+	/**
140
+	 * Creates a new address book.
141
+	 *
142
+	 * @param string $principalUri
143
+	 * @param string $url          just the 'basename' of the url
144
+	 *
145
+	 * @return int Last insert id
146
+	 */
147
+	public function createAddressBook($principalUri, $url, array $properties)
148
+	{
149
+		$values = [
150
+			'displayname' => null,
151
+			'description' => null,
152
+			'principaluri' => $principalUri,
153
+			'uri' => $url,
154
+		];
155
+
156
+		foreach ($properties as $property => $newValue) {
157
+			switch ($property) {
158
+				case '{DAV:}displayname':
159
+					$values['displayname'] = $newValue;
160
+					break;
161
+				case '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description':
162
+					$values['description'] = $newValue;
163
+					break;
164
+				default:
165
+					throw new DAV\Exception\BadRequest('Unknown property: '.$property);
166
+			}
167
+		}
168
+
169
+		$query = 'INSERT INTO '.$this->addressBooksTableName.' (uri, displayname, description, principaluri, synctoken) VALUES (:uri, :displayname, :description, :principaluri, 1)';
170
+		$stmt = $this->pdo->prepare($query);
171
+		$stmt->execute($values);
172
+
173
+		return $this->pdo->lastInsertId(
174
+			$this->addressBooksTableName.'_id_seq'
175
+		);
176
+	}
177
+
178
+	/**
179
+	 * Deletes an entire addressbook and all its contents.
180
+	 *
181
+	 * @param int $addressBookId
182
+	 */
183
+	public function deleteAddressBook($addressBookId)
184
+	{
185
+		$stmt = $this->pdo->prepare('DELETE FROM '.$this->cardsTableName.' WHERE addressbookid = ?');
186
+		$stmt->execute([$addressBookId]);
187
+
188
+		$stmt = $this->pdo->prepare('DELETE FROM '.$this->addressBooksTableName.' WHERE id = ?');
189
+		$stmt->execute([$addressBookId]);
190
+
191
+		$stmt = $this->pdo->prepare('DELETE FROM '.$this->addressBookChangesTableName.' WHERE addressbookid = ?');
192
+		$stmt->execute([$addressBookId]);
193
+	}
194
+
195
+	/**
196
+	 * Returns all cards for a specific addressbook id.
197
+	 *
198
+	 * This method should return the following properties for each card:
199
+	 *   * carddata - raw vcard data
200
+	 *   * uri - Some unique url
201
+	 *   * lastmodified - A unix timestamp
202
+	 *
203
+	 * It's recommended to also return the following properties:
204
+	 *   * etag - A unique etag. This must change every time the card changes.
205
+	 *   * size - The size of the card in bytes.
206
+	 *
207
+	 * If these last two properties are provided, less time will be spent
208
+	 * calculating them. If they are specified, you can also ommit carddata.
209
+	 * This may speed up certain requests, especially with large cards.
210
+	 *
211
+	 * @param mixed $addressbookId
212
+	 *
213
+	 * @return array
214
+	 */
215
+	public function getCards($addressbookId)
216
+	{
217
+		$stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, size FROM '.$this->cardsTableName.' WHERE addressbookid = ?');
218
+		$stmt->execute([$addressbookId]);
219
+
220
+		$result = [];
221
+		while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
222
+			$row['etag'] = '"'.$row['etag'].'"';
223
+			$row['lastmodified'] = (int) $row['lastmodified'];
224
+			$result[] = $row;
225
+		}
226
+
227
+		return $result;
228
+	}
229
+
230
+	/**
231
+	 * Returns a specific card.
232
+	 *
233
+	 * The same set of properties must be returned as with getCards. The only
234
+	 * exception is that 'carddata' is absolutely required.
235
+	 *
236
+	 * If the card does not exist, you must return false.
237
+	 *
238
+	 * @param mixed  $addressBookId
239
+	 * @param string $cardUri
240
+	 *
241
+	 * @return array
242
+	 */
243
+	public function getCard($addressBookId, $cardUri)
244
+	{
245
+		$stmt = $this->pdo->prepare('SELECT id, carddata, uri, lastmodified, etag, size FROM '.$this->cardsTableName.' WHERE addressbookid = ? AND uri = ? LIMIT 1');
246
+		$stmt->execute([$addressBookId, $cardUri]);
247
+
248
+		$result = $stmt->fetch(\PDO::FETCH_ASSOC);
249
+
250
+		if (!$result) {
251
+			return false;
252
+		}
253
+
254
+		$result['etag'] = '"'.$result['etag'].'"';
255
+		$result['lastmodified'] = (int) $result['lastmodified'];
256
+
257
+		return $result;
258
+	}
259
+
260
+	/**
261
+	 * Returns a list of cards.
262
+	 *
263
+	 * This method should work identical to getCard, but instead return all the
264
+	 * cards in the list as an array.
265
+	 *
266
+	 * If the backend supports this, it may allow for some speed-ups.
267
+	 *
268
+	 * @param mixed $addressBookId
269
+	 *
270
+	 * @return array
271
+	 */
272
+	public function getMultipleCards($addressBookId, array $uris)
273
+	{
274
+		$query = 'SELECT id, uri, lastmodified, etag, size, carddata FROM '.$this->cardsTableName.' WHERE addressbookid = ? AND uri IN (';
275
+		// Inserting a whole bunch of question marks
276
+		$query .= implode(',', array_fill(0, count($uris), '?'));
277
+		$query .= ')';
278
+
279
+		$stmt = $this->pdo->prepare($query);
280
+		$stmt->execute(array_merge([$addressBookId], $uris));
281
+		$result = [];
282
+		while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
283
+			$row['etag'] = '"'.$row['etag'].'"';
284
+			$row['lastmodified'] = (int) $row['lastmodified'];
285
+			$result[] = $row;
286
+		}
287
+
288
+		return $result;
289
+	}
290
+
291
+	/**
292
+	 * Creates a new card.
293
+	 *
294
+	 * The addressbook id will be passed as the first argument. This is the
295
+	 * same id as it is returned from the getAddressBooksForUser method.
296
+	 *
297
+	 * The cardUri is a base uri, and doesn't include the full path. The
298
+	 * cardData argument is the vcard body, and is passed as a string.
299
+	 *
300
+	 * It is possible to return an ETag from this method. This ETag is for the
301
+	 * newly created resource, and must be enclosed with double quotes (that
302
+	 * is, the string itself must contain the double quotes).
303
+	 *
304
+	 * You should only return the ETag if you store the carddata as-is. If a
305
+	 * subsequent GET request on the same card does not have the same body,
306
+	 * byte-by-byte and you did return an ETag here, clients tend to get
307
+	 * confused.
308
+	 *
309
+	 * If you don't return an ETag, you can just return null.
310
+	 *
311
+	 * @param mixed  $addressBookId
312
+	 * @param string $cardUri
313
+	 * @param string $cardData
314
+	 *
315
+	 * @return string|null
316
+	 */
317
+	public function createCard($addressBookId, $cardUri, $cardData)
318
+	{
319
+		$stmt = $this->pdo->prepare('INSERT INTO '.$this->cardsTableName.' (carddata, uri, lastmodified, addressbookid, size, etag) VALUES (?, ?, ?, ?, ?, ?)');
320
+
321
+		$etag = md5($cardData);
322
+
323
+		$stmt->execute([
324
+			$cardData,
325
+			$cardUri,
326
+			time(),
327
+			$addressBookId,
328
+			strlen($cardData),
329
+			$etag,
330
+		]);
331
+
332
+		$this->addChange($addressBookId, $cardUri, 1);
333
+
334
+		return '"'.$etag.'"';
335
+	}
336
+
337
+	/**
338
+	 * Updates a card.
339
+	 *
340
+	 * The addressbook id will be passed as the first argument. This is the
341
+	 * same id as it is returned from the getAddressBooksForUser method.
342
+	 *
343
+	 * The cardUri is a base uri, and doesn't include the full path. The
344
+	 * cardData argument is the vcard body, and is passed as a string.
345
+	 *
346
+	 * It is possible to return an ETag from this method. This ETag should
347
+	 * match that of the updated resource, and must be enclosed with double
348
+	 * quotes (that is: the string itself must contain the actual quotes).
349
+	 *
350
+	 * You should only return the ETag if you store the carddata as-is. If a
351
+	 * subsequent GET request on the same card does not have the same body,
352
+	 * byte-by-byte and you did return an ETag here, clients tend to get
353
+	 * confused.
354
+	 *
355
+	 * If you don't return an ETag, you can just return null.
356
+	 *
357
+	 * @param mixed  $addressBookId
358
+	 * @param string $cardUri
359
+	 * @param string $cardData
360
+	 *
361
+	 * @return string|null
362
+	 */
363
+	public function updateCard($addressBookId, $cardUri, $cardData)
364
+	{
365
+		$stmt = $this->pdo->prepare('UPDATE '.$this->cardsTableName.' SET carddata = ?, lastmodified = ?, size = ?, etag = ? WHERE uri = ? AND addressbookid =?');
366
+
367
+		$etag = md5($cardData);
368
+		$stmt->execute([
369
+			$cardData,
370
+			time(),
371
+			strlen($cardData),
372
+			$etag,
373
+			$cardUri,
374
+			$addressBookId,
375
+		]);
376
+
377
+		$this->addChange($addressBookId, $cardUri, 2);
378
+
379
+		return '"'.$etag.'"';
380
+	}
381
+
382
+	/**
383
+	 * Deletes a card.
384
+	 *
385
+	 * @param mixed  $addressBookId
386
+	 * @param string $cardUri
387
+	 *
388
+	 * @return bool
389
+	 */
390
+	public function deleteCard($addressBookId, $cardUri)
391
+	{
392
+		$stmt = $this->pdo->prepare('DELETE FROM '.$this->cardsTableName.' WHERE addressbookid = ? AND uri = ?');
393
+		$stmt->execute([$addressBookId, $cardUri]);
394
+
395
+		$this->addChange($addressBookId, $cardUri, 3);
396
+
397
+		return 1 === $stmt->rowCount();
398
+	}
399
+
400
+	/**
401
+	 * The getChanges method returns all the changes that have happened, since
402
+	 * the specified syncToken in the specified address book.
403
+	 *
404
+	 * This function should return an array, such as the following:
405
+	 *
406
+	 * [
407
+	 *   'syncToken' => 'The current synctoken',
408
+	 *   'added'   => [
409
+	 *      'new.txt',
410
+	 *   ],
411
+	 *   'modified'   => [
412
+	 *      'updated.txt',
413
+	 *   ],
414
+	 *   'deleted' => [
415
+	 *      'foo.php.bak',
416
+	 *      'old.txt'
417
+	 *   ]
418
+	 * ];
419
+	 *
420
+	 * The returned syncToken property should reflect the *current* syncToken
421
+	 * of the addressbook, as reported in the {http://sabredav.org/ns}sync-token
422
+	 * property. This is needed here too, to ensure the operation is atomic.
423
+	 *
424
+	 * If the $syncToken argument is specified as null, this is an initial
425
+	 * sync, and all members should be reported.
426
+	 *
427
+	 * The modified property is an array of nodenames that have changed since
428
+	 * the last token.
429
+	 *
430
+	 * The deleted property is an array with nodenames, that have been deleted
431
+	 * from collection.
432
+	 *
433
+	 * The $syncLevel argument is basically the 'depth' of the report. If it's
434
+	 * 1, you only have to report changes that happened only directly in
435
+	 * immediate descendants. If it's 2, it should also include changes from
436
+	 * the nodes below the child collections. (grandchildren)
437
+	 *
438
+	 * The $limit argument allows a client to specify how many results should
439
+	 * be returned at most. If the limit is not specified, it should be treated
440
+	 * as infinite.
441
+	 *
442
+	 * If the limit (infinite or not) is higher than you're willing to return,
443
+	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
444
+	 *
445
+	 * If the syncToken is expired (due to data cleanup) or unknown, you must
446
+	 * return null.
447
+	 *
448
+	 * The limit is 'suggestive'. You are free to ignore it.
449
+	 *
450
+	 * @param string $addressBookId
451
+	 * @param string $syncToken
452
+	 * @param int    $syncLevel
453
+	 * @param int    $limit
454
+	 *
455
+	 * @return array|null
456
+	 */
457
+	public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null)
458
+	{
459
+		// Current synctoken
460
+		$stmt = $this->pdo->prepare('SELECT synctoken FROM '.$this->addressBooksTableName.' WHERE id = ?');
461
+		$stmt->execute([$addressBookId]);
462
+		$currentToken = $stmt->fetchColumn(0);
463
+
464
+		if (is_null($currentToken)) {
465
+			return null;
466
+		}
467
+
468
+		$result = [
469
+			'syncToken' => $currentToken,
470
+			'added' => [],
471
+			'modified' => [],
472
+			'deleted' => [],
473
+		];
474
+
475
+		if ($syncToken) {
476
+			$query = 'SELECT uri, operation FROM '.$this->addressBookChangesTableName.' WHERE synctoken >= ? AND synctoken < ? AND addressbookid = ? ORDER BY synctoken';
477
+			if ($limit > 0) {
478
+				$query .= ' LIMIT '.(int) $limit;
479
+			}
480
+
481
+			// Fetching all changes
482
+			$stmt = $this->pdo->prepare($query);
483
+			$stmt->execute([$syncToken, $currentToken, $addressBookId]);
484
+
485
+			$changes = [];
486
+
487
+			// This loop ensures that any duplicates are overwritten, only the
488
+			// last change on a node is relevant.
489
+			while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
490
+				$changes[$row['uri']] = $row['operation'];
491
+			}
492
+
493
+			foreach ($changes as $uri => $operation) {
494
+				switch ($operation) {
495
+					case 1:
496
+						$result['added'][] = $uri;
497
+						break;
498
+					case 2:
499
+						$result['modified'][] = $uri;
500
+						break;
501
+					case 3:
502
+						$result['deleted'][] = $uri;
503
+						break;
504
+				}
505
+			}
506
+		} else {
507
+			// No synctoken supplied, this is the initial sync.
508
+			$query = 'SELECT uri FROM '.$this->cardsTableName.' WHERE addressbookid = ?';
509
+			$stmt = $this->pdo->prepare($query);
510
+			$stmt->execute([$addressBookId]);
511
+
512
+			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
513
+		}
514
+
515
+		return $result;
516
+	}
517
+
518
+	/**
519
+	 * Adds a change record to the addressbookchanges table.
520
+	 *
521
+	 * @param mixed  $addressBookId
522
+	 * @param string $objectUri
523
+	 * @param int    $operation     1 = add, 2 = modify, 3 = delete
524
+	 */
525
+	protected function addChange($addressBookId, $objectUri, $operation)
526
+	{
527
+		$stmt = $this->pdo->prepare('INSERT INTO '.$this->addressBookChangesTableName.' (uri, synctoken, addressbookid, operation) SELECT ?, synctoken, ?, ? FROM '.$this->addressBooksTableName.' WHERE id = ?');
528
+		$stmt->execute([
529
+			$objectUri,
530
+			$addressBookId,
531
+			$operation,
532
+			$addressBookId,
533
+		]);
534
+		$stmt = $this->pdo->prepare('UPDATE '.$this->addressBooksTableName.' SET synctoken = synctoken + 1 WHERE id = ?');
535
+		$stmt->execute([
536
+			$addressBookId,
537
+		]);
538
+	}
539 539
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/dav/lib/CardDAV/AddressBookRoot.php 1 patch
Indentation   +51 added lines, -51 removed lines patch added patch discarded remove patch
@@ -17,59 +17,59 @@
 block discarded – undo
17 17
  */
18 18
 class AddressBookRoot extends DAVACL\AbstractPrincipalCollection
19 19
 {
20
-    /**
21
-     * Principal Backend.
22
-     *
23
-     * @var DAVACL\PrincipalBackend\BackendInterface
24
-     */
25
-    protected $principalBackend;
20
+	/**
21
+	 * Principal Backend.
22
+	 *
23
+	 * @var DAVACL\PrincipalBackend\BackendInterface
24
+	 */
25
+	protected $principalBackend;
26 26
 
27
-    /**
28
-     * CardDAV backend.
29
-     *
30
-     * @var Backend\BackendInterface
31
-     */
32
-    protected $carddavBackend;
27
+	/**
28
+	 * CardDAV backend.
29
+	 *
30
+	 * @var Backend\BackendInterface
31
+	 */
32
+	protected $carddavBackend;
33 33
 
34
-    /**
35
-     * Constructor.
36
-     *
37
-     * This constructor needs both a principal and a carddav backend.
38
-     *
39
-     * By default this class will show a list of addressbook collections for
40
-     * principals in the 'principals' collection. If your main principals are
41
-     * actually located in a different path, use the $principalPrefix argument
42
-     * to override this.
43
-     *
44
-     * @param string $principalPrefix
45
-     */
46
-    public function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend, Backend\BackendInterface $carddavBackend, $principalPrefix = 'principals')
47
-    {
48
-        $this->carddavBackend = $carddavBackend;
49
-        parent::__construct($principalBackend, $principalPrefix);
50
-    }
34
+	/**
35
+	 * Constructor.
36
+	 *
37
+	 * This constructor needs both a principal and a carddav backend.
38
+	 *
39
+	 * By default this class will show a list of addressbook collections for
40
+	 * principals in the 'principals' collection. If your main principals are
41
+	 * actually located in a different path, use the $principalPrefix argument
42
+	 * to override this.
43
+	 *
44
+	 * @param string $principalPrefix
45
+	 */
46
+	public function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend, Backend\BackendInterface $carddavBackend, $principalPrefix = 'principals')
47
+	{
48
+		$this->carddavBackend = $carddavBackend;
49
+		parent::__construct($principalBackend, $principalPrefix);
50
+	}
51 51
 
52
-    /**
53
-     * Returns the name of the node.
54
-     *
55
-     * @return string
56
-     */
57
-    public function getName()
58
-    {
59
-        return Plugin::ADDRESSBOOK_ROOT;
60
-    }
52
+	/**
53
+	 * Returns the name of the node.
54
+	 *
55
+	 * @return string
56
+	 */
57
+	public function getName()
58
+	{
59
+		return Plugin::ADDRESSBOOK_ROOT;
60
+	}
61 61
 
62
-    /**
63
-     * This method returns a node for a principal.
64
-     *
65
-     * The passed array contains principal information, and is guaranteed to
66
-     * at least contain a uri item. Other properties may or may not be
67
-     * supplied by the authentication backend.
68
-     *
69
-     * @return \Sabre\DAV\INode
70
-     */
71
-    public function getChildForPrincipal(array $principal)
72
-    {
73
-        return new AddressBookHome($this->carddavBackend, $principal['uri']);
74
-    }
62
+	/**
63
+	 * This method returns a node for a principal.
64
+	 *
65
+	 * The passed array contains principal information, and is guaranteed to
66
+	 * at least contain a uri item. Other properties may or may not be
67
+	 * supplied by the authentication backend.
68
+	 *
69
+	 * @return \Sabre\DAV\INode
70
+	 */
71
+	public function getChildForPrincipal(array $principal)
72
+	{
73
+		return new AddressBookHome($this->carddavBackend, $principal['uri']);
74
+	}
75 75
 }
Please login to merge, or discard this patch.