]> git.mxchange.org Git - friendica.git/blobdiff - src/Content/Text/BBCode.php
Merge pull request #6580 from MrPetovan/bug/6575-escaped-field-input-attributes
[friendica.git] / src / Content / Text / BBCode.php
index ebaef2b29d1accf43625b1e56c9cdcafc7fb61e1..a8b5ec2025546e7ffa0e694597d2f38524ccd8b1 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 /**
  * @file src/Content/Text/BBCode.php
  */
@@ -12,12 +11,13 @@ use Exception;
 use Friendica\BaseObject;
 use Friendica\Content\OEmbed;
 use Friendica\Content\Smilies;
-use Friendica\Core\Addon;
 use Friendica\Core\Cache;
 use Friendica\Core\Config;
+use Friendica\Core\Hook;
 use Friendica\Core\L10n;
-use Friendica\Core\PConfig;
+use Friendica\Core\Logger;
 use Friendica\Core\Protocol;
+use Friendica\Core\Renderer;
 use Friendica\Core\System;
 use Friendica\Model\Contact;
 use Friendica\Model\Event;
@@ -26,9 +26,9 @@ use Friendica\Object\Image;
 use Friendica\Util\Map;
 use Friendica\Util\Network;
 use Friendica\Util\ParseUrl;
-use League\HTMLToMarkdown\HtmlConverter;
-
-require_once "mod/proxy.php";
+use Friendica\Util\Proxy as ProxyUtils;
+use Friendica\Util\Strings;
+use Friendica\Util\XML;
 
 class BBCode extends BaseObject
 {
@@ -37,13 +37,14 @@ class BBCode extends BaseObject
         *
         * @param string $body Message body
         * @return array
-        * 'type' -> Message type ("link", "video", "photo")
-        * 'text' -> Text before the shared message
-        * 'after' -> Text after the shared message
-        * 'image' -> Preview image of the message
-        * 'url' -> Url to the attached message
-        * 'title' -> Title of the attachment
-        * 'description' -> Description of the attachment
+        *                     'type' -> Message type ("link", "video", "photo")
+        *                     'text' -> Text before the shared message
+        *                     'after' -> Text after the shared message
+        *                     'image' -> Preview image of the message
+        *                     'url' -> Url to the attached message
+        *                     'title' -> Title of the attachment
+        *                     'description' -> Description of the attachment
+        * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         */
        private static function getOldAttachmentData($body)
        {
@@ -76,10 +77,12 @@ class BBCode extends BaseObject
 
                                        $picturedata = Image::getInfoFromURL($matches[1]);
 
-                                       if (($picturedata[0] >= 500) && ($picturedata[0] >= $picturedata[1])) {
-                                               $post["image"] = $matches[1];
-                                       } else {
-                                               $post["preview"] = $matches[1];
+                                       if ($picturedata) {
+                                               if (($picturedata[0] >= 500) && ($picturedata[0] >= $picturedata[1])) {
+                                                       $post["image"] = $matches[1];
+                                               } else {
+                                                       $post["preview"] = $matches[1];
+                                               }
                                        }
                                }
 
@@ -87,7 +90,7 @@ class BBCode extends BaseObject
                                        $post["url"] = $matches[1];
                                        $post["title"] = $matches[2];
                                }
-                               if (($post["url"] == "") && (in_array($post["type"], ["link", "video"]))
+                               if (!empty($post["url"]) && (in_array($post["type"], ["link", "video"]))
                                        && preg_match("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", $attacheddata, $matches)) {
                                        $post["url"] = $matches[1];
                                }
@@ -106,13 +109,14 @@ class BBCode extends BaseObject
         *
         * @param string $body Message body
         * @return array
-        * 'type' -> Message type ("link", "video", "photo")
-        * 'text' -> Text before the shared message
-        * 'after' -> Text after the shared message
-        * 'image' -> Preview image of the message
-        * 'url' -> Url to the attached message
-        * 'title' -> Title of the attachment
-        * 'description' -> Description of the attachment
+        *                     'type' -> Message type ("link", "video", "photo")
+        *                     'text' -> Text before the shared message
+        *                     'after' -> Text after the shared message
+        *                     'image' -> Preview image of the message
+        *                     'url' -> Url to the attached message
+        *                     'title' -> Title of the attachment
+        *                     'description' -> Description of the attachment
+        * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         */
        public static function getAttachmentData($body)
        {
@@ -128,12 +132,12 @@ class BBCode extends BaseObject
 
                $type = "";
                preg_match("/type='(.*?)'/ism", $attributes, $matches);
-               if (x($matches, 1)) {
+               if (!empty($matches[1])) {
                        $type = strtolower($matches[1]);
                }
 
                preg_match('/type="(.*?)"/ism', $attributes, $matches);
-               if (x($matches, 1)) {
+               if (!empty($matches[1])) {
                        $type = strtolower($matches[1]);
                }
 
@@ -151,12 +155,12 @@ class BBCode extends BaseObject
 
                $url = "";
                preg_match("/url='(.*?)'/ism", $attributes, $matches);
-               if (x($matches, 1)) {
+               if (!empty($matches[1])) {
                        $url = $matches[1];
                }
 
                preg_match('/url="(.*?)"/ism', $attributes, $matches);
-               if (x($matches, 1)) {
+               if (!empty($matches[1])) {
                        $url = $matches[1];
                }
 
@@ -166,12 +170,12 @@ class BBCode extends BaseObject
 
                $title = "";
                preg_match("/title='(.*?)'/ism", $attributes, $matches);
-               if (x($matches, 1)) {
+               if (!empty($matches[1])) {
                        $title = $matches[1];
                }
 
                preg_match('/title="(.*?)"/ism', $attributes, $matches);
-               if (x($matches, 1)) {
+               if (!empty($matches[1])) {
                        $title = $matches[1];
                }
 
@@ -184,12 +188,12 @@ class BBCode extends BaseObject
 
                $image = "";
                preg_match("/image='(.*?)'/ism", $attributes, $matches);
-               if (x($matches, 1)) {
+               if (!empty($matches[1])) {
                        $image = $matches[1];
                }
 
                preg_match('/image="(.*?)"/ism', $attributes, $matches);
-               if (x($matches, 1)) {
+               if (!empty($matches[1])) {
                        $image = $matches[1];
                }
 
@@ -199,12 +203,12 @@ class BBCode extends BaseObject
 
                $preview = "";
                preg_match("/preview='(.*?)'/ism", $attributes, $matches);
-               if (x($matches, 1)) {
+               if (!empty($matches[1])) {
                        $preview = $matches[1];
                }
 
                preg_match('/preview="(.*?)"/ism', $attributes, $matches);
-               if (x($matches, 1)) {
+               if (!empty($matches[1])) {
                        $preview = $matches[1];
                }
 
@@ -232,7 +236,7 @@ class BBCode extends BaseObject
                */
 
                $has_title = !empty($item['title']);
-               $plink = (!empty($item['plink']) ? $item['plink'] : '');
+               $plink = defaults($item, 'plink', '');
                $post = self::getAttachmentData($body);
 
                // if nothing is found, it maybe having an image.
@@ -241,6 +245,9 @@ class BBCode extends BaseObject
                        $body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body);
 
                        $URLSearchString = "^\[\]";
+
+                       $body = preg_replace("/\[img\=([$URLSearchString]*)\](.*?)\[\/img\]/ism", '[img]$1[/img]', $body);
+
                        if (preg_match_all("(\[url=([$URLSearchString]*)\]\s*\[img\]([$URLSearchString]*)\[\/img\]\s*\[\/url\])ism", $body, $pictures, PREG_SET_ORDER)) {
                                if ((count($pictures) == 1) && !$has_title) {
                                        // Checking, if the link goes to a picture
@@ -266,7 +273,7 @@ class BBCode extends BaseObject
                                                $post["text"] = str_replace($pictures[0][0], "", $body);
                                        } else {
                                                $imgdata = Image::getInfoFromURL($pictures[0][1]);
-                                               if (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];
@@ -338,159 +345,31 @@ class BBCode extends BaseObject
        }
 
        /**
-        * @brief Convert a message into plaintext for connectors to other networks
+        * @brief Converts a BBCode text into plaintext
         *
-        * @param array $b The message array that is about to be posted
-        * @param int $limit The maximum number of characters when posting to that network
-        * @param bool $includedlinks Has an attached link to be included into the message?
-        * @param int $htmlmode This triggers the behaviour of the bbcode conversion
-        * @param string $target_network Name of the network where the post should go to.
+        * @param      $text
+        * @param bool $keep_urls Whether to keep URLs in the resulting plaintext
         *
-        * @return string The converted message
+        * @return string
         */
-       public static function toPlaintext($b, $limit = 0, $includedlinks = false, $htmlmode = 2, $target_network = "")
+       public static function toPlaintext($text, $keep_urls = true)
        {
-               // Remove the hash tags
-               $URLSearchString = "^\[\]";
-               $body = preg_replace("/([#@])\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '$1$3', $b["body"]);
-
-               // Add an URL element if the text contains a raw link
-               $body = preg_replace("/([^\]\='".'"'."]|^)(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", '$1[url]$2[/url]', $body);
-
-               // Remove the abstract
-               $body = self::stripAbstract($body);
-
-               // At first look at data that is attached via "type-..." stuff
-               // This will hopefully replaced with a dedicated bbcode later
-               //$post = self::getAttachedData($b["body"]);
-               $post = self::getAttachedData($body, $b);
-
-               if (($b["title"] != "") && ($post["text"] != "")) {
-                       $post["text"] = trim($b["title"]."\n\n".$post["text"]);
-               } elseif ($b["title"] != "") {
-                       $post["text"] = trim($b["title"]);
+               $naked_text = preg_replace('/\[(.+?)\]\s*/','', $text);
+               if (!$keep_urls) {
+                       $naked_text = preg_replace('#https?\://[^\s<]+[^\s\.\)]#i', '', $naked_text);
                }
 
-               $abstract = "";
-
-               // Fetch the abstract from the given target network
-               if ($target_network != "") {
-                       $default_abstract = self::getAbstract($b["body"]);
-                       $abstract = self::getAbstract($b["body"], $target_network);
-
-                       // If we post to a network with no limit we only fetch
-                       // an abstract exactly for this network
-                       if (($limit == 0) && ($abstract == $default_abstract)) {
-                               $abstract = "";
-                       }
-               } else {// Try to guess the correct target network
-                       switch ($htmlmode) {
-                               case 8:
-                                       $abstract = self::getAbstract($b["body"], NETWORK_TWITTER);
-                                       break;
-                               case 7:
-                                       $abstract = self::getAbstract($b["body"], NETWORK_STATUSNET);
-                                       break;
-                               case 6:
-                                       $abstract = self::getAbstract($b["body"], NETWORK_APPNET);
-                                       break;
-                               default: // We don't know the exact target.
-                                       // We fetch an abstract since there is a posting limit.
-                                       if ($limit > 0) {
-                                               $abstract = self::getAbstract($b["body"]);
-                                       }
-                       }
-               }
-
-               if ($abstract != "") {
-                       $post["text"] = $abstract;
-
-                       if ($post["type"] == "text") {
-                               $post["type"] = "link";
-                               $post["url"] = $b["plink"];
-                       }
-               }
-
-               $html = self::convert($post["text"].$post["after"], false, $htmlmode);
-               $msg = HTML::toPlaintext($html, 0, true);
-               $msg = trim(html_entity_decode($msg, ENT_QUOTES, 'UTF-8'));
-
-               $link = "";
-               if ($includedlinks) {
-                       if ($post["type"] == "link") {
-                               $link = $post["url"];
-                       } elseif ($post["type"] == "text") {
-                               $link = $post["url"];
-                       } elseif ($post["type"] == "video") {
-                               $link = $post["url"];
-                       } elseif ($post["type"] == "photo") {
-                               $link = $post["image"];
-                       }
-
-                       if (($msg == "") && isset($post["title"])) {
-                               $msg = trim($post["title"]);
-                       }
-
-                       if (($msg == "") && isset($post["description"])) {
-                               $msg = trim($post["description"]);
-                       }
-
-                       // If the link is already contained in the post, then it neeedn't to be added again
-                       // But: if the link is beyond the limit, then it has to be added.
-                       if (($link != "") && strstr($msg, $link)) {
-                               $pos = strpos($msg, $link);
-
-                               // Will the text be shortened in the link?
-                               // Or is the link the last item in the post?
-                               if (($limit > 0) && ($pos < $limit) && (($pos + 23 > $limit) || ($pos + strlen($link) == strlen($msg)))) {
-                                       $msg = trim(str_replace($link, "", $msg));
-                               } elseif (($limit == 0) || ($pos < $limit)) {
-                                       // The limit has to be increased since it will be shortened - but not now
-                                       // Only do it with Twitter (htmlmode = 8)
-                                       if (($limit > 0) && (strlen($link) > 23) && ($htmlmode == 8)) {
-                                               $limit = $limit - 23 + strlen($link);
-                                       }
-
-                                       $link = "";
-
-                                       if ($post["type"] == "text") {
-                                               unset($post["url"]);
-                                       }
-                               }
-                       }
-               }
-
-               if ($limit > 0) {
-                       // Reduce multiple spaces
-                       // When posted to a network with limited space, we try to gain space where possible
-                       while (strpos($msg, "  ") !== false) {
-                               $msg = str_replace("  ", " ", $msg);
-                       }
-
-                       // Twitter is using its own limiter, so we always assume that shortened links will have this length
-                       if (iconv_strlen($link, "UTF-8") > 0) {
-                               $limit = $limit - 23;
-                       }
+               return $naked_text;
+       }
 
-                       if (iconv_strlen($msg, "UTF-8") > $limit) {
-                               if (($post["type"] == "text") && isset($post["url"])) {
-                                       $post["url"] = $b["plink"];
-                               } elseif (!isset($post["url"])) {
-                                       $limit = $limit - 23;
-                                       $post["url"] = $b["plink"];
-                               // Which purpose has this line? It is now uncommented, but left as a reminder
-                               //} elseif (strpos($b["body"], "[share") !== false) {
-                               //      $post["url"] = $b["plink"];
-                               } elseif (PConfig::get($b["uid"], "system", "no_intelligent_shortening")) {
-                                       $post["url"] = $b["plink"];
-                               }
-                               $msg = Plaintext::shorten($msg, $limit);
-                       }
+       private static function proxyUrl($image, $simplehtml = false)
+       {
+               // Only send proxied pictures to API and for internal display
+               if (in_array($simplehtml, [false, 2])) {
+                       return ProxyUtils::proxifyUrl($image);
+               } else {
+                       return $image;
                }
-
-               $post["text"] = trim($msg);
-
-               return($post);
        }
 
        public static function scaleExternalImages($srctext, $include_link = true, $scale_replace = false)
@@ -507,7 +386,7 @@ class BBCode extends BaseObject
                $c = preg_match_all('/\[img.*?\](.*?)\[\/img\]/ism', $s, $matches, PREG_SET_ORDER);
                if ($c) {
                        foreach ($matches as $mtch) {
-                               logger('scale_external_image: ' . $mtch[1]);
+                               Logger::log('scale_external_image: ' . $mtch[1]);
 
                                $hostname = str_replace('www.', '', substr(System::baseUrl(), strpos(System::baseUrl(), '://') + 3));
                                if (stristr($mtch[1], $hostname)) {
@@ -542,7 +421,7 @@ class BBCode extends BaseObject
                                                        $Image->scaleDown(640);
                                                        $new_width = $Image->getWidth();
                                                        $new_height = $Image->getHeight();
-                                                       logger('scale_external_images: ' . $orig_width . '->' . $new_width . 'w ' . $orig_height . '->' . $new_height . 'h' . ' match: ' . $mtch[0], LOGGER_DEBUG);
+                                                       Logger::log('scale_external_images: ' . $orig_width . '->' . $new_width . 'w ' . $orig_height . '->' . $new_height . 'h' . ' match: ' . $mtch[0], Logger::DEBUG);
                                                        $s = str_replace(
                                                                $mtch[0],
                                                                '[img=' . $new_width . 'x' . $new_height. ']' . $scaled . '[/img]'
@@ -551,7 +430,7 @@ class BBCode extends BaseObject
                                                                        : ''),
                                                                $s
                                                        );
-                                                       logger('scale_external_images: new string: ' . $s, LOGGER_DEBUG);
+                                                       Logger::log('scale_external_images: new string: ' . $s, Logger::DEBUG);
                                                }
                                        }
                                }
@@ -570,16 +449,17 @@ class BBCode extends BaseObject
         * @brief Truncates imported message body string length to max_import_size
         * @param string $body
         * @return string
+        * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         */
        public static function limitBodySize($body)
        {
-               $maxlen = get_max_import_size();
+               $maxlen = Config::get('config', 'max_import_size', 0);
 
                // If the length of the body, including the embedded images, is smaller
                // than the maximum, then don't waste time looking for the images
                if ($maxlen && (strlen($body) > $maxlen)) {
 
-                       logger('the total body length exceeds the limit', LOGGER_DEBUG);
+                       Logger::log('the total body length exceeds the limit', Logger::DEBUG);
 
                        $orig_body = $body;
                        $new_body = '';
@@ -599,7 +479,7 @@ class BBCode extends BaseObject
 
                                        if (($textlen + $img_start) > $maxlen) {
                                                if ($textlen < $maxlen) {
-                                                       logger('the limit happens before an embedded image', LOGGER_DEBUG);
+                                                       Logger::log('the limit happens before an embedded image', Logger::DEBUG);
                                                        $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
                                                        $textlen = $maxlen;
                                                }
@@ -613,7 +493,7 @@ class BBCode extends BaseObject
 
                                        if (($textlen + $img_end) > $maxlen) {
                                                if ($textlen < $maxlen) {
-                                                       logger('the limit happens before the end of a non-embedded image', LOGGER_DEBUG);
+                                                       Logger::log('the limit happens before the end of a non-embedded image', Logger::DEBUG);
                                                        $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
                                                        $textlen = $maxlen;
                                                }
@@ -636,11 +516,11 @@ class BBCode extends BaseObject
 
                        if (($textlen + strlen($orig_body)) > $maxlen) {
                                if ($textlen < $maxlen) {
-                                       logger('the limit happens after the end of the last image', LOGGER_DEBUG);
+                                       Logger::log('the limit happens after the end of the last image', Logger::DEBUG);
                                        $new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
                                }
                        } else {
-                               logger('the text size with embedded images extracted did not violate the limit', LOGGER_DEBUG);
+                               Logger::log('the text size with embedded images extracted did not violate the limit', Logger::DEBUG);
                                $new_body = $new_body . $orig_body;
                        }
 
@@ -656,24 +536,27 @@ class BBCode extends BaseObject
         * Note: Can produce a [bookmark] tag in the returned string
         *
         * @brief Processes [attachment] tags
-        * @param string $return
+        * @param string   $return
         * @param bool|int $simplehtml
-        * @param bool $tryoembed
+        * @param bool     $tryoembed
         * @return string
+        * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         */
        private static function convertAttachment($return, $simplehtml = false, $tryoembed = true)
        {
                $data = self::getAttachmentData($return);
-               if (!$data) {
+               if (empty($data) || empty($data["url"])) {
                        return $return;
                }
 
                if (isset($data["title"])) {
                        $data["title"] = strip_tags($data["title"]);
                        $data["title"] = str_replace(["http://", "https://"], "", $data["title"]);
+               } else {
+                       $data["title"] = null;
                }
 
-               if (((strpos($data["text"], "[img=") !== false) || (strpos($data["text"], "[img]") !== false) || Config::get('system', 'always_show_preview')) && ($data["image"] != "")) {
+               if (((strpos($data["text"], "[img=") !== false) || (strpos($data["text"], "[img]") !== false) || Config::get('system', 'always_show_preview')) && !empty($data["image"])) {
                        $data["preview"] = $data["image"];
                        $data["image"] = "";
                }
@@ -691,28 +574,32 @@ class BBCode extends BaseObject
                                        throw new Exception('OEmbed is disabled for this attachment.');
                                }
                        } catch (Exception $e) {
+                               $data["title"] = defaults($data, 'title', $data['url']);
+
                                if ($simplehtml != 4) {
                                        $return = sprintf('<div class="type-%s">', $data["type"]);
                                }
 
-                               if ($data["image"] != "") {
-                                       $return .= sprintf('<a href="%s" target="_blank"><img src="%s" alt="" title="%s" class="attachment-image" /></a><br />', $data["url"], proxy_url($data["image"]), $data["title"]);
-                               } elseif ($data["preview"] != "") {
-                                       $return .= sprintf('<a href="%s" target="_blank"><img src="%s" alt="" title="%s" class="attachment-preview" /></a><br />', $data["url"], proxy_url($data["preview"]), $data["title"]);
-                               }
-
-                               if (($data["type"] == "photo") && ($data["url"] != "") && ($data["image"] != "")) {
-                                       $return .= sprintf('<a href="%s" target="_blank"><img src="%s" alt="" title="%s" class="attachment-image" /></a>', $data["url"], proxy_url($data["image"]), $data["title"]);
-                               } else {
-                                       $return .= sprintf('<h4><a href="%s">%s</a></h4>', $data['url'], $data['title']);
+                               if (!empty($data['title']) && !empty($data['url'])) {
+                                       if (!empty($data["image"]) && empty($data["text"]) && ($data["type"] == "photo")) {
+                                               $return .= sprintf('<a href="%s" target="_blank"><img src="%s" alt="" title="%s" class="attachment-image" /></a>', $data["url"], self::proxyUrl($data["image"], $simplehtml), $data["title"]);
+                                       } else {
+                                               if (!empty($data["image"])) {
+                                                       $return .= sprintf('<a href="%s" target="_blank"><img src="%s" alt="" title="%s" class="attachment-image" /></a><br />', $data["url"], self::proxyUrl($data["image"], $simplehtml), $data["title"]);
+                                               } elseif (!empty($data["preview"])) {
+                                                       $return .= sprintf('<a href="%s" target="_blank"><img src="%s" alt="" title="%s" class="attachment-preview" /></a><br />', $data["url"], self::proxyUrl($data["preview"], $simplehtml), $data["title"]);
+                                               }
+                                               $return .= sprintf('<h4><a href="%s">%s</a></h4>', $data['url'], $data['title']);
+                                       }
                                }
 
-                               if ($data["description"] != "" && $data["description"] != $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)));
                                }
-                               if ($data["type"] == "link") {
+
+                               if (!empty($data['url'])) {
                                        $return .= sprintf('<sup><a href="%s">%s</a></sup>', $data['url'], parse_url($data['url'], PHP_URL_HOST));
                                }
 
@@ -722,7 +609,7 @@ class BBCode extends BaseObject
                        }
                }
 
-               return trim($data["text"] . ' ' . $return . ' ' . $data["after"]);
+               return trim(defaults($data, 'text', '') . ' ' . $return . ' ' . defaults($data, 'after', ''));
        }
 
        public static function removeShareInformation($Text, $plaintext = false, $nolink = false)
@@ -732,10 +619,10 @@ class BBCode extends BaseObject
                if (!$data) {
                        return $Text;
                } elseif ($nolink) {
-                       return $data["text"] . $data["after"];
+                       return $data["text"] . defaults($data, 'after', '');
                }
 
-               $title = htmlentities($data["title"], ENT_QUOTES, 'UTF-8', false);
+               $title = htmlentities(defaults($data, 'title', ''), ENT_QUOTES, 'UTF-8', false);
                $text = htmlentities($data["text"], ENT_QUOTES, 'UTF-8', false);
                if ($plaintext || (($title != "") && strstr($text, $title))) {
                        $data["title"] = $data["url"];
@@ -744,20 +631,20 @@ class BBCode extends BaseObject
                        $data["title"] = $data["url"];
                }
 
-               if (($data["text"] == "") && ($data["title"] != "") && ($data["url"] == "")) {
+               if (empty($data["text"]) && !empty($data["title"]) && empty($data["url"])) {
                        return $data["title"] . $data["after"];
                }
 
                // If the link already is included in the post, don't add it again
-               if (($data["url"] != "") && strpos($data["text"], $data["url"])) {
+               if (!empty($data["url"]) && strpos($data["text"], $data["url"])) {
                        return $data["text"] . $data["after"];
                }
 
                $text = $data["text"];
 
-               if (($data["url"] != "") && ($data["title"] != "")) {
+               if (!empty($data["url"]) && !empty($data["title"])) {
                        $text .= "\n[url=" . $data["url"] . "]" . $data["title"] . "[/url]";
-               } elseif (($data["url"] != "")) {
+               } elseif (!empty($data["url"])) {
                        $text .= "\n[url]" . $data["url"] . "[/url]";
                }
 
@@ -895,10 +782,10 @@ class BBCode extends BaseObject
        /**
         * Performs a preg_replace within the boundaries of all named BBCode tags in a text
         *
-        * @param type $pattern Preg pattern string
-        * @param type $replace Preg replace string
-        * @param type $name    BBCode tag name
-        * @param type $text    Text to search
+        * @param string $pattern Preg pattern string
+        * @param string $replace Preg replace string
+        * @param string $name    BBCode tag name
+        * @param string $text    Text to search
         * @return string
         */
        public static function pregReplaceInTag($pattern, $replace, $name, $text)
@@ -973,7 +860,7 @@ class BBCode extends BaseObject
                        // it loops over the array starting from the first element and going sequentially
                        // to the last element
                        $newbody = str_replace('[$#saved_image' . $cnt . '#$]',
-                               '<img src="' . proxy_url($image) . '" alt="' . L10n::t('Image/photo') . '" />', $newbody);
+                               '<img src="' . self::proxyUrl($image) . '" alt="' . L10n::t('Image/photo') . '" />', $newbody);
                        $cnt++;
                }
 
@@ -981,190 +868,141 @@ class BBCode extends BaseObject
        }
 
        /**
-        * Processes [share] tags
+        * This function converts a [share] block to text according to a provided callback function whose signature is:
         *
-        * Note: Can produce a [bookmark] tag in the output
+        * function(array $attributes, array $author_contact, string $content, boolean $is_quote_share): string
         *
-        * @brief Processes [share] tags
-        * @param array    $share      preg_match_callback result array
-        * @param bool|int $simplehtml
-        * @return string
+        * Where:
+        * - $attributes is an array of attributes of the [share] block itself. Missing keys will be completed by the contact
+        * data lookup
+        * - $author_contact is a contact record array
+        * - $content is the inner content of the [share] block
+        * - $is_quote_share indicates whether there's any content before the [share] block
+        * - Return value is the string that should replace the [share] block in the provided text
+        *
+        * This function is intended to be used by addon connector to format a share block like the target network is expecting it.
+        *
+        * @param  string   $text     A BBCode string
+        * @param  callable $callback
+        * @return string The BBCode string with all [share] blocks replaced
         */
-       private static function convertShare($share, $simplehtml)
+       public static function convertShare($text, callable $callback)
        {
-               $attributes = $share[2];
-
-               $author = "";
-               preg_match("/author='(.*?)'/ism", $attributes, $matches);
-               if (x($matches, 1)) {
-                       $author = html_entity_decode($matches[1], ENT_QUOTES, 'UTF-8');
-               }
-
-               preg_match('/author="(.*?)"/ism', $attributes, $matches);
-               if (x($matches, 1)) {
-                       $author = $matches[1];
-               }
-
-               $profile = "";
-               preg_match("/profile='(.*?)'/ism", $attributes, $matches);
-               if (x($matches, 1)) {
-                       $profile = $matches[1];
-               }
-
-               preg_match('/profile="(.*?)"/ism', $attributes, $matches);
-               if (x($matches, 1)) {
-                       $profile = $matches[1];
-               }
-
-               $avatar = "";
-               preg_match("/avatar='(.*?)'/ism", $attributes, $matches);
-               if (x($matches, 1)) {
-                       $avatar = $matches[1];
-               }
-
-               preg_match('/avatar="(.*?)"/ism', $attributes, $matches);
-               if (x($matches, 1)) {
-                       $avatar = $matches[1];
-               }
-
-               $link = "";
-               preg_match("/link='(.*?)'/ism", $attributes, $matches);
-               if (x($matches, 1)) {
-                       $link = $matches[1];
-               }
-
-               preg_match('/link="(.*?)"/ism', $attributes, $matches);
-               if (x($matches, 1)) {
-                       $link = $matches[1];
-               }
-
-               $posted = "";
-
-               preg_match("/posted='(.*?)'/ism", $attributes, $matches);
-               if (x($matches, 1)) {
-                       $posted = $matches[1];
-               }
-
-               preg_match('/posted="(.*?)"/ism', $attributes, $matches);
-               if (x($matches, 1)) {
-                       $posted = $matches[1];
-               }
+               $return = preg_replace_callback(
+                       "/(.*?)\[share(.*?)\](.*?)\[\/share\]/ism",
+                       function ($match) use ($callback) {
+                               $attribute_string = $match[2];
+
+                               $attributes = [];
+                               foreach(['author', 'profile', 'avatar', 'link', 'posted'] as $field) {
+                                       preg_match("/$field=(['\"])(.+?)\\1/ism", $attribute_string, $matches);
+                                       $attributes[$field] = html_entity_decode(defaults($matches, 2, ''), ENT_QUOTES, 'UTF-8');
+                               }
 
-               // We only call this so that a previously unknown contact can be added.
-               // This is important for the function "Model\Contact::getDetailsByURL()".
-               // This function then can fetch an entry from the contact table.
-               Contact::getIdForURL($profile, 0, true);
+                               // We only call this so that a previously unknown contact can be added.
+                               // This is important for the function "Model\Contact::getDetailsByURL()".
+                               // This function then can fetch an entry from the contact table.
+                               Contact::getIdForURL($attributes['profile'], 0, true);
 
-               $data = Contact::getDetailsByURL($profile);
+                               $author_contact = Contact::getDetailsByURL($attributes['profile']);
+                               $author_contact['addr'] = defaults($author_contact, 'addr' , Protocol::getAddrFromProfileUrl($attributes['profile']));
 
-               if (x($data, "name") && x($data, "addr")) {
-                       $userid_compact = $data["name"] . " (" . $data["addr"] . ")";
-               } else {
-                       $userid_compact = Protocol::getAddrFromProfileUrl($profile, $author);
-               }
+                               $attributes['author']   = defaults($author_contact, 'name' , $attributes['author']);
+                               $attributes['avatar']   = defaults($author_contact, 'micro', $attributes['avatar']);
+                               $attributes['profile']  = defaults($author_contact, 'url'  , $attributes['profile']);
 
-               if (x($data, "addr")) {
-                       $userid = $data["addr"];
-               } else {
-                       $userid = Protocol::formatMention($profile, $author);
-               }
+                               if ($attributes['avatar']) {
+                                       $attributes['avatar'] = ProxyUtils::proxifyUrl($attributes['avatar'], false, ProxyUtils::SIZE_THUMB);
+                               }
 
-               if (x($data, "name")) {
-                       $author = $data["name"];
-               }
+                               return $match[1] . $callback($attributes, $author_contact, $match[3], trim($match[1]) != '');
+                       },
+                       $text
+               );
 
-               if (x($data, "micro")) {
-                       $avatar = $data["micro"];
-               }
+               return $return;
+       }
 
-               $preshare = trim($share[1]);
-               if ($preshare != "") {
-                       $preshare .= "<br />";
-               }
+       /**
+        * Default [share] tag conversion callback
+        *
+        * Note: Can produce a [bookmark] tag in the output
+        *
+        * @see BBCode::convertShare()
+        * @param array   $attributes     [share] block attribute values
+        * @param array   $author_contact Contact row of the shared author
+        * @param string  $content        Inner content of the [share] block
+        * @param boolean $is_quote_share Whether there is content before the [share] block
+        * @param integer $simplehtml     Mysterious integer value depending on the target network/formatting style
+        * @return string
+        * @throws \Friendica\Network\HTTPException\InternalServerErrorException
+        */
+       private static function convertShareCallback(array $attributes, array $author_contact, $content, $is_quote_share, $simplehtml)
+       {
+               $mention = Protocol::formatMention($attributes['profile'], $attributes['author']);
 
                switch ($simplehtml) {
                        case 1:
-                               $text = $preshare . html_entity_decode("&#x2672; ", ENT_QUOTES, 'UTF-8') . ' <a href="' . $profile . '">' . $userid . "</a>: <br />»" . $share[3] . "«";
+                               $text = ($is_quote_share? '<br />' : '') . '<p>' . html_entity_decode('&#x2672; ', ENT_QUOTES, 'UTF-8') . ' <a href="' . $attributes['profile'] . '">' . $mention . '</a>: </p>' . "\n" . '«' . $content . '»';
                                break;
                        case 2:
-                               $text = $preshare . html_entity_decode("&#x2672; ", ENT_QUOTES, 'UTF-8') . ' ' . $userid_compact . ": <br />" . $share[3];
+                               $text = ($is_quote_share? '<br />' : '') . '<p>' . html_entity_decode('&#x2672; ', ENT_QUOTES, 'UTF-8') . ' ' . $author_contact['addr'] . ': </p>' . "\n" . $content;
                                break;
                        case 3: // Diaspora
-                               $headline = '<b>' . html_entity_decode("&#x2672; ", ENT_QUOTES, 'UTF-8') . $userid . ':</b><br />';
-
-                               $text = trim($share[1]);
+                               $headline = '<p><b>' . html_entity_decode('&#x2672; ', ENT_QUOTES, 'UTF-8') . $mention . ':</b></p>' . "\n";
 
-                               if ($text != "") {
-                                       $text .= "<hr />";
-                               }
-
-                               if (stripos(normalise_link($link), 'http://twitter.com/') === 0) {
-                                       $text .= '<br /><a href="' . $link . '">' . $link . '</a>';
+                               if (stripos(Strings::normaliseLink($attributes['link']), 'http://twitter.com/') === 0) {
+                                       $text = ($is_quote_share? '<hr />' : '') . '<p><a href="' . $attributes['link'] . '">' . $attributes['link'] . '</a></p>' . "\n";
                                } else {
-                                       $text .= $headline . '<blockquote>' . trim($share[3]) . "</blockquote><br />";
+                                       $text = ($is_quote_share? '<hr />' : '') . $headline . '<blockquote>' . trim($content) . '</blockquote>' . "\n";
 
-                                       if ($link != "") {
-                                               $text .= '<br /><a href="' . $link . '">[l]</a>';
+                                       if ($attributes['link'] != '') {
+                                               $text .= '<p><a href="' . $attributes['link'] . '">[l]</a></p>' . "\n";
                                        }
                                }
 
                                break;
                        case 4:
-                               $headline = '<br /><b>' . html_entity_decode("&#x2672; ", ENT_QUOTES, 'UTF-8');
-                               $headline .= L10n::t('<a href="%1$s" target="_blank">%2$s</a> %3$s', $link, $userid, $posted);
-                               $headline .= ":</b><br />";
-
-                               $text = trim($share[1]);
+                               $headline = '<p><b>' . html_entity_decode('&#x2672; ', ENT_QUOTES, 'UTF-8');
+                               $headline .= L10n::t('<a href="%1$s" target="_blank">%2$s</a> %3$s', $attributes['link'], $mention, $attributes['posted']);
+                               $headline .= ':</b></p>' . "\n";
 
-                               if ($text != "") {
-                                       $text .= "<hr />";
-                               }
-
-                               $text .= $headline . '<blockquote class="shared_content">' . trim($share[3]) . "</blockquote><br />";
+                               $text = ($is_quote_share? '<hr />' : '') . $headline . '<blockquote class="shared_content">' . trim($content) . '</blockquote>' . "\n";
 
                                break;
                        case 5:
-                               $text = $preshare . html_entity_decode("&#x2672; ", ENT_QUOTES, 'UTF-8') . ' ' . $userid_compact . ": <br />" . $share[3];
-                               break;
-                       case 6: // app.net
-                               $text = $preshare . "&gt;&gt; @" . $userid_compact . ": <br />" . $share[3];
+                               $text = ($is_quote_share? '<br />' : '') . '<p>' . html_entity_decode('&#x2672; ', ENT_QUOTES, 'UTF-8') . ' ' . $author_contact['addr'] . ': </p>' . "\n" . $content;
                                break;
                        case 7: // statusnet/GNU Social
-                               $text = $preshare . html_entity_decode("&#x2672; ", ENT_QUOTES, 'UTF-8') . " @" . $userid_compact . ": " . $share[3];
+                               $text = ($is_quote_share? '<br />' : '') . '<p>' . html_entity_decode('&#x2672; ', ENT_QUOTES, 'UTF-8') . ' @' . $author_contact['addr'] . ': ' . $content . '</p>' . "\n";
                                break;
-                       case 8: // twitter
-                               $text = $preshare . "RT @" . $userid_compact . ": " . $share[3];
-                               break;
-                       case 9: // Google+/Facebook
-                               $text = $preshare . html_entity_decode("&#x2672; ", ENT_QUOTES, 'UTF-8') . ' ' . $userid_compact . ": <br />" . $share[3];
+                       case 9: // Google+
+                               $text = ($is_quote_share? '<br />' : '') . '<p>' . html_entity_decode('&#x2672; ', ENT_QUOTES, 'UTF-8') . ' ' . $author_contact['addr'] . ': </p>' . "\n";
+                               $text .= '<p>' . $content . '</p>' . "\n";
 
-                               if ($link != "") {
-                                       $text .= "<br /><br />" . $link;
+                               if ($attributes['link'] != '') {
+                                       $text .= '<p>' . $attributes['link'] . '</p>';
                                }
                                break;
                        default:
                                // Transforms quoted tweets in rich attachments to avoid nested tweets
-                               if (stripos(normalise_link($link), 'http://twitter.com/') === 0 && OEmbed::isAllowedURL($link)) {
+                               if (stripos(Strings::normaliseLink($attributes['link']), 'http://twitter.com/') === 0 && OEmbed::isAllowedURL($attributes['link'])) {
                                        try {
-                                               $oembed = OEmbed::getHTML($link, $preshare);
+                                               $text = ($is_quote_share? '<br />' : '') . OEmbed::getHTML($attributes['link']);
                                        } catch (Exception $e) {
-                                               $oembed = sprintf('[bookmark=%s]%s[/bookmark]', $link, $preshare);
+                                               $text = ($is_quote_share? '<br />' : '') . sprintf('[bookmark=%s]%s[/bookmark]', $attributes['link'], $content);
                                        }
-
-                                       $text = $preshare . $oembed;
                                } else {
-                                       $text = trim($share[1]) . "\n";
-
-                                       $avatar = proxy_url($avatar, false, PROXY_SIZE_THUMB);
-
-                                       $tpl = get_markup_template('shared_content.tpl');
-                                       $text .= replace_macros($tpl, [
-                                               '$profile' => $profile,
-                                               '$avatar' => $avatar,
-                                               '$author' => $author,
-                                               '$link' => $link,
-                                               '$posted' => $posted,
-                                               '$content' => trim($share[3])
+                                       $text = ($is_quote_share? "\n" : '');
+
+                                       $tpl = Renderer::getMarkupTemplate('shared_content.tpl');
+                                       $text .= Renderer::replaceMacros($tpl, [
+                                               '$profile' => $attributes['profile'],
+                                               '$avatar'  => $attributes['avatar'],
+                                               '$author'  => $attributes['author'],
+                                               '$link'    => $attributes['link'],
+                                               '$posted'  => $attributes['posted'],
+                                               '$content' => trim($content)
                                        ]);
                                }
                                break;
@@ -1185,11 +1023,11 @@ class BBCode extends BaseObject
                        $ch = @curl_init($match[1]);
                        @curl_setopt($ch, CURLOPT_NOBODY, true);
                        @curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
-                       @curl_setopt($ch, CURLOPT_USERAGENT, $a->get_useragent());
+                       @curl_setopt($ch, CURLOPT_USERAGENT, $a->getUserAgent());
                        @curl_exec($ch);
                        $curl_info = @curl_getinfo($ch);
 
-                       $a->save_timestamp($stamp1, "network");
+                       $a->saveTimestamp($stamp1, "network");
 
                        if (substr($curl_info["content_type"], 0, 6) == "image/") {
                                $text = "[url=" . $match[1] . "]" . $match[1] . "[/url]";
@@ -1244,11 +1082,11 @@ class BBCode extends BaseObject
                        $ch = @curl_init($match[1]);
                        @curl_setopt($ch, CURLOPT_NOBODY, true);
                        @curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
-                       @curl_setopt($ch, CURLOPT_USERAGENT, $a->get_useragent());
+                       @curl_setopt($ch, CURLOPT_USERAGENT, $a->getUserAgent());
                        @curl_exec($ch);
                        $curl_info = @curl_getinfo($ch);
 
-                       $a->save_timestamp($stamp1, "network");
+                       $a->saveTimestamp($stamp1, "network");
 
                        // if its a link to a picture then embed this picture
                        if (substr($curl_info["content_type"], 0, 6) == "image/") {
@@ -1288,17 +1126,6 @@ class BBCode extends BaseObject
                return $return;
        }
 
-       private static function textHighlightCallback($match)
-       {
-               if (in_array(strtolower($match[1]),
-                               ['php', 'css', 'mysql', 'sql', 'abap', 'diff', 'html', 'perl', 'ruby',
-                               'vbscript', 'avrc', 'dtd', 'java', 'xml', 'cpp', 'python', 'javascript', 'js', 'sh'])
-               ) {
-                       return text_highlight($match[2], strtolower($match[1]));
-               }
-               return $match[0];
-       }
-
        /**
         * @brief Converts a BBCode message to HTML message
         *
@@ -1308,11 +1135,11 @@ class BBCode extends BaseObject
         * Simple HTML values meaning:
         * - 0: Friendica display
         * - 1: Unused
-        * - 2: Used for Facebook, Google+, Windows Phone push, Friendica API
+        * - 2: Used for Google+, Windows Phone push, Friendica API
         * - 3: Used before converting to Markdown in bb2diaspora.php
         * - 4: Used for WordPress, Libertree (before Markdown), pump.io and tumblr
         * - 5: Unused
-        * - 6: Used for Appnet
+        * - 6: Unused
         * - 7: Used for dfrn, OStatus
         * - 8: Used for WP backlink text setting
         *
@@ -1321,8 +1148,9 @@ class BBCode extends BaseObject
         * @param int    $simple_html
         * @param bool   $for_plaintext
         * @return string
+        * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         */
-       public static function convert($text, $try_oembed = true, $simple_html = false, $for_plaintext = false)
+       public static function convert($text, $try_oembed = true, $simple_html = 0, $for_plaintext = false)
        {
                $a = self::getApp();
 
@@ -1347,6 +1175,22 @@ class BBCode extends BaseObject
                        return $return;
                };
 
+               // Extracting multi-line code blocks before the whitespace processing
+               $codeblocks = [];
+
+               $text = preg_replace_callback("#\[code(?:=([^\]]*))?\](.*?)\[\/code\]#ism",
+                       function ($matches) use (&$codeblocks) {
+                               $return = $matches[0];
+                               if (strpos($matches[2], "\n") !== false) {
+                                       $return = '#codeblock-' . count($codeblocks) . '#';
+
+                                       $codeblocks[] =  '<pre><code class="language-' . trim($matches[1]) . '">' . trim($matches[2], "\n\r") . '</code></pre>';
+                               }
+                               return $return;
+                       },
+                       $text
+               );
+
                // Hide all [noparse] contained bbtags by spacefying them
                // POSSIBLE BUG --> Will the 'preg' functions crash if there's an embedded image?
 
@@ -1385,19 +1229,11 @@ class BBCode extends BaseObject
                $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("/\n\[code\]/ism", "[code]", $text);
-               $text = preg_replace("/\[\/code\]\n/ism", "[/code]", $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) {
                        $text = preg_replace("/\[share(.*?)avatar\s?=\s?'.*?'\s?(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "\n[share$1$2]$3[/share]", $text);
                }
 
-               // Check for [code] text here, before the linefeeds are messed with.
-               // The highlighter will unescape and re-escape the content.
-               if (strpos($text, '[code=') !== false) {
-                       $text = preg_replace_callback("/\[code=(.*?)\](.*?)\[\/code\]/ism", 'self::textHighlightCallback', $text);
-               }
                // Convert new line chars to html <br /> tags
 
                // nlbr seems to be hopelessly messed up
@@ -1457,7 +1293,7 @@ class BBCode extends BaseObject
                                $text);
                } elseif ($simple_html == 7) {
                        $text = preg_replace("/([@!])\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
-                               '$1<span class="vcard"><a href="$2" class="url" title="$3"><span class="fn nickname mention">$3</span></a></span>',
+                               '$1<span class="vcard"><a href="$2" class="url u-url mention" title="$3"><span class="fn nickname mention">$3</span></a></span>',
                                $text);
                } elseif (!$simple_html) {
                        $text = preg_replace("/([@!])\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
@@ -1494,26 +1330,45 @@ class BBCode extends BaseObject
 
                // Handle Diaspora posts
                $text = preg_replace_callback(
-                       "&\[url=/posts/([^\[\]]*)\](.*)\[\/url\]&Usi",
+                       "&\[url=/?posts/([^\[\]]*)\](.*)\[\/url\]&Usi",
                        function ($match) {
                                return "[url=" . System::baseUrl() . "/display/" . $match[1] . "]" . $match[2] . "[/url]";
                        }, $text
                );
 
+               $text = preg_replace_callback(
+                       "&\[url=/people\?q\=(.*)\](.*)\[\/url\]&Usi",
+                       function ($match) {
+                               return "[url=" . System::baseUrl() . "/search?search=%40" . $match[1] . "]" . $match[2] . "[/url]";
+                       }, $text
+               );
+
                // Server independent link to posts and comments
                // See issue: https://github.com/diaspora/diaspora_federation/issues/75
                $expression = "=diaspora://.*?/post/([0-9A-Za-z\-_@.:]{15,254}[0-9A-Za-z])=ism";
                $text = preg_replace($expression, System::baseUrl()."/display/$1", $text);
 
-               $text = preg_replace("/([#])\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
-                                       '$1<a href="' . System::baseUrl() . '/search?tag=$3" class="tag" title="$3">$3</a>', $text);
-
-               $text = preg_replace("/\[url\=([$URLSearchString]*)\]#(.*?)\[\/url\]/ism",
-                                       '#<a href="' . System::baseUrl() . '/search?tag=$2" class="tag" title="$2">$2</a>', $text);
+               /* Tag conversion
+                * Supports:
+                * - #[url=<anything>]<term>[/url]
+                * - [url=<anything>]#<term>[/url]
+                */
+               $text = preg_replace_callback("/(?:#\[url\=[$URLSearchString]*\]|\[url\=[$URLSearchString]*\]#)(.*?)\[\/url\]/ism", function($matches) {
+                       return '#<a href="'
+                               . System::baseUrl()     . '/search?tag=' . rawurlencode($matches[1])
+                               . '" class="tag" title="' . XML::escape($matches[1]) . '">'
+                               . XML::escape($matches[1])
+                               . '</a>';
+               }, $text);
+
+               // We need no target="_blank" for local links
+               // convert links start with System::baseUrl() as local link without the target="_blank" attribute
+               $escapedBaseUrl = preg_quote(System::baseUrl(), '/');
+               $text = preg_replace("/\[url\](".$escapedBaseUrl."[$URLSearchString]*)\[\/url\]/ism", '<a href="$1">$1</a>', $text);
+               $text = preg_replace("/\[url\=(".$escapedBaseUrl."[$URLSearchString]*)\](.*?)\[\/url\]/ism", '<a href="$1">$2</a>', $text);             
 
                $text = preg_replace("/\[url\]([$URLSearchString]*)\[\/url\]/ism", '<a href="$1" target="_blank">$1</a>', $text);
                $text = preg_replace("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '<a href="$1" target="_blank">$2</a>', $text);
-               //$Text = preg_replace("/\[url\=([$URLSearchString]*)\]([$URLSearchString]*)\[\/url\]/ism", '<a href="$1" target="_blank">$2</a>', $Text);
 
                // Red compatibility, though the link can't be authenticated on Friendica
                $text = preg_replace("/\[zrl\=([$URLSearchString]*)\](.*?)\[\/zrl\]/ism", '<a href="$1" target="_blank">$2</a>', $text);
@@ -1529,7 +1384,7 @@ class BBCode extends BaseObject
                $text = preg_replace("/\[mail\=([$MAILSearchString]*)\](.*?)\[\/mail\]/", '<a href="mailto:$1">$2</a>', $text);
 
                // leave open the posibility of [map=something]
-               // this is replaced in prepare_body() which has knowledge of the item location
+               // this is replaced in Item::prepareBody() which has knowledge of the item location
 
                if (strpos($text, '[/map]') !== false) {
                        $text = preg_replace_callback(
@@ -1574,7 +1429,7 @@ class BBCode extends BaseObject
                $text = preg_replace("(\[u\](.*?)\[\/u\])ism", '<u>$1</u>', $text);
 
                // Check for strike-through text
-               $text = preg_replace("(\[s\](.*?)\[\/s\])ism", '<strike>$1</strike>', $text);
+               $text = preg_replace("(\[s\](.*?)\[\/s\])ism", '<s>$1</s>', $text);
 
                // Check for over-line text
                $text = preg_replace("(\[o\](.*?)\[\/o\])ism", '<span class="overline">$1</span>', $text);
@@ -1640,7 +1495,7 @@ class BBCode extends BaseObject
 
                $text = str_replace('[hr]', '<hr />', $text);
 
-               // This is actually executed in prepare_body()
+               // This is actually executed in Item::prepareBody()
 
                $text = str_replace('[nosmile]', '', $text);
 
@@ -1701,12 +1556,12 @@ class BBCode extends BaseObject
                // [img=widthxheight]image source[/img]
                $text = preg_replace_callback(
                        "/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism",
-                       function ($matches) {
+                       function ($matches) use ($simple_html) {
                                if (strpos($matches[3], "data:image/") === 0) {
                                        return $matches[0];
                                }
 
-                               $matches[3] = proxy_url($matches[3]);
+                               $matches[3] = self::proxyUrl($matches[3], $simple_html);
                                return "[img=" . $matches[1] . "x" . $matches[2] . "]" . $matches[3] . "[/img]";
                        },
                        $text
@@ -1715,16 +1570,24 @@ class BBCode extends BaseObject
                $text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '<img src="$3" style="width: $1px;" >', $text);
                $text = preg_replace("/\[zmg\=([0-9]*)x([0-9]*)\](.*?)\[\/zmg\]/ism", '<img class="zrl" src="$3" style="width: $1px;" >', $text);
 
+               $text = preg_replace_callback("/\[img\=([$URLSearchString]*)\](.*?)\[\/img\]/ism",
+                       function ($matches) use ($simple_html) {
+                               $matches[1] = self::proxyUrl($matches[1], $simple_html);
+                               $matches[2] = htmlspecialchars($matches[2], ENT_COMPAT);
+                               return '<img src="' . $matches[1] . '" alt="' . $matches[2] . '">';
+                       },
+                       $text);
+
                // Images
                // [img]pathtoimage[/img]
                $text = preg_replace_callback(
                        "/\[img\](.*?)\[\/img\]/ism",
-                       function ($matches) {
+                       function ($matches) use ($simple_html) {
                                if (strpos($matches[1], "data:image/") === 0) {
                                        return $matches[0];
                                }
 
-                               $matches[1] = proxy_url($matches[1]);
+                               $matches[1] = self::proxyUrl($matches[1], $simple_html);
                                return "[img]" . $matches[1] . "[/img]";
                        },
                        $text
@@ -1734,10 +1597,12 @@ class BBCode extends BaseObject
                $text = preg_replace("/\[zmg\](.*?)\[\/zmg\]/ism", '<img src="$1" alt="' . L10n::t('Image/photo') . '" />', $text);
 
                // Shared content
-               $text = preg_replace_callback("/(.*?)\[share(.*?)\](.*?)\[\/share\]/ism",
-                       function ($match) use ($simple_html) {
-                               return self::convertShare($match, $simple_html);
-                       }, $text);
+               $text = self::convertShare(
+                       $text,
+                       function (array $attributes, array $author_contact, $content, $is_quote_share) use ($simple_html) {
+                               return self::convertShareCallback($attributes, $author_contact, $content, $is_quote_share, $simple_html);
+                       }
+               );
 
                $text = preg_replace("/\[crypt\](.*?)\[\/crypt\]/ism", '<br/><img src="' .System::baseUrl() . '/images/lock_icon.gif" alt="' . L10n::t('Encrypted content') . '" title="' . L10n::t('Encrypted content') . '" /><br />', $text);
                $text = preg_replace("/\[crypt(.*?)\](.*?)\[\/crypt\]/ism", '<br/><img src="' .System::baseUrl() . '/images/lock_icon.gif" alt="' . L10n::t('Encrypted content') . '" title="' . '$1' . ' ' . L10n::t('Encrypted content') . '" /><br />', $text);
@@ -1745,15 +1610,15 @@ class BBCode extends BaseObject
 
                // Try to Oembed
                if ($try_oembed) {
-                       $text = preg_replace("/\[video\](.*?\.(ogg|ogv|oga|ogm|webm|mp4))\[\/video\]/ism", '<video src="$1" controls="controls" width="' . $a->videowidth . '" height="' . $a->videoheight . '" loop="true"><a href="$1">$1</a></video>', $text);
-                       $text = preg_replace("/\[audio\](.*?\.(ogg|ogv|oga|ogm|webm|mp4|mp3))\[\/audio\]/ism", '<audio src="$1" controls="controls"><a href="$1">$1</a></audio>', $text);
+                       $text = preg_replace("/\[video\](.*?\.(ogg|ogv|oga|ogm|webm|mp4).*?)\[\/video\]/ism", '<video src="$1" controls="controls" width="' . $a->videowidth . '" height="' . $a->videoheight . '" loop="true"><a href="$1">$1</a></video>', $text);
+                       $text = preg_replace("/\[audio\](.*?\.(ogg|ogv|oga|ogm|webm|mp4|mp3).*?)\[\/audio\]/ism", '<audio src="$1" controls="controls"><a href="$1">$1</a></audio>', $text);
 
                        $text = preg_replace_callback("/\[video\](.*?)\[\/video\]/ism", $try_oembed_callback, $text);
                        $text = preg_replace_callback("/\[audio\](.*?)\[\/audio\]/ism", $try_oembed_callback, $text);
                } else {
-                       $text = preg_replace("/\[video\](.*?)\[\/video\]/",
+                       $text = preg_replace("/\[video\](.*?)\[\/video\]/ism",
                                                '<a href="$1" target="_blank">$1</a>', $text);
-                       $text = preg_replace("/\[audio\](.*?)\[\/audio\]/",
+                       $text = preg_replace("/\[audio\](.*?)\[\/audio\]/ism",
                                                '<a href="$1" target="_blank">$1</a>', $text);
                }
 
@@ -1810,7 +1675,7 @@ class BBCode extends BaseObject
                // Summary (e.g. title) is required, earlier revisions only required description (in addition to
                // start which is always required). Allow desc with a missing summary for compatibility.
 
-               if ((x($ev, 'desc') || x($ev, 'summary')) && x($ev, 'start')) {
+               if ((!empty($ev['desc']) || !empty($ev['summary'])) && !empty($ev['start'])) {
                        $sub = Event::getHTML($ev, $simple_html);
 
                        $text = preg_replace("/\[event\-summary\](.*?)\[\/event\-summary\]/ism", '', $text);
@@ -1824,21 +1689,9 @@ class BBCode extends BaseObject
 
                // Replace non graphical smilies for external posts
                if ($simple_html) {
-                       $text = Smilies::replace($text, false, true);
+                       $text = Smilies::replace($text);
                }
 
-               // Replace inline code blocks
-               $text = preg_replace_callback("|(?!<br[^>]*>)<code>([^<]*)</code>(?!<br[^>]*>)|ism",
-                       function ($match) use ($simple_html) {
-                               $return = '<key>' . $match[1] . '</key>';
-                               // Use <code> for Diaspora inline code blocks
-                               if ($simple_html === 3) {
-                                       $return = '<code>' . $match[1] . '</code>';
-                               }
-                               return $return;
-                       }
-               , $text);
-
                // Unhide all [noparse] contained bbtags unspacefying them
                // and triming the [noparse] tag.
 
@@ -1846,10 +1699,12 @@ class BBCode extends BaseObject
                $text = preg_replace_callback("/\[nobb\](.*?)\[\/nobb\]/ism", 'self::unescapeNoparseCallback', $text);
                $text = preg_replace_callback("/\[pre\](.*?)\[\/pre\]/ism", 'self::unescapeNoparseCallback', $text);
 
-
+               /// @todo What is the meaning of these lines?
                $text = preg_replace('/\[\&amp\;([#a-z0-9]+)\;\]/', '&$1;', $text);
                $text = preg_replace('/\&\#039\;/', '\'', $text);
-               $text = preg_replace('/\&quot\;/', '"', $text);
+
+               // Currently deactivated, it made problems with " inside of alt texts.
+               //$text = preg_replace('/\&quot\;/', '"', $text);
 
                // fix any escaped ampersands that may have been converted into links
                $text = preg_replace('/\<([^>]*?)(src|href)=(.*?)\&amp\;(.*?)\>/ism', '<$1$2=$3&$4>', $text);
@@ -1874,9 +1729,21 @@ class BBCode extends BaseObject
                        $text = self::interpolateSavedImagesIntoItemBody($text, $saved_image);
                }
 
+               // Restore code blocks
+               $text = preg_replace_callback('/#codeblock-([0-9]+)#/iU',
+                       function ($matches) use ($codeblocks) {
+                               $return = $matches[0];
+                               if (isset($codeblocks[intval($matches[1])])) {
+                                       $return = $codeblocks[$matches[1]];
+                               }
+                               return $return;
+                       },
+                       $text
+               );
+
                // Clean up the HTML by loading and saving the HTML with the DOM.
                // Bad structured html can break a whole page.
-               // For performance reasons do it only with ativated item cache or at export.
+               // For performance reasons do it only with activated item cache or at export.
                if (!$try_oembed || (get_itemcachepath() != "")) {
                        $doc = new DOMDocument();
                        $doc->preserveWhiteSpace = false;
@@ -1902,7 +1769,7 @@ class BBCode extends BaseObject
                //$Text = str_replace('<br /><li>', '<li>', $Text);
                //$Text = str_replace('<br /><ul', '<ul ', $Text);
 
-               Addon::callHooks('bbcode', $text);
+               Hook::callAll('bbcode', $text);
 
                return trim($text);
        }
@@ -1928,7 +1795,7 @@ class BBCode extends BaseObject
         * @param string $addon The addon for which the abstract is meant for
         * @return string The abstract
         */
-       private static function getAbstract($text, $addon = "")
+       public static function getAbstract($text, $addon = "")
        {
                $abstract = "";
                $abstracts = [];
@@ -1955,7 +1822,12 @@ class BBCode extends BaseObject
         * @brief Callback function to replace a Friendica style mention in a mention for Diaspora
         *
         * @param array $match Matching values for the callback
+        *                     [1] = Mention type (! or @)
+        *                     [2] = Name
+        *                     [3] = Address
         * @return string Replaced mention
+        * @throws \Friendica\Network\HTTPException\InternalServerErrorException
+        * @throws \ImagickException
         */
        private static function bbCodeMention2DiasporaCallback($match)
        {
@@ -1969,7 +1841,7 @@ class BBCode extends BaseObject
                        return $match[0];
                }
 
-               $mention = '@{' . $match[2] . '; ' . $contact['addr'] . '}';
+               $mention = $match[1] . '{' . $match[2] . '; ' . $contact['addr'] . '}';
                return $mention;
        }
 
@@ -1982,6 +1854,7 @@ class BBCode extends BaseObject
         * @param string $text
         * @param bool   $for_diaspora Diaspora requires more changes than Libertree
         * @return string
+        * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         */
        public static function toMarkdown($text, $for_diaspora = true)
        {
@@ -2008,23 +1881,6 @@ class BBCode extends BaseObject
                // Converting images with size parameters to simple images. Markdown doesn't know it.
                $text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $text);
 
-               // Extracting multi-line code blocks before the whitespace processing/code highlighter in self::convert()
-               $codeblocks = [];
-
-               $text = preg_replace_callback("#\[code(?:=([^\]]*))?\](.*?)\[\/code\]#is",
-                       function ($matches) use (&$codeblocks) {
-                               $return = $matches[0];
-                               if (strpos($matches[2], "\n") !== false) {
-                                       $return = '#codeblock-' . count($codeblocks) . '#';
-
-                                       $prefix = '````' . $matches[1] . PHP_EOL;
-                                       $codeblocks[] = $prefix . trim($matches[2]) . PHP_EOL . '````';
-                               }
-                               return $return;
-                       },
-                       $text
-               );
-
                // Convert it to HTML - don't try oembed
                if ($for_diaspora) {
                        $text = self::convert($text, false, 3);
@@ -2054,13 +1910,12 @@ class BBCode extends BaseObject
                $stamp1 = microtime(true);
 
                // Now convert HTML to Markdown
-               $converter = new HtmlConverter();
-               $text = $converter->convert($text);
+               $text = HTML::toMarkdown($text);
 
                // unmask the special chars back to HTML
                $text = str_replace(['&\_lt\_;', '&\_gt\_;', '&\_amp\_;'], ['&lt;', '&gt;', '&amp;'], $text);
 
-               $a->save_timestamp($stamp1, "parser");
+               $a->saveTimestamp($stamp1, "parser");
 
                // Libertree has a problem with escaped hashtags.
                $text = str_replace(['\#'], ['#'], $text);
@@ -2072,26 +1927,88 @@ class BBCode extends BaseObject
                if ($for_diaspora) {
                        $url_search_string = "^\[\]";
                        $text = preg_replace_callback(
-                               "/([@]\[(.*?)\])\(([$url_search_string]*?)\)/ism",
+                               "/([@!])\[(.*?)\]\(([$url_search_string]*?)\)/ism",
                                ['self', 'bbCodeMention2DiasporaCallback'],
                                $text
                        );
                }
 
-               // Restore code blocks
-               $text = preg_replace_callback('/#codeblock-([0-9]+)#/iU',
-                       function ($matches) use ($codeblocks) {
-                               $return = '';
-                               if (isset($codeblocks[intval($matches[1])])) {
-                                       $return = $codeblocks[$matches[1]];
-                               }
-                               return $return;
-                       },
-                       $text
-               );
-
-               Addon::callHooks('bb2diaspora', $text);
+               Hook::callAll('bb2diaspora', $text);
 
                return $text;
        }
+
+       /**
+     * @brief Pull out all #hashtags and @person tags from $string.
+     *
+     * We also get @person@domain.com - which would make
+     * the regex quite complicated as tags can also
+     * end a sentence. So we'll run through our results
+     * and strip the period from any tags which end with one.
+     * 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)
+    {
+        $ret = [];
+
+        // Convert hashtag links to hashtags
+        $string = preg_replace('/#\[url\=([^\[\]]*)\](.*?)\[\/url\]/ism', '#$2', $string);
+
+        // ignore anything in a code block
+        $string = preg_replace('/\[code.*?\].*?\[\/code\]/sm', '', $string);
+
+        // Force line feeds at bbtags
+        $string = str_replace(['[', ']'], ["\n[", "]\n"], $string);
+
+        // ignore anything in a bbtag
+        $string = preg_replace('/\[(.*?)\]/sm', '', $string);
+
+        // Match full names against @tags including the space between first and last
+        // We will look these up afterward to see if they are full names or not recognisable.
+
+        if (preg_match_all('/(@[^ \x0D\x0A,:?]+ [^ \x0D\x0A@,:?]+)([ \x0D\x0A@,:?]|$)/', $string, $matches)) {
+            foreach ($matches[1] as $match) {
+                if (strstr($match, ']')) {
+                    // we might be inside a bbcode color tag - leave it alone
+                    continue;
+                }
+
+                if (substr($match, -1, 1) === '.') {
+                    $ret[] = substr($match, 0, -1);
+                } else {
+                    $ret[] = $match;
+                }
+            }
+        }
+
+        // Otherwise pull out single word tags. These can be @nickname, @first_last
+        // and #hash tags.
+
+        if (preg_match_all('/([!#@][^\^ \x0D\x0A,;:?]+)([ \x0D\x0A,;:?]|$)/', $string, $matches)) {
+            foreach ($matches[1] as $match) {
+                if (strstr($match, ']')) {
+                    // we might be inside a bbcode color tag - leave it alone
+                    continue;
+                }
+                if (substr($match, -1, 1) === '.') {
+                    $match = substr($match,0,-1);
+                }
+                // ignore strictly numeric tags like #1
+                if ((strpos($match, '#') === 0) && ctype_digit(substr($match, 1))) {
+                    continue;
+                }
+                // try not to catch url fragments
+                if (strpos($string, $match) && preg_match('/[a-zA-z0-9\/]/', substr($string, strpos($string, $match) - 1, 1))) {
+                    continue;
+                }
+                $ret[] = $match;
+            }
+        }
+
+        return $ret;
+    }
 }