aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Dockerfile1
-rw-r--r--application/History.php1
-rw-r--r--application/Utils.php33
-rw-r--r--application/api/controllers/Links.php2
-rw-r--r--application/bookmark/BookmarkFileService.php47
-rw-r--r--application/bookmark/BookmarkServiceInterface.php32
-rw-r--r--application/config/ConfigJson.php6
-rw-r--r--application/front/controller/admin/ManageShaareController.php360
-rw-r--r--application/front/controller/admin/ServerController.php87
-rw-r--r--application/front/controller/admin/ShaareAddController.php34
-rw-r--r--application/front/controller/admin/ShaareManageController.php202
-rw-r--r--application/front/controller/admin/ShaarePublishController.php263
-rw-r--r--application/front/controller/visitor/BookmarkListController.php32
-rw-r--r--application/front/controller/visitor/DailyController.php105
-rw-r--r--application/front/controller/visitor/InstallController.php14
-rw-r--r--application/helper/ApplicationUtils.php (renamed from application/ApplicationUtils.php)95
-rw-r--r--application/helper/DailyPageHelper.php208
-rw-r--r--application/helper/FileUtils.php (renamed from application/FileUtils.php)58
-rw-r--r--application/legacy/LegacyLinkDB.php2
-rw-r--r--application/legacy/LegacyUpdater.php2
-rw-r--r--application/render/PageBuilder.php4
-rw-r--r--application/render/TemplatePage.php1
-rw-r--r--application/security/BanManager.php2
-rw-r--r--application/security/SessionManager.php7
-rw-r--r--assets/common/js/metadata.js50
-rw-r--r--assets/common/js/shaare-batch.js121
-rw-r--r--assets/default/js/base.js29
-rw-r--r--assets/default/scss/shaarli.scss128
-rw-r--r--composer.json1
-rw-r--r--inc/languages/fr/LC_MESSAGES/shaarli.po585
-rw-r--r--index.php18
-rw-r--r--init.php2
-rw-r--r--tests/api/controllers/links/PostLinkTest.php8
-rw-r--r--tests/bookmark/BookmarkFileServiceTest.php155
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php47
-rw-r--r--tests/front/controller/admin/ServerControllerTest.php184
-rw-r--r--tests/front/controller/admin/ShaareAddControllerTest.php97
-rw-r--r--tests/front/controller/admin/ShaareManageControllerTest/ChangeVisibilityBookmarkTest.php (renamed from tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php)8
-rw-r--r--tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php (renamed from tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php)8
-rw-r--r--tests/front/controller/admin/ShaareManageControllerTest/PinBookmarkTest.php (renamed from tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php)8
-rw-r--r--tests/front/controller/admin/ShaareManageControllerTest/SharePrivateTest.php139
-rw-r--r--tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php63
-rw-r--r--tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateFormTest.php (renamed from tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php)8
-rw-r--r--tests/front/controller/admin/ShaarePublishControllerTest/DisplayEditFormTest.php (renamed from tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php)8
-rw-r--r--tests/front/controller/admin/ShaarePublishControllerTest/SaveBookmarkTest.php (renamed from tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php)8
-rw-r--r--tests/front/controller/visitor/BookmarkListControllerTest.php31
-rw-r--r--tests/front/controller/visitor/DailyControllerTest.php412
-rw-r--r--tests/front/controller/visitor/InstallControllerTest.php9
-rw-r--r--tests/helper/ApplicationUtilsTest.php (renamed from tests/ApplicationUtilsTest.php)65
-rw-r--r--tests/helper/DailyPageHelperTest.php262
-rw-r--r--tests/helper/FileUtilsTest.php (renamed from tests/FileUtilsTest.php)91
-rw-r--r--tests/security/BanManagerTest.php2
-rw-r--r--tests/utils/FakeApplicationUtils.php2
-rw-r--r--tests/utils/ReferenceHistory.php2
-rw-r--r--tpl/default/addlink.html56
-rw-r--r--tpl/default/daily.html32
-rw-r--r--tpl/default/dailyrss.html11
-rw-r--r--tpl/default/editlink.batch.html32
-rw-r--r--tpl/default/editlink.html17
-rw-r--r--tpl/default/install.html10
-rw-r--r--tpl/default/linklist.html7
-rw-r--r--tpl/default/server.html129
-rw-r--r--tpl/default/server.requirements.html68
-rw-r--r--tpl/default/tools.html14
-rw-r--r--webpack.config.js1
65 files changed, 3616 insertions, 910 deletions
diff --git a/Dockerfile b/Dockerfile
index e2ff71fd..f6120b71 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -44,6 +44,7 @@ RUN apk --update --no-cache add \
44 php7-openssl \ 44 php7-openssl \
45 php7-session \ 45 php7-session \
46 php7-xml \ 46 php7-xml \
47 php7-simplexml \
47 php7-zlib \ 48 php7-zlib \
48 s6 49 s6
49 50
diff --git a/application/History.php b/application/History.php
index 4fd2f294..bd5c1bf7 100644
--- a/application/History.php
+++ b/application/History.php
@@ -4,6 +4,7 @@ namespace Shaarli;
4use DateTime; 4use DateTime;
5use Exception; 5use Exception;
6use Shaarli\Bookmark\Bookmark; 6use Shaarli\Bookmark\Bookmark;
7use Shaarli\Helper\FileUtils;
7 8
8/** 9/**
9 * Class History 10 * Class History
diff --git a/application/Utils.php b/application/Utils.php
index bc1c9f5d..db046893 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -327,6 +327,23 @@ function format_date($date, $time = true, $intl = true)
327} 327}
328 328
329/** 329/**
330 * Format the date month according to the locale.
331 *
332 * @param DateTimeInterface $date to format.
333 *
334 * @return bool|string Formatted date, or false if the input is invalid.
335 */
336function format_month(DateTimeInterface $date)
337{
338 if (! $date instanceof DateTimeInterface) {
339 return false;
340 }
341
342 return strftime('%B', $date->getTimestamp());
343}
344
345
346/**
330 * Check if the input is an integer, no matter its real type. 347 * Check if the input is an integer, no matter its real type.
331 * 348 *
332 * PHP is a bit messy regarding this: 349 * PHP is a bit messy regarding this:
@@ -454,16 +471,20 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
454 * Wrapper function for translation which match the API 471 * Wrapper function for translation which match the API
455 * of gettext()/_() and ngettext(). 472 * of gettext()/_() and ngettext().
456 * 473 *
457 * @param string $text Text to translate. 474 * @param string $text Text to translate.
458 * @param string $nText The plural message ID. 475 * @param string $nText The plural message ID.
459 * @param int $nb The number of items for plural forms. 476 * @param int $nb The number of items for plural forms.
460 * @param string $domain The domain where the translation is stored (default: shaarli). 477 * @param string $domain The domain where the translation is stored (default: shaarli).
478 * @param array $variables Associative array of variables to replace in translated text.
479 * @param bool $fixCase Apply `ucfirst` on the translated string, might be useful for strings with variables.
461 * 480 *
462 * @return string Text translated. 481 * @return string Text translated.
463 */ 482 */
464function t($text, $nText = '', $nb = 1, $domain = 'shaarli') 483function t($text, $nText = '', $nb = 1, $domain = 'shaarli', $variables = [], $fixCase = false)
465{ 484{
466 return dn__($domain, $text, $nText, $nb); 485 $postFunction = $fixCase ? 'ucfirst' : function ($input) { return $input; };
486
487 return $postFunction(dn__($domain, $text, $nText, $nb, $variables));
467} 488}
468 489
469/** 490/**
diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php
index 73a1b84e..6bf529e4 100644
--- a/application/api/controllers/Links.php
+++ b/application/api/controllers/Links.php
@@ -131,7 +131,7 @@ class Links extends ApiController
131 131
132 $this->bookmarkService->add($bookmark); 132 $this->bookmarkService->add($bookmark);
133 $out = ApiUtils::formatLink($bookmark, index_url($this->ci['environment'])); 133 $out = ApiUtils::formatLink($bookmark, index_url($this->ci['environment']));
134 $redirect = $this->ci->router->relativePathFor('getLink', ['id' => $bookmark->getId()]); 134 $redirect = $this->ci->router->pathFor('getLink', ['id' => $bookmark->getId()]);
135 return $response->withAddedHeader('Location', $redirect) 135 return $response->withAddedHeader('Location', $redirect)
136 ->withJson($out, 201, $this->jsonStyle); 136 ->withJson($out, 201, $this->jsonStyle);
137 } 137 }
diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php
index eb7899bf..3ea98a45 100644
--- a/application/bookmark/BookmarkFileService.php
+++ b/application/bookmark/BookmarkFileService.php
@@ -97,13 +97,16 @@ class BookmarkFileService implements BookmarkServiceInterface
97 /** 97 /**
98 * @inheritDoc 98 * @inheritDoc
99 */ 99 */
100 public function findByHash(string $hash): Bookmark 100 public function findByHash(string $hash, string $privateKey = null): Bookmark
101 { 101 {
102 $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash); 102 $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash);
103 // PHP 7.3 introduced array_key_first() to avoid this hack 103 // PHP 7.3 introduced array_key_first() to avoid this hack
104 $first = reset($bookmark); 104 $first = reset($bookmark);
105 if (! $this->isLoggedIn && $first->isPrivate()) { 105 if (!$this->isLoggedIn
106 throw new Exception('Not authorized'); 106 && $first->isPrivate()
107 && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key'))
108 ) {
109 throw new BookmarkNotFoundException();
107 } 110 }
108 111
109 return $first; 112 return $first;
@@ -340,26 +343,42 @@ class BookmarkFileService implements BookmarkServiceInterface
340 /** 343 /**
341 * @inheritDoc 344 * @inheritDoc
342 */ 345 */
343 public function days(): array 346 public function findByDate(
344 { 347 \DateTimeInterface $from,
345 $bookmarkDays = []; 348 \DateTimeInterface $to,
346 foreach ($this->search() as $bookmark) { 349 ?\DateTimeInterface &$previous,
347 $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0; 350 ?\DateTimeInterface &$next
351 ): array {
352 $out = [];
353 $previous = null;
354 $next = null;
355
356 foreach ($this->search([], null, false, false, true) as $bookmark) {
357 if ($to < $bookmark->getCreated()) {
358 $next = $bookmark->getCreated();
359 } else if ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) {
360 $out[] = $bookmark;
361 } else {
362 if ($previous !== null) {
363 break;
364 }
365 $previous = $bookmark->getCreated();
366 }
348 } 367 }
349 $bookmarkDays = array_keys($bookmarkDays);
350 sort($bookmarkDays);
351 368
352 return array_map('strval', $bookmarkDays); 369 return $out;
353 } 370 }
354 371
355 /** 372 /**
356 * @inheritDoc 373 * @inheritDoc
357 */ 374 */
358 public function filterDay(string $request) 375 public function getLatest(): ?Bookmark
359 { 376 {
360 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; 377 foreach ($this->search([], null, false, false, true) as $bookmark) {
378 return $bookmark;
379 }
361 380
362 return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request, false, $visibility); 381 return null;
363 } 382 }
364 383
365 /** 384 /**
diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php
index 37a54d03..08cdbb4e 100644
--- a/application/bookmark/BookmarkServiceInterface.php
+++ b/application/bookmark/BookmarkServiceInterface.php
@@ -20,13 +20,14 @@ interface BookmarkServiceInterface
20 /** 20 /**
21 * Find a bookmark by hash 21 * Find a bookmark by hash
22 * 22 *
23 * @param string $hash 23 * @param string $hash Bookmark's hash
24 * @param string|null $privateKey Optional key used to access private links while logged out
24 * 25 *
25 * @return Bookmark 26 * @return Bookmark
26 * 27 *
27 * @throws \Exception 28 * @throws \Exception
28 */ 29 */
29 public function findByHash(string $hash): Bookmark; 30 public function findByHash(string $hash, string $privateKey = null);
30 31
31 /** 32 /**
32 * @param $url 33 * @param $url
@@ -155,22 +156,29 @@ interface BookmarkServiceInterface
155 public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array; 156 public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array;
156 157
157 /** 158 /**
158 * Returns the list of days containing articles (oldest first) 159 * Return a list of bookmark matching provided period of time.
160 * It also update directly previous and next date outside of given period found in the datastore.
159 * 161 *
160 * @return array containing days (in format YYYYMMDD). 162 * @param \DateTimeInterface $from Starting date.
163 * @param \DateTimeInterface $to Ending date.
164 * @param \DateTimeInterface|null $previous (by reference) updated with first created date found before $from.
165 * @param \DateTimeInterface|null $next (by reference) updated with first created date found after $to.
166 *
167 * @return array List of bookmarks matching provided period of time.
161 */ 168 */
162 public function days(): array; 169 public function findByDate(
170 \DateTimeInterface $from,
171 \DateTimeInterface $to,
172 ?\DateTimeInterface &$previous,
173 ?\DateTimeInterface &$next
174 ): array;
163 175
164 /** 176 /**
165 * Returns the list of articles for a given day. 177 * Returns the latest bookmark by creation date.
166 *
167 * @param string $request day to filter. Format: YYYYMMDD.
168 * 178 *
169 * @return Bookmark[] list of shaare found. 179 * @return Bookmark|null Found Bookmark or null if the datastore is empty.
170 *
171 * @throws BookmarkNotFoundException
172 */ 180 */
173 public function filterDay(string $request); 181 public function getLatest(): ?Bookmark;
174 182
175 /** 183 /**
176 * Creates the default database after a fresh install. 184 * Creates the default database after a fresh install.
diff --git a/application/config/ConfigJson.php b/application/config/ConfigJson.php
index c0c0dab9..23b22269 100644
--- a/application/config/ConfigJson.php
+++ b/application/config/ConfigJson.php
@@ -19,7 +19,7 @@ class ConfigJson implements ConfigIO
19 $data = file_get_contents($filepath); 19 $data = file_get_contents($filepath);
20 $data = str_replace(self::getPhpHeaders(), '', $data); 20 $data = str_replace(self::getPhpHeaders(), '', $data);
21 $data = str_replace(self::getPhpSuffix(), '', $data); 21 $data = str_replace(self::getPhpSuffix(), '', $data);
22 $data = json_decode($data, true); 22 $data = json_decode(trim($data), true);
23 if ($data === null) { 23 if ($data === null) {
24 $errorCode = json_last_error(); 24 $errorCode = json_last_error();
25 $error = sprintf( 25 $error = sprintf(
@@ -73,7 +73,7 @@ class ConfigJson implements ConfigIO
73 */ 73 */
74 public static function getPhpHeaders() 74 public static function getPhpHeaders()
75 { 75 {
76 return '<?php /*'. PHP_EOL; 76 return '<?php /*';
77 } 77 }
78 78
79 /** 79 /**
@@ -85,6 +85,6 @@ class ConfigJson implements ConfigIO
85 */ 85 */
86 public static function getPhpSuffix() 86 public static function getPhpSuffix()
87 { 87 {
88 return PHP_EOL . '*/ ?>'; 88 return '*/ ?>';
89 } 89 }
90} 90}
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
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Formatter\BookmarkMarkdownFormatter;
10use Shaarli\Render\TemplatePage;
11use Shaarli\Thumbnailer;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15/**
16 * Class PostBookmarkController
17 *
18 * Slim controller used to handle Shaarli create or edit bookmarks.
19 */
20class 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
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Helper\ApplicationUtils;
8use Shaarli\Helper\FileUtils;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Slim controller used to handle Server administration page, and actions.
14 */
15class 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
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Formatter\BookmarkMarkdownFormatter;
8use Shaarli\Render\TemplatePage;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12class 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
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Class PostBookmarkController
13 *
14 * Slim controller used to handle Shaarli create or edit bookmarks.
15 */
16class 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
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Formatter\BookmarkFormatter;
10use Shaarli\Formatter\BookmarkMarkdownFormatter;
11use Shaarli\Render\TemplatePage;
12use Shaarli\Thumbnailer;
13use Slim\Http\Request;
14use Slim\Http\Response;
15
16class 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);
5namespace Shaarli\Front\Controller\Visitor; 5namespace Shaarli\Front\Controller\Visitor;
6 6
7use DateTime; 7use DateTime;
8use DateTimeImmutable;
9use Shaarli\Bookmark\Bookmark; 8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Helper\DailyPageHelper;
10use Shaarli\Render\TemplatePage; 10use Shaarli\Render\TemplatePage;
11use Slim\Http\Request; 11use Slim\Http\Request;
12use Slim\Http\Response; 12use 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
5namespace Shaarli\Front\Controller\Visitor; 5namespace Shaarli\Front\Controller\Visitor;
6 6
7use Shaarli\ApplicationUtils;
8use Shaarli\Container\ShaarliContainer; 7use Shaarli\Container\ShaarliContainer;
9use Shaarli\Front\Exception\AlreadyInstalledException; 8use Shaarli\Front\Exception\AlreadyInstalledException;
10use Shaarli\Front\Exception\ResourcePermissionException; 9use Shaarli\Front\Exception\ResourcePermissionException;
10use Shaarli\Helper\ApplicationUtils;
11use Shaarli\Languages; 11use Shaarli\Languages;
12use Shaarli\Security\SessionManager; 12use Shaarli\Security\SessionManager;
13use Slim\Http\Request; 13use 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 }
diff --git a/application/ApplicationUtils.php b/application/helper/ApplicationUtils.php
index 3aa21829..4b34e114 100644
--- a/application/ApplicationUtils.php
+++ b/application/helper/ApplicationUtils.php
@@ -1,5 +1,5 @@
1<?php 1<?php
2namespace Shaarli; 2namespace Shaarli\Helper;
3 3
4use Exception; 4use Exception;
5use Shaarli\Config\ConfigManager; 5use Shaarli\Config\ConfigManager;
@@ -14,8 +14,9 @@ class ApplicationUtils
14 */ 14 */
15 public static $VERSION_FILE = 'shaarli_version.php'; 15 public static $VERSION_FILE = 'shaarli_version.php';
16 16
17 private static $GIT_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli'; 17 public static $GITHUB_URL = 'https://github.com/shaarli/Shaarli';
18 private static $GIT_BRANCHES = array('latest', 'stable'); 18 public static $GIT_RAW_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli';
19 public static $GIT_BRANCHES = array('latest', 'stable');
19 private static $VERSION_START_TAG = '<?php /* '; 20 private static $VERSION_START_TAG = '<?php /* ';
20 private static $VERSION_END_TAG = ' */ ?>'; 21 private static $VERSION_END_TAG = ' */ ?>';
21 22
@@ -125,7 +126,7 @@ class ApplicationUtils
125 // Late Static Binding allows overriding within tests 126 // Late Static Binding allows overriding within tests
126 // See http://php.net/manual/en/language.oop5.late-static-bindings.php 127 // See http://php.net/manual/en/language.oop5.late-static-bindings.php
127 $latestVersion = static::getVersion( 128 $latestVersion = static::getVersion(
128 self::$GIT_URL . '/' . $branch . '/' . self::$VERSION_FILE 129 self::$GIT_RAW_URL . '/' . $branch . '/' . self::$VERSION_FILE
129 ); 130 );
130 131
131 if (!$latestVersion) { 132 if (!$latestVersion) {
@@ -171,35 +172,45 @@ class ApplicationUtils
171 /** 172 /**
172 * Checks Shaarli has the proper access permissions to its resources 173 * Checks Shaarli has the proper access permissions to its resources
173 * 174 *
174 * @param ConfigManager $conf Configuration Manager instance. 175 * @param ConfigManager $conf Configuration Manager instance.
176 * @param bool $minimalMode In minimal mode we only check permissions to be able to display a template.
177 * Currently we only need to be able to read the theme and write in raintpl cache.
175 * 178 *
176 * @return array A list of the detected configuration issues 179 * @return array A list of the detected configuration issues
177 */ 180 */
178 public static function checkResourcePermissions($conf) 181 public static function checkResourcePermissions(ConfigManager $conf, bool $minimalMode = false): array
179 { 182 {
180 $errors = array(); 183 $errors = [];
181 $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/'); 184 $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/');
182 185
183 // Check script and template directories are readable 186 // Check script and template directories are readable
184 foreach (array( 187 foreach ([
185 'application', 188 'application',
186 'inc', 189 'inc',
187 'plugins', 190 'plugins',
188 $rainTplDir, 191 $rainTplDir,
189 $rainTplDir . '/' . $conf->get('resource.theme'), 192 $rainTplDir . '/' . $conf->get('resource.theme'),
190 ) as $path) { 193 ] as $path) {
191 if (!is_readable(realpath($path))) { 194 if (!is_readable(realpath($path))) {
192 $errors[] = '"' . $path . '" ' . t('directory is not readable'); 195 $errors[] = '"' . $path . '" ' . t('directory is not readable');
193 } 196 }
194 } 197 }
195 198
196 // Check cache and data directories are readable and writable 199 // Check cache and data directories are readable and writable
197 foreach (array( 200 if ($minimalMode) {
198 $conf->get('resource.thumbnails_cache'), 201 $folders = [
199 $conf->get('resource.data_dir'), 202 $conf->get('resource.raintpl_tmp'),
200 $conf->get('resource.page_cache'), 203 ];
201 $conf->get('resource.raintpl_tmp'), 204 } else {
202 ) as $path) { 205 $folders = [
206 $conf->get('resource.thumbnails_cache'),
207 $conf->get('resource.data_dir'),
208 $conf->get('resource.page_cache'),
209 $conf->get('resource.raintpl_tmp'),
210 ];
211 }
212
213 foreach ($folders as $path) {
203 if (!is_readable(realpath($path))) { 214 if (!is_readable(realpath($path))) {
204 $errors[] = '"' . $path . '" ' . t('directory is not readable'); 215 $errors[] = '"' . $path . '" ' . t('directory is not readable');
205 } 216 }
@@ -208,6 +219,10 @@ class ApplicationUtils
208 } 219 }
209 } 220 }
210 221
222 if ($minimalMode) {
223 return $errors;
224 }
225
211 // Check configuration files are readable and writable 226 // Check configuration files are readable and writable
212 foreach (array( 227 foreach (array(
213 $conf->getConfigFileExt(), 228 $conf->getConfigFileExt(),
@@ -246,4 +261,54 @@ class ApplicationUtils
246 { 261 {
247 return hash_hmac('sha256', $currentVersion, $salt); 262 return hash_hmac('sha256', $currentVersion, $salt);
248 } 263 }
264
265 /**
266 * Get a list of PHP extensions used by Shaarli.
267 *
268 * @return array[] List of extension with following keys:
269 * - name: extension name
270 * - required: whether the extension is required to use Shaarli
271 * - desc: short description of extension usage in Shaarli
272 * - loaded: whether the extension is properly loaded or not
273 */
274 public static function getPhpExtensionsRequirement(): array
275 {
276 $extensions = [
277 ['name' => 'json', 'required' => true, 'desc' => t('Configuration parsing')],
278 ['name' => 'simplexml', 'required' => true, 'desc' => t('Slim Framework (routing, etc.)')],
279 ['name' => 'mbstring', 'required' => true, 'desc' => t('Multibyte (Unicode) string support')],
280 ['name' => 'gd', 'required' => false, 'desc' => t('Required to use thumbnails')],
281 ['name' => 'intl', 'required' => false, 'desc' => t('Localized text sorting (e.g. e->è->f)')],
282 ['name' => 'curl', 'required' => false, 'desc' => t('Better retrieval of bookmark metadata and thumbnail')],
283 ['name' => 'gettext', 'required' => false, 'desc' => t('Use the translation system in gettext mode')],
284 ['name' => 'ldap', 'required' => false, 'desc' => t('Login using LDAP server')],
285 ];
286
287 foreach ($extensions as &$extension) {
288 $extension['loaded'] = extension_loaded($extension['name']);
289 }
290
291 return $extensions;
292 }
293
294 /**
295 * Return the EOL date of given PHP version. If the version is unknown,
296 * we return today + 2 years.
297 *
298 * @param string $fullVersion PHP version, e.g. 7.4.7
299 *
300 * @return string Date format: YYYY-MM-DD
301 */
302 public static function getPhpEol(string $fullVersion): string
303 {
304 preg_match('/(\d+\.\d+)\.\d+/', $fullVersion, $matches);
305
306 return [
307 '7.1' => '2019-12-01',
308 '7.2' => '2020-11-30',
309 '7.3' => '2021-12-06',
310 '7.4' => '2022-11-28',
311 '8.0' => '2023-12-01',
312 ][$matches[1]] ?? (new \DateTime('+2 year'))->format('Y-m-d');
313 }
249} 314}
diff --git a/application/helper/DailyPageHelper.php b/application/helper/DailyPageHelper.php
new file mode 100644
index 00000000..5fabc907
--- /dev/null
+++ b/application/helper/DailyPageHelper.php
@@ -0,0 +1,208 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Helper;
6
7use Shaarli\Bookmark\Bookmark;
8use Slim\Http\Request;
9
10class DailyPageHelper
11{
12 public const MONTH = 'month';
13 public const WEEK = 'week';
14 public const DAY = 'day';
15
16 /**
17 * Extracts the type of the daily to display from the HTTP request parameters
18 *
19 * @param Request $request HTTP request
20 *
21 * @return string month/week/day
22 */
23 public static function extractRequestedType(Request $request): string
24 {
25 if ($request->getQueryParam(static::MONTH) !== null) {
26 return static::MONTH;
27 } elseif ($request->getQueryParam(static::WEEK) !== null) {
28 return static::WEEK;
29 }
30
31 return static::DAY;
32 }
33
34 /**
35 * Extracts a DateTimeImmutable from provided HTTP request.
36 * If no parameter is provided, we rely on the creation date of the latest provided created bookmark.
37 * If the datastore is empty or no bookmark is provided, we use the current date.
38 *
39 * @param string $type month/week/day
40 * @param string|null $requestedDate Input string extracted from the request
41 * @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date)
42 *
43 * @return \DateTimeImmutable from input or latest bookmark.
44 *
45 * @throws \Exception Type not supported.
46 */
47 public static function extractRequestedDateTime(
48 string $type,
49 ?string $requestedDate,
50 Bookmark $latestBookmark = null
51 ): \DateTimeImmutable {
52 $format = static::getFormatByType($type);
53 if (empty($requestedDate)) {
54 return $latestBookmark instanceof Bookmark
55 ? new \DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM))
56 : new \DateTimeImmutable()
57 ;
58 }
59
60 // W is not supported by createFromFormat...
61 if ($type === static::WEEK) {
62 return (new \DateTimeImmutable())
63 ->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2))
64 ;
65 }
66
67 return \DateTimeImmutable::createFromFormat($format, $requestedDate);
68 }
69
70 /**
71 * Get the DateTime format used by provided type
72 * Examples:
73 * - day: 20201016 (<year><month><day>)
74 * - week: 202041 (<year><week number>)
75 * - month: 202010 (<year><month>)
76 *
77 * @param string $type month/week/day
78 *
79 * @return string DateTime compatible format
80 *
81 * @see https://www.php.net/manual/en/datetime.format.php
82 *
83 * @throws \Exception Type not supported.
84 */
85 public static function getFormatByType(string $type): string
86 {
87 switch ($type) {
88 case static::MONTH:
89 return 'Ym';
90 case static::WEEK:
91 return 'YW';
92 case static::DAY:
93 return 'Ymd';
94 default:
95 throw new \Exception('Unsupported daily format type');
96 }
97 }
98
99 /**
100 * Get the first DateTime of the time period depending on given datetime and type.
101 * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax
102 * and we don't want to alter original datetime.
103 *
104 * @param string $type month/week/day
105 * @param \DateTimeImmutable $requested DateTime extracted from request input
106 * (should come from extractRequestedDateTime)
107 *
108 * @return \DateTimeInterface First DateTime of the time period
109 *
110 * @throws \Exception Type not supported.
111 */
112 public static function getStartDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface
113 {
114 switch ($type) {
115 case static::MONTH:
116 return $requested->modify('first day of this month midnight');
117 case static::WEEK:
118 return $requested->modify('Monday this week midnight');
119 case static::DAY:
120 return $requested->modify('Today midnight');
121 default:
122 throw new \Exception('Unsupported daily format type');
123 }
124 }
125
126 /**
127 * Get the last DateTime of the time period depending on given datetime and type.
128 * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax
129 * and we don't want to alter original datetime.
130 *
131 * @param string $type month/week/day
132 * @param \DateTimeImmutable $requested DateTime extracted from request input
133 * (should come from extractRequestedDateTime)
134 *
135 * @return \DateTimeInterface Last DateTime of the time period
136 *
137 * @throws \Exception Type not supported.
138 */
139 public static function getEndDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface
140 {
141 switch ($type) {
142 case static::MONTH:
143 return $requested->modify('last day of this month 23:59:59');
144 case static::WEEK:
145 return $requested->modify('Sunday this week 23:59:59');
146 case static::DAY:
147 return $requested->modify('Today 23:59:59');
148 default:
149 throw new \Exception('Unsupported daily format type');
150 }
151 }
152
153 /**
154 * Get localized description of the time period depending on given datetime and type.
155 * Example: for a month period, it returns `October, 2020`.
156 *
157 * @param string $type month/week/day
158 * @param \DateTimeImmutable $requested DateTime extracted from request input
159 * (should come from extractRequestedDateTime)
160 *
161 * @return string Localized time period description
162 *
163 * @throws \Exception Type not supported.
164 */
165 public static function getDescriptionByType(string $type, \DateTimeImmutable $requested): string
166 {
167 switch ($type) {
168 case static::MONTH:
169 return $requested->format('F') . ', ' . $requested->format('Y');
170 case static::WEEK:
171 $requested = $requested->modify('Monday this week');
172 return t('Week') . ' ' . $requested->format('W') . ' (' . format_date($requested, false) . ')';
173 case static::DAY:
174 $out = '';
175 if ($requested->format('Ymd') === date('Ymd')) {
176 $out = t('Today') . ' - ';
177 } elseif ($requested->format('Ymd') === date('Ymd', strtotime('-1 days'))) {
178 $out = t('Yesterday') . ' - ';
179 }
180 return $out . format_date($requested, false);
181 default:
182 throw new \Exception('Unsupported daily format type');
183 }
184 }
185
186 /**
187 * Get the number of items to display in the RSS feed depending on the given type.
188 *
189 * @param string $type month/week/day
190 *
191 * @return int number of elements
192 *
193 * @throws \Exception Type not supported.
194 */
195 public static function getRssLengthByType(string $type): int
196 {
197 switch ($type) {
198 case static::MONTH:
199 return 12; // 1 year
200 case static::WEEK:
201 return 26; // ~6 months
202 case static::DAY:
203 return 30; // ~1 month
204 default:
205 throw new \Exception('Unsupported daily format type');
206 }
207 }
208}
diff --git a/application/FileUtils.php b/application/helper/FileUtils.php
index 30560bfc..2eac0793 100644
--- a/application/FileUtils.php
+++ b/application/helper/FileUtils.php
@@ -1,6 +1,6 @@
1<?php 1<?php
2 2
3namespace Shaarli; 3namespace Shaarli\Helper;
4 4
5use Shaarli\Exceptions\IOException; 5use Shaarli\Exceptions\IOException;
6 6
@@ -81,4 +81,60 @@ class FileUtils
81 ) 81 )
82 ); 82 );
83 } 83 }
84
85 /**
86 * Recursively deletes a folder content, and deletes itself optionally.
87 * If an excluded file is found, folders won't be deleted.
88 *
89 * Additional security: raise an exception if it tries to delete a folder outside of Shaarli directory.
90 *
91 * @param string $path
92 * @param bool $selfDelete Delete the provided folder if true, only its content if false.
93 * @param array $exclude
94 */
95 public static function clearFolder(string $path, bool $selfDelete, array $exclude = []): bool
96 {
97 $skipped = false;
98
99 if (!is_dir($path)) {
100 throw new IOException(t('Provided path is not a directory.'));
101 }
102
103 if (!static::isPathInShaarliFolder($path)) {
104 throw new IOException(t('Trying to delete a folder outside of Shaarli path.'));
105 }
106
107 foreach (new \DirectoryIterator($path) as $file) {
108 if($file->isDot()) {
109 continue;
110 }
111
112 if (in_array($file->getBasename(), $exclude, true)) {
113 $skipped = true;
114 continue;
115 }
116
117 if ($file->isFile()) {
118 unlink($file->getPathname());
119 } elseif($file->isDir()) {
120 $skipped = static::clearFolder($file->getRealPath(), true, $exclude) || $skipped;
121 }
122 }
123
124 if ($selfDelete && !$skipped) {
125 rmdir($path);
126 }
127
128 return $skipped;
129 }
130
131 /**
132 * Checks that the given path is inside Shaarli directory.
133 */
134 public static function isPathInShaarliFolder(string $path): bool
135 {
136 $rootDirectory = dirname(dirname(dirname(__FILE__)));
137
138 return strpos(realpath($path), $rootDirectory) !== false;
139 }
84} 140}
diff --git a/application/legacy/LegacyLinkDB.php b/application/legacy/LegacyLinkDB.php
index 7bf76fd4..5c02a21b 100644
--- a/application/legacy/LegacyLinkDB.php
+++ b/application/legacy/LegacyLinkDB.php
@@ -8,7 +8,7 @@ use DateTime;
8use Iterator; 8use Iterator;
9use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 9use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
10use Shaarli\Exceptions\IOException; 10use Shaarli\Exceptions\IOException;
11use Shaarli\FileUtils; 11use Shaarli\Helper\FileUtils;
12use Shaarli\Render\PageCacheManager; 12use Shaarli\Render\PageCacheManager;
13 13
14/** 14/**
diff --git a/application/legacy/LegacyUpdater.php b/application/legacy/LegacyUpdater.php
index 0ab3a55b..fe1a286f 100644
--- a/application/legacy/LegacyUpdater.php
+++ b/application/legacy/LegacyUpdater.php
@@ -7,7 +7,6 @@ use RainTPL;
7use ReflectionClass; 7use ReflectionClass;
8use ReflectionException; 8use ReflectionException;
9use ReflectionMethod; 9use ReflectionMethod;
10use Shaarli\ApplicationUtils;
11use Shaarli\Bookmark\Bookmark; 10use Shaarli\Bookmark\Bookmark;
12use Shaarli\Bookmark\BookmarkArray; 11use Shaarli\Bookmark\BookmarkArray;
13use Shaarli\Bookmark\BookmarkFilter; 12use Shaarli\Bookmark\BookmarkFilter;
@@ -17,6 +16,7 @@ use Shaarli\Config\ConfigJson;
17use Shaarli\Config\ConfigManager; 16use Shaarli\Config\ConfigManager;
18use Shaarli\Config\ConfigPhp; 17use Shaarli\Config\ConfigPhp;
19use Shaarli\Exceptions\IOException; 18use Shaarli\Exceptions\IOException;
19use Shaarli\Helper\ApplicationUtils;
20use Shaarli\Thumbnailer; 20use Shaarli\Thumbnailer;
21use Shaarli\Updater\Exception\UpdaterException; 21use Shaarli\Updater\Exception\UpdaterException;
22 22
diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php
index 512bb79e..c2fae705 100644
--- a/application/render/PageBuilder.php
+++ b/application/render/PageBuilder.php
@@ -5,9 +5,9 @@ namespace Shaarli\Render;
5use Exception; 5use Exception;
6use Psr\Log\LoggerInterface; 6use Psr\Log\LoggerInterface;
7use RainTPL; 7use RainTPL;
8use Shaarli\ApplicationUtils;
9use Shaarli\Bookmark\BookmarkServiceInterface; 8use Shaarli\Bookmark\BookmarkServiceInterface;
10use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
10use Shaarli\Helper\ApplicationUtils;
11use Shaarli\Security\SessionManager; 11use Shaarli\Security\SessionManager;
12use Shaarli\Thumbnailer; 12use Shaarli\Thumbnailer;
13 13
@@ -160,7 +160,7 @@ class PageBuilder
160 160
161 $this->tpl->assign('formatter', $this->conf->get('formatter', 'default')); 161 $this->tpl->assign('formatter', $this->conf->get('formatter', 'default'));
162 162
163 $this->tpl->assign('links_per_page', $this->session['LINKS_PER_PAGE']); 163 $this->tpl->assign('links_per_page', $this->session['LINKS_PER_PAGE'] ?? 20);
164 164
165 // To be removed with a proper theme configuration. 165 // To be removed with a proper theme configuration.
166 $this->tpl->assign('conf', $this->conf); 166 $this->tpl->assign('conf', $this->conf);
diff --git a/application/render/TemplatePage.php b/application/render/TemplatePage.php
index 8af8228a..03b424f3 100644
--- a/application/render/TemplatePage.php
+++ b/application/render/TemplatePage.php
@@ -14,6 +14,7 @@ interface TemplatePage
14 public const DAILY = 'daily'; 14 public const DAILY = 'daily';
15 public const DAILY_RSS = 'dailyrss'; 15 public const DAILY_RSS = 'dailyrss';
16 public const EDIT_LINK = 'editlink'; 16 public const EDIT_LINK = 'editlink';
17 public const EDIT_LINK_BATCH = 'editlink.batch';
17 public const ERROR = 'error'; 18 public const ERROR = 'error';
18 public const EXPORT = 'export'; 19 public const EXPORT = 'export';
19 public const NETSCAPE_EXPORT_BOOKMARKS = 'export.bookmarks'; 20 public const NETSCAPE_EXPORT_BOOKMARKS = 'export.bookmarks';
diff --git a/application/security/BanManager.php b/application/security/BanManager.php
index f72c8b7b..288cbde0 100644
--- a/application/security/BanManager.php
+++ b/application/security/BanManager.php
@@ -4,7 +4,7 @@
4namespace Shaarli\Security; 4namespace Shaarli\Security;
5 5
6use Psr\Log\LoggerInterface; 6use Psr\Log\LoggerInterface;
7use Shaarli\FileUtils; 7use Shaarli\Helper\FileUtils;
8 8
9/** 9/**
10 * Class BanManager 10 * Class BanManager
diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php
index 36df8c1c..96bf193c 100644
--- a/application/security/SessionManager.php
+++ b/application/security/SessionManager.php
@@ -293,9 +293,12 @@ class SessionManager
293 return session_start(); 293 return session_start();
294 } 294 }
295 295
296 public function cookieParameters(int $lifeTime, string $path, string $domain): bool 296 /**
297 * Be careful, return type of session_set_cookie_params() changed between PHP 7.1 and 7.2.
298 */
299 public function cookieParameters(int $lifeTime, string $path, string $domain): void
297 { 300 {
298 return session_set_cookie_params($lifeTime, $path, $domain); 301 session_set_cookie_params($lifeTime, $path, $domain);
299 } 302 }
300 303
301 public function regenerateId(bool $deleteOldSession = false): bool 304 public function regenerateId(bool $deleteOldSession = false): bool
diff --git a/assets/common/js/metadata.js b/assets/common/js/metadata.js
index 2b013364..d5a28a35 100644
--- a/assets/common/js/metadata.js
+++ b/assets/common/js/metadata.js
@@ -56,37 +56,41 @@ function updateThumb(basePath, divElement, id) {
56 56
57(() => { 57(() => {
58 const basePath = document.querySelector('input[name="js_base_path"]').value; 58 const basePath = document.querySelector('input[name="js_base_path"]').value;
59 const loaders = document.querySelectorAll('.loading-input');
60 59
61 /* 60 /*
62 * METADATA FOR EDIT BOOKMARK PAGE 61 * METADATA FOR EDIT BOOKMARK PAGE
63 */ 62 */
64 const inputTitle = document.querySelector('input[name="lf_title"]'); 63 const inputTitles = document.querySelectorAll('input[name="lf_title"]');
65 if (inputTitle != null) { 64 if (inputTitles != null) {
66 if (inputTitle.value.length > 0) { 65 [...inputTitles].forEach((inputTitle) => {
67 clearLoaders(loaders); 66 const form = inputTitle.closest('form[name="linkform"]');
68 return; 67 const loaders = form.querySelectorAll('.loading-input');
69 } 68
69 if (inputTitle.value.length > 0) {
70 clearLoaders(loaders);
71 return;
72 }
70 73
71 const url = document.querySelector('input[name="lf_url"]').value; 74 const url = form.querySelector('input[name="lf_url"]').value;
72 75
73 const xhr = new XMLHttpRequest(); 76 const xhr = new XMLHttpRequest();
74 xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true); 77 xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true);
75 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 78 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
76 xhr.onload = () => { 79 xhr.onload = () => {
77 const result = JSON.parse(xhr.response); 80 const result = JSON.parse(xhr.response);
78 Object.keys(result).forEach((key) => { 81 Object.keys(result).forEach((key) => {
79 if (result[key] !== null && result[key].length) { 82 if (result[key] !== null && result[key].length) {
80 const element = document.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`); 83 const element = form.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`);
81 if (element != null && element.value.length === 0) { 84 if (element != null && element.value.length === 0) {
82 element.value = he.decode(result[key]); 85 element.value = he.decode(result[key]);
86 }
83 } 87 }
84 } 88 });
85 }); 89 clearLoaders(loaders);
86 clearLoaders(loaders); 90 };
87 };
88 91
89 xhr.send(); 92 xhr.send();
93 });
90 } 94 }
91 95
92 /* 96 /*
diff --git a/assets/common/js/shaare-batch.js b/assets/common/js/shaare-batch.js
new file mode 100644
index 00000000..557325ee
--- /dev/null
+++ b/assets/common/js/shaare-batch.js
@@ -0,0 +1,121 @@
1const sendBookmarkForm = (basePath, formElement) => {
2 const inputs = formElement
3 .querySelectorAll('input[type="text"], textarea, input[type="checkbox"], input[type="hidden"]');
4
5 const formData = new FormData();
6 [...inputs].forEach((input) => {
7 formData.append(input.getAttribute('name'), input.value);
8 });
9
10 return new Promise((resolve, reject) => {
11 const xhr = new XMLHttpRequest();
12 xhr.open('POST', `${basePath}/admin/shaare`);
13 xhr.onload = () => {
14 if (xhr.status !== 200) {
15 alert(`An error occurred. Return code: ${xhr.status}`);
16 reject();
17 } else {
18 formElement.closest('.edit-link-container').remove();
19 resolve();
20 }
21 };
22 xhr.send(formData);
23 });
24};
25
26const sendBookmarkDelete = (buttonElement, formElement) => (
27 new Promise((resolve, reject) => {
28 const xhr = new XMLHttpRequest();
29 xhr.open('GET', buttonElement.href);
30 xhr.onload = () => {
31 if (xhr.status !== 200) {
32 alert(`An error occurred. Return code: ${xhr.status}`);
33 reject();
34 } else {
35 formElement.closest('.edit-link-container').remove();
36 resolve();
37 }
38 };
39 xhr.send();
40 })
41);
42
43const redirectIfEmptyBatch = (basePath, formElements, path) => {
44 if (formElements == null || formElements.length === 0) {
45 window.location.href = `${basePath}${path}`;
46 }
47};
48
49(() => {
50 const basePath = document.querySelector('input[name="js_base_path"]').value;
51 const getForms = () => document.querySelectorAll('form[name="linkform"]');
52
53 const cancelButtons = document.querySelectorAll('[name="cancel-batch-link"]');
54 if (cancelButtons != null) {
55 [...cancelButtons].forEach((cancelButton) => {
56 cancelButton.addEventListener('click', (e) => {
57 e.preventDefault();
58 e.target.closest('form[name="linkform"]').remove();
59 redirectIfEmptyBatch(basePath, getForms(), '/admin/add-shaare');
60 });
61 });
62 }
63
64 const saveButtons = document.querySelectorAll('[name="save_edit"]');
65 if (saveButtons != null) {
66 [...saveButtons].forEach((saveButton) => {
67 saveButton.addEventListener('click', (e) => {
68 e.preventDefault();
69
70 const formElement = e.target.closest('form[name="linkform"]');
71 sendBookmarkForm(basePath, formElement)
72 .then(() => redirectIfEmptyBatch(basePath, getForms(), '/'));
73 });
74 });
75 }
76
77 const saveAllButtons = document.querySelectorAll('[name="save_edit_batch"]');
78 if (saveAllButtons != null) {
79 [...saveAllButtons].forEach((saveAllButton) => {
80 saveAllButton.addEventListener('click', (e) => {
81 e.preventDefault();
82
83 const forms = [...getForms()];
84 const nbForm = forms.length;
85 let current = 0;
86 const progressBar = document.querySelector('.progressbar > div');
87 const progressBarCurrent = document.querySelector('.progressbar-current');
88
89 document.querySelector('.dark-layer').style.display = 'block';
90 document.querySelector('.progressbar-max').innerHTML = nbForm;
91 progressBarCurrent.innerHTML = current;
92
93 const promises = [];
94 forms.forEach((formElement) => {
95 promises.push(sendBookmarkForm(basePath, formElement).then(() => {
96 current += 1;
97 progressBar.style.width = `${(current * 100) / nbForm}%`;
98 progressBarCurrent.innerHTML = current;
99 }));
100 });
101
102 Promise.all(promises).then(() => {
103 window.location.href = basePath || '/';
104 });
105 });
106 });
107 }
108
109 const deleteButtons = document.querySelectorAll('[name="delete_link"]');
110 if (deleteButtons != null) {
111 [...deleteButtons].forEach((deleteButton) => {
112 deleteButton.addEventListener('click', (e) => {
113 e.preventDefault();
114
115 const formElement = e.target.closest('form[name="linkform"]');
116 sendBookmarkDelete(e.target, formElement)
117 .then(() => redirectIfEmptyBatch(basePath, getForms(), '/'));
118 });
119 });
120 }
121})();
diff --git a/assets/default/js/base.js b/assets/default/js/base.js
index 7f6b9637..4163577d 100644
--- a/assets/default/js/base.js
+++ b/assets/default/js/base.js
@@ -634,4 +634,33 @@ function init(description) {
634 }); 634 });
635 }); 635 });
636 } 636 }
637
638 const bulkCreationButton = document.querySelector('.addlink-batch-show-more-block');
639 if (bulkCreationButton != null) {
640 const toggleBulkCreationVisibility = (showMoreBlockElement, formElement) => {
641 if (bulkCreationButton.classList.contains('pure-u-0')) {
642 showMoreBlockElement.classList.remove('pure-u-0');
643 formElement.classList.add('pure-u-0');
644 } else {
645 showMoreBlockElement.classList.add('pure-u-0');
646 formElement.classList.remove('pure-u-0');
647 }
648 };
649
650 const bulkCreationForm = document.querySelector('.addlink-batch-form-block');
651
652 toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm);
653 bulkCreationButton.querySelector('a').addEventListener('click', (e) => {
654 e.preventDefault();
655 toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm);
656 });
657
658 // Force to send falsy value if the checkbox is not checked.
659 const privateButton = bulkCreationForm.querySelector('input[type="checkbox"][name="private"]');
660 const privateHiddenButton = bulkCreationForm.querySelector('input[type="hidden"][name="private"]');
661 privateButton.addEventListener('click', () => {
662 privateHiddenButton.disabled = !privateHiddenButton.disabled;
663 });
664 privateHiddenButton.disabled = privateButton.checked;
665 }
637})(); 666})();
diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss
index 286ac83b..a7f091e9 100644
--- a/assets/default/scss/shaarli.scss
+++ b/assets/default/scss/shaarli.scss
@@ -1023,6 +1023,10 @@ body,
1023 &.button-red { 1023 &.button-red {
1024 background: $red; 1024 background: $red;
1025 } 1025 }
1026
1027 &.button-grey {
1028 background: $light-grey;
1029 }
1026 } 1030 }
1027 1031
1028 .submit-buttons { 1032 .submit-buttons {
@@ -1047,7 +1051,7 @@ body,
1047 } 1051 }
1048 1052
1049 table { 1053 table {
1050 margin: auto; 1054 margin: 10px auto 25px auto;
1051 width: 90%; 1055 width: 90%;
1052 1056
1053 .order { 1057 .order {
@@ -1083,6 +1087,11 @@ body,
1083 position: absolute; 1087 position: absolute;
1084 right: 5%; 1088 right: 5%;
1085 } 1089 }
1090
1091 &.button-grey {
1092 position: absolute;
1093 left: 5%;
1094 }
1086 } 1095 }
1087 } 1096 }
1088 } 1097 }
@@ -1696,6 +1705,123 @@ form {
1696 } 1705 }
1697} 1706}
1698 1707
1708// SERVER PAGE
1709
1710.server-tables-page,
1711.server-tables {
1712 .window-subtitle {
1713 &::before {
1714 display: block;
1715 margin: 8px auto;
1716 background: linear-gradient(to right, var(--background-color), $dark-grey, var(--background-color));
1717 width: 50%;
1718 height: 1px;
1719 content: '';
1720 }
1721 }
1722
1723 .server-row {
1724 p {
1725 height: 25px;
1726 padding: 0 10px;
1727 }
1728 }
1729
1730 .server-label {
1731 text-align: right;
1732 font-weight: bold;
1733 }
1734
1735 i {
1736 &.fa-color-green {
1737 color: $main-green;
1738 }
1739
1740 &.fa-color-orange {
1741 color: $orange;
1742 }
1743
1744 &.fa-color-red {
1745 color: $red;
1746 }
1747 }
1748
1749 @media screen and (max-width: 64em) {
1750 .server-label {
1751 text-align: center;
1752 }
1753
1754 .server-row {
1755 p {
1756 text-align: center;
1757 }
1758 }
1759 }
1760}
1761
1762// Batch creation
1763input[name='save_edit_batch'] {
1764 @extend %page-form-button;
1765}
1766
1767.addlink-batch-show-more {
1768 display: flex;
1769 align-items: center;
1770 margin: 20px 0 8px;
1771
1772 a {
1773 color: var(--main-color);
1774 text-decoration: none;
1775 }
1776
1777 &::before,
1778 &::after {
1779 content: "";
1780 flex-grow: 1;
1781 background: rgba(0, 0, 0, 0.35);
1782 height: 1px;
1783 font-size: 0;
1784 line-height: 0;
1785 }
1786
1787 &::before {
1788 margin: 0 16px 0 0;
1789 }
1790
1791 &::after {
1792 margin: 0 0 0 16px;
1793 }
1794}
1795
1796.dark-layer {
1797 display: none;
1798 position: fixed;
1799 height: 100%;
1800 width: 100%;
1801 z-index: 998;
1802 background-color: rgba(0, 0, 0, .75);
1803 color: #fff;
1804
1805 .screen-center {
1806 display: flex;
1807 flex-direction: column;
1808 justify-content: center;
1809 align-items: center;
1810 text-align: center;
1811 min-height: 100vh;
1812 }
1813
1814 .progressbar {
1815 width: 33%;
1816 }
1817}
1818
1819.addlink-batch-form-block {
1820 .pure-alert {
1821 margin: 25px 0 0 0;
1822 }
1823}
1824
1699// Print rules 1825// Print rules
1700@media print { 1826@media print {
1701 .shaarli-menu { 1827 .shaarli-menu {
diff --git a/composer.json b/composer.json
index 64f0025e..94492586 100644
--- a/composer.json
+++ b/composer.json
@@ -59,6 +59,7 @@
59 "Shaarli\\Front\\Controller\\Admin\\": "application/front/controller/admin", 59 "Shaarli\\Front\\Controller\\Admin\\": "application/front/controller/admin",
60 "Shaarli\\Front\\Controller\\Visitor\\": "application/front/controller/visitor", 60 "Shaarli\\Front\\Controller\\Visitor\\": "application/front/controller/visitor",
61 "Shaarli\\Front\\Exception\\": "application/front/exceptions", 61 "Shaarli\\Front\\Exception\\": "application/front/exceptions",
62 "Shaarli\\Helper\\": "application/helper",
62 "Shaarli\\Http\\": "application/http", 63 "Shaarli\\Http\\": "application/http",
63 "Shaarli\\Legacy\\": "application/legacy", 64 "Shaarli\\Legacy\\": "application/legacy",
64 "Shaarli\\Netscape\\": "application/netscape", 65 "Shaarli\\Netscape\\": "application/netscape",
diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po
index f7baedfb..60ea7a97 100644
--- a/inc/languages/fr/LC_MESSAGES/shaarli.po
+++ b/inc/languages/fr/LC_MESSAGES/shaarli.po
@@ -1,8 +1,8 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Project-Id-Version: Shaarli\n" 3"Project-Id-Version: Shaarli\n"
4"POT-Creation-Date: 2020-10-16 20:01+0200\n" 4"POT-Creation-Date: 2020-10-27 19:44+0100\n"
5"PO-Revision-Date: 2020-10-16 20:02+0200\n" 5"PO-Revision-Date: 2020-10-27 19:44+0100\n"
6"Last-Translator: \n" 6"Last-Translator: \n"
7"Language-Team: Shaarli\n" 7"Language-Team: Shaarli\n"
8"Language: fr_FR\n" 8"Language: fr_FR\n"
@@ -20,38 +20,11 @@ msgstr ""
20"X-Poedit-SearchPath-3: init.php\n" 20"X-Poedit-SearchPath-3: init.php\n"
21"X-Poedit-SearchPath-4: plugins\n" 21"X-Poedit-SearchPath-4: plugins\n"
22 22
23#: application/ApplicationUtils.php:161 23#: application/History.php:180
24#, php-format
25msgid ""
26"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
27"cannot run. Your PHP version has known security vulnerabilities and should "
28"be updated as soon as possible."
29msgstr ""
30"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne "
31"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
32"connues et devrait être mise à jour au plus tôt."
33
34#: application/ApplicationUtils.php:192 application/ApplicationUtils.php:204
35msgid "directory is not readable"
36msgstr "le répertoire n'est pas accessible en lecture"
37
38#: application/ApplicationUtils.php:207
39msgid "directory is not writable"
40msgstr "le répertoire n'est pas accessible en écriture"
41
42#: application/ApplicationUtils.php:225
43msgid "file is not readable"
44msgstr "le fichier n'est pas accessible en lecture"
45
46#: application/ApplicationUtils.php:228
47msgid "file is not writable"
48msgstr "le fichier n'est pas accessible en écriture"
49
50#: application/History.php:179
51msgid "History file isn't readable or writable" 24msgid "History file isn't readable or writable"
52msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture" 25msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture"
53 26
54#: application/History.php:190 27#: application/History.php:191
55msgid "Could not parse history file" 28msgid "Could not parse history file"
56msgstr "Format incorrect pour le fichier d'historique" 29msgstr "Format incorrect pour le fichier d'historique"
57 30
@@ -83,40 +56,40 @@ msgstr ""
83"l'extension php-gd doit être chargée pour utiliser les miniatures. Les " 56"l'extension php-gd doit être chargée pour utiliser les miniatures. Les "
84"miniatures sont désormais désactivées. Rechargez la page." 57"miniatures sont désormais désactivées. Rechargez la page."
85 58
86#: application/Utils.php:383 59#: application/Utils.php:402
87msgid "Setting not set" 60msgid "Setting not set"
88msgstr "Paramètre non défini" 61msgstr "Paramètre non défini"
89 62
90#: application/Utils.php:390 63#: application/Utils.php:409
91msgid "Unlimited" 64msgid "Unlimited"
92msgstr "Illimité" 65msgstr "Illimité"
93 66
94#: application/Utils.php:393 67#: application/Utils.php:412
95msgid "B" 68msgid "B"
96msgstr "o" 69msgstr "o"
97 70
98#: application/Utils.php:393 71#: application/Utils.php:412
99msgid "kiB" 72msgid "kiB"
100msgstr "ko" 73msgstr "ko"
101 74
102#: application/Utils.php:393 75#: application/Utils.php:412
103msgid "MiB" 76msgid "MiB"
104msgstr "Mo" 77msgstr "Mo"
105 78
106#: application/Utils.php:393 79#: application/Utils.php:412
107msgid "GiB" 80msgid "GiB"
108msgstr "Go" 81msgstr "Go"
109 82
110#: application/bookmark/BookmarkFileService.php:180 83#: application/bookmark/BookmarkFileService.php:183
111#: application/bookmark/BookmarkFileService.php:202 84#: application/bookmark/BookmarkFileService.php:205
112#: application/bookmark/BookmarkFileService.php:224 85#: application/bookmark/BookmarkFileService.php:227
113#: application/bookmark/BookmarkFileService.php:238 86#: application/bookmark/BookmarkFileService.php:241
114msgid "You're not authorized to alter the datastore" 87msgid "You're not authorized to alter the datastore"
115msgstr "Vous n'êtes pas autorisé à modifier les données" 88msgstr "Vous n'êtes pas autorisé à modifier les données"
116 89
117#: application/bookmark/BookmarkFileService.php:205 90#: application/bookmark/BookmarkFileService.php:208
118msgid "This bookmarks already exists" 91msgid "This bookmarks already exists"
119msgstr "Ce marque-page existe déjà." 92msgstr "Ce marque-page existe déjà"
120 93
121#: application/bookmark/BookmarkInitializer.php:39 94#: application/bookmark/BookmarkInitializer.php:39
122msgid "(private bookmark with thumbnail demo)" 95msgid "(private bookmark with thumbnail demo)"
@@ -314,7 +287,8 @@ msgid "Direct link"
314msgstr "Liens directs" 287msgstr "Liens directs"
315 288
316#: application/feed/FeedBuilder.php:181 289#: application/feed/FeedBuilder.php:181
317#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 290#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
291#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
318#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179 292#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
319msgid "Permalink" 293msgid "Permalink"
320msgstr "Permalien" 294msgstr "Permalien"
@@ -330,12 +304,13 @@ msgid "You have enabled or changed thumbnails mode."
330msgstr "Vous avez activé ou changé le mode de miniatures." 304msgstr "Vous avez activé ou changé le mode de miniatures."
331 305
332#: application/front/controller/admin/ConfigureController.php:103 306#: application/front/controller/admin/ConfigureController.php:103
307#: application/front/controller/admin/ServerController.php:68
333#: application/legacy/LegacyUpdater.php:538 308#: application/legacy/LegacyUpdater.php:538
334msgid "Please synchronize them." 309msgid "Please synchronize them."
335msgstr "Merci de les synchroniser." 310msgstr "Merci de les synchroniser."
336 311
337#: application/front/controller/admin/ConfigureController.php:113 312#: application/front/controller/admin/ConfigureController.php:113
338#: application/front/controller/visitor/InstallController.php:136 313#: application/front/controller/visitor/InstallController.php:146
339msgid "Error while writing config file after configuration update." 314msgid "Error while writing config file after configuration update."
340msgstr "" 315msgstr ""
341"Une erreur s'est produite lors de la sauvegarde du fichier de configuration." 316"Une erreur s'est produite lors de la sauvegarde du fichier de configuration."
@@ -372,46 +347,19 @@ msgstr ""
372"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus " 347"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus "
373"légères." 348"légères."
374 349
375#: application/front/controller/admin/ManageShaareController.php:29 350#: application/front/controller/admin/ManageShaareController.php:64
376#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 351#: application/front/controller/admin/ManageShaareController.php:95
377msgid "Shaare a new link" 352#: application/front/controller/admin/ManageShaareController.php:193
378msgstr "Partager un nouveau lien" 353#: application/front/controller/admin/ManageShaareController.php:262
379 354#: application/front/controller/admin/ManageShaareController.php:302
380#: application/front/controller/admin/ManageShaareController.php:78 355#: application/front/controller/admin/ManageShaareController.php:181
381msgid "Note: " 356#: application/front/controller/admin/ManageShaareController.php:239
382msgstr "Note : " 357#: application/front/controller/admin/ManageShaareController.php:247
383 358#: application/front/controller/admin/ManageShaareController.php:378
384#: application/front/controller/admin/ManageShaareController.php:109 359#: application/front/controller/admin/ManageShaareController.php:381
385#: application/front/controller/admin/ManageShaareController.php:206
386#: application/front/controller/admin/ManageShaareController.php:275
387#: application/front/controller/admin/ManageShaareController.php:315
388#, php-format
389msgid "Bookmark with identifier %s could not be found."
390msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé."
391
392#: application/front/controller/admin/ManageShaareController.php:194
393#: application/front/controller/admin/ManageShaareController.php:252
394msgid "Invalid bookmark ID provided."
395msgstr "ID du lien non valide."
396
397#: application/front/controller/admin/ManageShaareController.php:260
398msgid "Invalid visibility provided."
399msgstr "Visibilité du lien non valide."
400
401#: application/front/controller/admin/ManageShaareController.php:363
402#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
403msgid "Edit"
404msgstr "Modifier"
405
406#: application/front/controller/admin/ManageShaareController.php:366
407#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
408#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28
409msgid "Shaare"
410msgstr "Shaare"
411
412#: application/front/controller/admin/ManageTagController.php:29 360#: application/front/controller/admin/ManageTagController.php:29
413#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 361#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
414#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 362#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
415msgid "Manage tags" 363msgid "Manage tags"
416msgstr "Gérer les tags" 364msgstr "Gérer les tags"
417 365
@@ -435,7 +383,7 @@ msgstr[1] "Le tag a été renommé dans %d liens."
435 383
436#: application/front/controller/admin/PasswordController.php:28 384#: application/front/controller/admin/PasswordController.php:28
437#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 385#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
438#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 386#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
439msgid "Change password" 387msgid "Change password"
440msgstr "Modifier le mot de passe" 388msgstr "Modifier le mot de passe"
441 389
@@ -467,6 +415,43 @@ msgstr ""
467"Une erreur s'est produite lors de la sauvegarde de la configuration des " 415"Une erreur s'est produite lors de la sauvegarde de la configuration des "
468"plugins : " 416"plugins : "
469 417
418#: application/front/controller/admin/ServerController.php:50
419#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
420#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
421msgid "Server administration"
422msgstr "Administration serveur"
423
424#: application/front/controller/admin/ServerController.php:67
425msgid "Thumbnails cache has been cleared."
426msgstr "Le cache des miniatures a été vidé."
427
428#: application/front/controller/admin/ServerController.php:76
429msgid "Shaarli's cache folder has been cleared!"
430msgstr "Le dossier de cache de Shaarli a été vidé !"
431
432#, php-format
433msgid "Bookmark with identifier %s could not be found."
434msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé."
435
436#: application/front/controller/admin/ShaareManageController.php:101
437msgid "Invalid visibility provided."
438msgstr "Visibilité du lien non valide."
439
440#: application/front/controller/admin/ShaarePublishController.php:154
441#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
442msgid "Edit"
443msgstr "Modifier"
444
445#: application/front/controller/admin/ShaarePublishController.php:157
446#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
447#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28
448msgid "Shaare"
449msgstr "Shaare"
450
451#: application/front/controller/admin/ShaarePublishController.php:184
452msgid "Note: "
453msgstr "Note : "
454
470#: application/front/controller/admin/ThumbnailsController.php:37 455#: application/front/controller/admin/ThumbnailsController.php:37
471#: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 456#: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
472msgid "Thumbnails update" 457msgid "Thumbnails update"
@@ -482,29 +467,50 @@ msgstr "Outils"
482msgid "Search: " 467msgid "Search: "
483msgstr "Recherche : " 468msgstr "Recherche : "
484 469
485#: application/front/controller/visitor/DailyController.php:45 470#: application/front/controller/visitor/DailyController.php:200
486msgid "Today" 471msgid "day"
487msgstr "Aujourd'hui" 472msgstr "jour"
488
489#: application/front/controller/visitor/DailyController.php:47
490msgid "Yesterday"
491msgstr "Hier"
492 473
493#: application/front/controller/visitor/DailyController.php:85 474#: application/front/controller/visitor/DailyController.php:200
475#: application/front/controller/visitor/DailyController.php:203
476#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
494#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 477#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
495#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48 478#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48
496msgid "Daily" 479msgid "Daily"
497msgstr "Quotidien" 480msgstr "Quotidien"
498 481
499#: application/front/controller/visitor/ErrorController.php:36 482#: application/front/controller/visitor/DailyController.php:201
483msgid "week"
484msgstr "semaine"
485
486#: application/front/controller/visitor/DailyController.php:201
487#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
488msgid "Weekly"
489msgstr "Hebdomadaire"
490
491#: application/front/controller/visitor/DailyController.php:202
492msgid "month"
493msgstr "mois"
494
495#: application/front/controller/visitor/DailyController.php:202
496#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
497msgid "Monthly"
498msgstr "Mensuel"
499
500#: application/front/controller/visitor/ErrorController.php:33
500msgid "An unexpected error occurred." 501msgid "An unexpected error occurred."
501msgstr "Une erreur inattendue s'est produite." 502msgstr "Une erreur inattendue s'est produite."
502 503
503#: application/front/controller/visitor/ErrorNotFoundController.php:25 504#: application/front/controller/visitor/ErrorNotFoundController.php:25
504msgid "Requested page could not be found." 505msgid "Requested page could not be found."
505msgstr "" 506msgstr "La page demandée n'a pas pu être trouvée."
507
508#: application/front/controller/visitor/InstallController.php:64
509#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
510msgid "Install Shaarli"
511msgstr "Installation de Shaarli"
506 512
507#: application/front/controller/visitor/InstallController.php:73 513#: application/front/controller/visitor/InstallController.php:83
508#, php-format 514#, php-format
509msgid "" 515msgid ""
510"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the " 516"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
@@ -523,14 +529,14 @@ msgstr ""
523"des cookies. Nous vous recommandons d'accéder à votre serveur depuis son " 529"des cookies. Nous vous recommandons d'accéder à votre serveur depuis son "
524"adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>" 530"adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>"
525 531
526#: application/front/controller/visitor/InstallController.php:144 532#: application/front/controller/visitor/InstallController.php:154
527msgid "" 533msgid ""
528"Shaarli is now configured. Please login and start shaaring your bookmarks!" 534"Shaarli is now configured. Please login and start shaaring your bookmarks!"
529msgstr "" 535msgstr ""
530"Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à " 536"Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à "
531"shaare vos liens !" 537"shaare vos liens !"
532 538
533#: application/front/controller/visitor/InstallController.php:158 539#: application/front/controller/visitor/InstallController.php:168
534msgid "Insufficient permissions:" 540msgid "Insufficient permissions:"
535msgstr "Permissions insuffisantes :" 541msgstr "Permissions insuffisantes :"
536 542
@@ -544,7 +550,7 @@ msgstr "Permissions insuffisantes :"
544msgid "Login" 550msgid "Login"
545msgstr "Connexion" 551msgstr "Connexion"
546 552
547#: application/front/controller/visitor/LoginController.php:78 553#: application/front/controller/visitor/LoginController.php:77
548msgid "Wrong login/password." 554msgid "Wrong login/password."
549msgstr "Nom d'utilisateur ou mot de passe incorrect(s)." 555msgstr "Nom d'utilisateur ou mot de passe incorrect(s)."
550 556
@@ -556,7 +562,7 @@ msgstr "Mur d'images"
556 562
557#: application/front/controller/visitor/TagCloudController.php:88 563#: application/front/controller/visitor/TagCloudController.php:88
558msgid "Tag " 564msgid "Tag "
559msgstr "Tag" 565msgstr "Tag "
560 566
561#: application/front/exceptions/AlreadyInstalledException.php:11 567#: application/front/exceptions/AlreadyInstalledException.php:11
562msgid "Shaarli has already been installed. Login to edit the configuration." 568msgid "Shaarli has already been installed. Login to edit the configuration."
@@ -584,6 +590,86 @@ msgstr ""
584msgid "Wrong token." 590msgid "Wrong token."
585msgstr "Jeton invalide." 591msgstr "Jeton invalide."
586 592
593#: application/helper/ApplicationUtils.php:162
594#, php-format
595msgid ""
596"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
597"cannot run. Your PHP version has known security vulnerabilities and should "
598"be updated as soon as possible."
599msgstr ""
600"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne "
601"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
602"connues et devrait être mise à jour au plus tôt."
603
604#: application/helper/ApplicationUtils.php:195
605#: application/helper/ApplicationUtils.php:215
606msgid "directory is not readable"
607msgstr "le répertoire n'est pas accessible en lecture"
608
609#: application/helper/ApplicationUtils.php:218
610msgid "directory is not writable"
611msgstr "le répertoire n'est pas accessible en écriture"
612
613#: application/helper/ApplicationUtils.php:240
614msgid "file is not readable"
615msgstr "le fichier n'est pas accessible en lecture"
616
617#: application/helper/ApplicationUtils.php:243
618msgid "file is not writable"
619msgstr "le fichier n'est pas accessible en écriture"
620
621#: application/helper/ApplicationUtils.php:277
622msgid "Configuration parsing"
623msgstr "Chargement de la configuration"
624
625#: application/helper/ApplicationUtils.php:278
626msgid "Slim Framework (routing, etc.)"
627msgstr "Slim Framwork (routage, etc.)"
628
629#: application/helper/ApplicationUtils.php:279
630msgid "Multibyte (Unicode) string support"
631msgstr "Support des chaînes de caractère multibytes (Unicode)"
632
633#: application/helper/ApplicationUtils.php:280
634msgid "Required to use thumbnails"
635msgstr "Obligatoire pour utiliser les miniatures"
636
637#: application/helper/ApplicationUtils.php:281
638msgid "Localized text sorting (e.g. e->è->f)"
639msgstr "Tri des textes traduits (ex : e->è->f)"
640
641#: application/helper/ApplicationUtils.php:282
642msgid "Better retrieval of bookmark metadata and thumbnail"
643msgstr "Meilleure récupération des meta-données des marque-pages et minatures"
644
645#: application/helper/ApplicationUtils.php:283
646msgid "Use the translation system in gettext mode"
647msgstr "Utiliser le système de traduction en mode gettext"
648
649#: application/helper/ApplicationUtils.php:284
650msgid "Login using LDAP server"
651msgstr "Authentification via un serveur LDAP"
652
653#: application/helper/DailyPageHelper.php:172
654msgid "Week"
655msgstr "Semaine"
656
657#: application/helper/DailyPageHelper.php:176
658msgid "Today"
659msgstr "Aujourd'hui"
660
661#: application/helper/DailyPageHelper.php:178
662msgid "Yesterday"
663msgstr "Hier"
664
665#: application/helper/FileUtils.php:100
666msgid "Provided path is not a directory."
667msgstr "Le chemin fourni n'est pas un dossier."
668
669#: application/helper/FileUtils.php:104
670msgid "Trying to delete a folder outside of Shaarli path."
671msgstr "Tentative de supprimer un dossier en dehors du chemin de Shaarli."
672
587#: application/legacy/LegacyLinkDB.php:131 673#: application/legacy/LegacyLinkDB.php:131
588msgid "You are not authorized to add a link." 674msgid "You are not authorized to add a link."
589msgstr "Vous n'êtes pas autorisé à ajouter un lien." 675msgstr "Vous n'êtes pas autorisé à ajouter un lien."
@@ -678,7 +764,7 @@ msgstr "Impossible de purger %s : le répertoire n'existe pas"
678msgid "An error occurred while running the update " 764msgid "An error occurred while running the update "
679msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour " 765msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour "
680 766
681#: index.php:65 767#: index.php:80
682msgid "Shared bookmarks on " 768msgid "Shared bookmarks on "
683msgstr "Liens partagés sur " 769msgstr "Liens partagés sur "
684 770
@@ -851,6 +937,48 @@ msgstr "Désolé, il y a rien à voir ici."
851msgid "URL or leave empty to post a note" 937msgid "URL or leave empty to post a note"
852msgstr "URL ou laisser vide pour créer une note" 938msgstr "URL ou laisser vide pour créer une note"
853 939
940#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
941msgid "BULK CREATION"
942msgstr "CRÉATION DE MASSE"
943
944#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
945msgid "Metadata asynchronous retrieval is disabled."
946msgstr "La récupération asynchrone des meta-données est désactivée."
947
948#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
949msgid ""
950"We recommend that you enable the setting <em>general > "
951"enable_async_metadata</em> in your configuration file to use bulk link "
952"creation."
953msgstr ""
954"Nous recommandons d'activer le paramètre <em>general > "
955"enable_async_metadata</em> dans votre fichier de configuration pour utiliser "
956"la création de masse."
957
958#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
959msgid "Shaare multiple new links"
960msgstr "Partagez plusieurs nouveaux liens"
961
962#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59
963msgid "Add one URL per line to create multiple bookmarks."
964msgstr "Ajouter une URL par ligne pour créer plusieurs marque-pages."
965
966#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
967#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
968msgid "Tags"
969msgstr "Tags"
970
971#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73
972#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
973#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
974#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
975msgid "Private"
976msgstr "Privé"
977
978#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
979msgid "Add links"
980msgstr "Ajouter des liens"
981
854#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 982#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
855msgid "Current password" 983msgid "Current password"
856msgstr "Mot de passe actuel" 984msgstr "Mot de passe actuel"
@@ -1016,71 +1144,79 @@ msgstr ""
1016"miniatures." 1144"miniatures."
1017 1145
1018#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328 1146#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328
1019#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 1147#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
1020msgid "Synchronize thumbnails" 1148msgid "Synchronize thumbnails"
1021msgstr "Synchroniser les miniatures" 1149msgstr "Synchroniser les miniatures"
1022 1150
1023#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339 1151#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339
1024#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 1152#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
1153#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
1025msgid "All" 1154msgid "All"
1026msgstr "Tous" 1155msgstr "Tous"
1027 1156
1028#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343 1157#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343
1158#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
1029msgid "Only common media hosts" 1159msgid "Only common media hosts"
1030msgstr "Seulement les hébergeurs de média connus" 1160msgstr "Seulement les hébergeurs de média connus"
1031 1161
1032#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347 1162#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347
1163#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
1033msgid "None" 1164msgid "None"
1034msgstr "Aucune" 1165msgstr "Aucune"
1035 1166
1036#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355 1167#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355
1037#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 1168#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
1038#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 1169#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
1039#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 1170#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
1040msgid "Save" 1171msgid "Save"
1041msgstr "Enregistrer" 1172msgstr "Enregistrer"
1042 1173
1043#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 1174#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
1044msgid "The Daily Shaarli" 1175msgid "1 RSS entry per :type"
1045msgstr "Le Quotidien Shaarli" 1176msgid_plural ""
1046 1177msgstr[0] "1 entrée RSS par :type"
1047#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 1178msgstr[1] ""
1048msgid "1 RSS entry per day" 1179
1049msgstr "1 entrée RSS par jour" 1180#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
1050 1181msgid "Previous :type"
1051#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 1182msgid_plural ""
1052msgid "Previous day" 1183msgstr[0] ":type précédent"
1053msgstr "Jour précédent" 1184msgstr[1] "Jour précédent"
1054 1185
1055#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 1186#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
1056msgid "All links of one day in a single page." 1187#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
1057msgstr "Tous les liens d'un jour sur une page." 1188msgid "All links of one :type in a single page."
1058 1189msgid_plural ""
1059#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 1190msgstr[0] "Tous les liens d'un :type sur une page."
1060msgid "Next day" 1191msgstr[1] "Tous les liens d'un jour sur une page."
1061msgstr "Jour suivant" 1192
1062 1193#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
1063#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 1194msgid "Next :type"
1195msgid_plural ""
1196msgstr[0] ":type suivant"
1197msgstr[1] ""
1198
1199#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
1064msgid "Edit Shaare" 1200msgid "Edit Shaare"
1065msgstr "Modifier le Shaare" 1201msgstr "Modifier le Shaare"
1066 1202
1067#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 1203#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
1068msgid "New Shaare" 1204msgid "New Shaare"
1069msgstr "Nouveau Shaare" 1205msgstr "Nouveau Shaare"
1070 1206
1071#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 1207#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
1072msgid "Created:" 1208msgid "Created:"
1073msgstr "Création :" 1209msgstr "Création :"
1074 1210
1075#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 1211#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
1076msgid "URL" 1212msgid "URL"
1077msgstr "URL" 1213msgstr "URL"
1078 1214
1079#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 1215#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
1080msgid "Title" 1216msgid "Title"
1081msgstr "Titre" 1217msgstr "Titre"
1082 1218
1083#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 1219#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
1084#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 1220#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
1085#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 1221#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
1086#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 1222#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
@@ -1088,33 +1224,34 @@ msgstr "Titre"
1088msgid "Description" 1224msgid "Description"
1089msgstr "Description" 1225msgstr "Description"
1090 1226
1091#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 1227#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
1092msgid "Tags" 1228#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
1093msgstr "Tags" 1229#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80
1094
1095#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
1096#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
1097#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
1098msgid "Private"
1099msgstr "Privé"
1100
1101#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
1102msgid "Description will be rendered with" 1230msgid "Description will be rendered with"
1103msgstr "La description sera générée avec" 1231msgstr "La description sera générée avec"
1104 1232
1105#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68 1233#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82
1106msgid "Markdown syntax documentation" 1234msgid "Markdown syntax documentation"
1107msgstr "Documentation sur la syntaxe Markdown" 1235msgstr "Documentation sur la syntaxe Markdown"
1108 1236
1109#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69 1237#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
1110msgid "Markdown syntax" 1238msgid "Markdown syntax"
1111msgstr "la syntaxe Markdown" 1239msgstr "la syntaxe Markdown"
1112 1240
1113#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 1241#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
1242msgid "Cancel"
1243msgstr "Annuler"
1244
1245#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
1114msgid "Apply Changes" 1246msgid "Apply Changes"
1115msgstr "Appliquer les changements" 1247msgstr "Appliquer les changements"
1116 1248
1117#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:93 1249#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
1250#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
1251msgid "Save all"
1252msgstr "Tout enregistrer"
1253
1254#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107
1118#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 1255#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173
1119#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 1256#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
1120#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147 1257#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147
@@ -1179,10 +1316,6 @@ msgstr "Les doublons s'appuient sur les URL"
1179msgid "Add default tags" 1316msgid "Add default tags"
1180msgstr "Ajouter des tags par défaut" 1317msgstr "Ajouter des tags par défaut"
1181 1318
1182#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
1183msgid "Install Shaarli"
1184msgstr "Installation de Shaarli"
1185
1186#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 1319#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
1187msgid "It looks like it's the first time you run Shaarli. Please configure it." 1320msgid "It looks like it's the first time you run Shaarli. Please configure it."
1188msgstr "" 1321msgstr ""
@@ -1215,6 +1348,10 @@ msgstr "Mes liens"
1215msgid "Install" 1348msgid "Install"
1216msgstr "Installer" 1349msgstr "Installer"
1217 1350
1351#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:190
1352msgid "Server requirements"
1353msgstr "Pré-requis serveur"
1354
1218#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 1355#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
1219#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 1356#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
1220msgid "shaare" 1357msgid "shaare"
@@ -1313,6 +1450,10 @@ msgstr "Changer statut épinglé"
1313msgid "Sticky" 1450msgid "Sticky"
1314msgstr "Épinglé" 1451msgstr "Épinglé"
1315 1452
1453#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189
1454msgid "Share a private link"
1455msgstr "Partager un lien privé"
1456
1316#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5 1457#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5
1317#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5 1458#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5
1318msgid "Filters" 1459msgid "Filters"
@@ -1511,6 +1652,100 @@ msgstr "Configuration des extensions"
1511msgid "No parameter available." 1652msgid "No parameter available."
1512msgstr "Aucun paramètre disponible." 1653msgstr "Aucun paramètre disponible."
1513 1654
1655#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
1656msgid "General"
1657msgstr "Général"
1658
1659#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20
1660msgid "Index URL"
1661msgstr "URL de l'index"
1662
1663#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
1664msgid "Base path"
1665msgstr "Chemin de base"
1666
1667#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
1668msgid "Client IP"
1669msgstr "IP du client"
1670
1671#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
1672msgid "Trusted reverse proxies"
1673msgstr "Reverse proxies de confiance"
1674
1675#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
1676msgid "N/A"
1677msgstr "N/A"
1678
1679#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84
1680msgid "Visit releases page on Github"
1681msgstr "Visiter la page des releases sur Github"
1682
1683#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
1684msgid "Synchronize all link thumbnails"
1685msgstr "Synchroniser toutes les miniatures"
1686
1687#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:2
1688#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:2
1689msgid "Permissions"
1690msgstr "Permissions"
1691
1692#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:8
1693#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:8
1694msgid "There are permissions that need to be fixed."
1695msgstr "Il y a des permissions qui doivent être corrigées."
1696
1697#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
1698#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:23
1699msgid "All read/write permissions are properly set."
1700msgstr "Toutes les permissions de lecture/écriture sont définies correctement."
1701
1702#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
1703#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:32
1704msgid "Running PHP"
1705msgstr "Fonctionnant avec PHP"
1706
1707#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
1708#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:36
1709msgid "End of life: "
1710msgstr "Fin de vie : "
1711
1712#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
1713#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:48
1714msgid "Extension"
1715msgstr "Extension"
1716
1717#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
1718#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:49
1719msgid "Usage"
1720msgstr "Utilisation"
1721
1722#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50
1723#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:50
1724msgid "Status"
1725msgstr "Statut"
1726
1727#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
1728#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
1729#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:51
1730#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:66
1731msgid "Loaded"
1732msgstr "Chargé"
1733
1734#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
1735#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60
1736msgid "Required"
1737msgstr "Obligatoire"
1738
1739#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
1740#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60
1741msgid "Optional"
1742msgstr "Optionnel"
1743
1744#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70
1745#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:70
1746msgid "Not loaded"
1747msgstr "Non chargé"
1748
1514#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 1749#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1515#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 1750#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1516msgid "tags" 1751msgid "tags"
@@ -1561,15 +1796,19 @@ msgstr "Configurer Shaarli"
1561msgid "Enable, disable and configure plugins" 1796msgid "Enable, disable and configure plugins"
1562msgstr "Activer, désactiver et configurer les extensions" 1797msgstr "Activer, désactiver et configurer les extensions"
1563 1798
1564#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 1799#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27
1800msgid "Check instance's server configuration"
1801msgstr "Vérifier la configuration serveur de l'instance"
1802
1803#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
1565msgid "Change your password" 1804msgid "Change your password"
1566msgstr "Modifier le mot de passe" 1805msgstr "Modifier le mot de passe"
1567 1806
1568#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 1807#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1569msgid "Rename or delete a tag in all links" 1808msgid "Rename or delete a tag in all links"
1570msgstr "Renommer ou supprimer un tag dans tous les liens" 1809msgstr "Renommer ou supprimer un tag dans tous les liens"
1571 1810
1572#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 1811#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
1573msgid "" 1812msgid ""
1574"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " 1813"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
1575"delicious...)" 1814"delicious...)"
@@ -1577,11 +1816,11 @@ msgstr ""
1577"Importer des marques pages au format Netscape HTML (comme exportés depuis " 1816"Importer des marques pages au format Netscape HTML (comme exportés depuis "
1578"Firefox, Chrome, Opera, delicious...)" 1817"Firefox, Chrome, Opera, delicious...)"
1579 1818
1580#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 1819#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
1581msgid "Import links" 1820msgid "Import links"
1582msgstr "Importer des liens" 1821msgstr "Importer des liens"
1583 1822
1584#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 1823#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
1585msgid "" 1824msgid ""
1586"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " 1825"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
1587"Opera, delicious...)" 1826"Opera, delicious...)"
@@ -1589,15 +1828,11 @@ msgstr ""
1589"Exporter les marques pages au format Netscape HTML (comme exportés depuis " 1828"Exporter les marques pages au format Netscape HTML (comme exportés depuis "
1590"Firefox, Chrome, Opera, delicious...)" 1829"Firefox, Chrome, Opera, delicious...)"
1591 1830
1592#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 1831#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:54
1593msgid "Export database" 1832msgid "Export database"
1594msgstr "Exporter les données" 1833msgstr "Exporter les données"
1595 1834
1596#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:55 1835#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
1597msgid "Synchronize all link thumbnails"
1598msgstr "Synchroniser toutes les miniatures"
1599
1600#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81
1601msgid "" 1836msgid ""
1602"Drag one of these button to your bookmarks toolbar or right-click it and " 1837"Drag one of these button to your bookmarks toolbar or right-click it and "
1603"\"Bookmark This Link\"" 1838"\"Bookmark This Link\""
@@ -1605,13 +1840,13 @@ msgstr ""
1605"Glisser un de ces boutons dans votre barre de favoris ou cliquer droit " 1840"Glisser un de ces boutons dans votre barre de favoris ou cliquer droit "
1606"dessus et « Ajouter aux favoris »" 1841"dessus et « Ajouter aux favoris »"
1607 1842
1608#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82 1843#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
1609msgid "then click on the bookmarklet in any page you want to share." 1844msgid "then click on the bookmarklet in any page you want to share."
1610msgstr "" 1845msgstr ""
1611"puis cliquer sur le marque-page depuis un site que vous souhaitez partager." 1846"puis cliquer sur le marque-page depuis un site que vous souhaitez partager."
1612 1847
1613#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 1848#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82
1614#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 1849#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
1615msgid "" 1850msgid ""
1616"Drag this link to your bookmarks toolbar or right-click it and Bookmark This " 1851"Drag this link to your bookmarks toolbar or right-click it and Bookmark This "
1617"Link" 1852"Link"
@@ -1619,40 +1854,40 @@ msgstr ""
1619"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " 1854"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
1620"Ajouter aux favoris »" 1855"Ajouter aux favoris »"
1621 1856
1622#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 1857#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
1623msgid "then click ✚Shaare link button in any page you want to share" 1858msgid "then click ✚Shaare link button in any page you want to share"
1624msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager" 1859msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager"
1625 1860
1626#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 1861#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92
1627#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118 1862#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114
1628msgid "The selected text is too long, it will be truncated." 1863msgid "The selected text is too long, it will be truncated."
1629msgstr "Le texte sélectionné est trop long, il sera tronqué." 1864msgstr "Le texte sélectionné est trop long, il sera tronqué."
1630 1865
1631#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 1866#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
1632msgid "Shaare link" 1867msgid "Shaare link"
1633msgstr "Shaare" 1868msgstr "Shaare"
1634 1869
1635#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111 1870#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107
1636msgid "" 1871msgid ""
1637"Then click ✚Add Note button anytime to start composing a private Note (text " 1872"Then click ✚Add Note button anytime to start composing a private Note (text "
1638"post) to your Shaarli" 1873"post) to your Shaarli"
1639msgstr "" 1874msgstr ""
1640"Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli" 1875"Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli"
1641 1876
1642#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:127 1877#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
1643msgid "Add Note" 1878msgid "Add Note"
1644msgstr "Ajouter une Note" 1879msgstr "Ajouter une Note"
1645 1880
1646#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136 1881#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:132
1647msgid "3rd party" 1882msgid "3rd party"
1648msgstr "Applications tierces" 1883msgstr "Applications tierces"
1649 1884
1650#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 1885#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135
1651#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 1886#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:140
1652msgid "plugin" 1887msgid "plugin"
1653msgstr "extension" 1888msgstr "extension"
1654 1889
1655#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 1890#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165
1656msgid "" 1891msgid ""
1657"Drag this link to your bookmarks toolbar, or right-click it and choose " 1892"Drag this link to your bookmarks toolbar, or right-click it and choose "
1658"Bookmark This Link" 1893"Bookmark This Link"
@@ -1660,11 +1895,11 @@ msgstr ""
1660"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " 1895"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
1661"Ajouter aux favoris »" 1896"Ajouter aux favoris »"
1662 1897
1663#~ msgid "Provided data is invalid" 1898#~ msgid "Display:"
1664#~ msgstr "Les informations fournies ne sont pas valides" 1899#~ msgstr "Afficher :"
1665 1900
1666#~ msgid "Rename" 1901#~ msgid "The Daily Shaarli"
1667#~ msgstr "Renommer" 1902#~ msgstr "Le Quotidien Shaarli"
1668 1903
1669#, fuzzy 1904#, fuzzy
1670#~| msgid "Selection" 1905#~| msgid "Selection"
diff --git a/index.php b/index.php
index 1b10ee41..4b5602ac 100644
--- a/index.php
+++ b/index.php
@@ -125,13 +125,15 @@ $app->group('/admin', function () {
125 $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save'); 125 $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save');
126 $this->get('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index'); 126 $this->get('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index');
127 $this->post('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save'); 127 $this->post('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save');
128 $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:addShaare'); 128 $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ShaareAddController:addShaare');
129 $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayCreateForm'); 129 $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateForm');
130 $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayEditForm'); 130 $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayEditForm');
131 $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:save'); 131 $this->get('/shaare/private/{hash}', '\Shaarli\Front\Controller\Admin\ShaareManageController:sharePrivate');
132 $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ManageShaareController:deleteBookmark'); 132 $this->post('/shaare-batch', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateBatchForms');
133 $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ManageShaareController:changeVisibility'); 133 $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:save');
134 $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ManageShaareController:pinBookmark'); 134 $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ShaareManageController:deleteBookmark');
135 $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ShaareManageController:changeVisibility');
136 $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ShaareManageController:pinBookmark');
135 $this->patch( 137 $this->patch(
136 '/shaare/{id:[0-9]+}/update-thumbnail', 138 '/shaare/{id:[0-9]+}/update-thumbnail',
137 '\Shaarli\Front\Controller\Admin\ThumbnailsController:ajaxUpdate' 139 '\Shaarli\Front\Controller\Admin\ThumbnailsController:ajaxUpdate'
@@ -143,6 +145,8 @@ $app->group('/admin', function () {
143 $this->get('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index'); 145 $this->get('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index');
144 $this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save'); 146 $this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save');
145 $this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken'); 147 $this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken');
148 $this->get('/server', '\Shaarli\Front\Controller\Admin\ServerController:index');
149 $this->get('/clear-cache', '\Shaarli\Front\Controller\Admin\ServerController:clearCache');
146 $this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index'); 150 $this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index');
147 $this->get('/metadata', '\Shaarli\Front\Controller\Admin\MetadataController:ajaxRetrieveTitle'); 151 $this->get('/metadata', '\Shaarli\Front\Controller\Admin\MetadataController:ajaxRetrieveTitle');
148 $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility'); 152 $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility');
diff --git a/init.php b/init.php
index ab0e4ea7..d8462712 100644
--- a/init.php
+++ b/init.php
@@ -2,7 +2,7 @@
2 2
3require_once __DIR__ . '/vendor/autoload.php'; 3require_once __DIR__ . '/vendor/autoload.php';
4 4
5use Shaarli\ApplicationUtils; 5use Shaarli\Helper\ApplicationUtils;
6use Shaarli\Security\SessionManager; 6use Shaarli\Security\SessionManager;
7 7
8// Set 'UTC' as the default timezone if it is not defined in php.ini 8// Set 'UTC' as the default timezone if it is not defined in php.ini
diff --git a/tests/api/controllers/links/PostLinkTest.php b/tests/api/controllers/links/PostLinkTest.php
index 7ff92f5c..e12f803b 100644
--- a/tests/api/controllers/links/PostLinkTest.php
+++ b/tests/api/controllers/links/PostLinkTest.php
@@ -92,8 +92,8 @@ class PostLinkTest extends TestCase
92 92
93 $mock = $this->createMock(Router::class); 93 $mock = $this->createMock(Router::class);
94 $mock->expects($this->any()) 94 $mock->expects($this->any())
95 ->method('relativePathFor') 95 ->method('pathFor')
96 ->willReturn('api/v1/bookmarks/1'); 96 ->willReturn('/api/v1/bookmarks/1');
97 97
98 // affect @property-read... seems to work 98 // affect @property-read... seems to work
99 $this->controller->getCi()->router = $mock; 99 $this->controller->getCi()->router = $mock;
@@ -128,7 +128,7 @@ class PostLinkTest extends TestCase
128 128
129 $response = $this->controller->postLink($request, new Response()); 129 $response = $this->controller->postLink($request, new Response());
130 $this->assertEquals(201, $response->getStatusCode()); 130 $this->assertEquals(201, $response->getStatusCode());
131 $this->assertEquals('api/v1/bookmarks/1', $response->getHeader('Location')[0]); 131 $this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]);
132 $data = json_decode((string) $response->getBody(), true); 132 $data = json_decode((string) $response->getBody(), true);
133 $this->assertEquals(self::NB_FIELDS_LINK, count($data)); 133 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
134 $this->assertEquals(43, $data['id']); 134 $this->assertEquals(43, $data['id']);
@@ -175,7 +175,7 @@ class PostLinkTest extends TestCase
175 $response = $this->controller->postLink($request, new Response()); 175 $response = $this->controller->postLink($request, new Response());
176 176
177 $this->assertEquals(201, $response->getStatusCode()); 177 $this->assertEquals(201, $response->getStatusCode());
178 $this->assertEquals('api/v1/bookmarks/1', $response->getHeader('Location')[0]); 178 $this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]);
179 $data = json_decode((string) $response->getBody(), true); 179 $data = json_decode((string) $response->getBody(), true);
180 $this->assertEquals(self::NB_FIELDS_LINK, count($data)); 180 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
181 $this->assertEquals(43, $data['id']); 181 $this->assertEquals(43, $data['id']);
diff --git a/tests/bookmark/BookmarkFileServiceTest.php b/tests/bookmark/BookmarkFileServiceTest.php
index daafd250..f619aff3 100644
--- a/tests/bookmark/BookmarkFileServiceTest.php
+++ b/tests/bookmark/BookmarkFileServiceTest.php
@@ -686,22 +686,6 @@ class BookmarkFileServiceTest extends TestCase
686 } 686 }
687 687
688 /** 688 /**
689 * List the days for which bookmarks have been posted
690 */
691 public function testDays()
692 {
693 $this->assertSame(
694 ['20100309', '20100310', '20121206', '20121207', '20130614', '20150310'],
695 $this->publicLinkDB->days()
696 );
697
698 $this->assertSame(
699 ['20100309', '20100310', '20121206', '20121207', '20130614', '20141125', '20150310'],
700 $this->privateLinkDB->days()
701 );
702 }
703
704 /**
705 * The URL corresponds to an existing entry in the DB 689 * The URL corresponds to an existing entry in the DB
706 */ 690 */
707 public function testGetKnownLinkFromURL() 691 public function testGetKnownLinkFromURL()
@@ -898,6 +882,37 @@ class BookmarkFileServiceTest extends TestCase
898 } 882 }
899 883
900 /** 884 /**
885 * Test filterHash() on a private bookmark while logged out.
886 */
887 public function testFilterHashPrivateWhileLoggedOut()
888 {
889 $this->expectException(BookmarkNotFoundException::class);
890 $this->expectExceptionMessage('The link you are trying to reach does not exist or has been deleted');
891
892 $hash = smallHash('20141125_084734' . 6);
893
894 $this->publicLinkDB->findByHash($hash);
895 }
896
897 /**
898 * Test filterHash() with private key.
899 */
900 public function testFilterHashWithPrivateKey()
901 {
902 $hash = smallHash('20141125_084734' . 6);
903 $privateKey = 'this is usually auto generated';
904
905 $bookmark = $this->privateLinkDB->findByHash($hash);
906 $bookmark->addAdditionalContentEntry('private_key', $privateKey);
907 $this->privateLinkDB->save();
908
909 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false);
910 $bookmark = $this->privateLinkDB->findByHash($hash, $privateKey);
911
912 static::assertSame(6, $bookmark->getId());
913 }
914
915 /**
901 * Test linksCountPerTag all tags without filter. 916 * Test linksCountPerTag all tags without filter.
902 * Equal occurrences should be sorted alphabetically. 917 * Equal occurrences should be sorted alphabetically.
903 */ 918 */
@@ -1043,33 +1058,105 @@ class BookmarkFileServiceTest extends TestCase
1043 } 1058 }
1044 1059
1045 /** 1060 /**
1046 * Test filterDay while logged in 1061 * Test find by dates in the middle of the datastore (sorted by dates) with a single bookmark as a result.
1047 */ 1062 */
1048 public function testFilterDayLoggedIn(): void 1063 public function testFilterByDateMidTimePeriodSingleBookmark(): void
1049 { 1064 {
1050 $bookmarks = $this->privateLinkDB->filterDay('20121206'); 1065 $bookmarks = $this->privateLinkDB->findByDate(
1051 $expectedIds = [4, 9, 1, 0]; 1066 DateTime::createFromFormat('Ymd_His', '20121206_150000'),
1067 DateTime::createFromFormat('Ymd_His', '20121206_160000'),
1068 $before,
1069 $after
1070 );
1052 1071
1053 static::assertCount(4, $bookmarks); 1072 static::assertCount(1, $bookmarks);
1054 foreach ($bookmarks as $bookmark) { 1073
1055 $i = ($i ?? -1) + 1; 1074 static::assertSame(9, $bookmarks[0]->getId());
1056 static::assertSame($expectedIds[$i], $bookmark->getId()); 1075 static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_142300'), $before);
1057 } 1076 static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_172539'), $after);
1058 } 1077 }
1059 1078
1060 /** 1079 /**
1061 * Test filterDay while logged out 1080 * Test find by dates in the middle of the datastore (sorted by dates) with a multiple bookmarks as a result.
1062 */ 1081 */
1063 public function testFilterDayLoggedOut(): void 1082 public function testFilterByDateMidTimePeriodMultipleBookmarks(): void
1064 { 1083 {
1065 $bookmarks = $this->publicLinkDB->filterDay('20121206'); 1084 $bookmarks = $this->privateLinkDB->findByDate(
1066 $expectedIds = [4, 9, 1]; 1085 DateTime::createFromFormat('Ymd_His', '20121206_150000'),
1086 DateTime::createFromFormat('Ymd_His', '20121206_180000'),
1087 $before,
1088 $after
1089 );
1067 1090
1068 static::assertCount(3, $bookmarks); 1091 static::assertCount(2, $bookmarks);
1069 foreach ($bookmarks as $bookmark) { 1092
1070 $i = ($i ?? -1) + 1; 1093 static::assertSame(1, $bookmarks[0]->getId());
1071 static::assertSame($expectedIds[$i], $bookmark->getId()); 1094 static::assertSame(9, $bookmarks[1]->getId());
1072 } 1095 static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_142300'), $before);
1096 static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_182539'), $after);
1097 }
1098
1099 /**
1100 * Test find by dates at the end of the datastore (sorted by dates).
1101 */
1102 public function testFilterByDateLastTimePeriod(): void
1103 {
1104 $after = new DateTime();
1105 $bookmarks = $this->privateLinkDB->findByDate(
1106 DateTime::createFromFormat('Ymd_His', '20150310_114640'),
1107 DateTime::createFromFormat('Ymd_His', '20450101_010101'),
1108 $before,
1109 $after
1110 );
1111
1112 static::assertCount(1, $bookmarks);
1113
1114 static::assertSame(41, $bookmarks[0]->getId());
1115 static::assertEquals(DateTime::createFromFormat('Ymd_His', '20150310_114633'), $before);
1116 static::assertNull($after);
1117 }
1118
1119 /**
1120 * Test find by dates at the beginning of the datastore (sorted by dates).
1121 */
1122 public function testFilterByDateFirstTimePeriod(): void
1123 {
1124 $before = new DateTime();
1125 $bookmarks = $this->privateLinkDB->findByDate(
1126 DateTime::createFromFormat('Ymd_His', '20000101_101010'),
1127 DateTime::createFromFormat('Ymd_His', '20100309_110000'),
1128 $before,
1129 $after
1130 );
1131
1132 static::assertCount(1, $bookmarks);
1133
1134 static::assertSame(11, $bookmarks[0]->getId());
1135 static::assertNull($before);
1136 static::assertEquals(DateTime::createFromFormat('Ymd_His', '20100310_101010'), $after);
1137 }
1138
1139 /**
1140 * Test getLatest with a sticky bookmark: it should be ignored and return the latest by creation date instead.
1141 */
1142 public function testGetLatestWithSticky(): void
1143 {
1144 $bookmark = $this->publicLinkDB->getLatest();
1145
1146 static::assertSame(41, $bookmark->getId());
1147 }
1148
1149 /**
1150 * Test getLatest with a sticky bookmark: it should be ignored and return the latest by creation date instead.
1151 */
1152 public function testGetLatestEmptyDatastore(): void
1153 {
1154 unlink($this->conf->get('resource.datastore'));
1155 $this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false);
1156
1157 $bookmark = $this->publicLinkDB->getLatest();
1158
1159 static::assertNull($bookmark);
1073 } 1160 }
1074 1161
1075 /** 1162 /**
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php b/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php
deleted file mode 100644
index 0f27ec2f..00000000
--- a/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php
+++ /dev/null
@@ -1,47 +0,0 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
6
7use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
8use Shaarli\Front\Controller\Admin\ManageShaareController;
9use Shaarli\Http\HttpAccess;
10use Shaarli\TestCase;
11use Slim\Http\Request;
12use Slim\Http\Response;
13
14class AddShaareTest extends TestCase
15{
16 use FrontAdminControllerMockHelper;
17
18 /** @var ManageShaareController */
19 protected $controller;
20
21 public function setUp(): void
22 {
23 $this->createContainer();
24
25 $this->container->httpAccess = $this->createMock(HttpAccess::class);
26 $this->controller = new ManageShaareController($this->container);
27 }
28
29 /**
30 * Test displaying add link page
31 */
32 public function testAddShaare(): void
33 {
34 $assignedVariables = [];
35 $this->assignTemplateVars($assignedVariables);
36
37 $request = $this->createMock(Request::class);
38 $response = new Response();
39
40 $result = $this->controller->addShaare($request, $response);
41
42 static::assertSame(200, $result->getStatusCode());
43 static::assertSame('addlink', (string) $result->getBody());
44
45 static::assertSame('Shaare a new link - Shaarli', $assignedVariables['pagetitle']);
46 }
47}
diff --git a/tests/front/controller/admin/ServerControllerTest.php b/tests/front/controller/admin/ServerControllerTest.php
new file mode 100644
index 00000000..355cce7d
--- /dev/null
+++ b/tests/front/controller/admin/ServerControllerTest.php
@@ -0,0 +1,184 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Config\ConfigManager;
8use Shaarli\Security\SessionManager;
9use Shaarli\TestCase;
10use Slim\Http\Request;
11use Slim\Http\Response;
12
13/**
14 * Test Server administration controller.
15 */
16class ServerControllerTest extends TestCase
17{
18 use FrontAdminControllerMockHelper;
19
20 /** @var ServerController */
21 protected $controller;
22
23 public function setUp(): void
24 {
25 $this->createContainer();
26
27 $this->controller = new ServerController($this->container);
28
29 // initialize dummy cache
30 @mkdir('sandbox/');
31 foreach (['pagecache', 'tmp', 'cache'] as $folder) {
32 @mkdir('sandbox/' . $folder);
33 @touch('sandbox/' . $folder . '/.htaccess');
34 @touch('sandbox/' . $folder . '/1');
35 @touch('sandbox/' . $folder . '/2');
36 }
37 }
38
39 public function tearDown(): void
40 {
41 foreach (['pagecache', 'tmp', 'cache'] as $folder) {
42 @unlink('sandbox/' . $folder . '/.htaccess');
43 @unlink('sandbox/' . $folder . '/1');
44 @unlink('sandbox/' . $folder . '/2');
45 @rmdir('sandbox/' . $folder);
46 }
47 }
48
49 /**
50 * Test default display of server administration page.
51 */
52 public function testIndex(): void
53 {
54 $request = $this->createMock(Request::class);
55 $response = new Response();
56
57 // Save RainTPL assigned variables
58 $assignedVariables = [];
59 $this->assignTemplateVars($assignedVariables);
60
61 $result = $this->controller->index($request, $response);
62
63 static::assertSame(200, $result->getStatusCode());
64 static::assertSame('server', (string) $result->getBody());
65
66 static::assertSame(PHP_VERSION, $assignedVariables['php_version']);
67 static::assertArrayHasKey('php_has_reached_eol', $assignedVariables);
68 static::assertArrayHasKey('php_eol', $assignedVariables);
69 static::assertArrayHasKey('php_extensions', $assignedVariables);
70 static::assertArrayHasKey('permissions', $assignedVariables);
71 static::assertEmpty($assignedVariables['permissions']);
72
73 static::assertRegExp(
74 '#https://github\.com/shaarli/Shaarli/releases/tag/v\d+\.\d+\.\d+#',
75 $assignedVariables['release_url']
76 );
77 static::assertRegExp('#v\d+\.\d+\.\d+#', $assignedVariables['latest_version']);
78 static::assertRegExp('#(v\d+\.\d+\.\d+|dev)#', $assignedVariables['current_version']);
79 static::assertArrayHasKey('index_url', $assignedVariables);
80 static::assertArrayHasKey('client_ip', $assignedVariables);
81 static::assertArrayHasKey('trusted_proxies', $assignedVariables);
82
83 static::assertSame('Server administration - Shaarli', $assignedVariables['pagetitle']);
84 }
85
86 /**
87 * Test clearing the main cache
88 */
89 public function testClearMainCache(): void
90 {
91 $this->container->conf = $this->createMock(ConfigManager::class);
92 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
93 if ($key === 'resource.page_cache') {
94 return 'sandbox/pagecache';
95 } elseif ($key === 'resource.raintpl_tmp') {
96 return 'sandbox/tmp';
97 } elseif ($key === 'resource.thumbnails_cache') {
98 return 'sandbox/cache';
99 } else {
100 return $default;
101 }
102 });
103
104 $this->container->sessionManager
105 ->expects(static::once())
106 ->method('setSessionParameter')
107 ->with(SessionManager::KEY_SUCCESS_MESSAGES, ['Shaarli\'s cache folder has been cleared!'])
108 ;
109
110 $request = $this->createMock(Request::class);
111 $request->method('getQueryParam')->with('type')->willReturn('main');
112 $response = new Response();
113
114 $result = $this->controller->clearCache($request, $response);
115
116 static::assertSame(302, $result->getStatusCode());
117 static::assertSame('/subfolder/admin/server', (string) $result->getHeaderLine('Location'));
118
119 static::assertFileNotExists('sandbox/pagecache/1');
120 static::assertFileNotExists('sandbox/pagecache/2');
121 static::assertFileNotExists('sandbox/tmp/1');
122 static::assertFileNotExists('sandbox/tmp/2');
123
124 static::assertFileExists('sandbox/pagecache/.htaccess');
125 static::assertFileExists('sandbox/tmp/.htaccess');
126 static::assertFileExists('sandbox/cache');
127 static::assertFileExists('sandbox/cache/.htaccess');
128 static::assertFileExists('sandbox/cache/1');
129 static::assertFileExists('sandbox/cache/2');
130 }
131
132 /**
133 * Test clearing thumbnails cache
134 */
135 public function testClearThumbnailsCache(): void
136 {
137 $this->container->conf = $this->createMock(ConfigManager::class);
138 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
139 if ($key === 'resource.page_cache') {
140 return 'sandbox/pagecache';
141 } elseif ($key === 'resource.raintpl_tmp') {
142 return 'sandbox/tmp';
143 } elseif ($key === 'resource.thumbnails_cache') {
144 return 'sandbox/cache';
145 } else {
146 return $default;
147 }
148 });
149
150 $this->container->sessionManager
151 ->expects(static::once())
152 ->method('setSessionParameter')
153 ->willReturnCallback(function (string $key, array $value): SessionManager {
154 static::assertSame(SessionManager::KEY_WARNING_MESSAGES, $key);
155 static::assertCount(1, $value);
156 static::assertStringStartsWith('Thumbnails cache has been cleared.', $value[0]);
157
158 return $this->container->sessionManager;
159 });
160 ;
161
162 $request = $this->createMock(Request::class);
163 $request->method('getQueryParam')->with('type')->willReturn('thumbnails');
164 $response = new Response();
165
166 $result = $this->controller->clearCache($request, $response);
167
168 static::assertSame(302, $result->getStatusCode());
169 static::assertSame('/subfolder/admin/server', (string) $result->getHeaderLine('Location'));
170
171 static::assertFileNotExists('sandbox/cache/1');
172 static::assertFileNotExists('sandbox/cache/2');
173
174 static::assertFileExists('sandbox/cache/.htaccess');
175 static::assertFileExists('sandbox/pagecache');
176 static::assertFileExists('sandbox/pagecache/.htaccess');
177 static::assertFileExists('sandbox/pagecache/1');
178 static::assertFileExists('sandbox/pagecache/2');
179 static::assertFileExists('sandbox/tmp');
180 static::assertFileExists('sandbox/tmp/.htaccess');
181 static::assertFileExists('sandbox/tmp/1');
182 static::assertFileExists('sandbox/tmp/2');
183 }
184}
diff --git a/tests/front/controller/admin/ShaareAddControllerTest.php b/tests/front/controller/admin/ShaareAddControllerTest.php
new file mode 100644
index 00000000..a27ebe64
--- /dev/null
+++ b/tests/front/controller/admin/ShaareAddControllerTest.php
@@ -0,0 +1,97 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Config\ConfigManager;
8use Shaarli\Formatter\BookmarkMarkdownFormatter;
9use Shaarli\Http\HttpAccess;
10use Shaarli\TestCase;
11use Slim\Http\Request;
12use Slim\Http\Response;
13
14class ShaareAddControllerTest extends TestCase
15{
16 use FrontAdminControllerMockHelper;
17
18 /** @var ShaareAddController */
19 protected $controller;
20
21 public function setUp(): void
22 {
23 $this->createContainer();
24
25 $this->container->httpAccess = $this->createMock(HttpAccess::class);
26 $this->controller = new ShaareAddController($this->container);
27 }
28
29 /**
30 * Test displaying add link page
31 */
32 public function testAddShaare(): void
33 {
34 $assignedVariables = [];
35 $this->assignTemplateVars($assignedVariables);
36
37 $request = $this->createMock(Request::class);
38 $response = new Response();
39
40 $expectedTags = [
41 'tag1' => 32,
42 'tag2' => 24,
43 'tag3' => 1,
44 ];
45 $this->container->bookmarkService
46 ->expects(static::once())
47 ->method('bookmarksCountPerTag')
48 ->willReturn($expectedTags)
49 ;
50 $expectedTags = array_merge($expectedTags, [BookmarkMarkdownFormatter::NO_MD_TAG => 1]);
51
52 $this->container->conf = $this->createMock(ConfigManager::class);
53 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
54 return $key === 'formatter' ? 'markdown' : $default;
55 });
56
57 $result = $this->controller->addShaare($request, $response);
58
59 static::assertSame(200, $result->getStatusCode());
60 static::assertSame('addlink', (string) $result->getBody());
61
62 static::assertSame('Shaare a new link - Shaarli', $assignedVariables['pagetitle']);
63 static::assertFalse($assignedVariables['default_private_links']);
64 static::assertTrue($assignedVariables['async_metadata']);
65 static::assertSame($expectedTags, $assignedVariables['tags']);
66 }
67
68 /**
69 * Test displaying add link page
70 */
71 public function testAddShaareWithoutMd(): void
72 {
73 $assignedVariables = [];
74 $this->assignTemplateVars($assignedVariables);
75
76 $request = $this->createMock(Request::class);
77 $response = new Response();
78
79 $expectedTags = [
80 'tag1' => 32,
81 'tag2' => 24,
82 'tag3' => 1,
83 ];
84 $this->container->bookmarkService
85 ->expects(static::once())
86 ->method('bookmarksCountPerTag')
87 ->willReturn($expectedTags)
88 ;
89
90 $result = $this->controller->addShaare($request, $response);
91
92 static::assertSame(200, $result->getStatusCode());
93 static::assertSame('addlink', (string) $result->getBody());
94
95 static::assertSame($expectedTags, $assignedVariables['tags']);
96 }
97}
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/ChangeVisibilityBookmarkTest.php
index 096d0774..28b1c023 100644
--- a/tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php
+++ b/tests/front/controller/admin/ShaareManageControllerTest/ChangeVisibilityBookmarkTest.php
@@ -2,7 +2,7 @@
2 2
3declare(strict_types=1); 3declare(strict_types=1);
4 4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; 5namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest;
6 6
7use Shaarli\Bookmark\Bookmark; 7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
@@ -10,7 +10,7 @@ use Shaarli\Formatter\BookmarkFormatter;
10use Shaarli\Formatter\BookmarkRawFormatter; 10use Shaarli\Formatter\BookmarkRawFormatter;
11use Shaarli\Formatter\FormatterFactory; 11use Shaarli\Formatter\FormatterFactory;
12use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; 12use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
13use Shaarli\Front\Controller\Admin\ManageShaareController; 13use Shaarli\Front\Controller\Admin\ShaareManageController;
14use Shaarli\Http\HttpAccess; 14use Shaarli\Http\HttpAccess;
15use Shaarli\Security\SessionManager; 15use Shaarli\Security\SessionManager;
16use Shaarli\TestCase; 16use Shaarli\TestCase;
@@ -21,7 +21,7 @@ class ChangeVisibilityBookmarkTest extends TestCase
21{ 21{
22 use FrontAdminControllerMockHelper; 22 use FrontAdminControllerMockHelper;
23 23
24 /** @var ManageShaareController */ 24 /** @var ShaareManageController */
25 protected $controller; 25 protected $controller;
26 26
27 public function setUp(): void 27 public function setUp(): void
@@ -29,7 +29,7 @@ class ChangeVisibilityBookmarkTest extends TestCase
29 $this->createContainer(); 29 $this->createContainer();
30 30
31 $this->container->httpAccess = $this->createMock(HttpAccess::class); 31 $this->container->httpAccess = $this->createMock(HttpAccess::class);
32 $this->controller = new ManageShaareController($this->container); 32 $this->controller = new ShaareManageController($this->container);
33 } 33 }
34 34
35 /** 35 /**
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php
index 83bbee7c..770a16d7 100644
--- a/tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php
+++ b/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php
@@ -2,14 +2,14 @@
2 2
3declare(strict_types=1); 3declare(strict_types=1);
4 4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; 5namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest;
6 6
7use Shaarli\Bookmark\Bookmark; 7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Formatter\BookmarkFormatter; 9use Shaarli\Formatter\BookmarkFormatter;
10use Shaarli\Formatter\FormatterFactory; 10use Shaarli\Formatter\FormatterFactory;
11use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; 11use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
12use Shaarli\Front\Controller\Admin\ManageShaareController; 12use Shaarli\Front\Controller\Admin\ShaareManageController;
13use Shaarli\Http\HttpAccess; 13use Shaarli\Http\HttpAccess;
14use Shaarli\Security\SessionManager; 14use Shaarli\Security\SessionManager;
15use Shaarli\TestCase; 15use Shaarli\TestCase;
@@ -20,7 +20,7 @@ class DeleteBookmarkTest extends TestCase
20{ 20{
21 use FrontAdminControllerMockHelper; 21 use FrontAdminControllerMockHelper;
22 22
23 /** @var ManageShaareController */ 23 /** @var ShaareManageController */
24 protected $controller; 24 protected $controller;
25 25
26 public function setUp(): void 26 public function setUp(): void
@@ -28,7 +28,7 @@ class DeleteBookmarkTest extends TestCase
28 $this->createContainer(); 28 $this->createContainer();
29 29
30 $this->container->httpAccess = $this->createMock(HttpAccess::class); 30 $this->container->httpAccess = $this->createMock(HttpAccess::class);
31 $this->controller = new ManageShaareController($this->container); 31 $this->controller = new ShaareManageController($this->container);
32 } 32 }
33 33
34 /** 34 /**
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/PinBookmarkTest.php
index 50ce7df1..b89206ce 100644
--- a/tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php
+++ b/tests/front/controller/admin/ShaareManageControllerTest/PinBookmarkTest.php
@@ -2,12 +2,12 @@
2 2
3declare(strict_types=1); 3declare(strict_types=1);
4 4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; 5namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest;
6 6
7use Shaarli\Bookmark\Bookmark; 7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; 9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
10use Shaarli\Front\Controller\Admin\ManageShaareController; 10use Shaarli\Front\Controller\Admin\ShaareManageController;
11use Shaarli\Http\HttpAccess; 11use Shaarli\Http\HttpAccess;
12use Shaarli\Security\SessionManager; 12use Shaarli\Security\SessionManager;
13use Shaarli\TestCase; 13use Shaarli\TestCase;
@@ -18,7 +18,7 @@ class PinBookmarkTest extends TestCase
18{ 18{
19 use FrontAdminControllerMockHelper; 19 use FrontAdminControllerMockHelper;
20 20
21 /** @var ManageShaareController */ 21 /** @var ShaareManageController */
22 protected $controller; 22 protected $controller;
23 23
24 public function setUp(): void 24 public function setUp(): void
@@ -26,7 +26,7 @@ class PinBookmarkTest extends TestCase
26 $this->createContainer(); 26 $this->createContainer();
27 27
28 $this->container->httpAccess = $this->createMock(HttpAccess::class); 28 $this->container->httpAccess = $this->createMock(HttpAccess::class);
29 $this->controller = new ManageShaareController($this->container); 29 $this->controller = new ShaareManageController($this->container);
30 } 30 }
31 31
32 /** 32 /**
diff --git a/tests/front/controller/admin/ShaareManageControllerTest/SharePrivateTest.php b/tests/front/controller/admin/ShaareManageControllerTest/SharePrivateTest.php
new file mode 100644
index 00000000..ae61dfb7
--- /dev/null
+++ b/tests/front/controller/admin/ShaareManageControllerTest/SharePrivateTest.php
@@ -0,0 +1,139 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
9use Shaarli\Front\Controller\Admin\ShaareManageController;
10use Shaarli\Http\HttpAccess;
11use Shaarli\TestCase;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15/**
16 * Test GET /admin/shaare/private/{hash}
17 */
18class SharePrivateTest extends TestCase
19{
20 use FrontAdminControllerMockHelper;
21
22 /** @var ShaareManageController */
23 protected $controller;
24
25 public function setUp(): void
26 {
27 $this->createContainer();
28
29 $this->container->httpAccess = $this->createMock(HttpAccess::class);
30 $this->controller = new ShaareManageController($this->container);
31 }
32
33 /**
34 * Test shaare private with a private bookmark which does not have a key yet.
35 */
36 public function testSharePrivateWithNewPrivateBookmark(): void
37 {
38 $hash = 'abcdcef';
39 $request = $this->createMock(Request::class);
40 $response = new Response();
41
42 $bookmark = (new Bookmark())
43 ->setId(123)
44 ->setUrl('http://domain.tld')
45 ->setTitle('Title 123')
46 ->setPrivate(true)
47 ;
48
49 $this->container->bookmarkService
50 ->expects(static::once())
51 ->method('findByHash')
52 ->with($hash)
53 ->willReturn($bookmark)
54 ;
55 $this->container->bookmarkService
56 ->expects(static::once())
57 ->method('set')
58 ->with($bookmark, true)
59 ->willReturnCallback(function (Bookmark $bookmark): Bookmark {
60 static::assertSame(32, strlen($bookmark->getAdditionalContentEntry('private_key')));
61
62 return $bookmark;
63 })
64 ;
65
66 $result = $this->controller->sharePrivate($request, $response, ['hash' => $hash]);
67
68 static::assertSame(302, $result->getStatusCode());
69 static::assertRegExp('#/subfolder/shaare/' . $hash . '\?key=\w{32}#', $result->getHeaderLine('Location'));
70 }
71
72 /**
73 * Test shaare private with a private bookmark which does already have a key.
74 */
75 public function testSharePrivateWithExistingPrivateBookmark(): void
76 {
77 $hash = 'abcdcef';
78 $existingKey = 'this is a private key';
79 $request = $this->createMock(Request::class);
80 $response = new Response();
81
82 $bookmark = (new Bookmark())
83 ->setId(123)
84 ->setUrl('http://domain.tld')
85 ->setTitle('Title 123')
86 ->setPrivate(true)
87 ->addAdditionalContentEntry('private_key', $existingKey)
88 ;
89
90 $this->container->bookmarkService
91 ->expects(static::once())
92 ->method('findByHash')
93 ->with($hash)
94 ->willReturn($bookmark)
95 ;
96 $this->container->bookmarkService
97 ->expects(static::never())
98 ->method('set')
99 ;
100
101 $result = $this->controller->sharePrivate($request, $response, ['hash' => $hash]);
102
103 static::assertSame(302, $result->getStatusCode());
104 static::assertSame('/subfolder/shaare/' . $hash . '?key=' . $existingKey, $result->getHeaderLine('Location'));
105 }
106
107 /**
108 * Test shaare private with a public bookmark.
109 */
110 public function testSharePrivateWithPublicBookmark(): void
111 {
112 $hash = 'abcdcef';
113 $request = $this->createMock(Request::class);
114 $response = new Response();
115
116 $bookmark = (new Bookmark())
117 ->setId(123)
118 ->setUrl('http://domain.tld')
119 ->setTitle('Title 123')
120 ->setPrivate(false)
121 ;
122
123 $this->container->bookmarkService
124 ->expects(static::once())
125 ->method('findByHash')
126 ->with($hash)
127 ->willReturn($bookmark)
128 ;
129 $this->container->bookmarkService
130 ->expects(static::never())
131 ->method('set')
132 ;
133
134 $result = $this->controller->sharePrivate($request, $response, ['hash' => $hash]);
135
136 static::assertSame(302, $result->getStatusCode());
137 static::assertSame('/subfolder/shaare/' . $hash, $result->getHeaderLine('Location'));
138 }
139}
diff --git a/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php
new file mode 100644
index 00000000..ce8e112b
--- /dev/null
+++ b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php
@@ -0,0 +1,63 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest;
6
7use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
8use Shaarli\Front\Controller\Admin\ShaarePublishController;
9use Shaarli\Http\HttpAccess;
10use Shaarli\Http\MetadataRetriever;
11use Shaarli\TestCase;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15class DisplayCreateBatchFormTest extends TestCase
16{
17 use FrontAdminControllerMockHelper;
18
19 /** @var ShaarePublishController */
20 protected $controller;
21
22 public function setUp(): void
23 {
24 $this->createContainer();
25
26 $this->container->httpAccess = $this->createMock(HttpAccess::class);
27 $this->container->metadataRetriever = $this->createMock(MetadataRetriever::class);
28 $this->controller = new ShaarePublishController($this->container);
29 }
30
31 /**
32 * TODO
33 */
34 public function testDisplayCreateFormBatch(): void
35 {
36 $urls = [
37 'https://domain1.tld/url1',
38 'https://domain2.tld/url2',
39 ' ',
40 'https://domain3.tld/url3',
41 ];
42
43 $request = $this->createMock(Request::class);
44 $request->method('getParam')->willReturnCallback(function (string $key) use ($urls): ?string {
45 return $key === 'urls' ? implode(PHP_EOL, $urls) : null;
46 });
47 $response = new Response();
48
49 $assignedVariables = [];
50 $this->assignTemplateVars($assignedVariables);
51
52 $result = $this->controller->displayCreateBatchForms($request, $response);
53
54 static::assertSame(200, $result->getStatusCode());
55 static::assertSame('editlink.batch', (string) $result->getBody());
56
57 static::assertTrue($assignedVariables['batch_mode']);
58 static::assertCount(3, $assignedVariables['links']);
59 static::assertSame($urls[0], $assignedVariables['links'][0]['link']['url']);
60 static::assertSame($urls[1], $assignedVariables['links'][1]['link']['url']);
61 static::assertSame($urls[3], $assignedVariables['links'][2]['link']['url']);
62 }
63}
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateFormTest.php
index eafa54eb..f20b1def 100644
--- a/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php
+++ b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateFormTest.php
@@ -2,12 +2,12 @@
2 2
3declare(strict_types=1); 3declare(strict_types=1);
4 4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; 5namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest;
6 6
7use Shaarli\Bookmark\Bookmark; 7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; 9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
10use Shaarli\Front\Controller\Admin\ManageShaareController; 10use Shaarli\Front\Controller\Admin\ShaarePublishController;
11use Shaarli\Http\HttpAccess; 11use Shaarli\Http\HttpAccess;
12use Shaarli\Http\MetadataRetriever; 12use Shaarli\Http\MetadataRetriever;
13use Shaarli\TestCase; 13use Shaarli\TestCase;
@@ -18,7 +18,7 @@ class DisplayCreateFormTest extends TestCase
18{ 18{
19 use FrontAdminControllerMockHelper; 19 use FrontAdminControllerMockHelper;
20 20
21 /** @var ManageShaareController */ 21 /** @var ShaarePublishController */
22 protected $controller; 22 protected $controller;
23 23
24 public function setUp(): void 24 public function setUp(): void
@@ -27,7 +27,7 @@ class DisplayCreateFormTest extends TestCase
27 27
28 $this->container->httpAccess = $this->createMock(HttpAccess::class); 28 $this->container->httpAccess = $this->createMock(HttpAccess::class);
29 $this->container->metadataRetriever = $this->createMock(MetadataRetriever::class); 29 $this->container->metadataRetriever = $this->createMock(MetadataRetriever::class);
30 $this->controller = new ManageShaareController($this->container); 30 $this->controller = new ShaarePublishController($this->container);
31 } 31 }
32 32
33 /** 33 /**
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayEditFormTest.php
index 2dc3f41c..da393e49 100644
--- a/tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php
+++ b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayEditFormTest.php
@@ -2,12 +2,12 @@
2 2
3declare(strict_types=1); 3declare(strict_types=1);
4 4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; 5namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest;
6 6
7use Shaarli\Bookmark\Bookmark; 7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; 9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
10use Shaarli\Front\Controller\Admin\ManageShaareController; 10use Shaarli\Front\Controller\Admin\ShaarePublishController;
11use Shaarli\Http\HttpAccess; 11use Shaarli\Http\HttpAccess;
12use Shaarli\Security\SessionManager; 12use Shaarli\Security\SessionManager;
13use Shaarli\TestCase; 13use Shaarli\TestCase;
@@ -18,7 +18,7 @@ class DisplayEditFormTest extends TestCase
18{ 18{
19 use FrontAdminControllerMockHelper; 19 use FrontAdminControllerMockHelper;
20 20
21 /** @var ManageShaareController */ 21 /** @var ShaarePublishController */
22 protected $controller; 22 protected $controller;
23 23
24 public function setUp(): void 24 public function setUp(): void
@@ -26,7 +26,7 @@ class DisplayEditFormTest extends TestCase
26 $this->createContainer(); 26 $this->createContainer();
27 27
28 $this->container->httpAccess = $this->createMock(HttpAccess::class); 28 $this->container->httpAccess = $this->createMock(HttpAccess::class);
29 $this->controller = new ManageShaareController($this->container); 29 $this->controller = new ShaarePublishController($this->container);
30 } 30 }
31 31
32 /** 32 /**
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/SaveBookmarkTest.php
index 1adeef5a..b6a861bc 100644
--- a/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php
+++ b/tests/front/controller/admin/ShaarePublishControllerTest/SaveBookmarkTest.php
@@ -2,12 +2,12 @@
2 2
3declare(strict_types=1); 3declare(strict_types=1);
4 4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; 5namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest;
6 6
7use Shaarli\Bookmark\Bookmark; 7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; 9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
10use Shaarli\Front\Controller\Admin\ManageShaareController; 10use Shaarli\Front\Controller\Admin\ShaarePublishController;
11use Shaarli\Front\Exception\WrongTokenException; 11use Shaarli\Front\Exception\WrongTokenException;
12use Shaarli\Http\HttpAccess; 12use Shaarli\Http\HttpAccess;
13use Shaarli\Security\SessionManager; 13use Shaarli\Security\SessionManager;
@@ -20,7 +20,7 @@ class SaveBookmarkTest extends TestCase
20{ 20{
21 use FrontAdminControllerMockHelper; 21 use FrontAdminControllerMockHelper;
22 22
23 /** @var ManageShaareController */ 23 /** @var ShaarePublishController */
24 protected $controller; 24 protected $controller;
25 25
26 public function setUp(): void 26 public function setUp(): void
@@ -28,7 +28,7 @@ class SaveBookmarkTest extends TestCase
28 $this->createContainer(); 28 $this->createContainer();
29 29
30 $this->container->httpAccess = $this->createMock(HttpAccess::class); 30 $this->container->httpAccess = $this->createMock(HttpAccess::class);
31 $this->controller = new ManageShaareController($this->container); 31 $this->controller = new ShaarePublishController($this->container);
32 } 32 }
33 33
34 /** 34 /**
diff --git a/tests/front/controller/visitor/BookmarkListControllerTest.php b/tests/front/controller/visitor/BookmarkListControllerTest.php
index 5ca92507..5cbc8c73 100644
--- a/tests/front/controller/visitor/BookmarkListControllerTest.php
+++ b/tests/front/controller/visitor/BookmarkListControllerTest.php
@@ -292,6 +292,37 @@ class BookmarkListControllerTest extends TestCase
292 } 292 }
293 293
294 /** 294 /**
295 * Test GET /shaare/{hash}?key={key} - Find a link by hash using a private link.
296 */
297 public function testPermalinkWithPrivateKey(): void
298 {
299 $hash = 'abcdef';
300 $privateKey = 'this is a private key';
301
302 $assignedVariables = [];
303 $this->assignTemplateVars($assignedVariables);
304
305 $request = $this->createMock(Request::class);
306 $request->method('getParam')->willReturnCallback(function (string $key, $default = null) use ($privateKey) {
307 return $key === 'key' ? $privateKey : $default;
308 });
309 $response = new Response();
310
311 $this->container->bookmarkService
312 ->expects(static::once())
313 ->method('findByHash')
314 ->with($hash, $privateKey)
315 ->willReturn((new Bookmark())->setId(123)->setTitle('Title 1')->setUrl('http://url1.tld'))
316 ;
317
318 $result = $this->controller->permalink($request, $response, ['hash' => $hash]);
319
320 static::assertSame(200, $result->getStatusCode());
321 static::assertSame('linklist', (string) $result->getBody());
322 static::assertCount(1, $assignedVariables['links']);
323 }
324
325 /**
295 * Test getting link list with thumbnail updates. 326 * Test getting link list with thumbnail updates.
296 * -> 2 thumbnails update, only 1 datastore write 327 * -> 2 thumbnails update, only 1 datastore write
297 */ 328 */
diff --git a/tests/front/controller/visitor/DailyControllerTest.php b/tests/front/controller/visitor/DailyControllerTest.php
index fc78bc13..70fbce54 100644
--- a/tests/front/controller/visitor/DailyControllerTest.php
+++ b/tests/front/controller/visitor/DailyControllerTest.php
@@ -28,52 +28,49 @@ class DailyControllerTest extends TestCase
28 public function testValidIndexControllerInvokeDefault(): void 28 public function testValidIndexControllerInvokeDefault(): void
29 { 29 {
30 $currentDay = new \DateTimeImmutable('2020-05-13'); 30 $currentDay = new \DateTimeImmutable('2020-05-13');
31 $previousDate = new \DateTime('2 days ago 00:00:00');
32 $nextDate = new \DateTime('today 00:00:00');
31 33
32 $request = $this->createMock(Request::class); 34 $request = $this->createMock(Request::class);
33 $request->method('getQueryParam')->willReturn($currentDay->format('Ymd')); 35 $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
36 return $key === 'day' ? $currentDay->format('Ymd') : null;
37 });
34 $response = new Response(); 38 $response = new Response();
35 39
36 // Save RainTPL assigned variables 40 // Save RainTPL assigned variables
37 $assignedVariables = []; 41 $assignedVariables = [];
38 $this->assignTemplateVars($assignedVariables); 42 $this->assignTemplateVars($assignedVariables);
39 43
40 // Links dataset: 2 links with thumbnails
41 $this->container->bookmarkService
42 ->expects(static::once())
43 ->method('days')
44 ->willReturnCallback(function () use ($currentDay): array {
45 return [
46 '20200510',
47 $currentDay->format('Ymd'),
48 '20200516',
49 ];
50 })
51 ;
52 $this->container->bookmarkService 44 $this->container->bookmarkService
53 ->expects(static::once()) 45 ->expects(static::once())
54 ->method('filterDay') 46 ->method('findByDate')
55 ->willReturnCallback(function (): array { 47 ->willReturnCallback(
56 return [ 48 function ($from, $to, &$previous, &$next) use ($currentDay, $previousDate, $nextDate): array {
57 (new Bookmark()) 49 $previous = $previousDate;
58 ->setId(1) 50 $next = $nextDate;
59 ->setUrl('http://url.tld') 51
60 ->setTitle(static::generateString(50)) 52 return [
61 ->setDescription(static::generateString(500)) 53 (new Bookmark())
62 , 54 ->setId(1)
63 (new Bookmark()) 55 ->setUrl('http://url.tld')
64 ->setId(2) 56 ->setTitle(static::generateString(50))
65 ->setUrl('http://url2.tld') 57 ->setDescription(static::generateString(500))
66 ->setTitle(static::generateString(50)) 58 ,
67 ->setDescription(static::generateString(500)) 59 (new Bookmark())
68 , 60 ->setId(2)
69 (new Bookmark()) 61 ->setUrl('http://url2.tld')
70 ->setId(3) 62 ->setTitle(static::generateString(50))
71 ->setUrl('http://url3.tld') 63 ->setDescription(static::generateString(500))
72 ->setTitle(static::generateString(50)) 64 ,
73 ->setDescription(static::generateString(500)) 65 (new Bookmark())
74 , 66 ->setId(3)
75 ]; 67 ->setUrl('http://url3.tld')
76 }) 68 ->setTitle(static::generateString(50))
69 ->setDescription(static::generateString(500))
70 ,
71 ];
72 }
73 )
77 ; 74 ;
78 75
79 // Make sure that PluginManager hook is triggered 76 // Make sure that PluginManager hook is triggered
@@ -81,20 +78,22 @@ class DailyControllerTest extends TestCase
81 ->expects(static::atLeastOnce()) 78 ->expects(static::atLeastOnce())
82 ->method('executeHooks') 79 ->method('executeHooks')
83 ->withConsecutive(['render_daily']) 80 ->withConsecutive(['render_daily'])
84 ->willReturnCallback(function (string $hook, array $data, array $param) use ($currentDay): array { 81 ->willReturnCallback(
85 if ('render_daily' === $hook) { 82 function (string $hook, array $data, array $param) use ($currentDay, $previousDate, $nextDate): array {
86 static::assertArrayHasKey('linksToDisplay', $data); 83 if ('render_daily' === $hook) {
87 static::assertCount(3, $data['linksToDisplay']); 84 static::assertArrayHasKey('linksToDisplay', $data);
88 static::assertSame(1, $data['linksToDisplay'][0]['id']); 85 static::assertCount(3, $data['linksToDisplay']);
89 static::assertSame($currentDay->getTimestamp(), $data['day']); 86 static::assertSame(1, $data['linksToDisplay'][0]['id']);
90 static::assertSame('20200510', $data['previousday']); 87 static::assertSame($currentDay->getTimestamp(), $data['day']);
91 static::assertSame('20200516', $data['nextday']); 88 static::assertSame($previousDate->format('Ymd'), $data['previousday']);
92 89 static::assertSame($nextDate->format('Ymd'), $data['nextday']);
93 static::assertArrayHasKey('loggedin', $param); 90
91 static::assertArrayHasKey('loggedin', $param);
92 }
93
94 return $data;
94 } 95 }
95 96 )
96 return $data;
97 })
98 ; 97 ;
99 98
100 $result = $this->controller->index($request, $response); 99 $result = $this->controller->index($request, $response);
@@ -107,6 +106,11 @@ class DailyControllerTest extends TestCase
107 ); 106 );
108 static::assertEquals($currentDay, $assignedVariables['dayDate']); 107 static::assertEquals($currentDay, $assignedVariables['dayDate']);
109 static::assertEquals($currentDay->getTimestamp(), $assignedVariables['day']); 108 static::assertEquals($currentDay->getTimestamp(), $assignedVariables['day']);
109 static::assertSame($previousDate->format('Ymd'), $assignedVariables['previousday']);
110 static::assertSame($nextDate->format('Ymd'), $assignedVariables['nextday']);
111 static::assertSame('day', $assignedVariables['type']);
112 static::assertSame('May 13, 2020', $assignedVariables['dayDesc']);
113 static::assertSame('Daily', $assignedVariables['localizedType']);
110 static::assertCount(3, $assignedVariables['linksToDisplay']); 114 static::assertCount(3, $assignedVariables['linksToDisplay']);
111 115
112 $link = $assignedVariables['linksToDisplay'][0]; 116 $link = $assignedVariables['linksToDisplay'][0];
@@ -171,27 +175,20 @@ class DailyControllerTest extends TestCase
171 $currentDay = new \DateTimeImmutable('2020-05-13'); 175 $currentDay = new \DateTimeImmutable('2020-05-13');
172 176
173 $request = $this->createMock(Request::class); 177 $request = $this->createMock(Request::class);
178 $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
179 return $key === 'day' ? $currentDay->format('Ymd') : null;
180 });
174 $response = new Response(); 181 $response = new Response();
175 182
176 // Save RainTPL assigned variables 183 // Save RainTPL assigned variables
177 $assignedVariables = []; 184 $assignedVariables = [];
178 $this->assignTemplateVars($assignedVariables); 185 $this->assignTemplateVars($assignedVariables);
179 186
180 // Links dataset: 2 links with thumbnails
181 $this->container->bookmarkService 187 $this->container->bookmarkService
182 ->expects(static::once()) 188 ->expects(static::once())
183 ->method('days') 189 ->method('findByDate')
184 ->willReturnCallback(function () use ($currentDay): array { 190 ->willReturnCallback(function () use ($currentDay): array {
185 return [ 191 return [
186 $currentDay->format($currentDay->format('Ymd')),
187 ];
188 })
189 ;
190 $this->container->bookmarkService
191 ->expects(static::once())
192 ->method('filterDay')
193 ->willReturnCallback(function (): array {
194 return [
195 (new Bookmark()) 192 (new Bookmark())
196 ->setId(1) 193 ->setId(1)
197 ->setUrl('http://url.tld') 194 ->setUrl('http://url.tld')
@@ -250,21 +247,11 @@ class DailyControllerTest extends TestCase
250 $assignedVariables = []; 247 $assignedVariables = [];
251 $this->assignTemplateVars($assignedVariables); 248 $this->assignTemplateVars($assignedVariables);
252 249
253 // Links dataset: 2 links with thumbnails
254 $this->container->bookmarkService 250 $this->container->bookmarkService
255 ->expects(static::once()) 251 ->expects(static::once())
256 ->method('days') 252 ->method('findByDate')
257 ->willReturnCallback(function () use ($currentDay): array { 253 ->willReturnCallback(function () use ($currentDay): array {
258 return [ 254 return [
259 $currentDay->format($currentDay->format('Ymd')),
260 ];
261 })
262 ;
263 $this->container->bookmarkService
264 ->expects(static::once())
265 ->method('filterDay')
266 ->willReturnCallback(function (): array {
267 return [
268 (new Bookmark())->setId(1)->setUrl('http://url.tld')->setTitle('title'), 255 (new Bookmark())->setId(1)->setUrl('http://url.tld')->setTitle('title'),
269 (new Bookmark()) 256 (new Bookmark())
270 ->setId(2) 257 ->setId(2)
@@ -320,14 +307,7 @@ class DailyControllerTest extends TestCase
320 // Links dataset: 2 links with thumbnails 307 // Links dataset: 2 links with thumbnails
321 $this->container->bookmarkService 308 $this->container->bookmarkService
322 ->expects(static::once()) 309 ->expects(static::once())
323 ->method('days') 310 ->method('findByDate')
324 ->willReturnCallback(function (): array {
325 return [];
326 })
327 ;
328 $this->container->bookmarkService
329 ->expects(static::once())
330 ->method('filterDay')
331 ->willReturnCallback(function (): array { 311 ->willReturnCallback(function (): array {
332 return []; 312 return [];
333 }) 313 })
@@ -347,7 +327,7 @@ class DailyControllerTest extends TestCase
347 static::assertSame(200, $result->getStatusCode()); 327 static::assertSame(200, $result->getStatusCode());
348 static::assertSame('daily', (string) $result->getBody()); 328 static::assertSame('daily', (string) $result->getBody());
349 static::assertCount(0, $assignedVariables['linksToDisplay']); 329 static::assertCount(0, $assignedVariables['linksToDisplay']);
350 static::assertSame('Today', $assignedVariables['dayDesc']); 330 static::assertSame('Today - ' . (new \DateTime())->format('F j, Y'), $assignedVariables['dayDesc']);
351 static::assertEquals((new \DateTime())->setTime(0, 0)->getTimestamp(), $assignedVariables['day']); 331 static::assertEquals((new \DateTime())->setTime(0, 0)->getTimestamp(), $assignedVariables['day']);
352 static::assertEquals((new \DateTime())->setTime(0, 0), $assignedVariables['dayDate']); 332 static::assertEquals((new \DateTime())->setTime(0, 0), $assignedVariables['dayDate']);
353 } 333 }
@@ -361,6 +341,7 @@ class DailyControllerTest extends TestCase
361 new \DateTimeImmutable('2020-05-17'), 341 new \DateTimeImmutable('2020-05-17'),
362 new \DateTimeImmutable('2020-05-15'), 342 new \DateTimeImmutable('2020-05-15'),
363 new \DateTimeImmutable('2020-05-13'), 343 new \DateTimeImmutable('2020-05-13'),
344 new \DateTimeImmutable('+1 month'),
364 ]; 345 ];
365 346
366 $request = $this->createMock(Request::class); 347 $request = $this->createMock(Request::class);
@@ -371,6 +352,7 @@ class DailyControllerTest extends TestCase
371 (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), 352 (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
372 (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), 353 (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
373 (new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'), 354 (new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'),
355 (new Bookmark())->setId(5)->setCreated($dates[3])->setUrl('http://domain.tld/5'),
374 ]); 356 ]);
375 357
376 $this->container->pageCacheManager 358 $this->container->pageCacheManager
@@ -397,13 +379,14 @@ class DailyControllerTest extends TestCase
397 static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']); 379 static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
398 static::assertSame('http://shaarli/subfolder/daily-rss', $assignedVariables['page_url']); 380 static::assertSame('http://shaarli/subfolder/daily-rss', $assignedVariables['page_url']);
399 static::assertFalse($assignedVariables['hide_timestamps']); 381 static::assertFalse($assignedVariables['hide_timestamps']);
400 static::assertCount(2, $assignedVariables['days']); 382 static::assertCount(3, $assignedVariables['days']);
401 383
402 $day = $assignedVariables['days'][$dates[0]->format('Ymd')]; 384 $day = $assignedVariables['days'][$dates[0]->format('Ymd')];
385 $date = $dates[0]->setTime(23, 59, 59);
403 386
404 static::assertEquals($dates[0], $day['date']); 387 static::assertEquals($date, $day['date']);
405 static::assertSame($dates[0]->format(\DateTime::RSS), $day['date_rss']); 388 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
406 static::assertSame(format_date($dates[0], false), $day['date_human']); 389 static::assertSame(format_date($date, false), $day['date_human']);
407 static::assertSame('http://shaarli/subfolder/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']); 390 static::assertSame('http://shaarli/subfolder/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']);
408 static::assertCount(1, $day['links']); 391 static::assertCount(1, $day['links']);
409 static::assertSame(1, $day['links'][0]['id']); 392 static::assertSame(1, $day['links'][0]['id']);
@@ -411,10 +394,11 @@ class DailyControllerTest extends TestCase
411 static::assertEquals($dates[0], $day['links'][0]['created']); 394 static::assertEquals($dates[0], $day['links'][0]['created']);
412 395
413 $day = $assignedVariables['days'][$dates[1]->format('Ymd')]; 396 $day = $assignedVariables['days'][$dates[1]->format('Ymd')];
397 $date = $dates[1]->setTime(23, 59, 59);
414 398
415 static::assertEquals($dates[1], $day['date']); 399 static::assertEquals($date, $day['date']);
416 static::assertSame($dates[1]->format(\DateTime::RSS), $day['date_rss']); 400 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
417 static::assertSame(format_date($dates[1], false), $day['date_human']); 401 static::assertSame(format_date($date, false), $day['date_human']);
418 static::assertSame('http://shaarli/subfolder/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']); 402 static::assertSame('http://shaarli/subfolder/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']);
419 static::assertCount(2, $day['links']); 403 static::assertCount(2, $day['links']);
420 404
@@ -424,6 +408,18 @@ class DailyControllerTest extends TestCase
424 static::assertSame(3, $day['links'][1]['id']); 408 static::assertSame(3, $day['links'][1]['id']);
425 static::assertSame('http://domain.tld/3', $day['links'][1]['url']); 409 static::assertSame('http://domain.tld/3', $day['links'][1]['url']);
426 static::assertEquals($dates[1], $day['links'][1]['created']); 410 static::assertEquals($dates[1], $day['links'][1]['created']);
411
412 $day = $assignedVariables['days'][$dates[2]->format('Ymd')];
413 $date = $dates[2]->setTime(23, 59, 59);
414
415 static::assertEquals($date, $day['date']);
416 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
417 static::assertSame(format_date($date, false), $day['date_human']);
418 static::assertSame('http://shaarli/subfolder/daily?day='. $dates[2]->format('Ymd'), $day['absolute_url']);
419 static::assertCount(1, $day['links']);
420 static::assertSame(4, $day['links'][0]['id']);
421 static::assertSame('http://domain.tld/4', $day['links'][0]['url']);
422 static::assertEquals($dates[2], $day['links'][0]['created']);
427 } 423 }
428 424
429 /** 425 /**
@@ -475,4 +471,246 @@ class DailyControllerTest extends TestCase
475 static::assertFalse($assignedVariables['hide_timestamps']); 471 static::assertFalse($assignedVariables['hide_timestamps']);
476 static::assertCount(0, $assignedVariables['days']); 472 static::assertCount(0, $assignedVariables['days']);
477 } 473 }
474
475 /**
476 * Test simple display index with week parameter
477 */
478 public function testSimpleIndexWeekly(): void
479 {
480 $currentDay = new \DateTimeImmutable('2020-05-13');
481 $expectedDay = new \DateTimeImmutable('2020-05-11');
482
483 $request = $this->createMock(Request::class);
484 $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
485 return $key === 'week' ? $currentDay->format('YW') : null;
486 });
487 $response = new Response();
488
489 // Save RainTPL assigned variables
490 $assignedVariables = [];
491 $this->assignTemplateVars($assignedVariables);
492
493 $this->container->bookmarkService
494 ->expects(static::once())
495 ->method('findByDate')
496 ->willReturnCallback(
497 function (): array {
498 return [
499 (new Bookmark())
500 ->setId(1)
501 ->setUrl('http://url.tld')
502 ->setTitle(static::generateString(50))
503 ->setDescription(static::generateString(500))
504 ,
505 (new Bookmark())
506 ->setId(2)
507 ->setUrl('http://url2.tld')
508 ->setTitle(static::generateString(50))
509 ->setDescription(static::generateString(500))
510 ,
511 ];
512 }
513 )
514 ;
515
516 $result = $this->controller->index($request, $response);
517
518 static::assertSame(200, $result->getStatusCode());
519 static::assertSame('daily', (string) $result->getBody());
520 static::assertSame(
521 'Weekly - Week 20 (May 11, 2020) - Shaarli',
522 $assignedVariables['pagetitle']
523 );
524
525 static::assertCount(2, $assignedVariables['linksToDisplay']);
526 static::assertEquals($expectedDay->setTime(0, 0), $assignedVariables['dayDate']);
527 static::assertSame($expectedDay->setTime(0, 0)->getTimestamp(), $assignedVariables['day']);
528 static::assertSame('', $assignedVariables['previousday']);
529 static::assertSame('', $assignedVariables['nextday']);
530 static::assertSame('Week 20 (May 11, 2020)', $assignedVariables['dayDesc']);
531 static::assertSame('week', $assignedVariables['type']);
532 static::assertSame('Weekly', $assignedVariables['localizedType']);
533 }
534
535 /**
536 * Test simple display index with month parameter
537 */
538 public function testSimpleIndexMonthly(): void
539 {
540 $currentDay = new \DateTimeImmutable('2020-05-13');
541 $expectedDay = new \DateTimeImmutable('2020-05-01');
542
543 $request = $this->createMock(Request::class);
544 $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
545 return $key === 'month' ? $currentDay->format('Ym') : null;
546 });
547 $response = new Response();
548
549 // Save RainTPL assigned variables
550 $assignedVariables = [];
551 $this->assignTemplateVars($assignedVariables);
552
553 $this->container->bookmarkService
554 ->expects(static::once())
555 ->method('findByDate')
556 ->willReturnCallback(
557 function (): array {
558 return [
559 (new Bookmark())
560 ->setId(1)
561 ->setUrl('http://url.tld')
562 ->setTitle(static::generateString(50))
563 ->setDescription(static::generateString(500))
564 ,
565 (new Bookmark())
566 ->setId(2)
567 ->setUrl('http://url2.tld')
568 ->setTitle(static::generateString(50))
569 ->setDescription(static::generateString(500))
570 ,
571 ];
572 }
573 )
574 ;
575
576 $result = $this->controller->index($request, $response);
577
578 static::assertSame(200, $result->getStatusCode());
579 static::assertSame('daily', (string) $result->getBody());
580 static::assertSame(
581 'Monthly - May, 2020 - Shaarli',
582 $assignedVariables['pagetitle']
583 );
584
585 static::assertCount(2, $assignedVariables['linksToDisplay']);
586 static::assertEquals($expectedDay->setTime(0, 0), $assignedVariables['dayDate']);
587 static::assertSame($expectedDay->setTime(0, 0)->getTimestamp(), $assignedVariables['day']);
588 static::assertSame('', $assignedVariables['previousday']);
589 static::assertSame('', $assignedVariables['nextday']);
590 static::assertSame('May, 2020', $assignedVariables['dayDesc']);
591 static::assertSame('month', $assignedVariables['type']);
592 static::assertSame('Monthly', $assignedVariables['localizedType']);
593 }
594
595 /**
596 * Test simple display RSS with week parameter
597 */
598 public function testSimpleRssWeekly(): void
599 {
600 $dates = [
601 new \DateTimeImmutable('2020-05-19'),
602 new \DateTimeImmutable('2020-05-13'),
603 ];
604 $expectedDates = [
605 new \DateTimeImmutable('2020-05-24 23:59:59'),
606 new \DateTimeImmutable('2020-05-17 23:59:59'),
607 ];
608
609 $this->container->environment['QUERY_STRING'] = 'week';
610 $request = $this->createMock(Request::class);
611 $request->method('getQueryParam')->willReturnCallback(function (string $key): ?string {
612 return $key === 'week' ? '' : null;
613 });
614 $response = new Response();
615
616 $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([
617 (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
618 (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
619 (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
620 ]);
621
622 // Save RainTPL assigned variables
623 $assignedVariables = [];
624 $this->assignTemplateVars($assignedVariables);
625
626 $result = $this->controller->rss($request, $response);
627
628 static::assertSame(200, $result->getStatusCode());
629 static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
630 static::assertSame('dailyrss', (string) $result->getBody());
631 static::assertSame('Shaarli', $assignedVariables['title']);
632 static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
633 static::assertSame('http://shaarli/subfolder/daily-rss?week', $assignedVariables['page_url']);
634 static::assertFalse($assignedVariables['hide_timestamps']);
635 static::assertCount(2, $assignedVariables['days']);
636
637 $day = $assignedVariables['days'][$dates[0]->format('YW')];
638 $date = $expectedDates[0];
639
640 static::assertEquals($date, $day['date']);
641 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
642 static::assertSame('Week 21 (May 18, 2020)', $day['date_human']);
643 static::assertSame('http://shaarli/subfolder/daily?week='. $dates[0]->format('YW'), $day['absolute_url']);
644 static::assertCount(1, $day['links']);
645
646 $day = $assignedVariables['days'][$dates[1]->format('YW')];
647 $date = $expectedDates[1];
648
649 static::assertEquals($date, $day['date']);
650 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
651 static::assertSame('Week 20 (May 11, 2020)', $day['date_human']);
652 static::assertSame('http://shaarli/subfolder/daily?week='. $dates[1]->format('YW'), $day['absolute_url']);
653 static::assertCount(2, $day['links']);
654 }
655
656 /**
657 * Test simple display RSS with month parameter
658 */
659 public function testSimpleRssMonthly(): void
660 {
661 $dates = [
662 new \DateTimeImmutable('2020-05-19'),
663 new \DateTimeImmutable('2020-04-13'),
664 ];
665 $expectedDates = [
666 new \DateTimeImmutable('2020-05-31 23:59:59'),
667 new \DateTimeImmutable('2020-04-30 23:59:59'),
668 ];
669
670 $this->container->environment['QUERY_STRING'] = 'month';
671 $request = $this->createMock(Request::class);
672 $request->method('getQueryParam')->willReturnCallback(function (string $key): ?string {
673 return $key === 'month' ? '' : null;
674 });
675 $response = new Response();
676
677 $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([
678 (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
679 (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
680 (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
681 ]);
682
683 // Save RainTPL assigned variables
684 $assignedVariables = [];
685 $this->assignTemplateVars($assignedVariables);
686
687 $result = $this->controller->rss($request, $response);
688
689 static::assertSame(200, $result->getStatusCode());
690 static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
691 static::assertSame('dailyrss', (string) $result->getBody());
692 static::assertSame('Shaarli', $assignedVariables['title']);
693 static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
694 static::assertSame('http://shaarli/subfolder/daily-rss?month', $assignedVariables['page_url']);
695 static::assertFalse($assignedVariables['hide_timestamps']);
696 static::assertCount(2, $assignedVariables['days']);
697
698 $day = $assignedVariables['days'][$dates[0]->format('Ym')];
699 $date = $expectedDates[0];
700
701 static::assertEquals($date, $day['date']);
702 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
703 static::assertSame('May, 2020', $day['date_human']);
704 static::assertSame('http://shaarli/subfolder/daily?month='. $dates[0]->format('Ym'), $day['absolute_url']);
705 static::assertCount(1, $day['links']);
706
707 $day = $assignedVariables['days'][$dates[1]->format('Ym')];
708 $date = $expectedDates[1];
709
710 static::assertEquals($date, $day['date']);
711 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
712 static::assertSame('April, 2020', $day['date_human']);
713 static::assertSame('http://shaarli/subfolder/daily?month='. $dates[1]->format('Ym'), $day['absolute_url']);
714 static::assertCount(2, $day['links']);
715 }
478} 716}
diff --git a/tests/front/controller/visitor/InstallControllerTest.php b/tests/front/controller/visitor/InstallControllerTest.php
index 345ad544..2105ed77 100644
--- a/tests/front/controller/visitor/InstallControllerTest.php
+++ b/tests/front/controller/visitor/InstallControllerTest.php
@@ -79,6 +79,15 @@ class InstallControllerTest extends TestCase
79 static::assertIsArray($assignedVariables['languages']); 79 static::assertIsArray($assignedVariables['languages']);
80 static::assertSame('Automatic', $assignedVariables['languages']['auto']); 80 static::assertSame('Automatic', $assignedVariables['languages']['auto']);
81 static::assertSame('French', $assignedVariables['languages']['fr']); 81 static::assertSame('French', $assignedVariables['languages']['fr']);
82
83 static::assertSame(PHP_VERSION, $assignedVariables['php_version']);
84 static::assertArrayHasKey('php_has_reached_eol', $assignedVariables);
85 static::assertArrayHasKey('php_eol', $assignedVariables);
86 static::assertArrayHasKey('php_extensions', $assignedVariables);
87 static::assertArrayHasKey('permissions', $assignedVariables);
88 static::assertEmpty($assignedVariables['permissions']);
89
90 static::assertSame('Install Shaarli', $assignedVariables['pagetitle']);
82 } 91 }
83 92
84 /** 93 /**
diff --git a/tests/ApplicationUtilsTest.php b/tests/helper/ApplicationUtilsTest.php
index a232b351..654857b9 100644
--- a/tests/ApplicationUtilsTest.php
+++ b/tests/helper/ApplicationUtilsTest.php
@@ -1,7 +1,8 @@
1<?php 1<?php
2namespace Shaarli; 2namespace Shaarli\Helper;
3 3
4use Shaarli\Config\ConfigManager; 4use Shaarli\Config\ConfigManager;
5use Shaarli\FakeApplicationUtils;
5 6
6require_once 'tests/utils/FakeApplicationUtils.php'; 7require_once 'tests/utils/FakeApplicationUtils.php';
7 8
@@ -340,6 +341,35 @@ class ApplicationUtilsTest extends \Shaarli\TestCase
340 } 341 }
341 342
342 /** 343 /**
344 * Checks resource permissions in minimal mode.
345 */
346 public function testCheckCurrentResourcePermissionsErrorsMinimalMode(): void
347 {
348 $conf = new ConfigManager('');
349 $conf->set('resource.thumbnails_cache', 'null/cache');
350 $conf->set('resource.config', 'null/data/config.php');
351 $conf->set('resource.data_dir', 'null/data');
352 $conf->set('resource.datastore', 'null/data/store.php');
353 $conf->set('resource.ban_file', 'null/data/ipbans.php');
354 $conf->set('resource.log', 'null/data/log.txt');
355 $conf->set('resource.page_cache', 'null/pagecache');
356 $conf->set('resource.raintpl_tmp', 'null/tmp');
357 $conf->set('resource.raintpl_tpl', 'null/tpl');
358 $conf->set('resource.raintpl_theme', 'null/tpl/default');
359 $conf->set('resource.update_check', 'null/data/lastupdatecheck.txt');
360
361 static::assertSame(
362 [
363 '"null/tpl" directory is not readable',
364 '"null/tpl/default" directory is not readable',
365 '"null/tmp" directory is not readable',
366 '"null/tmp" directory is not writable'
367 ],
368 ApplicationUtils::checkResourcePermissions($conf, true)
369 );
370 }
371
372 /**
343 * Check update with 'dev' as curent version (master branch). 373 * Check update with 'dev' as curent version (master branch).
344 * It should always return false. 374 * It should always return false.
345 */ 375 */
@@ -349,4 +379,37 @@ class ApplicationUtilsTest extends \Shaarli\TestCase
349 ApplicationUtils::checkUpdate('dev', self::$testUpdateFile, 100, true, true) 379 ApplicationUtils::checkUpdate('dev', self::$testUpdateFile, 100, true, true)
350 ); 380 );
351 } 381 }
382
383 /**
384 * Basic test of getPhpExtensionsRequirement()
385 */
386 public function testGetPhpExtensionsRequirementSimple(): void
387 {
388 static::assertCount(8, ApplicationUtils::getPhpExtensionsRequirement());
389 static::assertSame([
390 'name' => 'json',
391 'required' => true,
392 'desc' => 'Configuration parsing',
393 'loaded' => true,
394 ], ApplicationUtils::getPhpExtensionsRequirement()[0]);
395 }
396
397 /**
398 * Test getPhpEol with a known version: 7.4 -> 2022
399 */
400 public function testGetKnownPhpEol(): void
401 {
402 static::assertSame('2022-11-28', ApplicationUtils::getPhpEol('7.4.7'));
403 }
404
405 /**
406 * Test getPhpEol with an unknown version: 7.4 -> 2022
407 */
408 public function testGetUnknownPhpEol(): void
409 {
410 static::assertSame(
411 (((int) (new \DateTime())->format('Y')) + 2) . (new \DateTime())->format('-m-d'),
412 ApplicationUtils::getPhpEol('7.51.34')
413 );
414 }
352} 415}
diff --git a/tests/helper/DailyPageHelperTest.php b/tests/helper/DailyPageHelperTest.php
new file mode 100644
index 00000000..5255b7b1
--- /dev/null
+++ b/tests/helper/DailyPageHelperTest.php
@@ -0,0 +1,262 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Helper;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\TestCase;
9use Slim\Http\Request;
10
11class DailyPageHelperTest extends TestCase
12{
13 /**
14 * @dataProvider getRequestedTypes
15 */
16 public function testExtractRequestedType(array $queryParams, string $expectedType): void
17 {
18 $request = $this->createMock(Request::class);
19 $request->method('getQueryParam')->willReturnCallback(function ($key) use ($queryParams): ?string {
20 return $queryParams[$key] ?? null;
21 });
22
23 $type = DailyPageHelper::extractRequestedType($request);
24
25 static::assertSame($type, $expectedType);
26 }
27
28 /**
29 * @dataProvider getRequestedDateTimes
30 */
31 public function testExtractRequestedDateTime(
32 string $type,
33 string $input,
34 ?Bookmark $bookmark,
35 \DateTimeInterface $expectedDateTime,
36 string $compareFormat = 'Ymd'
37 ): void {
38 $dateTime = DailyPageHelper::extractRequestedDateTime($type, $input, $bookmark);
39
40 static::assertSame($dateTime->format($compareFormat), $expectedDateTime->format($compareFormat));
41 }
42
43 public function testExtractRequestedDateTimeExceptionUnknownType(): void
44 {
45 $this->expectException(\Exception::class);
46 $this->expectExceptionMessage('Unsupported daily format type');
47
48 DailyPageHelper::extractRequestedDateTime('nope', null, null);
49 }
50
51 /**
52 * @dataProvider getFormatsByType
53 */
54 public function testGetFormatByType(string $type, string $expectedFormat): void
55 {
56 $format = DailyPageHelper::getFormatByType($type);
57
58 static::assertSame($expectedFormat, $format);
59 }
60
61 public function testGetFormatByTypeExceptionUnknownType(): void
62 {
63 $this->expectException(\Exception::class);
64 $this->expectExceptionMessage('Unsupported daily format type');
65
66 DailyPageHelper::getFormatByType('nope');
67 }
68
69 /**
70 * @dataProvider getStartDatesByType
71 */
72 public function testGetStartDatesByType(
73 string $type,
74 \DateTimeImmutable $dateTime,
75 \DateTimeInterface $expectedDateTime
76 ): void {
77 $startDateTime = DailyPageHelper::getStartDateTimeByType($type, $dateTime);
78
79 static::assertEquals($expectedDateTime, $startDateTime);
80 }
81
82 public function testGetStartDatesByTypeExceptionUnknownType(): void
83 {
84 $this->expectException(\Exception::class);
85 $this->expectExceptionMessage('Unsupported daily format type');
86
87 DailyPageHelper::getStartDateTimeByType('nope', new \DateTimeImmutable());
88 }
89
90 /**
91 * @dataProvider getEndDatesByType
92 */
93 public function testGetEndDatesByType(
94 string $type,
95 \DateTimeImmutable $dateTime,
96 \DateTimeInterface $expectedDateTime
97 ): void {
98 $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dateTime);
99
100 static::assertEquals($expectedDateTime, $endDateTime);
101 }
102
103 public function testGetEndDatesByTypeExceptionUnknownType(): void
104 {
105 $this->expectException(\Exception::class);
106 $this->expectExceptionMessage('Unsupported daily format type');
107
108 DailyPageHelper::getEndDateTimeByType('nope', new \DateTimeImmutable());
109 }
110
111 /**
112 * @dataProvider getDescriptionsByType
113 */
114 public function testGeDescriptionsByType(
115 string $type,
116 \DateTimeImmutable $dateTime,
117 string $expectedDescription
118 ): void {
119 $description = DailyPageHelper::getDescriptionByType($type, $dateTime);
120
121 static::assertEquals($expectedDescription, $description);
122 }
123
124 public function getDescriptionByTypeExceptionUnknownType(): void
125 {
126 $this->expectException(\Exception::class);
127 $this->expectExceptionMessage('Unsupported daily format type');
128
129 DailyPageHelper::getDescriptionByType('nope', new \DateTimeImmutable());
130 }
131
132 /**
133 * @dataProvider getRssLengthsByType
134 */
135 public function testGeRssLengthsByType(string $type): void {
136 $length = DailyPageHelper::getRssLengthByType($type);
137
138 static::assertIsInt($length);
139 }
140
141 public function testGeRssLengthsByTypeExceptionUnknownType(): void
142 {
143 $this->expectException(\Exception::class);
144 $this->expectExceptionMessage('Unsupported daily format type');
145
146 DailyPageHelper::getRssLengthByType('nope');
147 }
148
149 /**
150 * Data provider for testExtractRequestedType() test method.
151 */
152 public function getRequestedTypes(): array
153 {
154 return [
155 [['month' => null], DailyPageHelper::DAY],
156 [['month' => ''], DailyPageHelper::MONTH],
157 [['month' => 'content'], DailyPageHelper::MONTH],
158 [['week' => null], DailyPageHelper::DAY],
159 [['week' => ''], DailyPageHelper::WEEK],
160 [['week' => 'content'], DailyPageHelper::WEEK],
161 [['day' => null], DailyPageHelper::DAY],
162 [['day' => ''], DailyPageHelper::DAY],
163 [['day' => 'content'], DailyPageHelper::DAY],
164 ];
165 }
166
167 /**
168 * Data provider for testExtractRequestedDateTime() test method.
169 */
170 public function getRequestedDateTimes(): array
171 {
172 return [
173 [DailyPageHelper::DAY, '20201013', null, new \DateTime('2020-10-13')],
174 [
175 DailyPageHelper::DAY,
176 '',
177 (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')),
178 $date,
179 ],
180 [DailyPageHelper::DAY, '', null, new \DateTime()],
181 [DailyPageHelper::WEEK, '202030', null, new \DateTime('2020-07-20')],
182 [
183 DailyPageHelper::WEEK,
184 '',
185 (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')),
186 new \DateTime('2020-10-13'),
187 ],
188 [DailyPageHelper::WEEK, '', null, new \DateTime(), 'Ym'],
189 [DailyPageHelper::MONTH, '202008', null, new \DateTime('2020-08-01'), 'Ym'],
190 [
191 DailyPageHelper::MONTH,
192 '',
193 (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')),
194 new \DateTime('2020-10-13'),
195 'Ym'
196 ],
197 [DailyPageHelper::MONTH, '', null, new \DateTime(), 'Ym'],
198 ];
199 }
200
201 /**
202 * Data provider for testGetFormatByType() test method.
203 */
204 public function getFormatsByType(): array
205 {
206 return [
207 [DailyPageHelper::DAY, 'Ymd'],
208 [DailyPageHelper::WEEK, 'YW'],
209 [DailyPageHelper::MONTH, 'Ym'],
210 ];
211 }
212
213 /**
214 * Data provider for testGetStartDatesByType() test method.
215 */
216 public function getStartDatesByType(): array
217 {
218 return [
219 [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')],
220 [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')],
221 [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')],
222 ];
223 }
224
225 /**
226 * Data provider for testGetEndDatesByType() test method.
227 */
228 public function getEndDatesByType(): array
229 {
230 return [
231 [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')],
232 [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')],
233 [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')],
234 ];
235 }
236
237 /**
238 * Data provider for testGetDescriptionsByType() test method.
239 */
240 public function getDescriptionsByType(): array
241 {
242 return [
243 [DailyPageHelper::DAY, $date = new \DateTimeImmutable(), 'Today - ' . $date->format('F j, Y')],
244 [DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F j, Y')],
245 [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'],
246 [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'],
247 [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'],
248 ];
249 }
250
251 /**
252 * Data provider for testGetDescriptionsByType() test method.
253 */
254 public function getRssLengthsByType(): array
255 {
256 return [
257 [DailyPageHelper::DAY],
258 [DailyPageHelper::WEEK],
259 [DailyPageHelper::MONTH],
260 ];
261 }
262}
diff --git a/tests/FileUtilsTest.php b/tests/helper/FileUtilsTest.php
index 9163bdf1..8035f79c 100644
--- a/tests/FileUtilsTest.php
+++ b/tests/helper/FileUtilsTest.php
@@ -1,27 +1,51 @@
1<?php 1<?php
2 2
3namespace Shaarli; 3namespace Shaarli\Helper;
4 4
5use Exception; 5use Exception;
6use Shaarli\Exceptions\IOException;
7use Shaarli\TestCase;
6 8
7/** 9/**
8 * Class FileUtilsTest 10 * Class FileUtilsTest
9 * 11 *
10 * Test file utility class. 12 * Test file utility class.
11 */ 13 */
12class FileUtilsTest extends \Shaarli\TestCase 14class FileUtilsTest extends TestCase
13{ 15{
14 /** 16 /**
15 * @var string Test file path. 17 * @var string Test file path.
16 */ 18 */
17 protected static $file = 'sandbox/flat.db'; 19 protected static $file = 'sandbox/flat.db';
18 20
21 protected function setUp(): void
22 {
23 @mkdir('sandbox');
24 mkdir('sandbox/folder2');
25 touch('sandbox/file1');
26 touch('sandbox/file2');
27 mkdir('sandbox/folder1');
28 touch('sandbox/folder1/file1');
29 touch('sandbox/folder1/file2');
30 mkdir('sandbox/folder3');
31 mkdir('/tmp/shaarli-to-delete');
32 }
33
19 /** 34 /**
20 * Delete test file after every test. 35 * Delete test file after every test.
21 */ 36 */
22 protected function tearDown(): void 37 protected function tearDown(): void
23 { 38 {
24 @unlink(self::$file); 39 @unlink(self::$file);
40
41 @unlink('sandbox/folder1/file1');
42 @unlink('sandbox/folder1/file2');
43 @rmdir('sandbox/folder1');
44 @unlink('sandbox/file1');
45 @unlink('sandbox/file2');
46 @rmdir('sandbox/folder2');
47 @rmdir('sandbox/folder3');
48 @rmdir('/tmp/shaarli-to-delete');
25 } 49 }
26 50
27 /** 51 /**
@@ -107,4 +131,67 @@ class FileUtilsTest extends \Shaarli\TestCase
107 $this->assertEquals(null, FileUtils::readFlatDB(self::$file)); 131 $this->assertEquals(null, FileUtils::readFlatDB(self::$file));
108 $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test'])); 132 $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test']));
109 } 133 }
134
135 /**
136 * Test clearFolder with self delete and excluded files
137 */
138 public function testClearFolderSelfDeleteWithExclusion(): void
139 {
140 FileUtils::clearFolder('sandbox', true, ['file2']);
141
142 static::assertFileExists('sandbox/folder1/file2');
143 static::assertFileExists('sandbox/folder1');
144 static::assertFileExists('sandbox/file2');
145 static::assertFileExists('sandbox');
146
147 static::assertFileNotExists('sandbox/folder1/file1');
148 static::assertFileNotExists('sandbox/file1');
149 static::assertFileNotExists('sandbox/folder3');
150 }
151
152 /**
153 * Test clearFolder with self delete and excluded files
154 */
155 public function testClearFolderSelfDeleteWithoutExclusion(): void
156 {
157 FileUtils::clearFolder('sandbox', true);
158
159 static::assertFileNotExists('sandbox');
160 }
161
162 /**
163 * Test clearFolder with self delete and excluded files
164 */
165 public function testClearFolderNoSelfDeleteWithoutExclusion(): void
166 {
167 FileUtils::clearFolder('sandbox', false);
168
169 static::assertFileExists('sandbox');
170
171 // 2 because '.' and '..'
172 static::assertCount(2, new \DirectoryIterator('sandbox'));
173 }
174
175 /**
176 * Test clearFolder on a file instead of a folder
177 */
178 public function testClearFolderOnANonDirectory(): void
179 {
180 $this->expectException(IOException::class);
181 $this->expectExceptionMessage('Provided path is not a directory.');
182
183 FileUtils::clearFolder('sandbox/file1', false);
184 }
185
186 /**
187 * Test clearFolder on a file instead of a folder
188 */
189 public function testClearFolderOutsideOfShaarliDirectory(): void
190 {
191 $this->expectException(IOException::class);
192 $this->expectExceptionMessage('Trying to delete a folder outside of Shaarli path.');
193
194
195 FileUtils::clearFolder('/tmp/shaarli-to-delete', true);
196 }
110} 197}
diff --git a/tests/security/BanManagerTest.php b/tests/security/BanManagerTest.php
index 22aa8666..29d2791b 100644
--- a/tests/security/BanManagerTest.php
+++ b/tests/security/BanManagerTest.php
@@ -4,7 +4,7 @@
4namespace Shaarli\Security; 4namespace Shaarli\Security;
5 5
6use Psr\Log\LoggerInterface; 6use Psr\Log\LoggerInterface;
7use Shaarli\FileUtils; 7use Shaarli\Helper\FileUtils;
8use Shaarli\TestCase; 8use Shaarli\TestCase;
9 9
10/** 10/**
diff --git a/tests/utils/FakeApplicationUtils.php b/tests/utils/FakeApplicationUtils.php
index de83d598..d5289ede 100644
--- a/tests/utils/FakeApplicationUtils.php
+++ b/tests/utils/FakeApplicationUtils.php
@@ -2,6 +2,8 @@
2 2
3namespace Shaarli; 3namespace Shaarli;
4 4
5use Shaarli\Helper\ApplicationUtils;
6
5/** 7/**
6 * Fake ApplicationUtils class to avoid HTTP requests 8 * Fake ApplicationUtils class to avoid HTTP requests
7 */ 9 */
diff --git a/tests/utils/ReferenceHistory.php b/tests/utils/ReferenceHistory.php
index 516c9f51..aed5d2cf 100644
--- a/tests/utils/ReferenceHistory.php
+++ b/tests/utils/ReferenceHistory.php
@@ -1,6 +1,6 @@
1<?php 1<?php
2 2
3use Shaarli\FileUtils; 3use Shaarli\Helper\FileUtils;
4use Shaarli\History; 4use Shaarli\History;
5 5
6/** 6/**
diff --git a/tpl/default/addlink.html b/tpl/default/addlink.html
index 67d3ebd1..4aac7ff1 100644
--- a/tpl/default/addlink.html
+++ b/tpl/default/addlink.html
@@ -20,6 +20,62 @@
20 </form> 20 </form>
21 </div> 21 </div>
22</div> 22</div>
23
24<div class="pure-g addlink-batch-show-more-block pure-u-0">
25 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
26 <div class="pure-u-lg-1-3 pure-u-22-24 addlink-batch-show-more">
27 <a href="#">{'BULK CREATION'|t}&nbsp;<i class="fa fa-plus-circle" aria-hidden="true"></i></a>
28 </div>
29</div>
30
31<div class="addlink-batch-form-block">
32 {if="empty($async_metadata)"}
33 <div class="pure-g pure-alert pure-alert-warning pure-alert-closable">
34 <div class="pure-u-2-24"></div>
35 <div class="pure-u-20-24">
36 <p>
37 {'Metadata asynchronous retrieval is disabled.'|t}
38 {'We recommend that you enable the setting <em>general > enable_async_metadata</em> in your configuration file to use bulk link creation.'|t}
39 </p>
40 </div>
41 <div class="pure-u-2-24">
42 <i class="fa fa-times pure-alert-close"></i>
43 </div>
44 </div>
45 {/if}
46
47 <div class="pure-g">
48 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
49 <div id="batch-addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24">
50 <h2 class="window-title">{"Shaare multiple new links"|t}</h2>
51 <form method="POST" action="{$base_path}/admin/shaare-batch" name="batch-addform" class="batch-addform">
52 <div>
53 <label for="urls">{'Add one URL per line to create multiple bookmarks.'|t}</label>
54 <textarea name="urls" id="urls"></textarea>
55
56 <div>
57 <label for="tags">{'Tags'|t}</label>
58 </div>
59 <div>
60 <input type="text" name="tags" id="tags" class="lf_input"
61 data-list="{loop="$tags"}{$key}, {/loop}" data-multiple data-autofirst autocomplete="off">
62 </div>
63
64 <div>
65 <input type="hidden" name="private" value="0">
66 <input type="checkbox" name="private" {if="$default_private_links"} checked="checked"{/if}>
67 &nbsp; <label for="lf_private">{'Private'|t}</label>
68 </div>
69 </div>
70 <div>
71 <input type="hidden" name="token" value="{$token}">
72 <input type="submit" value="{'Add links'|t}">
73 </div>
74 </form>
75 </div>
76 </div>
77</div>
78
23{include="page.footer"} 79{include="page.footer"}
24</body> 80</body>
25</html> 81</html>
diff --git a/tpl/default/daily.html b/tpl/default/daily.html
index 3749bffb..5e038c39 100644
--- a/tpl/default/daily.html
+++ b/tpl/default/daily.html
@@ -7,11 +7,24 @@
7{include="page.header"} 7{include="page.header"}
8 8
9<div class="pure-g"> 9<div class="pure-g">
10 <div class="pure-u-1 pure-alert pure-alert-success tag-sort">
11 <a href="{$base_path}/daily?day">{'Daily'|t}</a>
12 <a href="{$base_path}/daily?week">{'Weekly'|t}</a>
13 <a href="{$base_path}/daily?month">{'Monthly'|t}</a>
14 </div>
15</div>
16
17
18<div class="pure-g">
10 <div class="pure-u-lg-1-6 pure-u-1-24"></div> 19 <div class="pure-u-lg-1-6 pure-u-1-24"></div>
11 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor" id="daily"> 20 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor" id="daily">
12 <h2 class="window-title"> 21 <h2 class="window-title">
13 {'The Daily Shaarli'|t} 22 {$localizedType} Shaarli
14 <a href="{$base_path}/daily-rss" title="{'1 RSS entry per day'|t}"><i class="fa fa-rss"></i></a> 23 <a href="{$base_path}/daily-rss?{$type}"
24 title="{function="t('1 RSS entry per :type', '', 1, 'shaarli', [':type' => t($type)])"}"
25 >
26 <i class="fa fa-rss"></i>
27 </a>
15 </h2> 28 </h2>
16 29
17 <div id="plugin_zone_start_daily" class="plugin_zone"> 30 <div id="plugin_zone_start_daily" class="plugin_zone">
@@ -25,19 +38,19 @@
25 <div class="pure-g"> 38 <div class="pure-g">
26 <div class="pure-u-lg-1-3 pure-u-1 center"> 39 <div class="pure-u-lg-1-3 pure-u-1 center">
27 {if="$previousday"} 40 {if="$previousday"}
28 <a href="{$base_path}/daily?day={$previousday}"> 41 <a href="{$base_path}/daily?{$type}={$previousday}">
29 <i class="fa fa-arrow-left"></i> 42 <i class="fa fa-arrow-left"></i>
30 {'Previous day'|t} 43 {function="t('Previous :type', '', 1, 'shaarli', [':type' => t($type)], true)"}
31 </a> 44 </a>
32 {/if} 45 {/if}
33 </div> 46 </div>
34 <div class="daily-desc pure-u-lg-1-3 pure-u-1 center"> 47 <div class="daily-desc pure-u-lg-1-3 pure-u-1 center">
35 {'All links of one day in a single page.'|t} 48 {function="t('All links of one :type in a single page.', '', 1, 'shaarli', [':type' => t($type)])"}
36 </div> 49 </div>
37 <div class="pure-u-lg-1-3 pure-u-1 center"> 50 <div class="pure-u-lg-1-3 pure-u-1 center">
38 {if="$nextday"} 51 {if="$nextday"}
39 <a href="{$base_path}/daily?day={$nextday}"> 52 <a href="{$base_path}/daily?{$type}={$nextday}">
40 {'Next day'|t} 53 {function="t('Next :type', '', 1, 'shaarli', [':type' => t($type)], true)"}
41 <i class="fa fa-arrow-right"></i> 54 <i class="fa fa-arrow-right"></i>
42 </a> 55 </a>
43 {/if} 56 {/if}
@@ -45,10 +58,7 @@
45 </div> 58 </div>
46 <div> 59 <div>
47 <h3 class="window-subtitle"> 60 <h3 class="window-subtitle">
48 {if="!empty($dayDesc)"} 61 {$dayDesc}
49 {$dayDesc} -
50 {/if}
51 {function="format_date($dayDate, false)"}
52 </h3> 62 </h3>
53 63
54 <div id="plugin_zone_about_daily" class="plugin_zone"> 64 <div id="plugin_zone_about_daily" class="plugin_zone">
diff --git a/tpl/default/dailyrss.html b/tpl/default/dailyrss.html
index d40d9496..871a3ba7 100644
--- a/tpl/default/dailyrss.html
+++ b/tpl/default/dailyrss.html
@@ -1,9 +1,9 @@
1<?xml version="1.0" encoding="UTF-8"?> 1<?xml version="1.0" encoding="UTF-8"?>
2<rss version="2.0"> 2<rss version="2.0">
3 <channel> 3 <channel>
4 <title>Daily - {$title}</title> 4 <title>{$localizedType} - {$title}</title>
5 <link>{$index_url}</link> 5 <link>{$index_url}</link>
6 <description>Daily shaared bookmarks</description> 6 <description>{function="t('All links of one :type in a single page.', '', 1, 'shaarli', [':type' => t($type)])"}</description>
7 <language>{$language}</language> 7 <language>{$language}</language>
8 <copyright>{$index_url}</copyright> 8 <copyright>{$index_url}</copyright>
9 <generator>Shaarli</generator> 9 <generator>Shaarli</generator>
@@ -18,12 +18,15 @@
18 {loop="$value.links"} 18 {loop="$value.links"}
19 <h3><a href="{$value.url}">{$value.title}</a></h3> 19 <h3><a href="{$value.url}">{$value.title}</a></h3>
20 <small> 20 <small>
21 {if="!$hide_timestamps"}{$value.created|format_date} - {/if}{if="$value.tags"}{$value.tags}{/if}<br> 21 {if="!$hide_timestamps"}{$value.created|format_date} &#8212; {/if}
22 <a href="{$index_url}shaare/{$value.shorturl}">{'Permalink'|t}</a>
23 {if="$value.tags"} &#8212; {$value.tags}{/if}
24 <br>
22 {$value.url} 25 {$value.url}
23 </small><br> 26 </small><br>
24 {if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br> 27 {if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br>
25 {if="$value.description"}{$value.description}{/if} 28 {if="$value.description"}{$value.description}{/if}
26 <br><br><hr> 29 <br><hr>
27 {/loop} 30 {/loop}
28 ]]></description> 31 ]]></description>
29 </item> 32 </item>
diff --git a/tpl/default/editlink.batch.html b/tpl/default/editlink.batch.html
new file mode 100644
index 00000000..b1f8e5bd
--- /dev/null
+++ b/tpl/default/editlink.batch.html
@@ -0,0 +1,32 @@
1<!DOCTYPE html>
2<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
3<head>
4 {include="includes"}
5</head>
6<body>
7<div class="dark-layer">
8 <div class="screen-center">
9 <div><span class="progressbar-current"></span> / <span class="progressbar-max"></span></div>
10 <div class="progressbar">
11 <div></div>
12 </div>
13 </div>
14</div>
15
16{include="page.header"}
17
18<div class="center">
19 <input type="submit" name="save_edit_batch" class="pure-button-shaarli" value="{'Save all'|t}">
20</div>
21
22{loop="$links"}
23 {include="editlink"}
24{/loop}
25
26<div class="center">
27 <input type="submit" name="save_edit_batch" class="pure-button-shaarli" value="{'Save all'|t}">
28</div>
29
30{include="page.footer"}
31{if="$async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if}
32<script src="{$asset_path}/js/shaare_batch.min.js?v={$version_hash}#"></script>
diff --git a/tpl/default/editlink.html b/tpl/default/editlink.html
index 7ab7e1fe..83e541fd 100644
--- a/tpl/default/editlink.html
+++ b/tpl/default/editlink.html
@@ -1,3 +1,4 @@
1{if="empty($batch_mode)"}
1<!DOCTYPE html> 2<!DOCTYPE html>
2<html{if="$language !== 'auto'"} lang="{$language}"{/if}> 3<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
3<head> 4<head>
@@ -5,6 +6,10 @@
5</head> 6</head>
6<body> 7<body>
7 {include="page.header"} 8 {include="page.header"}
9{else}
10 {ignore}Lil hack: when included in a loop in batch mode, `$value` is assigned by RainTPL with template vars.{/ignore}
11 {function="extract($value) ? '' : ''"}
12{/if}
8 <div id="editlinkform" class="edit-link-container" class="pure-g"> 13 <div id="editlinkform" class="edit-link-container" class="pure-g">
9 <div class="pure-u-lg-1-5 pure-u-1-24"></div> 14 <div class="pure-u-lg-1-5 pure-u-1-24"></div>
10 <form method="post" 15 <form method="post"
@@ -60,7 +65,7 @@
60 65
61 <div> 66 <div>
62 <input type="checkbox" name="lf_private" id="lf_private" 67 <input type="checkbox" name="lf_private" id="lf_private"
63 {if="($link_is_new && $default_private_links || $link.private == true)"} 68 {if="$link.private === true"}
64 checked="checked" 69 checked="checked"
65 {/if}> 70 {/if}>
66 &nbsp;<label for="lf_private">{'Private'|t}</label> 71 &nbsp;<label for="lf_private">{'Private'|t}</label>
@@ -83,6 +88,13 @@
83 88
84 89
85 <div class="submit-buttons center"> 90 <div class="submit-buttons center">
91 {if="!empty($batch_mode)"}
92 <a href="#" class="button button-grey" name="cancel-batch-link"
93 title="{'Remove this bookmark from batch creation/modification.'}"
94 >
95 {'Cancel'|t}
96 </a>
97 {/if}
86 <input type="submit" name="save_edit" class="" id="button-save-edit" 98 <input type="submit" name="save_edit" class="" id="button-save-edit"
87 value="{if="$link_is_new"}{'Save'|t}{else}{'Apply Changes'|t}{/if}"> 99 value="{if="$link_is_new"}{'Save'|t}{else}{'Apply Changes'|t}{/if}">
88 {if="!$link_is_new"} 100 {if="!$link_is_new"}
@@ -100,7 +112,10 @@
100 {/if} 112 {/if}
101 </form> 113 </form>
102 </div> 114 </div>
115
116{if="empty($batch_mode)"}
103 {include="page.footer"} 117 {include="page.footer"}
104 {if="$link_is_new && $async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if} 118 {if="$link_is_new && $async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if}
105</body> 119</body>
106</html> 120</html>
121{/if}
diff --git a/tpl/default/install.html b/tpl/default/install.html
index a506a2eb..4f98d49d 100644
--- a/tpl/default/install.html
+++ b/tpl/default/install.html
@@ -163,6 +163,16 @@
163 </div> 163 </div>
164</div> 164</div>
165</form> 165</form>
166
167<div class="pure-g">
168 <div class="pure-u-lg-1-6 pure-u-1-24"></div>
169 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-form-complete">
170 <h2 class="window-title">{'Server requirements'|t}</h2>
171
172 {include="server.requirements"}
173 </div>
174</div>
175
166{include="page.footer"} 176{include="page.footer"}
167</body> 177</body>
168</html> 178</html>
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html
index 48cd9aad..e1115d49 100644
--- a/tpl/default/linklist.html
+++ b/tpl/default/linklist.html
@@ -129,6 +129,7 @@
129 {$strAddTag=t('Add tag')} 129 {$strAddTag=t('Add tag')}
130 {$strToggleSticky=t('Toggle sticky')} 130 {$strToggleSticky=t('Toggle sticky')}
131 {$strSticky=t('Sticky')} 131 {$strSticky=t('Sticky')}
132 {$strShaarePrivate=t('Share a private link')}
132 {ignore}End of translations{/ignore} 133 {ignore}End of translations{/ignore}
133 {loop="links"} 134 {loop="links"}
134 <div class="anchor" id="{$value.shorturl}"></div> 135 <div class="anchor" id="{$value.shorturl}"></div>
@@ -241,6 +242,12 @@
241 {$strPermalinkLc} 242 {$strPermalinkLc}
242 </a> 243 </a>
243 244
245 {if="$is_logged_in && $value.private"}
246 <a href="{$base_path}/admin/shaare/private/{$value.shorturl}?token={$token}" title="{$strShaarePrivate}">
247 <i class="fa fa-share-alt"></i>
248 </a>
249 {/if}
250
244 <div class="pure-u-0 pure-u-lg-visible"> 251 <div class="pure-u-0 pure-u-lg-visible">
245 {if="isset($value.link_plugin)"} 252 {if="isset($value.link_plugin)"}
246 &middot; 253 &middot;
diff --git a/tpl/default/server.html b/tpl/default/server.html
new file mode 100644
index 00000000..de1c8b53
--- /dev/null
+++ b/tpl/default/server.html
@@ -0,0 +1,129 @@
1<!DOCTYPE html>
2<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
3<head>
4 {include="includes"}
5</head>
6<body>
7{include="page.header"}
8
9<div class="pure-g">
10 <div class="pure-u-lg-1-4 pure-u-1-24"></div>
11 <div class="pure-u-lg-1-2 pure-u-22-24 page-form server-tables-page">
12 <h2 class="window-title">{'Server administration'|t}</h2>
13
14 <h3 class="window-subtitle">{'General'|t}</h3>
15
16 <div class="pure-g server-row">
17 <div class="pure-u-lg-1-2 pure-u-1 server-label">
18 <p>{'Index URL'|t}</p>
19 </div>
20 <div class="pure-u-lg-1-2 pure-u-1">
21 <p><a href="{$index_url}" title="{$pagetitle}">{$index_url}</a></p>
22 </div>
23 </div>
24 <div class="pure-g server-row">
25 <div class="pure-u-lg-1-2 pure-u-1 server-label">
26 <p>{'Base path'|t}</p>
27 </div>
28 <div class="pure-u-lg-1-2 pure-u-1">
29 <p>{$base_path}</p>
30 </div>
31 </div>
32 <div class="pure-g server-row">
33 <div class="pure-u-lg-1-2 pure-u-1 server-label">
34 <p>{'Client IP'|t}</p>
35 </div>
36 <div class="pure-u-lg-1-2 pure-u-1">
37 <p>{$client_ip}</p>
38 </div>
39 </div>
40 <div class="pure-g server-row">
41 <div class="pure-u-lg-1-2 pure-u-1 server-label">
42 <p>{'Trusted reverse proxies'|t}</p>
43 </div>
44 <div class="pure-u-lg-1-2 pure-u-1">
45 {if="count($trusted_proxies) > 0"}
46 <p>
47 {loop="$trusted_proxies"}
48 {$value}<br>
49 {/loop}
50 </p>
51 {else}
52 <p>{'N/A'|t}</p>
53 {/if}
54 </div>
55 </div>
56
57 {include="server.requirements"}
58
59 <h3 class="window-subtitle">Version</h3>
60
61 <div class="pure-g server-row">
62 <div class="pure-u-lg-1-2 pure-u-1 server-label">
63 <p>Current version</p>
64 </div>
65 <div class="pure-u-lg-1-2 pure-u-1">
66 <p>{$current_version}</p>
67 </div>
68 </div>
69
70 <div class="pure-g server-row">
71 <div class="pure-u-lg-1-2 pure-u-1 server-label">
72 <p>Latest release</p>
73 </div>
74 <div class="pure-u-lg-1-2 pure-u-1">
75 <p>
76 <a href="{$release_url}" title="{'Visit releases page on Github'|t}">
77 {$latest_version}
78 </a>
79 </p>
80 </div>
81 </div>
82
83 <h3 class="window-subtitle">Thumbnails</h3>
84
85 <div class="pure-g server-row">
86 <div class="pure-u-lg-1-2 pure-u-1 server-label">
87 <p>Thumbnails status</p>
88 </div>
89 <div class="pure-u-lg-1-2 pure-u-1">
90 <p>
91 {if="$thumbnails_mode==='all'"}
92 {'All'|t}
93 {elseif="$thumbnails_mode==='common'"}
94 {'Only common media hosts'|t}
95 {else}
96 {'None'|t}
97 {/if}
98 </p>
99 </div>
100 </div>
101
102 {if="$thumbnails_mode!=='none'"}
103 <div class="center tools-item">
104 <a href="{$base_path}/admin/thumbnails" title="{'Synchronize all link thumbnails'|t}">
105 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Synchronize thumbnails'|t}</span>
106 </a>
107 </div>
108 {/if}
109
110 <h3 class="window-subtitle">Cache</h3>
111
112 <div class="center tools-item">
113 <a href="{$base_path}/admin/clear-cache?type=main">
114 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">Clear main cache</span>
115 </a>
116 </div>
117
118 <div class="center tools-item">
119 <a href="{$base_path}/admin/clear-cache?type=thumbnails">
120 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">Clear thumbnails cache</span>
121 </a>
122 </div>
123 </div>
124</div>
125
126{include="page.footer"}
127
128</body>
129</html>
diff --git a/tpl/default/server.requirements.html b/tpl/default/server.requirements.html
new file mode 100644
index 00000000..85def9b7
--- /dev/null
+++ b/tpl/default/server.requirements.html
@@ -0,0 +1,68 @@
1<div class="server-tables">
2 <h3 class="window-subtitle">{'Permissions'|t}</h3>
3
4 {if="count($permissions) > 0"}
5 <p class="center">
6 <i class="fa fa-close fa-color-red" aria-hidden="true"></i>
7 {'There are permissions that need to be fixed.'|t}
8 </p>
9
10 <p>
11 {loop="$permissions"}
12 <div class="center">{$value}</div>
13 {/loop}
14 </p>
15 {else}
16 <p class="center">
17 <i class="fa fa-check fa-color-green" aria-hidden="true"></i>
18 {'All read/write permissions are properly set.'|t}
19 </p>
20 {/if}
21
22 <h3 class="window-subtitle">PHP</h3>
23
24 <p class="center">
25 <strong>{'Running PHP'|t} {$php_version}</strong>
26 {if="$php_has_reached_eol"}
27 <i class="fa fa-circle fa-color-orange" aria-label="hidden"></i><br>
28 {'End of life: '|t} {$php_eol}
29 {else}
30 <i class="fa fa-circle fa-color-green" aria-label="hidden"></i><br>
31 {/if}
32 </p>
33
34 <table class="center">
35 <thead>
36 <tr>
37 <th>{'Extension'|t}</th>
38 <th>{'Usage'|t}</th>
39 <th>{'Status'|t}</th>
40 <th>{'Loaded'|t}</th>
41 </tr>
42 </thead>
43 <tbody>
44 {loop="$php_extensions"}
45 <tr>
46 <td>{$value.name}</td>
47 <td>{$value.desc}</td>
48 <td>{$value.required ? t('Required') : t('Optional')}</td>
49 <td>
50 {if="$value.loaded"}
51 {$classLoaded="fa-color-green"}
52 {$strLoaded=t('Loaded')}
53 {else}
54 {$strLoaded=t('Not loaded')}
55 {if="$value.required"}
56 {$classLoaded="fa-color-red"}
57 {else}
58 {$classLoaded="fa-color-orange"}
59 {/if}
60 {/if}
61
62 <i class="fa fa-circle {$classLoaded}" aria-label="{$strLoaded}" title="{$strLoaded}"></i>
63 </td>
64 </tr>
65 {/loop}
66 </tbody>
67 </table>
68</div>
diff --git a/tpl/default/tools.html b/tpl/default/tools.html
index 2cb08e38..2df73598 100644
--- a/tpl/default/tools.html
+++ b/tpl/default/tools.html
@@ -20,6 +20,12 @@
20 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Plugin administration'|t}</span> 20 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Plugin administration'|t}</span>
21 </a> 21 </a>
22 </div> 22 </div>
23 <div class="tools-item">
24 <a href="{$base_path}/admin/server"
25 title="{'Check instance\'s server configuration'|t}">
26 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Server administration'|t}</span>
27 </a>
28 </div>
23 {if="!$openshaarli"} 29 {if="!$openshaarli"}
24 <div class="tools-item"> 30 <div class="tools-item">
25 <a href="{$base_path}/admin/password" title="{'Change your password'|t}"> 31 <a href="{$base_path}/admin/password" title="{'Change your password'|t}">
@@ -45,14 +51,6 @@
45 </a> 51 </a>
46 </div> 52 </div>
47 53
48 {if="$thumbnails_enabled"}
49 <div class="tools-item">
50 <a href="{$base_path}/admin/thumbnails" title="{'Synchronize all link thumbnails'|t}">
51 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Synchronize thumbnails'|t}</span>
52 </a>
53 </div>
54 {/if}
55
56 {loop="$tools_plugin"} 54 {loop="$tools_plugin"}
57 <div class="tools-item"> 55 <div class="tools-item">
58 {$value} 56 {$value}
diff --git a/webpack.config.js b/webpack.config.js
index 8e3d1470..a4aa633e 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -18,6 +18,7 @@ module.exports = [
18 { 18 {
19 mode: 'production', 19 mode: 'production',
20 entry: { 20 entry: {
21 shaare_batch: './assets/common/js/shaare-batch.js',
21 thumbnails: './assets/common/js/thumbnails.js', 22 thumbnails: './assets/common/js/thumbnails.js',
22 thumbnails_update: './assets/common/js/thumbnails-update.js', 23 thumbnails_update: './assets/common/js/thumbnails-update.js',
23 metadata: './assets/common/js/metadata.js', 24 metadata: './assets/common/js/metadata.js',