diff options
author | blizzz <blizzz@arthur-schiwon.de> | 2021-06-08 14:25:18 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-06-08 14:25:18 +0200 |
commit | 662ab937e0d30947727be1462f8744681fdd2e49 (patch) | |
tree | 3b857453adb11fb00d23d68515b85f6f84cfad6c /lib/private/Accounts | |
parent | b3cfa1859b14384ae8134e8eb88c171667f77799 (diff) | |
parent | ff2382e5a4a5c29e3e1c948a514c151cae71d402 (diff) | |
download | nextcloud-server-662ab937e0d30947727be1462f8744681fdd2e49.tar.gz nextcloud-server-662ab937e0d30947727be1462f8744681fdd2e49.zip |
Merge pull request #27189 from nextcloud/feat/26866/account-collection-properties
Extend Accounts with multivalue properties (PropertyCollection)
Diffstat (limited to 'lib/private/Accounts')
-rw-r--r-- | lib/private/Accounts/Account.php | 62 | ||||
-rw-r--r-- | lib/private/Accounts/AccountManager.php | 138 | ||||
-rw-r--r-- | lib/private/Accounts/AccountPropertyCollection.php | 90 | ||||
-rw-r--r-- | lib/private/Accounts/TAccountsHelper.php | 40 |
4 files changed, 273 insertions, 57 deletions
diff --git a/lib/private/Accounts/Account.php b/lib/private/Accounts/Account.php index f7094f14cd9..7d2a51c7d4e 100644 --- a/lib/private/Accounts/Account.php +++ b/lib/private/Accounts/Account.php @@ -27,14 +27,17 @@ declare(strict_types=1); */ namespace OC\Accounts; +use Generator; use OCP\Accounts\IAccount; use OCP\Accounts\IAccountProperty; +use OCP\Accounts\IAccountPropertyCollection; use OCP\Accounts\PropertyDoesNotExistException; use OCP\IUser; class Account implements IAccount { + use TAccountsHelper; - /** @var IAccountProperty[] */ + /** @var IAccountPropertyCollection[]|IAccountProperty[] */ private $properties = []; /** @var IUser */ @@ -45,31 +48,59 @@ class Account implements IAccount { } public function setProperty(string $property, string $value, string $scope, string $verified, string $verificationData = ''): IAccount { + if ($this->isCollection($property)) { + throw new \InvalidArgumentException('setProperty cannot set an IAccountsPropertyCollection'); + } $this->properties[$property] = new AccountProperty($property, $value, $scope, $verified, $verificationData); return $this; } public function getProperty(string $property): IAccountProperty { - if (!array_key_exists($property, $this->properties)) { + if ($this->isCollection($property)) { + throw new \InvalidArgumentException('getProperty cannot retrieve an IAccountsPropertyCollection'); + } + if (!array_key_exists($property, $this->properties) || !$this->properties[$property] instanceof IAccountProperty) { throw new PropertyDoesNotExistException($property); } return $this->properties[$property]; } public function getProperties(): array { - return $this->properties; + return array_filter($this->properties, function ($obj) { + return $obj instanceof IAccountProperty; + }); + } + + public function getAllProperties(): Generator { + foreach ($this->properties as $propertyObject) { + if ($propertyObject instanceof IAccountProperty) { + yield $propertyObject; + } elseif ($propertyObject instanceof IAccountPropertyCollection) { + foreach ($propertyObject->getProperties() as $property) { + yield $property; + } + } + } } public function getFilteredProperties(string $scope = null, string $verified = null): array { - return \array_filter($this->properties, function (IAccountProperty $obj) use ($scope, $verified) { + $result = $incrementals = []; + /** @var IAccountProperty $obj */ + foreach ($this->getAllProperties() as $obj) { if ($scope !== null && $scope !== $obj->getScope()) { - return false; + continue; } if ($verified !== null && $verified !== $obj->getVerified()) { - return false; + continue; } - return true; - }); + $index = $obj->getName(); + if ($this->isCollection($index)) { + $incrementals[$index] = ($incrementals[$index] ?? -1) + 1; + $index .= '#' . $incrementals[$index]; + } + $result[$index] = $obj; + } + return $result; } public function jsonSerialize() { @@ -79,4 +110,19 @@ class Account implements IAccount { public function getUser(): IUser { return $this->user; } + + public function setPropertyCollection(IAccountPropertyCollection $propertyCollection): IAccount { + $this->properties[$propertyCollection->getName()] = $propertyCollection; + return $this; + } + + public function getPropertyCollection(string $propertyCollection): IAccountPropertyCollection { + if (!array_key_exists($propertyCollection, $this->properties)) { + throw new PropertyDoesNotExistException($propertyCollection); + } + if (!$this->properties[$propertyCollection] instanceof IAccountPropertyCollection) { + throw new \RuntimeException('Requested collection is not an IAccountPropertyCollection'); + } + return $this->properties[$propertyCollection]; + } } diff --git a/lib/private/Accounts/AccountManager.php b/lib/private/Accounts/AccountManager.php index 62fe4b6e5c6..4d75c94346b 100644 --- a/lib/private/Accounts/AccountManager.php +++ b/lib/private/Accounts/AccountManager.php @@ -47,6 +47,7 @@ use OCP\IUser; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\GenericEvent; +use function array_flip; use function json_decode; use function json_last_error; @@ -59,6 +60,7 @@ use function json_last_error; * @package OC\Accounts */ class AccountManager implements IAccountManager { + use TAccountsHelper; /** @var IDBConnection database connection */ private $connection; @@ -141,6 +143,61 @@ class AccountManager implements IAccountManager { return $input; } + protected function sanitizeLength(array &$propertyData, bool $throwOnData = false): void { + if (isset($propertyData['value']) && strlen($propertyData['value']) > 2048) { + if ($throwOnData) { + throw new \InvalidArgumentException(); + } else { + $propertyData['value'] = ''; + } + } + } + + protected function testValueLengths(array &$data, bool $throwOnData = false): void { + try { + foreach ($data as $propertyName => &$propertyData) { + if ($this->isCollection($propertyName)) { + $this->testValueLengths($propertyData, $throwOnData); + } else { + $this->sanitizeLength($propertyData, $throwOnData); + } + } + } catch (\InvalidArgumentException $e) { + throw new \InvalidArgumentException($propertyName); + } + } + + protected function testPropertyScopes(array &$data, array $allowedScopes, bool $throwOnData = false, string $parentPropertyName = null): void { + foreach ($data as $propertyNameOrIndex => &$propertyData) { + if ($this->isCollection($propertyNameOrIndex)) { + $this->testPropertyScopes($propertyData, $allowedScopes, $throwOnData); + } elseif (isset($propertyData['scope'])) { + $effectivePropertyName = $parentPropertyName ?? $propertyNameOrIndex; + + if ($throwOnData && !in_array($propertyData['scope'], $allowedScopes, true)) { + throw new \InvalidArgumentException('scope'); + } + + if ( + $propertyData['scope'] === self::SCOPE_PRIVATE + && ($effectivePropertyName === self::PROPERTY_DISPLAYNAME || $effectivePropertyName === self::PROPERTY_EMAIL) + ) { + if ($throwOnData) { + // v2-private is not available for these fields + throw new \InvalidArgumentException('scope'); + } else { + // default to local + $data[$propertyNameOrIndex]['scope'] = self::SCOPE_LOCAL; + } + } else { + // migrate scope values to the new format + // invalid scopes are mapped to a default value + $data[$propertyNameOrIndex]['scope'] = AccountProperty::mapScopeToV2($propertyData['scope']); + } + } + } + } + /** * update user record * @@ -168,16 +225,7 @@ class AccountManager implements IAccountManager { } } - // set a max length - foreach ($data as $propertyName => $propertyData) { - if (isset($data[$propertyName]) && isset($data[$propertyName]['value']) && strlen($data[$propertyName]['value']) > 2048) { - if ($throwOnData) { - throw new \InvalidArgumentException($propertyName); - } else { - $data[$propertyName]['value'] = ''; - } - } - } + $this->testValueLengths($data); if (isset($data[self::PROPERTY_WEBSITE]) && $data[self::PROPERTY_WEBSITE]['value'] !== '') { try { @@ -200,31 +248,7 @@ class AccountManager implements IAccountManager { self::VISIBILITY_PUBLIC, ]; - // validate and convert scope values - foreach ($data as $propertyName => $propertyData) { - if (isset($propertyData['scope'])) { - if ($throwOnData && !in_array($propertyData['scope'], $allowedScopes, true)) { - throw new \InvalidArgumentException('scope'); - } - - if ( - $propertyData['scope'] === self::SCOPE_PRIVATE - && ($propertyName === self::PROPERTY_DISPLAYNAME || $propertyName === self::PROPERTY_EMAIL) - ) { - if ($throwOnData) { - // v2-private is not available for these fields - throw new \InvalidArgumentException('scope'); - } else { - // default to local - $data[$propertyName]['scope'] = self::SCOPE_LOCAL; - } - } else { - // migrate scope values to the new format - // invalid scopes are mapped to a default value - $data[$propertyName]['scope'] = AccountProperty::mapScopeToV2($propertyData['scope']); - } - } - } + $this->testPropertyScopes($data, $allowedScopes, $throwOnData); if (empty($userData)) { $this->insertNewUser($user, $data); @@ -278,12 +302,9 @@ class AccountManager implements IAccountManager { /** * get stored data from a given user * - * @param IUser $user - * @return array - * * @deprecated use getAccount instead to make sure migrated properties work correctly */ - public function getUser(IUser $user) { + public function getUser(IUser $user, bool $insertIfNotExists = true): array { $uid = $user->getUID(); $query = $this->connection->getQueryBuilder(); $query->select('data') @@ -296,7 +317,9 @@ class AccountManager implements IAccountManager { if (empty($accountData)) { $userData = $this->buildDefaultUserRecord($user); - $this->insertNewUser($user, $userData); + if ($insertIfNotExists) { + $this->insertNewUser($user, $userData); + } return $userData; } @@ -307,9 +330,7 @@ class AccountManager implements IAccountManager { return $this->buildDefaultUserRecord($user); } - $userDataArray = $this->addMissingDefaultValues($userDataArray); - - return $userDataArray; + return $this->addMissingDefaultValues($userDataArray); } public function searchUsers(string $property, array $values): array { @@ -326,12 +347,23 @@ class AccountManager implements IAccountManager { $result = $query->execute(); while ($row = $result->fetch()) { - $matches[$row['value']] = $row['uid']; + $matches[$row['uid']] = $row['value']; } $result->closeCursor(); } - return $matches; + $result = array_merge($matches, $this->searchUsersForRelatedCollection($property, $values)); + + return array_flip($result); + } + + 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 []; + } } /** @@ -342,7 +374,7 @@ class AccountManager implements IAccountManager { * @param IUser $user * @return array */ - protected function checkEmailVerification($oldData, $newData, IUser $user) { + protected function checkEmailVerification($oldData, $newData, IUser $user): array { if ($oldData[self::PROPERTY_EMAIL]['value'] !== $newData[self::PROPERTY_EMAIL]['value']) { $this->jobList->add(VerifyUserData::class, [ @@ -383,7 +415,7 @@ class AccountManager implements IAccountManager { * @param array $newData * @return array */ - protected function updateVerifyStatus($oldData, $newData) { + protected function updateVerifyStatus(array $oldData, array $newData): array { // which account was already verified successfully? $twitterVerified = isset($oldData[self::PROPERTY_TWITTER]['verified']) && $oldData[self::PROPERTY_TWITTER]['verified'] === self::VERIFIED; @@ -483,12 +515,20 @@ class AccountManager implements IAccountManager { 'value' => $query->createParameter('value'), ] ); + $this->writeUserDataProperties($query, $data); + } + + protected function writeUserDataProperties(IQueryBuilder $query, array $data, string $parentPropertyName = null): void { foreach ($data as $propertyName => $property) { - if ($propertyName === self::PROPERTY_AVATAR) { + if ($this->isCollection($propertyName)) { + $this->writeUserDataProperties($query, $property, $propertyName); + continue; + } + if (($parentPropertyName ?? $propertyName) === self::PROPERTY_AVATAR) { continue; } - $query->setParameter('name', $propertyName) + $query->setParameter('name', $parentPropertyName ?? $propertyName) ->setParameter('value', $property['value'] ?? ''); $query->execute(); } diff --git a/lib/private/Accounts/AccountPropertyCollection.php b/lib/private/Accounts/AccountPropertyCollection.php new file mode 100644 index 00000000000..84e10e6a507 --- /dev/null +++ b/lib/private/Accounts/AccountPropertyCollection.php @@ -0,0 +1,90 @@ +<?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/>. + * + */ + +namespace OC\Accounts; + +use InvalidArgumentException; +use OCP\Accounts\IAccountProperty; +use OCP\Accounts\IAccountPropertyCollection; + +class AccountPropertyCollection implements IAccountPropertyCollection { + + /** @var string */ + protected $collectionName = ''; + + /** @var IAccountProperty[] */ + protected $properties = []; + + public function __construct(string $collectionName) { + $this->collectionName = $collectionName; + } + + public function setProperties(array $properties): IAccountPropertyCollection { + /** @var IAccountProperty $property */ + $this->properties = []; + foreach ($properties as $property) { + $this->addProperty($property); + } + return $this; + } + + public function getProperties(): array { + return $this->properties; + } + + public function addProperty(IAccountProperty $property): IAccountPropertyCollection { + if ($property->getName() !== $this->collectionName) { + throw new InvalidArgumentException('Provided property does not match collection name'); + } + $this->properties[] = $property; + return $this; + } + + public function removeProperty(IAccountProperty $property): IAccountPropertyCollection { + $ref = array_search($property, $this->properties, true); + if ($ref !== false) { + unset($this->properties[$ref]); + } + return $this; + } + + public function removePropertyByValue(string $value): IAccountPropertyCollection { + foreach ($this->properties as $i => $property) { + if ($property->getValue() === $value) { + unset($this->properties[$i]); + } + } + return $this; + } + + public function jsonSerialize() { + return [$this->collectionName => $this->properties]; + } + + public function getName(): string { + return $this->collectionName; + } +} diff --git a/lib/private/Accounts/TAccountsHelper.php b/lib/private/Accounts/TAccountsHelper.php new file mode 100644 index 00000000000..530204b451f --- /dev/null +++ b/lib/private/Accounts/TAccountsHelper.php @@ -0,0 +1,40 @@ +<?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/>. + * + */ + +namespace OC\Accounts; + +use OCP\Accounts\IAccountManager; + +trait TAccountsHelper { + protected function isCollection(string $propertyName): bool { + return in_array($propertyName, + [ + IAccountManager::COLLECTION_EMAIL, + ], + true + ); + } +} |