class BBCode
{
// Update this value to the current date whenever changes are made to BBCode::convert
- const VERSION = '2021-04-24';
+ const VERSION = '2021-05-21';
const INTERNAL = 0;
+ const EXTERNAL = 1;
const API = 2;
const DIASPORA = 3;
const CONNECTORS = 4;
const BACKLINK = 8;
const ACTIVITYPUB = 9;
- const ANCHOR = '<br class="anchor">';
+ const TOP_ANCHOR = '<br class="top-anchor">';
+ const BOTTOM_ANCHOR = '<br class="button-anchor">';
/**
* Fetches attachment data that were generated the old way
*
*/
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'])) {
*/
public static function toPlaintext($text, $keep_urls = true)
{
+ // Remove pictures in advance to avoid unneeded proxy calls
+ $text = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", ' $2 ', $text);
+ $text = preg_replace("/\[img.*?\[\/img\]/ism", ' ', $text);
+
$naked_text = HTML::toPlaintext(self::convert($text, false, 0, true), 0, !$keep_urls);
return $naked_text;
* @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'] ?? ''));
}
*/
public static function fetchShareAttributes($text)
{
+ // 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)) {
return $attributes;
$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']);
$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'] = ProxyUtils::proxifyUrl($attributes['avatar'], ProxyUtils::SIZE_THUMB);
}
$content = preg_replace(Strings::autoLinkRegEx(), '<a href="$1">$1</a>', $match[3]);
switch ($simplehtml) {
case self::API:
- $text = ($is_quote_share? '<br>' : '') . '<p>' . html_entity_decode('♲ ', ENT_QUOTES, 'UTF-8') . ' ' . $author_contact['addr'] . ': </p>' . "\n" . $content;
+ $text = ($is_quote_share? '<br>' : '') .
+ '<p><b><a href="' . $attributes['link'] . '">' . html_entity_decode('♲ ', ENT_QUOTES, 'UTF-8') . ' ' . $author_contact['addr'] . "</a>:</b> </p>\n" .
+ '<blockquote class="shared_content">' . $content . '</blockquote>';
break;
case self::DIASPORA:
if (stripos(Strings::normaliseLink($attributes['link']), 'http://twitter.com/') === 0) {
'$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;
}
$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]";
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])) {
// 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>',
+ '<bdi>$1<a href="$2" class="userinfo mention" title="$3">$3</a></bdi>',
$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.
*
}
}
- $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;
}
}