X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FModel%2FItem.php;h=18b56215ce39ca9a680e64fc062f693c84dca392;hb=58e5f0d9c5ba75f92cc4c77b5b27069789f11318;hp=51ac03b4eff2abfe1689faa427f8b7fd143381a6;hpb=6d911a8f395a99337d4317b2441dc529a74d9e45;p=friendica.git diff --git a/src/Model/Item.php b/src/Model/Item.php index 51ac03b4ef..18b56215ce 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -21,6 +21,10 @@ namespace Friendica\Model; +use Friendica\Contact\LocalRelationship\Entity\LocalRelationship; +use Friendica\Content\Image; +use Friendica\Content\Post\Collection\PostMedias; +use Friendica\Content\Post\Entity\PostMedia; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\HTML; use Friendica\Core\Hook; @@ -31,7 +35,9 @@ use Friendica\Core\System; use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; +use Friendica\Model\Post\Category; use Friendica\Network\HTTPException\InternalServerErrorException; +use Friendica\Network\HTTPException\ServiceUnavailableException; use Friendica\Protocol\Activity; use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Delivery; @@ -94,9 +100,9 @@ class Item 'wall', 'private', 'starred', 'origin', 'parent-origin', 'title', 'body', 'language', 'content-warning', 'location', 'coord', 'app', 'rendered-hash', 'rendered-html', 'object', 'quote-uri', 'quote-uri-id', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'mention', 'global', - 'author-id', 'author-link', 'author-name', 'author-avatar', 'author-network', 'author-updated', 'author-gsid', 'author-addr', 'author-uri-id', - 'owner-id', 'owner-link', 'owner-name', 'owner-avatar', 'owner-network', 'owner-contact-type', 'owner-updated', - 'causer-id', 'causer-link', 'causer-name', 'causer-avatar', 'causer-contact-type', 'causer-network', + 'author-id', 'author-link', 'author-alias', 'author-name', 'author-avatar', 'author-network', 'author-updated', 'author-gsid', 'author-baseurl', 'author-addr', 'author-uri-id', + 'owner-id', 'owner-link', 'owner-alias', 'owner-name', 'owner-avatar', 'owner-network', 'owner-contact-type', 'owner-updated', 'owner-gsid', + 'causer-id', 'causer-link', 'causer-alias', 'causer-name', 'causer-avatar', 'causer-contact-type', 'causer-network', 'causer-gsid', 'contact-id', 'contact-uid', 'contact-link', 'contact-name', 'contact-avatar', 'writable', 'self', 'cid', 'alias', 'event-created', 'event-edited', 'event-start', 'event-finish', @@ -108,31 +114,35 @@ class Item ]; // Field list that is used to deliver items via the protocols - const DELIVER_FIELDLIST = ['uid', 'id', 'parent', 'uri-id', 'uri', 'thr-parent', 'parent-uri', 'guid', - 'parent-guid', 'conversation', 'received', 'created', 'edited', 'verb', 'object-type', 'object', 'target', - 'private', 'title', 'body', 'raw-body', 'location', 'coord', 'app', - 'inform', 'deleted', 'extid', 'post-type', 'post-reason', 'gravity', - 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', - 'author-id', 'author-addr', 'author-link', 'author-name', 'author-avatar', 'owner-id', 'owner-link', 'contact-uid', - 'signed_text', 'network', 'wall', 'contact-id', 'plink', 'origin', - 'thr-parent-id', 'parent-uri-id', 'quote-uri', 'quote-uri-id', 'postopts', 'pubmail', - 'event-created', 'event-edited', 'event-start', 'event-finish', - 'event-summary', 'event-desc', 'event-location', 'event-type', - 'event-nofinish', 'event-ignore', 'event-id']; + const DELIVER_FIELDLIST = [ + 'uid', 'id', 'parent', 'uri-id', 'uri', 'thr-parent', 'parent-uri', 'guid', + 'parent-guid', 'conversation', 'received', 'created', 'edited', 'verb', 'object-type', 'object', 'target', + 'private', 'title', 'body', 'raw-body', 'language', 'location', 'coord', 'app', + 'inform', 'deleted', 'extid', 'post-type', 'post-reason', 'gravity', + 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', + 'author-id', 'author-addr', 'author-link', 'author-name', 'author-avatar', 'owner-id', 'owner-link', 'contact-uid', + 'signed_text', 'network', 'wall', 'contact-id', 'plink', 'origin', + 'thr-parent-id', 'parent-uri-id', 'quote-uri', 'quote-uri-id', 'postopts', 'pubmail', + 'event-created', 'event-edited', 'event-start', 'event-finish', + 'event-summary', 'event-desc', 'event-location', 'event-type', + 'event-nofinish', 'event-ignore', 'event-id' + ]; // All fields in the item table - const ITEM_FIELDLIST = ['id', 'uid', 'parent', 'uri', 'parent-uri', 'thr-parent', - 'guid', 'uri-id', 'parent-uri-id', 'thr-parent-id', 'conversation', 'vid', - 'quote-uri', 'quote-uri-id', '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', 'post-reason', - 'private', 'pubmail', 'visible', 'starred', - 'unseen', 'deleted', 'origin', 'mention', 'global', 'network', - 'title', 'content-warning', 'body', 'location', 'coord', 'app', - 'rendered-hash', 'rendered-html', 'object-type', 'object', 'target-type', 'target', - 'author-id', 'author-link', 'author-name', 'author-avatar', 'author-network', - 'owner-id', 'owner-link', 'owner-name', 'owner-avatar', 'causer-id']; + const ITEM_FIELDLIST = [ + 'id', 'uid', 'parent', 'uri', 'parent-uri', 'thr-parent', + 'guid', 'uri-id', 'parent-uri-id', 'thr-parent-id', 'conversation', 'vid', + 'quote-uri', 'quote-uri-id', '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', 'post-reason', + 'private', 'pubmail', 'visible', 'starred', + 'unseen', 'deleted', 'origin', 'mention', 'global', 'network', + 'title', 'content-warning', 'body', 'location', 'coord', 'app', + 'rendered-hash', 'rendered-html', 'object-type', 'object', 'target-type', 'target', + 'author-id', 'author-link', 'author-name', 'author-avatar', 'author-network', + 'owner-id', 'owner-link', 'owner-name', 'owner-avatar', 'causer-id' + ]; // List of all verbs that don't need additional content data. // Never reorder or remove entries from this list. Just add new ones at the end, if needed. @@ -140,7 +150,8 @@ class Item Activity::LIKE, Activity::DISLIKE, Activity::ATTEND, Activity::ATTENDNO, Activity::ATTENDMAYBE, Activity::FOLLOW, - Activity::ANNOUNCE]; + Activity::ANNOUNCE + ]; // Privacy levels const PUBLIC = 0; @@ -191,8 +202,10 @@ class Item } // We only need to call the line by line update for specific fields - if (empty($fields['body']) && empty($fields['file']) && - empty($fields['attach']) && empty($fields['edited'])) { + if ( + empty($fields['body']) && empty($fields['file']) && + empty($fields['attach']) && empty($fields['edited']) + ) { return $rows; } @@ -317,11 +330,13 @@ class Item */ public static function markForDeletionById(int $item_id, int $priority = Worker::PRIORITY_HIGH): bool { - Logger::info('Mark item for deletion by id', ['id' => $item_id, 'callstack' => System::callstack()]); + Logger::info('Mark item for deletion by id', ['id' => $item_id]); // locate item to be deleted - $fields = ['id', 'uri', 'uri-id', 'uid', 'parent', 'parent-uri-id', 'origin', + $fields = [ + 'id', 'uri', 'uri-id', 'uid', 'parent', 'parent-uri-id', 'origin', 'deleted', 'resource-id', 'event-id', - 'verb', 'object-type', 'object', 'target', 'contact-id', 'psid', 'gravity']; + 'verb', 'object-type', 'object', 'target', 'contact-id', 'psid', 'gravity' + ]; $item = Post::selectFirst($fields, ['id' => $item_id]); if (!DBA::isResult($item)) { Logger::info('Item not found.', ['id' => $item_id]); @@ -359,7 +374,7 @@ class Item // If item has attachments, drop them $attachments = Post\Media::getByURIId($item['uri-id'], [Post\Media::DOCUMENT]); - foreach($attachments as $attachment) { + foreach ($attachments as $attachment) { if (preg_match('|attach/(\d+)|', $attachment['url'], $matches)) { Attach::delete(['id' => $matches[1], 'uid' => $item['uid']]); } @@ -473,7 +488,7 @@ class Item private static function contactId(array $item): int { if ($item['uid'] == 0) { - return $item['author-id']; + return $item['owner-id']; } if ($item['origin']) { @@ -481,31 +496,31 @@ class Item return $owner['id']; } - if (!empty($item['causer-id']) && Contact::isSharing($item['causer-id'], $item['uid'], true)) { - $cdata = Contact::getPublicAndUserContactID($item['causer-id'], $item['uid']); - if (!empty($cdata['user'])) { - return $cdata['user']; + $contact_id = 0; + $user_contact_id = 0; + foreach (['group-link', 'causer-link', 'owner-link', 'author-link'] as $field) { + if (empty($item[$field])) { + continue; + } + if (!$user_contact_id && Contact::isSharingByURL($item[$field], $item['uid'], true)) { + $user_contact_id = Contact::getIdForURL($item[$field], $item['uid']); + } elseif (!$contact_id) { + $contact_id = Contact::getIdForURL($item[$field]); } } - if ($item['gravity'] == self::GRAVITY_PARENT) { - if (Contact::isSharingByURL($item['owner-link'], $item['uid'], true)) { - $contact_id = Contact::getIdForURL($item['owner-link'], $item['uid']); - } else { - $contact_id = Contact::getIdForURL($item['owner-link']); - } - if (!empty($contact_id)) { - return $contact_id; - } + if ($user_contact_id) { + return $user_contact_id; } - if (Contact::isSharingByURL($item['author-link'], $item['uid'], true)) { - $contact_id = Contact::getIdForURL($item['author-link'], $item['uid']); - } else { - $contact_id = Contact::getIdForURL($item['author-link']); + if (!empty($item['causer-id']) && Contact::isSharing($item['causer-id'], $item['uid'], true)) { + $cdata = Contact::getPublicAndUserContactID($item['causer-id'], $item['uid']); + if (!empty($cdata['user'])) { + return $cdata['user']; + } } - if (!empty($contact_id)) { + if ($contact_id) { return $contact_id; } @@ -551,8 +566,10 @@ class Item return true; } - $condition = ['uri-id' => $item['uri-id'], 'uid' => $item['uid'], - 'network' => [$item['network'], Protocol::DFRN]]; + $condition = [ + 'uri-id' => $item['uri-id'], 'uid' => $item['uid'], + 'network' => [$item['network'], Protocol::DFRN] + ]; if (Post::exists($condition)) { Logger::notice('duplicated item with the same uri found.', $condition); return true; @@ -567,8 +584,10 @@ class Item } } elseif ($item['network'] == Protocol::OSTATUS) { // Check for an existing post with the same content. There seems to be a problem with OStatus. - $condition = ["`body` = ? AND `network` = ? AND `created` = ? AND `contact-id` = ? AND `uid` = ?", - $item['body'], $item['network'], $item['created'], $item['contact-id'], $item['uid']]; + $condition = [ + "`body` = ? AND `network` = ? AND `created` = ? AND `contact-id` = ? AND `uid` = ?", + $item['body'], $item['network'], $item['created'], $item['contact-id'], $item['uid'] + ]; if (Post::exists($condition)) { Logger::notice('duplicated item with the same body found.', $item); return true; @@ -642,8 +661,10 @@ class Item return false; } - $condition = ['verb' => Activity::FOLLOW, 'uid' => $item['uid'], - 'parent-uri' => $item['parent-uri'], 'author-id' => $item['author-id']]; + $condition = [ + 'verb' => Activity::FOLLOW, 'uid' => $item['uid'], + 'parent-uri' => $item['parent-uri'], 'author-id' => $item['author-id'] + ]; if (Post::exists($condition)) { // It happens that we receive multiple follow requests by the same author - we only store one. Logger::info('Follow: Found existing follow request from author', ['author-id' => $item['author-id'], 'parent-uri' => $item['parent-uri']]); @@ -695,7 +716,8 @@ class Item private static function getDuplicateID(array $item): int { if (empty($item['network']) || in_array($item['network'], Protocol::FEDERATED)) { - $condition = ['`uri-id` = ? AND `uid` = ? AND `network` IN (?, ?, ?, ?)', + $condition = [ + '`uri-id` = ? AND `uid` = ? AND `network` IN (?, ?, ?, ?)', $item['uri-id'], $item['uid'], Protocol::ACTIVITYPUB, @@ -751,10 +773,12 @@ class Item */ private static function getTopLevelParent(array $item): array { - $fields = ['uid', 'uri', 'parent-uri', 'id', 'deleted', + $fields = [ + 'uid', 'uri', 'parent-uri', 'id', 'deleted', 'uri-id', 'parent-uri-id', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', - 'wall', 'private', 'origin', 'author-id']; + 'wall', 'private', 'origin', 'author-id' + ]; $condition = ['uri-id' => [$item['thr-parent-id'], $item['parent-uri-id']], 'uid' => $item['uid']]; $params = ['order' => ['id' => false]]; $parent = Post::selectFirst($fields, $condition, $params); @@ -771,7 +795,7 @@ class Item } if (!DBA::isResult($parent)) { - Logger::notice('item parent was not found - ignoring item', ['uri-id' => $item['uri-id'], 'thr-parent-id' => $item['thr-parent-id'], 'uid' => $item['uid'], 'callstack' => System::callstack(20)]); + Logger::notice('item parent was not found - ignoring item', ['uri-id' => $item['uri-id'], 'thr-parent-id' => $item['thr-parent-id'], 'uid' => $item['uid']]); return []; } @@ -779,9 +803,11 @@ class Item return $parent; } - $condition = ['uri-id' => $parent['parent-uri-id'], + $condition = [ + 'uri-id' => $parent['parent-uri-id'], 'parent-uri-id' => $parent['parent-uri-id'], - 'uid' => $parent['uid']]; + 'uid' => $parent['uid'] + ]; $params = ['order' => ['id' => false]]; $toplevel_parent = Post::selectFirst($fields, $condition, $params); @@ -854,6 +880,10 @@ class Item if (is_int($notify) && in_array($notify, Worker::PRIORITIES)) { $priority = $notify; } + + // Mastodon style API visibility + $copy_permissions = ($item['visibility'] ?? 'private') == 'private'; + unset($item['visibility']); } else { $item['network'] = trim(($item['network'] ?? '') ?: Protocol::PHANTOM); } @@ -946,7 +976,7 @@ class Item // Communities aren't working with the Diaspora protocol if (($uid != 0) && ($item['network'] == Protocol::DIASPORA)) { $user = User::getById($uid, ['account-type']); - if ($user['account-type'] == Contact::TYPE_COMMUNITY) { + if ($user['account-type'] == Contact::TYPE_COMMUNITY) { Logger::info('Community posts are not supported via Diaspora'); return 0; } @@ -966,12 +996,16 @@ class Item $item['gravity'] = self::getGravity($item); - $default = ['url' => $item['author-link'], 'name' => $item['author-name'], - 'photo' => $item['author-avatar'], 'network' => $item['network']]; + $default = [ + 'url' => $item['author-link'], 'name' => $item['author-name'], + 'photo' => $item['author-avatar'], 'network' => $item['network'] + ]; $item['author-id'] = ($item['author-id'] ?? 0) ?: Contact::getIdForURL($item['author-link'], 0, null, $default); - $default = ['url' => $item['owner-link'], 'name' => $item['owner-name'], - 'photo' => $item['owner-avatar'], 'network' => $item['network']]; + $default = [ + 'url' => $item['owner-link'], 'name' => $item['owner-name'], + 'photo' => $item['owner-avatar'], 'network' => $item['network'] + ]; $item['owner-id'] = ($item['owner-id'] ?? 0) ?: Contact::getIdForURL($item['owner-link'], 0, null, $default); $item['post-reason'] = self::getPostReason($item); @@ -982,8 +1016,10 @@ class Item $item['contact-id'] = self::contactId($item); - if (!empty($item['direction']) && in_array($item['direction'], [Conversation::PUSH, Conversation::RELAY]) && - empty($item['origin']) && self::isTooOld($item)) { + if ( + !empty($item['direction']) && in_array($item['direction'], [Conversation::PUSH, Conversation::RELAY]) && + empty($item['origin']) && self::isTooOld($item) + ) { Logger::info('Item is too old', ['item' => $item]); return 0; } @@ -1006,10 +1042,12 @@ class Item // Reshares have to keep their permissions to allow groups to work if (!$defined_permissions && (!$item['origin'] || ($item['verb'] != Activity::ANNOUNCE))) { - $item['allow_cid'] = $toplevel_parent['allow_cid']; - $item['allow_gid'] = $toplevel_parent['allow_gid']; - $item['deny_cid'] = $toplevel_parent['deny_cid']; - $item['deny_gid'] = $toplevel_parent['deny_gid']; + // Don't store the permissions on pure AP posts + $store_permissions = ($item['network'] != Protocol::ACTIVITYPUB) || $item['origin'] || !empty($item['diaspora_signed_text']); + $item['allow_cid'] = $store_permissions ? $toplevel_parent['allow_cid'] : ''; + $item['allow_gid'] = $store_permissions ? $toplevel_parent['allow_gid'] : ''; + $item['deny_cid'] = $store_permissions ? $toplevel_parent['deny_cid'] : ''; + $item['deny_gid'] = $store_permissions ? $toplevel_parent['deny_gid'] : ''; } $parent_origin = $toplevel_parent['origin']; @@ -1050,7 +1088,10 @@ class Item } if ($item['origin']) { - if (Photo::setPermissionFromBody($item['body'], $item['uid'], $item['contact-id'], $item['allow_cid'], $item['allow_gid'], $item['deny_cid'], $item['deny_gid'])) { + if ( + Photo::setPermissionFromBody($item['body'], $item['uid'], $item['contact-id'], $item['allow_cid'], $item['allow_gid'], $item['deny_cid'], $item['deny_gid']) + && ($item['object-type'] != Activity\ObjectType::EVENT) + ) { $item['object-type'] = Activity\ObjectType::IMAGE; } @@ -1124,7 +1165,8 @@ class Item $item['allow_gid'], $item['deny_cid'], $item['deny_gid'] - ))->id; + ) + )->id; if (!empty($item['extid'])) { $item['external-id'] = ItemURI::getIdByURI($item['extid']); @@ -1147,11 +1189,16 @@ class Item if (!empty($quote_id)) { // This is one of these "should not happen" situations. // The protocol implementations should already have done this job. - Logger::notice('Quote-uri-id detected in post', ['id' => $quote_id, 'guid' => $item['guid'], 'uri-id' => $item['uri-id'], 'callstack' => System::callstack(20)]); + Logger::notice('Quote-uri-id detected in post', ['id' => $quote_id, 'guid' => $item['guid'], 'uri-id' => $item['uri-id']]); $item['quote-uri-id'] = $quote_id; } } + if (!empty($item['quote-uri-id']) && ($item['quote-uri-id'] == $item['uri-id'])) { + Logger::info('Quote-Uri-Id is identical to Uri-Id', ['uri-id' => $item['uri-id'], 'guid' => $item['guid']]); + unset($item['quote-uri-id']); + } + if (!empty($item['quote-uri-id'])) { $item['raw-body'] = BBCode::removeSharedData($item['raw-body']); $item['body'] = BBCode::removeSharedData($item['body']); @@ -1171,8 +1218,6 @@ class Item // Check for hashtags in the body and repair or add hashtag links $item['body'] = self::setHashtags($item['body']); - $item['language'] = self::getLanguage($item); - $notify_type = Delivery::POST; // Filling item related side tables @@ -1221,7 +1266,9 @@ class Item } } - Post::insert($item['uri-id'], $item); + $item['language'] = self::getLanguage($item); + + $inserted = Post::insert($item['uri-id'], $item); if ($item['gravity'] == self::GRAVITY_PARENT) { Post\Thread::insert($item['uri-id'], $item); @@ -1318,6 +1365,9 @@ class Item if ($notify) { DI::contentItem()->postProcessPost($posted_item); + if ($copy_permissions && ($posted_item['thr-parent-id'] != $posted_item['uri-id']) && ($posted_item['private'] == self::PRIVATE)) { + DI::contentItem()->copyPermissions($posted_item['thr-parent-id'], $posted_item['uri-id']); + } } else { Hook::callAll('post_remote_end', $posted_item); } @@ -1356,7 +1406,8 @@ class Item // Don't relay participation messages if (($posted_item['verb'] == Activity::FOLLOW) && - (!$posted_item['origin'] || ($posted_item['author-id'] != Contact::getPublicIdByUserId($uid)))) { + (!$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; } @@ -1375,6 +1426,10 @@ class Item self::updateDisplayCache($posted_item['uri-id']); } + if ($inserted) { + Post\Engagement::storeFromItem($posted_item); + } + return $post_user_id; } @@ -1429,8 +1484,14 @@ class Item */ private static function setOwnerforResharedItem(array $item) { - $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 ($item['uid'] == 0) { + return; + } + + $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']]); return; @@ -1478,10 +1539,13 @@ class Item return; } - $uids = Tag::getUIDListByURIId($item['uri-id']); - foreach ($uids as $uid) { + foreach (Tag::getUIDListByURIId($item['uri-id']) as $uid => $tags) { $stored = self::storeForUserByUriId($item['uri-id'], $uid, ['post-reason' => self::PR_TAG]); Logger::info('Stored item for users', ['uri-id' => $item['uri-id'], 'uid' => $uid, 'stored' => $stored]); + foreach ($tags as $tag) { + $stored = Category::storeFileByURIId($item['uri-id'], $uid, Category::SUBCRIPTION, $tag); + Logger::debug('Stored tag subscription for user', ['uri-id' => $item['uri-id'], 'uid' => $uid, $tag, 'stored' => $stored]); + } } } @@ -1502,9 +1566,11 @@ class Item } // Only distribute public items from native networks - $condition = ['id' => $itemid, 'uid' => 0, - 'network' => array_merge(Protocol::FEDERATED ,['']), - 'visible' => true, 'deleted' => false, 'private' => [self::PUBLIC, self::UNLISTED]]; + $condition = [ + 'id' => $itemid, 'uid' => 0, + 'network' => array_merge(Protocol::FEDERATED, ['']), + 'visible' => true, 'deleted' => false, 'private' => [self::PUBLIC, self::UNLISTED] + ]; $item = Post::selectFirst(array_merge(self::ITEM_FIELDLIST, ['protocol']), $condition); if (!DBA::isResult($item)) { Logger::warning('Item not found', ['condition' => $condition]); @@ -1625,7 +1691,8 @@ class Item if (($uid != 0) && (($item['gravity'] == self::GRAVITY_PARENT) || $is_reshare) && DI::pConfig()->get($uid, 'system', 'accept_only_sharer') == self::COMPLETION_NONE && - !in_array($item['post-reason'], [self::PR_FOLLOWER, self::PR_TAG, self::PR_TO, self::PR_CC, self::PR_ACTIVITY, self::PR_AUDIENCE])) { + !in_array($item['post-reason'], [self::PR_FOLLOWER, self::PR_TAG, self::PR_TO, self::PR_CC, self::PR_ACTIVITY, self::PR_AUDIENCE]) + ) { Logger::info('Contact is not a follower, thread will not be stored', ['author' => $item['author-link'], 'uid' => $uid, 'uri-id' => $uri_id, 'post-reason' => $item['post-reason']]); return 0; } @@ -1821,7 +1888,7 @@ class Item } // is it an entry from a connector? Only add an entry for natively connected networks - if (!in_array($item["network"], array_merge(Protocol::FEDERATED ,['']))) { + if (!in_array($item["network"], array_merge(Protocol::FEDERATED, ['']))) { return; } @@ -1937,7 +2004,7 @@ class Item return ''; } - $languages = self::getLanguageArray(trim($item['title'] . "\n" . $item['body']), 3); + $languages = self::getLanguageArray($item['title'] . ' ' . ($item['content-warning'] ?? '') . ' ' . $item['body'], 3, $item['uri-id'], $item['author-id']); if (empty($languages)) { return ''; } @@ -1950,66 +2017,147 @@ class Item * * @param string $body * @param integer $count + * @param integer $uri_id + * @param integer $author_id * @return array */ - public static function getLanguageArray(string $body, int $count): array + public static function getLanguageArray(string $body, int $count, int $uri_id = 0, int $author_id = 0): array { - // Convert attachments to links - $naked_body = BBCode::removeAttachment($body); - if (empty($naked_body)) { + $searchtext = BBCode::toSearchText($body, $uri_id); + + if ((count(explode(' ', $searchtext)) < 10) && (mb_strlen($searchtext) < 30) && $author_id) { + $author = Contact::selectFirst(['about'], ['id' => $author_id]); + if (!empty($author['about'])) { + $about = BBCode::toSearchText($author['about'], 0); + Logger::debug('About field added', ['author' => $author_id, 'body' => $searchtext, 'about' => $about]); + $searchtext .= ' ' . $about; + } + } + + if (empty($searchtext)) { return []; } - // Remove links and pictures - $naked_body = BBCode::removeLinks($naked_body); + $ld = new Language(DI::l10n()->getDetectableLanguages()); - // Convert the title and the body to plain text - $naked_body = BBCode::toPlaintext($naked_body); + $result = []; - // Remove possibly remaining links - $naked_body = preg_replace(Strings::autoLinkRegEx(), '', $naked_body); + foreach (self::splitByBlocks($searchtext) as $block) { + $languages = $ld->detect($block)->close() ?: []; - if (empty($naked_body)) { - return []; + $data = [ + 'text' => $block, + 'detected' => $languages, + 'uri-id' => $uri_id, + 'author-id' => $author_id, + ]; + Hook::callAll('detect_languages', $data); + + foreach ($data['detected'] as $language => $quality) { + $result[$language] = max($result[$language] ?? 0, $quality * (strlen($block) / strlen($searchtext))); + } } - $naked_body = self::getDominantLanguage($naked_body); + $result = self::compactLanguages($result); - $availableLanguages = DI::l10n()->getAvailableLanguages(); - // See https://github.com/friendica/friendica/issues/10511 - // Persian is manually added to language detection until a persian translation is provided for the interface, at - // which point it will be automatically available through `getAvailableLanguages()` and this should be removed. - $availableLanguages['fa'] = 'fa'; + arsort($result); + return array_slice($result, 0, $count); + } - $ld = new Language(array_keys($availableLanguages)); - return $ld->detect($naked_body)->limit(0, $count)->close() ?: []; + /** + * Concert the language code in the detection result to ISO 639-1. + * On duplicates the system uses the higher quality value. + * + * @param array $result + * @return array + */ + private static function compactLanguages(array $result): array + { + $languages = []; + foreach ($result as $language => $quality) { + if ($quality == 0) { + continue; + } + $code = DI::l10n()->toISO6391($language); + if (empty($languages[$code]) || ($languages[$code] < $quality)) { + $languages[$code] = $quality; + } + } + return $languages; } /** - * Check if latin or non latin are dominant in the body and only return the dominant one + * Split a string into different unicode blocks + * Currently the text is split into the latin and the non latin part. * * @param string $body - * @return string + * @return array */ - private static function getDominantLanguage(string $body): string + private static function splitByBlocks(string $body): array { - $latin = ''; - $non_latin = ''; + if (!class_exists('IntlChar')) { + return [$body]; + } + + $blocks = []; + $previous_block = 0; + for ($i = 0; $i < mb_strlen($body); $i++) { $character = mb_substr($body, $i, 1); - $ord = mb_ord($character); - - // We add the most common characters to both strings. - if (($ord <= 64) || ($ord >= 91 && $ord <= 96) || ($ord >= 123 && $ord <= 191) || in_array($ord, [215, 247]) || ($ord >= 697 && $ord <= 735) || ($ord > 65535)) { - $latin .= $character; - $non_latin .= $character; - } elseif ($ord < 768) { - $latin .= $character; + $previous = ($i > 0) ? mb_substr($body, $i - 1, 1) : ''; + $next = ($i < mb_strlen($body)) ? mb_substr($body, $i + 1, 1) : ''; + + if (!\IntlChar::isalpha($character)) { + if (($previous != '') && (\IntlChar::isalpha($previous))) { + $previous_block = self::getBlockCode($previous); + } + + $block = (($next != '') && \IntlChar::isalpha($next)) ? self::getBlockCode($next) : $previous_block; + $blocks[$block] = ($blocks[$block] ?? '') . $character; } else { - $non_latin .= $character; + $block = self::getBlockCode($character); + $blocks[$block] = ($blocks[$block] ?? '') . $character; } } - return (mb_strlen($latin) > mb_strlen($non_latin)) ? $latin : $non_latin; + + foreach (array_keys($blocks) as $key) { + $blocks[$key] = trim($blocks[$key]); + if (empty($blocks[$key])) { + unset($blocks[$key]); + } + } + + return array_values($blocks); + } + + /** + * returns the block code for the given character + * + * @param string $character + * @return integer 0 = no alpha character (blank, signs, emojis, ...), 1 = latin character, 2 = character in every other language + */ + private static function getBlockCode(string $character): int + { + if (!\IntlChar::isalpha($character)) { + return 0; + } + return self::isLatin($character) ? 1 : 2; + } + + /** + * Checks if the given character is in one of the latin code blocks + * + * @param string $character + * @return boolean + */ + private static function isLatin(string $character): bool + { + return in_array(\IntlChar::getBlockCode($character), [ + \IntlChar::BLOCK_CODE_BASIC_LATIN, \IntlChar::BLOCK_CODE_LATIN_1_SUPPLEMENT, + \IntlChar::BLOCK_CODE_LATIN_EXTENDED_A, \IntlChar::BLOCK_CODE_LATIN_EXTENDED_B, + \IntlChar::BLOCK_CODE_LATIN_EXTENDED_C, \IntlChar::BLOCK_CODE_LATIN_EXTENDED_D, + \IntlChar::BLOCK_CODE_LATIN_EXTENDED_E, \IntlChar::BLOCK_CODE_LATIN_EXTENDED_ADDITIONAL + ]); } public static function getLanguageMessage(array $item): string @@ -2018,7 +2166,15 @@ class Item $used_languages = ''; foreach (json_decode($item['language'], true) as $language => $reliability) { - $used_languages .= $iso639->languageByCode1($language) . ' (' . $language . "): " . number_format($reliability, 5) . '\n'; + $code = DI::l10n()->toISO6391($language); + + $native = $iso639->nativeByCode1($code); + $language = $iso639->languageByCode1($code); + if ($native != $language) { + $used_languages .= DI::l10n()->t('%s (%s - %s): %s', $native, $language, $code, number_format($reliability, 5)) . '\n'; + } else { + $used_languages .= DI::l10n()->t('%s (%s): %s', $native, $code, number_format($reliability, 5)) . '\n'; + } } $used_languages = DI::l10n()->t('Detected languages in this post:\n%s', $used_languages); return $used_languages; @@ -2046,7 +2202,7 @@ class Item $hostPart = $host ?: $parsed['host'] ?? ''; if (!$hostPart) { - Logger::warning('Empty host GUID part', ['uri' => $uri, 'host' => $host, 'parsed' => $parsed, 'callstack' => System::callstack(10)]); + Logger::warning('Empty host GUID part', ['uri' => $uri, 'host' => $host, 'parsed' => $parsed]); } // Glue it together to be able to make a hash from it @@ -2124,12 +2280,16 @@ class Item } // Now do the same for the system wide contacts with uid=0 if ($arr['private'] != self::PRIVATE) { - Contact::update(['failed' => false, 'local-data' => true, 'success_update' => $arr['received'], 'last-item' => $arr['received']], - ['id' => $arr['owner-id']]); + Contact::update( + ['failed' => false, 'local-data' => true, 'success_update' => $arr['received'], 'last-item' => $arr['received']], + ['id' => $arr['owner-id']] + ); if ($arr['owner-id'] != $arr['author-id']) { - Contact::update(['failed' => false, 'local-data' => true, 'success_update' => $arr['received'], 'last-item' => $arr['received']], - ['id' => $arr['author-id']]); + Contact::update( + ['failed' => false, 'local-data' => true, 'success_update' => $arr['received'], 'last-item' => $arr['received']], + ['id' => $arr['author-id']] + ); } } } @@ -2155,29 +2315,44 @@ class Item // All hashtags should point to the home server if "local_tags" is activated if (DI::config()->get('system', 'local_tags')) { - $body = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", - "#[url=" . DI::baseUrl() . "/search?tag=$2]$2[/url]", $body); + $body = preg_replace( + "/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", + "#[url=" . DI::baseUrl() . "/search?tag=$2]$2[/url]", + $body + ); } // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls - $body = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", + $body = preg_replace_callback( + "/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", function ($match) { return ("[url=" . str_replace("#", "#", $match[1]) . "]" . str_replace("#", "#", $match[2]) . "[/url]"); - }, $body); + }, + $body + ); - $body = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism", + $body = preg_replace_callback( + "/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism", function ($match) { return ("[bookmark=" . str_replace("#", "#", $match[1]) . "]" . str_replace("#", "#", $match[2]) . "[/bookmark]"); - }, $body); + }, + $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); + }, + $body + ); // Repair recursive urls - $body = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", - "#$2", $body); + $body = preg_replace( + "/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", + "#$2", + $body + ); foreach ($tags as $tag) { if ((strpos($tag, '#') !== 0) || strpos($tag, '[url=') || strlen($tag) < 2 || $tag[1] == '#') { @@ -2281,16 +2456,15 @@ class Item return; } - $cdata = Contact::getPublicAndUserContactID($item['author-id'], $item['uid']); - if (empty($cdata['user']) || ($cdata['user'] != $item['contact-id'])) { + if (!DBA::exists('contact', ['id' => $cdata['user'], 'remote_self' => LocalRelationship::MIRROR_NATIVE_RESHARE])) { return; } - if (!DBA::exists('contact', ['id' => $cdata['user'], 'remote_self' => Contact::MIRROR_NATIVE_RESHARE])) { + if (!in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN])) { return; } - if (!in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN])) { + if (User::getById($item['uid'], ['blocked'])['blocked'] ?? false) { return; } @@ -2301,7 +2475,7 @@ class Item public static function isRemoteSelf(array $contact, array &$datarray): bool { - if ($contact['remote_self'] != Contact::MIRROR_OWN_POST) { + if ($contact['remote_self'] != LocalRelationship::MIRROR_OWN_POST) { return false; } @@ -2328,11 +2502,19 @@ class Item return false; } + if (User::getById($contact['uid'], ['blocked'])['blocked'] ?? false) { + Logger::info('User is blocked', ['contact' => $contact]); + return false; + } + $datarray2 = $datarray; - Logger::info('remote-self start', ['contact' => $contact['url'], 'remote_self'=> $contact['remote_self'], 'item' => $datarray]); + Logger::info('remote-self start', ['contact' => $contact['url'], 'remote_self' => $contact['remote_self'], 'item' => $datarray]); - $self = DBA::selectFirst('contact', ['id', 'name', 'url', 'thumb'], - ['uid' => $contact['uid'], 'self' => true]); + $self = DBA::selectFirst( + 'contact', + ['id', 'name', 'url', 'thumb'], + ['uid' => $contact['uid'], 'self' => true] + ); if (!DBA::isResult($self)) { Logger::error('Self contact not found', ['uid' => $contact['uid']]); return false; @@ -2368,7 +2550,7 @@ class Item // Store the original post $result = self::insert($datarray2); - Logger::info('remote-self post original item', ['contact' => $contact['url'], 'result'=> $result, 'item' => $datarray2]); + Logger::info('remote-self post original item', ['contact' => $contact['url'], 'result' => $result, 'item' => $datarray2]); } else { $datarray['private'] = self::PUBLIC; $datarray['app'] = 'Feed'; @@ -2495,7 +2677,8 @@ class Item if (($obj1['allow_cid'] == $obj2['allow_cid']) && ($obj1['allow_gid'] == $obj2['allow_gid']) && ($obj1['deny_cid'] == $obj2['deny_cid']) - && ($obj1['deny_gid'] == $obj2['deny_gid'])) { + && ($obj1['deny_gid'] == $obj2['deny_gid']) + ) { return true; } @@ -2542,8 +2725,10 @@ class Item return; } - $condition = ["`uid` = ? AND NOT `deleted` AND `gravity` = ?", - $uid, self::GRAVITY_PARENT]; + $condition = [ + "`uid` = ? AND NOT `deleted` AND `gravity` = ?", + $uid, self::GRAVITY_PARENT + ]; /* * $expire_network_only = save your own wall posts @@ -2617,8 +2802,10 @@ class Item return false; } - $condition = ["`uid` = ? AND `wall` = ? AND NOT `deleted` AND `visible` AND `received` >= ?", - $uid, $wall, $user['register_date']]; + $condition = [ + "`uid` = ? AND `wall` = ? AND NOT `deleted` AND `visible` AND `received` >= ?", + $uid, $wall, $user['register_date'] + ]; $params = ['order' => ['received' => false]]; $thread = Post::selectFirstThread(['received'], $condition, $params); if (DBA::isResult($thread)) { @@ -2750,8 +2937,10 @@ class Item $vids = Verb::getID($activity); } - $condition = ['vid' => $vids, 'deleted' => false, 'gravity' => self::GRAVITY_ACTIVITY, - 'author-id' => $author_id, 'uid' => $uid, 'thr-parent-id' => $uri_id]; + $condition = [ + 'vid' => $vids, 'deleted' => false, 'gravity' => self::GRAVITY_ACTIVITY, + 'author-id' => $author_id, 'uid' => $uid, 'thr-parent-id' => $uri_id + ]; $like_item = Post::selectFirst(['id', 'guid', 'verb'], $condition); if (DBA::isResult($like_item)) { @@ -2855,12 +3044,14 @@ class Item // Profile owner - everything is visible $condition = []; } elseif ($remote_user) { - // Authenticated visitor - fetch the matching permissionsets + // Authenticated visitor - fetch the matching permissionsets $permissionSets = DI::permissionSet()->selectByContactId($remote_user, $owner_id); if (!empty($set)) { - $condition = ["(`private` != ? OR (`private` = ? AND `wall` + $condition = [ + "(`private` != ? OR (`private` = ? AND `wall` AND `psid` IN (" . implode(', ', array_fill(0, count($set), '?')) . ")))", - self::PRIVATE, self::PRIVATE]; + self::PRIVATE, self::PRIVATE + ]; $condition = array_merge($condition, $permissionSets->column('id')); } } @@ -2956,7 +3147,8 @@ class Item $rendered_hash = $item['rendered-hash'] ?? ''; $rendered_html = $item['rendered-html'] ?? ''; - if ($rendered_hash == '' + if ( + $rendered_hash == '' || $rendered_html == '' || $rendered_hash != hash('md5', BBCode::VERSION . '::' . $body) || DI::config()->get('system', 'ignore_cache') @@ -3047,7 +3239,7 @@ class Item $item['body'] = BBCode::removeSharedData($item['body']); } elseif (empty($shared_item['uri-id']) && empty($item['quote-uri-id']) && ($item['network'] != Protocol::DIASPORA)) { $media = Post\Media::getByURIId($item['uri-id'], [Post\Media::ACTIVITY]); - if (!empty($media)) { + if (!empty($media) && ($media[0]['media-uri-id'] != $item['uri-id'])) { $shared_item = Post::selectFirst($fields, ['uri-id' => $media[0]['media-uri-id'], 'uid' => [$item['uid'], 0]]); if (empty($shared_item['uri-id'])) { $shared_item = Post::selectFirst($fields, ['plink' => $media[0]['url'], 'uid' => [$item['uid'], 0]]); @@ -3073,7 +3265,7 @@ class Item if (!empty($quote_uri_id)) { if (isset($shared_item['plink'])) { - $item['body'] .= "\n" . DI::contentItem()->createSharedBlockByArray($shared_item); + $item['body'] .= "\n" . DI::contentItem()->createSharedBlockByArray($shared_item, false, true); } else { DI::logger()->warning('Missing plink in shared item', ['item' => $item, 'shared' => $shared, 'quote_uri_id' => $quote_uri_id, 'shared_item' => $shared_item]); } @@ -3082,15 +3274,15 @@ class Item if (!empty($shared_item['uri-id'])) { $shared_uri_id = $shared_item['uri-id']; $shared_links[] = strtolower($shared_item['plink']); - $shared_attachments = Post\Media::splitAttachments($shared_uri_id, [], $shared_item['has-media']); - $shared_links = array_merge($shared_links, array_column($shared_attachments['visual'], 'url')); - $shared_links = array_merge($shared_links, array_column($shared_attachments['link'], 'url')); - $shared_links = array_merge($shared_links, array_column($shared_attachments['additional'], 'url')); - $item['body'] = self::replaceVisualAttachments($shared_attachments, $item['body']); + $sharedSplitAttachments = DI::postMediaRepository()->splitAttachments($shared_uri_id, [], $shared_item['has-media']); + $shared_links = array_merge($shared_links, $sharedSplitAttachments['visual']->column('url')); + $shared_links = array_merge($shared_links, $sharedSplitAttachments['link']->column('url')); + $shared_links = array_merge($shared_links, $sharedSplitAttachments['additional']->column('url')); + $item['body'] = self::replaceVisualAttachments($sharedSplitAttachments['visual'], $item['body']); } - $attachments = Post\Media::splitAttachments($item['uri-id'], $shared_links, $item['has-media'] ?? false); - $item['body'] = self::replaceVisualAttachments($attachments, $item['body'] ?? ''); + $itemSplitAttachments = DI::postMediaRepository()->splitAttachments($item['uri-id'], $shared_links, $item['has-media'] ?? false); + $item['body'] = self::replaceVisualAttachments($itemSplitAttachments['visual'], $item['body'] ?? ''); self::putInCache($item); $item['body'] = $body; @@ -3115,7 +3307,7 @@ class Item $filter_reasons[] = DI::l10n()->t('Content warning: %s', $item['content-warning']); } - $item['attachments'] = $attachments; + $item['attachments'] = $itemSplitAttachments; $hook_data = [ 'item' => $item, @@ -3144,18 +3336,24 @@ class Item return $s; } - if (!empty($shared_attachments)) { - $s = self::addGallery($s, $shared_attachments, $item['uri-id']); - $s = self::addVisualAttachments($shared_attachments, $shared_item, $s, true); - $s = self::addLinkAttachment($shared_uri_id ?: $item['uri-id'], $shared_attachments, $body, $s, true, $quote_shared_links); - $s = self::addNonVisualAttachments($shared_attachments, $item, $s, true); + if (!empty($sharedSplitAttachments)) { + $s = self::addGallery($s, $sharedSplitAttachments['visual']); + $s = self::addVisualAttachments($sharedSplitAttachments['visual'], $shared_item, $s, true); + $s = self::addLinkAttachment($shared_uri_id ?: $item['uri-id'], $sharedSplitAttachments, $body, $s, true, $quote_shared_links); + $s = self::addNonVisualAttachments($sharedSplitAttachments['additional'], $item, $s, true); $body = BBCode::removeSharedData($body); } - $s = self::addGallery($s, $attachments, $item['uri-id']); - $s = self::addVisualAttachments($attachments, $item, $s, false); - $s = self::addLinkAttachment($item['uri-id'], $attachments, $body, $s, false, $shared_links); - $s = self::addNonVisualAttachments($attachments, $item, $s, false); + $pos = strpos($s, BBCode::SHARED_ANCHOR); + if ($pos) { + $shared_html = substr($s, $pos + strlen(BBCode::SHARED_ANCHOR)); + $s = substr($s, 0, $pos); + } + + $s = self::addGallery($s, $itemSplitAttachments['visual']); + $s = self::addVisualAttachments($itemSplitAttachments['visual'], $item, $s, false); + $s = self::addLinkAttachment($item['uri-id'], $itemSplitAttachments, $body, $s, false, $shared_links); + $s = self::addNonVisualAttachments($itemSplitAttachments['additional'], $item, $s, false); $s = self::addQuestions($item, $s); // Map. @@ -3172,6 +3370,10 @@ class Item $s = preg_replace('|(]+src="[^"]+/photo/[0-9a-f]+)-[0-9]|', "$1-" . $ps, $s); } + if (!empty($shared_html)) { + $s .= $shared_html; + } + $s = HTML::applyContentFilter($s, $filter_reasons); $hook_data = ['item' => $item, 'html' => $s]; @@ -3179,45 +3381,35 @@ class Item return $hook_data['html']; } - /** - * @param array $images - * @return string - * @throws \Friendica\Network\HTTPException\ServiceUnavailableException - */ - private static function makeImageGrid(array $images): string - { - // Image for first column (fc) and second column (sc) - $images_fc = []; - $images_sc = []; - - for ($i = 0; $i < count($images); $i++) { - ($i % 2 == 0) ? ($images_fc[] = $images[$i]) : ($images_sc[] = $images[$i]); - } - - return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image_grid.tpl'), [ - 'columns' => [ - 'fc' => $images_fc, - 'sc' => $images_sc, - ], - ]); - } - /** * Modify links to pictures to links for the "Fancybox" gallery * - * @param string $s - * @param array $attachments - * @param integer $uri_id + * @param string $s + * @param PostMedias $PostMedias * @return string */ - private static function addGallery(string $s, array $attachments, int $uri_id): string + private static function addGallery(string $s, PostMedias $PostMedias): string { - foreach ($attachments['visual'] as $attachment) { - if (empty($attachment['preview']) || ($attachment['type'] != Post\Media::IMAGE)) { + foreach ($PostMedias as $PostMedia) { + if (!$PostMedia->preview || ($PostMedia->type !== Post\Media::IMAGE)) { continue; } - $s = str_replace('hasDimensions()) { + $pattern = '#(.*?)">#'; + + $s = preg_replace_callback($pattern, function () use ($PostMedia) { + return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/single_with_height_allocation.tpl'), [ + '$image' => $PostMedia, + '$allocated_height' => $PostMedia->getAllocatedHeight(), + '$allocated_max_width' => ($PostMedia->previewWidth ?? $PostMedia->width) . 'px', + ]); + }, $s); + } else { + $s = str_replace('startRecording('rendering'); - foreach ($attachments['visual'] as $attachment) { - if (!empty($attachment['preview'])) { - if (Network::isLocalLink($attachment['preview'])) { + foreach ($PostMedias as $PostMedia) { + if ($PostMedia->preview) { + if (DI::baseUrl()->isLocalUri($PostMedia->preview)) { continue; } - $proxy = Post\Media::getPreviewUrlForId($attachment['id'], Proxy::SIZE_LARGE); - $search = ['[img=' . $attachment['preview'] . ']', ']' . $attachment['preview'] . '[/img]']; + $proxy = DI::baseUrl() . $PostMedia->getPreviewPath(Proxy::SIZE_LARGE); + $search = ['[img=' . $PostMedia->preview . ']', ']' . $PostMedia->preview . '[/img]']; $replace = ['[img=' . $proxy . ']', ']' . $proxy . '[/img]']; $body = str_replace($search, $replace, $body); - } elseif ($attachment['filetype'] == 'image') { - if (Network::isLocalLink($attachment['url'])) { + } elseif ($PostMedia->mimetype->type == 'image') { + if (DI::baseUrl()->isLocalUri($PostMedia->url)) { continue; } - $proxy = Post\Media::getUrlForId($attachment['id']); - $search = ['[img=' . $attachment['url'] . ']', ']' . $attachment['url'] . '[/img]']; + $proxy = DI::baseUrl() . $PostMedia->getPreviewPath(Proxy::SIZE_LARGE); + $search = ['[img=' . $PostMedia->url . ']', ']' . $PostMedia->url . '[/img]']; $replace = ['[img=' . $proxy . ']', ']' . $proxy . '[/img]']; $body = str_replace($search, $replace, $body); @@ -3309,29 +3503,31 @@ class Item /** * Add visual attachments to the content * - * @param array $attachments - * @param array $item - * @param string $content + * @param PostMedias $PostMedias + * @param array $item + * @param string $content + * @param bool $shared * @return string modified content + * @throws ServiceUnavailableException */ - private static function addVisualAttachments(array $attachments, array $item, string $content, bool $shared): string + private static function addVisualAttachments(PostMedias $PostMedias, array $item, string $content, bool $shared): string { DI::profiler()->startRecording('rendering'); $leading = ''; $trailing = ''; - $images = []; + $images = new PostMedias(); // @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['preview'] ?? $attachment['url'], $attachment['type'])) { + foreach ($PostMedias as $PostMedia) { + if (self::containsLink($item['body'], $PostMedia->preview ?? $PostMedia->url, $PostMedia->type)) { continue; } - if ($attachment['filetype'] == 'image') { - $preview_url = Post\Media::getPreviewUrlForId($attachment['id'], ($attachment['width'] > $attachment['height']) ? Proxy::SIZE_MEDIUM : Proxy::SIZE_LARGE); - } elseif (!empty($attachment['preview'])) { - $preview_url = Post\Media::getPreviewUrlForId($attachment['id'], Proxy::SIZE_LARGE); + if ($PostMedia->mimetype->type == 'image' || $PostMedia->preview) { + $preview_size = Proxy::SIZE_MEDIUM; + $preview_url = DI::baseUrl() . $PostMedia->getPreviewPath($preview_size); } else { + $preview_size = 0; $preview_url = ''; } @@ -3339,15 +3535,15 @@ class Item continue; } - if (($attachment['filetype'] == 'video')) { + if ($PostMedia->mimetype->type == 'video') { /// @todo Move the template to /content as well $media = Renderer::replaceMacros(Renderer::getMarkupTemplate('video_top.tpl'), [ '$video' => [ - 'id' => $attachment['id'], - 'src' => $attachment['url'], - 'name' => $attachment['name'] ?: $attachment['url'], + 'id' => $PostMedia->id, + 'src' => (string)$PostMedia->url, + 'name' => $PostMedia->name ?: $PostMedia->url, 'preview' => $preview_url, - 'mime' => $attachment['mimetype'], + 'mime' => (string)$PostMedia->mimetype, ], ]); if (($item['post-type'] ?? null) == Item::PT_VIDEO) { @@ -3355,13 +3551,13 @@ class Item } else { $trailing .= $media; } - } elseif ($attachment['filetype'] == 'audio') { + } elseif ($PostMedia->mimetype->type == 'audio') { $media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/audio.tpl'), [ '$audio' => [ - 'id' => $attachment['id'], - 'src' => $attachment['url'], - 'name' => $attachment['name'] ?: $attachment['url'], - 'mime' => $attachment['mimetype'], + 'id' => $PostMedia->id, + 'src' => (string)$PostMedia->url, + 'name' => $PostMedia->name ?: $PostMedia->url, + 'mime' => (string)$PostMedia->mimetype, ], ]); if (($item['post-type'] ?? null) == Item::PT_AUDIO) { @@ -3369,23 +3565,17 @@ class Item } else { $trailing .= $media; } - } elseif ($attachment['filetype'] == 'image') { - $src_url = Post\Media::getUrlForId($attachment['id']); + } elseif ($PostMedia->mimetype->type == 'image') { + $src_url = DI::baseUrl() . $PostMedia->getPhotoPath(); if (self::containsLink($item['body'], $src_url)) { continue; } - $images[] = ['src' => $src_url, 'preview' => $preview_url, 'attachment' => $attachment, 'uri_id' => $item['uri-id']]; + + $images[] = $PostMedia->withUrl(new Uri($src_url))->withPreview(new Uri($preview_url), $preview_size); } } - $media = ''; - if (count($images) > 1) { - $media = self::makeImageGrid($images); - } elseif (count($images) == 1) { - $media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image.tpl'), [ - '$image' => $images[0], - ]); - } + $media = Image::getBodyAttachHtml($images); // On Diaspora posts the attached pictures are leading if ($item['network'] == Protocol::DIASPORA) { @@ -3414,58 +3604,62 @@ class Item /** * 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 + * @param int $uriid + * @param PostMedias[] $attachments + * @param string $body + * @param string $content + * @param bool $shared + * @param array $ignore_links A list of URLs to ignore * @return string modified content + * @throws InternalServerErrorException + * @throws ServiceUnavailableException */ private static function addLinkAttachment(int $uriid, array $attachments, string $body, string $content, bool $shared, array $ignore_links): string { DI::profiler()->startRecording('rendering'); // Don't show a preview when there is a visual attachment (audio or video) - $types = array_column($attachments['visual'], 'type'); - $preview = !in_array(Post\Media::IMAGE, $types) && !in_array(Post\Media::VIDEO, $types); - - 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; - } - } - // @todo Judge between the links to use the one with most information - if (!$found && (empty($attachment) || !empty($link['author-name']) || - (empty($attachment['name']) && !empty($link['name'])) || - (empty($attachment['description']) && !empty($link['description'])) || - (empty($attachment['preview']) && !empty($link['preview'])))) { - $attachment = $link; + $types = $attachments['visual']->column('type'); + $preview = !in_array(PostMedia::TYPE_IMAGE, $types) && !in_array(PostMedia::TYPE_VIDEO, $types); + + /** @var ?PostMedia $attachment */ + $attachment = null; + foreach ($attachments['link'] as $PostMedia) { + $found = false; + foreach ($ignore_links as $ignore_link) { + if (Strings::compareLink($PostMedia->url, $ignore_link)) { + $found = true; } } + // @todo Judge between the links to use the one with most information + if (!$found && (empty($attachment) || $PostMedia->authorName || + (!$attachment->name && $PostMedia->name) || + (!$attachment->description && $PostMedia->description) || + (!$attachment->preview && $PostMedia->preview))) { + $attachment = $PostMedia; + } } if (!empty($attachment)) { $data = [ 'after' => '', - 'author_name' => $attachment['author-name'] ?? '', - 'author_url' => $attachment['author-url'] ?? '', - 'description' => $attachment['description'] ?? '', + 'author_name' => $attachment->authorName ?? '', + 'author_url' => (string)($attachment->authorUrl ?? ''), + 'description' => $attachment->description ?? '', 'image' => '', 'preview' => '', - 'provider_name' => $attachment['publisher-name'] ?? '', - 'provider_url' => $attachment['publisher-url'] ?? '', + 'provider_name' => $attachment->publisherName ?? '', + 'provider_url' => (string)($attachment->publisherUrl ?? ''), 'text' => '', - 'title' => $attachment['name'] ?? '', + 'title' => $attachment->name ?? '', 'type' => 'link', - 'url' => $attachment['url']]; + 'url' => (string)$attachment->url, + ]; - if ($preview && !empty($attachment['preview'])) { - if ($attachment['preview-width'] >= 500) { - $data['image'] = Post\Media::getPreviewUrlForId($attachment['id'], Proxy::SIZE_MEDIUM); + if ($preview && $attachment->preview) { + if ($attachment->previewWidth >= 500) { + $data['image'] = DI::baseUrl() . $attachment->getPreviewPath(Proxy::SIZE_MEDIUM); } else { - $data['preview'] = Post\Media::getPreviewUrlForId($attachment['id'], Proxy::SIZE_MEDIUM); + $data['preview'] = DI::baseUrl() . $attachment->getPreviewPath(Proxy::SIZE_MEDIUM); } } @@ -3533,34 +3727,41 @@ class Item } /** - * Add non visual attachments to the content + * Add non-visual attachments to the content * - * @param array $attachments - * @param array $item - * @param string $content + * @param PostMedias $PostMedias + * @param array $item + * @param string $content * @return string modified content + * @throws InternalServerErrorException + * @throws \ImagickException */ - private static function addNonVisualAttachments(array $attachments, array $item, string $content): string + private static function addNonVisualAttachments(PostMedias $PostMedias, array $item, string $content): string { DI::profiler()->startRecording('rendering'); $trailing = ''; - foreach ($attachments['additional'] as $attachment) { - if (strpos($item['body'], $attachment['url'])) { + foreach ($PostMedias as $PostMedia) { + if (strpos($item['body'], $PostMedia->url)) { continue; } - $author = ['uid' => 0, 'id' => $item['author-id'], - 'network' => $item['author-network'], 'url' => $item['author-link']]; - $the_url = Contact::magicLinkByContact($author, $attachment['url']); + $author = [ + 'uid' => 0, + 'id' => $item['author-id'], + 'network' => $item['author-network'], + 'url' => $item['author-link'], + 'alias' => $item['author-alias'] + ]; + $the_url = Contact::magicLinkByContact($author, $PostMedia->url); - $title = Strings::escapeHtml(trim(($attachment['description'] ?? '') ?: $attachment['url'])); + $title = Strings::escapeHtml(trim($PostMedia->description ?? '' ?: $PostMedia->url)); - if (!empty($attachment['size'])) { - $title .= ' ' . $attachment['size'] . ' ' . DI::l10n()->t('bytes'); + if ($PostMedia->size) { + $title .= ' ' . $PostMedia->size . ' ' . DI::l10n()->t('bytes'); } /// @todo Use a template - $icon = '
'; + $icon = '
'; $trailing .= '
' . $icon . ''; } @@ -3599,7 +3800,7 @@ class Item $summary = DI::l10n()->tt('%d voter.', '%d voters.', $question['voters']); } elseif (!empty($question['endtime'])) { $summary = DI::l10n()->t('Poll end: %s', Temporal::getRelativeDate($question['endtime'])); - } else { + } else { $summary = ''; } @@ -3608,7 +3809,7 @@ class Item '$options' => $options, '$summary' => $summary, ]); - } + } DI::profiler()->stopRecording(); return $content; } @@ -3637,8 +3838,13 @@ class Item ]; if (!empty($plink) && ($item['private'] == self::PRIVATE)) { - $author = ['uid' => 0, 'id' => $item['author-id'], - 'network' => $item['author-network'], 'url' => $item['author-link']]; + $author = [ + 'uid' => 0, + 'id' => $item['author-id'], + 'network' => $item['author-network'], + 'url' => $item['author-link'], + 'alias' => $item['author-alias'], + ]; $plink = Contact::magicLinkByContact($author, $plink); } @@ -3662,7 +3868,7 @@ class Item /** * Does the given uri-id belongs to a post that is sent as starting post to a group? - * This does not apply to posts that are sent only in parallel to a group. + * This does apply to posts that are sent via ! and not in parallel to a group via @ * * @param int $uri_id * @@ -3739,11 +3945,12 @@ class Item * Fetches item for given URI or plink * * @param string $uri - * @param integer $uid + * @param int $uid + * @param int $completion * * @return integer item id */ - public static function fetchByLink(string $uri, int $uid = 0): int + public static function fetchByLink(string $uri, int $uid = 0, int $completion = ActivityPub\Receiver::COMPLETION_MANUAL): int { Logger::info('Trying to fetch link', ['uid' => $uid, 'uri' => $uri]); $item_id = self::searchByLink($uri, $uid); @@ -3764,7 +3971,7 @@ class Item return is_numeric($hookData['item_id']) ? $hookData['item_id'] : 0; } - $fetched_uri = ActivityPub\Processor::fetchMissingActivity($uri, [], '', ActivityPub\Receiver::COMPLETION_MANUAL, $uid); + $fetched_uri = ActivityPub\Processor::fetchMissingActivity($uri, [], '', $completion, $uid); if ($fetched_uri) { $item_id = self::searchByLink($fetched_uri, $uid); @@ -3824,7 +4031,7 @@ class Item } $url = $shared['message_id'] ?: $shared['link']; - $id = self::fetchByLink($url); + $id = self::fetchByLink($url, 0, ActivityPub\Receiver::COMPLETION_ASYNC); if (!$id) { Logger::notice('Post could not be fetched.', ['url' => $url, 'uid' => $uid]); return 0;