diff --git a/src/BaseDepository.php b/src/BaseDepository.php index 18cca9d30..69bd93eef 100644 --- a/src/BaseDepository.php +++ b/src/BaseDepository.php @@ -13,7 +13,7 @@ use Psr\Log\LoggerInterface; * Depositories are meant to store and retrieve Entities from the database. * * The reason why there are methods prefixed with an underscore is because PHP doesn't support generic polymorphism - * which means we can't direcly overload base methods and make parameters more strict (from a parent class to a child + * which means we can't directly overload base methods and make parameters more strict (from a parent class to a child * class for example) * * Similarly, we can't make an overloaded method return type more strict until we only support PHP version 7.4 but this diff --git a/src/Capabilities/ICanCreateFromTableRow.php b/src/Capabilities/ICanCreateFromTableRow.php index bdb6d662d..ad6053beb 100644 --- a/src/Capabilities/ICanCreateFromTableRow.php +++ b/src/Capabilities/ICanCreateFromTableRow.php @@ -7,7 +7,7 @@ use Friendica\BaseEntity; interface ICanCreateFromTableRow { /** - * Returns the correcponding Entity given a table row record + * Returns the corresponding Entity given a table row record * * @param array $row * @return BaseEntity diff --git a/src/Collection/PermissionSets.php b/src/Collection/PermissionSets.php deleted file mode 100644 index 66dad1734..000000000 --- a/src/Collection/PermissionSets.php +++ /dev/null @@ -1,29 +0,0 @@ -. - * - */ - -namespace Friendica\Collection; - -use Friendica\BaseCollection; - -class PermissionSets extends BaseCollection -{ - -} diff --git a/src/Core/UserImport.php b/src/Core/UserImport.php index f0d8f762e..c0725bee8 100644 --- a/src/Core/UserImport.php +++ b/src/Core/UserImport.php @@ -21,14 +21,12 @@ namespace Friendica\Core; -use Friendica\App; use Friendica\Database\DBA; use Friendica\Database\DBStructure; use Friendica\DI; -use Friendica\Model\Contact; use Friendica\Model\Photo; use Friendica\Object\Image; -use Friendica\Repository\PermissionSet; +use Friendica\Security\PermissionSet\Depository\PermissionSet; use Friendica\Util\Strings; use Friendica\Worker\Delivery; @@ -283,16 +281,13 @@ class UserImport DI::profileField()->migrateFromLegacyProfile($profile); } - ///@TODO Replace with permissionset import - $self_contact = Contact::selectFirst(['id'], ['uid' => $newuid, 'self' => true]); - $allow_cid = DI::aclFormatter()->toString($self_contact['id']); - $self_psid = DI::permissionSet()->getIdFromACL($newuid, $allow_cid); + $permissionSet = DI::permissionSet()->selectDefaultForUser($newuid); foreach ($account['profile_fields'] ?? [] as $profile_field) { $profile_field['uid'] = $newuid; ///@TODO Replace with permissionset import - $profile_field['psid'] = $profile_field['psid'] ? $self_psid : PermissionSet::PUBLIC; + $profile_field['psid'] = $profile_field['psid'] ? $permissionSet->uid : PermissionSet::PUBLIC; if (self::dbImportAssoc('profile_field', $profile_field) === false) { Logger::info("uimport:insert profile field " . $profile_field['id'] . " : ERROR : " . DBA::errorMessage()); diff --git a/src/DI.php b/src/DI.php index ea7310fee..9025feb47 100644 --- a/src/DI.php +++ b/src/DI.php @@ -442,12 +442,14 @@ abstract class DI return self::$dice->create(Repository\Introduction::class); } - /** - * @return Repository\PermissionSet - */ - public static function permissionSet() + public static function permissionSet(): Security\PermissionSet\Depository\PermissionSet { - return self::$dice->create(Repository\PermissionSet::class); + return self::$dice->create(Security\PermissionSet\Depository\PermissionSet::class); + } + + public static function permissionSetFactory(): Security\PermissionSet\Factory\PermissionSet + { + return self::$dice->create(Security\PermissionSet\Factory\PermissionSet::class); } /** diff --git a/src/Factory/Api/Mastodon/Account.php b/src/Factory/Api/Mastodon/Account.php index 85b3d4316..1d067352a 100644 --- a/src/Factory/Api/Mastodon/Account.php +++ b/src/Factory/Api/Mastodon/Account.php @@ -27,8 +27,8 @@ use Friendica\Collection\Api\Mastodon\Fields; use Friendica\Model\APContact; use Friendica\Model\Contact; use Friendica\Network\HTTPException; -use Friendica\Repository\PermissionSet; use Friendica\Repository\ProfileField; +use Friendica\Security\PermissionSet\Depository\PermissionSet; use ImagickException; use Psr\Log\LoggerInterface; diff --git a/src/Model/Item.php b/src/Model/Item.php index db1d08888..285187e5f 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -980,13 +980,14 @@ class Item } // Creates or assigns the permission set - $item['psid'] = PermissionSet::getIdFromACL( - $item['uid'], - $item['allow_cid'], - $item['allow_gid'], - $item['deny_cid'], - $item['deny_gid'] - ); + $item['psid'] = DI::permissionSet()->selectOrCreate( + DI::permissionSetFactory()->createFromString( + $item['uid'], + $item['allow_cid'], + $item['allow_gid'], + $item['deny_cid'], + $item['deny_gid'] + ))->id; if (!empty($item['extid'])) { $item['external-id'] = ItemURI::getIdByURI($item['extid']); @@ -1952,18 +1953,19 @@ class Item $private = self::PUBLIC; } - $psid = PermissionSet::getIdFromACL( - $user['uid'], - $user['allow_cid'], - $user['allow_gid'], - $user['deny_cid'], - $user['deny_gid'] - ); + $permissionSet = DI::permissionSet()->selectOrCreate( + DI::permissionSetFactory()->createFromString( + $user['uid'], + $user['allow_cid'], + $user['allow_gid'], + $user['deny_cid'], + $user['deny_gid'] + )); $forum_mode = ($prvgroup ? 2 : 1); $fields = ['wall' => true, 'origin' => true, 'forum_mode' => $forum_mode, 'contact-id' => $self['id'], - 'owner-id' => $owner_id, 'private' => $private, 'psid' => $psid]; + 'owner-id' => $owner_id, 'private' => $private, 'psid' => $permissionSet->id]; self::update($fields, ['id' => $item['id']]); Worker::add(['priority' => PRIORITY_HIGH, 'dont_fork' => true], 'Notifier', Delivery::POST, (int)$item['uri-id'], (int)$item['uid']); @@ -2549,12 +2551,12 @@ class Item $condition = []; } elseif ($remote_user) { // Authenticated visitor - fetch the matching permissionsets - $set = PermissionSet::get($owner_id, $remote_user); + $permissionSets = DI::permissionSet()->selectByContactId($remote_user, $owner_id); if (!empty($set)) { $condition = ["(`private` != ? OR (`private` = ? AND `wall` AND `psid` IN (" . implode(', ', array_fill(0, count($set), '?')) . ")))", self::PRIVATE, self::PRIVATE]; - $condition = array_merge($condition, $set); + $condition = array_merge($condition, $permissionSets->column('id')); } } @@ -2595,10 +2597,10 @@ class Item * If pre-verified, the caller is expected to have already * done this and passed the groups into this function. */ - $set = PermissionSet::get($owner_id, $remote_user); + $permissionSets = DI::permissionSet()->selectByContactId($remote_user, $owner_id); if (!empty($set)) { - $sql_set = sprintf(" OR (" . $table . "`private` = %d AND " . $table . "`wall` AND " . $table . "`psid` IN (", self::PRIVATE) . implode(',', $set) . "))"; + $sql_set = sprintf(" OR (" . $table . "`private` = %d AND " . $table . "`wall` AND " . $table . "`psid` IN (", self::PRIVATE) . implode(',', $permissionSets->column('id')) . "))"; } else { $sql_set = ''; } diff --git a/src/Model/PermissionSet.php b/src/Model/PermissionSet.php deleted file mode 100644 index 9138d46b7..000000000 --- a/src/Model/PermissionSet.php +++ /dev/null @@ -1,78 +0,0 @@ -. - * - */ - -namespace Friendica\Model; - -use Friendica\BaseModel; -use Friendica\DI; - -/** - * functions for interacting with the permission set of an object (item, photo, event, ...) - * - * @property int uid - * @property string allow_cid - * @property string allow_gid - * @property string deny_cid - * @property string deny_gid - */ -class PermissionSet extends BaseModel -{ - /** - * Fetch the id of a given permission set. Generate a new one when needed - * - * @param int $uid - * @param string|null $allow_cid Allowed contact IDs - empty = everyone - * @param string|null $allow_gid Allowed group IDs - empty = everyone - * @param string|null $deny_cid Disallowed contact IDs - empty = no one - * @param string|null $deny_gid Disallowed group IDs - empty = no one - * @return int id - * @throws \Exception - * @deprecated since 2020.03, use Repository\PermissionSet instead - * @see \Friendica\Repository\PermissionSet->getIdFromACL - */ - public static function getIdFromACL( - int $uid, - string $allow_cid = null, - string $allow_gid = null, - string $deny_cid = null, - string $deny_gid = null - ) { - return DI::permissionSet()->getIdFromACL($uid, $allow_cid, $allow_gid, $deny_cid, $deny_gid); - } - - /** - * Returns a permission set for a given contact - * - * @param integer $uid User id whom the items belong - * @param integer $contact_id Contact id of the visitor - * - * @return array of permission set ids. - * @throws \Exception - * @deprecated since 2020.03, use Repository\PermissionSet instead - * @see \Friendica\Repository\PermissionSet->selectByContactId - */ - public static function get($uid, $contact_id) - { - $permissionSets = DI::permissionSet()->selectByContactId($contact_id, $uid); - - return $permissionSets->column('id'); - } -} diff --git a/src/Model/ProfileField.php b/src/Model/ProfileField.php index 3fc33b2c8..66833890e 100644 --- a/src/Model/ProfileField.php +++ b/src/Model/ProfileField.php @@ -23,7 +23,9 @@ namespace Friendica\Model; use Friendica\BaseModel; use Friendica\Database\Database; -use Friendica\Network\HTTPException; +use Friendica\Network\HTTPException\NotFoundException; +use Friendica\Security\PermissionSet\Depository\PermissionSet as PermissionSetDepository; +use Friendica\Security\PermissionSet\Entity\PermissionSet; use Psr\Log\LoggerInterface; /** @@ -39,21 +41,21 @@ use Psr\Log\LoggerInterface; * @property string value * @property string created * @property string edited - * @property PermissionSet permissionset + * @property PermissionSet permissionSet */ class ProfileField extends BaseModel { /** @var PermissionSet */ - private $permissionset; + private $permissionSet; - /** @var \Friendica\Repository\PermissionSet */ - private $permissionSetRepository; + /** @var PermissionSetDepository */ + private $permissionSetDepository; - public function __construct(Database $dba, LoggerInterface $logger, \Friendica\Repository\PermissionSet $permissionSetRepository, array $data = []) + public function __construct(Database $dba, LoggerInterface $logger, PermissionSetDepository $permissionSetDepository, array $data = []) { parent::__construct($dba, $logger, $data); - $this->permissionSetRepository = $permissionSetRepository; + $this->permissionSetDepository = $permissionSetDepository; } public function __get($name) @@ -61,12 +63,17 @@ class ProfileField extends BaseModel $this->checkValid(); switch ($name) { - case 'permissionset': - $this->permissionset = - $this->permissionset ?? - $this->permissionSetRepository->selectFirst(['id' => $this->psid, 'uid' => $this->uid]); + case 'permissionSet': + if (empty($this->permissionSet)) { + $permissionSet = $this->permissionSetDepository->selectOneById($this->psid); + 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)); + } - $return = $this->permissionset; + $this->permissionSet = $permissionSet; + } + + $return = $this->permissionSet; break; default: $return = parent::__get($name); diff --git a/src/Module/ActivityPub/Objects.php b/src/Module/ActivityPub/Objects.php index f597b0905..7e2ceb8b0 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()->getIdFromACL($item['uid'], '', '', '', '')]); + [DI::permissionSet()->selectEmptyForUser($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 9058ba521..b1d4314b7 100644 --- a/src/Module/Api/Friendica/Profile/Show.php +++ b/src/Module/Api/Friendica/Profile/Show.php @@ -28,7 +28,7 @@ use Friendica\Model\Contact; use Friendica\Model\Profile; use Friendica\Module\BaseApi; use Friendica\Network\HTTPException; -use Friendica\Repository\PermissionSet; +use Friendica\Security\PermissionSet\Depository\PermissionSet; /** * API endpoint: /api/friendica/profile/show diff --git a/src/Module/PermissionTooltip.php b/src/Module/PermissionTooltip.php index b9429fce2..3f23032d6 100644 --- a/src/Module/PermissionTooltip.php +++ b/src/Module/PermissionTooltip.php @@ -39,7 +39,7 @@ class PermissionTooltip extends \Friendica\BaseModule } if (isset($model['psid'])) { - $permissionSet = DI::permissionSet()->selectFirst(['id' => $model['psid']]); + $permissionSet = DI::permissionSet()->selectOneById($model['psid']); $model['allow_cid'] = $permissionSet->allow_cid; $model['allow_gid'] = $permissionSet->allow_gid; $model['deny_cid'] = $permissionSet->deny_cid; @@ -61,12 +61,10 @@ class PermissionTooltip extends \Friendica\BaseModule exit; } - $aclFormatter = DI::aclFormatter(); - - $allowed_users = $aclFormatter->expand($model['allow_cid']); - $allowed_groups = $aclFormatter->expand($model['allow_gid']); - $deny_users = $aclFormatter->expand($model['deny_cid']); - $deny_groups = $aclFormatter->expand($model['deny_gid']); + $allowed_users = $model['allow_cid']; + $allowed_groups = $model['allow_gid']; + $deny_users = $model['deny_cid']; + $deny_groups = $model['deny_gid']; $o = DI::l10n()->t('Visible to:') . '
'; $l = []; diff --git a/src/Module/Profile/Status.php b/src/Module/Profile/Status.php index 8235c3265..d7e4ca671 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()->getIdFromACL($profile['uid'], '', '', '', '')])]; + [DI::permissionSet()->selectEmptyForUser($profile['uid'])])]; } } elseif ($profile['uid'] == local_user()) { $condition = []; diff --git a/src/Module/Settings/Profile/Index.php b/src/Module/Settings/Profile/Index.php index b850a8273..c51393c98 100644 --- a/src/Module/Settings/Profile/Index.php +++ b/src/Module/Settings/Profile/Index.php @@ -28,6 +28,7 @@ use Friendica\Core\Renderer; use Friendica\Core\Theme; use Friendica\Database\DBA; use Friendica\DI; +use Friendica\Model\Contact; use Friendica\Model\Profile; use Friendica\Model\ProfileField; use Friendica\Model\User; @@ -161,7 +162,9 @@ class Index extends BaseSettings $profileFields = DI::profileField()->selectByUserId(local_user()); foreach ($profileFields as $profileField) { /** @var ProfileField $profileField */ - $defaultPermissions = ACL::getDefaultUserPermissions($profileField->permissionset->toArray()); + $defaultPermissions = $profileField->permissionSet->withAllowedContacts( + Contact::pruneUnavailable($profileField->permissionSet->allow_cid) + ); $custom_fields[] = [ 'id' => $profileField->id, @@ -173,7 +176,7 @@ class Index extends BaseSettings DI::page(), $a->getLoggedInUserId(), false, - $defaultPermissions, + $defaultPermissions->toArray(), ['network' => Protocol::DFRN], 'profile_field[' . $profileField->id . ']' ), diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index 5c9142a5d..9bf846c28 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -242,7 +242,7 @@ class Transmitter $permissionSets = DI::permissionSet()->selectByContactId($requester_id, $owner['uid']); if (!empty($permissionSets)) { $condition = ['psid' => array_merge($permissionSets->column('id'), - [DI::permissionSet()->getIdFromACL($owner['uid'], '', '', '', '')])]; + [DI::permissionSet()->selectEmptyForUser($owner['uid'])])]; } } } diff --git a/src/Repository/PermissionSet.php b/src/Repository/PermissionSet.php deleted file mode 100644 index 828247fb4..000000000 --- a/src/Repository/PermissionSet.php +++ /dev/null @@ -1,197 +0,0 @@ -. - * - */ - -namespace Friendica\Repository; - -use Friendica\BaseRepository; -use Friendica\Collection; -use Friendica\Database\Database; -use Friendica\Model; -use Friendica\Network\HTTPException; -use Friendica\Util\ACLFormatter; -use Psr\Log\LoggerInterface; - -class PermissionSet extends BaseRepository -{ - /** @var int Virtual permission set id for public permission */ - const PUBLIC = 0; - - protected static $table_name = 'permissionset'; - - protected static $model_class = Model\PermissionSet::class; - - protected static $collection_class = Collection\PermissionSets::class; - - /** @var ACLFormatter */ - private $aclFormatter; - - public function __construct(Database $dba, LoggerInterface $logger, ACLFormatter $aclFormatter) - { - parent::__construct($dba, $logger); - - $this->aclFormatter = $aclFormatter; - } - - /** - * @param array $data - * @return Model\PermissionSet - */ - protected function create(array $data) - { - return new Model\PermissionSet($this->dba, $this->logger, $data); - } - - /** - * @param array $condition - * @return Model\PermissionSet - * @throws \Friendica\Network\HTTPException\NotFoundException - */ - public function selectFirst(array $condition) - { - if (isset($condition['id']) && !$condition['id']) { - return $this->create([ - 'id' => self::PUBLIC, - 'uid' => $condition['uid'] ?? 0, - 'allow_cid' => '', - 'allow_gid' => '', - 'deny_cid' => '', - 'deny_gid' => '', - ]); - } - - return parent::selectFirst($condition); - } - - /** - * @param array $condition - * @param array $params - * @return Collection\PermissionSets - * @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\PermissionSets - * @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); - } - - /** - * Fetch the id of a given permission set. Generate a new one when needed - * - * @param int $uid - * @param string|null $allow_cid Allowed contact IDs - empty = everyone - * @param string|null $allow_gid Allowed group IDs - empty = everyone - * @param string|null $deny_cid Disallowed contact IDs - empty = no one - * @param string|null $deny_gid Disallowed group IDs - empty = no one - * @return int id - * @throws \Exception - */ - public function getIdFromACL( - int $uid, - string $allow_cid = null, - string $allow_gid = null, - string $deny_cid = null, - string $deny_gid = null - ) { - $allow_cid = $this->aclFormatter->sanitize($allow_cid); - $allow_gid = $this->aclFormatter->sanitize($allow_gid); - $deny_cid = $this->aclFormatter->sanitize($deny_cid); - $deny_gid = $this->aclFormatter->sanitize($deny_gid); - - // Public permission - if (!$allow_cid && !$allow_gid && !$deny_cid && !$deny_gid) { - return self::PUBLIC; - } - - $condition = [ - 'uid' => $uid, - 'allow_cid' => $allow_cid, - 'allow_gid' => $allow_gid, - 'deny_cid' => $deny_cid, - 'deny_gid' => $deny_gid - ]; - - try { - $permissionset = $this->selectFirst($condition); - } catch(HTTPException\NotFoundException $exception) { - $permissionset = $this->insert($condition); - } - - return $permissionset->id; - } - - /** - * Returns a permission set collection for a given contact - * - * @param integer $contact_id Contact id of the visitor - * @param integer $uid User id whom the items belong, used for ownership check. - * - * @return Collection\PermissionSets - * @throws \Exception - */ - public function selectByContactId($contact_id, $uid) - { - $cdata = Model\Contact::getPublicAndUserContactID($contact_id, $uid); - if (!empty($cdata)) { - $public_contact_str = '<' . $cdata['public'] . '>'; - $user_contact_str = '<' . $cdata['user'] . '>'; - $contact_id = $cdata['user']; - } else { - $public_contact_str = '<' . $contact_id . '>'; - $user_contact_str = ''; - } - - $groups = []; - if (!empty($user_contact_str) && $this->dba->exists('contact', ['id' => $contact_id, 'uid' => $uid, 'blocked' => false])) { - $groups = Model\Group::getIdsByContactId($contact_id); - } - - $group_str = '<<>>'; // should be impossible to match - foreach ($groups as $group_id) { - $group_str .= '|<' . preg_quote($group_id) . '>'; - } - - if (!empty($user_contact_str)) { - $condition = ["`uid` = ? AND (NOT (`deny_cid` REGEXP ? OR `deny_cid` REGEXP ? OR deny_gid REGEXP ?) - AND (allow_cid REGEXP ? OR allow_cid REGEXP ? OR allow_gid REGEXP ? OR (allow_cid = '' AND allow_gid = '')))", - $uid, $user_contact_str, $public_contact_str, $group_str, - $user_contact_str, $public_contact_str, $group_str]; - } else { - $condition = ["`uid` = ? AND (NOT (`deny_cid` REGEXP ? OR deny_gid REGEXP ?) - AND (allow_cid REGEXP ? OR allow_gid REGEXP ? OR (allow_cid = '' AND allow_gid = '')))", - $uid, $public_contact_str, $group_str, $public_contact_str, $group_str]; - } - - return $this->select($condition); - } -} diff --git a/src/Repository/ProfileField.php b/src/Repository/ProfileField.php index bf7ac69d6..40417be48 100644 --- a/src/Repository/ProfileField.php +++ b/src/Repository/ProfileField.php @@ -28,7 +28,7 @@ use Friendica\Core\L10n; use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\Model; -use Friendica\Util\ACLFormatter; +use Friendica\Security\PermissionSet\Depository\PermissionSet; use Friendica\Util\DateTimeFormat; use Psr\Log\LoggerInterface; @@ -42,18 +42,18 @@ class ProfileField extends BaseRepository /** @var PermissionSet */ private $permissionSet; - /** @var ACLFormatter */ - private $aclFormatter; + /** @var \Friendica\Security\PermissionSet\Factory\PermissionSet */ + private $permissionSetFactory; /** @var L10n */ private $l10n; - public function __construct(Database $dba, LoggerInterface $logger, PermissionSet $permissionSet, ACLFormatter $aclFormatter, L10n $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->aclFormatter = $aclFormatter; - $this->l10n = $l10n; + $this->permissionSet = $permissionSet; + $this->permissionSetFactory = $permissionSetFactory; + $this->l10n = $l10n; } /** @@ -160,7 +160,7 @@ class ProfileField extends BaseRepository return parent::update($model); } - + /** * @param int $uid User Id * @param Collection\ProfileFields $profileFields Collection of existing profile fields @@ -176,24 +176,24 @@ class ProfileField extends BaseRepository // Creation of the new field if (!empty($profileFieldInputs['new']['label'])) { - $psid = $this->permissionSet->getIdFromACL( + $psid = $this->permissionSet->selectOrCreate($this->permissionSetFactory->createFromString( $uid, - $this->aclFormatter->toString($profileFieldInputs['new']['contact_allow'] ?? ''), - $this->aclFormatter->toString($profileFieldInputs['new']['group_allow'] ?? ''), - $this->aclFormatter->toString($profileFieldInputs['new']['contact_deny'] ?? ''), - $this->aclFormatter->toString($profileFieldInputs['new']['group_deny'] ?? '') - ); + $profileFieldInputs['new']['contact_allow'] ?? '', + $profileFieldInputs['new']['group_allow'] ?? '', + $profileFieldInputs['new']['contact_deny'] ?? '', + $profileFieldInputs['new']['group_deny'] ?? '' + ))->id; $newProfileField = $this->insert([ - 'uid' => $uid, + 'uid' => $uid, 'label' => $profileFieldInputs['new']['label'], 'value' => $profileFieldInputs['new']['value'], - 'psid' => $psid, + 'psid' => $psid, 'order' => $profileFieldOrder['new'], ]); $profileFieldInputs[$newProfileField->id] = $profileFieldInputs['new']; - $profileFieldOrder[$newProfileField->id] = $profileFieldOrder['new']; + $profileFieldOrder[$newProfileField->id] = $profileFieldOrder['new']; $profileFields[] = $newProfileField; } @@ -220,15 +220,15 @@ class ProfileField extends BaseRepository // 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->getIdFromACL( + $psid = $this->permissionSet->selectOrCreate($this->permissionSetFactory->createFromString( $uid, - $this->aclFormatter->toString($profileFieldInputs[$profileField->id]['contact_allow'] ?? ''), - $this->aclFormatter->toString($profileFieldInputs[$profileField->id]['group_allow'] ?? ''), - $this->aclFormatter->toString($profileFieldInputs[$profileField->id]['contact_deny'] ?? ''), - $this->aclFormatter->toString($profileFieldInputs[$profileField->id]['group_deny'] ?? '') - ); + $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->psid = $psid; $profileField->label = $profileFieldInputs[$profileField->id]['label']; $profileField->value = $profileFieldInputs[$profileField->id]['value']; $profileField->order = $profileFieldOrder[$profileField->id]; @@ -257,17 +257,22 @@ class ProfileField extends BaseRepository 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]); } - - $allow_cid = $this->aclFormatter->toString(array_column($contacts, 'id')); } - $psid = $this->permissionSet->getIdFromACL($profile['uid'], $allow_cid ?? ''); + $psid = $this->permissionSet->selectOrCreate( + new \Friendica\Security\PermissionSet\Entity\PermissionSet( + $profile['uid'], + array_column($contacts, 'id') ?? [] + ) + )->id; $order = 1; @@ -297,8 +302,8 @@ class ProfileField extends BaseRepository 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, + 'uid' => $profile['uid'], + 'psid' => $psid, 'order' => $order++, 'label' => trim($label, ':'), 'value' => $profile[$field], @@ -310,7 +315,7 @@ class ProfileField extends BaseRepository if ($profile['is-default']) { $profile['profile-name'] = null; - $profile['is-default'] = 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/Collection/PermissionSets.php b/src/Security/PermissionSet/Collection/PermissionSets.php new file mode 100644 index 000000000..c15b23a68 --- /dev/null +++ b/src/Security/PermissionSet/Collection/PermissionSets.php @@ -0,0 +1,9 @@ +. + * + */ + +namespace Friendica\Security\PermissionSet\Depository; + +use Exception; +use Friendica\BaseDepository; +use Friendica\Database\Database; +use Friendica\Model\Contact; +use Friendica\Model\Group; +use Friendica\Network\HTTPException\NotFoundException; +use Friendica\Security\PermissionSet\Factory; +use Friendica\Security\PermissionSet\Collection; +use Friendica\Security\PermissionSet\Entity; +use Friendica\Util\ACLFormatter; +use Psr\Log\LoggerInterface; + +class PermissionSet extends BaseDepository +{ + /** @var int Virtual permission set id for public permission */ + const PUBLIC = 0; + + /** @var Factory\PermissionSet */ + protected $factory; + + protected static $table_name = 'permissionset'; + + /** @var ACLFormatter */ + private $aclFormatter; + + public function __construct(Database $database, LoggerInterface $logger, Factory\PermissionSet $factory, ACLFormatter $aclFormatter) + { + parent::__construct($database, $logger, $factory); + + $this->aclFormatter = $aclFormatter; + } + + /** + * @param array $condition + * @param array $params + * + * @return Entity\PermissionSet + * @throws NotFoundException + */ + private function selectOne(array $condition, array $params = []): Entity\PermissionSet + { + return parent::_selectOne($condition, $params); + } + + private function select(array $condition, array $params = []): Collection\PermissionSets + { + return new Collection\PermissionSets(parent::_select($condition, $params)->getArrayCopy()); + } + + /** + * Converts a given PermissionSet into a DB compatible row array + * + * @param Entity\PermissionSet $permissionSet + * + * @return array + */ + protected function convertToTableRow(Entity\PermissionSet $permissionSet): array + { + return [ + 'uid' => $permissionSet->uid, + 'allow_cid' => $this->aclFormatter->toString($permissionSet->allow_cid), + 'allow_gid' => $this->aclFormatter->toString($permissionSet->allow_gid), + 'deny_cid' => $this->aclFormatter->toString($permissionSet->deny_cid), + 'deny_gid' => $this->aclFormatter->toString($permissionSet->deny_gid), + ]; + } + + /** + * @param int $id + * + * @return Entity\PermissionSet + * @throws NotFoundException + */ + public function selectOneById(int $id): Entity\PermissionSet + { + return $this->selectOne(['id' => $id]); + } + + /** + * Returns a permission set collection for a given contact + * + * @param int $cid Contact id of the visitor + * @param int $uid User id whom the items belong, used for ownership check. + * + * @return Collection\PermissionSets + */ + public function selectByContactId(int $cid, int $uid): Collection\PermissionSets + { + $cdata = Contact::getPublicAndUserContactID($cid, $uid); + if (!empty($cdata)) { + $public_contact_str = $this->aclFormatter->toString($cdata['public']); + $user_contact_str = $this->aclFormatter->toString($cdata['user']); + $cid = $cdata['user']; + } else { + $public_contact_str = $this->aclFormatter->toString($cid); + $user_contact_str = ''; + } + + $groups = []; + if (!empty($user_contact_str) && $this->db->exists('contact', [ + 'id' => $cid, + 'uid' => $uid, + 'blocked' => false + ])) { + $groups = Group::getIdsByContactId($cid); + } + + $group_str = '<<>>'; // should be impossible to match + foreach ($groups as $group_id) { + $group_str .= '|<' . preg_quote($group_id) . '>'; + } + + if (!empty($user_contact_str)) { + $condition = ["`uid` = ? AND (NOT (`deny_cid` REGEXP ? OR `deny_cid` REGEXP ? OR deny_gid REGEXP ?) + AND (allow_cid REGEXP ? OR allow_cid REGEXP ? OR allow_gid REGEXP ? OR (allow_cid = '' AND allow_gid = '')))", + $uid, $user_contact_str, $public_contact_str, $group_str, + $user_contact_str, $public_contact_str, $group_str]; + } else { + $condition = ["`uid` = ? AND (NOT (`deny_cid` REGEXP ? OR deny_gid REGEXP ?) + AND (allow_cid REGEXP ? OR allow_gid REGEXP ? OR (allow_cid = '' AND allow_gid = '')))", + $uid, $public_contact_str, $group_str, $public_contact_str, $group_str]; + } + + return $this->select($condition); + } + + /** + * Fetch the default PermissionSet for a given user, create it if it doesn't exist + * + * @param int $uid + * + * @return Entity\PermissionSet + * @throws Exception + */ + public function selectDefaultForUser(int $uid): Entity\PermissionSet + { + $self_contact = Contact::selectFirst(['id'], ['uid' => $uid, 'self' => true]); + + return $this->selectOrCreate($this->factory->createFromString( + $uid, + $this->aclFormatter->toString($self_contact['id']) + )); + } + + /** + * Fetch the empty PermissionSet for a given user, create it if it doesn't exist + * + * @param int $uid + * + * @return Entity\PermissionSet + */ + public function selectEmptyForUser(int $uid): Entity\PermissionSet + { + return $this->selectOrCreate($this->factory->createFromString($uid)); + } + + /** + * Selects or creates a PermissionSet based on it's fields + * + * @param Entity\PermissionSet $permissionSet + * + * @return Entity\PermissionSet + */ + public function selectOrCreate(Entity\PermissionSet $permissionSet): Entity\PermissionSet + { + if ($permissionSet->id) { + return $permissionSet; + } + + try { + return $this->selectOne($this->convertToTableRow($permissionSet)); + } catch (NotFoundException $exception) { + return $this->save($permissionSet); + } + } + + public function save(Entity\PermissionSet $permissionSet): Entity\PermissionSet + { + $fields = $this->convertToTableRow($permissionSet); + + if ($permissionSet->id) { + $this->db->update(self::$table_name, $fields, ['id' => $permissionSet->id]); + } else { + $this->db->insert(self::$table_name, $fields); + + $permissionSet = $this->selectOneById($this->db->lastInsertId()); + } + + return $permissionSet; + } +} diff --git a/src/Security/PermissionSet/Entity/PermissionSet.php b/src/Security/PermissionSet/Entity/PermissionSet.php new file mode 100644 index 000000000..849871647 --- /dev/null +++ b/src/Security/PermissionSet/Entity/PermissionSet.php @@ -0,0 +1,66 @@ +id = $id; + $this->uid = $uid; + $this->allow_cid = $allow_cid; + $this->allow_gid = $allow_gid; + $this->deny_cid = $deny_cid; + $this->deny_gid = $deny_gid; + } + + /** + * Creates a new Entity with a new allowed_cid list (wipes the id because it isn't the same entity anymore) + * + * @param array $allow_cid + * + * @return $this + */ + public function withAllowedContacts(array $allow_cid): PermissionSet + { + $clone = clone $this; + + $clone->allow_cid = $allow_cid; + $clone->id = null; + + return $clone; + } +} diff --git a/src/Security/PermissionSet/Factory/PermissionSet.php b/src/Security/PermissionSet/Factory/PermissionSet.php new file mode 100644 index 000000000..84c7d5838 --- /dev/null +++ b/src/Security/PermissionSet/Factory/PermissionSet.php @@ -0,0 +1,64 @@ +formatter = $formatter; + } + + /** + * @inheritDoc + */ + public function createFromTableRow(array $row): Entity\PermissionSet + { + return new Entity\PermissionSet( + $row['uid'], + $this->formatter->expand($row['allow_cid'] ?? ''), + $this->formatter->expand($row['allow_gid'] ?? ''), + $this->formatter->expand($row['deny_cid'] ?? ''), + $this->formatter->expand($row['deny_gid'] ?? ''), + $row['id'] ?? null + ); + } + + /** + * Creates a new PermissionSet based on it's fields + * + * @param int $uid + * @param string $allow_cid + * @param string $allow_gid + * @param string $deny_cid + * @param string $deny_gid + * + * @return Entity\PermissionSet + */ + public function createFromString( + int $uid, + string $allow_cid = '', + string $allow_gid = '', + string $deny_cid = '', + string $deny_gid = ''): Entity\PermissionSet + { + return $this->createFromTableRow([ + 'uid' => $uid, + 'allow_cid' => $allow_cid, + 'allow_gid' => $allow_gid, + 'deny_cid' => $deny_cid, + 'deny_gid' => $deny_gid, + ]); + } +} diff --git a/tests/src/Security/PermissionSet/Entity/PermissionSetTest.php b/tests/src/Security/PermissionSet/Entity/PermissionSetTest.php new file mode 100644 index 000000000..e404694d4 --- /dev/null +++ b/tests/src/Security/PermissionSet/Entity/PermissionSetTest.php @@ -0,0 +1,45 @@ + [ + 'id' => 10, + 'allow_cid' => ['1', '2'], + 'allow_gid' => ['3', '4'], + 'deny_cid' => ['5', '6', '7'], + 'deny_gid' => ['8'], + 'update_cid' => ['10'], + ], + ]; + } + + /** + * Test if the call "withAllowedContacts()" creates a clone + * + * @dataProvider dateAllowedContacts + */ + public function testWithAllowedContacts(int $id, array $allow_cid, array $allow_gid, array $deny_cid, array $deny_gid, array $update_cid) + { + $permissionSetOrig = new PermissionSet( + $id, + $allow_cid, + $allow_gid, + $deny_cid, + $deny_gid + ); + + $permissionSetNew = $permissionSetOrig->withAllowedContacts($update_cid); + + self::assertNotSame($permissionSetOrig, $permissionSetNew); + self::assertEquals($update_cid, $permissionSetNew->allow_cid); + self::assertEquals($allow_cid, $permissionSetOrig->allow_cid); + } +} diff --git a/tests/src/Security/PermissionSet/Factory/PermissionSetTest.php b/tests/src/Security/PermissionSet/Factory/PermissionSetTest.php new file mode 100644 index 000000000..1cb1c16ad --- /dev/null +++ b/tests/src/Security/PermissionSet/Factory/PermissionSetTest.php @@ -0,0 +1,134 @@ +permissionSet = new PermissionSet(new NullLogger(), new ACLFormatter()); + } + + public function dataInput() + { + return [ + 'new' => [ + 'input' => [ + 'uid' => 12, + 'allow_cid' => '<1>,<2>', + 'allow_gid' => '<3>,<4>', + 'deny_cid' => '<6>', + 'deny_gid' => '<8>', + ], + 'assertion' => [ + 'id' => null, + 'uid' => 12, + 'allow_cid' => ['1', '2'], + 'allow_gid' => ['3', '4'], + 'deny_cid' => ['6'], + 'deny_gid' => ['8'], + ], + ], + 'full' => [ + 'input' => [ + 'id' => 3, + 'uid' => 12, + 'allow_cid' => '<1>,<2>', + 'allow_gid' => '<3>,<4>', + 'deny_cid' => '<6>', + 'deny_gid' => '<8>', + ], + 'assertion' => [ + 'id' => 3, + 'uid' => 12, + 'allow_cid' => ['1', '2'], + 'allow_gid' => ['3', '4'], + 'deny_cid' => ['6'], + 'deny_gid' => ['8'], + ], + ], + 'mini' => [ + 'input' => [ + 'id' => null, + 'uid' => 12, + ], + 'assertion' => [ + 'id' => null, + 'uid' => 12, + 'allow_cid' => [], + 'allow_gid' => [], + 'deny_cid' => [], + 'deny_gid' => [], + ], + ], + 'wrong' => [ + 'input' => [ + 'id' => 3, + 'uid' => 12, + 'allow_cid' => '<1,<2>', + ], + 'assertion' => [ + 'id' => 3, + 'uid' => 12, + 'allow_cid' => ['2'], + 'allow_gid' => [], + 'deny_cid' => [], + 'deny_gid' => [], + ], + ] + ]; + } + + protected function assertPermissionSet(\Friendica\Security\PermissionSet\Entity\PermissionSet $permissionSet, array $assertion) + { + self::assertEquals($assertion['id'] ?? null, $permissionSet->id); + self::assertNotNull($permissionSet->uid); + self::assertEquals($assertion['uid'], $permissionSet->uid); + self::assertEquals($assertion['allow_cid'], $permissionSet->allow_cid); + self::assertEquals($assertion['allow_gid'], $permissionSet->allow_gid); + self::assertEquals($assertion['deny_cid'], $permissionSet->deny_cid); + self::assertEquals($assertion['deny_gid'], $permissionSet->deny_gid); + } + + /** + * Test the createFromTableRow method + * + * @dataProvider dataInput + */ + public function testCreateFromTableRow(array $input, array $assertion) + { + $permissionSet = $this->permissionSet->createFromTableRow($input); + + $this->assertPermissionSet($permissionSet, $assertion); + } + + /** + * Test the createFromString method + * + * @dataProvider dataInput + */ + public function testCreateFromString(array $input, array $assertion) + { + $permissionSet = $this->permissionSet->createFromString( + $input['uid'], + $input['allow_cid'] ?? '', + $input['allow_gid'] ?? '', + $input['deny_cid'] ?? '', + $input['deny_gid'] ?? '' + ); + + unset($assertion['id']); + + $this->assertPermissionSet($permissionSet, $assertion); + } +}