X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;ds=sidebyside;f=src%2FContent%2FText%2FBBCode.php;h=e95ed0675983e48f238c6930f99c9f5db5faf1a3;hb=7e618856ab09ac74a3760e238c73ecb9515f6701;hp=8a3eb3d570f540fbc25666a8ab7f6c4789b456b7;hpb=c4ec80e839cae1105191fb2c1d394590f9938c3f;p=friendica.git diff --git a/src/Content/Text/BBCode.php b/src/Content/Text/BBCode.php index 8a3eb3d570..e95ed06759 100644 --- a/src/Content/Text/BBCode.php +++ b/src/Content/Text/BBCode.php @@ -1,6 +1,6 @@ '; const BOTTOM_ANCHOR = '
'; + + const PREVIEW_NONE = 0; + const PREVIEW_NO_IMAGE = 1; + const PREVIEW_LARGE = 2; + const PREVIEW_SMALL = 3; + /** * Fetches attachment data that were generated the old way * @@ -81,7 +88,7 @@ class BBCode * 'description' -> Description of the attachment * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private static function getOldAttachmentData($body) + private static function getOldAttachmentData(string $body): array { $post = []; @@ -152,7 +159,7 @@ class BBCode * 'description' -> Description of the attachment * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function getAttachmentData($body) + public static function getAttachmentData(string $body): array { DI::profiler()->startRecording('rendering'); $data = [ @@ -187,26 +194,31 @@ class BBCode case 'publisher_name': $data['provider_name'] = html_entity_decode($value, ENT_QUOTES, 'UTF-8'); break; + case 'publisher_url': $data['provider_url'] = html_entity_decode($value, ENT_QUOTES, 'UTF-8'); break; + case 'author_name': $data['author_name'] = html_entity_decode($value, ENT_QUOTES, 'UTF-8'); if ($data['provider_name'] == $data['author_name']) { $data['author_name'] = ''; } break; + case 'author_url': $data['author_url'] = html_entity_decode($value, ENT_QUOTES, 'UTF-8'); if ($data['provider_url'] == $data['author_url']) { $data['author_url'] = ''; } break; + case 'title': $value = self::convert(html_entity_decode($value, ENT_QUOTES, 'UTF-8'), false, true); $value = html_entity_decode($value, ENT_QUOTES, 'UTF-8'); $value = str_replace(['[', ']'], ['[', ']'], $value); $data['title'] = $value; + default: $data[$field] = html_entity_decode($value, ENT_QUOTES, 'UTF-8'); break; @@ -241,7 +253,7 @@ class BBCode return $data; } - public static function getAttachedData($body, $item = []) + public static function getAttachedData(string $body, array $item = []): array { /* - text: @@ -261,8 +273,8 @@ class BBCode // Get all linked images with alternative image description 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]]; + if ($id = Photo::getIdForName($picture[1])) { + $post['images'][] = ['url' => str_replace('-1.', '-0.', $picture[1]), 'description' => $picture[2], 'id' => $id]; } else { $post['remote_images'][] = ['url' => $picture[1], 'description' => $picture[2]]; } @@ -274,22 +286,25 @@ class BBCode if (preg_match_all("/\[img\]([^\[\]]*)\[\/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' => '']; + if ($id = Photo::getIdForName($picture[1])) { + $post['images'][] = ['url' => str_replace('-1.', '-0.', $picture[1]), 'description' => '', 'id' => $id]; } else { $post['remote_images'][] = ['url' => $picture[1], 'description' => '']; } } } - // if nothing is found, it maybe having an image. if (!isset($post['type'])) { - // Simplify image codes - $body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body); - $body = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", '[img]$1[/img]', $body); $post['text'] = $body; + } + + // Simplify image codes + $post['text'] = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $post['text']); + $post['text'] = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", '[img]$1[/img]', $post['text']); - if (preg_match_all("#\[url=([^\]]+?)\]\s*\[img\]([^\[]+?)\[/img\]\s*\[/url\]#ism", $body, $pictures, PREG_SET_ORDER)) { + // if nothing is found, it maybe having an image. + if (!isset($post['type'])) { + if (preg_match_all("#\[url=([^\]]+?)\]\s*\[img\]([^\[]+?)\[/img\]\s*\[/url\]#ism", $post['text'], $pictures, PREG_SET_ORDER)) { if ((count($pictures) == 1) && !$has_title) { if (!empty($item['object-type']) && ($item['object-type'] == Activity\ObjectType::IMAGE)) { // Replace the preview picture with the real picture @@ -303,7 +318,7 @@ class BBCode // Workaround: // Sometimes photo posts to the own album are not detected at the start. // So we seem to cannot use the cache for these cases. That's strange. - if (($data['type'] != 'photo') && strstr($pictures[0][1], "/photos/")) { + if (($data['type'] != 'photo') && strstr($pictures[0][1], '/photos/')) { $data = ParseUrl::getSiteinfo($pictures[0][1]); } @@ -317,14 +332,14 @@ class BBCode } $post['preview'] = $pictures[0][2]; - $post['text'] = trim(str_replace($pictures[0][0], '', $body)); + $post['text'] = trim(str_replace($pictures[0][0], '', $post['text'])); } else { $imgdata = Images::getInfoFromURLCached($pictures[0][1]); - if ($imgdata && substr($imgdata['mime'], 0, 6) == 'image/') { + if (($imgdata) && substr($imgdata['mime'], 0, 6) == 'image/') { $post['type'] = 'photo'; $post['image'] = $pictures[0][1]; $post['preview'] = $pictures[0][2]; - $post['text'] = trim(str_replace($pictures[0][0], '', $body)); + $post['text'] = trim(str_replace($pictures[0][0], '', $post['text'])); } } } elseif (count($pictures) > 0) { @@ -336,13 +351,12 @@ class BBCode } $post['image'] = $pictures[0][2]; - $post['text'] = $body; foreach ($pictures as $picture) { $post['text'] = trim(str_replace($picture[0], '', $post['text'])); } } - } elseif (preg_match_all("(\[img\](.*?)\[\/img\])ism", $body, $pictures, PREG_SET_ORDER)) { + } elseif (preg_match_all("(\[img\](.*?)\[\/img\])ism", $post['text'], $pictures, PREG_SET_ORDER)) { if ($has_title) { $post['type'] = 'link'; $post['url'] = $plink; @@ -351,7 +365,6 @@ class BBCode } $post['image'] = $pictures[0][1]; - $post['text'] = $body; foreach ($pictures as $picture) { $post['text'] = trim(str_replace($picture[0], '', $post['text'])); } @@ -390,8 +403,7 @@ class BBCode } if (!isset($post['type'])) { - $post['type'] = "text"; - $post['text'] = trim($body); + $post['type'] = 'text'; } if (($post['type'] == 'photo') && empty($post['images']) && !empty($post['remote_images'])) { @@ -408,6 +420,10 @@ class BBCode if (isset($data['images'][0])) { $post['image'] = $data['images'][0]['src']; } + } elseif (preg_match_all("#\[url=([^\]]+?)\]\s*\[img\]([^\[]+?)\[/img\]\s*\[/url\]#ism", $post['text'], $pictures, PREG_SET_ORDER)) { + foreach ($pictures as $picture) { + $post['text'] = trim(str_replace($picture[0], '', $post['text'])); + } } DI::profiler()->stopRecording(); @@ -419,10 +435,9 @@ class BBCode * * @param string $body * @param boolean $no_link_desc No link description - * * @return string with replaced body */ - public static function removeAttachment($body, $no_link_desc = false) + public static function removeAttachment(string $body, bool $no_link_desc = false): string { return preg_replace_callback("/\s*\[attachment (.*?)\](.*?)\[\/attachment\]\s*/ism", function ($match) use ($body, $no_link_desc) { @@ -442,12 +457,11 @@ class BBCode /** * Converts a BBCode text into plaintext * - * @param $text + * @param string $text * @param bool $keep_urls Whether to keep URLs in the resulting plaintext - * * @return string */ - public static function toPlaintext($text, $keep_urls = true) + public static function toPlaintext(string $text, bool $keep_urls = true): string { DI::profiler()->startRecording('rendering'); // Remove pictures in advance to avoid unneeded proxy calls @@ -463,10 +477,10 @@ class BBCode return $naked_text; } - private static function proxyUrl($image, $simplehtml = self::INTERNAL, $uriid = 0, $size = '') + private static function proxyUrl(string $image, int $simplehtml = self::INTERNAL, int $uriid = 0, string $size = ''): string { // Only send proxied pictures to API and for internal display - if (!in_array($simplehtml, [self::INTERNAL, self::API])) { + if (!in_array($simplehtml, [self::INTERNAL, self::MASTODON_API, self::TWITTER_API])) { return $image; } elseif ($uriid > 0) { return Post\Link::getByLink($uriid, $image, $size); @@ -483,7 +497,7 @@ class BBCode * @param string $srctext The body with images * @return string The body with possibly scaled images */ - public static function scaleExternalImages(string $srctext) + public static function scaleExternalImages(string $srctext): string { DI::profiler()->startRecording('rendering'); $s = $srctext; @@ -495,7 +509,7 @@ class BBCode $c = preg_match_all('/\[img.*?\](.*?)\[\/img\]/ism', $s, $matches, PREG_SET_ORDER); if ($c) { foreach ($matches as $mtch) { - Logger::info('scale_external_image', ['image' => $mtch[1]]); + Logger::debug('scale_external_image', ['image' => $mtch[1]]); $hostname = str_replace('www.', '', substr(DI::baseUrl(), strpos(DI::baseUrl(), '://') + 3)); if (stristr($mtch[1], $hostname)) { @@ -523,14 +537,14 @@ class BBCode $Image->scaleDown(640); $new_width = $Image->getWidth(); $new_height = $Image->getHeight(); - Logger::info('External images scaled', ['orig_width' => $orig_width, 'new_width' => $new_width, 'orig_height' => $orig_height, 'new_height' => $new_height, 'match' => $mtch[0]]); + Logger::debug('External images scaled', ['orig_width' => $orig_width, 'new_width' => $new_width, 'orig_height' => $orig_height, 'new_height' => $new_height, 'match' => $mtch[0]]); $s = str_replace( $mtch[0], '[img=' . $new_width . 'x' . $new_height. ']' . $mtch[1] . '[/img]' . "\n", $s ); - Logger::info('New string', ['image' => $s]); + Logger::debug('New string', ['image' => $s]); } } } @@ -551,7 +565,7 @@ class BBCode * @return string * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function limitBodySize($body) + public static function limitBodySize(string $body): string { DI::profiler()->startRecording('rendering'); $maxlen = DI::config()->get('config', 'max_import_size', 0); @@ -580,7 +594,7 @@ class BBCode if (($textlen + $img_start) > $maxlen) { if ($textlen < $maxlen) { - Logger::info('the limit happens before an embedded image'); + Logger::debug('the limit happens before an embedded image'); $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen); $textlen = $maxlen; } @@ -594,7 +608,7 @@ class BBCode if (($textlen + $img_end) > $maxlen) { if ($textlen < $maxlen) { - Logger::info('the limit happens before the end of a non-embedded image'); + Logger::debug('the limit happens before the end of a non-embedded image'); $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen); $textlen = $maxlen; } @@ -617,11 +631,11 @@ class BBCode if (($textlen + strlen($orig_body)) > $maxlen) { if ($textlen < $maxlen) { - Logger::info('the limit happens after the end of the last image'); + Logger::debug('the limit happens after the end of the last image'); $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen); } } else { - Logger::info('the text size with embedded images extracted did not violate the limit'); + Logger::debug('the text size with embedded images extracted did not violate the limit'); $new_body = $new_body . $orig_body; } @@ -646,7 +660,7 @@ class BBCode * @return string * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function convertAttachment($text, $simplehtml = self::INTERNAL, $tryoembed = true, array $data = [], $uriid = 0) + public static function convertAttachment(string $text, int $simplehtml = self::INTERNAL, bool $tryoembed = true, array $data = [], int $uriid = 0, int $preview_mode = self::PREVIEW_LARGE): string { DI::profiler()->startRecording('rendering'); $data = $data ?: self::getAttachmentData($text); @@ -659,10 +673,10 @@ class BBCode $data['title'] = strip_tags($data['title']); $data['title'] = str_replace(['http://', 'https://'], '', $data['title']); } else { - $data['title'] = null; + $data['title'] = ''; } - if (((strpos($data['text'], "[img=") !== false) || (strpos($data['text'], "[img]") !== false) || DI::config()->get('system', 'always_show_preview')) && !empty($data['image'])) { + if (((strpos($data['text'], '[img=') !== false) || (strpos($data['text'], '[img]') !== false) || DI::config()->get('system', 'always_show_preview')) && !empty($data['image'])) { $data['preview'] = $data['image']; $data['image'] = ''; } @@ -681,12 +695,18 @@ class BBCode $return = sprintf('
', $data['type']); } + if ($preview_mode == self::PREVIEW_NO_IMAGE) { + unset($data['image']); + unset($data['preview']); + } + if (!empty($data['title']) && !empty($data['url'])) { + $preview_class = $preview_mode == self::PREVIEW_LARGE ? 'attachment-image' : 'attachment-preview'; if (!empty($data['image']) && empty($data['text']) && ($data['type'] == 'photo')) { - $return .= sprintf('', $data['url'], self::proxyUrl($data['image'], $simplehtml, $uriid), $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, $uriid), $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, $uriid), $data['title']); } @@ -716,14 +736,14 @@ class BBCode return trim(($data['text'] ?? '') . ' ' . $return . ' ' . ($data['after'] ?? '')); } - public static function removeShareInformation($Text, $plaintext = false, $nolink = false) + public static function removeShareInformation(string $text, bool $plaintext = false, bool $nolink = false): string { DI::profiler()->startRecording('rendering'); - $data = self::getAttachmentData($Text); + $data = self::getAttachmentData($text); if (!$data) { DI::profiler()->stopRecording(); - return $Text; + return $text; } elseif ($nolink) { DI::profiler()->stopRecording(); return $data['text'] . ($data['after'] ?? ''); @@ -767,7 +787,7 @@ class BBCode * @param array $match Array with the matching values * @return string reformatted link including HTML codes */ - private static function convertUrlForActivityPubCallback($match) + private static function convertUrlForActivityPubCallback(array $match): string { $url = $match[1]; @@ -789,10 +809,9 @@ class BBCode * @param string $url URL that is about to be reformatted * @return string reformatted link including HTML codes */ - private static function convertUrlForActivityPub($url) + private static function convertUrlForActivityPub(string $url): string { - $html = '%s'; - return sprintf($html, $url, self::getStyledURL($url)); + return sprintf('%s', $url, self::getStyledURL($url)); } /** @@ -801,7 +820,7 @@ class BBCode * @param string $url URL that is about to be reformatted * @return string reformatted link */ - private static function getStyledURL($url) + private static function getStyledURL(string $url): string { $parts = parse_url($url); $scheme = $parts['scheme'] . '://'; @@ -818,8 +837,11 @@ class BBCode * [noparse][i]italic[/i][/noparse] turns into * [noparse][ i ]italic[ /i ][/noparse], * to hide them from parser. + * + * @param array $match + * @return string */ - private static function escapeNoparseCallback($match) + private static function escapeNoparseCallback(array $match): string { $whole_match = $match[0]; $captured = $match[1]; @@ -832,8 +854,11 @@ class BBCode * The previously spacefied [noparse][ i ]italic[ /i ][/noparse], * now turns back and the [noparse] tags are trimed * returning [i]italic[/i] + * + * @param array $match + * @return string */ - private static function unescapeNoparseCallback($match) + private static function unescapeNoparseCallback(array $match): string { $captured = $match[1]; $unspacefied = preg_replace("/\[ (.*?)\ ]/", "[$1]", $captured); @@ -849,7 +874,7 @@ class BBCode * @param int $occurrences Number of first occurrences to skip * @return boolean|array */ - public static function getTagPosition($text, $name, $occurrences = 0) + public static function getTagPosition(string $text, string $name, int $occurrences = 0) { DI::profiler()->startRecording('rendering'); if ($occurrences < 0) { @@ -913,7 +938,7 @@ class BBCode * @param string $text Text to search * @return string */ - public static function pregReplaceInTag($pattern, $replace, $name, $text) + public static function pregReplaceInTag(string $pattern, string $replace, string $name, string $text): string { DI::profiler()->startRecording('rendering'); $occurrences = 0; @@ -936,7 +961,7 @@ class BBCode return $text; } - private static function extractImagesFromItemBody($body) + private static function extractImagesFromItemBody(string $body): array { $saved_image = []; $orig_body = $body; @@ -977,7 +1002,7 @@ class BBCode return ['body' => $new_body, 'images' => $saved_image]; } - private static function interpolateSavedImagesIntoItemBody($uriid, $body, array $images) + private static function interpolateSavedImagesIntoItemBody(int $uriid, string $body, array $images): string { $newbody = $body; @@ -995,32 +1020,80 @@ class BBCode } /** - * - * @param string $text A BBCode string - * @return array share attributes + * @param string $text A BBCode string + * @return array Empty array if no share tag is present or the following array, missing attributes end up empty strings: + * - comment : Text before the opening share tag + * - shared : Text inside the share tags + * - author : (Optional) Display name of the shared author + * - profile : (Optional) Profile page URL of the shared author + * - avatar : (Optional) Profile picture URL of the shared author + * - link : (Optional) Canonical URL of the shared post + * - posted : (Optional) Date the shared post was initially posted ("Y-m-d H:i:s" in GMT) + * - message_id: (Optional) Shared post URI if any + * - guid : (Optional) Shared post GUID if any */ - public static function fetchShareAttributes($text) + public static function fetchShareAttributes(string $text): array { DI::profiler()->startRecording('rendering'); + if (preg_match('~(.*?)\[share](.*)\[/share]~ism', $text, $matches)) { + DI::profiler()->stopRecording(); + return [ + 'author' => '', + 'profile' => '', + 'avatar' => '', + 'link' => '', + 'posted' => '', + 'guid' => '', + 'message_id' => trim($matches[2]), + 'comment' => trim($matches[1]), + 'shared' => '', + ]; + } // 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); + $text = preg_replace('~([@!#])\[url=.*?](.*?)\[/url]~ism', '$1$2', $text); - $attributes = []; - if (!preg_match("/(.*?)\[share(.*?)\](.*)\[\/share\]/ism", $text, $matches)) { + if (!preg_match('~(.*?)\[share(.*?)](.*)\[/share]~ism', $text, $matches)) { DI::profiler()->stopRecording(); - return $attributes; + return []; } - $attribute_string = $matches[2]; - foreach (['author', 'profile', 'avatar', 'link', 'posted', 'guid'] as $field) { - preg_match("/$field=(['\"])(.+?)\\1/ism", $attribute_string, $matches); + $attributes = self::extractShareAttributes($matches[2]); + + $attributes['comment'] = trim($matches[1]); + $attributes['shared'] = trim($matches[3]); + + DI::profiler()->stopRecording(); + return $attributes; + } + + /** + * @see BBCode::fetchShareAttributes() + * @param string $shareString Internal opening share tag string matched by the regular expression + * @return array A fixed attribute array where missing attribute are represented by empty strings + */ + private static function extractShareAttributes(string $shareString): array + { + $attributes = []; + foreach (['author', 'profile', 'avatar', 'link', 'posted', 'guid', 'message_id'] as $field) { + preg_match("/$field=(['\"])(.+?)\\1/ism", $shareString, $matches); $attributes[$field] = html_entity_decode($matches[2] ?? '', ENT_QUOTES, 'UTF-8'); } - DI::profiler()->stopRecording(); + return $attributes; } + /** + * Remove the share block + * + * @param string $body + * @return string + */ + public static function removeSharedData(string $body): string + { + return trim(preg_replace("/\s*\[share.*?\].*?\[\/share\]\s*/ism", '', $body)); + } + /** * This function converts a [share] block to text according to a provided callback function whose signature is: * @@ -1040,18 +1113,13 @@ class BBCode * @param callable $callback * @return string The BBCode string with all [share] blocks replaced */ - public static function convertShare($text, callable $callback, int $uriid = 0) + public static function convertShare(string $text, callable $callback, int $uriid = 0): string { DI::profiler()->startRecording('rendering'); $return = preg_replace_callback( - "/(.*?)\[share(.*?)\](.*)\[\/share\]/ism", + '~(.*?)\[share(.*?)](.*)\[/share]~ism', function ($match) use ($callback, $uriid) { - $attribute_string = $match[2]; - $attributes = []; - foreach (['author', 'profile', 'avatar', 'link', 'posted', 'guid'] as $field) { - preg_match("/$field=(['\"])(.+?)\\1/ism", $attribute_string, $matches); - $attributes[$field] = html_entity_decode($matches[2] ?? '', ENT_QUOTES, 'UTF-8'); - } + $attributes = self::extractShareAttributes($match[2]); $author_contact = Contact::getByURL($attributes['profile'], false, ['id', 'url', 'addr', 'name', 'micro']); $author_contact['url'] = ($author_contact['url'] ?? $attributes['profile']); @@ -1075,7 +1143,7 @@ class BBCode ); DI::profiler()->stopRecording(); - return $return; + return trim($return); } /** @@ -1086,7 +1154,7 @@ class BBCode * @param integer $uriid * @return string */ - private static function convertImages(string $text, int $simplehtml, int $uriid = 0):string + private static function convertImages(string $text, int $simplehtml, int $uriid = 0): string { DI::profiler()->startRecording('rendering'); $return = preg_replace_callback( @@ -1129,13 +1197,14 @@ class BBCode * @return string * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private static function convertShareCallback(array $attributes, array $author_contact, $content, $is_quote_share, $simplehtml) + private static function convertShareCallback(array $attributes, array $author_contact, string $content, bool $is_quote_share, int $simplehtml): string { DI::profiler()->startRecording('rendering'); $mention = $attributes['author'] . ' (' . ($author_contact['addr'] ?? '') . ')'; switch ($simplehtml) { - case self::API: + case self::MASTODON_API: + case self::TWITTER_API: $text = ($is_quote_share? '
' : '') . '' . html_entity_decode('♲', ENT_QUOTES, 'UTF-8') . ' ' . $author_contact['addr'] . ":
\n" . '
' . $content . '
'; @@ -1198,7 +1267,7 @@ class BBCode return $text; } - private static function removePictureLinksCallback($match) + private static function removePictureLinksCallback(array $match): string { $cache_key = 'remove:' . $match[1]; $text = DI::cache()->get($cache_key); @@ -1212,9 +1281,9 @@ class BBCode } if (substr($mimetype, 0, 6) == 'image/') { - $text = "[url=" . $match[1] . ']' . $match[1] . "[/url]"; + $text = '[url=' . $match[1] . ']' . $match[1] . '[/url]'; } else { - $text = "[url=" . $match[2] . ']' . $match[2] . "[/url]"; + $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::httpClient()->fetch($match[1], HttpClientAccept::HTML, 0); @@ -1226,7 +1295,7 @@ class BBCode $doc = new DOMDocument(); @$doc->loadHTML($body); $xpath = new DOMXPath($doc); - $list = $xpath->query("//meta[@name]"); + $list = $xpath->query('//meta[@name]'); foreach ($list as $node) { $attr = []; @@ -1247,16 +1316,28 @@ class BBCode return $text; } - private static function expandLinksCallback($match) + /** + * Callback: Expands links from given $match array + * + * @param array $match Array with link match + * @return string BBCode + */ + private static function expandLinksCallback(array $match): string { if (($match[3] == '') || ($match[2] == $match[3]) || stristr($match[2], $match[3])) { - return ($match[1] . "[url]" . $match[2] . "[/url]"); + return ($match[1] . '[url]' . $match[2] . '[/url]'); } else { - return ($match[1] . $match[3] . " [url]" . $match[2] . "[/url]"); + return ($match[1] . $match[3] . ' [url]' . $match[2] . '[/url]'); } } - private static function cleanPictureLinksCallback($match) + /** + * Callback: Cleans picture links + * + * @param array $match Array with link match + * @return string BBCode + */ + private static function cleanPictureLinksCallback(array $match): string { // When the picture link is the own photo path then we can avoid fetching the link $own_photo_url = preg_quote(Strings::normaliseLink(DI::baseUrl()->get()) . '/photos/'); @@ -1302,7 +1383,7 @@ class BBCode $doc = new DOMDocument(); @$doc->loadHTML($body); $xpath = new DOMXPath($doc); - $list = $xpath->query("//meta[@name]"); + $list = $xpath->query('//meta[@name]'); foreach ($list as $node) { $attr = []; if ($node->attributes->length) { @@ -1325,16 +1406,28 @@ class BBCode return $text; } - public static function cleanPictureLinks($text) + /** + * Cleans picture links + * + * @param string $text HTML/BBCode string + * @return string Cleaned HTML/BBCode + */ + public static function cleanPictureLinks(string $text): string { 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); + $return = preg_replace_callback("&\[url=([^\[\]]*)\]\[img=(.*)\](.*)\[\/img\]\[\/url\]&Usi", [self::class, 'cleanPictureLinksCallback'], $text); + $return = preg_replace_callback("&\[url=([^\[\]]*)\]\[img\](.*)\[\/img\]\[\/url\]&Usi", [self::class, 'cleanPictureLinksCallback'], $return); DI::profiler()->stopRecording(); return $return; } - public static function removeLinks(string $bbcode) + /** + * Removes links + * + * @param string $text HTML/BBCode string + * @return string Cleaned HTML/BBCode + */ + public static function removeLinks(string $bbcode): string { DI::profiler()->startRecording('rendering'); $bbcode = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", ' $1 ', $bbcode); @@ -1350,14 +1443,14 @@ class BBCode /** * Replace names in mentions with nicknames * - * @param string $body + * @param string $body HTML/BBCode * @return string Body with replaced mentions */ - public static function setMentionsToNicknames(string $body):string + public static function setMentionsToNicknames(string $body): string { DI::profiler()->startRecording('rendering'); $regexp = "/([@!])\[url\=([^\[\]]*)\].*?\[\/url\]/ism"; - $body = preg_replace_callback($regexp, ['self', 'mentionCallback'], $body); + $body = preg_replace_callback($regexp, [self::class, 'mentionCallback'], $body); DI::profiler()->stopRecording(); return $body; } @@ -1366,10 +1459,10 @@ class BBCode * 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 + * @return string Replaced mention or empty string * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private static function mentionCallback($match) + private static function mentionCallback(array $match): string { if (empty($match[2])) { return ''; @@ -1407,7 +1500,7 @@ class BBCode * @return string * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function convertForUriId(int $uriid = null, string $text = null, int $simple_html = self::INTERNAL) + public static function convertForUriId(int $uriid = null, string $text = null, int $simple_html = self::INTERNAL): string { $try_oembed = ($simple_html == self::INTERNAL); @@ -1437,10 +1530,10 @@ class BBCode * @param int $simple_html * @param bool $for_plaintext * @param int $uriid - * @return string + * @return string Converted code or empty string * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function convert(string $text = null, $try_oembed = true, $simple_html = self::INTERNAL, $for_plaintext = false, $uriid = 0) + public static function convert(string $text = null, bool $try_oembed = true, int $simple_html = self::INTERNAL, bool $for_plaintext = false, int $uriid = 0): string { // Accounting for null default column values if (is_null($text) || $text === '') { @@ -1462,10 +1555,10 @@ class BBCode * $match[1] = $url * $match[2] = $title or absent */ - $try_oembed_callback = function ($match) + $try_oembed_callback = function (array $match) { $url = $match[1]; - $title = $match[2] ?? null; + $title = $match[2] ?? ''; try { $return = OEmbed::getHTML($url, $title); @@ -1507,8 +1600,8 @@ class BBCode $text = str_replace(">", ">", $text); // remove some newlines before the general conversion - $text = preg_replace("/\s?\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "[share$1]$2[/share]", $text); - $text = preg_replace("/\s?\[quote(.*?)\]\s?(.*?)\s?\[\/quote\]\s?/ism", "[quote$1]$2[/quote]", $text); + $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); // 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) { @@ -1555,7 +1648,7 @@ class BBCode /// @todo Have a closer look at the different html modes // Handle attached links or videos - if (in_array($simple_html, [self::API, self::ACTIVITYPUB])) { + if (in_array($simple_html, [self::MASTODON_API, self::TWITTER_API, self::ACTIVITYPUB])) { $text = self::removeAttachment($text); } elseif (!in_array($simple_html, [self::INTERNAL, self::EXTERNAL, self::CONNECTORS])) { $text = self::removeAttachment($text, true); @@ -1788,7 +1881,7 @@ class BBCode $text = preg_replace("/\[crypt\](.*?)\[\/crypt\]/ism", '
' . DI::l10n()->t('Encrypted content') . '
', $text); $text = preg_replace("/\[crypt(.*?)\](.*?)\[\/crypt\]/ism", '
' . DI::l10n()->t('Encrypted content') . '
', $text); - //$Text = preg_replace("/\[crypt=(.*?)\](.*?)\[\/crypt\]/ism", '
' . DI::l10n()->t('Encrypted content') . '
', $Text); + //$text = preg_replace("/\[crypt=(.*?)\](.*?)\[\/crypt\]/ism", '
' . DI::l10n()->t('Encrypted content') . '
', $text); // Simplify "video" element $text = preg_replace('(\[video[^\]]*?\ssrc\s?=\s?([^\s\]]+)[^\]]*?\].*?\[/video\])ism', '[video]$1[/video]', $text); @@ -1818,11 +1911,13 @@ class BBCode if ($try_oembed) { $text = preg_replace_callback("/\[youtube\](https?:\/\/www.youtube.com\/watch\?v\=.*?)\[\/youtube\]/ism", $try_oembed_callback, $text); $text = preg_replace_callback("/\[youtube\](www.youtube.com\/watch\?v\=.*?)\[\/youtube\]/ism", $try_oembed_callback, $text); + $text = preg_replace_callback("/\[youtube\](https?:\/\/www.youtube.com\/shorts\/.*?)\[\/youtube\]/ism", $try_oembed_callback, $text); $text = preg_replace_callback("/\[youtube\](https?:\/\/youtu.be\/.*?)\[\/youtube\]/ism", $try_oembed_callback, $text); } $text = preg_replace("/\[youtube\]https?:\/\/www.youtube.com\/watch\?v\=(.*?)\[\/youtube\]/ism", '[youtube]$1[/youtube]', $text); $text = preg_replace("/\[youtube\]https?:\/\/www.youtube.com\/embed\/(.*?)\[\/youtube\]/ism", '[youtube]$1[/youtube]', $text); + $text = preg_replace("/\[youtube\]https?:\/\/www.youtube.com\/shorts\/(.*?)\[\/youtube\]/ism", '[youtube]$1[/youtube]', $text); $text = preg_replace("/\[youtube\]https?:\/\/youtu.be\/(.*?)\[\/youtube\]/ism", '[youtube]$1[/youtube]', $text); if ($try_oembed) { @@ -1890,22 +1985,29 @@ class BBCode $text = preg_replace("/([#])\[url\=(.*?)\](.*?)\[\/url\]/ism", '', $text); - } elseif (in_array($simple_html, [self::INTERNAL, self::EXTERNAL, self::API])) { + } elseif (in_array($simple_html, [self::INTERNAL, self::EXTERNAL, self::TWITTER_API])) { $text = preg_replace("/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", '$1$3', $text); + } elseif ($simple_html == self::MASTODON_API) { + $text = preg_replace("/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", + '$1$3', + $text); + $text = preg_replace("/([#])\[url\=(.*?)\](.*?)\[\/url\]/ism", + '', + $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); + if (in_array($simple_html, [self::OSTATUS, self::MASTODON_API, self::TWITTER_API, self::ACTIVITYPUB])) { + $text = preg_replace_callback("/\[url\](.*?)\[\/url\]/ism", [self::class, 'convertUrlForActivityPubCallback'], $text); + $text = preg_replace_callback("/\[url\=(.*?)\](.*?)\[\/url\]/ism", [self::class, 'convertUrlForActivityPubCallback'], $text); } } else { $text = preg_replace("(\[url\](.*?)\[\/url\])ism", " $1 ", $text); - $text = preg_replace_callback("&\[url=([^\[\]]*)\]\[img\](.*)\[\/img\]\[\/url\]&Usi", 'self::removePictureLinksCallback', $text); + $text = preg_replace_callback("&\[url=([^\[\]]*)\]\[img\](.*)\[\/img\]\[\/url\]&Usi", [self::class, 'removePictureLinksCallback'], $text); } // Bookmarks in red - will be converted to bookmarks in friendica @@ -1915,9 +2017,9 @@ class BBCode "[bookmark=$1]$2[/bookmark]", $text); 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); + $text = preg_replace_callback("/([^#@!])\[url\=([^\]]*)\](.*?)\[\/url\]/ism", [self::class, 'expandLinksCallback'], $text); + //$text = preg_replace("/[^#@!]\[url\=([^\]]*)\](.*?)\[\/url\]/ism", ' $2 [url]$1[/url]', $text); + $text = preg_replace("/\[bookmark\=([^\]]*)\](.*?)\[\/bookmark\]/ism", ' $2 [url]$1[/url]', $text); } // Perform URL Search @@ -2000,7 +2102,7 @@ class BBCode $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://', 'redir/', 'cid:']; + $allowed_src_protocols = ['//', 'http://', 'https://', 'contact/redir/', 'cid:']; array_walk($allowed_src_protocols, function(&$value) { $value = preg_quote($value, '#');}); @@ -2015,7 +2117,7 @@ class BBCode $allowed_link_protocols[] = '//'; $allowed_link_protocols[] = 'http://'; $allowed_link_protocols[] = 'https://'; - $allowed_link_protocols[] = 'redir/'; + $allowed_link_protocols[] = 'contact/redir/'; array_walk($allowed_link_protocols, function(&$value) { $value = preg_quote($value, '#');}); @@ -2142,7 +2244,7 @@ class BBCode * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function bbCodeMention2DiasporaCallback($match) + private static function bbCodeMention2DiasporaCallback(array $match): string { $contact = Contact::getByURL($match[3], false, ['addr']); if (empty($contact['addr'])) { @@ -2164,7 +2266,7 @@ class BBCode * @return string * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function toMarkdown($text, $for_diaspora = true) + public static function toMarkdown(string $text, bool $for_diaspora = true): string { DI::profiler()->startRecording('rendering'); $original_text = $text; @@ -2201,7 +2303,7 @@ class BBCode $tagline .= '#' . $tag . ' '; } } - $text = $text . " " . $tagline; + $text = $text . ' ' . $tagline; } } else { $text = self::convert($text, false, self::CONNECTORS); @@ -2225,7 +2327,7 @@ class BBCode $url_search_string = "^\[\]"; $text = preg_replace_callback( "/([@!])\[(.*?)\]\(([$url_search_string]*?)\)/ism", - ['self', 'bbCodeMention2DiasporaCallback'], + [self::class, 'bbCodeMention2DiasporaCallback'], $text ); } @@ -2246,15 +2348,14 @@ class BBCode * Returns array of tags found, or empty array. * * @param string $string Post content - * * @return array List of tag and person names */ - public static function getTags($string) + public static function getTags(string $string): array { DI::profiler()->startRecording('rendering'); $ret = []; - self::performWithEscapedTags($string, ['noparse', 'pre', 'code', 'img'], function ($string) use (&$ret) { + self::performWithEscapedTags($string, ['noparse', 'pre', 'code', 'img', 'attachment'], function ($string) use (&$ret) { // Convert hashtag links to hashtags $string = preg_replace('/#\[url\=([^\[\]]*)\](.*?)\[\/url\]/ism', '#$2 ', $string); @@ -2309,13 +2410,13 @@ class BBCode /** * Expand tags to URLs, checks the tag is at the start of a line or preceded by a non-word character * - * @param string $body + * @param string $body HTML/BBCode * @return string body with expanded tags */ - public static function expandTags(string $body) + public static function expandTags(string $body): string { return preg_replace_callback("/(?<=\W|^)([!#@])([^\^ \x0D\x0A,;:?'\"]*[^\^ \x0D\x0A,;:?!'\".])/", - function ($match) { + function (array $match) { switch ($match[1]) { case '!': case '@': @@ -2326,6 +2427,7 @@ class BBCode return $match[1] . $match[2]; } break; + case '#': default: return $match[1] . '[url=' . DI::baseUrl() . '/search?tag=' . $match[2] . ']' . $match[2] . '[/url]'; @@ -2336,7 +2438,7 @@ class BBCode /** * Perform a custom function on a text after having escaped blocks enclosed in the provided tag list. * - * @param string $text + * @param string $text HTML/BBCode * @param array $tagList A list of tag names, e.g ['noparse', 'nobb', 'pre'] * @param callable $callback * @return string @@ -2352,14 +2454,14 @@ class BBCode /** * Replaces mentions in the provided message body in BBCode links for the provided user and network if any * - * @param $body - * @param $profile_uid - * @param $network - * @return string + * @param string $body HTML/BBCode + * @param int $profile_uid Profile user id + * @param string $network Network name + * @return string HTML/BBCode with inserted images * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function setMentions($body, $profile_uid = 0, $network = '') + public static function setMentions(string $body, $profile_uid = 0, $network = '') { DI::profiler()->startRecording('rendering'); $body = self::performWithEscapedTags($body, ['noparse', 'pre', 'code', 'img'], function ($body) use ($profile_uid, $network) { @@ -2403,10 +2505,11 @@ class BBCode * @param string $link Post source URL * @param string $posted Post created date * @param string|null $guid Post guid (if any) + * @param string|null $uri Post uri (if any) * @return string * @TODO Rewrite to handle over whole record array */ - public static function getShareOpeningTag(string $author, string $profile, string $avatar, string $link, string $posted, string $guid = null) + public static function getShareOpeningTag(string $author, string $profile, string $avatar, string $link, string $posted, string $guid = null, string $uri = null): string { DI::profiler()->startRecording('rendering'); $header = "[share author='" . str_replace(["'", "[", "]"], ["'", "[", "]"], $author) . @@ -2419,6 +2522,10 @@ class BBCode $header .= "' guid='" . str_replace(["'", "[", "]"], ["'", "[", "]"], $guid); } + if ($uri) { + $header .= "' message_id='" . str_replace(["'", "[", "]"], ["'", "[", "]"], $uri); + } + $header .= "']"; DI::profiler()->stopRecording(); @@ -2438,8 +2545,7 @@ class BBCode * @param string|null $tags * @return string * @throws \Friendica\Network\HTTPException\InternalServerErrorException - *@see ParseUrl::getSiteinfoCached - * + * @see ParseUrl::getSiteinfoCached */ public static function embedURL(string $url, bool $tryAttachment = true, string $title = null, string $description = null, string $tags = null): string { @@ -2493,7 +2599,7 @@ class BBCode // Bypass attachment if parse url for a comment if (!$tryAttachment) { DI::profiler()->stopRecording(); - return "\n" . '[url=' . $url . ']' . $siteinfo['title'] . '[/url]'; + return "\n" . '[url=' . $url . ']' . ($siteinfo['title'] ?? $url) . '[/url]'; } // Format it as BBCode attachment