class BBCode
{
// Update this value to the current date whenever changes are made to BBCode::convert
- const VERSION = '2021-04-05';
+ const VERSION = '2021-05-01';
const INTERNAL = 0;
+ const EXTERNAL = 1;
const API = 2;
const DIASPORA = 3;
const CONNECTORS = 4;
const BACKLINK = 8;
const ACTIVITYPUB = 9;
+ const TOP_ANCHOR = '<br class="top-anchor">';
+ const BOTTOM_ANCHOR = '<br class="button-anchor">';
/**
* Fetches attachment data that were generated the old way
*
'image' => null,
'url' => '',
'author_name' => '',
+ 'author_url' => '',
'provider_name' => '',
'provider_url' => '',
'title' => '',
$data['text'] = trim($match[1]);
- $type = '';
- preg_match("/type='(.*?)'/ism", $attributes, $matches);
- if (!empty($matches[1])) {
- $type = strtolower($matches[1]);
- }
-
- preg_match('/type="(.*?)"/ism', $attributes, $matches);
- if (!empty($matches[1])) {
- $type = strtolower($matches[1]);
- }
-
- if ($type == '') {
- return [];
+ foreach (['type', 'url', 'title', 'image', 'preview', 'publisher_name', 'publisher_url', 'author_name', 'author_url'] as $field) {
+ preg_match('/' . preg_quote($field, '/') . '=("|\')(.*?)\1/ism', $attributes, $matches);
+ $value = $matches[2] ?? '';
+
+ if ($value != '') {
+ switch ($field) {
+ 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;
+ }
+ }
}
- if (!in_array($type, ['link', 'audio', 'photo', 'video'])) {
+ if (!in_array($data['type'], ['link', 'audio', 'photo', 'video'])) {
return [];
}
- if ($type != '') {
- $data['type'] = $type;
- }
-
- $url = '';
- preg_match("/url='(.*?)'/ism", $attributes, $matches);
- if (!empty($matches[1])) {
- $url = $matches[1];
- }
-
- preg_match('/url="(.*?)"/ism', $attributes, $matches);
- if (!empty($matches[1])) {
- $url = $matches[1];
- }
-
- if ($url != '') {
- $data['url'] = html_entity_decode($url, ENT_QUOTES, 'UTF-8');
- }
-
- $title = '';
- preg_match("/title='(.*?)'/ism", $attributes, $matches);
- if (!empty($matches[1])) {
- $title = $matches[1];
- }
-
- preg_match('/title="(.*?)"/ism', $attributes, $matches);
- if (!empty($matches[1])) {
- $title = $matches[1];
- }
-
- if ($title != '') {
- $title = self::convert(html_entity_decode($title, ENT_QUOTES, 'UTF-8'), false, true);
- $title = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
- $title = str_replace(['[', ']'], ['[', ']'], $title);
- $data['title'] = $title;
- }
-
- $image = '';
- preg_match("/image='(.*?)'/ism", $attributes, $matches);
- if (!empty($matches[1])) {
- $image = $matches[1];
- }
-
- preg_match('/image="(.*?)"/ism', $attributes, $matches);
- if (!empty($matches[1])) {
- $image = $matches[1];
- }
-
- if ($image != '') {
- $data['image'] = html_entity_decode($image, ENT_QUOTES, 'UTF-8');
- }
-
- $preview = '';
- preg_match("/preview='(.*?)'/ism", $attributes, $matches);
- if (!empty($matches[1])) {
- $preview = $matches[1];
- }
-
- preg_match('/preview="(.*?)"/ism', $attributes, $matches);
- if (!empty($matches[1])) {
- $preview = $matches[1];
- }
-
- if ($preview != '') {
- $data['preview'] = html_entity_decode($preview, ENT_QUOTES, 'UTF-8');
- }
-
- $provider_name = '';
- preg_match("/publisher_name='(.*?)'/ism", $attributes, $matches);
- if (!empty($matches[1])) {
- $provider_name = $matches[1];
- }
-
- preg_match('/publisher_name="(.*?)"/ism', $attributes, $matches);
- if (!empty($matches[1])) {
- $provider_name = $matches[1];
- }
-
- if ($provider_name != '') {
- $data['provider_name'] = html_entity_decode($provider_name, ENT_QUOTES, 'UTF-8');
- }
-
- $provider_url = '';
- preg_match("/publisher_url='(.*?)'/ism", $attributes, $matches);
- if (!empty($matches[1])) {
- $provider_url = $matches[1];
- }
-
- preg_match('/publisher_url="(.*?)"/ism', $attributes, $matches);
- if (!empty($matches[1])) {
- $provider_url = $matches[1];
- }
-
- if ($provider_url != '') {
- $data['provider_url'] = html_entity_decode($provider_url, ENT_QUOTES, 'UTF-8');
- }
-
- $author_name = '';
- preg_match("/author_name='(.*?)'/ism", $attributes, $matches);
- if (!empty($matches[1])) {
- $author_name = $matches[1];
- }
-
- preg_match('/author_name="(.*?)"/ism', $attributes, $matches);
- if (!empty($matches[1])) {
- $author_name = $matches[1];
- }
-
- if (($author_name != '') && ($author_name != $provider_name)) {
- $data['author_name'] = html_entity_decode($author_name, ENT_QUOTES, 'UTF-8');
- }
-
$data['description'] = trim($match[3]);
$data['after'] = trim($match[4]);
if (empty($data['provider_name'])) {
$data['provider_name'] = $parts['host'];
}
- if (empty($data['provider_url'])) {
+ if (empty($data['provider_url']) || empty(parse_url($data['provider_url'], PHP_URL_SCHEME))) {
$data['provider_url'] = $parts['scheme'] . '://' . $parts['host'];
if (!empty($parts['port'])) {
foreach ($pictures as $picture) {
if (Photo::isLocal($picture[1])) {
$post['images'][] = ['url' => str_replace('-1.', '-0.', $picture[1]), 'description' => $picture[2]];
+ } else {
+ $post['remote_images'][] = ['url' => $picture[1], 'description' => $picture[2]];
}
}
if (!empty($post['images']) && !empty($post['images'][0]['description'])) {
foreach ($pictures as $picture) {
if (Photo::isLocal($picture[1])) {
$post['images'][] = ['url' => str_replace('-1.', '-0.', $picture[1]), 'description' => ''];
+ } else {
+ $post['remote_images'][] = ['url' => $picture[1], 'description' => ''];
}
}
}
}
}
} elseif (preg_match_all("(\[img\](.*?)\[\/img\])ism", $body, $pictures, PREG_SET_ORDER)) {
- if ((count($pictures) == 1) && !$has_title) {
- $post['type'] = 'photo';
- $post['image'] = $pictures[0][1];
- $post['text'] = str_replace($pictures[0][0], '', $body);
- } elseif (count($pictures) > 0) {
+ if ($has_title) {
$post['type'] = 'link';
$post['url'] = $plink;
- $post['image'] = $pictures[0][1];
- $post['text'] = $body;
+ } else {
+ $post['type'] = 'photo';
+ }
- foreach ($pictures as $picture) {
- $post['text'] = trim(str_replace($picture[0], '', $post['text']));
- }
+ $post['image'] = $pictures[0][1];
+ $post['text'] = $body;
+ foreach ($pictures as $picture) {
+ $post['text'] = trim(str_replace($picture[0], '', $post['text']));
}
}
$post['type'] = "text";
$post['text'] = trim($body);
}
+
+ if (($post['type'] == 'photo') && empty($post['images']) && !empty($post['remote_images'])) {
+ $post['images'] = $post['remote_images'];
+ $post['image'] = $post['images'][0]['url'];
+ if (!empty($post['images']) && !empty($post['images'][0]['description'])) {
+ $post['image_description'] = $post['images'][0]['description'];
+ }
+ }
+ unset($post['remote_images']);
} elseif (isset($post['url']) && ($post['type'] == 'video')) {
$data = ParseUrl::getSiteinfoCached($post['url']);
*/
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'])) {
$c = preg_match_all('/\[img.*?\](.*?)\[\/img\]/ism', $s, $matches, PREG_SET_ORDER);
if ($c) {
foreach ($matches as $mtch) {
- Logger::log('scale_external_image: ' . $mtch[1]);
+ Logger::info('scale_external_image', ['image' => $mtch[1]]);
$hostname = str_replace('www.', '', substr(DI::baseUrl(), strpos(DI::baseUrl(), '://') + 3));
if (stristr($mtch[1], $hostname)) {
* @param string $text
* @param integer $simplehtml
* @param bool $tryoembed
+ * @param array $data
* @return string
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
- private static function convertAttachment($text, $simplehtml = self::INTERNAL, $tryoembed = true)
+ public static function convertAttachment($text, $simplehtml = self::INTERNAL, $tryoembed = true, array $data = [])
{
- $data = self::getAttachmentData($text);
+ $data = $data ?: self::getAttachmentData($text);
if (empty($data) || empty($data['url'])) {
return $text;
}
+ $stamp1 = microtime(true);
+
if (isset($data['title'])) {
$data['title'] = strip_tags($data['title']);
$data['title'] = str_replace(['http://', 'https://'], '', $data['title']);
}
if (!empty($data['description']) && $data['description'] != $data['title']) {
- // Sanitize the HTML by converting it to BBCode
- $bbcode = HTML::toBBCode($data['description']);
- $return .= sprintf('<blockquote>%s</blockquote>', trim(self::convert($bbcode)));
+ // Sanitize the HTML
+ $return .= sprintf('<blockquote>%s</blockquote>', trim(HTML::purify($data['description'])));
}
if (!empty($data['provider_url']) && !empty($data['provider_name'])) {
}
}
+ DI::profiler()->saveTimestamp($stamp1, 'rendering');
return trim(($data['text'] ?? '') . ' ' . $return . ' ' . ($data['after'] ?? ''));
}
return $newbody;
}
+ /**
+ *
+ * @param string $text A BBCode string
+ * @return array share attributes
+ */
+ public static function fetchShareAttributes($text)
+ {
+ $attributes = [];
+ if (!preg_match("/(.*?)\[share(.*?)\](.*)\[\/share\]/ism", $text, $matches)) {
+ return $attributes;
+ }
+
+ $attribute_string = $matches[2];
+ 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');
+ }
+ return $attributes;
+ }
+
/**
* This function converts a [share] block to text according to a provided callback function whose signature is:
*
'$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),
+ '$content' => self::TOP_ANCHOR . self::setMentions(trim($content), 0, $network) . self::BOTTOM_ANCHOR,
]);
break;
}
$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::httpRequest()->head($match[1], ['timeout' => DI::config()->get('system', 'xrd_timeout')]);
+ if ($curlResult->isSuccess()) {
+ $mimetype = $curlResult->getHeader('Content-Type');
+ } 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]);
+ if (empty($body)) {
+ DI::cache()->set($cache_key, $text);
+ return $text;
+ }
$doc = new DOMDocument();
@$doc->loadHTML($body);
private static function cleanPictureLinksCallback($match)
{
- $a = DI::app();
-
// 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/');
if (preg_match('|' . $own_photo_url . '.*?/image/|', Strings::normaliseLink($match[1]))) {
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::httpRequest()->head($match[1], ['timeout' => DI::config()->get('system', 'xrd_timeout')]);
+ if ($curlResult->isSuccess()) {
+ $mimetype = $curlResult->getHeader('Content-Type');
+ } 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])) {
// if its not a picture then look if its a page that contains a picture link
$body = DI::httpRequest()->fetch($match[1]);
+ if (empty($body)) {
+ DI::cache()->set($cache_key, $text);
+ return $text;
+ }
$doc = new DOMDocument();
@$doc->loadHTML($body);
$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] "];
$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]"];
do {
$oldtext = $text;
// Handle attached links or videos
if ($simple_html == 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 = 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);
});
}
$text);
} elseif (!$simple_html) {
$text = preg_replace("/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism",
- '$1<a href="$2" class="userinfo mention" title="$3">$3</a>',
+ '$1<a href="$2" class="userinfo mention" title="$3"><bdi>$3</bdi></a>',
$text);
}
$text = HTML::purify($text, $allowedIframeDomains);
- return $text;
+ return trim($text);
}
/**
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.
*