Skip to content

Commit

Permalink
Merge pull request #3783 from Spuds/LinkRefer
Browse files Browse the repository at this point in the history
Link refer
  • Loading branch information
Spuds committed Apr 5, 2024
2 parents 8e83898 + 23cc73a commit e156b5f
Show file tree
Hide file tree
Showing 16 changed files with 191 additions and 32 deletions.
9 changes: 2 additions & 7 deletions sources/ElkArte/AdminController/ManageEditor.php
Expand Up @@ -30,8 +30,8 @@ class ManageEditor extends AbstractController
*
* What it does:
*
* - This method is the entry point for index.php?action=admin;area=postsettings;sa=editor
* and it calls a function based on the sub-action, here only display.
* - This method is the entry point for index.php?action=admin;area=editor
* - It calls a function based on the sub-action, here only display.
* - requires admin_forum permissions
*
* @event integrate_sa_manage_editor Used to add more sub actions
Expand Down Expand Up @@ -145,11 +145,6 @@ private function _settings()
array('text', 'giphyApiKey', 40, 'subtext' => $txt['giphyApiURL']),
array('select', 'giphyRating', ['g' => 'G', 'pg' => 'PG', 'pg13' => 'PG13', 'r' => 'R']),
array('text', 'giphyLanguage', 5, 'subtext' => $txt['giphyLanguageURL']),

array('title', 'mods_cat_modifications_misc'),
array('check', 'autoLinkUrls'), // @todo not editor or bbc
array('check', 'enablePostHTML'),
array('check', 'enablePostMarkdown'),
);

// Add new settings with a nice hook, makes them available for admin settings search as well
Expand Down
17 changes: 15 additions & 2 deletions sources/ElkArte/AdminController/ManagePosts.php
Expand Up @@ -234,6 +234,11 @@ public function action_postSettings_display()
// Initialize the form
$settingsForm = new SettingsForm(SettingsForm::DB_ADAPTER);

if (!empty($modSettings['nofollow_allowlist']))
{
$modSettings['nofollow_allowlist'] = implode("\n", (array) json_decode($modSettings['nofollow_allowlist'], true));
}

// Initialize it with our settings
$settingsForm->setConfigVars($this->_settings());

Expand Down Expand Up @@ -264,7 +269,6 @@ public function action_postSettings_display()
{
throw new Exception('convert_to_mediumtext', false, array(getUrl('admin', ['action' => 'admin', 'area' => 'maintain', 'sa' => 'database'])));
}

}

// If we're changing the post preview length let's check its valid
Expand All @@ -279,6 +283,10 @@ public function action_postSettings_display()
$this->_req->post->heightBeforeShowMore = max((int) $this->_req->post->heightBeforeShowMore, 155);
}

$allowList = array_unique(explode("\n", $this->_req->post['nofollow_allowlist']));
$allowList = array_filter(array_map('\ElkArte\Helper\Util::htmlspecialchars', array_map('trim', $allowList)));
$this->_req->post['nofollow_allowlist'] = json_encode($allowList);

call_integration_hook('integrate_save_post_settings');

$settingsForm->setConfigValues((array) $this->_req->post);
Expand Down Expand Up @@ -321,16 +329,21 @@ private function _settings()
['int', 'spamWaitTime', 'postinput' => $txt['manageposts_seconds']],
['int', 'edit_wait_time', 'postinput' => $txt['manageposts_seconds']],
['int', 'edit_disable_time', 'subtext' => $txt['edit_disable_time_zero'], 'postinput' => $txt['manageposts_minutes']],
'',
['check', 'show_modify'],
'',
['check', 'show_user_images'],
['check', 'hide_post_group'],
'',
// First & Last message preview lengths
['select', 'message_index_preview', [$txt['message_index_preview_off'], $txt['message_index_preview_first'], $txt['message_index_preview_last']]],
['int', 'preview_characters', 'subtext' => $txt['preview_characters_zero'], 'postinput' => $txt['preview_characters_units']],
// Misc
['title', 'mods_cat_modifications_misc'],
['check', 'enableCodePrettify'],
['check', 'autoLinkUrls'],
['large_text', 'nofollow_allowlist', 'subtext' => $txt['nofollow_allowlist_desc']],
['check', 'enablePostHTML'],
['check', 'enablePostMarkdown'],
];

// Add new settings with a nice hook, makes them available for admin settings search as well
Expand Down
2 changes: 1 addition & 1 deletion sources/ElkArte/BBC/Autolink.php
Expand Up @@ -77,7 +77,7 @@ protected function load()
//'[url_auto=$1]$1[/url_auto]',
//'[url_auto=$1]$1[/url_auto]',
'[url]$1[/url]',
'[url=http://$1]$1[/url]',
'[url=https://$1]$1[/url]',
);

$search_email = array(
Expand Down
56 changes: 49 additions & 7 deletions sources/ElkArte/BBC/Codes.php
Expand Up @@ -154,7 +154,7 @@ class Codes
/** a regular expression to validate and match the value. */
public const PARAM_ATTR_MATCH = 0;

/** true if the value should be quoted. */
/** @var int Constant that represents if the value should be quoted. */
public const PARAM_ATTR_QUOTED = 1;

/** callback to evaluate on the data, which is $data. */
Expand All @@ -166,16 +166,16 @@ class Codes
/** true if the parameter is optional. */
public const PARAM_ATTR_OPTIONAL = 4;

/** */
/** @var int Constant that represents no trimming. */
public const TRIM_NONE = 0;

/** */
/** @var int Constant that represents trimming inside of a tag. */
public const TRIM_INSIDE = 1;

/** */
/** @var int Constant that represents trimming outside of a tag. */
public const TRIM_OUTSIDE = 2;

/** */
/** @var int Constant that represents trimming both the left and right sides of a string. */
public const TRIM_BOTH = 3;

// These are mainly for *ATTR_QUOTED since there are 3 options
Expand All @@ -185,6 +185,9 @@ class Codes

public const REQUIRED = 1;

/** @var string can be used to build tags in a ATTR_VALIDATE function and consumed in ATTR_CONTEXT */
public static $contentTag;

/** An array of self::ATTR_*
ATTR_TAG and ATTR_TYPE are required for every tag.
The rest of the attributes depend on the type and other options. */
Expand Down Expand Up @@ -826,9 +829,13 @@ public function getDefault()
array(
self::ATTR_TAG => 'url',
self::ATTR_TYPE => self::TYPE_UNPARSED_CONTENT,
self::ATTR_CONTENT => '<a href="$1" class="bbc_link" target="_blank" rel="noopener noreferrer">$1</a>',
self::ATTR_CONTENT => &self::$contentTag,
self::ATTR_VALIDATE => static function (&$data) {
$data = addProtocol($data);

self::$contentTag = '<a href="$1" class="bbc_link" target="_blank"';
self::$contentTag .= validateURLAllowList($data) ? ' rel="noopener ugc">' : ' rel="noopener noreferrer nofollow ugc">';
self::$contentTag .= '$1</a>';
},
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => false,
Expand All @@ -837,10 +844,13 @@ public function getDefault()
array(
self::ATTR_TAG => 'url',
self::ATTR_TYPE => self::TYPE_UNPARSED_EQUALS,
self::ATTR_BEFORE => '<a href="$1" class="bbc_link" target="_blank" rel="noopener noreferrer">',
self::ATTR_BEFORE => &self::$contentTag,
self::ATTR_AFTER => '</a>',
self::ATTR_VALIDATE => static function (&$data) {
$data = addProtocol($data);

self::$contentTag = '<a href="$1" class="bbc_link" target="_blank"';
self::$contentTag .= validateURLAllowList($data) ? ' rel="noopener ugc">' : ' rel="noopener noreferrer nofollow ugc">';
},
self::ATTR_DISALLOW_CHILDREN => array(
'email' => 1,
Expand All @@ -852,6 +862,38 @@ public function getDefault()
self::ATTR_AUTOLINK => false,
self::ATTR_LENGTH => 3,
),
array(
self::ATTR_TAG => 'url',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<a href="{url}" class="bbc_link" target="_blank" rel="{follow}">',
self::ATTR_AFTER => '</a>',
self::ATTR_PARAM => array(
'url' => array(
self::PARAM_ATTR_MATCH => '([^\s\]]+\]?)',
// preparse will check the domain allowList
self::PARAM_ATTR_VALIDATE => static function($param) {
return addProtocol($param);
}
),
'follow' => array(
self::PARAM_ATTR_MATCH => '([^\s\]]+\]?)',
self::PARAM_ATTR_VALIDATE => static function($param) {
// preparse will validate permissions
$on = in_array($param, ['follow', 'true', 'on', 'yes'], true);
return ($on) ? 'noopener ugc' : 'noopener noreferrer nofollow ugc';
}
),
),
self::ATTR_DISALLOW_CHILDREN => array(
'email' => 1,
'url' => 1,
'iurl' => 1,
),
self::ATTR_DISABLED_AFTER => ' ($1)',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => false,
self::ATTR_LENGTH => 3
),
));
}

Expand Down
47 changes: 46 additions & 1 deletion sources/ElkArte/BBC/PreparseCode.php
Expand Up @@ -60,7 +60,7 @@ protected function __construct($user_name)
* What it does:
* - Cleans up links (javascript, etc.)
* - Fixes improperly constructed lists [lists]
* - Repairs improperly constructed tables, row, headers, etc
* - Repairs improperly constructed tables, row, headers, etc.
* - Protects code sections
* - Checks for proper quote open / closing
* - Processes /me tag
Expand Down Expand Up @@ -134,6 +134,9 @@ public function preparsecode(&$message, $previewing = false)
$this->message = preg_replace_callback('~\[font=([^]]*)](.*?(?:\[/font\]))~s',
fn($matches) => $this->_preparsecode_font_callback($matches), $this->message);

// Don't allow rel follow links if they don't have permissions
$this->_validateLinks();

// Allow integration to do further processing on protected code block message
call_integration_hook('integrate_preparse_tokenized_code', array(&$this->message, $previewing, $this->code_blocks));

Expand Down Expand Up @@ -770,6 +773,48 @@ private function _preparseTable()
}
}

/**
* Validates bbc code URL of the form: [url url=123.com follow=true]123[/url]
*
* - Modifies if the user does not have the post_nofollow permission
* - Checks if the domain is on the allowList and modifies as required
*/
private function _validateLinks()
{
$allowed = allowedTo('post_nofollow');
$regexFollow = '~\[url[^]]*(follow=([^] \s]+))[^]]*]~';
$regexUrl = '~\[url[^]]*(url=([^] \s]+))[^]]*]~';

preg_match_all($regexFollow, $this->message, $matches);
if (isset($matches[1]) && is_array($matches[1]))
{
// Every [URL} code with follow= in them
foreach ($matches[1] as $key => $followTerm)
{
// Flush out the actual URL and follow value
preg_match($regexUrl, $matches[0][$key], $match);
$allowedDomain = validateURLAllowList(addProtocol($match[2]));
$followChoice = in_array(trim($matches[2][$key]), ['follow', 'true', 'on', 'yes'], true);

// Allowed domain and purposely turning it off?
if ($allowedDomain && $allowed && !$followChoice)
{
$this->message = str_replace($followTerm, 'follow=false', $this->message);
}
// Allowed domain OR you are allowed and already have it on
elseif ($allowedDomain || ($allowed && $followChoice))
{
$this->message = str_replace($followTerm, 'follow=true', $this->message);
}
// Not allowed to use the function and the domain is not on the allowList
else
{
$this->message = str_replace($followTerm, 'follow=false', $this->message);
}
}
}
}

/**
* This is very simple, and just removes things done by preparsecode.
*
Expand Down
4 changes: 3 additions & 1 deletion sources/ElkArte/Languages/Admin/English.php
Expand Up @@ -652,7 +652,6 @@
$txt['show_user_images'] = 'Show user avatars in message view.';
$txt['hide_post_group'] = 'Hide post group titles for grouped members.';


$txt['enableBBC'] = 'Enable bulletin board code (BBC)';
$txt['enablePostHTML'] = 'Enable basic HTML in posts';
$txt['enablePostMarkdown'] = 'Enable basic Markdown in posts';
Expand All @@ -671,6 +670,9 @@
$txt['giphyLanguageURL'] = '<a href="https://developers.giphy.com/docs/optional-settings/#language-support">Language Codes</a>';
$txt['giphyApiURL'] = '<a href="https://support.giphy.com/hc/en-us/articles/360020283431-Request-A-GIPHY-API-Key">Request API Key</a>';

$txt['nofollow_allowlist'] = 'Allow listed domains without nofollow attribute';
$txt['nofollow_allowlist_desc'] = 'The domains listed here (one per line) will always be presented without the nofollow attribute';

$txt['enableParticipation'] = 'Enable participation icons';
$txt['enableFollowup'] = 'Enable followups';
$txt['enable_unwatch'] = 'Enable unwatching of topics';
Expand Down
2 changes: 2 additions & 0 deletions sources/ElkArte/Languages/Errors/English.php
Expand Up @@ -120,6 +120,8 @@
$txt['cannot_split_any'] = 'Splitting just any topic is not allowed in this board.';
$txt['cannot_view_attachments'] = 'It seems that you are not allowed to download or view attachments on this board.';
$txt['cannot_view_mlist'] = 'You can\'t view the member list because you don\'t have permission to.';
$txt['cannot_post_nofollow'] = 'You can\'t post URLs without a "nofollow" attribute.';

$txt['cannot_view_stats'] = 'You aren\'t allowed to view the forum statistics.';
$txt['cannot_who_view'] = 'Sorry - you don\'t have the proper permissions to view the Who\'s Online list.';
$txt['cannot_like_posts_stats'] = 'Sorry - you don\'t have the proper permissions to view the Like posts stats.';
Expand Down
1 change: 1 addition & 0 deletions sources/ElkArte/Languages/ManagePermissions/English.php
Expand Up @@ -90,6 +90,7 @@
$txt['permissionhelp_like_posts_stats'] = 'This will allow users to see stats of posts liking';
$txt['permissionname_disable_censor'] = 'Disable word censor';
$txt['permissionhelp_disable_censor'] = 'Allows members the option to disable the word censor.';
$txt['permissionname_post_nofollow'] = 'Post URLs without "nofollow" attribute.';

$txt['permissiongroup_pm'] = 'Personal Messaging';
$txt['permissionname_pm_read'] = 'Read personal messages';
Expand Down
39 changes: 39 additions & 0 deletions sources/Subs.php
Expand Up @@ -1737,6 +1737,45 @@ function addProtocol($url, $protocols = array())
return $protocols[0] . $url;
}

/**
* Validate if a URL is allowed to be a "dofollow"
*
* @param string $checkUrl The URL to be checked
* @return bool Returns true if the URL is allowed, false otherwise
*/
function validateURLAllowList($checkUrl)
{
global $modSettings, $boardurl;
static $allowList = null;

if ($allowList === null)
{
$allowList = empty($modSettings['nofollow_allowlist']) ? [] : json_decode($modSettings['nofollow_allowlist']);

// Always allow your own site
$parse = parse_url($boardurl);
$allowList[] = $parse['host'];

$allowList = array_unique($allowList);
}

$parsed = parse_url($checkUrl);
if (empty($parsed['host']))
{
return false;
}

foreach ($allowList as $validDomain)
{
if (substr($parsed['host'], -strlen($validDomain)) === $validDomain)
{
return true;
}
}

return false;
}

/**
* Removes all, or those over a limit, of nested quotes from a text string.
*
Expand Down
1 change: 1 addition & 0 deletions sources/subs/ManagePermissions.subs.php
Expand Up @@ -449,6 +449,7 @@ function loadAllPermissions()
'karma_edit' => array(false, 'general'),
'like_posts_stats' => array(false, 'general'),
'disable_censor' => array(false, 'general'),
'post_nofollow' => array(false, 'general'),
'pm_read' => array(false, 'pm'),
'pm_send' => array(false, 'pm'),
'send_email_to_members' => array(false, 'pm'),
Expand Down
2 changes: 1 addition & 1 deletion sources/subs/Memberlist.subs.php
Expand Up @@ -376,7 +376,7 @@ function printMemberListRows($request)
$context['members'][$member]['real_name'] = $context['members'][$member]['link'];
$context['members'][$member]['avatar'] = '<a href="' . $context['members'][$member]['href'] . '">' . $context['members'][$member]['avatar']['image'] . '</a>';
$context['members'][$member]['email_address'] = $context['members'][$member]['email'];
$context['members'][$member]['website_url'] = $context['members'][$member]['website']['url'] != '' ? '<a href="' . $context['members'][$member]['website']['url'] . '" target="_blank" rel="noopener noreferrer" class="new_win"><i class="icon i-website" title="' . $context['members'][$member]['website']['title'] . '" title="' . $context['members'][$member]['website']['title'] . '"></i></a>' : '';
$context['members'][$member]['website_url'] = $context['members'][$member]['website']['url'] != '' ? '<a href="' . $context['members'][$member]['website']['url'] . '" target="_blank" rel="noopener noreferrer nofollow ugc" class="new_win"><i class="icon i-website" title="' . $context['members'][$member]['website']['title'] . '" title="' . $context['members'][$member]['website']['title'] . '"></i></a>' : '';
$context['members'][$member]['id_group'] = empty($context['members'][$member]['group']) ? $context['members'][$member]['post_group'] : $context['members'][$member]['group'];
$context['members'][$member]['date_registered'] = $context['members'][$member]['registered'];

Expand Down
8 changes: 4 additions & 4 deletions tests/ElkArte/BBC/ParserWrapperTest.php
Expand Up @@ -455,12 +455,12 @@ protected function setUp(): void
array(
'Named links',
'[url=http://www.elkarte.net/]ElkArte[/url]',
'<a href="http://www.elkarte.net/" class="bbc_link" target="_blank" rel="noopener noreferrer">ElkArte</a>',
'<a href="http://www.elkarte.net/" class="bbc_link" target="_blank" rel="noopener noreferrer nofollow ugc">ElkArte</a>',
),
array(
'URL link',
'http://www.elkarte.net/',
'<a href="http://www.elkarte.net/" class="bbc_link" target="_blank" rel="noopener noreferrer">http://www.elkarte.net/</a>',
'<a href="http://www.elkarte.net/" class="bbc_link" target="_blank" rel="noopener noreferrer nofollow ugc">http://www.elkarte.net/</a>',
),
array(
'Test italic coming from the db ref #3054',
Expand Down Expand Up @@ -559,7 +559,7 @@ protected function setUp(): void
array(
'UTF8',
'[url]www.ñchan.org[/url]',
'<a href="http://www.ñchan.org" class="bbc_link" target="_blank" rel="noopener noreferrer">www.ñchan.org</a>'
'<a href="http://www.ñchan.org" class="bbc_link" target="_blank" rel="noopener noreferrer nofollow ugc">www.ñchan.org</a>'
),
array(
'ListCode1',
Expand Down Expand Up @@ -599,7 +599,7 @@ protected function setUp(): void
array(
'schemelessUrl',
'[url=//www.google.com]Google[/url]',
'<a href="http://www.google.com" class="bbc_link" target="_blank" rel="noopener noreferrer">Google</a>'
'<a href="http://www.google.com" class="bbc_link" target="_blank" rel="noopener noreferrer nofollow ugc">Google</a>'
)
);
}
Expand Down

0 comments on commit e156b5f

Please sign in to comment.