Completed
Push — master ( 594d22...842df4 )
by
unknown
29:50
created
lib/private/legacy/OC_Util.php 1 patch
Indentation   +797 added lines, -797 removed lines patch added patch discarded remove patch
@@ -22,801 +22,801 @@
 block discarded – undo
22 22
  * @deprecated 32.0.0 Use \OCP\Util or any appropriate official API instead
23 23
  */
24 24
 class OC_Util {
25
-	public static $styles = [];
26
-	public static $headers = [];
27
-
28
-	/**
29
-	 * Setup the file system
30
-	 *
31
-	 * @param string|null $user
32
-	 * @return boolean
33
-	 * @description configure the initial filesystem based on the configuration
34
-	 * @suppress PhanDeprecatedFunction
35
-	 * @suppress PhanAccessMethodInternal
36
-	 */
37
-	public static function setupFS(?string $user = '') {
38
-		// If we are not forced to load a specific user we load the one that is logged in
39
-		if ($user === '') {
40
-			$userObject = \OC::$server->get(\OCP\IUserSession::class)->getUser();
41
-		} else {
42
-			$userObject = \OC::$server->get(\OCP\IUserManager::class)->get($user);
43
-		}
44
-
45
-		/** @var SetupManager $setupManager */
46
-		$setupManager = \OC::$server->get(SetupManager::class);
47
-
48
-		if ($userObject) {
49
-			$setupManager->setupForUser($userObject);
50
-		} else {
51
-			$setupManager->setupRoot();
52
-		}
53
-		return true;
54
-	}
55
-
56
-	/**
57
-	 * Check if a password is required for each public link
58
-	 *
59
-	 * @param bool $checkGroupMembership Check group membership exclusion
60
-	 * @return bool
61
-	 * @deprecated 32.0.0 use OCP\Share\IManager's shareApiLinkEnforcePassword directly
62
-	 */
63
-	public static function isPublicLinkPasswordRequired(bool $checkGroupMembership = true) {
64
-		/** @var IManager $shareManager */
65
-		$shareManager = \OC::$server->get(IManager::class);
66
-		return $shareManager->shareApiLinkEnforcePassword($checkGroupMembership);
67
-	}
68
-
69
-	/**
70
-	 * check if sharing is disabled for the current user
71
-	 * @param IConfig $config
72
-	 * @param IGroupManager $groupManager
73
-	 * @param IUser|null $user
74
-	 * @return bool
75
-	 * @deprecated 32.0.0 use OCP\Share\IManager's sharingDisabledForUser directly
76
-	 */
77
-	public static function isSharingDisabledForUser(IConfig $config, IGroupManager $groupManager, $user) {
78
-		/** @var IManager $shareManager */
79
-		$shareManager = \OC::$server->get(IManager::class);
80
-		$userId = $user ? $user->getUID() : null;
81
-		return $shareManager->sharingDisabledForUser($userId);
82
-	}
83
-
84
-	/**
85
-	 * check if share API enforces a default expire date
86
-	 *
87
-	 * @return bool
88
-	 * @deprecated 32.0.0 use OCP\Share\IManager's shareApiLinkDefaultExpireDateEnforced directly
89
-	 */
90
-	public static function isDefaultExpireDateEnforced() {
91
-		/** @var IManager $shareManager */
92
-		$shareManager = \OC::$server->get(IManager::class);
93
-		return $shareManager->shareApiLinkDefaultExpireDateEnforced();
94
-	}
95
-
96
-	/**
97
-	 * Get the quota of a user
98
-	 *
99
-	 * @param IUser|null $user
100
-	 * @return int|\OCP\Files\FileInfo::SPACE_UNLIMITED|false|float Quota bytes
101
-	 * @deprecated 9.0.0 - Use \OCP\IUser::getQuota or \OCP\IUser::getQuotaBytes
102
-	 */
103
-	public static function getUserQuota(?IUser $user) {
104
-		if (is_null($user)) {
105
-			return \OCP\Files\FileInfo::SPACE_UNLIMITED;
106
-		}
107
-		$userQuota = $user->getQuota();
108
-		if ($userQuota === 'none') {
109
-			return \OCP\Files\FileInfo::SPACE_UNLIMITED;
110
-		}
111
-		return \OCP\Util::computerFileSize($userQuota);
112
-	}
113
-
114
-	/**
115
-	 * copies the skeleton to the users /files
116
-	 *
117
-	 * @param string $userId
118
-	 * @param \OCP\Files\Folder $userDirectory
119
-	 * @throws \OCP\Files\NotFoundException
120
-	 * @throws \OCP\Files\NotPermittedException
121
-	 * @suppress PhanDeprecatedFunction
122
-	 */
123
-	public static function copySkeleton($userId, \OCP\Files\Folder $userDirectory) {
124
-		/** @var LoggerInterface $logger */
125
-		$logger = \OC::$server->get(LoggerInterface::class);
126
-
127
-		$plainSkeletonDirectory = \OC::$server->getConfig()->getSystemValueString('skeletondirectory', \OC::$SERVERROOT . '/core/skeleton');
128
-		$userLang = \OC::$server->get(IFactory::class)->findLanguage();
129
-		$skeletonDirectory = str_replace('{lang}', $userLang, $plainSkeletonDirectory);
130
-
131
-		if (!file_exists($skeletonDirectory)) {
132
-			$dialectStart = strpos($userLang, '_');
133
-			if ($dialectStart !== false) {
134
-				$skeletonDirectory = str_replace('{lang}', substr($userLang, 0, $dialectStart), $plainSkeletonDirectory);
135
-			}
136
-			if ($dialectStart === false || !file_exists($skeletonDirectory)) {
137
-				$skeletonDirectory = str_replace('{lang}', 'default', $plainSkeletonDirectory);
138
-			}
139
-			if (!file_exists($skeletonDirectory)) {
140
-				$skeletonDirectory = '';
141
-			}
142
-		}
143
-
144
-		$instanceId = \OC::$server->getConfig()->getSystemValue('instanceid', '');
145
-
146
-		if ($instanceId === null) {
147
-			throw new \RuntimeException('no instance id!');
148
-		}
149
-		$appdata = 'appdata_' . $instanceId;
150
-		if ($userId === $appdata) {
151
-			throw new \RuntimeException('username is reserved name: ' . $appdata);
152
-		}
153
-
154
-		if (!empty($skeletonDirectory)) {
155
-			$logger->debug('copying skeleton for ' . $userId . ' from ' . $skeletonDirectory . ' to ' . $userDirectory->getFullPath('/'), ['app' => 'files_skeleton']);
156
-			self::copyr($skeletonDirectory, $userDirectory);
157
-			// update the file cache
158
-			$userDirectory->getStorage()->getScanner()->scan('', \OC\Files\Cache\Scanner::SCAN_RECURSIVE);
159
-
160
-			/** @var ITemplateManager $templateManager */
161
-			$templateManager = \OC::$server->get(ITemplateManager::class);
162
-			$templateManager->initializeTemplateDirectory(null, $userId);
163
-		}
164
-	}
165
-
166
-	/**
167
-	 * copies a directory recursively by using streams
168
-	 *
169
-	 * @param string $source
170
-	 * @param \OCP\Files\Folder $target
171
-	 * @return void
172
-	 */
173
-	public static function copyr($source, \OCP\Files\Folder $target) {
174
-		$logger = \OCP\Server::get(LoggerInterface::class);
175
-
176
-		// Verify if folder exists
177
-		$dir = opendir($source);
178
-		if ($dir === false) {
179
-			$logger->error(sprintf('Could not opendir "%s"', $source), ['app' => 'core']);
180
-			return;
181
-		}
182
-
183
-		// Copy the files
184
-		while (false !== ($file = readdir($dir))) {
185
-			if (!\OC\Files\Filesystem::isIgnoredDir($file)) {
186
-				if (is_dir($source . '/' . $file)) {
187
-					$child = $target->newFolder($file);
188
-					self::copyr($source . '/' . $file, $child);
189
-				} else {
190
-					$sourceStream = fopen($source . '/' . $file, 'r');
191
-					if ($sourceStream === false) {
192
-						$logger->error(sprintf('Could not fopen "%s"', $source . '/' . $file), ['app' => 'core']);
193
-						closedir($dir);
194
-						return;
195
-					}
196
-					$target->newFile($file, $sourceStream);
197
-				}
198
-			}
199
-		}
200
-		closedir($dir);
201
-	}
202
-
203
-	/**
204
-	 * @deprecated 32.0.0 Call tearDown directly on SetupManager
205
-	 */
206
-	public static function tearDownFS(): void {
207
-		$setupManager = \OCP\Server::get(SetupManager::class);
208
-		$setupManager->tearDown();
209
-	}
210
-
211
-	/**
212
-	 * generates a path for JS/CSS files. If no application is provided it will create the path for core.
213
-	 *
214
-	 * @param string $application application to get the files from
215
-	 * @param string $directory directory within this application (css, js, vendor, etc)
216
-	 * @param ?string $file the file inside of the above folder
217
-	 */
218
-	private static function generatePath($application, $directory, $file): string {
219
-		if (is_null($file)) {
220
-			$file = $application;
221
-			$application = '';
222
-		}
223
-		if (!empty($application)) {
224
-			return "$application/$directory/$file";
225
-		} else {
226
-			return "$directory/$file";
227
-		}
228
-	}
229
-
230
-	/**
231
-	 * add a css file
232
-	 *
233
-	 * @param string $application application id
234
-	 * @param string|null $file filename
235
-	 * @param bool $prepend prepend the Style to the beginning of the list
236
-	 * @deprecated 32.0.0 Use \OCP\Util::addStyle
237
-	 */
238
-	public static function addStyle($application, $file = null, $prepend = false): void {
239
-		$path = OC_Util::generatePath($application, 'css', $file);
240
-		self::addExternalResource($application, $prepend, $path, 'style');
241
-	}
242
-
243
-	/**
244
-	 * add a css file from the vendor sub folder
245
-	 *
246
-	 * @param string $application application id
247
-	 * @param string|null $file filename
248
-	 * @param bool $prepend prepend the Style to the beginning of the list
249
-	 * @deprecated 32.0.0
250
-	 */
251
-	public static function addVendorStyle($application, $file = null, $prepend = false): void {
252
-		$path = OC_Util::generatePath($application, 'vendor', $file);
253
-		self::addExternalResource($application, $prepend, $path, 'style');
254
-	}
255
-
256
-	/**
257
-	 * add an external resource css/js file
258
-	 *
259
-	 * @param string $application application id
260
-	 * @param bool $prepend prepend the file to the beginning of the list
261
-	 * @param string $path
262
-	 * @param string $type (script or style)
263
-	 */
264
-	private static function addExternalResource($application, $prepend, $path, $type = 'script'): void {
265
-		if ($type === 'style') {
266
-			if (!in_array($path, self::$styles)) {
267
-				if ($prepend === true) {
268
-					array_unshift(self::$styles, $path);
269
-				} else {
270
-					self::$styles[] = $path;
271
-				}
272
-			}
273
-		}
274
-	}
275
-
276
-	/**
277
-	 * Add a custom element to the header
278
-	 * If $text is null then the element will be written as empty element.
279
-	 * So use "" to get a closing tag.
280
-	 * @param string $tag tag name of the element
281
-	 * @param array $attributes array of attributes for the element
282
-	 * @param string $text the text content for the element
283
-	 * @param bool $prepend prepend the header to the beginning of the list
284
-	 * @deprecated 32.0.0 Use \OCP\Util::addHeader instead
285
-	 */
286
-	public static function addHeader($tag, $attributes, $text = null, $prepend = false): void {
287
-		$header = [
288
-			'tag' => $tag,
289
-			'attributes' => $attributes,
290
-			'text' => $text
291
-		];
292
-		if ($prepend === true) {
293
-			array_unshift(self::$headers, $header);
294
-		} else {
295
-			self::$headers[] = $header;
296
-		}
297
-	}
298
-
299
-	/**
300
-	 * check if the current server configuration is suitable for ownCloud
301
-	 *
302
-	 * @return array arrays with error messages and hints
303
-	 */
304
-	public static function checkServer(\OC\SystemConfig $config) {
305
-		$l = \OC::$server->getL10N('lib');
306
-		$errors = [];
307
-		$CONFIG_DATADIRECTORY = $config->getValue('datadirectory', OC::$SERVERROOT . '/data');
308
-
309
-		if (!self::needUpgrade($config) && $config->getValue('installed', false)) {
310
-			// this check needs to be done every time
311
-			$errors = self::checkDataDirectoryValidity($CONFIG_DATADIRECTORY);
312
-		}
313
-
314
-		// Assume that if checkServer() succeeded before in this session, then all is fine.
315
-		if (\OC::$server->getSession()->exists('checkServer_succeeded') && \OC::$server->getSession()->get('checkServer_succeeded')) {
316
-			return $errors;
317
-		}
318
-
319
-		$webServerRestart = false;
320
-		$setup = \OCP\Server::get(\OC\Setup::class);
321
-
322
-		$urlGenerator = \OC::$server->getURLGenerator();
323
-
324
-		$availableDatabases = $setup->getSupportedDatabases();
325
-		if (empty($availableDatabases)) {
326
-			$errors[] = [
327
-				'error' => $l->t('No database drivers (sqlite, mysql, or postgresql) installed.'),
328
-				'hint' => '' //TODO: sane hint
329
-			];
330
-			$webServerRestart = true;
331
-		}
332
-
333
-		// Check if config folder is writable.
334
-		if (!(bool)$config->getValue('config_is_read_only', false)) {
335
-			if (!is_writable(OC::$configDir) || !is_readable(OC::$configDir)) {
336
-				$errors[] = [
337
-					'error' => $l->t('Cannot write into "config" directory.'),
338
-					'hint' => $l->t('This can usually be fixed by giving the web server write access to the config directory. See %s',
339
-						[ $urlGenerator->linkToDocs('admin-dir_permissions') ]) . '. '
340
-						. $l->t('Or, if you prefer to keep config.php file read only, set the option "config_is_read_only" to true in it. See %s',
341
-							[ $urlGenerator->linkToDocs('admin-config') ])
342
-				];
343
-			}
344
-		}
345
-
346
-		// Create root dir.
347
-		if ($config->getValue('installed', false)) {
348
-			if (!is_dir($CONFIG_DATADIRECTORY)) {
349
-				$success = @mkdir($CONFIG_DATADIRECTORY);
350
-				if ($success) {
351
-					$errors = array_merge($errors, self::checkDataDirectoryPermissions($CONFIG_DATADIRECTORY));
352
-				} else {
353
-					$errors[] = [
354
-						'error' => $l->t('Cannot create "data" directory.'),
355
-						'hint' => $l->t('This can usually be fixed by giving the web server write access to the root directory. See %s',
356
-							[$urlGenerator->linkToDocs('admin-dir_permissions')])
357
-					];
358
-				}
359
-			} elseif (!is_writable($CONFIG_DATADIRECTORY) || !is_readable($CONFIG_DATADIRECTORY)) {
360
-				// is_writable doesn't work for NFS mounts, so try to write a file and check if it exists.
361
-				$testFile = sprintf('%s/%s.tmp', $CONFIG_DATADIRECTORY, uniqid('data_dir_writability_test_'));
362
-				$handle = fopen($testFile, 'w');
363
-				if (!$handle || fwrite($handle, 'Test write operation') === false) {
364
-					$permissionsHint = $l->t('Permissions can usually be fixed by giving the web server write access to the root directory. See %s.',
365
-						[$urlGenerator->linkToDocs('admin-dir_permissions')]);
366
-					$errors[] = [
367
-						'error' => $l->t('Your data directory is not writable.'),
368
-						'hint' => $permissionsHint
369
-					];
370
-				} else {
371
-					fclose($handle);
372
-					unlink($testFile);
373
-				}
374
-			} else {
375
-				$errors = array_merge($errors, self::checkDataDirectoryPermissions($CONFIG_DATADIRECTORY));
376
-			}
377
-		}
378
-
379
-		if (!OC_Util::isSetLocaleWorking()) {
380
-			$errors[] = [
381
-				'error' => $l->t('Setting locale to %s failed.',
382
-					['en_US.UTF-8/fr_FR.UTF-8/es_ES.UTF-8/de_DE.UTF-8/ru_RU.UTF-8/'
383
-						. 'pt_BR.UTF-8/it_IT.UTF-8/ja_JP.UTF-8/zh_CN.UTF-8']),
384
-				'hint' => $l->t('Please install one of these locales on your system and restart your web server.')
385
-			];
386
-		}
387
-
388
-		// Contains the dependencies that should be checked against
389
-		// classes = class_exists
390
-		// functions = function_exists
391
-		// defined = defined
392
-		// ini = ini_get
393
-		// If the dependency is not found the missing module name is shown to the EndUser
394
-		// When adding new checks always verify that they pass on CI as well
395
-		$dependencies = [
396
-			'classes' => [
397
-				'ZipArchive' => 'zip',
398
-				'DOMDocument' => 'dom',
399
-				'XMLWriter' => 'XMLWriter',
400
-				'XMLReader' => 'XMLReader',
401
-			],
402
-			'functions' => [
403
-				'xml_parser_create' => 'libxml',
404
-				'mb_strcut' => 'mbstring',
405
-				'ctype_digit' => 'ctype',
406
-				'json_encode' => 'JSON',
407
-				'gd_info' => 'GD',
408
-				'gzencode' => 'zlib',
409
-				'simplexml_load_string' => 'SimpleXML',
410
-				'hash' => 'HASH Message Digest Framework',
411
-				'curl_init' => 'cURL',
412
-				'openssl_verify' => 'OpenSSL',
413
-			],
414
-			'defined' => [
415
-				'PDO::ATTR_DRIVER_NAME' => 'PDO'
416
-			],
417
-			'ini' => [
418
-				'default_charset' => 'UTF-8',
419
-			],
420
-		];
421
-		$missingDependencies = [];
422
-		$invalidIniSettings = [];
423
-
424
-		$iniWrapper = \OC::$server->get(IniGetWrapper::class);
425
-		foreach ($dependencies['classes'] as $class => $module) {
426
-			if (!class_exists($class)) {
427
-				$missingDependencies[] = $module;
428
-			}
429
-		}
430
-		foreach ($dependencies['functions'] as $function => $module) {
431
-			if (!function_exists($function)) {
432
-				$missingDependencies[] = $module;
433
-			}
434
-		}
435
-		foreach ($dependencies['defined'] as $defined => $module) {
436
-			if (!defined($defined)) {
437
-				$missingDependencies[] = $module;
438
-			}
439
-		}
440
-		foreach ($dependencies['ini'] as $setting => $expected) {
441
-			if (strtolower($iniWrapper->getString($setting)) !== strtolower($expected)) {
442
-				$invalidIniSettings[] = [$setting, $expected];
443
-			}
444
-		}
445
-
446
-		foreach ($missingDependencies as $missingDependency) {
447
-			$errors[] = [
448
-				'error' => $l->t('PHP module %s not installed.', [$missingDependency]),
449
-				'hint' => $l->t('Please ask your server administrator to install the module.'),
450
-			];
451
-			$webServerRestart = true;
452
-		}
453
-		foreach ($invalidIniSettings as $setting) {
454
-			$errors[] = [
455
-				'error' => $l->t('PHP setting "%s" is not set to "%s".', [$setting[0], var_export($setting[1], true)]),
456
-				'hint' => $l->t('Adjusting this setting in php.ini will make Nextcloud run again')
457
-			];
458
-			$webServerRestart = true;
459
-		}
460
-
461
-		if (!self::isAnnotationsWorking()) {
462
-			$errors[] = [
463
-				'error' => $l->t('PHP is apparently set up to strip inline doc blocks. This will make several core apps inaccessible.'),
464
-				'hint' => $l->t('This is probably caused by a cache/accelerator such as Zend OPcache or eAccelerator.')
465
-			];
466
-		}
467
-
468
-		if (!\OC::$CLI && $webServerRestart) {
469
-			$errors[] = [
470
-				'error' => $l->t('PHP modules have been installed, but they are still listed as missing?'),
471
-				'hint' => $l->t('Please ask your server administrator to restart the web server.')
472
-			];
473
-		}
474
-
475
-		foreach (['secret', 'instanceid', 'passwordsalt'] as $requiredConfig) {
476
-			if ($config->getValue($requiredConfig, '') === '' && !\OC::$CLI && $config->getValue('installed', false)) {
477
-				$errors[] = [
478
-					'error' => $l->t('The required %s config variable is not configured in the config.php file.', [$requiredConfig]),
479
-					'hint' => $l->t('Please ask your server administrator to check the Nextcloud configuration.')
480
-				];
481
-			}
482
-		}
483
-
484
-		// Cache the result of this function
485
-		\OC::$server->getSession()->set('checkServer_succeeded', count($errors) == 0);
486
-
487
-		return $errors;
488
-	}
489
-
490
-	/**
491
-	 * Check for correct file permissions of data directory
492
-	 *
493
-	 * @param string $dataDirectory
494
-	 * @return array arrays with error messages and hints
495
-	 * @internal
496
-	 */
497
-	public static function checkDataDirectoryPermissions($dataDirectory) {
498
-		if (!\OC::$server->getConfig()->getSystemValueBool('check_data_directory_permissions', true)) {
499
-			return  [];
500
-		}
501
-
502
-		$perms = substr(decoct(@fileperms($dataDirectory)), -3);
503
-		if (substr($perms, -1) !== '0') {
504
-			chmod($dataDirectory, 0770);
505
-			clearstatcache();
506
-			$perms = substr(decoct(@fileperms($dataDirectory)), -3);
507
-			if ($perms[2] !== '0') {
508
-				$l = \OC::$server->getL10N('lib');
509
-				return [[
510
-					'error' => $l->t('Your data directory is readable by other people.'),
511
-					'hint' => $l->t('Please change the permissions to 0770 so that the directory cannot be listed by other people.'),
512
-				]];
513
-			}
514
-		}
515
-		return [];
516
-	}
517
-
518
-	/**
519
-	 * Check that the data directory exists and is valid by
520
-	 * checking the existence of the ".ncdata" file.
521
-	 *
522
-	 * @param string $dataDirectory data directory path
523
-	 * @return array errors found
524
-	 * @internal
525
-	 */
526
-	public static function checkDataDirectoryValidity($dataDirectory) {
527
-		$l = \OC::$server->getL10N('lib');
528
-		$errors = [];
529
-		if ($dataDirectory[0] !== '/') {
530
-			$errors[] = [
531
-				'error' => $l->t('Your data directory must be an absolute path.'),
532
-				'hint' => $l->t('Check the value of "datadirectory" in your configuration.')
533
-			];
534
-		}
535
-
536
-		if (!file_exists($dataDirectory . '/.ncdata')) {
537
-			$errors[] = [
538
-				'error' => $l->t('Your data directory is invalid.'),
539
-				'hint' => $l->t('Ensure there is a file called "%1$s" in the root of the data directory. It should have the content: "%2$s"', ['.ncdata', '# Nextcloud data directory']),
540
-			];
541
-		}
542
-		return $errors;
543
-	}
544
-
545
-	/**
546
-	 * Check if the user is logged in, redirects to home if not. With
547
-	 * redirect URL parameter to the request URI.
548
-	 *
549
-	 * @deprecated 32.0.0
550
-	 */
551
-	public static function checkLoggedIn(): void {
552
-		// Check if we are a user
553
-		if (!\OC::$server->getUserSession()->isLoggedIn()) {
554
-			header('Location: ' . \OC::$server->getURLGenerator()->linkToRoute(
555
-				'core.login.showLoginForm',
556
-				[
557
-					'redirect_url' => \OC::$server->getRequest()->getRequestUri(),
558
-				]
559
-			)
560
-			);
561
-			exit();
562
-		}
563
-		// Redirect to 2FA challenge selection if 2FA challenge was not solved yet
564
-		if (\OC::$server->get(TwoFactorAuthManager::class)->needsSecondFactor(\OC::$server->getUserSession()->getUser())) {
565
-			header('Location: ' . \OC::$server->getURLGenerator()->linkToRoute('core.TwoFactorChallenge.selectChallenge'));
566
-			exit();
567
-		}
568
-	}
569
-
570
-	/**
571
-	 * Check if the user is a admin, redirects to home if not
572
-	 *
573
-	 * @deprecated 32.0.0
574
-	 */
575
-	public static function checkAdminUser(): void {
576
-		self::checkLoggedIn();
577
-		if (!OC_User::isAdminUser(OC_User::getUser())) {
578
-			header('Location: ' . \OCP\Util::linkToAbsolute('', 'index.php'));
579
-			exit();
580
-		}
581
-	}
582
-
583
-	/**
584
-	 * Returns the URL of the default page
585
-	 * based on the system configuration and
586
-	 * the apps visible for the current user
587
-	 *
588
-	 * @return string URL
589
-	 * @deprecated 32.0.0 use IURLGenerator's linkToDefaultPageUrl directly
590
-	 */
591
-	public static function getDefaultPageUrl() {
592
-		/** @var IURLGenerator $urlGenerator */
593
-		$urlGenerator = \OC::$server->get(IURLGenerator::class);
594
-		return $urlGenerator->linkToDefaultPageUrl();
595
-	}
596
-
597
-	/**
598
-	 * Redirect to the user default page
599
-	 *
600
-	 * @deprecated 32.0.0
601
-	 */
602
-	public static function redirectToDefaultPage(): void {
603
-		$location = self::getDefaultPageUrl();
604
-		header('Location: ' . $location);
605
-		exit();
606
-	}
607
-
608
-	/**
609
-	 * get an id unique for this instance
610
-	 *
611
-	 * @return string
612
-	 */
613
-	public static function getInstanceId(): string {
614
-		$id = \OC::$server->getSystemConfig()->getValue('instanceid', null);
615
-		if (is_null($id)) {
616
-			// We need to guarantee at least one letter in instanceid so it can be used as the session_name
617
-			$id = 'oc' . \OC::$server->get(ISecureRandom::class)->generate(10, \OCP\Security\ISecureRandom::CHAR_LOWER . \OCP\Security\ISecureRandom::CHAR_DIGITS);
618
-			\OC::$server->getSystemConfig()->setValue('instanceid', $id);
619
-		}
620
-		return $id;
621
-	}
622
-
623
-	/**
624
-	 * Public function to sanitize HTML
625
-	 *
626
-	 * This function is used to sanitize HTML and should be applied on any
627
-	 * string or array of strings before displaying it on a web page.
628
-	 *
629
-	 * @param string|string[] $value
630
-	 * @return ($value is array ? string[] : string)
631
-	 * @deprecated 32.0.0 use \OCP\Util::sanitizeHTML instead
632
-	 */
633
-	public static function sanitizeHTML($value) {
634
-		if (is_array($value)) {
635
-			$value = array_map(function ($value) {
636
-				return self::sanitizeHTML($value);
637
-			}, $value);
638
-		} else {
639
-			// Specify encoding for PHP<5.4
640
-			$value = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
641
-		}
642
-		return $value;
643
-	}
644
-
645
-	/**
646
-	 * Public function to encode url parameters
647
-	 *
648
-	 * This function is used to encode path to file before output.
649
-	 * Encoding is done according to RFC 3986 with one exception:
650
-	 * Character '/' is preserved as is.
651
-	 *
652
-	 * @param string $component part of URI to encode
653
-	 * @return string
654
-	 * @deprecated 32.0.0 use \OCP\Util::encodePath instead
655
-	 */
656
-	public static function encodePath($component) {
657
-		$encoded = rawurlencode($component);
658
-		$encoded = str_replace('%2F', '/', $encoded);
659
-		return $encoded;
660
-	}
661
-
662
-	/**
663
-	 * Check if current locale is non-UTF8
664
-	 *
665
-	 * @return bool
666
-	 */
667
-	private static function isNonUTF8Locale() {
668
-		if (function_exists('escapeshellcmd')) {
669
-			return escapeshellcmd('§') === '';
670
-		} elseif (function_exists('escapeshellarg')) {
671
-			return escapeshellarg('§') === '\'\'';
672
-		} else {
673
-			return preg_match('/utf-?8/i', setlocale(LC_CTYPE, 0)) === 0;
674
-		}
675
-	}
676
-
677
-	/**
678
-	 * Check if the setlocale call does not work. This can happen if the right
679
-	 * local packages are not available on the server.
680
-	 *
681
-	 * @internal
682
-	 */
683
-	public static function isSetLocaleWorking(): bool {
684
-		if (self::isNonUTF8Locale()) {
685
-			// Borrowed from \Patchwork\Utf8\Bootup::initLocale
686
-			setlocale(LC_ALL, 'C.UTF-8', 'C');
687
-			setlocale(LC_CTYPE, 'en_US.UTF-8', 'fr_FR.UTF-8', 'es_ES.UTF-8', 'de_DE.UTF-8', 'ru_RU.UTF-8', 'pt_BR.UTF-8', 'it_IT.UTF-8', 'ja_JP.UTF-8', 'zh_CN.UTF-8', '0');
688
-
689
-			// Check again
690
-			if (self::isNonUTF8Locale()) {
691
-				return false;
692
-			}
693
-		}
694
-
695
-		return true;
696
-	}
697
-
698
-	/**
699
-	 * Check if it's possible to get the inline annotations
700
-	 *
701
-	 * @internal
702
-	 */
703
-	public static function isAnnotationsWorking(): bool {
704
-		if (PHP_VERSION_ID >= 80300) {
705
-			/** @psalm-suppress UndefinedMethod */
706
-			$reflection = \ReflectionMethod::createFromMethodName(__METHOD__);
707
-		} else {
708
-			$reflection = new \ReflectionMethod(__METHOD__);
709
-		}
710
-		$docs = $reflection->getDocComment();
711
-
712
-		return (is_string($docs) && strlen($docs) > 50);
713
-	}
714
-
715
-	/**
716
-	 * Check if the PHP module fileinfo is loaded.
717
-	 *
718
-	 * @internal
719
-	 */
720
-	public static function fileInfoLoaded(): bool {
721
-		return function_exists('finfo_open');
722
-	}
723
-
724
-	/**
725
-	 * clear all levels of output buffering
726
-	 *
727
-	 * @return void
728
-	 */
729
-	public static function obEnd() {
730
-		while (ob_get_level()) {
731
-			ob_end_clean();
732
-		}
733
-	}
734
-
735
-	/**
736
-	 * Handles the case that there may not be a theme, then check if a "default"
737
-	 * theme exists and take that one
738
-	 *
739
-	 * @return string the theme
740
-	 */
741
-	public static function getTheme() {
742
-		$theme = \OC::$server->getSystemConfig()->getValue('theme', '');
743
-
744
-		if ($theme === '') {
745
-			if (is_dir(OC::$SERVERROOT . '/themes/default')) {
746
-				$theme = 'default';
747
-			}
748
-		}
749
-
750
-		return $theme;
751
-	}
752
-
753
-	/**
754
-	 * Normalize a unicode string
755
-	 *
756
-	 * @param string $value a not normalized string
757
-	 * @return string The normalized string or the input if the normalization failed
758
-	 */
759
-	public static function normalizeUnicode(string $value): string {
760
-		if (Normalizer::isNormalized($value)) {
761
-			return $value;
762
-		}
763
-
764
-		$normalizedValue = Normalizer::normalize($value);
765
-		if ($normalizedValue === false) {
766
-			\OCP\Server::get(LoggerInterface::class)->warning('normalizing failed for "' . $value . '"', ['app' => 'core']);
767
-			return $value;
768
-		}
769
-
770
-		return $normalizedValue;
771
-	}
772
-
773
-	/**
774
-	 * Check whether the instance needs to perform an upgrade,
775
-	 * either when the core version is higher or any app requires
776
-	 * an upgrade.
777
-	 *
778
-	 * @param \OC\SystemConfig $config
779
-	 * @return bool whether the core or any app needs an upgrade
780
-	 * @throws \OCP\HintException When the upgrade from the given version is not allowed
781
-	 * @deprecated 32.0.0 Use \OCP\Util::needUpgrade instead
782
-	 */
783
-	public static function needUpgrade(\OC\SystemConfig $config) {
784
-		if ($config->getValue('installed', false)) {
785
-			$installedVersion = $config->getValue('version', '0.0.0');
786
-			$currentVersion = implode('.', \OCP\Util::getVersion());
787
-			$versionDiff = version_compare($currentVersion, $installedVersion);
788
-			if ($versionDiff > 0) {
789
-				return true;
790
-			} elseif ($config->getValue('debug', false) && $versionDiff < 0) {
791
-				// downgrade with debug
792
-				$installedMajor = explode('.', $installedVersion);
793
-				$installedMajor = $installedMajor[0] . '.' . $installedMajor[1];
794
-				$currentMajor = explode('.', $currentVersion);
795
-				$currentMajor = $currentMajor[0] . '.' . $currentMajor[1];
796
-				if ($installedMajor === $currentMajor) {
797
-					// Same major, allow downgrade for developers
798
-					return true;
799
-				} else {
800
-					// downgrade attempt, throw exception
801
-					throw new \OCP\HintException('Downgrading is not supported and is likely to cause unpredictable issues (from ' . $installedVersion . ' to ' . $currentVersion . ')');
802
-				}
803
-			} elseif ($versionDiff < 0) {
804
-				// downgrade attempt, throw exception
805
-				throw new \OCP\HintException('Downgrading is not supported and is likely to cause unpredictable issues (from ' . $installedVersion . ' to ' . $currentVersion . ')');
806
-			}
807
-
808
-			// also check for upgrades for apps (independently from the user)
809
-			$apps = \OC_App::getEnabledApps(false, true);
810
-			$shouldUpgrade = false;
811
-			foreach ($apps as $app) {
812
-				if (\OC_App::shouldUpgrade($app)) {
813
-					$shouldUpgrade = true;
814
-					break;
815
-				}
816
-			}
817
-			return $shouldUpgrade;
818
-		} else {
819
-			return false;
820
-		}
821
-	}
25
+    public static $styles = [];
26
+    public static $headers = [];
27
+
28
+    /**
29
+     * Setup the file system
30
+     *
31
+     * @param string|null $user
32
+     * @return boolean
33
+     * @description configure the initial filesystem based on the configuration
34
+     * @suppress PhanDeprecatedFunction
35
+     * @suppress PhanAccessMethodInternal
36
+     */
37
+    public static function setupFS(?string $user = '') {
38
+        // If we are not forced to load a specific user we load the one that is logged in
39
+        if ($user === '') {
40
+            $userObject = \OC::$server->get(\OCP\IUserSession::class)->getUser();
41
+        } else {
42
+            $userObject = \OC::$server->get(\OCP\IUserManager::class)->get($user);
43
+        }
44
+
45
+        /** @var SetupManager $setupManager */
46
+        $setupManager = \OC::$server->get(SetupManager::class);
47
+
48
+        if ($userObject) {
49
+            $setupManager->setupForUser($userObject);
50
+        } else {
51
+            $setupManager->setupRoot();
52
+        }
53
+        return true;
54
+    }
55
+
56
+    /**
57
+     * Check if a password is required for each public link
58
+     *
59
+     * @param bool $checkGroupMembership Check group membership exclusion
60
+     * @return bool
61
+     * @deprecated 32.0.0 use OCP\Share\IManager's shareApiLinkEnforcePassword directly
62
+     */
63
+    public static function isPublicLinkPasswordRequired(bool $checkGroupMembership = true) {
64
+        /** @var IManager $shareManager */
65
+        $shareManager = \OC::$server->get(IManager::class);
66
+        return $shareManager->shareApiLinkEnforcePassword($checkGroupMembership);
67
+    }
68
+
69
+    /**
70
+     * check if sharing is disabled for the current user
71
+     * @param IConfig $config
72
+     * @param IGroupManager $groupManager
73
+     * @param IUser|null $user
74
+     * @return bool
75
+     * @deprecated 32.0.0 use OCP\Share\IManager's sharingDisabledForUser directly
76
+     */
77
+    public static function isSharingDisabledForUser(IConfig $config, IGroupManager $groupManager, $user) {
78
+        /** @var IManager $shareManager */
79
+        $shareManager = \OC::$server->get(IManager::class);
80
+        $userId = $user ? $user->getUID() : null;
81
+        return $shareManager->sharingDisabledForUser($userId);
82
+    }
83
+
84
+    /**
85
+     * check if share API enforces a default expire date
86
+     *
87
+     * @return bool
88
+     * @deprecated 32.0.0 use OCP\Share\IManager's shareApiLinkDefaultExpireDateEnforced directly
89
+     */
90
+    public static function isDefaultExpireDateEnforced() {
91
+        /** @var IManager $shareManager */
92
+        $shareManager = \OC::$server->get(IManager::class);
93
+        return $shareManager->shareApiLinkDefaultExpireDateEnforced();
94
+    }
95
+
96
+    /**
97
+     * Get the quota of a user
98
+     *
99
+     * @param IUser|null $user
100
+     * @return int|\OCP\Files\FileInfo::SPACE_UNLIMITED|false|float Quota bytes
101
+     * @deprecated 9.0.0 - Use \OCP\IUser::getQuota or \OCP\IUser::getQuotaBytes
102
+     */
103
+    public static function getUserQuota(?IUser $user) {
104
+        if (is_null($user)) {
105
+            return \OCP\Files\FileInfo::SPACE_UNLIMITED;
106
+        }
107
+        $userQuota = $user->getQuota();
108
+        if ($userQuota === 'none') {
109
+            return \OCP\Files\FileInfo::SPACE_UNLIMITED;
110
+        }
111
+        return \OCP\Util::computerFileSize($userQuota);
112
+    }
113
+
114
+    /**
115
+     * copies the skeleton to the users /files
116
+     *
117
+     * @param string $userId
118
+     * @param \OCP\Files\Folder $userDirectory
119
+     * @throws \OCP\Files\NotFoundException
120
+     * @throws \OCP\Files\NotPermittedException
121
+     * @suppress PhanDeprecatedFunction
122
+     */
123
+    public static function copySkeleton($userId, \OCP\Files\Folder $userDirectory) {
124
+        /** @var LoggerInterface $logger */
125
+        $logger = \OC::$server->get(LoggerInterface::class);
126
+
127
+        $plainSkeletonDirectory = \OC::$server->getConfig()->getSystemValueString('skeletondirectory', \OC::$SERVERROOT . '/core/skeleton');
128
+        $userLang = \OC::$server->get(IFactory::class)->findLanguage();
129
+        $skeletonDirectory = str_replace('{lang}', $userLang, $plainSkeletonDirectory);
130
+
131
+        if (!file_exists($skeletonDirectory)) {
132
+            $dialectStart = strpos($userLang, '_');
133
+            if ($dialectStart !== false) {
134
+                $skeletonDirectory = str_replace('{lang}', substr($userLang, 0, $dialectStart), $plainSkeletonDirectory);
135
+            }
136
+            if ($dialectStart === false || !file_exists($skeletonDirectory)) {
137
+                $skeletonDirectory = str_replace('{lang}', 'default', $plainSkeletonDirectory);
138
+            }
139
+            if (!file_exists($skeletonDirectory)) {
140
+                $skeletonDirectory = '';
141
+            }
142
+        }
143
+
144
+        $instanceId = \OC::$server->getConfig()->getSystemValue('instanceid', '');
145
+
146
+        if ($instanceId === null) {
147
+            throw new \RuntimeException('no instance id!');
148
+        }
149
+        $appdata = 'appdata_' . $instanceId;
150
+        if ($userId === $appdata) {
151
+            throw new \RuntimeException('username is reserved name: ' . $appdata);
152
+        }
153
+
154
+        if (!empty($skeletonDirectory)) {
155
+            $logger->debug('copying skeleton for ' . $userId . ' from ' . $skeletonDirectory . ' to ' . $userDirectory->getFullPath('/'), ['app' => 'files_skeleton']);
156
+            self::copyr($skeletonDirectory, $userDirectory);
157
+            // update the file cache
158
+            $userDirectory->getStorage()->getScanner()->scan('', \OC\Files\Cache\Scanner::SCAN_RECURSIVE);
159
+
160
+            /** @var ITemplateManager $templateManager */
161
+            $templateManager = \OC::$server->get(ITemplateManager::class);
162
+            $templateManager->initializeTemplateDirectory(null, $userId);
163
+        }
164
+    }
165
+
166
+    /**
167
+     * copies a directory recursively by using streams
168
+     *
169
+     * @param string $source
170
+     * @param \OCP\Files\Folder $target
171
+     * @return void
172
+     */
173
+    public static function copyr($source, \OCP\Files\Folder $target) {
174
+        $logger = \OCP\Server::get(LoggerInterface::class);
175
+
176
+        // Verify if folder exists
177
+        $dir = opendir($source);
178
+        if ($dir === false) {
179
+            $logger->error(sprintf('Could not opendir "%s"', $source), ['app' => 'core']);
180
+            return;
181
+        }
182
+
183
+        // Copy the files
184
+        while (false !== ($file = readdir($dir))) {
185
+            if (!\OC\Files\Filesystem::isIgnoredDir($file)) {
186
+                if (is_dir($source . '/' . $file)) {
187
+                    $child = $target->newFolder($file);
188
+                    self::copyr($source . '/' . $file, $child);
189
+                } else {
190
+                    $sourceStream = fopen($source . '/' . $file, 'r');
191
+                    if ($sourceStream === false) {
192
+                        $logger->error(sprintf('Could not fopen "%s"', $source . '/' . $file), ['app' => 'core']);
193
+                        closedir($dir);
194
+                        return;
195
+                    }
196
+                    $target->newFile($file, $sourceStream);
197
+                }
198
+            }
199
+        }
200
+        closedir($dir);
201
+    }
202
+
203
+    /**
204
+     * @deprecated 32.0.0 Call tearDown directly on SetupManager
205
+     */
206
+    public static function tearDownFS(): void {
207
+        $setupManager = \OCP\Server::get(SetupManager::class);
208
+        $setupManager->tearDown();
209
+    }
210
+
211
+    /**
212
+     * generates a path for JS/CSS files. If no application is provided it will create the path for core.
213
+     *
214
+     * @param string $application application to get the files from
215
+     * @param string $directory directory within this application (css, js, vendor, etc)
216
+     * @param ?string $file the file inside of the above folder
217
+     */
218
+    private static function generatePath($application, $directory, $file): string {
219
+        if (is_null($file)) {
220
+            $file = $application;
221
+            $application = '';
222
+        }
223
+        if (!empty($application)) {
224
+            return "$application/$directory/$file";
225
+        } else {
226
+            return "$directory/$file";
227
+        }
228
+    }
229
+
230
+    /**
231
+     * add a css file
232
+     *
233
+     * @param string $application application id
234
+     * @param string|null $file filename
235
+     * @param bool $prepend prepend the Style to the beginning of the list
236
+     * @deprecated 32.0.0 Use \OCP\Util::addStyle
237
+     */
238
+    public static function addStyle($application, $file = null, $prepend = false): void {
239
+        $path = OC_Util::generatePath($application, 'css', $file);
240
+        self::addExternalResource($application, $prepend, $path, 'style');
241
+    }
242
+
243
+    /**
244
+     * add a css file from the vendor sub folder
245
+     *
246
+     * @param string $application application id
247
+     * @param string|null $file filename
248
+     * @param bool $prepend prepend the Style to the beginning of the list
249
+     * @deprecated 32.0.0
250
+     */
251
+    public static function addVendorStyle($application, $file = null, $prepend = false): void {
252
+        $path = OC_Util::generatePath($application, 'vendor', $file);
253
+        self::addExternalResource($application, $prepend, $path, 'style');
254
+    }
255
+
256
+    /**
257
+     * add an external resource css/js file
258
+     *
259
+     * @param string $application application id
260
+     * @param bool $prepend prepend the file to the beginning of the list
261
+     * @param string $path
262
+     * @param string $type (script or style)
263
+     */
264
+    private static function addExternalResource($application, $prepend, $path, $type = 'script'): void {
265
+        if ($type === 'style') {
266
+            if (!in_array($path, self::$styles)) {
267
+                if ($prepend === true) {
268
+                    array_unshift(self::$styles, $path);
269
+                } else {
270
+                    self::$styles[] = $path;
271
+                }
272
+            }
273
+        }
274
+    }
275
+
276
+    /**
277
+     * Add a custom element to the header
278
+     * If $text is null then the element will be written as empty element.
279
+     * So use "" to get a closing tag.
280
+     * @param string $tag tag name of the element
281
+     * @param array $attributes array of attributes for the element
282
+     * @param string $text the text content for the element
283
+     * @param bool $prepend prepend the header to the beginning of the list
284
+     * @deprecated 32.0.0 Use \OCP\Util::addHeader instead
285
+     */
286
+    public static function addHeader($tag, $attributes, $text = null, $prepend = false): void {
287
+        $header = [
288
+            'tag' => $tag,
289
+            'attributes' => $attributes,
290
+            'text' => $text
291
+        ];
292
+        if ($prepend === true) {
293
+            array_unshift(self::$headers, $header);
294
+        } else {
295
+            self::$headers[] = $header;
296
+        }
297
+    }
298
+
299
+    /**
300
+     * check if the current server configuration is suitable for ownCloud
301
+     *
302
+     * @return array arrays with error messages and hints
303
+     */
304
+    public static function checkServer(\OC\SystemConfig $config) {
305
+        $l = \OC::$server->getL10N('lib');
306
+        $errors = [];
307
+        $CONFIG_DATADIRECTORY = $config->getValue('datadirectory', OC::$SERVERROOT . '/data');
308
+
309
+        if (!self::needUpgrade($config) && $config->getValue('installed', false)) {
310
+            // this check needs to be done every time
311
+            $errors = self::checkDataDirectoryValidity($CONFIG_DATADIRECTORY);
312
+        }
313
+
314
+        // Assume that if checkServer() succeeded before in this session, then all is fine.
315
+        if (\OC::$server->getSession()->exists('checkServer_succeeded') && \OC::$server->getSession()->get('checkServer_succeeded')) {
316
+            return $errors;
317
+        }
318
+
319
+        $webServerRestart = false;
320
+        $setup = \OCP\Server::get(\OC\Setup::class);
321
+
322
+        $urlGenerator = \OC::$server->getURLGenerator();
323
+
324
+        $availableDatabases = $setup->getSupportedDatabases();
325
+        if (empty($availableDatabases)) {
326
+            $errors[] = [
327
+                'error' => $l->t('No database drivers (sqlite, mysql, or postgresql) installed.'),
328
+                'hint' => '' //TODO: sane hint
329
+            ];
330
+            $webServerRestart = true;
331
+        }
332
+
333
+        // Check if config folder is writable.
334
+        if (!(bool)$config->getValue('config_is_read_only', false)) {
335
+            if (!is_writable(OC::$configDir) || !is_readable(OC::$configDir)) {
336
+                $errors[] = [
337
+                    'error' => $l->t('Cannot write into "config" directory.'),
338
+                    'hint' => $l->t('This can usually be fixed by giving the web server write access to the config directory. See %s',
339
+                        [ $urlGenerator->linkToDocs('admin-dir_permissions') ]) . '. '
340
+                        . $l->t('Or, if you prefer to keep config.php file read only, set the option "config_is_read_only" to true in it. See %s',
341
+                            [ $urlGenerator->linkToDocs('admin-config') ])
342
+                ];
343
+            }
344
+        }
345
+
346
+        // Create root dir.
347
+        if ($config->getValue('installed', false)) {
348
+            if (!is_dir($CONFIG_DATADIRECTORY)) {
349
+                $success = @mkdir($CONFIG_DATADIRECTORY);
350
+                if ($success) {
351
+                    $errors = array_merge($errors, self::checkDataDirectoryPermissions($CONFIG_DATADIRECTORY));
352
+                } else {
353
+                    $errors[] = [
354
+                        'error' => $l->t('Cannot create "data" directory.'),
355
+                        'hint' => $l->t('This can usually be fixed by giving the web server write access to the root directory. See %s',
356
+                            [$urlGenerator->linkToDocs('admin-dir_permissions')])
357
+                    ];
358
+                }
359
+            } elseif (!is_writable($CONFIG_DATADIRECTORY) || !is_readable($CONFIG_DATADIRECTORY)) {
360
+                // is_writable doesn't work for NFS mounts, so try to write a file and check if it exists.
361
+                $testFile = sprintf('%s/%s.tmp', $CONFIG_DATADIRECTORY, uniqid('data_dir_writability_test_'));
362
+                $handle = fopen($testFile, 'w');
363
+                if (!$handle || fwrite($handle, 'Test write operation') === false) {
364
+                    $permissionsHint = $l->t('Permissions can usually be fixed by giving the web server write access to the root directory. See %s.',
365
+                        [$urlGenerator->linkToDocs('admin-dir_permissions')]);
366
+                    $errors[] = [
367
+                        'error' => $l->t('Your data directory is not writable.'),
368
+                        'hint' => $permissionsHint
369
+                    ];
370
+                } else {
371
+                    fclose($handle);
372
+                    unlink($testFile);
373
+                }
374
+            } else {
375
+                $errors = array_merge($errors, self::checkDataDirectoryPermissions($CONFIG_DATADIRECTORY));
376
+            }
377
+        }
378
+
379
+        if (!OC_Util::isSetLocaleWorking()) {
380
+            $errors[] = [
381
+                'error' => $l->t('Setting locale to %s failed.',
382
+                    ['en_US.UTF-8/fr_FR.UTF-8/es_ES.UTF-8/de_DE.UTF-8/ru_RU.UTF-8/'
383
+                        . 'pt_BR.UTF-8/it_IT.UTF-8/ja_JP.UTF-8/zh_CN.UTF-8']),
384
+                'hint' => $l->t('Please install one of these locales on your system and restart your web server.')
385
+            ];
386
+        }
387
+
388
+        // Contains the dependencies that should be checked against
389
+        // classes = class_exists
390
+        // functions = function_exists
391
+        // defined = defined
392
+        // ini = ini_get
393
+        // If the dependency is not found the missing module name is shown to the EndUser
394
+        // When adding new checks always verify that they pass on CI as well
395
+        $dependencies = [
396
+            'classes' => [
397
+                'ZipArchive' => 'zip',
398
+                'DOMDocument' => 'dom',
399
+                'XMLWriter' => 'XMLWriter',
400
+                'XMLReader' => 'XMLReader',
401
+            ],
402
+            'functions' => [
403
+                'xml_parser_create' => 'libxml',
404
+                'mb_strcut' => 'mbstring',
405
+                'ctype_digit' => 'ctype',
406
+                'json_encode' => 'JSON',
407
+                'gd_info' => 'GD',
408
+                'gzencode' => 'zlib',
409
+                'simplexml_load_string' => 'SimpleXML',
410
+                'hash' => 'HASH Message Digest Framework',
411
+                'curl_init' => 'cURL',
412
+                'openssl_verify' => 'OpenSSL',
413
+            ],
414
+            'defined' => [
415
+                'PDO::ATTR_DRIVER_NAME' => 'PDO'
416
+            ],
417
+            'ini' => [
418
+                'default_charset' => 'UTF-8',
419
+            ],
420
+        ];
421
+        $missingDependencies = [];
422
+        $invalidIniSettings = [];
423
+
424
+        $iniWrapper = \OC::$server->get(IniGetWrapper::class);
425
+        foreach ($dependencies['classes'] as $class => $module) {
426
+            if (!class_exists($class)) {
427
+                $missingDependencies[] = $module;
428
+            }
429
+        }
430
+        foreach ($dependencies['functions'] as $function => $module) {
431
+            if (!function_exists($function)) {
432
+                $missingDependencies[] = $module;
433
+            }
434
+        }
435
+        foreach ($dependencies['defined'] as $defined => $module) {
436
+            if (!defined($defined)) {
437
+                $missingDependencies[] = $module;
438
+            }
439
+        }
440
+        foreach ($dependencies['ini'] as $setting => $expected) {
441
+            if (strtolower($iniWrapper->getString($setting)) !== strtolower($expected)) {
442
+                $invalidIniSettings[] = [$setting, $expected];
443
+            }
444
+        }
445
+
446
+        foreach ($missingDependencies as $missingDependency) {
447
+            $errors[] = [
448
+                'error' => $l->t('PHP module %s not installed.', [$missingDependency]),
449
+                'hint' => $l->t('Please ask your server administrator to install the module.'),
450
+            ];
451
+            $webServerRestart = true;
452
+        }
453
+        foreach ($invalidIniSettings as $setting) {
454
+            $errors[] = [
455
+                'error' => $l->t('PHP setting "%s" is not set to "%s".', [$setting[0], var_export($setting[1], true)]),
456
+                'hint' => $l->t('Adjusting this setting in php.ini will make Nextcloud run again')
457
+            ];
458
+            $webServerRestart = true;
459
+        }
460
+
461
+        if (!self::isAnnotationsWorking()) {
462
+            $errors[] = [
463
+                'error' => $l->t('PHP is apparently set up to strip inline doc blocks. This will make several core apps inaccessible.'),
464
+                'hint' => $l->t('This is probably caused by a cache/accelerator such as Zend OPcache or eAccelerator.')
465
+            ];
466
+        }
467
+
468
+        if (!\OC::$CLI && $webServerRestart) {
469
+            $errors[] = [
470
+                'error' => $l->t('PHP modules have been installed, but they are still listed as missing?'),
471
+                'hint' => $l->t('Please ask your server administrator to restart the web server.')
472
+            ];
473
+        }
474
+
475
+        foreach (['secret', 'instanceid', 'passwordsalt'] as $requiredConfig) {
476
+            if ($config->getValue($requiredConfig, '') === '' && !\OC::$CLI && $config->getValue('installed', false)) {
477
+                $errors[] = [
478
+                    'error' => $l->t('The required %s config variable is not configured in the config.php file.', [$requiredConfig]),
479
+                    'hint' => $l->t('Please ask your server administrator to check the Nextcloud configuration.')
480
+                ];
481
+            }
482
+        }
483
+
484
+        // Cache the result of this function
485
+        \OC::$server->getSession()->set('checkServer_succeeded', count($errors) == 0);
486
+
487
+        return $errors;
488
+    }
489
+
490
+    /**
491
+     * Check for correct file permissions of data directory
492
+     *
493
+     * @param string $dataDirectory
494
+     * @return array arrays with error messages and hints
495
+     * @internal
496
+     */
497
+    public static function checkDataDirectoryPermissions($dataDirectory) {
498
+        if (!\OC::$server->getConfig()->getSystemValueBool('check_data_directory_permissions', true)) {
499
+            return  [];
500
+        }
501
+
502
+        $perms = substr(decoct(@fileperms($dataDirectory)), -3);
503
+        if (substr($perms, -1) !== '0') {
504
+            chmod($dataDirectory, 0770);
505
+            clearstatcache();
506
+            $perms = substr(decoct(@fileperms($dataDirectory)), -3);
507
+            if ($perms[2] !== '0') {
508
+                $l = \OC::$server->getL10N('lib');
509
+                return [[
510
+                    'error' => $l->t('Your data directory is readable by other people.'),
511
+                    'hint' => $l->t('Please change the permissions to 0770 so that the directory cannot be listed by other people.'),
512
+                ]];
513
+            }
514
+        }
515
+        return [];
516
+    }
517
+
518
+    /**
519
+     * Check that the data directory exists and is valid by
520
+     * checking the existence of the ".ncdata" file.
521
+     *
522
+     * @param string $dataDirectory data directory path
523
+     * @return array errors found
524
+     * @internal
525
+     */
526
+    public static function checkDataDirectoryValidity($dataDirectory) {
527
+        $l = \OC::$server->getL10N('lib');
528
+        $errors = [];
529
+        if ($dataDirectory[0] !== '/') {
530
+            $errors[] = [
531
+                'error' => $l->t('Your data directory must be an absolute path.'),
532
+                'hint' => $l->t('Check the value of "datadirectory" in your configuration.')
533
+            ];
534
+        }
535
+
536
+        if (!file_exists($dataDirectory . '/.ncdata')) {
537
+            $errors[] = [
538
+                'error' => $l->t('Your data directory is invalid.'),
539
+                'hint' => $l->t('Ensure there is a file called "%1$s" in the root of the data directory. It should have the content: "%2$s"', ['.ncdata', '# Nextcloud data directory']),
540
+            ];
541
+        }
542
+        return $errors;
543
+    }
544
+
545
+    /**
546
+     * Check if the user is logged in, redirects to home if not. With
547
+     * redirect URL parameter to the request URI.
548
+     *
549
+     * @deprecated 32.0.0
550
+     */
551
+    public static function checkLoggedIn(): void {
552
+        // Check if we are a user
553
+        if (!\OC::$server->getUserSession()->isLoggedIn()) {
554
+            header('Location: ' . \OC::$server->getURLGenerator()->linkToRoute(
555
+                'core.login.showLoginForm',
556
+                [
557
+                    'redirect_url' => \OC::$server->getRequest()->getRequestUri(),
558
+                ]
559
+            )
560
+            );
561
+            exit();
562
+        }
563
+        // Redirect to 2FA challenge selection if 2FA challenge was not solved yet
564
+        if (\OC::$server->get(TwoFactorAuthManager::class)->needsSecondFactor(\OC::$server->getUserSession()->getUser())) {
565
+            header('Location: ' . \OC::$server->getURLGenerator()->linkToRoute('core.TwoFactorChallenge.selectChallenge'));
566
+            exit();
567
+        }
568
+    }
569
+
570
+    /**
571
+     * Check if the user is a admin, redirects to home if not
572
+     *
573
+     * @deprecated 32.0.0
574
+     */
575
+    public static function checkAdminUser(): void {
576
+        self::checkLoggedIn();
577
+        if (!OC_User::isAdminUser(OC_User::getUser())) {
578
+            header('Location: ' . \OCP\Util::linkToAbsolute('', 'index.php'));
579
+            exit();
580
+        }
581
+    }
582
+
583
+    /**
584
+     * Returns the URL of the default page
585
+     * based on the system configuration and
586
+     * the apps visible for the current user
587
+     *
588
+     * @return string URL
589
+     * @deprecated 32.0.0 use IURLGenerator's linkToDefaultPageUrl directly
590
+     */
591
+    public static function getDefaultPageUrl() {
592
+        /** @var IURLGenerator $urlGenerator */
593
+        $urlGenerator = \OC::$server->get(IURLGenerator::class);
594
+        return $urlGenerator->linkToDefaultPageUrl();
595
+    }
596
+
597
+    /**
598
+     * Redirect to the user default page
599
+     *
600
+     * @deprecated 32.0.0
601
+     */
602
+    public static function redirectToDefaultPage(): void {
603
+        $location = self::getDefaultPageUrl();
604
+        header('Location: ' . $location);
605
+        exit();
606
+    }
607
+
608
+    /**
609
+     * get an id unique for this instance
610
+     *
611
+     * @return string
612
+     */
613
+    public static function getInstanceId(): string {
614
+        $id = \OC::$server->getSystemConfig()->getValue('instanceid', null);
615
+        if (is_null($id)) {
616
+            // We need to guarantee at least one letter in instanceid so it can be used as the session_name
617
+            $id = 'oc' . \OC::$server->get(ISecureRandom::class)->generate(10, \OCP\Security\ISecureRandom::CHAR_LOWER . \OCP\Security\ISecureRandom::CHAR_DIGITS);
618
+            \OC::$server->getSystemConfig()->setValue('instanceid', $id);
619
+        }
620
+        return $id;
621
+    }
622
+
623
+    /**
624
+     * Public function to sanitize HTML
625
+     *
626
+     * This function is used to sanitize HTML and should be applied on any
627
+     * string or array of strings before displaying it on a web page.
628
+     *
629
+     * @param string|string[] $value
630
+     * @return ($value is array ? string[] : string)
631
+     * @deprecated 32.0.0 use \OCP\Util::sanitizeHTML instead
632
+     */
633
+    public static function sanitizeHTML($value) {
634
+        if (is_array($value)) {
635
+            $value = array_map(function ($value) {
636
+                return self::sanitizeHTML($value);
637
+            }, $value);
638
+        } else {
639
+            // Specify encoding for PHP<5.4
640
+            $value = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
641
+        }
642
+        return $value;
643
+    }
644
+
645
+    /**
646
+     * Public function to encode url parameters
647
+     *
648
+     * This function is used to encode path to file before output.
649
+     * Encoding is done according to RFC 3986 with one exception:
650
+     * Character '/' is preserved as is.
651
+     *
652
+     * @param string $component part of URI to encode
653
+     * @return string
654
+     * @deprecated 32.0.0 use \OCP\Util::encodePath instead
655
+     */
656
+    public static function encodePath($component) {
657
+        $encoded = rawurlencode($component);
658
+        $encoded = str_replace('%2F', '/', $encoded);
659
+        return $encoded;
660
+    }
661
+
662
+    /**
663
+     * Check if current locale is non-UTF8
664
+     *
665
+     * @return bool
666
+     */
667
+    private static function isNonUTF8Locale() {
668
+        if (function_exists('escapeshellcmd')) {
669
+            return escapeshellcmd('§') === '';
670
+        } elseif (function_exists('escapeshellarg')) {
671
+            return escapeshellarg('§') === '\'\'';
672
+        } else {
673
+            return preg_match('/utf-?8/i', setlocale(LC_CTYPE, 0)) === 0;
674
+        }
675
+    }
676
+
677
+    /**
678
+     * Check if the setlocale call does not work. This can happen if the right
679
+     * local packages are not available on the server.
680
+     *
681
+     * @internal
682
+     */
683
+    public static function isSetLocaleWorking(): bool {
684
+        if (self::isNonUTF8Locale()) {
685
+            // Borrowed from \Patchwork\Utf8\Bootup::initLocale
686
+            setlocale(LC_ALL, 'C.UTF-8', 'C');
687
+            setlocale(LC_CTYPE, 'en_US.UTF-8', 'fr_FR.UTF-8', 'es_ES.UTF-8', 'de_DE.UTF-8', 'ru_RU.UTF-8', 'pt_BR.UTF-8', 'it_IT.UTF-8', 'ja_JP.UTF-8', 'zh_CN.UTF-8', '0');
688
+
689
+            // Check again
690
+            if (self::isNonUTF8Locale()) {
691
+                return false;
692
+            }
693
+        }
694
+
695
+        return true;
696
+    }
697
+
698
+    /**
699
+     * Check if it's possible to get the inline annotations
700
+     *
701
+     * @internal
702
+     */
703
+    public static function isAnnotationsWorking(): bool {
704
+        if (PHP_VERSION_ID >= 80300) {
705
+            /** @psalm-suppress UndefinedMethod */
706
+            $reflection = \ReflectionMethod::createFromMethodName(__METHOD__);
707
+        } else {
708
+            $reflection = new \ReflectionMethod(__METHOD__);
709
+        }
710
+        $docs = $reflection->getDocComment();
711
+
712
+        return (is_string($docs) && strlen($docs) > 50);
713
+    }
714
+
715
+    /**
716
+     * Check if the PHP module fileinfo is loaded.
717
+     *
718
+     * @internal
719
+     */
720
+    public static function fileInfoLoaded(): bool {
721
+        return function_exists('finfo_open');
722
+    }
723
+
724
+    /**
725
+     * clear all levels of output buffering
726
+     *
727
+     * @return void
728
+     */
729
+    public static function obEnd() {
730
+        while (ob_get_level()) {
731
+            ob_end_clean();
732
+        }
733
+    }
734
+
735
+    /**
736
+     * Handles the case that there may not be a theme, then check if a "default"
737
+     * theme exists and take that one
738
+     *
739
+     * @return string the theme
740
+     */
741
+    public static function getTheme() {
742
+        $theme = \OC::$server->getSystemConfig()->getValue('theme', '');
743
+
744
+        if ($theme === '') {
745
+            if (is_dir(OC::$SERVERROOT . '/themes/default')) {
746
+                $theme = 'default';
747
+            }
748
+        }
749
+
750
+        return $theme;
751
+    }
752
+
753
+    /**
754
+     * Normalize a unicode string
755
+     *
756
+     * @param string $value a not normalized string
757
+     * @return string The normalized string or the input if the normalization failed
758
+     */
759
+    public static function normalizeUnicode(string $value): string {
760
+        if (Normalizer::isNormalized($value)) {
761
+            return $value;
762
+        }
763
+
764
+        $normalizedValue = Normalizer::normalize($value);
765
+        if ($normalizedValue === false) {
766
+            \OCP\Server::get(LoggerInterface::class)->warning('normalizing failed for "' . $value . '"', ['app' => 'core']);
767
+            return $value;
768
+        }
769
+
770
+        return $normalizedValue;
771
+    }
772
+
773
+    /**
774
+     * Check whether the instance needs to perform an upgrade,
775
+     * either when the core version is higher or any app requires
776
+     * an upgrade.
777
+     *
778
+     * @param \OC\SystemConfig $config
779
+     * @return bool whether the core or any app needs an upgrade
780
+     * @throws \OCP\HintException When the upgrade from the given version is not allowed
781
+     * @deprecated 32.0.0 Use \OCP\Util::needUpgrade instead
782
+     */
783
+    public static function needUpgrade(\OC\SystemConfig $config) {
784
+        if ($config->getValue('installed', false)) {
785
+            $installedVersion = $config->getValue('version', '0.0.0');
786
+            $currentVersion = implode('.', \OCP\Util::getVersion());
787
+            $versionDiff = version_compare($currentVersion, $installedVersion);
788
+            if ($versionDiff > 0) {
789
+                return true;
790
+            } elseif ($config->getValue('debug', false) && $versionDiff < 0) {
791
+                // downgrade with debug
792
+                $installedMajor = explode('.', $installedVersion);
793
+                $installedMajor = $installedMajor[0] . '.' . $installedMajor[1];
794
+                $currentMajor = explode('.', $currentVersion);
795
+                $currentMajor = $currentMajor[0] . '.' . $currentMajor[1];
796
+                if ($installedMajor === $currentMajor) {
797
+                    // Same major, allow downgrade for developers
798
+                    return true;
799
+                } else {
800
+                    // downgrade attempt, throw exception
801
+                    throw new \OCP\HintException('Downgrading is not supported and is likely to cause unpredictable issues (from ' . $installedVersion . ' to ' . $currentVersion . ')');
802
+                }
803
+            } elseif ($versionDiff < 0) {
804
+                // downgrade attempt, throw exception
805
+                throw new \OCP\HintException('Downgrading is not supported and is likely to cause unpredictable issues (from ' . $installedVersion . ' to ' . $currentVersion . ')');
806
+            }
807
+
808
+            // also check for upgrades for apps (independently from the user)
809
+            $apps = \OC_App::getEnabledApps(false, true);
810
+            $shouldUpgrade = false;
811
+            foreach ($apps as $app) {
812
+                if (\OC_App::shouldUpgrade($app)) {
813
+                    $shouldUpgrade = true;
814
+                    break;
815
+                }
816
+            }
817
+            return $shouldUpgrade;
818
+        } else {
819
+            return false;
820
+        }
821
+    }
822 822
 }
Please login to merge, or discard this patch.
lib/private/Setup.php 1 patch
Indentation   +623 added lines, -623 removed lines patch added patch discarded remove patch
@@ -41,630 +41,630 @@
 block discarded – undo
41 41
 use Psr\Log\LoggerInterface;
42 42
 
43 43
 class Setup {
44
-	protected IL10N $l10n;
45
-
46
-	public function __construct(
47
-		protected SystemConfig $config,
48
-		protected IniGetWrapper $iniWrapper,
49
-		IL10NFactory $l10nFactory,
50
-		protected Defaults $defaults,
51
-		protected LoggerInterface $logger,
52
-		protected ISecureRandom $random,
53
-		protected Installer $installer,
54
-	) {
55
-		$this->l10n = $l10nFactory->get('lib');
56
-	}
57
-
58
-	protected static array $dbSetupClasses = [
59
-		'mysql' => \OC\Setup\MySQL::class,
60
-		'pgsql' => \OC\Setup\PostgreSQL::class,
61
-		'oci' => \OC\Setup\OCI::class,
62
-		'sqlite' => \OC\Setup\Sqlite::class,
63
-		'sqlite3' => \OC\Setup\Sqlite::class,
64
-	];
65
-
66
-	/**
67
-	 * Wrapper around the "class_exists" PHP function to be able to mock it
68
-	 */
69
-	protected function class_exists(string $name): bool {
70
-		return class_exists($name);
71
-	}
72
-
73
-	/**
74
-	 * Wrapper around the "is_callable" PHP function to be able to mock it
75
-	 */
76
-	protected function is_callable(string $name): bool {
77
-		return is_callable($name);
78
-	}
79
-
80
-	/**
81
-	 * Wrapper around \PDO::getAvailableDrivers
82
-	 */
83
-	protected function getAvailableDbDriversForPdo(): array {
84
-		if (class_exists(\PDO::class)) {
85
-			return \PDO::getAvailableDrivers();
86
-		}
87
-		return [];
88
-	}
89
-
90
-	/**
91
-	 * Get the available and supported databases of this instance
92
-	 *
93
-	 * @return array
94
-	 * @throws Exception
95
-	 */
96
-	public function getSupportedDatabases(bool $allowAllDatabases = false): array {
97
-		$availableDatabases = [
98
-			'sqlite' => [
99
-				'type' => 'pdo',
100
-				'call' => 'sqlite',
101
-				'name' => 'SQLite',
102
-			],
103
-			'mysql' => [
104
-				'type' => 'pdo',
105
-				'call' => 'mysql',
106
-				'name' => 'MySQL/MariaDB',
107
-			],
108
-			'pgsql' => [
109
-				'type' => 'pdo',
110
-				'call' => 'pgsql',
111
-				'name' => 'PostgreSQL',
112
-			],
113
-			'oci' => [
114
-				'type' => 'function',
115
-				'call' => 'oci_connect',
116
-				'name' => 'Oracle',
117
-			],
118
-		];
119
-		if ($allowAllDatabases) {
120
-			$configuredDatabases = array_keys($availableDatabases);
121
-		} else {
122
-			$configuredDatabases = $this->config->getValue('supportedDatabases',
123
-				['sqlite', 'mysql', 'pgsql']);
124
-		}
125
-		if (!is_array($configuredDatabases)) {
126
-			throw new Exception('Supported databases are not properly configured.');
127
-		}
128
-
129
-		$supportedDatabases = [];
130
-
131
-		foreach ($configuredDatabases as $database) {
132
-			if (array_key_exists($database, $availableDatabases)) {
133
-				$working = false;
134
-				$type = $availableDatabases[$database]['type'];
135
-				$call = $availableDatabases[$database]['call'];
136
-
137
-				if ($type === 'function') {
138
-					$working = $this->is_callable($call);
139
-				} elseif ($type === 'pdo') {
140
-					$working = in_array($call, $this->getAvailableDbDriversForPdo(), true);
141
-				}
142
-				if ($working) {
143
-					$supportedDatabases[$database] = $availableDatabases[$database]['name'];
144
-				}
145
-			}
146
-		}
147
-
148
-		return $supportedDatabases;
149
-	}
150
-
151
-	/**
152
-	 * Gathers system information like database type and does
153
-	 * a few system checks.
154
-	 *
155
-	 * @return array of system info, including an "errors" value
156
-	 *               in case of errors/warnings
157
-	 */
158
-	public function getSystemInfo(bool $allowAllDatabases = false): array {
159
-		$databases = $this->getSupportedDatabases($allowAllDatabases);
160
-
161
-		$dataDir = $this->config->getValue('datadirectory', \OC::$SERVERROOT . '/data');
162
-
163
-		$errors = [];
164
-
165
-		// Create data directory to test whether the .htaccess works
166
-		// Notice that this is not necessarily the same data directory as the one
167
-		// that will effectively be used.
168
-		if (!file_exists($dataDir)) {
169
-			@mkdir($dataDir);
170
-		}
171
-		$htAccessWorking = true;
172
-		if (is_dir($dataDir) && is_writable($dataDir)) {
173
-			// Protect data directory here, so we can test if the protection is working
174
-			self::protectDataDirectory();
175
-
176
-			try {
177
-				$htAccessWorking = $this->isHtaccessWorking($dataDir);
178
-			} catch (\OCP\HintException $e) {
179
-				$errors[] = [
180
-					'error' => $e->getMessage(),
181
-					'exception' => $e,
182
-					'hint' => $e->getHint(),
183
-				];
184
-				$htAccessWorking = false;
185
-			}
186
-		}
187
-
188
-		// Check if running directly on macOS (note: Linux containers on macOS will not trigger this)
189
-		if (PHP_OS_FAMILY === 'Darwin') {
190
-			$errors[] = [
191
-				'error' => $this->l10n->t(
192
-					'macOS is not supported and %s will not work properly on this platform. '
193
-					. 'Use it at your own risk!',
194
-					[$this->defaults->getProductName()]
195
-				),
196
-				'hint' => $this->l10n->t('For the best results, please consider using a GNU/Linux server instead.'),
197
-			];
198
-		}
199
-
200
-		if ($this->iniWrapper->getString('open_basedir') !== '' && PHP_INT_SIZE === 4) {
201
-			$errors[] = [
202
-				'error' => $this->l10n->t(
203
-					'It seems that this %s instance is running on a 32-bit PHP environment and the open_basedir has been configured in php.ini. '
204
-					. 'This will lead to problems with files over 4 GB and is highly discouraged.',
205
-					[$this->defaults->getProductName()]
206
-				),
207
-				'hint' => $this->l10n->t('Please remove the open_basedir setting within your php.ini or switch to 64-bit PHP.'),
208
-			];
209
-		}
210
-
211
-		return [
212
-			'databases' => $databases,
213
-			'directory' => $dataDir,
214
-			'htaccessWorking' => $htAccessWorking,
215
-			'errors' => $errors,
216
-		];
217
-	}
218
-
219
-	public function createHtaccessTestFile(string $dataDir): string|false {
220
-		// php dev server does not support htaccess
221
-		if (php_sapi_name() === 'cli-server') {
222
-			return false;
223
-		}
224
-
225
-		// testdata
226
-		$fileName = '/htaccesstest.txt';
227
-		$testContent = 'This is used for testing whether htaccess is properly enabled to disallow access from the outside. This file can be safely removed.';
228
-
229
-		// creating a test file
230
-		$testFile = $dataDir . '/' . $fileName;
231
-
232
-		if (file_exists($testFile)) {// already running this test, possible recursive call
233
-			return false;
234
-		}
235
-
236
-		$fp = @fopen($testFile, 'w');
237
-		if (!$fp) {
238
-			throw new \OCP\HintException('Can\'t create test file to check for working .htaccess file.',
239
-				'Make sure it is possible for the web server to write to ' . $testFile);
240
-		}
241
-		fwrite($fp, $testContent);
242
-		fclose($fp);
243
-
244
-		return $testContent;
245
-	}
246
-
247
-	/**
248
-	 * Check if the .htaccess file is working
249
-	 *
250
-	 * @param \OCP\IConfig $config
251
-	 * @return bool
252
-	 * @throws Exception
253
-	 * @throws \OCP\HintException If the test file can't get written.
254
-	 */
255
-	public function isHtaccessWorking(string $dataDir) {
256
-		$config = Server::get(IConfig::class);
257
-
258
-		if (\OC::$CLI || !$config->getSystemValueBool('check_for_working_htaccess', true)) {
259
-			return true;
260
-		}
261
-
262
-		$testContent = $this->createHtaccessTestFile($dataDir);
263
-		if ($testContent === false) {
264
-			return false;
265
-		}
266
-
267
-		$fileName = '/htaccesstest.txt';
268
-		$testFile = $dataDir . '/' . $fileName;
269
-
270
-		// accessing the file via http
271
-		$url = Server::get(IURLGenerator::class)->getAbsoluteURL(\OC::$WEBROOT . '/data' . $fileName);
272
-		try {
273
-			$content = Server::get(IClientService::class)->newClient()->get($url)->getBody();
274
-		} catch (\Exception $e) {
275
-			$content = false;
276
-		}
277
-
278
-		if (str_starts_with($url, 'https:')) {
279
-			$url = 'http:' . substr($url, 6);
280
-		} else {
281
-			$url = 'https:' . substr($url, 5);
282
-		}
283
-
284
-		try {
285
-			$fallbackContent = Server::get(IClientService::class)->newClient()->get($url)->getBody();
286
-		} catch (\Exception $e) {
287
-			$fallbackContent = false;
288
-		}
289
-
290
-		// cleanup
291
-		@unlink($testFile);
292
-
293
-		/*
44
+    protected IL10N $l10n;
45
+
46
+    public function __construct(
47
+        protected SystemConfig $config,
48
+        protected IniGetWrapper $iniWrapper,
49
+        IL10NFactory $l10nFactory,
50
+        protected Defaults $defaults,
51
+        protected LoggerInterface $logger,
52
+        protected ISecureRandom $random,
53
+        protected Installer $installer,
54
+    ) {
55
+        $this->l10n = $l10nFactory->get('lib');
56
+    }
57
+
58
+    protected static array $dbSetupClasses = [
59
+        'mysql' => \OC\Setup\MySQL::class,
60
+        'pgsql' => \OC\Setup\PostgreSQL::class,
61
+        'oci' => \OC\Setup\OCI::class,
62
+        'sqlite' => \OC\Setup\Sqlite::class,
63
+        'sqlite3' => \OC\Setup\Sqlite::class,
64
+    ];
65
+
66
+    /**
67
+     * Wrapper around the "class_exists" PHP function to be able to mock it
68
+     */
69
+    protected function class_exists(string $name): bool {
70
+        return class_exists($name);
71
+    }
72
+
73
+    /**
74
+     * Wrapper around the "is_callable" PHP function to be able to mock it
75
+     */
76
+    protected function is_callable(string $name): bool {
77
+        return is_callable($name);
78
+    }
79
+
80
+    /**
81
+     * Wrapper around \PDO::getAvailableDrivers
82
+     */
83
+    protected function getAvailableDbDriversForPdo(): array {
84
+        if (class_exists(\PDO::class)) {
85
+            return \PDO::getAvailableDrivers();
86
+        }
87
+        return [];
88
+    }
89
+
90
+    /**
91
+     * Get the available and supported databases of this instance
92
+     *
93
+     * @return array
94
+     * @throws Exception
95
+     */
96
+    public function getSupportedDatabases(bool $allowAllDatabases = false): array {
97
+        $availableDatabases = [
98
+            'sqlite' => [
99
+                'type' => 'pdo',
100
+                'call' => 'sqlite',
101
+                'name' => 'SQLite',
102
+            ],
103
+            'mysql' => [
104
+                'type' => 'pdo',
105
+                'call' => 'mysql',
106
+                'name' => 'MySQL/MariaDB',
107
+            ],
108
+            'pgsql' => [
109
+                'type' => 'pdo',
110
+                'call' => 'pgsql',
111
+                'name' => 'PostgreSQL',
112
+            ],
113
+            'oci' => [
114
+                'type' => 'function',
115
+                'call' => 'oci_connect',
116
+                'name' => 'Oracle',
117
+            ],
118
+        ];
119
+        if ($allowAllDatabases) {
120
+            $configuredDatabases = array_keys($availableDatabases);
121
+        } else {
122
+            $configuredDatabases = $this->config->getValue('supportedDatabases',
123
+                ['sqlite', 'mysql', 'pgsql']);
124
+        }
125
+        if (!is_array($configuredDatabases)) {
126
+            throw new Exception('Supported databases are not properly configured.');
127
+        }
128
+
129
+        $supportedDatabases = [];
130
+
131
+        foreach ($configuredDatabases as $database) {
132
+            if (array_key_exists($database, $availableDatabases)) {
133
+                $working = false;
134
+                $type = $availableDatabases[$database]['type'];
135
+                $call = $availableDatabases[$database]['call'];
136
+
137
+                if ($type === 'function') {
138
+                    $working = $this->is_callable($call);
139
+                } elseif ($type === 'pdo') {
140
+                    $working = in_array($call, $this->getAvailableDbDriversForPdo(), true);
141
+                }
142
+                if ($working) {
143
+                    $supportedDatabases[$database] = $availableDatabases[$database]['name'];
144
+                }
145
+            }
146
+        }
147
+
148
+        return $supportedDatabases;
149
+    }
150
+
151
+    /**
152
+     * Gathers system information like database type and does
153
+     * a few system checks.
154
+     *
155
+     * @return array of system info, including an "errors" value
156
+     *               in case of errors/warnings
157
+     */
158
+    public function getSystemInfo(bool $allowAllDatabases = false): array {
159
+        $databases = $this->getSupportedDatabases($allowAllDatabases);
160
+
161
+        $dataDir = $this->config->getValue('datadirectory', \OC::$SERVERROOT . '/data');
162
+
163
+        $errors = [];
164
+
165
+        // Create data directory to test whether the .htaccess works
166
+        // Notice that this is not necessarily the same data directory as the one
167
+        // that will effectively be used.
168
+        if (!file_exists($dataDir)) {
169
+            @mkdir($dataDir);
170
+        }
171
+        $htAccessWorking = true;
172
+        if (is_dir($dataDir) && is_writable($dataDir)) {
173
+            // Protect data directory here, so we can test if the protection is working
174
+            self::protectDataDirectory();
175
+
176
+            try {
177
+                $htAccessWorking = $this->isHtaccessWorking($dataDir);
178
+            } catch (\OCP\HintException $e) {
179
+                $errors[] = [
180
+                    'error' => $e->getMessage(),
181
+                    'exception' => $e,
182
+                    'hint' => $e->getHint(),
183
+                ];
184
+                $htAccessWorking = false;
185
+            }
186
+        }
187
+
188
+        // Check if running directly on macOS (note: Linux containers on macOS will not trigger this)
189
+        if (PHP_OS_FAMILY === 'Darwin') {
190
+            $errors[] = [
191
+                'error' => $this->l10n->t(
192
+                    'macOS is not supported and %s will not work properly on this platform. '
193
+                    . 'Use it at your own risk!',
194
+                    [$this->defaults->getProductName()]
195
+                ),
196
+                'hint' => $this->l10n->t('For the best results, please consider using a GNU/Linux server instead.'),
197
+            ];
198
+        }
199
+
200
+        if ($this->iniWrapper->getString('open_basedir') !== '' && PHP_INT_SIZE === 4) {
201
+            $errors[] = [
202
+                'error' => $this->l10n->t(
203
+                    'It seems that this %s instance is running on a 32-bit PHP environment and the open_basedir has been configured in php.ini. '
204
+                    . 'This will lead to problems with files over 4 GB and is highly discouraged.',
205
+                    [$this->defaults->getProductName()]
206
+                ),
207
+                'hint' => $this->l10n->t('Please remove the open_basedir setting within your php.ini or switch to 64-bit PHP.'),
208
+            ];
209
+        }
210
+
211
+        return [
212
+            'databases' => $databases,
213
+            'directory' => $dataDir,
214
+            'htaccessWorking' => $htAccessWorking,
215
+            'errors' => $errors,
216
+        ];
217
+    }
218
+
219
+    public function createHtaccessTestFile(string $dataDir): string|false {
220
+        // php dev server does not support htaccess
221
+        if (php_sapi_name() === 'cli-server') {
222
+            return false;
223
+        }
224
+
225
+        // testdata
226
+        $fileName = '/htaccesstest.txt';
227
+        $testContent = 'This is used for testing whether htaccess is properly enabled to disallow access from the outside. This file can be safely removed.';
228
+
229
+        // creating a test file
230
+        $testFile = $dataDir . '/' . $fileName;
231
+
232
+        if (file_exists($testFile)) {// already running this test, possible recursive call
233
+            return false;
234
+        }
235
+
236
+        $fp = @fopen($testFile, 'w');
237
+        if (!$fp) {
238
+            throw new \OCP\HintException('Can\'t create test file to check for working .htaccess file.',
239
+                'Make sure it is possible for the web server to write to ' . $testFile);
240
+        }
241
+        fwrite($fp, $testContent);
242
+        fclose($fp);
243
+
244
+        return $testContent;
245
+    }
246
+
247
+    /**
248
+     * Check if the .htaccess file is working
249
+     *
250
+     * @param \OCP\IConfig $config
251
+     * @return bool
252
+     * @throws Exception
253
+     * @throws \OCP\HintException If the test file can't get written.
254
+     */
255
+    public function isHtaccessWorking(string $dataDir) {
256
+        $config = Server::get(IConfig::class);
257
+
258
+        if (\OC::$CLI || !$config->getSystemValueBool('check_for_working_htaccess', true)) {
259
+            return true;
260
+        }
261
+
262
+        $testContent = $this->createHtaccessTestFile($dataDir);
263
+        if ($testContent === false) {
264
+            return false;
265
+        }
266
+
267
+        $fileName = '/htaccesstest.txt';
268
+        $testFile = $dataDir . '/' . $fileName;
269
+
270
+        // accessing the file via http
271
+        $url = Server::get(IURLGenerator::class)->getAbsoluteURL(\OC::$WEBROOT . '/data' . $fileName);
272
+        try {
273
+            $content = Server::get(IClientService::class)->newClient()->get($url)->getBody();
274
+        } catch (\Exception $e) {
275
+            $content = false;
276
+        }
277
+
278
+        if (str_starts_with($url, 'https:')) {
279
+            $url = 'http:' . substr($url, 6);
280
+        } else {
281
+            $url = 'https:' . substr($url, 5);
282
+        }
283
+
284
+        try {
285
+            $fallbackContent = Server::get(IClientService::class)->newClient()->get($url)->getBody();
286
+        } catch (\Exception $e) {
287
+            $fallbackContent = false;
288
+        }
289
+
290
+        // cleanup
291
+        @unlink($testFile);
292
+
293
+        /*
294 294
 		 * If the content is not equal to test content our .htaccess
295 295
 		 * is working as required
296 296
 		 */
297
-		return $content !== $testContent && $fallbackContent !== $testContent;
298
-	}
299
-
300
-	/**
301
-	 * @return array<string|array> errors
302
-	 */
303
-	public function install(array $options, ?IOutput $output = null): array {
304
-		$l = $this->l10n;
305
-
306
-		$error = [];
307
-		$dbType = $options['dbtype'];
308
-
309
-		$disableAdminUser = (bool)($options['admindisable'] ?? false);
310
-
311
-		if (!$disableAdminUser) {
312
-			if (empty($options['adminlogin'])) {
313
-				$error[] = $l->t('Set an admin Login.');
314
-			}
315
-			if (empty($options['adminpass'])) {
316
-				$error[] = $l->t('Set an admin password.');
317
-			}
318
-		}
319
-		if (empty($options['directory'])) {
320
-			$options['directory'] = \OC::$SERVERROOT . '/data';
321
-		}
322
-
323
-		if (!isset(self::$dbSetupClasses[$dbType])) {
324
-			$dbType = 'sqlite';
325
-		}
326
-
327
-		$dataDir = htmlspecialchars_decode($options['directory']);
328
-
329
-		$class = self::$dbSetupClasses[$dbType];
330
-		/** @var \OC\Setup\AbstractDatabase $dbSetup */
331
-		$dbSetup = new $class($l, $this->config, $this->logger, $this->random);
332
-		$error = array_merge($error, $dbSetup->validate($options));
333
-
334
-		// validate the data directory
335
-		if ((!is_dir($dataDir) && !mkdir($dataDir)) || !is_writable($dataDir)) {
336
-			$error[] = $l->t('Cannot create or write into the data directory %s', [$dataDir]);
337
-		}
338
-
339
-		if (!empty($error)) {
340
-			return $error;
341
-		}
342
-
343
-		$request = Server::get(IRequest::class);
344
-
345
-		//no errors, good
346
-		if (isset($options['trusted_domains'])
347
-			&& is_array($options['trusted_domains'])) {
348
-			$trustedDomains = $options['trusted_domains'];
349
-		} else {
350
-			$trustedDomains = [$request->getInsecureServerHost()];
351
-		}
352
-
353
-		//use sqlite3 when available, otherwise sqlite2 will be used.
354
-		if ($dbType === 'sqlite' && class_exists('SQLite3')) {
355
-			$dbType = 'sqlite3';
356
-		}
357
-
358
-		//generate a random salt that is used to salt the local  passwords
359
-		$salt = $this->random->generate(30);
360
-		// generate a secret
361
-		$secret = $this->random->generate(48);
362
-
363
-		//write the config file
364
-		$newConfigValues = [
365
-			'passwordsalt' => $salt,
366
-			'secret' => $secret,
367
-			'trusted_domains' => $trustedDomains,
368
-			'datadirectory' => $dataDir,
369
-			'dbtype' => $dbType,
370
-			'version' => implode('.', \OCP\Util::getVersion()),
371
-		];
372
-
373
-		if ($this->config->getValue('overwrite.cli.url', null) === null) {
374
-			$newConfigValues['overwrite.cli.url'] = $request->getServerProtocol() . '://' . $request->getInsecureServerHost() . \OC::$WEBROOT;
375
-		}
376
-
377
-		$this->config->setValues($newConfigValues);
378
-
379
-		$this->outputDebug($output, 'Configuring database');
380
-		$dbSetup->initialize($options);
381
-		try {
382
-			$dbSetup->setupDatabase();
383
-		} catch (\OC\DatabaseSetupException $e) {
384
-			$error[] = [
385
-				'error' => $e->getMessage(),
386
-				'exception' => $e,
387
-				'hint' => $e->getHint(),
388
-			];
389
-			return $error;
390
-		} catch (Exception $e) {
391
-			$error[] = [
392
-				'error' => 'Error while trying to create admin account: ' . $e->getMessage(),
393
-				'exception' => $e,
394
-				'hint' => '',
395
-			];
396
-			return $error;
397
-		}
398
-
399
-		$this->outputDebug($output, 'Run server migrations');
400
-		try {
401
-			// apply necessary migrations
402
-			$dbSetup->runMigrations($output);
403
-		} catch (Exception $e) {
404
-			$error[] = [
405
-				'error' => 'Error while trying to initialise the database: ' . $e->getMessage(),
406
-				'exception' => $e,
407
-				'hint' => '',
408
-			];
409
-			return $error;
410
-		}
411
-
412
-		$user = null;
413
-		if (!$disableAdminUser) {
414
-			$username = htmlspecialchars_decode($options['adminlogin']);
415
-			$password = htmlspecialchars_decode($options['adminpass']);
416
-			$this->outputDebug($output, 'Create admin account');
417
-
418
-			try {
419
-				$user = Server::get(IUserManager::class)->createUser($username, $password);
420
-				if (!$user) {
421
-					$error[] = "Account <$username> could not be created.";
422
-					return $error;
423
-				}
424
-			} catch (Exception $exception) {
425
-				$error[] = $exception->getMessage();
426
-				return $error;
427
-			}
428
-		}
429
-
430
-		$config = Server::get(IConfig::class);
431
-		$config->setAppValue('core', 'installedat', (string)microtime(true));
432
-		$appConfig = Server::get(IAppConfig::class);
433
-		$appConfig->setValueInt('core', 'lastupdatedat', time());
434
-
435
-		$vendorData = $this->getVendorData();
436
-		$config->setAppValue('core', 'vendor', $vendorData['vendor']);
437
-		if ($vendorData['channel'] !== 'stable') {
438
-			$config->setSystemValue('updater.release.channel', $vendorData['channel']);
439
-		}
440
-
441
-		$group = Server::get(IGroupManager::class)->createGroup('admin');
442
-		if ($user !== null && $group instanceof IGroup) {
443
-			$group->addUser($user);
444
-		}
445
-
446
-		// Install shipped apps and specified app bundles
447
-		$this->outputDebug($output, 'Install default apps');
448
-		$installer = Server::get(Installer::class);
449
-		$installer->installShippedApps(false, $output);
450
-
451
-		// create empty file in data dir, so we can later find
452
-		// out that this is indeed a Nextcloud data directory
453
-		$this->outputDebug($output, 'Setup data directory');
454
-		file_put_contents(
455
-			$config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/.ncdata',
456
-			"# Nextcloud data directory\n# Do not change this file",
457
-		);
458
-
459
-		// Update .htaccess files
460
-		self::updateHtaccess();
461
-		self::protectDataDirectory();
462
-
463
-		$this->outputDebug($output, 'Install background jobs');
464
-		self::installBackgroundJobs();
465
-
466
-		//and we are done
467
-		$config->setSystemValue('installed', true);
468
-		if (self::shouldRemoveCanInstallFile()) {
469
-			unlink(\OC::$configDir . '/CAN_INSTALL');
470
-		}
471
-
472
-		$bootstrapCoordinator = Server::get(\OC\AppFramework\Bootstrap\Coordinator::class);
473
-		$bootstrapCoordinator->runInitialRegistration();
474
-
475
-		if (!$disableAdminUser) {
476
-			// Create a session token for the newly created user
477
-			// The token provider requires a working db, so it's not injected on setup
478
-			/** @var \OC\User\Session $userSession */
479
-			$userSession = Server::get(IUserSession::class);
480
-			$provider = Server::get(PublicKeyTokenProvider::class);
481
-			$userSession->setTokenProvider($provider);
482
-			$userSession->login($username, $password);
483
-			$user = $userSession->getUser();
484
-			if (!$user) {
485
-				$error[] = 'No account found in session.';
486
-				return $error;
487
-			}
488
-			$userSession->createSessionToken($request, $user->getUID(), $username, $password);
489
-
490
-			$session = $userSession->getSession();
491
-			$session->set('last-password-confirm', Server::get(ITimeFactory::class)->getTime());
492
-
493
-			// Set email for admin
494
-			if (!empty($options['adminemail'])) {
495
-				$user->setSystemEMailAddress($options['adminemail']);
496
-			}
497
-		}
498
-
499
-		return $error;
500
-	}
501
-
502
-	public static function installBackgroundJobs(): void {
503
-		$jobList = Server::get(IJobList::class);
504
-		$jobList->add(TokenCleanupJob::class);
505
-		$jobList->add(Rotate::class);
506
-		$jobList->add(BackgroundCleanupJob::class);
507
-		$jobList->add(RemoveOldTasksBackgroundJob::class);
508
-		$jobList->add(CleanupDeletedUsers::class);
509
-		$jobList->add(GenerateMetadataJob::class);
510
-		$jobList->add(MovePreviewJob::class);
511
-	}
512
-
513
-	/**
514
-	 * @return string Absolute path to htaccess
515
-	 */
516
-	private function pathToHtaccess(): string {
517
-		return \OC::$SERVERROOT . '/.htaccess';
518
-	}
519
-
520
-	/**
521
-	 * Find webroot from config
522
-	 *
523
-	 * @throws InvalidArgumentException when invalid value for overwrite.cli.url
524
-	 */
525
-	private static function findWebRoot(SystemConfig $config): string {
526
-		// For CLI read the value from overwrite.cli.url
527
-		if (\OC::$CLI) {
528
-			$webRoot = $config->getValue('overwrite.cli.url', '');
529
-			if ($webRoot === '') {
530
-				throw new InvalidArgumentException('overwrite.cli.url is empty');
531
-			}
532
-			if (!filter_var($webRoot, FILTER_VALIDATE_URL)) {
533
-				throw new InvalidArgumentException('invalid value for overwrite.cli.url');
534
-			}
535
-			$webRoot = rtrim((parse_url($webRoot, PHP_URL_PATH) ?? ''), '/');
536
-		} else {
537
-			$webRoot = !empty(\OC::$WEBROOT) ? \OC::$WEBROOT : '/';
538
-		}
539
-
540
-		return $webRoot;
541
-	}
542
-
543
-	/**
544
-	 * Append the correct ErrorDocument path for Apache hosts
545
-	 *
546
-	 * @return bool True when success, False otherwise
547
-	 * @throws \OCP\AppFramework\QueryException
548
-	 */
549
-	public static function updateHtaccess(): bool {
550
-		$config = Server::get(SystemConfig::class);
551
-
552
-		try {
553
-			$webRoot = self::findWebRoot($config);
554
-		} catch (InvalidArgumentException $e) {
555
-			return false;
556
-		}
557
-
558
-		$setupHelper = Server::get(\OC\Setup::class);
559
-
560
-		if (!is_writable($setupHelper->pathToHtaccess())) {
561
-			return false;
562
-		}
563
-
564
-		$htaccessContent = file_get_contents($setupHelper->pathToHtaccess());
565
-		$content = "#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####\n";
566
-		$htaccessContent = explode($content, $htaccessContent, 2)[0];
567
-
568
-		//custom 403 error page
569
-		$content .= "\nErrorDocument 403 " . $webRoot . '/index.php/error/403';
570
-
571
-		//custom 404 error page
572
-		$content .= "\nErrorDocument 404 " . $webRoot . '/index.php/error/404';
573
-
574
-		// Add rewrite rules if the RewriteBase is configured
575
-		$rewriteBase = $config->getValue('htaccess.RewriteBase', '');
576
-		if ($rewriteBase !== '') {
577
-			$content .= "\n<IfModule mod_rewrite.c>";
578
-			$content .= "\n  Options -MultiViews";
579
-			$content .= "\n  RewriteRule ^core/js/oc.js$ index.php [PT,E=PATH_INFO:$1]";
580
-			$content .= "\n  RewriteRule ^core/preview.png$ index.php [PT,E=PATH_INFO:$1]";
581
-			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !\\.(css|js|mjs|svg|gif|ico|jpg|jpeg|png|webp|html|otf|ttf|woff2?|map|webm|mp4|mp3|ogg|wav|flac|wasm|tflite)$";
582
-			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/core/ajax/update\\.php";
583
-			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/core/img/(favicon\\.ico|manifest\\.json)$";
584
-			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/(cron|public|remote|status)\\.php";
585
-			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/ocs/v(1|2)\\.php";
586
-			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/robots\\.txt";
587
-			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/(ocs-provider|updater)/";
588
-			$content .= "\n  RewriteCond %{REQUEST_URI} !^/\\.well-known/(acme-challenge|pki-validation)/.*";
589
-			$content .= "\n  RewriteCond %{REQUEST_FILENAME} !/richdocumentscode(_arm64)?/proxy.php$";
590
-			$content .= "\n  RewriteRule . index.php [PT,E=PATH_INFO:$1]";
591
-			$content .= "\n  RewriteBase " . $rewriteBase;
592
-			$content .= "\n  <IfModule mod_env.c>";
593
-			$content .= "\n    SetEnv front_controller_active true";
594
-			$content .= "\n    <IfModule mod_dir.c>";
595
-			$content .= "\n      DirectorySlash off";
596
-			$content .= "\n    </IfModule>";
597
-			$content .= "\n  </IfModule>";
598
-			$content .= "\n</IfModule>";
599
-		}
600
-
601
-		// Never write file back if disk space should be too low
602
-		if (function_exists('disk_free_space')) {
603
-			$df = disk_free_space(\OC::$SERVERROOT);
604
-			$size = strlen($content) + 10240;
605
-			if ($df !== false && $df < (float)$size) {
606
-				throw new \Exception(\OC::$SERVERROOT . ' does not have enough space for writing the htaccess file! Not writing it back!');
607
-			}
608
-		}
609
-		//suppress errors in case we don't have permissions for it
610
-		return (bool)@file_put_contents($setupHelper->pathToHtaccess(), $htaccessContent . $content . "\n");
611
-	}
612
-
613
-	public static function protectDataDirectory(): void {
614
-		//Require all denied
615
-		$now = date('Y-m-d H:i:s');
616
-		$content = "# Generated by Nextcloud on $now\n";
617
-		$content .= "# Section for Apache 2.4 to 2.6\n";
618
-		$content .= "<IfModule mod_authz_core.c>\n";
619
-		$content .= "  Require all denied\n";
620
-		$content .= "</IfModule>\n";
621
-		$content .= "<IfModule mod_access_compat.c>\n";
622
-		$content .= "  Order Allow,Deny\n";
623
-		$content .= "  Deny from all\n";
624
-		$content .= "  Satisfy All\n";
625
-		$content .= "</IfModule>\n\n";
626
-		$content .= "# Section for Apache 2.2\n";
627
-		$content .= "<IfModule !mod_authz_core.c>\n";
628
-		$content .= "  <IfModule !mod_access_compat.c>\n";
629
-		$content .= "    <IfModule mod_authz_host.c>\n";
630
-		$content .= "      Order Allow,Deny\n";
631
-		$content .= "      Deny from all\n";
632
-		$content .= "    </IfModule>\n";
633
-		$content .= "    Satisfy All\n";
634
-		$content .= "  </IfModule>\n";
635
-		$content .= "</IfModule>\n\n";
636
-		$content .= "# Section for Apache 2.2 to 2.6\n";
637
-		$content .= "<IfModule mod_autoindex.c>\n";
638
-		$content .= "  IndexIgnore *\n";
639
-		$content .= '</IfModule>';
640
-
641
-		$baseDir = Server::get(IConfig::class)->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data');
642
-		file_put_contents($baseDir . '/.htaccess', $content);
643
-		file_put_contents($baseDir . '/index.html', '');
644
-	}
645
-
646
-	private function getVendorData(): array {
647
-		// this should really be a JSON file
648
-		require \OC::$SERVERROOT . '/version.php';
649
-		/** @var mixed $vendor */
650
-		/** @var mixed $OC_Channel */
651
-		return [
652
-			'vendor' => (string)$vendor,
653
-			'channel' => (string)$OC_Channel,
654
-		];
655
-	}
656
-
657
-	public function shouldRemoveCanInstallFile(): bool {
658
-		return Server::get(ServerVersion::class)->getChannel() !== 'git' && is_file(\OC::$configDir . '/CAN_INSTALL');
659
-	}
660
-
661
-	public function canInstallFileExists(): bool {
662
-		return is_file(\OC::$configDir . '/CAN_INSTALL');
663
-	}
664
-
665
-	protected function outputDebug(?IOutput $output, string $message): void {
666
-		if ($output instanceof IOutput) {
667
-			$output->debug($message);
668
-		}
669
-	}
297
+        return $content !== $testContent && $fallbackContent !== $testContent;
298
+    }
299
+
300
+    /**
301
+     * @return array<string|array> errors
302
+     */
303
+    public function install(array $options, ?IOutput $output = null): array {
304
+        $l = $this->l10n;
305
+
306
+        $error = [];
307
+        $dbType = $options['dbtype'];
308
+
309
+        $disableAdminUser = (bool)($options['admindisable'] ?? false);
310
+
311
+        if (!$disableAdminUser) {
312
+            if (empty($options['adminlogin'])) {
313
+                $error[] = $l->t('Set an admin Login.');
314
+            }
315
+            if (empty($options['adminpass'])) {
316
+                $error[] = $l->t('Set an admin password.');
317
+            }
318
+        }
319
+        if (empty($options['directory'])) {
320
+            $options['directory'] = \OC::$SERVERROOT . '/data';
321
+        }
322
+
323
+        if (!isset(self::$dbSetupClasses[$dbType])) {
324
+            $dbType = 'sqlite';
325
+        }
326
+
327
+        $dataDir = htmlspecialchars_decode($options['directory']);
328
+
329
+        $class = self::$dbSetupClasses[$dbType];
330
+        /** @var \OC\Setup\AbstractDatabase $dbSetup */
331
+        $dbSetup = new $class($l, $this->config, $this->logger, $this->random);
332
+        $error = array_merge($error, $dbSetup->validate($options));
333
+
334
+        // validate the data directory
335
+        if ((!is_dir($dataDir) && !mkdir($dataDir)) || !is_writable($dataDir)) {
336
+            $error[] = $l->t('Cannot create or write into the data directory %s', [$dataDir]);
337
+        }
338
+
339
+        if (!empty($error)) {
340
+            return $error;
341
+        }
342
+
343
+        $request = Server::get(IRequest::class);
344
+
345
+        //no errors, good
346
+        if (isset($options['trusted_domains'])
347
+            && is_array($options['trusted_domains'])) {
348
+            $trustedDomains = $options['trusted_domains'];
349
+        } else {
350
+            $trustedDomains = [$request->getInsecureServerHost()];
351
+        }
352
+
353
+        //use sqlite3 when available, otherwise sqlite2 will be used.
354
+        if ($dbType === 'sqlite' && class_exists('SQLite3')) {
355
+            $dbType = 'sqlite3';
356
+        }
357
+
358
+        //generate a random salt that is used to salt the local  passwords
359
+        $salt = $this->random->generate(30);
360
+        // generate a secret
361
+        $secret = $this->random->generate(48);
362
+
363
+        //write the config file
364
+        $newConfigValues = [
365
+            'passwordsalt' => $salt,
366
+            'secret' => $secret,
367
+            'trusted_domains' => $trustedDomains,
368
+            'datadirectory' => $dataDir,
369
+            'dbtype' => $dbType,
370
+            'version' => implode('.', \OCP\Util::getVersion()),
371
+        ];
372
+
373
+        if ($this->config->getValue('overwrite.cli.url', null) === null) {
374
+            $newConfigValues['overwrite.cli.url'] = $request->getServerProtocol() . '://' . $request->getInsecureServerHost() . \OC::$WEBROOT;
375
+        }
376
+
377
+        $this->config->setValues($newConfigValues);
378
+
379
+        $this->outputDebug($output, 'Configuring database');
380
+        $dbSetup->initialize($options);
381
+        try {
382
+            $dbSetup->setupDatabase();
383
+        } catch (\OC\DatabaseSetupException $e) {
384
+            $error[] = [
385
+                'error' => $e->getMessage(),
386
+                'exception' => $e,
387
+                'hint' => $e->getHint(),
388
+            ];
389
+            return $error;
390
+        } catch (Exception $e) {
391
+            $error[] = [
392
+                'error' => 'Error while trying to create admin account: ' . $e->getMessage(),
393
+                'exception' => $e,
394
+                'hint' => '',
395
+            ];
396
+            return $error;
397
+        }
398
+
399
+        $this->outputDebug($output, 'Run server migrations');
400
+        try {
401
+            // apply necessary migrations
402
+            $dbSetup->runMigrations($output);
403
+        } catch (Exception $e) {
404
+            $error[] = [
405
+                'error' => 'Error while trying to initialise the database: ' . $e->getMessage(),
406
+                'exception' => $e,
407
+                'hint' => '',
408
+            ];
409
+            return $error;
410
+        }
411
+
412
+        $user = null;
413
+        if (!$disableAdminUser) {
414
+            $username = htmlspecialchars_decode($options['adminlogin']);
415
+            $password = htmlspecialchars_decode($options['adminpass']);
416
+            $this->outputDebug($output, 'Create admin account');
417
+
418
+            try {
419
+                $user = Server::get(IUserManager::class)->createUser($username, $password);
420
+                if (!$user) {
421
+                    $error[] = "Account <$username> could not be created.";
422
+                    return $error;
423
+                }
424
+            } catch (Exception $exception) {
425
+                $error[] = $exception->getMessage();
426
+                return $error;
427
+            }
428
+        }
429
+
430
+        $config = Server::get(IConfig::class);
431
+        $config->setAppValue('core', 'installedat', (string)microtime(true));
432
+        $appConfig = Server::get(IAppConfig::class);
433
+        $appConfig->setValueInt('core', 'lastupdatedat', time());
434
+
435
+        $vendorData = $this->getVendorData();
436
+        $config->setAppValue('core', 'vendor', $vendorData['vendor']);
437
+        if ($vendorData['channel'] !== 'stable') {
438
+            $config->setSystemValue('updater.release.channel', $vendorData['channel']);
439
+        }
440
+
441
+        $group = Server::get(IGroupManager::class)->createGroup('admin');
442
+        if ($user !== null && $group instanceof IGroup) {
443
+            $group->addUser($user);
444
+        }
445
+
446
+        // Install shipped apps and specified app bundles
447
+        $this->outputDebug($output, 'Install default apps');
448
+        $installer = Server::get(Installer::class);
449
+        $installer->installShippedApps(false, $output);
450
+
451
+        // create empty file in data dir, so we can later find
452
+        // out that this is indeed a Nextcloud data directory
453
+        $this->outputDebug($output, 'Setup data directory');
454
+        file_put_contents(
455
+            $config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/.ncdata',
456
+            "# Nextcloud data directory\n# Do not change this file",
457
+        );
458
+
459
+        // Update .htaccess files
460
+        self::updateHtaccess();
461
+        self::protectDataDirectory();
462
+
463
+        $this->outputDebug($output, 'Install background jobs');
464
+        self::installBackgroundJobs();
465
+
466
+        //and we are done
467
+        $config->setSystemValue('installed', true);
468
+        if (self::shouldRemoveCanInstallFile()) {
469
+            unlink(\OC::$configDir . '/CAN_INSTALL');
470
+        }
471
+
472
+        $bootstrapCoordinator = Server::get(\OC\AppFramework\Bootstrap\Coordinator::class);
473
+        $bootstrapCoordinator->runInitialRegistration();
474
+
475
+        if (!$disableAdminUser) {
476
+            // Create a session token for the newly created user
477
+            // The token provider requires a working db, so it's not injected on setup
478
+            /** @var \OC\User\Session $userSession */
479
+            $userSession = Server::get(IUserSession::class);
480
+            $provider = Server::get(PublicKeyTokenProvider::class);
481
+            $userSession->setTokenProvider($provider);
482
+            $userSession->login($username, $password);
483
+            $user = $userSession->getUser();
484
+            if (!$user) {
485
+                $error[] = 'No account found in session.';
486
+                return $error;
487
+            }
488
+            $userSession->createSessionToken($request, $user->getUID(), $username, $password);
489
+
490
+            $session = $userSession->getSession();
491
+            $session->set('last-password-confirm', Server::get(ITimeFactory::class)->getTime());
492
+
493
+            // Set email for admin
494
+            if (!empty($options['adminemail'])) {
495
+                $user->setSystemEMailAddress($options['adminemail']);
496
+            }
497
+        }
498
+
499
+        return $error;
500
+    }
501
+
502
+    public static function installBackgroundJobs(): void {
503
+        $jobList = Server::get(IJobList::class);
504
+        $jobList->add(TokenCleanupJob::class);
505
+        $jobList->add(Rotate::class);
506
+        $jobList->add(BackgroundCleanupJob::class);
507
+        $jobList->add(RemoveOldTasksBackgroundJob::class);
508
+        $jobList->add(CleanupDeletedUsers::class);
509
+        $jobList->add(GenerateMetadataJob::class);
510
+        $jobList->add(MovePreviewJob::class);
511
+    }
512
+
513
+    /**
514
+     * @return string Absolute path to htaccess
515
+     */
516
+    private function pathToHtaccess(): string {
517
+        return \OC::$SERVERROOT . '/.htaccess';
518
+    }
519
+
520
+    /**
521
+     * Find webroot from config
522
+     *
523
+     * @throws InvalidArgumentException when invalid value for overwrite.cli.url
524
+     */
525
+    private static function findWebRoot(SystemConfig $config): string {
526
+        // For CLI read the value from overwrite.cli.url
527
+        if (\OC::$CLI) {
528
+            $webRoot = $config->getValue('overwrite.cli.url', '');
529
+            if ($webRoot === '') {
530
+                throw new InvalidArgumentException('overwrite.cli.url is empty');
531
+            }
532
+            if (!filter_var($webRoot, FILTER_VALIDATE_URL)) {
533
+                throw new InvalidArgumentException('invalid value for overwrite.cli.url');
534
+            }
535
+            $webRoot = rtrim((parse_url($webRoot, PHP_URL_PATH) ?? ''), '/');
536
+        } else {
537
+            $webRoot = !empty(\OC::$WEBROOT) ? \OC::$WEBROOT : '/';
538
+        }
539
+
540
+        return $webRoot;
541
+    }
542
+
543
+    /**
544
+     * Append the correct ErrorDocument path for Apache hosts
545
+     *
546
+     * @return bool True when success, False otherwise
547
+     * @throws \OCP\AppFramework\QueryException
548
+     */
549
+    public static function updateHtaccess(): bool {
550
+        $config = Server::get(SystemConfig::class);
551
+
552
+        try {
553
+            $webRoot = self::findWebRoot($config);
554
+        } catch (InvalidArgumentException $e) {
555
+            return false;
556
+        }
557
+
558
+        $setupHelper = Server::get(\OC\Setup::class);
559
+
560
+        if (!is_writable($setupHelper->pathToHtaccess())) {
561
+            return false;
562
+        }
563
+
564
+        $htaccessContent = file_get_contents($setupHelper->pathToHtaccess());
565
+        $content = "#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####\n";
566
+        $htaccessContent = explode($content, $htaccessContent, 2)[0];
567
+
568
+        //custom 403 error page
569
+        $content .= "\nErrorDocument 403 " . $webRoot . '/index.php/error/403';
570
+
571
+        //custom 404 error page
572
+        $content .= "\nErrorDocument 404 " . $webRoot . '/index.php/error/404';
573
+
574
+        // Add rewrite rules if the RewriteBase is configured
575
+        $rewriteBase = $config->getValue('htaccess.RewriteBase', '');
576
+        if ($rewriteBase !== '') {
577
+            $content .= "\n<IfModule mod_rewrite.c>";
578
+            $content .= "\n  Options -MultiViews";
579
+            $content .= "\n  RewriteRule ^core/js/oc.js$ index.php [PT,E=PATH_INFO:$1]";
580
+            $content .= "\n  RewriteRule ^core/preview.png$ index.php [PT,E=PATH_INFO:$1]";
581
+            $content .= "\n  RewriteCond %{REQUEST_FILENAME} !\\.(css|js|mjs|svg|gif|ico|jpg|jpeg|png|webp|html|otf|ttf|woff2?|map|webm|mp4|mp3|ogg|wav|flac|wasm|tflite)$";
582
+            $content .= "\n  RewriteCond %{REQUEST_FILENAME} !/core/ajax/update\\.php";
583
+            $content .= "\n  RewriteCond %{REQUEST_FILENAME} !/core/img/(favicon\\.ico|manifest\\.json)$";
584
+            $content .= "\n  RewriteCond %{REQUEST_FILENAME} !/(cron|public|remote|status)\\.php";
585
+            $content .= "\n  RewriteCond %{REQUEST_FILENAME} !/ocs/v(1|2)\\.php";
586
+            $content .= "\n  RewriteCond %{REQUEST_FILENAME} !/robots\\.txt";
587
+            $content .= "\n  RewriteCond %{REQUEST_FILENAME} !/(ocs-provider|updater)/";
588
+            $content .= "\n  RewriteCond %{REQUEST_URI} !^/\\.well-known/(acme-challenge|pki-validation)/.*";
589
+            $content .= "\n  RewriteCond %{REQUEST_FILENAME} !/richdocumentscode(_arm64)?/proxy.php$";
590
+            $content .= "\n  RewriteRule . index.php [PT,E=PATH_INFO:$1]";
591
+            $content .= "\n  RewriteBase " . $rewriteBase;
592
+            $content .= "\n  <IfModule mod_env.c>";
593
+            $content .= "\n    SetEnv front_controller_active true";
594
+            $content .= "\n    <IfModule mod_dir.c>";
595
+            $content .= "\n      DirectorySlash off";
596
+            $content .= "\n    </IfModule>";
597
+            $content .= "\n  </IfModule>";
598
+            $content .= "\n</IfModule>";
599
+        }
600
+
601
+        // Never write file back if disk space should be too low
602
+        if (function_exists('disk_free_space')) {
603
+            $df = disk_free_space(\OC::$SERVERROOT);
604
+            $size = strlen($content) + 10240;
605
+            if ($df !== false && $df < (float)$size) {
606
+                throw new \Exception(\OC::$SERVERROOT . ' does not have enough space for writing the htaccess file! Not writing it back!');
607
+            }
608
+        }
609
+        //suppress errors in case we don't have permissions for it
610
+        return (bool)@file_put_contents($setupHelper->pathToHtaccess(), $htaccessContent . $content . "\n");
611
+    }
612
+
613
+    public static function protectDataDirectory(): void {
614
+        //Require all denied
615
+        $now = date('Y-m-d H:i:s');
616
+        $content = "# Generated by Nextcloud on $now\n";
617
+        $content .= "# Section for Apache 2.4 to 2.6\n";
618
+        $content .= "<IfModule mod_authz_core.c>\n";
619
+        $content .= "  Require all denied\n";
620
+        $content .= "</IfModule>\n";
621
+        $content .= "<IfModule mod_access_compat.c>\n";
622
+        $content .= "  Order Allow,Deny\n";
623
+        $content .= "  Deny from all\n";
624
+        $content .= "  Satisfy All\n";
625
+        $content .= "</IfModule>\n\n";
626
+        $content .= "# Section for Apache 2.2\n";
627
+        $content .= "<IfModule !mod_authz_core.c>\n";
628
+        $content .= "  <IfModule !mod_access_compat.c>\n";
629
+        $content .= "    <IfModule mod_authz_host.c>\n";
630
+        $content .= "      Order Allow,Deny\n";
631
+        $content .= "      Deny from all\n";
632
+        $content .= "    </IfModule>\n";
633
+        $content .= "    Satisfy All\n";
634
+        $content .= "  </IfModule>\n";
635
+        $content .= "</IfModule>\n\n";
636
+        $content .= "# Section for Apache 2.2 to 2.6\n";
637
+        $content .= "<IfModule mod_autoindex.c>\n";
638
+        $content .= "  IndexIgnore *\n";
639
+        $content .= '</IfModule>';
640
+
641
+        $baseDir = Server::get(IConfig::class)->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data');
642
+        file_put_contents($baseDir . '/.htaccess', $content);
643
+        file_put_contents($baseDir . '/index.html', '');
644
+    }
645
+
646
+    private function getVendorData(): array {
647
+        // this should really be a JSON file
648
+        require \OC::$SERVERROOT . '/version.php';
649
+        /** @var mixed $vendor */
650
+        /** @var mixed $OC_Channel */
651
+        return [
652
+            'vendor' => (string)$vendor,
653
+            'channel' => (string)$OC_Channel,
654
+        ];
655
+    }
656
+
657
+    public function shouldRemoveCanInstallFile(): bool {
658
+        return Server::get(ServerVersion::class)->getChannel() !== 'git' && is_file(\OC::$configDir . '/CAN_INSTALL');
659
+    }
660
+
661
+    public function canInstallFileExists(): bool {
662
+        return is_file(\OC::$configDir . '/CAN_INSTALL');
663
+    }
664
+
665
+    protected function outputDebug(?IOutput $output, string $message): void {
666
+        if ($output instanceof IOutput) {
667
+            $output->debug($message);
668
+        }
669
+    }
670 670
 }
Please login to merge, or discard this patch.
tests/lib/Files/ViewTest.php 2 patches
Indentation   +2775 added lines, -2775 removed lines patch added patch discarded remove patch
@@ -51,40 +51,40 @@  discard block
 block discarded – undo
51 51
 use Test\Traits\UserTrait;
52 52
 
53 53
 class TemporaryNoTouch extends Temporary {
54
-	public function touch(string $path, ?int $mtime = null): bool {
55
-		return false;
56
-	}
54
+    public function touch(string $path, ?int $mtime = null): bool {
55
+        return false;
56
+    }
57 57
 }
58 58
 
59 59
 class TemporaryNoCross extends Temporary {
60
-	public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): bool {
61
-		return Common::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime);
62
-	}
60
+    public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): bool {
61
+        return Common::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime);
62
+    }
63 63
 
64
-	public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
65
-		return Common::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
66
-	}
64
+    public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
65
+        return Common::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
66
+    }
67 67
 }
68 68
 
69 69
 class TemporaryNoLocal extends Temporary {
70
-	public function instanceOfStorage(string $class): bool {
71
-		if ($class === '\OC\Files\Storage\Local') {
72
-			return false;
73
-		} else {
74
-			return parent::instanceOfStorage($class);
75
-		}
76
-	}
70
+    public function instanceOfStorage(string $class): bool {
71
+        if ($class === '\OC\Files\Storage\Local') {
72
+            return false;
73
+        } else {
74
+            return parent::instanceOfStorage($class);
75
+        }
76
+    }
77 77
 }
78 78
 
79 79
 class TestEventHandler {
80
-	public function umount() {
81
-	}
82
-	public function post_umount() {
83
-	}
84
-	public function preCallback() {
85
-	}
86
-	public function postCallback() {
87
-	}
80
+    public function umount() {
81
+    }
82
+    public function post_umount() {
83
+    }
84
+    public function preCallback() {
85
+    }
86
+    public function postCallback() {
87
+    }
88 88
 }
89 89
 
90 90
 /**
@@ -96,2757 +96,2757 @@  discard block
 block discarded – undo
96 96
 #[\PHPUnit\Framework\Attributes\Medium]
97 97
 #[\PHPUnit\Framework\Attributes\Group('DB')]
98 98
 class ViewTest extends \Test\TestCase {
99
-	use UserTrait;
100
-
101
-	/**
102
-	 * @var Storage[] $storages
103
-	 */
104
-	private $storages = [];
105
-
106
-	/**
107
-	 * @var string
108
-	 */
109
-	private $user;
110
-
111
-	/**
112
-	 * @var IUser
113
-	 */
114
-	private $userObject;
115
-
116
-	/**
117
-	 * @var IGroup
118
-	 */
119
-	private $groupObject;
120
-
121
-	/** @var Storage */
122
-	private $tempStorage;
123
-
124
-	protected function setUp(): void {
125
-		parent::setUp();
126
-		\OC_Hook::clear();
127
-
128
-		Server::get(IUserManager::class)->clearBackends();
129
-		Server::get(IUserManager::class)->registerBackend(new \Test\Util\User\Dummy());
130
-
131
-		//login
132
-		$userManager = Server::get(IUserManager::class);
133
-		$groupManager = Server::get(IGroupManager::class);
134
-		$this->user = 'test';
135
-		$this->userObject = $userManager->createUser('test', 'test');
136
-
137
-		$this->groupObject = $groupManager->createGroup('group1');
138
-		$this->groupObject->addUser($this->userObject);
139
-
140
-		self::loginAsUser($this->user);
141
-
142
-		/** @var IMountManager $manager */
143
-		$manager = Server::get(IMountManager::class);
144
-		$manager->removeMount('/test');
145
-
146
-		$this->tempStorage = null;
147
-	}
148
-
149
-	protected function tearDown(): void {
150
-		\OC_User::setUserId($this->user);
151
-		foreach ($this->storages as $storage) {
152
-			$cache = $storage->getCache();
153
-			$ids = $cache->getAll();
154
-			$cache->clear();
155
-		}
156
-
157
-		if ($this->tempStorage) {
158
-			system('rm -rf ' . escapeshellarg($this->tempStorage->getDataDir()));
159
-		}
160
-
161
-		self::logout();
162
-
163
-		/** @var SetupManager $setupManager */
164
-		$setupManager = Server::get(SetupManager::class);
165
-		$setupManager->setupRoot();
166
-
167
-		$this->userObject->delete();
168
-		$this->groupObject->delete();
169
-
170
-		$mountProviderCollection = Server::get(IMountProviderCollection::class);
171
-		self::invokePrivate($mountProviderCollection, 'providers', [[]]);
172
-
173
-		parent::tearDown();
174
-	}
175
-
176
-	public function testCacheAPI(): void {
177
-		$storage1 = $this->getTestStorage();
178
-		$storage2 = $this->getTestStorage();
179
-		$storage3 = $this->getTestStorage();
180
-		$root = self::getUniqueID('/');
181
-		Filesystem::mount($storage1, [], $root . '/');
182
-		Filesystem::mount($storage2, [], $root . '/substorage');
183
-		Filesystem::mount($storage3, [], $root . '/folder/anotherstorage');
184
-		$textSize = strlen("dummy file data\n");
185
-		$imageSize = filesize(\OC::$SERVERROOT . '/core/img/logo/logo.png');
186
-		$storageSize = $textSize * 2 + $imageSize;
187
-
188
-		$storageInfo = $storage3->getCache()->get('');
189
-		$this->assertEquals($storageSize, $storageInfo['size']);
190
-
191
-		$rootView = new View($root);
192
-
193
-		$cachedData = $rootView->getFileInfo('/foo.txt');
194
-		$this->assertEquals($textSize, $cachedData['size']);
195
-		$this->assertEquals('text/plain', $cachedData['mimetype']);
196
-		$this->assertNotEquals(-1, $cachedData['permissions']);
197
-
198
-		$cachedData = $rootView->getFileInfo('/');
199
-		$this->assertEquals($storageSize * 3, $cachedData['size']);
200
-		$this->assertEquals('httpd/unix-directory', $cachedData['mimetype']);
201
-
202
-		// get cached data excluding mount points
203
-		$cachedData = $rootView->getFileInfo('/', false);
204
-		$this->assertEquals($storageSize, $cachedData['size']);
205
-		$this->assertEquals('httpd/unix-directory', $cachedData['mimetype']);
206
-
207
-		$cachedData = $rootView->getFileInfo('/folder');
208
-		$this->assertEquals($storageSize + $textSize, $cachedData['size']);
209
-		$this->assertEquals('httpd/unix-directory', $cachedData['mimetype']);
210
-
211
-		$folderData = $rootView->getDirectoryContent('/');
212
-		/**
213
-		 * expected entries:
214
-		 * folder
215
-		 * foo.png
216
-		 * foo.txt
217
-		 * substorage
218
-		 */
219
-		$this->assertCount(4, $folderData);
220
-		$this->assertEquals('folder', $folderData[0]['name']);
221
-		$this->assertEquals('foo.png', $folderData[1]['name']);
222
-		$this->assertEquals('foo.txt', $folderData[2]['name']);
223
-		$this->assertEquals('substorage', $folderData[3]['name']);
224
-
225
-		$this->assertEquals($storageSize + $textSize, $folderData[0]['size']);
226
-		$this->assertEquals($imageSize, $folderData[1]['size']);
227
-		$this->assertEquals($textSize, $folderData[2]['size']);
228
-		$this->assertEquals($storageSize, $folderData[3]['size']);
229
-
230
-		$folderData = $rootView->getDirectoryContent('/substorage');
231
-		/**
232
-		 * expected entries:
233
-		 * folder
234
-		 * foo.png
235
-		 * foo.txt
236
-		 */
237
-		$this->assertCount(3, $folderData);
238
-		$this->assertEquals('folder', $folderData[0]['name']);
239
-		$this->assertEquals('foo.png', $folderData[1]['name']);
240
-		$this->assertEquals('foo.txt', $folderData[2]['name']);
241
-
242
-		$folderView = new View($root . '/folder');
243
-		$this->assertEquals($rootView->getFileInfo('/folder'), $folderView->getFileInfo('/'));
244
-
245
-		$cachedData = $rootView->getFileInfo('/foo.txt');
246
-		$this->assertFalse($cachedData['encrypted']);
247
-		$id = $rootView->putFileInfo('/foo.txt', ['encrypted' => true]);
248
-		$cachedData = $rootView->getFileInfo('/foo.txt');
249
-		$this->assertTrue($cachedData['encrypted']);
250
-		$this->assertEquals($cachedData['fileid'], $id);
251
-
252
-		$this->assertFalse($rootView->getFileInfo('/non/existing'));
253
-		$this->assertEquals([], $rootView->getDirectoryContent('/non/existing'));
254
-	}
255
-
256
-	public function testGetPath(): void {
257
-		$user = $this->createMock(IUser::class);
258
-		$user->method('getUID')
259
-			->willReturn('test');
260
-		$storage1 = $this->getTestStorage();
261
-		$storage2 = $this->getTestStorage();
262
-		$storage3 = $this->getTestStorage();
263
-
264
-		Filesystem::mount($storage1, [], '/test/files');
265
-		Filesystem::mount($storage2, [], '/test/files/substorage');
266
-		Filesystem::mount($storage3, [], '/test/files/folder/anotherstorage');
267
-
268
-		$userMountCache = Server::get(IUserMountCache::class);
269
-		$userMountCache->registerMounts($user, [
270
-			new MountPoint($storage1, '/test/files'),
271
-			new MountPoint($storage2, '/test/files/substorage'),
272
-			new MountPoint($storage3, '/test/files/folder/anotherstorage'),
273
-		]);
274
-
275
-		$rootView = new View('/test/files');
276
-
277
-
278
-		$cachedData = $rootView->getFileInfo('/foo.txt');
279
-		$id1 = $cachedData->getId();
280
-		$this->assertEquals('/foo.txt', $rootView->getPath($id1));
281
-
282
-		$cachedData = $rootView->getFileInfo('/substorage/foo.txt');
283
-		$id2 = $cachedData->getId();
284
-		$this->assertEquals('/substorage/foo.txt', $rootView->getPath($id2));
285
-
286
-		$folderView = new View('/test/files/substorage');
287
-		$this->assertEquals('/foo.txt', $folderView->getPath($id2));
288
-	}
289
-
290
-
291
-	public function testGetPathNotExisting(): void {
292
-		$this->expectException(NotFoundException::class);
293
-
294
-		$storage1 = $this->getTestStorage();
295
-		Filesystem::mount($storage1, [], '/');
296
-
297
-		$rootView = new View('');
298
-		$cachedData = $rootView->getFileInfo('/foo.txt');
299
-		/** @var int $id1 */
300
-		$id1 = $cachedData['fileid'];
301
-		$folderView = new View('/substorage');
302
-		$this->assertNull($folderView->getPath($id1));
303
-	}
304
-
305
-	public function testMountPointOverwrite(): void {
306
-		$storage1 = $this->getTestStorage(false);
307
-		$storage2 = $this->getTestStorage();
308
-		$storage1->mkdir('substorage');
309
-		Filesystem::mount($storage1, [], '/');
310
-		Filesystem::mount($storage2, [], '/substorage');
311
-
312
-		$rootView = new View('');
313
-		$folderContent = $rootView->getDirectoryContent('/');
314
-		$this->assertCount(4, $folderContent);
315
-	}
316
-
317
-	public static function sharingDisabledPermissionProvider(): array {
318
-		return [
319
-			['no', '', true],
320
-			['yes', 'group1', false],
321
-		];
322
-	}
323
-
324
-	#[\PHPUnit\Framework\Attributes\DataProvider('sharingDisabledPermissionProvider')]
325
-	public function testRemoveSharePermissionWhenSharingDisabledForUser($excludeGroups, $excludeGroupsList, $expectedShareable): void {
326
-		// Reset sharing disabled for users cache
327
-		self::invokePrivate(Server::get(ShareDisableChecker::class), 'sharingDisabledForUsersCache', [new CappedMemoryCache()]);
328
-
329
-		$config = Server::get(IConfig::class);
330
-		$oldExcludeGroupsFlag = $config->getAppValue('core', 'shareapi_exclude_groups', 'no');
331
-		$oldExcludeGroupsList = $config->getAppValue('core', 'shareapi_exclude_groups_list', '');
332
-		$config->setAppValue('core', 'shareapi_exclude_groups', $excludeGroups);
333
-		$config->setAppValue('core', 'shareapi_exclude_groups_list', $excludeGroupsList);
334
-
335
-		$storage1 = $this->getTestStorage();
336
-		$storage2 = $this->getTestStorage();
337
-		Filesystem::mount($storage1, [], '/');
338
-		Filesystem::mount($storage2, [], '/mount');
339
-
340
-		$view = new View('/');
341
-
342
-		$folderContent = $view->getDirectoryContent('');
343
-		$this->assertEquals($expectedShareable, $folderContent[0]->isShareable());
344
-
345
-		$folderContent = $view->getDirectoryContent('mount');
346
-		$this->assertEquals($expectedShareable, $folderContent[0]->isShareable());
347
-
348
-		$config->setAppValue('core', 'shareapi_exclude_groups', $oldExcludeGroupsFlag);
349
-		$config->setAppValue('core', 'shareapi_exclude_groups_list', $oldExcludeGroupsList);
350
-
351
-		// Reset sharing disabled for users cache
352
-		self::invokePrivate(Server::get(ShareDisableChecker::class), 'sharingDisabledForUsersCache', [new CappedMemoryCache()]);
353
-	}
354
-
355
-	public function testCacheIncompleteFolder(): void {
356
-		$storage1 = $this->getTestStorage(false);
357
-		Filesystem::mount($storage1, [], '/incomplete');
358
-		$rootView = new View('/incomplete');
359
-
360
-		$entries = $rootView->getDirectoryContent('/');
361
-		$this->assertCount(3, $entries);
362
-
363
-		// /folder will already be in the cache but not scanned
364
-		$entries = $rootView->getDirectoryContent('/folder');
365
-		$this->assertCount(1, $entries);
366
-	}
367
-
368
-	public function testAutoScan(): void {
369
-		$storage1 = $this->getTestStorage(false);
370
-		$storage2 = $this->getTestStorage(false);
371
-		Filesystem::mount($storage1, [], '/');
372
-		Filesystem::mount($storage2, [], '/substorage');
373
-		$textSize = strlen("dummy file data\n");
374
-
375
-		$rootView = new View('');
376
-
377
-		$cachedData = $rootView->getFileInfo('/');
378
-		$this->assertEquals('httpd/unix-directory', $cachedData['mimetype']);
379
-		$this->assertEquals(-1, $cachedData['size']);
380
-
381
-		$folderData = $rootView->getDirectoryContent('/substorage/folder');
382
-		$this->assertEquals('text/plain', $folderData[0]['mimetype']);
383
-		$this->assertEquals($textSize, $folderData[0]['size']);
384
-	}
385
-
386
-	public function testSearch(): void {
387
-		$storage1 = $this->getTestStorage();
388
-		$storage2 = $this->getTestStorage();
389
-		$storage3 = $this->getTestStorage();
390
-		Filesystem::mount($storage1, [], '/');
391
-		Filesystem::mount($storage2, [], '/substorage');
392
-		Filesystem::mount($storage3, [], '/folder/anotherstorage');
393
-
394
-		$rootView = new View('');
395
-
396
-		$results = $rootView->search('foo');
397
-		$this->assertCount(6, $results);
398
-		$paths = [];
399
-		foreach ($results as $result) {
400
-			$this->assertEquals($result['path'], Filesystem::normalizePath($result['path']));
401
-			$paths[] = $result['path'];
402
-		}
403
-		$this->assertContains('/foo.txt', $paths);
404
-		$this->assertContains('/foo.png', $paths);
405
-		$this->assertContains('/substorage/foo.txt', $paths);
406
-		$this->assertContains('/substorage/foo.png', $paths);
407
-		$this->assertContains('/folder/anotherstorage/foo.txt', $paths);
408
-		$this->assertContains('/folder/anotherstorage/foo.png', $paths);
409
-
410
-		$folderView = new View('/folder');
411
-		$results = $folderView->search('bar');
412
-		$this->assertCount(2, $results);
413
-		$paths = [];
414
-		foreach ($results as $result) {
415
-			$paths[] = $result['path'];
416
-		}
417
-		$this->assertContains('/anotherstorage/folder/bar.txt', $paths);
418
-		$this->assertContains('/bar.txt', $paths);
419
-
420
-		$results = $folderView->search('foo');
421
-		$this->assertCount(2, $results);
422
-		$paths = [];
423
-		foreach ($results as $result) {
424
-			$paths[] = $result['path'];
425
-		}
426
-		$this->assertContains('/anotherstorage/foo.txt', $paths);
427
-		$this->assertContains('/anotherstorage/foo.png', $paths);
428
-
429
-		$this->assertCount(6, $rootView->searchByMime('text'));
430
-		$this->assertCount(3, $folderView->searchByMime('text'));
431
-	}
432
-
433
-	public function testWatcher(): void {
434
-		$storage1 = $this->getTestStorage();
435
-		Filesystem::mount($storage1, [], '/');
436
-		$storage1->getWatcher()->setPolicy(Watcher::CHECK_ALWAYS);
437
-
438
-		$rootView = new View('');
439
-
440
-		$cachedData = $rootView->getFileInfo('foo.txt');
441
-		$this->assertEquals(16, $cachedData['size']);
442
-
443
-		$rootView->putFileInfo('foo.txt', ['storage_mtime' => 10]);
444
-		$storage1->file_put_contents('foo.txt', 'foo');
445
-		clearstatcache();
446
-
447
-		$cachedData = $rootView->getFileInfo('foo.txt');
448
-		$this->assertEquals(3, $cachedData['size']);
449
-	}
450
-
451
-	public function testCopyBetweenStorageNoCross(): void {
452
-		$storage1 = $this->getTestStorage(true, TemporaryNoCross::class);
453
-		$storage2 = $this->getTestStorage(true, TemporaryNoCross::class);
454
-		$this->copyBetweenStorages($storage1, $storage2);
455
-	}
456
-
457
-	public function testCopyBetweenStorageCross(): void {
458
-		$storage1 = $this->getTestStorage();
459
-		$storage2 = $this->getTestStorage();
460
-		$this->copyBetweenStorages($storage1, $storage2);
461
-	}
462
-
463
-	public function testCopyBetweenStorageCrossNonLocal(): void {
464
-		$storage1 = $this->getTestStorage(true, TemporaryNoLocal::class);
465
-		$storage2 = $this->getTestStorage(true, TemporaryNoLocal::class);
466
-		$this->copyBetweenStorages($storage1, $storage2);
467
-	}
468
-
469
-	public function copyBetweenStorages($storage1, $storage2) {
470
-		Filesystem::mount($storage1, [], '/');
471
-		Filesystem::mount($storage2, [], '/substorage');
472
-
473
-		$rootView = new View('');
474
-		$rootView->mkdir('substorage/emptyfolder');
475
-		$rootView->copy('substorage', 'anotherfolder');
476
-		$this->assertTrue($rootView->is_dir('/anotherfolder'));
477
-		$this->assertTrue($rootView->is_dir('/substorage'));
478
-		$this->assertTrue($rootView->is_dir('/anotherfolder/emptyfolder'));
479
-		$this->assertTrue($rootView->is_dir('/substorage/emptyfolder'));
480
-		$this->assertTrue($rootView->file_exists('/anotherfolder/foo.txt'));
481
-		$this->assertTrue($rootView->file_exists('/anotherfolder/foo.png'));
482
-		$this->assertTrue($rootView->file_exists('/anotherfolder/folder/bar.txt'));
483
-		$this->assertTrue($rootView->file_exists('/substorage/foo.txt'));
484
-		$this->assertTrue($rootView->file_exists('/substorage/foo.png'));
485
-		$this->assertTrue($rootView->file_exists('/substorage/folder/bar.txt'));
486
-	}
487
-
488
-	public function testMoveBetweenStorageNoCross(): void {
489
-		$storage1 = $this->getTestStorage(true, TemporaryNoCross::class);
490
-		$storage2 = $this->getTestStorage(true, TemporaryNoCross::class);
491
-		$this->moveBetweenStorages($storage1, $storage2);
492
-	}
493
-
494
-	public function testMoveBetweenStorageCross(): void {
495
-		$storage1 = $this->getTestStorage();
496
-		$storage2 = $this->getTestStorage();
497
-		$this->moveBetweenStorages($storage1, $storage2);
498
-	}
499
-
500
-	public function testMoveBetweenStorageCrossNonLocal(): void {
501
-		$storage1 = $this->getTestStorage(true, TemporaryNoLocal::class);
502
-		$storage2 = $this->getTestStorage(true, TemporaryNoLocal::class);
503
-		$this->moveBetweenStorages($storage1, $storage2);
504
-	}
505
-
506
-	public function moveBetweenStorages($storage1, $storage2) {
507
-		Filesystem::mount($storage1, [], '/' . $this->user . '/');
508
-		Filesystem::mount($storage2, [], '/' . $this->user . '/substorage');
509
-
510
-		$rootView = new View('/' . $this->user);
511
-		$rootView->rename('foo.txt', 'substorage/folder/foo.txt');
512
-		$this->assertFalse($rootView->file_exists('foo.txt'));
513
-		$this->assertTrue($rootView->file_exists('substorage/folder/foo.txt'));
514
-		$rootView->rename('substorage/folder', 'anotherfolder');
515
-		$this->assertFalse($rootView->is_dir('substorage/folder'));
516
-		$this->assertTrue($rootView->file_exists('anotherfolder/foo.txt'));
517
-		$this->assertTrue($rootView->file_exists('anotherfolder/bar.txt'));
518
-	}
519
-
520
-	public function testUnlink(): void {
521
-		$storage1 = $this->getTestStorage();
522
-		$storage2 = $this->getTestStorage();
523
-		Filesystem::mount($storage1, [], '/');
524
-		Filesystem::mount($storage2, [], '/substorage');
525
-
526
-		$rootView = new View('');
527
-		$rootView->file_put_contents('/foo.txt', 'asd');
528
-		$rootView->file_put_contents('/substorage/bar.txt', 'asd');
529
-
530
-		$this->assertTrue($rootView->file_exists('foo.txt'));
531
-		$this->assertTrue($rootView->file_exists('substorage/bar.txt'));
532
-
533
-		$this->assertTrue($rootView->unlink('foo.txt'));
534
-		$this->assertTrue($rootView->unlink('substorage/bar.txt'));
535
-
536
-		$this->assertFalse($rootView->file_exists('foo.txt'));
537
-		$this->assertFalse($rootView->file_exists('substorage/bar.txt'));
538
-	}
539
-
540
-	public static function rmdirOrUnlinkDataProvider(): array {
541
-		return [['rmdir'], ['unlink']];
542
-	}
543
-
544
-	#[\PHPUnit\Framework\Attributes\DataProvider('rmdirOrUnlinkDataProvider')]
545
-	public function testRmdir($method): void {
546
-		$storage1 = $this->getTestStorage();
547
-		Filesystem::mount($storage1, [], '/');
548
-
549
-		$rootView = new View('');
550
-		$rootView->mkdir('sub');
551
-		$rootView->mkdir('sub/deep');
552
-		$rootView->file_put_contents('/sub/deep/foo.txt', 'asd');
553
-
554
-		$this->assertTrue($rootView->file_exists('sub/deep/foo.txt'));
555
-
556
-		$this->assertTrue($rootView->$method('sub'));
557
-
558
-		$this->assertFalse($rootView->file_exists('sub'));
559
-	}
560
-
561
-	public function testUnlinkRootMustFail(): void {
562
-		$storage1 = $this->getTestStorage();
563
-		$storage2 = $this->getTestStorage();
564
-		Filesystem::mount($storage1, [], '/');
565
-		Filesystem::mount($storage2, [], '/substorage');
566
-
567
-		$rootView = new View('');
568
-		$rootView->file_put_contents('/foo.txt', 'asd');
569
-		$rootView->file_put_contents('/substorage/bar.txt', 'asd');
570
-
571
-		$this->assertFalse($rootView->unlink(''));
572
-		$this->assertFalse($rootView->unlink('/'));
573
-		$this->assertFalse($rootView->unlink('substorage'));
574
-		$this->assertFalse($rootView->unlink('/substorage'));
575
-	}
576
-
577
-	public function testTouch(): void {
578
-		$storage = $this->getTestStorage(true, TemporaryNoTouch::class);
579
-
580
-		Filesystem::mount($storage, [], '/');
581
-
582
-		$rootView = new View('');
583
-		$oldCachedData = $rootView->getFileInfo('foo.txt');
584
-
585
-		$rootView->touch('foo.txt', 500);
586
-
587
-		$cachedData = $rootView->getFileInfo('foo.txt');
588
-		$this->assertEquals(500, $cachedData['mtime']);
589
-		$this->assertEquals($oldCachedData['storage_mtime'], $cachedData['storage_mtime']);
590
-
591
-		$rootView->putFileInfo('foo.txt', ['storage_mtime' => 1000]); //make sure the watcher detects the change
592
-		$rootView->file_put_contents('foo.txt', 'asd');
593
-		$cachedData = $rootView->getFileInfo('foo.txt');
594
-		$this->assertGreaterThanOrEqual($oldCachedData['mtime'], $cachedData['mtime']);
595
-		$this->assertEquals($cachedData['storage_mtime'], $cachedData['mtime']);
596
-	}
597
-
598
-	public function testTouchFloat(): void {
599
-		$storage = $this->getTestStorage(true, TemporaryNoTouch::class);
600
-
601
-		Filesystem::mount($storage, [], '/');
602
-
603
-		$rootView = new View('');
604
-		$oldCachedData = $rootView->getFileInfo('foo.txt');
605
-
606
-		$rootView->touch('foo.txt', 500.5);
607
-
608
-		$cachedData = $rootView->getFileInfo('foo.txt');
609
-		$this->assertEquals(500, $cachedData['mtime']);
610
-	}
611
-
612
-	public function testViewHooks(): void {
613
-		$storage1 = $this->getTestStorage();
614
-		$storage2 = $this->getTestStorage();
615
-		$defaultRoot = Filesystem::getRoot();
616
-		Filesystem::mount($storage1, [], '/');
617
-		Filesystem::mount($storage2, [], $defaultRoot . '/substorage');
618
-		\OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHook');
619
-
620
-		$rootView = new View('');
621
-		$subView = new View($defaultRoot . '/substorage');
622
-		$this->hookPath = null;
623
-
624
-		$rootView->file_put_contents('/foo.txt', 'asd');
625
-		$this->assertNull($this->hookPath);
626
-
627
-		$subView->file_put_contents('/foo.txt', 'asd');
628
-		$this->assertEquals('/substorage/foo.txt', $this->hookPath);
629
-	}
630
-
631
-	private $hookPath;
632
-
633
-	public function dummyHook($params) {
634
-		$this->hookPath = $params['path'];
635
-	}
636
-
637
-	public function testSearchNotOutsideView(): void {
638
-		$storage1 = $this->getTestStorage();
639
-		Filesystem::mount($storage1, [], '/');
640
-		$storage1->rename('folder', 'foo');
641
-		$scanner = $storage1->getScanner();
642
-		$scanner->scan('');
643
-
644
-		$view = new View('/foo');
645
-
646
-		$result = $view->search('.txt');
647
-		$this->assertCount(1, $result);
648
-	}
649
-
650
-	/**
651
-	 * @param bool $scan
652
-	 * @param string $class
653
-	 * @return Storage
654
-	 */
655
-	private function getTestStorage($scan = true, $class = Temporary::class) {
656
-		/**
657
-		 * @var Storage $storage
658
-		 */
659
-		$storage = new $class([]);
660
-		$textData = "dummy file data\n";
661
-		$imgData = file_get_contents(\OC::$SERVERROOT . '/core/img/logo/logo.png');
662
-		$storage->mkdir('folder');
663
-		$storage->file_put_contents('foo.txt', $textData);
664
-		$storage->file_put_contents('foo.png', $imgData);
665
-		$storage->file_put_contents('folder/bar.txt', $textData);
666
-
667
-		if ($scan) {
668
-			$scanner = $storage->getScanner();
669
-			$scanner->scan('');
670
-		}
671
-		$this->storages[] = $storage;
672
-		return $storage;
673
-	}
674
-
675
-	public function testViewHooksIfRootStartsTheSame(): void {
676
-		$storage1 = $this->getTestStorage();
677
-		$storage2 = $this->getTestStorage();
678
-		$defaultRoot = Filesystem::getRoot();
679
-		Filesystem::mount($storage1, [], '/');
680
-		Filesystem::mount($storage2, [], $defaultRoot . '_substorage');
681
-		\OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHook');
682
-
683
-		$subView = new View($defaultRoot . '_substorage');
684
-		$this->hookPath = null;
685
-
686
-		$subView->file_put_contents('/foo.txt', 'asd');
687
-		$this->assertNull($this->hookPath);
688
-	}
689
-
690
-	private $hookWritePath;
691
-	private $hookCreatePath;
692
-	private $hookUpdatePath;
693
-
694
-	public function dummyHookWrite($params) {
695
-		$this->hookWritePath = $params['path'];
696
-	}
697
-
698
-	public function dummyHookUpdate($params) {
699
-		$this->hookUpdatePath = $params['path'];
700
-	}
701
-
702
-	public function dummyHookCreate($params) {
703
-		$this->hookCreatePath = $params['path'];
704
-	}
705
-
706
-	public function testEditNoCreateHook(): void {
707
-		$storage1 = $this->getTestStorage();
708
-		$storage2 = $this->getTestStorage();
709
-		$defaultRoot = Filesystem::getRoot();
710
-		Filesystem::mount($storage1, [], '/');
711
-		Filesystem::mount($storage2, [], $defaultRoot);
712
-		\OC_Hook::connect('OC_Filesystem', 'post_create', $this, 'dummyHookCreate');
713
-		\OC_Hook::connect('OC_Filesystem', 'post_update', $this, 'dummyHookUpdate');
714
-		\OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHookWrite');
715
-
716
-		$view = new View($defaultRoot);
717
-		$this->hookWritePath = $this->hookUpdatePath = $this->hookCreatePath = null;
718
-
719
-		$view->file_put_contents('/asd.txt', 'foo');
720
-		$this->assertEquals('/asd.txt', $this->hookCreatePath);
721
-		$this->assertNull($this->hookUpdatePath);
722
-		$this->assertEquals('/asd.txt', $this->hookWritePath);
723
-
724
-		$this->hookWritePath = $this->hookUpdatePath = $this->hookCreatePath = null;
725
-
726
-		$view->file_put_contents('/asd.txt', 'foo');
727
-		$this->assertNull($this->hookCreatePath);
728
-		$this->assertEquals('/asd.txt', $this->hookUpdatePath);
729
-		$this->assertEquals('/asd.txt', $this->hookWritePath);
730
-
731
-		\OC_Hook::clear('OC_Filesystem', 'post_create');
732
-		\OC_Hook::clear('OC_Filesystem', 'post_update');
733
-		\OC_Hook::clear('OC_Filesystem', 'post_write');
734
-	}
735
-
736
-	#[\PHPUnit\Framework\Attributes\DataProvider('resolvePathTestProvider')]
737
-	public function testResolvePath($expected, $pathToTest): void {
738
-		$storage1 = $this->getTestStorage();
739
-		Filesystem::mount($storage1, [], '/');
740
-
741
-		$view = new View('');
742
-
743
-		$result = $view->resolvePath($pathToTest);
744
-		$this->assertEquals($expected, $result[1]);
745
-
746
-		$exists = $view->file_exists($pathToTest);
747
-		$this->assertTrue($exists);
748
-
749
-		$exists = $view->file_exists($result[1]);
750
-		$this->assertTrue($exists);
751
-	}
752
-
753
-	public static function resolvePathTestProvider(): array {
754
-		return [
755
-			['foo.txt', 'foo.txt'],
756
-			['foo.txt', '/foo.txt'],
757
-			['folder', 'folder'],
758
-			['folder', '/folder'],
759
-			['folder', 'folder/'],
760
-			['folder', '/folder/'],
761
-			['folder/bar.txt', 'folder/bar.txt'],
762
-			['folder/bar.txt', '/folder/bar.txt'],
763
-			['', ''],
764
-			['', '/'],
765
-		];
766
-	}
767
-
768
-	public function testUTF8Names(): void {
769
-		$names = ['虚', '和知しゃ和で', 'regular ascii', 'sɨˈrɪlɪk', 'ѨѬ', 'أنا أحب القراءة كثيرا'];
770
-
771
-		$storage = new Temporary([]);
772
-		Filesystem::mount($storage, [], '/');
773
-
774
-		$rootView = new View('');
775
-		foreach ($names as $name) {
776
-			$rootView->file_put_contents('/' . $name, 'dummy content');
777
-		}
778
-
779
-		$list = $rootView->getDirectoryContent('/');
780
-
781
-		$this->assertCount(count($names), $list);
782
-		foreach ($list as $item) {
783
-			$this->assertContains($item['name'], $names);
784
-		}
785
-
786
-		$cache = $storage->getCache();
787
-		$scanner = $storage->getScanner();
788
-		$scanner->scan('');
789
-
790
-		$list = $cache->getFolderContents('');
791
-
792
-		$this->assertCount(count($names), $list);
793
-		foreach ($list as $item) {
794
-			$this->assertContains($item['name'], $names);
795
-		}
796
-	}
797
-
798
-	public function xtestLongPath() {
799
-		$storage = new Temporary([]);
800
-		Filesystem::mount($storage, [], '/');
801
-
802
-		$rootView = new View('');
803
-
804
-		$longPath = '';
805
-		$ds = DIRECTORY_SEPARATOR;
806
-		/*
99
+    use UserTrait;
100
+
101
+    /**
102
+     * @var Storage[] $storages
103
+     */
104
+    private $storages = [];
105
+
106
+    /**
107
+     * @var string
108
+     */
109
+    private $user;
110
+
111
+    /**
112
+     * @var IUser
113
+     */
114
+    private $userObject;
115
+
116
+    /**
117
+     * @var IGroup
118
+     */
119
+    private $groupObject;
120
+
121
+    /** @var Storage */
122
+    private $tempStorage;
123
+
124
+    protected function setUp(): void {
125
+        parent::setUp();
126
+        \OC_Hook::clear();
127
+
128
+        Server::get(IUserManager::class)->clearBackends();
129
+        Server::get(IUserManager::class)->registerBackend(new \Test\Util\User\Dummy());
130
+
131
+        //login
132
+        $userManager = Server::get(IUserManager::class);
133
+        $groupManager = Server::get(IGroupManager::class);
134
+        $this->user = 'test';
135
+        $this->userObject = $userManager->createUser('test', 'test');
136
+
137
+        $this->groupObject = $groupManager->createGroup('group1');
138
+        $this->groupObject->addUser($this->userObject);
139
+
140
+        self::loginAsUser($this->user);
141
+
142
+        /** @var IMountManager $manager */
143
+        $manager = Server::get(IMountManager::class);
144
+        $manager->removeMount('/test');
145
+
146
+        $this->tempStorage = null;
147
+    }
148
+
149
+    protected function tearDown(): void {
150
+        \OC_User::setUserId($this->user);
151
+        foreach ($this->storages as $storage) {
152
+            $cache = $storage->getCache();
153
+            $ids = $cache->getAll();
154
+            $cache->clear();
155
+        }
156
+
157
+        if ($this->tempStorage) {
158
+            system('rm -rf ' . escapeshellarg($this->tempStorage->getDataDir()));
159
+        }
160
+
161
+        self::logout();
162
+
163
+        /** @var SetupManager $setupManager */
164
+        $setupManager = Server::get(SetupManager::class);
165
+        $setupManager->setupRoot();
166
+
167
+        $this->userObject->delete();
168
+        $this->groupObject->delete();
169
+
170
+        $mountProviderCollection = Server::get(IMountProviderCollection::class);
171
+        self::invokePrivate($mountProviderCollection, 'providers', [[]]);
172
+
173
+        parent::tearDown();
174
+    }
175
+
176
+    public function testCacheAPI(): void {
177
+        $storage1 = $this->getTestStorage();
178
+        $storage2 = $this->getTestStorage();
179
+        $storage3 = $this->getTestStorage();
180
+        $root = self::getUniqueID('/');
181
+        Filesystem::mount($storage1, [], $root . '/');
182
+        Filesystem::mount($storage2, [], $root . '/substorage');
183
+        Filesystem::mount($storage3, [], $root . '/folder/anotherstorage');
184
+        $textSize = strlen("dummy file data\n");
185
+        $imageSize = filesize(\OC::$SERVERROOT . '/core/img/logo/logo.png');
186
+        $storageSize = $textSize * 2 + $imageSize;
187
+
188
+        $storageInfo = $storage3->getCache()->get('');
189
+        $this->assertEquals($storageSize, $storageInfo['size']);
190
+
191
+        $rootView = new View($root);
192
+
193
+        $cachedData = $rootView->getFileInfo('/foo.txt');
194
+        $this->assertEquals($textSize, $cachedData['size']);
195
+        $this->assertEquals('text/plain', $cachedData['mimetype']);
196
+        $this->assertNotEquals(-1, $cachedData['permissions']);
197
+
198
+        $cachedData = $rootView->getFileInfo('/');
199
+        $this->assertEquals($storageSize * 3, $cachedData['size']);
200
+        $this->assertEquals('httpd/unix-directory', $cachedData['mimetype']);
201
+
202
+        // get cached data excluding mount points
203
+        $cachedData = $rootView->getFileInfo('/', false);
204
+        $this->assertEquals($storageSize, $cachedData['size']);
205
+        $this->assertEquals('httpd/unix-directory', $cachedData['mimetype']);
206
+
207
+        $cachedData = $rootView->getFileInfo('/folder');
208
+        $this->assertEquals($storageSize + $textSize, $cachedData['size']);
209
+        $this->assertEquals('httpd/unix-directory', $cachedData['mimetype']);
210
+
211
+        $folderData = $rootView->getDirectoryContent('/');
212
+        /**
213
+         * expected entries:
214
+         * folder
215
+         * foo.png
216
+         * foo.txt
217
+         * substorage
218
+         */
219
+        $this->assertCount(4, $folderData);
220
+        $this->assertEquals('folder', $folderData[0]['name']);
221
+        $this->assertEquals('foo.png', $folderData[1]['name']);
222
+        $this->assertEquals('foo.txt', $folderData[2]['name']);
223
+        $this->assertEquals('substorage', $folderData[3]['name']);
224
+
225
+        $this->assertEquals($storageSize + $textSize, $folderData[0]['size']);
226
+        $this->assertEquals($imageSize, $folderData[1]['size']);
227
+        $this->assertEquals($textSize, $folderData[2]['size']);
228
+        $this->assertEquals($storageSize, $folderData[3]['size']);
229
+
230
+        $folderData = $rootView->getDirectoryContent('/substorage');
231
+        /**
232
+         * expected entries:
233
+         * folder
234
+         * foo.png
235
+         * foo.txt
236
+         */
237
+        $this->assertCount(3, $folderData);
238
+        $this->assertEquals('folder', $folderData[0]['name']);
239
+        $this->assertEquals('foo.png', $folderData[1]['name']);
240
+        $this->assertEquals('foo.txt', $folderData[2]['name']);
241
+
242
+        $folderView = new View($root . '/folder');
243
+        $this->assertEquals($rootView->getFileInfo('/folder'), $folderView->getFileInfo('/'));
244
+
245
+        $cachedData = $rootView->getFileInfo('/foo.txt');
246
+        $this->assertFalse($cachedData['encrypted']);
247
+        $id = $rootView->putFileInfo('/foo.txt', ['encrypted' => true]);
248
+        $cachedData = $rootView->getFileInfo('/foo.txt');
249
+        $this->assertTrue($cachedData['encrypted']);
250
+        $this->assertEquals($cachedData['fileid'], $id);
251
+
252
+        $this->assertFalse($rootView->getFileInfo('/non/existing'));
253
+        $this->assertEquals([], $rootView->getDirectoryContent('/non/existing'));
254
+    }
255
+
256
+    public function testGetPath(): void {
257
+        $user = $this->createMock(IUser::class);
258
+        $user->method('getUID')
259
+            ->willReturn('test');
260
+        $storage1 = $this->getTestStorage();
261
+        $storage2 = $this->getTestStorage();
262
+        $storage3 = $this->getTestStorage();
263
+
264
+        Filesystem::mount($storage1, [], '/test/files');
265
+        Filesystem::mount($storage2, [], '/test/files/substorage');
266
+        Filesystem::mount($storage3, [], '/test/files/folder/anotherstorage');
267
+
268
+        $userMountCache = Server::get(IUserMountCache::class);
269
+        $userMountCache->registerMounts($user, [
270
+            new MountPoint($storage1, '/test/files'),
271
+            new MountPoint($storage2, '/test/files/substorage'),
272
+            new MountPoint($storage3, '/test/files/folder/anotherstorage'),
273
+        ]);
274
+
275
+        $rootView = new View('/test/files');
276
+
277
+
278
+        $cachedData = $rootView->getFileInfo('/foo.txt');
279
+        $id1 = $cachedData->getId();
280
+        $this->assertEquals('/foo.txt', $rootView->getPath($id1));
281
+
282
+        $cachedData = $rootView->getFileInfo('/substorage/foo.txt');
283
+        $id2 = $cachedData->getId();
284
+        $this->assertEquals('/substorage/foo.txt', $rootView->getPath($id2));
285
+
286
+        $folderView = new View('/test/files/substorage');
287
+        $this->assertEquals('/foo.txt', $folderView->getPath($id2));
288
+    }
289
+
290
+
291
+    public function testGetPathNotExisting(): void {
292
+        $this->expectException(NotFoundException::class);
293
+
294
+        $storage1 = $this->getTestStorage();
295
+        Filesystem::mount($storage1, [], '/');
296
+
297
+        $rootView = new View('');
298
+        $cachedData = $rootView->getFileInfo('/foo.txt');
299
+        /** @var int $id1 */
300
+        $id1 = $cachedData['fileid'];
301
+        $folderView = new View('/substorage');
302
+        $this->assertNull($folderView->getPath($id1));
303
+    }
304
+
305
+    public function testMountPointOverwrite(): void {
306
+        $storage1 = $this->getTestStorage(false);
307
+        $storage2 = $this->getTestStorage();
308
+        $storage1->mkdir('substorage');
309
+        Filesystem::mount($storage1, [], '/');
310
+        Filesystem::mount($storage2, [], '/substorage');
311
+
312
+        $rootView = new View('');
313
+        $folderContent = $rootView->getDirectoryContent('/');
314
+        $this->assertCount(4, $folderContent);
315
+    }
316
+
317
+    public static function sharingDisabledPermissionProvider(): array {
318
+        return [
319
+            ['no', '', true],
320
+            ['yes', 'group1', false],
321
+        ];
322
+    }
323
+
324
+    #[\PHPUnit\Framework\Attributes\DataProvider('sharingDisabledPermissionProvider')]
325
+    public function testRemoveSharePermissionWhenSharingDisabledForUser($excludeGroups, $excludeGroupsList, $expectedShareable): void {
326
+        // Reset sharing disabled for users cache
327
+        self::invokePrivate(Server::get(ShareDisableChecker::class), 'sharingDisabledForUsersCache', [new CappedMemoryCache()]);
328
+
329
+        $config = Server::get(IConfig::class);
330
+        $oldExcludeGroupsFlag = $config->getAppValue('core', 'shareapi_exclude_groups', 'no');
331
+        $oldExcludeGroupsList = $config->getAppValue('core', 'shareapi_exclude_groups_list', '');
332
+        $config->setAppValue('core', 'shareapi_exclude_groups', $excludeGroups);
333
+        $config->setAppValue('core', 'shareapi_exclude_groups_list', $excludeGroupsList);
334
+
335
+        $storage1 = $this->getTestStorage();
336
+        $storage2 = $this->getTestStorage();
337
+        Filesystem::mount($storage1, [], '/');
338
+        Filesystem::mount($storage2, [], '/mount');
339
+
340
+        $view = new View('/');
341
+
342
+        $folderContent = $view->getDirectoryContent('');
343
+        $this->assertEquals($expectedShareable, $folderContent[0]->isShareable());
344
+
345
+        $folderContent = $view->getDirectoryContent('mount');
346
+        $this->assertEquals($expectedShareable, $folderContent[0]->isShareable());
347
+
348
+        $config->setAppValue('core', 'shareapi_exclude_groups', $oldExcludeGroupsFlag);
349
+        $config->setAppValue('core', 'shareapi_exclude_groups_list', $oldExcludeGroupsList);
350
+
351
+        // Reset sharing disabled for users cache
352
+        self::invokePrivate(Server::get(ShareDisableChecker::class), 'sharingDisabledForUsersCache', [new CappedMemoryCache()]);
353
+    }
354
+
355
+    public function testCacheIncompleteFolder(): void {
356
+        $storage1 = $this->getTestStorage(false);
357
+        Filesystem::mount($storage1, [], '/incomplete');
358
+        $rootView = new View('/incomplete');
359
+
360
+        $entries = $rootView->getDirectoryContent('/');
361
+        $this->assertCount(3, $entries);
362
+
363
+        // /folder will already be in the cache but not scanned
364
+        $entries = $rootView->getDirectoryContent('/folder');
365
+        $this->assertCount(1, $entries);
366
+    }
367
+
368
+    public function testAutoScan(): void {
369
+        $storage1 = $this->getTestStorage(false);
370
+        $storage2 = $this->getTestStorage(false);
371
+        Filesystem::mount($storage1, [], '/');
372
+        Filesystem::mount($storage2, [], '/substorage');
373
+        $textSize = strlen("dummy file data\n");
374
+
375
+        $rootView = new View('');
376
+
377
+        $cachedData = $rootView->getFileInfo('/');
378
+        $this->assertEquals('httpd/unix-directory', $cachedData['mimetype']);
379
+        $this->assertEquals(-1, $cachedData['size']);
380
+
381
+        $folderData = $rootView->getDirectoryContent('/substorage/folder');
382
+        $this->assertEquals('text/plain', $folderData[0]['mimetype']);
383
+        $this->assertEquals($textSize, $folderData[0]['size']);
384
+    }
385
+
386
+    public function testSearch(): void {
387
+        $storage1 = $this->getTestStorage();
388
+        $storage2 = $this->getTestStorage();
389
+        $storage3 = $this->getTestStorage();
390
+        Filesystem::mount($storage1, [], '/');
391
+        Filesystem::mount($storage2, [], '/substorage');
392
+        Filesystem::mount($storage3, [], '/folder/anotherstorage');
393
+
394
+        $rootView = new View('');
395
+
396
+        $results = $rootView->search('foo');
397
+        $this->assertCount(6, $results);
398
+        $paths = [];
399
+        foreach ($results as $result) {
400
+            $this->assertEquals($result['path'], Filesystem::normalizePath($result['path']));
401
+            $paths[] = $result['path'];
402
+        }
403
+        $this->assertContains('/foo.txt', $paths);
404
+        $this->assertContains('/foo.png', $paths);
405
+        $this->assertContains('/substorage/foo.txt', $paths);
406
+        $this->assertContains('/substorage/foo.png', $paths);
407
+        $this->assertContains('/folder/anotherstorage/foo.txt', $paths);
408
+        $this->assertContains('/folder/anotherstorage/foo.png', $paths);
409
+
410
+        $folderView = new View('/folder');
411
+        $results = $folderView->search('bar');
412
+        $this->assertCount(2, $results);
413
+        $paths = [];
414
+        foreach ($results as $result) {
415
+            $paths[] = $result['path'];
416
+        }
417
+        $this->assertContains('/anotherstorage/folder/bar.txt', $paths);
418
+        $this->assertContains('/bar.txt', $paths);
419
+
420
+        $results = $folderView->search('foo');
421
+        $this->assertCount(2, $results);
422
+        $paths = [];
423
+        foreach ($results as $result) {
424
+            $paths[] = $result['path'];
425
+        }
426
+        $this->assertContains('/anotherstorage/foo.txt', $paths);
427
+        $this->assertContains('/anotherstorage/foo.png', $paths);
428
+
429
+        $this->assertCount(6, $rootView->searchByMime('text'));
430
+        $this->assertCount(3, $folderView->searchByMime('text'));
431
+    }
432
+
433
+    public function testWatcher(): void {
434
+        $storage1 = $this->getTestStorage();
435
+        Filesystem::mount($storage1, [], '/');
436
+        $storage1->getWatcher()->setPolicy(Watcher::CHECK_ALWAYS);
437
+
438
+        $rootView = new View('');
439
+
440
+        $cachedData = $rootView->getFileInfo('foo.txt');
441
+        $this->assertEquals(16, $cachedData['size']);
442
+
443
+        $rootView->putFileInfo('foo.txt', ['storage_mtime' => 10]);
444
+        $storage1->file_put_contents('foo.txt', 'foo');
445
+        clearstatcache();
446
+
447
+        $cachedData = $rootView->getFileInfo('foo.txt');
448
+        $this->assertEquals(3, $cachedData['size']);
449
+    }
450
+
451
+    public function testCopyBetweenStorageNoCross(): void {
452
+        $storage1 = $this->getTestStorage(true, TemporaryNoCross::class);
453
+        $storage2 = $this->getTestStorage(true, TemporaryNoCross::class);
454
+        $this->copyBetweenStorages($storage1, $storage2);
455
+    }
456
+
457
+    public function testCopyBetweenStorageCross(): void {
458
+        $storage1 = $this->getTestStorage();
459
+        $storage2 = $this->getTestStorage();
460
+        $this->copyBetweenStorages($storage1, $storage2);
461
+    }
462
+
463
+    public function testCopyBetweenStorageCrossNonLocal(): void {
464
+        $storage1 = $this->getTestStorage(true, TemporaryNoLocal::class);
465
+        $storage2 = $this->getTestStorage(true, TemporaryNoLocal::class);
466
+        $this->copyBetweenStorages($storage1, $storage2);
467
+    }
468
+
469
+    public function copyBetweenStorages($storage1, $storage2) {
470
+        Filesystem::mount($storage1, [], '/');
471
+        Filesystem::mount($storage2, [], '/substorage');
472
+
473
+        $rootView = new View('');
474
+        $rootView->mkdir('substorage/emptyfolder');
475
+        $rootView->copy('substorage', 'anotherfolder');
476
+        $this->assertTrue($rootView->is_dir('/anotherfolder'));
477
+        $this->assertTrue($rootView->is_dir('/substorage'));
478
+        $this->assertTrue($rootView->is_dir('/anotherfolder/emptyfolder'));
479
+        $this->assertTrue($rootView->is_dir('/substorage/emptyfolder'));
480
+        $this->assertTrue($rootView->file_exists('/anotherfolder/foo.txt'));
481
+        $this->assertTrue($rootView->file_exists('/anotherfolder/foo.png'));
482
+        $this->assertTrue($rootView->file_exists('/anotherfolder/folder/bar.txt'));
483
+        $this->assertTrue($rootView->file_exists('/substorage/foo.txt'));
484
+        $this->assertTrue($rootView->file_exists('/substorage/foo.png'));
485
+        $this->assertTrue($rootView->file_exists('/substorage/folder/bar.txt'));
486
+    }
487
+
488
+    public function testMoveBetweenStorageNoCross(): void {
489
+        $storage1 = $this->getTestStorage(true, TemporaryNoCross::class);
490
+        $storage2 = $this->getTestStorage(true, TemporaryNoCross::class);
491
+        $this->moveBetweenStorages($storage1, $storage2);
492
+    }
493
+
494
+    public function testMoveBetweenStorageCross(): void {
495
+        $storage1 = $this->getTestStorage();
496
+        $storage2 = $this->getTestStorage();
497
+        $this->moveBetweenStorages($storage1, $storage2);
498
+    }
499
+
500
+    public function testMoveBetweenStorageCrossNonLocal(): void {
501
+        $storage1 = $this->getTestStorage(true, TemporaryNoLocal::class);
502
+        $storage2 = $this->getTestStorage(true, TemporaryNoLocal::class);
503
+        $this->moveBetweenStorages($storage1, $storage2);
504
+    }
505
+
506
+    public function moveBetweenStorages($storage1, $storage2) {
507
+        Filesystem::mount($storage1, [], '/' . $this->user . '/');
508
+        Filesystem::mount($storage2, [], '/' . $this->user . '/substorage');
509
+
510
+        $rootView = new View('/' . $this->user);
511
+        $rootView->rename('foo.txt', 'substorage/folder/foo.txt');
512
+        $this->assertFalse($rootView->file_exists('foo.txt'));
513
+        $this->assertTrue($rootView->file_exists('substorage/folder/foo.txt'));
514
+        $rootView->rename('substorage/folder', 'anotherfolder');
515
+        $this->assertFalse($rootView->is_dir('substorage/folder'));
516
+        $this->assertTrue($rootView->file_exists('anotherfolder/foo.txt'));
517
+        $this->assertTrue($rootView->file_exists('anotherfolder/bar.txt'));
518
+    }
519
+
520
+    public function testUnlink(): void {
521
+        $storage1 = $this->getTestStorage();
522
+        $storage2 = $this->getTestStorage();
523
+        Filesystem::mount($storage1, [], '/');
524
+        Filesystem::mount($storage2, [], '/substorage');
525
+
526
+        $rootView = new View('');
527
+        $rootView->file_put_contents('/foo.txt', 'asd');
528
+        $rootView->file_put_contents('/substorage/bar.txt', 'asd');
529
+
530
+        $this->assertTrue($rootView->file_exists('foo.txt'));
531
+        $this->assertTrue($rootView->file_exists('substorage/bar.txt'));
532
+
533
+        $this->assertTrue($rootView->unlink('foo.txt'));
534
+        $this->assertTrue($rootView->unlink('substorage/bar.txt'));
535
+
536
+        $this->assertFalse($rootView->file_exists('foo.txt'));
537
+        $this->assertFalse($rootView->file_exists('substorage/bar.txt'));
538
+    }
539
+
540
+    public static function rmdirOrUnlinkDataProvider(): array {
541
+        return [['rmdir'], ['unlink']];
542
+    }
543
+
544
+    #[\PHPUnit\Framework\Attributes\DataProvider('rmdirOrUnlinkDataProvider')]
545
+    public function testRmdir($method): void {
546
+        $storage1 = $this->getTestStorage();
547
+        Filesystem::mount($storage1, [], '/');
548
+
549
+        $rootView = new View('');
550
+        $rootView->mkdir('sub');
551
+        $rootView->mkdir('sub/deep');
552
+        $rootView->file_put_contents('/sub/deep/foo.txt', 'asd');
553
+
554
+        $this->assertTrue($rootView->file_exists('sub/deep/foo.txt'));
555
+
556
+        $this->assertTrue($rootView->$method('sub'));
557
+
558
+        $this->assertFalse($rootView->file_exists('sub'));
559
+    }
560
+
561
+    public function testUnlinkRootMustFail(): void {
562
+        $storage1 = $this->getTestStorage();
563
+        $storage2 = $this->getTestStorage();
564
+        Filesystem::mount($storage1, [], '/');
565
+        Filesystem::mount($storage2, [], '/substorage');
566
+
567
+        $rootView = new View('');
568
+        $rootView->file_put_contents('/foo.txt', 'asd');
569
+        $rootView->file_put_contents('/substorage/bar.txt', 'asd');
570
+
571
+        $this->assertFalse($rootView->unlink(''));
572
+        $this->assertFalse($rootView->unlink('/'));
573
+        $this->assertFalse($rootView->unlink('substorage'));
574
+        $this->assertFalse($rootView->unlink('/substorage'));
575
+    }
576
+
577
+    public function testTouch(): void {
578
+        $storage = $this->getTestStorage(true, TemporaryNoTouch::class);
579
+
580
+        Filesystem::mount($storage, [], '/');
581
+
582
+        $rootView = new View('');
583
+        $oldCachedData = $rootView->getFileInfo('foo.txt');
584
+
585
+        $rootView->touch('foo.txt', 500);
586
+
587
+        $cachedData = $rootView->getFileInfo('foo.txt');
588
+        $this->assertEquals(500, $cachedData['mtime']);
589
+        $this->assertEquals($oldCachedData['storage_mtime'], $cachedData['storage_mtime']);
590
+
591
+        $rootView->putFileInfo('foo.txt', ['storage_mtime' => 1000]); //make sure the watcher detects the change
592
+        $rootView->file_put_contents('foo.txt', 'asd');
593
+        $cachedData = $rootView->getFileInfo('foo.txt');
594
+        $this->assertGreaterThanOrEqual($oldCachedData['mtime'], $cachedData['mtime']);
595
+        $this->assertEquals($cachedData['storage_mtime'], $cachedData['mtime']);
596
+    }
597
+
598
+    public function testTouchFloat(): void {
599
+        $storage = $this->getTestStorage(true, TemporaryNoTouch::class);
600
+
601
+        Filesystem::mount($storage, [], '/');
602
+
603
+        $rootView = new View('');
604
+        $oldCachedData = $rootView->getFileInfo('foo.txt');
605
+
606
+        $rootView->touch('foo.txt', 500.5);
607
+
608
+        $cachedData = $rootView->getFileInfo('foo.txt');
609
+        $this->assertEquals(500, $cachedData['mtime']);
610
+    }
611
+
612
+    public function testViewHooks(): void {
613
+        $storage1 = $this->getTestStorage();
614
+        $storage2 = $this->getTestStorage();
615
+        $defaultRoot = Filesystem::getRoot();
616
+        Filesystem::mount($storage1, [], '/');
617
+        Filesystem::mount($storage2, [], $defaultRoot . '/substorage');
618
+        \OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHook');
619
+
620
+        $rootView = new View('');
621
+        $subView = new View($defaultRoot . '/substorage');
622
+        $this->hookPath = null;
623
+
624
+        $rootView->file_put_contents('/foo.txt', 'asd');
625
+        $this->assertNull($this->hookPath);
626
+
627
+        $subView->file_put_contents('/foo.txt', 'asd');
628
+        $this->assertEquals('/substorage/foo.txt', $this->hookPath);
629
+    }
630
+
631
+    private $hookPath;
632
+
633
+    public function dummyHook($params) {
634
+        $this->hookPath = $params['path'];
635
+    }
636
+
637
+    public function testSearchNotOutsideView(): void {
638
+        $storage1 = $this->getTestStorage();
639
+        Filesystem::mount($storage1, [], '/');
640
+        $storage1->rename('folder', 'foo');
641
+        $scanner = $storage1->getScanner();
642
+        $scanner->scan('');
643
+
644
+        $view = new View('/foo');
645
+
646
+        $result = $view->search('.txt');
647
+        $this->assertCount(1, $result);
648
+    }
649
+
650
+    /**
651
+     * @param bool $scan
652
+     * @param string $class
653
+     * @return Storage
654
+     */
655
+    private function getTestStorage($scan = true, $class = Temporary::class) {
656
+        /**
657
+         * @var Storage $storage
658
+         */
659
+        $storage = new $class([]);
660
+        $textData = "dummy file data\n";
661
+        $imgData = file_get_contents(\OC::$SERVERROOT . '/core/img/logo/logo.png');
662
+        $storage->mkdir('folder');
663
+        $storage->file_put_contents('foo.txt', $textData);
664
+        $storage->file_put_contents('foo.png', $imgData);
665
+        $storage->file_put_contents('folder/bar.txt', $textData);
666
+
667
+        if ($scan) {
668
+            $scanner = $storage->getScanner();
669
+            $scanner->scan('');
670
+        }
671
+        $this->storages[] = $storage;
672
+        return $storage;
673
+    }
674
+
675
+    public function testViewHooksIfRootStartsTheSame(): void {
676
+        $storage1 = $this->getTestStorage();
677
+        $storage2 = $this->getTestStorage();
678
+        $defaultRoot = Filesystem::getRoot();
679
+        Filesystem::mount($storage1, [], '/');
680
+        Filesystem::mount($storage2, [], $defaultRoot . '_substorage');
681
+        \OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHook');
682
+
683
+        $subView = new View($defaultRoot . '_substorage');
684
+        $this->hookPath = null;
685
+
686
+        $subView->file_put_contents('/foo.txt', 'asd');
687
+        $this->assertNull($this->hookPath);
688
+    }
689
+
690
+    private $hookWritePath;
691
+    private $hookCreatePath;
692
+    private $hookUpdatePath;
693
+
694
+    public function dummyHookWrite($params) {
695
+        $this->hookWritePath = $params['path'];
696
+    }
697
+
698
+    public function dummyHookUpdate($params) {
699
+        $this->hookUpdatePath = $params['path'];
700
+    }
701
+
702
+    public function dummyHookCreate($params) {
703
+        $this->hookCreatePath = $params['path'];
704
+    }
705
+
706
+    public function testEditNoCreateHook(): void {
707
+        $storage1 = $this->getTestStorage();
708
+        $storage2 = $this->getTestStorage();
709
+        $defaultRoot = Filesystem::getRoot();
710
+        Filesystem::mount($storage1, [], '/');
711
+        Filesystem::mount($storage2, [], $defaultRoot);
712
+        \OC_Hook::connect('OC_Filesystem', 'post_create', $this, 'dummyHookCreate');
713
+        \OC_Hook::connect('OC_Filesystem', 'post_update', $this, 'dummyHookUpdate');
714
+        \OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHookWrite');
715
+
716
+        $view = new View($defaultRoot);
717
+        $this->hookWritePath = $this->hookUpdatePath = $this->hookCreatePath = null;
718
+
719
+        $view->file_put_contents('/asd.txt', 'foo');
720
+        $this->assertEquals('/asd.txt', $this->hookCreatePath);
721
+        $this->assertNull($this->hookUpdatePath);
722
+        $this->assertEquals('/asd.txt', $this->hookWritePath);
723
+
724
+        $this->hookWritePath = $this->hookUpdatePath = $this->hookCreatePath = null;
725
+
726
+        $view->file_put_contents('/asd.txt', 'foo');
727
+        $this->assertNull($this->hookCreatePath);
728
+        $this->assertEquals('/asd.txt', $this->hookUpdatePath);
729
+        $this->assertEquals('/asd.txt', $this->hookWritePath);
730
+
731
+        \OC_Hook::clear('OC_Filesystem', 'post_create');
732
+        \OC_Hook::clear('OC_Filesystem', 'post_update');
733
+        \OC_Hook::clear('OC_Filesystem', 'post_write');
734
+    }
735
+
736
+    #[\PHPUnit\Framework\Attributes\DataProvider('resolvePathTestProvider')]
737
+    public function testResolvePath($expected, $pathToTest): void {
738
+        $storage1 = $this->getTestStorage();
739
+        Filesystem::mount($storage1, [], '/');
740
+
741
+        $view = new View('');
742
+
743
+        $result = $view->resolvePath($pathToTest);
744
+        $this->assertEquals($expected, $result[1]);
745
+
746
+        $exists = $view->file_exists($pathToTest);
747
+        $this->assertTrue($exists);
748
+
749
+        $exists = $view->file_exists($result[1]);
750
+        $this->assertTrue($exists);
751
+    }
752
+
753
+    public static function resolvePathTestProvider(): array {
754
+        return [
755
+            ['foo.txt', 'foo.txt'],
756
+            ['foo.txt', '/foo.txt'],
757
+            ['folder', 'folder'],
758
+            ['folder', '/folder'],
759
+            ['folder', 'folder/'],
760
+            ['folder', '/folder/'],
761
+            ['folder/bar.txt', 'folder/bar.txt'],
762
+            ['folder/bar.txt', '/folder/bar.txt'],
763
+            ['', ''],
764
+            ['', '/'],
765
+        ];
766
+    }
767
+
768
+    public function testUTF8Names(): void {
769
+        $names = ['虚', '和知しゃ和で', 'regular ascii', 'sɨˈrɪlɪk', 'ѨѬ', 'أنا أحب القراءة كثيرا'];
770
+
771
+        $storage = new Temporary([]);
772
+        Filesystem::mount($storage, [], '/');
773
+
774
+        $rootView = new View('');
775
+        foreach ($names as $name) {
776
+            $rootView->file_put_contents('/' . $name, 'dummy content');
777
+        }
778
+
779
+        $list = $rootView->getDirectoryContent('/');
780
+
781
+        $this->assertCount(count($names), $list);
782
+        foreach ($list as $item) {
783
+            $this->assertContains($item['name'], $names);
784
+        }
785
+
786
+        $cache = $storage->getCache();
787
+        $scanner = $storage->getScanner();
788
+        $scanner->scan('');
789
+
790
+        $list = $cache->getFolderContents('');
791
+
792
+        $this->assertCount(count($names), $list);
793
+        foreach ($list as $item) {
794
+            $this->assertContains($item['name'], $names);
795
+        }
796
+    }
797
+
798
+    public function xtestLongPath() {
799
+        $storage = new Temporary([]);
800
+        Filesystem::mount($storage, [], '/');
801
+
802
+        $rootView = new View('');
803
+
804
+        $longPath = '';
805
+        $ds = DIRECTORY_SEPARATOR;
806
+        /*
807 807
 		 * 4096 is the maximum path length in file_cache.path in *nix
808 808
 		 */
809
-		$folderName = 'abcdefghijklmnopqrstuvwxyz012345678901234567890123456789';
810
-		$tmpdirLength = strlen(Server::get(ITempManager::class)->getTemporaryFolder());
811
-		$depth = ((4000 - $tmpdirLength) / 57);
812
-
813
-		foreach (range(0, $depth - 1) as $i) {
814
-			$longPath .= $ds . $folderName;
815
-			$result = $rootView->mkdir($longPath);
816
-			$this->assertTrue($result, "mkdir failed on $i - path length: " . strlen($longPath));
817
-
818
-			$result = $rootView->file_put_contents($longPath . "{$ds}test.txt", 'lorem');
819
-			$this->assertEquals(5, $result, "file_put_contents failed on $i");
820
-
821
-			$this->assertTrue($rootView->file_exists($longPath));
822
-			$this->assertTrue($rootView->file_exists($longPath . "{$ds}test.txt"));
823
-		}
824
-
825
-		$cache = $storage->getCache();
826
-		$scanner = $storage->getScanner();
827
-		$scanner->scan('');
828
-
829
-		$longPath = $folderName;
830
-		foreach (range(0, $depth - 1) as $i) {
831
-			$cachedFolder = $cache->get($longPath);
832
-			$this->assertTrue(is_array($cachedFolder), "No cache entry for folder at $i");
833
-			$this->assertEquals($folderName, $cachedFolder['name'], "Wrong cache entry for folder at $i");
834
-
835
-			$cachedFile = $cache->get($longPath . '/test.txt');
836
-			$this->assertTrue(is_array($cachedFile), "No cache entry for file at $i");
837
-			$this->assertEquals('test.txt', $cachedFile['name'], "Wrong cache entry for file at $i");
838
-
839
-			$longPath .= $ds . $folderName;
840
-		}
841
-	}
842
-
843
-	public function testTouchNotSupported(): void {
844
-		$storage = new TemporaryNoTouch([]);
845
-		$scanner = $storage->getScanner();
846
-		Filesystem::mount($storage, [], '/test/');
847
-		$past = time() - 100;
848
-		$storage->file_put_contents('test', 'foobar');
849
-		$scanner->scan('');
850
-		$view = new View('');
851
-		$info = $view->getFileInfo('/test/test');
852
-
853
-		$view->touch('/test/test', $past);
854
-		$scanner->scanFile('test', Scanner::REUSE_ETAG);
855
-
856
-		$info2 = $view->getFileInfo('/test/test');
857
-		$this->assertSame($info['etag'], $info2['etag']);
858
-	}
859
-
860
-	public function testWatcherEtagCrossStorage(): void {
861
-		$storage1 = new Temporary([]);
862
-		$storage2 = new Temporary([]);
863
-		$scanner1 = $storage1->getScanner();
864
-		$scanner2 = $storage2->getScanner();
865
-		$storage1->mkdir('sub');
866
-		Filesystem::mount($storage1, [], '/test/');
867
-		Filesystem::mount($storage2, [], '/test/sub/storage');
868
-
869
-		$past = time() - 100;
870
-		$storage2->file_put_contents('test.txt', 'foobar');
871
-		$scanner1->scan('');
872
-		$scanner2->scan('');
873
-		$view = new View('');
874
-
875
-		$storage2->getWatcher('')->setPolicy(Watcher::CHECK_ALWAYS);
876
-
877
-		$oldFileInfo = $view->getFileInfo('/test/sub/storage/test.txt');
878
-		$oldFolderInfo = $view->getFileInfo('/test');
879
-
880
-		$storage2->getCache()->update($oldFileInfo->getId(), [
881
-			'storage_mtime' => $past,
882
-		]);
883
-
884
-		$oldEtag = $oldFolderInfo->getEtag();
885
-
886
-		$view->getFileInfo('/test/sub/storage/test.txt');
887
-		$newFolderInfo = $view->getFileInfo('/test');
888
-
889
-		$this->assertNotEquals($newFolderInfo->getEtag(), $oldEtag);
890
-	}
891
-
892
-	#[\PHPUnit\Framework\Attributes\DataProvider('absolutePathProvider')]
893
-	public function testGetAbsolutePath($expectedPath, $relativePath): void {
894
-		$view = new View('/files');
895
-		$this->assertEquals($expectedPath, $view->getAbsolutePath($relativePath));
896
-	}
897
-
898
-	public function testPartFileInfo(): void {
899
-		$storage = new Temporary([]);
900
-		$scanner = $storage->getScanner();
901
-		Filesystem::mount($storage, [], '/test/');
902
-		$sizeWritten = $storage->file_put_contents('test.part', 'foobar');
903
-		$scanner->scan('');
904
-		$view = new View('/test');
905
-		$info = $view->getFileInfo('test.part');
906
-
907
-		$this->assertInstanceOf('\OCP\Files\FileInfo', $info);
908
-		$this->assertNull($info->getId());
909
-		$this->assertEquals(6, $sizeWritten);
910
-		$this->assertEquals(6, $info->getSize());
911
-		$this->assertEquals('foobar', $view->file_get_contents('test.part'));
912
-	}
913
-
914
-	public static function absolutePathProvider(): array {
915
-		return [
916
-			['/files', ''],
917
-			['/files/0', '0'],
918
-			['/files/false', 'false'],
919
-			['/files/true', 'true'],
920
-			['/files', '/'],
921
-			['/files/test', 'test'],
922
-			['/files/test', '/test'],
923
-		];
924
-	}
925
-
926
-	#[\PHPUnit\Framework\Attributes\DataProvider('chrootRelativePathProvider')]
927
-	public function testChrootGetRelativePath($root, $absolutePath, $expectedPath): void {
928
-		$view = new View('/files');
929
-		$view->chroot($root);
930
-		$this->assertEquals($expectedPath, $view->getRelativePath($absolutePath));
931
-	}
932
-
933
-	public static function chrootRelativePathProvider(): array {
934
-		return self::relativePathProvider('/');
935
-	}
936
-
937
-	#[\PHPUnit\Framework\Attributes\DataProvider('initRelativePathProvider')]
938
-	public function testInitGetRelativePath($root, $absolutePath, $expectedPath): void {
939
-		$view = new View($root);
940
-		$this->assertEquals($expectedPath, $view->getRelativePath($absolutePath));
941
-	}
942
-
943
-	public static function initRelativePathProvider(): array {
944
-		return self::relativePathProvider(null);
945
-	}
946
-
947
-	public static function relativePathProvider($missingRootExpectedPath): array {
948
-		return [
949
-			// No root - returns the path
950
-			['', '/files', '/files'],
951
-			['', '/files/', '/files/'],
952
-
953
-			// Root equals path - /
954
-			['/files/', '/files/', '/'],
955
-			['/files/', '/files', '/'],
956
-			['/files', '/files/', '/'],
957
-			['/files', '/files', '/'],
958
-
959
-			// False negatives: chroot fixes those by adding the leading slash.
960
-			// But setting them up with this root (instead of chroot($root))
961
-			// will fail them, although they should be the same.
962
-			// TODO init should be fixed, so it also adds the leading slash
963
-			['files/', '/files/', $missingRootExpectedPath],
964
-			['files', '/files/', $missingRootExpectedPath],
965
-			['files/', '/files', $missingRootExpectedPath],
966
-			['files', '/files', $missingRootExpectedPath],
967
-
968
-			// False negatives: Paths provided to the method should have a leading slash
969
-			// TODO input should be checked to have a leading slash
970
-			['/files/', 'files/', null],
971
-			['/files', 'files/', null],
972
-			['/files/', 'files', null],
973
-			['/files', 'files', null],
974
-
975
-			// with trailing slashes
976
-			['/files/', '/files/0', '0'],
977
-			['/files/', '/files/false', 'false'],
978
-			['/files/', '/files/true', 'true'],
979
-			['/files/', '/files/test', 'test'],
980
-			['/files/', '/files/test/foo', 'test/foo'],
981
-
982
-			// without trailing slashes
983
-			// TODO false expectation: Should match "with trailing slashes"
984
-			['/files', '/files/0', '/0'],
985
-			['/files', '/files/false', '/false'],
986
-			['/files', '/files/true', '/true'],
987
-			['/files', '/files/test', '/test'],
988
-			['/files', '/files/test/foo', '/test/foo'],
989
-
990
-			// leading slashes
991
-			['/files/', '/files_trashbin/', null],
992
-			['/files', '/files_trashbin/', null],
993
-			['/files/', '/files_trashbin', null],
994
-			['/files', '/files_trashbin', null],
995
-
996
-			// no leading slashes
997
-			['files/', 'files_trashbin/', null],
998
-			['files', 'files_trashbin/', null],
999
-			['files/', 'files_trashbin', null],
1000
-			['files', 'files_trashbin', null],
1001
-
1002
-			// mixed leading slashes
1003
-			['files/', '/files_trashbin/', null],
1004
-			['/files/', 'files_trashbin/', null],
1005
-			['files', '/files_trashbin/', null],
1006
-			['/files', 'files_trashbin/', null],
1007
-			['files/', '/files_trashbin', null],
1008
-			['/files/', 'files_trashbin', null],
1009
-			['files', '/files_trashbin', null],
1010
-			['/files', 'files_trashbin', null],
1011
-
1012
-			['files', 'files_trashbin/test', null],
1013
-			['/files', '/files_trashbin/test', null],
1014
-			['/files', 'files_trashbin/test', null],
1015
-		];
1016
-	}
1017
-
1018
-	public function testFileView(): void {
1019
-		$storage = new Temporary([]);
1020
-		$scanner = $storage->getScanner();
1021
-		$storage->file_put_contents('foo.txt', 'bar');
1022
-		Filesystem::mount($storage, [], '/test/');
1023
-		$scanner->scan('');
1024
-		$view = new View('/test/foo.txt');
1025
-
1026
-		$this->assertEquals('bar', $view->file_get_contents(''));
1027
-		$fh = tmpfile();
1028
-		fwrite($fh, 'foo');
1029
-		rewind($fh);
1030
-		$view->file_put_contents('', $fh);
1031
-		$this->assertEquals('foo', $view->file_get_contents(''));
1032
-	}
1033
-
1034
-	#[\PHPUnit\Framework\Attributes\DataProvider('tooLongPathDataProvider')]
1035
-	public function testTooLongPath($operation, $param0 = null): void {
1036
-		$this->expectException(InvalidPathException::class);
1037
-
1038
-
1039
-		$longPath = '';
1040
-		// 4000 is the maximum path length in file_cache.path
1041
-		$folderName = 'abcdefghijklmnopqrstuvwxyz012345678901234567890123456789';
1042
-		$depth = (4000 / 57);
1043
-		foreach (range(0, $depth + 1) as $i) {
1044
-			$longPath .= '/' . $folderName;
1045
-		}
1046
-
1047
-		$storage = new Temporary([]);
1048
-		$this->tempStorage = $storage; // for later hard cleanup
1049
-		Filesystem::mount($storage, [], '/');
1050
-
1051
-		$rootView = new View('');
1052
-
1053
-		if ($param0 === '@0') {
1054
-			$param0 = $longPath;
1055
-		}
1056
-
1057
-		if ($operation === 'hash') {
1058
-			$param0 = $longPath;
1059
-			$longPath = 'md5';
1060
-		}
1061
-
1062
-		call_user_func([$rootView, $operation], $longPath, $param0);
1063
-	}
1064
-
1065
-	public static function tooLongPathDataProvider(): array {
1066
-		return [
1067
-			['getAbsolutePath'],
1068
-			['getRelativePath'],
1069
-			['getMountPoint'],
1070
-			['resolvePath'],
1071
-			['getLocalFile'],
1072
-			['mkdir'],
1073
-			['rmdir'],
1074
-			['opendir'],
1075
-			['is_dir'],
1076
-			['is_file'],
1077
-			['stat'],
1078
-			['filetype'],
1079
-			['filesize'],
1080
-			['readfile'],
1081
-			['isCreatable'],
1082
-			['isReadable'],
1083
-			['isUpdatable'],
1084
-			['isDeletable'],
1085
-			['isSharable'],
1086
-			['file_exists'],
1087
-			['filemtime'],
1088
-			['touch'],
1089
-			['file_get_contents'],
1090
-			['unlink'],
1091
-			['deleteAll'],
1092
-			['toTmpFile'],
1093
-			['getMimeType'],
1094
-			['free_space'],
1095
-			['getFileInfo'],
1096
-			['getDirectoryContent'],
1097
-			['getOwner'],
1098
-			['getETag'],
1099
-			['file_put_contents', 'ipsum'],
1100
-			['rename', '@0'],
1101
-			['copy', '@0'],
1102
-			['fopen', 'r'],
1103
-			['fromTmpFile', '@0'],
1104
-			['hash'],
1105
-			['hasUpdated', 0],
1106
-			['putFileInfo', []],
1107
-		];
1108
-	}
1109
-
1110
-	public function testRenameCrossStoragePreserveMtime(): void {
1111
-		$storage1 = new Temporary([]);
1112
-		$storage2 = new Temporary([]);
1113
-		$storage1->mkdir('sub');
1114
-		$storage1->mkdir('foo');
1115
-		$storage1->file_put_contents('foo.txt', 'asd');
1116
-		$storage1->file_put_contents('foo/bar.txt', 'asd');
1117
-		Filesystem::mount($storage1, [], '/test/');
1118
-		Filesystem::mount($storage2, [], '/test/sub/storage');
1119
-
1120
-		$view = new View('');
1121
-		$time = time() - 200;
1122
-		$view->touch('/test/foo.txt', $time);
1123
-		$view->touch('/test/foo', $time);
1124
-		$view->touch('/test/foo/bar.txt', $time);
1125
-
1126
-		$view->rename('/test/foo.txt', '/test/sub/storage/foo.txt');
1127
-
1128
-		$this->assertEquals($time, $view->filemtime('/test/sub/storage/foo.txt'));
1129
-
1130
-		$view->rename('/test/foo', '/test/sub/storage/foo');
1131
-
1132
-		$this->assertEquals($time, $view->filemtime('/test/sub/storage/foo/bar.txt'));
1133
-	}
1134
-
1135
-	public function testRenameFailDeleteTargetKeepSource(): void {
1136
-		$this->doTestCopyRenameFail('rename');
1137
-	}
1138
-
1139
-	public function testCopyFailDeleteTargetKeepSource(): void {
1140
-		$this->doTestCopyRenameFail('copy');
1141
-	}
1142
-
1143
-	private function doTestCopyRenameFail($operation) {
1144
-		$storage1 = new Temporary([]);
1145
-		/** @var \PHPUnit\Framework\MockObject\MockObject|Temporary $storage2 */
1146
-		$storage2 = $this->getMockBuilder(TemporaryNoCross::class)
1147
-			->setConstructorArgs([[]])
1148
-			->onlyMethods(['fopen', 'writeStream'])
1149
-			->getMock();
1150
-
1151
-		$storage2->method('writeStream')
1152
-			->willThrowException(new GenericFileException('Failed to copy stream'));
1153
-
1154
-		$storage2->method('fopen')
1155
-			->willReturn(false);
1156
-
1157
-		$storage1->mkdir('sub');
1158
-		$storage1->file_put_contents('foo.txt', '0123456789ABCDEFGH');
1159
-		$storage1->mkdir('dirtomove');
1160
-		$storage1->file_put_contents('dirtomove/indir1.txt', '0123456'); // fits
1161
-		$storage1->file_put_contents('dirtomove/indir2.txt', '0123456789ABCDEFGH'); // doesn't fit
1162
-		$storage2->file_put_contents('existing.txt', '0123');
1163
-		$storage1->getScanner()->scan('');
1164
-		$storage2->getScanner()->scan('');
1165
-		Filesystem::mount($storage1, [], '/test/');
1166
-		Filesystem::mount($storage2, [], '/test/sub/storage');
1167
-
1168
-		// move file
1169
-		$view = new View('');
1170
-		$this->assertTrue($storage1->file_exists('foo.txt'));
1171
-		$this->assertFalse($storage2->file_exists('foo.txt'));
1172
-		$this->assertFalse($view->$operation('/test/foo.txt', '/test/sub/storage/foo.txt'));
1173
-		$this->assertFalse($storage2->file_exists('foo.txt'));
1174
-		$this->assertFalse($storage2->getCache()->get('foo.txt'));
1175
-		$this->assertTrue($storage1->file_exists('foo.txt'));
1176
-
1177
-		// if target exists, it will be deleted too
1178
-		$this->assertFalse($view->$operation('/test/foo.txt', '/test/sub/storage/existing.txt'));
1179
-		$this->assertFalse($storage2->file_exists('existing.txt'));
1180
-		$this->assertFalse($storage2->getCache()->get('existing.txt'));
1181
-		$this->assertTrue($storage1->file_exists('foo.txt'));
1182
-
1183
-		// move folder
1184
-		$this->assertFalse($view->$operation('/test/dirtomove/', '/test/sub/storage/dirtomove/'));
1185
-		// since the move failed, the full source tree is kept
1186
-		$this->assertTrue($storage1->file_exists('dirtomove/indir1.txt'));
1187
-		$this->assertTrue($storage1->file_exists('dirtomove/indir2.txt'));
1188
-		// second file not moved/copied
1189
-		$this->assertFalse($storage2->file_exists('dirtomove/indir2.txt'));
1190
-		$this->assertFalse($storage2->getCache()->get('dirtomove/indir2.txt'));
1191
-	}
1192
-
1193
-	public function testDeleteFailKeepCache(): void {
1194
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
1195
-		$storage = $this->getMockBuilder(Temporary::class)
1196
-			->setConstructorArgs([[]])
1197
-			->onlyMethods(['unlink'])
1198
-			->getMock();
1199
-		$storage->expects($this->once())
1200
-			->method('unlink')
1201
-			->willReturn(false);
1202
-		$scanner = $storage->getScanner();
1203
-		$cache = $storage->getCache();
1204
-		$storage->file_put_contents('foo.txt', 'asd');
1205
-		$scanner->scan('');
1206
-		Filesystem::mount($storage, [], '/test/');
1207
-
1208
-		$view = new View('/test');
1209
-
1210
-		$this->assertFalse($view->unlink('foo.txt'));
1211
-		$this->assertTrue($cache->inCache('foo.txt'));
1212
-	}
1213
-
1214
-	public static function directoryTraversalProvider(): array {
1215
-		return [
1216
-			['../test/'],
1217
-			['..\\test\\my/../folder'],
1218
-			['/test/my/../foo\\'],
1219
-		];
1220
-	}
1221
-
1222
-	/**
1223
-	 * @param string $root
1224
-	 */
1225
-	#[\PHPUnit\Framework\Attributes\DataProvider('directoryTraversalProvider')]
1226
-	public function testConstructDirectoryTraversalException($root): void {
1227
-		$this->expectException(\Exception::class);
1228
-
1229
-		new View($root);
1230
-	}
1231
-
1232
-	public function testRenameOverWrite(): void {
1233
-		$storage = new Temporary([]);
1234
-		$scanner = $storage->getScanner();
1235
-		$storage->mkdir('sub');
1236
-		$storage->mkdir('foo');
1237
-		$storage->file_put_contents('foo.txt', 'asd');
1238
-		$storage->file_put_contents('foo/bar.txt', 'asd');
1239
-		$scanner->scan('');
1240
-		Filesystem::mount($storage, [], '/test/');
1241
-		$view = new View('');
1242
-		$this->assertTrue($view->rename('/test/foo.txt', '/test/foo/bar.txt'));
1243
-	}
1244
-
1245
-	public function testSetMountOptionsInStorage(): void {
1246
-		$mount = new MountPoint(Temporary::class, '/asd/', [[]], Filesystem::getLoader(), ['foo' => 'bar']);
1247
-		Filesystem::getMountManager()->addMount($mount);
1248
-		/** @var Common $storage */
1249
-		$storage = $mount->getStorage();
1250
-		$this->assertEquals($storage->getMountOption('foo'), 'bar');
1251
-	}
1252
-
1253
-	public function testSetMountOptionsWatcherPolicy(): void {
1254
-		$mount = new MountPoint(Temporary::class, '/asd/', [[]], Filesystem::getLoader(), ['filesystem_check_changes' => Watcher::CHECK_NEVER]);
1255
-		Filesystem::getMountManager()->addMount($mount);
1256
-		/** @var Common $storage */
1257
-		$storage = $mount->getStorage();
1258
-		$watcher = $storage->getWatcher();
1259
-		$this->assertEquals(Watcher::CHECK_NEVER, $watcher->getPolicy());
1260
-	}
1261
-
1262
-	public function testGetAbsolutePathOnNull(): void {
1263
-		$view = new View();
1264
-		$this->assertNull($view->getAbsolutePath(null));
1265
-	}
1266
-
1267
-	public function testGetRelativePathOnNull(): void {
1268
-		$view = new View();
1269
-		$this->assertNull($view->getRelativePath(null));
1270
-	}
1271
-
1272
-
1273
-	public function testNullAsRoot(): void {
1274
-		$this->expectException(\TypeError::class);
1275
-
1276
-		new View(null);
1277
-	}
1278
-
1279
-	/**
1280
-	 * e.g. reading from a folder that's being renamed
1281
-	 *
1282
-	 *
1283
-	 *
1284
-	 * @param string $rootPath
1285
-	 * @param string $pathPrefix
1286
-	 */
1287
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')]
1288
-	public function testReadFromWriteLockedPath($rootPath, $pathPrefix): void {
1289
-		$this->expectException(LockedException::class);
1290
-
1291
-		$rootPath = str_replace('{folder}', 'files', $rootPath);
1292
-		$pathPrefix = str_replace('{folder}', 'files', $pathPrefix);
1293
-
1294
-		$view = new View($rootPath);
1295
-		$storage = new Temporary([]);
1296
-		Filesystem::mount($storage, [], '/');
1297
-		$this->assertTrue($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1298
-		$view->lockFile($pathPrefix . '/foo/bar/asd', ILockingProvider::LOCK_SHARED);
1299
-	}
1300
-
1301
-	/**
1302
-	 * Reading from a files_encryption folder that's being renamed
1303
-	 *
1304
-	 *
1305
-	 * @param string $rootPath
1306
-	 * @param string $pathPrefix
1307
-	 */
1308
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')]
1309
-	public function testReadFromWriteUnlockablePath($rootPath, $pathPrefix): void {
1310
-		$rootPath = str_replace('{folder}', 'files_encryption', $rootPath);
1311
-		$pathPrefix = str_replace('{folder}', 'files_encryption', $pathPrefix);
1312
-
1313
-		$view = new View($rootPath);
1314
-		$storage = new Temporary([]);
1315
-		Filesystem::mount($storage, [], '/');
1316
-		$this->assertFalse($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1317
-		$this->assertFalse($view->lockFile($pathPrefix . '/foo/bar/asd', ILockingProvider::LOCK_SHARED));
1318
-	}
1319
-
1320
-	/**
1321
-	 * e.g. writing a file that's being downloaded
1322
-	 *
1323
-	 *
1324
-	 *
1325
-	 * @param string $rootPath
1326
-	 * @param string $pathPrefix
1327
-	 */
1328
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')]
1329
-	public function testWriteToReadLockedFile($rootPath, $pathPrefix): void {
1330
-		$this->expectException(LockedException::class);
1331
-
1332
-		$rootPath = str_replace('{folder}', 'files', $rootPath);
1333
-		$pathPrefix = str_replace('{folder}', 'files', $pathPrefix);
1334
-
1335
-		$view = new View($rootPath);
1336
-		$storage = new Temporary([]);
1337
-		Filesystem::mount($storage, [], '/');
1338
-		$this->assertTrue($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_SHARED));
1339
-		$view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE);
1340
-	}
1341
-
1342
-	/**
1343
-	 * Writing a file that's being downloaded
1344
-	 *
1345
-	 *
1346
-	 * @param string $rootPath
1347
-	 * @param string $pathPrefix
1348
-	 */
1349
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')]
1350
-	public function testWriteToReadUnlockableFile($rootPath, $pathPrefix): void {
1351
-		$rootPath = str_replace('{folder}', 'files_encryption', $rootPath);
1352
-		$pathPrefix = str_replace('{folder}', 'files_encryption', $pathPrefix);
1353
-
1354
-		$view = new View($rootPath);
1355
-		$storage = new Temporary([]);
1356
-		Filesystem::mount($storage, [], '/');
1357
-		$this->assertFalse($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_SHARED));
1358
-		$this->assertFalse($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1359
-	}
1360
-
1361
-	/**
1362
-	 * Test that locks are on mount point paths instead of mount root
1363
-	 */
1364
-	public function testLockLocalMountPointPathInsteadOfStorageRoot(): void {
1365
-		$lockingProvider = Server::get(ILockingProvider::class);
1366
-		$view = new View('/testuser/files/');
1367
-		$storage = new Temporary([]);
1368
-		Filesystem::mount($storage, [], '/');
1369
-		$mountedStorage = new Temporary([]);
1370
-		Filesystem::mount($mountedStorage, [], '/testuser/files/mountpoint');
1371
-
1372
-		$this->assertTrue(
1373
-			$view->lockFile('/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, true),
1374
-			'Can lock mount point'
1375
-		);
1376
-
1377
-		// no exception here because storage root was not locked
1378
-		$mountedStorage->acquireLock('', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1379
-
1380
-		$thrown = false;
1381
-		try {
1382
-			$storage->acquireLock('/testuser/files/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1383
-		} catch (LockedException $e) {
1384
-			$thrown = true;
1385
-		}
1386
-		$this->assertTrue($thrown, 'Mount point path was locked on root storage');
1387
-
1388
-		$lockingProvider->releaseAll();
1389
-	}
1390
-
1391
-	/**
1392
-	 * Test that locks are on mount point paths and also mount root when requested
1393
-	 */
1394
-	public function testLockStorageRootButNotLocalMountPoint(): void {
1395
-		$lockingProvider = Server::get(ILockingProvider::class);
1396
-		$view = new View('/testuser/files/');
1397
-		$storage = new Temporary([]);
1398
-		Filesystem::mount($storage, [], '/');
1399
-		$mountedStorage = new Temporary([]);
1400
-		Filesystem::mount($mountedStorage, [], '/testuser/files/mountpoint');
1401
-
1402
-		$this->assertTrue(
1403
-			$view->lockFile('/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, false),
1404
-			'Can lock mount point'
1405
-		);
1406
-
1407
-		$thrown = false;
1408
-		try {
1409
-			$mountedStorage->acquireLock('', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1410
-		} catch (LockedException $e) {
1411
-			$thrown = true;
1412
-		}
1413
-		$this->assertTrue($thrown, 'Mount point storage root was locked on original storage');
1414
-
1415
-		// local mount point was not locked
1416
-		$storage->acquireLock('/testuser/files/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1417
-
1418
-		$lockingProvider->releaseAll();
1419
-	}
1420
-
1421
-	/**
1422
-	 * Test that locks are on mount point paths and also mount root when requested
1423
-	 */
1424
-	public function testLockMountPointPathFailReleasesBoth(): void {
1425
-		$lockingProvider = Server::get(ILockingProvider::class);
1426
-		$view = new View('/testuser/files/');
1427
-		$storage = new Temporary([]);
1428
-		Filesystem::mount($storage, [], '/');
1429
-		$mountedStorage = new Temporary([]);
1430
-		Filesystem::mount($mountedStorage, [], '/testuser/files/mountpoint.txt');
1431
-
1432
-		// this would happen if someone is writing on the mount point
1433
-		$mountedStorage->acquireLock('', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1434
-
1435
-		$thrown = false;
1436
-		try {
1437
-			// this actually acquires two locks, one on the mount point and one on the storage root,
1438
-			// but the one on the storage root will fail
1439
-			$view->lockFile('/mountpoint.txt', ILockingProvider::LOCK_SHARED);
1440
-		} catch (LockedException $e) {
1441
-			$thrown = true;
1442
-		}
1443
-		$this->assertTrue($thrown, 'Cannot acquire shared lock because storage root is already locked');
1444
-
1445
-		// from here we expect that the lock on the local mount point was released properly
1446
-		// so acquiring an exclusive lock will succeed
1447
-		$storage->acquireLock('/testuser/files/mountpoint.txt', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1448
-
1449
-		$lockingProvider->releaseAll();
1450
-	}
1451
-
1452
-	public static function dataLockPaths(): array {
1453
-		return [
1454
-			['/testuser/{folder}', ''],
1455
-			['/testuser', '/{folder}'],
1456
-			['', '/testuser/{folder}'],
1457
-		];
1458
-	}
1459
-
1460
-	public static function pathRelativeToFilesProvider(): array {
1461
-		return [
1462
-			['admin/files', ''],
1463
-			['admin/files/x', 'x'],
1464
-			['/admin/files', ''],
1465
-			['/admin/files/sub', 'sub'],
1466
-			['/admin/files/sub/', 'sub'],
1467
-			['/admin/files/sub/sub2', 'sub/sub2'],
1468
-			['//admin//files/sub//sub2', 'sub/sub2'],
1469
-		];
1470
-	}
1471
-
1472
-	#[\PHPUnit\Framework\Attributes\DataProvider('pathRelativeToFilesProvider')]
1473
-	public function testGetPathRelativeToFiles($path, $expectedPath): void {
1474
-		$view = new View();
1475
-		$this->assertEquals($expectedPath, $view->getPathRelativeToFiles($path));
1476
-	}
1477
-
1478
-	public static function pathRelativeToFilesProviderExceptionCases(): array {
1479
-		return [
1480
-			[''],
1481
-			['x'],
1482
-			['files'],
1483
-			['/files'],
1484
-			['/admin/files_versions/abc'],
1485
-		];
1486
-	}
1487
-
1488
-	/**
1489
-	 * @param string $path
1490
-	 */
1491
-	#[\PHPUnit\Framework\Attributes\DataProvider('pathRelativeToFilesProviderExceptionCases')]
1492
-	public function testGetPathRelativeToFilesWithInvalidArgument($path): void {
1493
-		$this->expectException(\InvalidArgumentException::class);
1494
-		$this->expectExceptionMessage('$absolutePath must be relative to "files"');
1495
-
1496
-		$view = new View();
1497
-		$view->getPathRelativeToFiles($path);
1498
-	}
1499
-
1500
-	public function testChangeLock(): void {
1501
-		$view = new View('/testuser/files/');
1502
-		$storage = new Temporary([]);
1503
-		Filesystem::mount($storage, [], '/');
1504
-
1505
-		$view->lockFile('/test/sub', ILockingProvider::LOCK_SHARED);
1506
-		$this->assertTrue($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_SHARED));
1507
-		$this->assertFalse($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_EXCLUSIVE));
1508
-
1509
-		$view->changeLock('//test/sub', ILockingProvider::LOCK_EXCLUSIVE);
1510
-		$this->assertTrue($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_EXCLUSIVE));
1511
-
1512
-		$view->changeLock('test/sub', ILockingProvider::LOCK_SHARED);
1513
-		$this->assertTrue($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_SHARED));
1514
-
1515
-		$view->unlockFile('/test/sub/', ILockingProvider::LOCK_SHARED);
1516
-
1517
-		$this->assertFalse($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_SHARED));
1518
-		$this->assertFalse($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_EXCLUSIVE));
1519
-	}
1520
-
1521
-	public static function hookPathProvider(): array {
1522
-		return [
1523
-			['/foo/files', '/foo', true],
1524
-			['/foo/files/bar', '/foo', true],
1525
-			['/foo', '/foo', false],
1526
-			['/foo', '/files/foo', true],
1527
-			['/foo', 'filesfoo', false],
1528
-			['', '/foo/files', true],
1529
-			['', '/foo/files/bar.txt', true],
1530
-		];
1531
-	}
1532
-
1533
-	/**
1534
-	 * @param $root
1535
-	 * @param $path
1536
-	 * @param $shouldEmit
1537
-	 */
1538
-	#[\PHPUnit\Framework\Attributes\DataProvider('hookPathProvider')]
1539
-	public function testHookPaths($root, $path, $shouldEmit): void {
1540
-		$filesystemReflection = new \ReflectionClass(Filesystem::class);
1541
-		$defaultRootValue = $filesystemReflection->getProperty('defaultInstance');
1542
-		$defaultRootValue->setAccessible(true);
1543
-		$oldRoot = $defaultRootValue->getValue();
1544
-		$defaultView = new View('/foo/files');
1545
-		$defaultRootValue->setValue(null, $defaultView);
1546
-		$view = new View($root);
1547
-		$result = self::invokePrivate($view, 'shouldEmitHooks', [$path]);
1548
-		$defaultRootValue->setValue(null, $oldRoot);
1549
-		$this->assertEquals($shouldEmit, $result);
1550
-	}
1551
-
1552
-	/**
1553
-	 * Create test movable mount points
1554
-	 *
1555
-	 * @param array $mountPoints array of mount point locations
1556
-	 * @return array array of MountPoint objects
1557
-	 */
1558
-	private function createTestMovableMountPoints($mountPoints) {
1559
-		$mounts = [];
1560
-		foreach ($mountPoints as $mountPoint) {
1561
-			$storage = $this->getMockBuilder(Storage::class)
1562
-				->onlyMethods([])
1563
-				->getMock();
1564
-			$storage->method('getId')->willReturn('non-null-id');
1565
-			$storage->method('getStorageCache')->willReturnCallback(function () use ($storage) {
1566
-				return new \OC\Files\Cache\Storage($storage, true, Server::get(IDBConnection::class));
1567
-			});
1568
-
1569
-			$mounts[] = $this->getMockBuilder(TestMoveableMountPoint::class)
1570
-				->onlyMethods(['moveMount'])
1571
-				->setConstructorArgs([$storage, $mountPoint])
1572
-				->getMock();
1573
-		}
1574
-
1575
-		/** @var IMountProvider|\PHPUnit\Framework\MockObject\MockObject $mountProvider */
1576
-		$mountProvider = $this->createMock(IMountProvider::class);
1577
-		$mountProvider->expects($this->any())
1578
-			->method('getMountsForUser')
1579
-			->willReturn($mounts);
1580
-
1581
-		$mountProviderCollection = Server::get(IMountProviderCollection::class);
1582
-		$mountProviderCollection->registerProvider($mountProvider);
1583
-
1584
-		return $mounts;
1585
-	}
1586
-
1587
-	/**
1588
-	 * Test mount point move
1589
-	 */
1590
-	public function testMountPointMove(): void {
1591
-		self::loginAsUser($this->user);
1592
-
1593
-		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1594
-			$this->user . '/files/mount1',
1595
-			$this->user . '/files/mount2',
1596
-		]);
1597
-		$mount1->expects($this->once())
1598
-			->method('moveMount')
1599
-			->willReturn(true);
1600
-
1601
-		$mount2->expects($this->once())
1602
-			->method('moveMount')
1603
-			->willReturn(true);
1604
-
1605
-		$view = new View('/' . $this->user . '/files/');
1606
-		$view->mkdir('sub');
1607
-
1608
-		$this->assertTrue($view->rename('mount1', 'renamed_mount'), 'Can rename mount point');
1609
-		$this->assertTrue($view->rename('mount2', 'sub/moved_mount'), 'Can move a mount point into a subdirectory');
1610
-	}
1611
-
1612
-	public function testMoveMountPointOverwrite(): void {
1613
-		self::loginAsUser($this->user);
1614
-
1615
-		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1616
-			$this->user . '/files/mount1',
1617
-			$this->user . '/files/mount2',
1618
-		]);
1619
-
1620
-		$mount1->expects($this->never())
1621
-			->method('moveMount');
1622
-
1623
-		$mount2->expects($this->never())
1624
-			->method('moveMount');
1625
-
1626
-		$view = new View('/' . $this->user . '/files/');
1627
-
1628
-		$this->expectException(ForbiddenException::class);
1629
-		$view->rename('mount1', 'mount2');
1630
-	}
1631
-
1632
-	public function testMoveMountPointIntoMount(): void {
1633
-		self::loginAsUser($this->user);
1634
-
1635
-		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1636
-			$this->user . '/files/mount1',
1637
-			$this->user . '/files/mount2',
1638
-		]);
1639
-
1640
-		$mount1->expects($this->never())
1641
-			->method('moveMount');
1642
-
1643
-		$mount2->expects($this->never())
1644
-			->method('moveMount');
1645
-
1646
-		$view = new View('/' . $this->user . '/files/');
1647
-
1648
-		$this->expectException(ForbiddenException::class);
1649
-		$view->rename('mount1', 'mount2/sub');
1650
-	}
1651
-
1652
-	/**
1653
-	 * Test that moving a mount point into a shared folder is forbidden
1654
-	 */
1655
-	public function testMoveMountPointIntoSharedFolder(): void {
1656
-		self::loginAsUser($this->user);
1657
-
1658
-		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1659
-			$this->user . '/files/mount1',
1660
-			$this->user . '/files/mount2',
1661
-		]);
1662
-
1663
-		$mount1->expects($this->never())
1664
-			->method('moveMount');
1665
-
1666
-		$mount2->expects($this->once())
1667
-			->method('moveMount')
1668
-			->willReturn(true);
1669
-
1670
-		$view = new View('/' . $this->user . '/files/');
1671
-		$view->mkdir('shareddir');
1672
-		$view->mkdir('shareddir/sub');
1673
-		$view->mkdir('shareddir/sub2');
1674
-		// Create a similar named but non-shared folder
1675
-		$view->mkdir('shareddir notshared');
1676
-
1677
-		$fileId = $view->getFileInfo('shareddir')->getId();
1678
-		$userObject = Server::get(IUserManager::class)->createUser('test2', 'IHateNonMockableStaticClasses');
1679
-
1680
-		$userFolder = \OC::$server->getUserFolder($this->user);
1681
-		$shareDir = $userFolder->get('shareddir');
1682
-		$shareManager = Server::get(IShareManager::class);
1683
-		$share = $shareManager->newShare();
1684
-		$share->setSharedWith('test2')
1685
-			->setSharedBy($this->user)
1686
-			->setShareType(IShare::TYPE_USER)
1687
-			->setPermissions(Constants::PERMISSION_READ)
1688
-			->setNode($shareDir);
1689
-		$shareManager->createShare($share);
1690
-
1691
-		try {
1692
-			$view->rename('mount1', 'shareddir');
1693
-			$this->fail('Cannot overwrite shared folder');
1694
-		} catch (ForbiddenException $e) {
1695
-
1696
-		}
1697
-		try {
1698
-			$view->rename('mount1', 'shareddir/sub');
1699
-			$this->fail('Cannot move mount point into shared folder');
1700
-		} catch (ForbiddenException $e) {
1701
-
1702
-		}
1703
-		try {
1704
-			$view->rename('mount1', 'shareddir/sub/sub2');
1705
-			$this->fail('Cannot move mount point into shared subfolder');
1706
-		} catch (ForbiddenException $e) {
1707
-
1708
-		}
1709
-		$this->assertTrue($view->rename('mount2', 'shareddir notshared/sub'), 'Can move mount point into a similarly named but non-shared folder');
1710
-
1711
-		$shareManager->deleteShare($share);
1712
-		$userObject->delete();
1713
-	}
1714
-
1715
-	public static function basicOperationProviderForLocks(): array {
1716
-		return [
1717
-			// --- write hook ----
1718
-			[
1719
-				'touch',
1720
-				['touch-create.txt'],
1721
-				'touch-create.txt',
1722
-				'create',
1723
-				ILockingProvider::LOCK_SHARED,
1724
-				ILockingProvider::LOCK_EXCLUSIVE,
1725
-				ILockingProvider::LOCK_SHARED,
1726
-			],
1727
-			[
1728
-				'fopen',
1729
-				['test-write.txt', 'w'],
1730
-				'test-write.txt',
1731
-				'write',
1732
-				ILockingProvider::LOCK_SHARED,
1733
-				ILockingProvider::LOCK_EXCLUSIVE,
1734
-				null,
1735
-				// exclusive lock stays until fclose
1736
-				ILockingProvider::LOCK_EXCLUSIVE,
1737
-			],
1738
-			[
1739
-				'mkdir',
1740
-				['newdir'],
1741
-				'newdir',
1742
-				'write',
1743
-				ILockingProvider::LOCK_SHARED,
1744
-				ILockingProvider::LOCK_EXCLUSIVE,
1745
-				ILockingProvider::LOCK_SHARED,
1746
-			],
1747
-			[
1748
-				'file_put_contents',
1749
-				['file_put_contents.txt', 'blah'],
1750
-				'file_put_contents.txt',
1751
-				'write',
1752
-				ILockingProvider::LOCK_SHARED,
1753
-				ILockingProvider::LOCK_EXCLUSIVE,
1754
-				ILockingProvider::LOCK_SHARED,
1755
-				null,
1756
-				0,
1757
-			],
1758
-
1759
-			// ---- delete hook ----
1760
-			[
1761
-				'rmdir',
1762
-				['dir'],
1763
-				'dir',
1764
-				'delete',
1765
-				ILockingProvider::LOCK_SHARED,
1766
-				ILockingProvider::LOCK_EXCLUSIVE,
1767
-				ILockingProvider::LOCK_SHARED,
1768
-			],
1769
-			[
1770
-				'unlink',
1771
-				['test.txt'],
1772
-				'test.txt',
1773
-				'delete',
1774
-				ILockingProvider::LOCK_SHARED,
1775
-				ILockingProvider::LOCK_EXCLUSIVE,
1776
-				ILockingProvider::LOCK_SHARED,
1777
-			],
1778
-
1779
-			// ---- read hook (no post hooks) ----
1780
-			[
1781
-				'file_get_contents',
1782
-				['test.txt'],
1783
-				'test.txt',
1784
-				'read',
1785
-				ILockingProvider::LOCK_SHARED,
1786
-				ILockingProvider::LOCK_SHARED,
1787
-				null,
1788
-				null,
1789
-				false,
1790
-			],
1791
-			[
1792
-				'fopen',
1793
-				['test.txt', 'r'],
1794
-				'test.txt',
1795
-				'read',
1796
-				ILockingProvider::LOCK_SHARED,
1797
-				ILockingProvider::LOCK_SHARED,
1798
-				null,
1799
-			],
1800
-			[
1801
-				'opendir',
1802
-				['dir'],
1803
-				'dir',
1804
-				'read',
1805
-				ILockingProvider::LOCK_SHARED,
1806
-				ILockingProvider::LOCK_SHARED,
1807
-				null,
1808
-			],
1809
-
1810
-			// ---- no lock, touch hook ---
1811
-			['touch', ['test.txt'], 'test.txt', 'touch', null, null, null],
1812
-
1813
-			// ---- no hooks, no locks ---
1814
-			['is_dir', ['dir'], 'dir', null],
1815
-			['is_file', ['dir'], 'dir', null],
1816
-			[
1817
-				'stat',
1818
-				['dir'],
1819
-				'dir',
1820
-				null,
1821
-				ILockingProvider::LOCK_SHARED,
1822
-				ILockingProvider::LOCK_SHARED,
1823
-				ILockingProvider::LOCK_SHARED,
1824
-				null,
1825
-				false,
1826
-			],
1827
-			[
1828
-				'filetype',
1829
-				['dir'],
1830
-				'dir',
1831
-				null,
1832
-				ILockingProvider::LOCK_SHARED,
1833
-				ILockingProvider::LOCK_SHARED,
1834
-				ILockingProvider::LOCK_SHARED,
1835
-				null,
1836
-				false,
1837
-			],
1838
-			[
1839
-				'filesize',
1840
-				['dir'],
1841
-				'dir',
1842
-				null,
1843
-				ILockingProvider::LOCK_SHARED,
1844
-				ILockingProvider::LOCK_SHARED,
1845
-				ILockingProvider::LOCK_SHARED,
1846
-				null,
1847
-				/* Return an int */
1848
-				100
1849
-			],
1850
-			['isCreatable', ['dir'], 'dir', null],
1851
-			['isReadable', ['dir'], 'dir', null],
1852
-			['isUpdatable', ['dir'], 'dir', null],
1853
-			['isDeletable', ['dir'], 'dir', null],
1854
-			['isSharable', ['dir'], 'dir', null],
1855
-			['file_exists', ['dir'], 'dir', null],
1856
-			[
1857
-				'filemtime',
1858
-				['dir'],
1859
-				'dir',
1860
-				null,
1861
-				ILockingProvider::LOCK_SHARED,
1862
-				ILockingProvider::LOCK_SHARED,
1863
-				ILockingProvider::LOCK_SHARED,
1864
-				null,
1865
-				false,
1866
-			],
1867
-		];
1868
-	}
1869
-
1870
-	/**
1871
-	 * Test whether locks are set before and after the operation
1872
-	 *
1873
-	 *
1874
-	 * @param string $operation operation name on the view
1875
-	 * @param array $operationArgs arguments for the operation
1876
-	 * @param string $lockedPath path of the locked item to check
1877
-	 * @param string $hookType hook type
1878
-	 * @param int $expectedLockBefore expected lock during pre hooks
1879
-	 * @param int $expectedLockDuring expected lock during operation
1880
-	 * @param int $expectedLockAfter expected lock during post hooks
1881
-	 * @param int $expectedStrayLock expected lock after returning, should
1882
-	 *                               be null (unlock) for most operations
1883
-	 */
1884
-	#[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')]
1885
-	public function testLockBasicOperation(
1886
-		$operation,
1887
-		$operationArgs,
1888
-		$lockedPath,
1889
-		$hookType,
1890
-		$expectedLockBefore = ILockingProvider::LOCK_SHARED,
1891
-		$expectedLockDuring = ILockingProvider::LOCK_SHARED,
1892
-		$expectedLockAfter = ILockingProvider::LOCK_SHARED,
1893
-		$expectedStrayLock = null,
1894
-		$returnValue = true,
1895
-	): void {
1896
-		$view = new View('/' . $this->user . '/files/');
1897
-
1898
-		/** @var Temporary&MockObject $storage */
1899
-		$storage = $this->getMockBuilder(Temporary::class)
1900
-			->onlyMethods([$operation])
1901
-			->getMock();
1902
-
1903
-		/* Pause trash to avoid the trashbin intercepting rmdir and unlink calls */
1904
-		Server::get(ITrashManager::class)->pauseTrash();
1905
-		/* Same thing with encryption wrapper */
1906
-		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
1907
-
1908
-		Filesystem::mount($storage, [], $this->user . '/');
1909
-
1910
-		// work directly on disk because mkdir might be mocked
1911
-		$realPath = $storage->getSourcePath('');
1912
-		mkdir($realPath . '/files');
1913
-		mkdir($realPath . '/files/dir');
1914
-		file_put_contents($realPath . '/files/test.txt', 'blah');
1915
-		$storage->getScanner()->scan('files');
1916
-
1917
-		$storage->expects($this->once())
1918
-			->method($operation)
1919
-			->willReturnCallback(
1920
-				function () use ($view, $lockedPath, &$lockTypeDuring, $returnValue) {
1921
-					$lockTypeDuring = $this->getFileLockType($view, $lockedPath);
1922
-
1923
-					return $returnValue;
1924
-				}
1925
-			);
1926
-
1927
-		$this->assertNull($this->getFileLockType($view, $lockedPath), 'File not locked before operation');
1928
-
1929
-		$this->connectMockHooks($hookType, $view, $lockedPath, $lockTypePre, $lockTypePost);
1930
-
1931
-		// do operation
1932
-		call_user_func_array([$view, $operation], $operationArgs);
1933
-
1934
-		if ($hookType !== null) {
1935
-			$this->assertEquals($expectedLockBefore, $lockTypePre, 'File locked properly during pre-hook');
1936
-			$this->assertEquals($expectedLockAfter, $lockTypePost, 'File locked properly during post-hook');
1937
-			$this->assertEquals($expectedLockDuring, $lockTypeDuring, 'File locked properly during operation');
1938
-		} else {
1939
-			$this->assertNull($lockTypeDuring, 'File not locked during operation');
1940
-		}
1941
-
1942
-		$this->assertEquals($expectedStrayLock, $this->getFileLockType($view, $lockedPath));
1943
-
1944
-		/* Resume trash to avoid side effects */
1945
-		Server::get(ITrashManager::class)->resumeTrash();
1946
-	}
1947
-
1948
-	/**
1949
-	 * Test locks for file_put_content with stream.
1950
-	 * This code path uses $storage->fopen instead
1951
-	 */
1952
-	public function testLockFilePutContentWithStream(): void {
1953
-		$view = new View('/' . $this->user . '/files/');
1954
-
1955
-		$path = 'test_file_put_contents.txt';
1956
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
1957
-		$storage = $this->getMockBuilder(Temporary::class)
1958
-			->onlyMethods(['fopen'])
1959
-			->getMock();
1960
-
1961
-		Filesystem::mount($storage, [], $this->user . '/');
1962
-		$storage->mkdir('files');
1963
-
1964
-		$storage->expects($this->once())
1965
-			->method('fopen')
1966
-			->willReturnCallback(
1967
-				function () use ($view, $path, &$lockTypeDuring) {
1968
-					$lockTypeDuring = $this->getFileLockType($view, $path);
1969
-
1970
-					return fopen('php://temp', 'r+');
1971
-				}
1972
-			);
1973
-
1974
-		$this->connectMockHooks('write', $view, $path, $lockTypePre, $lockTypePost);
1975
-
1976
-		$this->assertNull($this->getFileLockType($view, $path), 'File not locked before operation');
1977
-
1978
-		// do operation
1979
-		$view->file_put_contents($path, fopen('php://temp', 'r+'));
1980
-
1981
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePre, 'File locked properly during pre-hook');
1982
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePost, 'File locked properly during post-hook');
1983
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File locked properly during operation');
1984
-
1985
-		$this->assertNull($this->getFileLockType($view, $path));
1986
-	}
1987
-
1988
-	/**
1989
-	 * Test locks for fopen with fclose at the end
1990
-	 */
1991
-	public function testLockFopen(): void {
1992
-		$view = new View('/' . $this->user . '/files/');
1993
-
1994
-		$path = 'test_file_put_contents.txt';
1995
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
1996
-		$storage = $this->getMockBuilder(Temporary::class)
1997
-			->onlyMethods(['fopen'])
1998
-			->getMock();
1999
-
2000
-		Filesystem::mount($storage, [], $this->user . '/');
2001
-		$storage->mkdir('files');
2002
-
2003
-		$storage->expects($this->once())
2004
-			->method('fopen')
2005
-			->willReturnCallback(
2006
-				function () use ($view, $path, &$lockTypeDuring) {
2007
-					$lockTypeDuring = $this->getFileLockType($view, $path);
2008
-
2009
-					return fopen('php://temp', 'r+');
2010
-				}
2011
-			);
2012
-
2013
-		$this->connectMockHooks('write', $view, $path, $lockTypePre, $lockTypePost);
2014
-
2015
-		$this->assertNull($this->getFileLockType($view, $path), 'File not locked before operation');
2016
-
2017
-		// do operation
2018
-		$res = $view->fopen($path, 'w');
2019
-
2020
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePre, 'File locked properly during pre-hook');
2021
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File locked properly during operation');
2022
-		$this->assertNull($lockTypePost, 'No post hook, no lock check possible');
2023
-
2024
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File still locked after fopen');
2025
-
2026
-		fclose($res);
2027
-
2028
-		$this->assertNull($this->getFileLockType($view, $path), 'File unlocked after fclose');
2029
-	}
2030
-
2031
-	/**
2032
-	 * Test locks for fopen with fclose at the end
2033
-	 *
2034
-	 *
2035
-	 * @param string $operation operation name on the view
2036
-	 * @param array $operationArgs arguments for the operation
2037
-	 * @param string $path path of the locked item to check
2038
-	 */
2039
-	#[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')]
2040
-	public function testLockBasicOperationUnlocksAfterException(
2041
-		$operation,
2042
-		$operationArgs,
2043
-		$path,
2044
-	): void {
2045
-		if ($operation === 'touch') {
2046
-			$this->markTestSkipped('touch handles storage exceptions internally');
2047
-		}
2048
-		$view = new View('/' . $this->user . '/files/');
2049
-
2050
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2051
-		$storage = $this->getMockBuilder(Temporary::class)
2052
-			->onlyMethods([$operation])
2053
-			->getMock();
2054
-
2055
-		/* Pause trash to avoid the trashbin intercepting rmdir and unlink calls */
2056
-		Server::get(ITrashManager::class)->pauseTrash();
2057
-		/* Same thing with encryption wrapper */
2058
-		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2059
-
2060
-		Filesystem::mount($storage, [], $this->user . '/');
2061
-
2062
-		// work directly on disk because mkdir might be mocked
2063
-		$realPath = $storage->getSourcePath('');
2064
-		mkdir($realPath . '/files');
2065
-		mkdir($realPath . '/files/dir');
2066
-		file_put_contents($realPath . '/files/test.txt', 'blah');
2067
-		$storage->getScanner()->scan('files');
2068
-
2069
-		$storage->expects($this->once())
2070
-			->method($operation)
2071
-			->willReturnCallback(
2072
-				function (): void {
2073
-					throw new \Exception('Simulated exception');
2074
-				}
2075
-			);
2076
-
2077
-		$thrown = false;
2078
-		try {
2079
-			call_user_func_array([$view, $operation], $operationArgs);
2080
-		} catch (\Exception $e) {
2081
-			$thrown = true;
2082
-			$this->assertEquals('Simulated exception', $e->getMessage());
2083
-		}
2084
-		$this->assertTrue($thrown, 'Exception was rethrown');
2085
-		$this->assertNull($this->getFileLockType($view, $path), 'File got unlocked after exception');
2086
-
2087
-		/* Resume trash to avoid side effects */
2088
-		Server::get(ITrashManager::class)->resumeTrash();
2089
-	}
2090
-
2091
-	public function testLockBasicOperationUnlocksAfterLockException(): void {
2092
-		$view = new View('/' . $this->user . '/files/');
2093
-
2094
-		$storage = new Temporary([]);
2095
-
2096
-		Filesystem::mount($storage, [], $this->user . '/');
2097
-
2098
-		$storage->mkdir('files');
2099
-		$storage->mkdir('files/dir');
2100
-		$storage->file_put_contents('files/test.txt', 'blah');
2101
-		$storage->getScanner()->scan('files');
2102
-
2103
-		// get a shared lock
2104
-		$handle = $view->fopen('test.txt', 'r');
2105
-
2106
-		$thrown = false;
2107
-		try {
2108
-			// try (and fail) to get a write lock
2109
-			$view->unlink('test.txt');
2110
-		} catch (\Exception $e) {
2111
-			$thrown = true;
2112
-			$this->assertInstanceOf(LockedException::class, $e);
2113
-		}
2114
-		$this->assertTrue($thrown, 'Exception was rethrown');
2115
-
2116
-		// clean shared lock
2117
-		fclose($handle);
2118
-
2119
-		$this->assertNull($this->getFileLockType($view, 'test.txt'), 'File got unlocked');
2120
-	}
2121
-
2122
-	/**
2123
-	 * Test locks for fopen with fclose at the end
2124
-	 *
2125
-	 *
2126
-	 * @param string $operation operation name on the view
2127
-	 * @param array $operationArgs arguments for the operation
2128
-	 * @param string $path path of the locked item to check
2129
-	 * @param string $hookType hook type
2130
-	 */
2131
-	#[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')]
2132
-	public function testLockBasicOperationUnlocksAfterCancelledHook(
2133
-		$operation,
2134
-		$operationArgs,
2135
-		$path,
2136
-		$hookType,
2137
-	): void {
2138
-		$view = new View('/' . $this->user . '/files/');
2139
-
2140
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2141
-		$storage = $this->getMockBuilder(Temporary::class)
2142
-			->onlyMethods([$operation])
2143
-			->getMock();
2144
-
2145
-		Filesystem::mount($storage, [], $this->user . '/');
2146
-		$storage->mkdir('files');
2147
-
2148
-		Util::connectHook(
2149
-			Filesystem::CLASSNAME,
2150
-			$hookType,
2151
-			HookHelper::class,
2152
-			'cancellingCallback'
2153
-		);
2154
-
2155
-		call_user_func_array([$view, $operation], $operationArgs);
2156
-
2157
-		$this->assertNull($this->getFileLockType($view, $path), 'File got unlocked after exception');
2158
-	}
2159
-
2160
-	public static function lockFileRenameOrCopyDataProvider(): array {
2161
-		return [
2162
-			['rename', ILockingProvider::LOCK_EXCLUSIVE],
2163
-			['copy', ILockingProvider::LOCK_SHARED],
2164
-		];
2165
-	}
2166
-
2167
-	/**
2168
-	 * Test locks for rename or copy operation
2169
-	 *
2170
-	 *
2171
-	 * @param string $operation operation to be done on the view
2172
-	 * @param int $expectedLockTypeSourceDuring expected lock type on source file during
2173
-	 *                                          the operation
2174
-	 */
2175
-	#[\PHPUnit\Framework\Attributes\DataProvider('lockFileRenameOrCopyDataProvider')]
2176
-	public function testLockFileRename($operation, $expectedLockTypeSourceDuring): void {
2177
-		$view = new View('/' . $this->user . '/files/');
2178
-
2179
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2180
-		$storage = $this->getMockBuilder(Temporary::class)
2181
-			->onlyMethods([$operation, 'getMetaData', 'filemtime'])
2182
-			->getMock();
2183
-
2184
-		$storage->expects($this->any())
2185
-			->method('getMetaData')
2186
-			->willReturn([
2187
-				'mtime' => 1885434487,
2188
-				'etag' => '',
2189
-				'mimetype' => 'text/plain',
2190
-				'permissions' => Constants::PERMISSION_ALL,
2191
-				'size' => 3
2192
-			]);
2193
-		$storage->expects($this->any())
2194
-			->method('filemtime')
2195
-			->willReturn(123456789);
2196
-
2197
-		$sourcePath = 'original.txt';
2198
-		$targetPath = 'target.txt';
2199
-
2200
-		/* Disable encryption wrapper to avoid it intercepting mocked call */
2201
-		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2202
-
2203
-		Filesystem::mount($storage, [], $this->user . '/');
2204
-		$storage->mkdir('files');
2205
-		$view->file_put_contents($sourcePath, 'meh');
2206
-
2207
-		$storage->expects($this->once())
2208
-			->method($operation)
2209
-			->willReturnCallback(
2210
-				function () use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2211
-					$lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath);
2212
-					$lockTypeTargetDuring = $this->getFileLockType($view, $targetPath);
2213
-
2214
-					return true;
2215
-				}
2216
-			);
2217
-
2218
-		$this->connectMockHooks($operation, $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost);
2219
-		$this->connectMockHooks($operation, $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost);
2220
-
2221
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2222
-		$this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation');
2223
-
2224
-		$view->$operation($sourcePath, $targetPath);
2225
-
2226
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source file locked properly during pre-hook');
2227
-		$this->assertEquals($expectedLockTypeSourceDuring, $lockTypeSourceDuring, 'Source file locked properly during operation');
2228
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source file locked properly during post-hook');
2229
-
2230
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target file locked properly during pre-hook');
2231
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target file locked properly during operation');
2232
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target file locked properly during post-hook');
2233
-
2234
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2235
-		$this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation');
2236
-	}
2237
-
2238
-	/**
2239
-	 * simulate a failed copy operation.
2240
-	 * We expect that we catch the exception, free the lock and re-throw it.
2241
-	 *
2242
-	 */
2243
-	public function testLockFileCopyException(): void {
2244
-		$this->expectException(\Exception::class);
2245
-
2246
-		$view = new View('/' . $this->user . '/files/');
2247
-
2248
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2249
-		$storage = $this->getMockBuilder(Temporary::class)
2250
-			->onlyMethods(['copy'])
2251
-			->getMock();
2252
-
2253
-		$sourcePath = 'original.txt';
2254
-		$targetPath = 'target.txt';
2255
-
2256
-		/* Disable encryption wrapper to avoid it intercepting mocked call */
2257
-		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2258
-
2259
-		Filesystem::mount($storage, [], $this->user . '/');
2260
-		$storage->mkdir('files');
2261
-		$view->file_put_contents($sourcePath, 'meh');
2262
-
2263
-		$storage->expects($this->once())
2264
-			->method('copy')
2265
-			->willReturnCallback(
2266
-				function (): void {
2267
-					throw new \Exception();
2268
-				}
2269
-			);
2270
-
2271
-		$this->connectMockHooks('copy', $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost);
2272
-		$this->connectMockHooks('copy', $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost);
2273
-
2274
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2275
-		$this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation');
2276
-
2277
-		try {
2278
-			$view->copy($sourcePath, $targetPath);
2279
-		} catch (\Exception $e) {
2280
-			$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2281
-			$this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation');
2282
-			throw $e;
2283
-		}
2284
-	}
2285
-
2286
-	/**
2287
-	 * Test rename operation: unlock first path when second path was locked
2288
-	 */
2289
-	public function testLockFileRenameUnlockOnException(): void {
2290
-		self::loginAsUser('test');
2291
-
2292
-		$view = new View('/' . $this->user . '/files/');
2293
-
2294
-		$sourcePath = 'original.txt';
2295
-		$targetPath = 'target.txt';
2296
-		$view->file_put_contents($sourcePath, 'meh');
2297
-
2298
-		// simulate that the target path is already locked
2299
-		$view->lockFile($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
2300
-
2301
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2302
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $this->getFileLockType($view, $targetPath), 'Target file is locked before operation');
2303
-
2304
-		$thrown = false;
2305
-		try {
2306
-			$view->rename($sourcePath, $targetPath);
2307
-		} catch (LockedException $e) {
2308
-			$thrown = true;
2309
-		}
2310
-
2311
-		$this->assertTrue($thrown, 'LockedException thrown');
2312
-
2313
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2314
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $this->getFileLockType($view, $targetPath), 'Target file still locked after operation');
2315
-
2316
-		$view->unlockFile($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
2317
-	}
2318
-
2319
-	/**
2320
-	 * Test rename operation: unlock first path when second path was locked
2321
-	 */
2322
-	public function testGetOwner(): void {
2323
-		self::loginAsUser('test');
2324
-
2325
-		$view = new View('/test/files/');
2326
-
2327
-		$path = 'foo.txt';
2328
-		$view->file_put_contents($path, 'meh');
2329
-
2330
-		$this->assertEquals('test', $view->getFileInfo($path)->getOwner()->getUID());
2331
-
2332
-		$folderInfo = $view->getDirectoryContent('');
2333
-		$folderInfo = array_values(array_filter($folderInfo, function (FileInfo $info) {
2334
-			return $info->getName() === 'foo.txt';
2335
-		}));
2336
-
2337
-		$this->assertEquals('test', $folderInfo[0]->getOwner()->getUID());
2338
-
2339
-		$subStorage = new Temporary();
2340
-		Filesystem::mount($subStorage, [], '/test/files/asd');
2341
-
2342
-		$folderInfo = $view->getDirectoryContent('');
2343
-		$folderInfo = array_values(array_filter($folderInfo, function (FileInfo $info) {
2344
-			return $info->getName() === 'asd';
2345
-		}));
2346
-
2347
-		$this->assertEquals('test', $folderInfo[0]->getOwner()->getUID());
2348
-	}
2349
-
2350
-	public static function lockFileRenameOrCopyCrossStorageDataProvider(): array {
2351
-		return [
2352
-			['rename', 'moveFromStorage', ILockingProvider::LOCK_EXCLUSIVE],
2353
-			['copy', 'copyFromStorage', ILockingProvider::LOCK_SHARED],
2354
-		];
2355
-	}
2356
-
2357
-	/**
2358
-	 * Test locks for rename or copy operation cross-storage
2359
-	 *
2360
-	 *
2361
-	 * @param string $viewOperation operation to be done on the view
2362
-	 * @param string $storageOperation operation to be mocked on the storage
2363
-	 * @param int $expectedLockTypeSourceDuring expected lock type on source file during
2364
-	 *                                          the operation
2365
-	 */
2366
-	#[\PHPUnit\Framework\Attributes\DataProvider('lockFileRenameOrCopyCrossStorageDataProvider')]
2367
-	public function testLockFileRenameCrossStorage($viewOperation, $storageOperation, $expectedLockTypeSourceDuring): void {
2368
-		$view = new View('/' . $this->user . '/files/');
2369
-
2370
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2371
-		$storage = $this->getMockBuilder(Temporary::class)
2372
-			->onlyMethods([$storageOperation])
2373
-			->getMock();
2374
-		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage2 */
2375
-		$storage2 = $this->getMockBuilder(Temporary::class)
2376
-			->onlyMethods([$storageOperation, 'getMetaData', 'filemtime'])
2377
-			->getMock();
2378
-
2379
-		$storage2->expects($this->any())
2380
-			->method('getMetaData')
2381
-			->willReturn([
2382
-				'mtime' => 1885434487,
2383
-				'etag' => '',
2384
-				'mimetype' => 'text/plain',
2385
-				'permissions' => Constants::PERMISSION_ALL,
2386
-				'size' => 3
2387
-			]);
2388
-		$storage2->expects($this->any())
2389
-			->method('filemtime')
2390
-			->willReturn(123456789);
2391
-
2392
-		$sourcePath = 'original.txt';
2393
-		$targetPath = 'substorage/target.txt';
2394
-
2395
-		/* Disable encryption wrapper to avoid it intercepting mocked call */
2396
-		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2397
-
2398
-		Filesystem::mount($storage, [], $this->user . '/');
2399
-		Filesystem::mount($storage2, [], $this->user . '/files/substorage');
2400
-		$storage->mkdir('files');
2401
-		$view->file_put_contents($sourcePath, 'meh');
2402
-		$storage2->getUpdater()->update('');
2403
-
2404
-		$storage->expects($this->never())
2405
-			->method($storageOperation);
2406
-		$storage2->expects($this->once())
2407
-			->method($storageOperation)
2408
-			->willReturnCallback(
2409
-				function () use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2410
-					$lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath);
2411
-					$lockTypeTargetDuring = $this->getFileLockType($view, $targetPath);
2412
-
2413
-					return true;
2414
-				}
2415
-			);
2416
-
2417
-		$this->connectMockHooks($viewOperation, $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost);
2418
-		$this->connectMockHooks($viewOperation, $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost);
2419
-
2420
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2421
-		$this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation');
2422
-
2423
-		$view->$viewOperation($sourcePath, $targetPath);
2424
-
2425
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source file locked properly during pre-hook');
2426
-		$this->assertEquals($expectedLockTypeSourceDuring, $lockTypeSourceDuring, 'Source file locked properly during operation');
2427
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source file locked properly during post-hook');
2428
-
2429
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target file locked properly during pre-hook');
2430
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target file locked properly during operation');
2431
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target file locked properly during post-hook');
2432
-
2433
-		$this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2434
-		$this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation');
2435
-	}
2436
-
2437
-	/**
2438
-	 * Test locks when moving a mount point
2439
-	 */
2440
-	public function testLockMoveMountPoint(): void {
2441
-		self::loginAsUser('test');
2442
-
2443
-		[$mount] = $this->createTestMovableMountPoints([
2444
-			$this->user . '/files/substorage',
2445
-		]);
2446
-
2447
-		$view = new View('/' . $this->user . '/files/');
2448
-		$view->mkdir('subdir');
2449
-
2450
-		$sourcePath = 'substorage';
2451
-		$targetPath = 'subdir/substorage_moved';
2452
-
2453
-		$mount->expects($this->once())
2454
-			->method('moveMount')
2455
-			->willReturnCallback(
2456
-				function ($target) use ($mount, $view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring, &$lockTypeSharedRootDuring) {
2457
-					$lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath, true);
2458
-					$lockTypeTargetDuring = $this->getFileLockType($view, $targetPath, true);
2459
-
2460
-					$lockTypeSharedRootDuring = $this->getFileLockType($view, $sourcePath, false);
2461
-
2462
-					$mount->setMountPoint($target);
2463
-
2464
-					return true;
2465
-				}
2466
-			);
2467
-
2468
-		$this->connectMockHooks('rename', $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost, true);
2469
-		$this->connectMockHooks('rename', $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost, true);
2470
-		// in pre-hook, mount point is still on $sourcePath
2471
-		$this->connectMockHooks('rename', $view, $sourcePath, $lockTypeSharedRootPre, $dummy, false);
2472
-		// in post-hook, mount point is now on $targetPath
2473
-		$this->connectMockHooks('rename', $view, $targetPath, $dummy, $lockTypeSharedRootPost, false);
2474
-
2475
-		$this->assertNull($this->getFileLockType($view, $sourcePath, false), 'Shared storage root not locked before operation');
2476
-		$this->assertNull($this->getFileLockType($view, $sourcePath, true), 'Source path not locked before operation');
2477
-		$this->assertNull($this->getFileLockType($view, $targetPath, true), 'Target path not locked before operation');
2478
-
2479
-		$view->rename($sourcePath, $targetPath);
2480
-
2481
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source path locked properly during pre-hook');
2482
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeSourceDuring, 'Source path locked properly during operation');
2483
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source path locked properly during post-hook');
2484
-
2485
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target path locked properly during pre-hook');
2486
-		$this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target path locked properly during operation');
2487
-		$this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target path locked properly during post-hook');
2488
-
2489
-		$this->assertNull($lockTypeSharedRootPre, 'Shared storage root not locked during pre-hook');
2490
-		$this->assertNull($lockTypeSharedRootDuring, 'Shared storage root not locked during move');
2491
-		$this->assertNull($lockTypeSharedRootPost, 'Shared storage root not locked during post-hook');
2492
-
2493
-		$this->assertNull($this->getFileLockType($view, $sourcePath, false), 'Shared storage root not locked after operation');
2494
-		$this->assertNull($this->getFileLockType($view, $sourcePath, true), 'Source path not locked after operation');
2495
-		$this->assertNull($this->getFileLockType($view, $targetPath, true), 'Target path not locked after operation');
2496
-	}
2497
-
2498
-	/**
2499
-	 * Connect hook callbacks for hook type
2500
-	 *
2501
-	 * @param string $hookType hook type or null for none
2502
-	 * @param View $view view to check the lock on
2503
-	 * @param string $path path for which to check the lock
2504
-	 * @param int $lockTypePre variable to receive lock type that was active in the pre-hook
2505
-	 * @param int $lockTypePost variable to receive lock type that was active in the post-hook
2506
-	 * @param bool $onMountPoint true to check the mount point instead of the
2507
-	 *                           mounted storage
2508
-	 */
2509
-	private function connectMockHooks($hookType, $view, $path, &$lockTypePre, &$lockTypePost, $onMountPoint = false) {
2510
-		if ($hookType === null) {
2511
-			return;
2512
-		}
2513
-
2514
-		$eventHandler = $this->getMockBuilder(TestEventHandler::class)
2515
-			->onlyMethods(['preCallback', 'postCallback'])
2516
-			->getMock();
2517
-
2518
-		$eventHandler->expects($this->any())
2519
-			->method('preCallback')
2520
-			->willReturnCallback(
2521
-				function () use ($view, $path, $onMountPoint, &$lockTypePre): void {
2522
-					$lockTypePre = $this->getFileLockType($view, $path, $onMountPoint);
2523
-				}
2524
-			);
2525
-		$eventHandler->expects($this->any())
2526
-			->method('postCallback')
2527
-			->willReturnCallback(
2528
-				function () use ($view, $path, $onMountPoint, &$lockTypePost): void {
2529
-					$lockTypePost = $this->getFileLockType($view, $path, $onMountPoint);
2530
-				}
2531
-			);
2532
-
2533
-		if ($hookType !== null) {
2534
-			Util::connectHook(
2535
-				Filesystem::CLASSNAME,
2536
-				$hookType,
2537
-				$eventHandler,
2538
-				'preCallback'
2539
-			);
2540
-			Util::connectHook(
2541
-				Filesystem::CLASSNAME,
2542
-				'post_' . $hookType,
2543
-				$eventHandler,
2544
-				'postCallback'
2545
-			);
2546
-		}
2547
-	}
2548
-
2549
-	/**
2550
-	 * Returns the file lock type
2551
-	 *
2552
-	 * @param View $view view
2553
-	 * @param string $path path
2554
-	 * @param bool $onMountPoint true to check the mount point instead of the
2555
-	 *                           mounted storage
2556
-	 *
2557
-	 * @return int lock type or null if file was not locked
2558
-	 */
2559
-	private function getFileLockType(View $view, $path, $onMountPoint = false) {
2560
-		if ($this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE, $onMountPoint)) {
2561
-			return ILockingProvider::LOCK_EXCLUSIVE;
2562
-		} elseif ($this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED, $onMountPoint)) {
2563
-			return ILockingProvider::LOCK_SHARED;
2564
-		}
2565
-		return null;
2566
-	}
2567
-
2568
-
2569
-	public function testRemoveMoveableMountPoint(): void {
2570
-		$mountPoint = '/' . $this->user . '/files/mount/';
2571
-
2572
-		// Mock the mount point
2573
-		/** @var TestMoveableMountPoint|\PHPUnit\Framework\MockObject\MockObject $mount */
2574
-		$mount = $this->createMock(TestMoveableMountPoint::class);
2575
-		$mount->expects($this->once())
2576
-			->method('getMountPoint')
2577
-			->willReturn($mountPoint);
2578
-		$mount->expects($this->once())
2579
-			->method('removeMount')
2580
-			->willReturn('foo');
2581
-		$mount->expects($this->any())
2582
-			->method('getInternalPath')
2583
-			->willReturn('');
2584
-
2585
-		// Register mount
2586
-		Filesystem::getMountManager()->addMount($mount);
2587
-
2588
-		// Listen for events
2589
-		$eventHandler = $this->getMockBuilder(TestEventHandler::class)
2590
-			->onlyMethods(['umount', 'post_umount'])
2591
-			->getMock();
2592
-		$eventHandler->expects($this->once())
2593
-			->method('umount')
2594
-			->with([Filesystem::signal_param_path => '/mount']);
2595
-		$eventHandler->expects($this->once())
2596
-			->method('post_umount')
2597
-			->with([Filesystem::signal_param_path => '/mount']);
2598
-		Util::connectHook(
2599
-			Filesystem::CLASSNAME,
2600
-			'umount',
2601
-			$eventHandler,
2602
-			'umount'
2603
-		);
2604
-		Util::connectHook(
2605
-			Filesystem::CLASSNAME,
2606
-			'post_umount',
2607
-			$eventHandler,
2608
-			'post_umount'
2609
-		);
2610
-
2611
-		//Delete the mountpoint
2612
-		$view = new View('/' . $this->user . '/files');
2613
-		$this->assertEquals('foo', $view->rmdir('mount'));
2614
-	}
2615
-
2616
-	public static function mimeFilterProvider(): array {
2617
-		return [
2618
-			[null, ['test1.txt', 'test2.txt', 'test3.md', 'test4.png']],
2619
-			['text/plain', ['test1.txt', 'test2.txt']],
2620
-			['text/markdown', ['test3.md']],
2621
-			['text', ['test1.txt', 'test2.txt', 'test3.md']],
2622
-		];
2623
-	}
2624
-
2625
-	/**
2626
-	 * @param string $filter
2627
-	 * @param string[] $expected
2628
-	 */
2629
-	#[\PHPUnit\Framework\Attributes\DataProvider('mimeFilterProvider')]
2630
-	public function testGetDirectoryContentMimeFilter($filter, $expected): void {
2631
-		$storage1 = new Temporary();
2632
-		$root = self::getUniqueID('/');
2633
-		Filesystem::mount($storage1, [], $root . '/');
2634
-		$view = new View($root);
2635
-
2636
-		$view->file_put_contents('test1.txt', 'asd');
2637
-		$view->file_put_contents('test2.txt', 'asd');
2638
-		$view->file_put_contents('test3.md', 'asd');
2639
-		$view->file_put_contents('test4.png', '');
2640
-
2641
-		$content = $view->getDirectoryContent('', $filter);
2642
-
2643
-		$files = array_map(function (FileInfo $info) {
2644
-			return $info->getName();
2645
-		}, $content);
2646
-		sort($files);
2647
-
2648
-		$this->assertEquals($expected, $files);
2649
-	}
2650
-
2651
-	public function testFilePutContentsClearsChecksum(): void {
2652
-		$storage = new Temporary([]);
2653
-		$scanner = $storage->getScanner();
2654
-		$storage->file_put_contents('foo.txt', 'bar');
2655
-		Filesystem::mount($storage, [], '/test/');
2656
-		$scanner->scan('');
2657
-
2658
-		$view = new View('/test/foo.txt');
2659
-		$view->putFileInfo('.', ['checksum' => '42']);
2660
-
2661
-		$this->assertEquals('bar', $view->file_get_contents(''));
2662
-		$fh = tmpfile();
2663
-		fwrite($fh, 'fooo');
2664
-		rewind($fh);
2665
-		clearstatcache();
2666
-		$view->file_put_contents('', $fh);
2667
-		$this->assertEquals('fooo', $view->file_get_contents(''));
2668
-		$data = $view->getFileInfo('.');
2669
-		$this->assertEquals('', $data->getChecksum());
2670
-	}
2671
-
2672
-	public function testDeleteGhostFile(): void {
2673
-		$storage = new Temporary([]);
2674
-		$scanner = $storage->getScanner();
2675
-		$cache = $storage->getCache();
2676
-		$storage->file_put_contents('foo.txt', 'bar');
2677
-		Filesystem::mount($storage, [], '/test/');
2678
-		$scanner->scan('');
2679
-
2680
-		$storage->unlink('foo.txt');
2681
-
2682
-		$this->assertTrue($cache->inCache('foo.txt'));
2683
-
2684
-		$view = new View('/test');
2685
-		$rootInfo = $view->getFileInfo('');
2686
-		$this->assertEquals(3, $rootInfo->getSize());
2687
-		$view->unlink('foo.txt');
2688
-		$newInfo = $view->getFileInfo('');
2689
-
2690
-		$this->assertFalse($cache->inCache('foo.txt'));
2691
-		$this->assertNotEquals($rootInfo->getEtag(), $newInfo->getEtag());
2692
-		$this->assertEquals(0, $newInfo->getSize());
2693
-	}
2694
-
2695
-	public function testDeleteGhostFolder(): void {
2696
-		$storage = new Temporary([]);
2697
-		$scanner = $storage->getScanner();
2698
-		$cache = $storage->getCache();
2699
-		$storage->mkdir('foo');
2700
-		$storage->file_put_contents('foo/foo.txt', 'bar');
2701
-		Filesystem::mount($storage, [], '/test/');
2702
-		$scanner->scan('');
2703
-
2704
-		$storage->rmdir('foo');
2705
-
2706
-		$this->assertTrue($cache->inCache('foo'));
2707
-		$this->assertTrue($cache->inCache('foo/foo.txt'));
2708
-
2709
-		$view = new View('/test');
2710
-		$rootInfo = $view->getFileInfo('');
2711
-		$this->assertEquals(3, $rootInfo->getSize());
2712
-		$view->rmdir('foo');
2713
-		$newInfo = $view->getFileInfo('');
2714
-
2715
-		$this->assertFalse($cache->inCache('foo'));
2716
-		$this->assertFalse($cache->inCache('foo/foo.txt'));
2717
-		$this->assertNotEquals($rootInfo->getEtag(), $newInfo->getEtag());
2718
-		$this->assertEquals(0, $newInfo->getSize());
2719
-	}
2720
-
2721
-	public function testCreateParentDirectories(): void {
2722
-		$view = $this->getMockBuilder(View::class)
2723
-			->disableOriginalConstructor()
2724
-			->onlyMethods([
2725
-				'is_file',
2726
-				'file_exists',
2727
-				'mkdir',
2728
-			])
2729
-			->getMock();
2730
-
2731
-		$view->expects($this->exactly(3))
2732
-			->method('is_file')
2733
-			->willReturnMap([
2734
-				['/new', false],
2735
-				['/new/folder', false],
2736
-				['/new/folder/structure', false],
2737
-			]);
2738
-		$view->expects($this->exactly(3))
2739
-			->method('file_exists')
2740
-			->willReturnMap([
2741
-				['/new', true],
2742
-				['/new/folder', false],
2743
-				['/new/folder/structure', false],
2744
-			]);
2745
-
2746
-		$calls = ['/new/folder', '/new/folder/structure'];
2747
-		$view->expects($this->exactly(2))
2748
-			->method('mkdir')
2749
-			->willReturnCallback(function ($dir) use (&$calls): void {
2750
-				$expected = array_shift($calls);
2751
-				$this->assertEquals($expected, $dir);
2752
-			});
2753
-
2754
-		$this->assertTrue(self::invokePrivate($view, 'createParentDirectories', ['/new/folder/structure']));
2755
-	}
2756
-
2757
-	public function testCreateParentDirectoriesWithExistingFile(): void {
2758
-		$view = $this->getMockBuilder(View::class)
2759
-			->disableOriginalConstructor()
2760
-			->onlyMethods([
2761
-				'is_file',
2762
-				'file_exists',
2763
-				'mkdir',
2764
-			])
2765
-			->getMock();
2766
-
2767
-		$view
2768
-			->expects($this->once())
2769
-			->method('is_file')
2770
-			->with('/file.txt')
2771
-			->willReturn(true);
2772
-		$this->assertFalse(self::invokePrivate($view, 'createParentDirectories', ['/file.txt/folder/structure']));
2773
-	}
2774
-
2775
-	public function testCacheExtension(): void {
2776
-		$storage = new Temporary([]);
2777
-		$scanner = $storage->getScanner();
2778
-		$storage->file_put_contents('foo.txt', 'bar');
2779
-		$scanner->scan('');
2780
-
2781
-		Filesystem::mount($storage, [], '/test/');
2782
-		$view = new View('/test');
2783
-
2784
-		$info = $view->getFileInfo('/foo.txt');
2785
-		$this->assertEquals(0, $info->getUploadTime());
2786
-		$this->assertEquals(0, $info->getCreationTime());
2787
-
2788
-		$view->putFileInfo('/foo.txt', ['upload_time' => 25]);
2789
-
2790
-		$info = $view->getFileInfo('/foo.txt');
2791
-		$this->assertEquals(25, $info->getUploadTime());
2792
-		$this->assertEquals(0, $info->getCreationTime());
2793
-	}
2794
-
2795
-	public function testFopenGone(): void {
2796
-		$storage = new Temporary([]);
2797
-		$scanner = $storage->getScanner();
2798
-		$storage->file_put_contents('foo.txt', 'bar');
2799
-		$scanner->scan('');
2800
-		$cache = $storage->getCache();
2801
-
2802
-		Filesystem::mount($storage, [], '/test/');
2803
-		$view = new View('/test');
2804
-
2805
-		$storage->unlink('foo.txt');
2806
-
2807
-		$this->assertTrue($cache->inCache('foo.txt'));
2808
-
2809
-		$this->assertFalse($view->fopen('foo.txt', 'r'));
2810
-
2811
-		$this->assertFalse($cache->inCache('foo.txt'));
2812
-	}
2813
-
2814
-	public function testMountpointParentsCreated(): void {
2815
-		$storage1 = $this->getTestStorage();
2816
-		Filesystem::mount($storage1, [], '/');
2817
-
2818
-		$storage2 = $this->getTestStorage();
2819
-		Filesystem::mount($storage2, [], '/A/B/C');
2820
-
2821
-		$rootView = new View('');
2822
-
2823
-		$folderData = $rootView->getDirectoryContent('/');
2824
-		$this->assertCount(4, $folderData);
2825
-		$this->assertEquals('folder', $folderData[0]['name']);
2826
-		$this->assertEquals('foo.png', $folderData[1]['name']);
2827
-		$this->assertEquals('foo.txt', $folderData[2]['name']);
2828
-		$this->assertEquals('A', $folderData[3]['name']);
2829
-
2830
-		$folderData = $rootView->getDirectoryContent('/A');
2831
-		$this->assertCount(1, $folderData);
2832
-		$this->assertEquals('B', $folderData[0]['name']);
2833
-
2834
-		$folderData = $rootView->getDirectoryContent('/A/B');
2835
-		$this->assertCount(1, $folderData);
2836
-		$this->assertEquals('C', $folderData[0]['name']);
2837
-
2838
-		$folderData = $rootView->getDirectoryContent('/A/B/C');
2839
-		$this->assertCount(3, $folderData);
2840
-		$this->assertEquals('folder', $folderData[0]['name']);
2841
-		$this->assertEquals('foo.png', $folderData[1]['name']);
2842
-		$this->assertEquals('foo.txt', $folderData[2]['name']);
2843
-	}
2844
-
2845
-	public function testCopyPreservesContent() {
2846
-		$viewUser1 = new View('/' . 'userId' . '/files');
2847
-		$viewUser1->mkdir('');
2848
-		$viewUser1->file_put_contents('foo.txt', 'foo');
2849
-		$viewUser1->copy('foo.txt', 'bar.txt');
2850
-		$this->assertEquals('foo', $viewUser1->file_get_contents('bar.txt'));
2851
-	}
809
+        $folderName = 'abcdefghijklmnopqrstuvwxyz012345678901234567890123456789';
810
+        $tmpdirLength = strlen(Server::get(ITempManager::class)->getTemporaryFolder());
811
+        $depth = ((4000 - $tmpdirLength) / 57);
812
+
813
+        foreach (range(0, $depth - 1) as $i) {
814
+            $longPath .= $ds . $folderName;
815
+            $result = $rootView->mkdir($longPath);
816
+            $this->assertTrue($result, "mkdir failed on $i - path length: " . strlen($longPath));
817
+
818
+            $result = $rootView->file_put_contents($longPath . "{$ds}test.txt", 'lorem');
819
+            $this->assertEquals(5, $result, "file_put_contents failed on $i");
820
+
821
+            $this->assertTrue($rootView->file_exists($longPath));
822
+            $this->assertTrue($rootView->file_exists($longPath . "{$ds}test.txt"));
823
+        }
824
+
825
+        $cache = $storage->getCache();
826
+        $scanner = $storage->getScanner();
827
+        $scanner->scan('');
828
+
829
+        $longPath = $folderName;
830
+        foreach (range(0, $depth - 1) as $i) {
831
+            $cachedFolder = $cache->get($longPath);
832
+            $this->assertTrue(is_array($cachedFolder), "No cache entry for folder at $i");
833
+            $this->assertEquals($folderName, $cachedFolder['name'], "Wrong cache entry for folder at $i");
834
+
835
+            $cachedFile = $cache->get($longPath . '/test.txt');
836
+            $this->assertTrue(is_array($cachedFile), "No cache entry for file at $i");
837
+            $this->assertEquals('test.txt', $cachedFile['name'], "Wrong cache entry for file at $i");
838
+
839
+            $longPath .= $ds . $folderName;
840
+        }
841
+    }
842
+
843
+    public function testTouchNotSupported(): void {
844
+        $storage = new TemporaryNoTouch([]);
845
+        $scanner = $storage->getScanner();
846
+        Filesystem::mount($storage, [], '/test/');
847
+        $past = time() - 100;
848
+        $storage->file_put_contents('test', 'foobar');
849
+        $scanner->scan('');
850
+        $view = new View('');
851
+        $info = $view->getFileInfo('/test/test');
852
+
853
+        $view->touch('/test/test', $past);
854
+        $scanner->scanFile('test', Scanner::REUSE_ETAG);
855
+
856
+        $info2 = $view->getFileInfo('/test/test');
857
+        $this->assertSame($info['etag'], $info2['etag']);
858
+    }
859
+
860
+    public function testWatcherEtagCrossStorage(): void {
861
+        $storage1 = new Temporary([]);
862
+        $storage2 = new Temporary([]);
863
+        $scanner1 = $storage1->getScanner();
864
+        $scanner2 = $storage2->getScanner();
865
+        $storage1->mkdir('sub');
866
+        Filesystem::mount($storage1, [], '/test/');
867
+        Filesystem::mount($storage2, [], '/test/sub/storage');
868
+
869
+        $past = time() - 100;
870
+        $storage2->file_put_contents('test.txt', 'foobar');
871
+        $scanner1->scan('');
872
+        $scanner2->scan('');
873
+        $view = new View('');
874
+
875
+        $storage2->getWatcher('')->setPolicy(Watcher::CHECK_ALWAYS);
876
+
877
+        $oldFileInfo = $view->getFileInfo('/test/sub/storage/test.txt');
878
+        $oldFolderInfo = $view->getFileInfo('/test');
879
+
880
+        $storage2->getCache()->update($oldFileInfo->getId(), [
881
+            'storage_mtime' => $past,
882
+        ]);
883
+
884
+        $oldEtag = $oldFolderInfo->getEtag();
885
+
886
+        $view->getFileInfo('/test/sub/storage/test.txt');
887
+        $newFolderInfo = $view->getFileInfo('/test');
888
+
889
+        $this->assertNotEquals($newFolderInfo->getEtag(), $oldEtag);
890
+    }
891
+
892
+    #[\PHPUnit\Framework\Attributes\DataProvider('absolutePathProvider')]
893
+    public function testGetAbsolutePath($expectedPath, $relativePath): void {
894
+        $view = new View('/files');
895
+        $this->assertEquals($expectedPath, $view->getAbsolutePath($relativePath));
896
+    }
897
+
898
+    public function testPartFileInfo(): void {
899
+        $storage = new Temporary([]);
900
+        $scanner = $storage->getScanner();
901
+        Filesystem::mount($storage, [], '/test/');
902
+        $sizeWritten = $storage->file_put_contents('test.part', 'foobar');
903
+        $scanner->scan('');
904
+        $view = new View('/test');
905
+        $info = $view->getFileInfo('test.part');
906
+
907
+        $this->assertInstanceOf('\OCP\Files\FileInfo', $info);
908
+        $this->assertNull($info->getId());
909
+        $this->assertEquals(6, $sizeWritten);
910
+        $this->assertEquals(6, $info->getSize());
911
+        $this->assertEquals('foobar', $view->file_get_contents('test.part'));
912
+    }
913
+
914
+    public static function absolutePathProvider(): array {
915
+        return [
916
+            ['/files', ''],
917
+            ['/files/0', '0'],
918
+            ['/files/false', 'false'],
919
+            ['/files/true', 'true'],
920
+            ['/files', '/'],
921
+            ['/files/test', 'test'],
922
+            ['/files/test', '/test'],
923
+        ];
924
+    }
925
+
926
+    #[\PHPUnit\Framework\Attributes\DataProvider('chrootRelativePathProvider')]
927
+    public function testChrootGetRelativePath($root, $absolutePath, $expectedPath): void {
928
+        $view = new View('/files');
929
+        $view->chroot($root);
930
+        $this->assertEquals($expectedPath, $view->getRelativePath($absolutePath));
931
+    }
932
+
933
+    public static function chrootRelativePathProvider(): array {
934
+        return self::relativePathProvider('/');
935
+    }
936
+
937
+    #[\PHPUnit\Framework\Attributes\DataProvider('initRelativePathProvider')]
938
+    public function testInitGetRelativePath($root, $absolutePath, $expectedPath): void {
939
+        $view = new View($root);
940
+        $this->assertEquals($expectedPath, $view->getRelativePath($absolutePath));
941
+    }
942
+
943
+    public static function initRelativePathProvider(): array {
944
+        return self::relativePathProvider(null);
945
+    }
946
+
947
+    public static function relativePathProvider($missingRootExpectedPath): array {
948
+        return [
949
+            // No root - returns the path
950
+            ['', '/files', '/files'],
951
+            ['', '/files/', '/files/'],
952
+
953
+            // Root equals path - /
954
+            ['/files/', '/files/', '/'],
955
+            ['/files/', '/files', '/'],
956
+            ['/files', '/files/', '/'],
957
+            ['/files', '/files', '/'],
958
+
959
+            // False negatives: chroot fixes those by adding the leading slash.
960
+            // But setting them up with this root (instead of chroot($root))
961
+            // will fail them, although they should be the same.
962
+            // TODO init should be fixed, so it also adds the leading slash
963
+            ['files/', '/files/', $missingRootExpectedPath],
964
+            ['files', '/files/', $missingRootExpectedPath],
965
+            ['files/', '/files', $missingRootExpectedPath],
966
+            ['files', '/files', $missingRootExpectedPath],
967
+
968
+            // False negatives: Paths provided to the method should have a leading slash
969
+            // TODO input should be checked to have a leading slash
970
+            ['/files/', 'files/', null],
971
+            ['/files', 'files/', null],
972
+            ['/files/', 'files', null],
973
+            ['/files', 'files', null],
974
+
975
+            // with trailing slashes
976
+            ['/files/', '/files/0', '0'],
977
+            ['/files/', '/files/false', 'false'],
978
+            ['/files/', '/files/true', 'true'],
979
+            ['/files/', '/files/test', 'test'],
980
+            ['/files/', '/files/test/foo', 'test/foo'],
981
+
982
+            // without trailing slashes
983
+            // TODO false expectation: Should match "with trailing slashes"
984
+            ['/files', '/files/0', '/0'],
985
+            ['/files', '/files/false', '/false'],
986
+            ['/files', '/files/true', '/true'],
987
+            ['/files', '/files/test', '/test'],
988
+            ['/files', '/files/test/foo', '/test/foo'],
989
+
990
+            // leading slashes
991
+            ['/files/', '/files_trashbin/', null],
992
+            ['/files', '/files_trashbin/', null],
993
+            ['/files/', '/files_trashbin', null],
994
+            ['/files', '/files_trashbin', null],
995
+
996
+            // no leading slashes
997
+            ['files/', 'files_trashbin/', null],
998
+            ['files', 'files_trashbin/', null],
999
+            ['files/', 'files_trashbin', null],
1000
+            ['files', 'files_trashbin', null],
1001
+
1002
+            // mixed leading slashes
1003
+            ['files/', '/files_trashbin/', null],
1004
+            ['/files/', 'files_trashbin/', null],
1005
+            ['files', '/files_trashbin/', null],
1006
+            ['/files', 'files_trashbin/', null],
1007
+            ['files/', '/files_trashbin', null],
1008
+            ['/files/', 'files_trashbin', null],
1009
+            ['files', '/files_trashbin', null],
1010
+            ['/files', 'files_trashbin', null],
1011
+
1012
+            ['files', 'files_trashbin/test', null],
1013
+            ['/files', '/files_trashbin/test', null],
1014
+            ['/files', 'files_trashbin/test', null],
1015
+        ];
1016
+    }
1017
+
1018
+    public function testFileView(): void {
1019
+        $storage = new Temporary([]);
1020
+        $scanner = $storage->getScanner();
1021
+        $storage->file_put_contents('foo.txt', 'bar');
1022
+        Filesystem::mount($storage, [], '/test/');
1023
+        $scanner->scan('');
1024
+        $view = new View('/test/foo.txt');
1025
+
1026
+        $this->assertEquals('bar', $view->file_get_contents(''));
1027
+        $fh = tmpfile();
1028
+        fwrite($fh, 'foo');
1029
+        rewind($fh);
1030
+        $view->file_put_contents('', $fh);
1031
+        $this->assertEquals('foo', $view->file_get_contents(''));
1032
+    }
1033
+
1034
+    #[\PHPUnit\Framework\Attributes\DataProvider('tooLongPathDataProvider')]
1035
+    public function testTooLongPath($operation, $param0 = null): void {
1036
+        $this->expectException(InvalidPathException::class);
1037
+
1038
+
1039
+        $longPath = '';
1040
+        // 4000 is the maximum path length in file_cache.path
1041
+        $folderName = 'abcdefghijklmnopqrstuvwxyz012345678901234567890123456789';
1042
+        $depth = (4000 / 57);
1043
+        foreach (range(0, $depth + 1) as $i) {
1044
+            $longPath .= '/' . $folderName;
1045
+        }
1046
+
1047
+        $storage = new Temporary([]);
1048
+        $this->tempStorage = $storage; // for later hard cleanup
1049
+        Filesystem::mount($storage, [], '/');
1050
+
1051
+        $rootView = new View('');
1052
+
1053
+        if ($param0 === '@0') {
1054
+            $param0 = $longPath;
1055
+        }
1056
+
1057
+        if ($operation === 'hash') {
1058
+            $param0 = $longPath;
1059
+            $longPath = 'md5';
1060
+        }
1061
+
1062
+        call_user_func([$rootView, $operation], $longPath, $param0);
1063
+    }
1064
+
1065
+    public static function tooLongPathDataProvider(): array {
1066
+        return [
1067
+            ['getAbsolutePath'],
1068
+            ['getRelativePath'],
1069
+            ['getMountPoint'],
1070
+            ['resolvePath'],
1071
+            ['getLocalFile'],
1072
+            ['mkdir'],
1073
+            ['rmdir'],
1074
+            ['opendir'],
1075
+            ['is_dir'],
1076
+            ['is_file'],
1077
+            ['stat'],
1078
+            ['filetype'],
1079
+            ['filesize'],
1080
+            ['readfile'],
1081
+            ['isCreatable'],
1082
+            ['isReadable'],
1083
+            ['isUpdatable'],
1084
+            ['isDeletable'],
1085
+            ['isSharable'],
1086
+            ['file_exists'],
1087
+            ['filemtime'],
1088
+            ['touch'],
1089
+            ['file_get_contents'],
1090
+            ['unlink'],
1091
+            ['deleteAll'],
1092
+            ['toTmpFile'],
1093
+            ['getMimeType'],
1094
+            ['free_space'],
1095
+            ['getFileInfo'],
1096
+            ['getDirectoryContent'],
1097
+            ['getOwner'],
1098
+            ['getETag'],
1099
+            ['file_put_contents', 'ipsum'],
1100
+            ['rename', '@0'],
1101
+            ['copy', '@0'],
1102
+            ['fopen', 'r'],
1103
+            ['fromTmpFile', '@0'],
1104
+            ['hash'],
1105
+            ['hasUpdated', 0],
1106
+            ['putFileInfo', []],
1107
+        ];
1108
+    }
1109
+
1110
+    public function testRenameCrossStoragePreserveMtime(): void {
1111
+        $storage1 = new Temporary([]);
1112
+        $storage2 = new Temporary([]);
1113
+        $storage1->mkdir('sub');
1114
+        $storage1->mkdir('foo');
1115
+        $storage1->file_put_contents('foo.txt', 'asd');
1116
+        $storage1->file_put_contents('foo/bar.txt', 'asd');
1117
+        Filesystem::mount($storage1, [], '/test/');
1118
+        Filesystem::mount($storage2, [], '/test/sub/storage');
1119
+
1120
+        $view = new View('');
1121
+        $time = time() - 200;
1122
+        $view->touch('/test/foo.txt', $time);
1123
+        $view->touch('/test/foo', $time);
1124
+        $view->touch('/test/foo/bar.txt', $time);
1125
+
1126
+        $view->rename('/test/foo.txt', '/test/sub/storage/foo.txt');
1127
+
1128
+        $this->assertEquals($time, $view->filemtime('/test/sub/storage/foo.txt'));
1129
+
1130
+        $view->rename('/test/foo', '/test/sub/storage/foo');
1131
+
1132
+        $this->assertEquals($time, $view->filemtime('/test/sub/storage/foo/bar.txt'));
1133
+    }
1134
+
1135
+    public function testRenameFailDeleteTargetKeepSource(): void {
1136
+        $this->doTestCopyRenameFail('rename');
1137
+    }
1138
+
1139
+    public function testCopyFailDeleteTargetKeepSource(): void {
1140
+        $this->doTestCopyRenameFail('copy');
1141
+    }
1142
+
1143
+    private function doTestCopyRenameFail($operation) {
1144
+        $storage1 = new Temporary([]);
1145
+        /** @var \PHPUnit\Framework\MockObject\MockObject|Temporary $storage2 */
1146
+        $storage2 = $this->getMockBuilder(TemporaryNoCross::class)
1147
+            ->setConstructorArgs([[]])
1148
+            ->onlyMethods(['fopen', 'writeStream'])
1149
+            ->getMock();
1150
+
1151
+        $storage2->method('writeStream')
1152
+            ->willThrowException(new GenericFileException('Failed to copy stream'));
1153
+
1154
+        $storage2->method('fopen')
1155
+            ->willReturn(false);
1156
+
1157
+        $storage1->mkdir('sub');
1158
+        $storage1->file_put_contents('foo.txt', '0123456789ABCDEFGH');
1159
+        $storage1->mkdir('dirtomove');
1160
+        $storage1->file_put_contents('dirtomove/indir1.txt', '0123456'); // fits
1161
+        $storage1->file_put_contents('dirtomove/indir2.txt', '0123456789ABCDEFGH'); // doesn't fit
1162
+        $storage2->file_put_contents('existing.txt', '0123');
1163
+        $storage1->getScanner()->scan('');
1164
+        $storage2->getScanner()->scan('');
1165
+        Filesystem::mount($storage1, [], '/test/');
1166
+        Filesystem::mount($storage2, [], '/test/sub/storage');
1167
+
1168
+        // move file
1169
+        $view = new View('');
1170
+        $this->assertTrue($storage1->file_exists('foo.txt'));
1171
+        $this->assertFalse($storage2->file_exists('foo.txt'));
1172
+        $this->assertFalse($view->$operation('/test/foo.txt', '/test/sub/storage/foo.txt'));
1173
+        $this->assertFalse($storage2->file_exists('foo.txt'));
1174
+        $this->assertFalse($storage2->getCache()->get('foo.txt'));
1175
+        $this->assertTrue($storage1->file_exists('foo.txt'));
1176
+
1177
+        // if target exists, it will be deleted too
1178
+        $this->assertFalse($view->$operation('/test/foo.txt', '/test/sub/storage/existing.txt'));
1179
+        $this->assertFalse($storage2->file_exists('existing.txt'));
1180
+        $this->assertFalse($storage2->getCache()->get('existing.txt'));
1181
+        $this->assertTrue($storage1->file_exists('foo.txt'));
1182
+
1183
+        // move folder
1184
+        $this->assertFalse($view->$operation('/test/dirtomove/', '/test/sub/storage/dirtomove/'));
1185
+        // since the move failed, the full source tree is kept
1186
+        $this->assertTrue($storage1->file_exists('dirtomove/indir1.txt'));
1187
+        $this->assertTrue($storage1->file_exists('dirtomove/indir2.txt'));
1188
+        // second file not moved/copied
1189
+        $this->assertFalse($storage2->file_exists('dirtomove/indir2.txt'));
1190
+        $this->assertFalse($storage2->getCache()->get('dirtomove/indir2.txt'));
1191
+    }
1192
+
1193
+    public function testDeleteFailKeepCache(): void {
1194
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
1195
+        $storage = $this->getMockBuilder(Temporary::class)
1196
+            ->setConstructorArgs([[]])
1197
+            ->onlyMethods(['unlink'])
1198
+            ->getMock();
1199
+        $storage->expects($this->once())
1200
+            ->method('unlink')
1201
+            ->willReturn(false);
1202
+        $scanner = $storage->getScanner();
1203
+        $cache = $storage->getCache();
1204
+        $storage->file_put_contents('foo.txt', 'asd');
1205
+        $scanner->scan('');
1206
+        Filesystem::mount($storage, [], '/test/');
1207
+
1208
+        $view = new View('/test');
1209
+
1210
+        $this->assertFalse($view->unlink('foo.txt'));
1211
+        $this->assertTrue($cache->inCache('foo.txt'));
1212
+    }
1213
+
1214
+    public static function directoryTraversalProvider(): array {
1215
+        return [
1216
+            ['../test/'],
1217
+            ['..\\test\\my/../folder'],
1218
+            ['/test/my/../foo\\'],
1219
+        ];
1220
+    }
1221
+
1222
+    /**
1223
+     * @param string $root
1224
+     */
1225
+    #[\PHPUnit\Framework\Attributes\DataProvider('directoryTraversalProvider')]
1226
+    public function testConstructDirectoryTraversalException($root): void {
1227
+        $this->expectException(\Exception::class);
1228
+
1229
+        new View($root);
1230
+    }
1231
+
1232
+    public function testRenameOverWrite(): void {
1233
+        $storage = new Temporary([]);
1234
+        $scanner = $storage->getScanner();
1235
+        $storage->mkdir('sub');
1236
+        $storage->mkdir('foo');
1237
+        $storage->file_put_contents('foo.txt', 'asd');
1238
+        $storage->file_put_contents('foo/bar.txt', 'asd');
1239
+        $scanner->scan('');
1240
+        Filesystem::mount($storage, [], '/test/');
1241
+        $view = new View('');
1242
+        $this->assertTrue($view->rename('/test/foo.txt', '/test/foo/bar.txt'));
1243
+    }
1244
+
1245
+    public function testSetMountOptionsInStorage(): void {
1246
+        $mount = new MountPoint(Temporary::class, '/asd/', [[]], Filesystem::getLoader(), ['foo' => 'bar']);
1247
+        Filesystem::getMountManager()->addMount($mount);
1248
+        /** @var Common $storage */
1249
+        $storage = $mount->getStorage();
1250
+        $this->assertEquals($storage->getMountOption('foo'), 'bar');
1251
+    }
1252
+
1253
+    public function testSetMountOptionsWatcherPolicy(): void {
1254
+        $mount = new MountPoint(Temporary::class, '/asd/', [[]], Filesystem::getLoader(), ['filesystem_check_changes' => Watcher::CHECK_NEVER]);
1255
+        Filesystem::getMountManager()->addMount($mount);
1256
+        /** @var Common $storage */
1257
+        $storage = $mount->getStorage();
1258
+        $watcher = $storage->getWatcher();
1259
+        $this->assertEquals(Watcher::CHECK_NEVER, $watcher->getPolicy());
1260
+    }
1261
+
1262
+    public function testGetAbsolutePathOnNull(): void {
1263
+        $view = new View();
1264
+        $this->assertNull($view->getAbsolutePath(null));
1265
+    }
1266
+
1267
+    public function testGetRelativePathOnNull(): void {
1268
+        $view = new View();
1269
+        $this->assertNull($view->getRelativePath(null));
1270
+    }
1271
+
1272
+
1273
+    public function testNullAsRoot(): void {
1274
+        $this->expectException(\TypeError::class);
1275
+
1276
+        new View(null);
1277
+    }
1278
+
1279
+    /**
1280
+     * e.g. reading from a folder that's being renamed
1281
+     *
1282
+     *
1283
+     *
1284
+     * @param string $rootPath
1285
+     * @param string $pathPrefix
1286
+     */
1287
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')]
1288
+    public function testReadFromWriteLockedPath($rootPath, $pathPrefix): void {
1289
+        $this->expectException(LockedException::class);
1290
+
1291
+        $rootPath = str_replace('{folder}', 'files', $rootPath);
1292
+        $pathPrefix = str_replace('{folder}', 'files', $pathPrefix);
1293
+
1294
+        $view = new View($rootPath);
1295
+        $storage = new Temporary([]);
1296
+        Filesystem::mount($storage, [], '/');
1297
+        $this->assertTrue($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1298
+        $view->lockFile($pathPrefix . '/foo/bar/asd', ILockingProvider::LOCK_SHARED);
1299
+    }
1300
+
1301
+    /**
1302
+     * Reading from a files_encryption folder that's being renamed
1303
+     *
1304
+     *
1305
+     * @param string $rootPath
1306
+     * @param string $pathPrefix
1307
+     */
1308
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')]
1309
+    public function testReadFromWriteUnlockablePath($rootPath, $pathPrefix): void {
1310
+        $rootPath = str_replace('{folder}', 'files_encryption', $rootPath);
1311
+        $pathPrefix = str_replace('{folder}', 'files_encryption', $pathPrefix);
1312
+
1313
+        $view = new View($rootPath);
1314
+        $storage = new Temporary([]);
1315
+        Filesystem::mount($storage, [], '/');
1316
+        $this->assertFalse($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1317
+        $this->assertFalse($view->lockFile($pathPrefix . '/foo/bar/asd', ILockingProvider::LOCK_SHARED));
1318
+    }
1319
+
1320
+    /**
1321
+     * e.g. writing a file that's being downloaded
1322
+     *
1323
+     *
1324
+     *
1325
+     * @param string $rootPath
1326
+     * @param string $pathPrefix
1327
+     */
1328
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')]
1329
+    public function testWriteToReadLockedFile($rootPath, $pathPrefix): void {
1330
+        $this->expectException(LockedException::class);
1331
+
1332
+        $rootPath = str_replace('{folder}', 'files', $rootPath);
1333
+        $pathPrefix = str_replace('{folder}', 'files', $pathPrefix);
1334
+
1335
+        $view = new View($rootPath);
1336
+        $storage = new Temporary([]);
1337
+        Filesystem::mount($storage, [], '/');
1338
+        $this->assertTrue($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_SHARED));
1339
+        $view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE);
1340
+    }
1341
+
1342
+    /**
1343
+     * Writing a file that's being downloaded
1344
+     *
1345
+     *
1346
+     * @param string $rootPath
1347
+     * @param string $pathPrefix
1348
+     */
1349
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataLockPaths')]
1350
+    public function testWriteToReadUnlockableFile($rootPath, $pathPrefix): void {
1351
+        $rootPath = str_replace('{folder}', 'files_encryption', $rootPath);
1352
+        $pathPrefix = str_replace('{folder}', 'files_encryption', $pathPrefix);
1353
+
1354
+        $view = new View($rootPath);
1355
+        $storage = new Temporary([]);
1356
+        Filesystem::mount($storage, [], '/');
1357
+        $this->assertFalse($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_SHARED));
1358
+        $this->assertFalse($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1359
+    }
1360
+
1361
+    /**
1362
+     * Test that locks are on mount point paths instead of mount root
1363
+     */
1364
+    public function testLockLocalMountPointPathInsteadOfStorageRoot(): void {
1365
+        $lockingProvider = Server::get(ILockingProvider::class);
1366
+        $view = new View('/testuser/files/');
1367
+        $storage = new Temporary([]);
1368
+        Filesystem::mount($storage, [], '/');
1369
+        $mountedStorage = new Temporary([]);
1370
+        Filesystem::mount($mountedStorage, [], '/testuser/files/mountpoint');
1371
+
1372
+        $this->assertTrue(
1373
+            $view->lockFile('/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, true),
1374
+            'Can lock mount point'
1375
+        );
1376
+
1377
+        // no exception here because storage root was not locked
1378
+        $mountedStorage->acquireLock('', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1379
+
1380
+        $thrown = false;
1381
+        try {
1382
+            $storage->acquireLock('/testuser/files/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1383
+        } catch (LockedException $e) {
1384
+            $thrown = true;
1385
+        }
1386
+        $this->assertTrue($thrown, 'Mount point path was locked on root storage');
1387
+
1388
+        $lockingProvider->releaseAll();
1389
+    }
1390
+
1391
+    /**
1392
+     * Test that locks are on mount point paths and also mount root when requested
1393
+     */
1394
+    public function testLockStorageRootButNotLocalMountPoint(): void {
1395
+        $lockingProvider = Server::get(ILockingProvider::class);
1396
+        $view = new View('/testuser/files/');
1397
+        $storage = new Temporary([]);
1398
+        Filesystem::mount($storage, [], '/');
1399
+        $mountedStorage = new Temporary([]);
1400
+        Filesystem::mount($mountedStorage, [], '/testuser/files/mountpoint');
1401
+
1402
+        $this->assertTrue(
1403
+            $view->lockFile('/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, false),
1404
+            'Can lock mount point'
1405
+        );
1406
+
1407
+        $thrown = false;
1408
+        try {
1409
+            $mountedStorage->acquireLock('', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1410
+        } catch (LockedException $e) {
1411
+            $thrown = true;
1412
+        }
1413
+        $this->assertTrue($thrown, 'Mount point storage root was locked on original storage');
1414
+
1415
+        // local mount point was not locked
1416
+        $storage->acquireLock('/testuser/files/mountpoint', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1417
+
1418
+        $lockingProvider->releaseAll();
1419
+    }
1420
+
1421
+    /**
1422
+     * Test that locks are on mount point paths and also mount root when requested
1423
+     */
1424
+    public function testLockMountPointPathFailReleasesBoth(): void {
1425
+        $lockingProvider = Server::get(ILockingProvider::class);
1426
+        $view = new View('/testuser/files/');
1427
+        $storage = new Temporary([]);
1428
+        Filesystem::mount($storage, [], '/');
1429
+        $mountedStorage = new Temporary([]);
1430
+        Filesystem::mount($mountedStorage, [], '/testuser/files/mountpoint.txt');
1431
+
1432
+        // this would happen if someone is writing on the mount point
1433
+        $mountedStorage->acquireLock('', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1434
+
1435
+        $thrown = false;
1436
+        try {
1437
+            // this actually acquires two locks, one on the mount point and one on the storage root,
1438
+            // but the one on the storage root will fail
1439
+            $view->lockFile('/mountpoint.txt', ILockingProvider::LOCK_SHARED);
1440
+        } catch (LockedException $e) {
1441
+            $thrown = true;
1442
+        }
1443
+        $this->assertTrue($thrown, 'Cannot acquire shared lock because storage root is already locked');
1444
+
1445
+        // from here we expect that the lock on the local mount point was released properly
1446
+        // so acquiring an exclusive lock will succeed
1447
+        $storage->acquireLock('/testuser/files/mountpoint.txt', ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
1448
+
1449
+        $lockingProvider->releaseAll();
1450
+    }
1451
+
1452
+    public static function dataLockPaths(): array {
1453
+        return [
1454
+            ['/testuser/{folder}', ''],
1455
+            ['/testuser', '/{folder}'],
1456
+            ['', '/testuser/{folder}'],
1457
+        ];
1458
+    }
1459
+
1460
+    public static function pathRelativeToFilesProvider(): array {
1461
+        return [
1462
+            ['admin/files', ''],
1463
+            ['admin/files/x', 'x'],
1464
+            ['/admin/files', ''],
1465
+            ['/admin/files/sub', 'sub'],
1466
+            ['/admin/files/sub/', 'sub'],
1467
+            ['/admin/files/sub/sub2', 'sub/sub2'],
1468
+            ['//admin//files/sub//sub2', 'sub/sub2'],
1469
+        ];
1470
+    }
1471
+
1472
+    #[\PHPUnit\Framework\Attributes\DataProvider('pathRelativeToFilesProvider')]
1473
+    public function testGetPathRelativeToFiles($path, $expectedPath): void {
1474
+        $view = new View();
1475
+        $this->assertEquals($expectedPath, $view->getPathRelativeToFiles($path));
1476
+    }
1477
+
1478
+    public static function pathRelativeToFilesProviderExceptionCases(): array {
1479
+        return [
1480
+            [''],
1481
+            ['x'],
1482
+            ['files'],
1483
+            ['/files'],
1484
+            ['/admin/files_versions/abc'],
1485
+        ];
1486
+    }
1487
+
1488
+    /**
1489
+     * @param string $path
1490
+     */
1491
+    #[\PHPUnit\Framework\Attributes\DataProvider('pathRelativeToFilesProviderExceptionCases')]
1492
+    public function testGetPathRelativeToFilesWithInvalidArgument($path): void {
1493
+        $this->expectException(\InvalidArgumentException::class);
1494
+        $this->expectExceptionMessage('$absolutePath must be relative to "files"');
1495
+
1496
+        $view = new View();
1497
+        $view->getPathRelativeToFiles($path);
1498
+    }
1499
+
1500
+    public function testChangeLock(): void {
1501
+        $view = new View('/testuser/files/');
1502
+        $storage = new Temporary([]);
1503
+        Filesystem::mount($storage, [], '/');
1504
+
1505
+        $view->lockFile('/test/sub', ILockingProvider::LOCK_SHARED);
1506
+        $this->assertTrue($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_SHARED));
1507
+        $this->assertFalse($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_EXCLUSIVE));
1508
+
1509
+        $view->changeLock('//test/sub', ILockingProvider::LOCK_EXCLUSIVE);
1510
+        $this->assertTrue($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_EXCLUSIVE));
1511
+
1512
+        $view->changeLock('test/sub', ILockingProvider::LOCK_SHARED);
1513
+        $this->assertTrue($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_SHARED));
1514
+
1515
+        $view->unlockFile('/test/sub/', ILockingProvider::LOCK_SHARED);
1516
+
1517
+        $this->assertFalse($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_SHARED));
1518
+        $this->assertFalse($this->isFileLocked($view, '/test//sub', ILockingProvider::LOCK_EXCLUSIVE));
1519
+    }
1520
+
1521
+    public static function hookPathProvider(): array {
1522
+        return [
1523
+            ['/foo/files', '/foo', true],
1524
+            ['/foo/files/bar', '/foo', true],
1525
+            ['/foo', '/foo', false],
1526
+            ['/foo', '/files/foo', true],
1527
+            ['/foo', 'filesfoo', false],
1528
+            ['', '/foo/files', true],
1529
+            ['', '/foo/files/bar.txt', true],
1530
+        ];
1531
+    }
1532
+
1533
+    /**
1534
+     * @param $root
1535
+     * @param $path
1536
+     * @param $shouldEmit
1537
+     */
1538
+    #[\PHPUnit\Framework\Attributes\DataProvider('hookPathProvider')]
1539
+    public function testHookPaths($root, $path, $shouldEmit): void {
1540
+        $filesystemReflection = new \ReflectionClass(Filesystem::class);
1541
+        $defaultRootValue = $filesystemReflection->getProperty('defaultInstance');
1542
+        $defaultRootValue->setAccessible(true);
1543
+        $oldRoot = $defaultRootValue->getValue();
1544
+        $defaultView = new View('/foo/files');
1545
+        $defaultRootValue->setValue(null, $defaultView);
1546
+        $view = new View($root);
1547
+        $result = self::invokePrivate($view, 'shouldEmitHooks', [$path]);
1548
+        $defaultRootValue->setValue(null, $oldRoot);
1549
+        $this->assertEquals($shouldEmit, $result);
1550
+    }
1551
+
1552
+    /**
1553
+     * Create test movable mount points
1554
+     *
1555
+     * @param array $mountPoints array of mount point locations
1556
+     * @return array array of MountPoint objects
1557
+     */
1558
+    private function createTestMovableMountPoints($mountPoints) {
1559
+        $mounts = [];
1560
+        foreach ($mountPoints as $mountPoint) {
1561
+            $storage = $this->getMockBuilder(Storage::class)
1562
+                ->onlyMethods([])
1563
+                ->getMock();
1564
+            $storage->method('getId')->willReturn('non-null-id');
1565
+            $storage->method('getStorageCache')->willReturnCallback(function () use ($storage) {
1566
+                return new \OC\Files\Cache\Storage($storage, true, Server::get(IDBConnection::class));
1567
+            });
1568
+
1569
+            $mounts[] = $this->getMockBuilder(TestMoveableMountPoint::class)
1570
+                ->onlyMethods(['moveMount'])
1571
+                ->setConstructorArgs([$storage, $mountPoint])
1572
+                ->getMock();
1573
+        }
1574
+
1575
+        /** @var IMountProvider|\PHPUnit\Framework\MockObject\MockObject $mountProvider */
1576
+        $mountProvider = $this->createMock(IMountProvider::class);
1577
+        $mountProvider->expects($this->any())
1578
+            ->method('getMountsForUser')
1579
+            ->willReturn($mounts);
1580
+
1581
+        $mountProviderCollection = Server::get(IMountProviderCollection::class);
1582
+        $mountProviderCollection->registerProvider($mountProvider);
1583
+
1584
+        return $mounts;
1585
+    }
1586
+
1587
+    /**
1588
+     * Test mount point move
1589
+     */
1590
+    public function testMountPointMove(): void {
1591
+        self::loginAsUser($this->user);
1592
+
1593
+        [$mount1, $mount2] = $this->createTestMovableMountPoints([
1594
+            $this->user . '/files/mount1',
1595
+            $this->user . '/files/mount2',
1596
+        ]);
1597
+        $mount1->expects($this->once())
1598
+            ->method('moveMount')
1599
+            ->willReturn(true);
1600
+
1601
+        $mount2->expects($this->once())
1602
+            ->method('moveMount')
1603
+            ->willReturn(true);
1604
+
1605
+        $view = new View('/' . $this->user . '/files/');
1606
+        $view->mkdir('sub');
1607
+
1608
+        $this->assertTrue($view->rename('mount1', 'renamed_mount'), 'Can rename mount point');
1609
+        $this->assertTrue($view->rename('mount2', 'sub/moved_mount'), 'Can move a mount point into a subdirectory');
1610
+    }
1611
+
1612
+    public function testMoveMountPointOverwrite(): void {
1613
+        self::loginAsUser($this->user);
1614
+
1615
+        [$mount1, $mount2] = $this->createTestMovableMountPoints([
1616
+            $this->user . '/files/mount1',
1617
+            $this->user . '/files/mount2',
1618
+        ]);
1619
+
1620
+        $mount1->expects($this->never())
1621
+            ->method('moveMount');
1622
+
1623
+        $mount2->expects($this->never())
1624
+            ->method('moveMount');
1625
+
1626
+        $view = new View('/' . $this->user . '/files/');
1627
+
1628
+        $this->expectException(ForbiddenException::class);
1629
+        $view->rename('mount1', 'mount2');
1630
+    }
1631
+
1632
+    public function testMoveMountPointIntoMount(): void {
1633
+        self::loginAsUser($this->user);
1634
+
1635
+        [$mount1, $mount2] = $this->createTestMovableMountPoints([
1636
+            $this->user . '/files/mount1',
1637
+            $this->user . '/files/mount2',
1638
+        ]);
1639
+
1640
+        $mount1->expects($this->never())
1641
+            ->method('moveMount');
1642
+
1643
+        $mount2->expects($this->never())
1644
+            ->method('moveMount');
1645
+
1646
+        $view = new View('/' . $this->user . '/files/');
1647
+
1648
+        $this->expectException(ForbiddenException::class);
1649
+        $view->rename('mount1', 'mount2/sub');
1650
+    }
1651
+
1652
+    /**
1653
+     * Test that moving a mount point into a shared folder is forbidden
1654
+     */
1655
+    public function testMoveMountPointIntoSharedFolder(): void {
1656
+        self::loginAsUser($this->user);
1657
+
1658
+        [$mount1, $mount2] = $this->createTestMovableMountPoints([
1659
+            $this->user . '/files/mount1',
1660
+            $this->user . '/files/mount2',
1661
+        ]);
1662
+
1663
+        $mount1->expects($this->never())
1664
+            ->method('moveMount');
1665
+
1666
+        $mount2->expects($this->once())
1667
+            ->method('moveMount')
1668
+            ->willReturn(true);
1669
+
1670
+        $view = new View('/' . $this->user . '/files/');
1671
+        $view->mkdir('shareddir');
1672
+        $view->mkdir('shareddir/sub');
1673
+        $view->mkdir('shareddir/sub2');
1674
+        // Create a similar named but non-shared folder
1675
+        $view->mkdir('shareddir notshared');
1676
+
1677
+        $fileId = $view->getFileInfo('shareddir')->getId();
1678
+        $userObject = Server::get(IUserManager::class)->createUser('test2', 'IHateNonMockableStaticClasses');
1679
+
1680
+        $userFolder = \OC::$server->getUserFolder($this->user);
1681
+        $shareDir = $userFolder->get('shareddir');
1682
+        $shareManager = Server::get(IShareManager::class);
1683
+        $share = $shareManager->newShare();
1684
+        $share->setSharedWith('test2')
1685
+            ->setSharedBy($this->user)
1686
+            ->setShareType(IShare::TYPE_USER)
1687
+            ->setPermissions(Constants::PERMISSION_READ)
1688
+            ->setNode($shareDir);
1689
+        $shareManager->createShare($share);
1690
+
1691
+        try {
1692
+            $view->rename('mount1', 'shareddir');
1693
+            $this->fail('Cannot overwrite shared folder');
1694
+        } catch (ForbiddenException $e) {
1695
+
1696
+        }
1697
+        try {
1698
+            $view->rename('mount1', 'shareddir/sub');
1699
+            $this->fail('Cannot move mount point into shared folder');
1700
+        } catch (ForbiddenException $e) {
1701
+
1702
+        }
1703
+        try {
1704
+            $view->rename('mount1', 'shareddir/sub/sub2');
1705
+            $this->fail('Cannot move mount point into shared subfolder');
1706
+        } catch (ForbiddenException $e) {
1707
+
1708
+        }
1709
+        $this->assertTrue($view->rename('mount2', 'shareddir notshared/sub'), 'Can move mount point into a similarly named but non-shared folder');
1710
+
1711
+        $shareManager->deleteShare($share);
1712
+        $userObject->delete();
1713
+    }
1714
+
1715
+    public static function basicOperationProviderForLocks(): array {
1716
+        return [
1717
+            // --- write hook ----
1718
+            [
1719
+                'touch',
1720
+                ['touch-create.txt'],
1721
+                'touch-create.txt',
1722
+                'create',
1723
+                ILockingProvider::LOCK_SHARED,
1724
+                ILockingProvider::LOCK_EXCLUSIVE,
1725
+                ILockingProvider::LOCK_SHARED,
1726
+            ],
1727
+            [
1728
+                'fopen',
1729
+                ['test-write.txt', 'w'],
1730
+                'test-write.txt',
1731
+                'write',
1732
+                ILockingProvider::LOCK_SHARED,
1733
+                ILockingProvider::LOCK_EXCLUSIVE,
1734
+                null,
1735
+                // exclusive lock stays until fclose
1736
+                ILockingProvider::LOCK_EXCLUSIVE,
1737
+            ],
1738
+            [
1739
+                'mkdir',
1740
+                ['newdir'],
1741
+                'newdir',
1742
+                'write',
1743
+                ILockingProvider::LOCK_SHARED,
1744
+                ILockingProvider::LOCK_EXCLUSIVE,
1745
+                ILockingProvider::LOCK_SHARED,
1746
+            ],
1747
+            [
1748
+                'file_put_contents',
1749
+                ['file_put_contents.txt', 'blah'],
1750
+                'file_put_contents.txt',
1751
+                'write',
1752
+                ILockingProvider::LOCK_SHARED,
1753
+                ILockingProvider::LOCK_EXCLUSIVE,
1754
+                ILockingProvider::LOCK_SHARED,
1755
+                null,
1756
+                0,
1757
+            ],
1758
+
1759
+            // ---- delete hook ----
1760
+            [
1761
+                'rmdir',
1762
+                ['dir'],
1763
+                'dir',
1764
+                'delete',
1765
+                ILockingProvider::LOCK_SHARED,
1766
+                ILockingProvider::LOCK_EXCLUSIVE,
1767
+                ILockingProvider::LOCK_SHARED,
1768
+            ],
1769
+            [
1770
+                'unlink',
1771
+                ['test.txt'],
1772
+                'test.txt',
1773
+                'delete',
1774
+                ILockingProvider::LOCK_SHARED,
1775
+                ILockingProvider::LOCK_EXCLUSIVE,
1776
+                ILockingProvider::LOCK_SHARED,
1777
+            ],
1778
+
1779
+            // ---- read hook (no post hooks) ----
1780
+            [
1781
+                'file_get_contents',
1782
+                ['test.txt'],
1783
+                'test.txt',
1784
+                'read',
1785
+                ILockingProvider::LOCK_SHARED,
1786
+                ILockingProvider::LOCK_SHARED,
1787
+                null,
1788
+                null,
1789
+                false,
1790
+            ],
1791
+            [
1792
+                'fopen',
1793
+                ['test.txt', 'r'],
1794
+                'test.txt',
1795
+                'read',
1796
+                ILockingProvider::LOCK_SHARED,
1797
+                ILockingProvider::LOCK_SHARED,
1798
+                null,
1799
+            ],
1800
+            [
1801
+                'opendir',
1802
+                ['dir'],
1803
+                'dir',
1804
+                'read',
1805
+                ILockingProvider::LOCK_SHARED,
1806
+                ILockingProvider::LOCK_SHARED,
1807
+                null,
1808
+            ],
1809
+
1810
+            // ---- no lock, touch hook ---
1811
+            ['touch', ['test.txt'], 'test.txt', 'touch', null, null, null],
1812
+
1813
+            // ---- no hooks, no locks ---
1814
+            ['is_dir', ['dir'], 'dir', null],
1815
+            ['is_file', ['dir'], 'dir', null],
1816
+            [
1817
+                'stat',
1818
+                ['dir'],
1819
+                'dir',
1820
+                null,
1821
+                ILockingProvider::LOCK_SHARED,
1822
+                ILockingProvider::LOCK_SHARED,
1823
+                ILockingProvider::LOCK_SHARED,
1824
+                null,
1825
+                false,
1826
+            ],
1827
+            [
1828
+                'filetype',
1829
+                ['dir'],
1830
+                'dir',
1831
+                null,
1832
+                ILockingProvider::LOCK_SHARED,
1833
+                ILockingProvider::LOCK_SHARED,
1834
+                ILockingProvider::LOCK_SHARED,
1835
+                null,
1836
+                false,
1837
+            ],
1838
+            [
1839
+                'filesize',
1840
+                ['dir'],
1841
+                'dir',
1842
+                null,
1843
+                ILockingProvider::LOCK_SHARED,
1844
+                ILockingProvider::LOCK_SHARED,
1845
+                ILockingProvider::LOCK_SHARED,
1846
+                null,
1847
+                /* Return an int */
1848
+                100
1849
+            ],
1850
+            ['isCreatable', ['dir'], 'dir', null],
1851
+            ['isReadable', ['dir'], 'dir', null],
1852
+            ['isUpdatable', ['dir'], 'dir', null],
1853
+            ['isDeletable', ['dir'], 'dir', null],
1854
+            ['isSharable', ['dir'], 'dir', null],
1855
+            ['file_exists', ['dir'], 'dir', null],
1856
+            [
1857
+                'filemtime',
1858
+                ['dir'],
1859
+                'dir',
1860
+                null,
1861
+                ILockingProvider::LOCK_SHARED,
1862
+                ILockingProvider::LOCK_SHARED,
1863
+                ILockingProvider::LOCK_SHARED,
1864
+                null,
1865
+                false,
1866
+            ],
1867
+        ];
1868
+    }
1869
+
1870
+    /**
1871
+     * Test whether locks are set before and after the operation
1872
+     *
1873
+     *
1874
+     * @param string $operation operation name on the view
1875
+     * @param array $operationArgs arguments for the operation
1876
+     * @param string $lockedPath path of the locked item to check
1877
+     * @param string $hookType hook type
1878
+     * @param int $expectedLockBefore expected lock during pre hooks
1879
+     * @param int $expectedLockDuring expected lock during operation
1880
+     * @param int $expectedLockAfter expected lock during post hooks
1881
+     * @param int $expectedStrayLock expected lock after returning, should
1882
+     *                               be null (unlock) for most operations
1883
+     */
1884
+    #[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')]
1885
+    public function testLockBasicOperation(
1886
+        $operation,
1887
+        $operationArgs,
1888
+        $lockedPath,
1889
+        $hookType,
1890
+        $expectedLockBefore = ILockingProvider::LOCK_SHARED,
1891
+        $expectedLockDuring = ILockingProvider::LOCK_SHARED,
1892
+        $expectedLockAfter = ILockingProvider::LOCK_SHARED,
1893
+        $expectedStrayLock = null,
1894
+        $returnValue = true,
1895
+    ): void {
1896
+        $view = new View('/' . $this->user . '/files/');
1897
+
1898
+        /** @var Temporary&MockObject $storage */
1899
+        $storage = $this->getMockBuilder(Temporary::class)
1900
+            ->onlyMethods([$operation])
1901
+            ->getMock();
1902
+
1903
+        /* Pause trash to avoid the trashbin intercepting rmdir and unlink calls */
1904
+        Server::get(ITrashManager::class)->pauseTrash();
1905
+        /* Same thing with encryption wrapper */
1906
+        Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
1907
+
1908
+        Filesystem::mount($storage, [], $this->user . '/');
1909
+
1910
+        // work directly on disk because mkdir might be mocked
1911
+        $realPath = $storage->getSourcePath('');
1912
+        mkdir($realPath . '/files');
1913
+        mkdir($realPath . '/files/dir');
1914
+        file_put_contents($realPath . '/files/test.txt', 'blah');
1915
+        $storage->getScanner()->scan('files');
1916
+
1917
+        $storage->expects($this->once())
1918
+            ->method($operation)
1919
+            ->willReturnCallback(
1920
+                function () use ($view, $lockedPath, &$lockTypeDuring, $returnValue) {
1921
+                    $lockTypeDuring = $this->getFileLockType($view, $lockedPath);
1922
+
1923
+                    return $returnValue;
1924
+                }
1925
+            );
1926
+
1927
+        $this->assertNull($this->getFileLockType($view, $lockedPath), 'File not locked before operation');
1928
+
1929
+        $this->connectMockHooks($hookType, $view, $lockedPath, $lockTypePre, $lockTypePost);
1930
+
1931
+        // do operation
1932
+        call_user_func_array([$view, $operation], $operationArgs);
1933
+
1934
+        if ($hookType !== null) {
1935
+            $this->assertEquals($expectedLockBefore, $lockTypePre, 'File locked properly during pre-hook');
1936
+            $this->assertEquals($expectedLockAfter, $lockTypePost, 'File locked properly during post-hook');
1937
+            $this->assertEquals($expectedLockDuring, $lockTypeDuring, 'File locked properly during operation');
1938
+        } else {
1939
+            $this->assertNull($lockTypeDuring, 'File not locked during operation');
1940
+        }
1941
+
1942
+        $this->assertEquals($expectedStrayLock, $this->getFileLockType($view, $lockedPath));
1943
+
1944
+        /* Resume trash to avoid side effects */
1945
+        Server::get(ITrashManager::class)->resumeTrash();
1946
+    }
1947
+
1948
+    /**
1949
+     * Test locks for file_put_content with stream.
1950
+     * This code path uses $storage->fopen instead
1951
+     */
1952
+    public function testLockFilePutContentWithStream(): void {
1953
+        $view = new View('/' . $this->user . '/files/');
1954
+
1955
+        $path = 'test_file_put_contents.txt';
1956
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
1957
+        $storage = $this->getMockBuilder(Temporary::class)
1958
+            ->onlyMethods(['fopen'])
1959
+            ->getMock();
1960
+
1961
+        Filesystem::mount($storage, [], $this->user . '/');
1962
+        $storage->mkdir('files');
1963
+
1964
+        $storage->expects($this->once())
1965
+            ->method('fopen')
1966
+            ->willReturnCallback(
1967
+                function () use ($view, $path, &$lockTypeDuring) {
1968
+                    $lockTypeDuring = $this->getFileLockType($view, $path);
1969
+
1970
+                    return fopen('php://temp', 'r+');
1971
+                }
1972
+            );
1973
+
1974
+        $this->connectMockHooks('write', $view, $path, $lockTypePre, $lockTypePost);
1975
+
1976
+        $this->assertNull($this->getFileLockType($view, $path), 'File not locked before operation');
1977
+
1978
+        // do operation
1979
+        $view->file_put_contents($path, fopen('php://temp', 'r+'));
1980
+
1981
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePre, 'File locked properly during pre-hook');
1982
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePost, 'File locked properly during post-hook');
1983
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File locked properly during operation');
1984
+
1985
+        $this->assertNull($this->getFileLockType($view, $path));
1986
+    }
1987
+
1988
+    /**
1989
+     * Test locks for fopen with fclose at the end
1990
+     */
1991
+    public function testLockFopen(): void {
1992
+        $view = new View('/' . $this->user . '/files/');
1993
+
1994
+        $path = 'test_file_put_contents.txt';
1995
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
1996
+        $storage = $this->getMockBuilder(Temporary::class)
1997
+            ->onlyMethods(['fopen'])
1998
+            ->getMock();
1999
+
2000
+        Filesystem::mount($storage, [], $this->user . '/');
2001
+        $storage->mkdir('files');
2002
+
2003
+        $storage->expects($this->once())
2004
+            ->method('fopen')
2005
+            ->willReturnCallback(
2006
+                function () use ($view, $path, &$lockTypeDuring) {
2007
+                    $lockTypeDuring = $this->getFileLockType($view, $path);
2008
+
2009
+                    return fopen('php://temp', 'r+');
2010
+                }
2011
+            );
2012
+
2013
+        $this->connectMockHooks('write', $view, $path, $lockTypePre, $lockTypePost);
2014
+
2015
+        $this->assertNull($this->getFileLockType($view, $path), 'File not locked before operation');
2016
+
2017
+        // do operation
2018
+        $res = $view->fopen($path, 'w');
2019
+
2020
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypePre, 'File locked properly during pre-hook');
2021
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File locked properly during operation');
2022
+        $this->assertNull($lockTypePost, 'No post hook, no lock check possible');
2023
+
2024
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeDuring, 'File still locked after fopen');
2025
+
2026
+        fclose($res);
2027
+
2028
+        $this->assertNull($this->getFileLockType($view, $path), 'File unlocked after fclose');
2029
+    }
2030
+
2031
+    /**
2032
+     * Test locks for fopen with fclose at the end
2033
+     *
2034
+     *
2035
+     * @param string $operation operation name on the view
2036
+     * @param array $operationArgs arguments for the operation
2037
+     * @param string $path path of the locked item to check
2038
+     */
2039
+    #[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')]
2040
+    public function testLockBasicOperationUnlocksAfterException(
2041
+        $operation,
2042
+        $operationArgs,
2043
+        $path,
2044
+    ): void {
2045
+        if ($operation === 'touch') {
2046
+            $this->markTestSkipped('touch handles storage exceptions internally');
2047
+        }
2048
+        $view = new View('/' . $this->user . '/files/');
2049
+
2050
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2051
+        $storage = $this->getMockBuilder(Temporary::class)
2052
+            ->onlyMethods([$operation])
2053
+            ->getMock();
2054
+
2055
+        /* Pause trash to avoid the trashbin intercepting rmdir and unlink calls */
2056
+        Server::get(ITrashManager::class)->pauseTrash();
2057
+        /* Same thing with encryption wrapper */
2058
+        Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2059
+
2060
+        Filesystem::mount($storage, [], $this->user . '/');
2061
+
2062
+        // work directly on disk because mkdir might be mocked
2063
+        $realPath = $storage->getSourcePath('');
2064
+        mkdir($realPath . '/files');
2065
+        mkdir($realPath . '/files/dir');
2066
+        file_put_contents($realPath . '/files/test.txt', 'blah');
2067
+        $storage->getScanner()->scan('files');
2068
+
2069
+        $storage->expects($this->once())
2070
+            ->method($operation)
2071
+            ->willReturnCallback(
2072
+                function (): void {
2073
+                    throw new \Exception('Simulated exception');
2074
+                }
2075
+            );
2076
+
2077
+        $thrown = false;
2078
+        try {
2079
+            call_user_func_array([$view, $operation], $operationArgs);
2080
+        } catch (\Exception $e) {
2081
+            $thrown = true;
2082
+            $this->assertEquals('Simulated exception', $e->getMessage());
2083
+        }
2084
+        $this->assertTrue($thrown, 'Exception was rethrown');
2085
+        $this->assertNull($this->getFileLockType($view, $path), 'File got unlocked after exception');
2086
+
2087
+        /* Resume trash to avoid side effects */
2088
+        Server::get(ITrashManager::class)->resumeTrash();
2089
+    }
2090
+
2091
+    public function testLockBasicOperationUnlocksAfterLockException(): void {
2092
+        $view = new View('/' . $this->user . '/files/');
2093
+
2094
+        $storage = new Temporary([]);
2095
+
2096
+        Filesystem::mount($storage, [], $this->user . '/');
2097
+
2098
+        $storage->mkdir('files');
2099
+        $storage->mkdir('files/dir');
2100
+        $storage->file_put_contents('files/test.txt', 'blah');
2101
+        $storage->getScanner()->scan('files');
2102
+
2103
+        // get a shared lock
2104
+        $handle = $view->fopen('test.txt', 'r');
2105
+
2106
+        $thrown = false;
2107
+        try {
2108
+            // try (and fail) to get a write lock
2109
+            $view->unlink('test.txt');
2110
+        } catch (\Exception $e) {
2111
+            $thrown = true;
2112
+            $this->assertInstanceOf(LockedException::class, $e);
2113
+        }
2114
+        $this->assertTrue($thrown, 'Exception was rethrown');
2115
+
2116
+        // clean shared lock
2117
+        fclose($handle);
2118
+
2119
+        $this->assertNull($this->getFileLockType($view, 'test.txt'), 'File got unlocked');
2120
+    }
2121
+
2122
+    /**
2123
+     * Test locks for fopen with fclose at the end
2124
+     *
2125
+     *
2126
+     * @param string $operation operation name on the view
2127
+     * @param array $operationArgs arguments for the operation
2128
+     * @param string $path path of the locked item to check
2129
+     * @param string $hookType hook type
2130
+     */
2131
+    #[\PHPUnit\Framework\Attributes\DataProvider('basicOperationProviderForLocks')]
2132
+    public function testLockBasicOperationUnlocksAfterCancelledHook(
2133
+        $operation,
2134
+        $operationArgs,
2135
+        $path,
2136
+        $hookType,
2137
+    ): void {
2138
+        $view = new View('/' . $this->user . '/files/');
2139
+
2140
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2141
+        $storage = $this->getMockBuilder(Temporary::class)
2142
+            ->onlyMethods([$operation])
2143
+            ->getMock();
2144
+
2145
+        Filesystem::mount($storage, [], $this->user . '/');
2146
+        $storage->mkdir('files');
2147
+
2148
+        Util::connectHook(
2149
+            Filesystem::CLASSNAME,
2150
+            $hookType,
2151
+            HookHelper::class,
2152
+            'cancellingCallback'
2153
+        );
2154
+
2155
+        call_user_func_array([$view, $operation], $operationArgs);
2156
+
2157
+        $this->assertNull($this->getFileLockType($view, $path), 'File got unlocked after exception');
2158
+    }
2159
+
2160
+    public static function lockFileRenameOrCopyDataProvider(): array {
2161
+        return [
2162
+            ['rename', ILockingProvider::LOCK_EXCLUSIVE],
2163
+            ['copy', ILockingProvider::LOCK_SHARED],
2164
+        ];
2165
+    }
2166
+
2167
+    /**
2168
+     * Test locks for rename or copy operation
2169
+     *
2170
+     *
2171
+     * @param string $operation operation to be done on the view
2172
+     * @param int $expectedLockTypeSourceDuring expected lock type on source file during
2173
+     *                                          the operation
2174
+     */
2175
+    #[\PHPUnit\Framework\Attributes\DataProvider('lockFileRenameOrCopyDataProvider')]
2176
+    public function testLockFileRename($operation, $expectedLockTypeSourceDuring): void {
2177
+        $view = new View('/' . $this->user . '/files/');
2178
+
2179
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2180
+        $storage = $this->getMockBuilder(Temporary::class)
2181
+            ->onlyMethods([$operation, 'getMetaData', 'filemtime'])
2182
+            ->getMock();
2183
+
2184
+        $storage->expects($this->any())
2185
+            ->method('getMetaData')
2186
+            ->willReturn([
2187
+                'mtime' => 1885434487,
2188
+                'etag' => '',
2189
+                'mimetype' => 'text/plain',
2190
+                'permissions' => Constants::PERMISSION_ALL,
2191
+                'size' => 3
2192
+            ]);
2193
+        $storage->expects($this->any())
2194
+            ->method('filemtime')
2195
+            ->willReturn(123456789);
2196
+
2197
+        $sourcePath = 'original.txt';
2198
+        $targetPath = 'target.txt';
2199
+
2200
+        /* Disable encryption wrapper to avoid it intercepting mocked call */
2201
+        Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2202
+
2203
+        Filesystem::mount($storage, [], $this->user . '/');
2204
+        $storage->mkdir('files');
2205
+        $view->file_put_contents($sourcePath, 'meh');
2206
+
2207
+        $storage->expects($this->once())
2208
+            ->method($operation)
2209
+            ->willReturnCallback(
2210
+                function () use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2211
+                    $lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath);
2212
+                    $lockTypeTargetDuring = $this->getFileLockType($view, $targetPath);
2213
+
2214
+                    return true;
2215
+                }
2216
+            );
2217
+
2218
+        $this->connectMockHooks($operation, $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost);
2219
+        $this->connectMockHooks($operation, $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost);
2220
+
2221
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2222
+        $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation');
2223
+
2224
+        $view->$operation($sourcePath, $targetPath);
2225
+
2226
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source file locked properly during pre-hook');
2227
+        $this->assertEquals($expectedLockTypeSourceDuring, $lockTypeSourceDuring, 'Source file locked properly during operation');
2228
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source file locked properly during post-hook');
2229
+
2230
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target file locked properly during pre-hook');
2231
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target file locked properly during operation');
2232
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target file locked properly during post-hook');
2233
+
2234
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2235
+        $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation');
2236
+    }
2237
+
2238
+    /**
2239
+     * simulate a failed copy operation.
2240
+     * We expect that we catch the exception, free the lock and re-throw it.
2241
+     *
2242
+     */
2243
+    public function testLockFileCopyException(): void {
2244
+        $this->expectException(\Exception::class);
2245
+
2246
+        $view = new View('/' . $this->user . '/files/');
2247
+
2248
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2249
+        $storage = $this->getMockBuilder(Temporary::class)
2250
+            ->onlyMethods(['copy'])
2251
+            ->getMock();
2252
+
2253
+        $sourcePath = 'original.txt';
2254
+        $targetPath = 'target.txt';
2255
+
2256
+        /* Disable encryption wrapper to avoid it intercepting mocked call */
2257
+        Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2258
+
2259
+        Filesystem::mount($storage, [], $this->user . '/');
2260
+        $storage->mkdir('files');
2261
+        $view->file_put_contents($sourcePath, 'meh');
2262
+
2263
+        $storage->expects($this->once())
2264
+            ->method('copy')
2265
+            ->willReturnCallback(
2266
+                function (): void {
2267
+                    throw new \Exception();
2268
+                }
2269
+            );
2270
+
2271
+        $this->connectMockHooks('copy', $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost);
2272
+        $this->connectMockHooks('copy', $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost);
2273
+
2274
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2275
+        $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation');
2276
+
2277
+        try {
2278
+            $view->copy($sourcePath, $targetPath);
2279
+        } catch (\Exception $e) {
2280
+            $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2281
+            $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation');
2282
+            throw $e;
2283
+        }
2284
+    }
2285
+
2286
+    /**
2287
+     * Test rename operation: unlock first path when second path was locked
2288
+     */
2289
+    public function testLockFileRenameUnlockOnException(): void {
2290
+        self::loginAsUser('test');
2291
+
2292
+        $view = new View('/' . $this->user . '/files/');
2293
+
2294
+        $sourcePath = 'original.txt';
2295
+        $targetPath = 'target.txt';
2296
+        $view->file_put_contents($sourcePath, 'meh');
2297
+
2298
+        // simulate that the target path is already locked
2299
+        $view->lockFile($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
2300
+
2301
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2302
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $this->getFileLockType($view, $targetPath), 'Target file is locked before operation');
2303
+
2304
+        $thrown = false;
2305
+        try {
2306
+            $view->rename($sourcePath, $targetPath);
2307
+        } catch (LockedException $e) {
2308
+            $thrown = true;
2309
+        }
2310
+
2311
+        $this->assertTrue($thrown, 'LockedException thrown');
2312
+
2313
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2314
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $this->getFileLockType($view, $targetPath), 'Target file still locked after operation');
2315
+
2316
+        $view->unlockFile($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
2317
+    }
2318
+
2319
+    /**
2320
+     * Test rename operation: unlock first path when second path was locked
2321
+     */
2322
+    public function testGetOwner(): void {
2323
+        self::loginAsUser('test');
2324
+
2325
+        $view = new View('/test/files/');
2326
+
2327
+        $path = 'foo.txt';
2328
+        $view->file_put_contents($path, 'meh');
2329
+
2330
+        $this->assertEquals('test', $view->getFileInfo($path)->getOwner()->getUID());
2331
+
2332
+        $folderInfo = $view->getDirectoryContent('');
2333
+        $folderInfo = array_values(array_filter($folderInfo, function (FileInfo $info) {
2334
+            return $info->getName() === 'foo.txt';
2335
+        }));
2336
+
2337
+        $this->assertEquals('test', $folderInfo[0]->getOwner()->getUID());
2338
+
2339
+        $subStorage = new Temporary();
2340
+        Filesystem::mount($subStorage, [], '/test/files/asd');
2341
+
2342
+        $folderInfo = $view->getDirectoryContent('');
2343
+        $folderInfo = array_values(array_filter($folderInfo, function (FileInfo $info) {
2344
+            return $info->getName() === 'asd';
2345
+        }));
2346
+
2347
+        $this->assertEquals('test', $folderInfo[0]->getOwner()->getUID());
2348
+    }
2349
+
2350
+    public static function lockFileRenameOrCopyCrossStorageDataProvider(): array {
2351
+        return [
2352
+            ['rename', 'moveFromStorage', ILockingProvider::LOCK_EXCLUSIVE],
2353
+            ['copy', 'copyFromStorage', ILockingProvider::LOCK_SHARED],
2354
+        ];
2355
+    }
2356
+
2357
+    /**
2358
+     * Test locks for rename or copy operation cross-storage
2359
+     *
2360
+     *
2361
+     * @param string $viewOperation operation to be done on the view
2362
+     * @param string $storageOperation operation to be mocked on the storage
2363
+     * @param int $expectedLockTypeSourceDuring expected lock type on source file during
2364
+     *                                          the operation
2365
+     */
2366
+    #[\PHPUnit\Framework\Attributes\DataProvider('lockFileRenameOrCopyCrossStorageDataProvider')]
2367
+    public function testLockFileRenameCrossStorage($viewOperation, $storageOperation, $expectedLockTypeSourceDuring): void {
2368
+        $view = new View('/' . $this->user . '/files/');
2369
+
2370
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2371
+        $storage = $this->getMockBuilder(Temporary::class)
2372
+            ->onlyMethods([$storageOperation])
2373
+            ->getMock();
2374
+        /** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage2 */
2375
+        $storage2 = $this->getMockBuilder(Temporary::class)
2376
+            ->onlyMethods([$storageOperation, 'getMetaData', 'filemtime'])
2377
+            ->getMock();
2378
+
2379
+        $storage2->expects($this->any())
2380
+            ->method('getMetaData')
2381
+            ->willReturn([
2382
+                'mtime' => 1885434487,
2383
+                'etag' => '',
2384
+                'mimetype' => 'text/plain',
2385
+                'permissions' => Constants::PERMISSION_ALL,
2386
+                'size' => 3
2387
+            ]);
2388
+        $storage2->expects($this->any())
2389
+            ->method('filemtime')
2390
+            ->willReturn(123456789);
2391
+
2392
+        $sourcePath = 'original.txt';
2393
+        $targetPath = 'substorage/target.txt';
2394
+
2395
+        /* Disable encryption wrapper to avoid it intercepting mocked call */
2396
+        Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2397
+
2398
+        Filesystem::mount($storage, [], $this->user . '/');
2399
+        Filesystem::mount($storage2, [], $this->user . '/files/substorage');
2400
+        $storage->mkdir('files');
2401
+        $view->file_put_contents($sourcePath, 'meh');
2402
+        $storage2->getUpdater()->update('');
2403
+
2404
+        $storage->expects($this->never())
2405
+            ->method($storageOperation);
2406
+        $storage2->expects($this->once())
2407
+            ->method($storageOperation)
2408
+            ->willReturnCallback(
2409
+                function () use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2410
+                    $lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath);
2411
+                    $lockTypeTargetDuring = $this->getFileLockType($view, $targetPath);
2412
+
2413
+                    return true;
2414
+                }
2415
+            );
2416
+
2417
+        $this->connectMockHooks($viewOperation, $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost);
2418
+        $this->connectMockHooks($viewOperation, $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost);
2419
+
2420
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked before operation');
2421
+        $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked before operation');
2422
+
2423
+        $view->$viewOperation($sourcePath, $targetPath);
2424
+
2425
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source file locked properly during pre-hook');
2426
+        $this->assertEquals($expectedLockTypeSourceDuring, $lockTypeSourceDuring, 'Source file locked properly during operation');
2427
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source file locked properly during post-hook');
2428
+
2429
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target file locked properly during pre-hook');
2430
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target file locked properly during operation');
2431
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target file locked properly during post-hook');
2432
+
2433
+        $this->assertNull($this->getFileLockType($view, $sourcePath), 'Source file not locked after operation');
2434
+        $this->assertNull($this->getFileLockType($view, $targetPath), 'Target file not locked after operation');
2435
+    }
2436
+
2437
+    /**
2438
+     * Test locks when moving a mount point
2439
+     */
2440
+    public function testLockMoveMountPoint(): void {
2441
+        self::loginAsUser('test');
2442
+
2443
+        [$mount] = $this->createTestMovableMountPoints([
2444
+            $this->user . '/files/substorage',
2445
+        ]);
2446
+
2447
+        $view = new View('/' . $this->user . '/files/');
2448
+        $view->mkdir('subdir');
2449
+
2450
+        $sourcePath = 'substorage';
2451
+        $targetPath = 'subdir/substorage_moved';
2452
+
2453
+        $mount->expects($this->once())
2454
+            ->method('moveMount')
2455
+            ->willReturnCallback(
2456
+                function ($target) use ($mount, $view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring, &$lockTypeSharedRootDuring) {
2457
+                    $lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath, true);
2458
+                    $lockTypeTargetDuring = $this->getFileLockType($view, $targetPath, true);
2459
+
2460
+                    $lockTypeSharedRootDuring = $this->getFileLockType($view, $sourcePath, false);
2461
+
2462
+                    $mount->setMountPoint($target);
2463
+
2464
+                    return true;
2465
+                }
2466
+            );
2467
+
2468
+        $this->connectMockHooks('rename', $view, $sourcePath, $lockTypeSourcePre, $lockTypeSourcePost, true);
2469
+        $this->connectMockHooks('rename', $view, $targetPath, $lockTypeTargetPre, $lockTypeTargetPost, true);
2470
+        // in pre-hook, mount point is still on $sourcePath
2471
+        $this->connectMockHooks('rename', $view, $sourcePath, $lockTypeSharedRootPre, $dummy, false);
2472
+        // in post-hook, mount point is now on $targetPath
2473
+        $this->connectMockHooks('rename', $view, $targetPath, $dummy, $lockTypeSharedRootPost, false);
2474
+
2475
+        $this->assertNull($this->getFileLockType($view, $sourcePath, false), 'Shared storage root not locked before operation');
2476
+        $this->assertNull($this->getFileLockType($view, $sourcePath, true), 'Source path not locked before operation');
2477
+        $this->assertNull($this->getFileLockType($view, $targetPath, true), 'Target path not locked before operation');
2478
+
2479
+        $view->rename($sourcePath, $targetPath);
2480
+
2481
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePre, 'Source path locked properly during pre-hook');
2482
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeSourceDuring, 'Source path locked properly during operation');
2483
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeSourcePost, 'Source path locked properly during post-hook');
2484
+
2485
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPre, 'Target path locked properly during pre-hook');
2486
+        $this->assertEquals(ILockingProvider::LOCK_EXCLUSIVE, $lockTypeTargetDuring, 'Target path locked properly during operation');
2487
+        $this->assertEquals(ILockingProvider::LOCK_SHARED, $lockTypeTargetPost, 'Target path locked properly during post-hook');
2488
+
2489
+        $this->assertNull($lockTypeSharedRootPre, 'Shared storage root not locked during pre-hook');
2490
+        $this->assertNull($lockTypeSharedRootDuring, 'Shared storage root not locked during move');
2491
+        $this->assertNull($lockTypeSharedRootPost, 'Shared storage root not locked during post-hook');
2492
+
2493
+        $this->assertNull($this->getFileLockType($view, $sourcePath, false), 'Shared storage root not locked after operation');
2494
+        $this->assertNull($this->getFileLockType($view, $sourcePath, true), 'Source path not locked after operation');
2495
+        $this->assertNull($this->getFileLockType($view, $targetPath, true), 'Target path not locked after operation');
2496
+    }
2497
+
2498
+    /**
2499
+     * Connect hook callbacks for hook type
2500
+     *
2501
+     * @param string $hookType hook type or null for none
2502
+     * @param View $view view to check the lock on
2503
+     * @param string $path path for which to check the lock
2504
+     * @param int $lockTypePre variable to receive lock type that was active in the pre-hook
2505
+     * @param int $lockTypePost variable to receive lock type that was active in the post-hook
2506
+     * @param bool $onMountPoint true to check the mount point instead of the
2507
+     *                           mounted storage
2508
+     */
2509
+    private function connectMockHooks($hookType, $view, $path, &$lockTypePre, &$lockTypePost, $onMountPoint = false) {
2510
+        if ($hookType === null) {
2511
+            return;
2512
+        }
2513
+
2514
+        $eventHandler = $this->getMockBuilder(TestEventHandler::class)
2515
+            ->onlyMethods(['preCallback', 'postCallback'])
2516
+            ->getMock();
2517
+
2518
+        $eventHandler->expects($this->any())
2519
+            ->method('preCallback')
2520
+            ->willReturnCallback(
2521
+                function () use ($view, $path, $onMountPoint, &$lockTypePre): void {
2522
+                    $lockTypePre = $this->getFileLockType($view, $path, $onMountPoint);
2523
+                }
2524
+            );
2525
+        $eventHandler->expects($this->any())
2526
+            ->method('postCallback')
2527
+            ->willReturnCallback(
2528
+                function () use ($view, $path, $onMountPoint, &$lockTypePost): void {
2529
+                    $lockTypePost = $this->getFileLockType($view, $path, $onMountPoint);
2530
+                }
2531
+            );
2532
+
2533
+        if ($hookType !== null) {
2534
+            Util::connectHook(
2535
+                Filesystem::CLASSNAME,
2536
+                $hookType,
2537
+                $eventHandler,
2538
+                'preCallback'
2539
+            );
2540
+            Util::connectHook(
2541
+                Filesystem::CLASSNAME,
2542
+                'post_' . $hookType,
2543
+                $eventHandler,
2544
+                'postCallback'
2545
+            );
2546
+        }
2547
+    }
2548
+
2549
+    /**
2550
+     * Returns the file lock type
2551
+     *
2552
+     * @param View $view view
2553
+     * @param string $path path
2554
+     * @param bool $onMountPoint true to check the mount point instead of the
2555
+     *                           mounted storage
2556
+     *
2557
+     * @return int lock type or null if file was not locked
2558
+     */
2559
+    private function getFileLockType(View $view, $path, $onMountPoint = false) {
2560
+        if ($this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE, $onMountPoint)) {
2561
+            return ILockingProvider::LOCK_EXCLUSIVE;
2562
+        } elseif ($this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED, $onMountPoint)) {
2563
+            return ILockingProvider::LOCK_SHARED;
2564
+        }
2565
+        return null;
2566
+    }
2567
+
2568
+
2569
+    public function testRemoveMoveableMountPoint(): void {
2570
+        $mountPoint = '/' . $this->user . '/files/mount/';
2571
+
2572
+        // Mock the mount point
2573
+        /** @var TestMoveableMountPoint|\PHPUnit\Framework\MockObject\MockObject $mount */
2574
+        $mount = $this->createMock(TestMoveableMountPoint::class);
2575
+        $mount->expects($this->once())
2576
+            ->method('getMountPoint')
2577
+            ->willReturn($mountPoint);
2578
+        $mount->expects($this->once())
2579
+            ->method('removeMount')
2580
+            ->willReturn('foo');
2581
+        $mount->expects($this->any())
2582
+            ->method('getInternalPath')
2583
+            ->willReturn('');
2584
+
2585
+        // Register mount
2586
+        Filesystem::getMountManager()->addMount($mount);
2587
+
2588
+        // Listen for events
2589
+        $eventHandler = $this->getMockBuilder(TestEventHandler::class)
2590
+            ->onlyMethods(['umount', 'post_umount'])
2591
+            ->getMock();
2592
+        $eventHandler->expects($this->once())
2593
+            ->method('umount')
2594
+            ->with([Filesystem::signal_param_path => '/mount']);
2595
+        $eventHandler->expects($this->once())
2596
+            ->method('post_umount')
2597
+            ->with([Filesystem::signal_param_path => '/mount']);
2598
+        Util::connectHook(
2599
+            Filesystem::CLASSNAME,
2600
+            'umount',
2601
+            $eventHandler,
2602
+            'umount'
2603
+        );
2604
+        Util::connectHook(
2605
+            Filesystem::CLASSNAME,
2606
+            'post_umount',
2607
+            $eventHandler,
2608
+            'post_umount'
2609
+        );
2610
+
2611
+        //Delete the mountpoint
2612
+        $view = new View('/' . $this->user . '/files');
2613
+        $this->assertEquals('foo', $view->rmdir('mount'));
2614
+    }
2615
+
2616
+    public static function mimeFilterProvider(): array {
2617
+        return [
2618
+            [null, ['test1.txt', 'test2.txt', 'test3.md', 'test4.png']],
2619
+            ['text/plain', ['test1.txt', 'test2.txt']],
2620
+            ['text/markdown', ['test3.md']],
2621
+            ['text', ['test1.txt', 'test2.txt', 'test3.md']],
2622
+        ];
2623
+    }
2624
+
2625
+    /**
2626
+     * @param string $filter
2627
+     * @param string[] $expected
2628
+     */
2629
+    #[\PHPUnit\Framework\Attributes\DataProvider('mimeFilterProvider')]
2630
+    public function testGetDirectoryContentMimeFilter($filter, $expected): void {
2631
+        $storage1 = new Temporary();
2632
+        $root = self::getUniqueID('/');
2633
+        Filesystem::mount($storage1, [], $root . '/');
2634
+        $view = new View($root);
2635
+
2636
+        $view->file_put_contents('test1.txt', 'asd');
2637
+        $view->file_put_contents('test2.txt', 'asd');
2638
+        $view->file_put_contents('test3.md', 'asd');
2639
+        $view->file_put_contents('test4.png', '');
2640
+
2641
+        $content = $view->getDirectoryContent('', $filter);
2642
+
2643
+        $files = array_map(function (FileInfo $info) {
2644
+            return $info->getName();
2645
+        }, $content);
2646
+        sort($files);
2647
+
2648
+        $this->assertEquals($expected, $files);
2649
+    }
2650
+
2651
+    public function testFilePutContentsClearsChecksum(): void {
2652
+        $storage = new Temporary([]);
2653
+        $scanner = $storage->getScanner();
2654
+        $storage->file_put_contents('foo.txt', 'bar');
2655
+        Filesystem::mount($storage, [], '/test/');
2656
+        $scanner->scan('');
2657
+
2658
+        $view = new View('/test/foo.txt');
2659
+        $view->putFileInfo('.', ['checksum' => '42']);
2660
+
2661
+        $this->assertEquals('bar', $view->file_get_contents(''));
2662
+        $fh = tmpfile();
2663
+        fwrite($fh, 'fooo');
2664
+        rewind($fh);
2665
+        clearstatcache();
2666
+        $view->file_put_contents('', $fh);
2667
+        $this->assertEquals('fooo', $view->file_get_contents(''));
2668
+        $data = $view->getFileInfo('.');
2669
+        $this->assertEquals('', $data->getChecksum());
2670
+    }
2671
+
2672
+    public function testDeleteGhostFile(): void {
2673
+        $storage = new Temporary([]);
2674
+        $scanner = $storage->getScanner();
2675
+        $cache = $storage->getCache();
2676
+        $storage->file_put_contents('foo.txt', 'bar');
2677
+        Filesystem::mount($storage, [], '/test/');
2678
+        $scanner->scan('');
2679
+
2680
+        $storage->unlink('foo.txt');
2681
+
2682
+        $this->assertTrue($cache->inCache('foo.txt'));
2683
+
2684
+        $view = new View('/test');
2685
+        $rootInfo = $view->getFileInfo('');
2686
+        $this->assertEquals(3, $rootInfo->getSize());
2687
+        $view->unlink('foo.txt');
2688
+        $newInfo = $view->getFileInfo('');
2689
+
2690
+        $this->assertFalse($cache->inCache('foo.txt'));
2691
+        $this->assertNotEquals($rootInfo->getEtag(), $newInfo->getEtag());
2692
+        $this->assertEquals(0, $newInfo->getSize());
2693
+    }
2694
+
2695
+    public function testDeleteGhostFolder(): void {
2696
+        $storage = new Temporary([]);
2697
+        $scanner = $storage->getScanner();
2698
+        $cache = $storage->getCache();
2699
+        $storage->mkdir('foo');
2700
+        $storage->file_put_contents('foo/foo.txt', 'bar');
2701
+        Filesystem::mount($storage, [], '/test/');
2702
+        $scanner->scan('');
2703
+
2704
+        $storage->rmdir('foo');
2705
+
2706
+        $this->assertTrue($cache->inCache('foo'));
2707
+        $this->assertTrue($cache->inCache('foo/foo.txt'));
2708
+
2709
+        $view = new View('/test');
2710
+        $rootInfo = $view->getFileInfo('');
2711
+        $this->assertEquals(3, $rootInfo->getSize());
2712
+        $view->rmdir('foo');
2713
+        $newInfo = $view->getFileInfo('');
2714
+
2715
+        $this->assertFalse($cache->inCache('foo'));
2716
+        $this->assertFalse($cache->inCache('foo/foo.txt'));
2717
+        $this->assertNotEquals($rootInfo->getEtag(), $newInfo->getEtag());
2718
+        $this->assertEquals(0, $newInfo->getSize());
2719
+    }
2720
+
2721
+    public function testCreateParentDirectories(): void {
2722
+        $view = $this->getMockBuilder(View::class)
2723
+            ->disableOriginalConstructor()
2724
+            ->onlyMethods([
2725
+                'is_file',
2726
+                'file_exists',
2727
+                'mkdir',
2728
+            ])
2729
+            ->getMock();
2730
+
2731
+        $view->expects($this->exactly(3))
2732
+            ->method('is_file')
2733
+            ->willReturnMap([
2734
+                ['/new', false],
2735
+                ['/new/folder', false],
2736
+                ['/new/folder/structure', false],
2737
+            ]);
2738
+        $view->expects($this->exactly(3))
2739
+            ->method('file_exists')
2740
+            ->willReturnMap([
2741
+                ['/new', true],
2742
+                ['/new/folder', false],
2743
+                ['/new/folder/structure', false],
2744
+            ]);
2745
+
2746
+        $calls = ['/new/folder', '/new/folder/structure'];
2747
+        $view->expects($this->exactly(2))
2748
+            ->method('mkdir')
2749
+            ->willReturnCallback(function ($dir) use (&$calls): void {
2750
+                $expected = array_shift($calls);
2751
+                $this->assertEquals($expected, $dir);
2752
+            });
2753
+
2754
+        $this->assertTrue(self::invokePrivate($view, 'createParentDirectories', ['/new/folder/structure']));
2755
+    }
2756
+
2757
+    public function testCreateParentDirectoriesWithExistingFile(): void {
2758
+        $view = $this->getMockBuilder(View::class)
2759
+            ->disableOriginalConstructor()
2760
+            ->onlyMethods([
2761
+                'is_file',
2762
+                'file_exists',
2763
+                'mkdir',
2764
+            ])
2765
+            ->getMock();
2766
+
2767
+        $view
2768
+            ->expects($this->once())
2769
+            ->method('is_file')
2770
+            ->with('/file.txt')
2771
+            ->willReturn(true);
2772
+        $this->assertFalse(self::invokePrivate($view, 'createParentDirectories', ['/file.txt/folder/structure']));
2773
+    }
2774
+
2775
+    public function testCacheExtension(): void {
2776
+        $storage = new Temporary([]);
2777
+        $scanner = $storage->getScanner();
2778
+        $storage->file_put_contents('foo.txt', 'bar');
2779
+        $scanner->scan('');
2780
+
2781
+        Filesystem::mount($storage, [], '/test/');
2782
+        $view = new View('/test');
2783
+
2784
+        $info = $view->getFileInfo('/foo.txt');
2785
+        $this->assertEquals(0, $info->getUploadTime());
2786
+        $this->assertEquals(0, $info->getCreationTime());
2787
+
2788
+        $view->putFileInfo('/foo.txt', ['upload_time' => 25]);
2789
+
2790
+        $info = $view->getFileInfo('/foo.txt');
2791
+        $this->assertEquals(25, $info->getUploadTime());
2792
+        $this->assertEquals(0, $info->getCreationTime());
2793
+    }
2794
+
2795
+    public function testFopenGone(): void {
2796
+        $storage = new Temporary([]);
2797
+        $scanner = $storage->getScanner();
2798
+        $storage->file_put_contents('foo.txt', 'bar');
2799
+        $scanner->scan('');
2800
+        $cache = $storage->getCache();
2801
+
2802
+        Filesystem::mount($storage, [], '/test/');
2803
+        $view = new View('/test');
2804
+
2805
+        $storage->unlink('foo.txt');
2806
+
2807
+        $this->assertTrue($cache->inCache('foo.txt'));
2808
+
2809
+        $this->assertFalse($view->fopen('foo.txt', 'r'));
2810
+
2811
+        $this->assertFalse($cache->inCache('foo.txt'));
2812
+    }
2813
+
2814
+    public function testMountpointParentsCreated(): void {
2815
+        $storage1 = $this->getTestStorage();
2816
+        Filesystem::mount($storage1, [], '/');
2817
+
2818
+        $storage2 = $this->getTestStorage();
2819
+        Filesystem::mount($storage2, [], '/A/B/C');
2820
+
2821
+        $rootView = new View('');
2822
+
2823
+        $folderData = $rootView->getDirectoryContent('/');
2824
+        $this->assertCount(4, $folderData);
2825
+        $this->assertEquals('folder', $folderData[0]['name']);
2826
+        $this->assertEquals('foo.png', $folderData[1]['name']);
2827
+        $this->assertEquals('foo.txt', $folderData[2]['name']);
2828
+        $this->assertEquals('A', $folderData[3]['name']);
2829
+
2830
+        $folderData = $rootView->getDirectoryContent('/A');
2831
+        $this->assertCount(1, $folderData);
2832
+        $this->assertEquals('B', $folderData[0]['name']);
2833
+
2834
+        $folderData = $rootView->getDirectoryContent('/A/B');
2835
+        $this->assertCount(1, $folderData);
2836
+        $this->assertEquals('C', $folderData[0]['name']);
2837
+
2838
+        $folderData = $rootView->getDirectoryContent('/A/B/C');
2839
+        $this->assertCount(3, $folderData);
2840
+        $this->assertEquals('folder', $folderData[0]['name']);
2841
+        $this->assertEquals('foo.png', $folderData[1]['name']);
2842
+        $this->assertEquals('foo.txt', $folderData[2]['name']);
2843
+    }
2844
+
2845
+    public function testCopyPreservesContent() {
2846
+        $viewUser1 = new View('/' . 'userId' . '/files');
2847
+        $viewUser1->mkdir('');
2848
+        $viewUser1->file_put_contents('foo.txt', 'foo');
2849
+        $viewUser1->copy('foo.txt', 'bar.txt');
2850
+        $this->assertEquals('foo', $viewUser1->file_get_contents('bar.txt'));
2851
+    }
2852 2852
 }
Please login to merge, or discard this patch.
Spacing   +90 added lines, -90 removed lines patch added patch discarded remove patch
@@ -155,7 +155,7 @@  discard block
 block discarded – undo
155 155
 		}
156 156
 
157 157
 		if ($this->tempStorage) {
158
-			system('rm -rf ' . escapeshellarg($this->tempStorage->getDataDir()));
158
+			system('rm -rf '.escapeshellarg($this->tempStorage->getDataDir()));
159 159
 		}
160 160
 
161 161
 		self::logout();
@@ -178,11 +178,11 @@  discard block
 block discarded – undo
178 178
 		$storage2 = $this->getTestStorage();
179 179
 		$storage3 = $this->getTestStorage();
180 180
 		$root = self::getUniqueID('/');
181
-		Filesystem::mount($storage1, [], $root . '/');
182
-		Filesystem::mount($storage2, [], $root . '/substorage');
183
-		Filesystem::mount($storage3, [], $root . '/folder/anotherstorage');
181
+		Filesystem::mount($storage1, [], $root.'/');
182
+		Filesystem::mount($storage2, [], $root.'/substorage');
183
+		Filesystem::mount($storage3, [], $root.'/folder/anotherstorage');
184 184
 		$textSize = strlen("dummy file data\n");
185
-		$imageSize = filesize(\OC::$SERVERROOT . '/core/img/logo/logo.png');
185
+		$imageSize = filesize(\OC::$SERVERROOT.'/core/img/logo/logo.png');
186 186
 		$storageSize = $textSize * 2 + $imageSize;
187 187
 
188 188
 		$storageInfo = $storage3->getCache()->get('');
@@ -239,7 +239,7 @@  discard block
 block discarded – undo
239 239
 		$this->assertEquals('foo.png', $folderData[1]['name']);
240 240
 		$this->assertEquals('foo.txt', $folderData[2]['name']);
241 241
 
242
-		$folderView = new View($root . '/folder');
242
+		$folderView = new View($root.'/folder');
243 243
 		$this->assertEquals($rootView->getFileInfo('/folder'), $folderView->getFileInfo('/'));
244 244
 
245 245
 		$cachedData = $rootView->getFileInfo('/foo.txt');
@@ -504,10 +504,10 @@  discard block
 block discarded – undo
504 504
 	}
505 505
 
506 506
 	public function moveBetweenStorages($storage1, $storage2) {
507
-		Filesystem::mount($storage1, [], '/' . $this->user . '/');
508
-		Filesystem::mount($storage2, [], '/' . $this->user . '/substorage');
507
+		Filesystem::mount($storage1, [], '/'.$this->user.'/');
508
+		Filesystem::mount($storage2, [], '/'.$this->user.'/substorage');
509 509
 
510
-		$rootView = new View('/' . $this->user);
510
+		$rootView = new View('/'.$this->user);
511 511
 		$rootView->rename('foo.txt', 'substorage/folder/foo.txt');
512 512
 		$this->assertFalse($rootView->file_exists('foo.txt'));
513 513
 		$this->assertTrue($rootView->file_exists('substorage/folder/foo.txt'));
@@ -614,11 +614,11 @@  discard block
 block discarded – undo
614 614
 		$storage2 = $this->getTestStorage();
615 615
 		$defaultRoot = Filesystem::getRoot();
616 616
 		Filesystem::mount($storage1, [], '/');
617
-		Filesystem::mount($storage2, [], $defaultRoot . '/substorage');
617
+		Filesystem::mount($storage2, [], $defaultRoot.'/substorage');
618 618
 		\OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHook');
619 619
 
620 620
 		$rootView = new View('');
621
-		$subView = new View($defaultRoot . '/substorage');
621
+		$subView = new View($defaultRoot.'/substorage');
622 622
 		$this->hookPath = null;
623 623
 
624 624
 		$rootView->file_put_contents('/foo.txt', 'asd');
@@ -658,7 +658,7 @@  discard block
 block discarded – undo
658 658
 		 */
659 659
 		$storage = new $class([]);
660 660
 		$textData = "dummy file data\n";
661
-		$imgData = file_get_contents(\OC::$SERVERROOT . '/core/img/logo/logo.png');
661
+		$imgData = file_get_contents(\OC::$SERVERROOT.'/core/img/logo/logo.png');
662 662
 		$storage->mkdir('folder');
663 663
 		$storage->file_put_contents('foo.txt', $textData);
664 664
 		$storage->file_put_contents('foo.png', $imgData);
@@ -677,10 +677,10 @@  discard block
 block discarded – undo
677 677
 		$storage2 = $this->getTestStorage();
678 678
 		$defaultRoot = Filesystem::getRoot();
679 679
 		Filesystem::mount($storage1, [], '/');
680
-		Filesystem::mount($storage2, [], $defaultRoot . '_substorage');
680
+		Filesystem::mount($storage2, [], $defaultRoot.'_substorage');
681 681
 		\OC_Hook::connect('OC_Filesystem', 'post_write', $this, 'dummyHook');
682 682
 
683
-		$subView = new View($defaultRoot . '_substorage');
683
+		$subView = new View($defaultRoot.'_substorage');
684 684
 		$this->hookPath = null;
685 685
 
686 686
 		$subView->file_put_contents('/foo.txt', 'asd');
@@ -773,7 +773,7 @@  discard block
 block discarded – undo
773 773
 
774 774
 		$rootView = new View('');
775 775
 		foreach ($names as $name) {
776
-			$rootView->file_put_contents('/' . $name, 'dummy content');
776
+			$rootView->file_put_contents('/'.$name, 'dummy content');
777 777
 		}
778 778
 
779 779
 		$list = $rootView->getDirectoryContent('/');
@@ -811,15 +811,15 @@  discard block
 block discarded – undo
811 811
 		$depth = ((4000 - $tmpdirLength) / 57);
812 812
 
813 813
 		foreach (range(0, $depth - 1) as $i) {
814
-			$longPath .= $ds . $folderName;
814
+			$longPath .= $ds.$folderName;
815 815
 			$result = $rootView->mkdir($longPath);
816
-			$this->assertTrue($result, "mkdir failed on $i - path length: " . strlen($longPath));
816
+			$this->assertTrue($result, "mkdir failed on $i - path length: ".strlen($longPath));
817 817
 
818
-			$result = $rootView->file_put_contents($longPath . "{$ds}test.txt", 'lorem');
818
+			$result = $rootView->file_put_contents($longPath."{$ds}test.txt", 'lorem');
819 819
 			$this->assertEquals(5, $result, "file_put_contents failed on $i");
820 820
 
821 821
 			$this->assertTrue($rootView->file_exists($longPath));
822
-			$this->assertTrue($rootView->file_exists($longPath . "{$ds}test.txt"));
822
+			$this->assertTrue($rootView->file_exists($longPath."{$ds}test.txt"));
823 823
 		}
824 824
 
825 825
 		$cache = $storage->getCache();
@@ -832,11 +832,11 @@  discard block
 block discarded – undo
832 832
 			$this->assertTrue(is_array($cachedFolder), "No cache entry for folder at $i");
833 833
 			$this->assertEquals($folderName, $cachedFolder['name'], "Wrong cache entry for folder at $i");
834 834
 
835
-			$cachedFile = $cache->get($longPath . '/test.txt');
835
+			$cachedFile = $cache->get($longPath.'/test.txt');
836 836
 			$this->assertTrue(is_array($cachedFile), "No cache entry for file at $i");
837 837
 			$this->assertEquals('test.txt', $cachedFile['name'], "Wrong cache entry for file at $i");
838 838
 
839
-			$longPath .= $ds . $folderName;
839
+			$longPath .= $ds.$folderName;
840 840
 		}
841 841
 	}
842 842
 
@@ -1041,7 +1041,7 @@  discard block
 block discarded – undo
1041 1041
 		$folderName = 'abcdefghijklmnopqrstuvwxyz012345678901234567890123456789';
1042 1042
 		$depth = (4000 / 57);
1043 1043
 		foreach (range(0, $depth + 1) as $i) {
1044
-			$longPath .= '/' . $folderName;
1044
+			$longPath .= '/'.$folderName;
1045 1045
 		}
1046 1046
 
1047 1047
 		$storage = new Temporary([]);
@@ -1294,8 +1294,8 @@  discard block
 block discarded – undo
1294 1294
 		$view = new View($rootPath);
1295 1295
 		$storage = new Temporary([]);
1296 1296
 		Filesystem::mount($storage, [], '/');
1297
-		$this->assertTrue($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1298
-		$view->lockFile($pathPrefix . '/foo/bar/asd', ILockingProvider::LOCK_SHARED);
1297
+		$this->assertTrue($view->lockFile($pathPrefix.'/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1298
+		$view->lockFile($pathPrefix.'/foo/bar/asd', ILockingProvider::LOCK_SHARED);
1299 1299
 	}
1300 1300
 
1301 1301
 	/**
@@ -1313,8 +1313,8 @@  discard block
 block discarded – undo
1313 1313
 		$view = new View($rootPath);
1314 1314
 		$storage = new Temporary([]);
1315 1315
 		Filesystem::mount($storage, [], '/');
1316
-		$this->assertFalse($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1317
-		$this->assertFalse($view->lockFile($pathPrefix . '/foo/bar/asd', ILockingProvider::LOCK_SHARED));
1316
+		$this->assertFalse($view->lockFile($pathPrefix.'/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1317
+		$this->assertFalse($view->lockFile($pathPrefix.'/foo/bar/asd', ILockingProvider::LOCK_SHARED));
1318 1318
 	}
1319 1319
 
1320 1320
 	/**
@@ -1335,8 +1335,8 @@  discard block
 block discarded – undo
1335 1335
 		$view = new View($rootPath);
1336 1336
 		$storage = new Temporary([]);
1337 1337
 		Filesystem::mount($storage, [], '/');
1338
-		$this->assertTrue($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_SHARED));
1339
-		$view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE);
1338
+		$this->assertTrue($view->lockFile($pathPrefix.'/foo/bar', ILockingProvider::LOCK_SHARED));
1339
+		$view->lockFile($pathPrefix.'/foo/bar', ILockingProvider::LOCK_EXCLUSIVE);
1340 1340
 	}
1341 1341
 
1342 1342
 	/**
@@ -1354,8 +1354,8 @@  discard block
 block discarded – undo
1354 1354
 		$view = new View($rootPath);
1355 1355
 		$storage = new Temporary([]);
1356 1356
 		Filesystem::mount($storage, [], '/');
1357
-		$this->assertFalse($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_SHARED));
1358
-		$this->assertFalse($view->lockFile($pathPrefix . '/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1357
+		$this->assertFalse($view->lockFile($pathPrefix.'/foo/bar', ILockingProvider::LOCK_SHARED));
1358
+		$this->assertFalse($view->lockFile($pathPrefix.'/foo/bar', ILockingProvider::LOCK_EXCLUSIVE));
1359 1359
 	}
1360 1360
 
1361 1361
 	/**
@@ -1562,7 +1562,7 @@  discard block
 block discarded – undo
1562 1562
 				->onlyMethods([])
1563 1563
 				->getMock();
1564 1564
 			$storage->method('getId')->willReturn('non-null-id');
1565
-			$storage->method('getStorageCache')->willReturnCallback(function () use ($storage) {
1565
+			$storage->method('getStorageCache')->willReturnCallback(function() use ($storage) {
1566 1566
 				return new \OC\Files\Cache\Storage($storage, true, Server::get(IDBConnection::class));
1567 1567
 			});
1568 1568
 
@@ -1591,8 +1591,8 @@  discard block
 block discarded – undo
1591 1591
 		self::loginAsUser($this->user);
1592 1592
 
1593 1593
 		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1594
-			$this->user . '/files/mount1',
1595
-			$this->user . '/files/mount2',
1594
+			$this->user.'/files/mount1',
1595
+			$this->user.'/files/mount2',
1596 1596
 		]);
1597 1597
 		$mount1->expects($this->once())
1598 1598
 			->method('moveMount')
@@ -1602,7 +1602,7 @@  discard block
 block discarded – undo
1602 1602
 			->method('moveMount')
1603 1603
 			->willReturn(true);
1604 1604
 
1605
-		$view = new View('/' . $this->user . '/files/');
1605
+		$view = new View('/'.$this->user.'/files/');
1606 1606
 		$view->mkdir('sub');
1607 1607
 
1608 1608
 		$this->assertTrue($view->rename('mount1', 'renamed_mount'), 'Can rename mount point');
@@ -1613,8 +1613,8 @@  discard block
 block discarded – undo
1613 1613
 		self::loginAsUser($this->user);
1614 1614
 
1615 1615
 		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1616
-			$this->user . '/files/mount1',
1617
-			$this->user . '/files/mount2',
1616
+			$this->user.'/files/mount1',
1617
+			$this->user.'/files/mount2',
1618 1618
 		]);
1619 1619
 
1620 1620
 		$mount1->expects($this->never())
@@ -1623,7 +1623,7 @@  discard block
 block discarded – undo
1623 1623
 		$mount2->expects($this->never())
1624 1624
 			->method('moveMount');
1625 1625
 
1626
-		$view = new View('/' . $this->user . '/files/');
1626
+		$view = new View('/'.$this->user.'/files/');
1627 1627
 
1628 1628
 		$this->expectException(ForbiddenException::class);
1629 1629
 		$view->rename('mount1', 'mount2');
@@ -1633,8 +1633,8 @@  discard block
 block discarded – undo
1633 1633
 		self::loginAsUser($this->user);
1634 1634
 
1635 1635
 		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1636
-			$this->user . '/files/mount1',
1637
-			$this->user . '/files/mount2',
1636
+			$this->user.'/files/mount1',
1637
+			$this->user.'/files/mount2',
1638 1638
 		]);
1639 1639
 
1640 1640
 		$mount1->expects($this->never())
@@ -1643,7 +1643,7 @@  discard block
 block discarded – undo
1643 1643
 		$mount2->expects($this->never())
1644 1644
 			->method('moveMount');
1645 1645
 
1646
-		$view = new View('/' . $this->user . '/files/');
1646
+		$view = new View('/'.$this->user.'/files/');
1647 1647
 
1648 1648
 		$this->expectException(ForbiddenException::class);
1649 1649
 		$view->rename('mount1', 'mount2/sub');
@@ -1656,8 +1656,8 @@  discard block
 block discarded – undo
1656 1656
 		self::loginAsUser($this->user);
1657 1657
 
1658 1658
 		[$mount1, $mount2] = $this->createTestMovableMountPoints([
1659
-			$this->user . '/files/mount1',
1660
-			$this->user . '/files/mount2',
1659
+			$this->user.'/files/mount1',
1660
+			$this->user.'/files/mount2',
1661 1661
 		]);
1662 1662
 
1663 1663
 		$mount1->expects($this->never())
@@ -1667,7 +1667,7 @@  discard block
 block discarded – undo
1667 1667
 			->method('moveMount')
1668 1668
 			->willReturn(true);
1669 1669
 
1670
-		$view = new View('/' . $this->user . '/files/');
1670
+		$view = new View('/'.$this->user.'/files/');
1671 1671
 		$view->mkdir('shareddir');
1672 1672
 		$view->mkdir('shareddir/sub');
1673 1673
 		$view->mkdir('shareddir/sub2');
@@ -1893,7 +1893,7 @@  discard block
 block discarded – undo
1893 1893
 		$expectedStrayLock = null,
1894 1894
 		$returnValue = true,
1895 1895
 	): void {
1896
-		$view = new View('/' . $this->user . '/files/');
1896
+		$view = new View('/'.$this->user.'/files/');
1897 1897
 
1898 1898
 		/** @var Temporary&MockObject $storage */
1899 1899
 		$storage = $this->getMockBuilder(Temporary::class)
@@ -1905,19 +1905,19 @@  discard block
 block discarded – undo
1905 1905
 		/* Same thing with encryption wrapper */
1906 1906
 		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
1907 1907
 
1908
-		Filesystem::mount($storage, [], $this->user . '/');
1908
+		Filesystem::mount($storage, [], $this->user.'/');
1909 1909
 
1910 1910
 		// work directly on disk because mkdir might be mocked
1911 1911
 		$realPath = $storage->getSourcePath('');
1912
-		mkdir($realPath . '/files');
1913
-		mkdir($realPath . '/files/dir');
1914
-		file_put_contents($realPath . '/files/test.txt', 'blah');
1912
+		mkdir($realPath.'/files');
1913
+		mkdir($realPath.'/files/dir');
1914
+		file_put_contents($realPath.'/files/test.txt', 'blah');
1915 1915
 		$storage->getScanner()->scan('files');
1916 1916
 
1917 1917
 		$storage->expects($this->once())
1918 1918
 			->method($operation)
1919 1919
 			->willReturnCallback(
1920
-				function () use ($view, $lockedPath, &$lockTypeDuring, $returnValue) {
1920
+				function() use ($view, $lockedPath, &$lockTypeDuring, $returnValue) {
1921 1921
 					$lockTypeDuring = $this->getFileLockType($view, $lockedPath);
1922 1922
 
1923 1923
 					return $returnValue;
@@ -1950,7 +1950,7 @@  discard block
 block discarded – undo
1950 1950
 	 * This code path uses $storage->fopen instead
1951 1951
 	 */
1952 1952
 	public function testLockFilePutContentWithStream(): void {
1953
-		$view = new View('/' . $this->user . '/files/');
1953
+		$view = new View('/'.$this->user.'/files/');
1954 1954
 
1955 1955
 		$path = 'test_file_put_contents.txt';
1956 1956
 		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
@@ -1958,13 +1958,13 @@  discard block
 block discarded – undo
1958 1958
 			->onlyMethods(['fopen'])
1959 1959
 			->getMock();
1960 1960
 
1961
-		Filesystem::mount($storage, [], $this->user . '/');
1961
+		Filesystem::mount($storage, [], $this->user.'/');
1962 1962
 		$storage->mkdir('files');
1963 1963
 
1964 1964
 		$storage->expects($this->once())
1965 1965
 			->method('fopen')
1966 1966
 			->willReturnCallback(
1967
-				function () use ($view, $path, &$lockTypeDuring) {
1967
+				function() use ($view, $path, &$lockTypeDuring) {
1968 1968
 					$lockTypeDuring = $this->getFileLockType($view, $path);
1969 1969
 
1970 1970
 					return fopen('php://temp', 'r+');
@@ -1989,7 +1989,7 @@  discard block
 block discarded – undo
1989 1989
 	 * Test locks for fopen with fclose at the end
1990 1990
 	 */
1991 1991
 	public function testLockFopen(): void {
1992
-		$view = new View('/' . $this->user . '/files/');
1992
+		$view = new View('/'.$this->user.'/files/');
1993 1993
 
1994 1994
 		$path = 'test_file_put_contents.txt';
1995 1995
 		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
@@ -1997,13 +1997,13 @@  discard block
 block discarded – undo
1997 1997
 			->onlyMethods(['fopen'])
1998 1998
 			->getMock();
1999 1999
 
2000
-		Filesystem::mount($storage, [], $this->user . '/');
2000
+		Filesystem::mount($storage, [], $this->user.'/');
2001 2001
 		$storage->mkdir('files');
2002 2002
 
2003 2003
 		$storage->expects($this->once())
2004 2004
 			->method('fopen')
2005 2005
 			->willReturnCallback(
2006
-				function () use ($view, $path, &$lockTypeDuring) {
2006
+				function() use ($view, $path, &$lockTypeDuring) {
2007 2007
 					$lockTypeDuring = $this->getFileLockType($view, $path);
2008 2008
 
2009 2009
 					return fopen('php://temp', 'r+');
@@ -2045,7 +2045,7 @@  discard block
 block discarded – undo
2045 2045
 		if ($operation === 'touch') {
2046 2046
 			$this->markTestSkipped('touch handles storage exceptions internally');
2047 2047
 		}
2048
-		$view = new View('/' . $this->user . '/files/');
2048
+		$view = new View('/'.$this->user.'/files/');
2049 2049
 
2050 2050
 		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2051 2051
 		$storage = $this->getMockBuilder(Temporary::class)
@@ -2057,19 +2057,19 @@  discard block
 block discarded – undo
2057 2057
 		/* Same thing with encryption wrapper */
2058 2058
 		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2059 2059
 
2060
-		Filesystem::mount($storage, [], $this->user . '/');
2060
+		Filesystem::mount($storage, [], $this->user.'/');
2061 2061
 
2062 2062
 		// work directly on disk because mkdir might be mocked
2063 2063
 		$realPath = $storage->getSourcePath('');
2064
-		mkdir($realPath . '/files');
2065
-		mkdir($realPath . '/files/dir');
2066
-		file_put_contents($realPath . '/files/test.txt', 'blah');
2064
+		mkdir($realPath.'/files');
2065
+		mkdir($realPath.'/files/dir');
2066
+		file_put_contents($realPath.'/files/test.txt', 'blah');
2067 2067
 		$storage->getScanner()->scan('files');
2068 2068
 
2069 2069
 		$storage->expects($this->once())
2070 2070
 			->method($operation)
2071 2071
 			->willReturnCallback(
2072
-				function (): void {
2072
+				function(): void {
2073 2073
 					throw new \Exception('Simulated exception');
2074 2074
 				}
2075 2075
 			);
@@ -2089,11 +2089,11 @@  discard block
 block discarded – undo
2089 2089
 	}
2090 2090
 
2091 2091
 	public function testLockBasicOperationUnlocksAfterLockException(): void {
2092
-		$view = new View('/' . $this->user . '/files/');
2092
+		$view = new View('/'.$this->user.'/files/');
2093 2093
 
2094 2094
 		$storage = new Temporary([]);
2095 2095
 
2096
-		Filesystem::mount($storage, [], $this->user . '/');
2096
+		Filesystem::mount($storage, [], $this->user.'/');
2097 2097
 
2098 2098
 		$storage->mkdir('files');
2099 2099
 		$storage->mkdir('files/dir');
@@ -2135,14 +2135,14 @@  discard block
 block discarded – undo
2135 2135
 		$path,
2136 2136
 		$hookType,
2137 2137
 	): void {
2138
-		$view = new View('/' . $this->user . '/files/');
2138
+		$view = new View('/'.$this->user.'/files/');
2139 2139
 
2140 2140
 		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2141 2141
 		$storage = $this->getMockBuilder(Temporary::class)
2142 2142
 			->onlyMethods([$operation])
2143 2143
 			->getMock();
2144 2144
 
2145
-		Filesystem::mount($storage, [], $this->user . '/');
2145
+		Filesystem::mount($storage, [], $this->user.'/');
2146 2146
 		$storage->mkdir('files');
2147 2147
 
2148 2148
 		Util::connectHook(
@@ -2174,7 +2174,7 @@  discard block
 block discarded – undo
2174 2174
 	 */
2175 2175
 	#[\PHPUnit\Framework\Attributes\DataProvider('lockFileRenameOrCopyDataProvider')]
2176 2176
 	public function testLockFileRename($operation, $expectedLockTypeSourceDuring): void {
2177
-		$view = new View('/' . $this->user . '/files/');
2177
+		$view = new View('/'.$this->user.'/files/');
2178 2178
 
2179 2179
 		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2180 2180
 		$storage = $this->getMockBuilder(Temporary::class)
@@ -2200,14 +2200,14 @@  discard block
 block discarded – undo
2200 2200
 		/* Disable encryption wrapper to avoid it intercepting mocked call */
2201 2201
 		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2202 2202
 
2203
-		Filesystem::mount($storage, [], $this->user . '/');
2203
+		Filesystem::mount($storage, [], $this->user.'/');
2204 2204
 		$storage->mkdir('files');
2205 2205
 		$view->file_put_contents($sourcePath, 'meh');
2206 2206
 
2207 2207
 		$storage->expects($this->once())
2208 2208
 			->method($operation)
2209 2209
 			->willReturnCallback(
2210
-				function () use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2210
+				function() use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2211 2211
 					$lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath);
2212 2212
 					$lockTypeTargetDuring = $this->getFileLockType($view, $targetPath);
2213 2213
 
@@ -2243,7 +2243,7 @@  discard block
 block discarded – undo
2243 2243
 	public function testLockFileCopyException(): void {
2244 2244
 		$this->expectException(\Exception::class);
2245 2245
 
2246
-		$view = new View('/' . $this->user . '/files/');
2246
+		$view = new View('/'.$this->user.'/files/');
2247 2247
 
2248 2248
 		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2249 2249
 		$storage = $this->getMockBuilder(Temporary::class)
@@ -2256,14 +2256,14 @@  discard block
 block discarded – undo
2256 2256
 		/* Disable encryption wrapper to avoid it intercepting mocked call */
2257 2257
 		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2258 2258
 
2259
-		Filesystem::mount($storage, [], $this->user . '/');
2259
+		Filesystem::mount($storage, [], $this->user.'/');
2260 2260
 		$storage->mkdir('files');
2261 2261
 		$view->file_put_contents($sourcePath, 'meh');
2262 2262
 
2263 2263
 		$storage->expects($this->once())
2264 2264
 			->method('copy')
2265 2265
 			->willReturnCallback(
2266
-				function (): void {
2266
+				function(): void {
2267 2267
 					throw new \Exception();
2268 2268
 				}
2269 2269
 			);
@@ -2289,7 +2289,7 @@  discard block
 block discarded – undo
2289 2289
 	public function testLockFileRenameUnlockOnException(): void {
2290 2290
 		self::loginAsUser('test');
2291 2291
 
2292
-		$view = new View('/' . $this->user . '/files/');
2292
+		$view = new View('/'.$this->user.'/files/');
2293 2293
 
2294 2294
 		$sourcePath = 'original.txt';
2295 2295
 		$targetPath = 'target.txt';
@@ -2330,7 +2330,7 @@  discard block
 block discarded – undo
2330 2330
 		$this->assertEquals('test', $view->getFileInfo($path)->getOwner()->getUID());
2331 2331
 
2332 2332
 		$folderInfo = $view->getDirectoryContent('');
2333
-		$folderInfo = array_values(array_filter($folderInfo, function (FileInfo $info) {
2333
+		$folderInfo = array_values(array_filter($folderInfo, function(FileInfo $info) {
2334 2334
 			return $info->getName() === 'foo.txt';
2335 2335
 		}));
2336 2336
 
@@ -2340,7 +2340,7 @@  discard block
 block discarded – undo
2340 2340
 		Filesystem::mount($subStorage, [], '/test/files/asd');
2341 2341
 
2342 2342
 		$folderInfo = $view->getDirectoryContent('');
2343
-		$folderInfo = array_values(array_filter($folderInfo, function (FileInfo $info) {
2343
+		$folderInfo = array_values(array_filter($folderInfo, function(FileInfo $info) {
2344 2344
 			return $info->getName() === 'asd';
2345 2345
 		}));
2346 2346
 
@@ -2365,7 +2365,7 @@  discard block
 block discarded – undo
2365 2365
 	 */
2366 2366
 	#[\PHPUnit\Framework\Attributes\DataProvider('lockFileRenameOrCopyCrossStorageDataProvider')]
2367 2367
 	public function testLockFileRenameCrossStorage($viewOperation, $storageOperation, $expectedLockTypeSourceDuring): void {
2368
-		$view = new View('/' . $this->user . '/files/');
2368
+		$view = new View('/'.$this->user.'/files/');
2369 2369
 
2370 2370
 		/** @var Temporary|\PHPUnit\Framework\MockObject\MockObject $storage */
2371 2371
 		$storage = $this->getMockBuilder(Temporary::class)
@@ -2395,8 +2395,8 @@  discard block
 block discarded – undo
2395 2395
 		/* Disable encryption wrapper to avoid it intercepting mocked call */
2396 2396
 		Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption');
2397 2397
 
2398
-		Filesystem::mount($storage, [], $this->user . '/');
2399
-		Filesystem::mount($storage2, [], $this->user . '/files/substorage');
2398
+		Filesystem::mount($storage, [], $this->user.'/');
2399
+		Filesystem::mount($storage2, [], $this->user.'/files/substorage');
2400 2400
 		$storage->mkdir('files');
2401 2401
 		$view->file_put_contents($sourcePath, 'meh');
2402 2402
 		$storage2->getUpdater()->update('');
@@ -2406,7 +2406,7 @@  discard block
 block discarded – undo
2406 2406
 		$storage2->expects($this->once())
2407 2407
 			->method($storageOperation)
2408 2408
 			->willReturnCallback(
2409
-				function () use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2409
+				function() use ($view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring) {
2410 2410
 					$lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath);
2411 2411
 					$lockTypeTargetDuring = $this->getFileLockType($view, $targetPath);
2412 2412
 
@@ -2441,10 +2441,10 @@  discard block
 block discarded – undo
2441 2441
 		self::loginAsUser('test');
2442 2442
 
2443 2443
 		[$mount] = $this->createTestMovableMountPoints([
2444
-			$this->user . '/files/substorage',
2444
+			$this->user.'/files/substorage',
2445 2445
 		]);
2446 2446
 
2447
-		$view = new View('/' . $this->user . '/files/');
2447
+		$view = new View('/'.$this->user.'/files/');
2448 2448
 		$view->mkdir('subdir');
2449 2449
 
2450 2450
 		$sourcePath = 'substorage';
@@ -2453,7 +2453,7 @@  discard block
 block discarded – undo
2453 2453
 		$mount->expects($this->once())
2454 2454
 			->method('moveMount')
2455 2455
 			->willReturnCallback(
2456
-				function ($target) use ($mount, $view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring, &$lockTypeSharedRootDuring) {
2456
+				function($target) use ($mount, $view, $sourcePath, $targetPath, &$lockTypeSourceDuring, &$lockTypeTargetDuring, &$lockTypeSharedRootDuring) {
2457 2457
 					$lockTypeSourceDuring = $this->getFileLockType($view, $sourcePath, true);
2458 2458
 					$lockTypeTargetDuring = $this->getFileLockType($view, $targetPath, true);
2459 2459
 
@@ -2518,14 +2518,14 @@  discard block
 block discarded – undo
2518 2518
 		$eventHandler->expects($this->any())
2519 2519
 			->method('preCallback')
2520 2520
 			->willReturnCallback(
2521
-				function () use ($view, $path, $onMountPoint, &$lockTypePre): void {
2521
+				function() use ($view, $path, $onMountPoint, &$lockTypePre): void {
2522 2522
 					$lockTypePre = $this->getFileLockType($view, $path, $onMountPoint);
2523 2523
 				}
2524 2524
 			);
2525 2525
 		$eventHandler->expects($this->any())
2526 2526
 			->method('postCallback')
2527 2527
 			->willReturnCallback(
2528
-				function () use ($view, $path, $onMountPoint, &$lockTypePost): void {
2528
+				function() use ($view, $path, $onMountPoint, &$lockTypePost): void {
2529 2529
 					$lockTypePost = $this->getFileLockType($view, $path, $onMountPoint);
2530 2530
 				}
2531 2531
 			);
@@ -2539,7 +2539,7 @@  discard block
 block discarded – undo
2539 2539
 			);
2540 2540
 			Util::connectHook(
2541 2541
 				Filesystem::CLASSNAME,
2542
-				'post_' . $hookType,
2542
+				'post_'.$hookType,
2543 2543
 				$eventHandler,
2544 2544
 				'postCallback'
2545 2545
 			);
@@ -2567,7 +2567,7 @@  discard block
 block discarded – undo
2567 2567
 
2568 2568
 
2569 2569
 	public function testRemoveMoveableMountPoint(): void {
2570
-		$mountPoint = '/' . $this->user . '/files/mount/';
2570
+		$mountPoint = '/'.$this->user.'/files/mount/';
2571 2571
 
2572 2572
 		// Mock the mount point
2573 2573
 		/** @var TestMoveableMountPoint|\PHPUnit\Framework\MockObject\MockObject $mount */
@@ -2609,7 +2609,7 @@  discard block
 block discarded – undo
2609 2609
 		);
2610 2610
 
2611 2611
 		//Delete the mountpoint
2612
-		$view = new View('/' . $this->user . '/files');
2612
+		$view = new View('/'.$this->user.'/files');
2613 2613
 		$this->assertEquals('foo', $view->rmdir('mount'));
2614 2614
 	}
2615 2615
 
@@ -2630,7 +2630,7 @@  discard block
 block discarded – undo
2630 2630
 	public function testGetDirectoryContentMimeFilter($filter, $expected): void {
2631 2631
 		$storage1 = new Temporary();
2632 2632
 		$root = self::getUniqueID('/');
2633
-		Filesystem::mount($storage1, [], $root . '/');
2633
+		Filesystem::mount($storage1, [], $root.'/');
2634 2634
 		$view = new View($root);
2635 2635
 
2636 2636
 		$view->file_put_contents('test1.txt', 'asd');
@@ -2640,7 +2640,7 @@  discard block
 block discarded – undo
2640 2640
 
2641 2641
 		$content = $view->getDirectoryContent('', $filter);
2642 2642
 
2643
-		$files = array_map(function (FileInfo $info) {
2643
+		$files = array_map(function(FileInfo $info) {
2644 2644
 			return $info->getName();
2645 2645
 		}, $content);
2646 2646
 		sort($files);
@@ -2746,7 +2746,7 @@  discard block
 block discarded – undo
2746 2746
 		$calls = ['/new/folder', '/new/folder/structure'];
2747 2747
 		$view->expects($this->exactly(2))
2748 2748
 			->method('mkdir')
2749
-			->willReturnCallback(function ($dir) use (&$calls): void {
2749
+			->willReturnCallback(function($dir) use (&$calls): void {
2750 2750
 				$expected = array_shift($calls);
2751 2751
 				$this->assertEquals($expected, $dir);
2752 2752
 			});
@@ -2843,7 +2843,7 @@  discard block
 block discarded – undo
2843 2843
 	}
2844 2844
 
2845 2845
 	public function testCopyPreservesContent() {
2846
-		$viewUser1 = new View('/' . 'userId' . '/files');
2846
+		$viewUser1 = new View('/'.'userId'.'/files');
2847 2847
 		$viewUser1->mkdir('');
2848 2848
 		$viewUser1->file_put_contents('foo.txt', 'foo');
2849 2849
 		$viewUser1->copy('foo.txt', 'bar.txt');
Please login to merge, or discard this patch.