Completed
Push — master ( 26c516...ac70e1 )
by Blizzz
20:04 queued 16s
created
apps/dav/lib/Connector/Sabre/TagsPlugin.php 2 patches
Indentation   +228 added lines, -228 removed lines patch added patch discarded remove patch
@@ -36,256 +36,256 @@
 block discarded – undo
36 36
 
37 37
 class TagsPlugin extends \Sabre\DAV\ServerPlugin {
38 38
 
39
-	// namespace
40
-	public const NS_OWNCLOUD = 'http://owncloud.org/ns';
41
-	public const TAGS_PROPERTYNAME = '{http://owncloud.org/ns}tags';
42
-	public const FAVORITE_PROPERTYNAME = '{http://owncloud.org/ns}favorite';
43
-	public const TAG_FAVORITE = '_$!<Favorite>!$_';
39
+    // namespace
40
+    public const NS_OWNCLOUD = 'http://owncloud.org/ns';
41
+    public const TAGS_PROPERTYNAME = '{http://owncloud.org/ns}tags';
42
+    public const FAVORITE_PROPERTYNAME = '{http://owncloud.org/ns}favorite';
43
+    public const TAG_FAVORITE = '_$!<Favorite>!$_';
44 44
 
45
-	/**
46
-	 * Reference to main server object
47
-	 *
48
-	 * @var \Sabre\DAV\Server
49
-	 */
50
-	private $server;
45
+    /**
46
+     * Reference to main server object
47
+     *
48
+     * @var \Sabre\DAV\Server
49
+     */
50
+    private $server;
51 51
 
52
-	/**
53
-	 * @var ITags
54
-	 */
55
-	private $tagger;
52
+    /**
53
+     * @var ITags
54
+     */
55
+    private $tagger;
56 56
 
57
-	/**
58
-	 * Array of file id to tags array
59
-	 * The null value means the cache wasn't initialized.
60
-	 *
61
-	 * @var array
62
-	 */
63
-	private $cachedTags;
57
+    /**
58
+     * Array of file id to tags array
59
+     * The null value means the cache wasn't initialized.
60
+     *
61
+     * @var array
62
+     */
63
+    private $cachedTags;
64 64
 
65
-	/**
66
-	 * @param \Sabre\DAV\Tree $tree tree
67
-	 * @param ITagManager $tagManager tag manager
68
-	 */
69
-	public function __construct(
70
-		private \Sabre\DAV\Tree $tree,
71
-		private ITagManager $tagManager,
72
-		private IEventDispatcher $eventDispatcher,
73
-		private IUserSession $userSession,
74
-	) {
75
-		$this->tagger = null;
76
-		$this->cachedTags = [];
77
-	}
65
+    /**
66
+     * @param \Sabre\DAV\Tree $tree tree
67
+     * @param ITagManager $tagManager tag manager
68
+     */
69
+    public function __construct(
70
+        private \Sabre\DAV\Tree $tree,
71
+        private ITagManager $tagManager,
72
+        private IEventDispatcher $eventDispatcher,
73
+        private IUserSession $userSession,
74
+    ) {
75
+        $this->tagger = null;
76
+        $this->cachedTags = [];
77
+    }
78 78
 
79
-	/**
80
-	 * This initializes the plugin.
81
-	 *
82
-	 * This function is called by \Sabre\DAV\Server, after
83
-	 * addPlugin is called.
84
-	 *
85
-	 * This method should set up the required event subscriptions.
86
-	 *
87
-	 * @param \Sabre\DAV\Server $server
88
-	 * @return void
89
-	 */
90
-	public function initialize(\Sabre\DAV\Server $server) {
91
-		$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
92
-		$server->xml->elementMap[self::TAGS_PROPERTYNAME] = TagList::class;
79
+    /**
80
+     * This initializes the plugin.
81
+     *
82
+     * This function is called by \Sabre\DAV\Server, after
83
+     * addPlugin is called.
84
+     *
85
+     * This method should set up the required event subscriptions.
86
+     *
87
+     * @param \Sabre\DAV\Server $server
88
+     * @return void
89
+     */
90
+    public function initialize(\Sabre\DAV\Server $server) {
91
+        $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
92
+        $server->xml->elementMap[self::TAGS_PROPERTYNAME] = TagList::class;
93 93
 
94
-		$this->server = $server;
95
-		$this->server->on('propFind', [$this, 'handleGetProperties']);
96
-		$this->server->on('propPatch', [$this, 'handleUpdateProperties']);
97
-		$this->server->on('preloadProperties', [$this, 'handlePreloadProperties']);
98
-	}
94
+        $this->server = $server;
95
+        $this->server->on('propFind', [$this, 'handleGetProperties']);
96
+        $this->server->on('propPatch', [$this, 'handleUpdateProperties']);
97
+        $this->server->on('preloadProperties', [$this, 'handlePreloadProperties']);
98
+    }
99 99
 
100
-	/**
101
-	 * Returns the tagger
102
-	 *
103
-	 * @return ITags tagger
104
-	 */
105
-	private function getTagger() {
106
-		if (!$this->tagger) {
107
-			$this->tagger = $this->tagManager->load('files');
108
-		}
109
-		return $this->tagger;
110
-	}
100
+    /**
101
+     * Returns the tagger
102
+     *
103
+     * @return ITags tagger
104
+     */
105
+    private function getTagger() {
106
+        if (!$this->tagger) {
107
+            $this->tagger = $this->tagManager->load('files');
108
+        }
109
+        return $this->tagger;
110
+    }
111 111
 
112
-	/**
113
-	 * Returns tags and favorites.
114
-	 *
115
-	 * @param integer $fileId file id
116
-	 * @return array list($tags, $favorite) with $tags as tag array
117
-	 *               and $favorite is a boolean whether the file was favorited
118
-	 */
119
-	private function getTagsAndFav($fileId) {
120
-		$isFav = false;
121
-		$tags = $this->getTags($fileId);
122
-		if ($tags) {
123
-			$favPos = array_search(self::TAG_FAVORITE, $tags);
124
-			if ($favPos !== false) {
125
-				$isFav = true;
126
-				unset($tags[$favPos]);
127
-			}
128
-		}
129
-		return [$tags, $isFav];
130
-	}
112
+    /**
113
+     * Returns tags and favorites.
114
+     *
115
+     * @param integer $fileId file id
116
+     * @return array list($tags, $favorite) with $tags as tag array
117
+     *               and $favorite is a boolean whether the file was favorited
118
+     */
119
+    private function getTagsAndFav($fileId) {
120
+        $isFav = false;
121
+        $tags = $this->getTags($fileId);
122
+        if ($tags) {
123
+            $favPos = array_search(self::TAG_FAVORITE, $tags);
124
+            if ($favPos !== false) {
125
+                $isFav = true;
126
+                unset($tags[$favPos]);
127
+            }
128
+        }
129
+        return [$tags, $isFav];
130
+    }
131 131
 
132
-	/**
133
-	 * Returns tags for the given file id
134
-	 *
135
-	 * @param integer $fileId file id
136
-	 * @return array list of tags for that file
137
-	 */
138
-	private function getTags($fileId) {
139
-		if (isset($this->cachedTags[$fileId])) {
140
-			return $this->cachedTags[$fileId];
141
-		} else {
142
-			$tags = $this->getTagger()->getTagsForObjects([$fileId]);
143
-			if ($tags !== false) {
144
-				if (empty($tags)) {
145
-					return [];
146
-				}
147
-				return current($tags);
148
-			}
149
-		}
150
-		return null;
151
-	}
132
+    /**
133
+     * Returns tags for the given file id
134
+     *
135
+     * @param integer $fileId file id
136
+     * @return array list of tags for that file
137
+     */
138
+    private function getTags($fileId) {
139
+        if (isset($this->cachedTags[$fileId])) {
140
+            return $this->cachedTags[$fileId];
141
+        } else {
142
+            $tags = $this->getTagger()->getTagsForObjects([$fileId]);
143
+            if ($tags !== false) {
144
+                if (empty($tags)) {
145
+                    return [];
146
+                }
147
+                return current($tags);
148
+            }
149
+        }
150
+        return null;
151
+    }
152 152
 
153
-	/**
154
-	 * Prefetches tags for a list of file IDs and caches the results
155
-	 *
156
-	 * @param array $fileIds List of file IDs to prefetch tags for
157
-	 * @return void
158
-	 */
159
-	private function prefetchTagsForFileIds(array $fileIds) {
160
-		$tags = $this->getTagger()->getTagsForObjects($fileIds);
161
-		if ($tags === false) {
162
-			// the tags API returns false on error...
163
-			$tags = [];
164
-		}
153
+    /**
154
+     * Prefetches tags for a list of file IDs and caches the results
155
+     *
156
+     * @param array $fileIds List of file IDs to prefetch tags for
157
+     * @return void
158
+     */
159
+    private function prefetchTagsForFileIds(array $fileIds) {
160
+        $tags = $this->getTagger()->getTagsForObjects($fileIds);
161
+        if ($tags === false) {
162
+            // the tags API returns false on error...
163
+            $tags = [];
164
+        }
165 165
 
166
-		foreach ($fileIds as $fileId) {
167
-			$this->cachedTags[$fileId] = $tags[$fileId] ?? [];
168
-		}
169
-	}
166
+        foreach ($fileIds as $fileId) {
167
+            $this->cachedTags[$fileId] = $tags[$fileId] ?? [];
168
+        }
169
+    }
170 170
 
171
-	/**
172
-	 * Updates the tags of the given file id
173
-	 *
174
-	 * @param int $fileId
175
-	 * @param array $tags array of tag strings
176
-	 */
177
-	private function updateTags($fileId, $tags) {
178
-		$tagger = $this->getTagger();
179
-		$currentTags = $this->getTags($fileId);
171
+    /**
172
+     * Updates the tags of the given file id
173
+     *
174
+     * @param int $fileId
175
+     * @param array $tags array of tag strings
176
+     */
177
+    private function updateTags($fileId, $tags) {
178
+        $tagger = $this->getTagger();
179
+        $currentTags = $this->getTags($fileId);
180 180
 
181
-		$newTags = array_diff($tags, $currentTags);
182
-		foreach ($newTags as $tag) {
183
-			if ($tag === self::TAG_FAVORITE) {
184
-				continue;
185
-			}
186
-			$tagger->tagAs($fileId, $tag);
187
-		}
188
-		$deletedTags = array_diff($currentTags, $tags);
189
-		foreach ($deletedTags as $tag) {
190
-			if ($tag === self::TAG_FAVORITE) {
191
-				continue;
192
-			}
193
-			$tagger->unTag($fileId, $tag);
194
-		}
195
-	}
181
+        $newTags = array_diff($tags, $currentTags);
182
+        foreach ($newTags as $tag) {
183
+            if ($tag === self::TAG_FAVORITE) {
184
+                continue;
185
+            }
186
+            $tagger->tagAs($fileId, $tag);
187
+        }
188
+        $deletedTags = array_diff($currentTags, $tags);
189
+        foreach ($deletedTags as $tag) {
190
+            if ($tag === self::TAG_FAVORITE) {
191
+                continue;
192
+            }
193
+            $tagger->unTag($fileId, $tag);
194
+        }
195
+    }
196 196
 
197
-	/**
198
-	 * Adds tags and favorites properties to the response,
199
-	 * if requested.
200
-	 *
201
-	 * @param PropFind $propFind
202
-	 * @param \Sabre\DAV\INode $node
203
-	 * @return void
204
-	 */
205
-	public function handleGetProperties(
206
-		PropFind $propFind,
207
-		\Sabre\DAV\INode $node,
208
-	) {
209
-		if (!($node instanceof Node)) {
210
-			return;
211
-		}
197
+    /**
198
+     * Adds tags and favorites properties to the response,
199
+     * if requested.
200
+     *
201
+     * @param PropFind $propFind
202
+     * @param \Sabre\DAV\INode $node
203
+     * @return void
204
+     */
205
+    public function handleGetProperties(
206
+        PropFind $propFind,
207
+        \Sabre\DAV\INode $node,
208
+    ) {
209
+        if (!($node instanceof Node)) {
210
+            return;
211
+        }
212 212
 
213
-		// need prefetch ?
214
-		if ($node instanceof Directory
215
-			&& $propFind->getDepth() !== 0
216
-			&& (!is_null($propFind->getStatus(self::TAGS_PROPERTYNAME))
217
-			|| !is_null($propFind->getStatus(self::FAVORITE_PROPERTYNAME))
218
-			)) {
219
-			// note: pre-fetching only supported for depth <= 1
220
-			$folderContent = $node->getChildren();
221
-			$fileIds = [(int)$node->getId()];
222
-			foreach ($folderContent as $info) {
223
-				$fileIds[] = (int)$info->getId();
224
-			}
225
-			$this->prefetchTagsForFileIds($fileIds);
226
-		}
213
+        // need prefetch ?
214
+        if ($node instanceof Directory
215
+            && $propFind->getDepth() !== 0
216
+            && (!is_null($propFind->getStatus(self::TAGS_PROPERTYNAME))
217
+            || !is_null($propFind->getStatus(self::FAVORITE_PROPERTYNAME))
218
+            )) {
219
+            // note: pre-fetching only supported for depth <= 1
220
+            $folderContent = $node->getChildren();
221
+            $fileIds = [(int)$node->getId()];
222
+            foreach ($folderContent as $info) {
223
+                $fileIds[] = (int)$info->getId();
224
+            }
225
+            $this->prefetchTagsForFileIds($fileIds);
226
+        }
227 227
 
228
-		$isFav = null;
228
+        $isFav = null;
229 229
 
230
-		$propFind->handle(self::TAGS_PROPERTYNAME, function () use (&$isFav, $node) {
231
-			[$tags, $isFav] = $this->getTagsAndFav($node->getId());
232
-			return new TagList($tags);
233
-		});
230
+        $propFind->handle(self::TAGS_PROPERTYNAME, function () use (&$isFav, $node) {
231
+            [$tags, $isFav] = $this->getTagsAndFav($node->getId());
232
+            return new TagList($tags);
233
+        });
234 234
 
235
-		$propFind->handle(self::FAVORITE_PROPERTYNAME, function () use ($isFav, $node) {
236
-			if (is_null($isFav)) {
237
-				[, $isFav] = $this->getTagsAndFav($node->getId());
238
-			}
239
-			if ($isFav) {
240
-				return 1;
241
-			} else {
242
-				return 0;
243
-			}
244
-		});
245
-	}
235
+        $propFind->handle(self::FAVORITE_PROPERTYNAME, function () use ($isFav, $node) {
236
+            if (is_null($isFav)) {
237
+                [, $isFav] = $this->getTagsAndFav($node->getId());
238
+            }
239
+            if ($isFav) {
240
+                return 1;
241
+            } else {
242
+                return 0;
243
+            }
244
+        });
245
+    }
246 246
 
247
-	/**
248
-	 * Updates tags and favorites properties, if applicable.
249
-	 *
250
-	 * @param string $path
251
-	 * @param PropPatch $propPatch
252
-	 *
253
-	 * @return void
254
-	 */
255
-	public function handleUpdateProperties($path, PropPatch $propPatch) {
256
-		$node = $this->tree->getNodeForPath($path);
257
-		if (!($node instanceof Node)) {
258
-			return;
259
-		}
247
+    /**
248
+     * Updates tags and favorites properties, if applicable.
249
+     *
250
+     * @param string $path
251
+     * @param PropPatch $propPatch
252
+     *
253
+     * @return void
254
+     */
255
+    public function handleUpdateProperties($path, PropPatch $propPatch) {
256
+        $node = $this->tree->getNodeForPath($path);
257
+        if (!($node instanceof Node)) {
258
+            return;
259
+        }
260 260
 
261
-		$propPatch->handle(self::TAGS_PROPERTYNAME, function ($tagList) use ($node) {
262
-			$this->updateTags($node->getId(), $tagList->getTags());
263
-			return true;
264
-		});
261
+        $propPatch->handle(self::TAGS_PROPERTYNAME, function ($tagList) use ($node) {
262
+            $this->updateTags($node->getId(), $tagList->getTags());
263
+            return true;
264
+        });
265 265
 
266
-		$propPatch->handle(self::FAVORITE_PROPERTYNAME, function ($favState) use ($node, $path) {
267
-			if ((int)$favState === 1 || $favState === 'true') {
268
-				$this->getTagger()->tagAs($node->getId(), self::TAG_FAVORITE);
269
-			} else {
270
-				$this->getTagger()->unTag($node->getId(), self::TAG_FAVORITE);
271
-			}
266
+        $propPatch->handle(self::FAVORITE_PROPERTYNAME, function ($favState) use ($node, $path) {
267
+            if ((int)$favState === 1 || $favState === 'true') {
268
+                $this->getTagger()->tagAs($node->getId(), self::TAG_FAVORITE);
269
+            } else {
270
+                $this->getTagger()->unTag($node->getId(), self::TAG_FAVORITE);
271
+            }
272 272
 
273
-			if (is_null($favState)) {
274
-				// confirm deletion
275
-				return 204;
276
-			}
273
+            if (is_null($favState)) {
274
+                // confirm deletion
275
+                return 204;
276
+            }
277 277
 
278
-			return 200;
279
-		});
280
-	}
278
+            return 200;
279
+        });
280
+    }
281 281
 
282
-	public function handlePreloadProperties(array $nodes, array $requestProperties): void {
283
-		if (
284
-			!in_array(self::FAVORITE_PROPERTYNAME, $requestProperties, true) &&
285
-			!in_array(self::TAGS_PROPERTYNAME, $requestProperties, true)
286
-		) {
287
-			return;
288
-		}
289
-		$this->prefetchTagsForFileIds(array_map(fn ($node) => $node->getId(), $nodes));
290
-	}
282
+    public function handlePreloadProperties(array $nodes, array $requestProperties): void {
283
+        if (
284
+            !in_array(self::FAVORITE_PROPERTYNAME, $requestProperties, true) &&
285
+            !in_array(self::TAGS_PROPERTYNAME, $requestProperties, true)
286
+        ) {
287
+            return;
288
+        }
289
+        $this->prefetchTagsForFileIds(array_map(fn ($node) => $node->getId(), $nodes));
290
+    }
291 291
 }
Please login to merge, or discard this patch.
Spacing   +7 added lines, -7 removed lines patch added patch discarded remove patch
@@ -218,21 +218,21 @@  discard block
 block discarded – undo
218 218
 			)) {
219 219
 			// note: pre-fetching only supported for depth <= 1
220 220
 			$folderContent = $node->getChildren();
221
-			$fileIds = [(int)$node->getId()];
221
+			$fileIds = [(int) $node->getId()];
222 222
 			foreach ($folderContent as $info) {
223
-				$fileIds[] = (int)$info->getId();
223
+				$fileIds[] = (int) $info->getId();
224 224
 			}
225 225
 			$this->prefetchTagsForFileIds($fileIds);
226 226
 		}
227 227
 
228 228
 		$isFav = null;
229 229
 
230
-		$propFind->handle(self::TAGS_PROPERTYNAME, function () use (&$isFav, $node) {
230
+		$propFind->handle(self::TAGS_PROPERTYNAME, function() use (&$isFav, $node) {
231 231
 			[$tags, $isFav] = $this->getTagsAndFav($node->getId());
232 232
 			return new TagList($tags);
233 233
 		});
234 234
 
235
-		$propFind->handle(self::FAVORITE_PROPERTYNAME, function () use ($isFav, $node) {
235
+		$propFind->handle(self::FAVORITE_PROPERTYNAME, function() use ($isFav, $node) {
236 236
 			if (is_null($isFav)) {
237 237
 				[, $isFav] = $this->getTagsAndFav($node->getId());
238 238
 			}
@@ -258,13 +258,13 @@  discard block
 block discarded – undo
258 258
 			return;
259 259
 		}
260 260
 
261
-		$propPatch->handle(self::TAGS_PROPERTYNAME, function ($tagList) use ($node) {
261
+		$propPatch->handle(self::TAGS_PROPERTYNAME, function($tagList) use ($node) {
262 262
 			$this->updateTags($node->getId(), $tagList->getTags());
263 263
 			return true;
264 264
 		});
265 265
 
266
-		$propPatch->handle(self::FAVORITE_PROPERTYNAME, function ($favState) use ($node, $path) {
267
-			if ((int)$favState === 1 || $favState === 'true') {
266
+		$propPatch->handle(self::FAVORITE_PROPERTYNAME, function($favState) use ($node, $path) {
267
+			if ((int) $favState === 1 || $favState === 'true') {
268 268
 				$this->getTagger()->tagAs($node->getId(), self::TAG_FAVORITE);
269 269
 			} else {
270 270
 				$this->getTagger()->unTag($node->getId(), self::TAG_FAVORITE);
Please login to merge, or discard this patch.
apps/dav/lib/Server.php 1 patch
Indentation   +326 added lines, -326 removed lines patch added patch discarded remove patch
@@ -100,331 +100,331 @@
 block discarded – undo
100 100
 use SearchDAV\DAV\SearchPlugin;
101 101
 
102 102
 class Server {
103
-	public Connector\Sabre\Server $server;
104
-	private IProfiler $profiler;
105
-
106
-	public function __construct(
107
-		private IRequest $request,
108
-		private string $baseUri,
109
-	) {
110
-		$this->profiler = \OCP\Server::get(IProfiler::class);
111
-		if ($this->profiler->isEnabled()) {
112
-			/** @var IEventLogger $eventLogger */
113
-			$eventLogger = \OCP\Server::get(IEventLogger::class);
114
-			$eventLogger->start('runtime', 'DAV Runtime');
115
-		}
116
-
117
-		$logger = \OCP\Server::get(LoggerInterface::class);
118
-		$eventDispatcher = \OCP\Server::get(IEventDispatcher::class);
119
-
120
-		$root = new RootCollection();
121
-		$this->server = new \OCA\DAV\Connector\Sabre\Server(new CachingTree($root));
122
-
123
-		// Add maintenance plugin
124
-		$this->server->addPlugin(new MaintenancePlugin(\OCP\Server::get(IConfig::class), \OC::$server->getL10N('dav')));
125
-
126
-		$this->server->addPlugin(new AppleQuirksPlugin());
127
-
128
-		// Backends
129
-		$authBackend = new Auth(
130
-			\OCP\Server::get(ISession::class),
131
-			\OCP\Server::get(IUserSession::class),
132
-			\OCP\Server::get(IRequest::class),
133
-			\OCP\Server::get(\OC\Authentication\TwoFactorAuth\Manager::class),
134
-			\OCP\Server::get(IThrottler::class)
135
-		);
136
-
137
-		// Set URL explicitly due to reverse-proxy situations
138
-		$this->server->httpRequest->setUrl($this->request->getRequestUri());
139
-		$this->server->setBaseUri($this->baseUri);
140
-
141
-		$this->server->addPlugin(new ProfilerPlugin($this->request));
142
-		$this->server->addPlugin(new BlockLegacyClientPlugin(
143
-			\OCP\Server::get(IConfig::class),
144
-			\OCP\Server::get(ThemingDefaults::class),
145
-		));
146
-		$this->server->addPlugin(new AnonymousOptionsPlugin());
147
-		$authPlugin = new Plugin();
148
-		$authPlugin->addBackend(new PublicAuth());
149
-		$this->server->addPlugin($authPlugin);
150
-
151
-		// allow setup of additional auth backends
152
-		$event = new SabrePluginEvent($this->server);
153
-		$eventDispatcher->dispatch('OCA\DAV\Connector\Sabre::authInit', $event);
154
-
155
-		$newAuthEvent = new SabrePluginAuthInitEvent($this->server);
156
-		$eventDispatcher->dispatchTyped($newAuthEvent);
157
-
158
-		$bearerAuthBackend = new BearerAuth(
159
-			\OCP\Server::get(IUserSession::class),
160
-			\OCP\Server::get(ISession::class),
161
-			\OCP\Server::get(IRequest::class),
162
-			\OCP\Server::get(IConfig::class),
163
-		);
164
-		$authPlugin->addBackend($bearerAuthBackend);
165
-		// because we are throwing exceptions this plugin has to be the last one
166
-		$authPlugin->addBackend($authBackend);
167
-
168
-		// debugging
169
-		if (\OCP\Server::get(IConfig::class)->getSystemValue('debug', false)) {
170
-			$this->server->addPlugin(new \Sabre\DAV\Browser\Plugin());
171
-		} else {
172
-			$this->server->addPlugin(new DummyGetResponsePlugin());
173
-		}
174
-
175
-		$this->server->addPlugin(new ExceptionLoggerPlugin('webdav', $logger));
176
-		$this->server->addPlugin(new LockPlugin());
177
-		$this->server->addPlugin(new \Sabre\DAV\Sync\Plugin());
178
-
179
-		// acl
180
-		$acl = new DavAclPlugin();
181
-		$acl->principalCollectionSet = [
182
-			'principals/users',
183
-			'principals/groups',
184
-			'principals/calendar-resources',
185
-			'principals/calendar-rooms',
186
-		];
187
-		$this->server->addPlugin($acl);
188
-
189
-		// calendar plugins
190
-		if ($this->requestIsForSubtree(['calendars', 'public-calendars', 'system-calendars', 'principals'])) {
191
-			$this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OCP\Server::get(IRequest::class), \OCP\Server::get(IConfig::class)));
192
-			$this->server->addPlugin(new \OCA\DAV\CalDAV\Plugin());
193
-			$this->server->addPlugin(new ICSExportPlugin(\OCP\Server::get(IConfig::class), $logger));
194
-			$this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OCP\Server::get(IConfig::class), \OCP\Server::get(LoggerInterface::class), \OCP\Server::get(DefaultCalendarValidator::class)));
195
-
196
-			$this->server->addPlugin(\OCP\Server::get(\OCA\DAV\CalDAV\Trashbin\Plugin::class));
197
-			$this->server->addPlugin(new \OCA\DAV\CalDAV\WebcalCaching\Plugin($this->request));
198
-			if (\OCP\Server::get(IConfig::class)->getAppValue('dav', 'allow_calendar_link_subscriptions', 'yes') === 'yes') {
199
-				$this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin());
200
-			}
201
-
202
-			$this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin());
203
-			$this->server->addPlugin(new PublishPlugin(
204
-				\OCP\Server::get(IConfig::class),
205
-				\OCP\Server::get(IURLGenerator::class)
206
-			));
207
-
208
-			$this->server->addPlugin(\OCP\Server::get(RateLimitingPlugin::class));
209
-			$this->server->addPlugin(\OCP\Server::get(CalDavValidatePlugin::class));
210
-		}
211
-
212
-		// addressbook plugins
213
-		if ($this->requestIsForSubtree(['addressbooks', 'principals'])) {
214
-			$this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OCP\Server::get(IRequest::class), \OCP\Server::get(IConfig::class)));
215
-			$this->server->addPlugin(new \OCA\DAV\CardDAV\Plugin());
216
-			$this->server->addPlugin(new VCFExportPlugin());
217
-			$this->server->addPlugin(new MultiGetExportPlugin());
218
-			$this->server->addPlugin(new HasPhotoPlugin());
219
-			$this->server->addPlugin(new ImageExportPlugin(\OCP\Server::get(PhotoCache::class)));
220
-
221
-			$this->server->addPlugin(\OCP\Server::get(CardDavRateLimitingPlugin::class));
222
-			$this->server->addPlugin(\OCP\Server::get(CardDavValidatePlugin::class));
223
-		}
224
-
225
-		// system tags plugins
226
-		$this->server->addPlugin(\OCP\Server::get(SystemTagPlugin::class));
227
-
228
-		// comments plugin
229
-		$this->server->addPlugin(new CommentsPlugin(
230
-			\OCP\Server::get(ICommentsManager::class),
231
-			\OCP\Server::get(IUserSession::class)
232
-		));
233
-
234
-		$this->server->addPlugin(new CopyEtagHeaderPlugin());
235
-		$this->server->addPlugin(new RequestIdHeaderPlugin(\OCP\Server::get(IRequest::class)));
236
-		$this->server->addPlugin(new UploadAutoMkcolPlugin());
237
-		$this->server->addPlugin(new ChunkingV2Plugin(\OCP\Server::get(ICacheFactory::class)));
238
-		$this->server->addPlugin(new ChunkingPlugin());
239
-		$this->server->addPlugin(new ZipFolderPlugin(
240
-			$this->server->tree,
241
-			$logger,
242
-			$eventDispatcher,
243
-		));
244
-		$this->server->addPlugin(\OCP\Server::get(PaginatePlugin::class));
245
-
246
-		// allow setup of additional plugins
247
-		$eventDispatcher->dispatch('OCA\DAV\Connector\Sabre::addPlugin', $event);
248
-		$typedEvent = new SabrePluginAddEvent($this->server);
249
-		$eventDispatcher->dispatchTyped($typedEvent);
250
-
251
-		// Some WebDAV clients do require Class 2 WebDAV support (locking), since
252
-		// we do not provide locking we emulate it using a fake locking plugin.
253
-		if ($this->request->isUserAgent([
254
-			'/WebDAVFS/',
255
-			'/OneNote/',
256
-			'/^Microsoft-WebDAV/',// Microsoft-WebDAV-MiniRedir/6.1.7601
257
-		])) {
258
-			$this->server->addPlugin(new FakeLockerPlugin());
259
-		}
260
-
261
-		if (BrowserErrorPagePlugin::isBrowserRequest($request)) {
262
-			$this->server->addPlugin(new BrowserErrorPagePlugin());
263
-		}
264
-
265
-		$lazySearchBackend = new LazySearchBackend();
266
-		$this->server->addPlugin(new SearchPlugin($lazySearchBackend));
267
-
268
-		// wait with registering these until auth is handled and the filesystem is setup
269
-		$this->server->on('beforeMethod:*', function () use ($root, $lazySearchBackend, $logger): void {
270
-			// Allow view-only plugin for webdav requests
271
-			$this->server->addPlugin(new ViewOnlyPlugin(
272
-				\OC::$server->getUserFolder(),
273
-			));
274
-
275
-			// custom properties plugin must be the last one
276
-			$userSession = \OCP\Server::get(IUserSession::class);
277
-			$user = $userSession->getUser();
278
-			if ($user !== null) {
279
-				$view = Filesystem::getView();
280
-				$config = \OCP\Server::get(IConfig::class);
281
-				$this->server->addPlugin(
282
-					new FilesPlugin(
283
-						$this->server->tree,
284
-						$config,
285
-						$this->request,
286
-						\OCP\Server::get(IPreview::class),
287
-						\OCP\Server::get(IUserSession::class),
288
-						\OCP\Server::get(IFilenameValidator::class),
289
-						\OCP\Server::get(IAccountManager::class),
290
-						false,
291
-						$config->getSystemValueBool('debug', false) === false,
292
-					)
293
-				);
294
-				$this->server->addPlugin(new ChecksumUpdatePlugin());
295
-
296
-				$this->server->addPlugin(
297
-					new \Sabre\DAV\PropertyStorage\Plugin(
298
-						new CustomPropertiesBackend(
299
-							$this->server,
300
-							$this->server->tree,
301
-							\OCP\Server::get(IDBConnection::class),
302
-							\OCP\Server::get(IUserSession::class)->getUser(),
303
-							\OCP\Server::get(DefaultCalendarValidator::class),
304
-						)
305
-					)
306
-				);
307
-				if ($view !== null) {
308
-					$this->server->addPlugin(
309
-						new QuotaPlugin($view));
310
-				}
311
-				$this->server->addPlugin(
312
-					new TagsPlugin(
313
-						$this->server->tree, \OCP\Server::get(ITagManager::class), \OCP\Server::get(IEventDispatcher::class), \OCP\Server::get(IUserSession::class)
314
-					)
315
-				);
316
-
317
-				// TODO: switch to LazyUserFolder
318
-				$userFolder = \OC::$server->getUserFolder();
319
-				$shareManager = \OCP\Server::get(\OCP\Share\IManager::class);
320
-				$this->server->addPlugin(new SharesPlugin(
321
-					$this->server->tree,
322
-					$userSession,
323
-					$userFolder,
324
-					$shareManager,
325
-				));
326
-				$this->server->addPlugin(new CommentPropertiesPlugin(
327
-					\OCP\Server::get(ICommentsManager::class),
328
-					$userSession
329
-				));
330
-				if (\OCP\Server::get(IConfig::class)->getAppValue('dav', 'sendInvitations', 'yes') === 'yes') {
331
-					$this->server->addPlugin(new IMipPlugin(
332
-						\OCP\Server::get(IAppConfig::class),
333
-						\OCP\Server::get(IMailer::class),
334
-						\OCP\Server::get(LoggerInterface::class),
335
-						\OCP\Server::get(ITimeFactory::class),
336
-						\OCP\Server::get(Defaults::class),
337
-						$userSession,
338
-						\OCP\Server::get(IMipService::class),
339
-						\OCP\Server::get(EventComparisonService::class),
340
-						\OCP\Server::get(\OCP\Mail\Provider\IManager::class)
341
-					));
342
-				}
343
-				$this->server->addPlugin(new \OCA\DAV\CalDAV\Search\SearchPlugin());
344
-				if ($view !== null) {
345
-					$this->server->addPlugin(new FilesReportPlugin(
346
-						$this->server->tree,
347
-						$view,
348
-						\OCP\Server::get(ISystemTagManager::class),
349
-						\OCP\Server::get(ISystemTagObjectMapper::class),
350
-						\OCP\Server::get(ITagManager::class),
351
-						$userSession,
352
-						\OCP\Server::get(IGroupManager::class),
353
-						$userFolder,
354
-						\OCP\Server::get(IAppManager::class)
355
-					));
356
-					$lazySearchBackend->setBackend(new FileSearchBackend(
357
-						$this->server,
358
-						$this->server->tree,
359
-						$user,
360
-						\OCP\Server::get(IRootFolder::class),
361
-						$shareManager,
362
-						$view,
363
-						\OCP\Server::get(IFilesMetadataManager::class)
364
-					));
365
-					$this->server->addPlugin(
366
-						new BulkUploadPlugin(
367
-							$userFolder,
368
-							$logger
369
-						)
370
-					);
371
-				}
372
-				$this->server->addPlugin(new EnablePlugin(
373
-					\OCP\Server::get(IConfig::class),
374
-					\OCP\Server::get(BirthdayService::class),
375
-					$user
376
-				));
377
-				$this->server->addPlugin(new AppleProvisioningPlugin(
378
-					\OCP\Server::get(IUserSession::class),
379
-					\OCP\Server::get(IURLGenerator::class),
380
-					\OCP\Server::get(ThemingDefaults::class),
381
-					\OCP\Server::get(IRequest::class),
382
-					\OC::$server->getL10N('dav'),
383
-					function () {
384
-						return UUIDUtil::getUUID();
385
-					}
386
-				));
387
-			}
388
-
389
-			// register plugins from apps
390
-			$pluginManager = new PluginManager(
391
-				\OC::$server,
392
-				\OCP\Server::get(IAppManager::class)
393
-			);
394
-			foreach ($pluginManager->getAppPlugins() as $appPlugin) {
395
-				$this->server->addPlugin($appPlugin);
396
-			}
397
-			foreach ($pluginManager->getAppCollections() as $appCollection) {
398
-				$root->addChild($appCollection);
399
-			}
400
-		});
401
-
402
-		$this->server->addPlugin(
403
-			new PropfindCompressionPlugin()
404
-		);
405
-	}
406
-
407
-	public function exec() {
408
-		/** @var IEventLogger $eventLogger */
409
-		$eventLogger = \OCP\Server::get(IEventLogger::class);
410
-		$eventLogger->start('dav_server_exec', '');
411
-		$this->server->start();
412
-		$eventLogger->end('dav_server_exec');
413
-		if ($this->profiler->isEnabled()) {
414
-			$eventLogger->end('runtime');
415
-			$profile = $this->profiler->collect(\OCP\Server::get(IRequest::class), new Response());
416
-			$this->profiler->saveProfile($profile);
417
-		}
418
-	}
419
-
420
-	private function requestIsForSubtree(array $subTrees): bool {
421
-		foreach ($subTrees as $subTree) {
422
-			$subTree = trim($subTree, ' /');
423
-			if (str_starts_with($this->server->getRequestUri(), $subTree . '/')) {
424
-				return true;
425
-			}
426
-		}
427
-		return false;
428
-	}
103
+    public Connector\Sabre\Server $server;
104
+    private IProfiler $profiler;
105
+
106
+    public function __construct(
107
+        private IRequest $request,
108
+        private string $baseUri,
109
+    ) {
110
+        $this->profiler = \OCP\Server::get(IProfiler::class);
111
+        if ($this->profiler->isEnabled()) {
112
+            /** @var IEventLogger $eventLogger */
113
+            $eventLogger = \OCP\Server::get(IEventLogger::class);
114
+            $eventLogger->start('runtime', 'DAV Runtime');
115
+        }
116
+
117
+        $logger = \OCP\Server::get(LoggerInterface::class);
118
+        $eventDispatcher = \OCP\Server::get(IEventDispatcher::class);
119
+
120
+        $root = new RootCollection();
121
+        $this->server = new \OCA\DAV\Connector\Sabre\Server(new CachingTree($root));
122
+
123
+        // Add maintenance plugin
124
+        $this->server->addPlugin(new MaintenancePlugin(\OCP\Server::get(IConfig::class), \OC::$server->getL10N('dav')));
125
+
126
+        $this->server->addPlugin(new AppleQuirksPlugin());
127
+
128
+        // Backends
129
+        $authBackend = new Auth(
130
+            \OCP\Server::get(ISession::class),
131
+            \OCP\Server::get(IUserSession::class),
132
+            \OCP\Server::get(IRequest::class),
133
+            \OCP\Server::get(\OC\Authentication\TwoFactorAuth\Manager::class),
134
+            \OCP\Server::get(IThrottler::class)
135
+        );
136
+
137
+        // Set URL explicitly due to reverse-proxy situations
138
+        $this->server->httpRequest->setUrl($this->request->getRequestUri());
139
+        $this->server->setBaseUri($this->baseUri);
140
+
141
+        $this->server->addPlugin(new ProfilerPlugin($this->request));
142
+        $this->server->addPlugin(new BlockLegacyClientPlugin(
143
+            \OCP\Server::get(IConfig::class),
144
+            \OCP\Server::get(ThemingDefaults::class),
145
+        ));
146
+        $this->server->addPlugin(new AnonymousOptionsPlugin());
147
+        $authPlugin = new Plugin();
148
+        $authPlugin->addBackend(new PublicAuth());
149
+        $this->server->addPlugin($authPlugin);
150
+
151
+        // allow setup of additional auth backends
152
+        $event = new SabrePluginEvent($this->server);
153
+        $eventDispatcher->dispatch('OCA\DAV\Connector\Sabre::authInit', $event);
154
+
155
+        $newAuthEvent = new SabrePluginAuthInitEvent($this->server);
156
+        $eventDispatcher->dispatchTyped($newAuthEvent);
157
+
158
+        $bearerAuthBackend = new BearerAuth(
159
+            \OCP\Server::get(IUserSession::class),
160
+            \OCP\Server::get(ISession::class),
161
+            \OCP\Server::get(IRequest::class),
162
+            \OCP\Server::get(IConfig::class),
163
+        );
164
+        $authPlugin->addBackend($bearerAuthBackend);
165
+        // because we are throwing exceptions this plugin has to be the last one
166
+        $authPlugin->addBackend($authBackend);
167
+
168
+        // debugging
169
+        if (\OCP\Server::get(IConfig::class)->getSystemValue('debug', false)) {
170
+            $this->server->addPlugin(new \Sabre\DAV\Browser\Plugin());
171
+        } else {
172
+            $this->server->addPlugin(new DummyGetResponsePlugin());
173
+        }
174
+
175
+        $this->server->addPlugin(new ExceptionLoggerPlugin('webdav', $logger));
176
+        $this->server->addPlugin(new LockPlugin());
177
+        $this->server->addPlugin(new \Sabre\DAV\Sync\Plugin());
178
+
179
+        // acl
180
+        $acl = new DavAclPlugin();
181
+        $acl->principalCollectionSet = [
182
+            'principals/users',
183
+            'principals/groups',
184
+            'principals/calendar-resources',
185
+            'principals/calendar-rooms',
186
+        ];
187
+        $this->server->addPlugin($acl);
188
+
189
+        // calendar plugins
190
+        if ($this->requestIsForSubtree(['calendars', 'public-calendars', 'system-calendars', 'principals'])) {
191
+            $this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OCP\Server::get(IRequest::class), \OCP\Server::get(IConfig::class)));
192
+            $this->server->addPlugin(new \OCA\DAV\CalDAV\Plugin());
193
+            $this->server->addPlugin(new ICSExportPlugin(\OCP\Server::get(IConfig::class), $logger));
194
+            $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OCP\Server::get(IConfig::class), \OCP\Server::get(LoggerInterface::class), \OCP\Server::get(DefaultCalendarValidator::class)));
195
+
196
+            $this->server->addPlugin(\OCP\Server::get(\OCA\DAV\CalDAV\Trashbin\Plugin::class));
197
+            $this->server->addPlugin(new \OCA\DAV\CalDAV\WebcalCaching\Plugin($this->request));
198
+            if (\OCP\Server::get(IConfig::class)->getAppValue('dav', 'allow_calendar_link_subscriptions', 'yes') === 'yes') {
199
+                $this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin());
200
+            }
201
+
202
+            $this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin());
203
+            $this->server->addPlugin(new PublishPlugin(
204
+                \OCP\Server::get(IConfig::class),
205
+                \OCP\Server::get(IURLGenerator::class)
206
+            ));
207
+
208
+            $this->server->addPlugin(\OCP\Server::get(RateLimitingPlugin::class));
209
+            $this->server->addPlugin(\OCP\Server::get(CalDavValidatePlugin::class));
210
+        }
211
+
212
+        // addressbook plugins
213
+        if ($this->requestIsForSubtree(['addressbooks', 'principals'])) {
214
+            $this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OCP\Server::get(IRequest::class), \OCP\Server::get(IConfig::class)));
215
+            $this->server->addPlugin(new \OCA\DAV\CardDAV\Plugin());
216
+            $this->server->addPlugin(new VCFExportPlugin());
217
+            $this->server->addPlugin(new MultiGetExportPlugin());
218
+            $this->server->addPlugin(new HasPhotoPlugin());
219
+            $this->server->addPlugin(new ImageExportPlugin(\OCP\Server::get(PhotoCache::class)));
220
+
221
+            $this->server->addPlugin(\OCP\Server::get(CardDavRateLimitingPlugin::class));
222
+            $this->server->addPlugin(\OCP\Server::get(CardDavValidatePlugin::class));
223
+        }
224
+
225
+        // system tags plugins
226
+        $this->server->addPlugin(\OCP\Server::get(SystemTagPlugin::class));
227
+
228
+        // comments plugin
229
+        $this->server->addPlugin(new CommentsPlugin(
230
+            \OCP\Server::get(ICommentsManager::class),
231
+            \OCP\Server::get(IUserSession::class)
232
+        ));
233
+
234
+        $this->server->addPlugin(new CopyEtagHeaderPlugin());
235
+        $this->server->addPlugin(new RequestIdHeaderPlugin(\OCP\Server::get(IRequest::class)));
236
+        $this->server->addPlugin(new UploadAutoMkcolPlugin());
237
+        $this->server->addPlugin(new ChunkingV2Plugin(\OCP\Server::get(ICacheFactory::class)));
238
+        $this->server->addPlugin(new ChunkingPlugin());
239
+        $this->server->addPlugin(new ZipFolderPlugin(
240
+            $this->server->tree,
241
+            $logger,
242
+            $eventDispatcher,
243
+        ));
244
+        $this->server->addPlugin(\OCP\Server::get(PaginatePlugin::class));
245
+
246
+        // allow setup of additional plugins
247
+        $eventDispatcher->dispatch('OCA\DAV\Connector\Sabre::addPlugin', $event);
248
+        $typedEvent = new SabrePluginAddEvent($this->server);
249
+        $eventDispatcher->dispatchTyped($typedEvent);
250
+
251
+        // Some WebDAV clients do require Class 2 WebDAV support (locking), since
252
+        // we do not provide locking we emulate it using a fake locking plugin.
253
+        if ($this->request->isUserAgent([
254
+            '/WebDAVFS/',
255
+            '/OneNote/',
256
+            '/^Microsoft-WebDAV/',// Microsoft-WebDAV-MiniRedir/6.1.7601
257
+        ])) {
258
+            $this->server->addPlugin(new FakeLockerPlugin());
259
+        }
260
+
261
+        if (BrowserErrorPagePlugin::isBrowserRequest($request)) {
262
+            $this->server->addPlugin(new BrowserErrorPagePlugin());
263
+        }
264
+
265
+        $lazySearchBackend = new LazySearchBackend();
266
+        $this->server->addPlugin(new SearchPlugin($lazySearchBackend));
267
+
268
+        // wait with registering these until auth is handled and the filesystem is setup
269
+        $this->server->on('beforeMethod:*', function () use ($root, $lazySearchBackend, $logger): void {
270
+            // Allow view-only plugin for webdav requests
271
+            $this->server->addPlugin(new ViewOnlyPlugin(
272
+                \OC::$server->getUserFolder(),
273
+            ));
274
+
275
+            // custom properties plugin must be the last one
276
+            $userSession = \OCP\Server::get(IUserSession::class);
277
+            $user = $userSession->getUser();
278
+            if ($user !== null) {
279
+                $view = Filesystem::getView();
280
+                $config = \OCP\Server::get(IConfig::class);
281
+                $this->server->addPlugin(
282
+                    new FilesPlugin(
283
+                        $this->server->tree,
284
+                        $config,
285
+                        $this->request,
286
+                        \OCP\Server::get(IPreview::class),
287
+                        \OCP\Server::get(IUserSession::class),
288
+                        \OCP\Server::get(IFilenameValidator::class),
289
+                        \OCP\Server::get(IAccountManager::class),
290
+                        false,
291
+                        $config->getSystemValueBool('debug', false) === false,
292
+                    )
293
+                );
294
+                $this->server->addPlugin(new ChecksumUpdatePlugin());
295
+
296
+                $this->server->addPlugin(
297
+                    new \Sabre\DAV\PropertyStorage\Plugin(
298
+                        new CustomPropertiesBackend(
299
+                            $this->server,
300
+                            $this->server->tree,
301
+                            \OCP\Server::get(IDBConnection::class),
302
+                            \OCP\Server::get(IUserSession::class)->getUser(),
303
+                            \OCP\Server::get(DefaultCalendarValidator::class),
304
+                        )
305
+                    )
306
+                );
307
+                if ($view !== null) {
308
+                    $this->server->addPlugin(
309
+                        new QuotaPlugin($view));
310
+                }
311
+                $this->server->addPlugin(
312
+                    new TagsPlugin(
313
+                        $this->server->tree, \OCP\Server::get(ITagManager::class), \OCP\Server::get(IEventDispatcher::class), \OCP\Server::get(IUserSession::class)
314
+                    )
315
+                );
316
+
317
+                // TODO: switch to LazyUserFolder
318
+                $userFolder = \OC::$server->getUserFolder();
319
+                $shareManager = \OCP\Server::get(\OCP\Share\IManager::class);
320
+                $this->server->addPlugin(new SharesPlugin(
321
+                    $this->server->tree,
322
+                    $userSession,
323
+                    $userFolder,
324
+                    $shareManager,
325
+                ));
326
+                $this->server->addPlugin(new CommentPropertiesPlugin(
327
+                    \OCP\Server::get(ICommentsManager::class),
328
+                    $userSession
329
+                ));
330
+                if (\OCP\Server::get(IConfig::class)->getAppValue('dav', 'sendInvitations', 'yes') === 'yes') {
331
+                    $this->server->addPlugin(new IMipPlugin(
332
+                        \OCP\Server::get(IAppConfig::class),
333
+                        \OCP\Server::get(IMailer::class),
334
+                        \OCP\Server::get(LoggerInterface::class),
335
+                        \OCP\Server::get(ITimeFactory::class),
336
+                        \OCP\Server::get(Defaults::class),
337
+                        $userSession,
338
+                        \OCP\Server::get(IMipService::class),
339
+                        \OCP\Server::get(EventComparisonService::class),
340
+                        \OCP\Server::get(\OCP\Mail\Provider\IManager::class)
341
+                    ));
342
+                }
343
+                $this->server->addPlugin(new \OCA\DAV\CalDAV\Search\SearchPlugin());
344
+                if ($view !== null) {
345
+                    $this->server->addPlugin(new FilesReportPlugin(
346
+                        $this->server->tree,
347
+                        $view,
348
+                        \OCP\Server::get(ISystemTagManager::class),
349
+                        \OCP\Server::get(ISystemTagObjectMapper::class),
350
+                        \OCP\Server::get(ITagManager::class),
351
+                        $userSession,
352
+                        \OCP\Server::get(IGroupManager::class),
353
+                        $userFolder,
354
+                        \OCP\Server::get(IAppManager::class)
355
+                    ));
356
+                    $lazySearchBackend->setBackend(new FileSearchBackend(
357
+                        $this->server,
358
+                        $this->server->tree,
359
+                        $user,
360
+                        \OCP\Server::get(IRootFolder::class),
361
+                        $shareManager,
362
+                        $view,
363
+                        \OCP\Server::get(IFilesMetadataManager::class)
364
+                    ));
365
+                    $this->server->addPlugin(
366
+                        new BulkUploadPlugin(
367
+                            $userFolder,
368
+                            $logger
369
+                        )
370
+                    );
371
+                }
372
+                $this->server->addPlugin(new EnablePlugin(
373
+                    \OCP\Server::get(IConfig::class),
374
+                    \OCP\Server::get(BirthdayService::class),
375
+                    $user
376
+                ));
377
+                $this->server->addPlugin(new AppleProvisioningPlugin(
378
+                    \OCP\Server::get(IUserSession::class),
379
+                    \OCP\Server::get(IURLGenerator::class),
380
+                    \OCP\Server::get(ThemingDefaults::class),
381
+                    \OCP\Server::get(IRequest::class),
382
+                    \OC::$server->getL10N('dav'),
383
+                    function () {
384
+                        return UUIDUtil::getUUID();
385
+                    }
386
+                ));
387
+            }
388
+
389
+            // register plugins from apps
390
+            $pluginManager = new PluginManager(
391
+                \OC::$server,
392
+                \OCP\Server::get(IAppManager::class)
393
+            );
394
+            foreach ($pluginManager->getAppPlugins() as $appPlugin) {
395
+                $this->server->addPlugin($appPlugin);
396
+            }
397
+            foreach ($pluginManager->getAppCollections() as $appCollection) {
398
+                $root->addChild($appCollection);
399
+            }
400
+        });
401
+
402
+        $this->server->addPlugin(
403
+            new PropfindCompressionPlugin()
404
+        );
405
+    }
406
+
407
+    public function exec() {
408
+        /** @var IEventLogger $eventLogger */
409
+        $eventLogger = \OCP\Server::get(IEventLogger::class);
410
+        $eventLogger->start('dav_server_exec', '');
411
+        $this->server->start();
412
+        $eventLogger->end('dav_server_exec');
413
+        if ($this->profiler->isEnabled()) {
414
+            $eventLogger->end('runtime');
415
+            $profile = $this->profiler->collect(\OCP\Server::get(IRequest::class), new Response());
416
+            $this->profiler->saveProfile($profile);
417
+        }
418
+    }
419
+
420
+    private function requestIsForSubtree(array $subTrees): bool {
421
+        foreach ($subTrees as $subTree) {
422
+            $subTree = trim($subTree, ' /');
423
+            if (str_starts_with($this->server->getRequestUri(), $subTree . '/')) {
424
+                return true;
425
+            }
426
+        }
427
+        return false;
428
+    }
429 429
 
430 430
 }
Please login to merge, or discard this patch.
apps/dav/lib/Files/FileSearchBackend.php 1 patch
Indentation   +499 added lines, -499 removed lines patch added patch discarded remove patch
@@ -42,503 +42,503 @@
 block discarded – undo
42 42
 use SearchDAV\Query\Query;
43 43
 
44 44
 class FileSearchBackend implements ISearchBackend {
45
-	public const OPERATOR_LIMIT = 100;
46
-
47
-	public function __construct(
48
-		private Server $server,
49
-		private CachingTree $tree,
50
-		private IUser $user,
51
-		private IRootFolder $rootFolder,
52
-		private IManager $shareManager,
53
-		private View $view,
54
-		private IFilesMetadataManager $filesMetadataManager,
55
-	) {
56
-	}
57
-
58
-	/**
59
-	 * Search endpoint will be remote.php/dav
60
-	 */
61
-	public function getArbiterPath(): string {
62
-		return '';
63
-	}
64
-
65
-	public function isValidScope(string $href, $depth, ?string $path): bool {
66
-		// only allow scopes inside the dav server
67
-		if (is_null($path)) {
68
-			return false;
69
-		}
70
-
71
-		try {
72
-			$node = $this->tree->getNodeForPath($path);
73
-			return $node instanceof Directory;
74
-		} catch (NotFound $e) {
75
-			return false;
76
-		}
77
-	}
78
-
79
-	public function getPropertyDefinitionsForScope(string $href, ?string $path): array {
80
-		// all valid scopes support the same schema
81
-
82
-		//todo dynamically load all propfind properties that are supported
83
-		$props = [
84
-			// queryable properties
85
-			new SearchPropertyDefinition('{DAV:}displayname', true, true, true),
86
-			new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
87
-			new SearchPropertyDefinition('{DAV:}getlastmodified', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
88
-			new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
89
-			new SearchPropertyDefinition(TagsPlugin::FAVORITE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_BOOLEAN),
90
-			new SearchPropertyDefinition(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME, true, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
91
-			new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, true, true, false),
92
-
93
-			// select only properties
94
-			new SearchPropertyDefinition('{DAV:}resourcetype', true, false, false),
95
-			new SearchPropertyDefinition('{DAV:}getcontentlength', true, false, false),
96
-			new SearchPropertyDefinition(FilesPlugin::CHECKSUMS_PROPERTYNAME, true, false, false),
97
-			new SearchPropertyDefinition(FilesPlugin::PERMISSIONS_PROPERTYNAME, true, false, false),
98
-			new SearchPropertyDefinition(FilesPlugin::GETETAG_PROPERTYNAME, true, false, false),
99
-			new SearchPropertyDefinition(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, true, false, false),
100
-			new SearchPropertyDefinition(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, true, false, false),
101
-			new SearchPropertyDefinition(FilesPlugin::HAS_PREVIEW_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_BOOLEAN),
102
-			new SearchPropertyDefinition(FilesPlugin::FILEID_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
103
-		];
104
-
105
-		return array_merge($props, $this->getPropertyDefinitionsForMetadata());
106
-	}
107
-
108
-
109
-	private function getPropertyDefinitionsForMetadata(): array {
110
-		$metadataProps = [];
111
-		$metadata = $this->filesMetadataManager->getKnownMetadata();
112
-		$indexes = $metadata->getIndexes();
113
-		foreach ($metadata->getKeys() as $key) {
114
-			$isIndex = in_array($key, $indexes);
115
-			$type = match ($metadata->getType($key)) {
116
-				IMetadataValueWrapper::TYPE_INT => SearchPropertyDefinition::DATATYPE_INTEGER,
117
-				IMetadataValueWrapper::TYPE_FLOAT => SearchPropertyDefinition::DATATYPE_DECIMAL,
118
-				IMetadataValueWrapper::TYPE_BOOL => SearchPropertyDefinition::DATATYPE_BOOLEAN,
119
-				default => SearchPropertyDefinition::DATATYPE_STRING
120
-			};
121
-			$metadataProps[] = new SearchPropertyDefinition(
122
-				FilesPlugin::FILE_METADATA_PREFIX . $key,
123
-				true,
124
-				$isIndex,
125
-				$isIndex,
126
-				$type
127
-			);
128
-		}
129
-
130
-		return $metadataProps;
131
-	}
132
-
133
-	/**
134
-	 * @param INode[] $nodes
135
-	 * @param string[] $requestProperties
136
-	 */
137
-	public function preloadPropertyFor(array $nodes, array $requestProperties): void {
138
-		$this->server->emit('preloadProperties', [$nodes, $requestProperties]);
139
-	}
140
-
141
-	private function getFolderForPath(?string $path = null): Folder {
142
-		if ($path === null) {
143
-			throw new \InvalidArgumentException('Using uri\'s as scope is not supported, please use a path relative to the search arbiter instead');
144
-		}
145
-
146
-		$node = $this->tree->getNodeForPath($path);
147
-
148
-		if (!$node instanceof Directory) {
149
-			throw new \InvalidArgumentException('Search is only supported on directories');
150
-		}
151
-
152
-		$fileInfo = $node->getFileInfo();
153
-
154
-		/** @var Folder */
155
-		return $this->rootFolder->get($fileInfo->getPath());
156
-	}
157
-
158
-	/**
159
-	 * @param Query $search
160
-	 * @return SearchResult[]
161
-	 */
162
-	public function search(Query $search): array {
163
-		switch (count($search->from)) {
164
-			case 0:
165
-				throw new \InvalidArgumentException('You need to specify a scope for the search.');
166
-				break;
167
-			case 1:
168
-				$scope = $search->from[0];
169
-				$folder = $this->getFolderForPath($scope->path);
170
-				$query = $this->transformQuery($search);
171
-				$results = $folder->search($query);
172
-				break;
173
-			default:
174
-				$scopes = [];
175
-				foreach ($search->from as $scope) {
176
-					$folder = $this->getFolderForPath($scope->path);
177
-					$folderStorage = $folder->getStorage();
178
-					if ($folderStorage->instanceOfStorage(Jail::class)) {
179
-						/** @var Jail $folderStorage */
180
-						$internalPath = $folderStorage->getUnjailedPath($folder->getInternalPath());
181
-					} else {
182
-						$internalPath = $folder->getInternalPath();
183
-					}
184
-
185
-					$scopes[] = new SearchBinaryOperator(
186
-						ISearchBinaryOperator::OPERATOR_AND,
187
-						[
188
-							new SearchComparison(
189
-								ISearchComparison::COMPARE_EQUAL,
190
-								'storage',
191
-								$folderStorage->getCache()->getNumericStorageId(),
192
-								''
193
-							),
194
-							new SearchComparison(
195
-								ISearchComparison::COMPARE_LIKE,
196
-								'path',
197
-								$internalPath . '/%',
198
-								''
199
-							),
200
-						]
201
-					);
202
-				}
203
-
204
-				$scopeOperators = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $scopes);
205
-				$query = $this->transformQuery($search, $scopeOperators);
206
-				$userFolder = $this->rootFolder->getUserFolder($this->user->getUID());
207
-				$results = $userFolder->search($query);
208
-		}
209
-
210
-		/** @var SearchResult[] $nodes */
211
-		$nodes = array_map(function (Node $node) {
212
-			if ($node instanceof Folder) {
213
-				$davNode = new Directory($this->view, $node, $this->tree, $this->shareManager);
214
-			} else {
215
-				$davNode = new File($this->view, $node, $this->shareManager);
216
-			}
217
-			$path = $this->getHrefForNode($node);
218
-			$this->tree->cacheNode($davNode, $path);
219
-			return new SearchResult($davNode, $path);
220
-		}, $results);
221
-
222
-		if (!$query->limitToHome()) {
223
-			// Sort again, since the result from multiple storages is appended and not sorted
224
-			usort($nodes, function (SearchResult $a, SearchResult $b) use ($search) {
225
-				return $this->sort($a, $b, $search->orderBy);
226
-			});
227
-		}
228
-
229
-		// If a limit is provided use only return that number of files
230
-		if ($search->limit->maxResults !== 0) {
231
-			$nodes = \array_slice($nodes, 0, $search->limit->maxResults);
232
-		}
233
-
234
-		return $nodes;
235
-	}
236
-
237
-	private function sort(SearchResult $a, SearchResult $b, array $orders) {
238
-		/** @var Order $order */
239
-		foreach ($orders as $order) {
240
-			$v1 = $this->getSearchResultProperty($a, $order->property);
241
-			$v2 = $this->getSearchResultProperty($b, $order->property);
242
-
243
-
244
-			if ($v1 === null && $v2 === null) {
245
-				continue;
246
-			}
247
-			if ($v1 === null) {
248
-				return $order->order === Order::ASC ? 1 : -1;
249
-			}
250
-			if ($v2 === null) {
251
-				return $order->order === Order::ASC ? -1 : 1;
252
-			}
253
-
254
-			$s = $this->compareProperties($v1, $v2, $order);
255
-			if ($s === 0) {
256
-				continue;
257
-			}
258
-
259
-			if ($order->order === Order::DESC) {
260
-				$s = -$s;
261
-			}
262
-			return $s;
263
-		}
264
-
265
-		return 0;
266
-	}
267
-
268
-	private function compareProperties($a, $b, Order $order) {
269
-		switch ($order->property->dataType) {
270
-			case SearchPropertyDefinition::DATATYPE_STRING:
271
-				return strcmp($a, $b);
272
-			case SearchPropertyDefinition::DATATYPE_BOOLEAN:
273
-				if ($a === $b) {
274
-					return 0;
275
-				}
276
-				if ($a === false) {
277
-					return -1;
278
-				}
279
-				return 1;
280
-			default:
281
-				if ($a === $b) {
282
-					return 0;
283
-				}
284
-				if ($a < $b) {
285
-					return -1;
286
-				}
287
-				return 1;
288
-		}
289
-	}
290
-
291
-	private function getSearchResultProperty(SearchResult $result, SearchPropertyDefinition $property) {
292
-		/** @var \OCA\DAV\Connector\Sabre\Node $node */
293
-		$node = $result->node;
294
-
295
-		switch ($property->name) {
296
-			case '{DAV:}displayname':
297
-				return $node->getName();
298
-			case '{DAV:}getlastmodified':
299
-				return $node->getLastModified();
300
-			case FilesPlugin::SIZE_PROPERTYNAME:
301
-				return $node->getSize();
302
-			case FilesPlugin::INTERNAL_FILEID_PROPERTYNAME:
303
-				return $node->getInternalFileId();
304
-			default:
305
-				return null;
306
-		}
307
-	}
308
-
309
-	/**
310
-	 * @param Node $node
311
-	 * @return string
312
-	 */
313
-	private function getHrefForNode(Node $node) {
314
-		$base = '/files/' . $this->user->getUID();
315
-		return $base . $this->view->getRelativePath($node->getPath());
316
-	}
317
-
318
-	/**
319
-	 * @param Query $query
320
-	 *
321
-	 * @return ISearchQuery
322
-	 */
323
-	private function transformQuery(Query $query, ?SearchBinaryOperator $scopeOperators = null): ISearchQuery {
324
-		$orders = array_map(function (Order $order): ISearchOrder {
325
-			$direction = $order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING;
326
-			if (str_starts_with($order->property->name, FilesPlugin::FILE_METADATA_PREFIX)) {
327
-				return new SearchOrder($direction, substr($order->property->name, strlen(FilesPlugin::FILE_METADATA_PREFIX)), IMetadataQuery::EXTRA);
328
-			} else {
329
-				return new SearchOrder($direction, $this->mapPropertyNameToColumn($order->property));
330
-			}
331
-		}, $query->orderBy);
332
-
333
-		$limit = $query->limit;
334
-		$offset = $limit->firstResult;
335
-
336
-		$limitHome = false;
337
-		$ownerProp = $this->extractWhereValue($query->where, FilesPlugin::OWNER_ID_PROPERTYNAME, Operator::OPERATION_EQUAL);
338
-		if ($ownerProp !== null) {
339
-			if ($ownerProp === $this->user->getUID()) {
340
-				$limitHome = true;
341
-			} else {
342
-				throw new \InvalidArgumentException("Invalid search value for '{http://owncloud.org/ns}owner-id', only the current user id is allowed");
343
-			}
344
-		}
345
-
346
-		$operatorCount = $this->countSearchOperators($query->where);
347
-		if ($operatorCount > self::OPERATOR_LIMIT) {
348
-			throw new \InvalidArgumentException('Invalid search query, maximum operator limit of ' . self::OPERATOR_LIMIT . ' exceeded, got ' . $operatorCount . ' operators');
349
-		}
350
-
351
-		/** @var SearchBinaryOperator|SearchComparison */
352
-		$queryOperators = $this->transformSearchOperation($query->where);
353
-		if ($scopeOperators === null) {
354
-			$operators = $queryOperators;
355
-		} else {
356
-			$operators = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$queryOperators, $scopeOperators]);
357
-		}
358
-
359
-		return new SearchQuery(
360
-			$operators,
361
-			(int)$limit->maxResults,
362
-			$offset,
363
-			$orders,
364
-			$this->user,
365
-			$limitHome
366
-		);
367
-	}
368
-
369
-	private function countSearchOperators(Operator $operator): int {
370
-		switch ($operator->type) {
371
-			case Operator::OPERATION_AND:
372
-			case Operator::OPERATION_OR:
373
-			case Operator::OPERATION_NOT:
374
-				/** @var Operator[] $arguments */
375
-				$arguments = $operator->arguments;
376
-				return array_sum(array_map([$this, 'countSearchOperators'], $arguments));
377
-			case Operator::OPERATION_EQUAL:
378
-			case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
379
-			case Operator::OPERATION_GREATER_THAN:
380
-			case Operator::OPERATION_LESS_OR_EQUAL_THAN:
381
-			case Operator::OPERATION_LESS_THAN:
382
-			case Operator::OPERATION_IS_LIKE:
383
-			case Operator::OPERATION_IS_COLLECTION:
384
-			default:
385
-				return 1;
386
-		}
387
-	}
388
-
389
-	/**
390
-	 * @param Operator $operator
391
-	 * @return ISearchOperator
392
-	 */
393
-	private function transformSearchOperation(Operator $operator) {
394
-		[, $trimmedType] = explode('}', $operator->type);
395
-		switch ($operator->type) {
396
-			case Operator::OPERATION_AND:
397
-			case Operator::OPERATION_OR:
398
-			case Operator::OPERATION_NOT:
399
-				$arguments = array_map([$this, 'transformSearchOperation'], $operator->arguments);
400
-				return new SearchBinaryOperator($trimmedType, $arguments);
401
-			case Operator::OPERATION_EQUAL:
402
-			case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
403
-			case Operator::OPERATION_GREATER_THAN:
404
-			case Operator::OPERATION_LESS_OR_EQUAL_THAN:
405
-			case Operator::OPERATION_LESS_THAN:
406
-			case Operator::OPERATION_IS_LIKE:
407
-				if (count($operator->arguments) !== 2) {
408
-					throw new \InvalidArgumentException('Invalid number of arguments for ' . $trimmedType . ' operation');
409
-				}
410
-				if (!($operator->arguments[1] instanceof Literal)) {
411
-					throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal');
412
-				}
413
-				$value = $operator->arguments[1]->value;
414
-				// no break
415
-			case Operator::OPERATION_IS_DEFINED:
416
-				if (!($operator->arguments[0] instanceof SearchPropertyDefinition)) {
417
-					throw new \InvalidArgumentException('Invalid argument 1 for ' . $trimmedType . ' operation, expected property');
418
-				}
419
-				$property = $operator->arguments[0];
420
-
421
-				if (str_starts_with($property->name, FilesPlugin::FILE_METADATA_PREFIX)) {
422
-					$field = substr($property->name, strlen(FilesPlugin::FILE_METADATA_PREFIX));
423
-					$extra = IMetadataQuery::EXTRA;
424
-				} else {
425
-					$field = $this->mapPropertyNameToColumn($property);
426
-				}
427
-
428
-				try {
429
-					$castedValue = $this->castValue($property, $value ?? '');
430
-				} catch (\Error $e) {
431
-					throw new \InvalidArgumentException('Invalid property value for ' . $property->name, previous: $e);
432
-				}
433
-
434
-				return new SearchComparison(
435
-					$trimmedType,
436
-					$field,
437
-					$castedValue,
438
-					$extra ?? ''
439
-				);
440
-
441
-			case Operator::OPERATION_IS_COLLECTION:
442
-				return new SearchComparison('eq', 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE);
443
-			default:
444
-				throw new \InvalidArgumentException('Unsupported operation ' . $trimmedType . ' (' . $operator->type . ')');
445
-		}
446
-	}
447
-
448
-	/**
449
-	 * @param SearchPropertyDefinition $property
450
-	 * @return string
451
-	 */
452
-	private function mapPropertyNameToColumn(SearchPropertyDefinition $property) {
453
-		switch ($property->name) {
454
-			case '{DAV:}displayname':
455
-				return 'name';
456
-			case '{DAV:}getcontenttype':
457
-				return 'mimetype';
458
-			case '{DAV:}getlastmodified':
459
-				return 'mtime';
460
-			case FilesPlugin::SIZE_PROPERTYNAME:
461
-				return 'size';
462
-			case TagsPlugin::FAVORITE_PROPERTYNAME:
463
-				return 'favorite';
464
-			case TagsPlugin::TAGS_PROPERTYNAME:
465
-				return 'tagname';
466
-			case FilesPlugin::INTERNAL_FILEID_PROPERTYNAME:
467
-				return 'fileid';
468
-			default:
469
-				throw new \InvalidArgumentException('Unsupported property for search or order: ' . $property->name);
470
-		}
471
-	}
472
-
473
-	private function castValue(SearchPropertyDefinition $property, $value) {
474
-		if ($value === '') {
475
-			return '';
476
-		}
477
-
478
-		switch ($property->dataType) {
479
-			case SearchPropertyDefinition::DATATYPE_BOOLEAN:
480
-				return $value === 'yes';
481
-			case SearchPropertyDefinition::DATATYPE_DECIMAL:
482
-			case SearchPropertyDefinition::DATATYPE_INTEGER:
483
-			case SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER:
484
-				return 0 + $value;
485
-			case SearchPropertyDefinition::DATATYPE_DATETIME:
486
-				if (is_numeric($value)) {
487
-					return max(0, 0 + $value);
488
-				}
489
-				$date = \DateTime::createFromFormat(\DateTimeInterface::ATOM, (string)$value);
490
-				return ($date instanceof \DateTime && $date->getTimestamp() !== false) ? $date->getTimestamp() : 0;
491
-			default:
492
-				return $value;
493
-		}
494
-	}
495
-
496
-	/**
497
-	 * Get a specific property from the were clause
498
-	 */
499
-	private function extractWhereValue(Operator &$operator, string $propertyName, string $comparison, bool $acceptableLocation = true): ?string {
500
-		switch ($operator->type) {
501
-			case Operator::OPERATION_AND:
502
-			case Operator::OPERATION_OR:
503
-			case Operator::OPERATION_NOT:
504
-				foreach ($operator->arguments as &$argument) {
505
-					$value = $this->extractWhereValue($argument, $propertyName, $comparison, $acceptableLocation && $operator->type === Operator::OPERATION_AND);
506
-					if ($value !== null) {
507
-						return $value;
508
-					}
509
-				}
510
-				return null;
511
-			case Operator::OPERATION_EQUAL:
512
-			case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
513
-			case Operator::OPERATION_GREATER_THAN:
514
-			case Operator::OPERATION_LESS_OR_EQUAL_THAN:
515
-			case Operator::OPERATION_LESS_THAN:
516
-			case Operator::OPERATION_IS_LIKE:
517
-				if ($operator->arguments[0]->name === $propertyName) {
518
-					if ($operator->type === $comparison) {
519
-						if ($acceptableLocation) {
520
-							if ($operator->arguments[1] instanceof Literal) {
521
-								$value = $operator->arguments[1]->value;
522
-
523
-								// to remove the comparison from the query, we replace it with an empty AND
524
-								$operator = new Operator(Operator::OPERATION_AND);
525
-
526
-								return $value;
527
-							} else {
528
-								throw new \InvalidArgumentException("searching by '$propertyName' is only allowed with a literal value");
529
-							}
530
-						} else {
531
-							throw new \InvalidArgumentException("searching by '$propertyName' is not allowed inside a '{DAV:}or' or '{DAV:}not'");
532
-						}
533
-					} else {
534
-						throw new \InvalidArgumentException("searching by '$propertyName' is only allowed inside a '$comparison'");
535
-					}
536
-				} else {
537
-					return null;
538
-				}
539
-				// no break
540
-			default:
541
-				return null;
542
-		}
543
-	}
45
+    public const OPERATOR_LIMIT = 100;
46
+
47
+    public function __construct(
48
+        private Server $server,
49
+        private CachingTree $tree,
50
+        private IUser $user,
51
+        private IRootFolder $rootFolder,
52
+        private IManager $shareManager,
53
+        private View $view,
54
+        private IFilesMetadataManager $filesMetadataManager,
55
+    ) {
56
+    }
57
+
58
+    /**
59
+     * Search endpoint will be remote.php/dav
60
+     */
61
+    public function getArbiterPath(): string {
62
+        return '';
63
+    }
64
+
65
+    public function isValidScope(string $href, $depth, ?string $path): bool {
66
+        // only allow scopes inside the dav server
67
+        if (is_null($path)) {
68
+            return false;
69
+        }
70
+
71
+        try {
72
+            $node = $this->tree->getNodeForPath($path);
73
+            return $node instanceof Directory;
74
+        } catch (NotFound $e) {
75
+            return false;
76
+        }
77
+    }
78
+
79
+    public function getPropertyDefinitionsForScope(string $href, ?string $path): array {
80
+        // all valid scopes support the same schema
81
+
82
+        //todo dynamically load all propfind properties that are supported
83
+        $props = [
84
+            // queryable properties
85
+            new SearchPropertyDefinition('{DAV:}displayname', true, true, true),
86
+            new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
87
+            new SearchPropertyDefinition('{DAV:}getlastmodified', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
88
+            new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
89
+            new SearchPropertyDefinition(TagsPlugin::FAVORITE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_BOOLEAN),
90
+            new SearchPropertyDefinition(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME, true, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
91
+            new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, true, true, false),
92
+
93
+            // select only properties
94
+            new SearchPropertyDefinition('{DAV:}resourcetype', true, false, false),
95
+            new SearchPropertyDefinition('{DAV:}getcontentlength', true, false, false),
96
+            new SearchPropertyDefinition(FilesPlugin::CHECKSUMS_PROPERTYNAME, true, false, false),
97
+            new SearchPropertyDefinition(FilesPlugin::PERMISSIONS_PROPERTYNAME, true, false, false),
98
+            new SearchPropertyDefinition(FilesPlugin::GETETAG_PROPERTYNAME, true, false, false),
99
+            new SearchPropertyDefinition(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, true, false, false),
100
+            new SearchPropertyDefinition(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, true, false, false),
101
+            new SearchPropertyDefinition(FilesPlugin::HAS_PREVIEW_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_BOOLEAN),
102
+            new SearchPropertyDefinition(FilesPlugin::FILEID_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
103
+        ];
104
+
105
+        return array_merge($props, $this->getPropertyDefinitionsForMetadata());
106
+    }
107
+
108
+
109
+    private function getPropertyDefinitionsForMetadata(): array {
110
+        $metadataProps = [];
111
+        $metadata = $this->filesMetadataManager->getKnownMetadata();
112
+        $indexes = $metadata->getIndexes();
113
+        foreach ($metadata->getKeys() as $key) {
114
+            $isIndex = in_array($key, $indexes);
115
+            $type = match ($metadata->getType($key)) {
116
+                IMetadataValueWrapper::TYPE_INT => SearchPropertyDefinition::DATATYPE_INTEGER,
117
+                IMetadataValueWrapper::TYPE_FLOAT => SearchPropertyDefinition::DATATYPE_DECIMAL,
118
+                IMetadataValueWrapper::TYPE_BOOL => SearchPropertyDefinition::DATATYPE_BOOLEAN,
119
+                default => SearchPropertyDefinition::DATATYPE_STRING
120
+            };
121
+            $metadataProps[] = new SearchPropertyDefinition(
122
+                FilesPlugin::FILE_METADATA_PREFIX . $key,
123
+                true,
124
+                $isIndex,
125
+                $isIndex,
126
+                $type
127
+            );
128
+        }
129
+
130
+        return $metadataProps;
131
+    }
132
+
133
+    /**
134
+     * @param INode[] $nodes
135
+     * @param string[] $requestProperties
136
+     */
137
+    public function preloadPropertyFor(array $nodes, array $requestProperties): void {
138
+        $this->server->emit('preloadProperties', [$nodes, $requestProperties]);
139
+    }
140
+
141
+    private function getFolderForPath(?string $path = null): Folder {
142
+        if ($path === null) {
143
+            throw new \InvalidArgumentException('Using uri\'s as scope is not supported, please use a path relative to the search arbiter instead');
144
+        }
145
+
146
+        $node = $this->tree->getNodeForPath($path);
147
+
148
+        if (!$node instanceof Directory) {
149
+            throw new \InvalidArgumentException('Search is only supported on directories');
150
+        }
151
+
152
+        $fileInfo = $node->getFileInfo();
153
+
154
+        /** @var Folder */
155
+        return $this->rootFolder->get($fileInfo->getPath());
156
+    }
157
+
158
+    /**
159
+     * @param Query $search
160
+     * @return SearchResult[]
161
+     */
162
+    public function search(Query $search): array {
163
+        switch (count($search->from)) {
164
+            case 0:
165
+                throw new \InvalidArgumentException('You need to specify a scope for the search.');
166
+                break;
167
+            case 1:
168
+                $scope = $search->from[0];
169
+                $folder = $this->getFolderForPath($scope->path);
170
+                $query = $this->transformQuery($search);
171
+                $results = $folder->search($query);
172
+                break;
173
+            default:
174
+                $scopes = [];
175
+                foreach ($search->from as $scope) {
176
+                    $folder = $this->getFolderForPath($scope->path);
177
+                    $folderStorage = $folder->getStorage();
178
+                    if ($folderStorage->instanceOfStorage(Jail::class)) {
179
+                        /** @var Jail $folderStorage */
180
+                        $internalPath = $folderStorage->getUnjailedPath($folder->getInternalPath());
181
+                    } else {
182
+                        $internalPath = $folder->getInternalPath();
183
+                    }
184
+
185
+                    $scopes[] = new SearchBinaryOperator(
186
+                        ISearchBinaryOperator::OPERATOR_AND,
187
+                        [
188
+                            new SearchComparison(
189
+                                ISearchComparison::COMPARE_EQUAL,
190
+                                'storage',
191
+                                $folderStorage->getCache()->getNumericStorageId(),
192
+                                ''
193
+                            ),
194
+                            new SearchComparison(
195
+                                ISearchComparison::COMPARE_LIKE,
196
+                                'path',
197
+                                $internalPath . '/%',
198
+                                ''
199
+                            ),
200
+                        ]
201
+                    );
202
+                }
203
+
204
+                $scopeOperators = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $scopes);
205
+                $query = $this->transformQuery($search, $scopeOperators);
206
+                $userFolder = $this->rootFolder->getUserFolder($this->user->getUID());
207
+                $results = $userFolder->search($query);
208
+        }
209
+
210
+        /** @var SearchResult[] $nodes */
211
+        $nodes = array_map(function (Node $node) {
212
+            if ($node instanceof Folder) {
213
+                $davNode = new Directory($this->view, $node, $this->tree, $this->shareManager);
214
+            } else {
215
+                $davNode = new File($this->view, $node, $this->shareManager);
216
+            }
217
+            $path = $this->getHrefForNode($node);
218
+            $this->tree->cacheNode($davNode, $path);
219
+            return new SearchResult($davNode, $path);
220
+        }, $results);
221
+
222
+        if (!$query->limitToHome()) {
223
+            // Sort again, since the result from multiple storages is appended and not sorted
224
+            usort($nodes, function (SearchResult $a, SearchResult $b) use ($search) {
225
+                return $this->sort($a, $b, $search->orderBy);
226
+            });
227
+        }
228
+
229
+        // If a limit is provided use only return that number of files
230
+        if ($search->limit->maxResults !== 0) {
231
+            $nodes = \array_slice($nodes, 0, $search->limit->maxResults);
232
+        }
233
+
234
+        return $nodes;
235
+    }
236
+
237
+    private function sort(SearchResult $a, SearchResult $b, array $orders) {
238
+        /** @var Order $order */
239
+        foreach ($orders as $order) {
240
+            $v1 = $this->getSearchResultProperty($a, $order->property);
241
+            $v2 = $this->getSearchResultProperty($b, $order->property);
242
+
243
+
244
+            if ($v1 === null && $v2 === null) {
245
+                continue;
246
+            }
247
+            if ($v1 === null) {
248
+                return $order->order === Order::ASC ? 1 : -1;
249
+            }
250
+            if ($v2 === null) {
251
+                return $order->order === Order::ASC ? -1 : 1;
252
+            }
253
+
254
+            $s = $this->compareProperties($v1, $v2, $order);
255
+            if ($s === 0) {
256
+                continue;
257
+            }
258
+
259
+            if ($order->order === Order::DESC) {
260
+                $s = -$s;
261
+            }
262
+            return $s;
263
+        }
264
+
265
+        return 0;
266
+    }
267
+
268
+    private function compareProperties($a, $b, Order $order) {
269
+        switch ($order->property->dataType) {
270
+            case SearchPropertyDefinition::DATATYPE_STRING:
271
+                return strcmp($a, $b);
272
+            case SearchPropertyDefinition::DATATYPE_BOOLEAN:
273
+                if ($a === $b) {
274
+                    return 0;
275
+                }
276
+                if ($a === false) {
277
+                    return -1;
278
+                }
279
+                return 1;
280
+            default:
281
+                if ($a === $b) {
282
+                    return 0;
283
+                }
284
+                if ($a < $b) {
285
+                    return -1;
286
+                }
287
+                return 1;
288
+        }
289
+    }
290
+
291
+    private function getSearchResultProperty(SearchResult $result, SearchPropertyDefinition $property) {
292
+        /** @var \OCA\DAV\Connector\Sabre\Node $node */
293
+        $node = $result->node;
294
+
295
+        switch ($property->name) {
296
+            case '{DAV:}displayname':
297
+                return $node->getName();
298
+            case '{DAV:}getlastmodified':
299
+                return $node->getLastModified();
300
+            case FilesPlugin::SIZE_PROPERTYNAME:
301
+                return $node->getSize();
302
+            case FilesPlugin::INTERNAL_FILEID_PROPERTYNAME:
303
+                return $node->getInternalFileId();
304
+            default:
305
+                return null;
306
+        }
307
+    }
308
+
309
+    /**
310
+     * @param Node $node
311
+     * @return string
312
+     */
313
+    private function getHrefForNode(Node $node) {
314
+        $base = '/files/' . $this->user->getUID();
315
+        return $base . $this->view->getRelativePath($node->getPath());
316
+    }
317
+
318
+    /**
319
+     * @param Query $query
320
+     *
321
+     * @return ISearchQuery
322
+     */
323
+    private function transformQuery(Query $query, ?SearchBinaryOperator $scopeOperators = null): ISearchQuery {
324
+        $orders = array_map(function (Order $order): ISearchOrder {
325
+            $direction = $order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING;
326
+            if (str_starts_with($order->property->name, FilesPlugin::FILE_METADATA_PREFIX)) {
327
+                return new SearchOrder($direction, substr($order->property->name, strlen(FilesPlugin::FILE_METADATA_PREFIX)), IMetadataQuery::EXTRA);
328
+            } else {
329
+                return new SearchOrder($direction, $this->mapPropertyNameToColumn($order->property));
330
+            }
331
+        }, $query->orderBy);
332
+
333
+        $limit = $query->limit;
334
+        $offset = $limit->firstResult;
335
+
336
+        $limitHome = false;
337
+        $ownerProp = $this->extractWhereValue($query->where, FilesPlugin::OWNER_ID_PROPERTYNAME, Operator::OPERATION_EQUAL);
338
+        if ($ownerProp !== null) {
339
+            if ($ownerProp === $this->user->getUID()) {
340
+                $limitHome = true;
341
+            } else {
342
+                throw new \InvalidArgumentException("Invalid search value for '{http://owncloud.org/ns}owner-id', only the current user id is allowed");
343
+            }
344
+        }
345
+
346
+        $operatorCount = $this->countSearchOperators($query->where);
347
+        if ($operatorCount > self::OPERATOR_LIMIT) {
348
+            throw new \InvalidArgumentException('Invalid search query, maximum operator limit of ' . self::OPERATOR_LIMIT . ' exceeded, got ' . $operatorCount . ' operators');
349
+        }
350
+
351
+        /** @var SearchBinaryOperator|SearchComparison */
352
+        $queryOperators = $this->transformSearchOperation($query->where);
353
+        if ($scopeOperators === null) {
354
+            $operators = $queryOperators;
355
+        } else {
356
+            $operators = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$queryOperators, $scopeOperators]);
357
+        }
358
+
359
+        return new SearchQuery(
360
+            $operators,
361
+            (int)$limit->maxResults,
362
+            $offset,
363
+            $orders,
364
+            $this->user,
365
+            $limitHome
366
+        );
367
+    }
368
+
369
+    private function countSearchOperators(Operator $operator): int {
370
+        switch ($operator->type) {
371
+            case Operator::OPERATION_AND:
372
+            case Operator::OPERATION_OR:
373
+            case Operator::OPERATION_NOT:
374
+                /** @var Operator[] $arguments */
375
+                $arguments = $operator->arguments;
376
+                return array_sum(array_map([$this, 'countSearchOperators'], $arguments));
377
+            case Operator::OPERATION_EQUAL:
378
+            case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
379
+            case Operator::OPERATION_GREATER_THAN:
380
+            case Operator::OPERATION_LESS_OR_EQUAL_THAN:
381
+            case Operator::OPERATION_LESS_THAN:
382
+            case Operator::OPERATION_IS_LIKE:
383
+            case Operator::OPERATION_IS_COLLECTION:
384
+            default:
385
+                return 1;
386
+        }
387
+    }
388
+
389
+    /**
390
+     * @param Operator $operator
391
+     * @return ISearchOperator
392
+     */
393
+    private function transformSearchOperation(Operator $operator) {
394
+        [, $trimmedType] = explode('}', $operator->type);
395
+        switch ($operator->type) {
396
+            case Operator::OPERATION_AND:
397
+            case Operator::OPERATION_OR:
398
+            case Operator::OPERATION_NOT:
399
+                $arguments = array_map([$this, 'transformSearchOperation'], $operator->arguments);
400
+                return new SearchBinaryOperator($trimmedType, $arguments);
401
+            case Operator::OPERATION_EQUAL:
402
+            case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
403
+            case Operator::OPERATION_GREATER_THAN:
404
+            case Operator::OPERATION_LESS_OR_EQUAL_THAN:
405
+            case Operator::OPERATION_LESS_THAN:
406
+            case Operator::OPERATION_IS_LIKE:
407
+                if (count($operator->arguments) !== 2) {
408
+                    throw new \InvalidArgumentException('Invalid number of arguments for ' . $trimmedType . ' operation');
409
+                }
410
+                if (!($operator->arguments[1] instanceof Literal)) {
411
+                    throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal');
412
+                }
413
+                $value = $operator->arguments[1]->value;
414
+                // no break
415
+            case Operator::OPERATION_IS_DEFINED:
416
+                if (!($operator->arguments[0] instanceof SearchPropertyDefinition)) {
417
+                    throw new \InvalidArgumentException('Invalid argument 1 for ' . $trimmedType . ' operation, expected property');
418
+                }
419
+                $property = $operator->arguments[0];
420
+
421
+                if (str_starts_with($property->name, FilesPlugin::FILE_METADATA_PREFIX)) {
422
+                    $field = substr($property->name, strlen(FilesPlugin::FILE_METADATA_PREFIX));
423
+                    $extra = IMetadataQuery::EXTRA;
424
+                } else {
425
+                    $field = $this->mapPropertyNameToColumn($property);
426
+                }
427
+
428
+                try {
429
+                    $castedValue = $this->castValue($property, $value ?? '');
430
+                } catch (\Error $e) {
431
+                    throw new \InvalidArgumentException('Invalid property value for ' . $property->name, previous: $e);
432
+                }
433
+
434
+                return new SearchComparison(
435
+                    $trimmedType,
436
+                    $field,
437
+                    $castedValue,
438
+                    $extra ?? ''
439
+                );
440
+
441
+            case Operator::OPERATION_IS_COLLECTION:
442
+                return new SearchComparison('eq', 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE);
443
+            default:
444
+                throw new \InvalidArgumentException('Unsupported operation ' . $trimmedType . ' (' . $operator->type . ')');
445
+        }
446
+    }
447
+
448
+    /**
449
+     * @param SearchPropertyDefinition $property
450
+     * @return string
451
+     */
452
+    private function mapPropertyNameToColumn(SearchPropertyDefinition $property) {
453
+        switch ($property->name) {
454
+            case '{DAV:}displayname':
455
+                return 'name';
456
+            case '{DAV:}getcontenttype':
457
+                return 'mimetype';
458
+            case '{DAV:}getlastmodified':
459
+                return 'mtime';
460
+            case FilesPlugin::SIZE_PROPERTYNAME:
461
+                return 'size';
462
+            case TagsPlugin::FAVORITE_PROPERTYNAME:
463
+                return 'favorite';
464
+            case TagsPlugin::TAGS_PROPERTYNAME:
465
+                return 'tagname';
466
+            case FilesPlugin::INTERNAL_FILEID_PROPERTYNAME:
467
+                return 'fileid';
468
+            default:
469
+                throw new \InvalidArgumentException('Unsupported property for search or order: ' . $property->name);
470
+        }
471
+    }
472
+
473
+    private function castValue(SearchPropertyDefinition $property, $value) {
474
+        if ($value === '') {
475
+            return '';
476
+        }
477
+
478
+        switch ($property->dataType) {
479
+            case SearchPropertyDefinition::DATATYPE_BOOLEAN:
480
+                return $value === 'yes';
481
+            case SearchPropertyDefinition::DATATYPE_DECIMAL:
482
+            case SearchPropertyDefinition::DATATYPE_INTEGER:
483
+            case SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER:
484
+                return 0 + $value;
485
+            case SearchPropertyDefinition::DATATYPE_DATETIME:
486
+                if (is_numeric($value)) {
487
+                    return max(0, 0 + $value);
488
+                }
489
+                $date = \DateTime::createFromFormat(\DateTimeInterface::ATOM, (string)$value);
490
+                return ($date instanceof \DateTime && $date->getTimestamp() !== false) ? $date->getTimestamp() : 0;
491
+            default:
492
+                return $value;
493
+        }
494
+    }
495
+
496
+    /**
497
+     * Get a specific property from the were clause
498
+     */
499
+    private function extractWhereValue(Operator &$operator, string $propertyName, string $comparison, bool $acceptableLocation = true): ?string {
500
+        switch ($operator->type) {
501
+            case Operator::OPERATION_AND:
502
+            case Operator::OPERATION_OR:
503
+            case Operator::OPERATION_NOT:
504
+                foreach ($operator->arguments as &$argument) {
505
+                    $value = $this->extractWhereValue($argument, $propertyName, $comparison, $acceptableLocation && $operator->type === Operator::OPERATION_AND);
506
+                    if ($value !== null) {
507
+                        return $value;
508
+                    }
509
+                }
510
+                return null;
511
+            case Operator::OPERATION_EQUAL:
512
+            case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
513
+            case Operator::OPERATION_GREATER_THAN:
514
+            case Operator::OPERATION_LESS_OR_EQUAL_THAN:
515
+            case Operator::OPERATION_LESS_THAN:
516
+            case Operator::OPERATION_IS_LIKE:
517
+                if ($operator->arguments[0]->name === $propertyName) {
518
+                    if ($operator->type === $comparison) {
519
+                        if ($acceptableLocation) {
520
+                            if ($operator->arguments[1] instanceof Literal) {
521
+                                $value = $operator->arguments[1]->value;
522
+
523
+                                // to remove the comparison from the query, we replace it with an empty AND
524
+                                $operator = new Operator(Operator::OPERATION_AND);
525
+
526
+                                return $value;
527
+                            } else {
528
+                                throw new \InvalidArgumentException("searching by '$propertyName' is only allowed with a literal value");
529
+                            }
530
+                        } else {
531
+                            throw new \InvalidArgumentException("searching by '$propertyName' is not allowed inside a '{DAV:}or' or '{DAV:}not'");
532
+                        }
533
+                    } else {
534
+                        throw new \InvalidArgumentException("searching by '$propertyName' is only allowed inside a '$comparison'");
535
+                    }
536
+                } else {
537
+                    return null;
538
+                }
539
+                // no break
540
+            default:
541
+                return null;
542
+        }
543
+    }
544 544
 }
Please login to merge, or discard this patch.
apps/dav/tests/unit/Files/FileSearchBackendTest.php 1 patch
Indentation   +382 added lines, -382 removed lines patch added patch discarded remove patch
@@ -36,386 +36,386 @@
 block discarded – undo
36 36
 use Test\TestCase;
37 37
 
38 38
 class FileSearchBackendTest extends TestCase {
39
-	private ObjectTree&MockObject $tree;
40
-	private Server&MockObject $server;
41
-	private IUser&MockObject $user;
42
-	private IRootFolder&MockObject $rootFolder;
43
-	private IManager&MockObject $shareManager;
44
-	private View&MockObject $view;
45
-	private Folder&MockObject $searchFolder;
46
-	private Directory&MockObject $davFolder;
47
-	private FileSearchBackend $search;
48
-
49
-	protected function setUp(): void {
50
-		parent::setUp();
51
-
52
-		$this->user = $this->createMock(IUser::class);
53
-		$this->user->expects($this->any())
54
-			->method('getUID')
55
-			->willReturn('test');
56
-
57
-		$this->tree = $this->createMock(ObjectTree::class);
58
-		$this->server = $this->createMock(Server::class);
59
-		$this->view = $this->createMock(View::class);
60
-		$this->rootFolder = $this->createMock(IRootFolder::class);
61
-		$this->shareManager = $this->createMock(IManager::class);
62
-		$this->searchFolder = $this->createMock(Folder::class);
63
-		$fileInfo = $this->createMock(FileInfo::class);
64
-		$this->davFolder = $this->createMock(Directory::class);
65
-
66
-		$this->view->expects($this->any())
67
-			->method('getRoot')
68
-			->willReturn('');
69
-
70
-		$this->view->expects($this->any())
71
-			->method('getRelativePath')
72
-			->willReturnArgument(0);
73
-
74
-		$this->davFolder->expects($this->any())
75
-			->method('getFileInfo')
76
-			->willReturn($fileInfo);
77
-
78
-		$this->rootFolder->expects($this->any())
79
-			->method('get')
80
-			->willReturn($this->searchFolder);
81
-
82
-		$filesMetadataManager = $this->createMock(IFilesMetadataManager::class);
83
-
84
-		$this->search = new FileSearchBackend($this->server, $this->tree, $this->user, $this->rootFolder, $this->shareManager, $this->view, $filesMetadataManager);
85
-	}
86
-
87
-	public function testSearchFilename(): void {
88
-		$this->tree->expects($this->any())
89
-			->method('getNodeForPath')
90
-			->willReturn($this->davFolder);
91
-
92
-		$this->searchFolder->expects($this->once())
93
-			->method('search')
94
-			->with(new SearchQuery(
95
-				new SearchComparison(
96
-					ISearchComparison::COMPARE_EQUAL,
97
-					'name',
98
-					'foo'
99
-				),
100
-				0,
101
-				0,
102
-				[],
103
-				$this->user
104
-			))
105
-			->willReturn([
106
-				new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
107
-			]);
108
-
109
-		$query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}displayname', 'foo');
110
-		$result = $this->search->search($query);
111
-
112
-		$this->assertCount(1, $result);
113
-		$this->assertEquals('/files/test/test/path', $result[0]->href);
114
-	}
115
-
116
-	public function testSearchMimetype(): void {
117
-		$this->tree->expects($this->any())
118
-			->method('getNodeForPath')
119
-			->willReturn($this->davFolder);
120
-
121
-		$this->searchFolder->expects($this->once())
122
-			->method('search')
123
-			->with(new SearchQuery(
124
-				new SearchComparison(
125
-					ISearchComparison::COMPARE_EQUAL,
126
-					'mimetype',
127
-					'foo'
128
-				),
129
-				0,
130
-				0,
131
-				[],
132
-				$this->user
133
-			))
134
-			->willReturn([
135
-				new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
136
-			]);
137
-
138
-		$query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}getcontenttype', 'foo');
139
-		$result = $this->search->search($query);
140
-
141
-		$this->assertCount(1, $result);
142
-		$this->assertEquals('/files/test/test/path', $result[0]->href);
143
-	}
144
-
145
-	public function testSearchSize(): void {
146
-		$this->tree->expects($this->any())
147
-			->method('getNodeForPath')
148
-			->willReturn($this->davFolder);
149
-
150
-		$this->searchFolder->expects($this->once())
151
-			->method('search')
152
-			->with(new SearchQuery(
153
-				new SearchComparison(
154
-					ISearchComparison::COMPARE_GREATER_THAN,
155
-					'size',
156
-					10
157
-				),
158
-				0,
159
-				0,
160
-				[],
161
-				$this->user
162
-			))
163
-			->willReturn([
164
-				new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
165
-			]);
166
-
167
-		$query = $this->getBasicQuery(Operator::OPERATION_GREATER_THAN, FilesPlugin::SIZE_PROPERTYNAME, 10);
168
-		$result = $this->search->search($query);
169
-
170
-		$this->assertCount(1, $result);
171
-		$this->assertEquals('/files/test/test/path', $result[0]->href);
172
-	}
173
-
174
-	public function testSearchMtime(): void {
175
-		$this->tree->expects($this->any())
176
-			->method('getNodeForPath')
177
-			->willReturn($this->davFolder);
178
-
179
-		$this->searchFolder->expects($this->once())
180
-			->method('search')
181
-			->with(new SearchQuery(
182
-				new SearchComparison(
183
-					ISearchComparison::COMPARE_GREATER_THAN,
184
-					'mtime',
185
-					10
186
-				),
187
-				0,
188
-				0,
189
-				[],
190
-				$this->user
191
-			))
192
-			->willReturn([
193
-				new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
194
-			]);
195
-
196
-		$query = $this->getBasicQuery(Operator::OPERATION_GREATER_THAN, '{DAV:}getlastmodified', 10);
197
-		$result = $this->search->search($query);
198
-
199
-		$this->assertCount(1, $result);
200
-		$this->assertEquals('/files/test/test/path', $result[0]->href);
201
-	}
202
-
203
-	public function testSearchIsCollection(): void {
204
-		$this->tree->expects($this->any())
205
-			->method('getNodeForPath')
206
-			->willReturn($this->davFolder);
207
-
208
-		$this->searchFolder->expects($this->once())
209
-			->method('search')
210
-			->with(new SearchQuery(
211
-				new SearchComparison(
212
-					ISearchComparison::COMPARE_EQUAL,
213
-					'mimetype',
214
-					FileInfo::MIMETYPE_FOLDER
215
-				),
216
-				0,
217
-				0,
218
-				[],
219
-				$this->user
220
-			))
221
-			->willReturn([
222
-				new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
223
-			]);
224
-
225
-		$query = $this->getBasicQuery(Operator::OPERATION_IS_COLLECTION, 'yes');
226
-		$result = $this->search->search($query);
227
-
228
-		$this->assertCount(1, $result);
229
-		$this->assertEquals('/files/test/test/path', $result[0]->href);
230
-	}
231
-
232
-
233
-	public function testSearchInvalidProp(): void {
234
-		$this->expectException(\InvalidArgumentException::class);
235
-
236
-		$this->tree->expects($this->any())
237
-			->method('getNodeForPath')
238
-			->willReturn($this->davFolder);
239
-
240
-		$this->searchFolder->expects($this->never())
241
-			->method('search');
242
-
243
-		$query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}getetag', 'foo');
244
-		$this->search->search($query);
245
-	}
246
-
247
-	private function getBasicQuery(string $type, string $property, int|string|null $value = null) {
248
-		$scope = new Scope('/', 'infinite');
249
-		$scope->path = '/';
250
-		$from = [$scope];
251
-		$orderBy = [];
252
-		$select = [];
253
-		if (is_null($value)) {
254
-			$where = new Operator(
255
-				$type,
256
-				[new Literal($property)]
257
-			);
258
-		} else {
259
-			$where = new Operator(
260
-				$type,
261
-				[new SearchPropertyDefinition($property, true, true, true), new Literal($value)]
262
-			);
263
-		}
264
-		$limit = new Limit();
265
-
266
-		return new Query($select, $from, $where, $orderBy, $limit);
267
-	}
268
-
269
-
270
-	public function testSearchNonFolder(): void {
271
-		$this->expectException(\InvalidArgumentException::class);
272
-
273
-		$davNode = $this->createMock(File::class);
274
-
275
-		$this->tree->expects($this->any())
276
-			->method('getNodeForPath')
277
-			->willReturn($davNode);
278
-
279
-		$query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}displayname', 'foo');
280
-		$this->search->search($query);
281
-	}
282
-
283
-	public function testSearchLimitOwnerBasic(): void {
284
-		$this->tree->expects($this->any())
285
-			->method('getNodeForPath')
286
-			->willReturn($this->davFolder);
287
-
288
-		/** @var ISearchQuery|null $receivedQuery */
289
-		$receivedQuery = null;
290
-		$this->searchFolder
291
-			->method('search')
292
-			->willReturnCallback(function ($query) use (&$receivedQuery) {
293
-				$receivedQuery = $query;
294
-				return [
295
-					new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
296
-				];
297
-			});
298
-
299
-		$query = $this->getBasicQuery(Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID());
300
-		$this->search->search($query);
301
-
302
-		$this->assertNotNull($receivedQuery);
303
-		$this->assertTrue($receivedQuery->limitToHome());
304
-
305
-		/** @var ISearchBinaryOperator $operator */
306
-		$operator = $receivedQuery->getSearchOperation();
307
-		$this->assertInstanceOf(ISearchBinaryOperator::class, $operator);
308
-		$this->assertEquals(ISearchBinaryOperator::OPERATOR_AND, $operator->getType());
309
-		$this->assertEmpty($operator->getArguments());
310
-	}
311
-
312
-	public function testSearchLimitOwnerNested(): void {
313
-		$this->tree->expects($this->any())
314
-			->method('getNodeForPath')
315
-			->willReturn($this->davFolder);
316
-
317
-		/** @var ISearchQuery|null $receivedQuery */
318
-		$receivedQuery = null;
319
-		$this->searchFolder
320
-			->method('search')
321
-			->willReturnCallback(function ($query) use (&$receivedQuery) {
322
-				$receivedQuery = $query;
323
-				return [
324
-					new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
325
-				];
326
-			});
327
-
328
-		$query = $this->getBasicQuery(Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID());
329
-		$query->where = new Operator(
330
-			Operator::OPERATION_AND,
331
-			[
332
-				new Operator(
333
-					Operator::OPERATION_EQUAL,
334
-					[new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), new Literal('image/png')]
335
-				),
336
-				new Operator(
337
-					Operator::OPERATION_EQUAL,
338
-					[new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, true, true, true), new Literal($this->user->getUID())]
339
-				),
340
-			]
341
-		);
342
-		$this->search->search($query);
343
-
344
-		$this->assertNotNull($receivedQuery);
345
-		$this->assertTrue($receivedQuery->limitToHome());
346
-
347
-		/** @var ISearchBinaryOperator $operator */
348
-		$operator = $receivedQuery->getSearchOperation();
349
-		$this->assertInstanceOf(ISearchBinaryOperator::class, $operator);
350
-		$this->assertEquals(ISearchBinaryOperator::OPERATOR_AND, $operator->getType());
351
-		$this->assertCount(2, $operator->getArguments());
352
-
353
-		/** @var ISearchBinaryOperator $operator */
354
-		$operator = $operator->getArguments()[1];
355
-		$this->assertInstanceOf(ISearchBinaryOperator::class, $operator);
356
-		$this->assertEquals(ISearchBinaryOperator::OPERATOR_AND, $operator->getType());
357
-		$this->assertEmpty($operator->getArguments());
358
-	}
359
-
360
-	public function testSearchOperatorLimit(): void {
361
-		$this->tree->expects($this->any())
362
-			->method('getNodeForPath')
363
-			->willReturn($this->davFolder);
364
-
365
-		$innerOperator = new Operator(
366
-			Operator::OPERATION_EQUAL,
367
-			[new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), new Literal('image/png')]
368
-		);
369
-		// 5 child operators
370
-		$level1Operator = new Operator(
371
-			Operator::OPERATION_AND,
372
-			[
373
-				$innerOperator,
374
-				$innerOperator,
375
-				$innerOperator,
376
-				$innerOperator,
377
-				$innerOperator,
378
-			]
379
-		);
380
-		// 5^2 = 25 child operators
381
-		$level2Operator = new Operator(
382
-			Operator::OPERATION_AND,
383
-			[
384
-				$level1Operator,
385
-				$level1Operator,
386
-				$level1Operator,
387
-				$level1Operator,
388
-				$level1Operator,
389
-			]
390
-		);
391
-		// 5^3 = 125 child operators
392
-		$level3Operator = new Operator(
393
-			Operator::OPERATION_AND,
394
-			[
395
-				$level2Operator,
396
-				$level2Operator,
397
-				$level2Operator,
398
-				$level2Operator,
399
-				$level2Operator,
400
-			]
401
-		);
402
-
403
-		$query = $this->getBasicQuery(Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID());
404
-		$query->where = $level3Operator;
405
-		$this->expectException(\InvalidArgumentException::class);
406
-		$this->search->search($query);
407
-	}
408
-
409
-	public function testPreloadPropertyFor(): void {
410
-		$node1 = $this->createMock(File::class);
411
-		$node2 = $this->createMock(Directory::class);
412
-		$nodes = [$node1, $node2];
413
-		$requestProperties = ['{DAV:}getcontenttype', '{DAV:}getlastmodified'];
414
-
415
-		$this->server->expects($this->once())
416
-			->method('emit')
417
-			->with('preloadProperties', [$nodes, $requestProperties]);
418
-
419
-		$this->search->preloadPropertyFor($nodes, $requestProperties);
420
-	}
39
+    private ObjectTree&MockObject $tree;
40
+    private Server&MockObject $server;
41
+    private IUser&MockObject $user;
42
+    private IRootFolder&MockObject $rootFolder;
43
+    private IManager&MockObject $shareManager;
44
+    private View&MockObject $view;
45
+    private Folder&MockObject $searchFolder;
46
+    private Directory&MockObject $davFolder;
47
+    private FileSearchBackend $search;
48
+
49
+    protected function setUp(): void {
50
+        parent::setUp();
51
+
52
+        $this->user = $this->createMock(IUser::class);
53
+        $this->user->expects($this->any())
54
+            ->method('getUID')
55
+            ->willReturn('test');
56
+
57
+        $this->tree = $this->createMock(ObjectTree::class);
58
+        $this->server = $this->createMock(Server::class);
59
+        $this->view = $this->createMock(View::class);
60
+        $this->rootFolder = $this->createMock(IRootFolder::class);
61
+        $this->shareManager = $this->createMock(IManager::class);
62
+        $this->searchFolder = $this->createMock(Folder::class);
63
+        $fileInfo = $this->createMock(FileInfo::class);
64
+        $this->davFolder = $this->createMock(Directory::class);
65
+
66
+        $this->view->expects($this->any())
67
+            ->method('getRoot')
68
+            ->willReturn('');
69
+
70
+        $this->view->expects($this->any())
71
+            ->method('getRelativePath')
72
+            ->willReturnArgument(0);
73
+
74
+        $this->davFolder->expects($this->any())
75
+            ->method('getFileInfo')
76
+            ->willReturn($fileInfo);
77
+
78
+        $this->rootFolder->expects($this->any())
79
+            ->method('get')
80
+            ->willReturn($this->searchFolder);
81
+
82
+        $filesMetadataManager = $this->createMock(IFilesMetadataManager::class);
83
+
84
+        $this->search = new FileSearchBackend($this->server, $this->tree, $this->user, $this->rootFolder, $this->shareManager, $this->view, $filesMetadataManager);
85
+    }
86
+
87
+    public function testSearchFilename(): void {
88
+        $this->tree->expects($this->any())
89
+            ->method('getNodeForPath')
90
+            ->willReturn($this->davFolder);
91
+
92
+        $this->searchFolder->expects($this->once())
93
+            ->method('search')
94
+            ->with(new SearchQuery(
95
+                new SearchComparison(
96
+                    ISearchComparison::COMPARE_EQUAL,
97
+                    'name',
98
+                    'foo'
99
+                ),
100
+                0,
101
+                0,
102
+                [],
103
+                $this->user
104
+            ))
105
+            ->willReturn([
106
+                new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
107
+            ]);
108
+
109
+        $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}displayname', 'foo');
110
+        $result = $this->search->search($query);
111
+
112
+        $this->assertCount(1, $result);
113
+        $this->assertEquals('/files/test/test/path', $result[0]->href);
114
+    }
115
+
116
+    public function testSearchMimetype(): void {
117
+        $this->tree->expects($this->any())
118
+            ->method('getNodeForPath')
119
+            ->willReturn($this->davFolder);
120
+
121
+        $this->searchFolder->expects($this->once())
122
+            ->method('search')
123
+            ->with(new SearchQuery(
124
+                new SearchComparison(
125
+                    ISearchComparison::COMPARE_EQUAL,
126
+                    'mimetype',
127
+                    'foo'
128
+                ),
129
+                0,
130
+                0,
131
+                [],
132
+                $this->user
133
+            ))
134
+            ->willReturn([
135
+                new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
136
+            ]);
137
+
138
+        $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}getcontenttype', 'foo');
139
+        $result = $this->search->search($query);
140
+
141
+        $this->assertCount(1, $result);
142
+        $this->assertEquals('/files/test/test/path', $result[0]->href);
143
+    }
144
+
145
+    public function testSearchSize(): void {
146
+        $this->tree->expects($this->any())
147
+            ->method('getNodeForPath')
148
+            ->willReturn($this->davFolder);
149
+
150
+        $this->searchFolder->expects($this->once())
151
+            ->method('search')
152
+            ->with(new SearchQuery(
153
+                new SearchComparison(
154
+                    ISearchComparison::COMPARE_GREATER_THAN,
155
+                    'size',
156
+                    10
157
+                ),
158
+                0,
159
+                0,
160
+                [],
161
+                $this->user
162
+            ))
163
+            ->willReturn([
164
+                new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
165
+            ]);
166
+
167
+        $query = $this->getBasicQuery(Operator::OPERATION_GREATER_THAN, FilesPlugin::SIZE_PROPERTYNAME, 10);
168
+        $result = $this->search->search($query);
169
+
170
+        $this->assertCount(1, $result);
171
+        $this->assertEquals('/files/test/test/path', $result[0]->href);
172
+    }
173
+
174
+    public function testSearchMtime(): void {
175
+        $this->tree->expects($this->any())
176
+            ->method('getNodeForPath')
177
+            ->willReturn($this->davFolder);
178
+
179
+        $this->searchFolder->expects($this->once())
180
+            ->method('search')
181
+            ->with(new SearchQuery(
182
+                new SearchComparison(
183
+                    ISearchComparison::COMPARE_GREATER_THAN,
184
+                    'mtime',
185
+                    10
186
+                ),
187
+                0,
188
+                0,
189
+                [],
190
+                $this->user
191
+            ))
192
+            ->willReturn([
193
+                new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
194
+            ]);
195
+
196
+        $query = $this->getBasicQuery(Operator::OPERATION_GREATER_THAN, '{DAV:}getlastmodified', 10);
197
+        $result = $this->search->search($query);
198
+
199
+        $this->assertCount(1, $result);
200
+        $this->assertEquals('/files/test/test/path', $result[0]->href);
201
+    }
202
+
203
+    public function testSearchIsCollection(): void {
204
+        $this->tree->expects($this->any())
205
+            ->method('getNodeForPath')
206
+            ->willReturn($this->davFolder);
207
+
208
+        $this->searchFolder->expects($this->once())
209
+            ->method('search')
210
+            ->with(new SearchQuery(
211
+                new SearchComparison(
212
+                    ISearchComparison::COMPARE_EQUAL,
213
+                    'mimetype',
214
+                    FileInfo::MIMETYPE_FOLDER
215
+                ),
216
+                0,
217
+                0,
218
+                [],
219
+                $this->user
220
+            ))
221
+            ->willReturn([
222
+                new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
223
+            ]);
224
+
225
+        $query = $this->getBasicQuery(Operator::OPERATION_IS_COLLECTION, 'yes');
226
+        $result = $this->search->search($query);
227
+
228
+        $this->assertCount(1, $result);
229
+        $this->assertEquals('/files/test/test/path', $result[0]->href);
230
+    }
231
+
232
+
233
+    public function testSearchInvalidProp(): void {
234
+        $this->expectException(\InvalidArgumentException::class);
235
+
236
+        $this->tree->expects($this->any())
237
+            ->method('getNodeForPath')
238
+            ->willReturn($this->davFolder);
239
+
240
+        $this->searchFolder->expects($this->never())
241
+            ->method('search');
242
+
243
+        $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}getetag', 'foo');
244
+        $this->search->search($query);
245
+    }
246
+
247
+    private function getBasicQuery(string $type, string $property, int|string|null $value = null) {
248
+        $scope = new Scope('/', 'infinite');
249
+        $scope->path = '/';
250
+        $from = [$scope];
251
+        $orderBy = [];
252
+        $select = [];
253
+        if (is_null($value)) {
254
+            $where = new Operator(
255
+                $type,
256
+                [new Literal($property)]
257
+            );
258
+        } else {
259
+            $where = new Operator(
260
+                $type,
261
+                [new SearchPropertyDefinition($property, true, true, true), new Literal($value)]
262
+            );
263
+        }
264
+        $limit = new Limit();
265
+
266
+        return new Query($select, $from, $where, $orderBy, $limit);
267
+    }
268
+
269
+
270
+    public function testSearchNonFolder(): void {
271
+        $this->expectException(\InvalidArgumentException::class);
272
+
273
+        $davNode = $this->createMock(File::class);
274
+
275
+        $this->tree->expects($this->any())
276
+            ->method('getNodeForPath')
277
+            ->willReturn($davNode);
278
+
279
+        $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}displayname', 'foo');
280
+        $this->search->search($query);
281
+    }
282
+
283
+    public function testSearchLimitOwnerBasic(): void {
284
+        $this->tree->expects($this->any())
285
+            ->method('getNodeForPath')
286
+            ->willReturn($this->davFolder);
287
+
288
+        /** @var ISearchQuery|null $receivedQuery */
289
+        $receivedQuery = null;
290
+        $this->searchFolder
291
+            ->method('search')
292
+            ->willReturnCallback(function ($query) use (&$receivedQuery) {
293
+                $receivedQuery = $query;
294
+                return [
295
+                    new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
296
+                ];
297
+            });
298
+
299
+        $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID());
300
+        $this->search->search($query);
301
+
302
+        $this->assertNotNull($receivedQuery);
303
+        $this->assertTrue($receivedQuery->limitToHome());
304
+
305
+        /** @var ISearchBinaryOperator $operator */
306
+        $operator = $receivedQuery->getSearchOperation();
307
+        $this->assertInstanceOf(ISearchBinaryOperator::class, $operator);
308
+        $this->assertEquals(ISearchBinaryOperator::OPERATOR_AND, $operator->getType());
309
+        $this->assertEmpty($operator->getArguments());
310
+    }
311
+
312
+    public function testSearchLimitOwnerNested(): void {
313
+        $this->tree->expects($this->any())
314
+            ->method('getNodeForPath')
315
+            ->willReturn($this->davFolder);
316
+
317
+        /** @var ISearchQuery|null $receivedQuery */
318
+        $receivedQuery = null;
319
+        $this->searchFolder
320
+            ->method('search')
321
+            ->willReturnCallback(function ($query) use (&$receivedQuery) {
322
+                $receivedQuery = $query;
323
+                return [
324
+                    new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'),
325
+                ];
326
+            });
327
+
328
+        $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID());
329
+        $query->where = new Operator(
330
+            Operator::OPERATION_AND,
331
+            [
332
+                new Operator(
333
+                    Operator::OPERATION_EQUAL,
334
+                    [new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), new Literal('image/png')]
335
+                ),
336
+                new Operator(
337
+                    Operator::OPERATION_EQUAL,
338
+                    [new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, true, true, true), new Literal($this->user->getUID())]
339
+                ),
340
+            ]
341
+        );
342
+        $this->search->search($query);
343
+
344
+        $this->assertNotNull($receivedQuery);
345
+        $this->assertTrue($receivedQuery->limitToHome());
346
+
347
+        /** @var ISearchBinaryOperator $operator */
348
+        $operator = $receivedQuery->getSearchOperation();
349
+        $this->assertInstanceOf(ISearchBinaryOperator::class, $operator);
350
+        $this->assertEquals(ISearchBinaryOperator::OPERATOR_AND, $operator->getType());
351
+        $this->assertCount(2, $operator->getArguments());
352
+
353
+        /** @var ISearchBinaryOperator $operator */
354
+        $operator = $operator->getArguments()[1];
355
+        $this->assertInstanceOf(ISearchBinaryOperator::class, $operator);
356
+        $this->assertEquals(ISearchBinaryOperator::OPERATOR_AND, $operator->getType());
357
+        $this->assertEmpty($operator->getArguments());
358
+    }
359
+
360
+    public function testSearchOperatorLimit(): void {
361
+        $this->tree->expects($this->any())
362
+            ->method('getNodeForPath')
363
+            ->willReturn($this->davFolder);
364
+
365
+        $innerOperator = new Operator(
366
+            Operator::OPERATION_EQUAL,
367
+            [new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), new Literal('image/png')]
368
+        );
369
+        // 5 child operators
370
+        $level1Operator = new Operator(
371
+            Operator::OPERATION_AND,
372
+            [
373
+                $innerOperator,
374
+                $innerOperator,
375
+                $innerOperator,
376
+                $innerOperator,
377
+                $innerOperator,
378
+            ]
379
+        );
380
+        // 5^2 = 25 child operators
381
+        $level2Operator = new Operator(
382
+            Operator::OPERATION_AND,
383
+            [
384
+                $level1Operator,
385
+                $level1Operator,
386
+                $level1Operator,
387
+                $level1Operator,
388
+                $level1Operator,
389
+            ]
390
+        );
391
+        // 5^3 = 125 child operators
392
+        $level3Operator = new Operator(
393
+            Operator::OPERATION_AND,
394
+            [
395
+                $level2Operator,
396
+                $level2Operator,
397
+                $level2Operator,
398
+                $level2Operator,
399
+                $level2Operator,
400
+            ]
401
+        );
402
+
403
+        $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID());
404
+        $query->where = $level3Operator;
405
+        $this->expectException(\InvalidArgumentException::class);
406
+        $this->search->search($query);
407
+    }
408
+
409
+    public function testPreloadPropertyFor(): void {
410
+        $node1 = $this->createMock(File::class);
411
+        $node2 = $this->createMock(Directory::class);
412
+        $nodes = [$node1, $node2];
413
+        $requestProperties = ['{DAV:}getcontenttype', '{DAV:}getlastmodified'];
414
+
415
+        $this->server->expects($this->once())
416
+            ->method('emit')
417
+            ->with('preloadProperties', [$nodes, $requestProperties]);
418
+
419
+        $this->search->preloadPropertyFor($nodes, $requestProperties);
420
+    }
421 421
 }
Please login to merge, or discard this patch.