Completed
Branch develop (eb876f)
by
unknown
21:14
created
htdocs/includes/sabre/sabre/dav/lib/DAVACL/IPrincipalCollection.php 1 patch
Indentation   +42 added lines, -42 removed lines patch added patch discarded remove patch
@@ -18,47 +18,47 @@
 block discarded – undo
18 18
  */
19 19
 interface IPrincipalCollection extends DAV\ICollection
20 20
 {
21
-    /**
22
-     * This method is used to search for principals matching a set of
23
-     * properties.
24
-     *
25
-     * This search is specifically used by RFC3744's principal-property-search
26
-     * REPORT. You should at least allow searching on
27
-     * http://sabredav.org/ns}email-address.
28
-     *
29
-     * The actual search should be a unicode-non-case-sensitive search. The
30
-     * keys in searchProperties are the WebDAV property names, while the values
31
-     * are the property values to search on.
32
-     *
33
-     * By default, if multiple properties are submitted to this method, the
34
-     * various properties should be combined with 'AND'. If $test is set to
35
-     * 'anyof', it should be combined using 'OR'.
36
-     *
37
-     * This method should simply return a list of 'child names', which may be
38
-     * used to call $this->getChild in the future.
39
-     *
40
-     * @param string $test
41
-     *
42
-     * @return array
43
-     */
44
-    public function searchPrincipals(array $searchProperties, $test = 'allof');
21
+	/**
22
+	 * This method is used to search for principals matching a set of
23
+	 * properties.
24
+	 *
25
+	 * This search is specifically used by RFC3744's principal-property-search
26
+	 * REPORT. You should at least allow searching on
27
+	 * http://sabredav.org/ns}email-address.
28
+	 *
29
+	 * The actual search should be a unicode-non-case-sensitive search. The
30
+	 * keys in searchProperties are the WebDAV property names, while the values
31
+	 * are the property values to search on.
32
+	 *
33
+	 * By default, if multiple properties are submitted to this method, the
34
+	 * various properties should be combined with 'AND'. If $test is set to
35
+	 * 'anyof', it should be combined using 'OR'.
36
+	 *
37
+	 * This method should simply return a list of 'child names', which may be
38
+	 * used to call $this->getChild in the future.
39
+	 *
40
+	 * @param string $test
41
+	 *
42
+	 * @return array
43
+	 */
44
+	public function searchPrincipals(array $searchProperties, $test = 'allof');
45 45
 
46
-    /**
47
-     * Finds a principal by its URI.
48
-     *
49
-     * This method may receive any type of uri, but mailto: addresses will be
50
-     * the most common.
51
-     *
52
-     * Implementation of this API is optional. It is currently used by the
53
-     * CalDAV system to find principals based on their email addresses. If this
54
-     * API is not implemented, some features may not work correctly.
55
-     *
56
-     * This method must return a relative principal path, or null, if the
57
-     * principal was not found or you refuse to find it.
58
-     *
59
-     * @param string $uri
60
-     *
61
-     * @return string
62
-     */
63
-    public function findByUri($uri);
46
+	/**
47
+	 * Finds a principal by its URI.
48
+	 *
49
+	 * This method may receive any type of uri, but mailto: addresses will be
50
+	 * the most common.
51
+	 *
52
+	 * Implementation of this API is optional. It is currently used by the
53
+	 * CalDAV system to find principals based on their email addresses. If this
54
+	 * API is not implemented, some features may not work correctly.
55
+	 *
56
+	 * This method must return a relative principal path, or null, if the
57
+	 * principal was not found or you refuse to find it.
58
+	 *
59
+	 * @param string $uri
60
+	 *
61
+	 * @return string
62
+	 */
63
+	public function findByUri($uri);
64 64
 }
Please login to merge, or discard this patch.
sabre/sabre/dav/lib/DAVACL/PrincipalBackend/CreatePrincipalSupport.php 1 patch
Indentation   +10 added lines, -10 removed lines patch added patch discarded remove patch
@@ -16,14 +16,14 @@
 block discarded – undo
16 16
  */
17 17
 interface CreatePrincipalSupport extends BackendInterface
18 18
 {
19
-    /**
20
-     * Creates a new principal.
21
-     *
22
-     * This method receives a full path for the new principal. The mkCol object
23
-     * contains any additional webdav properties specified during the creation
24
-     * of the principal.
25
-     *
26
-     * @param string $path
27
-     */
28
-    public function createPrincipal($path, MkCol $mkCol);
19
+	/**
20
+	 * Creates a new principal.
21
+	 *
22
+	 * This method receives a full path for the new principal. The mkCol object
23
+	 * contains any additional webdav properties specified during the creation
24
+	 * of the principal.
25
+	 *
26
+	 * @param string $path
27
+	 */
28
+	public function createPrincipal($path, MkCol $mkCol);
29 29
 }
Please login to merge, or discard this patch.
includes/sabre/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php 1 patch
Indentation   +33 added lines, -33 removed lines patch added patch discarded remove patch
@@ -17,38 +17,38 @@
 block discarded – undo
17 17
  */
18 18
 abstract class AbstractBackend implements BackendInterface
19 19
 {
20
-    /**
21
-     * Finds a principal by its URI.
22
-     *
23
-     * This method may receive any type of uri, but mailto: addresses will be
24
-     * the most common.
25
-     *
26
-     * Implementation of this API is optional. It is currently used by the
27
-     * CalDAV system to find principals based on their email addresses. If this
28
-     * API is not implemented, some features may not work correctly.
29
-     *
30
-     * This method must return a relative principal path, or null, if the
31
-     * principal was not found or you refuse to find it.
32
-     *
33
-     * @param string $uri
34
-     * @param string $principalPrefix
35
-     *
36
-     * @return string|null
37
-     */
38
-    public function findByUri($uri, $principalPrefix)
39
-    {
40
-        // Note that the default implementation here is a bit slow and could
41
-        // likely be optimized.
42
-        if ('mailto:' !== substr($uri, 0, 7)) {
43
-            return;
44
-        }
45
-        $result = $this->searchPrincipals(
46
-            $principalPrefix,
47
-            ['{http://sabredav.org/ns}email-address' => substr($uri, 7)]
48
-        );
20
+	/**
21
+	 * Finds a principal by its URI.
22
+	 *
23
+	 * This method may receive any type of uri, but mailto: addresses will be
24
+	 * the most common.
25
+	 *
26
+	 * Implementation of this API is optional. It is currently used by the
27
+	 * CalDAV system to find principals based on their email addresses. If this
28
+	 * API is not implemented, some features may not work correctly.
29
+	 *
30
+	 * This method must return a relative principal path, or null, if the
31
+	 * principal was not found or you refuse to find it.
32
+	 *
33
+	 * @param string $uri
34
+	 * @param string $principalPrefix
35
+	 *
36
+	 * @return string|null
37
+	 */
38
+	public function findByUri($uri, $principalPrefix)
39
+	{
40
+		// Note that the default implementation here is a bit slow and could
41
+		// likely be optimized.
42
+		if ('mailto:' !== substr($uri, 0, 7)) {
43
+			return;
44
+		}
45
+		$result = $this->searchPrincipals(
46
+			$principalPrefix,
47
+			['{http://sabredav.org/ns}email-address' => substr($uri, 7)]
48
+		);
49 49
 
50
-        if ($result) {
51
-            return $result[0];
52
-        }
53
-    }
50
+		if ($result) {
51
+			return $result[0];
52
+		}
53
+	}
54 54
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/dav/lib/DAVACL/PrincipalBackend/PDO.php 2 patches
Indentation   +415 added lines, -415 removed lines patch added patch discarded remove patch
@@ -20,424 +20,424 @@
 block discarded – undo
20 20
  */
21 21
 class PDO extends AbstractBackend implements CreatePrincipalSupport
22 22
 {
23
-    /**
24
-     * PDO table name for 'principals'.
25
-     *
26
-     * @var string
27
-     */
28
-    public $tableName = 'principals';
29
-
30
-    /**
31
-     * PDO table name for 'group members'.
32
-     *
33
-     * @var string
34
-     */
35
-    public $groupMembersTableName = 'groupmembers';
36
-
37
-    /**
38
-     * pdo.
39
-     *
40
-     * @var PDO
41
-     */
42
-    protected $pdo;
43
-
44
-    /**
45
-     * A list of additional fields to support.
46
-     *
47
-     * @var array
48
-     */
49
-    protected $fieldMap = [
50
-        /*
23
+	/**
24
+	 * PDO table name for 'principals'.
25
+	 *
26
+	 * @var string
27
+	 */
28
+	public $tableName = 'principals';
29
+
30
+	/**
31
+	 * PDO table name for 'group members'.
32
+	 *
33
+	 * @var string
34
+	 */
35
+	public $groupMembersTableName = 'groupmembers';
36
+
37
+	/**
38
+	 * pdo.
39
+	 *
40
+	 * @var PDO
41
+	 */
42
+	protected $pdo;
43
+
44
+	/**
45
+	 * A list of additional fields to support.
46
+	 *
47
+	 * @var array
48
+	 */
49
+	protected $fieldMap = [
50
+		/*
51 51
          * This property can be used to display the users' real name.
52 52
          */
53
-        '{DAV:}displayname' => [
54
-            'dbField' => 'displayname',
55
-        ],
53
+		'{DAV:}displayname' => [
54
+			'dbField' => 'displayname',
55
+		],
56 56
 
57
-        /*
57
+		/*
58 58
          * This is the users' primary email-address.
59 59
          */
60
-        '{http://sabredav.org/ns}email-address' => [
61
-            'dbField' => 'email',
62
-        ],
63
-    ];
64
-
65
-    /**
66
-     * Sets up the backend.
67
-     */
68
-    public function __construct(\PDO $pdo)
69
-    {
70
-        $this->pdo = $pdo;
71
-    }
72
-
73
-    /**
74
-     * Returns a list of principals based on a prefix.
75
-     *
76
-     * This prefix will often contain something like 'principals'. You are only
77
-     * expected to return principals that are in this base path.
78
-     *
79
-     * You are expected to return at least a 'uri' for every user, you can
80
-     * return any additional properties if you wish so. Common properties are:
81
-     *   {DAV:}displayname
82
-     *   {http://sabredav.org/ns}email-address - This is a custom SabreDAV
83
-     *     field that's actualy injected in a number of other properties. If
84
-     *     you have an email address, use this property.
85
-     *
86
-     * @param string $prefixPath
87
-     *
88
-     * @return array
89
-     */
90
-    public function getPrincipalsByPrefix($prefixPath)
91
-    {
92
-        $fields = [
93
-            'uri',
94
-        ];
95
-
96
-        foreach ($this->fieldMap as $key => $value) {
97
-            $fields[] = $value['dbField'];
98
-        }
99
-        $result = $this->pdo->query('SELECT '.implode(',', $fields).'  FROM '.$this->tableName);
100
-
101
-        $principals = [];
102
-
103
-        while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
104
-            // Checking if the principal is in the prefix
105
-            list($rowPrefix) = Uri\split($row['uri']);
106
-            if ($rowPrefix !== $prefixPath) {
107
-                continue;
108
-            }
109
-
110
-            $principal = [
111
-                'uri' => $row['uri'],
112
-            ];
113
-            foreach ($this->fieldMap as $key => $value) {
114
-                if ($row[$value['dbField']]) {
115
-                    $principal[$key] = $row[$value['dbField']];
116
-                }
117
-            }
118
-            $principals[] = $principal;
119
-        }
120
-
121
-        return $principals;
122
-    }
123
-
124
-    /**
125
-     * Returns a specific principal, specified by it's path.
126
-     * The returned structure should be the exact same as from
127
-     * getPrincipalsByPrefix.
128
-     *
129
-     * @param string $path
130
-     *
131
-     * @return array
132
-     */
133
-    public function getPrincipalByPath($path)
134
-    {
135
-        $fields = [
136
-            'id',
137
-            'uri',
138
-        ];
139
-
140
-        foreach ($this->fieldMap as $key => $value) {
141
-            $fields[] = $value['dbField'];
142
-        }
143
-        $stmt = $this->pdo->prepare('SELECT '.implode(',', $fields).'  FROM '.$this->tableName.' WHERE uri = ?');
144
-        $stmt->execute([$path]);
145
-
146
-        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
147
-        if (!$row) {
148
-            return;
149
-        }
150
-
151
-        $principal = [
152
-            'id' => $row['id'],
153
-            'uri' => $row['uri'],
154
-        ];
155
-        foreach ($this->fieldMap as $key => $value) {
156
-            if ($row[$value['dbField']]) {
157
-                $principal[$key] = $row[$value['dbField']];
158
-            }
159
-        }
160
-
161
-        return $principal;
162
-    }
163
-
164
-    /**
165
-     * Updates one ore more webdav properties on a principal.
166
-     *
167
-     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
168
-     * To do the actual updates, you must tell this object which properties
169
-     * you're going to process with the handle() method.
170
-     *
171
-     * Calling the handle method is like telling the PropPatch object "I
172
-     * promise I can handle updating this property".
173
-     *
174
-     * Read the PropPatch documentation for more info and examples.
175
-     *
176
-     * @param string $path
177
-     */
178
-    public function updatePrincipal($path, DAV\PropPatch $propPatch)
179
-    {
180
-        $propPatch->handle(array_keys($this->fieldMap), function ($properties) use ($path) {
181
-            $query = 'UPDATE '.$this->tableName.' SET ';
182
-            $first = true;
183
-
184
-            $values = [];
185
-
186
-            foreach ($properties as $key => $value) {
187
-                $dbField = $this->fieldMap[$key]['dbField'];
188
-
189
-                if (!$first) {
190
-                    $query .= ', ';
191
-                }
192
-                $first = false;
193
-                $query .= $dbField.' = :'.$dbField;
194
-                $values[$dbField] = $value;
195
-            }
196
-
197
-            $query .= ' WHERE uri = :uri';
198
-            $values['uri'] = $path;
199
-
200
-            $stmt = $this->pdo->prepare($query);
201
-            $stmt->execute($values);
202
-
203
-            return true;
204
-        });
205
-    }
206
-
207
-    /**
208
-     * This method is used to search for principals matching a set of
209
-     * properties.
210
-     *
211
-     * This search is specifically used by RFC3744's principal-property-search
212
-     * REPORT.
213
-     *
214
-     * The actual search should be a unicode-non-case-sensitive search. The
215
-     * keys in searchProperties are the WebDAV property names, while the values
216
-     * are the property values to search on.
217
-     *
218
-     * By default, if multiple properties are submitted to this method, the
219
-     * various properties should be combined with 'AND'. If $test is set to
220
-     * 'anyof', it should be combined using 'OR'.
221
-     *
222
-     * This method should simply return an array with full principal uri's.
223
-     *
224
-     * If somebody attempted to search on a property the backend does not
225
-     * support, you should simply return 0 results.
226
-     *
227
-     * You can also just return 0 results if you choose to not support
228
-     * searching at all, but keep in mind that this may stop certain features
229
-     * from working.
230
-     *
231
-     * @param string $prefixPath
232
-     * @param string $test
233
-     *
234
-     * @return array
235
-     */
236
-    public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof')
237
-    {
238
-        if (0 == count($searchProperties)) {
239
-            return [];
240
-        }    //No criteria
241
-
242
-        $query = 'SELECT uri FROM '.$this->tableName.' WHERE ';
243
-        $values = [];
244
-        foreach ($searchProperties as $property => $value) {
245
-            switch ($property) {
246
-                case '{DAV:}displayname':
247
-                    $column = 'displayname';
248
-                    break;
249
-                case '{http://sabredav.org/ns}email-address':
250
-                    $column = 'email';
251
-                    break;
252
-                default:
253
-                    // Unsupported property
254
-                    return [];
255
-            }
256
-            if (count($values) > 0) {
257
-                $query .= (0 == strcmp($test, 'anyof') ? ' OR ' : ' AND ');
258
-            }
259
-            $query .= 'lower('.$column.') LIKE lower(?)';
260
-            $values[] = '%'.$value.'%';
261
-        }
262
-        $stmt = $this->pdo->prepare($query);
263
-        $stmt->execute($values);
264
-
265
-        $principals = [];
266
-        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
267
-            // Checking if the principal is in the prefix
268
-            list($rowPrefix) = Uri\split($row['uri']);
269
-            if ($rowPrefix !== $prefixPath) {
270
-                continue;
271
-            }
272
-
273
-            $principals[] = $row['uri'];
274
-        }
275
-
276
-        return $principals;
277
-    }
278
-
279
-    /**
280
-     * Finds a principal by its URI.
281
-     *
282
-     * This method may receive any type of uri, but mailto: addresses will be
283
-     * the most common.
284
-     *
285
-     * Implementation of this API is optional. It is currently used by the
286
-     * CalDAV system to find principals based on their email addresses. If this
287
-     * API is not implemented, some features may not work correctly.
288
-     *
289
-     * This method must return a relative principal path, or null, if the
290
-     * principal was not found or you refuse to find it.
291
-     *
292
-     * @param string $uri
293
-     * @param string $principalPrefix
294
-     *
295
-     * @return string
296
-     */
297
-    public function findByUri($uri, $principalPrefix)
298
-    {
299
-        $uriParts = Uri\parse($uri);
300
-
301
-        // Only two types of uri are supported :
302
-        //   - the "mailto:" scheme with some non-empty address
303
-        //   - a principals uri, in the form "principals/NAME"
304
-        // In both cases, `path` must not be empty.
305
-        if (empty($uriParts['path'])) {
306
-            return null;
307
-        }
308
-
309
-        $uri = null;
310
-        if ('mailto' === $uriParts['scheme']) {
311
-            $query = 'SELECT uri FROM '.$this->tableName.' WHERE lower(email)=lower(?)';
312
-            $stmt = $this->pdo->prepare($query);
313
-            $stmt->execute([$uriParts['path']]);
314
-
315
-            while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
316
-                // Checking if the principal is in the prefix
317
-                list($rowPrefix) = Uri\split($row['uri']);
318
-                if ($rowPrefix !== $principalPrefix) {
319
-                    continue;
320
-                }
321
-
322
-                $uri = $row['uri'];
323
-                break; //Stop on first match
324
-            }
325
-        } else {
326
-            $pathParts = Uri\split($uriParts['path']); // We can do this since $uriParts['path'] is not null
327
-
328
-            if (2 === count($pathParts) && $pathParts[0] === $principalPrefix) {
329
-                // Checking that this uri exists
330
-                $query = 'SELECT * FROM '.$this->tableName.' WHERE uri = ?';
331
-                $stmt = $this->pdo->prepare($query);
332
-                $stmt->execute([$uriParts['path']]);
333
-                $rows = $stmt->fetchAll();
334
-
335
-                if (count($rows) > 0) {
336
-                    $uri = $uriParts['path'];
337
-                }
338
-            }
339
-        }
340
-
341
-        return $uri;
342
-    }
343
-
344
-    /**
345
-     * Returns the list of members for a group-principal.
346
-     *
347
-     * @param string $principal
348
-     *
349
-     * @return array
350
-     */
351
-    public function getGroupMemberSet($principal)
352
-    {
353
-        $principal = $this->getPrincipalByPath($principal);
354
-        if (!$principal) {
355
-            throw new DAV\Exception('Principal not found');
356
-        }
357
-        $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.member_id = principals.id WHERE groupmembers.principal_id = ?');
358
-        $stmt->execute([$principal['id']]);
359
-
360
-        $result = [];
361
-        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
362
-            $result[] = $row['uri'];
363
-        }
364
-
365
-        return $result;
366
-    }
367
-
368
-    /**
369
-     * Returns the list of groups a principal is a member of.
370
-     *
371
-     * @param string $principal
372
-     *
373
-     * @return array
374
-     */
375
-    public function getGroupMembership($principal)
376
-    {
377
-        $principal = $this->getPrincipalByPath($principal);
378
-        if (!$principal) {
379
-            throw new DAV\Exception('Principal not found');
380
-        }
381
-        $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.principal_id = principals.id WHERE groupmembers.member_id = ?');
382
-        $stmt->execute([$principal['id']]);
383
-
384
-        $result = [];
385
-        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
386
-            $result[] = $row['uri'];
387
-        }
388
-
389
-        return $result;
390
-    }
391
-
392
-    /**
393
-     * Updates the list of group members for a group principal.
394
-     *
395
-     * The principals should be passed as a list of uri's.
396
-     *
397
-     * @param string $principal
398
-     */
399
-    public function setGroupMemberSet($principal, array $members)
400
-    {
401
-        // Grabbing the list of principal id's.
402
-        $stmt = $this->pdo->prepare('SELECT id, uri FROM '.$this->tableName.' WHERE uri IN (? '.str_repeat(', ? ', count($members)).');');
403
-        $stmt->execute(array_merge([$principal], $members));
404
-
405
-        $memberIds = [];
406
-        $principalId = null;
407
-
408
-        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
409
-            if ($row['uri'] == $principal) {
410
-                $principalId = $row['id'];
411
-            } else {
412
-                $memberIds[] = $row['id'];
413
-            }
414
-        }
415
-        if (!$principalId) {
416
-            throw new DAV\Exception('Principal not found');
417
-        }
418
-        // Wiping out old members
419
-        $stmt = $this->pdo->prepare('DELETE FROM '.$this->groupMembersTableName.' WHERE principal_id = ?;');
420
-        $stmt->execute([$principalId]);
421
-
422
-        foreach ($memberIds as $memberId) {
423
-            $stmt = $this->pdo->prepare('INSERT INTO '.$this->groupMembersTableName.' (principal_id, member_id) VALUES (?, ?);');
424
-            $stmt->execute([$principalId, $memberId]);
425
-        }
426
-    }
427
-
428
-    /**
429
-     * Creates a new principal.
430
-     *
431
-     * This method receives a full path for the new principal. The mkCol object
432
-     * contains any additional webdav properties specified during the creation
433
-     * of the principal.
434
-     *
435
-     * @param string $path
436
-     */
437
-    public function createPrincipal($path, MkCol $mkCol)
438
-    {
439
-        $stmt = $this->pdo->prepare('INSERT INTO '.$this->tableName.' (uri) VALUES (?)');
440
-        $stmt->execute([$path]);
441
-        $this->updatePrincipal($path, $mkCol);
442
-    }
60
+		'{http://sabredav.org/ns}email-address' => [
61
+			'dbField' => 'email',
62
+		],
63
+	];
64
+
65
+	/**
66
+	 * Sets up the backend.
67
+	 */
68
+	public function __construct(\PDO $pdo)
69
+	{
70
+		$this->pdo = $pdo;
71
+	}
72
+
73
+	/**
74
+	 * Returns a list of principals based on a prefix.
75
+	 *
76
+	 * This prefix will often contain something like 'principals'. You are only
77
+	 * expected to return principals that are in this base path.
78
+	 *
79
+	 * You are expected to return at least a 'uri' for every user, you can
80
+	 * return any additional properties if you wish so. Common properties are:
81
+	 *   {DAV:}displayname
82
+	 *   {http://sabredav.org/ns}email-address - This is a custom SabreDAV
83
+	 *     field that's actualy injected in a number of other properties. If
84
+	 *     you have an email address, use this property.
85
+	 *
86
+	 * @param string $prefixPath
87
+	 *
88
+	 * @return array
89
+	 */
90
+	public function getPrincipalsByPrefix($prefixPath)
91
+	{
92
+		$fields = [
93
+			'uri',
94
+		];
95
+
96
+		foreach ($this->fieldMap as $key => $value) {
97
+			$fields[] = $value['dbField'];
98
+		}
99
+		$result = $this->pdo->query('SELECT '.implode(',', $fields).'  FROM '.$this->tableName);
100
+
101
+		$principals = [];
102
+
103
+		while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
104
+			// Checking if the principal is in the prefix
105
+			list($rowPrefix) = Uri\split($row['uri']);
106
+			if ($rowPrefix !== $prefixPath) {
107
+				continue;
108
+			}
109
+
110
+			$principal = [
111
+				'uri' => $row['uri'],
112
+			];
113
+			foreach ($this->fieldMap as $key => $value) {
114
+				if ($row[$value['dbField']]) {
115
+					$principal[$key] = $row[$value['dbField']];
116
+				}
117
+			}
118
+			$principals[] = $principal;
119
+		}
120
+
121
+		return $principals;
122
+	}
123
+
124
+	/**
125
+	 * Returns a specific principal, specified by it's path.
126
+	 * The returned structure should be the exact same as from
127
+	 * getPrincipalsByPrefix.
128
+	 *
129
+	 * @param string $path
130
+	 *
131
+	 * @return array
132
+	 */
133
+	public function getPrincipalByPath($path)
134
+	{
135
+		$fields = [
136
+			'id',
137
+			'uri',
138
+		];
139
+
140
+		foreach ($this->fieldMap as $key => $value) {
141
+			$fields[] = $value['dbField'];
142
+		}
143
+		$stmt = $this->pdo->prepare('SELECT '.implode(',', $fields).'  FROM '.$this->tableName.' WHERE uri = ?');
144
+		$stmt->execute([$path]);
145
+
146
+		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
147
+		if (!$row) {
148
+			return;
149
+		}
150
+
151
+		$principal = [
152
+			'id' => $row['id'],
153
+			'uri' => $row['uri'],
154
+		];
155
+		foreach ($this->fieldMap as $key => $value) {
156
+			if ($row[$value['dbField']]) {
157
+				$principal[$key] = $row[$value['dbField']];
158
+			}
159
+		}
160
+
161
+		return $principal;
162
+	}
163
+
164
+	/**
165
+	 * Updates one ore more webdav properties on a principal.
166
+	 *
167
+	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
168
+	 * To do the actual updates, you must tell this object which properties
169
+	 * you're going to process with the handle() method.
170
+	 *
171
+	 * Calling the handle method is like telling the PropPatch object "I
172
+	 * promise I can handle updating this property".
173
+	 *
174
+	 * Read the PropPatch documentation for more info and examples.
175
+	 *
176
+	 * @param string $path
177
+	 */
178
+	public function updatePrincipal($path, DAV\PropPatch $propPatch)
179
+	{
180
+		$propPatch->handle(array_keys($this->fieldMap), function ($properties) use ($path) {
181
+			$query = 'UPDATE '.$this->tableName.' SET ';
182
+			$first = true;
183
+
184
+			$values = [];
185
+
186
+			foreach ($properties as $key => $value) {
187
+				$dbField = $this->fieldMap[$key]['dbField'];
188
+
189
+				if (!$first) {
190
+					$query .= ', ';
191
+				}
192
+				$first = false;
193
+				$query .= $dbField.' = :'.$dbField;
194
+				$values[$dbField] = $value;
195
+			}
196
+
197
+			$query .= ' WHERE uri = :uri';
198
+			$values['uri'] = $path;
199
+
200
+			$stmt = $this->pdo->prepare($query);
201
+			$stmt->execute($values);
202
+
203
+			return true;
204
+		});
205
+	}
206
+
207
+	/**
208
+	 * This method is used to search for principals matching a set of
209
+	 * properties.
210
+	 *
211
+	 * This search is specifically used by RFC3744's principal-property-search
212
+	 * REPORT.
213
+	 *
214
+	 * The actual search should be a unicode-non-case-sensitive search. The
215
+	 * keys in searchProperties are the WebDAV property names, while the values
216
+	 * are the property values to search on.
217
+	 *
218
+	 * By default, if multiple properties are submitted to this method, the
219
+	 * various properties should be combined with 'AND'. If $test is set to
220
+	 * 'anyof', it should be combined using 'OR'.
221
+	 *
222
+	 * This method should simply return an array with full principal uri's.
223
+	 *
224
+	 * If somebody attempted to search on a property the backend does not
225
+	 * support, you should simply return 0 results.
226
+	 *
227
+	 * You can also just return 0 results if you choose to not support
228
+	 * searching at all, but keep in mind that this may stop certain features
229
+	 * from working.
230
+	 *
231
+	 * @param string $prefixPath
232
+	 * @param string $test
233
+	 *
234
+	 * @return array
235
+	 */
236
+	public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof')
237
+	{
238
+		if (0 == count($searchProperties)) {
239
+			return [];
240
+		}    //No criteria
241
+
242
+		$query = 'SELECT uri FROM '.$this->tableName.' WHERE ';
243
+		$values = [];
244
+		foreach ($searchProperties as $property => $value) {
245
+			switch ($property) {
246
+				case '{DAV:}displayname':
247
+					$column = 'displayname';
248
+					break;
249
+				case '{http://sabredav.org/ns}email-address':
250
+					$column = 'email';
251
+					break;
252
+				default:
253
+					// Unsupported property
254
+					return [];
255
+			}
256
+			if (count($values) > 0) {
257
+				$query .= (0 == strcmp($test, 'anyof') ? ' OR ' : ' AND ');
258
+			}
259
+			$query .= 'lower('.$column.') LIKE lower(?)';
260
+			$values[] = '%'.$value.'%';
261
+		}
262
+		$stmt = $this->pdo->prepare($query);
263
+		$stmt->execute($values);
264
+
265
+		$principals = [];
266
+		while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
267
+			// Checking if the principal is in the prefix
268
+			list($rowPrefix) = Uri\split($row['uri']);
269
+			if ($rowPrefix !== $prefixPath) {
270
+				continue;
271
+			}
272
+
273
+			$principals[] = $row['uri'];
274
+		}
275
+
276
+		return $principals;
277
+	}
278
+
279
+	/**
280
+	 * Finds a principal by its URI.
281
+	 *
282
+	 * This method may receive any type of uri, but mailto: addresses will be
283
+	 * the most common.
284
+	 *
285
+	 * Implementation of this API is optional. It is currently used by the
286
+	 * CalDAV system to find principals based on their email addresses. If this
287
+	 * API is not implemented, some features may not work correctly.
288
+	 *
289
+	 * This method must return a relative principal path, or null, if the
290
+	 * principal was not found or you refuse to find it.
291
+	 *
292
+	 * @param string $uri
293
+	 * @param string $principalPrefix
294
+	 *
295
+	 * @return string
296
+	 */
297
+	public function findByUri($uri, $principalPrefix)
298
+	{
299
+		$uriParts = Uri\parse($uri);
300
+
301
+		// Only two types of uri are supported :
302
+		//   - the "mailto:" scheme with some non-empty address
303
+		//   - a principals uri, in the form "principals/NAME"
304
+		// In both cases, `path` must not be empty.
305
+		if (empty($uriParts['path'])) {
306
+			return null;
307
+		}
308
+
309
+		$uri = null;
310
+		if ('mailto' === $uriParts['scheme']) {
311
+			$query = 'SELECT uri FROM '.$this->tableName.' WHERE lower(email)=lower(?)';
312
+			$stmt = $this->pdo->prepare($query);
313
+			$stmt->execute([$uriParts['path']]);
314
+
315
+			while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
316
+				// Checking if the principal is in the prefix
317
+				list($rowPrefix) = Uri\split($row['uri']);
318
+				if ($rowPrefix !== $principalPrefix) {
319
+					continue;
320
+				}
321
+
322
+				$uri = $row['uri'];
323
+				break; //Stop on first match
324
+			}
325
+		} else {
326
+			$pathParts = Uri\split($uriParts['path']); // We can do this since $uriParts['path'] is not null
327
+
328
+			if (2 === count($pathParts) && $pathParts[0] === $principalPrefix) {
329
+				// Checking that this uri exists
330
+				$query = 'SELECT * FROM '.$this->tableName.' WHERE uri = ?';
331
+				$stmt = $this->pdo->prepare($query);
332
+				$stmt->execute([$uriParts['path']]);
333
+				$rows = $stmt->fetchAll();
334
+
335
+				if (count($rows) > 0) {
336
+					$uri = $uriParts['path'];
337
+				}
338
+			}
339
+		}
340
+
341
+		return $uri;
342
+	}
343
+
344
+	/**
345
+	 * Returns the list of members for a group-principal.
346
+	 *
347
+	 * @param string $principal
348
+	 *
349
+	 * @return array
350
+	 */
351
+	public function getGroupMemberSet($principal)
352
+	{
353
+		$principal = $this->getPrincipalByPath($principal);
354
+		if (!$principal) {
355
+			throw new DAV\Exception('Principal not found');
356
+		}
357
+		$stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.member_id = principals.id WHERE groupmembers.principal_id = ?');
358
+		$stmt->execute([$principal['id']]);
359
+
360
+		$result = [];
361
+		while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
362
+			$result[] = $row['uri'];
363
+		}
364
+
365
+		return $result;
366
+	}
367
+
368
+	/**
369
+	 * Returns the list of groups a principal is a member of.
370
+	 *
371
+	 * @param string $principal
372
+	 *
373
+	 * @return array
374
+	 */
375
+	public function getGroupMembership($principal)
376
+	{
377
+		$principal = $this->getPrincipalByPath($principal);
378
+		if (!$principal) {
379
+			throw new DAV\Exception('Principal not found');
380
+		}
381
+		$stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.principal_id = principals.id WHERE groupmembers.member_id = ?');
382
+		$stmt->execute([$principal['id']]);
383
+
384
+		$result = [];
385
+		while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
386
+			$result[] = $row['uri'];
387
+		}
388
+
389
+		return $result;
390
+	}
391
+
392
+	/**
393
+	 * Updates the list of group members for a group principal.
394
+	 *
395
+	 * The principals should be passed as a list of uri's.
396
+	 *
397
+	 * @param string $principal
398
+	 */
399
+	public function setGroupMemberSet($principal, array $members)
400
+	{
401
+		// Grabbing the list of principal id's.
402
+		$stmt = $this->pdo->prepare('SELECT id, uri FROM '.$this->tableName.' WHERE uri IN (? '.str_repeat(', ? ', count($members)).');');
403
+		$stmt->execute(array_merge([$principal], $members));
404
+
405
+		$memberIds = [];
406
+		$principalId = null;
407
+
408
+		while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
409
+			if ($row['uri'] == $principal) {
410
+				$principalId = $row['id'];
411
+			} else {
412
+				$memberIds[] = $row['id'];
413
+			}
414
+		}
415
+		if (!$principalId) {
416
+			throw new DAV\Exception('Principal not found');
417
+		}
418
+		// Wiping out old members
419
+		$stmt = $this->pdo->prepare('DELETE FROM '.$this->groupMembersTableName.' WHERE principal_id = ?;');
420
+		$stmt->execute([$principalId]);
421
+
422
+		foreach ($memberIds as $memberId) {
423
+			$stmt = $this->pdo->prepare('INSERT INTO '.$this->groupMembersTableName.' (principal_id, member_id) VALUES (?, ?);');
424
+			$stmt->execute([$principalId, $memberId]);
425
+		}
426
+	}
427
+
428
+	/**
429
+	 * Creates a new principal.
430
+	 *
431
+	 * This method receives a full path for the new principal. The mkCol object
432
+	 * contains any additional webdav properties specified during the creation
433
+	 * of the principal.
434
+	 *
435
+	 * @param string $path
436
+	 */
437
+	public function createPrincipal($path, MkCol $mkCol)
438
+	{
439
+		$stmt = $this->pdo->prepare('INSERT INTO '.$this->tableName.' (uri) VALUES (?)');
440
+		$stmt->execute([$path]);
441
+		$this->updatePrincipal($path, $mkCol);
442
+	}
443 443
 }
Please login to merge, or discard this patch.
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -177,7 +177,7 @@
 block discarded – undo
177 177
      */
178 178
     public function updatePrincipal($path, DAV\PropPatch $propPatch)
179 179
     {
180
-        $propPatch->handle(array_keys($this->fieldMap), function ($properties) use ($path) {
180
+        $propPatch->handle(array_keys($this->fieldMap), function($properties) use ($path) {
181 181
             $query = 'UPDATE '.$this->tableName.' SET ';
182 182
             $first = true;
183 183
 
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/dav/lib/DAVACL/PrincipalCollection.php 1 patch
Indentation   +69 added lines, -69 removed lines patch added patch discarded remove patch
@@ -20,77 +20,77 @@
 block discarded – undo
20 20
  */
21 21
 class PrincipalCollection extends AbstractPrincipalCollection implements IExtendedCollection, IACL
22 22
 {
23
-    use ACLTrait;
23
+	use ACLTrait;
24 24
 
25
-    /**
26
-     * This method returns a node for a principal.
27
-     *
28
-     * The passed array contains principal information, and is guaranteed to
29
-     * at least contain a uri item. Other properties may or may not be
30
-     * supplied by the authentication backend.
31
-     *
32
-     * @return \Sabre\DAV\INode
33
-     */
34
-    public function getChildForPrincipal(array $principal)
35
-    {
36
-        return new Principal($this->principalBackend, $principal);
37
-    }
25
+	/**
26
+	 * This method returns a node for a principal.
27
+	 *
28
+	 * The passed array contains principal information, and is guaranteed to
29
+	 * at least contain a uri item. Other properties may or may not be
30
+	 * supplied by the authentication backend.
31
+	 *
32
+	 * @return \Sabre\DAV\INode
33
+	 */
34
+	public function getChildForPrincipal(array $principal)
35
+	{
36
+		return new Principal($this->principalBackend, $principal);
37
+	}
38 38
 
39
-    /**
40
-     * Creates a new collection.
41
-     *
42
-     * This method will receive a MkCol object with all the information about
43
-     * the new collection that's being created.
44
-     *
45
-     * The MkCol object contains information about the resourceType of the new
46
-     * collection. If you don't support the specified resourceType, you should
47
-     * throw Exception\InvalidResourceType.
48
-     *
49
-     * The object also contains a list of WebDAV properties for the new
50
-     * collection.
51
-     *
52
-     * You should call the handle() method on this object to specify exactly
53
-     * which properties you are storing. This allows the system to figure out
54
-     * exactly which properties you didn't store, which in turn allows other
55
-     * plugins (such as the propertystorage plugin) to handle storing the
56
-     * property for you.
57
-     *
58
-     * @param string $name
59
-     *
60
-     * @throws InvalidResourceType
61
-     */
62
-    public function createExtendedCollection($name, MkCol $mkCol)
63
-    {
64
-        if (!$mkCol->hasResourceType('{DAV:}principal')) {
65
-            throw new InvalidResourceType('Only resources of type {DAV:}principal may be created here');
66
-        }
39
+	/**
40
+	 * Creates a new collection.
41
+	 *
42
+	 * This method will receive a MkCol object with all the information about
43
+	 * the new collection that's being created.
44
+	 *
45
+	 * The MkCol object contains information about the resourceType of the new
46
+	 * collection. If you don't support the specified resourceType, you should
47
+	 * throw Exception\InvalidResourceType.
48
+	 *
49
+	 * The object also contains a list of WebDAV properties for the new
50
+	 * collection.
51
+	 *
52
+	 * You should call the handle() method on this object to specify exactly
53
+	 * which properties you are storing. This allows the system to figure out
54
+	 * exactly which properties you didn't store, which in turn allows other
55
+	 * plugins (such as the propertystorage plugin) to handle storing the
56
+	 * property for you.
57
+	 *
58
+	 * @param string $name
59
+	 *
60
+	 * @throws InvalidResourceType
61
+	 */
62
+	public function createExtendedCollection($name, MkCol $mkCol)
63
+	{
64
+		if (!$mkCol->hasResourceType('{DAV:}principal')) {
65
+			throw new InvalidResourceType('Only resources of type {DAV:}principal may be created here');
66
+		}
67 67
 
68
-        $this->principalBackend->createPrincipal(
69
-            $this->principalPrefix.'/'.$name,
70
-            $mkCol
71
-        );
72
-    }
68
+		$this->principalBackend->createPrincipal(
69
+			$this->principalPrefix.'/'.$name,
70
+			$mkCol
71
+		);
72
+	}
73 73
 
74
-    /**
75
-     * Returns a list of ACE's for this node.
76
-     *
77
-     * Each ACE has the following properties:
78
-     *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
79
-     *     currently the only supported privileges
80
-     *   * 'principal', a url to the principal who owns the node
81
-     *   * 'protected' (optional), indicating that this ACE is not allowed to
82
-     *      be updated.
83
-     *
84
-     * @return array
85
-     */
86
-    public function getACL()
87
-    {
88
-        return [
89
-            [
90
-                'principal' => '{DAV:}authenticated',
91
-                'privilege' => '{DAV:}read',
92
-                'protected' => true,
93
-            ],
94
-        ];
95
-    }
74
+	/**
75
+	 * Returns a list of ACE's for this node.
76
+	 *
77
+	 * Each ACE has the following properties:
78
+	 *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
79
+	 *     currently the only supported privileges
80
+	 *   * 'principal', a url to the principal who owns the node
81
+	 *   * 'protected' (optional), indicating that this ACE is not allowed to
82
+	 *      be updated.
83
+	 *
84
+	 * @return array
85
+	 */
86
+	public function getACL()
87
+	{
88
+		return [
89
+			[
90
+				'principal' => '{DAV:}authenticated',
91
+				'privilege' => '{DAV:}read',
92
+				'protected' => true,
93
+			],
94
+		];
95
+	}
96 96
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/dav/lib/DAV/ICopyTarget.php 1 patch
Indentation   +18 added lines, -18 removed lines patch added patch discarded remove patch
@@ -17,22 +17,22 @@
 block discarded – undo
17 17
  */
18 18
 interface ICopyTarget extends ICollection
19 19
 {
20
-    /**
21
-     * Copies a node into this collection.
22
-     *
23
-     * It is up to the implementors to:
24
-     *   1. Create the new resource.
25
-     *   2. Copy the data and any properties.
26
-     *
27
-     * If you return true from this function, the assumption
28
-     * is that the copy was successful.
29
-     * If you return false, sabre/dav will handle the copy itself.
30
-     *
31
-     * @param string $targetName new local file/collection name
32
-     * @param string $sourcePath Full path to source node
33
-     * @param INode  $sourceNode Source node itself
34
-     *
35
-     * @return bool
36
-     */
37
-    public function copyInto($targetName, $sourcePath, INode $sourceNode);
20
+	/**
21
+	 * Copies a node into this collection.
22
+	 *
23
+	 * It is up to the implementors to:
24
+	 *   1. Create the new resource.
25
+	 *   2. Copy the data and any properties.
26
+	 *
27
+	 * If you return true from this function, the assumption
28
+	 * is that the copy was successful.
29
+	 * If you return false, sabre/dav will handle the copy itself.
30
+	 *
31
+	 * @param string $targetName new local file/collection name
32
+	 * @param string $sourcePath Full path to source node
33
+	 * @param INode  $sourceNode Source node itself
34
+	 *
35
+	 * @return bool
36
+	 */
37
+	public function copyInto($targetName, $sourcePath, INode $sourceNode);
38 38
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/dav/lib/DAV/IMoveTarget.php 1 patch
Indentation   +22 added lines, -22 removed lines patch added patch discarded remove patch
@@ -21,26 +21,26 @@
 block discarded – undo
21 21
  */
22 22
 interface IMoveTarget extends ICollection
23 23
 {
24
-    /**
25
-     * Moves a node into this collection.
26
-     *
27
-     * It is up to the implementors to:
28
-     *   1. Create the new resource.
29
-     *   2. Remove the old resource.
30
-     *   3. Transfer any properties or other data.
31
-     *
32
-     * Generally you should make very sure that your collection can easily move
33
-     * the move.
34
-     *
35
-     * If you don't, just return false, which will trigger sabre/dav to handle
36
-     * the move itself. If you return true from this function, the assumption
37
-     * is that the move was successful.
38
-     *
39
-     * @param string $targetName new local file/collection name
40
-     * @param string $sourcePath Full path to source node
41
-     * @param INode  $sourceNode Source node itself
42
-     *
43
-     * @return bool
44
-     */
45
-    public function moveInto($targetName, $sourcePath, INode $sourceNode);
24
+	/**
25
+	 * Moves a node into this collection.
26
+	 *
27
+	 * It is up to the implementors to:
28
+	 *   1. Create the new resource.
29
+	 *   2. Remove the old resource.
30
+	 *   3. Transfer any properties or other data.
31
+	 *
32
+	 * Generally you should make very sure that your collection can easily move
33
+	 * the move.
34
+	 *
35
+	 * If you don't, just return false, which will trigger sabre/dav to handle
36
+	 * the move itself. If you return true from this function, the assumption
37
+	 * is that the move was successful.
38
+	 *
39
+	 * @param string $targetName new local file/collection name
40
+	 * @param string $sourcePath Full path to source node
41
+	 * @param INode  $sourceNode Source node itself
42
+	 *
43
+	 * @return bool
44
+	 */
45
+	public function moveInto($targetName, $sourcePath, INode $sourceNode);
46 46
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/dav/lib/DAV/SimpleCollection.php 1 patch
Indentation   +82 added lines, -82 removed lines patch added patch discarded remove patch
@@ -18,92 +18,92 @@
 block discarded – undo
18 18
  */
19 19
 class SimpleCollection extends Collection
20 20
 {
21
-    /**
22
-     * List of childnodes.
23
-     *
24
-     * @var INode[]
25
-     */
26
-    protected $children = [];
21
+	/**
22
+	 * List of childnodes.
23
+	 *
24
+	 * @var INode[]
25
+	 */
26
+	protected $children = [];
27 27
 
28
-    /**
29
-     * Name of this resource.
30
-     *
31
-     * @var string
32
-     */
33
-    protected $name;
28
+	/**
29
+	 * Name of this resource.
30
+	 *
31
+	 * @var string
32
+	 */
33
+	protected $name;
34 34
 
35
-    /**
36
-     * Creates this node.
37
-     *
38
-     * The name of the node must be passed, child nodes can also be passed.
39
-     * This nodes must be instances of INode
40
-     *
41
-     * @param string  $name
42
-     * @param INode[] $children
43
-     */
44
-    public function __construct($name, array $children = [])
45
-    {
46
-        $this->name = $name;
47
-        foreach ($children as $key => $child) {
48
-            if (is_string($child)) {
49
-                $child = new SimpleFile($key, $child);
50
-            } elseif (is_array($child)) {
51
-                $child = new self($key, $child);
52
-            } elseif (!$child instanceof INode) {
53
-                throw new InvalidArgumentException('Children must be specified as strings, arrays or instances of Sabre\DAV\INode');
54
-            }
55
-            $this->addChild($child);
56
-        }
57
-    }
35
+	/**
36
+	 * Creates this node.
37
+	 *
38
+	 * The name of the node must be passed, child nodes can also be passed.
39
+	 * This nodes must be instances of INode
40
+	 *
41
+	 * @param string  $name
42
+	 * @param INode[] $children
43
+	 */
44
+	public function __construct($name, array $children = [])
45
+	{
46
+		$this->name = $name;
47
+		foreach ($children as $key => $child) {
48
+			if (is_string($child)) {
49
+				$child = new SimpleFile($key, $child);
50
+			} elseif (is_array($child)) {
51
+				$child = new self($key, $child);
52
+			} elseif (!$child instanceof INode) {
53
+				throw new InvalidArgumentException('Children must be specified as strings, arrays or instances of Sabre\DAV\INode');
54
+			}
55
+			$this->addChild($child);
56
+		}
57
+	}
58 58
 
59
-    /**
60
-     * Adds a new childnode to this collection.
61
-     */
62
-    public function addChild(INode $child)
63
-    {
64
-        $this->children[$child->getName()] = $child;
65
-    }
59
+	/**
60
+	 * Adds a new childnode to this collection.
61
+	 */
62
+	public function addChild(INode $child)
63
+	{
64
+		$this->children[$child->getName()] = $child;
65
+	}
66 66
 
67
-    /**
68
-     * Returns the name of the collection.
69
-     *
70
-     * @return string
71
-     */
72
-    public function getName()
73
-    {
74
-        return $this->name;
75
-    }
67
+	/**
68
+	 * Returns the name of the collection.
69
+	 *
70
+	 * @return string
71
+	 */
72
+	public function getName()
73
+	{
74
+		return $this->name;
75
+	}
76 76
 
77
-    /**
78
-     * Returns a child object, by its name.
79
-     *
80
-     * This method makes use of the getChildren method to grab all the child nodes, and compares the name.
81
-     * Generally its wise to override this, as this can usually be optimized
82
-     *
83
-     * This method must throw Sabre\DAV\Exception\NotFound if the node does not
84
-     * exist.
85
-     *
86
-     * @param string $name
87
-     *
88
-     * @throws Exception\NotFound
89
-     *
90
-     * @return INode
91
-     */
92
-    public function getChild($name)
93
-    {
94
-        if (isset($this->children[$name])) {
95
-            return $this->children[$name];
96
-        }
97
-        throw new Exception\NotFound('File not found: '.$name.' in \''.$this->getName().'\'');
98
-    }
77
+	/**
78
+	 * Returns a child object, by its name.
79
+	 *
80
+	 * This method makes use of the getChildren method to grab all the child nodes, and compares the name.
81
+	 * Generally its wise to override this, as this can usually be optimized
82
+	 *
83
+	 * This method must throw Sabre\DAV\Exception\NotFound if the node does not
84
+	 * exist.
85
+	 *
86
+	 * @param string $name
87
+	 *
88
+	 * @throws Exception\NotFound
89
+	 *
90
+	 * @return INode
91
+	 */
92
+	public function getChild($name)
93
+	{
94
+		if (isset($this->children[$name])) {
95
+			return $this->children[$name];
96
+		}
97
+		throw new Exception\NotFound('File not found: '.$name.' in \''.$this->getName().'\'');
98
+	}
99 99
 
100
-    /**
101
-     * Returns a list of children for this collection.
102
-     *
103
-     * @return INode[]
104
-     */
105
-    public function getChildren()
106
-    {
107
-        return array_values($this->children);
108
-    }
100
+	/**
101
+	 * Returns a list of children for this collection.
102
+	 *
103
+	 * @return INode[]
104
+	 */
105
+	public function getChildren()
106
+	{
107
+		return array_values($this->children);
108
+	}
109 109
 }
Please login to merge, or discard this patch.
htdocs/includes/sabre/sabre/dav/lib/DAV/Server.php 2 patches
Indentation   +1644 added lines, -1644 removed lines patch added patch discarded remove patch
@@ -25,1656 +25,1656 @@
 block discarded – undo
25 25
  */
26 26
 class Server implements LoggerAwareInterface, EmitterInterface
27 27
 {
28
-    use LoggerAwareTrait;
29
-    use WildcardEmitterTrait;
30
-
31
-    /**
32
-     * Infinity is used for some request supporting the HTTP Depth header and indicates that the operation should traverse the entire tree.
33
-     */
34
-    const DEPTH_INFINITY = -1;
35
-
36
-    /**
37
-     * XML namespace for all SabreDAV related elements.
38
-     */
39
-    const NS_SABREDAV = 'http://sabredav.org/ns';
40
-
41
-    /**
42
-     * The tree object.
43
-     *
44
-     * @var Tree
45
-     */
46
-    public $tree;
47
-
48
-    /**
49
-     * The base uri.
50
-     *
51
-     * @var string
52
-     */
53
-    protected $baseUri = null;
54
-
55
-    /**
56
-     * httpResponse.
57
-     *
58
-     * @var HTTP\Response
59
-     */
60
-    public $httpResponse;
61
-
62
-    /**
63
-     * httpRequest.
64
-     *
65
-     * @var HTTP\Request
66
-     */
67
-    public $httpRequest;
68
-
69
-    /**
70
-     * PHP HTTP Sapi.
71
-     *
72
-     * @var HTTP\Sapi
73
-     */
74
-    public $sapi;
75
-
76
-    /**
77
-     * The list of plugins.
78
-     *
79
-     * @var array
80
-     */
81
-    protected $plugins = [];
82
-
83
-    /**
84
-     * This property will be filled with a unique string that describes the
85
-     * transaction. This is useful for performance measuring and logging
86
-     * purposes.
87
-     *
88
-     * By default it will just fill it with a lowercased HTTP method name, but
89
-     * plugins override this. For example, the WebDAV-Sync sync-collection
90
-     * report will set this to 'report-sync-collection'.
91
-     *
92
-     * @var string
93
-     */
94
-    public $transactionType;
95
-
96
-    /**
97
-     * This is a list of properties that are always server-controlled, and
98
-     * must not get modified with PROPPATCH.
99
-     *
100
-     * Plugins may add to this list.
101
-     *
102
-     * @var string[]
103
-     */
104
-    public $protectedProperties = [
105
-        // RFC4918
106
-        '{DAV:}getcontentlength',
107
-        '{DAV:}getetag',
108
-        '{DAV:}getlastmodified',
109
-        '{DAV:}lockdiscovery',
110
-        '{DAV:}supportedlock',
111
-
112
-        // RFC4331
113
-        '{DAV:}quota-available-bytes',
114
-        '{DAV:}quota-used-bytes',
115
-
116
-        // RFC3744
117
-        '{DAV:}supported-privilege-set',
118
-        '{DAV:}current-user-privilege-set',
119
-        '{DAV:}acl',
120
-        '{DAV:}acl-restrictions',
121
-        '{DAV:}inherited-acl-set',
122
-
123
-        // RFC3253
124
-        '{DAV:}supported-method-set',
125
-        '{DAV:}supported-report-set',
126
-
127
-        // RFC6578
128
-        '{DAV:}sync-token',
129
-
130
-        // calendarserver.org extensions
131
-        '{http://calendarserver.org/ns/}ctag',
132
-
133
-        // sabredav extensions
134
-        '{http://sabredav.org/ns}sync-token',
135
-    ];
136
-
137
-    /**
138
-     * This is a flag that allow or not showing file, line and code
139
-     * of the exception in the returned XML.
140
-     *
141
-     * @var bool
142
-     */
143
-    public $debugExceptions = false;
144
-
145
-    /**
146
-     * This property allows you to automatically add the 'resourcetype' value
147
-     * based on a node's classname or interface.
148
-     *
149
-     * The preset ensures that {DAV:}collection is automatically added for nodes
150
-     * implementing Sabre\DAV\ICollection.
151
-     *
152
-     * @var array
153
-     */
154
-    public $resourceTypeMapping = [
155
-        'Sabre\\DAV\\ICollection' => '{DAV:}collection',
156
-    ];
157
-
158
-    /**
159
-     * This property allows the usage of Depth: infinity on PROPFIND requests.
160
-     *
161
-     * By default Depth: infinity is treated as Depth: 1. Allowing Depth:
162
-     * infinity is potentially risky, as it allows a single client to do a full
163
-     * index of the webdav server, which is an easy DoS attack vector.
164
-     *
165
-     * Only turn this on if you know what you're doing.
166
-     *
167
-     * @var bool
168
-     */
169
-    public $enablePropfindDepthInfinity = false;
170
-
171
-    /**
172
-     * Reference to the XML utility object.
173
-     *
174
-     * @var Xml\Service
175
-     */
176
-    public $xml;
177
-
178
-    /**
179
-     * If this setting is turned off, SabreDAV's version number will be hidden
180
-     * from various places.
181
-     *
182
-     * Some people feel this is a good security measure.
183
-     *
184
-     * @var bool
185
-     */
186
-    public static $exposeVersion = true;
187
-
188
-    /**
189
-     * If this setting is turned on, any multi status response on any PROPFIND will be streamed to the output buffer.
190
-     * This will be beneficial for large result sets which will no longer consume a large amount of memory as well as
191
-     * send back data to the client earlier.
192
-     *
193
-     * @var bool
194
-     */
195
-    public static $streamMultiStatus = false;
196
-
197
-    /**
198
-     * Sets up the server.
199
-     *
200
-     * If a Sabre\DAV\Tree object is passed as an argument, it will
201
-     * use it as the directory tree. If a Sabre\DAV\INode is passed, it
202
-     * will create a Sabre\DAV\Tree and use the node as the root.
203
-     *
204
-     * If nothing is passed, a Sabre\DAV\SimpleCollection is created in
205
-     * a Sabre\DAV\Tree.
206
-     *
207
-     * If an array is passed, we automatically create a root node, and use
208
-     * the nodes in the array as top-level children.
209
-     *
210
-     * @param Tree|INode|array|null $treeOrNode The tree object
211
-     *
212
-     * @throws Exception
213
-     */
214
-    public function __construct($treeOrNode = null, HTTP\Sapi $sapi = null)
215
-    {
216
-        if ($treeOrNode instanceof Tree) {
217
-            $this->tree = $treeOrNode;
218
-        } elseif ($treeOrNode instanceof INode) {
219
-            $this->tree = new Tree($treeOrNode);
220
-        } elseif (is_array($treeOrNode)) {
221
-            $root = new SimpleCollection('root', $treeOrNode);
222
-            $this->tree = new Tree($root);
223
-        } elseif (is_null($treeOrNode)) {
224
-            $root = new SimpleCollection('root');
225
-            $this->tree = new Tree($root);
226
-        } else {
227
-            throw new Exception('Invalid argument passed to constructor. Argument must either be an instance of Sabre\\DAV\\Tree, Sabre\\DAV\\INode, an array or null');
228
-        }
229
-
230
-        $this->xml = new Xml\Service();
231
-        $this->sapi = $sapi ?? new HTTP\Sapi();
232
-        $this->httpResponse = new HTTP\Response();
233
-        $this->httpRequest = $this->sapi->getRequest();
234
-        $this->addPlugin(new CorePlugin());
235
-    }
236
-
237
-    /**
238
-     * Starts the DAV Server.
239
-     */
240
-    public function start()
241
-    {
242
-        try {
243
-            // If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an
244
-            // origin, we must make sure we send back HTTP/1.0 if this was
245
-            // requested.
246
-            // This is mainly because nginx doesn't support Chunked Transfer
247
-            // Encoding, and this forces the webserver SabreDAV is running on,
248
-            // to buffer entire responses to calculate Content-Length.
249
-            $this->httpResponse->setHTTPVersion($this->httpRequest->getHTTPVersion());
250
-
251
-            // Setting the base url
252
-            $this->httpRequest->setBaseUrl($this->getBaseUri());
253
-            $this->invokeMethod($this->httpRequest, $this->httpResponse);
254
-        } catch (\Throwable $e) {
255
-            try {
256
-                $this->emit('exception', [$e]);
257
-            } catch (\Exception $ignore) {
258
-            }
259
-            $DOM = new \DOMDocument('1.0', 'utf-8');
260
-            $DOM->formatOutput = true;
261
-
262
-            $error = $DOM->createElementNS('DAV:', 'd:error');
263
-            $error->setAttribute('xmlns:s', self::NS_SABREDAV);
264
-            $DOM->appendChild($error);
265
-
266
-            $h = function ($v) {
267
-                return htmlspecialchars((string) $v, ENT_NOQUOTES, 'UTF-8');
268
-            };
269
-
270
-            if (self::$exposeVersion) {
271
-                $error->appendChild($DOM->createElement('s:sabredav-version', $h(Version::VERSION)));
272
-            }
273
-
274
-            $error->appendChild($DOM->createElement('s:exception', $h(get_class($e))));
275
-            $error->appendChild($DOM->createElement('s:message', $h($e->getMessage())));
276
-            if ($this->debugExceptions) {
277
-                $error->appendChild($DOM->createElement('s:file', $h($e->getFile())));
278
-                $error->appendChild($DOM->createElement('s:line', $h($e->getLine())));
279
-                $error->appendChild($DOM->createElement('s:code', $h($e->getCode())));
280
-                $error->appendChild($DOM->createElement('s:stacktrace', $h($e->getTraceAsString())));
281
-            }
282
-
283
-            if ($this->debugExceptions) {
284
-                $previous = $e;
285
-                while ($previous = $previous->getPrevious()) {
286
-                    $xPrevious = $DOM->createElement('s:previous-exception');
287
-                    $xPrevious->appendChild($DOM->createElement('s:exception', $h(get_class($previous))));
288
-                    $xPrevious->appendChild($DOM->createElement('s:message', $h($previous->getMessage())));
289
-                    $xPrevious->appendChild($DOM->createElement('s:file', $h($previous->getFile())));
290
-                    $xPrevious->appendChild($DOM->createElement('s:line', $h($previous->getLine())));
291
-                    $xPrevious->appendChild($DOM->createElement('s:code', $h($previous->getCode())));
292
-                    $xPrevious->appendChild($DOM->createElement('s:stacktrace', $h($previous->getTraceAsString())));
293
-                    $error->appendChild($xPrevious);
294
-                }
295
-            }
296
-
297
-            if ($e instanceof Exception) {
298
-                $httpCode = $e->getHTTPCode();
299
-                $e->serialize($this, $error);
300
-                $headers = $e->getHTTPHeaders($this);
301
-            } else {
302
-                $httpCode = 500;
303
-                $headers = [];
304
-            }
305
-            $headers['Content-Type'] = 'application/xml; charset=utf-8';
306
-
307
-            $this->httpResponse->setStatus($httpCode);
308
-            $this->httpResponse->setHeaders($headers);
309
-            $this->httpResponse->setBody($DOM->saveXML());
310
-            $this->sapi->sendResponse($this->httpResponse);
311
-        }
312
-    }
313
-
314
-    /**
315
-     * Alias of start().
316
-     *
317
-     * @deprecated
318
-     */
319
-    public function exec()
320
-    {
321
-        $this->start();
322
-    }
323
-
324
-    /**
325
-     * Sets the base server uri.
326
-     *
327
-     * @param string $uri
328
-     */
329
-    public function setBaseUri($uri)
330
-    {
331
-        // If the baseUri does not end with a slash, we must add it
332
-        if ('/' !== $uri[strlen($uri) - 1]) {
333
-            $uri .= '/';
334
-        }
335
-
336
-        $this->baseUri = $uri;
337
-    }
338
-
339
-    /**
340
-     * Returns the base responding uri.
341
-     *
342
-     * @return string
343
-     */
344
-    public function getBaseUri()
345
-    {
346
-        if (is_null($this->baseUri)) {
347
-            $this->baseUri = $this->guessBaseUri();
348
-        }
349
-
350
-        return $this->baseUri;
351
-    }
352
-
353
-    /**
354
-     * This method attempts to detect the base uri.
355
-     * Only the PATH_INFO variable is considered.
356
-     *
357
-     * If this variable is not set, the root (/) is assumed.
358
-     *
359
-     * @return string
360
-     */
361
-    public function guessBaseUri()
362
-    {
363
-        $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO');
364
-        $uri = $this->httpRequest->getRawServerValue('REQUEST_URI');
365
-
366
-        // If PATH_INFO is found, we can assume it's accurate.
367
-        if (!empty($pathInfo)) {
368
-            // We need to make sure we ignore the QUERY_STRING part
369
-            if ($pos = strpos($uri, '?')) {
370
-                $uri = substr($uri, 0, $pos);
371
-            }
372
-
373
-            // PATH_INFO is only set for urls, such as: /example.php/path
374
-            // in that case PATH_INFO contains '/path'.
375
-            // Note that REQUEST_URI is percent encoded, while PATH_INFO is
376
-            // not, Therefore they are only comparable if we first decode
377
-            // REQUEST_INFO as well.
378
-            $decodedUri = HTTP\decodePath($uri);
379
-
380
-            // A simple sanity check:
381
-            if (substr($decodedUri, strlen($decodedUri) - strlen($pathInfo)) === $pathInfo) {
382
-                $baseUri = substr($decodedUri, 0, strlen($decodedUri) - strlen($pathInfo));
383
-
384
-                return rtrim($baseUri, '/').'/';
385
-            }
386
-
387
-            throw new Exception('The REQUEST_URI ('.$uri.') did not end with the contents of PATH_INFO ('.$pathInfo.'). This server might be misconfigured.');
388
-        }
389
-
390
-        // The last fallback is that we're just going to assume the server root.
391
-        return '/';
392
-    }
393
-
394
-    /**
395
-     * Adds a plugin to the server.
396
-     *
397
-     * For more information, console the documentation of Sabre\DAV\ServerPlugin
398
-     */
399
-    public function addPlugin(ServerPlugin $plugin)
400
-    {
401
-        $this->plugins[$plugin->getPluginName()] = $plugin;
402
-        $plugin->initialize($this);
403
-    }
404
-
405
-    /**
406
-     * Returns an initialized plugin by it's name.
407
-     *
408
-     * This function returns null if the plugin was not found.
409
-     *
410
-     * @param string $name
411
-     *
412
-     * @return ServerPlugin
413
-     */
414
-    public function getPlugin($name)
415
-    {
416
-        if (isset($this->plugins[$name])) {
417
-            return $this->plugins[$name];
418
-        }
419
-
420
-        return null;
421
-    }
422
-
423
-    /**
424
-     * Returns all plugins.
425
-     *
426
-     * @return array
427
-     */
428
-    public function getPlugins()
429
-    {
430
-        return $this->plugins;
431
-    }
432
-
433
-    /**
434
-     * Returns the PSR-3 logger object.
435
-     *
436
-     * @return LoggerInterface
437
-     */
438
-    public function getLogger()
439
-    {
440
-        if (!$this->logger) {
441
-            $this->logger = new NullLogger();
442
-        }
443
-
444
-        return $this->logger;
445
-    }
446
-
447
-    /**
448
-     * Handles a http request, and execute a method based on its name.
449
-     *
450
-     * @param bool $sendResponse whether to send the HTTP response to the DAV client
451
-     */
452
-    public function invokeMethod(RequestInterface $request, ResponseInterface $response, $sendResponse = true)
453
-    {
454
-        $method = $request->getMethod();
455
-
456
-        if (!$this->emit('beforeMethod:'.$method, [$request, $response])) {
457
-            return;
458
-        }
459
-
460
-        if (self::$exposeVersion) {
461
-            $response->setHeader('X-Sabre-Version', Version::VERSION);
462
-        }
463
-
464
-        $this->transactionType = strtolower($method);
465
-
466
-        if (!$this->checkPreconditions($request, $response)) {
467
-            $this->sapi->sendResponse($response);
468
-
469
-            return;
470
-        }
471
-
472
-        if ($this->emit('method:'.$method, [$request, $response])) {
473
-            $exMessage = 'There was no plugin in the system that was willing to handle this '.$method.' method.';
474
-            if ('GET' === $method) {
475
-                $exMessage .= ' Enable the Browser plugin to get a better result here.';
476
-            }
477
-
478
-            // Unsupported method
479
-            throw new Exception\NotImplemented($exMessage);
480
-        }
481
-
482
-        if (!$this->emit('afterMethod:'.$method, [$request, $response])) {
483
-            return;
484
-        }
485
-
486
-        if (null === $response->getStatus()) {
487
-            throw new Exception('No subsystem set a valid HTTP status code. Something must have interrupted the request without providing further detail.');
488
-        }
489
-        if ($sendResponse) {
490
-            $this->sapi->sendResponse($response);
491
-            $this->emit('afterResponse', [$request, $response]);
492
-        }
493
-    }
494
-
495
-    // {{{ HTTP/WebDAV protocol helpers
496
-
497
-    /**
498
-     * Returns an array with all the supported HTTP methods for a specific uri.
499
-     *
500
-     * @param string $path
501
-     *
502
-     * @return array
503
-     */
504
-    public function getAllowedMethods($path)
505
-    {
506
-        $methods = [
507
-            'OPTIONS',
508
-            'GET',
509
-            'HEAD',
510
-            'DELETE',
511
-            'PROPFIND',
512
-            'PUT',
513
-            'PROPPATCH',
514
-            'COPY',
515
-            'MOVE',
516
-            'REPORT',
517
-        ];
518
-
519
-        // The MKCOL is only allowed on an unmapped uri
520
-        try {
521
-            $this->tree->getNodeForPath($path);
522
-        } catch (Exception\NotFound $e) {
523
-            $methods[] = 'MKCOL';
524
-        }
525
-
526
-        // We're also checking if any of the plugins register any new methods
527
-        foreach ($this->plugins as $plugin) {
528
-            $methods = array_merge($methods, $plugin->getHTTPMethods($path));
529
-        }
530
-        array_unique($methods);
531
-
532
-        return $methods;
533
-    }
534
-
535
-    /**
536
-     * Gets the uri for the request, keeping the base uri into consideration.
537
-     *
538
-     * @return string
539
-     */
540
-    public function getRequestUri()
541
-    {
542
-        return $this->calculateUri($this->httpRequest->getUrl());
543
-    }
544
-
545
-    /**
546
-     * Turns a URI such as the REQUEST_URI into a local path.
547
-     *
548
-     * This method:
549
-     *   * strips off the base path
550
-     *   * normalizes the path
551
-     *   * uri-decodes the path
552
-     *
553
-     * @param string $uri
554
-     *
555
-     * @throws Exception\Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri
556
-     *
557
-     * @return string
558
-     */
559
-    public function calculateUri($uri)
560
-    {
561
-        if ('' != $uri && '/' != $uri[0] && strpos($uri, '://')) {
562
-            $uri = parse_url($uri, PHP_URL_PATH);
563
-        }
564
-
565
-        $uri = Uri\normalize(preg_replace('|/+|', '/', $uri));
566
-        $baseUri = Uri\normalize($this->getBaseUri());
567
-
568
-        if (0 === strpos($uri, $baseUri)) {
569
-            return trim(HTTP\decodePath(substr($uri, strlen($baseUri))), '/');
570
-
571
-        // A special case, if the baseUri was accessed without a trailing
572
-        // slash, we'll accept it as well.
573
-        } elseif ($uri.'/' === $baseUri) {
574
-            return '';
575
-        } else {
576
-            throw new Exception\Forbidden('Requested uri ('.$uri.') is out of base uri ('.$this->getBaseUri().')');
577
-        }
578
-    }
579
-
580
-    /**
581
-     * Returns the HTTP depth header.
582
-     *
583
-     * This method returns the contents of the HTTP depth request header. If the depth header was 'infinity' it will return the Sabre\DAV\Server::DEPTH_INFINITY object
584
-     * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent
585
-     *
586
-     * @param mixed $default
587
-     *
588
-     * @return int
589
-     */
590
-    public function getHTTPDepth($default = self::DEPTH_INFINITY)
591
-    {
592
-        // If its not set, we'll grab the default
593
-        $depth = $this->httpRequest->getHeader('Depth');
594
-
595
-        if (is_null($depth)) {
596
-            return $default;
597
-        }
598
-
599
-        if ('infinity' == $depth) {
600
-            return self::DEPTH_INFINITY;
601
-        }
602
-
603
-        // If its an unknown value. we'll grab the default
604
-        if (!ctype_digit($depth)) {
605
-            return $default;
606
-        }
607
-
608
-        return (int) $depth;
609
-    }
610
-
611
-    /**
612
-     * Returns the HTTP range header.
613
-     *
614
-     * This method returns null if there is no well-formed HTTP range request
615
-     * header or array($start, $end).
616
-     *
617
-     * The first number is the offset of the first byte in the range.
618
-     * The second number is the offset of the last byte in the range.
619
-     *
620
-     * If the second offset is null, it should be treated as the offset of the last byte of the entity
621
-     * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity
622
-     *
623
-     * @return int[]|null
624
-     */
625
-    public function getHTTPRange()
626
-    {
627
-        $range = $this->httpRequest->getHeader('range');
628
-        if (is_null($range)) {
629
-            return null;
630
-        }
631
-
632
-        // Matching "Range: bytes=1234-5678: both numbers are optional
633
-
634
-        if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i', $range, $matches)) {
635
-            return null;
636
-        }
637
-
638
-        if ('' === $matches[1] && '' === $matches[2]) {
639
-            return null;
640
-        }
641
-
642
-        return [
643
-            '' !== $matches[1] ? (int) $matches[1] : null,
644
-            '' !== $matches[2] ? (int) $matches[2] : null,
645
-        ];
646
-    }
647
-
648
-    /**
649
-     * Returns the HTTP Prefer header information.
650
-     *
651
-     * The prefer header is defined in:
652
-     * http://tools.ietf.org/html/draft-snell-http-prefer-14
653
-     *
654
-     * This method will return an array with options.
655
-     *
656
-     * Currently, the following options may be returned:
657
-     *  [
658
-     *      'return-asynch'         => true,
659
-     *      'return-minimal'        => true,
660
-     *      'return-representation' => true,
661
-     *      'wait'                  => 30,
662
-     *      'strict'                => true,
663
-     *      'lenient'               => true,
664
-     *  ]
665
-     *
666
-     * This method also supports the Brief header, and will also return
667
-     * 'return-minimal' if the brief header was set to 't'.
668
-     *
669
-     * For the boolean options, false will be returned if the headers are not
670
-     * specified. For the integer options it will be 'null'.
671
-     *
672
-     * @return array
673
-     */
674
-    public function getHTTPPrefer()
675
-    {
676
-        $result = [
677
-            // can be true or false
678
-            'respond-async' => false,
679
-            // Could be set to 'representation' or 'minimal'.
680
-            'return' => null,
681
-            // Used as a timeout, is usually a number.
682
-            'wait' => null,
683
-            // can be 'strict' or 'lenient'.
684
-            'handling' => false,
685
-        ];
686
-
687
-        if ($prefer = $this->httpRequest->getHeader('Prefer')) {
688
-            $result = array_merge(
689
-                $result,
690
-                HTTP\parsePrefer($prefer)
691
-            );
692
-        } elseif ('t' == $this->httpRequest->getHeader('Brief')) {
693
-            $result['return'] = 'minimal';
694
-        }
695
-
696
-        return $result;
697
-    }
698
-
699
-    /**
700
-     * Returns information about Copy and Move requests.
701
-     *
702
-     * This function is created to help getting information about the source and the destination for the
703
-     * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions
704
-     *
705
-     * The returned value is an array with the following keys:
706
-     *   * destination - Destination path
707
-     *   * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten)
708
-     *
709
-     * @throws Exception\BadRequest           upon missing or broken request headers
710
-     * @throws Exception\UnsupportedMediaType when trying to copy into a
711
-     *                                        non-collection
712
-     * @throws Exception\PreconditionFailed   if overwrite is set to false, but
713
-     *                                        the destination exists
714
-     * @throws Exception\Forbidden            when source and destination paths are
715
-     *                                        identical
716
-     * @throws Exception\Conflict             when trying to copy a node into its own
717
-     *                                        subtree
718
-     *
719
-     * @return array
720
-     */
721
-    public function getCopyAndMoveInfo(RequestInterface $request)
722
-    {
723
-        // Collecting the relevant HTTP headers
724
-        if (!$request->getHeader('Destination')) {
725
-            throw new Exception\BadRequest('The destination header was not supplied');
726
-        }
727
-        $destination = $this->calculateUri($request->getHeader('Destination'));
728
-        $overwrite = $request->getHeader('Overwrite');
729
-        if (!$overwrite) {
730
-            $overwrite = 'T';
731
-        }
732
-        if ('T' == strtoupper($overwrite)) {
733
-            $overwrite = true;
734
-        } elseif ('F' == strtoupper($overwrite)) {
735
-            $overwrite = false;
736
-        }
737
-        // We need to throw a bad request exception, if the header was invalid
738
-        else {
739
-            throw new Exception\BadRequest('The HTTP Overwrite header should be either T or F');
740
-        }
741
-        list($destinationDir) = Uri\split($destination);
742
-
743
-        try {
744
-            $destinationParent = $this->tree->getNodeForPath($destinationDir);
745
-            if (!($destinationParent instanceof ICollection)) {
746
-                throw new Exception\UnsupportedMediaType('The destination node is not a collection');
747
-            }
748
-        } catch (Exception\NotFound $e) {
749
-            // If the destination parent node is not found, we throw a 409
750
-            throw new Exception\Conflict('The destination node is not found');
751
-        }
752
-
753
-        try {
754
-            $destinationNode = $this->tree->getNodeForPath($destination);
755
-
756
-            // If this succeeded, it means the destination already exists
757
-            // we'll need to throw precondition failed in case overwrite is false
758
-            if (!$overwrite) {
759
-                throw new Exception\PreconditionFailed('The destination node already exists, and the overwrite header is set to false', 'Overwrite');
760
-            }
761
-        } catch (Exception\NotFound $e) {
762
-            // Destination didn't exist, we're all good
763
-            $destinationNode = false;
764
-        }
765
-
766
-        $requestPath = $request->getPath();
767
-        if ($destination === $requestPath) {
768
-            throw new Exception\Forbidden('Source and destination uri are identical.');
769
-        }
770
-        if (substr($destination, 0, strlen($requestPath) + 1) === $requestPath.'/') {
771
-            throw new Exception\Conflict('The destination may not be part of the same subtree as the source path.');
772
-        }
773
-
774
-        // These are the three relevant properties we need to return
775
-        return [
776
-            'destination' => $destination,
777
-            'destinationExists' => (bool) $destinationNode,
778
-            'destinationNode' => $destinationNode,
779
-        ];
780
-    }
781
-
782
-    /**
783
-     * Returns a list of properties for a path.
784
-     *
785
-     * This is a simplified version getPropertiesForPath. If you aren't
786
-     * interested in status codes, but you just want to have a flat list of
787
-     * properties, use this method.
788
-     *
789
-     * Please note though that any problems related to retrieving properties,
790
-     * such as permission issues will just result in an empty array being
791
-     * returned.
792
-     *
793
-     * @param string $path
794
-     * @param array  $propertyNames
795
-     *
796
-     * @return array
797
-     */
798
-    public function getProperties($path, $propertyNames)
799
-    {
800
-        $result = $this->getPropertiesForPath($path, $propertyNames, 0);
801
-        if (isset($result[0][200])) {
802
-            return $result[0][200];
803
-        } else {
804
-            return [];
805
-        }
806
-    }
807
-
808
-    /**
809
-     * A kid-friendly way to fetch properties for a node's children.
810
-     *
811
-     * The returned array will be indexed by the path of the of child node.
812
-     * Only properties that are actually found will be returned.
813
-     *
814
-     * The parent node will not be returned.
815
-     *
816
-     * @param string $path
817
-     * @param array  $propertyNames
818
-     *
819
-     * @return array
820
-     */
821
-    public function getPropertiesForChildren($path, $propertyNames)
822
-    {
823
-        $result = [];
824
-        foreach ($this->getPropertiesForPath($path, $propertyNames, 1) as $k => $row) {
825
-            // Skipping the parent path
826
-            if (0 === $k) {
827
-                continue;
828
-            }
829
-
830
-            $result[$row['href']] = $row[200];
831
-        }
832
-
833
-        return $result;
834
-    }
835
-
836
-    /**
837
-     * Returns a list of HTTP headers for a particular resource.
838
-     *
839
-     * The generated http headers are based on properties provided by the
840
-     * resource. The method basically provides a simple mapping between
841
-     * DAV property and HTTP header.
842
-     *
843
-     * The headers are intended to be used for HEAD and GET requests.
844
-     *
845
-     * @param string $path
846
-     *
847
-     * @return array
848
-     */
849
-    public function getHTTPHeaders($path)
850
-    {
851
-        $propertyMap = [
852
-            '{DAV:}getcontenttype' => 'Content-Type',
853
-            '{DAV:}getcontentlength' => 'Content-Length',
854
-            '{DAV:}getlastmodified' => 'Last-Modified',
855
-            '{DAV:}getetag' => 'ETag',
856
-        ];
857
-
858
-        $properties = $this->getProperties($path, array_keys($propertyMap));
859
-
860
-        $headers = [];
861
-        foreach ($propertyMap as $property => $header) {
862
-            if (!isset($properties[$property])) {
863
-                continue;
864
-            }
865
-
866
-            if (is_scalar($properties[$property])) {
867
-                $headers[$header] = $properties[$property];
868
-
869
-            // GetLastModified gets special cased
870
-            } elseif ($properties[$property] instanceof Xml\Property\GetLastModified) {
871
-                $headers[$header] = HTTP\toDate($properties[$property]->getTime());
872
-            }
873
-        }
874
-
875
-        return $headers;
876
-    }
877
-
878
-    /**
879
-     * Small helper to support PROPFIND with DEPTH_INFINITY.
880
-     *
881
-     * @param array $yieldFirst
882
-     *
883
-     * @return \Traversable
884
-     */
885
-    private function generatePathNodes(PropFind $propFind, array $yieldFirst = null)
886
-    {
887
-        if (null !== $yieldFirst) {
888
-            yield $yieldFirst;
889
-        }
890
-        $newDepth = $propFind->getDepth();
891
-        $path = $propFind->getPath();
892
-
893
-        if (self::DEPTH_INFINITY !== $newDepth) {
894
-            --$newDepth;
895
-        }
896
-
897
-        $propertyNames = $propFind->getRequestedProperties();
898
-        $propFindType = !$propFind->isAllProps() ? PropFind::NORMAL : PropFind::ALLPROPS;
899
-
900
-        foreach ($this->tree->getChildren($path) as $childNode) {
901
-            if ('' !== $path) {
902
-                $subPath = $path.'/'.$childNode->getName();
903
-            } else {
904
-                $subPath = $childNode->getName();
905
-            }
906
-            $subPropFind = new PropFind($subPath, $propertyNames, $newDepth, $propFindType);
907
-
908
-            yield [
909
-                $subPropFind,
910
-                $childNode,
911
-            ];
912
-
913
-            if ((self::DEPTH_INFINITY === $newDepth || $newDepth >= 1) && $childNode instanceof ICollection) {
914
-                foreach ($this->generatePathNodes($subPropFind) as $subItem) {
915
-                    yield $subItem;
916
-                }
917
-            }
918
-        }
919
-    }
920
-
921
-    /**
922
-     * Returns a list of properties for a given path.
923
-     *
924
-     * The path that should be supplied should have the baseUrl stripped out
925
-     * The list of properties should be supplied in Clark notation. If the list is empty
926
-     * 'allprops' is assumed.
927
-     *
928
-     * If a depth of 1 is requested child elements will also be returned.
929
-     *
930
-     * @param string $path
931
-     * @param array  $propertyNames
932
-     * @param int    $depth
933
-     *
934
-     * @return array
935
-     *
936
-     * @deprecated Use getPropertiesIteratorForPath() instead (as it's more memory efficient)
937
-     * @see getPropertiesIteratorForPath()
938
-     */
939
-    public function getPropertiesForPath($path, $propertyNames = [], $depth = 0)
940
-    {
941
-        return iterator_to_array($this->getPropertiesIteratorForPath($path, $propertyNames, $depth));
942
-    }
943
-
944
-    /**
945
-     * Returns a list of properties for a given path.
946
-     *
947
-     * The path that should be supplied should have the baseUrl stripped out
948
-     * The list of properties should be supplied in Clark notation. If the list is empty
949
-     * 'allprops' is assumed.
950
-     *
951
-     * If a depth of 1 is requested child elements will also be returned.
952
-     *
953
-     * @param string $path
954
-     * @param array  $propertyNames
955
-     * @param int    $depth
956
-     *
957
-     * @return \Iterator
958
-     */
959
-    public function getPropertiesIteratorForPath($path, $propertyNames = [], $depth = 0)
960
-    {
961
-        // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled
962
-        if (!$this->enablePropfindDepthInfinity && 0 != $depth) {
963
-            $depth = 1;
964
-        }
965
-
966
-        $path = trim($path, '/');
967
-
968
-        $propFindType = $propertyNames ? PropFind::NORMAL : PropFind::ALLPROPS;
969
-        $propFind = new PropFind($path, (array) $propertyNames, $depth, $propFindType);
970
-
971
-        $parentNode = $this->tree->getNodeForPath($path);
972
-
973
-        $propFindRequests = [[
974
-            $propFind,
975
-            $parentNode,
976
-        ]];
977
-
978
-        if (($depth > 0 || self::DEPTH_INFINITY === $depth) && $parentNode instanceof ICollection) {
979
-            $propFindRequests = $this->generatePathNodes(clone $propFind, current($propFindRequests));
980
-        }
981
-
982
-        foreach ($propFindRequests as $propFindRequest) {
983
-            list($propFind, $node) = $propFindRequest;
984
-            $r = $this->getPropertiesByNode($propFind, $node);
985
-            if ($r) {
986
-                $result = $propFind->getResultForMultiStatus();
987
-                $result['href'] = $propFind->getPath();
988
-
989
-                // WebDAV recommends adding a slash to the path, if the path is
990
-                // a collection.
991
-                // Furthermore, iCal also demands this to be the case for
992
-                // principals. This is non-standard, but we support it.
993
-                $resourceType = $this->getResourceTypeForNode($node);
994
-                if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) {
995
-                    $result['href'] .= '/';
996
-                }
997
-                yield $result;
998
-            }
999
-        }
1000
-    }
1001
-
1002
-    /**
1003
-     * Returns a list of properties for a list of paths.
1004
-     *
1005
-     * The path that should be supplied should have the baseUrl stripped out
1006
-     * The list of properties should be supplied in Clark notation. If the list is empty
1007
-     * 'allprops' is assumed.
1008
-     *
1009
-     * The result is returned as an array, with paths for it's keys.
1010
-     * The result may be returned out of order.
1011
-     *
1012
-     * @return array
1013
-     */
1014
-    public function getPropertiesForMultiplePaths(array $paths, array $propertyNames = [])
1015
-    {
1016
-        $result = [
1017
-        ];
1018
-
1019
-        $nodes = $this->tree->getMultipleNodes($paths);
1020
-
1021
-        foreach ($nodes as $path => $node) {
1022
-            $propFind = new PropFind($path, $propertyNames);
1023
-            $r = $this->getPropertiesByNode($propFind, $node);
1024
-            if ($r) {
1025
-                $result[$path] = $propFind->getResultForMultiStatus();
1026
-                $result[$path]['href'] = $path;
1027
-
1028
-                $resourceType = $this->getResourceTypeForNode($node);
1029
-                if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) {
1030
-                    $result[$path]['href'] .= '/';
1031
-                }
1032
-            }
1033
-        }
1034
-
1035
-        return $result;
1036
-    }
1037
-
1038
-    /**
1039
-     * Determines all properties for a node.
1040
-     *
1041
-     * This method tries to grab all properties for a node. This method is used
1042
-     * internally getPropertiesForPath and a few others.
1043
-     *
1044
-     * It could be useful to call this, if you already have an instance of your
1045
-     * target node and simply want to run through the system to get a correct
1046
-     * list of properties.
1047
-     *
1048
-     * @return bool
1049
-     */
1050
-    public function getPropertiesByNode(PropFind $propFind, INode $node)
1051
-    {
1052
-        return $this->emit('propFind', [$propFind, $node]);
1053
-    }
1054
-
1055
-    /**
1056
-     * This method is invoked by sub-systems creating a new file.
1057
-     *
1058
-     * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin).
1059
-     * It was important to get this done through a centralized function,
1060
-     * allowing plugins to intercept this using the beforeCreateFile event.
1061
-     *
1062
-     * This method will return true if the file was actually created
1063
-     *
1064
-     * @param string   $uri
1065
-     * @param resource $data
1066
-     * @param string   $etag
1067
-     *
1068
-     * @return bool
1069
-     */
1070
-    public function createFile($uri, $data, &$etag = null)
1071
-    {
1072
-        list($dir, $name) = Uri\split($uri);
1073
-
1074
-        if (!$this->emit('beforeBind', [$uri])) {
1075
-            return false;
1076
-        }
1077
-
1078
-        try {
1079
-            $parent = $this->tree->getNodeForPath($dir);
1080
-        } catch (Exception\NotFound $e) {
1081
-            throw new Exception\Conflict('Files cannot be created in non-existent collections');
1082
-        }
1083
-
1084
-        if (!$parent instanceof ICollection) {
1085
-            throw new Exception\Conflict('Files can only be created as children of collections');
1086
-        }
1087
-
1088
-        // It is possible for an event handler to modify the content of the
1089
-        // body, before it gets written. If this is the case, $modified
1090
-        // should be set to true.
1091
-        //
1092
-        // If $modified is true, we must not send back an ETag.
1093
-        $modified = false;
1094
-        if (!$this->emit('beforeCreateFile', [$uri, &$data, $parent, &$modified])) {
1095
-            return false;
1096
-        }
1097
-
1098
-        $etag = $parent->createFile($name, $data);
1099
-
1100
-        if ($modified) {
1101
-            $etag = null;
1102
-        }
1103
-
1104
-        $this->tree->markDirty($dir.'/'.$name);
1105
-
1106
-        $this->emit('afterBind', [$uri]);
1107
-        $this->emit('afterCreateFile', [$uri, $parent]);
1108
-
1109
-        return true;
1110
-    }
1111
-
1112
-    /**
1113
-     * This method is invoked by sub-systems updating a file.
1114
-     *
1115
-     * This method will return true if the file was actually updated
1116
-     *
1117
-     * @param string   $uri
1118
-     * @param resource $data
1119
-     * @param string   $etag
1120
-     *
1121
-     * @return bool
1122
-     */
1123
-    public function updateFile($uri, $data, &$etag = null)
1124
-    {
1125
-        $node = $this->tree->getNodeForPath($uri);
1126
-
1127
-        // It is possible for an event handler to modify the content of the
1128
-        // body, before it gets written. If this is the case, $modified
1129
-        // should be set to true.
1130
-        //
1131
-        // If $modified is true, we must not send back an ETag.
1132
-        $modified = false;
1133
-        if (!$this->emit('beforeWriteContent', [$uri, $node, &$data, &$modified])) {
1134
-            return false;
1135
-        }
1136
-
1137
-        $etag = $node->put($data);
1138
-        if ($modified) {
1139
-            $etag = null;
1140
-        }
1141
-        $this->emit('afterWriteContent', [$uri, $node]);
1142
-
1143
-        return true;
1144
-    }
1145
-
1146
-    /**
1147
-     * This method is invoked by sub-systems creating a new directory.
1148
-     *
1149
-     * @param string $uri
1150
-     */
1151
-    public function createDirectory($uri)
1152
-    {
1153
-        $this->createCollection($uri, new MkCol(['{DAV:}collection'], []));
1154
-    }
1155
-
1156
-    /**
1157
-     * Use this method to create a new collection.
1158
-     *
1159
-     * @param string $uri The new uri
1160
-     *
1161
-     * @return array|null
1162
-     */
1163
-    public function createCollection($uri, MkCol $mkCol)
1164
-    {
1165
-        list($parentUri, $newName) = Uri\split($uri);
1166
-
1167
-        // Making sure the parent exists
1168
-        try {
1169
-            $parent = $this->tree->getNodeForPath($parentUri);
1170
-        } catch (Exception\NotFound $e) {
1171
-            throw new Exception\Conflict('Parent node does not exist');
1172
-        }
1173
-
1174
-        // Making sure the parent is a collection
1175
-        if (!$parent instanceof ICollection) {
1176
-            throw new Exception\Conflict('Parent node is not a collection');
1177
-        }
1178
-
1179
-        // Making sure the child does not already exist
1180
-        try {
1181
-            $parent->getChild($newName);
1182
-
1183
-            // If we got here.. it means there's already a node on that url, and we need to throw a 405
1184
-            throw new Exception\MethodNotAllowed('The resource you tried to create already exists');
1185
-        } catch (Exception\NotFound $e) {
1186
-            // NotFound is the expected behavior.
1187
-        }
1188
-
1189
-        if (!$this->emit('beforeBind', [$uri])) {
1190
-            return;
1191
-        }
1192
-
1193
-        if ($parent instanceof IExtendedCollection) {
1194
-            /*
28
+	use LoggerAwareTrait;
29
+	use WildcardEmitterTrait;
30
+
31
+	/**
32
+	 * Infinity is used for some request supporting the HTTP Depth header and indicates that the operation should traverse the entire tree.
33
+	 */
34
+	const DEPTH_INFINITY = -1;
35
+
36
+	/**
37
+	 * XML namespace for all SabreDAV related elements.
38
+	 */
39
+	const NS_SABREDAV = 'http://sabredav.org/ns';
40
+
41
+	/**
42
+	 * The tree object.
43
+	 *
44
+	 * @var Tree
45
+	 */
46
+	public $tree;
47
+
48
+	/**
49
+	 * The base uri.
50
+	 *
51
+	 * @var string
52
+	 */
53
+	protected $baseUri = null;
54
+
55
+	/**
56
+	 * httpResponse.
57
+	 *
58
+	 * @var HTTP\Response
59
+	 */
60
+	public $httpResponse;
61
+
62
+	/**
63
+	 * httpRequest.
64
+	 *
65
+	 * @var HTTP\Request
66
+	 */
67
+	public $httpRequest;
68
+
69
+	/**
70
+	 * PHP HTTP Sapi.
71
+	 *
72
+	 * @var HTTP\Sapi
73
+	 */
74
+	public $sapi;
75
+
76
+	/**
77
+	 * The list of plugins.
78
+	 *
79
+	 * @var array
80
+	 */
81
+	protected $plugins = [];
82
+
83
+	/**
84
+	 * This property will be filled with a unique string that describes the
85
+	 * transaction. This is useful for performance measuring and logging
86
+	 * purposes.
87
+	 *
88
+	 * By default it will just fill it with a lowercased HTTP method name, but
89
+	 * plugins override this. For example, the WebDAV-Sync sync-collection
90
+	 * report will set this to 'report-sync-collection'.
91
+	 *
92
+	 * @var string
93
+	 */
94
+	public $transactionType;
95
+
96
+	/**
97
+	 * This is a list of properties that are always server-controlled, and
98
+	 * must not get modified with PROPPATCH.
99
+	 *
100
+	 * Plugins may add to this list.
101
+	 *
102
+	 * @var string[]
103
+	 */
104
+	public $protectedProperties = [
105
+		// RFC4918
106
+		'{DAV:}getcontentlength',
107
+		'{DAV:}getetag',
108
+		'{DAV:}getlastmodified',
109
+		'{DAV:}lockdiscovery',
110
+		'{DAV:}supportedlock',
111
+
112
+		// RFC4331
113
+		'{DAV:}quota-available-bytes',
114
+		'{DAV:}quota-used-bytes',
115
+
116
+		// RFC3744
117
+		'{DAV:}supported-privilege-set',
118
+		'{DAV:}current-user-privilege-set',
119
+		'{DAV:}acl',
120
+		'{DAV:}acl-restrictions',
121
+		'{DAV:}inherited-acl-set',
122
+
123
+		// RFC3253
124
+		'{DAV:}supported-method-set',
125
+		'{DAV:}supported-report-set',
126
+
127
+		// RFC6578
128
+		'{DAV:}sync-token',
129
+
130
+		// calendarserver.org extensions
131
+		'{http://calendarserver.org/ns/}ctag',
132
+
133
+		// sabredav extensions
134
+		'{http://sabredav.org/ns}sync-token',
135
+	];
136
+
137
+	/**
138
+	 * This is a flag that allow or not showing file, line and code
139
+	 * of the exception in the returned XML.
140
+	 *
141
+	 * @var bool
142
+	 */
143
+	public $debugExceptions = false;
144
+
145
+	/**
146
+	 * This property allows you to automatically add the 'resourcetype' value
147
+	 * based on a node's classname or interface.
148
+	 *
149
+	 * The preset ensures that {DAV:}collection is automatically added for nodes
150
+	 * implementing Sabre\DAV\ICollection.
151
+	 *
152
+	 * @var array
153
+	 */
154
+	public $resourceTypeMapping = [
155
+		'Sabre\\DAV\\ICollection' => '{DAV:}collection',
156
+	];
157
+
158
+	/**
159
+	 * This property allows the usage of Depth: infinity on PROPFIND requests.
160
+	 *
161
+	 * By default Depth: infinity is treated as Depth: 1. Allowing Depth:
162
+	 * infinity is potentially risky, as it allows a single client to do a full
163
+	 * index of the webdav server, which is an easy DoS attack vector.
164
+	 *
165
+	 * Only turn this on if you know what you're doing.
166
+	 *
167
+	 * @var bool
168
+	 */
169
+	public $enablePropfindDepthInfinity = false;
170
+
171
+	/**
172
+	 * Reference to the XML utility object.
173
+	 *
174
+	 * @var Xml\Service
175
+	 */
176
+	public $xml;
177
+
178
+	/**
179
+	 * If this setting is turned off, SabreDAV's version number will be hidden
180
+	 * from various places.
181
+	 *
182
+	 * Some people feel this is a good security measure.
183
+	 *
184
+	 * @var bool
185
+	 */
186
+	public static $exposeVersion = true;
187
+
188
+	/**
189
+	 * If this setting is turned on, any multi status response on any PROPFIND will be streamed to the output buffer.
190
+	 * This will be beneficial for large result sets which will no longer consume a large amount of memory as well as
191
+	 * send back data to the client earlier.
192
+	 *
193
+	 * @var bool
194
+	 */
195
+	public static $streamMultiStatus = false;
196
+
197
+	/**
198
+	 * Sets up the server.
199
+	 *
200
+	 * If a Sabre\DAV\Tree object is passed as an argument, it will
201
+	 * use it as the directory tree. If a Sabre\DAV\INode is passed, it
202
+	 * will create a Sabre\DAV\Tree and use the node as the root.
203
+	 *
204
+	 * If nothing is passed, a Sabre\DAV\SimpleCollection is created in
205
+	 * a Sabre\DAV\Tree.
206
+	 *
207
+	 * If an array is passed, we automatically create a root node, and use
208
+	 * the nodes in the array as top-level children.
209
+	 *
210
+	 * @param Tree|INode|array|null $treeOrNode The tree object
211
+	 *
212
+	 * @throws Exception
213
+	 */
214
+	public function __construct($treeOrNode = null, HTTP\Sapi $sapi = null)
215
+	{
216
+		if ($treeOrNode instanceof Tree) {
217
+			$this->tree = $treeOrNode;
218
+		} elseif ($treeOrNode instanceof INode) {
219
+			$this->tree = new Tree($treeOrNode);
220
+		} elseif (is_array($treeOrNode)) {
221
+			$root = new SimpleCollection('root', $treeOrNode);
222
+			$this->tree = new Tree($root);
223
+		} elseif (is_null($treeOrNode)) {
224
+			$root = new SimpleCollection('root');
225
+			$this->tree = new Tree($root);
226
+		} else {
227
+			throw new Exception('Invalid argument passed to constructor. Argument must either be an instance of Sabre\\DAV\\Tree, Sabre\\DAV\\INode, an array or null');
228
+		}
229
+
230
+		$this->xml = new Xml\Service();
231
+		$this->sapi = $sapi ?? new HTTP\Sapi();
232
+		$this->httpResponse = new HTTP\Response();
233
+		$this->httpRequest = $this->sapi->getRequest();
234
+		$this->addPlugin(new CorePlugin());
235
+	}
236
+
237
+	/**
238
+	 * Starts the DAV Server.
239
+	 */
240
+	public function start()
241
+	{
242
+		try {
243
+			// If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an
244
+			// origin, we must make sure we send back HTTP/1.0 if this was
245
+			// requested.
246
+			// This is mainly because nginx doesn't support Chunked Transfer
247
+			// Encoding, and this forces the webserver SabreDAV is running on,
248
+			// to buffer entire responses to calculate Content-Length.
249
+			$this->httpResponse->setHTTPVersion($this->httpRequest->getHTTPVersion());
250
+
251
+			// Setting the base url
252
+			$this->httpRequest->setBaseUrl($this->getBaseUri());
253
+			$this->invokeMethod($this->httpRequest, $this->httpResponse);
254
+		} catch (\Throwable $e) {
255
+			try {
256
+				$this->emit('exception', [$e]);
257
+			} catch (\Exception $ignore) {
258
+			}
259
+			$DOM = new \DOMDocument('1.0', 'utf-8');
260
+			$DOM->formatOutput = true;
261
+
262
+			$error = $DOM->createElementNS('DAV:', 'd:error');
263
+			$error->setAttribute('xmlns:s', self::NS_SABREDAV);
264
+			$DOM->appendChild($error);
265
+
266
+			$h = function ($v) {
267
+				return htmlspecialchars((string) $v, ENT_NOQUOTES, 'UTF-8');
268
+			};
269
+
270
+			if (self::$exposeVersion) {
271
+				$error->appendChild($DOM->createElement('s:sabredav-version', $h(Version::VERSION)));
272
+			}
273
+
274
+			$error->appendChild($DOM->createElement('s:exception', $h(get_class($e))));
275
+			$error->appendChild($DOM->createElement('s:message', $h($e->getMessage())));
276
+			if ($this->debugExceptions) {
277
+				$error->appendChild($DOM->createElement('s:file', $h($e->getFile())));
278
+				$error->appendChild($DOM->createElement('s:line', $h($e->getLine())));
279
+				$error->appendChild($DOM->createElement('s:code', $h($e->getCode())));
280
+				$error->appendChild($DOM->createElement('s:stacktrace', $h($e->getTraceAsString())));
281
+			}
282
+
283
+			if ($this->debugExceptions) {
284
+				$previous = $e;
285
+				while ($previous = $previous->getPrevious()) {
286
+					$xPrevious = $DOM->createElement('s:previous-exception');
287
+					$xPrevious->appendChild($DOM->createElement('s:exception', $h(get_class($previous))));
288
+					$xPrevious->appendChild($DOM->createElement('s:message', $h($previous->getMessage())));
289
+					$xPrevious->appendChild($DOM->createElement('s:file', $h($previous->getFile())));
290
+					$xPrevious->appendChild($DOM->createElement('s:line', $h($previous->getLine())));
291
+					$xPrevious->appendChild($DOM->createElement('s:code', $h($previous->getCode())));
292
+					$xPrevious->appendChild($DOM->createElement('s:stacktrace', $h($previous->getTraceAsString())));
293
+					$error->appendChild($xPrevious);
294
+				}
295
+			}
296
+
297
+			if ($e instanceof Exception) {
298
+				$httpCode = $e->getHTTPCode();
299
+				$e->serialize($this, $error);
300
+				$headers = $e->getHTTPHeaders($this);
301
+			} else {
302
+				$httpCode = 500;
303
+				$headers = [];
304
+			}
305
+			$headers['Content-Type'] = 'application/xml; charset=utf-8';
306
+
307
+			$this->httpResponse->setStatus($httpCode);
308
+			$this->httpResponse->setHeaders($headers);
309
+			$this->httpResponse->setBody($DOM->saveXML());
310
+			$this->sapi->sendResponse($this->httpResponse);
311
+		}
312
+	}
313
+
314
+	/**
315
+	 * Alias of start().
316
+	 *
317
+	 * @deprecated
318
+	 */
319
+	public function exec()
320
+	{
321
+		$this->start();
322
+	}
323
+
324
+	/**
325
+	 * Sets the base server uri.
326
+	 *
327
+	 * @param string $uri
328
+	 */
329
+	public function setBaseUri($uri)
330
+	{
331
+		// If the baseUri does not end with a slash, we must add it
332
+		if ('/' !== $uri[strlen($uri) - 1]) {
333
+			$uri .= '/';
334
+		}
335
+
336
+		$this->baseUri = $uri;
337
+	}
338
+
339
+	/**
340
+	 * Returns the base responding uri.
341
+	 *
342
+	 * @return string
343
+	 */
344
+	public function getBaseUri()
345
+	{
346
+		if (is_null($this->baseUri)) {
347
+			$this->baseUri = $this->guessBaseUri();
348
+		}
349
+
350
+		return $this->baseUri;
351
+	}
352
+
353
+	/**
354
+	 * This method attempts to detect the base uri.
355
+	 * Only the PATH_INFO variable is considered.
356
+	 *
357
+	 * If this variable is not set, the root (/) is assumed.
358
+	 *
359
+	 * @return string
360
+	 */
361
+	public function guessBaseUri()
362
+	{
363
+		$pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO');
364
+		$uri = $this->httpRequest->getRawServerValue('REQUEST_URI');
365
+
366
+		// If PATH_INFO is found, we can assume it's accurate.
367
+		if (!empty($pathInfo)) {
368
+			// We need to make sure we ignore the QUERY_STRING part
369
+			if ($pos = strpos($uri, '?')) {
370
+				$uri = substr($uri, 0, $pos);
371
+			}
372
+
373
+			// PATH_INFO is only set for urls, such as: /example.php/path
374
+			// in that case PATH_INFO contains '/path'.
375
+			// Note that REQUEST_URI is percent encoded, while PATH_INFO is
376
+			// not, Therefore they are only comparable if we first decode
377
+			// REQUEST_INFO as well.
378
+			$decodedUri = HTTP\decodePath($uri);
379
+
380
+			// A simple sanity check:
381
+			if (substr($decodedUri, strlen($decodedUri) - strlen($pathInfo)) === $pathInfo) {
382
+				$baseUri = substr($decodedUri, 0, strlen($decodedUri) - strlen($pathInfo));
383
+
384
+				return rtrim($baseUri, '/').'/';
385
+			}
386
+
387
+			throw new Exception('The REQUEST_URI ('.$uri.') did not end with the contents of PATH_INFO ('.$pathInfo.'). This server might be misconfigured.');
388
+		}
389
+
390
+		// The last fallback is that we're just going to assume the server root.
391
+		return '/';
392
+	}
393
+
394
+	/**
395
+	 * Adds a plugin to the server.
396
+	 *
397
+	 * For more information, console the documentation of Sabre\DAV\ServerPlugin
398
+	 */
399
+	public function addPlugin(ServerPlugin $plugin)
400
+	{
401
+		$this->plugins[$plugin->getPluginName()] = $plugin;
402
+		$plugin->initialize($this);
403
+	}
404
+
405
+	/**
406
+	 * Returns an initialized plugin by it's name.
407
+	 *
408
+	 * This function returns null if the plugin was not found.
409
+	 *
410
+	 * @param string $name
411
+	 *
412
+	 * @return ServerPlugin
413
+	 */
414
+	public function getPlugin($name)
415
+	{
416
+		if (isset($this->plugins[$name])) {
417
+			return $this->plugins[$name];
418
+		}
419
+
420
+		return null;
421
+	}
422
+
423
+	/**
424
+	 * Returns all plugins.
425
+	 *
426
+	 * @return array
427
+	 */
428
+	public function getPlugins()
429
+	{
430
+		return $this->plugins;
431
+	}
432
+
433
+	/**
434
+	 * Returns the PSR-3 logger object.
435
+	 *
436
+	 * @return LoggerInterface
437
+	 */
438
+	public function getLogger()
439
+	{
440
+		if (!$this->logger) {
441
+			$this->logger = new NullLogger();
442
+		}
443
+
444
+		return $this->logger;
445
+	}
446
+
447
+	/**
448
+	 * Handles a http request, and execute a method based on its name.
449
+	 *
450
+	 * @param bool $sendResponse whether to send the HTTP response to the DAV client
451
+	 */
452
+	public function invokeMethod(RequestInterface $request, ResponseInterface $response, $sendResponse = true)
453
+	{
454
+		$method = $request->getMethod();
455
+
456
+		if (!$this->emit('beforeMethod:'.$method, [$request, $response])) {
457
+			return;
458
+		}
459
+
460
+		if (self::$exposeVersion) {
461
+			$response->setHeader('X-Sabre-Version', Version::VERSION);
462
+		}
463
+
464
+		$this->transactionType = strtolower($method);
465
+
466
+		if (!$this->checkPreconditions($request, $response)) {
467
+			$this->sapi->sendResponse($response);
468
+
469
+			return;
470
+		}
471
+
472
+		if ($this->emit('method:'.$method, [$request, $response])) {
473
+			$exMessage = 'There was no plugin in the system that was willing to handle this '.$method.' method.';
474
+			if ('GET' === $method) {
475
+				$exMessage .= ' Enable the Browser plugin to get a better result here.';
476
+			}
477
+
478
+			// Unsupported method
479
+			throw new Exception\NotImplemented($exMessage);
480
+		}
481
+
482
+		if (!$this->emit('afterMethod:'.$method, [$request, $response])) {
483
+			return;
484
+		}
485
+
486
+		if (null === $response->getStatus()) {
487
+			throw new Exception('No subsystem set a valid HTTP status code. Something must have interrupted the request without providing further detail.');
488
+		}
489
+		if ($sendResponse) {
490
+			$this->sapi->sendResponse($response);
491
+			$this->emit('afterResponse', [$request, $response]);
492
+		}
493
+	}
494
+
495
+	// {{{ HTTP/WebDAV protocol helpers
496
+
497
+	/**
498
+	 * Returns an array with all the supported HTTP methods for a specific uri.
499
+	 *
500
+	 * @param string $path
501
+	 *
502
+	 * @return array
503
+	 */
504
+	public function getAllowedMethods($path)
505
+	{
506
+		$methods = [
507
+			'OPTIONS',
508
+			'GET',
509
+			'HEAD',
510
+			'DELETE',
511
+			'PROPFIND',
512
+			'PUT',
513
+			'PROPPATCH',
514
+			'COPY',
515
+			'MOVE',
516
+			'REPORT',
517
+		];
518
+
519
+		// The MKCOL is only allowed on an unmapped uri
520
+		try {
521
+			$this->tree->getNodeForPath($path);
522
+		} catch (Exception\NotFound $e) {
523
+			$methods[] = 'MKCOL';
524
+		}
525
+
526
+		// We're also checking if any of the plugins register any new methods
527
+		foreach ($this->plugins as $plugin) {
528
+			$methods = array_merge($methods, $plugin->getHTTPMethods($path));
529
+		}
530
+		array_unique($methods);
531
+
532
+		return $methods;
533
+	}
534
+
535
+	/**
536
+	 * Gets the uri for the request, keeping the base uri into consideration.
537
+	 *
538
+	 * @return string
539
+	 */
540
+	public function getRequestUri()
541
+	{
542
+		return $this->calculateUri($this->httpRequest->getUrl());
543
+	}
544
+
545
+	/**
546
+	 * Turns a URI such as the REQUEST_URI into a local path.
547
+	 *
548
+	 * This method:
549
+	 *   * strips off the base path
550
+	 *   * normalizes the path
551
+	 *   * uri-decodes the path
552
+	 *
553
+	 * @param string $uri
554
+	 *
555
+	 * @throws Exception\Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri
556
+	 *
557
+	 * @return string
558
+	 */
559
+	public function calculateUri($uri)
560
+	{
561
+		if ('' != $uri && '/' != $uri[0] && strpos($uri, '://')) {
562
+			$uri = parse_url($uri, PHP_URL_PATH);
563
+		}
564
+
565
+		$uri = Uri\normalize(preg_replace('|/+|', '/', $uri));
566
+		$baseUri = Uri\normalize($this->getBaseUri());
567
+
568
+		if (0 === strpos($uri, $baseUri)) {
569
+			return trim(HTTP\decodePath(substr($uri, strlen($baseUri))), '/');
570
+
571
+		// A special case, if the baseUri was accessed without a trailing
572
+		// slash, we'll accept it as well.
573
+		} elseif ($uri.'/' === $baseUri) {
574
+			return '';
575
+		} else {
576
+			throw new Exception\Forbidden('Requested uri ('.$uri.') is out of base uri ('.$this->getBaseUri().')');
577
+		}
578
+	}
579
+
580
+	/**
581
+	 * Returns the HTTP depth header.
582
+	 *
583
+	 * This method returns the contents of the HTTP depth request header. If the depth header was 'infinity' it will return the Sabre\DAV\Server::DEPTH_INFINITY object
584
+	 * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent
585
+	 *
586
+	 * @param mixed $default
587
+	 *
588
+	 * @return int
589
+	 */
590
+	public function getHTTPDepth($default = self::DEPTH_INFINITY)
591
+	{
592
+		// If its not set, we'll grab the default
593
+		$depth = $this->httpRequest->getHeader('Depth');
594
+
595
+		if (is_null($depth)) {
596
+			return $default;
597
+		}
598
+
599
+		if ('infinity' == $depth) {
600
+			return self::DEPTH_INFINITY;
601
+		}
602
+
603
+		// If its an unknown value. we'll grab the default
604
+		if (!ctype_digit($depth)) {
605
+			return $default;
606
+		}
607
+
608
+		return (int) $depth;
609
+	}
610
+
611
+	/**
612
+	 * Returns the HTTP range header.
613
+	 *
614
+	 * This method returns null if there is no well-formed HTTP range request
615
+	 * header or array($start, $end).
616
+	 *
617
+	 * The first number is the offset of the first byte in the range.
618
+	 * The second number is the offset of the last byte in the range.
619
+	 *
620
+	 * If the second offset is null, it should be treated as the offset of the last byte of the entity
621
+	 * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity
622
+	 *
623
+	 * @return int[]|null
624
+	 */
625
+	public function getHTTPRange()
626
+	{
627
+		$range = $this->httpRequest->getHeader('range');
628
+		if (is_null($range)) {
629
+			return null;
630
+		}
631
+
632
+		// Matching "Range: bytes=1234-5678: both numbers are optional
633
+
634
+		if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i', $range, $matches)) {
635
+			return null;
636
+		}
637
+
638
+		if ('' === $matches[1] && '' === $matches[2]) {
639
+			return null;
640
+		}
641
+
642
+		return [
643
+			'' !== $matches[1] ? (int) $matches[1] : null,
644
+			'' !== $matches[2] ? (int) $matches[2] : null,
645
+		];
646
+	}
647
+
648
+	/**
649
+	 * Returns the HTTP Prefer header information.
650
+	 *
651
+	 * The prefer header is defined in:
652
+	 * http://tools.ietf.org/html/draft-snell-http-prefer-14
653
+	 *
654
+	 * This method will return an array with options.
655
+	 *
656
+	 * Currently, the following options may be returned:
657
+	 *  [
658
+	 *      'return-asynch'         => true,
659
+	 *      'return-minimal'        => true,
660
+	 *      'return-representation' => true,
661
+	 *      'wait'                  => 30,
662
+	 *      'strict'                => true,
663
+	 *      'lenient'               => true,
664
+	 *  ]
665
+	 *
666
+	 * This method also supports the Brief header, and will also return
667
+	 * 'return-minimal' if the brief header was set to 't'.
668
+	 *
669
+	 * For the boolean options, false will be returned if the headers are not
670
+	 * specified. For the integer options it will be 'null'.
671
+	 *
672
+	 * @return array
673
+	 */
674
+	public function getHTTPPrefer()
675
+	{
676
+		$result = [
677
+			// can be true or false
678
+			'respond-async' => false,
679
+			// Could be set to 'representation' or 'minimal'.
680
+			'return' => null,
681
+			// Used as a timeout, is usually a number.
682
+			'wait' => null,
683
+			// can be 'strict' or 'lenient'.
684
+			'handling' => false,
685
+		];
686
+
687
+		if ($prefer = $this->httpRequest->getHeader('Prefer')) {
688
+			$result = array_merge(
689
+				$result,
690
+				HTTP\parsePrefer($prefer)
691
+			);
692
+		} elseif ('t' == $this->httpRequest->getHeader('Brief')) {
693
+			$result['return'] = 'minimal';
694
+		}
695
+
696
+		return $result;
697
+	}
698
+
699
+	/**
700
+	 * Returns information about Copy and Move requests.
701
+	 *
702
+	 * This function is created to help getting information about the source and the destination for the
703
+	 * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions
704
+	 *
705
+	 * The returned value is an array with the following keys:
706
+	 *   * destination - Destination path
707
+	 *   * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten)
708
+	 *
709
+	 * @throws Exception\BadRequest           upon missing or broken request headers
710
+	 * @throws Exception\UnsupportedMediaType when trying to copy into a
711
+	 *                                        non-collection
712
+	 * @throws Exception\PreconditionFailed   if overwrite is set to false, but
713
+	 *                                        the destination exists
714
+	 * @throws Exception\Forbidden            when source and destination paths are
715
+	 *                                        identical
716
+	 * @throws Exception\Conflict             when trying to copy a node into its own
717
+	 *                                        subtree
718
+	 *
719
+	 * @return array
720
+	 */
721
+	public function getCopyAndMoveInfo(RequestInterface $request)
722
+	{
723
+		// Collecting the relevant HTTP headers
724
+		if (!$request->getHeader('Destination')) {
725
+			throw new Exception\BadRequest('The destination header was not supplied');
726
+		}
727
+		$destination = $this->calculateUri($request->getHeader('Destination'));
728
+		$overwrite = $request->getHeader('Overwrite');
729
+		if (!$overwrite) {
730
+			$overwrite = 'T';
731
+		}
732
+		if ('T' == strtoupper($overwrite)) {
733
+			$overwrite = true;
734
+		} elseif ('F' == strtoupper($overwrite)) {
735
+			$overwrite = false;
736
+		}
737
+		// We need to throw a bad request exception, if the header was invalid
738
+		else {
739
+			throw new Exception\BadRequest('The HTTP Overwrite header should be either T or F');
740
+		}
741
+		list($destinationDir) = Uri\split($destination);
742
+
743
+		try {
744
+			$destinationParent = $this->tree->getNodeForPath($destinationDir);
745
+			if (!($destinationParent instanceof ICollection)) {
746
+				throw new Exception\UnsupportedMediaType('The destination node is not a collection');
747
+			}
748
+		} catch (Exception\NotFound $e) {
749
+			// If the destination parent node is not found, we throw a 409
750
+			throw new Exception\Conflict('The destination node is not found');
751
+		}
752
+
753
+		try {
754
+			$destinationNode = $this->tree->getNodeForPath($destination);
755
+
756
+			// If this succeeded, it means the destination already exists
757
+			// we'll need to throw precondition failed in case overwrite is false
758
+			if (!$overwrite) {
759
+				throw new Exception\PreconditionFailed('The destination node already exists, and the overwrite header is set to false', 'Overwrite');
760
+			}
761
+		} catch (Exception\NotFound $e) {
762
+			// Destination didn't exist, we're all good
763
+			$destinationNode = false;
764
+		}
765
+
766
+		$requestPath = $request->getPath();
767
+		if ($destination === $requestPath) {
768
+			throw new Exception\Forbidden('Source and destination uri are identical.');
769
+		}
770
+		if (substr($destination, 0, strlen($requestPath) + 1) === $requestPath.'/') {
771
+			throw new Exception\Conflict('The destination may not be part of the same subtree as the source path.');
772
+		}
773
+
774
+		// These are the three relevant properties we need to return
775
+		return [
776
+			'destination' => $destination,
777
+			'destinationExists' => (bool) $destinationNode,
778
+			'destinationNode' => $destinationNode,
779
+		];
780
+	}
781
+
782
+	/**
783
+	 * Returns a list of properties for a path.
784
+	 *
785
+	 * This is a simplified version getPropertiesForPath. If you aren't
786
+	 * interested in status codes, but you just want to have a flat list of
787
+	 * properties, use this method.
788
+	 *
789
+	 * Please note though that any problems related to retrieving properties,
790
+	 * such as permission issues will just result in an empty array being
791
+	 * returned.
792
+	 *
793
+	 * @param string $path
794
+	 * @param array  $propertyNames
795
+	 *
796
+	 * @return array
797
+	 */
798
+	public function getProperties($path, $propertyNames)
799
+	{
800
+		$result = $this->getPropertiesForPath($path, $propertyNames, 0);
801
+		if (isset($result[0][200])) {
802
+			return $result[0][200];
803
+		} else {
804
+			return [];
805
+		}
806
+	}
807
+
808
+	/**
809
+	 * A kid-friendly way to fetch properties for a node's children.
810
+	 *
811
+	 * The returned array will be indexed by the path of the of child node.
812
+	 * Only properties that are actually found will be returned.
813
+	 *
814
+	 * The parent node will not be returned.
815
+	 *
816
+	 * @param string $path
817
+	 * @param array  $propertyNames
818
+	 *
819
+	 * @return array
820
+	 */
821
+	public function getPropertiesForChildren($path, $propertyNames)
822
+	{
823
+		$result = [];
824
+		foreach ($this->getPropertiesForPath($path, $propertyNames, 1) as $k => $row) {
825
+			// Skipping the parent path
826
+			if (0 === $k) {
827
+				continue;
828
+			}
829
+
830
+			$result[$row['href']] = $row[200];
831
+		}
832
+
833
+		return $result;
834
+	}
835
+
836
+	/**
837
+	 * Returns a list of HTTP headers for a particular resource.
838
+	 *
839
+	 * The generated http headers are based on properties provided by the
840
+	 * resource. The method basically provides a simple mapping between
841
+	 * DAV property and HTTP header.
842
+	 *
843
+	 * The headers are intended to be used for HEAD and GET requests.
844
+	 *
845
+	 * @param string $path
846
+	 *
847
+	 * @return array
848
+	 */
849
+	public function getHTTPHeaders($path)
850
+	{
851
+		$propertyMap = [
852
+			'{DAV:}getcontenttype' => 'Content-Type',
853
+			'{DAV:}getcontentlength' => 'Content-Length',
854
+			'{DAV:}getlastmodified' => 'Last-Modified',
855
+			'{DAV:}getetag' => 'ETag',
856
+		];
857
+
858
+		$properties = $this->getProperties($path, array_keys($propertyMap));
859
+
860
+		$headers = [];
861
+		foreach ($propertyMap as $property => $header) {
862
+			if (!isset($properties[$property])) {
863
+				continue;
864
+			}
865
+
866
+			if (is_scalar($properties[$property])) {
867
+				$headers[$header] = $properties[$property];
868
+
869
+			// GetLastModified gets special cased
870
+			} elseif ($properties[$property] instanceof Xml\Property\GetLastModified) {
871
+				$headers[$header] = HTTP\toDate($properties[$property]->getTime());
872
+			}
873
+		}
874
+
875
+		return $headers;
876
+	}
877
+
878
+	/**
879
+	 * Small helper to support PROPFIND with DEPTH_INFINITY.
880
+	 *
881
+	 * @param array $yieldFirst
882
+	 *
883
+	 * @return \Traversable
884
+	 */
885
+	private function generatePathNodes(PropFind $propFind, array $yieldFirst = null)
886
+	{
887
+		if (null !== $yieldFirst) {
888
+			yield $yieldFirst;
889
+		}
890
+		$newDepth = $propFind->getDepth();
891
+		$path = $propFind->getPath();
892
+
893
+		if (self::DEPTH_INFINITY !== $newDepth) {
894
+			--$newDepth;
895
+		}
896
+
897
+		$propertyNames = $propFind->getRequestedProperties();
898
+		$propFindType = !$propFind->isAllProps() ? PropFind::NORMAL : PropFind::ALLPROPS;
899
+
900
+		foreach ($this->tree->getChildren($path) as $childNode) {
901
+			if ('' !== $path) {
902
+				$subPath = $path.'/'.$childNode->getName();
903
+			} else {
904
+				$subPath = $childNode->getName();
905
+			}
906
+			$subPropFind = new PropFind($subPath, $propertyNames, $newDepth, $propFindType);
907
+
908
+			yield [
909
+				$subPropFind,
910
+				$childNode,
911
+			];
912
+
913
+			if ((self::DEPTH_INFINITY === $newDepth || $newDepth >= 1) && $childNode instanceof ICollection) {
914
+				foreach ($this->generatePathNodes($subPropFind) as $subItem) {
915
+					yield $subItem;
916
+				}
917
+			}
918
+		}
919
+	}
920
+
921
+	/**
922
+	 * Returns a list of properties for a given path.
923
+	 *
924
+	 * The path that should be supplied should have the baseUrl stripped out
925
+	 * The list of properties should be supplied in Clark notation. If the list is empty
926
+	 * 'allprops' is assumed.
927
+	 *
928
+	 * If a depth of 1 is requested child elements will also be returned.
929
+	 *
930
+	 * @param string $path
931
+	 * @param array  $propertyNames
932
+	 * @param int    $depth
933
+	 *
934
+	 * @return array
935
+	 *
936
+	 * @deprecated Use getPropertiesIteratorForPath() instead (as it's more memory efficient)
937
+	 * @see getPropertiesIteratorForPath()
938
+	 */
939
+	public function getPropertiesForPath($path, $propertyNames = [], $depth = 0)
940
+	{
941
+		return iterator_to_array($this->getPropertiesIteratorForPath($path, $propertyNames, $depth));
942
+	}
943
+
944
+	/**
945
+	 * Returns a list of properties for a given path.
946
+	 *
947
+	 * The path that should be supplied should have the baseUrl stripped out
948
+	 * The list of properties should be supplied in Clark notation. If the list is empty
949
+	 * 'allprops' is assumed.
950
+	 *
951
+	 * If a depth of 1 is requested child elements will also be returned.
952
+	 *
953
+	 * @param string $path
954
+	 * @param array  $propertyNames
955
+	 * @param int    $depth
956
+	 *
957
+	 * @return \Iterator
958
+	 */
959
+	public function getPropertiesIteratorForPath($path, $propertyNames = [], $depth = 0)
960
+	{
961
+		// The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled
962
+		if (!$this->enablePropfindDepthInfinity && 0 != $depth) {
963
+			$depth = 1;
964
+		}
965
+
966
+		$path = trim($path, '/');
967
+
968
+		$propFindType = $propertyNames ? PropFind::NORMAL : PropFind::ALLPROPS;
969
+		$propFind = new PropFind($path, (array) $propertyNames, $depth, $propFindType);
970
+
971
+		$parentNode = $this->tree->getNodeForPath($path);
972
+
973
+		$propFindRequests = [[
974
+			$propFind,
975
+			$parentNode,
976
+		]];
977
+
978
+		if (($depth > 0 || self::DEPTH_INFINITY === $depth) && $parentNode instanceof ICollection) {
979
+			$propFindRequests = $this->generatePathNodes(clone $propFind, current($propFindRequests));
980
+		}
981
+
982
+		foreach ($propFindRequests as $propFindRequest) {
983
+			list($propFind, $node) = $propFindRequest;
984
+			$r = $this->getPropertiesByNode($propFind, $node);
985
+			if ($r) {
986
+				$result = $propFind->getResultForMultiStatus();
987
+				$result['href'] = $propFind->getPath();
988
+
989
+				// WebDAV recommends adding a slash to the path, if the path is
990
+				// a collection.
991
+				// Furthermore, iCal also demands this to be the case for
992
+				// principals. This is non-standard, but we support it.
993
+				$resourceType = $this->getResourceTypeForNode($node);
994
+				if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) {
995
+					$result['href'] .= '/';
996
+				}
997
+				yield $result;
998
+			}
999
+		}
1000
+	}
1001
+
1002
+	/**
1003
+	 * Returns a list of properties for a list of paths.
1004
+	 *
1005
+	 * The path that should be supplied should have the baseUrl stripped out
1006
+	 * The list of properties should be supplied in Clark notation. If the list is empty
1007
+	 * 'allprops' is assumed.
1008
+	 *
1009
+	 * The result is returned as an array, with paths for it's keys.
1010
+	 * The result may be returned out of order.
1011
+	 *
1012
+	 * @return array
1013
+	 */
1014
+	public function getPropertiesForMultiplePaths(array $paths, array $propertyNames = [])
1015
+	{
1016
+		$result = [
1017
+		];
1018
+
1019
+		$nodes = $this->tree->getMultipleNodes($paths);
1020
+
1021
+		foreach ($nodes as $path => $node) {
1022
+			$propFind = new PropFind($path, $propertyNames);
1023
+			$r = $this->getPropertiesByNode($propFind, $node);
1024
+			if ($r) {
1025
+				$result[$path] = $propFind->getResultForMultiStatus();
1026
+				$result[$path]['href'] = $path;
1027
+
1028
+				$resourceType = $this->getResourceTypeForNode($node);
1029
+				if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) {
1030
+					$result[$path]['href'] .= '/';
1031
+				}
1032
+			}
1033
+		}
1034
+
1035
+		return $result;
1036
+	}
1037
+
1038
+	/**
1039
+	 * Determines all properties for a node.
1040
+	 *
1041
+	 * This method tries to grab all properties for a node. This method is used
1042
+	 * internally getPropertiesForPath and a few others.
1043
+	 *
1044
+	 * It could be useful to call this, if you already have an instance of your
1045
+	 * target node and simply want to run through the system to get a correct
1046
+	 * list of properties.
1047
+	 *
1048
+	 * @return bool
1049
+	 */
1050
+	public function getPropertiesByNode(PropFind $propFind, INode $node)
1051
+	{
1052
+		return $this->emit('propFind', [$propFind, $node]);
1053
+	}
1054
+
1055
+	/**
1056
+	 * This method is invoked by sub-systems creating a new file.
1057
+	 *
1058
+	 * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin).
1059
+	 * It was important to get this done through a centralized function,
1060
+	 * allowing plugins to intercept this using the beforeCreateFile event.
1061
+	 *
1062
+	 * This method will return true if the file was actually created
1063
+	 *
1064
+	 * @param string   $uri
1065
+	 * @param resource $data
1066
+	 * @param string   $etag
1067
+	 *
1068
+	 * @return bool
1069
+	 */
1070
+	public function createFile($uri, $data, &$etag = null)
1071
+	{
1072
+		list($dir, $name) = Uri\split($uri);
1073
+
1074
+		if (!$this->emit('beforeBind', [$uri])) {
1075
+			return false;
1076
+		}
1077
+
1078
+		try {
1079
+			$parent = $this->tree->getNodeForPath($dir);
1080
+		} catch (Exception\NotFound $e) {
1081
+			throw new Exception\Conflict('Files cannot be created in non-existent collections');
1082
+		}
1083
+
1084
+		if (!$parent instanceof ICollection) {
1085
+			throw new Exception\Conflict('Files can only be created as children of collections');
1086
+		}
1087
+
1088
+		// It is possible for an event handler to modify the content of the
1089
+		// body, before it gets written. If this is the case, $modified
1090
+		// should be set to true.
1091
+		//
1092
+		// If $modified is true, we must not send back an ETag.
1093
+		$modified = false;
1094
+		if (!$this->emit('beforeCreateFile', [$uri, &$data, $parent, &$modified])) {
1095
+			return false;
1096
+		}
1097
+
1098
+		$etag = $parent->createFile($name, $data);
1099
+
1100
+		if ($modified) {
1101
+			$etag = null;
1102
+		}
1103
+
1104
+		$this->tree->markDirty($dir.'/'.$name);
1105
+
1106
+		$this->emit('afterBind', [$uri]);
1107
+		$this->emit('afterCreateFile', [$uri, $parent]);
1108
+
1109
+		return true;
1110
+	}
1111
+
1112
+	/**
1113
+	 * This method is invoked by sub-systems updating a file.
1114
+	 *
1115
+	 * This method will return true if the file was actually updated
1116
+	 *
1117
+	 * @param string   $uri
1118
+	 * @param resource $data
1119
+	 * @param string   $etag
1120
+	 *
1121
+	 * @return bool
1122
+	 */
1123
+	public function updateFile($uri, $data, &$etag = null)
1124
+	{
1125
+		$node = $this->tree->getNodeForPath($uri);
1126
+
1127
+		// It is possible for an event handler to modify the content of the
1128
+		// body, before it gets written. If this is the case, $modified
1129
+		// should be set to true.
1130
+		//
1131
+		// If $modified is true, we must not send back an ETag.
1132
+		$modified = false;
1133
+		if (!$this->emit('beforeWriteContent', [$uri, $node, &$data, &$modified])) {
1134
+			return false;
1135
+		}
1136
+
1137
+		$etag = $node->put($data);
1138
+		if ($modified) {
1139
+			$etag = null;
1140
+		}
1141
+		$this->emit('afterWriteContent', [$uri, $node]);
1142
+
1143
+		return true;
1144
+	}
1145
+
1146
+	/**
1147
+	 * This method is invoked by sub-systems creating a new directory.
1148
+	 *
1149
+	 * @param string $uri
1150
+	 */
1151
+	public function createDirectory($uri)
1152
+	{
1153
+		$this->createCollection($uri, new MkCol(['{DAV:}collection'], []));
1154
+	}
1155
+
1156
+	/**
1157
+	 * Use this method to create a new collection.
1158
+	 *
1159
+	 * @param string $uri The new uri
1160
+	 *
1161
+	 * @return array|null
1162
+	 */
1163
+	public function createCollection($uri, MkCol $mkCol)
1164
+	{
1165
+		list($parentUri, $newName) = Uri\split($uri);
1166
+
1167
+		// Making sure the parent exists
1168
+		try {
1169
+			$parent = $this->tree->getNodeForPath($parentUri);
1170
+		} catch (Exception\NotFound $e) {
1171
+			throw new Exception\Conflict('Parent node does not exist');
1172
+		}
1173
+
1174
+		// Making sure the parent is a collection
1175
+		if (!$parent instanceof ICollection) {
1176
+			throw new Exception\Conflict('Parent node is not a collection');
1177
+		}
1178
+
1179
+		// Making sure the child does not already exist
1180
+		try {
1181
+			$parent->getChild($newName);
1182
+
1183
+			// If we got here.. it means there's already a node on that url, and we need to throw a 405
1184
+			throw new Exception\MethodNotAllowed('The resource you tried to create already exists');
1185
+		} catch (Exception\NotFound $e) {
1186
+			// NotFound is the expected behavior.
1187
+		}
1188
+
1189
+		if (!$this->emit('beforeBind', [$uri])) {
1190
+			return;
1191
+		}
1192
+
1193
+		if ($parent instanceof IExtendedCollection) {
1194
+			/*
1195 1195
              * If the parent is an instance of IExtendedCollection, it means that
1196 1196
              * we can pass the MkCol object directly as it may be able to store
1197 1197
              * properties immediately.
1198 1198
              */
1199
-            $parent->createExtendedCollection($newName, $mkCol);
1200
-        } else {
1201
-            /*
1199
+			$parent->createExtendedCollection($newName, $mkCol);
1200
+		} else {
1201
+			/*
1202 1202
              * If the parent is a standard ICollection, it means only
1203 1203
              * 'standard' collections can be created, so we should fail any
1204 1204
              * MKCOL operation that carries extra resourcetypes.
1205 1205
              */
1206
-            if (count($mkCol->getResourceType()) > 1) {
1207
-                throw new Exception\InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.');
1208
-            }
1209
-
1210
-            $parent->createDirectory($newName);
1211
-        }
1212
-
1213
-        // If there are any properties that have not been handled/stored,
1214
-        // we ask the 'propPatch' event to handle them. This will allow for
1215
-        // example the propertyStorage system to store properties upon MKCOL.
1216
-        if ($mkCol->getRemainingMutations()) {
1217
-            $this->emit('propPatch', [$uri, $mkCol]);
1218
-        }
1219
-        $success = $mkCol->commit();
1220
-
1221
-        if (!$success) {
1222
-            $result = $mkCol->getResult();
1223
-
1224
-            $formattedResult = [
1225
-                'href' => $uri,
1226
-            ];
1227
-
1228
-            foreach ($result as $propertyName => $status) {
1229
-                if (!isset($formattedResult[$status])) {
1230
-                    $formattedResult[$status] = [];
1231
-                }
1232
-                $formattedResult[$status][$propertyName] = null;
1233
-            }
1234
-
1235
-            return $formattedResult;
1236
-        }
1237
-
1238
-        $this->tree->markDirty($parentUri);
1239
-        $this->emit('afterBind', [$uri]);
1240
-        $this->emit('afterCreateCollection', [$uri]);
1241
-    }
1242
-
1243
-    /**
1244
-     * This method updates a resource's properties.
1245
-     *
1246
-     * The properties array must be a list of properties. Array-keys are
1247
-     * property names in clarknotation, array-values are it's values.
1248
-     * If a property must be deleted, the value should be null.
1249
-     *
1250
-     * Note that this request should either completely succeed, or
1251
-     * completely fail.
1252
-     *
1253
-     * The response is an array with properties for keys, and http status codes
1254
-     * as their values.
1255
-     *
1256
-     * @param string $path
1257
-     *
1258
-     * @return array
1259
-     */
1260
-    public function updateProperties($path, array $properties)
1261
-    {
1262
-        $propPatch = new PropPatch($properties);
1263
-        $this->emit('propPatch', [$path, $propPatch]);
1264
-        $propPatch->commit();
1265
-
1266
-        return $propPatch->getResult();
1267
-    }
1268
-
1269
-    /**
1270
-     * This method checks the main HTTP preconditions.
1271
-     *
1272
-     * Currently these are:
1273
-     *   * If-Match
1274
-     *   * If-None-Match
1275
-     *   * If-Modified-Since
1276
-     *   * If-Unmodified-Since
1277
-     *
1278
-     * The method will return true if all preconditions are met
1279
-     * The method will return false, or throw an exception if preconditions
1280
-     * failed. If false is returned the operation should be aborted, and
1281
-     * the appropriate HTTP response headers are already set.
1282
-     *
1283
-     * Normally this method will throw 412 Precondition Failed for failures
1284
-     * related to If-None-Match, If-Match and If-Unmodified Since. It will
1285
-     * set the status to 304 Not Modified for If-Modified_since.
1286
-     *
1287
-     * @return bool
1288
-     */
1289
-    public function checkPreconditions(RequestInterface $request, ResponseInterface $response)
1290
-    {
1291
-        $path = $request->getPath();
1292
-        $node = null;
1293
-        $lastMod = null;
1294
-        $etag = null;
1295
-
1296
-        if ($ifMatch = $request->getHeader('If-Match')) {
1297
-            // If-Match contains an entity tag. Only if the entity-tag
1298
-            // matches we are allowed to make the request succeed.
1299
-            // If the entity-tag is '*' we are only allowed to make the
1300
-            // request succeed if a resource exists at that url.
1301
-            try {
1302
-                $node = $this->tree->getNodeForPath($path);
1303
-            } catch (Exception\NotFound $e) {
1304
-                throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist', 'If-Match');
1305
-            }
1306
-
1307
-            // Only need to check entity tags if they are not *
1308
-            if ('*' !== $ifMatch) {
1309
-                // There can be multiple ETags
1310
-                $ifMatch = explode(',', $ifMatch);
1311
-                $haveMatch = false;
1312
-                foreach ($ifMatch as $ifMatchItem) {
1313
-                    // Stripping any extra spaces
1314
-                    $ifMatchItem = trim($ifMatchItem, ' ');
1315
-
1316
-                    $etag = $node instanceof IFile ? $node->getETag() : null;
1317
-                    if ($etag === $ifMatchItem) {
1318
-                        $haveMatch = true;
1319
-                    } else {
1320
-                        // Evolution has a bug where it sometimes prepends the "
1321
-                        // with a \. This is our workaround.
1322
-                        if (str_replace('\\"', '"', $ifMatchItem) === $etag) {
1323
-                            $haveMatch = true;
1324
-                        }
1325
-                    }
1326
-                }
1327
-                if (!$haveMatch) {
1328
-                    if ($etag) {
1329
-                        $response->setHeader('ETag', $etag);
1330
-                    }
1331
-                    throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified ETags matched.', 'If-Match');
1332
-                }
1333
-            }
1334
-        }
1335
-
1336
-        if ($ifNoneMatch = $request->getHeader('If-None-Match')) {
1337
-            // The If-None-Match header contains an ETag.
1338
-            // Only if the ETag does not match the current ETag, the request will succeed
1339
-            // The header can also contain *, in which case the request
1340
-            // will only succeed if the entity does not exist at all.
1341
-            $nodeExists = true;
1342
-            if (!$node) {
1343
-                try {
1344
-                    $node = $this->tree->getNodeForPath($path);
1345
-                } catch (Exception\NotFound $e) {
1346
-                    $nodeExists = false;
1347
-                }
1348
-            }
1349
-            if ($nodeExists) {
1350
-                $haveMatch = false;
1351
-                if ('*' === $ifNoneMatch) {
1352
-                    $haveMatch = true;
1353
-                } else {
1354
-                    // There might be multiple ETags
1355
-                    $ifNoneMatch = explode(',', $ifNoneMatch);
1356
-                    $etag = $node instanceof IFile ? $node->getETag() : null;
1357
-
1358
-                    foreach ($ifNoneMatch as $ifNoneMatchItem) {
1359
-                        // Stripping any extra spaces
1360
-                        $ifNoneMatchItem = trim($ifNoneMatchItem, ' ');
1361
-
1362
-                        if ($etag === $ifNoneMatchItem) {
1363
-                            $haveMatch = true;
1364
-                        }
1365
-                    }
1366
-                }
1367
-
1368
-                if ($haveMatch) {
1369
-                    if ($etag) {
1370
-                        $response->setHeader('ETag', $etag);
1371
-                    }
1372
-                    if ('GET' === $request->getMethod()) {
1373
-                        $response->setStatus(304);
1374
-
1375
-                        return false;
1376
-                    } else {
1377
-                        throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).', 'If-None-Match');
1378
-                    }
1379
-                }
1380
-            }
1381
-        }
1382
-
1383
-        if (!$ifNoneMatch && ($ifModifiedSince = $request->getHeader('If-Modified-Since'))) {
1384
-            // The If-Modified-Since header contains a date. We
1385
-            // will only return the entity if it has been changed since
1386
-            // that date. If it hasn't been changed, we return a 304
1387
-            // header
1388
-            // Note that this header only has to be checked if there was no If-None-Match header
1389
-            // as per the HTTP spec.
1390
-            $date = HTTP\parseDate($ifModifiedSince);
1391
-
1392
-            if ($date) {
1393
-                if (is_null($node)) {
1394
-                    $node = $this->tree->getNodeForPath($path);
1395
-                }
1396
-                $lastMod = $node->getLastModified();
1397
-                if ($lastMod) {
1398
-                    $lastMod = new \DateTime('@'.$lastMod);
1399
-                    if ($lastMod <= $date) {
1400
-                        $response->setStatus(304);
1401
-                        $response->setHeader('Last-Modified', HTTP\toDate($lastMod));
1402
-
1403
-                        return false;
1404
-                    }
1405
-                }
1406
-            }
1407
-        }
1408
-
1409
-        if ($ifUnmodifiedSince = $request->getHeader('If-Unmodified-Since')) {
1410
-            // The If-Unmodified-Since will allow allow the request if the
1411
-            // entity has not changed since the specified date.
1412
-            $date = HTTP\parseDate($ifUnmodifiedSince);
1413
-
1414
-            // We must only check the date if it's valid
1415
-            if ($date) {
1416
-                if (is_null($node)) {
1417
-                    $node = $this->tree->getNodeForPath($path);
1418
-                }
1419
-                $lastMod = $node->getLastModified();
1420
-                if ($lastMod) {
1421
-                    $lastMod = new \DateTime('@'.$lastMod);
1422
-                    if ($lastMod > $date) {
1423
-                        throw new Exception\PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.', 'If-Unmodified-Since');
1424
-                    }
1425
-                }
1426
-            }
1427
-        }
1428
-
1429
-        // Now the hardest, the If: header. The If: header can contain multiple
1430
-        // urls, ETags and so-called 'state tokens'.
1431
-        //
1432
-        // Examples of state tokens include lock-tokens (as defined in rfc4918)
1433
-        // and sync-tokens (as defined in rfc6578).
1434
-        //
1435
-        // The only proper way to deal with these, is to emit events, that a
1436
-        // Sync and Lock plugin can pick up.
1437
-        $ifConditions = $this->getIfConditions($request);
1438
-
1439
-        foreach ($ifConditions as $kk => $ifCondition) {
1440
-            foreach ($ifCondition['tokens'] as $ii => $token) {
1441
-                $ifConditions[$kk]['tokens'][$ii]['validToken'] = false;
1442
-            }
1443
-        }
1444
-
1445
-        // Plugins are responsible for validating all the tokens.
1446
-        // If a plugin deemed a token 'valid', it will set 'validToken' to
1447
-        // true.
1448
-        $this->emit('validateTokens', [$request, &$ifConditions]);
1449
-
1450
-        // Now we're going to analyze the result.
1451
-
1452
-        // Every ifCondition needs to validate to true, so we exit as soon as
1453
-        // we have an invalid condition.
1454
-        foreach ($ifConditions as $ifCondition) {
1455
-            $uri = $ifCondition['uri'];
1456
-            $tokens = $ifCondition['tokens'];
1457
-
1458
-            // We only need 1 valid token for the condition to succeed.
1459
-            foreach ($tokens as $token) {
1460
-                $tokenValid = $token['validToken'] || !$token['token'];
1461
-
1462
-                $etagValid = false;
1463
-                if (!$token['etag']) {
1464
-                    $etagValid = true;
1465
-                }
1466
-                // Checking the ETag, only if the token was already deemed
1467
-                // valid and there is one.
1468
-                if ($token['etag'] && $tokenValid) {
1469
-                    // The token was valid, and there was an ETag. We must
1470
-                    // grab the current ETag and check it.
1471
-                    $node = $this->tree->getNodeForPath($uri);
1472
-                    $etagValid = $node instanceof IFile && $node->getETag() == $token['etag'];
1473
-                }
1474
-
1475
-                if (($tokenValid && $etagValid) ^ $token['negate']) {
1476
-                    // Both were valid, so we can go to the next condition.
1477
-                    continue 2;
1478
-                }
1479
-            }
1480
-
1481
-            // If we ended here, it means there was no valid ETag + token
1482
-            // combination found for the current condition. This means we fail!
1483
-            throw new Exception\PreconditionFailed('Failed to find a valid token/etag combination for '.$uri, 'If');
1484
-        }
1485
-
1486
-        return true;
1487
-    }
1488
-
1489
-    /**
1490
-     * This method is created to extract information from the WebDAV HTTP 'If:' header.
1491
-     *
1492
-     * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information
1493
-     * The function will return an array, containing structs with the following keys
1494
-     *
1495
-     *   * uri   - the uri the condition applies to.
1496
-     *   * tokens - The lock token. another 2 dimensional array containing 3 elements
1497
-     *
1498
-     * Example 1:
1499
-     *
1500
-     * If: (<opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>)
1501
-     *
1502
-     * Would result in:
1503
-     *
1504
-     * [
1505
-     *    [
1506
-     *       'uri' => '/request/uri',
1507
-     *       'tokens' => [
1508
-     *          [
1509
-     *              [
1510
-     *                  'negate' => false,
1511
-     *                  'token'  => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2',
1512
-     *                  'etag'   => ""
1513
-     *              ]
1514
-     *          ]
1515
-     *       ],
1516
-     *    ]
1517
-     * ]
1518
-     *
1519
-     * Example 2:
1520
-     *
1521
-     * If: </path/> (Not <opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> ["Im An ETag"]) (["Another ETag"]) </path2/> (Not ["Path2 ETag"])
1522
-     *
1523
-     * Would result in:
1524
-     *
1525
-     * [
1526
-     *    [
1527
-     *       'uri' => 'path',
1528
-     *       'tokens' => [
1529
-     *          [
1530
-     *              [
1531
-     *                  'negate' => true,
1532
-     *                  'token'  => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2',
1533
-     *                  'etag'   => '"Im An ETag"'
1534
-     *              ],
1535
-     *              [
1536
-     *                  'negate' => false,
1537
-     *                  'token'  => '',
1538
-     *                  'etag'   => '"Another ETag"'
1539
-     *              ]
1540
-     *          ]
1541
-     *       ],
1542
-     *    ],
1543
-     *    [
1544
-     *       'uri' => 'path2',
1545
-     *       'tokens' => [
1546
-     *          [
1547
-     *              [
1548
-     *                  'negate' => true,
1549
-     *                  'token'  => '',
1550
-     *                  'etag'   => '"Path2 ETag"'
1551
-     *              ]
1552
-     *          ]
1553
-     *       ],
1554
-     *    ],
1555
-     * ]
1556
-     *
1557
-     * @return array
1558
-     */
1559
-    public function getIfConditions(RequestInterface $request)
1560
-    {
1561
-        $header = $request->getHeader('If');
1562
-        if (!$header) {
1563
-            return [];
1564
-        }
1565
-
1566
-        $matches = [];
1567
-
1568
-        $regex = '/(?:\<(?P<uri>.*?)\>\s)?\((?P<not>Not\s)?(?:\<(?P<token>[^\>]*)\>)?(?:\s?)(?:\[(?P<etag>[^\]]*)\])?\)/im';
1569
-        preg_match_all($regex, $header, $matches, PREG_SET_ORDER);
1570
-
1571
-        $conditions = [];
1572
-
1573
-        foreach ($matches as $match) {
1574
-            // If there was no uri specified in this match, and there were
1575
-            // already conditions parsed, we add the condition to the list of
1576
-            // conditions for the previous uri.
1577
-            if (!$match['uri'] && count($conditions)) {
1578
-                $conditions[count($conditions) - 1]['tokens'][] = [
1579
-                    'negate' => $match['not'] ? true : false,
1580
-                    'token' => $match['token'],
1581
-                    'etag' => isset($match['etag']) ? $match['etag'] : '',
1582
-                ];
1583
-            } else {
1584
-                if (!$match['uri']) {
1585
-                    $realUri = $request->getPath();
1586
-                } else {
1587
-                    $realUri = $this->calculateUri($match['uri']);
1588
-                }
1589
-
1590
-                $conditions[] = [
1591
-                    'uri' => $realUri,
1592
-                    'tokens' => [
1593
-                        [
1594
-                            'negate' => $match['not'] ? true : false,
1595
-                            'token' => $match['token'],
1596
-                            'etag' => isset($match['etag']) ? $match['etag'] : '',
1597
-                        ],
1598
-                    ],
1599
-                ];
1600
-            }
1601
-        }
1602
-
1603
-        return $conditions;
1604
-    }
1605
-
1606
-    /**
1607
-     * Returns an array with resourcetypes for a node.
1608
-     *
1609
-     * @return array
1610
-     */
1611
-    public function getResourceTypeForNode(INode $node)
1612
-    {
1613
-        $result = [];
1614
-        foreach ($this->resourceTypeMapping as $className => $resourceType) {
1615
-            if ($node instanceof $className) {
1616
-                $result[] = $resourceType;
1617
-            }
1618
-        }
1619
-
1620
-        return $result;
1621
-    }
1622
-
1623
-    // }}}
1624
-    // {{{ XML Readers & Writers
1625
-
1626
-    /**
1627
-     * Returns a callback generating a WebDAV propfind response body based on a list of nodes.
1628
-     *
1629
-     * If 'strip404s' is set to true, all 404 responses will be removed.
1630
-     *
1631
-     * @param array|\Traversable $fileProperties The list with nodes
1632
-     * @param bool               $strip404s
1633
-     *
1634
-     * @return callable|string
1635
-     */
1636
-    public function generateMultiStatus($fileProperties, $strip404s = false)
1637
-    {
1638
-        $w = $this->xml->getWriter();
1639
-        if (self::$streamMultiStatus) {
1640
-            return function () use ($fileProperties, $strip404s, $w) {
1641
-                $w->openUri('php://output');
1642
-                $this->writeMultiStatus($w, $fileProperties, $strip404s);
1643
-                $w->flush();
1644
-            };
1645
-        }
1646
-        $w->openMemory();
1647
-        $this->writeMultiStatus($w, $fileProperties, $strip404s);
1648
-
1649
-        return $w->outputMemory();
1650
-    }
1651
-
1652
-    /**
1653
-     * @param $fileProperties
1654
-     */
1655
-    private function writeMultiStatus(Writer $w, $fileProperties, bool $strip404s)
1656
-    {
1657
-        $w->contextUri = $this->baseUri;
1658
-        $w->startDocument();
1659
-
1660
-        $w->startElement('{DAV:}multistatus');
1661
-
1662
-        foreach ($fileProperties as $entry) {
1663
-            $href = $entry['href'];
1664
-            unset($entry['href']);
1665
-            if ($strip404s) {
1666
-                unset($entry[404]);
1667
-            }
1668
-            $response = new Xml\Element\Response(
1669
-                ltrim($href, '/'),
1670
-                $entry
1671
-            );
1672
-            $w->write([
1673
-                'name' => '{DAV:}response',
1674
-                'value' => $response,
1675
-            ]);
1676
-        }
1677
-        $w->endElement();
1678
-        $w->endDocument();
1679
-    }
1206
+			if (count($mkCol->getResourceType()) > 1) {
1207
+				throw new Exception\InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.');
1208
+			}
1209
+
1210
+			$parent->createDirectory($newName);
1211
+		}
1212
+
1213
+		// If there are any properties that have not been handled/stored,
1214
+		// we ask the 'propPatch' event to handle them. This will allow for
1215
+		// example the propertyStorage system to store properties upon MKCOL.
1216
+		if ($mkCol->getRemainingMutations()) {
1217
+			$this->emit('propPatch', [$uri, $mkCol]);
1218
+		}
1219
+		$success = $mkCol->commit();
1220
+
1221
+		if (!$success) {
1222
+			$result = $mkCol->getResult();
1223
+
1224
+			$formattedResult = [
1225
+				'href' => $uri,
1226
+			];
1227
+
1228
+			foreach ($result as $propertyName => $status) {
1229
+				if (!isset($formattedResult[$status])) {
1230
+					$formattedResult[$status] = [];
1231
+				}
1232
+				$formattedResult[$status][$propertyName] = null;
1233
+			}
1234
+
1235
+			return $formattedResult;
1236
+		}
1237
+
1238
+		$this->tree->markDirty($parentUri);
1239
+		$this->emit('afterBind', [$uri]);
1240
+		$this->emit('afterCreateCollection', [$uri]);
1241
+	}
1242
+
1243
+	/**
1244
+	 * This method updates a resource's properties.
1245
+	 *
1246
+	 * The properties array must be a list of properties. Array-keys are
1247
+	 * property names in clarknotation, array-values are it's values.
1248
+	 * If a property must be deleted, the value should be null.
1249
+	 *
1250
+	 * Note that this request should either completely succeed, or
1251
+	 * completely fail.
1252
+	 *
1253
+	 * The response is an array with properties for keys, and http status codes
1254
+	 * as their values.
1255
+	 *
1256
+	 * @param string $path
1257
+	 *
1258
+	 * @return array
1259
+	 */
1260
+	public function updateProperties($path, array $properties)
1261
+	{
1262
+		$propPatch = new PropPatch($properties);
1263
+		$this->emit('propPatch', [$path, $propPatch]);
1264
+		$propPatch->commit();
1265
+
1266
+		return $propPatch->getResult();
1267
+	}
1268
+
1269
+	/**
1270
+	 * This method checks the main HTTP preconditions.
1271
+	 *
1272
+	 * Currently these are:
1273
+	 *   * If-Match
1274
+	 *   * If-None-Match
1275
+	 *   * If-Modified-Since
1276
+	 *   * If-Unmodified-Since
1277
+	 *
1278
+	 * The method will return true if all preconditions are met
1279
+	 * The method will return false, or throw an exception if preconditions
1280
+	 * failed. If false is returned the operation should be aborted, and
1281
+	 * the appropriate HTTP response headers are already set.
1282
+	 *
1283
+	 * Normally this method will throw 412 Precondition Failed for failures
1284
+	 * related to If-None-Match, If-Match and If-Unmodified Since. It will
1285
+	 * set the status to 304 Not Modified for If-Modified_since.
1286
+	 *
1287
+	 * @return bool
1288
+	 */
1289
+	public function checkPreconditions(RequestInterface $request, ResponseInterface $response)
1290
+	{
1291
+		$path = $request->getPath();
1292
+		$node = null;
1293
+		$lastMod = null;
1294
+		$etag = null;
1295
+
1296
+		if ($ifMatch = $request->getHeader('If-Match')) {
1297
+			// If-Match contains an entity tag. Only if the entity-tag
1298
+			// matches we are allowed to make the request succeed.
1299
+			// If the entity-tag is '*' we are only allowed to make the
1300
+			// request succeed if a resource exists at that url.
1301
+			try {
1302
+				$node = $this->tree->getNodeForPath($path);
1303
+			} catch (Exception\NotFound $e) {
1304
+				throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist', 'If-Match');
1305
+			}
1306
+
1307
+			// Only need to check entity tags if they are not *
1308
+			if ('*' !== $ifMatch) {
1309
+				// There can be multiple ETags
1310
+				$ifMatch = explode(',', $ifMatch);
1311
+				$haveMatch = false;
1312
+				foreach ($ifMatch as $ifMatchItem) {
1313
+					// Stripping any extra spaces
1314
+					$ifMatchItem = trim($ifMatchItem, ' ');
1315
+
1316
+					$etag = $node instanceof IFile ? $node->getETag() : null;
1317
+					if ($etag === $ifMatchItem) {
1318
+						$haveMatch = true;
1319
+					} else {
1320
+						// Evolution has a bug where it sometimes prepends the "
1321
+						// with a \. This is our workaround.
1322
+						if (str_replace('\\"', '"', $ifMatchItem) === $etag) {
1323
+							$haveMatch = true;
1324
+						}
1325
+					}
1326
+				}
1327
+				if (!$haveMatch) {
1328
+					if ($etag) {
1329
+						$response->setHeader('ETag', $etag);
1330
+					}
1331
+					throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified ETags matched.', 'If-Match');
1332
+				}
1333
+			}
1334
+		}
1335
+
1336
+		if ($ifNoneMatch = $request->getHeader('If-None-Match')) {
1337
+			// The If-None-Match header contains an ETag.
1338
+			// Only if the ETag does not match the current ETag, the request will succeed
1339
+			// The header can also contain *, in which case the request
1340
+			// will only succeed if the entity does not exist at all.
1341
+			$nodeExists = true;
1342
+			if (!$node) {
1343
+				try {
1344
+					$node = $this->tree->getNodeForPath($path);
1345
+				} catch (Exception\NotFound $e) {
1346
+					$nodeExists = false;
1347
+				}
1348
+			}
1349
+			if ($nodeExists) {
1350
+				$haveMatch = false;
1351
+				if ('*' === $ifNoneMatch) {
1352
+					$haveMatch = true;
1353
+				} else {
1354
+					// There might be multiple ETags
1355
+					$ifNoneMatch = explode(',', $ifNoneMatch);
1356
+					$etag = $node instanceof IFile ? $node->getETag() : null;
1357
+
1358
+					foreach ($ifNoneMatch as $ifNoneMatchItem) {
1359
+						// Stripping any extra spaces
1360
+						$ifNoneMatchItem = trim($ifNoneMatchItem, ' ');
1361
+
1362
+						if ($etag === $ifNoneMatchItem) {
1363
+							$haveMatch = true;
1364
+						}
1365
+					}
1366
+				}
1367
+
1368
+				if ($haveMatch) {
1369
+					if ($etag) {
1370
+						$response->setHeader('ETag', $etag);
1371
+					}
1372
+					if ('GET' === $request->getMethod()) {
1373
+						$response->setStatus(304);
1374
+
1375
+						return false;
1376
+					} else {
1377
+						throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).', 'If-None-Match');
1378
+					}
1379
+				}
1380
+			}
1381
+		}
1382
+
1383
+		if (!$ifNoneMatch && ($ifModifiedSince = $request->getHeader('If-Modified-Since'))) {
1384
+			// The If-Modified-Since header contains a date. We
1385
+			// will only return the entity if it has been changed since
1386
+			// that date. If it hasn't been changed, we return a 304
1387
+			// header
1388
+			// Note that this header only has to be checked if there was no If-None-Match header
1389
+			// as per the HTTP spec.
1390
+			$date = HTTP\parseDate($ifModifiedSince);
1391
+
1392
+			if ($date) {
1393
+				if (is_null($node)) {
1394
+					$node = $this->tree->getNodeForPath($path);
1395
+				}
1396
+				$lastMod = $node->getLastModified();
1397
+				if ($lastMod) {
1398
+					$lastMod = new \DateTime('@'.$lastMod);
1399
+					if ($lastMod <= $date) {
1400
+						$response->setStatus(304);
1401
+						$response->setHeader('Last-Modified', HTTP\toDate($lastMod));
1402
+
1403
+						return false;
1404
+					}
1405
+				}
1406
+			}
1407
+		}
1408
+
1409
+		if ($ifUnmodifiedSince = $request->getHeader('If-Unmodified-Since')) {
1410
+			// The If-Unmodified-Since will allow allow the request if the
1411
+			// entity has not changed since the specified date.
1412
+			$date = HTTP\parseDate($ifUnmodifiedSince);
1413
+
1414
+			// We must only check the date if it's valid
1415
+			if ($date) {
1416
+				if (is_null($node)) {
1417
+					$node = $this->tree->getNodeForPath($path);
1418
+				}
1419
+				$lastMod = $node->getLastModified();
1420
+				if ($lastMod) {
1421
+					$lastMod = new \DateTime('@'.$lastMod);
1422
+					if ($lastMod > $date) {
1423
+						throw new Exception\PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.', 'If-Unmodified-Since');
1424
+					}
1425
+				}
1426
+			}
1427
+		}
1428
+
1429
+		// Now the hardest, the If: header. The If: header can contain multiple
1430
+		// urls, ETags and so-called 'state tokens'.
1431
+		//
1432
+		// Examples of state tokens include lock-tokens (as defined in rfc4918)
1433
+		// and sync-tokens (as defined in rfc6578).
1434
+		//
1435
+		// The only proper way to deal with these, is to emit events, that a
1436
+		// Sync and Lock plugin can pick up.
1437
+		$ifConditions = $this->getIfConditions($request);
1438
+
1439
+		foreach ($ifConditions as $kk => $ifCondition) {
1440
+			foreach ($ifCondition['tokens'] as $ii => $token) {
1441
+				$ifConditions[$kk]['tokens'][$ii]['validToken'] = false;
1442
+			}
1443
+		}
1444
+
1445
+		// Plugins are responsible for validating all the tokens.
1446
+		// If a plugin deemed a token 'valid', it will set 'validToken' to
1447
+		// true.
1448
+		$this->emit('validateTokens', [$request, &$ifConditions]);
1449
+
1450
+		// Now we're going to analyze the result.
1451
+
1452
+		// Every ifCondition needs to validate to true, so we exit as soon as
1453
+		// we have an invalid condition.
1454
+		foreach ($ifConditions as $ifCondition) {
1455
+			$uri = $ifCondition['uri'];
1456
+			$tokens = $ifCondition['tokens'];
1457
+
1458
+			// We only need 1 valid token for the condition to succeed.
1459
+			foreach ($tokens as $token) {
1460
+				$tokenValid = $token['validToken'] || !$token['token'];
1461
+
1462
+				$etagValid = false;
1463
+				if (!$token['etag']) {
1464
+					$etagValid = true;
1465
+				}
1466
+				// Checking the ETag, only if the token was already deemed
1467
+				// valid and there is one.
1468
+				if ($token['etag'] && $tokenValid) {
1469
+					// The token was valid, and there was an ETag. We must
1470
+					// grab the current ETag and check it.
1471
+					$node = $this->tree->getNodeForPath($uri);
1472
+					$etagValid = $node instanceof IFile && $node->getETag() == $token['etag'];
1473
+				}
1474
+
1475
+				if (($tokenValid && $etagValid) ^ $token['negate']) {
1476
+					// Both were valid, so we can go to the next condition.
1477
+					continue 2;
1478
+				}
1479
+			}
1480
+
1481
+			// If we ended here, it means there was no valid ETag + token
1482
+			// combination found for the current condition. This means we fail!
1483
+			throw new Exception\PreconditionFailed('Failed to find a valid token/etag combination for '.$uri, 'If');
1484
+		}
1485
+
1486
+		return true;
1487
+	}
1488
+
1489
+	/**
1490
+	 * This method is created to extract information from the WebDAV HTTP 'If:' header.
1491
+	 *
1492
+	 * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information
1493
+	 * The function will return an array, containing structs with the following keys
1494
+	 *
1495
+	 *   * uri   - the uri the condition applies to.
1496
+	 *   * tokens - The lock token. another 2 dimensional array containing 3 elements
1497
+	 *
1498
+	 * Example 1:
1499
+	 *
1500
+	 * If: (<opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>)
1501
+	 *
1502
+	 * Would result in:
1503
+	 *
1504
+	 * [
1505
+	 *    [
1506
+	 *       'uri' => '/request/uri',
1507
+	 *       'tokens' => [
1508
+	 *          [
1509
+	 *              [
1510
+	 *                  'negate' => false,
1511
+	 *                  'token'  => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2',
1512
+	 *                  'etag'   => ""
1513
+	 *              ]
1514
+	 *          ]
1515
+	 *       ],
1516
+	 *    ]
1517
+	 * ]
1518
+	 *
1519
+	 * Example 2:
1520
+	 *
1521
+	 * If: </path/> (Not <opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> ["Im An ETag"]) (["Another ETag"]) </path2/> (Not ["Path2 ETag"])
1522
+	 *
1523
+	 * Would result in:
1524
+	 *
1525
+	 * [
1526
+	 *    [
1527
+	 *       'uri' => 'path',
1528
+	 *       'tokens' => [
1529
+	 *          [
1530
+	 *              [
1531
+	 *                  'negate' => true,
1532
+	 *                  'token'  => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2',
1533
+	 *                  'etag'   => '"Im An ETag"'
1534
+	 *              ],
1535
+	 *              [
1536
+	 *                  'negate' => false,
1537
+	 *                  'token'  => '',
1538
+	 *                  'etag'   => '"Another ETag"'
1539
+	 *              ]
1540
+	 *          ]
1541
+	 *       ],
1542
+	 *    ],
1543
+	 *    [
1544
+	 *       'uri' => 'path2',
1545
+	 *       'tokens' => [
1546
+	 *          [
1547
+	 *              [
1548
+	 *                  'negate' => true,
1549
+	 *                  'token'  => '',
1550
+	 *                  'etag'   => '"Path2 ETag"'
1551
+	 *              ]
1552
+	 *          ]
1553
+	 *       ],
1554
+	 *    ],
1555
+	 * ]
1556
+	 *
1557
+	 * @return array
1558
+	 */
1559
+	public function getIfConditions(RequestInterface $request)
1560
+	{
1561
+		$header = $request->getHeader('If');
1562
+		if (!$header) {
1563
+			return [];
1564
+		}
1565
+
1566
+		$matches = [];
1567
+
1568
+		$regex = '/(?:\<(?P<uri>.*?)\>\s)?\((?P<not>Not\s)?(?:\<(?P<token>[^\>]*)\>)?(?:\s?)(?:\[(?P<etag>[^\]]*)\])?\)/im';
1569
+		preg_match_all($regex, $header, $matches, PREG_SET_ORDER);
1570
+
1571
+		$conditions = [];
1572
+
1573
+		foreach ($matches as $match) {
1574
+			// If there was no uri specified in this match, and there were
1575
+			// already conditions parsed, we add the condition to the list of
1576
+			// conditions for the previous uri.
1577
+			if (!$match['uri'] && count($conditions)) {
1578
+				$conditions[count($conditions) - 1]['tokens'][] = [
1579
+					'negate' => $match['not'] ? true : false,
1580
+					'token' => $match['token'],
1581
+					'etag' => isset($match['etag']) ? $match['etag'] : '',
1582
+				];
1583
+			} else {
1584
+				if (!$match['uri']) {
1585
+					$realUri = $request->getPath();
1586
+				} else {
1587
+					$realUri = $this->calculateUri($match['uri']);
1588
+				}
1589
+
1590
+				$conditions[] = [
1591
+					'uri' => $realUri,
1592
+					'tokens' => [
1593
+						[
1594
+							'negate' => $match['not'] ? true : false,
1595
+							'token' => $match['token'],
1596
+							'etag' => isset($match['etag']) ? $match['etag'] : '',
1597
+						],
1598
+					],
1599
+				];
1600
+			}
1601
+		}
1602
+
1603
+		return $conditions;
1604
+	}
1605
+
1606
+	/**
1607
+	 * Returns an array with resourcetypes for a node.
1608
+	 *
1609
+	 * @return array
1610
+	 */
1611
+	public function getResourceTypeForNode(INode $node)
1612
+	{
1613
+		$result = [];
1614
+		foreach ($this->resourceTypeMapping as $className => $resourceType) {
1615
+			if ($node instanceof $className) {
1616
+				$result[] = $resourceType;
1617
+			}
1618
+		}
1619
+
1620
+		return $result;
1621
+	}
1622
+
1623
+	// }}}
1624
+	// {{{ XML Readers & Writers
1625
+
1626
+	/**
1627
+	 * Returns a callback generating a WebDAV propfind response body based on a list of nodes.
1628
+	 *
1629
+	 * If 'strip404s' is set to true, all 404 responses will be removed.
1630
+	 *
1631
+	 * @param array|\Traversable $fileProperties The list with nodes
1632
+	 * @param bool               $strip404s
1633
+	 *
1634
+	 * @return callable|string
1635
+	 */
1636
+	public function generateMultiStatus($fileProperties, $strip404s = false)
1637
+	{
1638
+		$w = $this->xml->getWriter();
1639
+		if (self::$streamMultiStatus) {
1640
+			return function () use ($fileProperties, $strip404s, $w) {
1641
+				$w->openUri('php://output');
1642
+				$this->writeMultiStatus($w, $fileProperties, $strip404s);
1643
+				$w->flush();
1644
+			};
1645
+		}
1646
+		$w->openMemory();
1647
+		$this->writeMultiStatus($w, $fileProperties, $strip404s);
1648
+
1649
+		return $w->outputMemory();
1650
+	}
1651
+
1652
+	/**
1653
+	 * @param $fileProperties
1654
+	 */
1655
+	private function writeMultiStatus(Writer $w, $fileProperties, bool $strip404s)
1656
+	{
1657
+		$w->contextUri = $this->baseUri;
1658
+		$w->startDocument();
1659
+
1660
+		$w->startElement('{DAV:}multistatus');
1661
+
1662
+		foreach ($fileProperties as $entry) {
1663
+			$href = $entry['href'];
1664
+			unset($entry['href']);
1665
+			if ($strip404s) {
1666
+				unset($entry[404]);
1667
+			}
1668
+			$response = new Xml\Element\Response(
1669
+				ltrim($href, '/'),
1670
+				$entry
1671
+			);
1672
+			$w->write([
1673
+				'name' => '{DAV:}response',
1674
+				'value' => $response,
1675
+			]);
1676
+		}
1677
+		$w->endElement();
1678
+		$w->endDocument();
1679
+	}
1680 1680
 }
Please login to merge, or discard this patch.
Spacing   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -263,7 +263,7 @@  discard block
 block discarded – undo
263 263
             $error->setAttribute('xmlns:s', self::NS_SABREDAV);
264 264
             $DOM->appendChild($error);
265 265
 
266
-            $h = function ($v) {
266
+            $h = function($v) {
267 267
                 return htmlspecialchars((string) $v, ENT_NOQUOTES, 'UTF-8');
268 268
             };
269 269
 
@@ -1637,7 +1637,7 @@  discard block
 block discarded – undo
1637 1637
     {
1638 1638
         $w = $this->xml->getWriter();
1639 1639
         if (self::$streamMultiStatus) {
1640
-            return function () use ($fileProperties, $strip404s, $w) {
1640
+            return function() use ($fileProperties, $strip404s, $w) {
1641 1641
                 $w->openUri('php://output');
1642 1642
                 $this->writeMultiStatus($w, $fileProperties, $strip404s);
1643 1643
                 $w->flush();
Please login to merge, or discard this patch.