diff options
Diffstat (limited to 'lib/private/Accounts/AccountManager.php')
-rw-r--r-- | lib/private/Accounts/AccountManager.php | 548 |
1 files changed, 310 insertions, 238 deletions
diff --git a/lib/private/Accounts/AccountManager.php b/lib/private/Accounts/AccountManager.php index 5792ba1dc5d..d00b1d2e9a3 100644 --- a/lib/private/Accounts/AccountManager.php +++ b/lib/private/Accounts/AccountManager.php @@ -1,69 +1,42 @@ <?php /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @copyright Copyright (c) 2016, Björn Schießle - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Accounts; use Exception; use InvalidArgumentException; -use libphonenumber\NumberParseException; -use libphonenumber\PhoneNumber; -use libphonenumber\PhoneNumberFormat; -use libphonenumber\PhoneNumberUtil; use OC\Profile\TProfileHelper; -use OC\Cache\CappedMemoryCache; use OCA\Settings\BackgroundJobs\VerifyUserData; use OCP\Accounts\IAccount; use OCP\Accounts\IAccountManager; use OCP\Accounts\IAccountProperty; use OCP\Accounts\IAccountPropertyCollection; use OCP\Accounts\PropertyDoesNotExistException; +use OCP\Accounts\UserUpdatedEvent; use OCP\BackgroundJob\IJobList; +use OCP\Cache\CappedMemoryCache; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Defaults; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IDBConnection; use OCP\IL10N; +use OCP\IPhoneNumberUtil; use OCP\IURLGenerator; use OCP\IUser; use OCP\L10N\IFactory; use OCP\Mail\IMailer; use OCP\Security\ICrypto; use OCP\Security\VerificationToken\IVerificationToken; +use OCP\User\Backend\IGetDisplayNameBackend; use OCP\Util; use Psr\Log\LoggerInterface; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; use function array_flip; use function iterator_to_array; use function json_decode; @@ -83,116 +56,48 @@ class AccountManager implements IAccountManager { use TProfileHelper; - /** @var IDBConnection database connection */ - private $connection; - - /** @var IConfig */ - private $config; - - /** @var string table name */ - private $table = 'accounts'; - - /** @var string table name */ - private $dataTable = 'accounts_data'; - - /** @var EventDispatcherInterface */ - private $eventDispatcher; - - /** @var IJobList */ - private $jobList; - - /** @var LoggerInterface */ - private $logger; - /** @var IVerificationToken */ - private $verificationToken; - /** @var IMailer */ - private $mailer; - /** @var Defaults */ - private $defaults; - /** @var IL10N */ - private $l10n; - /** @var IURLGenerator */ - private $urlGenerator; - /** @var ICrypto */ - private $crypto; - /** @var IFactory */ - private $l10nfactory; + private string $table = 'accounts'; + private string $dataTable = 'accounts_data'; + private ?IL10N $l10n = null; private CappedMemoryCache $internalCache; - public function __construct( - IDBConnection $connection, - IConfig $config, - EventDispatcherInterface $eventDispatcher, - IJobList $jobList, - LoggerInterface $logger, - IVerificationToken $verificationToken, - IMailer $mailer, - Defaults $defaults, - IFactory $factory, - IURLGenerator $urlGenerator, - ICrypto $crypto - ) { - $this->connection = $connection; - $this->config = $config; - $this->eventDispatcher = $eventDispatcher; - $this->jobList = $jobList; - $this->logger = $logger; - $this->verificationToken = $verificationToken; - $this->mailer = $mailer; - $this->defaults = $defaults; - $this->urlGenerator = $urlGenerator; - $this->crypto = $crypto; - // DIing IL10N results in a dependency loop - $this->l10nfactory = $factory; - $this->internalCache = new CappedMemoryCache(); - } - - /** - * @param string $input - * @return string Provided phone number in E.164 format when it was a valid number - * @throws InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code - */ - protected function parsePhoneNumber(string $input): string { - $defaultRegion = $this->config->getSystemValueString('default_phone_region', ''); - - if ($defaultRegion === '') { - // When no default region is set, only +49… numbers are valid - if (strpos($input, '+') !== 0) { - throw new InvalidArgumentException(self::PROPERTY_PHONE); - } - - $defaultRegion = 'EN'; - } - - $phoneUtil = PhoneNumberUtil::getInstance(); - try { - $phoneNumber = $phoneUtil->parse($input, $defaultRegion); - if ($phoneNumber instanceof PhoneNumber && $phoneUtil->isValidNumber($phoneNumber)) { - return $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164); - } - } catch (NumberParseException $e) { - } - - throw new InvalidArgumentException(self::PROPERTY_PHONE); - } - /** - * - * @param string $input - * @return string - * @throws InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty + * The list of default scopes for each property. */ - protected function parseWebsite(string $input): string { - $parts = parse_url($input); - if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) { - throw new InvalidArgumentException(self::PROPERTY_WEBSITE); - } - - if (!isset($parts['host']) || $parts['host'] === '') { - throw new InvalidArgumentException(self::PROPERTY_WEBSITE); - } + public const DEFAULT_SCOPES = [ + self::PROPERTY_ADDRESS => self::SCOPE_LOCAL, + self::PROPERTY_AVATAR => self::SCOPE_FEDERATED, + self::PROPERTY_BIOGRAPHY => self::SCOPE_LOCAL, + self::PROPERTY_BIRTHDATE => self::SCOPE_LOCAL, + self::PROPERTY_DISPLAYNAME => self::SCOPE_FEDERATED, + self::PROPERTY_EMAIL => self::SCOPE_FEDERATED, + self::PROPERTY_FEDIVERSE => self::SCOPE_LOCAL, + self::PROPERTY_HEADLINE => self::SCOPE_LOCAL, + self::PROPERTY_ORGANISATION => self::SCOPE_LOCAL, + self::PROPERTY_PHONE => self::SCOPE_LOCAL, + self::PROPERTY_PRONOUNS => self::SCOPE_FEDERATED, + self::PROPERTY_ROLE => self::SCOPE_LOCAL, + self::PROPERTY_TWITTER => self::SCOPE_LOCAL, + self::PROPERTY_BLUESKY => self::SCOPE_LOCAL, + self::PROPERTY_WEBSITE => self::SCOPE_LOCAL, + ]; - return $input; + public function __construct( + private IDBConnection $connection, + private IConfig $config, + private IEventDispatcher $dispatcher, + private IJobList $jobList, + private LoggerInterface $logger, + private IVerificationToken $verificationToken, + private IMailer $mailer, + private Defaults $defaults, + private IFactory $l10nFactory, + private IURLGenerator $urlGenerator, + private ICrypto $crypto, + private IPhoneNumberUtil $phoneNumberUtil, + private IClientService $clientService, + ) { + $this->internalCache = new CappedMemoryCache(); } /** @@ -202,7 +107,7 @@ class AccountManager implements IAccountManager { foreach ($properties as $property) { if (strlen($property->getValue()) > 2048) { if ($throwOnData) { - throw new InvalidArgumentException(); + throw new InvalidArgumentException($property->getName()); } else { $property->setValue(''); } @@ -227,64 +132,29 @@ class AccountManager implements IAccountManager { $property->setScope(self::SCOPE_LOCAL); } } else { - // migrate scope values to the new format - // invalid scopes are mapped to a default value - $property->setScope(AccountProperty::mapScopeToV2($property->getScope())); + $property->setScope($property->getScope()); } } - protected function sanitizePhoneNumberValue(IAccountProperty $property, bool $throwOnData = false) { - if ($property->getName() !== self::PROPERTY_PHONE) { - if ($throwOnData) { - throw new InvalidArgumentException(sprintf('sanitizePhoneNumberValue can only sanitize phone numbers, %s given', $property->getName())); - } - return; + protected function updateUser(IUser $user, array $data, ?array $oldUserData, bool $throwOnData = false): array { + if ($oldUserData === null) { + $oldUserData = $this->getUser($user, false); } - if ($property->getValue() === '') { - return; - } - try { - $property->setValue($this->parsePhoneNumber($property->getValue())); - } catch (InvalidArgumentException $e) { - if ($throwOnData) { - throw $e; - } - $property->setValue(''); - } - } - - protected function sanitizeWebsite(IAccountProperty $property, bool $throwOnData = false) { - if ($property->getName() !== self::PROPERTY_WEBSITE) { - if ($throwOnData) { - throw new InvalidArgumentException(sprintf('sanitizeWebsite can only sanitize web domains, %s given', $property->getName())); - } - } - try { - $property->setValue($this->parseWebsite($property->getValue())); - } catch (InvalidArgumentException $e) { - if ($throwOnData) { - throw $e; - } - $property->setValue(''); - } - } - protected function updateUser(IUser $user, array $data, bool $throwOnData = false): array { - $oldUserData = $this->getUser($user, false); $updated = true; if ($oldUserData !== $data) { - $this->updateExistingUser($user, $data); + $this->updateExistingUser($user, $data, $oldUserData); } else { // nothing needs to be done if new and old data set are the same $updated = false; } if ($updated) { - $this->eventDispatcher->dispatch( - 'OC\AccountManager::userUpdated', - new GenericEvent($user, $data) - ); + $this->dispatcher->dispatchTyped(new UserUpdatedEvent( + $user, + $data, + )); } return $data; @@ -292,30 +162,26 @@ class AccountManager implements IAccountManager { /** * delete user from accounts table - * - * @param IUser $user */ - public function deleteUser(IUser $user) { + public function deleteUser(IUser $user): void { $uid = $user->getUID(); $query = $this->connection->getQueryBuilder(); $query->delete($this->table) ->where($query->expr()->eq('uid', $query->createNamedParameter($uid))) - ->execute(); + ->executeStatement(); $this->deleteUserData($user); } /** * delete user from accounts table - * - * @param IUser $user */ public function deleteUserData(IUser $user): void { $uid = $user->getUID(); $query = $this->connection->getQueryBuilder(); $query->delete($this->dataTable) ->where($query->expr()->eq('uid', $query->createNamedParameter($uid))) - ->execute(); + ->executeStatement(); } /** @@ -377,12 +243,10 @@ class AccountManager implements IAccountManager { } protected function searchUsersForRelatedCollection(string $property, array $values): array { - switch ($property) { - case IAccountManager::PROPERTY_EMAIL: - return array_flip($this->searchUsers(IAccountManager::COLLECTION_EMAIL, $values)); - default: - return []; - } + return match ($property) { + IAccountManager::PROPERTY_EMAIL => array_flip($this->searchUsers(IAccountManager::COLLECTION_EMAIL, $values)), + default => [], + }; } /** @@ -446,7 +310,7 @@ class AccountManager implements IAccountManager { ]); if (!$this->l10n) { - $this->l10n = $this->l10nfactory->get('core'); + $this->l10n = $this->l10nFactory->get('core'); } $emailTemplate->setSubject($this->l10n->t('%s email verification', [$this->defaults->getName()])); @@ -504,6 +368,7 @@ class AccountManager implements IAccountManager { protected function updateVerificationStatus(IAccount $updatedAccount, array $oldData): void { static $propertiesVerifiableByLookupServer = [ self::PROPERTY_TWITTER, + self::PROPERTY_FEDIVERSE, self::PROPERTY_WEBSITE, self::PROPERTY_EMAIL, ]; @@ -530,9 +395,6 @@ class AccountManager implements IAccountManager { /** * add new user to accounts table - * - * @param IUser $user - * @param array $data */ protected function insertNewUser(IUser $user, array $data): void { $uid = $user->getUID(); @@ -601,12 +463,9 @@ class AccountManager implements IAccountManager { } /** - * update existing user in accounts table - * - * @param IUser $user - * @param array $data + * Update existing user in accounts table */ - protected function updateExistingUser(IUser $user, array $data): void { + protected function updateExistingUser(IUser $user, array $data, array $oldData): void { $uid = $user->getUID(); $jsonEncodedData = $this->prepareJson($data); $query = $this->connection->getQueryBuilder(); @@ -649,87 +508,116 @@ class AccountManager implements IAccountManager { /** * build default user record in case not data set exists yet - * - * @param IUser $user - * @return array */ - protected function buildDefaultUserRecord(IUser $user) { + protected function buildDefaultUserRecord(IUser $user): array { + $scopes = array_merge(self::DEFAULT_SCOPES, array_filter($this->config->getSystemValue('account_manager.default_property_scope', []), static function (string $scope, string $property) { + return in_array($property, self::ALLOWED_PROPERTIES, true) && in_array($scope, self::ALLOWED_SCOPES, true); + }, ARRAY_FILTER_USE_BOTH)); + return [ [ 'name' => self::PROPERTY_DISPLAYNAME, 'value' => $user->getDisplayName(), - 'scope' => self::SCOPE_FEDERATED, + // Display name must be at least SCOPE_LOCAL + 'scope' => $scopes[self::PROPERTY_DISPLAYNAME] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_DISPLAYNAME], 'verified' => self::NOT_VERIFIED, ], [ 'name' => self::PROPERTY_ADDRESS, 'value' => '', - 'scope' => self::SCOPE_LOCAL, + 'scope' => $scopes[self::PROPERTY_ADDRESS], 'verified' => self::NOT_VERIFIED, ], [ 'name' => self::PROPERTY_WEBSITE, 'value' => '', - 'scope' => self::SCOPE_LOCAL, + 'scope' => $scopes[self::PROPERTY_WEBSITE], 'verified' => self::NOT_VERIFIED, ], [ 'name' => self::PROPERTY_EMAIL, 'value' => $user->getEMailAddress(), - 'scope' => self::SCOPE_FEDERATED, + // Email must be at least SCOPE_LOCAL + 'scope' => $scopes[self::PROPERTY_EMAIL] === self::SCOPE_PRIVATE ? self::SCOPE_LOCAL : $scopes[self::PROPERTY_EMAIL], 'verified' => self::NOT_VERIFIED, ], [ 'name' => self::PROPERTY_AVATAR, - 'scope' => self::SCOPE_FEDERATED + 'scope' => $scopes[self::PROPERTY_AVATAR], ], [ 'name' => self::PROPERTY_PHONE, 'value' => '', - 'scope' => self::SCOPE_LOCAL, + 'scope' => $scopes[self::PROPERTY_PHONE], 'verified' => self::NOT_VERIFIED, ], [ 'name' => self::PROPERTY_TWITTER, 'value' => '', - 'scope' => self::SCOPE_LOCAL, + 'scope' => $scopes[self::PROPERTY_TWITTER], + 'verified' => self::NOT_VERIFIED, + ], + + [ + 'name' => self::PROPERTY_BLUESKY, + 'value' => '', + 'scope' => $scopes[self::PROPERTY_BLUESKY], + 'verified' => self::NOT_VERIFIED, + ], + + [ + 'name' => self::PROPERTY_FEDIVERSE, + 'value' => '', + 'scope' => $scopes[self::PROPERTY_FEDIVERSE], 'verified' => self::NOT_VERIFIED, ], [ 'name' => self::PROPERTY_ORGANISATION, 'value' => '', - 'scope' => self::SCOPE_LOCAL, + 'scope' => $scopes[self::PROPERTY_ORGANISATION], ], [ 'name' => self::PROPERTY_ROLE, 'value' => '', - 'scope' => self::SCOPE_LOCAL, + 'scope' => $scopes[self::PROPERTY_ROLE], ], [ 'name' => self::PROPERTY_HEADLINE, 'value' => '', - 'scope' => self::SCOPE_LOCAL, + 'scope' => $scopes[self::PROPERTY_HEADLINE], ], [ 'name' => self::PROPERTY_BIOGRAPHY, 'value' => '', - 'scope' => self::SCOPE_LOCAL, + 'scope' => $scopes[self::PROPERTY_BIOGRAPHY], + ], + + [ + 'name' => self::PROPERTY_BIRTHDATE, + 'value' => '', + 'scope' => $scopes[self::PROPERTY_BIRTHDATE], ], [ 'name' => self::PROPERTY_PROFILE_ENABLED, 'value' => $this->isProfileEnabledByDefault($this->config) ? '1' : '0', ], + + [ + 'name' => self::PROPERTY_PRONOUNS, + 'value' => '', + 'scope' => $scopes[self::PROPERTY_PRONOUNS], + ], ]; } @@ -766,41 +654,225 @@ class AccountManager implements IAccountManager { } public function getAccount(IUser $user): IAccount { - if ($this->internalCache->hasKey($user->getUID())) { - return $this->internalCache->get($user->getUID()); + $cached = $this->internalCache->get($user->getUID()); + if ($cached !== null) { + return $cached; } $account = $this->parseAccountData($user, $this->getUser($user)); + if ($user->getBackend() instanceof IGetDisplayNameBackend) { + $property = $account->getProperty(self::PROPERTY_DISPLAYNAME); + $account->setProperty(self::PROPERTY_DISPLAYNAME, $user->getDisplayName(), $property->getScope(), $property->getVerified()); + } $this->internalCache->set($user->getUID(), $account); return $account; } + /** + * Converts value (phone number) in E.164 format when it was a valid number + * @throws InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code + */ + protected function sanitizePropertyPhoneNumber(IAccountProperty $property): void { + $defaultRegion = $this->config->getSystemValueString('default_phone_region', ''); + + if ($defaultRegion === '') { + // When no default region is set, only +49… numbers are valid + if (!str_starts_with($property->getValue(), '+')) { + throw new InvalidArgumentException(self::PROPERTY_PHONE); + } + + $defaultRegion = 'EN'; + } + + $phoneNumber = $this->phoneNumberUtil->convertToStandardFormat($property->getValue(), $defaultRegion); + if ($phoneNumber === null) { + throw new InvalidArgumentException(self::PROPERTY_PHONE); + } + $property->setValue($phoneNumber); + } + + /** + * @throws InvalidArgumentException When the website did not have http(s) as protocol or the host name was empty + */ + private function sanitizePropertyWebsite(IAccountProperty $property): void { + $parts = parse_url($property->getValue()); + if (!isset($parts['scheme']) || ($parts['scheme'] !== 'https' && $parts['scheme'] !== 'http')) { + throw new InvalidArgumentException(self::PROPERTY_WEBSITE); + } + + if (!isset($parts['host']) || $parts['host'] === '') { + throw new InvalidArgumentException(self::PROPERTY_WEBSITE); + } + } + + /** + * @throws InvalidArgumentException If the property value is not a valid user handle according to X's rules + */ + private function sanitizePropertyTwitter(IAccountProperty $property): void { + if ($property->getName() === self::PROPERTY_TWITTER) { + $matches = []; + // twitter handles only contain alpha numeric characters and the underscore and must not be longer than 15 characters + if (preg_match('/^@?([a-zA-Z0-9_]{2,15})$/', $property->getValue(), $matches) !== 1) { + throw new InvalidArgumentException(self::PROPERTY_TWITTER); + } + + // drop the leading @ if any to make it the valid handle + $property->setValue($matches[1]); + + } + } + + private function validateBlueSkyHandle(string $text): bool { + if ($text === '') { + return true; + } + + $lowerText = strtolower($text); + + if ($lowerText === 'bsky.social') { + // "bsky.social" itself is not a valid handle + return false; + } + + if (str_ends_with($lowerText, '.bsky.social')) { + $parts = explode('.', $lowerText); + + // Must be exactly: username.bsky.social → 3 parts + if (count($parts) !== 3 || $parts[1] !== 'bsky' || $parts[2] !== 'social') { + return false; + } + + $username = $parts[0]; + + // Must be 3–18 chars, alphanumeric/hyphen, no start/end hyphen + return preg_match('/^[a-z0-9][a-z0-9-]{2,17}$/', $username) === 1; + } + + // Allow custom domains (Bluesky handle via personal domain) + return filter_var($text, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false; + } + + + private function sanitizePropertyBluesky(IAccountProperty $property): void { + if ($property->getName() === self::PROPERTY_BLUESKY) { + if (!$this->validateBlueSkyHandle($property->getValue())) { + throw new InvalidArgumentException(self::PROPERTY_BLUESKY); + } + + $property->setValue($property->getValue()); + } + } + + /** + * @throws InvalidArgumentException If the property value is not a valid fediverse handle (username@instance where instance is a valid domain) + */ + private function sanitizePropertyFediverse(IAccountProperty $property): void { + if ($property->getName() === self::PROPERTY_FEDIVERSE) { + $matches = []; + if (preg_match('/^@?([^@\s\/\\\]+)@([^\s\/\\\]+)$/', trim($property->getValue()), $matches) !== 1) { + throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE); + } + + [, $username, $instance] = $matches; + $validated = filter_var($instance, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME); + if ($validated !== $instance) { + throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE); + } + + if ($this->config->getSystemValueBool('has_internet_connection', true)) { + $client = $this->clientService->newClient(); + + try { + // try the public account lookup API of mastodon + $response = $client->get("https://{$instance}/.well-known/webfinger?resource=acct:{$username}@{$instance}"); + // should be a json response with account information + $data = $response->getBody(); + if (is_resource($data)) { + $data = stream_get_contents($data); + } + $decoded = json_decode($data, true); + // ensure the username is the same the user passed + // in this case we can assume this is a valid fediverse server and account + if (!is_array($decoded) || ($decoded['subject'] ?? '') !== "acct:{$username}@{$instance}") { + throw new InvalidArgumentException(); + } + // check for activitypub link + if (is_array($decoded['links']) && isset($decoded['links'])) { + $found = false; + foreach ($decoded['links'] as $link) { + // have application/activity+json or application/ld+json + if (isset($link['type']) && ( + $link['type'] === 'application/activity+json' + || $link['type'] === 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' + )) { + $found = true; + break; + } + } + if (!$found) { + throw new InvalidArgumentException(); + } + } + } catch (InvalidArgumentException) { + throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE); + } catch (\Exception $error) { + $this->logger->error('Could not verify fediverse account', ['exception' => $error, 'instance' => $instance]); + throw new InvalidArgumentException(self::PROPERTY_FEDIVERSE); + } + } + + $property->setValue("$username@$instance"); + } + } + public function updateAccount(IAccount $account): void { $this->testValueLengths(iterator_to_array($account->getAllProperties()), true); try { $property = $account->getProperty(self::PROPERTY_PHONE); - $this->sanitizePhoneNumberValue($property); + if ($property->getValue() !== '') { + $this->sanitizePropertyPhoneNumber($property); + } } catch (PropertyDoesNotExistException $e) { // valid case, nothing to do } try { $property = $account->getProperty(self::PROPERTY_WEBSITE); - $this->sanitizeWebsite($property); + if ($property->getValue() !== '') { + $this->sanitizePropertyWebsite($property); + } + } catch (PropertyDoesNotExistException $e) { + // valid case, nothing to do + } + + try { + $property = $account->getProperty(self::PROPERTY_TWITTER); + if ($property->getValue() !== '') { + $this->sanitizePropertyTwitter($property); + } + } catch (PropertyDoesNotExistException $e) { + // valid case, nothing to do + } + + try { + $property = $account->getProperty(self::PROPERTY_BLUESKY); + if ($property->getValue() !== '') { + $this->sanitizePropertyBluesky($property); + } + } catch (PropertyDoesNotExistException $e) { + // valid case, nothing to do + } + + try { + $property = $account->getProperty(self::PROPERTY_FEDIVERSE); + if ($property->getValue() !== '') { + $this->sanitizePropertyFediverse($property); + } } catch (PropertyDoesNotExistException $e) { // valid case, nothing to do } - static $allowedScopes = [ - self::SCOPE_PRIVATE, - self::SCOPE_LOCAL, - self::SCOPE_FEDERATED, - self::SCOPE_PUBLISHED, - self::VISIBILITY_PRIVATE, - self::VISIBILITY_CONTACTS_ONLY, - self::VISIBILITY_PUBLIC, - ]; foreach ($account->getAllProperties() as $property) { - $this->testPropertyScope($property, $allowedScopes, true); + $this->testPropertyScope($property, self::ALLOWED_SCOPES, true); } $oldData = $this->getUser($account->getUser(), false); @@ -820,7 +892,7 @@ class AccountManager implements IAccountManager { ]; } - $this->updateUser($account->getUser(), $data, true); + $this->updateUser($account->getUser(), $data, $oldData, true); $this->internalCache->set($account->getUser()->getUID(), $account); } } |