Passed
Push — master ( e81fdf...9f1d49 )
by Robin
16:12 queued 13s
created
lib/private/IntegrityCheck/Checker.php 1 patch
Indentation   +550 added lines, -550 removed lines patch added patch discarded remove patch
@@ -57,554 +57,554 @@
 block discarded – undo
57 57
  * @package OC\IntegrityCheck
58 58
  */
59 59
 class Checker {
60
-	public const CACHE_KEY = 'oc.integritycheck.checker';
61
-	/** @var EnvironmentHelper */
62
-	private $environmentHelper;
63
-	/** @var AppLocator */
64
-	private $appLocator;
65
-	/** @var FileAccessHelper */
66
-	private $fileAccessHelper;
67
-	/** @var IConfig|null */
68
-	private $config;
69
-	/** @var ICache */
70
-	private $cache;
71
-	/** @var IAppManager|null */
72
-	private $appManager;
73
-	/** @var IMimeTypeDetector */
74
-	private $mimeTypeDetector;
75
-
76
-	/**
77
-	 * @param EnvironmentHelper $environmentHelper
78
-	 * @param FileAccessHelper $fileAccessHelper
79
-	 * @param AppLocator $appLocator
80
-	 * @param IConfig|null $config
81
-	 * @param ICacheFactory $cacheFactory
82
-	 * @param IAppManager|null $appManager
83
-	 * @param IMimeTypeDetector $mimeTypeDetector
84
-	 */
85
-	public function __construct(EnvironmentHelper $environmentHelper,
86
-								FileAccessHelper $fileAccessHelper,
87
-								AppLocator $appLocator,
88
-								?IConfig $config,
89
-								ICacheFactory $cacheFactory,
90
-								?IAppManager $appManager,
91
-								IMimeTypeDetector $mimeTypeDetector) {
92
-		$this->environmentHelper = $environmentHelper;
93
-		$this->fileAccessHelper = $fileAccessHelper;
94
-		$this->appLocator = $appLocator;
95
-		$this->config = $config;
96
-		$this->cache = $cacheFactory->createDistributed(self::CACHE_KEY);
97
-		$this->appManager = $appManager;
98
-		$this->mimeTypeDetector = $mimeTypeDetector;
99
-	}
100
-
101
-	/**
102
-	 * Whether code signing is enforced or not.
103
-	 *
104
-	 * @return bool
105
-	 */
106
-	public function isCodeCheckEnforced(): bool {
107
-		$notSignedChannels = [ '', 'git'];
108
-		if (\in_array($this->environmentHelper->getChannel(), $notSignedChannels, true)) {
109
-			return false;
110
-		}
111
-
112
-		/**
113
-		 * This config option is undocumented and supposed to be so, it's only
114
-		 * applicable for very specific scenarios and we should not advertise it
115
-		 * too prominent. So please do not add it to config.sample.php.
116
-		 */
117
-		$isIntegrityCheckDisabled = false;
118
-		if ($this->config !== null) {
119
-			$isIntegrityCheckDisabled = $this->config->getSystemValueBool('integrity.check.disabled', false);
120
-		}
121
-		if ($isIntegrityCheckDisabled) {
122
-			return false;
123
-		}
124
-
125
-		return true;
126
-	}
127
-
128
-	/**
129
-	 * Enumerates all files belonging to the folder. Sensible defaults are excluded.
130
-	 *
131
-	 * @param string $folderToIterate
132
-	 * @param string $root
133
-	 * @return \RecursiveIteratorIterator
134
-	 * @throws \Exception
135
-	 */
136
-	private function getFolderIterator(string $folderToIterate, string $root = ''): \RecursiveIteratorIterator {
137
-		$dirItr = new \RecursiveDirectoryIterator(
138
-			$folderToIterate,
139
-			\RecursiveDirectoryIterator::SKIP_DOTS
140
-		);
141
-		if ($root === '') {
142
-			$root = \OC::$SERVERROOT;
143
-		}
144
-		$root = rtrim($root, '/');
145
-
146
-		$excludeGenericFilesIterator = new ExcludeFileByNameFilterIterator($dirItr);
147
-		$excludeFoldersIterator = new ExcludeFoldersByPathFilterIterator($excludeGenericFilesIterator, $root);
148
-
149
-		return new \RecursiveIteratorIterator(
150
-			$excludeFoldersIterator,
151
-			\RecursiveIteratorIterator::SELF_FIRST
152
-		);
153
-	}
154
-
155
-	/**
156
-	 * Returns an array of ['filename' => 'SHA512-hash-of-file'] for all files found
157
-	 * in the iterator.
158
-	 *
159
-	 * @param \RecursiveIteratorIterator $iterator
160
-	 * @param string $path
161
-	 * @return array Array of hashes.
162
-	 */
163
-	private function generateHashes(\RecursiveIteratorIterator $iterator,
164
-									string $path): array {
165
-		$hashes = [];
166
-
167
-		$baseDirectoryLength = \strlen($path);
168
-		foreach ($iterator as $filename => $data) {
169
-			/** @var \DirectoryIterator $data */
170
-			if ($data->isDir()) {
171
-				continue;
172
-			}
173
-
174
-			$relativeFileName = substr($filename, $baseDirectoryLength);
175
-			$relativeFileName = ltrim($relativeFileName, '/');
176
-
177
-			// Exclude signature.json files in the appinfo and root folder
178
-			if ($relativeFileName === 'appinfo/signature.json') {
179
-				continue;
180
-			}
181
-			// Exclude signature.json files in the appinfo and core folder
182
-			if ($relativeFileName === 'core/signature.json') {
183
-				continue;
184
-			}
185
-
186
-			// The .htaccess file in the root folder of ownCloud can contain
187
-			// custom content after the installation due to the fact that dynamic
188
-			// content is written into it at installation time as well. This
189
-			// includes for example the 404 and 403 instructions.
190
-			// Thus we ignore everything below the first occurrence of
191
-			// "#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####" and have the
192
-			// hash generated based on this.
193
-			if ($filename === $this->environmentHelper->getServerRoot() . '/.htaccess') {
194
-				$fileContent = file_get_contents($filename);
195
-				$explodedArray = explode('#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####', $fileContent);
196
-				if (\count($explodedArray) === 2) {
197
-					$hashes[$relativeFileName] = hash('sha512', $explodedArray[0]);
198
-					continue;
199
-				}
200
-			}
201
-			if ($filename === $this->environmentHelper->getServerRoot() . '/core/js/mimetypelist.js') {
202
-				$oldMimetypeList = new GenerateMimetypeFileBuilder();
203
-				$newFile = $oldMimetypeList->generateFile($this->mimeTypeDetector->getAllAliases());
204
-				$oldFile = $this->fileAccessHelper->file_get_contents($filename);
205
-				if ($newFile === $oldFile) {
206
-					$hashes[$relativeFileName] = hash('sha512', $oldMimetypeList->generateFile($this->mimeTypeDetector->getOnlyDefaultAliases()));
207
-					continue;
208
-				}
209
-			}
210
-
211
-			$hashes[$relativeFileName] = hash_file('sha512', $filename);
212
-		}
213
-
214
-		return $hashes;
215
-	}
216
-
217
-	/**
218
-	 * Creates the signature data
219
-	 *
220
-	 * @param array $hashes
221
-	 * @param X509 $certificate
222
-	 * @param RSA $privateKey
223
-	 * @return array
224
-	 */
225
-	private function createSignatureData(array $hashes,
226
-										 X509 $certificate,
227
-										 RSA $privateKey): array {
228
-		ksort($hashes);
229
-
230
-		$privateKey->setSignatureMode(RSA::SIGNATURE_PSS);
231
-		$privateKey->setMGFHash('sha512');
232
-		// See https://tools.ietf.org/html/rfc3447#page-38
233
-		$privateKey->setSaltLength(0);
234
-		$signature = $privateKey->sign(json_encode($hashes));
235
-
236
-		return [
237
-			'hashes' => $hashes,
238
-			'signature' => base64_encode($signature),
239
-			'certificate' => $certificate->saveX509($certificate->currentCert),
240
-		];
241
-	}
242
-
243
-	/**
244
-	 * Write the signature of the app in the specified folder
245
-	 *
246
-	 * @param string $path
247
-	 * @param X509 $certificate
248
-	 * @param RSA $privateKey
249
-	 * @throws \Exception
250
-	 */
251
-	public function writeAppSignature($path,
252
-									  X509 $certificate,
253
-									  RSA $privateKey) {
254
-		$appInfoDir = $path . '/appinfo';
255
-		try {
256
-			$this->fileAccessHelper->assertDirectoryExists($appInfoDir);
257
-
258
-			$iterator = $this->getFolderIterator($path);
259
-			$hashes = $this->generateHashes($iterator, $path);
260
-			$signature = $this->createSignatureData($hashes, $certificate, $privateKey);
261
-			$this->fileAccessHelper->file_put_contents(
262
-				$appInfoDir . '/signature.json',
263
-				json_encode($signature, JSON_PRETTY_PRINT)
264
-			);
265
-		} catch (\Exception $e) {
266
-			if (!$this->fileAccessHelper->is_writable($appInfoDir)) {
267
-				throw new \Exception($appInfoDir . ' is not writable');
268
-			}
269
-			throw $e;
270
-		}
271
-	}
272
-
273
-	/**
274
-	 * Write the signature of core
275
-	 *
276
-	 * @param X509 $certificate
277
-	 * @param RSA $rsa
278
-	 * @param string $path
279
-	 * @throws \Exception
280
-	 */
281
-	public function writeCoreSignature(X509 $certificate,
282
-									   RSA $rsa,
283
-									   $path) {
284
-		$coreDir = $path . '/core';
285
-		try {
286
-			$this->fileAccessHelper->assertDirectoryExists($coreDir);
287
-			$iterator = $this->getFolderIterator($path, $path);
288
-			$hashes = $this->generateHashes($iterator, $path);
289
-			$signatureData = $this->createSignatureData($hashes, $certificate, $rsa);
290
-			$this->fileAccessHelper->file_put_contents(
291
-				$coreDir . '/signature.json',
292
-				json_encode($signatureData, JSON_PRETTY_PRINT)
293
-			);
294
-		} catch (\Exception $e) {
295
-			if (!$this->fileAccessHelper->is_writable($coreDir)) {
296
-				throw new \Exception($coreDir . ' is not writable');
297
-			}
298
-			throw $e;
299
-		}
300
-	}
301
-
302
-	/**
303
-	 * Split the certificate file in individual certs
304
-	 *
305
-	 * @param string $cert
306
-	 * @return string[]
307
-	 */
308
-	private function splitCerts(string $cert): array {
309
-		preg_match_all('([\-]{3,}[\S\ ]+?[\-]{3,}[\S\s]+?[\-]{3,}[\S\ ]+?[\-]{3,})', $cert, $matches);
310
-
311
-		return $matches[0];
312
-	}
313
-
314
-	/**
315
-	 * Verifies the signature for the specified path.
316
-	 *
317
-	 * @param string $signaturePath
318
-	 * @param string $basePath
319
-	 * @param string $certificateCN
320
-	 * @param bool $forceVerify
321
-	 * @return array
322
-	 * @throws InvalidSignatureException
323
-	 * @throws \Exception
324
-	 */
325
-	private function verify(string $signaturePath, string $basePath, string $certificateCN, bool $forceVerify = false): array {
326
-		if (!$forceVerify && !$this->isCodeCheckEnforced()) {
327
-			return [];
328
-		}
329
-
330
-		$content = $this->fileAccessHelper->file_get_contents($signaturePath);
331
-		$signatureData = null;
332
-
333
-		if (\is_string($content)) {
334
-			$signatureData = json_decode($content, true);
335
-		}
336
-		if (!\is_array($signatureData)) {
337
-			throw new InvalidSignatureException('Signature data not found.');
338
-		}
339
-
340
-		$expectedHashes = $signatureData['hashes'];
341
-		ksort($expectedHashes);
342
-		$signature = base64_decode($signatureData['signature']);
343
-		$certificate = $signatureData['certificate'];
344
-
345
-		// Check if certificate is signed by Nextcloud Root Authority
346
-		$x509 = new \phpseclib\File\X509();
347
-		$rootCertificatePublicKey = $this->fileAccessHelper->file_get_contents($this->environmentHelper->getServerRoot().'/resources/codesigning/root.crt');
348
-
349
-		$rootCerts = $this->splitCerts($rootCertificatePublicKey);
350
-		foreach ($rootCerts as $rootCert) {
351
-			$x509->loadCA($rootCert);
352
-		}
353
-		$x509->loadX509($certificate);
354
-		if (!$x509->validateSignature()) {
355
-			throw new InvalidSignatureException('Certificate is not valid.');
356
-		}
357
-		// Verify if certificate has proper CN. "core" CN is always trusted.
358
-		if ($x509->getDN(X509::DN_OPENSSL)['CN'] !== $certificateCN && $x509->getDN(X509::DN_OPENSSL)['CN'] !== 'core') {
359
-			throw new InvalidSignatureException(
360
-				sprintf('Certificate is not valid for required scope. (Requested: %s, current: CN=%s)', $certificateCN, $x509->getDN(true)['CN'])
361
-			);
362
-		}
363
-
364
-		// Check if the signature of the files is valid
365
-		$rsa = new \phpseclib\Crypt\RSA();
366
-		$rsa->loadKey($x509->currentCert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']);
367
-		$rsa->setSignatureMode(RSA::SIGNATURE_PSS);
368
-		$rsa->setMGFHash('sha512');
369
-		// See https://tools.ietf.org/html/rfc3447#page-38
370
-		$rsa->setSaltLength(0);
371
-		if (!$rsa->verify(json_encode($expectedHashes), $signature)) {
372
-			throw new InvalidSignatureException('Signature could not get verified.');
373
-		}
374
-
375
-		// Fixes for the updater as shipped with ownCloud 9.0.x: The updater is
376
-		// replaced after the code integrity check is performed.
377
-		//
378
-		// Due to this reason we exclude the whole updater/ folder from the code
379
-		// integrity check.
380
-		if ($basePath === $this->environmentHelper->getServerRoot()) {
381
-			foreach ($expectedHashes as $fileName => $hash) {
382
-				if (str_starts_with($fileName, 'updater/')) {
383
-					unset($expectedHashes[$fileName]);
384
-				}
385
-			}
386
-		}
387
-
388
-		// Compare the list of files which are not identical
389
-		$currentInstanceHashes = $this->generateHashes($this->getFolderIterator($basePath), $basePath);
390
-		$differencesA = array_diff($expectedHashes, $currentInstanceHashes);
391
-		$differencesB = array_diff($currentInstanceHashes, $expectedHashes);
392
-		$differences = array_unique(array_merge($differencesA, $differencesB));
393
-		$differenceArray = [];
394
-		foreach ($differences as $filename => $hash) {
395
-			// Check if file should not exist in the new signature table
396
-			if (!array_key_exists($filename, $expectedHashes)) {
397
-				$differenceArray['EXTRA_FILE'][$filename]['expected'] = '';
398
-				$differenceArray['EXTRA_FILE'][$filename]['current'] = $hash;
399
-				continue;
400
-			}
401
-
402
-			// Check if file is missing
403
-			if (!array_key_exists($filename, $currentInstanceHashes)) {
404
-				$differenceArray['FILE_MISSING'][$filename]['expected'] = $expectedHashes[$filename];
405
-				$differenceArray['FILE_MISSING'][$filename]['current'] = '';
406
-				continue;
407
-			}
408
-
409
-			// Check if hash does mismatch
410
-			if ($expectedHashes[$filename] !== $currentInstanceHashes[$filename]) {
411
-				$differenceArray['INVALID_HASH'][$filename]['expected'] = $expectedHashes[$filename];
412
-				$differenceArray['INVALID_HASH'][$filename]['current'] = $currentInstanceHashes[$filename];
413
-				continue;
414
-			}
415
-
416
-			// Should never happen.
417
-			throw new \Exception('Invalid behaviour in file hash comparison experienced. Please report this error to the developers.');
418
-		}
419
-
420
-		return $differenceArray;
421
-	}
422
-
423
-	/**
424
-	 * Whether the code integrity check has passed successful or not
425
-	 *
426
-	 * @return bool
427
-	 */
428
-	public function hasPassedCheck(): bool {
429
-		$results = $this->getResults();
430
-		if (empty($results)) {
431
-			return true;
432
-		}
433
-
434
-		return false;
435
-	}
436
-
437
-	/**
438
-	 * @return array
439
-	 */
440
-	public function getResults(): array {
441
-		$cachedResults = $this->cache->get(self::CACHE_KEY);
442
-		if (!\is_null($cachedResults) and $cachedResults !== false) {
443
-			return json_decode($cachedResults, true);
444
-		}
445
-
446
-		if ($this->config !== null) {
447
-			return json_decode($this->config->getAppValue('core', self::CACHE_KEY, '{}'), true);
448
-		}
449
-		return [];
450
-	}
451
-
452
-	/**
453
-	 * Stores the results in the app config as well as cache
454
-	 *
455
-	 * @param string $scope
456
-	 * @param array $result
457
-	 */
458
-	private function storeResults(string $scope, array $result) {
459
-		$resultArray = $this->getResults();
460
-		unset($resultArray[$scope]);
461
-		if (!empty($result)) {
462
-			$resultArray[$scope] = $result;
463
-		}
464
-		if ($this->config !== null) {
465
-			$this->config->setAppValue('core', self::CACHE_KEY, json_encode($resultArray));
466
-		}
467
-		$this->cache->set(self::CACHE_KEY, json_encode($resultArray));
468
-	}
469
-
470
-	/**
471
-	 *
472
-	 * Clean previous results for a proper rescanning. Otherwise
473
-	 */
474
-	private function cleanResults() {
475
-		$this->config->deleteAppValue('core', self::CACHE_KEY);
476
-		$this->cache->remove(self::CACHE_KEY);
477
-	}
478
-
479
-	/**
480
-	 * Verify the signature of $appId. Returns an array with the following content:
481
-	 * [
482
-	 * 	'FILE_MISSING' =>
483
-	 * 	[
484
-	 * 		'filename' => [
485
-	 * 			'expected' => 'expectedSHA512',
486
-	 * 			'current' => 'currentSHA512',
487
-	 * 		],
488
-	 * 	],
489
-	 * 	'EXTRA_FILE' =>
490
-	 * 	[
491
-	 * 		'filename' => [
492
-	 * 			'expected' => 'expectedSHA512',
493
-	 * 			'current' => 'currentSHA512',
494
-	 * 		],
495
-	 * 	],
496
-	 * 	'INVALID_HASH' =>
497
-	 * 	[
498
-	 * 		'filename' => [
499
-	 * 			'expected' => 'expectedSHA512',
500
-	 * 			'current' => 'currentSHA512',
501
-	 * 		],
502
-	 * 	],
503
-	 * ]
504
-	 *
505
-	 * Array may be empty in case no problems have been found.
506
-	 *
507
-	 * @param string $appId
508
-	 * @param string $path Optional path. If none is given it will be guessed.
509
-	 * @param bool $forceVerify
510
-	 * @return array
511
-	 */
512
-	public function verifyAppSignature(string $appId, string $path = '', bool $forceVerify = false): array {
513
-		try {
514
-			if ($path === '') {
515
-				$path = $this->appLocator->getAppPath($appId);
516
-			}
517
-			$result = $this->verify(
518
-				$path . '/appinfo/signature.json',
519
-				$path,
520
-				$appId,
521
-				$forceVerify
522
-			);
523
-		} catch (\Exception $e) {
524
-			$result = [
525
-				'EXCEPTION' => [
526
-					'class' => \get_class($e),
527
-					'message' => $e->getMessage(),
528
-				],
529
-			];
530
-		}
531
-		$this->storeResults($appId, $result);
532
-
533
-		return $result;
534
-	}
535
-
536
-	/**
537
-	 * Verify the signature of core. Returns an array with the following content:
538
-	 * [
539
-	 * 	'FILE_MISSING' =>
540
-	 * 	[
541
-	 * 		'filename' => [
542
-	 * 			'expected' => 'expectedSHA512',
543
-	 * 			'current' => 'currentSHA512',
544
-	 * 		],
545
-	 * 	],
546
-	 * 	'EXTRA_FILE' =>
547
-	 * 	[
548
-	 * 		'filename' => [
549
-	 * 			'expected' => 'expectedSHA512',
550
-	 * 			'current' => 'currentSHA512',
551
-	 * 		],
552
-	 * 	],
553
-	 * 	'INVALID_HASH' =>
554
-	 * 	[
555
-	 * 		'filename' => [
556
-	 * 			'expected' => 'expectedSHA512',
557
-	 * 			'current' => 'currentSHA512',
558
-	 * 		],
559
-	 * 	],
560
-	 * ]
561
-	 *
562
-	 * Array may be empty in case no problems have been found.
563
-	 *
564
-	 * @return array
565
-	 */
566
-	public function verifyCoreSignature(): array {
567
-		try {
568
-			$result = $this->verify(
569
-				$this->environmentHelper->getServerRoot() . '/core/signature.json',
570
-				$this->environmentHelper->getServerRoot(),
571
-				'core'
572
-			);
573
-		} catch (\Exception $e) {
574
-			$result = [
575
-				'EXCEPTION' => [
576
-					'class' => \get_class($e),
577
-					'message' => $e->getMessage(),
578
-				],
579
-			];
580
-		}
581
-		$this->storeResults('core', $result);
582
-
583
-		return $result;
584
-	}
585
-
586
-	/**
587
-	 * Verify the core code of the instance as well as all applicable applications
588
-	 * and store the results.
589
-	 */
590
-	public function runInstanceVerification() {
591
-		$this->cleanResults();
592
-		$this->verifyCoreSignature();
593
-		$appIds = $this->appLocator->getAllApps();
594
-		foreach ($appIds as $appId) {
595
-			// If an application is shipped a valid signature is required
596
-			$isShipped = $this->appManager->isShipped($appId);
597
-			$appNeedsToBeChecked = false;
598
-			if ($isShipped) {
599
-				$appNeedsToBeChecked = true;
600
-			} elseif ($this->fileAccessHelper->file_exists($this->appLocator->getAppPath($appId) . '/appinfo/signature.json')) {
601
-				// Otherwise only if the application explicitly ships a signature.json file
602
-				$appNeedsToBeChecked = true;
603
-			}
604
-
605
-			if ($appNeedsToBeChecked) {
606
-				$this->verifyAppSignature($appId);
607
-			}
608
-		}
609
-	}
60
+    public const CACHE_KEY = 'oc.integritycheck.checker';
61
+    /** @var EnvironmentHelper */
62
+    private $environmentHelper;
63
+    /** @var AppLocator */
64
+    private $appLocator;
65
+    /** @var FileAccessHelper */
66
+    private $fileAccessHelper;
67
+    /** @var IConfig|null */
68
+    private $config;
69
+    /** @var ICache */
70
+    private $cache;
71
+    /** @var IAppManager|null */
72
+    private $appManager;
73
+    /** @var IMimeTypeDetector */
74
+    private $mimeTypeDetector;
75
+
76
+    /**
77
+     * @param EnvironmentHelper $environmentHelper
78
+     * @param FileAccessHelper $fileAccessHelper
79
+     * @param AppLocator $appLocator
80
+     * @param IConfig|null $config
81
+     * @param ICacheFactory $cacheFactory
82
+     * @param IAppManager|null $appManager
83
+     * @param IMimeTypeDetector $mimeTypeDetector
84
+     */
85
+    public function __construct(EnvironmentHelper $environmentHelper,
86
+                                FileAccessHelper $fileAccessHelper,
87
+                                AppLocator $appLocator,
88
+                                ?IConfig $config,
89
+                                ICacheFactory $cacheFactory,
90
+                                ?IAppManager $appManager,
91
+                                IMimeTypeDetector $mimeTypeDetector) {
92
+        $this->environmentHelper = $environmentHelper;
93
+        $this->fileAccessHelper = $fileAccessHelper;
94
+        $this->appLocator = $appLocator;
95
+        $this->config = $config;
96
+        $this->cache = $cacheFactory->createDistributed(self::CACHE_KEY);
97
+        $this->appManager = $appManager;
98
+        $this->mimeTypeDetector = $mimeTypeDetector;
99
+    }
100
+
101
+    /**
102
+     * Whether code signing is enforced or not.
103
+     *
104
+     * @return bool
105
+     */
106
+    public function isCodeCheckEnforced(): bool {
107
+        $notSignedChannels = [ '', 'git'];
108
+        if (\in_array($this->environmentHelper->getChannel(), $notSignedChannels, true)) {
109
+            return false;
110
+        }
111
+
112
+        /**
113
+         * This config option is undocumented and supposed to be so, it's only
114
+         * applicable for very specific scenarios and we should not advertise it
115
+         * too prominent. So please do not add it to config.sample.php.
116
+         */
117
+        $isIntegrityCheckDisabled = false;
118
+        if ($this->config !== null) {
119
+            $isIntegrityCheckDisabled = $this->config->getSystemValueBool('integrity.check.disabled', false);
120
+        }
121
+        if ($isIntegrityCheckDisabled) {
122
+            return false;
123
+        }
124
+
125
+        return true;
126
+    }
127
+
128
+    /**
129
+     * Enumerates all files belonging to the folder. Sensible defaults are excluded.
130
+     *
131
+     * @param string $folderToIterate
132
+     * @param string $root
133
+     * @return \RecursiveIteratorIterator
134
+     * @throws \Exception
135
+     */
136
+    private function getFolderIterator(string $folderToIterate, string $root = ''): \RecursiveIteratorIterator {
137
+        $dirItr = new \RecursiveDirectoryIterator(
138
+            $folderToIterate,
139
+            \RecursiveDirectoryIterator::SKIP_DOTS
140
+        );
141
+        if ($root === '') {
142
+            $root = \OC::$SERVERROOT;
143
+        }
144
+        $root = rtrim($root, '/');
145
+
146
+        $excludeGenericFilesIterator = new ExcludeFileByNameFilterIterator($dirItr);
147
+        $excludeFoldersIterator = new ExcludeFoldersByPathFilterIterator($excludeGenericFilesIterator, $root);
148
+
149
+        return new \RecursiveIteratorIterator(
150
+            $excludeFoldersIterator,
151
+            \RecursiveIteratorIterator::SELF_FIRST
152
+        );
153
+    }
154
+
155
+    /**
156
+     * Returns an array of ['filename' => 'SHA512-hash-of-file'] for all files found
157
+     * in the iterator.
158
+     *
159
+     * @param \RecursiveIteratorIterator $iterator
160
+     * @param string $path
161
+     * @return array Array of hashes.
162
+     */
163
+    private function generateHashes(\RecursiveIteratorIterator $iterator,
164
+                                    string $path): array {
165
+        $hashes = [];
166
+
167
+        $baseDirectoryLength = \strlen($path);
168
+        foreach ($iterator as $filename => $data) {
169
+            /** @var \DirectoryIterator $data */
170
+            if ($data->isDir()) {
171
+                continue;
172
+            }
173
+
174
+            $relativeFileName = substr($filename, $baseDirectoryLength);
175
+            $relativeFileName = ltrim($relativeFileName, '/');
176
+
177
+            // Exclude signature.json files in the appinfo and root folder
178
+            if ($relativeFileName === 'appinfo/signature.json') {
179
+                continue;
180
+            }
181
+            // Exclude signature.json files in the appinfo and core folder
182
+            if ($relativeFileName === 'core/signature.json') {
183
+                continue;
184
+            }
185
+
186
+            // The .htaccess file in the root folder of ownCloud can contain
187
+            // custom content after the installation due to the fact that dynamic
188
+            // content is written into it at installation time as well. This
189
+            // includes for example the 404 and 403 instructions.
190
+            // Thus we ignore everything below the first occurrence of
191
+            // "#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####" and have the
192
+            // hash generated based on this.
193
+            if ($filename === $this->environmentHelper->getServerRoot() . '/.htaccess') {
194
+                $fileContent = file_get_contents($filename);
195
+                $explodedArray = explode('#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####', $fileContent);
196
+                if (\count($explodedArray) === 2) {
197
+                    $hashes[$relativeFileName] = hash('sha512', $explodedArray[0]);
198
+                    continue;
199
+                }
200
+            }
201
+            if ($filename === $this->environmentHelper->getServerRoot() . '/core/js/mimetypelist.js') {
202
+                $oldMimetypeList = new GenerateMimetypeFileBuilder();
203
+                $newFile = $oldMimetypeList->generateFile($this->mimeTypeDetector->getAllAliases());
204
+                $oldFile = $this->fileAccessHelper->file_get_contents($filename);
205
+                if ($newFile === $oldFile) {
206
+                    $hashes[$relativeFileName] = hash('sha512', $oldMimetypeList->generateFile($this->mimeTypeDetector->getOnlyDefaultAliases()));
207
+                    continue;
208
+                }
209
+            }
210
+
211
+            $hashes[$relativeFileName] = hash_file('sha512', $filename);
212
+        }
213
+
214
+        return $hashes;
215
+    }
216
+
217
+    /**
218
+     * Creates the signature data
219
+     *
220
+     * @param array $hashes
221
+     * @param X509 $certificate
222
+     * @param RSA $privateKey
223
+     * @return array
224
+     */
225
+    private function createSignatureData(array $hashes,
226
+                                            X509 $certificate,
227
+                                            RSA $privateKey): array {
228
+        ksort($hashes);
229
+
230
+        $privateKey->setSignatureMode(RSA::SIGNATURE_PSS);
231
+        $privateKey->setMGFHash('sha512');
232
+        // See https://tools.ietf.org/html/rfc3447#page-38
233
+        $privateKey->setSaltLength(0);
234
+        $signature = $privateKey->sign(json_encode($hashes));
235
+
236
+        return [
237
+            'hashes' => $hashes,
238
+            'signature' => base64_encode($signature),
239
+            'certificate' => $certificate->saveX509($certificate->currentCert),
240
+        ];
241
+    }
242
+
243
+    /**
244
+     * Write the signature of the app in the specified folder
245
+     *
246
+     * @param string $path
247
+     * @param X509 $certificate
248
+     * @param RSA $privateKey
249
+     * @throws \Exception
250
+     */
251
+    public function writeAppSignature($path,
252
+                                        X509 $certificate,
253
+                                        RSA $privateKey) {
254
+        $appInfoDir = $path . '/appinfo';
255
+        try {
256
+            $this->fileAccessHelper->assertDirectoryExists($appInfoDir);
257
+
258
+            $iterator = $this->getFolderIterator($path);
259
+            $hashes = $this->generateHashes($iterator, $path);
260
+            $signature = $this->createSignatureData($hashes, $certificate, $privateKey);
261
+            $this->fileAccessHelper->file_put_contents(
262
+                $appInfoDir . '/signature.json',
263
+                json_encode($signature, JSON_PRETTY_PRINT)
264
+            );
265
+        } catch (\Exception $e) {
266
+            if (!$this->fileAccessHelper->is_writable($appInfoDir)) {
267
+                throw new \Exception($appInfoDir . ' is not writable');
268
+            }
269
+            throw $e;
270
+        }
271
+    }
272
+
273
+    /**
274
+     * Write the signature of core
275
+     *
276
+     * @param X509 $certificate
277
+     * @param RSA $rsa
278
+     * @param string $path
279
+     * @throws \Exception
280
+     */
281
+    public function writeCoreSignature(X509 $certificate,
282
+                                        RSA $rsa,
283
+                                        $path) {
284
+        $coreDir = $path . '/core';
285
+        try {
286
+            $this->fileAccessHelper->assertDirectoryExists($coreDir);
287
+            $iterator = $this->getFolderIterator($path, $path);
288
+            $hashes = $this->generateHashes($iterator, $path);
289
+            $signatureData = $this->createSignatureData($hashes, $certificate, $rsa);
290
+            $this->fileAccessHelper->file_put_contents(
291
+                $coreDir . '/signature.json',
292
+                json_encode($signatureData, JSON_PRETTY_PRINT)
293
+            );
294
+        } catch (\Exception $e) {
295
+            if (!$this->fileAccessHelper->is_writable($coreDir)) {
296
+                throw new \Exception($coreDir . ' is not writable');
297
+            }
298
+            throw $e;
299
+        }
300
+    }
301
+
302
+    /**
303
+     * Split the certificate file in individual certs
304
+     *
305
+     * @param string $cert
306
+     * @return string[]
307
+     */
308
+    private function splitCerts(string $cert): array {
309
+        preg_match_all('([\-]{3,}[\S\ ]+?[\-]{3,}[\S\s]+?[\-]{3,}[\S\ ]+?[\-]{3,})', $cert, $matches);
310
+
311
+        return $matches[0];
312
+    }
313
+
314
+    /**
315
+     * Verifies the signature for the specified path.
316
+     *
317
+     * @param string $signaturePath
318
+     * @param string $basePath
319
+     * @param string $certificateCN
320
+     * @param bool $forceVerify
321
+     * @return array
322
+     * @throws InvalidSignatureException
323
+     * @throws \Exception
324
+     */
325
+    private function verify(string $signaturePath, string $basePath, string $certificateCN, bool $forceVerify = false): array {
326
+        if (!$forceVerify && !$this->isCodeCheckEnforced()) {
327
+            return [];
328
+        }
329
+
330
+        $content = $this->fileAccessHelper->file_get_contents($signaturePath);
331
+        $signatureData = null;
332
+
333
+        if (\is_string($content)) {
334
+            $signatureData = json_decode($content, true);
335
+        }
336
+        if (!\is_array($signatureData)) {
337
+            throw new InvalidSignatureException('Signature data not found.');
338
+        }
339
+
340
+        $expectedHashes = $signatureData['hashes'];
341
+        ksort($expectedHashes);
342
+        $signature = base64_decode($signatureData['signature']);
343
+        $certificate = $signatureData['certificate'];
344
+
345
+        // Check if certificate is signed by Nextcloud Root Authority
346
+        $x509 = new \phpseclib\File\X509();
347
+        $rootCertificatePublicKey = $this->fileAccessHelper->file_get_contents($this->environmentHelper->getServerRoot().'/resources/codesigning/root.crt');
348
+
349
+        $rootCerts = $this->splitCerts($rootCertificatePublicKey);
350
+        foreach ($rootCerts as $rootCert) {
351
+            $x509->loadCA($rootCert);
352
+        }
353
+        $x509->loadX509($certificate);
354
+        if (!$x509->validateSignature()) {
355
+            throw new InvalidSignatureException('Certificate is not valid.');
356
+        }
357
+        // Verify if certificate has proper CN. "core" CN is always trusted.
358
+        if ($x509->getDN(X509::DN_OPENSSL)['CN'] !== $certificateCN && $x509->getDN(X509::DN_OPENSSL)['CN'] !== 'core') {
359
+            throw new InvalidSignatureException(
360
+                sprintf('Certificate is not valid for required scope. (Requested: %s, current: CN=%s)', $certificateCN, $x509->getDN(true)['CN'])
361
+            );
362
+        }
363
+
364
+        // Check if the signature of the files is valid
365
+        $rsa = new \phpseclib\Crypt\RSA();
366
+        $rsa->loadKey($x509->currentCert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']);
367
+        $rsa->setSignatureMode(RSA::SIGNATURE_PSS);
368
+        $rsa->setMGFHash('sha512');
369
+        // See https://tools.ietf.org/html/rfc3447#page-38
370
+        $rsa->setSaltLength(0);
371
+        if (!$rsa->verify(json_encode($expectedHashes), $signature)) {
372
+            throw new InvalidSignatureException('Signature could not get verified.');
373
+        }
374
+
375
+        // Fixes for the updater as shipped with ownCloud 9.0.x: The updater is
376
+        // replaced after the code integrity check is performed.
377
+        //
378
+        // Due to this reason we exclude the whole updater/ folder from the code
379
+        // integrity check.
380
+        if ($basePath === $this->environmentHelper->getServerRoot()) {
381
+            foreach ($expectedHashes as $fileName => $hash) {
382
+                if (str_starts_with($fileName, 'updater/')) {
383
+                    unset($expectedHashes[$fileName]);
384
+                }
385
+            }
386
+        }
387
+
388
+        // Compare the list of files which are not identical
389
+        $currentInstanceHashes = $this->generateHashes($this->getFolderIterator($basePath), $basePath);
390
+        $differencesA = array_diff($expectedHashes, $currentInstanceHashes);
391
+        $differencesB = array_diff($currentInstanceHashes, $expectedHashes);
392
+        $differences = array_unique(array_merge($differencesA, $differencesB));
393
+        $differenceArray = [];
394
+        foreach ($differences as $filename => $hash) {
395
+            // Check if file should not exist in the new signature table
396
+            if (!array_key_exists($filename, $expectedHashes)) {
397
+                $differenceArray['EXTRA_FILE'][$filename]['expected'] = '';
398
+                $differenceArray['EXTRA_FILE'][$filename]['current'] = $hash;
399
+                continue;
400
+            }
401
+
402
+            // Check if file is missing
403
+            if (!array_key_exists($filename, $currentInstanceHashes)) {
404
+                $differenceArray['FILE_MISSING'][$filename]['expected'] = $expectedHashes[$filename];
405
+                $differenceArray['FILE_MISSING'][$filename]['current'] = '';
406
+                continue;
407
+            }
408
+
409
+            // Check if hash does mismatch
410
+            if ($expectedHashes[$filename] !== $currentInstanceHashes[$filename]) {
411
+                $differenceArray['INVALID_HASH'][$filename]['expected'] = $expectedHashes[$filename];
412
+                $differenceArray['INVALID_HASH'][$filename]['current'] = $currentInstanceHashes[$filename];
413
+                continue;
414
+            }
415
+
416
+            // Should never happen.
417
+            throw new \Exception('Invalid behaviour in file hash comparison experienced. Please report this error to the developers.');
418
+        }
419
+
420
+        return $differenceArray;
421
+    }
422
+
423
+    /**
424
+     * Whether the code integrity check has passed successful or not
425
+     *
426
+     * @return bool
427
+     */
428
+    public function hasPassedCheck(): bool {
429
+        $results = $this->getResults();
430
+        if (empty($results)) {
431
+            return true;
432
+        }
433
+
434
+        return false;
435
+    }
436
+
437
+    /**
438
+     * @return array
439
+     */
440
+    public function getResults(): array {
441
+        $cachedResults = $this->cache->get(self::CACHE_KEY);
442
+        if (!\is_null($cachedResults) and $cachedResults !== false) {
443
+            return json_decode($cachedResults, true);
444
+        }
445
+
446
+        if ($this->config !== null) {
447
+            return json_decode($this->config->getAppValue('core', self::CACHE_KEY, '{}'), true);
448
+        }
449
+        return [];
450
+    }
451
+
452
+    /**
453
+     * Stores the results in the app config as well as cache
454
+     *
455
+     * @param string $scope
456
+     * @param array $result
457
+     */
458
+    private function storeResults(string $scope, array $result) {
459
+        $resultArray = $this->getResults();
460
+        unset($resultArray[$scope]);
461
+        if (!empty($result)) {
462
+            $resultArray[$scope] = $result;
463
+        }
464
+        if ($this->config !== null) {
465
+            $this->config->setAppValue('core', self::CACHE_KEY, json_encode($resultArray));
466
+        }
467
+        $this->cache->set(self::CACHE_KEY, json_encode($resultArray));
468
+    }
469
+
470
+    /**
471
+     *
472
+     * Clean previous results for a proper rescanning. Otherwise
473
+     */
474
+    private function cleanResults() {
475
+        $this->config->deleteAppValue('core', self::CACHE_KEY);
476
+        $this->cache->remove(self::CACHE_KEY);
477
+    }
478
+
479
+    /**
480
+     * Verify the signature of $appId. Returns an array with the following content:
481
+     * [
482
+     * 	'FILE_MISSING' =>
483
+     * 	[
484
+     * 		'filename' => [
485
+     * 			'expected' => 'expectedSHA512',
486
+     * 			'current' => 'currentSHA512',
487
+     * 		],
488
+     * 	],
489
+     * 	'EXTRA_FILE' =>
490
+     * 	[
491
+     * 		'filename' => [
492
+     * 			'expected' => 'expectedSHA512',
493
+     * 			'current' => 'currentSHA512',
494
+     * 		],
495
+     * 	],
496
+     * 	'INVALID_HASH' =>
497
+     * 	[
498
+     * 		'filename' => [
499
+     * 			'expected' => 'expectedSHA512',
500
+     * 			'current' => 'currentSHA512',
501
+     * 		],
502
+     * 	],
503
+     * ]
504
+     *
505
+     * Array may be empty in case no problems have been found.
506
+     *
507
+     * @param string $appId
508
+     * @param string $path Optional path. If none is given it will be guessed.
509
+     * @param bool $forceVerify
510
+     * @return array
511
+     */
512
+    public function verifyAppSignature(string $appId, string $path = '', bool $forceVerify = false): array {
513
+        try {
514
+            if ($path === '') {
515
+                $path = $this->appLocator->getAppPath($appId);
516
+            }
517
+            $result = $this->verify(
518
+                $path . '/appinfo/signature.json',
519
+                $path,
520
+                $appId,
521
+                $forceVerify
522
+            );
523
+        } catch (\Exception $e) {
524
+            $result = [
525
+                'EXCEPTION' => [
526
+                    'class' => \get_class($e),
527
+                    'message' => $e->getMessage(),
528
+                ],
529
+            ];
530
+        }
531
+        $this->storeResults($appId, $result);
532
+
533
+        return $result;
534
+    }
535
+
536
+    /**
537
+     * Verify the signature of core. Returns an array with the following content:
538
+     * [
539
+     * 	'FILE_MISSING' =>
540
+     * 	[
541
+     * 		'filename' => [
542
+     * 			'expected' => 'expectedSHA512',
543
+     * 			'current' => 'currentSHA512',
544
+     * 		],
545
+     * 	],
546
+     * 	'EXTRA_FILE' =>
547
+     * 	[
548
+     * 		'filename' => [
549
+     * 			'expected' => 'expectedSHA512',
550
+     * 			'current' => 'currentSHA512',
551
+     * 		],
552
+     * 	],
553
+     * 	'INVALID_HASH' =>
554
+     * 	[
555
+     * 		'filename' => [
556
+     * 			'expected' => 'expectedSHA512',
557
+     * 			'current' => 'currentSHA512',
558
+     * 		],
559
+     * 	],
560
+     * ]
561
+     *
562
+     * Array may be empty in case no problems have been found.
563
+     *
564
+     * @return array
565
+     */
566
+    public function verifyCoreSignature(): array {
567
+        try {
568
+            $result = $this->verify(
569
+                $this->environmentHelper->getServerRoot() . '/core/signature.json',
570
+                $this->environmentHelper->getServerRoot(),
571
+                'core'
572
+            );
573
+        } catch (\Exception $e) {
574
+            $result = [
575
+                'EXCEPTION' => [
576
+                    'class' => \get_class($e),
577
+                    'message' => $e->getMessage(),
578
+                ],
579
+            ];
580
+        }
581
+        $this->storeResults('core', $result);
582
+
583
+        return $result;
584
+    }
585
+
586
+    /**
587
+     * Verify the core code of the instance as well as all applicable applications
588
+     * and store the results.
589
+     */
590
+    public function runInstanceVerification() {
591
+        $this->cleanResults();
592
+        $this->verifyCoreSignature();
593
+        $appIds = $this->appLocator->getAllApps();
594
+        foreach ($appIds as $appId) {
595
+            // If an application is shipped a valid signature is required
596
+            $isShipped = $this->appManager->isShipped($appId);
597
+            $appNeedsToBeChecked = false;
598
+            if ($isShipped) {
599
+                $appNeedsToBeChecked = true;
600
+            } elseif ($this->fileAccessHelper->file_exists($this->appLocator->getAppPath($appId) . '/appinfo/signature.json')) {
601
+                // Otherwise only if the application explicitly ships a signature.json file
602
+                $appNeedsToBeChecked = true;
603
+            }
604
+
605
+            if ($appNeedsToBeChecked) {
606
+                $this->verifyAppSignature($appId);
607
+            }
608
+        }
609
+    }
610 610
 }
Please login to merge, or discard this patch.
lib/private/Files/SetupManager.php 2 patches
Indentation   +541 added lines, -541 removed lines patch added patch discarded remove patch
@@ -63,547 +63,547 @@
 block discarded – undo
63 63
 use Psr\Log\LoggerInterface;
64 64
 
65 65
 class SetupManager {
66
-	private bool $rootSetup = false;
67
-	private IEventLogger $eventLogger;
68
-	private MountProviderCollection $mountProviderCollection;
69
-	private IMountManager $mountManager;
70
-	private IUserManager $userManager;
71
-	// List of users for which at least one mount is setup
72
-	private array $setupUsers = [];
73
-	// List of users for which all mounts are setup
74
-	private array $setupUsersComplete = [];
75
-	/** @var array<string, string[]> */
76
-	private array $setupUserMountProviders = [];
77
-	private IEventDispatcher $eventDispatcher;
78
-	private IUserMountCache $userMountCache;
79
-	private ILockdownManager $lockdownManager;
80
-	private IUserSession $userSession;
81
-	private ICache $cache;
82
-	private LoggerInterface $logger;
83
-	private IConfig $config;
84
-	private bool $listeningForProviders;
85
-	private array $fullSetupRequired = [];
86
-	private bool $setupBuiltinWrappersDone = false;
87
-
88
-	public function __construct(
89
-		IEventLogger $eventLogger,
90
-		MountProviderCollection $mountProviderCollection,
91
-		IMountManager $mountManager,
92
-		IUserManager $userManager,
93
-		IEventDispatcher $eventDispatcher,
94
-		IUserMountCache $userMountCache,
95
-		ILockdownManager $lockdownManager,
96
-		IUserSession $userSession,
97
-		ICacheFactory $cacheFactory,
98
-		LoggerInterface $logger,
99
-		IConfig $config
100
-	) {
101
-		$this->eventLogger = $eventLogger;
102
-		$this->mountProviderCollection = $mountProviderCollection;
103
-		$this->mountManager = $mountManager;
104
-		$this->userManager = $userManager;
105
-		$this->eventDispatcher = $eventDispatcher;
106
-		$this->userMountCache = $userMountCache;
107
-		$this->lockdownManager = $lockdownManager;
108
-		$this->logger = $logger;
109
-		$this->userSession = $userSession;
110
-		$this->cache = $cacheFactory->createDistributed('setupmanager::');
111
-		$this->listeningForProviders = false;
112
-		$this->config = $config;
113
-
114
-		$this->setupListeners();
115
-	}
116
-
117
-	private function isSetupStarted(IUser $user): bool {
118
-		return in_array($user->getUID(), $this->setupUsers, true);
119
-	}
120
-
121
-	public function isSetupComplete(IUser $user): bool {
122
-		return in_array($user->getUID(), $this->setupUsersComplete, true);
123
-	}
124
-
125
-	private function setupBuiltinWrappers() {
126
-		if ($this->setupBuiltinWrappersDone) {
127
-			return;
128
-		}
129
-		$this->setupBuiltinWrappersDone = true;
130
-
131
-		// load all filesystem apps before, so no setup-hook gets lost
132
-		OC_App::loadApps(['filesystem']);
133
-		$prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false);
134
-
135
-		Filesystem::addStorageWrapper('mount_options', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
136
-			if ($storage->instanceOfStorage(Common::class)) {
137
-				$storage->setMountOptions($mount->getOptions());
138
-			}
139
-			return $storage;
140
-		});
141
-
142
-		Filesystem::addStorageWrapper('enable_sharing', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
143
-			if (!$mount->getOption('enable_sharing', true)) {
144
-				return new PermissionsMask([
145
-					'storage' => $storage,
146
-					'mask' => Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE,
147
-				]);
148
-			}
149
-			return $storage;
150
-		});
151
-
152
-		// install storage availability wrapper, before most other wrappers
153
-		Filesystem::addStorageWrapper('oc_availability', function ($mountPoint, IStorage $storage) {
154
-			if (!$storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage') && !$storage->isLocal()) {
155
-				return new Availability(['storage' => $storage]);
156
-			}
157
-			return $storage;
158
-		});
159
-
160
-		Filesystem::addStorageWrapper('oc_encoding', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
161
-			if ($mount->getOption('encoding_compatibility', false) && !$storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) {
162
-				return new Encoding(['storage' => $storage]);
163
-			}
164
-			return $storage;
165
-		});
166
-
167
-		Filesystem::addStorageWrapper('oc_quota', function ($mountPoint, $storage) {
168
-			// set up quota for home storages, even for other users
169
-			// which can happen when using sharing
170
-
171
-			/**
172
-			 * @var Storage $storage
173
-			 */
174
-			if ($storage->instanceOfStorage(HomeObjectStoreStorage::class) || $storage->instanceOfStorage(Home::class)) {
175
-				if (is_object($storage->getUser())) {
176
-					$user = $storage->getUser();
177
-					return new Quota(['storage' => $storage, 'quotaCallback' => function () use ($user) {
178
-						return OC_Util::getUserQuota($user);
179
-					}, 'root' => 'files']);
180
-				}
181
-			}
182
-
183
-			return $storage;
184
-		});
185
-
186
-		Filesystem::addStorageWrapper('readonly', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
187
-			/*
66
+    private bool $rootSetup = false;
67
+    private IEventLogger $eventLogger;
68
+    private MountProviderCollection $mountProviderCollection;
69
+    private IMountManager $mountManager;
70
+    private IUserManager $userManager;
71
+    // List of users for which at least one mount is setup
72
+    private array $setupUsers = [];
73
+    // List of users for which all mounts are setup
74
+    private array $setupUsersComplete = [];
75
+    /** @var array<string, string[]> */
76
+    private array $setupUserMountProviders = [];
77
+    private IEventDispatcher $eventDispatcher;
78
+    private IUserMountCache $userMountCache;
79
+    private ILockdownManager $lockdownManager;
80
+    private IUserSession $userSession;
81
+    private ICache $cache;
82
+    private LoggerInterface $logger;
83
+    private IConfig $config;
84
+    private bool $listeningForProviders;
85
+    private array $fullSetupRequired = [];
86
+    private bool $setupBuiltinWrappersDone = false;
87
+
88
+    public function __construct(
89
+        IEventLogger $eventLogger,
90
+        MountProviderCollection $mountProviderCollection,
91
+        IMountManager $mountManager,
92
+        IUserManager $userManager,
93
+        IEventDispatcher $eventDispatcher,
94
+        IUserMountCache $userMountCache,
95
+        ILockdownManager $lockdownManager,
96
+        IUserSession $userSession,
97
+        ICacheFactory $cacheFactory,
98
+        LoggerInterface $logger,
99
+        IConfig $config
100
+    ) {
101
+        $this->eventLogger = $eventLogger;
102
+        $this->mountProviderCollection = $mountProviderCollection;
103
+        $this->mountManager = $mountManager;
104
+        $this->userManager = $userManager;
105
+        $this->eventDispatcher = $eventDispatcher;
106
+        $this->userMountCache = $userMountCache;
107
+        $this->lockdownManager = $lockdownManager;
108
+        $this->logger = $logger;
109
+        $this->userSession = $userSession;
110
+        $this->cache = $cacheFactory->createDistributed('setupmanager::');
111
+        $this->listeningForProviders = false;
112
+        $this->config = $config;
113
+
114
+        $this->setupListeners();
115
+    }
116
+
117
+    private function isSetupStarted(IUser $user): bool {
118
+        return in_array($user->getUID(), $this->setupUsers, true);
119
+    }
120
+
121
+    public function isSetupComplete(IUser $user): bool {
122
+        return in_array($user->getUID(), $this->setupUsersComplete, true);
123
+    }
124
+
125
+    private function setupBuiltinWrappers() {
126
+        if ($this->setupBuiltinWrappersDone) {
127
+            return;
128
+        }
129
+        $this->setupBuiltinWrappersDone = true;
130
+
131
+        // load all filesystem apps before, so no setup-hook gets lost
132
+        OC_App::loadApps(['filesystem']);
133
+        $prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false);
134
+
135
+        Filesystem::addStorageWrapper('mount_options', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
136
+            if ($storage->instanceOfStorage(Common::class)) {
137
+                $storage->setMountOptions($mount->getOptions());
138
+            }
139
+            return $storage;
140
+        });
141
+
142
+        Filesystem::addStorageWrapper('enable_sharing', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
143
+            if (!$mount->getOption('enable_sharing', true)) {
144
+                return new PermissionsMask([
145
+                    'storage' => $storage,
146
+                    'mask' => Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE,
147
+                ]);
148
+            }
149
+            return $storage;
150
+        });
151
+
152
+        // install storage availability wrapper, before most other wrappers
153
+        Filesystem::addStorageWrapper('oc_availability', function ($mountPoint, IStorage $storage) {
154
+            if (!$storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage') && !$storage->isLocal()) {
155
+                return new Availability(['storage' => $storage]);
156
+            }
157
+            return $storage;
158
+        });
159
+
160
+        Filesystem::addStorageWrapper('oc_encoding', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
161
+            if ($mount->getOption('encoding_compatibility', false) && !$storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) {
162
+                return new Encoding(['storage' => $storage]);
163
+            }
164
+            return $storage;
165
+        });
166
+
167
+        Filesystem::addStorageWrapper('oc_quota', function ($mountPoint, $storage) {
168
+            // set up quota for home storages, even for other users
169
+            // which can happen when using sharing
170
+
171
+            /**
172
+             * @var Storage $storage
173
+             */
174
+            if ($storage->instanceOfStorage(HomeObjectStoreStorage::class) || $storage->instanceOfStorage(Home::class)) {
175
+                if (is_object($storage->getUser())) {
176
+                    $user = $storage->getUser();
177
+                    return new Quota(['storage' => $storage, 'quotaCallback' => function () use ($user) {
178
+                        return OC_Util::getUserQuota($user);
179
+                    }, 'root' => 'files']);
180
+                }
181
+            }
182
+
183
+            return $storage;
184
+        });
185
+
186
+        Filesystem::addStorageWrapper('readonly', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
187
+            /*
188 188
 			 * Do not allow any operations that modify the storage
189 189
 			 */
190
-			if ($mount->getOption('readonly', false)) {
191
-				return new PermissionsMask([
192
-					'storage' => $storage,
193
-					'mask' => Constants::PERMISSION_ALL & ~(
194
-						Constants::PERMISSION_UPDATE |
195
-						Constants::PERMISSION_CREATE |
196
-						Constants::PERMISSION_DELETE
197
-					),
198
-				]);
199
-			}
200
-			return $storage;
201
-		});
202
-
203
-		Filesystem::logWarningWhenAddingStorageWrapper($prevLogging);
204
-	}
205
-
206
-	/**
207
-	 * Setup the full filesystem for the specified user
208
-	 */
209
-	public function setupForUser(IUser $user): void {
210
-		if ($this->isSetupComplete($user)) {
211
-			return;
212
-		}
213
-		$this->setupUsersComplete[] = $user->getUID();
214
-
215
-		$this->eventLogger->start('fs:setup:user:full', 'Setup full filesystem for user');
216
-
217
-		if (!isset($this->setupUserMountProviders[$user->getUID()])) {
218
-			$this->setupUserMountProviders[$user->getUID()] = [];
219
-		}
220
-
221
-		$previouslySetupProviders = $this->setupUserMountProviders[$user->getUID()];
222
-
223
-		$this->setupForUserWith($user, function () use ($user) {
224
-			$this->mountProviderCollection->addMountForUser($user, $this->mountManager, function (
225
-				IMountProvider $provider
226
-			) use ($user) {
227
-				return !in_array(get_class($provider), $this->setupUserMountProviders[$user->getUID()]);
228
-			});
229
-		});
230
-		$this->afterUserFullySetup($user, $previouslySetupProviders);
231
-		$this->eventLogger->end('fs:setup:user:full');
232
-	}
233
-
234
-	/**
235
-	 * part of the user setup that is run only once per user
236
-	 */
237
-	private function oneTimeUserSetup(IUser $user) {
238
-		if ($this->isSetupStarted($user)) {
239
-			return;
240
-		}
241
-		$this->setupUsers[] = $user->getUID();
242
-
243
-		$this->setupRoot();
244
-
245
-		$this->eventLogger->start('fs:setup:user:onetime', 'Onetime filesystem for user');
246
-
247
-		$this->setupBuiltinWrappers();
248
-
249
-		$prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false);
250
-
251
-		OC_Hook::emit('OC_Filesystem', 'preSetup', ['user' => $user->getUID()]);
252
-
253
-		Filesystem::logWarningWhenAddingStorageWrapper($prevLogging);
254
-
255
-		$userDir = '/' . $user->getUID() . '/files';
256
-
257
-		Filesystem::initInternal($userDir);
258
-
259
-		if ($this->lockdownManager->canAccessFilesystem()) {
260
-			$this->eventLogger->start('fs:setup:user:home', 'Setup home filesystem for user');
261
-			// home mounts are handled separate since we need to ensure this is mounted before we call the other mount providers
262
-			$homeMount = $this->mountProviderCollection->getHomeMountForUser($user);
263
-			$this->mountManager->addMount($homeMount);
264
-
265
-			if ($homeMount->getStorageRootId() === -1) {
266
-				$this->eventLogger->start('fs:setup:user:home:scan', 'Scan home filesystem for user');
267
-				$homeMount->getStorage()->mkdir('');
268
-				$homeMount->getStorage()->getScanner()->scan('');
269
-				$this->eventLogger->end('fs:setup:user:home:scan');
270
-			}
271
-			$this->eventLogger->end('fs:setup:user:home');
272
-		} else {
273
-			$this->mountManager->addMount(new MountPoint(
274
-				new NullStorage([]),
275
-				'/' . $user->getUID()
276
-			));
277
-			$this->mountManager->addMount(new MountPoint(
278
-				new NullStorage([]),
279
-				'/' . $user->getUID() . '/files'
280
-			));
281
-			$this->setupUsersComplete[] = $user->getUID();
282
-		}
283
-
284
-		$this->listenForNewMountProviders();
285
-
286
-		$this->eventLogger->end('fs:setup:user:onetime');
287
-	}
288
-
289
-	/**
290
-	 * Final housekeeping after a user has been fully setup
291
-	 */
292
-	private function afterUserFullySetup(IUser $user, array $previouslySetupProviders): void {
293
-		$this->eventLogger->start('fs:setup:user:full:post', 'Housekeeping after user is setup');
294
-		$userRoot = '/' . $user->getUID() . '/';
295
-		$mounts = $this->mountManager->getAll();
296
-		$mounts = array_filter($mounts, function (IMountPoint $mount) use ($userRoot) {
297
-			return str_starts_with($mount->getMountPoint(), $userRoot);
298
-		});
299
-		$allProviders = array_map(function (IMountProvider $provider) {
300
-			return get_class($provider);
301
-		}, $this->mountProviderCollection->getProviders());
302
-		$newProviders = array_diff($allProviders, $previouslySetupProviders);
303
-		$mounts = array_filter($mounts, function (IMountPoint $mount) use ($previouslySetupProviders) {
304
-			return !in_array($mount->getMountProvider(), $previouslySetupProviders);
305
-		});
306
-		$this->userMountCache->registerMounts($user, $mounts, $newProviders);
307
-
308
-		$cacheDuration = $this->config->getSystemValueInt('fs_mount_cache_duration', 5 * 60);
309
-		if ($cacheDuration > 0) {
310
-			$this->cache->set($user->getUID(), true, $cacheDuration);
311
-			$this->fullSetupRequired[$user->getUID()] = false;
312
-		}
313
-		$this->eventLogger->end('fs:setup:user:full:post');
314
-	}
315
-
316
-	/**
317
-	 * @param IUser $user
318
-	 * @param IMountPoint $mounts
319
-	 * @return void
320
-	 * @throws \OCP\HintException
321
-	 * @throws \OC\ServerNotAvailableException
322
-	 */
323
-	private function setupForUserWith(IUser $user, callable $mountCallback): void {
324
-		$this->oneTimeUserSetup($user);
325
-
326
-		if ($this->lockdownManager->canAccessFilesystem()) {
327
-			$mountCallback();
328
-		}
329
-		$this->eventLogger->start('fs:setup:user:post-init-mountpoint', 'post_initMountPoints legacy hook');
330
-		\OC_Hook::emit('OC_Filesystem', 'post_initMountPoints', ['user' => $user->getUID()]);
331
-		$this->eventLogger->end('fs:setup:user:post-init-mountpoint');
332
-
333
-		$userDir = '/' . $user->getUID() . '/files';
334
-		$this->eventLogger->start('fs:setup:user:setup-hook', 'setup legacy hook');
335
-		OC_Hook::emit('OC_Filesystem', 'setup', ['user' => $user->getUID(), 'user_dir' => $userDir]);
336
-		$this->eventLogger->end('fs:setup:user:setup-hook');
337
-	}
338
-
339
-	/**
340
-	 * Set up the root filesystem
341
-	 */
342
-	public function setupRoot(): void {
343
-		//setting up the filesystem twice can only lead to trouble
344
-		if ($this->rootSetup) {
345
-			return;
346
-		}
347
-		$this->rootSetup = true;
348
-
349
-		$this->eventLogger->start('fs:setup:root', 'Setup root filesystem');
350
-
351
-		$this->setupBuiltinWrappers();
352
-
353
-		$rootMounts = $this->mountProviderCollection->getRootMounts();
354
-		foreach ($rootMounts as $rootMountProvider) {
355
-			$this->mountManager->addMount($rootMountProvider);
356
-		}
357
-
358
-		$this->eventLogger->end('fs:setup:root');
359
-	}
360
-
361
-	/**
362
-	 * Get the user to setup for a path or `null` if the root needs to be setup
363
-	 *
364
-	 * @param string $path
365
-	 * @return IUser|null
366
-	 */
367
-	private function getUserForPath(string $path) {
368
-		if (str_starts_with($path, '/__groupfolders')) {
369
-			return null;
370
-		} elseif (substr_count($path, '/') < 2) {
371
-			if ($user = $this->userSession->getUser()) {
372
-				return $user;
373
-			} else {
374
-				return null;
375
-			}
376
-		} elseif (str_starts_with($path, '/appdata_' . \OC_Util::getInstanceId()) || str_starts_with($path, '/files_external/')) {
377
-			return null;
378
-		} else {
379
-			[, $userId] = explode('/', $path);
380
-		}
381
-
382
-		return $this->userManager->get($userId);
383
-	}
384
-
385
-	/**
386
-	 * Set up the filesystem for the specified path
387
-	 */
388
-	public function setupForPath(string $path, bool $includeChildren = false): void {
389
-		$user = $this->getUserForPath($path);
390
-		if (!$user) {
391
-			$this->setupRoot();
392
-			return;
393
-		}
394
-
395
-		if ($this->isSetupComplete($user)) {
396
-			return;
397
-		}
398
-
399
-		if ($this->fullSetupRequired($user)) {
400
-			$this->setupForUser($user);
401
-			return;
402
-		}
403
-
404
-		// for the user's home folder, and includes children we need everything always
405
-		if (rtrim($path) === "/" . $user->getUID() . "/files" && $includeChildren) {
406
-			$this->setupForUser($user);
407
-			return;
408
-		}
409
-
410
-		if (!isset($this->setupUserMountProviders[$user->getUID()])) {
411
-			$this->setupUserMountProviders[$user->getUID()] = [];
412
-		}
413
-		$setupProviders = &$this->setupUserMountProviders[$user->getUID()];
414
-		$currentProviders = [];
415
-
416
-		try {
417
-			$cachedMount = $this->userMountCache->getMountForPath($user, $path);
418
-		} catch (NotFoundException $e) {
419
-			$this->setupForUser($user);
420
-			return;
421
-		}
422
-
423
-		$this->oneTimeUserSetup($user);
424
-
425
-		$this->eventLogger->start('fs:setup:user:path', "Setup $path filesystem for user");
426
-		$this->eventLogger->start('fs:setup:user:path:find', "Find mountpoint for $path");
427
-
428
-		$mounts = [];
429
-		if (!in_array($cachedMount->getMountProvider(), $setupProviders)) {
430
-			$currentProviders[] = $cachedMount->getMountProvider();
431
-			if ($cachedMount->getMountProvider()) {
432
-				$setupProviders[] = $cachedMount->getMountProvider();
433
-				$mounts = $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()]);
434
-			} else {
435
-				$this->logger->debug("mount at " . $cachedMount->getMountPoint() . " has no provider set, performing full setup");
436
-				$this->eventLogger->end('fs:setup:user:path:find');
437
-				$this->setupForUser($user);
438
-				$this->eventLogger->end('fs:setup:user:path');
439
-				return;
440
-			}
441
-		}
442
-
443
-		if ($includeChildren) {
444
-			$subCachedMounts = $this->userMountCache->getMountsInPath($user, $path);
445
-			$this->eventLogger->end('fs:setup:user:path:find');
446
-
447
-			$needsFullSetup = array_reduce($subCachedMounts, function (bool $needsFullSetup, ICachedMountInfo $cachedMountInfo) {
448
-				return $needsFullSetup || $cachedMountInfo->getMountProvider() === '';
449
-			}, false);
450
-
451
-			if ($needsFullSetup) {
452
-				$this->logger->debug("mount has no provider set, performing full setup");
453
-				$this->setupForUser($user);
454
-				$this->eventLogger->end('fs:setup:user:path');
455
-				return;
456
-			} else {
457
-				foreach ($subCachedMounts as $cachedMount) {
458
-					if (!in_array($cachedMount->getMountProvider(), $setupProviders)) {
459
-						$currentProviders[] = $cachedMount->getMountProvider();
460
-						$setupProviders[] = $cachedMount->getMountProvider();
461
-						$mounts = array_merge($mounts, $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()]));
462
-					}
463
-				}
464
-			}
465
-		} else {
466
-			$this->eventLogger->end('fs:setup:user:path:find');
467
-		}
468
-
469
-		if (count($mounts)) {
470
-			$this->userMountCache->registerMounts($user, $mounts, $currentProviders);
471
-			$this->setupForUserWith($user, function () use ($mounts) {
472
-				array_walk($mounts, [$this->mountManager, 'addMount']);
473
-			});
474
-		} elseif (!$this->isSetupStarted($user)) {
475
-			$this->oneTimeUserSetup($user);
476
-		}
477
-		$this->eventLogger->end('fs:setup:user:path');
478
-	}
479
-
480
-	private function fullSetupRequired(IUser $user): bool {
481
-		// we perform a "cached" setup only after having done the full setup recently
482
-		// this is also used to trigger a full setup after handling events that are likely
483
-		// to change the available mounts
484
-		if (!isset($this->fullSetupRequired[$user->getUID()])) {
485
-			$this->fullSetupRequired[$user->getUID()] = !$this->cache->get($user->getUID());
486
-		}
487
-		return $this->fullSetupRequired[$user->getUID()];
488
-	}
489
-
490
-	/**
491
-	 * @param string $path
492
-	 * @param string[] $providers
493
-	 */
494
-	public function setupForProvider(string $path, array $providers): void {
495
-		$user = $this->getUserForPath($path);
496
-		if (!$user) {
497
-			$this->setupRoot();
498
-			return;
499
-		}
500
-
501
-		if ($this->isSetupComplete($user)) {
502
-			return;
503
-		}
504
-
505
-		if ($this->fullSetupRequired($user)) {
506
-			$this->setupForUser($user);
507
-			return;
508
-		}
509
-
510
-		$this->eventLogger->start('fs:setup:user:providers', "Setup filesystem for " . implode(', ', $providers));
511
-
512
-		$this->oneTimeUserSetup($user);
513
-
514
-		// home providers are always used
515
-		$providers = array_filter($providers, function (string $provider) {
516
-			return !is_subclass_of($provider, IHomeMountProvider::class);
517
-		});
518
-
519
-		if (in_array('', $providers)) {
520
-			$this->setupForUser($user);
521
-			return;
522
-		}
523
-		$setupProviders = $this->setupUserMountProviders[$user->getUID()] ?? [];
524
-
525
-		$providers = array_diff($providers, $setupProviders);
526
-		if (count($providers) === 0) {
527
-			if (!$this->isSetupStarted($user)) {
528
-				$this->oneTimeUserSetup($user);
529
-			}
530
-			$this->eventLogger->end('fs:setup:user:providers');
531
-			return;
532
-		} else {
533
-			$this->setupUserMountProviders[$user->getUID()] = array_merge($setupProviders, $providers);
534
-			$mounts = $this->mountProviderCollection->getUserMountsForProviderClasses($user, $providers);
535
-		}
536
-
537
-		$this->userMountCache->registerMounts($user, $mounts, $providers);
538
-		$this->setupForUserWith($user, function () use ($mounts) {
539
-			array_walk($mounts, [$this->mountManager, 'addMount']);
540
-		});
541
-		$this->eventLogger->end('fs:setup:user:providers');
542
-	}
543
-
544
-	public function tearDown() {
545
-		$this->setupUsers = [];
546
-		$this->setupUsersComplete = [];
547
-		$this->setupUserMountProviders = [];
548
-		$this->fullSetupRequired = [];
549
-		$this->rootSetup = false;
550
-		$this->mountManager->clear();
551
-		$this->eventDispatcher->dispatchTyped(new FilesystemTornDownEvent());
552
-	}
553
-
554
-	/**
555
-	 * Get mounts from mount providers that are registered after setup
556
-	 */
557
-	private function listenForNewMountProviders() {
558
-		if (!$this->listeningForProviders) {
559
-			$this->listeningForProviders = true;
560
-			$this->mountProviderCollection->listen('\OC\Files\Config', 'registerMountProvider', function (
561
-				IMountProvider $provider
562
-			) {
563
-				foreach ($this->setupUsers as $userId) {
564
-					$user = $this->userManager->get($userId);
565
-					if ($user) {
566
-						$mounts = $provider->getMountsForUser($user, Filesystem::getLoader());
567
-						array_walk($mounts, [$this->mountManager, 'addMount']);
568
-					}
569
-				}
570
-			});
571
-		}
572
-	}
573
-
574
-	private function setupListeners() {
575
-		// note that this event handling is intentionally pessimistic
576
-		// clearing the cache to often is better than not enough
577
-
578
-		$this->eventDispatcher->addListener(UserAddedEvent::class, function (UserAddedEvent $event) {
579
-			$this->cache->remove($event->getUser()->getUID());
580
-		});
581
-		$this->eventDispatcher->addListener(UserRemovedEvent::class, function (UserRemovedEvent $event) {
582
-			$this->cache->remove($event->getUser()->getUID());
583
-		});
584
-		$this->eventDispatcher->addListener(ShareCreatedEvent::class, function (ShareCreatedEvent $event) {
585
-			$this->cache->remove($event->getShare()->getSharedWith());
586
-		});
587
-		$this->eventDispatcher->addListener(InvalidateMountCacheEvent::class, function (InvalidateMountCacheEvent $event
588
-		) {
589
-			if ($user = $event->getUser()) {
590
-				$this->cache->remove($user->getUID());
591
-			} else {
592
-				$this->cache->clear();
593
-			}
594
-		});
595
-
596
-		$genericEvents = [
597
-			'OCA\Circles\Events\CreatingCircleEvent',
598
-			'OCA\Circles\Events\DestroyingCircleEvent',
599
-			'OCA\Circles\Events\AddingCircleMemberEvent',
600
-			'OCA\Circles\Events\RemovingCircleMemberEvent',
601
-		];
602
-
603
-		foreach ($genericEvents as $genericEvent) {
604
-			$this->eventDispatcher->addListener($genericEvent, function ($event) {
605
-				$this->cache->clear();
606
-			});
607
-		}
608
-	}
190
+            if ($mount->getOption('readonly', false)) {
191
+                return new PermissionsMask([
192
+                    'storage' => $storage,
193
+                    'mask' => Constants::PERMISSION_ALL & ~(
194
+                        Constants::PERMISSION_UPDATE |
195
+                        Constants::PERMISSION_CREATE |
196
+                        Constants::PERMISSION_DELETE
197
+                    ),
198
+                ]);
199
+            }
200
+            return $storage;
201
+        });
202
+
203
+        Filesystem::logWarningWhenAddingStorageWrapper($prevLogging);
204
+    }
205
+
206
+    /**
207
+     * Setup the full filesystem for the specified user
208
+     */
209
+    public function setupForUser(IUser $user): void {
210
+        if ($this->isSetupComplete($user)) {
211
+            return;
212
+        }
213
+        $this->setupUsersComplete[] = $user->getUID();
214
+
215
+        $this->eventLogger->start('fs:setup:user:full', 'Setup full filesystem for user');
216
+
217
+        if (!isset($this->setupUserMountProviders[$user->getUID()])) {
218
+            $this->setupUserMountProviders[$user->getUID()] = [];
219
+        }
220
+
221
+        $previouslySetupProviders = $this->setupUserMountProviders[$user->getUID()];
222
+
223
+        $this->setupForUserWith($user, function () use ($user) {
224
+            $this->mountProviderCollection->addMountForUser($user, $this->mountManager, function (
225
+                IMountProvider $provider
226
+            ) use ($user) {
227
+                return !in_array(get_class($provider), $this->setupUserMountProviders[$user->getUID()]);
228
+            });
229
+        });
230
+        $this->afterUserFullySetup($user, $previouslySetupProviders);
231
+        $this->eventLogger->end('fs:setup:user:full');
232
+    }
233
+
234
+    /**
235
+     * part of the user setup that is run only once per user
236
+     */
237
+    private function oneTimeUserSetup(IUser $user) {
238
+        if ($this->isSetupStarted($user)) {
239
+            return;
240
+        }
241
+        $this->setupUsers[] = $user->getUID();
242
+
243
+        $this->setupRoot();
244
+
245
+        $this->eventLogger->start('fs:setup:user:onetime', 'Onetime filesystem for user');
246
+
247
+        $this->setupBuiltinWrappers();
248
+
249
+        $prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false);
250
+
251
+        OC_Hook::emit('OC_Filesystem', 'preSetup', ['user' => $user->getUID()]);
252
+
253
+        Filesystem::logWarningWhenAddingStorageWrapper($prevLogging);
254
+
255
+        $userDir = '/' . $user->getUID() . '/files';
256
+
257
+        Filesystem::initInternal($userDir);
258
+
259
+        if ($this->lockdownManager->canAccessFilesystem()) {
260
+            $this->eventLogger->start('fs:setup:user:home', 'Setup home filesystem for user');
261
+            // home mounts are handled separate since we need to ensure this is mounted before we call the other mount providers
262
+            $homeMount = $this->mountProviderCollection->getHomeMountForUser($user);
263
+            $this->mountManager->addMount($homeMount);
264
+
265
+            if ($homeMount->getStorageRootId() === -1) {
266
+                $this->eventLogger->start('fs:setup:user:home:scan', 'Scan home filesystem for user');
267
+                $homeMount->getStorage()->mkdir('');
268
+                $homeMount->getStorage()->getScanner()->scan('');
269
+                $this->eventLogger->end('fs:setup:user:home:scan');
270
+            }
271
+            $this->eventLogger->end('fs:setup:user:home');
272
+        } else {
273
+            $this->mountManager->addMount(new MountPoint(
274
+                new NullStorage([]),
275
+                '/' . $user->getUID()
276
+            ));
277
+            $this->mountManager->addMount(new MountPoint(
278
+                new NullStorage([]),
279
+                '/' . $user->getUID() . '/files'
280
+            ));
281
+            $this->setupUsersComplete[] = $user->getUID();
282
+        }
283
+
284
+        $this->listenForNewMountProviders();
285
+
286
+        $this->eventLogger->end('fs:setup:user:onetime');
287
+    }
288
+
289
+    /**
290
+     * Final housekeeping after a user has been fully setup
291
+     */
292
+    private function afterUserFullySetup(IUser $user, array $previouslySetupProviders): void {
293
+        $this->eventLogger->start('fs:setup:user:full:post', 'Housekeeping after user is setup');
294
+        $userRoot = '/' . $user->getUID() . '/';
295
+        $mounts = $this->mountManager->getAll();
296
+        $mounts = array_filter($mounts, function (IMountPoint $mount) use ($userRoot) {
297
+            return str_starts_with($mount->getMountPoint(), $userRoot);
298
+        });
299
+        $allProviders = array_map(function (IMountProvider $provider) {
300
+            return get_class($provider);
301
+        }, $this->mountProviderCollection->getProviders());
302
+        $newProviders = array_diff($allProviders, $previouslySetupProviders);
303
+        $mounts = array_filter($mounts, function (IMountPoint $mount) use ($previouslySetupProviders) {
304
+            return !in_array($mount->getMountProvider(), $previouslySetupProviders);
305
+        });
306
+        $this->userMountCache->registerMounts($user, $mounts, $newProviders);
307
+
308
+        $cacheDuration = $this->config->getSystemValueInt('fs_mount_cache_duration', 5 * 60);
309
+        if ($cacheDuration > 0) {
310
+            $this->cache->set($user->getUID(), true, $cacheDuration);
311
+            $this->fullSetupRequired[$user->getUID()] = false;
312
+        }
313
+        $this->eventLogger->end('fs:setup:user:full:post');
314
+    }
315
+
316
+    /**
317
+     * @param IUser $user
318
+     * @param IMountPoint $mounts
319
+     * @return void
320
+     * @throws \OCP\HintException
321
+     * @throws \OC\ServerNotAvailableException
322
+     */
323
+    private function setupForUserWith(IUser $user, callable $mountCallback): void {
324
+        $this->oneTimeUserSetup($user);
325
+
326
+        if ($this->lockdownManager->canAccessFilesystem()) {
327
+            $mountCallback();
328
+        }
329
+        $this->eventLogger->start('fs:setup:user:post-init-mountpoint', 'post_initMountPoints legacy hook');
330
+        \OC_Hook::emit('OC_Filesystem', 'post_initMountPoints', ['user' => $user->getUID()]);
331
+        $this->eventLogger->end('fs:setup:user:post-init-mountpoint');
332
+
333
+        $userDir = '/' . $user->getUID() . '/files';
334
+        $this->eventLogger->start('fs:setup:user:setup-hook', 'setup legacy hook');
335
+        OC_Hook::emit('OC_Filesystem', 'setup', ['user' => $user->getUID(), 'user_dir' => $userDir]);
336
+        $this->eventLogger->end('fs:setup:user:setup-hook');
337
+    }
338
+
339
+    /**
340
+     * Set up the root filesystem
341
+     */
342
+    public function setupRoot(): void {
343
+        //setting up the filesystem twice can only lead to trouble
344
+        if ($this->rootSetup) {
345
+            return;
346
+        }
347
+        $this->rootSetup = true;
348
+
349
+        $this->eventLogger->start('fs:setup:root', 'Setup root filesystem');
350
+
351
+        $this->setupBuiltinWrappers();
352
+
353
+        $rootMounts = $this->mountProviderCollection->getRootMounts();
354
+        foreach ($rootMounts as $rootMountProvider) {
355
+            $this->mountManager->addMount($rootMountProvider);
356
+        }
357
+
358
+        $this->eventLogger->end('fs:setup:root');
359
+    }
360
+
361
+    /**
362
+     * Get the user to setup for a path or `null` if the root needs to be setup
363
+     *
364
+     * @param string $path
365
+     * @return IUser|null
366
+     */
367
+    private function getUserForPath(string $path) {
368
+        if (str_starts_with($path, '/__groupfolders')) {
369
+            return null;
370
+        } elseif (substr_count($path, '/') < 2) {
371
+            if ($user = $this->userSession->getUser()) {
372
+                return $user;
373
+            } else {
374
+                return null;
375
+            }
376
+        } elseif (str_starts_with($path, '/appdata_' . \OC_Util::getInstanceId()) || str_starts_with($path, '/files_external/')) {
377
+            return null;
378
+        } else {
379
+            [, $userId] = explode('/', $path);
380
+        }
381
+
382
+        return $this->userManager->get($userId);
383
+    }
384
+
385
+    /**
386
+     * Set up the filesystem for the specified path
387
+     */
388
+    public function setupForPath(string $path, bool $includeChildren = false): void {
389
+        $user = $this->getUserForPath($path);
390
+        if (!$user) {
391
+            $this->setupRoot();
392
+            return;
393
+        }
394
+
395
+        if ($this->isSetupComplete($user)) {
396
+            return;
397
+        }
398
+
399
+        if ($this->fullSetupRequired($user)) {
400
+            $this->setupForUser($user);
401
+            return;
402
+        }
403
+
404
+        // for the user's home folder, and includes children we need everything always
405
+        if (rtrim($path) === "/" . $user->getUID() . "/files" && $includeChildren) {
406
+            $this->setupForUser($user);
407
+            return;
408
+        }
409
+
410
+        if (!isset($this->setupUserMountProviders[$user->getUID()])) {
411
+            $this->setupUserMountProviders[$user->getUID()] = [];
412
+        }
413
+        $setupProviders = &$this->setupUserMountProviders[$user->getUID()];
414
+        $currentProviders = [];
415
+
416
+        try {
417
+            $cachedMount = $this->userMountCache->getMountForPath($user, $path);
418
+        } catch (NotFoundException $e) {
419
+            $this->setupForUser($user);
420
+            return;
421
+        }
422
+
423
+        $this->oneTimeUserSetup($user);
424
+
425
+        $this->eventLogger->start('fs:setup:user:path', "Setup $path filesystem for user");
426
+        $this->eventLogger->start('fs:setup:user:path:find', "Find mountpoint for $path");
427
+
428
+        $mounts = [];
429
+        if (!in_array($cachedMount->getMountProvider(), $setupProviders)) {
430
+            $currentProviders[] = $cachedMount->getMountProvider();
431
+            if ($cachedMount->getMountProvider()) {
432
+                $setupProviders[] = $cachedMount->getMountProvider();
433
+                $mounts = $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()]);
434
+            } else {
435
+                $this->logger->debug("mount at " . $cachedMount->getMountPoint() . " has no provider set, performing full setup");
436
+                $this->eventLogger->end('fs:setup:user:path:find');
437
+                $this->setupForUser($user);
438
+                $this->eventLogger->end('fs:setup:user:path');
439
+                return;
440
+            }
441
+        }
442
+
443
+        if ($includeChildren) {
444
+            $subCachedMounts = $this->userMountCache->getMountsInPath($user, $path);
445
+            $this->eventLogger->end('fs:setup:user:path:find');
446
+
447
+            $needsFullSetup = array_reduce($subCachedMounts, function (bool $needsFullSetup, ICachedMountInfo $cachedMountInfo) {
448
+                return $needsFullSetup || $cachedMountInfo->getMountProvider() === '';
449
+            }, false);
450
+
451
+            if ($needsFullSetup) {
452
+                $this->logger->debug("mount has no provider set, performing full setup");
453
+                $this->setupForUser($user);
454
+                $this->eventLogger->end('fs:setup:user:path');
455
+                return;
456
+            } else {
457
+                foreach ($subCachedMounts as $cachedMount) {
458
+                    if (!in_array($cachedMount->getMountProvider(), $setupProviders)) {
459
+                        $currentProviders[] = $cachedMount->getMountProvider();
460
+                        $setupProviders[] = $cachedMount->getMountProvider();
461
+                        $mounts = array_merge($mounts, $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()]));
462
+                    }
463
+                }
464
+            }
465
+        } else {
466
+            $this->eventLogger->end('fs:setup:user:path:find');
467
+        }
468
+
469
+        if (count($mounts)) {
470
+            $this->userMountCache->registerMounts($user, $mounts, $currentProviders);
471
+            $this->setupForUserWith($user, function () use ($mounts) {
472
+                array_walk($mounts, [$this->mountManager, 'addMount']);
473
+            });
474
+        } elseif (!$this->isSetupStarted($user)) {
475
+            $this->oneTimeUserSetup($user);
476
+        }
477
+        $this->eventLogger->end('fs:setup:user:path');
478
+    }
479
+
480
+    private function fullSetupRequired(IUser $user): bool {
481
+        // we perform a "cached" setup only after having done the full setup recently
482
+        // this is also used to trigger a full setup after handling events that are likely
483
+        // to change the available mounts
484
+        if (!isset($this->fullSetupRequired[$user->getUID()])) {
485
+            $this->fullSetupRequired[$user->getUID()] = !$this->cache->get($user->getUID());
486
+        }
487
+        return $this->fullSetupRequired[$user->getUID()];
488
+    }
489
+
490
+    /**
491
+     * @param string $path
492
+     * @param string[] $providers
493
+     */
494
+    public function setupForProvider(string $path, array $providers): void {
495
+        $user = $this->getUserForPath($path);
496
+        if (!$user) {
497
+            $this->setupRoot();
498
+            return;
499
+        }
500
+
501
+        if ($this->isSetupComplete($user)) {
502
+            return;
503
+        }
504
+
505
+        if ($this->fullSetupRequired($user)) {
506
+            $this->setupForUser($user);
507
+            return;
508
+        }
509
+
510
+        $this->eventLogger->start('fs:setup:user:providers', "Setup filesystem for " . implode(', ', $providers));
511
+
512
+        $this->oneTimeUserSetup($user);
513
+
514
+        // home providers are always used
515
+        $providers = array_filter($providers, function (string $provider) {
516
+            return !is_subclass_of($provider, IHomeMountProvider::class);
517
+        });
518
+
519
+        if (in_array('', $providers)) {
520
+            $this->setupForUser($user);
521
+            return;
522
+        }
523
+        $setupProviders = $this->setupUserMountProviders[$user->getUID()] ?? [];
524
+
525
+        $providers = array_diff($providers, $setupProviders);
526
+        if (count($providers) === 0) {
527
+            if (!$this->isSetupStarted($user)) {
528
+                $this->oneTimeUserSetup($user);
529
+            }
530
+            $this->eventLogger->end('fs:setup:user:providers');
531
+            return;
532
+        } else {
533
+            $this->setupUserMountProviders[$user->getUID()] = array_merge($setupProviders, $providers);
534
+            $mounts = $this->mountProviderCollection->getUserMountsForProviderClasses($user, $providers);
535
+        }
536
+
537
+        $this->userMountCache->registerMounts($user, $mounts, $providers);
538
+        $this->setupForUserWith($user, function () use ($mounts) {
539
+            array_walk($mounts, [$this->mountManager, 'addMount']);
540
+        });
541
+        $this->eventLogger->end('fs:setup:user:providers');
542
+    }
543
+
544
+    public function tearDown() {
545
+        $this->setupUsers = [];
546
+        $this->setupUsersComplete = [];
547
+        $this->setupUserMountProviders = [];
548
+        $this->fullSetupRequired = [];
549
+        $this->rootSetup = false;
550
+        $this->mountManager->clear();
551
+        $this->eventDispatcher->dispatchTyped(new FilesystemTornDownEvent());
552
+    }
553
+
554
+    /**
555
+     * Get mounts from mount providers that are registered after setup
556
+     */
557
+    private function listenForNewMountProviders() {
558
+        if (!$this->listeningForProviders) {
559
+            $this->listeningForProviders = true;
560
+            $this->mountProviderCollection->listen('\OC\Files\Config', 'registerMountProvider', function (
561
+                IMountProvider $provider
562
+            ) {
563
+                foreach ($this->setupUsers as $userId) {
564
+                    $user = $this->userManager->get($userId);
565
+                    if ($user) {
566
+                        $mounts = $provider->getMountsForUser($user, Filesystem::getLoader());
567
+                        array_walk($mounts, [$this->mountManager, 'addMount']);
568
+                    }
569
+                }
570
+            });
571
+        }
572
+    }
573
+
574
+    private function setupListeners() {
575
+        // note that this event handling is intentionally pessimistic
576
+        // clearing the cache to often is better than not enough
577
+
578
+        $this->eventDispatcher->addListener(UserAddedEvent::class, function (UserAddedEvent $event) {
579
+            $this->cache->remove($event->getUser()->getUID());
580
+        });
581
+        $this->eventDispatcher->addListener(UserRemovedEvent::class, function (UserRemovedEvent $event) {
582
+            $this->cache->remove($event->getUser()->getUID());
583
+        });
584
+        $this->eventDispatcher->addListener(ShareCreatedEvent::class, function (ShareCreatedEvent $event) {
585
+            $this->cache->remove($event->getShare()->getSharedWith());
586
+        });
587
+        $this->eventDispatcher->addListener(InvalidateMountCacheEvent::class, function (InvalidateMountCacheEvent $event
588
+        ) {
589
+            if ($user = $event->getUser()) {
590
+                $this->cache->remove($user->getUID());
591
+            } else {
592
+                $this->cache->clear();
593
+            }
594
+        });
595
+
596
+        $genericEvents = [
597
+            'OCA\Circles\Events\CreatingCircleEvent',
598
+            'OCA\Circles\Events\DestroyingCircleEvent',
599
+            'OCA\Circles\Events\AddingCircleMemberEvent',
600
+            'OCA\Circles\Events\RemovingCircleMemberEvent',
601
+        ];
602
+
603
+        foreach ($genericEvents as $genericEvent) {
604
+            $this->eventDispatcher->addListener($genericEvent, function ($event) {
605
+                $this->cache->clear();
606
+            });
607
+        }
608
+    }
609 609
 }
Please login to merge, or discard this patch.
Spacing   +31 added lines, -31 removed lines patch added patch discarded remove patch
@@ -132,14 +132,14 @@  discard block
 block discarded – undo
132 132
 		OC_App::loadApps(['filesystem']);
133 133
 		$prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false);
134 134
 
135
-		Filesystem::addStorageWrapper('mount_options', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
135
+		Filesystem::addStorageWrapper('mount_options', function($mountPoint, IStorage $storage, IMountPoint $mount) {
136 136
 			if ($storage->instanceOfStorage(Common::class)) {
137 137
 				$storage->setMountOptions($mount->getOptions());
138 138
 			}
139 139
 			return $storage;
140 140
 		});
141 141
 
142
-		Filesystem::addStorageWrapper('enable_sharing', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
142
+		Filesystem::addStorageWrapper('enable_sharing', function($mountPoint, IStorage $storage, IMountPoint $mount) {
143 143
 			if (!$mount->getOption('enable_sharing', true)) {
144 144
 				return new PermissionsMask([
145 145
 					'storage' => $storage,
@@ -150,21 +150,21 @@  discard block
 block discarded – undo
150 150
 		});
151 151
 
152 152
 		// install storage availability wrapper, before most other wrappers
153
-		Filesystem::addStorageWrapper('oc_availability', function ($mountPoint, IStorage $storage) {
153
+		Filesystem::addStorageWrapper('oc_availability', function($mountPoint, IStorage $storage) {
154 154
 			if (!$storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage') && !$storage->isLocal()) {
155 155
 				return new Availability(['storage' => $storage]);
156 156
 			}
157 157
 			return $storage;
158 158
 		});
159 159
 
160
-		Filesystem::addStorageWrapper('oc_encoding', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
160
+		Filesystem::addStorageWrapper('oc_encoding', function($mountPoint, IStorage $storage, IMountPoint $mount) {
161 161
 			if ($mount->getOption('encoding_compatibility', false) && !$storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) {
162 162
 				return new Encoding(['storage' => $storage]);
163 163
 			}
164 164
 			return $storage;
165 165
 		});
166 166
 
167
-		Filesystem::addStorageWrapper('oc_quota', function ($mountPoint, $storage) {
167
+		Filesystem::addStorageWrapper('oc_quota', function($mountPoint, $storage) {
168 168
 			// set up quota for home storages, even for other users
169 169
 			// which can happen when using sharing
170 170
 
@@ -174,7 +174,7 @@  discard block
 block discarded – undo
174 174
 			if ($storage->instanceOfStorage(HomeObjectStoreStorage::class) || $storage->instanceOfStorage(Home::class)) {
175 175
 				if (is_object($storage->getUser())) {
176 176
 					$user = $storage->getUser();
177
-					return new Quota(['storage' => $storage, 'quotaCallback' => function () use ($user) {
177
+					return new Quota(['storage' => $storage, 'quotaCallback' => function() use ($user) {
178 178
 						return OC_Util::getUserQuota($user);
179 179
 					}, 'root' => 'files']);
180 180
 				}
@@ -183,7 +183,7 @@  discard block
 block discarded – undo
183 183
 			return $storage;
184 184
 		});
185 185
 
186
-		Filesystem::addStorageWrapper('readonly', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
186
+		Filesystem::addStorageWrapper('readonly', function($mountPoint, IStorage $storage, IMountPoint $mount) {
187 187
 			/*
188 188
 			 * Do not allow any operations that modify the storage
189 189
 			 */
@@ -220,8 +220,8 @@  discard block
 block discarded – undo
220 220
 
221 221
 		$previouslySetupProviders = $this->setupUserMountProviders[$user->getUID()];
222 222
 
223
-		$this->setupForUserWith($user, function () use ($user) {
224
-			$this->mountProviderCollection->addMountForUser($user, $this->mountManager, function (
223
+		$this->setupForUserWith($user, function() use ($user) {
224
+			$this->mountProviderCollection->addMountForUser($user, $this->mountManager, function(
225 225
 				IMountProvider $provider
226 226
 			) use ($user) {
227 227
 				return !in_array(get_class($provider), $this->setupUserMountProviders[$user->getUID()]);
@@ -252,7 +252,7 @@  discard block
 block discarded – undo
252 252
 
253 253
 		Filesystem::logWarningWhenAddingStorageWrapper($prevLogging);
254 254
 
255
-		$userDir = '/' . $user->getUID() . '/files';
255
+		$userDir = '/'.$user->getUID().'/files';
256 256
 
257 257
 		Filesystem::initInternal($userDir);
258 258
 
@@ -272,11 +272,11 @@  discard block
 block discarded – undo
272 272
 		} else {
273 273
 			$this->mountManager->addMount(new MountPoint(
274 274
 				new NullStorage([]),
275
-				'/' . $user->getUID()
275
+				'/'.$user->getUID()
276 276
 			));
277 277
 			$this->mountManager->addMount(new MountPoint(
278 278
 				new NullStorage([]),
279
-				'/' . $user->getUID() . '/files'
279
+				'/'.$user->getUID().'/files'
280 280
 			));
281 281
 			$this->setupUsersComplete[] = $user->getUID();
282 282
 		}
@@ -291,16 +291,16 @@  discard block
 block discarded – undo
291 291
 	 */
292 292
 	private function afterUserFullySetup(IUser $user, array $previouslySetupProviders): void {
293 293
 		$this->eventLogger->start('fs:setup:user:full:post', 'Housekeeping after user is setup');
294
-		$userRoot = '/' . $user->getUID() . '/';
294
+		$userRoot = '/'.$user->getUID().'/';
295 295
 		$mounts = $this->mountManager->getAll();
296
-		$mounts = array_filter($mounts, function (IMountPoint $mount) use ($userRoot) {
296
+		$mounts = array_filter($mounts, function(IMountPoint $mount) use ($userRoot) {
297 297
 			return str_starts_with($mount->getMountPoint(), $userRoot);
298 298
 		});
299
-		$allProviders = array_map(function (IMountProvider $provider) {
299
+		$allProviders = array_map(function(IMountProvider $provider) {
300 300
 			return get_class($provider);
301 301
 		}, $this->mountProviderCollection->getProviders());
302 302
 		$newProviders = array_diff($allProviders, $previouslySetupProviders);
303
-		$mounts = array_filter($mounts, function (IMountPoint $mount) use ($previouslySetupProviders) {
303
+		$mounts = array_filter($mounts, function(IMountPoint $mount) use ($previouslySetupProviders) {
304 304
 			return !in_array($mount->getMountProvider(), $previouslySetupProviders);
305 305
 		});
306 306
 		$this->userMountCache->registerMounts($user, $mounts, $newProviders);
@@ -330,7 +330,7 @@  discard block
 block discarded – undo
330 330
 		\OC_Hook::emit('OC_Filesystem', 'post_initMountPoints', ['user' => $user->getUID()]);
331 331
 		$this->eventLogger->end('fs:setup:user:post-init-mountpoint');
332 332
 
333
-		$userDir = '/' . $user->getUID() . '/files';
333
+		$userDir = '/'.$user->getUID().'/files';
334 334
 		$this->eventLogger->start('fs:setup:user:setup-hook', 'setup legacy hook');
335 335
 		OC_Hook::emit('OC_Filesystem', 'setup', ['user' => $user->getUID(), 'user_dir' => $userDir]);
336 336
 		$this->eventLogger->end('fs:setup:user:setup-hook');
@@ -373,7 +373,7 @@  discard block
 block discarded – undo
373 373
 			} else {
374 374
 				return null;
375 375
 			}
376
-		} elseif (str_starts_with($path, '/appdata_' . \OC_Util::getInstanceId()) || str_starts_with($path, '/files_external/')) {
376
+		} elseif (str_starts_with($path, '/appdata_'.\OC_Util::getInstanceId()) || str_starts_with($path, '/files_external/')) {
377 377
 			return null;
378 378
 		} else {
379 379
 			[, $userId] = explode('/', $path);
@@ -402,7 +402,7 @@  discard block
 block discarded – undo
402 402
 		}
403 403
 
404 404
 		// for the user's home folder, and includes children we need everything always
405
-		if (rtrim($path) === "/" . $user->getUID() . "/files" && $includeChildren) {
405
+		if (rtrim($path) === "/".$user->getUID()."/files" && $includeChildren) {
406 406
 			$this->setupForUser($user);
407 407
 			return;
408 408
 		}
@@ -432,7 +432,7 @@  discard block
 block discarded – undo
432 432
 				$setupProviders[] = $cachedMount->getMountProvider();
433 433
 				$mounts = $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()]);
434 434
 			} else {
435
-				$this->logger->debug("mount at " . $cachedMount->getMountPoint() . " has no provider set, performing full setup");
435
+				$this->logger->debug("mount at ".$cachedMount->getMountPoint()." has no provider set, performing full setup");
436 436
 				$this->eventLogger->end('fs:setup:user:path:find');
437 437
 				$this->setupForUser($user);
438 438
 				$this->eventLogger->end('fs:setup:user:path');
@@ -444,7 +444,7 @@  discard block
 block discarded – undo
444 444
 			$subCachedMounts = $this->userMountCache->getMountsInPath($user, $path);
445 445
 			$this->eventLogger->end('fs:setup:user:path:find');
446 446
 
447
-			$needsFullSetup = array_reduce($subCachedMounts, function (bool $needsFullSetup, ICachedMountInfo $cachedMountInfo) {
447
+			$needsFullSetup = array_reduce($subCachedMounts, function(bool $needsFullSetup, ICachedMountInfo $cachedMountInfo) {
448 448
 				return $needsFullSetup || $cachedMountInfo->getMountProvider() === '';
449 449
 			}, false);
450 450
 
@@ -468,7 +468,7 @@  discard block
 block discarded – undo
468 468
 
469 469
 		if (count($mounts)) {
470 470
 			$this->userMountCache->registerMounts($user, $mounts, $currentProviders);
471
-			$this->setupForUserWith($user, function () use ($mounts) {
471
+			$this->setupForUserWith($user, function() use ($mounts) {
472 472
 				array_walk($mounts, [$this->mountManager, 'addMount']);
473 473
 			});
474 474
 		} elseif (!$this->isSetupStarted($user)) {
@@ -507,12 +507,12 @@  discard block
 block discarded – undo
507 507
 			return;
508 508
 		}
509 509
 
510
-		$this->eventLogger->start('fs:setup:user:providers', "Setup filesystem for " . implode(', ', $providers));
510
+		$this->eventLogger->start('fs:setup:user:providers', "Setup filesystem for ".implode(', ', $providers));
511 511
 
512 512
 		$this->oneTimeUserSetup($user);
513 513
 
514 514
 		// home providers are always used
515
-		$providers = array_filter($providers, function (string $provider) {
515
+		$providers = array_filter($providers, function(string $provider) {
516 516
 			return !is_subclass_of($provider, IHomeMountProvider::class);
517 517
 		});
518 518
 
@@ -535,7 +535,7 @@  discard block
 block discarded – undo
535 535
 		}
536 536
 
537 537
 		$this->userMountCache->registerMounts($user, $mounts, $providers);
538
-		$this->setupForUserWith($user, function () use ($mounts) {
538
+		$this->setupForUserWith($user, function() use ($mounts) {
539 539
 			array_walk($mounts, [$this->mountManager, 'addMount']);
540 540
 		});
541 541
 		$this->eventLogger->end('fs:setup:user:providers');
@@ -557,7 +557,7 @@  discard block
 block discarded – undo
557 557
 	private function listenForNewMountProviders() {
558 558
 		if (!$this->listeningForProviders) {
559 559
 			$this->listeningForProviders = true;
560
-			$this->mountProviderCollection->listen('\OC\Files\Config', 'registerMountProvider', function (
560
+			$this->mountProviderCollection->listen('\OC\Files\Config', 'registerMountProvider', function(
561 561
 				IMountProvider $provider
562 562
 			) {
563 563
 				foreach ($this->setupUsers as $userId) {
@@ -575,16 +575,16 @@  discard block
 block discarded – undo
575 575
 		// note that this event handling is intentionally pessimistic
576 576
 		// clearing the cache to often is better than not enough
577 577
 
578
-		$this->eventDispatcher->addListener(UserAddedEvent::class, function (UserAddedEvent $event) {
578
+		$this->eventDispatcher->addListener(UserAddedEvent::class, function(UserAddedEvent $event) {
579 579
 			$this->cache->remove($event->getUser()->getUID());
580 580
 		});
581
-		$this->eventDispatcher->addListener(UserRemovedEvent::class, function (UserRemovedEvent $event) {
581
+		$this->eventDispatcher->addListener(UserRemovedEvent::class, function(UserRemovedEvent $event) {
582 582
 			$this->cache->remove($event->getUser()->getUID());
583 583
 		});
584
-		$this->eventDispatcher->addListener(ShareCreatedEvent::class, function (ShareCreatedEvent $event) {
584
+		$this->eventDispatcher->addListener(ShareCreatedEvent::class, function(ShareCreatedEvent $event) {
585 585
 			$this->cache->remove($event->getShare()->getSharedWith());
586 586
 		});
587
-		$this->eventDispatcher->addListener(InvalidateMountCacheEvent::class, function (InvalidateMountCacheEvent $event
587
+		$this->eventDispatcher->addListener(InvalidateMountCacheEvent::class, function(InvalidateMountCacheEvent $event
588 588
 		) {
589 589
 			if ($user = $event->getUser()) {
590 590
 				$this->cache->remove($user->getUID());
@@ -601,7 +601,7 @@  discard block
 block discarded – undo
601 601
 		];
602 602
 
603 603
 		foreach ($genericEvents as $genericEvent) {
604
-			$this->eventDispatcher->addListener($genericEvent, function ($event) {
604
+			$this->eventDispatcher->addListener($genericEvent, function($event) {
605 605
 				$this->cache->clear();
606 606
 			});
607 607
 		}
Please login to merge, or discard this patch.
lib/private/Files/Config/UserMountCache.php 2 patches
Indentation   +444 added lines, -444 removed lines patch added patch discarded remove patch
@@ -46,448 +46,448 @@
 block discarded – undo
46 46
  * Cache mounts points per user in the cache so we can easily look them up
47 47
  */
48 48
 class UserMountCache implements IUserMountCache {
49
-	private IDBConnection $connection;
50
-	private IUserManager $userManager;
51
-
52
-	/**
53
-	 * Cached mount info.
54
-	 * @var CappedMemoryCache<ICachedMountInfo[]>
55
-	 **/
56
-	private CappedMemoryCache $mountsForUsers;
57
-	private LoggerInterface $logger;
58
-	/** @var CappedMemoryCache<array> */
59
-	private CappedMemoryCache $cacheInfoCache;
60
-	private IEventLogger $eventLogger;
61
-
62
-	/**
63
-	 * UserMountCache constructor.
64
-	 */
65
-	public function __construct(
66
-		IDBConnection $connection,
67
-		IUserManager $userManager,
68
-		LoggerInterface $logger,
69
-		IEventLogger $eventLogger
70
-	) {
71
-		$this->connection = $connection;
72
-		$this->userManager = $userManager;
73
-		$this->logger = $logger;
74
-		$this->eventLogger = $eventLogger;
75
-		$this->cacheInfoCache = new CappedMemoryCache();
76
-		$this->mountsForUsers = new CappedMemoryCache();
77
-	}
78
-
79
-	public function registerMounts(IUser $user, array $mounts, array $mountProviderClasses = null) {
80
-		$this->eventLogger->start('fs:setup:user:register', 'Registering mounts for user');
81
-		// filter out non-proper storages coming from unit tests
82
-		$mounts = array_filter($mounts, function (IMountPoint $mount) {
83
-			return $mount instanceof SharedMount || ($mount->getStorage() && $mount->getStorage()->getCache());
84
-		});
85
-		/** @var ICachedMountInfo[] $newMounts */
86
-		$newMounts = array_map(function (IMountPoint $mount) use ($user) {
87
-			// filter out any storages which aren't scanned yet since we aren't interested in files from those storages (yet)
88
-			if ($mount->getStorageRootId() === -1) {
89
-				return null;
90
-			} else {
91
-				return new LazyStorageMountInfo($user, $mount);
92
-			}
93
-		}, $mounts);
94
-		$newMounts = array_values(array_filter($newMounts));
95
-		$newMountKeys = array_map(function (ICachedMountInfo $mount) {
96
-			return $mount->getRootId() . '::' . $mount->getMountPoint();
97
-		}, $newMounts);
98
-		$newMounts = array_combine($newMountKeys, $newMounts);
99
-
100
-		$cachedMounts = $this->getMountsForUser($user);
101
-		if (is_array($mountProviderClasses)) {
102
-			$cachedMounts = array_filter($cachedMounts, function (ICachedMountInfo $mountInfo) use ($mountProviderClasses, $newMounts) {
103
-				// for existing mounts that didn't have a mount provider set
104
-				// we still want the ones that map to new mounts
105
-				$mountKey = $mountInfo->getRootId() . '::' . $mountInfo->getMountPoint();
106
-				if ($mountInfo->getMountProvider() === '' && isset($newMounts[$mountKey])) {
107
-					return true;
108
-				}
109
-				return in_array($mountInfo->getMountProvider(), $mountProviderClasses);
110
-			});
111
-		}
112
-		$cachedRootKeys = array_map(function (ICachedMountInfo $mount) {
113
-			return $mount->getRootId() . '::' . $mount->getMountPoint();
114
-		}, $cachedMounts);
115
-		$cachedMounts = array_combine($cachedRootKeys, $cachedMounts);
116
-
117
-		$addedMounts = [];
118
-		$removedMounts = [];
119
-
120
-		foreach ($newMounts as $mountKey => $newMount) {
121
-			if (!isset($cachedMounts[$mountKey])) {
122
-				$addedMounts[] = $newMount;
123
-			}
124
-		}
125
-
126
-		foreach ($cachedMounts as $mountKey => $cachedMount) {
127
-			if (!isset($newMounts[$mountKey])) {
128
-				$removedMounts[] = $cachedMount;
129
-			}
130
-		}
131
-
132
-		$changedMounts = $this->findChangedMounts($newMounts, $cachedMounts);
133
-
134
-		$this->connection->beginTransaction();
135
-		try {
136
-			foreach ($addedMounts as $mount) {
137
-				$this->addToCache($mount);
138
-				/** @psalm-suppress InvalidArgument */
139
-				$this->mountsForUsers[$user->getUID()][] = $mount;
140
-			}
141
-			foreach ($removedMounts as $mount) {
142
-				$this->removeFromCache($mount);
143
-				$index = array_search($mount, $this->mountsForUsers[$user->getUID()]);
144
-				unset($this->mountsForUsers[$user->getUID()][$index]);
145
-			}
146
-			foreach ($changedMounts as $mount) {
147
-				$this->updateCachedMount($mount);
148
-			}
149
-			$this->connection->commit();
150
-		} catch (\Throwable $e) {
151
-			$this->connection->rollBack();
152
-			throw $e;
153
-		}
154
-		$this->eventLogger->end('fs:setup:user:register');
155
-	}
156
-
157
-	/**
158
-	 * @param ICachedMountInfo[] $newMounts
159
-	 * @param ICachedMountInfo[] $cachedMounts
160
-	 * @return ICachedMountInfo[]
161
-	 */
162
-	private function findChangedMounts(array $newMounts, array $cachedMounts) {
163
-		$new = [];
164
-		foreach ($newMounts as $mount) {
165
-			$new[$mount->getRootId() . '::' . $mount->getMountPoint()] = $mount;
166
-		}
167
-		$changed = [];
168
-		foreach ($cachedMounts as $cachedMount) {
169
-			$key = $cachedMount->getRootId() . '::' . $cachedMount->getMountPoint();
170
-			if (isset($new[$key])) {
171
-				$newMount = $new[$key];
172
-				if (
173
-					$newMount->getMountPoint() !== $cachedMount->getMountPoint() ||
174
-					$newMount->getStorageId() !== $cachedMount->getStorageId() ||
175
-					$newMount->getMountId() !== $cachedMount->getMountId() ||
176
-					$newMount->getMountProvider() !== $cachedMount->getMountProvider()
177
-				) {
178
-					$changed[] = $newMount;
179
-				}
180
-			}
181
-		}
182
-		return $changed;
183
-	}
184
-
185
-	private function addToCache(ICachedMountInfo $mount) {
186
-		if ($mount->getStorageId() !== -1) {
187
-			$this->connection->insertIfNotExist('*PREFIX*mounts', [
188
-				'storage_id' => $mount->getStorageId(),
189
-				'root_id' => $mount->getRootId(),
190
-				'user_id' => $mount->getUser()->getUID(),
191
-				'mount_point' => $mount->getMountPoint(),
192
-				'mount_id' => $mount->getMountId(),
193
-				'mount_provider_class' => $mount->getMountProvider(),
194
-			], ['root_id', 'user_id', 'mount_point']);
195
-		} else {
196
-			// in some cases this is legitimate, like orphaned shares
197
-			$this->logger->debug('Could not get storage info for mount at ' . $mount->getMountPoint());
198
-		}
199
-	}
200
-
201
-	private function updateCachedMount(ICachedMountInfo $mount) {
202
-		$builder = $this->connection->getQueryBuilder();
203
-
204
-		$query = $builder->update('mounts')
205
-			->set('storage_id', $builder->createNamedParameter($mount->getStorageId()))
206
-			->set('mount_point', $builder->createNamedParameter($mount->getMountPoint()))
207
-			->set('mount_id', $builder->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT))
208
-			->set('mount_provider_class', $builder->createNamedParameter($mount->getMountProvider()))
209
-			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
210
-			->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)));
211
-
212
-		$query->execute();
213
-	}
214
-
215
-	private function removeFromCache(ICachedMountInfo $mount) {
216
-		$builder = $this->connection->getQueryBuilder();
217
-
218
-		$query = $builder->delete('mounts')
219
-			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
220
-			->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)))
221
-			->andWhere($builder->expr()->eq('mount_point', $builder->createNamedParameter($mount->getMountPoint())));
222
-		$query->execute();
223
-	}
224
-
225
-	private function dbRowToMountInfo(array $row) {
226
-		$user = $this->userManager->get($row['user_id']);
227
-		if (is_null($user)) {
228
-			return null;
229
-		}
230
-		$mount_id = $row['mount_id'];
231
-		if (!is_null($mount_id)) {
232
-			$mount_id = (int)$mount_id;
233
-		}
234
-		return new CachedMountInfo(
235
-			$user,
236
-			(int)$row['storage_id'],
237
-			(int)$row['root_id'],
238
-			$row['mount_point'],
239
-			$row['mount_provider_class'] ?? '',
240
-			$mount_id,
241
-			isset($row['path']) ? $row['path'] : '',
242
-		);
243
-	}
244
-
245
-	/**
246
-	 * @param IUser $user
247
-	 * @return ICachedMountInfo[]
248
-	 */
249
-	public function getMountsForUser(IUser $user) {
250
-		if (!isset($this->mountsForUsers[$user->getUID()])) {
251
-			$builder = $this->connection->getQueryBuilder();
252
-			$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
253
-				->from('mounts', 'm')
254
-				->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
255
-				->where($builder->expr()->eq('user_id', $builder->createPositionalParameter($user->getUID())));
256
-
257
-			$result = $query->execute();
258
-			$rows = $result->fetchAll();
259
-			$result->closeCursor();
260
-
261
-			$this->mountsForUsers[$user->getUID()] = array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
262
-		}
263
-		return $this->mountsForUsers[$user->getUID()];
264
-	}
265
-
266
-	/**
267
-	 * @param int $numericStorageId
268
-	 * @param string|null $user limit the results to a single user
269
-	 * @return CachedMountInfo[]
270
-	 */
271
-	public function getMountsForStorageId($numericStorageId, $user = null) {
272
-		$builder = $this->connection->getQueryBuilder();
273
-		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
274
-			->from('mounts', 'm')
275
-			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
276
-			->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($numericStorageId, IQueryBuilder::PARAM_INT)));
277
-
278
-		if ($user) {
279
-			$query->andWhere($builder->expr()->eq('user_id', $builder->createPositionalParameter($user)));
280
-		}
281
-
282
-		$result = $query->execute();
283
-		$rows = $result->fetchAll();
284
-		$result->closeCursor();
285
-
286
-		return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
287
-	}
288
-
289
-	/**
290
-	 * @param int $rootFileId
291
-	 * @return CachedMountInfo[]
292
-	 */
293
-	public function getMountsForRootId($rootFileId) {
294
-		$builder = $this->connection->getQueryBuilder();
295
-		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
296
-			->from('mounts', 'm')
297
-			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
298
-			->where($builder->expr()->eq('root_id', $builder->createPositionalParameter($rootFileId, IQueryBuilder::PARAM_INT)));
299
-
300
-		$result = $query->execute();
301
-		$rows = $result->fetchAll();
302
-		$result->closeCursor();
303
-
304
-		return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
305
-	}
306
-
307
-	/**
308
-	 * @param $fileId
309
-	 * @return array{int, string, int}
310
-	 * @throws \OCP\Files\NotFoundException
311
-	 */
312
-	private function getCacheInfoFromFileId($fileId): array {
313
-		if (!isset($this->cacheInfoCache[$fileId])) {
314
-			$builder = $this->connection->getQueryBuilder();
315
-			$query = $builder->select('storage', 'path', 'mimetype')
316
-				->from('filecache')
317
-				->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
318
-
319
-			$result = $query->execute();
320
-			$row = $result->fetch();
321
-			$result->closeCursor();
322
-
323
-			if (is_array($row)) {
324
-				$this->cacheInfoCache[$fileId] = [
325
-					(int)$row['storage'],
326
-					(string)$row['path'],
327
-					(int)$row['mimetype']
328
-				];
329
-			} else {
330
-				throw new NotFoundException('File with id "' . $fileId . '" not found');
331
-			}
332
-		}
333
-		return $this->cacheInfoCache[$fileId];
334
-	}
335
-
336
-	/**
337
-	 * @param int $fileId
338
-	 * @param string|null $user optionally restrict the results to a single user
339
-	 * @return ICachedMountFileInfo[]
340
-	 * @since 9.0.0
341
-	 */
342
-	public function getMountsForFileId($fileId, $user = null) {
343
-		try {
344
-			[$storageId, $internalPath] = $this->getCacheInfoFromFileId($fileId);
345
-		} catch (NotFoundException $e) {
346
-			return [];
347
-		}
348
-		$builder = $this->connection->getQueryBuilder();
349
-		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
350
-			->from('mounts', 'm')
351
-			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
352
-			->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($storageId, IQueryBuilder::PARAM_INT)));
353
-
354
-		if ($user) {
355
-			$query->andWhere($builder->expr()->eq('user_id', $builder->createPositionalParameter($user)));
356
-		}
357
-
358
-		$result = $query->execute();
359
-		$rows = $result->fetchAll();
360
-		$result->closeCursor();
361
-		// filter mounts that are from the same storage but a different directory
362
-		$filteredMounts = array_filter($rows, function (array $row) use ($internalPath, $fileId) {
363
-			if ($fileId === (int)$row['root_id']) {
364
-				return true;
365
-			}
366
-			$internalMountPath = $row['path'] ?? '';
367
-
368
-			return $internalMountPath === '' || substr($internalPath, 0, strlen($internalMountPath) + 1) === $internalMountPath . '/';
369
-		});
370
-
371
-		$filteredMounts = array_filter(array_map([$this, 'dbRowToMountInfo'], $filteredMounts));
372
-		return array_map(function (ICachedMountInfo $mount) use ($internalPath) {
373
-			return new CachedMountFileInfo(
374
-				$mount->getUser(),
375
-				$mount->getStorageId(),
376
-				$mount->getRootId(),
377
-				$mount->getMountPoint(),
378
-				$mount->getMountId(),
379
-				$mount->getMountProvider(),
380
-				$mount->getRootInternalPath(),
381
-				$internalPath
382
-			);
383
-		}, $filteredMounts);
384
-	}
385
-
386
-	/**
387
-	 * Remove all cached mounts for a user
388
-	 *
389
-	 * @param IUser $user
390
-	 */
391
-	public function removeUserMounts(IUser $user) {
392
-		$builder = $this->connection->getQueryBuilder();
393
-
394
-		$query = $builder->delete('mounts')
395
-			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID())));
396
-		$query->execute();
397
-	}
398
-
399
-	public function removeUserStorageMount($storageId, $userId) {
400
-		$builder = $this->connection->getQueryBuilder();
401
-
402
-		$query = $builder->delete('mounts')
403
-			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userId)))
404
-			->andWhere($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
405
-		$query->execute();
406
-	}
407
-
408
-	public function remoteStorageMounts($storageId) {
409
-		$builder = $this->connection->getQueryBuilder();
410
-
411
-		$query = $builder->delete('mounts')
412
-			->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
413
-		$query->execute();
414
-	}
415
-
416
-	/**
417
-	 * @param array $users
418
-	 * @return array
419
-	 */
420
-	public function getUsedSpaceForUsers(array $users) {
421
-		$builder = $this->connection->getQueryBuilder();
422
-
423
-		$slash = $builder->createNamedParameter('/');
424
-
425
-		$mountPoint = $builder->func()->concat(
426
-			$builder->func()->concat($slash, 'user_id'),
427
-			$slash
428
-		);
429
-
430
-		$userIds = array_map(function (IUser $user) {
431
-			return $user->getUID();
432
-		}, $users);
433
-
434
-		$query = $builder->select('m.user_id', 'f.size')
435
-			->from('mounts', 'm')
436
-			->innerJoin('m', 'filecache', 'f',
437
-				$builder->expr()->andX(
438
-					$builder->expr()->eq('m.storage_id', 'f.storage'),
439
-					$builder->expr()->eq('f.path_hash', $builder->createNamedParameter(md5('files')))
440
-				))
441
-			->where($builder->expr()->eq('m.mount_point', $mountPoint))
442
-			->andWhere($builder->expr()->in('m.user_id', $builder->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)));
443
-
444
-		$result = $query->execute();
445
-
446
-		$results = [];
447
-		while ($row = $result->fetch()) {
448
-			$results[$row['user_id']] = $row['size'];
449
-		}
450
-		$result->closeCursor();
451
-		return $results;
452
-	}
453
-
454
-	public function clear(): void {
455
-		$this->cacheInfoCache = new CappedMemoryCache();
456
-		$this->mountsForUsers = new CappedMemoryCache();
457
-	}
458
-
459
-	public function getMountForPath(IUser $user, string $path): ICachedMountInfo {
460
-		$mounts = $this->getMountsForUser($user);
461
-		$mountPoints = array_map(function (ICachedMountInfo $mount) {
462
-			return $mount->getMountPoint();
463
-		}, $mounts);
464
-		$mounts = array_combine($mountPoints, $mounts);
465
-
466
-		$current = $path;
467
-		// walk up the directory tree until we find a path that has a mountpoint set
468
-		// the loop will return if a mountpoint is found or break if none are found
469
-		while (true) {
470
-			$mountPoint = $current . '/';
471
-			if (isset($mounts[$mountPoint])) {
472
-				return $mounts[$mountPoint];
473
-			} elseif ($current === '') {
474
-				break;
475
-			}
476
-
477
-			$current = dirname($current);
478
-			if ($current === '.' || $current === '/') {
479
-				$current = '';
480
-			}
481
-		}
482
-
483
-		throw new NotFoundException("No cached mount for path " . $path);
484
-	}
485
-
486
-	public function getMountsInPath(IUser $user, string $path): array {
487
-		$path = rtrim($path, '/') . '/';
488
-		$mounts = $this->getMountsForUser($user);
489
-		return array_filter($mounts, function (ICachedMountInfo $mount) use ($path) {
490
-			return $mount->getMountPoint() !== $path && str_starts_with($mount->getMountPoint(), $path);
491
-		});
492
-	}
49
+    private IDBConnection $connection;
50
+    private IUserManager $userManager;
51
+
52
+    /**
53
+     * Cached mount info.
54
+     * @var CappedMemoryCache<ICachedMountInfo[]>
55
+     **/
56
+    private CappedMemoryCache $mountsForUsers;
57
+    private LoggerInterface $logger;
58
+    /** @var CappedMemoryCache<array> */
59
+    private CappedMemoryCache $cacheInfoCache;
60
+    private IEventLogger $eventLogger;
61
+
62
+    /**
63
+     * UserMountCache constructor.
64
+     */
65
+    public function __construct(
66
+        IDBConnection $connection,
67
+        IUserManager $userManager,
68
+        LoggerInterface $logger,
69
+        IEventLogger $eventLogger
70
+    ) {
71
+        $this->connection = $connection;
72
+        $this->userManager = $userManager;
73
+        $this->logger = $logger;
74
+        $this->eventLogger = $eventLogger;
75
+        $this->cacheInfoCache = new CappedMemoryCache();
76
+        $this->mountsForUsers = new CappedMemoryCache();
77
+    }
78
+
79
+    public function registerMounts(IUser $user, array $mounts, array $mountProviderClasses = null) {
80
+        $this->eventLogger->start('fs:setup:user:register', 'Registering mounts for user');
81
+        // filter out non-proper storages coming from unit tests
82
+        $mounts = array_filter($mounts, function (IMountPoint $mount) {
83
+            return $mount instanceof SharedMount || ($mount->getStorage() && $mount->getStorage()->getCache());
84
+        });
85
+        /** @var ICachedMountInfo[] $newMounts */
86
+        $newMounts = array_map(function (IMountPoint $mount) use ($user) {
87
+            // filter out any storages which aren't scanned yet since we aren't interested in files from those storages (yet)
88
+            if ($mount->getStorageRootId() === -1) {
89
+                return null;
90
+            } else {
91
+                return new LazyStorageMountInfo($user, $mount);
92
+            }
93
+        }, $mounts);
94
+        $newMounts = array_values(array_filter($newMounts));
95
+        $newMountKeys = array_map(function (ICachedMountInfo $mount) {
96
+            return $mount->getRootId() . '::' . $mount->getMountPoint();
97
+        }, $newMounts);
98
+        $newMounts = array_combine($newMountKeys, $newMounts);
99
+
100
+        $cachedMounts = $this->getMountsForUser($user);
101
+        if (is_array($mountProviderClasses)) {
102
+            $cachedMounts = array_filter($cachedMounts, function (ICachedMountInfo $mountInfo) use ($mountProviderClasses, $newMounts) {
103
+                // for existing mounts that didn't have a mount provider set
104
+                // we still want the ones that map to new mounts
105
+                $mountKey = $mountInfo->getRootId() . '::' . $mountInfo->getMountPoint();
106
+                if ($mountInfo->getMountProvider() === '' && isset($newMounts[$mountKey])) {
107
+                    return true;
108
+                }
109
+                return in_array($mountInfo->getMountProvider(), $mountProviderClasses);
110
+            });
111
+        }
112
+        $cachedRootKeys = array_map(function (ICachedMountInfo $mount) {
113
+            return $mount->getRootId() . '::' . $mount->getMountPoint();
114
+        }, $cachedMounts);
115
+        $cachedMounts = array_combine($cachedRootKeys, $cachedMounts);
116
+
117
+        $addedMounts = [];
118
+        $removedMounts = [];
119
+
120
+        foreach ($newMounts as $mountKey => $newMount) {
121
+            if (!isset($cachedMounts[$mountKey])) {
122
+                $addedMounts[] = $newMount;
123
+            }
124
+        }
125
+
126
+        foreach ($cachedMounts as $mountKey => $cachedMount) {
127
+            if (!isset($newMounts[$mountKey])) {
128
+                $removedMounts[] = $cachedMount;
129
+            }
130
+        }
131
+
132
+        $changedMounts = $this->findChangedMounts($newMounts, $cachedMounts);
133
+
134
+        $this->connection->beginTransaction();
135
+        try {
136
+            foreach ($addedMounts as $mount) {
137
+                $this->addToCache($mount);
138
+                /** @psalm-suppress InvalidArgument */
139
+                $this->mountsForUsers[$user->getUID()][] = $mount;
140
+            }
141
+            foreach ($removedMounts as $mount) {
142
+                $this->removeFromCache($mount);
143
+                $index = array_search($mount, $this->mountsForUsers[$user->getUID()]);
144
+                unset($this->mountsForUsers[$user->getUID()][$index]);
145
+            }
146
+            foreach ($changedMounts as $mount) {
147
+                $this->updateCachedMount($mount);
148
+            }
149
+            $this->connection->commit();
150
+        } catch (\Throwable $e) {
151
+            $this->connection->rollBack();
152
+            throw $e;
153
+        }
154
+        $this->eventLogger->end('fs:setup:user:register');
155
+    }
156
+
157
+    /**
158
+     * @param ICachedMountInfo[] $newMounts
159
+     * @param ICachedMountInfo[] $cachedMounts
160
+     * @return ICachedMountInfo[]
161
+     */
162
+    private function findChangedMounts(array $newMounts, array $cachedMounts) {
163
+        $new = [];
164
+        foreach ($newMounts as $mount) {
165
+            $new[$mount->getRootId() . '::' . $mount->getMountPoint()] = $mount;
166
+        }
167
+        $changed = [];
168
+        foreach ($cachedMounts as $cachedMount) {
169
+            $key = $cachedMount->getRootId() . '::' . $cachedMount->getMountPoint();
170
+            if (isset($new[$key])) {
171
+                $newMount = $new[$key];
172
+                if (
173
+                    $newMount->getMountPoint() !== $cachedMount->getMountPoint() ||
174
+                    $newMount->getStorageId() !== $cachedMount->getStorageId() ||
175
+                    $newMount->getMountId() !== $cachedMount->getMountId() ||
176
+                    $newMount->getMountProvider() !== $cachedMount->getMountProvider()
177
+                ) {
178
+                    $changed[] = $newMount;
179
+                }
180
+            }
181
+        }
182
+        return $changed;
183
+    }
184
+
185
+    private function addToCache(ICachedMountInfo $mount) {
186
+        if ($mount->getStorageId() !== -1) {
187
+            $this->connection->insertIfNotExist('*PREFIX*mounts', [
188
+                'storage_id' => $mount->getStorageId(),
189
+                'root_id' => $mount->getRootId(),
190
+                'user_id' => $mount->getUser()->getUID(),
191
+                'mount_point' => $mount->getMountPoint(),
192
+                'mount_id' => $mount->getMountId(),
193
+                'mount_provider_class' => $mount->getMountProvider(),
194
+            ], ['root_id', 'user_id', 'mount_point']);
195
+        } else {
196
+            // in some cases this is legitimate, like orphaned shares
197
+            $this->logger->debug('Could not get storage info for mount at ' . $mount->getMountPoint());
198
+        }
199
+    }
200
+
201
+    private function updateCachedMount(ICachedMountInfo $mount) {
202
+        $builder = $this->connection->getQueryBuilder();
203
+
204
+        $query = $builder->update('mounts')
205
+            ->set('storage_id', $builder->createNamedParameter($mount->getStorageId()))
206
+            ->set('mount_point', $builder->createNamedParameter($mount->getMountPoint()))
207
+            ->set('mount_id', $builder->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT))
208
+            ->set('mount_provider_class', $builder->createNamedParameter($mount->getMountProvider()))
209
+            ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
210
+            ->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)));
211
+
212
+        $query->execute();
213
+    }
214
+
215
+    private function removeFromCache(ICachedMountInfo $mount) {
216
+        $builder = $this->connection->getQueryBuilder();
217
+
218
+        $query = $builder->delete('mounts')
219
+            ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
220
+            ->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)))
221
+            ->andWhere($builder->expr()->eq('mount_point', $builder->createNamedParameter($mount->getMountPoint())));
222
+        $query->execute();
223
+    }
224
+
225
+    private function dbRowToMountInfo(array $row) {
226
+        $user = $this->userManager->get($row['user_id']);
227
+        if (is_null($user)) {
228
+            return null;
229
+        }
230
+        $mount_id = $row['mount_id'];
231
+        if (!is_null($mount_id)) {
232
+            $mount_id = (int)$mount_id;
233
+        }
234
+        return new CachedMountInfo(
235
+            $user,
236
+            (int)$row['storage_id'],
237
+            (int)$row['root_id'],
238
+            $row['mount_point'],
239
+            $row['mount_provider_class'] ?? '',
240
+            $mount_id,
241
+            isset($row['path']) ? $row['path'] : '',
242
+        );
243
+    }
244
+
245
+    /**
246
+     * @param IUser $user
247
+     * @return ICachedMountInfo[]
248
+     */
249
+    public function getMountsForUser(IUser $user) {
250
+        if (!isset($this->mountsForUsers[$user->getUID()])) {
251
+            $builder = $this->connection->getQueryBuilder();
252
+            $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
253
+                ->from('mounts', 'm')
254
+                ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
255
+                ->where($builder->expr()->eq('user_id', $builder->createPositionalParameter($user->getUID())));
256
+
257
+            $result = $query->execute();
258
+            $rows = $result->fetchAll();
259
+            $result->closeCursor();
260
+
261
+            $this->mountsForUsers[$user->getUID()] = array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
262
+        }
263
+        return $this->mountsForUsers[$user->getUID()];
264
+    }
265
+
266
+    /**
267
+     * @param int $numericStorageId
268
+     * @param string|null $user limit the results to a single user
269
+     * @return CachedMountInfo[]
270
+     */
271
+    public function getMountsForStorageId($numericStorageId, $user = null) {
272
+        $builder = $this->connection->getQueryBuilder();
273
+        $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
274
+            ->from('mounts', 'm')
275
+            ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
276
+            ->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($numericStorageId, IQueryBuilder::PARAM_INT)));
277
+
278
+        if ($user) {
279
+            $query->andWhere($builder->expr()->eq('user_id', $builder->createPositionalParameter($user)));
280
+        }
281
+
282
+        $result = $query->execute();
283
+        $rows = $result->fetchAll();
284
+        $result->closeCursor();
285
+
286
+        return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
287
+    }
288
+
289
+    /**
290
+     * @param int $rootFileId
291
+     * @return CachedMountInfo[]
292
+     */
293
+    public function getMountsForRootId($rootFileId) {
294
+        $builder = $this->connection->getQueryBuilder();
295
+        $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
296
+            ->from('mounts', 'm')
297
+            ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
298
+            ->where($builder->expr()->eq('root_id', $builder->createPositionalParameter($rootFileId, IQueryBuilder::PARAM_INT)));
299
+
300
+        $result = $query->execute();
301
+        $rows = $result->fetchAll();
302
+        $result->closeCursor();
303
+
304
+        return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
305
+    }
306
+
307
+    /**
308
+     * @param $fileId
309
+     * @return array{int, string, int}
310
+     * @throws \OCP\Files\NotFoundException
311
+     */
312
+    private function getCacheInfoFromFileId($fileId): array {
313
+        if (!isset($this->cacheInfoCache[$fileId])) {
314
+            $builder = $this->connection->getQueryBuilder();
315
+            $query = $builder->select('storage', 'path', 'mimetype')
316
+                ->from('filecache')
317
+                ->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
318
+
319
+            $result = $query->execute();
320
+            $row = $result->fetch();
321
+            $result->closeCursor();
322
+
323
+            if (is_array($row)) {
324
+                $this->cacheInfoCache[$fileId] = [
325
+                    (int)$row['storage'],
326
+                    (string)$row['path'],
327
+                    (int)$row['mimetype']
328
+                ];
329
+            } else {
330
+                throw new NotFoundException('File with id "' . $fileId . '" not found');
331
+            }
332
+        }
333
+        return $this->cacheInfoCache[$fileId];
334
+    }
335
+
336
+    /**
337
+     * @param int $fileId
338
+     * @param string|null $user optionally restrict the results to a single user
339
+     * @return ICachedMountFileInfo[]
340
+     * @since 9.0.0
341
+     */
342
+    public function getMountsForFileId($fileId, $user = null) {
343
+        try {
344
+            [$storageId, $internalPath] = $this->getCacheInfoFromFileId($fileId);
345
+        } catch (NotFoundException $e) {
346
+            return [];
347
+        }
348
+        $builder = $this->connection->getQueryBuilder();
349
+        $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
350
+            ->from('mounts', 'm')
351
+            ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
352
+            ->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($storageId, IQueryBuilder::PARAM_INT)));
353
+
354
+        if ($user) {
355
+            $query->andWhere($builder->expr()->eq('user_id', $builder->createPositionalParameter($user)));
356
+        }
357
+
358
+        $result = $query->execute();
359
+        $rows = $result->fetchAll();
360
+        $result->closeCursor();
361
+        // filter mounts that are from the same storage but a different directory
362
+        $filteredMounts = array_filter($rows, function (array $row) use ($internalPath, $fileId) {
363
+            if ($fileId === (int)$row['root_id']) {
364
+                return true;
365
+            }
366
+            $internalMountPath = $row['path'] ?? '';
367
+
368
+            return $internalMountPath === '' || substr($internalPath, 0, strlen($internalMountPath) + 1) === $internalMountPath . '/';
369
+        });
370
+
371
+        $filteredMounts = array_filter(array_map([$this, 'dbRowToMountInfo'], $filteredMounts));
372
+        return array_map(function (ICachedMountInfo $mount) use ($internalPath) {
373
+            return new CachedMountFileInfo(
374
+                $mount->getUser(),
375
+                $mount->getStorageId(),
376
+                $mount->getRootId(),
377
+                $mount->getMountPoint(),
378
+                $mount->getMountId(),
379
+                $mount->getMountProvider(),
380
+                $mount->getRootInternalPath(),
381
+                $internalPath
382
+            );
383
+        }, $filteredMounts);
384
+    }
385
+
386
+    /**
387
+     * Remove all cached mounts for a user
388
+     *
389
+     * @param IUser $user
390
+     */
391
+    public function removeUserMounts(IUser $user) {
392
+        $builder = $this->connection->getQueryBuilder();
393
+
394
+        $query = $builder->delete('mounts')
395
+            ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID())));
396
+        $query->execute();
397
+    }
398
+
399
+    public function removeUserStorageMount($storageId, $userId) {
400
+        $builder = $this->connection->getQueryBuilder();
401
+
402
+        $query = $builder->delete('mounts')
403
+            ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userId)))
404
+            ->andWhere($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
405
+        $query->execute();
406
+    }
407
+
408
+    public function remoteStorageMounts($storageId) {
409
+        $builder = $this->connection->getQueryBuilder();
410
+
411
+        $query = $builder->delete('mounts')
412
+            ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
413
+        $query->execute();
414
+    }
415
+
416
+    /**
417
+     * @param array $users
418
+     * @return array
419
+     */
420
+    public function getUsedSpaceForUsers(array $users) {
421
+        $builder = $this->connection->getQueryBuilder();
422
+
423
+        $slash = $builder->createNamedParameter('/');
424
+
425
+        $mountPoint = $builder->func()->concat(
426
+            $builder->func()->concat($slash, 'user_id'),
427
+            $slash
428
+        );
429
+
430
+        $userIds = array_map(function (IUser $user) {
431
+            return $user->getUID();
432
+        }, $users);
433
+
434
+        $query = $builder->select('m.user_id', 'f.size')
435
+            ->from('mounts', 'm')
436
+            ->innerJoin('m', 'filecache', 'f',
437
+                $builder->expr()->andX(
438
+                    $builder->expr()->eq('m.storage_id', 'f.storage'),
439
+                    $builder->expr()->eq('f.path_hash', $builder->createNamedParameter(md5('files')))
440
+                ))
441
+            ->where($builder->expr()->eq('m.mount_point', $mountPoint))
442
+            ->andWhere($builder->expr()->in('m.user_id', $builder->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)));
443
+
444
+        $result = $query->execute();
445
+
446
+        $results = [];
447
+        while ($row = $result->fetch()) {
448
+            $results[$row['user_id']] = $row['size'];
449
+        }
450
+        $result->closeCursor();
451
+        return $results;
452
+    }
453
+
454
+    public function clear(): void {
455
+        $this->cacheInfoCache = new CappedMemoryCache();
456
+        $this->mountsForUsers = new CappedMemoryCache();
457
+    }
458
+
459
+    public function getMountForPath(IUser $user, string $path): ICachedMountInfo {
460
+        $mounts = $this->getMountsForUser($user);
461
+        $mountPoints = array_map(function (ICachedMountInfo $mount) {
462
+            return $mount->getMountPoint();
463
+        }, $mounts);
464
+        $mounts = array_combine($mountPoints, $mounts);
465
+
466
+        $current = $path;
467
+        // walk up the directory tree until we find a path that has a mountpoint set
468
+        // the loop will return if a mountpoint is found or break if none are found
469
+        while (true) {
470
+            $mountPoint = $current . '/';
471
+            if (isset($mounts[$mountPoint])) {
472
+                return $mounts[$mountPoint];
473
+            } elseif ($current === '') {
474
+                break;
475
+            }
476
+
477
+            $current = dirname($current);
478
+            if ($current === '.' || $current === '/') {
479
+                $current = '';
480
+            }
481
+        }
482
+
483
+        throw new NotFoundException("No cached mount for path " . $path);
484
+    }
485
+
486
+    public function getMountsInPath(IUser $user, string $path): array {
487
+        $path = rtrim($path, '/') . '/';
488
+        $mounts = $this->getMountsForUser($user);
489
+        return array_filter($mounts, function (ICachedMountInfo $mount) use ($path) {
490
+            return $mount->getMountPoint() !== $path && str_starts_with($mount->getMountPoint(), $path);
491
+        });
492
+    }
493 493
 }
Please login to merge, or discard this patch.
Spacing   +28 added lines, -28 removed lines patch added patch discarded remove patch
@@ -79,11 +79,11 @@  discard block
 block discarded – undo
79 79
 	public function registerMounts(IUser $user, array $mounts, array $mountProviderClasses = null) {
80 80
 		$this->eventLogger->start('fs:setup:user:register', 'Registering mounts for user');
81 81
 		// filter out non-proper storages coming from unit tests
82
-		$mounts = array_filter($mounts, function (IMountPoint $mount) {
82
+		$mounts = array_filter($mounts, function(IMountPoint $mount) {
83 83
 			return $mount instanceof SharedMount || ($mount->getStorage() && $mount->getStorage()->getCache());
84 84
 		});
85 85
 		/** @var ICachedMountInfo[] $newMounts */
86
-		$newMounts = array_map(function (IMountPoint $mount) use ($user) {
86
+		$newMounts = array_map(function(IMountPoint $mount) use ($user) {
87 87
 			// filter out any storages which aren't scanned yet since we aren't interested in files from those storages (yet)
88 88
 			if ($mount->getStorageRootId() === -1) {
89 89
 				return null;
@@ -92,25 +92,25 @@  discard block
 block discarded – undo
92 92
 			}
93 93
 		}, $mounts);
94 94
 		$newMounts = array_values(array_filter($newMounts));
95
-		$newMountKeys = array_map(function (ICachedMountInfo $mount) {
96
-			return $mount->getRootId() . '::' . $mount->getMountPoint();
95
+		$newMountKeys = array_map(function(ICachedMountInfo $mount) {
96
+			return $mount->getRootId().'::'.$mount->getMountPoint();
97 97
 		}, $newMounts);
98 98
 		$newMounts = array_combine($newMountKeys, $newMounts);
99 99
 
100 100
 		$cachedMounts = $this->getMountsForUser($user);
101 101
 		if (is_array($mountProviderClasses)) {
102
-			$cachedMounts = array_filter($cachedMounts, function (ICachedMountInfo $mountInfo) use ($mountProviderClasses, $newMounts) {
102
+			$cachedMounts = array_filter($cachedMounts, function(ICachedMountInfo $mountInfo) use ($mountProviderClasses, $newMounts) {
103 103
 				// for existing mounts that didn't have a mount provider set
104 104
 				// we still want the ones that map to new mounts
105
-				$mountKey = $mountInfo->getRootId() . '::' . $mountInfo->getMountPoint();
105
+				$mountKey = $mountInfo->getRootId().'::'.$mountInfo->getMountPoint();
106 106
 				if ($mountInfo->getMountProvider() === '' && isset($newMounts[$mountKey])) {
107 107
 					return true;
108 108
 				}
109 109
 				return in_array($mountInfo->getMountProvider(), $mountProviderClasses);
110 110
 			});
111 111
 		}
112
-		$cachedRootKeys = array_map(function (ICachedMountInfo $mount) {
113
-			return $mount->getRootId() . '::' . $mount->getMountPoint();
112
+		$cachedRootKeys = array_map(function(ICachedMountInfo $mount) {
113
+			return $mount->getRootId().'::'.$mount->getMountPoint();
114 114
 		}, $cachedMounts);
115 115
 		$cachedMounts = array_combine($cachedRootKeys, $cachedMounts);
116 116
 
@@ -162,11 +162,11 @@  discard block
 block discarded – undo
162 162
 	private function findChangedMounts(array $newMounts, array $cachedMounts) {
163 163
 		$new = [];
164 164
 		foreach ($newMounts as $mount) {
165
-			$new[$mount->getRootId() . '::' . $mount->getMountPoint()] = $mount;
165
+			$new[$mount->getRootId().'::'.$mount->getMountPoint()] = $mount;
166 166
 		}
167 167
 		$changed = [];
168 168
 		foreach ($cachedMounts as $cachedMount) {
169
-			$key = $cachedMount->getRootId() . '::' . $cachedMount->getMountPoint();
169
+			$key = $cachedMount->getRootId().'::'.$cachedMount->getMountPoint();
170 170
 			if (isset($new[$key])) {
171 171
 				$newMount = $new[$key];
172 172
 				if (
@@ -194,7 +194,7 @@  discard block
 block discarded – undo
194 194
 			], ['root_id', 'user_id', 'mount_point']);
195 195
 		} else {
196 196
 			// in some cases this is legitimate, like orphaned shares
197
-			$this->logger->debug('Could not get storage info for mount at ' . $mount->getMountPoint());
197
+			$this->logger->debug('Could not get storage info for mount at '.$mount->getMountPoint());
198 198
 		}
199 199
 	}
200 200
 
@@ -229,12 +229,12 @@  discard block
 block discarded – undo
229 229
 		}
230 230
 		$mount_id = $row['mount_id'];
231 231
 		if (!is_null($mount_id)) {
232
-			$mount_id = (int)$mount_id;
232
+			$mount_id = (int) $mount_id;
233 233
 		}
234 234
 		return new CachedMountInfo(
235 235
 			$user,
236
-			(int)$row['storage_id'],
237
-			(int)$row['root_id'],
236
+			(int) $row['storage_id'],
237
+			(int) $row['root_id'],
238 238
 			$row['mount_point'],
239 239
 			$row['mount_provider_class'] ?? '',
240 240
 			$mount_id,
@@ -322,12 +322,12 @@  discard block
 block discarded – undo
322 322
 
323 323
 			if (is_array($row)) {
324 324
 				$this->cacheInfoCache[$fileId] = [
325
-					(int)$row['storage'],
326
-					(string)$row['path'],
327
-					(int)$row['mimetype']
325
+					(int) $row['storage'],
326
+					(string) $row['path'],
327
+					(int) $row['mimetype']
328 328
 				];
329 329
 			} else {
330
-				throw new NotFoundException('File with id "' . $fileId . '" not found');
330
+				throw new NotFoundException('File with id "'.$fileId.'" not found');
331 331
 			}
332 332
 		}
333 333
 		return $this->cacheInfoCache[$fileId];
@@ -359,17 +359,17 @@  discard block
 block discarded – undo
359 359
 		$rows = $result->fetchAll();
360 360
 		$result->closeCursor();
361 361
 		// filter mounts that are from the same storage but a different directory
362
-		$filteredMounts = array_filter($rows, function (array $row) use ($internalPath, $fileId) {
363
-			if ($fileId === (int)$row['root_id']) {
362
+		$filteredMounts = array_filter($rows, function(array $row) use ($internalPath, $fileId) {
363
+			if ($fileId === (int) $row['root_id']) {
364 364
 				return true;
365 365
 			}
366 366
 			$internalMountPath = $row['path'] ?? '';
367 367
 
368
-			return $internalMountPath === '' || substr($internalPath, 0, strlen($internalMountPath) + 1) === $internalMountPath . '/';
368
+			return $internalMountPath === '' || substr($internalPath, 0, strlen($internalMountPath) + 1) === $internalMountPath.'/';
369 369
 		});
370 370
 
371 371
 		$filteredMounts = array_filter(array_map([$this, 'dbRowToMountInfo'], $filteredMounts));
372
-		return array_map(function (ICachedMountInfo $mount) use ($internalPath) {
372
+		return array_map(function(ICachedMountInfo $mount) use ($internalPath) {
373 373
 			return new CachedMountFileInfo(
374 374
 				$mount->getUser(),
375 375
 				$mount->getStorageId(),
@@ -427,7 +427,7 @@  discard block
 block discarded – undo
427 427
 			$slash
428 428
 		);
429 429
 
430
-		$userIds = array_map(function (IUser $user) {
430
+		$userIds = array_map(function(IUser $user) {
431 431
 			return $user->getUID();
432 432
 		}, $users);
433 433
 
@@ -458,7 +458,7 @@  discard block
 block discarded – undo
458 458
 
459 459
 	public function getMountForPath(IUser $user, string $path): ICachedMountInfo {
460 460
 		$mounts = $this->getMountsForUser($user);
461
-		$mountPoints = array_map(function (ICachedMountInfo $mount) {
461
+		$mountPoints = array_map(function(ICachedMountInfo $mount) {
462 462
 			return $mount->getMountPoint();
463 463
 		}, $mounts);
464 464
 		$mounts = array_combine($mountPoints, $mounts);
@@ -467,7 +467,7 @@  discard block
 block discarded – undo
467 467
 		// walk up the directory tree until we find a path that has a mountpoint set
468 468
 		// the loop will return if a mountpoint is found or break if none are found
469 469
 		while (true) {
470
-			$mountPoint = $current . '/';
470
+			$mountPoint = $current.'/';
471 471
 			if (isset($mounts[$mountPoint])) {
472 472
 				return $mounts[$mountPoint];
473 473
 			} elseif ($current === '') {
@@ -480,13 +480,13 @@  discard block
 block discarded – undo
480 480
 			}
481 481
 		}
482 482
 
483
-		throw new NotFoundException("No cached mount for path " . $path);
483
+		throw new NotFoundException("No cached mount for path ".$path);
484 484
 	}
485 485
 
486 486
 	public function getMountsInPath(IUser $user, string $path): array {
487
-		$path = rtrim($path, '/') . '/';
487
+		$path = rtrim($path, '/').'/';
488 488
 		$mounts = $this->getMountsForUser($user);
489
-		return array_filter($mounts, function (ICachedMountInfo $mount) use ($path) {
489
+		return array_filter($mounts, function(ICachedMountInfo $mount) use ($path) {
490 490
 			return $mount->getMountPoint() !== $path && str_starts_with($mount->getMountPoint(), $path);
491 491
 		});
492 492
 	}
Please login to merge, or discard this patch.
lib/private/Files/Filesystem.php 2 patches
Indentation   +713 added lines, -713 removed lines patch added patch discarded remove patch
@@ -50,717 +50,717 @@
 block discarded – undo
50 50
 use OCP\IUserSession;
51 51
 
52 52
 class Filesystem {
53
-	private static ?Mount\Manager $mounts = null;
54
-
55
-	public static bool $loaded = false;
56
-
57
-	private static ?View $defaultInstance = null;
58
-
59
-	private static ?CappedMemoryCache $normalizedPathCache = null;
60
-
61
-	/** @var string[]|null */
62
-	private static ?array $blacklist = null;
63
-
64
-	/**
65
-	 * classname which used for hooks handling
66
-	 * used as signalclass in OC_Hooks::emit()
67
-	 */
68
-	public const CLASSNAME = 'OC_Filesystem';
69
-
70
-	/**
71
-	 * signalname emitted before file renaming
72
-	 *
73
-	 * @param string $oldpath
74
-	 * @param string $newpath
75
-	 */
76
-	public const signal_rename = 'rename';
77
-
78
-	/**
79
-	 * signal emitted after file renaming
80
-	 *
81
-	 * @param string $oldpath
82
-	 * @param string $newpath
83
-	 */
84
-	public const signal_post_rename = 'post_rename';
85
-
86
-	/**
87
-	 * signal emitted before file/dir creation
88
-	 *
89
-	 * @param string $path
90
-	 * @param bool $run changing this flag to false in hook handler will cancel event
91
-	 */
92
-	public const signal_create = 'create';
93
-
94
-	/**
95
-	 * signal emitted after file/dir creation
96
-	 *
97
-	 * @param string $path
98
-	 * @param bool $run changing this flag to false in hook handler will cancel event
99
-	 */
100
-	public const signal_post_create = 'post_create';
101
-
102
-	/**
103
-	 * signal emits before file/dir copy
104
-	 *
105
-	 * @param string $oldpath
106
-	 * @param string $newpath
107
-	 * @param bool $run changing this flag to false in hook handler will cancel event
108
-	 */
109
-	public const signal_copy = 'copy';
110
-
111
-	/**
112
-	 * signal emits after file/dir copy
113
-	 *
114
-	 * @param string $oldpath
115
-	 * @param string $newpath
116
-	 */
117
-	public const signal_post_copy = 'post_copy';
118
-
119
-	/**
120
-	 * signal emits before file/dir save
121
-	 *
122
-	 * @param string $path
123
-	 * @param bool $run changing this flag to false in hook handler will cancel event
124
-	 */
125
-	public const signal_write = 'write';
126
-
127
-	/**
128
-	 * signal emits after file/dir save
129
-	 *
130
-	 * @param string $path
131
-	 */
132
-	public const signal_post_write = 'post_write';
133
-
134
-	/**
135
-	 * signal emitted before file/dir update
136
-	 *
137
-	 * @param string $path
138
-	 * @param bool $run changing this flag to false in hook handler will cancel event
139
-	 */
140
-	public const signal_update = 'update';
141
-
142
-	/**
143
-	 * signal emitted after file/dir update
144
-	 *
145
-	 * @param string $path
146
-	 * @param bool $run changing this flag to false in hook handler will cancel event
147
-	 */
148
-	public const signal_post_update = 'post_update';
149
-
150
-	/**
151
-	 * signal emits when reading file/dir
152
-	 *
153
-	 * @param string $path
154
-	 */
155
-	public const signal_read = 'read';
156
-
157
-	/**
158
-	 * signal emits when removing file/dir
159
-	 *
160
-	 * @param string $path
161
-	 */
162
-	public const signal_delete = 'delete';
163
-
164
-	/**
165
-	 * parameters definitions for signals
166
-	 */
167
-	public const signal_param_path = 'path';
168
-	public const signal_param_oldpath = 'oldpath';
169
-	public const signal_param_newpath = 'newpath';
170
-
171
-	/**
172
-	 * run - changing this flag to false in hook handler will cancel event
173
-	 */
174
-	public const signal_param_run = 'run';
175
-
176
-	public const signal_create_mount = 'create_mount';
177
-	public const signal_delete_mount = 'delete_mount';
178
-	public const signal_param_mount_type = 'mounttype';
179
-	public const signal_param_users = 'users';
180
-
181
-	private static ?\OC\Files\Storage\StorageFactory $loader = null;
182
-
183
-	private static bool $logWarningWhenAddingStorageWrapper = true;
184
-
185
-	/**
186
-	 * @param bool $shouldLog
187
-	 * @return bool previous value
188
-	 * @internal
189
-	 */
190
-	public static function logWarningWhenAddingStorageWrapper(bool $shouldLog): bool {
191
-		$previousValue = self::$logWarningWhenAddingStorageWrapper;
192
-		self::$logWarningWhenAddingStorageWrapper = $shouldLog;
193
-		return $previousValue;
194
-	}
195
-
196
-	/**
197
-	 * @param string $wrapperName
198
-	 * @param callable $wrapper
199
-	 * @param int $priority
200
-	 */
201
-	public static function addStorageWrapper($wrapperName, $wrapper, $priority = 50) {
202
-		if (self::$logWarningWhenAddingStorageWrapper) {
203
-			\OC::$server->getLogger()->warning("Storage wrapper '{wrapper}' was not registered via the 'OC_Filesystem - preSetup' hook which could cause potential problems.", [
204
-				'wrapper' => $wrapperName,
205
-				'app' => 'filesystem',
206
-			]);
207
-		}
208
-
209
-		$mounts = self::getMountManager()->getAll();
210
-		if (!self::getLoader()->addStorageWrapper($wrapperName, $wrapper, $priority, $mounts)) {
211
-			// do not re-wrap if storage with this name already existed
212
-			return;
213
-		}
214
-	}
215
-
216
-	/**
217
-	 * Returns the storage factory
218
-	 *
219
-	 * @return IStorageFactory
220
-	 */
221
-	public static function getLoader() {
222
-		if (!self::$loader) {
223
-			self::$loader = \OC::$server->get(IStorageFactory::class);
224
-		}
225
-		return self::$loader;
226
-	}
227
-
228
-	/**
229
-	 * Returns the mount manager
230
-	 */
231
-	public static function getMountManager(): Mount\Manager {
232
-		self::initMountManager();
233
-		assert(self::$mounts !== null);
234
-		return self::$mounts;
235
-	}
236
-
237
-	/**
238
-	 * get the mountpoint of the storage object for a path
239
-	 * ( note: because a storage is not always mounted inside the fakeroot, the
240
-	 * returned mountpoint is relative to the absolute root of the filesystem
241
-	 * and doesn't take the chroot into account )
242
-	 *
243
-	 * @param string $path
244
-	 * @return string
245
-	 */
246
-	public static function getMountPoint($path) {
247
-		if (!self::$mounts) {
248
-			\OC_Util::setupFS();
249
-		}
250
-		$mount = self::$mounts->find($path);
251
-		return $mount->getMountPoint();
252
-	}
253
-
254
-	/**
255
-	 * get a list of all mount points in a directory
256
-	 *
257
-	 * @param string $path
258
-	 * @return string[]
259
-	 */
260
-	public static function getMountPoints($path) {
261
-		if (!self::$mounts) {
262
-			\OC_Util::setupFS();
263
-		}
264
-		$result = [];
265
-		$mounts = self::$mounts->findIn($path);
266
-		foreach ($mounts as $mount) {
267
-			$result[] = $mount->getMountPoint();
268
-		}
269
-		return $result;
270
-	}
271
-
272
-	/**
273
-	 * get the storage mounted at $mountPoint
274
-	 *
275
-	 * @param string $mountPoint
276
-	 * @return \OC\Files\Storage\Storage|null
277
-	 */
278
-	public static function getStorage($mountPoint) {
279
-		$mount = self::getMountManager()->find($mountPoint);
280
-		return $mount->getStorage();
281
-	}
282
-
283
-	/**
284
-	 * @param string $id
285
-	 * @return Mount\MountPoint[]
286
-	 */
287
-	public static function getMountByStorageId($id) {
288
-		return self::getMountManager()->findByStorageId($id);
289
-	}
290
-
291
-	/**
292
-	 * @param int $id
293
-	 * @return Mount\MountPoint[]
294
-	 */
295
-	public static function getMountByNumericId($id) {
296
-		return self::getMountManager()->findByNumericId($id);
297
-	}
298
-
299
-	/**
300
-	 * resolve a path to a storage and internal path
301
-	 *
302
-	 * @param string $path
303
-	 * @return array{?\OCP\Files\Storage\IStorage, string} an array consisting of the storage and the internal path
304
-	 */
305
-	public static function resolvePath($path): array {
306
-		$mount = self::getMountManager()->find($path);
307
-		return [$mount->getStorage(), rtrim($mount->getInternalPath($path), '/')];
308
-	}
309
-
310
-	public static function init(string|IUser|null $user, string $root): bool {
311
-		if (self::$defaultInstance) {
312
-			return false;
313
-		}
314
-		self::initInternal($root);
315
-
316
-		//load custom mount config
317
-		self::initMountPoints($user);
318
-
319
-		return true;
320
-	}
321
-
322
-	public static function initInternal(string $root): bool {
323
-		if (self::$defaultInstance) {
324
-			return false;
325
-		}
326
-		self::getLoader();
327
-		self::$defaultInstance = new View($root);
328
-		/** @var IEventDispatcher $eventDispatcher */
329
-		$eventDispatcher = \OC::$server->get(IEventDispatcher::class);
330
-		$eventDispatcher->addListener(FilesystemTornDownEvent::class, function () {
331
-			self::$defaultInstance = null;
332
-			self::$loaded = false;
333
-		});
334
-
335
-		self::initMountManager();
336
-
337
-		self::$loaded = true;
338
-
339
-		return true;
340
-	}
341
-
342
-	public static function initMountManager(): void {
343
-		if (!self::$mounts) {
344
-			self::$mounts = \OC::$server->get(IMountManager::class);
345
-		}
346
-	}
347
-
348
-	/**
349
-	 * Initialize system and personal mount points for a user
350
-	 *
351
-	 * @throws \OC\User\NoUserException if the user is not available
352
-	 */
353
-	public static function initMountPoints(string|IUser|null $user = ''): void {
354
-		/** @var IUserManager $userManager */
355
-		$userManager = \OC::$server->get(IUserManager::class);
356
-
357
-		$userObject = ($user instanceof IUser) ? $user : $userManager->get($user);
358
-		if ($userObject) {
359
-			/** @var SetupManager $setupManager */
360
-			$setupManager = \OC::$server->get(SetupManager::class);
361
-			$setupManager->setupForUser($userObject);
362
-		} else {
363
-			throw new NoUserException();
364
-		}
365
-	}
366
-
367
-	/**
368
-	 * Get the default filesystem view
369
-	 */
370
-	public static function getView(): ?View {
371
-		if (!self::$defaultInstance) {
372
-			/** @var IUserSession $session */
373
-			$session = \OC::$server->get(IUserSession::class);
374
-			$user = $session->getUser();
375
-			if ($user) {
376
-				$userDir = '/' . $user->getUID() . '/files';
377
-				self::initInternal($userDir);
378
-			}
379
-		}
380
-		return self::$defaultInstance;
381
-	}
382
-
383
-	/**
384
-	 * tear down the filesystem, removing all storage providers
385
-	 */
386
-	public static function tearDown() {
387
-		\OC_Util::tearDownFS();
388
-	}
389
-
390
-	/**
391
-	 * get the relative path of the root data directory for the current user
392
-	 *
393
-	 * @return ?string
394
-	 *
395
-	 * Returns path like /admin/files
396
-	 */
397
-	public static function getRoot() {
398
-		if (!self::$defaultInstance) {
399
-			return null;
400
-		}
401
-		return self::$defaultInstance->getRoot();
402
-	}
403
-
404
-	/**
405
-	 * mount an \OC\Files\Storage\Storage in our virtual filesystem
406
-	 *
407
-	 * @param \OC\Files\Storage\Storage|string $class
408
-	 * @param array $arguments
409
-	 * @param string $mountpoint
410
-	 */
411
-	public static function mount($class, $arguments, $mountpoint) {
412
-		if (!self::$mounts) {
413
-			\OC_Util::setupFS();
414
-		}
415
-		$mount = new Mount\MountPoint($class, $mountpoint, $arguments, self::getLoader());
416
-		self::$mounts->addMount($mount);
417
-	}
418
-
419
-	/**
420
-	 * return the path to a local version of the file
421
-	 * we need this because we can't know if a file is stored local or not from
422
-	 * outside the filestorage and for some purposes a local file is needed
423
-	 */
424
-	public static function getLocalFile(string $path): string|false {
425
-		return self::$defaultInstance->getLocalFile($path);
426
-	}
427
-
428
-	/**
429
-	 * return path to file which reflects one visible in browser
430
-	 *
431
-	 * @param string $path
432
-	 * @return string
433
-	 */
434
-	public static function getLocalPath($path) {
435
-		$datadir = \OC_User::getHome(\OC_User::getUser()) . '/files';
436
-		$newpath = $path;
437
-		if (strncmp($newpath, $datadir, strlen($datadir)) == 0) {
438
-			$newpath = substr($path, strlen($datadir));
439
-		}
440
-		return $newpath;
441
-	}
442
-
443
-	/**
444
-	 * check if the requested path is valid
445
-	 *
446
-	 * @param string $path
447
-	 * @return bool
448
-	 */
449
-	public static function isValidPath($path) {
450
-		$path = self::normalizePath($path);
451
-		if (!$path || $path[0] !== '/') {
452
-			$path = '/' . $path;
453
-		}
454
-		if (str_contains($path, '/../') || strrchr($path, '/') === '/..') {
455
-			return false;
456
-		}
457
-		return true;
458
-	}
459
-
460
-	/**
461
-	 * @param string $filename
462
-	 * @return bool
463
-	 */
464
-	public static function isFileBlacklisted($filename) {
465
-		$filename = self::normalizePath($filename);
466
-
467
-		if (self::$blacklist === null) {
468
-			self::$blacklist = \OC::$server->getConfig()->getSystemValue('blacklisted_files', ['.htaccess']);
469
-		}
470
-
471
-		$filename = strtolower(basename($filename));
472
-		return in_array($filename, self::$blacklist);
473
-	}
474
-
475
-	/**
476
-	 * check if the directory should be ignored when scanning
477
-	 * NOTE: the special directories . and .. would cause never ending recursion
478
-	 *
479
-	 * @param string $dir
480
-	 * @return boolean
481
-	 */
482
-	public static function isIgnoredDir($dir) {
483
-		if ($dir === '.' || $dir === '..') {
484
-			return true;
485
-		}
486
-		return false;
487
-	}
488
-
489
-	/**
490
-	 * following functions are equivalent to their php builtin equivalents for arguments/return values.
491
-	 */
492
-	public static function mkdir($path) {
493
-		return self::$defaultInstance->mkdir($path);
494
-	}
495
-
496
-	public static function rmdir($path) {
497
-		return self::$defaultInstance->rmdir($path);
498
-	}
499
-
500
-	public static function is_dir($path) {
501
-		return self::$defaultInstance->is_dir($path);
502
-	}
503
-
504
-	public static function is_file($path) {
505
-		return self::$defaultInstance->is_file($path);
506
-	}
507
-
508
-	public static function stat($path) {
509
-		return self::$defaultInstance->stat($path);
510
-	}
511
-
512
-	public static function filetype($path) {
513
-		return self::$defaultInstance->filetype($path);
514
-	}
515
-
516
-	public static function filesize($path) {
517
-		return self::$defaultInstance->filesize($path);
518
-	}
519
-
520
-	public static function readfile($path) {
521
-		return self::$defaultInstance->readfile($path);
522
-	}
523
-
524
-	public static function isCreatable($path) {
525
-		return self::$defaultInstance->isCreatable($path);
526
-	}
527
-
528
-	public static function isReadable($path) {
529
-		return self::$defaultInstance->isReadable($path);
530
-	}
531
-
532
-	public static function isUpdatable($path) {
533
-		return self::$defaultInstance->isUpdatable($path);
534
-	}
535
-
536
-	public static function isDeletable($path) {
537
-		return self::$defaultInstance->isDeletable($path);
538
-	}
539
-
540
-	public static function isSharable($path) {
541
-		return self::$defaultInstance->isSharable($path);
542
-	}
543
-
544
-	public static function file_exists($path) {
545
-		return self::$defaultInstance->file_exists($path);
546
-	}
547
-
548
-	public static function filemtime($path) {
549
-		return self::$defaultInstance->filemtime($path);
550
-	}
551
-
552
-	public static function touch($path, $mtime = null) {
553
-		return self::$defaultInstance->touch($path, $mtime);
554
-	}
555
-
556
-	/**
557
-	 * @return string|false
558
-	 */
559
-	public static function file_get_contents($path) {
560
-		return self::$defaultInstance->file_get_contents($path);
561
-	}
562
-
563
-	public static function file_put_contents($path, $data) {
564
-		return self::$defaultInstance->file_put_contents($path, $data);
565
-	}
566
-
567
-	public static function unlink($path) {
568
-		return self::$defaultInstance->unlink($path);
569
-	}
570
-
571
-	public static function rename($source, $target) {
572
-		return self::$defaultInstance->rename($source, $target);
573
-	}
574
-
575
-	public static function copy($source, $target) {
576
-		return self::$defaultInstance->copy($source, $target);
577
-	}
578
-
579
-	public static function fopen($path, $mode) {
580
-		return self::$defaultInstance->fopen($path, $mode);
581
-	}
582
-
583
-	/**
584
-	 * @param string $path
585
-	 * @throws \OCP\Files\InvalidPathException
586
-	 */
587
-	public static function toTmpFile($path): string|false {
588
-		return self::$defaultInstance->toTmpFile($path);
589
-	}
590
-
591
-	public static function fromTmpFile($tmpFile, $path) {
592
-		return self::$defaultInstance->fromTmpFile($tmpFile, $path);
593
-	}
594
-
595
-	public static function getMimeType($path) {
596
-		return self::$defaultInstance->getMimeType($path);
597
-	}
598
-
599
-	public static function hash($type, $path, $raw = false) {
600
-		return self::$defaultInstance->hash($type, $path, $raw);
601
-	}
602
-
603
-	public static function free_space($path = '/') {
604
-		return self::$defaultInstance->free_space($path);
605
-	}
606
-
607
-	public static function search($query) {
608
-		return self::$defaultInstance->search($query);
609
-	}
610
-
611
-	/**
612
-	 * @param string $query
613
-	 */
614
-	public static function searchByMime($query) {
615
-		return self::$defaultInstance->searchByMime($query);
616
-	}
617
-
618
-	/**
619
-	 * @param string|int $tag name or tag id
620
-	 * @param string $userId owner of the tags
621
-	 * @return FileInfo[] array or file info
622
-	 */
623
-	public static function searchByTag($tag, $userId) {
624
-		return self::$defaultInstance->searchByTag($tag, $userId);
625
-	}
626
-
627
-	/**
628
-	 * check if a file or folder has been updated since $time
629
-	 *
630
-	 * @param string $path
631
-	 * @param int $time
632
-	 * @return bool
633
-	 */
634
-	public static function hasUpdated($path, $time) {
635
-		return self::$defaultInstance->hasUpdated($path, $time);
636
-	}
637
-
638
-	/**
639
-	 * Fix common problems with a file path
640
-	 *
641
-	 * @param string $path
642
-	 * @param bool $stripTrailingSlash whether to strip the trailing slash
643
-	 * @param bool $isAbsolutePath whether the given path is absolute
644
-	 * @param bool $keepUnicode true to disable unicode normalization
645
-	 * @psalm-taint-escape file
646
-	 * @return string
647
-	 */
648
-	public static function normalizePath($path, $stripTrailingSlash = true, $isAbsolutePath = false, $keepUnicode = false) {
649
-		/**
650
-		 * FIXME: This is a workaround for existing classes and files which call
651
-		 *        this function with another type than a valid string. This
652
-		 *        conversion should get removed as soon as all existing
653
-		 *        function calls have been fixed.
654
-		 */
655
-		$path = (string)$path;
656
-
657
-		if ($path === '') {
658
-			return '/';
659
-		}
660
-
661
-		if (is_null(self::$normalizedPathCache)) {
662
-			self::$normalizedPathCache = new CappedMemoryCache(2048);
663
-		}
664
-
665
-		$cacheKey = json_encode([$path, $stripTrailingSlash, $isAbsolutePath, $keepUnicode]);
666
-
667
-		if ($cacheKey && isset(self::$normalizedPathCache[$cacheKey])) {
668
-			return self::$normalizedPathCache[$cacheKey];
669
-		}
670
-
671
-		//normalize unicode if possible
672
-		if (!$keepUnicode) {
673
-			$path = \OC_Util::normalizeUnicode($path);
674
-		}
675
-
676
-		//add leading slash, if it is already there we strip it anyway
677
-		$path = '/' . $path;
678
-
679
-		$patterns = [
680
-			'#\\\\#s',       // no windows style '\\' slashes
681
-			'#/\.(/\.)*/#s', // remove '/./'
682
-			'#\//+#s',       // remove sequence of slashes
683
-			'#/\.$#s',       // remove trailing '/.'
684
-		];
685
-
686
-		do {
687
-			$count = 0;
688
-			$path = preg_replace($patterns, '/', $path, -1, $count);
689
-		} while ($count > 0);
690
-
691
-		//remove trailing slash
692
-		if ($stripTrailingSlash && strlen($path) > 1) {
693
-			$path = rtrim($path, '/');
694
-		}
695
-
696
-		self::$normalizedPathCache[$cacheKey] = $path;
697
-
698
-		return $path;
699
-	}
700
-
701
-	/**
702
-	 * get the filesystem info
703
-	 *
704
-	 * @param string $path
705
-	 * @param bool|string $includeMountPoints whether to add mountpoint sizes,
706
-	 * defaults to true
707
-	 * @return \OC\Files\FileInfo|false False if file does not exist
708
-	 */
709
-	public static function getFileInfo($path, $includeMountPoints = true) {
710
-		return self::getView()->getFileInfo($path, $includeMountPoints);
711
-	}
712
-
713
-	/**
714
-	 * change file metadata
715
-	 *
716
-	 * @param string $path
717
-	 * @param array $data
718
-	 * @return int
719
-	 *
720
-	 * returns the fileid of the updated file
721
-	 */
722
-	public static function putFileInfo($path, $data) {
723
-		return self::$defaultInstance->putFileInfo($path, $data);
724
-	}
725
-
726
-	/**
727
-	 * get the content of a directory
728
-	 *
729
-	 * @param string $directory path under datadirectory
730
-	 * @param string $mimetype_filter limit returned content to this mimetype or mimepart
731
-	 * @return \OC\Files\FileInfo[]
732
-	 */
733
-	public static function getDirectoryContent($directory, $mimetype_filter = '') {
734
-		return self::$defaultInstance->getDirectoryContent($directory, $mimetype_filter);
735
-	}
736
-
737
-	/**
738
-	 * Get the path of a file by id
739
-	 *
740
-	 * Note that the resulting path is not guaranteed to be unique for the id, multiple paths can point to the same file
741
-	 *
742
-	 * @param int $id
743
-	 * @throws NotFoundException
744
-	 * @return string
745
-	 */
746
-	public static function getPath($id) {
747
-		return self::$defaultInstance->getPath($id);
748
-	}
749
-
750
-	/**
751
-	 * Get the owner for a file or folder
752
-	 *
753
-	 * @param string $path
754
-	 * @return string
755
-	 */
756
-	public static function getOwner($path) {
757
-		return self::$defaultInstance->getOwner($path);
758
-	}
759
-
760
-	/**
761
-	 * get the ETag for a file or folder
762
-	 */
763
-	public static function getETag(string $path): string|false {
764
-		return self::$defaultInstance->getETag($path);
765
-	}
53
+    private static ?Mount\Manager $mounts = null;
54
+
55
+    public static bool $loaded = false;
56
+
57
+    private static ?View $defaultInstance = null;
58
+
59
+    private static ?CappedMemoryCache $normalizedPathCache = null;
60
+
61
+    /** @var string[]|null */
62
+    private static ?array $blacklist = null;
63
+
64
+    /**
65
+     * classname which used for hooks handling
66
+     * used as signalclass in OC_Hooks::emit()
67
+     */
68
+    public const CLASSNAME = 'OC_Filesystem';
69
+
70
+    /**
71
+     * signalname emitted before file renaming
72
+     *
73
+     * @param string $oldpath
74
+     * @param string $newpath
75
+     */
76
+    public const signal_rename = 'rename';
77
+
78
+    /**
79
+     * signal emitted after file renaming
80
+     *
81
+     * @param string $oldpath
82
+     * @param string $newpath
83
+     */
84
+    public const signal_post_rename = 'post_rename';
85
+
86
+    /**
87
+     * signal emitted before file/dir creation
88
+     *
89
+     * @param string $path
90
+     * @param bool $run changing this flag to false in hook handler will cancel event
91
+     */
92
+    public const signal_create = 'create';
93
+
94
+    /**
95
+     * signal emitted after file/dir creation
96
+     *
97
+     * @param string $path
98
+     * @param bool $run changing this flag to false in hook handler will cancel event
99
+     */
100
+    public const signal_post_create = 'post_create';
101
+
102
+    /**
103
+     * signal emits before file/dir copy
104
+     *
105
+     * @param string $oldpath
106
+     * @param string $newpath
107
+     * @param bool $run changing this flag to false in hook handler will cancel event
108
+     */
109
+    public const signal_copy = 'copy';
110
+
111
+    /**
112
+     * signal emits after file/dir copy
113
+     *
114
+     * @param string $oldpath
115
+     * @param string $newpath
116
+     */
117
+    public const signal_post_copy = 'post_copy';
118
+
119
+    /**
120
+     * signal emits before file/dir save
121
+     *
122
+     * @param string $path
123
+     * @param bool $run changing this flag to false in hook handler will cancel event
124
+     */
125
+    public const signal_write = 'write';
126
+
127
+    /**
128
+     * signal emits after file/dir save
129
+     *
130
+     * @param string $path
131
+     */
132
+    public const signal_post_write = 'post_write';
133
+
134
+    /**
135
+     * signal emitted before file/dir update
136
+     *
137
+     * @param string $path
138
+     * @param bool $run changing this flag to false in hook handler will cancel event
139
+     */
140
+    public const signal_update = 'update';
141
+
142
+    /**
143
+     * signal emitted after file/dir update
144
+     *
145
+     * @param string $path
146
+     * @param bool $run changing this flag to false in hook handler will cancel event
147
+     */
148
+    public const signal_post_update = 'post_update';
149
+
150
+    /**
151
+     * signal emits when reading file/dir
152
+     *
153
+     * @param string $path
154
+     */
155
+    public const signal_read = 'read';
156
+
157
+    /**
158
+     * signal emits when removing file/dir
159
+     *
160
+     * @param string $path
161
+     */
162
+    public const signal_delete = 'delete';
163
+
164
+    /**
165
+     * parameters definitions for signals
166
+     */
167
+    public const signal_param_path = 'path';
168
+    public const signal_param_oldpath = 'oldpath';
169
+    public const signal_param_newpath = 'newpath';
170
+
171
+    /**
172
+     * run - changing this flag to false in hook handler will cancel event
173
+     */
174
+    public const signal_param_run = 'run';
175
+
176
+    public const signal_create_mount = 'create_mount';
177
+    public const signal_delete_mount = 'delete_mount';
178
+    public const signal_param_mount_type = 'mounttype';
179
+    public const signal_param_users = 'users';
180
+
181
+    private static ?\OC\Files\Storage\StorageFactory $loader = null;
182
+
183
+    private static bool $logWarningWhenAddingStorageWrapper = true;
184
+
185
+    /**
186
+     * @param bool $shouldLog
187
+     * @return bool previous value
188
+     * @internal
189
+     */
190
+    public static function logWarningWhenAddingStorageWrapper(bool $shouldLog): bool {
191
+        $previousValue = self::$logWarningWhenAddingStorageWrapper;
192
+        self::$logWarningWhenAddingStorageWrapper = $shouldLog;
193
+        return $previousValue;
194
+    }
195
+
196
+    /**
197
+     * @param string $wrapperName
198
+     * @param callable $wrapper
199
+     * @param int $priority
200
+     */
201
+    public static function addStorageWrapper($wrapperName, $wrapper, $priority = 50) {
202
+        if (self::$logWarningWhenAddingStorageWrapper) {
203
+            \OC::$server->getLogger()->warning("Storage wrapper '{wrapper}' was not registered via the 'OC_Filesystem - preSetup' hook which could cause potential problems.", [
204
+                'wrapper' => $wrapperName,
205
+                'app' => 'filesystem',
206
+            ]);
207
+        }
208
+
209
+        $mounts = self::getMountManager()->getAll();
210
+        if (!self::getLoader()->addStorageWrapper($wrapperName, $wrapper, $priority, $mounts)) {
211
+            // do not re-wrap if storage with this name already existed
212
+            return;
213
+        }
214
+    }
215
+
216
+    /**
217
+     * Returns the storage factory
218
+     *
219
+     * @return IStorageFactory
220
+     */
221
+    public static function getLoader() {
222
+        if (!self::$loader) {
223
+            self::$loader = \OC::$server->get(IStorageFactory::class);
224
+        }
225
+        return self::$loader;
226
+    }
227
+
228
+    /**
229
+     * Returns the mount manager
230
+     */
231
+    public static function getMountManager(): Mount\Manager {
232
+        self::initMountManager();
233
+        assert(self::$mounts !== null);
234
+        return self::$mounts;
235
+    }
236
+
237
+    /**
238
+     * get the mountpoint of the storage object for a path
239
+     * ( note: because a storage is not always mounted inside the fakeroot, the
240
+     * returned mountpoint is relative to the absolute root of the filesystem
241
+     * and doesn't take the chroot into account )
242
+     *
243
+     * @param string $path
244
+     * @return string
245
+     */
246
+    public static function getMountPoint($path) {
247
+        if (!self::$mounts) {
248
+            \OC_Util::setupFS();
249
+        }
250
+        $mount = self::$mounts->find($path);
251
+        return $mount->getMountPoint();
252
+    }
253
+
254
+    /**
255
+     * get a list of all mount points in a directory
256
+     *
257
+     * @param string $path
258
+     * @return string[]
259
+     */
260
+    public static function getMountPoints($path) {
261
+        if (!self::$mounts) {
262
+            \OC_Util::setupFS();
263
+        }
264
+        $result = [];
265
+        $mounts = self::$mounts->findIn($path);
266
+        foreach ($mounts as $mount) {
267
+            $result[] = $mount->getMountPoint();
268
+        }
269
+        return $result;
270
+    }
271
+
272
+    /**
273
+     * get the storage mounted at $mountPoint
274
+     *
275
+     * @param string $mountPoint
276
+     * @return \OC\Files\Storage\Storage|null
277
+     */
278
+    public static function getStorage($mountPoint) {
279
+        $mount = self::getMountManager()->find($mountPoint);
280
+        return $mount->getStorage();
281
+    }
282
+
283
+    /**
284
+     * @param string $id
285
+     * @return Mount\MountPoint[]
286
+     */
287
+    public static function getMountByStorageId($id) {
288
+        return self::getMountManager()->findByStorageId($id);
289
+    }
290
+
291
+    /**
292
+     * @param int $id
293
+     * @return Mount\MountPoint[]
294
+     */
295
+    public static function getMountByNumericId($id) {
296
+        return self::getMountManager()->findByNumericId($id);
297
+    }
298
+
299
+    /**
300
+     * resolve a path to a storage and internal path
301
+     *
302
+     * @param string $path
303
+     * @return array{?\OCP\Files\Storage\IStorage, string} an array consisting of the storage and the internal path
304
+     */
305
+    public static function resolvePath($path): array {
306
+        $mount = self::getMountManager()->find($path);
307
+        return [$mount->getStorage(), rtrim($mount->getInternalPath($path), '/')];
308
+    }
309
+
310
+    public static function init(string|IUser|null $user, string $root): bool {
311
+        if (self::$defaultInstance) {
312
+            return false;
313
+        }
314
+        self::initInternal($root);
315
+
316
+        //load custom mount config
317
+        self::initMountPoints($user);
318
+
319
+        return true;
320
+    }
321
+
322
+    public static function initInternal(string $root): bool {
323
+        if (self::$defaultInstance) {
324
+            return false;
325
+        }
326
+        self::getLoader();
327
+        self::$defaultInstance = new View($root);
328
+        /** @var IEventDispatcher $eventDispatcher */
329
+        $eventDispatcher = \OC::$server->get(IEventDispatcher::class);
330
+        $eventDispatcher->addListener(FilesystemTornDownEvent::class, function () {
331
+            self::$defaultInstance = null;
332
+            self::$loaded = false;
333
+        });
334
+
335
+        self::initMountManager();
336
+
337
+        self::$loaded = true;
338
+
339
+        return true;
340
+    }
341
+
342
+    public static function initMountManager(): void {
343
+        if (!self::$mounts) {
344
+            self::$mounts = \OC::$server->get(IMountManager::class);
345
+        }
346
+    }
347
+
348
+    /**
349
+     * Initialize system and personal mount points for a user
350
+     *
351
+     * @throws \OC\User\NoUserException if the user is not available
352
+     */
353
+    public static function initMountPoints(string|IUser|null $user = ''): void {
354
+        /** @var IUserManager $userManager */
355
+        $userManager = \OC::$server->get(IUserManager::class);
356
+
357
+        $userObject = ($user instanceof IUser) ? $user : $userManager->get($user);
358
+        if ($userObject) {
359
+            /** @var SetupManager $setupManager */
360
+            $setupManager = \OC::$server->get(SetupManager::class);
361
+            $setupManager->setupForUser($userObject);
362
+        } else {
363
+            throw new NoUserException();
364
+        }
365
+    }
366
+
367
+    /**
368
+     * Get the default filesystem view
369
+     */
370
+    public static function getView(): ?View {
371
+        if (!self::$defaultInstance) {
372
+            /** @var IUserSession $session */
373
+            $session = \OC::$server->get(IUserSession::class);
374
+            $user = $session->getUser();
375
+            if ($user) {
376
+                $userDir = '/' . $user->getUID() . '/files';
377
+                self::initInternal($userDir);
378
+            }
379
+        }
380
+        return self::$defaultInstance;
381
+    }
382
+
383
+    /**
384
+     * tear down the filesystem, removing all storage providers
385
+     */
386
+    public static function tearDown() {
387
+        \OC_Util::tearDownFS();
388
+    }
389
+
390
+    /**
391
+     * get the relative path of the root data directory for the current user
392
+     *
393
+     * @return ?string
394
+     *
395
+     * Returns path like /admin/files
396
+     */
397
+    public static function getRoot() {
398
+        if (!self::$defaultInstance) {
399
+            return null;
400
+        }
401
+        return self::$defaultInstance->getRoot();
402
+    }
403
+
404
+    /**
405
+     * mount an \OC\Files\Storage\Storage in our virtual filesystem
406
+     *
407
+     * @param \OC\Files\Storage\Storage|string $class
408
+     * @param array $arguments
409
+     * @param string $mountpoint
410
+     */
411
+    public static function mount($class, $arguments, $mountpoint) {
412
+        if (!self::$mounts) {
413
+            \OC_Util::setupFS();
414
+        }
415
+        $mount = new Mount\MountPoint($class, $mountpoint, $arguments, self::getLoader());
416
+        self::$mounts->addMount($mount);
417
+    }
418
+
419
+    /**
420
+     * return the path to a local version of the file
421
+     * we need this because we can't know if a file is stored local or not from
422
+     * outside the filestorage and for some purposes a local file is needed
423
+     */
424
+    public static function getLocalFile(string $path): string|false {
425
+        return self::$defaultInstance->getLocalFile($path);
426
+    }
427
+
428
+    /**
429
+     * return path to file which reflects one visible in browser
430
+     *
431
+     * @param string $path
432
+     * @return string
433
+     */
434
+    public static function getLocalPath($path) {
435
+        $datadir = \OC_User::getHome(\OC_User::getUser()) . '/files';
436
+        $newpath = $path;
437
+        if (strncmp($newpath, $datadir, strlen($datadir)) == 0) {
438
+            $newpath = substr($path, strlen($datadir));
439
+        }
440
+        return $newpath;
441
+    }
442
+
443
+    /**
444
+     * check if the requested path is valid
445
+     *
446
+     * @param string $path
447
+     * @return bool
448
+     */
449
+    public static function isValidPath($path) {
450
+        $path = self::normalizePath($path);
451
+        if (!$path || $path[0] !== '/') {
452
+            $path = '/' . $path;
453
+        }
454
+        if (str_contains($path, '/../') || strrchr($path, '/') === '/..') {
455
+            return false;
456
+        }
457
+        return true;
458
+    }
459
+
460
+    /**
461
+     * @param string $filename
462
+     * @return bool
463
+     */
464
+    public static function isFileBlacklisted($filename) {
465
+        $filename = self::normalizePath($filename);
466
+
467
+        if (self::$blacklist === null) {
468
+            self::$blacklist = \OC::$server->getConfig()->getSystemValue('blacklisted_files', ['.htaccess']);
469
+        }
470
+
471
+        $filename = strtolower(basename($filename));
472
+        return in_array($filename, self::$blacklist);
473
+    }
474
+
475
+    /**
476
+     * check if the directory should be ignored when scanning
477
+     * NOTE: the special directories . and .. would cause never ending recursion
478
+     *
479
+     * @param string $dir
480
+     * @return boolean
481
+     */
482
+    public static function isIgnoredDir($dir) {
483
+        if ($dir === '.' || $dir === '..') {
484
+            return true;
485
+        }
486
+        return false;
487
+    }
488
+
489
+    /**
490
+     * following functions are equivalent to their php builtin equivalents for arguments/return values.
491
+     */
492
+    public static function mkdir($path) {
493
+        return self::$defaultInstance->mkdir($path);
494
+    }
495
+
496
+    public static function rmdir($path) {
497
+        return self::$defaultInstance->rmdir($path);
498
+    }
499
+
500
+    public static function is_dir($path) {
501
+        return self::$defaultInstance->is_dir($path);
502
+    }
503
+
504
+    public static function is_file($path) {
505
+        return self::$defaultInstance->is_file($path);
506
+    }
507
+
508
+    public static function stat($path) {
509
+        return self::$defaultInstance->stat($path);
510
+    }
511
+
512
+    public static function filetype($path) {
513
+        return self::$defaultInstance->filetype($path);
514
+    }
515
+
516
+    public static function filesize($path) {
517
+        return self::$defaultInstance->filesize($path);
518
+    }
519
+
520
+    public static function readfile($path) {
521
+        return self::$defaultInstance->readfile($path);
522
+    }
523
+
524
+    public static function isCreatable($path) {
525
+        return self::$defaultInstance->isCreatable($path);
526
+    }
527
+
528
+    public static function isReadable($path) {
529
+        return self::$defaultInstance->isReadable($path);
530
+    }
531
+
532
+    public static function isUpdatable($path) {
533
+        return self::$defaultInstance->isUpdatable($path);
534
+    }
535
+
536
+    public static function isDeletable($path) {
537
+        return self::$defaultInstance->isDeletable($path);
538
+    }
539
+
540
+    public static function isSharable($path) {
541
+        return self::$defaultInstance->isSharable($path);
542
+    }
543
+
544
+    public static function file_exists($path) {
545
+        return self::$defaultInstance->file_exists($path);
546
+    }
547
+
548
+    public static function filemtime($path) {
549
+        return self::$defaultInstance->filemtime($path);
550
+    }
551
+
552
+    public static function touch($path, $mtime = null) {
553
+        return self::$defaultInstance->touch($path, $mtime);
554
+    }
555
+
556
+    /**
557
+     * @return string|false
558
+     */
559
+    public static function file_get_contents($path) {
560
+        return self::$defaultInstance->file_get_contents($path);
561
+    }
562
+
563
+    public static function file_put_contents($path, $data) {
564
+        return self::$defaultInstance->file_put_contents($path, $data);
565
+    }
566
+
567
+    public static function unlink($path) {
568
+        return self::$defaultInstance->unlink($path);
569
+    }
570
+
571
+    public static function rename($source, $target) {
572
+        return self::$defaultInstance->rename($source, $target);
573
+    }
574
+
575
+    public static function copy($source, $target) {
576
+        return self::$defaultInstance->copy($source, $target);
577
+    }
578
+
579
+    public static function fopen($path, $mode) {
580
+        return self::$defaultInstance->fopen($path, $mode);
581
+    }
582
+
583
+    /**
584
+     * @param string $path
585
+     * @throws \OCP\Files\InvalidPathException
586
+     */
587
+    public static function toTmpFile($path): string|false {
588
+        return self::$defaultInstance->toTmpFile($path);
589
+    }
590
+
591
+    public static function fromTmpFile($tmpFile, $path) {
592
+        return self::$defaultInstance->fromTmpFile($tmpFile, $path);
593
+    }
594
+
595
+    public static function getMimeType($path) {
596
+        return self::$defaultInstance->getMimeType($path);
597
+    }
598
+
599
+    public static function hash($type, $path, $raw = false) {
600
+        return self::$defaultInstance->hash($type, $path, $raw);
601
+    }
602
+
603
+    public static function free_space($path = '/') {
604
+        return self::$defaultInstance->free_space($path);
605
+    }
606
+
607
+    public static function search($query) {
608
+        return self::$defaultInstance->search($query);
609
+    }
610
+
611
+    /**
612
+     * @param string $query
613
+     */
614
+    public static function searchByMime($query) {
615
+        return self::$defaultInstance->searchByMime($query);
616
+    }
617
+
618
+    /**
619
+     * @param string|int $tag name or tag id
620
+     * @param string $userId owner of the tags
621
+     * @return FileInfo[] array or file info
622
+     */
623
+    public static function searchByTag($tag, $userId) {
624
+        return self::$defaultInstance->searchByTag($tag, $userId);
625
+    }
626
+
627
+    /**
628
+     * check if a file or folder has been updated since $time
629
+     *
630
+     * @param string $path
631
+     * @param int $time
632
+     * @return bool
633
+     */
634
+    public static function hasUpdated($path, $time) {
635
+        return self::$defaultInstance->hasUpdated($path, $time);
636
+    }
637
+
638
+    /**
639
+     * Fix common problems with a file path
640
+     *
641
+     * @param string $path
642
+     * @param bool $stripTrailingSlash whether to strip the trailing slash
643
+     * @param bool $isAbsolutePath whether the given path is absolute
644
+     * @param bool $keepUnicode true to disable unicode normalization
645
+     * @psalm-taint-escape file
646
+     * @return string
647
+     */
648
+    public static function normalizePath($path, $stripTrailingSlash = true, $isAbsolutePath = false, $keepUnicode = false) {
649
+        /**
650
+         * FIXME: This is a workaround for existing classes and files which call
651
+         *        this function with another type than a valid string. This
652
+         *        conversion should get removed as soon as all existing
653
+         *        function calls have been fixed.
654
+         */
655
+        $path = (string)$path;
656
+
657
+        if ($path === '') {
658
+            return '/';
659
+        }
660
+
661
+        if (is_null(self::$normalizedPathCache)) {
662
+            self::$normalizedPathCache = new CappedMemoryCache(2048);
663
+        }
664
+
665
+        $cacheKey = json_encode([$path, $stripTrailingSlash, $isAbsolutePath, $keepUnicode]);
666
+
667
+        if ($cacheKey && isset(self::$normalizedPathCache[$cacheKey])) {
668
+            return self::$normalizedPathCache[$cacheKey];
669
+        }
670
+
671
+        //normalize unicode if possible
672
+        if (!$keepUnicode) {
673
+            $path = \OC_Util::normalizeUnicode($path);
674
+        }
675
+
676
+        //add leading slash, if it is already there we strip it anyway
677
+        $path = '/' . $path;
678
+
679
+        $patterns = [
680
+            '#\\\\#s',       // no windows style '\\' slashes
681
+            '#/\.(/\.)*/#s', // remove '/./'
682
+            '#\//+#s',       // remove sequence of slashes
683
+            '#/\.$#s',       // remove trailing '/.'
684
+        ];
685
+
686
+        do {
687
+            $count = 0;
688
+            $path = preg_replace($patterns, '/', $path, -1, $count);
689
+        } while ($count > 0);
690
+
691
+        //remove trailing slash
692
+        if ($stripTrailingSlash && strlen($path) > 1) {
693
+            $path = rtrim($path, '/');
694
+        }
695
+
696
+        self::$normalizedPathCache[$cacheKey] = $path;
697
+
698
+        return $path;
699
+    }
700
+
701
+    /**
702
+     * get the filesystem info
703
+     *
704
+     * @param string $path
705
+     * @param bool|string $includeMountPoints whether to add mountpoint sizes,
706
+     * defaults to true
707
+     * @return \OC\Files\FileInfo|false False if file does not exist
708
+     */
709
+    public static function getFileInfo($path, $includeMountPoints = true) {
710
+        return self::getView()->getFileInfo($path, $includeMountPoints);
711
+    }
712
+
713
+    /**
714
+     * change file metadata
715
+     *
716
+     * @param string $path
717
+     * @param array $data
718
+     * @return int
719
+     *
720
+     * returns the fileid of the updated file
721
+     */
722
+    public static function putFileInfo($path, $data) {
723
+        return self::$defaultInstance->putFileInfo($path, $data);
724
+    }
725
+
726
+    /**
727
+     * get the content of a directory
728
+     *
729
+     * @param string $directory path under datadirectory
730
+     * @param string $mimetype_filter limit returned content to this mimetype or mimepart
731
+     * @return \OC\Files\FileInfo[]
732
+     */
733
+    public static function getDirectoryContent($directory, $mimetype_filter = '') {
734
+        return self::$defaultInstance->getDirectoryContent($directory, $mimetype_filter);
735
+    }
736
+
737
+    /**
738
+     * Get the path of a file by id
739
+     *
740
+     * Note that the resulting path is not guaranteed to be unique for the id, multiple paths can point to the same file
741
+     *
742
+     * @param int $id
743
+     * @throws NotFoundException
744
+     * @return string
745
+     */
746
+    public static function getPath($id) {
747
+        return self::$defaultInstance->getPath($id);
748
+    }
749
+
750
+    /**
751
+     * Get the owner for a file or folder
752
+     *
753
+     * @param string $path
754
+     * @return string
755
+     */
756
+    public static function getOwner($path) {
757
+        return self::$defaultInstance->getOwner($path);
758
+    }
759
+
760
+    /**
761
+     * get the ETag for a file or folder
762
+     */
763
+    public static function getETag(string $path): string|false {
764
+        return self::$defaultInstance->getETag($path);
765
+    }
766 766
 }
Please login to merge, or discard this patch.
Spacing   +17 added lines, -17 removed lines patch added patch discarded remove patch
@@ -50,7 +50,7 @@  discard block
 block discarded – undo
50 50
 use OCP\IUserSession;
51 51
 
52 52
 class Filesystem {
53
-	private static ?Mount\Manager $mounts = null;
53
+	private static ? Mount\Manager $mounts = null;
54 54
 
55 55
 	public static bool $loaded = false;
56 56
 
@@ -59,7 +59,7 @@  discard block
 block discarded – undo
59 59
 	private static ?CappedMemoryCache $normalizedPathCache = null;
60 60
 
61 61
 	/** @var string[]|null */
62
-	private static ?array $blacklist = null;
62
+	private static ? array $blacklist = null;
63 63
 
64 64
 	/**
65 65
 	 * classname which used for hooks handling
@@ -178,7 +178,7 @@  discard block
 block discarded – undo
178 178
 	public const signal_param_mount_type = 'mounttype';
179 179
 	public const signal_param_users = 'users';
180 180
 
181
-	private static ?\OC\Files\Storage\StorageFactory $loader = null;
181
+	private static ? \OC\Files\Storage\StorageFactory $loader = null;
182 182
 
183 183
 	private static bool $logWarningWhenAddingStorageWrapper = true;
184 184
 
@@ -307,7 +307,7 @@  discard block
 block discarded – undo
307 307
 		return [$mount->getStorage(), rtrim($mount->getInternalPath($path), '/')];
308 308
 	}
309 309
 
310
-	public static function init(string|IUser|null $user, string $root): bool {
310
+	public static function init(string | IUser | null $user, string $root): bool {
311 311
 		if (self::$defaultInstance) {
312 312
 			return false;
313 313
 		}
@@ -327,7 +327,7 @@  discard block
 block discarded – undo
327 327
 		self::$defaultInstance = new View($root);
328 328
 		/** @var IEventDispatcher $eventDispatcher */
329 329
 		$eventDispatcher = \OC::$server->get(IEventDispatcher::class);
330
-		$eventDispatcher->addListener(FilesystemTornDownEvent::class, function () {
330
+		$eventDispatcher->addListener(FilesystemTornDownEvent::class, function() {
331 331
 			self::$defaultInstance = null;
332 332
 			self::$loaded = false;
333 333
 		});
@@ -350,7 +350,7 @@  discard block
 block discarded – undo
350 350
 	 *
351 351
 	 * @throws \OC\User\NoUserException if the user is not available
352 352
 	 */
353
-	public static function initMountPoints(string|IUser|null $user = ''): void {
353
+	public static function initMountPoints(string | IUser | null $user = ''): void {
354 354
 		/** @var IUserManager $userManager */
355 355
 		$userManager = \OC::$server->get(IUserManager::class);
356 356
 
@@ -373,7 +373,7 @@  discard block
 block discarded – undo
373 373
 			$session = \OC::$server->get(IUserSession::class);
374 374
 			$user = $session->getUser();
375 375
 			if ($user) {
376
-				$userDir = '/' . $user->getUID() . '/files';
376
+				$userDir = '/'.$user->getUID().'/files';
377 377
 				self::initInternal($userDir);
378 378
 			}
379 379
 		}
@@ -421,7 +421,7 @@  discard block
 block discarded – undo
421 421
 	 * we need this because we can't know if a file is stored local or not from
422 422
 	 * outside the filestorage and for some purposes a local file is needed
423 423
 	 */
424
-	public static function getLocalFile(string $path): string|false {
424
+	public static function getLocalFile(string $path): string | false {
425 425
 		return self::$defaultInstance->getLocalFile($path);
426 426
 	}
427 427
 
@@ -432,7 +432,7 @@  discard block
 block discarded – undo
432 432
 	 * @return string
433 433
 	 */
434 434
 	public static function getLocalPath($path) {
435
-		$datadir = \OC_User::getHome(\OC_User::getUser()) . '/files';
435
+		$datadir = \OC_User::getHome(\OC_User::getUser()).'/files';
436 436
 		$newpath = $path;
437 437
 		if (strncmp($newpath, $datadir, strlen($datadir)) == 0) {
438 438
 			$newpath = substr($path, strlen($datadir));
@@ -449,7 +449,7 @@  discard block
 block discarded – undo
449 449
 	public static function isValidPath($path) {
450 450
 		$path = self::normalizePath($path);
451 451
 		if (!$path || $path[0] !== '/') {
452
-			$path = '/' . $path;
452
+			$path = '/'.$path;
453 453
 		}
454 454
 		if (str_contains($path, '/../') || strrchr($path, '/') === '/..') {
455 455
 			return false;
@@ -584,7 +584,7 @@  discard block
 block discarded – undo
584 584
 	 * @param string $path
585 585
 	 * @throws \OCP\Files\InvalidPathException
586 586
 	 */
587
-	public static function toTmpFile($path): string|false {
587
+	public static function toTmpFile($path): string | false {
588 588
 		return self::$defaultInstance->toTmpFile($path);
589 589
 	}
590 590
 
@@ -652,7 +652,7 @@  discard block
 block discarded – undo
652 652
 		 *        conversion should get removed as soon as all existing
653 653
 		 *        function calls have been fixed.
654 654
 		 */
655
-		$path = (string)$path;
655
+		$path = (string) $path;
656 656
 
657 657
 		if ($path === '') {
658 658
 			return '/';
@@ -674,13 +674,13 @@  discard block
 block discarded – undo
674 674
 		}
675 675
 
676 676
 		//add leading slash, if it is already there we strip it anyway
677
-		$path = '/' . $path;
677
+		$path = '/'.$path;
678 678
 
679 679
 		$patterns = [
680
-			'#\\\\#s',       // no windows style '\\' slashes
680
+			'#\\\\#s', // no windows style '\\' slashes
681 681
 			'#/\.(/\.)*/#s', // remove '/./'
682
-			'#\//+#s',       // remove sequence of slashes
683
-			'#/\.$#s',       // remove trailing '/.'
682
+			'#\//+#s', // remove sequence of slashes
683
+			'#/\.$#s', // remove trailing '/.'
684 684
 		];
685 685
 
686 686
 		do {
@@ -760,7 +760,7 @@  discard block
 block discarded – undo
760 760
 	/**
761 761
 	 * get the ETag for a file or folder
762 762
 	 */
763
-	public static function getETag(string $path): string|false {
763
+	public static function getETag(string $path): string | false {
764 764
 		return self::$defaultInstance->getETag($path);
765 765
 	}
766 766
 }
Please login to merge, or discard this patch.
lib/private/Files/Node/Folder.php 2 patches
Indentation   +398 added lines, -398 removed lines patch added patch discarded remove patch
@@ -50,402 +50,402 @@
 block discarded – undo
50 50
 use OCP\IUserManager;
51 51
 
52 52
 class Folder extends Node implements \OCP\Files\Folder {
53
-	/**
54
-	 * Creates a Folder that represents a non-existing path
55
-	 *
56
-	 * @param string $path path
57
-	 * @return NonExistingFolder non-existing node
58
-	 */
59
-	protected function createNonExistingNode($path) {
60
-		return new NonExistingFolder($this->root, $this->view, $path);
61
-	}
62
-
63
-	/**
64
-	 * @param string $path path relative to the folder
65
-	 * @return string
66
-	 * @throws \OCP\Files\NotPermittedException
67
-	 */
68
-	public function getFullPath($path) {
69
-		$path = $this->normalizePath($path);
70
-		if (!$this->isValidPath($path)) {
71
-			throw new NotPermittedException('Invalid path "' . $path . '"');
72
-		}
73
-		return $this->path . $path;
74
-	}
75
-
76
-	/**
77
-	 * @param string $path
78
-	 * @return string|null
79
-	 */
80
-	public function getRelativePath($path) {
81
-		return PathHelper::getRelativePath($this->getPath(), $path);
82
-	}
83
-
84
-	/**
85
-	 * check if a node is a (grand-)child of the folder
86
-	 *
87
-	 * @param \OC\Files\Node\Node $node
88
-	 * @return bool
89
-	 */
90
-	public function isSubNode($node) {
91
-		return str_starts_with($node->getPath(), $this->path . '/');
92
-	}
93
-
94
-	/**
95
-	 * get the content of this directory
96
-	 *
97
-	 * @return Node[]
98
-	 * @throws \OCP\Files\NotFoundException
99
-	 */
100
-	public function getDirectoryListing() {
101
-		$folderContent = $this->view->getDirectoryContent($this->path, '', $this->getFileInfo(false));
102
-
103
-		return array_map(function (FileInfo $info) {
104
-			if ($info->getMimetype() === FileInfo::MIMETYPE_FOLDER) {
105
-				return new Folder($this->root, $this->view, $info->getPath(), $info, $this);
106
-			} else {
107
-				return new File($this->root, $this->view, $info->getPath(), $info, $this);
108
-			}
109
-		}, $folderContent);
110
-	}
111
-
112
-	/**
113
-	 * @param string $path
114
-	 * @param FileInfo $info
115
-	 * @return File|Folder
116
-	 */
117
-	protected function createNode($path, FileInfo $info = null, bool $infoHasSubMountsIncluded = true) {
118
-		if (is_null($info)) {
119
-			$isDir = $this->view->is_dir($path);
120
-		} else {
121
-			$isDir = $info->getType() === FileInfo::TYPE_FOLDER;
122
-		}
123
-		$parent = dirname($path) === $this->getPath() ? $this : null;
124
-		if ($isDir) {
125
-			return new Folder($this->root, $this->view, $path, $info, $parent, $infoHasSubMountsIncluded);
126
-		} else {
127
-			return new File($this->root, $this->view, $path, $info, $parent);
128
-		}
129
-	}
130
-
131
-	/**
132
-	 * Get the node at $path
133
-	 *
134
-	 * @param string $path
135
-	 * @return \OC\Files\Node\Node
136
-	 * @throws \OCP\Files\NotFoundException
137
-	 */
138
-	public function get($path) {
139
-		return $this->root->get($this->getFullPath($path));
140
-	}
141
-
142
-	/**
143
-	 * @param string $path
144
-	 * @return bool
145
-	 */
146
-	public function nodeExists($path) {
147
-		try {
148
-			$this->get($path);
149
-			return true;
150
-		} catch (NotFoundException $e) {
151
-			return false;
152
-		}
153
-	}
154
-
155
-	/**
156
-	 * @param string $path
157
-	 * @return \OC\Files\Node\Folder
158
-	 * @throws \OCP\Files\NotPermittedException
159
-	 */
160
-	public function newFolder($path) {
161
-		if ($this->checkPermissions(\OCP\Constants::PERMISSION_CREATE)) {
162
-			$fullPath = $this->getFullPath($path);
163
-			$nonExisting = new NonExistingFolder($this->root, $this->view, $fullPath);
164
-			$this->sendHooks(['preWrite', 'preCreate'], [$nonExisting]);
165
-			if (!$this->view->mkdir($fullPath)) {
166
-				throw new NotPermittedException('Could not create folder "' . $fullPath . '"');
167
-			}
168
-			$parent = dirname($fullPath) === $this->getPath() ? $this : null;
169
-			$node = new Folder($this->root, $this->view, $fullPath, null, $parent);
170
-			$this->sendHooks(['postWrite', 'postCreate'], [$node]);
171
-			return $node;
172
-		} else {
173
-			throw new NotPermittedException('No create permission for folder "' . $path . '"');
174
-		}
175
-	}
176
-
177
-	/**
178
-	 * @param string $path
179
-	 * @param string | resource | null $content
180
-	 * @return \OC\Files\Node\File
181
-	 * @throws \OCP\Files\NotPermittedException
182
-	 */
183
-	public function newFile($path, $content = null) {
184
-		if (empty($path)) {
185
-			throw new NotPermittedException('Could not create as provided path is empty');
186
-		}
187
-		if ($this->checkPermissions(\OCP\Constants::PERMISSION_CREATE)) {
188
-			$fullPath = $this->getFullPath($path);
189
-			$nonExisting = new NonExistingFile($this->root, $this->view, $fullPath);
190
-			$this->sendHooks(['preWrite', 'preCreate'], [$nonExisting]);
191
-			if ($content !== null) {
192
-				$result = $this->view->file_put_contents($fullPath, $content);
193
-			} else {
194
-				$result = $this->view->touch($fullPath);
195
-			}
196
-			if ($result === false) {
197
-				throw new NotPermittedException('Could not create path "' . $fullPath . '"');
198
-			}
199
-			$node = new File($this->root, $this->view, $fullPath, null, $this);
200
-			$this->sendHooks(['postWrite', 'postCreate'], [$node]);
201
-			return $node;
202
-		}
203
-		throw new NotPermittedException('No create permission for path "' . $path . '"');
204
-	}
205
-
206
-	private function queryFromOperator(ISearchOperator $operator, string $uid = null, int $limit = 0, int $offset = 0): ISearchQuery {
207
-		if ($uid === null) {
208
-			$user = null;
209
-		} else {
210
-			/** @var IUserManager $userManager */
211
-			$userManager = \OC::$server->query(IUserManager::class);
212
-			$user = $userManager->get($uid);
213
-		}
214
-		return new SearchQuery($operator, $limit, $offset, [], $user);
215
-	}
216
-
217
-	/**
218
-	 * search for files with the name matching $query
219
-	 *
220
-	 * @param string|ISearchQuery $query
221
-	 * @return \OC\Files\Node\Node[]
222
-	 */
223
-	public function search($query) {
224
-		if (is_string($query)) {
225
-			$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $query . '%'));
226
-		}
227
-
228
-		// search is handled by a single query covering all caches that this folder contains
229
-		// this is done by collect
230
-
231
-		$limitToHome = $query->limitToHome();
232
-		if ($limitToHome && count(explode('/', $this->path)) !== 3) {
233
-			throw new \InvalidArgumentException('searching by owner is only allowed in the users home folder');
234
-		}
235
-
236
-		/** @var QuerySearchHelper $searchHelper */
237
-		$searchHelper = \OC::$server->get(QuerySearchHelper::class);
238
-		[$caches, $mountByMountPoint] = $searchHelper->getCachesAndMountPointsForSearch($this->root, $this->path, $limitToHome);
239
-		$resultsPerCache = $searchHelper->searchInCaches($query, $caches);
240
-
241
-		// loop through all results per-cache, constructing the FileInfo object from the CacheEntry and merge them all
242
-		$files = array_merge(...array_map(function (array $results, string $relativeMountPoint) use ($mountByMountPoint) {
243
-			$mount = $mountByMountPoint[$relativeMountPoint];
244
-			return array_map(function (ICacheEntry $result) use ($relativeMountPoint, $mount) {
245
-				return $this->cacheEntryToFileInfo($mount, $relativeMountPoint, $result);
246
-			}, $results);
247
-		}, array_values($resultsPerCache), array_keys($resultsPerCache)));
248
-
249
-		// don't include this folder in the results
250
-		$files = array_filter($files, function (FileInfo $file) {
251
-			return $file->getPath() !== $this->getPath();
252
-		});
253
-
254
-		// since results were returned per-cache, they are no longer fully sorted
255
-		$order = $query->getOrder();
256
-		if ($order) {
257
-			usort($files, function (FileInfo $a, FileInfo $b) use ($order) {
258
-				foreach ($order as $orderField) {
259
-					$cmp = $orderField->sortFileInfo($a, $b);
260
-					if ($cmp !== 0) {
261
-						return $cmp;
262
-					}
263
-				}
264
-				return 0;
265
-			});
266
-		}
267
-
268
-		return array_map(function (FileInfo $file) {
269
-			return $this->createNode($file->getPath(), $file);
270
-		}, $files);
271
-	}
272
-
273
-	private function cacheEntryToFileInfo(IMountPoint $mount, string $appendRoot, ICacheEntry $cacheEntry): FileInfo {
274
-		$cacheEntry['internalPath'] = $cacheEntry['path'];
275
-		$cacheEntry['path'] = rtrim($appendRoot . $cacheEntry->getPath(), '/');
276
-		$subPath = $cacheEntry['path'] !== '' ? '/' . $cacheEntry['path'] : '';
277
-		return new \OC\Files\FileInfo($this->path . $subPath, $mount->getStorage(), $cacheEntry['internalPath'], $cacheEntry, $mount);
278
-	}
279
-
280
-	/**
281
-	 * search for files by mimetype
282
-	 *
283
-	 * @param string $mimetype
284
-	 * @return Node[]
285
-	 */
286
-	public function searchByMime($mimetype) {
287
-		if (!str_contains($mimetype, '/')) {
288
-			$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%'));
289
-		} else {
290
-			$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype));
291
-		}
292
-		return $this->search($query);
293
-	}
294
-
295
-	/**
296
-	 * search for files by tag
297
-	 *
298
-	 * @param string|int $tag name or tag id
299
-	 * @param string $userId owner of the tags
300
-	 * @return Node[]
301
-	 */
302
-	public function searchByTag($tag, $userId) {
303
-		$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'tagname', $tag), $userId);
304
-		return $this->search($query);
305
-	}
306
-
307
-	/**
308
-	 * @param int $id
309
-	 * @return \OC\Files\Node\Node[]
310
-	 */
311
-	public function getById($id) {
312
-		return $this->root->getByIdInPath((int)$id, $this->getPath());
313
-	}
314
-
315
-	protected function getAppDataDirectoryName(): string {
316
-		$instanceId = \OC::$server->getConfig()->getSystemValueString('instanceid');
317
-		return 'appdata_' . $instanceId;
318
-	}
319
-
320
-	/**
321
-	 * In case the path we are currently in is inside the appdata_* folder,
322
-	 * the original getById method does not work, because it can only look inside
323
-	 * the user's mount points. But the user has no mount point for the root storage.
324
-	 *
325
-	 * So in that case we directly check the mount of the root if it contains
326
-	 * the id. If it does we check if the path is inside the path we are working
327
-	 * in.
328
-	 *
329
-	 * @param int $id
330
-	 * @return array
331
-	 */
332
-	protected function getByIdInRootMount(int $id): array {
333
-		$mount = $this->root->getMount('');
334
-		$cacheEntry = $mount->getStorage()->getCache($this->path)->get($id);
335
-		if (!$cacheEntry) {
336
-			return [];
337
-		}
338
-
339
-		$absolutePath = '/' . ltrim($cacheEntry->getPath(), '/');
340
-		$currentPath = rtrim($this->path, '/') . '/';
341
-
342
-		if (!str_starts_with($absolutePath, $currentPath)) {
343
-			return [];
344
-		}
345
-
346
-		return [$this->root->createNode(
347
-			$absolutePath, new \OC\Files\FileInfo(
348
-				$absolutePath,
349
-				$mount->getStorage(),
350
-				$cacheEntry->getPath(),
351
-				$cacheEntry,
352
-				$mount
353
-			))];
354
-	}
355
-
356
-	public function getFreeSpace() {
357
-		return $this->view->free_space($this->path);
358
-	}
359
-
360
-	public function delete() {
361
-		if ($this->checkPermissions(\OCP\Constants::PERMISSION_DELETE)) {
362
-			$this->sendHooks(['preDelete']);
363
-			$fileInfo = $this->getFileInfo();
364
-			$this->view->rmdir($this->path);
365
-			$nonExisting = new NonExistingFolder($this->root, $this->view, $this->path, $fileInfo);
366
-			$this->sendHooks(['postDelete'], [$nonExisting]);
367
-		} else {
368
-			throw new NotPermittedException('No delete permission for path "' . $this->path . '"');
369
-		}
370
-	}
371
-
372
-	/**
373
-	 * Add a suffix to the name in case the file exists
374
-	 *
375
-	 * @param string $name
376
-	 * @return string
377
-	 * @throws NotPermittedException
378
-	 */
379
-	public function getNonExistingName($name) {
380
-		$uniqueName = \OC_Helper::buildNotExistingFileNameForView($this->getPath(), $name, $this->view);
381
-		return trim($this->getRelativePath($uniqueName), '/');
382
-	}
383
-
384
-	/**
385
-	 * @param int $limit
386
-	 * @param int $offset
387
-	 * @return \OCP\Files\Node[]
388
-	 */
389
-	public function getRecent($limit, $offset = 0) {
390
-		$filterOutNonEmptyFolder = new SearchBinaryOperator(
391
-			// filter out non empty folders
392
-			ISearchBinaryOperator::OPERATOR_OR,
393
-			[
394
-				new SearchBinaryOperator(
395
-					ISearchBinaryOperator::OPERATOR_NOT,
396
-					[
397
-						new SearchComparison(
398
-							ISearchComparison::COMPARE_EQUAL,
399
-							'mimetype',
400
-							FileInfo::MIMETYPE_FOLDER
401
-						),
402
-					]
403
-				),
404
-				new SearchComparison(
405
-					ISearchComparison::COMPARE_EQUAL,
406
-					'size',
407
-					0
408
-				),
409
-			]
410
-		);
411
-
412
-		$filterNonRecentFiles = new SearchComparison(
413
-			ISearchComparison::COMPARE_GREATER_THAN,
414
-			'mtime',
415
-			strtotime("-2 week")
416
-		);
417
-		if ($offset === 0 && $limit <= 100) {
418
-			$query = new SearchQuery(
419
-				new SearchBinaryOperator(
420
-					ISearchBinaryOperator::OPERATOR_AND,
421
-					[
422
-						$filterOutNonEmptyFolder,
423
-						$filterNonRecentFiles,
424
-					],
425
-				),
426
-				$limit,
427
-				$offset,
428
-				[
429
-					new SearchOrder(
430
-						ISearchOrder::DIRECTION_DESCENDING,
431
-						'mtime'
432
-					),
433
-				]
434
-			);
435
-		} else {
436
-			$query = new SearchQuery(
437
-				$filterOutNonEmptyFolder,
438
-				$limit,
439
-				$offset,
440
-				[
441
-					new SearchOrder(
442
-						ISearchOrder::DIRECTION_DESCENDING,
443
-						'mtime'
444
-					),
445
-				]
446
-			);
447
-		}
448
-
449
-		return $this->search($query);
450
-	}
53
+    /**
54
+     * Creates a Folder that represents a non-existing path
55
+     *
56
+     * @param string $path path
57
+     * @return NonExistingFolder non-existing node
58
+     */
59
+    protected function createNonExistingNode($path) {
60
+        return new NonExistingFolder($this->root, $this->view, $path);
61
+    }
62
+
63
+    /**
64
+     * @param string $path path relative to the folder
65
+     * @return string
66
+     * @throws \OCP\Files\NotPermittedException
67
+     */
68
+    public function getFullPath($path) {
69
+        $path = $this->normalizePath($path);
70
+        if (!$this->isValidPath($path)) {
71
+            throw new NotPermittedException('Invalid path "' . $path . '"');
72
+        }
73
+        return $this->path . $path;
74
+    }
75
+
76
+    /**
77
+     * @param string $path
78
+     * @return string|null
79
+     */
80
+    public function getRelativePath($path) {
81
+        return PathHelper::getRelativePath($this->getPath(), $path);
82
+    }
83
+
84
+    /**
85
+     * check if a node is a (grand-)child of the folder
86
+     *
87
+     * @param \OC\Files\Node\Node $node
88
+     * @return bool
89
+     */
90
+    public function isSubNode($node) {
91
+        return str_starts_with($node->getPath(), $this->path . '/');
92
+    }
93
+
94
+    /**
95
+     * get the content of this directory
96
+     *
97
+     * @return Node[]
98
+     * @throws \OCP\Files\NotFoundException
99
+     */
100
+    public function getDirectoryListing() {
101
+        $folderContent = $this->view->getDirectoryContent($this->path, '', $this->getFileInfo(false));
102
+
103
+        return array_map(function (FileInfo $info) {
104
+            if ($info->getMimetype() === FileInfo::MIMETYPE_FOLDER) {
105
+                return new Folder($this->root, $this->view, $info->getPath(), $info, $this);
106
+            } else {
107
+                return new File($this->root, $this->view, $info->getPath(), $info, $this);
108
+            }
109
+        }, $folderContent);
110
+    }
111
+
112
+    /**
113
+     * @param string $path
114
+     * @param FileInfo $info
115
+     * @return File|Folder
116
+     */
117
+    protected function createNode($path, FileInfo $info = null, bool $infoHasSubMountsIncluded = true) {
118
+        if (is_null($info)) {
119
+            $isDir = $this->view->is_dir($path);
120
+        } else {
121
+            $isDir = $info->getType() === FileInfo::TYPE_FOLDER;
122
+        }
123
+        $parent = dirname($path) === $this->getPath() ? $this : null;
124
+        if ($isDir) {
125
+            return new Folder($this->root, $this->view, $path, $info, $parent, $infoHasSubMountsIncluded);
126
+        } else {
127
+            return new File($this->root, $this->view, $path, $info, $parent);
128
+        }
129
+    }
130
+
131
+    /**
132
+     * Get the node at $path
133
+     *
134
+     * @param string $path
135
+     * @return \OC\Files\Node\Node
136
+     * @throws \OCP\Files\NotFoundException
137
+     */
138
+    public function get($path) {
139
+        return $this->root->get($this->getFullPath($path));
140
+    }
141
+
142
+    /**
143
+     * @param string $path
144
+     * @return bool
145
+     */
146
+    public function nodeExists($path) {
147
+        try {
148
+            $this->get($path);
149
+            return true;
150
+        } catch (NotFoundException $e) {
151
+            return false;
152
+        }
153
+    }
154
+
155
+    /**
156
+     * @param string $path
157
+     * @return \OC\Files\Node\Folder
158
+     * @throws \OCP\Files\NotPermittedException
159
+     */
160
+    public function newFolder($path) {
161
+        if ($this->checkPermissions(\OCP\Constants::PERMISSION_CREATE)) {
162
+            $fullPath = $this->getFullPath($path);
163
+            $nonExisting = new NonExistingFolder($this->root, $this->view, $fullPath);
164
+            $this->sendHooks(['preWrite', 'preCreate'], [$nonExisting]);
165
+            if (!$this->view->mkdir($fullPath)) {
166
+                throw new NotPermittedException('Could not create folder "' . $fullPath . '"');
167
+            }
168
+            $parent = dirname($fullPath) === $this->getPath() ? $this : null;
169
+            $node = new Folder($this->root, $this->view, $fullPath, null, $parent);
170
+            $this->sendHooks(['postWrite', 'postCreate'], [$node]);
171
+            return $node;
172
+        } else {
173
+            throw new NotPermittedException('No create permission for folder "' . $path . '"');
174
+        }
175
+    }
176
+
177
+    /**
178
+     * @param string $path
179
+     * @param string | resource | null $content
180
+     * @return \OC\Files\Node\File
181
+     * @throws \OCP\Files\NotPermittedException
182
+     */
183
+    public function newFile($path, $content = null) {
184
+        if (empty($path)) {
185
+            throw new NotPermittedException('Could not create as provided path is empty');
186
+        }
187
+        if ($this->checkPermissions(\OCP\Constants::PERMISSION_CREATE)) {
188
+            $fullPath = $this->getFullPath($path);
189
+            $nonExisting = new NonExistingFile($this->root, $this->view, $fullPath);
190
+            $this->sendHooks(['preWrite', 'preCreate'], [$nonExisting]);
191
+            if ($content !== null) {
192
+                $result = $this->view->file_put_contents($fullPath, $content);
193
+            } else {
194
+                $result = $this->view->touch($fullPath);
195
+            }
196
+            if ($result === false) {
197
+                throw new NotPermittedException('Could not create path "' . $fullPath . '"');
198
+            }
199
+            $node = new File($this->root, $this->view, $fullPath, null, $this);
200
+            $this->sendHooks(['postWrite', 'postCreate'], [$node]);
201
+            return $node;
202
+        }
203
+        throw new NotPermittedException('No create permission for path "' . $path . '"');
204
+    }
205
+
206
+    private function queryFromOperator(ISearchOperator $operator, string $uid = null, int $limit = 0, int $offset = 0): ISearchQuery {
207
+        if ($uid === null) {
208
+            $user = null;
209
+        } else {
210
+            /** @var IUserManager $userManager */
211
+            $userManager = \OC::$server->query(IUserManager::class);
212
+            $user = $userManager->get($uid);
213
+        }
214
+        return new SearchQuery($operator, $limit, $offset, [], $user);
215
+    }
216
+
217
+    /**
218
+     * search for files with the name matching $query
219
+     *
220
+     * @param string|ISearchQuery $query
221
+     * @return \OC\Files\Node\Node[]
222
+     */
223
+    public function search($query) {
224
+        if (is_string($query)) {
225
+            $query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $query . '%'));
226
+        }
227
+
228
+        // search is handled by a single query covering all caches that this folder contains
229
+        // this is done by collect
230
+
231
+        $limitToHome = $query->limitToHome();
232
+        if ($limitToHome && count(explode('/', $this->path)) !== 3) {
233
+            throw new \InvalidArgumentException('searching by owner is only allowed in the users home folder');
234
+        }
235
+
236
+        /** @var QuerySearchHelper $searchHelper */
237
+        $searchHelper = \OC::$server->get(QuerySearchHelper::class);
238
+        [$caches, $mountByMountPoint] = $searchHelper->getCachesAndMountPointsForSearch($this->root, $this->path, $limitToHome);
239
+        $resultsPerCache = $searchHelper->searchInCaches($query, $caches);
240
+
241
+        // loop through all results per-cache, constructing the FileInfo object from the CacheEntry and merge them all
242
+        $files = array_merge(...array_map(function (array $results, string $relativeMountPoint) use ($mountByMountPoint) {
243
+            $mount = $mountByMountPoint[$relativeMountPoint];
244
+            return array_map(function (ICacheEntry $result) use ($relativeMountPoint, $mount) {
245
+                return $this->cacheEntryToFileInfo($mount, $relativeMountPoint, $result);
246
+            }, $results);
247
+        }, array_values($resultsPerCache), array_keys($resultsPerCache)));
248
+
249
+        // don't include this folder in the results
250
+        $files = array_filter($files, function (FileInfo $file) {
251
+            return $file->getPath() !== $this->getPath();
252
+        });
253
+
254
+        // since results were returned per-cache, they are no longer fully sorted
255
+        $order = $query->getOrder();
256
+        if ($order) {
257
+            usort($files, function (FileInfo $a, FileInfo $b) use ($order) {
258
+                foreach ($order as $orderField) {
259
+                    $cmp = $orderField->sortFileInfo($a, $b);
260
+                    if ($cmp !== 0) {
261
+                        return $cmp;
262
+                    }
263
+                }
264
+                return 0;
265
+            });
266
+        }
267
+
268
+        return array_map(function (FileInfo $file) {
269
+            return $this->createNode($file->getPath(), $file);
270
+        }, $files);
271
+    }
272
+
273
+    private function cacheEntryToFileInfo(IMountPoint $mount, string $appendRoot, ICacheEntry $cacheEntry): FileInfo {
274
+        $cacheEntry['internalPath'] = $cacheEntry['path'];
275
+        $cacheEntry['path'] = rtrim($appendRoot . $cacheEntry->getPath(), '/');
276
+        $subPath = $cacheEntry['path'] !== '' ? '/' . $cacheEntry['path'] : '';
277
+        return new \OC\Files\FileInfo($this->path . $subPath, $mount->getStorage(), $cacheEntry['internalPath'], $cacheEntry, $mount);
278
+    }
279
+
280
+    /**
281
+     * search for files by mimetype
282
+     *
283
+     * @param string $mimetype
284
+     * @return Node[]
285
+     */
286
+    public function searchByMime($mimetype) {
287
+        if (!str_contains($mimetype, '/')) {
288
+            $query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%'));
289
+        } else {
290
+            $query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype));
291
+        }
292
+        return $this->search($query);
293
+    }
294
+
295
+    /**
296
+     * search for files by tag
297
+     *
298
+     * @param string|int $tag name or tag id
299
+     * @param string $userId owner of the tags
300
+     * @return Node[]
301
+     */
302
+    public function searchByTag($tag, $userId) {
303
+        $query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'tagname', $tag), $userId);
304
+        return $this->search($query);
305
+    }
306
+
307
+    /**
308
+     * @param int $id
309
+     * @return \OC\Files\Node\Node[]
310
+     */
311
+    public function getById($id) {
312
+        return $this->root->getByIdInPath((int)$id, $this->getPath());
313
+    }
314
+
315
+    protected function getAppDataDirectoryName(): string {
316
+        $instanceId = \OC::$server->getConfig()->getSystemValueString('instanceid');
317
+        return 'appdata_' . $instanceId;
318
+    }
319
+
320
+    /**
321
+     * In case the path we are currently in is inside the appdata_* folder,
322
+     * the original getById method does not work, because it can only look inside
323
+     * the user's mount points. But the user has no mount point for the root storage.
324
+     *
325
+     * So in that case we directly check the mount of the root if it contains
326
+     * the id. If it does we check if the path is inside the path we are working
327
+     * in.
328
+     *
329
+     * @param int $id
330
+     * @return array
331
+     */
332
+    protected function getByIdInRootMount(int $id): array {
333
+        $mount = $this->root->getMount('');
334
+        $cacheEntry = $mount->getStorage()->getCache($this->path)->get($id);
335
+        if (!$cacheEntry) {
336
+            return [];
337
+        }
338
+
339
+        $absolutePath = '/' . ltrim($cacheEntry->getPath(), '/');
340
+        $currentPath = rtrim($this->path, '/') . '/';
341
+
342
+        if (!str_starts_with($absolutePath, $currentPath)) {
343
+            return [];
344
+        }
345
+
346
+        return [$this->root->createNode(
347
+            $absolutePath, new \OC\Files\FileInfo(
348
+                $absolutePath,
349
+                $mount->getStorage(),
350
+                $cacheEntry->getPath(),
351
+                $cacheEntry,
352
+                $mount
353
+            ))];
354
+    }
355
+
356
+    public function getFreeSpace() {
357
+        return $this->view->free_space($this->path);
358
+    }
359
+
360
+    public function delete() {
361
+        if ($this->checkPermissions(\OCP\Constants::PERMISSION_DELETE)) {
362
+            $this->sendHooks(['preDelete']);
363
+            $fileInfo = $this->getFileInfo();
364
+            $this->view->rmdir($this->path);
365
+            $nonExisting = new NonExistingFolder($this->root, $this->view, $this->path, $fileInfo);
366
+            $this->sendHooks(['postDelete'], [$nonExisting]);
367
+        } else {
368
+            throw new NotPermittedException('No delete permission for path "' . $this->path . '"');
369
+        }
370
+    }
371
+
372
+    /**
373
+     * Add a suffix to the name in case the file exists
374
+     *
375
+     * @param string $name
376
+     * @return string
377
+     * @throws NotPermittedException
378
+     */
379
+    public function getNonExistingName($name) {
380
+        $uniqueName = \OC_Helper::buildNotExistingFileNameForView($this->getPath(), $name, $this->view);
381
+        return trim($this->getRelativePath($uniqueName), '/');
382
+    }
383
+
384
+    /**
385
+     * @param int $limit
386
+     * @param int $offset
387
+     * @return \OCP\Files\Node[]
388
+     */
389
+    public function getRecent($limit, $offset = 0) {
390
+        $filterOutNonEmptyFolder = new SearchBinaryOperator(
391
+            // filter out non empty folders
392
+            ISearchBinaryOperator::OPERATOR_OR,
393
+            [
394
+                new SearchBinaryOperator(
395
+                    ISearchBinaryOperator::OPERATOR_NOT,
396
+                    [
397
+                        new SearchComparison(
398
+                            ISearchComparison::COMPARE_EQUAL,
399
+                            'mimetype',
400
+                            FileInfo::MIMETYPE_FOLDER
401
+                        ),
402
+                    ]
403
+                ),
404
+                new SearchComparison(
405
+                    ISearchComparison::COMPARE_EQUAL,
406
+                    'size',
407
+                    0
408
+                ),
409
+            ]
410
+        );
411
+
412
+        $filterNonRecentFiles = new SearchComparison(
413
+            ISearchComparison::COMPARE_GREATER_THAN,
414
+            'mtime',
415
+            strtotime("-2 week")
416
+        );
417
+        if ($offset === 0 && $limit <= 100) {
418
+            $query = new SearchQuery(
419
+                new SearchBinaryOperator(
420
+                    ISearchBinaryOperator::OPERATOR_AND,
421
+                    [
422
+                        $filterOutNonEmptyFolder,
423
+                        $filterNonRecentFiles,
424
+                    ],
425
+                ),
426
+                $limit,
427
+                $offset,
428
+                [
429
+                    new SearchOrder(
430
+                        ISearchOrder::DIRECTION_DESCENDING,
431
+                        'mtime'
432
+                    ),
433
+                ]
434
+            );
435
+        } else {
436
+            $query = new SearchQuery(
437
+                $filterOutNonEmptyFolder,
438
+                $limit,
439
+                $offset,
440
+                [
441
+                    new SearchOrder(
442
+                        ISearchOrder::DIRECTION_DESCENDING,
443
+                        'mtime'
444
+                    ),
445
+                ]
446
+            );
447
+        }
448
+
449
+        return $this->search($query);
450
+    }
451 451
 }
Please login to merge, or discard this patch.
Spacing   +23 added lines, -23 removed lines patch added patch discarded remove patch
@@ -68,9 +68,9 @@  discard block
 block discarded – undo
68 68
 	public function getFullPath($path) {
69 69
 		$path = $this->normalizePath($path);
70 70
 		if (!$this->isValidPath($path)) {
71
-			throw new NotPermittedException('Invalid path "' . $path . '"');
71
+			throw new NotPermittedException('Invalid path "'.$path.'"');
72 72
 		}
73
-		return $this->path . $path;
73
+		return $this->path.$path;
74 74
 	}
75 75
 
76 76
 	/**
@@ -88,7 +88,7 @@  discard block
 block discarded – undo
88 88
 	 * @return bool
89 89
 	 */
90 90
 	public function isSubNode($node) {
91
-		return str_starts_with($node->getPath(), $this->path . '/');
91
+		return str_starts_with($node->getPath(), $this->path.'/');
92 92
 	}
93 93
 
94 94
 	/**
@@ -100,7 +100,7 @@  discard block
 block discarded – undo
100 100
 	public function getDirectoryListing() {
101 101
 		$folderContent = $this->view->getDirectoryContent($this->path, '', $this->getFileInfo(false));
102 102
 
103
-		return array_map(function (FileInfo $info) {
103
+		return array_map(function(FileInfo $info) {
104 104
 			if ($info->getMimetype() === FileInfo::MIMETYPE_FOLDER) {
105 105
 				return new Folder($this->root, $this->view, $info->getPath(), $info, $this);
106 106
 			} else {
@@ -163,14 +163,14 @@  discard block
 block discarded – undo
163 163
 			$nonExisting = new NonExistingFolder($this->root, $this->view, $fullPath);
164 164
 			$this->sendHooks(['preWrite', 'preCreate'], [$nonExisting]);
165 165
 			if (!$this->view->mkdir($fullPath)) {
166
-				throw new NotPermittedException('Could not create folder "' . $fullPath . '"');
166
+				throw new NotPermittedException('Could not create folder "'.$fullPath.'"');
167 167
 			}
168 168
 			$parent = dirname($fullPath) === $this->getPath() ? $this : null;
169 169
 			$node = new Folder($this->root, $this->view, $fullPath, null, $parent);
170 170
 			$this->sendHooks(['postWrite', 'postCreate'], [$node]);
171 171
 			return $node;
172 172
 		} else {
173
-			throw new NotPermittedException('No create permission for folder "' . $path . '"');
173
+			throw new NotPermittedException('No create permission for folder "'.$path.'"');
174 174
 		}
175 175
 	}
176 176
 
@@ -194,13 +194,13 @@  discard block
 block discarded – undo
194 194
 				$result = $this->view->touch($fullPath);
195 195
 			}
196 196
 			if ($result === false) {
197
-				throw new NotPermittedException('Could not create path "' . $fullPath . '"');
197
+				throw new NotPermittedException('Could not create path "'.$fullPath.'"');
198 198
 			}
199 199
 			$node = new File($this->root, $this->view, $fullPath, null, $this);
200 200
 			$this->sendHooks(['postWrite', 'postCreate'], [$node]);
201 201
 			return $node;
202 202
 		}
203
-		throw new NotPermittedException('No create permission for path "' . $path . '"');
203
+		throw new NotPermittedException('No create permission for path "'.$path.'"');
204 204
 	}
205 205
 
206 206
 	private function queryFromOperator(ISearchOperator $operator, string $uid = null, int $limit = 0, int $offset = 0): ISearchQuery {
@@ -222,7 +222,7 @@  discard block
 block discarded – undo
222 222
 	 */
223 223
 	public function search($query) {
224 224
 		if (is_string($query)) {
225
-			$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $query . '%'));
225
+			$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%'.$query.'%'));
226 226
 		}
227 227
 
228 228
 		// search is handled by a single query covering all caches that this folder contains
@@ -239,22 +239,22 @@  discard block
 block discarded – undo
239 239
 		$resultsPerCache = $searchHelper->searchInCaches($query, $caches);
240 240
 
241 241
 		// loop through all results per-cache, constructing the FileInfo object from the CacheEntry and merge them all
242
-		$files = array_merge(...array_map(function (array $results, string $relativeMountPoint) use ($mountByMountPoint) {
242
+		$files = array_merge(...array_map(function(array $results, string $relativeMountPoint) use ($mountByMountPoint) {
243 243
 			$mount = $mountByMountPoint[$relativeMountPoint];
244
-			return array_map(function (ICacheEntry $result) use ($relativeMountPoint, $mount) {
244
+			return array_map(function(ICacheEntry $result) use ($relativeMountPoint, $mount) {
245 245
 				return $this->cacheEntryToFileInfo($mount, $relativeMountPoint, $result);
246 246
 			}, $results);
247 247
 		}, array_values($resultsPerCache), array_keys($resultsPerCache)));
248 248
 
249 249
 		// don't include this folder in the results
250
-		$files = array_filter($files, function (FileInfo $file) {
250
+		$files = array_filter($files, function(FileInfo $file) {
251 251
 			return $file->getPath() !== $this->getPath();
252 252
 		});
253 253
 
254 254
 		// since results were returned per-cache, they are no longer fully sorted
255 255
 		$order = $query->getOrder();
256 256
 		if ($order) {
257
-			usort($files, function (FileInfo $a, FileInfo $b) use ($order) {
257
+			usort($files, function(FileInfo $a, FileInfo $b) use ($order) {
258 258
 				foreach ($order as $orderField) {
259 259
 					$cmp = $orderField->sortFileInfo($a, $b);
260 260
 					if ($cmp !== 0) {
@@ -265,16 +265,16 @@  discard block
 block discarded – undo
265 265
 			});
266 266
 		}
267 267
 
268
-		return array_map(function (FileInfo $file) {
268
+		return array_map(function(FileInfo $file) {
269 269
 			return $this->createNode($file->getPath(), $file);
270 270
 		}, $files);
271 271
 	}
272 272
 
273 273
 	private function cacheEntryToFileInfo(IMountPoint $mount, string $appendRoot, ICacheEntry $cacheEntry): FileInfo {
274 274
 		$cacheEntry['internalPath'] = $cacheEntry['path'];
275
-		$cacheEntry['path'] = rtrim($appendRoot . $cacheEntry->getPath(), '/');
276
-		$subPath = $cacheEntry['path'] !== '' ? '/' . $cacheEntry['path'] : '';
277
-		return new \OC\Files\FileInfo($this->path . $subPath, $mount->getStorage(), $cacheEntry['internalPath'], $cacheEntry, $mount);
275
+		$cacheEntry['path'] = rtrim($appendRoot.$cacheEntry->getPath(), '/');
276
+		$subPath = $cacheEntry['path'] !== '' ? '/'.$cacheEntry['path'] : '';
277
+		return new \OC\Files\FileInfo($this->path.$subPath, $mount->getStorage(), $cacheEntry['internalPath'], $cacheEntry, $mount);
278 278
 	}
279 279
 
280 280
 	/**
@@ -285,7 +285,7 @@  discard block
 block discarded – undo
285 285
 	 */
286 286
 	public function searchByMime($mimetype) {
287 287
 		if (!str_contains($mimetype, '/')) {
288
-			$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%'));
288
+			$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype.'/%'));
289 289
 		} else {
290 290
 			$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype));
291 291
 		}
@@ -309,12 +309,12 @@  discard block
 block discarded – undo
309 309
 	 * @return \OC\Files\Node\Node[]
310 310
 	 */
311 311
 	public function getById($id) {
312
-		return $this->root->getByIdInPath((int)$id, $this->getPath());
312
+		return $this->root->getByIdInPath((int) $id, $this->getPath());
313 313
 	}
314 314
 
315 315
 	protected function getAppDataDirectoryName(): string {
316 316
 		$instanceId = \OC::$server->getConfig()->getSystemValueString('instanceid');
317
-		return 'appdata_' . $instanceId;
317
+		return 'appdata_'.$instanceId;
318 318
 	}
319 319
 
320 320
 	/**
@@ -336,8 +336,8 @@  discard block
 block discarded – undo
336 336
 			return [];
337 337
 		}
338 338
 
339
-		$absolutePath = '/' . ltrim($cacheEntry->getPath(), '/');
340
-		$currentPath = rtrim($this->path, '/') . '/';
339
+		$absolutePath = '/'.ltrim($cacheEntry->getPath(), '/');
340
+		$currentPath = rtrim($this->path, '/').'/';
341 341
 
342 342
 		if (!str_starts_with($absolutePath, $currentPath)) {
343 343
 			return [];
@@ -365,7 +365,7 @@  discard block
 block discarded – undo
365 365
 			$nonExisting = new NonExistingFolder($this->root, $this->view, $this->path, $fileInfo);
366 366
 			$this->sendHooks(['postDelete'], [$nonExisting]);
367 367
 		} else {
368
-			throw new NotPermittedException('No delete permission for path "' . $this->path . '"');
368
+			throw new NotPermittedException('No delete permission for path "'.$this->path.'"');
369 369
 		}
370 370
 	}
371 371
 
Please login to merge, or discard this patch.
lib/private/Files/Type/Detection.php 1 patch
Indentation   +334 added lines, -334 removed lines patch added patch discarded remove patch
@@ -53,338 +53,338 @@
 block discarded – undo
53 53
  * @package OC\Files\Type
54 54
  */
55 55
 class Detection implements IMimeTypeDetector {
56
-	private const CUSTOM_MIMETYPEMAPPING = 'mimetypemapping.json';
57
-	private const CUSTOM_MIMETYPEALIASES = 'mimetypealiases.json';
58
-
59
-	protected $mimetypes = [];
60
-	protected $secureMimeTypes = [];
61
-
62
-	protected $mimetypeIcons = [];
63
-	/** @var string[] */
64
-	protected $mimeTypeAlias = [];
65
-
66
-	/** @var IURLGenerator */
67
-	private $urlGenerator;
68
-
69
-	private LoggerInterface $logger;
70
-
71
-	/** @var string */
72
-	private $customConfigDir;
73
-
74
-	/** @var string */
75
-	private $defaultConfigDir;
76
-
77
-	public function __construct(IURLGenerator $urlGenerator,
78
-								LoggerInterface $logger,
79
-								string $customConfigDir,
80
-								string $defaultConfigDir) {
81
-		$this->urlGenerator = $urlGenerator;
82
-		$this->logger = $logger;
83
-		$this->customConfigDir = $customConfigDir;
84
-		$this->defaultConfigDir = $defaultConfigDir;
85
-	}
86
-
87
-	/**
88
-	 * Add an extension -> mimetype mapping
89
-	 *
90
-	 * $mimetype is the assumed correct mime type
91
-	 * The optional $secureMimeType is an alternative to send to send
92
-	 * to avoid potential XSS.
93
-	 *
94
-	 * @param string $extension
95
-	 * @param string $mimetype
96
-	 * @param string|null $secureMimeType
97
-	 */
98
-	public function registerType(string $extension,
99
-								 string $mimetype,
100
-								 ?string $secureMimeType = null): void {
101
-		$this->mimetypes[$extension] = [$mimetype, $secureMimeType];
102
-		$this->secureMimeTypes[$mimetype] = $secureMimeType ?: $mimetype;
103
-	}
104
-
105
-	/**
106
-	 * Add an array of extension -> mimetype mappings
107
-	 *
108
-	 * The mimetype value is in itself an array where the first index is
109
-	 * the assumed correct mimetype and the second is either a secure alternative
110
-	 * or null if the correct is considered secure.
111
-	 *
112
-	 * @param array $types
113
-	 */
114
-	public function registerTypeArray(array $types): void {
115
-		$this->mimetypes = array_merge($this->mimetypes, $types);
116
-
117
-		// Update the alternative mimetypes to avoid having to look them up each time.
118
-		foreach ($this->mimetypes as $extension => $mimeType) {
119
-			if (str_starts_with($extension, '_comment')) {
120
-				continue;
121
-			}
122
-			$this->secureMimeTypes[$mimeType[0]] = $mimeType[1] ?? $mimeType[0];
123
-			if (isset($mimeType[1])) {
124
-				$this->secureMimeTypes[$mimeType[1]] = $mimeType[1];
125
-			}
126
-		}
127
-	}
128
-
129
-	private function loadCustomDefinitions(string $fileName, array $definitions): array {
130
-		if (file_exists($this->customConfigDir . '/' . $fileName)) {
131
-			$custom = json_decode(file_get_contents($this->customConfigDir . '/' . $fileName), true);
132
-			if (json_last_error() === JSON_ERROR_NONE) {
133
-				$definitions = array_merge($definitions, $custom);
134
-			} else {
135
-				$this->logger->warning('Failed to parse ' . $fileName . ': ' . json_last_error_msg());
136
-			}
137
-		}
138
-		return $definitions;
139
-	}
140
-
141
-	/**
142
-	 * Add the mimetype aliases if they are not yet present
143
-	 */
144
-	private function loadAliases(): void {
145
-		if (!empty($this->mimeTypeAlias)) {
146
-			return;
147
-		}
148
-
149
-		$this->mimeTypeAlias = json_decode(file_get_contents($this->defaultConfigDir . '/mimetypealiases.dist.json'), true);
150
-		$this->mimeTypeAlias = $this->loadCustomDefinitions(self::CUSTOM_MIMETYPEALIASES, $this->mimeTypeAlias);
151
-	}
152
-
153
-	/**
154
-	 * @return string[]
155
-	 */
156
-	public function getAllAliases(): array {
157
-		$this->loadAliases();
158
-		return $this->mimeTypeAlias;
159
-	}
160
-
161
-	public function getOnlyDefaultAliases(): array {
162
-		$this->loadMappings();
163
-		$this->mimeTypeAlias = json_decode(file_get_contents($this->defaultConfigDir . '/mimetypealiases.dist.json'), true);
164
-		return $this->mimeTypeAlias;
165
-	}
166
-
167
-	/**
168
-	 * Add mimetype mappings if they are not yet present
169
-	 */
170
-	private function loadMappings(): void {
171
-		if (!empty($this->mimetypes)) {
172
-			return;
173
-		}
174
-
175
-		$mimetypeMapping = json_decode(file_get_contents($this->defaultConfigDir . '/mimetypemapping.dist.json'), true);
176
-		$mimetypeMapping = $this->loadCustomDefinitions(self::CUSTOM_MIMETYPEMAPPING, $mimetypeMapping);
177
-
178
-		$this->registerTypeArray($mimetypeMapping);
179
-	}
180
-
181
-	/**
182
-	 * @return array
183
-	 */
184
-	public function getAllMappings(): array {
185
-		$this->loadMappings();
186
-		return $this->mimetypes;
187
-	}
188
-
189
-	/**
190
-	 * detect mimetype only based on filename, content of file is not used
191
-	 *
192
-	 * @param string $path
193
-	 * @return string
194
-	 */
195
-	public function detectPath($path): string {
196
-		$this->loadMappings();
197
-
198
-		$fileName = basename($path);
199
-
200
-		// remove leading dot on hidden files with a file extension
201
-		$fileName = ltrim($fileName, '.');
202
-
203
-		// note: leading dot doesn't qualify as extension
204
-		if (strpos($fileName, '.') > 0) {
205
-			// remove versioning extension: name.v1508946057 and transfer extension: name.ocTransferId2057600214.part
206
-			$fileName = preg_replace('!((\.v\d+)|((\.ocTransferId\d+)?\.part))$!', '', $fileName);
207
-
208
-			//try to guess the type by the file extension
209
-			$extension = strrchr($fileName, '.');
210
-			if ($extension !== false) {
211
-				$extension = strtolower($extension);
212
-				$extension = substr($extension, 1); //remove leading .
213
-				return $this->mimetypes[$extension][0] ?? 'application/octet-stream';
214
-			}
215
-		}
216
-
217
-		return 'application/octet-stream';
218
-	}
219
-
220
-	/**
221
-	 * detect mimetype only based on the content of file
222
-	 * @param string $path
223
-	 * @return string
224
-	 * @since 18.0.0
225
-	 */
226
-	public function detectContent(string $path): string {
227
-		$this->loadMappings();
228
-
229
-		if (@is_dir($path)) {
230
-			// directories are easy
231
-			return 'httpd/unix-directory';
232
-		}
233
-
234
-		if (function_exists('finfo_open')
235
-			&& function_exists('finfo_file')
236
-			&& $finfo = finfo_open(FILEINFO_MIME)) {
237
-			$info = @finfo_file($finfo, $path);
238
-			finfo_close($finfo);
239
-			if ($info) {
240
-				$info = strtolower($info);
241
-				$mimeType = str_contains($info, ';') ? substr($info, 0, strpos($info, ';')) : $info;
242
-				$mimeType = $this->getSecureMimeType($mimeType);
243
-				if ($mimeType !== 'application/octet-stream') {
244
-					return $mimeType;
245
-				}
246
-			}
247
-		}
248
-
249
-		if (str_starts_with($path, 'file://')) {
250
-			// Is the file wrapped in a stream?
251
-			return 'application/octet-stream';
252
-		}
253
-
254
-		if (function_exists('mime_content_type')) {
255
-			// use mime magic extension if available
256
-			$mimeType = mime_content_type($path);
257
-			if ($mimeType !== false) {
258
-				$mimeType = $this->getSecureMimeType($mimeType);
259
-				if ($mimeType !== 'application/octet-stream') {
260
-					return $mimeType;
261
-				}
262
-			}
263
-		}
264
-
265
-		if (\OC_Helper::canExecute('file')) {
266
-			// it looks like we have a 'file' command,
267
-			// lets see if it does have mime support
268
-			$path = escapeshellarg($path);
269
-			$fp = popen("test -f $path && file -b --mime-type $path", 'r');
270
-			$mimeType = fgets($fp);
271
-			pclose($fp);
272
-
273
-			if ($mimeType !== false) {
274
-				//trim the newline
275
-				$mimeType = trim($mimeType);
276
-				$mimeType = $this->getSecureMimeType($mimeType);
277
-				if ($mimeType !== 'application/octet-stream') {
278
-					return $mimeType;
279
-				}
280
-			}
281
-		}
282
-		return 'application/octet-stream';
283
-	}
284
-
285
-	/**
286
-	 * detect mimetype based on both filename and content
287
-	 *
288
-	 * @param string $path
289
-	 * @return string
290
-	 */
291
-	public function detect($path): string {
292
-		$mimeType = $this->detectPath($path);
293
-
294
-		if ($mimeType !== 'application/octet-stream') {
295
-			return $mimeType;
296
-		}
297
-
298
-		return $this->detectContent($path);
299
-	}
300
-
301
-	/**
302
-	 * detect mimetype based on the content of a string
303
-	 *
304
-	 * @param string $data
305
-	 * @return string
306
-	 */
307
-	public function detectString($data): string {
308
-		if (function_exists('finfo_open') && function_exists('finfo_file')) {
309
-			$finfo = finfo_open(FILEINFO_MIME);
310
-			$info = finfo_buffer($finfo, $data);
311
-			return str_contains($info, ';') ? substr($info, 0, strpos($info, ';')) : $info;
312
-		}
313
-
314
-		$tmpFile = \OC::$server->getTempManager()->getTemporaryFile();
315
-		$fh = fopen($tmpFile, 'wb');
316
-		fwrite($fh, $data, 8024);
317
-		fclose($fh);
318
-		$mime = $this->detect($tmpFile);
319
-		unset($tmpFile);
320
-		return $mime;
321
-	}
322
-
323
-	/**
324
-	 * Get a secure mimetype that won't expose potential XSS.
325
-	 *
326
-	 * @param string $mimeType
327
-	 * @return string
328
-	 */
329
-	public function getSecureMimeType($mimeType): string {
330
-		$this->loadMappings();
331
-
332
-		return $this->secureMimeTypes[$mimeType] ?? 'application/octet-stream';
333
-	}
334
-
335
-	/**
336
-	 * Get path to the icon of a file type
337
-	 * @param string $mimetype the MIME type
338
-	 * @return string the url
339
-	 */
340
-	public function mimeTypeIcon($mimetype): string {
341
-		$this->loadAliases();
342
-
343
-		while (isset($this->mimeTypeAlias[$mimetype])) {
344
-			$mimetype = $this->mimeTypeAlias[$mimetype];
345
-		}
346
-		if (isset($this->mimetypeIcons[$mimetype])) {
347
-			return $this->mimetypeIcons[$mimetype];
348
-		}
349
-
350
-		// Replace slash and backslash with a minus
351
-		$icon = str_replace(['/', '\\'], '-', $mimetype);
352
-
353
-		// Is it a dir?
354
-		if ($mimetype === 'dir') {
355
-			$this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/folder.svg');
356
-			return $this->mimetypeIcons[$mimetype];
357
-		}
358
-		if ($mimetype === 'dir-shared') {
359
-			$this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/folder-shared.svg');
360
-			return $this->mimetypeIcons[$mimetype];
361
-		}
362
-		if ($mimetype === 'dir-external') {
363
-			$this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/folder-external.svg');
364
-			return $this->mimetypeIcons[$mimetype];
365
-		}
366
-
367
-		// Icon exists?
368
-		try {
369
-			$this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/' . $icon . '.svg');
370
-			return $this->mimetypeIcons[$mimetype];
371
-		} catch (\RuntimeException $e) {
372
-			// Specified image not found
373
-		}
374
-
375
-		// Try only the first part of the filetype
376
-
377
-		if (strpos($icon, '-')) {
378
-			$mimePart = substr($icon, 0, strpos($icon, '-'));
379
-			try {
380
-				$this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/' . $mimePart . '.svg');
381
-				return $this->mimetypeIcons[$mimetype];
382
-			} catch (\RuntimeException $e) {
383
-				// Image for the first part of the mimetype not found
384
-			}
385
-		}
386
-
387
-		$this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/file.svg');
388
-		return $this->mimetypeIcons[$mimetype];
389
-	}
56
+    private const CUSTOM_MIMETYPEMAPPING = 'mimetypemapping.json';
57
+    private const CUSTOM_MIMETYPEALIASES = 'mimetypealiases.json';
58
+
59
+    protected $mimetypes = [];
60
+    protected $secureMimeTypes = [];
61
+
62
+    protected $mimetypeIcons = [];
63
+    /** @var string[] */
64
+    protected $mimeTypeAlias = [];
65
+
66
+    /** @var IURLGenerator */
67
+    private $urlGenerator;
68
+
69
+    private LoggerInterface $logger;
70
+
71
+    /** @var string */
72
+    private $customConfigDir;
73
+
74
+    /** @var string */
75
+    private $defaultConfigDir;
76
+
77
+    public function __construct(IURLGenerator $urlGenerator,
78
+                                LoggerInterface $logger,
79
+                                string $customConfigDir,
80
+                                string $defaultConfigDir) {
81
+        $this->urlGenerator = $urlGenerator;
82
+        $this->logger = $logger;
83
+        $this->customConfigDir = $customConfigDir;
84
+        $this->defaultConfigDir = $defaultConfigDir;
85
+    }
86
+
87
+    /**
88
+     * Add an extension -> mimetype mapping
89
+     *
90
+     * $mimetype is the assumed correct mime type
91
+     * The optional $secureMimeType is an alternative to send to send
92
+     * to avoid potential XSS.
93
+     *
94
+     * @param string $extension
95
+     * @param string $mimetype
96
+     * @param string|null $secureMimeType
97
+     */
98
+    public function registerType(string $extension,
99
+                                    string $mimetype,
100
+                                 ?string $secureMimeType = null): void {
101
+        $this->mimetypes[$extension] = [$mimetype, $secureMimeType];
102
+        $this->secureMimeTypes[$mimetype] = $secureMimeType ?: $mimetype;
103
+    }
104
+
105
+    /**
106
+     * Add an array of extension -> mimetype mappings
107
+     *
108
+     * The mimetype value is in itself an array where the first index is
109
+     * the assumed correct mimetype and the second is either a secure alternative
110
+     * or null if the correct is considered secure.
111
+     *
112
+     * @param array $types
113
+     */
114
+    public function registerTypeArray(array $types): void {
115
+        $this->mimetypes = array_merge($this->mimetypes, $types);
116
+
117
+        // Update the alternative mimetypes to avoid having to look them up each time.
118
+        foreach ($this->mimetypes as $extension => $mimeType) {
119
+            if (str_starts_with($extension, '_comment')) {
120
+                continue;
121
+            }
122
+            $this->secureMimeTypes[$mimeType[0]] = $mimeType[1] ?? $mimeType[0];
123
+            if (isset($mimeType[1])) {
124
+                $this->secureMimeTypes[$mimeType[1]] = $mimeType[1];
125
+            }
126
+        }
127
+    }
128
+
129
+    private function loadCustomDefinitions(string $fileName, array $definitions): array {
130
+        if (file_exists($this->customConfigDir . '/' . $fileName)) {
131
+            $custom = json_decode(file_get_contents($this->customConfigDir . '/' . $fileName), true);
132
+            if (json_last_error() === JSON_ERROR_NONE) {
133
+                $definitions = array_merge($definitions, $custom);
134
+            } else {
135
+                $this->logger->warning('Failed to parse ' . $fileName . ': ' . json_last_error_msg());
136
+            }
137
+        }
138
+        return $definitions;
139
+    }
140
+
141
+    /**
142
+     * Add the mimetype aliases if they are not yet present
143
+     */
144
+    private function loadAliases(): void {
145
+        if (!empty($this->mimeTypeAlias)) {
146
+            return;
147
+        }
148
+
149
+        $this->mimeTypeAlias = json_decode(file_get_contents($this->defaultConfigDir . '/mimetypealiases.dist.json'), true);
150
+        $this->mimeTypeAlias = $this->loadCustomDefinitions(self::CUSTOM_MIMETYPEALIASES, $this->mimeTypeAlias);
151
+    }
152
+
153
+    /**
154
+     * @return string[]
155
+     */
156
+    public function getAllAliases(): array {
157
+        $this->loadAliases();
158
+        return $this->mimeTypeAlias;
159
+    }
160
+
161
+    public function getOnlyDefaultAliases(): array {
162
+        $this->loadMappings();
163
+        $this->mimeTypeAlias = json_decode(file_get_contents($this->defaultConfigDir . '/mimetypealiases.dist.json'), true);
164
+        return $this->mimeTypeAlias;
165
+    }
166
+
167
+    /**
168
+     * Add mimetype mappings if they are not yet present
169
+     */
170
+    private function loadMappings(): void {
171
+        if (!empty($this->mimetypes)) {
172
+            return;
173
+        }
174
+
175
+        $mimetypeMapping = json_decode(file_get_contents($this->defaultConfigDir . '/mimetypemapping.dist.json'), true);
176
+        $mimetypeMapping = $this->loadCustomDefinitions(self::CUSTOM_MIMETYPEMAPPING, $mimetypeMapping);
177
+
178
+        $this->registerTypeArray($mimetypeMapping);
179
+    }
180
+
181
+    /**
182
+     * @return array
183
+     */
184
+    public function getAllMappings(): array {
185
+        $this->loadMappings();
186
+        return $this->mimetypes;
187
+    }
188
+
189
+    /**
190
+     * detect mimetype only based on filename, content of file is not used
191
+     *
192
+     * @param string $path
193
+     * @return string
194
+     */
195
+    public function detectPath($path): string {
196
+        $this->loadMappings();
197
+
198
+        $fileName = basename($path);
199
+
200
+        // remove leading dot on hidden files with a file extension
201
+        $fileName = ltrim($fileName, '.');
202
+
203
+        // note: leading dot doesn't qualify as extension
204
+        if (strpos($fileName, '.') > 0) {
205
+            // remove versioning extension: name.v1508946057 and transfer extension: name.ocTransferId2057600214.part
206
+            $fileName = preg_replace('!((\.v\d+)|((\.ocTransferId\d+)?\.part))$!', '', $fileName);
207
+
208
+            //try to guess the type by the file extension
209
+            $extension = strrchr($fileName, '.');
210
+            if ($extension !== false) {
211
+                $extension = strtolower($extension);
212
+                $extension = substr($extension, 1); //remove leading .
213
+                return $this->mimetypes[$extension][0] ?? 'application/octet-stream';
214
+            }
215
+        }
216
+
217
+        return 'application/octet-stream';
218
+    }
219
+
220
+    /**
221
+     * detect mimetype only based on the content of file
222
+     * @param string $path
223
+     * @return string
224
+     * @since 18.0.0
225
+     */
226
+    public function detectContent(string $path): string {
227
+        $this->loadMappings();
228
+
229
+        if (@is_dir($path)) {
230
+            // directories are easy
231
+            return 'httpd/unix-directory';
232
+        }
233
+
234
+        if (function_exists('finfo_open')
235
+            && function_exists('finfo_file')
236
+            && $finfo = finfo_open(FILEINFO_MIME)) {
237
+            $info = @finfo_file($finfo, $path);
238
+            finfo_close($finfo);
239
+            if ($info) {
240
+                $info = strtolower($info);
241
+                $mimeType = str_contains($info, ';') ? substr($info, 0, strpos($info, ';')) : $info;
242
+                $mimeType = $this->getSecureMimeType($mimeType);
243
+                if ($mimeType !== 'application/octet-stream') {
244
+                    return $mimeType;
245
+                }
246
+            }
247
+        }
248
+
249
+        if (str_starts_with($path, 'file://')) {
250
+            // Is the file wrapped in a stream?
251
+            return 'application/octet-stream';
252
+        }
253
+
254
+        if (function_exists('mime_content_type')) {
255
+            // use mime magic extension if available
256
+            $mimeType = mime_content_type($path);
257
+            if ($mimeType !== false) {
258
+                $mimeType = $this->getSecureMimeType($mimeType);
259
+                if ($mimeType !== 'application/octet-stream') {
260
+                    return $mimeType;
261
+                }
262
+            }
263
+        }
264
+
265
+        if (\OC_Helper::canExecute('file')) {
266
+            // it looks like we have a 'file' command,
267
+            // lets see if it does have mime support
268
+            $path = escapeshellarg($path);
269
+            $fp = popen("test -f $path && file -b --mime-type $path", 'r');
270
+            $mimeType = fgets($fp);
271
+            pclose($fp);
272
+
273
+            if ($mimeType !== false) {
274
+                //trim the newline
275
+                $mimeType = trim($mimeType);
276
+                $mimeType = $this->getSecureMimeType($mimeType);
277
+                if ($mimeType !== 'application/octet-stream') {
278
+                    return $mimeType;
279
+                }
280
+            }
281
+        }
282
+        return 'application/octet-stream';
283
+    }
284
+
285
+    /**
286
+     * detect mimetype based on both filename and content
287
+     *
288
+     * @param string $path
289
+     * @return string
290
+     */
291
+    public function detect($path): string {
292
+        $mimeType = $this->detectPath($path);
293
+
294
+        if ($mimeType !== 'application/octet-stream') {
295
+            return $mimeType;
296
+        }
297
+
298
+        return $this->detectContent($path);
299
+    }
300
+
301
+    /**
302
+     * detect mimetype based on the content of a string
303
+     *
304
+     * @param string $data
305
+     * @return string
306
+     */
307
+    public function detectString($data): string {
308
+        if (function_exists('finfo_open') && function_exists('finfo_file')) {
309
+            $finfo = finfo_open(FILEINFO_MIME);
310
+            $info = finfo_buffer($finfo, $data);
311
+            return str_contains($info, ';') ? substr($info, 0, strpos($info, ';')) : $info;
312
+        }
313
+
314
+        $tmpFile = \OC::$server->getTempManager()->getTemporaryFile();
315
+        $fh = fopen($tmpFile, 'wb');
316
+        fwrite($fh, $data, 8024);
317
+        fclose($fh);
318
+        $mime = $this->detect($tmpFile);
319
+        unset($tmpFile);
320
+        return $mime;
321
+    }
322
+
323
+    /**
324
+     * Get a secure mimetype that won't expose potential XSS.
325
+     *
326
+     * @param string $mimeType
327
+     * @return string
328
+     */
329
+    public function getSecureMimeType($mimeType): string {
330
+        $this->loadMappings();
331
+
332
+        return $this->secureMimeTypes[$mimeType] ?? 'application/octet-stream';
333
+    }
334
+
335
+    /**
336
+     * Get path to the icon of a file type
337
+     * @param string $mimetype the MIME type
338
+     * @return string the url
339
+     */
340
+    public function mimeTypeIcon($mimetype): string {
341
+        $this->loadAliases();
342
+
343
+        while (isset($this->mimeTypeAlias[$mimetype])) {
344
+            $mimetype = $this->mimeTypeAlias[$mimetype];
345
+        }
346
+        if (isset($this->mimetypeIcons[$mimetype])) {
347
+            return $this->mimetypeIcons[$mimetype];
348
+        }
349
+
350
+        // Replace slash and backslash with a minus
351
+        $icon = str_replace(['/', '\\'], '-', $mimetype);
352
+
353
+        // Is it a dir?
354
+        if ($mimetype === 'dir') {
355
+            $this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/folder.svg');
356
+            return $this->mimetypeIcons[$mimetype];
357
+        }
358
+        if ($mimetype === 'dir-shared') {
359
+            $this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/folder-shared.svg');
360
+            return $this->mimetypeIcons[$mimetype];
361
+        }
362
+        if ($mimetype === 'dir-external') {
363
+            $this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/folder-external.svg');
364
+            return $this->mimetypeIcons[$mimetype];
365
+        }
366
+
367
+        // Icon exists?
368
+        try {
369
+            $this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/' . $icon . '.svg');
370
+            return $this->mimetypeIcons[$mimetype];
371
+        } catch (\RuntimeException $e) {
372
+            // Specified image not found
373
+        }
374
+
375
+        // Try only the first part of the filetype
376
+
377
+        if (strpos($icon, '-')) {
378
+            $mimePart = substr($icon, 0, strpos($icon, '-'));
379
+            try {
380
+                $this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/' . $mimePart . '.svg');
381
+                return $this->mimetypeIcons[$mimetype];
382
+            } catch (\RuntimeException $e) {
383
+                // Image for the first part of the mimetype not found
384
+            }
385
+        }
386
+
387
+        $this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/file.svg');
388
+        return $this->mimetypeIcons[$mimetype];
389
+    }
390 390
 }
Please login to merge, or discard this patch.
lib/private/Files/Mount/MountPoint.php 2 patches
Indentation   +250 added lines, -250 removed lines patch added patch discarded remove patch
@@ -38,279 +38,279 @@
 block discarded – undo
38 38
 use Psr\Log\LoggerInterface;
39 39
 
40 40
 class MountPoint implements IMountPoint {
41
-	/**
42
-	 * @var \OC\Files\Storage\Storage|null $storage
43
-	 */
44
-	protected $storage = null;
45
-	protected $class;
46
-	protected $storageId;
47
-	protected $numericStorageId = null;
48
-	protected $rootId = null;
41
+    /**
42
+     * @var \OC\Files\Storage\Storage|null $storage
43
+     */
44
+    protected $storage = null;
45
+    protected $class;
46
+    protected $storageId;
47
+    protected $numericStorageId = null;
48
+    protected $rootId = null;
49 49
 
50
-	/**
51
-	 * Configuration options for the storage backend
52
-	 *
53
-	 * @var array
54
-	 */
55
-	protected $arguments = [];
56
-	protected $mountPoint;
50
+    /**
51
+     * Configuration options for the storage backend
52
+     *
53
+     * @var array
54
+     */
55
+    protected $arguments = [];
56
+    protected $mountPoint;
57 57
 
58
-	/**
59
-	 * Mount specific options
60
-	 *
61
-	 * @var array
62
-	 */
63
-	protected $mountOptions = [];
58
+    /**
59
+     * Mount specific options
60
+     *
61
+     * @var array
62
+     */
63
+    protected $mountOptions = [];
64 64
 
65
-	/**
66
-	 * @var \OC\Files\Storage\StorageFactory $loader
67
-	 */
68
-	private $loader;
65
+    /**
66
+     * @var \OC\Files\Storage\StorageFactory $loader
67
+     */
68
+    private $loader;
69 69
 
70
-	/**
71
-	 * Specified whether the storage is invalid after failing to
72
-	 * instantiate it.
73
-	 *
74
-	 * @var bool
75
-	 */
76
-	private $invalidStorage = false;
70
+    /**
71
+     * Specified whether the storage is invalid after failing to
72
+     * instantiate it.
73
+     *
74
+     * @var bool
75
+     */
76
+    private $invalidStorage = false;
77 77
 
78
-	/** @var int|null */
79
-	protected $mountId;
78
+    /** @var int|null */
79
+    protected $mountId;
80 80
 
81
-	/** @var string */
82
-	protected $mountProvider;
81
+    /** @var string */
82
+    protected $mountProvider;
83 83
 
84
-	/**
85
-	 * @param string|\OC\Files\Storage\Storage $storage
86
-	 * @param string $mountpoint
87
-	 * @param array $arguments (optional) configuration for the storage backend
88
-	 * @param \OCP\Files\Storage\IStorageFactory $loader
89
-	 * @param array $mountOptions mount specific options
90
-	 * @param int|null $mountId
91
-	 * @param string|null $mountProvider
92
-	 * @throws \Exception
93
-	 */
94
-	public function __construct(
95
-		$storage,
96
-		string $mountpoint,
97
-		array $arguments = null,
98
-		IStorageFactory $loader = null,
99
-		array $mountOptions = null,
100
-		int $mountId = null,
101
-		string $mountProvider = null
102
-	) {
103
-		if (is_null($arguments)) {
104
-			$arguments = [];
105
-		}
106
-		if (is_null($loader)) {
107
-			$this->loader = new StorageFactory();
108
-		} else {
109
-			$this->loader = $loader;
110
-		}
84
+    /**
85
+     * @param string|\OC\Files\Storage\Storage $storage
86
+     * @param string $mountpoint
87
+     * @param array $arguments (optional) configuration for the storage backend
88
+     * @param \OCP\Files\Storage\IStorageFactory $loader
89
+     * @param array $mountOptions mount specific options
90
+     * @param int|null $mountId
91
+     * @param string|null $mountProvider
92
+     * @throws \Exception
93
+     */
94
+    public function __construct(
95
+        $storage,
96
+        string $mountpoint,
97
+        array $arguments = null,
98
+        IStorageFactory $loader = null,
99
+        array $mountOptions = null,
100
+        int $mountId = null,
101
+        string $mountProvider = null
102
+    ) {
103
+        if (is_null($arguments)) {
104
+            $arguments = [];
105
+        }
106
+        if (is_null($loader)) {
107
+            $this->loader = new StorageFactory();
108
+        } else {
109
+            $this->loader = $loader;
110
+        }
111 111
 
112
-		if (!is_null($mountOptions)) {
113
-			$this->mountOptions = $mountOptions;
114
-		}
112
+        if (!is_null($mountOptions)) {
113
+            $this->mountOptions = $mountOptions;
114
+        }
115 115
 
116
-		$mountpoint = $this->formatPath($mountpoint);
117
-		$this->mountPoint = $mountpoint;
118
-		$this->mountId = $mountId;
119
-		if ($storage instanceof Storage) {
120
-			$this->class = get_class($storage);
121
-			$this->storage = $this->loader->wrap($this, $storage);
122
-		} else {
123
-			// Update old classes to new namespace
124
-			if (str_contains($storage, 'OC_Filestorage_')) {
125
-				$storage = '\OC\Files\Storage\\' . substr($storage, 15);
126
-			}
127
-			$this->class = $storage;
128
-			$this->arguments = $arguments;
129
-		}
130
-		if ($mountProvider) {
131
-			if (strlen($mountProvider) > 128) {
132
-				throw new \Exception("Mount provider $mountProvider name exceeds the limit of 128 characters");
133
-			}
134
-		}
135
-		$this->mountProvider = $mountProvider ?? '';
136
-	}
116
+        $mountpoint = $this->formatPath($mountpoint);
117
+        $this->mountPoint = $mountpoint;
118
+        $this->mountId = $mountId;
119
+        if ($storage instanceof Storage) {
120
+            $this->class = get_class($storage);
121
+            $this->storage = $this->loader->wrap($this, $storage);
122
+        } else {
123
+            // Update old classes to new namespace
124
+            if (str_contains($storage, 'OC_Filestorage_')) {
125
+                $storage = '\OC\Files\Storage\\' . substr($storage, 15);
126
+            }
127
+            $this->class = $storage;
128
+            $this->arguments = $arguments;
129
+        }
130
+        if ($mountProvider) {
131
+            if (strlen($mountProvider) > 128) {
132
+                throw new \Exception("Mount provider $mountProvider name exceeds the limit of 128 characters");
133
+            }
134
+        }
135
+        $this->mountProvider = $mountProvider ?? '';
136
+    }
137 137
 
138
-	/**
139
-	 * get complete path to the mount point, relative to data/
140
-	 *
141
-	 * @return string
142
-	 */
143
-	public function getMountPoint() {
144
-		return $this->mountPoint;
145
-	}
138
+    /**
139
+     * get complete path to the mount point, relative to data/
140
+     *
141
+     * @return string
142
+     */
143
+    public function getMountPoint() {
144
+        return $this->mountPoint;
145
+    }
146 146
 
147
-	/**
148
-	 * Sets the mount point path, relative to data/
149
-	 *
150
-	 * @param string $mountPoint new mount point
151
-	 */
152
-	public function setMountPoint($mountPoint) {
153
-		$this->mountPoint = $this->formatPath($mountPoint);
154
-	}
147
+    /**
148
+     * Sets the mount point path, relative to data/
149
+     *
150
+     * @param string $mountPoint new mount point
151
+     */
152
+    public function setMountPoint($mountPoint) {
153
+        $this->mountPoint = $this->formatPath($mountPoint);
154
+    }
155 155
 
156
-	/**
157
-	 * create the storage that is mounted
158
-	 */
159
-	private function createStorage() {
160
-		if ($this->invalidStorage) {
161
-			return;
162
-		}
156
+    /**
157
+     * create the storage that is mounted
158
+     */
159
+    private function createStorage() {
160
+        if ($this->invalidStorage) {
161
+            return;
162
+        }
163 163
 
164
-		if (class_exists($this->class)) {
165
-			try {
166
-				$class = $this->class;
167
-				// prevent recursion by setting the storage before applying wrappers
168
-				$this->storage = new $class($this->arguments);
169
-				$this->storage = $this->loader->wrap($this, $this->storage);
170
-			} catch (\Exception $exception) {
171
-				$this->storage = null;
172
-				$this->invalidStorage = true;
173
-				if ($this->mountPoint === '/') {
174
-					// the root storage could not be initialized, show the user!
175
-					throw new \Exception('The root storage could not be initialized. Please contact your local administrator.', $exception->getCode(), $exception);
176
-				} else {
177
-					\OC::$server->get(LoggerInterface::class)->error($exception->getMessage(), ['exception' => $exception]);
178
-				}
179
-				return;
180
-			}
181
-		} else {
182
-			\OC::$server->get(LoggerInterface::class)->error('Storage backend ' . $this->class . ' not found', ['app' => 'core']);
183
-			$this->invalidStorage = true;
184
-			return;
185
-		}
186
-	}
164
+        if (class_exists($this->class)) {
165
+            try {
166
+                $class = $this->class;
167
+                // prevent recursion by setting the storage before applying wrappers
168
+                $this->storage = new $class($this->arguments);
169
+                $this->storage = $this->loader->wrap($this, $this->storage);
170
+            } catch (\Exception $exception) {
171
+                $this->storage = null;
172
+                $this->invalidStorage = true;
173
+                if ($this->mountPoint === '/') {
174
+                    // the root storage could not be initialized, show the user!
175
+                    throw new \Exception('The root storage could not be initialized. Please contact your local administrator.', $exception->getCode(), $exception);
176
+                } else {
177
+                    \OC::$server->get(LoggerInterface::class)->error($exception->getMessage(), ['exception' => $exception]);
178
+                }
179
+                return;
180
+            }
181
+        } else {
182
+            \OC::$server->get(LoggerInterface::class)->error('Storage backend ' . $this->class . ' not found', ['app' => 'core']);
183
+            $this->invalidStorage = true;
184
+            return;
185
+        }
186
+    }
187 187
 
188
-	/**
189
-	 * @return \OC\Files\Storage\Storage|null
190
-	 */
191
-	public function getStorage() {
192
-		if (is_null($this->storage)) {
193
-			$this->createStorage();
194
-		}
195
-		return $this->storage;
196
-	}
188
+    /**
189
+     * @return \OC\Files\Storage\Storage|null
190
+     */
191
+    public function getStorage() {
192
+        if (is_null($this->storage)) {
193
+            $this->createStorage();
194
+        }
195
+        return $this->storage;
196
+    }
197 197
 
198
-	/**
199
-	 * @return string|null
200
-	 */
201
-	public function getStorageId() {
202
-		if (!$this->storageId) {
203
-			$storage = $this->getStorage();
204
-			if (is_null($storage)) {
205
-				return null;
206
-			}
207
-			$this->storageId = $storage->getId();
208
-			if (strlen($this->storageId) > 64) {
209
-				$this->storageId = md5($this->storageId);
210
-			}
211
-		}
212
-		return $this->storageId;
213
-	}
198
+    /**
199
+     * @return string|null
200
+     */
201
+    public function getStorageId() {
202
+        if (!$this->storageId) {
203
+            $storage = $this->getStorage();
204
+            if (is_null($storage)) {
205
+                return null;
206
+            }
207
+            $this->storageId = $storage->getId();
208
+            if (strlen($this->storageId) > 64) {
209
+                $this->storageId = md5($this->storageId);
210
+            }
211
+        }
212
+        return $this->storageId;
213
+    }
214 214
 
215
-	/**
216
-	 * @return int
217
-	 */
218
-	public function getNumericStorageId() {
219
-		if (is_null($this->numericStorageId)) {
220
-			$storage = $this->getStorage();
221
-			if (is_null($storage)) {
222
-				return -1;
223
-			}
224
-			$this->numericStorageId = $storage->getStorageCache()->getNumericId();
225
-		}
226
-		return $this->numericStorageId;
227
-	}
215
+    /**
216
+     * @return int
217
+     */
218
+    public function getNumericStorageId() {
219
+        if (is_null($this->numericStorageId)) {
220
+            $storage = $this->getStorage();
221
+            if (is_null($storage)) {
222
+                return -1;
223
+            }
224
+            $this->numericStorageId = $storage->getStorageCache()->getNumericId();
225
+        }
226
+        return $this->numericStorageId;
227
+    }
228 228
 
229
-	/**
230
-	 * @param string $path
231
-	 * @return string
232
-	 */
233
-	public function getInternalPath($path) {
234
-		$path = Filesystem::normalizePath($path, true, false, true);
235
-		if ($this->mountPoint === $path or $this->mountPoint . '/' === $path) {
236
-			$internalPath = '';
237
-		} else {
238
-			$internalPath = substr($path, strlen($this->mountPoint));
239
-		}
240
-		// substr returns false instead of an empty string, we always want a string
241
-		return (string)$internalPath;
242
-	}
229
+    /**
230
+     * @param string $path
231
+     * @return string
232
+     */
233
+    public function getInternalPath($path) {
234
+        $path = Filesystem::normalizePath($path, true, false, true);
235
+        if ($this->mountPoint === $path or $this->mountPoint . '/' === $path) {
236
+            $internalPath = '';
237
+        } else {
238
+            $internalPath = substr($path, strlen($this->mountPoint));
239
+        }
240
+        // substr returns false instead of an empty string, we always want a string
241
+        return (string)$internalPath;
242
+    }
243 243
 
244
-	/**
245
-	 * @param string $path
246
-	 * @return string
247
-	 */
248
-	private function formatPath($path) {
249
-		$path = Filesystem::normalizePath($path);
250
-		if (strlen($path) > 1) {
251
-			$path .= '/';
252
-		}
253
-		return $path;
254
-	}
244
+    /**
245
+     * @param string $path
246
+     * @return string
247
+     */
248
+    private function formatPath($path) {
249
+        $path = Filesystem::normalizePath($path);
250
+        if (strlen($path) > 1) {
251
+            $path .= '/';
252
+        }
253
+        return $path;
254
+    }
255 255
 
256
-	/**
257
-	 * @param callable $wrapper
258
-	 */
259
-	public function wrapStorage($wrapper) {
260
-		$storage = $this->getStorage();
261
-		// storage can be null if it couldn't be initialized
262
-		if ($storage != null) {
263
-			$this->storage = $wrapper($this->mountPoint, $storage, $this);
264
-		}
265
-	}
256
+    /**
257
+     * @param callable $wrapper
258
+     */
259
+    public function wrapStorage($wrapper) {
260
+        $storage = $this->getStorage();
261
+        // storage can be null if it couldn't be initialized
262
+        if ($storage != null) {
263
+            $this->storage = $wrapper($this->mountPoint, $storage, $this);
264
+        }
265
+    }
266 266
 
267
-	/**
268
-	 * Get a mount option
269
-	 *
270
-	 * @param string $name Name of the mount option to get
271
-	 * @param mixed $default Default value for the mount option
272
-	 * @return mixed
273
-	 */
274
-	public function getOption($name, $default) {
275
-		return isset($this->mountOptions[$name]) ? $this->mountOptions[$name] : $default;
276
-	}
267
+    /**
268
+     * Get a mount option
269
+     *
270
+     * @param string $name Name of the mount option to get
271
+     * @param mixed $default Default value for the mount option
272
+     * @return mixed
273
+     */
274
+    public function getOption($name, $default) {
275
+        return isset($this->mountOptions[$name]) ? $this->mountOptions[$name] : $default;
276
+    }
277 277
 
278
-	/**
279
-	 * Get all options for the mount
280
-	 *
281
-	 * @return array
282
-	 */
283
-	public function getOptions() {
284
-		return $this->mountOptions;
285
-	}
278
+    /**
279
+     * Get all options for the mount
280
+     *
281
+     * @return array
282
+     */
283
+    public function getOptions() {
284
+        return $this->mountOptions;
285
+    }
286 286
 
287
-	/**
288
-	 * Get the file id of the root of the storage
289
-	 *
290
-	 * @return int
291
-	 */
292
-	public function getStorageRootId() {
293
-		if (is_null($this->rootId) || $this->rootId === -1) {
294
-			$storage = $this->getStorage();
295
-			// if we can't create the storage return -1 as root id, this is then handled the same as if the root isn't scanned yet
296
-			if ($storage === null) {
297
-				$this->rootId = -1;
298
-			} else {
299
-				$this->rootId = (int)$storage->getCache()->getId('');
300
-			}
301
-		}
302
-		return $this->rootId;
303
-	}
287
+    /**
288
+     * Get the file id of the root of the storage
289
+     *
290
+     * @return int
291
+     */
292
+    public function getStorageRootId() {
293
+        if (is_null($this->rootId) || $this->rootId === -1) {
294
+            $storage = $this->getStorage();
295
+            // if we can't create the storage return -1 as root id, this is then handled the same as if the root isn't scanned yet
296
+            if ($storage === null) {
297
+                $this->rootId = -1;
298
+            } else {
299
+                $this->rootId = (int)$storage->getCache()->getId('');
300
+            }
301
+        }
302
+        return $this->rootId;
303
+    }
304 304
 
305
-	public function getMountId() {
306
-		return $this->mountId;
307
-	}
305
+    public function getMountId() {
306
+        return $this->mountId;
307
+    }
308 308
 
309
-	public function getMountType() {
310
-		return '';
311
-	}
309
+    public function getMountType() {
310
+        return '';
311
+    }
312 312
 
313
-	public function getMountProvider(): string {
314
-		return $this->mountProvider;
315
-	}
313
+    public function getMountProvider(): string {
314
+        return $this->mountProvider;
315
+    }
316 316
 }
Please login to merge, or discard this patch.
Spacing   +5 added lines, -5 removed lines patch added patch discarded remove patch
@@ -122,7 +122,7 @@  discard block
 block discarded – undo
122 122
 		} else {
123 123
 			// Update old classes to new namespace
124 124
 			if (str_contains($storage, 'OC_Filestorage_')) {
125
-				$storage = '\OC\Files\Storage\\' . substr($storage, 15);
125
+				$storage = '\OC\Files\Storage\\'.substr($storage, 15);
126 126
 			}
127 127
 			$this->class = $storage;
128 128
 			$this->arguments = $arguments;
@@ -179,7 +179,7 @@  discard block
 block discarded – undo
179 179
 				return;
180 180
 			}
181 181
 		} else {
182
-			\OC::$server->get(LoggerInterface::class)->error('Storage backend ' . $this->class . ' not found', ['app' => 'core']);
182
+			\OC::$server->get(LoggerInterface::class)->error('Storage backend '.$this->class.' not found', ['app' => 'core']);
183 183
 			$this->invalidStorage = true;
184 184
 			return;
185 185
 		}
@@ -232,13 +232,13 @@  discard block
 block discarded – undo
232 232
 	 */
233 233
 	public function getInternalPath($path) {
234 234
 		$path = Filesystem::normalizePath($path, true, false, true);
235
-		if ($this->mountPoint === $path or $this->mountPoint . '/' === $path) {
235
+		if ($this->mountPoint === $path or $this->mountPoint.'/' === $path) {
236 236
 			$internalPath = '';
237 237
 		} else {
238 238
 			$internalPath = substr($path, strlen($this->mountPoint));
239 239
 		}
240 240
 		// substr returns false instead of an empty string, we always want a string
241
-		return (string)$internalPath;
241
+		return (string) $internalPath;
242 242
 	}
243 243
 
244 244
 	/**
@@ -296,7 +296,7 @@  discard block
 block discarded – undo
296 296
 			if ($storage === null) {
297 297
 				$this->rootId = -1;
298 298
 			} else {
299
-				$this->rootId = (int)$storage->getCache()->getId('');
299
+				$this->rootId = (int) $storage->getCache()->getId('');
300 300
 			}
301 301
 		}
302 302
 		return $this->rootId;
Please login to merge, or discard this patch.
lib/private/Files/Mount/RootMountProvider.php 1 patch
Indentation   +67 added lines, -67 removed lines patch added patch discarded remove patch
@@ -33,71 +33,71 @@
 block discarded – undo
33 33
 use Psr\Log\LoggerInterface;
34 34
 
35 35
 class RootMountProvider implements IRootMountProvider {
36
-	private IConfig $config;
37
-	private LoggerInterface $logger;
38
-
39
-	public function __construct(IConfig $config, LoggerInterface $logger) {
40
-		$this->config = $config;
41
-		$this->logger = $logger;
42
-	}
43
-
44
-	public function getRootMounts(IStorageFactory $loader): array {
45
-		$objectStore = $this->config->getSystemValue('objectstore', null);
46
-		$objectStoreMultiBucket = $this->config->getSystemValue('objectstore_multibucket', null);
47
-
48
-		if ($objectStoreMultiBucket) {
49
-			return [$this->getMultiBucketStoreRootMount($loader, $objectStoreMultiBucket)];
50
-		} elseif ($objectStore) {
51
-			return [$this->getObjectStoreRootMount($loader, $objectStore)];
52
-		} else {
53
-			return [$this->getLocalRootMount($loader)];
54
-		}
55
-	}
56
-
57
-	private function validateObjectStoreConfig(array &$config) {
58
-		if (empty($config['class'])) {
59
-			$this->logger->error('No class given for objectstore', ['app' => 'files']);
60
-		}
61
-		if (!isset($config['arguments'])) {
62
-			$config['arguments'] = [];
63
-		}
64
-
65
-		// instantiate object store implementation
66
-		$name = $config['class'];
67
-		if (str_starts_with($name, 'OCA\\') && substr_count($name, '\\') >= 2) {
68
-			$segments = explode('\\', $name);
69
-			OC_App::loadApp(strtolower($segments[1]));
70
-		}
71
-	}
72
-
73
-	private function getLocalRootMount(IStorageFactory $loader): MountPoint {
74
-		$configDataDirectory = $this->config->getSystemValue("datadirectory", OC::$SERVERROOT . "/data");
75
-		return new MountPoint(LocalRootStorage::class, '/', ['datadir' => $configDataDirectory], $loader, null, null, self::class);
76
-	}
77
-
78
-	private function getObjectStoreRootMount(IStorageFactory $loader, array $config): MountPoint {
79
-		$this->validateObjectStoreConfig($config);
80
-
81
-		$config['arguments']['objectstore'] = new $config['class']($config['arguments']);
82
-		// mount with plain / root object store implementation
83
-		$config['class'] = ObjectStoreStorage::class;
84
-
85
-		return new MountPoint($config['class'], '/', $config['arguments'], $loader, null, null, self::class);
86
-	}
87
-
88
-	private function getMultiBucketStoreRootMount(IStorageFactory $loader, array $config): MountPoint {
89
-		$this->validateObjectStoreConfig($config);
90
-
91
-		if (!isset($config['arguments']['bucket'])) {
92
-			$config['arguments']['bucket'] = '';
93
-		}
94
-		// put the root FS always in first bucket for multibucket configuration
95
-		$config['arguments']['bucket'] .= '0';
96
-
97
-		$config['arguments']['objectstore'] = new $config['class']($config['arguments']);
98
-		// mount with plain / root object store implementation
99
-		$config['class'] = ObjectStoreStorage::class;
100
-
101
-		return new MountPoint($config['class'], '/', $config['arguments'], $loader, null, null, self::class);
102
-	}
36
+    private IConfig $config;
37
+    private LoggerInterface $logger;
38
+
39
+    public function __construct(IConfig $config, LoggerInterface $logger) {
40
+        $this->config = $config;
41
+        $this->logger = $logger;
42
+    }
43
+
44
+    public function getRootMounts(IStorageFactory $loader): array {
45
+        $objectStore = $this->config->getSystemValue('objectstore', null);
46
+        $objectStoreMultiBucket = $this->config->getSystemValue('objectstore_multibucket', null);
47
+
48
+        if ($objectStoreMultiBucket) {
49
+            return [$this->getMultiBucketStoreRootMount($loader, $objectStoreMultiBucket)];
50
+        } elseif ($objectStore) {
51
+            return [$this->getObjectStoreRootMount($loader, $objectStore)];
52
+        } else {
53
+            return [$this->getLocalRootMount($loader)];
54
+        }
55
+    }
56
+
57
+    private function validateObjectStoreConfig(array &$config) {
58
+        if (empty($config['class'])) {
59
+            $this->logger->error('No class given for objectstore', ['app' => 'files']);
60
+        }
61
+        if (!isset($config['arguments'])) {
62
+            $config['arguments'] = [];
63
+        }
64
+
65
+        // instantiate object store implementation
66
+        $name = $config['class'];
67
+        if (str_starts_with($name, 'OCA\\') && substr_count($name, '\\') >= 2) {
68
+            $segments = explode('\\', $name);
69
+            OC_App::loadApp(strtolower($segments[1]));
70
+        }
71
+    }
72
+
73
+    private function getLocalRootMount(IStorageFactory $loader): MountPoint {
74
+        $configDataDirectory = $this->config->getSystemValue("datadirectory", OC::$SERVERROOT . "/data");
75
+        return new MountPoint(LocalRootStorage::class, '/', ['datadir' => $configDataDirectory], $loader, null, null, self::class);
76
+    }
77
+
78
+    private function getObjectStoreRootMount(IStorageFactory $loader, array $config): MountPoint {
79
+        $this->validateObjectStoreConfig($config);
80
+
81
+        $config['arguments']['objectstore'] = new $config['class']($config['arguments']);
82
+        // mount with plain / root object store implementation
83
+        $config['class'] = ObjectStoreStorage::class;
84
+
85
+        return new MountPoint($config['class'], '/', $config['arguments'], $loader, null, null, self::class);
86
+    }
87
+
88
+    private function getMultiBucketStoreRootMount(IStorageFactory $loader, array $config): MountPoint {
89
+        $this->validateObjectStoreConfig($config);
90
+
91
+        if (!isset($config['arguments']['bucket'])) {
92
+            $config['arguments']['bucket'] = '';
93
+        }
94
+        // put the root FS always in first bucket for multibucket configuration
95
+        $config['arguments']['bucket'] .= '0';
96
+
97
+        $config['arguments']['objectstore'] = new $config['class']($config['arguments']);
98
+        // mount with plain / root object store implementation
99
+        $config['class'] = ObjectStoreStorage::class;
100
+
101
+        return new MountPoint($config['class'], '/', $config['arguments'], $loader, null, null, self::class);
102
+    }
103 103
 }
Please login to merge, or discard this patch.
lib/private/Files/ObjectStore/S3Signature.php 2 patches
Indentation   +184 added lines, -184 removed lines patch added patch discarded remove patch
@@ -35,188 +35,188 @@
 block discarded – undo
35 35
  * Legacy Amazon S3 signature implementation
36 36
  */
37 37
 class S3Signature implements SignatureInterface {
38
-	/** @var array Query string values that must be signed */
39
-	private $signableQueryString = [
40
-		'acl', 'cors', 'delete', 'lifecycle', 'location', 'logging',
41
-		'notification', 'partNumber', 'policy', 'requestPayment',
42
-		'response-cache-control', 'response-content-disposition',
43
-		'response-content-encoding', 'response-content-language',
44
-		'response-content-type', 'response-expires', 'restore', 'tagging',
45
-		'torrent', 'uploadId', 'uploads', 'versionId', 'versioning',
46
-		'versions', 'website'
47
-	];
48
-
49
-	/** @var array Sorted headers that must be signed */
50
-	private $signableHeaders = ['Content-MD5', 'Content-Type'];
51
-
52
-	/** @var \Aws\S3\S3UriParser S3 URI parser */
53
-	private $parser;
54
-
55
-	public function __construct() {
56
-		$this->parser = new S3UriParser();
57
-		// Ensure that the signable query string parameters are sorted
58
-		sort($this->signableQueryString);
59
-	}
60
-
61
-	public function signRequest(
62
-		RequestInterface $request,
63
-		CredentialsInterface $credentials
64
-	) {
65
-		$request = $this->prepareRequest($request, $credentials);
66
-		$stringToSign = $this->createCanonicalizedString($request);
67
-		$auth = 'AWS '
68
-			. $credentials->getAccessKeyId() . ':'
69
-			. $this->signString($stringToSign, $credentials);
70
-
71
-		return $request->withHeader('Authorization', $auth);
72
-	}
73
-
74
-	public function presign(
75
-		RequestInterface $request,
76
-		CredentialsInterface $credentials,
77
-		$expires,
78
-		array $options = []
79
-	) {
80
-		$query = [];
81
-		// URL encoding already occurs in the URI template expansion. Undo that
82
-		// and encode using the same encoding as GET object, PUT object, etc.
83
-		$uri = $request->getUri();
84
-		$path = S3Client::encodeKey(rawurldecode($uri->getPath()));
85
-		$request = $request->withUri($uri->withPath($path));
86
-
87
-		// Make sure to handle temporary credentials
88
-		if ($token = $credentials->getSecurityToken()) {
89
-			$request = $request->withHeader('X-Amz-Security-Token', $token);
90
-			$query['X-Amz-Security-Token'] = $token;
91
-		}
92
-
93
-		if ($expires instanceof \DateTime) {
94
-			$expires = $expires->getTimestamp();
95
-		} elseif (!is_numeric($expires)) {
96
-			$expires = strtotime($expires);
97
-		}
98
-
99
-		// Set query params required for pre-signed URLs
100
-		$query['AWSAccessKeyId'] = $credentials->getAccessKeyId();
101
-		$query['Expires'] = $expires;
102
-		$query['Signature'] = $this->signString(
103
-			$this->createCanonicalizedString($request, $expires),
104
-			$credentials
105
-		);
106
-
107
-		// Move X-Amz-* headers to the query string
108
-		foreach ($request->getHeaders() as $name => $header) {
109
-			$name = strtolower($name);
110
-			if (str_starts_with($name, 'x-amz-')) {
111
-				$query[$name] = implode(',', $header);
112
-			}
113
-		}
114
-
115
-		$queryString = http_build_query($query, null, '&', PHP_QUERY_RFC3986);
116
-
117
-		return $request->withUri($request->getUri()->withQuery($queryString));
118
-	}
119
-
120
-	/**
121
-	 * @param RequestInterface     $request
122
-	 * @param CredentialsInterface $creds
123
-	 *
124
-	 * @return RequestInterface
125
-	 */
126
-	private function prepareRequest(
127
-		RequestInterface $request,
128
-		CredentialsInterface $creds
129
-	) {
130
-		$modify = [
131
-			'remove_headers' => ['X-Amz-Date'],
132
-			'set_headers' => ['Date' => gmdate(\DateTimeInterface::RFC2822)]
133
-		];
134
-
135
-		// Add the security token header if one is being used by the credentials
136
-		if ($token = $creds->getSecurityToken()) {
137
-			$modify['set_headers']['X-Amz-Security-Token'] = $token;
138
-		}
139
-
140
-		return Psr7\Utils::modifyRequest($request, $modify);
141
-	}
142
-
143
-	private function signString($string, CredentialsInterface $credentials) {
144
-		return base64_encode(
145
-			hash_hmac('sha1', $string, $credentials->getSecretKey(), true)
146
-		);
147
-	}
148
-
149
-	private function createCanonicalizedString(
150
-		RequestInterface $request,
151
-		$expires = null
152
-	) {
153
-		$buffer = $request->getMethod() . "\n";
154
-
155
-		// Add the interesting headers
156
-		foreach ($this->signableHeaders as $header) {
157
-			$buffer .= $request->getHeaderLine($header) . "\n";
158
-		}
159
-
160
-		$date = $expires ?: $request->getHeaderLine('date');
161
-		$buffer .= "{$date}\n"
162
-			. $this->createCanonicalizedAmzHeaders($request)
163
-			. $this->createCanonicalizedResource($request);
164
-
165
-		return $buffer;
166
-	}
167
-
168
-	private function createCanonicalizedAmzHeaders(RequestInterface $request) {
169
-		$headers = [];
170
-		foreach ($request->getHeaders() as $name => $header) {
171
-			$name = strtolower($name);
172
-			if (str_starts_with($name, 'x-amz-')) {
173
-				$value = implode(',', $header);
174
-				if (strlen($value) > 0) {
175
-					$headers[$name] = $name . ':' . $value;
176
-				}
177
-			}
178
-		}
179
-
180
-		if (!$headers) {
181
-			return '';
182
-		}
183
-
184
-		ksort($headers);
185
-
186
-		return implode("\n", $headers) . "\n";
187
-	}
188
-
189
-	private function createCanonicalizedResource(RequestInterface $request) {
190
-		$data = $this->parser->parse($request->getUri());
191
-		$buffer = '/';
192
-
193
-		if ($data['bucket']) {
194
-			$buffer .= $data['bucket'];
195
-			if (!empty($data['key']) || !$data['path_style']) {
196
-				$buffer .= '/' . $data['key'];
197
-			}
198
-		}
199
-
200
-		// Add sub resource parameters if present.
201
-		$query = $request->getUri()->getQuery();
202
-
203
-		if ($query) {
204
-			$params = Psr7\Query::parse($query);
205
-			$first = true;
206
-			foreach ($this->signableQueryString as $key) {
207
-				if (array_key_exists($key, $params)) {
208
-					$value = $params[$key];
209
-					$buffer .= $first ? '?' : '&';
210
-					$first = false;
211
-					$buffer .= $key;
212
-					// Don't add values for empty sub-resources
213
-					if (strlen($value)) {
214
-						$buffer .= "={$value}";
215
-					}
216
-				}
217
-			}
218
-		}
219
-
220
-		return $buffer;
221
-	}
38
+    /** @var array Query string values that must be signed */
39
+    private $signableQueryString = [
40
+        'acl', 'cors', 'delete', 'lifecycle', 'location', 'logging',
41
+        'notification', 'partNumber', 'policy', 'requestPayment',
42
+        'response-cache-control', 'response-content-disposition',
43
+        'response-content-encoding', 'response-content-language',
44
+        'response-content-type', 'response-expires', 'restore', 'tagging',
45
+        'torrent', 'uploadId', 'uploads', 'versionId', 'versioning',
46
+        'versions', 'website'
47
+    ];
48
+
49
+    /** @var array Sorted headers that must be signed */
50
+    private $signableHeaders = ['Content-MD5', 'Content-Type'];
51
+
52
+    /** @var \Aws\S3\S3UriParser S3 URI parser */
53
+    private $parser;
54
+
55
+    public function __construct() {
56
+        $this->parser = new S3UriParser();
57
+        // Ensure that the signable query string parameters are sorted
58
+        sort($this->signableQueryString);
59
+    }
60
+
61
+    public function signRequest(
62
+        RequestInterface $request,
63
+        CredentialsInterface $credentials
64
+    ) {
65
+        $request = $this->prepareRequest($request, $credentials);
66
+        $stringToSign = $this->createCanonicalizedString($request);
67
+        $auth = 'AWS '
68
+            . $credentials->getAccessKeyId() . ':'
69
+            . $this->signString($stringToSign, $credentials);
70
+
71
+        return $request->withHeader('Authorization', $auth);
72
+    }
73
+
74
+    public function presign(
75
+        RequestInterface $request,
76
+        CredentialsInterface $credentials,
77
+        $expires,
78
+        array $options = []
79
+    ) {
80
+        $query = [];
81
+        // URL encoding already occurs in the URI template expansion. Undo that
82
+        // and encode using the same encoding as GET object, PUT object, etc.
83
+        $uri = $request->getUri();
84
+        $path = S3Client::encodeKey(rawurldecode($uri->getPath()));
85
+        $request = $request->withUri($uri->withPath($path));
86
+
87
+        // Make sure to handle temporary credentials
88
+        if ($token = $credentials->getSecurityToken()) {
89
+            $request = $request->withHeader('X-Amz-Security-Token', $token);
90
+            $query['X-Amz-Security-Token'] = $token;
91
+        }
92
+
93
+        if ($expires instanceof \DateTime) {
94
+            $expires = $expires->getTimestamp();
95
+        } elseif (!is_numeric($expires)) {
96
+            $expires = strtotime($expires);
97
+        }
98
+
99
+        // Set query params required for pre-signed URLs
100
+        $query['AWSAccessKeyId'] = $credentials->getAccessKeyId();
101
+        $query['Expires'] = $expires;
102
+        $query['Signature'] = $this->signString(
103
+            $this->createCanonicalizedString($request, $expires),
104
+            $credentials
105
+        );
106
+
107
+        // Move X-Amz-* headers to the query string
108
+        foreach ($request->getHeaders() as $name => $header) {
109
+            $name = strtolower($name);
110
+            if (str_starts_with($name, 'x-amz-')) {
111
+                $query[$name] = implode(',', $header);
112
+            }
113
+        }
114
+
115
+        $queryString = http_build_query($query, null, '&', PHP_QUERY_RFC3986);
116
+
117
+        return $request->withUri($request->getUri()->withQuery($queryString));
118
+    }
119
+
120
+    /**
121
+     * @param RequestInterface     $request
122
+     * @param CredentialsInterface $creds
123
+     *
124
+     * @return RequestInterface
125
+     */
126
+    private function prepareRequest(
127
+        RequestInterface $request,
128
+        CredentialsInterface $creds
129
+    ) {
130
+        $modify = [
131
+            'remove_headers' => ['X-Amz-Date'],
132
+            'set_headers' => ['Date' => gmdate(\DateTimeInterface::RFC2822)]
133
+        ];
134
+
135
+        // Add the security token header if one is being used by the credentials
136
+        if ($token = $creds->getSecurityToken()) {
137
+            $modify['set_headers']['X-Amz-Security-Token'] = $token;
138
+        }
139
+
140
+        return Psr7\Utils::modifyRequest($request, $modify);
141
+    }
142
+
143
+    private function signString($string, CredentialsInterface $credentials) {
144
+        return base64_encode(
145
+            hash_hmac('sha1', $string, $credentials->getSecretKey(), true)
146
+        );
147
+    }
148
+
149
+    private function createCanonicalizedString(
150
+        RequestInterface $request,
151
+        $expires = null
152
+    ) {
153
+        $buffer = $request->getMethod() . "\n";
154
+
155
+        // Add the interesting headers
156
+        foreach ($this->signableHeaders as $header) {
157
+            $buffer .= $request->getHeaderLine($header) . "\n";
158
+        }
159
+
160
+        $date = $expires ?: $request->getHeaderLine('date');
161
+        $buffer .= "{$date}\n"
162
+            . $this->createCanonicalizedAmzHeaders($request)
163
+            . $this->createCanonicalizedResource($request);
164
+
165
+        return $buffer;
166
+    }
167
+
168
+    private function createCanonicalizedAmzHeaders(RequestInterface $request) {
169
+        $headers = [];
170
+        foreach ($request->getHeaders() as $name => $header) {
171
+            $name = strtolower($name);
172
+            if (str_starts_with($name, 'x-amz-')) {
173
+                $value = implode(',', $header);
174
+                if (strlen($value) > 0) {
175
+                    $headers[$name] = $name . ':' . $value;
176
+                }
177
+            }
178
+        }
179
+
180
+        if (!$headers) {
181
+            return '';
182
+        }
183
+
184
+        ksort($headers);
185
+
186
+        return implode("\n", $headers) . "\n";
187
+    }
188
+
189
+    private function createCanonicalizedResource(RequestInterface $request) {
190
+        $data = $this->parser->parse($request->getUri());
191
+        $buffer = '/';
192
+
193
+        if ($data['bucket']) {
194
+            $buffer .= $data['bucket'];
195
+            if (!empty($data['key']) || !$data['path_style']) {
196
+                $buffer .= '/' . $data['key'];
197
+            }
198
+        }
199
+
200
+        // Add sub resource parameters if present.
201
+        $query = $request->getUri()->getQuery();
202
+
203
+        if ($query) {
204
+            $params = Psr7\Query::parse($query);
205
+            $first = true;
206
+            foreach ($this->signableQueryString as $key) {
207
+                if (array_key_exists($key, $params)) {
208
+                    $value = $params[$key];
209
+                    $buffer .= $first ? '?' : '&';
210
+                    $first = false;
211
+                    $buffer .= $key;
212
+                    // Don't add values for empty sub-resources
213
+                    if (strlen($value)) {
214
+                        $buffer .= "={$value}";
215
+                    }
216
+                }
217
+            }
218
+        }
219
+
220
+        return $buffer;
221
+    }
222 222
 }
Please login to merge, or discard this patch.
Spacing   +6 added lines, -6 removed lines patch added patch discarded remove patch
@@ -65,7 +65,7 @@  discard block
 block discarded – undo
65 65
 		$request = $this->prepareRequest($request, $credentials);
66 66
 		$stringToSign = $this->createCanonicalizedString($request);
67 67
 		$auth = 'AWS '
68
-			. $credentials->getAccessKeyId() . ':'
68
+			. $credentials->getAccessKeyId().':'
69 69
 			. $this->signString($stringToSign, $credentials);
70 70
 
71 71
 		return $request->withHeader('Authorization', $auth);
@@ -150,11 +150,11 @@  discard block
 block discarded – undo
150 150
 		RequestInterface $request,
151 151
 		$expires = null
152 152
 	) {
153
-		$buffer = $request->getMethod() . "\n";
153
+		$buffer = $request->getMethod()."\n";
154 154
 
155 155
 		// Add the interesting headers
156 156
 		foreach ($this->signableHeaders as $header) {
157
-			$buffer .= $request->getHeaderLine($header) . "\n";
157
+			$buffer .= $request->getHeaderLine($header)."\n";
158 158
 		}
159 159
 
160 160
 		$date = $expires ?: $request->getHeaderLine('date');
@@ -172,7 +172,7 @@  discard block
 block discarded – undo
172 172
 			if (str_starts_with($name, 'x-amz-')) {
173 173
 				$value = implode(',', $header);
174 174
 				if (strlen($value) > 0) {
175
-					$headers[$name] = $name . ':' . $value;
175
+					$headers[$name] = $name.':'.$value;
176 176
 				}
177 177
 			}
178 178
 		}
@@ -183,7 +183,7 @@  discard block
 block discarded – undo
183 183
 
184 184
 		ksort($headers);
185 185
 
186
-		return implode("\n", $headers) . "\n";
186
+		return implode("\n", $headers)."\n";
187 187
 	}
188 188
 
189 189
 	private function createCanonicalizedResource(RequestInterface $request) {
@@ -193,7 +193,7 @@  discard block
 block discarded – undo
193 193
 		if ($data['bucket']) {
194 194
 			$buffer .= $data['bucket'];
195 195
 			if (!empty($data['key']) || !$data['path_style']) {
196
-				$buffer .= '/' . $data['key'];
196
+				$buffer .= '/'.$data['key'];
197 197
 			}
198 198
 		}
199 199
 
Please login to merge, or discard this patch.