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
This commit is contained in:
Hypolite Petovan 2022-11-30 13:50:52 -05:00
parent 0d53c69610
commit b83526ad0b
16 changed files with 135 additions and 84 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 <hypolite@mrpetovan.com>
*/
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);
}
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,63 @@
<?php
/**
* @copyright Copyright (C) 2010-2022, 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\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.'),
]);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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' => [