Completed
Push — master ( ec3be2...1b63b5 )
by
unknown
28:05
created
lib/private/Accounts/AccountManager.php 2 patches
Spacing   +3 added lines, -3 removed lines patch added patch discarded remove patch
@@ -371,7 +371,7 @@  discard block
 block discarded – undo
371 371
 
372 372
 	public function searchUsers(string $property, array $values): array {
373 373
 		// the value col is limited to 255 bytes. It is used for searches only.
374
-		$values = array_map(function (string $value) {
374
+		$values = array_map(function(string $value) {
375 375
 			return Util::shortenMultibyteString($value, 255);
376 376
 		}, $values);
377 377
 		$chunks = array_chunk($values, 500);
@@ -451,7 +451,7 @@  discard block
 block discarded – undo
451 451
 	protected function sendEmailVerificationEmail(IUser $user, string $email): bool {
452 452
 		$ref = \substr(hash('sha256', $email), 0, 8);
453 453
 		$key = $this->crypto->encrypt($email);
454
-		$token = $this->verificationToken->create($user, 'verifyMail' . $ref, $email);
454
+		$token = $this->verificationToken->create($user, 'verifyMail'.$ref, $email);
455 455
 
456 456
 		$link = $this->urlGenerator->linkToRouteAbsolute(
457 457
 			'provisioning_api.Verification.verifyMail',
@@ -669,7 +669,7 @@  discard block
 block discarded – undo
669 669
 	 * build default user record in case not data set exists yet
670 670
 	 */
671 671
 	protected function buildDefaultUserRecord(IUser $user): array {
672
-		$scopes = array_merge(self::DEFAULT_SCOPES, array_filter($this->config->getSystemValue('account_manager.default_property_scope', []), static function (string $scope, string $property) {
672
+		$scopes = array_merge(self::DEFAULT_SCOPES, array_filter($this->config->getSystemValue('account_manager.default_property_scope', []), static function(string $scope, string $property) {
673 673
 			return in_array($property, self::ALLOWED_PROPERTIES, true) && in_array($scope, self::ALLOWED_SCOPES, true);
674 674
 		}, ARRAY_FILTER_USE_BOTH));
675 675
 
Please login to merge, or discard this patch.
Indentation   +843 added lines, -843 removed lines patch added patch discarded remove patch
@@ -52,847 +52,847 @@
 block discarded – undo
52 52
  * @package OC\Accounts
53 53
  */
54 54
 class AccountManager implements IAccountManager {
55
-	use TAccountsHelper;
56
-
57
-	use TProfileHelper;
58
-
59
-	private string $table = 'accounts';
60
-	private string $dataTable = 'accounts_data';
61
-	private ?IL10N $l10n = null;
62
-	private CappedMemoryCache $internalCache;
63
-
64
-	/**
65
-	 * The list of default scopes for each property.
66
-	 */
67
-	public const DEFAULT_SCOPES = [
68
-		self::PROPERTY_ADDRESS => self::SCOPE_LOCAL,
69
-		self::PROPERTY_AVATAR => self::SCOPE_FEDERATED,
70
-		self::PROPERTY_BIOGRAPHY => self::SCOPE_LOCAL,
71
-		self::PROPERTY_BIRTHDATE => self::SCOPE_LOCAL,
72
-		self::PROPERTY_DISPLAYNAME => self::SCOPE_FEDERATED,
73
-		self::PROPERTY_EMAIL => self::SCOPE_FEDERATED,
74
-		self::PROPERTY_FEDIVERSE => self::SCOPE_LOCAL,
75
-		self::PROPERTY_HEADLINE => self::SCOPE_LOCAL,
76
-		self::PROPERTY_ORGANISATION => self::SCOPE_LOCAL,
77
-		self::PROPERTY_PHONE => self::SCOPE_LOCAL,
78
-		self::PROPERTY_PRONOUNS => self::SCOPE_FEDERATED,
79
-		self::PROPERTY_ROLE => self::SCOPE_LOCAL,
80
-		self::PROPERTY_TWITTER => self::SCOPE_LOCAL,
81
-		self::PROPERTY_BLUESKY => self::SCOPE_LOCAL,
82
-		self::PROPERTY_WEBSITE => self::SCOPE_LOCAL,
83
-	];
84
-
85
-	public function __construct(
86
-		private IDBConnection $connection,
87
-		private IConfig $config,
88
-		private IEventDispatcher $dispatcher,
89
-		private IJobList $jobList,
90
-		private LoggerInterface $logger,
91
-		private IVerificationToken $verificationToken,
92
-		private IMailer $mailer,
93
-		private Defaults $defaults,
94
-		private IFactory $l10nFactory,
95
-		private IURLGenerator $urlGenerator,
96
-		private ICrypto $crypto,
97
-		private IPhoneNumberUtil $phoneNumberUtil,
98
-		private IClientService $clientService,
99
-	) {
100
-		$this->internalCache = new CappedMemoryCache();
101
-	}
102
-
103
-	/**
104
-	 * @param IAccountProperty[] $properties
105
-	 */
106
-	protected function testValueLengths(array $properties, bool $throwOnData = false): void {
107
-		foreach ($properties as $property) {
108
-			if (strlen($property->getValue()) > 2048) {
109
-				if ($throwOnData) {
110
-					throw new InvalidArgumentException($property->getName());
111
-				} else {
112
-					$property->setValue('');
113
-				}
114
-			}
115
-		}
116
-	}
117
-
118
-	protected function testPropertyScope(IAccountProperty $property, array $allowedScopes, bool $throwOnData): void {
119
-		if ($throwOnData && !in_array($property->getScope(), $allowedScopes, true)) {
120
-			throw new InvalidArgumentException('scope');
121
-		}
122
-
123
-		if (
124
-			$property->getScope() === self::SCOPE_PRIVATE
125
-			&& in_array($property->getName(), [self::PROPERTY_DISPLAYNAME, self::PROPERTY_EMAIL])
126
-		) {
127
-			if ($throwOnData) {
128
-				// v2-private is not available for these fields
129
-				throw new InvalidArgumentException('scope');
130
-			} else {
131
-				// default to local
132
-				$property->setScope(self::SCOPE_LOCAL);
133
-			}
134
-		} else {
135
-			$property->setScope($property->getScope());
136
-		}
137
-	}
138
-
139
-	protected function updateUser(IUser $user, array $data, ?array $oldUserData, bool $throwOnData = false): array {
140
-		if ($oldUserData === null) {
141
-			$oldUserData = $this->getUser($user, false);
142
-		}
143
-
144
-		$updated = true;
145
-
146
-		if ($oldUserData !== $data) {
147
-			$this->updateExistingUser($user, $data, $oldUserData);
148
-		} else {
149
-			// nothing needs to be done if new and old data set are the same
150
-			$updated = false;
151
-		}
152
-
153
-		if ($updated) {
154
-			$this->dispatcher->dispatchTyped(new UserUpdatedEvent(
155
-				$user,
156
-				$data,
157
-			));
158
-		}
159
-
160
-		return $data;
161
-	}
162
-
163
-	/**
164
-	 * delete user from accounts table
165
-	 */
166
-	public function deleteUser(IUser $user): void {
167
-		$uid = $user->getUID();
168
-		$query = $this->connection->getQueryBuilder();
169
-		$query->delete($this->table)
170
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
171
-			->executeStatement();
172
-
173
-		$this->deleteUserData($user);
174
-	}
175
-
176
-	/**
177
-	 * delete user from accounts table
178
-	 */
179
-	public function deleteUserData(IUser $user): void {
180
-		$uid = $user->getUID();
181
-		$query = $this->connection->getQueryBuilder();
182
-		$query->delete($this->dataTable)
183
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
184
-			->executeStatement();
185
-	}
186
-
187
-	/**
188
-	 * get stored data from a given user
189
-	 */
190
-	protected function getUser(IUser $user, bool $insertIfNotExists = true): array {
191
-		$uid = $user->getUID();
192
-		$query = $this->connection->getQueryBuilder();
193
-		$query->select('data')
194
-			->from($this->table)
195
-			->where($query->expr()->eq('uid', $query->createParameter('uid')))
196
-			->setParameter('uid', $uid);
197
-		$result = $query->executeQuery();
198
-		$accountData = $result->fetchAll();
199
-		$result->closeCursor();
200
-
201
-		if (empty($accountData)) {
202
-			$userData = $this->buildDefaultUserRecord($user);
203
-			if ($insertIfNotExists) {
204
-				$this->insertNewUser($user, $userData);
205
-			}
206
-			return $userData;
207
-		}
208
-
209
-		$userDataArray = $this->importFromJson($accountData[0]['data'], $uid);
210
-		if ($userDataArray === null || $userDataArray === []) {
211
-			return $this->buildDefaultUserRecord($user);
212
-		}
213
-
214
-		return $this->addMissingDefaultValues($userDataArray, $this->buildDefaultUserRecord($user));
215
-	}
216
-
217
-	public function searchUsers(string $property, array $values): array {
218
-		// the value col is limited to 255 bytes. It is used for searches only.
219
-		$values = array_map(function (string $value) {
220
-			return Util::shortenMultibyteString($value, 255);
221
-		}, $values);
222
-		$chunks = array_chunk($values, 500);
223
-		$query = $this->connection->getQueryBuilder();
224
-		$query->select('*')
225
-			->from($this->dataTable)
226
-			->where($query->expr()->eq('name', $query->createNamedParameter($property)))
227
-			->andWhere($query->expr()->in('value', $query->createParameter('values')));
228
-
229
-		$matches = [];
230
-		foreach ($chunks as $chunk) {
231
-			$query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
232
-			$result = $query->executeQuery();
233
-
234
-			while ($row = $result->fetch()) {
235
-				$matches[$row['uid']] = $row['value'];
236
-			}
237
-			$result->closeCursor();
238
-		}
239
-
240
-		$result = array_merge($matches, $this->searchUsersForRelatedCollection($property, $values));
241
-
242
-		return array_flip($result);
243
-	}
244
-
245
-	protected function searchUsersForRelatedCollection(string $property, array $values): array {
246
-		return match ($property) {
247
-			IAccountManager::PROPERTY_EMAIL => array_flip($this->searchUsers(IAccountManager::COLLECTION_EMAIL, $values)),
248
-			default => [],
249
-		};
250
-	}
251
-
252
-	/**
253
-	 * check if we need to ask the server for email verification, if yes we create a cronjob
254
-	 */
255
-	protected function checkEmailVerification(IAccount $updatedAccount, array $oldData): void {
256
-		try {
257
-			$property = $updatedAccount->getProperty(self::PROPERTY_EMAIL);
258
-		} catch (PropertyDoesNotExistException $e) {
259
-			return;
260
-		}
261
-
262
-		$oldMailIndex = array_search(self::PROPERTY_EMAIL, array_column($oldData, 'name'), true);
263
-		$oldMail = $oldMailIndex !== false ? $oldData[$oldMailIndex]['value'] : '';
264
-
265
-		if ($oldMail !== $property->getValue()) {
266
-			$this->jobList->add(
267
-				VerifyUserData::class,
268
-				[
269
-					'verificationCode' => '',
270
-					'data' => $property->getValue(),
271
-					'type' => self::PROPERTY_EMAIL,
272
-					'uid' => $updatedAccount->getUser()->getUID(),
273
-					'try' => 0,
274
-					'lastRun' => time()
275
-				]
276
-			);
277
-
278
-			$property->setVerified(self::VERIFICATION_IN_PROGRESS);
279
-		}
280
-	}
281
-
282
-	protected function checkLocalEmailVerification(IAccount $updatedAccount, array $oldData): void {
283
-		$mailCollection = $updatedAccount->getPropertyCollection(self::COLLECTION_EMAIL);
284
-		foreach ($mailCollection->getProperties() as $property) {
285
-			if ($property->getLocallyVerified() !== self::NOT_VERIFIED) {
286
-				continue;
287
-			}
288
-			if ($this->sendEmailVerificationEmail($updatedAccount->getUser(), $property->getValue())) {
289
-				$property->setLocallyVerified(self::VERIFICATION_IN_PROGRESS);
290
-			}
291
-		}
292
-	}
293
-
294
-	protected function sendEmailVerificationEmail(IUser $user, string $email): bool {
295
-		$ref = \substr(hash('sha256', $email), 0, 8);
296
-		$key = $this->crypto->encrypt($email);
297
-		$token = $this->verificationToken->create($user, 'verifyMail' . $ref, $email);
298
-
299
-		$link = $this->urlGenerator->linkToRouteAbsolute(
300
-			'provisioning_api.Verification.verifyMail',
301
-			[
302
-				'userId' => $user->getUID(),
303
-				'token' => $token,
304
-				'key' => $key
305
-			]
306
-		);
307
-
308
-		$emailTemplate = $this->mailer->createEMailTemplate('core.EmailVerification', [
309
-			'link' => $link,
310
-		]);
311
-
312
-		if (!$this->l10n) {
313
-			$this->l10n = $this->l10nFactory->get('core');
314
-		}
315
-
316
-		$emailTemplate->setSubject($this->l10n->t('%s email verification', [$this->defaults->getName()]));
317
-		$emailTemplate->addHeader();
318
-		$emailTemplate->addHeading($this->l10n->t('Email verification'));
319
-
320
-		$emailTemplate->addBodyText(
321
-			htmlspecialchars($this->l10n->t('Click the following button to confirm your email.')),
322
-			$this->l10n->t('Click the following link to confirm your email.')
323
-		);
324
-
325
-		$emailTemplate->addBodyButton(
326
-			htmlspecialchars($this->l10n->t('Confirm your email')),
327
-			$link,
328
-			false
329
-		);
330
-		$emailTemplate->addFooter();
331
-
332
-		try {
333
-			$message = $this->mailer->createMessage();
334
-			$message->setTo([$email => $user->getDisplayName()]);
335
-			$message->setFrom([Util::getDefaultEmailAddress('verification-noreply') => $this->defaults->getName()]);
336
-			$message->useTemplate($emailTemplate);
337
-			$this->mailer->send($message);
338
-		} catch (Exception $e) {
339
-			// Log the exception and continue
340
-			$this->logger->info('Failed to send verification mail', [
341
-				'app' => 'core',
342
-				'exception' => $e
343
-			]);
344
-			return false;
345
-		}
346
-		return true;
347
-	}
348
-
349
-	/**
350
-	 * Make sure that all expected data are set
351
-	 */
352
-	protected function addMissingDefaultValues(array $userData, array $defaultUserData): array {
353
-		foreach ($defaultUserData as $defaultDataItem) {
354
-			// If property does not exist, initialize it
355
-			$userDataIndex = array_search($defaultDataItem['name'], array_column($userData, 'name'));
356
-			if ($userDataIndex === false) {
357
-				$userData[] = $defaultDataItem;
358
-				continue;
359
-			}
360
-
361
-			// Merge and extend default missing values
362
-			$userData[$userDataIndex] = array_merge($defaultDataItem, $userData[$userDataIndex]);
363
-		}
364
-
365
-		return $userData;
366
-	}
367
-
368
-	protected function updateVerificationStatus(IAccount $updatedAccount, array $oldData): void {
369
-		static $propertiesVerifiableByLookupServer = [
370
-			self::PROPERTY_TWITTER,
371
-			self::PROPERTY_FEDIVERSE,
372
-			self::PROPERTY_WEBSITE,
373
-			self::PROPERTY_EMAIL,
374
-		];
375
-
376
-		foreach ($propertiesVerifiableByLookupServer as $propertyName) {
377
-			try {
378
-				$property = $updatedAccount->getProperty($propertyName);
379
-			} catch (PropertyDoesNotExistException $e) {
380
-				continue;
381
-			}
382
-			$wasVerified = isset($oldData[$propertyName])
383
-				&& isset($oldData[$propertyName]['verified'])
384
-				&& $oldData[$propertyName]['verified'] === self::VERIFIED;
385
-			if ((!isset($oldData[$propertyName])
386
-					|| !isset($oldData[$propertyName]['value'])
387
-					|| $property->getValue() !== $oldData[$propertyName]['value'])
388
-				&& ($property->getVerified() !== self::NOT_VERIFIED
389
-					|| $wasVerified)
390
-			) {
391
-				$property->setVerified(self::NOT_VERIFIED);
392
-			}
393
-		}
394
-	}
395
-
396
-	/**
397
-	 * add new user to accounts table
398
-	 */
399
-	protected function insertNewUser(IUser $user, array $data): void {
400
-		$uid = $user->getUID();
401
-		$jsonEncodedData = $this->prepareJson($data);
402
-		$query = $this->connection->getQueryBuilder();
403
-		$query->insert($this->table)
404
-			->values(
405
-				[
406
-					'uid' => $query->createNamedParameter($uid),
407
-					'data' => $query->createNamedParameter($jsonEncodedData),
408
-				]
409
-			)
410
-			->executeStatement();
411
-
412
-		$this->deleteUserData($user);
413
-		$this->writeUserData($user, $data);
414
-	}
415
-
416
-	protected function prepareJson(array $data): string {
417
-		$preparedData = [];
418
-		foreach ($data as $dataRow) {
419
-			$propertyName = $dataRow['name'];
420
-			unset($dataRow['name']);
421
-
422
-			if (isset($dataRow['locallyVerified']) && $dataRow['locallyVerified'] === self::NOT_VERIFIED) {
423
-				// do not write default value, save DB space
424
-				unset($dataRow['locallyVerified']);
425
-			}
426
-
427
-			if (!$this->isCollection($propertyName)) {
428
-				$preparedData[$propertyName] = $dataRow;
429
-				continue;
430
-			}
431
-			if (!isset($preparedData[$propertyName])) {
432
-				$preparedData[$propertyName] = [];
433
-			}
434
-			$preparedData[$propertyName][] = $dataRow;
435
-		}
436
-		return json_encode($preparedData);
437
-	}
438
-
439
-	protected function importFromJson(string $json, string $userId): ?array {
440
-		$result = [];
441
-		$jsonArray = json_decode($json, true);
442
-		$jsonError = json_last_error();
443
-		if ($jsonError !== JSON_ERROR_NONE) {
444
-			$this->logger->critical(
445
-				'User data of {uid} contained invalid JSON (error {json_error}), hence falling back to a default user record',
446
-				[
447
-					'uid' => $userId,
448
-					'json_error' => $jsonError
449
-				]
450
-			);
451
-			return null;
452
-		}
453
-		foreach ($jsonArray as $propertyName => $row) {
454
-			if (!$this->isCollection($propertyName)) {
455
-				$result[] = array_merge($row, ['name' => $propertyName]);
456
-				continue;
457
-			}
458
-			foreach ($row as $singleRow) {
459
-				$result[] = array_merge($singleRow, ['name' => $propertyName]);
460
-			}
461
-		}
462
-		return $result;
463
-	}
464
-
465
-	/**
466
-	 * Update existing user in accounts table
467
-	 */
468
-	protected function updateExistingUser(IUser $user, array $data, array $oldData): void {
469
-		$uid = $user->getUID();
470
-		$jsonEncodedData = $this->prepareJson($data);
471
-		$query = $this->connection->getQueryBuilder();
472
-		$query->update($this->table)
473
-			->set('data', $query->createNamedParameter($jsonEncodedData))
474
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
475
-			->executeStatement();
476
-
477
-		$this->deleteUserData($user);
478
-		$this->writeUserData($user, $data);
479
-	}
480
-
481
-	protected function writeUserData(IUser $user, array $data): void {
482
-		$query = $this->connection->getQueryBuilder();
483
-		$query->insert($this->dataTable)
484
-			->values(
485
-				[
486
-					'uid' => $query->createNamedParameter($user->getUID()),
487
-					'name' => $query->createParameter('name'),
488
-					'value' => $query->createParameter('value'),
489
-				]
490
-			);
491
-		$this->writeUserDataProperties($query, $data);
492
-	}
493
-
494
-	protected function writeUserDataProperties(IQueryBuilder $query, array $data): void {
495
-		foreach ($data as $property) {
496
-			if ($property['name'] === self::PROPERTY_AVATAR) {
497
-				continue;
498
-			}
499
-
500
-			// the value col is limited to 255 bytes. It is used for searches only.
501
-			$value = $property['value'] ? Util::shortenMultibyteString($property['value'], 255) : '';
502
-
503
-			$query->setParameter('name', $property['name'])
504
-				->setParameter('value', $value);
505
-			$query->executeStatement();
506
-		}
507
-	}
508
-
509
-	/**
510
-	 * build default user record in case not data set exists yet
511
-	 */
512
-	protected function buildDefaultUserRecord(IUser $user): array {
513
-		$scopes = array_merge(self::DEFAULT_SCOPES, array_filter($this->config->getSystemValue('account_manager.default_property_scope', []), static function (string $scope, string $property) {
514
-			return in_array($property, self::ALLOWED_PROPERTIES, true) && in_array($scope, self::ALLOWED_SCOPES, true);
515
-		}, ARRAY_FILTER_USE_BOTH));
516
-
517
-		return [
518
-			[
519
-				'name' => self::PROPERTY_DISPLAYNAME,
520
-				'value' => $user->getDisplayName(),
521
-				// Display name must be at least SCOPE_LOCAL
522
-				'scope' => $scopes[self::PROPERTY_DISPLAYNAME] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_DISPLAYNAME],
523
-				'verified' => self::NOT_VERIFIED,
524
-			],
525
-
526
-			[
527
-				'name' => self::PROPERTY_ADDRESS,
528
-				'value' => '',
529
-				'scope' => $scopes[self::PROPERTY_ADDRESS],
530
-				'verified' => self::NOT_VERIFIED,
531
-			],
532
-
533
-			[
534
-				'name' => self::PROPERTY_WEBSITE,
535
-				'value' => '',
536
-				'scope' => $scopes[self::PROPERTY_WEBSITE],
537
-				'verified' => self::NOT_VERIFIED,
538
-			],
539
-
540
-			[
541
-				'name' => self::PROPERTY_EMAIL,
542
-				'value' => $user->getEMailAddress(),
543
-				// Email must be at least SCOPE_LOCAL
544
-				'scope' => $scopes[self::PROPERTY_EMAIL] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_EMAIL],
545
-				'verified' => self::NOT_VERIFIED,
546
-			],
547
-
548
-			[
549
-				'name' => self::PROPERTY_AVATAR,
550
-				'scope' => $scopes[self::PROPERTY_AVATAR],
551
-			],
552
-
553
-			[
554
-				'name' => self::PROPERTY_PHONE,
555
-				'value' => '',
556
-				'scope' => $scopes[self::PROPERTY_PHONE],
557
-				'verified' => self::NOT_VERIFIED,
558
-			],
559
-
560
-			[
561
-				'name' => self::PROPERTY_TWITTER,
562
-				'value' => '',
563
-				'scope' => $scopes[self::PROPERTY_TWITTER],
564
-				'verified' => self::NOT_VERIFIED,
565
-			],
566
-
567
-			[
568
-				'name' => self::PROPERTY_BLUESKY,
569
-				'value' => '',
570
-				'scope' => $scopes[self::PROPERTY_BLUESKY],
571
-				'verified' => self::NOT_VERIFIED,
572
-			],
573
-
574
-			[
575
-				'name' => self::PROPERTY_FEDIVERSE,
576
-				'value' => '',
577
-				'scope' => $scopes[self::PROPERTY_FEDIVERSE],
578
-				'verified' => self::NOT_VERIFIED,
579
-			],
580
-
581
-			[
582
-				'name' => self::PROPERTY_ORGANISATION,
583
-				'value' => '',
584
-				'scope' => $scopes[self::PROPERTY_ORGANISATION],
585
-			],
586
-
587
-			[
588
-				'name' => self::PROPERTY_ROLE,
589
-				'value' => '',
590
-				'scope' => $scopes[self::PROPERTY_ROLE],
591
-			],
592
-
593
-			[
594
-				'name' => self::PROPERTY_HEADLINE,
595
-				'value' => '',
596
-				'scope' => $scopes[self::PROPERTY_HEADLINE],
597
-			],
598
-
599
-			[
600
-				'name' => self::PROPERTY_BIOGRAPHY,
601
-				'value' => '',
602
-				'scope' => $scopes[self::PROPERTY_BIOGRAPHY],
603
-			],
604
-
605
-			[
606
-				'name' => self::PROPERTY_BIRTHDATE,
607
-				'value' => '',
608
-				'scope' => $scopes[self::PROPERTY_BIRTHDATE],
609
-			],
610
-
611
-			[
612
-				'name' => self::PROPERTY_PROFILE_ENABLED,
613
-				'value' => $this->isProfileEnabledByDefault($this->config) ? '1' : '0',
614
-			],
615
-
616
-			[
617
-				'name' => self::PROPERTY_PRONOUNS,
618
-				'value' => '',
619
-				'scope' => $scopes[self::PROPERTY_PRONOUNS],
620
-			],
621
-		];
622
-	}
623
-
624
-	private function arrayDataToCollection(IAccount $account, array $data): IAccountPropertyCollection {
625
-		$collection = $account->getPropertyCollection($data['name']);
626
-
627
-		$p = new AccountProperty(
628
-			$data['name'],
629
-			$data['value'] ?? '',
630
-			$data['scope'] ?? self::SCOPE_LOCAL,
631
-			$data['verified'] ?? self::NOT_VERIFIED,
632
-			''
633
-		);
634
-		$p->setLocallyVerified($data['locallyVerified'] ?? self::NOT_VERIFIED);
635
-		$collection->addProperty($p);
636
-
637
-		return $collection;
638
-	}
639
-
640
-	private function parseAccountData(IUser $user, $data): Account {
641
-		$account = new Account($user);
642
-		foreach ($data as $accountData) {
643
-			if ($this->isCollection($accountData['name'])) {
644
-				$account->setPropertyCollection($this->arrayDataToCollection($account, $accountData));
645
-			} else {
646
-				$account->setProperty($accountData['name'], $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
647
-				if (isset($accountData['locallyVerified'])) {
648
-					$property = $account->getProperty($accountData['name']);
649
-					$property->setLocallyVerified($accountData['locallyVerified']);
650
-				}
651
-			}
652
-		}
653
-		return $account;
654
-	}
655
-
656
-	public function getAccount(IUser $user): IAccount {
657
-		$cached = $this->internalCache->get($user->getUID());
658
-		if ($cached !== null) {
659
-			return $cached;
660
-		}
661
-		$account = $this->parseAccountData($user, $this->getUser($user));
662
-		if ($user->getBackend() instanceof IGetDisplayNameBackend) {
663
-			$property = $account->getProperty(self::PROPERTY_DISPLAYNAME);
664
-			$account->setProperty(self::PROPERTY_DISPLAYNAME, $user->getDisplayName(), $property->getScope(), $property->getVerified());
665
-		}
666
-		$this->internalCache->set($user->getUID(), $account);
667
-		return $account;
668
-	}
669
-
670
-	/**
671
-	 * Converts value (phone number) in E.164 format when it was a valid number
672
-	 * @throws InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code
673
-	 */
674
-	protected function sanitizePropertyPhoneNumber(IAccountProperty $property): void {
675
-		$defaultRegion = $this->config->getSystemValueString('default_phone_region', '');
676
-
677
-		if ($defaultRegion === '') {
678
-			// When no default region is set, only +49… numbers are valid
679
-			if (!str_starts_with($property->getValue(), '+')) {
680
-				throw new InvalidArgumentException(self::PROPERTY_PHONE);
681
-			}
682
-
683
-			$defaultRegion = 'EN';
684
-		}
685
-
686
-		$phoneNumber = $this->phoneNumberUtil->convertToStandardFormat($property->getValue(), $defaultRegion);
687
-		if ($phoneNumber === null) {
688
-			throw new InvalidArgumentException(self::PROPERTY_PHONE);
689
-		}
690
-		$property->setValue($phoneNumber);
691
-	}
692
-
693
-	/**
694
-	 * @throws InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty
695
-	 */
696
-	private function sanitizePropertyWebsite(IAccountProperty $property): void {
697
-		$parts = parse_url($property->getValue());
698
-		if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) {
699
-			throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
700
-		}
701
-
702
-		if (!isset($parts['host']) || $parts['host'] === '') {
703
-			throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
704
-		}
705
-	}
706
-
707
-	/**
708
-	 * @throws InvalidArgumentException If the property value is not a valid user handle according to X's rules
709
-	 */
710
-	private function sanitizePropertyTwitter(IAccountProperty $property): void {
711
-		if ($property->getName() === self::PROPERTY_TWITTER) {
712
-			$matches = [];
713
-			// twitter handles only contain alpha numeric characters and the underscore and must not be longer than 15 characters
714
-			if (preg_match('/^@?([a-zA-Z0-9_]{2,15})$/', $property->getValue(), $matches) !== 1) {
715
-				throw new InvalidArgumentException(self::PROPERTY_TWITTER);
716
-			}
717
-
718
-			// drop the leading @ if any to make it the valid handle
719
-			$property->setValue($matches[1]);
720
-
721
-		}
722
-	}
723
-
724
-	private function validateBlueSkyHandle(string $text): bool {
725
-		if ($text === '') {
726
-			return true;
727
-		}
728
-
729
-		$lowerText = strtolower($text);
730
-
731
-		if ($lowerText === 'bsky.social') {
732
-			// "bsky.social" itself is not a valid handle
733
-			return false;
734
-		}
735
-
736
-		if (str_ends_with($lowerText, '.bsky.social')) {
737
-			$parts = explode('.', $lowerText);
738
-
739
-			// Must be exactly: username.bsky.social → 3 parts
740
-			if (count($parts) !== 3 || $parts[1] !== 'bsky' || $parts[2] !== 'social') {
741
-				return false;
742
-			}
743
-
744
-			$username = $parts[0];
745
-
746
-			// Must be 3–18 chars, alphanumeric/hyphen, no start/end hyphen
747
-			return preg_match('/^[a-z0-9][a-z0-9-]{2,17}$/', $username) === 1;
748
-		}
749
-
750
-		// Allow custom domains (Bluesky handle via personal domain)
751
-		return filter_var($text, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false;
752
-	}
753
-
754
-
755
-	private function sanitizePropertyBluesky(IAccountProperty $property): void {
756
-		if ($property->getName() === self::PROPERTY_BLUESKY) {
757
-			if (!$this->validateBlueSkyHandle($property->getValue())) {
758
-				throw new InvalidArgumentException(self::PROPERTY_BLUESKY);
759
-			}
760
-
761
-			$property->setValue($property->getValue());
762
-		}
763
-	}
764
-
765
-	/**
766
-	 * @throws InvalidArgumentException If the property value is not a valid fediverse handle (username@instance where instance is a valid domain)
767
-	 */
768
-	private function sanitizePropertyFediverse(IAccountProperty $property): void {
769
-		if ($property->getName() === self::PROPERTY_FEDIVERSE) {
770
-			$matches = [];
771
-			if (preg_match('/^@?([^@\s\/\\\]+)@([^\s\/\\\]+)$/', trim($property->getValue()), $matches) !== 1) {
772
-				throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
773
-			}
774
-
775
-			[, $username, $instance] = $matches;
776
-			$validated = filter_var($instance, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
777
-			if ($validated !== $instance) {
778
-				throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
779
-			}
780
-
781
-			if ($this->config->getSystemValueBool('has_internet_connection', true)) {
782
-				$client = $this->clientService->newClient();
783
-
784
-				try {
785
-					// try the public account lookup API of mastodon
786
-					$response = $client->get("https://{$instance}/.well-known/webfinger?resource=acct:{$username}@{$instance}");
787
-					// should be a json response with account information
788
-					$data = $response->getBody();
789
-					if (is_resource($data)) {
790
-						$data = stream_get_contents($data);
791
-					}
792
-					$decoded = json_decode($data, true);
793
-					// ensure the username is the same the user passed
794
-					// in this case we can assume this is a valid fediverse server and account
795
-					if (!is_array($decoded) || ($decoded['subject'] ?? '') !== "acct:{$username}@{$instance}") {
796
-						throw new InvalidArgumentException();
797
-					}
798
-					// check for activitypub link
799
-					if (is_array($decoded['links']) && isset($decoded['links'])) {
800
-						$found = false;
801
-						foreach ($decoded['links'] as $link) {
802
-							// have application/activity+json or application/ld+json
803
-							if (isset($link['type']) && (
804
-								$link['type'] === 'application/activity+json'
805
-								|| $link['type'] === 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
806
-							)) {
807
-								$found = true;
808
-								break;
809
-							}
810
-						}
811
-						if (!$found) {
812
-							throw new InvalidArgumentException();
813
-						}
814
-					}
815
-				} catch (InvalidArgumentException) {
816
-					throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
817
-				} catch (\Exception $error) {
818
-					$this->logger->error('Could not verify fediverse account', ['exception' => $error, 'instance' => $instance]);
819
-					throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
820
-				}
821
-			}
822
-
823
-			$property->setValue("$username@$instance");
824
-		}
825
-	}
826
-
827
-	public function updateAccount(IAccount $account): void {
828
-		$this->testValueLengths(iterator_to_array($account->getAllProperties()), true);
829
-		try {
830
-			$property = $account->getProperty(self::PROPERTY_PHONE);
831
-			if ($property->getValue() !== '') {
832
-				$this->sanitizePropertyPhoneNumber($property);
833
-			}
834
-		} catch (PropertyDoesNotExistException $e) {
835
-			//  valid case, nothing to do
836
-		}
837
-
838
-		try {
839
-			$property = $account->getProperty(self::PROPERTY_WEBSITE);
840
-			if ($property->getValue() !== '') {
841
-				$this->sanitizePropertyWebsite($property);
842
-			}
843
-		} catch (PropertyDoesNotExistException $e) {
844
-			//  valid case, nothing to do
845
-		}
846
-
847
-		try {
848
-			$property = $account->getProperty(self::PROPERTY_TWITTER);
849
-			if ($property->getValue() !== '') {
850
-				$this->sanitizePropertyTwitter($property);
851
-			}
852
-		} catch (PropertyDoesNotExistException $e) {
853
-			//  valid case, nothing to do
854
-		}
855
-
856
-		try {
857
-			$property = $account->getProperty(self::PROPERTY_BLUESKY);
858
-			if ($property->getValue() !== '') {
859
-				$this->sanitizePropertyBluesky($property);
860
-			}
861
-		} catch (PropertyDoesNotExistException $e) {
862
-			//  valid case, nothing to do
863
-		}
864
-
865
-		try {
866
-			$property = $account->getProperty(self::PROPERTY_FEDIVERSE);
867
-			if ($property->getValue() !== '') {
868
-				$this->sanitizePropertyFediverse($property);
869
-			}
870
-		} catch (PropertyDoesNotExistException $e) {
871
-			//  valid case, nothing to do
872
-		}
873
-
874
-		foreach ($account->getAllProperties() as $property) {
875
-			$this->testPropertyScope($property, self::ALLOWED_SCOPES, true);
876
-		}
877
-
878
-		$oldData = $this->getUser($account->getUser(), false);
879
-		$this->updateVerificationStatus($account, $oldData);
880
-		$this->checkEmailVerification($account, $oldData);
881
-		$this->checkLocalEmailVerification($account, $oldData);
882
-
883
-		$data = [];
884
-		foreach ($account->getAllProperties() as $property) {
885
-			/** @var IAccountProperty $property */
886
-			$data[] = [
887
-				'name' => $property->getName(),
888
-				'value' => $property->getValue(),
889
-				'scope' => $property->getScope(),
890
-				'verified' => $property->getVerified(),
891
-				'locallyVerified' => $property->getLocallyVerified(),
892
-			];
893
-		}
894
-
895
-		$this->updateUser($account->getUser(), $data, $oldData, true);
896
-		$this->internalCache->set($account->getUser()->getUID(), $account);
897
-	}
55
+    use TAccountsHelper;
56
+
57
+    use TProfileHelper;
58
+
59
+    private string $table = 'accounts';
60
+    private string $dataTable = 'accounts_data';
61
+    private ?IL10N $l10n = null;
62
+    private CappedMemoryCache $internalCache;
63
+
64
+    /**
65
+     * The list of default scopes for each property.
66
+     */
67
+    public const DEFAULT_SCOPES = [
68
+        self::PROPERTY_ADDRESS => self::SCOPE_LOCAL,
69
+        self::PROPERTY_AVATAR => self::SCOPE_FEDERATED,
70
+        self::PROPERTY_BIOGRAPHY => self::SCOPE_LOCAL,
71
+        self::PROPERTY_BIRTHDATE => self::SCOPE_LOCAL,
72
+        self::PROPERTY_DISPLAYNAME => self::SCOPE_FEDERATED,
73
+        self::PROPERTY_EMAIL => self::SCOPE_FEDERATED,
74
+        self::PROPERTY_FEDIVERSE => self::SCOPE_LOCAL,
75
+        self::PROPERTY_HEADLINE => self::SCOPE_LOCAL,
76
+        self::PROPERTY_ORGANISATION => self::SCOPE_LOCAL,
77
+        self::PROPERTY_PHONE => self::SCOPE_LOCAL,
78
+        self::PROPERTY_PRONOUNS => self::SCOPE_FEDERATED,
79
+        self::PROPERTY_ROLE => self::SCOPE_LOCAL,
80
+        self::PROPERTY_TWITTER => self::SCOPE_LOCAL,
81
+        self::PROPERTY_BLUESKY => self::SCOPE_LOCAL,
82
+        self::PROPERTY_WEBSITE => self::SCOPE_LOCAL,
83
+    ];
84
+
85
+    public function __construct(
86
+        private IDBConnection $connection,
87
+        private IConfig $config,
88
+        private IEventDispatcher $dispatcher,
89
+        private IJobList $jobList,
90
+        private LoggerInterface $logger,
91
+        private IVerificationToken $verificationToken,
92
+        private IMailer $mailer,
93
+        private Defaults $defaults,
94
+        private IFactory $l10nFactory,
95
+        private IURLGenerator $urlGenerator,
96
+        private ICrypto $crypto,
97
+        private IPhoneNumberUtil $phoneNumberUtil,
98
+        private IClientService $clientService,
99
+    ) {
100
+        $this->internalCache = new CappedMemoryCache();
101
+    }
102
+
103
+    /**
104
+     * @param IAccountProperty[] $properties
105
+     */
106
+    protected function testValueLengths(array $properties, bool $throwOnData = false): void {
107
+        foreach ($properties as $property) {
108
+            if (strlen($property->getValue()) > 2048) {
109
+                if ($throwOnData) {
110
+                    throw new InvalidArgumentException($property->getName());
111
+                } else {
112
+                    $property->setValue('');
113
+                }
114
+            }
115
+        }
116
+    }
117
+
118
+    protected function testPropertyScope(IAccountProperty $property, array $allowedScopes, bool $throwOnData): void {
119
+        if ($throwOnData && !in_array($property->getScope(), $allowedScopes, true)) {
120
+            throw new InvalidArgumentException('scope');
121
+        }
122
+
123
+        if (
124
+            $property->getScope() === self::SCOPE_PRIVATE
125
+            && in_array($property->getName(), [self::PROPERTY_DISPLAYNAME, self::PROPERTY_EMAIL])
126
+        ) {
127
+            if ($throwOnData) {
128
+                // v2-private is not available for these fields
129
+                throw new InvalidArgumentException('scope');
130
+            } else {
131
+                // default to local
132
+                $property->setScope(self::SCOPE_LOCAL);
133
+            }
134
+        } else {
135
+            $property->setScope($property->getScope());
136
+        }
137
+    }
138
+
139
+    protected function updateUser(IUser $user, array $data, ?array $oldUserData, bool $throwOnData = false): array {
140
+        if ($oldUserData === null) {
141
+            $oldUserData = $this->getUser($user, false);
142
+        }
143
+
144
+        $updated = true;
145
+
146
+        if ($oldUserData !== $data) {
147
+            $this->updateExistingUser($user, $data, $oldUserData);
148
+        } else {
149
+            // nothing needs to be done if new and old data set are the same
150
+            $updated = false;
151
+        }
152
+
153
+        if ($updated) {
154
+            $this->dispatcher->dispatchTyped(new UserUpdatedEvent(
155
+                $user,
156
+                $data,
157
+            ));
158
+        }
159
+
160
+        return $data;
161
+    }
162
+
163
+    /**
164
+     * delete user from accounts table
165
+     */
166
+    public function deleteUser(IUser $user): void {
167
+        $uid = $user->getUID();
168
+        $query = $this->connection->getQueryBuilder();
169
+        $query->delete($this->table)
170
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
171
+            ->executeStatement();
172
+
173
+        $this->deleteUserData($user);
174
+    }
175
+
176
+    /**
177
+     * delete user from accounts table
178
+     */
179
+    public function deleteUserData(IUser $user): void {
180
+        $uid = $user->getUID();
181
+        $query = $this->connection->getQueryBuilder();
182
+        $query->delete($this->dataTable)
183
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
184
+            ->executeStatement();
185
+    }
186
+
187
+    /**
188
+     * get stored data from a given user
189
+     */
190
+    protected function getUser(IUser $user, bool $insertIfNotExists = true): array {
191
+        $uid = $user->getUID();
192
+        $query = $this->connection->getQueryBuilder();
193
+        $query->select('data')
194
+            ->from($this->table)
195
+            ->where($query->expr()->eq('uid', $query->createParameter('uid')))
196
+            ->setParameter('uid', $uid);
197
+        $result = $query->executeQuery();
198
+        $accountData = $result->fetchAll();
199
+        $result->closeCursor();
200
+
201
+        if (empty($accountData)) {
202
+            $userData = $this->buildDefaultUserRecord($user);
203
+            if ($insertIfNotExists) {
204
+                $this->insertNewUser($user, $userData);
205
+            }
206
+            return $userData;
207
+        }
208
+
209
+        $userDataArray = $this->importFromJson($accountData[0]['data'], $uid);
210
+        if ($userDataArray === null || $userDataArray === []) {
211
+            return $this->buildDefaultUserRecord($user);
212
+        }
213
+
214
+        return $this->addMissingDefaultValues($userDataArray, $this->buildDefaultUserRecord($user));
215
+    }
216
+
217
+    public function searchUsers(string $property, array $values): array {
218
+        // the value col is limited to 255 bytes. It is used for searches only.
219
+        $values = array_map(function (string $value) {
220
+            return Util::shortenMultibyteString($value, 255);
221
+        }, $values);
222
+        $chunks = array_chunk($values, 500);
223
+        $query = $this->connection->getQueryBuilder();
224
+        $query->select('*')
225
+            ->from($this->dataTable)
226
+            ->where($query->expr()->eq('name', $query->createNamedParameter($property)))
227
+            ->andWhere($query->expr()->in('value', $query->createParameter('values')));
228
+
229
+        $matches = [];
230
+        foreach ($chunks as $chunk) {
231
+            $query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
232
+            $result = $query->executeQuery();
233
+
234
+            while ($row = $result->fetch()) {
235
+                $matches[$row['uid']] = $row['value'];
236
+            }
237
+            $result->closeCursor();
238
+        }
239
+
240
+        $result = array_merge($matches, $this->searchUsersForRelatedCollection($property, $values));
241
+
242
+        return array_flip($result);
243
+    }
244
+
245
+    protected function searchUsersForRelatedCollection(string $property, array $values): array {
246
+        return match ($property) {
247
+            IAccountManager::PROPERTY_EMAIL => array_flip($this->searchUsers(IAccountManager::COLLECTION_EMAIL, $values)),
248
+            default => [],
249
+        };
250
+    }
251
+
252
+    /**
253
+     * check if we need to ask the server for email verification, if yes we create a cronjob
254
+     */
255
+    protected function checkEmailVerification(IAccount $updatedAccount, array $oldData): void {
256
+        try {
257
+            $property = $updatedAccount->getProperty(self::PROPERTY_EMAIL);
258
+        } catch (PropertyDoesNotExistException $e) {
259
+            return;
260
+        }
261
+
262
+        $oldMailIndex = array_search(self::PROPERTY_EMAIL, array_column($oldData, 'name'), true);
263
+        $oldMail = $oldMailIndex !== false ? $oldData[$oldMailIndex]['value'] : '';
264
+
265
+        if ($oldMail !== $property->getValue()) {
266
+            $this->jobList->add(
267
+                VerifyUserData::class,
268
+                [
269
+                    'verificationCode' => '',
270
+                    'data' => $property->getValue(),
271
+                    'type' => self::PROPERTY_EMAIL,
272
+                    'uid' => $updatedAccount->getUser()->getUID(),
273
+                    'try' => 0,
274
+                    'lastRun' => time()
275
+                ]
276
+            );
277
+
278
+            $property->setVerified(self::VERIFICATION_IN_PROGRESS);
279
+        }
280
+    }
281
+
282
+    protected function checkLocalEmailVerification(IAccount $updatedAccount, array $oldData): void {
283
+        $mailCollection = $updatedAccount->getPropertyCollection(self::COLLECTION_EMAIL);
284
+        foreach ($mailCollection->getProperties() as $property) {
285
+            if ($property->getLocallyVerified() !== self::NOT_VERIFIED) {
286
+                continue;
287
+            }
288
+            if ($this->sendEmailVerificationEmail($updatedAccount->getUser(), $property->getValue())) {
289
+                $property->setLocallyVerified(self::VERIFICATION_IN_PROGRESS);
290
+            }
291
+        }
292
+    }
293
+
294
+    protected function sendEmailVerificationEmail(IUser $user, string $email): bool {
295
+        $ref = \substr(hash('sha256', $email), 0, 8);
296
+        $key = $this->crypto->encrypt($email);
297
+        $token = $this->verificationToken->create($user, 'verifyMail' . $ref, $email);
298
+
299
+        $link = $this->urlGenerator->linkToRouteAbsolute(
300
+            'provisioning_api.Verification.verifyMail',
301
+            [
302
+                'userId' => $user->getUID(),
303
+                'token' => $token,
304
+                'key' => $key
305
+            ]
306
+        );
307
+
308
+        $emailTemplate = $this->mailer->createEMailTemplate('core.EmailVerification', [
309
+            'link' => $link,
310
+        ]);
311
+
312
+        if (!$this->l10n) {
313
+            $this->l10n = $this->l10nFactory->get('core');
314
+        }
315
+
316
+        $emailTemplate->setSubject($this->l10n->t('%s email verification', [$this->defaults->getName()]));
317
+        $emailTemplate->addHeader();
318
+        $emailTemplate->addHeading($this->l10n->t('Email verification'));
319
+
320
+        $emailTemplate->addBodyText(
321
+            htmlspecialchars($this->l10n->t('Click the following button to confirm your email.')),
322
+            $this->l10n->t('Click the following link to confirm your email.')
323
+        );
324
+
325
+        $emailTemplate->addBodyButton(
326
+            htmlspecialchars($this->l10n->t('Confirm your email')),
327
+            $link,
328
+            false
329
+        );
330
+        $emailTemplate->addFooter();
331
+
332
+        try {
333
+            $message = $this->mailer->createMessage();
334
+            $message->setTo([$email => $user->getDisplayName()]);
335
+            $message->setFrom([Util::getDefaultEmailAddress('verification-noreply') => $this->defaults->getName()]);
336
+            $message->useTemplate($emailTemplate);
337
+            $this->mailer->send($message);
338
+        } catch (Exception $e) {
339
+            // Log the exception and continue
340
+            $this->logger->info('Failed to send verification mail', [
341
+                'app' => 'core',
342
+                'exception' => $e
343
+            ]);
344
+            return false;
345
+        }
346
+        return true;
347
+    }
348
+
349
+    /**
350
+     * Make sure that all expected data are set
351
+     */
352
+    protected function addMissingDefaultValues(array $userData, array $defaultUserData): array {
353
+        foreach ($defaultUserData as $defaultDataItem) {
354
+            // If property does not exist, initialize it
355
+            $userDataIndex = array_search($defaultDataItem['name'], array_column($userData, 'name'));
356
+            if ($userDataIndex === false) {
357
+                $userData[] = $defaultDataItem;
358
+                continue;
359
+            }
360
+
361
+            // Merge and extend default missing values
362
+            $userData[$userDataIndex] = array_merge($defaultDataItem, $userData[$userDataIndex]);
363
+        }
364
+
365
+        return $userData;
366
+    }
367
+
368
+    protected function updateVerificationStatus(IAccount $updatedAccount, array $oldData): void {
369
+        static $propertiesVerifiableByLookupServer = [
370
+            self::PROPERTY_TWITTER,
371
+            self::PROPERTY_FEDIVERSE,
372
+            self::PROPERTY_WEBSITE,
373
+            self::PROPERTY_EMAIL,
374
+        ];
375
+
376
+        foreach ($propertiesVerifiableByLookupServer as $propertyName) {
377
+            try {
378
+                $property = $updatedAccount->getProperty($propertyName);
379
+            } catch (PropertyDoesNotExistException $e) {
380
+                continue;
381
+            }
382
+            $wasVerified = isset($oldData[$propertyName])
383
+                && isset($oldData[$propertyName]['verified'])
384
+                && $oldData[$propertyName]['verified'] === self::VERIFIED;
385
+            if ((!isset($oldData[$propertyName])
386
+                    || !isset($oldData[$propertyName]['value'])
387
+                    || $property->getValue() !== $oldData[$propertyName]['value'])
388
+                && ($property->getVerified() !== self::NOT_VERIFIED
389
+                    || $wasVerified)
390
+            ) {
391
+                $property->setVerified(self::NOT_VERIFIED);
392
+            }
393
+        }
394
+    }
395
+
396
+    /**
397
+     * add new user to accounts table
398
+     */
399
+    protected function insertNewUser(IUser $user, array $data): void {
400
+        $uid = $user->getUID();
401
+        $jsonEncodedData = $this->prepareJson($data);
402
+        $query = $this->connection->getQueryBuilder();
403
+        $query->insert($this->table)
404
+            ->values(
405
+                [
406
+                    'uid' => $query->createNamedParameter($uid),
407
+                    'data' => $query->createNamedParameter($jsonEncodedData),
408
+                ]
409
+            )
410
+            ->executeStatement();
411
+
412
+        $this->deleteUserData($user);
413
+        $this->writeUserData($user, $data);
414
+    }
415
+
416
+    protected function prepareJson(array $data): string {
417
+        $preparedData = [];
418
+        foreach ($data as $dataRow) {
419
+            $propertyName = $dataRow['name'];
420
+            unset($dataRow['name']);
421
+
422
+            if (isset($dataRow['locallyVerified']) && $dataRow['locallyVerified'] === self::NOT_VERIFIED) {
423
+                // do not write default value, save DB space
424
+                unset($dataRow['locallyVerified']);
425
+            }
426
+
427
+            if (!$this->isCollection($propertyName)) {
428
+                $preparedData[$propertyName] = $dataRow;
429
+                continue;
430
+            }
431
+            if (!isset($preparedData[$propertyName])) {
432
+                $preparedData[$propertyName] = [];
433
+            }
434
+            $preparedData[$propertyName][] = $dataRow;
435
+        }
436
+        return json_encode($preparedData);
437
+    }
438
+
439
+    protected function importFromJson(string $json, string $userId): ?array {
440
+        $result = [];
441
+        $jsonArray = json_decode($json, true);
442
+        $jsonError = json_last_error();
443
+        if ($jsonError !== JSON_ERROR_NONE) {
444
+            $this->logger->critical(
445
+                'User data of {uid} contained invalid JSON (error {json_error}), hence falling back to a default user record',
446
+                [
447
+                    'uid' => $userId,
448
+                    'json_error' => $jsonError
449
+                ]
450
+            );
451
+            return null;
452
+        }
453
+        foreach ($jsonArray as $propertyName => $row) {
454
+            if (!$this->isCollection($propertyName)) {
455
+                $result[] = array_merge($row, ['name' => $propertyName]);
456
+                continue;
457
+            }
458
+            foreach ($row as $singleRow) {
459
+                $result[] = array_merge($singleRow, ['name' => $propertyName]);
460
+            }
461
+        }
462
+        return $result;
463
+    }
464
+
465
+    /**
466
+     * Update existing user in accounts table
467
+     */
468
+    protected function updateExistingUser(IUser $user, array $data, array $oldData): void {
469
+        $uid = $user->getUID();
470
+        $jsonEncodedData = $this->prepareJson($data);
471
+        $query = $this->connection->getQueryBuilder();
472
+        $query->update($this->table)
473
+            ->set('data', $query->createNamedParameter($jsonEncodedData))
474
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
475
+            ->executeStatement();
476
+
477
+        $this->deleteUserData($user);
478
+        $this->writeUserData($user, $data);
479
+    }
480
+
481
+    protected function writeUserData(IUser $user, array $data): void {
482
+        $query = $this->connection->getQueryBuilder();
483
+        $query->insert($this->dataTable)
484
+            ->values(
485
+                [
486
+                    'uid' => $query->createNamedParameter($user->getUID()),
487
+                    'name' => $query->createParameter('name'),
488
+                    'value' => $query->createParameter('value'),
489
+                ]
490
+            );
491
+        $this->writeUserDataProperties($query, $data);
492
+    }
493
+
494
+    protected function writeUserDataProperties(IQueryBuilder $query, array $data): void {
495
+        foreach ($data as $property) {
496
+            if ($property['name'] === self::PROPERTY_AVATAR) {
497
+                continue;
498
+            }
499
+
500
+            // the value col is limited to 255 bytes. It is used for searches only.
501
+            $value = $property['value'] ? Util::shortenMultibyteString($property['value'], 255) : '';
502
+
503
+            $query->setParameter('name', $property['name'])
504
+                ->setParameter('value', $value);
505
+            $query->executeStatement();
506
+        }
507
+    }
508
+
509
+    /**
510
+     * build default user record in case not data set exists yet
511
+     */
512
+    protected function buildDefaultUserRecord(IUser $user): array {
513
+        $scopes = array_merge(self::DEFAULT_SCOPES, array_filter($this->config->getSystemValue('account_manager.default_property_scope', []), static function (string $scope, string $property) {
514
+            return in_array($property, self::ALLOWED_PROPERTIES, true) && in_array($scope, self::ALLOWED_SCOPES, true);
515
+        }, ARRAY_FILTER_USE_BOTH));
516
+
517
+        return [
518
+            [
519
+                'name' => self::PROPERTY_DISPLAYNAME,
520
+                'value' => $user->getDisplayName(),
521
+                // Display name must be at least SCOPE_LOCAL
522
+                'scope' => $scopes[self::PROPERTY_DISPLAYNAME] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_DISPLAYNAME],
523
+                'verified' => self::NOT_VERIFIED,
524
+            ],
525
+
526
+            [
527
+                'name' => self::PROPERTY_ADDRESS,
528
+                'value' => '',
529
+                'scope' => $scopes[self::PROPERTY_ADDRESS],
530
+                'verified' => self::NOT_VERIFIED,
531
+            ],
532
+
533
+            [
534
+                'name' => self::PROPERTY_WEBSITE,
535
+                'value' => '',
536
+                'scope' => $scopes[self::PROPERTY_WEBSITE],
537
+                'verified' => self::NOT_VERIFIED,
538
+            ],
539
+
540
+            [
541
+                'name' => self::PROPERTY_EMAIL,
542
+                'value' => $user->getEMailAddress(),
543
+                // Email must be at least SCOPE_LOCAL
544
+                'scope' => $scopes[self::PROPERTY_EMAIL] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_EMAIL],
545
+                'verified' => self::NOT_VERIFIED,
546
+            ],
547
+
548
+            [
549
+                'name' => self::PROPERTY_AVATAR,
550
+                'scope' => $scopes[self::PROPERTY_AVATAR],
551
+            ],
552
+
553
+            [
554
+                'name' => self::PROPERTY_PHONE,
555
+                'value' => '',
556
+                'scope' => $scopes[self::PROPERTY_PHONE],
557
+                'verified' => self::NOT_VERIFIED,
558
+            ],
559
+
560
+            [
561
+                'name' => self::PROPERTY_TWITTER,
562
+                'value' => '',
563
+                'scope' => $scopes[self::PROPERTY_TWITTER],
564
+                'verified' => self::NOT_VERIFIED,
565
+            ],
566
+
567
+            [
568
+                'name' => self::PROPERTY_BLUESKY,
569
+                'value' => '',
570
+                'scope' => $scopes[self::PROPERTY_BLUESKY],
571
+                'verified' => self::NOT_VERIFIED,
572
+            ],
573
+
574
+            [
575
+                'name' => self::PROPERTY_FEDIVERSE,
576
+                'value' => '',
577
+                'scope' => $scopes[self::PROPERTY_FEDIVERSE],
578
+                'verified' => self::NOT_VERIFIED,
579
+            ],
580
+
581
+            [
582
+                'name' => self::PROPERTY_ORGANISATION,
583
+                'value' => '',
584
+                'scope' => $scopes[self::PROPERTY_ORGANISATION],
585
+            ],
586
+
587
+            [
588
+                'name' => self::PROPERTY_ROLE,
589
+                'value' => '',
590
+                'scope' => $scopes[self::PROPERTY_ROLE],
591
+            ],
592
+
593
+            [
594
+                'name' => self::PROPERTY_HEADLINE,
595
+                'value' => '',
596
+                'scope' => $scopes[self::PROPERTY_HEADLINE],
597
+            ],
598
+
599
+            [
600
+                'name' => self::PROPERTY_BIOGRAPHY,
601
+                'value' => '',
602
+                'scope' => $scopes[self::PROPERTY_BIOGRAPHY],
603
+            ],
604
+
605
+            [
606
+                'name' => self::PROPERTY_BIRTHDATE,
607
+                'value' => '',
608
+                'scope' => $scopes[self::PROPERTY_BIRTHDATE],
609
+            ],
610
+
611
+            [
612
+                'name' => self::PROPERTY_PROFILE_ENABLED,
613
+                'value' => $this->isProfileEnabledByDefault($this->config) ? '1' : '0',
614
+            ],
615
+
616
+            [
617
+                'name' => self::PROPERTY_PRONOUNS,
618
+                'value' => '',
619
+                'scope' => $scopes[self::PROPERTY_PRONOUNS],
620
+            ],
621
+        ];
622
+    }
623
+
624
+    private function arrayDataToCollection(IAccount $account, array $data): IAccountPropertyCollection {
625
+        $collection = $account->getPropertyCollection($data['name']);
626
+
627
+        $p = new AccountProperty(
628
+            $data['name'],
629
+            $data['value'] ?? '',
630
+            $data['scope'] ?? self::SCOPE_LOCAL,
631
+            $data['verified'] ?? self::NOT_VERIFIED,
632
+            ''
633
+        );
634
+        $p->setLocallyVerified($data['locallyVerified'] ?? self::NOT_VERIFIED);
635
+        $collection->addProperty($p);
636
+
637
+        return $collection;
638
+    }
639
+
640
+    private function parseAccountData(IUser $user, $data): Account {
641
+        $account = new Account($user);
642
+        foreach ($data as $accountData) {
643
+            if ($this->isCollection($accountData['name'])) {
644
+                $account->setPropertyCollection($this->arrayDataToCollection($account, $accountData));
645
+            } else {
646
+                $account->setProperty($accountData['name'], $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
647
+                if (isset($accountData['locallyVerified'])) {
648
+                    $property = $account->getProperty($accountData['name']);
649
+                    $property->setLocallyVerified($accountData['locallyVerified']);
650
+                }
651
+            }
652
+        }
653
+        return $account;
654
+    }
655
+
656
+    public function getAccount(IUser $user): IAccount {
657
+        $cached = $this->internalCache->get($user->getUID());
658
+        if ($cached !== null) {
659
+            return $cached;
660
+        }
661
+        $account = $this->parseAccountData($user, $this->getUser($user));
662
+        if ($user->getBackend() instanceof IGetDisplayNameBackend) {
663
+            $property = $account->getProperty(self::PROPERTY_DISPLAYNAME);
664
+            $account->setProperty(self::PROPERTY_DISPLAYNAME, $user->getDisplayName(), $property->getScope(), $property->getVerified());
665
+        }
666
+        $this->internalCache->set($user->getUID(), $account);
667
+        return $account;
668
+    }
669
+
670
+    /**
671
+     * Converts value (phone number) in E.164 format when it was a valid number
672
+     * @throws InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code
673
+     */
674
+    protected function sanitizePropertyPhoneNumber(IAccountProperty $property): void {
675
+        $defaultRegion = $this->config->getSystemValueString('default_phone_region', '');
676
+
677
+        if ($defaultRegion === '') {
678
+            // When no default region is set, only +49… numbers are valid
679
+            if (!str_starts_with($property->getValue(), '+')) {
680
+                throw new InvalidArgumentException(self::PROPERTY_PHONE);
681
+            }
682
+
683
+            $defaultRegion = 'EN';
684
+        }
685
+
686
+        $phoneNumber = $this->phoneNumberUtil->convertToStandardFormat($property->getValue(), $defaultRegion);
687
+        if ($phoneNumber === null) {
688
+            throw new InvalidArgumentException(self::PROPERTY_PHONE);
689
+        }
690
+        $property->setValue($phoneNumber);
691
+    }
692
+
693
+    /**
694
+     * @throws InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty
695
+     */
696
+    private function sanitizePropertyWebsite(IAccountProperty $property): void {
697
+        $parts = parse_url($property->getValue());
698
+        if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) {
699
+            throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
700
+        }
701
+
702
+        if (!isset($parts['host']) || $parts['host'] === '') {
703
+            throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
704
+        }
705
+    }
706
+
707
+    /**
708
+     * @throws InvalidArgumentException If the property value is not a valid user handle according to X's rules
709
+     */
710
+    private function sanitizePropertyTwitter(IAccountProperty $property): void {
711
+        if ($property->getName() === self::PROPERTY_TWITTER) {
712
+            $matches = [];
713
+            // twitter handles only contain alpha numeric characters and the underscore and must not be longer than 15 characters
714
+            if (preg_match('/^@?([a-zA-Z0-9_]{2,15})$/', $property->getValue(), $matches) !== 1) {
715
+                throw new InvalidArgumentException(self::PROPERTY_TWITTER);
716
+            }
717
+
718
+            // drop the leading @ if any to make it the valid handle
719
+            $property->setValue($matches[1]);
720
+
721
+        }
722
+    }
723
+
724
+    private function validateBlueSkyHandle(string $text): bool {
725
+        if ($text === '') {
726
+            return true;
727
+        }
728
+
729
+        $lowerText = strtolower($text);
730
+
731
+        if ($lowerText === 'bsky.social') {
732
+            // "bsky.social" itself is not a valid handle
733
+            return false;
734
+        }
735
+
736
+        if (str_ends_with($lowerText, '.bsky.social')) {
737
+            $parts = explode('.', $lowerText);
738
+
739
+            // Must be exactly: username.bsky.social → 3 parts
740
+            if (count($parts) !== 3 || $parts[1] !== 'bsky' || $parts[2] !== 'social') {
741
+                return false;
742
+            }
743
+
744
+            $username = $parts[0];
745
+
746
+            // Must be 3–18 chars, alphanumeric/hyphen, no start/end hyphen
747
+            return preg_match('/^[a-z0-9][a-z0-9-]{2,17}$/', $username) === 1;
748
+        }
749
+
750
+        // Allow custom domains (Bluesky handle via personal domain)
751
+        return filter_var($text, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false;
752
+    }
753
+
754
+
755
+    private function sanitizePropertyBluesky(IAccountProperty $property): void {
756
+        if ($property->getName() === self::PROPERTY_BLUESKY) {
757
+            if (!$this->validateBlueSkyHandle($property->getValue())) {
758
+                throw new InvalidArgumentException(self::PROPERTY_BLUESKY);
759
+            }
760
+
761
+            $property->setValue($property->getValue());
762
+        }
763
+    }
764
+
765
+    /**
766
+     * @throws InvalidArgumentException If the property value is not a valid fediverse handle (username@instance where instance is a valid domain)
767
+     */
768
+    private function sanitizePropertyFediverse(IAccountProperty $property): void {
769
+        if ($property->getName() === self::PROPERTY_FEDIVERSE) {
770
+            $matches = [];
771
+            if (preg_match('/^@?([^@\s\/\\\]+)@([^\s\/\\\]+)$/', trim($property->getValue()), $matches) !== 1) {
772
+                throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
773
+            }
774
+
775
+            [, $username, $instance] = $matches;
776
+            $validated = filter_var($instance, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
777
+            if ($validated !== $instance) {
778
+                throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
779
+            }
780
+
781
+            if ($this->config->getSystemValueBool('has_internet_connection', true)) {
782
+                $client = $this->clientService->newClient();
783
+
784
+                try {
785
+                    // try the public account lookup API of mastodon
786
+                    $response = $client->get("https://{$instance}/.well-known/webfinger?resource=acct:{$username}@{$instance}");
787
+                    // should be a json response with account information
788
+                    $data = $response->getBody();
789
+                    if (is_resource($data)) {
790
+                        $data = stream_get_contents($data);
791
+                    }
792
+                    $decoded = json_decode($data, true);
793
+                    // ensure the username is the same the user passed
794
+                    // in this case we can assume this is a valid fediverse server and account
795
+                    if (!is_array($decoded) || ($decoded['subject'] ?? '') !== "acct:{$username}@{$instance}") {
796
+                        throw new InvalidArgumentException();
797
+                    }
798
+                    // check for activitypub link
799
+                    if (is_array($decoded['links']) && isset($decoded['links'])) {
800
+                        $found = false;
801
+                        foreach ($decoded['links'] as $link) {
802
+                            // have application/activity+json or application/ld+json
803
+                            if (isset($link['type']) && (
804
+                                $link['type'] === 'application/activity+json'
805
+                                || $link['type'] === 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
806
+                            )) {
807
+                                $found = true;
808
+                                break;
809
+                            }
810
+                        }
811
+                        if (!$found) {
812
+                            throw new InvalidArgumentException();
813
+                        }
814
+                    }
815
+                } catch (InvalidArgumentException) {
816
+                    throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
817
+                } catch (\Exception $error) {
818
+                    $this->logger->error('Could not verify fediverse account', ['exception' => $error, 'instance' => $instance]);
819
+                    throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
820
+                }
821
+            }
822
+
823
+            $property->setValue("$username@$instance");
824
+        }
825
+    }
826
+
827
+    public function updateAccount(IAccount $account): void {
828
+        $this->testValueLengths(iterator_to_array($account->getAllProperties()), true);
829
+        try {
830
+            $property = $account->getProperty(self::PROPERTY_PHONE);
831
+            if ($property->getValue() !== '') {
832
+                $this->sanitizePropertyPhoneNumber($property);
833
+            }
834
+        } catch (PropertyDoesNotExistException $e) {
835
+            //  valid case, nothing to do
836
+        }
837
+
838
+        try {
839
+            $property = $account->getProperty(self::PROPERTY_WEBSITE);
840
+            if ($property->getValue() !== '') {
841
+                $this->sanitizePropertyWebsite($property);
842
+            }
843
+        } catch (PropertyDoesNotExistException $e) {
844
+            //  valid case, nothing to do
845
+        }
846
+
847
+        try {
848
+            $property = $account->getProperty(self::PROPERTY_TWITTER);
849
+            if ($property->getValue() !== '') {
850
+                $this->sanitizePropertyTwitter($property);
851
+            }
852
+        } catch (PropertyDoesNotExistException $e) {
853
+            //  valid case, nothing to do
854
+        }
855
+
856
+        try {
857
+            $property = $account->getProperty(self::PROPERTY_BLUESKY);
858
+            if ($property->getValue() !== '') {
859
+                $this->sanitizePropertyBluesky($property);
860
+            }
861
+        } catch (PropertyDoesNotExistException $e) {
862
+            //  valid case, nothing to do
863
+        }
864
+
865
+        try {
866
+            $property = $account->getProperty(self::PROPERTY_FEDIVERSE);
867
+            if ($property->getValue() !== '') {
868
+                $this->sanitizePropertyFediverse($property);
869
+            }
870
+        } catch (PropertyDoesNotExistException $e) {
871
+            //  valid case, nothing to do
872
+        }
873
+
874
+        foreach ($account->getAllProperties() as $property) {
875
+            $this->testPropertyScope($property, self::ALLOWED_SCOPES, true);
876
+        }
877
+
878
+        $oldData = $this->getUser($account->getUser(), false);
879
+        $this->updateVerificationStatus($account, $oldData);
880
+        $this->checkEmailVerification($account, $oldData);
881
+        $this->checkLocalEmailVerification($account, $oldData);
882
+
883
+        $data = [];
884
+        foreach ($account->getAllProperties() as $property) {
885
+            /** @var IAccountProperty $property */
886
+            $data[] = [
887
+                'name' => $property->getName(),
888
+                'value' => $property->getValue(),
889
+                'scope' => $property->getScope(),
890
+                'verified' => $property->getVerified(),
891
+                'locallyVerified' => $property->getLocallyVerified(),
892
+            ];
893
+        }
894
+
895
+        $this->updateUser($account->getUser(), $data, $oldData, true);
896
+        $this->internalCache->set($account->getUser()->getUID(), $account);
897
+    }
898 898
 }
Please login to merge, or discard this patch.
lib/private/Memcache/Memcached.php 2 patches
Spacing   +8 added lines, -8 removed lines patch added patch discarded remove patch
@@ -105,7 +105,7 @@  discard block
 block discarded – undo
105 105
 	}
106 106
 
107 107
 	public function get($key) {
108
-		$result = self::$cache->get($this->getNameSpace() . $key);
108
+		$result = self::$cache->get($this->getNameSpace().$key);
109 109
 		if ($result === false and self::$cache->getResultCode() == \Memcached::RES_NOTFOUND) {
110 110
 			return null;
111 111
 		} else {
@@ -115,20 +115,20 @@  discard block
 block discarded – undo
115 115
 
116 116
 	public function set($key, $value, $ttl = 0) {
117 117
 		if ($ttl > 0) {
118
-			$result = self::$cache->set($this->getNameSpace() . $key, $value, $ttl);
118
+			$result = self::$cache->set($this->getNameSpace().$key, $value, $ttl);
119 119
 		} else {
120
-			$result = self::$cache->set($this->getNameSpace() . $key, $value);
120
+			$result = self::$cache->set($this->getNameSpace().$key, $value);
121 121
 		}
122 122
 		return $result || $this->isSuccess();
123 123
 	}
124 124
 
125 125
 	public function hasKey($key) {
126
-		self::$cache->get($this->getNameSpace() . $key);
126
+		self::$cache->get($this->getNameSpace().$key);
127 127
 		return self::$cache->getResultCode() === \Memcached::RES_SUCCESS;
128 128
 	}
129 129
 
130 130
 	public function remove($key) {
131
-		$result = self::$cache->delete($this->getNameSpace() . $key);
131
+		$result = self::$cache->delete($this->getNameSpace().$key);
132 132
 		return $result || $this->isSuccess() || self::$cache->getResultCode() === \Memcached::RES_NOTFOUND;
133 133
 	}
134 134
 
@@ -147,7 +147,7 @@  discard block
 block discarded – undo
147 147
 	 * @return bool
148 148
 	 */
149 149
 	public function add($key, $value, $ttl = 0) {
150
-		$result = self::$cache->add($this->getPrefix() . $key, $value, $ttl);
150
+		$result = self::$cache->add($this->getPrefix().$key, $value, $ttl);
151 151
 		return $result || $this->isSuccess();
152 152
 	}
153 153
 
@@ -160,7 +160,7 @@  discard block
 block discarded – undo
160 160
 	 */
161 161
 	public function inc($key, $step = 1) {
162 162
 		$this->add($key, 0);
163
-		$result = self::$cache->increment($this->getPrefix() . $key, $step);
163
+		$result = self::$cache->increment($this->getPrefix().$key, $step);
164 164
 
165 165
 		if (self::$cache->getResultCode() !== \Memcached::RES_SUCCESS) {
166 166
 			return false;
@@ -177,7 +177,7 @@  discard block
 block discarded – undo
177 177
 	 * @return int | bool
178 178
 	 */
179 179
 	public function dec($key, $step = 1) {
180
-		$result = self::$cache->decrement($this->getPrefix() . $key, $step);
180
+		$result = self::$cache->decrement($this->getPrefix().$key, $step);
181 181
 
182 182
 		if (self::$cache->getResultCode() !== \Memcached::RES_SUCCESS) {
183 183
 			return false;
Please login to merge, or discard this patch.
Indentation   +158 added lines, -158 removed lines patch added patch discarded remove patch
@@ -11,162 +11,162 @@
 block discarded – undo
11 11
 use OCP\IMemcache;
12 12
 
13 13
 class Memcached extends Cache implements IMemcache {
14
-	use CASTrait;
15
-
16
-	/**
17
-	 * @var \Memcached $cache
18
-	 */
19
-	private static $cache = null;
20
-
21
-	use CADTrait;
22
-
23
-	public function __construct($prefix = '') {
24
-		parent::__construct($prefix);
25
-		if (is_null(self::$cache)) {
26
-			self::$cache = new \Memcached();
27
-
28
-			$defaultOptions = [
29
-				\Memcached::OPT_CONNECT_TIMEOUT => 50,
30
-				\Memcached::OPT_RETRY_TIMEOUT => 50,
31
-				\Memcached::OPT_SEND_TIMEOUT => 50,
32
-				\Memcached::OPT_RECV_TIMEOUT => 50,
33
-				\Memcached::OPT_POLL_TIMEOUT => 50,
34
-
35
-				// Enable compression
36
-				\Memcached::OPT_COMPRESSION => true,
37
-
38
-				// Turn on consistent hashing
39
-				\Memcached::OPT_LIBKETAMA_COMPATIBLE => true,
40
-
41
-				// Enable Binary Protocol
42
-				\Memcached::OPT_BINARY_PROTOCOL => true,
43
-			];
44
-			/**
45
-			 * By default enable igbinary serializer if available
46
-			 *
47
-			 * Psalm checks depend on if igbinary is installed or not with memcached
48
-			 * @psalm-suppress RedundantCondition
49
-			 * @psalm-suppress TypeDoesNotContainType
50
-			 */
51
-			if (\Memcached::HAVE_IGBINARY) {
52
-				$defaultOptions[\Memcached::OPT_SERIALIZER]
53
-					= \Memcached::SERIALIZER_IGBINARY;
54
-			}
55
-			$options = \OC::$server->getConfig()->getSystemValue('memcached_options', []);
56
-			if (is_array($options)) {
57
-				$options = $options + $defaultOptions;
58
-				self::$cache->setOptions($options);
59
-			} else {
60
-				throw new HintException("Expected 'memcached_options' config to be an array, got $options");
61
-			}
62
-
63
-			$servers = \OC::$server->getSystemConfig()->getValue('memcached_servers');
64
-			if (!$servers) {
65
-				$server = \OC::$server->getSystemConfig()->getValue('memcached_server');
66
-				if ($server) {
67
-					$servers = [$server];
68
-				} else {
69
-					$servers = [['localhost', 11211]];
70
-				}
71
-			}
72
-			self::$cache->addServers($servers);
73
-		}
74
-	}
75
-
76
-	/**
77
-	 * entries in XCache gets namespaced to prevent collisions between owncloud instances and users
78
-	 */
79
-	protected function getNameSpace() {
80
-		return $this->prefix;
81
-	}
82
-
83
-	public function get($key) {
84
-		$result = self::$cache->get($this->getNameSpace() . $key);
85
-		if ($result === false and self::$cache->getResultCode() == \Memcached::RES_NOTFOUND) {
86
-			return null;
87
-		} else {
88
-			return $result;
89
-		}
90
-	}
91
-
92
-	public function set($key, $value, $ttl = 0) {
93
-		if ($ttl > 0) {
94
-			$result = self::$cache->set($this->getNameSpace() . $key, $value, $ttl);
95
-		} else {
96
-			$result = self::$cache->set($this->getNameSpace() . $key, $value);
97
-		}
98
-		return $result || $this->isSuccess();
99
-	}
100
-
101
-	public function hasKey($key) {
102
-		self::$cache->get($this->getNameSpace() . $key);
103
-		return self::$cache->getResultCode() === \Memcached::RES_SUCCESS;
104
-	}
105
-
106
-	public function remove($key) {
107
-		$result = self::$cache->delete($this->getNameSpace() . $key);
108
-		return $result || $this->isSuccess() || self::$cache->getResultCode() === \Memcached::RES_NOTFOUND;
109
-	}
110
-
111
-	public function clear($prefix = '') {
112
-		// Newer Memcached doesn't like getAllKeys(), flush everything
113
-		self::$cache->flush();
114
-		return true;
115
-	}
116
-
117
-	/**
118
-	 * Set a value in the cache if it's not already stored
119
-	 *
120
-	 * @param string $key
121
-	 * @param mixed $value
122
-	 * @param int $ttl Time To Live in seconds. Defaults to 60*60*24
123
-	 * @return bool
124
-	 */
125
-	public function add($key, $value, $ttl = 0) {
126
-		$result = self::$cache->add($this->getPrefix() . $key, $value, $ttl);
127
-		return $result || $this->isSuccess();
128
-	}
129
-
130
-	/**
131
-	 * Increase a stored number
132
-	 *
133
-	 * @param string $key
134
-	 * @param int $step
135
-	 * @return int | bool
136
-	 */
137
-	public function inc($key, $step = 1) {
138
-		$this->add($key, 0);
139
-		$result = self::$cache->increment($this->getPrefix() . $key, $step);
140
-
141
-		if (self::$cache->getResultCode() !== \Memcached::RES_SUCCESS) {
142
-			return false;
143
-		}
144
-
145
-		return $result;
146
-	}
147
-
148
-	/**
149
-	 * Decrease a stored number
150
-	 *
151
-	 * @param string $key
152
-	 * @param int $step
153
-	 * @return int | bool
154
-	 */
155
-	public function dec($key, $step = 1) {
156
-		$result = self::$cache->decrement($this->getPrefix() . $key, $step);
157
-
158
-		if (self::$cache->getResultCode() !== \Memcached::RES_SUCCESS) {
159
-			return false;
160
-		}
161
-
162
-		return $result;
163
-	}
164
-
165
-	public static function isAvailable(): bool {
166
-		return extension_loaded('memcached');
167
-	}
168
-
169
-	private function isSuccess(): bool {
170
-		return self::$cache->getResultCode() === \Memcached::RES_SUCCESS;
171
-	}
14
+    use CASTrait;
15
+
16
+    /**
17
+     * @var \Memcached $cache
18
+     */
19
+    private static $cache = null;
20
+
21
+    use CADTrait;
22
+
23
+    public function __construct($prefix = '') {
24
+        parent::__construct($prefix);
25
+        if (is_null(self::$cache)) {
26
+            self::$cache = new \Memcached();
27
+
28
+            $defaultOptions = [
29
+                \Memcached::OPT_CONNECT_TIMEOUT => 50,
30
+                \Memcached::OPT_RETRY_TIMEOUT => 50,
31
+                \Memcached::OPT_SEND_TIMEOUT => 50,
32
+                \Memcached::OPT_RECV_TIMEOUT => 50,
33
+                \Memcached::OPT_POLL_TIMEOUT => 50,
34
+
35
+                // Enable compression
36
+                \Memcached::OPT_COMPRESSION => true,
37
+
38
+                // Turn on consistent hashing
39
+                \Memcached::OPT_LIBKETAMA_COMPATIBLE => true,
40
+
41
+                // Enable Binary Protocol
42
+                \Memcached::OPT_BINARY_PROTOCOL => true,
43
+            ];
44
+            /**
45
+             * By default enable igbinary serializer if available
46
+             *
47
+             * Psalm checks depend on if igbinary is installed or not with memcached
48
+             * @psalm-suppress RedundantCondition
49
+             * @psalm-suppress TypeDoesNotContainType
50
+             */
51
+            if (\Memcached::HAVE_IGBINARY) {
52
+                $defaultOptions[\Memcached::OPT_SERIALIZER]
53
+                    = \Memcached::SERIALIZER_IGBINARY;
54
+            }
55
+            $options = \OC::$server->getConfig()->getSystemValue('memcached_options', []);
56
+            if (is_array($options)) {
57
+                $options = $options + $defaultOptions;
58
+                self::$cache->setOptions($options);
59
+            } else {
60
+                throw new HintException("Expected 'memcached_options' config to be an array, got $options");
61
+            }
62
+
63
+            $servers = \OC::$server->getSystemConfig()->getValue('memcached_servers');
64
+            if (!$servers) {
65
+                $server = \OC::$server->getSystemConfig()->getValue('memcached_server');
66
+                if ($server) {
67
+                    $servers = [$server];
68
+                } else {
69
+                    $servers = [['localhost', 11211]];
70
+                }
71
+            }
72
+            self::$cache->addServers($servers);
73
+        }
74
+    }
75
+
76
+    /**
77
+     * entries in XCache gets namespaced to prevent collisions between owncloud instances and users
78
+     */
79
+    protected function getNameSpace() {
80
+        return $this->prefix;
81
+    }
82
+
83
+    public function get($key) {
84
+        $result = self::$cache->get($this->getNameSpace() . $key);
85
+        if ($result === false and self::$cache->getResultCode() == \Memcached::RES_NOTFOUND) {
86
+            return null;
87
+        } else {
88
+            return $result;
89
+        }
90
+    }
91
+
92
+    public function set($key, $value, $ttl = 0) {
93
+        if ($ttl > 0) {
94
+            $result = self::$cache->set($this->getNameSpace() . $key, $value, $ttl);
95
+        } else {
96
+            $result = self::$cache->set($this->getNameSpace() . $key, $value);
97
+        }
98
+        return $result || $this->isSuccess();
99
+    }
100
+
101
+    public function hasKey($key) {
102
+        self::$cache->get($this->getNameSpace() . $key);
103
+        return self::$cache->getResultCode() === \Memcached::RES_SUCCESS;
104
+    }
105
+
106
+    public function remove($key) {
107
+        $result = self::$cache->delete($this->getNameSpace() . $key);
108
+        return $result || $this->isSuccess() || self::$cache->getResultCode() === \Memcached::RES_NOTFOUND;
109
+    }
110
+
111
+    public function clear($prefix = '') {
112
+        // Newer Memcached doesn't like getAllKeys(), flush everything
113
+        self::$cache->flush();
114
+        return true;
115
+    }
116
+
117
+    /**
118
+     * Set a value in the cache if it's not already stored
119
+     *
120
+     * @param string $key
121
+     * @param mixed $value
122
+     * @param int $ttl Time To Live in seconds. Defaults to 60*60*24
123
+     * @return bool
124
+     */
125
+    public function add($key, $value, $ttl = 0) {
126
+        $result = self::$cache->add($this->getPrefix() . $key, $value, $ttl);
127
+        return $result || $this->isSuccess();
128
+    }
129
+
130
+    /**
131
+     * Increase a stored number
132
+     *
133
+     * @param string $key
134
+     * @param int $step
135
+     * @return int | bool
136
+     */
137
+    public function inc($key, $step = 1) {
138
+        $this->add($key, 0);
139
+        $result = self::$cache->increment($this->getPrefix() . $key, $step);
140
+
141
+        if (self::$cache->getResultCode() !== \Memcached::RES_SUCCESS) {
142
+            return false;
143
+        }
144
+
145
+        return $result;
146
+    }
147
+
148
+    /**
149
+     * Decrease a stored number
150
+     *
151
+     * @param string $key
152
+     * @param int $step
153
+     * @return int | bool
154
+     */
155
+    public function dec($key, $step = 1) {
156
+        $result = self::$cache->decrement($this->getPrefix() . $key, $step);
157
+
158
+        if (self::$cache->getResultCode() !== \Memcached::RES_SUCCESS) {
159
+            return false;
160
+        }
161
+
162
+        return $result;
163
+    }
164
+
165
+    public static function isAvailable(): bool {
166
+        return extension_loaded('memcached');
167
+    }
168
+
169
+    private function isSuccess(): bool {
170
+        return self::$cache->getResultCode() === \Memcached::RES_SUCCESS;
171
+    }
172 172
 }
Please login to merge, or discard this patch.
lib/private/Diagnostics/EventLogger.php 2 patches
Spacing   +4 added lines, -4 removed lines patch added patch discarded remove patch
@@ -60,8 +60,8 @@  discard block
 block discarded – undo
60 60
 	}
61 61
 
62 62
 	public function isLoggingActivated(): bool {
63
-		$systemValue = (bool)$this->config->getValue('diagnostics.logging', false)
64
-			|| (bool)$this->config->getValue('profiler', false);
63
+		$systemValue = (bool) $this->config->getValue('diagnostics.logging', false)
64
+			|| (bool) $this->config->getValue('profiler', false);
65 65
 
66 66
 		if ($systemValue && $this->config->getValue('debug', false)) {
67 67
 			return true;
@@ -125,12 +125,12 @@  discard block
 block discarded – undo
125 125
 			$duration = $event->getDuration();
126 126
 			$timeInMs = round($duration * 1000, 4);
127 127
 
128
-			$loggingMinimum = (int)$this->config->getValue('diagnostics.logging.threshold', 0);
128
+			$loggingMinimum = (int) $this->config->getValue('diagnostics.logging.threshold', 0);
129 129
 			if ($loggingMinimum === 0 || $timeInMs < $loggingMinimum) {
130 130
 				return;
131 131
 			}
132 132
 
133
-			$message = microtime() . ' - ' . $event->getId() . ': ' . $timeInMs . ' (' . $event->getDescription() . ')';
133
+			$message = microtime().' - '.$event->getId().': '.$timeInMs.' ('.$event->getDescription().')';
134 134
 			$this->logger->debug($message, ['app' => 'diagnostics']);
135 135
 		}
136 136
 	}
Please login to merge, or discard this patch.
Indentation   +102 added lines, -102 removed lines patch added patch discarded remove patch
@@ -13,106 +13,106 @@
 block discarded – undo
13 13
 use Psr\Log\LoggerInterface;
14 14
 
15 15
 class EventLogger implements IEventLogger {
16
-	/** @var Event[] */
17
-	private $events = [];
18
-
19
-	/** @var SystemConfig */
20
-	private $config;
21
-
22
-	/** @var LoggerInterface */
23
-	private $logger;
24
-
25
-	/** @var Log */
26
-	private $internalLogger;
27
-
28
-	/**
29
-	 * @var bool - Module needs to be activated by some app
30
-	 */
31
-	private $activated = false;
32
-
33
-	public function __construct(SystemConfig $config, LoggerInterface $logger, Log $internalLogger) {
34
-		$this->config = $config;
35
-		$this->logger = $logger;
36
-		$this->internalLogger = $internalLogger;
37
-
38
-		if ($this->isLoggingActivated()) {
39
-			$this->activate();
40
-		}
41
-	}
42
-
43
-	public function isLoggingActivated(): bool {
44
-		$systemValue = (bool)$this->config->getValue('diagnostics.logging', false)
45
-			|| (bool)$this->config->getValue('profiler', false);
46
-
47
-		if ($systemValue && $this->config->getValue('debug', false)) {
48
-			return true;
49
-		}
50
-
51
-		$isDebugLevel = $this->internalLogger->getLogLevel([], '') === Log::DEBUG;
52
-		return $systemValue && $isDebugLevel;
53
-	}
54
-
55
-	/**
56
-	 * @inheritdoc
57
-	 */
58
-	public function start($id, $description = '') {
59
-		if ($this->activated) {
60
-			$this->events[$id] = new Event($id, $description, microtime(true));
61
-			$this->writeLog($this->events[$id]);
62
-		}
63
-	}
64
-
65
-	/**
66
-	 * @inheritdoc
67
-	 */
68
-	public function end($id) {
69
-		if ($this->activated && isset($this->events[$id])) {
70
-			$timing = $this->events[$id];
71
-			$timing->end(microtime(true));
72
-			$this->writeLog($timing);
73
-		}
74
-	}
75
-
76
-	/**
77
-	 * @inheritdoc
78
-	 */
79
-	public function log($id, $description, $start, $end) {
80
-		if ($this->activated) {
81
-			$this->events[$id] = new Event($id, $description, $start);
82
-			$this->events[$id]->end($end);
83
-			$this->writeLog($this->events[$id]);
84
-		}
85
-	}
86
-
87
-	/**
88
-	 * @inheritdoc
89
-	 */
90
-	public function getEvents() {
91
-		return $this->events;
92
-	}
93
-
94
-	/**
95
-	 * @inheritdoc
96
-	 */
97
-	public function activate() {
98
-		$this->activated = true;
99
-	}
100
-
101
-	private function writeLog(IEvent $event) {
102
-		if ($this->activated) {
103
-			if ($event->getEnd() === null) {
104
-				return;
105
-			}
106
-			$duration = $event->getDuration();
107
-			$timeInMs = round($duration * 1000, 4);
108
-
109
-			$loggingMinimum = (int)$this->config->getValue('diagnostics.logging.threshold', 0);
110
-			if ($loggingMinimum === 0 || $timeInMs < $loggingMinimum) {
111
-				return;
112
-			}
113
-
114
-			$message = microtime() . ' - ' . $event->getId() . ': ' . $timeInMs . ' (' . $event->getDescription() . ')';
115
-			$this->logger->debug($message, ['app' => 'diagnostics']);
116
-		}
117
-	}
16
+    /** @var Event[] */
17
+    private $events = [];
18
+
19
+    /** @var SystemConfig */
20
+    private $config;
21
+
22
+    /** @var LoggerInterface */
23
+    private $logger;
24
+
25
+    /** @var Log */
26
+    private $internalLogger;
27
+
28
+    /**
29
+     * @var bool - Module needs to be activated by some app
30
+     */
31
+    private $activated = false;
32
+
33
+    public function __construct(SystemConfig $config, LoggerInterface $logger, Log $internalLogger) {
34
+        $this->config = $config;
35
+        $this->logger = $logger;
36
+        $this->internalLogger = $internalLogger;
37
+
38
+        if ($this->isLoggingActivated()) {
39
+            $this->activate();
40
+        }
41
+    }
42
+
43
+    public function isLoggingActivated(): bool {
44
+        $systemValue = (bool)$this->config->getValue('diagnostics.logging', false)
45
+            || (bool)$this->config->getValue('profiler', false);
46
+
47
+        if ($systemValue && $this->config->getValue('debug', false)) {
48
+            return true;
49
+        }
50
+
51
+        $isDebugLevel = $this->internalLogger->getLogLevel([], '') === Log::DEBUG;
52
+        return $systemValue && $isDebugLevel;
53
+    }
54
+
55
+    /**
56
+     * @inheritdoc
57
+     */
58
+    public function start($id, $description = '') {
59
+        if ($this->activated) {
60
+            $this->events[$id] = new Event($id, $description, microtime(true));
61
+            $this->writeLog($this->events[$id]);
62
+        }
63
+    }
64
+
65
+    /**
66
+     * @inheritdoc
67
+     */
68
+    public function end($id) {
69
+        if ($this->activated && isset($this->events[$id])) {
70
+            $timing = $this->events[$id];
71
+            $timing->end(microtime(true));
72
+            $this->writeLog($timing);
73
+        }
74
+    }
75
+
76
+    /**
77
+     * @inheritdoc
78
+     */
79
+    public function log($id, $description, $start, $end) {
80
+        if ($this->activated) {
81
+            $this->events[$id] = new Event($id, $description, $start);
82
+            $this->events[$id]->end($end);
83
+            $this->writeLog($this->events[$id]);
84
+        }
85
+    }
86
+
87
+    /**
88
+     * @inheritdoc
89
+     */
90
+    public function getEvents() {
91
+        return $this->events;
92
+    }
93
+
94
+    /**
95
+     * @inheritdoc
96
+     */
97
+    public function activate() {
98
+        $this->activated = true;
99
+    }
100
+
101
+    private function writeLog(IEvent $event) {
102
+        if ($this->activated) {
103
+            if ($event->getEnd() === null) {
104
+                return;
105
+            }
106
+            $duration = $event->getDuration();
107
+            $timeInMs = round($duration * 1000, 4);
108
+
109
+            $loggingMinimum = (int)$this->config->getValue('diagnostics.logging.threshold', 0);
110
+            if ($loggingMinimum === 0 || $timeInMs < $loggingMinimum) {
111
+                return;
112
+            }
113
+
114
+            $message = microtime() . ' - ' . $event->getId() . ': ' . $timeInMs . ' (' . $event->getDescription() . ')';
115
+            $this->logger->debug($message, ['app' => 'diagnostics']);
116
+        }
117
+    }
118 118
 }
Please login to merge, or discard this patch.
lib/private/Files/SimpleFS/SimpleFolder.php 2 patches
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -52,7 +52,7 @@
 block discarded – undo
52 52
 	public function getDirectoryListing(): array {
53 53
 		$listing = $this->folder->getDirectoryListing();
54 54
 
55
-		$fileListing = array_map(function (Node $file) {
55
+		$fileListing = array_map(function(Node $file) {
56 56
 			if ($file instanceof File) {
57 57
 				return new SimpleFile($file);
58 58
 			}
Please login to merge, or discard this patch.
Indentation   +73 added lines, -73 removed lines patch added patch discarded remove patch
@@ -13,77 +13,77 @@
 block discarded – undo
13 13
 use OCP\Files\SimpleFS\ISimpleFolder;
14 14
 
15 15
 class SimpleFolder implements ISimpleFolder {
16
-	/** @var Folder */
17
-	private $folder;
18
-
19
-	/**
20
-	 * Folder constructor.
21
-	 *
22
-	 * @param Folder $folder
23
-	 */
24
-	public function __construct(Folder $folder) {
25
-		$this->folder = $folder;
26
-	}
27
-
28
-	public function getName(): string {
29
-		return $this->folder->getName();
30
-	}
31
-
32
-	public function getDirectoryListing(): array {
33
-		$listing = $this->folder->getDirectoryListing();
34
-
35
-		$fileListing = array_map(function (Node $file) {
36
-			if ($file instanceof File) {
37
-				return new SimpleFile($file);
38
-			}
39
-			return null;
40
-		}, $listing);
41
-
42
-		$fileListing = array_filter($fileListing);
43
-
44
-		return array_values($fileListing);
45
-	}
46
-
47
-	public function delete(): void {
48
-		$this->folder->delete();
49
-	}
50
-
51
-	public function fileExists(string $name): bool {
52
-		return $this->folder->nodeExists($name);
53
-	}
54
-
55
-	public function getFile(string $name): ISimpleFile {
56
-		$file = $this->folder->get($name);
57
-
58
-		if (!($file instanceof File)) {
59
-			throw new NotFoundException();
60
-		}
61
-
62
-		return new SimpleFile($file);
63
-	}
64
-
65
-	public function newFile(string $name, $content = null): ISimpleFile {
66
-		if ($content === null) {
67
-			// delay creating the file until it's written to
68
-			return new NewSimpleFile($this->folder, $name);
69
-		} else {
70
-			$file = $this->folder->newFile($name, $content);
71
-			return new SimpleFile($file);
72
-		}
73
-	}
74
-
75
-	public function getFolder(string $name): ISimpleFolder {
76
-		$folder = $this->folder->get($name);
77
-
78
-		if (!($folder instanceof Folder)) {
79
-			throw new NotFoundException();
80
-		}
81
-
82
-		return new SimpleFolder($folder);
83
-	}
84
-
85
-	public function newFolder(string $path): ISimpleFolder {
86
-		$folder = $this->folder->newFolder($path);
87
-		return new SimpleFolder($folder);
88
-	}
16
+    /** @var Folder */
17
+    private $folder;
18
+
19
+    /**
20
+     * Folder constructor.
21
+     *
22
+     * @param Folder $folder
23
+     */
24
+    public function __construct(Folder $folder) {
25
+        $this->folder = $folder;
26
+    }
27
+
28
+    public function getName(): string {
29
+        return $this->folder->getName();
30
+    }
31
+
32
+    public function getDirectoryListing(): array {
33
+        $listing = $this->folder->getDirectoryListing();
34
+
35
+        $fileListing = array_map(function (Node $file) {
36
+            if ($file instanceof File) {
37
+                return new SimpleFile($file);
38
+            }
39
+            return null;
40
+        }, $listing);
41
+
42
+        $fileListing = array_filter($fileListing);
43
+
44
+        return array_values($fileListing);
45
+    }
46
+
47
+    public function delete(): void {
48
+        $this->folder->delete();
49
+    }
50
+
51
+    public function fileExists(string $name): bool {
52
+        return $this->folder->nodeExists($name);
53
+    }
54
+
55
+    public function getFile(string $name): ISimpleFile {
56
+        $file = $this->folder->get($name);
57
+
58
+        if (!($file instanceof File)) {
59
+            throw new NotFoundException();
60
+        }
61
+
62
+        return new SimpleFile($file);
63
+    }
64
+
65
+    public function newFile(string $name, $content = null): ISimpleFile {
66
+        if ($content === null) {
67
+            // delay creating the file until it's written to
68
+            return new NewSimpleFile($this->folder, $name);
69
+        } else {
70
+            $file = $this->folder->newFile($name, $content);
71
+            return new SimpleFile($file);
72
+        }
73
+    }
74
+
75
+    public function getFolder(string $name): ISimpleFolder {
76
+        $folder = $this->folder->get($name);
77
+
78
+        if (!($folder instanceof Folder)) {
79
+            throw new NotFoundException();
80
+        }
81
+
82
+        return new SimpleFolder($folder);
83
+    }
84
+
85
+    public function newFolder(string $path): ISimpleFolder {
86
+        $folder = $this->folder->newFolder($path);
87
+        return new SimpleFolder($folder);
88
+    }
89 89
 }
Please login to merge, or discard this patch.
core/templates/loginflowv2/grant.php 2 patches
Indentation   +6 added lines, -6 removed lines patch added patch discarded remove patch
@@ -31,15 +31,15 @@
 block discarded – undo
31 31
 	<h2><?php p($l->t('Account access')) ?></h2>
32 32
 	<p class="info">
33 33
 		<?php p($l->t('Currently logged in as %1$s (%2$s).', [
34
-			$_['userDisplayName'],
35
-			$_['userId'],
36
-		])) ?>
34
+            $_['userDisplayName'],
35
+            $_['userId'],
36
+        ])) ?>
37 37
 	</p>
38 38
 	<p class="info">
39 39
 		<?php print_unescaped($l->t('You are about to grant %1$s access to your %2$s account.', [
40
-			'<strong>' . \OCP\Util::sanitizeHTML($_['client']) . '</strong>',
41
-			\OCP\Util::sanitizeHTML($_['instanceName'])
42
-		])) ?>
40
+            '<strong>' . \OCP\Util::sanitizeHTML($_['client']) . '</strong>',
41
+            \OCP\Util::sanitizeHTML($_['instanceName'])
42
+        ])) ?>
43 43
 	</p>
44 44
 
45 45
 	<br/>
Please login to merge, or discard this patch.
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -37,7 +37,7 @@
 block discarded – undo
37 37
 	</p>
38 38
 	<p class="info">
39 39
 		<?php print_unescaped($l->t('You are about to grant %1$s access to your %2$s account.', [
40
-			'<strong>' . \OCP\Util::sanitizeHTML($_['client']) . '</strong>',
40
+			'<strong>'.\OCP\Util::sanitizeHTML($_['client']).'</strong>',
41 41
 			\OCP\Util::sanitizeHTML($_['instanceName'])
42 42
 		])) ?>
43 43
 	</p>
Please login to merge, or discard this patch.
apps/federatedfilesharing/lib/BackgroundJob/RetryJob.php 2 patches
Spacing   +3 added lines, -3 removed lines patch added patch discarded remove patch
@@ -75,7 +75,7 @@  discard block
 block discarded – undo
75 75
 		$token = $argument['token'];
76 76
 		$action = $argument['action'];
77 77
 		$data = json_decode($argument['data'], true);
78
-		$try = (int)$argument['try'] + 1;
78
+		$try = (int) $argument['try'] + 1;
79 79
 
80 80
 		$result = $this->notifications->sendUpdateToRemote($remote, $remoteId, $token, $action, $data, $try);
81 81
 
@@ -95,7 +95,7 @@  discard block
 block discarded – undo
95 95
 				'token' => $argument['token'],
96 96
 				'data' => $argument['data'],
97 97
 				'action' => $argument['action'],
98
-				'try' => (int)$argument['try'] + 1,
98
+				'try' => (int) $argument['try'] + 1,
99 99
 				'lastRun' => $this->time->getTime()
100 100
 			]
101 101
 		);
@@ -105,7 +105,7 @@  discard block
 block discarded – undo
105 105
 	 * Test if it is time for the next run
106 106
 	 */
107 107
 	protected function shouldRun(array $argument): bool {
108
-		$lastRun = (int)$argument['lastRun'];
108
+		$lastRun = (int) $argument['lastRun'];
109 109
 		return (($this->time->getTime() - $lastRun) > $this->interval);
110 110
 	}
111 111
 }
Please login to merge, or discard this patch.
Indentation   +58 added lines, -58 removed lines patch added patch discarded remove patch
@@ -21,71 +21,71 @@
 block discarded – undo
21 21
  * @package OCA\FederatedFileSharing\BackgroundJob
22 22
  */
23 23
 class RetryJob extends Job {
24
-	private bool $retainJob = true;
24
+    private bool $retainJob = true;
25 25
 
26
-	/** @var int max number of attempts to send the request */
27
-	private int $maxTry = 20;
26
+    /** @var int max number of attempts to send the request */
27
+    private int $maxTry = 20;
28 28
 
29
-	/** @var int how much time should be between two tries (10 minutes) */
30
-	private int $interval = 600;
29
+    /** @var int how much time should be between two tries (10 minutes) */
30
+    private int $interval = 600;
31 31
 
32
-	public function __construct(
33
-		private Notifications $notifications,
34
-		ITimeFactory $time,
35
-	) {
36
-		parent::__construct($time);
37
-	}
32
+    public function __construct(
33
+        private Notifications $notifications,
34
+        ITimeFactory $time,
35
+    ) {
36
+        parent::__construct($time);
37
+    }
38 38
 
39
-	/**
40
-	 * Run the job, then remove it from the jobList
41
-	 */
42
-	public function start(IJobList $jobList): void {
43
-		if ($this->shouldRun($this->argument)) {
44
-			parent::start($jobList);
45
-			$jobList->remove($this, $this->argument);
46
-			if ($this->retainJob) {
47
-				$this->reAddJob($jobList, $this->argument);
48
-			}
49
-		}
50
-	}
39
+    /**
40
+     * Run the job, then remove it from the jobList
41
+     */
42
+    public function start(IJobList $jobList): void {
43
+        if ($this->shouldRun($this->argument)) {
44
+            parent::start($jobList);
45
+            $jobList->remove($this, $this->argument);
46
+            if ($this->retainJob) {
47
+                $this->reAddJob($jobList, $this->argument);
48
+            }
49
+        }
50
+    }
51 51
 
52
-	protected function run($argument) {
53
-		$remote = $argument['remote'];
54
-		$remoteId = $argument['remoteId'];
55
-		$token = $argument['token'];
56
-		$action = $argument['action'];
57
-		$data = json_decode($argument['data'], true);
58
-		$try = (int)$argument['try'] + 1;
52
+    protected function run($argument) {
53
+        $remote = $argument['remote'];
54
+        $remoteId = $argument['remoteId'];
55
+        $token = $argument['token'];
56
+        $action = $argument['action'];
57
+        $data = json_decode($argument['data'], true);
58
+        $try = (int)$argument['try'] + 1;
59 59
 
60
-		$result = $this->notifications->sendUpdateToRemote($remote, $remoteId, $token, $action, $data, $try);
60
+        $result = $this->notifications->sendUpdateToRemote($remote, $remoteId, $token, $action, $data, $try);
61 61
 
62
-		if ($result === true || $try > $this->maxTry) {
63
-			$this->retainJob = false;
64
-		}
65
-	}
62
+        if ($result === true || $try > $this->maxTry) {
63
+            $this->retainJob = false;
64
+        }
65
+    }
66 66
 
67
-	/**
68
-	 * Re-add background job with new arguments
69
-	 */
70
-	protected function reAddJob(IJobList $jobList, array $argument): void {
71
-		$jobList->add(RetryJob::class,
72
-			[
73
-				'remote' => $argument['remote'],
74
-				'remoteId' => $argument['remoteId'],
75
-				'token' => $argument['token'],
76
-				'data' => $argument['data'],
77
-				'action' => $argument['action'],
78
-				'try' => (int)$argument['try'] + 1,
79
-				'lastRun' => $this->time->getTime()
80
-			]
81
-		);
82
-	}
67
+    /**
68
+     * Re-add background job with new arguments
69
+     */
70
+    protected function reAddJob(IJobList $jobList, array $argument): void {
71
+        $jobList->add(RetryJob::class,
72
+            [
73
+                'remote' => $argument['remote'],
74
+                'remoteId' => $argument['remoteId'],
75
+                'token' => $argument['token'],
76
+                'data' => $argument['data'],
77
+                'action' => $argument['action'],
78
+                'try' => (int)$argument['try'] + 1,
79
+                'lastRun' => $this->time->getTime()
80
+            ]
81
+        );
82
+    }
83 83
 
84
-	/**
85
-	 * Test if it is time for the next run
86
-	 */
87
-	protected function shouldRun(array $argument): bool {
88
-		$lastRun = (int)$argument['lastRun'];
89
-		return (($this->time->getTime() - $lastRun) > $this->interval);
90
-	}
84
+    /**
85
+     * Test if it is time for the next run
86
+     */
87
+    protected function shouldRun(array $argument): bool {
88
+        $lastRun = (int)$argument['lastRun'];
89
+        return (($this->time->getTime() - $lastRun) > $this->interval);
90
+    }
91 91
 }
Please login to merge, or discard this patch.
lib/public/IBinaryFinder.php 2 patches
Indentation   +7 added lines, -7 removed lines patch added patch discarded remove patch
@@ -31,11 +31,11 @@
 block discarded – undo
31 31
  * @since 25.0.0
32 32
  */
33 33
 interface IBinaryFinder {
34
-	/**
35
-	 * Try to find a program
36
-	 *
37
-	 * @return false|string
38
-	 * @since 25.0.0
39
-	 */
40
-	public function findBinaryPath(string $program);
34
+    /**
35
+     * Try to find a program
36
+     *
37
+     * @return false|string
38
+     * @since 25.0.0
39
+     */
40
+    public function findBinaryPath(string $program);
41 41
 }
Please login to merge, or discard this patch.
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -1,6 +1,6 @@
 block discarded – undo
1 1
 <?php
2 2
 
3
-declare(strict_types = 1);
3
+declare(strict_types=1);
4 4
 /**
5 5
  * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
6 6
  * SPDX-License-Identifier: AGPL-3.0-or-later
Please login to merge, or discard this patch.
lib/private/PreviewManager.php 2 patches
Spacing   +3 added lines, -3 removed lines patch added patch discarded remove patch
@@ -326,7 +326,7 @@  discard block
 block discarded – undo
326 326
 	 */
327 327
 	protected function registerCoreProvider($class, $mimeType, $options = []) {
328 328
 		if (in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) {
329
-			$this->registerProvider($mimeType, function () use ($class, $options) {
329
+			$this->registerProvider($mimeType, function() use ($class, $options) {
330 330
 				return new $class($options);
331 331
 			});
332 332
 		}
@@ -425,14 +425,14 @@  discard block
 block discarded – undo
425 425
 
426 426
 		$providers = $context->getPreviewProviders();
427 427
 		foreach ($providers as $provider) {
428
-			$key = $provider->getMimeTypeRegex() . '-' . $provider->getService();
428
+			$key = $provider->getMimeTypeRegex().'-'.$provider->getService();
429 429
 			if (array_key_exists($key, $this->loadedBootstrapProviders)) {
430 430
 				// Do not load the provider more than once
431 431
 				continue;
432 432
 			}
433 433
 			$this->loadedBootstrapProviders[$key] = null;
434 434
 
435
-			$this->registerProvider($provider->getMimeTypeRegex(), function () use ($provider) {
435
+			$this->registerProvider($provider->getMimeTypeRegex(), function() use ($provider) {
436 436
 				try {
437 437
 					return $this->container->get($provider->getService());
438 438
 				} catch (QueryException $e) {
Please login to merge, or discard this patch.
Indentation   +434 added lines, -434 removed lines patch added patch discarded remove patch
@@ -28,438 +28,438 @@
 block discarded – undo
28 28
 use function array_key_exists;
29 29
 
30 30
 class PreviewManager implements IPreview {
31
-	protected IConfig $config;
32
-	protected IRootFolder $rootFolder;
33
-	protected IAppData $appData;
34
-	protected IEventDispatcher $eventDispatcher;
35
-	private ?Generator $generator = null;
36
-	private GeneratorHelper $helper;
37
-	protected bool $providerListDirty = false;
38
-	protected bool $registeredCoreProviders = false;
39
-	protected array $providers = [];
40
-
41
-	/** @var array mime type => support status */
42
-	protected array $mimeTypeSupportMap = [];
43
-	protected ?array $defaultProviders = null;
44
-	protected ?string $userId;
45
-	private Coordinator $bootstrapCoordinator;
46
-
47
-	/**
48
-	 * Hash map (without value) of loaded bootstrap providers
49
-	 * @psalm-var array<string, null>
50
-	 */
51
-	private array $loadedBootstrapProviders = [];
52
-	private ContainerInterface $container;
53
-	private IBinaryFinder $binaryFinder;
54
-	private IMagickSupport $imagickSupport;
55
-	private bool $enablePreviews;
56
-
57
-	public function __construct(
58
-		IConfig $config,
59
-		IRootFolder $rootFolder,
60
-		IAppData $appData,
61
-		IEventDispatcher $eventDispatcher,
62
-		GeneratorHelper $helper,
63
-		?string $userId,
64
-		Coordinator $bootstrapCoordinator,
65
-		ContainerInterface $container,
66
-		IBinaryFinder $binaryFinder,
67
-		IMagickSupport $imagickSupport,
68
-	) {
69
-		$this->config = $config;
70
-		$this->rootFolder = $rootFolder;
71
-		$this->appData = $appData;
72
-		$this->eventDispatcher = $eventDispatcher;
73
-		$this->helper = $helper;
74
-		$this->userId = $userId;
75
-		$this->bootstrapCoordinator = $bootstrapCoordinator;
76
-		$this->container = $container;
77
-		$this->binaryFinder = $binaryFinder;
78
-		$this->imagickSupport = $imagickSupport;
79
-		$this->enablePreviews = $config->getSystemValueBool('enable_previews', true);
80
-	}
81
-
82
-	/**
83
-	 * In order to improve lazy loading a closure can be registered which will be
84
-	 * called in case preview providers are actually requested
85
-	 *
86
-	 * $callable has to return an instance of \OCP\Preview\IProvider or \OCP\Preview\IProviderV2
87
-	 *
88
-	 * @param string $mimeTypeRegex Regex with the mime types that are supported by this provider
89
-	 * @param \Closure $callable
90
-	 * @return void
91
-	 */
92
-	public function registerProvider($mimeTypeRegex, \Closure $callable): void {
93
-		if (!$this->enablePreviews) {
94
-			return;
95
-		}
96
-
97
-		if (!isset($this->providers[$mimeTypeRegex])) {
98
-			$this->providers[$mimeTypeRegex] = [];
99
-		}
100
-		$this->providers[$mimeTypeRegex][] = $callable;
101
-		$this->providerListDirty = true;
102
-	}
103
-
104
-	/**
105
-	 * Get all providers
106
-	 */
107
-	public function getProviders(): array {
108
-		if (!$this->enablePreviews) {
109
-			return [];
110
-		}
111
-
112
-		$this->registerCoreProviders();
113
-		$this->registerBootstrapProviders();
114
-		if ($this->providerListDirty) {
115
-			$keys = array_map('strlen', array_keys($this->providers));
116
-			array_multisort($keys, SORT_DESC, $this->providers);
117
-			$this->providerListDirty = false;
118
-		}
119
-
120
-		return $this->providers;
121
-	}
122
-
123
-	/**
124
-	 * Does the manager have any providers
125
-	 */
126
-	public function hasProviders(): bool {
127
-		$this->registerCoreProviders();
128
-		return !empty($this->providers);
129
-	}
130
-
131
-	private function getGenerator(): Generator {
132
-		if ($this->generator === null) {
133
-			$this->generator = new Generator(
134
-				$this->config,
135
-				$this,
136
-				$this->appData,
137
-				new GeneratorHelper(
138
-					$this->rootFolder,
139
-					$this->config
140
-				),
141
-				$this->eventDispatcher,
142
-				$this->container->get(LoggerInterface::class),
143
-			);
144
-		}
145
-		return $this->generator;
146
-	}
147
-
148
-	public function getPreview(
149
-		File $file,
150
-		$width = -1,
151
-		$height = -1,
152
-		$crop = false,
153
-		$mode = IPreview::MODE_FILL,
154
-		$mimeType = null,
155
-		bool $cacheResult = true,
156
-	): ISimpleFile {
157
-		$this->throwIfPreviewsDisabled($file, $mimeType);
158
-		$previewConcurrency = $this->getGenerator()->getNumConcurrentPreviews('preview_concurrency_all');
159
-		$sem = Generator::guardWithSemaphore(Generator::SEMAPHORE_ID_ALL, $previewConcurrency);
160
-		try {
161
-			$preview = $this->getGenerator()->getPreview($file, $width, $height, $crop, $mode, $mimeType, $cacheResult);
162
-		} finally {
163
-			Generator::unguardWithSemaphore($sem);
164
-		}
165
-
166
-		return $preview;
167
-	}
168
-
169
-	/**
170
-	 * Generates previews of a file
171
-	 *
172
-	 * @param File $file
173
-	 * @param array $specifications
174
-	 * @param string $mimeType
175
-	 * @return ISimpleFile the last preview that was generated
176
-	 * @throws NotFoundException
177
-	 * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
178
-	 * @since 19.0.0
179
-	 */
180
-	public function generatePreviews(File $file, array $specifications, $mimeType = null) {
181
-		$this->throwIfPreviewsDisabled($file, $mimeType);
182
-		return $this->getGenerator()->generatePreviews($file, $specifications, $mimeType);
183
-	}
184
-
185
-	/**
186
-	 * returns true if the passed mime type is supported
187
-	 *
188
-	 * @param string $mimeType
189
-	 * @return boolean
190
-	 */
191
-	public function isMimeSupported($mimeType = '*') {
192
-		if (!$this->enablePreviews) {
193
-			return false;
194
-		}
195
-
196
-		if (isset($this->mimeTypeSupportMap[$mimeType])) {
197
-			return $this->mimeTypeSupportMap[$mimeType];
198
-		}
199
-
200
-		$this->registerCoreProviders();
201
-		$this->registerBootstrapProviders();
202
-		$providerMimeTypes = array_keys($this->providers);
203
-		foreach ($providerMimeTypes as $supportedMimeType) {
204
-			if (preg_match($supportedMimeType, $mimeType)) {
205
-				$this->mimeTypeSupportMap[$mimeType] = true;
206
-				return true;
207
-			}
208
-		}
209
-		$this->mimeTypeSupportMap[$mimeType] = false;
210
-		return false;
211
-	}
212
-
213
-	/**
214
-	 * Check if a preview can be generated for a file
215
-	 */
216
-	public function isAvailable(\OCP\Files\FileInfo $file, ?string $mimeType = null): bool {
217
-		if (!$this->enablePreviews) {
218
-			return false;
219
-		}
220
-
221
-		$fileMimeType = $mimeType ?? $file->getMimeType();
222
-
223
-		$this->registerCoreProviders();
224
-		if (!$this->isMimeSupported($fileMimeType)) {
225
-			return false;
226
-		}
227
-
228
-		$mount = $file->getMountPoint();
229
-		if ($mount and !$mount->getOption('previews', true)) {
230
-			return false;
231
-		}
232
-
233
-		foreach ($this->providers as $supportedMimeType => $providers) {
234
-			if (preg_match($supportedMimeType, $fileMimeType)) {
235
-				foreach ($providers as $providerClosure) {
236
-					$provider = $this->helper->getProvider($providerClosure);
237
-					if (!($provider instanceof IProviderV2)) {
238
-						continue;
239
-					}
240
-
241
-					if ($provider->isAvailable($file)) {
242
-						return true;
243
-					}
244
-				}
245
-			}
246
-		}
247
-		return false;
248
-	}
249
-
250
-	/**
251
-	 * List of enabled default providers
252
-	 *
253
-	 * The following providers are enabled by default:
254
-	 *  - OC\Preview\PNG
255
-	 *  - OC\Preview\JPEG
256
-	 *  - OC\Preview\GIF
257
-	 *  - OC\Preview\BMP
258
-	 *  - OC\Preview\XBitmap
259
-	 *  - OC\Preview\MarkDown
260
-	 *  - OC\Preview\MP3
261
-	 *  - OC\Preview\TXT
262
-	 *
263
-	 * The following providers are disabled by default due to performance or privacy concerns:
264
-	 *  - OC\Preview\Font
265
-	 *  - OC\Preview\HEIC
266
-	 *  - OC\Preview\Illustrator
267
-	 *  - OC\Preview\Movie
268
-	 *  - OC\Preview\MSOfficeDoc
269
-	 *  - OC\Preview\MSOffice2003
270
-	 *  - OC\Preview\MSOffice2007
271
-	 *  - OC\Preview\OpenDocument
272
-	 *  - OC\Preview\PDF
273
-	 *  - OC\Preview\Photoshop
274
-	 *  - OC\Preview\Postscript
275
-	 *  - OC\Preview\StarOffice
276
-	 *  - OC\Preview\SVG
277
-	 *  - OC\Preview\TIFF
278
-	 *
279
-	 * @return array
280
-	 */
281
-	protected function getEnabledDefaultProvider() {
282
-		if ($this->defaultProviders !== null) {
283
-			return $this->defaultProviders;
284
-		}
285
-
286
-		$imageProviders = [
287
-			Preview\PNG::class,
288
-			Preview\JPEG::class,
289
-			Preview\GIF::class,
290
-			Preview\BMP::class,
291
-			Preview\XBitmap::class,
292
-			Preview\Krita::class,
293
-			Preview\WebP::class,
294
-		];
295
-
296
-		$this->defaultProviders = $this->config->getSystemValue('enabledPreviewProviders', array_merge([
297
-			Preview\MarkDown::class,
298
-			Preview\MP3::class,
299
-			Preview\TXT::class,
300
-			Preview\OpenDocument::class,
301
-		], $imageProviders));
302
-
303
-		if (in_array(Preview\Image::class, $this->defaultProviders)) {
304
-			$this->defaultProviders = array_merge($this->defaultProviders, $imageProviders);
305
-		}
306
-		$this->defaultProviders = array_unique($this->defaultProviders);
307
-		return $this->defaultProviders;
308
-	}
309
-
310
-	/**
311
-	 * Register the default providers (if enabled)
312
-	 *
313
-	 * @param string $class
314
-	 * @param string $mimeType
315
-	 */
316
-	protected function registerCoreProvider($class, $mimeType, $options = []) {
317
-		if (in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) {
318
-			$this->registerProvider($mimeType, function () use ($class, $options) {
319
-				return new $class($options);
320
-			});
321
-		}
322
-	}
323
-
324
-	/**
325
-	 * Register the default providers (if enabled)
326
-	 */
327
-	protected function registerCoreProviders() {
328
-		if ($this->registeredCoreProviders) {
329
-			return;
330
-		}
331
-		$this->registeredCoreProviders = true;
332
-
333
-		$this->registerCoreProvider(Preview\TXT::class, '/text\/plain/');
334
-		$this->registerCoreProvider(Preview\MarkDown::class, '/text\/(x-)?markdown/');
335
-		$this->registerCoreProvider(Preview\PNG::class, '/image\/png/');
336
-		$this->registerCoreProvider(Preview\JPEG::class, '/image\/jpeg/');
337
-		$this->registerCoreProvider(Preview\GIF::class, '/image\/gif/');
338
-		$this->registerCoreProvider(Preview\BMP::class, '/image\/bmp/');
339
-		$this->registerCoreProvider(Preview\XBitmap::class, '/image\/x-xbitmap/');
340
-		$this->registerCoreProvider(Preview\WebP::class, '/image\/webp/');
341
-		$this->registerCoreProvider(Preview\Krita::class, '/application\/x-krita/');
342
-		$this->registerCoreProvider(Preview\MP3::class, '/audio\/mpeg$/');
343
-		$this->registerCoreProvider(Preview\OpenDocument::class, '/application\/vnd.oasis.opendocument.*/');
344
-		$this->registerCoreProvider(Preview\Imaginary::class, Preview\Imaginary::supportedMimeTypes());
345
-		$this->registerCoreProvider(Preview\ImaginaryPDF::class, Preview\ImaginaryPDF::supportedMimeTypes());
346
-
347
-		// SVG and Bitmap require imagick
348
-		if ($this->imagickSupport->hasExtension()) {
349
-			$imagickProviders = [
350
-				'SVG' => ['mimetype' => '/image\/svg\+xml/', 'class' => Preview\SVG::class],
351
-				'TIFF' => ['mimetype' => '/image\/tiff/', 'class' => Preview\TIFF::class],
352
-				'PDF' => ['mimetype' => '/application\/pdf/', 'class' => Preview\PDF::class],
353
-				'AI' => ['mimetype' => '/application\/illustrator/', 'class' => Preview\Illustrator::class],
354
-				'PSD' => ['mimetype' => '/application\/x-photoshop/', 'class' => Preview\Photoshop::class],
355
-				'EPS' => ['mimetype' => '/application\/postscript/', 'class' => Preview\Postscript::class],
356
-				'TTF' => ['mimetype' => '/application\/(?:font-sfnt|x-font$)/', 'class' => Preview\Font::class],
357
-				'HEIC' => ['mimetype' => '/image\/(x-)?hei(f|c)/', 'class' => Preview\HEIC::class],
358
-				'TGA' => ['mimetype' => '/image\/(x-)?t(ar)?ga/', 'class' => Preview\TGA::class],
359
-				'SGI' => ['mimetype' => '/image\/(x-)?sgi/', 'class' => Preview\SGI::class],
360
-			];
361
-
362
-			foreach ($imagickProviders as $queryFormat => $provider) {
363
-				$class = $provider['class'];
364
-				if (!in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) {
365
-					continue;
366
-				}
367
-
368
-				if ($this->imagickSupport->supportsFormat($queryFormat)) {
369
-					$this->registerCoreProvider($class, $provider['mimetype']);
370
-				}
371
-			}
372
-		}
373
-
374
-		$this->registerCoreProvidersOffice();
375
-
376
-		// Video requires avconv or ffmpeg
377
-		if (in_array(Preview\Movie::class, $this->getEnabledDefaultProvider())) {
378
-			$movieBinary = $this->config->getSystemValue('preview_ffmpeg_path', null);
379
-			if (!is_string($movieBinary)) {
380
-				$movieBinary = $this->binaryFinder->findBinaryPath('avconv');
381
-				if (!is_string($movieBinary)) {
382
-					$movieBinary = $this->binaryFinder->findBinaryPath('ffmpeg');
383
-				}
384
-			}
385
-
386
-
387
-			if (is_string($movieBinary)) {
388
-				$this->registerCoreProvider(Preview\Movie::class, '/video\/.*/', ['movieBinary' => $movieBinary]);
389
-			}
390
-		}
391
-	}
392
-
393
-	private function registerCoreProvidersOffice(): void {
394
-		$officeProviders = [
395
-			['mimetype' => '/application\/msword/', 'class' => Preview\MSOfficeDoc::class],
396
-			['mimetype' => '/application\/vnd.ms-.*/', 'class' => Preview\MSOffice2003::class],
397
-			['mimetype' => '/application\/vnd.openxmlformats-officedocument.*/', 'class' => Preview\MSOffice2007::class],
398
-			['mimetype' => '/application\/vnd.oasis.opendocument.*/', 'class' => Preview\OpenDocument::class],
399
-			['mimetype' => '/application\/vnd.sun.xml.*/', 'class' => Preview\StarOffice::class],
400
-			['mimetype' => '/image\/emf/', 'class' => Preview\EMF::class],
401
-		];
402
-
403
-		$findBinary = true;
404
-		$officeBinary = false;
405
-
406
-		foreach ($officeProviders as $provider) {
407
-			$class = $provider['class'];
408
-			if (!in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) {
409
-				continue;
410
-			}
411
-
412
-			if ($findBinary) {
413
-				// Office requires openoffice or libreoffice
414
-				$officeBinary = $this->config->getSystemValue('preview_libreoffice_path', false);
415
-				if ($officeBinary === false) {
416
-					$officeBinary = $this->binaryFinder->findBinaryPath('libreoffice');
417
-				}
418
-				if ($officeBinary === false) {
419
-					$officeBinary = $this->binaryFinder->findBinaryPath('openoffice');
420
-				}
421
-				$findBinary = false;
422
-			}
423
-
424
-			if ($officeBinary) {
425
-				$this->registerCoreProvider($class, $provider['mimetype'], ['officeBinary' => $officeBinary]);
426
-			}
427
-		}
428
-	}
429
-
430
-	private function registerBootstrapProviders(): void {
431
-		$context = $this->bootstrapCoordinator->getRegistrationContext();
432
-
433
-		if ($context === null) {
434
-			// Just ignore for now
435
-			return;
436
-		}
437
-
438
-		$providers = $context->getPreviewProviders();
439
-		foreach ($providers as $provider) {
440
-			$key = $provider->getMimeTypeRegex() . '-' . $provider->getService();
441
-			if (array_key_exists($key, $this->loadedBootstrapProviders)) {
442
-				// Do not load the provider more than once
443
-				continue;
444
-			}
445
-			$this->loadedBootstrapProviders[$key] = null;
446
-
447
-			$this->registerProvider($provider->getMimeTypeRegex(), function () use ($provider) {
448
-				try {
449
-					return $this->container->get($provider->getService());
450
-				} catch (QueryException $e) {
451
-					return null;
452
-				}
453
-			});
454
-		}
455
-	}
456
-
457
-	/**
458
-	 * @throws NotFoundException if preview generation is disabled
459
-	 */
460
-	private function throwIfPreviewsDisabled(File $file, ?string $mimeType = null): void {
461
-		if (!$this->isAvailable($file, $mimeType)) {
462
-			throw new NotFoundException('Previews disabled');
463
-		}
464
-	}
31
+    protected IConfig $config;
32
+    protected IRootFolder $rootFolder;
33
+    protected IAppData $appData;
34
+    protected IEventDispatcher $eventDispatcher;
35
+    private ?Generator $generator = null;
36
+    private GeneratorHelper $helper;
37
+    protected bool $providerListDirty = false;
38
+    protected bool $registeredCoreProviders = false;
39
+    protected array $providers = [];
40
+
41
+    /** @var array mime type => support status */
42
+    protected array $mimeTypeSupportMap = [];
43
+    protected ?array $defaultProviders = null;
44
+    protected ?string $userId;
45
+    private Coordinator $bootstrapCoordinator;
46
+
47
+    /**
48
+     * Hash map (without value) of loaded bootstrap providers
49
+     * @psalm-var array<string, null>
50
+     */
51
+    private array $loadedBootstrapProviders = [];
52
+    private ContainerInterface $container;
53
+    private IBinaryFinder $binaryFinder;
54
+    private IMagickSupport $imagickSupport;
55
+    private bool $enablePreviews;
56
+
57
+    public function __construct(
58
+        IConfig $config,
59
+        IRootFolder $rootFolder,
60
+        IAppData $appData,
61
+        IEventDispatcher $eventDispatcher,
62
+        GeneratorHelper $helper,
63
+        ?string $userId,
64
+        Coordinator $bootstrapCoordinator,
65
+        ContainerInterface $container,
66
+        IBinaryFinder $binaryFinder,
67
+        IMagickSupport $imagickSupport,
68
+    ) {
69
+        $this->config = $config;
70
+        $this->rootFolder = $rootFolder;
71
+        $this->appData = $appData;
72
+        $this->eventDispatcher = $eventDispatcher;
73
+        $this->helper = $helper;
74
+        $this->userId = $userId;
75
+        $this->bootstrapCoordinator = $bootstrapCoordinator;
76
+        $this->container = $container;
77
+        $this->binaryFinder = $binaryFinder;
78
+        $this->imagickSupport = $imagickSupport;
79
+        $this->enablePreviews = $config->getSystemValueBool('enable_previews', true);
80
+    }
81
+
82
+    /**
83
+     * In order to improve lazy loading a closure can be registered which will be
84
+     * called in case preview providers are actually requested
85
+     *
86
+     * $callable has to return an instance of \OCP\Preview\IProvider or \OCP\Preview\IProviderV2
87
+     *
88
+     * @param string $mimeTypeRegex Regex with the mime types that are supported by this provider
89
+     * @param \Closure $callable
90
+     * @return void
91
+     */
92
+    public function registerProvider($mimeTypeRegex, \Closure $callable): void {
93
+        if (!$this->enablePreviews) {
94
+            return;
95
+        }
96
+
97
+        if (!isset($this->providers[$mimeTypeRegex])) {
98
+            $this->providers[$mimeTypeRegex] = [];
99
+        }
100
+        $this->providers[$mimeTypeRegex][] = $callable;
101
+        $this->providerListDirty = true;
102
+    }
103
+
104
+    /**
105
+     * Get all providers
106
+     */
107
+    public function getProviders(): array {
108
+        if (!$this->enablePreviews) {
109
+            return [];
110
+        }
111
+
112
+        $this->registerCoreProviders();
113
+        $this->registerBootstrapProviders();
114
+        if ($this->providerListDirty) {
115
+            $keys = array_map('strlen', array_keys($this->providers));
116
+            array_multisort($keys, SORT_DESC, $this->providers);
117
+            $this->providerListDirty = false;
118
+        }
119
+
120
+        return $this->providers;
121
+    }
122
+
123
+    /**
124
+     * Does the manager have any providers
125
+     */
126
+    public function hasProviders(): bool {
127
+        $this->registerCoreProviders();
128
+        return !empty($this->providers);
129
+    }
130
+
131
+    private function getGenerator(): Generator {
132
+        if ($this->generator === null) {
133
+            $this->generator = new Generator(
134
+                $this->config,
135
+                $this,
136
+                $this->appData,
137
+                new GeneratorHelper(
138
+                    $this->rootFolder,
139
+                    $this->config
140
+                ),
141
+                $this->eventDispatcher,
142
+                $this->container->get(LoggerInterface::class),
143
+            );
144
+        }
145
+        return $this->generator;
146
+    }
147
+
148
+    public function getPreview(
149
+        File $file,
150
+        $width = -1,
151
+        $height = -1,
152
+        $crop = false,
153
+        $mode = IPreview::MODE_FILL,
154
+        $mimeType = null,
155
+        bool $cacheResult = true,
156
+    ): ISimpleFile {
157
+        $this->throwIfPreviewsDisabled($file, $mimeType);
158
+        $previewConcurrency = $this->getGenerator()->getNumConcurrentPreviews('preview_concurrency_all');
159
+        $sem = Generator::guardWithSemaphore(Generator::SEMAPHORE_ID_ALL, $previewConcurrency);
160
+        try {
161
+            $preview = $this->getGenerator()->getPreview($file, $width, $height, $crop, $mode, $mimeType, $cacheResult);
162
+        } finally {
163
+            Generator::unguardWithSemaphore($sem);
164
+        }
165
+
166
+        return $preview;
167
+    }
168
+
169
+    /**
170
+     * Generates previews of a file
171
+     *
172
+     * @param File $file
173
+     * @param array $specifications
174
+     * @param string $mimeType
175
+     * @return ISimpleFile the last preview that was generated
176
+     * @throws NotFoundException
177
+     * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
178
+     * @since 19.0.0
179
+     */
180
+    public function generatePreviews(File $file, array $specifications, $mimeType = null) {
181
+        $this->throwIfPreviewsDisabled($file, $mimeType);
182
+        return $this->getGenerator()->generatePreviews($file, $specifications, $mimeType);
183
+    }
184
+
185
+    /**
186
+     * returns true if the passed mime type is supported
187
+     *
188
+     * @param string $mimeType
189
+     * @return boolean
190
+     */
191
+    public function isMimeSupported($mimeType = '*') {
192
+        if (!$this->enablePreviews) {
193
+            return false;
194
+        }
195
+
196
+        if (isset($this->mimeTypeSupportMap[$mimeType])) {
197
+            return $this->mimeTypeSupportMap[$mimeType];
198
+        }
199
+
200
+        $this->registerCoreProviders();
201
+        $this->registerBootstrapProviders();
202
+        $providerMimeTypes = array_keys($this->providers);
203
+        foreach ($providerMimeTypes as $supportedMimeType) {
204
+            if (preg_match($supportedMimeType, $mimeType)) {
205
+                $this->mimeTypeSupportMap[$mimeType] = true;
206
+                return true;
207
+            }
208
+        }
209
+        $this->mimeTypeSupportMap[$mimeType] = false;
210
+        return false;
211
+    }
212
+
213
+    /**
214
+     * Check if a preview can be generated for a file
215
+     */
216
+    public function isAvailable(\OCP\Files\FileInfo $file, ?string $mimeType = null): bool {
217
+        if (!$this->enablePreviews) {
218
+            return false;
219
+        }
220
+
221
+        $fileMimeType = $mimeType ?? $file->getMimeType();
222
+
223
+        $this->registerCoreProviders();
224
+        if (!$this->isMimeSupported($fileMimeType)) {
225
+            return false;
226
+        }
227
+
228
+        $mount = $file->getMountPoint();
229
+        if ($mount and !$mount->getOption('previews', true)) {
230
+            return false;
231
+        }
232
+
233
+        foreach ($this->providers as $supportedMimeType => $providers) {
234
+            if (preg_match($supportedMimeType, $fileMimeType)) {
235
+                foreach ($providers as $providerClosure) {
236
+                    $provider = $this->helper->getProvider($providerClosure);
237
+                    if (!($provider instanceof IProviderV2)) {
238
+                        continue;
239
+                    }
240
+
241
+                    if ($provider->isAvailable($file)) {
242
+                        return true;
243
+                    }
244
+                }
245
+            }
246
+        }
247
+        return false;
248
+    }
249
+
250
+    /**
251
+     * List of enabled default providers
252
+     *
253
+     * The following providers are enabled by default:
254
+     *  - OC\Preview\PNG
255
+     *  - OC\Preview\JPEG
256
+     *  - OC\Preview\GIF
257
+     *  - OC\Preview\BMP
258
+     *  - OC\Preview\XBitmap
259
+     *  - OC\Preview\MarkDown
260
+     *  - OC\Preview\MP3
261
+     *  - OC\Preview\TXT
262
+     *
263
+     * The following providers are disabled by default due to performance or privacy concerns:
264
+     *  - OC\Preview\Font
265
+     *  - OC\Preview\HEIC
266
+     *  - OC\Preview\Illustrator
267
+     *  - OC\Preview\Movie
268
+     *  - OC\Preview\MSOfficeDoc
269
+     *  - OC\Preview\MSOffice2003
270
+     *  - OC\Preview\MSOffice2007
271
+     *  - OC\Preview\OpenDocument
272
+     *  - OC\Preview\PDF
273
+     *  - OC\Preview\Photoshop
274
+     *  - OC\Preview\Postscript
275
+     *  - OC\Preview\StarOffice
276
+     *  - OC\Preview\SVG
277
+     *  - OC\Preview\TIFF
278
+     *
279
+     * @return array
280
+     */
281
+    protected function getEnabledDefaultProvider() {
282
+        if ($this->defaultProviders !== null) {
283
+            return $this->defaultProviders;
284
+        }
285
+
286
+        $imageProviders = [
287
+            Preview\PNG::class,
288
+            Preview\JPEG::class,
289
+            Preview\GIF::class,
290
+            Preview\BMP::class,
291
+            Preview\XBitmap::class,
292
+            Preview\Krita::class,
293
+            Preview\WebP::class,
294
+        ];
295
+
296
+        $this->defaultProviders = $this->config->getSystemValue('enabledPreviewProviders', array_merge([
297
+            Preview\MarkDown::class,
298
+            Preview\MP3::class,
299
+            Preview\TXT::class,
300
+            Preview\OpenDocument::class,
301
+        ], $imageProviders));
302
+
303
+        if (in_array(Preview\Image::class, $this->defaultProviders)) {
304
+            $this->defaultProviders = array_merge($this->defaultProviders, $imageProviders);
305
+        }
306
+        $this->defaultProviders = array_unique($this->defaultProviders);
307
+        return $this->defaultProviders;
308
+    }
309
+
310
+    /**
311
+     * Register the default providers (if enabled)
312
+     *
313
+     * @param string $class
314
+     * @param string $mimeType
315
+     */
316
+    protected function registerCoreProvider($class, $mimeType, $options = []) {
317
+        if (in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) {
318
+            $this->registerProvider($mimeType, function () use ($class, $options) {
319
+                return new $class($options);
320
+            });
321
+        }
322
+    }
323
+
324
+    /**
325
+     * Register the default providers (if enabled)
326
+     */
327
+    protected function registerCoreProviders() {
328
+        if ($this->registeredCoreProviders) {
329
+            return;
330
+        }
331
+        $this->registeredCoreProviders = true;
332
+
333
+        $this->registerCoreProvider(Preview\TXT::class, '/text\/plain/');
334
+        $this->registerCoreProvider(Preview\MarkDown::class, '/text\/(x-)?markdown/');
335
+        $this->registerCoreProvider(Preview\PNG::class, '/image\/png/');
336
+        $this->registerCoreProvider(Preview\JPEG::class, '/image\/jpeg/');
337
+        $this->registerCoreProvider(Preview\GIF::class, '/image\/gif/');
338
+        $this->registerCoreProvider(Preview\BMP::class, '/image\/bmp/');
339
+        $this->registerCoreProvider(Preview\XBitmap::class, '/image\/x-xbitmap/');
340
+        $this->registerCoreProvider(Preview\WebP::class, '/image\/webp/');
341
+        $this->registerCoreProvider(Preview\Krita::class, '/application\/x-krita/');
342
+        $this->registerCoreProvider(Preview\MP3::class, '/audio\/mpeg$/');
343
+        $this->registerCoreProvider(Preview\OpenDocument::class, '/application\/vnd.oasis.opendocument.*/');
344
+        $this->registerCoreProvider(Preview\Imaginary::class, Preview\Imaginary::supportedMimeTypes());
345
+        $this->registerCoreProvider(Preview\ImaginaryPDF::class, Preview\ImaginaryPDF::supportedMimeTypes());
346
+
347
+        // SVG and Bitmap require imagick
348
+        if ($this->imagickSupport->hasExtension()) {
349
+            $imagickProviders = [
350
+                'SVG' => ['mimetype' => '/image\/svg\+xml/', 'class' => Preview\SVG::class],
351
+                'TIFF' => ['mimetype' => '/image\/tiff/', 'class' => Preview\TIFF::class],
352
+                'PDF' => ['mimetype' => '/application\/pdf/', 'class' => Preview\PDF::class],
353
+                'AI' => ['mimetype' => '/application\/illustrator/', 'class' => Preview\Illustrator::class],
354
+                'PSD' => ['mimetype' => '/application\/x-photoshop/', 'class' => Preview\Photoshop::class],
355
+                'EPS' => ['mimetype' => '/application\/postscript/', 'class' => Preview\Postscript::class],
356
+                'TTF' => ['mimetype' => '/application\/(?:font-sfnt|x-font$)/', 'class' => Preview\Font::class],
357
+                'HEIC' => ['mimetype' => '/image\/(x-)?hei(f|c)/', 'class' => Preview\HEIC::class],
358
+                'TGA' => ['mimetype' => '/image\/(x-)?t(ar)?ga/', 'class' => Preview\TGA::class],
359
+                'SGI' => ['mimetype' => '/image\/(x-)?sgi/', 'class' => Preview\SGI::class],
360
+            ];
361
+
362
+            foreach ($imagickProviders as $queryFormat => $provider) {
363
+                $class = $provider['class'];
364
+                if (!in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) {
365
+                    continue;
366
+                }
367
+
368
+                if ($this->imagickSupport->supportsFormat($queryFormat)) {
369
+                    $this->registerCoreProvider($class, $provider['mimetype']);
370
+                }
371
+            }
372
+        }
373
+
374
+        $this->registerCoreProvidersOffice();
375
+
376
+        // Video requires avconv or ffmpeg
377
+        if (in_array(Preview\Movie::class, $this->getEnabledDefaultProvider())) {
378
+            $movieBinary = $this->config->getSystemValue('preview_ffmpeg_path', null);
379
+            if (!is_string($movieBinary)) {
380
+                $movieBinary = $this->binaryFinder->findBinaryPath('avconv');
381
+                if (!is_string($movieBinary)) {
382
+                    $movieBinary = $this->binaryFinder->findBinaryPath('ffmpeg');
383
+                }
384
+            }
385
+
386
+
387
+            if (is_string($movieBinary)) {
388
+                $this->registerCoreProvider(Preview\Movie::class, '/video\/.*/', ['movieBinary' => $movieBinary]);
389
+            }
390
+        }
391
+    }
392
+
393
+    private function registerCoreProvidersOffice(): void {
394
+        $officeProviders = [
395
+            ['mimetype' => '/application\/msword/', 'class' => Preview\MSOfficeDoc::class],
396
+            ['mimetype' => '/application\/vnd.ms-.*/', 'class' => Preview\MSOffice2003::class],
397
+            ['mimetype' => '/application\/vnd.openxmlformats-officedocument.*/', 'class' => Preview\MSOffice2007::class],
398
+            ['mimetype' => '/application\/vnd.oasis.opendocument.*/', 'class' => Preview\OpenDocument::class],
399
+            ['mimetype' => '/application\/vnd.sun.xml.*/', 'class' => Preview\StarOffice::class],
400
+            ['mimetype' => '/image\/emf/', 'class' => Preview\EMF::class],
401
+        ];
402
+
403
+        $findBinary = true;
404
+        $officeBinary = false;
405
+
406
+        foreach ($officeProviders as $provider) {
407
+            $class = $provider['class'];
408
+            if (!in_array(trim($class, '\\'), $this->getEnabledDefaultProvider())) {
409
+                continue;
410
+            }
411
+
412
+            if ($findBinary) {
413
+                // Office requires openoffice or libreoffice
414
+                $officeBinary = $this->config->getSystemValue('preview_libreoffice_path', false);
415
+                if ($officeBinary === false) {
416
+                    $officeBinary = $this->binaryFinder->findBinaryPath('libreoffice');
417
+                }
418
+                if ($officeBinary === false) {
419
+                    $officeBinary = $this->binaryFinder->findBinaryPath('openoffice');
420
+                }
421
+                $findBinary = false;
422
+            }
423
+
424
+            if ($officeBinary) {
425
+                $this->registerCoreProvider($class, $provider['mimetype'], ['officeBinary' => $officeBinary]);
426
+            }
427
+        }
428
+    }
429
+
430
+    private function registerBootstrapProviders(): void {
431
+        $context = $this->bootstrapCoordinator->getRegistrationContext();
432
+
433
+        if ($context === null) {
434
+            // Just ignore for now
435
+            return;
436
+        }
437
+
438
+        $providers = $context->getPreviewProviders();
439
+        foreach ($providers as $provider) {
440
+            $key = $provider->getMimeTypeRegex() . '-' . $provider->getService();
441
+            if (array_key_exists($key, $this->loadedBootstrapProviders)) {
442
+                // Do not load the provider more than once
443
+                continue;
444
+            }
445
+            $this->loadedBootstrapProviders[$key] = null;
446
+
447
+            $this->registerProvider($provider->getMimeTypeRegex(), function () use ($provider) {
448
+                try {
449
+                    return $this->container->get($provider->getService());
450
+                } catch (QueryException $e) {
451
+                    return null;
452
+                }
453
+            });
454
+        }
455
+    }
456
+
457
+    /**
458
+     * @throws NotFoundException if preview generation is disabled
459
+     */
460
+    private function throwIfPreviewsDisabled(File $file, ?string $mimeType = null): void {
461
+        if (!$this->isAvailable($file, $mimeType)) {
462
+            throw new NotFoundException('Previews disabled');
463
+        }
464
+    }
465 465
 }
Please login to merge, or discard this patch.
apps/settings/lib/Controller/HelpController.php 2 patches
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -79,7 +79,7 @@
 block discarded – undo
79 79
 		}
80 80
 
81 81
 		$documentationUrl = $this->urlGenerator->getAbsoluteURL(
82
-			$this->urlGenerator->linkTo('', 'core/doc/' . $mode . '/index.html')
82
+			$this->urlGenerator->linkTo('', 'core/doc/'.$mode.'/index.html')
83 83
 		);
84 84
 
85 85
 		$urlUserDocs = $this->urlGenerator->linkToRoute('settings.Help.help', ['mode' => 'user']);
Please login to merge, or discard this patch.
Indentation   +57 added lines, -57 removed lines patch added patch discarded remove patch
@@ -25,67 +25,67 @@
 block discarded – undo
25 25
 #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
26 26
 class HelpController extends Controller {
27 27
 
28
-	public function __construct(
29
-		string $appName,
30
-		IRequest $request,
31
-		private INavigationManager $navigationManager,
32
-		private IURLGenerator $urlGenerator,
33
-		/** @var string */
34
-		private ?string $userId,
35
-		private IGroupManager $groupManager,
36
-		private IL10N $l10n,
37
-		private IConfig $config,
38
-		private IAppConfig $appConfig,
39
-	) {
40
-		parent::__construct($appName, $request);
41
-	}
28
+    public function __construct(
29
+        string $appName,
30
+        IRequest $request,
31
+        private INavigationManager $navigationManager,
32
+        private IURLGenerator $urlGenerator,
33
+        /** @var string */
34
+        private ?string $userId,
35
+        private IGroupManager $groupManager,
36
+        private IL10N $l10n,
37
+        private IConfig $config,
38
+        private IAppConfig $appConfig,
39
+    ) {
40
+        parent::__construct($appName, $request);
41
+    }
42 42
 
43
-	/**
44
-	 * @return TemplateResponse
45
-	 *
46
-	 * @NoSubAdminRequired
47
-	 */
48
-	#[NoCSRFRequired]
49
-	#[NoAdminRequired]
50
-	public function help(string $mode = 'user'): TemplateResponse {
51
-		$this->navigationManager->setActiveEntry('help');
52
-		$pageTitle = $this->l10n->t('Administrator documentation');
53
-		if ($mode !== 'admin') {
54
-			$pageTitle = $this->l10n->t('User documentation');
55
-			$mode = 'user';
56
-		}
43
+    /**
44
+     * @return TemplateResponse
45
+     *
46
+     * @NoSubAdminRequired
47
+     */
48
+    #[NoCSRFRequired]
49
+    #[NoAdminRequired]
50
+    public function help(string $mode = 'user'): TemplateResponse {
51
+        $this->navigationManager->setActiveEntry('help');
52
+        $pageTitle = $this->l10n->t('Administrator documentation');
53
+        if ($mode !== 'admin') {
54
+            $pageTitle = $this->l10n->t('User documentation');
55
+            $mode = 'user';
56
+        }
57 57
 
58
-		$documentationUrl = $this->urlGenerator->getAbsoluteURL(
59
-			$this->urlGenerator->linkTo('', 'core/doc/' . $mode . '/index.html')
60
-		);
58
+        $documentationUrl = $this->urlGenerator->getAbsoluteURL(
59
+            $this->urlGenerator->linkTo('', 'core/doc/' . $mode . '/index.html')
60
+        );
61 61
 
62
-		$urlUserDocs = $this->urlGenerator->linkToRoute('settings.Help.help', ['mode' => 'user']);
63
-		$urlAdminDocs = $this->urlGenerator->linkToRoute('settings.Help.help', ['mode' => 'admin']);
62
+        $urlUserDocs = $this->urlGenerator->linkToRoute('settings.Help.help', ['mode' => 'user']);
63
+        $urlAdminDocs = $this->urlGenerator->linkToRoute('settings.Help.help', ['mode' => 'admin']);
64 64
 
65
-		$knowledgebaseEmbedded = $this->config->getSystemValueBool('knowledgebase.embedded', false);
66
-		if (!$knowledgebaseEmbedded) {
67
-			$pageTitle = $this->l10n->t('Nextcloud help overview');
68
-			$urlUserDocs = $this->urlGenerator->linkToDocs('user');
69
-			$urlAdminDocs = $this->urlGenerator->linkToDocs('admin');
70
-		}
65
+        $knowledgebaseEmbedded = $this->config->getSystemValueBool('knowledgebase.embedded', false);
66
+        if (!$knowledgebaseEmbedded) {
67
+            $pageTitle = $this->l10n->t('Nextcloud help overview');
68
+            $urlUserDocs = $this->urlGenerator->linkToDocs('user');
69
+            $urlAdminDocs = $this->urlGenerator->linkToDocs('admin');
70
+        }
71 71
 
72
-		$legalNoticeUrl = $this->appConfig->getValueString('theming', 'imprintUrl');
73
-		$privacyUrl = $this->appConfig->getValueString('theming', 'privacyUrl');
72
+        $legalNoticeUrl = $this->appConfig->getValueString('theming', 'imprintUrl');
73
+        $privacyUrl = $this->appConfig->getValueString('theming', 'privacyUrl');
74 74
 
75
-		$response = new TemplateResponse('settings', 'help', [
76
-			'admin' => $this->groupManager->isAdmin($this->userId),
77
-			'url' => $documentationUrl,
78
-			'urlUserDocs' => $urlUserDocs,
79
-			'urlAdminDocs' => $urlAdminDocs,
80
-			'mode' => $mode,
81
-			'pageTitle' => $pageTitle,
82
-			'knowledgebaseEmbedded' => $knowledgebaseEmbedded,
83
-			'legalNoticeUrl' => $legalNoticeUrl,
84
-			'privacyUrl' => $privacyUrl,
85
-		]);
86
-		$policy = new ContentSecurityPolicy();
87
-		$policy->addAllowedFrameDomain('\'self\'');
88
-		$response->setContentSecurityPolicy($policy);
89
-		return $response;
90
-	}
75
+        $response = new TemplateResponse('settings', 'help', [
76
+            'admin' => $this->groupManager->isAdmin($this->userId),
77
+            'url' => $documentationUrl,
78
+            'urlUserDocs' => $urlUserDocs,
79
+            'urlAdminDocs' => $urlAdminDocs,
80
+            'mode' => $mode,
81
+            'pageTitle' => $pageTitle,
82
+            'knowledgebaseEmbedded' => $knowledgebaseEmbedded,
83
+            'legalNoticeUrl' => $legalNoticeUrl,
84
+            'privacyUrl' => $privacyUrl,
85
+        ]);
86
+        $policy = new ContentSecurityPolicy();
87
+        $policy->addAllowedFrameDomain('\'self\'');
88
+        $response->setContentSecurityPolicy($policy);
89
+        return $response;
90
+    }
91 91
 }
Please login to merge, or discard this patch.