diff --git a/src/DI.php b/src/DI.php index e64c5cef2d..562532f5c1 100644 --- a/src/DI.php +++ b/src/DI.php @@ -465,6 +465,11 @@ abstract class DI return self::$dice->create(Profile\ProfileField\Depository\ProfileField::class); } + public static function profileFieldFactory(): Profile\ProfileField\Factory\ProfileField + { + return self::$dice->create(Profile\ProfileField\Factory\ProfileField::class); + } + public static function notification(): Navigation\Notifications\Depository\Notification { return self::$dice->create(Navigation\Notifications\Depository\Notification::class); diff --git a/src/Module/Settings/Profile/Index.php b/src/Module/Settings/Profile/Index.php index 1ab3e32fc2..28fe695ba3 100644 --- a/src/Module/Settings/Profile/Index.php +++ b/src/Module/Settings/Profile/Index.php @@ -30,6 +30,7 @@ use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\Profile; +use Friendica\Profile\ProfileField\Collection\ProfileFields; use Friendica\Profile\ProfileField\Entity\ProfileField; use Friendica\Model\User; use Friendica\Module\BaseSettings; @@ -100,16 +101,13 @@ class Index extends BaseSettings $homepage = 'http://' . $homepage; } - $profileFields = DI::profileFieldNew()->selectByUserId(local_user()); - - $profileFields = DI::profileField()->updateCollectionFromForm( + $profileFieldsNew = self::getProfileFieldsFromInput( local_user(), - $profileFields, $_REQUEST['profile_field'], $_REQUEST['profile_field_order'] ); - DI::profileField()->saveCollection($profileFields); + DI::profileFieldNew()->saveCollectionForUser(local_user(), $profileFieldsNew); $result = Profile::update( [ @@ -265,6 +263,53 @@ class Index extends BaseSettings return $o; } + private static function getProfileFieldsFromInput(int $uid, array $profileFieldInputs, array $profileFieldOrder): ProfileFields + { + $profileFields = new ProfileFields(); + + // Returns an associative array of id => order values + $profileFieldOrder = array_flip($profileFieldOrder); + + // Creation of the new field + if (!empty($profileFieldInputs['new']['label'])) { + $permissionSet = DI::permissionSet()->selectOrCreate(DI::permissionSetFactory()->createFromString( + $uid, + $profileFieldInputs['new']['contact_allow'] ?? '', + $profileFieldInputs['new']['group_allow'] ?? '', + $profileFieldInputs['new']['contact_deny'] ?? '', + $profileFieldInputs['new']['group_deny'] ?? '' + )); + + $profileFields->append(DI::profileFieldFactory()->createFromString( + $uid, + $profileFieldOrder['new'], + $profileFieldInputs['new']['label'], + $profileFieldInputs['new']['value'], + $permissionSet + )); + } + + foreach ($profileFieldInputs as $id => $profileFieldInput) { + $permissionSet = DI::permissionSet()->selectOrCreate(DI::permissionSetFactory()->createFromString( + $uid, + $profileFieldInput['contact_allow'] ?? '', + $profileFieldInput['group_allow'] ?? '', + $profileFieldInput['contact_deny'] ?? '', + $profileFieldInput['group_deny'] ?? '' + )); + + $profileFields->append(DI::profileFieldFactory()->createFromString( + $uid, + $profileFieldOrder[$id], + $profileFieldInput['label'], + $profileFieldInput['value'], + $permissionSet + )); + } + + return $profileFields; + } + private static function cleanKeywords($keywords) { $keywords = str_replace(',', ' ', $keywords); diff --git a/src/Profile/ProfileField/Collection/ProfileFields.php b/src/Profile/ProfileField/Collection/ProfileFields.php index b235ad2ecd..b954e3ba37 100644 --- a/src/Profile/ProfileField/Collection/ProfileFields.php +++ b/src/Profile/ProfileField/Collection/ProfileFields.php @@ -22,14 +22,20 @@ namespace Friendica\Profile\ProfileField\Collection; use Friendica\BaseCollection; +use Friendica\Profile\ProfileField\Entity; class ProfileFields extends BaseCollection { + public function current(): Entity\ProfileField + { + return parent::current(); + } + /** * @param callable $callback * @return ProfileFields */ - public function map(callable $callback) + public function map(callable $callback): self { return parent::map($callback); } @@ -39,7 +45,7 @@ class ProfileFields extends BaseCollection * @param int $flag * @return ProfileFields */ - public function filter(callable $callback = null, int $flag = 0) + public function filter(callable $callback = null, int $flag = 0): self { return parent::filter($callback, $flag); } diff --git a/src/Profile/ProfileField/Depository/ProfileField.php b/src/Profile/ProfileField/Depository/ProfileField.php index 9ec2d4fb45..8ae74fefa1 100644 --- a/src/Profile/ProfileField/Depository/ProfileField.php +++ b/src/Profile/ProfileField/Depository/ProfileField.php @@ -5,6 +5,8 @@ namespace Friendica\Profile\ProfileField\Depository; use Friendica\BaseDepository; use Friendica\Database\Database; use Friendica\Network\HTTPException\NotFoundException; +use Friendica\Profile\ProfileField\Exception\ProfileFieldNotFoundException; +use Friendica\Profile\ProfileField\Exception\ProfileFieldPersistenceException; use Friendica\Profile\ProfileField\Factory; use Friendica\Profile\ProfileField\Entity; use Friendica\Profile\ProfileField\Collection; @@ -25,23 +27,57 @@ class ProfileField extends BaseDepository { parent::__construct($database, $logger, $factory); - $this->permissionSetDepository = $this->permissionSetDepository; + $this->permissionSetDepository = permissionSetDepository; } /** * @param array $condition * @param array $params * @return Entity\ProfileField - * @throws NotFoundException + * @throws ProfileFieldNotFoundException */ private function selectOne(array $condition, array $params = []): Entity\ProfileField { - return parent::_selectOne($condition, $params); + try { + return parent::_selectOne($condition, $params); + } catch (NotFoundException $exception) { + throw new ProfileFieldNotFoundException($exception->getMessage()); + } } + /** + * @param array $condition + * @param array $params + * + * @return Collection\ProfileFields + * + * @throws ProfileFieldPersistenceException In case of underlying persistence exceptions + */ private function select(array $condition, array $params = []): Collection\ProfileFields { - return new Collection\ProfileFields(parent::_select($condition, $params)->getArrayCopy()); + try { + return new Collection\ProfileFields(parent::_select($condition, $params)->getArrayCopy()); + } catch (\Exception $exception) { + throw new ProfileFieldPersistenceException('Cannot select ProfileFields', $exception); + } + } + + /** + * Converts a given ProfileField into a DB compatible row array + * + * @param Entity\ProfileField $profileField + * + * @return array + */ + protected function convertToTableRow(Entity\ProfileField $profileField): array + { + return [ + 'label' => $profileField->label, + 'value' => $profileField->value, + 'order' => $profileField->order, + 'created' => $profileField->created, + 'edited' => $profileField->edited, + ]; } /** @@ -50,26 +86,36 @@ class ProfileField extends BaseDepository * @param int $uid the user id * * @return Collection\ProfileFields + * + * @throws ProfileFieldPersistenceException In case of underlying persistence exceptions */ public function selectPublicFieldsByUserId(int $uid): Collection\ProfileFields { - return $this->select([ - 'uid' => $uid, - 'psid' => PermissionSetDepository::PUBLIC, - ]); + try { + return $this->select([ + 'uid' => $uid, + 'psid' => PermissionSetDepository::PUBLIC, + ]); + } catch (\Exception $exception) { + throw new ProfileFieldPersistenceException(sprintf('Cannot select public ProfileField for user "%d"', $uid), $exception); + } } /** * @param int $uid Field owner user Id * - * @throws \Exception + * @throws ProfileFieldPersistenceException In case of underlying persistence exceptions */ public function selectByUserId(int $uid): Collection\ProfileFields { - return $this->select( - ['uid' => $uid], - ['order' => ['order']] - ); + try { + return $this->select( + ['uid' => $uid], + ['order' => ['order']] + ); + } catch (\Exception $exception) { + throw new ProfileFieldPersistenceException(sprintf('Cannot select ProfileField for user "%d"', $uid), $exception); + } } /** @@ -94,4 +140,103 @@ class ProfileField extends BaseDepository ['order' => ['order']] ); } + + /** + * @param int $id + * + * @return Entity\ProfileField + * + * @ProfileFieldNotFoundException In case there is no ProfileField found + */ + public function selectOnyById(int $id): Entity\ProfileField + { + try { + return $this->selectOne(['id' => $id]); + } catch (\Exception $exception) { + throw new ProfileFieldNotFoundException(sprintf('Cannot find Profile "%s"', $id), $exception); + } + } + + /** + * Delets a whole collection of ProfileFields + * + * @param Collection\ProfileFields $profileFields + * + * @return bool + * @throws ProfileFieldPersistenceException in case the persistence layer cannot delete the ProfileFields + */ + public function deleteCollection(Collection\ProfileFields $profileFields): bool + { + try { + return $this->db->delete(self::$table_name, ['id' => $profileFields->column('id')]); + } catch (\Exception $exception) { + throw new ProfileFieldPersistenceException('Cannot delete ProfileFields', $exception); + } + } + + /** + * @param Entity\ProfileField $profileField + * + * @return Entity\ProfileField + * @throws ProfileFieldPersistenceException in case the persistence layer cannot save the ProfileField + */ + public function save(Entity\ProfileField $profileField): Entity\ProfileField + { + $fields = $this->convertToTableRow($profileField); + + try { + if ($profileField->id) { + $this->db->update(self::$table_name, $fields, ['id' => $profileField]); + } else { + $this->db->insert(self::$table_name, $fields); + + $profileField = $this->selectOnyById($this->db->lastInsertId()); + } + } catch (\Exception $exception) { + throw new ProfileFieldPersistenceException(sprintf('Cannot save ProfileField with id "%d" and label "%s"', $profileField->id, $profileField->label), $exception); + } + + return $profileField; + } + + public function saveCollectionForUser(int $uid, Collection\ProfileFields $profileFields): Collection\ProfileFields + { + $savedProfileFields = new Collection\ProfileFields(); + + $profileFieldsOld = $this->selectByUserId($uid); + + // Prunes profile field whose label has been emptied + $labels = $profileFields->column('label'); + $prunedProfileFieldsOld = $profileFieldsOld->filter(function (Entity\ProfileField $profileFieldOld) use ($labels) { + return array_search($profileFieldOld->label, $labels) === false; + }); + $this->deleteCollection($prunedProfileFieldsOld); + + // Update the order based on the new Profile Field Collection + $order = 0; + $labelProfileFieldsOld = $profileFieldsOld->column('id', 'label'); + + foreach ($profileFields as $profileField) { + // Update existing field (preserve + if (array_key_exists($profileField->label, $labelProfileFieldsOld)) { + $profileFieldOldId = $labelProfileFieldsOld[$profileField->label]; + /** @var Entity\ProfileField $foundProfileFieldOld */ + $foundProfileFieldOld = $profileFieldsOld[$profileFieldOldId]; + $foundProfileFieldOld->update( + $profileField->value, + $order, + $profileField->permissionSet + ); + + $savedProfileFields->append($this->save($foundProfileFieldOld)); + } else { + $profileField->setOrder($order); + $savedProfileFields->append($this->save($profileField)); + } + + $order++; + } + + return $savedProfileFields; + } } diff --git a/src/Profile/ProfileField/Entity/ProfileField.php b/src/Profile/ProfileField/Entity/ProfileField.php index ad122344f1..deab955d3c 100644 --- a/src/Profile/ProfileField/Entity/ProfileField.php +++ b/src/Profile/ProfileField/Entity/ProfileField.php @@ -22,6 +22,9 @@ namespace Friendica\Profile\ProfileField\Entity; use Friendica\BaseEntity; +use Friendica\Network\HTTPException\InternalServerErrorException; +use Friendica\Network\HTTPException\NotFoundException; +use Friendica\Profile\ProfileField\Exception\ProfileFieldNotFoundException; use Friendica\Profile\ProfileField\Exception\UnexpectedPermissionSetException; use Friendica\Security\PermissionSet\Depository\PermissionSet as PermissionSetDepository; use Friendica\Security\PermissionSet\Entity\PermissionSet; @@ -65,9 +68,10 @@ class ProfileField extends BaseEntity /** @var \DateTime */ protected $edited; - public function __construct(PermissionSetDepository $permissionSetDepository, int $uid, int $order, int $permissionSetId, string $label, string $value, \DateTime $created, \DateTime $edited, int $id = null) + public function __construct(PermissionSetDepository $permissionSetDepository, int $uid, int $order, int $permissionSetId, string $label, string $value, \DateTime $created, \DateTime $edited, int $id = null, PermissionSet $permissionSet = null) { $this->permissionSetDepository = $permissionSetDepository; + $this->permissionSet = $permissionSet; $this->uid = $uid; $this->order = $order; @@ -79,26 +83,65 @@ class ProfileField extends BaseEntity $this->id = $id; } + /** + * @throws ProfileFieldNotFoundException + * @throws UnexpectedPermissionSetException + */ public function __get($name) { switch ($name) { case 'permissionSet': if (empty($this->permissionSet)) { - $permissionSet = $this->permissionSetDepository->selectOneById($this->psid, $this->uid); - if ($permissionSet->uid !== $this->uid) { - throw new UnexpectedPermissionSetException(sprintf('PermissionSet %d (user-id: %d) for ProfileField %d (user-id: %d) is invalid.', $permissionSet->id, $permissionSet->uid, $this->id, $this->uid)); - } + try { + $permissionSet = $this->permissionSetDepository->selectOneById($this->psid, $this->uid); + if ($permissionSet->uid !== $this->uid) { + throw new UnexpectedPermissionSetException(sprintf('PermissionSet %d (user-id: %d) for ProfileField %d (user-id: %d) is invalid.', $permissionSet->id, $permissionSet->uid, $this->id, $this->uid)); + } - $this->permissionSet = $permissionSet; + $this->permissionSet = $permissionSet; + } catch (NotFoundException $exception) { + throw new UnexpectedPermissionSetException(sprintf('No PermissionSet found for ProfileField %d (user-id: %d).', $this->id, $this->uid)); + } } $return = $this->permissionSet; break; default: - $return = parent::__get($name); + try { + $return = parent::__get($name); + } catch (InternalServerErrorException $exception) { + throw new ProfileFieldNotFoundException($exception->getMessage()); + } break; } return $return; } + + /** + * Updates a ProfileField + * + * @param string $value The current or changed value + * @param int $order The current or changed order + * @param PermissionSet $permissionSet The current or changed PermissionSet + */ + public function update(string $value, int $order, PermissionSet $permissionSet) + { + $this->value = $value; + $this->order = $order; + $this->permissionSet = $permissionSet; + $this->psid = $permissionSet->id; + $this->edited = new \DateTime('now', new \DateTimeZone('UTC')); + } + + /** + * Sets the order of the ProfileField + * + * @param int $order + */ + public function setOrder(int $order) + { + $this->order = $order; + $this->edited = new \DateTime('now', new \DateTimeZone('UTC')); + } } diff --git a/src/Profile/ProfileField/Exception/ProfileFieldNotFoundException.php b/src/Profile/ProfileField/Exception/ProfileFieldNotFoundException.php new file mode 100644 index 0000000000..b204d9e545 --- /dev/null +++ b/src/Profile/ProfileField/Exception/ProfileFieldNotFoundException.php @@ -0,0 +1,14 @@ +permissionSetDepository, @@ -32,8 +33,26 @@ class ProfileField extends BaseFactory implements ICanCreateFromTableRow $row['psid'], $row['label'], $row['value'], - new \DateTime($row['created'], new \DateTimeZone('UTC')), - new \DateTime($row['edited'] ?? 'now', new \DateTimeZone('UTC')) + new \DateTime($row['created'] ?? 'now', new \DateTimeZone('UTC')), + new \DateTime($row['edited'] ?? 'now', new \DateTimeZone('UTC')), + $row['id'], + $permissionSet ); } + + public function createFromString( + int $uid, + int $order, + string $label, + string $value, + PermissionSet $permissionSet + ): Entity\ProfileField { + return $this->createFromTableRow([ + 'uid' => $uid, + 'order' => $order, + 'psid' => $permissionSet->id, + 'label' => $label, + 'value' => $value, + ], $permissionSet); + } } diff --git a/src/Repository/ProfileField.php b/src/Repository/ProfileField.php index c7fe74ea1a..3bb4a0b44b 100644 --- a/src/Repository/ProfileField.php +++ b/src/Repository/ProfileField.php @@ -21,7 +21,6 @@ namespace Friendica\Repository; -use Friendica\BaseModel; use Friendica\BaseRepository; use Friendica\Core\L10n; use Friendica\Database\Database; @@ -55,56 +54,6 @@ class ProfileField extends BaseRepository $this->l10n = $l10n; } - /** - * @param array $data - * - * @return \Friendica\Profile\ProfileField\Entity\ProfileField - */ - protected function create(array $data) - { - return new Model\ProfileField($this->dba, $this->logger, $this->permissionSet, $data); - } - - /** - * @param array $condition - * - * @return \Friendica\Profile\ProfileField\Entity\ProfileField - * @throws \Friendica\Network\HTTPException\NotFoundException - */ - public function selectFirst(array $condition) - { - return parent::selectFirst($condition); - } - - /** - * @param array $condition - * @param array $params - * - * @return \Friendica\Profile\ProfileField\Collection\ProfileFields - * @throws \Exception - */ - public function select(array $condition = [], array $params = []) - { - return parent::select($condition, $params); - } - - /** - * @param array $condition - * @param array $params - * @param int|null $min_id - * @param int|null $max_id - * @param int $limit - * - * @return \Friendica\Profile\ProfileField\Collection\ProfileFields - * @throws \Exception - */ - public function selectByBoundaries(array $condition = [], array $params = [], int $min_id = null, int $max_id = null, int $limit = self::LIMIT) - { - return parent::selectByBoundaries($condition, $params, $min_id, $max_id, $limit); - } - - - /** * @param array $fields * @@ -119,102 +68,6 @@ class ProfileField extends BaseRepository return parent::insert($fields); } - /** - * @param \Friendica\Profile\ProfileField\Entity\ProfileField $model - * - * @return bool - * @throws \Exception - */ - public function update(BaseModel $model) - { - $model->edited = DateTimeFormat::utcNow(); - - return parent::update($model); - } - - /** - * @param int $uid User Id - * @param \Friendica\Profile\ProfileField\Collection\ProfileFields $profileFields Collection of existing profile fields - * @param array $profileFieldInputs Array of profile field form inputs indexed by profile field id - * @param array $profileFieldOrder List of profile field id in order - * - * @return \Friendica\Profile\ProfileField\Collection\ProfileFields - * @throws \Exception - */ - public function updateCollectionFromForm(int $uid, \Friendica\Profile\ProfileField\Collection\ProfileFields $profileFields, array $profileFieldInputs, array $profileFieldOrder) - { - // Returns an associative array of id => order values - $profileFieldOrder = array_flip($profileFieldOrder); - - // Creation of the new field - if (!empty($profileFieldInputs['new']['label'])) { - $psid = $this->permissionSet->selectOrCreate($this->permissionSetFactory->createFromString( - $uid, - $profileFieldInputs['new']['contact_allow'] ?? '', - $profileFieldInputs['new']['group_allow'] ?? '', - $profileFieldInputs['new']['contact_deny'] ?? '', - $profileFieldInputs['new']['group_deny'] ?? '' - ))->id; - - $newProfileField = $this->insert([ - 'uid' => $uid, - 'label' => $profileFieldInputs['new']['label'], - 'value' => $profileFieldInputs['new']['value'], - 'psid' => $psid, - 'order' => $profileFieldOrder['new'], - ]); - - $profileFieldInputs[$newProfileField->id] = $profileFieldInputs['new']; - $profileFieldOrder[$newProfileField->id] = $profileFieldOrder['new']; - - $profileFields[] = $newProfileField; - } - - unset($profileFieldInputs['new']); - unset($profileFieldOrder['new']); - - // Prunes profile field whose label has been emptied - $profileFields = $profileFields->filter(function (\Friendica\Profile\ProfileField\Entity\ProfileField $profileField) use (&$profileFieldInputs, &$profileFieldOrder) { - $keepModel = !isset($profileFieldInputs[$profileField->id]) || !empty($profileFieldInputs[$profileField->id]['label']); - - if (!$keepModel) { - unset($profileFieldInputs[$profileField->id]); - unset($profileFieldOrder[$profileField->id]); - $this->delete($profileField); - } - - return $keepModel; - }); - - // Regenerates the order values if items were deleted - $profileFieldOrder = array_flip(array_keys($profileFieldOrder)); - - // Update existing profile fields from form values - $profileFields = $profileFields->map(function (\Friendica\Profile\ProfileField\Entity\ProfileField $profileField) use ($uid, &$profileFieldInputs, &$profileFieldOrder) { - if (isset($profileFieldInputs[$profileField->id]) && isset($profileFieldOrder[$profileField->id])) { - $psid = $this->permissionSet->selectOrCreate($this->permissionSetFactory->createFromString( - $uid, - $profileFieldInputs[$profileField->id]['contact_allow'] ?? '', - $profileFieldInputs[$profileField->id]['group_allow'] ?? '', - $profileFieldInputs[$profileField->id]['contact_deny'] ?? '', - $profileFieldInputs[$profileField->id]['group_deny'] ?? '' - ))->id; - - $profileField->permissionSetId = $psid; - $profileField->label = $profileFieldInputs[$profileField->id]['label']; - $profileField->value = $profileFieldInputs[$profileField->id]['value']; - $profileField->order = $profileFieldOrder[$profileField->id]; - - unset($profileFieldInputs[$profileField->id]); - unset($profileFieldOrder[$profileField->id]); - } - - return $profileField; - }); - - return $profileFields; - } - /** * Migrates a legacy profile to the new slimmer profile with extra custom fields. * Multi profiles are converted to ACl-protected custom fields and deleted.