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>
</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>
<li>[noparse][b]bold[/b][/noparse]</li>
<li>[nobb][b]bold[/b][/nobb]</li>
<li>[pre][b]bold[/b][/pre]</li>
</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>[b]bold[/b]</td>
</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>
<td>[nosmile] is used to disable smilies on a post by post basis<br>
<br>

View file

@ -624,7 +624,7 @@ function api_get_user(App $a, $contact_id = null)
'name' => $contact["name"],
'screen_name' => (($contact['nick']) ? $contact['nick'] : $contact['name']),
'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_https' => $contact["micro"],
'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']),
'screen_name' => (($uinfo[0]['nick']) ? $uinfo[0]['nick'] : $uinfo[0]['name']),
'location' => $location,
'description' => BBCode::toPlaintext($description),
'description' => BBCode::toPlaintext($description ?? ''),
'profile_image_url' => $uinfo[0]['micro'],
'profile_image_url_https' => $uinfo[0]['micro'],
'profile_image_url_profile_size' => $uinfo[0]["thumb"],

View file

@ -465,7 +465,7 @@ function notification($params)
if ($show_in_notification_page) {
$notification = DI::notify()->insert([
'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'] ?? '',
'photo' => $params['source_photo'] ?? '',
'link' => $itemlink ?? '',

View file

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

View file

@ -369,16 +369,16 @@ function item_post(App $a) {
// Look for any tags and linkify them
$inform = '';
$tags = BBCode::getTags($body);
$tagged = [];
$private_forum = false;
$private_id = null;
$only_to_forum = false;
$forum_contact = [];
if (count($tags)) {
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);
$tagged = [];
foreach ($tags as $tag) {
$tag_type = substr($tag, 0, 1);
@ -386,41 +386,36 @@ function item_post(App $a) {
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
*/
$fullnametagged = false;
/// @TODO $tagged is initialized above if () block and is not filled, maybe old-lost code?
foreach ($tagged as $nextTag) {
if (stristr($nextTag, $tag . ' ')) {
$fullnametagged = true;
break;
continue 2;
}
}
if ($fullnametagged) {
continue;
}
$success = handle_tag($body, $inform, local_user() ? local_user() : $profile_uid, $tag, $network);
if ($success['replaced']) {
$tagged[] = $tag;
}
// 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'];
$only_to_forum = ($tag_type == Tag::TAG_CHARACTER[Tag::EXCLUSIVE_MENTION]);
$private_id = $success['contact']['id'];
$forum_contact = $success['contact'];
} elseif (is_array($success['contact']) && !empty($success['contact']['forum']) &&
($str_contact_allow == '<' . $success['contact']['id'] . '>')) {
} elseif (!empty($success['contact']['forum']) && ($str_contact_allow == '<' . $success['contact']['id'] . '>')) {
$private_forum = false;
$only_to_forum = true;
$private_id = $success['contact']['id'];
$forum_contact = $success['contact'];
}
}
}
return $body;
});
$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
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

View file

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

View file

@ -204,142 +204,142 @@ function poco_init(App $a) {
}
}
if (is_array($contacts)) {
if (DBA::isResult($contacts)) {
foreach ($contacts as $contact) {
if (!isset($contact['updated'])) {
$contact['updated'] = '';
}
if (!is_array($contacts)) {
throw new \Friendica\Network\HTTPException\InternalServerErrorException();
}
if (! isset($contact['generation'])) {
if ($global) {
$contact['generation'] = 3;
} elseif ($system_mode) {
$contact['generation'] = 1;
} else {
$contact['generation'] = 2;
}
}
if (($contact['keywords'] == "") && isset($contact['pub_keywords'])) {
$contact['keywords'] = $contact['pub_keywords'];
}
if (isset($contact['account-type'])) {
$contact['contact-type'] = $contact['account-type'];
}
$about = DI::cache()->get("about:" . $contact['updated'] . ":" . $contact['nurl']);
if (is_null($about)) {
$about = BBCode::convert($contact['about'], false);
DI::cache()->set("about:" . $contact['updated'] . ":" . $contact['nurl'], $about);
}
// Non connected persons can only see the keywords of a Diaspora account
if ($contact['network'] == Protocol::DIASPORA) {
$contact['location'] = "";
$about = "";
}
$entry = [];
if ($fields_ret['id']) {
$entry['id'] = (int)$contact['id'];
}
if ($fields_ret['displayName']) {
$entry['displayName'] = $contact['name'];
}
if ($fields_ret['aboutMe']) {
$entry['aboutMe'] = $about;
}
if ($fields_ret['currentLocation']) {
$entry['currentLocation'] = $contact['location'];
}
if ($fields_ret['generation']) {
$entry['generation'] = (int)$contact['generation'];
}
if ($fields_ret['urls']) {
$entry['urls'] = [['value' => $contact['url'], 'type' => 'profile']];
if ($contact['addr'] && ($contact['network'] !== Protocol::MAIL)) {
$entry['urls'][] = ['value' => 'acct:' . $contact['addr'], 'type' => 'webfinger'];
}
}
if ($fields_ret['preferredUsername']) {
$entry['preferredUsername'] = $contact['nick'];
}
if ($fields_ret['updated']) {
if (! $global) {
$entry['updated'] = $contact['success_update'];
if ($contact['name-date'] > $entry['updated']) {
$entry['updated'] = $contact['name-date'];
}
if ($contact['uri-date'] > $entry['updated']) {
$entry['updated'] = $contact['uri-date'];
}
if ($contact['avatar-date'] > $entry['updated']) {
$entry['updated'] = $contact['avatar-date'];
}
} else {
$entry['updated'] = $contact['updated'];
}
$entry['updated'] = date("c", strtotime($entry['updated']));
}
if ($fields_ret['photos']) {
$entry['photos'] = [['value' => $contact['photo'], 'type' => 'profile']];
}
if ($fields_ret['network']) {
$entry['network'] = $contact['network'];
if ($entry['network'] == Protocol::STATUSNET) {
$entry['network'] = Protocol::OSTATUS;
}
if (($entry['network'] == "") && ($contact['self'])) {
$entry['network'] = Protocol::DFRN;
}
}
if ($fields_ret['tags']) {
$tags = str_replace(",", " ", $contact['keywords']);
$tags = explode(" ", $tags);
$cleaned = [];
foreach ($tags as $tag) {
$tag = trim(strtolower($tag));
if ($tag != "") {
$cleaned[] = $tag;
}
}
$entry['tags'] = [$cleaned];
}
if ($fields_ret['address']) {
$entry['address'] = [];
// Deactivated. It just reveals too much data. (Although its from the default profile)
//if (isset($rr['address']))
// $entry['address']['streetAddress'] = $rr['address'];
if (isset($contact['locality'])) {
$entry['address']['locality'] = $contact['locality'];
}
if (isset($contact['region'])) {
$entry['address']['region'] = $contact['region'];
}
// See above
//if (isset($rr['postal-code']))
// $entry['address']['postalCode'] = $rr['postal-code'];
if (isset($contact['country'])) {
$entry['address']['country'] = $contact['country'];
}
}
if ($fields_ret['contactType']) {
$entry['contactType'] = intval($contact['contact-type']);
}
$ret['entry'][] = $entry;
if (DBA::isResult($contacts)) {
foreach ($contacts as $contact) {
if (!isset($contact['updated'])) {
$contact['updated'] = '';
}
} else {
$ret['entry'][] = [];
if (! isset($contact['generation'])) {
if ($global) {
$contact['generation'] = 3;
} elseif ($system_mode) {
$contact['generation'] = 1;
} else {
$contact['generation'] = 2;
}
}
if (($contact['keywords'] == "") && isset($contact['pub_keywords'])) {
$contact['keywords'] = $contact['pub_keywords'];
}
if (isset($contact['account-type'])) {
$contact['contact-type'] = $contact['account-type'];
}
$about = DI::cache()->get("about:" . $contact['updated'] . ":" . $contact['nurl']);
if (is_null($about)) {
$about = BBCode::convert($contact['about'], false);
DI::cache()->set("about:" . $contact['updated'] . ":" . $contact['nurl'], $about);
}
// Non connected persons can only see the keywords of a Diaspora account
if ($contact['network'] == Protocol::DIASPORA) {
$contact['location'] = "";
$about = "";
}
$entry = [];
if ($fields_ret['id']) {
$entry['id'] = (int)$contact['id'];
}
if ($fields_ret['displayName']) {
$entry['displayName'] = $contact['name'];
}
if ($fields_ret['aboutMe']) {
$entry['aboutMe'] = $about;
}
if ($fields_ret['currentLocation']) {
$entry['currentLocation'] = $contact['location'];
}
if ($fields_ret['generation']) {
$entry['generation'] = (int)$contact['generation'];
}
if ($fields_ret['urls']) {
$entry['urls'] = [['value' => $contact['url'], 'type' => 'profile']];
if ($contact['addr'] && ($contact['network'] !== Protocol::MAIL)) {
$entry['urls'][] = ['value' => 'acct:' . $contact['addr'], 'type' => 'webfinger'];
}
}
if ($fields_ret['preferredUsername']) {
$entry['preferredUsername'] = $contact['nick'];
}
if ($fields_ret['updated']) {
if (! $global) {
$entry['updated'] = $contact['success_update'];
if ($contact['name-date'] > $entry['updated']) {
$entry['updated'] = $contact['name-date'];
}
if ($contact['uri-date'] > $entry['updated']) {
$entry['updated'] = $contact['uri-date'];
}
if ($contact['avatar-date'] > $entry['updated']) {
$entry['updated'] = $contact['avatar-date'];
}
} else {
$entry['updated'] = $contact['updated'];
}
$entry['updated'] = date("c", strtotime($entry['updated']));
}
if ($fields_ret['photos']) {
$entry['photos'] = [['value' => $contact['photo'], 'type' => 'profile']];
}
if ($fields_ret['network']) {
$entry['network'] = $contact['network'];
if ($entry['network'] == Protocol::STATUSNET) {
$entry['network'] = Protocol::OSTATUS;
}
if (($entry['network'] == "") && ($contact['self'])) {
$entry['network'] = Protocol::DFRN;
}
}
if ($fields_ret['tags']) {
$tags = str_replace(",", " ", $contact['keywords']);
$tags = explode(" ", $tags);
$cleaned = [];
foreach ($tags as $tag) {
$tag = trim(strtolower($tag));
if ($tag != "") {
$cleaned[] = $tag;
}
}
$entry['tags'] = [$cleaned];
}
if ($fields_ret['address']) {
$entry['address'] = [];
// Deactivated. It just reveals too much data. (Although its from the default profile)
//if (isset($rr['address']))
// $entry['address']['streetAddress'] = $rr['address'];
if (isset($contact['locality'])) {
$entry['address']['locality'] = $contact['locality'];
}
if (isset($contact['region'])) {
$entry['address']['region'] = $contact['region'];
}
// See above
//if (isset($rr['postal-code']))
// $entry['address']['postalCode'] = $rr['postal-code'];
if (isset($contact['country'])) {
$entry['address']['country'] = $contact['country'];
}
}
if ($fields_ret['contactType']) {
$entry['contactType'] = intval($contact['contact-type']);
}
$ret['entry'][] = $entry;
}
} else {
throw new \Friendica\Network\HTTPException\InternalServerErrorException();
$ret['entry'][] = [];
}
Logger::log("End of poco", Logger::DEBUG);

View file

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

File diff suppressed because it is too large Load diff

View file

@ -167,252 +167,238 @@ class HTML
{
$message = str_replace("\r", "", $message);
// Removing code blocks before the whitespace removal processing below
$codeblocks = [];
$message = Strings::performWithEscapedBlocks($message, '#<pre><code.*</code></pre>#iUs', function ($message) {
$message = str_replace(
[
"<li><p>",
"</p></li>",
],
[
"<li>",
"</li>",
],
$message
);
// remove namespaces
$message = preg_replace('=<(\w+):(.+?)>=', '<removeme>', $message);
$message = preg_replace('=</(\w+):(.+?)>=', '</removeme>', $message);
$doc = new DOMDocument();
$doc->preserveWhiteSpace = false;
$message = mb_convert_encoding($message, 'HTML-ENTITIES', "UTF-8");
@$doc->loadHTML($message, LIBXML_HTML_NODEFDTD);
XML::deleteNode($doc, 'style');
XML::deleteNode($doc, 'head');
XML::deleteNode($doc, 'title');
XML::deleteNode($doc, 'meta');
XML::deleteNode($doc, 'xml');
XML::deleteNode($doc, 'removeme');
$xpath = new DomXPath($doc);
$list = $xpath->query("//pre");
foreach ($list as $node) {
// Ensure to escape unescaped & - they will otherwise raise a warning
$safe_value = preg_replace('/&(?!\w+;)/', '&amp;', $node->nodeValue);
$node->nodeValue = str_replace("\n", "\r", $safe_value);
}
$message = $doc->saveHTML();
$message = str_replace(["\n<", ">\n", "\r", "\n", "\xC3\x82\xC2\xA0"], ["<", ">", "<br />", " ", ""], $message);
$message = preg_replace('= [\s]*=i', " ", $message);
@$doc->loadHTML($message, LIBXML_HTML_NODEFDTD);
self::tagToBBCode($doc, 'html', [], "", "");
self::tagToBBCode($doc, 'body', [], "", "");
// Outlook-Quote - Variant 1
self::tagToBBCode($doc, 'p', ['class' => 'MsoNormal', 'style' => 'margin-left:35.4pt'], '[quote]', '[/quote]');
// Outlook-Quote - Variant 2
self::tagToBBCode(
$doc,
'div',
['style' => 'border:none;border-left:solid blue 1.5pt;padding:0cm 0cm 0cm 4.0pt'],
'[quote]',
'[/quote]'
);
// MyBB-Stuff
self::tagToBBCode($doc, 'span', ['style' => 'text-decoration: underline;'], '[u]', '[/u]');
self::tagToBBCode($doc, 'span', ['style' => 'font-style: italic;'], '[i]', '[/i]');
self::tagToBBCode($doc, 'span', ['style' => 'font-weight: bold;'], '[b]', '[/b]');
/* self::node2BBCode($doc, 'font', array('face'=>'/([\w ]+)/', 'size'=>'/(\d+)/', 'color'=>'/(.+)/'), '[font=$1][size=$2][color=$3]', '[/color][/size][/font]');
self::node2BBCode($doc, 'font', array('size'=>'/(\d+)/', 'color'=>'/(.+)/'), '[size=$1][color=$2]', '[/color][/size]');
self::node2BBCode($doc, 'font', array('face'=>'/([\w ]+)/', 'size'=>'/(.+)/'), '[font=$1][size=$2]', '[/size][/font]');
self::node2BBCode($doc, 'font', array('face'=>'/([\w ]+)/', 'color'=>'/(.+)/'), '[font=$1][color=$3]', '[/color][/font]');
self::node2BBCode($doc, 'font', array('face'=>'/([\w ]+)/'), '[font=$1]', '[/font]');
self::node2BBCode($doc, 'font', array('size'=>'/(\d+)/'), '[size=$1]', '[/size]');
self::node2BBCode($doc, 'font', array('color'=>'/(.+)/'), '[color=$1]', '[/color]');
*/
// Untested
//self::node2BBCode($doc, 'span', array('style'=>'/.*font-size:\s*(.+?)[,;].*font-family:\s*(.+?)[,;].*color:\s*(.+?)[,;].*/'), '[size=$1][font=$2][color=$3]', '[/color][/font][/size]');
//self::node2BBCode($doc, 'span', array('style'=>'/.*font-size:\s*(\d+)[,;].*/'), '[size=$1]', '[/size]');
//self::node2BBCode($doc, 'span', array('style'=>'/.*font-size:\s*(.+?)[,;].*/'), '[size=$1]', '[/size]');
self::tagToBBCode($doc, 'span', ['style' => '/.*color:\s*(.+?)[,;].*/'], '[color="$1"]', '[/color]');
//self::node2BBCode($doc, 'span', array('style'=>'/.*font-family:\s*(.+?)[,;].*/'), '[font=$1]', '[/font]');
//self::node2BBCode($doc, 'div', array('style'=>'/.*font-family:\s*(.+?)[,;].*font-size:\s*(\d+?)pt.*/'), '[font=$1][size=$2]', '[/size][/font]');
//self::node2BBCode($doc, 'div', array('style'=>'/.*font-family:\s*(.+?)[,;].*font-size:\s*(\d+?)px.*/'), '[font=$1][size=$2]', '[/size][/font]');
//self::node2BBCode($doc, 'div', array('style'=>'/.*font-family:\s*(.+?)[,;].*/'), '[font=$1]', '[/font]');
// Importing the classes - interesting for importing of posts from third party networks that were exported from friendica
// Test
//self::node2BBCode($doc, 'span', array('class'=>'/([\w ]+)/'), '[class=$1]', '[/class]');
self::tagToBBCode($doc, 'span', ['class' => 'type-link'], '[class=type-link]', '[/class]');
self::tagToBBCode($doc, 'span', ['class' => 'type-video'], '[class=type-video]', '[/class]');
self::tagToBBCode($doc, 'strong', [], '[b]', '[/b]');
self::tagToBBCode($doc, 'em', [], '[i]', '[/i]');
self::tagToBBCode($doc, 'b', [], '[b]', '[/b]');
self::tagToBBCode($doc, 'i', [], '[i]', '[/i]');
self::tagToBBCode($doc, 'u', [], '[u]', '[/u]');
self::tagToBBCode($doc, 's', [], '[s]', '[/s]');
self::tagToBBCode($doc, 'del', [], '[s]', '[/s]');
self::tagToBBCode($doc, 'strike', [], '[s]', '[/s]');
self::tagToBBCode($doc, 'big', [], "[size=large]", "[/size]");
self::tagToBBCode($doc, 'small', [], "[size=small]", "[/size]");
self::tagToBBCode($doc, 'blockquote', [], '[quote]', '[/quote]');
self::tagToBBCode($doc, 'br', [], "\n", '');
self::tagToBBCode($doc, 'p', ['class' => 'MsoNormal'], "\n", "");
self::tagToBBCode($doc, 'div', ['class' => 'MsoNormal'], "\r", "");
self::tagToBBCode($doc, 'span', [], "", "");
self::tagToBBCode($doc, 'span', [], "", "");
self::tagToBBCode($doc, 'pre', [], "", "");
self::tagToBBCode($doc, 'div', [], "\r", "\r");
self::tagToBBCode($doc, 'p', [], "\n", "\n");
self::tagToBBCode($doc, 'ul', [], "[list]", "[/list]");
self::tagToBBCode($doc, 'ol', [], "[list=1]", "[/list]");
self::tagToBBCode($doc, 'li', [], "[*]", "");
self::tagToBBCode($doc, 'hr', [], "[hr]", "");
self::tagToBBCode($doc, 'table', [], "[table]", "[/table]");
self::tagToBBCode($doc, 'th', [], "[th]", "[/th]");
self::tagToBBCode($doc, 'tr', [], "[tr]", "[/tr]");
self::tagToBBCode($doc, 'td', [], "[td]", "[/td]");
self::tagToBBCode($doc, 'h1', [], "[h1]", "[/h1]");
self::tagToBBCode($doc, 'h2', [], "[h2]", "[/h2]");
self::tagToBBCode($doc, 'h3', [], "[h3]", "[/h3]");
self::tagToBBCode($doc, 'h4', [], "[h4]", "[/h4]");
self::tagToBBCode($doc, 'h5', [], "[h5]", "[/h5]");
self::tagToBBCode($doc, 'h6', [], "[h6]", "[/h6]");
self::tagToBBCode($doc, 'a', ['href' => '/mailto:(.+)/'], '[mail=$1]', '[/mail]');
self::tagToBBCode($doc, 'a', ['href' => '/(.+)/'], '[url=$1]', '[/url]');
self::tagToBBCode($doc, 'img', ['src' => '/(.+)/', 'alt' => '/(.+)/'], '[img=$1]$2', '[/img]', true);
self::tagToBBCode($doc, 'img', ['src' => '/(.+)/', 'width' => '/(\d+)/', 'height' => '/(\d+)/'], '[img=$2x$3]$1', '[/img]', true);
self::tagToBBCode($doc, 'img', ['src' => '/(.+)/'], '[img]$1', '[/img]', true);
self::tagToBBCode($doc, 'video', ['src' => '/(.+)/'], '[video]$1', '[/video]', true);
self::tagToBBCode($doc, 'audio', ['src' => '/(.+)/'], '[audio]$1', '[/audio]', true);
self::tagToBBCode($doc, 'iframe', ['src' => '/(.+)/'], '[iframe]$1', '[/iframe]', true);
self::tagToBBCode($doc, 'key', [], '[code]', '[/code]');
self::tagToBBCode($doc, 'code', [], '[code]', '[/code]');
$message = $doc->saveHTML();
// I'm removing something really disturbing
// Don't know exactly what it is
$message = str_replace(chr(194) . chr(160), ' ', $message);
$message = str_replace("&nbsp;", " ", $message);
// removing multiple DIVs
$message = preg_replace('=\r *\r=i', "\n", $message);
$message = str_replace("\r", "\n", $message);
Hook::callAll('html2bbcode', $message);
$message = strip_tags($message);
$message = html_entity_decode($message, ENT_QUOTES, 'UTF-8');
// remove quotes if they don't make sense
$message = preg_replace('=\[/quote\][\s]*\[quote\]=i', "\n", $message);
$message = preg_replace('=\[quote\]\s*=i', "[quote]", $message);
$message = preg_replace('=\s*\[/quote\]=i', "[/quote]", $message);
do {
$oldmessage = $message;
$message = str_replace("\n \n", "\n\n", $message);
} while ($oldmessage != $message);
do {
$oldmessage = $message;
$message = str_replace("\n\n\n", "\n\n", $message);
} while ($oldmessage != $message);
do {
$oldmessage = $message;
$message = str_replace(
[
"[/size]\n\n",
"\n[hr]",
"[hr]\n",
"\n[list",
"[/list]\n",
"\n[/",
"[list]\n",
"[list=1]\n",
"\n[*]"],
[
"[/size]\n",
"[hr]",
"[hr]",
"[list",
"[/list]",
"[/",
"[list]",
"[list=1]",
"[*]"],
$message
);
} while ($message != $oldmessage);
$message = str_replace(
['[b][b]', '[/b][/b]', '[i][i]', '[/i][/i]'],
['[b]', '[/b]', '[i]', '[/i]'],
$message
);
// Handling Yahoo style of mails
$message = str_replace('[hr][b]From:[/b]', '[quote][b]From:[/b]', $message);
return $message;
});
$message = preg_replace_callback(
'#<pre><code(?: class="language-([^"]*)")?>(.*)</code></pre>#iUs',
function ($matches) use (&$codeblocks) {
$return = '[codeblock-' . count($codeblocks) . ']';
function ($matches) {
$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(
[
"<li><p>",
"</p></li>",
],
[
"<li>",
"</li>",
],
$message
);
// remove namespaces
$message = preg_replace('=<(\w+):(.+?)>=', '<removeme>', $message);
$message = preg_replace('=</(\w+):(.+?)>=', '</removeme>', $message);
$doc = new DOMDocument();
$doc->preserveWhiteSpace = false;
$message = mb_convert_encoding($message, 'HTML-ENTITIES', "UTF-8");
@$doc->loadHTML($message, LIBXML_HTML_NODEFDTD);
XML::deleteNode($doc, 'style');
XML::deleteNode($doc, 'head');
XML::deleteNode($doc, 'title');
XML::deleteNode($doc, 'meta');
XML::deleteNode($doc, 'xml');
XML::deleteNode($doc, 'removeme');
$xpath = new DomXPath($doc);
$list = $xpath->query("//pre");
foreach ($list as $node) {
// Ensure to escape unescaped & - they will otherwise raise a warning
$safe_value = preg_replace('/&(?!\w+;)/', '&amp;', $node->nodeValue);
$node->nodeValue = str_replace("\n", "\r", $safe_value);
}
$message = $doc->saveHTML();
$message = str_replace(["\n<", ">\n", "\r", "\n", "\xC3\x82\xC2\xA0"], ["<", ">", "<br />", " ", ""], $message);
$message = preg_replace('= [\s]*=i', " ", $message);
@$doc->loadHTML($message, LIBXML_HTML_NODEFDTD);
self::tagToBBCode($doc, 'html', [], "", "");
self::tagToBBCode($doc, 'body', [], "", "");
// Outlook-Quote - Variant 1
self::tagToBBCode($doc, 'p', ['class' => 'MsoNormal', 'style' => 'margin-left:35.4pt'], '[quote]', '[/quote]');
// Outlook-Quote - Variant 2
self::tagToBBCode(
$doc,
'div',
['style' => 'border:none;border-left:solid blue 1.5pt;padding:0cm 0cm 0cm 4.0pt'],
'[quote]',
'[/quote]'
);
// MyBB-Stuff
self::tagToBBCode($doc, 'span', ['style' => 'text-decoration: underline;'], '[u]', '[/u]');
self::tagToBBCode($doc, 'span', ['style' => 'font-style: italic;'], '[i]', '[/i]');
self::tagToBBCode($doc, 'span', ['style' => 'font-weight: bold;'], '[b]', '[/b]');
/* self::node2BBCode($doc, 'font', array('face'=>'/([\w ]+)/', 'size'=>'/(\d+)/', 'color'=>'/(.+)/'), '[font=$1][size=$2][color=$3]', '[/color][/size][/font]');
self::node2BBCode($doc, 'font', array('size'=>'/(\d+)/', 'color'=>'/(.+)/'), '[size=$1][color=$2]', '[/color][/size]');
self::node2BBCode($doc, 'font', array('face'=>'/([\w ]+)/', 'size'=>'/(.+)/'), '[font=$1][size=$2]', '[/size][/font]');
self::node2BBCode($doc, 'font', array('face'=>'/([\w ]+)/', 'color'=>'/(.+)/'), '[font=$1][color=$3]', '[/color][/font]');
self::node2BBCode($doc, 'font', array('face'=>'/([\w ]+)/'), '[font=$1]', '[/font]');
self::node2BBCode($doc, 'font', array('size'=>'/(\d+)/'), '[size=$1]', '[/size]');
self::node2BBCode($doc, 'font', array('color'=>'/(.+)/'), '[color=$1]', '[/color]');
*/
// Untested
//self::node2BBCode($doc, 'span', array('style'=>'/.*font-size:\s*(.+?)[,;].*font-family:\s*(.+?)[,;].*color:\s*(.+?)[,;].*/'), '[size=$1][font=$2][color=$3]', '[/color][/font][/size]');
//self::node2BBCode($doc, 'span', array('style'=>'/.*font-size:\s*(\d+)[,;].*/'), '[size=$1]', '[/size]');
//self::node2BBCode($doc, 'span', array('style'=>'/.*font-size:\s*(.+?)[,;].*/'), '[size=$1]', '[/size]');
self::tagToBBCode($doc, 'span', ['style' => '/.*color:\s*(.+?)[,;].*/'], '[color="$1"]', '[/color]');
//self::node2BBCode($doc, 'span', array('style'=>'/.*font-family:\s*(.+?)[,;].*/'), '[font=$1]', '[/font]');
//self::node2BBCode($doc, 'div', array('style'=>'/.*font-family:\s*(.+?)[,;].*font-size:\s*(\d+?)pt.*/'), '[font=$1][size=$2]', '[/size][/font]');
//self::node2BBCode($doc, 'div', array('style'=>'/.*font-family:\s*(.+?)[,;].*font-size:\s*(\d+?)px.*/'), '[font=$1][size=$2]', '[/size][/font]');
//self::node2BBCode($doc, 'div', array('style'=>'/.*font-family:\s*(.+?)[,;].*/'), '[font=$1]', '[/font]');
// Importing the classes - interesting for importing of posts from third party networks that were exported from friendica
// Test
//self::node2BBCode($doc, 'span', array('class'=>'/([\w ]+)/'), '[class=$1]', '[/class]');
self::tagToBBCode($doc, 'span', ['class' => 'type-link'], '[class=type-link]', '[/class]');
self::tagToBBCode($doc, 'span', ['class' => 'type-video'], '[class=type-video]', '[/class]');
self::tagToBBCode($doc, 'strong', [], '[b]', '[/b]');
self::tagToBBCode($doc, 'em', [], '[i]', '[/i]');
self::tagToBBCode($doc, 'b', [], '[b]', '[/b]');
self::tagToBBCode($doc, 'i', [], '[i]', '[/i]');
self::tagToBBCode($doc, 'u', [], '[u]', '[/u]');
self::tagToBBCode($doc, 's', [], '[s]', '[/s]');
self::tagToBBCode($doc, 'del', [], '[s]', '[/s]');
self::tagToBBCode($doc, 'strike', [], '[s]', '[/s]');
self::tagToBBCode($doc, 'big', [], "[size=large]", "[/size]");
self::tagToBBCode($doc, 'small', [], "[size=small]", "[/size]");
self::tagToBBCode($doc, 'blockquote', [], '[quote]', '[/quote]');
self::tagToBBCode($doc, 'br', [], "\n", '');
self::tagToBBCode($doc, 'p', ['class' => 'MsoNormal'], "\n", "");
self::tagToBBCode($doc, 'div', ['class' => 'MsoNormal'], "\r", "");
self::tagToBBCode($doc, 'span', [], "", "");
self::tagToBBCode($doc, 'span', [], "", "");
self::tagToBBCode($doc, 'pre', [], "", "");
self::tagToBBCode($doc, 'div', [], "\r", "\r");
self::tagToBBCode($doc, 'p', [], "\n", "\n");
self::tagToBBCode($doc, 'ul', [], "[list]", "[/list]");
self::tagToBBCode($doc, 'ol', [], "[list=1]", "[/list]");
self::tagToBBCode($doc, 'li', [], "[*]", "");
self::tagToBBCode($doc, 'hr', [], "[hr]", "");
self::tagToBBCode($doc, 'table', [], "[table]", "[/table]");
self::tagToBBCode($doc, 'th', [], "[th]", "[/th]");
self::tagToBBCode($doc, 'tr', [], "[tr]", "[/tr]");
self::tagToBBCode($doc, 'td', [], "[td]", "[/td]");
self::tagToBBCode($doc, 'h1', [], "[h1]", "[/h1]");
self::tagToBBCode($doc, 'h2', [], "[h2]", "[/h2]");
self::tagToBBCode($doc, 'h3', [], "[h3]", "[/h3]");
self::tagToBBCode($doc, 'h4', [], "[h4]", "[/h4]");
self::tagToBBCode($doc, 'h5', [], "[h5]", "[/h5]");
self::tagToBBCode($doc, 'h6', [], "[h6]", "[/h6]");
self::tagToBBCode($doc, 'a', ['href' => '/mailto:(.+)/'], '[mail=$1]', '[/mail]');
self::tagToBBCode($doc, 'a', ['href' => '/(.+)/'], '[url=$1]', '[/url]');
self::tagToBBCode($doc, 'img', ['src' => '/(.+)/', 'alt' => '/(.+)/'], '[img=$1]$2', '[/img]', true);
self::tagToBBCode($doc, 'img', ['src' => '/(.+)/', 'width' => '/(\d+)/', 'height' => '/(\d+)/'], '[img=$2x$3]$1', '[/img]', true);
self::tagToBBCode($doc, 'img', ['src' => '/(.+)/'], '[img]$1', '[/img]', true);
self::tagToBBCode($doc, 'video', ['src' => '/(.+)/'], '[video]$1', '[/video]', true);
self::tagToBBCode($doc, 'audio', ['src' => '/(.+)/'], '[audio]$1', '[/audio]', true);
self::tagToBBCode($doc, 'iframe', ['src' => '/(.+)/'], '[iframe]$1', '[/iframe]', true);
self::tagToBBCode($doc, 'key', [], '[code]', '[/code]');
self::tagToBBCode($doc, 'code', [], '[code]', '[/code]');
$message = $doc->saveHTML();
// I'm removing something really disturbing
// Don't know exactly what it is
$message = str_replace(chr(194) . chr(160), ' ', $message);
$message = str_replace("&nbsp;", " ", $message);
// removing multiple DIVs
$message = preg_replace('=\r *\r=i', "\n", $message);
$message = str_replace("\r", "\n", $message);
Hook::callAll('html2bbcode', $message);
$message = strip_tags($message);
$message = html_entity_decode($message, ENT_QUOTES, 'UTF-8');
// remove quotes if they don't make sense
$message = preg_replace('=\[/quote\][\s]*\[quote\]=i', "\n", $message);
$message = preg_replace('=\[quote\]\s*=i', "[quote]", $message);
$message = preg_replace('=\s*\[/quote\]=i', "[/quote]", $message);
do {
$oldmessage = $message;
$message = str_replace("\n \n", "\n\n", $message);
} while ($oldmessage != $message);
do {
$oldmessage = $message;
$message = str_replace("\n\n\n", "\n\n", $message);
} while ($oldmessage != $message);
do {
$oldmessage = $message;
$message = str_replace(
[
"[/size]\n\n",
"\n[hr]",
"[hr]\n",
"\n[list",
"[/list]\n",
"\n[/",
"[list]\n",
"[list=1]\n",
"\n[*]"],
[
"[/size]\n",
"[hr]",
"[hr]",
"[list",
"[/list]",
"[/",
"[list]",
"[list=1]",
"[*]"],
$message
);
} while ($message != $oldmessage);
$message = str_replace(
['[b][b]', '[/b][/b]', '[i][i]', '[/i][/i]'],
['[b]', '[/b]', '[i]', '[/i]'],
$message
);
// Handling Yahoo style of mails
$message = str_replace('[hr][b]From:[/b]', '[quote][b]From:[/b]', $message);
// Restore code blocks
$message = preg_replace_callback(
'#\[codeblock-([0-9]+)\]#iU',
function ($matches) use ($codeblocks) {
$return = '';
if (isset($codeblocks[intval($matches[1])])) {
$return = $codeblocks[$matches[1]];
}
return $return;
return $prefix . PHP_EOL . trim($matches[2]) . PHP_EOL . '[/code]';
},
$message
);

View file

@ -1780,7 +1780,7 @@ class Item
// 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
self::putInCache($item);
@ -2424,84 +2424,69 @@ 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?
if (!count($tags)) {
return false;
}
// 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
// Otherwise there could be problems with hashtags like #test and #test2
// Because of this we are sorting from the longest to the shortest tag.
usort($tags, function($a, $b) {
return strlen($b) <=> strlen($a);
});
$URLSearchString = "^\[\]";
// All hashtags should point to the home server if "local_tags" is activated
if (DI::config()->get('system', 'local_tags')) {
$item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
"#[url=".DI::baseUrl()."/search?tag=$2]$2[/url]", $item["body"]);
}
// mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
$item["body"] = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
function ($match) {
return ("[url=" . str_replace("#", "&num;", $match[1]) . "]" . str_replace("#", "&num;", $match[2]) . "[/url]");
}, $item["body"]);
$item["body"] = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",
function ($match) {
return ("[bookmark=" . str_replace("#", "&num;", $match[1]) . "]" . str_replace("#", "&num;", $match[2]) . "[/bookmark]");
}, $item["body"]);
$item["body"] = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
function ($match) {
return ("[attachment " . str_replace("#", "&num;", $match[1]) . "]" . $match[2] . "[/attachment]");
}, $item["body"]);
// Repair recursive urls
$item["body"] = preg_replace("/&num;\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
"&num;$2", $item["body"]);
foreach ($tags as $tag) {
if ((strpos($tag, '#') !== 0) || strpos($tag, '[url=') || strlen($tag) < 2 || $tag[1] == '#') {
continue;
// No hashtags?
if (!count($tags)) {
return $body;
}
$basetag = str_replace('_',' ',substr($tag,1));
$newtag = '#[url=' . DI::baseUrl() . '/search?tag=' . $basetag . ']' . $basetag . '[/url]';
// 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
// Because of this we are sorting from the longest to the shortest tag.
usort($tags, function ($a, $b) {
return strlen($b) <=> strlen($a);
});
$item["body"] = str_replace($tag, $newtag, $item["body"]);
}
$URLSearchString = "^\[\]";
// Convert back the masked hashtags
$item["body"] = str_replace("&num;", "#", $item["body"]);
// All hashtags should point to the home server if "local_tags" is activated
if (DI::config()->get('system', 'local_tags')) {
$body = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
"#[url=" . DI::baseUrl() . "/search?tag=$2]$2[/url]", $body);
}
// Remember! What happens in [code], stays in [code]
// roleback the # and [
$item["body"] = preg_replace_callback("/\[code(.*?)\](.*?)\[\/code\]/ism",
function ($match) {
// 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"]);
// mask hashtags inside of url, bookmarks and attachments to avoid urls in urls
$body = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
function ($match) {
return ("[url=" . str_replace("#", "&num;", $match[1]) . "]" . str_replace("#", "&num;", $match[2]) . "[/url]");
}, $body);
$body = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism",
function ($match) {
return ("[bookmark=" . str_replace("#", "&num;", $match[1]) . "]" . str_replace("#", "&num;", $match[2]) . "[/bookmark]");
}, $body);
$body = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
function ($match) {
return ("[attachment " . str_replace("#", "&num;", $match[1]) . "]" . $match[2] . "[/attachment]");
}, $body);
// Repair recursive urls
$body = preg_replace("/&num;\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
"&num;$2", $body);
foreach ($tags as $tag) {
if ((strpos($tag, '#') !== 0) || strpos($tag, '[url=') || strlen($tag) < 2 || $tag[1] == '#') {
continue;
}
$basetag = str_replace('_', ' ', substr($tag, 1));
$newtag = '#[url=' . DI::baseUrl() . '/search?tag=' . $basetag . ']' . $basetag . '[/url]';
$body = str_replace($tag, $newtag, $body);
}
// Convert back the masked hashtags
$body = str_replace("&num;", "#", $body);
return $body;
});
return $body;
}
/**

View file

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

View file

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