aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/front/controller/admin/ManageShaareController.php386
-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/render/TemplatePage.php1
-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.scss72
-rw-r--r--inc/languages/fr/LC_MESSAGES/shaarli.po109
-rw-r--r--index.php17
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php47
-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.php (renamed from tests/front/controller/admin/ManageShaareControllerTest/SharePrivateTest.php)8
-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--tpl/default/addlink.html56
-rw-r--r--tpl/default/editlink.batch.html32
-rw-r--r--tpl/default/editlink.html17
-rw-r--r--webpack.config.js1
25 files changed, 1125 insertions, 528 deletions
diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php
deleted file mode 100644
index e490f85a..00000000
--- a/application/front/controller/admin/ManageShaareController.php
+++ /dev/null
@@ -1,386 +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 * GET /admin/shaare/private/{hash} - Attach a private key to given bookmark, then redirect to the sharing URL.
325 */
326 public function sharePrivate(Request $request, Response $response, array $args): Response
327 {
328 $this->checkToken($request);
329
330 $hash = $args['hash'] ?? '';
331 $bookmark = $this->container->bookmarkService->findByHash($hash);
332
333 if ($bookmark->isPrivate() !== true) {
334 return $this->redirect($response, '/shaare/' . $hash);
335 }
336
337 if (empty($bookmark->getAdditionalContentEntry('private_key'))) {
338 $privateKey = bin2hex(random_bytes(16));
339 $bookmark->addAdditionalContentEntry('private_key', $privateKey);
340 $this->container->bookmarkService->set($bookmark);
341 }
342
343 return $this->redirect(
344 $response,
345 '/shaare/' . $hash . '?key=' . $bookmark->getAdditionalContentEntry('private_key')
346 );
347 }
348
349 /**
350 * Helper function used to display the shaare form whether it's a new or existing bookmark.
351 *
352 * @param array $link data used in template, either from parameters or from the data store
353 */
354 protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response
355 {
356 $tags = $this->container->bookmarkService->bookmarksCountPerTag();
357 if ($this->container->conf->get('formatter') === 'markdown') {
358 $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
359 }
360
361 $data = escape([
362 'link' => $link,
363 'link_is_new' => $isNew,
364 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '',
365 'source' => $request->getParam('source') ?? '',
366 'tags' => $tags,
367 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false),
368 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true),
369 'retrieve_description' => $this->container->conf->get('general.retrieve_description', false),
370 ]);
371
372 $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
373
374 foreach ($data as $key => $value) {
375 $this->assignView($key, $value);
376 }
377
378 $editLabel = false === $isNew ? t('Edit') .' ' : '';
379 $this->assignView(
380 'pagetitle',
381 $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli')
382 );
383
384 return $response->write($this->render(TemplatePage::EDIT_LINK));
385 }
386}
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..ddcffdc7
--- /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'] = escape($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/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/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 7dc61903..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 {
@@ -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 }
@@ -1750,6 +1759,69 @@ form {
1750 } 1759 }
1751} 1760}
1752 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
1753// Print rules 1825// Print rules
1754@media print { 1826@media print {
1755 .shaarli-menu { 1827 .shaarli-menu {
diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po
index 6d4ff0bd..60ea7a97 100644
--- a/inc/languages/fr/LC_MESSAGES/shaarli.po
+++ b/inc/languages/fr/LC_MESSAGES/shaarli.po
@@ -347,43 +347,16 @@ msgstr ""
347"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 "
348"légères." 348"légères."
349 349
350#: application/front/controller/admin/ManageShaareController.php:29
351#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
352msgid "Shaare a new link"
353msgstr "Partager un nouveau lien"
354
355#: application/front/controller/admin/ManageShaareController.php:64 350#: application/front/controller/admin/ManageShaareController.php:64
356msgid "Note: "
357msgstr "Note : "
358
359#: application/front/controller/admin/ManageShaareController.php:95 351#: application/front/controller/admin/ManageShaareController.php:95
360#: application/front/controller/admin/ManageShaareController.php:193 352#: application/front/controller/admin/ManageShaareController.php:193
361#: application/front/controller/admin/ManageShaareController.php:262 353#: application/front/controller/admin/ManageShaareController.php:262
362#: application/front/controller/admin/ManageShaareController.php:302 354#: application/front/controller/admin/ManageShaareController.php:302
363#, php-format
364msgid "Bookmark with identifier %s could not be found."
365msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé."
366
367#: application/front/controller/admin/ManageShaareController.php:181 355#: application/front/controller/admin/ManageShaareController.php:181
368#: application/front/controller/admin/ManageShaareController.php:239 356#: application/front/controller/admin/ManageShaareController.php:239
369msgid "Invalid bookmark ID provided."
370msgstr "ID du lien non valide."
371
372#: application/front/controller/admin/ManageShaareController.php:247 357#: application/front/controller/admin/ManageShaareController.php:247
373msgid "Invalid visibility provided."
374msgstr "Visibilité du lien non valide."
375
376#: application/front/controller/admin/ManageShaareController.php:378 358#: application/front/controller/admin/ManageShaareController.php:378
377#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
378msgid "Edit"
379msgstr "Modifier"
380
381#: application/front/controller/admin/ManageShaareController.php:381 359#: application/front/controller/admin/ManageShaareController.php:381
382#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
383#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28
384msgid "Shaare"
385msgstr "Shaare"
386
387#: application/front/controller/admin/ManageTagController.php:29 360#: application/front/controller/admin/ManageTagController.php:29
388#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 361#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
389#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 362#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
@@ -456,6 +429,29 @@ msgstr "Le cache des miniatures a été vidé."
456msgid "Shaarli's cache folder has been cleared!" 429msgid "Shaarli's cache folder has been cleared!"
457msgstr "Le dossier de cache de Shaarli a été vidé !" 430msgstr "Le dossier de cache de Shaarli a été vidé !"
458 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
459#: application/front/controller/admin/ThumbnailsController.php:37 455#: application/front/controller/admin/ThumbnailsController.php:37
460#: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 456#: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
461msgid "Thumbnails update" 457msgid "Thumbnails update"
@@ -941,6 +937,48 @@ msgstr "Désolé, il y a rien à voir ici."
941msgid "URL or leave empty to post a note" 937msgid "URL or leave empty to post a note"
942msgstr "URL ou laisser vide pour créer une note" 938msgstr "URL ou laisser vide pour créer une note"
943 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
944#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 982#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
945msgid "Current password" 983msgid "Current password"
946msgstr "Mot de passe actuel" 984msgstr "Mot de passe actuel"
@@ -1187,15 +1225,7 @@ msgid "Description"
1187msgstr "Description" 1225msgstr "Description"
1188 1226
1189#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 1227#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
1190msgid "Tags"
1191msgstr "Tags"
1192
1193#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 1228#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
1194#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
1195#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
1196msgid "Private"
1197msgstr "Privé"
1198
1199#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80 1229#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80
1200msgid "Description will be rendered with" 1230msgid "Description will be rendered with"
1201msgstr "La description sera générée avec" 1231msgstr "La description sera générée avec"
@@ -1209,9 +1239,18 @@ msgid "Markdown syntax"
1209msgstr "la syntaxe Markdown" 1239msgstr "la syntaxe Markdown"
1210 1240
1211#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 1241#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
1242msgid "Cancel"
1243msgstr "Annuler"
1244
1245#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
1212msgid "Apply Changes" 1246msgid "Apply Changes"
1213msgstr "Appliquer les changements" 1247msgstr "Appliquer les changements"
1214 1248
1249#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
1250#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
1251msgid "Save all"
1252msgstr "Tout enregistrer"
1253
1215#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107 1254#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107
1216#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 1255#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173
1217#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 1256#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
diff --git a/index.php b/index.php
index 0ed52bad..4b5602ac 100644
--- a/index.php
+++ b/index.php
@@ -125,14 +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->get('/shaare/private/{hash}', '\Shaarli\Front\Controller\Admin\ManageShaareController:sharePrivate'); 131 $this->get('/shaare/private/{hash}', '\Shaarli\Front\Controller\Admin\ShaareManageController:sharePrivate');
132 $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:save'); 132 $this->post('/shaare-batch', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateBatchForms');
133 $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ManageShaareController:deleteBookmark'); 133 $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:save');
134 $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ManageShaareController:changeVisibility'); 134 $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ShaareManageController:deleteBookmark');
135 $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ManageShaareController:pinBookmark'); 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');
136 $this->patch( 137 $this->patch(
137 '/shaare/{id:[0-9]+}/update-thumbnail', 138 '/shaare/{id:[0-9]+}/update-thumbnail',
138 '\Shaarli\Front\Controller\Admin\ThumbnailsController:ajaxUpdate' 139 '\Shaarli\Front\Controller\Admin\ThumbnailsController:ajaxUpdate'
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/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/ManageShaareControllerTest/SharePrivateTest.php b/tests/front/controller/admin/ShaareManageControllerTest/SharePrivateTest.php
index 1e7877c7..ae61dfb7 100644
--- a/tests/front/controller/admin/ManageShaareControllerTest/SharePrivateTest.php
+++ b/tests/front/controller/admin/ShaareManageControllerTest/SharePrivateTest.php
@@ -2,11 +2,11 @@
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\Front\Controller\Admin\FrontAdminControllerMockHelper; 8use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
9use Shaarli\Front\Controller\Admin\ManageShaareController; 9use Shaarli\Front\Controller\Admin\ShaareManageController;
10use Shaarli\Http\HttpAccess; 10use Shaarli\Http\HttpAccess;
11use Shaarli\TestCase; 11use Shaarli\TestCase;
12use Slim\Http\Request; 12use Slim\Http\Request;
@@ -19,7 +19,7 @@ class SharePrivateTest extends TestCase
19{ 19{
20 use FrontAdminControllerMockHelper; 20 use FrontAdminControllerMockHelper;
21 21
22 /** @var ManageShaareController */ 22 /** @var ShaareManageController */
23 protected $controller; 23 protected $controller;
24 24
25 public function setUp(): void 25 public function setUp(): void
@@ -27,7 +27,7 @@ class SharePrivateTest extends TestCase
27 $this->createContainer(); 27 $this->createContainer();
28 28
29 $this->container->httpAccess = $this->createMock(HttpAccess::class); 29 $this->container->httpAccess = $this->createMock(HttpAccess::class);
30 $this->controller = new ManageShaareController($this->container); 30 $this->controller = new ShaareManageController($this->container);
31 } 31 }
32 32
33 /** 33 /**
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/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/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/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',