diff options
Diffstat (limited to 'src/Wallabag/CoreBundle/Helper')
-rw-r--r-- | src/Wallabag/CoreBundle/Helper/ContentProxy.php | 238 | ||||
-rw-r--r-- | src/Wallabag/CoreBundle/Helper/CryptoProxy.php | 86 | ||||
-rw-r--r-- | src/Wallabag/CoreBundle/Helper/DetectActiveTheme.php | 2 | ||||
-rw-r--r-- | src/Wallabag/CoreBundle/Helper/DownloadImages.php | 97 | ||||
-rw-r--r-- | src/Wallabag/CoreBundle/Helper/EntityTimestampsTrait.php | 24 | ||||
-rw-r--r-- | src/Wallabag/CoreBundle/Helper/EntriesExport.php | 107 | ||||
-rw-r--r-- | src/Wallabag/CoreBundle/Helper/HttpClientFactory.php | 31 | ||||
-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 |
11 files changed, 518 insertions, 173 deletions
diff --git a/src/Wallabag/CoreBundle/Helper/ContentProxy.php b/src/Wallabag/CoreBundle/Helper/ContentProxy.php index f222dd88..854acb6a 100644 --- a/src/Wallabag/CoreBundle/Helper/ContentProxy.php +++ b/src/Wallabag/CoreBundle/Helper/ContentProxy.php | |||
@@ -4,11 +4,12 @@ namespace Wallabag\CoreBundle\Helper; | |||
4 | 4 | ||
5 | use Graby\Graby; | 5 | use Graby\Graby; |
6 | use Psr\Log\LoggerInterface; | 6 | use Psr\Log\LoggerInterface; |
7 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser; | ||
8 | use Symfony\Component\Validator\Constraints\Locale as LocaleConstraint; | ||
9 | use Symfony\Component\Validator\Constraints\Url as UrlConstraint; | ||
10 | use Symfony\Component\Validator\Validator\ValidatorInterface; | ||
7 | use Wallabag\CoreBundle\Entity\Entry; | 11 | use Wallabag\CoreBundle\Entity\Entry; |
8 | use Wallabag\CoreBundle\Entity\Tag; | ||
9 | use Wallabag\CoreBundle\Tools\Utils; | 12 | use Wallabag\CoreBundle\Tools\Utils; |
10 | use Wallabag\CoreBundle\Repository\TagRepository; | ||
11 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser; | ||
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 | * We can also force the content, in case of an import from the v1 for example, so the function won't | ||
41 | * fetch the content from the website but rather use information given with the $content parameter. | ||
42 | * | 40 | * |
43 | * @param Entry $entry Entry to update | 41 | * @param Entry $entry Entry to update |
44 | * @param string $url Url to grab content for | 42 | * @param string $url Url of the content |
45 | * @param array $content An array with AT LEAST keys title, html, url, language & content_type to skip the fetchContent from the url | 43 | * @param array $content Array with content provided for import with AT LEAST keys title, html, url to skip the fetchContent from the url |
46 | * | 44 | * @param bool $disableContentUpdate Whether to skip trying to fetch content using Graby |
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,107 +59,169 @@ class ContentProxy | |||
59 | } | 59 | } |
60 | } | 60 | } |
61 | 61 | ||
62 | $title = $content['title']; | 62 | // be sure to keep the url in case of error |
63 | if (!$title && isset($content['open_graph']['og_title'])) { | 63 | // so we'll be able to refetch it in the future |
64 | $title = $content['open_graph']['og_title']; | 64 | $content['url'] = !empty($content['url']) ? $content['url'] : $url; |
65 | } | ||
66 | 65 | ||
67 | $html = $content['html']; | 66 | $this->stockEntry($entry, $content); |
68 | if (false === $html) { | 67 | } |
69 | $html = $this->fetchingErrorMessage; | ||
70 | 68 | ||
71 | if (isset($content['open_graph']['og_description'])) { | 69 | /** |
72 | $html .= '<p><i>But we found a short description: </i></p>'; | 70 | * Use a Symfony validator to ensure the language is well formatted. |
73 | $html .= $content['open_graph']['og_description']; | 71 | * |
74 | } | 72 | * @param Entry $entry |
75 | } | 73 | * @param string $value Language to validate and save |
74 | */ | ||
75 | public function updateLanguage(Entry $entry, $value) | ||
76 | { | ||
77 | // some lang are defined as fr-FR, es-ES. | ||
78 | // replacing - by _ might increase language support | ||
79 | $value = str_replace('-', '_', $value); | ||
76 | 80 | ||
77 | $entry->setUrl($content['url'] ?: $url); | 81 | $errors = $this->validator->validate( |
78 | $entry->setTitle($title); | 82 | $value, |
79 | $entry->setContent($html); | 83 | (new LocaleConstraint()) |
80 | $entry->setHttpStatus(isset($content['status']) ? $content['status'] : ''); | 84 | ); |
81 | 85 | ||
82 | $entry->setLanguage(isset($content['language']) ? $content['language'] : ''); | 86 | if (0 === count($errors)) { |
83 | $entry->setMimetype(isset($content['content_type']) ? $content['content_type'] : ''); | 87 | $entry->setLanguage($value); |
84 | $entry->setReadingTime(Utils::getReadingTime($html)); | ||
85 | 88 | ||
86 | $domainName = parse_url($entry->getUrl(), PHP_URL_HOST); | 89 | return; |
87 | if (false !== $domainName) { | ||
88 | $entry->setDomainName($domainName); | ||
89 | } | 90 | } |
90 | 91 | ||
91 | if (isset($content['open_graph']['og_image']) && $content['open_graph']['og_image']) { | 92 | $this->logger->warning('Language validation failed. ' . (string) $errors); |
92 | $entry->setPreviewPicture($content['open_graph']['og_image']); | 93 | } |
94 | |||
95 | /** | ||
96 | * Use a Symfony validator to ensure the preview picture is a real url. | ||
97 | * | ||
98 | * @param Entry $entry | ||
99 | * @param string $value URL to validate and save | ||
100 | */ | ||
101 | public function updatePreviewPicture(Entry $entry, $value) | ||
102 | { | ||
103 | $errors = $this->validator->validate( | ||
104 | $value, | ||
105 | (new UrlConstraint()) | ||
106 | ); | ||
107 | |||
108 | if (0 === count($errors)) { | ||
109 | $entry->setPreviewPicture($value); | ||
110 | |||
111 | return; | ||
93 | } | 112 | } |
94 | 113 | ||
95 | // if content is an image define as a preview too | 114 | $this->logger->warning('PreviewPicture validation failed. ' . (string) $errors); |
96 | if (isset($content['content_type']) && in_array($this->mimeGuesser->guess($content['content_type']), ['jpeg', 'jpg', 'gif', 'png'], true)) { | 115 | } |
97 | $entry->setPreviewPicture($content['url']); | 116 | |
117 | /** | ||
118 | * Update date. | ||
119 | * | ||
120 | * @param Entry $entry | ||
121 | * @param string $value Date to validate and save | ||
122 | */ | ||
123 | public function updatePublishedAt(Entry $entry, $value) | ||
124 | { | ||
125 | $date = $value; | ||
126 | |||
127 | // is it a timestamp? | ||
128 | if (false !== filter_var($date, FILTER_VALIDATE_INT)) { | ||
129 | $date = '@' . $date; | ||
98 | } | 130 | } |
99 | 131 | ||
100 | try { | 132 | try { |
101 | $this->tagger->tag($entry); | 133 | // is it already a DateTime? |
134 | // (it's inside the try/catch in case of fail to be parse time string) | ||
135 | if (!$date instanceof \DateTime) { | ||
136 | $date = new \DateTime($date); | ||
137 | } | ||
138 | |||
139 | $entry->setPublishedAt($date); | ||
102 | } catch (\Exception $e) { | 140 | } catch (\Exception $e) { |
103 | $this->logger->error('Error while trying to automatically tag an entry.', [ | 141 | $this->logger->warning('Error while defining date', ['e' => $e, 'url' => $entry->getUrl(), 'date' => $value]); |
104 | 'entry_url' => $url, | ||
105 | 'error_msg' => $e->getMessage(), | ||
106 | ]); | ||
107 | } | 142 | } |
108 | |||
109 | return $entry; | ||
110 | } | 143 | } |
111 | 144 | ||
112 | /** | 145 | /** |
113 | * Assign some tags to an entry. | 146 | * Stock entry with fetched or imported content. |
147 | * Will fall back to OpenGraph data if available. | ||
114 | * | 148 | * |
115 | * @param Entry $entry | 149 | * @param Entry $entry Entry to stock |
116 | * @param array|string $tags An array of tag or a string coma separated of tag | 150 | * @param array $content Array with at least title, url & html |
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 | */ | 151 | */ |
120 | public function assignTagsToEntry(Entry $entry, $tags, array $entitiesReady = []) | 152 | private function stockEntry(Entry $entry, array $content) |
121 | { | 153 | { |
122 | if (!is_array($tags)) { | 154 | $entry->setUrl($content['url']); |
123 | $tags = explode(',', $tags); | 155 | |
156 | $domainName = parse_url($entry->getUrl(), PHP_URL_HOST); | ||
157 | if (false !== $domainName) { | ||
158 | $entry->setDomainName($domainName); | ||
124 | } | 159 | } |
125 | 160 | ||
126 | // keeps only Tag entity from the "not yet flushed entities" | 161 | if (!empty($content['title'])) { |
127 | $tagsNotYetFlushed = []; | 162 | $entry->setTitle($content['title']); |
128 | foreach ($entitiesReady as $entity) { | 163 | } elseif (!empty($content['open_graph']['og_title'])) { |
129 | if ($entity instanceof Tag) { | 164 | $entry->setTitle($content['open_graph']['og_title']); |
130 | $tagsNotYetFlushed[$entity->getLabel()] = $entity; | ||
131 | } | ||
132 | } | 165 | } |
133 | 166 | ||
134 | foreach ($tags as $label) { | 167 | $html = $content['html']; |
135 | $label = trim($label); | 168 | if (false === $html) { |
169 | $html = $this->fetchingErrorMessage; | ||
136 | 170 | ||
137 | // avoid empty tag | 171 | if (!empty($content['open_graph']['og_description'])) { |
138 | if (0 === strlen($label)) { | 172 | $html .= '<p><i>But we found a short description: </i></p>'; |
139 | continue; | 173 | $html .= $content['open_graph']['og_description']; |
140 | } | 174 | } |
175 | } | ||
141 | 176 | ||
142 | if (isset($tagsNotYetFlushed[$label])) { | 177 | $entry->setContent($html); |
143 | $tagEntity = $tagsNotYetFlushed[$label]; | 178 | $entry->setReadingTime(Utils::getReadingTime($html)); |
144 | } else { | ||
145 | $tagEntity = $this->tagRepository->findOneByLabel($label); | ||
146 | 179 | ||
147 | if (is_null($tagEntity)) { | 180 | if (!empty($content['status'])) { |
148 | $tagEntity = new Tag(); | 181 | $entry->setHttpStatus($content['status']); |
149 | $tagEntity->setLabel($label); | 182 | } |
150 | } | ||
151 | } | ||
152 | 183 | ||
153 | // only add the tag on the entry if the relation doesn't exist | 184 | if (!empty($content['authors']) && is_array($content['authors'])) { |
154 | if (false === $entry->getTags()->contains($tagEntity)) { | 185 | $entry->setPublishedBy($content['authors']); |
155 | $entry->addTag($tagEntity); | 186 | } |
156 | } | 187 | |
188 | if (!empty($content['all_headers'])) { | ||
189 | $entry->setHeaders($content['all_headers']); | ||
190 | } | ||
191 | |||
192 | if (!empty($content['date'])) { | ||
193 | $this->updatePublishedAt($entry, $content['date']); | ||
194 | } | ||
195 | |||
196 | if (!empty($content['language'])) { | ||
197 | $this->updateLanguage($entry, $content['language']); | ||
198 | } | ||
199 | |||
200 | if (!empty($content['open_graph']['og_image'])) { | ||
201 | $this->updatePreviewPicture($entry, $content['open_graph']['og_image']); | ||
202 | } | ||
203 | |||
204 | // if content is an image, define it as a preview too | ||
205 | if (!empty($content['content_type']) && in_array($this->mimeGuesser->guess($content['content_type']), ['jpeg', 'jpg', 'gif', 'png'], true)) { | ||
206 | $this->updatePreviewPicture($entry, $content['url']); | ||
207 | } | ||
208 | |||
209 | if (!empty($content['content_type'])) { | ||
210 | $entry->setMimetype($content['content_type']); | ||
211 | } | ||
212 | |||
213 | try { | ||
214 | $this->tagger->tag($entry); | ||
215 | } catch (\Exception $e) { | ||
216 | $this->logger->error('Error while trying to automatically tag an entry.', [ | ||
217 | 'entry_url' => $content['url'], | ||
218 | 'error_msg' => $e->getMessage(), | ||
219 | ]); | ||
157 | } | 220 | } |
158 | } | 221 | } |
159 | 222 | ||
160 | /** | 223 | /** |
161 | * Validate that the given content as enough value to be used | 224 | * Validate that the given content has at least a title, an html and a url. |
162 | * instead of fetch the content from the url. | ||
163 | * | 225 | * |
164 | * @param array $content | 226 | * @param array $content |
165 | * | 227 | * |
@@ -167,6 +229,6 @@ class ContentProxy | |||
167 | */ | 229 | */ |
168 | private function validateContent(array $content) | 230 | private function validateContent(array $content) |
169 | { | 231 | { |
170 | return isset($content['title']) && isset($content['html']) && isset($content['url']) && isset($content['language']) && isset($content['content_type']); | 232 | return !empty($content['title']) && !empty($content['html']) && !empty($content['url']); |
171 | } | 233 | } |
172 | } | 234 | } |
diff --git a/src/Wallabag/CoreBundle/Helper/CryptoProxy.php b/src/Wallabag/CoreBundle/Helper/CryptoProxy.php new file mode 100644 index 00000000..7d8c9888 --- /dev/null +++ b/src/Wallabag/CoreBundle/Helper/CryptoProxy.php | |||
@@ -0,0 +1,86 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\CoreBundle\Helper; | ||
4 | |||
5 | use Defuse\Crypto\Crypto; | ||
6 | use Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException; | ||
7 | use Defuse\Crypto\Key; | ||
8 | use Psr\Log\LoggerInterface; | ||
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/DetectActiveTheme.php b/src/Wallabag/CoreBundle/Helper/DetectActiveTheme.php index 23e98042..9f90ee3e 100644 --- a/src/Wallabag/CoreBundle/Helper/DetectActiveTheme.php +++ b/src/Wallabag/CoreBundle/Helper/DetectActiveTheme.php | |||
@@ -44,7 +44,7 @@ class DetectActiveTheme implements DeviceDetectionInterface | |||
44 | { | 44 | { |
45 | $token = $this->tokenStorage->getToken(); | 45 | $token = $this->tokenStorage->getToken(); |
46 | 46 | ||
47 | if (is_null($token)) { | 47 | if (null === $token) { |
48 | return $this->defaultTheme; | 48 | return $this->defaultTheme; |
49 | } | 49 | } |
50 | 50 | ||
diff --git a/src/Wallabag/CoreBundle/Helper/DownloadImages.php b/src/Wallabag/CoreBundle/Helper/DownloadImages.php index 0d330d2a..252ba57c 100644 --- a/src/Wallabag/CoreBundle/Helper/DownloadImages.php +++ b/src/Wallabag/CoreBundle/Helper/DownloadImages.php | |||
@@ -2,11 +2,12 @@ | |||
2 | 2 | ||
3 | namespace Wallabag\CoreBundle\Helper; | 3 | namespace Wallabag\CoreBundle\Helper; |
4 | 4 | ||
5 | use GuzzleHttp\Client; | ||
6 | use GuzzleHttp\Message\Response; | ||
5 | use Psr\Log\LoggerInterface; | 7 | use Psr\Log\LoggerInterface; |
6 | use Symfony\Component\DomCrawler\Crawler; | 8 | use Symfony\Component\DomCrawler\Crawler; |
7 | use GuzzleHttp\Client; | ||
8 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser; | ||
9 | use Symfony\Component\Finder\Finder; | 9 | use Symfony\Component\Finder\Finder; |
10 | use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser; | ||
10 | 11 | ||
11 | class DownloadImages | 12 | class DownloadImages |
12 | { | 13 | { |
@@ -30,17 +31,6 @@ class DownloadImages | |||
30 | } | 31 | } |
31 | 32 | ||
32 | /** | 33 | /** |
33 | * Setup base folder where all images are going to be saved. | ||
34 | */ | ||
35 | private function setFolder() | ||
36 | { | ||
37 | // if folder doesn't exist, attempt to create one and store the folder name in property $folder | ||
38 | if (!file_exists($this->baseFolder)) { | ||
39 | mkdir($this->baseFolder, 0755, true); | ||
40 | } | ||
41 | } | ||
42 | |||
43 | /** | ||
44 | * Process the html and extract image from it, save them to local and return the updated html. | 34 | * Process the html and extract image from it, save them to local and return the updated html. |
45 | * | 35 | * |
46 | * @param int $entryId ID of the entry | 36 | * @param int $entryId ID of the entry |
@@ -54,7 +44,7 @@ class DownloadImages | |||
54 | $crawler = new Crawler($html); | 44 | $crawler = new Crawler($html); |
55 | $result = $crawler | 45 | $result = $crawler |
56 | ->filterXpath('//img') | 46 | ->filterXpath('//img') |
57 | ->extract(array('src')); | 47 | ->extract(['src']); |
58 | 48 | ||
59 | $relativePath = $this->getRelativePath($entryId); | 49 | $relativePath = $this->getRelativePath($entryId); |
60 | 50 | ||
@@ -66,6 +56,11 @@ class DownloadImages | |||
66 | continue; | 56 | continue; |
67 | } | 57 | } |
68 | 58 | ||
59 | // if image contains "&" and we can't find it in the html it might be because it's encoded as & | ||
60 | if (false !== stripos($image, '&') && false === stripos($html, $image)) { | ||
61 | $image = str_replace('&', '&', $image); | ||
62 | } | ||
63 | |||
69 | $html = str_replace($image, $imagePath, $html); | 64 | $html = str_replace($image, $imagePath, $html); |
70 | } | 65 | } |
71 | 66 | ||
@@ -91,9 +86,9 @@ class DownloadImages | |||
91 | $relativePath = $this->getRelativePath($entryId); | 86 | $relativePath = $this->getRelativePath($entryId); |
92 | } | 87 | } |
93 | 88 | ||
94 | $this->logger->debug('DownloadImages: working on image: '.$imagePath); | 89 | $this->logger->debug('DownloadImages: working on image: ' . $imagePath); |
95 | 90 | ||
96 | $folderPath = $this->baseFolder.'/'.$relativePath; | 91 | $folderPath = $this->baseFolder . '/' . $relativePath; |
97 | 92 | ||
98 | // build image path | 93 | // build image path |
99 | $absolutePath = $this->getAbsoluteLink($url, $imagePath); | 94 | $absolutePath = $this->getAbsoluteLink($url, $imagePath); |
@@ -111,15 +106,13 @@ class DownloadImages | |||
111 | return false; | 106 | return false; |
112 | } | 107 | } |
113 | 108 | ||
114 | $ext = $this->mimeGuesser->guess($res->getHeader('content-type')); | 109 | $ext = $this->getExtensionFromResponse($res, $imagePath); |
115 | $this->logger->debug('DownloadImages: Checking extension', ['ext' => $ext, 'header' => $res->getHeader('content-type')]); | 110 | 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; | 111 | return false; |
120 | } | 112 | } |
113 | |||
121 | $hashImage = hash('crc32', $absolutePath); | 114 | $hashImage = hash('crc32', $absolutePath); |
122 | $localPath = $folderPath.'/'.$hashImage.'.'.$ext; | 115 | $localPath = $folderPath . '/' . $hashImage . '.' . $ext; |
123 | 116 | ||
124 | try { | 117 | try { |
125 | $im = imagecreatefromstring($res->getBody()); | 118 | $im = imagecreatefromstring($res->getBody()); |
@@ -152,7 +145,7 @@ class DownloadImages | |||
152 | 145 | ||
153 | imagedestroy($im); | 146 | imagedestroy($im); |
154 | 147 | ||
155 | return $this->wallabagUrl.'/assets/images/'.$relativePath.'/'.$hashImage.'.'.$ext; | 148 | return $this->wallabagUrl . '/assets/images/' . $relativePath . '/' . $hashImage . '.' . $ext; |
156 | } | 149 | } |
157 | 150 | ||
158 | /** | 151 | /** |
@@ -163,7 +156,7 @@ class DownloadImages | |||
163 | public function removeImages($entryId) | 156 | public function removeImages($entryId) |
164 | { | 157 | { |
165 | $relativePath = $this->getRelativePath($entryId); | 158 | $relativePath = $this->getRelativePath($entryId); |
166 | $folderPath = $this->baseFolder.'/'.$relativePath; | 159 | $folderPath = $this->baseFolder . '/' . $relativePath; |
167 | 160 | ||
168 | $finder = new Finder(); | 161 | $finder = new Finder(); |
169 | $finder | 162 | $finder |
@@ -179,6 +172,17 @@ class DownloadImages | |||
179 | } | 172 | } |
180 | 173 | ||
181 | /** | 174 | /** |
175 | * Setup base folder where all images are going to be saved. | ||
176 | */ | ||
177 | private function setFolder() | ||
178 | { | ||
179 | // if folder doesn't exist, attempt to create one and store the folder name in property $folder | ||
180 | if (!file_exists($this->baseFolder)) { | ||
181 | mkdir($this->baseFolder, 0755, true); | ||
182 | } | ||
183 | } | ||
184 | |||
185 | /** | ||
182 | * Generate the folder where we are going to save images based on the entry url. | 186 | * Generate the folder where we are going to save images based on the entry url. |
183 | * | 187 | * |
184 | * @param int $entryId ID of the entry | 188 | * @param int $entryId ID of the entry |
@@ -188,8 +192,8 @@ class DownloadImages | |||
188 | private function getRelativePath($entryId) | 192 | private function getRelativePath($entryId) |
189 | { | 193 | { |
190 | $hashId = hash('crc32', $entryId); | 194 | $hashId = hash('crc32', $entryId); |
191 | $relativePath = $hashId[0].'/'.$hashId[1].'/'.$hashId; | 195 | $relativePath = $hashId[0] . '/' . $hashId[1] . '/' . $hashId; |
192 | $folderPath = $this->baseFolder.'/'.$relativePath; | 196 | $folderPath = $this->baseFolder . '/' . $relativePath; |
193 | 197 | ||
194 | if (!file_exists($folderPath)) { | 198 | if (!file_exists($folderPath)) { |
195 | mkdir($folderPath, 0777, true); | 199 | mkdir($folderPath, 0777, true); |
@@ -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/EntityTimestampsTrait.php b/src/Wallabag/CoreBundle/Helper/EntityTimestampsTrait.php new file mode 100644 index 00000000..1b1ff54a --- /dev/null +++ b/src/Wallabag/CoreBundle/Helper/EntityTimestampsTrait.php | |||
@@ -0,0 +1,24 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\CoreBundle\Helper; | ||
4 | |||
5 | use Doctrine\ORM\Mapping as ORM; | ||
6 | |||
7 | /** | ||
8 | * Trait to handle created & updated date of an Entity. | ||
9 | */ | ||
10 | trait EntityTimestampsTrait | ||
11 | { | ||
12 | /** | ||
13 | * @ORM\PrePersist | ||
14 | * @ORM\PreUpdate | ||
15 | */ | ||
16 | public function timestamps() | ||
17 | { | ||
18 | if (null === $this->createdAt) { | ||
19 | $this->createdAt = new \DateTime(); | ||
20 | } | ||
21 | |||
22 | $this->updatedAt = new \DateTime(); | ||
23 | } | ||
24 | } | ||
diff --git a/src/Wallabag/CoreBundle/Helper/EntriesExport.php b/src/Wallabag/CoreBundle/Helper/EntriesExport.php index 3d36a4c8..830798b8 100644 --- a/src/Wallabag/CoreBundle/Helper/EntriesExport.php +++ b/src/Wallabag/CoreBundle/Helper/EntriesExport.php | |||
@@ -2,12 +2,14 @@ | |||
2 | 2 | ||
3 | namespace Wallabag\CoreBundle\Helper; | 3 | namespace Wallabag\CoreBundle\Helper; |
4 | 4 | ||
5 | use JMS\Serializer; | 5 | use Html2Text\Html2Text; |
6 | use JMS\Serializer\SerializationContext; | 6 | use JMS\Serializer\SerializationContext; |
7 | use JMS\Serializer\SerializerBuilder; | 7 | use JMS\Serializer\SerializerBuilder; |
8 | use PHPePub\Core\EPub; | 8 | use PHPePub\Core\EPub; |
9 | use PHPePub\Core\Structure\OPF\DublinCore; | 9 | use PHPePub\Core\Structure\OPF\DublinCore; |
10 | use Symfony\Component\HttpFoundation\Response; | 10 | use Symfony\Component\HttpFoundation\Response; |
11 | use Symfony\Component\Translation\TranslatorInterface; | ||
12 | use Wallabag\CoreBundle\Entity\Entry; | ||
11 | 13 | ||
12 | /** | 14 | /** |
13 | * This class doesn't have unit test BUT it's fully covered by a functional test with ExportControllerTest. | 15 | * This class doesn't have unit test BUT it's fully covered by a functional test with ExportControllerTest. |
@@ -16,21 +18,20 @@ class EntriesExport | |||
16 | { | 18 | { |
17 | private $wallabagUrl; | 19 | private $wallabagUrl; |
18 | private $logoPath; | 20 | private $logoPath; |
21 | private $translator; | ||
19 | private $title = ''; | 22 | private $title = ''; |
20 | private $entries = []; | 23 | private $entries = []; |
21 | private $authors = ['wallabag']; | 24 | private $author = 'wallabag'; |
22 | private $language = ''; | 25 | private $language = ''; |
23 | private $footerTemplate = '<div style="text-align:center;"> | ||
24 | <p>Produced by wallabag with %EXPORT_METHOD%</p> | ||
25 | <p>Please open <a href="https://github.com/wallabag/wallabag/issues">an issue</a> if you have trouble with the display of this E-Book on your device.</p> | ||
26 | </div>'; | ||
27 | 26 | ||
28 | /** | 27 | /** |
29 | * @param string $wallabagUrl Wallabag instance url | 28 | * @param TranslatorInterface $translator Translator service |
30 | * @param string $logoPath Path to the logo FROM THE BUNDLE SCOPE | 29 | * @param string $wallabagUrl Wallabag instance url |
30 | * @param string $logoPath Path to the logo FROM THE BUNDLE SCOPE | ||
31 | */ | 31 | */ |
32 | public function __construct($wallabagUrl, $logoPath) | 32 | public function __construct(TranslatorInterface $translator, $wallabagUrl, $logoPath) |
33 | { | 33 | { |
34 | $this->translator = $translator; | ||
34 | $this->wallabagUrl = $wallabagUrl; | 35 | $this->wallabagUrl = $wallabagUrl; |
35 | $this->logoPath = $logoPath; | 36 | $this->logoPath = $logoPath; |
36 | } | 37 | } |
@@ -63,7 +64,7 @@ class EntriesExport | |||
63 | */ | 64 | */ |
64 | public function updateTitle($method) | 65 | public function updateTitle($method) |
65 | { | 66 | { |
66 | $this->title = $method.' articles'; | 67 | $this->title = $method . ' articles'; |
67 | 68 | ||
68 | if ('entry' === $method) { | 69 | if ('entry' === $method) { |
69 | $this->title = $this->entries[0]->getTitle(); | 70 | $this->title = $this->entries[0]->getTitle(); |
@@ -73,6 +74,33 @@ class EntriesExport | |||
73 | } | 74 | } |
74 | 75 | ||
75 | /** | 76 | /** |
77 | * Sets the author for one entry or category. | ||
78 | * | ||
79 | * The publishers are used, or the domain name if empty. | ||
80 | * | ||
81 | * @param string $method Method to get articles | ||
82 | * | ||
83 | * @return EntriesExport | ||
84 | */ | ||
85 | public function updateAuthor($method) | ||
86 | { | ||
87 | if ('entry' !== $method) { | ||
88 | $this->author = $method . ' authors'; | ||
89 | |||
90 | return $this; | ||
91 | } | ||
92 | |||
93 | $this->author = $this->entries[0]->getDomainName(); | ||
94 | |||
95 | $publishedBy = $this->entries[0]->getPublishedBy(); | ||
96 | if (!empty($publishedBy)) { | ||
97 | $this->author = implode(', ', $publishedBy); | ||
98 | } | ||
99 | |||
100 | return $this; | ||
101 | } | ||
102 | |||
103 | /** | ||
76 | * Sets the output format. | 104 | * Sets the output format. |
77 | * | 105 | * |
78 | * @param string $format | 106 | * @param string $format |
@@ -81,7 +109,7 @@ class EntriesExport | |||
81 | */ | 109 | */ |
82 | public function exportAs($format) | 110 | public function exportAs($format) |
83 | { | 111 | { |
84 | $functionName = 'produce'.ucfirst($format); | 112 | $functionName = 'produce' . ucfirst($format); |
85 | if (method_exists($this, $functionName)) { | 113 | if (method_exists($this, $functionName)) { |
86 | return $this->$functionName(); | 114 | return $this->$functionName(); |
87 | } | 115 | } |
@@ -106,12 +134,12 @@ class EntriesExport | |||
106 | */ | 134 | */ |
107 | $content_start = | 135 | $content_start = |
108 | "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" | 136 | "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" |
109 | ."<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n" | 137 | . "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n" |
110 | .'<head>' | 138 | . '<head>' |
111 | ."<meta http-equiv=\"Default-Style\" content=\"text/html; charset=utf-8\" />\n" | 139 | . "<meta http-equiv=\"Default-Style\" content=\"text/html; charset=utf-8\" />\n" |
112 | ."<title>wallabag articles book</title>\n" | 140 | . "<title>wallabag articles book</title>\n" |
113 | ."</head>\n" | 141 | . "</head>\n" |
114 | ."<body>\n"; | 142 | . "<body>\n"; |
115 | 143 | ||
116 | $bookEnd = "</body>\n</html>\n"; | 144 | $bookEnd = "</body>\n</html>\n"; |
117 | 145 | ||
@@ -128,9 +156,7 @@ class EntriesExport | |||
128 | $book->setLanguage($this->language); | 156 | $book->setLanguage($this->language); |
129 | $book->setDescription('Some articles saved on my wallabag'); | 157 | $book->setDescription('Some articles saved on my wallabag'); |
130 | 158 | ||
131 | foreach ($this->authors as $author) { | 159 | $book->setAuthor($this->author, $this->author); |
132 | $book->setAuthor($author, $author); | ||
133 | } | ||
134 | 160 | ||
135 | // I hope this is a non existant address :) | 161 | // I hope this is a non existant address :) |
136 | $book->setPublisher('wallabag', 'wallabag'); | 162 | $book->setPublisher('wallabag', 'wallabag'); |
@@ -164,11 +190,11 @@ class EntriesExport | |||
164 | // in filenames, we limit to A-z/0-9 | 190 | // in filenames, we limit to A-z/0-9 |
165 | $filename = preg_replace('/[^A-Za-z0-9\-]/', '', $entry->getTitle()); | 191 | $filename = preg_replace('/[^A-Za-z0-9\-]/', '', $entry->getTitle()); |
166 | 192 | ||
167 | $chapter = $content_start.$entry->getContent().$bookEnd; | 193 | $chapter = $content_start . $entry->getContent() . $bookEnd; |
168 | $book->addChapter($entry->getTitle(), htmlspecialchars($filename).'.html', $chapter, true, EPub::EXTERNAL_REF_ADD); | 194 | $book->addChapter($entry->getTitle(), htmlspecialchars($filename) . '.html', $chapter, true, EPub::EXTERNAL_REF_ADD); |
169 | } | 195 | } |
170 | 196 | ||
171 | $book->addChapter('Notices', 'Cover2.html', $content_start.$this->getExportInformation('PHPePub').$bookEnd); | 197 | $book->addChapter('Notices', 'Cover2.html', $content_start . $this->getExportInformation('PHPePub') . $bookEnd); |
172 | 198 | ||
173 | return Response::create( | 199 | return Response::create( |
174 | $book->getBook(), | 200 | $book->getBook(), |
@@ -176,7 +202,7 @@ class EntriesExport | |||
176 | [ | 202 | [ |
177 | 'Content-Description' => 'File Transfer', | 203 | 'Content-Description' => 'File Transfer', |
178 | 'Content-type' => 'application/epub+zip', | 204 | 'Content-type' => 'application/epub+zip', |
179 | 'Content-Disposition' => 'attachment; filename="'.$this->title.'.epub"', | 205 | 'Content-Disposition' => 'attachment; filename="' . $this->title . '.epub"', |
180 | 'Content-Transfer-Encoding' => 'binary', | 206 | 'Content-Transfer-Encoding' => 'binary', |
181 | ] | 207 | ] |
182 | ); | 208 | ); |
@@ -196,7 +222,7 @@ class EntriesExport | |||
196 | * Book metadata | 222 | * Book metadata |
197 | */ | 223 | */ |
198 | $content->set('title', $this->title); | 224 | $content->set('title', $this->title); |
199 | $content->set('author', implode($this->authors)); | 225 | $content->set('author', $this->author); |
200 | $content->set('subject', $this->title); | 226 | $content->set('subject', $this->title); |
201 | 227 | ||
202 | /* | 228 | /* |
@@ -228,7 +254,7 @@ class EntriesExport | |||
228 | 'Accept-Ranges' => 'bytes', | 254 | 'Accept-Ranges' => 'bytes', |
229 | 'Content-Description' => 'File Transfer', | 255 | 'Content-Description' => 'File Transfer', |
230 | 'Content-type' => 'application/x-mobipocket-ebook', | 256 | 'Content-type' => 'application/x-mobipocket-ebook', |
231 | 'Content-Disposition' => 'attachment; filename="'.$this->title.'.mobi"', | 257 | 'Content-Disposition' => 'attachment; filename="' . $this->title . '.mobi"', |
232 | 'Content-Transfer-Encoding' => 'binary', | 258 | 'Content-Transfer-Encoding' => 'binary', |
233 | ] | 259 | ] |
234 | ); | 260 | ); |
@@ -247,7 +273,7 @@ class EntriesExport | |||
247 | * Book metadata | 273 | * Book metadata |
248 | */ | 274 | */ |
249 | $pdf->SetCreator(PDF_CREATOR); | 275 | $pdf->SetCreator(PDF_CREATOR); |
250 | $pdf->SetAuthor('wallabag'); | 276 | $pdf->SetAuthor($this->author); |
251 | $pdf->SetTitle($this->title); | 277 | $pdf->SetTitle($this->title); |
252 | $pdf->SetSubject('Articles via wallabag'); | 278 | $pdf->SetSubject('Articles via wallabag'); |
253 | $pdf->SetKeywords('wallabag'); | 279 | $pdf->SetKeywords('wallabag'); |
@@ -256,7 +282,7 @@ class EntriesExport | |||
256 | * Front page | 282 | * Front page |
257 | */ | 283 | */ |
258 | $pdf->AddPage(); | 284 | $pdf->AddPage(); |
259 | $intro = '<h1>'.$this->title.'</h1>'.$this->getExportInformation('tcpdf'); | 285 | $intro = '<h1>' . $this->title . '</h1>' . $this->getExportInformation('tcpdf'); |
260 | 286 | ||
261 | $pdf->writeHTMLCell(0, 0, '', '', $intro, 0, 1, 0, true, '', true); | 287 | $pdf->writeHTMLCell(0, 0, '', '', $intro, 0, 1, 0, true, '', true); |
262 | 288 | ||
@@ -269,7 +295,7 @@ class EntriesExport | |||
269 | } | 295 | } |
270 | 296 | ||
271 | $pdf->AddPage(); | 297 | $pdf->AddPage(); |
272 | $html = '<h1>'.$entry->getTitle().'</h1>'; | 298 | $html = '<h1>' . $entry->getTitle() . '</h1>'; |
273 | $html .= $entry->getContent(); | 299 | $html .= $entry->getContent(); |
274 | 300 | ||
275 | $pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true); | 301 | $pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true); |
@@ -284,7 +310,7 @@ class EntriesExport | |||
284 | [ | 310 | [ |
285 | 'Content-Description' => 'File Transfer', | 311 | 'Content-Description' => 'File Transfer', |
286 | 'Content-type' => 'application/pdf', | 312 | 'Content-type' => 'application/pdf', |
287 | 'Content-Disposition' => 'attachment; filename="'.$this->title.'.pdf"', | 313 | 'Content-Disposition' => 'attachment; filename="' . $this->title . '.pdf"', |
288 | 'Content-Transfer-Encoding' => 'binary', | 314 | 'Content-Transfer-Encoding' => 'binary', |
289 | ] | 315 | ] |
290 | ); | 316 | ); |
@@ -330,7 +356,7 @@ class EntriesExport | |||
330 | 200, | 356 | 200, |
331 | [ | 357 | [ |
332 | 'Content-type' => 'application/csv', | 358 | 'Content-type' => 'application/csv', |
333 | 'Content-Disposition' => 'attachment; filename="'.$this->title.'.csv"', | 359 | 'Content-Disposition' => 'attachment; filename="' . $this->title . '.csv"', |
334 | 'Content-Transfer-Encoding' => 'UTF-8', | 360 | 'Content-Transfer-Encoding' => 'UTF-8', |
335 | ] | 361 | ] |
336 | ); | 362 | ); |
@@ -348,7 +374,7 @@ class EntriesExport | |||
348 | 200, | 374 | 200, |
349 | [ | 375 | [ |
350 | 'Content-type' => 'application/json', | 376 | 'Content-type' => 'application/json', |
351 | 'Content-Disposition' => 'attachment; filename="'.$this->title.'.json"', | 377 | 'Content-Disposition' => 'attachment; filename="' . $this->title . '.json"', |
352 | 'Content-Transfer-Encoding' => 'UTF-8', | 378 | 'Content-Transfer-Encoding' => 'UTF-8', |
353 | ] | 379 | ] |
354 | ); | 380 | ); |
@@ -366,7 +392,7 @@ class EntriesExport | |||
366 | 200, | 392 | 200, |
367 | [ | 393 | [ |
368 | 'Content-type' => 'application/xml', | 394 | 'Content-type' => 'application/xml', |
369 | 'Content-Disposition' => 'attachment; filename="'.$this->title.'.xml"', | 395 | 'Content-Disposition' => 'attachment; filename="' . $this->title . '.xml"', |
370 | 'Content-Transfer-Encoding' => 'UTF-8', | 396 | 'Content-Transfer-Encoding' => 'UTF-8', |
371 | ] | 397 | ] |
372 | ); | 398 | ); |
@@ -382,8 +408,9 @@ class EntriesExport | |||
382 | $content = ''; | 408 | $content = ''; |
383 | $bar = str_repeat('=', 100); | 409 | $bar = str_repeat('=', 100); |
384 | foreach ($this->entries as $entry) { | 410 | foreach ($this->entries as $entry) { |
385 | $content .= "\n\n".$bar."\n\n".$entry->getTitle()."\n\n".$bar."\n\n"; | 411 | $content .= "\n\n" . $bar . "\n\n" . $entry->getTitle() . "\n\n" . $bar . "\n\n"; |
386 | $content .= trim(preg_replace('/\s+/S', ' ', strip_tags($entry->getContent())))."\n\n"; | 412 | $html = new Html2Text($entry->getContent(), ['do_links' => 'none', 'width' => 100]); |
413 | $content .= $html->getText(); | ||
387 | } | 414 | } |
388 | 415 | ||
389 | return Response::create( | 416 | return Response::create( |
@@ -391,7 +418,7 @@ class EntriesExport | |||
391 | 200, | 418 | 200, |
392 | [ | 419 | [ |
393 | 'Content-type' => 'text/plain', | 420 | 'Content-type' => 'text/plain', |
394 | 'Content-Disposition' => 'attachment; filename="'.$this->title.'.txt"', | 421 | 'Content-Disposition' => 'attachment; filename="' . $this->title . '.txt"', |
395 | 'Content-Transfer-Encoding' => 'UTF-8', | 422 | 'Content-Transfer-Encoding' => 'UTF-8', |
396 | ] | 423 | ] |
397 | ); | 424 | ); |
@@ -402,7 +429,7 @@ class EntriesExport | |||
402 | * | 429 | * |
403 | * @param string $format | 430 | * @param string $format |
404 | * | 431 | * |
405 | * @return Serializer | 432 | * @return string |
406 | */ | 433 | */ |
407 | private function prepareSerializingContent($format) | 434 | private function prepareSerializingContent($format) |
408 | { | 435 | { |
@@ -424,10 +451,12 @@ class EntriesExport | |||
424 | */ | 451 | */ |
425 | private function getExportInformation($type) | 452 | private function getExportInformation($type) |
426 | { | 453 | { |
427 | $info = str_replace('%EXPORT_METHOD%', $type, $this->footerTemplate); | 454 | $info = $this->translator->trans('export.footer_template', [ |
455 | '%method%' => $type, | ||
456 | ]); | ||
428 | 457 | ||
429 | if ('tcpdf' === $type) { | 458 | if ('tcpdf' === $type) { |
430 | return str_replace('%IMAGE%', '<img src="'.$this->logoPath.'" />', $info); | 459 | return str_replace('%IMAGE%', '<img src="' . $this->logoPath . '" />', $info); |
431 | } | 460 | } |
432 | 461 | ||
433 | return str_replace('%IMAGE%', '', $info); | 462 | return str_replace('%IMAGE%', '', $info); |
diff --git a/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php b/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php index 1ac8feb1..4602a684 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; |
@@ -43,7 +41,7 @@ class HttpClientFactory | |||
43 | */ | 41 | */ |
44 | public function buildHttpClient() | 42 | public function buildHttpClient() |
45 | { | 43 | { |
46 | $this->logger->log('debug', 'Restricted access config enabled?', array('enabled' => (int) $this->restrictedAccess)); | 44 | $this->logger->log('debug', 'Restricted access config enabled?', ['enabled' => (int) $this->restrictedAccess]); |
47 | 45 | ||
48 | if (0 === (int) $this->restrictedAccess) { | 46 | if (0 === (int) $this->restrictedAccess) { |
49 | return; | 47 | return; |
@@ -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..49c1ea41 100644 --- a/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php +++ b/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php | |||
@@ -6,6 +6,7 @@ use Pagerfanta\Adapter\AdapterInterface; | |||
6 | use Pagerfanta\Pagerfanta; | 6 | use Pagerfanta\Pagerfanta; |
7 | use Symfony\Component\Routing\Router; | 7 | use Symfony\Component\Routing\Router; |
8 | use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; | 8 | use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; |
9 | use Wallabag\UserBundle\Entity\User; | ||
9 | 10 | ||
10 | class PreparePagerForEntries | 11 | class PreparePagerForEntries |
11 | { | 12 | { |
@@ -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..63f65067 100644 --- a/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php +++ b/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | namespace Wallabag\CoreBundle\Helper; | 3 | namespace Wallabag\CoreBundle\Helper; |
4 | 4 | ||
5 | use Psr\Log\LoggerInterface; | ||
5 | use RulerZ\RulerZ; | 6 | use RulerZ\RulerZ; |
6 | use Wallabag\CoreBundle\Entity\Entry; | 7 | use Wallabag\CoreBundle\Entity\Entry; |
7 | use Wallabag\CoreBundle\Entity\Tag; | 8 | use Wallabag\CoreBundle\Entity\Tag; |
@@ -14,12 +15,14 @@ class RuleBasedTagger | |||
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..0bfe5c57 --- /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(mb_convert_case($label, MB_CASE_LOWER)); | ||
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 | } | ||