diff --git a/.gitignore b/.gitignore index 99d6ebd..7c36eb5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ static/* vendor/ var/ .phpunit.cache/ +phpunit.xml diff --git a/Client/Client.php b/Client/Client.php new file mode 100644 index 0000000..44b947c --- /dev/null +++ b/Client/Client.php @@ -0,0 +1,54 @@ +userAgent = $userAgent; + } + + public function jsonGet(string $url, array $headers = []): array { + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_HEADER => 0, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_FAILONERROR => true, + CURLOPT_USERAGENT => $this->userAgent, + CURLOPT_HTTPHEADER => $headers, + ]); + $jsonString = curl_exec($ch); + if (curl_errno($ch)) { + curl_close($ch); + throw new ClientException(curl_error($ch)); + } + curl_close($ch); + + return json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR); + } + + public function isAccessible(string $url, array $headers = []): bool { + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_NOBODY => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_FAILONERROR => true, + CURLOPT_USERAGENT => $this->userAgent, + CURLOPT_HTTPHEADER => $headers, + ]); + curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + return 200 === $httpCode; + } +} diff --git a/Exception/ClientException.php b/Exception/ClientException.php new file mode 100644 index 0000000..a259959 --- /dev/null +++ b/Exception/ClientException.php @@ -0,0 +1,10 @@ +url = $url; } - public function getUrl(): string { - return $this->url; - } - public function toDomElement(\DomDocument $domDocument): \DomElement { $image = $domDocument->createElement('img'); $image->setAttribute('src', $this->url); diff --git a/Media/Link.php b/Media/Link.php new file mode 100644 index 0000000..ce49222 --- /dev/null +++ b/Media/Link.php @@ -0,0 +1,22 @@ +url = $url; + } + + public function toDomElement(\DomDocument $domDocument): \DomElement { + $p = $domDocument->createElement('p'); + $a = $p->appendChild($domDocument->createElement('a')); + $a->setAttribute('href', $this->url); + $a->appendChild($domDocument->createTextNode($this->url)); + + return $p; + } +} diff --git a/Processor/AbstractProcessor.php b/Processor/AbstractProcessor.php new file mode 100644 index 0000000..df46cae --- /dev/null +++ b/Processor/AbstractProcessor.php @@ -0,0 +1,31 @@ +settings = $settings; + $this->settings->setProcessor(get_class($this)); + } + + /** + * @param FreshRSS_Entry $entry + * @return FreshRSS_Entry + */ + abstract public function process($entry); + + protected function isRedditLink($entry): bool { + return (bool) strpos($entry->link(), static::MATCH_REDDIT); + } +} diff --git a/Processor/BeforeDisplayProcessor.php b/Processor/BeforeDisplayProcessor.php new file mode 100644 index 0000000..04991e6 --- /dev/null +++ b/Processor/BeforeDisplayProcessor.php @@ -0,0 +1,129 @@ +settings->getDisplayImage()) { + $this->transformers[] = new AgnosticImageTransformer($this->settings); + $this->transformers[] = new ImgurVideoTransformer($this->settings); + $this->transformers[] = new ImgurImageTransformer($this->settings); + } + if ($this->settings->getDisplayVideo()) { + $this->transformers[] = new AgnosticVideoTransformer($this->settings); + } + $this->transformers[] = new AgnosticLinkTransformer($this->settings); + } + + /** + * @param FreshRSS_Entry $entry + * @return FreshRSS_Entry + */ + public function process($entry) { + if (false === $this->isRedditLink($entry)) { + return $entry; + } + + $content = new Content($entry->content()); + $improved = $this->getImprovedContent($content); + $original = $this->getOriginalContent($content); + $metadata = $this->getMetadataContent($content); + + if (!$this->settings->getDisplayThumbnails()) { + $entry->_attributes('thumbnail', null); + $entry->_attributes('enclosures', null); + } + $entry->_content("{$improved}{$content->getReal()}{$original}{$metadata}"); + $entry->_link($content->getContentLink()); + + return $entry; + } + + private function getImprovedContent(Content $content): string { + $improved = $content->hasBeenPreprocessed() ? $content->getPreprocessed() : $this->processContent($content); + + if ($improved === '') { + return ''; + } + + $dom = new \DomDocument('1.0', 'UTF-8'); + $dom->loadHTML($improved, LIBXML_NOERROR); + + if (!$this->settings->getDisplayImage()) { + $images = $dom->getElementsByTagName('img'); + // See https://www.php.net/manual/en/class.domnodelist.php#83390 + for ($i = $images->length; --$i >= 0; ) { + $image = $images->item($i); + $image->parentNode->removeChild($image); + } + } + + if (!$this->settings->getDisplayVideo()) { + $videos = $dom->getElementsByTagName('video'); + // See https://www.php.net/manual/en/class.domnodelist.php#83390 + for ($i = $videos->length; --$i >= 0; ) { + $video = $videos->item($i); + $video->parentNode->removeChild($video); + } + } + + if ($this->settings->getMutedVideo()) { + $videos = $dom->getElementsByTagName('video'); + foreach ($videos as $video) { + $video->setAttribute('muted', 'true'); + } + $audios = $dom->getElementsByTagName('audio'); + foreach ($audios as $audio) { + $audio->setAttribute('muted', 'true'); + } + } + + return $dom->saveHTML(); + } + + private function processContent(Content $content): string { + foreach ($this->transformers as $transformer) { + if (!$transformer->canTransform($content)) { + continue; + } + + try { + return $transformer->transform($content); + } catch (Throwable $e) { + Minz_Log::error("{$e->__toString()} - {$content->getContentLink()}"); + } + } + + return ''; + } + + private function getOriginalContent(Content $content): string { + if ($this->settings->getDisplayOriginal()) { + return $content->getRaw(); + } + + return ''; + } + + private function getMetadataContent(Content $content): string { + if ($this->settings->getDisplayMetadata()) { + return "
{$content->getMetadata()}
"; + } + + return ''; + } +} diff --git a/Processor/BeforeInsertProcessor.php b/Processor/BeforeInsertProcessor.php new file mode 100644 index 0000000..474afc6 --- /dev/null +++ b/Processor/BeforeInsertProcessor.php @@ -0,0 +1,68 @@ +transformers[] = new AgnosticImageTransformer($this->settings); + $this->transformers[] = new ImgurGalleryWithClientIdTransformer($this->settings); + $this->transformers[] = new ImgurImageTransformer($this->settings); + $this->transformers[] = new ImgurVideoTransformer($this->settings); + $this->transformers[] = new GfycatVideoTransformer($this->settings); + $this->transformers[] = new RedditVideoTransformer($this->settings); + $this->transformers[] = new RedditGalleryTransformer($this->settings); + + foreach ($this->transformers as $transformer) { + $transformer->setClient($client); + } + } + + /** + * @param FreshRSS_Entry $entry + * @return FreshRSS_Entry + */ + public function process($entry) { + if (false === $this->isRedditLink($entry)) { + return $entry; + } + + $newContent = ''; + $content = new Content($entry->content()); + + foreach ($this->transformers as $transformer) { + if (!$transformer->canTransform($content)) { + continue; + } + + try { + $newContent = $transformer->transform($content); + break; + } catch (Throwable $e) { + Minz_Log::error("{$e->__toString()} - {$content->getContentLink()}"); + } + } + + if ($newContent !== '') { + $entry->_content("{$newContent}{$content->getRaw()}"); + } + + return $entry; + } +} diff --git a/README.md b/README.md index f283d36..62ad2d1 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ At the moment, the following resources are recognized:   |match | type | support -------|------|------|-------- -1 | links finished by jpg, png, gif, bmp | image | full +1 | links finished by jpg, jpeg, png, gif, bmp | image | full 2 | imgur links finished by gifv | video | full 3 | imgur links finished with a token | image | partial 4 | links finished by webm, mp4 | video | full @@ -16,7 +16,7 @@ At the moment, the following resources are recognized: 6 | redgifs links finished with a token | video | none 7 | reddit links finished with a token | video | limited (no audio) 8 | reddit image galleries | image | full -9 | imgur image galleries | image | full with API client id; partial without +9 | imgur image galleries | image | full with API client id; none without **Note** the support from redgifs links with a token went from full to none after a change in their API. @@ -31,6 +31,7 @@ Display images | Choose if images are displayed | **True** Display videos | Choose if videos are displayed | **True** Display original content | Choose if original contents are displayed | **True** Display metadata | Choose if original content metadata are displayed | **False** +Display thumbnails | Choose if feed enclosure are displayed | **False** **Note:** When the *display original content* option is set to *true*, text content will be displayed twice. Once from the extracted content and once from the original content. To have a nicer interface, it is recommended to set that option to *false*. diff --git a/Settings.php b/Settings.php new file mode 100644 index 0000000..0c119c3 --- /dev/null +++ b/Settings.php @@ -0,0 +1,70 @@ +settings = $settings; + } + + public function hasImgurClientId(): bool { + return $this->getImgurClientId() !== ''; + } + + public function getImgurClientId(): string { + return $this->settings['imgurClientId'] ?? ''; + } + + public function getDefaultImageHeight(): int { + return static::DEFAULT_IMAGEHEIGHT; + } + + public function getImageHeight(): int { + return $this->settings['imageHeight'] ?? static::DEFAULT_IMAGEHEIGHT; + } + + public function getMutedVideo(): bool { + return $this->settings['mutedVideo'] ?? static::DEFAULT_MUTEDVIDEO; + } + + public function getDisplayImage(): bool { + return $this->settings['displayImage'] ?? static::DEFAULT_DISPLAYIMAGE; + } + + public function getDisplayVideo(): bool { + return $this->settings['displayVideo'] ?? static::DEFAULT_DISPLAYVIDEO; + } + + public function getDisplayOriginal(): bool { + return $this->settings['displayOriginal'] ?? static::DEFAULT_DISPLAYORIGINAL; + } + + public function getDisplayMetadata(): bool { + return $this->settings['displayMetadata'] ?? static::DEFAULT_DISPLAYMETADATA; + } + + public function getDisplayThumbnails(): bool { + return $this->settings['displayThumbnails'] ?? static::DEFAULT_DISPLAYTHUMBNAILS; + } + + public function getProcessor(): string { + return $this->processor; + } + + public function setProcessor(string $processor): void { + $this->processor = $processor; + } +} diff --git a/Transformer/AbstractTransformer.php b/Transformer/AbstractTransformer.php index 8a61e05..7e90ded 100644 --- a/Transformer/AbstractTransformer.php +++ b/Transformer/AbstractTransformer.php @@ -4,13 +4,22 @@ namespace RedditImage\Transformer; +use RedditImage\Client\Client; use RedditImage\Media\DomElementInterface; use RedditImage\Media\Image; use RedditImage\Media\Video; +use RedditImage\Settings; abstract class AbstractTransformer { protected const MATCH_REDDIT = 'reddit.com'; + protected Client $client; + protected Settings $settings; + + public function __construct(Settings $settings) { + $this->settings = $settings; + } + /** * @param FreshRSS_Entry $entry */ @@ -18,24 +27,26 @@ protected function isRedditLink($entry): bool { return (bool) strpos($entry->link(), static::MATCH_REDDIT); } - /** - * @return FreshRSS_Entry - */ - abstract public function transform($entry); - - protected function getOriginComment(string $origin): string { - $className = (new \ReflectionClass($this))->getShortName(); + public function setClient(Client $client): void { + $this->client = $client; + } - return "xExtension-RedditImage | $className | $origin"; + protected function getOriginComment(): string { + return sprintf( + 'xExtension-RedditImage/%s | %s | %s', + REDDITIMAGE_VERSION, + $this->settings->getProcessor(), + get_class($this) + ); } - protected function generateDom(string $origin, array $media = []): \DomDocument { + protected function generateDom(array $media = []): \DomDocument { $dom = new \DomDocument('1.0', 'UTF-8'); $div = $dom->appendChild($dom->createElement('div')); $div->setAttribute('class', 'reddit-image figure'); - $div->appendChild($dom->createComment($this->getOriginComment($origin))); + $div->appendChild($dom->createComment($this->getOriginComment())); foreach ($media as $medium) { if ($medium instanceof DomElementInterface) { diff --git a/Transformer/Agnostic/ImageTransformer.php b/Transformer/Agnostic/ImageTransformer.php new file mode 100644 index 0000000..32b4ec5 --- /dev/null +++ b/Transformer/Agnostic/ImageTransformer.php @@ -0,0 +1,25 @@ +.*\.(jpg|jpeg|png|gif|bmp))(\?.*)?$#'; + + public function canTransform(Content $content): bool { + return preg_match(self::MATCHING_REGEX, $content->getContentLink()) === 1; + } + + public function transform(Content $content): string { + preg_match(self::MATCHING_REGEX, $content->getContentLink(), $matches); + $dom = $this->generateDom([new Image($matches['link'])]); + + return $dom->saveHTML(); + } +} diff --git a/Transformer/Agnostic/LinkTransformer.php b/Transformer/Agnostic/LinkTransformer.php new file mode 100644 index 0000000..e88705b --- /dev/null +++ b/Transformer/Agnostic/LinkTransformer.php @@ -0,0 +1,22 @@ +hasReal(); + } + + public function transform(Content $content): string { + $dom = $this->generateDom([new Link($content->getContentLink())]); + + return $dom->saveHTML(); + } +} diff --git a/Transformer/Agnostic/VideoTransformer.php b/Transformer/Agnostic/VideoTransformer.php new file mode 100644 index 0000000..9521f47 --- /dev/null +++ b/Transformer/Agnostic/VideoTransformer.php @@ -0,0 +1,32 @@ +.+\.)(?Pwebm|mp4)$#'; + + public function canTransform(Content $content): bool { + return preg_match(self::MATCHING_REGEX, $content->getContentLink()) === 1; + } + + public function transform(Content $content): string { + $url = $content->getContentLink(); + preg_match(self::MATCHING_REGEX, $url, $matches); + + if (!$this->client->isAccessible($url)) { + return ''; + } + + $extension = $matches['extension']; + $dom = $this->generateDom([new Video("video/{$extension}", $url)]); + + return $dom->saveHTML(); + } +} diff --git a/Transformer/DisplayTransformer.php b/Transformer/DisplayTransformer.php deleted file mode 100644 index 6e5e3b6..0000000 --- a/Transformer/DisplayTransformer.php +++ /dev/null @@ -1,169 +0,0 @@ -displayImage = $displayImage; - $this->displayVideo = $displayVideo; - $this->mutedVideo = $mutedVideo; - $this->displayOriginal = $displayOriginal; - $this->displayMetadata = $displayMetadata; - $this->displayThumbnails = $displayThumbnails; - } - - /** - * @param FreshRSS_Entry $entry - * @return FreshRSS_Entry - */ - public function transform($entry) { - if (false === $this->isRedditLink($entry)) { - return $entry; - } - - $content = new Content($entry->content()); - - if (null === $content->getContentLink()) { - return $entry; - } - - $improved = $content->hasBeenPreprocessed() ? $this->getPreprocessedContent($content) : $this->getImprovedContent($content); - $original = $this->displayOriginal ? $content->getRaw() : ''; - $metadata = $this->displayMetadata ? "
{$content->getMetadata()}
" : ''; - - if (!$this->displayThumbnails) { - $entry->_attributes('thumbnail', null); - $entry->_attributes('enclosures', null); - } - - $entry->_content("{$improved}{$content->getReal()}{$original}{$metadata}"); - $entry->_link($content->getContentLink()); - - return $entry; - } - - private function getPreprocessedContent(Content $content): string { - $preprocessed = $content->getPreprocessed(); - if (!$this->displayImage && false !== strpos($preprocessed, 'img')) { - return ''; - } - if (!$this->displayVideo && false !== strpos($preprocessed, 'video')) { - return ''; - } - if (false === strpos($preprocessed, 'video')) { - return $preprocessed; - } - if (!$this->mutedVideo) { - return $preprocessed; - } - - $dom = new \DomDocument('1.0', 'UTF-8'); - $dom->loadHTML($preprocessed, LIBXML_NOERROR); - - $videos = $dom->getElementsByTagName('video'); - foreach ($videos as $video) { - $video->setAttribute('muted', 'true'); - } - $audios = $dom->getElementsByTagName('audio'); - foreach ($audios as $audio) { - $audio->setAttribute('muted', 'true'); - } - - return $dom->saveHTML(); - } - - private function getImprovedContent(Content $content): string { - $href = $content->getContentLink(); - - // Add image tag in content when the href links to an image - if (preg_match('#(jpg|png|gif|bmp)(\?.*)?$#', $href)) { - return $this->getNewImageContent($href, 'Image link'); - } - // Add video tag in content when the href links to an imgur gifv - if (preg_match('#(?P.*imgur.com/[^/]*.)gifv$#', $href, $matches)) { - return $this->getNewVideoContent($matches['gifv'], 'Imgur gifv'); - } - // Add image tag in content when the href links to an imgur image - if (preg_match('#(?Pimgur.com/[^/]*)$#', $href)) { - $href = "{$href}.png"; - return $this->getNewImageContent($href, 'Imgur token'); - } - // Add video tag in content when the href links to a video - if (preg_match('#(?P.+)(webm|mp4)$#', $href, $matches)) { - return $this->getNewVideoContent($matches['baseurl'], 'Video link'); - } - if (!$content->hasReal()) { - return $this->getNewLinkContent($href); - } - return ''; - } - - private function getNewImageContent(string $href, string $origin): ?string { - if (!$this->displayImage) { - return null; - } - - $dom = $this->generateDom($origin, [new Image($href)]); - return $dom->saveHTML(); - } - - private function getNewVideoContent(string $baseUrl, string $origin): ?string { - if (!$this->displayVideo) { - return null; - } - - $mp4Url = "{$baseUrl}mp4"; - if ($this->isAccessible($mp4Url)) { - $dom = $this->generateDom($origin, [new Video('video/mp4', $mp4Url)]); - $videos = $dom->getElementsByTagName('video'); - foreach ($videos as $video) { - $video->setAttribute('muted', 'true'); - } - return $dom->saveHTML(); - } - - $webmUrl = "{$baseUrl}webm"; - if ($this->isAccessible($webmUrl)) { - $dom = $this->generateDom($origin, [new Video('video/webm', $webmUrl)]); - $videos = $dom->getElementsByTagName('video'); - foreach ($videos as $video) { - $video->setAttribute('muted', 'true'); - } - return $dom->saveHTML(); - } - } - - private function isAccessible(string $href): bool { - $channel = curl_init($href); - curl_setopt($channel, CURLOPT_NOBODY, true); - curl_exec($channel); - $httpCode = curl_getinfo($channel, CURLINFO_HTTP_CODE); - curl_close($channel); - - return 200 === $httpCode; - } - - private function getNewLinkContent(string $href): string { - $dom = new \DomDocument('1.0', 'UTF-8'); - - $p = $dom->appendChild($dom->createElement('p')); - $a = $p->appendChild($dom->createElement('a')); - $a->setAttribute('href', $href); - $a->textContent = $href; - - return $dom->saveHTML(); - } -} diff --git a/Transformer/Gfycat/VideoTransformer.php b/Transformer/Gfycat/VideoTransformer.php new file mode 100644 index 0000000..d47d0fa --- /dev/null +++ b/Transformer/Gfycat/VideoTransformer.php @@ -0,0 +1,42 @@ +\w+)$#'; + + public function canTransform(Content $content): bool { + return preg_match(self::MATCHING_REGEX, $content->getContentLink()) === 1; + } + + public function transform(Content $content): string { + $arrayResponse = $this->getMediaMetadata($content); + + $mp4Url = $arrayResponse['gfyItem']['mp4Url'] ?? ''; + $webmUrl = $arrayResponse['gfyItem']['webmUrl'] ?? ''; + if ($mp4Url === '' || $webmUrl === '') { + return ''; + } + + $video = new Video('video/mp4', $mp4Url); + $video->addSource('video/webm', $webmUrl); + + $dom = $this->generateDom([$video]); + + return $dom->saveHTML(); + } + + private function getMediaMetadata(Content $content): array { + preg_match(self::MATCHING_REGEX, $content->getContentLink(), $matches); + + return $this->client->jsonGet("https://api.gfycat.com/v1/gfycats/{$matches['token']}"); + } +} diff --git a/Transformer/Imgur/GalleryWithClientIdTransformer.php b/Transformer/Imgur/GalleryWithClientIdTransformer.php new file mode 100644 index 0000000..1afc640 --- /dev/null +++ b/Transformer/Imgur/GalleryWithClientIdTransformer.php @@ -0,0 +1,49 @@ +\w+)@'; + + public function canTransform(Content $content): bool { + return $this->settings->hasImgurClientId() && preg_match(self::MATCHING_REGEX, $content->getContentLink()) === 1; + } + + public function transform(Content $content): string { + $gallery = []; + $media = $this->getMediaMetadata($content); + + foreach ($media as $medium) { + if (false !== strpos($medium['type'], 'video')) { + $gallery[] = new Video($medium['type'], $medium['link']); + } elseif (false !== strpos($medium['type'], 'image')) { + $gallery[] = new Image($medium['link']); + } + } + + if ($gallery !== []) { + $dom = $this->generateDom($gallery); + + return $dom->saveHTML(); + } + + return ''; + } + + private function getMediaMetadata(Content $content): array { + preg_match(self::MATCHING_REGEX, $content->getContentLink(), $matches); + $arrayResponse = $this->client->jsonGet("https://api.imgur.com/3/album/{$matches['imageHash']}/images", [ + "Authorization: Client-ID {$this->settings->getImgurClientId()}", + ]); + + return $arrayResponse['data'] ?? []; + } +} diff --git a/Transformer/Imgur/ImageTransformer.php b/Transformer/Imgur/ImageTransformer.php new file mode 100644 index 0000000..2b30471 --- /dev/null +++ b/Transformer/Imgur/ImageTransformer.php @@ -0,0 +1,22 @@ +getContentLink()) === 1; + } + + public function transform(Content $content): string { + $dom = $this->generateDom([new Image("{$content->getContentLink()}.png")]); + + return $dom->saveHTML(); + } +} diff --git a/Transformer/Imgur/VideoTransformer.php b/Transformer/Imgur/VideoTransformer.php new file mode 100644 index 0000000..bb070c2 --- /dev/null +++ b/Transformer/Imgur/VideoTransformer.php @@ -0,0 +1,25 @@ +.*imgur.com/[^/]*.)gifv$#'; + + public function canTransform(Content $content): bool { + return preg_match(self::MATCHING_REGEX, $content->getContentLink()) === 1; + } + + public function transform(Content $content): string { + preg_match(self::MATCHING_REGEX, $content->getContentLink(), $matches); + $dom = $this->generateDom([new Video('video/mp4', "{$matches['gifv']}mp4")]); + + return $dom->saveHTML(); + } +} diff --git a/Transformer/InsertTransformer.php b/Transformer/InsertTransformer.php deleted file mode 100644 index 8645fe3..0000000 --- a/Transformer/InsertTransformer.php +++ /dev/null @@ -1,156 +0,0 @@ -imgurClientId = $imgurClientId; - } - - /** - * @param FreshRSS_Entry $entry - * @return FreshRSS_Entry - */ - public function transform($entry) { - if (false === $this->isRedditLink($entry)) { - return $entry; - } - - $content = new Content($entry->content()); - if (null === $href = $content->getContentLink()) { - return $entry; - } - - if (preg_match('#(jpg|png|gif|bmp)(\?.*)?$#', $href)) { - $dom = $this->generateDom('Image link', [new Image($href)]); - $entry->_content("{$dom->saveHTML()}{$content->getRaw()}"); - } elseif (preg_match('#(?P.*imgur.com/[^/]*.)gifv$#', $href, $matches)) { - $dom = $this->generateDom('Imgur gifv', [new Video('video/mp4', $matches['gifv']. "mp4")]); - $entry->_content("{$dom->saveHTML()}{$content->getRaw()}"); - } elseif (preg_match('#(?Pimgur.com/[^/]*)$#', $href)) { - $dom = $this->generateDom('Imgur image with URL token', [new Image("$href.png")]); - $entry->_content("{$dom->saveHTML()}{$content->getRaw()}"); - } elseif (preg_match('#(?Pgfycat.com/)(.*/)*(?P[^/\-.]*)#', $href, $matches)) { - try { - $jsonResponse = file_get_contents("https://api.gfycat.com/v1/gfycats/{$matches['token']}"); - $arrayResponse = json_decode($jsonResponse, true); - - if (JSON_ERROR_NONE !== json_last_error()) { - throw new Exception(); - } - - $video = new Video('video/mp4', $arrayResponse['gfyItem']['mp4Url']); - $video->addSource('video/webm', $arrayResponse['gfyItem']['webmUrl']); - $dom = $this->generateDom('Gfycat with token', [$video]); - $entry->_content("{$dom->saveHTML()}{$content->getRaw()}"); - } catch (Exception $e) { - Minz_Log::error("GFYCAT API ERROR - {$href}"); - } - } elseif (preg_match('#(?Predgifs.com/)(.*/)*(?P[^/\-.]*)#', $href, $matches)) { - // API has changed and this code does not work anymore. - // try { - // $jsonResponse = file_get_contents("https://api.redgifs.com/v1/gfycats/{$matches['token']}"); - // $arrayResponse = json_decode($jsonResponse, true); - - // if (JSON_ERROR_NONE !== json_last_error()) { - // throw new Exception(); - // } - - // $dom = $this->generateDom('Redgifs with token', [new Video('video/mp4', $arrayResponse['gfyItem']['mp4Url'])]); - // $entry->_content("{$dom->saveHTML()}{$content->getRaw()}"); - // } catch (Exception $e) { - // Minz_Log::error("REDGIFS API ERROR - {$href}"); - // } - } elseif (preg_match('#v.redd.it#', $href)) { - try { - $jsonResponse = file_get_contents("{$content->getCommentsLink()}.json"); - $arrayResponse = json_decode($jsonResponse, true); - - if (JSON_ERROR_NONE !== json_last_error()) { - throw new Exception(); - } - - $videoUrl = str_replace('?source=fallback', '', $arrayResponse[0]['data']['children'][0]['data']['media']['reddit_video']['fallback_url'] ?? null); - $audioTrack = preg_replace('#DASH_.+\.mp4#', 'DASH_audio.mp4', $videoUrl); - - $dom = $this->generateDom('Reddit video', [new Video('video/mp4', $videoUrl, $audioTrack)]); - $entry->_content("{$dom->saveHTML()}{$content->getRaw()}"); - } catch (Exception $e) { - Minz_Log::error("REDDIT API ERROR - {$href}"); - } - } elseif (preg_match('#reddit.com/gallery#', $href)) { - try { - $jsonResponse = file_get_contents("{$content->getCommentsLink()}.json"); - $arrayResponse = json_decode($jsonResponse, true); - $pictures = $arrayResponse[0]['data']['children'][0]['data']['media_metadata'] ?? null; - if (!empty($pictures)) { - $images = []; - foreach ($pictures as $id => $metadata) { - list(,$extension) = explode('/', $metadata['m']); - $images[] = new Image("https://i.redd.it/{$id}.{$extension}"); - } - $dom = $this->generateDom('Reddit gallery', $images); - $entry->_content("{$dom->saveHTML()}{$content->getRaw()}"); - } - } catch (Exception $e) { - Minz_Log::error("REDDIT API ERROR - {$href}"); - } - } elseif (preg_match('#imgur.com/(a|gallery)/.?#', $href)) { - try { - if (0 < strlen($this->imgurClientId)) { - $token = basename($href); - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, "https://api.imgur.com/3/album/$token/images"); - curl_setopt($ch, CURLOPT_HEADER, 0); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Client-ID {$this->imgurClientId}"]); - curl_setopt($ch, CURLOPT_FAILONERROR, true); - $jsonString = curl_exec($ch); - if (curl_errno($ch)) { - throw new Exception(); - } - curl_close($ch); - - $media = []; - $json = json_decode($jsonString, true); - if (JSON_ERROR_NONE !== json_last_error()) { - throw new Exception(); - } - foreach ($json['data'] as $medium) { - if (false !== strpos($medium['type'], 'video')) { - $media[] = new Video($medium['type'], $medium['link']); - } elseif (false !== strpos($medium['type'], 'image')) { - $media[] = new Image($medium['link']); - } - } - $dom = $this->generateDom('Imgur gallery with API token', $media); - $entry->_content("{$dom->saveHTML()}{$content->getRaw()}"); - } else { - $galleryDom = new \DomDocument('1.0', 'UTF-8'); - $galleryDom->loadHTML(htmlspecialchars_decode(htmlentities(html_entity_decode($href))), LIBXML_NOERROR); - $galleryXpath = new \DomXpath($galleryDom); - $images = $galleryXpath->query("//meta[@name='twitter:image']"); - $media = []; - foreach ($images as $image) { - $media[] = $image->getAttribute('content'); - } - $dom = $this->generateDom('Imgur gallery without API token', $media); - $entry->_content("{$dom->saveHTML()}{$content->getRaw()}"); - } - } catch (Exception $e) { - Minz_Log::error("IMGUR GALLERY ERROR - {$href}"); - } - } - - return $entry; - } -} diff --git a/Transformer/Reddit/GalleryTransformer.php b/Transformer/Reddit/GalleryTransformer.php new file mode 100644 index 0000000..4250dc7 --- /dev/null +++ b/Transformer/Reddit/GalleryTransformer.php @@ -0,0 +1,42 @@ +getContentLink()) === 1; + } + + public function transform(Content $content): string { + $images = []; + $media = $this->getMediaMetadata($content); + + foreach ($media as $id => $metadata) { + list(,$extension) = explode('/', $metadata['m']); + $images[] = new Image("https://i.redd.it/{$id}.{$extension}"); + } + + if ($images !== []) { + $dom = $this->generateDom($images); + + return $dom->saveHTML(); + } + + return ''; + } + + private function getMediaMetadata(Content $content): array { + $arrayResponse = $this->client->jsonGet("{$content->getCommentsLink()}.json"); + + return $arrayResponse[0]['data']['children'][0]['data']['media_metadata'] + ?? $arrayResponse[0]['data']['children'][0]['data']['crosspost_parent_list'][0]['media_metadata'] + ?? []; + } +} diff --git a/Transformer/Reddit/VideoTransformer.php b/Transformer/Reddit/VideoTransformer.php new file mode 100644 index 0000000..a78a1a2 --- /dev/null +++ b/Transformer/Reddit/VideoTransformer.php @@ -0,0 +1,40 @@ +getContentLink()) === 1; + } + + public function transform(Content $content): string { + if ('' === $videoUrl = $this->getVideoUrl($content)) { + return ''; + } + + $dom = $this->generateDom([new Video('video/mp4', $videoUrl, $this->getAudioUrl($videoUrl))]); + + return $dom->saveHTML(); + } + + private function getVideoUrl(Content $content): string { + $arrayResponse = $this->client->jsonGet("{$content->getCommentsLink()}.json"); + + $videoUrl = $arrayResponse[0]['data']['children'][0]['data']['media']['reddit_video']['fallback_url'] + ?? $arrayResponse[0]['data']['children'][0]['data']['crosspost_parent_list'][0]['media']['reddit_video']['fallback_url'] + ?? ''; + + return str_replace('?source=fallback', '', $videoUrl); + } + + private function getAudioUrl(string $videoUrl): string { + return preg_replace('#DASH_.+\.mp4#', 'DASH_audio.mp4', $videoUrl); + } +} diff --git a/Transformer/TransformerInterface.php b/Transformer/TransformerInterface.php new file mode 100644 index 0000000..5f5a3d4 --- /dev/null +++ b/Transformer/TransformerInterface.php @@ -0,0 +1,13 @@ +
- +
@@ -12,7 +12,7 @@
@@ -21,7 +21,7 @@
@@ -30,7 +30,7 @@
@@ -39,7 +39,7 @@
@@ -48,7 +48,7 @@
@@ -57,7 +57,7 @@
@@ -74,7 +74,7 @@
- +

diff --git a/extension.php b/extension.php index fe9a8b0..80d7d97 100644 --- a/extension.php +++ b/extension.php @@ -2,20 +2,15 @@ declare(strict_types=1); -use RedditImage\Transformer\DisplayTransformer; -use RedditImage\Transformer\InsertTransformer; +use RedditImage\Client\Client; +use RedditImage\Processor\BeforeInsertProcessor; +use RedditImage\Processor\BeforeDisplayProcessor; +use RedditImage\Settings; class RedditImageExtension extends Minz_Extension { - private const DEFAULT_IMAGEHEIGHT = 70; - private const DEFAULT_MUTEDVIDEO = true; - private const DEFAULT_DISPLAYIMAGE = true; - private const DEFAULT_DISPLAYVIDEO = true; - private const DEFAULT_DISPLAYORIGINAL = true; - private const DEFAULT_DISPLAYMETADATA = false; - private const DEFAULT_DISPLAYTHUMBNAILS = false; - - private DisplayTransformer $displayTransformer; - private InsertTransformer $insertTransformer; + private BeforeDisplayProcessor $beforeDisplayProcessor; + private BeforeInsertProcessor $beforeInsertProcessor; + public Settings $settings; public function autoload($class_name): void { if (0 === strpos($class_name, 'RedditImage')) { @@ -24,9 +19,14 @@ public function autoload($class_name): void { } } + private function getUserAgent(): string { + return "{$this->getName()}/{$this->getVersion()} by {$this->getAuthor()}"; + } + public function init(): void { spl_autoload_register([$this, 'autoload']); + define('REDDITIMAGE_VERSION', $this->getVersion()); $this->registerTranslates(); $current_user = Minz_Session::param('currentUser'); @@ -37,11 +37,13 @@ public function init(): void { Minz_View::appendStyle($this->getFileUrl($filename, 'css')); } - $this->displayTransformer = new DisplayTransformer($this->getDisplayImage(), $this->getDisplayVideo(), $this->getMutedVideo(), $this->getDisplayOriginal(), $this->getDisplayMetadata(), $this->getDisplayThumbnails()); - $this->insertTransformer = new InsertTransformer($this->getImgurClientId()); + $this->settings = new Settings($this->getUserConfiguration()); - $this->registerHook('entry_before_display', [$this->displayTransformer, 'transform']); - $this->registerHook('entry_before_insert', [$this->insertTransformer, 'transform']); + $this->beforeDisplayProcessor = new BeforeDisplayProcessor($this->settings); + $this->beforeInsertProcessor = new BeforeInsertProcessor($this->settings, new Client($this->getUserAgent())); + + $this->registerHook('entry_before_display', [$this->beforeDisplayProcessor, 'process']); + $this->registerHook('entry_before_insert', [$this->beforeInsertProcessor, 'process']); } public function handleConfigureAction(): void { @@ -51,7 +53,7 @@ public function handleConfigureAction(): void { if (Minz_Request::isPost()) { $configuration = [ - 'imageHeight' => (int) Minz_Request::param('image-height', static::DEFAULT_IMAGEHEIGHT), + 'imageHeight' => (int) Minz_Request::param('image-height', $this->settings->getDefaultImageHeight()), 'mutedVideo' => (bool) Minz_Request::param('muted-video'), 'displayImage' => (bool) Minz_Request::param('display-image'), 'displayVideo' => (bool) Minz_Request::param('display-video'), @@ -67,36 +69,4 @@ public function handleConfigureAction(): void { ); } } - - public function getImageHeight(): int { - return $this->getUserConfigurationValue('imageHeight', static::DEFAULT_IMAGEHEIGHT); - } - - public function getMutedVideo(): bool { - return $this->getUserConfigurationValue('mutedVideo', static::DEFAULT_MUTEDVIDEO); - } - - public function getDisplayImage(): bool { - return $this->getUserConfigurationValue('displayImage', static::DEFAULT_DISPLAYIMAGE); - } - - public function getDisplayVideo(): bool { - return $this->getUserConfigurationValue('displayVideo', static::DEFAULT_DISPLAYVIDEO); - } - - public function getDisplayOriginal(): bool { - return $this->getUserConfigurationValue('displayOriginal', static::DEFAULT_DISPLAYORIGINAL); - } - - public function getDisplayMetadata(): bool { - return $this->getUserConfigurationValue('displayMetadata', static::DEFAULT_DISPLAYMETADATA); - } - - public function getDisplayThumbnails(): bool { - return $this->getUserConfigurationValue('displayThumbnails', static::DEFAULT_DISPLAYTHUMBNAILS); - } - - public function getImgurClientId(): string { - return $this->getUserConfigurationValue('imgurClientId', ''); - } } diff --git a/metadata.json b/metadata.json index cf3cc3c..5e0d939 100644 --- a/metadata.json +++ b/metadata.json @@ -2,7 +2,7 @@ "name": "Reddit Image", "author": "Alexis Degrugillier", "description": "Modify reddit entries by displaying known medias and easing access to the linked resource", - "version": "0.12.5", + "version": "1.0.0", "entrypoint": "RedditImage", "type": "user" } diff --git a/phpunit.xml b/phpunit.xml.dist similarity index 57% rename from phpunit.xml rename to phpunit.xml.dist index 96240dd..0e9880f 100644 --- a/phpunit.xml +++ b/phpunit.xml.dist @@ -8,10 +8,19 @@ beStrictAboutCoverageMetadata="true" beStrictAboutOutputDuringTests="true" failOnRisky="true" - failOnWarning="true"> + failOnWarning="true" + displayDetailsOnTestsThatTriggerErrors="true" + displayDetailsOnTestsThatTriggerNotices="true" + displayDetailsOnTestsThatTriggerDeprecations="true" + displayDetailsOnTestsThatTriggerWarnings="true" + displayDetailsOnSkippedTests="true"> tests + tests/Client/ClientTest.php + + + tests/Client/ClientTest.php @@ -19,4 +28,7 @@ . + + + diff --git a/tests/Client/ClientTest.php b/tests/Client/ClientTest.php new file mode 100644 index 0000000..75e3b2d --- /dev/null +++ b/tests/Client/ClientTest.php @@ -0,0 +1,135 @@ +client = new Client('Reddit Image/test by Alexis Degrugillier'); + } + + /** + * @testWith ["https://example.org", true] + * ["https://github.com/aledeg/xExtension-RedditImage/404", false] + */ + public function testIsAccessible(string $url, bool $isAccessible): void { + $this->assertEquals($isAccessible, $this->client->isAccessible($url)); + } + + public function testJsonGetGfycatVideo(): void { + $json = $this->client->jsonGet('https://api.gfycat.com/v1/gfycats/sandytatteredakitainu'); + $this->assertIsArray($json); + + $this->assertEquals('https://giant.gfycat.com/SandyTatteredAkitainu.mp4', $json['gfyItem']['mp4Url'] ?? null); + $this->assertEquals('https://giant.gfycat.com/SandyTatteredAkitainu.webm', $json['gfyItem']['webmUrl'] ?? null); + } + + public function testJsonGetImgurGalleryWithClientId(): void { + if (!defined('IMGUR_CLIENT_ID')) { + $this->markTestSkipped('Imgur client ID is not defined in the configuration file'); + } + + $clientId = IMGUR_CLIENT_ID; + $json = $this->client->jsonGet('https://api.imgur.com/3/album/JOh3bhn/images', [ + "Authorization: Client-ID {$clientId}", + ]); + $this->assertIsArray($json); + + $metadata = $json['data']; + $this->assertIsArray($metadata); + $this->assertCount(14, $metadata); + + $links = array_column($metadata, 'link'); + $this->assertIsArray($links); + $this->assertCount(14, $links); + $this->assertEquals([ + 'https://i.imgur.com/kHJbvmh.png', + 'https://i.imgur.com/M5XwH7K.png', + 'https://i.imgur.com/nIAIjJO.png', + 'https://i.imgur.com/ASyDzpc.png', + 'https://i.imgur.com/aMQ00r6.png', + 'https://i.imgur.com/F6mnogZ.png', + 'https://i.imgur.com/hGgJ4vX.png', + 'https://i.imgur.com/tnd6dKW.png', + 'https://i.imgur.com/Bi1mSnq.png', + 'https://i.imgur.com/4mIdYn5.png', + 'https://i.imgur.com/HcburKp.png', + 'https://i.imgur.com/dtiq3CN.png', + 'https://i.imgur.com/07byVN9.png', + 'https://i.imgur.com/U07O5Co.png', + ], $links); + } + + public function testJsonGetRedditGallery(): void { + $json = $this->client->jsonGet('https://www.reddit.com/r/quilting/comments/13ajhjf/lochness_monster_fpp_block/.json'); + $this->assertIsArray($json); + + $metadata = $json[0]['data']['children'][0]['data']['media_metadata']; + $this->assertIsArray($metadata); + + $this->assertCount(7, $metadata); + $this->assertEquals([ + 'm8do0y12wdya1', + 'fvbthx12wdya1', + 'nb0y7y12wdya1', + '2bel5y12wdya1', + 'chnqmy12wdya1', + '9w7sby12wdya1', + 'rgww3y12wdya1', + ], array_keys($metadata)); + foreach ($metadata as $values) { + $this->assertArrayHasKey('m', $values); + $this->assertEquals('image/jpg', $values['m']); + } + } + + public function testJsonGetRedditGalleryWithCrosspost(): void { + $json = $this->client->jsonGet('https://www.reddit.com/r/LegendsOfRuneterra/comments/l2k33y/might_be_an_obvious_connection_but_i_just_noticed/.json'); + $this->assertIsArray($json); + + $metadata = $json[0]['data']['children'][0]['data']['crosspost_parent_list'][0]['media_metadata']; + $this->assertCount(4, $metadata); + $this->assertEquals([ + 'xpt2x73houc61', + '5lztrcniouc61', + 'fnwszdajouc61', + '9wq3m82iouc61', + ], array_keys($metadata)); + foreach ($metadata as $values) { + $this->assertArrayHasKey('m', $values); + $this->assertEquals('image/jpg', $values['m']); + } + } + + public function testJsonGetRedditVideo(): void { + $json = $this->client->jsonGet('https://www.reddit.com/r/oddlyterrifying/comments/13ommr2/while_some_find_a_baby_stingray_video_shoot/.json'); + $this->assertIsArray($json); + + $metadata = $json[0]['data']['children'][0]['data']['media']['reddit_video']['fallback_url'] ?? null; + $this->assertEquals('https://v.redd.it/nvcnk4qsee1b1/DASH_1080.mp4?source=fallback', $metadata); + } + + public function testJsonGetRedditVideoWithCrosspost(): void { + $json = $this->client->jsonGet('https://www.reddit.com/r/hiddenrooms/comments/11vohr2/crosspost_from_rcarpentry_throwback_to_a_really/.json'); + $this->assertIsArray($json); + + $metadata = $json[0]['data']['children'][0]['data']['crosspost_parent_list'][0]['media']['reddit_video']['fallback_url'] ?? null; + $this->assertEquals('https://v.redd.it/a9ticvrffpoa1/DASH_1080.mp4?source=fallback', $metadata); + } +} diff --git a/tests/Media/ImageTest.php b/tests/Media/ImageTest.php index a9a3618..9c594fd 100644 --- a/tests/Media/ImageTest.php +++ b/tests/Media/ImageTest.php @@ -17,7 +17,6 @@ public function test(): void { $media = new Image('https://example.org'); $this->assertInstanceOf(DomElementInterface::class, $media); - $this->assertEquals('https://example.org', $media->getUrl()); $domElement = m::mock(\DomElement::class); $domElement->expects('setAttribute') diff --git a/tests/Media/LinkTest.php b/tests/Media/LinkTest.php new file mode 100644 index 0000000..05e2727 --- /dev/null +++ b/tests/Media/LinkTest.php @@ -0,0 +1,53 @@ +assertInstanceOf(DomElementInterface::class, $media); + + $textNode = m::mock(\DomText::class); + + $aElement = m::mock(\DomElement::class); + $aElement->expects('setAttribute') + ->once() + ->with('href', 'https://example.org'); + $aElement->expects('appendChild') + ->once() + ->with($textNode); + + $pElement = m::mock(\DomElement::class); + $pElement->expects('appendChild') + ->once() + ->with($aElement) + ->andReturns($aElement); + + $domDocument = m::mock(\DomDocument::class); + $domDocument->expects('createElement') + ->once() + ->with('p') + ->andReturns($pElement); + $domDocument->expects('createElement') + ->once() + ->with('a') + ->andReturns($aElement); + $domDocument->expects('createTextNode') + ->once() + ->with('https://example.org') + ->andReturns($textNode); + + $this->assertEquals($pElement, $media->toDomElement($domDocument)); + } +} diff --git a/tests/PHPUnit/Constraint/htmlHasGeneratedContentContainer.php b/tests/PHPUnit/Constraint/htmlHasGeneratedContentContainer.php new file mode 100644 index 0000000..7365143 --- /dev/null +++ b/tests/PHPUnit/Constraint/htmlHasGeneratedContentContainer.php @@ -0,0 +1,37 @@ +loadHTML($other, LIBXML_NOERROR) === false) { + return false; + } + + $xpath = new \DOMXpath($dom); + $container = $xpath->query("body/div[@class='reddit-image figure']"); + if ($container->length !== 1) { + return false; + } + + $comment = $container->item(0)->firstChild; + if ($comment->nodeType !== XML_COMMENT_NODE) { + return false; + } + + return preg_match('#xExtension-RedditImage/\w+ \| \w+ \| RedditImage\\\\Transformer\\\\.+Transformer#', $comment->textContent) === 1; + } + + public function toString(): string { + return 'has a content container'; + } +} diff --git a/tests/PHPUnit/Constraint/htmlHasImage.php b/tests/PHPUnit/Constraint/htmlHasImage.php new file mode 100644 index 0000000..f6f0cf1 --- /dev/null +++ b/tests/PHPUnit/Constraint/htmlHasImage.php @@ -0,0 +1,39 @@ +imageUrl = $imageUrl; + } + + public function matches($other): bool { + if (!is_string($other)) { + return false; + } + + $dom = new \DomDocument('1.0', 'UTF-8'); + if ($dom->loadHTML($other, LIBXML_NOERROR) === false) { + return false; + } + + $xpath = new \DOMXpath($dom); + $images = $xpath->query("body/div/img[@class='reddit-image'][@src='{$this->imageUrl}']"); + + if ($images->length !== 1) { + return false; + } + + return true; + } + + public function toString(): string { + return "has the image with {$this->imageUrl} source"; + } +} diff --git a/tests/PHPUnit/Constraint/htmlHasLink.php b/tests/PHPUnit/Constraint/htmlHasLink.php new file mode 100644 index 0000000..efc59a0 --- /dev/null +++ b/tests/PHPUnit/Constraint/htmlHasLink.php @@ -0,0 +1,39 @@ +link = $link; + } + + public function matches($other): bool { + if (!is_string($other)) { + return false; + } + + $dom = new \DomDocument('1.0', 'UTF-8'); + if ($dom->loadHTML($other, LIBXML_NOERROR) === false) { + return false; + } + + $xpath = new \DOMXpath($dom); + $links = $xpath->query("body/div/p/a[@href='{$this->link}']"); + + if ($links->length !== 1) { + return false; + } + + return true; + } + + public function toString(): string { + return "has the link {$this->imageUrl}"; + } +} diff --git a/tests/PHPUnit/Constraint/htmlHasVideo.php b/tests/PHPUnit/Constraint/htmlHasVideo.php new file mode 100644 index 0000000..14d8b9d --- /dev/null +++ b/tests/PHPUnit/Constraint/htmlHasVideo.php @@ -0,0 +1,41 @@ +format = $format; + $this->videoUrl = $videoUrl; + } + + public function matches($other): bool { + if (!is_string($other)) { + return false; + } + + $dom = new \DomDocument('1.0', 'UTF-8'); + if ($dom->loadHTML($other, LIBXML_NOERROR) === false) { + return false; + } + + $xpath = new \DOMXpath($dom); + $videos = $xpath->query("body/div/video[@class='reddit-image'][@controls='true'][@preload='metadata']/source[@src='{$this->videoUrl}'][@type='video/{$this->format}']"); + + if ($videos->length !== 1) { + return false; + } + + return true; + } + + public function toString(): string { + return "has the {$this->format} video with {$this->videoUrl} source"; + } +} diff --git a/tests/PHPUnit/Constraint/htmlHasVideoWithAudio.php b/tests/PHPUnit/Constraint/htmlHasVideoWithAudio.php new file mode 100644 index 0000000..a6c149f --- /dev/null +++ b/tests/PHPUnit/Constraint/htmlHasVideoWithAudio.php @@ -0,0 +1,49 @@ +format = $format; + $this->videoUrl = $videoUrl; + $this->audioUrl = $audioUrl; + } + + public function matches($other): bool { + if (!is_string($other)) { + return false; + } + + $dom = new \DomDocument('1.0', 'UTF-8'); + if ($dom->loadHTML($other, LIBXML_NOERROR) === false) { + return false; + } + + $xpath = new \DOMXpath($dom); + $videos = $xpath->query("body/div/video[@class='reddit-image'][@controls='true'][@preload='metadata']/source[@src='{$this->videoUrl}'][@type='video/{$this->format}']"); + + if ($videos->length !== 1) { + return false; + } + + $audios = $xpath->query("body/div/video/audio[@controls='true']/source[@src='{$this->audioUrl}']"); + + if ($audios->length !== 1) { + return false; + } + + return true; + } + + public function toString(): string { + return "has the {$this->format} video with {$this->videoUrl} source and {$this->audioUrl} audio"; + } +} diff --git a/tests/PHPUnit/TestCase.php b/tests/PHPUnit/TestCase.php new file mode 100644 index 0000000..36c4da4 --- /dev/null +++ b/tests/PHPUnit/TestCase.php @@ -0,0 +1,38 @@ +assertFalse($settings->hasImgurClientId()); + $this->assertEquals('', $settings->getImgurClientId()); + $this->assertEquals(70, $settings->getDefaultImageHeight()); + $this->assertEquals(70, $settings->getImageHeight()); + $this->assertTrue($settings->getMutedVideo()); + $this->assertTrue($settings->getDisplayImage()); + $this->assertTrue($settings->getDisplayVideo()); + $this->assertTrue($settings->getDisplayOriginal()); + $this->assertFalse($settings->getDisplayMetadata()); + $this->assertFalse($settings->getDisplayThumbnails()); + } + + public function testWhenSettings(): void + { + $settings = new Settings([ + 'imgurClientId' => 'abc123def', + 'imageHeight' => 90, + 'mutedVideo' => false, + 'displayImage' => false, + 'displayVideo' => false, + 'displayOriginal' => false, + 'displayMetadata' => true, + 'displayThumbnails' => true, + ]); + + $this->assertTrue($settings->hasImgurClientId()); + $this->assertEquals('abc123def', $settings->getImgurClientId()); + $this->assertEquals(70, $settings->getDefaultImageHeight()); + $this->assertEquals(90, $settings->getImageHeight()); + $this->assertFalse($settings->getMutedVideo()); + $this->assertFalse($settings->getDisplayImage()); + $this->assertFalse($settings->getDisplayVideo()); + $this->assertFalse($settings->getDisplayOriginal()); + $this->assertTrue($settings->getDisplayMetadata()); + $this->assertTrue($settings->getDisplayThumbnails()); + } + + public function testProcessor(): void { + $settings = new Settings([]); + + $this->assertEquals('no', $settings->getProcessor()); + + $settings->setProcessor('test'); + + $this->assertEquals('test', $settings->getProcessor()); + } +} diff --git a/tests/Transformer/Agnostic/ImageTransformerTest.php b/tests/Transformer/Agnostic/ImageTransformerTest.php new file mode 100644 index 0000000..e717411 --- /dev/null +++ b/tests/Transformer/Agnostic/ImageTransformerTest.php @@ -0,0 +1,92 @@ +content = m::mock(Content::class); + $this->settings = m::mock(Settings::class); + $this->transformer = new ImageTransformer($this->settings); + } + + /** + * @dataProvider provideDataForCanTransform + */ + public function testCanTransform(string $link, bool $expected): void { + $this->content->expects('getContentLink') + ->once() + ->andReturns($link); + + $this->assertEquals($expected, $this->transformer->canTransform($this->content)); + } + + public static function provideDataForCanTransform(): \Generator { + yield 'JPG image without query string' => ['https://example.org/image.jpg', true]; + yield 'JPEG image without query string' => ['https://example.org/image.jpeg', true]; + yield 'PNG image without query string' => ['https://example.org/image.png', true]; + yield 'BMP image without query string' => ['https://example.org/image.bmp', true]; + yield 'GIF image without query string' => ['https://example.org/image.gif', true]; + yield 'JPG image with query string' => ['https://example.org/image.jpg?key=value', true]; + yield 'JPEG image with query string' => ['https://example.org/image.jpeg?key=value', true]; + yield 'PNG image with query string' => ['https://example.org/image.png?key=value', true]; + yield 'BMP image with query string' => ['https://example.org/image.bmp?key=value', true]; + yield 'GIF image with query string' => ['https://example.org/image.gif?key=value', true]; + yield 'JPG format in query string' => ['https://example.org/image?format=jpg', false]; + yield 'JPEG format in query string' => ['https://example.org/image?format=jpeg', false]; + yield 'PNG format in query string' => ['https://example.org/image?format=png', false]; + yield 'BMP format in query string' => ['https://example.org/image?format=bmp', false]; + yield 'GIF format in query string' => ['https://example.org/image?format=gif', false]; + yield 'JPG as route section' => ['https://example.org/jpg', false]; + yield 'JPEG as route section' => ['https://example.org/jpeg', false]; + yield 'PNG as route section' => ['https://example.org/png', false]; + yield 'BMP as route section' => ['https://example.org/bmp', false]; + yield 'GIF as route section' => ['https://example.org/gif', false]; + } + + /** + * @dataProvider provideDataForTransform + */ + public function testTransform(string $input, string $expected): void { + $this->content->expects('getContentLink') + ->once() + ->andReturns($input); + $this->settings->expects('getProcessor') + ->once() + ->andReturns('test'); + + $html = $this->transformer->transform($this->content); + + $this->assertHtmlHasGeneratedContentContainer($html); + $this->assertHtmlHasImage($html, $expected); + } + + public static function provideDataForTransform(): \Generator { + yield 'JPG image without query string' => ['https://example.org/image.jpg', 'https://example.org/image.jpg']; + yield 'JPEG image without query string' => ['https://example.org/image.jpeg', 'https://example.org/image.jpeg']; + yield 'PNG image without query string' => ['https://example.org/image.png', 'https://example.org/image.png']; + yield 'BMP image without query string' => ['https://example.org/image.bmp', 'https://example.org/image.bmp']; + yield 'GIF image without query string' => ['https://example.org/image.gif', 'https://example.org/image.gif']; + yield 'JPG image with query string' => ['https://example.org/image.jpg?key=value', 'https://example.org/image.jpg']; + yield 'JPEG image with query string' => ['https://example.org/image.jpeg?key=value', 'https://example.org/image.jpeg']; + yield 'PNG image with query string' => ['https://example.org/image.png?key=value', 'https://example.org/image.png']; + yield 'BMP image with query string' => ['https://example.org/image.bmp?key=value', 'https://example.org/image.bmp']; + yield 'GIF image with query string' => ['https://example.org/image.gif?key=value', 'https://example.org/image.gif']; + } +} diff --git a/tests/Transformer/Agnostic/LinkTransformerTest.php b/tests/Transformer/Agnostic/LinkTransformerTest.php new file mode 100644 index 0000000..2bcfdd2 --- /dev/null +++ b/tests/Transformer/Agnostic/LinkTransformerTest.php @@ -0,0 +1,54 @@ +content = m::mock(Content::class); + $this->settings = m::mock(Settings::class); + $this->transformer = new LinkTransformer($this->settings); + } + + /** + * @testWith [true] + * [false] + */ + public function testCanTransform(bool $hasRealContent): void { + $this->content->expects('hasReal') + ->once() + ->andReturns($hasRealContent); + + $this->assertEquals(!$hasRealContent, $this->transformer->canTransform($this->content)); + } + + public function testTransform(): void { + $this->content->expects('getContentLink') + ->once() + ->andReturns('https://example.org'); + $this->settings->expects('getProcessor') + ->once() + ->andReturns('test'); + + $html = $this->transformer->transform($this->content); + + $this->assertHtmlHasGeneratedContentContainer($html); + $this->assertHtmlHasLink($html, 'https://example.org'); + } +} diff --git a/tests/Transformer/Agnostic/VideoTransformerTest.php b/tests/Transformer/Agnostic/VideoTransformerTest.php new file mode 100644 index 0000000..3857906 --- /dev/null +++ b/tests/Transformer/Agnostic/VideoTransformerTest.php @@ -0,0 +1,116 @@ +content = m::mock(Content::class); + $this->settings = m::mock(Settings::class); + $this->transformer = new VideoTransformer($this->settings); + } + + /** + * @dataProvider provideDataForCanTransform + */ + public function testCanTransform(string $url, bool $expected): void { + $this->content->expects('getContentLink') + ->once() + ->andReturns($url); + + $this->assertEquals($expected, $this->transformer->canTransform($this->content)); + } + + public static function provideDataForCanTransform(): \Generator { + yield 'MP4 video without query string' => ['https://example.org/video.mp4', true]; + yield 'WEBM video without query string' => ['https://example.org/video.webm', true]; + yield 'MP4 video with query string' => ['https://example.org/video.mp4?key=value', false]; + yield 'WEBM video with query string' => ['https://example.org/video.webm?key=value', false]; + yield 'MP4 format in query string' => ['https://example.org/video?format=mp4', false]; + yield 'WEBM format in query string' => ['https://example.org/video?format=webm', false]; + yield 'MP4 as route section' => ['https://example.org/mp4', false]; + yield 'WEBM as route section' => ['https://example.org/webm', false]; + } + + /** + * @testWith ["https://example.org/video.mp4"] + * ["https://example.org/video.webm"] + */ + public function testTransformWithInaccessibleResource(string $url): void { + $client = m::mock(Client::class); + $client->expects('isAccessible') + ->once() + ->with($url) + ->andReturnFalse(); + + $this->content->expects('getContentLink') + ->once() + ->andReturns($url); + + $this->transformer->setClient($client); + $this->assertEquals('', $this->transformer->transform($this->content)); + } + + public function testTransformWithAccessibleMp4Resource(): void { + $url = 'https://example.org/video.mp4'; + + $client = m::mock(Client::class); + $client->expects('isAccessible') + ->once() + ->with($url) + ->andReturnTrue(); + + $this->content->expects('getContentLink') + ->once() + ->andReturns($url); + $this->settings->expects('getProcessor') + ->once() + ->andReturns('test'); + + $this->transformer->setClient($client); + $html = $this->transformer->transform($this->content); + + $this->assertHtmlHasGeneratedContentContainer($html); + $this->assertHtmlHasMp4Video($html, $url); + } + + public function testTransformWithAccessibleWebmResource(): void { + $url = 'https://example.org/video.webm'; + + $client = m::mock(Client::class); + $client->expects('isAccessible') + ->once() + ->with($url) + ->andReturnTrue(); + + $this->content->expects('getContentLink') + ->once() + ->andReturns($url); + $this->settings->expects('getProcessor') + ->once() + ->andReturns('test'); + + $this->transformer->setClient($client); + $html = $this->transformer->transform($this->content); + + $this->assertHtmlHasGeneratedContentContainer($html); + $this->assertHtmlHasWebmVideo($html, $url); + } +} diff --git a/tests/Transformer/DisplayTransformerTest.php b/tests/Transformer/DisplayTransformerTest.php deleted file mode 100644 index 8561a0a..0000000 --- a/tests/Transformer/DisplayTransformerTest.php +++ /dev/null @@ -1,12 +0,0 @@ -content = m::mock(Content::class); + $this->settings = m::mock(Settings::class); + $this->transformer = new VideoTransformer($this->settings); + } + + /** + * @dataProvider provideDataForCanTransform + */ + public function testCanTransform(string $url, bool $expected): void { + $this->content->expects('getContentLink') + ->once() + ->andReturns($url); + + $this->assertEquals($expected, $this->transformer->canTransform($this->content)); + } + + public static function provideDataForCanTransform(): \Generator { + yield 'not Gfycat URL' => ['https://example.org', false]; + yield 'Gfycat without token' => ['https://gfycat.com/', false]; + yield 'Gfycat with token' => ['https://gfycat.com/abc123', true]; + yield 'Gfycat with "token" containing -' => ['https://gfycat.com/abc-123', false]; + yield 'Gfycat with "token" containing .' => ['https://gfycat.com/abc.123', false]; + yield 'Gfycat with token and route parameters' => ['https://gfycat.com/abc/def/ghi123', true]; + } + + public function testTransformWhenNoContent(): void { + $client = m::mock(Client::class); + $client->expects('jsonGet') + ->once() + ->with('https://api.gfycat.com/v1/gfycats/hello') + ->andReturns([]); + + $this->content->expects('getContentLink') + ->once() + ->andReturns('https://gfycat.com/hello'); + + $this->transformer->setClient($client); + $this->assertEquals('', $this->transformer->transform($this->content)); + } + + public function testTransformWhenContent(): void { + $client = m::mock(Client::class); + $client->expects('jsonGet') + ->once() + ->with('https://api.gfycat.com/v1/gfycats/hello') + ->andReturns([ + 'gfyItem' => [ + 'mp4Url' => 'https://gfycat.com/hello.mp4', + 'webmUrl' => 'https://gfycat.com/hello.webm', + ], + ]); + + $this->content->expects('getContentLink') + ->once() + ->andReturns('https://gfycat.com/hello'); + $this->settings->expects('getProcessor') + ->once() + ->andReturns('test'); + + $this->transformer->setClient($client); + $html = $this->transformer->transform($this->content); + + $this->assertHtmlHasGeneratedContentContainer($html); + $this->assertHtmlHasMp4Video($html, 'https://gfycat.com/hello.mp4'); + $this->assertHtmlHasWebmVideo($html, 'https://gfycat.com/hello.webm'); + } +} diff --git a/tests/Transformer/Imgur/GalleryWithClientIdTransformerTest.php b/tests/Transformer/Imgur/GalleryWithClientIdTransformerTest.php new file mode 100644 index 0000000..361f303 --- /dev/null +++ b/tests/Transformer/Imgur/GalleryWithClientIdTransformerTest.php @@ -0,0 +1,122 @@ +content = m::mock(Content::class); + $this->settings = m::mock(Settings::class); + $this->transformer = new GalleryWithClientIdTransformer($this->settings); + } + + /** + * @dataProvider provideDataForCanTransform + */ + public function testCanTransform(string $link, bool $hasClientId, bool $expected): void { + $this->content->expects('getContentLink') + ->once() + ->andReturns($link); + $this->settings->expects('hasImgurClientId') + ->once() + ->andReturns($hasClientId); + + $this->assertEquals($expected, $this->transformer->canTransform($this->content)); + } + + public static function provideDataForCanTransform(): \Generator { + yield 'client id, not an Imgur URL' => ['https://example.org', true, false]; + yield 'client id, not an Imgur album URL' => ['https://imgur.com/image.png', false, false]; + yield 'client id, Imgur album URL' => ['https://imgur.com/a/abc123', true, true]; + yield 'client id, Imgur album URL with query string' => ['https://imgur.com/a/abc123?key=value', true, true]; + yield 'no client id, not an Imgur URL' => ['https://example.org', false, false]; + yield 'no client id, not an Imgur album URL' => ['https://imgur.com/image.png', false, false]; + yield 'no client id, Imgur album URL' => ['https://imgur.com/a/abc123', false, false]; + yield 'no client id, Imgur album URL with query string' => ['https://imgur.com/a/abc123?key=value', false, false]; + } + + public function testTransformWhenNoContent(): void { + $client = m::mock(Client::class); + $client->expects('jsonGet') + ->once() + ->with('https://api.imgur.com/3/album/hello/images', [ + 'Authorization: Client-ID abc123', + ]) + ->andReturns([]); + + $this->content->expects('getContentLink') + ->once() + ->andReturns('https://imgur.com/a/hello'); + $this->settings->expects('getImgurClientId') + ->once() + ->andReturns('abc123'); + + $this->transformer->setClient($client); + $this->assertEquals('', $this->transformer->transform($this->content)); + } + + public function testTransformWhenContent(): void { + $client = m::mock(Client::class); + $client->expects('jsonGet') + ->once() + ->with('https://api.imgur.com/3/album/hello/images', [ + 'Authorization: Client-ID abc123', + ]) + ->andReturns([ + 'data' => [ + [ + 'type' => 'video/mp4', + 'link' => 'https://example.org/hello.mp4', + ], + [ + 'type' => 'image/jpg', + 'link' => 'https://example.org/hello.jpg', + ], + [ + 'type' => 'video/webm', + 'link' => 'https://example.org/hello.webm', + ], + [ + 'type' => 'image/png', + 'link' => 'https://example.org/hello.png', + ], + ], + ]); + + $this->content->expects('getContentLink') + ->once() + ->andReturns('https://imgur.com/a/hello'); + $this->settings->expects('getImgurClientId') + ->once() + ->andReturns('abc123'); + $this->settings->expects('getProcessor') + ->once() + ->andReturns('test'); + + $this->transformer->setClient($client); + $html = $this->transformer->transform($this->content); + + $this->assertHtmlHasGeneratedContentContainer($html); + $this->assertHtmlHasImage($html, 'https://example.org/hello.png'); + $this->assertHtmlHasImage($html, 'https://example.org/hello.jpg'); + $this->assertHtmlHasMp4Video($html, 'https://example.org/hello.mp4'); + $this->assertHtmlHasWebmVideo($html, 'https://example.org/hello.webm'); + } +} diff --git a/tests/Transformer/Imgur/ImageTransformerTest.php b/tests/Transformer/Imgur/ImageTransformerTest.php new file mode 100644 index 0000000..cc62779 --- /dev/null +++ b/tests/Transformer/Imgur/ImageTransformerTest.php @@ -0,0 +1,63 @@ +content = m::mock(Content::class); + $this->settings = m::mock(Settings::class); + $this->transformer = new ImageTransformer($this->settings); + } + + /** + * @dataProvider provideDataForCanTransform + */ + public function testCanTransform(string $link, bool $expected): void { + $this->content->expects('getContentLink') + ->once() + ->andReturns($link); + + $this->assertEquals($expected, $this->transformer->canTransform($this->content)); + } + + public static function provideDataForCanTransform(): \Generator { + yield 'not an Imgur URL' => ['https://example.org/abc123', false]; + yield 'Imgur URL' => ['https://imgur.com/', false]; + yield 'Imgur URL with album token' => ['https://imgur.com/a/abc123', false]; + yield 'Imgur URL with token' => ['https://imgur.com/abc123', true]; + yield 'Imgur URL with query string' => ['https://imgur.com/abc123?key=value', false]; + yield 'Imgur URL with terminal slash' => ['https://imgur.com/abc123/', false]; + yield 'Imgur URL with extension' => ['https://imgur.com/abc123.png', false]; + } + + public function testTransform(): void { + $this->content->expects('getContentLink') + ->once() + ->andReturns('https://example.org/abc123'); + $this->settings->expects('getProcessor') + ->once() + ->andReturns('test'); + + $html = $this->transformer->transform($this->content); + + $this->assertHtmlHasGeneratedContentContainer($html); + $this->assertHtmlHasImage($html, 'https://example.org/abc123.png'); + } +} diff --git a/tests/Transformer/Imgur/VideoTransformerTest.php b/tests/Transformer/Imgur/VideoTransformerTest.php new file mode 100644 index 0000000..817c835 --- /dev/null +++ b/tests/Transformer/Imgur/VideoTransformerTest.php @@ -0,0 +1,61 @@ +content = m::mock(Content::class); + $this->settings = m::mock(Settings::class); + $this->transformer = new VideoTransformer($this->settings); + } + + /** + * @dataProvider provideDataForCanTransform + */ + public function testCanTransform(string $url, bool $expected): void { + $this->content->expects('getContentLink') + ->once() + ->andReturns($url); + + $this->assertEquals($expected, $this->transformer->canTransform($this->content)); + } + + public static function provideDataForCanTransform(): \Generator { + yield 'not Imgur URL' => ['https://example.org', false]; + yield 'Imgur URL without video' => ['https://imgur.com/image.jpg', false]; + yield 'Imgur URL with video' => ['https://imgur.com/video.gifv', true]; + yield 'Imgur URL with video and query string' => ['https://imgur.com/video.gifv?key=value', false]; + yield 'Imgur URL with video and route parameter' => ['https://imgur.com/videos/video.gifv', false]; + } + + public function testTransform(): void { + $this->content->expects('getContentLink') + ->once() + ->andReturns('https://imgur.com/video.gifv'); + $this->settings->expects('getProcessor') + ->once() + ->andReturns('test'); + + $html = $this->transformer->transform($this->content); + + $this->assertHtmlHasGeneratedContentContainer($html); + $this->assertHtmlHasMp4Video($html, 'https://imgur.com/video.mp4'); + } +} diff --git a/tests/Transformer/InsertTransformerTest.php b/tests/Transformer/InsertTransformerTest.php deleted file mode 100644 index 7b9ac3a..0000000 --- a/tests/Transformer/InsertTransformerTest.php +++ /dev/null @@ -1,12 +0,0 @@ -content = m::mock(Content::class); + $this->settings = m::mock(Settings::class); + $this->transformer = new GalleryTransformer($this->settings); + } + + /** + * @dataProvider provideDataForCanTransform + */ + public function testCanTransform(string $url, bool $expected): void { + $this->content->expects('getContentLink') + ->once() + ->andReturns($url); + + $this->assertEquals($expected, $this->transformer->canTransform($this->content)); + } + + public static function provideDataForCanTransform(): \Generator { + yield 'not Reddit URL' => ['https://example.org', false]; + yield 'not Reddit gallery URL' => ['https://reddit.com', false]; + yield 'Reddit gallery URL' => ['https://reddit.com/gallery', true]; + } + + public function testTransformWhenNoContent(): void { + $client = m::mock(Client::class); + $client->expects('jsonGet') + ->once() + ->with('https://example.org/.json') + ->andReturns([]); + + $this->content->expects('getCommentsLink') + ->once() + ->andReturns('https://example.org/'); + + $this->transformer->setClient($client); + $this->assertEquals('', $this->transformer->transform($this->content)); + } + + public function testTransformWhenContent(): void { + $client = m::mock(Client::class); + $client->expects('jsonGet') + ->once() + ->with('https://example.org/.json') + ->andReturns([[ + 'data' => [ + 'children' => [[ + 'data' => [ + 'media_metadata' => [ + 'image1of2' => [ + 'm' => 'image/jpg', + ], + 'image2of2' => [ + 'm' => 'image/jpg', + ], + ], + ], + ]], + ], + ]]); + + $this->content->expects('getCommentsLink') + ->once() + ->andReturns('https://example.org/'); + $this->settings->expects('getProcessor') + ->once() + ->andReturns('test'); + + $this->transformer->setClient($client); + $html = $this->transformer->transform($this->content); + + $this->assertHtmlHasGeneratedContentContainer($html); + $this->assertHtmlHasImage($html, 'https://i.redd.it/image1of2.jpg'); + $this->assertHtmlHasImage($html, 'https://i.redd.it/image2of2.jpg'); + } + + public function testTransformWhenCrosspostedContent(): void { + $client = m::mock(Client::class); + $client->expects('jsonGet') + ->once() + ->with('https://example.org/.json') + ->andReturns([[ + 'data' => [ + 'children' => [[ + 'data' => [ + 'crosspost_parent_list' => [[ + 'media_metadata' => [ + 'image1of2' => [ + 'm' => 'image/jpg', + ], + 'image2of2' => [ + 'm' => 'image/jpg', + ], + ], + ]], + ], + ]], + ], + ]]); + + $this->content->expects('getCommentsLink') + ->once() + ->andReturns('https://example.org/'); + $this->settings->expects('getProcessor') + ->once() + ->andReturns('test'); + + $this->transformer->setClient($client); + $html = $this->transformer->transform($this->content); + + $this->assertHtmlHasGeneratedContentContainer($html); + $this->assertHtmlHasImage($html, 'https://i.redd.it/image1of2.jpg'); + $this->assertHtmlHasImage($html, 'https://i.redd.it/image2of2.jpg'); + } +} diff --git a/tests/Transformer/Reddit/VideoTransformerTest.php b/tests/Transformer/Reddit/VideoTransformerTest.php new file mode 100644 index 0000000..08d8fac --- /dev/null +++ b/tests/Transformer/Reddit/VideoTransformerTest.php @@ -0,0 +1,131 @@ +content = m::mock(Content::class); + $this->settings = m::mock(Settings::class); + $this->transformer = new VideoTransformer($this->settings); + } + + /** + * @dataProvider provideDataForCanTransform + */ + public function testCanTransform(string $url, bool $expected): void { + $this->content->expects('getContentLink') + ->once() + ->andReturns($url); + + $this->assertEquals($expected, $this->transformer->canTransform($this->content)); + } + + public static function provideDataForCanTransform(): \Generator { + yield 'not Reddit URL' => ['https://example.org', false]; + yield 'not Redd.it URL' => ['https://reddit.com', false]; + yield 'not Redd.it video URL' => ['https://redd.it', false]; + yield 'Redd.it video URL' => ['https://v.redd.it', true]; + yield 'Redd.it video URL with query string' => ['https://v.redd.it?key=param', true]; + } + + public function testTransformWhenNoContent(): void { + $client = m::mock(Client::class); + $client->expects('jsonGet') + ->once() + ->with('https://example.org/.json') + ->andReturns([]); + + $this->content->expects('getCommentsLink') + ->once() + ->andReturns('https://example.org/'); + + $this->transformer->setClient($client); + $this->assertEquals('', $this->transformer->transform($this->content)); + } + + public function testTransformWhenContent(): void { + $client = m::mock(Client::class); + $client->expects('jsonGet') + ->once() + ->with('https://example.org/.json') + ->andReturns([[ + 'data' => [ + 'children' => [[ + 'data' => [ + 'media' => [ + 'reddit_video' => [ + 'fallback_url' => 'https://example.org/DASH_video.mp4?source=fallback', + ], + ], + ], + ]], + ], + ]]); + + $this->content->expects('getCommentsLink') + ->once() + ->andReturns('https://example.org/'); + $this->settings->expects('getProcessor') + ->once() + ->andReturns('test'); + + $this->transformer->setClient($client); + $html = $this->transformer->transform($this->content); + + $this->assertHtmlHasGeneratedContentContainer($html); + $this->assertHtmlHasMp4VideoWithAudio($html, 'https://example.org/DASH_video.mp4', 'https://example.org/DASH_audio.mp4'); + } + + public function testTransformWhenCrosspostedContent(): void { + $client = m::mock(Client::class); + $client->expects('jsonGet') + ->once() + ->with('https://example.org/.json') + ->andReturns([[ + 'data' => [ + 'children' => [[ + 'data' => [ + 'crosspost_parent_list' => [[ + 'media' => [ + 'reddit_video' => [ + 'fallback_url' => 'https://example.org/DASH_video.mp4?source=fallback', + ], + ], + ]], + ], + ]], + ], + ]]); + + $this->content->expects('getCommentsLink') + ->once() + ->andReturns('https://example.org/'); + $this->settings->expects('getProcessor') + ->once() + ->andReturns('test'); + + $this->transformer->setClient($client); + $html = $this->transformer->transform($this->content); + + $this->assertHtmlHasGeneratedContentContainer($html); + $this->assertHtmlHasMp4VideoWithAudio($html, 'https://example.org/DASH_video.mp4', 'https://example.org/DASH_audio.mp4'); + } +}