X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FContent%2FText%2FBBCode.php;h=5a2b966dd57c2ce6501522409e111e84d9c263ed;hb=7fb9e1c7c8f5f4887e44aadf6115b68c6cc66d03;hp=6da5b490586c8c23854ffa07b26cecca11d4e62d;hpb=28c9bc871c12d25dd4112acd822e5ed92b1d5fcd;p=friendica.git diff --git a/src/Content/Text/BBCode.php b/src/Content/Text/BBCode.php index 6da5b49058..5a2b966dd5 100644 --- a/src/Content/Text/BBCode.php +++ b/src/Content/Text/BBCode.php @@ -37,7 +37,9 @@ use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\Event; use Friendica\Model\Photo; +use Friendica\Model\Post; use Friendica\Model\Tag; +use Friendica\Network\HTTPClientOptions; use Friendica\Object\Image; use Friendica\Protocol\Activity; use Friendica\Util\Images; @@ -50,9 +52,10 @@ use Friendica\Util\XML; class BBCode { // Update this value to the current date whenever changes are made to BBCode::convert - const VERSION = '2021-04-24'; + const VERSION = '2021-07-28'; const INTERNAL = 0; + const EXTERNAL = 1; const API = 2; const DIASPORA = 3; const CONNECTORS = 4; @@ -61,7 +64,8 @@ class BBCode const BACKLINK = 8; const ACTIVITYPUB = 9; - const ANCHOR = '
'; + const TOP_ANCHOR = '
'; + const BOTTOM_ANCHOR = '
'; /** * Fetches attachment data that were generated the old way * @@ -149,6 +153,7 @@ class BBCode */ public static function getAttachmentData($body) { + DI::profiler()->startRecording('rendering'); $data = [ 'type' => '', 'text' => '', @@ -164,6 +169,7 @@ class BBCode ]; if (!preg_match("/(.*)\[attachment(.*?)\](.*?)\[\/attachment\](.*)/ism", $body, $match)) { + DI::profiler()->stopRecording(); return self::getOldAttachmentData($body); } @@ -208,6 +214,7 @@ class BBCode } if (!in_array($data['type'], ['link', 'audio', 'photo', 'video'])) { + DI::profiler()->stopRecording(); return []; } @@ -229,6 +236,7 @@ class BBCode } } + DI::profiler()->stopRecording(); return $data; } @@ -244,12 +252,13 @@ class BBCode - (thumbnail) */ + DI::profiler()->startRecording('rendering'); $has_title = !empty($item['title']); $plink = $item['plink'] ?? ''; $post = self::getAttachmentData($body); // Get all linked images with alternative image description - if (preg_match_all("/\[img=([^\[\]]*)\]([^\[\]]*)\[\/img\]/Usi", $body, $pictures, PREG_SET_ORDER)) { + if (preg_match_all("/\[img=(http[^\[\]]*)\]([^\[\]]*)\[\/img\]/Usi", $body, $pictures, PREG_SET_ORDER)) { foreach ($pictures as $picture) { if (Photo::isLocal($picture[1])) { $post['images'][] = ['url' => str_replace('-1.', '-0.', $picture[1]), 'description' => $picture[2]]; @@ -395,6 +404,7 @@ class BBCode } } + DI::profiler()->stopRecording(); return $post; } @@ -408,15 +418,15 @@ class BBCode */ public static function removeAttachment($body, $no_link_desc = false) { - return preg_replace_callback("/\s*\[attachment (.*)\](.*?)\[\/attachment\]\s*/ism", + return preg_replace_callback("/\s*\[attachment (.*?)\](.*?)\[\/attachment\]\s*/ism", function ($match) use ($no_link_desc) { $attach_data = self::getAttachmentData($match[0]); if (empty($attach_data['url'])) { return $match[0]; } elseif (empty($attach_data['title']) || $no_link_desc) { - return "\n[url]" . $attach_data['url'] . "[/url]\n"; + return " \n[url]" . $attach_data['url'] . "[/url]\n"; } else { - return "\n[url=" . $attach_data['url'] . ']' . $attach_data['title'] . "[/url]\n"; + return " \n[url=" . $attach_data['url'] . ']' . $attach_data['title'] . "[/url]\n"; } }, $body); } @@ -431,18 +441,29 @@ class BBCode */ public static function toPlaintext($text, $keep_urls = true) { + DI::profiler()->startRecording('rendering'); + // Remove pictures in advance to avoid unneeded proxy calls + $text = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", ' $2 ', $text); + $text = preg_replace("/\[img.*?\[\/img\]/ism", ' ', $text); + + // Remove attachment + $text = self::removeAttachment($text); + $naked_text = HTML::toPlaintext(self::convert($text, false, 0, true), 0, !$keep_urls); + DI::profiler()->stopRecording(); return $naked_text; } - private static function proxyUrl($image, $simplehtml = self::INTERNAL) + private static function proxyUrl($image, $simplehtml = self::INTERNAL, $uriid = 0, $size = '') { // Only send proxied pictures to API and for internal display - if (in_array($simplehtml, [self::INTERNAL, self::API])) { - return ProxyUtils::proxifyUrl($image); - } else { + if (!in_array($simplehtml, [self::INTERNAL, self::API])) { return $image; + } elseif ($uriid > 0) { + return Post\Link::getByLink($uriid, $image, $size); + } else { + return ProxyUtils::proxifyUrl($image, $size); } } @@ -456,6 +477,7 @@ class BBCode */ public static function scaleExternalImages(string $srctext) { + DI::profiler()->startRecording('rendering'); $s = $srctext; // Simplify image links @@ -472,7 +494,7 @@ class BBCode continue; } - $curlResult = DI::httpRequest()->get($mtch[1]); + $curlResult = DI::httpClient()->get($mtch[1]); if (!$curlResult->isSuccess()) { continue; } @@ -505,6 +527,7 @@ class BBCode } } + DI::profiler()->stopRecording(); return $s; } @@ -520,6 +543,7 @@ class BBCode */ public static function limitBodySize($body) { + DI::profiler()->startRecording('rendering'); $maxlen = DI::config()->get('config', 'max_import_size', 0); // If the length of the body, including the embedded images, is smaller @@ -591,8 +615,10 @@ class BBCode $new_body = $new_body . $orig_body; } + DI::profiler()->stopRecording(); return $new_body; } else { + DI::profiler()->stopRecording(); return $body; } } @@ -606,18 +632,19 @@ class BBCode * @param integer $simplehtml * @param bool $tryoembed * @param array $data + * @param int $uriid * @return string * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function convertAttachment($text, $simplehtml = self::INTERNAL, $tryoembed = true, array $data = []) + public static function convertAttachment($text, $simplehtml = self::INTERNAL, $tryoembed = true, array $data = [], $uriid = 0) { + DI::profiler()->startRecording('rendering'); $data = $data ?: self::getAttachmentData($text); if (empty($data) || empty($data['url'])) { + DI::profiler()->stopRecording(); return $text; } - $stamp1 = microtime(true); - if (isset($data['title'])) { $data['title'] = strip_tags($data['title']); $data['title'] = str_replace(['http://', 'https://'], '', $data['title']); @@ -646,12 +673,12 @@ class BBCode if (!empty($data['title']) && !empty($data['url'])) { if (!empty($data['image']) && empty($data['text']) && ($data['type'] == 'photo')) { - $return .= sprintf('', $data['url'], self::proxyUrl($data['image'], $simplehtml), $data['title']); + $return .= sprintf('', $data['url'], self::proxyUrl($data['image'], $simplehtml, $uriid), $data['title']); } else { if (!empty($data['image'])) { - $return .= sprintf('
', $data['url'], self::proxyUrl($data['image'], $simplehtml), $data['title']); + $return .= sprintf('
', $data['url'], self::proxyUrl($data['image'], $simplehtml, $uriid), $data['title']); } elseif (!empty($data['preview'])) { - $return .= sprintf('
', $data['url'], self::proxyUrl($data['preview'], $simplehtml), $data['title']); + $return .= sprintf('
', $data['url'], self::proxyUrl($data['preview'], $simplehtml, $uriid), $data['title']); } $return .= sprintf('

%s

', $data['url'], $data['title']); } @@ -675,17 +702,20 @@ class BBCode } } - DI::profiler()->saveTimestamp($stamp1, 'rendering'); + DI::profiler()->stopRecording(); return trim(($data['text'] ?? '') . ' ' . $return . ' ' . ($data['after'] ?? '')); } public static function removeShareInformation($Text, $plaintext = false, $nolink = false) { + DI::profiler()->startRecording('rendering'); $data = self::getAttachmentData($Text); if (!$data) { + DI::profiler()->stopRecording(); return $Text; } elseif ($nolink) { + DI::profiler()->stopRecording(); return $data['text'] . ($data['after'] ?? ''); } @@ -699,11 +729,13 @@ class BBCode } if (empty($data['text']) && !empty($data['title']) && empty($data['url'])) { + DI::profiler()->stopRecording(); return $data['title'] . $data['after']; } // If the link already is included in the post, don't add it again if (!empty($data['url']) && strpos($data['text'], $data['url'])) { + DI::profiler()->stopRecording(); return $data['text'] . $data['after']; } @@ -715,6 +747,7 @@ class BBCode $text .= "\n[url]" . $data['url'] . '[/url]'; } + DI::profiler()->stopRecording(); return $text . "\n" . $data['after']; } @@ -808,6 +841,7 @@ class BBCode */ public static function getTagPosition($text, $name, $occurrences = 0) { + DI::profiler()->startRecording('rendering'); if ($occurrences < 0) { $occurrences = 0; } @@ -820,6 +854,7 @@ class BBCode } if ($start_open === false) { + DI::profiler()->stopRecording(); return false; } @@ -827,6 +862,7 @@ class BBCode $start_close = strpos($text, ']', $start_open); if ($start_close === false) { + DI::profiler()->stopRecording(); return false; } @@ -835,6 +871,7 @@ class BBCode $end_open = strpos($text, '[/' . $name . ']', $start_close); if ($end_open === false) { + DI::profiler()->stopRecording(); return false; } @@ -853,6 +890,7 @@ class BBCode $res['start']['equal'] = $start_equal + 1; } + DI::profiler()->stopRecording(); return $res; } @@ -867,6 +905,7 @@ class BBCode */ public static function pregReplaceInTag($pattern, $replace, $name, $text) { + DI::profiler()->startRecording('rendering'); $occurrences = 0; $pos = self::getTagPosition($text, $name, $occurrences); while ($pos !== false && $occurrences++ < 1000) { @@ -883,6 +922,7 @@ class BBCode $pos = self::getTagPosition($text, $name, $occurrences); } + DI::profiler()->stopRecording(); return $text; } @@ -927,7 +967,7 @@ class BBCode return ['body' => $new_body, 'images' => $saved_image]; } - private static function interpolateSavedImagesIntoItemBody($body, array $images) + private static function interpolateSavedImagesIntoItemBody($uriid, $body, array $images) { $newbody = $body; @@ -937,7 +977,7 @@ class BBCode // it loops over the array starting from the first element and going sequentially // to the last element $newbody = str_replace('[$#saved_image' . $cnt . '#$]', - '' . DI::l10n()->t('Image/photo') . '', $newbody); + '' . DI::l10n()->t('Image/photo') . '', $newbody); $cnt++; } @@ -951,8 +991,14 @@ class BBCode */ public static function fetchShareAttributes($text) { + DI::profiler()->startRecording('rendering'); + // See Issue https://github.com/friendica/friendica/issues/10454 + // Hashtags in usernames are expanded to links. This here is a quick fix. + $text = preg_replace('/([@!#])\[url\=.*?\](.*?)\[\/url\]/ism', '$1$2', $text); + $attributes = []; if (!preg_match("/(.*?)\[share(.*?)\](.*)\[\/share\]/ism", $text, $matches)) { + DI::profiler()->stopRecording(); return $attributes; } @@ -961,6 +1007,7 @@ class BBCode preg_match("/$field=(['\"])(.+?)\\1/ism", $attribute_string, $matches); $attributes[$field] = html_entity_decode($matches[2] ?? '', ENT_QUOTES, 'UTF-8'); } + DI::profiler()->stopRecording(); return $attributes; } @@ -983,11 +1030,12 @@ class BBCode * @param callable $callback * @return string The BBCode string with all [share] blocks replaced */ - public static function convertShare($text, callable $callback) + public static function convertShare($text, callable $callback, int $uriid = 0) { + DI::profiler()->startRecording('rendering'); $return = preg_replace_callback( "/(.*?)\[share(.*?)\](.*)\[\/share\]/ism", - function ($match) use ($callback) { + function ($match) use ($callback, $uriid) { $attribute_string = $match[2]; $attributes = []; foreach (['author', 'profile', 'avatar', 'link', 'posted', 'guid'] as $field) { @@ -995,7 +1043,7 @@ class BBCode $attributes[$field] = html_entity_decode($matches[2] ?? '', ENT_QUOTES, 'UTF-8'); } - $author_contact = Contact::getByURL($attributes['profile'], false, ['url', 'addr', 'name', 'micro']); + $author_contact = Contact::getByURL($attributes['profile'], false, ['id', 'url', 'addr', 'name', 'micro']); $author_contact['url'] = ($author_contact['url'] ?? $attributes['profile']); $author_contact['addr'] = ($author_contact['addr'] ?? '') ?: Protocol::getAddrFromProfileUrl($attributes['profile']); @@ -1003,8 +1051,10 @@ class BBCode $attributes['avatar'] = ($author_contact['micro'] ?? '') ?: $attributes['avatar']; $attributes['profile'] = ($author_contact['url'] ?? '') ?: $attributes['profile']; - if ($attributes['avatar']) { - $attributes['avatar'] = ProxyUtils::proxifyUrl($attributes['avatar'], false, ProxyUtils::SIZE_THUMB); + if (!empty($author_contact['id'])) { + $attributes['avatar'] = Contact::getAvatarUrlForId($author_contact['id'], ProxyUtils::SIZE_THUMB); + } elseif ($attributes['avatar']) { + $attributes['avatar'] = self::proxyUrl($attributes['avatar'], self::INTERNAL, $uriid, ProxyUtils::SIZE_THUMB); } $content = preg_replace(Strings::autoLinkRegEx(), '$1', $match[3]); @@ -1014,6 +1064,44 @@ class BBCode $text ); + DI::profiler()->stopRecording(); + return $return; + } + + /** + * Convert complex IMG and ZMG elements + * + * @param [type] $text + * @param integer $simplehtml + * @param integer $uriid + * @return string + */ + private static function convertImages(string $text, int $simplehtml, int $uriid = 0):string + { + DI::profiler()->startRecording('rendering'); + $return = preg_replace_callback( + "/\[[zi]mg(.*?)\]([^\[\]]*)\[\/[zi]mg\]/ism", + function ($match) use ($simplehtml, $uriid) { + $attribute_string = $match[1]; + $attributes = []; + foreach (['alt', 'width', 'height'] as $field) { + preg_match("/$field=(['\"])(.+?)\\1/ism", $attribute_string, $matches); + $attributes[$field] = html_entity_decode($matches[2] ?? '', ENT_QUOTES, 'UTF-8'); + } + + $img_str = ' $value) { + if (!empty($value)) { + $img_str .= ' ' . $key . '="' . htmlspecialchars($value, ENT_COMPAT) . '"'; + } + } + return $img_str . '>'; + }, + $text + ); + + DI::profiler()->stopRecording(); return $return; } @@ -1033,11 +1121,14 @@ class BBCode */ private static function convertShareCallback(array $attributes, array $author_contact, $content, $is_quote_share, $simplehtml) { + DI::profiler()->startRecording('rendering'); $mention = Protocol::formatMention($attributes['profile'], $attributes['author']); switch ($simplehtml) { case self::API: - $text = ($is_quote_share? '
' : '') . '

' . html_entity_decode('♲ ', ENT_QUOTES, 'UTF-8') . ' ' . $author_contact['addr'] . ':

' . "\n" . $content; + $text = ($is_quote_share? '
' : '') . + '' . html_entity_decode('♲ ', ENT_QUOTES, 'UTF-8') . ' ' . $author_contact['addr'] . ":
\n" . + '
' . $content . '
'; break; case self::DIASPORA: if (stripos(Strings::normaliseLink($attributes['link']), 'http://twitter.com/') === 0) { @@ -1084,12 +1175,12 @@ class BBCode '$avatar' => $attributes['avatar'], '$author' => $attributes['author'], '$link' => $attributes['link'], - '$link_title' => DI::l10n()->t('link to source'), + '$link_title' => DI::l10n()->t('Link to source'), '$posted' => $attributes['posted'], '$guid' => $attributes['guid'], '$network_name' => ContactSelector::networkToName($network, $attributes['profile']), '$network_icon' => ContactSelector::networkToIcon($network, $attributes['profile']), - '$content' => self::setMentions(trim($content), 0, $network) . self::ANCHOR, + '$content' => self::TOP_ANCHOR . self::setMentions(trim($content), 0, $network) . self::BOTTOM_ANCHOR, ]); break; } @@ -1103,26 +1194,20 @@ class BBCode $text = DI::cache()->get($cache_key); if (is_null($text)) { - $a = DI::app(); - - $stamp1 = microtime(true); - - $ch = @curl_init($match[1]); - @curl_setopt($ch, CURLOPT_NOBODY, true); - @curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - @curl_setopt($ch, CURLOPT_USERAGENT, DI::httpRequest()->getUserAgent()); - @curl_exec($ch); - $curl_info = @curl_getinfo($ch); - - DI::profiler()->saveTimestamp($stamp1, "network"); + $curlResult = DI::httpClient()->head($match[1], [HTTPClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout')]); + if ($curlResult->isSuccess()) { + $mimetype = $curlResult->getHeader('Content-Type')[0] ?? ''; + } else { + $mimetype = ''; + } - if (substr($curl_info['content_type'], 0, 6) == 'image/') { + if (substr($mimetype, 0, 6) == 'image/') { $text = "[url=" . $match[1] . ']' . $match[1] . "[/url]"; } else { $text = "[url=" . $match[2] . ']' . $match[2] . "[/url]"; // if its not a picture then look if its a page that contains a picture link - $body = DI::httpRequest()->fetch($match[1]); + $body = DI::httpClient()->fetch($match[1]); if (empty($body)) { DI::cache()->set($cache_key, $text); return $text; @@ -1180,20 +1265,15 @@ class BBCode return $text; } - // Only fetch the header, not the content - $stamp1 = microtime(true); - - $ch = @curl_init($match[1]); - @curl_setopt($ch, CURLOPT_NOBODY, true); - @curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - @curl_setopt($ch, CURLOPT_USERAGENT, DI::httpRequest()->getUserAgent()); - @curl_exec($ch); - $curl_info = @curl_getinfo($ch); - - DI::profiler()->saveTimestamp($stamp1, "network"); + $curlResult = DI::httpClient()->head($match[1], [HTTPClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout')]); + if ($curlResult->isSuccess()) { + $mimetype = $curlResult->getHeader('Content-Type')[0] ?? ''; + } else { + $mimetype = ''; + } // if its a link to a picture then embed this picture - if (substr($curl_info['content_type'], 0, 6) == 'image/') { + if (substr($mimetype, 0, 6) == 'image/') { $text = '[img]' . $match[1] . '[/img]'; } else { if (!empty($match[3])) { @@ -1203,7 +1283,7 @@ class BBCode } // if its not a picture then look if its a page that contains a picture link - $body = DI::httpRequest()->fetch($match[1]); + $body = DI::httpClient()->fetch($match[1]); if (empty($body)) { DI::cache()->set($cache_key, $text); return $text; @@ -1237,22 +1317,93 @@ class BBCode public static function cleanPictureLinks($text) { + DI::profiler()->startRecording('rendering'); $return = preg_replace_callback("&\[url=([^\[\]]*)\]\[img=(.*)\](.*)\[\/img\]\[\/url\]&Usi", 'self::cleanPictureLinksCallback', $text); $return = preg_replace_callback("&\[url=([^\[\]]*)\]\[img\](.*)\[\/img\]\[\/url\]&Usi", 'self::cleanPictureLinksCallback', $return); + DI::profiler()->stopRecording(); return $return; } public static function removeLinks(string $bbcode) { + DI::profiler()->startRecording('rendering'); $bbcode = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", ' $1 ', $bbcode); $bbcode = preg_replace("/\[img.*?\[\/img\]/ism", ' ', $bbcode); $bbcode = preg_replace('/[@!#]\[url\=.*?\].*?\[\/url\]/ism', '', $bbcode); $bbcode = preg_replace("/\[url=[^\[\]]*\](.*)\[\/url\]/Usi", ' $1 ', $bbcode); $bbcode = preg_replace('/[@!#]?\[url.*?\[\/url\]/ism', '', $bbcode); + DI::profiler()->stopRecording(); return $bbcode; } + /** + * Replace names in mentions with nicknames + * + * @param string $body + * @return string Body with replaced mentions + */ + public static function setMentionsToNicknames(string $body):string + { + DI::profiler()->startRecording('rendering'); + $regexp = "/([@!])\[url\=([^\[\]]*)\].*?\[\/url\]/ism"; + $body = preg_replace_callback($regexp, ['self', 'mentionCallback'], $body); + DI::profiler()->stopRecording(); + return $body; + } + + /** + * Callback function to replace a Friendica style mention in a mention with the nickname + * + * @param array $match Matching values for the callback + * @return string Replaced mention + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + private static function mentionCallback($match) + { + if (empty($match[2])) { + return ''; + } + + $data = Contact::getByURL($match[2], false, ['url', 'nick']); + if (empty($data['nick'])) { + return $match[0]; + } + + return $match[1] . '[url=' . $data['url'] . ']' . $data['nick'] . '[/url]'; + } + + /** + * Converts a BBCode message for a given URI-ID to a HTML message + * + * BBcode 2 HTML was written by WAY2WEB.net + * extended to work with Mistpark/Friendica - Mike Macgirvin + * + * Simple HTML values meaning: + * - 0: Friendica display + * - 1: Unused + * - 2: Used for Windows Phone push, Friendica API + * - 3: Used before converting to Markdown in bb2diaspora.php + * - 4: Used for WordPress, Libertree (before Markdown), pump.io and tumblr + * - 5: Unused + * - 6: Unused + * - 7: Used for dfrn, OStatus + * - 8: Used for Twitter, WP backlink text setting + * - 9: ActivityPub + * + * @param int $uriid + * @param string $text + * @param int $simple_html + * @return string + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public static function convertForUriId(int $uriid = null, string $text = null, int $simple_html = self::INTERNAL) + { + $try_oembed = ($simple_html == self::INTERNAL); + + return self::convert($text ?? '', $try_oembed, $simple_html, false, $uriid ?? 0); + } + /** * Converts a BBCode message to HTML message * @@ -1275,22 +1426,25 @@ class BBCode * @param bool $try_oembed * @param int $simple_html * @param bool $for_plaintext + * @param int $uriid * @return string * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function convert(string $text = null, $try_oembed = true, $simple_html = self::INTERNAL, $for_plaintext = false) + public static function convert(string $text = null, $try_oembed = true, $simple_html = self::INTERNAL, $for_plaintext = false, $uriid = 0) { // Accounting for null default column values if (is_null($text) || $text === '') { return ''; } + DI::profiler()->startRecording('rendering'); + Hook::callAll('bbcode', $text); $a = DI::app(); - $text = self::performWithEscapedTags($text, ['code'], function ($text) use ($try_oembed, $simple_html, $for_plaintext, $a) { - $text = self::performWithEscapedTags($text, ['noparse', 'nobb', 'pre'], function ($text) use ($try_oembed, $simple_html, $for_plaintext, $a) { + $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) { /* * preg_match_callback function to replace potential Oembed tags with Oembed content * @@ -1389,26 +1543,26 @@ class BBCode } while ($oldtext != $text); } - // Add HTML new lines - $text = str_replace("\n", '
', $text); - /// @todo Have a closer look at the different html modes // Handle attached links or videos - if ($simple_html == self::ACTIVITYPUB) { + if (in_array($simple_html, [self::API, self::ACTIVITYPUB])) { $text = self::removeAttachment($text); - } elseif (!in_array($simple_html, [self::INTERNAL, self::CONNECTORS])) { + } elseif (!in_array($simple_html, [self::INTERNAL, self::EXTERNAL, self::CONNECTORS])) { $text = self::removeAttachment($text, true); } else { - $text = self::convertAttachment($text, $simple_html, $try_oembed); + $text = self::convertAttachment($text, $simple_html, $try_oembed, [], $uriid); } + // Add HTML new lines + $text = str_replace("\n", '
', $text); + $nosmile = strpos($text, '[nosmile]') !== false; $text = str_replace('[nosmile]', '', $text); // Replace non graphical smilies for external posts - if (!$nosmile && !$for_plaintext) { - $text = self::performWithEscapedTags($text, ['img'], function ($text) { - return Smilies::replace($text); + if (!$nosmile) { + $text = self::performWithEscapedTags($text, ['img'], function ($text) use ($simple_html, $for_plaintext) { + return Smilies::replace($text, ($simple_html != self::INTERNAL) || $for_plaintext); }); } @@ -1574,12 +1728,12 @@ class BBCode // [img=widthxheight]image source[/img] $text = preg_replace_callback( "/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", - function ($matches) use ($simple_html) { + function ($matches) use ($simple_html, $uriid) { if (strpos($matches[3], "data:image/") === 0) { return $matches[0]; } - $matches[3] = self::proxyUrl($matches[3], $simple_html); + $matches[3] = self::proxyUrl($matches[3], $simple_html, $uriid); return "[img=" . $matches[1] . "x" . $matches[2] . "]" . $matches[3] . "[/img]"; }, $text @@ -1589,8 +1743,8 @@ class BBCode $text = preg_replace("/\[zmg\=([0-9]*)x([0-9]*)\](.*?)\[\/zmg\]/ism", '', $text); $text = preg_replace_callback("/\[img\=(.*?)\](.*?)\[\/img\]/ism", - function ($matches) use ($simple_html) { - $matches[1] = self::proxyUrl($matches[1], $simple_html); + function ($matches) use ($simple_html, $uriid) { + $matches[1] = self::proxyUrl($matches[1], $simple_html, $uriid); $matches[2] = htmlspecialchars($matches[2], ENT_COMPAT); return '' . $matches[2] . ''; }, @@ -1600,12 +1754,12 @@ class BBCode // [img]pathtoimage[/img] $text = preg_replace_callback( "/\[img\](.*?)\[\/img\]/ism", - function ($matches) use ($simple_html) { + function ($matches) use ($simple_html, $uriid) { if (strpos($matches[1], "data:image/") === 0) { return $matches[0]; } - $matches[1] = self::proxyUrl($matches[1], $simple_html); + $matches[1] = self::proxyUrl($matches[1], $simple_html, $uriid); return "[img]" . $matches[1] . "[/img]"; }, $text @@ -1613,6 +1767,8 @@ class BBCode $text = preg_replace("/\[img\](.*?)\[\/img\]/ism", '' . DI::l10n()->t('Image/photo') . '', $text); $text = preg_replace("/\[zmg\](.*?)\[\/zmg\]/ism", '' . DI::l10n()->t('Image/photo') . '', $text); + + $text = self::convertImages($text, $simple_html, $uriid); $text = preg_replace("/\[crypt\](.*?)\[\/crypt\]/ism", '
' . DI::l10n()->t('Encrypted content') . '
', $text); $text = preg_replace("/\[crypt(.*?)\](.*?)\[\/crypt\]/ism", '
' . DI::l10n()->t('Encrypted content') . '
', $text); @@ -1654,7 +1810,7 @@ class BBCode $text = preg_replace("/\[youtube\]https?:\/\/youtu.be\/(.*?)\[\/youtube\]/ism", '[youtube]$1[/youtube]', $text); if ($try_oembed) { - $text = preg_replace("/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism", '', $text); + $text = preg_replace("/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism", '', $text); } else { $text = preg_replace("/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism", 'https://www.youtube.com/watch?v=$1', $text); @@ -1669,7 +1825,7 @@ class BBCode $text = preg_replace("/\[vimeo\]https?:\/\/vimeo.com\/([0-9]+)(.*?)\[\/vimeo\]/ism", '[vimeo]$1[/vimeo]', $text); if ($try_oembed) { - $text = preg_replace("/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism", '', $text); + $text = preg_replace("/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism", '', $text); } else { $text = preg_replace("/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism", 'https://vimeo.com/$1', $text); @@ -1687,7 +1843,7 @@ class BBCode // 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); + $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); @@ -1706,32 +1862,35 @@ class BBCode } } - if (!$for_plaintext) { - if (in_array($simple_html, [self::OSTATUS, self::ACTIVITYPUB])) { - $text = preg_replace_callback("/\[url\](.*?)\[\/url\]/ism", 'self::convertUrlForActivityPubCallback', $text); - $text = preg_replace_callback("/\[url\=(.*?)\](.*?)\[\/url\]/ism", 'self::convertUrlForActivityPubCallback', $text); - } - } else { - $text = preg_replace("(\[url\](.*?)\[\/url\])ism", " $1 ", $text); - $text = preg_replace_callback("&\[url=([^\[\]]*)\]\[img\](.*)\[\/img\]\[\/url\]&Usi", 'self::removePictureLinksCallback', $text); - } - - // Remove all hashtag addresses - if ($simple_html && !in_array($simple_html, [self::DIASPORA, self::OSTATUS, self::ACTIVITYPUB])) { - $text = preg_replace("/([#@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", '$1$3', $text); - } elseif ($simple_html == self::DIASPORA) { + // Handle mentions and hashtag links + if ($simple_html == self::DIASPORA) { // The ! is converted to @ since Diaspora only understands the @ $text = preg_replace("/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", '@$3', $text); } elseif (in_array($simple_html, [self::OSTATUS, self::ACTIVITYPUB])) { $text = preg_replace("/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", - '$1$3', + '$1$3', $text); - } elseif (!$simple_html) { + $text = preg_replace("/([#])\[url\=(.*?)\](.*?)\[\/url\]/ism", + '', + $text); + } elseif (in_array($simple_html, [self::INTERNAL, self::EXTERNAL, self::API])) { $text = preg_replace("/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", - '$1$3', + '$1$3', $text); + } else { + $text = preg_replace("/([#@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", '$1$3', $text); + } + + if (!$for_plaintext) { + if (in_array($simple_html, [self::OSTATUS, self::API, self::ACTIVITYPUB])) { + $text = preg_replace_callback("/\[url\](.*?)\[\/url\]/ism", 'self::convertUrlForActivityPubCallback', $text); + $text = preg_replace_callback("/\[url\=(.*?)\](.*?)\[\/url\]/ism", 'self::convertUrlForActivityPubCallback', $text); + } + } else { + $text = preg_replace("(\[url\](.*?)\[\/url\])ism", " $1 ", $text); + $text = preg_replace_callback("&\[url=([^\[\]]*)\]\[img\](.*)\[\/img\]\[\/url\]&Usi", 'self::removePictureLinksCallback', $text); } // Bookmarks in red - will be converted to bookmarks in friendica @@ -1740,7 +1899,7 @@ class BBCode $text = preg_replace("/#\[url\=.*?\]\^\[\/url\]\[url\=(.*?)\](.*?)\[\/url\]/i", "[bookmark=$1]$2[/bookmark]", $text); - if (in_array($simple_html, [self::API, self::OSTATUS, self::TWITTER])) { + if (in_array($simple_html, [self::OSTATUS, self::TWITTER])) { $text = preg_replace_callback("/([^#@!])\[url\=([^\]]*)\](.*?)\[\/url\]/ism", "self::expandLinksCallback", $text); //$Text = preg_replace("/[^#@!]\[url\=([^\]]*)\](.*?)\[\/url\]/ism", ' $2 [url]$1[/url]', $Text); $text = preg_replace("/\[bookmark\=([^\]]*)\](.*?)\[\/bookmark\]/ism", ' $2 [url]$1[/url]',$text); @@ -1779,7 +1938,7 @@ class BBCode * - [url=]#[/url] */ $text = preg_replace_callback("/(?:#\[url\=[^\[\]]*\]|\[url\=[^\[\]]*\]#)(.*?)\[\/url\]/ism", function($matches) use ($simple_html) { - if ($simple_html == BBCode::ACTIVITYPUB) { + if ($simple_html == self::ACTIVITYPUB) { return '#' . XML::escape($matches[1]) . ''; @@ -1850,10 +2009,10 @@ class BBCode $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 ); - $text = self::interpolateSavedImagesIntoItemBody($text, $saved_image); + $text = self::interpolateSavedImagesIntoItemBody($uriid, $text, $saved_image); return $text; }); // Escaped noparse, nobb, pre @@ -1901,8 +2060,9 @@ class BBCode ); $text = HTML::purify($text, $allowedIframeDomains); + DI::profiler()->stopRecording(); - return $text; + return trim($text); } /** @@ -1913,9 +2073,11 @@ class BBCode */ public static function stripAbstract($text) { + DI::profiler()->startRecording('rendering'); $text = preg_replace("/[\s|\n]*\[abstract\].*?\[\/abstract\][\s|\n]*/ism", '', $text); $text = preg_replace("/[\s|\n]*\[abstract=.*?\].*?\[\/abstract][\s|\n]*/ism", '', $text); + DI::profiler()->stopRecording(); return $text; } @@ -1928,6 +2090,7 @@ class BBCode */ public static function getAbstract($text, $addon = '') { + DI::profiler()->startRecording('rendering'); $abstract = ''; $abstracts = []; $addon = strtolower($addon); @@ -1946,6 +2109,7 @@ class BBCode $abstract = $result[1]; } + DI::profiler()->stopRecording(); return $abstract; } @@ -1984,6 +2148,7 @@ class BBCode */ public static function toMarkdown($text, $for_diaspora = true) { + DI::profiler()->startRecording('rendering'); $original_text = $text; // Since Diaspora is creating a summary for links, this function removes them before posting @@ -2028,13 +2193,9 @@ class BBCode // Maybe we should make this newline at every time before a quote. $text = str_replace(['
'], ['
'], $text); - $stamp1 = microtime(true); - // Now convert HTML to Markdown $text = HTML::toMarkdown($text); - DI::profiler()->saveTimestamp($stamp1, "parser"); - // Libertree has a problem with escaped hashtags. $text = str_replace(['\#'], ['#'], $text); @@ -2053,6 +2214,7 @@ class BBCode Hook::callAll('bb2diaspora', $text); + DI::profiler()->stopRecording(); return $text; } @@ -2071,9 +2233,10 @@ class BBCode */ public static function getTags($string) { + DI::profiler()->startRecording('rendering'); $ret = []; - BBCode::performWithEscapedTags($string, ['noparse', 'pre', 'code', 'img'], function ($string) use (&$ret) { + self::performWithEscapedTags($string, ['noparse', 'pre', 'code', 'img'], function ($string) use (&$ret) { // Convert hashtag links to hashtags $string = preg_replace('/#\[url\=([^\[\]]*)\](.*?)\[\/url\]/ism', '#$2 ', $string); @@ -2121,9 +2284,36 @@ class BBCode } }); + DI::profiler()->stopRecording(); return array_unique($ret); } + /** + * Expand tags to URLs + * + * @param string $body + * @return string body with expanded tags + */ + public static function expandTags(string $body) + { + return preg_replace_callback("/([!#@])([^\^ \x0D\x0A,;:?\']*[^\^ \x0D\x0A,;:?!\'.])/", + function ($match) { + switch ($match[1]) { + case '!': + case '@': + $contact = Contact::getByURL($match[2]); + if (!empty($contact)) { + return $match[1] . '[url=' . $contact['url'] . ']' . $contact['name'] . '[/url]'; + } else { + return $match[1] . $match[2]; + } + break; + case '#': + return $match[1] . '[url=' . 'https://' . DI::baseUrl() . '/search?tag=' . $match[2] . ']' . $match[2] . '[/url]'; + } + }, $body); + } + /** * Perform a custom function on a text after having escaped blocks enclosed in the provided tag list. * @@ -2154,8 +2344,9 @@ class BBCode */ public static function setMentions($body, $profile_uid = 0, $network = '') { - BBCode::performWithEscapedTags($body, ['noparse', 'pre', 'code', 'img'], function ($body) use ($profile_uid, $network) { - $tags = BBCode::getTags($body); + DI::profiler()->startRecording('rendering'); + self::performWithEscapedTags($body, ['noparse', 'pre', 'code', 'img'], function ($body) use ($profile_uid, $network) { + $tags = self::getTags($body); $tagged = []; $inform = ''; @@ -2177,9 +2368,7 @@ class BBCode } } - $success = Item::replaceTag($body, $inform, $profile_uid, $tag, $network); - - if ($success['replaced']) { + if (($success = Item::replaceTag($body, $inform, $profile_uid, $tag, $network)) && $success['replaced']) { $tagged[] = $tag; } } @@ -2187,6 +2376,7 @@ class BBCode return $body; }); + DI::profiler()->stopRecording(); return $body; } @@ -2202,6 +2392,7 @@ class BBCode */ public static function getShareOpeningTag(string $author, string $profile, string $avatar, string $link, string $posted, string $guid = null) { + DI::profiler()->startRecording('rendering'); $header = "[share author='" . str_replace(["'", "[", "]"], ["'", "[", "]"], $author) . "' profile='" . str_replace(["'", "[", "]"], ["'", "[", "]"], $profile) . "' avatar='" . str_replace(["'", "[", "]"], ["'", "[", "]"], $avatar) . @@ -2214,6 +2405,7 @@ class BBCode $header .= "']"; + DI::profiler()->stopRecording(); return $header; } @@ -2235,6 +2427,7 @@ class BBCode */ public static function embedURL(string $url, bool $tryAttachment = true, string $title = null, string $description = null, string $tags = null): string { + DI::profiler()->startRecording('rendering'); DI::logger()->info($url); // If there is already some content information submitted we don't @@ -2256,6 +2449,7 @@ class BBCode DI::logger()->info('(unparsed): returns: ' . $result); + DI::profiler()->stopRecording(); return $result; } @@ -2274,6 +2468,7 @@ class BBCode break; } + DI::profiler()->stopRecording(); return $bbcode; } @@ -2281,10 +2476,13 @@ class BBCode // Bypass attachment if parse url for a comment if (!$tryAttachment) { + DI::profiler()->stopRecording(); return "\n" . '[url=' . $url . ']' . $siteinfo['title'] . '[/url]'; } // Format it as BBCode attachment - return "\n" . PageInfo::getFooterFromData($siteinfo); + $bbcode = "\n" . PageInfo::getFooterFromData($siteinfo); + DI::profiler()->stopRecording(); + return $bbcode; } }