summaryrefslogtreecommitdiffstats
path: root/lib/private/Accounts
diff options
context:
space:
mode:
authorblizzz <blizzz@arthur-schiwon.de>2021-06-08 14:25:18 +0200
committerGitHub <noreply@github.com>2021-06-08 14:25:18 +0200
commit662ab937e0d30947727be1462f8744681fdd2e49 (patch)
tree3b857453adb11fb00d23d68515b85f6f84cfad6c /lib/private/Accounts
parentb3cfa1859b14384ae8134e8eb88c171667f77799 (diff)
parentff2382e5a4a5c29e3e1c948a514c151cae71d402 (diff)
downloadnextcloud-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.php62
-rw-r--r--lib/private/Accounts/AccountManager.php138
-rw-r--r--lib/private/Accounts/AccountPropertyCollection.php90
-rw-r--r--lib/private/Accounts/TAccountsHelper.php40
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
+ );
+ }
+}