]> git.mxchange.org Git - friendica.git/blobdiff - src/Content/Text/BBCode.php
API: We now can post statuses via API
[friendica.git] / src / Content / Text / BBCode.php
index 6c46163f729006c1ad943e8ec563f3410f00e592..32cd818cac690c29364802f70bc7df59dc9008d7 100644 (file)
@@ -50,9 +50,10 @@ use Friendica\Util\XML;
 class BBCode
 {
        // Update this value to the current date whenever changes are made to BBCode::convert
-       const VERSION = '2021-03-21';
+       const VERSION = '2021-05-01';
 
        const INTERNAL = 0;
+       const EXTERNAL = 1;
        const API = 2;
        const DIASPORA = 3;
        const CONNECTORS = 4;
@@ -61,6 +62,8 @@ class BBCode
        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
         *
@@ -155,6 +158,7 @@ class BBCode
                        'image'         => null,
                        'url'           => '',
                        'author_name'   => '',
+                       'author_url'    => '',
                        'provider_name' => '',
                        'provider_url'  => '',
                        'title'         => '',
@@ -169,137 +173,46 @@ class BBCode
 
                $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(['[', ']'], ['&#91;', '&#93;'], $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(['[', ']'], ['&#91;', '&#93;'], $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]);
@@ -309,7 +222,7 @@ class BBCode
                        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'])) {
@@ -342,6 +255,8 @@ class BBCode
                        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'])) {
@@ -353,6 +268,8 @@ class BBCode
                        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' => ''];
                                }
                        }
                }
@@ -413,19 +330,17 @@ class BBCode
                                        }
                                }
                        } 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']));
                                }
                        }
 
@@ -465,6 +380,15 @@ class BBCode
                                $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']);
 
@@ -486,7 +410,7 @@ 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'])) {
@@ -543,7 +467,7 @@ class BBCode
                $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)) {
@@ -683,16 +607,19 @@ class BBCode
         * @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']);
@@ -733,9 +660,8 @@ class BBCode
                        }
 
                        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'])) {
@@ -751,6 +677,7 @@ class BBCode
                        }
                }
 
+               DI::profiler()->saveTimestamp($stamp1, 'rendering');
                return trim(($data['text'] ?? '') . ' ' . $return . ' ' . ($data['after'] ?? ''));
        }
 
@@ -1019,6 +946,26 @@ class BBCode
                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:
         *
@@ -1139,12 +1086,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),
+                                       '$content'      => self::TOP_ANCHOR . self::setMentions(trim($content), 0, $network) . self::BOTTOM_ANCHOR,
                                ]);
                                break;
                }
@@ -1158,26 +1105,24 @@ 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::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);
@@ -1214,8 +1159,6 @@ class BBCode
 
        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]))) {
@@ -1233,20 +1176,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::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])) {
@@ -1257,6 +1195,10 @@ class BBCode
 
                        // 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);
@@ -1404,10 +1346,14 @@ class BBCode
                                $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;
@@ -1441,7 +1387,7 @@ class BBCode
                                // 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);
@@ -1451,9 +1397,9 @@ class BBCode
                                $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);
                                        });
                                }
 
@@ -1669,7 +1615,7 @@ class BBCode
                                if ($try_oembed) {
                                        // html5 video and audio
                                        $text = preg_replace("/\[video\](.*?\.(ogg|ogv|oga|ogm|webm|mp4).*?)\[\/video\]/ism",
-                                               '<video src="$1" controls width="' . $a->videowidth . '" height="' . $a->videoheight . '" loop="true"><a href="$1">$1</a></video>', $text);
+                                               '<video src="$1" controls width="100%" height="auto"><a href="$1">$1</a></video>', $text);
 
                                        $text = preg_replace_callback("/\[video\](.*?)\[\/video\]/ism", $try_oembed_callback, $text);
                                        $text = preg_replace_callback("/\[audio\](.*?)\[\/audio\]/ism", $try_oembed_callback, $text);
@@ -1775,7 +1721,7 @@ class BBCode
                                                $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);
                                }
 
@@ -1947,7 +1893,7 @@ class BBCode
 
                $text = HTML::purify($text, $allowedIframeDomains);
 
-               return $text;
+               return trim($text);
        }
 
        /**
@@ -2169,6 +2115,32 @@ class BBCode
                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.
         *