Merge pull request #8729 from MrPetovan/bug/8726-mention-parsing
Add tag escaping to BBCode::setTags
This commit is contained in:
commit
ad47ff50a9
16 changed files with 1198 additions and 1149 deletions
|
@ -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>
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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 ?? '',
|
||||||
|
|
|
@ -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();
|
||||||
|
|
33
mod/item.php
33
mod/item.php
|
@ -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
|
||||||
|
|
|
@ -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']);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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('/\[\&\;([#a-z0-9]+)\;\]/', '&$1;', $text);
|
$text = preg_replace('/\[\&\;([#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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,26 +2424,16 @@ 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.
|
||||||
|
@ -2455,29 +2445,29 @@ 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("#", "#", $match[1]) . "]" . str_replace("#", "#", $match[2]) . "[/url]");
|
return ("[url=" . str_replace("#", "#", $match[1]) . "]" . str_replace("#", "#", $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("#", "#", $match[1]) . "]" . str_replace("#", "#", $match[2]) . "[/bookmark]");
|
return ("[bookmark=" . str_replace("#", "#", $match[1]) . "]" . str_replace("#", "#", $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("#", "#", $match[1]) . "]" . $match[2] . "[/attachment]");
|
return ("[attachment " . str_replace("#", "#", $match[1]) . "]" . $match[2] . "[/attachment]");
|
||||||
}, $item["body"]);
|
}, $body);
|
||||||
|
|
||||||
// Repair recursive urls
|
// Repair recursive urls
|
||||||
$item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
|
$body = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
|
||||||
"#$2", $item["body"]);
|
"#$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] == '#') {
|
||||||
|
@ -2487,21 +2477,16 @@ class Item
|
||||||
$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("#", "#", $item["body"]);
|
$body = str_replace("#", "#", $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"]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)'),
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue