|
|
|
<?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(['[', ']'], ['[', ']'], $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]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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) {
|
|
|
|
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
|
|
|
|
$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)
|
|
|
|
{
|
|
|
|
$naked_text = HTML::toPlaintext(self::convert($text, false, 0, true), 0, !$keep_urls);
|
|
|
|
|
|
|
|
return $naked_text;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static function proxyUrl($image, $simplehtml = self::INTERNAL)
|
|
|
|
{
|
|
|
|
// Only send proxied pictures to API and for internal display
|
|
|
|
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
|
|
|
|
*/
|
|
|
|
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'];
|
|
|
|
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|