diff options
author | Jeremy Benoist <jeremy.benoist@gmail.com> | 2017-06-08 21:51:46 +0200 |
---|---|---|
committer | Jeremy Benoist <jeremy.benoist@gmail.com> | 2017-06-08 21:51:46 +0200 |
commit | 0d349ea67073c535e1aa7f19f3cf842a54458bfe (patch) | |
tree | c5449f49c941ea7cc09f7606e87464c3d03367cf | |
parent | 3f474025d889c3eff20b481f005f4d292f1ef29d (diff) | |
download | wallabag-0d349ea67073c535e1aa7f19f3cf842a54458bfe.tar.gz wallabag-0d349ea67073c535e1aa7f19f3cf842a54458bfe.tar.zst wallabag-0d349ea67073c535e1aa7f19f3cf842a54458bfe.zip |
Validate language & preview picture fields
Instead of saving the value of each field right into the content without any validation, it seems better to validate them.
This might sounds obvious now we say that.
-rw-r--r-- | src/Wallabag/CoreBundle/Helper/ContentProxy.php | 78 | ||||
-rw-r--r-- | src/Wallabag/CoreBundle/Resources/config/services.yml | 1 | ||||
-rw-r--r-- | tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php | 129 |
3 files changed, 185 insertions, 23 deletions
diff --git a/src/Wallabag/CoreBundle/Helper/ContentProxy.php b/src/Wallabag/CoreBundle/Helper/ContentProxy.php index d5820e66..dd9170ad 100644 --- a/src/Wallabag/CoreBundle/Helper/ContentProxy.php +++ b/src/Wallabag/CoreBundle/Helper/ContentProxy.php | |||
@@ -7,6 +7,9 @@ use Psr\Log\LoggerInterface; | |||
7 | use Wallabag\CoreBundle\Entity\Entry; | 7 | use Wallabag\CoreBundle\Entity\Entry; |
8 | use Wallabag\CoreBundle\Tools\Utils; | 8 | use Wallabag\CoreBundle\Tools\Utils; |
9 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser; | 9 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser; |
10 | use Symfony\Component\Validator\Constraints\Language as LanguageConstraint; | ||
11 | use Symfony\Component\Validator\Constraints\Url as UrlConstraint; | ||
12 | use Symfony\Component\Validator\Validator\ValidatorInterface; | ||
10 | 13 | ||
11 | /** | 14 | /** |
12 | * This kind of proxy class take care of getting the content from an url | 15 | * This kind of proxy class take care of getting the content from an url |
@@ -21,10 +24,11 @@ class ContentProxy | |||
21 | protected $fetchingErrorMessage; | 24 | protected $fetchingErrorMessage; |
22 | protected $eventDispatcher; | 25 | protected $eventDispatcher; |
23 | 26 | ||
24 | public function __construct(Graby $graby, RuleBasedTagger $tagger, LoggerInterface $logger, $fetchingErrorMessage) | 27 | public function __construct(Graby $graby, RuleBasedTagger $tagger, ValidatorInterface $validator, LoggerInterface $logger, $fetchingErrorMessage) |
25 | { | 28 | { |
26 | $this->graby = $graby; | 29 | $this->graby = $graby; |
27 | $this->tagger = $tagger; | 30 | $this->tagger = $tagger; |
31 | $this->validator = $validator; | ||
28 | $this->logger = $logger; | 32 | $this->logger = $logger; |
29 | $this->mimeGuesser = new MimeTypeExtensionGuesser(); | 33 | $this->mimeGuesser = new MimeTypeExtensionGuesser(); |
30 | $this->fetchingErrorMessage = $fetchingErrorMessage; | 34 | $this->fetchingErrorMessage = $fetchingErrorMessage; |
@@ -113,7 +117,24 @@ class ContentProxy | |||
113 | $entry->setHeaders($content['all_headers']); | 117 | $entry->setHeaders($content['all_headers']); |
114 | } | 118 | } |
115 | 119 | ||
116 | $entry->setLanguage(isset($content['language']) ? $content['language'] : ''); | 120 | $this->validateAndSetLanguage( |
121 | $entry, | ||
122 | isset($content['language']) ? $content['language'] : '' | ||
123 | ); | ||
124 | |||
125 | $this->validateAndSetPreviewPicture( | ||
126 | $entry, | ||
127 | isset($content['open_graph']['og_image']) ? $content['open_graph']['og_image'] : '' | ||
128 | ); | ||
129 | |||
130 | // if content is an image define as a preview too | ||
131 | if (!empty($content['content_type']) && in_array($this->mimeGuesser->guess($content['content_type']), ['jpeg', 'jpg', 'gif', 'png'], true)) { | ||
132 | $this->validateAndSetPreviewPicture( | ||
133 | $entry, | ||
134 | $content['url'] | ||
135 | ); | ||
136 | } | ||
137 | |||
117 | $entry->setMimetype(isset($content['content_type']) ? $content['content_type'] : ''); | 138 | $entry->setMimetype(isset($content['content_type']) ? $content['content_type'] : ''); |
118 | $entry->setReadingTime(Utils::getReadingTime($html)); | 139 | $entry->setReadingTime(Utils::getReadingTime($html)); |
119 | 140 | ||
@@ -122,15 +143,6 @@ class ContentProxy | |||
122 | $entry->setDomainName($domainName); | 143 | $entry->setDomainName($domainName); |
123 | } | 144 | } |
124 | 145 | ||
125 | if (!empty($content['open_graph']['og_image'])) { | ||
126 | $entry->setPreviewPicture($content['open_graph']['og_image']); | ||
127 | } | ||
128 | |||
129 | // if content is an image define as a preview too | ||
130 | if (!empty($content['content_type']) && in_array($this->mimeGuesser->guess($content['content_type']), ['jpeg', 'jpg', 'gif', 'png'], true)) { | ||
131 | $entry->setPreviewPicture($content['url']); | ||
132 | } | ||
133 | |||
134 | try { | 146 | try { |
135 | $this->tagger->tag($entry); | 147 | $this->tagger->tag($entry); |
136 | } catch (\Exception $e) { | 148 | } catch (\Exception $e) { |
@@ -152,4 +164,48 @@ class ContentProxy | |||
152 | { | 164 | { |
153 | return !empty($content['title']) && !empty($content['html']) && !empty($content['url']); | 165 | return !empty($content['title']) && !empty($content['html']) && !empty($content['url']); |
154 | } | 166 | } |
167 | |||
168 | /** | ||
169 | * Use a Symfony validator to ensure the language is well formatted. | ||
170 | * | ||
171 | * @param Entry $entry | ||
172 | * @param string $value Language to validate | ||
173 | */ | ||
174 | private function validateAndSetLanguage($entry, $value) | ||
175 | { | ||
176 | $errors = $this->validator->validate( | ||
177 | $value, | ||
178 | (new LanguageConstraint()) | ||
179 | ); | ||
180 | |||
181 | if (0 === count($errors)) { | ||
182 | $entry->setLanguage($value); | ||
183 | |||
184 | return; | ||
185 | } | ||
186 | |||
187 | $this->logger->warning('Language validation failed. '.(string) $errors); | ||
188 | } | ||
189 | |||
190 | /** | ||
191 | * Use a Symfony validator to ensure the preview picture is a real url. | ||
192 | * | ||
193 | * @param Entry $entry | ||
194 | * @param string $value URL to validate | ||
195 | */ | ||
196 | private function validateAndSetPreviewPicture($entry, $value) | ||
197 | { | ||
198 | $errors = $this->validator->validate( | ||
199 | $value, | ||
200 | (new UrlConstraint()) | ||
201 | ); | ||
202 | |||
203 | if (0 === count($errors)) { | ||
204 | $entry->setPreviewPicture($value); | ||
205 | |||
206 | return; | ||
207 | } | ||
208 | |||
209 | $this->logger->warning('PreviewPicture validation failed. '.(string) $errors); | ||
210 | } | ||
155 | } | 211 | } |
diff --git a/src/Wallabag/CoreBundle/Resources/config/services.yml b/src/Wallabag/CoreBundle/Resources/config/services.yml index a9b0d2d5..2ae5d27f 100644 --- a/src/Wallabag/CoreBundle/Resources/config/services.yml +++ b/src/Wallabag/CoreBundle/Resources/config/services.yml | |||
@@ -90,6 +90,7 @@ services: | |||
90 | arguments: | 90 | arguments: |
91 | - "@wallabag_core.graby" | 91 | - "@wallabag_core.graby" |
92 | - "@wallabag_core.rule_based_tagger" | 92 | - "@wallabag_core.rule_based_tagger" |
93 | - "@validator" | ||
93 | - "@logger" | 94 | - "@logger" |
94 | - '%wallabag_core.fetching_error_message%' | 95 | - '%wallabag_core.fetching_error_message%' |
95 | 96 | ||
diff --git a/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php b/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php index a3570125..95dd75ba 100644 --- a/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php +++ b/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php | |||
@@ -11,6 +11,9 @@ use Wallabag\CoreBundle\Entity\Tag; | |||
11 | use Wallabag\UserBundle\Entity\User; | 11 | use Wallabag\UserBundle\Entity\User; |
12 | use Wallabag\CoreBundle\Helper\RuleBasedTagger; | 12 | use Wallabag\CoreBundle\Helper\RuleBasedTagger; |
13 | use Graby\Graby; | 13 | use Graby\Graby; |
14 | use Symfony\Component\Validator\Validator\RecursiveValidator; | ||
15 | use Symfony\Component\Validator\ConstraintViolationList; | ||
16 | use Symfony\Component\Validator\ConstraintViolation; | ||
14 | 17 | ||
15 | class ContentProxyTest extends \PHPUnit_Framework_TestCase | 18 | class ContentProxyTest extends \PHPUnit_Framework_TestCase |
16 | { | 19 | { |
@@ -37,7 +40,7 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase | |||
37 | 'language' => '', | 40 | 'language' => '', |
38 | ]); | 41 | ]); |
39 | 42 | ||
40 | $proxy = new ContentProxy($graby, $tagger, $this->getLogger(), $this->fetchingErrorMessage); | 43 | $proxy = new ContentProxy($graby, $tagger, $this->getValidator(), $this->getLogger(), $this->fetchingErrorMessage); |
41 | $entry = new Entry(new User()); | 44 | $entry = new Entry(new User()); |
42 | $proxy->updateEntry($entry, 'http://user@:80'); | 45 | $proxy->updateEntry($entry, 'http://user@:80'); |
43 | 46 | ||
@@ -72,7 +75,7 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase | |||
72 | 'language' => '', | 75 | 'language' => '', |
73 | ]); | 76 | ]); |
74 | 77 | ||
75 | $proxy = new ContentProxy($graby, $tagger, $this->getLogger(), $this->fetchingErrorMessage); | 78 | $proxy = new ContentProxy($graby, $tagger, $this->getValidator(), $this->getLogger(), $this->fetchingErrorMessage); |
76 | $entry = new Entry(new User()); | 79 | $entry = new Entry(new User()); |
77 | $proxy->updateEntry($entry, 'http://0.0.0.0'); | 80 | $proxy->updateEntry($entry, 'http://0.0.0.0'); |
78 | 81 | ||
@@ -112,7 +115,7 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase | |||
112 | ], | 115 | ], |
113 | ]); | 116 | ]); |
114 | 117 | ||
115 | $proxy = new ContentProxy($graby, $tagger, $this->getLogger(), $this->fetchingErrorMessage); | 118 | $proxy = new ContentProxy($graby, $tagger, $this->getValidator(), $this->getLogger(), $this->fetchingErrorMessage); |
116 | $entry = new Entry(new User()); | 119 | $entry = new Entry(new User()); |
117 | $proxy->updateEntry($entry, 'http://domain.io'); | 120 | $proxy->updateEntry($entry, 'http://domain.io'); |
118 | 121 | ||
@@ -154,7 +157,7 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase | |||
154 | ], | 157 | ], |
155 | ]); | 158 | ]); |
156 | 159 | ||
157 | $proxy = new ContentProxy($graby, $tagger, $this->getLogger(), $this->fetchingErrorMessage); | 160 | $proxy = new ContentProxy($graby, $tagger, $this->getValidator(), $this->getLogger(), $this->fetchingErrorMessage); |
158 | $entry = new Entry(new User()); | 161 | $entry = new Entry(new User()); |
159 | $proxy->updateEntry($entry, 'http://0.0.0.0'); | 162 | $proxy->updateEntry($entry, 'http://0.0.0.0'); |
160 | 163 | ||
@@ -192,18 +195,112 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase | |||
192 | 'open_graph' => [ | 195 | 'open_graph' => [ |
193 | 'og_title' => 'my OG title', | 196 | 'og_title' => 'my OG title', |
194 | 'og_description' => 'OG desc', | 197 | 'og_description' => 'OG desc', |
195 | 'og_image' => false, | 198 | 'og_image' => null, |
196 | ], | 199 | ], |
197 | ]); | 200 | ]); |
198 | 201 | ||
199 | $proxy = new ContentProxy($graby, $tagger, $this->getLogger(), $this->fetchingErrorMessage); | 202 | $proxy = new ContentProxy($graby, $tagger, $this->getValidator(), $this->getLogger(), $this->fetchingErrorMessage); |
200 | $entry = new Entry(new User()); | 203 | $entry = new Entry(new User()); |
201 | $proxy->updateEntry($entry, 'http://0.0.0.0'); | 204 | $proxy->updateEntry($entry, 'http://0.0.0.0'); |
202 | 205 | ||
203 | $this->assertEquals('http://1.1.1.1', $entry->getUrl()); | 206 | $this->assertEquals('http://1.1.1.1', $entry->getUrl()); |
204 | $this->assertEquals('this is my title', $entry->getTitle()); | 207 | $this->assertEquals('this is my title', $entry->getTitle()); |
205 | $this->assertContains('this is my content', $entry->getContent()); | 208 | $this->assertContains('this is my content', $entry->getContent()); |
206 | $this->assertNull($entry->getPreviewPicture()); | 209 | $this->assertEmpty($entry->getPreviewPicture()); |
210 | $this->assertEquals('text/html', $entry->getMimetype()); | ||
211 | $this->assertEquals('fr', $entry->getLanguage()); | ||
212 | $this->assertEquals('200', $entry->getHttpStatus()); | ||
213 | $this->assertEquals(4.0, $entry->getReadingTime()); | ||
214 | $this->assertEquals('1.1.1.1', $entry->getDomainName()); | ||
215 | } | ||
216 | |||
217 | public function testWithContentAndBadLanguage() | ||
218 | { | ||
219 | $tagger = $this->getTaggerMock(); | ||
220 | $tagger->expects($this->once()) | ||
221 | ->method('tag'); | ||
222 | |||
223 | $validator = $this->getValidator(); | ||
224 | $validator->expects($this->exactly(2)) | ||
225 | ->method('validate') | ||
226 | ->will($this->onConsecutiveCalls( | ||
227 | new ConstraintViolationList([new ConstraintViolation('oops', 'oops', [], 'oops', 'language', 'dontexist')]), | ||
228 | new ConstraintViolationList() | ||
229 | )); | ||
230 | |||
231 | $graby = $this->getMockBuilder('Graby\Graby') | ||
232 | ->setMethods(['fetchContent']) | ||
233 | ->disableOriginalConstructor() | ||
234 | ->getMock(); | ||
235 | |||
236 | $graby->expects($this->any()) | ||
237 | ->method('fetchContent') | ||
238 | ->willReturn([ | ||
239 | 'html' => str_repeat('this is my content', 325), | ||
240 | 'title' => 'this is my title', | ||
241 | 'url' => 'http://1.1.1.1', | ||
242 | 'content_type' => 'text/html', | ||
243 | 'language' => 'dontexist', | ||
244 | 'status' => '200', | ||
245 | ]); | ||
246 | |||
247 | $proxy = new ContentProxy($graby, $tagger, $validator, $this->getLogger(), $this->fetchingErrorMessage); | ||
248 | $entry = new Entry(new User()); | ||
249 | $proxy->updateEntry($entry, 'http://0.0.0.0'); | ||
250 | |||
251 | $this->assertEquals('http://1.1.1.1', $entry->getUrl()); | ||
252 | $this->assertEquals('this is my title', $entry->getTitle()); | ||
253 | $this->assertContains('this is my content', $entry->getContent()); | ||
254 | $this->assertEquals('text/html', $entry->getMimetype()); | ||
255 | $this->assertEmpty($entry->getLanguage()); | ||
256 | $this->assertEquals('200', $entry->getHttpStatus()); | ||
257 | $this->assertEquals(4.0, $entry->getReadingTime()); | ||
258 | $this->assertEquals('1.1.1.1', $entry->getDomainName()); | ||
259 | } | ||
260 | |||
261 | public function testWithContentAndBadOgImage() | ||
262 | { | ||
263 | $tagger = $this->getTaggerMock(); | ||
264 | $tagger->expects($this->once()) | ||
265 | ->method('tag'); | ||
266 | |||
267 | $validator = $this->getValidator(); | ||
268 | $validator->expects($this->exactly(2)) | ||
269 | ->method('validate') | ||
270 | ->will($this->onConsecutiveCalls( | ||
271 | new ConstraintViolationList(), | ||
272 | new ConstraintViolationList([new ConstraintViolation('oops', 'oops', [], 'oops', 'url', 'https://')]) | ||
273 | )); | ||
274 | |||
275 | $graby = $this->getMockBuilder('Graby\Graby') | ||
276 | ->setMethods(['fetchContent']) | ||
277 | ->disableOriginalConstructor() | ||
278 | ->getMock(); | ||
279 | |||
280 | $graby->expects($this->any()) | ||
281 | ->method('fetchContent') | ||
282 | ->willReturn([ | ||
283 | 'html' => str_repeat('this is my content', 325), | ||
284 | 'title' => 'this is my title', | ||
285 | 'url' => 'http://1.1.1.1', | ||
286 | 'content_type' => 'text/html', | ||
287 | 'language' => 'fr', | ||
288 | 'status' => '200', | ||
289 | 'open_graph' => [ | ||
290 | 'og_title' => 'my OG title', | ||
291 | 'og_description' => 'OG desc', | ||
292 | 'og_image' => 'https://', | ||
293 | ], | ||
294 | ]); | ||
295 | |||
296 | $proxy = new ContentProxy($graby, $tagger, $validator, $this->getLogger(), $this->fetchingErrorMessage); | ||
297 | $entry = new Entry(new User()); | ||
298 | $proxy->updateEntry($entry, 'http://0.0.0.0'); | ||
299 | |||
300 | $this->assertEquals('http://1.1.1.1', $entry->getUrl()); | ||
301 | $this->assertEquals('this is my title', $entry->getTitle()); | ||
302 | $this->assertContains('this is my content', $entry->getContent()); | ||
303 | $this->assertEmpty($entry->getPreviewPicture()); | ||
207 | $this->assertEquals('text/html', $entry->getMimetype()); | 304 | $this->assertEquals('text/html', $entry->getMimetype()); |
208 | $this->assertEquals('fr', $entry->getLanguage()); | 305 | $this->assertEquals('fr', $entry->getLanguage()); |
209 | $this->assertEquals('200', $entry->getHttpStatus()); | 306 | $this->assertEquals('200', $entry->getHttpStatus()); |
@@ -217,7 +314,7 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase | |||
217 | $tagger->expects($this->once()) | 314 | $tagger->expects($this->once()) |
218 | ->method('tag'); | 315 | ->method('tag'); |
219 | 316 | ||
220 | $proxy = new ContentProxy((new Graby()), $tagger, $this->getLogger(), $this->fetchingErrorMessage); | 317 | $proxy = new ContentProxy((new Graby()), $tagger, $this->getValidator(), $this->getLogger(), $this->fetchingErrorMessage); |
221 | $entry = new Entry(new User()); | 318 | $entry = new Entry(new User()); |
222 | $proxy->updateEntry( | 319 | $proxy->updateEntry( |
223 | $entry, | 320 | $entry, |
@@ -259,7 +356,7 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase | |||
259 | $logHandler = new TestHandler(); | 356 | $logHandler = new TestHandler(); |
260 | $logger = new Logger('test', [$logHandler]); | 357 | $logger = new Logger('test', [$logHandler]); |
261 | 358 | ||
262 | $proxy = new ContentProxy((new Graby()), $tagger, $logger, $this->fetchingErrorMessage); | 359 | $proxy = new ContentProxy((new Graby()), $tagger, $this->getValidator(), $logger, $this->fetchingErrorMessage); |
263 | $entry = new Entry(new User()); | 360 | $entry = new Entry(new User()); |
264 | $proxy->updateEntry( | 361 | $proxy->updateEntry( |
265 | $entry, | 362 | $entry, |
@@ -294,7 +391,7 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase | |||
294 | $handler = new TestHandler(); | 391 | $handler = new TestHandler(); |
295 | $logger->pushHandler($handler); | 392 | $logger->pushHandler($handler); |
296 | 393 | ||
297 | $proxy = new ContentProxy((new Graby()), $tagger, $logger, $this->fetchingErrorMessage); | 394 | $proxy = new ContentProxy((new Graby()), $tagger, $this->getValidator(), $logger, $this->fetchingErrorMessage); |
298 | $entry = new Entry(new User()); | 395 | $entry = new Entry(new User()); |
299 | $proxy->updateEntry( | 396 | $proxy->updateEntry( |
300 | $entry, | 397 | $entry, |
@@ -331,7 +428,7 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase | |||
331 | ->method('tag') | 428 | ->method('tag') |
332 | ->will($this->throwException(new \Exception())); | 429 | ->will($this->throwException(new \Exception())); |
333 | 430 | ||
334 | $proxy = new ContentProxy((new Graby()), $tagger, $this->getLogger(), $this->fetchingErrorMessage); | 431 | $proxy = new ContentProxy((new Graby()), $tagger, $this->getValidator(), $this->getLogger(), $this->fetchingErrorMessage); |
335 | $entry = new Entry(new User()); | 432 | $entry = new Entry(new User()); |
336 | $proxy->updateEntry( | 433 | $proxy->updateEntry( |
337 | $entry, | 434 | $entry, |
@@ -371,7 +468,7 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase | |||
371 | $tagger->expects($this->once()) | 468 | $tagger->expects($this->once()) |
372 | ->method('tag'); | 469 | ->method('tag'); |
373 | 470 | ||
374 | $proxy = new ContentProxy((new Graby()), $tagger, $this->getLogger(), $this->fetchingErrorMessage); | 471 | $proxy = new ContentProxy((new Graby()), $tagger, $this->getValidator(), $this->getLogger(), $this->fetchingErrorMessage); |
375 | $entry = new Entry(new User()); | 472 | $entry = new Entry(new User()); |
376 | $proxy->updateEntry( | 473 | $proxy->updateEntry( |
377 | $entry, | 474 | $entry, |
@@ -413,4 +510,12 @@ class ContentProxyTest extends \PHPUnit_Framework_TestCase | |||
413 | { | 510 | { |
414 | return new NullLogger(); | 511 | return new NullLogger(); |
415 | } | 512 | } |
513 | |||
514 | private function getValidator() | ||
515 | { | ||
516 | return $this->getMockBuilder(RecursiveValidator::class) | ||
517 | ->setMethods(['validate']) | ||
518 | ->disableOriginalConstructor() | ||
519 | ->getMock(); | ||
520 | } | ||
416 | } | 521 | } |