diff options
Diffstat (limited to 'lib/private/Accounts')
-rw-r--r-- | lib/private/Accounts/Account.php | 35 | ||||
-rw-r--r-- | lib/private/Accounts/AccountManager.php | 548 | ||||
-rw-r--r-- | lib/private/Accounts/AccountProperty.php | 101 | ||||
-rw-r--r-- | lib/private/Accounts/AccountPropertyCollection.php | 34 | ||||
-rw-r--r-- | lib/private/Accounts/Hooks.php | 40 | ||||
-rw-r--r-- | lib/private/Accounts/TAccountsHelper.php | 23 |
6 files changed, 353 insertions, 428 deletions
diff --git a/lib/private/Accounts/Account.php b/lib/private/Accounts/Account.php index d3287b219d0..946168d7755 100644 --- a/lib/private/Accounts/Account.php +++ b/lib/private/Accounts/Account.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Julius Härtl <jus@bitgrid.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Accounts; @@ -39,13 +20,11 @@ class Account implements IAccount { use TAccountsHelper; /** @var IAccountPropertyCollection[]|IAccountProperty[] */ - private $properties = []; + private array $properties = []; - /** @var IUser */ - private $user; - - public function __construct(IUser $user) { - $this->user = $user; + public function __construct( + private IUser $user, + ) { } public function setProperty(string $property, string $value, string $scope, string $verified, string $verificationData = ''): IAccount { @@ -106,7 +85,7 @@ class Account implements IAccount { } } - public function getFilteredProperties(string $scope = null, string $verified = null): array { + public function getFilteredProperties(?string $scope = null, ?string $verified = null): array { $result = $incrementals = []; /** @var IAccountProperty $obj */ foreach ($this->getAllProperties() as $obj) { 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); } } diff --git a/lib/private/Accounts/AccountProperty.php b/lib/private/Accounts/AccountProperty.php index 896dfd247b5..3a89e9bbc7a 100644 --- a/lib/private/Accounts/AccountProperty.php +++ b/lib/private/Accounts/AccountProperty.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Julius Härtl <jus@bitgrid.net> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Accounts; @@ -32,26 +13,20 @@ use OCP\Accounts\IAccountManager; use OCP\Accounts\IAccountProperty; class AccountProperty implements IAccountProperty { - - /** @var string */ - private $name; - /** @var string */ - private $value; - /** @var string */ - private $scope; - /** @var string */ - private $verified; - /** @var string */ - private $verificationData; - /** @var string */ - private $locallyVerified = IAccountManager::NOT_VERIFIED; - - public function __construct(string $name, string $value, string $scope, string $verified, string $verificationData) { - $this->name = $name; - $this->value = $value; + /** + * @var IAccountManager::SCOPE_* + */ + private string $scope; + private string $locallyVerified = IAccountManager::NOT_VERIFIED; + + public function __construct( + private string $name, + private string $value, + string $scope, + private string $verified, + private string $verificationData, + ) { $this->setScope($scope); - $this->verified = $verified; - $this->verificationData = $verificationData; } public function jsonSerialize(): array { @@ -68,9 +43,6 @@ class AccountProperty implements IAccountProperty { * Set the value of a property * * @since 15.0.0 - * - * @param string $value - * @return IAccountProperty */ public function setValue(string $value): IAccountProperty { $this->value = $value; @@ -81,21 +53,13 @@ class AccountProperty implements IAccountProperty { * Set the scope of a property * * @since 15.0.0 - * - * @param string $scope - * @return IAccountProperty */ public function setScope(string $scope): IAccountProperty { - $newScope = $this->mapScopeToV2($scope); - if (!in_array($newScope, [ - IAccountManager::SCOPE_LOCAL, - IAccountManager::SCOPE_FEDERATED, - IAccountManager::SCOPE_PRIVATE, - IAccountManager::SCOPE_PUBLISHED - ])) { + if (!in_array($scope, IAccountManager::ALLOWED_SCOPES, )) { throw new InvalidArgumentException('Invalid scope'); } - $this->scope = $newScope; + /** @var IAccountManager::SCOPE_* $scope */ + $this->scope = $scope; return $this; } @@ -103,9 +67,6 @@ class AccountProperty implements IAccountProperty { * Set the verification status of a property * * @since 15.0.0 - * - * @param string $verified - * @return IAccountProperty */ public function setVerified(string $verified): IAccountProperty { $this->verified = $verified; @@ -116,8 +77,6 @@ class AccountProperty implements IAccountProperty { * Get the name of a property * * @since 15.0.0 - * - * @return string */ public function getName(): string { return $this->name; @@ -127,8 +86,6 @@ class AccountProperty implements IAccountProperty { * Get the value of a property * * @since 15.0.0 - * - * @return string */ public function getValue(): string { return $this->value; @@ -138,37 +95,15 @@ class AccountProperty implements IAccountProperty { * Get the scope of a property * * @since 15.0.0 - * - * @return string */ public function getScope(): string { return $this->scope; } - public static function mapScopeToV2(string $scope): string { - if (strpos($scope, 'v2-') === 0) { - return $scope; - } - - switch ($scope) { - case IAccountManager::VISIBILITY_PRIVATE: - case '': - return IAccountManager::SCOPE_LOCAL; - case IAccountManager::VISIBILITY_CONTACTS_ONLY: - return IAccountManager::SCOPE_FEDERATED; - case IAccountManager::VISIBILITY_PUBLIC: - return IAccountManager::SCOPE_PUBLISHED; - default: - return $scope; - } - } - /** * Get the verification status of a property * * @since 15.0.0 - * - * @return string */ public function getVerified(): string { return $this->verified; diff --git a/lib/private/Accounts/AccountPropertyCollection.php b/lib/private/Accounts/AccountPropertyCollection.php index 091c5734218..75eea76e686 100644 --- a/lib/private/Accounts/AccountPropertyCollection.php +++ b/lib/private/Accounts/AccountPropertyCollection.php @@ -1,29 +1,10 @@ <?php declare(strict_types=1); - /** - * @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ - namespace OC\Accounts; use InvalidArgumentException; @@ -32,15 +13,12 @@ use OCP\Accounts\IAccountProperty; use OCP\Accounts\IAccountPropertyCollection; class AccountPropertyCollection implements IAccountPropertyCollection { - - /** @var string */ - protected $collectionName = ''; - /** @var IAccountProperty[] */ - protected $properties = []; + protected array $properties = []; - public function __construct(string $collectionName) { - $this->collectionName = $collectionName; + public function __construct( + protected string $collectionName, + ) { } public function setProperties(array $properties): IAccountPropertyCollection { diff --git a/lib/private/Accounts/Hooks.php b/lib/private/Accounts/Hooks.php index 93918284180..12f2b4777f8 100644 --- a/lib/private/Accounts/Hooks.php +++ b/lib/private/Accounts/Hooks.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Bjoern Schiessle <bjoern@schiessle.org> - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Accounts; @@ -32,16 +14,14 @@ use OCP\IUser; use OCP\User\Events\UserChangedEvent; use Psr\Log\LoggerInterface; +/** + * @template-implements IEventListener<UserChangedEvent> + */ class Hooks implements IEventListener { - - /** @var IAccountManager */ - private $accountManager; - /** @var LoggerInterface */ - private $logger; - - public function __construct(LoggerInterface $logger, IAccountManager $accountManager) { - $this->logger = $logger; - $this->accountManager = $accountManager; + public function __construct( + private LoggerInterface $logger, + private IAccountManager $accountManager, + ) { } /** diff --git a/lib/private/Accounts/TAccountsHelper.php b/lib/private/Accounts/TAccountsHelper.php index f3be6523d29..69d25d9a933 100644 --- a/lib/private/Accounts/TAccountsHelper.php +++ b/lib/private/Accounts/TAccountsHelper.php @@ -1,29 +1,10 @@ <?php declare(strict_types=1); - /** - * @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ - namespace OC\Accounts; use OCP\Accounts\IAccountManager; |