From 5d8de7587d67b5c3e5d1fed8562d9b87ecde80c1 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 10 Oct 2020 17:40:26 +0200 Subject: 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 --- .../controller/admin/ShaarePublishController.php | 222 +++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 application/front/controller/admin/ShaarePublishController.php (limited to 'application/front/controller/admin/ShaarePublishController.php') 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), + ]); + } +} -- cgit v1.2.3 From 25e90d8d75382721ff7473fa1686090fcfeb46ff Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 11 Oct 2020 13:34:38 +0200 Subject: Bulk creation: fix private status based on the first form --- application/front/controller/admin/ShaarePublishController.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'application/front/controller/admin/ShaarePublishController.php') diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index 608f79cf..fd680ea0 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -169,7 +169,11 @@ class ShaarePublishController extends ShaarliAdminController $title = $request->getParam('title'); $description = $request->getParam('description'); $tags = $request->getParam('tags'); - $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN); + if ($request->getParam('private') !== null) { + $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN); + } else { + $private = $this->container->conf->get('privacy.default_private_links', false); + } // 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.) -- cgit v1.2.3 From c609944cb906a2f5002cd86a808aa36d8deb2afd Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 23 Oct 2020 12:29:52 +0200 Subject: Bulk creation: improve performances using memoization Reduced additional processing time per links from ~40ms to ~5ms --- .../controller/admin/ShaarePublishController.php | 52 ++++++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) (limited to 'application/front/controller/admin/ShaarePublishController.php') diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index fd680ea0..65fdcdee 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -6,6 +6,7 @@ namespace Shaarli\Front\Controller\Admin; use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; +use Shaarli\Formatter\BookmarkFormatter; use Shaarli\Formatter\BookmarkMarkdownFormatter; use Shaarli\Render\TemplatePage; use Shaarli\Thumbnailer; @@ -14,6 +15,16 @@ use Slim\Http\Response; class ShaarePublishController extends ShaarliAdminController { + /** + * @var BookmarkFormatter[] Statically cached instances of formatters + */ + protected $formatters = []; + + /** + * @var array Statically cached bookmark's tags counts + */ + protected $tags; + /** * 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. @@ -72,7 +83,7 @@ class ShaarePublishController extends ShaarliAdminController return $this->redirect($response, '/'); } - $formatter = $this->container->formatterFactory->getFormatter('raw'); + $formatter = $this->getFormatter('raw'); $link = $formatter->format($bookmark); return $this->displayForm($link, false, $request, $response); @@ -110,7 +121,7 @@ class ShaarePublishController extends ShaarliAdminController $this->container->bookmarkService->addOrSet($bookmark, false); // To preserve backward compatibility with 3rd parties, plugins still use arrays - $formatter = $this->container->formatterFactory->getFormatter('raw'); + $formatter = $this->getFormatter('raw'); $data = $formatter->format($bookmark); $this->executePageHooks('save_link', $data); @@ -198,7 +209,7 @@ class ShaarePublishController extends ShaarliAdminController ]; } - $formatter = $this->container->formatterFactory->getFormatter('raw'); + $formatter = $this->getFormatter('raw'); $link = $formatter->format($bookmark); $link['linkIsNew'] = false; @@ -207,20 +218,43 @@ class ShaarePublishController extends ShaarliAdminController 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, + 'tags' => $this->getTags(), '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), ]); } + + /** + * Memoize formatterFactory->getFormatter() calls. + */ + protected function getFormatter(string $type): BookmarkFormatter + { + if (!array_key_exists($type, $this->formatters) || $this->formatters[$type] === null) { + $this->formatters[$type] = $this->container->formatterFactory->getFormatter($type); + } + + return $this->formatters[$type]; + } + + /** + * Memoize bookmarkService->bookmarksCountPerTag() calls. + */ + protected function getTags(): array + { + if ($this->tags === null) { + $this->tags = $this->container->bookmarkService->bookmarksCountPerTag(); + + if ($this->container->conf->get('formatter') === 'markdown') { + $this->tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; + } + } + + return $this->tags; + } } -- cgit v1.2.3 From 34c8f558e595d4f90e46e3753c8455b0b515771a Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 23 Oct 2020 13:28:02 +0200 Subject: Bulk creation: ignore blank lines --- application/front/controller/admin/ShaarePublishController.php | 3 +++ 1 file changed, 3 insertions(+) (limited to 'application/front/controller/admin/ShaarePublishController.php') diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index 65fdcdee..ddcffdc7 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -46,6 +46,9 @@ class ShaarePublishController extends ShaarliAdminController $links = []; foreach ($urls as $url) { + if (empty($url)) { + continue; + } $link = $this->buildLinkDataFromUrl($request, $url); $data = $this->buildFormData($link, $link['linkIsNew'], $request); $data['token'] = $this->container->sessionManager->generateToken(); -- cgit v1.2.3 From 114a43b20e9a1f83647d4f0f7a001e80a76c75ce Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 28 Oct 2020 14:13:50 +0100 Subject: Remove unnecessary escape of referer Fixes #1611 --- application/front/controller/admin/ShaarePublishController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'application/front/controller/admin/ShaarePublishController.php') diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index ddcffdc7..18afc2d1 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -139,7 +139,7 @@ class ShaarePublishController extends ShaarliAdminController } if (!empty($request->getParam('returnurl'))) { - $this->container->environment['HTTP_REFERER'] = escape($request->getParam('returnurl')); + $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl'); } return $this->redirectFromReferer( -- cgit v1.2.3 From b3bd8c3e8d367975980043e772f7cd78b7f96bc6 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 22 Oct 2020 16:21:03 +0200 Subject: Feature: support any tag separator So it allows to have multiple words tags. Breaking change: commas ',' are no longer a default separator. Fixes #594 --- .../front/controller/admin/ShaarePublishController.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'application/front/controller/admin/ShaarePublishController.php') diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index 18afc2d1..625a5680 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -113,7 +113,10 @@ class ShaarePublishController extends ShaarliAdminController $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')); + $bookmark->setTagsString( + $request->getParam('lf_tags'), + $this->container->conf->get('general.tags_separator', ' ') + ); if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE && true !== $this->container->conf->get('general.enable_async_metadata', true) @@ -128,7 +131,7 @@ class ShaarePublishController extends ShaarliAdminController $data = $formatter->format($bookmark); $this->executePageHooks('save_link', $data); - $bookmark->fromArray($data); + $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' ')); $this->container->bookmarkService->set($bookmark); // If we are called from the bookmarklet, we must close the popup: @@ -221,6 +224,11 @@ class ShaarePublishController extends ShaarliAdminController protected function buildFormData(array $link, bool $isNew, Request $request): array { + $link['tags'] = strlen($link['tags']) > 0 + ? $link['tags'] . $this->container->conf->get('general.tags_separator', ' ') + : $link['tags'] + ; + return escape([ 'link' => $link, 'link_is_new' => $isNew, -- cgit v1.2.3 From 53054b2bf6a919fd4ff9b44b6ad1986f21f488b6 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 22 Sep 2020 20:25:47 +0200 Subject: Apply PHP Code Beautifier on source code for linter automatic fixes --- .../front/controller/admin/ShaarePublishController.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'application/front/controller/admin/ShaarePublishController.php') diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index 625a5680..4cbfcdc5 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -118,7 +118,8 @@ class ShaarePublishController extends ShaarliAdminController $this->container->conf->get('general.tags_separator', ' ') ); - if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE + 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() ) { @@ -148,7 +149,8 @@ class ShaarePublishController extends ShaarliAdminController return $this->redirectFromReferer( $request, $response, - ['/admin/add-shaare', '/admin/shaare'], ['addlink', 'post', 'edit_link'], + ['/admin/add-shaare', '/admin/shaare'], + ['addlink', 'post', 'edit_link'], $bookmark->getShortUrl() ); } @@ -168,10 +170,10 @@ class ShaarePublishController extends ShaarliAdminController $this->assignView($key, $value); } - $editLabel = false === $isNew ? t('Edit') .' ' : ''; + $editLabel = false === $isNew ? t('Edit') . ' ' : ''; $this->assignView( 'pagetitle', - $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli') + $editLabel . t('Shaare') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') ); return $response->write($this->render(TemplatePage::EDIT_LINK)); @@ -194,7 +196,8 @@ class ShaarePublishController extends ShaarliAdminController // 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) + if ( + true !== $this->container->conf->get('general.enable_async_metadata', true) && empty($title) && strpos(get_url_scheme($url) ?: '', 'http') !== false ) { -- cgit v1.2.3