X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;ds=sidebyside;f=src%2FModel%2FItem.php;h=1d8742926db6944ddba0600927b55e59ba37ff1e;hb=4889e8248926193b2f5e72060dbf809ee5aca3e6;hp=76402280c2073583950b32cf87cb36a75f3dff54;hpb=1849bf0a120e236624ddc25461f424071ab8c8a2;p=friendica.git diff --git a/src/Model/Item.php b/src/Model/Item.php index 76402280c2..1d8742926d 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -21,6 +21,7 @@ namespace Friendica\Model; +use Friendica\Content\PageInfo; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\HTML; use Friendica\Core\Hook; @@ -40,6 +41,7 @@ use Friendica\Protocol\Diaspora; use Friendica\Util\DateTimeFormat; use Friendica\Util\Map; use Friendica\Util\Network; +use Friendica\Util\Proxy; use Friendica\Util\Strings; use Friendica\Worker\Delivery; use LanguageDetection\Language; @@ -55,20 +57,23 @@ class Item const PT_VIDEO = 18; const PT_DOCUMENT = 19; const PT_EVENT = 32; - const PT_TAG = 64; - const PT_TO = 65; - const PT_CC = 66; - const PT_BTO = 67; - const PT_BCC = 68; - const PT_FOLLOWER = 69; - const PT_ANNOUNCEMENT = 70; - const PT_COMMENT = 71; - const PT_STORED = 72; - const PT_GLOBAL = 73; - const PT_RELAY = 74; - const PT_FETCHED = 75; const PT_PERSONAL_NOTE = 128; + // Posting reasons (Why had a post been stored for a user?) + const PR_NONE = 0; + const PR_TAG = 64; + const PR_TO = 65; + const PR_CC = 66; + const PR_BTO = 67; + const PR_BCC = 68; + const PR_FOLLOWER = 69; + const PR_ANNOUNCEMENT = 70; + const PR_COMMENT = 71; + const PR_STORED = 72; + const PR_GLOBAL = 73; + const PR_RELAY = 74; + const PR_FETCHED = 75; + // Field list that is used to display the items const DISPLAY_FIELDLIST = [ 'uid', 'id', 'parent', 'guid', 'network', 'gravity', @@ -92,7 +97,7 @@ class Item const DELIVER_FIELDLIST = ['uid', 'id', 'parent', 'uri-id', 'uri', 'thr-parent', 'parent-uri', 'guid', 'parent-guid', 'received', 'created', 'edited', 'verb', 'object-type', 'object', 'target', 'private', 'title', 'body', 'raw-body', 'location', 'coord', 'app', - 'inform', 'deleted', 'extid', 'post-type', 'gravity', + 'inform', 'deleted', 'extid', 'post-type', 'post-reason', 'gravity', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'author-id', 'author-link', 'author-name', 'author-avatar', 'owner-id', 'owner-link', 'contact-uid', 'signed_text', 'network', 'wall', 'contact-id', 'plink', 'forum_mode', 'origin', @@ -107,7 +112,7 @@ class Item 'contact-id', 'wall', 'gravity', 'extid', 'psid', 'created', 'edited', 'commented', 'received', 'changed', 'verb', 'postopts', 'plink', 'resource-id', 'event-id', 'inform', - 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'post-type', + 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'post-type', 'post-reason', 'private', 'pubmail', 'visible', 'starred', 'unseen', 'deleted', 'origin', 'forum_mode', 'mention', 'global', 'network', 'title', 'content-warning', 'body', 'location', 'coord', 'app', @@ -147,6 +152,10 @@ class Item return false; } + if (isset($fields['extid'])) { + $fields['external-id'] = ItemURI::getIdByURI($fields['extid']); + } + if (!empty($fields['verb'])) { $fields['vid'] = Verb::getID($fields['verb']); } @@ -170,9 +179,12 @@ 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 + // @todo On shared postings (Diaspora style and commented reshare) don't fetch content from the shared part $content_fields['raw-body'] = Post\Media::insertFromBody($item['uri-id'], $content_fields['raw-body']); $content_fields['raw-body'] = self::setHashtags($content_fields['raw-body']); } @@ -501,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; } @@ -744,6 +756,10 @@ class Item return 0; } + if (!isset($item['post-type'])) { + $item['post-type'] = empty($item['title']) ? self::PT_NOTE : self::PT_ARTICLE; + } + $item['wall'] = intval($item['wall'] ?? 0); $item['extid'] = trim($item['extid'] ?? ''); $item['author-name'] = trim($item['author-name'] ?? ''); @@ -762,7 +778,6 @@ class Item $item['coord'] = trim($item['coord'] ?? ''); $item['visible'] = (isset($item['visible']) ? intval($item['visible']) : 1); $item['deleted'] = 0; - $item['post-type'] = ($item['post-type'] ?? '') ?: self::PT_ARTICLE; $item['verb'] = trim($item['verb'] ?? ''); $item['object-type'] = trim($item['object-type'] ?? ''); $item['object'] = trim($item['object'] ?? ''); @@ -810,7 +825,7 @@ class Item $actor = ($item['gravity'] == GRAVITY_PARENT) ? $item['owner-id'] : $item['author-id']; if (!$item['origin'] && ($item['uid'] != 0) && Contact::isSharing($actor, $item['uid'])) { - $item['post-type'] = self::PT_FOLLOWER; + $item['post-reason'] = self::PR_FOLLOWER; } // Ensure that there is an avatar cache @@ -945,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']); @@ -1064,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) { @@ -1089,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; @@ -1115,7 +1132,7 @@ class Item */ private static function setOwnerforResharedItem(array $item) { - $parent = Post::selectFirst(['id', 'causer-id', 'owner-id', 'author-id', 'author-link', 'origin', 'post-type'], + $parent = Post::selectFirst(['id', 'causer-id', 'owner-id', 'author-id', 'author-link', 'origin', 'post-reason'], ['uri-id' => $item['thr-parent-id'], 'uid' => $item['uid']]); if (!DBA::isResult($parent)) { Logger::error('Parent not found', ['uri-id' => $item['thr-parent-id'], 'uid' => $item['uid']]); @@ -1135,7 +1152,7 @@ class Item } if ($author['contact-type'] != Contact::TYPE_COMMUNITY) { - if ($parent['post-type'] == self::PT_ANNOUNCEMENT) { + if ($parent['post-reason'] == self::PR_ANNOUNCEMENT) { Logger::info('The parent is already marked as announced: quit', ['causer' => $parent['causer-id'], 'owner' => $parent['owner-id'], 'author' => $parent['author-id'], 'uid' => $item['uid']]); return; } @@ -1144,8 +1161,8 @@ class Item Logger::info('The resharer is no forum: quit', ['resharer' => $item['author-id'], 'owner' => $parent['owner-id'], 'author' => $parent['author-id'], 'uid' => $item['uid']]); return; } - self::update(['post-type' => self::PT_ANNOUNCEMENT, 'causer-id' => $item['author-id']], ['id' => $parent['id']]); - Logger::info('Set announcement post-type', ['uri-id' => $item['uri-id'], 'thr-parent-id' => $item['thr-parent-id'], 'uid' => $item['uid']]); + self::update(['post-reason' => self::PR_ANNOUNCEMENT, 'causer-id' => $item['author-id']], ['id' => $parent['id']]); + Logger::info('Set announcement post-reason', ['uri-id' => $item['uri-id'], 'thr-parent-id' => $item['thr-parent-id'], 'uid' => $item['uid']]); return; } @@ -1169,7 +1186,7 @@ class Item if (Contact::isSharing($item['author-id'], $uid)) { $fields = []; } else { - $fields = ['post-type' => self::PT_TAG]; + $fields = ['post-reason' => self::PR_TAG]; } $stored = self::storeForUserByUriId($item['uri-id'], $uid, $fields); @@ -1289,7 +1306,7 @@ class Item return 0; } - $item['post-type'] = self::PT_STORED; + $item['post-reason'] = self::PR_STORED; $item = array_merge($item, $fields); @@ -1416,7 +1433,7 @@ class Item unset($item['starred']); unset($item['postopts']); unset($item['inform']); - unset($item['post-type']); + unset($item['post-reason']); if ($item['uri-id'] == $item['parent-uri-id']) { $item['contact-id'] = $item['owner-id']; } else { @@ -1479,7 +1496,7 @@ class Item unset($item['starred']); unset($item['postopts']); unset($item['inform']); - unset($item['post-type']); + unset($item['post-reason']); $item['contact-id'] = Contact::getIdForURL($item['author-link']); $public_shadow = self::insert($item, false, true); @@ -1637,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); @@ -1690,7 +1707,7 @@ class Item return ("[bookmark=" . str_replace("#", "#", $match[1]) . "]" . str_replace("#", "#", $match[2]) . "[/bookmark]"); }, $body); - $body = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism", + $body = preg_replace_callback("/\[attachment (.*?)\](.*?)\[\/attachment\]/ism", function ($match) { return ("[attachment " . str_replace("#", "#", $match[1]) . "]" . $match[2] . "[/attachment]"); }, $body); @@ -1772,7 +1789,7 @@ class Item } } } - + if (!$mention) { if (($community_page || $prvgroup) && !$item['wall'] && !$item['origin'] && ($item['gravity'] == GRAVITY_PARENT)) { @@ -2157,7 +2174,7 @@ class Item // Only expire posts, not photos and photo comments - if (!$expire_photos && (!empty($item['resource-id']) || ($item['post-type'] == self::PT_IMAGE))) { + if (!$expire_photos && !empty($item['resource-id'])) { continue; } elseif (!$expire_starred && intval($item['starred'])) { continue; @@ -2432,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 = '') { @@ -2623,7 +2640,10 @@ class Item unset($hook_data); } + $body = $item['body'] ?? ''; + $item['body'] = preg_replace("/\s*\[attachment .*?\].*?\[\/attachment\]\s*/ism", '', $item['body']); self::putInCache($item); + $item['body'] = $body; $s = $item["rendered-html"]; $hook_data = [ @@ -2643,7 +2663,28 @@ 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_links = [strtolower($shared_item['plink'] ?? '')]; + $attachments = Post\Media::splitAttachments($shared_uri_id, $shared['guid']); + $s = self::addVisualAttachments($attachments, $item, $s, true); + $s = self::addLinkAttachment($attachments, $body, $s, true, []); + $s = self::addNonVisualAttachments($attachments, $item, $s, true); + $shared_links = array_merge($shared_links, array_column($attachments['visual'], 'url')); + $shared_links = array_merge($shared_links, array_column($attachments['link'], 'url')); + $shared_links = array_merge($shared_links, array_column($attachments['additional'], 'url')); + $body = preg_replace("/\s*\[share .*?\].*?\[\/share\]\s*/ism", '', $body); + } else { + $shared_uri_id = 0; + $shared_links = []; + } + + $attachments = Post\Media::splitAttachments($item['uri-id'], $item['guid'], $shared_links); + $s = self::addVisualAttachments($attachments, $item, $s, false); + $s = self::addLinkAttachment($attachments, $body, $s, false, $shared_links); + $s = self::addNonVisualAttachments($attachments, $item, $s, false); // Map. if (strpos($s, '
') !== false && !empty($item['coord'])) { @@ -2668,70 +2709,229 @@ class Item } /** - * Add media attachments to the content + * Check if the body contains a link * + * @param string $body + * @param string $url + * @return bool + */ + 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 modified content + * @return string modified content */ - private static function addMediaAttachments(array $item, string $content) + private static function addVisualAttachments(array $attachments, array $item, string $content, bool $shared) { + $stamp1 = microtime(true); $leading = ''; $trailing = ''; - foreach (Post\Media::getByURIId($item['uri-id'], [Post\Media::DOCUMENT, Post\Media::TORRENT, Post\Media::UNKNOWN]) as $attachment) { - $mime = $attachment['mimetype']; + + // @todo In the future we should make a single for the template engine with all media in it. This allows more flexibilty. + foreach ($attachments['visual'] as $attachment) { + if (self::containsLink($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']); - $filetype = strtolower(substr($mime, 0, strpos($mime, '/'))); - if ($filetype) { - $filesubtype = strtolower(substr($mime, strpos($mime, '/') + 1)); - $filesubtype = str_replace('.', '-', $filesubtype); + if (!empty($attachment['preview'])) { + $preview_url = Proxy::proxifyUrl(Contact::magicLinkByContact($author, $attachment['preview'])); } else { - $filetype = 'unkn'; - $filesubtype = 'unkn'; + $preview_url = ''; } - if (($filetype == 'video')) { + if (($attachment['filetype'] == 'video')) { /// @todo Move the template to /content as well - $leading .= Renderer::replaceMacros(Renderer::getMarkupTemplate('video_top.tpl'), [ + $media = Renderer::replaceMacros(Renderer::getMarkupTemplate('video_top.tpl'), [ '$video' => [ - 'id' => $item['author-id'], - 'src' => $the_url, - 'mime' => $mime, + 'id' => $attachment['id'], + 'src' => $the_url, + 'name' => $attachment['name'] ?: $attachment['url'], + 'preview' => $preview_url, + 'mime' => $attachment['mimetype'], ], ]); - } elseif ($filetype == 'audio') { - $leading .= Renderer::replaceMacros(Renderer::getMarkupTemplate('content/audio.tpl'), [ + if ($item['post-type'] == Item::PT_VIDEO) { + $leading .= $media; + } else { + $trailing .= $media; + } + } elseif ($attachment['filetype'] == 'audio') { + $media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/audio.tpl'), [ '$audio' => [ - 'id' => $item['author-id'], + 'id' => $attachment['id'], 'src' => $the_url, - 'mime' => $mime, + 'name' => $attachment['name'] ?: $attachment['url'], + 'mime' => $attachment['mimetype'], ], ]); - } else { - $title = Strings::escapeHtml(trim(($attachment['description'] ?? '') ?: $attachment['url'])); + if ($item['post-type'] == Item::PT_AUDIO) { + $leading .= $media; + } else { + $trailing .= $media; + } + } elseif ($attachment['filetype'] == 'image') { + if (empty($preview_url) && (max($attachment['width'], $attachment['height']) > 600)) { + $preview_url = Proxy::proxifyUrl($the_url, false, ($attachment['width'] > $attachment['height']) ? Proxy::SIZE_MEDIUM : Proxy::SIZE_LARGE); + } + $media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image.tpl'), [ + '$image' => [ + 'src' => Proxy::proxifyUrl($the_url), + 'preview' => $preview_url, + 'attachment' => $attachment, + ], + ]); + // On Diaspora posts the attached pictures are leading + if ($item['network'] == Protocol::DIASPORA) { + $leading .= $media; + } else { + $trailing .= $media; + } + } + } - if (!empty($attachment['size'])) { - $title .= ' ' . $attachment['size'] . ' ' . DI::l10n()->t('bytes'); + if ($shared) { + $content = str_replace(BBCode::TOP_ANCHOR, '
' . $leading . '
' . BBCode::TOP_ANCHOR, $content); + $content = str_replace(BBCode::BOTTOM_ANCHOR, '
' . $trailing . '
' . BBCode::BOTTOM_ANCHOR, $content); + } else { + if ($leading != '') { + $content = '
' . $leading . '
' . $content; + } + + if ($trailing != '') { + $content .= '
' . $trailing . '
'; + } + } + + DI::profiler()->saveTimestamp($stamp1, 'rendering'); + return $content; + } + + /** + * Add link attachment to the content + * + * @param array $attachments + * @param string $body + * @param string $content + * @param bool $shared + * @param array $ignore_links A list of URLs to ignore + * @return string modified content + */ + private static function addLinkAttachment(array $attachments, string $body, string $content, bool $shared, array $ignore_links) + { + $stamp1 = microtime(true); + // @ToDo Check only for audio and video + $preview = empty($attachments['visual']); + + if (!empty($attachments['link'])) { + foreach ($attachments['link'] as $link) { + $found = false; + foreach ($ignore_links as $ignore_link) { + if (Strings::compareLink($link['url'], $ignore_link)) { + $found = true; + } + } + if (!$found) { + $attachment = $link; } + } + } - /// @todo Use a template - $icon = '
'; - $trailing .= '' . $icon . ''; + if (!empty($attachment)) { + $data = [ + 'after' => '', + 'author_name' => $attachment['author-name'] ?? '', + 'author_url' => $attachment['author-url'] ?? '', + 'description' => $attachment['description'] ?? '', + 'image' => '', + 'preview' => '', + 'provider_name' => $attachment['publisher-name'] ?? '', + 'provider_url' => $attachment['publisher-url'] ?? '', + 'text' => '', + 'title' => $attachment['name'] ?? '', + 'type' => 'link', + 'url' => $attachment['url']]; + + if ($preview) { + if ($attachment['preview-width'] >= 500) { + $data['image'] = $attachment['preview'] ?? ''; + } else { + $data['preview'] = $attachment['preview'] ?? ''; + } } + } elseif (preg_match("/.*(\[attachment.*?\].*?\[\/attachment\]).*/ism", $body, $match)) { + $data = BBCode::getAttachmentData($match[1]); } + DI::profiler()->saveTimestamp($stamp1, 'rendering'); - if ($leading != '') { - $content = '
' . $leading . '
' . $content; + if (isset($data['url']) && !in_array($data['url'], $ignore_links)) { + // @todo Use a template + $rendered = BBCode::convertAttachment('', BBCode::INTERNAL, false, $data); + if ($shared) { + return str_replace(BBCode::BOTTOM_ANCHOR, BBCode::BOTTOM_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) + { + $stamp1 = microtime(true); + $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 != '') { $content .= '
' . $trailing . '
'; } + DI::profiler()->saveTimestamp($stamp1, 'rendering'); return $content; } @@ -2823,8 +3023,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