Passed
Push — master ( e81fdf...9f1d49 )
by Robin
16:12 queued 13s
created
lib/private/legacy/OC_Util.php 2 patches
Indentation   +1136 added lines, -1136 removed lines patch added patch discarded remove patch
@@ -76,1143 +76,1143 @@
 block discarded – undo
76 76
 use Psr\Log\LoggerInterface;
77 77
 
78 78
 class OC_Util {
79
-	public static $scripts = [];
80
-	public static $styles = [];
81
-	public static $headers = [];
82
-
83
-	/** @var array Local cache of version.php */
84
-	private static $versionCache = null;
85
-
86
-	protected static function getAppManager() {
87
-		return \OC::$server->getAppManager();
88
-	}
89
-
90
-	/**
91
-	 * Setup the file system
92
-	 *
93
-	 * @param string|null $user
94
-	 * @return boolean
95
-	 * @description configure the initial filesystem based on the configuration
96
-	 * @suppress PhanDeprecatedFunction
97
-	 * @suppress PhanAccessMethodInternal
98
-	 */
99
-	public static function setupFS(?string $user = '') {
100
-		// If we are not forced to load a specific user we load the one that is logged in
101
-		if ($user === '') {
102
-			$userObject = \OC::$server->get(\OCP\IUserSession::class)->getUser();
103
-		} else {
104
-			$userObject = \OC::$server->get(\OCP\IUserManager::class)->get($user);
105
-		}
106
-
107
-		/** @var SetupManager $setupManager */
108
-		$setupManager = \OC::$server->get(SetupManager::class);
109
-
110
-		if ($userObject) {
111
-			$setupManager->setupForUser($userObject);
112
-		} else {
113
-			$setupManager->setupRoot();
114
-		}
115
-		return true;
116
-	}
117
-
118
-	/**
119
-	 * Check if a password is required for each public link
120
-	 *
121
-	 * @param bool $checkGroupMembership Check group membership exclusion
122
-	 * @return boolean
123
-	 * @suppress PhanDeprecatedFunction
124
-	 */
125
-	public static function isPublicLinkPasswordRequired(bool $checkGroupMembership = true) {
126
-		/** @var IManager $shareManager */
127
-		$shareManager = \OC::$server->get(IManager::class);
128
-		return $shareManager->shareApiLinkEnforcePassword($checkGroupMembership);
129
-	}
130
-
131
-	/**
132
-	 * check if sharing is disabled for the current user
133
-	 * @param IConfig $config
134
-	 * @param IGroupManager $groupManager
135
-	 * @param IUser|null $user
136
-	 * @return bool
137
-	 */
138
-	public static function isSharingDisabledForUser(IConfig $config, IGroupManager $groupManager, $user) {
139
-		/** @var IManager $shareManager */
140
-		$shareManager = \OC::$server->get(IManager::class);
141
-		$userId = $user ? $user->getUID() : null;
142
-		return $shareManager->sharingDisabledForUser($userId);
143
-	}
144
-
145
-	/**
146
-	 * check if share API enforces a default expire date
147
-	 *
148
-	 * @return bool
149
-	 * @suppress PhanDeprecatedFunction
150
-	 */
151
-	public static function isDefaultExpireDateEnforced() {
152
-		/** @var IManager $shareManager */
153
-		$shareManager = \OC::$server->get(IManager::class);
154
-		return $shareManager->shareApiLinkDefaultExpireDateEnforced();
155
-	}
156
-
157
-	/**
158
-	 * Get the quota of a user
159
-	 *
160
-	 * @param IUser|null $user
161
-	 * @return int|\OCP\Files\FileInfo::SPACE_UNLIMITED|false|float Quota bytes
162
-	 */
163
-	public static function getUserQuota(?IUser $user) {
164
-		if (is_null($user)) {
165
-			return \OCP\Files\FileInfo::SPACE_UNLIMITED;
166
-		}
167
-		$userQuota = $user->getQuota();
168
-		if ($userQuota === 'none') {
169
-			return \OCP\Files\FileInfo::SPACE_UNLIMITED;
170
-		}
171
-		return OC_Helper::computerFileSize($userQuota);
172
-	}
173
-
174
-	/**
175
-	 * copies the skeleton to the users /files
176
-	 *
177
-	 * @param string $userId
178
-	 * @param \OCP\Files\Folder $userDirectory
179
-	 * @throws \OCP\Files\NotFoundException
180
-	 * @throws \OCP\Files\NotPermittedException
181
-	 * @suppress PhanDeprecatedFunction
182
-	 */
183
-	public static function copySkeleton($userId, \OCP\Files\Folder $userDirectory) {
184
-		/** @var LoggerInterface $logger */
185
-		$logger = \OC::$server->get(LoggerInterface::class);
186
-
187
-		$plainSkeletonDirectory = \OC::$server->getConfig()->getSystemValueString('skeletondirectory', \OC::$SERVERROOT . '/core/skeleton');
188
-		$userLang = \OC::$server->getL10NFactory()->findLanguage();
189
-		$skeletonDirectory = str_replace('{lang}', $userLang, $plainSkeletonDirectory);
190
-
191
-		if (!file_exists($skeletonDirectory)) {
192
-			$dialectStart = strpos($userLang, '_');
193
-			if ($dialectStart !== false) {
194
-				$skeletonDirectory = str_replace('{lang}', substr($userLang, 0, $dialectStart), $plainSkeletonDirectory);
195
-			}
196
-			if ($dialectStart === false || !file_exists($skeletonDirectory)) {
197
-				$skeletonDirectory = str_replace('{lang}', 'default', $plainSkeletonDirectory);
198
-			}
199
-			if (!file_exists($skeletonDirectory)) {
200
-				$skeletonDirectory = '';
201
-			}
202
-		}
203
-
204
-		$instanceId = \OC::$server->getConfig()->getSystemValue('instanceid', '');
205
-
206
-		if ($instanceId === null) {
207
-			throw new \RuntimeException('no instance id!');
208
-		}
209
-		$appdata = 'appdata_' . $instanceId;
210
-		if ($userId === $appdata) {
211
-			throw new \RuntimeException('username is reserved name: ' . $appdata);
212
-		}
213
-
214
-		if (!empty($skeletonDirectory)) {
215
-			$logger->debug('copying skeleton for '.$userId.' from '.$skeletonDirectory.' to '.$userDirectory->getFullPath('/'), ['app' => 'files_skeleton']);
216
-			self::copyr($skeletonDirectory, $userDirectory);
217
-			// update the file cache
218
-			$userDirectory->getStorage()->getScanner()->scan('', \OC\Files\Cache\Scanner::SCAN_RECURSIVE);
219
-
220
-			/** @var ITemplateManager $templateManager */
221
-			$templateManager = \OC::$server->get(ITemplateManager::class);
222
-			$templateManager->initializeTemplateDirectory(null, $userId);
223
-		}
224
-	}
225
-
226
-	/**
227
-	 * copies a directory recursively by using streams
228
-	 *
229
-	 * @param string $source
230
-	 * @param \OCP\Files\Folder $target
231
-	 * @return void
232
-	 */
233
-	public static function copyr($source, \OCP\Files\Folder $target) {
234
-		$logger = \OC::$server->getLogger();
235
-
236
-		// Verify if folder exists
237
-		$dir = opendir($source);
238
-		if ($dir === false) {
239
-			$logger->error(sprintf('Could not opendir "%s"', $source), ['app' => 'core']);
240
-			return;
241
-		}
242
-
243
-		// Copy the files
244
-		while (false !== ($file = readdir($dir))) {
245
-			if (!\OC\Files\Filesystem::isIgnoredDir($file)) {
246
-				if (is_dir($source . '/' . $file)) {
247
-					$child = $target->newFolder($file);
248
-					self::copyr($source . '/' . $file, $child);
249
-				} else {
250
-					$child = $target->newFile($file);
251
-					$sourceStream = fopen($source . '/' . $file, 'r');
252
-					if ($sourceStream === false) {
253
-						$logger->error(sprintf('Could not fopen "%s"', $source . '/' . $file), ['app' => 'core']);
254
-						closedir($dir);
255
-						return;
256
-					}
257
-					$child->putContent($sourceStream);
258
-				}
259
-			}
260
-		}
261
-		closedir($dir);
262
-	}
263
-
264
-	/**
265
-	 * @return void
266
-	 * @suppress PhanUndeclaredMethod
267
-	 */
268
-	public static function tearDownFS() {
269
-		/** @var SetupManager $setupManager */
270
-		$setupManager = \OC::$server->get(SetupManager::class);
271
-		$setupManager->tearDown();
272
-	}
273
-
274
-	/**
275
-	 * get the current installed version of ownCloud
276
-	 *
277
-	 * @return array
278
-	 */
279
-	public static function getVersion() {
280
-		OC_Util::loadVersion();
281
-		return self::$versionCache['OC_Version'];
282
-	}
283
-
284
-	/**
285
-	 * get the current installed version string of ownCloud
286
-	 *
287
-	 * @return string
288
-	 */
289
-	public static function getVersionString() {
290
-		OC_Util::loadVersion();
291
-		return self::$versionCache['OC_VersionString'];
292
-	}
293
-
294
-	/**
295
-	 * @deprecated the value is of no use anymore
296
-	 * @return string
297
-	 */
298
-	public static function getEditionString() {
299
-		return '';
300
-	}
301
-
302
-	/**
303
-	 * @description get the update channel of the current installed of ownCloud.
304
-	 * @return string
305
-	 */
306
-	public static function getChannel() {
307
-		OC_Util::loadVersion();
308
-		return \OC::$server->getConfig()->getSystemValueString('updater.release.channel', self::$versionCache['OC_Channel']);
309
-	}
310
-
311
-	/**
312
-	 * @description get the build number of the current installed of ownCloud.
313
-	 * @return string
314
-	 */
315
-	public static function getBuild() {
316
-		OC_Util::loadVersion();
317
-		return self::$versionCache['OC_Build'];
318
-	}
319
-
320
-	/**
321
-	 * @description load the version.php into the session as cache
322
-	 * @suppress PhanUndeclaredVariable
323
-	 */
324
-	private static function loadVersion() {
325
-		if (self::$versionCache !== null) {
326
-			return;
327
-		}
328
-
329
-		require OC::$SERVERROOT . '/version.php';
330
-		/** @var int $timestamp */
331
-		self::$versionCache['OC_Version_Timestamp'] = \OC::$VERSION_MTIME;
332
-		/** @var string $OC_Version */
333
-		self::$versionCache['OC_Version'] = $OC_Version;
334
-		/** @var string $OC_VersionString */
335
-		self::$versionCache['OC_VersionString'] = $OC_VersionString;
336
-		/** @var string $OC_Build */
337
-		self::$versionCache['OC_Build'] = $OC_Build;
338
-
339
-		/** @var string $OC_Channel */
340
-		self::$versionCache['OC_Channel'] = $OC_Channel;
341
-	}
342
-
343
-	/**
344
-	 * generates a path for JS/CSS files. If no application is provided it will create the path for core.
345
-	 *
346
-	 * @param string $application application to get the files from
347
-	 * @param string $directory directory within this application (css, js, vendor, etc)
348
-	 * @param string $file the file inside of the above folder
349
-	 * @return string the path
350
-	 */
351
-	private static function generatePath($application, $directory, $file) {
352
-		if (is_null($file)) {
353
-			$file = $application;
354
-			$application = "";
355
-		}
356
-		if (!empty($application)) {
357
-			return "$application/$directory/$file";
358
-		} else {
359
-			return "$directory/$file";
360
-		}
361
-	}
362
-
363
-	/**
364
-	 * add a javascript file
365
-	 *
366
-	 * @deprecated 24.0.0 - Use \OCP\Util::addScript
367
-	 *
368
-	 * @param string $application application id
369
-	 * @param string|null $file filename
370
-	 * @param bool $prepend prepend the Script to the beginning of the list
371
-	 * @return void
372
-	 */
373
-	public static function addScript($application, $file = null, $prepend = false) {
374
-		$path = OC_Util::generatePath($application, 'js', $file);
375
-
376
-		// core js files need separate handling
377
-		if ($application !== 'core' && $file !== null) {
378
-			self::addTranslations($application);
379
-		}
380
-		self::addExternalResource($application, $prepend, $path, "script");
381
-	}
382
-
383
-	/**
384
-	 * add a javascript file from the vendor sub folder
385
-	 *
386
-	 * @param string $application application id
387
-	 * @param string|null $file filename
388
-	 * @param bool $prepend prepend the Script to the beginning of the list
389
-	 * @return void
390
-	 */
391
-	public static function addVendorScript($application, $file = null, $prepend = false) {
392
-		$path = OC_Util::generatePath($application, 'vendor', $file);
393
-		self::addExternalResource($application, $prepend, $path, "script");
394
-	}
395
-
396
-	/**
397
-	 * add a translation JS file
398
-	 *
399
-	 * @deprecated 24.0.0
400
-	 *
401
-	 * @param string $application application id
402
-	 * @param string|null $languageCode language code, defaults to the current language
403
-	 * @param bool|null $prepend prepend the Script to the beginning of the list
404
-	 */
405
-	public static function addTranslations($application, $languageCode = null, $prepend = false) {
406
-		if (is_null($languageCode)) {
407
-			$languageCode = \OC::$server->getL10NFactory()->findLanguage($application);
408
-		}
409
-		if (!empty($application)) {
410
-			$path = "$application/l10n/$languageCode";
411
-		} else {
412
-			$path = "l10n/$languageCode";
413
-		}
414
-		self::addExternalResource($application, $prepend, $path, "script");
415
-	}
416
-
417
-	/**
418
-	 * add a css file
419
-	 *
420
-	 * @param string $application application id
421
-	 * @param string|null $file filename
422
-	 * @param bool $prepend prepend the Style to the beginning of the list
423
-	 * @return void
424
-	 */
425
-	public static function addStyle($application, $file = null, $prepend = false) {
426
-		$path = OC_Util::generatePath($application, 'css', $file);
427
-		self::addExternalResource($application, $prepend, $path, "style");
428
-	}
429
-
430
-	/**
431
-	 * add a css file from the vendor sub folder
432
-	 *
433
-	 * @param string $application application id
434
-	 * @param string|null $file filename
435
-	 * @param bool $prepend prepend the Style to the beginning of the list
436
-	 * @return void
437
-	 */
438
-	public static function addVendorStyle($application, $file = null, $prepend = false) {
439
-		$path = OC_Util::generatePath($application, 'vendor', $file);
440
-		self::addExternalResource($application, $prepend, $path, "style");
441
-	}
442
-
443
-	/**
444
-	 * add an external resource css/js file
445
-	 *
446
-	 * @param string $application application id
447
-	 * @param bool $prepend prepend the file to the beginning of the list
448
-	 * @param string $path
449
-	 * @param string $type (script or style)
450
-	 * @return void
451
-	 */
452
-	private static function addExternalResource($application, $prepend, $path, $type = "script") {
453
-		if ($type === "style") {
454
-			if (!in_array($path, self::$styles)) {
455
-				if ($prepend === true) {
456
-					array_unshift(self::$styles, $path);
457
-				} else {
458
-					self::$styles[] = $path;
459
-				}
460
-			}
461
-		} elseif ($type === "script") {
462
-			if (!in_array($path, self::$scripts)) {
463
-				if ($prepend === true) {
464
-					array_unshift(self::$scripts, $path);
465
-				} else {
466
-					self::$scripts [] = $path;
467
-				}
468
-			}
469
-		}
470
-	}
471
-
472
-	/**
473
-	 * Add a custom element to the header
474
-	 * If $text is null then the element will be written as empty element.
475
-	 * So use "" to get a closing tag.
476
-	 * @param string $tag tag name of the element
477
-	 * @param array $attributes array of attributes for the element
478
-	 * @param string $text the text content for the element
479
-	 * @param bool $prepend prepend the header to the beginning of the list
480
-	 */
481
-	public static function addHeader($tag, $attributes, $text = null, $prepend = false) {
482
-		$header = [
483
-			'tag' => $tag,
484
-			'attributes' => $attributes,
485
-			'text' => $text
486
-		];
487
-		if ($prepend === true) {
488
-			array_unshift(self::$headers, $header);
489
-		} else {
490
-			self::$headers[] = $header;
491
-		}
492
-	}
493
-
494
-	/**
495
-	 * check if the current server configuration is suitable for ownCloud
496
-	 *
497
-	 * @param \OC\SystemConfig $config
498
-	 * @return array arrays with error messages and hints
499
-	 */
500
-	public static function checkServer(\OC\SystemConfig $config) {
501
-		$l = \OC::$server->getL10N('lib');
502
-		$errors = [];
503
-		$CONFIG_DATADIRECTORY = $config->getValue('datadirectory', OC::$SERVERROOT . '/data');
504
-
505
-		if (!self::needUpgrade($config) && $config->getValue('installed', false)) {
506
-			// this check needs to be done every time
507
-			$errors = self::checkDataDirectoryValidity($CONFIG_DATADIRECTORY);
508
-		}
509
-
510
-		// Assume that if checkServer() succeeded before in this session, then all is fine.
511
-		if (\OC::$server->getSession()->exists('checkServer_succeeded') && \OC::$server->getSession()->get('checkServer_succeeded')) {
512
-			return $errors;
513
-		}
514
-
515
-		$webServerRestart = false;
516
-		$setup = new \OC\Setup(
517
-			$config,
518
-			\OC::$server->get(IniGetWrapper::class),
519
-			\OC::$server->getL10N('lib'),
520
-			\OC::$server->get(\OCP\Defaults::class),
521
-			\OC::$server->get(LoggerInterface::class),
522
-			\OC::$server->getSecureRandom(),
523
-			\OC::$server->get(\OC\Installer::class)
524
-		);
525
-
526
-		$urlGenerator = \OC::$server->getURLGenerator();
527
-
528
-		$availableDatabases = $setup->getSupportedDatabases();
529
-		if (empty($availableDatabases)) {
530
-			$errors[] = [
531
-				'error' => $l->t('No database drivers (sqlite, mysql, or postgresql) installed.'),
532
-				'hint' => '' //TODO: sane hint
533
-			];
534
-			$webServerRestart = true;
535
-		}
536
-
537
-		// Check if config folder is writable.
538
-		if (!OC_Helper::isReadOnlyConfigEnabled()) {
539
-			if (!is_writable(OC::$configDir) or !is_readable(OC::$configDir)) {
540
-				$errors[] = [
541
-					'error' => $l->t('Cannot write into "config" directory.'),
542
-					'hint' => $l->t('This can usually be fixed by giving the web server write access to the config directory. See %s',
543
-						[ $urlGenerator->linkToDocs('admin-dir_permissions') ]) . '. '
544
-						. $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',
545
-							[ $urlGenerator->linkToDocs('admin-config') ])
546
-				];
547
-			}
548
-		}
549
-
550
-		// Check if there is a writable install folder.
551
-		if ($config->getValue('appstoreenabled', true)) {
552
-			if (OC_App::getInstallPath() === null
553
-				|| !is_writable(OC_App::getInstallPath())
554
-				|| !is_readable(OC_App::getInstallPath())
555
-			) {
556
-				$errors[] = [
557
-					'error' => $l->t('Cannot write into "apps" directory.'),
558
-					'hint' => $l->t('This can usually be fixed by giving the web server write access to the apps directory'
559
-						. ' or disabling the App Store in the config file.')
560
-				];
561
-			}
562
-		}
563
-		// Create root dir.
564
-		if ($config->getValue('installed', false)) {
565
-			if (!is_dir($CONFIG_DATADIRECTORY)) {
566
-				$success = @mkdir($CONFIG_DATADIRECTORY);
567
-				if ($success) {
568
-					$errors = array_merge($errors, self::checkDataDirectoryPermissions($CONFIG_DATADIRECTORY));
569
-				} else {
570
-					$errors[] = [
571
-						'error' => $l->t('Cannot create "data" directory.'),
572
-						'hint' => $l->t('This can usually be fixed by giving the web server write access to the root directory. See %s',
573
-							[$urlGenerator->linkToDocs('admin-dir_permissions')])
574
-					];
575
-				}
576
-			} elseif (!is_writable($CONFIG_DATADIRECTORY) or !is_readable($CONFIG_DATADIRECTORY)) {
577
-				// is_writable doesn't work for NFS mounts, so try to write a file and check if it exists.
578
-				$testFile = sprintf('%s/%s.tmp', $CONFIG_DATADIRECTORY, uniqid('data_dir_writability_test_'));
579
-				$handle = fopen($testFile, 'w');
580
-				if (!$handle || fwrite($handle, 'Test write operation') === false) {
581
-					$permissionsHint = $l->t('Permissions can usually be fixed by giving the web server write access to the root directory. See %s.',
582
-						[$urlGenerator->linkToDocs('admin-dir_permissions')]);
583
-					$errors[] = [
584
-						'error' => $l->t('Your data directory is not writable.'),
585
-						'hint' => $permissionsHint
586
-					];
587
-				} else {
588
-					fclose($handle);
589
-					unlink($testFile);
590
-				}
591
-			} else {
592
-				$errors = array_merge($errors, self::checkDataDirectoryPermissions($CONFIG_DATADIRECTORY));
593
-			}
594
-		}
595
-
596
-		if (!OC_Util::isSetLocaleWorking()) {
597
-			$errors[] = [
598
-				'error' => $l->t('Setting locale to %s failed.',
599
-					['en_US.UTF-8/fr_FR.UTF-8/es_ES.UTF-8/de_DE.UTF-8/ru_RU.UTF-8/'
600
-						. 'pt_BR.UTF-8/it_IT.UTF-8/ja_JP.UTF-8/zh_CN.UTF-8']),
601
-				'hint' => $l->t('Please install one of these locales on your system and restart your web server.')
602
-			];
603
-		}
604
-
605
-		// Contains the dependencies that should be checked against
606
-		// classes = class_exists
607
-		// functions = function_exists
608
-		// defined = defined
609
-		// ini = ini_get
610
-		// If the dependency is not found the missing module name is shown to the EndUser
611
-		// When adding new checks always verify that they pass on Travis as well
612
-		// for ini settings, see https://github.com/owncloud/administration/blob/master/travis-ci/custom.ini
613
-		$dependencies = [
614
-			'classes' => [
615
-				'ZipArchive' => 'zip',
616
-				'DOMDocument' => 'dom',
617
-				'XMLWriter' => 'XMLWriter',
618
-				'XMLReader' => 'XMLReader',
619
-			],
620
-			'functions' => [
621
-				'xml_parser_create' => 'libxml',
622
-				'mb_strcut' => 'mbstring',
623
-				'ctype_digit' => 'ctype',
624
-				'json_encode' => 'JSON',
625
-				'gd_info' => 'GD',
626
-				'gzencode' => 'zlib',
627
-				'simplexml_load_string' => 'SimpleXML',
628
-				'hash' => 'HASH Message Digest Framework',
629
-				'curl_init' => 'cURL',
630
-				'openssl_verify' => 'OpenSSL',
631
-			],
632
-			'defined' => [
633
-				'PDO::ATTR_DRIVER_NAME' => 'PDO'
634
-			],
635
-			'ini' => [
636
-				'default_charset' => 'UTF-8',
637
-			],
638
-		];
639
-		$missingDependencies = [];
640
-		$invalidIniSettings = [];
641
-
642
-		$iniWrapper = \OC::$server->get(IniGetWrapper::class);
643
-		foreach ($dependencies['classes'] as $class => $module) {
644
-			if (!class_exists($class)) {
645
-				$missingDependencies[] = $module;
646
-			}
647
-		}
648
-		foreach ($dependencies['functions'] as $function => $module) {
649
-			if (!function_exists($function)) {
650
-				$missingDependencies[] = $module;
651
-			}
652
-		}
653
-		foreach ($dependencies['defined'] as $defined => $module) {
654
-			if (!defined($defined)) {
655
-				$missingDependencies[] = $module;
656
-			}
657
-		}
658
-		foreach ($dependencies['ini'] as $setting => $expected) {
659
-			if (strtolower($iniWrapper->getString($setting)) !== strtolower($expected)) {
660
-				$invalidIniSettings[] = [$setting, $expected];
661
-			}
662
-		}
663
-
664
-		foreach ($missingDependencies as $missingDependency) {
665
-			$errors[] = [
666
-				'error' => $l->t('PHP module %s not installed.', [$missingDependency]),
667
-				'hint' => $l->t('Please ask your server administrator to install the module.'),
668
-			];
669
-			$webServerRestart = true;
670
-		}
671
-		foreach ($invalidIniSettings as $setting) {
672
-			$errors[] = [
673
-				'error' => $l->t('PHP setting "%s" is not set to "%s".', [$setting[0], var_export($setting[1], true)]),
674
-				'hint' => $l->t('Adjusting this setting in php.ini will make Nextcloud run again')
675
-			];
676
-			$webServerRestart = true;
677
-		}
678
-
679
-		/**
680
-		 * The mbstring.func_overload check can only be performed if the mbstring
681
-		 * module is installed as it will return null if the checking setting is
682
-		 * not available and thus a check on the boolean value fails.
683
-		 *
684
-		 * TODO: Should probably be implemented in the above generic dependency
685
-		 *       check somehow in the long-term.
686
-		 */
687
-		if ($iniWrapper->getBool('mbstring.func_overload') !== null &&
688
-			$iniWrapper->getBool('mbstring.func_overload') === true) {
689
-			$errors[] = [
690
-				'error' => $l->t('<code>mbstring.func_overload</code> is set to <code>%s</code> instead of the expected value <code>0</code>.', [$iniWrapper->getString('mbstring.func_overload')]),
691
-				'hint' => $l->t('To fix this issue set <code>mbstring.func_overload</code> to <code>0</code> in your php.ini.')
692
-			];
693
-		}
694
-
695
-		if (!self::isAnnotationsWorking()) {
696
-			$errors[] = [
697
-				'error' => $l->t('PHP is apparently set up to strip inline doc blocks. This will make several core apps inaccessible.'),
698
-				'hint' => $l->t('This is probably caused by a cache/accelerator such as Zend OPcache or eAccelerator.')
699
-			];
700
-		}
701
-
702
-		if (!\OC::$CLI && $webServerRestart) {
703
-			$errors[] = [
704
-				'error' => $l->t('PHP modules have been installed, but they are still listed as missing?'),
705
-				'hint' => $l->t('Please ask your server administrator to restart the web server.')
706
-			];
707
-		}
708
-
709
-		foreach (['secret', 'instanceid', 'passwordsalt'] as $requiredConfig) {
710
-			if ($config->getValue($requiredConfig, '') === '' && !\OC::$CLI && $config->getValue('installed', false)) {
711
-				$errors[] = [
712
-					'error' => $l->t('The required %s config variable is not configured in the config.php file.', [$requiredConfig]),
713
-					'hint' => $l->t('Please ask your server administrator to check the Nextcloud configuration.')
714
-				];
715
-			}
716
-		}
717
-
718
-		$errors = array_merge($errors, self::checkDatabaseVersion());
719
-
720
-		// Cache the result of this function
721
-		\OC::$server->getSession()->set('checkServer_succeeded', count($errors) == 0);
722
-
723
-		return $errors;
724
-	}
725
-
726
-	/**
727
-	 * Check the database version
728
-	 *
729
-	 * @return array errors array
730
-	 */
731
-	public static function checkDatabaseVersion() {
732
-		$l = \OC::$server->getL10N('lib');
733
-		$errors = [];
734
-		$dbType = \OC::$server->getSystemConfig()->getValue('dbtype', 'sqlite');
735
-		if ($dbType === 'pgsql') {
736
-			// check PostgreSQL version
737
-			// TODO latest postgresql 8 released was 8 years ago, maybe remove the
738
-			// check completely?
739
-			try {
740
-				/** @var IDBConnection $connection */
741
-				$connection = \OC::$server->get(IDBConnection::class);
742
-				$result = $connection->executeQuery('SHOW SERVER_VERSION');
743
-				$data = $result->fetch();
744
-				$result->closeCursor();
745
-				if (isset($data['server_version'])) {
746
-					$version = $data['server_version'];
747
-					if (version_compare($version, '9.0.0', '<')) {
748
-						$errors[] = [
749
-							'error' => $l->t('PostgreSQL >= 9 required.'),
750
-							'hint' => $l->t('Please upgrade your database version.')
751
-						];
752
-					}
753
-				}
754
-			} catch (\Doctrine\DBAL\Exception $e) {
755
-				$logger = \OC::$server->getLogger();
756
-				$logger->warning('Error occurred while checking PostgreSQL version, assuming >= 9');
757
-				$logger->logException($e);
758
-			}
759
-		}
760
-		return $errors;
761
-	}
762
-
763
-	/**
764
-	 * Check for correct file permissions of data directory
765
-	 *
766
-	 * @param string $dataDirectory
767
-	 * @return array arrays with error messages and hints
768
-	 */
769
-	public static function checkDataDirectoryPermissions($dataDirectory) {
770
-		if (!\OC::$server->getConfig()->getSystemValueBool('check_data_directory_permissions', true)) {
771
-			return  [];
772
-		}
773
-
774
-		$perms = substr(decoct(@fileperms($dataDirectory)), -3);
775
-		if (substr($perms, -1) !== '0') {
776
-			chmod($dataDirectory, 0770);
777
-			clearstatcache();
778
-			$perms = substr(decoct(@fileperms($dataDirectory)), -3);
779
-			if ($perms[2] !== '0') {
780
-				$l = \OC::$server->getL10N('lib');
781
-				return [[
782
-					'error' => $l->t('Your data directory is readable by other users.'),
783
-					'hint' => $l->t('Please change the permissions to 0770 so that the directory cannot be listed by other users.'),
784
-				]];
785
-			}
786
-		}
787
-		return [];
788
-	}
789
-
790
-	/**
791
-	 * Check that the data directory exists and is valid by
792
-	 * checking the existence of the ".ocdata" file.
793
-	 *
794
-	 * @param string $dataDirectory data directory path
795
-	 * @return array errors found
796
-	 */
797
-	public static function checkDataDirectoryValidity($dataDirectory) {
798
-		$l = \OC::$server->getL10N('lib');
799
-		$errors = [];
800
-		if ($dataDirectory[0] !== '/') {
801
-			$errors[] = [
802
-				'error' => $l->t('Your data directory must be an absolute path.'),
803
-				'hint' => $l->t('Check the value of "datadirectory" in your configuration.')
804
-			];
805
-		}
806
-		if (!file_exists($dataDirectory . '/.ocdata')) {
807
-			$errors[] = [
808
-				'error' => $l->t('Your data directory is invalid.'),
809
-				'hint' => $l->t('Ensure there is a file called ".ocdata"' .
810
-					' in the root of the data directory.')
811
-			];
812
-		}
813
-		return $errors;
814
-	}
815
-
816
-	/**
817
-	 * Check if the user is logged in, redirects to home if not. With
818
-	 * redirect URL parameter to the request URI.
819
-	 *
820
-	 * @return void
821
-	 */
822
-	public static function checkLoggedIn() {
823
-		// Check if we are a user
824
-		if (!\OC::$server->getUserSession()->isLoggedIn()) {
825
-			header('Location: ' . \OC::$server->getURLGenerator()->linkToRoute(
826
-				'core.login.showLoginForm',
827
-				[
828
-					'redirect_url' => \OC::$server->getRequest()->getRequestUri(),
829
-				]
830
-			)
831
-			);
832
-			exit();
833
-		}
834
-		// Redirect to 2FA challenge selection if 2FA challenge was not solved yet
835
-		if (\OC::$server->getTwoFactorAuthManager()->needsSecondFactor(\OC::$server->getUserSession()->getUser())) {
836
-			header('Location: ' . \OC::$server->getURLGenerator()->linkToRoute('core.TwoFactorChallenge.selectChallenge'));
837
-			exit();
838
-		}
839
-	}
840
-
841
-	/**
842
-	 * Check if the user is a admin, redirects to home if not
843
-	 *
844
-	 * @return void
845
-	 */
846
-	public static function checkAdminUser() {
847
-		OC_Util::checkLoggedIn();
848
-		if (!OC_User::isAdminUser(OC_User::getUser())) {
849
-			header('Location: ' . \OCP\Util::linkToAbsolute('', 'index.php'));
850
-			exit();
851
-		}
852
-	}
853
-
854
-	/**
855
-	 * Returns the URL of the default page
856
-	 * based on the system configuration and
857
-	 * the apps visible for the current user
858
-	 *
859
-	 * @return string URL
860
-	 * @suppress PhanDeprecatedFunction
861
-	 */
862
-	public static function getDefaultPageUrl() {
863
-		/** @var IURLGenerator $urlGenerator */
864
-		$urlGenerator = \OC::$server->get(IURLGenerator::class);
865
-		return $urlGenerator->linkToDefaultPageUrl();
866
-	}
867
-
868
-	/**
869
-	 * Redirect to the user default page
870
-	 *
871
-	 * @return void
872
-	 */
873
-	public static function redirectToDefaultPage() {
874
-		$location = self::getDefaultPageUrl();
875
-		header('Location: ' . $location);
876
-		exit();
877
-	}
878
-
879
-	/**
880
-	 * get an id unique for this instance
881
-	 *
882
-	 * @return string
883
-	 */
884
-	public static function getInstanceId() {
885
-		$id = \OC::$server->getSystemConfig()->getValue('instanceid', null);
886
-		if (is_null($id)) {
887
-			// We need to guarantee at least one letter in instanceid so it can be used as the session_name
888
-			$id = 'oc' . \OC::$server->getSecureRandom()->generate(10, \OCP\Security\ISecureRandom::CHAR_LOWER.\OCP\Security\ISecureRandom::CHAR_DIGITS);
889
-			\OC::$server->getSystemConfig()->setValue('instanceid', $id);
890
-		}
891
-		return $id;
892
-	}
893
-
894
-	/**
895
-	 * Public function to sanitize HTML
896
-	 *
897
-	 * This function is used to sanitize HTML and should be applied on any
898
-	 * string or array of strings before displaying it on a web page.
899
-	 *
900
-	 * @param string|string[] $value
901
-	 * @return string|string[] an array of sanitized strings or a single sanitized string, depends on the input parameter.
902
-	 */
903
-	public static function sanitizeHTML($value) {
904
-		if (is_array($value)) {
905
-			/** @var string[] $value */
906
-			$value = array_map(function ($value) {
907
-				return self::sanitizeHTML($value);
908
-			}, $value);
909
-		} else {
910
-			// Specify encoding for PHP<5.4
911
-			$value = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
912
-		}
913
-		return $value;
914
-	}
915
-
916
-	/**
917
-	 * Public function to encode url parameters
918
-	 *
919
-	 * This function is used to encode path to file before output.
920
-	 * Encoding is done according to RFC 3986 with one exception:
921
-	 * Character '/' is preserved as is.
922
-	 *
923
-	 * @param string $component part of URI to encode
924
-	 * @return string
925
-	 */
926
-	public static function encodePath($component) {
927
-		$encoded = rawurlencode($component);
928
-		$encoded = str_replace('%2F', '/', $encoded);
929
-		return $encoded;
930
-	}
931
-
932
-
933
-	public function createHtaccessTestFile(\OCP\IConfig $config) {
934
-		// php dev server does not support htaccess
935
-		if (php_sapi_name() === 'cli-server') {
936
-			return false;
937
-		}
938
-
939
-		// testdata
940
-		$fileName = '/htaccesstest.txt';
941
-		$testContent = 'This is used for testing whether htaccess is properly enabled to disallow access from the outside. This file can be safely removed.';
942
-
943
-		// creating a test file
944
-		$testFile = $config->getSystemValueString('datadirectory', OC::$SERVERROOT . '/data') . '/' . $fileName;
945
-
946
-		if (file_exists($testFile)) {// already running this test, possible recursive call
947
-			return false;
948
-		}
949
-
950
-		$fp = @fopen($testFile, 'w');
951
-		if (!$fp) {
952
-			throw new \OCP\HintException('Can\'t create test file to check for working .htaccess file.',
953
-				'Make sure it is possible for the web server to write to ' . $testFile);
954
-		}
955
-		fwrite($fp, $testContent);
956
-		fclose($fp);
957
-
958
-		return $testContent;
959
-	}
960
-
961
-	/**
962
-	 * Check if the .htaccess file is working
963
-	 *
964
-	 * @param \OCP\IConfig $config
965
-	 * @return bool
966
-	 * @throws Exception
967
-	 * @throws \OCP\HintException If the test file can't get written.
968
-	 */
969
-	public function isHtaccessWorking(\OCP\IConfig $config) {
970
-		if (\OC::$CLI || !$config->getSystemValueBool('check_for_working_htaccess', true)) {
971
-			return true;
972
-		}
973
-
974
-		$testContent = $this->createHtaccessTestFile($config);
975
-		if ($testContent === false) {
976
-			return false;
977
-		}
978
-
979
-		$fileName = '/htaccesstest.txt';
980
-		$testFile = $config->getSystemValueString('datadirectory', OC::$SERVERROOT . '/data') . '/' . $fileName;
981
-
982
-		// accessing the file via http
983
-		$url = \OC::$server->getURLGenerator()->getAbsoluteURL(OC::$WEBROOT . '/data' . $fileName);
984
-		try {
985
-			$content = \OC::$server->getHTTPClientService()->newClient()->get($url)->getBody();
986
-		} catch (\Exception $e) {
987
-			$content = false;
988
-		}
989
-
990
-		if (str_starts_with($url, 'https:')) {
991
-			$url = 'http:' . substr($url, 6);
992
-		} else {
993
-			$url = 'https:' . substr($url, 5);
994
-		}
995
-
996
-		try {
997
-			$fallbackContent = \OC::$server->getHTTPClientService()->newClient()->get($url)->getBody();
998
-		} catch (\Exception $e) {
999
-			$fallbackContent = false;
1000
-		}
1001
-
1002
-		// cleanup
1003
-		@unlink($testFile);
1004
-
1005
-		/*
79
+    public static $scripts = [];
80
+    public static $styles = [];
81
+    public static $headers = [];
82
+
83
+    /** @var array Local cache of version.php */
84
+    private static $versionCache = null;
85
+
86
+    protected static function getAppManager() {
87
+        return \OC::$server->getAppManager();
88
+    }
89
+
90
+    /**
91
+     * Setup the file system
92
+     *
93
+     * @param string|null $user
94
+     * @return boolean
95
+     * @description configure the initial filesystem based on the configuration
96
+     * @suppress PhanDeprecatedFunction
97
+     * @suppress PhanAccessMethodInternal
98
+     */
99
+    public static function setupFS(?string $user = '') {
100
+        // If we are not forced to load a specific user we load the one that is logged in
101
+        if ($user === '') {
102
+            $userObject = \OC::$server->get(\OCP\IUserSession::class)->getUser();
103
+        } else {
104
+            $userObject = \OC::$server->get(\OCP\IUserManager::class)->get($user);
105
+        }
106
+
107
+        /** @var SetupManager $setupManager */
108
+        $setupManager = \OC::$server->get(SetupManager::class);
109
+
110
+        if ($userObject) {
111
+            $setupManager->setupForUser($userObject);
112
+        } else {
113
+            $setupManager->setupRoot();
114
+        }
115
+        return true;
116
+    }
117
+
118
+    /**
119
+     * Check if a password is required for each public link
120
+     *
121
+     * @param bool $checkGroupMembership Check group membership exclusion
122
+     * @return boolean
123
+     * @suppress PhanDeprecatedFunction
124
+     */
125
+    public static function isPublicLinkPasswordRequired(bool $checkGroupMembership = true) {
126
+        /** @var IManager $shareManager */
127
+        $shareManager = \OC::$server->get(IManager::class);
128
+        return $shareManager->shareApiLinkEnforcePassword($checkGroupMembership);
129
+    }
130
+
131
+    /**
132
+     * check if sharing is disabled for the current user
133
+     * @param IConfig $config
134
+     * @param IGroupManager $groupManager
135
+     * @param IUser|null $user
136
+     * @return bool
137
+     */
138
+    public static function isSharingDisabledForUser(IConfig $config, IGroupManager $groupManager, $user) {
139
+        /** @var IManager $shareManager */
140
+        $shareManager = \OC::$server->get(IManager::class);
141
+        $userId = $user ? $user->getUID() : null;
142
+        return $shareManager->sharingDisabledForUser($userId);
143
+    }
144
+
145
+    /**
146
+     * check if share API enforces a default expire date
147
+     *
148
+     * @return bool
149
+     * @suppress PhanDeprecatedFunction
150
+     */
151
+    public static function isDefaultExpireDateEnforced() {
152
+        /** @var IManager $shareManager */
153
+        $shareManager = \OC::$server->get(IManager::class);
154
+        return $shareManager->shareApiLinkDefaultExpireDateEnforced();
155
+    }
156
+
157
+    /**
158
+     * Get the quota of a user
159
+     *
160
+     * @param IUser|null $user
161
+     * @return int|\OCP\Files\FileInfo::SPACE_UNLIMITED|false|float Quota bytes
162
+     */
163
+    public static function getUserQuota(?IUser $user) {
164
+        if (is_null($user)) {
165
+            return \OCP\Files\FileInfo::SPACE_UNLIMITED;
166
+        }
167
+        $userQuota = $user->getQuota();
168
+        if ($userQuota === 'none') {
169
+            return \OCP\Files\FileInfo::SPACE_UNLIMITED;
170
+        }
171
+        return OC_Helper::computerFileSize($userQuota);
172
+    }
173
+
174
+    /**
175
+     * copies the skeleton to the users /files
176
+     *
177
+     * @param string $userId
178
+     * @param \OCP\Files\Folder $userDirectory
179
+     * @throws \OCP\Files\NotFoundException
180
+     * @throws \OCP\Files\NotPermittedException
181
+     * @suppress PhanDeprecatedFunction
182
+     */
183
+    public static function copySkeleton($userId, \OCP\Files\Folder $userDirectory) {
184
+        /** @var LoggerInterface $logger */
185
+        $logger = \OC::$server->get(LoggerInterface::class);
186
+
187
+        $plainSkeletonDirectory = \OC::$server->getConfig()->getSystemValueString('skeletondirectory', \OC::$SERVERROOT . '/core/skeleton');
188
+        $userLang = \OC::$server->getL10NFactory()->findLanguage();
189
+        $skeletonDirectory = str_replace('{lang}', $userLang, $plainSkeletonDirectory);
190
+
191
+        if (!file_exists($skeletonDirectory)) {
192
+            $dialectStart = strpos($userLang, '_');
193
+            if ($dialectStart !== false) {
194
+                $skeletonDirectory = str_replace('{lang}', substr($userLang, 0, $dialectStart), $plainSkeletonDirectory);
195
+            }
196
+            if ($dialectStart === false || !file_exists($skeletonDirectory)) {
197
+                $skeletonDirectory = str_replace('{lang}', 'default', $plainSkeletonDirectory);
198
+            }
199
+            if (!file_exists($skeletonDirectory)) {
200
+                $skeletonDirectory = '';
201
+            }
202
+        }
203
+
204
+        $instanceId = \OC::$server->getConfig()->getSystemValue('instanceid', '');
205
+
206
+        if ($instanceId === null) {
207
+            throw new \RuntimeException('no instance id!');
208
+        }
209
+        $appdata = 'appdata_' . $instanceId;
210
+        if ($userId === $appdata) {
211
+            throw new \RuntimeException('username is reserved name: ' . $appdata);
212
+        }
213
+
214
+        if (!empty($skeletonDirectory)) {
215
+            $logger->debug('copying skeleton for '.$userId.' from '.$skeletonDirectory.' to '.$userDirectory->getFullPath('/'), ['app' => 'files_skeleton']);
216
+            self::copyr($skeletonDirectory, $userDirectory);
217
+            // update the file cache
218
+            $userDirectory->getStorage()->getScanner()->scan('', \OC\Files\Cache\Scanner::SCAN_RECURSIVE);
219
+
220
+            /** @var ITemplateManager $templateManager */
221
+            $templateManager = \OC::$server->get(ITemplateManager::class);
222
+            $templateManager->initializeTemplateDirectory(null, $userId);
223
+        }
224
+    }
225
+
226
+    /**
227
+     * copies a directory recursively by using streams
228
+     *
229
+     * @param string $source
230
+     * @param \OCP\Files\Folder $target
231
+     * @return void
232
+     */
233
+    public static function copyr($source, \OCP\Files\Folder $target) {
234
+        $logger = \OC::$server->getLogger();
235
+
236
+        // Verify if folder exists
237
+        $dir = opendir($source);
238
+        if ($dir === false) {
239
+            $logger->error(sprintf('Could not opendir "%s"', $source), ['app' => 'core']);
240
+            return;
241
+        }
242
+
243
+        // Copy the files
244
+        while (false !== ($file = readdir($dir))) {
245
+            if (!\OC\Files\Filesystem::isIgnoredDir($file)) {
246
+                if (is_dir($source . '/' . $file)) {
247
+                    $child = $target->newFolder($file);
248
+                    self::copyr($source . '/' . $file, $child);
249
+                } else {
250
+                    $child = $target->newFile($file);
251
+                    $sourceStream = fopen($source . '/' . $file, 'r');
252
+                    if ($sourceStream === false) {
253
+                        $logger->error(sprintf('Could not fopen "%s"', $source . '/' . $file), ['app' => 'core']);
254
+                        closedir($dir);
255
+                        return;
256
+                    }
257
+                    $child->putContent($sourceStream);
258
+                }
259
+            }
260
+        }
261
+        closedir($dir);
262
+    }
263
+
264
+    /**
265
+     * @return void
266
+     * @suppress PhanUndeclaredMethod
267
+     */
268
+    public static function tearDownFS() {
269
+        /** @var SetupManager $setupManager */
270
+        $setupManager = \OC::$server->get(SetupManager::class);
271
+        $setupManager->tearDown();
272
+    }
273
+
274
+    /**
275
+     * get the current installed version of ownCloud
276
+     *
277
+     * @return array
278
+     */
279
+    public static function getVersion() {
280
+        OC_Util::loadVersion();
281
+        return self::$versionCache['OC_Version'];
282
+    }
283
+
284
+    /**
285
+     * get the current installed version string of ownCloud
286
+     *
287
+     * @return string
288
+     */
289
+    public static function getVersionString() {
290
+        OC_Util::loadVersion();
291
+        return self::$versionCache['OC_VersionString'];
292
+    }
293
+
294
+    /**
295
+     * @deprecated the value is of no use anymore
296
+     * @return string
297
+     */
298
+    public static function getEditionString() {
299
+        return '';
300
+    }
301
+
302
+    /**
303
+     * @description get the update channel of the current installed of ownCloud.
304
+     * @return string
305
+     */
306
+    public static function getChannel() {
307
+        OC_Util::loadVersion();
308
+        return \OC::$server->getConfig()->getSystemValueString('updater.release.channel', self::$versionCache['OC_Channel']);
309
+    }
310
+
311
+    /**
312
+     * @description get the build number of the current installed of ownCloud.
313
+     * @return string
314
+     */
315
+    public static function getBuild() {
316
+        OC_Util::loadVersion();
317
+        return self::$versionCache['OC_Build'];
318
+    }
319
+
320
+    /**
321
+     * @description load the version.php into the session as cache
322
+     * @suppress PhanUndeclaredVariable
323
+     */
324
+    private static function loadVersion() {
325
+        if (self::$versionCache !== null) {
326
+            return;
327
+        }
328
+
329
+        require OC::$SERVERROOT . '/version.php';
330
+        /** @var int $timestamp */
331
+        self::$versionCache['OC_Version_Timestamp'] = \OC::$VERSION_MTIME;
332
+        /** @var string $OC_Version */
333
+        self::$versionCache['OC_Version'] = $OC_Version;
334
+        /** @var string $OC_VersionString */
335
+        self::$versionCache['OC_VersionString'] = $OC_VersionString;
336
+        /** @var string $OC_Build */
337
+        self::$versionCache['OC_Build'] = $OC_Build;
338
+
339
+        /** @var string $OC_Channel */
340
+        self::$versionCache['OC_Channel'] = $OC_Channel;
341
+    }
342
+
343
+    /**
344
+     * generates a path for JS/CSS files. If no application is provided it will create the path for core.
345
+     *
346
+     * @param string $application application to get the files from
347
+     * @param string $directory directory within this application (css, js, vendor, etc)
348
+     * @param string $file the file inside of the above folder
349
+     * @return string the path
350
+     */
351
+    private static function generatePath($application, $directory, $file) {
352
+        if (is_null($file)) {
353
+            $file = $application;
354
+            $application = "";
355
+        }
356
+        if (!empty($application)) {
357
+            return "$application/$directory/$file";
358
+        } else {
359
+            return "$directory/$file";
360
+        }
361
+    }
362
+
363
+    /**
364
+     * add a javascript file
365
+     *
366
+     * @deprecated 24.0.0 - Use \OCP\Util::addScript
367
+     *
368
+     * @param string $application application id
369
+     * @param string|null $file filename
370
+     * @param bool $prepend prepend the Script to the beginning of the list
371
+     * @return void
372
+     */
373
+    public static function addScript($application, $file = null, $prepend = false) {
374
+        $path = OC_Util::generatePath($application, 'js', $file);
375
+
376
+        // core js files need separate handling
377
+        if ($application !== 'core' && $file !== null) {
378
+            self::addTranslations($application);
379
+        }
380
+        self::addExternalResource($application, $prepend, $path, "script");
381
+    }
382
+
383
+    /**
384
+     * add a javascript file from the vendor sub folder
385
+     *
386
+     * @param string $application application id
387
+     * @param string|null $file filename
388
+     * @param bool $prepend prepend the Script to the beginning of the list
389
+     * @return void
390
+     */
391
+    public static function addVendorScript($application, $file = null, $prepend = false) {
392
+        $path = OC_Util::generatePath($application, 'vendor', $file);
393
+        self::addExternalResource($application, $prepend, $path, "script");
394
+    }
395
+
396
+    /**
397
+     * add a translation JS file
398
+     *
399
+     * @deprecated 24.0.0
400
+     *
401
+     * @param string $application application id
402
+     * @param string|null $languageCode language code, defaults to the current language
403
+     * @param bool|null $prepend prepend the Script to the beginning of the list
404
+     */
405
+    public static function addTranslations($application, $languageCode = null, $prepend = false) {
406
+        if (is_null($languageCode)) {
407
+            $languageCode = \OC::$server->getL10NFactory()->findLanguage($application);
408
+        }
409
+        if (!empty($application)) {
410
+            $path = "$application/l10n/$languageCode";
411
+        } else {
412
+            $path = "l10n/$languageCode";
413
+        }
414
+        self::addExternalResource($application, $prepend, $path, "script");
415
+    }
416
+
417
+    /**
418
+     * add a css file
419
+     *
420
+     * @param string $application application id
421
+     * @param string|null $file filename
422
+     * @param bool $prepend prepend the Style to the beginning of the list
423
+     * @return void
424
+     */
425
+    public static function addStyle($application, $file = null, $prepend = false) {
426
+        $path = OC_Util::generatePath($application, 'css', $file);
427
+        self::addExternalResource($application, $prepend, $path, "style");
428
+    }
429
+
430
+    /**
431
+     * add a css file from the vendor sub folder
432
+     *
433
+     * @param string $application application id
434
+     * @param string|null $file filename
435
+     * @param bool $prepend prepend the Style to the beginning of the list
436
+     * @return void
437
+     */
438
+    public static function addVendorStyle($application, $file = null, $prepend = false) {
439
+        $path = OC_Util::generatePath($application, 'vendor', $file);
440
+        self::addExternalResource($application, $prepend, $path, "style");
441
+    }
442
+
443
+    /**
444
+     * add an external resource css/js file
445
+     *
446
+     * @param string $application application id
447
+     * @param bool $prepend prepend the file to the beginning of the list
448
+     * @param string $path
449
+     * @param string $type (script or style)
450
+     * @return void
451
+     */
452
+    private static function addExternalResource($application, $prepend, $path, $type = "script") {
453
+        if ($type === "style") {
454
+            if (!in_array($path, self::$styles)) {
455
+                if ($prepend === true) {
456
+                    array_unshift(self::$styles, $path);
457
+                } else {
458
+                    self::$styles[] = $path;
459
+                }
460
+            }
461
+        } elseif ($type === "script") {
462
+            if (!in_array($path, self::$scripts)) {
463
+                if ($prepend === true) {
464
+                    array_unshift(self::$scripts, $path);
465
+                } else {
466
+                    self::$scripts [] = $path;
467
+                }
468
+            }
469
+        }
470
+    }
471
+
472
+    /**
473
+     * Add a custom element to the header
474
+     * If $text is null then the element will be written as empty element.
475
+     * So use "" to get a closing tag.
476
+     * @param string $tag tag name of the element
477
+     * @param array $attributes array of attributes for the element
478
+     * @param string $text the text content for the element
479
+     * @param bool $prepend prepend the header to the beginning of the list
480
+     */
481
+    public static function addHeader($tag, $attributes, $text = null, $prepend = false) {
482
+        $header = [
483
+            'tag' => $tag,
484
+            'attributes' => $attributes,
485
+            'text' => $text
486
+        ];
487
+        if ($prepend === true) {
488
+            array_unshift(self::$headers, $header);
489
+        } else {
490
+            self::$headers[] = $header;
491
+        }
492
+    }
493
+
494
+    /**
495
+     * check if the current server configuration is suitable for ownCloud
496
+     *
497
+     * @param \OC\SystemConfig $config
498
+     * @return array arrays with error messages and hints
499
+     */
500
+    public static function checkServer(\OC\SystemConfig $config) {
501
+        $l = \OC::$server->getL10N('lib');
502
+        $errors = [];
503
+        $CONFIG_DATADIRECTORY = $config->getValue('datadirectory', OC::$SERVERROOT . '/data');
504
+
505
+        if (!self::needUpgrade($config) && $config->getValue('installed', false)) {
506
+            // this check needs to be done every time
507
+            $errors = self::checkDataDirectoryValidity($CONFIG_DATADIRECTORY);
508
+        }
509
+
510
+        // Assume that if checkServer() succeeded before in this session, then all is fine.
511
+        if (\OC::$server->getSession()->exists('checkServer_succeeded') && \OC::$server->getSession()->get('checkServer_succeeded')) {
512
+            return $errors;
513
+        }
514
+
515
+        $webServerRestart = false;
516
+        $setup = new \OC\Setup(
517
+            $config,
518
+            \OC::$server->get(IniGetWrapper::class),
519
+            \OC::$server->getL10N('lib'),
520
+            \OC::$server->get(\OCP\Defaults::class),
521
+            \OC::$server->get(LoggerInterface::class),
522
+            \OC::$server->getSecureRandom(),
523
+            \OC::$server->get(\OC\Installer::class)
524
+        );
525
+
526
+        $urlGenerator = \OC::$server->getURLGenerator();
527
+
528
+        $availableDatabases = $setup->getSupportedDatabases();
529
+        if (empty($availableDatabases)) {
530
+            $errors[] = [
531
+                'error' => $l->t('No database drivers (sqlite, mysql, or postgresql) installed.'),
532
+                'hint' => '' //TODO: sane hint
533
+            ];
534
+            $webServerRestart = true;
535
+        }
536
+
537
+        // Check if config folder is writable.
538
+        if (!OC_Helper::isReadOnlyConfigEnabled()) {
539
+            if (!is_writable(OC::$configDir) or !is_readable(OC::$configDir)) {
540
+                $errors[] = [
541
+                    'error' => $l->t('Cannot write into "config" directory.'),
542
+                    'hint' => $l->t('This can usually be fixed by giving the web server write access to the config directory. See %s',
543
+                        [ $urlGenerator->linkToDocs('admin-dir_permissions') ]) . '. '
544
+                        . $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',
545
+                            [ $urlGenerator->linkToDocs('admin-config') ])
546
+                ];
547
+            }
548
+        }
549
+
550
+        // Check if there is a writable install folder.
551
+        if ($config->getValue('appstoreenabled', true)) {
552
+            if (OC_App::getInstallPath() === null
553
+                || !is_writable(OC_App::getInstallPath())
554
+                || !is_readable(OC_App::getInstallPath())
555
+            ) {
556
+                $errors[] = [
557
+                    'error' => $l->t('Cannot write into "apps" directory.'),
558
+                    'hint' => $l->t('This can usually be fixed by giving the web server write access to the apps directory'
559
+                        . ' or disabling the App Store in the config file.')
560
+                ];
561
+            }
562
+        }
563
+        // Create root dir.
564
+        if ($config->getValue('installed', false)) {
565
+            if (!is_dir($CONFIG_DATADIRECTORY)) {
566
+                $success = @mkdir($CONFIG_DATADIRECTORY);
567
+                if ($success) {
568
+                    $errors = array_merge($errors, self::checkDataDirectoryPermissions($CONFIG_DATADIRECTORY));
569
+                } else {
570
+                    $errors[] = [
571
+                        'error' => $l->t('Cannot create "data" directory.'),
572
+                        'hint' => $l->t('This can usually be fixed by giving the web server write access to the root directory. See %s',
573
+                            [$urlGenerator->linkToDocs('admin-dir_permissions')])
574
+                    ];
575
+                }
576
+            } elseif (!is_writable($CONFIG_DATADIRECTORY) or !is_readable($CONFIG_DATADIRECTORY)) {
577
+                // is_writable doesn't work for NFS mounts, so try to write a file and check if it exists.
578
+                $testFile = sprintf('%s/%s.tmp', $CONFIG_DATADIRECTORY, uniqid('data_dir_writability_test_'));
579
+                $handle = fopen($testFile, 'w');
580
+                if (!$handle || fwrite($handle, 'Test write operation') === false) {
581
+                    $permissionsHint = $l->t('Permissions can usually be fixed by giving the web server write access to the root directory. See %s.',
582
+                        [$urlGenerator->linkToDocs('admin-dir_permissions')]);
583
+                    $errors[] = [
584
+                        'error' => $l->t('Your data directory is not writable.'),
585
+                        'hint' => $permissionsHint
586
+                    ];
587
+                } else {
588
+                    fclose($handle);
589
+                    unlink($testFile);
590
+                }
591
+            } else {
592
+                $errors = array_merge($errors, self::checkDataDirectoryPermissions($CONFIG_DATADIRECTORY));
593
+            }
594
+        }
595
+
596
+        if (!OC_Util::isSetLocaleWorking()) {
597
+            $errors[] = [
598
+                'error' => $l->t('Setting locale to %s failed.',
599
+                    ['en_US.UTF-8/fr_FR.UTF-8/es_ES.UTF-8/de_DE.UTF-8/ru_RU.UTF-8/'
600
+                        . 'pt_BR.UTF-8/it_IT.UTF-8/ja_JP.UTF-8/zh_CN.UTF-8']),
601
+                'hint' => $l->t('Please install one of these locales on your system and restart your web server.')
602
+            ];
603
+        }
604
+
605
+        // Contains the dependencies that should be checked against
606
+        // classes = class_exists
607
+        // functions = function_exists
608
+        // defined = defined
609
+        // ini = ini_get
610
+        // If the dependency is not found the missing module name is shown to the EndUser
611
+        // When adding new checks always verify that they pass on Travis as well
612
+        // for ini settings, see https://github.com/owncloud/administration/blob/master/travis-ci/custom.ini
613
+        $dependencies = [
614
+            'classes' => [
615
+                'ZipArchive' => 'zip',
616
+                'DOMDocument' => 'dom',
617
+                'XMLWriter' => 'XMLWriter',
618
+                'XMLReader' => 'XMLReader',
619
+            ],
620
+            'functions' => [
621
+                'xml_parser_create' => 'libxml',
622
+                'mb_strcut' => 'mbstring',
623
+                'ctype_digit' => 'ctype',
624
+                'json_encode' => 'JSON',
625
+                'gd_info' => 'GD',
626
+                'gzencode' => 'zlib',
627
+                'simplexml_load_string' => 'SimpleXML',
628
+                'hash' => 'HASH Message Digest Framework',
629
+                'curl_init' => 'cURL',
630
+                'openssl_verify' => 'OpenSSL',
631
+            ],
632
+            'defined' => [
633
+                'PDO::ATTR_DRIVER_NAME' => 'PDO'
634
+            ],
635
+            'ini' => [
636
+                'default_charset' => 'UTF-8',
637
+            ],
638
+        ];
639
+        $missingDependencies = [];
640
+        $invalidIniSettings = [];
641
+
642
+        $iniWrapper = \OC::$server->get(IniGetWrapper::class);
643
+        foreach ($dependencies['classes'] as $class => $module) {
644
+            if (!class_exists($class)) {
645
+                $missingDependencies[] = $module;
646
+            }
647
+        }
648
+        foreach ($dependencies['functions'] as $function => $module) {
649
+            if (!function_exists($function)) {
650
+                $missingDependencies[] = $module;
651
+            }
652
+        }
653
+        foreach ($dependencies['defined'] as $defined => $module) {
654
+            if (!defined($defined)) {
655
+                $missingDependencies[] = $module;
656
+            }
657
+        }
658
+        foreach ($dependencies['ini'] as $setting => $expected) {
659
+            if (strtolower($iniWrapper->getString($setting)) !== strtolower($expected)) {
660
+                $invalidIniSettings[] = [$setting, $expected];
661
+            }
662
+        }
663
+
664
+        foreach ($missingDependencies as $missingDependency) {
665
+            $errors[] = [
666
+                'error' => $l->t('PHP module %s not installed.', [$missingDependency]),
667
+                'hint' => $l->t('Please ask your server administrator to install the module.'),
668
+            ];
669
+            $webServerRestart = true;
670
+        }
671
+        foreach ($invalidIniSettings as $setting) {
672
+            $errors[] = [
673
+                'error' => $l->t('PHP setting "%s" is not set to "%s".', [$setting[0], var_export($setting[1], true)]),
674
+                'hint' => $l->t('Adjusting this setting in php.ini will make Nextcloud run again')
675
+            ];
676
+            $webServerRestart = true;
677
+        }
678
+
679
+        /**
680
+         * The mbstring.func_overload check can only be performed if the mbstring
681
+         * module is installed as it will return null if the checking setting is
682
+         * not available and thus a check on the boolean value fails.
683
+         *
684
+         * TODO: Should probably be implemented in the above generic dependency
685
+         *       check somehow in the long-term.
686
+         */
687
+        if ($iniWrapper->getBool('mbstring.func_overload') !== null &&
688
+            $iniWrapper->getBool('mbstring.func_overload') === true) {
689
+            $errors[] = [
690
+                'error' => $l->t('<code>mbstring.func_overload</code> is set to <code>%s</code> instead of the expected value <code>0</code>.', [$iniWrapper->getString('mbstring.func_overload')]),
691
+                'hint' => $l->t('To fix this issue set <code>mbstring.func_overload</code> to <code>0</code> in your php.ini.')
692
+            ];
693
+        }
694
+
695
+        if (!self::isAnnotationsWorking()) {
696
+            $errors[] = [
697
+                'error' => $l->t('PHP is apparently set up to strip inline doc blocks. This will make several core apps inaccessible.'),
698
+                'hint' => $l->t('This is probably caused by a cache/accelerator such as Zend OPcache or eAccelerator.')
699
+            ];
700
+        }
701
+
702
+        if (!\OC::$CLI && $webServerRestart) {
703
+            $errors[] = [
704
+                'error' => $l->t('PHP modules have been installed, but they are still listed as missing?'),
705
+                'hint' => $l->t('Please ask your server administrator to restart the web server.')
706
+            ];
707
+        }
708
+
709
+        foreach (['secret', 'instanceid', 'passwordsalt'] as $requiredConfig) {
710
+            if ($config->getValue($requiredConfig, '') === '' && !\OC::$CLI && $config->getValue('installed', false)) {
711
+                $errors[] = [
712
+                    'error' => $l->t('The required %s config variable is not configured in the config.php file.', [$requiredConfig]),
713
+                    'hint' => $l->t('Please ask your server administrator to check the Nextcloud configuration.')
714
+                ];
715
+            }
716
+        }
717
+
718
+        $errors = array_merge($errors, self::checkDatabaseVersion());
719
+
720
+        // Cache the result of this function
721
+        \OC::$server->getSession()->set('checkServer_succeeded', count($errors) == 0);
722
+
723
+        return $errors;
724
+    }
725
+
726
+    /**
727
+     * Check the database version
728
+     *
729
+     * @return array errors array
730
+     */
731
+    public static function checkDatabaseVersion() {
732
+        $l = \OC::$server->getL10N('lib');
733
+        $errors = [];
734
+        $dbType = \OC::$server->getSystemConfig()->getValue('dbtype', 'sqlite');
735
+        if ($dbType === 'pgsql') {
736
+            // check PostgreSQL version
737
+            // TODO latest postgresql 8 released was 8 years ago, maybe remove the
738
+            // check completely?
739
+            try {
740
+                /** @var IDBConnection $connection */
741
+                $connection = \OC::$server->get(IDBConnection::class);
742
+                $result = $connection->executeQuery('SHOW SERVER_VERSION');
743
+                $data = $result->fetch();
744
+                $result->closeCursor();
745
+                if (isset($data['server_version'])) {
746
+                    $version = $data['server_version'];
747
+                    if (version_compare($version, '9.0.0', '<')) {
748
+                        $errors[] = [
749
+                            'error' => $l->t('PostgreSQL >= 9 required.'),
750
+                            'hint' => $l->t('Please upgrade your database version.')
751
+                        ];
752
+                    }
753
+                }
754
+            } catch (\Doctrine\DBAL\Exception $e) {
755
+                $logger = \OC::$server->getLogger();
756
+                $logger->warning('Error occurred while checking PostgreSQL version, assuming >= 9');
757
+                $logger->logException($e);
758
+            }
759
+        }
760
+        return $errors;
761
+    }
762
+
763
+    /**
764
+     * Check for correct file permissions of data directory
765
+     *
766
+     * @param string $dataDirectory
767
+     * @return array arrays with error messages and hints
768
+     */
769
+    public static function checkDataDirectoryPermissions($dataDirectory) {
770
+        if (!\OC::$server->getConfig()->getSystemValueBool('check_data_directory_permissions', true)) {
771
+            return  [];
772
+        }
773
+
774
+        $perms = substr(decoct(@fileperms($dataDirectory)), -3);
775
+        if (substr($perms, -1) !== '0') {
776
+            chmod($dataDirectory, 0770);
777
+            clearstatcache();
778
+            $perms = substr(decoct(@fileperms($dataDirectory)), -3);
779
+            if ($perms[2] !== '0') {
780
+                $l = \OC::$server->getL10N('lib');
781
+                return [[
782
+                    'error' => $l->t('Your data directory is readable by other users.'),
783
+                    'hint' => $l->t('Please change the permissions to 0770 so that the directory cannot be listed by other users.'),
784
+                ]];
785
+            }
786
+        }
787
+        return [];
788
+    }
789
+
790
+    /**
791
+     * Check that the data directory exists and is valid by
792
+     * checking the existence of the ".ocdata" file.
793
+     *
794
+     * @param string $dataDirectory data directory path
795
+     * @return array errors found
796
+     */
797
+    public static function checkDataDirectoryValidity($dataDirectory) {
798
+        $l = \OC::$server->getL10N('lib');
799
+        $errors = [];
800
+        if ($dataDirectory[0] !== '/') {
801
+            $errors[] = [
802
+                'error' => $l->t('Your data directory must be an absolute path.'),
803
+                'hint' => $l->t('Check the value of "datadirectory" in your configuration.')
804
+            ];
805
+        }
806
+        if (!file_exists($dataDirectory . '/.ocdata')) {
807
+            $errors[] = [
808
+                'error' => $l->t('Your data directory is invalid.'),
809
+                'hint' => $l->t('Ensure there is a file called ".ocdata"' .
810
+                    ' in the root of the data directory.')
811
+            ];
812
+        }
813
+        return $errors;
814
+    }
815
+
816
+    /**
817
+     * Check if the user is logged in, redirects to home if not. With
818
+     * redirect URL parameter to the request URI.
819
+     *
820
+     * @return void
821
+     */
822
+    public static function checkLoggedIn() {
823
+        // Check if we are a user
824
+        if (!\OC::$server->getUserSession()->isLoggedIn()) {
825
+            header('Location: ' . \OC::$server->getURLGenerator()->linkToRoute(
826
+                'core.login.showLoginForm',
827
+                [
828
+                    'redirect_url' => \OC::$server->getRequest()->getRequestUri(),
829
+                ]
830
+            )
831
+            );
832
+            exit();
833
+        }
834
+        // Redirect to 2FA challenge selection if 2FA challenge was not solved yet
835
+        if (\OC::$server->getTwoFactorAuthManager()->needsSecondFactor(\OC::$server->getUserSession()->getUser())) {
836
+            header('Location: ' . \OC::$server->getURLGenerator()->linkToRoute('core.TwoFactorChallenge.selectChallenge'));
837
+            exit();
838
+        }
839
+    }
840
+
841
+    /**
842
+     * Check if the user is a admin, redirects to home if not
843
+     *
844
+     * @return void
845
+     */
846
+    public static function checkAdminUser() {
847
+        OC_Util::checkLoggedIn();
848
+        if (!OC_User::isAdminUser(OC_User::getUser())) {
849
+            header('Location: ' . \OCP\Util::linkToAbsolute('', 'index.php'));
850
+            exit();
851
+        }
852
+    }
853
+
854
+    /**
855
+     * Returns the URL of the default page
856
+     * based on the system configuration and
857
+     * the apps visible for the current user
858
+     *
859
+     * @return string URL
860
+     * @suppress PhanDeprecatedFunction
861
+     */
862
+    public static function getDefaultPageUrl() {
863
+        /** @var IURLGenerator $urlGenerator */
864
+        $urlGenerator = \OC::$server->get(IURLGenerator::class);
865
+        return $urlGenerator->linkToDefaultPageUrl();
866
+    }
867
+
868
+    /**
869
+     * Redirect to the user default page
870
+     *
871
+     * @return void
872
+     */
873
+    public static function redirectToDefaultPage() {
874
+        $location = self::getDefaultPageUrl();
875
+        header('Location: ' . $location);
876
+        exit();
877
+    }
878
+
879
+    /**
880
+     * get an id unique for this instance
881
+     *
882
+     * @return string
883
+     */
884
+    public static function getInstanceId() {
885
+        $id = \OC::$server->getSystemConfig()->getValue('instanceid', null);
886
+        if (is_null($id)) {
887
+            // We need to guarantee at least one letter in instanceid so it can be used as the session_name
888
+            $id = 'oc' . \OC::$server->getSecureRandom()->generate(10, \OCP\Security\ISecureRandom::CHAR_LOWER.\OCP\Security\ISecureRandom::CHAR_DIGITS);
889
+            \OC::$server->getSystemConfig()->setValue('instanceid', $id);
890
+        }
891
+        return $id;
892
+    }
893
+
894
+    /**
895
+     * Public function to sanitize HTML
896
+     *
897
+     * This function is used to sanitize HTML and should be applied on any
898
+     * string or array of strings before displaying it on a web page.
899
+     *
900
+     * @param string|string[] $value
901
+     * @return string|string[] an array of sanitized strings or a single sanitized string, depends on the input parameter.
902
+     */
903
+    public static function sanitizeHTML($value) {
904
+        if (is_array($value)) {
905
+            /** @var string[] $value */
906
+            $value = array_map(function ($value) {
907
+                return self::sanitizeHTML($value);
908
+            }, $value);
909
+        } else {
910
+            // Specify encoding for PHP<5.4
911
+            $value = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
912
+        }
913
+        return $value;
914
+    }
915
+
916
+    /**
917
+     * Public function to encode url parameters
918
+     *
919
+     * This function is used to encode path to file before output.
920
+     * Encoding is done according to RFC 3986 with one exception:
921
+     * Character '/' is preserved as is.
922
+     *
923
+     * @param string $component part of URI to encode
924
+     * @return string
925
+     */
926
+    public static function encodePath($component) {
927
+        $encoded = rawurlencode($component);
928
+        $encoded = str_replace('%2F', '/', $encoded);
929
+        return $encoded;
930
+    }
931
+
932
+
933
+    public function createHtaccessTestFile(\OCP\IConfig $config) {
934
+        // php dev server does not support htaccess
935
+        if (php_sapi_name() === 'cli-server') {
936
+            return false;
937
+        }
938
+
939
+        // testdata
940
+        $fileName = '/htaccesstest.txt';
941
+        $testContent = 'This is used for testing whether htaccess is properly enabled to disallow access from the outside. This file can be safely removed.';
942
+
943
+        // creating a test file
944
+        $testFile = $config->getSystemValueString('datadirectory', OC::$SERVERROOT . '/data') . '/' . $fileName;
945
+
946
+        if (file_exists($testFile)) {// already running this test, possible recursive call
947
+            return false;
948
+        }
949
+
950
+        $fp = @fopen($testFile, 'w');
951
+        if (!$fp) {
952
+            throw new \OCP\HintException('Can\'t create test file to check for working .htaccess file.',
953
+                'Make sure it is possible for the web server to write to ' . $testFile);
954
+        }
955
+        fwrite($fp, $testContent);
956
+        fclose($fp);
957
+
958
+        return $testContent;
959
+    }
960
+
961
+    /**
962
+     * Check if the .htaccess file is working
963
+     *
964
+     * @param \OCP\IConfig $config
965
+     * @return bool
966
+     * @throws Exception
967
+     * @throws \OCP\HintException If the test file can't get written.
968
+     */
969
+    public function isHtaccessWorking(\OCP\IConfig $config) {
970
+        if (\OC::$CLI || !$config->getSystemValueBool('check_for_working_htaccess', true)) {
971
+            return true;
972
+        }
973
+
974
+        $testContent = $this->createHtaccessTestFile($config);
975
+        if ($testContent === false) {
976
+            return false;
977
+        }
978
+
979
+        $fileName = '/htaccesstest.txt';
980
+        $testFile = $config->getSystemValueString('datadirectory', OC::$SERVERROOT . '/data') . '/' . $fileName;
981
+
982
+        // accessing the file via http
983
+        $url = \OC::$server->getURLGenerator()->getAbsoluteURL(OC::$WEBROOT . '/data' . $fileName);
984
+        try {
985
+            $content = \OC::$server->getHTTPClientService()->newClient()->get($url)->getBody();
986
+        } catch (\Exception $e) {
987
+            $content = false;
988
+        }
989
+
990
+        if (str_starts_with($url, 'https:')) {
991
+            $url = 'http:' . substr($url, 6);
992
+        } else {
993
+            $url = 'https:' . substr($url, 5);
994
+        }
995
+
996
+        try {
997
+            $fallbackContent = \OC::$server->getHTTPClientService()->newClient()->get($url)->getBody();
998
+        } catch (\Exception $e) {
999
+            $fallbackContent = false;
1000
+        }
1001
+
1002
+        // cleanup
1003
+        @unlink($testFile);
1004
+
1005
+        /*
1006 1006
 		 * If the content is not equal to test content our .htaccess
1007 1007
 		 * is working as required
1008 1008
 		 */
1009
-		return $content !== $testContent && $fallbackContent !== $testContent;
1010
-	}
1011
-
1012
-	/**
1013
-	 * Check if current locale is non-UTF8
1014
-	 *
1015
-	 * @return bool
1016
-	 */
1017
-	private static function isNonUTF8Locale() {
1018
-		if (function_exists('escapeshellcmd')) {
1019
-			return '' === escapeshellcmd('§');
1020
-		} elseif (function_exists('escapeshellarg')) {
1021
-			return '\'\'' === escapeshellarg('§');
1022
-		} else {
1023
-			return 0 === preg_match('/utf-?8/i', setlocale(LC_CTYPE, 0));
1024
-		}
1025
-	}
1026
-
1027
-	/**
1028
-	 * Check if the setlocale call does not work. This can happen if the right
1029
-	 * local packages are not available on the server.
1030
-	 *
1031
-	 * @return bool
1032
-	 */
1033
-	public static function isSetLocaleWorking() {
1034
-		if (self::isNonUTF8Locale()) {
1035
-			// Borrowed from \Patchwork\Utf8\Bootup::initLocale
1036
-			setlocale(LC_ALL, 'C.UTF-8', 'C');
1037
-			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');
1038
-
1039
-			// Check again
1040
-			if (self::isNonUTF8Locale()) {
1041
-				return false;
1042
-			}
1043
-		}
1044
-
1045
-		return true;
1046
-	}
1047
-
1048
-	/**
1049
-	 * Check if it's possible to get the inline annotations
1050
-	 *
1051
-	 * @return bool
1052
-	 */
1053
-	public static function isAnnotationsWorking() {
1054
-		$reflection = new \ReflectionMethod(__METHOD__);
1055
-		$docs = $reflection->getDocComment();
1056
-
1057
-		return (is_string($docs) && strlen($docs) > 50);
1058
-	}
1059
-
1060
-	/**
1061
-	 * Check if the PHP module fileinfo is loaded.
1062
-	 *
1063
-	 * @return bool
1064
-	 */
1065
-	public static function fileInfoLoaded() {
1066
-		return function_exists('finfo_open');
1067
-	}
1068
-
1069
-	/**
1070
-	 * clear all levels of output buffering
1071
-	 *
1072
-	 * @return void
1073
-	 */
1074
-	public static function obEnd() {
1075
-		while (ob_get_level()) {
1076
-			ob_end_clean();
1077
-		}
1078
-	}
1079
-
1080
-	/**
1081
-	 * Checks whether the server is running on Mac OS X
1082
-	 *
1083
-	 * @return bool true if running on Mac OS X, false otherwise
1084
-	 */
1085
-	public static function runningOnMac() {
1086
-		return (strtoupper(substr(PHP_OS, 0, 6)) === 'DARWIN');
1087
-	}
1088
-
1089
-	/**
1090
-	 * Handles the case that there may not be a theme, then check if a "default"
1091
-	 * theme exists and take that one
1092
-	 *
1093
-	 * @return string the theme
1094
-	 */
1095
-	public static function getTheme() {
1096
-		$theme = \OC::$server->getSystemConfig()->getValue("theme", '');
1097
-
1098
-		if ($theme === '') {
1099
-			if (is_dir(OC::$SERVERROOT . '/themes/default')) {
1100
-				$theme = 'default';
1101
-			}
1102
-		}
1103
-
1104
-		return $theme;
1105
-	}
1106
-
1107
-	/**
1108
-	 * Normalize a unicode string
1109
-	 *
1110
-	 * @param string $value a not normalized string
1111
-	 * @return bool|string
1112
-	 */
1113
-	public static function normalizeUnicode($value) {
1114
-		if (Normalizer::isNormalized($value)) {
1115
-			return $value;
1116
-		}
1117
-
1118
-		$normalizedValue = Normalizer::normalize($value);
1119
-		if ($normalizedValue === null || $normalizedValue === false) {
1120
-			\OC::$server->getLogger()->warning('normalizing failed for "' . $value . '"', ['app' => 'core']);
1121
-			return $value;
1122
-		}
1123
-
1124
-		return $normalizedValue;
1125
-	}
1126
-
1127
-	/**
1128
-	 * A human readable string is generated based on version and build number
1129
-	 *
1130
-	 * @return string
1131
-	 */
1132
-	public static function getHumanVersion() {
1133
-		$version = OC_Util::getVersionString();
1134
-		$build = OC_Util::getBuild();
1135
-		if (!empty($build) and OC_Util::getChannel() === 'daily') {
1136
-			$version .= ' Build:' . $build;
1137
-		}
1138
-		return $version;
1139
-	}
1140
-
1141
-	/**
1142
-	 * Returns whether the given file name is valid
1143
-	 *
1144
-	 * @param string $file file name to check
1145
-	 * @return bool true if the file name is valid, false otherwise
1146
-	 * @deprecated use \OC\Files\View::verifyPath()
1147
-	 */
1148
-	public static function isValidFileName($file) {
1149
-		$trimmed = trim($file);
1150
-		if ($trimmed === '') {
1151
-			return false;
1152
-		}
1153
-		if (\OC\Files\Filesystem::isIgnoredDir($trimmed)) {
1154
-			return false;
1155
-		}
1156
-
1157
-		// detect part files
1158
-		if (preg_match('/' . \OCP\Files\FileInfo::BLACKLIST_FILES_REGEX . '/', $trimmed) !== 0) {
1159
-			return false;
1160
-		}
1161
-
1162
-		foreach (str_split($trimmed) as $char) {
1163
-			if (str_contains(\OCP\Constants::FILENAME_INVALID_CHARS, $char)) {
1164
-				return false;
1165
-			}
1166
-		}
1167
-		return true;
1168
-	}
1169
-
1170
-	/**
1171
-	 * Check whether the instance needs to perform an upgrade,
1172
-	 * either when the core version is higher or any app requires
1173
-	 * an upgrade.
1174
-	 *
1175
-	 * @param \OC\SystemConfig $config
1176
-	 * @return bool whether the core or any app needs an upgrade
1177
-	 * @throws \OCP\HintException When the upgrade from the given version is not allowed
1178
-	 */
1179
-	public static function needUpgrade(\OC\SystemConfig $config) {
1180
-		if ($config->getValue('installed', false)) {
1181
-			$installedVersion = $config->getValue('version', '0.0.0');
1182
-			$currentVersion = implode('.', \OCP\Util::getVersion());
1183
-			$versionDiff = version_compare($currentVersion, $installedVersion);
1184
-			if ($versionDiff > 0) {
1185
-				return true;
1186
-			} elseif ($config->getValue('debug', false) && $versionDiff < 0) {
1187
-				// downgrade with debug
1188
-				$installedMajor = explode('.', $installedVersion);
1189
-				$installedMajor = $installedMajor[0] . '.' . $installedMajor[1];
1190
-				$currentMajor = explode('.', $currentVersion);
1191
-				$currentMajor = $currentMajor[0] . '.' . $currentMajor[1];
1192
-				if ($installedMajor === $currentMajor) {
1193
-					// Same major, allow downgrade for developers
1194
-					return true;
1195
-				} else {
1196
-					// downgrade attempt, throw exception
1197
-					throw new \OCP\HintException('Downgrading is not supported and is likely to cause unpredictable issues (from ' . $installedVersion . ' to ' . $currentVersion . ')');
1198
-				}
1199
-			} elseif ($versionDiff < 0) {
1200
-				// downgrade attempt, throw exception
1201
-				throw new \OCP\HintException('Downgrading is not supported and is likely to cause unpredictable issues (from ' . $installedVersion . ' to ' . $currentVersion . ')');
1202
-			}
1203
-
1204
-			// also check for upgrades for apps (independently from the user)
1205
-			$apps = \OC_App::getEnabledApps(false, true);
1206
-			$shouldUpgrade = false;
1207
-			foreach ($apps as $app) {
1208
-				if (\OC_App::shouldUpgrade($app)) {
1209
-					$shouldUpgrade = true;
1210
-					break;
1211
-				}
1212
-			}
1213
-			return $shouldUpgrade;
1214
-		} else {
1215
-			return false;
1216
-		}
1217
-	}
1009
+        return $content !== $testContent && $fallbackContent !== $testContent;
1010
+    }
1011
+
1012
+    /**
1013
+     * Check if current locale is non-UTF8
1014
+     *
1015
+     * @return bool
1016
+     */
1017
+    private static function isNonUTF8Locale() {
1018
+        if (function_exists('escapeshellcmd')) {
1019
+            return '' === escapeshellcmd('§');
1020
+        } elseif (function_exists('escapeshellarg')) {
1021
+            return '\'\'' === escapeshellarg('§');
1022
+        } else {
1023
+            return 0 === preg_match('/utf-?8/i', setlocale(LC_CTYPE, 0));
1024
+        }
1025
+    }
1026
+
1027
+    /**
1028
+     * Check if the setlocale call does not work. This can happen if the right
1029
+     * local packages are not available on the server.
1030
+     *
1031
+     * @return bool
1032
+     */
1033
+    public static function isSetLocaleWorking() {
1034
+        if (self::isNonUTF8Locale()) {
1035
+            // Borrowed from \Patchwork\Utf8\Bootup::initLocale
1036
+            setlocale(LC_ALL, 'C.UTF-8', 'C');
1037
+            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');
1038
+
1039
+            // Check again
1040
+            if (self::isNonUTF8Locale()) {
1041
+                return false;
1042
+            }
1043
+        }
1044
+
1045
+        return true;
1046
+    }
1047
+
1048
+    /**
1049
+     * Check if it's possible to get the inline annotations
1050
+     *
1051
+     * @return bool
1052
+     */
1053
+    public static function isAnnotationsWorking() {
1054
+        $reflection = new \ReflectionMethod(__METHOD__);
1055
+        $docs = $reflection->getDocComment();
1056
+
1057
+        return (is_string($docs) && strlen($docs) > 50);
1058
+    }
1059
+
1060
+    /**
1061
+     * Check if the PHP module fileinfo is loaded.
1062
+     *
1063
+     * @return bool
1064
+     */
1065
+    public static function fileInfoLoaded() {
1066
+        return function_exists('finfo_open');
1067
+    }
1068
+
1069
+    /**
1070
+     * clear all levels of output buffering
1071
+     *
1072
+     * @return void
1073
+     */
1074
+    public static function obEnd() {
1075
+        while (ob_get_level()) {
1076
+            ob_end_clean();
1077
+        }
1078
+    }
1079
+
1080
+    /**
1081
+     * Checks whether the server is running on Mac OS X
1082
+     *
1083
+     * @return bool true if running on Mac OS X, false otherwise
1084
+     */
1085
+    public static function runningOnMac() {
1086
+        return (strtoupper(substr(PHP_OS, 0, 6)) === 'DARWIN');
1087
+    }
1088
+
1089
+    /**
1090
+     * Handles the case that there may not be a theme, then check if a "default"
1091
+     * theme exists and take that one
1092
+     *
1093
+     * @return string the theme
1094
+     */
1095
+    public static function getTheme() {
1096
+        $theme = \OC::$server->getSystemConfig()->getValue("theme", '');
1097
+
1098
+        if ($theme === '') {
1099
+            if (is_dir(OC::$SERVERROOT . '/themes/default')) {
1100
+                $theme = 'default';
1101
+            }
1102
+        }
1103
+
1104
+        return $theme;
1105
+    }
1106
+
1107
+    /**
1108
+     * Normalize a unicode string
1109
+     *
1110
+     * @param string $value a not normalized string
1111
+     * @return bool|string
1112
+     */
1113
+    public static function normalizeUnicode($value) {
1114
+        if (Normalizer::isNormalized($value)) {
1115
+            return $value;
1116
+        }
1117
+
1118
+        $normalizedValue = Normalizer::normalize($value);
1119
+        if ($normalizedValue === null || $normalizedValue === false) {
1120
+            \OC::$server->getLogger()->warning('normalizing failed for "' . $value . '"', ['app' => 'core']);
1121
+            return $value;
1122
+        }
1123
+
1124
+        return $normalizedValue;
1125
+    }
1126
+
1127
+    /**
1128
+     * A human readable string is generated based on version and build number
1129
+     *
1130
+     * @return string
1131
+     */
1132
+    public static function getHumanVersion() {
1133
+        $version = OC_Util::getVersionString();
1134
+        $build = OC_Util::getBuild();
1135
+        if (!empty($build) and OC_Util::getChannel() === 'daily') {
1136
+            $version .= ' Build:' . $build;
1137
+        }
1138
+        return $version;
1139
+    }
1140
+
1141
+    /**
1142
+     * Returns whether the given file name is valid
1143
+     *
1144
+     * @param string $file file name to check
1145
+     * @return bool true if the file name is valid, false otherwise
1146
+     * @deprecated use \OC\Files\View::verifyPath()
1147
+     */
1148
+    public static function isValidFileName($file) {
1149
+        $trimmed = trim($file);
1150
+        if ($trimmed === '') {
1151
+            return false;
1152
+        }
1153
+        if (\OC\Files\Filesystem::isIgnoredDir($trimmed)) {
1154
+            return false;
1155
+        }
1156
+
1157
+        // detect part files
1158
+        if (preg_match('/' . \OCP\Files\FileInfo::BLACKLIST_FILES_REGEX . '/', $trimmed) !== 0) {
1159
+            return false;
1160
+        }
1161
+
1162
+        foreach (str_split($trimmed) as $char) {
1163
+            if (str_contains(\OCP\Constants::FILENAME_INVALID_CHARS, $char)) {
1164
+                return false;
1165
+            }
1166
+        }
1167
+        return true;
1168
+    }
1169
+
1170
+    /**
1171
+     * Check whether the instance needs to perform an upgrade,
1172
+     * either when the core version is higher or any app requires
1173
+     * an upgrade.
1174
+     *
1175
+     * @param \OC\SystemConfig $config
1176
+     * @return bool whether the core or any app needs an upgrade
1177
+     * @throws \OCP\HintException When the upgrade from the given version is not allowed
1178
+     */
1179
+    public static function needUpgrade(\OC\SystemConfig $config) {
1180
+        if ($config->getValue('installed', false)) {
1181
+            $installedVersion = $config->getValue('version', '0.0.0');
1182
+            $currentVersion = implode('.', \OCP\Util::getVersion());
1183
+            $versionDiff = version_compare($currentVersion, $installedVersion);
1184
+            if ($versionDiff > 0) {
1185
+                return true;
1186
+            } elseif ($config->getValue('debug', false) && $versionDiff < 0) {
1187
+                // downgrade with debug
1188
+                $installedMajor = explode('.', $installedVersion);
1189
+                $installedMajor = $installedMajor[0] . '.' . $installedMajor[1];
1190
+                $currentMajor = explode('.', $currentVersion);
1191
+                $currentMajor = $currentMajor[0] . '.' . $currentMajor[1];
1192
+                if ($installedMajor === $currentMajor) {
1193
+                    // Same major, allow downgrade for developers
1194
+                    return true;
1195
+                } else {
1196
+                    // downgrade attempt, throw exception
1197
+                    throw new \OCP\HintException('Downgrading is not supported and is likely to cause unpredictable issues (from ' . $installedVersion . ' to ' . $currentVersion . ')');
1198
+                }
1199
+            } elseif ($versionDiff < 0) {
1200
+                // downgrade attempt, throw exception
1201
+                throw new \OCP\HintException('Downgrading is not supported and is likely to cause unpredictable issues (from ' . $installedVersion . ' to ' . $currentVersion . ')');
1202
+            }
1203
+
1204
+            // also check for upgrades for apps (independently from the user)
1205
+            $apps = \OC_App::getEnabledApps(false, true);
1206
+            $shouldUpgrade = false;
1207
+            foreach ($apps as $app) {
1208
+                if (\OC_App::shouldUpgrade($app)) {
1209
+                    $shouldUpgrade = true;
1210
+                    break;
1211
+                }
1212
+            }
1213
+            return $shouldUpgrade;
1214
+        } else {
1215
+            return false;
1216
+        }
1217
+    }
1218 1218
 }
Please login to merge, or discard this patch.
Spacing   +34 added lines, -34 removed lines patch added patch discarded remove patch
@@ -184,7 +184,7 @@  discard block
 block discarded – undo
184 184
 		/** @var LoggerInterface $logger */
185 185
 		$logger = \OC::$server->get(LoggerInterface::class);
186 186
 
187
-		$plainSkeletonDirectory = \OC::$server->getConfig()->getSystemValueString('skeletondirectory', \OC::$SERVERROOT . '/core/skeleton');
187
+		$plainSkeletonDirectory = \OC::$server->getConfig()->getSystemValueString('skeletondirectory', \OC::$SERVERROOT.'/core/skeleton');
188 188
 		$userLang = \OC::$server->getL10NFactory()->findLanguage();
189 189
 		$skeletonDirectory = str_replace('{lang}', $userLang, $plainSkeletonDirectory);
190 190
 
@@ -206,9 +206,9 @@  discard block
 block discarded – undo
206 206
 		if ($instanceId === null) {
207 207
 			throw new \RuntimeException('no instance id!');
208 208
 		}
209
-		$appdata = 'appdata_' . $instanceId;
209
+		$appdata = 'appdata_'.$instanceId;
210 210
 		if ($userId === $appdata) {
211
-			throw new \RuntimeException('username is reserved name: ' . $appdata);
211
+			throw new \RuntimeException('username is reserved name: '.$appdata);
212 212
 		}
213 213
 
214 214
 		if (!empty($skeletonDirectory)) {
@@ -243,14 +243,14 @@  discard block
 block discarded – undo
243 243
 		// Copy the files
244 244
 		while (false !== ($file = readdir($dir))) {
245 245
 			if (!\OC\Files\Filesystem::isIgnoredDir($file)) {
246
-				if (is_dir($source . '/' . $file)) {
246
+				if (is_dir($source.'/'.$file)) {
247 247
 					$child = $target->newFolder($file);
248
-					self::copyr($source . '/' . $file, $child);
248
+					self::copyr($source.'/'.$file, $child);
249 249
 				} else {
250 250
 					$child = $target->newFile($file);
251
-					$sourceStream = fopen($source . '/' . $file, 'r');
251
+					$sourceStream = fopen($source.'/'.$file, 'r');
252 252
 					if ($sourceStream === false) {
253
-						$logger->error(sprintf('Could not fopen "%s"', $source . '/' . $file), ['app' => 'core']);
253
+						$logger->error(sprintf('Could not fopen "%s"', $source.'/'.$file), ['app' => 'core']);
254 254
 						closedir($dir);
255 255
 						return;
256 256
 					}
@@ -326,7 +326,7 @@  discard block
 block discarded – undo
326 326
 			return;
327 327
 		}
328 328
 
329
-		require OC::$SERVERROOT . '/version.php';
329
+		require OC::$SERVERROOT.'/version.php';
330 330
 		/** @var int $timestamp */
331 331
 		self::$versionCache['OC_Version_Timestamp'] = \OC::$VERSION_MTIME;
332 332
 		/** @var string $OC_Version */
@@ -500,7 +500,7 @@  discard block
 block discarded – undo
500 500
 	public static function checkServer(\OC\SystemConfig $config) {
501 501
 		$l = \OC::$server->getL10N('lib');
502 502
 		$errors = [];
503
-		$CONFIG_DATADIRECTORY = $config->getValue('datadirectory', OC::$SERVERROOT . '/data');
503
+		$CONFIG_DATADIRECTORY = $config->getValue('datadirectory', OC::$SERVERROOT.'/data');
504 504
 
505 505
 		if (!self::needUpgrade($config) && $config->getValue('installed', false)) {
506 506
 			// this check needs to be done every time
@@ -540,9 +540,9 @@  discard block
 block discarded – undo
540 540
 				$errors[] = [
541 541
 					'error' => $l->t('Cannot write into "config" directory.'),
542 542
 					'hint' => $l->t('This can usually be fixed by giving the web server write access to the config directory. See %s',
543
-						[ $urlGenerator->linkToDocs('admin-dir_permissions') ]) . '. '
543
+						[$urlGenerator->linkToDocs('admin-dir_permissions')]).'. '
544 544
 						. $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',
545
-							[ $urlGenerator->linkToDocs('admin-config') ])
545
+							[$urlGenerator->linkToDocs('admin-config')])
546 546
 				];
547 547
 			}
548 548
 		}
@@ -803,10 +803,10 @@  discard block
 block discarded – undo
803 803
 				'hint' => $l->t('Check the value of "datadirectory" in your configuration.')
804 804
 			];
805 805
 		}
806
-		if (!file_exists($dataDirectory . '/.ocdata')) {
806
+		if (!file_exists($dataDirectory.'/.ocdata')) {
807 807
 			$errors[] = [
808 808
 				'error' => $l->t('Your data directory is invalid.'),
809
-				'hint' => $l->t('Ensure there is a file called ".ocdata"' .
809
+				'hint' => $l->t('Ensure there is a file called ".ocdata"'.
810 810
 					' in the root of the data directory.')
811 811
 			];
812 812
 		}
@@ -822,7 +822,7 @@  discard block
 block discarded – undo
822 822
 	public static function checkLoggedIn() {
823 823
 		// Check if we are a user
824 824
 		if (!\OC::$server->getUserSession()->isLoggedIn()) {
825
-			header('Location: ' . \OC::$server->getURLGenerator()->linkToRoute(
825
+			header('Location: '.\OC::$server->getURLGenerator()->linkToRoute(
826 826
 				'core.login.showLoginForm',
827 827
 				[
828 828
 					'redirect_url' => \OC::$server->getRequest()->getRequestUri(),
@@ -833,7 +833,7 @@  discard block
 block discarded – undo
833 833
 		}
834 834
 		// Redirect to 2FA challenge selection if 2FA challenge was not solved yet
835 835
 		if (\OC::$server->getTwoFactorAuthManager()->needsSecondFactor(\OC::$server->getUserSession()->getUser())) {
836
-			header('Location: ' . \OC::$server->getURLGenerator()->linkToRoute('core.TwoFactorChallenge.selectChallenge'));
836
+			header('Location: '.\OC::$server->getURLGenerator()->linkToRoute('core.TwoFactorChallenge.selectChallenge'));
837 837
 			exit();
838 838
 		}
839 839
 	}
@@ -846,7 +846,7 @@  discard block
 block discarded – undo
846 846
 	public static function checkAdminUser() {
847 847
 		OC_Util::checkLoggedIn();
848 848
 		if (!OC_User::isAdminUser(OC_User::getUser())) {
849
-			header('Location: ' . \OCP\Util::linkToAbsolute('', 'index.php'));
849
+			header('Location: '.\OCP\Util::linkToAbsolute('', 'index.php'));
850 850
 			exit();
851 851
 		}
852 852
 	}
@@ -872,7 +872,7 @@  discard block
 block discarded – undo
872 872
 	 */
873 873
 	public static function redirectToDefaultPage() {
874 874
 		$location = self::getDefaultPageUrl();
875
-		header('Location: ' . $location);
875
+		header('Location: '.$location);
876 876
 		exit();
877 877
 	}
878 878
 
@@ -885,7 +885,7 @@  discard block
 block discarded – undo
885 885
 		$id = \OC::$server->getSystemConfig()->getValue('instanceid', null);
886 886
 		if (is_null($id)) {
887 887
 			// We need to guarantee at least one letter in instanceid so it can be used as the session_name
888
-			$id = 'oc' . \OC::$server->getSecureRandom()->generate(10, \OCP\Security\ISecureRandom::CHAR_LOWER.\OCP\Security\ISecureRandom::CHAR_DIGITS);
888
+			$id = 'oc'.\OC::$server->getSecureRandom()->generate(10, \OCP\Security\ISecureRandom::CHAR_LOWER.\OCP\Security\ISecureRandom::CHAR_DIGITS);
889 889
 			\OC::$server->getSystemConfig()->setValue('instanceid', $id);
890 890
 		}
891 891
 		return $id;
@@ -903,12 +903,12 @@  discard block
 block discarded – undo
903 903
 	public static function sanitizeHTML($value) {
904 904
 		if (is_array($value)) {
905 905
 			/** @var string[] $value */
906
-			$value = array_map(function ($value) {
906
+			$value = array_map(function($value) {
907 907
 				return self::sanitizeHTML($value);
908 908
 			}, $value);
909 909
 		} else {
910 910
 			// Specify encoding for PHP<5.4
911
-			$value = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
911
+			$value = htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
912 912
 		}
913 913
 		return $value;
914 914
 	}
@@ -941,7 +941,7 @@  discard block
 block discarded – undo
941 941
 		$testContent = 'This is used for testing whether htaccess is properly enabled to disallow access from the outside. This file can be safely removed.';
942 942
 
943 943
 		// creating a test file
944
-		$testFile = $config->getSystemValueString('datadirectory', OC::$SERVERROOT . '/data') . '/' . $fileName;
944
+		$testFile = $config->getSystemValueString('datadirectory', OC::$SERVERROOT.'/data').'/'.$fileName;
945 945
 
946 946
 		if (file_exists($testFile)) {// already running this test, possible recursive call
947 947
 			return false;
@@ -950,7 +950,7 @@  discard block
 block discarded – undo
950 950
 		$fp = @fopen($testFile, 'w');
951 951
 		if (!$fp) {
952 952
 			throw new \OCP\HintException('Can\'t create test file to check for working .htaccess file.',
953
-				'Make sure it is possible for the web server to write to ' . $testFile);
953
+				'Make sure it is possible for the web server to write to '.$testFile);
954 954
 		}
955 955
 		fwrite($fp, $testContent);
956 956
 		fclose($fp);
@@ -977,10 +977,10 @@  discard block
 block discarded – undo
977 977
 		}
978 978
 
979 979
 		$fileName = '/htaccesstest.txt';
980
-		$testFile = $config->getSystemValueString('datadirectory', OC::$SERVERROOT . '/data') . '/' . $fileName;
980
+		$testFile = $config->getSystemValueString('datadirectory', OC::$SERVERROOT.'/data').'/'.$fileName;
981 981
 
982 982
 		// accessing the file via http
983
-		$url = \OC::$server->getURLGenerator()->getAbsoluteURL(OC::$WEBROOT . '/data' . $fileName);
983
+		$url = \OC::$server->getURLGenerator()->getAbsoluteURL(OC::$WEBROOT.'/data'.$fileName);
984 984
 		try {
985 985
 			$content = \OC::$server->getHTTPClientService()->newClient()->get($url)->getBody();
986 986
 		} catch (\Exception $e) {
@@ -988,9 +988,9 @@  discard block
 block discarded – undo
988 988
 		}
989 989
 
990 990
 		if (str_starts_with($url, 'https:')) {
991
-			$url = 'http:' . substr($url, 6);
991
+			$url = 'http:'.substr($url, 6);
992 992
 		} else {
993
-			$url = 'https:' . substr($url, 5);
993
+			$url = 'https:'.substr($url, 5);
994 994
 		}
995 995
 
996 996
 		try {
@@ -1096,7 +1096,7 @@  discard block
 block discarded – undo
1096 1096
 		$theme = \OC::$server->getSystemConfig()->getValue("theme", '');
1097 1097
 
1098 1098
 		if ($theme === '') {
1099
-			if (is_dir(OC::$SERVERROOT . '/themes/default')) {
1099
+			if (is_dir(OC::$SERVERROOT.'/themes/default')) {
1100 1100
 				$theme = 'default';
1101 1101
 			}
1102 1102
 		}
@@ -1117,7 +1117,7 @@  discard block
 block discarded – undo
1117 1117
 
1118 1118
 		$normalizedValue = Normalizer::normalize($value);
1119 1119
 		if ($normalizedValue === null || $normalizedValue === false) {
1120
-			\OC::$server->getLogger()->warning('normalizing failed for "' . $value . '"', ['app' => 'core']);
1120
+			\OC::$server->getLogger()->warning('normalizing failed for "'.$value.'"', ['app' => 'core']);
1121 1121
 			return $value;
1122 1122
 		}
1123 1123
 
@@ -1133,7 +1133,7 @@  discard block
 block discarded – undo
1133 1133
 		$version = OC_Util::getVersionString();
1134 1134
 		$build = OC_Util::getBuild();
1135 1135
 		if (!empty($build) and OC_Util::getChannel() === 'daily') {
1136
-			$version .= ' Build:' . $build;
1136
+			$version .= ' Build:'.$build;
1137 1137
 		}
1138 1138
 		return $version;
1139 1139
 	}
@@ -1155,7 +1155,7 @@  discard block
 block discarded – undo
1155 1155
 		}
1156 1156
 
1157 1157
 		// detect part files
1158
-		if (preg_match('/' . \OCP\Files\FileInfo::BLACKLIST_FILES_REGEX . '/', $trimmed) !== 0) {
1158
+		if (preg_match('/'.\OCP\Files\FileInfo::BLACKLIST_FILES_REGEX.'/', $trimmed) !== 0) {
1159 1159
 			return false;
1160 1160
 		}
1161 1161
 
@@ -1186,19 +1186,19 @@  discard block
 block discarded – undo
1186 1186
 			} elseif ($config->getValue('debug', false) && $versionDiff < 0) {
1187 1187
 				// downgrade with debug
1188 1188
 				$installedMajor = explode('.', $installedVersion);
1189
-				$installedMajor = $installedMajor[0] . '.' . $installedMajor[1];
1189
+				$installedMajor = $installedMajor[0].'.'.$installedMajor[1];
1190 1190
 				$currentMajor = explode('.', $currentVersion);
1191
-				$currentMajor = $currentMajor[0] . '.' . $currentMajor[1];
1191
+				$currentMajor = $currentMajor[0].'.'.$currentMajor[1];
1192 1192
 				if ($installedMajor === $currentMajor) {
1193 1193
 					// Same major, allow downgrade for developers
1194 1194
 					return true;
1195 1195
 				} else {
1196 1196
 					// downgrade attempt, throw exception
1197
-					throw new \OCP\HintException('Downgrading is not supported and is likely to cause unpredictable issues (from ' . $installedVersion . ' to ' . $currentVersion . ')');
1197
+					throw new \OCP\HintException('Downgrading is not supported and is likely to cause unpredictable issues (from '.$installedVersion.' to '.$currentVersion.')');
1198 1198
 				}
1199 1199
 			} elseif ($versionDiff < 0) {
1200 1200
 				// downgrade attempt, throw exception
1201
-				throw new \OCP\HintException('Downgrading is not supported and is likely to cause unpredictable issues (from ' . $installedVersion . ' to ' . $currentVersion . ')');
1201
+				throw new \OCP\HintException('Downgrading is not supported and is likely to cause unpredictable issues (from '.$installedVersion.' to '.$currentVersion.')');
1202 1202
 			}
1203 1203
 
1204 1204
 			// also check for upgrades for apps (independently from the user)
Please login to merge, or discard this patch.
lib/private/legacy/OC_App.php 2 patches
Indentation   +899 added lines, -899 removed lines patch added patch discarded remove patch
@@ -73,903 +73,903 @@
 block discarded – undo
73 73
  * upgrading and removing apps.
74 74
  */
75 75
 class OC_App {
76
-	private static $adminForms = [];
77
-	private static $personalForms = [];
78
-	private static $altLogin = [];
79
-	private static $alreadyRegistered = [];
80
-	public const supportedApp = 300;
81
-	public const officialApp = 200;
82
-
83
-	/**
84
-	 * clean the appId
85
-	 *
86
-	 * @psalm-taint-escape file
87
-	 * @psalm-taint-escape include
88
-	 * @psalm-taint-escape html
89
-	 * @psalm-taint-escape has_quotes
90
-	 *
91
-	 * @param string $app AppId that needs to be cleaned
92
-	 * @return string
93
-	 */
94
-	public static function cleanAppId(string $app): string {
95
-		return str_replace(['<', '>', '"', "'", '\0', '/', '\\', '..'], '', $app);
96
-	}
97
-
98
-	/**
99
-	 * Check if an app is loaded
100
-	 *
101
-	 * @param string $app
102
-	 * @return bool
103
-	 * @deprecated 27.0.0 use IAppManager::isAppLoaded
104
-	 */
105
-	public static function isAppLoaded(string $app): bool {
106
-		return \OC::$server->get(IAppManager::class)->isAppLoaded($app);
107
-	}
108
-
109
-	/**
110
-	 * loads all apps
111
-	 *
112
-	 * @param string[] $types
113
-	 * @return bool
114
-	 *
115
-	 * This function walks through the ownCloud directory and loads all apps
116
-	 * it can find. A directory contains an app if the file /appinfo/info.xml
117
-	 * exists.
118
-	 *
119
-	 * if $types is set to non-empty array, only apps of those types will be loaded
120
-	 */
121
-	public static function loadApps(array $types = []): bool {
122
-		if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
123
-			// This should be done before calling this method so that appmanager can be used
124
-			return false;
125
-		}
126
-		return \OC::$server->get(IAppManager::class)->loadApps($types);
127
-	}
128
-
129
-	/**
130
-	 * load a single app
131
-	 *
132
-	 * @param string $app
133
-	 * @throws Exception
134
-	 * @deprecated 27.0.0 use IAppManager::loadApp
135
-	 */
136
-	public static function loadApp(string $app): void {
137
-		\OC::$server->get(IAppManager::class)->loadApp($app);
138
-	}
139
-
140
-	/**
141
-	 * @internal
142
-	 * @param string $app
143
-	 * @param string $path
144
-	 * @param bool $force
145
-	 */
146
-	public static function registerAutoloading(string $app, string $path, bool $force = false) {
147
-		$key = $app . '-' . $path;
148
-		if (!$force && isset(self::$alreadyRegistered[$key])) {
149
-			return;
150
-		}
151
-
152
-		self::$alreadyRegistered[$key] = true;
153
-
154
-		// Register on PSR-4 composer autoloader
155
-		$appNamespace = \OC\AppFramework\App::buildAppNamespace($app);
156
-		\OC::$server->registerNamespace($app, $appNamespace);
157
-
158
-		if (file_exists($path . '/composer/autoload.php')) {
159
-			require_once $path . '/composer/autoload.php';
160
-		} else {
161
-			\OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true);
162
-		}
163
-
164
-		// Register Test namespace only when testing
165
-		if (defined('PHPUNIT_RUN') || defined('CLI_TEST_RUN')) {
166
-			\OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true);
167
-		}
168
-	}
169
-
170
-	/**
171
-	 * check if an app is of a specific type
172
-	 *
173
-	 * @param string $app
174
-	 * @param array $types
175
-	 * @return bool
176
-	 * @deprecated 27.0.0 use IAppManager::isType
177
-	 */
178
-	public static function isType(string $app, array $types): bool {
179
-		return \OC::$server->get(IAppManager::class)->isType($app, $types);
180
-	}
181
-
182
-	/**
183
-	 * read app types from info.xml and cache them in the database
184
-	 */
185
-	public static function setAppTypes(string $app) {
186
-		$appManager = \OC::$server->getAppManager();
187
-		$appData = $appManager->getAppInfo($app);
188
-		if (!is_array($appData)) {
189
-			return;
190
-		}
191
-
192
-		if (isset($appData['types'])) {
193
-			$appTypes = implode(',', $appData['types']);
194
-		} else {
195
-			$appTypes = '';
196
-			$appData['types'] = [];
197
-		}
198
-
199
-		$config = \OC::$server->getConfig();
200
-		$config->setAppValue($app, 'types', $appTypes);
201
-
202
-		if ($appManager->hasProtectedAppType($appData['types'])) {
203
-			$enabled = $config->getAppValue($app, 'enabled', 'yes');
204
-			if ($enabled !== 'yes' && $enabled !== 'no') {
205
-				$config->setAppValue($app, 'enabled', 'yes');
206
-			}
207
-		}
208
-	}
209
-
210
-	/**
211
-	 * Returns apps enabled for the current user.
212
-	 *
213
-	 * @param bool $forceRefresh whether to refresh the cache
214
-	 * @param bool $all whether to return apps for all users, not only the
215
-	 * currently logged in one
216
-	 * @return string[]
217
-	 */
218
-	public static function getEnabledApps(bool $forceRefresh = false, bool $all = false): array {
219
-		if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
220
-			return [];
221
-		}
222
-		// in incognito mode or when logged out, $user will be false,
223
-		// which is also the case during an upgrade
224
-		$appManager = \OC::$server->getAppManager();
225
-		if ($all) {
226
-			$user = null;
227
-		} else {
228
-			$user = \OC::$server->getUserSession()->getUser();
229
-		}
230
-
231
-		if (is_null($user)) {
232
-			$apps = $appManager->getInstalledApps();
233
-		} else {
234
-			$apps = $appManager->getEnabledAppsForUser($user);
235
-		}
236
-		$apps = array_filter($apps, function ($app) {
237
-			return $app !== 'files';//we add this manually
238
-		});
239
-		sort($apps);
240
-		array_unshift($apps, 'files');
241
-		return $apps;
242
-	}
243
-
244
-	/**
245
-	 * enables an app
246
-	 *
247
-	 * @param string $appId
248
-	 * @param array $groups (optional) when set, only these groups will have access to the app
249
-	 * @throws \Exception
250
-	 * @return void
251
-	 *
252
-	 * This function set an app as enabled in appconfig.
253
-	 */
254
-	public function enable(string $appId,
255
-						   array $groups = []) {
256
-		// Check if app is already downloaded
257
-		/** @var Installer $installer */
258
-		$installer = \OC::$server->query(Installer::class);
259
-		$isDownloaded = $installer->isDownloaded($appId);
260
-
261
-		if (!$isDownloaded) {
262
-			$installer->downloadApp($appId);
263
-		}
264
-
265
-		$installer->installApp($appId);
266
-
267
-		$appManager = \OC::$server->getAppManager();
268
-		if ($groups !== []) {
269
-			$groupManager = \OC::$server->getGroupManager();
270
-			$groupsList = [];
271
-			foreach ($groups as $group) {
272
-				$groupItem = $groupManager->get($group);
273
-				if ($groupItem instanceof \OCP\IGroup) {
274
-					$groupsList[] = $groupManager->get($group);
275
-				}
276
-			}
277
-			$appManager->enableAppForGroups($appId, $groupsList);
278
-		} else {
279
-			$appManager->enableApp($appId);
280
-		}
281
-	}
282
-
283
-	/**
284
-	 * Get the path where to install apps
285
-	 *
286
-	 * @return string|false
287
-	 */
288
-	public static function getInstallPath() {
289
-		foreach (OC::$APPSROOTS as $dir) {
290
-			if (isset($dir['writable']) && $dir['writable'] === true) {
291
-				return $dir['path'];
292
-			}
293
-		}
294
-
295
-		\OCP\Util::writeLog('core', 'No application directories are marked as writable.', ILogger::ERROR);
296
-		return null;
297
-	}
298
-
299
-
300
-	/**
301
-	 * search for an app in all app-directories
302
-	 *
303
-	 * @param string $appId
304
-	 * @param bool $ignoreCache ignore cache and rebuild it
305
-	 * @return false|string
306
-	 */
307
-	public static function findAppInDirectories(string $appId, bool $ignoreCache = false) {
308
-		$sanitizedAppId = self::cleanAppId($appId);
309
-		if ($sanitizedAppId !== $appId) {
310
-			return false;
311
-		}
312
-		static $app_dir = [];
313
-
314
-		if (isset($app_dir[$appId]) && !$ignoreCache) {
315
-			return $app_dir[$appId];
316
-		}
317
-
318
-		$possibleApps = [];
319
-		foreach (OC::$APPSROOTS as $dir) {
320
-			if (file_exists($dir['path'] . '/' . $appId)) {
321
-				$possibleApps[] = $dir;
322
-			}
323
-		}
324
-
325
-		if (empty($possibleApps)) {
326
-			return false;
327
-		} elseif (count($possibleApps) === 1) {
328
-			$dir = array_shift($possibleApps);
329
-			$app_dir[$appId] = $dir;
330
-			return $dir;
331
-		} else {
332
-			$versionToLoad = [];
333
-			foreach ($possibleApps as $possibleApp) {
334
-				$version = self::getAppVersionByPath($possibleApp['path'] . '/' . $appId);
335
-				if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) {
336
-					$versionToLoad = [
337
-						'dir' => $possibleApp,
338
-						'version' => $version,
339
-					];
340
-				}
341
-			}
342
-			$app_dir[$appId] = $versionToLoad['dir'];
343
-			return $versionToLoad['dir'];
344
-			//TODO - write test
345
-		}
346
-	}
347
-
348
-	/**
349
-	 * Get the directory for the given app.
350
-	 * If the app is defined in multiple directories, the first one is taken. (false if not found)
351
-	 *
352
-	 * @psalm-taint-specialize
353
-	 *
354
-	 * @param string $appId
355
-	 * @param bool $refreshAppPath should be set to true only during install/upgrade
356
-	 * @return string|false
357
-	 * @deprecated 11.0.0 use \OC::$server->getAppManager()->getAppPath()
358
-	 */
359
-	public static function getAppPath(string $appId, bool $refreshAppPath = false) {
360
-		if ($appId === null || trim($appId) === '') {
361
-			return false;
362
-		}
363
-
364
-		if (($dir = self::findAppInDirectories($appId, $refreshAppPath)) != false) {
365
-			return $dir['path'] . '/' . $appId;
366
-		}
367
-		return false;
368
-	}
369
-
370
-	/**
371
-	 * Get the path for the given app on the access
372
-	 * If the app is defined in multiple directories, the first one is taken. (false if not found)
373
-	 *
374
-	 * @param string $appId
375
-	 * @return string|false
376
-	 * @deprecated 18.0.0 use \OC::$server->getAppManager()->getAppWebPath()
377
-	 */
378
-	public static function getAppWebPath(string $appId) {
379
-		if (($dir = self::findAppInDirectories($appId)) != false) {
380
-			return OC::$WEBROOT . $dir['url'] . '/' . $appId;
381
-		}
382
-		return false;
383
-	}
384
-
385
-	/**
386
-	 * get app's version based on it's path
387
-	 *
388
-	 * @param string $path
389
-	 * @return string
390
-	 */
391
-	public static function getAppVersionByPath(string $path): string {
392
-		$infoFile = $path . '/appinfo/info.xml';
393
-		$appData = \OC::$server->getAppManager()->getAppInfo($infoFile, true);
394
-		return isset($appData['version']) ? $appData['version'] : '';
395
-	}
396
-
397
-	/**
398
-	 * get the id of loaded app
399
-	 *
400
-	 * @return string
401
-	 */
402
-	public static function getCurrentApp(): string {
403
-		if (\OC::$CLI) {
404
-			return '';
405
-		}
406
-
407
-		$request = \OC::$server->getRequest();
408
-		$script = substr($request->getScriptName(), strlen(OC::$WEBROOT) + 1);
409
-		$topFolder = substr($script, 0, strpos($script, '/') ?: 0);
410
-		if (empty($topFolder)) {
411
-			try {
412
-				$path_info = $request->getPathInfo();
413
-			} catch (Exception $e) {
414
-				// Can happen from unit tests because the script name is `./vendor/bin/phpunit` or something a like then.
415
-				\OC::$server->get(LoggerInterface::class)->error('Failed to detect current app from script path', ['exception' => $e]);
416
-				return '';
417
-			}
418
-			if ($path_info) {
419
-				$topFolder = substr($path_info, 1, strpos($path_info, '/', 1) - 1);
420
-			}
421
-		}
422
-		if ($topFolder == 'apps') {
423
-			$length = strlen($topFolder);
424
-			return substr($script, $length + 1, strpos($script, '/', $length + 1) - $length - 1) ?: '';
425
-		} else {
426
-			return $topFolder;
427
-		}
428
-	}
429
-
430
-	/**
431
-	 * @param string $type
432
-	 * @return array
433
-	 */
434
-	public static function getForms(string $type): array {
435
-		$forms = [];
436
-		switch ($type) {
437
-			case 'admin':
438
-				$source = self::$adminForms;
439
-				break;
440
-			case 'personal':
441
-				$source = self::$personalForms;
442
-				break;
443
-			default:
444
-				return [];
445
-		}
446
-		foreach ($source as $form) {
447
-			$forms[] = include $form;
448
-		}
449
-		return $forms;
450
-	}
451
-
452
-	/**
453
-	 * @param array $entry
454
-	 * @deprecated 20.0.0 Please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface
455
-	 */
456
-	public static function registerLogIn(array $entry) {
457
-		\OC::$server->getLogger()->debug('OC_App::registerLogIn() is deprecated, please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface');
458
-		self::$altLogin[] = $entry;
459
-	}
460
-
461
-	/**
462
-	 * @return array
463
-	 */
464
-	public static function getAlternativeLogIns(): array {
465
-		/** @var Coordinator $bootstrapCoordinator */
466
-		$bootstrapCoordinator = \OC::$server->query(Coordinator::class);
467
-
468
-		foreach ($bootstrapCoordinator->getRegistrationContext()->getAlternativeLogins() as $registration) {
469
-			if (!in_array(IAlternativeLogin::class, class_implements($registration->getService()), true)) {
470
-				\OC::$server->getLogger()->error('Alternative login option {option} does not implement {interface} and is therefore ignored.', [
471
-					'option' => $registration->getService(),
472
-					'interface' => IAlternativeLogin::class,
473
-					'app' => $registration->getAppId(),
474
-				]);
475
-				continue;
476
-			}
477
-
478
-			try {
479
-				/** @var IAlternativeLogin $provider */
480
-				$provider = \OC::$server->query($registration->getService());
481
-			} catch (QueryException $e) {
482
-				\OC::$server->getLogger()->logException($e, [
483
-					'message' => 'Alternative login option {option} can not be initialised.',
484
-					'option' => $registration->getService(),
485
-					'app' => $registration->getAppId(),
486
-				]);
487
-			}
488
-
489
-			try {
490
-				$provider->load();
491
-
492
-				self::$altLogin[] = [
493
-					'name' => $provider->getLabel(),
494
-					'href' => $provider->getLink(),
495
-					'class' => $provider->getClass(),
496
-				];
497
-			} catch (Throwable $e) {
498
-				\OC::$server->getLogger()->logException($e, [
499
-					'message' => 'Alternative login option {option} had an error while loading.',
500
-					'option' => $registration->getService(),
501
-					'app' => $registration->getAppId(),
502
-				]);
503
-			}
504
-		}
505
-
506
-		return self::$altLogin;
507
-	}
508
-
509
-	/**
510
-	 * get a list of all apps in the apps folder
511
-	 *
512
-	 * @return string[] an array of app names (string IDs)
513
-	 * @todo: change the name of this method to getInstalledApps, which is more accurate
514
-	 */
515
-	public static function getAllApps(): array {
516
-		$apps = [];
517
-
518
-		foreach (OC::$APPSROOTS as $apps_dir) {
519
-			if (!is_readable($apps_dir['path'])) {
520
-				\OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], ILogger::WARN);
521
-				continue;
522
-			}
523
-			$dh = opendir($apps_dir['path']);
524
-
525
-			if (is_resource($dh)) {
526
-				while (($file = readdir($dh)) !== false) {
527
-					if ($file[0] != '.' and is_dir($apps_dir['path'] . '/' . $file) and is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) {
528
-						$apps[] = $file;
529
-					}
530
-				}
531
-			}
532
-		}
533
-
534
-		$apps = array_unique($apps);
535
-
536
-		return $apps;
537
-	}
538
-
539
-	/**
540
-	 * List all supported apps
541
-	 *
542
-	 * @return array
543
-	 */
544
-	public function getSupportedApps(): array {
545
-		/** @var \OCP\Support\Subscription\IRegistry $subscriptionRegistry */
546
-		$subscriptionRegistry = \OC::$server->query(\OCP\Support\Subscription\IRegistry::class);
547
-		$supportedApps = $subscriptionRegistry->delegateGetSupportedApps();
548
-		return $supportedApps;
549
-	}
550
-
551
-	/**
552
-	 * List all apps, this is used in apps.php
553
-	 *
554
-	 * @return array
555
-	 */
556
-	public function listAllApps(): array {
557
-		$installedApps = OC_App::getAllApps();
558
-
559
-		$appManager = \OC::$server->getAppManager();
560
-		//we don't want to show configuration for these
561
-		$blacklist = $appManager->getAlwaysEnabledApps();
562
-		$appList = [];
563
-		$langCode = \OC::$server->getL10N('core')->getLanguageCode();
564
-		$urlGenerator = \OC::$server->getURLGenerator();
565
-		$supportedApps = $this->getSupportedApps();
566
-
567
-		foreach ($installedApps as $app) {
568
-			if (array_search($app, $blacklist) === false) {
569
-				$info = $appManager->getAppInfo($app, false, $langCode);
570
-				if (!is_array($info)) {
571
-					\OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', ILogger::ERROR);
572
-					continue;
573
-				}
574
-
575
-				if (!isset($info['name'])) {
576
-					\OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', ILogger::ERROR);
577
-					continue;
578
-				}
579
-
580
-				$enabled = \OC::$server->getConfig()->getAppValue($app, 'enabled', 'no');
581
-				$info['groups'] = null;
582
-				if ($enabled === 'yes') {
583
-					$active = true;
584
-				} elseif ($enabled === 'no') {
585
-					$active = false;
586
-				} else {
587
-					$active = true;
588
-					$info['groups'] = $enabled;
589
-				}
590
-
591
-				$info['active'] = $active;
592
-
593
-				if ($appManager->isShipped($app)) {
594
-					$info['internal'] = true;
595
-					$info['level'] = self::officialApp;
596
-					$info['removable'] = false;
597
-				} else {
598
-					$info['internal'] = false;
599
-					$info['removable'] = true;
600
-				}
601
-
602
-				if (in_array($app, $supportedApps)) {
603
-					$info['level'] = self::supportedApp;
604
-				}
605
-
606
-				$appPath = self::getAppPath($app);
607
-				if ($appPath !== false) {
608
-					$appIcon = $appPath . '/img/' . $app . '.svg';
609
-					if (file_exists($appIcon)) {
610
-						$info['preview'] = $urlGenerator->imagePath($app, $app . '.svg');
611
-						$info['previewAsIcon'] = true;
612
-					} else {
613
-						$appIcon = $appPath . '/img/app.svg';
614
-						if (file_exists($appIcon)) {
615
-							$info['preview'] = $urlGenerator->imagePath($app, 'app.svg');
616
-							$info['previewAsIcon'] = true;
617
-						}
618
-					}
619
-				}
620
-				// fix documentation
621
-				if (isset($info['documentation']) && is_array($info['documentation'])) {
622
-					foreach ($info['documentation'] as $key => $url) {
623
-						// If it is not an absolute URL we assume it is a key
624
-						// i.e. admin-ldap will get converted to go.php?to=admin-ldap
625
-						if (stripos($url, 'https://') !== 0 && stripos($url, 'http://') !== 0) {
626
-							$url = $urlGenerator->linkToDocs($url);
627
-						}
628
-
629
-						$info['documentation'][$key] = $url;
630
-					}
631
-				}
632
-
633
-				$info['version'] = $appManager->getAppVersion($app);
634
-				$appList[] = $info;
635
-			}
636
-		}
637
-
638
-		return $appList;
639
-	}
640
-
641
-	public static function shouldUpgrade(string $app): bool {
642
-		$versions = self::getAppVersions();
643
-		$currentVersion = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppVersion($app);
644
-		if ($currentVersion && isset($versions[$app])) {
645
-			$installedVersion = $versions[$app];
646
-			if (!version_compare($currentVersion, $installedVersion, '=')) {
647
-				return true;
648
-			}
649
-		}
650
-		return false;
651
-	}
652
-
653
-	/**
654
-	 * Adjust the number of version parts of $version1 to match
655
-	 * the number of version parts of $version2.
656
-	 *
657
-	 * @param string $version1 version to adjust
658
-	 * @param string $version2 version to take the number of parts from
659
-	 * @return string shortened $version1
660
-	 */
661
-	private static function adjustVersionParts(string $version1, string $version2): string {
662
-		$version1 = explode('.', $version1);
663
-		$version2 = explode('.', $version2);
664
-		// reduce $version1 to match the number of parts in $version2
665
-		while (count($version1) > count($version2)) {
666
-			array_pop($version1);
667
-		}
668
-		// if $version1 does not have enough parts, add some
669
-		while (count($version1) < count($version2)) {
670
-			$version1[] = '0';
671
-		}
672
-		return implode('.', $version1);
673
-	}
674
-
675
-	/**
676
-	 * Check whether the current ownCloud version matches the given
677
-	 * application's version requirements.
678
-	 *
679
-	 * The comparison is made based on the number of parts that the
680
-	 * app info version has. For example for ownCloud 6.0.3 if the
681
-	 * app info version is expecting version 6.0, the comparison is
682
-	 * made on the first two parts of the ownCloud version.
683
-	 * This means that it's possible to specify "requiremin" => 6
684
-	 * and "requiremax" => 6 and it will still match ownCloud 6.0.3.
685
-	 *
686
-	 * @param string $ocVersion ownCloud version to check against
687
-	 * @param array $appInfo app info (from xml)
688
-	 *
689
-	 * @return boolean true if compatible, otherwise false
690
-	 */
691
-	public static function isAppCompatible(string $ocVersion, array $appInfo, bool $ignoreMax = false): bool {
692
-		$requireMin = '';
693
-		$requireMax = '';
694
-		if (isset($appInfo['dependencies']['nextcloud']['@attributes']['min-version'])) {
695
-			$requireMin = $appInfo['dependencies']['nextcloud']['@attributes']['min-version'];
696
-		} elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) {
697
-			$requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version'];
698
-		} elseif (isset($appInfo['requiremin'])) {
699
-			$requireMin = $appInfo['requiremin'];
700
-		} elseif (isset($appInfo['require'])) {
701
-			$requireMin = $appInfo['require'];
702
-		}
703
-
704
-		if (isset($appInfo['dependencies']['nextcloud']['@attributes']['max-version'])) {
705
-			$requireMax = $appInfo['dependencies']['nextcloud']['@attributes']['max-version'];
706
-		} elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) {
707
-			$requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version'];
708
-		} elseif (isset($appInfo['requiremax'])) {
709
-			$requireMax = $appInfo['requiremax'];
710
-		}
711
-
712
-		if (!empty($requireMin)
713
-			&& version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<')
714
-		) {
715
-			return false;
716
-		}
717
-
718
-		if (!$ignoreMax && !empty($requireMax)
719
-			&& version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>')
720
-		) {
721
-			return false;
722
-		}
723
-
724
-		return true;
725
-	}
726
-
727
-	/**
728
-	 * get the installed version of all apps
729
-	 */
730
-	public static function getAppVersions() {
731
-		static $versions;
732
-
733
-		if (!$versions) {
734
-			$appConfig = \OC::$server->getAppConfig();
735
-			$versions = $appConfig->getValues(false, 'installed_version');
736
-		}
737
-		return $versions;
738
-	}
739
-
740
-	/**
741
-	 * update the database for the app and call the update script
742
-	 *
743
-	 * @param string $appId
744
-	 * @return bool
745
-	 */
746
-	public static function updateApp(string $appId): bool {
747
-		// for apps distributed with core, we refresh app path in case the downloaded version
748
-		// have been installed in custom apps and not in the default path
749
-		$appPath = self::getAppPath($appId, true);
750
-		if ($appPath === false) {
751
-			return false;
752
-		}
753
-
754
-		if (is_file($appPath . '/appinfo/database.xml')) {
755
-			\OC::$server->getLogger()->error('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
756
-			return false;
757
-		}
758
-
759
-		\OC::$server->getAppManager()->clearAppsCache();
760
-		$l = \OC::$server->getL10N('core');
761
-		$appData = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppInfo($appId, false, $l->getLanguageCode());
762
-
763
-		$ignoreMaxApps = \OC::$server->getConfig()->getSystemValue('app_install_overwrite', []);
764
-		$ignoreMax = in_array($appId, $ignoreMaxApps, true);
765
-		\OC_App::checkAppDependencies(
766
-			\OC::$server->getConfig(),
767
-			$l,
768
-			$appData,
769
-			$ignoreMax
770
-		);
771
-
772
-		self::registerAutoloading($appId, $appPath, true);
773
-		self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
774
-
775
-		$ms = new MigrationService($appId, \OC::$server->get(\OC\DB\Connection::class));
776
-		$ms->migrate();
777
-
778
-		self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
779
-		self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']);
780
-		// update appversion in app manager
781
-		\OC::$server->getAppManager()->clearAppsCache();
782
-		\OC::$server->getAppManager()->getAppVersion($appId, false);
783
-
784
-		self::setupBackgroundJobs($appData['background-jobs']);
785
-
786
-		//set remote/public handlers
787
-		if (array_key_exists('ocsid', $appData)) {
788
-			\OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']);
789
-		} elseif (\OC::$server->getConfig()->getAppValue($appId, 'ocsid', null) !== null) {
790
-			\OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
791
-		}
792
-		foreach ($appData['remote'] as $name => $path) {
793
-			\OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
794
-		}
795
-		foreach ($appData['public'] as $name => $path) {
796
-			\OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
797
-		}
798
-
799
-		self::setAppTypes($appId);
800
-
801
-		$version = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppVersion($appId);
802
-		\OC::$server->getConfig()->setAppValue($appId, 'installed_version', $version);
803
-
804
-		\OC::$server->get(IEventDispatcher::class)->dispatchTyped(new AppUpdateEvent($appId));
805
-		\OC::$server->getEventDispatcher()->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(
806
-			ManagerEvent::EVENT_APP_UPDATE, $appId
807
-		));
808
-
809
-		return true;
810
-	}
811
-
812
-	/**
813
-	 * @param string $appId
814
-	 * @param string[] $steps
815
-	 * @throws \OC\NeedsUpdateException
816
-	 */
817
-	public static function executeRepairSteps(string $appId, array $steps) {
818
-		if (empty($steps)) {
819
-			return;
820
-		}
821
-		// load the app
822
-		self::loadApp($appId);
823
-
824
-		$dispatcher = \OC::$server->get(IEventDispatcher::class);
825
-
826
-		// load the steps
827
-		$r = new Repair([], $dispatcher, \OC::$server->get(LoggerInterface::class));
828
-		foreach ($steps as $step) {
829
-			try {
830
-				$r->addStep($step);
831
-			} catch (Exception $ex) {
832
-				$dispatcher->dispatchTyped(new RepairErrorEvent($ex->getMessage()));
833
-				\OC::$server->getLogger()->logException($ex);
834
-			}
835
-		}
836
-		// run the steps
837
-		$r->run();
838
-	}
839
-
840
-	public static function setupBackgroundJobs(array $jobs) {
841
-		$queue = \OC::$server->getJobList();
842
-		foreach ($jobs as $job) {
843
-			$queue->add($job);
844
-		}
845
-	}
846
-
847
-	/**
848
-	 * @param string $appId
849
-	 * @param string[] $steps
850
-	 */
851
-	private static function setupLiveMigrations(string $appId, array $steps) {
852
-		$queue = \OC::$server->getJobList();
853
-		foreach ($steps as $step) {
854
-			$queue->add('OC\Migration\BackgroundRepair', [
855
-				'app' => $appId,
856
-				'step' => $step]);
857
-		}
858
-	}
859
-
860
-	/**
861
-	 * @param string $appId
862
-	 * @return \OC\Files\View|false
863
-	 */
864
-	public static function getStorage(string $appId) {
865
-		if (\OC::$server->getAppManager()->isEnabledForUser($appId)) { //sanity check
866
-			if (\OC::$server->getUserSession()->isLoggedIn()) {
867
-				$view = new \OC\Files\View('/' . OC_User::getUser());
868
-				if (!$view->file_exists($appId)) {
869
-					$view->mkdir($appId);
870
-				}
871
-				return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId);
872
-			} else {
873
-				\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', ILogger::ERROR);
874
-				return false;
875
-			}
876
-		} else {
877
-			\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', ILogger::ERROR);
878
-			return false;
879
-		}
880
-	}
881
-
882
-	protected static function findBestL10NOption(array $options, string $lang): string {
883
-		// only a single option
884
-		if (isset($options['@value'])) {
885
-			return $options['@value'];
886
-		}
887
-
888
-		$fallback = $similarLangFallback = $englishFallback = false;
889
-
890
-		$lang = strtolower($lang);
891
-		$similarLang = $lang;
892
-		if (strpos($similarLang, '_')) {
893
-			// For "de_DE" we want to find "de" and the other way around
894
-			$similarLang = substr($lang, 0, strpos($lang, '_'));
895
-		}
896
-
897
-		foreach ($options as $option) {
898
-			if (is_array($option)) {
899
-				if ($fallback === false) {
900
-					$fallback = $option['@value'];
901
-				}
902
-
903
-				if (!isset($option['@attributes']['lang'])) {
904
-					continue;
905
-				}
906
-
907
-				$attributeLang = strtolower($option['@attributes']['lang']);
908
-				if ($attributeLang === $lang) {
909
-					return $option['@value'];
910
-				}
911
-
912
-				if ($attributeLang === $similarLang) {
913
-					$similarLangFallback = $option['@value'];
914
-				} elseif (str_starts_with($attributeLang, $similarLang . '_')) {
915
-					if ($similarLangFallback === false) {
916
-						$similarLangFallback = $option['@value'];
917
-					}
918
-				}
919
-			} else {
920
-				$englishFallback = $option;
921
-			}
922
-		}
923
-
924
-		if ($similarLangFallback !== false) {
925
-			return $similarLangFallback;
926
-		} elseif ($englishFallback !== false) {
927
-			return $englishFallback;
928
-		}
929
-		return (string) $fallback;
930
-	}
931
-
932
-	/**
933
-	 * parses the app data array and enhanced the 'description' value
934
-	 *
935
-	 * @param array $data the app data
936
-	 * @param string $lang
937
-	 * @return array improved app data
938
-	 */
939
-	public static function parseAppInfo(array $data, $lang = null): array {
940
-		if ($lang && isset($data['name']) && is_array($data['name'])) {
941
-			$data['name'] = self::findBestL10NOption($data['name'], $lang);
942
-		}
943
-		if ($lang && isset($data['summary']) && is_array($data['summary'])) {
944
-			$data['summary'] = self::findBestL10NOption($data['summary'], $lang);
945
-		}
946
-		if ($lang && isset($data['description']) && is_array($data['description'])) {
947
-			$data['description'] = trim(self::findBestL10NOption($data['description'], $lang));
948
-		} elseif (isset($data['description']) && is_string($data['description'])) {
949
-			$data['description'] = trim($data['description']);
950
-		} else {
951
-			$data['description'] = '';
952
-		}
953
-
954
-		return $data;
955
-	}
956
-
957
-	/**
958
-	 * @param \OCP\IConfig $config
959
-	 * @param \OCP\IL10N $l
960
-	 * @param array $info
961
-	 * @throws \Exception
962
-	 */
963
-	public static function checkAppDependencies(\OCP\IConfig $config, \OCP\IL10N $l, array $info, bool $ignoreMax) {
964
-		$dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l);
965
-		$missing = $dependencyAnalyzer->analyze($info, $ignoreMax);
966
-		if (!empty($missing)) {
967
-			$missingMsg = implode(PHP_EOL, $missing);
968
-			throw new \Exception(
969
-				$l->t('App "%1$s" cannot be installed because the following dependencies are not fulfilled: %2$s',
970
-					[$info['name'], $missingMsg]
971
-				)
972
-			);
973
-		}
974
-	}
76
+    private static $adminForms = [];
77
+    private static $personalForms = [];
78
+    private static $altLogin = [];
79
+    private static $alreadyRegistered = [];
80
+    public const supportedApp = 300;
81
+    public const officialApp = 200;
82
+
83
+    /**
84
+     * clean the appId
85
+     *
86
+     * @psalm-taint-escape file
87
+     * @psalm-taint-escape include
88
+     * @psalm-taint-escape html
89
+     * @psalm-taint-escape has_quotes
90
+     *
91
+     * @param string $app AppId that needs to be cleaned
92
+     * @return string
93
+     */
94
+    public static function cleanAppId(string $app): string {
95
+        return str_replace(['<', '>', '"', "'", '\0', '/', '\\', '..'], '', $app);
96
+    }
97
+
98
+    /**
99
+     * Check if an app is loaded
100
+     *
101
+     * @param string $app
102
+     * @return bool
103
+     * @deprecated 27.0.0 use IAppManager::isAppLoaded
104
+     */
105
+    public static function isAppLoaded(string $app): bool {
106
+        return \OC::$server->get(IAppManager::class)->isAppLoaded($app);
107
+    }
108
+
109
+    /**
110
+     * loads all apps
111
+     *
112
+     * @param string[] $types
113
+     * @return bool
114
+     *
115
+     * This function walks through the ownCloud directory and loads all apps
116
+     * it can find. A directory contains an app if the file /appinfo/info.xml
117
+     * exists.
118
+     *
119
+     * if $types is set to non-empty array, only apps of those types will be loaded
120
+     */
121
+    public static function loadApps(array $types = []): bool {
122
+        if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
123
+            // This should be done before calling this method so that appmanager can be used
124
+            return false;
125
+        }
126
+        return \OC::$server->get(IAppManager::class)->loadApps($types);
127
+    }
128
+
129
+    /**
130
+     * load a single app
131
+     *
132
+     * @param string $app
133
+     * @throws Exception
134
+     * @deprecated 27.0.0 use IAppManager::loadApp
135
+     */
136
+    public static function loadApp(string $app): void {
137
+        \OC::$server->get(IAppManager::class)->loadApp($app);
138
+    }
139
+
140
+    /**
141
+     * @internal
142
+     * @param string $app
143
+     * @param string $path
144
+     * @param bool $force
145
+     */
146
+    public static function registerAutoloading(string $app, string $path, bool $force = false) {
147
+        $key = $app . '-' . $path;
148
+        if (!$force && isset(self::$alreadyRegistered[$key])) {
149
+            return;
150
+        }
151
+
152
+        self::$alreadyRegistered[$key] = true;
153
+
154
+        // Register on PSR-4 composer autoloader
155
+        $appNamespace = \OC\AppFramework\App::buildAppNamespace($app);
156
+        \OC::$server->registerNamespace($app, $appNamespace);
157
+
158
+        if (file_exists($path . '/composer/autoload.php')) {
159
+            require_once $path . '/composer/autoload.php';
160
+        } else {
161
+            \OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true);
162
+        }
163
+
164
+        // Register Test namespace only when testing
165
+        if (defined('PHPUNIT_RUN') || defined('CLI_TEST_RUN')) {
166
+            \OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true);
167
+        }
168
+    }
169
+
170
+    /**
171
+     * check if an app is of a specific type
172
+     *
173
+     * @param string $app
174
+     * @param array $types
175
+     * @return bool
176
+     * @deprecated 27.0.0 use IAppManager::isType
177
+     */
178
+    public static function isType(string $app, array $types): bool {
179
+        return \OC::$server->get(IAppManager::class)->isType($app, $types);
180
+    }
181
+
182
+    /**
183
+     * read app types from info.xml and cache them in the database
184
+     */
185
+    public static function setAppTypes(string $app) {
186
+        $appManager = \OC::$server->getAppManager();
187
+        $appData = $appManager->getAppInfo($app);
188
+        if (!is_array($appData)) {
189
+            return;
190
+        }
191
+
192
+        if (isset($appData['types'])) {
193
+            $appTypes = implode(',', $appData['types']);
194
+        } else {
195
+            $appTypes = '';
196
+            $appData['types'] = [];
197
+        }
198
+
199
+        $config = \OC::$server->getConfig();
200
+        $config->setAppValue($app, 'types', $appTypes);
201
+
202
+        if ($appManager->hasProtectedAppType($appData['types'])) {
203
+            $enabled = $config->getAppValue($app, 'enabled', 'yes');
204
+            if ($enabled !== 'yes' && $enabled !== 'no') {
205
+                $config->setAppValue($app, 'enabled', 'yes');
206
+            }
207
+        }
208
+    }
209
+
210
+    /**
211
+     * Returns apps enabled for the current user.
212
+     *
213
+     * @param bool $forceRefresh whether to refresh the cache
214
+     * @param bool $all whether to return apps for all users, not only the
215
+     * currently logged in one
216
+     * @return string[]
217
+     */
218
+    public static function getEnabledApps(bool $forceRefresh = false, bool $all = false): array {
219
+        if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
220
+            return [];
221
+        }
222
+        // in incognito mode or when logged out, $user will be false,
223
+        // which is also the case during an upgrade
224
+        $appManager = \OC::$server->getAppManager();
225
+        if ($all) {
226
+            $user = null;
227
+        } else {
228
+            $user = \OC::$server->getUserSession()->getUser();
229
+        }
230
+
231
+        if (is_null($user)) {
232
+            $apps = $appManager->getInstalledApps();
233
+        } else {
234
+            $apps = $appManager->getEnabledAppsForUser($user);
235
+        }
236
+        $apps = array_filter($apps, function ($app) {
237
+            return $app !== 'files';//we add this manually
238
+        });
239
+        sort($apps);
240
+        array_unshift($apps, 'files');
241
+        return $apps;
242
+    }
243
+
244
+    /**
245
+     * enables an app
246
+     *
247
+     * @param string $appId
248
+     * @param array $groups (optional) when set, only these groups will have access to the app
249
+     * @throws \Exception
250
+     * @return void
251
+     *
252
+     * This function set an app as enabled in appconfig.
253
+     */
254
+    public function enable(string $appId,
255
+                            array $groups = []) {
256
+        // Check if app is already downloaded
257
+        /** @var Installer $installer */
258
+        $installer = \OC::$server->query(Installer::class);
259
+        $isDownloaded = $installer->isDownloaded($appId);
260
+
261
+        if (!$isDownloaded) {
262
+            $installer->downloadApp($appId);
263
+        }
264
+
265
+        $installer->installApp($appId);
266
+
267
+        $appManager = \OC::$server->getAppManager();
268
+        if ($groups !== []) {
269
+            $groupManager = \OC::$server->getGroupManager();
270
+            $groupsList = [];
271
+            foreach ($groups as $group) {
272
+                $groupItem = $groupManager->get($group);
273
+                if ($groupItem instanceof \OCP\IGroup) {
274
+                    $groupsList[] = $groupManager->get($group);
275
+                }
276
+            }
277
+            $appManager->enableAppForGroups($appId, $groupsList);
278
+        } else {
279
+            $appManager->enableApp($appId);
280
+        }
281
+    }
282
+
283
+    /**
284
+     * Get the path where to install apps
285
+     *
286
+     * @return string|false
287
+     */
288
+    public static function getInstallPath() {
289
+        foreach (OC::$APPSROOTS as $dir) {
290
+            if (isset($dir['writable']) && $dir['writable'] === true) {
291
+                return $dir['path'];
292
+            }
293
+        }
294
+
295
+        \OCP\Util::writeLog('core', 'No application directories are marked as writable.', ILogger::ERROR);
296
+        return null;
297
+    }
298
+
299
+
300
+    /**
301
+     * search for an app in all app-directories
302
+     *
303
+     * @param string $appId
304
+     * @param bool $ignoreCache ignore cache and rebuild it
305
+     * @return false|string
306
+     */
307
+    public static function findAppInDirectories(string $appId, bool $ignoreCache = false) {
308
+        $sanitizedAppId = self::cleanAppId($appId);
309
+        if ($sanitizedAppId !== $appId) {
310
+            return false;
311
+        }
312
+        static $app_dir = [];
313
+
314
+        if (isset($app_dir[$appId]) && !$ignoreCache) {
315
+            return $app_dir[$appId];
316
+        }
317
+
318
+        $possibleApps = [];
319
+        foreach (OC::$APPSROOTS as $dir) {
320
+            if (file_exists($dir['path'] . '/' . $appId)) {
321
+                $possibleApps[] = $dir;
322
+            }
323
+        }
324
+
325
+        if (empty($possibleApps)) {
326
+            return false;
327
+        } elseif (count($possibleApps) === 1) {
328
+            $dir = array_shift($possibleApps);
329
+            $app_dir[$appId] = $dir;
330
+            return $dir;
331
+        } else {
332
+            $versionToLoad = [];
333
+            foreach ($possibleApps as $possibleApp) {
334
+                $version = self::getAppVersionByPath($possibleApp['path'] . '/' . $appId);
335
+                if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) {
336
+                    $versionToLoad = [
337
+                        'dir' => $possibleApp,
338
+                        'version' => $version,
339
+                    ];
340
+                }
341
+            }
342
+            $app_dir[$appId] = $versionToLoad['dir'];
343
+            return $versionToLoad['dir'];
344
+            //TODO - write test
345
+        }
346
+    }
347
+
348
+    /**
349
+     * Get the directory for the given app.
350
+     * If the app is defined in multiple directories, the first one is taken. (false if not found)
351
+     *
352
+     * @psalm-taint-specialize
353
+     *
354
+     * @param string $appId
355
+     * @param bool $refreshAppPath should be set to true only during install/upgrade
356
+     * @return string|false
357
+     * @deprecated 11.0.0 use \OC::$server->getAppManager()->getAppPath()
358
+     */
359
+    public static function getAppPath(string $appId, bool $refreshAppPath = false) {
360
+        if ($appId === null || trim($appId) === '') {
361
+            return false;
362
+        }
363
+
364
+        if (($dir = self::findAppInDirectories($appId, $refreshAppPath)) != false) {
365
+            return $dir['path'] . '/' . $appId;
366
+        }
367
+        return false;
368
+    }
369
+
370
+    /**
371
+     * Get the path for the given app on the access
372
+     * If the app is defined in multiple directories, the first one is taken. (false if not found)
373
+     *
374
+     * @param string $appId
375
+     * @return string|false
376
+     * @deprecated 18.0.0 use \OC::$server->getAppManager()->getAppWebPath()
377
+     */
378
+    public static function getAppWebPath(string $appId) {
379
+        if (($dir = self::findAppInDirectories($appId)) != false) {
380
+            return OC::$WEBROOT . $dir['url'] . '/' . $appId;
381
+        }
382
+        return false;
383
+    }
384
+
385
+    /**
386
+     * get app's version based on it's path
387
+     *
388
+     * @param string $path
389
+     * @return string
390
+     */
391
+    public static function getAppVersionByPath(string $path): string {
392
+        $infoFile = $path . '/appinfo/info.xml';
393
+        $appData = \OC::$server->getAppManager()->getAppInfo($infoFile, true);
394
+        return isset($appData['version']) ? $appData['version'] : '';
395
+    }
396
+
397
+    /**
398
+     * get the id of loaded app
399
+     *
400
+     * @return string
401
+     */
402
+    public static function getCurrentApp(): string {
403
+        if (\OC::$CLI) {
404
+            return '';
405
+        }
406
+
407
+        $request = \OC::$server->getRequest();
408
+        $script = substr($request->getScriptName(), strlen(OC::$WEBROOT) + 1);
409
+        $topFolder = substr($script, 0, strpos($script, '/') ?: 0);
410
+        if (empty($topFolder)) {
411
+            try {
412
+                $path_info = $request->getPathInfo();
413
+            } catch (Exception $e) {
414
+                // Can happen from unit tests because the script name is `./vendor/bin/phpunit` or something a like then.
415
+                \OC::$server->get(LoggerInterface::class)->error('Failed to detect current app from script path', ['exception' => $e]);
416
+                return '';
417
+            }
418
+            if ($path_info) {
419
+                $topFolder = substr($path_info, 1, strpos($path_info, '/', 1) - 1);
420
+            }
421
+        }
422
+        if ($topFolder == 'apps') {
423
+            $length = strlen($topFolder);
424
+            return substr($script, $length + 1, strpos($script, '/', $length + 1) - $length - 1) ?: '';
425
+        } else {
426
+            return $topFolder;
427
+        }
428
+    }
429
+
430
+    /**
431
+     * @param string $type
432
+     * @return array
433
+     */
434
+    public static function getForms(string $type): array {
435
+        $forms = [];
436
+        switch ($type) {
437
+            case 'admin':
438
+                $source = self::$adminForms;
439
+                break;
440
+            case 'personal':
441
+                $source = self::$personalForms;
442
+                break;
443
+            default:
444
+                return [];
445
+        }
446
+        foreach ($source as $form) {
447
+            $forms[] = include $form;
448
+        }
449
+        return $forms;
450
+    }
451
+
452
+    /**
453
+     * @param array $entry
454
+     * @deprecated 20.0.0 Please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface
455
+     */
456
+    public static function registerLogIn(array $entry) {
457
+        \OC::$server->getLogger()->debug('OC_App::registerLogIn() is deprecated, please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface');
458
+        self::$altLogin[] = $entry;
459
+    }
460
+
461
+    /**
462
+     * @return array
463
+     */
464
+    public static function getAlternativeLogIns(): array {
465
+        /** @var Coordinator $bootstrapCoordinator */
466
+        $bootstrapCoordinator = \OC::$server->query(Coordinator::class);
467
+
468
+        foreach ($bootstrapCoordinator->getRegistrationContext()->getAlternativeLogins() as $registration) {
469
+            if (!in_array(IAlternativeLogin::class, class_implements($registration->getService()), true)) {
470
+                \OC::$server->getLogger()->error('Alternative login option {option} does not implement {interface} and is therefore ignored.', [
471
+                    'option' => $registration->getService(),
472
+                    'interface' => IAlternativeLogin::class,
473
+                    'app' => $registration->getAppId(),
474
+                ]);
475
+                continue;
476
+            }
477
+
478
+            try {
479
+                /** @var IAlternativeLogin $provider */
480
+                $provider = \OC::$server->query($registration->getService());
481
+            } catch (QueryException $e) {
482
+                \OC::$server->getLogger()->logException($e, [
483
+                    'message' => 'Alternative login option {option} can not be initialised.',
484
+                    'option' => $registration->getService(),
485
+                    'app' => $registration->getAppId(),
486
+                ]);
487
+            }
488
+
489
+            try {
490
+                $provider->load();
491
+
492
+                self::$altLogin[] = [
493
+                    'name' => $provider->getLabel(),
494
+                    'href' => $provider->getLink(),
495
+                    'class' => $provider->getClass(),
496
+                ];
497
+            } catch (Throwable $e) {
498
+                \OC::$server->getLogger()->logException($e, [
499
+                    'message' => 'Alternative login option {option} had an error while loading.',
500
+                    'option' => $registration->getService(),
501
+                    'app' => $registration->getAppId(),
502
+                ]);
503
+            }
504
+        }
505
+
506
+        return self::$altLogin;
507
+    }
508
+
509
+    /**
510
+     * get a list of all apps in the apps folder
511
+     *
512
+     * @return string[] an array of app names (string IDs)
513
+     * @todo: change the name of this method to getInstalledApps, which is more accurate
514
+     */
515
+    public static function getAllApps(): array {
516
+        $apps = [];
517
+
518
+        foreach (OC::$APPSROOTS as $apps_dir) {
519
+            if (!is_readable($apps_dir['path'])) {
520
+                \OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], ILogger::WARN);
521
+                continue;
522
+            }
523
+            $dh = opendir($apps_dir['path']);
524
+
525
+            if (is_resource($dh)) {
526
+                while (($file = readdir($dh)) !== false) {
527
+                    if ($file[0] != '.' and is_dir($apps_dir['path'] . '/' . $file) and is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) {
528
+                        $apps[] = $file;
529
+                    }
530
+                }
531
+            }
532
+        }
533
+
534
+        $apps = array_unique($apps);
535
+
536
+        return $apps;
537
+    }
538
+
539
+    /**
540
+     * List all supported apps
541
+     *
542
+     * @return array
543
+     */
544
+    public function getSupportedApps(): array {
545
+        /** @var \OCP\Support\Subscription\IRegistry $subscriptionRegistry */
546
+        $subscriptionRegistry = \OC::$server->query(\OCP\Support\Subscription\IRegistry::class);
547
+        $supportedApps = $subscriptionRegistry->delegateGetSupportedApps();
548
+        return $supportedApps;
549
+    }
550
+
551
+    /**
552
+     * List all apps, this is used in apps.php
553
+     *
554
+     * @return array
555
+     */
556
+    public function listAllApps(): array {
557
+        $installedApps = OC_App::getAllApps();
558
+
559
+        $appManager = \OC::$server->getAppManager();
560
+        //we don't want to show configuration for these
561
+        $blacklist = $appManager->getAlwaysEnabledApps();
562
+        $appList = [];
563
+        $langCode = \OC::$server->getL10N('core')->getLanguageCode();
564
+        $urlGenerator = \OC::$server->getURLGenerator();
565
+        $supportedApps = $this->getSupportedApps();
566
+
567
+        foreach ($installedApps as $app) {
568
+            if (array_search($app, $blacklist) === false) {
569
+                $info = $appManager->getAppInfo($app, false, $langCode);
570
+                if (!is_array($info)) {
571
+                    \OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', ILogger::ERROR);
572
+                    continue;
573
+                }
574
+
575
+                if (!isset($info['name'])) {
576
+                    \OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', ILogger::ERROR);
577
+                    continue;
578
+                }
579
+
580
+                $enabled = \OC::$server->getConfig()->getAppValue($app, 'enabled', 'no');
581
+                $info['groups'] = null;
582
+                if ($enabled === 'yes') {
583
+                    $active = true;
584
+                } elseif ($enabled === 'no') {
585
+                    $active = false;
586
+                } else {
587
+                    $active = true;
588
+                    $info['groups'] = $enabled;
589
+                }
590
+
591
+                $info['active'] = $active;
592
+
593
+                if ($appManager->isShipped($app)) {
594
+                    $info['internal'] = true;
595
+                    $info['level'] = self::officialApp;
596
+                    $info['removable'] = false;
597
+                } else {
598
+                    $info['internal'] = false;
599
+                    $info['removable'] = true;
600
+                }
601
+
602
+                if (in_array($app, $supportedApps)) {
603
+                    $info['level'] = self::supportedApp;
604
+                }
605
+
606
+                $appPath = self::getAppPath($app);
607
+                if ($appPath !== false) {
608
+                    $appIcon = $appPath . '/img/' . $app . '.svg';
609
+                    if (file_exists($appIcon)) {
610
+                        $info['preview'] = $urlGenerator->imagePath($app, $app . '.svg');
611
+                        $info['previewAsIcon'] = true;
612
+                    } else {
613
+                        $appIcon = $appPath . '/img/app.svg';
614
+                        if (file_exists($appIcon)) {
615
+                            $info['preview'] = $urlGenerator->imagePath($app, 'app.svg');
616
+                            $info['previewAsIcon'] = true;
617
+                        }
618
+                    }
619
+                }
620
+                // fix documentation
621
+                if (isset($info['documentation']) && is_array($info['documentation'])) {
622
+                    foreach ($info['documentation'] as $key => $url) {
623
+                        // If it is not an absolute URL we assume it is a key
624
+                        // i.e. admin-ldap will get converted to go.php?to=admin-ldap
625
+                        if (stripos($url, 'https://') !== 0 && stripos($url, 'http://') !== 0) {
626
+                            $url = $urlGenerator->linkToDocs($url);
627
+                        }
628
+
629
+                        $info['documentation'][$key] = $url;
630
+                    }
631
+                }
632
+
633
+                $info['version'] = $appManager->getAppVersion($app);
634
+                $appList[] = $info;
635
+            }
636
+        }
637
+
638
+        return $appList;
639
+    }
640
+
641
+    public static function shouldUpgrade(string $app): bool {
642
+        $versions = self::getAppVersions();
643
+        $currentVersion = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppVersion($app);
644
+        if ($currentVersion && isset($versions[$app])) {
645
+            $installedVersion = $versions[$app];
646
+            if (!version_compare($currentVersion, $installedVersion, '=')) {
647
+                return true;
648
+            }
649
+        }
650
+        return false;
651
+    }
652
+
653
+    /**
654
+     * Adjust the number of version parts of $version1 to match
655
+     * the number of version parts of $version2.
656
+     *
657
+     * @param string $version1 version to adjust
658
+     * @param string $version2 version to take the number of parts from
659
+     * @return string shortened $version1
660
+     */
661
+    private static function adjustVersionParts(string $version1, string $version2): string {
662
+        $version1 = explode('.', $version1);
663
+        $version2 = explode('.', $version2);
664
+        // reduce $version1 to match the number of parts in $version2
665
+        while (count($version1) > count($version2)) {
666
+            array_pop($version1);
667
+        }
668
+        // if $version1 does not have enough parts, add some
669
+        while (count($version1) < count($version2)) {
670
+            $version1[] = '0';
671
+        }
672
+        return implode('.', $version1);
673
+    }
674
+
675
+    /**
676
+     * Check whether the current ownCloud version matches the given
677
+     * application's version requirements.
678
+     *
679
+     * The comparison is made based on the number of parts that the
680
+     * app info version has. For example for ownCloud 6.0.3 if the
681
+     * app info version is expecting version 6.0, the comparison is
682
+     * made on the first two parts of the ownCloud version.
683
+     * This means that it's possible to specify "requiremin" => 6
684
+     * and "requiremax" => 6 and it will still match ownCloud 6.0.3.
685
+     *
686
+     * @param string $ocVersion ownCloud version to check against
687
+     * @param array $appInfo app info (from xml)
688
+     *
689
+     * @return boolean true if compatible, otherwise false
690
+     */
691
+    public static function isAppCompatible(string $ocVersion, array $appInfo, bool $ignoreMax = false): bool {
692
+        $requireMin = '';
693
+        $requireMax = '';
694
+        if (isset($appInfo['dependencies']['nextcloud']['@attributes']['min-version'])) {
695
+            $requireMin = $appInfo['dependencies']['nextcloud']['@attributes']['min-version'];
696
+        } elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) {
697
+            $requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version'];
698
+        } elseif (isset($appInfo['requiremin'])) {
699
+            $requireMin = $appInfo['requiremin'];
700
+        } elseif (isset($appInfo['require'])) {
701
+            $requireMin = $appInfo['require'];
702
+        }
703
+
704
+        if (isset($appInfo['dependencies']['nextcloud']['@attributes']['max-version'])) {
705
+            $requireMax = $appInfo['dependencies']['nextcloud']['@attributes']['max-version'];
706
+        } elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) {
707
+            $requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version'];
708
+        } elseif (isset($appInfo['requiremax'])) {
709
+            $requireMax = $appInfo['requiremax'];
710
+        }
711
+
712
+        if (!empty($requireMin)
713
+            && version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<')
714
+        ) {
715
+            return false;
716
+        }
717
+
718
+        if (!$ignoreMax && !empty($requireMax)
719
+            && version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>')
720
+        ) {
721
+            return false;
722
+        }
723
+
724
+        return true;
725
+    }
726
+
727
+    /**
728
+     * get the installed version of all apps
729
+     */
730
+    public static function getAppVersions() {
731
+        static $versions;
732
+
733
+        if (!$versions) {
734
+            $appConfig = \OC::$server->getAppConfig();
735
+            $versions = $appConfig->getValues(false, 'installed_version');
736
+        }
737
+        return $versions;
738
+    }
739
+
740
+    /**
741
+     * update the database for the app and call the update script
742
+     *
743
+     * @param string $appId
744
+     * @return bool
745
+     */
746
+    public static function updateApp(string $appId): bool {
747
+        // for apps distributed with core, we refresh app path in case the downloaded version
748
+        // have been installed in custom apps and not in the default path
749
+        $appPath = self::getAppPath($appId, true);
750
+        if ($appPath === false) {
751
+            return false;
752
+        }
753
+
754
+        if (is_file($appPath . '/appinfo/database.xml')) {
755
+            \OC::$server->getLogger()->error('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
756
+            return false;
757
+        }
758
+
759
+        \OC::$server->getAppManager()->clearAppsCache();
760
+        $l = \OC::$server->getL10N('core');
761
+        $appData = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppInfo($appId, false, $l->getLanguageCode());
762
+
763
+        $ignoreMaxApps = \OC::$server->getConfig()->getSystemValue('app_install_overwrite', []);
764
+        $ignoreMax = in_array($appId, $ignoreMaxApps, true);
765
+        \OC_App::checkAppDependencies(
766
+            \OC::$server->getConfig(),
767
+            $l,
768
+            $appData,
769
+            $ignoreMax
770
+        );
771
+
772
+        self::registerAutoloading($appId, $appPath, true);
773
+        self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
774
+
775
+        $ms = new MigrationService($appId, \OC::$server->get(\OC\DB\Connection::class));
776
+        $ms->migrate();
777
+
778
+        self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
779
+        self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']);
780
+        // update appversion in app manager
781
+        \OC::$server->getAppManager()->clearAppsCache();
782
+        \OC::$server->getAppManager()->getAppVersion($appId, false);
783
+
784
+        self::setupBackgroundJobs($appData['background-jobs']);
785
+
786
+        //set remote/public handlers
787
+        if (array_key_exists('ocsid', $appData)) {
788
+            \OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']);
789
+        } elseif (\OC::$server->getConfig()->getAppValue($appId, 'ocsid', null) !== null) {
790
+            \OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
791
+        }
792
+        foreach ($appData['remote'] as $name => $path) {
793
+            \OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
794
+        }
795
+        foreach ($appData['public'] as $name => $path) {
796
+            \OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
797
+        }
798
+
799
+        self::setAppTypes($appId);
800
+
801
+        $version = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppVersion($appId);
802
+        \OC::$server->getConfig()->setAppValue($appId, 'installed_version', $version);
803
+
804
+        \OC::$server->get(IEventDispatcher::class)->dispatchTyped(new AppUpdateEvent($appId));
805
+        \OC::$server->getEventDispatcher()->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(
806
+            ManagerEvent::EVENT_APP_UPDATE, $appId
807
+        ));
808
+
809
+        return true;
810
+    }
811
+
812
+    /**
813
+     * @param string $appId
814
+     * @param string[] $steps
815
+     * @throws \OC\NeedsUpdateException
816
+     */
817
+    public static function executeRepairSteps(string $appId, array $steps) {
818
+        if (empty($steps)) {
819
+            return;
820
+        }
821
+        // load the app
822
+        self::loadApp($appId);
823
+
824
+        $dispatcher = \OC::$server->get(IEventDispatcher::class);
825
+
826
+        // load the steps
827
+        $r = new Repair([], $dispatcher, \OC::$server->get(LoggerInterface::class));
828
+        foreach ($steps as $step) {
829
+            try {
830
+                $r->addStep($step);
831
+            } catch (Exception $ex) {
832
+                $dispatcher->dispatchTyped(new RepairErrorEvent($ex->getMessage()));
833
+                \OC::$server->getLogger()->logException($ex);
834
+            }
835
+        }
836
+        // run the steps
837
+        $r->run();
838
+    }
839
+
840
+    public static function setupBackgroundJobs(array $jobs) {
841
+        $queue = \OC::$server->getJobList();
842
+        foreach ($jobs as $job) {
843
+            $queue->add($job);
844
+        }
845
+    }
846
+
847
+    /**
848
+     * @param string $appId
849
+     * @param string[] $steps
850
+     */
851
+    private static function setupLiveMigrations(string $appId, array $steps) {
852
+        $queue = \OC::$server->getJobList();
853
+        foreach ($steps as $step) {
854
+            $queue->add('OC\Migration\BackgroundRepair', [
855
+                'app' => $appId,
856
+                'step' => $step]);
857
+        }
858
+    }
859
+
860
+    /**
861
+     * @param string $appId
862
+     * @return \OC\Files\View|false
863
+     */
864
+    public static function getStorage(string $appId) {
865
+        if (\OC::$server->getAppManager()->isEnabledForUser($appId)) { //sanity check
866
+            if (\OC::$server->getUserSession()->isLoggedIn()) {
867
+                $view = new \OC\Files\View('/' . OC_User::getUser());
868
+                if (!$view->file_exists($appId)) {
869
+                    $view->mkdir($appId);
870
+                }
871
+                return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId);
872
+            } else {
873
+                \OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', ILogger::ERROR);
874
+                return false;
875
+            }
876
+        } else {
877
+            \OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', ILogger::ERROR);
878
+            return false;
879
+        }
880
+    }
881
+
882
+    protected static function findBestL10NOption(array $options, string $lang): string {
883
+        // only a single option
884
+        if (isset($options['@value'])) {
885
+            return $options['@value'];
886
+        }
887
+
888
+        $fallback = $similarLangFallback = $englishFallback = false;
889
+
890
+        $lang = strtolower($lang);
891
+        $similarLang = $lang;
892
+        if (strpos($similarLang, '_')) {
893
+            // For "de_DE" we want to find "de" and the other way around
894
+            $similarLang = substr($lang, 0, strpos($lang, '_'));
895
+        }
896
+
897
+        foreach ($options as $option) {
898
+            if (is_array($option)) {
899
+                if ($fallback === false) {
900
+                    $fallback = $option['@value'];
901
+                }
902
+
903
+                if (!isset($option['@attributes']['lang'])) {
904
+                    continue;
905
+                }
906
+
907
+                $attributeLang = strtolower($option['@attributes']['lang']);
908
+                if ($attributeLang === $lang) {
909
+                    return $option['@value'];
910
+                }
911
+
912
+                if ($attributeLang === $similarLang) {
913
+                    $similarLangFallback = $option['@value'];
914
+                } elseif (str_starts_with($attributeLang, $similarLang . '_')) {
915
+                    if ($similarLangFallback === false) {
916
+                        $similarLangFallback = $option['@value'];
917
+                    }
918
+                }
919
+            } else {
920
+                $englishFallback = $option;
921
+            }
922
+        }
923
+
924
+        if ($similarLangFallback !== false) {
925
+            return $similarLangFallback;
926
+        } elseif ($englishFallback !== false) {
927
+            return $englishFallback;
928
+        }
929
+        return (string) $fallback;
930
+    }
931
+
932
+    /**
933
+     * parses the app data array and enhanced the 'description' value
934
+     *
935
+     * @param array $data the app data
936
+     * @param string $lang
937
+     * @return array improved app data
938
+     */
939
+    public static function parseAppInfo(array $data, $lang = null): array {
940
+        if ($lang && isset($data['name']) && is_array($data['name'])) {
941
+            $data['name'] = self::findBestL10NOption($data['name'], $lang);
942
+        }
943
+        if ($lang && isset($data['summary']) && is_array($data['summary'])) {
944
+            $data['summary'] = self::findBestL10NOption($data['summary'], $lang);
945
+        }
946
+        if ($lang && isset($data['description']) && is_array($data['description'])) {
947
+            $data['description'] = trim(self::findBestL10NOption($data['description'], $lang));
948
+        } elseif (isset($data['description']) && is_string($data['description'])) {
949
+            $data['description'] = trim($data['description']);
950
+        } else {
951
+            $data['description'] = '';
952
+        }
953
+
954
+        return $data;
955
+    }
956
+
957
+    /**
958
+     * @param \OCP\IConfig $config
959
+     * @param \OCP\IL10N $l
960
+     * @param array $info
961
+     * @throws \Exception
962
+     */
963
+    public static function checkAppDependencies(\OCP\IConfig $config, \OCP\IL10N $l, array $info, bool $ignoreMax) {
964
+        $dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l);
965
+        $missing = $dependencyAnalyzer->analyze($info, $ignoreMax);
966
+        if (!empty($missing)) {
967
+            $missingMsg = implode(PHP_EOL, $missing);
968
+            throw new \Exception(
969
+                $l->t('App "%1$s" cannot be installed because the following dependencies are not fulfilled: %2$s',
970
+                    [$info['name'], $missingMsg]
971
+                )
972
+            );
973
+        }
974
+    }
975 975
 }
Please login to merge, or discard this patch.
Spacing   +28 added lines, -28 removed lines patch added patch discarded remove patch
@@ -144,7 +144,7 @@  discard block
 block discarded – undo
144 144
 	 * @param bool $force
145 145
 	 */
146 146
 	public static function registerAutoloading(string $app, string $path, bool $force = false) {
147
-		$key = $app . '-' . $path;
147
+		$key = $app.'-'.$path;
148 148
 		if (!$force && isset(self::$alreadyRegistered[$key])) {
149 149
 			return;
150 150
 		}
@@ -155,15 +155,15 @@  discard block
 block discarded – undo
155 155
 		$appNamespace = \OC\AppFramework\App::buildAppNamespace($app);
156 156
 		\OC::$server->registerNamespace($app, $appNamespace);
157 157
 
158
-		if (file_exists($path . '/composer/autoload.php')) {
159
-			require_once $path . '/composer/autoload.php';
158
+		if (file_exists($path.'/composer/autoload.php')) {
159
+			require_once $path.'/composer/autoload.php';
160 160
 		} else {
161
-			\OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true);
161
+			\OC::$composerAutoloader->addPsr4($appNamespace.'\\', $path.'/lib/', true);
162 162
 		}
163 163
 
164 164
 		// Register Test namespace only when testing
165 165
 		if (defined('PHPUNIT_RUN') || defined('CLI_TEST_RUN')) {
166
-			\OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true);
166
+			\OC::$composerAutoloader->addPsr4($appNamespace.'\\Tests\\', $path.'/tests/', true);
167 167
 		}
168 168
 	}
169 169
 
@@ -233,8 +233,8 @@  discard block
 block discarded – undo
233 233
 		} else {
234 234
 			$apps = $appManager->getEnabledAppsForUser($user);
235 235
 		}
236
-		$apps = array_filter($apps, function ($app) {
237
-			return $app !== 'files';//we add this manually
236
+		$apps = array_filter($apps, function($app) {
237
+			return $app !== 'files'; //we add this manually
238 238
 		});
239 239
 		sort($apps);
240 240
 		array_unshift($apps, 'files');
@@ -317,7 +317,7 @@  discard block
 block discarded – undo
317 317
 
318 318
 		$possibleApps = [];
319 319
 		foreach (OC::$APPSROOTS as $dir) {
320
-			if (file_exists($dir['path'] . '/' . $appId)) {
320
+			if (file_exists($dir['path'].'/'.$appId)) {
321 321
 				$possibleApps[] = $dir;
322 322
 			}
323 323
 		}
@@ -331,7 +331,7 @@  discard block
 block discarded – undo
331 331
 		} else {
332 332
 			$versionToLoad = [];
333 333
 			foreach ($possibleApps as $possibleApp) {
334
-				$version = self::getAppVersionByPath($possibleApp['path'] . '/' . $appId);
334
+				$version = self::getAppVersionByPath($possibleApp['path'].'/'.$appId);
335 335
 				if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) {
336 336
 					$versionToLoad = [
337 337
 						'dir' => $possibleApp,
@@ -362,7 +362,7 @@  discard block
 block discarded – undo
362 362
 		}
363 363
 
364 364
 		if (($dir = self::findAppInDirectories($appId, $refreshAppPath)) != false) {
365
-			return $dir['path'] . '/' . $appId;
365
+			return $dir['path'].'/'.$appId;
366 366
 		}
367 367
 		return false;
368 368
 	}
@@ -377,7 +377,7 @@  discard block
 block discarded – undo
377 377
 	 */
378 378
 	public static function getAppWebPath(string $appId) {
379 379
 		if (($dir = self::findAppInDirectories($appId)) != false) {
380
-			return OC::$WEBROOT . $dir['url'] . '/' . $appId;
380
+			return OC::$WEBROOT.$dir['url'].'/'.$appId;
381 381
 		}
382 382
 		return false;
383 383
 	}
@@ -389,7 +389,7 @@  discard block
 block discarded – undo
389 389
 	 * @return string
390 390
 	 */
391 391
 	public static function getAppVersionByPath(string $path): string {
392
-		$infoFile = $path . '/appinfo/info.xml';
392
+		$infoFile = $path.'/appinfo/info.xml';
393 393
 		$appData = \OC::$server->getAppManager()->getAppInfo($infoFile, true);
394 394
 		return isset($appData['version']) ? $appData['version'] : '';
395 395
 	}
@@ -517,14 +517,14 @@  discard block
 block discarded – undo
517 517
 
518 518
 		foreach (OC::$APPSROOTS as $apps_dir) {
519 519
 			if (!is_readable($apps_dir['path'])) {
520
-				\OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], ILogger::WARN);
520
+				\OCP\Util::writeLog('core', 'unable to read app folder : '.$apps_dir['path'], ILogger::WARN);
521 521
 				continue;
522 522
 			}
523 523
 			$dh = opendir($apps_dir['path']);
524 524
 
525 525
 			if (is_resource($dh)) {
526 526
 				while (($file = readdir($dh)) !== false) {
527
-					if ($file[0] != '.' and is_dir($apps_dir['path'] . '/' . $file) and is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) {
527
+					if ($file[0] != '.' and is_dir($apps_dir['path'].'/'.$file) and is_file($apps_dir['path'].'/'.$file.'/appinfo/info.xml')) {
528 528
 						$apps[] = $file;
529 529
 					}
530 530
 				}
@@ -568,12 +568,12 @@  discard block
 block discarded – undo
568 568
 			if (array_search($app, $blacklist) === false) {
569 569
 				$info = $appManager->getAppInfo($app, false, $langCode);
570 570
 				if (!is_array($info)) {
571
-					\OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', ILogger::ERROR);
571
+					\OCP\Util::writeLog('core', 'Could not read app info file for app "'.$app.'"', ILogger::ERROR);
572 572
 					continue;
573 573
 				}
574 574
 
575 575
 				if (!isset($info['name'])) {
576
-					\OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', ILogger::ERROR);
576
+					\OCP\Util::writeLog('core', 'App id "'.$app.'" has no name in appinfo', ILogger::ERROR);
577 577
 					continue;
578 578
 				}
579 579
 
@@ -605,12 +605,12 @@  discard block
 block discarded – undo
605 605
 
606 606
 				$appPath = self::getAppPath($app);
607 607
 				if ($appPath !== false) {
608
-					$appIcon = $appPath . '/img/' . $app . '.svg';
608
+					$appIcon = $appPath.'/img/'.$app.'.svg';
609 609
 					if (file_exists($appIcon)) {
610
-						$info['preview'] = $urlGenerator->imagePath($app, $app . '.svg');
610
+						$info['preview'] = $urlGenerator->imagePath($app, $app.'.svg');
611 611
 						$info['previewAsIcon'] = true;
612 612
 					} else {
613
-						$appIcon = $appPath . '/img/app.svg';
613
+						$appIcon = $appPath.'/img/app.svg';
614 614
 						if (file_exists($appIcon)) {
615 615
 							$info['preview'] = $urlGenerator->imagePath($app, 'app.svg');
616 616
 							$info['previewAsIcon'] = true;
@@ -751,8 +751,8 @@  discard block
 block discarded – undo
751 751
 			return false;
752 752
 		}
753 753
 
754
-		if (is_file($appPath . '/appinfo/database.xml')) {
755
-			\OC::$server->getLogger()->error('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
754
+		if (is_file($appPath.'/appinfo/database.xml')) {
755
+			\OC::$server->getLogger()->error('The appinfo/database.xml file is not longer supported. Used in '.$appId);
756 756
 			return false;
757 757
 		}
758 758
 
@@ -790,10 +790,10 @@  discard block
 block discarded – undo
790 790
 			\OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
791 791
 		}
792 792
 		foreach ($appData['remote'] as $name => $path) {
793
-			\OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
793
+			\OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $appId.'/'.$path);
794 794
 		}
795 795
 		foreach ($appData['public'] as $name => $path) {
796
-			\OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
796
+			\OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $appId.'/'.$path);
797 797
 		}
798 798
 
799 799
 		self::setAppTypes($appId);
@@ -864,17 +864,17 @@  discard block
 block discarded – undo
864 864
 	public static function getStorage(string $appId) {
865 865
 		if (\OC::$server->getAppManager()->isEnabledForUser($appId)) { //sanity check
866 866
 			if (\OC::$server->getUserSession()->isLoggedIn()) {
867
-				$view = new \OC\Files\View('/' . OC_User::getUser());
867
+				$view = new \OC\Files\View('/'.OC_User::getUser());
868 868
 				if (!$view->file_exists($appId)) {
869 869
 					$view->mkdir($appId);
870 870
 				}
871
-				return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId);
871
+				return new \OC\Files\View('/'.OC_User::getUser().'/'.$appId);
872 872
 			} else {
873
-				\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', ILogger::ERROR);
873
+				\OCP\Util::writeLog('core', 'Can\'t get app storage, app '.$appId.', user not logged in', ILogger::ERROR);
874 874
 				return false;
875 875
 			}
876 876
 		} else {
877
-			\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', ILogger::ERROR);
877
+			\OCP\Util::writeLog('core', 'Can\'t get app storage, app '.$appId.' not enabled', ILogger::ERROR);
878 878
 			return false;
879 879
 		}
880 880
 	}
@@ -911,7 +911,7 @@  discard block
 block discarded – undo
911 911
 
912 912
 				if ($attributeLang === $similarLang) {
913 913
 					$similarLangFallback = $option['@value'];
914
-				} elseif (str_starts_with($attributeLang, $similarLang . '_')) {
914
+				} elseif (str_starts_with($attributeLang, $similarLang.'_')) {
915 915
 					if ($similarLangFallback === false) {
916 916
 						$similarLangFallback = $option['@value'];
917 917
 					}
Please login to merge, or discard this patch.
lib/private/Security/TrustedDomainHelper.php 1 patch
Indentation   +72 added lines, -72 removed lines patch added patch discarded remove patch
@@ -34,85 +34,85 @@
 block discarded – undo
34 34
 use OCP\Security\ITrustedDomainHelper;
35 35
 
36 36
 class TrustedDomainHelper implements ITrustedDomainHelper {
37
-	/** @var IConfig */
38
-	private $config;
37
+    /** @var IConfig */
38
+    private $config;
39 39
 
40
-	/**
41
-	 * @param IConfig $config
42
-	 */
43
-	public function __construct(IConfig $config) {
44
-		$this->config = $config;
45
-	}
40
+    /**
41
+     * @param IConfig $config
42
+     */
43
+    public function __construct(IConfig $config) {
44
+        $this->config = $config;
45
+    }
46 46
 
47
-	/**
48
-	 * Strips a potential port from a domain (in format domain:port)
49
-	 * @param string $host
50
-	 * @return string $host without appended port
51
-	 */
52
-	private function getDomainWithoutPort(string $host): string {
53
-		$pos = strrpos($host, ':');
54
-		if ($pos !== false) {
55
-			$port = substr($host, $pos + 1);
56
-			if (is_numeric($port)) {
57
-				$host = substr($host, 0, $pos);
58
-			}
59
-		}
60
-		return $host;
61
-	}
47
+    /**
48
+     * Strips a potential port from a domain (in format domain:port)
49
+     * @param string $host
50
+     * @return string $host without appended port
51
+     */
52
+    private function getDomainWithoutPort(string $host): string {
53
+        $pos = strrpos($host, ':');
54
+        if ($pos !== false) {
55
+            $port = substr($host, $pos + 1);
56
+            if (is_numeric($port)) {
57
+                $host = substr($host, 0, $pos);
58
+            }
59
+        }
60
+        return $host;
61
+    }
62 62
 
63
-	/**
64
-	 * {@inheritDoc}
65
-	 */
66
-	public function isTrustedUrl(string $url): bool {
67
-		$parsedUrl = parse_url($url);
68
-		if (empty($parsedUrl['host'])) {
69
-			return false;
70
-		}
63
+    /**
64
+     * {@inheritDoc}
65
+     */
66
+    public function isTrustedUrl(string $url): bool {
67
+        $parsedUrl = parse_url($url);
68
+        if (empty($parsedUrl['host'])) {
69
+            return false;
70
+        }
71 71
 
72
-		if (isset($parsedUrl['port']) && $parsedUrl['port']) {
73
-			return $this->isTrustedDomain($parsedUrl['host'] . ':' . $parsedUrl['port']);
74
-		}
72
+        if (isset($parsedUrl['port']) && $parsedUrl['port']) {
73
+            return $this->isTrustedDomain($parsedUrl['host'] . ':' . $parsedUrl['port']);
74
+        }
75 75
 
76
-		return $this->isTrustedDomain($parsedUrl['host']);
77
-	}
76
+        return $this->isTrustedDomain($parsedUrl['host']);
77
+    }
78 78
 
79
-	/**
80
-	 * {@inheritDoc}
81
-	 */
82
-	public function isTrustedDomain(string $domainWithPort): bool {
83
-		// overwritehost is always trusted
84
-		if ($this->config->getSystemValue('overwritehost') !== '') {
85
-			return true;
86
-		}
79
+    /**
80
+     * {@inheritDoc}
81
+     */
82
+    public function isTrustedDomain(string $domainWithPort): bool {
83
+        // overwritehost is always trusted
84
+        if ($this->config->getSystemValue('overwritehost') !== '') {
85
+            return true;
86
+        }
87 87
 
88
-		$domain = $this->getDomainWithoutPort($domainWithPort);
88
+        $domain = $this->getDomainWithoutPort($domainWithPort);
89 89
 
90
-		// Read trusted domains from config
91
-		$trustedList = $this->config->getSystemValue('trusted_domains', []);
92
-		if (!is_array($trustedList)) {
93
-			return false;
94
-		}
90
+        // Read trusted domains from config
91
+        $trustedList = $this->config->getSystemValue('trusted_domains', []);
92
+        if (!is_array($trustedList)) {
93
+            return false;
94
+        }
95 95
 
96
-		// Always allow access from localhost
97
-		if (preg_match(Request::REGEX_LOCALHOST, $domain) === 1) {
98
-			return true;
99
-		}
100
-		// Reject malformed domains in any case
101
-		if (str_starts_with($domain, '-') || str_contains($domain, '..')) {
102
-			return false;
103
-		}
104
-		// Match, allowing for * wildcards
105
-		foreach ($trustedList as $trusted) {
106
-			if (gettype($trusted) !== 'string') {
107
-				break;
108
-			}
109
-			$regex = '/^' . implode('[-\.a-zA-Z0-9]*', array_map(function ($v) {
110
-				return preg_quote($v, '/');
111
-			}, explode('*', $trusted))) . '$/i';
112
-			if (preg_match($regex, $domain) || preg_match($regex, $domainWithPort)) {
113
-				return true;
114
-			}
115
-		}
116
-		return false;
117
-	}
96
+        // Always allow access from localhost
97
+        if (preg_match(Request::REGEX_LOCALHOST, $domain) === 1) {
98
+            return true;
99
+        }
100
+        // Reject malformed domains in any case
101
+        if (str_starts_with($domain, '-') || str_contains($domain, '..')) {
102
+            return false;
103
+        }
104
+        // Match, allowing for * wildcards
105
+        foreach ($trustedList as $trusted) {
106
+            if (gettype($trusted) !== 'string') {
107
+                break;
108
+            }
109
+            $regex = '/^' . implode('[-\.a-zA-Z0-9]*', array_map(function ($v) {
110
+                return preg_quote($v, '/');
111
+            }, explode('*', $trusted))) . '$/i';
112
+            if (preg_match($regex, $domain) || preg_match($regex, $domainWithPort)) {
113
+                return true;
114
+            }
115
+        }
116
+        return false;
117
+    }
118 118
 }
Please login to merge, or discard this patch.
lib/private/Security/RemoteHostValidator.php 1 patch
Indentation   +29 added lines, -29 removed lines patch added patch discarded remove patch
@@ -38,38 +38,38 @@
 block discarded – undo
38 38
  * @internal
39 39
  */
40 40
 final class RemoteHostValidator implements IRemoteHostValidator {
41
-	private IConfig $config;
42
-	private HostnameClassifier $hostnameClassifier;
43
-	private IpAddressClassifier $ipAddressClassifier;
44
-	private LoggerInterface $logger;
41
+    private IConfig $config;
42
+    private HostnameClassifier $hostnameClassifier;
43
+    private IpAddressClassifier $ipAddressClassifier;
44
+    private LoggerInterface $logger;
45 45
 
46
-	public function __construct(IConfig $config,
47
-								HostnameClassifier $hostnameClassifier,
48
-								IpAddressClassifier $ipAddressClassifier,
49
-								LoggerInterface $logger) {
50
-		$this->config = $config;
51
-		$this->hostnameClassifier = $hostnameClassifier;
52
-		$this->ipAddressClassifier = $ipAddressClassifier;
53
-		$this->logger = $logger;
54
-	}
46
+    public function __construct(IConfig $config,
47
+                                HostnameClassifier $hostnameClassifier,
48
+                                IpAddressClassifier $ipAddressClassifier,
49
+                                LoggerInterface $logger) {
50
+        $this->config = $config;
51
+        $this->hostnameClassifier = $hostnameClassifier;
52
+        $this->ipAddressClassifier = $ipAddressClassifier;
53
+        $this->logger = $logger;
54
+    }
55 55
 
56
-	public function isValid(string $host): bool {
57
-		if ($this->config->getSystemValueBool('allow_local_remote_servers', false)) {
58
-			return true;
59
-		}
56
+    public function isValid(string $host): bool {
57
+        if ($this->config->getSystemValueBool('allow_local_remote_servers', false)) {
58
+            return true;
59
+        }
60 60
 
61
-		$host = idn_to_utf8(strtolower(urldecode($host)));
62
-		// Remove brackets from IPv6 addresses
63
-		if (str_starts_with($host, '[') && str_ends_with($host, ']')) {
64
-			$host = substr($host, 1, -1);
65
-		}
61
+        $host = idn_to_utf8(strtolower(urldecode($host)));
62
+        // Remove brackets from IPv6 addresses
63
+        if (str_starts_with($host, '[') && str_ends_with($host, ']')) {
64
+            $host = substr($host, 1, -1);
65
+        }
66 66
 
67
-		if ($this->hostnameClassifier->isLocalHostname($host)
68
-			|| $this->ipAddressClassifier->isLocalAddress($host)) {
69
-			$this->logger->warning("Host $host was not connected to because it violates local access rules");
70
-			return false;
71
-		}
67
+        if ($this->hostnameClassifier->isLocalHostname($host)
68
+            || $this->ipAddressClassifier->isLocalAddress($host)) {
69
+            $this->logger->warning("Host $host was not connected to because it violates local access rules");
70
+            return false;
71
+        }
72 72
 
73
-		return true;
74
-	}
73
+        return true;
74
+    }
75 75
 }
Please login to merge, or discard this patch.
lib/private/Security/Bruteforce/Throttler.php 2 patches
Indentation   +308 added lines, -308 removed lines patch added patch discarded remove patch
@@ -54,312 +54,312 @@
 block discarded – undo
54 54
  * @package OC\Security\Bruteforce
55 55
  */
56 56
 class Throttler implements IThrottler {
57
-	public const LOGIN_ACTION = 'login';
58
-
59
-	/** @var IDBConnection */
60
-	private $db;
61
-	/** @var ITimeFactory */
62
-	private $timeFactory;
63
-	private LoggerInterface $logger;
64
-	/** @var IConfig */
65
-	private $config;
66
-	/** @var bool[] */
67
-	private $hasAttemptsDeleted = [];
68
-
69
-	public function __construct(IDBConnection $db,
70
-								ITimeFactory $timeFactory,
71
-								LoggerInterface $logger,
72
-								IConfig $config) {
73
-		$this->db = $db;
74
-		$this->timeFactory = $timeFactory;
75
-		$this->logger = $logger;
76
-		$this->config = $config;
77
-	}
78
-
79
-	/**
80
-	 * Convert a number of seconds into the appropriate DateInterval
81
-	 *
82
-	 * @param int $expire
83
-	 * @return \DateInterval
84
-	 */
85
-	private function getCutoff(int $expire): \DateInterval {
86
-		$d1 = new \DateTime();
87
-		$d2 = clone $d1;
88
-		$d2->sub(new \DateInterval('PT' . $expire . 'S'));
89
-		return $d2->diff($d1);
90
-	}
91
-
92
-	/**
93
-	 *  Calculate the cut off timestamp
94
-	 *
95
-	 * @param float $maxAgeHours
96
-	 * @return int
97
-	 */
98
-	private function getCutoffTimestamp(float $maxAgeHours = 12.0): int {
99
-		return (new \DateTime())
100
-			->sub($this->getCutoff((int) ($maxAgeHours * 3600)))
101
-			->getTimestamp();
102
-	}
103
-
104
-	/**
105
-	 * Register a failed attempt to bruteforce a security control
106
-	 *
107
-	 * @param string $action
108
-	 * @param string $ip
109
-	 * @param array $metadata Optional metadata logged to the database
110
-	 */
111
-	public function registerAttempt(string $action,
112
-									string $ip,
113
-									array $metadata = []): void {
114
-		// No need to log if the bruteforce protection is disabled
115
-		if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) {
116
-			return;
117
-		}
118
-
119
-		$ipAddress = new IpAddress($ip);
120
-		$values = [
121
-			'action' => $action,
122
-			'occurred' => $this->timeFactory->getTime(),
123
-			'ip' => (string)$ipAddress,
124
-			'subnet' => $ipAddress->getSubnet(),
125
-			'metadata' => json_encode($metadata),
126
-		];
127
-
128
-		$this->logger->notice(
129
-			sprintf(
130
-				'Bruteforce attempt from "%s" detected for action "%s".',
131
-				$ip,
132
-				$action
133
-			),
134
-			[
135
-				'app' => 'core',
136
-			]
137
-		);
138
-
139
-		$qb = $this->db->getQueryBuilder();
140
-		$qb->insert('bruteforce_attempts');
141
-		foreach ($values as $column => $value) {
142
-			$qb->setValue($column, $qb->createNamedParameter($value));
143
-		}
144
-		$qb->execute();
145
-	}
146
-
147
-	/**
148
-	 * Check if the IP is whitelisted
149
-	 *
150
-	 * @param string $ip
151
-	 * @return bool
152
-	 */
153
-	private function isIPWhitelisted(string $ip): bool {
154
-		if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) {
155
-			return true;
156
-		}
157
-
158
-		$keys = $this->config->getAppKeys('bruteForce');
159
-		$keys = array_filter($keys, function ($key) {
160
-			return str_starts_with($key, 'whitelist_');
161
-		});
162
-
163
-		if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
164
-			$type = 4;
165
-		} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
166
-			$type = 6;
167
-		} else {
168
-			return false;
169
-		}
170
-
171
-		$ip = inet_pton($ip);
172
-
173
-		foreach ($keys as $key) {
174
-			$cidr = $this->config->getAppValue('bruteForce', $key, null);
175
-
176
-			$cx = explode('/', $cidr);
177
-			$addr = $cx[0];
178
-			$mask = (int)$cx[1];
179
-
180
-			// Do not compare ipv4 to ipv6
181
-			if (($type === 4 && !filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ||
182
-				($type === 6 && !filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) {
183
-				continue;
184
-			}
185
-
186
-			$addr = inet_pton($addr);
187
-
188
-			$valid = true;
189
-			for ($i = 0; $i < $mask; $i++) {
190
-				$part = ord($addr[(int)($i / 8)]);
191
-				$orig = ord($ip[(int)($i / 8)]);
192
-
193
-				$bitmask = 1 << (7 - ($i % 8));
194
-
195
-				$part = $part & $bitmask;
196
-				$orig = $orig & $bitmask;
197
-
198
-				if ($part !== $orig) {
199
-					$valid = false;
200
-					break;
201
-				}
202
-			}
203
-
204
-			if ($valid === true) {
205
-				return true;
206
-			}
207
-		}
208
-
209
-		return false;
210
-	}
211
-
212
-	/**
213
-	 * Get the throttling delay (in milliseconds)
214
-	 *
215
-	 * @param string $ip
216
-	 * @param string $action optionally filter by action
217
-	 * @param float $maxAgeHours
218
-	 * @return int
219
-	 */
220
-	public function getAttempts(string $ip, string $action = '', float $maxAgeHours = 12): int {
221
-		if ($maxAgeHours > 48) {
222
-			$this->logger->error('Bruteforce has to use less than 48 hours');
223
-			$maxAgeHours = 48;
224
-		}
225
-
226
-		if ($ip === '' || isset($this->hasAttemptsDeleted[$action])) {
227
-			return 0;
228
-		}
229
-
230
-		$ipAddress = new IpAddress($ip);
231
-		if ($this->isIPWhitelisted((string)$ipAddress)) {
232
-			return 0;
233
-		}
234
-
235
-		$cutoffTime = $this->getCutoffTimestamp($maxAgeHours);
236
-
237
-		$qb = $this->db->getQueryBuilder();
238
-		$qb->select($qb->func()->count('*', 'attempts'))
239
-			->from('bruteforce_attempts')
240
-			->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
241
-			->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($ipAddress->getSubnet())));
242
-
243
-		if ($action !== '') {
244
-			$qb->andWhere($qb->expr()->eq('action', $qb->createNamedParameter($action)));
245
-		}
246
-
247
-		$result = $qb->execute();
248
-		$row = $result->fetch();
249
-		$result->closeCursor();
250
-
251
-		return (int) $row['attempts'];
252
-	}
253
-
254
-	/**
255
-	 * Get the throttling delay (in milliseconds)
256
-	 *
257
-	 * @param string $ip
258
-	 * @param string $action optionally filter by action
259
-	 * @return int
260
-	 */
261
-	public function getDelay(string $ip, string $action = ''): int {
262
-		$attempts = $this->getAttempts($ip, $action);
263
-		if ($attempts === 0) {
264
-			return 0;
265
-		}
266
-
267
-		$firstDelay = 0.1;
268
-		if ($attempts > self::MAX_ATTEMPTS) {
269
-			// Don't ever overflow. Just assume the maxDelay time:s
270
-			return self::MAX_DELAY_MS;
271
-		}
272
-
273
-		$delay = $firstDelay * 2 ** $attempts;
274
-		if ($delay > self::MAX_DELAY) {
275
-			return self::MAX_DELAY_MS;
276
-		}
277
-		return (int) \ceil($delay * 1000);
278
-	}
279
-
280
-	/**
281
-	 * Reset the throttling delay for an IP address, action and metadata
282
-	 *
283
-	 * @param string $ip
284
-	 * @param string $action
285
-	 * @param array $metadata
286
-	 */
287
-	public function resetDelay(string $ip, string $action, array $metadata): void {
288
-		$ipAddress = new IpAddress($ip);
289
-		if ($this->isIPWhitelisted((string)$ipAddress)) {
290
-			return;
291
-		}
292
-
293
-		$cutoffTime = $this->getCutoffTimestamp();
294
-
295
-		$qb = $this->db->getQueryBuilder();
296
-		$qb->delete('bruteforce_attempts')
297
-			->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
298
-			->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($ipAddress->getSubnet())))
299
-			->andWhere($qb->expr()->eq('action', $qb->createNamedParameter($action)))
300
-			->andWhere($qb->expr()->eq('metadata', $qb->createNamedParameter(json_encode($metadata))));
301
-
302
-		$qb->executeStatement();
303
-
304
-		$this->hasAttemptsDeleted[$action] = true;
305
-	}
306
-
307
-	/**
308
-	 * Reset the throttling delay for an IP address
309
-	 *
310
-	 * @param string $ip
311
-	 */
312
-	public function resetDelayForIP(string $ip): void {
313
-		$cutoffTime = $this->getCutoffTimestamp();
314
-
315
-		$qb = $this->db->getQueryBuilder();
316
-		$qb->delete('bruteforce_attempts')
317
-			->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
318
-			->andWhere($qb->expr()->eq('ip', $qb->createNamedParameter($ip)));
319
-
320
-		$qb->execute();
321
-	}
322
-
323
-	/**
324
-	 * Will sleep for the defined amount of time
325
-	 *
326
-	 * @param string $ip
327
-	 * @param string $action optionally filter by action
328
-	 * @return int the time spent sleeping
329
-	 */
330
-	public function sleepDelay(string $ip, string $action = ''): int {
331
-		$delay = $this->getDelay($ip, $action);
332
-		usleep($delay * 1000);
333
-		return $delay;
334
-	}
335
-
336
-	/**
337
-	 * Will sleep for the defined amount of time unless maximum was reached in the last 30 minutes
338
-	 * In this case a "429 Too Many Request" exception is thrown
339
-	 *
340
-	 * @param string $ip
341
-	 * @param string $action optionally filter by action
342
-	 * @return int the time spent sleeping
343
-	 * @throws MaxDelayReached when reached the maximum
344
-	 */
345
-	public function sleepDelayOrThrowOnMax(string $ip, string $action = ''): int {
346
-		$delay = $this->getDelay($ip, $action);
347
-		if (($delay === self::MAX_DELAY_MS) && $this->getAttempts($ip, $action, 0.5) > self::MAX_ATTEMPTS) {
348
-			$this->logger->info('IP address blocked because it reached the maximum failed attempts in the last 30 minutes [action: {action}, ip: {ip}]', [
349
-				'action' => $action,
350
-				'ip' => $ip,
351
-			]);
352
-			// If the ip made too many attempts within the last 30 mins we don't execute anymore
353
-			throw new MaxDelayReached('Reached maximum delay');
354
-		}
355
-		if ($delay > 100) {
356
-			$this->logger->info('IP address throttled because it reached the attempts limit in the last 30 minutes [action: {action}, delay: {delay}, ip: {ip}]', [
357
-				'action' => $action,
358
-				'ip' => $ip,
359
-				'delay' => $delay,
360
-			]);
361
-		}
362
-		usleep($delay * 1000);
363
-		return $delay;
364
-	}
57
+    public const LOGIN_ACTION = 'login';
58
+
59
+    /** @var IDBConnection */
60
+    private $db;
61
+    /** @var ITimeFactory */
62
+    private $timeFactory;
63
+    private LoggerInterface $logger;
64
+    /** @var IConfig */
65
+    private $config;
66
+    /** @var bool[] */
67
+    private $hasAttemptsDeleted = [];
68
+
69
+    public function __construct(IDBConnection $db,
70
+                                ITimeFactory $timeFactory,
71
+                                LoggerInterface $logger,
72
+                                IConfig $config) {
73
+        $this->db = $db;
74
+        $this->timeFactory = $timeFactory;
75
+        $this->logger = $logger;
76
+        $this->config = $config;
77
+    }
78
+
79
+    /**
80
+     * Convert a number of seconds into the appropriate DateInterval
81
+     *
82
+     * @param int $expire
83
+     * @return \DateInterval
84
+     */
85
+    private function getCutoff(int $expire): \DateInterval {
86
+        $d1 = new \DateTime();
87
+        $d2 = clone $d1;
88
+        $d2->sub(new \DateInterval('PT' . $expire . 'S'));
89
+        return $d2->diff($d1);
90
+    }
91
+
92
+    /**
93
+     *  Calculate the cut off timestamp
94
+     *
95
+     * @param float $maxAgeHours
96
+     * @return int
97
+     */
98
+    private function getCutoffTimestamp(float $maxAgeHours = 12.0): int {
99
+        return (new \DateTime())
100
+            ->sub($this->getCutoff((int) ($maxAgeHours * 3600)))
101
+            ->getTimestamp();
102
+    }
103
+
104
+    /**
105
+     * Register a failed attempt to bruteforce a security control
106
+     *
107
+     * @param string $action
108
+     * @param string $ip
109
+     * @param array $metadata Optional metadata logged to the database
110
+     */
111
+    public function registerAttempt(string $action,
112
+                                    string $ip,
113
+                                    array $metadata = []): void {
114
+        // No need to log if the bruteforce protection is disabled
115
+        if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) {
116
+            return;
117
+        }
118
+
119
+        $ipAddress = new IpAddress($ip);
120
+        $values = [
121
+            'action' => $action,
122
+            'occurred' => $this->timeFactory->getTime(),
123
+            'ip' => (string)$ipAddress,
124
+            'subnet' => $ipAddress->getSubnet(),
125
+            'metadata' => json_encode($metadata),
126
+        ];
127
+
128
+        $this->logger->notice(
129
+            sprintf(
130
+                'Bruteforce attempt from "%s" detected for action "%s".',
131
+                $ip,
132
+                $action
133
+            ),
134
+            [
135
+                'app' => 'core',
136
+            ]
137
+        );
138
+
139
+        $qb = $this->db->getQueryBuilder();
140
+        $qb->insert('bruteforce_attempts');
141
+        foreach ($values as $column => $value) {
142
+            $qb->setValue($column, $qb->createNamedParameter($value));
143
+        }
144
+        $qb->execute();
145
+    }
146
+
147
+    /**
148
+     * Check if the IP is whitelisted
149
+     *
150
+     * @param string $ip
151
+     * @return bool
152
+     */
153
+    private function isIPWhitelisted(string $ip): bool {
154
+        if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) {
155
+            return true;
156
+        }
157
+
158
+        $keys = $this->config->getAppKeys('bruteForce');
159
+        $keys = array_filter($keys, function ($key) {
160
+            return str_starts_with($key, 'whitelist_');
161
+        });
162
+
163
+        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
164
+            $type = 4;
165
+        } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
166
+            $type = 6;
167
+        } else {
168
+            return false;
169
+        }
170
+
171
+        $ip = inet_pton($ip);
172
+
173
+        foreach ($keys as $key) {
174
+            $cidr = $this->config->getAppValue('bruteForce', $key, null);
175
+
176
+            $cx = explode('/', $cidr);
177
+            $addr = $cx[0];
178
+            $mask = (int)$cx[1];
179
+
180
+            // Do not compare ipv4 to ipv6
181
+            if (($type === 4 && !filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ||
182
+                ($type === 6 && !filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) {
183
+                continue;
184
+            }
185
+
186
+            $addr = inet_pton($addr);
187
+
188
+            $valid = true;
189
+            for ($i = 0; $i < $mask; $i++) {
190
+                $part = ord($addr[(int)($i / 8)]);
191
+                $orig = ord($ip[(int)($i / 8)]);
192
+
193
+                $bitmask = 1 << (7 - ($i % 8));
194
+
195
+                $part = $part & $bitmask;
196
+                $orig = $orig & $bitmask;
197
+
198
+                if ($part !== $orig) {
199
+                    $valid = false;
200
+                    break;
201
+                }
202
+            }
203
+
204
+            if ($valid === true) {
205
+                return true;
206
+            }
207
+        }
208
+
209
+        return false;
210
+    }
211
+
212
+    /**
213
+     * Get the throttling delay (in milliseconds)
214
+     *
215
+     * @param string $ip
216
+     * @param string $action optionally filter by action
217
+     * @param float $maxAgeHours
218
+     * @return int
219
+     */
220
+    public function getAttempts(string $ip, string $action = '', float $maxAgeHours = 12): int {
221
+        if ($maxAgeHours > 48) {
222
+            $this->logger->error('Bruteforce has to use less than 48 hours');
223
+            $maxAgeHours = 48;
224
+        }
225
+
226
+        if ($ip === '' || isset($this->hasAttemptsDeleted[$action])) {
227
+            return 0;
228
+        }
229
+
230
+        $ipAddress = new IpAddress($ip);
231
+        if ($this->isIPWhitelisted((string)$ipAddress)) {
232
+            return 0;
233
+        }
234
+
235
+        $cutoffTime = $this->getCutoffTimestamp($maxAgeHours);
236
+
237
+        $qb = $this->db->getQueryBuilder();
238
+        $qb->select($qb->func()->count('*', 'attempts'))
239
+            ->from('bruteforce_attempts')
240
+            ->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
241
+            ->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($ipAddress->getSubnet())));
242
+
243
+        if ($action !== '') {
244
+            $qb->andWhere($qb->expr()->eq('action', $qb->createNamedParameter($action)));
245
+        }
246
+
247
+        $result = $qb->execute();
248
+        $row = $result->fetch();
249
+        $result->closeCursor();
250
+
251
+        return (int) $row['attempts'];
252
+    }
253
+
254
+    /**
255
+     * Get the throttling delay (in milliseconds)
256
+     *
257
+     * @param string $ip
258
+     * @param string $action optionally filter by action
259
+     * @return int
260
+     */
261
+    public function getDelay(string $ip, string $action = ''): int {
262
+        $attempts = $this->getAttempts($ip, $action);
263
+        if ($attempts === 0) {
264
+            return 0;
265
+        }
266
+
267
+        $firstDelay = 0.1;
268
+        if ($attempts > self::MAX_ATTEMPTS) {
269
+            // Don't ever overflow. Just assume the maxDelay time:s
270
+            return self::MAX_DELAY_MS;
271
+        }
272
+
273
+        $delay = $firstDelay * 2 ** $attempts;
274
+        if ($delay > self::MAX_DELAY) {
275
+            return self::MAX_DELAY_MS;
276
+        }
277
+        return (int) \ceil($delay * 1000);
278
+    }
279
+
280
+    /**
281
+     * Reset the throttling delay for an IP address, action and metadata
282
+     *
283
+     * @param string $ip
284
+     * @param string $action
285
+     * @param array $metadata
286
+     */
287
+    public function resetDelay(string $ip, string $action, array $metadata): void {
288
+        $ipAddress = new IpAddress($ip);
289
+        if ($this->isIPWhitelisted((string)$ipAddress)) {
290
+            return;
291
+        }
292
+
293
+        $cutoffTime = $this->getCutoffTimestamp();
294
+
295
+        $qb = $this->db->getQueryBuilder();
296
+        $qb->delete('bruteforce_attempts')
297
+            ->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
298
+            ->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($ipAddress->getSubnet())))
299
+            ->andWhere($qb->expr()->eq('action', $qb->createNamedParameter($action)))
300
+            ->andWhere($qb->expr()->eq('metadata', $qb->createNamedParameter(json_encode($metadata))));
301
+
302
+        $qb->executeStatement();
303
+
304
+        $this->hasAttemptsDeleted[$action] = true;
305
+    }
306
+
307
+    /**
308
+     * Reset the throttling delay for an IP address
309
+     *
310
+     * @param string $ip
311
+     */
312
+    public function resetDelayForIP(string $ip): void {
313
+        $cutoffTime = $this->getCutoffTimestamp();
314
+
315
+        $qb = $this->db->getQueryBuilder();
316
+        $qb->delete('bruteforce_attempts')
317
+            ->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
318
+            ->andWhere($qb->expr()->eq('ip', $qb->createNamedParameter($ip)));
319
+
320
+        $qb->execute();
321
+    }
322
+
323
+    /**
324
+     * Will sleep for the defined amount of time
325
+     *
326
+     * @param string $ip
327
+     * @param string $action optionally filter by action
328
+     * @return int the time spent sleeping
329
+     */
330
+    public function sleepDelay(string $ip, string $action = ''): int {
331
+        $delay = $this->getDelay($ip, $action);
332
+        usleep($delay * 1000);
333
+        return $delay;
334
+    }
335
+
336
+    /**
337
+     * Will sleep for the defined amount of time unless maximum was reached in the last 30 minutes
338
+     * In this case a "429 Too Many Request" exception is thrown
339
+     *
340
+     * @param string $ip
341
+     * @param string $action optionally filter by action
342
+     * @return int the time spent sleeping
343
+     * @throws MaxDelayReached when reached the maximum
344
+     */
345
+    public function sleepDelayOrThrowOnMax(string $ip, string $action = ''): int {
346
+        $delay = $this->getDelay($ip, $action);
347
+        if (($delay === self::MAX_DELAY_MS) && $this->getAttempts($ip, $action, 0.5) > self::MAX_ATTEMPTS) {
348
+            $this->logger->info('IP address blocked because it reached the maximum failed attempts in the last 30 minutes [action: {action}, ip: {ip}]', [
349
+                'action' => $action,
350
+                'ip' => $ip,
351
+            ]);
352
+            // If the ip made too many attempts within the last 30 mins we don't execute anymore
353
+            throw new MaxDelayReached('Reached maximum delay');
354
+        }
355
+        if ($delay > 100) {
356
+            $this->logger->info('IP address throttled because it reached the attempts limit in the last 30 minutes [action: {action}, delay: {delay}, ip: {ip}]', [
357
+                'action' => $action,
358
+                'ip' => $ip,
359
+                'delay' => $delay,
360
+            ]);
361
+        }
362
+        usleep($delay * 1000);
363
+        return $delay;
364
+    }
365 365
 }
Please login to merge, or discard this patch.
Spacing   +8 added lines, -8 removed lines patch added patch discarded remove patch
@@ -85,7 +85,7 @@  discard block
 block discarded – undo
85 85
 	private function getCutoff(int $expire): \DateInterval {
86 86
 		$d1 = new \DateTime();
87 87
 		$d2 = clone $d1;
88
-		$d2->sub(new \DateInterval('PT' . $expire . 'S'));
88
+		$d2->sub(new \DateInterval('PT'.$expire.'S'));
89 89
 		return $d2->diff($d1);
90 90
 	}
91 91
 
@@ -120,7 +120,7 @@  discard block
 block discarded – undo
120 120
 		$values = [
121 121
 			'action' => $action,
122 122
 			'occurred' => $this->timeFactory->getTime(),
123
-			'ip' => (string)$ipAddress,
123
+			'ip' => (string) $ipAddress,
124 124
 			'subnet' => $ipAddress->getSubnet(),
125 125
 			'metadata' => json_encode($metadata),
126 126
 		];
@@ -156,7 +156,7 @@  discard block
 block discarded – undo
156 156
 		}
157 157
 
158 158
 		$keys = $this->config->getAppKeys('bruteForce');
159
-		$keys = array_filter($keys, function ($key) {
159
+		$keys = array_filter($keys, function($key) {
160 160
 			return str_starts_with($key, 'whitelist_');
161 161
 		});
162 162
 
@@ -175,7 +175,7 @@  discard block
 block discarded – undo
175 175
 
176 176
 			$cx = explode('/', $cidr);
177 177
 			$addr = $cx[0];
178
-			$mask = (int)$cx[1];
178
+			$mask = (int) $cx[1];
179 179
 
180 180
 			// Do not compare ipv4 to ipv6
181 181
 			if (($type === 4 && !filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ||
@@ -187,8 +187,8 @@  discard block
 block discarded – undo
187 187
 
188 188
 			$valid = true;
189 189
 			for ($i = 0; $i < $mask; $i++) {
190
-				$part = ord($addr[(int)($i / 8)]);
191
-				$orig = ord($ip[(int)($i / 8)]);
190
+				$part = ord($addr[(int) ($i / 8)]);
191
+				$orig = ord($ip[(int) ($i / 8)]);
192 192
 
193 193
 				$bitmask = 1 << (7 - ($i % 8));
194 194
 
@@ -228,7 +228,7 @@  discard block
 block discarded – undo
228 228
 		}
229 229
 
230 230
 		$ipAddress = new IpAddress($ip);
231
-		if ($this->isIPWhitelisted((string)$ipAddress)) {
231
+		if ($this->isIPWhitelisted((string) $ipAddress)) {
232 232
 			return 0;
233 233
 		}
234 234
 
@@ -286,7 +286,7 @@  discard block
 block discarded – undo
286 286
 	 */
287 287
 	public function resetDelay(string $ip, string $action, array $metadata): void {
288 288
 		$ipAddress = new IpAddress($ip);
289
-		if ($this->isIPWhitelisted((string)$ipAddress)) {
289
+		if ($this->isIPWhitelisted((string) $ipAddress)) {
290 290
 			return;
291 291
 		}
292 292
 
Please login to merge, or discard this patch.
lib/private/Accounts/AccountManager.php 1 patch
Indentation   +764 added lines, -764 removed lines patch added patch discarded remove patch
@@ -79,768 +79,768 @@
 block discarded – undo
79 79
  * @package OC\Accounts
80 80
  */
81 81
 class AccountManager implements IAccountManager {
82
-	use TAccountsHelper;
83
-
84
-	use TProfileHelper;
85
-
86
-	/** @var  IDBConnection database connection */
87
-	private $connection;
88
-
89
-	/** @var IConfig */
90
-	private $config;
91
-
92
-	/** @var string table name */
93
-	private $table = 'accounts';
94
-
95
-	/** @var string table name */
96
-	private $dataTable = 'accounts_data';
97
-
98
-	/** @var EventDispatcherInterface */
99
-	private $eventDispatcher;
100
-
101
-	/** @var IJobList */
102
-	private $jobList;
103
-
104
-	/** @var LoggerInterface */
105
-	private $logger;
106
-	/** @var IVerificationToken */
107
-	private $verificationToken;
108
-	/** @var IMailer */
109
-	private $mailer;
110
-	/** @var Defaults */
111
-	private $defaults;
112
-	/** @var IL10N */
113
-	private $l10n;
114
-	/** @var IURLGenerator */
115
-	private $urlGenerator;
116
-	/** @var ICrypto */
117
-	private $crypto;
118
-	/** @var IFactory */
119
-	private $l10nfactory;
120
-	private CappedMemoryCache $internalCache;
121
-
122
-	/**
123
-	 * The list of default scopes for each property.
124
-	 */
125
-	public const DEFAULT_SCOPES = [
126
-		self::PROPERTY_DISPLAYNAME => self::SCOPE_FEDERATED,
127
-		self::PROPERTY_ADDRESS => self::SCOPE_LOCAL,
128
-		self::PROPERTY_WEBSITE => self::SCOPE_LOCAL,
129
-		self::PROPERTY_EMAIL => self::SCOPE_FEDERATED,
130
-		self::PROPERTY_AVATAR => self::SCOPE_FEDERATED,
131
-		self::PROPERTY_PHONE => self::SCOPE_LOCAL,
132
-		self::PROPERTY_TWITTER => self::SCOPE_LOCAL,
133
-		self::PROPERTY_FEDIVERSE => self::SCOPE_LOCAL,
134
-		self::PROPERTY_ORGANISATION => self::SCOPE_LOCAL,
135
-		self::PROPERTY_ROLE => self::SCOPE_LOCAL,
136
-		self::PROPERTY_HEADLINE => self::SCOPE_LOCAL,
137
-		self::PROPERTY_BIOGRAPHY => self::SCOPE_LOCAL,
138
-	];
139
-
140
-	public function __construct(
141
-		IDBConnection            $connection,
142
-		IConfig                  $config,
143
-		EventDispatcherInterface $eventDispatcher,
144
-		IJobList                 $jobList,
145
-		LoggerInterface          $logger,
146
-		IVerificationToken       $verificationToken,
147
-		IMailer                  $mailer,
148
-		Defaults                 $defaults,
149
-		IFactory                 $factory,
150
-		IURLGenerator            $urlGenerator,
151
-		ICrypto                  $crypto
152
-	) {
153
-		$this->connection = $connection;
154
-		$this->config = $config;
155
-		$this->eventDispatcher = $eventDispatcher;
156
-		$this->jobList = $jobList;
157
-		$this->logger = $logger;
158
-		$this->verificationToken = $verificationToken;
159
-		$this->mailer = $mailer;
160
-		$this->defaults = $defaults;
161
-		$this->urlGenerator = $urlGenerator;
162
-		$this->crypto = $crypto;
163
-		// DIing IL10N results in a dependency loop
164
-		$this->l10nfactory = $factory;
165
-		$this->internalCache = new CappedMemoryCache();
166
-	}
167
-
168
-	/**
169
-	 * @param string $input
170
-	 * @return string Provided phone number in E.164 format when it was a valid number
171
-	 * @throws InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code
172
-	 */
173
-	protected function parsePhoneNumber(string $input): string {
174
-		$defaultRegion = $this->config->getSystemValueString('default_phone_region', '');
175
-
176
-		if ($defaultRegion === '') {
177
-			// When no default region is set, only +49… numbers are valid
178
-			if (!str_starts_with($input, '+')) {
179
-				throw new InvalidArgumentException(self::PROPERTY_PHONE);
180
-			}
181
-
182
-			$defaultRegion = 'EN';
183
-		}
184
-
185
-		$phoneUtil = PhoneNumberUtil::getInstance();
186
-		try {
187
-			$phoneNumber = $phoneUtil->parse($input, $defaultRegion);
188
-			if ($phoneUtil->isValidNumber($phoneNumber)) {
189
-				return $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164);
190
-			}
191
-		} catch (NumberParseException $e) {
192
-		}
193
-
194
-		throw new InvalidArgumentException(self::PROPERTY_PHONE);
195
-	}
196
-
197
-	/**
198
-	 *
199
-	 * @param string $input
200
-	 * @return string
201
-	 * @throws InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty
202
-	 */
203
-	protected function parseWebsite(string $input): string {
204
-		$parts = parse_url($input);
205
-		if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) {
206
-			throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
207
-		}
208
-
209
-		if (!isset($parts['host']) || $parts['host'] === '') {
210
-			throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
211
-		}
212
-
213
-		return $input;
214
-	}
215
-
216
-	/**
217
-	 * @param IAccountProperty[] $properties
218
-	 */
219
-	protected function testValueLengths(array $properties, bool $throwOnData = false): void {
220
-		foreach ($properties as $property) {
221
-			if (strlen($property->getValue()) > 2048) {
222
-				if ($throwOnData) {
223
-					throw new InvalidArgumentException($property->getName());
224
-				} else {
225
-					$property->setValue('');
226
-				}
227
-			}
228
-		}
229
-	}
230
-
231
-	protected function testPropertyScope(IAccountProperty $property, array $allowedScopes, bool $throwOnData): void {
232
-		if ($throwOnData && !in_array($property->getScope(), $allowedScopes, true)) {
233
-			throw new InvalidArgumentException('scope');
234
-		}
235
-
236
-		if (
237
-			$property->getScope() === self::SCOPE_PRIVATE
238
-			&& in_array($property->getName(), [self::PROPERTY_DISPLAYNAME, self::PROPERTY_EMAIL])
239
-		) {
240
-			if ($throwOnData) {
241
-				// v2-private is not available for these fields
242
-				throw new InvalidArgumentException('scope');
243
-			} else {
244
-				// default to local
245
-				$property->setScope(self::SCOPE_LOCAL);
246
-			}
247
-		} else {
248
-			// migrate scope values to the new format
249
-			// invalid scopes are mapped to a default value
250
-			$property->setScope(AccountProperty::mapScopeToV2($property->getScope()));
251
-		}
252
-	}
253
-
254
-	protected function sanitizePhoneNumberValue(IAccountProperty $property, bool $throwOnData = false) {
255
-		if ($property->getName() !== self::PROPERTY_PHONE) {
256
-			if ($throwOnData) {
257
-				throw new InvalidArgumentException(sprintf('sanitizePhoneNumberValue can only sanitize phone numbers, %s given', $property->getName()));
258
-			}
259
-			return;
260
-		}
261
-		if ($property->getValue() === '') {
262
-			return;
263
-		}
264
-		try {
265
-			$property->setValue($this->parsePhoneNumber($property->getValue()));
266
-		} catch (InvalidArgumentException $e) {
267
-			if ($throwOnData) {
268
-				throw $e;
269
-			}
270
-			$property->setValue('');
271
-		}
272
-	}
273
-
274
-	protected function sanitizeWebsite(IAccountProperty $property, bool $throwOnData = false) {
275
-		if ($property->getName() !== self::PROPERTY_WEBSITE) {
276
-			if ($throwOnData) {
277
-				throw new InvalidArgumentException(sprintf('sanitizeWebsite can only sanitize web domains, %s given', $property->getName()));
278
-			}
279
-		}
280
-		try {
281
-			$property->setValue($this->parseWebsite($property->getValue()));
282
-		} catch (InvalidArgumentException $e) {
283
-			if ($throwOnData) {
284
-				throw $e;
285
-			}
286
-			$property->setValue('');
287
-		}
288
-	}
289
-
290
-	protected function updateUser(IUser $user, array $data, ?array $oldUserData, bool $throwOnData = false): array {
291
-		if ($oldUserData === null) {
292
-			$oldUserData = $this->getUser($user, false);
293
-		}
294
-
295
-		$updated = true;
296
-
297
-		if ($oldUserData !== $data) {
298
-			$this->updateExistingUser($user, $data, $oldUserData);
299
-		} else {
300
-			// nothing needs to be done if new and old data set are the same
301
-			$updated = false;
302
-		}
303
-
304
-		if ($updated) {
305
-			$this->eventDispatcher->dispatch(
306
-				'OC\AccountManager::userUpdated',
307
-				new GenericEvent($user, $data)
308
-			);
309
-		}
310
-
311
-		return $data;
312
-	}
313
-
314
-	/**
315
-	 * delete user from accounts table
316
-	 *
317
-	 * @param IUser $user
318
-	 */
319
-	public function deleteUser(IUser $user) {
320
-		$uid = $user->getUID();
321
-		$query = $this->connection->getQueryBuilder();
322
-		$query->delete($this->table)
323
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
324
-			->execute();
325
-
326
-		$this->deleteUserData($user);
327
-	}
328
-
329
-	/**
330
-	 * delete user from accounts table
331
-	 *
332
-	 * @param IUser $user
333
-	 */
334
-	public function deleteUserData(IUser $user): void {
335
-		$uid = $user->getUID();
336
-		$query = $this->connection->getQueryBuilder();
337
-		$query->delete($this->dataTable)
338
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
339
-			->execute();
340
-	}
341
-
342
-	/**
343
-	 * get stored data from a given user
344
-	 */
345
-	protected function getUser(IUser $user, bool $insertIfNotExists = true): array {
346
-		$uid = $user->getUID();
347
-		$query = $this->connection->getQueryBuilder();
348
-		$query->select('data')
349
-			->from($this->table)
350
-			->where($query->expr()->eq('uid', $query->createParameter('uid')))
351
-			->setParameter('uid', $uid);
352
-		$result = $query->executeQuery();
353
-		$accountData = $result->fetchAll();
354
-		$result->closeCursor();
355
-
356
-		if (empty($accountData)) {
357
-			$userData = $this->buildDefaultUserRecord($user);
358
-			if ($insertIfNotExists) {
359
-				$this->insertNewUser($user, $userData);
360
-			}
361
-			return $userData;
362
-		}
363
-
364
-		$userDataArray = $this->importFromJson($accountData[0]['data'], $uid);
365
-		if ($userDataArray === null || $userDataArray === []) {
366
-			return $this->buildDefaultUserRecord($user);
367
-		}
368
-
369
-		return $this->addMissingDefaultValues($userDataArray, $this->buildDefaultUserRecord($user));
370
-	}
371
-
372
-	public function searchUsers(string $property, array $values): array {
373
-		// the value col is limited to 255 bytes. It is used for searches only.
374
-		$values = array_map(function (string $value) {
375
-			return Util::shortenMultibyteString($value, 255);
376
-		}, $values);
377
-		$chunks = array_chunk($values, 500);
378
-		$query = $this->connection->getQueryBuilder();
379
-		$query->select('*')
380
-			->from($this->dataTable)
381
-			->where($query->expr()->eq('name', $query->createNamedParameter($property)))
382
-			->andWhere($query->expr()->in('value', $query->createParameter('values')));
383
-
384
-		$matches = [];
385
-		foreach ($chunks as $chunk) {
386
-			$query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
387
-			$result = $query->executeQuery();
388
-
389
-			while ($row = $result->fetch()) {
390
-				$matches[$row['uid']] = $row['value'];
391
-			}
392
-			$result->closeCursor();
393
-		}
394
-
395
-		$result = array_merge($matches, $this->searchUsersForRelatedCollection($property, $values));
396
-
397
-		return array_flip($result);
398
-	}
399
-
400
-	protected function searchUsersForRelatedCollection(string $property, array $values): array {
401
-		switch ($property) {
402
-			case IAccountManager::PROPERTY_EMAIL:
403
-				return array_flip($this->searchUsers(IAccountManager::COLLECTION_EMAIL, $values));
404
-			default:
405
-				return [];
406
-		}
407
-	}
408
-
409
-	/**
410
-	 * check if we need to ask the server for email verification, if yes we create a cronjob
411
-	 */
412
-	protected function checkEmailVerification(IAccount $updatedAccount, array $oldData): void {
413
-		try {
414
-			$property = $updatedAccount->getProperty(self::PROPERTY_EMAIL);
415
-		} catch (PropertyDoesNotExistException $e) {
416
-			return;
417
-		}
418
-
419
-		$oldMailIndex = array_search(self::PROPERTY_EMAIL, array_column($oldData, 'name'), true);
420
-		$oldMail = $oldMailIndex !== false ? $oldData[$oldMailIndex]['value'] : '';
421
-
422
-		if ($oldMail !== $property->getValue()) {
423
-			$this->jobList->add(
424
-				VerifyUserData::class,
425
-				[
426
-					'verificationCode' => '',
427
-					'data' => $property->getValue(),
428
-					'type' => self::PROPERTY_EMAIL,
429
-					'uid' => $updatedAccount->getUser()->getUID(),
430
-					'try' => 0,
431
-					'lastRun' => time()
432
-				]
433
-			);
434
-
435
-			$property->setVerified(self::VERIFICATION_IN_PROGRESS);
436
-		}
437
-	}
438
-
439
-	protected function checkLocalEmailVerification(IAccount $updatedAccount, array $oldData): void {
440
-		$mailCollection = $updatedAccount->getPropertyCollection(self::COLLECTION_EMAIL);
441
-		foreach ($mailCollection->getProperties() as $property) {
442
-			if ($property->getLocallyVerified() !== self::NOT_VERIFIED) {
443
-				continue;
444
-			}
445
-			if ($this->sendEmailVerificationEmail($updatedAccount->getUser(), $property->getValue())) {
446
-				$property->setLocallyVerified(self::VERIFICATION_IN_PROGRESS);
447
-			}
448
-		}
449
-	}
450
-
451
-	protected function sendEmailVerificationEmail(IUser $user, string $email): bool {
452
-		$ref = \substr(hash('sha256', $email), 0, 8);
453
-		$key = $this->crypto->encrypt($email);
454
-		$token = $this->verificationToken->create($user, 'verifyMail' . $ref, $email);
455
-
456
-		$link = $this->urlGenerator->linkToRouteAbsolute(
457
-			'provisioning_api.Verification.verifyMail',
458
-			[
459
-				'userId' => $user->getUID(),
460
-				'token' => $token,
461
-				'key' => $key
462
-			]
463
-		);
464
-
465
-		$emailTemplate = $this->mailer->createEMailTemplate('core.EmailVerification', [
466
-			'link' => $link,
467
-		]);
468
-
469
-		if (!$this->l10n) {
470
-			$this->l10n = $this->l10nfactory->get('core');
471
-		}
472
-
473
-		$emailTemplate->setSubject($this->l10n->t('%s email verification', [$this->defaults->getName()]));
474
-		$emailTemplate->addHeader();
475
-		$emailTemplate->addHeading($this->l10n->t('Email verification'));
476
-
477
-		$emailTemplate->addBodyText(
478
-			htmlspecialchars($this->l10n->t('Click the following button to confirm your email.')),
479
-			$this->l10n->t('Click the following link to confirm your email.')
480
-		);
481
-
482
-		$emailTemplate->addBodyButton(
483
-			htmlspecialchars($this->l10n->t('Confirm your email')),
484
-			$link,
485
-			false
486
-		);
487
-		$emailTemplate->addFooter();
488
-
489
-		try {
490
-			$message = $this->mailer->createMessage();
491
-			$message->setTo([$email => $user->getDisplayName()]);
492
-			$message->setFrom([Util::getDefaultEmailAddress('verification-noreply') => $this->defaults->getName()]);
493
-			$message->useTemplate($emailTemplate);
494
-			$this->mailer->send($message);
495
-		} catch (Exception $e) {
496
-			// Log the exception and continue
497
-			$this->logger->info('Failed to send verification mail', [
498
-				'app' => 'core',
499
-				'exception' => $e
500
-			]);
501
-			return false;
502
-		}
503
-		return true;
504
-	}
505
-
506
-	/**
507
-	 * Make sure that all expected data are set
508
-	 */
509
-	protected function addMissingDefaultValues(array $userData, array $defaultUserData): array {
510
-		foreach ($defaultUserData as $defaultDataItem) {
511
-			// If property does not exist, initialize it
512
-			$userDataIndex = array_search($defaultDataItem['name'], array_column($userData, 'name'));
513
-			if ($userDataIndex === false) {
514
-				$userData[] = $defaultDataItem;
515
-				continue;
516
-			}
517
-
518
-			// Merge and extend default missing values
519
-			$userData[$userDataIndex] = array_merge($defaultDataItem, $userData[$userDataIndex]);
520
-		}
521
-
522
-		return $userData;
523
-	}
524
-
525
-	protected function updateVerificationStatus(IAccount $updatedAccount, array $oldData): void {
526
-		static $propertiesVerifiableByLookupServer = [
527
-			self::PROPERTY_TWITTER,
528
-			self::PROPERTY_FEDIVERSE,
529
-			self::PROPERTY_WEBSITE,
530
-			self::PROPERTY_EMAIL,
531
-		];
532
-
533
-		foreach ($propertiesVerifiableByLookupServer as $propertyName) {
534
-			try {
535
-				$property = $updatedAccount->getProperty($propertyName);
536
-			} catch (PropertyDoesNotExistException $e) {
537
-				continue;
538
-			}
539
-			$wasVerified = isset($oldData[$propertyName])
540
-				&& isset($oldData[$propertyName]['verified'])
541
-				&& $oldData[$propertyName]['verified'] === self::VERIFIED;
542
-			if ((!isset($oldData[$propertyName])
543
-					|| !isset($oldData[$propertyName]['value'])
544
-					|| $property->getValue() !== $oldData[$propertyName]['value'])
545
-				&& ($property->getVerified() !== self::NOT_VERIFIED
546
-					|| $wasVerified)
547
-			) {
548
-				$property->setVerified(self::NOT_VERIFIED);
549
-			}
550
-		}
551
-	}
552
-
553
-	/**
554
-	 * add new user to accounts table
555
-	 *
556
-	 * @param IUser $user
557
-	 * @param array $data
558
-	 */
559
-	protected function insertNewUser(IUser $user, array $data): void {
560
-		$uid = $user->getUID();
561
-		$jsonEncodedData = $this->prepareJson($data);
562
-		$query = $this->connection->getQueryBuilder();
563
-		$query->insert($this->table)
564
-			->values(
565
-				[
566
-					'uid' => $query->createNamedParameter($uid),
567
-					'data' => $query->createNamedParameter($jsonEncodedData),
568
-				]
569
-			)
570
-			->executeStatement();
571
-
572
-		$this->deleteUserData($user);
573
-		$this->writeUserData($user, $data);
574
-	}
575
-
576
-	protected function prepareJson(array $data): string {
577
-		$preparedData = [];
578
-		foreach ($data as $dataRow) {
579
-			$propertyName = $dataRow['name'];
580
-			unset($dataRow['name']);
581
-
582
-			if (isset($dataRow['locallyVerified']) && $dataRow['locallyVerified'] === self::NOT_VERIFIED) {
583
-				// do not write default value, save DB space
584
-				unset($dataRow['locallyVerified']);
585
-			}
586
-
587
-			if (!$this->isCollection($propertyName)) {
588
-				$preparedData[$propertyName] = $dataRow;
589
-				continue;
590
-			}
591
-			if (!isset($preparedData[$propertyName])) {
592
-				$preparedData[$propertyName] = [];
593
-			}
594
-			$preparedData[$propertyName][] = $dataRow;
595
-		}
596
-		return json_encode($preparedData);
597
-	}
598
-
599
-	protected function importFromJson(string $json, string $userId): ?array {
600
-		$result = [];
601
-		$jsonArray = json_decode($json, true);
602
-		$jsonError = json_last_error();
603
-		if ($jsonError !== JSON_ERROR_NONE) {
604
-			$this->logger->critical(
605
-				'User data of {uid} contained invalid JSON (error {json_error}), hence falling back to a default user record',
606
-				[
607
-					'uid' => $userId,
608
-					'json_error' => $jsonError
609
-				]
610
-			);
611
-			return null;
612
-		}
613
-		foreach ($jsonArray as $propertyName => $row) {
614
-			if (!$this->isCollection($propertyName)) {
615
-				$result[] = array_merge($row, ['name' => $propertyName]);
616
-				continue;
617
-			}
618
-			foreach ($row as $singleRow) {
619
-				$result[] = array_merge($singleRow, ['name' => $propertyName]);
620
-			}
621
-		}
622
-		return $result;
623
-	}
624
-
625
-	/**
626
-	 * Update existing user in accounts table
627
-	 */
628
-	protected function updateExistingUser(IUser $user, array $data, array $oldData): void {
629
-		$uid = $user->getUID();
630
-		$jsonEncodedData = $this->prepareJson($data);
631
-		$query = $this->connection->getQueryBuilder();
632
-		$query->update($this->table)
633
-			->set('data', $query->createNamedParameter($jsonEncodedData))
634
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
635
-			->executeStatement();
636
-
637
-		$this->deleteUserData($user);
638
-		$this->writeUserData($user, $data);
639
-	}
640
-
641
-	protected function writeUserData(IUser $user, array $data): void {
642
-		$query = $this->connection->getQueryBuilder();
643
-		$query->insert($this->dataTable)
644
-			->values(
645
-				[
646
-					'uid' => $query->createNamedParameter($user->getUID()),
647
-					'name' => $query->createParameter('name'),
648
-					'value' => $query->createParameter('value'),
649
-				]
650
-			);
651
-		$this->writeUserDataProperties($query, $data);
652
-	}
653
-
654
-	protected function writeUserDataProperties(IQueryBuilder $query, array $data): void {
655
-		foreach ($data as $property) {
656
-			if ($property['name'] === self::PROPERTY_AVATAR) {
657
-				continue;
658
-			}
659
-
660
-			// the value col is limited to 255 bytes. It is used for searches only.
661
-			$value = $property['value'] ? Util::shortenMultibyteString($property['value'], 255) : '';
662
-
663
-			$query->setParameter('name', $property['name'])
664
-				->setParameter('value', $value);
665
-			$query->executeStatement();
666
-		}
667
-	}
668
-
669
-	/**
670
-	 * build default user record in case not data set exists yet
671
-	 */
672
-	protected function buildDefaultUserRecord(IUser $user): array {
673
-		$scopes = array_merge(self::DEFAULT_SCOPES, array_filter($this->config->getSystemValue('account_manager.default_property_scope', []), static function (string $scope, string $property) {
674
-			return in_array($property, self::ALLOWED_PROPERTIES, true) && in_array($scope, self::ALLOWED_SCOPES, true);
675
-		}, ARRAY_FILTER_USE_BOTH));
676
-
677
-		return [
678
-			[
679
-				'name' => self::PROPERTY_DISPLAYNAME,
680
-				'value' => $user->getDisplayName(),
681
-				// Display name must be at least SCOPE_LOCAL
682
-				'scope' => $scopes[self::PROPERTY_DISPLAYNAME] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_DISPLAYNAME],
683
-				'verified' => self::NOT_VERIFIED,
684
-			],
685
-
686
-			[
687
-				'name' => self::PROPERTY_ADDRESS,
688
-				'value' => '',
689
-				'scope' => $scopes[self::PROPERTY_ADDRESS],
690
-				'verified' => self::NOT_VERIFIED,
691
-			],
692
-
693
-			[
694
-				'name' => self::PROPERTY_WEBSITE,
695
-				'value' => '',
696
-				'scope' => $scopes[self::PROPERTY_WEBSITE],
697
-				'verified' => self::NOT_VERIFIED,
698
-			],
699
-
700
-			[
701
-				'name' => self::PROPERTY_EMAIL,
702
-				'value' => $user->getEMailAddress(),
703
-				// Email must be at least SCOPE_LOCAL
704
-				'scope' => $scopes[self::PROPERTY_EMAIL] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_EMAIL],
705
-				'verified' => self::NOT_VERIFIED,
706
-			],
707
-
708
-			[
709
-				'name' => self::PROPERTY_AVATAR,
710
-				'scope' => $scopes[self::PROPERTY_AVATAR],
711
-			],
712
-
713
-			[
714
-				'name' => self::PROPERTY_PHONE,
715
-				'value' => '',
716
-				'scope' => $scopes[self::PROPERTY_PHONE],
717
-				'verified' => self::NOT_VERIFIED,
718
-			],
719
-
720
-			[
721
-				'name' => self::PROPERTY_TWITTER,
722
-				'value' => '',
723
-				'scope' => $scopes[self::PROPERTY_TWITTER],
724
-				'verified' => self::NOT_VERIFIED,
725
-			],
726
-
727
-			[
728
-				'name' => self::PROPERTY_FEDIVERSE,
729
-				'value' => '',
730
-				'scope' => $scopes[self::PROPERTY_FEDIVERSE],
731
-				'verified' => self::NOT_VERIFIED,
732
-			],
733
-
734
-			[
735
-				'name' => self::PROPERTY_ORGANISATION,
736
-				'value' => '',
737
-				'scope' => $scopes[self::PROPERTY_ORGANISATION],
738
-			],
739
-
740
-			[
741
-				'name' => self::PROPERTY_ROLE,
742
-				'value' => '',
743
-				'scope' => $scopes[self::PROPERTY_ROLE],
744
-			],
745
-
746
-			[
747
-				'name' => self::PROPERTY_HEADLINE,
748
-				'value' => '',
749
-				'scope' => $scopes[self::PROPERTY_HEADLINE],
750
-			],
751
-
752
-			[
753
-				'name' => self::PROPERTY_BIOGRAPHY,
754
-				'value' => '',
755
-				'scope' => $scopes[self::PROPERTY_BIOGRAPHY],
756
-			],
757
-
758
-			[
759
-				'name' => self::PROPERTY_PROFILE_ENABLED,
760
-				'value' => $this->isProfileEnabledByDefault($this->config) ? '1' : '0',
761
-			],
762
-		];
763
-	}
764
-
765
-	private function arrayDataToCollection(IAccount $account, array $data): IAccountPropertyCollection {
766
-		$collection = $account->getPropertyCollection($data['name']);
767
-
768
-		$p = new AccountProperty(
769
-			$data['name'],
770
-			$data['value'] ?? '',
771
-			$data['scope'] ?? self::SCOPE_LOCAL,
772
-			$data['verified'] ?? self::NOT_VERIFIED,
773
-			''
774
-		);
775
-		$p->setLocallyVerified($data['locallyVerified'] ?? self::NOT_VERIFIED);
776
-		$collection->addProperty($p);
777
-
778
-		return $collection;
779
-	}
780
-
781
-	private function parseAccountData(IUser $user, $data): Account {
782
-		$account = new Account($user);
783
-		foreach ($data as $accountData) {
784
-			if ($this->isCollection($accountData['name'])) {
785
-				$account->setPropertyCollection($this->arrayDataToCollection($account, $accountData));
786
-			} else {
787
-				$account->setProperty($accountData['name'], $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
788
-				if (isset($accountData['locallyVerified'])) {
789
-					$property = $account->getProperty($accountData['name']);
790
-					$property->setLocallyVerified($accountData['locallyVerified']);
791
-				}
792
-			}
793
-		}
794
-		return $account;
795
-	}
796
-
797
-	public function getAccount(IUser $user): IAccount {
798
-		if ($this->internalCache->hasKey($user->getUID())) {
799
-			return $this->internalCache->get($user->getUID());
800
-		}
801
-		$account = $this->parseAccountData($user, $this->getUser($user));
802
-		$this->internalCache->set($user->getUID(), $account);
803
-		return $account;
804
-	}
805
-
806
-	public function updateAccount(IAccount $account): void {
807
-		$this->testValueLengths(iterator_to_array($account->getAllProperties()), true);
808
-		try {
809
-			$property = $account->getProperty(self::PROPERTY_PHONE);
810
-			$this->sanitizePhoneNumberValue($property);
811
-		} catch (PropertyDoesNotExistException $e) {
812
-			//  valid case, nothing to do
813
-		}
814
-
815
-		try {
816
-			$property = $account->getProperty(self::PROPERTY_WEBSITE);
817
-			$this->sanitizeWebsite($property);
818
-		} catch (PropertyDoesNotExistException $e) {
819
-			//  valid case, nothing to do
820
-		}
821
-
822
-		foreach ($account->getAllProperties() as $property) {
823
-			$this->testPropertyScope($property, self::ALLOWED_SCOPES, true);
824
-		}
825
-
826
-		$oldData = $this->getUser($account->getUser(), false);
827
-		$this->updateVerificationStatus($account, $oldData);
828
-		$this->checkEmailVerification($account, $oldData);
829
-		$this->checkLocalEmailVerification($account, $oldData);
830
-
831
-		$data = [];
832
-		foreach ($account->getAllProperties() as $property) {
833
-			/** @var IAccountProperty $property */
834
-			$data[] = [
835
-				'name' => $property->getName(),
836
-				'value' => $property->getValue(),
837
-				'scope' => $property->getScope(),
838
-				'verified' => $property->getVerified(),
839
-				'locallyVerified' => $property->getLocallyVerified(),
840
-			];
841
-		}
842
-
843
-		$this->updateUser($account->getUser(), $data, $oldData, true);
844
-		$this->internalCache->set($account->getUser()->getUID(), $account);
845
-	}
82
+    use TAccountsHelper;
83
+
84
+    use TProfileHelper;
85
+
86
+    /** @var  IDBConnection database connection */
87
+    private $connection;
88
+
89
+    /** @var IConfig */
90
+    private $config;
91
+
92
+    /** @var string table name */
93
+    private $table = 'accounts';
94
+
95
+    /** @var string table name */
96
+    private $dataTable = 'accounts_data';
97
+
98
+    /** @var EventDispatcherInterface */
99
+    private $eventDispatcher;
100
+
101
+    /** @var IJobList */
102
+    private $jobList;
103
+
104
+    /** @var LoggerInterface */
105
+    private $logger;
106
+    /** @var IVerificationToken */
107
+    private $verificationToken;
108
+    /** @var IMailer */
109
+    private $mailer;
110
+    /** @var Defaults */
111
+    private $defaults;
112
+    /** @var IL10N */
113
+    private $l10n;
114
+    /** @var IURLGenerator */
115
+    private $urlGenerator;
116
+    /** @var ICrypto */
117
+    private $crypto;
118
+    /** @var IFactory */
119
+    private $l10nfactory;
120
+    private CappedMemoryCache $internalCache;
121
+
122
+    /**
123
+     * The list of default scopes for each property.
124
+     */
125
+    public const DEFAULT_SCOPES = [
126
+        self::PROPERTY_DISPLAYNAME => self::SCOPE_FEDERATED,
127
+        self::PROPERTY_ADDRESS => self::SCOPE_LOCAL,
128
+        self::PROPERTY_WEBSITE => self::SCOPE_LOCAL,
129
+        self::PROPERTY_EMAIL => self::SCOPE_FEDERATED,
130
+        self::PROPERTY_AVATAR => self::SCOPE_FEDERATED,
131
+        self::PROPERTY_PHONE => self::SCOPE_LOCAL,
132
+        self::PROPERTY_TWITTER => self::SCOPE_LOCAL,
133
+        self::PROPERTY_FEDIVERSE => self::SCOPE_LOCAL,
134
+        self::PROPERTY_ORGANISATION => self::SCOPE_LOCAL,
135
+        self::PROPERTY_ROLE => self::SCOPE_LOCAL,
136
+        self::PROPERTY_HEADLINE => self::SCOPE_LOCAL,
137
+        self::PROPERTY_BIOGRAPHY => self::SCOPE_LOCAL,
138
+    ];
139
+
140
+    public function __construct(
141
+        IDBConnection            $connection,
142
+        IConfig                  $config,
143
+        EventDispatcherInterface $eventDispatcher,
144
+        IJobList                 $jobList,
145
+        LoggerInterface          $logger,
146
+        IVerificationToken       $verificationToken,
147
+        IMailer                  $mailer,
148
+        Defaults                 $defaults,
149
+        IFactory                 $factory,
150
+        IURLGenerator            $urlGenerator,
151
+        ICrypto                  $crypto
152
+    ) {
153
+        $this->connection = $connection;
154
+        $this->config = $config;
155
+        $this->eventDispatcher = $eventDispatcher;
156
+        $this->jobList = $jobList;
157
+        $this->logger = $logger;
158
+        $this->verificationToken = $verificationToken;
159
+        $this->mailer = $mailer;
160
+        $this->defaults = $defaults;
161
+        $this->urlGenerator = $urlGenerator;
162
+        $this->crypto = $crypto;
163
+        // DIing IL10N results in a dependency loop
164
+        $this->l10nfactory = $factory;
165
+        $this->internalCache = new CappedMemoryCache();
166
+    }
167
+
168
+    /**
169
+     * @param string $input
170
+     * @return string Provided phone number in E.164 format when it was a valid number
171
+     * @throws InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code
172
+     */
173
+    protected function parsePhoneNumber(string $input): string {
174
+        $defaultRegion = $this->config->getSystemValueString('default_phone_region', '');
175
+
176
+        if ($defaultRegion === '') {
177
+            // When no default region is set, only +49… numbers are valid
178
+            if (!str_starts_with($input, '+')) {
179
+                throw new InvalidArgumentException(self::PROPERTY_PHONE);
180
+            }
181
+
182
+            $defaultRegion = 'EN';
183
+        }
184
+
185
+        $phoneUtil = PhoneNumberUtil::getInstance();
186
+        try {
187
+            $phoneNumber = $phoneUtil->parse($input, $defaultRegion);
188
+            if ($phoneUtil->isValidNumber($phoneNumber)) {
189
+                return $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164);
190
+            }
191
+        } catch (NumberParseException $e) {
192
+        }
193
+
194
+        throw new InvalidArgumentException(self::PROPERTY_PHONE);
195
+    }
196
+
197
+    /**
198
+     *
199
+     * @param string $input
200
+     * @return string
201
+     * @throws InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty
202
+     */
203
+    protected function parseWebsite(string $input): string {
204
+        $parts = parse_url($input);
205
+        if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) {
206
+            throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
207
+        }
208
+
209
+        if (!isset($parts['host']) || $parts['host'] === '') {
210
+            throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
211
+        }
212
+
213
+        return $input;
214
+    }
215
+
216
+    /**
217
+     * @param IAccountProperty[] $properties
218
+     */
219
+    protected function testValueLengths(array $properties, bool $throwOnData = false): void {
220
+        foreach ($properties as $property) {
221
+            if (strlen($property->getValue()) > 2048) {
222
+                if ($throwOnData) {
223
+                    throw new InvalidArgumentException($property->getName());
224
+                } else {
225
+                    $property->setValue('');
226
+                }
227
+            }
228
+        }
229
+    }
230
+
231
+    protected function testPropertyScope(IAccountProperty $property, array $allowedScopes, bool $throwOnData): void {
232
+        if ($throwOnData && !in_array($property->getScope(), $allowedScopes, true)) {
233
+            throw new InvalidArgumentException('scope');
234
+        }
235
+
236
+        if (
237
+            $property->getScope() === self::SCOPE_PRIVATE
238
+            && in_array($property->getName(), [self::PROPERTY_DISPLAYNAME, self::PROPERTY_EMAIL])
239
+        ) {
240
+            if ($throwOnData) {
241
+                // v2-private is not available for these fields
242
+                throw new InvalidArgumentException('scope');
243
+            } else {
244
+                // default to local
245
+                $property->setScope(self::SCOPE_LOCAL);
246
+            }
247
+        } else {
248
+            // migrate scope values to the new format
249
+            // invalid scopes are mapped to a default value
250
+            $property->setScope(AccountProperty::mapScopeToV2($property->getScope()));
251
+        }
252
+    }
253
+
254
+    protected function sanitizePhoneNumberValue(IAccountProperty $property, bool $throwOnData = false) {
255
+        if ($property->getName() !== self::PROPERTY_PHONE) {
256
+            if ($throwOnData) {
257
+                throw new InvalidArgumentException(sprintf('sanitizePhoneNumberValue can only sanitize phone numbers, %s given', $property->getName()));
258
+            }
259
+            return;
260
+        }
261
+        if ($property->getValue() === '') {
262
+            return;
263
+        }
264
+        try {
265
+            $property->setValue($this->parsePhoneNumber($property->getValue()));
266
+        } catch (InvalidArgumentException $e) {
267
+            if ($throwOnData) {
268
+                throw $e;
269
+            }
270
+            $property->setValue('');
271
+        }
272
+    }
273
+
274
+    protected function sanitizeWebsite(IAccountProperty $property, bool $throwOnData = false) {
275
+        if ($property->getName() !== self::PROPERTY_WEBSITE) {
276
+            if ($throwOnData) {
277
+                throw new InvalidArgumentException(sprintf('sanitizeWebsite can only sanitize web domains, %s given', $property->getName()));
278
+            }
279
+        }
280
+        try {
281
+            $property->setValue($this->parseWebsite($property->getValue()));
282
+        } catch (InvalidArgumentException $e) {
283
+            if ($throwOnData) {
284
+                throw $e;
285
+            }
286
+            $property->setValue('');
287
+        }
288
+    }
289
+
290
+    protected function updateUser(IUser $user, array $data, ?array $oldUserData, bool $throwOnData = false): array {
291
+        if ($oldUserData === null) {
292
+            $oldUserData = $this->getUser($user, false);
293
+        }
294
+
295
+        $updated = true;
296
+
297
+        if ($oldUserData !== $data) {
298
+            $this->updateExistingUser($user, $data, $oldUserData);
299
+        } else {
300
+            // nothing needs to be done if new and old data set are the same
301
+            $updated = false;
302
+        }
303
+
304
+        if ($updated) {
305
+            $this->eventDispatcher->dispatch(
306
+                'OC\AccountManager::userUpdated',
307
+                new GenericEvent($user, $data)
308
+            );
309
+        }
310
+
311
+        return $data;
312
+    }
313
+
314
+    /**
315
+     * delete user from accounts table
316
+     *
317
+     * @param IUser $user
318
+     */
319
+    public function deleteUser(IUser $user) {
320
+        $uid = $user->getUID();
321
+        $query = $this->connection->getQueryBuilder();
322
+        $query->delete($this->table)
323
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
324
+            ->execute();
325
+
326
+        $this->deleteUserData($user);
327
+    }
328
+
329
+    /**
330
+     * delete user from accounts table
331
+     *
332
+     * @param IUser $user
333
+     */
334
+    public function deleteUserData(IUser $user): void {
335
+        $uid = $user->getUID();
336
+        $query = $this->connection->getQueryBuilder();
337
+        $query->delete($this->dataTable)
338
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
339
+            ->execute();
340
+    }
341
+
342
+    /**
343
+     * get stored data from a given user
344
+     */
345
+    protected function getUser(IUser $user, bool $insertIfNotExists = true): array {
346
+        $uid = $user->getUID();
347
+        $query = $this->connection->getQueryBuilder();
348
+        $query->select('data')
349
+            ->from($this->table)
350
+            ->where($query->expr()->eq('uid', $query->createParameter('uid')))
351
+            ->setParameter('uid', $uid);
352
+        $result = $query->executeQuery();
353
+        $accountData = $result->fetchAll();
354
+        $result->closeCursor();
355
+
356
+        if (empty($accountData)) {
357
+            $userData = $this->buildDefaultUserRecord($user);
358
+            if ($insertIfNotExists) {
359
+                $this->insertNewUser($user, $userData);
360
+            }
361
+            return $userData;
362
+        }
363
+
364
+        $userDataArray = $this->importFromJson($accountData[0]['data'], $uid);
365
+        if ($userDataArray === null || $userDataArray === []) {
366
+            return $this->buildDefaultUserRecord($user);
367
+        }
368
+
369
+        return $this->addMissingDefaultValues($userDataArray, $this->buildDefaultUserRecord($user));
370
+    }
371
+
372
+    public function searchUsers(string $property, array $values): array {
373
+        // the value col is limited to 255 bytes. It is used for searches only.
374
+        $values = array_map(function (string $value) {
375
+            return Util::shortenMultibyteString($value, 255);
376
+        }, $values);
377
+        $chunks = array_chunk($values, 500);
378
+        $query = $this->connection->getQueryBuilder();
379
+        $query->select('*')
380
+            ->from($this->dataTable)
381
+            ->where($query->expr()->eq('name', $query->createNamedParameter($property)))
382
+            ->andWhere($query->expr()->in('value', $query->createParameter('values')));
383
+
384
+        $matches = [];
385
+        foreach ($chunks as $chunk) {
386
+            $query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
387
+            $result = $query->executeQuery();
388
+
389
+            while ($row = $result->fetch()) {
390
+                $matches[$row['uid']] = $row['value'];
391
+            }
392
+            $result->closeCursor();
393
+        }
394
+
395
+        $result = array_merge($matches, $this->searchUsersForRelatedCollection($property, $values));
396
+
397
+        return array_flip($result);
398
+    }
399
+
400
+    protected function searchUsersForRelatedCollection(string $property, array $values): array {
401
+        switch ($property) {
402
+            case IAccountManager::PROPERTY_EMAIL:
403
+                return array_flip($this->searchUsers(IAccountManager::COLLECTION_EMAIL, $values));
404
+            default:
405
+                return [];
406
+        }
407
+    }
408
+
409
+    /**
410
+     * check if we need to ask the server for email verification, if yes we create a cronjob
411
+     */
412
+    protected function checkEmailVerification(IAccount $updatedAccount, array $oldData): void {
413
+        try {
414
+            $property = $updatedAccount->getProperty(self::PROPERTY_EMAIL);
415
+        } catch (PropertyDoesNotExistException $e) {
416
+            return;
417
+        }
418
+
419
+        $oldMailIndex = array_search(self::PROPERTY_EMAIL, array_column($oldData, 'name'), true);
420
+        $oldMail = $oldMailIndex !== false ? $oldData[$oldMailIndex]['value'] : '';
421
+
422
+        if ($oldMail !== $property->getValue()) {
423
+            $this->jobList->add(
424
+                VerifyUserData::class,
425
+                [
426
+                    'verificationCode' => '',
427
+                    'data' => $property->getValue(),
428
+                    'type' => self::PROPERTY_EMAIL,
429
+                    'uid' => $updatedAccount->getUser()->getUID(),
430
+                    'try' => 0,
431
+                    'lastRun' => time()
432
+                ]
433
+            );
434
+
435
+            $property->setVerified(self::VERIFICATION_IN_PROGRESS);
436
+        }
437
+    }
438
+
439
+    protected function checkLocalEmailVerification(IAccount $updatedAccount, array $oldData): void {
440
+        $mailCollection = $updatedAccount->getPropertyCollection(self::COLLECTION_EMAIL);
441
+        foreach ($mailCollection->getProperties() as $property) {
442
+            if ($property->getLocallyVerified() !== self::NOT_VERIFIED) {
443
+                continue;
444
+            }
445
+            if ($this->sendEmailVerificationEmail($updatedAccount->getUser(), $property->getValue())) {
446
+                $property->setLocallyVerified(self::VERIFICATION_IN_PROGRESS);
447
+            }
448
+        }
449
+    }
450
+
451
+    protected function sendEmailVerificationEmail(IUser $user, string $email): bool {
452
+        $ref = \substr(hash('sha256', $email), 0, 8);
453
+        $key = $this->crypto->encrypt($email);
454
+        $token = $this->verificationToken->create($user, 'verifyMail' . $ref, $email);
455
+
456
+        $link = $this->urlGenerator->linkToRouteAbsolute(
457
+            'provisioning_api.Verification.verifyMail',
458
+            [
459
+                'userId' => $user->getUID(),
460
+                'token' => $token,
461
+                'key' => $key
462
+            ]
463
+        );
464
+
465
+        $emailTemplate = $this->mailer->createEMailTemplate('core.EmailVerification', [
466
+            'link' => $link,
467
+        ]);
468
+
469
+        if (!$this->l10n) {
470
+            $this->l10n = $this->l10nfactory->get('core');
471
+        }
472
+
473
+        $emailTemplate->setSubject($this->l10n->t('%s email verification', [$this->defaults->getName()]));
474
+        $emailTemplate->addHeader();
475
+        $emailTemplate->addHeading($this->l10n->t('Email verification'));
476
+
477
+        $emailTemplate->addBodyText(
478
+            htmlspecialchars($this->l10n->t('Click the following button to confirm your email.')),
479
+            $this->l10n->t('Click the following link to confirm your email.')
480
+        );
481
+
482
+        $emailTemplate->addBodyButton(
483
+            htmlspecialchars($this->l10n->t('Confirm your email')),
484
+            $link,
485
+            false
486
+        );
487
+        $emailTemplate->addFooter();
488
+
489
+        try {
490
+            $message = $this->mailer->createMessage();
491
+            $message->setTo([$email => $user->getDisplayName()]);
492
+            $message->setFrom([Util::getDefaultEmailAddress('verification-noreply') => $this->defaults->getName()]);
493
+            $message->useTemplate($emailTemplate);
494
+            $this->mailer->send($message);
495
+        } catch (Exception $e) {
496
+            // Log the exception and continue
497
+            $this->logger->info('Failed to send verification mail', [
498
+                'app' => 'core',
499
+                'exception' => $e
500
+            ]);
501
+            return false;
502
+        }
503
+        return true;
504
+    }
505
+
506
+    /**
507
+     * Make sure that all expected data are set
508
+     */
509
+    protected function addMissingDefaultValues(array $userData, array $defaultUserData): array {
510
+        foreach ($defaultUserData as $defaultDataItem) {
511
+            // If property does not exist, initialize it
512
+            $userDataIndex = array_search($defaultDataItem['name'], array_column($userData, 'name'));
513
+            if ($userDataIndex === false) {
514
+                $userData[] = $defaultDataItem;
515
+                continue;
516
+            }
517
+
518
+            // Merge and extend default missing values
519
+            $userData[$userDataIndex] = array_merge($defaultDataItem, $userData[$userDataIndex]);
520
+        }
521
+
522
+        return $userData;
523
+    }
524
+
525
+    protected function updateVerificationStatus(IAccount $updatedAccount, array $oldData): void {
526
+        static $propertiesVerifiableByLookupServer = [
527
+            self::PROPERTY_TWITTER,
528
+            self::PROPERTY_FEDIVERSE,
529
+            self::PROPERTY_WEBSITE,
530
+            self::PROPERTY_EMAIL,
531
+        ];
532
+
533
+        foreach ($propertiesVerifiableByLookupServer as $propertyName) {
534
+            try {
535
+                $property = $updatedAccount->getProperty($propertyName);
536
+            } catch (PropertyDoesNotExistException $e) {
537
+                continue;
538
+            }
539
+            $wasVerified = isset($oldData[$propertyName])
540
+                && isset($oldData[$propertyName]['verified'])
541
+                && $oldData[$propertyName]['verified'] === self::VERIFIED;
542
+            if ((!isset($oldData[$propertyName])
543
+                    || !isset($oldData[$propertyName]['value'])
544
+                    || $property->getValue() !== $oldData[$propertyName]['value'])
545
+                && ($property->getVerified() !== self::NOT_VERIFIED
546
+                    || $wasVerified)
547
+            ) {
548
+                $property->setVerified(self::NOT_VERIFIED);
549
+            }
550
+        }
551
+    }
552
+
553
+    /**
554
+     * add new user to accounts table
555
+     *
556
+     * @param IUser $user
557
+     * @param array $data
558
+     */
559
+    protected function insertNewUser(IUser $user, array $data): void {
560
+        $uid = $user->getUID();
561
+        $jsonEncodedData = $this->prepareJson($data);
562
+        $query = $this->connection->getQueryBuilder();
563
+        $query->insert($this->table)
564
+            ->values(
565
+                [
566
+                    'uid' => $query->createNamedParameter($uid),
567
+                    'data' => $query->createNamedParameter($jsonEncodedData),
568
+                ]
569
+            )
570
+            ->executeStatement();
571
+
572
+        $this->deleteUserData($user);
573
+        $this->writeUserData($user, $data);
574
+    }
575
+
576
+    protected function prepareJson(array $data): string {
577
+        $preparedData = [];
578
+        foreach ($data as $dataRow) {
579
+            $propertyName = $dataRow['name'];
580
+            unset($dataRow['name']);
581
+
582
+            if (isset($dataRow['locallyVerified']) && $dataRow['locallyVerified'] === self::NOT_VERIFIED) {
583
+                // do not write default value, save DB space
584
+                unset($dataRow['locallyVerified']);
585
+            }
586
+
587
+            if (!$this->isCollection($propertyName)) {
588
+                $preparedData[$propertyName] = $dataRow;
589
+                continue;
590
+            }
591
+            if (!isset($preparedData[$propertyName])) {
592
+                $preparedData[$propertyName] = [];
593
+            }
594
+            $preparedData[$propertyName][] = $dataRow;
595
+        }
596
+        return json_encode($preparedData);
597
+    }
598
+
599
+    protected function importFromJson(string $json, string $userId): ?array {
600
+        $result = [];
601
+        $jsonArray = json_decode($json, true);
602
+        $jsonError = json_last_error();
603
+        if ($jsonError !== JSON_ERROR_NONE) {
604
+            $this->logger->critical(
605
+                'User data of {uid} contained invalid JSON (error {json_error}), hence falling back to a default user record',
606
+                [
607
+                    'uid' => $userId,
608
+                    'json_error' => $jsonError
609
+                ]
610
+            );
611
+            return null;
612
+        }
613
+        foreach ($jsonArray as $propertyName => $row) {
614
+            if (!$this->isCollection($propertyName)) {
615
+                $result[] = array_merge($row, ['name' => $propertyName]);
616
+                continue;
617
+            }
618
+            foreach ($row as $singleRow) {
619
+                $result[] = array_merge($singleRow, ['name' => $propertyName]);
620
+            }
621
+        }
622
+        return $result;
623
+    }
624
+
625
+    /**
626
+     * Update existing user in accounts table
627
+     */
628
+    protected function updateExistingUser(IUser $user, array $data, array $oldData): void {
629
+        $uid = $user->getUID();
630
+        $jsonEncodedData = $this->prepareJson($data);
631
+        $query = $this->connection->getQueryBuilder();
632
+        $query->update($this->table)
633
+            ->set('data', $query->createNamedParameter($jsonEncodedData))
634
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
635
+            ->executeStatement();
636
+
637
+        $this->deleteUserData($user);
638
+        $this->writeUserData($user, $data);
639
+    }
640
+
641
+    protected function writeUserData(IUser $user, array $data): void {
642
+        $query = $this->connection->getQueryBuilder();
643
+        $query->insert($this->dataTable)
644
+            ->values(
645
+                [
646
+                    'uid' => $query->createNamedParameter($user->getUID()),
647
+                    'name' => $query->createParameter('name'),
648
+                    'value' => $query->createParameter('value'),
649
+                ]
650
+            );
651
+        $this->writeUserDataProperties($query, $data);
652
+    }
653
+
654
+    protected function writeUserDataProperties(IQueryBuilder $query, array $data): void {
655
+        foreach ($data as $property) {
656
+            if ($property['name'] === self::PROPERTY_AVATAR) {
657
+                continue;
658
+            }
659
+
660
+            // the value col is limited to 255 bytes. It is used for searches only.
661
+            $value = $property['value'] ? Util::shortenMultibyteString($property['value'], 255) : '';
662
+
663
+            $query->setParameter('name', $property['name'])
664
+                ->setParameter('value', $value);
665
+            $query->executeStatement();
666
+        }
667
+    }
668
+
669
+    /**
670
+     * build default user record in case not data set exists yet
671
+     */
672
+    protected function buildDefaultUserRecord(IUser $user): array {
673
+        $scopes = array_merge(self::DEFAULT_SCOPES, array_filter($this->config->getSystemValue('account_manager.default_property_scope', []), static function (string $scope, string $property) {
674
+            return in_array($property, self::ALLOWED_PROPERTIES, true) && in_array($scope, self::ALLOWED_SCOPES, true);
675
+        }, ARRAY_FILTER_USE_BOTH));
676
+
677
+        return [
678
+            [
679
+                'name' => self::PROPERTY_DISPLAYNAME,
680
+                'value' => $user->getDisplayName(),
681
+                // Display name must be at least SCOPE_LOCAL
682
+                'scope' => $scopes[self::PROPERTY_DISPLAYNAME] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_DISPLAYNAME],
683
+                'verified' => self::NOT_VERIFIED,
684
+            ],
685
+
686
+            [
687
+                'name' => self::PROPERTY_ADDRESS,
688
+                'value' => '',
689
+                'scope' => $scopes[self::PROPERTY_ADDRESS],
690
+                'verified' => self::NOT_VERIFIED,
691
+            ],
692
+
693
+            [
694
+                'name' => self::PROPERTY_WEBSITE,
695
+                'value' => '',
696
+                'scope' => $scopes[self::PROPERTY_WEBSITE],
697
+                'verified' => self::NOT_VERIFIED,
698
+            ],
699
+
700
+            [
701
+                'name' => self::PROPERTY_EMAIL,
702
+                'value' => $user->getEMailAddress(),
703
+                // Email must be at least SCOPE_LOCAL
704
+                'scope' => $scopes[self::PROPERTY_EMAIL] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_EMAIL],
705
+                'verified' => self::NOT_VERIFIED,
706
+            ],
707
+
708
+            [
709
+                'name' => self::PROPERTY_AVATAR,
710
+                'scope' => $scopes[self::PROPERTY_AVATAR],
711
+            ],
712
+
713
+            [
714
+                'name' => self::PROPERTY_PHONE,
715
+                'value' => '',
716
+                'scope' => $scopes[self::PROPERTY_PHONE],
717
+                'verified' => self::NOT_VERIFIED,
718
+            ],
719
+
720
+            [
721
+                'name' => self::PROPERTY_TWITTER,
722
+                'value' => '',
723
+                'scope' => $scopes[self::PROPERTY_TWITTER],
724
+                'verified' => self::NOT_VERIFIED,
725
+            ],
726
+
727
+            [
728
+                'name' => self::PROPERTY_FEDIVERSE,
729
+                'value' => '',
730
+                'scope' => $scopes[self::PROPERTY_FEDIVERSE],
731
+                'verified' => self::NOT_VERIFIED,
732
+            ],
733
+
734
+            [
735
+                'name' => self::PROPERTY_ORGANISATION,
736
+                'value' => '',
737
+                'scope' => $scopes[self::PROPERTY_ORGANISATION],
738
+            ],
739
+
740
+            [
741
+                'name' => self::PROPERTY_ROLE,
742
+                'value' => '',
743
+                'scope' => $scopes[self::PROPERTY_ROLE],
744
+            ],
745
+
746
+            [
747
+                'name' => self::PROPERTY_HEADLINE,
748
+                'value' => '',
749
+                'scope' => $scopes[self::PROPERTY_HEADLINE],
750
+            ],
751
+
752
+            [
753
+                'name' => self::PROPERTY_BIOGRAPHY,
754
+                'value' => '',
755
+                'scope' => $scopes[self::PROPERTY_BIOGRAPHY],
756
+            ],
757
+
758
+            [
759
+                'name' => self::PROPERTY_PROFILE_ENABLED,
760
+                'value' => $this->isProfileEnabledByDefault($this->config) ? '1' : '0',
761
+            ],
762
+        ];
763
+    }
764
+
765
+    private function arrayDataToCollection(IAccount $account, array $data): IAccountPropertyCollection {
766
+        $collection = $account->getPropertyCollection($data['name']);
767
+
768
+        $p = new AccountProperty(
769
+            $data['name'],
770
+            $data['value'] ?? '',
771
+            $data['scope'] ?? self::SCOPE_LOCAL,
772
+            $data['verified'] ?? self::NOT_VERIFIED,
773
+            ''
774
+        );
775
+        $p->setLocallyVerified($data['locallyVerified'] ?? self::NOT_VERIFIED);
776
+        $collection->addProperty($p);
777
+
778
+        return $collection;
779
+    }
780
+
781
+    private function parseAccountData(IUser $user, $data): Account {
782
+        $account = new Account($user);
783
+        foreach ($data as $accountData) {
784
+            if ($this->isCollection($accountData['name'])) {
785
+                $account->setPropertyCollection($this->arrayDataToCollection($account, $accountData));
786
+            } else {
787
+                $account->setProperty($accountData['name'], $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
788
+                if (isset($accountData['locallyVerified'])) {
789
+                    $property = $account->getProperty($accountData['name']);
790
+                    $property->setLocallyVerified($accountData['locallyVerified']);
791
+                }
792
+            }
793
+        }
794
+        return $account;
795
+    }
796
+
797
+    public function getAccount(IUser $user): IAccount {
798
+        if ($this->internalCache->hasKey($user->getUID())) {
799
+            return $this->internalCache->get($user->getUID());
800
+        }
801
+        $account = $this->parseAccountData($user, $this->getUser($user));
802
+        $this->internalCache->set($user->getUID(), $account);
803
+        return $account;
804
+    }
805
+
806
+    public function updateAccount(IAccount $account): void {
807
+        $this->testValueLengths(iterator_to_array($account->getAllProperties()), true);
808
+        try {
809
+            $property = $account->getProperty(self::PROPERTY_PHONE);
810
+            $this->sanitizePhoneNumberValue($property);
811
+        } catch (PropertyDoesNotExistException $e) {
812
+            //  valid case, nothing to do
813
+        }
814
+
815
+        try {
816
+            $property = $account->getProperty(self::PROPERTY_WEBSITE);
817
+            $this->sanitizeWebsite($property);
818
+        } catch (PropertyDoesNotExistException $e) {
819
+            //  valid case, nothing to do
820
+        }
821
+
822
+        foreach ($account->getAllProperties() as $property) {
823
+            $this->testPropertyScope($property, self::ALLOWED_SCOPES, true);
824
+        }
825
+
826
+        $oldData = $this->getUser($account->getUser(), false);
827
+        $this->updateVerificationStatus($account, $oldData);
828
+        $this->checkEmailVerification($account, $oldData);
829
+        $this->checkLocalEmailVerification($account, $oldData);
830
+
831
+        $data = [];
832
+        foreach ($account->getAllProperties() as $property) {
833
+            /** @var IAccountProperty $property */
834
+            $data[] = [
835
+                'name' => $property->getName(),
836
+                'value' => $property->getValue(),
837
+                'scope' => $property->getScope(),
838
+                'verified' => $property->getVerified(),
839
+                'locallyVerified' => $property->getLocallyVerified(),
840
+            ];
841
+        }
842
+
843
+        $this->updateUser($account->getUser(), $data, $oldData, true);
844
+        $this->internalCache->set($account->getUser()->getUID(), $account);
845
+    }
846 846
 }
Please login to merge, or discard this patch.
lib/private/Accounts/AccountProperty.php 1 patch
Indentation   +165 added lines, -165 removed lines patch added patch discarded remove patch
@@ -32,169 +32,169 @@
 block discarded – undo
32 32
 use OCP\Accounts\IAccountProperty;
33 33
 
34 34
 class AccountProperty implements IAccountProperty {
35
-	/** @var string */
36
-	private $name;
37
-	/** @var string */
38
-	private $value;
39
-	/** @var string */
40
-	private $scope;
41
-	/** @var string */
42
-	private $verified;
43
-	/** @var string */
44
-	private $verificationData;
45
-	/** @var string */
46
-	private $locallyVerified = IAccountManager::NOT_VERIFIED;
47
-
48
-	public function __construct(string $name, string $value, string $scope, string $verified, string $verificationData) {
49
-		$this->name = $name;
50
-		$this->value = $value;
51
-		$this->setScope($scope);
52
-		$this->verified = $verified;
53
-		$this->verificationData = $verificationData;
54
-	}
55
-
56
-	public function jsonSerialize(): array {
57
-		return [
58
-			'name' => $this->getName(),
59
-			'value' => $this->getValue(),
60
-			'scope' => $this->getScope(),
61
-			'verified' => $this->getVerified(),
62
-			'verificationData' => $this->getVerificationData(),
63
-		];
64
-	}
65
-
66
-	/**
67
-	 * Set the value of a property
68
-	 *
69
-	 * @since 15.0.0
70
-	 *
71
-	 * @param string $value
72
-	 * @return IAccountProperty
73
-	 */
74
-	public function setValue(string $value): IAccountProperty {
75
-		$this->value = $value;
76
-		return $this;
77
-	}
78
-
79
-	/**
80
-	 * Set the scope of a property
81
-	 *
82
-	 * @since 15.0.0
83
-	 *
84
-	 * @param string $scope
85
-	 * @return IAccountProperty
86
-	 */
87
-	public function setScope(string $scope): IAccountProperty {
88
-		$newScope = $this->mapScopeToV2($scope);
89
-		if (!in_array($newScope, [
90
-			IAccountManager::SCOPE_LOCAL,
91
-			IAccountManager::SCOPE_FEDERATED,
92
-			IAccountManager::SCOPE_PRIVATE,
93
-			IAccountManager::SCOPE_PUBLISHED
94
-		])) {
95
-			throw new InvalidArgumentException('Invalid scope');
96
-		}
97
-		$this->scope = $newScope;
98
-		return $this;
99
-	}
100
-
101
-	/**
102
-	 * Set the verification status of a property
103
-	 *
104
-	 * @since 15.0.0
105
-	 *
106
-	 * @param string $verified
107
-	 * @return IAccountProperty
108
-	 */
109
-	public function setVerified(string $verified): IAccountProperty {
110
-		$this->verified = $verified;
111
-		return $this;
112
-	}
113
-
114
-	/**
115
-	 * Get the name of a property
116
-	 *
117
-	 * @since 15.0.0
118
-	 *
119
-	 * @return string
120
-	 */
121
-	public function getName(): string {
122
-		return $this->name;
123
-	}
124
-
125
-	/**
126
-	 * Get the value of a property
127
-	 *
128
-	 * @since 15.0.0
129
-	 *
130
-	 * @return string
131
-	 */
132
-	public function getValue(): string {
133
-		return $this->value;
134
-	}
135
-
136
-	/**
137
-	 * Get the scope of a property
138
-	 *
139
-	 * @since 15.0.0
140
-	 *
141
-	 * @return string
142
-	 */
143
-	public function getScope(): string {
144
-		return $this->scope;
145
-	}
146
-
147
-	public static function mapScopeToV2(string $scope): string {
148
-		if (str_starts_with($scope, 'v2-')) {
149
-			return $scope;
150
-		}
151
-
152
-		switch ($scope) {
153
-			case IAccountManager::VISIBILITY_PRIVATE:
154
-			case '':
155
-				return IAccountManager::SCOPE_LOCAL;
156
-			case IAccountManager::VISIBILITY_CONTACTS_ONLY:
157
-				return IAccountManager::SCOPE_FEDERATED;
158
-			case IAccountManager::VISIBILITY_PUBLIC:
159
-				return IAccountManager::SCOPE_PUBLISHED;
160
-			default:
161
-				return $scope;
162
-		}
163
-	}
164
-
165
-	/**
166
-	 * Get the verification status of a property
167
-	 *
168
-	 * @since 15.0.0
169
-	 *
170
-	 * @return string
171
-	 */
172
-	public function getVerified(): string {
173
-		return $this->verified;
174
-	}
175
-
176
-	public function setVerificationData(string $verificationData): IAccountProperty {
177
-		$this->verificationData = $verificationData;
178
-		return $this;
179
-	}
180
-
181
-	public function getVerificationData(): string {
182
-		return $this->verificationData;
183
-	}
184
-
185
-	public function setLocallyVerified(string $verified): IAccountProperty {
186
-		if (!in_array($verified, [
187
-			IAccountManager::NOT_VERIFIED,
188
-			IAccountManager::VERIFICATION_IN_PROGRESS,
189
-			IAccountManager::VERIFIED,
190
-		])) {
191
-			throw new InvalidArgumentException('Provided verification value is invalid');
192
-		}
193
-		$this->locallyVerified = $verified;
194
-		return $this;
195
-	}
196
-
197
-	public function getLocallyVerified(): string {
198
-		return $this->locallyVerified;
199
-	}
35
+    /** @var string */
36
+    private $name;
37
+    /** @var string */
38
+    private $value;
39
+    /** @var string */
40
+    private $scope;
41
+    /** @var string */
42
+    private $verified;
43
+    /** @var string */
44
+    private $verificationData;
45
+    /** @var string */
46
+    private $locallyVerified = IAccountManager::NOT_VERIFIED;
47
+
48
+    public function __construct(string $name, string $value, string $scope, string $verified, string $verificationData) {
49
+        $this->name = $name;
50
+        $this->value = $value;
51
+        $this->setScope($scope);
52
+        $this->verified = $verified;
53
+        $this->verificationData = $verificationData;
54
+    }
55
+
56
+    public function jsonSerialize(): array {
57
+        return [
58
+            'name' => $this->getName(),
59
+            'value' => $this->getValue(),
60
+            'scope' => $this->getScope(),
61
+            'verified' => $this->getVerified(),
62
+            'verificationData' => $this->getVerificationData(),
63
+        ];
64
+    }
65
+
66
+    /**
67
+     * Set the value of a property
68
+     *
69
+     * @since 15.0.0
70
+     *
71
+     * @param string $value
72
+     * @return IAccountProperty
73
+     */
74
+    public function setValue(string $value): IAccountProperty {
75
+        $this->value = $value;
76
+        return $this;
77
+    }
78
+
79
+    /**
80
+     * Set the scope of a property
81
+     *
82
+     * @since 15.0.0
83
+     *
84
+     * @param string $scope
85
+     * @return IAccountProperty
86
+     */
87
+    public function setScope(string $scope): IAccountProperty {
88
+        $newScope = $this->mapScopeToV2($scope);
89
+        if (!in_array($newScope, [
90
+            IAccountManager::SCOPE_LOCAL,
91
+            IAccountManager::SCOPE_FEDERATED,
92
+            IAccountManager::SCOPE_PRIVATE,
93
+            IAccountManager::SCOPE_PUBLISHED
94
+        ])) {
95
+            throw new InvalidArgumentException('Invalid scope');
96
+        }
97
+        $this->scope = $newScope;
98
+        return $this;
99
+    }
100
+
101
+    /**
102
+     * Set the verification status of a property
103
+     *
104
+     * @since 15.0.0
105
+     *
106
+     * @param string $verified
107
+     * @return IAccountProperty
108
+     */
109
+    public function setVerified(string $verified): IAccountProperty {
110
+        $this->verified = $verified;
111
+        return $this;
112
+    }
113
+
114
+    /**
115
+     * Get the name of a property
116
+     *
117
+     * @since 15.0.0
118
+     *
119
+     * @return string
120
+     */
121
+    public function getName(): string {
122
+        return $this->name;
123
+    }
124
+
125
+    /**
126
+     * Get the value of a property
127
+     *
128
+     * @since 15.0.0
129
+     *
130
+     * @return string
131
+     */
132
+    public function getValue(): string {
133
+        return $this->value;
134
+    }
135
+
136
+    /**
137
+     * Get the scope of a property
138
+     *
139
+     * @since 15.0.0
140
+     *
141
+     * @return string
142
+     */
143
+    public function getScope(): string {
144
+        return $this->scope;
145
+    }
146
+
147
+    public static function mapScopeToV2(string $scope): string {
148
+        if (str_starts_with($scope, 'v2-')) {
149
+            return $scope;
150
+        }
151
+
152
+        switch ($scope) {
153
+            case IAccountManager::VISIBILITY_PRIVATE:
154
+            case '':
155
+                return IAccountManager::SCOPE_LOCAL;
156
+            case IAccountManager::VISIBILITY_CONTACTS_ONLY:
157
+                return IAccountManager::SCOPE_FEDERATED;
158
+            case IAccountManager::VISIBILITY_PUBLIC:
159
+                return IAccountManager::SCOPE_PUBLISHED;
160
+            default:
161
+                return $scope;
162
+        }
163
+    }
164
+
165
+    /**
166
+     * Get the verification status of a property
167
+     *
168
+     * @since 15.0.0
169
+     *
170
+     * @return string
171
+     */
172
+    public function getVerified(): string {
173
+        return $this->verified;
174
+    }
175
+
176
+    public function setVerificationData(string $verificationData): IAccountProperty {
177
+        $this->verificationData = $verificationData;
178
+        return $this;
179
+    }
180
+
181
+    public function getVerificationData(): string {
182
+        return $this->verificationData;
183
+    }
184
+
185
+    public function setLocallyVerified(string $verified): IAccountProperty {
186
+        if (!in_array($verified, [
187
+            IAccountManager::NOT_VERIFIED,
188
+            IAccountManager::VERIFICATION_IN_PROGRESS,
189
+            IAccountManager::VERIFIED,
190
+        ])) {
191
+            throw new InvalidArgumentException('Provided verification value is invalid');
192
+        }
193
+        $this->locallyVerified = $verified;
194
+        return $this;
195
+    }
196
+
197
+    public function getLocallyVerified(): string {
198
+        return $this->locallyVerified;
199
+    }
200 200
 }
Please login to merge, or discard this patch.
lib/private/Memcache/ArrayCache.php 1 patch
Indentation   +117 added lines, -117 removed lines patch added patch discarded remove patch
@@ -27,133 +27,133 @@
 block discarded – undo
27 27
 use OCP\IMemcache;
28 28
 
29 29
 class ArrayCache extends Cache implements IMemcache {
30
-	/** @var array Array with the cached data */
31
-	protected $cachedData = [];
30
+    /** @var array Array with the cached data */
31
+    protected $cachedData = [];
32 32
 
33
-	use CADTrait;
33
+    use CADTrait;
34 34
 
35
-	/**
36
-	 * {@inheritDoc}
37
-	 */
38
-	public function get($key) {
39
-		if ($this->hasKey($key)) {
40
-			return $this->cachedData[$key];
41
-		}
42
-		return null;
43
-	}
35
+    /**
36
+     * {@inheritDoc}
37
+     */
38
+    public function get($key) {
39
+        if ($this->hasKey($key)) {
40
+            return $this->cachedData[$key];
41
+        }
42
+        return null;
43
+    }
44 44
 
45
-	/**
46
-	 * {@inheritDoc}
47
-	 */
48
-	public function set($key, $value, $ttl = 0) {
49
-		$this->cachedData[$key] = $value;
50
-		return true;
51
-	}
45
+    /**
46
+     * {@inheritDoc}
47
+     */
48
+    public function set($key, $value, $ttl = 0) {
49
+        $this->cachedData[$key] = $value;
50
+        return true;
51
+    }
52 52
 
53
-	/**
54
-	 * {@inheritDoc}
55
-	 */
56
-	public function hasKey($key) {
57
-		return isset($this->cachedData[$key]);
58
-	}
53
+    /**
54
+     * {@inheritDoc}
55
+     */
56
+    public function hasKey($key) {
57
+        return isset($this->cachedData[$key]);
58
+    }
59 59
 
60
-	/**
61
-	 * {@inheritDoc}
62
-	 */
63
-	public function remove($key) {
64
-		unset($this->cachedData[$key]);
65
-		return true;
66
-	}
60
+    /**
61
+     * {@inheritDoc}
62
+     */
63
+    public function remove($key) {
64
+        unset($this->cachedData[$key]);
65
+        return true;
66
+    }
67 67
 
68
-	/**
69
-	 * {@inheritDoc}
70
-	 */
71
-	public function clear($prefix = '') {
72
-		if ($prefix === '') {
73
-			$this->cachedData = [];
74
-			return true;
75
-		}
68
+    /**
69
+     * {@inheritDoc}
70
+     */
71
+    public function clear($prefix = '') {
72
+        if ($prefix === '') {
73
+            $this->cachedData = [];
74
+            return true;
75
+        }
76 76
 
77
-		foreach ($this->cachedData as $key => $value) {
78
-			if (str_starts_with($key, $prefix)) {
79
-				$this->remove($key);
80
-			}
81
-		}
82
-		return true;
83
-	}
77
+        foreach ($this->cachedData as $key => $value) {
78
+            if (str_starts_with($key, $prefix)) {
79
+                $this->remove($key);
80
+            }
81
+        }
82
+        return true;
83
+    }
84 84
 
85
-	/**
86
-	 * Set a value in the cache if it's not already stored
87
-	 *
88
-	 * @param string $key
89
-	 * @param mixed $value
90
-	 * @param int $ttl Time To Live in seconds. Defaults to 60*60*24
91
-	 * @return bool
92
-	 */
93
-	public function add($key, $value, $ttl = 0) {
94
-		// since this cache is not shared race conditions aren't an issue
95
-		if ($this->hasKey($key)) {
96
-			return false;
97
-		} else {
98
-			return $this->set($key, $value, $ttl);
99
-		}
100
-	}
85
+    /**
86
+     * Set a value in the cache if it's not already stored
87
+     *
88
+     * @param string $key
89
+     * @param mixed $value
90
+     * @param int $ttl Time To Live in seconds. Defaults to 60*60*24
91
+     * @return bool
92
+     */
93
+    public function add($key, $value, $ttl = 0) {
94
+        // since this cache is not shared race conditions aren't an issue
95
+        if ($this->hasKey($key)) {
96
+            return false;
97
+        } else {
98
+            return $this->set($key, $value, $ttl);
99
+        }
100
+    }
101 101
 
102
-	/**
103
-	 * Increase a stored number
104
-	 *
105
-	 * @param string $key
106
-	 * @param int $step
107
-	 * @return int | bool
108
-	 */
109
-	public function inc($key, $step = 1) {
110
-		$oldValue = $this->get($key);
111
-		if (is_int($oldValue)) {
112
-			$this->set($key, $oldValue + $step);
113
-			return $oldValue + $step;
114
-		} else {
115
-			$success = $this->add($key, $step);
116
-			return $success ? $step : false;
117
-		}
118
-	}
102
+    /**
103
+     * Increase a stored number
104
+     *
105
+     * @param string $key
106
+     * @param int $step
107
+     * @return int | bool
108
+     */
109
+    public function inc($key, $step = 1) {
110
+        $oldValue = $this->get($key);
111
+        if (is_int($oldValue)) {
112
+            $this->set($key, $oldValue + $step);
113
+            return $oldValue + $step;
114
+        } else {
115
+            $success = $this->add($key, $step);
116
+            return $success ? $step : false;
117
+        }
118
+    }
119 119
 
120
-	/**
121
-	 * Decrease a stored number
122
-	 *
123
-	 * @param string $key
124
-	 * @param int $step
125
-	 * @return int | bool
126
-	 */
127
-	public function dec($key, $step = 1) {
128
-		$oldValue = $this->get($key);
129
-		if (is_int($oldValue)) {
130
-			$this->set($key, $oldValue - $step);
131
-			return $oldValue - $step;
132
-		} else {
133
-			return false;
134
-		}
135
-	}
120
+    /**
121
+     * Decrease a stored number
122
+     *
123
+     * @param string $key
124
+     * @param int $step
125
+     * @return int | bool
126
+     */
127
+    public function dec($key, $step = 1) {
128
+        $oldValue = $this->get($key);
129
+        if (is_int($oldValue)) {
130
+            $this->set($key, $oldValue - $step);
131
+            return $oldValue - $step;
132
+        } else {
133
+            return false;
134
+        }
135
+    }
136 136
 
137
-	/**
138
-	 * Compare and set
139
-	 *
140
-	 * @param string $key
141
-	 * @param mixed $old
142
-	 * @param mixed $new
143
-	 * @return bool
144
-	 */
145
-	public function cas($key, $old, $new) {
146
-		if ($this->get($key) === $old) {
147
-			return $this->set($key, $new);
148
-		} else {
149
-			return false;
150
-		}
151
-	}
137
+    /**
138
+     * Compare and set
139
+     *
140
+     * @param string $key
141
+     * @param mixed $old
142
+     * @param mixed $new
143
+     * @return bool
144
+     */
145
+    public function cas($key, $old, $new) {
146
+        if ($this->get($key) === $old) {
147
+            return $this->set($key, $new);
148
+        } else {
149
+            return false;
150
+        }
151
+    }
152 152
 
153
-	/**
154
-	 * {@inheritDoc}
155
-	 */
156
-	public static function isAvailable(): bool {
157
-		return true;
158
-	}
153
+    /**
154
+     * {@inheritDoc}
155
+     */
156
+    public static function isAvailable(): bool {
157
+        return true;
158
+    }
159 159
 }
Please login to merge, or discard this patch.
lib/private/LargeFileHelper.php 2 patches
Indentation   +161 added lines, -161 removed lines patch added patch discarded remove patch
@@ -35,174 +35,174 @@
 block discarded – undo
35 35
  * Helper class for large files on 32-bit platforms.
36 36
  */
37 37
 class LargeFileHelper {
38
-	/**
39
-	 * pow(2, 53) as a base-10 string.
40
-	 * @var string
41
-	 */
42
-	public const POW_2_53 = '9007199254740992';
38
+    /**
39
+     * pow(2, 53) as a base-10 string.
40
+     * @var string
41
+     */
42
+    public const POW_2_53 = '9007199254740992';
43 43
 
44
-	/**
45
-	 * pow(2, 53) - 1 as a base-10 string.
46
-	 * @var string
47
-	 */
48
-	public const POW_2_53_MINUS_1 = '9007199254740991';
44
+    /**
45
+     * pow(2, 53) - 1 as a base-10 string.
46
+     * @var string
47
+     */
48
+    public const POW_2_53_MINUS_1 = '9007199254740991';
49 49
 
50
-	/**
51
-	 * @brief Checks whether our assumptions hold on the PHP platform we are on.
52
-	 *
53
-	 * @throws \RuntimeException if our assumptions do not hold on the current
54
-	 *                           PHP platform.
55
-	 */
56
-	public function __construct() {
57
-		$pow_2_53 = (float)self::POW_2_53_MINUS_1 + 1.0;
58
-		if ($this->formatUnsignedInteger($pow_2_53) !== self::POW_2_53) {
59
-			throw new \RuntimeException(
60
-				'This class assumes floats to be double precision or "better".'
61
-			);
62
-		}
63
-	}
50
+    /**
51
+     * @brief Checks whether our assumptions hold on the PHP platform we are on.
52
+     *
53
+     * @throws \RuntimeException if our assumptions do not hold on the current
54
+     *                           PHP platform.
55
+     */
56
+    public function __construct() {
57
+        $pow_2_53 = (float)self::POW_2_53_MINUS_1 + 1.0;
58
+        if ($this->formatUnsignedInteger($pow_2_53) !== self::POW_2_53) {
59
+            throw new \RuntimeException(
60
+                'This class assumes floats to be double precision or "better".'
61
+            );
62
+        }
63
+    }
64 64
 
65
-	/**
66
-	 * @brief Formats a signed integer or float as an unsigned integer base-10
67
-	 *        string. Passed strings will be checked for being base-10.
68
-	 *
69
-	 * @param int|float|string $number Number containing unsigned integer data
70
-	 *
71
-	 * @throws \UnexpectedValueException if $number is not a float, not an int
72
-	 *                                   and not a base-10 string.
73
-	 *
74
-	 * @return string Unsigned integer base-10 string
75
-	 */
76
-	public function formatUnsignedInteger(int|float|string $number): string {
77
-		if (is_float($number)) {
78
-			// Undo the effect of the php.ini setting 'precision'.
79
-			return number_format($number, 0, '', '');
80
-		} elseif (is_string($number) && ctype_digit($number)) {
81
-			return $number;
82
-		} elseif (is_int($number)) {
83
-			// Interpret signed integer as unsigned integer.
84
-			return sprintf('%u', $number);
85
-		} else {
86
-			throw new \UnexpectedValueException(
87
-				'Expected int, float or base-10 string'
88
-			);
89
-		}
90
-	}
65
+    /**
66
+     * @brief Formats a signed integer or float as an unsigned integer base-10
67
+     *        string. Passed strings will be checked for being base-10.
68
+     *
69
+     * @param int|float|string $number Number containing unsigned integer data
70
+     *
71
+     * @throws \UnexpectedValueException if $number is not a float, not an int
72
+     *                                   and not a base-10 string.
73
+     *
74
+     * @return string Unsigned integer base-10 string
75
+     */
76
+    public function formatUnsignedInteger(int|float|string $number): string {
77
+        if (is_float($number)) {
78
+            // Undo the effect of the php.ini setting 'precision'.
79
+            return number_format($number, 0, '', '');
80
+        } elseif (is_string($number) && ctype_digit($number)) {
81
+            return $number;
82
+        } elseif (is_int($number)) {
83
+            // Interpret signed integer as unsigned integer.
84
+            return sprintf('%u', $number);
85
+        } else {
86
+            throw new \UnexpectedValueException(
87
+                'Expected int, float or base-10 string'
88
+            );
89
+        }
90
+    }
91 91
 
92
-	/**
93
-	 * @brief Tries to get the size of a file via various workarounds that
94
-	 *        even work for large files on 32-bit platforms.
95
-	 *
96
-	 * @param string $filename Path to the file.
97
-	 *
98
-	 * @return int|float Number of bytes as number (float or int)
99
-	 */
100
-	public function getFileSize(string $filename): int|float {
101
-		$fileSize = $this->getFileSizeViaCurl($filename);
102
-		if (!is_null($fileSize)) {
103
-			return $fileSize;
104
-		}
105
-		$fileSize = $this->getFileSizeViaExec($filename);
106
-		if (!is_null($fileSize)) {
107
-			return $fileSize;
108
-		}
109
-		return $this->getFileSizeNative($filename);
110
-	}
92
+    /**
93
+     * @brief Tries to get the size of a file via various workarounds that
94
+     *        even work for large files on 32-bit platforms.
95
+     *
96
+     * @param string $filename Path to the file.
97
+     *
98
+     * @return int|float Number of bytes as number (float or int)
99
+     */
100
+    public function getFileSize(string $filename): int|float {
101
+        $fileSize = $this->getFileSizeViaCurl($filename);
102
+        if (!is_null($fileSize)) {
103
+            return $fileSize;
104
+        }
105
+        $fileSize = $this->getFileSizeViaExec($filename);
106
+        if (!is_null($fileSize)) {
107
+            return $fileSize;
108
+        }
109
+        return $this->getFileSizeNative($filename);
110
+    }
111 111
 
112
-	/**
113
-	 * @brief Tries to get the size of a file via a CURL HEAD request.
114
-	 *
115
-	 * @param string $fileName Path to the file.
116
-	 *
117
-	 * @return null|int|float Number of bytes as number (float or int) or
118
-	 *                        null on failure.
119
-	 */
120
-	public function getFileSizeViaCurl(string $fileName): null|int|float {
121
-		if (\OC::$server->get(IniGetWrapper::class)->getString('open_basedir') === '') {
122
-			$encodedFileName = rawurlencode($fileName);
123
-			$ch = curl_init("file:///$encodedFileName");
124
-			curl_setopt($ch, CURLOPT_NOBODY, true);
125
-			curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
126
-			curl_setopt($ch, CURLOPT_HEADER, true);
127
-			$data = curl_exec($ch);
128
-			curl_close($ch);
129
-			if ($data !== false) {
130
-				$matches = [];
131
-				preg_match('/Content-Length: (\d+)/', $data, $matches);
132
-				if (isset($matches[1])) {
133
-					return 0 + $matches[1];
134
-				}
135
-			}
136
-		}
137
-		return null;
138
-	}
112
+    /**
113
+     * @brief Tries to get the size of a file via a CURL HEAD request.
114
+     *
115
+     * @param string $fileName Path to the file.
116
+     *
117
+     * @return null|int|float Number of bytes as number (float or int) or
118
+     *                        null on failure.
119
+     */
120
+    public function getFileSizeViaCurl(string $fileName): null|int|float {
121
+        if (\OC::$server->get(IniGetWrapper::class)->getString('open_basedir') === '') {
122
+            $encodedFileName = rawurlencode($fileName);
123
+            $ch = curl_init("file:///$encodedFileName");
124
+            curl_setopt($ch, CURLOPT_NOBODY, true);
125
+            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
126
+            curl_setopt($ch, CURLOPT_HEADER, true);
127
+            $data = curl_exec($ch);
128
+            curl_close($ch);
129
+            if ($data !== false) {
130
+                $matches = [];
131
+                preg_match('/Content-Length: (\d+)/', $data, $matches);
132
+                if (isset($matches[1])) {
133
+                    return 0 + $matches[1];
134
+                }
135
+            }
136
+        }
137
+        return null;
138
+    }
139 139
 
140
-	/**
141
-	 * @brief Tries to get the size of a file via an exec() call.
142
-	 *
143
-	 * @param string $filename Path to the file.
144
-	 *
145
-	 * @return null|int|float Number of bytes as number (float or int) or
146
-	 *                        null on failure.
147
-	 */
148
-	public function getFileSizeViaExec(string $filename): null|int|float {
149
-		if (\OCP\Util::isFunctionEnabled('exec')) {
150
-			$os = strtolower(php_uname('s'));
151
-			$arg = escapeshellarg($filename);
152
-			$result = null;
153
-			if (str_contains($os, 'linux')) {
154
-				$result = $this->exec("stat -c %s $arg");
155
-			} elseif (str_contains($os, 'bsd') || str_contains($os, 'darwin')) {
156
-				$result = $this->exec("stat -f %z $arg");
157
-			}
158
-			return $result;
159
-		}
160
-		return null;
161
-	}
140
+    /**
141
+     * @brief Tries to get the size of a file via an exec() call.
142
+     *
143
+     * @param string $filename Path to the file.
144
+     *
145
+     * @return null|int|float Number of bytes as number (float or int) or
146
+     *                        null on failure.
147
+     */
148
+    public function getFileSizeViaExec(string $filename): null|int|float {
149
+        if (\OCP\Util::isFunctionEnabled('exec')) {
150
+            $os = strtolower(php_uname('s'));
151
+            $arg = escapeshellarg($filename);
152
+            $result = null;
153
+            if (str_contains($os, 'linux')) {
154
+                $result = $this->exec("stat -c %s $arg");
155
+            } elseif (str_contains($os, 'bsd') || str_contains($os, 'darwin')) {
156
+                $result = $this->exec("stat -f %z $arg");
157
+            }
158
+            return $result;
159
+        }
160
+        return null;
161
+    }
162 162
 
163
-	/**
164
-	 * @brief Gets the size of a file via a filesize() call and converts
165
-	 *        negative signed int to positive float. As the result of filesize()
166
-	 *        will wrap around after a file size of 2^32 bytes = 4 GiB, this
167
-	 *        should only be used as a last resort.
168
-	 *
169
-	 * @param string $filename Path to the file.
170
-	 *
171
-	 * @return int|float Number of bytes as number (float or int).
172
-	 */
173
-	public function getFileSizeNative(string $filename): int|float {
174
-		$result = filesize($filename);
175
-		if ($result < 0) {
176
-			// For file sizes between 2 GiB and 4 GiB, filesize() will return a
177
-			// negative int, as the PHP data type int is signed. Interpret the
178
-			// returned int as an unsigned integer and put it into a float.
179
-			return (float) sprintf('%u', $result);
180
-		}
181
-		return $result;
182
-	}
163
+    /**
164
+     * @brief Gets the size of a file via a filesize() call and converts
165
+     *        negative signed int to positive float. As the result of filesize()
166
+     *        will wrap around after a file size of 2^32 bytes = 4 GiB, this
167
+     *        should only be used as a last resort.
168
+     *
169
+     * @param string $filename Path to the file.
170
+     *
171
+     * @return int|float Number of bytes as number (float or int).
172
+     */
173
+    public function getFileSizeNative(string $filename): int|float {
174
+        $result = filesize($filename);
175
+        if ($result < 0) {
176
+            // For file sizes between 2 GiB and 4 GiB, filesize() will return a
177
+            // negative int, as the PHP data type int is signed. Interpret the
178
+            // returned int as an unsigned integer and put it into a float.
179
+            return (float) sprintf('%u', $result);
180
+        }
181
+        return $result;
182
+    }
183 183
 
184
-	/**
185
-	 * Returns the current mtime for $fullPath
186
-	 */
187
-	public function getFileMtime(string $fullPath): int {
188
-		try {
189
-			$result = filemtime($fullPath) ?: -1;
190
-		} catch (\Exception $e) {
191
-			$result = - 1;
192
-		}
193
-		if ($result < 0) {
194
-			if (\OCP\Util::isFunctionEnabled('exec')) {
195
-				$os = strtolower(php_uname('s'));
196
-				if (str_contains($os, 'linux')) {
197
-					return (int)($this->exec('stat -c %Y ' . escapeshellarg($fullPath)) ?? -1);
198
-				}
199
-			}
200
-		}
201
-		return $result;
202
-	}
184
+    /**
185
+     * Returns the current mtime for $fullPath
186
+     */
187
+    public function getFileMtime(string $fullPath): int {
188
+        try {
189
+            $result = filemtime($fullPath) ?: -1;
190
+        } catch (\Exception $e) {
191
+            $result = - 1;
192
+        }
193
+        if ($result < 0) {
194
+            if (\OCP\Util::isFunctionEnabled('exec')) {
195
+                $os = strtolower(php_uname('s'));
196
+                if (str_contains($os, 'linux')) {
197
+                    return (int)($this->exec('stat -c %Y ' . escapeshellarg($fullPath)) ?? -1);
198
+                }
199
+            }
200
+        }
201
+        return $result;
202
+    }
203 203
 
204
-	protected function exec(string $cmd): null|int|float {
205
-		$result = trim(exec($cmd));
206
-		return ctype_digit($result) ? 0 + $result : null;
207
-	}
204
+    protected function exec(string $cmd): null|int|float {
205
+        $result = trim(exec($cmd));
206
+        return ctype_digit($result) ? 0 + $result : null;
207
+    }
208 208
 }
Please login to merge, or discard this patch.
Spacing   +8 added lines, -8 removed lines patch added patch discarded remove patch
@@ -54,7 +54,7 @@  discard block
 block discarded – undo
54 54
 	 *                           PHP platform.
55 55
 	 */
56 56
 	public function __construct() {
57
-		$pow_2_53 = (float)self::POW_2_53_MINUS_1 + 1.0;
57
+		$pow_2_53 = (float) self::POW_2_53_MINUS_1 + 1.0;
58 58
 		if ($this->formatUnsignedInteger($pow_2_53) !== self::POW_2_53) {
59 59
 			throw new \RuntimeException(
60 60
 				'This class assumes floats to be double precision or "better".'
@@ -73,7 +73,7 @@  discard block
 block discarded – undo
73 73
 	 *
74 74
 	 * @return string Unsigned integer base-10 string
75 75
 	 */
76
-	public function formatUnsignedInteger(int|float|string $number): string {
76
+	public function formatUnsignedInteger(int | float | string $number): string {
77 77
 		if (is_float($number)) {
78 78
 			// Undo the effect of the php.ini setting 'precision'.
79 79
 			return number_format($number, 0, '', '');
@@ -97,7 +97,7 @@  discard block
 block discarded – undo
97 97
 	 *
98 98
 	 * @return int|float Number of bytes as number (float or int)
99 99
 	 */
100
-	public function getFileSize(string $filename): int|float {
100
+	public function getFileSize(string $filename): int | float {
101 101
 		$fileSize = $this->getFileSizeViaCurl($filename);
102 102
 		if (!is_null($fileSize)) {
103 103
 			return $fileSize;
@@ -117,7 +117,7 @@  discard block
 block discarded – undo
117 117
 	 * @return null|int|float Number of bytes as number (float or int) or
118 118
 	 *                        null on failure.
119 119
 	 */
120
-	public function getFileSizeViaCurl(string $fileName): null|int|float {
120
+	public function getFileSizeViaCurl(string $fileName): null | int | float {
121 121
 		if (\OC::$server->get(IniGetWrapper::class)->getString('open_basedir') === '') {
122 122
 			$encodedFileName = rawurlencode($fileName);
123 123
 			$ch = curl_init("file:///$encodedFileName");
@@ -145,7 +145,7 @@  discard block
 block discarded – undo
145 145
 	 * @return null|int|float Number of bytes as number (float or int) or
146 146
 	 *                        null on failure.
147 147
 	 */
148
-	public function getFileSizeViaExec(string $filename): null|int|float {
148
+	public function getFileSizeViaExec(string $filename): null | int | float {
149 149
 		if (\OCP\Util::isFunctionEnabled('exec')) {
150 150
 			$os = strtolower(php_uname('s'));
151 151
 			$arg = escapeshellarg($filename);
@@ -170,7 +170,7 @@  discard block
 block discarded – undo
170 170
 	 *
171 171
 	 * @return int|float Number of bytes as number (float or int).
172 172
 	 */
173
-	public function getFileSizeNative(string $filename): int|float {
173
+	public function getFileSizeNative(string $filename): int | float {
174 174
 		$result = filesize($filename);
175 175
 		if ($result < 0) {
176 176
 			// For file sizes between 2 GiB and 4 GiB, filesize() will return a
@@ -194,14 +194,14 @@  discard block
 block discarded – undo
194 194
 			if (\OCP\Util::isFunctionEnabled('exec')) {
195 195
 				$os = strtolower(php_uname('s'));
196 196
 				if (str_contains($os, 'linux')) {
197
-					return (int)($this->exec('stat -c %Y ' . escapeshellarg($fullPath)) ?? -1);
197
+					return (int) ($this->exec('stat -c %Y '.escapeshellarg($fullPath)) ?? -1);
198 198
 				}
199 199
 			}
200 200
 		}
201 201
 		return $result;
202 202
 	}
203 203
 
204
-	protected function exec(string $cmd): null|int|float {
204
+	protected function exec(string $cmd): null | int | float {
205 205
 		$result = trim(exec($cmd));
206 206
 		return ctype_digit($result) ? 0 + $result : null;
207 207
 	}
Please login to merge, or discard this patch.