From: ArthurHoaro Date: Sat, 10 Oct 2020 15:40:26 +0000 (+0200) Subject: Feature: bulk creation of bookmarks X-Git-Tag: v0.12.1^2~23^2~4 X-Git-Url: https://git.immae.eu/?p=github%2Fshaarli%2FShaarli.git;a=commitdiff_plain;h=5d8de7587d67b5c3e5d1fed8562d9b87ecde80c1 Feature: bulk creation of bookmarks This changes creates a new form in addlink page allowing to create multiple bookmarks at once more easily. It focuses on re-using as much existing code and template component as possible. These changes includes: - a new form in addlink (hidden behind a button by default), containing a text area for URL, and tags/private status to apply to created links - this form displays a new template called editlink.batch, itself including editlink template multiple times - User interation in this new templates are handle by a new JS script (shaare-batch.js) making AJAX requests, and therefore does not need page reloading - ManageShaareController has been split into 3 distinct controllers: + ShaareAdd: displays addlink template + ShaareManage: various operation applied on existing shaares (change visibility, pin, deletion, etc.) + ShaarePublish: handles creation/edit forms and saving Shaare's form - Updated translations Fixes #137 --- 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 @@ -assignView( - 'pagetitle', - t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli') - ); - - return $response->write($this->render(TemplatePage::ADDLINK)); - } - - /** - * GET /admin/shaare - Displays the bookmark form for creation. - * Note that if the URL is found in existing bookmarks, then it will be in edit mode. - */ - public function displayCreateForm(Request $request, Response $response): Response - { - $url = cleanup_url($request->getParam('post')); - - $linkIsNew = false; - // Check if URL is not already in database (in this case, we will edit the existing link) - $bookmark = $this->container->bookmarkService->findByUrl($url); - if (null === $bookmark) { - $linkIsNew = true; - // Get shaare data if it was provided in URL (e.g.: by the bookmarklet). - $title = $request->getParam('title'); - $description = $request->getParam('description'); - $tags = $request->getParam('tags'); - $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN); - - // If this is an HTTP(S) link, we try go get the page to extract - // the title (otherwise we will to straight to the edit form.) - if (true !== $this->container->conf->get('general.enable_async_metadata', true) - && empty($title) - && strpos(get_url_scheme($url) ?: '', 'http') !== false - ) { - $metadata = $this->container->metadataRetriever->retrieve($url); - } - - if (empty($url)) { - $metadata['title'] = $this->container->conf->get('general.default_note_title', t('Note: ')); - } - - $link = [ - 'title' => $title ?? $metadata['title'] ?? '', - 'url' => $url ?? '', - 'description' => $description ?? $metadata['description'] ?? '', - 'tags' => $tags ?? $metadata['tags'] ?? '', - 'private' => $private, - ]; - } else { - $formatter = $this->container->formatterFactory->getFormatter('raw'); - $link = $formatter->format($bookmark); - } - - return $this->displayForm($link, $linkIsNew, $request, $response); - } - - /** - * GET /admin/shaare/{id} - Displays the bookmark form in edition mode. - */ - public function displayEditForm(Request $request, Response $response, array $args): Response - { - $id = $args['id'] ?? ''; - try { - if (false === ctype_digit($id)) { - throw new BookmarkNotFoundException(); - } - $bookmark = $this->container->bookmarkService->get((int) $id); // Read database - } catch (BookmarkNotFoundException $e) { - $this->saveErrorMessage(sprintf( - t('Bookmark with identifier %s could not be found.'), - $id - )); - - return $this->redirect($response, '/'); - } - - $formatter = $this->container->formatterFactory->getFormatter('raw'); - $link = $formatter->format($bookmark); - - return $this->displayForm($link, false, $request, $response); - } - - /** - * POST /admin/shaare - */ - public function save(Request $request, Response $response): Response - { - $this->checkToken($request); - - // lf_id should only be present if the link exists. - $id = $request->getParam('lf_id') !== null ? intval(escape($request->getParam('lf_id'))) : null; - if (null !== $id && true === $this->container->bookmarkService->exists($id)) { - // Edit - $bookmark = $this->container->bookmarkService->get($id); - } else { - // New link - $bookmark = new Bookmark(); - } - - $bookmark->setTitle($request->getParam('lf_title')); - $bookmark->setDescription($request->getParam('lf_description')); - $bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', [])); - $bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN)); - $bookmark->setTagsString($request->getParam('lf_tags')); - - if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE - && true !== $this->container->conf->get('general.enable_async_metadata', true) - && $bookmark->shouldUpdateThumbnail() - ) { - $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); - } - $this->container->bookmarkService->addOrSet($bookmark, false); - - // To preserve backward compatibility with 3rd parties, plugins still use arrays - $formatter = $this->container->formatterFactory->getFormatter('raw'); - $data = $formatter->format($bookmark); - $this->executePageHooks('save_link', $data); - - $bookmark->fromArray($data); - $this->container->bookmarkService->set($bookmark); - - // If we are called from the bookmarklet, we must close the popup: - if ($request->getParam('source') === 'bookmarklet') { - return $response->write(''); - } - - if (!empty($request->getParam('returnurl'))) { - $this->container->environment['HTTP_REFERER'] = escape($request->getParam('returnurl')); - } - - return $this->redirectFromReferer( - $request, - $response, - ['/admin/add-shaare', '/admin/shaare'], ['addlink', 'post', 'edit_link'], - $bookmark->getShortUrl() - ); - } - - /** - * GET /admin/shaare/delete - Delete one or multiple bookmarks (depending on `id` query parameter). - */ - public function deleteBookmark(Request $request, Response $response): Response - { - $this->checkToken($request); - - $ids = escape(trim($request->getParam('id') ?? '')); - if (empty($ids) || strpos($ids, ' ') !== false) { - // multiple, space-separated ids provided - $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); - } else { - $ids = [$ids]; - } - - // assert at least one id is given - if (0 === count($ids)) { - $this->saveErrorMessage(t('Invalid bookmark ID provided.')); - - return $this->redirectFromReferer($request, $response, [], ['delete-shaare']); - } - - $formatter = $this->container->formatterFactory->getFormatter('raw'); - $count = 0; - foreach ($ids as $id) { - try { - $bookmark = $this->container->bookmarkService->get((int) $id); - } catch (BookmarkNotFoundException $e) { - $this->saveErrorMessage(sprintf( - t('Bookmark with identifier %s could not be found.'), - $id - )); - - continue; - } - - $data = $formatter->format($bookmark); - $this->executePageHooks('delete_link', $data); - $this->container->bookmarkService->remove($bookmark, false); - ++ $count; - } - - if ($count > 0) { - $this->container->bookmarkService->save(); - } - - // If we are called from the bookmarklet, we must close the popup: - if ($request->getParam('source') === 'bookmarklet') { - return $response->write(''); - } - - // Don't redirect to where we were previously because the datastore has changed. - return $this->redirect($response, '/'); - } - - /** - * GET /admin/shaare/visibility - * - * Change visibility (public/private) of one or multiple bookmarks (depending on `id` query parameter). - */ - public function changeVisibility(Request $request, Response $response): Response - { - $this->checkToken($request); - - $ids = trim(escape($request->getParam('id') ?? '')); - if (empty($ids) || strpos($ids, ' ') !== false) { - // multiple, space-separated ids provided - $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); - } else { - // only a single id provided - $ids = [$ids]; - } - - // assert at least one id is given - if (0 === count($ids)) { - $this->saveErrorMessage(t('Invalid bookmark ID provided.')); - - return $this->redirectFromReferer($request, $response, [], ['change_visibility']); - } - - // assert that the visibility is valid - $visibility = $request->getParam('newVisibility'); - if (null === $visibility || false === in_array($visibility, ['public', 'private'], true)) { - $this->saveErrorMessage(t('Invalid visibility provided.')); - - return $this->redirectFromReferer($request, $response, [], ['change_visibility']); - } else { - $isPrivate = $visibility === 'private'; - } - - $formatter = $this->container->formatterFactory->getFormatter('raw'); - $count = 0; - - foreach ($ids as $id) { - try { - $bookmark = $this->container->bookmarkService->get((int) $id); - } catch (BookmarkNotFoundException $e) { - $this->saveErrorMessage(sprintf( - t('Bookmark with identifier %s could not be found.'), - $id - )); - - continue; - } - - $bookmark->setPrivate($isPrivate); - - // To preserve backward compatibility with 3rd parties, plugins still use arrays - $data = $formatter->format($bookmark); - $this->executePageHooks('save_link', $data); - $bookmark->fromArray($data); - - $this->container->bookmarkService->set($bookmark, false); - ++$count; - } - - if ($count > 0) { - $this->container->bookmarkService->save(); - } - - return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']); - } - - /** - * GET /admin/shaare/{id}/pin - Pin or unpin a bookmark. - */ - public function pinBookmark(Request $request, Response $response, array $args): Response - { - $this->checkToken($request); - - $id = $args['id'] ?? ''; - try { - if (false === ctype_digit($id)) { - throw new BookmarkNotFoundException(); - } - $bookmark = $this->container->bookmarkService->get((int) $id); // Read database - } catch (BookmarkNotFoundException $e) { - $this->saveErrorMessage(sprintf( - t('Bookmark with identifier %s could not be found.'), - $id - )); - - return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); - } - - $formatter = $this->container->formatterFactory->getFormatter('raw'); - - $bookmark->setSticky(!$bookmark->isSticky()); - - // To preserve backward compatibility with 3rd parties, plugins still use arrays - $data = $formatter->format($bookmark); - $this->executePageHooks('save_link', $data); - $bookmark->fromArray($data); - - $this->container->bookmarkService->set($bookmark); - - return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); - } - - /** - * GET /admin/shaare/private/{hash} - Attach a private key to given bookmark, then redirect to the sharing URL. - */ - public function sharePrivate(Request $request, Response $response, array $args): Response - { - $this->checkToken($request); - - $hash = $args['hash'] ?? ''; - $bookmark = $this->container->bookmarkService->findByHash($hash); - - if ($bookmark->isPrivate() !== true) { - return $this->redirect($response, '/shaare/' . $hash); - } - - if (empty($bookmark->getAdditionalContentEntry('private_key'))) { - $privateKey = bin2hex(random_bytes(16)); - $bookmark->addAdditionalContentEntry('private_key', $privateKey); - $this->container->bookmarkService->set($bookmark); - } - - return $this->redirect( - $response, - '/shaare/' . $hash . '?key=' . $bookmark->getAdditionalContentEntry('private_key') - ); - } - - /** - * Helper function used to display the shaare form whether it's a new or existing bookmark. - * - * @param array $link data used in template, either from parameters or from the data store - */ - protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response - { - $tags = $this->container->bookmarkService->bookmarksCountPerTag(); - if ($this->container->conf->get('formatter') === 'markdown') { - $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; - } - - $data = escape([ - 'link' => $link, - 'link_is_new' => $isNew, - 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '', - 'source' => $request->getParam('source') ?? '', - 'tags' => $tags, - 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false), - 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true), - 'retrieve_description' => $this->container->conf->get('general.retrieve_description', false), - ]); - - $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK); - - foreach ($data as $key => $value) { - $this->assignView($key, $value); - } - - $editLabel = false === $isNew ? t('Edit') .' ' : ''; - $this->assignView( - 'pagetitle', - $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli') - ); - - return $response->write($this->render(TemplatePage::EDIT_LINK)); - } -} 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 @@ +container->bookmarkService->bookmarksCountPerTag(); + if ($this->container->conf->get('formatter') === 'markdown') { + $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; + } + + $this->assignView( + 'pagetitle', + t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + $this->assignView('tags', $tags); + $this->assignView('default_private_links', $this->container->conf->get('privacy.default_private_links', false)); + $this->assignView('async_metadata', $this->container->conf->get('general.enable_async_metadata', true)); + + return $response->write($this->render(TemplatePage::ADDLINK)); + } +} 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 @@ +checkToken($request); + + $ids = escape(trim($request->getParam('id') ?? '')); + if (empty($ids) || strpos($ids, ' ') !== false) { + // multiple, space-separated ids provided + $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); + } else { + $ids = [$ids]; + } + + // assert at least one id is given + if (0 === count($ids)) { + $this->saveErrorMessage(t('Invalid bookmark ID provided.')); + + return $this->redirectFromReferer($request, $response, [], ['delete-shaare']); + } + + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $count = 0; + foreach ($ids as $id) { + try { + $bookmark = $this->container->bookmarkService->get((int) $id); + } catch (BookmarkNotFoundException $e) { + $this->saveErrorMessage(sprintf( + t('Bookmark with identifier %s could not be found.'), + $id + )); + + continue; + } + + $data = $formatter->format($bookmark); + $this->executePageHooks('delete_link', $data); + $this->container->bookmarkService->remove($bookmark, false); + ++ $count; + } + + if ($count > 0) { + $this->container->bookmarkService->save(); + } + + // If we are called from the bookmarklet, we must close the popup: + if ($request->getParam('source') === 'bookmarklet') { + return $response->write(''); + } + + // Don't redirect to where we were previously because the datastore has changed. + return $this->redirect($response, '/'); + } + + /** + * GET /admin/shaare/visibility + * + * Change visibility (public/private) of one or multiple bookmarks (depending on `id` query parameter). + */ + public function changeVisibility(Request $request, Response $response): Response + { + $this->checkToken($request); + + $ids = trim(escape($request->getParam('id') ?? '')); + if (empty($ids) || strpos($ids, ' ') !== false) { + // multiple, space-separated ids provided + $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); + } else { + // only a single id provided + $ids = [$ids]; + } + + // assert at least one id is given + if (0 === count($ids)) { + $this->saveErrorMessage(t('Invalid bookmark ID provided.')); + + return $this->redirectFromReferer($request, $response, [], ['change_visibility']); + } + + // assert that the visibility is valid + $visibility = $request->getParam('newVisibility'); + if (null === $visibility || false === in_array($visibility, ['public', 'private'], true)) { + $this->saveErrorMessage(t('Invalid visibility provided.')); + + return $this->redirectFromReferer($request, $response, [], ['change_visibility']); + } else { + $isPrivate = $visibility === 'private'; + } + + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $count = 0; + + foreach ($ids as $id) { + try { + $bookmark = $this->container->bookmarkService->get((int) $id); + } catch (BookmarkNotFoundException $e) { + $this->saveErrorMessage(sprintf( + t('Bookmark with identifier %s could not be found.'), + $id + )); + + continue; + } + + $bookmark->setPrivate($isPrivate); + + // To preserve backward compatibility with 3rd parties, plugins still use arrays + $data = $formatter->format($bookmark); + $this->executePageHooks('save_link', $data); + $bookmark->fromArray($data); + + $this->container->bookmarkService->set($bookmark, false); + ++$count; + } + + if ($count > 0) { + $this->container->bookmarkService->save(); + } + + return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']); + } + + /** + * GET /admin/shaare/{id}/pin - Pin or unpin a bookmark. + */ + public function pinBookmark(Request $request, Response $response, array $args): Response + { + $this->checkToken($request); + + $id = $args['id'] ?? ''; + try { + if (false === ctype_digit($id)) { + throw new BookmarkNotFoundException(); + } + $bookmark = $this->container->bookmarkService->get((int) $id); // Read database + } catch (BookmarkNotFoundException $e) { + $this->saveErrorMessage(sprintf( + t('Bookmark with identifier %s could not be found.'), + $id + )); + + return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); + } + + $formatter = $this->container->formatterFactory->getFormatter('raw'); + + $bookmark->setSticky(!$bookmark->isSticky()); + + // To preserve backward compatibility with 3rd parties, plugins still use arrays + $data = $formatter->format($bookmark); + $this->executePageHooks('save_link', $data); + $bookmark->fromArray($data); + + $this->container->bookmarkService->set($bookmark); + + return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); + } + + /** + * GET /admin/shaare/private/{hash} - Attach a private key to given bookmark, then redirect to the sharing URL. + */ + public function sharePrivate(Request $request, Response $response, array $args): Response + { + $this->checkToken($request); + + $hash = $args['hash'] ?? ''; + $bookmark = $this->container->bookmarkService->findByHash($hash); + + if ($bookmark->isPrivate() !== true) { + return $this->redirect($response, '/shaare/' . $hash); + } + + if (empty($bookmark->getAdditionalContentEntry('private_key'))) { + $privateKey = bin2hex(random_bytes(16)); + $bookmark->addAdditionalContentEntry('private_key', $privateKey); + $this->container->bookmarkService->set($bookmark); + } + + return $this->redirect( + $response, + '/shaare/' . $hash . '?key=' . $bookmark->getAdditionalContentEntry('private_key') + ); + } +} diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php new file mode 100644 index 00000000..608f79cf --- /dev/null +++ b/application/front/controller/admin/ShaarePublishController.php @@ -0,0 +1,222 @@ +getParam('post')); + $link = $this->buildLinkDataFromUrl($request, $url); + + return $this->displayForm($link, $link['linkIsNew'], $request, $response); + } + + /** + * POST /admin/shaare-batch - Displays multiple creation/edit forms from bulk add in add-link page. + */ + public function displayCreateBatchForms(Request $request, Response $response): Response + { + $urls = array_map('cleanup_url', explode(PHP_EOL, $request->getParam('urls'))); + + $links = []; + foreach ($urls as $url) { + $link = $this->buildLinkDataFromUrl($request, $url); + $data = $this->buildFormData($link, $link['linkIsNew'], $request); + $data['token'] = $this->container->sessionManager->generateToken(); + $data['source'] = 'batch'; + + $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK); + + $links[] = $data; + } + + $this->assignView('links', $links); + $this->assignView('batch_mode', true); + $this->assignView('async_metadata', $this->container->conf->get('general.enable_async_metadata', true)); + + return $response->write($this->render(TemplatePage::EDIT_LINK_BATCH)); + } + + /** + * GET /admin/shaare/{id} - Displays the bookmark form in edition mode. + */ + public function displayEditForm(Request $request, Response $response, array $args): Response + { + $id = $args['id'] ?? ''; + try { + if (false === ctype_digit($id)) { + throw new BookmarkNotFoundException(); + } + $bookmark = $this->container->bookmarkService->get((int) $id); // Read database + } catch (BookmarkNotFoundException $e) { + $this->saveErrorMessage(sprintf( + t('Bookmark with identifier %s could not be found.'), + $id + )); + + return $this->redirect($response, '/'); + } + + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $link = $formatter->format($bookmark); + + return $this->displayForm($link, false, $request, $response); + } + + /** + * POST /admin/shaare + */ + public function save(Request $request, Response $response): Response + { + $this->checkToken($request); + + // lf_id should only be present if the link exists. + $id = $request->getParam('lf_id') !== null ? intval(escape($request->getParam('lf_id'))) : null; + if (null !== $id && true === $this->container->bookmarkService->exists($id)) { + // Edit + $bookmark = $this->container->bookmarkService->get($id); + } else { + // New link + $bookmark = new Bookmark(); + } + + $bookmark->setTitle($request->getParam('lf_title')); + $bookmark->setDescription($request->getParam('lf_description')); + $bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', [])); + $bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN)); + $bookmark->setTagsString($request->getParam('lf_tags')); + + if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE + && true !== $this->container->conf->get('general.enable_async_metadata', true) + && $bookmark->shouldUpdateThumbnail() + ) { + $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); + } + $this->container->bookmarkService->addOrSet($bookmark, false); + + // To preserve backward compatibility with 3rd parties, plugins still use arrays + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $data = $formatter->format($bookmark); + $this->executePageHooks('save_link', $data); + + $bookmark->fromArray($data); + $this->container->bookmarkService->set($bookmark); + + // If we are called from the bookmarklet, we must close the popup: + if ($request->getParam('source') === 'bookmarklet') { + return $response->write(''); + } elseif ($request->getParam('source') === 'batch') { + return $response; + } + + if (!empty($request->getParam('returnurl'))) { + $this->container->environment['HTTP_REFERER'] = escape($request->getParam('returnurl')); + } + + return $this->redirectFromReferer( + $request, + $response, + ['/admin/add-shaare', '/admin/shaare'], ['addlink', 'post', 'edit_link'], + $bookmark->getShortUrl() + ); + } + + /** + * Helper function used to display the shaare form whether it's a new or existing bookmark. + * + * @param array $link data used in template, either from parameters or from the data store + */ + protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response + { + $data = $this->buildFormData($link, $isNew, $request); + + $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK); + + foreach ($data as $key => $value) { + $this->assignView($key, $value); + } + + $editLabel = false === $isNew ? t('Edit') .' ' : ''; + $this->assignView( + 'pagetitle', + $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + + return $response->write($this->render(TemplatePage::EDIT_LINK)); + } + + protected function buildLinkDataFromUrl(Request $request, string $url): array + { + // Check if URL is not already in database (in this case, we will edit the existing link) + $bookmark = $this->container->bookmarkService->findByUrl($url); + if (null === $bookmark) { + // Get shaare data if it was provided in URL (e.g.: by the bookmarklet). + $title = $request->getParam('title'); + $description = $request->getParam('description'); + $tags = $request->getParam('tags'); + $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN); + + // If this is an HTTP(S) link, we try go get the page to extract + // the title (otherwise we will to straight to the edit form.) + if (true !== $this->container->conf->get('general.enable_async_metadata', true) + && empty($title) + && strpos(get_url_scheme($url) ?: '', 'http') !== false + ) { + $metadata = $this->container->metadataRetriever->retrieve($url); + } + + if (empty($url)) { + $metadata['title'] = $this->container->conf->get('general.default_note_title', t('Note: ')); + } + + return [ + 'title' => $title ?? $metadata['title'] ?? '', + 'url' => $url ?? '', + 'description' => $description ?? $metadata['description'] ?? '', + 'tags' => $tags ?? $metadata['tags'] ?? '', + 'private' => $private, + 'linkIsNew' => true, + ]; + } + + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $link = $formatter->format($bookmark); + $link['linkIsNew'] = false; + + return $link; + } + + protected function buildFormData(array $link, bool $isNew, Request $request): array + { + $tags = $this->container->bookmarkService->bookmarksCountPerTag(); + if ($this->container->conf->get('formatter') === 'markdown') { + $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; + } + + return escape([ + 'link' => $link, + 'link_is_new' => $isNew, + 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '', + 'source' => $request->getParam('source') ?? '', + 'tags' => $tags, + 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false), + 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true), + 'retrieve_description' => $this->container->conf->get('general.retrieve_description', false), + ]); + } +} 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 public const DAILY = 'daily'; public const DAILY_RSS = 'dailyrss'; public const EDIT_LINK = 'editlink'; + public const EDIT_LINK_BATCH = 'editlink.batch'; public const ERROR = 'error'; public const EXPORT = 'export'; 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) { (() => { const basePath = document.querySelector('input[name="js_base_path"]').value; - const loaders = document.querySelectorAll('.loading-input'); /* * METADATA FOR EDIT BOOKMARK PAGE */ - const inputTitle = document.querySelector('input[name="lf_title"]'); - if (inputTitle != null) { - if (inputTitle.value.length > 0) { - clearLoaders(loaders); - return; - } + const inputTitles = document.querySelectorAll('input[name="lf_title"]'); + if (inputTitles != null) { + [...inputTitles].forEach((inputTitle) => { + const form = inputTitle.closest('form[name="linkform"]'); + const loaders = form.querySelectorAll('.loading-input'); + + if (inputTitle.value.length > 0) { + clearLoaders(loaders); + return; + } - const url = document.querySelector('input[name="lf_url"]').value; + const url = form.querySelector('input[name="lf_url"]').value; - const xhr = new XMLHttpRequest(); - xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.onload = () => { - const result = JSON.parse(xhr.response); - Object.keys(result).forEach((key) => { - if (result[key] !== null && result[key].length) { - const element = document.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`); - if (element != null && element.value.length === 0) { - element.value = he.decode(result[key]); + const xhr = new XMLHttpRequest(); + xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true); + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + xhr.onload = () => { + const result = JSON.parse(xhr.response); + Object.keys(result).forEach((key) => { + if (result[key] !== null && result[key].length) { + const element = form.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`); + if (element != null && element.value.length === 0) { + element.value = he.decode(result[key]); + } } - } - }); - clearLoaders(loaders); - }; + }); + clearLoaders(loaders); + }; - xhr.send(); + xhr.send(); + }); } /* diff --git a/assets/common/js/shaare-batch.js b/assets/common/js/shaare-batch.js new file mode 100644 index 00000000..9f612993 --- /dev/null +++ b/assets/common/js/shaare-batch.js @@ -0,0 +1,107 @@ +const sendBookmarkForm = (basePath, formElement) => { + const inputs = formElement + .querySelectorAll('input[type="text"], textarea, input[type="checkbox"], input[type="hidden"]'); + + const formData = new FormData(); + [...inputs].forEach((input) => { + formData.append(input.getAttribute('name'), input.value); + }); + + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', `${basePath}/admin/shaare`); + xhr.onload = () => { + if (xhr.status !== 200) { + alert(`An error occurred. Return code: ${xhr.status}`); + reject(); + } else { + formElement.remove(); + resolve(); + } + }; + xhr.send(formData); + }); +}; + +const sendBookmarkDelete = (buttonElement, formElement) => ( + new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', buttonElement.href); + xhr.onload = () => { + if (xhr.status !== 200) { + alert(`An error occurred. Return code: ${xhr.status}`); + reject(); + } else { + formElement.remove(); + resolve(); + } + }; + xhr.send(); + }) +); + +const redirectIfEmptyBatch = (basePath, formElements, path) => { + if (formElements == null || formElements.length === 0) { + window.location.href = `${basePath}${path}`; + } +}; + +(() => { + const basePath = document.querySelector('input[name="js_base_path"]').value; + const getForms = () => document.querySelectorAll('form[name="linkform"]'); + + const cancelButtons = document.querySelectorAll('[name="cancel-batch-link"]'); + if (cancelButtons != null) { + [...cancelButtons].forEach((cancelButton) => { + cancelButton.addEventListener('click', (e) => { + e.preventDefault(); + e.target.closest('form[name="linkform"]').remove(); + redirectIfEmptyBatch(basePath, getForms(), '/admin/add-shaare'); + }); + }); + } + + const saveButtons = document.querySelectorAll('[name="save_edit"]'); + if (saveButtons != null) { + [...saveButtons].forEach((saveButton) => { + saveButton.addEventListener('click', (e) => { + e.preventDefault(); + + const formElement = e.target.closest('form[name="linkform"]'); + sendBookmarkForm(basePath, formElement) + .then(() => redirectIfEmptyBatch(basePath, getForms(), '/')); + }); + }); + } + + const saveAllButtons = document.querySelectorAll('[name="save_edit_batch"]'); + if (saveAllButtons != null) { + [...saveAllButtons].forEach((saveAllButton) => { + saveAllButton.addEventListener('click', (e) => { + e.preventDefault(); + + const promises = []; + [...getForms()].forEach((formElement) => { + promises.push(sendBookmarkForm(basePath, formElement)); + }); + + Promise.all(promises).then(() => { + window.location.href = basePath || '/'; + }); + }); + }); + } + + const deleteButtons = document.querySelectorAll('[name="delete_link"]'); + if (deleteButtons != null) { + [...deleteButtons].forEach((deleteButton) => { + deleteButton.addEventListener('click', (e) => { + e.preventDefault(); + + const formElement = e.target.closest('form[name="linkform"]'); + sendBookmarkDelete(e.target, formElement) + .then(() => redirectIfEmptyBatch(basePath, getForms(), '/')); + }); + }); + } +})(); diff --git a/assets/default/js/base.js b/assets/default/js/base.js index 7f6b9637..9161b4fc 100644 --- a/assets/default/js/base.js +++ b/assets/default/js/base.js @@ -634,4 +634,25 @@ function init(description) { }); }); } + + const bulkCreationButton = document.querySelector('.addlink-batch-show-more-block'); + if (bulkCreationButton != null) { + const toggleBulkCreationVisibility = (showMoreBlockElement, formElement) => { + if (bulkCreationButton.classList.contains('pure-u-0')) { + showMoreBlockElement.classList.remove('pure-u-0'); + formElement.classList.add('pure-u-0'); + } else { + showMoreBlockElement.classList.add('pure-u-0'); + formElement.classList.remove('pure-u-0'); + } + }; + + const bulkCreationForm = document.querySelector('.addlink-batch-form-block'); + + toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm); + bulkCreationButton.querySelector('a').addEventListener('click', (e) => { + e.preventDefault(); + toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm); + }); + } })(); diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss index 7dc61903..7c85dee8 100644 --- a/assets/default/scss/shaarli.scss +++ b/assets/default/scss/shaarli.scss @@ -1023,6 +1023,10 @@ body, &.button-red { background: $red; } + + &.button-grey { + background: $light-grey; + } } .submit-buttons { @@ -1083,6 +1087,11 @@ body, position: absolute; right: 5%; } + + &.button-grey { + position: absolute; + left: 5%; + } } } } @@ -1750,6 +1759,46 @@ form { } } +// Batch creation +input[name='save_edit_batch'] { + @extend %page-form-button; +} + +.addlink-batch-show-more { + display: flex; + align-items: center; + margin: 20px 0 8px; + + a { + color: var(--main-color); + text-decoration: none; + } + + &::before, + &::after { + content: ""; + flex-grow: 1; + background: rgba(0, 0, 0, 0.35); + height: 1px; + font-size: 0; + line-height: 0; + } + + &::before { + margin: 0 16px 0 0; + } + + &::after { + margin: 0 0 0 16px; + } +} + +.addlink-batch-form-block { + .pure-alert { + margin: 25px 0 0 0; + } +} + // Print rules @media print { .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 "" "le serveur web peut accepter (%s). Merci de l'envoyer en parties plus " "légères." -#: application/front/controller/admin/ManageShaareController.php:29 -#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 -msgid "Shaare a new link" -msgstr "Partager un nouveau lien" - #: application/front/controller/admin/ManageShaareController.php:64 -msgid "Note: " -msgstr "Note : " - #: application/front/controller/admin/ManageShaareController.php:95 #: application/front/controller/admin/ManageShaareController.php:193 #: application/front/controller/admin/ManageShaareController.php:262 #: application/front/controller/admin/ManageShaareController.php:302 -#, php-format -msgid "Bookmark with identifier %s could not be found." -msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé." - #: application/front/controller/admin/ManageShaareController.php:181 #: application/front/controller/admin/ManageShaareController.php:239 -msgid "Invalid bookmark ID provided." -msgstr "ID du lien non valide." - #: application/front/controller/admin/ManageShaareController.php:247 -msgid "Invalid visibility provided." -msgstr "Visibilité du lien non valide." - #: application/front/controller/admin/ManageShaareController.php:378 -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 -msgid "Edit" -msgstr "Modifier" - #: application/front/controller/admin/ManageShaareController.php:381 -#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 -msgid "Shaare" -msgstr "Shaare" - #: application/front/controller/admin/ManageTagController.php:29 #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 @@ -456,6 +429,29 @@ msgstr "Le cache des miniatures a été vidé." msgid "Shaarli's cache folder has been cleared!" msgstr "Le dossier de cache de Shaarli a été vidé !" +#, php-format +msgid "Bookmark with identifier %s could not be found." +msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé." + +#: application/front/controller/admin/ShaareManageController.php:101 +msgid "Invalid visibility provided." +msgstr "Visibilité du lien non valide." + +#: application/front/controller/admin/ShaarePublishController.php:154 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 +msgid "Edit" +msgstr "Modifier" + +#: application/front/controller/admin/ShaarePublishController.php:157 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 +msgid "Shaare" +msgstr "Shaare" + +#: application/front/controller/admin/ShaarePublishController.php:184 +msgid "Note: " +msgstr "Note : " + #: application/front/controller/admin/ThumbnailsController.php:37 #: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 msgid "Thumbnails update" @@ -941,6 +937,48 @@ msgstr "Désolé, il y a rien à voir ici." msgid "URL or leave empty to post a note" msgstr "URL ou laisser vide pour créer une note" +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "BULK CREATION" +msgstr "CRÉATION DE MASSE" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 +msgid "Metadata asynchronous retrieval is disabled." +msgstr "La récupération asynchrone des meta-données est désactivée." + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +msgid "" +"We recommend that you enable the setting general > " +"enable_async_metadata in your configuration file to use bulk link " +"creation." +msgstr "" +"Nous recommandons d'activer le paramètre general > " +"enable_async_metadata dans votre fichier de configuration pour utiliser " +"la création de masse." + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 +msgid "Shaare multiple new links" +msgstr "Partagez plusieurs nouveaux liens" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59 +msgid "Add one URL per line to create multiple bookmarks." +msgstr "Ajouter une URL par ligne pour créer plusieurs marque-pages." + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 +msgid "Tags" +msgstr "Tags" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 +msgid "Private" +msgstr "Privé" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 +msgid "Add links" +msgstr "Ajouter des liens" + #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 msgid "Current password" msgstr "Mot de passe actuel" @@ -1187,15 +1225,7 @@ msgid "Description" msgstr "Description" #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 -msgid "Tags" -msgstr "Tags" - #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 -#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 -msgid "Private" -msgstr "Privé" - #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80 msgid "Description will be rendered with" msgstr "La description sera générée avec" @@ -1209,9 +1239,18 @@ msgid "Markdown syntax" msgstr "la syntaxe Markdown" #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 +msgid "Cancel" +msgstr "Annuler" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 msgid "Apply Changes" msgstr "Appliquer les changements" +#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12 +#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 +msgid "Save all" +msgstr "Tout enregistrer" + #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 #: 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 () { $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save'); $this->get('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index'); $this->post('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save'); - $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:addShaare'); - $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayCreateForm'); - $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayEditForm'); - $this->get('/shaare/private/{hash}', '\Shaarli\Front\Controller\Admin\ManageShaareController:sharePrivate'); - $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:save'); - $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ManageShaareController:deleteBookmark'); - $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ManageShaareController:changeVisibility'); - $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ManageShaareController:pinBookmark'); + $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ShaareAddController:addShaare'); + $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateForm'); + $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayEditForm'); + $this->get('/shaare/private/{hash}', '\Shaarli\Front\Controller\Admin\ShaareManageController:sharePrivate'); + $this->post('/shaare-batch', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateBatchForms'); + $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:save'); + $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ShaareManageController:deleteBookmark'); + $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ShaareManageController:changeVisibility'); + $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ShaareManageController:pinBookmark'); $this->patch( '/shaare/{id:[0-9]+}/update-thumbnail', '\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 @@ -createContainer(); - - $this->container->httpAccess = $this->createMock(HttpAccess::class); - $this->controller = new ManageShaareController($this->container); - } - - /** - * Test displaying add link page - */ - public function testAddShaare(): void - { - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - $request = $this->createMock(Request::class); - $response = new Response(); - - $result = $this->controller->addShaare($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertSame('addlink', (string) $result->getBody()); - - static::assertSame('Shaare a new link - Shaarli', $assignedVariables['pagetitle']); - } -} 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 @@ +createContainer(); + + $this->container->httpAccess = $this->createMock(HttpAccess::class); + $this->controller = new ShaareAddController($this->container); + } + + /** + * Test displaying add link page + */ + public function testAddShaare(): void + { + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $request = $this->createMock(Request::class); + $response = new Response(); + + $expectedTags = [ + 'tag1' => 32, + 'tag2' => 24, + 'tag3' => 1, + ]; + $this->container->bookmarkService + ->expects(static::once()) + ->method('bookmarksCountPerTag') + ->willReturn($expectedTags) + ; + $expectedTags = array_merge($expectedTags, [BookmarkMarkdownFormatter::NO_MD_TAG => 1]); + + $this->container->conf = $this->createMock(ConfigManager::class); + $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { + return $key === 'formatter' ? 'markdown' : $default; + }); + + $result = $this->controller->addShaare($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('addlink', (string) $result->getBody()); + + static::assertSame('Shaare a new link - Shaarli', $assignedVariables['pagetitle']); + static::assertFalse($assignedVariables['default_private_links']); + static::assertTrue($assignedVariables['async_metadata']); + static::assertSame($expectedTags, $assignedVariables['tags']); + } + + /** + * Test displaying add link page + */ + public function testAddShaareWithoutMd(): void + { + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $request = $this->createMock(Request::class); + $response = new Response(); + + $expectedTags = [ + 'tag1' => 32, + 'tag2' => 24, + 'tag3' => 1, + ]; + $this->container->bookmarkService + ->expects(static::once()) + ->method('bookmarksCountPerTag') + ->willReturn($expectedTags) + ; + + $result = $this->controller->addShaare($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('addlink', (string) $result->getBody()); + + static::assertSame($expectedTags, $assignedVariables['tags']); + } +} diff --git a/tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/ChangeVisibilityBookmarkTest.php similarity index 98% rename from tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php rename to 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 @@ declare(strict_types=1); -namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; +namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest; use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; @@ -10,7 +10,7 @@ use Shaarli\Formatter\BookmarkFormatter; use Shaarli\Formatter\BookmarkRawFormatter; use Shaarli\Formatter\FormatterFactory; use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; -use Shaarli\Front\Controller\Admin\ManageShaareController; +use Shaarli\Front\Controller\Admin\ShaareManageController; use Shaarli\Http\HttpAccess; use Shaarli\Security\SessionManager; use Shaarli\TestCase; @@ -21,7 +21,7 @@ class ChangeVisibilityBookmarkTest extends TestCase { use FrontAdminControllerMockHelper; - /** @var ManageShaareController */ + /** @var ShaareManageController */ protected $controller; public function setUp(): void @@ -29,7 +29,7 @@ class ChangeVisibilityBookmarkTest extends TestCase $this->createContainer(); $this->container->httpAccess = $this->createMock(HttpAccess::class); - $this->controller = new ManageShaareController($this->container); + $this->controller = new ShaareManageController($this->container); } /** diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php similarity index 98% rename from tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php rename to 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 @@ declare(strict_types=1); -namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; +namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest; use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Formatter\BookmarkFormatter; use Shaarli\Formatter\FormatterFactory; use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; -use Shaarli\Front\Controller\Admin\ManageShaareController; +use Shaarli\Front\Controller\Admin\ShaareManageController; use Shaarli\Http\HttpAccess; use Shaarli\Security\SessionManager; use Shaarli\TestCase; @@ -20,7 +20,7 @@ class DeleteBookmarkTest extends TestCase { use FrontAdminControllerMockHelper; - /** @var ManageShaareController */ + /** @var ShaareManageController */ protected $controller; public function setUp(): void @@ -28,7 +28,7 @@ class DeleteBookmarkTest extends TestCase $this->createContainer(); $this->container->httpAccess = $this->createMock(HttpAccess::class); - $this->controller = new ManageShaareController($this->container); + $this->controller = new ShaareManageController($this->container); } /** diff --git a/tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/PinBookmarkTest.php similarity index 95% rename from tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php rename to 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 @@ declare(strict_types=1); -namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; +namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest; use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; -use Shaarli\Front\Controller\Admin\ManageShaareController; +use Shaarli\Front\Controller\Admin\ShaareManageController; use Shaarli\Http\HttpAccess; use Shaarli\Security\SessionManager; use Shaarli\TestCase; @@ -18,7 +18,7 @@ class PinBookmarkTest extends TestCase { use FrontAdminControllerMockHelper; - /** @var ManageShaareController */ + /** @var ShaareManageController */ protected $controller; public function setUp(): void @@ -26,7 +26,7 @@ class PinBookmarkTest extends TestCase $this->createContainer(); $this->container->httpAccess = $this->createMock(HttpAccess::class); - $this->controller = new ManageShaareController($this->container); + $this->controller = new ShaareManageController($this->container); } /** diff --git a/tests/front/controller/admin/ManageShaareControllerTest/SharePrivateTest.php b/tests/front/controller/admin/ShaareManageControllerTest/SharePrivateTest.php similarity index 94% rename from tests/front/controller/admin/ManageShaareControllerTest/SharePrivateTest.php rename to 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 @@ declare(strict_types=1); -namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; +namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest; use Shaarli\Bookmark\Bookmark; use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; -use Shaarli\Front\Controller\Admin\ManageShaareController; +use Shaarli\Front\Controller\Admin\ShaareManageController; use Shaarli\Http\HttpAccess; use Shaarli\TestCase; use Slim\Http\Request; @@ -19,7 +19,7 @@ class SharePrivateTest extends TestCase { use FrontAdminControllerMockHelper; - /** @var ManageShaareController */ + /** @var ShaareManageController */ protected $controller; public function setUp(): void @@ -27,7 +27,7 @@ class SharePrivateTest extends TestCase $this->createContainer(); $this->container->httpAccess = $this->createMock(HttpAccess::class); - $this->controller = new ManageShaareController($this->container); + $this->controller = new ShaareManageController($this->container); } /** diff --git a/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php new file mode 100644 index 00000000..34547120 --- /dev/null +++ b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php @@ -0,0 +1,62 @@ +createContainer(); + + $this->container->httpAccess = $this->createMock(HttpAccess::class); + $this->container->metadataRetriever = $this->createMock(MetadataRetriever::class); + $this->controller = new ShaarePublishController($this->container); + } + + /** + * TODO + */ + public function testDisplayCreateFormBatch(): void + { + $urls = [ + 'https://domain1.tld/url1', + 'https://domain2.tld/url2', + 'https://domain3.tld/url3', + ]; + + $request = $this->createMock(Request::class); + $request->method('getParam')->willReturnCallback(function (string $key) use ($urls): ?string { + return $key === 'urls' ? implode(PHP_EOL, $urls) : null; + }); + $response = new Response(); + + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $result = $this->controller->displayCreateBatchForms($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('editlink.batch', (string) $result->getBody()); + + static::assertTrue($assignedVariables['batch_mode']); + static::assertCount(3, $assignedVariables['links']); + static::assertSame($urls[0], $assignedVariables['links'][0]['link']['url']); + static::assertSame($urls[1], $assignedVariables['links'][1]['link']['url']); + static::assertSame($urls[2], $assignedVariables['links'][2]['link']['url']); + } +} diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateFormTest.php similarity index 98% rename from tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php rename to 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 @@ declare(strict_types=1); -namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; +namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest; use Shaarli\Bookmark\Bookmark; use Shaarli\Config\ConfigManager; use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; -use Shaarli\Front\Controller\Admin\ManageShaareController; +use Shaarli\Front\Controller\Admin\ShaarePublishController; use Shaarli\Http\HttpAccess; use Shaarli\Http\MetadataRetriever; use Shaarli\TestCase; @@ -18,7 +18,7 @@ class DisplayCreateFormTest extends TestCase { use FrontAdminControllerMockHelper; - /** @var ManageShaareController */ + /** @var ShaarePublishController */ protected $controller; public function setUp(): void @@ -27,7 +27,7 @@ class DisplayCreateFormTest extends TestCase $this->container->httpAccess = $this->createMock(HttpAccess::class); $this->container->metadataRetriever = $this->createMock(MetadataRetriever::class); - $this->controller = new ManageShaareController($this->container); + $this->controller = new ShaarePublishController($this->container); } /** diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayEditFormTest.php similarity index 95% rename from tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php rename to 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 @@ declare(strict_types=1); -namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; +namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest; use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; -use Shaarli\Front\Controller\Admin\ManageShaareController; +use Shaarli\Front\Controller\Admin\ShaarePublishController; use Shaarli\Http\HttpAccess; use Shaarli\Security\SessionManager; use Shaarli\TestCase; @@ -18,7 +18,7 @@ class DisplayEditFormTest extends TestCase { use FrontAdminControllerMockHelper; - /** @var ManageShaareController */ + /** @var ShaarePublishController */ protected $controller; public function setUp(): void @@ -26,7 +26,7 @@ class DisplayEditFormTest extends TestCase $this->createContainer(); $this->container->httpAccess = $this->createMock(HttpAccess::class); - $this->controller = new ManageShaareController($this->container); + $this->controller = new ShaarePublishController($this->container); } /** diff --git a/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/SaveBookmarkTest.php similarity index 98% rename from tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php rename to 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 @@ declare(strict_types=1); -namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest; +namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest; use Shaarli\Bookmark\Bookmark; use Shaarli\Config\ConfigManager; use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; -use Shaarli\Front\Controller\Admin\ManageShaareController; +use Shaarli\Front\Controller\Admin\ShaarePublishController; use Shaarli\Front\Exception\WrongTokenException; use Shaarli\Http\HttpAccess; use Shaarli\Security\SessionManager; @@ -20,7 +20,7 @@ class SaveBookmarkTest extends TestCase { use FrontAdminControllerMockHelper; - /** @var ManageShaareController */ + /** @var ShaarePublishController */ protected $controller; public function setUp(): void @@ -28,7 +28,7 @@ class SaveBookmarkTest extends TestCase $this->createContainer(); $this->container->httpAccess = $this->createMock(HttpAccess::class); - $this->controller = new ManageShaareController($this->container); + $this->controller = new ShaarePublishController($this->container); } /** diff --git a/tpl/default/addlink.html b/tpl/default/addlink.html index 67d3ebd1..7d4bc9e6 100644 --- a/tpl/default/addlink.html +++ b/tpl/default/addlink.html @@ -20,6 +20,62 @@ + + + + + {include="page.footer"} diff --git a/tpl/default/editlink.batch.html b/tpl/default/editlink.batch.html new file mode 100644 index 00000000..71985c1a --- /dev/null +++ b/tpl/default/editlink.batch.html @@ -0,0 +1,23 @@ + + + + {include="includes"} + + +{include="page.header"} + +
+ +
+ +{loop="$links"} + {include="editlink"} +{/loop} + +
+ +
+ +{include="page.footer"} +{if="$async_metadata"}{/if} + diff --git a/tpl/default/editlink.html b/tpl/default/editlink.html index 7ab7e1fe..980b2b8e 100644 --- a/tpl/default/editlink.html +++ b/tpl/default/editlink.html @@ -1,3 +1,4 @@ +{if="empty($batch_mode)"} @@ -5,6 +6,10 @@ {include="page.header"} +{else} + {ignore}Lil hack: when included in a loop in batch mode, `$value` is assigned by RainTPL with template vars.{/ignore} + {function="extract($value) ? '' : ''"} +{/if} + +{if="empty($batch_mode)"} {include="page.footer"} {if="$link_is_new && $async_metadata"}{/if} +{/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 = [ { mode: 'production', entry: { + shaare_batch: './assets/common/js/shaare-batch.js', thumbnails: './assets/common/js/thumbnails.js', thumbnails_update: './assets/common/js/thumbnails-update.js', metadata: './assets/common/js/metadata.js',