Completed
Push — master ( 88ba65...9a0892 )
by Christoph
24:20
created
lib/private/App/CompareVersion.php 1 patch
Indentation   +57 added lines, -57 removed lines patch added patch discarded remove patch
@@ -29,69 +29,69 @@
 block discarded – undo
29 29
 use function explode;
30 30
 
31 31
 class CompareVersion {
32
-	private const REGEX_MAJOR = '/^\d+$/';
33
-	private const REGEX_MAJOR_MINOR = '/^\d+\.\d+$/';
34
-	private const REGEX_MAJOR_MINOR_PATCH = '/^\d+\.\d+\.\d+(?!\.\d+)/';
35
-	private const REGEX_ACTUAL = '/^\d+(\.\d+){1,2}/';
32
+    private const REGEX_MAJOR = '/^\d+$/';
33
+    private const REGEX_MAJOR_MINOR = '/^\d+\.\d+$/';
34
+    private const REGEX_MAJOR_MINOR_PATCH = '/^\d+\.\d+\.\d+(?!\.\d+)/';
35
+    private const REGEX_ACTUAL = '/^\d+(\.\d+){1,2}/';
36 36
 
37
-	/**
38
-	 * Checks if the given server version fulfills the given (app) version requirements.
39
-	 *
40
-	 * Version requirements can be 'major.minor.patch', 'major.minor' or just 'major',
41
-	 * so '13.0.1', '13.0' and '13' are valid.
42
-	 *
43
-	 * @param string $actual version as major.minor.patch notation
44
-	 * @param string $required version where major is required and minor and patch are optional
45
-	 * @param string $comparator passed to `version_compare`
46
-	 * @return bool whether the requirement is fulfilled
47
-	 * @throws InvalidArgumentException if versions specified in an invalid format
48
-	 */
49
-	public function isCompatible(string $actual, string $required,
50
-		string $comparator = '>='): bool {
51
-		if (!preg_match(self::REGEX_ACTUAL, $actual, $matches)) {
52
-			throw new InvalidArgumentException("version specification $actual is invalid");
53
-		}
54
-		$cleanActual = $matches[0];
37
+    /**
38
+     * Checks if the given server version fulfills the given (app) version requirements.
39
+     *
40
+     * Version requirements can be 'major.minor.patch', 'major.minor' or just 'major',
41
+     * so '13.0.1', '13.0' and '13' are valid.
42
+     *
43
+     * @param string $actual version as major.minor.patch notation
44
+     * @param string $required version where major is required and minor and patch are optional
45
+     * @param string $comparator passed to `version_compare`
46
+     * @return bool whether the requirement is fulfilled
47
+     * @throws InvalidArgumentException if versions specified in an invalid format
48
+     */
49
+    public function isCompatible(string $actual, string $required,
50
+        string $comparator = '>='): bool {
51
+        if (!preg_match(self::REGEX_ACTUAL, $actual, $matches)) {
52
+            throw new InvalidArgumentException("version specification $actual is invalid");
53
+        }
54
+        $cleanActual = $matches[0];
55 55
 
56
-		if (preg_match(self::REGEX_MAJOR, $required) === 1) {
57
-			return $this->compareMajor($cleanActual, $required, $comparator);
58
-		} elseif (preg_match(self::REGEX_MAJOR_MINOR, $required) === 1) {
59
-			return $this->compareMajorMinor($cleanActual, $required, $comparator);
60
-		} elseif (preg_match(self::REGEX_MAJOR_MINOR_PATCH, $required) === 1) {
61
-			return $this->compareMajorMinorPatch($cleanActual, $required, $comparator);
62
-		} else {
63
-			throw new InvalidArgumentException("required version $required is invalid");
64
-		}
65
-	}
56
+        if (preg_match(self::REGEX_MAJOR, $required) === 1) {
57
+            return $this->compareMajor($cleanActual, $required, $comparator);
58
+        } elseif (preg_match(self::REGEX_MAJOR_MINOR, $required) === 1) {
59
+            return $this->compareMajorMinor($cleanActual, $required, $comparator);
60
+        } elseif (preg_match(self::REGEX_MAJOR_MINOR_PATCH, $required) === 1) {
61
+            return $this->compareMajorMinorPatch($cleanActual, $required, $comparator);
62
+        } else {
63
+            throw new InvalidArgumentException("required version $required is invalid");
64
+        }
65
+    }
66 66
 
67
-	private function compareMajor(string $actual, string $required,
68
-		string $comparator) {
69
-		$actualMajor = explode('.', $actual)[0];
70
-		$requiredMajor = explode('.', $required)[0];
67
+    private function compareMajor(string $actual, string $required,
68
+        string $comparator) {
69
+        $actualMajor = explode('.', $actual)[0];
70
+        $requiredMajor = explode('.', $required)[0];
71 71
 
72
-		return version_compare($actualMajor, $requiredMajor, $comparator);
73
-	}
72
+        return version_compare($actualMajor, $requiredMajor, $comparator);
73
+    }
74 74
 
75
-	private function compareMajorMinor(string $actual, string $required,
76
-		string $comparator) {
77
-		$actualMajor = explode('.', $actual)[0];
78
-		$actualMinor = explode('.', $actual)[1];
79
-		$requiredMajor = explode('.', $required)[0];
80
-		$requiredMinor = explode('.', $required)[1];
75
+    private function compareMajorMinor(string $actual, string $required,
76
+        string $comparator) {
77
+        $actualMajor = explode('.', $actual)[0];
78
+        $actualMinor = explode('.', $actual)[1];
79
+        $requiredMajor = explode('.', $required)[0];
80
+        $requiredMinor = explode('.', $required)[1];
81 81
 
82
-		return version_compare("$actualMajor.$actualMinor",
83
-			"$requiredMajor.$requiredMinor", $comparator);
84
-	}
82
+        return version_compare("$actualMajor.$actualMinor",
83
+            "$requiredMajor.$requiredMinor", $comparator);
84
+    }
85 85
 
86
-	private function compareMajorMinorPatch($actual, $required, $comparator) {
87
-		$actualMajor = explode('.', $actual)[0];
88
-		$actualMinor = explode('.', $actual)[1];
89
-		$actualPatch = explode('.', $actual)[2];
90
-		$requiredMajor = explode('.', $required)[0];
91
-		$requiredMinor = explode('.', $required)[1];
92
-		$requiredPatch = explode('.', $required)[2];
86
+    private function compareMajorMinorPatch($actual, $required, $comparator) {
87
+        $actualMajor = explode('.', $actual)[0];
88
+        $actualMinor = explode('.', $actual)[1];
89
+        $actualPatch = explode('.', $actual)[2];
90
+        $requiredMajor = explode('.', $required)[0];
91
+        $requiredMinor = explode('.', $required)[1];
92
+        $requiredPatch = explode('.', $required)[2];
93 93
 
94
-		return version_compare("$actualMajor.$actualMinor.$actualPatch",
95
-			"$requiredMajor.$requiredMinor.$requiredPatch", $comparator);
96
-	}
94
+        return version_compare("$actualMajor.$actualMinor.$actualPatch",
95
+            "$requiredMajor.$requiredMinor.$requiredPatch", $comparator);
96
+    }
97 97
 }
Please login to merge, or discard this patch.
lib/private/App/AppStore/Bundles/HubBundle.php 2 patches
Indentation   +17 added lines, -17 removed lines patch added patch discarded remove patch
@@ -27,24 +27,24 @@
 block discarded – undo
27 27
 namespace OC\App\AppStore\Bundles;
28 28
 
29 29
 class HubBundle extends Bundle {
30
-	public function getName() {
31
-		return $this->l10n->t('Hub bundle');
32
-	}
30
+    public function getName() {
31
+        return $this->l10n->t('Hub bundle');
32
+    }
33 33
 
34
-	public function getAppIdentifiers() {
35
-		$hubApps = [
36
-			'spreed',
37
-			'contacts',
38
-			'calendar',
39
-			'mail',
40
-		];
34
+    public function getAppIdentifiers() {
35
+        $hubApps = [
36
+            'spreed',
37
+            'contacts',
38
+            'calendar',
39
+            'mail',
40
+        ];
41 41
 
42
-		$architecture = function_exists('php_uname') ? php_uname('m') : null;
43
-		if (isset($architecture) && PHP_OS_FAMILY === 'Linux' && in_array($architecture, ['x86_64', 'aarch64'])) {
44
-			$hubApps[] = 'richdocuments';
45
-			$hubApps[] = 'richdocumentscode' . ($architecture === 'aarch64' ? '_arm64' : '');
46
-		}
42
+        $architecture = function_exists('php_uname') ? php_uname('m') : null;
43
+        if (isset($architecture) && PHP_OS_FAMILY === 'Linux' && in_array($architecture, ['x86_64', 'aarch64'])) {
44
+            $hubApps[] = 'richdocuments';
45
+            $hubApps[] = 'richdocumentscode' . ($architecture === 'aarch64' ? '_arm64' : '');
46
+        }
47 47
 
48
-		return $hubApps;
49
-	}
48
+        return $hubApps;
49
+    }
50 50
 }
Please login to merge, or discard this patch.
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -42,7 +42,7 @@
 block discarded – undo
42 42
 		$architecture = function_exists('php_uname') ? php_uname('m') : null;
43 43
 		if (isset($architecture) && PHP_OS_FAMILY === 'Linux' && in_array($architecture, ['x86_64', 'aarch64'])) {
44 44
 			$hubApps[] = 'richdocuments';
45
-			$hubApps[] = 'richdocumentscode' . ($architecture === 'aarch64' ? '_arm64' : '');
45
+			$hubApps[] = 'richdocumentscode'.($architecture === 'aarch64' ? '_arm64' : '');
46 46
 		}
47 47
 
48 48
 		return $hubApps;
Please login to merge, or discard this patch.
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   +845 added lines, -845 removed lines patch added patch discarded remove patch
@@ -52,849 +52,849 @@
 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
-			// migrate scope values to the new format
136
-			// invalid scopes are mapped to a default value
137
-			$property->setScope(AccountProperty::mapScopeToV2($property->getScope()));
138
-		}
139
-	}
140
-
141
-	protected function updateUser(IUser $user, array $data, ?array $oldUserData, bool $throwOnData = false): array {
142
-		if ($oldUserData === null) {
143
-			$oldUserData = $this->getUser($user, false);
144
-		}
145
-
146
-		$updated = true;
147
-
148
-		if ($oldUserData !== $data) {
149
-			$this->updateExistingUser($user, $data, $oldUserData);
150
-		} else {
151
-			// nothing needs to be done if new and old data set are the same
152
-			$updated = false;
153
-		}
154
-
155
-		if ($updated) {
156
-			$this->dispatcher->dispatchTyped(new UserUpdatedEvent(
157
-				$user,
158
-				$data,
159
-			));
160
-		}
161
-
162
-		return $data;
163
-	}
164
-
165
-	/**
166
-	 * delete user from accounts table
167
-	 */
168
-	public function deleteUser(IUser $user): void {
169
-		$uid = $user->getUID();
170
-		$query = $this->connection->getQueryBuilder();
171
-		$query->delete($this->table)
172
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
173
-			->executeStatement();
174
-
175
-		$this->deleteUserData($user);
176
-	}
177
-
178
-	/**
179
-	 * delete user from accounts table
180
-	 */
181
-	public function deleteUserData(IUser $user): void {
182
-		$uid = $user->getUID();
183
-		$query = $this->connection->getQueryBuilder();
184
-		$query->delete($this->dataTable)
185
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
186
-			->executeStatement();
187
-	}
188
-
189
-	/**
190
-	 * get stored data from a given user
191
-	 */
192
-	protected function getUser(IUser $user, bool $insertIfNotExists = true): array {
193
-		$uid = $user->getUID();
194
-		$query = $this->connection->getQueryBuilder();
195
-		$query->select('data')
196
-			->from($this->table)
197
-			->where($query->expr()->eq('uid', $query->createParameter('uid')))
198
-			->setParameter('uid', $uid);
199
-		$result = $query->executeQuery();
200
-		$accountData = $result->fetchAll();
201
-		$result->closeCursor();
202
-
203
-		if (empty($accountData)) {
204
-			$userData = $this->buildDefaultUserRecord($user);
205
-			if ($insertIfNotExists) {
206
-				$this->insertNewUser($user, $userData);
207
-			}
208
-			return $userData;
209
-		}
210
-
211
-		$userDataArray = $this->importFromJson($accountData[0]['data'], $uid);
212
-		if ($userDataArray === null || $userDataArray === []) {
213
-			return $this->buildDefaultUserRecord($user);
214
-		}
215
-
216
-		return $this->addMissingDefaultValues($userDataArray, $this->buildDefaultUserRecord($user));
217
-	}
218
-
219
-	public function searchUsers(string $property, array $values): array {
220
-		// the value col is limited to 255 bytes. It is used for searches only.
221
-		$values = array_map(function (string $value) {
222
-			return Util::shortenMultibyteString($value, 255);
223
-		}, $values);
224
-		$chunks = array_chunk($values, 500);
225
-		$query = $this->connection->getQueryBuilder();
226
-		$query->select('*')
227
-			->from($this->dataTable)
228
-			->where($query->expr()->eq('name', $query->createNamedParameter($property)))
229
-			->andWhere($query->expr()->in('value', $query->createParameter('values')));
230
-
231
-		$matches = [];
232
-		foreach ($chunks as $chunk) {
233
-			$query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
234
-			$result = $query->executeQuery();
235
-
236
-			while ($row = $result->fetch()) {
237
-				$matches[$row['uid']] = $row['value'];
238
-			}
239
-			$result->closeCursor();
240
-		}
241
-
242
-		$result = array_merge($matches, $this->searchUsersForRelatedCollection($property, $values));
243
-
244
-		return array_flip($result);
245
-	}
246
-
247
-	protected function searchUsersForRelatedCollection(string $property, array $values): array {
248
-		return match ($property) {
249
-			IAccountManager::PROPERTY_EMAIL => array_flip($this->searchUsers(IAccountManager::COLLECTION_EMAIL, $values)),
250
-			default => [],
251
-		};
252
-	}
253
-
254
-	/**
255
-	 * check if we need to ask the server for email verification, if yes we create a cronjob
256
-	 */
257
-	protected function checkEmailVerification(IAccount $updatedAccount, array $oldData): void {
258
-		try {
259
-			$property = $updatedAccount->getProperty(self::PROPERTY_EMAIL);
260
-		} catch (PropertyDoesNotExistException $e) {
261
-			return;
262
-		}
263
-
264
-		$oldMailIndex = array_search(self::PROPERTY_EMAIL, array_column($oldData, 'name'), true);
265
-		$oldMail = $oldMailIndex !== false ? $oldData[$oldMailIndex]['value'] : '';
266
-
267
-		if ($oldMail !== $property->getValue()) {
268
-			$this->jobList->add(
269
-				VerifyUserData::class,
270
-				[
271
-					'verificationCode' => '',
272
-					'data' => $property->getValue(),
273
-					'type' => self::PROPERTY_EMAIL,
274
-					'uid' => $updatedAccount->getUser()->getUID(),
275
-					'try' => 0,
276
-					'lastRun' => time()
277
-				]
278
-			);
279
-
280
-			$property->setVerified(self::VERIFICATION_IN_PROGRESS);
281
-		}
282
-	}
283
-
284
-	protected function checkLocalEmailVerification(IAccount $updatedAccount, array $oldData): void {
285
-		$mailCollection = $updatedAccount->getPropertyCollection(self::COLLECTION_EMAIL);
286
-		foreach ($mailCollection->getProperties() as $property) {
287
-			if ($property->getLocallyVerified() !== self::NOT_VERIFIED) {
288
-				continue;
289
-			}
290
-			if ($this->sendEmailVerificationEmail($updatedAccount->getUser(), $property->getValue())) {
291
-				$property->setLocallyVerified(self::VERIFICATION_IN_PROGRESS);
292
-			}
293
-		}
294
-	}
295
-
296
-	protected function sendEmailVerificationEmail(IUser $user, string $email): bool {
297
-		$ref = \substr(hash('sha256', $email), 0, 8);
298
-		$key = $this->crypto->encrypt($email);
299
-		$token = $this->verificationToken->create($user, 'verifyMail' . $ref, $email);
300
-
301
-		$link = $this->urlGenerator->linkToRouteAbsolute(
302
-			'provisioning_api.Verification.verifyMail',
303
-			[
304
-				'userId' => $user->getUID(),
305
-				'token' => $token,
306
-				'key' => $key
307
-			]
308
-		);
309
-
310
-		$emailTemplate = $this->mailer->createEMailTemplate('core.EmailVerification', [
311
-			'link' => $link,
312
-		]);
313
-
314
-		if (!$this->l10n) {
315
-			$this->l10n = $this->l10nFactory->get('core');
316
-		}
317
-
318
-		$emailTemplate->setSubject($this->l10n->t('%s email verification', [$this->defaults->getName()]));
319
-		$emailTemplate->addHeader();
320
-		$emailTemplate->addHeading($this->l10n->t('Email verification'));
321
-
322
-		$emailTemplate->addBodyText(
323
-			htmlspecialchars($this->l10n->t('Click the following button to confirm your email.')),
324
-			$this->l10n->t('Click the following link to confirm your email.')
325
-		);
326
-
327
-		$emailTemplate->addBodyButton(
328
-			htmlspecialchars($this->l10n->t('Confirm your email')),
329
-			$link,
330
-			false
331
-		);
332
-		$emailTemplate->addFooter();
333
-
334
-		try {
335
-			$message = $this->mailer->createMessage();
336
-			$message->setTo([$email => $user->getDisplayName()]);
337
-			$message->setFrom([Util::getDefaultEmailAddress('verification-noreply') => $this->defaults->getName()]);
338
-			$message->useTemplate($emailTemplate);
339
-			$this->mailer->send($message);
340
-		} catch (Exception $e) {
341
-			// Log the exception and continue
342
-			$this->logger->info('Failed to send verification mail', [
343
-				'app' => 'core',
344
-				'exception' => $e
345
-			]);
346
-			return false;
347
-		}
348
-		return true;
349
-	}
350
-
351
-	/**
352
-	 * Make sure that all expected data are set
353
-	 */
354
-	protected function addMissingDefaultValues(array $userData, array $defaultUserData): array {
355
-		foreach ($defaultUserData as $defaultDataItem) {
356
-			// If property does not exist, initialize it
357
-			$userDataIndex = array_search($defaultDataItem['name'], array_column($userData, 'name'));
358
-			if ($userDataIndex === false) {
359
-				$userData[] = $defaultDataItem;
360
-				continue;
361
-			}
362
-
363
-			// Merge and extend default missing values
364
-			$userData[$userDataIndex] = array_merge($defaultDataItem, $userData[$userDataIndex]);
365
-		}
366
-
367
-		return $userData;
368
-	}
369
-
370
-	protected function updateVerificationStatus(IAccount $updatedAccount, array $oldData): void {
371
-		static $propertiesVerifiableByLookupServer = [
372
-			self::PROPERTY_TWITTER,
373
-			self::PROPERTY_FEDIVERSE,
374
-			self::PROPERTY_WEBSITE,
375
-			self::PROPERTY_EMAIL,
376
-		];
377
-
378
-		foreach ($propertiesVerifiableByLookupServer as $propertyName) {
379
-			try {
380
-				$property = $updatedAccount->getProperty($propertyName);
381
-			} catch (PropertyDoesNotExistException $e) {
382
-				continue;
383
-			}
384
-			$wasVerified = isset($oldData[$propertyName])
385
-				&& isset($oldData[$propertyName]['verified'])
386
-				&& $oldData[$propertyName]['verified'] === self::VERIFIED;
387
-			if ((!isset($oldData[$propertyName])
388
-					|| !isset($oldData[$propertyName]['value'])
389
-					|| $property->getValue() !== $oldData[$propertyName]['value'])
390
-				&& ($property->getVerified() !== self::NOT_VERIFIED
391
-					|| $wasVerified)
392
-			) {
393
-				$property->setVerified(self::NOT_VERIFIED);
394
-			}
395
-		}
396
-	}
397
-
398
-	/**
399
-	 * add new user to accounts table
400
-	 */
401
-	protected function insertNewUser(IUser $user, array $data): void {
402
-		$uid = $user->getUID();
403
-		$jsonEncodedData = $this->prepareJson($data);
404
-		$query = $this->connection->getQueryBuilder();
405
-		$query->insert($this->table)
406
-			->values(
407
-				[
408
-					'uid' => $query->createNamedParameter($uid),
409
-					'data' => $query->createNamedParameter($jsonEncodedData),
410
-				]
411
-			)
412
-			->executeStatement();
413
-
414
-		$this->deleteUserData($user);
415
-		$this->writeUserData($user, $data);
416
-	}
417
-
418
-	protected function prepareJson(array $data): string {
419
-		$preparedData = [];
420
-		foreach ($data as $dataRow) {
421
-			$propertyName = $dataRow['name'];
422
-			unset($dataRow['name']);
423
-
424
-			if (isset($dataRow['locallyVerified']) && $dataRow['locallyVerified'] === self::NOT_VERIFIED) {
425
-				// do not write default value, save DB space
426
-				unset($dataRow['locallyVerified']);
427
-			}
428
-
429
-			if (!$this->isCollection($propertyName)) {
430
-				$preparedData[$propertyName] = $dataRow;
431
-				continue;
432
-			}
433
-			if (!isset($preparedData[$propertyName])) {
434
-				$preparedData[$propertyName] = [];
435
-			}
436
-			$preparedData[$propertyName][] = $dataRow;
437
-		}
438
-		return json_encode($preparedData);
439
-	}
440
-
441
-	protected function importFromJson(string $json, string $userId): ?array {
442
-		$result = [];
443
-		$jsonArray = json_decode($json, true);
444
-		$jsonError = json_last_error();
445
-		if ($jsonError !== JSON_ERROR_NONE) {
446
-			$this->logger->critical(
447
-				'User data of {uid} contained invalid JSON (error {json_error}), hence falling back to a default user record',
448
-				[
449
-					'uid' => $userId,
450
-					'json_error' => $jsonError
451
-				]
452
-			);
453
-			return null;
454
-		}
455
-		foreach ($jsonArray as $propertyName => $row) {
456
-			if (!$this->isCollection($propertyName)) {
457
-				$result[] = array_merge($row, ['name' => $propertyName]);
458
-				continue;
459
-			}
460
-			foreach ($row as $singleRow) {
461
-				$result[] = array_merge($singleRow, ['name' => $propertyName]);
462
-			}
463
-		}
464
-		return $result;
465
-	}
466
-
467
-	/**
468
-	 * Update existing user in accounts table
469
-	 */
470
-	protected function updateExistingUser(IUser $user, array $data, array $oldData): void {
471
-		$uid = $user->getUID();
472
-		$jsonEncodedData = $this->prepareJson($data);
473
-		$query = $this->connection->getQueryBuilder();
474
-		$query->update($this->table)
475
-			->set('data', $query->createNamedParameter($jsonEncodedData))
476
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
477
-			->executeStatement();
478
-
479
-		$this->deleteUserData($user);
480
-		$this->writeUserData($user, $data);
481
-	}
482
-
483
-	protected function writeUserData(IUser $user, array $data): void {
484
-		$query = $this->connection->getQueryBuilder();
485
-		$query->insert($this->dataTable)
486
-			->values(
487
-				[
488
-					'uid' => $query->createNamedParameter($user->getUID()),
489
-					'name' => $query->createParameter('name'),
490
-					'value' => $query->createParameter('value'),
491
-				]
492
-			);
493
-		$this->writeUserDataProperties($query, $data);
494
-	}
495
-
496
-	protected function writeUserDataProperties(IQueryBuilder $query, array $data): void {
497
-		foreach ($data as $property) {
498
-			if ($property['name'] === self::PROPERTY_AVATAR) {
499
-				continue;
500
-			}
501
-
502
-			// the value col is limited to 255 bytes. It is used for searches only.
503
-			$value = $property['value'] ? Util::shortenMultibyteString($property['value'], 255) : '';
504
-
505
-			$query->setParameter('name', $property['name'])
506
-				->setParameter('value', $value);
507
-			$query->executeStatement();
508
-		}
509
-	}
510
-
511
-	/**
512
-	 * build default user record in case not data set exists yet
513
-	 */
514
-	protected function buildDefaultUserRecord(IUser $user): array {
515
-		$scopes = array_merge(self::DEFAULT_SCOPES, array_filter($this->config->getSystemValue('account_manager.default_property_scope', []), static function (string $scope, string $property) {
516
-			return in_array($property, self::ALLOWED_PROPERTIES, true) && in_array($scope, self::ALLOWED_SCOPES, true);
517
-		}, ARRAY_FILTER_USE_BOTH));
518
-
519
-		return [
520
-			[
521
-				'name' => self::PROPERTY_DISPLAYNAME,
522
-				'value' => $user->getDisplayName(),
523
-				// Display name must be at least SCOPE_LOCAL
524
-				'scope' => $scopes[self::PROPERTY_DISPLAYNAME] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_DISPLAYNAME],
525
-				'verified' => self::NOT_VERIFIED,
526
-			],
527
-
528
-			[
529
-				'name' => self::PROPERTY_ADDRESS,
530
-				'value' => '',
531
-				'scope' => $scopes[self::PROPERTY_ADDRESS],
532
-				'verified' => self::NOT_VERIFIED,
533
-			],
534
-
535
-			[
536
-				'name' => self::PROPERTY_WEBSITE,
537
-				'value' => '',
538
-				'scope' => $scopes[self::PROPERTY_WEBSITE],
539
-				'verified' => self::NOT_VERIFIED,
540
-			],
541
-
542
-			[
543
-				'name' => self::PROPERTY_EMAIL,
544
-				'value' => $user->getEMailAddress(),
545
-				// Email must be at least SCOPE_LOCAL
546
-				'scope' => $scopes[self::PROPERTY_EMAIL] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_EMAIL],
547
-				'verified' => self::NOT_VERIFIED,
548
-			],
549
-
550
-			[
551
-				'name' => self::PROPERTY_AVATAR,
552
-				'scope' => $scopes[self::PROPERTY_AVATAR],
553
-			],
554
-
555
-			[
556
-				'name' => self::PROPERTY_PHONE,
557
-				'value' => '',
558
-				'scope' => $scopes[self::PROPERTY_PHONE],
559
-				'verified' => self::NOT_VERIFIED,
560
-			],
561
-
562
-			[
563
-				'name' => self::PROPERTY_TWITTER,
564
-				'value' => '',
565
-				'scope' => $scopes[self::PROPERTY_TWITTER],
566
-				'verified' => self::NOT_VERIFIED,
567
-			],
568
-
569
-			[
570
-				'name' => self::PROPERTY_BLUESKY,
571
-				'value' => '',
572
-				'scope' => $scopes[self::PROPERTY_BLUESKY],
573
-				'verified' => self::NOT_VERIFIED,
574
-			],
575
-
576
-			[
577
-				'name' => self::PROPERTY_FEDIVERSE,
578
-				'value' => '',
579
-				'scope' => $scopes[self::PROPERTY_FEDIVERSE],
580
-				'verified' => self::NOT_VERIFIED,
581
-			],
582
-
583
-			[
584
-				'name' => self::PROPERTY_ORGANISATION,
585
-				'value' => '',
586
-				'scope' => $scopes[self::PROPERTY_ORGANISATION],
587
-			],
588
-
589
-			[
590
-				'name' => self::PROPERTY_ROLE,
591
-				'value' => '',
592
-				'scope' => $scopes[self::PROPERTY_ROLE],
593
-			],
594
-
595
-			[
596
-				'name' => self::PROPERTY_HEADLINE,
597
-				'value' => '',
598
-				'scope' => $scopes[self::PROPERTY_HEADLINE],
599
-			],
600
-
601
-			[
602
-				'name' => self::PROPERTY_BIOGRAPHY,
603
-				'value' => '',
604
-				'scope' => $scopes[self::PROPERTY_BIOGRAPHY],
605
-			],
606
-
607
-			[
608
-				'name' => self::PROPERTY_BIRTHDATE,
609
-				'value' => '',
610
-				'scope' => $scopes[self::PROPERTY_BIRTHDATE],
611
-			],
612
-
613
-			[
614
-				'name' => self::PROPERTY_PROFILE_ENABLED,
615
-				'value' => $this->isProfileEnabledByDefault($this->config) ? '1' : '0',
616
-			],
617
-
618
-			[
619
-				'name' => self::PROPERTY_PRONOUNS,
620
-				'value' => '',
621
-				'scope' => $scopes[self::PROPERTY_PRONOUNS],
622
-			],
623
-		];
624
-	}
625
-
626
-	private function arrayDataToCollection(IAccount $account, array $data): IAccountPropertyCollection {
627
-		$collection = $account->getPropertyCollection($data['name']);
628
-
629
-		$p = new AccountProperty(
630
-			$data['name'],
631
-			$data['value'] ?? '',
632
-			$data['scope'] ?? self::SCOPE_LOCAL,
633
-			$data['verified'] ?? self::NOT_VERIFIED,
634
-			''
635
-		);
636
-		$p->setLocallyVerified($data['locallyVerified'] ?? self::NOT_VERIFIED);
637
-		$collection->addProperty($p);
638
-
639
-		return $collection;
640
-	}
641
-
642
-	private function parseAccountData(IUser $user, $data): Account {
643
-		$account = new Account($user);
644
-		foreach ($data as $accountData) {
645
-			if ($this->isCollection($accountData['name'])) {
646
-				$account->setPropertyCollection($this->arrayDataToCollection($account, $accountData));
647
-			} else {
648
-				$account->setProperty($accountData['name'], $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
649
-				if (isset($accountData['locallyVerified'])) {
650
-					$property = $account->getProperty($accountData['name']);
651
-					$property->setLocallyVerified($accountData['locallyVerified']);
652
-				}
653
-			}
654
-		}
655
-		return $account;
656
-	}
657
-
658
-	public function getAccount(IUser $user): IAccount {
659
-		$cached = $this->internalCache->get($user->getUID());
660
-		if ($cached !== null) {
661
-			return $cached;
662
-		}
663
-		$account = $this->parseAccountData($user, $this->getUser($user));
664
-		if ($user->getBackend() instanceof IGetDisplayNameBackend) {
665
-			$property = $account->getProperty(self::PROPERTY_DISPLAYNAME);
666
-			$account->setProperty(self::PROPERTY_DISPLAYNAME, $user->getDisplayName(), $property->getScope(), $property->getVerified());
667
-		}
668
-		$this->internalCache->set($user->getUID(), $account);
669
-		return $account;
670
-	}
671
-
672
-	/**
673
-	 * Converts value (phone number) in E.164 format when it was a valid number
674
-	 * @throws InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code
675
-	 */
676
-	protected function sanitizePropertyPhoneNumber(IAccountProperty $property): void {
677
-		$defaultRegion = $this->config->getSystemValueString('default_phone_region', '');
678
-
679
-		if ($defaultRegion === '') {
680
-			// When no default region is set, only +49… numbers are valid
681
-			if (!str_starts_with($property->getValue(), '+')) {
682
-				throw new InvalidArgumentException(self::PROPERTY_PHONE);
683
-			}
684
-
685
-			$defaultRegion = 'EN';
686
-		}
687
-
688
-		$phoneNumber = $this->phoneNumberUtil->convertToStandardFormat($property->getValue(), $defaultRegion);
689
-		if ($phoneNumber === null) {
690
-			throw new InvalidArgumentException(self::PROPERTY_PHONE);
691
-		}
692
-		$property->setValue($phoneNumber);
693
-	}
694
-
695
-	/**
696
-	 * @throws InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty
697
-	 */
698
-	private function sanitizePropertyWebsite(IAccountProperty $property): void {
699
-		$parts = parse_url($property->getValue());
700
-		if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) {
701
-			throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
702
-		}
703
-
704
-		if (!isset($parts['host']) || $parts['host'] === '') {
705
-			throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
706
-		}
707
-	}
708
-
709
-	/**
710
-	 * @throws InvalidArgumentException If the property value is not a valid user handle according to X's rules
711
-	 */
712
-	private function sanitizePropertyTwitter(IAccountProperty $property): void {
713
-		if ($property->getName() === self::PROPERTY_TWITTER) {
714
-			$matches = [];
715
-			// twitter handles only contain alpha numeric characters and the underscore and must not be longer than 15 characters
716
-			if (preg_match('/^@?([a-zA-Z0-9_]{2,15})$/', $property->getValue(), $matches) !== 1) {
717
-				throw new InvalidArgumentException(self::PROPERTY_TWITTER);
718
-			}
719
-
720
-			// drop the leading @ if any to make it the valid handle
721
-			$property->setValue($matches[1]);
722
-
723
-		}
724
-	}
725
-
726
-	private function validateBlueSkyHandle(string $text): bool {
727
-		if ($text === '') {
728
-			return true;
729
-		}
730
-
731
-		$lowerText = strtolower($text);
732
-
733
-		if ($lowerText === 'bsky.social') {
734
-			// "bsky.social" itself is not a valid handle
735
-			return false;
736
-		}
737
-
738
-		if (str_ends_with($lowerText, '.bsky.social')) {
739
-			$parts = explode('.', $lowerText);
740
-
741
-			// Must be exactly: username.bsky.social → 3 parts
742
-			if (count($parts) !== 3 || $parts[1] !== 'bsky' || $parts[2] !== 'social') {
743
-				return false;
744
-			}
745
-
746
-			$username = $parts[0];
747
-
748
-			// Must be 3–18 chars, alphanumeric/hyphen, no start/end hyphen
749
-			return preg_match('/^[a-z0-9][a-z0-9-]{2,17}$/', $username) === 1;
750
-		}
751
-
752
-		// Allow custom domains (Bluesky handle via personal domain)
753
-		return filter_var($text, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false;
754
-	}
755
-
756
-
757
-	private function sanitizePropertyBluesky(IAccountProperty $property): void {
758
-		if ($property->getName() === self::PROPERTY_BLUESKY) {
759
-			if (!$this->validateBlueSkyHandle($property->getValue())) {
760
-				throw new InvalidArgumentException(self::PROPERTY_BLUESKY);
761
-			}
762
-
763
-			$property->setValue($property->getValue());
764
-		}
765
-	}
766
-
767
-	/**
768
-	 * @throws InvalidArgumentException If the property value is not a valid fediverse handle (username@instance where instance is a valid domain)
769
-	 */
770
-	private function sanitizePropertyFediverse(IAccountProperty $property): void {
771
-		if ($property->getName() === self::PROPERTY_FEDIVERSE) {
772
-			$matches = [];
773
-			if (preg_match('/^@?([^@\s\/\\\]+)@([^\s\/\\\]+)$/', trim($property->getValue()), $matches) !== 1) {
774
-				throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
775
-			}
776
-
777
-			[, $username, $instance] = $matches;
778
-			$validated = filter_var($instance, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
779
-			if ($validated !== $instance) {
780
-				throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
781
-			}
782
-
783
-			if ($this->config->getSystemValueBool('has_internet_connection', true)) {
784
-				$client = $this->clientService->newClient();
785
-
786
-				try {
787
-					// try the public account lookup API of mastodon
788
-					$response = $client->get("https://{$instance}/.well-known/webfinger?resource=acct:{$username}@{$instance}");
789
-					// should be a json response with account information
790
-					$data = $response->getBody();
791
-					if (is_resource($data)) {
792
-						$data = stream_get_contents($data);
793
-					}
794
-					$decoded = json_decode($data, true);
795
-					// ensure the username is the same the user passed
796
-					// in this case we can assume this is a valid fediverse server and account
797
-					if (!is_array($decoded) || ($decoded['subject'] ?? '') !== "acct:{$username}@{$instance}") {
798
-						throw new InvalidArgumentException();
799
-					}
800
-					// check for activitypub link
801
-					if (is_array($decoded['links']) && isset($decoded['links'])) {
802
-						$found = false;
803
-						foreach ($decoded['links'] as $link) {
804
-							// have application/activity+json or application/ld+json
805
-							if (isset($link['type']) && (
806
-								$link['type'] === 'application/activity+json'
807
-								|| $link['type'] === 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
808
-							)) {
809
-								$found = true;
810
-								break;
811
-							}
812
-						}
813
-						if (!$found) {
814
-							throw new InvalidArgumentException();
815
-						}
816
-					}
817
-				} catch (InvalidArgumentException) {
818
-					throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
819
-				} catch (\Exception $error) {
820
-					$this->logger->error('Could not verify fediverse account', ['exception' => $error, 'instance' => $instance]);
821
-					throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
822
-				}
823
-			}
824
-
825
-			$property->setValue("$username@$instance");
826
-		}
827
-	}
828
-
829
-	public function updateAccount(IAccount $account): void {
830
-		$this->testValueLengths(iterator_to_array($account->getAllProperties()), true);
831
-		try {
832
-			$property = $account->getProperty(self::PROPERTY_PHONE);
833
-			if ($property->getValue() !== '') {
834
-				$this->sanitizePropertyPhoneNumber($property);
835
-			}
836
-		} catch (PropertyDoesNotExistException $e) {
837
-			//  valid case, nothing to do
838
-		}
839
-
840
-		try {
841
-			$property = $account->getProperty(self::PROPERTY_WEBSITE);
842
-			if ($property->getValue() !== '') {
843
-				$this->sanitizePropertyWebsite($property);
844
-			}
845
-		} catch (PropertyDoesNotExistException $e) {
846
-			//  valid case, nothing to do
847
-		}
848
-
849
-		try {
850
-			$property = $account->getProperty(self::PROPERTY_TWITTER);
851
-			if ($property->getValue() !== '') {
852
-				$this->sanitizePropertyTwitter($property);
853
-			}
854
-		} catch (PropertyDoesNotExistException $e) {
855
-			//  valid case, nothing to do
856
-		}
857
-
858
-		try {
859
-			$property = $account->getProperty(self::PROPERTY_BLUESKY);
860
-			if ($property->getValue() !== '') {
861
-				$this->sanitizePropertyBluesky($property);
862
-			}
863
-		} catch (PropertyDoesNotExistException $e) {
864
-			//  valid case, nothing to do
865
-		}
866
-
867
-		try {
868
-			$property = $account->getProperty(self::PROPERTY_FEDIVERSE);
869
-			if ($property->getValue() !== '') {
870
-				$this->sanitizePropertyFediverse($property);
871
-			}
872
-		} catch (PropertyDoesNotExistException $e) {
873
-			//  valid case, nothing to do
874
-		}
875
-
876
-		foreach ($account->getAllProperties() as $property) {
877
-			$this->testPropertyScope($property, self::ALLOWED_SCOPES, true);
878
-		}
879
-
880
-		$oldData = $this->getUser($account->getUser(), false);
881
-		$this->updateVerificationStatus($account, $oldData);
882
-		$this->checkEmailVerification($account, $oldData);
883
-		$this->checkLocalEmailVerification($account, $oldData);
884
-
885
-		$data = [];
886
-		foreach ($account->getAllProperties() as $property) {
887
-			/** @var IAccountProperty $property */
888
-			$data[] = [
889
-				'name' => $property->getName(),
890
-				'value' => $property->getValue(),
891
-				'scope' => $property->getScope(),
892
-				'verified' => $property->getVerified(),
893
-				'locallyVerified' => $property->getLocallyVerified(),
894
-			];
895
-		}
896
-
897
-		$this->updateUser($account->getUser(), $data, $oldData, true);
898
-		$this->internalCache->set($account->getUser()->getUID(), $account);
899
-	}
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
+            // migrate scope values to the new format
136
+            // invalid scopes are mapped to a default value
137
+            $property->setScope(AccountProperty::mapScopeToV2($property->getScope()));
138
+        }
139
+    }
140
+
141
+    protected function updateUser(IUser $user, array $data, ?array $oldUserData, bool $throwOnData = false): array {
142
+        if ($oldUserData === null) {
143
+            $oldUserData = $this->getUser($user, false);
144
+        }
145
+
146
+        $updated = true;
147
+
148
+        if ($oldUserData !== $data) {
149
+            $this->updateExistingUser($user, $data, $oldUserData);
150
+        } else {
151
+            // nothing needs to be done if new and old data set are the same
152
+            $updated = false;
153
+        }
154
+
155
+        if ($updated) {
156
+            $this->dispatcher->dispatchTyped(new UserUpdatedEvent(
157
+                $user,
158
+                $data,
159
+            ));
160
+        }
161
+
162
+        return $data;
163
+    }
164
+
165
+    /**
166
+     * delete user from accounts table
167
+     */
168
+    public function deleteUser(IUser $user): void {
169
+        $uid = $user->getUID();
170
+        $query = $this->connection->getQueryBuilder();
171
+        $query->delete($this->table)
172
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
173
+            ->executeStatement();
174
+
175
+        $this->deleteUserData($user);
176
+    }
177
+
178
+    /**
179
+     * delete user from accounts table
180
+     */
181
+    public function deleteUserData(IUser $user): void {
182
+        $uid = $user->getUID();
183
+        $query = $this->connection->getQueryBuilder();
184
+        $query->delete($this->dataTable)
185
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
186
+            ->executeStatement();
187
+    }
188
+
189
+    /**
190
+     * get stored data from a given user
191
+     */
192
+    protected function getUser(IUser $user, bool $insertIfNotExists = true): array {
193
+        $uid = $user->getUID();
194
+        $query = $this->connection->getQueryBuilder();
195
+        $query->select('data')
196
+            ->from($this->table)
197
+            ->where($query->expr()->eq('uid', $query->createParameter('uid')))
198
+            ->setParameter('uid', $uid);
199
+        $result = $query->executeQuery();
200
+        $accountData = $result->fetchAll();
201
+        $result->closeCursor();
202
+
203
+        if (empty($accountData)) {
204
+            $userData = $this->buildDefaultUserRecord($user);
205
+            if ($insertIfNotExists) {
206
+                $this->insertNewUser($user, $userData);
207
+            }
208
+            return $userData;
209
+        }
210
+
211
+        $userDataArray = $this->importFromJson($accountData[0]['data'], $uid);
212
+        if ($userDataArray === null || $userDataArray === []) {
213
+            return $this->buildDefaultUserRecord($user);
214
+        }
215
+
216
+        return $this->addMissingDefaultValues($userDataArray, $this->buildDefaultUserRecord($user));
217
+    }
218
+
219
+    public function searchUsers(string $property, array $values): array {
220
+        // the value col is limited to 255 bytes. It is used for searches only.
221
+        $values = array_map(function (string $value) {
222
+            return Util::shortenMultibyteString($value, 255);
223
+        }, $values);
224
+        $chunks = array_chunk($values, 500);
225
+        $query = $this->connection->getQueryBuilder();
226
+        $query->select('*')
227
+            ->from($this->dataTable)
228
+            ->where($query->expr()->eq('name', $query->createNamedParameter($property)))
229
+            ->andWhere($query->expr()->in('value', $query->createParameter('values')));
230
+
231
+        $matches = [];
232
+        foreach ($chunks as $chunk) {
233
+            $query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
234
+            $result = $query->executeQuery();
235
+
236
+            while ($row = $result->fetch()) {
237
+                $matches[$row['uid']] = $row['value'];
238
+            }
239
+            $result->closeCursor();
240
+        }
241
+
242
+        $result = array_merge($matches, $this->searchUsersForRelatedCollection($property, $values));
243
+
244
+        return array_flip($result);
245
+    }
246
+
247
+    protected function searchUsersForRelatedCollection(string $property, array $values): array {
248
+        return match ($property) {
249
+            IAccountManager::PROPERTY_EMAIL => array_flip($this->searchUsers(IAccountManager::COLLECTION_EMAIL, $values)),
250
+            default => [],
251
+        };
252
+    }
253
+
254
+    /**
255
+     * check if we need to ask the server for email verification, if yes we create a cronjob
256
+     */
257
+    protected function checkEmailVerification(IAccount $updatedAccount, array $oldData): void {
258
+        try {
259
+            $property = $updatedAccount->getProperty(self::PROPERTY_EMAIL);
260
+        } catch (PropertyDoesNotExistException $e) {
261
+            return;
262
+        }
263
+
264
+        $oldMailIndex = array_search(self::PROPERTY_EMAIL, array_column($oldData, 'name'), true);
265
+        $oldMail = $oldMailIndex !== false ? $oldData[$oldMailIndex]['value'] : '';
266
+
267
+        if ($oldMail !== $property->getValue()) {
268
+            $this->jobList->add(
269
+                VerifyUserData::class,
270
+                [
271
+                    'verificationCode' => '',
272
+                    'data' => $property->getValue(),
273
+                    'type' => self::PROPERTY_EMAIL,
274
+                    'uid' => $updatedAccount->getUser()->getUID(),
275
+                    'try' => 0,
276
+                    'lastRun' => time()
277
+                ]
278
+            );
279
+
280
+            $property->setVerified(self::VERIFICATION_IN_PROGRESS);
281
+        }
282
+    }
283
+
284
+    protected function checkLocalEmailVerification(IAccount $updatedAccount, array $oldData): void {
285
+        $mailCollection = $updatedAccount->getPropertyCollection(self::COLLECTION_EMAIL);
286
+        foreach ($mailCollection->getProperties() as $property) {
287
+            if ($property->getLocallyVerified() !== self::NOT_VERIFIED) {
288
+                continue;
289
+            }
290
+            if ($this->sendEmailVerificationEmail($updatedAccount->getUser(), $property->getValue())) {
291
+                $property->setLocallyVerified(self::VERIFICATION_IN_PROGRESS);
292
+            }
293
+        }
294
+    }
295
+
296
+    protected function sendEmailVerificationEmail(IUser $user, string $email): bool {
297
+        $ref = \substr(hash('sha256', $email), 0, 8);
298
+        $key = $this->crypto->encrypt($email);
299
+        $token = $this->verificationToken->create($user, 'verifyMail' . $ref, $email);
300
+
301
+        $link = $this->urlGenerator->linkToRouteAbsolute(
302
+            'provisioning_api.Verification.verifyMail',
303
+            [
304
+                'userId' => $user->getUID(),
305
+                'token' => $token,
306
+                'key' => $key
307
+            ]
308
+        );
309
+
310
+        $emailTemplate = $this->mailer->createEMailTemplate('core.EmailVerification', [
311
+            'link' => $link,
312
+        ]);
313
+
314
+        if (!$this->l10n) {
315
+            $this->l10n = $this->l10nFactory->get('core');
316
+        }
317
+
318
+        $emailTemplate->setSubject($this->l10n->t('%s email verification', [$this->defaults->getName()]));
319
+        $emailTemplate->addHeader();
320
+        $emailTemplate->addHeading($this->l10n->t('Email verification'));
321
+
322
+        $emailTemplate->addBodyText(
323
+            htmlspecialchars($this->l10n->t('Click the following button to confirm your email.')),
324
+            $this->l10n->t('Click the following link to confirm your email.')
325
+        );
326
+
327
+        $emailTemplate->addBodyButton(
328
+            htmlspecialchars($this->l10n->t('Confirm your email')),
329
+            $link,
330
+            false
331
+        );
332
+        $emailTemplate->addFooter();
333
+
334
+        try {
335
+            $message = $this->mailer->createMessage();
336
+            $message->setTo([$email => $user->getDisplayName()]);
337
+            $message->setFrom([Util::getDefaultEmailAddress('verification-noreply') => $this->defaults->getName()]);
338
+            $message->useTemplate($emailTemplate);
339
+            $this->mailer->send($message);
340
+        } catch (Exception $e) {
341
+            // Log the exception and continue
342
+            $this->logger->info('Failed to send verification mail', [
343
+                'app' => 'core',
344
+                'exception' => $e
345
+            ]);
346
+            return false;
347
+        }
348
+        return true;
349
+    }
350
+
351
+    /**
352
+     * Make sure that all expected data are set
353
+     */
354
+    protected function addMissingDefaultValues(array $userData, array $defaultUserData): array {
355
+        foreach ($defaultUserData as $defaultDataItem) {
356
+            // If property does not exist, initialize it
357
+            $userDataIndex = array_search($defaultDataItem['name'], array_column($userData, 'name'));
358
+            if ($userDataIndex === false) {
359
+                $userData[] = $defaultDataItem;
360
+                continue;
361
+            }
362
+
363
+            // Merge and extend default missing values
364
+            $userData[$userDataIndex] = array_merge($defaultDataItem, $userData[$userDataIndex]);
365
+        }
366
+
367
+        return $userData;
368
+    }
369
+
370
+    protected function updateVerificationStatus(IAccount $updatedAccount, array $oldData): void {
371
+        static $propertiesVerifiableByLookupServer = [
372
+            self::PROPERTY_TWITTER,
373
+            self::PROPERTY_FEDIVERSE,
374
+            self::PROPERTY_WEBSITE,
375
+            self::PROPERTY_EMAIL,
376
+        ];
377
+
378
+        foreach ($propertiesVerifiableByLookupServer as $propertyName) {
379
+            try {
380
+                $property = $updatedAccount->getProperty($propertyName);
381
+            } catch (PropertyDoesNotExistException $e) {
382
+                continue;
383
+            }
384
+            $wasVerified = isset($oldData[$propertyName])
385
+                && isset($oldData[$propertyName]['verified'])
386
+                && $oldData[$propertyName]['verified'] === self::VERIFIED;
387
+            if ((!isset($oldData[$propertyName])
388
+                    || !isset($oldData[$propertyName]['value'])
389
+                    || $property->getValue() !== $oldData[$propertyName]['value'])
390
+                && ($property->getVerified() !== self::NOT_VERIFIED
391
+                    || $wasVerified)
392
+            ) {
393
+                $property->setVerified(self::NOT_VERIFIED);
394
+            }
395
+        }
396
+    }
397
+
398
+    /**
399
+     * add new user to accounts table
400
+     */
401
+    protected function insertNewUser(IUser $user, array $data): void {
402
+        $uid = $user->getUID();
403
+        $jsonEncodedData = $this->prepareJson($data);
404
+        $query = $this->connection->getQueryBuilder();
405
+        $query->insert($this->table)
406
+            ->values(
407
+                [
408
+                    'uid' => $query->createNamedParameter($uid),
409
+                    'data' => $query->createNamedParameter($jsonEncodedData),
410
+                ]
411
+            )
412
+            ->executeStatement();
413
+
414
+        $this->deleteUserData($user);
415
+        $this->writeUserData($user, $data);
416
+    }
417
+
418
+    protected function prepareJson(array $data): string {
419
+        $preparedData = [];
420
+        foreach ($data as $dataRow) {
421
+            $propertyName = $dataRow['name'];
422
+            unset($dataRow['name']);
423
+
424
+            if (isset($dataRow['locallyVerified']) && $dataRow['locallyVerified'] === self::NOT_VERIFIED) {
425
+                // do not write default value, save DB space
426
+                unset($dataRow['locallyVerified']);
427
+            }
428
+
429
+            if (!$this->isCollection($propertyName)) {
430
+                $preparedData[$propertyName] = $dataRow;
431
+                continue;
432
+            }
433
+            if (!isset($preparedData[$propertyName])) {
434
+                $preparedData[$propertyName] = [];
435
+            }
436
+            $preparedData[$propertyName][] = $dataRow;
437
+        }
438
+        return json_encode($preparedData);
439
+    }
440
+
441
+    protected function importFromJson(string $json, string $userId): ?array {
442
+        $result = [];
443
+        $jsonArray = json_decode($json, true);
444
+        $jsonError = json_last_error();
445
+        if ($jsonError !== JSON_ERROR_NONE) {
446
+            $this->logger->critical(
447
+                'User data of {uid} contained invalid JSON (error {json_error}), hence falling back to a default user record',
448
+                [
449
+                    'uid' => $userId,
450
+                    'json_error' => $jsonError
451
+                ]
452
+            );
453
+            return null;
454
+        }
455
+        foreach ($jsonArray as $propertyName => $row) {
456
+            if (!$this->isCollection($propertyName)) {
457
+                $result[] = array_merge($row, ['name' => $propertyName]);
458
+                continue;
459
+            }
460
+            foreach ($row as $singleRow) {
461
+                $result[] = array_merge($singleRow, ['name' => $propertyName]);
462
+            }
463
+        }
464
+        return $result;
465
+    }
466
+
467
+    /**
468
+     * Update existing user in accounts table
469
+     */
470
+    protected function updateExistingUser(IUser $user, array $data, array $oldData): void {
471
+        $uid = $user->getUID();
472
+        $jsonEncodedData = $this->prepareJson($data);
473
+        $query = $this->connection->getQueryBuilder();
474
+        $query->update($this->table)
475
+            ->set('data', $query->createNamedParameter($jsonEncodedData))
476
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
477
+            ->executeStatement();
478
+
479
+        $this->deleteUserData($user);
480
+        $this->writeUserData($user, $data);
481
+    }
482
+
483
+    protected function writeUserData(IUser $user, array $data): void {
484
+        $query = $this->connection->getQueryBuilder();
485
+        $query->insert($this->dataTable)
486
+            ->values(
487
+                [
488
+                    'uid' => $query->createNamedParameter($user->getUID()),
489
+                    'name' => $query->createParameter('name'),
490
+                    'value' => $query->createParameter('value'),
491
+                ]
492
+            );
493
+        $this->writeUserDataProperties($query, $data);
494
+    }
495
+
496
+    protected function writeUserDataProperties(IQueryBuilder $query, array $data): void {
497
+        foreach ($data as $property) {
498
+            if ($property['name'] === self::PROPERTY_AVATAR) {
499
+                continue;
500
+            }
501
+
502
+            // the value col is limited to 255 bytes. It is used for searches only.
503
+            $value = $property['value'] ? Util::shortenMultibyteString($property['value'], 255) : '';
504
+
505
+            $query->setParameter('name', $property['name'])
506
+                ->setParameter('value', $value);
507
+            $query->executeStatement();
508
+        }
509
+    }
510
+
511
+    /**
512
+     * build default user record in case not data set exists yet
513
+     */
514
+    protected function buildDefaultUserRecord(IUser $user): array {
515
+        $scopes = array_merge(self::DEFAULT_SCOPES, array_filter($this->config->getSystemValue('account_manager.default_property_scope', []), static function (string $scope, string $property) {
516
+            return in_array($property, self::ALLOWED_PROPERTIES, true) && in_array($scope, self::ALLOWED_SCOPES, true);
517
+        }, ARRAY_FILTER_USE_BOTH));
518
+
519
+        return [
520
+            [
521
+                'name' => self::PROPERTY_DISPLAYNAME,
522
+                'value' => $user->getDisplayName(),
523
+                // Display name must be at least SCOPE_LOCAL
524
+                'scope' => $scopes[self::PROPERTY_DISPLAYNAME] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_DISPLAYNAME],
525
+                'verified' => self::NOT_VERIFIED,
526
+            ],
527
+
528
+            [
529
+                'name' => self::PROPERTY_ADDRESS,
530
+                'value' => '',
531
+                'scope' => $scopes[self::PROPERTY_ADDRESS],
532
+                'verified' => self::NOT_VERIFIED,
533
+            ],
534
+
535
+            [
536
+                'name' => self::PROPERTY_WEBSITE,
537
+                'value' => '',
538
+                'scope' => $scopes[self::PROPERTY_WEBSITE],
539
+                'verified' => self::NOT_VERIFIED,
540
+            ],
541
+
542
+            [
543
+                'name' => self::PROPERTY_EMAIL,
544
+                'value' => $user->getEMailAddress(),
545
+                // Email must be at least SCOPE_LOCAL
546
+                'scope' => $scopes[self::PROPERTY_EMAIL] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_EMAIL],
547
+                'verified' => self::NOT_VERIFIED,
548
+            ],
549
+
550
+            [
551
+                'name' => self::PROPERTY_AVATAR,
552
+                'scope' => $scopes[self::PROPERTY_AVATAR],
553
+            ],
554
+
555
+            [
556
+                'name' => self::PROPERTY_PHONE,
557
+                'value' => '',
558
+                'scope' => $scopes[self::PROPERTY_PHONE],
559
+                'verified' => self::NOT_VERIFIED,
560
+            ],
561
+
562
+            [
563
+                'name' => self::PROPERTY_TWITTER,
564
+                'value' => '',
565
+                'scope' => $scopes[self::PROPERTY_TWITTER],
566
+                'verified' => self::NOT_VERIFIED,
567
+            ],
568
+
569
+            [
570
+                'name' => self::PROPERTY_BLUESKY,
571
+                'value' => '',
572
+                'scope' => $scopes[self::PROPERTY_BLUESKY],
573
+                'verified' => self::NOT_VERIFIED,
574
+            ],
575
+
576
+            [
577
+                'name' => self::PROPERTY_FEDIVERSE,
578
+                'value' => '',
579
+                'scope' => $scopes[self::PROPERTY_FEDIVERSE],
580
+                'verified' => self::NOT_VERIFIED,
581
+            ],
582
+
583
+            [
584
+                'name' => self::PROPERTY_ORGANISATION,
585
+                'value' => '',
586
+                'scope' => $scopes[self::PROPERTY_ORGANISATION],
587
+            ],
588
+
589
+            [
590
+                'name' => self::PROPERTY_ROLE,
591
+                'value' => '',
592
+                'scope' => $scopes[self::PROPERTY_ROLE],
593
+            ],
594
+
595
+            [
596
+                'name' => self::PROPERTY_HEADLINE,
597
+                'value' => '',
598
+                'scope' => $scopes[self::PROPERTY_HEADLINE],
599
+            ],
600
+
601
+            [
602
+                'name' => self::PROPERTY_BIOGRAPHY,
603
+                'value' => '',
604
+                'scope' => $scopes[self::PROPERTY_BIOGRAPHY],
605
+            ],
606
+
607
+            [
608
+                'name' => self::PROPERTY_BIRTHDATE,
609
+                'value' => '',
610
+                'scope' => $scopes[self::PROPERTY_BIRTHDATE],
611
+            ],
612
+
613
+            [
614
+                'name' => self::PROPERTY_PROFILE_ENABLED,
615
+                'value' => $this->isProfileEnabledByDefault($this->config) ? '1' : '0',
616
+            ],
617
+
618
+            [
619
+                'name' => self::PROPERTY_PRONOUNS,
620
+                'value' => '',
621
+                'scope' => $scopes[self::PROPERTY_PRONOUNS],
622
+            ],
623
+        ];
624
+    }
625
+
626
+    private function arrayDataToCollection(IAccount $account, array $data): IAccountPropertyCollection {
627
+        $collection = $account->getPropertyCollection($data['name']);
628
+
629
+        $p = new AccountProperty(
630
+            $data['name'],
631
+            $data['value'] ?? '',
632
+            $data['scope'] ?? self::SCOPE_LOCAL,
633
+            $data['verified'] ?? self::NOT_VERIFIED,
634
+            ''
635
+        );
636
+        $p->setLocallyVerified($data['locallyVerified'] ?? self::NOT_VERIFIED);
637
+        $collection->addProperty($p);
638
+
639
+        return $collection;
640
+    }
641
+
642
+    private function parseAccountData(IUser $user, $data): Account {
643
+        $account = new Account($user);
644
+        foreach ($data as $accountData) {
645
+            if ($this->isCollection($accountData['name'])) {
646
+                $account->setPropertyCollection($this->arrayDataToCollection($account, $accountData));
647
+            } else {
648
+                $account->setProperty($accountData['name'], $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
649
+                if (isset($accountData['locallyVerified'])) {
650
+                    $property = $account->getProperty($accountData['name']);
651
+                    $property->setLocallyVerified($accountData['locallyVerified']);
652
+                }
653
+            }
654
+        }
655
+        return $account;
656
+    }
657
+
658
+    public function getAccount(IUser $user): IAccount {
659
+        $cached = $this->internalCache->get($user->getUID());
660
+        if ($cached !== null) {
661
+            return $cached;
662
+        }
663
+        $account = $this->parseAccountData($user, $this->getUser($user));
664
+        if ($user->getBackend() instanceof IGetDisplayNameBackend) {
665
+            $property = $account->getProperty(self::PROPERTY_DISPLAYNAME);
666
+            $account->setProperty(self::PROPERTY_DISPLAYNAME, $user->getDisplayName(), $property->getScope(), $property->getVerified());
667
+        }
668
+        $this->internalCache->set($user->getUID(), $account);
669
+        return $account;
670
+    }
671
+
672
+    /**
673
+     * Converts value (phone number) in E.164 format when it was a valid number
674
+     * @throws InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code
675
+     */
676
+    protected function sanitizePropertyPhoneNumber(IAccountProperty $property): void {
677
+        $defaultRegion = $this->config->getSystemValueString('default_phone_region', '');
678
+
679
+        if ($defaultRegion === '') {
680
+            // When no default region is set, only +49… numbers are valid
681
+            if (!str_starts_with($property->getValue(), '+')) {
682
+                throw new InvalidArgumentException(self::PROPERTY_PHONE);
683
+            }
684
+
685
+            $defaultRegion = 'EN';
686
+        }
687
+
688
+        $phoneNumber = $this->phoneNumberUtil->convertToStandardFormat($property->getValue(), $defaultRegion);
689
+        if ($phoneNumber === null) {
690
+            throw new InvalidArgumentException(self::PROPERTY_PHONE);
691
+        }
692
+        $property->setValue($phoneNumber);
693
+    }
694
+
695
+    /**
696
+     * @throws InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty
697
+     */
698
+    private function sanitizePropertyWebsite(IAccountProperty $property): void {
699
+        $parts = parse_url($property->getValue());
700
+        if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) {
701
+            throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
702
+        }
703
+
704
+        if (!isset($parts['host']) || $parts['host'] === '') {
705
+            throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
706
+        }
707
+    }
708
+
709
+    /**
710
+     * @throws InvalidArgumentException If the property value is not a valid user handle according to X's rules
711
+     */
712
+    private function sanitizePropertyTwitter(IAccountProperty $property): void {
713
+        if ($property->getName() === self::PROPERTY_TWITTER) {
714
+            $matches = [];
715
+            // twitter handles only contain alpha numeric characters and the underscore and must not be longer than 15 characters
716
+            if (preg_match('/^@?([a-zA-Z0-9_]{2,15})$/', $property->getValue(), $matches) !== 1) {
717
+                throw new InvalidArgumentException(self::PROPERTY_TWITTER);
718
+            }
719
+
720
+            // drop the leading @ if any to make it the valid handle
721
+            $property->setValue($matches[1]);
722
+
723
+        }
724
+    }
725
+
726
+    private function validateBlueSkyHandle(string $text): bool {
727
+        if ($text === '') {
728
+            return true;
729
+        }
730
+
731
+        $lowerText = strtolower($text);
732
+
733
+        if ($lowerText === 'bsky.social') {
734
+            // "bsky.social" itself is not a valid handle
735
+            return false;
736
+        }
737
+
738
+        if (str_ends_with($lowerText, '.bsky.social')) {
739
+            $parts = explode('.', $lowerText);
740
+
741
+            // Must be exactly: username.bsky.social → 3 parts
742
+            if (count($parts) !== 3 || $parts[1] !== 'bsky' || $parts[2] !== 'social') {
743
+                return false;
744
+            }
745
+
746
+            $username = $parts[0];
747
+
748
+            // Must be 3–18 chars, alphanumeric/hyphen, no start/end hyphen
749
+            return preg_match('/^[a-z0-9][a-z0-9-]{2,17}$/', $username) === 1;
750
+        }
751
+
752
+        // Allow custom domains (Bluesky handle via personal domain)
753
+        return filter_var($text, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false;
754
+    }
755
+
756
+
757
+    private function sanitizePropertyBluesky(IAccountProperty $property): void {
758
+        if ($property->getName() === self::PROPERTY_BLUESKY) {
759
+            if (!$this->validateBlueSkyHandle($property->getValue())) {
760
+                throw new InvalidArgumentException(self::PROPERTY_BLUESKY);
761
+            }
762
+
763
+            $property->setValue($property->getValue());
764
+        }
765
+    }
766
+
767
+    /**
768
+     * @throws InvalidArgumentException If the property value is not a valid fediverse handle (username@instance where instance is a valid domain)
769
+     */
770
+    private function sanitizePropertyFediverse(IAccountProperty $property): void {
771
+        if ($property->getName() === self::PROPERTY_FEDIVERSE) {
772
+            $matches = [];
773
+            if (preg_match('/^@?([^@\s\/\\\]+)@([^\s\/\\\]+)$/', trim($property->getValue()), $matches) !== 1) {
774
+                throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
775
+            }
776
+
777
+            [, $username, $instance] = $matches;
778
+            $validated = filter_var($instance, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
779
+            if ($validated !== $instance) {
780
+                throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
781
+            }
782
+
783
+            if ($this->config->getSystemValueBool('has_internet_connection', true)) {
784
+                $client = $this->clientService->newClient();
785
+
786
+                try {
787
+                    // try the public account lookup API of mastodon
788
+                    $response = $client->get("https://{$instance}/.well-known/webfinger?resource=acct:{$username}@{$instance}");
789
+                    // should be a json response with account information
790
+                    $data = $response->getBody();
791
+                    if (is_resource($data)) {
792
+                        $data = stream_get_contents($data);
793
+                    }
794
+                    $decoded = json_decode($data, true);
795
+                    // ensure the username is the same the user passed
796
+                    // in this case we can assume this is a valid fediverse server and account
797
+                    if (!is_array($decoded) || ($decoded['subject'] ?? '') !== "acct:{$username}@{$instance}") {
798
+                        throw new InvalidArgumentException();
799
+                    }
800
+                    // check for activitypub link
801
+                    if (is_array($decoded['links']) && isset($decoded['links'])) {
802
+                        $found = false;
803
+                        foreach ($decoded['links'] as $link) {
804
+                            // have application/activity+json or application/ld+json
805
+                            if (isset($link['type']) && (
806
+                                $link['type'] === 'application/activity+json'
807
+                                || $link['type'] === 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
808
+                            )) {
809
+                                $found = true;
810
+                                break;
811
+                            }
812
+                        }
813
+                        if (!$found) {
814
+                            throw new InvalidArgumentException();
815
+                        }
816
+                    }
817
+                } catch (InvalidArgumentException) {
818
+                    throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
819
+                } catch (\Exception $error) {
820
+                    $this->logger->error('Could not verify fediverse account', ['exception' => $error, 'instance' => $instance]);
821
+                    throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
822
+                }
823
+            }
824
+
825
+            $property->setValue("$username@$instance");
826
+        }
827
+    }
828
+
829
+    public function updateAccount(IAccount $account): void {
830
+        $this->testValueLengths(iterator_to_array($account->getAllProperties()), true);
831
+        try {
832
+            $property = $account->getProperty(self::PROPERTY_PHONE);
833
+            if ($property->getValue() !== '') {
834
+                $this->sanitizePropertyPhoneNumber($property);
835
+            }
836
+        } catch (PropertyDoesNotExistException $e) {
837
+            //  valid case, nothing to do
838
+        }
839
+
840
+        try {
841
+            $property = $account->getProperty(self::PROPERTY_WEBSITE);
842
+            if ($property->getValue() !== '') {
843
+                $this->sanitizePropertyWebsite($property);
844
+            }
845
+        } catch (PropertyDoesNotExistException $e) {
846
+            //  valid case, nothing to do
847
+        }
848
+
849
+        try {
850
+            $property = $account->getProperty(self::PROPERTY_TWITTER);
851
+            if ($property->getValue() !== '') {
852
+                $this->sanitizePropertyTwitter($property);
853
+            }
854
+        } catch (PropertyDoesNotExistException $e) {
855
+            //  valid case, nothing to do
856
+        }
857
+
858
+        try {
859
+            $property = $account->getProperty(self::PROPERTY_BLUESKY);
860
+            if ($property->getValue() !== '') {
861
+                $this->sanitizePropertyBluesky($property);
862
+            }
863
+        } catch (PropertyDoesNotExistException $e) {
864
+            //  valid case, nothing to do
865
+        }
866
+
867
+        try {
868
+            $property = $account->getProperty(self::PROPERTY_FEDIVERSE);
869
+            if ($property->getValue() !== '') {
870
+                $this->sanitizePropertyFediverse($property);
871
+            }
872
+        } catch (PropertyDoesNotExistException $e) {
873
+            //  valid case, nothing to do
874
+        }
875
+
876
+        foreach ($account->getAllProperties() as $property) {
877
+            $this->testPropertyScope($property, self::ALLOWED_SCOPES, true);
878
+        }
879
+
880
+        $oldData = $this->getUser($account->getUser(), false);
881
+        $this->updateVerificationStatus($account, $oldData);
882
+        $this->checkEmailVerification($account, $oldData);
883
+        $this->checkLocalEmailVerification($account, $oldData);
884
+
885
+        $data = [];
886
+        foreach ($account->getAllProperties() as $property) {
887
+            /** @var IAccountProperty $property */
888
+            $data[] = [
889
+                'name' => $property->getName(),
890
+                'value' => $property->getValue(),
891
+                'scope' => $property->getScope(),
892
+                'verified' => $property->getVerified(),
893
+                'locallyVerified' => $property->getLocallyVerified(),
894
+            ];
895
+        }
896
+
897
+        $this->updateUser($account->getUser(), $data, $oldData, true);
898
+        $this->internalCache->set($account->getUser()->getUID(), $account);
899
+    }
900 900
 }
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.