Completed
Push — master ( b3b630...ae6394 )
by John
29:43 queued 04:59
created
lib/public/Accounts/IAccountManager.php 1 patch
Indentation   +199 added lines, -199 removed lines patch added patch discarded remove patch
@@ -18,203 +18,203 @@
 block discarded – undo
18 18
  *
19 19
  */
20 20
 interface IAccountManager {
21
-	/**
22
-	 * Contact details visible locally only
23
-	 *
24
-	 * @since 21.0.1
25
-	 */
26
-	public const SCOPE_PRIVATE = 'v2-private';
27
-
28
-	/**
29
-	 * Contact details visible locally and through public link access on local instance
30
-	 *
31
-	 * @since 21.0.1
32
-	 */
33
-	public const SCOPE_LOCAL = 'v2-local';
34
-
35
-	/**
36
-	 * Contact details visible locally, through public link access and on trusted federated servers.
37
-	 *
38
-	 * @since 21.0.1
39
-	 */
40
-	public const SCOPE_FEDERATED = 'v2-federated';
41
-
42
-	/**
43
-	 * Contact details visible locally, through public link access, on trusted federated servers
44
-	 * and published to the public lookup server.
45
-	 *
46
-	 * @since 21.0.1
47
-	 */
48
-	public const SCOPE_PUBLISHED = 'v2-published';
49
-
50
-	/**
51
-	 * The list of allowed scopes
52
-	 *
53
-	 * @since 25.0.0
54
-	 */
55
-	public const ALLOWED_SCOPES = [
56
-		self::SCOPE_PRIVATE,
57
-		self::SCOPE_LOCAL,
58
-		self::SCOPE_FEDERATED,
59
-		self::SCOPE_PUBLISHED,
60
-	];
61
-
62
-	/**
63
-	 * @since 15.0.0
64
-	 */
65
-	public const PROPERTY_AVATAR = 'avatar';
66
-
67
-	/**
68
-	 * @since 15.0.0
69
-	 */
70
-	public const PROPERTY_DISPLAYNAME = 'displayname';
71
-
72
-	/**
73
-	 * @since 27.0.0
74
-	 * @deprecated 27.0.0 only added for backwards compatibility with provisioning_api UsersController::getCurrentUser
75
-	 */
76
-	public const PROPERTY_DISPLAYNAME_LEGACY = 'display-name';
77
-
78
-	/**
79
-	 * @since 15.0.0
80
-	 */
81
-	public const PROPERTY_PHONE = 'phone';
82
-
83
-	/**
84
-	 * @since 15.0.0
85
-	 */
86
-	public const PROPERTY_EMAIL = 'email';
87
-
88
-	/**
89
-	 * @since 15.0.0
90
-	 */
91
-	public const PROPERTY_WEBSITE = 'website';
92
-
93
-	/**
94
-	 * @since 15.0.0
95
-	 */
96
-	public const PROPERTY_ADDRESS = 'address';
97
-
98
-	/**
99
-	 * @since 15.0.0
100
-	 */
101
-	public const PROPERTY_TWITTER = 'twitter';
102
-
103
-	/**
104
-	 * @since 26.0.0
105
-	 */
106
-	public const PROPERTY_FEDIVERSE = 'fediverse';
107
-
108
-	/**
109
-	 * @since 23.0.0
110
-	 */
111
-	public const PROPERTY_ORGANISATION = 'organisation';
112
-
113
-	/**
114
-	 * @since 23.0.0
115
-	 */
116
-	public const PROPERTY_ROLE = 'role';
117
-
118
-	/**
119
-	 * @since 23.0.0
120
-	 */
121
-	public const PROPERTY_HEADLINE = 'headline';
122
-
123
-	/**
124
-	 * @since 23.0.0
125
-	 */
126
-	public const PROPERTY_BIOGRAPHY = 'biography';
127
-
128
-	/**
129
-	 * @since 23.0.0
130
-	 */
131
-	public const PROPERTY_PROFILE_ENABLED = 'profile_enabled';
132
-
133
-	/**
134
-	 * @since 30.0.0
135
-	 */
136
-	public const PROPERTY_BIRTHDATE = 'birthdate';
137
-
138
-	/**
139
-	 * @since 31.0.0
140
-	 */
141
-	public const PROPERTY_PRONOUNS = 'pronouns';
142
-
143
-	/**
144
-	 * The list of allowed properties
145
-	 *
146
-	 * @since 25.0.0
147
-	 */
148
-	public const ALLOWED_PROPERTIES = [
149
-		self::PROPERTY_ADDRESS,
150
-		self::PROPERTY_AVATAR,
151
-		self::PROPERTY_BIOGRAPHY,
152
-		self::PROPERTY_BIRTHDATE,
153
-		self::PROPERTY_DISPLAYNAME,
154
-		self::PROPERTY_EMAIL,
155
-		self::PROPERTY_FEDIVERSE,
156
-		self::PROPERTY_HEADLINE,
157
-		self::PROPERTY_ORGANISATION,
158
-		self::PROPERTY_PHONE,
159
-		self::PROPERTY_PROFILE_ENABLED,
160
-		self::PROPERTY_PRONOUNS,
161
-		self::PROPERTY_ROLE,
162
-		self::PROPERTY_TWITTER,
163
-		self::PROPERTY_WEBSITE,
164
-	];
165
-
166
-
167
-	/**
168
-	 * @since 22.0.0
169
-	 */
170
-	public const COLLECTION_EMAIL = 'additional_mail';
171
-
172
-	/**
173
-	 * @since 15.0.0
174
-	 */
175
-	public const NOT_VERIFIED = '0';
176
-
177
-	/**
178
-	 * @since 15.0.0
179
-	 */
180
-	public const VERIFICATION_IN_PROGRESS = '1';
181
-
182
-	/**
183
-	 * @since 15.0.0
184
-	 */
185
-	public const VERIFIED = '2';
186
-
187
-	/**
188
-	 * Get the account data for a given user
189
-	 *
190
-	 * @since 15.0.0
191
-	 *
192
-	 * @param IUser $user
193
-	 * @return IAccount
194
-	 */
195
-	public function getAccount(IUser $user): IAccount;
196
-
197
-	/**
198
-	 * Update the account data with for the user
199
-	 *
200
-	 * @since 21.0.1
201
-	 *
202
-	 * @param IAccount $account
203
-	 * @throws \InvalidArgumentException Message is the property that was invalid
204
-	 */
205
-	public function updateAccount(IAccount $account): void;
206
-
207
-	/**
208
-	 * Search for users based on account data
209
-	 *
210
-	 * @param string $property - property or property collection name – since
211
-	 *                         NC 22 the implementation MAY add a fitting property collection into the
212
-	 *                         search even if a property name was given e.g. email property and email
213
-	 *                         collection)
214
-	 * @param string[] $values
215
-	 * @return array
216
-	 *
217
-	 * @since 21.0.0
218
-	 */
219
-	public function searchUsers(string $property, array $values): array;
21
+    /**
22
+     * Contact details visible locally only
23
+     *
24
+     * @since 21.0.1
25
+     */
26
+    public const SCOPE_PRIVATE = 'v2-private';
27
+
28
+    /**
29
+     * Contact details visible locally and through public link access on local instance
30
+     *
31
+     * @since 21.0.1
32
+     */
33
+    public const SCOPE_LOCAL = 'v2-local';
34
+
35
+    /**
36
+     * Contact details visible locally, through public link access and on trusted federated servers.
37
+     *
38
+     * @since 21.0.1
39
+     */
40
+    public const SCOPE_FEDERATED = 'v2-federated';
41
+
42
+    /**
43
+     * Contact details visible locally, through public link access, on trusted federated servers
44
+     * and published to the public lookup server.
45
+     *
46
+     * @since 21.0.1
47
+     */
48
+    public const SCOPE_PUBLISHED = 'v2-published';
49
+
50
+    /**
51
+     * The list of allowed scopes
52
+     *
53
+     * @since 25.0.0
54
+     */
55
+    public const ALLOWED_SCOPES = [
56
+        self::SCOPE_PRIVATE,
57
+        self::SCOPE_LOCAL,
58
+        self::SCOPE_FEDERATED,
59
+        self::SCOPE_PUBLISHED,
60
+    ];
61
+
62
+    /**
63
+     * @since 15.0.0
64
+     */
65
+    public const PROPERTY_AVATAR = 'avatar';
66
+
67
+    /**
68
+     * @since 15.0.0
69
+     */
70
+    public const PROPERTY_DISPLAYNAME = 'displayname';
71
+
72
+    /**
73
+     * @since 27.0.0
74
+     * @deprecated 27.0.0 only added for backwards compatibility with provisioning_api UsersController::getCurrentUser
75
+     */
76
+    public const PROPERTY_DISPLAYNAME_LEGACY = 'display-name';
77
+
78
+    /**
79
+     * @since 15.0.0
80
+     */
81
+    public const PROPERTY_PHONE = 'phone';
82
+
83
+    /**
84
+     * @since 15.0.0
85
+     */
86
+    public const PROPERTY_EMAIL = 'email';
87
+
88
+    /**
89
+     * @since 15.0.0
90
+     */
91
+    public const PROPERTY_WEBSITE = 'website';
92
+
93
+    /**
94
+     * @since 15.0.0
95
+     */
96
+    public const PROPERTY_ADDRESS = 'address';
97
+
98
+    /**
99
+     * @since 15.0.0
100
+     */
101
+    public const PROPERTY_TWITTER = 'twitter';
102
+
103
+    /**
104
+     * @since 26.0.0
105
+     */
106
+    public const PROPERTY_FEDIVERSE = 'fediverse';
107
+
108
+    /**
109
+     * @since 23.0.0
110
+     */
111
+    public const PROPERTY_ORGANISATION = 'organisation';
112
+
113
+    /**
114
+     * @since 23.0.0
115
+     */
116
+    public const PROPERTY_ROLE = 'role';
117
+
118
+    /**
119
+     * @since 23.0.0
120
+     */
121
+    public const PROPERTY_HEADLINE = 'headline';
122
+
123
+    /**
124
+     * @since 23.0.0
125
+     */
126
+    public const PROPERTY_BIOGRAPHY = 'biography';
127
+
128
+    /**
129
+     * @since 23.0.0
130
+     */
131
+    public const PROPERTY_PROFILE_ENABLED = 'profile_enabled';
132
+
133
+    /**
134
+     * @since 30.0.0
135
+     */
136
+    public const PROPERTY_BIRTHDATE = 'birthdate';
137
+
138
+    /**
139
+     * @since 31.0.0
140
+     */
141
+    public const PROPERTY_PRONOUNS = 'pronouns';
142
+
143
+    /**
144
+     * The list of allowed properties
145
+     *
146
+     * @since 25.0.0
147
+     */
148
+    public const ALLOWED_PROPERTIES = [
149
+        self::PROPERTY_ADDRESS,
150
+        self::PROPERTY_AVATAR,
151
+        self::PROPERTY_BIOGRAPHY,
152
+        self::PROPERTY_BIRTHDATE,
153
+        self::PROPERTY_DISPLAYNAME,
154
+        self::PROPERTY_EMAIL,
155
+        self::PROPERTY_FEDIVERSE,
156
+        self::PROPERTY_HEADLINE,
157
+        self::PROPERTY_ORGANISATION,
158
+        self::PROPERTY_PHONE,
159
+        self::PROPERTY_PROFILE_ENABLED,
160
+        self::PROPERTY_PRONOUNS,
161
+        self::PROPERTY_ROLE,
162
+        self::PROPERTY_TWITTER,
163
+        self::PROPERTY_WEBSITE,
164
+    ];
165
+
166
+
167
+    /**
168
+     * @since 22.0.0
169
+     */
170
+    public const COLLECTION_EMAIL = 'additional_mail';
171
+
172
+    /**
173
+     * @since 15.0.0
174
+     */
175
+    public const NOT_VERIFIED = '0';
176
+
177
+    /**
178
+     * @since 15.0.0
179
+     */
180
+    public const VERIFICATION_IN_PROGRESS = '1';
181
+
182
+    /**
183
+     * @since 15.0.0
184
+     */
185
+    public const VERIFIED = '2';
186
+
187
+    /**
188
+     * Get the account data for a given user
189
+     *
190
+     * @since 15.0.0
191
+     *
192
+     * @param IUser $user
193
+     * @return IAccount
194
+     */
195
+    public function getAccount(IUser $user): IAccount;
196
+
197
+    /**
198
+     * Update the account data with for the user
199
+     *
200
+     * @since 21.0.1
201
+     *
202
+     * @param IAccount $account
203
+     * @throws \InvalidArgumentException Message is the property that was invalid
204
+     */
205
+    public function updateAccount(IAccount $account): void;
206
+
207
+    /**
208
+     * Search for users based on account data
209
+     *
210
+     * @param string $property - property or property collection name – since
211
+     *                         NC 22 the implementation MAY add a fitting property collection into the
212
+     *                         search even if a property name was given e.g. email property and email
213
+     *                         collection)
214
+     * @param string[] $values
215
+     * @return array
216
+     *
217
+     * @since 21.0.0
218
+     */
219
+    public function searchUsers(string $property, array $values): array;
220 220
 }
Please login to merge, or discard this patch.
lib/private/Accounts/AccountProperty.php 2 patches
Indentation   +120 added lines, -120 removed lines patch added patch discarded remove patch
@@ -13,124 +13,124 @@
 block discarded – undo
13 13
 use OCP\Accounts\IAccountProperty;
14 14
 
15 15
 class AccountProperty implements IAccountProperty {
16
-	/**
17
-	 * @var IAccountManager::SCOPE_*
18
-	 */
19
-	private string $scope;
20
-	private string $locallyVerified = IAccountManager::NOT_VERIFIED;
21
-
22
-	public function __construct(
23
-		private string $name,
24
-		private string $value,
25
-		string $scope,
26
-		private string $verified,
27
-		private string $verificationData,
28
-	) {
29
-		$this->setScope($scope);
30
-	}
31
-
32
-	public function jsonSerialize(): array {
33
-		return [
34
-			'name' => $this->getName(),
35
-			'value' => $this->getValue(),
36
-			'scope' => $this->getScope(),
37
-			'verified' => $this->getVerified(),
38
-			'verificationData' => $this->getVerificationData(),
39
-		];
40
-	}
41
-
42
-	/**
43
-	 * Set the value of a property
44
-	 *
45
-	 * @since 15.0.0
46
-	 */
47
-	public function setValue(string $value): IAccountProperty {
48
-		$this->value = $value;
49
-		return $this;
50
-	}
51
-
52
-	/**
53
-	 * Set the scope of a property
54
-	 *
55
-	 * @since 15.0.0
56
-	 */
57
-	public function setScope(string $scope): IAccountProperty {
58
-		if (!in_array($scope, IAccountManager::ALLOWED_SCOPES, )) {
59
-			throw new InvalidArgumentException('Invalid scope');
60
-		}
61
-		/** @var IAccountManager::SCOPE_* $scope */
62
-		$this->scope = $scope;
63
-		return $this;
64
-	}
65
-
66
-	/**
67
-	 * Set the verification status of a property
68
-	 *
69
-	 * @since 15.0.0
70
-	 */
71
-	public function setVerified(string $verified): IAccountProperty {
72
-		$this->verified = $verified;
73
-		return $this;
74
-	}
75
-
76
-	/**
77
-	 * Get the name of a property
78
-	 *
79
-	 * @since 15.0.0
80
-	 */
81
-	public function getName(): string {
82
-		return $this->name;
83
-	}
84
-
85
-	/**
86
-	 * Get the value of a property
87
-	 *
88
-	 * @since 15.0.0
89
-	 */
90
-	public function getValue(): string {
91
-		return $this->value;
92
-	}
93
-
94
-	/**
95
-	 * Get the scope of a property
96
-	 *
97
-	 * @since 15.0.0
98
-	 */
99
-	public function getScope(): string {
100
-		return $this->scope;
101
-	}
102
-
103
-	/**
104
-	 * Get the verification status of a property
105
-	 *
106
-	 * @since 15.0.0
107
-	 */
108
-	public function getVerified(): string {
109
-		return $this->verified;
110
-	}
111
-
112
-	public function setVerificationData(string $verificationData): IAccountProperty {
113
-		$this->verificationData = $verificationData;
114
-		return $this;
115
-	}
116
-
117
-	public function getVerificationData(): string {
118
-		return $this->verificationData;
119
-	}
120
-
121
-	public function setLocallyVerified(string $verified): IAccountProperty {
122
-		if (!in_array($verified, [
123
-			IAccountManager::NOT_VERIFIED,
124
-			IAccountManager::VERIFICATION_IN_PROGRESS,
125
-			IAccountManager::VERIFIED,
126
-		])) {
127
-			throw new InvalidArgumentException('Provided verification value is invalid');
128
-		}
129
-		$this->locallyVerified = $verified;
130
-		return $this;
131
-	}
132
-
133
-	public function getLocallyVerified(): string {
134
-		return $this->locallyVerified;
135
-	}
16
+    /**
17
+     * @var IAccountManager::SCOPE_*
18
+     */
19
+    private string $scope;
20
+    private string $locallyVerified = IAccountManager::NOT_VERIFIED;
21
+
22
+    public function __construct(
23
+        private string $name,
24
+        private string $value,
25
+        string $scope,
26
+        private string $verified,
27
+        private string $verificationData,
28
+    ) {
29
+        $this->setScope($scope);
30
+    }
31
+
32
+    public function jsonSerialize(): array {
33
+        return [
34
+            'name' => $this->getName(),
35
+            'value' => $this->getValue(),
36
+            'scope' => $this->getScope(),
37
+            'verified' => $this->getVerified(),
38
+            'verificationData' => $this->getVerificationData(),
39
+        ];
40
+    }
41
+
42
+    /**
43
+     * Set the value of a property
44
+     *
45
+     * @since 15.0.0
46
+     */
47
+    public function setValue(string $value): IAccountProperty {
48
+        $this->value = $value;
49
+        return $this;
50
+    }
51
+
52
+    /**
53
+     * Set the scope of a property
54
+     *
55
+     * @since 15.0.0
56
+     */
57
+    public function setScope(string $scope): IAccountProperty {
58
+        if (!in_array($scope, IAccountManager::ALLOWED_SCOPES, )) {
59
+            throw new InvalidArgumentException('Invalid scope');
60
+        }
61
+        /** @var IAccountManager::SCOPE_* $scope */
62
+        $this->scope = $scope;
63
+        return $this;
64
+    }
65
+
66
+    /**
67
+     * Set the verification status of a property
68
+     *
69
+     * @since 15.0.0
70
+     */
71
+    public function setVerified(string $verified): IAccountProperty {
72
+        $this->verified = $verified;
73
+        return $this;
74
+    }
75
+
76
+    /**
77
+     * Get the name of a property
78
+     *
79
+     * @since 15.0.0
80
+     */
81
+    public function getName(): string {
82
+        return $this->name;
83
+    }
84
+
85
+    /**
86
+     * Get the value of a property
87
+     *
88
+     * @since 15.0.0
89
+     */
90
+    public function getValue(): string {
91
+        return $this->value;
92
+    }
93
+
94
+    /**
95
+     * Get the scope of a property
96
+     *
97
+     * @since 15.0.0
98
+     */
99
+    public function getScope(): string {
100
+        return $this->scope;
101
+    }
102
+
103
+    /**
104
+     * Get the verification status of a property
105
+     *
106
+     * @since 15.0.0
107
+     */
108
+    public function getVerified(): string {
109
+        return $this->verified;
110
+    }
111
+
112
+    public function setVerificationData(string $verificationData): IAccountProperty {
113
+        $this->verificationData = $verificationData;
114
+        return $this;
115
+    }
116
+
117
+    public function getVerificationData(): string {
118
+        return $this->verificationData;
119
+    }
120
+
121
+    public function setLocallyVerified(string $verified): IAccountProperty {
122
+        if (!in_array($verified, [
123
+            IAccountManager::NOT_VERIFIED,
124
+            IAccountManager::VERIFICATION_IN_PROGRESS,
125
+            IAccountManager::VERIFIED,
126
+        ])) {
127
+            throw new InvalidArgumentException('Provided verification value is invalid');
128
+        }
129
+        $this->locallyVerified = $verified;
130
+        return $this;
131
+    }
132
+
133
+    public function getLocallyVerified(): string {
134
+        return $this->locallyVerified;
135
+    }
136 136
 }
Please login to merge, or discard this patch.
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -55,7 +55,7 @@
 block discarded – undo
55 55
 	 * @since 15.0.0
56 56
 	 */
57 57
 	public function setScope(string $scope): IAccountProperty {
58
-		if (!in_array($scope, IAccountManager::ALLOWED_SCOPES, )) {
58
+		if (!in_array($scope, IAccountManager::ALLOWED_SCOPES,)) {
59 59
 			throw new InvalidArgumentException('Invalid scope');
60 60
 		}
61 61
 		/** @var IAccountManager::SCOPE_* $scope */
Please login to merge, or discard this patch.
lib/private/Accounts/AccountManager.php 1 patch
Indentation   +768 added lines, -768 removed lines patch added patch discarded remove patch
@@ -52,772 +52,772 @@
 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_WEBSITE => self::SCOPE_LOCAL,
82
-	];
83
-
84
-	public function __construct(
85
-		private IDBConnection $connection,
86
-		private IConfig $config,
87
-		private IEventDispatcher $dispatcher,
88
-		private IJobList $jobList,
89
-		private LoggerInterface $logger,
90
-		private IVerificationToken $verificationToken,
91
-		private IMailer $mailer,
92
-		private Defaults $defaults,
93
-		private IFactory $l10nFactory,
94
-		private IURLGenerator $urlGenerator,
95
-		private ICrypto $crypto,
96
-		private IPhoneNumberUtil $phoneNumberUtil,
97
-		private IClientService $clientService,
98
-	) {
99
-		$this->internalCache = new CappedMemoryCache();
100
-	}
101
-
102
-	/**
103
-	 * @param IAccountProperty[] $properties
104
-	 */
105
-	protected function testValueLengths(array $properties, bool $throwOnData = false): void {
106
-		foreach ($properties as $property) {
107
-			if (strlen($property->getValue()) > 2048) {
108
-				if ($throwOnData) {
109
-					throw new InvalidArgumentException($property->getName());
110
-				} else {
111
-					$property->setValue('');
112
-				}
113
-			}
114
-		}
115
-	}
116
-
117
-	protected function testPropertyScope(IAccountProperty $property, array $allowedScopes, bool $throwOnData): void {
118
-		if ($throwOnData && !in_array($property->getScope(), $allowedScopes, true)) {
119
-			throw new InvalidArgumentException('scope');
120
-		}
121
-
122
-		if (
123
-			$property->getScope() === self::SCOPE_PRIVATE
124
-			&& in_array($property->getName(), [self::PROPERTY_DISPLAYNAME, self::PROPERTY_EMAIL])
125
-		) {
126
-			if ($throwOnData) {
127
-				// v2-private is not available for these fields
128
-				throw new InvalidArgumentException('scope');
129
-			} else {
130
-				// default to local
131
-				$property->setScope(self::SCOPE_LOCAL);
132
-			}
133
-		} else {
134
-			$property->setScope($property->getScope());
135
-		}
136
-	}
137
-
138
-	protected function updateUser(IUser $user, array $data, ?array $oldUserData, bool $throwOnData = false): array {
139
-		if ($oldUserData === null) {
140
-			$oldUserData = $this->getUser($user, false);
141
-		}
142
-
143
-		$updated = true;
144
-
145
-		if ($oldUserData !== $data) {
146
-			$this->updateExistingUser($user, $data, $oldUserData);
147
-		} else {
148
-			// nothing needs to be done if new and old data set are the same
149
-			$updated = false;
150
-		}
151
-
152
-		if ($updated) {
153
-			$this->dispatcher->dispatchTyped(new UserUpdatedEvent(
154
-				$user,
155
-				$data,
156
-			));
157
-		}
158
-
159
-		return $data;
160
-	}
161
-
162
-	/**
163
-	 * delete user from accounts table
164
-	 */
165
-	public function deleteUser(IUser $user): void {
166
-		$uid = $user->getUID();
167
-		$query = $this->connection->getQueryBuilder();
168
-		$query->delete($this->table)
169
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
170
-			->executeStatement();
171
-
172
-		$this->deleteUserData($user);
173
-	}
174
-
175
-	/**
176
-	 * delete user from accounts table
177
-	 */
178
-	public function deleteUserData(IUser $user): void {
179
-		$uid = $user->getUID();
180
-		$query = $this->connection->getQueryBuilder();
181
-		$query->delete($this->dataTable)
182
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
183
-			->executeStatement();
184
-	}
185
-
186
-	/**
187
-	 * get stored data from a given user
188
-	 */
189
-	protected function getUser(IUser $user, bool $insertIfNotExists = true): array {
190
-		$uid = $user->getUID();
191
-		$query = $this->connection->getQueryBuilder();
192
-		$query->select('data')
193
-			->from($this->table)
194
-			->where($query->expr()->eq('uid', $query->createParameter('uid')))
195
-			->setParameter('uid', $uid);
196
-		$result = $query->executeQuery();
197
-		$accountData = $result->fetchAll();
198
-		$result->closeCursor();
199
-
200
-		if (empty($accountData)) {
201
-			$userData = $this->buildDefaultUserRecord($user);
202
-			if ($insertIfNotExists) {
203
-				$this->insertNewUser($user, $userData);
204
-			}
205
-			return $userData;
206
-		}
207
-
208
-		$userDataArray = $this->importFromJson($accountData[0]['data'], $uid);
209
-		if ($userDataArray === null || $userDataArray === []) {
210
-			return $this->buildDefaultUserRecord($user);
211
-		}
212
-
213
-		return $this->addMissingDefaultValues($userDataArray, $this->buildDefaultUserRecord($user));
214
-	}
215
-
216
-	public function searchUsers(string $property, array $values): array {
217
-		// the value col is limited to 255 bytes. It is used for searches only.
218
-		$values = array_map(function (string $value) {
219
-			return Util::shortenMultibyteString($value, 255);
220
-		}, $values);
221
-		$chunks = array_chunk($values, 500);
222
-		$query = $this->connection->getQueryBuilder();
223
-		$query->select('*')
224
-			->from($this->dataTable)
225
-			->where($query->expr()->eq('name', $query->createNamedParameter($property)))
226
-			->andWhere($query->expr()->in('value', $query->createParameter('values')));
227
-
228
-		$matches = [];
229
-		foreach ($chunks as $chunk) {
230
-			$query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
231
-			$result = $query->executeQuery();
232
-
233
-			while ($row = $result->fetch()) {
234
-				$matches[$row['uid']] = $row['value'];
235
-			}
236
-			$result->closeCursor();
237
-		}
238
-
239
-		$result = array_merge($matches, $this->searchUsersForRelatedCollection($property, $values));
240
-
241
-		return array_flip($result);
242
-	}
243
-
244
-	protected function searchUsersForRelatedCollection(string $property, array $values): array {
245
-		return match ($property) {
246
-			IAccountManager::PROPERTY_EMAIL => array_flip($this->searchUsers(IAccountManager::COLLECTION_EMAIL, $values)),
247
-			default => [],
248
-		};
249
-	}
250
-
251
-	/**
252
-	 * check if we need to ask the server for email verification, if yes we create a cronjob
253
-	 */
254
-	protected function checkEmailVerification(IAccount $updatedAccount, array $oldData): void {
255
-		try {
256
-			$property = $updatedAccount->getProperty(self::PROPERTY_EMAIL);
257
-		} catch (PropertyDoesNotExistException $e) {
258
-			return;
259
-		}
260
-
261
-		$oldMailIndex = array_search(self::PROPERTY_EMAIL, array_column($oldData, 'name'), true);
262
-		$oldMail = $oldMailIndex !== false ? $oldData[$oldMailIndex]['value'] : '';
263
-
264
-		if ($oldMail !== $property->getValue()) {
265
-			$this->jobList->add(
266
-				VerifyUserData::class,
267
-				[
268
-					'verificationCode' => '',
269
-					'data' => $property->getValue(),
270
-					'type' => self::PROPERTY_EMAIL,
271
-					'uid' => $updatedAccount->getUser()->getUID(),
272
-					'try' => 0,
273
-					'lastRun' => time()
274
-				]
275
-			);
276
-
277
-			$property->setVerified(self::VERIFICATION_IN_PROGRESS);
278
-		}
279
-	}
280
-
281
-	protected function checkLocalEmailVerification(IAccount $updatedAccount, array $oldData): void {
282
-		$mailCollection = $updatedAccount->getPropertyCollection(self::COLLECTION_EMAIL);
283
-		foreach ($mailCollection->getProperties() as $property) {
284
-			if ($property->getLocallyVerified() !== self::NOT_VERIFIED) {
285
-				continue;
286
-			}
287
-			if ($this->sendEmailVerificationEmail($updatedAccount->getUser(), $property->getValue())) {
288
-				$property->setLocallyVerified(self::VERIFICATION_IN_PROGRESS);
289
-			}
290
-		}
291
-	}
292
-
293
-	protected function sendEmailVerificationEmail(IUser $user, string $email): bool {
294
-		$ref = \substr(hash('sha256', $email), 0, 8);
295
-		$key = $this->crypto->encrypt($email);
296
-		$token = $this->verificationToken->create($user, 'verifyMail' . $ref, $email);
297
-
298
-		$link = $this->urlGenerator->linkToRouteAbsolute(
299
-			'provisioning_api.Verification.verifyMail',
300
-			[
301
-				'userId' => $user->getUID(),
302
-				'token' => $token,
303
-				'key' => $key
304
-			]
305
-		);
306
-
307
-		$emailTemplate = $this->mailer->createEMailTemplate('core.EmailVerification', [
308
-			'link' => $link,
309
-		]);
310
-
311
-		if (!$this->l10n) {
312
-			$this->l10n = $this->l10nFactory->get('core');
313
-		}
314
-
315
-		$emailTemplate->setSubject($this->l10n->t('%s email verification', [$this->defaults->getName()]));
316
-		$emailTemplate->addHeader();
317
-		$emailTemplate->addHeading($this->l10n->t('Email verification'));
318
-
319
-		$emailTemplate->addBodyText(
320
-			htmlspecialchars($this->l10n->t('Click the following button to confirm your email.')),
321
-			$this->l10n->t('Click the following link to confirm your email.')
322
-		);
323
-
324
-		$emailTemplate->addBodyButton(
325
-			htmlspecialchars($this->l10n->t('Confirm your email')),
326
-			$link,
327
-			false
328
-		);
329
-		$emailTemplate->addFooter();
330
-
331
-		try {
332
-			$message = $this->mailer->createMessage();
333
-			$message->setTo([$email => $user->getDisplayName()]);
334
-			$message->setFrom([Util::getDefaultEmailAddress('verification-noreply') => $this->defaults->getName()]);
335
-			$message->useTemplate($emailTemplate);
336
-			$this->mailer->send($message);
337
-		} catch (Exception $e) {
338
-			// Log the exception and continue
339
-			$this->logger->info('Failed to send verification mail', [
340
-				'app' => 'core',
341
-				'exception' => $e
342
-			]);
343
-			return false;
344
-		}
345
-		return true;
346
-	}
347
-
348
-	/**
349
-	 * Make sure that all expected data are set
350
-	 */
351
-	protected function addMissingDefaultValues(array $userData, array $defaultUserData): array {
352
-		foreach ($defaultUserData as $defaultDataItem) {
353
-			// If property does not exist, initialize it
354
-			$userDataIndex = array_search($defaultDataItem['name'], array_column($userData, 'name'));
355
-			if ($userDataIndex === false) {
356
-				$userData[] = $defaultDataItem;
357
-				continue;
358
-			}
359
-
360
-			// Merge and extend default missing values
361
-			$userData[$userDataIndex] = array_merge($defaultDataItem, $userData[$userDataIndex]);
362
-		}
363
-
364
-		return $userData;
365
-	}
366
-
367
-	protected function updateVerificationStatus(IAccount $updatedAccount, array $oldData): void {
368
-		static $propertiesVerifiableByLookupServer = [
369
-			self::PROPERTY_TWITTER,
370
-			self::PROPERTY_FEDIVERSE,
371
-			self::PROPERTY_WEBSITE,
372
-			self::PROPERTY_EMAIL,
373
-		];
374
-
375
-		foreach ($propertiesVerifiableByLookupServer as $propertyName) {
376
-			try {
377
-				$property = $updatedAccount->getProperty($propertyName);
378
-			} catch (PropertyDoesNotExistException $e) {
379
-				continue;
380
-			}
381
-			$wasVerified = isset($oldData[$propertyName])
382
-				&& isset($oldData[$propertyName]['verified'])
383
-				&& $oldData[$propertyName]['verified'] === self::VERIFIED;
384
-			if ((!isset($oldData[$propertyName])
385
-					|| !isset($oldData[$propertyName]['value'])
386
-					|| $property->getValue() !== $oldData[$propertyName]['value'])
387
-				&& ($property->getVerified() !== self::NOT_VERIFIED
388
-					|| $wasVerified)
389
-			) {
390
-				$property->setVerified(self::NOT_VERIFIED);
391
-			}
392
-		}
393
-	}
394
-
395
-	/**
396
-	 * add new user to accounts table
397
-	 */
398
-	protected function insertNewUser(IUser $user, array $data): void {
399
-		$uid = $user->getUID();
400
-		$jsonEncodedData = $this->prepareJson($data);
401
-		$query = $this->connection->getQueryBuilder();
402
-		$query->insert($this->table)
403
-			->values(
404
-				[
405
-					'uid' => $query->createNamedParameter($uid),
406
-					'data' => $query->createNamedParameter($jsonEncodedData),
407
-				]
408
-			)
409
-			->executeStatement();
410
-
411
-		$this->deleteUserData($user);
412
-		$this->writeUserData($user, $data);
413
-	}
414
-
415
-	protected function prepareJson(array $data): string {
416
-		$preparedData = [];
417
-		foreach ($data as $dataRow) {
418
-			$propertyName = $dataRow['name'];
419
-			unset($dataRow['name']);
420
-
421
-			if (isset($dataRow['locallyVerified']) && $dataRow['locallyVerified'] === self::NOT_VERIFIED) {
422
-				// do not write default value, save DB space
423
-				unset($dataRow['locallyVerified']);
424
-			}
425
-
426
-			if (!$this->isCollection($propertyName)) {
427
-				$preparedData[$propertyName] = $dataRow;
428
-				continue;
429
-			}
430
-			if (!isset($preparedData[$propertyName])) {
431
-				$preparedData[$propertyName] = [];
432
-			}
433
-			$preparedData[$propertyName][] = $dataRow;
434
-		}
435
-		return json_encode($preparedData);
436
-	}
437
-
438
-	protected function importFromJson(string $json, string $userId): ?array {
439
-		$result = [];
440
-		$jsonArray = json_decode($json, true);
441
-		$jsonError = json_last_error();
442
-		if ($jsonError !== JSON_ERROR_NONE) {
443
-			$this->logger->critical(
444
-				'User data of {uid} contained invalid JSON (error {json_error}), hence falling back to a default user record',
445
-				[
446
-					'uid' => $userId,
447
-					'json_error' => $jsonError
448
-				]
449
-			);
450
-			return null;
451
-		}
452
-		foreach ($jsonArray as $propertyName => $row) {
453
-			if (!$this->isCollection($propertyName)) {
454
-				$result[] = array_merge($row, ['name' => $propertyName]);
455
-				continue;
456
-			}
457
-			foreach ($row as $singleRow) {
458
-				$result[] = array_merge($singleRow, ['name' => $propertyName]);
459
-			}
460
-		}
461
-		return $result;
462
-	}
463
-
464
-	/**
465
-	 * Update existing user in accounts table
466
-	 */
467
-	protected function updateExistingUser(IUser $user, array $data, array $oldData): void {
468
-		$uid = $user->getUID();
469
-		$jsonEncodedData = $this->prepareJson($data);
470
-		$query = $this->connection->getQueryBuilder();
471
-		$query->update($this->table)
472
-			->set('data', $query->createNamedParameter($jsonEncodedData))
473
-			->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
474
-			->executeStatement();
475
-
476
-		$this->deleteUserData($user);
477
-		$this->writeUserData($user, $data);
478
-	}
479
-
480
-	protected function writeUserData(IUser $user, array $data): void {
481
-		$query = $this->connection->getQueryBuilder();
482
-		$query->insert($this->dataTable)
483
-			->values(
484
-				[
485
-					'uid' => $query->createNamedParameter($user->getUID()),
486
-					'name' => $query->createParameter('name'),
487
-					'value' => $query->createParameter('value'),
488
-				]
489
-			);
490
-		$this->writeUserDataProperties($query, $data);
491
-	}
492
-
493
-	protected function writeUserDataProperties(IQueryBuilder $query, array $data): void {
494
-		foreach ($data as $property) {
495
-			if ($property['name'] === self::PROPERTY_AVATAR) {
496
-				continue;
497
-			}
498
-
499
-			// the value col is limited to 255 bytes. It is used for searches only.
500
-			$value = $property['value'] ? Util::shortenMultibyteString($property['value'], 255) : '';
501
-
502
-			$query->setParameter('name', $property['name'])
503
-				->setParameter('value', $value);
504
-			$query->executeStatement();
505
-		}
506
-	}
507
-
508
-	/**
509
-	 * build default user record in case not data set exists yet
510
-	 */
511
-	protected function buildDefaultUserRecord(IUser $user): array {
512
-		$scopes = array_merge(self::DEFAULT_SCOPES, array_filter($this->config->getSystemValue('account_manager.default_property_scope', []), static function (string $scope, string $property) {
513
-			return in_array($property, self::ALLOWED_PROPERTIES, true) && in_array($scope, self::ALLOWED_SCOPES, true);
514
-		}, ARRAY_FILTER_USE_BOTH));
515
-
516
-		return [
517
-			[
518
-				'name' => self::PROPERTY_DISPLAYNAME,
519
-				'value' => $user->getDisplayName(),
520
-				// Display name must be at least SCOPE_LOCAL
521
-				'scope' => $scopes[self::PROPERTY_DISPLAYNAME] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_DISPLAYNAME],
522
-				'verified' => self::NOT_VERIFIED,
523
-			],
524
-
525
-			[
526
-				'name' => self::PROPERTY_ADDRESS,
527
-				'value' => '',
528
-				'scope' => $scopes[self::PROPERTY_ADDRESS],
529
-				'verified' => self::NOT_VERIFIED,
530
-			],
531
-
532
-			[
533
-				'name' => self::PROPERTY_WEBSITE,
534
-				'value' => '',
535
-				'scope' => $scopes[self::PROPERTY_WEBSITE],
536
-				'verified' => self::NOT_VERIFIED,
537
-			],
538
-
539
-			[
540
-				'name' => self::PROPERTY_EMAIL,
541
-				'value' => $user->getEMailAddress(),
542
-				// Email must be at least SCOPE_LOCAL
543
-				'scope' => $scopes[self::PROPERTY_EMAIL] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_EMAIL],
544
-				'verified' => self::NOT_VERIFIED,
545
-			],
546
-
547
-			[
548
-				'name' => self::PROPERTY_AVATAR,
549
-				'scope' => $scopes[self::PROPERTY_AVATAR],
550
-			],
551
-
552
-			[
553
-				'name' => self::PROPERTY_PHONE,
554
-				'value' => '',
555
-				'scope' => $scopes[self::PROPERTY_PHONE],
556
-				'verified' => self::NOT_VERIFIED,
557
-			],
558
-
559
-			[
560
-				'name' => self::PROPERTY_TWITTER,
561
-				'value' => '',
562
-				'scope' => $scopes[self::PROPERTY_TWITTER],
563
-				'verified' => self::NOT_VERIFIED,
564
-			],
565
-
566
-			[
567
-				'name' => self::PROPERTY_FEDIVERSE,
568
-				'value' => '',
569
-				'scope' => $scopes[self::PROPERTY_FEDIVERSE],
570
-				'verified' => self::NOT_VERIFIED,
571
-			],
572
-
573
-			[
574
-				'name' => self::PROPERTY_ORGANISATION,
575
-				'value' => '',
576
-				'scope' => $scopes[self::PROPERTY_ORGANISATION],
577
-			],
578
-
579
-			[
580
-				'name' => self::PROPERTY_ROLE,
581
-				'value' => '',
582
-				'scope' => $scopes[self::PROPERTY_ROLE],
583
-			],
584
-
585
-			[
586
-				'name' => self::PROPERTY_HEADLINE,
587
-				'value' => '',
588
-				'scope' => $scopes[self::PROPERTY_HEADLINE],
589
-			],
590
-
591
-			[
592
-				'name' => self::PROPERTY_BIOGRAPHY,
593
-				'value' => '',
594
-				'scope' => $scopes[self::PROPERTY_BIOGRAPHY],
595
-			],
596
-
597
-			[
598
-				'name' => self::PROPERTY_BIRTHDATE,
599
-				'value' => '',
600
-				'scope' => $scopes[self::PROPERTY_BIRTHDATE],
601
-			],
602
-
603
-			[
604
-				'name' => self::PROPERTY_PROFILE_ENABLED,
605
-				'value' => $this->isProfileEnabledByDefault($this->config) ? '1' : '0',
606
-			],
607
-
608
-			[
609
-				'name' => self::PROPERTY_PRONOUNS,
610
-				'value' => '',
611
-				'scope' => $scopes[self::PROPERTY_PRONOUNS],
612
-			],
613
-		];
614
-	}
615
-
616
-	private function arrayDataToCollection(IAccount $account, array $data): IAccountPropertyCollection {
617
-		$collection = $account->getPropertyCollection($data['name']);
618
-
619
-		$p = new AccountProperty(
620
-			$data['name'],
621
-			$data['value'] ?? '',
622
-			$data['scope'] ?? self::SCOPE_LOCAL,
623
-			$data['verified'] ?? self::NOT_VERIFIED,
624
-			''
625
-		);
626
-		$p->setLocallyVerified($data['locallyVerified'] ?? self::NOT_VERIFIED);
627
-		$collection->addProperty($p);
628
-
629
-		return $collection;
630
-	}
631
-
632
-	private function parseAccountData(IUser $user, $data): Account {
633
-		$account = new Account($user);
634
-		foreach ($data as $accountData) {
635
-			if ($this->isCollection($accountData['name'])) {
636
-				$account->setPropertyCollection($this->arrayDataToCollection($account, $accountData));
637
-			} else {
638
-				$account->setProperty($accountData['name'], $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
639
-				if (isset($accountData['locallyVerified'])) {
640
-					$property = $account->getProperty($accountData['name']);
641
-					$property->setLocallyVerified($accountData['locallyVerified']);
642
-				}
643
-			}
644
-		}
645
-		return $account;
646
-	}
647
-
648
-	public function getAccount(IUser $user): IAccount {
649
-		$cached = $this->internalCache->get($user->getUID());
650
-		if ($cached !== null) {
651
-			return $cached;
652
-		}
653
-		$account = $this->parseAccountData($user, $this->getUser($user));
654
-		if ($user->getBackend() instanceof IGetDisplayNameBackend) {
655
-			$property = $account->getProperty(self::PROPERTY_DISPLAYNAME);
656
-			$account->setProperty(self::PROPERTY_DISPLAYNAME, $user->getDisplayName(), $property->getScope(), $property->getVerified());
657
-		}
658
-		$this->internalCache->set($user->getUID(), $account);
659
-		return $account;
660
-	}
661
-
662
-	/**
663
-	 * Converts value (phone number) in E.164 format when it was a valid number
664
-	 * @throws InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code
665
-	 */
666
-	protected function sanitizePropertyPhoneNumber(IAccountProperty $property): void {
667
-		$defaultRegion = $this->config->getSystemValueString('default_phone_region', '');
668
-
669
-		if ($defaultRegion === '') {
670
-			// When no default region is set, only +49… numbers are valid
671
-			if (!str_starts_with($property->getValue(), '+')) {
672
-				throw new InvalidArgumentException(self::PROPERTY_PHONE);
673
-			}
674
-
675
-			$defaultRegion = 'EN';
676
-		}
677
-
678
-		$phoneNumber = $this->phoneNumberUtil->convertToStandardFormat($property->getValue(), $defaultRegion);
679
-		if ($phoneNumber === null) {
680
-			throw new InvalidArgumentException(self::PROPERTY_PHONE);
681
-		}
682
-		$property->setValue($phoneNumber);
683
-	}
684
-
685
-	/**
686
-	 * @throws InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty
687
-	 */
688
-	private function sanitizePropertyWebsite(IAccountProperty $property): void {
689
-		$parts = parse_url($property->getValue());
690
-		if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) {
691
-			throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
692
-		}
693
-
694
-		if (!isset($parts['host']) || $parts['host'] === '') {
695
-			throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
696
-		}
697
-	}
698
-
699
-	/**
700
-	 * @throws InvalidArgumentException If the property value is not a valid user handle according to X's rules
701
-	 */
702
-	private function sanitizePropertyTwitter(IAccountProperty $property): void {
703
-		if ($property->getName() === self::PROPERTY_TWITTER) {
704
-			$matches = [];
705
-			// twitter handles only contain alpha numeric characters and the underscore and must not be longer than 15 characters
706
-			if (preg_match('/^@?([a-zA-Z0-9_]{2,15})$/', $property->getValue(), $matches) !== 1) {
707
-				throw new InvalidArgumentException(self::PROPERTY_TWITTER);
708
-			}
709
-
710
-			// drop the leading @ if any to make it the valid handle
711
-			$property->setValue($matches[1]);
712
-
713
-		}
714
-	}
715
-
716
-	/**
717
-	 * @throws InvalidArgumentException If the property value is not a valid fediverse handle (username@instance where instance is a valid domain)
718
-	 */
719
-	private function sanitizePropertyFediverse(IAccountProperty $property): void {
720
-		if ($property->getName() === self::PROPERTY_FEDIVERSE) {
721
-			$matches = [];
722
-			if (preg_match('/^@?([^@\s\/\\\]+)@([^\s\/\\\]+)$/', trim($property->getValue()), $matches) !== 1) {
723
-				throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
724
-			}
725
-
726
-			[, $username, $instance] = $matches;
727
-			$validated = filter_var($instance, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
728
-			if ($validated !== $instance) {
729
-				throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
730
-			}
731
-
732
-			if ($this->config->getSystemValueBool('has_internet_connection', true)) {
733
-				$client = $this->clientService->newClient();
734
-
735
-				try {
736
-					// try the public account lookup API of mastodon
737
-					$response = $client->get("https://{$instance}/api/v1/accounts/lookup?acct={$username}@{$instance}");
738
-					// should be a json response with account information
739
-					$data = $response->getBody();
740
-					if (is_resource($data)) {
741
-						$data = stream_get_contents($data);
742
-					}
743
-					$decoded = json_decode($data, true);
744
-					// ensure the username is the same the user passed
745
-					// in this case we can assume this is a valid fediverse server and account
746
-					if (!is_array($decoded) || ($decoded['username'] ?? '') !== $username) {
747
-						throw new InvalidArgumentException();
748
-					}
749
-				} catch (InvalidArgumentException) {
750
-					throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
751
-				} catch (\Exception $error) {
752
-					$this->logger->error('Could not verify fediverse account', ['exception' => $error, 'instance' => $instance]);
753
-					throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
754
-				}
755
-			}
756
-
757
-			$property->setValue("$username@$instance");
758
-		}
759
-	}
760
-
761
-	public function updateAccount(IAccount $account): void {
762
-		$this->testValueLengths(iterator_to_array($account->getAllProperties()), true);
763
-		try {
764
-			$property = $account->getProperty(self::PROPERTY_PHONE);
765
-			if ($property->getValue() !== '') {
766
-				$this->sanitizePropertyPhoneNumber($property);
767
-			}
768
-		} catch (PropertyDoesNotExistException $e) {
769
-			//  valid case, nothing to do
770
-		}
771
-
772
-		try {
773
-			$property = $account->getProperty(self::PROPERTY_WEBSITE);
774
-			if ($property->getValue() !== '') {
775
-				$this->sanitizePropertyWebsite($property);
776
-			}
777
-		} catch (PropertyDoesNotExistException $e) {
778
-			//  valid case, nothing to do
779
-		}
780
-
781
-		try {
782
-			$property = $account->getProperty(self::PROPERTY_TWITTER);
783
-			if ($property->getValue() !== '') {
784
-				$this->sanitizePropertyTwitter($property);
785
-			}
786
-		} catch (PropertyDoesNotExistException $e) {
787
-			//  valid case, nothing to do
788
-		}
789
-
790
-		try {
791
-			$property = $account->getProperty(self::PROPERTY_FEDIVERSE);
792
-			if ($property->getValue() !== '') {
793
-				$this->sanitizePropertyFediverse($property);
794
-			}
795
-		} catch (PropertyDoesNotExistException $e) {
796
-			//  valid case, nothing to do
797
-		}
798
-
799
-		foreach ($account->getAllProperties() as $property) {
800
-			$this->testPropertyScope($property, self::ALLOWED_SCOPES, true);
801
-		}
802
-
803
-		$oldData = $this->getUser($account->getUser(), false);
804
-		$this->updateVerificationStatus($account, $oldData);
805
-		$this->checkEmailVerification($account, $oldData);
806
-		$this->checkLocalEmailVerification($account, $oldData);
807
-
808
-		$data = [];
809
-		foreach ($account->getAllProperties() as $property) {
810
-			/** @var IAccountProperty $property */
811
-			$data[] = [
812
-				'name' => $property->getName(),
813
-				'value' => $property->getValue(),
814
-				'scope' => $property->getScope(),
815
-				'verified' => $property->getVerified(),
816
-				'locallyVerified' => $property->getLocallyVerified(),
817
-			];
818
-		}
819
-
820
-		$this->updateUser($account->getUser(), $data, $oldData, true);
821
-		$this->internalCache->set($account->getUser()->getUID(), $account);
822
-	}
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_WEBSITE => self::SCOPE_LOCAL,
82
+    ];
83
+
84
+    public function __construct(
85
+        private IDBConnection $connection,
86
+        private IConfig $config,
87
+        private IEventDispatcher $dispatcher,
88
+        private IJobList $jobList,
89
+        private LoggerInterface $logger,
90
+        private IVerificationToken $verificationToken,
91
+        private IMailer $mailer,
92
+        private Defaults $defaults,
93
+        private IFactory $l10nFactory,
94
+        private IURLGenerator $urlGenerator,
95
+        private ICrypto $crypto,
96
+        private IPhoneNumberUtil $phoneNumberUtil,
97
+        private IClientService $clientService,
98
+    ) {
99
+        $this->internalCache = new CappedMemoryCache();
100
+    }
101
+
102
+    /**
103
+     * @param IAccountProperty[] $properties
104
+     */
105
+    protected function testValueLengths(array $properties, bool $throwOnData = false): void {
106
+        foreach ($properties as $property) {
107
+            if (strlen($property->getValue()) > 2048) {
108
+                if ($throwOnData) {
109
+                    throw new InvalidArgumentException($property->getName());
110
+                } else {
111
+                    $property->setValue('');
112
+                }
113
+            }
114
+        }
115
+    }
116
+
117
+    protected function testPropertyScope(IAccountProperty $property, array $allowedScopes, bool $throwOnData): void {
118
+        if ($throwOnData && !in_array($property->getScope(), $allowedScopes, true)) {
119
+            throw new InvalidArgumentException('scope');
120
+        }
121
+
122
+        if (
123
+            $property->getScope() === self::SCOPE_PRIVATE
124
+            && in_array($property->getName(), [self::PROPERTY_DISPLAYNAME, self::PROPERTY_EMAIL])
125
+        ) {
126
+            if ($throwOnData) {
127
+                // v2-private is not available for these fields
128
+                throw new InvalidArgumentException('scope');
129
+            } else {
130
+                // default to local
131
+                $property->setScope(self::SCOPE_LOCAL);
132
+            }
133
+        } else {
134
+            $property->setScope($property->getScope());
135
+        }
136
+    }
137
+
138
+    protected function updateUser(IUser $user, array $data, ?array $oldUserData, bool $throwOnData = false): array {
139
+        if ($oldUserData === null) {
140
+            $oldUserData = $this->getUser($user, false);
141
+        }
142
+
143
+        $updated = true;
144
+
145
+        if ($oldUserData !== $data) {
146
+            $this->updateExistingUser($user, $data, $oldUserData);
147
+        } else {
148
+            // nothing needs to be done if new and old data set are the same
149
+            $updated = false;
150
+        }
151
+
152
+        if ($updated) {
153
+            $this->dispatcher->dispatchTyped(new UserUpdatedEvent(
154
+                $user,
155
+                $data,
156
+            ));
157
+        }
158
+
159
+        return $data;
160
+    }
161
+
162
+    /**
163
+     * delete user from accounts table
164
+     */
165
+    public function deleteUser(IUser $user): void {
166
+        $uid = $user->getUID();
167
+        $query = $this->connection->getQueryBuilder();
168
+        $query->delete($this->table)
169
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
170
+            ->executeStatement();
171
+
172
+        $this->deleteUserData($user);
173
+    }
174
+
175
+    /**
176
+     * delete user from accounts table
177
+     */
178
+    public function deleteUserData(IUser $user): void {
179
+        $uid = $user->getUID();
180
+        $query = $this->connection->getQueryBuilder();
181
+        $query->delete($this->dataTable)
182
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
183
+            ->executeStatement();
184
+    }
185
+
186
+    /**
187
+     * get stored data from a given user
188
+     */
189
+    protected function getUser(IUser $user, bool $insertIfNotExists = true): array {
190
+        $uid = $user->getUID();
191
+        $query = $this->connection->getQueryBuilder();
192
+        $query->select('data')
193
+            ->from($this->table)
194
+            ->where($query->expr()->eq('uid', $query->createParameter('uid')))
195
+            ->setParameter('uid', $uid);
196
+        $result = $query->executeQuery();
197
+        $accountData = $result->fetchAll();
198
+        $result->closeCursor();
199
+
200
+        if (empty($accountData)) {
201
+            $userData = $this->buildDefaultUserRecord($user);
202
+            if ($insertIfNotExists) {
203
+                $this->insertNewUser($user, $userData);
204
+            }
205
+            return $userData;
206
+        }
207
+
208
+        $userDataArray = $this->importFromJson($accountData[0]['data'], $uid);
209
+        if ($userDataArray === null || $userDataArray === []) {
210
+            return $this->buildDefaultUserRecord($user);
211
+        }
212
+
213
+        return $this->addMissingDefaultValues($userDataArray, $this->buildDefaultUserRecord($user));
214
+    }
215
+
216
+    public function searchUsers(string $property, array $values): array {
217
+        // the value col is limited to 255 bytes. It is used for searches only.
218
+        $values = array_map(function (string $value) {
219
+            return Util::shortenMultibyteString($value, 255);
220
+        }, $values);
221
+        $chunks = array_chunk($values, 500);
222
+        $query = $this->connection->getQueryBuilder();
223
+        $query->select('*')
224
+            ->from($this->dataTable)
225
+            ->where($query->expr()->eq('name', $query->createNamedParameter($property)))
226
+            ->andWhere($query->expr()->in('value', $query->createParameter('values')));
227
+
228
+        $matches = [];
229
+        foreach ($chunks as $chunk) {
230
+            $query->setParameter('values', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
231
+            $result = $query->executeQuery();
232
+
233
+            while ($row = $result->fetch()) {
234
+                $matches[$row['uid']] = $row['value'];
235
+            }
236
+            $result->closeCursor();
237
+        }
238
+
239
+        $result = array_merge($matches, $this->searchUsersForRelatedCollection($property, $values));
240
+
241
+        return array_flip($result);
242
+    }
243
+
244
+    protected function searchUsersForRelatedCollection(string $property, array $values): array {
245
+        return match ($property) {
246
+            IAccountManager::PROPERTY_EMAIL => array_flip($this->searchUsers(IAccountManager::COLLECTION_EMAIL, $values)),
247
+            default => [],
248
+        };
249
+    }
250
+
251
+    /**
252
+     * check if we need to ask the server for email verification, if yes we create a cronjob
253
+     */
254
+    protected function checkEmailVerification(IAccount $updatedAccount, array $oldData): void {
255
+        try {
256
+            $property = $updatedAccount->getProperty(self::PROPERTY_EMAIL);
257
+        } catch (PropertyDoesNotExistException $e) {
258
+            return;
259
+        }
260
+
261
+        $oldMailIndex = array_search(self::PROPERTY_EMAIL, array_column($oldData, 'name'), true);
262
+        $oldMail = $oldMailIndex !== false ? $oldData[$oldMailIndex]['value'] : '';
263
+
264
+        if ($oldMail !== $property->getValue()) {
265
+            $this->jobList->add(
266
+                VerifyUserData::class,
267
+                [
268
+                    'verificationCode' => '',
269
+                    'data' => $property->getValue(),
270
+                    'type' => self::PROPERTY_EMAIL,
271
+                    'uid' => $updatedAccount->getUser()->getUID(),
272
+                    'try' => 0,
273
+                    'lastRun' => time()
274
+                ]
275
+            );
276
+
277
+            $property->setVerified(self::VERIFICATION_IN_PROGRESS);
278
+        }
279
+    }
280
+
281
+    protected function checkLocalEmailVerification(IAccount $updatedAccount, array $oldData): void {
282
+        $mailCollection = $updatedAccount->getPropertyCollection(self::COLLECTION_EMAIL);
283
+        foreach ($mailCollection->getProperties() as $property) {
284
+            if ($property->getLocallyVerified() !== self::NOT_VERIFIED) {
285
+                continue;
286
+            }
287
+            if ($this->sendEmailVerificationEmail($updatedAccount->getUser(), $property->getValue())) {
288
+                $property->setLocallyVerified(self::VERIFICATION_IN_PROGRESS);
289
+            }
290
+        }
291
+    }
292
+
293
+    protected function sendEmailVerificationEmail(IUser $user, string $email): bool {
294
+        $ref = \substr(hash('sha256', $email), 0, 8);
295
+        $key = $this->crypto->encrypt($email);
296
+        $token = $this->verificationToken->create($user, 'verifyMail' . $ref, $email);
297
+
298
+        $link = $this->urlGenerator->linkToRouteAbsolute(
299
+            'provisioning_api.Verification.verifyMail',
300
+            [
301
+                'userId' => $user->getUID(),
302
+                'token' => $token,
303
+                'key' => $key
304
+            ]
305
+        );
306
+
307
+        $emailTemplate = $this->mailer->createEMailTemplate('core.EmailVerification', [
308
+            'link' => $link,
309
+        ]);
310
+
311
+        if (!$this->l10n) {
312
+            $this->l10n = $this->l10nFactory->get('core');
313
+        }
314
+
315
+        $emailTemplate->setSubject($this->l10n->t('%s email verification', [$this->defaults->getName()]));
316
+        $emailTemplate->addHeader();
317
+        $emailTemplate->addHeading($this->l10n->t('Email verification'));
318
+
319
+        $emailTemplate->addBodyText(
320
+            htmlspecialchars($this->l10n->t('Click the following button to confirm your email.')),
321
+            $this->l10n->t('Click the following link to confirm your email.')
322
+        );
323
+
324
+        $emailTemplate->addBodyButton(
325
+            htmlspecialchars($this->l10n->t('Confirm your email')),
326
+            $link,
327
+            false
328
+        );
329
+        $emailTemplate->addFooter();
330
+
331
+        try {
332
+            $message = $this->mailer->createMessage();
333
+            $message->setTo([$email => $user->getDisplayName()]);
334
+            $message->setFrom([Util::getDefaultEmailAddress('verification-noreply') => $this->defaults->getName()]);
335
+            $message->useTemplate($emailTemplate);
336
+            $this->mailer->send($message);
337
+        } catch (Exception $e) {
338
+            // Log the exception and continue
339
+            $this->logger->info('Failed to send verification mail', [
340
+                'app' => 'core',
341
+                'exception' => $e
342
+            ]);
343
+            return false;
344
+        }
345
+        return true;
346
+    }
347
+
348
+    /**
349
+     * Make sure that all expected data are set
350
+     */
351
+    protected function addMissingDefaultValues(array $userData, array $defaultUserData): array {
352
+        foreach ($defaultUserData as $defaultDataItem) {
353
+            // If property does not exist, initialize it
354
+            $userDataIndex = array_search($defaultDataItem['name'], array_column($userData, 'name'));
355
+            if ($userDataIndex === false) {
356
+                $userData[] = $defaultDataItem;
357
+                continue;
358
+            }
359
+
360
+            // Merge and extend default missing values
361
+            $userData[$userDataIndex] = array_merge($defaultDataItem, $userData[$userDataIndex]);
362
+        }
363
+
364
+        return $userData;
365
+    }
366
+
367
+    protected function updateVerificationStatus(IAccount $updatedAccount, array $oldData): void {
368
+        static $propertiesVerifiableByLookupServer = [
369
+            self::PROPERTY_TWITTER,
370
+            self::PROPERTY_FEDIVERSE,
371
+            self::PROPERTY_WEBSITE,
372
+            self::PROPERTY_EMAIL,
373
+        ];
374
+
375
+        foreach ($propertiesVerifiableByLookupServer as $propertyName) {
376
+            try {
377
+                $property = $updatedAccount->getProperty($propertyName);
378
+            } catch (PropertyDoesNotExistException $e) {
379
+                continue;
380
+            }
381
+            $wasVerified = isset($oldData[$propertyName])
382
+                && isset($oldData[$propertyName]['verified'])
383
+                && $oldData[$propertyName]['verified'] === self::VERIFIED;
384
+            if ((!isset($oldData[$propertyName])
385
+                    || !isset($oldData[$propertyName]['value'])
386
+                    || $property->getValue() !== $oldData[$propertyName]['value'])
387
+                && ($property->getVerified() !== self::NOT_VERIFIED
388
+                    || $wasVerified)
389
+            ) {
390
+                $property->setVerified(self::NOT_VERIFIED);
391
+            }
392
+        }
393
+    }
394
+
395
+    /**
396
+     * add new user to accounts table
397
+     */
398
+    protected function insertNewUser(IUser $user, array $data): void {
399
+        $uid = $user->getUID();
400
+        $jsonEncodedData = $this->prepareJson($data);
401
+        $query = $this->connection->getQueryBuilder();
402
+        $query->insert($this->table)
403
+            ->values(
404
+                [
405
+                    'uid' => $query->createNamedParameter($uid),
406
+                    'data' => $query->createNamedParameter($jsonEncodedData),
407
+                ]
408
+            )
409
+            ->executeStatement();
410
+
411
+        $this->deleteUserData($user);
412
+        $this->writeUserData($user, $data);
413
+    }
414
+
415
+    protected function prepareJson(array $data): string {
416
+        $preparedData = [];
417
+        foreach ($data as $dataRow) {
418
+            $propertyName = $dataRow['name'];
419
+            unset($dataRow['name']);
420
+
421
+            if (isset($dataRow['locallyVerified']) && $dataRow['locallyVerified'] === self::NOT_VERIFIED) {
422
+                // do not write default value, save DB space
423
+                unset($dataRow['locallyVerified']);
424
+            }
425
+
426
+            if (!$this->isCollection($propertyName)) {
427
+                $preparedData[$propertyName] = $dataRow;
428
+                continue;
429
+            }
430
+            if (!isset($preparedData[$propertyName])) {
431
+                $preparedData[$propertyName] = [];
432
+            }
433
+            $preparedData[$propertyName][] = $dataRow;
434
+        }
435
+        return json_encode($preparedData);
436
+    }
437
+
438
+    protected function importFromJson(string $json, string $userId): ?array {
439
+        $result = [];
440
+        $jsonArray = json_decode($json, true);
441
+        $jsonError = json_last_error();
442
+        if ($jsonError !== JSON_ERROR_NONE) {
443
+            $this->logger->critical(
444
+                'User data of {uid} contained invalid JSON (error {json_error}), hence falling back to a default user record',
445
+                [
446
+                    'uid' => $userId,
447
+                    'json_error' => $jsonError
448
+                ]
449
+            );
450
+            return null;
451
+        }
452
+        foreach ($jsonArray as $propertyName => $row) {
453
+            if (!$this->isCollection($propertyName)) {
454
+                $result[] = array_merge($row, ['name' => $propertyName]);
455
+                continue;
456
+            }
457
+            foreach ($row as $singleRow) {
458
+                $result[] = array_merge($singleRow, ['name' => $propertyName]);
459
+            }
460
+        }
461
+        return $result;
462
+    }
463
+
464
+    /**
465
+     * Update existing user in accounts table
466
+     */
467
+    protected function updateExistingUser(IUser $user, array $data, array $oldData): void {
468
+        $uid = $user->getUID();
469
+        $jsonEncodedData = $this->prepareJson($data);
470
+        $query = $this->connection->getQueryBuilder();
471
+        $query->update($this->table)
472
+            ->set('data', $query->createNamedParameter($jsonEncodedData))
473
+            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
474
+            ->executeStatement();
475
+
476
+        $this->deleteUserData($user);
477
+        $this->writeUserData($user, $data);
478
+    }
479
+
480
+    protected function writeUserData(IUser $user, array $data): void {
481
+        $query = $this->connection->getQueryBuilder();
482
+        $query->insert($this->dataTable)
483
+            ->values(
484
+                [
485
+                    'uid' => $query->createNamedParameter($user->getUID()),
486
+                    'name' => $query->createParameter('name'),
487
+                    'value' => $query->createParameter('value'),
488
+                ]
489
+            );
490
+        $this->writeUserDataProperties($query, $data);
491
+    }
492
+
493
+    protected function writeUserDataProperties(IQueryBuilder $query, array $data): void {
494
+        foreach ($data as $property) {
495
+            if ($property['name'] === self::PROPERTY_AVATAR) {
496
+                continue;
497
+            }
498
+
499
+            // the value col is limited to 255 bytes. It is used for searches only.
500
+            $value = $property['value'] ? Util::shortenMultibyteString($property['value'], 255) : '';
501
+
502
+            $query->setParameter('name', $property['name'])
503
+                ->setParameter('value', $value);
504
+            $query->executeStatement();
505
+        }
506
+    }
507
+
508
+    /**
509
+     * build default user record in case not data set exists yet
510
+     */
511
+    protected function buildDefaultUserRecord(IUser $user): array {
512
+        $scopes = array_merge(self::DEFAULT_SCOPES, array_filter($this->config->getSystemValue('account_manager.default_property_scope', []), static function (string $scope, string $property) {
513
+            return in_array($property, self::ALLOWED_PROPERTIES, true) && in_array($scope, self::ALLOWED_SCOPES, true);
514
+        }, ARRAY_FILTER_USE_BOTH));
515
+
516
+        return [
517
+            [
518
+                'name' => self::PROPERTY_DISPLAYNAME,
519
+                'value' => $user->getDisplayName(),
520
+                // Display name must be at least SCOPE_LOCAL
521
+                'scope' => $scopes[self::PROPERTY_DISPLAYNAME] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_DISPLAYNAME],
522
+                'verified' => self::NOT_VERIFIED,
523
+            ],
524
+
525
+            [
526
+                'name' => self::PROPERTY_ADDRESS,
527
+                'value' => '',
528
+                'scope' => $scopes[self::PROPERTY_ADDRESS],
529
+                'verified' => self::NOT_VERIFIED,
530
+            ],
531
+
532
+            [
533
+                'name' => self::PROPERTY_WEBSITE,
534
+                'value' => '',
535
+                'scope' => $scopes[self::PROPERTY_WEBSITE],
536
+                'verified' => self::NOT_VERIFIED,
537
+            ],
538
+
539
+            [
540
+                'name' => self::PROPERTY_EMAIL,
541
+                'value' => $user->getEMailAddress(),
542
+                // Email must be at least SCOPE_LOCAL
543
+                'scope' => $scopes[self::PROPERTY_EMAIL] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_EMAIL],
544
+                'verified' => self::NOT_VERIFIED,
545
+            ],
546
+
547
+            [
548
+                'name' => self::PROPERTY_AVATAR,
549
+                'scope' => $scopes[self::PROPERTY_AVATAR],
550
+            ],
551
+
552
+            [
553
+                'name' => self::PROPERTY_PHONE,
554
+                'value' => '',
555
+                'scope' => $scopes[self::PROPERTY_PHONE],
556
+                'verified' => self::NOT_VERIFIED,
557
+            ],
558
+
559
+            [
560
+                'name' => self::PROPERTY_TWITTER,
561
+                'value' => '',
562
+                'scope' => $scopes[self::PROPERTY_TWITTER],
563
+                'verified' => self::NOT_VERIFIED,
564
+            ],
565
+
566
+            [
567
+                'name' => self::PROPERTY_FEDIVERSE,
568
+                'value' => '',
569
+                'scope' => $scopes[self::PROPERTY_FEDIVERSE],
570
+                'verified' => self::NOT_VERIFIED,
571
+            ],
572
+
573
+            [
574
+                'name' => self::PROPERTY_ORGANISATION,
575
+                'value' => '',
576
+                'scope' => $scopes[self::PROPERTY_ORGANISATION],
577
+            ],
578
+
579
+            [
580
+                'name' => self::PROPERTY_ROLE,
581
+                'value' => '',
582
+                'scope' => $scopes[self::PROPERTY_ROLE],
583
+            ],
584
+
585
+            [
586
+                'name' => self::PROPERTY_HEADLINE,
587
+                'value' => '',
588
+                'scope' => $scopes[self::PROPERTY_HEADLINE],
589
+            ],
590
+
591
+            [
592
+                'name' => self::PROPERTY_BIOGRAPHY,
593
+                'value' => '',
594
+                'scope' => $scopes[self::PROPERTY_BIOGRAPHY],
595
+            ],
596
+
597
+            [
598
+                'name' => self::PROPERTY_BIRTHDATE,
599
+                'value' => '',
600
+                'scope' => $scopes[self::PROPERTY_BIRTHDATE],
601
+            ],
602
+
603
+            [
604
+                'name' => self::PROPERTY_PROFILE_ENABLED,
605
+                'value' => $this->isProfileEnabledByDefault($this->config) ? '1' : '0',
606
+            ],
607
+
608
+            [
609
+                'name' => self::PROPERTY_PRONOUNS,
610
+                'value' => '',
611
+                'scope' => $scopes[self::PROPERTY_PRONOUNS],
612
+            ],
613
+        ];
614
+    }
615
+
616
+    private function arrayDataToCollection(IAccount $account, array $data): IAccountPropertyCollection {
617
+        $collection = $account->getPropertyCollection($data['name']);
618
+
619
+        $p = new AccountProperty(
620
+            $data['name'],
621
+            $data['value'] ?? '',
622
+            $data['scope'] ?? self::SCOPE_LOCAL,
623
+            $data['verified'] ?? self::NOT_VERIFIED,
624
+            ''
625
+        );
626
+        $p->setLocallyVerified($data['locallyVerified'] ?? self::NOT_VERIFIED);
627
+        $collection->addProperty($p);
628
+
629
+        return $collection;
630
+    }
631
+
632
+    private function parseAccountData(IUser $user, $data): Account {
633
+        $account = new Account($user);
634
+        foreach ($data as $accountData) {
635
+            if ($this->isCollection($accountData['name'])) {
636
+                $account->setPropertyCollection($this->arrayDataToCollection($account, $accountData));
637
+            } else {
638
+                $account->setProperty($accountData['name'], $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED);
639
+                if (isset($accountData['locallyVerified'])) {
640
+                    $property = $account->getProperty($accountData['name']);
641
+                    $property->setLocallyVerified($accountData['locallyVerified']);
642
+                }
643
+            }
644
+        }
645
+        return $account;
646
+    }
647
+
648
+    public function getAccount(IUser $user): IAccount {
649
+        $cached = $this->internalCache->get($user->getUID());
650
+        if ($cached !== null) {
651
+            return $cached;
652
+        }
653
+        $account = $this->parseAccountData($user, $this->getUser($user));
654
+        if ($user->getBackend() instanceof IGetDisplayNameBackend) {
655
+            $property = $account->getProperty(self::PROPERTY_DISPLAYNAME);
656
+            $account->setProperty(self::PROPERTY_DISPLAYNAME, $user->getDisplayName(), $property->getScope(), $property->getVerified());
657
+        }
658
+        $this->internalCache->set($user->getUID(), $account);
659
+        return $account;
660
+    }
661
+
662
+    /**
663
+     * Converts value (phone number) in E.164 format when it was a valid number
664
+     * @throws InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code
665
+     */
666
+    protected function sanitizePropertyPhoneNumber(IAccountProperty $property): void {
667
+        $defaultRegion = $this->config->getSystemValueString('default_phone_region', '');
668
+
669
+        if ($defaultRegion === '') {
670
+            // When no default region is set, only +49… numbers are valid
671
+            if (!str_starts_with($property->getValue(), '+')) {
672
+                throw new InvalidArgumentException(self::PROPERTY_PHONE);
673
+            }
674
+
675
+            $defaultRegion = 'EN';
676
+        }
677
+
678
+        $phoneNumber = $this->phoneNumberUtil->convertToStandardFormat($property->getValue(), $defaultRegion);
679
+        if ($phoneNumber === null) {
680
+            throw new InvalidArgumentException(self::PROPERTY_PHONE);
681
+        }
682
+        $property->setValue($phoneNumber);
683
+    }
684
+
685
+    /**
686
+     * @throws InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty
687
+     */
688
+    private function sanitizePropertyWebsite(IAccountProperty $property): void {
689
+        $parts = parse_url($property->getValue());
690
+        if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) {
691
+            throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
692
+        }
693
+
694
+        if (!isset($parts['host']) || $parts['host'] === '') {
695
+            throw new InvalidArgumentException(self::PROPERTY_WEBSITE);
696
+        }
697
+    }
698
+
699
+    /**
700
+     * @throws InvalidArgumentException If the property value is not a valid user handle according to X's rules
701
+     */
702
+    private function sanitizePropertyTwitter(IAccountProperty $property): void {
703
+        if ($property->getName() === self::PROPERTY_TWITTER) {
704
+            $matches = [];
705
+            // twitter handles only contain alpha numeric characters and the underscore and must not be longer than 15 characters
706
+            if (preg_match('/^@?([a-zA-Z0-9_]{2,15})$/', $property->getValue(), $matches) !== 1) {
707
+                throw new InvalidArgumentException(self::PROPERTY_TWITTER);
708
+            }
709
+
710
+            // drop the leading @ if any to make it the valid handle
711
+            $property->setValue($matches[1]);
712
+
713
+        }
714
+    }
715
+
716
+    /**
717
+     * @throws InvalidArgumentException If the property value is not a valid fediverse handle (username@instance where instance is a valid domain)
718
+     */
719
+    private function sanitizePropertyFediverse(IAccountProperty $property): void {
720
+        if ($property->getName() === self::PROPERTY_FEDIVERSE) {
721
+            $matches = [];
722
+            if (preg_match('/^@?([^@\s\/\\\]+)@([^\s\/\\\]+)$/', trim($property->getValue()), $matches) !== 1) {
723
+                throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
724
+            }
725
+
726
+            [, $username, $instance] = $matches;
727
+            $validated = filter_var($instance, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
728
+            if ($validated !== $instance) {
729
+                throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
730
+            }
731
+
732
+            if ($this->config->getSystemValueBool('has_internet_connection', true)) {
733
+                $client = $this->clientService->newClient();
734
+
735
+                try {
736
+                    // try the public account lookup API of mastodon
737
+                    $response = $client->get("https://{$instance}/api/v1/accounts/lookup?acct={$username}@{$instance}");
738
+                    // should be a json response with account information
739
+                    $data = $response->getBody();
740
+                    if (is_resource($data)) {
741
+                        $data = stream_get_contents($data);
742
+                    }
743
+                    $decoded = json_decode($data, true);
744
+                    // ensure the username is the same the user passed
745
+                    // in this case we can assume this is a valid fediverse server and account
746
+                    if (!is_array($decoded) || ($decoded['username'] ?? '') !== $username) {
747
+                        throw new InvalidArgumentException();
748
+                    }
749
+                } catch (InvalidArgumentException) {
750
+                    throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
751
+                } catch (\Exception $error) {
752
+                    $this->logger->error('Could not verify fediverse account', ['exception' => $error, 'instance' => $instance]);
753
+                    throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE);
754
+                }
755
+            }
756
+
757
+            $property->setValue("$username@$instance");
758
+        }
759
+    }
760
+
761
+    public function updateAccount(IAccount $account): void {
762
+        $this->testValueLengths(iterator_to_array($account->getAllProperties()), true);
763
+        try {
764
+            $property = $account->getProperty(self::PROPERTY_PHONE);
765
+            if ($property->getValue() !== '') {
766
+                $this->sanitizePropertyPhoneNumber($property);
767
+            }
768
+        } catch (PropertyDoesNotExistException $e) {
769
+            //  valid case, nothing to do
770
+        }
771
+
772
+        try {
773
+            $property = $account->getProperty(self::PROPERTY_WEBSITE);
774
+            if ($property->getValue() !== '') {
775
+                $this->sanitizePropertyWebsite($property);
776
+            }
777
+        } catch (PropertyDoesNotExistException $e) {
778
+            //  valid case, nothing to do
779
+        }
780
+
781
+        try {
782
+            $property = $account->getProperty(self::PROPERTY_TWITTER);
783
+            if ($property->getValue() !== '') {
784
+                $this->sanitizePropertyTwitter($property);
785
+            }
786
+        } catch (PropertyDoesNotExistException $e) {
787
+            //  valid case, nothing to do
788
+        }
789
+
790
+        try {
791
+            $property = $account->getProperty(self::PROPERTY_FEDIVERSE);
792
+            if ($property->getValue() !== '') {
793
+                $this->sanitizePropertyFediverse($property);
794
+            }
795
+        } catch (PropertyDoesNotExistException $e) {
796
+            //  valid case, nothing to do
797
+        }
798
+
799
+        foreach ($account->getAllProperties() as $property) {
800
+            $this->testPropertyScope($property, self::ALLOWED_SCOPES, true);
801
+        }
802
+
803
+        $oldData = $this->getUser($account->getUser(), false);
804
+        $this->updateVerificationStatus($account, $oldData);
805
+        $this->checkEmailVerification($account, $oldData);
806
+        $this->checkLocalEmailVerification($account, $oldData);
807
+
808
+        $data = [];
809
+        foreach ($account->getAllProperties() as $property) {
810
+            /** @var IAccountProperty $property */
811
+            $data[] = [
812
+                'name' => $property->getName(),
813
+                'value' => $property->getValue(),
814
+                'scope' => $property->getScope(),
815
+                'verified' => $property->getVerified(),
816
+                'locallyVerified' => $property->getLocallyVerified(),
817
+            ];
818
+        }
819
+
820
+        $this->updateUser($account->getUser(), $data, $oldData, true);
821
+        $this->internalCache->set($account->getUser()->getUID(), $account);
822
+    }
823 823
 }
Please login to merge, or discard this patch.
tests/lib/Accounts/AccountPropertyTest.php 1 patch
Indentation   +106 added lines, -106 removed lines patch added patch discarded remove patch
@@ -16,117 +16,117 @@
 block discarded – undo
16 16
  * @package Test\Accounts
17 17
  */
18 18
 class AccountPropertyTest extends TestCase {
19
-	public function testConstructor(): void {
20
-		$accountProperty = new AccountProperty(
21
-			IAccountManager::PROPERTY_WEBSITE,
22
-			'https://example.com',
23
-			IAccountManager::SCOPE_PUBLISHED,
24
-			IAccountManager::VERIFIED,
25
-			''
26
-		);
27
-		$this->assertEquals(IAccountManager::PROPERTY_WEBSITE, $accountProperty->getName());
28
-		$this->assertEquals('https://example.com', $accountProperty->getValue());
29
-		$this->assertEquals(IAccountManager::SCOPE_PUBLISHED, $accountProperty->getScope());
30
-		$this->assertEquals(IAccountManager::VERIFIED, $accountProperty->getVerified());
31
-	}
19
+    public function testConstructor(): void {
20
+        $accountProperty = new AccountProperty(
21
+            IAccountManager::PROPERTY_WEBSITE,
22
+            'https://example.com',
23
+            IAccountManager::SCOPE_PUBLISHED,
24
+            IAccountManager::VERIFIED,
25
+            ''
26
+        );
27
+        $this->assertEquals(IAccountManager::PROPERTY_WEBSITE, $accountProperty->getName());
28
+        $this->assertEquals('https://example.com', $accountProperty->getValue());
29
+        $this->assertEquals(IAccountManager::SCOPE_PUBLISHED, $accountProperty->getScope());
30
+        $this->assertEquals(IAccountManager::VERIFIED, $accountProperty->getVerified());
31
+    }
32 32
 
33
-	public function testSetValue(): void {
34
-		$accountProperty = new AccountProperty(
35
-			IAccountManager::PROPERTY_WEBSITE,
36
-			'https://example.com',
37
-			IAccountManager::SCOPE_PUBLISHED,
38
-			IAccountManager::VERIFIED,
39
-			''
40
-		);
41
-		$actualReturn = $accountProperty->setValue('https://example.org');
42
-		$this->assertEquals('https://example.org', $accountProperty->getValue());
43
-		$this->assertEquals('https://example.org', $actualReturn->getValue());
44
-	}
33
+    public function testSetValue(): void {
34
+        $accountProperty = new AccountProperty(
35
+            IAccountManager::PROPERTY_WEBSITE,
36
+            'https://example.com',
37
+            IAccountManager::SCOPE_PUBLISHED,
38
+            IAccountManager::VERIFIED,
39
+            ''
40
+        );
41
+        $actualReturn = $accountProperty->setValue('https://example.org');
42
+        $this->assertEquals('https://example.org', $accountProperty->getValue());
43
+        $this->assertEquals('https://example.org', $actualReturn->getValue());
44
+    }
45 45
 
46
-	public function testSetScope(): void {
47
-		$accountProperty = new AccountProperty(
48
-			IAccountManager::PROPERTY_WEBSITE,
49
-			'https://example.com',
50
-			IAccountManager::SCOPE_PUBLISHED,
51
-			IAccountManager::VERIFIED,
52
-			''
53
-		);
54
-		$actualReturn = $accountProperty->setScope(IAccountManager::SCOPE_LOCAL);
55
-		$this->assertEquals(IAccountManager::SCOPE_LOCAL, $accountProperty->getScope());
56
-		$this->assertEquals(IAccountManager::SCOPE_LOCAL, $actualReturn->getScope());
57
-	}
46
+    public function testSetScope(): void {
47
+        $accountProperty = new AccountProperty(
48
+            IAccountManager::PROPERTY_WEBSITE,
49
+            'https://example.com',
50
+            IAccountManager::SCOPE_PUBLISHED,
51
+            IAccountManager::VERIFIED,
52
+            ''
53
+        );
54
+        $actualReturn = $accountProperty->setScope(IAccountManager::SCOPE_LOCAL);
55
+        $this->assertEquals(IAccountManager::SCOPE_LOCAL, $accountProperty->getScope());
56
+        $this->assertEquals(IAccountManager::SCOPE_LOCAL, $actualReturn->getScope());
57
+    }
58 58
 
59
-	public function scopesProvider() {
60
-		return [
61
-			// current values
62
-			[IAccountManager::SCOPE_PRIVATE, IAccountManager::SCOPE_PRIVATE],
63
-			[IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_LOCAL],
64
-			[IAccountManager::SCOPE_FEDERATED, IAccountManager::SCOPE_FEDERATED],
65
-			[IAccountManager::SCOPE_PUBLISHED, IAccountManager::SCOPE_PUBLISHED],
66
-			// invalid values
67
-			['unknown', null],
68
-			['v2-unknown', null],
69
-		];
70
-	}
59
+    public function scopesProvider() {
60
+        return [
61
+            // current values
62
+            [IAccountManager::SCOPE_PRIVATE, IAccountManager::SCOPE_PRIVATE],
63
+            [IAccountManager::SCOPE_LOCAL, IAccountManager::SCOPE_LOCAL],
64
+            [IAccountManager::SCOPE_FEDERATED, IAccountManager::SCOPE_FEDERATED],
65
+            [IAccountManager::SCOPE_PUBLISHED, IAccountManager::SCOPE_PUBLISHED],
66
+            // invalid values
67
+            ['unknown', null],
68
+            ['v2-unknown', null],
69
+        ];
70
+    }
71 71
 
72
-	/**
73
-	 * @dataProvider scopesProvider
74
-	 */
75
-	public function testSetScopeMapping(string $storedScope, ?string $returnedScope): void {
76
-		if ($returnedScope === null) {
77
-			$this->expectException(\InvalidArgumentException::class);
78
-		}
79
-		$accountProperty = new AccountProperty(
80
-			IAccountManager::PROPERTY_WEBSITE,
81
-			'https://example.com',
82
-			$storedScope,
83
-			IAccountManager::VERIFIED,
84
-			''
85
-		);
86
-		$this->assertEquals($returnedScope, $accountProperty->getScope());
87
-	}
72
+    /**
73
+     * @dataProvider scopesProvider
74
+     */
75
+    public function testSetScopeMapping(string $storedScope, ?string $returnedScope): void {
76
+        if ($returnedScope === null) {
77
+            $this->expectException(\InvalidArgumentException::class);
78
+        }
79
+        $accountProperty = new AccountProperty(
80
+            IAccountManager::PROPERTY_WEBSITE,
81
+            'https://example.com',
82
+            $storedScope,
83
+            IAccountManager::VERIFIED,
84
+            ''
85
+        );
86
+        $this->assertEquals($returnedScope, $accountProperty->getScope());
87
+    }
88 88
 
89
-	public function testSetVerified(): void {
90
-		$accountProperty = new AccountProperty(
91
-			IAccountManager::PROPERTY_WEBSITE,
92
-			'https://example.com',
93
-			IAccountManager::SCOPE_PUBLISHED,
94
-			IAccountManager::VERIFIED,
95
-			''
96
-		);
97
-		$actualReturn = $accountProperty->setVerified(IAccountManager::NOT_VERIFIED);
98
-		$this->assertEquals(IAccountManager::NOT_VERIFIED, $accountProperty->getVerified());
99
-		$this->assertEquals(IAccountManager::NOT_VERIFIED, $actualReturn->getVerified());
100
-	}
89
+    public function testSetVerified(): void {
90
+        $accountProperty = new AccountProperty(
91
+            IAccountManager::PROPERTY_WEBSITE,
92
+            'https://example.com',
93
+            IAccountManager::SCOPE_PUBLISHED,
94
+            IAccountManager::VERIFIED,
95
+            ''
96
+        );
97
+        $actualReturn = $accountProperty->setVerified(IAccountManager::NOT_VERIFIED);
98
+        $this->assertEquals(IAccountManager::NOT_VERIFIED, $accountProperty->getVerified());
99
+        $this->assertEquals(IAccountManager::NOT_VERIFIED, $actualReturn->getVerified());
100
+    }
101 101
 
102
-	public function testSetVerificationData(): void {
103
-		$accountProperty = new AccountProperty(
104
-			IAccountManager::PROPERTY_WEBSITE,
105
-			'https://example.com',
106
-			IAccountManager::SCOPE_PUBLISHED,
107
-			IAccountManager::VERIFIED,
108
-			''
109
-		);
110
-		$token = uniqid();
111
-		$actualReturn = $accountProperty->setVerificationData($token);
112
-		$this->assertEquals($token, $accountProperty->getVerificationData());
113
-		$this->assertEquals($token, $actualReturn->getVerificationData());
114
-	}
102
+    public function testSetVerificationData(): void {
103
+        $accountProperty = new AccountProperty(
104
+            IAccountManager::PROPERTY_WEBSITE,
105
+            'https://example.com',
106
+            IAccountManager::SCOPE_PUBLISHED,
107
+            IAccountManager::VERIFIED,
108
+            ''
109
+        );
110
+        $token = uniqid();
111
+        $actualReturn = $accountProperty->setVerificationData($token);
112
+        $this->assertEquals($token, $accountProperty->getVerificationData());
113
+        $this->assertEquals($token, $actualReturn->getVerificationData());
114
+    }
115 115
 
116
-	public function testJsonSerialize(): void {
117
-		$accountProperty = new AccountProperty(
118
-			IAccountManager::PROPERTY_WEBSITE,
119
-			'https://example.com',
120
-			IAccountManager::SCOPE_PUBLISHED,
121
-			IAccountManager::VERIFIED,
122
-			'60a7a633b74af',
123
-		);
124
-		$this->assertEquals([
125
-			'name' => IAccountManager::PROPERTY_WEBSITE,
126
-			'value' => 'https://example.com',
127
-			'scope' => IAccountManager::SCOPE_PUBLISHED,
128
-			'verified' => IAccountManager::VERIFIED,
129
-			'verificationData' => '60a7a633b74af'
130
-		], $accountProperty->jsonSerialize());
131
-	}
116
+    public function testJsonSerialize(): void {
117
+        $accountProperty = new AccountProperty(
118
+            IAccountManager::PROPERTY_WEBSITE,
119
+            'https://example.com',
120
+            IAccountManager::SCOPE_PUBLISHED,
121
+            IAccountManager::VERIFIED,
122
+            '60a7a633b74af',
123
+        );
124
+        $this->assertEquals([
125
+            'name' => IAccountManager::PROPERTY_WEBSITE,
126
+            'value' => 'https://example.com',
127
+            'scope' => IAccountManager::SCOPE_PUBLISHED,
128
+            'verified' => IAccountManager::VERIFIED,
129
+            'verificationData' => '60a7a633b74af'
130
+        ], $accountProperty->jsonSerialize());
131
+    }
132 132
 }
Please login to merge, or discard this patch.