Merge pull request #10860 from nupplaphil/feat/depository_profilefield

Move ProfileField to Depository Paradigm
This commit is contained in:
Hypolite Petovan 2021-10-18 18:52:33 -04:00 committed by GitHub
commit 235eab0d99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1519 additions and 644 deletions

View File

@ -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`;

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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

View File

@ -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);

View File

@ -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
*/

View File

@ -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']]);
}
}
}

View File

@ -1,85 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace 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;
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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 = [];

View File

@ -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);

View File

@ -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);
}

View File

@ -0,0 +1,281 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace 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;
}
}

View File

@ -0,0 +1,112 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace 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'));
}
}

View File

@ -0,0 +1,33 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Profile\ProfileField\Exception;
use OutOfBoundsException;
use Throwable;
class ProfileFieldNotFoundException extends OutOfBoundsException
{
public function __construct($message = "", Throwable $previous = null)
{
parent::__construct($message, 404, $previous);
}
}

View File

@ -0,0 +1,32 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Profile\ProfileField\Exception;
use Throwable;
class ProfileFieldPersistenceException extends \RuntimeException
{
public function __construct($message = "", Throwable $previous = null)
{
parent::__construct($message, 500, $previous);
}
}

View File

@ -0,0 +1,26 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Profile\ProfileField\Exception;
class UnexpectedPermissionSetException extends \Exception
{
}

View File

@ -0,0 +1,106 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace 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);
}
}

View File

@ -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'])])];
}
}
}

View File

@ -1,324 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace 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']]);
}
}
}

View File

@ -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;

View File

@ -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)
*

View File

@ -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,
]);
}
}

View File

@ -55,7 +55,7 @@
use Friendica\Database\DBA;
if (!defined('DB_UPDATE_VERSION')) {
define('DB_UPDATE_VERSION', 1439);
define('DB_UPDATE_VERSION', 1440);
}
return [

View File

@ -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`"
],
];

View File

@ -0,0 +1,9 @@
<?php
namespace Friendica\Test\Util;
use Friendica\BaseCollection;
class CollectionDouble extends BaseCollection
{
}

View File

@ -0,0 +1,26 @@
<?php
namespace Friendica\Test\Util;
use Friendica\BaseEntity;
/**
* @property-read string $protString
* @property-read int $protInt
* @property-read \DateTime $protDateTime
*/
class EntityDouble extends BaseEntity
{
protected $protString;
protected $protInt;
protected $protDateTime;
private $privString;
public function __construct(string $protString, int $protInt, \DateTime $protDateTime, string $privString)
{
$this->protString = $protString;
$this->protInt = $protInt;
$this->protDateTime = $protDateTime;
$this->privString = $privString;
}
}

View File

@ -26,6 +26,8 @@ use Friendica\Model\Notification;
return [
// Empty these tables
'profile_field',
'permissionset',
'cache',
'conversation',
'pconfig',

View File

@ -0,0 +1,24 @@
<?php
namespace Friendica\Test\src;
use Friendica\Test\MockedTest;
use Friendica\Test\Util\CollectionDouble;
use Friendica\Test\Util\EntityDouble;
class CollectionTest extends MockedTest
{
/**
* Test if the BaseCollection::column() works as expected
*/
public function testGetArrayCopy()
{
$collection = new CollectionDouble();
$collection->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'));
}
}

View File

@ -0,0 +1,136 @@
<?php
namespace Friendica\Test\src\Profile\ProfileField\Depository;
use Friendica\Profile\ProfileField\Collection\ProfileFields;
use Friendica\Profile\ProfileField\Depository\ProfileField as ProfileFieldDepository;
use Friendica\Profile\ProfileField\Exception\ProfileFieldPersistenceException;
use Friendica\Profile\ProfileField\Factory\ProfileField as ProfileFieldFactory;
use Friendica\Security\PermissionSet\Depository\PermissionSet;
use Friendica\Security\PermissionSet\Factory\PermissionSet as PermissionSetFactory;
use Friendica\Security\PermissionSet\Depository\PermissionSet as PermissionSetDepository;
use Friendica\Test\FixtureTest;
use Friendica\DI;
class ProfileFieldTest extends FixtureTest
{
/** @var ProfileFieldDepository */
private $depository;
/** @var ProfileFieldFactory */
private $factory;
/** @var PermissionSetFactory */
private $permissionSetFactory;
/** @var PermissionSetDepository */
private $permissionSetDepository;
public function setUp(): void
{
parent::setUp();
$this->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);
}
}

View File

@ -0,0 +1,208 @@
<?php
namespace Friendica\Test\src\Profile\ProfileField\Entity;
use Friendica\Profile\ProfileField\Entity\ProfileField;
use Friendica\Profile\ProfileField\Exception\ProfileFieldNotFoundException;
use Friendica\Profile\ProfileField\Exception\UnexpectedPermissionSetException;
use Friendica\Profile\ProfileField\Factory\ProfileField as ProfileFieldFactory;
use Friendica\Security\PermissionSet\Depository\PermissionSet as PermissionSetDepository;
use Friendica\Security\PermissionSet\Factory\PermissionSet as PermissionSetFactory;
use Friendica\Test\MockedTest;
use Friendica\Util\ACLFormatter;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Logger\VoidLogger;
use Mockery\MockInterface;
class ProfileFieldTest extends MockedTest
{
/** @var MockInterface|PermissionSetDepository */
protected $permissionSetDepository;
/** @var ProfileFieldFactory */
protected $profileFieldFactory;
/** @var MockInterface|PermissionSetFactory */
protected $permissionSetFactory;
protected function setUp(): void
{
parent::setUp();
$this->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,
]);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 ""
"<p>Custom fields appear on <a href=\"%s\">your profile page</a>.</p>\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 ""