diff --git a/include/api.php b/include/api.php index 115869fe27..c2a77bdcd8 100644 --- a/include/api.php +++ b/include/api.php @@ -2040,13 +2040,13 @@ function api_statuses_repeat($type) $fields = ['uri-id', 'network', 'body', 'title', 'author-name', 'author-link', 'author-avatar', 'guid', 'created', 'plink']; $item = Post::selectFirst($fields, ['id' => $id, 'private' => [Item::PUBLIC, Item::UNLISTED]]); - + if (DBA::isResult($item) && !empty($item['body'])) { if (in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::TWITTER])) { if (!Item::performActivity($id, 'announce', local_user())) { throw new InternalServerErrorException(); } - + $item_id = $id; } else { if (strpos($item['body'], "[/share]") !== false) { @@ -2508,7 +2508,8 @@ function api_format_messages($item, $recipient, $sender) */ function api_convert_item($item) { - $body = $item['body']; + $body = api_add_attachments_to_body($item); + $entities = api_get_entitities($statustext, $body); // Add pictures to the attachment array and remove them from the body @@ -2576,6 +2577,52 @@ function api_convert_item($item) ]; } +/** + * Add media attachments to the body + * + * @param array $item + * @return string body with added media + */ +function api_add_attachments_to_body(array $item) +{ + $body = $item['body']; + + foreach (Post\Media::getByURIId($item['uri-id'], [Post\Media::IMAGE, Post\Media::AUDIO, Post\Media::VIDEO]) as $media) { + if (Item::containsLink($item['body'], $media['url'])) { + continue; + } + + if ($media['type'] == Post\Media::IMAGE) { + if (!empty($media['description'])) { + $body .= "\n[img=" . $media['url'] . ']' . $media['description'] .'[/img]'; + } else { + $body .= "\n[img]" . $media['url'] .'[/img]'; + } + } elseif ($media['type'] == Post\Media::AUDIO) { + $body .= "\n[audio]" . $media['url'] . "[/audio]\n"; + } elseif ($media['type'] == Post\Media::VIDEO) { + $body .= "\n[video]" . $media['url'] . "[/video]\n"; + } + } + + if (strpos($body, '[/img]') !== false) { + return $body; + } + + foreach (Post\Media::getByURIId($item['uri-id'], [Post\Media::HTML]) as $media) { + if (!empty($media['preview'])) { + $description = $media['description'] ?: $media['name']; + if (!empty($description)) { + $body .= "\n[img=" . $media['preview'] . ']' . $description .'[/img]'; + } else { + $body .= "\n[img]" . $media['preview'] .'[/img]'; + } + } + } + + return $body; +} + /** * * @param string $body @@ -2604,7 +2651,11 @@ function api_get_attachments(&$body) $imagedata = Images::getInfoFromURLCached($image); if ($imagedata) { - $attachments[] = ["url" => $image, "mimetype" => $imagedata["mime"], "size" => $imagedata["size"]]; + if (DI::config()->get("system", "proxy_disabled")) { + $attachments[] = ["url" => $image, "mimetype" => $imagedata["mime"], "size" => $imagedata["size"]]; + } else { + $attachments[] = ["url" => ProxyUtils::proxifyUrl($image, false), "mimetype" => $imagedata["mime"], "size" => $imagedata["size"]]; + } } } @@ -2628,7 +2679,7 @@ function api_get_entitities(&$text, $bbcode) preg_match_all("/\[img](.*?)\[\/img\]/ism", $bbcode, $images); foreach ($images[1] as $image) { - $replace = ProxyUtils::proxifyUrl($image); + $replace = ProxyUtils::proxifyUrl($image, false); $text = str_replace($image, $replace, $text); } return []; @@ -2743,7 +2794,7 @@ function api_get_entitities(&$text, $bbcode) // If image cache is activated, then use the following sizes: // thumb (150), small (340), medium (600) and large (1024) if (!DI::config()->get("system", "proxy_disabled")) { - $media_url = ProxyUtils::proxifyUrl($url); + $media_url = ProxyUtils::proxifyUrl($url, false); $sizes = []; $scale = Images::getScalingDimensions($image[0], $image[1], 150); @@ -4824,7 +4875,7 @@ function prepare_photo_data($type, $scale, $photo_id) "SELECT %s `resource-id`, `created`, `edited`, `title`, `desc`, `album`, `filename`, `type`, `height`, `width`, `datasize`, `profile`, `allow_cid`, `deny_cid`, `allow_gid`, `deny_gid`, MIN(`scale`) AS `minscale`, MAX(`scale`) AS `maxscale` - FROM `photo` WHERE `uid` = %d AND `resource-id` = '%s' %s GROUP BY + FROM `photo` WHERE `uid` = %d AND `resource-id` = '%s' %s GROUP BY `resource-id`, `created`, `edited`, `title`, `desc`, `album`, `filename`, `type`, `height`, `width`, `datasize`, `profile`, `allow_cid`, `deny_cid`, `allow_gid`, `deny_gid`", $data_sql, @@ -5994,12 +6045,12 @@ api_register_func('api/saved_searches/list', 'api_saved_searches_list', true); * * @return void */ -function bindComments(&$data) +function bindComments(&$data) { if (count($data) == 0) { return; } - + $ids = []; $comments = []; foreach ($data as $item) { diff --git a/src/Content/PageInfo.php b/src/Content/PageInfo.php index e61db3865d..293da75a93 100644 --- a/src/Content/PageInfo.php +++ b/src/Content/PageInfo.php @@ -73,7 +73,7 @@ class PageInfo // Additional link attachments are prepended before the existing [attachment] tag $body = substr_replace($body, "\n[bookmark=" . $data['url'] . ']' . $linkTitle . "[/bookmark]\n", $existingAttachmentPos, 0); } else { - $footer = PageInfo::getFooterFromData($data, $no_photos); + $footer = self::getFooterFromData($data, $no_photos); $body = self::stripTrailingUrlFromBody($body, $data['url']); $body .= "\n" . $footer; } @@ -155,7 +155,7 @@ class PageInfo if (empty($data['text'])) { $data['text'] = $data['title']; } - + if (empty($data['text'])) { $data['text'] = $data['url']; } @@ -246,7 +246,7 @@ class PageInfo * @param bool $searchNakedUrls Whether we should pick a naked URL (outside of BBCode tags) as a last resort * @return string|null */ - protected static function getRelevantUrlFromBody(string $body, bool $searchNakedUrls = false) + public static function getRelevantUrlFromBody(string $body, bool $searchNakedUrls = false) { $URLSearchString = 'https?://[^\[\]]*'; diff --git a/src/Content/Text/BBCode.php b/src/Content/Text/BBCode.php index 039eda9462..bcc9f470a8 100644 --- a/src/Content/Text/BBCode.php +++ b/src/Content/Text/BBCode.php @@ -50,7 +50,7 @@ use Friendica\Util\XML; class BBCode { // Update this value to the current date whenever changes are made to BBCode::convert - const VERSION = '2021-04-07'; + const VERSION = '2021-04-24'; const INTERNAL = 0; const API = 2; @@ -61,6 +61,7 @@ class BBCode const BACKLINK = 8; const ACTIVITYPUB = 9; + const ANCHOR = '
'; /** * Fetches attachment data that were generated the old way * @@ -155,6 +156,7 @@ class BBCode 'image' => null, 'url' => '', 'author_name' => '', + 'author_url' => '', 'provider_name' => '', 'provider_url' => '', 'title' => '', @@ -172,7 +174,7 @@ class BBCode foreach (['type', 'url', 'title', 'image', 'preview', 'publisher_name', 'publisher_url', 'author_name', 'author_url'] as $field) { preg_match('/' . preg_quote($field, '/') . '=("|\')(.*?)\1/ism', $attributes, $matches); $value = $matches[2] ?? ''; - + if ($value != '') { switch ($field) { case 'publisher_name': @@ -939,6 +941,26 @@ class BBCode return $newbody; } + /** + * + * @param string $text A BBCode string + * @return array share attributes + */ + public static function fetchShareAttributes($text) + { + $attributes = []; + if (!preg_match("/(.*?)\[share(.*?)\](.*)\[\/share\]/ism", $text, $matches)) { + return $attributes; + } + + $attribute_string = $matches[2]; + foreach (['author', 'profile', 'avatar', 'link', 'posted', 'guid'] as $field) { + preg_match("/$field=(['\"])(.+?)\\1/ism", $attribute_string, $matches); + $attributes[$field] = html_entity_decode($matches[2] ?? '', ENT_QUOTES, 'UTF-8'); + } + return $attributes; + } + /** * This function converts a [share] block to text according to a provided callback function whose signature is: * @@ -1064,7 +1086,7 @@ class BBCode '$guid' => $attributes['guid'], '$network_name' => ContactSelector::networkToName($network, $attributes['profile']), '$network_icon' => ContactSelector::networkToIcon($network, $attributes['profile']), - '$content' => self::setMentions(trim($content), 0, $network), + '$content' => self::setMentions(trim($content), 0, $network) . self::ANCHOR, ]); break; } @@ -1102,7 +1124,7 @@ class BBCode DI::cache()->set($cache_key, $text); return $text; } - + $doc = new DOMDocument(); @$doc->loadHTML($body); $xpath = new DOMXPath($doc); diff --git a/src/Factory/Api/Mastodon/Status.php b/src/Factory/Api/Mastodon/Status.php index 102370d5f7..b157d809ac 100644 --- a/src/Factory/Api/Mastodon/Status.php +++ b/src/Factory/Api/Mastodon/Status.php @@ -26,7 +26,6 @@ use Friendica\BaseFactory; use Friendica\Content\Text\BBCode; use Friendica\Database\DBA; use Friendica\DI; -use Friendica\Model\Item; use Friendica\Model\Post; use Friendica\Model\Verb; use Friendica\Network\HTTPException; diff --git a/src/Model/Item.php b/src/Model/Item.php index 32da545d86..ee4474b45d 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -21,6 +21,8 @@ namespace Friendica\Model; +use Exception; +use Friendica\Content\PageInfo; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\HTML; use Friendica\Core\Hook; @@ -34,6 +36,7 @@ use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Post; +use Friendica\Model\Post\Media; use Friendica\Protocol\Activity; use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Diaspora; @@ -177,8 +180,10 @@ class Item while ($item = DBA::fetch($items)) { if (!empty($fields['body'])) { + Post\Media::insertFromAttachmentData($item['uri-id'], $fields['body']); + $content_fields = ['raw-body' => trim($fields['raw-body'] ?? $fields['body'])]; - + // Remove all media attachments from the body and store them in the post-media table $content_fields['raw-body'] = Post\Media::insertFromBody($item['uri-id'], $content_fields['raw-body']); $content_fields['raw-body'] = self::setHashtags($content_fields['raw-body']); @@ -508,7 +513,7 @@ class Item public static function isValid(array $item) { // When there is no content then we don't post it - if ($item['body'] . $item['title'] == '') { + if (($item['body'] . $item['title'] == '') && !Post\Media::existsByURIId($item['uri-id'])) { Logger::notice('No body, no title.'); return false; } @@ -955,6 +960,8 @@ class Item self::setOwnerforResharedItem($item); } + Post\Media::insertFromAttachmentData($item['uri-id'], $item['body']); + // Remove all media attachments from the body and store them in the post-media table $item['raw-body'] = Post\Media::insertFromBody($item['uri-id'], $item['raw-body']); $item['raw-body'] = self::setHashtags($item['raw-body']); @@ -1074,7 +1081,7 @@ class Item Hook::callAll('post_local_end', $posted_item); } else { Hook::callAll('post_remote_end', $posted_item); - } + } } if ($posted_item['gravity'] === GRAVITY_PARENT) { @@ -1099,7 +1106,7 @@ class Item if ($transmit) { // Don't relay participation messages - if (($posted_item['verb'] == Activity::FOLLOW) && + if (($posted_item['verb'] == Activity::FOLLOW) && (!$posted_item['origin'] || ($posted_item['author-id'] != Contact::getPublicIdByUserId($uid)))) { Logger::info('Participation messages will not be relayed', ['item' => $posted_item['id'], 'uri' => $posted_item['uri'], 'verb' => $posted_item['verb']]); $transmit = false; @@ -1647,7 +1654,7 @@ class Item // or it had been done by a "regular" contact. if (!empty($arr['wall'])) { $condition = ['id' => $arr['contact-id']]; - } else { + } else { $condition = ['id' => $arr['contact-id'], 'self' => false]; } DBA::update('contact', ['failed' => false, 'success_update' => $arr['received'], 'last-item' => $arr['received']], $condition); @@ -1782,7 +1789,7 @@ class Item } } } - + if (!$mention) { if (($community_page || $prvgroup) && !$item['wall'] && !$item['origin'] && ($item['gravity'] == GRAVITY_PARENT)) { @@ -2442,10 +2449,10 @@ class Item /** * Get a permission SQL string for the given user - * - * @param int $owner_id - * @param string $table - * @return string + * + * @param int $owner_id + * @param string $table + * @return string */ public static function getPermissionsSQLByUserId(int $owner_id, string $table = '') { @@ -2633,7 +2640,10 @@ class Item unset($hook_data); } + $orig_body = $item['body']; + $item['body'] = preg_replace("/\s*\[attachment .*\].*?\[\/attachment\]\s*/ism", '', $item['body']); self::putInCache($item); + $item['body'] = $orig_body; $s = $item["rendered-html"]; $hook_data = [ @@ -2653,7 +2663,24 @@ class Item return $s; } - $s = self::addMediaAttachments($item, $s); + $shared = BBCode::fetchShareAttributes($item['body']); + if (!empty($shared['guid'])) { + $shared_item = Post::selectFirst(['uri-id', 'plink'], ['guid' => $shared['guid']]); + $shared_uri_id = $shared_item['uri-id'] ?? 0; + $shared_plink = $shared_item['plink'] ?? ''; + $attachments = Post\Media::splitAttachments($shared_uri_id); + $s = self::addVisualAttachments($attachments, $item, $s, true); + $s = self::addLinkAttachment($attachments, $item, $s, true, ''); + $s = self::addNonVisualAttachments($attachments, $item, $s, true); + } else { + $shared_uri_id = 0; + $shared_plink = ''; + } + + $attachments = Post\Media::splitAttachments($item['uri-id']); + $s = self::addVisualAttachments($attachments, $item, $s, false); + $s = self::addLinkAttachment($attachments, $item, $s, false, $shared_plink); + $s = self::addNonVisualAttachments($attachments, $item, $s, false); // Map. if (strpos($s, '
') !== false && !empty($item['coord'])) { @@ -2678,45 +2705,55 @@ class Item } /** - * Add media attachments to the content + * Check if the body contains a link * - * @param array $item - * @param string $content - * @return modified content + * @param string $body + * @param string $url + * @return bool */ - private static function addMediaAttachments(array $item, string $content) + public static function containsLink(string $body, string $url) + { + if (strpos($body, $url)) { + return true; + } + foreach ([0, 1, 2] as $size) { + if (preg_match('#/photo/.*-' . $size . '\.#ism', $url) && + strpos(preg_replace('#(/photo/.*)-[012]\.#ism', '$1-' . $size . '.', $body), $url)) { + return true; + } + } + return false; + } + + /** + * Add visual attachments to the content + * + * @param array $attachments + * @param array $item + * @param string $content + * @return string modified content + */ + private static function addVisualAttachments(array $attachments, array $item, string $content, bool $shared) { $leading = ''; $trailing = ''; - // currently deactivated the request for Post\Media::VIDEO since it creates mutliple videos from Peertube - foreach (Post\Media::getByURIId($item['uri-id'], [Post\Media::AUDIO, - Post\Media::DOCUMENT, Post\Media::TORRENT, Post\Media::UNKNOWN]) as $attachment) { - if (in_array($attachment['type'], [Post\Media::AUDIO, Post\Media::VIDEO]) && strpos($item['body'], $attachment['url'])) { + + foreach ($attachments['visual'] as $attachment) { + if (self::containsLink($item['body'], $attachment['url'])) { continue; } - - $mime = $attachment['mimetype']; - + $author = ['uid' => 0, 'id' => $item['author-id'], 'network' => $item['author-network'], 'url' => $item['author-link']]; $the_url = Contact::magicLinkByContact($author, $attachment['url']); - $filetype = strtolower(substr($mime, 0, strpos($mime, '/'))); - if ($filetype) { - $filesubtype = strtolower(substr($mime, strpos($mime, '/') + 1)); - $filesubtype = str_replace('.', '-', $filesubtype); - } else { - $filetype = 'unkn'; - $filesubtype = 'unkn'; - } - - if (($filetype == 'video')) { + if (($attachment['filetype'] == 'video')) { /// @todo Move the template to /content as well $media = Renderer::replaceMacros(Renderer::getMarkupTemplate('video_top.tpl'), [ '$video' => [ 'id' => $item['author-id'], 'src' => $the_url, - 'mime' => $mime, + 'mime' => $attachment['mimetype'], ], ]); if ($item['post-type'] == Item::PT_VIDEO) { @@ -2724,12 +2761,12 @@ class Item } else { $trailing .= $media; } - } elseif ($filetype == 'audio') { + } elseif ($attachment['filetype'] == 'audio') { $media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/audio.tpl'), [ '$audio' => [ 'id' => $item['author-id'], 'src' => $the_url, - 'mime' => $mime, + 'mime' => $attachment['mimetype'], ], ]); if ($item['post-type'] == Item::PT_AUDIO) { @@ -2737,21 +2774,119 @@ class Item } else { $trailing .= $media; } - } else { - $title = Strings::escapeHtml(trim(($attachment['description'] ?? '') ?: $attachment['url'])); - - if (!empty($attachment['size'])) { - $title .= ' ' . $attachment['size'] . ' ' . DI::l10n()->t('bytes'); - } - - /// @todo Use a template - $icon = '
'; - $trailing .= '' . $icon . ''; + } elseif ($attachment['filetype'] == 'image') { + $media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image.tpl'), [ + '$image' => [ + 'src' => $the_url, + 'attachment' => $attachment, + ], + ]); + $trailing .= $media; } } - if ($leading != '') { - $content = '
' . $leading . '
' . $content; + if ($shared) { + $content = str_replace(BBCode::ANCHOR, '
' . $leading . '
' . BBCode::ANCHOR, $content); + $content = str_replace(BBCode::ANCHOR, BBCode::ANCHOR . '
' . $trailing . '
', $content); + } else { + if ($leading != '') { + $content = '
' . $leading . '
' . $content; + } + + if ($trailing != '') { + $content .= '
' . $trailing . '
'; + } + } + + return $content; + } + + /** + * Add link attachment to the content + * + * @param array $attachments + * @param array $item + * @param string $content + * @param bool $shared + * @return string modified content + */ + private static function addLinkAttachment(array $attachments, array $item, string $content, bool $shared, string $ignore_link) + { + // @ToDo Check only for audio and video + $preview = empty($attachments['visual']); + + if (!empty($attachments['link'])) { + foreach ($attachments['link'] as $link) { + if (!Strings::compareLink($link['url'], $ignore_link)) { + $attachment = $link; + } + } + } + + if (!empty($attachment)) { + $data = [ + 'author_img' => $attachment['author-image'] ?? '', + 'author_name' => $attachment['author-name'] ?? '', + 'author_url' => $attachment['author-url'] ?? '', + 'publisher_img' => $attachment['publisher-image'] ?? '', + 'publisher_name' => $attachment['publisher-name'] ?? '', + 'publisher_url' => $attachment['publisher-url'] ?? '', + 'text' => $attachment['description'] ?? '', + 'title' => $attachment['name'] ?? '', + 'type' => 'link', + 'url' => $attachment['url'] ?? '', + ]; + + if ($preview && !empty($attachment['preview']) && !empty($attachment['preview-height']) && !empty($attachment['preview-width'])) { + $data['images'][] = ['src' => $attachment['preview'], + 'width' => $attachment['preview-width'], 'height' => $attachment['preview-height']]; + } + $footer = PageInfo::getFooterFromData($data); + } elseif (preg_match("/.*(\[attachment.*?\].*?\[\/attachment\]).*/ism", $item['body'], $match)) { + $footer = $match[1]; + } + + if (!empty($footer)) { + // @todo Use a template + $rendered = BBCode::convert($footer); + if ($shared) { + return str_replace(BBCode::ANCHOR, BBCode::ANCHOR . $rendered, $content); + } else { + return $content . $rendered; + } + } + return $content; + } + + /** + * Add non visual attachments to the content + * + * @param array $attachments + * @param array $item + * @param string $content + * @return string modified content + */ + private static function addNonVisualAttachments(array $attachments, array $item, string $content) + { + $trailing = ''; + foreach ($attachments['additional'] as $attachment) { + if (strpos($item['body'], $attachment['url'])) { + continue; + } + + $author = ['uid' => 0, 'id' => $item['author-id'], + 'network' => $item['author-network'], 'url' => $item['author-link']]; + $the_url = Contact::magicLinkByContact($author, $attachment['url']); + + $title = Strings::escapeHtml(trim(($attachment['description'] ?? '') ?: $attachment['url'])); + + if (!empty($attachment['size'])) { + $title .= ' ' . $attachment['size'] . ' ' . DI::l10n()->t('bytes'); + } + + /// @todo Use a template + $icon = '
'; + $trailing .= '' . $icon . ''; } if ($trailing != '') { @@ -2849,8 +2984,8 @@ class Item } /** - * Return the URI for a link to the post - * + * Return the URI for a link to the post + * * @param string $uri URI or link to post * * @return string URI diff --git a/src/Model/ItemURI.php b/src/Model/ItemURI.php index a120bf02bf..460ee14470 100644 --- a/src/Model/ItemURI.php +++ b/src/Model/ItemURI.php @@ -76,6 +76,27 @@ class ItemURI return self::insert(['uri' => $uri]); } + return $itemuri['id']; + } + /** + * Searched for an id of a given guid. + * + * @param string $guid + * + * @return integer item-uri id + * @throws \Exception + */ + public static function getIdByGUID($guid) + { + // If the GUID gets too long we only take the first parts and hope for best + $guid = substr($guid, 0, 255); + + $itemuri = DBA::selectFirst('item-uri', ['id'], ['guid' => $guid]); + + if (!DBA::isResult($itemuri)) { + return 0; + } + return $itemuri['id']; } } diff --git a/src/Model/Post.php b/src/Model/Post.php index d56674c3e0..864e2a5742 100644 --- a/src/Model/Post.php +++ b/src/Model/Post.php @@ -406,7 +406,7 @@ class Post if (!DBA::isResult($postthreaduser)) { return $postthreaduser; } - + $pinned = []; while ($useritem = DBA::fetch($postthreaduser)) { $pinned[] = $useritem['uri-id']; diff --git a/src/Model/Post/Media.php b/src/Model/Post/Media.php index 8601be02a3..82525dc577 100644 --- a/src/Model/Post/Media.php +++ b/src/Model/Post/Media.php @@ -21,12 +21,15 @@ namespace Friendica\Model\Post; +use Friendica\Content\PageInfo; +use Friendica\Content\Text\BBCode; use Friendica\Core\Logger; use Friendica\Core\System; use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Util\Images; +use Friendica\Util\ParseUrl; /** * Class Media @@ -62,7 +65,7 @@ class Media } // "document" has got the lowest priority. So when the same file is both attached as document - // and embedded as picture then we only store the picture or replace the document + // and embedded as picture then we only store the picture or replace the document $found = DBA::selectFirst('post-media', ['type'], ['uri-id' => $media['uri-id'], 'url' => $media['url']]); if (!$force && !empty($found) && (($found['type'] != self::DOCUMENT) || ($media['type'] == self::DOCUMENT))) { Logger::info('Media already exists', ['uri-id' => $media['uri-id'], 'url' => $media['url'], 'callstack' => System::callstack()]); @@ -188,13 +191,27 @@ class Media $media = self::addType($media); } + if ($media['type'] == self::HTML) { + $data = ParseUrl::getSiteinfoCached($media['url'], false); + $media['preview'] = $data['images'][0]['src'] ?? null; + $media['preview-height'] = $data['images'][0]['height'] ?? null; + $media['preview-width'] = $data['images'][0]['width'] ?? null; + $media['description'] = $data['text'] ?? null; + $media['name'] = $data['title'] ?? null; + $media['author-url'] = $data['author_url'] ?? null; + $media['author-name'] = $data['author_name'] ?? null; + $media['author-image'] = $data['author_img'] ?? null; + $media['publisher-url'] = $data['publisher_url'] ?? null; + $media['publisher-name'] = $data['publisher_name'] ?? null; + $media['publisher-image'] = $data['publisher_img'] ?? null; + } return $media; } /** * Add the detected type to the media array * - * @param array $data + * @param array $data * @return array data array with the detected type */ public static function addType(array $data) @@ -274,7 +291,7 @@ class Media } $body = str_replace($picture[0], '', $body); $image = str_replace('-1.', '-0.', $picture[2]); - $attachments[] = ['uri-id' => $uriid, 'type' => self::IMAGE, 'url' => $image, + $attachments[$image] = ['uri-id' => $uriid, 'type' => self::IMAGE, 'url' => $image, 'preview' => $picture[2], 'description' => $picture[3]]; } } @@ -282,7 +299,7 @@ class Media if (preg_match_all("/\[img=([^\[\]]*)\]([^\[\]]*)\[\/img\]/Usi", $body, $pictures, PREG_SET_ORDER)) { foreach ($pictures as $picture) { $body = str_replace($picture[0], '', $body); - $attachments[] = ['uri-id' => $uriid, 'type' => self::IMAGE, 'url' => $picture[1], 'description' => $picture[2]]; + $attachments[$picture[1]] = ['uri-id' => $uriid, 'type' => self::IMAGE, 'url' => $picture[1], 'description' => $picture[2]]; } } @@ -293,7 +310,7 @@ class Media } $body = str_replace($picture[0], '', $body); $image = str_replace('-1.', '-0.', $picture[2]); - $attachments[] = ['uri-id' => $uriid, 'type' => self::IMAGE, 'url' => $image, + $attachments[$image] = ['uri-id' => $uriid, 'type' => self::IMAGE, 'url' => $image, 'preview' => $picture[2], 'description' => null]; } } @@ -301,24 +318,30 @@ class Media if (preg_match_all("/\[img\]([^\[\]]*)\[\/img\]/ism", $body, $pictures, PREG_SET_ORDER)) { foreach ($pictures as $picture) { $body = str_replace($picture[0], '', $body); - $attachments[] = ['uri-id' => $uriid, 'type' => self::IMAGE, 'url' => $picture[1]]; + $attachments[$picture[1]] = ['uri-id' => $uriid, 'type' => self::IMAGE, 'url' => $picture[1]]; } } if (preg_match_all("/\[audio\]([^\[\]]*)\[\/audio\]/ism", $body, $audios, PREG_SET_ORDER)) { foreach ($audios as $audio) { $body = str_replace($audio[0], '', $body); - $attachments[] = ['uri-id' => $uriid, 'type' => self::AUDIO, 'url' => $audio[1]]; + $attachments[$audio[1]] = ['uri-id' => $uriid, 'type' => self::AUDIO, 'url' => $audio[1]]; } } if (preg_match_all("/\[video\]([^\[\]]*)\[\/video\]/ism", $body, $videos, PREG_SET_ORDER)) { foreach ($videos as $video) { $body = str_replace($video[0], '', $body); - $attachments[] = ['uri-id' => $uriid, 'type' => self::VIDEO, 'url' => $video[1]]; + $attachments[$video[1]] = ['uri-id' => $uriid, 'type' => self::VIDEO, 'url' => $video[1]]; } } + $url = PageInfo::getRelevantUrlFromBody($body); + if (!empty($url)) { + Logger::debug('Got page url', ['url' => $url]); + $attachments[$url] = ['uri-id' => $uriid, 'type' => self::UNKNOWN, 'url' => $url]; + } + foreach ($attachments as $attachment) { self::insert($attachment); } @@ -326,6 +349,38 @@ class Media return trim($body); } + /** + * Add media links from the attachment field + * + * @param integer $uriid + * @param string $body + */ + public static function insertFromAttachmentData(int $uriid, string $body) + { + $data = BBCode::getAttachmentData($body); + if (empty($data)) { + return; + } + + Logger::info('Adding attachment data', ['data' => $data]); + $attachment = [ + 'uri-id' => $uriid, + 'type' => self::HTML, + 'url' => $data['url'], + 'preview' => $data['preview'] ?? null, + 'description' => $data['description'] ?? null, + 'name' => $data['title'] ?? null, + 'author-url' => $data['author_url'] ?? null, + 'author-name' => $data['author_name'] ?? null, + 'publisher-url' => $data['provider_url'] ?? null, + 'publisher-name' => $data['provider_name'] ?? null, + ]; + if (!empty($data['image'])) { + $attachment['preview'] = $data['image']; + } + self::insert($attachment); + } + /** * Add media links from the attach field * @@ -369,4 +424,67 @@ class Media return DBA::selectToArray('post-media', [], $condition); } + + /** + * Checks if media attachments are associated with the provided item ID. + * + * @param int $uri_id + * @param array $types + * @return array + * @throws \Exception + */ + public static function existsByURIId(int $uri_id, array $types = []) + { + $condition = ['uri-id' => $uri_id]; + + if (!empty($types)) { + $condition = DBA::mergeConditions($condition, ['type' => $types]); + } + + return DBA::exists('post-media', $condition); + } + + /** + * Split the attachment media in the three segments "visual", "link" and "additional" + * + * @param int $uri_id + * @return array attachments + */ + public static function splitAttachments(int $uri_id) + { + $attachments = ['visual' => [], 'link' => [], 'additional' => []]; + + $media = self::getByURIId($uri_id); + if (empty($media)) { + return $attachments; + } + + foreach ($media as $medium) { + $type = explode('/', current(explode(';', $medium['mimetype']))); + if (count($type) < 2) { + Logger::info('Unknown MimeType', ['type' => $type, 'media' => $medium]); + $filetype = 'unkn'; + $subtype = 'unkn'; + } else { + $filetype = strtolower($type[0]); + $subtype = strtolower($type[1]); + } + + $medium['filetype'] = $filetype; + $medium['subtype'] = $subtype; + + if ($medium['type'] == self::HTML || (($filetype == 'text') && ($subtype == 'html'))) { + $attachments['link'][] = $medium; + continue; + } + + if (in_array($medium['type'], [self::AUDIO, self::VIDEO, self::IMAGE]) || + in_array($filetype, ['audio', 'video', 'image'])) { + $attachments['visual'][] = $medium; + } else { + $attachments['additional'][] = $medium; + } + } + return $attachments; + } } diff --git a/src/Protocol/ActivityPub/Processor.php b/src/Protocol/ActivityPub/Processor.php index fdb97337fd..e36e2e7038 100644 --- a/src/Protocol/ActivityPub/Processor.php +++ b/src/Protocol/ActivityPub/Processor.php @@ -135,113 +135,20 @@ class Processor } /** - * Add attachment data to the item array + * Stire attachment data * * @param array $activity * @param array $item - * - * @return array array */ - private static function constructAttachList($activity, $item) + private static function storeAttachments($activity, $item) { if (empty($activity['attachments'])) { - return $item; + return; } - $leading = ''; - $trailing = ''; - foreach ($activity['attachments'] as $attach) { - switch ($attach['type']) { - case 'link': - $data = [ - 'url' => $attach['url'], - 'type' => $attach['type'], - 'title' => $attach['title'] ?? '', - 'text' => $attach['desc'] ?? '', - 'image' => $attach['image'] ?? '', - 'images' => [], - 'keywords' => [], - ]; - $item['body'] = PageInfo::appendDataToBody($item['body'], $data); - break; - default: - self::storeAttachmentAsMedia($item['uri-id'], $attach); - - $filetype = strtolower(substr($attach['mediaType'], 0, strpos($attach['mediaType'], '/'))); - if ($filetype == 'image') { - if (!empty($activity['source'])) { - foreach ([0, 1, 2] as $size) { - if (preg_match('#/photo/.*-' . $size . '\.#ism', $attach['url']) && - strpos(preg_replace('#(/photo/.*)-[012]\.#ism', '$1-' . $size . '.', $activity['source']), $attach['url'])) { - continue 3; - } - } - if (strpos($activity['source'], $attach['url'])) { - continue 2; - } - } - - // image is the preview/thumbnail URL - if (!empty($attach['image'])) { - $media = '[url=' . $attach['url'] . ']'; - $attach['url'] = $attach['image']; - } else { - $media = ''; - } - - if (empty($attach['name'])) { - $media .= '[img]' . $attach['url'] . '[/img]'; - } else { - $media .= '[img=' . $attach['url'] . ']' . $attach['name'] . '[/img]'; - } - - if (!empty($attach['image'])) { - $media .= '[/url]'; - } - - if ($item['post-type'] == Item::PT_IMAGE) { - $leading .= $media; - } else { - $trailing .= $media; - } - } elseif ($filetype == 'audio') { - if (!empty($activity['source']) && strpos($activity['source'], $attach['url'])) { - continue 2; - } - - if ($item['post-type'] == Item::PT_AUDIO) { - $leading .= '[audio]' . $attach['url'] . "[/audio]\n"; - } else { - $trailing .= '[audio]' . $attach['url'] . "[/audio]\n"; - } - } elseif ($filetype == 'video') { - if (!empty($activity['source']) && strpos($activity['source'], $attach['url'])) { - continue 2; - } - - if ($item['post-type'] == Item::PT_VIDEO) { - $leading .= '[video]' . $attach['url'] . "[/video]\n"; - } else { - $trailing .= '[video]' . $attach['url'] . "[/video]\n"; - } - } - } + self::storeAttachmentAsMedia($item['uri-id'], $attach); } - - if (!empty($leading) && !empty(trim($item['body']))) { - $item['body'] = $leading . "[hr]\n" . $item['body']; - } elseif (!empty($leading)) { - $item['body'] = $leading; - } - - if (!empty($trailing) && !empty(trim($item['body']))) { - $item['body'] = $item['body'] . "\n[hr]" . $trailing; - } elseif (!empty($trailing)) { - $item['body'] = $trailing; - } - - return $item; } /** @@ -265,7 +172,7 @@ class Processor $item = self::processContent($activity, $item); - $item = self::constructAttachList($activity, $item); + self::storeAttachments($activity, $item); if (empty($item)) { return; @@ -399,7 +306,7 @@ class Processor $item['plink'] = $activity['alternate-url'] ?? $item['uri']; - $item = self::constructAttachList($activity, $item); + self::storeAttachments($activity, $item); // We received the post via AP, so we set the protocol of the server to AP $contact = Contact::getById($item['author-id'], ['gsid']); @@ -863,12 +770,12 @@ class Processor $object = ActivityPub::fetchContent($url, $uid); if (empty($object)) { - Logger::log('Activity ' . $url . ' was not fetchable, aborting.'); + Logger::notice('Activity was not fetchable, aborting.', ['url' => $url]); return ''; } if (empty($object['id'])) { - Logger::log('Activity ' . $url . ' has got not id, aborting. ' . json_encode($object)); + Logger::notice('Activity has got not id, aborting. ', ['url' => $url, 'object' => $object]); return ''; } @@ -1019,7 +926,7 @@ class Processor DBA::update('contact', ['hub-verify' => $activity['id'], 'protocol' => Protocol::ACTIVITYPUB], ['id' => $cid]); } - Logger::log('Follow user ' . $uid . ' from contact ' . $cid . ' with id ' . $activity['id']); + Logger::notice('Follow user ' . $uid . ' from contact ' . $cid . ' with id ' . $activity['id']); } /** diff --git a/src/Protocol/Diaspora.php b/src/Protocol/Diaspora.php index 4b00ead002..2a4dfe75e7 100644 --- a/src/Protocol/Diaspora.php +++ b/src/Protocol/Diaspora.php @@ -22,7 +22,6 @@ namespace Friendica\Protocol; use Friendica\Content\Feature; -use Friendica\Content\PageInfo; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\Markdown; use Friendica\Core\Cache\Duration; @@ -1067,7 +1066,7 @@ class Diaspora * 'key' => The public key of the author * @throws \Exception */ - private static function message($guid, $server, $level = 0) + public static function message($guid, $server, $level = 0) { if ($level > 5) { return false; @@ -2303,9 +2302,6 @@ class Diaspora $item["body"] = self::replacePeopleGuid($item["body"], $item["author-link"]); - // Add OEmbed and other information to the body - $item["body"] = PageInfo::searchAndAppendToBody($item["body"], false, true); - return $item; } else { return $item; @@ -2489,7 +2485,7 @@ class Diaspora Tag::storeFromBody($datarray['uri-id'], $datarray["body"]); - Post\Media::copy($original_item['uri-id'], $datarray['uri-id']); + //Post\Media::copy($original_item['uri-id'], $datarray['uri-id']); $datarray["app"] = $original_item["app"]; $datarray["plink"] = self::plink($author, $guid); @@ -2733,8 +2729,8 @@ class Diaspora if ($data->photo) { foreach ($data->photo as $photo) { self::storePhotoAsMedia($datarray['uri-id'], $photo); - $body = "[img]".XML::unescape($photo->remote_photo_path). - XML::unescape($photo->remote_photo_name)."[/img]\n".$body; + //$body = "[img]".XML::unescape($photo->remote_photo_path). + // XML::unescape($photo->remote_photo_name)."[/img]\n".$body; } $datarray["object-type"] = Activity\ObjectType::IMAGE; @@ -2742,11 +2738,6 @@ class Diaspora } else { $datarray["object-type"] = Activity\ObjectType::NOTE; $datarray["post-type"] = Item::PT_NOTE; - - // Add OEmbed and other information to the body - if (!self::isHubzilla($contact["url"])) { - $body = PageInfo::searchAndAppendToBody($body, false, true); - } } /// @todo enable support for polls diff --git a/src/Protocol/OStatus.php b/src/Protocol/OStatus.php index 13e1d4de27..7ae98f070b 100644 --- a/src/Protocol/OStatus.php +++ b/src/Protocol/OStatus.php @@ -23,7 +23,6 @@ namespace Friendica\Protocol; use DOMDocument; use DOMXPath; -use Friendica\Content\PageInfo; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\HTML; use Friendica\Core\Cache\Duration; @@ -673,11 +672,6 @@ class OStatus $item["body"] .= $add_body; - // Only add additional data when there is no picture in the post - if (!strstr($item["body"], '[/img]')) { - $item["body"] = PageInfo::searchAndAppendToBody($item["body"]); - } - Tag::storeFromBody($item['uri-id'], $item['body']); // Mastodon Content Warning @@ -1098,7 +1092,9 @@ class OStatus if (($item["object-type"] == Activity\ObjectType::QUESTION) || ($item["object-type"] == Activity\ObjectType::EVENT) ) { - $item["body"] .= "\n" . PageInfo::getFooterFromUrl($attribute['href']); + Post\Media::insert(['uri-id' => $item['uri-id'], 'type' => Post\Media::UNKNOWN, + 'url' => $attribute['href'], 'mimetype' => $attribute['type'] ?? null, + 'size' => $attribute['length'] ?? null, 'description' => $attribute['title'] ?? null]); } break; case "ostatus:conversation": @@ -1125,7 +1121,9 @@ class OStatus } $link_data['related'] = $attribute['href']; } else { - $item["body"] .= "\n" . PageInfo::getFooterFromUrl($attribute['href']); + Post\Media::insert(['uri-id' => $item['uri-id'], 'type' => Post\Media::UNKNOWN, + 'url' => $attribute['href'], 'mimetype' => $attribute['type'] ?? null, + 'size' => $attribute['length'] ?? null, 'description' => $attribute['title'] ?? null]); } break; case "self": diff --git a/src/Util/ParseUrl.php b/src/Util/ParseUrl.php index 9ce9b0e9f9..77611168d4 100644 --- a/src/Util/ParseUrl.php +++ b/src/Util/ParseUrl.php @@ -54,7 +54,7 @@ class ParseUrl /** * Fetch the content type of the given url * @param string $url URL of the page - * @return array content type + * @return array content type */ public static function getContentType(string $url) { @@ -197,7 +197,7 @@ class ParseUrl ]; if ($count > 10) { - Logger::log('Endless loop detected for ' . $url, Logger::DEBUG); + Logger::notice('Endless loop detected', ['url' => $url]); return $siteinfo; } @@ -297,7 +297,7 @@ class ParseUrl // See https://github.com/friendica/friendica/issues/5470#issuecomment-418351211 $charset = str_ireplace('latin-1', 'latin1', $charset); - Logger::log('detected charset ' . $charset, Logger::DEBUG); + Logger::info('detected charset', ['charset' => $charset]); $body = iconv($charset, 'UTF-8//TRANSLIT', $body); } @@ -477,7 +477,7 @@ class ParseUrl } } -// Currently deactivated, see https://github.com/friendica/friendica/pull/10148#issuecomment-821512658 +// Currently deactivated, see https://github.com/friendica/friendica/pull/10148#issuecomment-821512658 // Prevent to have a photo type without an image // if ($twitter_card && $twitter_image && !empty($siteinfo['image'])) { // $siteinfo['type'] = 'photo'; @@ -778,7 +778,7 @@ class ParseUrl case 'QAPage': case 'RealEstateListing': case 'SearchResultsPage': - case 'MediaGallery': + case 'MediaGallery': case 'ImageGallery': case 'VideoGallery': case 'RadioEpisode': @@ -807,7 +807,7 @@ class ParseUrl case 'PerformingGroup': case 'DanceGroup'; case 'MusicGroup': - case 'TheaterGroup': + case 'TheaterGroup': return self::parseJsonLdWebPerson($siteinfo, $jsonld); case 'AudioObject': case 'Audio': diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index 8ec833ca64..e460480652 100644 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -1092,6 +1092,13 @@ return [ "preview-height" => ["type" => "smallint unsigned", "comment" => "Height of the preview picture"], "preview-width" => ["type" => "smallint unsigned", "comment" => "Width of the preview picture"], "description" => ["type" => "text", "comment" => ""], + "name" => ["type" => "varchar(255)", "comment" => "Name of the media"], + "author-url" => ["type" => "varbinary(255)", "comment" => "URL of the author of the media"], + "author-name" => ["type" => "varchar(255)", "comment" => "Name of the author of the media"], + "author-image" => ["type" => "varbinary(255)", "comment" => "Image of the author of the media"], + "publisher-url" => ["type" => "varbinary(255)", "comment" => "URL of the publisher of the media"], + "publisher-name" => ["type" => "varchar(255)", "comment" => "Name of the publisher of the media"], + "publisher-image" => ["type" => "varbinary(255)", "comment" => "Image of the publisher of the media"], ], "indexes" => [ "PRIMARY" => ["id"], @@ -1122,7 +1129,7 @@ return [ "network" => ["type" => "char(4)", "not null" => "1", "default" => "", "comment" => ""], "created" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => ""], "received" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => ""], - "changed" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Date that something in the conversation changed, indicating clients should fetch the conversation again"], + "changed" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Date that something in the conversation changed, indicating clients should fetch the conversation again"], "commented" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => ""] ], "indexes" => [ diff --git a/view/templates/content/image.tpl b/view/templates/content/image.tpl new file mode 100644 index 0000000000..6251761dc0 --- /dev/null +++ b/view/templates/content/image.tpl @@ -0,0 +1,2 @@ +{{$image.attachment.description}} +