diff options
Diffstat (limited to 'src/Wallabag/CoreBundle/Helper')
-rw-r--r-- | src/Wallabag/CoreBundle/Helper/ContentProxy.php | 200 | ||||
-rw-r--r-- | src/Wallabag/CoreBundle/Helper/CryptoProxy.php | 86 | ||||
-rw-r--r-- | src/Wallabag/CoreBundle/Helper/DownloadImages.php | 57 | ||||
-rw-r--r-- | src/Wallabag/CoreBundle/Helper/HttpClientFactory.php | 29 | ||||
-rw-r--r-- | src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php | 11 | ||||
-rw-r--r-- | src/Wallabag/CoreBundle/Helper/Redirect.php | 10 | ||||
-rw-r--r-- | src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php | 10 | ||||
-rw-r--r-- | src/Wallabag/CoreBundle/Helper/TagsAssigner.php | 75 |
8 files changed, 376 insertions, 102 deletions
diff --git a/src/Wallabag/CoreBundle/Helper/ContentProxy.php b/src/Wallabag/CoreBundle/Helper/ContentProxy.php index f222dd88..51bb2ca2 100644 --- a/src/Wallabag/CoreBundle/Helper/ContentProxy.php +++ b/src/Wallabag/CoreBundle/Helper/ContentProxy.php | |||
@@ -5,10 +5,11 @@ namespace Wallabag\CoreBundle\Helper; | |||
5 | use Graby\Graby; | 5 | use Graby\Graby; |
6 | use Psr\Log\LoggerInterface; | 6 | use Psr\Log\LoggerInterface; |
7 | use Wallabag\CoreBundle\Entity\Entry; | 7 | use Wallabag\CoreBundle\Entity\Entry; |
8 | use Wallabag\CoreBundle\Entity\Tag; | ||
9 | use Wallabag\CoreBundle\Tools\Utils; | 8 | use Wallabag\CoreBundle\Tools\Utils; |
10 | use Wallabag\CoreBundle\Repository\TagRepository; | ||
11 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser; | 9 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser; |
10 | use Symfony\Component\Validator\Constraints\Locale as LocaleConstraint; | ||
11 | use Symfony\Component\Validator\Constraints\Url as UrlConstraint; | ||
12 | use Symfony\Component\Validator\Validator\ValidatorInterface; | ||
12 | 13 | ||
13 | /** | 14 | /** |
14 | * 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 |
@@ -18,38 +19,37 @@ class ContentProxy | |||
18 | { | 19 | { |
19 | protected $graby; | 20 | protected $graby; |
20 | protected $tagger; | 21 | protected $tagger; |
22 | protected $validator; | ||
21 | protected $logger; | 23 | protected $logger; |
22 | protected $tagRepository; | ||
23 | protected $mimeGuesser; | 24 | protected $mimeGuesser; |
24 | protected $fetchingErrorMessage; | 25 | protected $fetchingErrorMessage; |
26 | protected $eventDispatcher; | ||
25 | 27 | ||
26 | public function __construct(Graby $graby, RuleBasedTagger $tagger, TagRepository $tagRepository, LoggerInterface $logger, $fetchingErrorMessage) | 28 | public function __construct(Graby $graby, RuleBasedTagger $tagger, ValidatorInterface $validator, LoggerInterface $logger, $fetchingErrorMessage) |
27 | { | 29 | { |
28 | $this->graby = $graby; | 30 | $this->graby = $graby; |
29 | $this->tagger = $tagger; | 31 | $this->tagger = $tagger; |
32 | $this->validator = $validator; | ||
30 | $this->logger = $logger; | 33 | $this->logger = $logger; |
31 | $this->tagRepository = $tagRepository; | ||
32 | $this->mimeGuesser = new MimeTypeExtensionGuesser(); | 34 | $this->mimeGuesser = new MimeTypeExtensionGuesser(); |
33 | $this->fetchingErrorMessage = $fetchingErrorMessage; | 35 | $this->fetchingErrorMessage = $fetchingErrorMessage; |
34 | } | 36 | } |
35 | 37 | ||
36 | /** | 38 | /** |
37 | * Fetch content using graby and hydrate given entry with results information. | 39 | * Update entry using either fetched or provided content. |
38 | * In case we couldn't find content, we'll try to use Open Graph data. | ||
39 | * | 40 | * |
40 | * We can also force the content, in case of an import from the v1 for example, so the function won't | 41 | * @param Entry $entry Entry to update |
41 | * fetch the content from the website but rather use information given with the $content parameter. | 42 | * @param string $url Url of the content |
42 | * | 43 | * @param array $content Array with content provided for import with AT LEAST keys title, html, url to skip the fetchContent from the url |
43 | * @param Entry $entry Entry to update | 44 | * @param bool $disableContentUpdate Whether to skip trying to fetch content using Graby |
44 | * @param string $url Url to grab content for | ||
45 | * @param array $content An array with AT LEAST keys title, html, url, language & content_type to skip the fetchContent from the url | ||
46 | * | ||
47 | * @return Entry | ||
48 | */ | 45 | */ |
49 | public function updateEntry(Entry $entry, $url, array $content = []) | 46 | public function updateEntry(Entry $entry, $url, array $content = [], $disableContentUpdate = false) |
50 | { | 47 | { |
51 | // do we have to fetch the content or the provided one is ok? | 48 | if (!empty($content['html'])) { |
52 | if (empty($content) || false === $this->validateContent($content)) { | 49 | $content['html'] = $this->graby->cleanupHtml($content['html'], $url); |
50 | } | ||
51 | |||
52 | if ((empty($content) || false === $this->validateContent($content)) && false === $disableContentUpdate) { | ||
53 | $fetchedContent = $this->graby->fetchContent($url); | 53 | $fetchedContent = $this->graby->fetchContent($url); |
54 | 54 | ||
55 | // when content is imported, we have information in $content | 55 | // when content is imported, we have information in $content |
@@ -59,8 +59,24 @@ class ContentProxy | |||
59 | } | 59 | } |
60 | } | 60 | } |
61 | 61 | ||
62 | // be sure to keep the url in case of error | ||
63 | // so we'll be able to refetch it in the future | ||
64 | $content['url'] = !empty($content['url']) ? $content['url'] : $url; | ||
65 | |||
66 | $this->stockEntry($entry, $content); | ||
67 | } | ||
68 | |||
69 | /** | ||
70 | * Stock entry with fetched or imported content. | ||
71 | * Will fall back to OpenGraph data if available. | ||
72 | * | ||
73 | * @param Entry $entry Entry to stock | ||
74 | * @param array $content Array with at least title, url & html | ||
75 | */ | ||
76 | private function stockEntry(Entry $entry, array $content) | ||
77 | { | ||
62 | $title = $content['title']; | 78 | $title = $content['title']; |
63 | if (!$title && isset($content['open_graph']['og_title'])) { | 79 | if (!$title && !empty($content['open_graph']['og_title'])) { |
64 | $title = $content['open_graph']['og_title']; | 80 | $title = $content['open_graph']['og_title']; |
65 | } | 81 | } |
66 | 82 | ||
@@ -68,18 +84,58 @@ class ContentProxy | |||
68 | if (false === $html) { | 84 | if (false === $html) { |
69 | $html = $this->fetchingErrorMessage; | 85 | $html = $this->fetchingErrorMessage; |
70 | 86 | ||
71 | if (isset($content['open_graph']['og_description'])) { | 87 | if (!empty($content['open_graph']['og_description'])) { |
72 | $html .= '<p><i>But we found a short description: </i></p>'; | 88 | $html .= '<p><i>But we found a short description: </i></p>'; |
73 | $html .= $content['open_graph']['og_description']; | 89 | $html .= $content['open_graph']['og_description']; |
74 | } | 90 | } |
75 | } | 91 | } |
76 | 92 | ||
77 | $entry->setUrl($content['url'] ?: $url); | 93 | $entry->setUrl($content['url']); |
78 | $entry->setTitle($title); | 94 | $entry->setTitle($title); |
79 | $entry->setContent($html); | 95 | $entry->setContent($html); |
80 | $entry->setHttpStatus(isset($content['status']) ? $content['status'] : ''); | 96 | $entry->setHttpStatus(isset($content['status']) ? $content['status'] : ''); |
81 | 97 | ||
82 | $entry->setLanguage(isset($content['language']) ? $content['language'] : ''); | 98 | if (!empty($content['date'])) { |
99 | $date = $content['date']; | ||
100 | |||
101 | // is it a timestamp? | ||
102 | if (filter_var($date, FILTER_VALIDATE_INT) !== false) { | ||
103 | $date = '@'.$content['date']; | ||
104 | } | ||
105 | |||
106 | try { | ||
107 | $entry->setPublishedAt(new \DateTime($date)); | ||
108 | } catch (\Exception $e) { | ||
109 | $this->logger->warning('Error while defining date', ['e' => $e, 'url' => $content['url'], 'date' => $content['date']]); | ||
110 | } | ||
111 | } | ||
112 | |||
113 | if (!empty($content['authors']) && is_array($content['authors'])) { | ||
114 | $entry->setPublishedBy($content['authors']); | ||
115 | } | ||
116 | |||
117 | if (!empty($content['all_headers'])) { | ||
118 | $entry->setHeaders($content['all_headers']); | ||
119 | } | ||
120 | |||
121 | $this->validateAndSetLanguage( | ||
122 | $entry, | ||
123 | isset($content['language']) ? $content['language'] : null | ||
124 | ); | ||
125 | |||
126 | $this->validateAndSetPreviewPicture( | ||
127 | $entry, | ||
128 | isset($content['open_graph']['og_image']) ? $content['open_graph']['og_image'] : null | ||
129 | ); | ||
130 | |||
131 | // if content is an image, define it as a preview too | ||
132 | if (!empty($content['content_type']) && in_array($this->mimeGuesser->guess($content['content_type']), ['jpeg', 'jpg', 'gif', 'png'], true)) { | ||
133 | $this->validateAndSetPreviewPicture( | ||
134 | $entry, | ||
135 | $content['url'] | ||
136 | ); | ||
137 | } | ||
138 | |||
83 | $entry->setMimetype(isset($content['content_type']) ? $content['content_type'] : ''); | 139 | $entry->setMimetype(isset($content['content_type']) ? $content['content_type'] : ''); |
84 | $entry->setReadingTime(Utils::getReadingTime($html)); | 140 | $entry->setReadingTime(Utils::getReadingTime($html)); |
85 | 141 | ||
@@ -88,85 +144,73 @@ class ContentProxy | |||
88 | $entry->setDomainName($domainName); | 144 | $entry->setDomainName($domainName); |
89 | } | 145 | } |
90 | 146 | ||
91 | if (isset($content['open_graph']['og_image']) && $content['open_graph']['og_image']) { | ||
92 | $entry->setPreviewPicture($content['open_graph']['og_image']); | ||
93 | } | ||
94 | |||
95 | // if content is an image define as a preview too | ||
96 | if (isset($content['content_type']) && in_array($this->mimeGuesser->guess($content['content_type']), ['jpeg', 'jpg', 'gif', 'png'], true)) { | ||
97 | $entry->setPreviewPicture($content['url']); | ||
98 | } | ||
99 | |||
100 | try { | 147 | try { |
101 | $this->tagger->tag($entry); | 148 | $this->tagger->tag($entry); |
102 | } catch (\Exception $e) { | 149 | } catch (\Exception $e) { |
103 | $this->logger->error('Error while trying to automatically tag an entry.', [ | 150 | $this->logger->error('Error while trying to automatically tag an entry.', [ |
104 | 'entry_url' => $url, | 151 | 'entry_url' => $content['url'], |
105 | 'error_msg' => $e->getMessage(), | 152 | 'error_msg' => $e->getMessage(), |
106 | ]); | 153 | ]); |
107 | } | 154 | } |
108 | |||
109 | return $entry; | ||
110 | } | 155 | } |
111 | 156 | ||
112 | /** | 157 | /** |
113 | * Assign some tags to an entry. | 158 | * Validate that the given content has at least a title, an html and a url. |
159 | * | ||
160 | * @param array $content | ||
114 | * | 161 | * |
115 | * @param Entry $entry | 162 | * @return bool true if valid otherwise false |
116 | * @param array|string $tags An array of tag or a string coma separated of tag | ||
117 | * @param array $entitiesReady Entities from the EntityManager which are persisted but not yet flushed | ||
118 | * It is mostly to fix duplicate tag on import @see http://stackoverflow.com/a/7879164/569101 | ||
119 | */ | 163 | */ |
120 | public function assignTagsToEntry(Entry $entry, $tags, array $entitiesReady = []) | 164 | private function validateContent(array $content) |
121 | { | 165 | { |
122 | if (!is_array($tags)) { | 166 | return !empty($content['title']) && !empty($content['html']) && !empty($content['url']); |
123 | $tags = explode(',', $tags); | 167 | } |
124 | } | ||
125 | |||
126 | // keeps only Tag entity from the "not yet flushed entities" | ||
127 | $tagsNotYetFlushed = []; | ||
128 | foreach ($entitiesReady as $entity) { | ||
129 | if ($entity instanceof Tag) { | ||
130 | $tagsNotYetFlushed[$entity->getLabel()] = $entity; | ||
131 | } | ||
132 | } | ||
133 | |||
134 | foreach ($tags as $label) { | ||
135 | $label = trim($label); | ||
136 | 168 | ||
137 | // avoid empty tag | 169 | /** |
138 | if (0 === strlen($label)) { | 170 | * Use a Symfony validator to ensure the language is well formatted. |
139 | continue; | 171 | * |
140 | } | 172 | * @param Entry $entry |
173 | * @param string $value Language to validate | ||
174 | */ | ||
175 | private function validateAndSetLanguage($entry, $value) | ||
176 | { | ||
177 | // some lang are defined as fr-FR, es-ES. | ||
178 | // replacing - by _ might increase language support | ||
179 | $value = str_replace('-', '_', $value); | ||
141 | 180 | ||
142 | if (isset($tagsNotYetFlushed[$label])) { | 181 | $errors = $this->validator->validate( |
143 | $tagEntity = $tagsNotYetFlushed[$label]; | 182 | $value, |
144 | } else { | 183 | (new LocaleConstraint()) |
145 | $tagEntity = $this->tagRepository->findOneByLabel($label); | 184 | ); |
146 | 185 | ||
147 | if (is_null($tagEntity)) { | 186 | if (0 === count($errors)) { |
148 | $tagEntity = new Tag(); | 187 | $entry->setLanguage($value); |
149 | $tagEntity->setLabel($label); | ||
150 | } | ||
151 | } | ||
152 | 188 | ||
153 | // only add the tag on the entry if the relation doesn't exist | 189 | return; |
154 | if (false === $entry->getTags()->contains($tagEntity)) { | ||
155 | $entry->addTag($tagEntity); | ||
156 | } | ||
157 | } | 190 | } |
191 | |||
192 | $this->logger->warning('Language validation failed. '.(string) $errors); | ||
158 | } | 193 | } |
159 | 194 | ||
160 | /** | 195 | /** |
161 | * Validate that the given content as enough value to be used | 196 | * Use a Symfony validator to ensure the preview picture is a real url. |
162 | * instead of fetch the content from the url. | ||
163 | * | ||
164 | * @param array $content | ||
165 | * | 197 | * |
166 | * @return bool true if valid otherwise false | 198 | * @param Entry $entry |
199 | * @param string $value URL to validate | ||
167 | */ | 200 | */ |
168 | private function validateContent(array $content) | 201 | private function validateAndSetPreviewPicture($entry, $value) |
169 | { | 202 | { |
170 | return isset($content['title']) && isset($content['html']) && isset($content['url']) && isset($content['language']) && isset($content['content_type']); | 203 | $errors = $this->validator->validate( |
204 | $value, | ||
205 | (new UrlConstraint()) | ||
206 | ); | ||
207 | |||
208 | if (0 === count($errors)) { | ||
209 | $entry->setPreviewPicture($value); | ||
210 | |||
211 | return; | ||
212 | } | ||
213 | |||
214 | $this->logger->warning('PreviewPicture validation failed. '.(string) $errors); | ||
171 | } | 215 | } |
172 | } | 216 | } |
diff --git a/src/Wallabag/CoreBundle/Helper/CryptoProxy.php b/src/Wallabag/CoreBundle/Helper/CryptoProxy.php new file mode 100644 index 00000000..e8b19cb9 --- /dev/null +++ b/src/Wallabag/CoreBundle/Helper/CryptoProxy.php | |||
@@ -0,0 +1,86 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\CoreBundle\Helper; | ||
4 | |||
5 | use Psr\Log\LoggerInterface; | ||
6 | use Defuse\Crypto\Key; | ||
7 | use Defuse\Crypto\Crypto; | ||
8 | use Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException; | ||
9 | |||
10 | /** | ||
11 | * This is a proxy to crypt and decrypt password used by SiteCredential entity. | ||
12 | * BTW, It might be re-use for sth else. | ||
13 | */ | ||
14 | class CryptoProxy | ||
15 | { | ||
16 | private $logger; | ||
17 | private $encryptionKey; | ||
18 | |||
19 | public function __construct($encryptionKeyPath, LoggerInterface $logger) | ||
20 | { | ||
21 | $this->logger = $logger; | ||
22 | |||
23 | if (!file_exists($encryptionKeyPath)) { | ||
24 | $key = Key::createNewRandomKey(); | ||
25 | |||
26 | file_put_contents($encryptionKeyPath, $key->saveToAsciiSafeString()); | ||
27 | chmod($encryptionKeyPath, 0600); | ||
28 | } | ||
29 | |||
30 | $this->encryptionKey = file_get_contents($encryptionKeyPath); | ||
31 | } | ||
32 | |||
33 | /** | ||
34 | * Ensure the given value will be crypted. | ||
35 | * | ||
36 | * @param string $secretValue Secret valye to crypt | ||
37 | * | ||
38 | * @return string | ||
39 | */ | ||
40 | public function crypt($secretValue) | ||
41 | { | ||
42 | $this->logger->debug('Crypto: crypting value: '.$this->mask($secretValue)); | ||
43 | |||
44 | return Crypto::encrypt($secretValue, $this->loadKey()); | ||
45 | } | ||
46 | |||
47 | /** | ||
48 | * Ensure the given crypted value will be decrypted. | ||
49 | * | ||
50 | * @param string $cryptedValue The value to be decrypted | ||
51 | * | ||
52 | * @return string | ||
53 | */ | ||
54 | public function decrypt($cryptedValue) | ||
55 | { | ||
56 | $this->logger->debug('Crypto: decrypting value: '.$this->mask($cryptedValue)); | ||
57 | |||
58 | try { | ||
59 | return Crypto::decrypt($cryptedValue, $this->loadKey()); | ||
60 | } catch (WrongKeyOrModifiedCiphertextException $e) { | ||
61 | throw new \RuntimeException('Decrypt fail: '.$e->getMessage()); | ||
62 | } | ||
63 | } | ||
64 | |||
65 | /** | ||
66 | * Load the private key. | ||
67 | * | ||
68 | * @return Key | ||
69 | */ | ||
70 | private function loadKey() | ||
71 | { | ||
72 | return Key::loadFromAsciiSafeString($this->encryptionKey); | ||
73 | } | ||
74 | |||
75 | /** | ||
76 | * Keep first and last character and put some stars in between. | ||
77 | * | ||
78 | * @param string $value Value to mask | ||
79 | * | ||
80 | * @return string | ||
81 | */ | ||
82 | private function mask($value) | ||
83 | { | ||
84 | return strlen($value) > 0 ? $value[0].'*****'.$value[strlen($value) - 1] : 'Empty value'; | ||
85 | } | ||
86 | } | ||
diff --git a/src/Wallabag/CoreBundle/Helper/DownloadImages.php b/src/Wallabag/CoreBundle/Helper/DownloadImages.php index 0d330d2a..ed888cdb 100644 --- a/src/Wallabag/CoreBundle/Helper/DownloadImages.php +++ b/src/Wallabag/CoreBundle/Helper/DownloadImages.php | |||
@@ -5,6 +5,7 @@ namespace Wallabag\CoreBundle\Helper; | |||
5 | use Psr\Log\LoggerInterface; | 5 | use Psr\Log\LoggerInterface; |
6 | use Symfony\Component\DomCrawler\Crawler; | 6 | use Symfony\Component\DomCrawler\Crawler; |
7 | use GuzzleHttp\Client; | 7 | use GuzzleHttp\Client; |
8 | use GuzzleHttp\Message\Response; | ||
8 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser; | 9 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser; |
9 | use Symfony\Component\Finder\Finder; | 10 | use Symfony\Component\Finder\Finder; |
10 | 11 | ||
@@ -54,7 +55,7 @@ class DownloadImages | |||
54 | $crawler = new Crawler($html); | 55 | $crawler = new Crawler($html); |
55 | $result = $crawler | 56 | $result = $crawler |
56 | ->filterXpath('//img') | 57 | ->filterXpath('//img') |
57 | ->extract(array('src')); | 58 | ->extract(['src']); |
58 | 59 | ||
59 | $relativePath = $this->getRelativePath($entryId); | 60 | $relativePath = $this->getRelativePath($entryId); |
60 | 61 | ||
@@ -66,6 +67,11 @@ class DownloadImages | |||
66 | continue; | 67 | continue; |
67 | } | 68 | } |
68 | 69 | ||
70 | // if image contains "&" and we can't find it in the html it might be because it's encoded as & | ||
71 | if (false !== stripos($image, '&') && false === stripos($html, $image)) { | ||
72 | $image = str_replace('&', '&', $image); | ||
73 | } | ||
74 | |||
69 | $html = str_replace($image, $imagePath, $html); | 75 | $html = str_replace($image, $imagePath, $html); |
70 | } | 76 | } |
71 | 77 | ||
@@ -111,13 +117,11 @@ class DownloadImages | |||
111 | return false; | 117 | return false; |
112 | } | 118 | } |
113 | 119 | ||
114 | $ext = $this->mimeGuesser->guess($res->getHeader('content-type')); | 120 | $ext = $this->getExtensionFromResponse($res, $imagePath); |
115 | $this->logger->debug('DownloadImages: Checking extension', ['ext' => $ext, 'header' => $res->getHeader('content-type')]); | 121 | if (false === $res) { |
116 | if (!in_array($ext, ['jpeg', 'jpg', 'gif', 'png'], true)) { | ||
117 | $this->logger->error('DownloadImages: Processed image with not allowed extension. Skipping '.$imagePath); | ||
118 | |||
119 | return false; | 122 | return false; |
120 | } | 123 | } |
124 | |||
121 | $hashImage = hash('crc32', $absolutePath); | 125 | $hashImage = hash('crc32', $absolutePath); |
122 | $localPath = $folderPath.'/'.$hashImage.'.'.$ext; | 126 | $localPath = $folderPath.'/'.$hashImage.'.'.$ext; |
123 | 127 | ||
@@ -232,4 +236,45 @@ class DownloadImages | |||
232 | 236 | ||
233 | return false; | 237 | return false; |
234 | } | 238 | } |
239 | |||
240 | /** | ||
241 | * Retrieve and validate the extension from the response of the url of the image. | ||
242 | * | ||
243 | * @param Response $res Guzzle Response | ||
244 | * @param string $imagePath Path from the src image from the content (used for log only) | ||
245 | * | ||
246 | * @return string|false Extension name or false if validation failed | ||
247 | */ | ||
248 | private function getExtensionFromResponse(Response $res, $imagePath) | ||
249 | { | ||
250 | $ext = $this->mimeGuesser->guess($res->getHeader('content-type')); | ||
251 | $this->logger->debug('DownloadImages: Checking extension', ['ext' => $ext, 'header' => $res->getHeader('content-type')]); | ||
252 | |||
253 | // ok header doesn't have the extension, try a different way | ||
254 | if (empty($ext)) { | ||
255 | $types = [ | ||
256 | 'jpeg' => "\xFF\xD8\xFF", | ||
257 | 'gif' => 'GIF', | ||
258 | 'png' => "\x89\x50\x4e\x47\x0d\x0a", | ||
259 | ]; | ||
260 | $bytes = substr((string) $res->getBody(), 0, 8); | ||
261 | |||
262 | foreach ($types as $type => $header) { | ||
263 | if (0 === strpos($bytes, $header)) { | ||
264 | $ext = $type; | ||
265 | break; | ||
266 | } | ||
267 | } | ||
268 | |||
269 | $this->logger->debug('DownloadImages: Checking extension (alternative)', ['ext' => $ext]); | ||
270 | } | ||
271 | |||
272 | if (!in_array($ext, ['jpeg', 'jpg', 'gif', 'png'], true)) { | ||
273 | $this->logger->error('DownloadImages: Processed image with not allowed extension. Skipping: '.$imagePath); | ||
274 | |||
275 | return false; | ||
276 | } | ||
277 | |||
278 | return $ext; | ||
279 | } | ||
235 | } | 280 | } |
diff --git a/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php b/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php index 1ac8feb1..43f5b119 100644 --- a/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php +++ b/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php | |||
@@ -13,8 +13,8 @@ use Psr\Log\LoggerInterface; | |||
13 | */ | 13 | */ |
14 | class HttpClientFactory | 14 | class HttpClientFactory |
15 | { | 15 | { |
16 | /** @var \GuzzleHttp\Event\SubscriberInterface */ | 16 | /** @var [\GuzzleHttp\Event\SubscriberInterface] */ |
17 | private $authenticatorSubscriber; | 17 | private $subscribers = []; |
18 | 18 | ||
19 | /** @var \GuzzleHttp\Cookie\CookieJar */ | 19 | /** @var \GuzzleHttp\Cookie\CookieJar */ |
20 | private $cookieJar; | 20 | private $cookieJar; |
@@ -25,14 +25,12 @@ class HttpClientFactory | |||
25 | /** | 25 | /** |
26 | * HttpClientFactory constructor. | 26 | * HttpClientFactory constructor. |
27 | * | 27 | * |
28 | * @param \GuzzleHttp\Event\SubscriberInterface $authenticatorSubscriber | 28 | * @param \GuzzleHttp\Cookie\CookieJar $cookieJar |
29 | * @param \GuzzleHttp\Cookie\CookieJar $cookieJar | 29 | * @param string $restrictedAccess This param is a kind of boolean. Values: 0 or 1 |
30 | * @param string $restrictedAccess this param is a kind of boolean. Values: 0 or 1 | 30 | * @param LoggerInterface $logger |
31 | * @param LoggerInterface $logger | ||
32 | */ | 31 | */ |
33 | public function __construct(SubscriberInterface $authenticatorSubscriber, CookieJar $cookieJar, $restrictedAccess, LoggerInterface $logger) | 32 | public function __construct(CookieJar $cookieJar, $restrictedAccess, LoggerInterface $logger) |
34 | { | 33 | { |
35 | $this->authenticatorSubscriber = $authenticatorSubscriber; | ||
36 | $this->cookieJar = $cookieJar; | 34 | $this->cookieJar = $cookieJar; |
37 | $this->restrictedAccess = $restrictedAccess; | 35 | $this->restrictedAccess = $restrictedAccess; |
38 | $this->logger = $logger; | 36 | $this->logger = $logger; |
@@ -53,8 +51,21 @@ class HttpClientFactory | |||
53 | $this->cookieJar->clear(); | 51 | $this->cookieJar->clear(); |
54 | // need to set the (shared) cookie jar | 52 | // need to set the (shared) cookie jar |
55 | $client = new Client(['handler' => new SafeCurlHandler(), 'defaults' => ['cookies' => $this->cookieJar]]); | 53 | $client = new Client(['handler' => new SafeCurlHandler(), 'defaults' => ['cookies' => $this->cookieJar]]); |
56 | $client->getEmitter()->attach($this->authenticatorSubscriber); | 54 | |
55 | foreach ($this->subscribers as $subscriber) { | ||
56 | $client->getEmitter()->attach($subscriber); | ||
57 | } | ||
57 | 58 | ||
58 | return $client; | 59 | return $client; |
59 | } | 60 | } |
61 | |||
62 | /** | ||
63 | * Adds a subscriber to the HTTP client. | ||
64 | * | ||
65 | * @param SubscriberInterface $subscriber | ||
66 | */ | ||
67 | public function addSubscriber(SubscriberInterface $subscriber) | ||
68 | { | ||
69 | $this->subscribers[] = $subscriber; | ||
70 | } | ||
60 | } | 71 | } |
diff --git a/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php b/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php index 7d3798b9..231a0b52 100644 --- a/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php +++ b/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php | |||
@@ -4,6 +4,7 @@ namespace Wallabag\CoreBundle\Helper; | |||
4 | 4 | ||
5 | use Pagerfanta\Adapter\AdapterInterface; | 5 | use Pagerfanta\Adapter\AdapterInterface; |
6 | use Pagerfanta\Pagerfanta; | 6 | use Pagerfanta\Pagerfanta; |
7 | use Wallabag\UserBundle\Entity\User; | ||
7 | use Symfony\Component\Routing\Router; | 8 | use Symfony\Component\Routing\Router; |
8 | use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; | 9 | use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; |
9 | 10 | ||
@@ -20,16 +21,18 @@ class PreparePagerForEntries | |||
20 | 21 | ||
21 | /** | 22 | /** |
22 | * @param AdapterInterface $adapter | 23 | * @param AdapterInterface $adapter |
23 | * @param int $page | 24 | * @param User $user If user isn't logged in, we can force it (like for rss) |
24 | * | 25 | * |
25 | * @return null|Pagerfanta | 26 | * @return null|Pagerfanta |
26 | */ | 27 | */ |
27 | public function prepare(AdapterInterface $adapter, $page = 1) | 28 | public function prepare(AdapterInterface $adapter, User $user = null) |
28 | { | 29 | { |
29 | $user = $this->tokenStorage->getToken() ? $this->tokenStorage->getToken()->getUser() : null; | 30 | if (null === $user) { |
31 | $user = $this->tokenStorage->getToken() ? $this->tokenStorage->getToken()->getUser() : null; | ||
32 | } | ||
30 | 33 | ||
31 | if (null === $user || !is_object($user)) { | 34 | if (null === $user || !is_object($user)) { |
32 | return null; | 35 | return; |
33 | } | 36 | } |
34 | 37 | ||
35 | $entries = new Pagerfanta($adapter); | 38 | $entries = new Pagerfanta($adapter); |
diff --git a/src/Wallabag/CoreBundle/Helper/Redirect.php b/src/Wallabag/CoreBundle/Helper/Redirect.php index f78b7fe0..abc84d08 100644 --- a/src/Wallabag/CoreBundle/Helper/Redirect.php +++ b/src/Wallabag/CoreBundle/Helper/Redirect.php | |||
@@ -21,12 +21,13 @@ class Redirect | |||
21 | } | 21 | } |
22 | 22 | ||
23 | /** | 23 | /** |
24 | * @param string $url URL to redirect | 24 | * @param string $url URL to redirect |
25 | * @param string $fallback Fallback URL if $url is null | 25 | * @param string $fallback Fallback URL if $url is null |
26 | * @param bool $ignoreActionMarkAsRead Ignore configured action when mark as read | ||
26 | * | 27 | * |
27 | * @return string | 28 | * @return string |
28 | */ | 29 | */ |
29 | public function to($url, $fallback = '') | 30 | public function to($url, $fallback = '', $ignoreActionMarkAsRead = false) |
30 | { | 31 | { |
31 | $user = $this->tokenStorage->getToken() ? $this->tokenStorage->getToken()->getUser() : null; | 32 | $user = $this->tokenStorage->getToken() ? $this->tokenStorage->getToken()->getUser() : null; |
32 | 33 | ||
@@ -34,7 +35,8 @@ class Redirect | |||
34 | return $url; | 35 | return $url; |
35 | } | 36 | } |
36 | 37 | ||
37 | if (Config::REDIRECT_TO_HOMEPAGE === $user->getConfig()->getActionMarkAsRead()) { | 38 | if (!$ignoreActionMarkAsRead && |
39 | Config::REDIRECT_TO_HOMEPAGE === $user->getConfig()->getActionMarkAsRead()) { | ||
38 | return $this->router->generate('homepage'); | 40 | return $this->router->generate('homepage'); |
39 | } | 41 | } |
40 | 42 | ||
diff --git a/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php b/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php index b490e209..509d0dec 100644 --- a/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php +++ b/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php | |||
@@ -8,18 +8,21 @@ use Wallabag\CoreBundle\Entity\Tag; | |||
8 | use Wallabag\CoreBundle\Repository\EntryRepository; | 8 | use Wallabag\CoreBundle\Repository\EntryRepository; |
9 | use Wallabag\CoreBundle\Repository\TagRepository; | 9 | use Wallabag\CoreBundle\Repository\TagRepository; |
10 | use Wallabag\UserBundle\Entity\User; | 10 | use Wallabag\UserBundle\Entity\User; |
11 | use Psr\Log\LoggerInterface; | ||
11 | 12 | ||
12 | class RuleBasedTagger | 13 | class RuleBasedTagger |
13 | { | 14 | { |
14 | private $rulerz; | 15 | private $rulerz; |
15 | private $tagRepository; | 16 | private $tagRepository; |
16 | private $entryRepository; | 17 | private $entryRepository; |
18 | private $logger; | ||
17 | 19 | ||
18 | public function __construct(RulerZ $rulerz, TagRepository $tagRepository, EntryRepository $entryRepository) | 20 | public function __construct(RulerZ $rulerz, TagRepository $tagRepository, EntryRepository $entryRepository, LoggerInterface $logger) |
19 | { | 21 | { |
20 | $this->rulerz = $rulerz; | 22 | $this->rulerz = $rulerz; |
21 | $this->tagRepository = $tagRepository; | 23 | $this->tagRepository = $tagRepository; |
22 | $this->entryRepository = $entryRepository; | 24 | $this->entryRepository = $entryRepository; |
25 | $this->logger = $logger; | ||
23 | } | 26 | } |
24 | 27 | ||
25 | /** | 28 | /** |
@@ -36,6 +39,11 @@ class RuleBasedTagger | |||
36 | continue; | 39 | continue; |
37 | } | 40 | } |
38 | 41 | ||
42 | $this->logger->info('Matching rule.', [ | ||
43 | 'rule' => $rule->getRule(), | ||
44 | 'tags' => $rule->getTags(), | ||
45 | ]); | ||
46 | |||
39 | foreach ($rule->getTags() as $label) { | 47 | foreach ($rule->getTags() as $label) { |
40 | $tag = $this->getTag($label); | 48 | $tag = $this->getTag($label); |
41 | 49 | ||
diff --git a/src/Wallabag/CoreBundle/Helper/TagsAssigner.php b/src/Wallabag/CoreBundle/Helper/TagsAssigner.php new file mode 100644 index 00000000..a2fb0b9a --- /dev/null +++ b/src/Wallabag/CoreBundle/Helper/TagsAssigner.php | |||
@@ -0,0 +1,75 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\CoreBundle\Helper; | ||
4 | |||
5 | use Wallabag\CoreBundle\Entity\Entry; | ||
6 | use Wallabag\CoreBundle\Entity\Tag; | ||
7 | use Wallabag\CoreBundle\Repository\TagRepository; | ||
8 | |||
9 | class TagsAssigner | ||
10 | { | ||
11 | /** | ||
12 | * @var TagRepository | ||
13 | */ | ||
14 | protected $tagRepository; | ||
15 | |||
16 | public function __construct(TagRepository $tagRepository) | ||
17 | { | ||
18 | $this->tagRepository = $tagRepository; | ||
19 | } | ||
20 | |||
21 | /** | ||
22 | * Assign some tags to an entry. | ||
23 | * | ||
24 | * @param Entry $entry | ||
25 | * @param array|string $tags An array of tag or a string coma separated of tag | ||
26 | * @param array $entitiesReady Entities from the EntityManager which are persisted but not yet flushed | ||
27 | * It is mostly to fix duplicate tag on import @see http://stackoverflow.com/a/7879164/569101 | ||
28 | * | ||
29 | * @return Tag[] | ||
30 | */ | ||
31 | public function assignTagsToEntry(Entry $entry, $tags, array $entitiesReady = []) | ||
32 | { | ||
33 | $tagsEntities = []; | ||
34 | |||
35 | if (!is_array($tags)) { | ||
36 | $tags = explode(',', $tags); | ||
37 | } | ||
38 | |||
39 | // keeps only Tag entity from the "not yet flushed entities" | ||
40 | $tagsNotYetFlushed = []; | ||
41 | foreach ($entitiesReady as $entity) { | ||
42 | if ($entity instanceof Tag) { | ||
43 | $tagsNotYetFlushed[$entity->getLabel()] = $entity; | ||
44 | } | ||
45 | } | ||
46 | |||
47 | foreach ($tags as $label) { | ||
48 | $label = trim($label); | ||
49 | |||
50 | // avoid empty tag | ||
51 | if (0 === strlen($label)) { | ||
52 | continue; | ||
53 | } | ||
54 | |||
55 | if (isset($tagsNotYetFlushed[$label])) { | ||
56 | $tagEntity = $tagsNotYetFlushed[$label]; | ||
57 | } else { | ||
58 | $tagEntity = $this->tagRepository->findOneByLabel($label); | ||
59 | |||
60 | if (null === $tagEntity) { | ||
61 | $tagEntity = new Tag(); | ||
62 | $tagEntity->setLabel($label); | ||
63 | } | ||
64 | } | ||
65 | |||
66 | // only add the tag on the entry if the relation doesn't exist | ||
67 | if (false === $entry->getTags()->contains($tagEntity)) { | ||
68 | $entry->addTag($tagEntity); | ||
69 | $tagsEntities[] = $tagEntity; | ||
70 | } | ||
71 | } | ||
72 | |||
73 | return $tagsEntities; | ||
74 | } | ||
75 | } | ||