diff options
Diffstat (limited to 'application/front/controller/admin')
15 files changed, 694 insertions, 388 deletions
diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php index 0ed7ad81..dc421661 100644 --- a/application/front/controller/admin/ConfigureController.php +++ b/application/front/controller/admin/ConfigureController.php | |||
@@ -51,7 +51,10 @@ class ConfigureController extends ShaarliAdminController | |||
51 | $this->assignView('languages', Languages::getAvailableLanguages()); | 51 | $this->assignView('languages', Languages::getAvailableLanguages()); |
52 | $this->assignView('gd_enabled', extension_loaded('gd')); | 52 | $this->assignView('gd_enabled', extension_loaded('gd')); |
53 | $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)); | 53 | $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)); |
54 | $this->assignView('pagetitle', t('Configure') .' - '. $this->container->conf->get('general.title', 'Shaarli')); | 54 | $this->assignView( |
55 | 'pagetitle', | ||
56 | t('Configure') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') | ||
57 | ); | ||
55 | 58 | ||
56 | return $response->write($this->render(TemplatePage::CONFIGURE)); | 59 | return $response->write($this->render(TemplatePage::CONFIGURE)); |
57 | } | 60 | } |
@@ -95,12 +98,15 @@ class ConfigureController extends ShaarliAdminController | |||
95 | } | 98 | } |
96 | 99 | ||
97 | $thumbnailsMode = extension_loaded('gd') ? $request->getParam('enableThumbnails') : Thumbnailer::MODE_NONE; | 100 | $thumbnailsMode = extension_loaded('gd') ? $request->getParam('enableThumbnails') : Thumbnailer::MODE_NONE; |
98 | if ($thumbnailsMode !== Thumbnailer::MODE_NONE | 101 | if ( |
102 | $thumbnailsMode !== Thumbnailer::MODE_NONE | ||
99 | && $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) | 103 | && $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) |
100 | ) { | 104 | ) { |
101 | $this->saveWarningMessage( | 105 | $this->saveWarningMessage( |
102 | t('You have enabled or changed thumbnails mode.') . | 106 | t('You have enabled or changed thumbnails mode.') . |
103 | '<a href="'. $this->container->basePath .'/admin/thumbnails">' . t('Please synchronize them.') .'</a>' | 107 | '<a href="' . $this->container->basePath . '/admin/thumbnails">' . |
108 | t('Please synchronize them.') . | ||
109 | '</a>' | ||
104 | ); | 110 | ); |
105 | } | 111 | } |
106 | $this->container->conf->set('thumbnails.mode', $thumbnailsMode); | 112 | $this->container->conf->set('thumbnails.mode', $thumbnailsMode); |
diff --git a/application/front/controller/admin/ExportController.php b/application/front/controller/admin/ExportController.php index 2be957fa..f01d7e9b 100644 --- a/application/front/controller/admin/ExportController.php +++ b/application/front/controller/admin/ExportController.php | |||
@@ -23,7 +23,7 @@ class ExportController extends ShaarliAdminController | |||
23 | */ | 23 | */ |
24 | public function index(Request $request, Response $response): Response | 24 | public function index(Request $request, Response $response): Response |
25 | { | 25 | { |
26 | $this->assignView('pagetitle', t('Export') .' - '. $this->container->conf->get('general.title', 'Shaarli')); | 26 | $this->assignView('pagetitle', t('Export') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')); |
27 | 27 | ||
28 | return $response->write($this->render(TemplatePage::EXPORT)); | 28 | return $response->write($this->render(TemplatePage::EXPORT)); |
29 | } | 29 | } |
@@ -68,7 +68,7 @@ class ExportController extends ShaarliAdminController | |||
68 | $response = $response->withHeader('Content-Type', 'text/html; charset=utf-8'); | 68 | $response = $response->withHeader('Content-Type', 'text/html; charset=utf-8'); |
69 | $response = $response->withHeader( | 69 | $response = $response->withHeader( |
70 | 'Content-disposition', | 70 | 'Content-disposition', |
71 | 'attachment; filename=bookmarks_'.$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html' | 71 | 'attachment; filename=bookmarks_' . $selection . '_' . $now->format(Bookmark::LINK_DATE_FORMAT) . '.html' |
72 | ); | 72 | ); |
73 | 73 | ||
74 | $this->assignView('date', $now->format(DateTime::RFC822)); | 74 | $this->assignView('date', $now->format(DateTime::RFC822)); |
diff --git a/application/front/controller/admin/ImportController.php b/application/front/controller/admin/ImportController.php index 758d5ef9..c2ad6a09 100644 --- a/application/front/controller/admin/ImportController.php +++ b/application/front/controller/admin/ImportController.php | |||
@@ -38,7 +38,7 @@ class ImportController extends ShaarliAdminController | |||
38 | true | 38 | true |
39 | ) | 39 | ) |
40 | ); | 40 | ); |
41 | $this->assignView('pagetitle', t('Import') .' - '. $this->container->conf->get('general.title', 'Shaarli')); | 41 | $this->assignView('pagetitle', t('Import') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')); |
42 | 42 | ||
43 | return $response->write($this->render(TemplatePage::IMPORT)); | 43 | return $response->write($this->render(TemplatePage::IMPORT)); |
44 | } | 44 | } |
@@ -64,7 +64,7 @@ class ImportController extends ShaarliAdminController | |||
64 | $msg = sprintf( | 64 | $msg = sprintf( |
65 | t( | 65 | t( |
66 | 'The file you are trying to upload is probably bigger than what this webserver can accept' | 66 | 'The file you are trying to upload is probably bigger than what this webserver can accept' |
67 | .' (%s). Please upload in smaller chunks.' | 67 | . ' (%s). Please upload in smaller chunks.' |
68 | ), | 68 | ), |
69 | get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')) | 69 | get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')) |
70 | ); | 70 | ); |
diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php deleted file mode 100644 index bb083486..00000000 --- a/application/front/controller/admin/ManageShaareController.php +++ /dev/null | |||
@@ -1,371 +0,0 @@ | |||
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\Render\TemplatePage; | ||
11 | use Shaarli\Thumbnailer; | ||
12 | use Slim\Http\Request; | ||
13 | use Slim\Http\Response; | ||
14 | |||
15 | /** | ||
16 | * Class PostBookmarkController | ||
17 | * | ||
18 | * Slim controller used to handle Shaarli create or edit bookmarks. | ||
19 | */ | ||
20 | class ManageShaareController extends ShaarliAdminController | ||
21 | { | ||
22 | /** | ||
23 | * GET /admin/add-shaare - Displays the form used to create a new bookmark from an URL | ||
24 | */ | ||
25 | public function addShaare(Request $request, Response $response): Response | ||
26 | { | ||
27 | $this->assignView( | ||
28 | 'pagetitle', | ||
29 | t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli') | ||
30 | ); | ||
31 | |||
32 | return $response->write($this->render(TemplatePage::ADDLINK)); | ||
33 | } | ||
34 | |||
35 | /** | ||
36 | * GET /admin/shaare - Displays the bookmark form for creation. | ||
37 | * Note that if the URL is found in existing bookmarks, then it will be in edit mode. | ||
38 | */ | ||
39 | public function displayCreateForm(Request $request, Response $response): Response | ||
40 | { | ||
41 | $url = cleanup_url($request->getParam('post')); | ||
42 | |||
43 | $linkIsNew = false; | ||
44 | // Check if URL is not already in database (in this case, we will edit the existing link) | ||
45 | $bookmark = $this->container->bookmarkService->findByUrl($url); | ||
46 | if (null === $bookmark) { | ||
47 | $linkIsNew = true; | ||
48 | // Get shaare data if it was provided in URL (e.g.: by the bookmarklet). | ||
49 | $title = $request->getParam('title'); | ||
50 | $description = $request->getParam('description'); | ||
51 | $tags = $request->getParam('tags'); | ||
52 | $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN); | ||
53 | |||
54 | // If this is an HTTP(S) link, we try go get the page to extract | ||
55 | // the title (otherwise we will to straight to the edit form.) | ||
56 | if (empty($title) && strpos(get_url_scheme($url) ?: '', 'http') !== false) { | ||
57 | $retrieveDescription = $this->container->conf->get('general.retrieve_description'); | ||
58 | // Short timeout to keep the application responsive | ||
59 | // The callback will fill $charset and $title with data from the downloaded page. | ||
60 | $this->container->httpAccess->getHttpResponse( | ||
61 | $url, | ||
62 | $this->container->conf->get('general.download_timeout', 30), | ||
63 | $this->container->conf->get('general.download_max_size', 4194304), | ||
64 | $this->container->httpAccess->getCurlDownloadCallback( | ||
65 | $charset, | ||
66 | $title, | ||
67 | $description, | ||
68 | $tags, | ||
69 | $retrieveDescription | ||
70 | ) | ||
71 | ); | ||
72 | if (! empty($title) && strtolower($charset) !== 'utf-8' && mb_check_encoding($charset)) { | ||
73 | $title = mb_convert_encoding($title, 'utf-8', $charset); | ||
74 | } | ||
75 | } | ||
76 | |||
77 | if (empty($url) && empty($title)) { | ||
78 | $title = $this->container->conf->get('general.default_note_title', t('Note: ')); | ||
79 | } | ||
80 | |||
81 | $link = [ | ||
82 | 'title' => $title, | ||
83 | 'url' => $url ?? '', | ||
84 | 'description' => $description ?? '', | ||
85 | 'tags' => $tags ?? '', | ||
86 | 'private' => $private, | ||
87 | ]; | ||
88 | } else { | ||
89 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
90 | $link = $formatter->format($bookmark); | ||
91 | } | ||
92 | |||
93 | return $this->displayForm($link, $linkIsNew, $request, $response); | ||
94 | } | ||
95 | |||
96 | /** | ||
97 | * GET /admin/shaare/{id} - Displays the bookmark form in edition mode. | ||
98 | */ | ||
99 | public function displayEditForm(Request $request, Response $response, array $args): Response | ||
100 | { | ||
101 | $id = $args['id'] ?? ''; | ||
102 | try { | ||
103 | if (false === ctype_digit($id)) { | ||
104 | throw new BookmarkNotFoundException(); | ||
105 | } | ||
106 | $bookmark = $this->container->bookmarkService->get((int) $id); // Read database | ||
107 | } catch (BookmarkNotFoundException $e) { | ||
108 | $this->saveErrorMessage(sprintf( | ||
109 | t('Bookmark with identifier %s could not be found.'), | ||
110 | $id | ||
111 | )); | ||
112 | |||
113 | return $this->redirect($response, '/'); | ||
114 | } | ||
115 | |||
116 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
117 | $link = $formatter->format($bookmark); | ||
118 | |||
119 | return $this->displayForm($link, false, $request, $response); | ||
120 | } | ||
121 | |||
122 | /** | ||
123 | * POST /admin/shaare | ||
124 | */ | ||
125 | public function save(Request $request, Response $response): Response | ||
126 | { | ||
127 | $this->checkToken($request); | ||
128 | |||
129 | // lf_id should only be present if the link exists. | ||
130 | $id = $request->getParam('lf_id') !== null ? intval(escape($request->getParam('lf_id'))) : null; | ||
131 | if (null !== $id && true === $this->container->bookmarkService->exists($id)) { | ||
132 | // Edit | ||
133 | $bookmark = $this->container->bookmarkService->get($id); | ||
134 | } else { | ||
135 | // New link | ||
136 | $bookmark = new Bookmark(); | ||
137 | } | ||
138 | |||
139 | $bookmark->setTitle($request->getParam('lf_title')); | ||
140 | $bookmark->setDescription($request->getParam('lf_description')); | ||
141 | $bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', [])); | ||
142 | $bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN)); | ||
143 | $bookmark->setTagsString($request->getParam('lf_tags')); | ||
144 | |||
145 | if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE | ||
146 | && false === $bookmark->isNote() | ||
147 | ) { | ||
148 | $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); | ||
149 | } | ||
150 | $this->container->bookmarkService->addOrSet($bookmark, false); | ||
151 | |||
152 | // To preserve backward compatibility with 3rd parties, plugins still use arrays | ||
153 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
154 | $data = $formatter->format($bookmark); | ||
155 | $this->executePageHooks('save_link', $data); | ||
156 | |||
157 | $bookmark->fromArray($data); | ||
158 | $this->container->bookmarkService->set($bookmark); | ||
159 | |||
160 | // If we are called from the bookmarklet, we must close the popup: | ||
161 | if ($request->getParam('source') === 'bookmarklet') { | ||
162 | return $response->write('<script>self.close();</script>'); | ||
163 | } | ||
164 | |||
165 | if (!empty($request->getParam('returnurl'))) { | ||
166 | $this->container->environment['HTTP_REFERER'] = escape($request->getParam('returnurl')); | ||
167 | } | ||
168 | |||
169 | return $this->redirectFromReferer( | ||
170 | $request, | ||
171 | $response, | ||
172 | ['/admin/add-shaare', '/admin/shaare'], ['addlink', 'post', 'edit_link'], | ||
173 | $bookmark->getShortUrl() | ||
174 | ); | ||
175 | } | ||
176 | |||
177 | /** | ||
178 | * GET /admin/shaare/delete - Delete one or multiple bookmarks (depending on `id` query parameter). | ||
179 | */ | ||
180 | public function deleteBookmark(Request $request, Response $response): Response | ||
181 | { | ||
182 | $this->checkToken($request); | ||
183 | |||
184 | $ids = escape(trim($request->getParam('id') ?? '')); | ||
185 | if (empty($ids) || strpos($ids, ' ') !== false) { | ||
186 | // multiple, space-separated ids provided | ||
187 | $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); | ||
188 | } else { | ||
189 | $ids = [$ids]; | ||
190 | } | ||
191 | |||
192 | // assert at least one id is given | ||
193 | if (0 === count($ids)) { | ||
194 | $this->saveErrorMessage(t('Invalid bookmark ID provided.')); | ||
195 | |||
196 | return $this->redirectFromReferer($request, $response, [], ['delete-shaare']); | ||
197 | } | ||
198 | |||
199 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
200 | $count = 0; | ||
201 | foreach ($ids as $id) { | ||
202 | try { | ||
203 | $bookmark = $this->container->bookmarkService->get((int) $id); | ||
204 | } catch (BookmarkNotFoundException $e) { | ||
205 | $this->saveErrorMessage(sprintf( | ||
206 | t('Bookmark with identifier %s could not be found.'), | ||
207 | $id | ||
208 | )); | ||
209 | |||
210 | continue; | ||
211 | } | ||
212 | |||
213 | $data = $formatter->format($bookmark); | ||
214 | $this->executePageHooks('delete_link', $data); | ||
215 | $this->container->bookmarkService->remove($bookmark, false); | ||
216 | ++ $count; | ||
217 | } | ||
218 | |||
219 | if ($count > 0) { | ||
220 | $this->container->bookmarkService->save(); | ||
221 | } | ||
222 | |||
223 | // If we are called from the bookmarklet, we must close the popup: | ||
224 | if ($request->getParam('source') === 'bookmarklet') { | ||
225 | return $response->write('<script>self.close();</script>'); | ||
226 | } | ||
227 | |||
228 | // Don't redirect to where we were previously because the datastore has changed. | ||
229 | return $this->redirect($response, '/'); | ||
230 | } | ||
231 | |||
232 | /** | ||
233 | * GET /admin/shaare/visibility | ||
234 | * | ||
235 | * Change visibility (public/private) of one or multiple bookmarks (depending on `id` query parameter). | ||
236 | */ | ||
237 | public function changeVisibility(Request $request, Response $response): Response | ||
238 | { | ||
239 | $this->checkToken($request); | ||
240 | |||
241 | $ids = trim(escape($request->getParam('id') ?? '')); | ||
242 | if (empty($ids) || strpos($ids, ' ') !== false) { | ||
243 | // multiple, space-separated ids provided | ||
244 | $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); | ||
245 | } else { | ||
246 | // only a single id provided | ||
247 | $ids = [$ids]; | ||
248 | } | ||
249 | |||
250 | // assert at least one id is given | ||
251 | if (0 === count($ids)) { | ||
252 | $this->saveErrorMessage(t('Invalid bookmark ID provided.')); | ||
253 | |||
254 | return $this->redirectFromReferer($request, $response, [], ['change_visibility']); | ||
255 | } | ||
256 | |||
257 | // assert that the visibility is valid | ||
258 | $visibility = $request->getParam('newVisibility'); | ||
259 | if (null === $visibility || false === in_array($visibility, ['public', 'private'], true)) { | ||
260 | $this->saveErrorMessage(t('Invalid visibility provided.')); | ||
261 | |||
262 | return $this->redirectFromReferer($request, $response, [], ['change_visibility']); | ||
263 | } else { | ||
264 | $isPrivate = $visibility === 'private'; | ||
265 | } | ||
266 | |||
267 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
268 | $count = 0; | ||
269 | |||
270 | foreach ($ids as $id) { | ||
271 | try { | ||
272 | $bookmark = $this->container->bookmarkService->get((int) $id); | ||
273 | } catch (BookmarkNotFoundException $e) { | ||
274 | $this->saveErrorMessage(sprintf( | ||
275 | t('Bookmark with identifier %s could not be found.'), | ||
276 | $id | ||
277 | )); | ||
278 | |||
279 | continue; | ||
280 | } | ||
281 | |||
282 | $bookmark->setPrivate($isPrivate); | ||
283 | |||
284 | // To preserve backward compatibility with 3rd parties, plugins still use arrays | ||
285 | $data = $formatter->format($bookmark); | ||
286 | $this->executePageHooks('save_link', $data); | ||
287 | $bookmark->fromArray($data); | ||
288 | |||
289 | $this->container->bookmarkService->set($bookmark, false); | ||
290 | ++$count; | ||
291 | } | ||
292 | |||
293 | if ($count > 0) { | ||
294 | $this->container->bookmarkService->save(); | ||
295 | } | ||
296 | |||
297 | return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']); | ||
298 | } | ||
299 | |||
300 | /** | ||
301 | * GET /admin/shaare/{id}/pin - Pin or unpin a bookmark. | ||
302 | */ | ||
303 | public function pinBookmark(Request $request, Response $response, array $args): Response | ||
304 | { | ||
305 | $this->checkToken($request); | ||
306 | |||
307 | $id = $args['id'] ?? ''; | ||
308 | try { | ||
309 | if (false === ctype_digit($id)) { | ||
310 | throw new BookmarkNotFoundException(); | ||
311 | } | ||
312 | $bookmark = $this->container->bookmarkService->get((int) $id); // Read database | ||
313 | } catch (BookmarkNotFoundException $e) { | ||
314 | $this->saveErrorMessage(sprintf( | ||
315 | t('Bookmark with identifier %s could not be found.'), | ||
316 | $id | ||
317 | )); | ||
318 | |||
319 | return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); | ||
320 | } | ||
321 | |||
322 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
323 | |||
324 | $bookmark->setSticky(!$bookmark->isSticky()); | ||
325 | |||
326 | // To preserve backward compatibility with 3rd parties, plugins still use arrays | ||
327 | $data = $formatter->format($bookmark); | ||
328 | $this->executePageHooks('save_link', $data); | ||
329 | $bookmark->fromArray($data); | ||
330 | |||
331 | $this->container->bookmarkService->set($bookmark); | ||
332 | |||
333 | return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); | ||
334 | } | ||
335 | |||
336 | /** | ||
337 | * Helper function used to display the shaare form whether it's a new or existing bookmark. | ||
338 | * | ||
339 | * @param array $link data used in template, either from parameters or from the data store | ||
340 | */ | ||
341 | protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response | ||
342 | { | ||
343 | $tags = $this->container->bookmarkService->bookmarksCountPerTag(); | ||
344 | if ($this->container->conf->get('formatter') === 'markdown') { | ||
345 | $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; | ||
346 | } | ||
347 | |||
348 | $data = escape([ | ||
349 | 'link' => $link, | ||
350 | 'link_is_new' => $isNew, | ||
351 | 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '', | ||
352 | 'source' => $request->getParam('source') ?? '', | ||
353 | 'tags' => $tags, | ||
354 | 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false), | ||
355 | ]); | ||
356 | |||
357 | $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK); | ||
358 | |||
359 | foreach ($data as $key => $value) { | ||
360 | $this->assignView($key, $value); | ||
361 | } | ||
362 | |||
363 | $editLabel = false === $isNew ? t('Edit') .' ' : ''; | ||
364 | $this->assignView( | ||
365 | 'pagetitle', | ||
366 | $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli') | ||
367 | ); | ||
368 | |||
369 | return $response->write($this->render(TemplatePage::EDIT_LINK)); | ||
370 | } | ||
371 | } | ||
diff --git a/application/front/controller/admin/ManageTagController.php b/application/front/controller/admin/ManageTagController.php index 2065c3e2..8675a0c5 100644 --- a/application/front/controller/admin/ManageTagController.php +++ b/application/front/controller/admin/ManageTagController.php | |||
@@ -24,9 +24,15 @@ class ManageTagController extends ShaarliAdminController | |||
24 | $fromTag = $request->getParam('fromtag') ?? ''; | 24 | $fromTag = $request->getParam('fromtag') ?? ''; |
25 | 25 | ||
26 | $this->assignView('fromtag', escape($fromTag)); | 26 | $this->assignView('fromtag', escape($fromTag)); |
27 | $separator = escape($this->container->conf->get('general.tags_separator', ' ')); | ||
28 | if ($separator === ' ') { | ||
29 | $separator = ' '; | ||
30 | $this->assignView('tags_separator_desc', t('whitespace')); | ||
31 | } | ||
32 | $this->assignView('tags_separator', $separator); | ||
27 | $this->assignView( | 33 | $this->assignView( |
28 | 'pagetitle', | 34 | 'pagetitle', |
29 | t('Manage tags') .' - '. $this->container->conf->get('general.title', 'Shaarli') | 35 | t('Manage tags') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') |
30 | ); | 36 | ); |
31 | 37 | ||
32 | return $response->write($this->render(TemplatePage::CHANGE_TAG)); | 38 | return $response->write($this->render(TemplatePage::CHANGE_TAG)); |
@@ -81,8 +87,35 @@ class ManageTagController extends ShaarliAdminController | |||
81 | 87 | ||
82 | $this->saveSuccessMessage($alert); | 88 | $this->saveSuccessMessage($alert); |
83 | 89 | ||
84 | $redirect = true === $isDelete ? '/admin/tags' : '/?searchtags='. urlencode($toTag); | 90 | $redirect = true === $isDelete ? '/admin/tags' : '/?searchtags=' . urlencode($toTag); |
85 | 91 | ||
86 | return $this->redirect($response, $redirect); | 92 | return $this->redirect($response, $redirect); |
87 | } | 93 | } |
94 | |||
95 | /** | ||
96 | * POST /admin/tags/change-separator - Change tag separator | ||
97 | */ | ||
98 | public function changeSeparator(Request $request, Response $response): Response | ||
99 | { | ||
100 | $this->checkToken($request); | ||
101 | |||
102 | $reservedCharacters = ['-', '.', '*']; | ||
103 | $newSeparator = $request->getParam('separator'); | ||
104 | if ($newSeparator === null || mb_strlen($newSeparator) !== 1) { | ||
105 | $this->saveErrorMessage(t('Tags separator must be a single character.')); | ||
106 | } elseif (in_array($newSeparator, $reservedCharacters, true)) { | ||
107 | $reservedCharacters = implode(' ', array_map(function (string $character) { | ||
108 | return '<code>' . $character . '</code>'; | ||
109 | }, $reservedCharacters)); | ||
110 | $this->saveErrorMessage( | ||
111 | t('These characters are reserved and can\'t be used as tags separator: ') . $reservedCharacters | ||
112 | ); | ||
113 | } else { | ||
114 | $this->container->conf->set('general.tags_separator', $newSeparator, true, true); | ||
115 | |||
116 | $this->saveSuccessMessage('Your tags separator setting has been updated!'); | ||
117 | } | ||
118 | |||
119 | return $this->redirect($response, '/admin/tags'); | ||
120 | } | ||
88 | } | 121 | } |
diff --git a/application/front/controller/admin/MetadataController.php b/application/front/controller/admin/MetadataController.php new file mode 100644 index 00000000..ff845944 --- /dev/null +++ b/application/front/controller/admin/MetadataController.php | |||
@@ -0,0 +1,29 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use Slim\Http\Request; | ||
8 | use Slim\Http\Response; | ||
9 | |||
10 | /** | ||
11 | * Controller used to retrieve/update bookmark's metadata. | ||
12 | */ | ||
13 | class MetadataController extends ShaarliAdminController | ||
14 | { | ||
15 | /** | ||
16 | * GET /admin/metadata/{url} - Attempt to retrieve the bookmark title from provided URL. | ||
17 | */ | ||
18 | public function ajaxRetrieveTitle(Request $request, Response $response): Response | ||
19 | { | ||
20 | $url = $request->getParam('url'); | ||
21 | |||
22 | // Only try to extract metadata from URL with HTTP(s) scheme | ||
23 | if (!empty($url) && strpos(get_url_scheme($url) ?: '', 'http') !== false) { | ||
24 | return $response->withJson($this->container->metadataRetriever->retrieve($url)); | ||
25 | } | ||
26 | |||
27 | return $response->withJson([]); | ||
28 | } | ||
29 | } | ||
diff --git a/application/front/controller/admin/PasswordController.php b/application/front/controller/admin/PasswordController.php index 5ec0d24b..4aaf1f82 100644 --- a/application/front/controller/admin/PasswordController.php +++ b/application/front/controller/admin/PasswordController.php | |||
@@ -25,7 +25,7 @@ class PasswordController extends ShaarliAdminController | |||
25 | 25 | ||
26 | $this->assignView( | 26 | $this->assignView( |
27 | 'pagetitle', | 27 | 'pagetitle', |
28 | t('Change password') .' - '. $this->container->conf->get('general.title', 'Shaarli') | 28 | t('Change password') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') |
29 | ); | 29 | ); |
30 | } | 30 | } |
31 | 31 | ||
@@ -78,7 +78,7 @@ class PasswordController extends ShaarliAdminController | |||
78 | 78 | ||
79 | // Save new password | 79 | // Save new password |
80 | // Salt renders rainbow-tables attacks useless. | 80 | // Salt renders rainbow-tables attacks useless. |
81 | $this->container->conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); | 81 | $this->container->conf->set('credentials.salt', sha1(uniqid('', true) . '_' . mt_rand())); |
82 | $this->container->conf->set( | 82 | $this->container->conf->set( |
83 | 'credentials.hash', | 83 | 'credentials.hash', |
84 | sha1( | 84 | sha1( |
diff --git a/application/front/controller/admin/PluginsController.php b/application/front/controller/admin/PluginsController.php index 8e059681..ae47c1af 100644 --- a/application/front/controller/admin/PluginsController.php +++ b/application/front/controller/admin/PluginsController.php | |||
@@ -42,7 +42,7 @@ class PluginsController extends ShaarliAdminController | |||
42 | $this->assignView('disabledPlugins', $disabledPlugins); | 42 | $this->assignView('disabledPlugins', $disabledPlugins); |
43 | $this->assignView( | 43 | $this->assignView( |
44 | 'pagetitle', | 44 | 'pagetitle', |
45 | t('Plugin Administration') .' - '. $this->container->conf->get('general.title', 'Shaarli') | 45 | t('Plugin Administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') |
46 | ); | 46 | ); |
47 | 47 | ||
48 | return $response->write($this->render(TemplatePage::PLUGINS_ADMIN)); | 48 | return $response->write($this->render(TemplatePage::PLUGINS_ADMIN)); |
@@ -64,7 +64,7 @@ class PluginsController extends ShaarliAdminController | |||
64 | unset($parameters['parameters_form']); | 64 | unset($parameters['parameters_form']); |
65 | unset($parameters['token']); | 65 | unset($parameters['token']); |
66 | foreach ($parameters as $param => $value) { | 66 | foreach ($parameters as $param => $value) { |
67 | $this->container->conf->set('plugins.'. $param, escape($value)); | 67 | $this->container->conf->set('plugins.' . $param, escape($value)); |
68 | } | 68 | } |
69 | } else { | 69 | } else { |
70 | $this->container->conf->set('general.enabled_plugins', save_plugin_config($parameters)); | 70 | $this->container->conf->set('general.enabled_plugins', save_plugin_config($parameters)); |
diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php new file mode 100644 index 00000000..4b74f4a9 --- /dev/null +++ b/application/front/controller/admin/ServerController.php | |||
@@ -0,0 +1,101 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use Shaarli\Helper\ApplicationUtils; | ||
8 | use Shaarli\Helper\FileUtils; | ||
9 | use Slim\Http\Request; | ||
10 | use Slim\Http\Response; | ||
11 | |||
12 | /** | ||
13 | * Slim controller used to handle Server administration page, and actions. | ||
14 | */ | ||
15 | class ServerController extends ShaarliAdminController | ||
16 | { | ||
17 | /** @var string Cache type - main - by default pagecache/ and tmp/ */ | ||
18 | protected const CACHE_MAIN = 'main'; | ||
19 | |||
20 | /** @var string Cache type - thumbnails - by default cache/ */ | ||
21 | protected const CACHE_THUMB = 'thumbnails'; | ||
22 | |||
23 | /** | ||
24 | * GET /admin/server - Display page Server administration | ||
25 | */ | ||
26 | public function index(Request $request, Response $response): Response | ||
27 | { | ||
28 | $releaseUrl = ApplicationUtils::$GITHUB_URL . '/releases/'; | ||
29 | if ($this->container->conf->get('updates.check_updates', true)) { | ||
30 | $latestVersion = 'v' . ApplicationUtils::getVersion( | ||
31 | ApplicationUtils::$GIT_RAW_URL . '/latest/' . ApplicationUtils::$VERSION_FILE | ||
32 | ); | ||
33 | $releaseUrl .= 'tag/' . $latestVersion; | ||
34 | } else { | ||
35 | $latestVersion = t('Check disabled'); | ||
36 | } | ||
37 | |||
38 | $currentVersion = ApplicationUtils::getVersion('./shaarli_version.php'); | ||
39 | $currentVersion = $currentVersion === 'dev' ? $currentVersion : 'v' . $currentVersion; | ||
40 | $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION)); | ||
41 | |||
42 | $permissions = array_merge( | ||
43 | ApplicationUtils::checkResourcePermissions($this->container->conf), | ||
44 | ApplicationUtils::checkDatastoreMutex() | ||
45 | ); | ||
46 | |||
47 | $this->assignView('php_version', PHP_VERSION); | ||
48 | $this->assignView('php_eol', format_date($phpEol, false)); | ||
49 | $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable()); | ||
50 | $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement()); | ||
51 | $this->assignView('permissions', $permissions); | ||
52 | $this->assignView('release_url', $releaseUrl); | ||
53 | $this->assignView('latest_version', $latestVersion); | ||
54 | $this->assignView('current_version', $currentVersion); | ||
55 | $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode')); | ||
56 | $this->assignView('index_url', index_url($this->container->environment)); | ||
57 | $this->assignView('client_ip', client_ip_id($this->container->environment)); | ||
58 | $this->assignView('trusted_proxies', $this->container->conf->get('security.trusted_proxies', [])); | ||
59 | |||
60 | $this->assignView( | ||
61 | 'pagetitle', | ||
62 | t('Server administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') | ||
63 | ); | ||
64 | |||
65 | return $response->write($this->render('server')); | ||
66 | } | ||
67 | |||
68 | /** | ||
69 | * GET /admin/clear-cache?type={$type} - Action to trigger cache folder clearing (either main or thumbnails). | ||
70 | */ | ||
71 | public function clearCache(Request $request, Response $response): Response | ||
72 | { | ||
73 | $exclude = ['.htaccess']; | ||
74 | |||
75 | if ($request->getQueryParam('type') === static::CACHE_THUMB) { | ||
76 | $folders = [$this->container->conf->get('resource.thumbnails_cache')]; | ||
77 | |||
78 | $this->saveWarningMessage( | ||
79 | t('Thumbnails cache has been cleared.') . ' ' . | ||
80 | '<a href="' . $this->container->basePath . '/admin/thumbnails">' . | ||
81 | t('Please synchronize them.') . | ||
82 | '</a>' | ||
83 | ); | ||
84 | } else { | ||
85 | $folders = [ | ||
86 | $this->container->conf->get('resource.page_cache'), | ||
87 | $this->container->conf->get('resource.raintpl_tmp'), | ||
88 | ]; | ||
89 | |||
90 | $this->saveSuccessMessage(t('Shaarli\'s cache folder has been cleared!')); | ||
91 | } | ||
92 | |||
93 | // Make sure that we don't delete root cache folder | ||
94 | $folders = array_map('realpath', array_values(array_filter(array_map('trim', $folders)))); | ||
95 | foreach ($folders as $folder) { | ||
96 | FileUtils::clearFolder($folder, false, $exclude); | ||
97 | } | ||
98 | |||
99 | return $this->redirect($response, '/admin/server'); | ||
100 | } | ||
101 | } | ||
diff --git a/application/front/controller/admin/SessionFilterController.php b/application/front/controller/admin/SessionFilterController.php index d9a7a2e0..0917b6d2 100644 --- a/application/front/controller/admin/SessionFilterController.php +++ b/application/front/controller/admin/SessionFilterController.php | |||
@@ -45,6 +45,4 @@ class SessionFilterController extends ShaarliAdminController | |||
45 | 45 | ||
46 | return $this->redirectFromReferer($request, $response, ['visibility']); | 46 | return $this->redirectFromReferer($request, $response, ['visibility']); |
47 | } | 47 | } |
48 | |||
49 | |||
50 | } | 48 | } |
diff --git a/application/front/controller/admin/ShaareAddController.php b/application/front/controller/admin/ShaareAddController.php new file mode 100644 index 00000000..ab8e7f40 --- /dev/null +++ b/application/front/controller/admin/ShaareAddController.php | |||
@@ -0,0 +1,34 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use Shaarli\Formatter\BookmarkMarkdownFormatter; | ||
8 | use Shaarli\Render\TemplatePage; | ||
9 | use Slim\Http\Request; | ||
10 | use Slim\Http\Response; | ||
11 | |||
12 | class ShaareAddController extends ShaarliAdminController | ||
13 | { | ||
14 | /** | ||
15 | * GET /admin/add-shaare - Displays the form used to create a new bookmark from an URL | ||
16 | */ | ||
17 | public function addShaare(Request $request, Response $response): Response | ||
18 | { | ||
19 | $tags = $this->container->bookmarkService->bookmarksCountPerTag(); | ||
20 | if ($this->container->conf->get('formatter') === 'markdown') { | ||
21 | $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; | ||
22 | } | ||
23 | |||
24 | $this->assignView( | ||
25 | 'pagetitle', | ||
26 | t('Shaare a new link') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') | ||
27 | ); | ||
28 | $this->assignView('tags', $tags); | ||
29 | $this->assignView('default_private_links', $this->container->conf->get('privacy.default_private_links', false)); | ||
30 | $this->assignView('async_metadata', $this->container->conf->get('general.enable_async_metadata', true)); | ||
31 | |||
32 | return $response->write($this->render(TemplatePage::ADDLINK)); | ||
33 | } | ||
34 | } | ||
diff --git a/application/front/controller/admin/ShaareManageController.php b/application/front/controller/admin/ShaareManageController.php new file mode 100644 index 00000000..35837baa --- /dev/null +++ b/application/front/controller/admin/ShaareManageController.php | |||
@@ -0,0 +1,202 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | ||
8 | use Slim\Http\Request; | ||
9 | use Slim\Http\Response; | ||
10 | |||
11 | /** | ||
12 | * Class PostBookmarkController | ||
13 | * | ||
14 | * Slim controller used to handle Shaarli create or edit bookmarks. | ||
15 | */ | ||
16 | class ShaareManageController extends ShaarliAdminController | ||
17 | { | ||
18 | /** | ||
19 | * GET /admin/shaare/delete - Delete one or multiple bookmarks (depending on `id` query parameter). | ||
20 | */ | ||
21 | public function deleteBookmark(Request $request, Response $response): Response | ||
22 | { | ||
23 | $this->checkToken($request); | ||
24 | |||
25 | $ids = escape(trim($request->getParam('id') ?? '')); | ||
26 | if (empty($ids) || strpos($ids, ' ') !== false) { | ||
27 | // multiple, space-separated ids provided | ||
28 | $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); | ||
29 | } else { | ||
30 | $ids = [$ids]; | ||
31 | } | ||
32 | |||
33 | // assert at least one id is given | ||
34 | if (0 === count($ids)) { | ||
35 | $this->saveErrorMessage(t('Invalid bookmark ID provided.')); | ||
36 | |||
37 | return $this->redirectFromReferer($request, $response, [], ['delete-shaare']); | ||
38 | } | ||
39 | |||
40 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
41 | $count = 0; | ||
42 | foreach ($ids as $id) { | ||
43 | try { | ||
44 | $bookmark = $this->container->bookmarkService->get((int) $id); | ||
45 | } catch (BookmarkNotFoundException $e) { | ||
46 | $this->saveErrorMessage(sprintf( | ||
47 | t('Bookmark with identifier %s could not be found.'), | ||
48 | $id | ||
49 | )); | ||
50 | |||
51 | continue; | ||
52 | } | ||
53 | |||
54 | $data = $formatter->format($bookmark); | ||
55 | $this->executePageHooks('delete_link', $data); | ||
56 | $this->container->bookmarkService->remove($bookmark, false); | ||
57 | ++$count; | ||
58 | } | ||
59 | |||
60 | if ($count > 0) { | ||
61 | $this->container->bookmarkService->save(); | ||
62 | } | ||
63 | |||
64 | // If we are called from the bookmarklet, we must close the popup: | ||
65 | if ($request->getParam('source') === 'bookmarklet') { | ||
66 | return $response->write('<script>self.close();</script>'); | ||
67 | } | ||
68 | |||
69 | // Don't redirect to permalink after deletion. | ||
70 | return $this->redirectFromReferer($request, $response, ['shaare/']); | ||
71 | } | ||
72 | |||
73 | /** | ||
74 | * GET /admin/shaare/visibility | ||
75 | * | ||
76 | * Change visibility (public/private) of one or multiple bookmarks (depending on `id` query parameter). | ||
77 | */ | ||
78 | public function changeVisibility(Request $request, Response $response): Response | ||
79 | { | ||
80 | $this->checkToken($request); | ||
81 | |||
82 | $ids = trim(escape($request->getParam('id') ?? '')); | ||
83 | if (empty($ids) || strpos($ids, ' ') !== false) { | ||
84 | // multiple, space-separated ids provided | ||
85 | $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); | ||
86 | } else { | ||
87 | // only a single id provided | ||
88 | $ids = [$ids]; | ||
89 | } | ||
90 | |||
91 | // assert at least one id is given | ||
92 | if (0 === count($ids)) { | ||
93 | $this->saveErrorMessage(t('Invalid bookmark ID provided.')); | ||
94 | |||
95 | return $this->redirectFromReferer($request, $response, [], ['change_visibility']); | ||
96 | } | ||
97 | |||
98 | // assert that the visibility is valid | ||
99 | $visibility = $request->getParam('newVisibility'); | ||
100 | if (null === $visibility || false === in_array($visibility, ['public', 'private'], true)) { | ||
101 | $this->saveErrorMessage(t('Invalid visibility provided.')); | ||
102 | |||
103 | return $this->redirectFromReferer($request, $response, [], ['change_visibility']); | ||
104 | } else { | ||
105 | $isPrivate = $visibility === 'private'; | ||
106 | } | ||
107 | |||
108 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
109 | $count = 0; | ||
110 | |||
111 | foreach ($ids as $id) { | ||
112 | try { | ||
113 | $bookmark = $this->container->bookmarkService->get((int) $id); | ||
114 | } catch (BookmarkNotFoundException $e) { | ||
115 | $this->saveErrorMessage(sprintf( | ||
116 | t('Bookmark with identifier %s could not be found.'), | ||
117 | $id | ||
118 | )); | ||
119 | |||
120 | continue; | ||
121 | } | ||
122 | |||
123 | $bookmark->setPrivate($isPrivate); | ||
124 | |||
125 | // To preserve backward compatibility with 3rd parties, plugins still use arrays | ||
126 | $data = $formatter->format($bookmark); | ||
127 | $this->executePageHooks('save_link', $data); | ||
128 | $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' ')); | ||
129 | |||
130 | $this->container->bookmarkService->set($bookmark, false); | ||
131 | ++$count; | ||
132 | } | ||
133 | |||
134 | if ($count > 0) { | ||
135 | $this->container->bookmarkService->save(); | ||
136 | } | ||
137 | |||
138 | return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']); | ||
139 | } | ||
140 | |||
141 | /** | ||
142 | * GET /admin/shaare/{id}/pin - Pin or unpin a bookmark. | ||
143 | */ | ||
144 | public function pinBookmark(Request $request, Response $response, array $args): Response | ||
145 | { | ||
146 | $this->checkToken($request); | ||
147 | |||
148 | $id = $args['id'] ?? ''; | ||
149 | try { | ||
150 | if (false === ctype_digit($id)) { | ||
151 | throw new BookmarkNotFoundException(); | ||
152 | } | ||
153 | $bookmark = $this->container->bookmarkService->get((int) $id); // Read database | ||
154 | } catch (BookmarkNotFoundException $e) { | ||
155 | $this->saveErrorMessage(sprintf( | ||
156 | t('Bookmark with identifier %s could not be found.'), | ||
157 | $id | ||
158 | )); | ||
159 | |||
160 | return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); | ||
161 | } | ||
162 | |||
163 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
164 | |||
165 | $bookmark->setSticky(!$bookmark->isSticky()); | ||
166 | |||
167 | // To preserve backward compatibility with 3rd parties, plugins still use arrays | ||
168 | $data = $formatter->format($bookmark); | ||
169 | $this->executePageHooks('save_link', $data); | ||
170 | $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' ')); | ||
171 | |||
172 | $this->container->bookmarkService->set($bookmark); | ||
173 | |||
174 | return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); | ||
175 | } | ||
176 | |||
177 | /** | ||
178 | * GET /admin/shaare/private/{hash} - Attach a private key to given bookmark, then redirect to the sharing URL. | ||
179 | */ | ||
180 | public function sharePrivate(Request $request, Response $response, array $args): Response | ||
181 | { | ||
182 | $this->checkToken($request); | ||
183 | |||
184 | $hash = $args['hash'] ?? ''; | ||
185 | $bookmark = $this->container->bookmarkService->findByHash($hash); | ||
186 | |||
187 | if ($bookmark->isPrivate() !== true) { | ||
188 | return $this->redirect($response, '/shaare/' . $hash); | ||
189 | } | ||
190 | |||
191 | if (empty($bookmark->getAdditionalContentEntry('private_key'))) { | ||
192 | $privateKey = bin2hex(random_bytes(16)); | ||
193 | $bookmark->addAdditionalContentEntry('private_key', $privateKey); | ||
194 | $this->container->bookmarkService->set($bookmark); | ||
195 | } | ||
196 | |||
197 | return $this->redirect( | ||
198 | $response, | ||
199 | '/shaare/' . $hash . '?key=' . $bookmark->getAdditionalContentEntry('private_key') | ||
200 | ); | ||
201 | } | ||
202 | } | ||
diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php new file mode 100644 index 00000000..fb9cacc2 --- /dev/null +++ b/application/front/controller/admin/ShaarePublishController.php | |||
@@ -0,0 +1,274 @@ | |||
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\BookmarkFormatter; | ||
10 | use Shaarli\Formatter\BookmarkMarkdownFormatter; | ||
11 | use Shaarli\Render\TemplatePage; | ||
12 | use Shaarli\Thumbnailer; | ||
13 | use Slim\Http\Request; | ||
14 | use Slim\Http\Response; | ||
15 | |||
16 | class ShaarePublishController extends ShaarliAdminController | ||
17 | { | ||
18 | /** | ||
19 | * @var BookmarkFormatter[] Statically cached instances of formatters | ||
20 | */ | ||
21 | protected $formatters = []; | ||
22 | |||
23 | /** | ||
24 | * @var array Statically cached bookmark's tags counts | ||
25 | */ | ||
26 | protected $tags; | ||
27 | |||
28 | /** | ||
29 | * GET /admin/shaare - Displays the bookmark form for creation. | ||
30 | * Note that if the URL is found in existing bookmarks, then it will be in edit mode. | ||
31 | */ | ||
32 | public function displayCreateForm(Request $request, Response $response): Response | ||
33 | { | ||
34 | $url = cleanup_url($request->getParam('post')); | ||
35 | $link = $this->buildLinkDataFromUrl($request, $url); | ||
36 | |||
37 | return $this->displayForm($link, $link['linkIsNew'], $request, $response); | ||
38 | } | ||
39 | |||
40 | /** | ||
41 | * POST /admin/shaare-batch - Displays multiple creation/edit forms from bulk add in add-link page. | ||
42 | */ | ||
43 | public function displayCreateBatchForms(Request $request, Response $response): Response | ||
44 | { | ||
45 | $urls = array_map('cleanup_url', explode(PHP_EOL, $request->getParam('urls'))); | ||
46 | |||
47 | $links = []; | ||
48 | foreach ($urls as $url) { | ||
49 | if (empty($url)) { | ||
50 | continue; | ||
51 | } | ||
52 | $link = $this->buildLinkDataFromUrl($request, $url); | ||
53 | $data = $this->buildFormData($link, $link['linkIsNew'], $request); | ||
54 | $data['token'] = $this->container->sessionManager->generateToken(); | ||
55 | $data['source'] = 'batch'; | ||
56 | |||
57 | $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK); | ||
58 | |||
59 | $links[] = $data; | ||
60 | } | ||
61 | |||
62 | $this->assignView('links', $links); | ||
63 | $this->assignView('batch_mode', true); | ||
64 | $this->assignView('async_metadata', $this->container->conf->get('general.enable_async_metadata', true)); | ||
65 | |||
66 | return $response->write($this->render(TemplatePage::EDIT_LINK_BATCH)); | ||
67 | } | ||
68 | |||
69 | /** | ||
70 | * GET /admin/shaare/{id} - Displays the bookmark form in edition mode. | ||
71 | */ | ||
72 | public function displayEditForm(Request $request, Response $response, array $args): Response | ||
73 | { | ||
74 | $id = $args['id'] ?? ''; | ||
75 | try { | ||
76 | if (false === ctype_digit($id)) { | ||
77 | throw new BookmarkNotFoundException(); | ||
78 | } | ||
79 | $bookmark = $this->container->bookmarkService->get((int) $id); // Read database | ||
80 | } catch (BookmarkNotFoundException $e) { | ||
81 | $this->saveErrorMessage(sprintf( | ||
82 | t('Bookmark with identifier %s could not be found.'), | ||
83 | $id | ||
84 | )); | ||
85 | |||
86 | return $this->redirect($response, '/'); | ||
87 | } | ||
88 | |||
89 | $formatter = $this->getFormatter('raw'); | ||
90 | $link = $formatter->format($bookmark); | ||
91 | |||
92 | return $this->displayForm($link, false, $request, $response); | ||
93 | } | ||
94 | |||
95 | /** | ||
96 | * POST /admin/shaare | ||
97 | */ | ||
98 | public function save(Request $request, Response $response): Response | ||
99 | { | ||
100 | $this->checkToken($request); | ||
101 | |||
102 | // lf_id should only be present if the link exists. | ||
103 | $id = $request->getParam('lf_id') !== null ? intval(escape($request->getParam('lf_id'))) : null; | ||
104 | if (null !== $id && true === $this->container->bookmarkService->exists($id)) { | ||
105 | // Edit | ||
106 | $bookmark = $this->container->bookmarkService->get($id); | ||
107 | } else { | ||
108 | // New link | ||
109 | $bookmark = new Bookmark(); | ||
110 | } | ||
111 | |||
112 | $bookmark->setTitle($request->getParam('lf_title')); | ||
113 | $bookmark->setDescription($request->getParam('lf_description')); | ||
114 | $bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', [])); | ||
115 | $bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN)); | ||
116 | $bookmark->setTagsString( | ||
117 | $request->getParam('lf_tags'), | ||
118 | $this->container->conf->get('general.tags_separator', ' ') | ||
119 | ); | ||
120 | |||
121 | if ( | ||
122 | $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE | ||
123 | && true !== $this->container->conf->get('general.enable_async_metadata', true) | ||
124 | && $bookmark->shouldUpdateThumbnail() | ||
125 | ) { | ||
126 | $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); | ||
127 | } | ||
128 | $this->container->bookmarkService->addOrSet($bookmark, false); | ||
129 | |||
130 | // To preserve backward compatibility with 3rd parties, plugins still use arrays | ||
131 | $formatter = $this->getFormatter('raw'); | ||
132 | $data = $formatter->format($bookmark); | ||
133 | $this->executePageHooks('save_link', $data); | ||
134 | |||
135 | $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' ')); | ||
136 | $this->container->bookmarkService->set($bookmark); | ||
137 | |||
138 | // If we are called from the bookmarklet, we must close the popup: | ||
139 | if ($request->getParam('source') === 'bookmarklet') { | ||
140 | return $response->write('<script>self.close();</script>'); | ||
141 | } elseif ($request->getParam('source') === 'batch') { | ||
142 | return $response; | ||
143 | } | ||
144 | |||
145 | if (!empty($request->getParam('returnurl'))) { | ||
146 | $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl'); | ||
147 | } | ||
148 | |||
149 | return $this->redirectFromReferer( | ||
150 | $request, | ||
151 | $response, | ||
152 | ['/admin/add-shaare', '/admin/shaare'], | ||
153 | ['addlink', 'post', 'edit_link'], | ||
154 | $bookmark->getShortUrl() | ||
155 | ); | ||
156 | } | ||
157 | |||
158 | /** | ||
159 | * Helper function used to display the shaare form whether it's a new or existing bookmark. | ||
160 | * | ||
161 | * @param array $link data used in template, either from parameters or from the data store | ||
162 | */ | ||
163 | protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response | ||
164 | { | ||
165 | $data = $this->buildFormData($link, $isNew, $request); | ||
166 | |||
167 | $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK); | ||
168 | |||
169 | foreach ($data as $key => $value) { | ||
170 | $this->assignView($key, $value); | ||
171 | } | ||
172 | |||
173 | $editLabel = false === $isNew ? t('Edit') . ' ' : ''; | ||
174 | $this->assignView( | ||
175 | 'pagetitle', | ||
176 | $editLabel . t('Shaare') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') | ||
177 | ); | ||
178 | |||
179 | return $response->write($this->render(TemplatePage::EDIT_LINK)); | ||
180 | } | ||
181 | |||
182 | protected function buildLinkDataFromUrl(Request $request, string $url): array | ||
183 | { | ||
184 | // Check if URL is not already in database (in this case, we will edit the existing link) | ||
185 | $bookmark = $this->container->bookmarkService->findByUrl($url); | ||
186 | if (null === $bookmark) { | ||
187 | // Get shaare data if it was provided in URL (e.g.: by the bookmarklet). | ||
188 | $title = $request->getParam('title'); | ||
189 | $description = $request->getParam('description'); | ||
190 | $tags = $request->getParam('tags'); | ||
191 | if ($request->getParam('private') !== null) { | ||
192 | $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN); | ||
193 | } else { | ||
194 | $private = $this->container->conf->get('privacy.default_private_links', false); | ||
195 | } | ||
196 | |||
197 | // If this is an HTTP(S) link, we try go get the page to extract | ||
198 | // the title (otherwise we will to straight to the edit form.) | ||
199 | if ( | ||
200 | true !== $this->container->conf->get('general.enable_async_metadata', true) | ||
201 | && empty($title) | ||
202 | && strpos(get_url_scheme($url) ?: '', 'http') !== false | ||
203 | ) { | ||
204 | $metadata = $this->container->metadataRetriever->retrieve($url); | ||
205 | } | ||
206 | |||
207 | if (empty($url)) { | ||
208 | $metadata['title'] = $this->container->conf->get('general.default_note_title', t('Note: ')); | ||
209 | } | ||
210 | |||
211 | return [ | ||
212 | 'title' => $title ?? $metadata['title'] ?? '', | ||
213 | 'url' => $url ?? '', | ||
214 | 'description' => $description ?? $metadata['description'] ?? '', | ||
215 | 'tags' => $tags ?? $metadata['tags'] ?? '', | ||
216 | 'private' => $private, | ||
217 | 'linkIsNew' => true, | ||
218 | ]; | ||
219 | } | ||
220 | |||
221 | $formatter = $this->getFormatter('raw'); | ||
222 | $link = $formatter->format($bookmark); | ||
223 | $link['linkIsNew'] = false; | ||
224 | |||
225 | return $link; | ||
226 | } | ||
227 | |||
228 | protected function buildFormData(array $link, bool $isNew, Request $request): array | ||
229 | { | ||
230 | $link['tags'] = $link['tags'] !== null && strlen($link['tags']) > 0 | ||
231 | ? $link['tags'] . $this->container->conf->get('general.tags_separator', ' ') | ||
232 | : $link['tags'] | ||
233 | ; | ||
234 | |||
235 | return escape([ | ||
236 | 'link' => $link, | ||
237 | 'link_is_new' => $isNew, | ||
238 | 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '', | ||
239 | 'source' => $request->getParam('source') ?? '', | ||
240 | 'tags' => $this->getTags(), | ||
241 | 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false), | ||
242 | 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true), | ||
243 | 'retrieve_description' => $this->container->conf->get('general.retrieve_description', false), | ||
244 | ]); | ||
245 | } | ||
246 | |||
247 | /** | ||
248 | * Memoize formatterFactory->getFormatter() calls. | ||
249 | */ | ||
250 | protected function getFormatter(string $type): BookmarkFormatter | ||
251 | { | ||
252 | if (!array_key_exists($type, $this->formatters) || $this->formatters[$type] === null) { | ||
253 | $this->formatters[$type] = $this->container->formatterFactory->getFormatter($type); | ||
254 | } | ||
255 | |||
256 | return $this->formatters[$type]; | ||
257 | } | ||
258 | |||
259 | /** | ||
260 | * Memoize bookmarkService->bookmarksCountPerTag() calls. | ||
261 | */ | ||
262 | protected function getTags(): array | ||
263 | { | ||
264 | if ($this->tags === null) { | ||
265 | $this->tags = $this->container->bookmarkService->bookmarksCountPerTag(); | ||
266 | |||
267 | if ($this->container->conf->get('formatter') === 'markdown') { | ||
268 | $this->tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; | ||
269 | } | ||
270 | } | ||
271 | |||
272 | return $this->tags; | ||
273 | } | ||
274 | } | ||
diff --git a/application/front/controller/admin/ThumbnailsController.php b/application/front/controller/admin/ThumbnailsController.php index 4dc09d38..94d97d4b 100644 --- a/application/front/controller/admin/ThumbnailsController.php +++ b/application/front/controller/admin/ThumbnailsController.php | |||
@@ -34,7 +34,7 @@ class ThumbnailsController extends ShaarliAdminController | |||
34 | $this->assignView('ids', $ids); | 34 | $this->assignView('ids', $ids); |
35 | $this->assignView( | 35 | $this->assignView( |
36 | 'pagetitle', | 36 | 'pagetitle', |
37 | t('Thumbnails update') .' - '. $this->container->conf->get('general.title', 'Shaarli') | 37 | t('Thumbnails update') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') |
38 | ); | 38 | ); |
39 | 39 | ||
40 | return $response->write($this->render(TemplatePage::THUMBNAILS)); | 40 | return $response->write($this->render(TemplatePage::THUMBNAILS)); |
diff --git a/application/front/controller/admin/ToolsController.php b/application/front/controller/admin/ToolsController.php index a87f20d2..560e5e3e 100644 --- a/application/front/controller/admin/ToolsController.php +++ b/application/front/controller/admin/ToolsController.php | |||
@@ -28,7 +28,7 @@ class ToolsController extends ShaarliAdminController | |||
28 | $this->assignView($key, $value); | 28 | $this->assignView($key, $value); |
29 | } | 29 | } |
30 | 30 | ||
31 | $this->assignView('pagetitle', t('Tools') .' - '. $this->container->conf->get('general.title', 'Shaarli')); | 31 | $this->assignView('pagetitle', t('Tools') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')); |
32 | 32 | ||
33 | return $response->write($this->render(TemplatePage::TOOLS)); | 33 | return $response->write($this->render(TemplatePage::TOOLS)); |
34 | } | 34 | } |