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