diff --git a/database.sql b/database.sql index 793aae3863..933502ef8e 100644 --- a/database.sql +++ b/database.sql @@ -1,6 +1,6 @@ -- ------------------------------------------ -- Friendica 2021.12-dev (Siberian Iris) --- DB_UPDATE_VERSION 1439 +-- DB_UPDATE_VERSION 1440 -- ------------------------------------------ @@ -2587,3 +2587,23 @@ CREATE VIEW `workerqueue-view` AS SELECT FROM `process` INNER JOIN `workerqueue` ON `workerqueue`.`pid` = `process`.`pid` WHERE NOT `workerqueue`.`done`; + +-- +-- VIEW profile_field-view +-- +DROP VIEW IF EXISTS `profile_field-view`; +CREATE VIEW `profile_field-view` AS SELECT + `profile_field`.`id` AS `id`, + `profile_field`.`uid` AS `uid`, + `profile_field`.`label` AS `label`, + `profile_field`.`value` AS `value`, + `profile_field`.`order` AS `order`, + `profile_field`.`psid` AS `psid`, + `permissionset`.`allow_cid` AS `allow_cid`, + `permissionset`.`allow_gid` AS `allow_gid`, + `permissionset`.`deny_cid` AS `deny_cid`, + `permissionset`.`deny_gid` AS `deny_gid`, + `profile_field`.`created` AS `created`, + `profile_field`.`edited` AS `edited` + FROM `profile_field` + INNER JOIN `permissionset` ON `permissionset`.`id` = `profile_field`.`psid`; diff --git a/src/BaseCollection.php b/src/BaseCollection.php index 1aa13ae961..d2212e244b 100644 --- a/src/BaseCollection.php +++ b/src/BaseCollection.php @@ -87,7 +87,7 @@ class BaseCollection extends \ArrayIterator */ public function column($column, $index_key = null) { - return array_column($this->getArrayCopy(), $column, $index_key); + return array_column($this->getArrayCopy(true), $column, $index_key); } /** @@ -124,4 +124,21 @@ class BaseCollection extends \ArrayIterator { return new static(array_reverse($this->getArrayCopy()), $this->getTotalCount()); } + + /** + * @inheritDoc + * + * includes recursion for entity::toArray() function + * @see BaseEntity::toArray() + */ + public function getArrayCopy(bool $recursive = false): array + { + if (!$recursive) { + return parent::getArrayCopy(); + } + + return array_map(function ($item) { + return is_object($item) ? $item->toArray() : $item; + }, parent::getArrayCopy()); + } } diff --git a/src/BaseEntity.php b/src/BaseEntity.php index a5b968397c..2ee22b1bb0 100644 --- a/src/BaseEntity.php +++ b/src/BaseEntity.php @@ -53,4 +53,18 @@ abstract class BaseEntity extends BaseDataTransferObject return $this->$name; } + + /** + * @param $name + * @return bool + * @throws HTTPException\InternalServerErrorException + */ + public function __isset($name) + { + if (!property_exists($this, $name)) { + throw new HTTPException\InternalServerErrorException('Unknown property ' . $name . ' in Entity ' . static::class); + } + + return !empty($this->$name); + } } diff --git a/src/Core/UserImport.php b/src/Core/UserImport.php index c0725bee8a..4ac59d14cb 100644 --- a/src/Core/UserImport.php +++ b/src/Core/UserImport.php @@ -25,6 +25,7 @@ use Friendica\Database\DBA; use Friendica\Database\DBStructure; use Friendica\DI; use Friendica\Model\Photo; +use Friendica\Model\Profile; use Friendica\Object\Image; use Friendica\Security\PermissionSet\Depository\PermissionSet; use Friendica\Util\Strings; @@ -278,7 +279,7 @@ class UserImport $profile['id'] = DBA::lastInsertId(); } - DI::profileField()->migrateFromLegacyProfile($profile); + Profile::migrate($profile); } $permissionSet = DI::permissionSet()->selectDefaultForUser($newuid); diff --git a/src/DI.php b/src/DI.php index 9025feb470..31542e3d52 100644 --- a/src/DI.php +++ b/src/DI.php @@ -452,12 +452,14 @@ abstract class DI return self::$dice->create(Security\PermissionSet\Factory\PermissionSet::class); } - /** - * @return Repository\ProfileField - */ - public static function profileField() + public static function profileField(): Profile\ProfileField\Depository\ProfileField { - return self::$dice->create(Repository\ProfileField::class); + 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 diff --git a/src/Factory/Api/Mastodon/Account.php b/src/Factory/Api/Mastodon/Account.php index 1d067352a6..a987e6874c 100644 --- a/src/Factory/Api/Mastodon/Account.php +++ b/src/Factory/Api/Mastodon/Account.php @@ -27,8 +27,7 @@ use Friendica\Collection\Api\Mastodon\Fields; use Friendica\Model\APContact; use Friendica\Model\Contact; use Friendica\Network\HTTPException; -use Friendica\Repository\ProfileField; -use Friendica\Security\PermissionSet\Depository\PermissionSet; +use Friendica\Profile\ProfileField\Depository\ProfileField as ProfileFieldDepository; use ImagickException; use Psr\Log\LoggerInterface; @@ -36,17 +35,17 @@ class Account extends BaseFactory { /** @var BaseURL */ private $baseUrl; - /** @var ProfileField */ - private $profileFieldRepo; + /** @var ProfileFieldDepository */ + private $profileFieldDepo; /** @var Field */ private $mstdnFieldFactory; - public function __construct(LoggerInterface $logger, BaseURL $baseURL, ProfileField $profileFieldRepo, Field $mstdnFieldFactory) + public function __construct(LoggerInterface $logger, BaseURL $baseURL, ProfileFieldDepository $profileFieldDepo, Field $mstdnFieldFactory) { parent::__construct($logger); $this->baseUrl = $baseURL; - $this->profileFieldRepo = $profileFieldRepo; + $this->profileFieldDepo = $profileFieldDepo; $this->mstdnFieldFactory = $mstdnFieldFactory; } @@ -77,7 +76,7 @@ class Account extends BaseFactory $self_contact = Contact::selectFirst(['uid'], ['nurl' => $publicContact['nurl'], 'self' => true]); if (!empty($self_contact['uid'])) { - $profileFields = $this->profileFieldRepo->select(['uid' => $self_contact['uid'], 'psid' => PermissionSet::PUBLIC]); + $profileFields = $this->profileFieldDepo->selectPublicFieldsByUserId($self_contact['uid']); $fields = $this->mstdnFieldFactory->createFromProfileFields($profileFields); } else { $fields = new Fields(); @@ -95,7 +94,7 @@ class Account extends BaseFactory { $publicContact = Contact::selectFirst([], ['uid' => $userId, 'self' => true]); - $profileFields = $this->profileFieldRepo->select(['uid' => $userId, 'psid' => PermissionSet::PUBLIC]); + $profileFields = $this->profileFieldDepo->selectPublicFieldsByUserId($userId); $fields = $this->mstdnFieldFactory->createFromProfileFields($profileFields); $apContact = APContact::getByURL($publicContact['url'], false); diff --git a/src/Factory/Api/Mastodon/Field.php b/src/Factory/Api/Mastodon/Field.php index 6ff63fb1bf..397ca57adb 100644 --- a/src/Factory/Api/Mastodon/Field.php +++ b/src/Factory/Api/Mastodon/Field.php @@ -23,15 +23,16 @@ namespace Friendica\Factory\Api\Mastodon; use Friendica\BaseFactory; use Friendica\Collection\Api\Mastodon\Fields; -use Friendica\Collection\ProfileFields; +use Friendica\Profile\ProfileField\Collection\ProfileFields; use Friendica\Content\Text\BBCode; -use Friendica\Model\ProfileField; +use Friendica\Profile\ProfileField\Entity\ProfileField; use Friendica\Network\HTTPException; class Field extends BaseFactory { /** * @param ProfileField $profileField + * * @return \Friendica\Object\Api\Mastodon\Field * @throws HTTPException\InternalServerErrorException */ diff --git a/src/Model/Profile.php b/src/Model/Profile.php index 9987f86760..d089ada3cd 100644 --- a/src/Model/Profile.php +++ b/src/Model/Profile.php @@ -38,6 +38,7 @@ use Friendica\DI; use Friendica\Network\HTTPException; use Friendica\Protocol\Activity; use Friendica\Protocol\Diaspora; +use Friendica\Security\PermissionSet\Entity\PermissionSet; use Friendica\Util\DateTimeFormat; use Friendica\Util\HTTPSignature; use Friendica\Util\Network; @@ -936,4 +937,86 @@ class Profile return ['total' => $total, 'entries' => $profiles]; } + + /** + * Migrates a legacy profile to the new slimmer profile with extra custom fields. + * Multi profiles are converted to ACl-protected custom fields and deleted. + * + * @param array $profile One profile array + * @throws \Exception + */ + public static function migrate(array $profile) + { + // Already processed, aborting + if ($profile['is-default'] === null) { + return; + } + + $contacts = []; + + if (!$profile['is-default']) { + $contacts = Contact::selectToArray(['id'], [ + 'uid' => $profile['uid'], + 'profile-id' => $profile['id'] + ]); + if (!count($contacts)) { + // No contact visibility selected defaults to user-only permission + $contacts = Contact::selectToArray(['id'], ['uid' => $profile['uid'], 'self' => true]); + } + } + + $permissionSet = DI::permissionSet()->selectOrCreate( + new PermissionSet( + $profile['uid'], + array_column($contacts, 'id') ?? [] + ) + ); + + $order = 1; + + $custom_fields = [ + 'hometown' => DI::l10n()->t('Hometown:'), + 'marital' => DI::l10n()->t('Marital Status:'), + 'with' => DI::l10n()->t('With:'), + 'howlong' => DI::l10n()->t('Since:'), + 'sexual' => DI::l10n()->t('Sexual Preference:'), + 'politic' => DI::l10n()->t('Political Views:'), + 'religion' => DI::l10n()->t('Religious Views:'), + 'likes' => DI::l10n()->t('Likes:'), + 'dislikes' => DI::l10n()->t('Dislikes:'), + 'pdesc' => DI::l10n()->t('Title/Description:'), + 'summary' => DI::l10n()->t('Summary'), + 'music' => DI::l10n()->t('Musical interests'), + 'book' => DI::l10n()->t('Books, literature'), + 'tv' => DI::l10n()->t('Television'), + 'film' => DI::l10n()->t('Film/dance/culture/entertainment'), + 'interest' => DI::l10n()->t('Hobbies/Interests'), + 'romance' => DI::l10n()->t('Love/romance'), + 'work' => DI::l10n()->t('Work/employment'), + 'education' => DI::l10n()->t('School/education'), + 'contact' => DI::l10n()->t('Contact information and Social Networks'), + ]; + + foreach ($custom_fields as $field => $label) { + if (!empty($profile[$field]) && $profile[$field] > DBA::NULL_DATE && $profile[$field] > DBA::NULL_DATETIME) { + DI::profileField()->save(DI::profileFieldFactory()->createFromValues( + $profile['uid'], + $order, + trim($label, ':'), + $profile[$field], + $permissionSet + )); + } + + $profile[$field] = null; + } + + if ($profile['is-default']) { + $profile['profile-name'] = null; + $profile['is-default'] = null; + DBA::update('profile', $profile, ['id' => $profile['id']]); + } else if (!empty($profile['id'])) { + DBA::delete('profile', ['id' => $profile['id']]); + } + } } diff --git a/src/Model/ProfileField.php b/src/Model/ProfileField.php deleted file mode 100644 index c5905f934c..0000000000 --- a/src/Model/ProfileField.php +++ /dev/null @@ -1,85 +0,0 @@ -. - * - */ - -namespace Friendica\Model; - -use Friendica\BaseModel; -use Friendica\Database\Database; -use Friendica\Network\HTTPException\NotFoundException; -use Friendica\Security\PermissionSet\Depository\PermissionSet as PermissionSetDepository; -use Friendica\Security\PermissionSet\Entity\PermissionSet; -use Psr\Log\LoggerInterface; - -/** - * Custom profile field model class. - * - * Custom profile fields are user-created arbitrary profile fields that can be assigned a permission set to restrict its - * display to specific Friendica contacts as it requires magic authentication to work. - * - * @property int uid - * @property int order - * @property int psid - * @property string label - * @property string value - * @property string created - * @property string edited - * @property PermissionSet permissionSet - */ -class ProfileField extends BaseModel -{ - /** @var PermissionSet */ - private $permissionSet; - - /** @var PermissionSetDepository */ - private $permissionSetDepository; - - public function __construct(Database $dba, LoggerInterface $logger, PermissionSetDepository $permissionSetDepository, array $data = []) - { - parent::__construct($dba, $logger, $data); - - $this->permissionSetDepository = $permissionSetDepository; - } - - public function __get($name) - { - $this->checkValid(); - - switch ($name) { - case 'permissionSet': - if (empty($this->permissionSet)) { - $permissionSet = $this->permissionSetDepository->selectOneById($this->psid, $this->uid); - if ($permissionSet->uid !== $this->uid) { - throw new NotFoundException(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; - } - - $return = $this->permissionSet; - break; - default: - $return = parent::__get($name); - break; - } - - return $return; - } -} diff --git a/src/Module/ActivityPub/Objects.php b/src/Module/ActivityPub/Objects.php index 7e2ceb8b0e..5ee1b65d88 100644 --- a/src/Module/ActivityPub/Objects.php +++ b/src/Module/ActivityPub/Objects.php @@ -86,7 +86,7 @@ class Objects extends BaseModule $permissionSets = DI::permissionSet()->selectByContactId($requester_id, $item['uid']); if (!empty($permissionSets)) { $psid = array_merge($permissionSets->column('id'), - [DI::permissionSet()->selectEmptyForUser($item['uid'])]); + [DI::permissionSet()->selectPublicForUser($item['uid'])]); $validated = in_array($item['psid'], $psid); } } diff --git a/src/Module/Api/Friendica/Profile/Show.php b/src/Module/Api/Friendica/Profile/Show.php index b1d4314b7f..8102ac4bcc 100644 --- a/src/Module/Api/Friendica/Profile/Show.php +++ b/src/Module/Api/Friendica/Profile/Show.php @@ -21,7 +21,7 @@ namespace Friendica\Module\Api\Friendica\Profile; -use Friendica\Collection\ProfileFields; +use Friendica\Profile\ProfileField\Collection\ProfileFields; use Friendica\Content\Text\BBCode; use Friendica\DI; use Friendica\Model\Contact; @@ -45,7 +45,7 @@ class Show extends BaseApi $profile = Profile::getByUID($uid); - $profileFields = DI::profileField()->select(['uid' => $uid, 'psid' => PermissionSet::PUBLIC]); + $profileFields = DI::profileField()->selectPublicFieldsByUserId($uid); $profile = self::formatProfile($profile, $profileFields); diff --git a/src/Module/Profile/Status.php b/src/Module/Profile/Status.php index 4cf0971204..e93fc5699e 100644 --- a/src/Module/Profile/Status.php +++ b/src/Module/Profile/Status.php @@ -213,7 +213,7 @@ class Status extends BaseProfile $permissionSets = DI::permissionSet()->selectByContactId($remote_user, $profile['uid']); if (!empty($permissionSets)) { $condition = ['psid' => array_merge($permissionSets->column('id'), - [DI::permissionSet()->selectEmptyForUser($profile['uid'])->id])]; + [DI::permissionSet()->selectPublicForUser($profile['uid'])->id])]; } } elseif ($profile['uid'] == local_user()) { $condition = []; diff --git a/src/Module/Settings/Profile/Index.php b/src/Module/Settings/Profile/Index.php index c51393c984..05b5fd3ea3 100644 --- a/src/Module/Settings/Profile/Index.php +++ b/src/Module/Settings/Profile/Index.php @@ -30,7 +30,8 @@ use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\Profile; -use Friendica\Model\ProfileField; +use Friendica\Profile\ProfileField\Collection\ProfileFields; +use Friendica\Profile\ProfileField\Entity\ProfileField; use Friendica\Model\User; use Friendica\Module\BaseSettings; use Friendica\Module\Security\Login; @@ -100,16 +101,13 @@ class Index extends BaseSettings $homepage = 'http://' . $homepage; } - $profileFields = DI::profileField()->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::profileField()->saveCollectionForUser(local_user(), $profileFieldsNew); $result = Profile::update( [ @@ -265,6 +263,56 @@ 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, + DI::aclFormatter()->toString($profileFieldInputs['new']['contact_allow'] ?? ''), + DI::aclFormatter()->toString($profileFieldInputs['new']['group_allow'] ?? ''), + DI::aclFormatter()->toString($profileFieldInputs['new']['contact_deny'] ?? ''), + DI::aclFormatter()->toString($profileFieldInputs['new']['group_deny'] ?? '') + )); + + $profileFields->append(DI::profileFieldFactory()->createFromValues( + $uid, + $profileFieldOrder['new'], + $profileFieldInputs['new']['label'], + $profileFieldInputs['new']['value'], + $permissionSet + )); + } + + unset($profileFieldInputs['new']); + unset($profileFieldOrder['new']); + + foreach ($profileFieldInputs as $id => $profileFieldInput) { + $permissionSet = DI::permissionSet()->selectOrCreate(DI::permissionSetFactory()->createFromString( + $uid, + DI::aclFormatter()->toString($profileFieldInput['contact_allow'] ?? ''), + DI::aclFormatter()->toString($profileFieldInput['group_allow'] ?? ''), + DI::aclFormatter()->toString($profileFieldInput['contact_deny'] ?? ''), + DI::aclFormatter()->toString($profileFieldInput['group_deny'] ?? '') + )); + + $profileFields->append(DI::profileFieldFactory()->createFromValues( + $uid, + $profileFieldOrder[$id], + $profileFieldInput['label'], + $profileFieldInput['value'], + $permissionSet + )); + } + + return $profileFields; + } + private static function cleanKeywords($keywords) { $keywords = str_replace(',', ' ', $keywords); diff --git a/src/Collection/ProfileFields.php b/src/Profile/ProfileField/Collection/ProfileFields.php similarity index 82% rename from src/Collection/ProfileFields.php rename to src/Profile/ProfileField/Collection/ProfileFields.php index 9e706da1f3..21af7397c9 100644 --- a/src/Collection/ProfileFields.php +++ b/src/Profile/ProfileField/Collection/ProfileFields.php @@ -19,17 +19,23 @@ * */ -namespace Friendica\Collection; +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): ProfileFields { 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): ProfileFields { return parent::filter($callback, $flag); } diff --git a/src/Profile/ProfileField/Depository/ProfileField.php b/src/Profile/ProfileField/Depository/ProfileField.php new file mode 100644 index 0000000000..b792edf8f7 --- /dev/null +++ b/src/Profile/ProfileField/Depository/ProfileField.php @@ -0,0 +1,281 @@ +. + * + */ + +namespace Friendica\Profile\ProfileField\Depository; + +use Friendica\BaseDepository; +use Friendica\Database\Database; +use Friendica\Profile\ProfileField\Exception\ProfileFieldNotFoundException; +use Friendica\Profile\ProfileField\Exception\ProfileFieldPersistenceException; +use Friendica\Profile\ProfileField\Exception\UnexpectedPermissionSetException; +use Friendica\Profile\ProfileField\Factory; +use Friendica\Profile\ProfileField\Entity; +use Friendica\Profile\ProfileField\Collection; +use Friendica\Security\PermissionSet\Depository\PermissionSet as PermissionSetDepository; +use Friendica\Util\DateTimeFormat; +use Psr\Log\LoggerInterface; + +class ProfileField extends BaseDepository +{ + /** @var Factory\ProfileField */ + protected $factory; + + protected static $table_name = 'profile_field'; + + protected static $view_name = 'profile_field-view'; + + /** @var PermissionSetDepository */ + protected $permissionSetDepository; + + public function __construct(Database $database, LoggerInterface $logger, Factory\ProfileField $factory, PermissionSetDepository $permissionSetDepository) + { + parent::__construct($database, $logger, $factory); + + $this->permissionSetDepository = $permissionSetDepository; + } + + /** + * @param array $condition + * @param array $params + * + * @return Entity\ProfileField + * + * @throws ProfileFieldNotFoundException + * @throws UnexpectedPermissionSetException + */ + private function selectOne(array $condition, array $params = []): Entity\ProfileField + { + $fields = $this->db->selectFirst(static::$view_name, [], $condition, $params); + if (!$this->db->isResult($fields)) { + throw new ProfileFieldNotFoundException(); + } + + return $this->factory->createFromTableRow($fields); + } + + /** + * @param array $condition + * @param array $params + * + * @return Collection\ProfileFields + * + * @throws ProfileFieldPersistenceException In case of underlying persistence exceptions + * @throws UnexpectedPermissionSetException + */ + private function select(array $condition, array $params = []): Collection\ProfileFields + { + $rows = $this->db->selectToArray(static::$view_name, [], $condition, $params); + + $Entities = new Collection\ProfileFields(); + foreach ($rows as $fields) { + $this->logger->warning('row', ['row' => $fields]); + $Entities[] = $this->factory->createFromTableRow($fields); + } + + return $Entities; + } + + /** + * Converts a given ProfileField into a DB compatible row array + * + * @param Entity\ProfileField $profileField + * + * @return array + */ + protected function convertToTableRow(Entity\ProfileField $profileField): array + { + return [ + 'uid' => $profileField->uid, + 'label' => $profileField->label, + 'value' => $profileField->value, + 'order' => $profileField->order, + 'created' => $profileField->created->format(DateTimeFormat::MYSQL), + 'edited' => $profileField->edited->format(DateTimeFormat::MYSQL), + 'psid' => $profileField->permissionSet->id + ]; + } + + /** + * Returns all public available ProfileFields for a specific user + * + * @param int $uid the user id + * + * @return Collection\ProfileFields + * + * @throws ProfileFieldPersistenceException In case of underlying persistence exceptions + */ + public function selectPublicFieldsByUserId(int $uid): Collection\ProfileFields + { + try { + $publicPermissionSet = $this->permissionSetDepository->selectPublicForUser($uid); + + return $this->select([ + 'uid' => $uid, + 'psid' => $publicPermissionSet->id + ]); + } catch (\Exception $exception) { + throw new ProfileFieldPersistenceException(sprintf('Cannot select public ProfileField for user "%d"', $uid), $exception); + } + } + + /** + * @param int $uid Field owner user Id + * + * @throws ProfileFieldPersistenceException In case of underlying persistence exceptions + */ + public function selectByUserId(int $uid): Collection\ProfileFields + { + try { + return $this->select( + ['uid' => $uid], + ['order' => ['order']] + ); + } catch (\Exception $exception) { + throw new ProfileFieldPersistenceException(sprintf('Cannot select ProfileField for user "%d"', $uid), $exception); + } + } + + /** + * Retrieve all custom profile field a given contact is able to access to, including public profile fields. + * + * @param int $cid Private contact id, must be owned by $uid + * @param int $uid Field owner user id + * + * @throws \Exception + */ + public function selectByContactId(int $cid, int $uid): Collection\ProfileFields + { + $permissionSets = $this->permissionSetDepository->selectByContactId($cid, $uid); + + $permissionSetIds = $permissionSets->column('id'); + + // Includes public custom fields + $permissionSetIds[] = $this->permissionSetDepository->selectPublicForUser($uid)->id; + + return $this->select( + ['uid' => $uid, 'psid' => $permissionSetIds], + ['order' => ['order']] + ); + } + + /** + * @param int $id + * + * @return Entity\ProfileField + * + * @ProfileFieldNotFoundException In case there is no ProfileField found + */ + public function selectOneById(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 + { + if ($profileField->permissionSet->id === null) { + throw new ProfileFieldPersistenceException('PermissionSet needs to be saved first.'); + } + + $fields = $this->convertToTableRow($profileField); + + try { + if ($profileField->id) { + $this->db->update(self::$table_name, $fields, ['id' => $profileField->id]); + } else { + $this->db->insert(self::$table_name, $fields); + + $profileField = $this->selectOneById($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 = array_flip($profileFieldsOld->column('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 new file mode 100644 index 0000000000..b81437d9a2 --- /dev/null +++ b/src/Profile/ProfileField/Entity/ProfileField.php @@ -0,0 +1,112 @@ +. + * + */ + +namespace Friendica\Profile\ProfileField\Entity; + +use Friendica\BaseEntity; +use Friendica\Network\HTTPException\InternalServerErrorException; +use Friendica\Profile\ProfileField\Exception\ProfileFieldNotFoundException; +use Friendica\Security\PermissionSet\Entity\PermissionSet; + +/** + * Custom profile field entity class. + * + * Custom profile fields are user-created arbitrary profile fields that can be assigned a permission set to restrict its + * display to specific Friendica contacts as it requires magic authentication to work. + * + * @property-read int|null $id + * @property-read int $uid + * @property-read int $order + * @property-read string $label + * @property-read string $value + * @property-read \DateTime $created + * @property-read \DateTime $edited + * @property PermissionSet $permissionSet + */ +class ProfileField extends BaseEntity +{ + /** @var int|null */ + protected $id; + /** @var PermissionSet */ + protected $permissionSet; + /** @var int */ + protected $uid; + /** @var int */ + protected $order; + /** @var string */ + protected $label; + /** @var string */ + protected $value; + /** @var \DateTime */ + protected $created; + /** @var \DateTime */ + protected $edited; + + public function __construct(int $uid, int $order, string $label, string $value, \DateTime $created, \DateTime $edited, PermissionSet $permissionSet, int $id = null) + { + $this->permissionSet = $permissionSet; + $this->uid = $uid; + $this->order = $order; + $this->label = $label; + $this->value = $value; + $this->created = $created; + $this->edited = $edited; + $this->id = $id; + } + + /** + * @throws ProfileFieldNotFoundException + */ + public function __get($name) + { + try { + return parent::__get($name); + } catch (InternalServerErrorException $exception) { + throw new ProfileFieldNotFoundException($exception->getMessage()); + } + } + + /** + * 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->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..3ee209cd79 --- /dev/null +++ b/src/Profile/ProfileField/Exception/ProfileFieldNotFoundException.php @@ -0,0 +1,33 @@ +. + * + */ + +namespace Friendica\Profile\ProfileField\Exception; + +use OutOfBoundsException; +use Throwable; + +class ProfileFieldNotFoundException extends OutOfBoundsException +{ + public function __construct($message = "", Throwable $previous = null) + { + parent::__construct($message, 404, $previous); + } +} diff --git a/src/Profile/ProfileField/Exception/ProfileFieldPersistenceException.php b/src/Profile/ProfileField/Exception/ProfileFieldPersistenceException.php new file mode 100644 index 0000000000..f14e17a081 --- /dev/null +++ b/src/Profile/ProfileField/Exception/ProfileFieldPersistenceException.php @@ -0,0 +1,32 @@ +. + * + */ + +namespace Friendica\Profile\ProfileField\Exception; + +use Throwable; + +class ProfileFieldPersistenceException extends \RuntimeException +{ + public function __construct($message = "", Throwable $previous = null) + { + parent::__construct($message, 500, $previous); + } +} diff --git a/src/Profile/ProfileField/Exception/UnexpectedPermissionSetException.php b/src/Profile/ProfileField/Exception/UnexpectedPermissionSetException.php new file mode 100644 index 0000000000..91220def63 --- /dev/null +++ b/src/Profile/ProfileField/Exception/UnexpectedPermissionSetException.php @@ -0,0 +1,26 @@ +. + * + */ + +namespace Friendica\Profile\ProfileField\Exception; + +class UnexpectedPermissionSetException extends \Exception +{ +} diff --git a/src/Profile/ProfileField/Factory/ProfileField.php b/src/Profile/ProfileField/Factory/ProfileField.php new file mode 100644 index 0000000000..80a8f5502a --- /dev/null +++ b/src/Profile/ProfileField/Factory/ProfileField.php @@ -0,0 +1,106 @@ +. + * + */ + +namespace Friendica\Profile\ProfileField\Factory; + +use Friendica\BaseFactory; +use Friendica\Profile\ProfileField\Exception\UnexpectedPermissionSetException; +use Friendica\Security\PermissionSet\Factory\PermissionSet as PermissionSetFactory; +use Friendica\Profile\ProfileField\Entity; +use Friendica\Capabilities\ICanCreateFromTableRow; +use Friendica\Security\PermissionSet\Entity\PermissionSet; +use Psr\Log\LoggerInterface; + +class ProfileField extends BaseFactory implements ICanCreateFromTableRow +{ + /** @var PermissionSetFactory */ + private $permissionSetFactory; + + public function __construct(LoggerInterface $logger, PermissionSetFactory $permissionSetFactory) + { + parent::__construct($logger); + + $this->permissionSetFactory = $permissionSetFactory; + } + + /** + * @inheritDoc + * + * @throws UnexpectedPermissionSetException + */ + public function createFromTableRow(array $row, PermissionSet $permissionSet = null): Entity\ProfileField + { + if (empty($permissionSet) && + (!array_key_exists('psid', $row) || !array_key_exists('allow_cid', $row) || !array_key_exists('allow_gid', $row) || !array_key_exists('deny_cid', $row) || !array_key_exists('deny_gid', $row)) + ) { + throw new UnexpectedPermissionSetException('Either set the PermissionSet fields (join) or the PermissionSet itself'); + } + + return new Entity\ProfileField( + $row['uid'], + $row['order'], + $row['label'], + $row['value'], + new \DateTime($row['created'] ?? 'now', new \DateTimeZone('UTC')), + new \DateTime($row['edited'] ?? 'now', new \DateTimeZone('UTC')), + $permissionSet ?? $this->permissionSetFactory->createFromString( + $row['uid'], + $row['allow_cid'], + $row['allow_gid'], + $row['deny_cid'], + $row['deny_gid'], + $row['psid'] + ), + $row['id'] ?? null + ); + } + + /** + * Creates a ProfileField instance based on it's values + * + * @param int $uid + * @param int $order + * @param string $label + * @param string $value + * @param PermissionSet $permissionSet + * @param int|null $id + * + * @return Entity\ProfileField + * @throws UnexpectedPermissionSetException + */ + public function createFromValues( + int $uid, + int $order, + string $label, + string $value, + PermissionSet $permissionSet, + int $id = null + ): Entity\ProfileField { + return $this->createFromTableRow([ + 'uid' => $uid, + 'order' => $order, + 'psid' => $permissionSet->id, + 'label' => $label, + 'value' => $value, + 'id' => $id, + ], $permissionSet); + } +} diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index e741789dad..ab7b63866d 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -229,7 +229,7 @@ class Transmitter $permissionSets = DI::permissionSet()->selectByContactId($requester_id, $owner['uid']); if (!empty($permissionSets)) { $condition = ['psid' => array_merge($permissionSets->column('id'), - [DI::permissionSet()->selectEmptyForUser($owner['uid'])])]; + [DI::permissionSet()->selectPublicForUser($owner['uid'])])]; } } } diff --git a/src/Repository/ProfileField.php b/src/Repository/ProfileField.php deleted file mode 100644 index 40417be486..0000000000 --- a/src/Repository/ProfileField.php +++ /dev/null @@ -1,324 +0,0 @@ -. - * - */ - -namespace Friendica\Repository; - -use Friendica\BaseModel; -use Friendica\BaseRepository; -use Friendica\Collection; -use Friendica\Core\L10n; -use Friendica\Database\Database; -use Friendica\Database\DBA; -use Friendica\Model; -use Friendica\Security\PermissionSet\Depository\PermissionSet; -use Friendica\Util\DateTimeFormat; -use Psr\Log\LoggerInterface; - -class ProfileField extends BaseRepository -{ - protected static $table_name = 'profile_field'; - - protected static $model_class = Model\ProfileField::class; - - protected static $collection_class = Collection\ProfileFields::class; - - /** @var PermissionSet */ - private $permissionSet; - /** @var \Friendica\Security\PermissionSet\Factory\PermissionSet */ - private $permissionSetFactory; - /** @var L10n */ - private $l10n; - - public function __construct(Database $dba, LoggerInterface $logger, PermissionSet $permissionSet, \Friendica\Security\PermissionSet\Factory\PermissionSet $permissionSetFactory, L10n $l10n) - { - parent::__construct($dba, $logger); - - $this->permissionSet = $permissionSet; - $this->permissionSetFactory = $permissionSetFactory; - $this->l10n = $l10n; - } - - /** - * @param array $data - * @return Model\ProfileField - */ - protected function create(array $data) - { - return new Model\ProfileField($this->dba, $this->logger, $this->permissionSet, $data); - } - - /** - * @param array $condition - * @return Model\ProfileField - * @throws \Friendica\Network\HTTPException\NotFoundException - */ - public function selectFirst(array $condition) - { - return parent::selectFirst($condition); - } - - /** - * @param array $condition - * @param array $params - * @return 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 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 int $uid Field owner user Id - * @return Collection\ProfileFields - * @throws \Exception - */ - public function selectByUserId(int $uid) - { - return $this->select( - ['uid' => $uid], - ['order' => ['order']] - ); - } - - /** - * Retrieve all custom profile field a given contact is able to access to, including public profile fields. - * - * @param int $cid Private contact id, must be owned by $uid - * @param int $uid Field owner user id - * @return Collection\ProfileFields - * @throws \Exception - */ - public function selectByContactId(int $cid, int $uid) - { - $permissionSets = $this->permissionSet->selectByContactId($cid, $uid); - - $psids = $permissionSets->column('id'); - - // Includes public custom fields - $psids[] = 0; - - return $this->select( - ['uid' => $uid, 'psid' => $psids], - ['order' => ['order']] - ); - } - - /** - * @param array $fields - * @return Model\ProfileField|bool - * @throws \Exception - */ - public function insert(array $fields) - { - $fields['created'] = DateTimeFormat::utcNow(); - $fields['edited'] = DateTimeFormat::utcNow(); - - return parent::insert($fields); - } - - /** - * @param Model\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 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 Collection\ProfileFields - * @throws \Exception - */ - public function updateCollectionFromForm(int $uid, 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 (Model\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 (Model\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->psid = $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. - * - * @param array $profile Profile table row - * @throws \Exception - */ - public function migrateFromLegacyProfile(array $profile) - { - // Already processed, aborting - if ($profile['is-default'] === null) { - return; - } - - $contacts = []; - - if (!$profile['is-default']) { - $contacts = Model\Contact::selectToArray(['id'], ['uid' => $profile['uid'], 'profile-id' => $profile['id']]); - if (!count($contacts)) { - // No contact visibility selected defaults to user-only permission - $contacts = Model\Contact::selectToArray(['id'], ['uid' => $profile['uid'], 'self' => true]); - } - } - - $psid = $this->permissionSet->selectOrCreate( - new \Friendica\Security\PermissionSet\Entity\PermissionSet( - $profile['uid'], - array_column($contacts, 'id') ?? [] - ) - )->id; - - $order = 1; - - $custom_fields = [ - 'hometown' => $this->l10n->t('Hometown:'), - 'marital' => $this->l10n->t('Marital Status:'), - 'with' => $this->l10n->t('With:'), - 'howlong' => $this->l10n->t('Since:'), - 'sexual' => $this->l10n->t('Sexual Preference:'), - 'politic' => $this->l10n->t('Political Views:'), - 'religion' => $this->l10n->t('Religious Views:'), - 'likes' => $this->l10n->t('Likes:'), - 'dislikes' => $this->l10n->t('Dislikes:'), - 'pdesc' => $this->l10n->t('Title/Description:'), - 'summary' => $this->l10n->t('Summary'), - 'music' => $this->l10n->t('Musical interests'), - 'book' => $this->l10n->t('Books, literature'), - 'tv' => $this->l10n->t('Television'), - 'film' => $this->l10n->t('Film/dance/culture/entertainment'), - 'interest' => $this->l10n->t('Hobbies/Interests'), - 'romance' => $this->l10n->t('Love/romance'), - 'work' => $this->l10n->t('Work/employment'), - 'education' => $this->l10n->t('School/education'), - 'contact' => $this->l10n->t('Contact information and Social Networks'), - ]; - - foreach ($custom_fields as $field => $label) { - if (!empty($profile[$field]) && $profile[$field] > DBA::NULL_DATE && $profile[$field] > DBA::NULL_DATETIME) { - $this->insert([ - 'uid' => $profile['uid'], - 'psid' => $psid, - 'order' => $order++, - 'label' => trim($label, ':'), - 'value' => $profile[$field], - ]); - } - - $profile[$field] = null; - } - - if ($profile['is-default']) { - $profile['profile-name'] = null; - $profile['is-default'] = null; - $this->dba->update('profile', $profile, ['id' => $profile['id']]); - } elseif (!empty($profile['id'])) { - $this->dba->delete('profile', ['id' => $profile['id']]); - } - } -} diff --git a/src/Security/PermissionSet/Depository/PermissionSet.php b/src/Security/PermissionSet/Depository/PermissionSet.php index b4a918b6d1..b91f452180 100644 --- a/src/Security/PermissionSet/Depository/PermissionSet.php +++ b/src/Security/PermissionSet/Depository/PermissionSet.php @@ -53,6 +53,22 @@ class PermissionSet extends BaseDepository $this->aclFormatter = $aclFormatter; } + /** + * replaces the PUBLIC id for the public permissionSet + * (no need to create the default permission set over and over again) + * + * @param $condition + */ + private function checkPublicSelect(&$condition) + { + if (empty($condition['allow_cid']) && + empty($condition['allow_gid']) && + empty($condition['deny_cid']) && + empty($condition['deny_gid'])) { + $condition['uid'] = self::PUBLIC; + } + } + /** * @param array $condition * @param array $params @@ -89,22 +105,18 @@ class PermissionSet extends BaseDepository } /** - * @param int $id A permissionset table row id or self::PUBLIC - * @param int|null $uid Should be provided when id can be self::PUBLIC + * @param int $id A PermissionSet table row id or self::PUBLIC + * @param int $uid The owner of the PermissionSet * @return Entity\PermissionSet * @throws NotFoundException */ - public function selectOneById(int $id, int $uid = null): Entity\PermissionSet + public function selectOneById(int $id, int $uid): Entity\PermissionSet { if ($id === self::PUBLIC) { - if (empty($uid)) { - throw new \InvalidArgumentException('Missing uid for Public permission set instantiation'); - } - return $this->factory->createFromString($uid); } - return $this->selectOne(['id' => $id]); + return $this->selectOne(['id' => $id, 'uid' => $uid]); } /** @@ -174,15 +186,15 @@ class PermissionSet extends BaseDepository } /** - * Fetch the empty PermissionSet for a given user, create it if it doesn't exist + * Fetch the public PermissionSet * * @param int $uid * * @return Entity\PermissionSet */ - public function selectEmptyForUser(int $uid): Entity\PermissionSet + public function selectPublicForUser(int $uid): Entity\PermissionSet { - return $this->selectOrCreate($this->factory->createFromString($uid)); + return $this->factory->createFromString($uid, '', '', '', '', self::PUBLIC); } /** @@ -198,6 +210,11 @@ class PermissionSet extends BaseDepository return $permissionSet; } + // Don't select/update Public permission sets + if ($permissionSet->isPublic()) { + return $this->selectPublicForUser($permissionSet->uid); + } + try { return $this->selectOne($this->convertToTableRow($permissionSet)); } catch (NotFoundException $exception) { @@ -205,8 +222,19 @@ class PermissionSet extends BaseDepository } } + /** + * @param Entity\PermissionSet $permissionSet + * + * @return Entity\PermissionSet + * @throws NotFoundException + */ public function save(Entity\PermissionSet $permissionSet): Entity\PermissionSet { + // Don't save/update the common public PermissionSet + if ($permissionSet->isPublic()) { + return $this->selectPublicForUser($permissionSet->uid); + } + $fields = $this->convertToTableRow($permissionSet); if ($permissionSet->id) { @@ -214,7 +242,7 @@ class PermissionSet extends BaseDepository } else { $this->db->insert(self::$table_name, $fields); - $permissionSet = $this->selectOneById($this->db->lastInsertId()); + $permissionSet = $this->selectOneById($this->db->lastInsertId(), $permissionSet->uid); } return $permissionSet; diff --git a/src/Security/PermissionSet/Entity/PermissionSet.php b/src/Security/PermissionSet/Entity/PermissionSet.php index 849871647d..6a04093315 100644 --- a/src/Security/PermissionSet/Entity/PermissionSet.php +++ b/src/Security/PermissionSet/Entity/PermissionSet.php @@ -3,6 +3,7 @@ namespace Friendica\Security\PermissionSet\Entity; use Friendica\BaseEntity; +use Friendica\Security\PermissionSet\Depository\PermissionSet as PermissionSetDepository; /** * @property-read int|null $id @@ -47,6 +48,21 @@ class PermissionSet extends BaseEntity $this->deny_gid = $deny_gid; } + /** + * Checks, if the current PermissionSet is a/the public PermissionSet + * + * @return bool + */ + public function isPublic(): bool + { + return (($this->id === PermissionSetDepository::PUBLIC) || + (is_null($this->id) && + empty($this->allow_cid) && + empty($this->allow_gid) && + empty($this->deny_cid) && + empty($this->deny_gid))); + } + /** * Creates a new Entity with a new allowed_cid list (wipes the id because it isn't the same entity anymore) * diff --git a/src/Security/PermissionSet/Factory/PermissionSet.php b/src/Security/PermissionSet/Factory/PermissionSet.php index 84c7d5838d..bde6cd73f1 100644 --- a/src/Security/PermissionSet/Factory/PermissionSet.php +++ b/src/Security/PermissionSet/Factory/PermissionSet.php @@ -51,7 +51,8 @@ class PermissionSet extends BaseFactory implements ICanCreateFromTableRow string $allow_cid = '', string $allow_gid = '', string $deny_cid = '', - string $deny_gid = ''): Entity\PermissionSet + string $deny_gid = '', + int $id = null): Entity\PermissionSet { return $this->createFromTableRow([ 'uid' => $uid, @@ -59,6 +60,7 @@ class PermissionSet extends BaseFactory implements ICanCreateFromTableRow 'allow_gid' => $allow_gid, 'deny_cid' => $deny_cid, 'deny_gid' => $deny_gid, + 'id' => $id, ]); } } diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index 6f19865073..ec7c0e3ef4 100644 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -55,7 +55,7 @@ use Friendica\Database\DBA; if (!defined('DB_UPDATE_VERSION')) { - define('DB_UPDATE_VERSION', 1439); + define('DB_UPDATE_VERSION', 1440); } return [ diff --git a/static/dbview.config.php b/static/dbview.config.php index 662425b02e..a12d5747a5 100644 --- a/static/dbview.config.php +++ b/static/dbview.config.php @@ -1047,5 +1047,23 @@ INNER JOIN `workerqueue` ON `workerqueue`.`pid` = `process`.`pid` WHERE NOT `workerqueue`.`done`" ], + "profile_field-view" => [ + "fields" => [ + "id" => ["profile_field", "id"], + "uid" => ["profile_field", "uid"], + "label" => ["profile_field", "label"], + "value" => ["profile_field", "value"], + "order" => ["profile_field", "order"], + "psid"=> ["profile_field", "psid"], + "allow_cid" => ["permissionset", "allow_cid"], + "allow_gid" => ["permissionset", "allow_gid"], + "deny_cid" => ["permissionset", "deny_cid"], + "deny_gid" => ["permissionset", "deny_gid"], + "created" => ["profile_field", "created"], + "edited" => ["profile_field", "edited"], + ], + "query" => "FROM `profile_field` + INNER JOIN `permissionset` ON `permissionset`.`id` = `profile_field`.`psid`" + ], ]; diff --git a/tests/Util/CollectionDouble.php b/tests/Util/CollectionDouble.php new file mode 100644 index 0000000000..ed5e371e45 --- /dev/null +++ b/tests/Util/CollectionDouble.php @@ -0,0 +1,9 @@ +protString = $protString; + $this->protInt = $protInt; + $this->protDateTime = $protDateTime; + $this->privString = $privString; + } +} diff --git a/tests/datasets/api.fixture.php b/tests/datasets/api.fixture.php index 86b2bd5648..6743dbf112 100644 --- a/tests/datasets/api.fixture.php +++ b/tests/datasets/api.fixture.php @@ -26,6 +26,8 @@ use Friendica\Model\Notification; return [ // Empty these tables + 'profile_field', + 'permissionset', 'cache', 'conversation', 'pconfig', diff --git a/tests/src/CollectionTest.php b/tests/src/CollectionTest.php new file mode 100644 index 0000000000..9e198c9859 --- /dev/null +++ b/tests/src/CollectionTest.php @@ -0,0 +1,24 @@ +append(new EntityDouble('test', 23, new \DateTime('now', new \DateTimeZone('UTC')), 'privTest')); + $collection->append(new EntityDouble('test2', 25, new \DateTime('now', new \DateTimeZone('UTC')), 'privTest23')); + + self::assertEquals(['test', 'test2'], $collection->column('protString')); + self::assertEmpty($collection->column('privString')); + self::assertEquals([23,25], $collection->column('protInt')); + } +} diff --git a/tests/src/Profile/ProfileField/Depository/ProfileFieldTest.php b/tests/src/Profile/ProfileField/Depository/ProfileFieldTest.php new file mode 100644 index 0000000000..87ea6ca9c5 --- /dev/null +++ b/tests/src/Profile/ProfileField/Depository/ProfileFieldTest.php @@ -0,0 +1,136 @@ +depository = DI::profileField(); + $this->factory = DI::profileFieldFactory(); + $this->permissionSetFactory = DI::permissionSetFactory(); + $this->permissionSetDepository = DI::permissionSet(); + } + + /** + * Test create ProfileField without a valid PermissionSet + */ + public function testSavingWithoutPermissionSet() + { + self::expectExceptionMessage('PermissionSet needs to be saved first.'); + self::expectException(ProfileFieldPersistenceException::class); + + $profileField = $this->factory->createFromValues(42, 0, 'public', 'value', $this->permissionSetFactory->createFromString(42, '', '<~>')); + + self::assertEquals($profileField->uid, $profileField->permissionSet->uid); + + $this->depository->save($profileField); + } + + /** + * Test saving a new entity + */ + public function testSaveNew() + { + $profileField = $this->factory->createFromValues(42, 0, 'public', 'value', $this->permissionSetDepository->save($this->permissionSetFactory->createFromString(42, '', '<~>'))); + + self::assertEquals($profileField->uid, $profileField->permissionSet->uid); + + $savedProfileField = $this->depository->save($profileField); + + self::assertNotNull($savedProfileField->id); + self::assertNull($profileField->id); + + $selectedProfileField = $this->depository->selectOneById($savedProfileField->id); + + self::assertEquals($savedProfileField, $selectedProfileField); + + $profileFields = new ProfileFields([$selectedProfileField]); + $this->depository->deleteCollection($profileFields); + } + + /** + * Test updating the order of a ProfileField + */ + public function testUpdateOrder() + { + $profileField = $this->factory->createFromValues(42, 0, 'public', 'value', $this->permissionSetDepository->save($this->permissionSetFactory->createFromString(42, '', '<~>'))); + + self::assertEquals($profileField->uid, $profileField->permissionSet->uid); + + $savedProfileField = $this->depository->save($profileField); + + self::assertNotNull($savedProfileField->id); + self::assertNull($profileField->id); + + $selectedProfileField = $this->depository->selectOneById($savedProfileField->id); + + self::assertEquals($savedProfileField, $selectedProfileField); + + $selectedProfileField->setOrder(66); + + $updatedOrderProfileField = $this->depository->save($selectedProfileField); + + self::assertEquals($selectedProfileField->id, $updatedOrderProfileField->id); + self::assertEquals(66, $updatedOrderProfileField->order); + + // Even using the ID of the old, saved ProfileField returns the right instance + $updatedFromOldProfileField = $this->depository->selectOneById($savedProfileField->id); + self::assertEquals(66, $updatedFromOldProfileField->order); + + $profileFields = new ProfileFields([$updatedFromOldProfileField]); + $this->depository->deleteCollection($profileFields); + } + + /** + * Test updating a whole entity + */ + public function testUpdate() + { + $profileField = $this->factory->createFromValues(42, 0, 'public', 'value', $this->permissionSetDepository->save($this->permissionSetFactory->createFromString(42, '', '<~>'))); + + self::assertEquals($profileField->uid, $profileField->permissionSet->uid); + + $savedProfileField = $this->depository->save($profileField); + + self::assertNotNull($savedProfileField->id); + self::assertNull($profileField->id); + + $selectedProfileField = $this->depository->selectOneById($savedProfileField->id); + + self::assertEquals($savedProfileField, $selectedProfileField); + + $savedProfileField->update('another', 5, $this->permissionSetDepository->selectPublicForUser(42)); + self::assertEquals(PermissionSet::PUBLIC, $savedProfileField->permissionSet->id); + + $publicProfileField = $this->depository->save($savedProfileField); + + self::assertEquals($this->permissionSetDepository->selectPublicForUser(42), $publicProfileField->permissionSet); + self::assertEquals('another', $publicProfileField->value); + self::assertEquals(5, $publicProfileField->order); + + $profileFields = new ProfileFields([$publicProfileField]); + $this->depository->deleteCollection($profileFields); + } +} diff --git a/tests/src/Profile/ProfileField/Entity/ProfileFieldTest.php b/tests/src/Profile/ProfileField/Entity/ProfileFieldTest.php new file mode 100644 index 0000000000..8acd1d94ca --- /dev/null +++ b/tests/src/Profile/ProfileField/Entity/ProfileFieldTest.php @@ -0,0 +1,208 @@ +permissionSetDepository = \Mockery::mock(PermissionSetDepository::class); + $this->permissionSetFactory = new PermissionSetFactory(new VoidLogger(), new ACLFormatter()); + $this->profileFieldFactory = new ProfileFieldFactory(new VoidLogger(), $this->permissionSetFactory); + } + + public function dataEntity() + { + return [ + 'default' => [ + 'uid' => 23, + 'order' => 1, + 'psid' => 2, + 'label' => 'test', + 'value' => 'more', + 'created' => new \DateTime('2021-10-10T21:12:00.000000+0000', new \DateTimeZone('UTC')), + 'edited' => new \DateTime('2021-10-10T21:12:00.000000+0000', new \DateTimeZone('UTC')), + 'permissionSet' => [ + 'uid' => 23, + 'allow_cid' => "<1>", + 'allow_gid' => "<~>", + 'deny_cid' => '<2>', + 'deny_gid' => '<3>', + 'id' => 2, + ] + ], + 'withId' => [ + 'uid' => 23, + 'order' => 1, + 'psid' => 2, + 'label' => 'test', + 'value' => 'more', + 'created' => new \DateTime('2021-10-10T21:12:00.000000+0000', new \DateTimeZone('UTC')), + 'edited' => new \DateTime('2021-10-10T21:12:00.000000+0000', new \DateTimeZone('UTC')), + 'permissionSet' => [ + 'uid' => 23, + 'allow_cid' => "<1>", + 'allow_gid' => "<~>", + 'deny_cid' => '<2>', + 'deny_gid' => '<3>', + 'id' => 2, + ], + 'id' => 54, + ], + ]; + } + + /** + * @dataProvider dataEntity + */ + public function testEntity(int $uid, int $order, int $psid, string $label, string $value, \DateTime $created, \DateTime $edited, array $permissionSet, $id = null) + { + $entity = new ProfileField($uid, $order, $label, $value, $created, $edited, $this->permissionSetFactory->createFromTableRow($permissionSet), $id); + + self::assertEquals($uid, $entity->uid); + self::assertEquals($order, $entity->order); + self::assertEquals($psid, $entity->permissionSet->id); + self::assertEquals($label, $entity->label); + self::assertEquals($value, $entity->value); + self::assertEquals($created, $entity->created); + self::assertEquals($edited, $entity->edited); + self::assertEquals($id, $entity->id); + } + + /** + * @dataProvider dataEntity + */ + public function testUpdate(int $uid, int $order, int $psid, string $label, string $value, \DateTime $created, \DateTime $edited, array $permissionSet, $id = null) + { + $permissionSet = $this->permissionSetFactory->createFromTableRow(['uid' => 2, 'id' => $psid]); + + $entity = $this->profileFieldFactory->createFromTableRow([ + 'uid' => $uid, + 'order' => $order, + 'psid' => $psid, + 'label' => $label, + 'value' => $value, + 'created' => $created->format(DateTimeFormat::MYSQL), + 'edited' => $edited->format(DateTimeFormat::MYSQL), + 'id' => $id, + ], $permissionSet); + + $permissionSetNew = $this->permissionSetFactory->createFromTableRow([ + 'uid' => 2, + 'allow_cid' => '<1>', + 'id' => 23 + ]); + + $entity->update('updatedValue', 2345, $permissionSetNew); + + self::assertEquals($uid, $entity->uid); + self::assertEquals(2345, $entity->order); + self::assertEquals(23, $entity->permissionSet->id); + self::assertEquals($label, $entity->label); + self::assertEquals('updatedValue', $entity->value); + self::assertEquals($created, $entity->created); + self::assertGreaterThan($edited, $entity->edited); + self::assertEquals($id, $entity->id); + } + + /** + * @dataProvider dataEntity + */ + public function testSetOrder(int $uid, int $order, int $psid, string $label, string $value, \DateTime $created, \DateTime $edited, array $permissionSet, $id = null) + { + $permissionSet = $this->permissionSetFactory->createFromTableRow(['uid' => 2, 'id' => $psid]); + + $entity = $this->profileFieldFactory->createFromTableRow([ + 'uid' => $uid, + 'order' => $order, + 'psid' => $psid, + 'label' => $label, + 'value' => $value, + 'created' => $created->format(DateTimeFormat::MYSQL), + 'edited' => $edited->format(DateTimeFormat::MYSQL), + 'id' => $id, + ], $permissionSet); + + $entity->setOrder(2345); + + self::assertEquals($uid, $entity->uid); + self::assertEquals(2345, $entity->order); + self::assertEquals($psid, $entity->permissionSet->id); + self::assertEquals($label, $entity->label); + self::assertEquals($value, $entity->value); + self::assertEquals($created, $entity->created); + self::assertGreaterThan($edited, $entity->edited); + self::assertEquals($id, $entity->id); + } + + /** + * Test the exception because of a wrong property + * + * @dataProvider dataEntity + */ + public function testWrongGet(int $uid, int $order, int $psid, string $label, string $value, \DateTime $created, \DateTime $edited, array $permissionSet, $id = null) + { + $entity = new ProfileField($uid, $order, $label, $value, $created, $edited, $this->permissionSetFactory->createFromTableRow($permissionSet), $id); + + self::expectException(ProfileFieldNotFoundException::class); + $entity->wrong; + } + + /** + * Test gathering the permissionset + * + * @dataProvider dataEntity + */ + public function testPermissionSet(int $uid, int $order, int $psid, string $label, string $value, \DateTime $created, \DateTime $edited, array $permissionSet, $id = null) + { + $entity = new ProfileField($uid, $order, $label, $value, $created, $edited, $this->permissionSetFactory->createFromTableRow($permissionSet), $id); + + $permissionSet = $this->permissionSetFactory->createFromTableRow(['uid' => $uid, 'id' => $psid]); + + $this->permissionSetDepository->shouldReceive('selectOneById')->with($psid, $uid)->andReturns($permissionSet); + + self::assertEquals($psid, $entity->permissionSet->id); + } + + /** + * Test the exception because the factory cannot find a permissionSet ID, nor the permissionSet itself + * + * @dataProvider dataEntity + */ + public function testMissingPermissionFactory(int $uid, int $order, int $psid, string $label, string $value, \DateTime $created, \DateTime $edited, array $permissionSet, $id = null) + { + self::expectException(UnexpectedPermissionSetException::class); + self::expectExceptionMessage('Either set the PermissionSet fields (join) or the PermissionSet itself'); + + $entity = $this->profileFieldFactory->createFromTableRow([ + 'uid' => $uid, + 'order' => $order, + 'label' => $label, + 'value' => $value, + 'created' => $created->format(DateTimeFormat::MYSQL), + 'edited' => $edited->format(DateTimeFormat::MYSQL), + 'id' => $id, + ]); + } +} diff --git a/tests/src/Security/PermissionSet/Depository/PermissionSetTest.php b/tests/src/Security/PermissionSet/Depository/PermissionSetTest.php index 3057694117..596b1e6c15 100644 --- a/tests/src/Security/PermissionSet/Depository/PermissionSetTest.php +++ b/tests/src/Security/PermissionSet/Depository/PermissionSetTest.php @@ -2,39 +2,63 @@ namespace Friendica\Test\src\Security\PermissionSet\Depository; -use Dice\Dice; -use Friendica\Database\Database; +use Friendica\Security\PermissionSet\Depository\PermissionSet as PermissionSetDepository; +use Friendica\Security\PermissionSet\Entity\PermissionSet; +use Friendica\Security\PermissionSet\Factory\PermissionSet as PermissionSetFactory; +use Friendica\Test\FixtureTest; use Friendica\DI; -use Friendica\Security\PermissionSet\Depository\PermissionSet; -use Friendica\Test\MockedTest; -use Friendica\Test\Util\Database\StaticDatabase; -class PermissionSetTest extends MockedTest +class PermissionSetTest extends FixtureTest { - /** @var PermissionSet */ + /** @var PermissionSetDepository */ private $depository; + /** @var PermissionSetFactory */ + private $factory; public function setUp(): void { - $dice = (new Dice()) - ->addRules(include __DIR__ . '/../../../../../static/dependencies.config.php') - ->addRule(Database::class, ['instanceOf' => StaticDatabase::class, 'shared' => true]); - DI::init($dice); + parent::setUp(); $this->depository = DI::permissionSet(); - } - - public function testSelectOneByIdPublicMissingUid() - { - $this->expectException(\InvalidArgumentException::class); - - $this->depository->selectOneById(PermissionSet::PUBLIC); + $this->factory = DI::permissionSetFactory(); } public function testSelectOneByIdPublic() { - $permissionSet = $this->depository->selectOneById(PermissionSet::PUBLIC, 1); + $permissionSet = $this->depository->selectPublicForUser(1); - $this->assertInstanceOf(\Friendica\Security\PermissionSet\Entity\PermissionSet::class, $permissionSet); + $this->assertInstanceOf(PermissionSet::class, $permissionSet); + self::assertEmpty($permissionSet->allow_cid); + self::assertEmpty($permissionSet->allow_gid); + self::assertEmpty($permissionSet->deny_cid); + self::assertEmpty($permissionSet->deny_gid); + self::assertEmpty(PermissionSetDepository::PUBLIC, $permissionSet->id); + self::assertEquals(1, $permissionSet->uid); + } + + /** + * Test create/update PermissionSets + */ + public function testSaving() + { + $permissionSet = $this->factory->createFromString(42, '', '<~>'); + + $permissionSet = $this->depository->selectOrCreate($permissionSet); + + self::assertNotNull($permissionSet->id); + + $permissionSetSelected = $this->depository->selectOneById($permissionSet->id, 42); + + self::assertEquals($permissionSet, $permissionSetSelected); + + $newPermissionSet = $permissionSet->withAllowedContacts(['1', '2']); + $savedPermissionSet = $this->depository->save($newPermissionSet); + + self::assertNotNull($savedPermissionSet->id); + self::assertNull($newPermissionSet->id); + + $permissionSetSavedSelected = $this->depository->selectOneById($savedPermissionSet->id, 42); + + self::assertEquals($savedPermissionSet, $permissionSetSavedSelected); } } diff --git a/update.php b/update.php index 6a53c83f16..64bd7a9c33 100644 --- a/update.php +++ b/update.php @@ -55,6 +55,7 @@ use Friendica\Model\Photo; use Friendica\Model\Post; use Friendica\Model\Profile; use Friendica\Model\Storage; +use Friendica\Security\PermissionSet\Depository\PermissionSet; use Friendica\Worker\Delivery; // Post-update script of PR 5751 @@ -206,7 +207,7 @@ function update_1332() $profiles = DBA::select('profile', [], $condition); while ($profile = DBA::fetch($profiles)) { - DI::profileField()->migrateFromLegacyProfile($profile); + Profile::migrate($profile); } DBA::close($profiles); @@ -1028,3 +1029,12 @@ function update_1439() } DBA::close($intros); } + +function update_1440() +{ + // Fix wrong public permissionset + DBA::p("UPDATE `profile_field` SET `psid` = ? WHERE psid IN (SELECT `id` FROM `permissionset` WHERE `id` != ? AND `allow_cid` = '' AND `allow_gid` = '' AND `deny_cid` = '' AND `deny_gid` = '')", PermissionSet::PUBLIC, PermissionSet::PUBLIC); + DBA::delete('permissionset', ["`id` != ? AND `allow_cid` = '' AND `allow_gid` = '' AND `deny_cid` = '' AND `deny_gid` = ''", PermissionSet::PUBLIC]); + + return Update::SUCCESS; +} diff --git a/view/lang/C/messages.po b/view/lang/C/messages.po index a2829de9f2..52412965f2 100644 --- a/view/lang/C/messages.po +++ b/view/lang/C/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 2021.12-dev\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-10-16 19:31-0400\n" +"POT-Creation-Date: 2021-10-18 00:07+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -331,7 +331,7 @@ msgid "Access denied." msgstr "" #: mod/cal.php:61 mod/cal.php:78 mod/photos.php:69 mod/photos.php:143 -#: mod/photos.php:815 src/Model/Profile.php:228 src/Module/HCard.php:52 +#: mod/photos.php:815 src/Model/Profile.php:229 src/Module/HCard.php:52 #: src/Module/Profile/Common.php:41 src/Module/Profile/Common.php:52 #: src/Module/Profile/Contacts.php:40 src/Module/Profile/Contacts.php:50 #: src/Module/Profile/Media.php:38 src/Module/Profile/Status.php:58 @@ -509,7 +509,7 @@ msgstr "" msgid "Permission settings" msgstr "" -#: mod/editpost.php:116 src/Core/ACL.php:327 +#: mod/editpost.php:116 src/Core/ACL.php:325 msgid "CC: email addresses" msgstr "" @@ -527,7 +527,7 @@ msgstr "" msgid "Categories (comma-separated list)" msgstr "" -#: mod/editpost.php:123 src/Core/ACL.php:328 +#: mod/editpost.php:123 src/Core/ACL.php:326 msgid "Example: bob@example.com, mary@example.com" msgstr "" @@ -545,7 +545,7 @@ msgid "Cancel" msgstr "" #: mod/editpost.php:134 src/Content/Conversation.php:380 -#: src/Content/Widget/VCard.php:107 src/Model/Profile.php:459 +#: src/Content/Widget/VCard.php:107 src/Model/Profile.php:460 #: src/Module/Admin/Logs/View.php:92 msgid "Message" msgstr "" @@ -615,13 +615,13 @@ msgid "Event Finishes:" msgstr "" #: mod/events.php:506 src/Module/Profile/Profile.php:172 -#: src/Module/Settings/Profile/Index.php:239 +#: src/Module/Settings/Profile/Index.php:237 msgid "Description:" msgstr "" #: mod/events.php:508 src/Content/Widget/VCard.php:98 src/Model/Event.php:80 #: src/Model/Event.php:107 src/Model/Event.php:466 src/Model/Event.php:915 -#: src/Model/Profile.php:367 src/Module/Contact.php:565 +#: src/Model/Profile.php:368 src/Module/Contact.php:565 #: src/Module/Directory.php:150 src/Module/Notifications/Introductions.php:165 #: src/Module/Profile/Profile.php:194 msgid "Location:" @@ -647,7 +647,7 @@ msgstr "" #: src/Module/Install.php:245 src/Module/Install.php:287 #: src/Module/Install.php:324 src/Module/Invite.php:177 #: src/Module/Item/Compose.php:150 src/Module/Profile/Profile.php:247 -#: src/Module/Settings/Profile/Index.php:223 src/Object/Post.php:963 +#: src/Module/Settings/Profile/Index.php:221 src/Object/Post.php:963 #: view/theme/duepuntozero/config.php:69 view/theme/frio/config.php:160 #: view/theme/quattro/config.php:71 view/theme/vier/config.php:119 msgid "Submit" @@ -2374,7 +2374,7 @@ msgstr "" msgid "All contacts" msgstr "" -#: src/BaseModule.php:212 src/Content/Widget.php:231 src/Core/ACL.php:195 +#: src/BaseModule.php:212 src/Content/Widget.php:231 src/Core/ACL.php:193 #: src/Module/Contact.php:756 src/Module/PermissionTooltip.php:75 #: src/Module/PermissionTooltip.php:97 msgid "Followers" @@ -2988,7 +2988,7 @@ msgstr "" #: src/Content/Item.php:444 src/Content/Item.php:466 src/Model/Contact.php:1010 #: src/Model/Contact.php:1068 src/Model/Contact.php:1077 -#: src/Module/Directory.php:160 src/Module/Settings/Profile/Index.php:226 +#: src/Module/Directory.php:160 src/Module/Settings/Profile/Index.php:224 msgid "View Profile" msgstr "" @@ -3357,7 +3357,7 @@ msgid "The end" msgstr "" #: src/Content/Text/HTML.php:885 src/Content/Widget/VCard.php:103 -#: src/Model/Profile.php:453 +#: src/Model/Profile.php:454 msgid "Follow" msgstr "" @@ -3533,68 +3533,68 @@ msgstr[1] "" msgid "More Trending Tags" msgstr "" -#: src/Content/Widget/VCard.php:96 src/Model/Profile.php:372 +#: src/Content/Widget/VCard.php:96 src/Model/Profile.php:373 #: src/Module/Contact.php:567 src/Module/Profile/Profile.php:176 msgid "XMPP:" msgstr "" -#: src/Content/Widget/VCard.php:97 src/Model/Profile.php:373 +#: src/Content/Widget/VCard.php:97 src/Model/Profile.php:374 #: src/Module/Contact.php:569 src/Module/Profile/Profile.php:180 msgid "Matrix:" msgstr "" -#: src/Content/Widget/VCard.php:101 src/Model/Profile.php:465 +#: src/Content/Widget/VCard.php:101 src/Model/Profile.php:466 #: src/Module/Notifications/Introductions.php:179 msgid "Network:" msgstr "" -#: src/Content/Widget/VCard.php:105 src/Model/Profile.php:455 +#: src/Content/Widget/VCard.php:105 src/Model/Profile.php:456 msgid "Unfollow" msgstr "" -#: src/Core/ACL.php:166 src/Module/Profile/Profile.php:242 +#: src/Core/ACL.php:164 src/Module/Profile/Profile.php:242 msgid "Yourself" msgstr "" -#: src/Core/ACL.php:202 src/Module/PermissionTooltip.php:81 +#: src/Core/ACL.php:200 src/Module/PermissionTooltip.php:81 #: src/Module/PermissionTooltip.php:103 msgid "Mutuals" msgstr "" -#: src/Core/ACL.php:294 +#: src/Core/ACL.php:292 msgid "Post to Email" msgstr "" -#: src/Core/ACL.php:321 +#: src/Core/ACL.php:319 msgid "Public" msgstr "" -#: src/Core/ACL.php:322 +#: src/Core/ACL.php:320 msgid "" "This content will be shown to all your followers and can be seen in the " "community pages and by anyone with its link." msgstr "" -#: src/Core/ACL.php:323 +#: src/Core/ACL.php:321 msgid "Limited/Private" msgstr "" -#: src/Core/ACL.php:324 +#: src/Core/ACL.php:322 msgid "" "This content will be shown only to the people in the first box, to the " "exception of the people mentioned in the second box. It won't appear " "anywhere public." msgstr "" -#: src/Core/ACL.php:325 +#: src/Core/ACL.php:323 msgid "Show to:" msgstr "" -#: src/Core/ACL.php:326 +#: src/Core/ACL.php:324 msgid "Except to:" msgstr "" -#: src/Core/ACL.php:329 +#: src/Core/ACL.php:327 msgid "Connectors" msgstr "" @@ -4178,35 +4178,35 @@ msgid "" "\t\t\t\t\tThe friendica database was successfully updated from %s to %s." msgstr "" -#: src/Core/UserImport.php:124 +#: src/Core/UserImport.php:125 msgid "Error decoding account file" msgstr "" -#: src/Core/UserImport.php:130 +#: src/Core/UserImport.php:131 msgid "Error! No version data in file! This is not a Friendica account file?" msgstr "" -#: src/Core/UserImport.php:138 +#: src/Core/UserImport.php:139 #, php-format msgid "User '%s' already exists on this server!" msgstr "" -#: src/Core/UserImport.php:174 +#: src/Core/UserImport.php:175 msgid "User creation error" msgstr "" -#: src/Core/UserImport.php:219 +#: src/Core/UserImport.php:220 #, php-format msgid "%d contact not imported" msgid_plural "%d contacts not imported" msgstr[0] "" msgstr[1] "" -#: src/Core/UserImport.php:272 +#: src/Core/UserImport.php:273 msgid "User profile creation error" msgstr "" -#: src/Core/UserImport.php:325 +#: src/Core/UserImport.php:326 msgid "Done. You can now login with your username and password" msgstr "" @@ -4515,66 +4515,146 @@ msgstr "" msgid "[no subject]" msgstr "" -#: src/Model/Profile.php:355 src/Module/Profile/Profile.php:256 +#: src/Model/Profile.php:356 src/Module/Profile/Profile.php:256 #: src/Module/Profile/Profile.php:258 msgid "Edit profile" msgstr "" -#: src/Model/Profile.php:357 +#: src/Model/Profile.php:358 msgid "Change profile photo" msgstr "" -#: src/Model/Profile.php:370 src/Module/Directory.php:155 +#: src/Model/Profile.php:371 src/Module/Directory.php:155 #: src/Module/Profile/Profile.php:184 msgid "Homepage:" msgstr "" -#: src/Model/Profile.php:371 src/Module/Contact.php:571 +#: src/Model/Profile.php:372 src/Module/Contact.php:571 #: src/Module/Notifications/Introductions.php:167 msgid "About:" msgstr "" -#: src/Model/Profile.php:457 +#: src/Model/Profile.php:458 msgid "Atom feed" msgstr "" -#: src/Model/Profile.php:495 src/Model/Profile.php:592 +#: src/Model/Profile.php:496 src/Model/Profile.php:593 msgid "g A l F d" msgstr "" -#: src/Model/Profile.php:496 +#: src/Model/Profile.php:497 msgid "F d" msgstr "" -#: src/Model/Profile.php:558 src/Model/Profile.php:643 +#: src/Model/Profile.php:559 src/Model/Profile.php:644 msgid "[today]" msgstr "" -#: src/Model/Profile.php:568 +#: src/Model/Profile.php:569 msgid "Birthday Reminders" msgstr "" -#: src/Model/Profile.php:569 +#: src/Model/Profile.php:570 msgid "Birthdays this week:" msgstr "" -#: src/Model/Profile.php:630 +#: src/Model/Profile.php:631 msgid "[No description]" msgstr "" -#: src/Model/Profile.php:656 +#: src/Model/Profile.php:657 msgid "Event Reminders" msgstr "" -#: src/Model/Profile.php:657 +#: src/Model/Profile.php:658 msgid "Upcoming events the next 7 days:" msgstr "" -#: src/Model/Profile.php:845 +#: src/Model/Profile.php:846 #, php-format msgid "OpenWebAuth: %1$s welcomes %2$s" msgstr "" +#: src/Model/Profile.php:978 +msgid "Hometown:" +msgstr "" + +#: src/Model/Profile.php:979 +msgid "Marital Status:" +msgstr "" + +#: src/Model/Profile.php:980 +msgid "With:" +msgstr "" + +#: src/Model/Profile.php:981 +msgid "Since:" +msgstr "" + +#: src/Model/Profile.php:982 +msgid "Sexual Preference:" +msgstr "" + +#: src/Model/Profile.php:983 +msgid "Political Views:" +msgstr "" + +#: src/Model/Profile.php:984 +msgid "Religious Views:" +msgstr "" + +#: src/Model/Profile.php:985 +msgid "Likes:" +msgstr "" + +#: src/Model/Profile.php:986 +msgid "Dislikes:" +msgstr "" + +#: src/Model/Profile.php:987 +msgid "Title/Description:" +msgstr "" + +#: src/Model/Profile.php:988 src/Module/Admin/Summary.php:234 +msgid "Summary" +msgstr "" + +#: src/Model/Profile.php:989 +msgid "Musical interests" +msgstr "" + +#: src/Model/Profile.php:990 +msgid "Books, literature" +msgstr "" + +#: src/Model/Profile.php:991 +msgid "Television" +msgstr "" + +#: src/Model/Profile.php:992 +msgid "Film/dance/culture/entertainment" +msgstr "" + +#: src/Model/Profile.php:993 +msgid "Hobbies/Interests" +msgstr "" + +#: src/Model/Profile.php:994 +msgid "Love/romance" +msgstr "" + +#: src/Model/Profile.php:995 +msgid "Work/employment" +msgstr "" + +#: src/Model/Profile.php:996 +msgid "School/education" +msgstr "" + +#: src/Model/Profile.php:997 +msgid "Contact information and Social Networks" +msgstr "" + #: src/Model/Storage/FilesystemConfig.php:77 msgid "Storage base path" msgstr "" @@ -6607,10 +6687,6 @@ msgstr "" msgid "Server Settings" msgstr "" -#: src/Module/Admin/Summary.php:234 src/Repository/ProfileField.php:290 -msgid "Summary" -msgstr "" - #: src/Module/Admin/Summary.php:236 msgid "Registered users" msgstr "" @@ -7731,7 +7807,7 @@ msgid "Sort by post received date" msgstr "" #: src/Module/Conversation/Network.php:250 -#: src/Module/Settings/Profile/Index.php:228 +#: src/Module/Settings/Profile/Index.php:226 msgid "Personal" msgstr "" @@ -7955,7 +8031,7 @@ msgid "Twitter Source / Tweet URL (requires API key)" msgstr "" #: src/Module/Debug/Feed.php:38 src/Module/Filer/SaveTag.php:40 -#: src/Module/Settings/Profile/Index.php:142 +#: src/Module/Settings/Profile/Index.php:140 msgid "You must be logged in to use this module" msgstr "" @@ -8718,12 +8794,12 @@ msgstr "" msgid "Birthday:" msgstr "" -#: src/Module/Profile/Profile.php:167 src/Module/Settings/Profile/Index.php:246 +#: src/Module/Profile/Profile.php:167 src/Module/Settings/Profile/Index.php:244 #: src/Util/Temporal.php:165 msgid "Age: " msgstr "" -#: src/Module/Profile/Profile.php:167 src/Module/Settings/Profile/Index.php:246 +#: src/Module/Profile/Profile.php:167 src/Module/Settings/Profile/Index.php:244 #: src/Util/Temporal.php:165 #, php-format msgid "%d year old" @@ -9298,133 +9374,133 @@ msgstr "" msgid "Beginning of week:" msgstr "" -#: src/Module/Settings/Profile/Index.php:83 +#: src/Module/Settings/Profile/Index.php:84 msgid "Profile Name is required." msgstr "" -#: src/Module/Settings/Profile/Index.php:134 +#: src/Module/Settings/Profile/Index.php:132 msgid "Profile couldn't be updated." msgstr "" -#: src/Module/Settings/Profile/Index.php:173 -#: src/Module/Settings/Profile/Index.php:193 +#: src/Module/Settings/Profile/Index.php:171 +#: src/Module/Settings/Profile/Index.php:191 msgid "Label:" msgstr "" -#: src/Module/Settings/Profile/Index.php:174 -#: src/Module/Settings/Profile/Index.php:194 +#: src/Module/Settings/Profile/Index.php:172 +#: src/Module/Settings/Profile/Index.php:192 msgid "Value:" msgstr "" -#: src/Module/Settings/Profile/Index.php:184 -#: src/Module/Settings/Profile/Index.php:204 +#: src/Module/Settings/Profile/Index.php:182 +#: src/Module/Settings/Profile/Index.php:202 msgid "Field Permissions" msgstr "" -#: src/Module/Settings/Profile/Index.php:185 -#: src/Module/Settings/Profile/Index.php:205 +#: src/Module/Settings/Profile/Index.php:183 +#: src/Module/Settings/Profile/Index.php:203 msgid "(click to open/close)" msgstr "" -#: src/Module/Settings/Profile/Index.php:191 +#: src/Module/Settings/Profile/Index.php:189 msgid "Add a new profile field" msgstr "" -#: src/Module/Settings/Profile/Index.php:221 +#: src/Module/Settings/Profile/Index.php:219 msgid "Profile Actions" msgstr "" -#: src/Module/Settings/Profile/Index.php:222 +#: src/Module/Settings/Profile/Index.php:220 msgid "Edit Profile Details" msgstr "" -#: src/Module/Settings/Profile/Index.php:224 +#: src/Module/Settings/Profile/Index.php:222 msgid "Change Profile Photo" msgstr "" -#: src/Module/Settings/Profile/Index.php:229 +#: src/Module/Settings/Profile/Index.php:227 msgid "Profile picture" msgstr "" -#: src/Module/Settings/Profile/Index.php:230 +#: src/Module/Settings/Profile/Index.php:228 msgid "Location" msgstr "" -#: src/Module/Settings/Profile/Index.php:231 src/Util/Temporal.php:93 +#: src/Module/Settings/Profile/Index.php:229 src/Util/Temporal.php:93 #: src/Util/Temporal.php:95 msgid "Miscellaneous" msgstr "" -#: src/Module/Settings/Profile/Index.php:232 +#: src/Module/Settings/Profile/Index.php:230 msgid "Custom Profile Fields" msgstr "" -#: src/Module/Settings/Profile/Index.php:234 src/Module/Welcome.php:58 +#: src/Module/Settings/Profile/Index.php:232 src/Module/Welcome.php:58 msgid "Upload Profile Photo" msgstr "" -#: src/Module/Settings/Profile/Index.php:238 +#: src/Module/Settings/Profile/Index.php:236 msgid "Display name:" msgstr "" -#: src/Module/Settings/Profile/Index.php:241 +#: src/Module/Settings/Profile/Index.php:239 msgid "Street Address:" msgstr "" -#: src/Module/Settings/Profile/Index.php:242 +#: src/Module/Settings/Profile/Index.php:240 msgid "Locality/City:" msgstr "" -#: src/Module/Settings/Profile/Index.php:243 +#: src/Module/Settings/Profile/Index.php:241 msgid "Region/State:" msgstr "" -#: src/Module/Settings/Profile/Index.php:244 +#: src/Module/Settings/Profile/Index.php:242 msgid "Postal/Zip Code:" msgstr "" -#: src/Module/Settings/Profile/Index.php:245 +#: src/Module/Settings/Profile/Index.php:243 msgid "Country:" msgstr "" -#: src/Module/Settings/Profile/Index.php:247 +#: src/Module/Settings/Profile/Index.php:245 msgid "XMPP (Jabber) address:" msgstr "" -#: src/Module/Settings/Profile/Index.php:247 +#: src/Module/Settings/Profile/Index.php:245 msgid "The XMPP address will be published so that people can follow you there." msgstr "" -#: src/Module/Settings/Profile/Index.php:248 +#: src/Module/Settings/Profile/Index.php:246 msgid "Matrix (Element) address:" msgstr "" -#: src/Module/Settings/Profile/Index.php:248 +#: src/Module/Settings/Profile/Index.php:246 msgid "" "The Matrix address will be published so that people can follow you there." msgstr "" -#: src/Module/Settings/Profile/Index.php:249 +#: src/Module/Settings/Profile/Index.php:247 msgid "Homepage URL:" msgstr "" -#: src/Module/Settings/Profile/Index.php:250 +#: src/Module/Settings/Profile/Index.php:248 msgid "Public Keywords:" msgstr "" -#: src/Module/Settings/Profile/Index.php:250 +#: src/Module/Settings/Profile/Index.php:248 msgid "(Used for suggesting potential friends, can be seen by others)" msgstr "" -#: src/Module/Settings/Profile/Index.php:251 +#: src/Module/Settings/Profile/Index.php:249 msgid "Private Keywords:" msgstr "" -#: src/Module/Settings/Profile/Index.php:251 +#: src/Module/Settings/Profile/Index.php:249 msgid "(Used for searching profiles, never shown to others)" msgstr "" -#: src/Module/Settings/Profile/Index.php:252 +#: src/Module/Settings/Profile/Index.php:250 #, php-format msgid "" "

Custom fields appear on your profile page.

\n" @@ -10124,15 +10200,15 @@ msgstr "" msgid "%s created a new post" msgstr "" -#: src/Navigation/Notifications/Factory/Introduction.php:134 +#: src/Navigation/Notifications/Factory/Introduction.php:133 msgid "Friend Suggestion" msgstr "" -#: src/Navigation/Notifications/Factory/Introduction.php:160 +#: src/Navigation/Notifications/Factory/Introduction.php:159 msgid "Friend/Connect Request" msgstr "" -#: src/Navigation/Notifications/Factory/Introduction.php:160 +#: src/Navigation/Notifications/Factory/Introduction.php:159 msgid "New Follower" msgstr "" @@ -10483,82 +10559,6 @@ msgstr "" msgid "The folder view/smarty3/ must be writable by webserver." msgstr "" -#: src/Repository/ProfileField.php:280 -msgid "Hometown:" -msgstr "" - -#: src/Repository/ProfileField.php:281 -msgid "Marital Status:" -msgstr "" - -#: src/Repository/ProfileField.php:282 -msgid "With:" -msgstr "" - -#: src/Repository/ProfileField.php:283 -msgid "Since:" -msgstr "" - -#: src/Repository/ProfileField.php:284 -msgid "Sexual Preference:" -msgstr "" - -#: src/Repository/ProfileField.php:285 -msgid "Political Views:" -msgstr "" - -#: src/Repository/ProfileField.php:286 -msgid "Religious Views:" -msgstr "" - -#: src/Repository/ProfileField.php:287 -msgid "Likes:" -msgstr "" - -#: src/Repository/ProfileField.php:288 -msgid "Dislikes:" -msgstr "" - -#: src/Repository/ProfileField.php:289 -msgid "Title/Description:" -msgstr "" - -#: src/Repository/ProfileField.php:291 -msgid "Musical interests" -msgstr "" - -#: src/Repository/ProfileField.php:292 -msgid "Books, literature" -msgstr "" - -#: src/Repository/ProfileField.php:293 -msgid "Television" -msgstr "" - -#: src/Repository/ProfileField.php:294 -msgid "Film/dance/culture/entertainment" -msgstr "" - -#: src/Repository/ProfileField.php:295 -msgid "Hobbies/Interests" -msgstr "" - -#: src/Repository/ProfileField.php:296 -msgid "Love/romance" -msgstr "" - -#: src/Repository/ProfileField.php:297 -msgid "Work/employment" -msgstr "" - -#: src/Repository/ProfileField.php:298 -msgid "School/education" -msgstr "" - -#: src/Repository/ProfileField.php:299 -msgid "Contact information and Social Networks" -msgstr "" - #: src/Security/Authentication.php:209 msgid "Login failed." msgstr ""