From b83526ad0b2978bce7ae1ed2909bd5ff3cde2fa4 Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Wed, 30 Nov 2022 13:50:52 -0500 Subject: [PATCH] Tighten profile restriction feature - Prevent feed access to restricted profiles - Rework display of restricted profiles with a redirect to the profile/restricted route - Normalize permission checking with IHandleUserSession->isAuthenticated - Remove unusable "nocache" parameter in feed module because session isn't initialized - Reword setting name and description --- mod/photos.php | 5 +-- src/Model/Event.php | 21 +++------- src/Model/Profile.php | 2 +- src/Module/ActivityPub/Objects.php | 5 ++- src/Module/Calendar/Show.php | 2 +- src/Module/DFRN/Poll.php | 16 +++++++- src/Module/Feed.php | 31 +++++++-------- src/Module/Item/Display.php | 16 ++++---- src/Module/Profile/Photos.php | 4 +- src/Module/Profile/Profile.php | 19 +++------ src/Module/Profile/Restricted.php | 63 ++++++++++++++++++++++++++++++ src/Module/Profile/Status.php | 5 +-- src/Module/Settings/Account.php | 2 +- src/Module/Update/Profile.php | 2 +- src/Protocol/Feed.php | 16 +++----- static/routes.config.php | 10 ++--- 16 files changed, 135 insertions(+), 84 deletions(-) create mode 100644 src/Module/Profile/Restricted.php diff --git a/mod/photos.php b/mod/photos.php index 14eb88ac95..865e0fb318 100644 --- a/mod/photos.php +++ b/mod/photos.php @@ -865,9 +865,8 @@ function photos_content(App $a) $contact = DBA::selectFirst('contact', [], ['id' => $contact_id, 'uid' => $owner_uid, 'blocked' => false, 'pending' => false]); } - if ($user['hidewall'] && (DI::userSession()->getLocalUserId() != $owner_uid) && !$remote_contact) { - DI::sysmsg()->addNotice(DI::l10n()->t('Access to this item is restricted.')); - return; + if ($user['hidewall'] && !DI::userSession()->isAuthenticated()) { + DI::baseUrl()->redirect('profile/' . $user['nickname'] . '/restricted'); } $sql_extra = Security::getPermissionsSQLByUserId($owner_uid); diff --git a/src/Model/Event.php b/src/Model/Event.php index 6f1b29a6c3..82f7702165 100644 --- a/src/Model/Event.php +++ b/src/Model/Event.php @@ -281,7 +281,7 @@ class Event if (!DBA::isResult($existing_event)) { return 0; } - + if ($existing_event['edited'] === $event['edited']) { return $event['id']; } @@ -501,29 +501,20 @@ class Event * Additionally, it can check if the owner array is selectable * * @param string $nickname - * @param bool $check * * @return array the owner array * @throws NotFoundException The given nickname does not exist * @throws UnauthorizedException The access for the given nickname is restricted */ - public static function getOwnerForNickname(string $nickname, bool $check = true): array + public static function getOwnerForNickname(string $nickname): array { $owner = User::getOwnerDataByNick($nickname); - if (empty($owner)) { + if (empty($owner) || $owner['account_removed'] || $owner['account_expired']) { throw new NotFoundException(DI::l10n()->t('User not found.')); } - if ($check) { - $contact_id = DI::userSession()->getRemoteContactID($owner['uid']); - - $remote_contact = $contact_id && DBA::exists('contact', ['id' => $contact_id, 'uid' => $owner['uid']]); - - $is_owner = DI::userSession()->getLocalUserId() == $owner['uid']; - - if ($owner['hidewall'] && !$is_owner && !$remote_contact) { - throw new UnauthorizedException(DI::l10n()->t('Access to this profile has been restricted.')); - } + if ($owner['hidewall'] && !DI::userSession()->isAuthenticated()) { + throw new UnauthorizedException(DI::l10n()->t('Access to this profile has been restricted.')); } return $owner; @@ -541,7 +532,7 @@ class Event public static function getByIdAndUid(int $owner_uid, int $event_id, string $nickname = null): array { if (!empty($nickname)) { - $owner = static::getOwnerForNickname($nickname, true); + $owner = static::getOwnerForNickname($nickname); $owner_uid = $owner['uid']; // get the permissions diff --git a/src/Model/Profile.php b/src/Model/Profile.php index dcd39a30c1..26395cb4d1 100644 --- a/src/Model/Profile.php +++ b/src/Model/Profile.php @@ -461,7 +461,7 @@ class Profile '$unfollow' => DI::l10n()->t('Unfollow'), '$unfollow_link' => $unfollow_link, '$subscribe_feed' => DI::l10n()->t('Atom feed'), - '$subscribe_feed_link' => $profile['poll'], + '$subscribe_feed_link' => $profile['hidewall'] ? '' : $profile['poll'], '$wallmessage' => DI::l10n()->t('Message'), '$wallmessage_link' => $wallmessage_link, '$account_type' => $account_type, diff --git a/src/Module/ActivityPub/Objects.php b/src/Module/ActivityPub/Objects.php index e2328b5e58..8c8109d667 100644 --- a/src/Module/ActivityPub/Objects.php +++ b/src/Module/ActivityPub/Objects.php @@ -29,6 +29,7 @@ use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\Item; use Friendica\Model\Post; +use Friendica\Model\User; use Friendica\Network\HTTPException; use Friendica\Protocol\ActivityPub; use Friendica\Util\HTTPSignature; @@ -74,7 +75,9 @@ class Objects extends BaseModule throw new HTTPException\NotFoundException(); } - $validated = in_array($item['private'], [Item::PUBLIC, Item::UNLISTED]); + $owner = User::getById($item['uid'], ['hidewall']); + + $validated = empty($owner['hidewall']) && in_array($item['private'], [Item::PUBLIC, Item::UNLISTED]); if (!$validated) { $requester = HTTPSignature::getSigner('', $_SERVER); diff --git a/src/Module/Calendar/Show.php b/src/Module/Calendar/Show.php index 9c5c9c4526..da0e285a91 100644 --- a/src/Module/Calendar/Show.php +++ b/src/Module/Calendar/Show.php @@ -91,7 +91,7 @@ class Show extends BaseModule $this->page['aside'] .= Widget\CalendarExport::getHTML($this->session->getLocalUserId()); } else { - $owner = Event::getOwnerForNickname($this->parameters['nickname'], true); + $owner = Event::getOwnerForNickname($this->parameters['nickname']); Nav::setSelected('calendar'); diff --git a/src/Module/DFRN/Poll.php b/src/Module/DFRN/Poll.php index e41dafed56..1562ce0de2 100644 --- a/src/Module/DFRN/Poll.php +++ b/src/Module/DFRN/Poll.php @@ -23,7 +23,9 @@ namespace Friendica\Module\DFRN; use Friendica\BaseModule; use Friendica\Core\System; +use Friendica\Model\User; use Friendica\Module\Response; +use Friendica\Network\HTTPException; use Friendica\Protocol\OStatus; /** @@ -33,7 +35,19 @@ class Poll extends BaseModule { protected function rawContent(array $request = []) { + $owner = User::getByNickname( + $this->parameters['nickname'] ?? '', + ['nickname', 'blocked', 'account_expired', 'account_removed', 'hidewall'] + ); + if (!$owner || $owner['account_expired'] || $owner['account_removed']) { + throw new HTTPException\NotFoundException($this->t('User not found.')); + } + + if ($owner['blocked'] || $owner['hidewall']) { + throw new HTTPException\UnauthorizedException($this->t('Access to this profile has been restricted.')); + } + $last_update = $request['last_update'] ?? ''; - System::httpExit(OStatus::feed($this->parameters['nickname'], $last_update, 10) ?? '', Response::TYPE_ATOM); + System::httpExit(OStatus::feed($owner['nickname'], $last_update, 10) ?? '', Response::TYPE_ATOM); } } diff --git a/src/Module/Feed.php b/src/Module/Feed.php index b24ccbc946..59714af387 100644 --- a/src/Module/Feed.php +++ b/src/Module/Feed.php @@ -23,9 +23,9 @@ namespace Friendica\Module; use Friendica\BaseModule; use Friendica\Core\System; -use Friendica\DI; -use Friendica\Protocol\Feed as ProtocolFeed; +use Friendica\Model\User; use Friendica\Network\HTTPException; +use Friendica\Protocol\Feed as ProtocolFeed; /** * Provides public Atom feeds @@ -37,23 +37,14 @@ use Friendica\Network\HTTPException; * - /feed/[nickname]/replies => comments * - /feed/[nickname]/activity => activity * - * The nocache GET parameter is provided mainly for debug purposes, requires auth - * * @author Hypolite Petovan */ class Feed extends BaseModule { protected function rawContent(array $request = []) { - $last_update = $this->getRequestValue($request, 'last_update', ''); - $nocache = !empty($request['nocache']) && DI::userSession()->getLocalUserId(); - - $type = null; - // @TODO: Replace with parameter from router - if (DI::args()->getArgc() > 2) { - $type = DI::args()->getArgv()[2]; - } - + $nick = $this->parameters['nickname'] ?? ''; + $type = $this->parameters['type'] ?? null; switch ($type) { case 'posts': case 'comments': @@ -67,11 +58,19 @@ class Feed extends BaseModule $type = 'posts'; } - $feed = ProtocolFeed::atom($this->parameters['nickname'], $last_update, 10, $type, $nocache, true); - if (empty($feed)) { - throw new HTTPException\NotFoundException(DI::l10n()->t('User not found.')); + $last_update = $this->getRequestValue($request, 'last_update', ''); + + $owner = User::getOwnerDataByNick($nick); + if (!$owner || $owner['account_expired'] || $owner['account_removed']) { + throw new HTTPException\NotFoundException($this->t('User not found.')); } + if ($owner['blocked'] || $owner['hidewall']) { + throw new HTTPException\UnauthorizedException($this->t('Access to this profile has been restricted.')); + } + + $feed = ProtocolFeed::atom($owner, $last_update, 10, $type); + System::httpExit($feed, Response::TYPE_ATOM); } } diff --git a/src/Module/Item/Display.php b/src/Module/Item/Display.php index df500f7ccb..01bf9a5587 100644 --- a/src/Module/Item/Display.php +++ b/src/Module/Item/Display.php @@ -196,8 +196,7 @@ class Display extends BaseModule protected function getDisplayData(array $item, bool $update = false, int $updateUid = 0, bool $force = false): string { - $isRemoteContact = false; - $itemUid = $this->session->getLocalUserId(); + $itemUid = $this->session->getLocalUserId(); $parent = null; if (!$this->session->getLocalUserId() && !empty($item['parent-uri-id'])) { @@ -206,8 +205,7 @@ class Display extends BaseModule if (!empty($parent)) { $pageUid = $parent['uid']; - $isRemoteContact = $this->session->getRemoteContactID($pageUid); - if ($isRemoteContact) { + if ($this->session->getRemoteContactID($pageUid)) { $itemUid = $parent['uid']; } } else { @@ -215,13 +213,11 @@ class Display extends BaseModule } if (!empty($pageUid) && ($pageUid != $this->session->getLocalUserId())) { - $page_user = User::getById($pageUid, ['hidewall']); + $page_user = User::getById($pageUid, ['nickname', 'hidewall']); } - $is_owner = $this->session->getLocalUserId() && (in_array($pageUid, [$this->session->getLocalUserId(), 0])); - - if (!empty($page_user['hidewall']) && !$is_owner && !$isRemoteContact) { - throw new HTTPException\ForbiddenException($this->t('Access to this profile has been restricted.')); + if (!empty($page_user['hidewall']) && !$this->session->isAuthenticated()) { + $this->baseUrl->redirect('profile/' . $page_user['nickname'] . '/restricted'); } $sql_extra = Item::getPermissionsSQLByUserId($pageUid); @@ -275,6 +271,8 @@ class Display extends BaseModule $output = ''; + $is_owner = $this->session->getLocalUserId() && (in_array($pageUid, [$this->session->getLocalUserId(), 0])); + // We need the editor here to be able to reshare an item. if ($is_owner && !$update) { $output .= $this->conversation->statusEditor([], 0, true); diff --git a/src/Module/Profile/Photos.php b/src/Module/Profile/Photos.php index 50af6f0e96..940c1257b2 100644 --- a/src/Module/Profile/Photos.php +++ b/src/Module/Profile/Photos.php @@ -88,8 +88,8 @@ class Photos extends \Friendica\Module\BaseProfile $remote_contact = $contact && !$contact['blocked'] && !$contact['pending']; } - if ($owner['hidewall'] && !$is_owner && !$remote_contact) { - throw new HttpException\ForbiddenException($this->t('Access to this item is restricted.')); + if ($owner['hidewall'] && !$this->session->isAuthenticated()) { + $this->baseUrl->redirect('profile/' . $owner['nickname'] . '/restricted'); } $this->session->set('photo_return', $this->args->getCommand()); diff --git a/src/Module/Profile/Profile.php b/src/Module/Profile/Profile.php index 07db082591..553e9c5231 100644 --- a/src/Module/Profile/Profile.php +++ b/src/Module/Profile/Profile.php @@ -76,21 +76,19 @@ class Profile extends BaseProfile { $a = DI::app(); - $profile = ProfileModel::load($a, $this->parameters['nickname']); + $profile = ProfileModel::load($a, $this->parameters['nickname'] ?? ''); if (!$profile) { throw new HTTPException\NotFoundException(DI::l10n()->t('Profile not found.')); } $remote_contact_id = DI::userSession()->getRemoteContactID($profile['uid']); - if (DI::config()->get('system', 'block_public') && !DI::userSession()->getLocalUserId() && !$remote_contact_id) { + if (DI::config()->get('system', 'block_public') && !DI::userSession()->isAuthenticated()) { return Login::form(); } - $is_owner = DI::userSession()->getLocalUserId() == $profile['uid']; - - if (!empty($profile['hidewall']) && !$is_owner && !$remote_contact_id) { - throw new HTTPException\ForbiddenException(DI::l10n()->t('Access to this profile has been restricted.')); + if (!empty($profile['hidewall']) && !DI::userSession()->isAuthenticated()) { + $this->baseUrl->redirect('profile/' . $profile['nickname'] . '/restricted'); } if (!empty($profile['page-flags']) && $profile['page-flags'] == User::PAGE_FLAGS_COMMUNITY) { @@ -104,11 +102,6 @@ class Profile extends BaseProfile $is_owner = DI::userSession()->getLocalUserId() == $profile['uid']; $o = self::getTabsHTML($a, 'profile', $is_owner, $profile['nickname'], $profile['hide-friends']); - if (!empty($profile['hidewall']) && !$is_owner && !$remote_contact_id) { - DI::sysmsg()->addNotice(DI::l10n()->t('Access to this profile has been restricted.')); - return ''; - } - $view_as_contacts = []; $view_as_contact_id = 0; $view_as_contact_alert = ''; @@ -307,8 +300,8 @@ class Profile extends BaseProfile } // site block - $blocked = !DI::userSession()->getLocalUserId() && !$remote_contact_id && DI::config()->get('system', 'block_public'); - $userblock = !DI::userSession()->getLocalUserId() && !$remote_contact_id && $profile['hidewall']; + $blocked = !DI::userSession()->isAuthenticated() && DI::config()->get('system', 'block_public'); + $userblock = !DI::userSession()->isAuthenticated() && $profile['hidewall']; if (!$blocked && !$userblock) { $keywords = str_replace(['#', ',', ' ', ',,'], ['', ' ', ',', ','], $profile['pub_keywords'] ?? ''); if (strlen($keywords)) { diff --git a/src/Module/Profile/Restricted.php b/src/Module/Profile/Restricted.php new file mode 100644 index 0000000000..31ce69fbd4 --- /dev/null +++ b/src/Module/Profile/Restricted.php @@ -0,0 +1,63 @@ +. + * + */ + +namespace Friendica\Module\Profile; + +use Friendica\App; +use Friendica\BaseModule; +use Friendica\Core\L10n; +use Friendica\Core\Renderer; +use Friendica\Model\Profile; +use Friendica\Module\Response; +use Friendica\Network\HTTPException; +use Friendica\Util\Profiler; +use Psr\Log\LoggerInterface; + +class Restricted extends BaseModule +{ + /** @var App */ + private $app; + + public function __construct(App $app, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = []) + { + parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); + + $this->app = $app; + } + + protected function content(array $request = []): string + { + $profile = Profile::load($this->app, $this->parameters['nickname'] ?? '', false); + if (!$profile) { + throw new HTTPException\NotFoundException($this->t('Profile not found.')); + } + + if (empty($profile['hidewall'])) { + $this->baseUrl->redirect('profile/' . $profile['nickname']); + } + + $tpl = Renderer::getMarkupTemplate('exception.tpl'); + return Renderer::replaceMacros($tpl, [ + '$title' => $this->t('Restricted profile'), + '$message' => $this->t('This profile has been restricted which prevents access to their public content from anonymous visitors.'), + ]); + } +} diff --git a/src/Module/Profile/Status.php b/src/Module/Profile/Status.php index 6543ea1952..4b5e567551 100644 --- a/src/Module/Profile/Status.php +++ b/src/Module/Profile/Status.php @@ -105,9 +105,8 @@ class Status extends BaseProfile $is_owner = DI::userSession()->getLocalUserId() == $profile['uid']; $last_updated_key = "profile:" . $profile['uid'] . ":" . DI::userSession()->getLocalUserId() . ":" . $remote_contact; - if (!empty($profile['hidewall']) && !$is_owner && !$remote_contact) { - DI::sysmsg()->addNotice(DI::l10n()->t('Access to this profile has been restricted.')); - return ''; + if (!empty($profile['hidewall']) && !DI::userSession()->isAuthenticated()) { + $this->baseUrl->redirect('profile/' . $profile['nickname'] . '/restricted'); } $o .= self::getTabsHTML($a, 'status', $is_owner, $profile['nickname'], $profile['hide-friends']); diff --git a/src/Module/Settings/Account.php b/src/Module/Settings/Account.php index 4217b1e88d..8768115ec3 100644 --- a/src/Module/Settings/Account.php +++ b/src/Module/Settings/Account.php @@ -586,7 +586,7 @@ class Account extends BaseSettings '$profile_in_dir' => $profile_in_dir, '$profile_in_net_dir' => ['profile_in_netdirectory', DI::l10n()->t('Allow your profile to be searchable globally?'), $profile['net-publish'], DI::l10n()->t("Activate this setting if you want others to easily find and follow you. Your profile will be searchable on remote systems. This setting also determines whether Friendica will inform search engines that your profile should be indexed or not.") . $net_pub_desc], '$hide_friends' => ['hide-friends', DI::l10n()->t('Hide your contact/friend list from viewers of your profile?'), $profile['hide-friends'], DI::l10n()->t('A list of your contacts is displayed on your profile page. Activate this option to disable the display of your contact list.')], - '$hide_wall' => ['hidewall', DI::l10n()->t('Hide your profile details from anonymous viewers?'), $user['hidewall'], DI::l10n()->t('Anonymous visitors will only see your profile picture, your display name and the nickname you are using on your profile page. Your public posts and replies will still be accessible by other means.')], + '$hide_wall' => ['hidewall', $this->t('Hide your public content from anonymous viewers'), $user['hidewall'], $this->t('Anonymous visitors will only see your basic profile details. Your public posts and replies will still be freely accessible on the remote servers of your followers and through relays.')], '$unlisted' => ['unlisted', DI::l10n()->t('Make public posts unlisted'), DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'unlisted'), DI::l10n()->t('Your public posts will not appear on the community pages or in search results, nor be sent to relay servers. However they can still appear on public feeds on remote servers.')], '$accessiblephotos' => ['accessible-photos', DI::l10n()->t('Make all posted pictures accessible'), DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'accessible-photos'), DI::l10n()->t("This option makes every posted picture accessible via the direct link. This is a workaround for the problem that most other networks can't handle permissions on pictures. Non public pictures still won't be visible for the public on your photo albums though.")], '$blockwall' => ['blockwall', DI::l10n()->t('Allow friends to post to your profile page?'), (intval($user['blockwall']) ? '0' : '1'), DI::l10n()->t('Your contacts may write posts on your profile wall. These posts will be distributed to your contacts')], diff --git a/src/Module/Update/Profile.php b/src/Module/Update/Profile.php index be15820e45..c1b7522e5c 100644 --- a/src/Module/Update/Profile.php +++ b/src/Module/Update/Profile.php @@ -49,7 +49,7 @@ class Profile extends BaseModule $is_owner = DI::userSession()->getLocalUserId() == $a->getProfileOwner(); $last_updated_key = "profile:" . $a->getProfileOwner() . ":" . DI::userSession()->getLocalUserId() . ":" . $remote_contact; - if (!$is_owner && !$remote_contact) { + if (!DI::userSession()->isAuthenticated()) { $user = User::getById($a->getProfileOwner(), ['hidewall']); if ($user['hidewall']) { throw new ForbiddenException(DI::l10n()->t('Access to this profile has been restricted.')); diff --git a/src/Protocol/Feed.php b/src/Protocol/Feed.php index 7c6a68b601..3dac91e849 100644 --- a/src/Protocol/Feed.php +++ b/src/Protocol/Feed.php @@ -40,6 +40,7 @@ use Friendica\Model\Item; use Friendica\Model\Post; use Friendica\Model\Tag; use Friendica\Model\User; +use Friendica\Network\HTTPException; use Friendica\Util\DateTimeFormat; use Friendica\Util\Network; use Friendica\Util\ParseUrl; @@ -915,28 +916,23 @@ class Feed * Updates the provided last_update parameter if the result comes from the * cache or it is empty * - * @param string $owner_nick Nickname of the feed owner + * @param array $owner owner-view record of the feed owner * @param string $last_update Date of the last update * @param integer $max_items Number of maximum items to fetch * @param string $filter Feed items filter (activity, posts or comments) * @param boolean $nocache Wether to bypass caching * * @return string Atom feed - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function atom(string $owner_nick, string $last_update, int $max_items = 300, string $filter = 'activity', bool $nocache = false) + public static function atom(array $owner, string $last_update, int $max_items = 300, string $filter = 'activity', bool $nocache = false) { $stamp = microtime(true); - $owner = User::getOwnerDataByNick($owner_nick); - if (!$owner) { - return; - } + $cachekey = 'feed:feed:' . $owner['nickname'] . ':' . $filter . ':' . $last_update; - $cachekey = 'feed:feed:' . $owner_nick . ':' . $filter . ':' . $last_update; - - // Display events in the users's timezone + // Display events in the user's timezone if (strlen($owner['timezone'])) { DI::app()->setTimeZone($owner['timezone']); } diff --git a/static/routes.config.php b/static/routes.config.php index 8d1fec6f0e..b4ce8f79ed 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -38,6 +38,7 @@ $profileRoutes = [ '/photos' => [Module\Profile\Photos::class, [R::GET ]], '/profile' => [Module\Profile\Profile::class, [R::GET]], '/remote_follow' => [Module\Profile\RemoteFollow::class, [R::GET, R::POST]], + '/restricted' => [Module\Profile\Restricted::class, [R::GET ]], '/schedule' => [Module\Profile\Schedule::class, [R::GET, R::POST]], '/status[/{category}[/{date1}[/{date2}]]]' => [Module\Profile\Status::class, [R::GET]], '/unkmail' => [Module\Profile\UnkMail::class, [R::GET, R::POST]], @@ -416,13 +417,8 @@ return [ '/featured/{nickname}' => [Module\ActivityPub\Featured::class, [R::GET]], - '/feed' => [ - '/{nickname}' => [Module\Feed::class, [R::GET]], - '/{nickname}/posts' => [Module\Feed::class, [R::GET]], - '/{nickname}/comments' => [Module\Feed::class, [R::GET]], - '/{nickname}/replies' => [Module\Feed::class, [R::GET]], - '/{nickname}/activity' => [Module\Feed::class, [R::GET]], - ], + '/feed/{nickname}[/{type:posts|comments|replies|activity}]' => [Module\Feed::class, [R::GET]], + '/feedtest' => [Module\Debug\Feed::class, [R::GET]], '/fetch' => [