Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2167 lines
78 KiB

<?php
/**
* @copyright Copyright (C) 2020, Friendica
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Content\Text;
use DOMDocument;
use DOMXPath;
use Exception;
use Friendica\Content\OEmbed;
use Friendica\Content\Smilies;
use Friendica\Core\Hook;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Core\Renderer;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Event;
use Friendica\Model\Photo;
use Friendica\Network\Probe;
use Friendica\Object\Image;
use Friendica\Protocol\Activity;
use Friendica\Util\Images;
use Friendica\Util\Map;
use Friendica\Util\Network;
use Friendica\Util\ParseUrl;
use Friendica\Util\Proxy as ProxyUtils;
use Friendica\Util\Strings;
use Friendica\Util\XML;
class BBCode
{
const INTERNAL = 0;
const API = 2;
const DIASPORA = 3;
const CONNECTORS = 4;
const OSTATUS = 7;
const TWITTER = 8;
const BACKLINK = 8;
const ACTIVITYPUB = 9;
/**
* Fetches attachment data that were generated the old way
*
* @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
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function getOldAttachmentData($body)
{
$post = [];
// Simplify image codes
$body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body);
if (preg_match_all("(\[class=(.*?)\](.*?)\[\/class\])ism", $body, $attached, PREG_SET_ORDER)) {
foreach ($attached as $data) {
if (!in_array($data[1], ['type-link', 'type-video', 'type-photo'])) {
continue;
}
$post['type'] = substr($data[1], 5);
$pos = strpos($body, $data[0]);
if ($pos > 0) {
$post['text'] = trim(substr($body, 0, $pos));
$post['after'] = trim(substr($body, $pos + strlen($data[0])));
} else {
$post['text'] = trim(str_replace($data[0], '', $body));
$post['after'] = '';
}
$attacheddata = $data[2];
if (preg_match("/\[img\](.*?)\[\/img\]/ism", $attacheddata, $matches)) {
$picturedata = Images::getInfoFromURLCached($matches[1]);
if ($picturedata) {
if (($picturedata[0] >= 500) && ($picturedata[0] >= $picturedata[1])) {
$post['image'] = $matches[1];
} else {
$post['preview'] = $matches[1];
}
}
}
if (preg_match("/\[bookmark\=(.*?)\](.*?)\[\/bookmark\]/ism", $attacheddata, $matches)) {
$post['url'] = $matches[1];
$post['title'] = $matches[2];
}
if (!empty($post['url']) && (in_array($post['type'], ['link', 'video']))
&& preg_match("/\[url\=(.*?)\](.*?)\[\/url\]/ism", $attacheddata, $matches)) {
$post['url'] = $matches[1];
}
// Search for description
if (preg_match("/\[quote\](.*?)\[\/quote\]/ism", $attacheddata, $matches)) {
$post['description'] = $matches[1];
}
}
}
return $post;
}
/**
* Fetches attachment data that were generated with the "attachment" element
*
* @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
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function getAttachmentData($body)
{
$data = [
'type' => '',
'text' => '',
'after' => '',
'image' => null,
'url' => '',
'title' => '',
'description' => '',
];
if (!preg_match("/(.*)\[attachment(.*?)\](.*?)\[\/attachment\](.*)/ism", $body, $match)) {
return self::getOldAttachmentData($body);
}
$attributes = $match[2];
$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 [];
}
if (!in_array($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');
}
$data['description'] = trim($match[3]);
$data['after'] = trim($match[4]);
return $data;
}
public static function getAttachedData($body, $item = [])
{
/*
- text:
- type: link, video, photo
- title:
- url:
- image:
- description:
- (thumbnail)
*/
$has_title = !empty($item['title']);
$plink = $item['plink'] ?? '';
$post = self::getAttachmentData($body);
// Get all linked images with alternative image description
if (preg_match_all("/\[img=([^\[\]]*)\]([^\[\]]*)\[\/img\]/Usi", $body, $pictures, PREG_SET_ORDER)) {
foreach ($pictures as $picture) {
if (Photo::isLocal($picture[1])) {
$post['images'][] = ['url' => str_replace('-1.', '-0.', $picture[1]), 'description' => $picture[2]];
}
}
3 years ago
if (!empty($post['images']) && !empty($post['images'][0]['description'])) {
$post['image_description'] = $post['images'][0]['description'];
}
}
if (preg_match_all("/\[img\]([^\[\]]*)\[\/img\]/Usi", $body, $pictures, PREG_SET_ORDER)) {
foreach ($pictures as $picture) {
if (Photo::isLocal($picture[1])) {
$post['images'][] = ['url' => str_replace('-1.', '-0.', $picture[1]), 'description' => ''];
}
}
}
// if nothing is found, it maybe having an image.
if (!isset($post['type'])) {
// Simplify image codes
$body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body);
$body = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", '[img]$1[/img]', $body);
$post['text'] = $body;
if (preg_match_all("#\[url=([^\]]+?)\]\s*\[img\]([^\[]+?)\[/img\]\s*\[/url\]#ism", $body, $pictures, PREG_SET_ORDER)) {
if ((count($pictures) == 1) && !$has_title) {
3 years ago
if (!empty($item['object-type']) && ($item['object-type'] == Activity\ObjectType::IMAGE)) {
// Replace the preview picture with the real picture
$url = str_replace('-1.', '-0.', $pictures[0][2]);
$data = ['url' => $url, 'type' => 'photo'];
} else {
// Checking, if the link goes to a picture
$data = ParseUrl::getSiteinfoCached($pictures[0][1], true);
}
// Workaround:
// Sometimes photo posts to the own album are not detected at the start.
// So we seem to cannot use the cache for these cases. That's strange.
if (($data['type'] != 'photo') && strstr($pictures[0][1], "/photos/")) {
$data = ParseUrl::getSiteinfo($pictures[0][1], true);
}
if ($data['type'] == 'photo') {
$post['type'] = 'photo';
if (isset($data['images'][0])) {
$post['image'] = $data['images'][0]['src'];
$post['url'] = $data['url'];
} else {
$post['image'] = $data['url'];
}
$post['preview'] = $pictures[0][2];
$post['text'] = trim(str_replace($pictures[0][0], '', $body));
} else {
$imgdata = Images::getInfoFromURLCached($pictures[0][1]);
if ($imgdata && substr($imgdata['mime'], 0, 6) == 'image/') {
$post['type'] = 'photo';
$post['image'] = $pictures[0][1];
$post['preview'] = $pictures[0][2];
$post['text'] = trim(str_replace($pictures[0][0], '', $body));
}
}
} elseif (count($pictures) > 0) {
$post['type'] = 'link';
$post['url'] = $plink;
$post['image'] = $pictures[0][2];
$post['text'] = $body;
foreach ($pictures as $picture) {
$post['text'] = trim(str_replace($picture[0], '', $post['text']));
}
}
} elseif (preg_match_all("(\[img\](.*?)\[\/img\])ism", $body, $pictures, PREG_SET_ORDER)) {
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) {
$post['type'] = 'link';
$post['url'] = $plink;
$post['image'] = $pictures[0][1];
$post['text'] = $body;
foreach ($pictures as $picture) {
$post['text'] = trim(str_replace($picture[0], '', $post['text']));
}
}
}
// Test for the external links
preg_match_all("(\[url\](.*?)\[\/url\])ism", $post['text'], $links1, PREG_SET_ORDER);
preg_match_all("(\[url\=(.*?)\].*?\[\/url\])ism", $post['text'], $links2, PREG_SET_ORDER);
$links = array_merge($links1, $links2);
// If there is only a single one, then use it.
// This should cover link posts via API.
if ((count($links) == 1) && !isset($post['preview']) && !$has_title) {
$post['type'] = 'link';
$post['url'] = $links[0][1];
}
// Simplify "video" element
3 years ago
$post['text'] = preg_replace('(\[video.*?\ssrc\s?=\s?([^\s\]]+).*?\].*?\[/video\])ism', '[video]$1[/video]', $post['text']);
// Now count the number of external media links
preg_match_all("(\[vimeo\](.*?)\[\/vimeo\])ism", $post['text'], $links1, PREG_SET_ORDER);
preg_match_all("(\[youtube\\](.*?)\[\/youtube\\])ism", $post['text'], $links2, PREG_SET_ORDER);
preg_match_all("(\[video\\](.*?)\[\/video\\])ism", $post['text'], $links3, PREG_SET_ORDER);
preg_match_all("(\[audio\\](.*?)\[\/audio\\])ism", $post['text'], $links4, PREG_SET_ORDER);
// Add them to the other external links
$links = array_merge($links, $links1, $links2, $links3, $links4);
// Are there more than one?
if (count($links) > 1) {
// The post will be the type "text", which means a blog post
unset($post['type']);
$post['url'] = $plink;
}
if (!isset($post['type'])) {
$post['type'] = "text";
$post['text'] = trim($body);
}
} elseif (isset($post['url']) && ($post['type'] == 'video')) {
$data = ParseUrl::getSiteinfoCached($post['url'], true);
if (isset($data['images'][0])) {
$post['image'] = $data['images'][0]['src'];
}
}
return $post;
}
/**
* Remove [attachment] BBCode and replaces it with a regular [url]
*
* @param string $body
* @param boolean $no_link_desc No link description
*
* @return string with replaced body
*/
public static function removeAttachment($body, $no_link_desc = false)
{
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'])) {
return $match[0];
} elseif (empty($attach_data['title']) || $no_link_desc) {
return "\n[url]" . $attach_data['url'] . "[/url]\n";
} else {
return "\n[url=" . $attach_data['url'] . ']' . $attach_data['title'] . "[/url]\n";
}
}, $body);
}
/**
* Converts a BBCode text into plaintext
*
* @param $text
* @param bool $keep_urls Whether to keep URLs in the resulting plaintext
*
* @return string
*/
public static function toPlaintext($text, $keep_urls = true)
{
2 years ago
$naked_text = HTML::toPlaintext(self::convert($text, false, 0, true), 0, !$keep_urls);
return $naked_text;
}
2 years ago
private static function proxyUrl($image, $simplehtml = self::INTERNAL)
{
// Only send proxied pictures to API and for internal display
2 years ago
if (in_array($simplehtml, [self::INTERNAL, self::API])) {
return ProxyUtils::proxifyUrl($image);
} else {
return $image;
}
}
/**
* This function changing the visual size (not the real size) of images.
* The function does not work for pictures with an alternate text description.
* This could only be changed by using some new "img" BBCode format.
*
* @param string $srctext The body with images
* @return string The body with possibly scaled images
*/
public static function scaleExternalImages(string $srctext)
{
$s = $srctext;
// Simplify image links
$s = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $s);
$matches = null;
$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]);
$hostname = str_replace('www.', '', substr(DI::baseUrl(), strpos(DI::baseUrl(), '://') + 3));
if (stristr($mtch[1], $hostname)) {
continue;
}
$curlResult = Network::curl($mtch[1], true);
if (!$curlResult->isSuccess()) {
continue;
}
$i = $curlResult->getBody();
$type = $curlResult->getContentType();
$type = Images::getMimeTypeByData($i, $mtch[1], $type);
if ($i) {
$Image = new Image($i, $type);
if ($Image->isValid()) {
$orig_width = $Image->getWidth();
$orig_height = $Image->getHeight();
if ($orig_width > 640 || $orig_height > 640) {
$Image->scaleDown(640);
$new_width = $Image->getWidth();
$new_height = $Image->getHeight();
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. ']' . $mtch[1] . '[/img]'
. "\n",
$s
);
Logger::log('scale_external_images: new string: ' . $s, Logger::DEBUG);
}
}
}
}
}
return $s;
}
/**
* Truncates imported message body string length to max_import_size
*
* The purpose of this function is to apply system message length limits to
* imported messages without including any embedded photos in the length
*
* @param string $body
* @return string
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function limitBodySize($body)
{
$maxlen = DI::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::log('the total body length exceeds the limit', Logger::DEBUG);
$orig_body = $body;
$new_body = '';
$textlen = 0;
$img_start = strpos($orig_body, '[img');
$img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
$img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
while (($img_st_close !== false) && ($img_end !== false)) {
$img_st_close++; // make it point to AFTER the closing bracket
$img_end += $img_start;
$img_end += strlen('[/img]');
if (!strcmp(substr($orig_body, $img_start + $img_st_close, 5), 'data:')) {
// This is an embedded image
if (($textlen + $img_start) > $maxlen) {
if ($textlen < $maxlen) {
Logger::log('the limit happens before an embedded image', Logger::DEBUG);
$new_body = $new_body . substr($orig_body, 0, $maxlen - $textlen);
$textlen = $maxlen;
}
} else {
$new_body = $new_body . substr($orig_body, 0, $img_start);
$textlen += $img_start;
}
$new_body = $new_body . substr($orig_body, $img_start, $img_end - $img_start);
} else {
if (($textlen + $img_end) > $maxlen) {
if ($textlen < $maxlen) {
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;
}
} else {
$new_body = $new_body . substr($orig_body, 0, $img_end);
$textlen += $img_end;
}
}
$orig_body = substr($orig_body, $img_end);
if ($orig_body === false) {
// in case the body ends on a closing image tag
$orig_body = '';
}
$img_start = strpos($orig_body, '[img');
$img_st_close = ($img_start !== false ? strpos(substr($orig_body, $img_start), ']') : false);
$img_end = ($img_start !== false ? strpos(substr($orig_body, $img_start), '[/img]') : false);
}
if (($textlen + strlen($orig_body)) > $maxlen) {
if ($textlen < $maxlen) {
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::log('the text size with embedded images extracted did not violate the limit', Logger::DEBUG);
$new_body = $new_body . $orig_body;
}
return $new_body;
} else {
return $body;
}
}
/**
* Processes [attachment] tags
*
* Note: Can produce a [bookmark] tag in the returned string
*
* @param string $text
* @param integer $simplehtml
* @param bool $tryoembed
* @return string
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
2 years ago
private static function convertAttachment($text, $simplehtml = self::INTERNAL, $tryoembed = true)
{
$data = self::getAttachmentData($text);
if (empty($data) || empty($data['url'])) {
return $text;
}
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) || DI::config()->get('system', 'always_show_preview')) && !empty($data['image'])) {
$data['preview'] = $data['image'];
$data['image'] = '';
}
$return = '';
try {
if ($tryoembed && OEmbed::isAllowedURL($data['url'])) {
$return = OEmbed::getHTML($data['url'], $data['title']);
} else {
throw new Exception('OEmbed is disabled for this attachment.');
}
} catch (Exception $e) {
$data['title'] = ($data['title'] ?? '') ?: $data['url'];
2 years ago
if ($simplehtml != self::CONNECTORS) {
$return = sprintf('<div class="type-%s">', $data['type']);
}
if (!empty($data['title']) && !empty($data['url'])) {
if (!empty($data['image']) && empty($data['text']) && ($data['type'] == 'photo')) {
$return .= sprintf('<a href="%s" target="_blank" rel="noopener noreferrer"><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" rel="noopener noreferrer"><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" rel="noopener noreferrer"><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 (!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 (!empty($data['url'])) {
$return .= sprintf('<sup><a href="%s">%s</a></sup>', $data['url'], parse_url($data['url'], PHP_URL_HOST));
}
2 years ago
if ($simplehtml != self::CONNECTORS) {
$return .= '</div>';
}
}
return trim(($data['text'] ?? '') . ' ' . $return . ' ' . ($data['after'] ?? ''));
}
public static function removeShareInformation($Text, $plaintext = false, $nolink = false)
{
$data = self::getAttachmentData($Text);
if (!$data) {
return $Text;
} elseif ($nolink) {
return $data['text'] . ($data['after'] ?? '');
}
$title = htmlentities($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'];
} elseif (($text != '') && strstr($title, $text)) {
$data['text'] = $data['title'];
$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 (!empty($data['url']) && strpos($data['text'], $data['url'])) {
return $data['text'] . $data['after'];
}
$text = $data['text'];
if (!empty($data['url']) && !empty($data['title'])) {
$text .= "\n[url=" . $data['url'] . ']' . $data['title'] . '[/url]';
} elseif (!empty($data['url'])) {
$text .= "\n[url]" . $data['url'] . '[/url]';
}
return $text . "\n" . $data['after'];
}
/**
* Converts [url] BBCodes in a format that looks fine on Mastodon. (callback function)
*
* @param array $match Array with the matching values
* @return string reformatted link including HTML codes
*/
private static function convertUrlForActivityPubCallback($match)
{
$url = $match[1];
if (isset($match[2]) && ($match[1] != $match[2])) {
return $match[0];
}
$parts = parse_url($url);
if (!isset($parts['scheme'])) {
return $match[0];
}
return self::convertUrlForActivityPub($url);
}
/**
* Converts [url] BBCodes in a format that looks fine on ActivityPub systems.
*
* @param string $url URL that is about to be reformatted
* @return string reformatted link including HTML codes
*/
private static function convertUrlForActivityPub($url)
{
$html = '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a>';
return sprintf($html, $url, self::getStyledURL($url));
}
/**
* Converts an URL in a nicer format (without the scheme and possibly shortened)
*
* @param string $url URL that is about to be reformatted
* @return string reformatted link
*/
private static function getStyledURL($url)