|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* @author Joas Schilling <[email protected]> |
|
4
|
|
|
* @author Thomas Müller <[email protected]> |
|
5
|
|
|
* @author Vincent Petry <[email protected]> |
|
6
|
|
|
* |
|
7
|
|
|
* @copyright Copyright (c) 2018, ownCloud GmbH |
|
8
|
|
|
* @license AGPL-3.0 |
|
9
|
|
|
* |
|
10
|
|
|
* This code is free software: you can redistribute it and/or modify |
|
11
|
|
|
* it under the terms of the GNU Affero General Public License, version 3, |
|
12
|
|
|
* as published by the Free Software Foundation. |
|
13
|
|
|
* |
|
14
|
|
|
* This program is distributed in the hope that it will be useful, |
|
15
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
16
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
17
|
|
|
* GNU Affero General Public License for more details. |
|
18
|
|
|
* |
|
19
|
|
|
* You should have received a copy of the GNU Affero General Public License, version 3, |
|
20
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/> |
|
21
|
|
|
* |
|
22
|
|
|
*/ |
|
23
|
|
|
|
|
24
|
|
|
namespace OCA\DAV\Connector\Sabre; |
|
25
|
|
|
|
|
26
|
|
|
use OC\Files\View; |
|
27
|
|
|
use OCA\DAV\Files\Xml\FilterRequest; |
|
28
|
|
|
use OCP\Files\Folder; |
|
29
|
|
|
use OCP\IGroupManager; |
|
30
|
|
|
use OCP\ITagManager; |
|
31
|
|
|
use OCP\IUserSession; |
|
32
|
|
|
use OCP\SystemTag\ISystemTagManager; |
|
33
|
|
|
use OCP\SystemTag\ISystemTagObjectMapper; |
|
34
|
|
|
use OCP\SystemTag\TagNotFoundException; |
|
35
|
|
|
use Sabre\DAV\Exception\BadRequest; |
|
36
|
|
|
use Sabre\DAV\Exception\PreconditionFailed; |
|
37
|
|
|
use Sabre\DAV\PropFind; |
|
38
|
|
|
use Sabre\DAV\ServerPlugin; |
|
39
|
|
|
use Sabre\DAV\Tree; |
|
40
|
|
|
use Sabre\DAV\Xml\Element\Response; |
|
41
|
|
|
|
|
42
|
|
|
class FilesReportPlugin extends ServerPlugin { |
|
43
|
|
|
|
|
44
|
|
|
// namespace |
|
45
|
|
|
const NS_OWNCLOUD = 'http://owncloud.org/ns'; |
|
46
|
|
|
const REPORT_NAME = '{http://owncloud.org/ns}filter-files'; |
|
47
|
|
|
const SYSTEMTAG_PROPERTYNAME = '{http://owncloud.org/ns}systemtag'; |
|
48
|
|
|
|
|
49
|
|
|
/** |
|
50
|
|
|
* Reference to main server object |
|
51
|
|
|
* |
|
52
|
|
|
* @var \Sabre\DAV\Server |
|
53
|
|
|
*/ |
|
54
|
|
|
private $server; |
|
55
|
|
|
|
|
56
|
|
|
/** |
|
57
|
|
|
* @var Tree |
|
58
|
|
|
*/ |
|
59
|
|
|
private $tree; |
|
60
|
|
|
|
|
61
|
|
|
/** |
|
62
|
|
|
* @var View |
|
63
|
|
|
*/ |
|
64
|
|
|
private $fileView; |
|
65
|
|
|
|
|
66
|
|
|
/** |
|
67
|
|
|
* @var ISystemTagManager |
|
68
|
|
|
*/ |
|
69
|
|
|
private $tagManager; |
|
70
|
|
|
|
|
71
|
|
|
/** |
|
72
|
|
|
* @var ISystemTagObjectMapper |
|
73
|
|
|
*/ |
|
74
|
|
|
private $tagMapper; |
|
75
|
|
|
|
|
76
|
|
|
/** |
|
77
|
|
|
* Manager for private tags |
|
78
|
|
|
* |
|
79
|
|
|
* @var ITagManager |
|
80
|
|
|
*/ |
|
81
|
|
|
private $fileTagger; |
|
82
|
|
|
|
|
83
|
|
|
/** |
|
84
|
|
|
* @var IUserSession |
|
85
|
|
|
*/ |
|
86
|
|
|
private $userSession; |
|
87
|
|
|
|
|
88
|
|
|
/** |
|
89
|
|
|
* @var IGroupManager |
|
90
|
|
|
*/ |
|
91
|
|
|
private $groupManager; |
|
92
|
|
|
|
|
93
|
|
|
/** |
|
94
|
|
|
* @var Folder |
|
95
|
|
|
*/ |
|
96
|
|
|
private $userFolder; |
|
97
|
|
|
|
|
98
|
|
|
/** |
|
99
|
|
|
* @param Tree $tree |
|
100
|
|
|
* @param View $view |
|
101
|
|
|
* @param ISystemTagManager $tagManager |
|
102
|
|
|
* @param ISystemTagObjectMapper $tagMapper |
|
103
|
|
|
* @param ITagManager $fileTagger manager for private tags |
|
104
|
|
|
* @param IUserSession $userSession |
|
105
|
|
|
* @param IGroupManager $groupManager |
|
106
|
|
|
* @param Folder $userFolder |
|
107
|
|
|
*/ |
|
108
|
|
|
public function __construct(Tree $tree, |
|
109
|
|
|
View $view, |
|
110
|
|
|
ISystemTagManager $tagManager, |
|
111
|
|
|
ISystemTagObjectMapper $tagMapper, |
|
112
|
|
|
ITagManager $fileTagger, |
|
113
|
|
|
IUserSession $userSession, |
|
114
|
|
|
IGroupManager $groupManager, |
|
115
|
|
|
Folder $userFolder |
|
116
|
|
|
) { |
|
117
|
|
|
$this->tree = $tree; |
|
118
|
|
|
$this->fileView = $view; |
|
119
|
|
|
$this->tagManager = $tagManager; |
|
120
|
|
|
$this->tagMapper = $tagMapper; |
|
121
|
|
|
$this->fileTagger = $fileTagger; |
|
122
|
|
|
$this->userSession = $userSession; |
|
123
|
|
|
$this->groupManager = $groupManager; |
|
124
|
|
|
$this->userFolder = $userFolder; |
|
125
|
|
|
} |
|
126
|
|
|
|
|
127
|
|
|
/** |
|
128
|
|
|
* This initializes the plugin. |
|
129
|
|
|
* |
|
130
|
|
|
* This function is called by \Sabre\DAV\Server, after |
|
131
|
|
|
* addPlugin is called. |
|
132
|
|
|
* |
|
133
|
|
|
* This method should set up the required event subscriptions. |
|
134
|
|
|
* |
|
135
|
|
|
* @param \Sabre\DAV\Server $server |
|
136
|
|
|
* @return void |
|
137
|
|
|
*/ |
|
138
|
|
View Code Duplication |
public function initialize(\Sabre\DAV\Server $server) { |
|
139
|
|
|
$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; |
|
140
|
|
|
|
|
141
|
|
|
$server->xml->elementMap[self::REPORT_NAME] = FilterRequest::class; |
|
142
|
|
|
|
|
143
|
|
|
$this->server = $server; |
|
144
|
|
|
$this->server->on('report', [$this, 'onReport']); |
|
145
|
|
|
} |
|
146
|
|
|
|
|
147
|
|
|
/** |
|
148
|
|
|
* Returns a list of reports this plugin supports. |
|
149
|
|
|
* |
|
150
|
|
|
* This will be used in the {DAV:}supported-report-set property. |
|
151
|
|
|
* |
|
152
|
|
|
* @param string $uri |
|
153
|
|
|
* @return array |
|
154
|
|
|
*/ |
|
155
|
|
|
public function getSupportedReportSet($uri) { |
|
156
|
|
|
return [self::REPORT_NAME]; |
|
157
|
|
|
} |
|
158
|
|
|
|
|
159
|
|
|
/** |
|
160
|
|
|
* REPORT operations to look for files |
|
161
|
|
|
* |
|
162
|
|
|
* @param string $reportName |
|
163
|
|
|
* @param mixed $report |
|
164
|
|
|
* @param string $uri |
|
165
|
|
|
* @return bool |
|
166
|
|
|
* @throws BadRequest |
|
167
|
|
|
* @throws PreconditionFailed |
|
168
|
|
|
* @internal param $ [] $report |
|
169
|
|
|
*/ |
|
170
|
|
|
public function onReport($reportName, $report, $uri) { |
|
171
|
|
|
$reportTargetNode = $this->server->tree->getNodeForPath($uri); |
|
172
|
|
|
if (!$reportTargetNode instanceof Directory || $reportName !== self::REPORT_NAME) { |
|
173
|
|
|
return; |
|
174
|
|
|
} |
|
175
|
|
|
|
|
176
|
|
|
$requestedProps = $report->properties; |
|
177
|
|
|
$filterRules = $report->filters; |
|
178
|
|
|
|
|
179
|
|
|
// "systemtag" is always an array of tags, favorite a string/int/null |
|
180
|
|
|
if (empty($filterRules['systemtag']) && $filterRules['favorite'] === null) { |
|
181
|
|
|
// FIXME: search currently not possible because results are missing properties! |
|
182
|
|
|
throw new BadRequest('No filter criteria specified'); |
|
183
|
|
|
} else { |
|
184
|
|
|
if (isset($report->search['pattern'])) { |
|
185
|
|
|
// TODO: implement this at some point... |
|
186
|
|
|
throw new BadRequest('Search pattern cannot be combined with filter'); |
|
187
|
|
|
} |
|
188
|
|
|
|
|
189
|
|
|
// gather all file ids matching filter |
|
190
|
|
|
try { |
|
191
|
|
|
$resultFileIds = $this->processFilterRules($filterRules); |
|
192
|
|
|
} catch (TagNotFoundException $e) { |
|
193
|
|
|
throw new PreconditionFailed('Cannot filter by non-existing tag', 0, $e); |
|
|
|
|
|
|
194
|
|
|
} |
|
195
|
|
|
|
|
196
|
|
|
// pre-slice the results if needed for pagination to not waste |
|
197
|
|
|
// time resolving nodes that will not be returned anyway |
|
198
|
|
|
$resultFileIds = $this->slice($resultFileIds, $report); |
|
199
|
|
|
|
|
200
|
|
|
// find sabre nodes by file id, restricted to the root node path |
|
201
|
|
|
$results = $this->findNodesByFileIds($reportTargetNode, $resultFileIds); |
|
202
|
|
|
} |
|
203
|
|
|
|
|
204
|
|
|
$filesUri = $this->getFilesBaseUri($uri, $reportTargetNode->getPath()); |
|
205
|
|
|
$results = $this->prepareResponses($filesUri, $requestedProps, $results); |
|
206
|
|
|
|
|
207
|
|
|
$xml = $this->server->generateMultiStatus($results); |
|
208
|
|
|
|
|
209
|
|
|
$this->server->httpResponse->setStatus(207); |
|
210
|
|
|
$this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); |
|
211
|
|
|
$this->server->httpResponse->setBody($xml); |
|
212
|
|
|
|
|
213
|
|
|
return false; |
|
214
|
|
|
} |
|
215
|
|
|
|
|
216
|
|
|
private function slice($results, $report) { |
|
217
|
|
|
if ($report->search !== null) { |
|
218
|
|
|
$length = $report->search['limit']; |
|
219
|
|
|
$offset = $report->search['offset']; |
|
220
|
|
|
$results = \array_slice($results, $offset, $length); |
|
221
|
|
|
} |
|
222
|
|
|
return $results; |
|
223
|
|
|
} |
|
224
|
|
|
|
|
225
|
|
|
/** |
|
226
|
|
|
* Returns the base uri of the files root by removing |
|
227
|
|
|
* the subpath from the URI |
|
228
|
|
|
* |
|
229
|
|
|
* @param string $uri URI from this request |
|
230
|
|
|
* @param string $subPath subpath to remove from the URI |
|
231
|
|
|
* |
|
232
|
|
|
* @return string files base uri |
|
233
|
|
|
*/ |
|
234
|
|
|
private function getFilesBaseUri($uri, $subPath) { |
|
235
|
|
|
$uri = \trim($uri, '/'); |
|
236
|
|
|
$subPath = \trim($subPath, '/'); |
|
237
|
|
|
if (empty($subPath)) { |
|
238
|
|
|
$filesUri = $uri; |
|
239
|
|
|
} else { |
|
240
|
|
|
$filesUri = \substr($uri, 0, \strlen($uri) - \strlen($subPath)); |
|
241
|
|
|
} |
|
242
|
|
|
$filesUri = \trim($filesUri, '/'); |
|
243
|
|
|
if (empty($filesUri)) { |
|
244
|
|
|
return ''; |
|
245
|
|
|
} |
|
246
|
|
|
return '/' . $filesUri; |
|
247
|
|
|
} |
|
248
|
|
|
|
|
249
|
|
|
/** |
|
250
|
|
|
* Find file ids matching the given filter rules |
|
251
|
|
|
* |
|
252
|
|
|
* @param array $filterRules |
|
253
|
|
|
* @return array array of unique file id results |
|
254
|
|
|
* |
|
255
|
|
|
* @throws TagNotFoundException whenever a tag was not found |
|
256
|
|
|
*/ |
|
257
|
|
|
protected function processFilterRules($filterRules) { |
|
258
|
|
|
$resultFileIds = null; |
|
259
|
|
|
$systemTagIds = $filterRules['systemtag']; |
|
260
|
|
|
$favoriteFilter = $filterRules['favorite']; |
|
261
|
|
|
|
|
262
|
|
|
if ($favoriteFilter !== null) { |
|
263
|
|
|
$resultFileIds = $this->fileTagger->load('files')->getFavorites(); |
|
264
|
|
|
if (empty($resultFileIds)) { |
|
265
|
|
|
return []; |
|
266
|
|
|
} |
|
267
|
|
|
} |
|
268
|
|
|
|
|
269
|
|
|
if (!empty($systemTagIds)) { |
|
270
|
|
|
$fileIds = $this->getSystemTagFileIds($systemTagIds); |
|
271
|
|
|
if (empty($resultFileIds)) { |
|
272
|
|
|
$resultFileIds = $fileIds; |
|
273
|
|
|
} else { |
|
274
|
|
|
$resultFileIds = \array_intersect($fileIds, $resultFileIds); |
|
275
|
|
|
} |
|
276
|
|
|
} |
|
277
|
|
|
|
|
278
|
|
|
return $resultFileIds; |
|
279
|
|
|
} |
|
280
|
|
|
|
|
281
|
|
|
private function getSystemTagFileIds($systemTagIds) { |
|
282
|
|
|
$resultFileIds = null; |
|
283
|
|
|
|
|
284
|
|
|
// check user permissions, if applicable |
|
285
|
|
|
if (!$this->isAdmin()) { |
|
286
|
|
|
// check visibility/permission |
|
287
|
|
|
$tags = $this->tagManager->getTagsByIds($systemTagIds); |
|
288
|
|
|
$unknownTagIds = []; |
|
289
|
|
|
foreach ($tags as $tag) { |
|
290
|
|
|
if (!$tag->isUserVisible()) { |
|
291
|
|
|
$unknownTagIds[] = $tag->getId(); |
|
292
|
|
|
} |
|
293
|
|
|
} |
|
294
|
|
|
|
|
295
|
|
|
if (!empty($unknownTagIds)) { |
|
296
|
|
|
throw new TagNotFoundException('Tag with ids ' . \implode(', ', $unknownTagIds) . ' not found'); |
|
297
|
|
|
} |
|
298
|
|
|
} |
|
299
|
|
|
|
|
300
|
|
|
// fetch all file ids and intersect them |
|
301
|
|
|
foreach ($systemTagIds as $systemTagId) { |
|
302
|
|
|
$fileIds = $this->tagMapper->getObjectIdsForTags($systemTagId, 'files'); |
|
303
|
|
|
|
|
304
|
|
|
if (empty($fileIds)) { |
|
305
|
|
|
// This tag has no files, nothing can ever show up |
|
306
|
|
|
return []; |
|
307
|
|
|
} |
|
308
|
|
|
|
|
309
|
|
|
// first run ? |
|
310
|
|
|
if ($resultFileIds === null) { |
|
311
|
|
|
$resultFileIds = $fileIds; |
|
312
|
|
|
} else { |
|
313
|
|
|
$resultFileIds = \array_intersect($resultFileIds, $fileIds); |
|
314
|
|
|
} |
|
315
|
|
|
|
|
316
|
|
|
if (empty($resultFileIds)) { |
|
317
|
|
|
// Empty intersection, nothing can show up anymore |
|
318
|
|
|
return []; |
|
319
|
|
|
} |
|
320
|
|
|
} |
|
321
|
|
|
return $resultFileIds; |
|
322
|
|
|
} |
|
323
|
|
|
|
|
324
|
|
|
/** |
|
325
|
|
|
* Prepare propfind response for the given nodes |
|
326
|
|
|
* |
|
327
|
|
|
* @param string $filesUri $filesUri URI leading to root of the files URI, |
|
328
|
|
|
* with a leading slash but no trailing slash |
|
329
|
|
|
* @param string[] $requestedProps requested properties |
|
330
|
|
|
* @param Node[] nodes nodes for which to fetch and prepare responses |
|
331
|
|
|
* @return Response[] |
|
332
|
|
|
*/ |
|
333
|
|
|
public function prepareResponses($filesUri, $requestedProps, $nodes) { |
|
334
|
|
|
$results = []; |
|
335
|
|
|
foreach ($nodes as $node) { |
|
336
|
|
|
$propFind = new PropFind($filesUri . $node->getPath(), $requestedProps); |
|
337
|
|
|
|
|
338
|
|
|
$this->server->getPropertiesByNode($propFind, $node); |
|
339
|
|
|
// copied from Sabre Server's getPropertiesForPath |
|
340
|
|
|
$result = $propFind->getResultForMultiStatus(); |
|
341
|
|
|
$result['href'] = $propFind->getPath(); |
|
342
|
|
|
|
|
343
|
|
|
$results[] = $result; |
|
344
|
|
|
} |
|
345
|
|
|
return $results; |
|
346
|
|
|
} |
|
347
|
|
|
|
|
348
|
|
|
/** |
|
349
|
|
|
* Find Sabre nodes by file ids |
|
350
|
|
|
* |
|
351
|
|
|
* @param Node $rootNode root node for search |
|
352
|
|
|
* @param array $fileIds file ids |
|
353
|
|
|
* @return Node[] array of Sabre nodes |
|
354
|
|
|
*/ |
|
355
|
|
|
public function findNodesByFileIds($rootNode, $fileIds) { |
|
356
|
|
|
$folder = $this->userFolder; |
|
357
|
|
|
if (\trim($rootNode->getPath(), '/') !== '') { |
|
358
|
|
|
$folder = $folder->get($rootNode->getPath()); |
|
359
|
|
|
} |
|
360
|
|
|
|
|
361
|
|
|
$results = []; |
|
362
|
|
|
foreach ($fileIds as $fileId) { |
|
363
|
|
|
$entry = $folder->getById($fileId); |
|
364
|
|
|
if ($entry) { |
|
365
|
|
|
$entry = \current($entry); |
|
366
|
|
|
$node = $this->makeSabreNode($entry); |
|
367
|
|
|
if ($node) { |
|
368
|
|
|
$results[] = $node; |
|
369
|
|
|
} |
|
370
|
|
|
} |
|
371
|
|
|
} |
|
372
|
|
|
|
|
373
|
|
|
return $results; |
|
374
|
|
|
} |
|
375
|
|
|
|
|
376
|
|
|
private function makeSabreNode(\OCP\Files\Node $filesNode) { |
|
377
|
|
|
if ($filesNode instanceof \OCP\Files\File) { |
|
378
|
|
|
return new File($this->fileView, $filesNode); |
|
379
|
|
|
} elseif ($filesNode instanceof \OCP\Files\Folder) { |
|
380
|
|
|
return new Directory($this->fileView, $filesNode); |
|
381
|
|
|
} |
|
382
|
|
|
throw new \Exception('Unrecognized Files API node returned, aborting'); |
|
383
|
|
|
} |
|
384
|
|
|
|
|
385
|
|
|
/** |
|
386
|
|
|
* Returns whether the currently logged in user is an administrator |
|
387
|
|
|
*/ |
|
388
|
|
View Code Duplication |
private function isAdmin() { |
|
389
|
|
|
$user = $this->userSession->getUser(); |
|
390
|
|
|
if ($user !== null) { |
|
391
|
|
|
return $this->groupManager->isAdmin($user->getUID()); |
|
392
|
|
|
} |
|
393
|
|
|
return false; |
|
394
|
|
|
} |
|
395
|
|
|
} |
|
396
|
|
|
|
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignorePhpDoc annotation to the duplicate definition and it will be ignored.