Hook::callAll('bbcode', $text);
- $a = DI::app();
-
- $text = self::performWithEscapedTags($text, ['code'], function ($text) use ($try_oembed, $simple_html, $for_plaintext, $a, $uriid) {
- $text = self::performWithEscapedTags($text, ['noparse', 'nobb', 'pre'], function ($text) use ($try_oembed, $simple_html, $for_plaintext, $a, $uriid) {
+ $text = self::performWithEscapedTags($text, ['code'], function ($text) use ($try_oembed, $simple_html, $for_plaintext, $uriid) {
+ $text = self::performWithEscapedTags($text, ['noparse', 'nobb', 'pre'], function ($text) use ($try_oembed, $simple_html, $for_plaintext, $uriid) {
/*
* preg_match_callback function to replace potential Oembed tags with Oembed content
*
return $return;
};
- // Remove the abstract element. It is a non visible element.
- $text = self::stripAbstract($text);
-
- // Line ending normalisation
- $text = str_replace("\r\n", "\n", $text);
-
- // Move new lines outside of tags
- $text = preg_replace("#\[(\w*)](\n*)#ism", '$2[$1]', $text);
- $text = preg_replace("#(\n*)\[/(\w*)]#ism", '[/$2]$1', $text);
-
// Extract the private images which use data urls since preg has issues with
// large data sizes. Stash them away while we do bbcode conversion, and then put them back
// in after we've done all the regex matching. We cannot use any preg functions to do this.
-
$extracted = self::extractImagesFromItemBody($text);
- $text = $extracted['body'];
$saved_image = $extracted['images'];
- // If we find any event code, turn it into an event.
- // After we're finished processing the bbcode we'll
- // replace all of the event code with a reformatted version.
+ // General clean up of the content, for example unneeded blanks and new lines
+ $text = self::normaliseInput($extracted['body']);
+
+ // Now the structural elements are converted
+ $text = self::convertHeaderToHtml($text, $simple_html);
+ $text = self::convertStylesToHtml($text, $simple_html);
+ $text = self::convertListsToHtml($text);
+ $text = self::convertTablesToHtml($text);
+ $text = self::convertSpoilersToHtml($text);
+ $text = self::convertStructuresToHtml($text);
+
+ // We add URL without a surrounding URL at this time, since at a earlier stage it would had been too early,
+ // since the used regular expression won't touch URL inside of BBCode elements, but with the structural ones it should.
+ // At a later stage we won't be able to exclude certain parts of the code.
+ $text = self::performWithEscapedTags($text, ['url', 'img', 'audio', 'video', 'youtube', 'vimeo', 'share', 'attachment', 'iframe', 'bookmark', 'map', 'oembed'], function ($text) use ($simple_html, $for_plaintext) {
+ if (!$for_plaintext) {
+ $text = preg_replace(Strings::autoLinkRegEx(), '[url]$1[/url]', $text);
+ }
+ return self::convertSmileysToHtml($text, $simple_html, $for_plaintext);
+ });
+
+ // Now for some more complex BBCode elements (mostly non standard ones)
+ $text = self::convertAttachmentsToHtml($text, $simple_html, $try_oembed, $uriid);
+ $text = self::convertMapsToHtml($text, $simple_html);
+ $text = self::convertQuotesToHtml($text);
+ $text = self::convertVideoPlatformsToHtml($text, $try_oembed);
+ $text = self::convertOEmbedToHtml($text, $uriid);
+ $text = self::convertEventsToHtml($text, $simple_html, $uriid);
+
+ // Some simpler non standard elements
+ $text = self::convertEmojisToHtml($text, $simple_html);
+ $text = self::convertCryptToHtml($text);
+ $text = self::convertIFramesToHtml($text);
+ $text = self::convertMailToHtml($text);
+ $text = self::convertAudioVideoToHtml($text, $simple_html, $try_oembed, $try_oembed_callback);
+
+ // At last, some standard elements. URL has to go last,
+ // since some previous conversions use URL elements.
+ $text = self::convertImagesToHtml($text, $simple_html, $uriid);
+ $text = self::convertUrlToHtml($text, $simple_html, $for_plaintext, $try_oembed, $try_oembed_callback);
- $ev = Event::fromBBCode($text);
+ // If the post only consists of an emoji, we display it larger than normal.
+ if (!$for_plaintext && DI::config()->get('system', 'big_emojis') && ($simple_html != self::DIASPORA) && Smilies::isEmojiPost($text)) {
+ $text = '<span style="font-size: xx-large; line-height: normal;">' . $text . '</span>';
+ }
- // Replace any html brackets with HTML Entities to prevent executing HTML or script
- // Don't use strip_tags here because it breaks [url] search by replacing & with amp
+ // Sanitize the created HTML.
+ $text = self::cleanupHtml($text);
- $text = str_replace("<", "<", $text);
- $text = str_replace(">", ">", $text);
+ // This needs to be called after the cleanup, since otherwise some links are invalidated
+ $text = self::convertSharesToHtml($text, $simple_html, $try_oembed, $uriid);
- // remove some newlines before the general conversion
- $text = preg_replace("/\s?\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "\n[share$1]$2[/share]\n", $text);
- $text = preg_replace("/\s?\[quote(.*?)\]\s?(.*?)\s?\[\/quote\]\s?/ism", "\n[quote$1]$2[/quote]\n", $text);
+ // Insert the previously extracted embedded image again.
+ return self::interpolateSavedImagesIntoItemBody($uriid, $text, $saved_image);
+ }); // Escaped noparse, nobb, pre
- // when the content is meant exporting to other systems then remove the avatar picture since this doesn't really look good on these systems
- if (!$try_oembed) {
- $text = preg_replace("/\[share(.*?)avatar\s?=\s?'.*?'\s?(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "\n[share$1$2]$3[/share]", $text);
- }
+ // Remove escaping tags and replace new lines that remain
+ $text = preg_replace_callback('/\[(noparse|nobb)](.*?)\[\/\1]/ism', function ($match) {
+ return str_replace("\n", "<br>", $match[2]);
+ }, $text);
- // Remove linefeeds inside of the table elements. See issue #6799
- $search = [
- "\n[th]", "[th]\n", " [th]", "\n[/th]", "[/th]\n", "[/th] ",
- "\n[td]", "[td]\n", " [td]", "\n[/td]", "[/td]\n", "[/td] ",
- "\n[tr]", "[tr]\n", " [tr]", "[tr] ", "\n[/tr]", "[/tr]\n", " [/tr]", "[/tr] ",
- "\n[hr]", "[hr]\n", " [hr]", "[hr] ",
- "\n[attachment ", " [attachment ", "\n[/attachment]", "[/attachment]\n", " [/attachment]", "[/attachment] ",
- "[table]\n", "[table] ", " [table]", "\n[/table]", " [/table]", "[/table] ",
- " \n", "\t\n", "[/li]\n", "\n[li]", "\n[*]",
- ];
- $replace = [
- "[th]", "[th]", "[th]", "[/th]", "[/th]", "[/th]",
- "[td]", "[td]", "[td]", "[/td]", "[/td]", "[/td]",
- "[tr]", "[tr]", "[tr]", "[tr]", "[/tr]", "[/tr]", "[/tr]", "[/tr]",
- "[hr]", "[hr]", "[hr]", "[hr]",
- "[attachment ", "[attachment ", "[/attachment]", "[/attachment]", "[/attachment]", "[/attachment]",
- "[table]", "[table]", "[table]", "[/table]", "[/table]", "[/table]",
- "\n", "\n", "[/li]", "[li]", "[*]",
- ];
- do {
- $oldtext = $text;
- $text = str_replace($search, $replace, $text);
- } while ($oldtext != $text);
-
- // Replace these here only once
- $search = ["\n[table]", "[/table]\n"];
- $replace = ["[table]", "[/table]"];
- $text = str_replace($search, $replace, $text);
+ // Additionally, [pre] tags preserve spaces
+ $text = preg_replace_callback("/\[pre\](.*?)\[\/pre\]/ism", function ($match) {
+ return str_replace([' ', "\n"], [' ', "<br>"], htmlentities($match[1], ENT_NOQUOTES, 'UTF-8'));
+ }, $text);
- // Trim new lines regardless of the system.remove_multiplicated_lines config value
- $text = trim($text, "\n");
-
- // removing multiplicated newlines
- if (DI::config()->get('system', 'remove_multiplicated_lines')) {
- $search = [
- "\n\n\n", "[/quote]\n\n", "\n[/quote]", "\n[ul]", "[/ul]\n", "\n[ol]", "[/ol]\n", "\n\n[share ", "[/attachment]\n",
- "\n[h1]", "[/h1]\n", "\n[h2]", "[/h2]\n", "\n[h3]", "[/h3]\n", "\n[h4]", "[/h4]\n", "\n[h5]", "[/h5]\n", "\n[h6]", "[/h6]\n"
- ];
- $replace = [
- "\n\n", "[/quote]\n", "[/quote]", "[ul]", "[/ul]", "[ol]", "[/ol]", "\n[share ", "[/attachment]",
- "[h1]", "[/h1]", "[h2]", "[/h2]", "[h3]", "[/h3]", "[h4]", "[/h4]", "[h5]", "[/h5]", "[h6]", "[/h6]"
- ];
- do {
- $oldtext = $text;
- $text = str_replace($search, $replace, $text);
- } while ($oldtext != $text);
- }
+ return $text;
+ }); // Escaped code
- /// @todo Have a closer look at the different html modes
- // Handle attached links or videos
- if ($simple_html == self::NPF) {
- $text = self::removeAttachment($text);
- } elseif (in_array($simple_html, [self::MASTODON_API, self::TWITTER_API, self::ACTIVITYPUB])) {
- $text = self::replaceAttachment($text);
- } elseif (!in_array($simple_html, [self::INTERNAL, self::EXTERNAL, self::CONNECTORS])) {
- $text = self::replaceAttachment($text, true);
+ $text = preg_replace_callback(
+ "#\[code(?:=([^\]]*))?\](.*?)\[\/code\]#ism",
+ function ($matches) {
+ if (strpos($matches[2], "\n") !== false) {
+ $return = '<pre><code class="language-' . trim($matches[1]) . '">' . htmlentities(trim($matches[2], "\n\r"), ENT_NOQUOTES, 'UTF-8') . '</code></pre>';
} else {
- $text = self::convertAttachment($text, $simple_html, $try_oembed, [], $uriid);
+ $return = '<code>' . htmlentities($matches[2], ENT_NOQUOTES, 'UTF-8') . '</code>';
}
- $nosmile = strpos($text, '[nosmile]') !== false;
- $text = str_replace('[nosmile]', '', $text);
+ return $return;
+ },
+ $text
+ );
- // Replace non graphical smilies for external posts
- if (!$nosmile) {
- $text = self::performWithEscapedTags($text, ['url', 'img', 'audio', 'video', 'youtube', 'vimeo', 'share', 'attachment', 'iframe', 'bookmark'], function ($text) use ($simple_html, $for_plaintext) {
- return Smilies::replace($text, ($simple_html != self::INTERNAL) || $for_plaintext);
- });
- }
+ // Default iframe allowed domains/path
+ $allowedIframeDomains = DI::config()->get('system', 'no_oembed_rich_content') ? [] : ['www.youtube.com/embed/', 'player.vimeo.com/video/'];
- // leave open the possibility of [map=something]
- // this is replaced in Item::prepareBody() which has knowledge of the item location
- if (strpos($text, '[/map]') !== false) {
- $text = preg_replace_callback(
- "/\[map\](.*?)\[\/map\]/ism",
- function ($match) use ($simple_html) {
- return str_replace($match[0], '<p class="map">' . Map::byLocation($match[1], $simple_html) . '</p>', $match[0]);
- },
- $text
- );
- }
+ $allowedIframeDomains = array_merge(
+ $allowedIframeDomains,
+ DI::config()->get('system', 'allowed_oembed') ?
+ explode(',', DI::config()->get('system', 'allowed_oembed'))
+ : []
+ );
- if (strpos($text, '[map=') !== false) {
- $text = preg_replace_callback(
- "/\[map=(.*?)\]/ism",
- function ($match) use ($simple_html) {
- return str_replace($match[0], '<p class="map">' . Map::byCoordinates(str_replace('/', ' ', $match[1]), $simple_html) . '</p>', $match[0]);
- },
- $text
- );
- }
+ if (strpos($text, '<p>') !== false || strpos($text, '</p>') !== false) {
+ $text = '<p>' . $text . '</p>';
+ }
- if (strpos($text, '[map]') !== false) {
- $text = preg_replace("/\[map\]/", '<p class="map"></p>', $text);
- }
+ $text = HTML::purify($text, $allowedIframeDomains);
+ DI::profiler()->stopRecording();
- // Check for headers
+ return trim($text);
+ }
- if ($simple_html == self::INTERNAL) {
- //Ensure to always start with <h4> if possible
- $heading_count = 0;
- for ($level = 6; $level > 0; $level--) {
- if (preg_match("(\[h$level\].*?\[\/h$level\])ism", $text)) {
- $heading_count++;
- }
- }
- if ($heading_count > 0) {
- $heading = min($heading_count + 3, 6);
- for ($level = 6; $level > 0; $level--) {
- if (preg_match("(\[h$level\].*?\[\/h$level\])ism", $text)) {
- $text = preg_replace("(\[h$level\](.*?)\[\/h$level\])ism", "</p><h$heading>$1</h$heading><p>", $text);
- $heading--;
- }
- }
- }
- } else {
- $text = preg_replace("(\[h1\](.*?)\[\/h1\])ism", '</p><h1>$1</h1><p>', $text);
- $text = preg_replace("(\[h2\](.*?)\[\/h2\])ism", '</p><h2>$1</h2><p>', $text);
- $text = preg_replace("(\[h3\](.*?)\[\/h3\])ism", '</p><h3>$1</h3><p>', $text);
- $text = preg_replace("(\[h4\](.*?)\[\/h4\])ism", '</p><h4>$1</h4><p>', $text);
- $text = preg_replace("(\[h5\](.*?)\[\/h5\])ism", '</p><h5>$1</h5><p>', $text);
- $text = preg_replace("(\[h6\](.*?)\[\/h6\])ism", '</p><h6>$1</h6><p>', $text);
- }
+ private static function normaliseInput(string $text): string
+ {
+ // Remove the abstract element. It is a non visible element.
+ $text = self::stripAbstract($text);
+
+ // Line ending normalisation
+ $text = str_replace("\r\n", "\n", $text);
+
+ // Move new lines outside of tags
+ $text = preg_replace("#\[(\w*)](\n*)#ism", '$2[$1]', $text);
+ $text = preg_replace("#(\n*)\[/(\w*)]#ism", '[/$2]$1', $text);
+
+ // Replace any html brackets with HTML Entities to prevent executing HTML or script
+ // Don't use strip_tags here because it breaks [url] search by replacing & with amp
+
+ $text = str_replace("<", "<", $text);
+ $text = str_replace(">", ">", $text);
+
+ // remove some newlines before the general conversion
+ $text = preg_replace("/\s?\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "\n[share$1]$2[/share]\n", $text);
+ $text = preg_replace("/\s?\[quote(.*?)\]\s?(.*?)\s?\[\/quote\]\s?/ism", "\n[quote$1]$2[/quote]\n", $text);
+
+ // Remove linefeeds inside of the table elements. See issue #6799
+ $search = [
+ "\n[th]", "[th]\n", " [th]", "\n[/th]", "[/th]\n", "[/th] ",
+ "\n[td]", "[td]\n", " [td]", "\n[/td]", "[/td]\n", "[/td] ",
+ "\n[tr]", "[tr]\n", " [tr]", "[tr] ", "\n[/tr]", "[/tr]\n", " [/tr]", "[/tr] ",
+ "\n[hr]", "[hr]\n", " [hr]", "[hr] ",
+ "\n[attachment ", " [attachment ", "\n[/attachment]", "[/attachment]\n", " [/attachment]", "[/attachment] ",
+ "[table]\n", "[table] ", " [table]", "\n[/table]", " [/table]", "[/table] ",
+ " \n", "\t\n", "[/li]\n", "\n[li]", "\n[*]",
+ ];
+ $replace = [
+ "[th]", "[th]", "[th]", "[/th]", "[/th]", "[/th]",
+ "[td]", "[td]", "[td]", "[/td]", "[/td]", "[/td]",
+ "[tr]", "[tr]", "[tr]", "[tr]", "[/tr]", "[/tr]", "[/tr]", "[/tr]",
+ "[hr]", "[hr]", "[hr]", "[hr]",
+ "[attachment ", "[attachment ", "[/attachment]", "[/attachment]", "[/attachment]", "[/attachment]",
+ "[table]", "[table]", "[table]", "[/table]", "[/table]", "[/table]",
+ "\n", "\n", "[/li]", "[li]", "[*]",
+ ];
+ do {
+ $oldtext = $text;
+ $text = str_replace($search, $replace, $text);
+ } while ($oldtext != $text);
- // Check for paragraph
- $text = preg_replace("(\[p\](.*?)\[\/p\])ism", '<p>$1</p>', $text);
+ // Replace these here only once
+ $search = ["\n[table]", "[/table]\n"];
+ $replace = ["[table]", "[/table]"];
+ $text = str_replace($search, $replace, $text);
- // Check for bold text
- $text = preg_replace("(\[b\](.*?)\[\/b\])ism", '<strong>$1</strong>', $text);
+ // Trim new lines regardless of the system.remove_multiplicated_lines config value
+ $text = trim($text, "\n");
- // Check for Italics text
- $text = preg_replace("(\[i\](.*?)\[\/i\])ism", '<em>$1</em>', $text);
+ // removing multiplicated newlines
+ if (DI::config()->get('system', 'remove_multiplicated_lines')) {
+ $search = [
+ "\n\n\n", "[/quote]\n\n", "\n[/quote]", "\n[ul]", "[/ul]\n", "\n[ol]", "[/ol]\n", "\n\n[share ", "[/attachment]\n",
+ "\n[h1]", "[/h1]\n", "\n[h2]", "[/h2]\n", "\n[h3]", "[/h3]\n", "\n[h4]", "[/h4]\n", "\n[h5]", "[/h5]\n", "\n[h6]", "[/h6]\n"
+ ];
+ $replace = [
+ "\n\n", "[/quote]\n", "[/quote]", "[ul]", "[/ul]", "[ol]", "[/ol]", "\n[share ", "[/attachment]",
+ "[h1]", "[/h1]", "[h2]", "[/h2]", "[h3]", "[/h3]", "[h4]", "[/h4]", "[h5]", "[/h5]", "[h6]", "[/h6]"
+ ];
+ do {
+ $oldtext = $text;
+ $text = str_replace($search, $replace, $text);
+ } while ($oldtext != $text);
+ }
- // Check for Underline text
- $text = preg_replace("(\[u\](.*?)\[\/u\])ism", '<u>$1</u>', $text);
+ return $text;
+ }
- // Check for strike-through text
- $text = preg_replace("(\[s\](.*?)\[\/s\])ism", '<s>$1</s>', $text);
+ private static function convertEventsToHtml(string $text, int $simple_html, int $uriid): string
+ {
+ // If we find any event code, turn it into an event.
+ // After we're finished processing the bbcode we'll
+ // replace all of the event code with a reformatted version.
+
+ $ev = Event::fromBBCode($text);
+
+ // If we found an event earlier, strip out all the event code and replace with a reformatted version.
+ // Replace the event-start section with the entire formatted event. The other bbcode is stripped.
+ // Summary (e.g. title) is required, earlier revisions only required description (in addition to
+ // start which is always required). Allow desc with a missing summary for compatibility.
+
+ if ((!empty($ev['desc']) || !empty($ev['summary'])) && !empty($ev['start'])) {
+ $sub = Event::getHTML($ev, $simple_html, $uriid);
+
+ $text = preg_replace("/\[event\-summary\](.*?)\[\/event\-summary\]/ism", '', $text);
+ $text = preg_replace("/\[event\-description\](.*?)\[\/event\-description\]/ism", '', $text);
+ $text = preg_replace("/\[event\-start\](.*?)\[\/event\-start\]/ism", $sub, $text);
+ $text = preg_replace("/\[event\-finish\](.*?)\[\/event\-finish\]/ism", '', $text);
+ $text = preg_replace("/\[event\-location\](.*?)\[\/event\-location\]/ism", '', $text);
+ $text = preg_replace("/\[event\-id\](.*?)\[\/event\-id\]/ism", '', $text);
+ }
- // Check for over-line text
- $text = preg_replace("(\[o\](.*?)\[\/o\])ism", '<span class="overline">$1</span>', $text);
+ return $text;
+ }
- // Check for colored text
- $text = preg_replace("(\[color=(.*?)\](.*?)\[\/color\])ism", "<span style=\"color: $1;\">$2</span>", $text);
+ private static function convertAttachmentsToHtml(string $text, int $simple_html, bool $try_oembed, int $uriid): string
+ {
+ /// @todo Have a closer look at the different html modes
+ // Handle attached links or videos
+ if ($simple_html == self::NPF) {
+ $text = self::removeAttachment($text);
+ } elseif (in_array($simple_html, [self::MASTODON_API, self::TWITTER_API, self::ACTIVITYPUB])) {
+ $text = self::replaceAttachment($text);
+ } elseif (!in_array($simple_html, [self::INTERNAL, self::EXTERNAL, self::CONNECTORS])) {
+ $text = self::replaceAttachment($text, true);
+ } else {
+ $text = self::convertAttachment($text, $simple_html, $try_oembed, [], $uriid);
+ }
- // Check for sized text
- // [size=50] --> font-size: 50px (with the unit).
- if ($simple_html != self::DIASPORA) {
- $text = preg_replace("(\[size=(\d*?)\](.*?)\[\/size\])ism", '<span style="font-size:$1px;line-height:normal;">$2</span>', $text);
- $text = preg_replace("(\[size=(.*?)\](.*?)\[\/size\])ism", '<span style="font-size:$1;line-height:normal;">$2</span>', $text);
- } else {
- // Issue 2199: Diaspora doesn't interpret the construct above, nor the <small> or <big> element
- $text = preg_replace("(\[size=(.*?)\](.*?)\[\/size\])ism", "$2", $text);
- }
+ return $text;
+ }
+ private static function convertMapsToHtml(string $text, int $simple_html): string
+ {
+ // leave open the possibility of [map=something]
+ // this is replaced in Item::prepareBody() which has knowledge of the item location
+ if (strpos($text, '[/map]') !== false) {
+ $text = preg_replace_callback(
+ "/\[map\](.*?)\[\/map\]/ism",
+ function ($match) use ($simple_html) {
+ return str_replace($match[0], '<p class="map">' . Map::byLocation($match[1], $simple_html) . '</p>', $match[0]);
+ },
+ $text
+ );
+ }
- // Check for centered text
- $text = preg_replace("(\[center\](.*?)\[\/center\])ism", '<div style="text-align:center;">$1</div>', $text);
+ if (strpos($text, '[map=') !== false) {
+ $text = preg_replace_callback(
+ "/\[map=(.*?)\]/ism",
+ function ($match) use ($simple_html) {
+ return str_replace($match[0], '<p class="map">' . Map::byCoordinates(str_replace('/', ' ', $match[1]), $simple_html) . '</p>', $match[0]);
+ },
+ $text
+ );
+ }
- // Check for block-level custom CSS
- $text = preg_replace('#(?<=^|\n)\[style=(.*?)](.*?)\[/style](?:\n|$)#ism', '<div style="$1">$2</div>', $text);
+ if (strpos($text, '[map]') !== false) {
+ $text = preg_replace("/\[map\]/", '<p class="map"></p>', $text);
+ }
- // Check for inline custom CSS
- $text = preg_replace("(\[style=(.*?)\](.*?)\[\/style\])ism", '<span style="$1">$2</span>', $text);
+ return $text;
+ }
- // Mastodon Emoji (internal tag, do not document for users)
- if ($simple_html == self::MASTODON_API) {
- $text = preg_replace("(\[emoji=(.*?)](.*?)\[/emoji])ism", '$2', $text);
- } else {
- $text = preg_replace("(\[emoji=(.*?)](.*?)\[/emoji])ism", '<span class="mastodon emoji"><img src="$1" alt="$2" title="$2"/></span>', $text);
+ private static function convertHeaderToHtml(string $text, int $simple_html): string
+ {
+ // Check for headers
+
+ if ($simple_html == self::INTERNAL) {
+ //Ensure to always start with <h4> if possible
+ $heading_count = 0;
+ for ($level = 6; $level > 0; $level--) {
+ if (preg_match("(\[h$level\].*?\[\/h$level\])ism", $text)) {
+ $heading_count++;
}
-
- // Check for CSS classes
- // @deprecated since 2021.12, left for backward-compatibility reasons
- $text = preg_replace("(\[class=(.*?)\](.*?)\[\/class\])ism", '<span class="$1">$2</span>', $text);
- // Add HTML new lines
- $text = str_replace("\n\n", '</p><p>', $text);
- $text = str_replace("\n", '<br>', $text);
-
- // handle nested lists
- $endlessloop = 0;
-
- while ((((strpos($text, "[/list]") !== false) && (strpos($text, "[list") !== false)) ||
- ((strpos($text, "[/ol]") !== false) && (strpos($text, "[ol]") !== false)) ||
- ((strpos($text, "[/ul]") !== false) && (strpos($text, "[ul]") !== false)) ||
- ((strpos($text, "[/li]") !== false) && (strpos($text, "[li]") !== false))) && (++$endlessloop < 20)) {
- $text = preg_replace("/\[list\](.*?)\[\/list\]/ism", '</p><ul class="listbullet" style="list-style-type: circle;">$1</ul><p>', $text);
- $text = preg_replace("/\[list=\](.*?)\[\/list\]/ism", '</p><ul class="listnone" style="list-style-type: none;">$1</ul><p>', $text);
- $text = preg_replace("/\[list=1\](.*?)\[\/list\]/ism", '</p><ul class="listdecimal" style="list-style-type: decimal;">$1</ul><p>', $text);
- $text = preg_replace("/\[list=((?-i)i)\](.*?)\[\/list\]/ism", '</p><ul class="listlowerroman" style="list-style-type: lower-roman;">$2</ul><p>', $text);
- $text = preg_replace("/\[list=((?-i)I)\](.*?)\[\/list\]/ism", '</p><ul class="listupperroman" style="list-style-type: upper-roman;">$2</ul><p>', $text);
- $text = preg_replace("/\[list=((?-i)a)\](.*?)\[\/list\]/ism", '</p><ul class="listloweralpha" style="list-style-type: lower-alpha;">$2</ul><p>', $text);
- $text = preg_replace("/\[list=((?-i)A)\](.*?)\[\/list\]/ism", '</p><ul class="listupperalpha" style="list-style-type: upper-alpha;">$2</ul><p>', $text);
- $text = preg_replace("/\[ul\](.*?)\[\/ul\]/ism", '</p><ul>$1</ul><p>', $text);
- $text = preg_replace("/\[ol\](.*?)\[\/ol\]/ism", '</p><ol>$1</ol><p>', $text);
- $text = preg_replace("/\[li\](.*?)\[\/li\]/ism", '<li>$1</li>', $text);
+ }
+ if ($heading_count > 0) {
+ $heading = min($heading_count + 3, 6);
+ for ($level = 6; $level > 0; $level--) {
+ if (preg_match("(\[h$level\].*?\[\/h$level\])ism", $text)) {
+ $text = preg_replace("(\[h$level\](.*?)\[\/h$level\])ism", "</p><h$heading>$1</h$heading><p>", $text);
+ $heading--;
+ }
}
+ }
+ } else {
+ $text = preg_replace("(\[h1\](.*?)\[\/h1\])ism", '</p><h1>$1</h1><p>', $text);
+ $text = preg_replace("(\[h2\](.*?)\[\/h2\])ism", '</p><h2>$1</h2><p>', $text);
+ $text = preg_replace("(\[h3\](.*?)\[\/h3\])ism", '</p><h3>$1</h3><p>', $text);
+ $text = preg_replace("(\[h4\](.*?)\[\/h4\])ism", '</p><h4>$1</h4><p>', $text);
+ $text = preg_replace("(\[h5\](.*?)\[\/h5\])ism", '</p><h5>$1</h5><p>', $text);
+ $text = preg_replace("(\[h6\](.*?)\[\/h6\])ism", '</p><h6>$1</h6><p>', $text);
+ }
- // Check for list text
- $text = str_replace("[*]", "<li>", $text);
- $text = str_replace("[li]", "<li>", $text);
-
- $text = preg_replace("/\[th\](.*?)\[\/th\]/sm", '<th>$1</th>', $text);
- $text = preg_replace("/\[td\](.*?)\[\/td\]/sm", '<td>$1</td>', $text);
- $text = preg_replace("/\[tr\](.*?)\[\/tr\]/sm", '<tr>$1</tr>', $text);
- $text = preg_replace("/\[table\](.*?)\[\/table\]/sm", '</p><table>$1</table><p>', $text);
-
- $text = preg_replace("/\[table border=1\](.*?)\[\/table\]/sm", '</p><table border="1" >$1</table><p>', $text);
- $text = preg_replace("/\[table border=0\](.*?)\[\/table\]/sm", '</p><table border="0" >$1</table><p>', $text);
-
- $text = str_replace('[hr]', '</p><hr /><p>', $text);
+ return $text;
+ }
- if (!$for_plaintext) {
- $text = self::performWithEscapedTags($text, ['url', 'img', 'audio', 'video', 'youtube', 'vimeo', 'share', 'attachment', 'iframe', 'bookmark'], function ($text) {
- return preg_replace(Strings::autoLinkRegEx(), '[url]$1[/url]', $text);
- });
- }
+ private static function convertEmojisToHtml(string $text, int $simple_html): string
+ {
+ // Mastodon Emoji (internal tag, do not document for users)
+ if ($simple_html == self::MASTODON_API) {
+ $text = preg_replace("(\[emoji=(.*?)](.*?)\[/emoji])ism", '$2', $text);
+ } else {
+ $text = preg_replace("(\[emoji=(.*?)](.*?)\[/emoji])ism", '<span class="mastodon emoji"><img src="$1" alt="$2" title="$2"/></span>', $text);
+ }
+ return $text;
+ }
- // Check for font change text
- $text = preg_replace("/\[font=(.*?)\](.*?)\[\/font\]/sm", "<span style=\"font-family: $1;\">$2</span>", $text);
+ private static function convertStylesToHtml(string $text, int $simple_html): string
+ {
+ // Markdown is designed to pass through HTML elements that it can't handle itself,
+ // so that the other system would parse the original HTML element.
+ // But Diaspora has chosen not to do this and doesn't parse HTML elements.
+ // So we need to make some changes here.
+ if ($simple_html == BBCode::DIASPORA) {
+ $elements = ['big', 'small'];
+ foreach ($elements as $bbcode) {
+ $text = preg_replace("(\[" . $bbcode . "\](.*?)\[\/" . $bbcode . "\])ism", '$1', $text);
+ }
+
+ $elements = ['del' => 's', 'ins' => 'em', 'kbd' => 'code', 'mark' => 'strong',
+ 'samp' => 'code', 'u' => 'em', 'var' => 'em'];
+ foreach ($elements as $bbcode => $html) {
+ $text = preg_replace("(\[" . $bbcode . "\](.*?)\[\/" . $bbcode . "\])ism", '<' . $html . '>$1</' . $html . '>', $text);
+ }
+ }
- // Declare the format for [spoiler] layout
- $SpoilerLayout = '<details class="spoiler"><summary>' . DI::l10n()->t('Click to open/close') . '</summary>$1</details>';
+ // Several easy to replace HTML elements
+ // @todo add the new elements to the documentation by the end of 2024 so that most systems will support them.
+ $elements = ['b', 'del', 'em', 'i', 'ins', 'kbd', 'mark',
+ 's', 'samp', 'small', 'strong', 'sub', 'sup', 'u', 'var'];
+ foreach ($elements as $element) {
+ $text = preg_replace("(\[" . $element . "\](.*?)\[\/" . $element . "\])ism", '<' . $element . '>$1</' . $element . '>', $text);
+ }
- // Check for [spoiler] text
- // handle nested quotes
- $endlessloop = 0;
- while ((strpos($text, "[/spoiler]") !== false) && (strpos($text, "[spoiler]") !== false) && (++$endlessloop < 20)) {
- $text = preg_replace("/\[spoiler\](.*?)\[\/spoiler\]/ism", $SpoilerLayout, $text);
- }
+ $text = preg_replace("(\[big\](.*?)\[\/big\])ism", "<span style=\"font-size: larger;\">$1</span>", $text);
- // Check for [spoiler=Title] text
+ // Check for over-line text
+ $text = preg_replace("(\[o\](.*?)\[\/o\])ism", '<span class="overline">$1</span>', $text);
- // handle nested quotes
- $endlessloop = 0;
- while ((strpos($text, "[/spoiler]") !== false) && (strpos($text, "[spoiler=") !== false) && (++$endlessloop < 20)) {
- $text = preg_replace(
- "/\[spoiler=[\"\']*(.*?)[\"\']*\](.*?)\[\/spoiler\]/ism",
- '<details class="spoiler"><summary>$1</summary>$2</details>',
- $text
- );
- }
+ // Check for colored text
+ $text = preg_replace("(\[color=(.*?)\](.*?)\[\/color\])ism", "<span style=\"color: $1;\">$2</span>", $text);
- // Declare the format for [quote] layout
- $QuoteLayout = '</p><blockquote>$1</blockquote><p>';
+ // Check for sized text
+ // [size=50] --> font-size: 50px (with the unit).
+ if ($simple_html != self::DIASPORA) {
+ $text = preg_replace("(\[size=(\d*?)\](.*?)\[\/size\])ism", '<span style="font-size:$1px;line-height:normal;">$2</span>', $text);
+ $text = preg_replace("(\[size=(.*?)\](.*?)\[\/size\])ism", '<span style="font-size:$1;line-height:normal;">$2</span>', $text);
+ } else {
+ // Issue 2199: Diaspora doesn't interpret the construct above, nor the <small> or <big> element
+ $text = preg_replace("(\[size=(.*?)\](.*?)\[\/size\])ism", "$2", $text);
+ }
- // Check for [quote] text
- // handle nested quotes
- $endlessloop = 0;
- while ((strpos($text, "[/quote]") !== false) && (strpos($text, "[quote]") !== false) && (++$endlessloop < 20)) {
- $text = preg_replace("/\[quote\](.*?)\[\/quote\]/ism", "$QuoteLayout", $text);
- }
+ // Check for centered text
+ $text = preg_replace("(\[center\](.*?)\[\/center\])ism", '<div style="text-align:center;">$1</div>', $text);
- // Check for [quote=Author] text
+ // Check for block-level custom CSS
+ $text = preg_replace('#(?<=^|\n)\[style=(.*?)](.*?)\[/style](?:\n|$)#ism', '<div style="$1">$2</div>', $text);
- $t_wrote = DI::l10n()->t('$1 wrote:');
+ // Check for inline custom CSS
+ $text = preg_replace("(\[style=(.*?)\](.*?)\[\/style\])ism", '<span style="$1">$2</span>', $text);
- // handle nested quotes
- $endlessloop = 0;
- while ((strpos($text, "[/quote]") !== false) && (strpos($text, "[quote=") !== false) && (++$endlessloop < 20)) {
- $text = preg_replace(
- "/\[quote=[\"\']*(.*?)[\"\']*\](.*?)\[\/quote\]/ism",
- "<p><strong class=" . '"author"' . ">" . $t_wrote . "</strong></p><blockquote>$2</blockquote>",
- $text
- );
- }
+ // Check for CSS classes
+ // @deprecated since 2021.12, left for backward-compatibility reasons
+ $text = preg_replace("(\[class=(.*?)\](.*?)\[\/class\])ism", '<span class="$1">$2</span>', $text);
+ // Add HTML new lines
+ $text = str_replace("\n\n", '</p><p>', $text);
+ $text = str_replace("\n", '<br>', $text);
+ // Check for font change text
+ $text = preg_replace("/\[font=(.*?)\](.*?)\[\/font\]/sm", "<span style=\"font-family: $1;\">$2</span>", $text);
- // [img=widthxheight]image source[/img]
- $text = preg_replace_callback(
- "/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism",
- function ($matches) use ($simple_html, $uriid) {
- if (strpos($matches[3], "data:image/") === 0) {
- return $matches[0];
- }
+ return $text;
+ }
+
+ private static function convertTablesToHtml(string $text): string
+ {
+ $text = preg_replace("/\[th\](.*?)\[\/th\]/sm", '<th>$1</th>', $text);
+ $text = preg_replace("/\[td\](.*?)\[\/td\]/sm", '<td>$1</td>', $text);
+ $text = preg_replace("/\[tr\](.*?)\[\/tr\]/sm", '<tr>$1</tr>', $text);
+ $text = preg_replace("/\[table\](.*?)\[\/table\]/sm", '</p><table>$1</table><p>', $text);
- $matches[3] = self::proxyUrl($matches[3], $simple_html, $uriid);
- return "[img=" . $matches[1] . "x" . $matches[2] . "]" . $matches[3] . "[/img]";
- },
- $text
- );
-
- $text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '<img src="$3" style="width: $1px;" >', $text);
- $text = preg_replace("/\[zmg\=([0-9]*)x([0-9]*)\](.*?)\[\/zmg\]/ism", '<img class="zrl" src="$3" style="width: $1px;" >', $text);
-
- $text = preg_replace_callback(
- "/\[[iz]mg\=(.*?)\](.*?)\[\/[iz]mg\]/ism",
- function ($matches) use ($simple_html, $uriid) {
- $matches[1] = self::proxyUrl($matches[1], $simple_html, $uriid);
- $alt = htmlspecialchars($matches[2], ENT_COMPAT);
- // Fix for Markdown problems with Diaspora, see issue #12701
- if (($simple_html != self::DIASPORA) || strpos($matches[2], '"') === false) {
- return '<img src="' . $matches[1] . '" alt="' . $alt . '" title="' . $alt . '">';
- } else {
- return '<img src="' . $matches[1] . '" alt="' . $alt . '">';
- }
- },
- $text
- );
-
- // Images
- // [img]pathtoimage[/img]
- $text = preg_replace_callback(
- "/\[[iz]mg\](.*?)\[\/[iz]mg\]/ism",
- function ($matches) use ($simple_html, $uriid) {
- if (strpos($matches[1], "data:image/") === 0) {
- return $matches[0];
- }
+ $text = preg_replace("/\[table border=1\](.*?)\[\/table\]/sm", '</p><table border="1" >$1</table><p>', $text);
+ $text = preg_replace("/\[table border=0\](.*?)\[\/table\]/sm", '</p><table border="0" >$1</table><p>', $text);
- $matches[1] = self::proxyUrl($matches[1], $simple_html, $uriid);
- return "[img]" . $matches[1] . "[/img]";
- },
- $text
- );
-
- $text = preg_replace("/\[img\](.*?)\[\/img\]/ism", '<img src="$1" alt="' . DI::l10n()->t('Image/photo') . '" />', $text);
- $text = preg_replace("/\[zmg\](.*?)\[\/zmg\]/ism", '<img src="$1" alt="' . DI::l10n()->t('Image/photo') . '" />', $text);
-
- $text = self::convertImages($text, $simple_html, $uriid);
-
- $text = preg_replace("/\[crypt\](.*?)\[\/crypt\]/ism", '<br><img src="' . DI::baseUrl() . '/images/lock_icon.gif" alt="' . DI::l10n()->t('Encrypted content') . '" title="' . DI::l10n()->t('Encrypted content') . '" /><br>', $text);
- $text = preg_replace("/\[crypt(.*?)\](.*?)\[\/crypt\]/ism", '<br><img src="' . DI::baseUrl() . '/images/lock_icon.gif" alt="' . DI::l10n()->t('Encrypted content') . '" title="' . '$1' . ' ' . DI::l10n()->t('Encrypted content') . '" /><br>', $text);
- //$text = preg_replace("/\[crypt=(.*?)\](.*?)\[\/crypt\]/ism", '<br><img src="' .DI::baseUrl() . '/images/lock_icon.gif" alt="' . DI::l10n()->t('Encrypted content') . '" title="' . '$1' . ' ' . DI::l10n()->t('Encrypted content') . '" /><br>', $text);
-
- // Simplify "video" element
- $text = preg_replace('(\[video[^\]]*?\ssrc\s?=\s?([^\s\]]+)[^\]]*?\].*?\[/video\])ism', '[video]$1[/video]', $text);
-
- $text = preg_replace_callback("/\[(video)\](.*?)\[\/video\]/ism", [self::class, 'sanitizeLinksCallback'], $text);
- $text = preg_replace_callback("/\[(audio)\](.*?)\[\/audio\]/ism", [self::class, 'sanitizeLinksCallback'], $text);
-
- if ($simple_html == self::NPF) {
- $text = preg_replace(
- "/\[video\](.*?)\[\/video\]/ism",
- '</p><video src="$1" controls width="100%" height="auto">$1</video><p>',
- $text
- );
- $text = preg_replace(
- "/\[audio\](.*?)\[\/audio\]/ism",
- '</p><audio src="$1" controls>$1">$1</audio><p>',
- $text
- );
- } elseif ($try_oembed) {
- // html5 video and audio
- $text = preg_replace(
- "/\[video\](.*?\.(ogg|ogv|oga|ogm|webm|mp4).*?)\[\/video\]/ism",
- '<video src="$1" controls width="100%" height="auto"><a href="$1">$1</a></video>',
- $text
- );
-
- $text = preg_replace_callback("/\[video\](.*?)\[\/video\]/ism", $try_oembed_callback, $text);
- $text = preg_replace_callback("/\[audio\](.*?)\[\/audio\]/ism", $try_oembed_callback, $text);
-
- $text = preg_replace(
- "/\[video\](.*?)\[\/video\]/ism",
- '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
- $text
- );
- $text = preg_replace("/\[audio\](.*?)\[\/audio\]/ism", '<audio src="$1" controls><a href="$1">$1</a></audio>', $text);
- } else {
- $text = preg_replace(
- "/\[video\](.*?)\[\/video\]/ism",
- '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
- $text
- );
- $text = preg_replace(
- "/\[audio\](.*?)\[\/audio\]/ism",
- '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
- $text
- );
- }
+ return $text;
+ }
- // Backward compatibility, [iframe] support has been removed in version 2020.12
- $text = preg_replace_callback("/\[(iframe)\](.*?)\[\/iframe\]/ism", [self::class, 'sanitizeLinksCallback'], $text);
- $text = preg_replace("/\[iframe\](.*?)\[\/iframe\]/ism", '<a href="$1">$1</a>', $text);
+ private static function convertListsToHtml(string $text): string
+ {
+ // handle nested lists
+ $endlessloop = 0;
+
+ while ((((strpos($text, "[/list]") !== false) && (strpos($text, "[list") !== false)) ||
+ ((strpos($text, "[/ol]") !== false) && (strpos($text, "[ol]") !== false)) ||
+ ((strpos($text, "[/ul]") !== false) && (strpos($text, "[ul]") !== false)) ||
+ ((strpos($text, "[/li]") !== false) && (strpos($text, "[li]") !== false))) && (++$endlessloop < 20)) {
+ $text = preg_replace("/\[list\](.*?)\[\/list\]/ism", '</p><ul class="listbullet" style="list-style-type: circle;">$1</ul><p>', $text);
+ $text = preg_replace("/\[list=\](.*?)\[\/list\]/ism", '</p><ul class="listnone" style="list-style-type: none;">$1</ul><p>', $text);
+ $text = preg_replace("/\[list=1\](.*?)\[\/list\]/ism", '</p><ul class="listdecimal" style="list-style-type: decimal;">$1</ul><p>', $text);
+ $text = preg_replace("/\[list=((?-i)i)\](.*?)\[\/list\]/ism", '</p><ul class="listlowerroman" style="list-style-type: lower-roman;">$2</ul><p>', $text);
+ $text = preg_replace("/\[list=((?-i)I)\](.*?)\[\/list\]/ism", '</p><ul class="listupperroman" style="list-style-type: upper-roman;">$2</ul><p>', $text);
+ $text = preg_replace("/\[list=((?-i)a)\](.*?)\[\/list\]/ism", '</p><ul class="listloweralpha" style="list-style-type: lower-alpha;">$2</ul><p>', $text);
+ $text = preg_replace("/\[list=((?-i)A)\](.*?)\[\/list\]/ism", '</p><ul class="listupperalpha" style="list-style-type: upper-alpha;">$2</ul><p>', $text);
+ $text = preg_replace("/\[ul\](.*?)\[\/ul\]/ism", '</p><ul>$1</ul><p>', $text);
+ $text = preg_replace("/\[ol\](.*?)\[\/ol\]/ism", '</p><ol>$1</ol><p>', $text);
+ $text = preg_replace("/\[li\](.*?)\[\/li\]/ism", '<li>$1</li>', $text);
+ }
- $text = self::normalizeVideoLinks($text);
+ // Check for list text
+ $text = str_replace("[*]", "<li>", $text);
+ $text = str_replace("[li]", "<li>", $text);
- // Youtube extensions
- if ($try_oembed && OEmbed::isAllowedURL('https://www.youtube.com/embed/')) {
- $text = preg_replace("/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism", '<iframe width="' . $a->getThemeInfoValue('videowidth') . '" height="' . $a->getThemeInfoValue('videoheight') . '" src="https://www.youtube.com/embed/$1" frameborder="0" ></iframe>', $text);
- } else {
- $text = preg_replace(
- "/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism",
- '<a href="https://www.youtube.com/watch?v=$1" target="_blank" rel="noopener noreferrer">https://www.youtube.com/watch?v=$1</a>',
- $text
- );
- }
+ return $text;
+ }
- // Vimeo extensions
- if ($try_oembed && OEmbed::isAllowedURL('https://player.vimeo.com/video')) {
- $text = preg_replace("/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism", '<iframe width="' . $a->getThemeInfoValue('videowidth') . '" height="' . $a->getThemeInfoValue('videoheight') . '" src="https://player.vimeo.com/video/$1" frameborder="0" ></iframe>', $text);
- } else {
- $text = preg_replace(
- "/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism",
- '<a href="https://vimeo.com/$1" target="_blank" rel="noopener noreferrer">https://vimeo.com/$1</a>',
- $text
- );
- }
+ private static function convertSpoilersToHtml(string $text): string
+ {
+ // Declare the format for [spoiler] layout
+ $SpoilerLayout = '<details class="spoiler"><summary>' . DI::l10n()->t('Click to open/close') . '</summary>$1</details>';
+
+ // Check for [spoiler] text
+ // handle nested quotes
+ $endlessloop = 0;
+ while ((strpos($text, "[/spoiler]") !== false) && (strpos($text, "[spoiler]") !== false) && (++$endlessloop < 20)) {
+ $text = preg_replace("/\[spoiler\](.*?)\[\/spoiler\]/ism", $SpoilerLayout, $text);
+ }
- // oembed tag
- $text = OEmbed::BBCode2HTML($text, $uriid);
+ // Check for [spoiler=Title] text
- // Avoid triple linefeeds through oembed
- $text = str_replace("<br style='clear:left'></span><br><br>", "<br style='clear:left'></span><br>", $text);
+ // handle nested quotes
+ $endlessloop = 0;
+ while ((strpos($text, "[/spoiler]") !== false) && (strpos($text, "[spoiler=") !== false) && (++$endlessloop < 20)) {
+ $text = preg_replace(
+ "/\[spoiler=[\"\']*(.*?)[\"\']*\](.*?)\[\/spoiler\]/ism",
+ '<details class="spoiler"><summary>$1</summary>$2</details>',
+ $text
+ );
+ }
- // If we found an event earlier, strip out all the event code and replace with a reformatted version.
- // Replace the event-start section with the entire formatted event. The other bbcode is stripped.
- // Summary (e.g. title) is required, earlier revisions only required description (in addition to
- // start which is always required). Allow desc with a missing summary for compatibility.
+ return $text;
+ }
- if ((!empty($ev['desc']) || !empty($ev['summary'])) && !empty($ev['start'])) {
- $sub = Event::getHTML($ev, $simple_html, $uriid);
+ private static function convertStructuresToHtml(string $text): string
+ {
+ $text = preg_replace("(\[p\](.*?)\[\/p\])ism", '<p>$1</p>', $text);
+ // Check for paragraph
+ return str_replace('[hr]', '</p><hr /><p>', $text);
+ }
- $text = preg_replace("/\[event\-summary\](.*?)\[\/event\-summary\]/ism", '', $text);
- $text = preg_replace("/\[event\-description\](.*?)\[\/event\-description\]/ism", '', $text);
- $text = preg_replace("/\[event\-start\](.*?)\[\/event\-start\]/ism", $sub, $text);
- $text = preg_replace("/\[event\-finish\](.*?)\[\/event\-finish\]/ism", '', $text);
- $text = preg_replace("/\[event\-location\](.*?)\[\/event\-location\]/ism", '', $text);
- $text = preg_replace("/\[event\-id\](.*?)\[\/event\-id\]/ism", '', $text);
- }
+ private static function convertSmileysToHtml(string $text, int $simple_html, bool $for_plaintext): string
+ {
+ if (strpos($text, '[nosmile]') !== false) {
+ $text = str_replace('[nosmile]', '', $text);
+ return $text;
+ }
- if (!$for_plaintext && DI::config()->get('system', 'big_emojis') && ($simple_html != self::DIASPORA) && Smilies::isEmojiPost($text)) {
- $text = '<span style="font-size: xx-large; line-height: normal;">' . $text . '</span>';
- }
+ return Smilies::replace($text, ($simple_html != self::INTERNAL) || $for_plaintext);
+ }
- $text = self::convertUrlToHtml($text, $simple_html, $for_plaintext, $try_oembed, $try_oembed_callback);
+ private static function convertQuotesToHtml(string $text): string
+ {
+ // Declare the format for [quote] layout
+ $QuoteLayout = '</p><blockquote>$1</blockquote><p>';
+
+ // Check for [quote] text
+ // handle nested quotes
+ $endlessloop = 0;
+ while ((strpos($text, "[/quote]") !== false) && (strpos($text, "[quote]") !== false) && (++$endlessloop < 20)) {
+ $text = preg_replace("/\[quote\](.*?)\[\/quote\]/ism", "$QuoteLayout", $text);
+ }
- // we may need to restrict this further if it picks up too many strays
- // link acct:user@host to a webfinger profile redirector
+ // Check for [quote=Author] text
- $text = preg_replace('/acct:([^@]+)@((?!\-)(?:[a-zA-Z\d\-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+)[a-zA-Z\d]{1,63})/', '<a href="' . DI::baseUrl() . '/acctlink?addr=$1@$2" target="extlink">acct:$1@$2</a>', $text);
+ $t_wrote = DI::l10n()->t('$1 wrote:');
- // Perform MAIL Search
- $text = preg_replace_callback("/\[(mail)\](.*?)\[\/mail\]/ism", [self::class, 'sanitizeLinksCallback'], $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);
+ // handle nested quotes
+ $endlessloop = 0;
+ while ((strpos($text, "[/quote]") !== false) && (strpos($text, "[quote=") !== false) && (++$endlessloop < 20)) {
+ $text = preg_replace(
+ "/\[quote=[\"\']*(.*?)[\"\']*\](.*?)\[\/quote\]/ism",
+ "<p><strong class=" . '"author"' . ">" . $t_wrote . "</strong></p><blockquote>$2</blockquote>",
+ $text
+ );
+ }
- /// @todo What is the meaning of these lines?
- $text = preg_replace('/\[\&\;([#a-z0-9]+)\;\]/', '&$1;', $text);
- $text = preg_replace('/\&\#039\;/', '\'', $text);
+ return $text;
+ }
- // Currently deactivated, it made problems with " inside of alt texts.
- //$text = preg_replace('/\"\;/', '"', $text);
+ private static function convertImagesToHtml(string $text, int $simple_html, int $uriid): string
+ {
+ // [img=widthxheight]image source[/img]
+ $text = preg_replace_callback(
+ "/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism",
+ function ($matches) use ($simple_html, $uriid) {
+ if (strpos($matches[3], "data:image/") === 0) {
+ return $matches[0];
+ }
- // fix any escaped ampersands that may have been converted into links
- $text = preg_replace('/\<([^>]*?)(src|href)=(.*?)\&\;(.*?)\>/ism', '<$1$2=$3&$4>', $text);
+ $matches[3] = self::proxyUrl($matches[3], $simple_html, $uriid);
+ return "[img=" . $matches[1] . "x" . $matches[2] . "]" . $matches[3] . "[/img]";
+ },
+ $text
+ );
- // sanitizes src attributes (http and redir URLs for displaying in a web page, cid used for inline images in emails)
- $allowed_src_protocols = ['//', 'http://', 'https://', 'contact/redir/', 'cid:'];
+ $text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '<img src="$3" style="width: $1px;" >', $text);
+ $text = preg_replace("/\[zmg\=([0-9]*)x([0-9]*)\](.*?)\[\/zmg\]/ism", '<img class="zrl" src="$3" style="width: $1px;" >', $text);
- array_walk($allowed_src_protocols, function (&$value) {
- $value = preg_quote($value, '#');
- });
+ $text = preg_replace_callback(
+ "/\[[iz]mg\=(.*?)\](.*?)\[\/[iz]mg\]/ism",
+ function ($matches) use ($simple_html, $uriid) {
+ $matches[1] = self::proxyUrl($matches[1], $simple_html, $uriid);
+ $alt = htmlspecialchars($matches[2], ENT_COMPAT);
+ // Fix for Markdown problems with Diaspora, see issue #12701
+ if (($simple_html != self::DIASPORA) || strpos($matches[2], '"') === false) {
+ return '<img src="' . $matches[1] . '" alt="' . $alt . '" title="' . $alt . '">';
+ } else {
+ return '<img src="' . $matches[1] . '" alt="' . $alt . '">';
+ }
+ },
+ $text
+ );
- $text = preg_replace(
- '#<([^>]*?)(src)="(?!' . implode('|', $allowed_src_protocols) . ')(.*?)"(.*?)>#ism',
- '<$1$2=""$4 data-original-src="$3" class="invalid-src" title="' . DI::l10n()->t('Invalid source protocol') . '">',
- $text
- );
+ // Images
+ // [img]pathtoimage[/img]
+ $text = preg_replace_callback(
+ "/\[[iz]mg\](.*?)\[\/[iz]mg\]/ism",
+ function ($matches) use ($simple_html, $uriid) {
+ if (strpos($matches[1], "data:image/") === 0) {
+ return $matches[0];
+ }
- // sanitize href attributes (only allowlisted protocols URLs)
- // default value for backward compatibility
- $allowed_link_protocols = DI::config()->get('system', 'allowed_link_protocols', []);
+ $matches[1] = self::proxyUrl($matches[1], $simple_html, $uriid);
+ return "[img]" . $matches[1] . "[/img]";
+ },
+ $text
+ );
- // Always allowed protocol even if config isn't set or not including it
- $allowed_link_protocols[] = '//';
- $allowed_link_protocols[] = 'http://';
- $allowed_link_protocols[] = 'https://';
- $allowed_link_protocols[] = 'contact/redir/';
+ $text = preg_replace("/\[img\](.*?)\[\/img\]/ism", '<img src="$1" alt="' . DI::l10n()->t('Image/photo') . '" />', $text);
+ $text = preg_replace("/\[zmg\](.*?)\[\/zmg\]/ism", '<img src="$1" alt="' . DI::l10n()->t('Image/photo') . '" />', $text);
- array_walk($allowed_link_protocols, function (&$value) {
- $value = preg_quote($value, '#');
- });
+ $text = self::convertImages($text, $simple_html, $uriid);
- $regex = '#<([^>]*?)(href)="(?!' . implode('|', $allowed_link_protocols) . ')(.*?)"(.*?)>#ism';
- $text = preg_replace($regex, '<$1$2="javascript:void(0)"$4 data-original-href="$3" class="invalid-href" title="' . DI::l10n()->t('Invalid link protocol') . '">', $text);
+ return $text;
+ }
- // Shared content
- $text = self::convertShare(
- $text,
- function (array $attributes, array $author_contact, $content, $is_quote_share) use ($simple_html) {
- return self::convertShareCallback($attributes, $author_contact, $content, $is_quote_share, $simple_html);
- },
- $uriid
- );
+ private static function convertCryptToHtml(string $text): string
+ {
+ $text = preg_replace("/\[crypt\](.*?)\[\/crypt\]/ism", '<br><img src="' . DI::baseUrl() . '/images/lock_icon.gif" alt="' . DI::l10n()->t('Encrypted content') . '" title="' . DI::l10n()->t('Encrypted content') . '" /><br>', $text);
+ $text = preg_replace("/\[crypt(.*?)\](.*?)\[\/crypt\]/ism", '<br><img src="' . DI::baseUrl() . '/images/lock_icon.gif" alt="' . DI::l10n()->t('Encrypted content') . '" title="' . '$1' . ' ' . DI::l10n()->t('Encrypted content') . '" /><br>', $text);
+ return $text;
+ }
- $text = self::interpolateSavedImagesIntoItemBody($uriid, $text, $saved_image);
+ private static function convertAudioVideoToHtml(string $text, int $simple_html, bool $try_oembed, \Closure $try_oembed_callback): string
+ {
+ // Simplify "video" element
+ $text = preg_replace('(\[video[^\]]*?\ssrc\s?=\s?([^\s\]]+)[^\]]*?\].*?\[/video\])ism', '[video]$1[/video]', $text);
- return $text;
- }); // Escaped noparse, nobb, pre
+ $text = preg_replace_callback("/\[(video)\](.*?)\[\/video\]/ism", [self::class, 'sanitizeLinksCallback'], $text);
+ $text = preg_replace_callback("/\[(audio)\](.*?)\[\/audio\]/ism", [self::class, 'sanitizeLinksCallback'], $text);
- // Remove escaping tags and replace new lines that remain
- $text = preg_replace_callback('/\[(noparse|nobb)](.*?)\[\/\1]/ism', function ($match) {
- return str_replace("\n", "<br>", $match[2]);
- }, $text);
+ if ($simple_html == self::NPF) {
+ $text = preg_replace(
+ "/\[video\](.*?)\[\/video\]/ism",
+ '</p><video src="$1" controls width="100%" height="auto">$1</video><p>',
+ $text
+ );
+ $text = preg_replace(
+ "/\[audio\](.*?)\[\/audio\]/ism",
+ '</p><audio src="$1" controls>$1">$1</audio><p>',
+ $text
+ );
+ } elseif ($try_oembed) {
+ // html5 video and audio
+ $text = preg_replace(
+ "/\[video\](.*?\.(ogg|ogv|oga|ogm|webm|mp4).*?)\[\/video\]/ism",
+ '<video src="$1" controls width="100%" height="auto"><a href="$1">$1</a></video>',
+ $text
+ );
- // Additionally, [pre] tags preserve spaces
- $text = preg_replace_callback("/\[pre\](.*?)\[\/pre\]/ism", function ($match) {
- return str_replace([' ', "\n"], [' ', "<br>"], htmlentities($match[1], ENT_NOQUOTES, 'UTF-8'));
- }, $text);
+ $text = preg_replace_callback("/\[video\](.*?)\[\/video\]/ism", $try_oembed_callback, $text);
+ $text = preg_replace_callback("/\[audio\](.*?)\[\/audio\]/ism", $try_oembed_callback, $text);
- return $text;
- }); // Escaped code
+ $text = preg_replace("/\[video\](.*?)\[\/video\]/ism", '[url]$1[/url]', $text);
+ $text = preg_replace("/\[audio\](.*?)\[\/audio\]/ism", '<audio src="$1" controls><a href="$1">$1</a></audio>', $text);
+ } else {
+ $text = preg_replace("/\[video\](.*?)\[\/video\]/ism", '[url]$1[/url]', $text);
+ $text = preg_replace("/\[audio\](.*?)\[\/audio\]/ism", '[url]$1[/url]', $text);
+ }
+ return $text;
+ }
- $text = preg_replace_callback(
- "#\[code(?:=([^\]]*))?\](.*?)\[\/code\]#ism",
- function ($matches) {
- if (strpos($matches[2], "\n") !== false) {
- $return = '<pre><code class="language-' . trim($matches[1]) . '">' . htmlentities(trim($matches[2], "\n\r"), ENT_NOQUOTES, 'UTF-8') . '</code></pre>';
- } else {
- $return = '<code>' . htmlentities($matches[2], ENT_NOQUOTES, 'UTF-8') . '</code>';
- }
+ private static function convertIFramesToHtml(string $text): string
+ {
+ // Backward compatibility, [iframe] support has been removed in version 2020.12
+ $text = preg_replace_callback("/\[(iframe)\](.*?)\[\/iframe\]/ism", [self::class, 'sanitizeLinksCallback'], $text);
+ $text = preg_replace("/\[iframe\](.*?)\[\/iframe\]/ism", '[url]$1[/url]', $text);
- return $return;
- },
- $text
- );
+ return $text;
+ }
- // Default iframe allowed domains/path
- $allowedIframeDomains = DI::config()->get('system', 'no_oembed_rich_content') ? [] : ['www.youtube.com/embed/', 'player.vimeo.com/video/'];
+ private static function convertVideoPlatformsToHtml(string $text, bool $try_oembed): string
+ {
+ $a = DI::app();
+ $text = self::normalizeVideoLinks($text);
- $allowedIframeDomains = array_merge(
- $allowedIframeDomains,
- DI::config()->get('system', 'allowed_oembed') ?
- explode(',', DI::config()->get('system', 'allowed_oembed'))
- : []
- );
+ // Youtube extensions
+ if ($try_oembed && OEmbed::isAllowedURL('https://www.youtube.com/embed/')) {
+ $text = preg_replace("/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism", '<iframe width="' . $a->getThemeInfoValue('videowidth') . '" height="' . $a->getThemeInfoValue('videoheight') . '" src="https://www.youtube.com/embed/$1" frameborder="0" ></iframe>', $text);
+ } else {
+ $text = preg_replace("/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism", '[url]https://www.youtube.com/watch?v=$1[/url]', $text);
+ }
- if (strpos($text, '<p>') !== false || strpos($text, '</p>') !== false) {
- $text = '<p>' . $text . '</p>';
+ // Vimeo extensions
+ if ($try_oembed && OEmbed::isAllowedURL('https://player.vimeo.com/video')) {
+ $text = preg_replace("/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism", '<iframe width="' . $a->getThemeInfoValue('videowidth') . '" height="' . $a->getThemeInfoValue('videoheight') . '" src="https://player.vimeo.com/video/$1" frameborder="0" ></iframe>', $text);
+ } else {
+ $text = preg_replace("/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism", '[url]https://vimeo.com/$1[/url]', $text);
}
+ return $text;
+ }
- $text = HTML::purify($text, $allowedIframeDomains);
- DI::profiler()->stopRecording();
+ private static function convertOEmbedToHtml(string $text, int $uriid): string
+ {
+ // oembed tag
+ $text = OEmbed::BBCode2HTML($text, $uriid);
- return trim($text);
+ // Avoid triple linefeeds through oembed
+ $text = str_replace("<br style='clear:left'></span><br><br>", "<br style='clear:left'></span><br>", $text);
+
+ return $text;
}
private static function convertUrlToHtml(string $text, int $simple_html, bool $for_plaintext, bool $try_oembed, \Closure $try_oembed_callback): string
$text = preg_replace("/\[url\=(" . preg_quote(DI::baseUrl(), '/') . ".*?)\](.*?)\[\/url\]/ism", '<a href="$1">$2</a>', $text);
$text = preg_replace("/\[url\=(.*?)\](.*?)\[\/url\]/ism", '<a href="$1" target="_blank" rel="noopener noreferrer">$2</a>', $text);
- return $text;
+
+ // we may need to restrict this further if it picks up too many strays
+ // link acct:user@host to a webfinger profile redirector
+ return preg_replace('/acct:([^@]+)@((?!\-)(?:[a-zA-Z\d\-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+)[a-zA-Z\d]{1,63})/', '<a href="' . DI::baseUrl() . '/acctlink?addr=$1@$2" target="extlink">acct:$1@$2</a>', $text);
}
private static function escapeUrl(string $url): string
return $text;
}
+ private static function convertMailToHtml(string $text): string
+ {
+ $text = preg_replace_callback("/\[(mail)\](.*?)\[\/mail\]/ism", [self::class, 'sanitizeLinksCallback'], $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);
+ return $text;
+ }
+
+ private static function convertSharesToHtml(string $text, int $simple_html, bool $try_oembed, int $uriid): string
+ {
+ // Shared content
+ // when the content is meant exporting to other systems then remove the avatar picture since this doesn't really look good on these systems
+ if (!$try_oembed) {
+ $text = preg_replace("/\[share(.*?)avatar\s?=\s?'.*?'\s?(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "\n[share$1$2]$3[/share]", $text);
+ }
+
+ $text = self::convertShare(
+ $text,
+ function (array $attributes, array $author_contact, $content, $is_quote_share) use ($simple_html) {
+ return self::convertShareCallback($attributes, $author_contact, $content, $is_quote_share, $simple_html);
+ },
+ $uriid
+ );
+
+ return $text;
+ }
+
+ private static function cleanupHtml(string $text): string
+ {
+ /// @todo What is the meaning of these lines?
+ $text = preg_replace('/\[\&\;([#a-z0-9]+)\;\]/', '&$1;', $text);
+ $text = preg_replace('/\&\#039\;/', '\'', $text);
+
+ // Currently deactivated, it made problems with " inside of alt texts.
+ //$text = preg_replace('/\"\;/', '"', $text);
+
+ // fix any escaped ampersands that may have been converted into links
+ $text = preg_replace('/\<([^>]*?)(src|href)=(.*?)\&\;(.*?)\>/ism', '<$1$2=$3&$4>', $text);
+
+ // sanitizes src attributes (http and redir URLs for displaying in a web page, cid used for inline images in emails)
+ $allowed_src_protocols = ['//', 'http://', 'https://', 'contact/redir/', 'cid:'];
+
+ array_walk($allowed_src_protocols, function (&$value) {
+ $value = preg_quote($value, '#');
+ });
+
+ $text = preg_replace(
+ '#<([^>]*?)(src)="(?!' . implode('|', $allowed_src_protocols) . ')(.*?)"(.*?)>#ism',
+ '<$1$2=""$4 data-original-src="$3" class="invalid-src" title="' . DI::l10n()->t('Invalid source protocol') . '">',
+ $text
+ );
+
+ // sanitize href attributes (only allowlisted protocols URLs)
+ // default value for backward compatibility
+ $allowed_link_protocols = DI::config()->get('system', 'allowed_link_protocols', []);
+
+ // Always allowed protocol even if config isn't set or not including it
+ $allowed_link_protocols[] = '//';
+ $allowed_link_protocols[] = 'http://';
+ $allowed_link_protocols[] = 'https://';
+ $allowed_link_protocols[] = 'contact/redir/';
+
+ array_walk($allowed_link_protocols, function (&$value) {
+ $value = preg_quote($value, '#');
+ });
+
+ $regex = '#<([^>]*?)(href)="(?!' . implode('|', $allowed_link_protocols) . ')(.*?)"(.*?)>#ism';
+ $text = preg_replace($regex, '<$1$2="javascript:void(0)"$4 data-original-href="$3" class="invalid-href" title="' . DI::l10n()->t('Invalid link protocol') . '">', $text);
+
+ return $text;
+ }
+
/**
* Strips the "abstract" tag from the provided text
*