diff --git a/mod/item.php b/mod/item.php index 8744ba3b2..9828d1acb 100644 --- a/mod/item.php +++ b/mod/item.php @@ -35,7 +35,6 @@ use Friendica\Content\Text\BBCode; use Friendica\Core\Hook; use Friendica\Core\Logger; use Friendica\Core\Protocol; -use Friendica\Core\Renderer; use Friendica\Core\Session; use Friendica\Core\System; use Friendica\Core\Worker; @@ -48,6 +47,7 @@ use Friendica\Model\FileTag; use Friendica\Model\Item; use Friendica\Model\Notify\Type; use Friendica\Model\Photo; +use Friendica\Model\Post; use Friendica\Model\Tag; use Friendica\Network\HTTPException; use Friendica\Object\EMail\ItemCCEMail; @@ -55,7 +55,6 @@ use Friendica\Protocol\Activity; use Friendica\Protocol\Diaspora; use Friendica\Util\DateTimeFormat; use Friendica\Security\Security; -use Friendica\Util\Strings; use Friendica\Worker\Delivery; function item_post(App $a) { @@ -532,9 +531,8 @@ function item_post(App $a) { if (strlen($attachments)) { $attachments .= ','; } - $attachments .= '[attach]href="' . DI::baseUrl() . '/attach/' . $attachment['id'] . - '" length="' . $attachment['filesize'] . '" type="' . $attachment['filetype'] . - '" title="' . ($attachment['filename'] ? $attachment['filename'] : '') . '"[/attach]'; + $attachments .= Post\Media::getAttachElement(DI::baseUrl() . '/attach/' . $attachment['id'], + $attachment['filesize'], $attachment['filetype'], $attachment['filename'] ?? ''); } $body = str_replace($match[1],'',$body); } diff --git a/src/DI.php b/src/DI.php index e259c271a..06397d0e2 100644 --- a/src/DI.php +++ b/src/DI.php @@ -239,6 +239,14 @@ abstract class DI return self::$dice->create(Factory\Api\Mastodon\Account::class); } + /** + * @return Factory\Api\Mastodon\Attachment + */ + public static function mstdnAttachment() + { + return self::$dice->create(Factory\Api\Mastodon\Attachment::class); + } + /** * @return Factory\Api\Mastodon\Emoji */ diff --git a/src/Factory/Api/Mastodon/Attachment.php b/src/Factory/Api/Mastodon/Attachment.php new file mode 100644 index 000000000..e681b34c2 --- /dev/null +++ b/src/Factory/Api/Mastodon/Attachment.php @@ -0,0 +1,96 @@ +. + * + */ + +namespace Friendica\Factory\Api\Mastodon; + +use Friendica\App\BaseURL; +use Friendica\BaseFactory; +use Friendica\Network\HTTPException; +use Friendica\Model\Post; +use Friendica\Repository\ProfileField; +use Friendica\Util\Proxy; +use Psr\Log\LoggerInterface; + +class Attachment extends BaseFactory +{ + /** @var BaseURL */ + protected $baseUrl; + /** @var ProfileField */ + protected $profileField; + /** @var Field */ + protected $mstdnField; + + public function __construct(LoggerInterface $logger, BaseURL $baseURL, ProfileField $profileField, Field $mstdnField) + { + parent::__construct($logger); + + $this->baseUrl = $baseURL; + $this->profileField = $profileField; + $this->mstdnField = $mstdnField; + } + + /** + * @param int $uriId Uri-ID of the attachments + * @return array + * @throws HTTPException\InternalServerErrorException + * @throws \ImagickException + */ + public function createFromUriId(int $uriId) + { + $attachments = []; + foreach (Post\Media::getByURIId($uriId) as $attachment) { + + $filetype = !empty($attachment['mimetype']) ? strtolower(substr($attachment['mimetype'], 0, strpos($attachment['mimetype'], '/'))) : ''; + + if (($filetype == 'audio') || ($attachment['type'] == Post\Media::AUDIO)) { + $type = 'audio'; + } elseif (($filetype == 'video') || ($attachment['type'] == Post\Media::VIDEO)) { + $type = 'video'; + } elseif ($attachment['mimetype'] == 'image/gif') { + $type = 'gifv'; + } elseif (($filetype == 'image') || ($attachment['type'] == Post\Media::IMAGE)) { + $type = 'image'; + } else { + $type = 'unknown'; + } + + $remote = $attachment['url']; + if ($type == 'image') { + if (Proxy::isLocalImage($attachment['url'])) { + $url = $attachment['url']; + $preview = $attachment['preview'] ?? $url; + $remote = ''; + } else { + $url = Proxy::proxifyUrl($attachment['url']); + $preview = Proxy::proxifyUrl($attachment['url'], false, Proxy::SIZE_SMALL); + } + } else { + $url = ''; + $preview = ''; + } + + $object = new \Friendica\Object\Api\Mastodon\Attachment($attachment, $type, $url, $preview, $remote); + $attachments[] = $object->toArray(); + } + + return $attachments; + } +} diff --git a/src/Factory/Api/Mastodon/Status.php b/src/Factory/Api/Mastodon/Status.php index b4ef0a0f7..5aac0f80c 100644 --- a/src/Factory/Api/Mastodon/Status.php +++ b/src/Factory/Api/Mastodon/Status.php @@ -82,9 +82,11 @@ class Status extends BaseFactory $mentions = DI::mstdnMention()->createFromUriId($uriId); $tags = DI::mstdnTag()->createFromUriId($uriId); - $attachment = BBCode::getAttachmentData($item['body']); - $card = new \Friendica\Object\Api\Mastodon\Card($attachment); + $data = BBCode::getAttachmentData($item['body']); + $card = new \Friendica\Object\Api\Mastodon\Card($data); - return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card); + $attachments = DI::mstdnAttachment()->createFromUriId($uriId); + + return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts, $userAttributes, $sensitive, $application, $mentions, $tags, $card, $attachments); } } diff --git a/src/Model/Item.php b/src/Model/Item.php index 46d28ee82..3b1a58cc8 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -968,6 +968,14 @@ class Item while ($item = DBA::fetch($items)) { if (empty($content_fields['verb']) || !in_array($content_fields['verb'], self::ACTIVITIES)) { + if (!empty($content_fields['body'])) { + $content_fields['raw-body'] = trim($content_fields['raw-body'] ?? $content_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']); + } + self::updateContent($content_fields, ['uri-id' => $item['uri-id']]); if (empty($item['icid'])) { @@ -994,6 +1002,10 @@ class Item } } + if (!empty($fields['attach'])) { + Post\Media::insertFromAttachment($item['uri-id'], $fields['attach']); + } + Post\DeliveryData::update($item['uri-id'], $delivery_data); self::updateThread($item['id']); @@ -1826,6 +1838,10 @@ class Item // Check for hashtags in the body and repair or add hashtag links $item['body'] = self::setHashtags($item['body']); + if (!empty($item['attach'])) { + Post\Media::insertFromAttachment($item['uri-id'], $item['attach']); + } + // Fill the cache field self::putInCache($item); diff --git a/src/Model/Post/Media.php b/src/Model/Post/Media.php index ec3bf967b..57668fa99 100644 --- a/src/Model/Post/Media.php +++ b/src/Model/Post/Media.php @@ -24,6 +24,7 @@ namespace Friendica\Model\Post; use Friendica\Core\Logger; use Friendica\Core\System; use Friendica\Database\DBA; +use Friendica\DI; use Friendica\Util\Images; /** @@ -34,11 +35,12 @@ use Friendica\Util\Images; */ class Media { - const UNKNOWN = 0; - const IMAGE = 1; - const VIDEO = 2; - const AUDIO = 3; - const TORRENT = 16; + const UNKNOWN = 0; + const IMAGE = 1; + const VIDEO = 2; + const AUDIO = 3; + const TORRENT = 16; + const DOCUMENT = 128; /** * Insert a post-media record @@ -46,25 +48,90 @@ class Media * @param array $media * @return void */ - public static function insert(array $media) + public static function insert(array $media, bool $force = false) { - if (empty($media['url']) || empty($media['uri-id'])) { + if (empty($media['url']) || empty($media['uri-id']) || empty($media['type'])) { + Logger::warning('Incomplete media data', ['media' => $media]); return; } - if (DBA::exists('post-media', ['uri-id' => $media['uri-id'], 'url' => $media['url']])) { + // "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 + $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()]); return; } - $fields = ['type', 'mimetype', 'height', 'width', 'size', 'preview', 'preview-height', 'preview-width', 'description']; + $fields = ['mimetype', 'height', 'width', 'size', 'preview', 'preview-height', 'preview-width', 'description']; foreach ($fields as $field) { if (empty($media[$field])) { unset($media[$field]); } } - if ($media['type'] == self::IMAGE) { + // We are storing as fast as possible to avoid duplicated network requests + // when fetching additional information for pictures and other content. + $result = DBA::insert('post-media', $media, true); + Logger::info('Stored media', ['result' => $result, 'media' => $media, 'callstack' => System::callstack()]); + $stored = $media; + + $media = self::fetchAdditionalData($media); + + if (array_diff_assoc($media, $stored)) { + $result = DBA::insert('post-media', $media, true); + Logger::info('Updated media', ['result' => $result, 'media' => $media]); + } else { + Logger::info('Nothing to update', ['media' => $media]); + } + } + + /** + * Creates the "[attach]" element from the given attributes + * + * @param string $href + * @param integer $length + * @param string $type + * @param string $title + * @return string "[attach]" element + */ + public static function getAttachElement(string $href, int $length, string $type, string $title = '') + { + $media = self::fetchAdditionalData(['type' => self::DOCUMENT, 'url' => $href, + 'size' => $length, 'mimetype' => $type, 'description' => $title]); + + return '[attach]href="' . $media['url'] . '" length="' . $media['size'] . + '" type="' . $media['mimetype'] . '" title="' . $media['description'] . '"[/attach]'; + } + + /** + * Fetch additional data for the provided media array + * + * @param array $media + * @return array media array with additional data + */ + public static function fetchAdditionalData(array $media) + { + // Fetch the mimetype or size if missing. + // We don't do it for torrent links since they need special treatment. + // We don't do this for images, since we are fetching their details some lines later anyway. + if (!in_array($media['type'], [self::TORRENT, self::IMAGE]) && (empty($media['mimetype']) || empty($media['size']))) { + $timeout = DI::config()->get('system', 'xrd_timeout'); + $curlResult = DI::httpRequest()->head($media['url'], ['timeout' => $timeout]); + if ($curlResult->isSuccess()) { + $header = $curlResult->getHeaderArray(); + if (empty($media['mimetype']) && !empty($header['content-type'])) { + $media['mimetype'] = $header['content-type']; + } + if (empty($media['size']) && !empty($header['content-length'])) { + $media['size'] = $header['content-length']; + } + } + } + + $filetype = !empty($media['mimetype']) ? strtolower(substr($media['mimetype'], 0, strpos($media['mimetype'], '/'))) : ''; + + if (($media['type'] == self::IMAGE) || ($filetype == 'image')) { $imagedata = Images::getInfoFromURLCached($media['url']); if (!empty($imagedata)) { $media['mimetype'] = $imagedata['mime']; @@ -80,9 +147,7 @@ class Media } } } - - $result = DBA::insert('post-media', $media, true); - Logger::info('Stored media', ['result' => $result, 'media' => $media, 'callstack' => System::callstack()]); + return $media; } /** @@ -168,4 +233,41 @@ class Media return trim($body); } + + /** + * Add media links from the attach field + * + * @param integer $uriid + * @param string $attach + * @return void + */ + public static function insertFromAttachment(int $uriid, string $attach) + { + if (!preg_match_all('|\[attach\]href=\"(.*?)\" length=\"(.*?)\" type=\"(.*?)\"(?: title=\"(.*?)\")?|', $attach, $matches, PREG_SET_ORDER)) { + return; + } + + foreach ($matches as $attachment) { + $media['type'] = self::DOCUMENT; + $media['uri-id'] = $uriid; + $media['url'] = $attachment[1]; + $media['size'] = $attachment[2]; + $media['mimetype'] = $attachment[3]; + $media['description'] = $attachment[4] ?? ''; + + self::insert($media); + } + } + + /** + * Retrieves the media attachments associated with the provided item ID. + * + * @param int $uri_id + * @return array + * @throws \Exception + */ + public static function getByURIId(int $uri_id) + { + return DBA::selectToArray('post-media', [], ['uri-id' => $uri_id]); + } } diff --git a/src/Object/Api/Mastodon/Attachment.php b/src/Object/Api/Mastodon/Attachment.php new file mode 100644 index 000000000..1651e9c40 --- /dev/null +++ b/src/Object/Api/Mastodon/Attachment.php @@ -0,0 +1,80 @@ +. + * + */ + +namespace Friendica\Object\Api\Mastodon; + +use Friendica\BaseEntity; + +/** + * Class Attachment + * + * @see https://docs.joinmastodon.org/entities/attachment + */ +class Attachment extends BaseEntity +{ + /** @var string */ + protected $id; + /** @var string */ + protected $type; + /** @var string */ + protected $url; + /** @var string */ + protected $preview_url; + /** @var string */ + protected $remote_url; + /** @var string */ + protected $text_url; + /** @var string */ + protected $description; + + /** + * Creates an attachment + * + * @param array $attachment + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public function __construct(array $attachment, string $type, string $url, string $preview, string $remote) + { + $this->id = (string)$attachment['id']; + $this->type = $type; + $this->url = $url; + $this->preview_url = $preview; + $this->remote_url = $remote; + $this->text_url = $this->remote_url ?? $this->url; + $this->description = $attachment['description']; + } + + /** + * Returns the current entity as an array + * + * @return array + */ + public function toArray() + { + $attachment = parent::toArray(); + + if (empty($attachment['remote_url'])) { + $attachment['remote_url'] = null; + } + + return $attachment; + } +} diff --git a/src/Object/Api/Mastodon/Status.php b/src/Object/Api/Mastodon/Status.php index 2d2beb583..558069f19 100644 --- a/src/Object/Api/Mastodon/Status.php +++ b/src/Object/Api/Mastodon/Status.php @@ -97,7 +97,7 @@ class Status extends BaseEntity * @param array $item * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public function __construct(array $item, Account $account, Counts $counts, UserAttributes $userAttributes, bool $sensitive, Application $application, array $mentions, array $tags, Card $card) + public function __construct(array $item, Account $account, Counts $counts, UserAttributes $userAttributes, bool $sensitive, Application $application, array $mentions, array $tags, Card $card, array $attachments) { $this->id = (string)$item['uri-id']; $this->created_at = DateTimeFormat::utc($item['created'], DateTimeFormat::ATOM); @@ -130,7 +130,7 @@ class Status extends BaseEntity $this->reblog = null; /// @todo $this->application = $application->toArray(); $this->account = $account->toArray(); - $this->media_attachments = []; /// @todo + $this->media_attachments = $attachments; $this->mentions = $mentions; $this->tags = $tags; $this->emojis = []; diff --git a/src/Protocol/ActivityPub/Processor.php b/src/Protocol/ActivityPub/Processor.php index 63732b0e1..4fa8a4e9f 100644 --- a/src/Protocol/ActivityPub/Processor.php +++ b/src/Protocol/ActivityPub/Processor.php @@ -196,7 +196,8 @@ class Processor $item['attach'] = ''; } - $item['attach'] .= '[attach]href="' . $attach['url'] . '" length="' . ($attach['length'] ?? '0') . '" type="' . $attach['mediaType'] . '" title="' . ($attach['name'] ?? '') . '"[/attach]'; + $item['attach'] .= Post\Media::getAttachElement($attach['url'], + $attach['length'] ?? 0, $attach['mediaType'], $attach['name'] ?? ''); } } } diff --git a/src/Protocol/DFRN.php b/src/Protocol/DFRN.php index d20864cf7..218608102 100644 --- a/src/Protocol/DFRN.php +++ b/src/Protocol/DFRN.php @@ -39,6 +39,7 @@ use Friendica\Model\ItemURI; use Friendica\Model\Mail; use Friendica\Model\Notify\Type; use Friendica\Model\PermissionSet; +use Friendica\Model\Post; use Friendica\Model\Post\Category; use Friendica\Model\Profile; use Friendica\Model\Tag; @@ -2176,7 +2177,7 @@ class DFRN $item["attach"] = ""; } - $item["attach"] .= '[attach]href="' . $href . '" length="' . $length . '" type="' . $type . '" title="' . $title . '"[/attach]'; + $item["attach"] .= Post\Media::getAttachElement($href, $length, $type, $title); break; } } diff --git a/src/Protocol/Feed.php b/src/Protocol/Feed.php index 67baf4b2a..4eb638be3 100644 --- a/src/Protocol/Feed.php +++ b/src/Protocol/Feed.php @@ -33,6 +33,7 @@ use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\Item; +use Friendica\Model\Post; use Friendica\Model\Tag; use Friendica\Model\User; use Friendica\Util\DateTimeFormat; @@ -457,7 +458,7 @@ class Feed $attachments[] = ["link" => $href, "type" => $type, "length" => $length]; - $item["attach"] .= '[attach]href="' . $href . '" length="' . $length . '" type="' . $type . '"[/attach]'; + $item["attach"] .= Post\Media::getAttachElement($href, $length, $type); } $taglist = []; diff --git a/src/Protocol/OStatus.php b/src/Protocol/OStatus.php index 4b67d8ecb..5c157c980 100644 --- a/src/Protocol/OStatus.php +++ b/src/Protocol/OStatus.php @@ -36,6 +36,7 @@ use Friendica\Model\Contact; use Friendica\Model\Conversation; use Friendica\Model\Item; use Friendica\Model\ItemURI; +use Friendica\Model\Post; use Friendica\Model\Tag; use Friendica\Model\User; use Friendica\Network\Probe; @@ -1126,7 +1127,8 @@ class OStatus if (!isset($attribute['length'])) { $attribute['length'] = "0"; } - $item["attach"] .= '[attach]href="'.$attribute['href'].'" length="'.$attribute['length'].'" type="'.$attribute['type'].'" title="'.($attribute['title'] ?? '') .'"[/attach]'; + $item["attach"] .= Post\Media::getAttachElement($attribute['href'], + $attribute['length'], $attribute['type'], $attribute['title'] ?? ''); } break; case "related":