Merge pull request #8729 from MrPetovan/bug/8726-mention-parsing

Add tag escaping to BBCode::setTags
This commit is contained in:
Michael Vogel 2020-06-09 22:03:06 +02:00 committed by GitHub
commit ad47ff50a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1198 additions and 1149 deletions

View File

@ -613,15 +613,26 @@ On Mastodon this field is used for the content warning.
<th>Result</th> <th>Result</th>
</tr> </tr>
<tr> <tr>
<td>If you need to put literal bbcode in a message, [noparse], [nobb] or [pre] are used to escape bbcode: <td>If you need to put literal BBCode in a message, [noparse], [nobb] or [pre] blocks prevent BBCode conversion:
<ul> <ul>
<li>[noparse][b]bold[/b][/noparse]</li> <li>[noparse][b]bold[/b][/noparse]</li>
<li>[nobb][b]bold[/b][/nobb]</li> <li>[nobb][b]bold[/b][/nobb]</li>
<li>[pre][b]bold[/b][/pre]</li> <li>[pre][b]bold[/b][/pre]</li>
</ul> </ul>
Note: [code] has priority over [noparse], [nobb] and [pre] which makes them display as BBCode tags in code blocks instead of being removed.
[code] blocks inside [noparse] will still be converted to a code block.
</td> </td>
<td>[b]bold[/b]</td> <td>[b]bold[/b]</td>
</tr> </tr>
<tr>
<td>Additionally, [noparse] and [pre] blocks prevent mention and hashtag conversion to links:
<ul>
<li>[noparse]@user@domain.tld #hashtag[/noparse]</li>
<li>[pre]@user@domain.tld #hashtag[/pre]</li>
</ul>
</td>
<td>@user@domain.tld #hashtag</td>
</tr>
<tr> <tr>
<td>[nosmile] is used to disable smilies on a post by post basis<br> <td>[nosmile] is used to disable smilies on a post by post basis<br>
<br> <br>

View File

@ -624,7 +624,7 @@ function api_get_user(App $a, $contact_id = null)
'name' => $contact["name"], 'name' => $contact["name"],
'screen_name' => (($contact['nick']) ? $contact['nick'] : $contact['name']), 'screen_name' => (($contact['nick']) ? $contact['nick'] : $contact['name']),
'location' => ($contact["location"] != "") ? $contact["location"] : ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol']), 'location' => ($contact["location"] != "") ? $contact["location"] : ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol']),
'description' => BBCode::toPlaintext($contact["about"]), 'description' => BBCode::toPlaintext($contact["about"] ?? ''),
'profile_image_url' => $contact["micro"], 'profile_image_url' => $contact["micro"],
'profile_image_url_https' => $contact["micro"], 'profile_image_url_https' => $contact["micro"],
'profile_image_url_profile_size' => $contact["thumb"], 'profile_image_url_profile_size' => $contact["thumb"],
@ -698,7 +698,7 @@ function api_get_user(App $a, $contact_id = null)
'name' => (($uinfo[0]['name']) ? $uinfo[0]['name'] : $uinfo[0]['nick']), 'name' => (($uinfo[0]['name']) ? $uinfo[0]['name'] : $uinfo[0]['nick']),
'screen_name' => (($uinfo[0]['nick']) ? $uinfo[0]['nick'] : $uinfo[0]['name']), 'screen_name' => (($uinfo[0]['nick']) ? $uinfo[0]['nick'] : $uinfo[0]['name']),
'location' => $location, 'location' => $location,
'description' => BBCode::toPlaintext($description), 'description' => BBCode::toPlaintext($description ?? ''),
'profile_image_url' => $uinfo[0]['micro'], 'profile_image_url' => $uinfo[0]['micro'],
'profile_image_url_https' => $uinfo[0]['micro'], 'profile_image_url_https' => $uinfo[0]['micro'],
'profile_image_url_profile_size' => $uinfo[0]["thumb"], 'profile_image_url_profile_size' => $uinfo[0]["thumb"],

View File

@ -465,7 +465,7 @@ function notification($params)
if ($show_in_notification_page) { if ($show_in_notification_page) {
$notification = DI::notify()->insert([ $notification = DI::notify()->insert([
'name' => $params['source_name'] ?? '', 'name' => $params['source_name'] ?? '',
'name_cache' => substr(strip_tags(BBCode::convert($params['source_name'] ?? '')), 0, 255), 'name_cache' => substr(strip_tags(BBCode::convert($params['source_name'])), 0, 255),
'url' => $params['source_link'] ?? '', 'url' => $params['source_link'] ?? '',
'photo' => $params['source_photo'] ?? '', 'photo' => $params['source_photo'] ?? '',
'link' => $itemlink ?? '', 'link' => $itemlink ?? '',

View File

@ -78,7 +78,7 @@ function cal_init(App $a)
'$photo' => $profile['photo'], '$photo' => $profile['photo'],
'$addr' => $profile['addr'] ?: '', '$addr' => $profile['addr'] ?: '',
'$account_type' => $account_type, '$account_type' => $account_type,
'$about' => BBCode::convert($profile['about'] ?: ''), '$about' => BBCode::convert($profile['about']),
]); ]);
$cal_widget = Widget\CalendarExport::getHTML(); $cal_widget = Widget\CalendarExport::getHTML();

View File

@ -369,16 +369,16 @@ function item_post(App $a) {
// Look for any tags and linkify them // Look for any tags and linkify them
$inform = ''; $inform = '';
$private_forum = false;
$private_id = null;
$only_to_forum = false;
$forum_contact = [];
BBCode::performWithEscapedTags($body, ['noparse', 'pre', 'code'], function ($body) use ($profile_uid, $network, $str_contact_allow, &$inform, &$private_forum, &$private_id, &$only_to_forum, &$forum_contact) {
$tags = BBCode::getTags($body); $tags = BBCode::getTags($body);
$tagged = []; $tagged = [];
$private_forum = false;
$only_to_forum = false;
$forum_contact = [];
if (count($tags)) {
foreach ($tags as $tag) { foreach ($tags as $tag) {
$tag_type = substr($tag, 0, 1); $tag_type = substr($tag, 0, 1);
@ -386,41 +386,36 @@ function item_post(App $a) {
continue; continue;
} }
/* /* If we already tagged 'Robert Johnson', don't try and tag 'Robert'.
* If we already tagged 'Robert Johnson', don't try and tag 'Robert'.
* Robert Johnson should be first in the $tags array * Robert Johnson should be first in the $tags array
*/ */
$fullnametagged = false;
/// @TODO $tagged is initialized above if () block and is not filled, maybe old-lost code?
foreach ($tagged as $nextTag) { foreach ($tagged as $nextTag) {
if (stristr($nextTag, $tag . ' ')) { if (stristr($nextTag, $tag . ' ')) {
$fullnametagged = true; continue 2;
break;
} }
} }
if ($fullnametagged) {
continue;
}
$success = handle_tag($body, $inform, local_user() ? local_user() : $profile_uid, $tag, $network); $success = handle_tag($body, $inform, local_user() ? local_user() : $profile_uid, $tag, $network);
if ($success['replaced']) { if ($success['replaced']) {
$tagged[] = $tag; $tagged[] = $tag;
} }
// When the forum is private or the forum is addressed with a "!" make the post private // When the forum is private or the forum is addressed with a "!" make the post private
if (is_array($success['contact']) && (!empty($success['contact']['prv']) || ($tag_type == Tag::TAG_CHARACTER[Tag::EXCLUSIVE_MENTION]))) { if (!empty($success['contact']['prv']) || ($tag_type == Tag::TAG_CHARACTER[Tag::EXCLUSIVE_MENTION])) {
$private_forum = $success['contact']['prv']; $private_forum = $success['contact']['prv'];
$only_to_forum = ($tag_type == Tag::TAG_CHARACTER[Tag::EXCLUSIVE_MENTION]); $only_to_forum = ($tag_type == Tag::TAG_CHARACTER[Tag::EXCLUSIVE_MENTION]);
$private_id = $success['contact']['id']; $private_id = $success['contact']['id'];
$forum_contact = $success['contact']; $forum_contact = $success['contact'];
} elseif (is_array($success['contact']) && !empty($success['contact']['forum']) && } elseif (!empty($success['contact']['forum']) && ($str_contact_allow == '<' . $success['contact']['id'] . '>')) {
($str_contact_allow == '<' . $success['contact']['id'] . '>')) {
$private_forum = false; $private_forum = false;
$only_to_forum = true; $only_to_forum = true;
$private_id = $success['contact']['id']; $private_id = $success['contact']['id'];
$forum_contact = $success['contact']; $forum_contact = $success['contact'];
} }
} }
}
return $body;
});
$original_contact_id = $contact_id; $original_contact_id = $contact_id;
@ -642,7 +637,7 @@ function item_post(App $a) {
// Check for hashtags in the body and repair or add hashtag links // Check for hashtags in the body and repair or add hashtag links
if ($preview || $orig_post) { if ($preview || $orig_post) {
Item::setHashtags($datarray); $datarray['body'] = Item::setHashtags($datarray['body']);
} }
// preview mode - prepare the body for display and send it via json // preview mode - prepare the body for display and send it via json

View File

@ -82,7 +82,7 @@ function photos_init(App $a) {
'$photo' => $profile['photo'], '$photo' => $profile['photo'],
'$addr' => $profile['addr'] ?? '', '$addr' => $profile['addr'] ?? '',
'$account_type' => $account_type, '$account_type' => $account_type,
'$about' => BBCode::convert($profile['about'] ?? ''), '$about' => BBCode::convert($profile['about']),
]); ]);
$albums = Photo::getAlbums($a->data['user']['uid']); $albums = Photo::getAlbums($a->data['user']['uid']);

View File

@ -204,7 +204,10 @@ function poco_init(App $a) {
} }
} }
if (is_array($contacts)) { if (!is_array($contacts)) {
throw new \Friendica\Network\HTTPException\InternalServerErrorException();
}
if (DBA::isResult($contacts)) { if (DBA::isResult($contacts)) {
foreach ($contacts as $contact) { foreach ($contacts as $contact) {
if (!isset($contact['updated'])) { if (!isset($contact['updated'])) {
@ -338,9 +341,6 @@ function poco_init(App $a) {
} else { } else {
$ret['entry'][] = []; $ret['entry'][] = [];
} }
} else {
throw new \Friendica\Network\HTTPException\InternalServerErrorException();
}
Logger::log("End of poco", Logger::DEBUG); Logger::log("End of poco", Logger::DEBUG);

View File

@ -67,7 +67,7 @@ function videos_init(App $a)
'$photo' => $profile['photo'], '$photo' => $profile['photo'],
'$addr' => $profile['addr'] ?? '', '$addr' => $profile['addr'] ?? '',
'$account_type' => $account_type, '$account_type' => $account_type,
'$about' => BBCode::convert($profile['about'] ?? ''), '$about' => BBCode::convert($profile['about']),
]); ]);
// If not there, create 'aside' empty // If not there, create 'aside' empty

View File

@ -1252,10 +1252,17 @@ class BBCode
* @return string * @return string
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/ */
public static function convert($text, $try_oembed = true, $simple_html = self::INTERNAL, $for_plaintext = false) public static function convert(string $text = null, $try_oembed = true, $simple_html = self::INTERNAL, $for_plaintext = false)
{ {
// Accounting for null default column values
if (is_null($text) || $text === '') {
return '';
}
$a = DI::app(); $a = DI::app();
$text = self::performWithEscapedTags($text, ['code'], function ($text) use ($try_oembed, $simple_html, $for_plaintext, $a) {
$text = self::performWithEscapedTags($text, ['noparse', 'nobb', 'pre'], function ($text) use ($try_oembed, $simple_html, $for_plaintext, $a) {
/* /*
* preg_match_callback function to replace potential Oembed tags with Oembed content * preg_match_callback function to replace potential Oembed tags with Oembed content
* *
@ -1277,29 +1284,7 @@ class BBCode
return $return; return $return;
}; };
// Extracting code blocks before the whitespace processing and the autolinker
$codeblocks = [];
$text = preg_replace_callback("#\[code(?:=([^\]]*))?\](.*?)\[\/code\]#ism",
function ($matches) use (&$codeblocks) {
$return = '#codeblock-' . count($codeblocks) . '#';
if (strpos($matches[2], "\n") !== false) {
$codeblocks[] = '<pre><code class="language-' . trim($matches[1]) . '">' . htmlspecialchars(trim($matches[2], "\n\r"), ENT_NOQUOTES, 'UTF-8') . '</code></pre>';
} else {
$codeblocks[] = '<code>' . htmlspecialchars($matches[2], ENT_NOQUOTES, 'UTF-8') . '</code>';
}
return $return;
},
$text
);
// Hide all [noparse] contained bbtags by spacefying them
// POSSIBLE BUG --> Will the 'preg' functions crash if there's an embedded image?
$text = preg_replace_callback("/\[noparse\](.*?)\[\/noparse\]/ism", 'self::escapeNoparseCallback', $text);
$text = preg_replace_callback("/\[nobb\](.*?)\[\/nobb\]/ism", 'self::escapeNoparseCallback', $text);
$text = preg_replace_callback("/\[pre\](.*?)\[\/pre\]/ism", 'self::escapeNoparseCallback', $text);
// Remove the abstract element. It is a non visible element. // Remove the abstract element. It is a non visible element.
$text = self::stripAbstract($text); $text = self::stripAbstract($text);
@ -1834,13 +1819,6 @@ class BBCode
$text = preg_replace("/\[mail\](.*?)\[\/mail\]/", '<a href="mailto:$1">$1</a>', $text); $text = preg_replace("/\[mail\](.*?)\[\/mail\]/", '<a href="mailto:$1">$1</a>', $text);
$text = preg_replace("/\[mail\=(.*?)\](.*?)\[\/mail\]/", '<a href="mailto:$1">$2</a>', $text); $text = preg_replace("/\[mail\=(.*?)\](.*?)\[\/mail\]/", '<a href="mailto:$1">$2</a>', $text);
// Unhide all [noparse] contained bbtags unspacefying them
// and triming the [noparse] tag.
$text = preg_replace_callback("/\[noparse\](.*?)\[\/noparse\]/ism", 'self::unescapeNoparseCallback', $text);
$text = preg_replace_callback("/\[nobb\](.*?)\[\/nobb\]/ism", 'self::unescapeNoparseCallback', $text);
$text = preg_replace_callback("/\[pre\](.*?)\[\/pre\]/ism", 'self::unescapeNoparseCallback', $text);
/// @todo What is the meaning of these lines? /// @todo What is the meaning of these lines?
$text = preg_replace('/\[\&amp\;([#a-z0-9]+)\;\]/', '&$1;', $text); $text = preg_replace('/\[\&amp\;([#a-z0-9]+)\;\]/', '&$1;', $text);
$text = preg_replace('/\&\#039\;/', '\'', $text); $text = preg_replace('/\&\#039\;/', '\'', $text);
@ -1882,17 +1860,27 @@ class BBCode
} }
); );
if ($saved_image) {
$text = self::interpolateSavedImagesIntoItemBody($text, $saved_image); $text = self::interpolateSavedImagesIntoItemBody($text, $saved_image);
return $text;
}); // Escaped noparse, nobb, pre
// Remove escaping tags
$text = preg_replace("/\[noparse\](.*?)\[\/noparse\]/ism", '\1', $text);
$text = preg_replace("/\[nobb\](.*?)\[\/nobb\]/ism", '\1', $text);
$text = preg_replace("/\[pre\](.*?)\[\/pre\]/ism", '\1', $text);
return $text;
}); // Escaped code
$text = preg_replace_callback("#\[code(?:=([^\]]*))?\](.*?)\[\/code\]#ism",
function ($matches) {
if (strpos($matches[2], "\n") !== false) {
$return = '<pre><code class="language-' . trim($matches[1]) . '">' . htmlspecialchars(trim($matches[2], "\n\r"), ENT_NOQUOTES, 'UTF-8') . '</code></pre>';
} else {
$return = '<code>' . htmlspecialchars($matches[2], ENT_NOQUOTES, 'UTF-8') . '</code>';
} }
// Restore code blocks
$text = preg_replace_callback('/#codeblock-([0-9]+)#/iU',
function ($matches) use ($codeblocks) {
$return = $matches[0];
if (isset($codeblocks[intval($matches[1])])) {
$return = $codeblocks[$matches[1]];
}
return $return; return $return;
}, },
$text $text
@ -2104,12 +2092,10 @@ class BBCode
{ {
$ret = []; $ret = [];
BBCode::performWithEscapedTags($string, ['noparse', 'pre', 'code'], function ($string) use (&$ret) {
// Convert hashtag links to hashtags // Convert hashtag links to hashtags
$string = preg_replace('/#\[url\=([^\[\]]*)\](.*?)\[\/url\]/ism', '#$2 ', $string); $string = preg_replace('/#\[url\=([^\[\]]*)\](.*?)\[\/url\]/ism', '#$2 ', $string);
// ignore anything in a code block
$string = preg_replace('/\[code.*?\].*?\[\/code\]/sm', '', $string);
// Force line feeds at bbtags // Force line feeds at bbtags
$string = str_replace(['[', ']'], ["\n[", "]\n"], $string); $string = str_replace(['[', ']'], ["\n[", "]\n"], $string);
@ -2137,17 +2123,13 @@ class BBCode
// Otherwise pull out single word tags. These can be @nickname, @first_last // Otherwise pull out single word tags. These can be @nickname, @first_last
// and #hash tags. // and #hash tags.
if (preg_match_all('/([!#@][^\^ \x0D\x0A,;:?]+)([ \x0D\x0A,;:?]|$)/', $string, $matches)) { if (preg_match_all('/([!#@][^\^ \x0D\x0A,;:?\']*[^\^ \x0D\x0A,;:?!\'.])/', $string, $matches)) {
foreach ($matches[1] as $match) { foreach ($matches[1] as $match) {
if (strstr($match, ']')) { if (strstr($match, ']')) {
// we might be inside a bbcode color tag - leave it alone // we might be inside a bbcode color tag - leave it alone
continue; continue;
} }
if (substr($match, -1, 1) === '.') {
$match = substr($match,0,-1);
}
// ignore strictly numeric tags like #1 // ignore strictly numeric tags like #1
if ((strpos($match, '#') === 0) && ctype_digit(substr($match, 1))) { if ((strpos($match, '#') === 0) && ctype_digit(substr($match, 1))) {
continue; continue;
@ -2157,10 +2139,30 @@ class BBCode
if (strpos($string, $match) && preg_match('/[a-zA-z0-9\/]/', substr($string, strpos($string, $match) - 1, 1))) { if (strpos($string, $match) && preg_match('/[a-zA-z0-9\/]/', substr($string, strpos($string, $match) - 1, 1))) {
continue; continue;
} }
$ret[] = $match; $ret[] = $match;
} }
} }
});
return $ret; return array_unique($ret);
}
/**
* Perform a custom function on a text after having escaped blocks enclosed in the provided tag list.
*
* @param string $text
* @param array $tagList A list of tag names, e.g ['noparse', 'nobb', 'pre']
* @param callable $callback
* @return string
* @throws Exception
*@see Strings::performWithEscapedBlocks
*
*/
public static function performWithEscapedTags(string $text, array $tagList, callable $callback)
{
$tagList = array_map('preg_quote', $tagList);
return Strings::performWithEscapedBlocks($text, '#\[(?:' . implode('|', $tagList) . ').*?\[/(?:' . implode('|', $tagList) . ')]#ism', $callback);
} }
} }

View File

@ -167,24 +167,7 @@ class HTML
{ {
$message = str_replace("\r", "", $message); $message = str_replace("\r", "", $message);
// Removing code blocks before the whitespace removal processing below $message = Strings::performWithEscapedBlocks($message, '#<pre><code.*</code></pre>#iUs', function ($message) {
$codeblocks = [];
$message = preg_replace_callback(
'#<pre><code(?: class="language-([^"]*)")?>(.*)</code></pre>#iUs',
function ($matches) use (&$codeblocks) {
$return = '[codeblock-' . count($codeblocks) . ']';
$prefix = '[code]';
if ($matches[1] != '') {
$prefix = '[code=' . $matches[1] . ']';
}
$codeblocks[] = $prefix . PHP_EOL . trim($matches[2]) . PHP_EOL . '[/code]';
return $return;
},
$message
);
$message = str_replace( $message = str_replace(
[ [
"<li><p>", "<li><p>",
@ -404,15 +387,18 @@ class HTML
// Handling Yahoo style of mails // Handling Yahoo style of mails
$message = str_replace('[hr][b]From:[/b]', '[quote][b]From:[/b]', $message); $message = str_replace('[hr][b]From:[/b]', '[quote][b]From:[/b]', $message);
// Restore code blocks return $message;
});
$message = preg_replace_callback( $message = preg_replace_callback(
'#\[codeblock-([0-9]+)\]#iU', '#<pre><code(?: class="language-([^"]*)")?>(.*)</code></pre>#iUs',
function ($matches) use ($codeblocks) { function ($matches) {
$return = ''; $prefix = '[code]';
if (isset($codeblocks[intval($matches[1])])) { if ($matches[1] != '') {
$return = $codeblocks[$matches[1]]; $prefix = '[code=' . $matches[1] . ']';
} }
return $return;
return $prefix . PHP_EOL . trim($matches[2]) . PHP_EOL . '[/code]';
}, },
$message $message
); );

View File

@ -1780,7 +1780,7 @@ class Item
// Check for hashtags in the body and repair or add hashtag links // Check for hashtags in the body and repair or add hashtag links
self::setHashtags($item); $item['body'] = self::setHashtags($item['body']);
// Fill the cache field // Fill the cache field
self::putInCache($item); self::putInCache($item);
@ -2424,30 +2424,20 @@ class Item
} }
} }
public static function setHashtags(&$item) public static function setHashtags($body)
{ {
$tags = BBCode::getTags($item["body"]); $body = BBCode::performWithEscapedTags($body, ['noparse', 'pre', 'code'], function ($body) {
$tags = BBCode::getTags($body);
// No hashtags? // No hashtags?
if (!count($tags)) { if (!count($tags)) {
return false; return $body;
} }
// What happens in [code], stays in [code]!
// escape the # and the [
// hint: we will also get in trouble with #tags, when we want markdown in posts -> ### Headline 3
$item["body"] = preg_replace_callback("/\[code(.*?)\](.*?)\[\/code\]/ism",
function ($match) {
// we truly ESCape all # and [ to prevent gettin weird tags in [code] blocks
$find = ['#', '['];
$replace = [chr(27).'sharp', chr(27).'leftsquarebracket'];
return ("[code" . $match[1] . "]" . str_replace($find, $replace, $match[2]) . "[/code]");
}, $item["body"]);
// This sorting is important when there are hashtags that are part of other hashtags // This sorting is important when there are hashtags that are part of other hashtags
// Otherwise there could be problems with hashtags like #test and #test2 // Otherwise there could be problems with hashtags like #test and #test2
// Because of this we are sorting from the longest to the shortest tag. // Because of this we are sorting from the longest to the shortest tag.
usort($tags, function($a, $b) { usort($tags, function ($a, $b) {
return strlen($b) <=> strlen($a); return strlen($b) <=> strlen($a);
}); });
@ -2455,53 +2445,48 @@ class Item
// All hashtags should point to the home server if "local_tags" is activated // All hashtags should point to the home server if "local_tags" is activated
if (DI::config()->get('system', 'local_tags')) { if (DI::config()->get('system', 'local_tags')) {
$item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", $body = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
"#[url=".DI::baseUrl()."/search?tag=$2]$2[/url]", $item["body"]); "#[url=" . DI::baseUrl() . "/search?tag=$2]$2[/url]", $body);
} }
// mask hashtags inside of url, bookmarks and attachments to avoid urls in urls // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
$item["body"] = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", $body = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
function ($match) { function ($match) {
return ("[url=" . str_replace("#", "&num;", $match[1]) . "]" . str_replace("#", "&num;", $match[2]) . "[/url]"); return ("[url=" . str_replace("#", "&num;", $match[1]) . "]" . str_replace("#", "&num;", $match[2]) . "[/url]");
}, $item["body"]); }, $body);
$item["body"] = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism", $body = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",
function ($match) { function ($match) {
return ("[bookmark=" . str_replace("#", "&num;", $match[1]) . "]" . str_replace("#", "&num;", $match[2]) . "[/bookmark]"); return ("[bookmark=" . str_replace("#", "&num;", $match[1]) . "]" . str_replace("#", "&num;", $match[2]) . "[/bookmark]");
}, $item["body"]); }, $body);
$item["body"] = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism", $body = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
function ($match) { function ($match) {
return ("[attachment " . str_replace("#", "&num;", $match[1]) . "]" . $match[2] . "[/attachment]"); return ("[attachment " . str_replace("#", "&num;", $match[1]) . "]" . $match[2] . "[/attachment]");
}, $item["body"]); }, $body);
// Repair recursive urls // Repair recursive urls
$item["body"] = preg_replace("/&num;\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", $body = preg_replace("/&num;\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
"&num;$2", $item["body"]); "&num;$2", $body);
foreach ($tags as $tag) { foreach ($tags as $tag) {
if ((strpos($tag, '#') !== 0) || strpos($tag, '[url=') || strlen($tag) < 2 || $tag[1] == '#') { if ((strpos($tag, '#') !== 0) || strpos($tag, '[url=') || strlen($tag) < 2 || $tag[1] == '#') {
continue; continue;
} }
$basetag = str_replace('_',' ',substr($tag,1)); $basetag = str_replace('_', ' ', substr($tag, 1));
$newtag = '#[url=' . DI::baseUrl() . '/search?tag=' . $basetag . ']' . $basetag . '[/url]'; $newtag = '#[url=' . DI::baseUrl() . '/search?tag=' . $basetag . ']' . $basetag . '[/url]';
$item["body"] = str_replace($tag, $newtag, $item["body"]); $body = str_replace($tag, $newtag, $body);
} }
// Convert back the masked hashtags // Convert back the masked hashtags
$item["body"] = str_replace("&num;", "#", $item["body"]); $body = str_replace("&num;", "#", $body);
// Remember! What happens in [code], stays in [code] return $body;
// roleback the # and [ });
$item["body"] = preg_replace_callback("/\[code(.*?)\](.*?)\[\/code\]/ism",
function ($match) { return $body;
// we truly unESCape all sharp and leftsquarebracket
$find = [chr(27).'sharp', chr(27).'leftsquarebracket'];
$replace = ['#', '['];
return ("[code" . $match[1] . "]" . str_replace($find, $replace, $match[2]) . "[/code]");
}, $item["body"]);
} }
/** /**

View File

@ -70,7 +70,7 @@ class Notify extends BaseModel
private function setNameCache() private function setNameCache()
{ {
try { try {
$this->name_cache = strip_tags(BBCode::convert($this->source_name ?? '')); $this->name_cache = strip_tags(BBCode::convert($this->source_name));
} catch (InternalServerErrorException $e) { } catch (InternalServerErrorException $e) {
} }
} }

View File

@ -102,14 +102,12 @@ class Babel extends BaseModule
'content' => visible_whitespace($bbcode4) 'content' => visible_whitespace($bbcode4)
]; ];
$item = ['body' => $bbcode];
$tags = Text\BBCode::getTags($bbcode); $tags = Text\BBCode::getTags($bbcode);
Item::setHashtags($item); $body = Item::setHashtags($bbcode);
$results[] = [ $results[] = [
'title' => DI::l10n()->t('Item Body'), 'title' => DI::l10n()->t('Item Body'),
'content' => visible_whitespace($item['body']) 'content' => visible_whitespace($body)
]; ];
$results[] = [ $results[] = [
'title' => DI::l10n()->t('Item Tags'), 'title' => DI::l10n()->t('Item Tags'),
@ -125,9 +123,7 @@ class Babel extends BaseModule
$markdown = XML::unescape($diaspora); $markdown = XML::unescape($diaspora);
case 'markdown': case 'markdown':
if (!isset($markdown)) { $markdown = $markdown ?? trim($_REQUEST['text']);
$markdown = trim($_REQUEST['text']);
}
$results[] = [ $results[] = [
'title' => DI::l10n()->t('Source input (Markdown)'), 'title' => DI::l10n()->t('Source input (Markdown)'),

View File

@ -472,4 +472,52 @@ class Strings
return mb_substr($string, 0, $start) . $replacement . mb_substr($string, $start + $length, $string_length - $start - $length); return mb_substr($string, 0, $start) . $replacement . mb_substr($string, $start + $length, $string_length - $start - $length);
} }
/**
* Perform a custom function on a text after having escaped blocks matched by the provided regular expressions.
* Only full matches are used, capturing group are ignored.
*
* To change the provided text, the callback function needs to return it and this function will return the modified
* version as well after having restored the escaped blocks.
*
* @param string $text
* @param string $regex
* @param callable $callback
* @return string
* @throws \Exception
*/
public static function performWithEscapedBlocks(string $text, string $regex, callable $callback)
{
// Enables nested use
$executionId = random_int(PHP_INT_MAX / 10, PHP_INT_MAX);
$blocks = [];
$text = preg_replace_callback($regex,
function ($matches) use ($executionId, &$blocks) {
$return = '«block-' . $executionId . '-' . count($blocks) . '»';
$blocks[] = $matches[0];
return $return;
},
$text
);
$text = $callback($text) ?? '';
// Restore code blocks
$text = preg_replace_callback('/«block-' . $executionId . '-([0-9]+)»/iU',
function ($matches) use ($blocks) {
$return = $matches[0];
if (isset($blocks[intval($matches[1])])) {
$return = $blocks[$matches[1]];
}
return $return;
},
$text
);
return $text;
}
} }

View File

@ -3852,7 +3852,7 @@ class ApiTest extends DatabaseTest
$assertXml=<<<XML $assertXml=<<<XML
<?xml version="1.0"?> <?xml version="1.0"?>
<notes> <notes>
<note id="1" hash="" type="8" name="Reply to" url="http://localhost/display/1" photo="http://localhost/" date="2020-01-01 12:12:02" msg="A test reply from an item" uid="42" uri-id="" link="http://localhost/notification/1" iid="4" parent="0" parent-uri-id="" seen="0" verb="" otype="item" name_cache="" msg_cache="A test reply from an item" timestamp="1577880722" date_rel="{$dateRel}" msg_html="A test reply from an item" msg_plain="A test reply from an item"/> <note id="1" hash="" type="8" name="Reply to" url="http://localhost/display/1" photo="http://localhost/" date="2020-01-01 12:12:02" msg="A test reply from an item" uid="42" uri-id="" link="http://localhost/notification/1" iid="4" parent="0" parent-uri-id="" seen="0" verb="" otype="item" name_cache="Reply to" msg_cache="A test reply from an item" timestamp="1577880722" date_rel="{$dateRel}" msg_html="A test reply from an item" msg_plain="A test reply from an item"/>
</notes> </notes>
XML; XML;
$this->assertXmlStringEqualsXmlString($assertXml, $result); $this->assertXmlStringEqualsXmlString($assertXml, $result);

View File

@ -194,4 +194,30 @@ class StringsTest extends TestCase
) )
); );
} }
public function testPerformWithEscapedBlocks()
{
$originalText = '[noparse][/noparse][nobb]nobb[/nobb][noparse]noparse[/noparse]';
$text = Strings::performWithEscapedBlocks($originalText, '#[(?:noparse|nobb)].*?\[/(?:noparse|nobb)]#is', function ($text) {
return $text;
});
$this->assertEquals($originalText, $text);
}
public function testPerformWithEscapedBlocksNested()
{
$originalText = '[noparse][/noparse][nobb]nobb[/nobb][noparse]noparse[/noparse]';
$text = Strings::performWithEscapedBlocks($originalText, '#[nobb].*?\[/nobb]#is', function ($text) {
$text = Strings::performWithEscapedBlocks($text, '#[noparse].*?\[/noparse]#is', function ($text) {
return $text;
});
return $text;
});
$this->assertEquals($originalText, $text);
}
} }