diff options
author | ArthurHoaro <arthur@hoa.ro> | 2020-06-06 14:01:03 +0200 |
---|---|---|
committer | ArthurHoaro <arthur@hoa.ro> | 2020-07-23 21:19:21 +0200 |
commit | c22fa57a5505fe95fd01860e3d3dfbb089f869cd (patch) | |
tree | a72b57e49b7b2b995ace278bad00fc47d5b6d61d | |
parent | 8eac2e54882d8adae8cbb45386dca1b465242632 (diff) | |
download | Shaarli-c22fa57a5505fe95fd01860e3d3dfbb089f869cd.tar.gz Shaarli-c22fa57a5505fe95fd01860e3d3dfbb089f869cd.tar.zst Shaarli-c22fa57a5505fe95fd01860e3d3dfbb089f869cd.zip |
Handle shaare creation/edition/deletion through Slim controllers
20 files changed, 1121 insertions, 280 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 | } | ||
diff --git a/doc/md/Translations.md b/doc/md/Translations.md index 9a16075a..5c775d30 100644 --- a/doc/md/Translations.md +++ b/doc/md/Translations.md | |||
@@ -32,7 +32,7 @@ Here is a list : | |||
32 | ``` | 32 | ``` |
33 | http://<replace_domain>/ | 33 | http://<replace_domain>/ |
34 | http://<replace_domain>/?nonope | 34 | http://<replace_domain>/?nonope |
35 | http://<replace_domain>/?do=addlink | 35 | http://<replace_domain>/add-shaare |
36 | http://<replace_domain>/?do=changepasswd | 36 | http://<replace_domain>/?do=changepasswd |
37 | http://<replace_domain>/?do=changetag | 37 | http://<replace_domain>/?do=changetag |
38 | http://<replace_domain>/configure | 38 | http://<replace_domain>/configure |
@@ -519,69 +519,20 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM | |||
519 | 519 | ||
520 | // -------- User wants to rename a tag or delete it | 520 | // -------- User wants to rename a tag or delete it |
521 | if ($targetPage == Router::$PAGE_CHANGETAG) { | 521 | if ($targetPage == Router::$PAGE_CHANGETAG) { |
522 | header('./manage-tags'); | 522 | header('Location: ./manage-tags'); |
523 | exit; | 523 | exit; |
524 | } | 524 | } |
525 | 525 | ||
526 | // -------- User wants to add a link without using the bookmarklet: Show form. | 526 | // -------- User wants to add a link without using the bookmarklet: Show form. |
527 | if ($targetPage == Router::$PAGE_ADDLINK) { | 527 | if ($targetPage == Router::$PAGE_ADDLINK) { |
528 | $PAGE->assign('pagetitle', t('Shaare a new link') .' - '. $conf->get('general.title', 'Shaarli')); | 528 | header('Location: ./shaare'); |
529 | $PAGE->renderPage('addlink'); | ||
530 | exit; | 529 | exit; |
531 | } | 530 | } |
532 | 531 | ||
533 | // -------- User clicked the "Save" button when editing a link: Save link to database. | 532 | // -------- User clicked the "Save" button when editing a link: Save link to database. |
534 | if (isset($_POST['save_edit'])) { | 533 | if (isset($_POST['save_edit'])) { |
535 | // Go away! | 534 | // This route is no longer supported in legacy mode |
536 | if (! $sessionManager->checkToken($_POST['token'])) { | 535 | header('Location: ./'); |
537 | die(t('Wrong token.')); | ||
538 | } | ||
539 | |||
540 | // lf_id should only be present if the link exists. | ||
541 | $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : null; | ||
542 | if ($id && $bookmarkService->exists($id)) { | ||
543 | // Edit | ||
544 | $bookmark = $bookmarkService->get($id); | ||
545 | } else { | ||
546 | // New link | ||
547 | $bookmark = new Bookmark(); | ||
548 | } | ||
549 | |||
550 | $bookmark->setTitle($_POST['lf_title']); | ||
551 | $bookmark->setDescription($_POST['lf_description']); | ||
552 | $bookmark->setUrl($_POST['lf_url'], $conf->get('security.allowed_protocols')); | ||
553 | $bookmark->setPrivate(isset($_POST['lf_private'])); | ||
554 | $bookmark->setTagsString($_POST['lf_tags']); | ||
555 | |||
556 | if ($conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE | ||
557 | && ! $bookmark->isNote() | ||
558 | ) { | ||
559 | $thumbnailer = new Thumbnailer($conf); | ||
560 | $bookmark->setThumbnail($thumbnailer->get($bookmark->getUrl())); | ||
561 | } | ||
562 | $bookmarkService->addOrSet($bookmark, false); | ||
563 | |||
564 | // To preserve backward compatibility with 3rd parties, plugins still use arrays | ||
565 | $factory = new FormatterFactory($conf, $loginManager->isLoggedIn()); | ||
566 | $formatter = $factory->getFormatter('raw'); | ||
567 | $data = $formatter->format($bookmark); | ||
568 | $pluginManager->executeHooks('save_link', $data); | ||
569 | |||
570 | $bookmark->fromArray($data); | ||
571 | $bookmarkService->set($bookmark); | ||
572 | |||
573 | // If we are called from the bookmarklet, we must close the popup: | ||
574 | if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { | ||
575 | echo '<script>self.close();</script>'; | ||
576 | exit; | ||
577 | } | ||
578 | |||
579 | $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?'; | ||
580 | $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link')); | ||
581 | // Scroll to the link which has been edited. | ||
582 | $location .= '#' . $bookmark->getShortUrl(); | ||
583 | // After saving the link, redirect to the page the user was on. | ||
584 | header('Location: '. $location); | ||
585 | exit; | 536 | exit; |
586 | } | 537 | } |
587 | 538 | ||
@@ -695,110 +646,13 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM | |||
695 | // -------- User clicked the "EDIT" button on a link: Display link edit form. | 646 | // -------- User clicked the "EDIT" button on a link: Display link edit form. |
696 | if (isset($_GET['edit_link'])) { | 647 | if (isset($_GET['edit_link'])) { |
697 | $id = (int) escape($_GET['edit_link']); | 648 | $id = (int) escape($_GET['edit_link']); |
698 | try { | 649 | header('Location: ./shaare-' . $id); |
699 | $link = $bookmarkService->get($id); // Read database | ||
700 | } catch (BookmarkNotFoundException $e) { | ||
701 | // Link not found in database. | ||
702 | header('Location: ?'); | ||
703 | exit; | ||
704 | } | ||
705 | |||
706 | $factory = new FormatterFactory($conf, $loginManager->isLoggedIn()); | ||
707 | $formatter = $factory->getFormatter('raw'); | ||
708 | $formattedLink = $formatter->format($link); | ||
709 | $tags = $bookmarkService->bookmarksCountPerTag(); | ||
710 | if ($conf->get('formatter') === 'markdown') { | ||
711 | $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; | ||
712 | } | ||
713 | $data = array( | ||
714 | 'link' => $formattedLink, | ||
715 | 'link_is_new' => false, | ||
716 | 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), | ||
717 | 'tags' => $tags, | ||
718 | ); | ||
719 | $pluginManager->executeHooks('render_editlink', $data); | ||
720 | |||
721 | foreach ($data as $key => $value) { | ||
722 | $PAGE->assign($key, $value); | ||
723 | } | ||
724 | |||
725 | $PAGE->assign('pagetitle', t('Edit') .' '. t('Shaare') .' - '. $conf->get('general.title', 'Shaarli')); | ||
726 | $PAGE->renderPage('editlink'); | ||
727 | exit; | 650 | exit; |
728 | } | 651 | } |
729 | 652 | ||
730 | // -------- User want to post a new link: Display link edit form. | 653 | // -------- User want to post a new link: Display link edit form. |
731 | if (isset($_GET['post'])) { | 654 | if (isset($_GET['post'])) { |
732 | $url = cleanup_url($_GET['post']); | 655 | header('Location: ./shaare?' . http_build_query($_GET)); |
733 | |||
734 | $link_is_new = false; | ||
735 | // Check if URL is not already in database (in this case, we will edit the existing link) | ||
736 | $bookmark = $bookmarkService->findByUrl($url); | ||
737 | if (! $bookmark) { | ||
738 | $link_is_new = true; | ||
739 | // Get title if it was provided in URL (by the bookmarklet). | ||
740 | $title = empty($_GET['title']) ? '' : escape($_GET['title']); | ||
741 | // Get description if it was provided in URL (by the bookmarklet). [Bronco added that] | ||
742 | $description = empty($_GET['description']) ? '' : escape($_GET['description']); | ||
743 | $tags = empty($_GET['tags']) ? '' : escape($_GET['tags']); | ||
744 | $private = !empty($_GET['private']) && $_GET['private'] === "1" ? 1 : 0; | ||
745 | |||
746 | // If this is an HTTP(S) link, we try go get the page to extract | ||
747 | // the title (otherwise we will to straight to the edit form.) | ||
748 | if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) { | ||
749 | $retrieveDescription = $conf->get('general.retrieve_description'); | ||
750 | // Short timeout to keep the application responsive | ||
751 | // The callback will fill $charset and $title with data from the downloaded page. | ||
752 | get_http_response( | ||
753 | $url, | ||
754 | $conf->get('general.download_timeout', 30), | ||
755 | $conf->get('general.download_max_size', 4194304), | ||
756 | get_curl_download_callback($charset, $title, $description, $tags, $retrieveDescription) | ||
757 | ); | ||
758 | if (! empty($title) && strtolower($charset) != 'utf-8') { | ||
759 | $title = mb_convert_encoding($title, 'utf-8', $charset); | ||
760 | } | ||
761 | } | ||
762 | |||
763 | if ($url == '') { | ||
764 | $title = $conf->get('general.default_note_title', t('Note: ')); | ||
765 | } | ||
766 | $url = escape($url); | ||
767 | $title = escape($title); | ||
768 | |||
769 | $link = [ | ||
770 | 'title' => $title, | ||
771 | 'url' => $url, | ||
772 | 'description' => $description, | ||
773 | 'tags' => $tags, | ||
774 | 'private' => $private, | ||
775 | ]; | ||
776 | } else { | ||
777 | $factory = new FormatterFactory($conf, $loginManager->isLoggedIn()); | ||
778 | $formatter = $factory->getFormatter('raw'); | ||
779 | $link = $formatter->format($bookmark); | ||
780 | } | ||
781 | |||
782 | $tags = $bookmarkService->bookmarksCountPerTag(); | ||
783 | if ($conf->get('formatter') === 'markdown') { | ||
784 | $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; | ||
785 | } | ||
786 | $data = [ | ||
787 | 'link' => $link, | ||
788 | 'link_is_new' => $link_is_new, | ||
789 | 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), | ||
790 | 'source' => (isset($_GET['source']) ? $_GET['source'] : ''), | ||
791 | 'tags' => $tags, | ||
792 | 'default_private_links' => $conf->get('privacy.default_private_links', false), | ||
793 | ]; | ||
794 | $pluginManager->executeHooks('render_editlink', $data); | ||
795 | |||
796 | foreach ($data as $key => $value) { | ||
797 | $PAGE->assign($key, $value); | ||
798 | } | ||
799 | |||
800 | $PAGE->assign('pagetitle', t('Shaare') .' - '. $conf->get('general.title', 'Shaarli')); | ||
801 | $PAGE->renderPage('editlink'); | ||
802 | exit; | 656 | exit; |
803 | } | 657 | } |
804 | 658 | ||
@@ -1351,19 +1205,29 @@ $app->group('', function () { | |||
1351 | $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save')->setName('saveConfigure'); | 1205 | $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save')->setName('saveConfigure'); |
1352 | $this->get('/manage-tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index')->setName('manageTag'); | 1206 | $this->get('/manage-tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index')->setName('manageTag'); |
1353 | $this->post('/manage-tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save')->setName('saveManageTag'); | 1207 | $this->post('/manage-tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save')->setName('saveManageTag'); |
1208 | $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\PostBookmarkController:addShaare')->setName('addShaare'); | ||
1209 | $this | ||
1210 | ->get('/shaare', '\Shaarli\Front\Controller\Admin\PostBookmarkController:displayCreateForm') | ||
1211 | ->setName('newShaare'); | ||
1212 | $this | ||
1213 | ->get('/shaare-{id}', '\Shaarli\Front\Controller\Admin\PostBookmarkController:displayEditForm') | ||
1214 | ->setName('editShaare'); | ||
1215 | $this | ||
1216 | ->post('/shaare', '\Shaarli\Front\Controller\Admin\PostBookmarkController:save') | ||
1217 | ->setName('saveShaare'); | ||
1218 | $this | ||
1219 | ->get('/delete-shaare', '\Shaarli\Front\Controller\Admin\PostBookmarkController:deleteBookmark') | ||
1220 | ->setName('deleteShaare'); | ||
1354 | 1221 | ||
1355 | $this | 1222 | $this |
1356 | ->get('/links-per-page', '\Shaarli\Front\Controller\Admin\SessionFilterController:linksPerPage') | 1223 | ->get('/links-per-page', '\Shaarli\Front\Controller\Admin\SessionFilterController:linksPerPage') |
1357 | ->setName('filter-links-per-page') | 1224 | ->setName('filter-links-per-page'); |
1358 | ; | ||
1359 | $this | 1225 | $this |
1360 | ->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility') | 1226 | ->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility') |
1361 | ->setName('visibility') | 1227 | ->setName('visibility'); |
1362 | ; | ||
1363 | $this | 1228 | $this |
1364 | ->get('/untagged-only', '\Shaarli\Front\Controller\Admin\SessionFilterController:untaggedOnly') | 1229 | ->get('/untagged-only', '\Shaarli\Front\Controller\Admin\SessionFilterController:untaggedOnly') |
1365 | ->setName('untagged-only') | 1230 | ->setName('untagged-only'); |
1366 | ; | ||
1367 | })->add('\Shaarli\Front\ShaarliMiddleware'); | 1231 | })->add('\Shaarli\Front\ShaarliMiddleware'); |
1368 | 1232 | ||
1369 | $response = $app->run(true); | 1233 | $response = $app->run(true); |
diff --git a/tests/container/ShaarliTestContainer.php b/tests/container/ShaarliTestContainer.php index 53197ae6..7dbe914c 100644 --- a/tests/container/ShaarliTestContainer.php +++ b/tests/container/ShaarliTestContainer.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 | * Test helper allowing auto-completion for MockObjects. | 22 | * Test helper allowing auto-completion for MockObjects. |
@@ -31,6 +33,8 @@ use Shaarli\Security\SessionManager; | |||
31 | * @property MockObject|FormatterFactory $formatterFactory | 33 | * @property MockObject|FormatterFactory $formatterFactory |
32 | * @property MockObject|PageCacheManager $pageCacheManager | 34 | * @property MockObject|PageCacheManager $pageCacheManager |
33 | * @property MockObject|FeedBuilder $feedBuilder | 35 | * @property MockObject|FeedBuilder $feedBuilder |
36 | * @property MockObject|Thumbnailer $thumbnailer | ||
37 | * @property MockObject|HttpAccess $httpAccess | ||
34 | */ | 38 | */ |
35 | class ShaarliTestContainer extends ShaarliContainer | 39 | class ShaarliTestContainer extends ShaarliContainer |
36 | { | 40 | { |
diff --git a/tests/front/controller/admin/PostBookmarkControllerTest.php b/tests/front/controller/admin/PostBookmarkControllerTest.php new file mode 100644 index 00000000..f00a15c9 --- /dev/null +++ b/tests/front/controller/admin/PostBookmarkControllerTest.php | |||
@@ -0,0 +1,652 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use PHPUnit\Framework\TestCase; | ||
8 | use Shaarli\Bookmark\Bookmark; | ||
9 | use Shaarli\Config\ConfigManager; | ||
10 | use Shaarli\Front\Exception\WrongTokenException; | ||
11 | use Shaarli\Http\HttpAccess; | ||
12 | use Shaarli\Security\SessionManager; | ||
13 | use Shaarli\Thumbnailer; | ||
14 | use Slim\Http\Request; | ||
15 | use Slim\Http\Response; | ||
16 | use Slim\Http\Uri; | ||
17 | |||
18 | class PostBookmarkControllerTest extends TestCase | ||
19 | { | ||
20 | use FrontAdminControllerMockHelper; | ||
21 | |||
22 | /** @var PostBookmarkController */ | ||
23 | protected $controller; | ||
24 | |||
25 | public function setUp(): void | ||
26 | { | ||
27 | $this->createContainer(); | ||
28 | |||
29 | $this->container->httpAccess = $this->createMock(HttpAccess::class); | ||
30 | $this->controller = new PostBookmarkController($this->container); | ||
31 | } | ||
32 | |||
33 | /** | ||
34 | * Test displaying add link page | ||
35 | */ | ||
36 | public function testAddShaare(): void | ||
37 | { | ||
38 | $assignedVariables = []; | ||
39 | $this->assignTemplateVars($assignedVariables); | ||
40 | |||
41 | $request = $this->createMock(Request::class); | ||
42 | $response = new Response(); | ||
43 | |||
44 | $result = $this->controller->addShaare($request, $response); | ||
45 | |||
46 | static::assertSame(200, $result->getStatusCode()); | ||
47 | static::assertSame('addlink', (string) $result->getBody()); | ||
48 | |||
49 | static::assertSame('Shaare a new link - Shaarli', $assignedVariables['pagetitle']); | ||
50 | } | ||
51 | |||
52 | /** | ||
53 | * Test displaying bookmark create form | ||
54 | * Ensure that every step of the standard workflow works properly. | ||
55 | */ | ||
56 | public function testDisplayCreateFormWithUrl(): void | ||
57 | { | ||
58 | $this->container->environment = [ | ||
59 | 'HTTP_REFERER' => $referer = 'http://shaarli/subfolder/controller/?searchtag=abc' | ||
60 | ]; | ||
61 | |||
62 | $assignedVariables = []; | ||
63 | $this->assignTemplateVars($assignedVariables); | ||
64 | |||
65 | $url = 'http://url.tld/other?part=3&utm_ad=pay#hash'; | ||
66 | $expectedUrl = str_replace('&utm_ad=pay', '', $url); | ||
67 | $remoteTitle = 'Remote Title'; | ||
68 | $remoteDesc = 'Sometimes the meta description is relevant.'; | ||
69 | $remoteTags = 'abc def'; | ||
70 | |||
71 | $request = $this->createMock(Request::class); | ||
72 | $request->method('getParam')->willReturnCallback(function (string $key) use ($url): ?string { | ||
73 | return $key === 'post' ? $url : null; | ||
74 | }); | ||
75 | $response = new Response(); | ||
76 | |||
77 | $this->container->httpAccess | ||
78 | ->expects(static::once()) | ||
79 | ->method('getCurlDownloadCallback') | ||
80 | ->willReturnCallback( | ||
81 | function (&$charset, &$title, &$description, &$tags) use ( | ||
82 | $remoteTitle, | ||
83 | $remoteDesc, | ||
84 | $remoteTags | ||
85 | ): callable { | ||
86 | return function () use ( | ||
87 | &$charset, | ||
88 | &$title, | ||
89 | &$description, | ||
90 | &$tags, | ||
91 | $remoteTitle, | ||
92 | $remoteDesc, | ||
93 | $remoteTags | ||
94 | ): void { | ||
95 | $charset = 'ISO-8859-1'; | ||
96 | $title = $remoteTitle; | ||
97 | $description = $remoteDesc; | ||
98 | $tags = $remoteTags; | ||
99 | }; | ||
100 | } | ||
101 | ) | ||
102 | ; | ||
103 | $this->container->httpAccess | ||
104 | ->expects(static::once()) | ||
105 | ->method('getHttpResponse') | ||
106 | ->with($expectedUrl, 30, 4194304) | ||
107 | ->willReturnCallback(function($url, $timeout, $maxBytes, $callback): void { | ||
108 | $callback(); | ||
109 | }) | ||
110 | ; | ||
111 | |||
112 | $this->container->bookmarkService | ||
113 | ->expects(static::once()) | ||
114 | ->method('bookmarksCountPerTag') | ||
115 | ->willReturn($tags = ['tag1' => 2, 'tag2' => 1]) | ||
116 | ; | ||
117 | |||
118 | // Make sure that PluginManager hook is triggered | ||
119 | $this->container->pluginManager | ||
120 | ->expects(static::at(0)) | ||
121 | ->method('executeHooks') | ||
122 | ->willReturnCallback(function (string $hook, array $data) use ($remoteTitle, $remoteDesc): array { | ||
123 | static::assertSame('render_editlink', $hook); | ||
124 | static::assertSame($remoteTitle, $data['link']['title']); | ||
125 | static::assertSame($remoteDesc, $data['link']['description']); | ||
126 | |||
127 | return $data; | ||
128 | }) | ||
129 | ; | ||
130 | |||
131 | $result = $this->controller->displayCreateForm($request, $response); | ||
132 | |||
133 | static::assertSame(200, $result->getStatusCode()); | ||
134 | static::assertSame('editlink', (string) $result->getBody()); | ||
135 | |||
136 | static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']); | ||
137 | |||
138 | static::assertSame($expectedUrl, $assignedVariables['link']['url']); | ||
139 | static::assertSame($remoteTitle, $assignedVariables['link']['title']); | ||
140 | static::assertSame($remoteDesc, $assignedVariables['link']['description']); | ||
141 | static::assertSame($remoteTags, $assignedVariables['link']['tags']); | ||
142 | static::assertFalse($assignedVariables['link']['private']); | ||
143 | |||
144 | static::assertTrue($assignedVariables['link_is_new']); | ||
145 | static::assertSame($referer, $assignedVariables['http_referer']); | ||
146 | static::assertSame($tags, $assignedVariables['tags']); | ||
147 | static::assertArrayHasKey('source', $assignedVariables); | ||
148 | static::assertArrayHasKey('default_private_links', $assignedVariables); | ||
149 | } | ||
150 | |||
151 | /** | ||
152 | * Test displaying bookmark create form | ||
153 | * Ensure all available query parameters are handled properly. | ||
154 | */ | ||
155 | public function testDisplayCreateFormWithFullParameters(): void | ||
156 | { | ||
157 | $assignedVariables = []; | ||
158 | $this->assignTemplateVars($assignedVariables); | ||
159 | |||
160 | $parameters = [ | ||
161 | 'post' => 'http://url.tld/other?part=3&utm_ad=pay#hash', | ||
162 | 'title' => 'Provided Title', | ||
163 | 'description' => 'Provided description.', | ||
164 | 'tags' => 'abc def', | ||
165 | 'private' => '1', | ||
166 | 'source' => 'apps', | ||
167 | ]; | ||
168 | $expectedUrl = str_replace('&utm_ad=pay', '', $parameters['post']); | ||
169 | |||
170 | $request = $this->createMock(Request::class); | ||
171 | $request | ||
172 | ->method('getParam') | ||
173 | ->willReturnCallback(function (string $key) use ($parameters): ?string { | ||
174 | return $parameters[$key] ?? null; | ||
175 | }); | ||
176 | $response = new Response(); | ||
177 | |||
178 | $result = $this->controller->displayCreateForm($request, $response); | ||
179 | |||
180 | static::assertSame(200, $result->getStatusCode()); | ||
181 | static::assertSame('editlink', (string) $result->getBody()); | ||
182 | |||
183 | static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']); | ||
184 | |||
185 | static::assertSame($expectedUrl, $assignedVariables['link']['url']); | ||
186 | static::assertSame($parameters['title'], $assignedVariables['link']['title']); | ||
187 | static::assertSame($parameters['description'], $assignedVariables['link']['description']); | ||
188 | static::assertSame($parameters['tags'], $assignedVariables['link']['tags']); | ||
189 | static::assertTrue($assignedVariables['link']['private']); | ||
190 | static::assertTrue($assignedVariables['link_is_new']); | ||
191 | static::assertSame($parameters['source'], $assignedVariables['source']); | ||
192 | } | ||
193 | |||
194 | /** | ||
195 | * Test displaying bookmark create form | ||
196 | * Without any parameter. | ||
197 | */ | ||
198 | public function testDisplayCreateFormEmpty(): void | ||
199 | { | ||
200 | $assignedVariables = []; | ||
201 | $this->assignTemplateVars($assignedVariables); | ||
202 | |||
203 | $request = $this->createMock(Request::class); | ||
204 | $response = new Response(); | ||
205 | |||
206 | $this->container->httpAccess->expects(static::never())->method('getHttpResponse'); | ||
207 | $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback'); | ||
208 | |||
209 | $result = $this->controller->displayCreateForm($request, $response); | ||
210 | |||
211 | static::assertSame(200, $result->getStatusCode()); | ||
212 | static::assertSame('editlink', (string) $result->getBody()); | ||
213 | static::assertSame('', $assignedVariables['link']['url']); | ||
214 | static::assertSame('Note: ', $assignedVariables['link']['title']); | ||
215 | static::assertSame('', $assignedVariables['link']['description']); | ||
216 | static::assertSame('', $assignedVariables['link']['tags']); | ||
217 | static::assertFalse($assignedVariables['link']['private']); | ||
218 | static::assertTrue($assignedVariables['link_is_new']); | ||
219 | } | ||
220 | |||
221 | /** | ||
222 | * Test displaying bookmark create form | ||
223 | * URL not using HTTP protocol: do not try to retrieve the title | ||
224 | */ | ||
225 | public function testDisplayCreateFormNotHttp(): void | ||
226 | { | ||
227 | $assignedVariables = []; | ||
228 | $this->assignTemplateVars($assignedVariables); | ||
229 | |||
230 | $url = 'magnet://kubuntu.torrent'; | ||
231 | $request = $this->createMock(Request::class); | ||
232 | $request | ||
233 | ->method('getParam') | ||
234 | ->willReturnCallback(function (string $key) use ($url): ?string { | ||
235 | return $key === 'post' ? $url : null; | ||
236 | }); | ||
237 | $response = new Response(); | ||
238 | |||
239 | $this->container->httpAccess->expects(static::never())->method('getHttpResponse'); | ||
240 | $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback'); | ||
241 | |||
242 | $result = $this->controller->displayCreateForm($request, $response); | ||
243 | |||
244 | static::assertSame(200, $result->getStatusCode()); | ||
245 | static::assertSame('editlink', (string) $result->getBody()); | ||
246 | static::assertSame($url, $assignedVariables['link']['url']); | ||
247 | static::assertTrue($assignedVariables['link_is_new']); | ||
248 | } | ||
249 | |||
250 | /** | ||
251 | * Test displaying bookmark create form | ||
252 | * When markdown formatter is enabled, the no markdown tag should be added to existing tags. | ||
253 | */ | ||
254 | public function testDisplayCreateFormWithMarkdownEnabled(): void | ||
255 | { | ||
256 | $assignedVariables = []; | ||
257 | $this->assignTemplateVars($assignedVariables); | ||
258 | |||
259 | $this->container->conf = $this->createMock(ConfigManager::class); | ||
260 | $this->container->conf | ||
261 | ->expects(static::atLeastOnce()) | ||
262 | ->method('get')->willReturnCallback(function (string $key): ?string { | ||
263 | if ($key === 'formatter') { | ||
264 | return 'markdown'; | ||
265 | } | ||
266 | |||
267 | return $key; | ||
268 | }) | ||
269 | ; | ||
270 | |||
271 | $request = $this->createMock(Request::class); | ||
272 | $response = new Response(); | ||
273 | |||
274 | $result = $this->controller->displayCreateForm($request, $response); | ||
275 | |||
276 | static::assertSame(200, $result->getStatusCode()); | ||
277 | static::assertSame('editlink', (string) $result->getBody()); | ||
278 | static::assertSame(['nomarkdown' => 1], $assignedVariables['tags']); | ||
279 | } | ||
280 | |||
281 | /** | ||
282 | * Test displaying bookmark create form | ||
283 | * When an existing URL is submitted, we want to edit the existing link. | ||
284 | */ | ||
285 | public function testDisplayCreateFormWithExistingUrl(): void | ||
286 | { | ||
287 | $assignedVariables = []; | ||
288 | $this->assignTemplateVars($assignedVariables); | ||
289 | |||
290 | $url = 'http://url.tld/other?part=3&utm_ad=pay#hash'; | ||
291 | $expectedUrl = str_replace('&utm_ad=pay', '', $url); | ||
292 | |||
293 | $request = $this->createMock(Request::class); | ||
294 | $request | ||
295 | ->method('getParam') | ||
296 | ->willReturnCallback(function (string $key) use ($url): ?string { | ||
297 | return $key === 'post' ? $url : null; | ||
298 | }); | ||
299 | $response = new Response(); | ||
300 | |||
301 | $this->container->httpAccess->expects(static::never())->method('getHttpResponse'); | ||
302 | $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback'); | ||
303 | |||
304 | $this->container->bookmarkService | ||
305 | ->expects(static::once()) | ||
306 | ->method('findByUrl') | ||
307 | ->with($expectedUrl) | ||
308 | ->willReturn( | ||
309 | (new Bookmark()) | ||
310 | ->setId($id = 23) | ||
311 | ->setUrl($expectedUrl) | ||
312 | ->setTitle($title = 'Bookmark Title') | ||
313 | ->setDescription($description = 'Bookmark description.') | ||
314 | ->setTags($tags = ['abc', 'def']) | ||
315 | ->setPrivate(true) | ||
316 | ->setCreated($createdAt = new \DateTime('2020-06-10 18:45:44')) | ||
317 | ) | ||
318 | ; | ||
319 | |||
320 | $result = $this->controller->displayCreateForm($request, $response); | ||
321 | |||
322 | static::assertSame(200, $result->getStatusCode()); | ||
323 | static::assertSame('editlink', (string) $result->getBody()); | ||
324 | |||
325 | static::assertSame('Edit Shaare - Shaarli', $assignedVariables['pagetitle']); | ||
326 | static::assertFalse($assignedVariables['link_is_new']); | ||
327 | |||
328 | static::assertSame($id, $assignedVariables['link']['id']); | ||
329 | static::assertSame($expectedUrl, $assignedVariables['link']['url']); | ||
330 | static::assertSame($title, $assignedVariables['link']['title']); | ||
331 | static::assertSame($description, $assignedVariables['link']['description']); | ||
332 | static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']); | ||
333 | static::assertTrue($assignedVariables['link']['private']); | ||
334 | static::assertSame($createdAt, $assignedVariables['link']['created']); | ||
335 | } | ||
336 | |||
337 | /** | ||
338 | * Test displaying bookmark edit form | ||
339 | * When an existing ID is provided, ensure that default workflow works properly. | ||
340 | */ | ||
341 | public function testDisplayEditFormDefault(): void | ||
342 | { | ||
343 | $assignedVariables = []; | ||
344 | $this->assignTemplateVars($assignedVariables); | ||
345 | |||
346 | $id = 11; | ||
347 | |||
348 | $request = $this->createMock(Request::class); | ||
349 | $response = new Response(); | ||
350 | |||
351 | $this->container->httpAccess->expects(static::never())->method('getHttpResponse'); | ||
352 | $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback'); | ||
353 | |||
354 | $this->container->bookmarkService | ||
355 | ->expects(static::once()) | ||
356 | ->method('get') | ||
357 | ->with($id) | ||
358 | ->willReturn( | ||
359 | (new Bookmark()) | ||
360 | ->setId($id) | ||
361 | ->setUrl($url = 'http://domain.tld') | ||
362 | ->setTitle($title = 'Bookmark Title') | ||
363 | ->setDescription($description = 'Bookmark description.') | ||
364 | ->setTags($tags = ['abc', 'def']) | ||
365 | ->setPrivate(true) | ||
366 | ->setCreated($createdAt = new \DateTime('2020-06-10 18:45:44')) | ||
367 | ) | ||
368 | ; | ||
369 | |||
370 | $result = $this->controller->displayEditForm($request, $response, ['id' => (string) $id]); | ||
371 | |||
372 | static::assertSame(200, $result->getStatusCode()); | ||
373 | static::assertSame('editlink', (string) $result->getBody()); | ||
374 | |||
375 | static::assertSame('Edit Shaare - Shaarli', $assignedVariables['pagetitle']); | ||
376 | static::assertFalse($assignedVariables['link_is_new']); | ||
377 | |||
378 | static::assertSame($id, $assignedVariables['link']['id']); | ||
379 | static::assertSame($url, $assignedVariables['link']['url']); | ||
380 | static::assertSame($title, $assignedVariables['link']['title']); | ||
381 | static::assertSame($description, $assignedVariables['link']['description']); | ||
382 | static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']); | ||
383 | static::assertTrue($assignedVariables['link']['private']); | ||
384 | static::assertSame($createdAt, $assignedVariables['link']['created']); | ||
385 | } | ||
386 | |||
387 | /** | ||
388 | * Test save a new bookmark | ||
389 | */ | ||
390 | public function testSaveBookmark(): void | ||
391 | { | ||
392 | $id = 21; | ||
393 | $parameters = [ | ||
394 | 'lf_url' => 'http://url.tld/other?part=3#hash', | ||
395 | 'lf_title' => 'Provided Title', | ||
396 | 'lf_description' => 'Provided description.', | ||
397 | 'lf_tags' => 'abc def', | ||
398 | 'lf_private' => '1', | ||
399 | 'returnurl' => 'http://shaarli.tld/subfolder/add-shaare' | ||
400 | ]; | ||
401 | |||
402 | $request = $this->createMock(Request::class); | ||
403 | $request | ||
404 | ->method('getParam') | ||
405 | ->willReturnCallback(function (string $key) use ($parameters): ?string { | ||
406 | return $parameters[$key] ?? null; | ||
407 | }) | ||
408 | ; | ||
409 | $request->method('getUri')->willReturnCallback(function (): Uri { | ||
410 | $uri = $this->createMock(Uri::class); | ||
411 | $uri->method('getBasePath')->willReturn('/subfolder'); | ||
412 | |||
413 | return $uri; | ||
414 | }); | ||
415 | $response = new Response(); | ||
416 | |||
417 | $checkBookmark = function (Bookmark $bookmark) use ($parameters) { | ||
418 | static::assertSame($parameters['lf_url'], $bookmark->getUrl()); | ||
419 | static::assertSame($parameters['lf_title'], $bookmark->getTitle()); | ||
420 | static::assertSame($parameters['lf_description'], $bookmark->getDescription()); | ||
421 | static::assertSame($parameters['lf_tags'], $bookmark->getTagsString()); | ||
422 | static::assertTrue($bookmark->isPrivate()); | ||
423 | }; | ||
424 | |||
425 | $this->container->bookmarkService | ||
426 | ->expects(static::once()) | ||
427 | ->method('addOrSet') | ||
428 | ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void { | ||
429 | static::assertFalse($save); | ||
430 | |||
431 | $checkBookmark($bookmark); | ||
432 | |||
433 | $bookmark->setId($id); | ||
434 | }) | ||
435 | ; | ||
436 | $this->container->bookmarkService | ||
437 | ->expects(static::once()) | ||
438 | ->method('set') | ||
439 | ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void { | ||
440 | static::assertTrue($save); | ||
441 | |||
442 | $checkBookmark($bookmark); | ||
443 | |||
444 | static::assertSame($id, $bookmark->getId()); | ||
445 | }) | ||
446 | ; | ||
447 | |||
448 | // Make sure that PluginManager hook is triggered | ||
449 | $this->container->pluginManager | ||
450 | ->expects(static::at(0)) | ||
451 | ->method('executeHooks') | ||
452 | ->willReturnCallback(function (string $hook, array $data) use ($parameters, $id): array { | ||
453 | static::assertSame('save_link', $hook); | ||
454 | |||
455 | static::assertSame($id, $data['id']); | ||
456 | static::assertSame($parameters['lf_url'], $data['url']); | ||
457 | static::assertSame($parameters['lf_title'], $data['title']); | ||
458 | static::assertSame($parameters['lf_description'], $data['description']); | ||
459 | static::assertSame($parameters['lf_tags'], $data['tags']); | ||
460 | static::assertTrue($data['private']); | ||
461 | |||
462 | return $data; | ||
463 | }) | ||
464 | ; | ||
465 | |||
466 | $result = $this->controller->save($request, $response); | ||
467 | |||
468 | static::assertSame(302, $result->getStatusCode()); | ||
469 | static::assertRegExp('@/subfolder/#\w{6}@', $result->getHeader('location')[0]); | ||
470 | } | ||
471 | |||
472 | |||
473 | /** | ||
474 | * Test save an existing bookmark | ||
475 | */ | ||
476 | public function testSaveExistingBookmark(): void | ||
477 | { | ||
478 | $id = 21; | ||
479 | $parameters = [ | ||
480 | 'lf_id' => (string) $id, | ||
481 | 'lf_url' => 'http://url.tld/other?part=3#hash', | ||
482 | 'lf_title' => 'Provided Title', | ||
483 | 'lf_description' => 'Provided description.', | ||
484 | 'lf_tags' => 'abc def', | ||
485 | 'lf_private' => '1', | ||
486 | 'returnurl' => 'http://shaarli.tld/subfolder/?page=2' | ||
487 | ]; | ||
488 | |||
489 | $request = $this->createMock(Request::class); | ||
490 | $request | ||
491 | ->method('getParam') | ||
492 | ->willReturnCallback(function (string $key) use ($parameters): ?string { | ||
493 | return $parameters[$key] ?? null; | ||
494 | }) | ||
495 | ; | ||
496 | $request->method('getUri')->willReturnCallback(function (): Uri { | ||
497 | $uri = $this->createMock(Uri::class); | ||
498 | $uri->method('getBasePath')->willReturn('/subfolder'); | ||
499 | |||
500 | return $uri; | ||
501 | }); | ||
502 | $response = new Response(); | ||
503 | |||
504 | $checkBookmark = function (Bookmark $bookmark) use ($parameters, $id) { | ||
505 | static::assertSame($id, $bookmark->getId()); | ||
506 | static::assertSame($parameters['lf_url'], $bookmark->getUrl()); | ||
507 | static::assertSame($parameters['lf_title'], $bookmark->getTitle()); | ||
508 | static::assertSame($parameters['lf_description'], $bookmark->getDescription()); | ||
509 | static::assertSame($parameters['lf_tags'], $bookmark->getTagsString()); | ||
510 | static::assertTrue($bookmark->isPrivate()); | ||
511 | }; | ||
512 | |||
513 | $this->container->bookmarkService->expects(static::atLeastOnce())->method('exists')->willReturn(true); | ||
514 | $this->container->bookmarkService | ||
515 | ->expects(static::once()) | ||
516 | ->method('get') | ||
517 | ->willReturn((new Bookmark())->setId($id)->setUrl('http://other.url')) | ||
518 | ; | ||
519 | $this->container->bookmarkService | ||
520 | ->expects(static::once()) | ||
521 | ->method('addOrSet') | ||
522 | ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void { | ||
523 | static::assertFalse($save); | ||
524 | |||
525 | $checkBookmark($bookmark); | ||
526 | }) | ||
527 | ; | ||
528 | $this->container->bookmarkService | ||
529 | ->expects(static::once()) | ||
530 | ->method('set') | ||
531 | ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void { | ||
532 | static::assertTrue($save); | ||
533 | |||
534 | $checkBookmark($bookmark); | ||
535 | |||
536 | static::assertSame($id, $bookmark->getId()); | ||
537 | }) | ||
538 | ; | ||
539 | |||
540 | // Make sure that PluginManager hook is triggered | ||
541 | $this->container->pluginManager | ||
542 | ->expects(static::at(0)) | ||
543 | ->method('executeHooks') | ||
544 | ->willReturnCallback(function (string $hook, array $data) use ($parameters, $id): array { | ||
545 | static::assertSame('save_link', $hook); | ||
546 | |||
547 | static::assertSame($id, $data['id']); | ||
548 | static::assertSame($parameters['lf_url'], $data['url']); | ||
549 | static::assertSame($parameters['lf_title'], $data['title']); | ||
550 | static::assertSame($parameters['lf_description'], $data['description']); | ||
551 | static::assertSame($parameters['lf_tags'], $data['tags']); | ||
552 | static::assertTrue($data['private']); | ||
553 | |||
554 | return $data; | ||
555 | }) | ||
556 | ; | ||
557 | |||
558 | $result = $this->controller->save($request, $response); | ||
559 | |||
560 | static::assertSame(302, $result->getStatusCode()); | ||
561 | static::assertRegExp('@/subfolder/\?page=2#\w{6}@', $result->getHeader('location')[0]); | ||
562 | } | ||
563 | |||
564 | /** | ||
565 | * Test save a bookmark - try to retrieve the thumbnail | ||
566 | */ | ||
567 | public function testSaveBookmarkWithThumbnail(): void | ||
568 | { | ||
569 | $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash']; | ||
570 | |||
571 | $request = $this->createMock(Request::class); | ||
572 | $request | ||
573 | ->method('getParam') | ||
574 | ->willReturnCallback(function (string $key) use ($parameters): ?string { | ||
575 | return $parameters[$key] ?? null; | ||
576 | }) | ||
577 | ; | ||
578 | $request->method('getUri')->willReturnCallback(function (): Uri { | ||
579 | $uri = $this->createMock(Uri::class); | ||
580 | $uri->method('getBasePath')->willReturn('/subfolder'); | ||
581 | |||
582 | return $uri; | ||
583 | }); | ||
584 | $response = new Response(); | ||
585 | |||
586 | $this->container->conf = $this->createMock(ConfigManager::class); | ||
587 | $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { | ||
588 | return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default; | ||
589 | }); | ||
590 | |||
591 | $this->container->thumbnailer = $this->createMock(Thumbnailer::class); | ||
592 | $this->container->thumbnailer | ||
593 | ->expects(static::once()) | ||
594 | ->method('get') | ||
595 | ->with($parameters['lf_url']) | ||
596 | ->willReturn($thumb = 'http://thumb.url') | ||
597 | ; | ||
598 | |||
599 | $this->container->bookmarkService | ||
600 | ->expects(static::once()) | ||
601 | ->method('addOrSet') | ||
602 | ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($thumb): void { | ||
603 | static::assertSame($thumb, $bookmark->getThumbnail()); | ||
604 | }) | ||
605 | ; | ||
606 | |||
607 | $result = $this->controller->save($request, $response); | ||
608 | |||
609 | static::assertSame(302, $result->getStatusCode()); | ||
610 | } | ||
611 | |||
612 | /** | ||
613 | * Change the password with a wrong existing password | ||
614 | */ | ||
615 | public function testSaveBookmarkFromBookmarklet(): void | ||
616 | { | ||
617 | $parameters = ['source' => 'bookmarklet']; | ||
618 | |||
619 | $request = $this->createMock(Request::class); | ||
620 | $request | ||
621 | ->method('getParam') | ||
622 | ->willReturnCallback(function (string $key) use ($parameters): ?string { | ||
623 | return $parameters[$key] ?? null; | ||
624 | }) | ||
625 | ; | ||
626 | $response = new Response(); | ||
627 | |||
628 | $result = $this->controller->save($request, $response); | ||
629 | |||
630 | static::assertSame(200, $result->getStatusCode()); | ||
631 | static::assertSame('<script>self.close();</script>', (string) $result->getBody()); | ||
632 | } | ||
633 | |||
634 | /** | ||
635 | * Change the password with a wrong existing password | ||
636 | */ | ||
637 | public function testSaveBookmarkWrongToken(): void | ||
638 | { | ||
639 | $this->container->sessionManager = $this->createMock(SessionManager::class); | ||
640 | $this->container->sessionManager->method('checkToken')->willReturn(false); | ||
641 | |||
642 | $this->container->bookmarkService->expects(static::never())->method('addOrSet'); | ||
643 | $this->container->bookmarkService->expects(static::never())->method('set'); | ||
644 | |||
645 | $request = $this->createMock(Request::class); | ||
646 | $response = new Response(); | ||
647 | |||
648 | $this->expectException(WrongTokenException::class); | ||
649 | |||
650 | $this->controller->save($request, $response); | ||
651 | } | ||
652 | } | ||
diff --git a/tpl/default/addlink.html b/tpl/default/addlink.html index b4b4a0ec..999d2f4d 100644 --- a/tpl/default/addlink.html +++ b/tpl/default/addlink.html | |||
@@ -9,7 +9,7 @@ | |||
9 | <div class="pure-u-lg-1-3 pure-u-1-24"></div> | 9 | <div class="pure-u-lg-1-3 pure-u-1-24"></div> |
10 | <div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24"> | 10 | <div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24"> |
11 | <h2 class="window-title">{"Shaare a new link"|t}</h2> | 11 | <h2 class="window-title">{"Shaare a new link"|t}</h2> |
12 | <form method="GET" action="#" name="addform" class="addform"> | 12 | <form method="GET" action="./shaare" name="addform" class="addform"> |
13 | <div> | 13 | <div> |
14 | <label for="shaare">{'URL or leave empty to post a note'|t}</label> | 14 | <label for="shaare">{'URL or leave empty to post a note'|t}</label> |
15 | <input type="text" name="post" id="shaare" class="autofocus"> | 15 | <input type="text" name="post" id="shaare" class="autofocus"> |
diff --git a/tpl/default/editlink.html b/tpl/default/editlink.html index d16059a3..9f6d6b74 100644 --- a/tpl/default/editlink.html +++ b/tpl/default/editlink.html | |||
@@ -7,7 +7,11 @@ | |||
7 | {include="page.header"} | 7 | {include="page.header"} |
8 | <div id="editlinkform" class="edit-link-container" class="pure-g"> | 8 | <div id="editlinkform" class="edit-link-container" class="pure-g"> |
9 | <div class="pure-u-lg-1-5 pure-u-1-24"></div> | 9 | <div class="pure-u-lg-1-5 pure-u-1-24"></div> |
10 | <form method="post" name="linkform" class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light"> | 10 | <form method="post" |
11 | name="linkform" | ||
12 | action="./shaare" | ||
13 | class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light" | ||
14 | > | ||
11 | <h2 class="window-title"> | 15 | <h2 class="window-title"> |
12 | {if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if} | 16 | {if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if} |
13 | </h2> | 17 | </h2> |
diff --git a/tpl/default/page.header.html b/tpl/default/page.header.html index bde5036d..cf59e89d 100644 --- a/tpl/default/page.header.html +++ b/tpl/default/page.header.html | |||
@@ -21,7 +21,7 @@ | |||
21 | </li> | 21 | </li> |
22 | {if="$is_logged_in || $openshaarli"} | 22 | {if="$is_logged_in || $openshaarli"} |
23 | <li class="pure-menu-item"> | 23 | <li class="pure-menu-item"> |
24 | <a href="./?do=addlink" class="pure-menu-link" id="shaarli-menu-shaare"> | 24 | <a href="./add-shaare" class="pure-menu-link" id="shaarli-menu-shaare"> |
25 | <i class="fa fa-plus" aria-hidden="true"></i> {'Shaare'|t} | 25 | <i class="fa fa-plus" aria-hidden="true"></i> {'Shaare'|t} |
26 | </a> | 26 | </a> |
27 | </li> | 27 | </li> |
diff --git a/tpl/vintage/addlink.html b/tpl/vintage/addlink.html index da50f45e..13dbb36e 100644 --- a/tpl/vintage/addlink.html +++ b/tpl/vintage/addlink.html | |||
@@ -5,7 +5,7 @@ | |||
5 | <div id="pageheader"> | 5 | <div id="pageheader"> |
6 | {include="page.header"} | 6 | {include="page.header"} |
7 | <div id="headerform"> | 7 | <div id="headerform"> |
8 | <form method="GET" action="" name="addform" class="addform"> | 8 | <form method="GET" action="./shaare" name="addform" class="addform"> |
9 | <input type="text" name="post" class="linkurl"> | 9 | <input type="text" name="post" class="linkurl"> |
10 | <input type="submit" value="Add link" class="bigbutton"> | 10 | <input type="submit" value="Add link" class="bigbutton"> |
11 | </form> | 11 | </form> |
diff --git a/tpl/vintage/page.header.html b/tpl/vintage/page.header.html index c265d6d0..8f9b6cc5 100644 --- a/tpl/vintage/page.header.html +++ b/tpl/vintage/page.header.html | |||
@@ -20,10 +20,10 @@ | |||
20 | {if="$is_logged_in"} | 20 | {if="$is_logged_in"} |
21 | <li><a href="./logout">Logout</a></li> | 21 | <li><a href="./logout">Logout</a></li> |
22 | <li><a href="./tools">Tools</a></li> | 22 | <li><a href="./tools">Tools</a></li> |
23 | <li><a href="?do=addlink">Add link</a></li> | 23 | <li><a href="./add-shaare">Add link</a></li> |
24 | {elseif="$openshaarli"} | 24 | {elseif="$openshaarli"} |
25 | <li><a href="./tools">Tools</a></li> | 25 | <li><a href="./tools">Tools</a></li> |
26 | <li><a href="./?do=addlink">Add link</a></li> | 26 | <li><a href="./add-shaare">Add link</a></li> |
27 | {else} | 27 | {else} |
28 | <li><a href="./login">Login</a></li> | 28 | <li><a href="./login">Login</a></li> |
29 | {/if} | 29 | {/if} |