diff options
Diffstat (limited to 'application/front/controller')
8 files changed, 678 insertions, 419 deletions
diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php deleted file mode 100644 index 908ebae3..00000000 --- a/application/front/controller/admin/ManageShaareController.php +++ /dev/null | |||
@@ -1,360 +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 (true !== $this->container->conf->get('general.enable_async_metadata', true) | ||
57 | && empty($title) | ||
58 | && strpos(get_url_scheme($url) ?: '', 'http') !== false | ||
59 | ) { | ||
60 | $metadata = $this->container->metadataRetriever->retrieve($url); | ||
61 | } | ||
62 | |||
63 | if (empty($url)) { | ||
64 | $metadata['title'] = $this->container->conf->get('general.default_note_title', t('Note: ')); | ||
65 | } | ||
66 | |||
67 | $link = [ | ||
68 | 'title' => $title ?? $metadata['title'] ?? '', | ||
69 | 'url' => $url ?? '', | ||
70 | 'description' => $description ?? $metadata['description'] ?? '', | ||
71 | 'tags' => $tags ?? $metadata['tags'] ?? '', | ||
72 | 'private' => $private, | ||
73 | ]; | ||
74 | } else { | ||
75 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
76 | $link = $formatter->format($bookmark); | ||
77 | } | ||
78 | |||
79 | return $this->displayForm($link, $linkIsNew, $request, $response); | ||
80 | } | ||
81 | |||
82 | /** | ||
83 | * GET /admin/shaare/{id} - Displays the bookmark form in edition mode. | ||
84 | */ | ||
85 | public function displayEditForm(Request $request, Response $response, array $args): Response | ||
86 | { | ||
87 | $id = $args['id'] ?? ''; | ||
88 | try { | ||
89 | if (false === ctype_digit($id)) { | ||
90 | throw new BookmarkNotFoundException(); | ||
91 | } | ||
92 | $bookmark = $this->container->bookmarkService->get((int) $id); // Read database | ||
93 | } catch (BookmarkNotFoundException $e) { | ||
94 | $this->saveErrorMessage(sprintf( | ||
95 | t('Bookmark with identifier %s could not be found.'), | ||
96 | $id | ||
97 | )); | ||
98 | |||
99 | return $this->redirect($response, '/'); | ||
100 | } | ||
101 | |||
102 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
103 | $link = $formatter->format($bookmark); | ||
104 | |||
105 | return $this->displayForm($link, false, $request, $response); | ||
106 | } | ||
107 | |||
108 | /** | ||
109 | * POST /admin/shaare | ||
110 | */ | ||
111 | public function save(Request $request, Response $response): Response | ||
112 | { | ||
113 | $this->checkToken($request); | ||
114 | |||
115 | // lf_id should only be present if the link exists. | ||
116 | $id = $request->getParam('lf_id') !== null ? intval(escape($request->getParam('lf_id'))) : null; | ||
117 | if (null !== $id && true === $this->container->bookmarkService->exists($id)) { | ||
118 | // Edit | ||
119 | $bookmark = $this->container->bookmarkService->get($id); | ||
120 | } else { | ||
121 | // New link | ||
122 | $bookmark = new Bookmark(); | ||
123 | } | ||
124 | |||
125 | $bookmark->setTitle($request->getParam('lf_title')); | ||
126 | $bookmark->setDescription($request->getParam('lf_description')); | ||
127 | $bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', [])); | ||
128 | $bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN)); | ||
129 | $bookmark->setTagsString($request->getParam('lf_tags')); | ||
130 | |||
131 | if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE | ||
132 | && true !== $this->container->conf->get('general.enable_async_metadata', true) | ||
133 | && $bookmark->shouldUpdateThumbnail() | ||
134 | ) { | ||
135 | $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); | ||
136 | } | ||
137 | $this->container->bookmarkService->addOrSet($bookmark, false); | ||
138 | |||
139 | // To preserve backward compatibility with 3rd parties, plugins still use arrays | ||
140 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
141 | $data = $formatter->format($bookmark); | ||
142 | $this->executePageHooks('save_link', $data); | ||
143 | |||
144 | $bookmark->fromArray($data); | ||
145 | $this->container->bookmarkService->set($bookmark); | ||
146 | |||
147 | // If we are called from the bookmarklet, we must close the popup: | ||
148 | if ($request->getParam('source') === 'bookmarklet') { | ||
149 | return $response->write('<script>self.close();</script>'); | ||
150 | } | ||
151 | |||
152 | if (!empty($request->getParam('returnurl'))) { | ||
153 | $this->container->environment['HTTP_REFERER'] = escape($request->getParam('returnurl')); | ||
154 | } | ||
155 | |||
156 | return $this->redirectFromReferer( | ||
157 | $request, | ||
158 | $response, | ||
159 | ['/admin/add-shaare', '/admin/shaare'], ['addlink', 'post', 'edit_link'], | ||
160 | $bookmark->getShortUrl() | ||
161 | ); | ||
162 | } | ||
163 | |||
164 | /** | ||
165 | * GET /admin/shaare/delete - Delete one or multiple bookmarks (depending on `id` query parameter). | ||
166 | */ | ||
167 | public function deleteBookmark(Request $request, Response $response): Response | ||
168 | { | ||
169 | $this->checkToken($request); | ||
170 | |||
171 | $ids = escape(trim($request->getParam('id') ?? '')); | ||
172 | if (empty($ids) || strpos($ids, ' ') !== false) { | ||
173 | // multiple, space-separated ids provided | ||
174 | $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); | ||
175 | } else { | ||
176 | $ids = [$ids]; | ||
177 | } | ||
178 | |||
179 | // assert at least one id is given | ||
180 | if (0 === count($ids)) { | ||
181 | $this->saveErrorMessage(t('Invalid bookmark ID provided.')); | ||
182 | |||
183 | return $this->redirectFromReferer($request, $response, [], ['delete-shaare']); | ||
184 | } | ||
185 | |||
186 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
187 | $count = 0; | ||
188 | foreach ($ids as $id) { | ||
189 | try { | ||
190 | $bookmark = $this->container->bookmarkService->get((int) $id); | ||
191 | } catch (BookmarkNotFoundException $e) { | ||
192 | $this->saveErrorMessage(sprintf( | ||
193 | t('Bookmark with identifier %s could not be found.'), | ||
194 | $id | ||
195 | )); | ||
196 | |||
197 | continue; | ||
198 | } | ||
199 | |||
200 | $data = $formatter->format($bookmark); | ||
201 | $this->executePageHooks('delete_link', $data); | ||
202 | $this->container->bookmarkService->remove($bookmark, false); | ||
203 | ++ $count; | ||
204 | } | ||
205 | |||
206 | if ($count > 0) { | ||
207 | $this->container->bookmarkService->save(); | ||
208 | } | ||
209 | |||
210 | // If we are called from the bookmarklet, we must close the popup: | ||
211 | if ($request->getParam('source') === 'bookmarklet') { | ||
212 | return $response->write('<script>self.close();</script>'); | ||
213 | } | ||
214 | |||
215 | // Don't redirect to where we were previously because the datastore has changed. | ||
216 | return $this->redirect($response, '/'); | ||
217 | } | ||
218 | |||
219 | /** | ||
220 | * GET /admin/shaare/visibility | ||
221 | * | ||
222 | * Change visibility (public/private) of one or multiple bookmarks (depending on `id` query parameter). | ||
223 | */ | ||
224 | public function changeVisibility(Request $request, Response $response): Response | ||
225 | { | ||
226 | $this->checkToken($request); | ||
227 | |||
228 | $ids = trim(escape($request->getParam('id') ?? '')); | ||
229 | if (empty($ids) || strpos($ids, ' ') !== false) { | ||
230 | // multiple, space-separated ids provided | ||
231 | $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); | ||
232 | } else { | ||
233 | // only a single id provided | ||
234 | $ids = [$ids]; | ||
235 | } | ||
236 | |||
237 | // assert at least one id is given | ||
238 | if (0 === count($ids)) { | ||
239 | $this->saveErrorMessage(t('Invalid bookmark ID provided.')); | ||
240 | |||
241 | return $this->redirectFromReferer($request, $response, [], ['change_visibility']); | ||
242 | } | ||
243 | |||
244 | // assert that the visibility is valid | ||
245 | $visibility = $request->getParam('newVisibility'); | ||
246 | if (null === $visibility || false === in_array($visibility, ['public', 'private'], true)) { | ||
247 | $this->saveErrorMessage(t('Invalid visibility provided.')); | ||
248 | |||
249 | return $this->redirectFromReferer($request, $response, [], ['change_visibility']); | ||
250 | } else { | ||
251 | $isPrivate = $visibility === 'private'; | ||
252 | } | ||
253 | |||
254 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
255 | $count = 0; | ||
256 | |||
257 | foreach ($ids as $id) { | ||
258 | try { | ||
259 | $bookmark = $this->container->bookmarkService->get((int) $id); | ||
260 | } catch (BookmarkNotFoundException $e) { | ||
261 | $this->saveErrorMessage(sprintf( | ||
262 | t('Bookmark with identifier %s could not be found.'), | ||
263 | $id | ||
264 | )); | ||
265 | |||
266 | continue; | ||
267 | } | ||
268 | |||
269 | $bookmark->setPrivate($isPrivate); | ||
270 | |||
271 | // To preserve backward compatibility with 3rd parties, plugins still use arrays | ||
272 | $data = $formatter->format($bookmark); | ||
273 | $this->executePageHooks('save_link', $data); | ||
274 | $bookmark->fromArray($data); | ||
275 | |||
276 | $this->container->bookmarkService->set($bookmark, false); | ||
277 | ++$count; | ||
278 | } | ||
279 | |||
280 | if ($count > 0) { | ||
281 | $this->container->bookmarkService->save(); | ||
282 | } | ||
283 | |||
284 | return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']); | ||
285 | } | ||
286 | |||
287 | /** | ||
288 | * GET /admin/shaare/{id}/pin - Pin or unpin a bookmark. | ||
289 | */ | ||
290 | public function pinBookmark(Request $request, Response $response, array $args): Response | ||
291 | { | ||
292 | $this->checkToken($request); | ||
293 | |||
294 | $id = $args['id'] ?? ''; | ||
295 | try { | ||
296 | if (false === ctype_digit($id)) { | ||
297 | throw new BookmarkNotFoundException(); | ||
298 | } | ||
299 | $bookmark = $this->container->bookmarkService->get((int) $id); // Read database | ||
300 | } catch (BookmarkNotFoundException $e) { | ||
301 | $this->saveErrorMessage(sprintf( | ||
302 | t('Bookmark with identifier %s could not be found.'), | ||
303 | $id | ||
304 | )); | ||
305 | |||
306 | return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); | ||
307 | } | ||
308 | |||
309 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
310 | |||
311 | $bookmark->setSticky(!$bookmark->isSticky()); | ||
312 | |||
313 | // To preserve backward compatibility with 3rd parties, plugins still use arrays | ||
314 | $data = $formatter->format($bookmark); | ||
315 | $this->executePageHooks('save_link', $data); | ||
316 | $bookmark->fromArray($data); | ||
317 | |||
318 | $this->container->bookmarkService->set($bookmark); | ||
319 | |||
320 | return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); | ||
321 | } | ||
322 | |||
323 | /** | ||
324 | * Helper function used to display the shaare form whether it's a new or existing bookmark. | ||
325 | * | ||
326 | * @param array $link data used in template, either from parameters or from the data store | ||
327 | */ | ||
328 | protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response | ||
329 | { | ||
330 | $tags = $this->container->bookmarkService->bookmarksCountPerTag(); | ||
331 | if ($this->container->conf->get('formatter') === 'markdown') { | ||
332 | $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; | ||
333 | } | ||
334 | |||
335 | $data = escape([ | ||
336 | 'link' => $link, | ||
337 | 'link_is_new' => $isNew, | ||
338 | 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '', | ||
339 | 'source' => $request->getParam('source') ?? '', | ||
340 | 'tags' => $tags, | ||
341 | 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false), | ||
342 | 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true), | ||
343 | 'retrieve_description' => $this->container->conf->get('general.retrieve_description', false), | ||
344 | ]); | ||
345 | |||
346 | $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK); | ||
347 | |||
348 | foreach ($data as $key => $value) { | ||
349 | $this->assignView($key, $value); | ||
350 | } | ||
351 | |||
352 | $editLabel = false === $isNew ? t('Edit') .' ' : ''; | ||
353 | $this->assignView( | ||
354 | 'pagetitle', | ||
355 | $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli') | ||
356 | ); | ||
357 | |||
358 | return $response->write($this->render(TemplatePage::EDIT_LINK)); | ||
359 | } | ||
360 | } | ||
diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php new file mode 100644 index 00000000..bfc99422 --- /dev/null +++ b/application/front/controller/admin/ServerController.php | |||
@@ -0,0 +1,87 @@ | |||
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 | $latestVersion = 'v' . ApplicationUtils::getVersion( | ||
29 | ApplicationUtils::$GIT_RAW_URL . '/latest/' . ApplicationUtils::$VERSION_FILE | ||
30 | ); | ||
31 | $currentVersion = ApplicationUtils::getVersion('./shaarli_version.php'); | ||
32 | $currentVersion = $currentVersion === 'dev' ? $currentVersion : 'v' . $currentVersion; | ||
33 | $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION)); | ||
34 | |||
35 | $this->assignView('php_version', PHP_VERSION); | ||
36 | $this->assignView('php_eol', format_date($phpEol, false)); | ||
37 | $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable()); | ||
38 | $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement()); | ||
39 | $this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf)); | ||
40 | $this->assignView('release_url', ApplicationUtils::$GITHUB_URL . '/releases/tag/' . $latestVersion); | ||
41 | $this->assignView('latest_version', $latestVersion); | ||
42 | $this->assignView('current_version', $currentVersion); | ||
43 | $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode')); | ||
44 | $this->assignView('index_url', index_url($this->container->environment)); | ||
45 | $this->assignView('client_ip', client_ip_id($this->container->environment)); | ||
46 | $this->assignView('trusted_proxies', $this->container->conf->get('security.trusted_proxies', [])); | ||
47 | |||
48 | $this->assignView( | ||
49 | 'pagetitle', | ||
50 | t('Server administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') | ||
51 | ); | ||
52 | |||
53 | return $response->write($this->render('server')); | ||
54 | } | ||
55 | |||
56 | /** | ||
57 | * GET /admin/clear-cache?type={$type} - Action to trigger cache folder clearing (either main or thumbnails). | ||
58 | */ | ||
59 | public function clearCache(Request $request, Response $response): Response | ||
60 | { | ||
61 | $exclude = ['.htaccess']; | ||
62 | |||
63 | if ($request->getQueryParam('type') === static::CACHE_THUMB) { | ||
64 | $folders = [$this->container->conf->get('resource.thumbnails_cache')]; | ||
65 | |||
66 | $this->saveWarningMessage( | ||
67 | t('Thumbnails cache has been cleared.') . ' ' . | ||
68 | '<a href="'. $this->container->basePath .'/admin/thumbnails">' . t('Please synchronize them.') .'</a>' | ||
69 | ); | ||
70 | } else { | ||
71 | $folders = [ | ||
72 | $this->container->conf->get('resource.page_cache'), | ||
73 | $this->container->conf->get('resource.raintpl_tmp'), | ||
74 | ]; | ||
75 | |||
76 | $this->saveSuccessMessage(t('Shaarli\'s cache folder has been cleared!')); | ||
77 | } | ||
78 | |||
79 | // Make sure that we don't delete root cache folder | ||
80 | $folders = array_map('realpath', array_values(array_filter(array_map('trim', $folders)))); | ||
81 | foreach ($folders as $folder) { | ||
82 | FileUtils::clearFolder($folder, false, $exclude); | ||
83 | } | ||
84 | |||
85 | return $this->redirect($response, '/admin/server'); | ||
86 | } | ||
87 | } | ||
diff --git a/application/front/controller/admin/ShaareAddController.php b/application/front/controller/admin/ShaareAddController.php new file mode 100644 index 00000000..8dc386b2 --- /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..7ceb8d8a --- /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 where we were previously because the datastore has changed. | ||
70 | return $this->redirect($response, '/'); | ||
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); | ||
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); | ||
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..18afc2d1 --- /dev/null +++ b/application/front/controller/admin/ShaarePublishController.php | |||
@@ -0,0 +1,263 @@ | |||
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($request->getParam('lf_tags')); | ||
117 | |||
118 | if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE | ||
119 | && true !== $this->container->conf->get('general.enable_async_metadata', true) | ||
120 | && $bookmark->shouldUpdateThumbnail() | ||
121 | ) { | ||
122 | $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); | ||
123 | } | ||
124 | $this->container->bookmarkService->addOrSet($bookmark, false); | ||
125 | |||
126 | // To preserve backward compatibility with 3rd parties, plugins still use arrays | ||
127 | $formatter = $this->getFormatter('raw'); | ||
128 | $data = $formatter->format($bookmark); | ||
129 | $this->executePageHooks('save_link', $data); | ||
130 | |||
131 | $bookmark->fromArray($data); | ||
132 | $this->container->bookmarkService->set($bookmark); | ||
133 | |||
134 | // If we are called from the bookmarklet, we must close the popup: | ||
135 | if ($request->getParam('source') === 'bookmarklet') { | ||
136 | return $response->write('<script>self.close();</script>'); | ||
137 | } elseif ($request->getParam('source') === 'batch') { | ||
138 | return $response; | ||
139 | } | ||
140 | |||
141 | if (!empty($request->getParam('returnurl'))) { | ||
142 | $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl'); | ||
143 | } | ||
144 | |||
145 | return $this->redirectFromReferer( | ||
146 | $request, | ||
147 | $response, | ||
148 | ['/admin/add-shaare', '/admin/shaare'], ['addlink', 'post', 'edit_link'], | ||
149 | $bookmark->getShortUrl() | ||
150 | ); | ||
151 | } | ||
152 | |||
153 | /** | ||
154 | * Helper function used to display the shaare form whether it's a new or existing bookmark. | ||
155 | * | ||
156 | * @param array $link data used in template, either from parameters or from the data store | ||
157 | */ | ||
158 | protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response | ||
159 | { | ||
160 | $data = $this->buildFormData($link, $isNew, $request); | ||
161 | |||
162 | $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK); | ||
163 | |||
164 | foreach ($data as $key => $value) { | ||
165 | $this->assignView($key, $value); | ||
166 | } | ||
167 | |||
168 | $editLabel = false === $isNew ? t('Edit') .' ' : ''; | ||
169 | $this->assignView( | ||
170 | 'pagetitle', | ||
171 | $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli') | ||
172 | ); | ||
173 | |||
174 | return $response->write($this->render(TemplatePage::EDIT_LINK)); | ||
175 | } | ||
176 | |||
177 | protected function buildLinkDataFromUrl(Request $request, string $url): array | ||
178 | { | ||
179 | // Check if URL is not already in database (in this case, we will edit the existing link) | ||
180 | $bookmark = $this->container->bookmarkService->findByUrl($url); | ||
181 | if (null === $bookmark) { | ||
182 | // Get shaare data if it was provided in URL (e.g.: by the bookmarklet). | ||
183 | $title = $request->getParam('title'); | ||
184 | $description = $request->getParam('description'); | ||
185 | $tags = $request->getParam('tags'); | ||
186 | if ($request->getParam('private') !== null) { | ||
187 | $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN); | ||
188 | } else { | ||
189 | $private = $this->container->conf->get('privacy.default_private_links', false); | ||
190 | } | ||
191 | |||
192 | // If this is an HTTP(S) link, we try go get the page to extract | ||
193 | // the title (otherwise we will to straight to the edit form.) | ||
194 | if (true !== $this->container->conf->get('general.enable_async_metadata', true) | ||
195 | && empty($title) | ||
196 | && strpos(get_url_scheme($url) ?: '', 'http') !== false | ||
197 | ) { | ||
198 | $metadata = $this->container->metadataRetriever->retrieve($url); | ||
199 | } | ||
200 | |||
201 | if (empty($url)) { | ||
202 | $metadata['title'] = $this->container->conf->get('general.default_note_title', t('Note: ')); | ||
203 | } | ||
204 | |||
205 | return [ | ||
206 | 'title' => $title ?? $metadata['title'] ?? '', | ||
207 | 'url' => $url ?? '', | ||
208 | 'description' => $description ?? $metadata['description'] ?? '', | ||
209 | 'tags' => $tags ?? $metadata['tags'] ?? '', | ||
210 | 'private' => $private, | ||
211 | 'linkIsNew' => true, | ||
212 | ]; | ||
213 | } | ||
214 | |||
215 | $formatter = $this->getFormatter('raw'); | ||
216 | $link = $formatter->format($bookmark); | ||
217 | $link['linkIsNew'] = false; | ||
218 | |||
219 | return $link; | ||
220 | } | ||
221 | |||
222 | protected function buildFormData(array $link, bool $isNew, Request $request): array | ||
223 | { | ||
224 | return escape([ | ||
225 | 'link' => $link, | ||
226 | 'link_is_new' => $isNew, | ||
227 | 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '', | ||
228 | 'source' => $request->getParam('source') ?? '', | ||
229 | 'tags' => $this->getTags(), | ||
230 | 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false), | ||
231 | 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true), | ||
232 | 'retrieve_description' => $this->container->conf->get('general.retrieve_description', false), | ||
233 | ]); | ||
234 | } | ||
235 | |||
236 | /** | ||
237 | * Memoize formatterFactory->getFormatter() calls. | ||
238 | */ | ||
239 | protected function getFormatter(string $type): BookmarkFormatter | ||
240 | { | ||
241 | if (!array_key_exists($type, $this->formatters) || $this->formatters[$type] === null) { | ||
242 | $this->formatters[$type] = $this->container->formatterFactory->getFormatter($type); | ||
243 | } | ||
244 | |||
245 | return $this->formatters[$type]; | ||
246 | } | ||
247 | |||
248 | /** | ||
249 | * Memoize bookmarkService->bookmarksCountPerTag() calls. | ||
250 | */ | ||
251 | protected function getTags(): array | ||
252 | { | ||
253 | if ($this->tags === null) { | ||
254 | $this->tags = $this->container->bookmarkService->bookmarksCountPerTag(); | ||
255 | |||
256 | if ($this->container->conf->get('formatter') === 'markdown') { | ||
257 | $this->tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; | ||
258 | } | ||
259 | } | ||
260 | |||
261 | return $this->tags; | ||
262 | } | ||
263 | } | ||
diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php index a8019ead..78c474c9 100644 --- a/application/front/controller/visitor/BookmarkListController.php +++ b/application/front/controller/visitor/BookmarkListController.php | |||
@@ -137,8 +137,10 @@ class BookmarkListController extends ShaarliVisitorController | |||
137 | */ | 137 | */ |
138 | public function permalink(Request $request, Response $response, array $args): Response | 138 | public function permalink(Request $request, Response $response, array $args): Response |
139 | { | 139 | { |
140 | $privateKey = $request->getParam('key'); | ||
141 | |||
140 | try { | 142 | try { |
141 | $bookmark = $this->container->bookmarkService->findByHash($args['hash']); | 143 | $bookmark = $this->container->bookmarkService->findByHash($args['hash'], $privateKey); |
142 | } catch (BookmarkNotFoundException $e) { | 144 | } catch (BookmarkNotFoundException $e) { |
143 | $this->assignView('error_message', $e->getMessage()); | 145 | $this->assignView('error_message', $e->getMessage()); |
144 | 146 | ||
@@ -169,16 +171,24 @@ class BookmarkListController extends ShaarliVisitorController | |||
169 | */ | 171 | */ |
170 | protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool | 172 | protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool |
171 | { | 173 | { |
172 | // Logged in, not async retrieval, thumbnails enabled, and thumbnail should be updated | 174 | if (false === $this->container->loginManager->isLoggedIn()) { |
173 | if ($this->container->loginManager->isLoggedIn() | 175 | return false; |
174 | && true !== $this->container->conf->get('general.enable_async_metadata', true) | 176 | } |
175 | && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE | 177 | |
176 | && $bookmark->shouldUpdateThumbnail() | 178 | // If thumbnail should be updated, we reset it to null |
177 | ) { | 179 | if ($bookmark->shouldUpdateThumbnail()) { |
178 | $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); | 180 | $bookmark->setThumbnail(null); |
179 | $this->container->bookmarkService->set($bookmark, $writeDatastore); | 181 | |
180 | 182 | // Requires an update, not async retrieval, thumbnails enabled | |
181 | return true; | 183 | if ($bookmark->shouldUpdateThumbnail() |
184 | && true !== $this->container->conf->get('general.enable_async_metadata', true) | ||
185 | && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE | ||
186 | ) { | ||
187 | $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); | ||
188 | $this->container->bookmarkService->set($bookmark, $writeDatastore); | ||
189 | |||
190 | return true; | ||
191 | } | ||
182 | } | 192 | } |
183 | 193 | ||
184 | return false; | 194 | return false; |
diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index 07617cf1..728bc2d8 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php | |||
@@ -5,8 +5,8 @@ declare(strict_types=1); | |||
5 | namespace Shaarli\Front\Controller\Visitor; | 5 | namespace Shaarli\Front\Controller\Visitor; |
6 | 6 | ||
7 | use DateTime; | 7 | use DateTime; |
8 | use DateTimeImmutable; | ||
9 | use Shaarli\Bookmark\Bookmark; | 8 | use Shaarli\Bookmark\Bookmark; |
9 | use Shaarli\Helper\DailyPageHelper; | ||
10 | use Shaarli\Render\TemplatePage; | 10 | use Shaarli\Render\TemplatePage; |
11 | use Slim\Http\Request; | 11 | use Slim\Http\Request; |
12 | use Slim\Http\Response; | 12 | use Slim\Http\Response; |
@@ -26,32 +26,20 @@ class DailyController extends ShaarliVisitorController | |||
26 | */ | 26 | */ |
27 | public function index(Request $request, Response $response): Response | 27 | public function index(Request $request, Response $response): Response |
28 | { | 28 | { |
29 | $day = $request->getQueryParam('day') ?? date('Ymd'); | 29 | $type = DailyPageHelper::extractRequestedType($request); |
30 | 30 | $format = DailyPageHelper::getFormatByType($type); | |
31 | $availableDates = $this->container->bookmarkService->days(); | 31 | $latestBookmark = $this->container->bookmarkService->getLatest(); |
32 | $nbAvailableDates = count($availableDates); | 32 | $dateTime = DailyPageHelper::extractRequestedDateTime($type, $request->getQueryParam($type), $latestBookmark); |
33 | $index = array_search($day, $availableDates); | 33 | $start = DailyPageHelper::getStartDateTimeByType($type, $dateTime); |
34 | 34 | $end = DailyPageHelper::getEndDateTimeByType($type, $dateTime); | |
35 | if ($index === false) { | 35 | $dailyDesc = DailyPageHelper::getDescriptionByType($type, $dateTime); |
36 | // no bookmarks for day, but at least one day with bookmarks | 36 | |
37 | $day = $availableDates[$nbAvailableDates - 1] ?? $day; | 37 | $linksToDisplay = $this->container->bookmarkService->findByDate( |
38 | $previousDay = $availableDates[$nbAvailableDates - 2] ?? ''; | 38 | $start, |
39 | } else { | 39 | $end, |
40 | $previousDay = $availableDates[$index - 1] ?? ''; | 40 | $previousDay, |
41 | $nextDay = $availableDates[$index + 1] ?? ''; | 41 | $nextDay |
42 | } | 42 | ); |
43 | |||
44 | if ($day === date('Ymd')) { | ||
45 | $this->assignView('dayDesc', t('Today')); | ||
46 | } elseif ($day === date('Ymd', strtotime('-1 days'))) { | ||
47 | $this->assignView('dayDesc', t('Yesterday')); | ||
48 | } | ||
49 | |||
50 | try { | ||
51 | $linksToDisplay = $this->container->bookmarkService->filterDay($day); | ||
52 | } catch (\Exception $exc) { | ||
53 | $linksToDisplay = []; | ||
54 | } | ||
55 | 43 | ||
56 | $formatter = $this->container->formatterFactory->getFormatter(); | 44 | $formatter = $this->container->formatterFactory->getFormatter(); |
57 | $formatter->addContextData('base_path', $this->container->basePath); | 45 | $formatter->addContextData('base_path', $this->container->basePath); |
@@ -63,13 +51,15 @@ class DailyController extends ShaarliVisitorController | |||
63 | $linksToDisplay[$key]['description'] = $bookmark->getDescription(); | 51 | $linksToDisplay[$key]['description'] = $bookmark->getDescription(); |
64 | } | 52 | } |
65 | 53 | ||
66 | $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); | ||
67 | $data = [ | 54 | $data = [ |
68 | 'linksToDisplay' => $linksToDisplay, | 55 | 'linksToDisplay' => $linksToDisplay, |
69 | 'day' => $dayDate->getTimestamp(), | 56 | 'dayDate' => $start, |
70 | 'dayDate' => $dayDate, | 57 | 'day' => $start->getTimestamp(), |
71 | 'previousday' => $previousDay ?? '', | 58 | 'previousday' => $previousDay ? $previousDay->format($format) : '', |
72 | 'nextday' => $nextDay ?? '', | 59 | 'nextday' => $nextDay ? $nextDay->format($format) : '', |
60 | 'dayDesc' => $dailyDesc, | ||
61 | 'type' => $type, | ||
62 | 'localizedType' => $this->translateType($type), | ||
73 | ]; | 63 | ]; |
74 | 64 | ||
75 | // Hooks are called before column construction so that plugins don't have to deal with columns. | 65 | // Hooks are called before column construction so that plugins don't have to deal with columns. |
@@ -82,7 +72,7 @@ class DailyController extends ShaarliVisitorController | |||
82 | $mainTitle = $this->container->conf->get('general.title', 'Shaarli'); | 72 | $mainTitle = $this->container->conf->get('general.title', 'Shaarli'); |
83 | $this->assignView( | 73 | $this->assignView( |
84 | 'pagetitle', | 74 | 'pagetitle', |
85 | t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle | 75 | $data['localizedType'] . ' - ' . $data['dayDesc'] . ' - ' . $mainTitle |
86 | ); | 76 | ); |
87 | 77 | ||
88 | return $response->write($this->render(TemplatePage::DAILY)); | 78 | return $response->write($this->render(TemplatePage::DAILY)); |
@@ -106,11 +96,14 @@ class DailyController extends ShaarliVisitorController | |||
106 | } | 96 | } |
107 | 97 | ||
108 | $days = []; | 98 | $days = []; |
99 | $type = DailyPageHelper::extractRequestedType($request); | ||
100 | $format = DailyPageHelper::getFormatByType($type); | ||
101 | $length = DailyPageHelper::getRssLengthByType($type); | ||
109 | foreach ($this->container->bookmarkService->search() as $bookmark) { | 102 | foreach ($this->container->bookmarkService->search() as $bookmark) { |
110 | $day = $bookmark->getCreated()->format('Ymd'); | 103 | $day = $bookmark->getCreated()->format($format); |
111 | 104 | ||
112 | // Stop iterating after DAILY_RSS_NB_DAYS entries | 105 | // Stop iterating after DAILY_RSS_NB_DAYS entries |
113 | if (count($days) === static::$DAILY_RSS_NB_DAYS && !isset($days[$day])) { | 106 | if (count($days) === $length && !isset($days[$day])) { |
114 | break; | 107 | break; |
115 | } | 108 | } |
116 | 109 | ||
@@ -127,12 +120,19 @@ class DailyController extends ShaarliVisitorController | |||
127 | 120 | ||
128 | /** @var Bookmark[] $bookmarks */ | 121 | /** @var Bookmark[] $bookmarks */ |
129 | foreach ($days as $day => $bookmarks) { | 122 | foreach ($days as $day => $bookmarks) { |
130 | $dayDatetime = DateTimeImmutable::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); | 123 | $dayDateTime = DailyPageHelper::extractRequestedDateTime($type, (string) $day); |
124 | $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dayDateTime); | ||
125 | |||
126 | // We only want the RSS entry to be published when the period is over. | ||
127 | if (new DateTime() < $endDateTime) { | ||
128 | continue; | ||
129 | } | ||
130 | |||
131 | $dataPerDay[$day] = [ | 131 | $dataPerDay[$day] = [ |
132 | 'date' => $dayDatetime, | 132 | 'date' => $endDateTime, |
133 | 'date_rss' => $dayDatetime->format(DateTime::RSS), | 133 | 'date_rss' => $endDateTime->format(DateTime::RSS), |
134 | 'date_human' => format_date($dayDatetime, false, true), | 134 | 'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime), |
135 | 'absolute_url' => $indexUrl . 'daily?day=' . $day, | 135 | 'absolute_url' => $indexUrl . 'daily?'. $type .'=' . $day, |
136 | 'links' => [], | 136 | 'links' => [], |
137 | ]; | 137 | ]; |
138 | 138 | ||
@@ -141,16 +141,20 @@ class DailyController extends ShaarliVisitorController | |||
141 | 141 | ||
142 | // Make permalink URL absolute | 142 | // Make permalink URL absolute |
143 | if ($bookmark->isNote()) { | 143 | if ($bookmark->isNote()) { |
144 | $dataPerDay[$day]['links'][$key]['url'] = $indexUrl . $bookmark->getUrl(); | 144 | $dataPerDay[$day]['links'][$key]['url'] = rtrim($indexUrl, '/') . $bookmark->getUrl(); |
145 | } | 145 | } |
146 | } | 146 | } |
147 | } | 147 | } |
148 | 148 | ||
149 | $this->assignView('title', $this->container->conf->get('general.title', 'Shaarli')); | 149 | $this->assignAllView([ |
150 | $this->assignView('index_url', $indexUrl); | 150 | 'title' => $this->container->conf->get('general.title', 'Shaarli'), |
151 | $this->assignView('page_url', $pageUrl); | 151 | 'index_url' => $indexUrl, |
152 | $this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false)); | 152 | 'page_url' => $pageUrl, |
153 | $this->assignView('days', $dataPerDay); | 153 | 'hide_timestamps' => $this->container->conf->get('privacy.hide_timestamps', false), |
154 | 'days' => $dataPerDay, | ||
155 | 'type' => $type, | ||
156 | 'localizedType' => $this->translateType($type), | ||
157 | ]); | ||
154 | 158 | ||
155 | $rssContent = $this->render(TemplatePage::DAILY_RSS); | 159 | $rssContent = $this->render(TemplatePage::DAILY_RSS); |
156 | 160 | ||
@@ -189,4 +193,13 @@ class DailyController extends ShaarliVisitorController | |||
189 | 193 | ||
190 | return $columns; | 194 | return $columns; |
191 | } | 195 | } |
196 | |||
197 | protected function translateType($type): string | ||
198 | { | ||
199 | return [ | ||
200 | t('day') => t('Daily'), | ||
201 | t('week') => t('Weekly'), | ||
202 | t('month') => t('Monthly'), | ||
203 | ][t($type)] ?? t('Daily'); | ||
204 | } | ||
192 | } | 205 | } |
diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php index 7cb32777..22329294 100644 --- a/application/front/controller/visitor/InstallController.php +++ b/application/front/controller/visitor/InstallController.php | |||
@@ -4,10 +4,10 @@ declare(strict_types=1); | |||
4 | 4 | ||
5 | namespace Shaarli\Front\Controller\Visitor; | 5 | namespace Shaarli\Front\Controller\Visitor; |
6 | 6 | ||
7 | use Shaarli\ApplicationUtils; | ||
8 | use Shaarli\Container\ShaarliContainer; | 7 | use Shaarli\Container\ShaarliContainer; |
9 | use Shaarli\Front\Exception\AlreadyInstalledException; | 8 | use Shaarli\Front\Exception\AlreadyInstalledException; |
10 | use Shaarli\Front\Exception\ResourcePermissionException; | 9 | use Shaarli\Front\Exception\ResourcePermissionException; |
10 | use Shaarli\Helper\ApplicationUtils; | ||
11 | use Shaarli\Languages; | 11 | use Shaarli\Languages; |
12 | use Shaarli\Security\SessionManager; | 12 | use Shaarli\Security\SessionManager; |
13 | use Slim\Http\Request; | 13 | use Slim\Http\Request; |
@@ -53,6 +53,16 @@ class InstallController extends ShaarliVisitorController | |||
53 | $this->assignView('cities', $cities); | 53 | $this->assignView('cities', $cities); |
54 | $this->assignView('languages', Languages::getAvailableLanguages()); | 54 | $this->assignView('languages', Languages::getAvailableLanguages()); |
55 | 55 | ||
56 | $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION)); | ||
57 | |||
58 | $this->assignView('php_version', PHP_VERSION); | ||
59 | $this->assignView('php_eol', format_date($phpEol, false)); | ||
60 | $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable()); | ||
61 | $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement()); | ||
62 | $this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf)); | ||
63 | |||
64 | $this->assignView('pagetitle', t('Install Shaarli')); | ||
65 | |||
56 | return $response->write($this->render('install')); | 66 | return $response->write($this->render('install')); |
57 | } | 67 | } |
58 | 68 | ||
@@ -150,7 +160,7 @@ class InstallController extends ShaarliVisitorController | |||
150 | protected function checkPermissions(): bool | 160 | protected function checkPermissions(): bool |
151 | { | 161 | { |
152 | // Ensure Shaarli has proper access to its resources | 162 | // Ensure Shaarli has proper access to its resources |
153 | $errors = ApplicationUtils::checkResourcePermissions($this->container->conf); | 163 | $errors = ApplicationUtils::checkResourcePermissions($this->container->conf, true); |
154 | if (empty($errors)) { | 164 | if (empty($errors)) { |
155 | return true; | 165 | return true; |
156 | } | 166 | } |