diff options
Diffstat (limited to 'application')
-rw-r--r-- | application/Utils.php | 4 | ||||
-rw-r--r-- | application/bookmark/LinkUtils.php | 106 | ||||
-rw-r--r-- | application/container/ContainerBuilder.php | 10 | ||||
-rw-r--r-- | application/container/ShaarliContainer.php | 4 | ||||
-rw-r--r-- | application/front/controller/admin/PostBookmarkController.php | 258 | ||||
-rw-r--r-- | application/front/controller/admin/ToolsController.php | 2 | ||||
-rw-r--r-- | application/front/controller/visitor/DailyController.php | 2 | ||||
-rw-r--r-- | application/front/controller/visitor/FeedController.php | 2 | ||||
-rw-r--r-- | application/front/controller/visitor/ShaarliVisitorController.php | 14 | ||||
-rw-r--r-- | application/http/HttpAccess.php | 39 | ||||
-rw-r--r-- | application/http/HttpUtils.php | 106 |
11 files changed, 432 insertions, 115 deletions
diff --git a/application/Utils.php b/application/Utils.php index 72c90049..9c9eaaa2 100644 --- a/application/Utils.php +++ b/application/Utils.php | |||
@@ -91,6 +91,10 @@ function endsWith($haystack, $needle, $case = true) | |||
91 | */ | 91 | */ |
92 | function escape($input) | 92 | function escape($input) |
93 | { | 93 | { |
94 | if (null === $input) { | ||
95 | return null; | ||
96 | } | ||
97 | |||
94 | if (is_bool($input)) { | 98 | if (is_bool($input)) { |
95 | return $input; | 99 | return $input; |
96 | } | 100 | } |
diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index 98d9038a..68914fca 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php | |||
@@ -3,112 +3,6 @@ | |||
3 | use Shaarli\Bookmark\Bookmark; | 3 | use Shaarli\Bookmark\Bookmark; |
4 | 4 | ||
5 | /** | 5 | /** |
6 | * Get cURL callback function for CURLOPT_WRITEFUNCTION | ||
7 | * | ||
8 | * @param string $charset to extract from the downloaded page (reference) | ||
9 | * @param string $title to extract from the downloaded page (reference) | ||
10 | * @param string $description to extract from the downloaded page (reference) | ||
11 | * @param string $keywords to extract from the downloaded page (reference) | ||
12 | * @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content | ||
13 | * @param string $curlGetInfo Optionally overrides curl_getinfo function | ||
14 | * | ||
15 | * @return Closure | ||
16 | */ | ||
17 | function get_curl_download_callback( | ||
18 | &$charset, | ||
19 | &$title, | ||
20 | &$description, | ||
21 | &$keywords, | ||
22 | $retrieveDescription, | ||
23 | $curlGetInfo = 'curl_getinfo' | ||
24 | ) { | ||
25 | $isRedirected = false; | ||
26 | $currentChunk = 0; | ||
27 | $foundChunk = null; | ||
28 | |||
29 | /** | ||
30 | * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download). | ||
31 | * | ||
32 | * While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text' | ||
33 | * Then we extract the title and the charset and stop the download when it's done. | ||
34 | * | ||
35 | * @param resource $ch cURL resource | ||
36 | * @param string $data chunk of data being downloaded | ||
37 | * | ||
38 | * @return int|bool length of $data or false if we need to stop the download | ||
39 | */ | ||
40 | return function (&$ch, $data) use ( | ||
41 | $retrieveDescription, | ||
42 | $curlGetInfo, | ||
43 | &$charset, | ||
44 | &$title, | ||
45 | &$description, | ||
46 | &$keywords, | ||
47 | &$isRedirected, | ||
48 | &$currentChunk, | ||
49 | &$foundChunk | ||
50 | ) { | ||
51 | $currentChunk++; | ||
52 | $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE); | ||
53 | if (!empty($responseCode) && in_array($responseCode, [301, 302])) { | ||
54 | $isRedirected = true; | ||
55 | return strlen($data); | ||
56 | } | ||
57 | if (!empty($responseCode) && $responseCode !== 200) { | ||
58 | return false; | ||
59 | } | ||
60 | // After a redirection, the content type will keep the previous request value | ||
61 | // until it finds the next content-type header. | ||
62 | if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) { | ||
63 | $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE); | ||
64 | } | ||
65 | if (!empty($contentType) && strpos($contentType, 'text/html') === false) { | ||
66 | return false; | ||
67 | } | ||
68 | if (!empty($contentType) && empty($charset)) { | ||
69 | $charset = header_extract_charset($contentType); | ||
70 | } | ||
71 | if (empty($charset)) { | ||
72 | $charset = html_extract_charset($data); | ||
73 | } | ||
74 | if (empty($title)) { | ||
75 | $title = html_extract_title($data); | ||
76 | $foundChunk = ! empty($title) ? $currentChunk : $foundChunk; | ||
77 | } | ||
78 | if ($retrieveDescription && empty($description)) { | ||
79 | $description = html_extract_tag('description', $data); | ||
80 | $foundChunk = ! empty($description) ? $currentChunk : $foundChunk; | ||
81 | } | ||
82 | if ($retrieveDescription && empty($keywords)) { | ||
83 | $keywords = html_extract_tag('keywords', $data); | ||
84 | if (! empty($keywords)) { | ||
85 | $foundChunk = $currentChunk; | ||
86 | // Keywords use the format tag1, tag2 multiple words, tag | ||
87 | // So we format them to match Shaarli's separator and glue multiple words with '-' | ||
88 | $keywords = implode(' ', array_map(function($keyword) { | ||
89 | return implode('-', preg_split('/\s+/', trim($keyword))); | ||
90 | }, explode(',', $keywords))); | ||
91 | } | ||
92 | } | ||
93 | |||
94 | // We got everything we want, stop the download. | ||
95 | // If we already found either the title, description or keywords, | ||
96 | // it's highly unlikely that we'll found the other metas further than | ||
97 | // in the same chunk of data or the next one. So we also stop the download after that. | ||
98 | if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null | ||
99 | && (! $retrieveDescription | ||
100 | || $foundChunk < $currentChunk | ||
101 | || (!empty($title) && !empty($description) && !empty($keywords)) | ||
102 | ) | ||
103 | ) { | ||
104 | return false; | ||
105 | } | ||
106 | |||
107 | return strlen($data); | ||
108 | }; | ||
109 | } | ||
110 | |||
111 | /** | ||
112 | * Extract title from an HTML document. | 6 | * Extract title from an HTML document. |
113 | * | 7 | * |
114 | * @param string $html HTML content where to look for a title. | 8 | * @param string $html HTML content where to look for a title. |
diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index 84406979..85126246 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php | |||
@@ -10,11 +10,13 @@ use Shaarli\Config\ConfigManager; | |||
10 | use Shaarli\Feed\FeedBuilder; | 10 | use Shaarli\Feed\FeedBuilder; |
11 | use Shaarli\Formatter\FormatterFactory; | 11 | use Shaarli\Formatter\FormatterFactory; |
12 | use Shaarli\History; | 12 | use Shaarli\History; |
13 | use Shaarli\Http\HttpAccess; | ||
13 | use Shaarli\Plugin\PluginManager; | 14 | use Shaarli\Plugin\PluginManager; |
14 | use Shaarli\Render\PageBuilder; | 15 | use Shaarli\Render\PageBuilder; |
15 | use Shaarli\Render\PageCacheManager; | 16 | use Shaarli\Render\PageCacheManager; |
16 | use Shaarli\Security\LoginManager; | 17 | use Shaarli\Security\LoginManager; |
17 | use Shaarli\Security\SessionManager; | 18 | use Shaarli\Security\SessionManager; |
19 | use Shaarli\Thumbnailer; | ||
18 | 20 | ||
19 | /** | 21 | /** |
20 | * Class ContainerBuilder | 22 | * Class ContainerBuilder |
@@ -110,6 +112,14 @@ class ContainerBuilder | |||
110 | ); | 112 | ); |
111 | }; | 113 | }; |
112 | 114 | ||
115 | $container['thumbnailer'] = function (ShaarliContainer $container): Thumbnailer { | ||
116 | return new Thumbnailer($container->conf); | ||
117 | }; | ||
118 | |||
119 | $container['httpAccess'] = function (): HttpAccess { | ||
120 | return new HttpAccess(); | ||
121 | }; | ||
122 | |||
113 | return $container; | 123 | return $container; |
114 | } | 124 | } |
115 | } | 125 | } |
diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php index deb07197..fec398d0 100644 --- a/application/container/ShaarliContainer.php +++ b/application/container/ShaarliContainer.php | |||
@@ -9,11 +9,13 @@ use Shaarli\Config\ConfigManager; | |||
9 | use Shaarli\Feed\FeedBuilder; | 9 | use Shaarli\Feed\FeedBuilder; |
10 | use Shaarli\Formatter\FormatterFactory; | 10 | use Shaarli\Formatter\FormatterFactory; |
11 | use Shaarli\History; | 11 | use Shaarli\History; |
12 | use Shaarli\Http\HttpAccess; | ||
12 | use Shaarli\Plugin\PluginManager; | 13 | use Shaarli\Plugin\PluginManager; |
13 | use Shaarli\Render\PageBuilder; | 14 | use Shaarli\Render\PageBuilder; |
14 | use Shaarli\Render\PageCacheManager; | 15 | use Shaarli\Render\PageCacheManager; |
15 | use Shaarli\Security\LoginManager; | 16 | use Shaarli\Security\LoginManager; |
16 | use Shaarli\Security\SessionManager; | 17 | use Shaarli\Security\SessionManager; |
18 | use Shaarli\Thumbnailer; | ||
17 | use Slim\Container; | 19 | use Slim\Container; |
18 | 20 | ||
19 | /** | 21 | /** |
@@ -31,6 +33,8 @@ use Slim\Container; | |||
31 | * @property FormatterFactory $formatterFactory | 33 | * @property FormatterFactory $formatterFactory |
32 | * @property PageCacheManager $pageCacheManager | 34 | * @property PageCacheManager $pageCacheManager |
33 | * @property FeedBuilder $feedBuilder | 35 | * @property FeedBuilder $feedBuilder |
36 | * @property Thumbnailer $thumbnailer | ||
37 | * @property HttpAccess $httpAccess | ||
34 | */ | 38 | */ |
35 | class ShaarliContainer extends Container | 39 | class ShaarliContainer extends Container |
36 | { | 40 | { |
diff --git a/application/front/controller/admin/PostBookmarkController.php b/application/front/controller/admin/PostBookmarkController.php new file mode 100644 index 00000000..dbe570e2 --- /dev/null +++ b/application/front/controller/admin/PostBookmarkController.php | |||
@@ -0,0 +1,258 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use Shaarli\Bookmark\Bookmark; | ||
8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | ||
9 | use Shaarli\Formatter\BookmarkMarkdownFormatter; | ||
10 | use Shaarli\Thumbnailer; | ||
11 | use Slim\Http\Request; | ||
12 | use Slim\Http\Response; | ||
13 | |||
14 | /** | ||
15 | * Class PostBookmarkController | ||
16 | * | ||
17 | * Slim controller used to handle Shaarli create or edit bookmarks. | ||
18 | */ | ||
19 | class PostBookmarkController extends ShaarliAdminController | ||
20 | { | ||
21 | /** | ||
22 | * GET /add-shaare - Displays the form used to create a new bookmark from an URL | ||
23 | */ | ||
24 | public function addShaare(Request $request, Response $response): Response | ||
25 | { | ||
26 | $this->assignView( | ||
27 | 'pagetitle', | ||
28 | t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli') | ||
29 | ); | ||
30 | |||
31 | return $response->write($this->render('addlink')); | ||
32 | } | ||
33 | |||
34 | /** | ||
35 | * GET /shaare - Displays the bookmark form for creation. | ||
36 | * Note that if the URL is found in existing bookmarks, then it will be in edit mode. | ||
37 | */ | ||
38 | public function displayCreateForm(Request $request, Response $response): Response | ||
39 | { | ||
40 | $url = cleanup_url($request->getParam('post')); | ||
41 | |||
42 | $linkIsNew = false; | ||
43 | // Check if URL is not already in database (in this case, we will edit the existing link) | ||
44 | $bookmark = $this->container->bookmarkService->findByUrl($url); | ||
45 | if (null === $bookmark) { | ||
46 | $linkIsNew = true; | ||
47 | // Get shaare data if it was provided in URL (e.g.: by the bookmarklet). | ||
48 | $title = $request->getParam('title'); | ||
49 | $description = $request->getParam('description'); | ||
50 | $tags = $request->getParam('tags'); | ||
51 | $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN); | ||
52 | |||
53 | // If this is an HTTP(S) link, we try go get the page to extract | ||
54 | // the title (otherwise we will to straight to the edit form.) | ||
55 | if (empty($title) && strpos(get_url_scheme($url) ?: '', 'http') !== false) { | ||
56 | $retrieveDescription = $this->container->conf->get('general.retrieve_description'); | ||
57 | // Short timeout to keep the application responsive | ||
58 | // The callback will fill $charset and $title with data from the downloaded page. | ||
59 | $this->container->httpAccess->getHttpResponse( | ||
60 | $url, | ||
61 | $this->container->conf->get('general.download_timeout', 30), | ||
62 | $this->container->conf->get('general.download_max_size', 4194304), | ||
63 | $this->container->httpAccess->getCurlDownloadCallback( | ||
64 | $charset, | ||
65 | $title, | ||
66 | $description, | ||
67 | $tags, | ||
68 | $retrieveDescription | ||
69 | ) | ||
70 | ); | ||
71 | if (! empty($title) && strtolower($charset) !== 'utf-8') { | ||
72 | $title = mb_convert_encoding($title, 'utf-8', $charset); | ||
73 | } | ||
74 | } | ||
75 | |||
76 | if (empty($url) && empty($title)) { | ||
77 | $title = $this->container->conf->get('general.default_note_title', t('Note: ')); | ||
78 | } | ||
79 | |||
80 | $link = escape([ | ||
81 | 'title' => $title, | ||
82 | 'url' => $url ?? '', | ||
83 | 'description' => $description ?? '', | ||
84 | 'tags' => $tags ?? '', | ||
85 | 'private' => $private, | ||
86 | ]); | ||
87 | } else { | ||
88 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
89 | $link = $formatter->format($bookmark); | ||
90 | } | ||
91 | |||
92 | return $this->displayForm($link, $linkIsNew, $request, $response); | ||
93 | } | ||
94 | |||
95 | /** | ||
96 | * GET /shaare-{id} - Displays the bookmark form in edition mode. | ||
97 | */ | ||
98 | public function displayEditForm(Request $request, Response $response, array $args): Response | ||
99 | { | ||
100 | $id = $args['id']; | ||
101 | try { | ||
102 | if (false === ctype_digit($id)) { | ||
103 | throw new BookmarkNotFoundException(); | ||
104 | } | ||
105 | $bookmark = $this->container->bookmarkService->get($id); // Read database | ||
106 | } catch (BookmarkNotFoundException $e) { | ||
107 | $this->saveErrorMessage(t('Bookmark not found')); | ||
108 | |||
109 | return $response->withRedirect('./'); | ||
110 | } | ||
111 | |||
112 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
113 | $link = $formatter->format($bookmark); | ||
114 | |||
115 | return $this->displayForm($link, false, $request, $response); | ||
116 | } | ||
117 | |||
118 | /** | ||
119 | * POST /shaare | ||
120 | */ | ||
121 | public function save(Request $request, Response $response): Response | ||
122 | { | ||
123 | $this->checkToken($request); | ||
124 | |||
125 | // lf_id should only be present if the link exists. | ||
126 | $id = $request->getParam('lf_id') ? intval(escape($request->getParam('lf_id'))) : null; | ||
127 | if (null !== $id && true === $this->container->bookmarkService->exists($id)) { | ||
128 | // Edit | ||
129 | $bookmark = $this->container->bookmarkService->get($id); | ||
130 | } else { | ||
131 | // New link | ||
132 | $bookmark = new Bookmark(); | ||
133 | } | ||
134 | |||
135 | $bookmark->setTitle($request->getParam('lf_title')); | ||
136 | $bookmark->setDescription($request->getParam('lf_description')); | ||
137 | $bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', [])); | ||
138 | $bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN)); | ||
139 | $bookmark->setTagsString($request->getParam('lf_tags')); | ||
140 | |||
141 | if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE | ||
142 | && false === $bookmark->isNote() | ||
143 | ) { | ||
144 | $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); | ||
145 | } | ||
146 | $this->container->bookmarkService->addOrSet($bookmark, false); | ||
147 | |||
148 | // To preserve backward compatibility with 3rd parties, plugins still use arrays | ||
149 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
150 | $data = $formatter->format($bookmark); | ||
151 | $data = $this->executeHooks('save_link', $data); | ||
152 | |||
153 | $bookmark->fromArray($data); | ||
154 | $this->container->bookmarkService->set($bookmark); | ||
155 | |||
156 | // If we are called from the bookmarklet, we must close the popup: | ||
157 | if ($request->getParam('source') === 'bookmarklet') { | ||
158 | return $response->write('<script>self.close();</script>'); | ||
159 | } | ||
160 | |||
161 | if (!empty($request->getParam('returnurl'))) { | ||
162 | $this->container->environment['HTTP_REFERER'] = escape($request->getParam('returnurl')); | ||
163 | } | ||
164 | |||
165 | return $this->redirectFromReferer( | ||
166 | $request, | ||
167 | $response, | ||
168 | ['add-shaare', 'shaare'], ['addlink', 'post', 'edit_link'], | ||
169 | $bookmark->getShortUrl() | ||
170 | ); | ||
171 | } | ||
172 | |||
173 | public function deleteBookmark(Request $request, Response $response): Response | ||
174 | { | ||
175 | $this->checkToken($request); | ||
176 | |||
177 | $ids = escape(trim($request->getParam('lf_linkdate'))); | ||
178 | if (strpos($ids, ' ') !== false) { | ||
179 | // multiple, space-separated ids provided | ||
180 | $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'strlen')); | ||
181 | } else { | ||
182 | $ids = [$ids]; | ||
183 | } | ||
184 | |||
185 | // assert at least one id is given | ||
186 | if (0 === count($ids)) { | ||
187 | $this->saveErrorMessage(t('Invalid bookmark ID provided.')); | ||
188 | |||
189 | return $this->redirectFromReferer($request, $response, [], ['delete-shaare']); | ||
190 | } | ||
191 | |||
192 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
193 | foreach ($ids as $id) { | ||
194 | $id = (int) $id; | ||
195 | // TODO: check if it exists | ||
196 | $bookmark = $this->container->bookmarkService->get($id); | ||
197 | $data = $formatter->format($bookmark); | ||
198 | $this->container->pluginManager->executeHooks('delete_link', $data); | ||
199 | $this->container->bookmarkService->remove($bookmark, false); | ||
200 | } | ||
201 | |||
202 | $this->container->bookmarkService->save(); | ||
203 | |||
204 | // If we are called from the bookmarklet, we must close the popup: | ||
205 | if ($request->getParam('source') === 'bookmarklet') { | ||
206 | return $response->write('<script>self.close();</script>'); | ||
207 | } | ||
208 | |||
209 | // Don't redirect to where we were previously because the datastore has changed. | ||
210 | return $response->withRedirect('./'); | ||
211 | } | ||
212 | |||
213 | protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response | ||
214 | { | ||
215 | $tags = $this->container->bookmarkService->bookmarksCountPerTag(); | ||
216 | if ($this->container->conf->get('formatter') === 'markdown') { | ||
217 | $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; | ||
218 | } | ||
219 | |||
220 | $data = [ | ||
221 | 'link' => $link, | ||
222 | 'link_is_new' => $isNew, | ||
223 | 'http_referer' => escape($this->container->environment['HTTP_REFERER'] ?? ''), | ||
224 | 'source' => $request->getParam('source') ?? '', | ||
225 | 'tags' => $tags, | ||
226 | 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false), | ||
227 | ]; | ||
228 | |||
229 | $data = $this->executeHooks('render_editlink', $data); | ||
230 | |||
231 | foreach ($data as $key => $value) { | ||
232 | $this->assignView($key, $value); | ||
233 | } | ||
234 | |||
235 | $editLabel = false === $isNew ? t('Edit') .' ' : ''; | ||
236 | $this->assignView( | ||
237 | 'pagetitle', | ||
238 | $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli') | ||
239 | ); | ||
240 | |||
241 | return $response->write($this->render('editlink')); | ||
242 | } | ||
243 | |||
244 | /** | ||
245 | * @param mixed[] $data Variables passed to the template engine | ||
246 | * | ||
247 | * @return mixed[] Template data after active plugins render_picwall hook execution. | ||
248 | */ | ||
249 | protected function executeHooks(string $hook, array $data): array | ||
250 | { | ||
251 | $this->container->pluginManager->executeHooks( | ||
252 | $hook, | ||
253 | $data | ||
254 | ); | ||
255 | |||
256 | return $data; | ||
257 | } | ||
258 | } | ||
diff --git a/application/front/controller/admin/ToolsController.php b/application/front/controller/admin/ToolsController.php index 66db5ad9..d087f2cd 100644 --- a/application/front/controller/admin/ToolsController.php +++ b/application/front/controller/admin/ToolsController.php | |||
@@ -21,7 +21,7 @@ class ToolsController extends ShaarliAdminController | |||
21 | 'sslenabled' => is_https($this->container->environment), | 21 | 'sslenabled' => is_https($this->container->environment), |
22 | ]; | 22 | ]; |
23 | 23 | ||
24 | $this->executeHooks($data); | 24 | $data = $this->executeHooks($data); |
25 | 25 | ||
26 | foreach ($data as $key => $value) { | 26 | foreach ($data as $key => $value) { |
27 | $this->assignView($key, $value); | 27 | $this->assignView($key, $value); |
diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index 47e2503a..e5c9ddac 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php | |||
@@ -71,7 +71,7 @@ class DailyController extends ShaarliVisitorController | |||
71 | ]; | 71 | ]; |
72 | 72 | ||
73 | // Hooks are called before column construction so that plugins don't have to deal with columns. | 73 | // Hooks are called before column construction so that plugins don't have to deal with columns. |
74 | $this->executeHooks($data); | 74 | $data = $this->executeHooks($data); |
75 | 75 | ||
76 | $data['cols'] = $this->calculateColumns($data['linksToDisplay']); | 76 | $data['cols'] = $this->calculateColumns($data['linksToDisplay']); |
77 | 77 | ||
diff --git a/application/front/controller/visitor/FeedController.php b/application/front/controller/visitor/FeedController.php index 70664635..f76f55fd 100644 --- a/application/front/controller/visitor/FeedController.php +++ b/application/front/controller/visitor/FeedController.php | |||
@@ -46,7 +46,7 @@ class FeedController extends ShaarliVisitorController | |||
46 | 46 | ||
47 | $data = $this->container->feedBuilder->buildData($feedType, $request->getParams()); | 47 | $data = $this->container->feedBuilder->buildData($feedType, $request->getParams()); |
48 | 48 | ||
49 | $this->executeHooks($data, $feedType); | 49 | $data = $this->executeHooks($data, $feedType); |
50 | $this->assignAllView($data); | 50 | $this->assignAllView($data); |
51 | 51 | ||
52 | $content = $this->render('feed.'. $feedType); | 52 | $content = $this->render('feed.'. $feedType); |
diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php index f12915c1..98423d90 100644 --- a/application/front/controller/visitor/ShaarliVisitorController.php +++ b/application/front/controller/visitor/ShaarliVisitorController.php | |||
@@ -78,16 +78,16 @@ abstract class ShaarliVisitorController | |||
78 | ]; | 78 | ]; |
79 | 79 | ||
80 | foreach ($common_hooks as $name) { | 80 | foreach ($common_hooks as $name) { |
81 | $plugin_data = []; | 81 | $pluginData = []; |
82 | $this->container->pluginManager->executeHooks( | 82 | $this->container->pluginManager->executeHooks( |
83 | 'render_' . $name, | 83 | 'render_' . $name, |
84 | $plugin_data, | 84 | $pluginData, |
85 | [ | 85 | [ |
86 | 'target' => $template, | 86 | 'target' => $template, |
87 | 'loggedin' => $this->container->loginManager->isLoggedIn() | 87 | 'loggedin' => $this->container->loginManager->isLoggedIn() |
88 | ] | 88 | ] |
89 | ); | 89 | ); |
90 | $this->assignView('plugins_' . $name, $plugin_data); | 90 | $this->assignView('plugins_' . $name, $pluginData); |
91 | } | 91 | } |
92 | } | 92 | } |
93 | 93 | ||
@@ -102,9 +102,10 @@ abstract class ShaarliVisitorController | |||
102 | Request $request, | 102 | Request $request, |
103 | Response $response, | 103 | Response $response, |
104 | array $loopTerms = [], | 104 | array $loopTerms = [], |
105 | array $clearParams = [] | 105 | array $clearParams = [], |
106 | string $anchor = null | ||
106 | ): Response { | 107 | ): Response { |
107 | $defaultPath = $request->getUri()->getBasePath(); | 108 | $defaultPath = rtrim($request->getUri()->getBasePath(), '/') . '/'; |
108 | $referer = $this->container->environment['HTTP_REFERER'] ?? null; | 109 | $referer = $this->container->environment['HTTP_REFERER'] ?? null; |
109 | 110 | ||
110 | if (null !== $referer) { | 111 | if (null !== $referer) { |
@@ -133,7 +134,8 @@ abstract class ShaarliVisitorController | |||
133 | } | 134 | } |
134 | 135 | ||
135 | $queryString = count($params) > 0 ? '?'. http_build_query($params) : ''; | 136 | $queryString = count($params) > 0 ? '?'. http_build_query($params) : ''; |
137 | $anchor = $anchor ? '#' . $anchor : ''; | ||
136 | 138 | ||
137 | return $response->withRedirect($path . $queryString); | 139 | return $response->withRedirect($path . $queryString . $anchor); |
138 | } | 140 | } |
139 | } | 141 | } |
diff --git a/application/http/HttpAccess.php b/application/http/HttpAccess.php new file mode 100644 index 00000000..81d9e076 --- /dev/null +++ b/application/http/HttpAccess.php | |||
@@ -0,0 +1,39 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Http; | ||
6 | |||
7 | /** | ||
8 | * Class HttpAccess | ||
9 | * | ||
10 | * This is mostly an OOP wrapper for HTTP functions defined in `HttpUtils`. | ||
11 | * It is used as dependency injection in Shaarli's container. | ||
12 | * | ||
13 | * @package Shaarli\Http | ||
14 | */ | ||
15 | class HttpAccess | ||
16 | { | ||
17 | public function getHttpResponse($url, $timeout = 30, $maxBytes = 4194304, $curlWriteFunction = null) | ||
18 | { | ||
19 | return get_http_response($url, $timeout, $maxBytes, $curlWriteFunction); | ||
20 | } | ||
21 | |||
22 | public function getCurlDownloadCallback( | ||
23 | &$charset, | ||
24 | &$title, | ||
25 | &$description, | ||
26 | &$keywords, | ||
27 | $retrieveDescription, | ||
28 | $curlGetInfo = 'curl_getinfo' | ||
29 | ) { | ||
30 | return get_curl_download_callback( | ||
31 | $charset, | ||
32 | $title, | ||
33 | $description, | ||
34 | $keywords, | ||
35 | $retrieveDescription, | ||
36 | $curlGetInfo | ||
37 | ); | ||
38 | } | ||
39 | } | ||
diff --git a/application/http/HttpUtils.php b/application/http/HttpUtils.php index f00c4336..4fc4e3dc 100644 --- a/application/http/HttpUtils.php +++ b/application/http/HttpUtils.php | |||
@@ -484,3 +484,109 @@ function is_https($server) | |||
484 | 484 | ||
485 | return ! empty($server['HTTPS']); | 485 | return ! empty($server['HTTPS']); |
486 | } | 486 | } |
487 | |||
488 | /** | ||
489 | * Get cURL callback function for CURLOPT_WRITEFUNCTION | ||
490 | * | ||
491 | * @param string $charset to extract from the downloaded page (reference) | ||
492 | * @param string $title to extract from the downloaded page (reference) | ||
493 | * @param string $description to extract from the downloaded page (reference) | ||
494 | * @param string $keywords to extract from the downloaded page (reference) | ||
495 | * @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content | ||
496 | * @param string $curlGetInfo Optionally overrides curl_getinfo function | ||
497 | * | ||
498 | * @return Closure | ||
499 | */ | ||
500 | function get_curl_download_callback( | ||
501 | &$charset, | ||
502 | &$title, | ||
503 | &$description, | ||
504 | &$keywords, | ||
505 | $retrieveDescription, | ||
506 | $curlGetInfo = 'curl_getinfo' | ||
507 | ) { | ||
508 | $isRedirected = false; | ||
509 | $currentChunk = 0; | ||
510 | $foundChunk = null; | ||
511 | |||
512 | /** | ||
513 | * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download). | ||
514 | * | ||
515 | * While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text' | ||
516 | * Then we extract the title and the charset and stop the download when it's done. | ||
517 | * | ||
518 | * @param resource $ch cURL resource | ||
519 | * @param string $data chunk of data being downloaded | ||
520 | * | ||
521 | * @return int|bool length of $data or false if we need to stop the download | ||
522 | */ | ||
523 | return function (&$ch, $data) use ( | ||
524 | $retrieveDescription, | ||
525 | $curlGetInfo, | ||
526 | &$charset, | ||
527 | &$title, | ||
528 | &$description, | ||
529 | &$keywords, | ||
530 | &$isRedirected, | ||
531 | &$currentChunk, | ||
532 | &$foundChunk | ||
533 | ) { | ||
534 | $currentChunk++; | ||
535 | $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE); | ||
536 | if (!empty($responseCode) && in_array($responseCode, [301, 302])) { | ||
537 | $isRedirected = true; | ||
538 | return strlen($data); | ||
539 | } | ||
540 | if (!empty($responseCode) && $responseCode !== 200) { | ||
541 | return false; | ||
542 | } | ||
543 | // After a redirection, the content type will keep the previous request value | ||
544 | // until it finds the next content-type header. | ||
545 | if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) { | ||
546 | $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE); | ||
547 | } | ||
548 | if (!empty($contentType) && strpos($contentType, 'text/html') === false) { | ||
549 | return false; | ||
550 | } | ||
551 | if (!empty($contentType) && empty($charset)) { | ||
552 | $charset = header_extract_charset($contentType); | ||
553 | } | ||
554 | if (empty($charset)) { | ||
555 | $charset = html_extract_charset($data); | ||
556 | } | ||
557 | if (empty($title)) { | ||
558 | $title = html_extract_title($data); | ||
559 | $foundChunk = ! empty($title) ? $currentChunk : $foundChunk; | ||
560 | } | ||
561 | if ($retrieveDescription && empty($description)) { | ||
562 | $description = html_extract_tag('description', $data); | ||
563 | $foundChunk = ! empty($description) ? $currentChunk : $foundChunk; | ||
564 | } | ||
565 | if ($retrieveDescription && empty($keywords)) { | ||
566 | $keywords = html_extract_tag('keywords', $data); | ||
567 | if (! empty($keywords)) { | ||
568 | $foundChunk = $currentChunk; | ||
569 | // Keywords use the format tag1, tag2 multiple words, tag | ||
570 | // So we format them to match Shaarli's separator and glue multiple words with '-' | ||
571 | $keywords = implode(' ', array_map(function($keyword) { | ||
572 | return implode('-', preg_split('/\s+/', trim($keyword))); | ||
573 | }, explode(',', $keywords))); | ||
574 | } | ||
575 | } | ||
576 | |||
577 | // We got everything we want, stop the download. | ||
578 | // If we already found either the title, description or keywords, | ||
579 | // it's highly unlikely that we'll found the other metas further than | ||
580 | // in the same chunk of data or the next one. So we also stop the download after that. | ||
581 | if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null | ||
582 | && (! $retrieveDescription | ||
583 | || $foundChunk < $currentChunk | ||
584 | || (!empty($title) && !empty($description) && !empty($keywords)) | ||
585 | ) | ||
586 | ) { | ||
587 | return false; | ||
588 | } | ||
589 | |||
590 | return strlen($data); | ||
591 | }; | ||
592 | } | ||