123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405 |
- <?php
- declare(strict_types = 1);
- namespace Plugin\Embed;
- use App\Core\Cache;
- use App\Core\DB\DB;
- use App\Core\Event;
- use App\Core\GSFile;
- use App\Core\HTTPClient;
- use function App\Core\I18n\_m;
- use App\Core\Log;
- use App\Core\Modules\Plugin;
- use App\Core\Router\RouteLoader;
- use App\Core\Router\Router;
- use App\Entity\Note;
- use App\Util\Common;
- use App\Util\Exception\ClientException;
- use App\Util\Exception\DuplicateFoundException;
- use App\Util\Exception\NotFoundException;
- use App\Util\Exception\ServerException;
- use App\Util\Formatting;
- use App\Util\TemporaryFile;
- use Component\Attachment\Entity\Attachment;
- use Component\Link\Entity\Link;
- use Embed\Embed as LibEmbed;
- use Exception;
- use Symfony\Component\HttpFoundation\Request;
- use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
- use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
- use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
- use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
- class Embed extends Plugin
- {
- public function version(): string
- {
- return '3.0.1';
- }
-
- public bool $check_whitelist = false;
- public bool $check_blacklist = false;
- public array $domain_whitelist = [
-
- '.*',
- ];
- public array $domain_blacklist = [];
-
- public bool $store_image = true;
- public ?int $thumbnail_width;
- public ?int $thumbnail_height;
- public ?int $max_size;
- public ?bool $smart_crop;
-
- private function getMaxFileSize(): int
- {
- return min(Common::config('plugin_embed', 'max_file_size'), Common::config('attachments', 'file_quota'));
- }
-
- public function onAddRoute(RouteLoader $m): bool
- {
- $m->connect('oembed', 'main/oembed', Controller\OEmbed::class);
- return Event::next;
- }
-
- public function onShowHeadElements(Request $request, array &$result): bool
- {
- $matches = [];
- preg_match(',/?([^/]+)/?(.*),', $request->getPathInfo(), $matches);
- $url = match ($matches[1]) {
- 'attachment' => "{$matches[1]}/{$matches[2]}",
- default => null,
- };
- if (\is_null($url)) {
- foreach (['xml', 'json'] as $format) {
- $result[] = [
- 'link' => [
- 'rel' => 'alternate',
- 'type' => "application/{$format}+oembed",
- 'href' => Router::url('oembed', ['format' => $format, 'url' => $url]),
- 'title' => 'oEmbed',
- ], ];
- }
- }
- return Event::next;
- }
-
- public function onViewLink(array $vars, array &$res): bool
- {
- $link = $vars['link'];
- try {
- $embed = Cache::get(
- 'attachment-embed-' . $link->getId(),
- fn () => DB::findOneBy('attachment_embed', ['link_id' => $link->getId()]),
- );
- } catch (DuplicateFoundException $e) {
- Log::warning($e->getMessage());
- return Event::next;
- } catch (NotFoundException) {
- Log::debug("Embed doesn't have a representation for the link id={$link->getId()}. Must have been stored before the plugin was enabled.");
- return Event::next;
- }
- $attributes = $embed->getImageHTMLAttributes();
- $res[] = Formatting::twigRenderFile(
- 'embed/embedView.html.twig',
- ['embed' => $embed, 'attributes' => $attributes, 'link' => $link, 'note' => $vars['note']],
- );
- return Event::stop;
- }
-
- public function onNewLinkFromNote(Link $link, Note $note): bool
- {
-
- $mimetype = $link->getMimetype();
- if (\is_null($mimetype) || !(Formatting::startsWith($mimetype, 'text/html') || Formatting::startsWith($mimetype, 'application/xhtml+xml'))) {
- return Event::next;
- }
-
- $attachment_embed = DB::find('attachment_embed', ['link_id' => $link->getId()]);
- if (!\is_null($attachment_embed)) {
- return Event::next;
- }
-
- $attachment_to_link = DB::find('attachment_to_link', ['link_id' => $link->getId()]);
- if (!\is_null($attachment_to_link)) {
- $attachment_id = $attachment_to_link->getAttachmentId();
- try {
- $attachment = DB::findOneBy('attachment', ['id' => $attachment_id]);
- $attachment->livesIncrementAndGet();
- return Event::next;
- } catch (DuplicateFoundException|NotFoundException $e) {
- Log::error($e->getMessage(), context: [$e]);
- }
- }
-
- $embed_data = $this->getEmbedLibMetadata($link->getUrl());
- $embed_data['link_id'] = $link->getId();
- $img_data = $this->downloadThumbnail($embed_data['thumbnail_url']);
- switch ($img_data) {
- case null:
- $embed_data['thumbnail_url'] = null;
-
- case false:
- DB::persist($attachment = Attachment::create(['mimetype' => $link->getMimetype()]));
- Event::handle('AttachmentStoreNew', [&$attachment]);
- break;
- default:
- $temp_file = new TemporaryFile();
- $temp_file->write($img_data);
- try {
- $attachment = GSFile::storeFileAsAttachment($temp_file);
- $embed_data['attachment_id'] = $attachment->getId();
- } catch (ClientException) {
- DB::persist($attachment = Attachment::create(['mimetype' => $link->getMimetype()]));
- Event::handle('AttachmentStoreNew', [&$attachment]);
- }
- }
- $embed_data['attachment_id'] = $attachment->getId();
- DB::persist(Entity\AttachmentEmbed::create($embed_data));
- DB::flush();
- return Event::stop;
- }
-
- private function allowedLink(string $url): bool
- {
- $passed_whitelist = !$this->check_whitelist;
- $passed_blacklist = !$this->check_blacklist;
- if ($this->check_whitelist) {
- $passed_whitelist = false;
- $host = parse_url($url, \PHP_URL_HOST);
- foreach ($this->domain_whitelist as $regex => $provider) {
- if (preg_match("/{$regex}/", $host)) {
- $passed_whitelist = true;
- }
- }
- }
- if ($this->check_blacklist) {
-
- $host = parse_url($url, \PHP_URL_HOST);
- foreach ($this->domain_blacklist as $regex => $provider) {
- if (preg_match("/{$regex}/", $host)) {
- $passed_blacklist = false;
- }
- }
- }
- return $passed_whitelist && $passed_blacklist;
- }
-
- private function getEmbedLibMetadata(string $url): array
- {
- Log::info("Trying to find Embed data for {$url} with 'oscarotero/Embed'");
- $embed = new LibEmbed();
- $info = $embed->get($url);
- $metadata['title'] = $info->title;
- $metadata['description'] = $info->description;
- $metadata['author_name'] = $info->authorName;
- $root_url = parse_url($url);
- $root_url = "{$root_url['scheme']}://{$root_url['host']}";
- $metadata['author_url'] = $info->authorUrl ? (string) $info->authorUrl : $root_url;
- $metadata['provider_name'] = $info->providerName;
- $metadata['provider_url'] = (string) ($info->providerUrl ?? $metadata['author_name']);
- if (!\is_null($info->image)) {
- $thumbnail_url = (string) $info->image;
- } else {
- $thumbnail_url = (string) $info->favicon;
- }
-
- $metadata['thumbnail_url'] = $thumbnail_url;
- return self::normalizeEmbedLibMetadata($metadata);
- }
-
- private static function normalizeEmbedLibMetadata(array $metadata): array
- {
- if (isset($metadata['thumbnail_url'])) {
-
-
-
- if ($metadata['thumbnail_url'][0] == '/') {
- $metadata['thumbnail_url'] = "{$metadata['provider_url']}{$metadata['thumbnail_url']}";
- }
-
-
- if ($metadata['thumbnail_url'] == 'https://s0.wp.com/i/blank.jpg') {
- $metadata['thumbnail_url'] = null;
- }
- }
- return $metadata;
- }
-
- private function downloadThumbnail(string $url): bool|string|null
- {
-
- if (!Common::isValidHttpUrl($url)) {
- Log::debug("Invalid URL ({$url}) in Embed->downloadThumbnail.");
- return null;
- }
-
- if (!$this->allowedLink($url)) {
- Log::info("Blocked URL ({$url}) in Embed->downloadThumbnail.");
- return false;
- }
-
- $head = HTTPClient::head($url);
- try {
- $headers = $head->getHeaders();
- } catch (ClientExceptionInterface|RedirectionExceptionInterface|ServerExceptionInterface|TransportExceptionInterface $e) {
- Log::debug('Embed->downloadThumbnail@HTTPHead->getHeaders: ' . $e->getMessage(), [$e]);
- return null;
- }
- if (empty($headers['content-type']) || GSFile::mimetypeMajor($headers['content-type'][0]) !== 'image') {
- Log::debug("URL ({$url}) doesn't point to an image (content-type: " . (!empty($headers['content-type'][0]) ? $headers['content-type'][0] : 'not available') . ') in Embed->downloadThumbnail.');
- return null;
- }
-
- $file_size = $headers['content-length'][0] ?? null;
- $max_size = $this->getMaxFileSize();
- if (\is_null($file_size) || $file_size > $max_size) {
- Log::debug("Went to download remote thumbnail of size {$file_size} but the plugin's filesize limit is {$max_size} so we aborted in Embed->downloadThumbnail.");
- return false;
- }
-
- Log::debug("Downloading remote thumbnail from URL: {$url} in Embed->downloadThumbnail.");
- return HTTPClient::get($url)->getContent();
- }
- public function onAttachmentGetBestTitle(Attachment $attachment, Note $note, ?string &$title)
- {
- try {
- $embed = DB::findOneBy('attachment_embed', ['attachment_id' => $attachment->getId()]);
- $title = $embed->getTitle();
- return Event::stop;
- } catch (NotFoundException) {
- }
- return Event::next;
- }
-
- public function onPluginVersion(array &$versions): bool
- {
- $versions[] = [
- 'name' => 'Embed',
- 'version' => $this->version(),
- 'author' => 'Mikael Nordfeldth, Hugo Sales, Diogo Peralta Cordeiro',
- 'homepage' => GNUSOCIAL_PROJECT_URL,
- 'description',
- ];
- return Event::next;
- }
- }
|