Skip to content

Commit 80b8491

Browse files
committed
Complete rewrite
The code has a better structure, it's more readable and more extendable. It is almost completly tested, and fully hardened. There is more things to come but for now, it's a good improvement.
1 parent 64ce9d2 commit 80b8491

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+2125
-425
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ static/*
44
vendor/
55
var/
66
.phpunit.cache/
7+
phpunit.xml

Client/Client.php

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RedditImage\Client;
6+
7+
use RedditImage\Exception\ClientException;
8+
9+
class Client {
10+
private string $userAgent;
11+
12+
public function __construct(string $userAgent) {
13+
$this->userAgent = $userAgent;
14+
}
15+
16+
public function jsonGet(string $url, array $headers = []): array {
17+
$ch = curl_init();
18+
curl_setopt_array($ch, [
19+
CURLOPT_URL => $url,
20+
CURLOPT_HEADER => 0,
21+
CURLOPT_RETURNTRANSFER => true,
22+
CURLOPT_FOLLOWLOCATION => true,
23+
CURLOPT_FAILONERROR => true,
24+
CURLOPT_USERAGENT => $this->userAgent,
25+
CURLOPT_HTTPHEADER => $headers,
26+
]);
27+
$jsonString = curl_exec($ch);
28+
if (curl_errno($ch)) {
29+
curl_close($ch);
30+
throw new ClientException(curl_error($ch));
31+
}
32+
curl_close($ch);
33+
34+
return json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR);
35+
}
36+
37+
public function isAccessible(string $url, array $headers = []): bool {
38+
$ch = curl_init();
39+
curl_setopt_array($ch, [
40+
CURLOPT_URL => $url,
41+
CURLOPT_NOBODY => true,
42+
CURLOPT_RETURNTRANSFER => true,
43+
CURLOPT_FOLLOWLOCATION => true,
44+
CURLOPT_FAILONERROR => true,
45+
CURLOPT_USERAGENT => $this->userAgent,
46+
CURLOPT_HTTPHEADER => $headers,
47+
]);
48+
curl_exec($ch);
49+
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
50+
curl_close($ch);
51+
52+
return 200 === $httpCode;
53+
}
54+
}

Exception/ClientException.php

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RedditImage\Exception;
6+
7+
use \Exception;
8+
9+
class ClientException extends Exception {
10+
}

Media/Image.php

-4
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ public function __construct(string $url) {
1111
$this->url = $url;
1212
}
1313

14-
public function getUrl(): string {
15-
return $this->url;
16-
}
17-
1814
public function toDomElement(\DomDocument $domDocument): \DomElement {
1915
$image = $domDocument->createElement('img');
2016
$image->setAttribute('src', $this->url);

Media/Link.php

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RedditImage\Media;
6+
7+
class Link implements DomElementInterface {
8+
private string $url;
9+
10+
public function __construct(string $url) {
11+
$this->url = $url;
12+
}
13+
14+
public function toDomElement(\DomDocument $domDocument): \DomElement {
15+
$p = $domDocument->createElement('p');
16+
$a = $p->appendChild($domDocument->createElement('a'));
17+
$a->setAttribute('href', $this->url);
18+
$a->appendChild($domDocument->createTextNode($this->url));
19+
20+
return $p;
21+
}
22+
}

Processor/AbstractProcessor.php

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RedditImage\Processor;
6+
7+
use RedditImage\Settings;
8+
9+
abstract class AbstractProcessor {
10+
protected const MATCH_REDDIT = 'reddit.com';
11+
12+
protected $settings;
13+
14+
/** @var TransformerInterface[] */
15+
protected array $transformers = [];
16+
17+
public function __construct(Settings $settings) {
18+
$this->settings = $settings;
19+
$this->settings->setProcessor(get_class($this));
20+
}
21+
22+
/**
23+
* @param FreshRSS_Entry $entry
24+
* @return FreshRSS_Entry
25+
*/
26+
abstract public function process($entry);
27+
28+
protected function isRedditLink($entry): bool {
29+
return (bool) strpos($entry->link(), static::MATCH_REDDIT);
30+
}
31+
}

Processor/BeforeDisplayProcessor.php

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RedditImage\Processor;
6+
7+
use \Throwable;
8+
use Minz_Log;
9+
use RedditImage\Content;
10+
use RedditImage\Settings;
11+
use RedditImage\Transformer\Agnostic\ImageTransformer as AgnosticImageTransformer;
12+
use RedditImage\Transformer\Agnostic\LinkTransformer as AgnosticLinkTransformer;
13+
use RedditImage\Transformer\Agnostic\VideoTransformer as AgnosticVideoTransformer;
14+
use RedditImage\Transformer\Imgur\ImageTransformer as ImgurImageTransformer;
15+
use RedditImage\Transformer\Imgur\VideoTransformer as ImgurVideoTransformer;
16+
17+
class BeforeDisplayProcessor extends AbstractProcessor {
18+
public function __construct(Settings $settings) {
19+
parent::__construct($settings);
20+
21+
if ($this->settings->getDisplayImage()) {
22+
$this->transformers[] = new AgnosticImageTransformer($this->settings);
23+
$this->transformers[] = new ImgurVideoTransformer($this->settings);
24+
$this->transformers[] = new ImgurImageTransformer($this->settings);
25+
}
26+
if ($this->settings->getDisplayVideo()) {
27+
$this->transformers[] = new AgnosticVideoTransformer($this->settings);
28+
}
29+
$this->transformers[] = new AgnosticLinkTransformer($this->settings);
30+
}
31+
32+
/**
33+
* @param FreshRSS_Entry $entry
34+
* @return FreshRSS_Entry
35+
*/
36+
public function process($entry) {
37+
if (false === $this->isRedditLink($entry)) {
38+
return $entry;
39+
}
40+
41+
$content = new Content($entry->content());
42+
$improved = $this->getImprovedContent($content);
43+
$original = $this->getOriginalContent($content);
44+
$metadata = $this->getMetadataContent($content);
45+
46+
if (!$this->settings->getDisplayThumbnails()) {
47+
$entry->_attributes('thumbnail', null);
48+
$entry->_attributes('enclosures', null);
49+
}
50+
$entry->_content("{$improved}{$content->getReal()}{$original}{$metadata}");
51+
$entry->_link($content->getContentLink());
52+
53+
return $entry;
54+
}
55+
56+
private function getImprovedContent(Content $content): string {
57+
$improved = $content->hasBeenPreprocessed() ? $content->getPreprocessed() : $this->processContent($content);
58+
59+
if ($improved === '') {
60+
return '';
61+
}
62+
63+
$dom = new \DomDocument('1.0', 'UTF-8');
64+
$dom->loadHTML($improved, LIBXML_NOERROR);
65+
66+
if (!$this->settings->getDisplayImage()) {
67+
$images = $dom->getElementsByTagName('img');
68+
// See https://www.php.net/manual/en/class.domnodelist.php#83390
69+
for ($i = $images->length; --$i >= 0; ) {
70+
$image = $images->item($i);
71+
$image->parentNode->removeChild($image);
72+
}
73+
}
74+
75+
if (!$this->settings->getDisplayVideo()) {
76+
$videos = $dom->getElementsByTagName('video');
77+
// See https://www.php.net/manual/en/class.domnodelist.php#83390
78+
for ($i = $videos->length; --$i >= 0; ) {
79+
$video = $videos->item($i);
80+
$video->parentNode->removeChild($video);
81+
}
82+
}
83+
84+
if ($this->settings->getMutedVideo()) {
85+
$videos = $dom->getElementsByTagName('video');
86+
foreach ($videos as $video) {
87+
$video->setAttribute('muted', 'true');
88+
}
89+
$audios = $dom->getElementsByTagName('audio');
90+
foreach ($audios as $audio) {
91+
$audio->setAttribute('muted', 'true');
92+
}
93+
}
94+
95+
return $dom->saveHTML();
96+
}
97+
98+
private function processContent(Content $content): string {
99+
foreach ($this->transformers as $transformer) {
100+
if (!$transformer->canTransform($content)) {
101+
continue;
102+
}
103+
104+
try {
105+
return $transformer->transform($content);
106+
} catch (Throwable $e) {
107+
Minz_Log::error("{$e->__toString()} - {$content->getContentLink()}");
108+
}
109+
}
110+
111+
return '';
112+
}
113+
114+
private function getOriginalContent(Content $content): string {
115+
if ($this->settings->getDisplayOriginal()) {
116+
return $content->getRaw();
117+
}
118+
119+
return '';
120+
}
121+
122+
private function getMetadataContent(Content $content): string {
123+
if ($this->settings->getDisplayMetadata()) {
124+
return "<div>{$content->getMetadata()}</div>";
125+
}
126+
127+
return '';
128+
}
129+
}

Processor/BeforeInsertProcessor.php

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RedditImage\Processor;
6+
7+
use \Throwable;
8+
use Minz_Log;
9+
use RedditImage\Client\Client;
10+
use RedditImage\Content;
11+
use RedditImage\Settings;
12+
use RedditImage\Transformer\Agnostic\ImageTransformer as AgnosticImageTransformer;
13+
use RedditImage\Transformer\Gfycat\VideoTransformer as GfycatVideoTransformer;
14+
use RedditImage\Transformer\Imgur\GalleryWithClientIdTransformer as ImgurGalleryWithClientIdTransformer;
15+
use RedditImage\Transformer\Imgur\ImageTransformer as ImgurImageTransformer;
16+
use RedditImage\Transformer\Imgur\VideoTransformer as ImgurVideoTransformer;
17+
use RedditImage\Transformer\Reddit\GalleryTransformer as RedditGalleryTransformer;
18+
use RedditImage\Transformer\Reddit\VideoTransformer as RedditVideoTransformer;
19+
20+
class BeforeInsertProcessor extends AbstractProcessor {
21+
public function __construct(Settings $settings, Client $client) {
22+
parent::__construct($settings);
23+
24+
$this->transformers[] = new AgnosticImageTransformer($this->settings);
25+
$this->transformers[] = new ImgurGalleryWithClientIdTransformer($this->settings);
26+
$this->transformers[] = new ImgurImageTransformer($this->settings);
27+
$this->transformers[] = new ImgurVideoTransformer($this->settings);
28+
$this->transformers[] = new GfycatVideoTransformer($this->settings);
29+
$this->transformers[] = new RedditVideoTransformer($this->settings);
30+
$this->transformers[] = new RedditGalleryTransformer($this->settings);
31+
32+
foreach ($this->transformers as $transformer) {
33+
$transformer->setClient($client);
34+
}
35+
}
36+
37+
/**
38+
* @param FreshRSS_Entry $entry
39+
* @return FreshRSS_Entry
40+
*/
41+
public function process($entry) {
42+
if (false === $this->isRedditLink($entry)) {
43+
return $entry;
44+
}
45+
46+
$newContent = '';
47+
$content = new Content($entry->content());
48+
49+
foreach ($this->transformers as $transformer) {
50+
if (!$transformer->canTransform($content)) {
51+
continue;
52+
}
53+
54+
try {
55+
$newContent = $transformer->transform($content);
56+
break;
57+
} catch (Throwable $e) {
58+
Minz_Log::error("{$e->__toString()} - {$content->getContentLink()}");
59+
}
60+
}
61+
62+
if ($newContent !== '') {
63+
$entry->_content("{$newContent}{$content->getRaw()}");
64+
}
65+
66+
return $entry;
67+
}
68+
}

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ At the moment, the following resources are recognized:
88

99
&nbsp; |match | type | support
1010
-------|------|------|--------
11-
1 | links finished by jpg, png, gif, bmp | image | full
11+
1 | links finished by jpg, jpeg, png, gif, bmp | image | full
1212
2 | imgur links finished by gifv | video | full
1313
3 | imgur links finished with a token | image | partial
1414
4 | links finished by webm, mp4 | video | full
1515
5 | gfycat links finished with a token | video | full
1616
6 | redgifs links finished with a token | video | none
1717
7 | reddit links finished with a token | video | limited (no audio)
1818
8 | reddit image galleries | image | full
19-
9 | imgur image galleries | image | full with API client id; partial without
19+
9 | imgur image galleries | image | full with API client id; none without
2020

2121
**Note** the support from redgifs links with a token went from full to none after a change in their API.
2222

@@ -31,6 +31,7 @@ Display images | Choose if images are displayed | **True**
3131
Display videos | Choose if videos are displayed | **True**
3232
Display original content | Choose if original contents are displayed | **True**
3333
Display metadata | Choose if original content metadata are displayed | **False**
34+
Display thumbnails | Choose if feed enclosure are displayed | **False**
3435

3536
**Note:**
3637
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*.

0 commit comments

Comments
 (0)