]> git.immae.eu Git - github/shaarli/Shaarli.git/commitdiff
Handle shaare creation/edition/deletion through Slim controllers
authorArthurHoaro <arthur@hoa.ro>
Sat, 6 Jun 2020 12:01:03 +0000 (14:01 +0200)
committerArthurHoaro <arthur@hoa.ro>
Thu, 23 Jul 2020 19:19:21 +0000 (21:19 +0200)
20 files changed:
application/Utils.php
application/bookmark/LinkUtils.php
application/container/ContainerBuilder.php
application/container/ShaarliContainer.php
application/front/controller/admin/PostBookmarkController.php [new file with mode: 0644]
application/front/controller/admin/ToolsController.php
application/front/controller/visitor/DailyController.php
application/front/controller/visitor/FeedController.php
application/front/controller/visitor/ShaarliVisitorController.php
application/http/HttpAccess.php [new file with mode: 0644]
application/http/HttpUtils.php
doc/md/Translations.md
index.php
tests/container/ShaarliTestContainer.php
tests/front/controller/admin/PostBookmarkControllerTest.php [new file with mode: 0644]
tpl/default/addlink.html
tpl/default/editlink.html
tpl/default/page.header.html
tpl/vintage/addlink.html
tpl/vintage/page.header.html

index 72c90049b9b41c3e7926bede39c4caf6facc2a6a..9c9eaaa2611eff24d62d24eb2d30e2da372d4aad 100644 (file)
@@ -91,6 +91,10 @@ function endsWith($haystack, $needle, $case = true)
  */
 function escape($input)
 {
+    if (null === $input) {
+        return null;
+    }
+
     if (is_bool($input)) {
         return $input;
     }
index 98d9038a4949325a23e2a2db2037b9ac1feca4de..68914fcab749a19b1ba15193decd99247158375a 100644 (file)
@@ -2,112 +2,6 @@
 
 use Shaarli\Bookmark\Bookmark;
 
-/**
- * Get cURL callback function for CURLOPT_WRITEFUNCTION
- *
- * @param string $charset     to extract from the downloaded page (reference)
- * @param string $title       to extract from the downloaded page (reference)
- * @param string $description to extract from the downloaded page (reference)
- * @param string $keywords    to extract from the downloaded page (reference)
- * @param bool   $retrieveDescription Automatically tries to retrieve description and keywords from HTML content
- * @param string $curlGetInfo Optionally overrides curl_getinfo function
- *
- * @return Closure
- */
-function get_curl_download_callback(
-    &$charset,
-    &$title,
-    &$description,
-    &$keywords,
-    $retrieveDescription,
-    $curlGetInfo = 'curl_getinfo'
-) {
-    $isRedirected = false;
-    $currentChunk = 0;
-    $foundChunk = null;
-
-    /**
-     * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
-     *
-     * While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text'
-     * Then we extract the title and the charset and stop the download when it's done.
-     *
-     * @param resource $ch   cURL resource
-     * @param string   $data chunk of data being downloaded
-     *
-     * @return int|bool length of $data or false if we need to stop the download
-     */
-    return function (&$ch, $data) use (
-        $retrieveDescription,
-        $curlGetInfo,
-        &$charset,
-        &$title,
-        &$description,
-        &$keywords,
-        &$isRedirected,
-        &$currentChunk,
-        &$foundChunk
-    ) {
-        $currentChunk++;
-        $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
-        if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
-            $isRedirected = true;
-            return strlen($data);
-        }
-        if (!empty($responseCode) && $responseCode !== 200) {
-            return false;
-        }
-        // After a redirection, the content type will keep the previous request value
-        // until it finds the next content-type header.
-        if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) {
-            $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
-        }
-        if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
-            return false;
-        }
-        if (!empty($contentType) && empty($charset)) {
-            $charset = header_extract_charset($contentType);
-        }
-        if (empty($charset)) {
-            $charset = html_extract_charset($data);
-        }
-        if (empty($title)) {
-            $title = html_extract_title($data);
-            $foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
-        }
-        if ($retrieveDescription && empty($description)) {
-            $description = html_extract_tag('description', $data);
-            $foundChunk = ! empty($description) ? $currentChunk : $foundChunk;
-        }
-        if ($retrieveDescription && empty($keywords)) {
-            $keywords = html_extract_tag('keywords', $data);
-            if (! empty($keywords)) {
-                $foundChunk = $currentChunk;
-                // Keywords use the format tag1, tag2 multiple words, tag
-                // So we format them to match Shaarli's separator and glue multiple words with '-'
-                $keywords = implode(' ', array_map(function($keyword) {
-                    return implode('-', preg_split('/\s+/', trim($keyword)));
-                }, explode(',', $keywords)));
-            }
-        }
-
-        // We got everything we want, stop the download.
-        // If we already found either the title, description or keywords,
-        // it's highly unlikely that we'll found the other metas further than
-        // in the same chunk of data or the next one. So we also stop the download after that.
-        if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null
-            && (! $retrieveDescription
-                || $foundChunk < $currentChunk
-                || (!empty($title) && !empty($description) && !empty($keywords))
-            )
-        ) {
-            return false;
-        }
-
-        return strlen($data);
-    };
-}
-
 /**
  * Extract title from an HTML document.
  *
index 84406979296e3a94d5a87af5645d38eb4ad96198..851262461fd650c9877d04f30b0c9b54105eeeea 100644 (file)
@@ -10,11 +10,13 @@ use Shaarli\Config\ConfigManager;
 use Shaarli\Feed\FeedBuilder;
 use Shaarli\Formatter\FormatterFactory;
 use Shaarli\History;
+use Shaarli\Http\HttpAccess;
 use Shaarli\Plugin\PluginManager;
 use Shaarli\Render\PageBuilder;
 use Shaarli\Render\PageCacheManager;
 use Shaarli\Security\LoginManager;
 use Shaarli\Security\SessionManager;
+use Shaarli\Thumbnailer;
 
 /**
  * Class ContainerBuilder
@@ -110,6 +112,14 @@ class ContainerBuilder
             );
         };
 
+        $container['thumbnailer'] = function (ShaarliContainer $container): Thumbnailer {
+            return new Thumbnailer($container->conf);
+        };
+
+        $container['httpAccess'] = function (): HttpAccess {
+            return new HttpAccess();
+        };
+
         return $container;
     }
 }
index deb071970a41a6872a0c87ca9ccffc6bccb04bd0..fec398d098a95d4ae9b8c0918d795086cba161c0 100644 (file)
@@ -9,11 +9,13 @@ use Shaarli\Config\ConfigManager;
 use Shaarli\Feed\FeedBuilder;
 use Shaarli\Formatter\FormatterFactory;
 use Shaarli\History;
+use Shaarli\Http\HttpAccess;
 use Shaarli\Plugin\PluginManager;
 use Shaarli\Render\PageBuilder;
 use Shaarli\Render\PageCacheManager;
 use Shaarli\Security\LoginManager;
 use Shaarli\Security\SessionManager;
+use Shaarli\Thumbnailer;
 use Slim\Container;
 
 /**
@@ -31,6 +33,8 @@ use Slim\Container;
  * @property FormatterFactory         $formatterFactory
  * @property PageCacheManager         $pageCacheManager
  * @property FeedBuilder              $feedBuilder
+ * @property Thumbnailer              $thumbnailer
+ * @property HttpAccess               $httpAccess
  */
 class ShaarliContainer extends Container
 {
diff --git a/application/front/controller/admin/PostBookmarkController.php b/application/front/controller/admin/PostBookmarkController.php
new file mode 100644 (file)
index 0000000..dbe570e
--- /dev/null
@@ -0,0 +1,258 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
+use Shaarli\Formatter\BookmarkMarkdownFormatter;
+use Shaarli\Thumbnailer;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Class PostBookmarkController
+ *
+ * Slim controller used to handle Shaarli create or edit bookmarks.
+ */
+class PostBookmarkController extends ShaarliAdminController
+{
+    /**
+     * GET /add-shaare - Displays the form used to create a new bookmark from an URL
+     */
+    public function addShaare(Request $request, Response $response): Response
+    {
+        $this->assignView(
+            'pagetitle',
+            t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli')
+        );
+
+        return $response->write($this->render('addlink'));
+    }
+
+    /**
+     * GET /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 (empty($title) && strpos(get_url_scheme($url) ?: '', 'http') !== false) {
+                $retrieveDescription = $this->container->conf->get('general.retrieve_description');
+                // Short timeout to keep the application responsive
+                // The callback will fill $charset and $title with data from the downloaded page.
+                $this->container->httpAccess->getHttpResponse(
+                    $url,
+                    $this->container->conf->get('general.download_timeout', 30),
+                    $this->container->conf->get('general.download_max_size', 4194304),
+                    $this->container->httpAccess->getCurlDownloadCallback(
+                        $charset,
+                        $title,
+                        $description,
+                        $tags,
+                        $retrieveDescription
+                    )
+                );
+                if (! empty($title) && strtolower($charset) !== 'utf-8') {
+                    $title = mb_convert_encoding($title, 'utf-8', $charset);
+                }
+            }
+
+            if (empty($url) && empty($title)) {
+                $title = $this->container->conf->get('general.default_note_title', t('Note: '));
+            }
+
+            $link = escape([
+                'title' => $title,
+                'url' => $url ?? '',
+                'description' => $description ?? '',
+                'tags' => $tags ?? '',
+                'private' => $private,
+            ]);
+        } else {
+            $formatter = $this->container->formatterFactory->getFormatter('raw');
+            $link = $formatter->format($bookmark);
+        }
+
+        return $this->displayForm($link, $linkIsNew, $request, $response);
+    }
+
+    /**
+     * GET /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($id);  // Read database
+        } catch (BookmarkNotFoundException $e) {
+            $this->saveErrorMessage(t('Bookmark not found'));
+
+            return $response->withRedirect('./');
+        }
+
+        $formatter = $this->container->formatterFactory->getFormatter('raw');
+        $link = $formatter->format($bookmark);
+
+        return $this->displayForm($link, false, $request, $response);
+    }
+
+    /**
+     * POST /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') ? 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
+            && false === $bookmark->isNote()
+        ) {
+            $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);
+        $data = $this->executeHooks('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('<script>self.close();</script>');
+        }
+
+        if (!empty($request->getParam('returnurl'))) {
+            $this->container->environment['HTTP_REFERER'] = escape($request->getParam('returnurl'));
+        }
+
+        return $this->redirectFromReferer(
+            $request,
+            $response,
+            ['add-shaare', 'shaare'], ['addlink', 'post', 'edit_link'],
+            $bookmark->getShortUrl()
+        );
+    }
+
+    public function deleteBookmark(Request $request, Response $response): Response
+    {
+        $this->checkToken($request);
+
+        $ids = escape(trim($request->getParam('lf_linkdate')));
+        if (strpos($ids, ' ') !== false) {
+            // multiple, space-separated ids provided
+            $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'strlen'));
+        } 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');
+        foreach ($ids as $id) {
+            $id = (int) $id;
+            // TODO: check if it exists
+            $bookmark = $this->container->bookmarkService->get($id);
+            $data = $formatter->format($bookmark);
+            $this->container->pluginManager->executeHooks('delete_link', $data);
+            $this->container->bookmarkService->remove($bookmark, false);
+        }
+
+        $this->container->bookmarkService->save();
+
+        // If we are called from the bookmarklet, we must close the popup:
+        if ($request->getParam('source') === 'bookmarklet') {
+            return $response->write('<script>self.close();</script>');
+        }
+
+        // Don't redirect to where we were previously because the datastore has changed.
+        return $response->withRedirect('./');
+    }
+
+    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 = [
+            'link' => $link,
+            'link_is_new' => $isNew,
+            'http_referer' => escape($this->container->environment['HTTP_REFERER'] ?? ''),
+            'source' => $request->getParam('source') ?? '',
+            'tags' => $tags,
+            'default_private_links' => $this->container->conf->get('privacy.default_private_links', false),
+        ];
+
+        $data = $this->executeHooks('render_editlink', $data);
+
+        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('editlink'));
+    }
+
+    /**
+     * @param mixed[] $data Variables passed to the template engine
+     *
+     * @return mixed[] Template data after active plugins render_picwall hook execution.
+     */
+    protected function executeHooks(string $hook, array $data): array
+    {
+        $this->container->pluginManager->executeHooks(
+            $hook,
+            $data
+        );
+
+        return $data;
+    }
+}
index 66db5ad9f526d51588a200157093b75ed9cdf126..d087f2cd7c330a8855331d2561685ba03ee18fdf 100644 (file)
@@ -21,7 +21,7 @@ class ToolsController extends ShaarliAdminController
             'sslenabled' => is_https($this->container->environment),
         ];
 
-        $this->executeHooks($data);
+        $data = $this->executeHooks($data);
 
         foreach ($data as $key => $value) {
             $this->assignView($key, $value);
index 47e2503a677093011c2fb542ee484e8781b89c85..e5c9ddaca03927c970c1468fe540ad288ad5f187 100644 (file)
@@ -71,7 +71,7 @@ class DailyController extends ShaarliVisitorController
         ];
 
         // Hooks are called before column construction so that plugins don't have to deal with columns.
-        $this->executeHooks($data);
+        $data = $this->executeHooks($data);
 
         $data['cols'] = $this->calculateColumns($data['linksToDisplay']);
 
index 70664635b454976e0875ecbba381308e13b74a78..f76f55fdf76cc5ff0f3a9a6ca30f3e1f1a30d1b0 100644 (file)
@@ -46,7 +46,7 @@ class FeedController extends ShaarliVisitorController
 
         $data = $this->container->feedBuilder->buildData($feedType, $request->getParams());
 
-        $this->executeHooks($data, $feedType);
+        $data = $this->executeHooks($data, $feedType);
         $this->assignAllView($data);
 
         $content = $this->render('feed.'. $feedType);
index f12915c135caaf92f57a5a44a2d4ffee11c335f0..98423d9000cfdca2958fcd685173974701074a49 100644 (file)
@@ -78,16 +78,16 @@ abstract class ShaarliVisitorController
         ];
 
         foreach ($common_hooks as $name) {
-            $plugin_data = [];
+            $pluginData = [];
             $this->container->pluginManager->executeHooks(
                 'render_' . $name,
-                $plugin_data,
+                $pluginData,
                 [
                     'target' => $template,
                     'loggedin' => $this->container->loginManager->isLoggedIn()
                 ]
             );
-            $this->assignView('plugins_' . $name, $plugin_data);
+            $this->assignView('plugins_' . $name, $pluginData);
         }
     }
 
@@ -102,9 +102,10 @@ abstract class ShaarliVisitorController
         Request $request,
         Response $response,
         array $loopTerms = [],
-        array $clearParams = []
+        array $clearParams = [],
+        string $anchor = null
     ): Response {
-        $defaultPath = $request->getUri()->getBasePath();
+        $defaultPath = rtrim($request->getUri()->getBasePath(), '/') . '/';
         $referer = $this->container->environment['HTTP_REFERER'] ?? null;
 
         if (null !== $referer) {
@@ -133,7 +134,8 @@ abstract class ShaarliVisitorController
         }
 
         $queryString = count($params) > 0 ? '?'. http_build_query($params) : '';
+        $anchor = $anchor ? '#' . $anchor : '';
 
-        return $response->withRedirect($path . $queryString);
+        return $response->withRedirect($path . $queryString . $anchor);
     }
 }
diff --git a/application/http/HttpAccess.php b/application/http/HttpAccess.php
new file mode 100644 (file)
index 0000000..81d9e07
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Http;
+
+/**
+ * Class HttpAccess
+ *
+ * This is mostly an OOP wrapper for HTTP functions defined in `HttpUtils`.
+ * It is used as dependency injection in Shaarli's container.
+ *
+ * @package Shaarli\Http
+ */
+class HttpAccess
+{
+    public function getHttpResponse($url, $timeout = 30, $maxBytes = 4194304, $curlWriteFunction = null)
+    {
+        return get_http_response($url, $timeout, $maxBytes, $curlWriteFunction);
+    }
+
+    public function getCurlDownloadCallback(
+        &$charset,
+        &$title,
+        &$description,
+        &$keywords,
+        $retrieveDescription,
+        $curlGetInfo = 'curl_getinfo'
+    ) {
+        return get_curl_download_callback(
+            $charset,
+            $title,
+            $description,
+            $keywords,
+            $retrieveDescription,
+            $curlGetInfo
+        );
+    }
+}
index f00c4336ad07fb24d3a798fc123f1dad24dbec77..4fc4e3dcff08f3457c7a2d541962c699979732c7 100644 (file)
@@ -484,3 +484,109 @@ function is_https($server)
 
     return ! empty($server['HTTPS']);
 }
+
+/**
+ * Get cURL callback function for CURLOPT_WRITEFUNCTION
+ *
+ * @param string $charset     to extract from the downloaded page (reference)
+ * @param string $title       to extract from the downloaded page (reference)
+ * @param string $description to extract from the downloaded page (reference)
+ * @param string $keywords    to extract from the downloaded page (reference)
+ * @param bool   $retrieveDescription Automatically tries to retrieve description and keywords from HTML content
+ * @param string $curlGetInfo Optionally overrides curl_getinfo function
+ *
+ * @return Closure
+ */
+function get_curl_download_callback(
+    &$charset,
+    &$title,
+    &$description,
+    &$keywords,
+    $retrieveDescription,
+    $curlGetInfo = 'curl_getinfo'
+) {
+    $isRedirected = false;
+    $currentChunk = 0;
+    $foundChunk = null;
+
+    /**
+     * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
+     *
+     * While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text'
+     * Then we extract the title and the charset and stop the download when it's done.
+     *
+     * @param resource $ch   cURL resource
+     * @param string   $data chunk of data being downloaded
+     *
+     * @return int|bool length of $data or false if we need to stop the download
+     */
+    return function (&$ch, $data) use (
+        $retrieveDescription,
+        $curlGetInfo,
+        &$charset,
+        &$title,
+        &$description,
+        &$keywords,
+        &$isRedirected,
+        &$currentChunk,
+        &$foundChunk
+    ) {
+        $currentChunk++;
+        $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
+        if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
+            $isRedirected = true;
+            return strlen($data);
+        }
+        if (!empty($responseCode) && $responseCode !== 200) {
+            return false;
+        }
+        // After a redirection, the content type will keep the previous request value
+        // until it finds the next content-type header.
+        if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) {
+            $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
+        }
+        if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
+            return false;
+        }
+        if (!empty($contentType) && empty($charset)) {
+            $charset = header_extract_charset($contentType);
+        }
+        if (empty($charset)) {
+            $charset = html_extract_charset($data);
+        }
+        if (empty($title)) {
+            $title = html_extract_title($data);
+            $foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
+        }
+        if ($retrieveDescription && empty($description)) {
+            $description = html_extract_tag('description', $data);
+            $foundChunk = ! empty($description) ? $currentChunk : $foundChunk;
+        }
+        if ($retrieveDescription && empty($keywords)) {
+            $keywords = html_extract_tag('keywords', $data);
+            if (! empty($keywords)) {
+                $foundChunk = $currentChunk;
+                // Keywords use the format tag1, tag2 multiple words, tag
+                // So we format them to match Shaarli's separator and glue multiple words with '-'
+                $keywords = implode(' ', array_map(function($keyword) {
+                    return implode('-', preg_split('/\s+/', trim($keyword)));
+                }, explode(',', $keywords)));
+            }
+        }
+
+        // We got everything we want, stop the download.
+        // If we already found either the title, description or keywords,
+        // it's highly unlikely that we'll found the other metas further than
+        // in the same chunk of data or the next one. So we also stop the download after that.
+        if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null
+            && (! $retrieveDescription
+                || $foundChunk < $currentChunk
+                || (!empty($title) && !empty($description) && !empty($keywords))
+            )
+        ) {
+            return false;
+        }
+
+        return strlen($data);
+    };
+}
index 9a16075a5f06e98cbbd0e3f26a5c138015889826..5c775d30ea34e168abf017a042fda4a5e6653bf1 100644 (file)
@@ -32,7 +32,7 @@ Here is a list :
 ```
 http://<replace_domain>/
 http://<replace_domain>/?nonope
-http://<replace_domain>/?do=addlink
+http://<replace_domain>/add-shaare
 http://<replace_domain>/?do=changepasswd
 http://<replace_domain>/?do=changetag
 http://<replace_domain>/configure
index 00e4a40be3014c256f887179076ea7dfcc98731c..fb528eeb61635e3bc4d620dc56bb2fb3e9dbe4ff 100644 (file)
--- a/index.php
+++ b/index.php
@@ -519,69 +519,20 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
 
     // -------- User wants to rename a tag or delete it
     if ($targetPage == Router::$PAGE_CHANGETAG) {
-        header('./manage-tags');
+        header('Location: ./manage-tags');
         exit;
     }
 
     // -------- User wants to add a link without using the bookmarklet: Show form.
     if ($targetPage == Router::$PAGE_ADDLINK) {
-        $PAGE->assign('pagetitle', t('Shaare a new link') .' - '. $conf->get('general.title', 'Shaarli'));
-        $PAGE->renderPage('addlink');
+        header('Location: ./shaare');
         exit;
     }
 
     // -------- User clicked the "Save" button when editing a link: Save link to database.
     if (isset($_POST['save_edit'])) {
-        // Go away!
-        if (! $sessionManager->checkToken($_POST['token'])) {
-            die(t('Wrong token.'));
-        }
-
-        // lf_id should only be present if the link exists.
-        $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : null;
-        if ($id && $bookmarkService->exists($id)) {
-            // Edit
-            $bookmark = $bookmarkService->get($id);
-        } else {
-            // New link
-            $bookmark = new Bookmark();
-        }
-
-        $bookmark->setTitle($_POST['lf_title']);
-        $bookmark->setDescription($_POST['lf_description']);
-        $bookmark->setUrl($_POST['lf_url'], $conf->get('security.allowed_protocols'));
-        $bookmark->setPrivate(isset($_POST['lf_private']));
-        $bookmark->setTagsString($_POST['lf_tags']);
-
-        if ($conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
-            && ! $bookmark->isNote()
-        ) {
-            $thumbnailer = new Thumbnailer($conf);
-            $bookmark->setThumbnail($thumbnailer->get($bookmark->getUrl()));
-        }
-        $bookmarkService->addOrSet($bookmark, false);
-
-        // To preserve backward compatibility with 3rd parties, plugins still use arrays
-        $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
-        $formatter = $factory->getFormatter('raw');
-        $data = $formatter->format($bookmark);
-        $pluginManager->executeHooks('save_link', $data);
-
-        $bookmark->fromArray($data);
-        $bookmarkService->set($bookmark);
-
-        // If we are called from the bookmarklet, we must close the popup:
-        if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
-            echo '<script>self.close();</script>';
-            exit;
-        }
-
-        $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?';
-        $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
-        // Scroll to the link which has been edited.
-        $location .= '#' . $bookmark->getShortUrl();
-        // After saving the link, redirect to the page the user was on.
-        header('Location: '. $location);
+        // This route is no longer supported in legacy mode
+        header('Location: ./');
         exit;
     }
 
@@ -695,110 +646,13 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
     // -------- User clicked the "EDIT" button on a link: Display link edit form.
     if (isset($_GET['edit_link'])) {
         $id = (int) escape($_GET['edit_link']);
-        try {
-            $link = $bookmarkService->get($id);  // Read database
-        } catch (BookmarkNotFoundException $e) {
-            // Link not found in database.
-            header('Location: ?');
-            exit;
-        }
-
-        $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
-        $formatter = $factory->getFormatter('raw');
-        $formattedLink = $formatter->format($link);
-        $tags = $bookmarkService->bookmarksCountPerTag();
-        if ($conf->get('formatter') === 'markdown') {
-            $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
-        }
-        $data = array(
-            'link' => $formattedLink,
-            'link_is_new' => false,
-            'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
-            'tags' => $tags,
-        );
-        $pluginManager->executeHooks('render_editlink', $data);
-
-        foreach ($data as $key => $value) {
-            $PAGE->assign($key, $value);
-        }
-
-        $PAGE->assign('pagetitle', t('Edit') .' '. t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
-        $PAGE->renderPage('editlink');
+        header('Location: ./shaare-' . $id);
         exit;
     }
 
     // -------- User want to post a new link: Display link edit form.
     if (isset($_GET['post'])) {
-        $url = cleanup_url($_GET['post']);
-
-        $link_is_new = false;
-        // Check if URL is not already in database (in this case, we will edit the existing link)
-        $bookmark = $bookmarkService->findByUrl($url);
-        if (! $bookmark) {
-            $link_is_new = true;
-            // Get title if it was provided in URL (by the bookmarklet).
-            $title = empty($_GET['title']) ? '' : escape($_GET['title']);
-            // Get description if it was provided in URL (by the bookmarklet). [Bronco added that]
-            $description = empty($_GET['description']) ? '' : escape($_GET['description']);
-            $tags = empty($_GET['tags']) ? '' : escape($_GET['tags']);
-            $private = !empty($_GET['private']) && $_GET['private'] === "1" ? 1 : 0;
-
-            // 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 (empty($title) && strpos(get_url_scheme($url), 'http') !== false) {
-                $retrieveDescription = $conf->get('general.retrieve_description');
-                // Short timeout to keep the application responsive
-                // The callback will fill $charset and $title with data from the downloaded page.
-                get_http_response(
-                    $url,
-                    $conf->get('general.download_timeout', 30),
-                    $conf->get('general.download_max_size', 4194304),
-                    get_curl_download_callback($charset, $title, $description, $tags, $retrieveDescription)
-                );
-                if (! empty($title) && strtolower($charset) != 'utf-8') {
-                    $title = mb_convert_encoding($title, 'utf-8', $charset);
-                }
-            }
-
-            if ($url == '') {
-                $title = $conf->get('general.default_note_title', t('Note: '));
-            }
-            $url = escape($url);
-            $title = escape($title);
-
-            $link = [
-                'title' => $title,
-                'url' => $url,
-                'description' => $description,
-                'tags' => $tags,
-                'private' => $private,
-            ];
-        } else {
-            $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
-            $formatter = $factory->getFormatter('raw');
-            $link = $formatter->format($bookmark);
-        }
-
-        $tags = $bookmarkService->bookmarksCountPerTag();
-        if ($conf->get('formatter') === 'markdown') {
-            $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
-        }
-        $data = [
-            'link' => $link,
-            'link_is_new' => $link_is_new,
-            'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
-            'source' => (isset($_GET['source']) ? $_GET['source'] : ''),
-            'tags' => $tags,
-            'default_private_links' => $conf->get('privacy.default_private_links', false),
-        ];
-        $pluginManager->executeHooks('render_editlink', $data);
-
-        foreach ($data as $key => $value) {
-            $PAGE->assign($key, $value);
-        }
-
-        $PAGE->assign('pagetitle', t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
-        $PAGE->renderPage('editlink');
+        header('Location: ./shaare?' . http_build_query($_GET));
         exit;
     }
 
@@ -1351,19 +1205,29 @@ $app->group('', function () {
     $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save')->setName('saveConfigure');
     $this->get('/manage-tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index')->setName('manageTag');
     $this->post('/manage-tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save')->setName('saveManageTag');
+    $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\PostBookmarkController:addShaare')->setName('addShaare');
+    $this
+        ->get('/shaare', '\Shaarli\Front\Controller\Admin\PostBookmarkController:displayCreateForm')
+        ->setName('newShaare');
+    $this
+        ->get('/shaare-{id}', '\Shaarli\Front\Controller\Admin\PostBookmarkController:displayEditForm')
+        ->setName('editShaare');
+    $this
+        ->post('/shaare', '\Shaarli\Front\Controller\Admin\PostBookmarkController:save')
+        ->setName('saveShaare');
+    $this
+        ->get('/delete-shaare', '\Shaarli\Front\Controller\Admin\PostBookmarkController:deleteBookmark')
+        ->setName('deleteShaare');
 
     $this
         ->get('/links-per-page', '\Shaarli\Front\Controller\Admin\SessionFilterController:linksPerPage')
-        ->setName('filter-links-per-page')
-    ;
+        ->setName('filter-links-per-page');
     $this
         ->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility')
-        ->setName('visibility')
-    ;
+        ->setName('visibility');
     $this
         ->get('/untagged-only', '\Shaarli\Front\Controller\Admin\SessionFilterController:untaggedOnly')
-        ->setName('untagged-only')
-    ;
+        ->setName('untagged-only');
 })->add('\Shaarli\Front\ShaarliMiddleware');
 
 $response = $app->run(true);
index 53197ae63e2559a3afd16f2494f71e07d12892e6..7dbe914ca5730faf754327e8f8024e149fcc1963 100644 (file)
@@ -10,11 +10,13 @@ use Shaarli\Config\ConfigManager;
 use Shaarli\Feed\FeedBuilder;
 use Shaarli\Formatter\FormatterFactory;
 use Shaarli\History;
+use Shaarli\Http\HttpAccess;
 use Shaarli\Plugin\PluginManager;
 use Shaarli\Render\PageBuilder;
 use Shaarli\Render\PageCacheManager;
 use Shaarli\Security\LoginManager;
 use Shaarli\Security\SessionManager;
+use Shaarli\Thumbnailer;
 
 /**
  * Test helper allowing auto-completion for MockObjects.
@@ -31,6 +33,8 @@ use Shaarli\Security\SessionManager;
  * @property MockObject|FormatterFactory         $formatterFactory
  * @property MockObject|PageCacheManager         $pageCacheManager
  * @property MockObject|FeedBuilder              $feedBuilder
+ * @property MockObject|Thumbnailer              $thumbnailer
+ * @property MockObject|HttpAccess               $httpAccess
  */
 class ShaarliTestContainer extends ShaarliContainer
 {
diff --git a/tests/front/controller/admin/PostBookmarkControllerTest.php b/tests/front/controller/admin/PostBookmarkControllerTest.php
new file mode 100644 (file)
index 0000000..f00a15c
--- /dev/null
@@ -0,0 +1,652 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Config\ConfigManager;
+use Shaarli\Front\Exception\WrongTokenException;
+use Shaarli\Http\HttpAccess;
+use Shaarli\Security\SessionManager;
+use Shaarli\Thumbnailer;
+use Slim\Http\Request;
+use Slim\Http\Response;
+use Slim\Http\Uri;
+
+class PostBookmarkControllerTest extends TestCase
+{
+    use FrontAdminControllerMockHelper;
+
+    /** @var PostBookmarkController */
+    protected $controller;
+
+    public function setUp(): void
+    {
+        $this->createContainer();
+
+        $this->container->httpAccess = $this->createMock(HttpAccess::class);
+        $this->controller = new PostBookmarkController($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']);
+    }
+
+    /**
+     * Test displaying bookmark create form
+     * Ensure that every step of the standard workflow works properly.
+     */
+    public function testDisplayCreateFormWithUrl(): void
+    {
+        $this->container->environment = [
+            'HTTP_REFERER' => $referer = 'http://shaarli/subfolder/controller/?searchtag=abc'
+        ];
+
+        $assignedVariables = [];
+        $this->assignTemplateVars($assignedVariables);
+
+        $url = 'http://url.tld/other?part=3&utm_ad=pay#hash';
+        $expectedUrl = str_replace('&utm_ad=pay', '', $url);
+        $remoteTitle = 'Remote Title';
+        $remoteDesc = 'Sometimes the meta description is relevant.';
+        $remoteTags = 'abc def';
+
+        $request = $this->createMock(Request::class);
+        $request->method('getParam')->willReturnCallback(function (string $key) use ($url): ?string {
+            return $key === 'post' ? $url : null;
+        });
+        $response = new Response();
+
+        $this->container->httpAccess
+            ->expects(static::once())
+            ->method('getCurlDownloadCallback')
+            ->willReturnCallback(
+                function (&$charset, &$title, &$description, &$tags) use (
+                    $remoteTitle,
+                    $remoteDesc,
+                    $remoteTags
+                ): callable {
+                    return function () use (
+                        &$charset,
+                        &$title,
+                        &$description,
+                        &$tags,
+                        $remoteTitle,
+                        $remoteDesc,
+                        $remoteTags
+                    ): void {
+                        $charset = 'ISO-8859-1';
+                        $title = $remoteTitle;
+                        $description = $remoteDesc;
+                        $tags = $remoteTags;
+                    };
+                }
+            )
+        ;
+        $this->container->httpAccess
+            ->expects(static::once())
+            ->method('getHttpResponse')
+            ->with($expectedUrl, 30, 4194304)
+            ->willReturnCallback(function($url, $timeout, $maxBytes, $callback): void {
+                $callback();
+            })
+        ;
+
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('bookmarksCountPerTag')
+            ->willReturn($tags = ['tag1' => 2, 'tag2' => 1])
+        ;
+
+        // Make sure that PluginManager hook is triggered
+        $this->container->pluginManager
+            ->expects(static::at(0))
+            ->method('executeHooks')
+            ->willReturnCallback(function (string $hook, array $data) use ($remoteTitle, $remoteDesc): array {
+                static::assertSame('render_editlink', $hook);
+                static::assertSame($remoteTitle, $data['link']['title']);
+                static::assertSame($remoteDesc, $data['link']['description']);
+
+                return $data;
+            })
+        ;
+
+        $result = $this->controller->displayCreateForm($request, $response);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('editlink', (string) $result->getBody());
+
+        static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']);
+
+        static::assertSame($expectedUrl, $assignedVariables['link']['url']);
+        static::assertSame($remoteTitle, $assignedVariables['link']['title']);
+        static::assertSame($remoteDesc, $assignedVariables['link']['description']);
+        static::assertSame($remoteTags, $assignedVariables['link']['tags']);
+        static::assertFalse($assignedVariables['link']['private']);
+
+        static::assertTrue($assignedVariables['link_is_new']);
+        static::assertSame($referer, $assignedVariables['http_referer']);
+        static::assertSame($tags, $assignedVariables['tags']);
+        static::assertArrayHasKey('source', $assignedVariables);
+        static::assertArrayHasKey('default_private_links', $assignedVariables);
+    }
+
+    /**
+     * Test displaying bookmark create form
+     * Ensure all available query parameters are handled properly.
+     */
+    public function testDisplayCreateFormWithFullParameters(): void
+    {
+        $assignedVariables = [];
+        $this->assignTemplateVars($assignedVariables);
+
+        $parameters = [
+            'post' => 'http://url.tld/other?part=3&utm_ad=pay#hash',
+            'title' => 'Provided Title',
+            'description' => 'Provided description.',
+            'tags' => 'abc def',
+            'private' => '1',
+            'source' => 'apps',
+        ];
+        $expectedUrl = str_replace('&utm_ad=pay', '', $parameters['post']);
+
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($parameters): ?string {
+            return $parameters[$key] ?? null;
+        });
+        $response = new Response();
+
+        $result = $this->controller->displayCreateForm($request, $response);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('editlink', (string) $result->getBody());
+
+        static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']);
+
+        static::assertSame($expectedUrl, $assignedVariables['link']['url']);
+        static::assertSame($parameters['title'], $assignedVariables['link']['title']);
+        static::assertSame($parameters['description'], $assignedVariables['link']['description']);
+        static::assertSame($parameters['tags'], $assignedVariables['link']['tags']);
+        static::assertTrue($assignedVariables['link']['private']);
+        static::assertTrue($assignedVariables['link_is_new']);
+        static::assertSame($parameters['source'], $assignedVariables['source']);
+    }
+
+    /**
+     * Test displaying bookmark create form
+     * Without any parameter.
+     */
+    public function testDisplayCreateFormEmpty(): void
+    {
+        $assignedVariables = [];
+        $this->assignTemplateVars($assignedVariables);
+
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
+        $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
+
+        $result = $this->controller->displayCreateForm($request, $response);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('editlink', (string) $result->getBody());
+        static::assertSame('', $assignedVariables['link']['url']);
+        static::assertSame('Note: ', $assignedVariables['link']['title']);
+        static::assertSame('', $assignedVariables['link']['description']);
+        static::assertSame('', $assignedVariables['link']['tags']);
+        static::assertFalse($assignedVariables['link']['private']);
+        static::assertTrue($assignedVariables['link_is_new']);
+    }
+
+    /**
+     * Test displaying bookmark create form
+     * URL not using HTTP protocol: do not try to retrieve the title
+     */
+    public function testDisplayCreateFormNotHttp(): void
+    {
+        $assignedVariables = [];
+        $this->assignTemplateVars($assignedVariables);
+
+        $url = 'magnet://kubuntu.torrent';
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($url): ?string {
+                return $key === 'post' ? $url : null;
+            });
+        $response = new Response();
+
+        $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
+        $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
+
+        $result = $this->controller->displayCreateForm($request, $response);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('editlink', (string) $result->getBody());
+        static::assertSame($url, $assignedVariables['link']['url']);
+        static::assertTrue($assignedVariables['link_is_new']);
+    }
+
+    /**
+     * Test displaying bookmark create form
+     * When markdown formatter is enabled, the no markdown tag should be added to existing tags.
+     */
+    public function testDisplayCreateFormWithMarkdownEnabled(): void
+    {
+        $assignedVariables = [];
+        $this->assignTemplateVars($assignedVariables);
+
+        $this->container->conf = $this->createMock(ConfigManager::class);
+        $this->container->conf
+            ->expects(static::atLeastOnce())
+            ->method('get')->willReturnCallback(function (string $key): ?string {
+                if ($key === 'formatter') {
+                    return 'markdown';
+                }
+
+                return $key;
+            })
+        ;
+
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $result = $this->controller->displayCreateForm($request, $response);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('editlink', (string) $result->getBody());
+        static::assertSame(['nomarkdown' => 1], $assignedVariables['tags']);
+    }
+
+    /**
+     * Test displaying bookmark create form
+     * When an existing URL is submitted, we want to edit the existing link.
+     */
+    public function testDisplayCreateFormWithExistingUrl(): void
+    {
+        $assignedVariables = [];
+        $this->assignTemplateVars($assignedVariables);
+
+        $url = 'http://url.tld/other?part=3&utm_ad=pay#hash';
+        $expectedUrl = str_replace('&utm_ad=pay', '', $url);
+
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($url): ?string {
+                return $key === 'post' ? $url : null;
+            });
+        $response = new Response();
+
+        $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
+        $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
+
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('findByUrl')
+            ->with($expectedUrl)
+            ->willReturn(
+                (new Bookmark())
+                    ->setId($id = 23)
+                    ->setUrl($expectedUrl)
+                    ->setTitle($title = 'Bookmark Title')
+                    ->setDescription($description = 'Bookmark description.')
+                    ->setTags($tags = ['abc', 'def'])
+                    ->setPrivate(true)
+                    ->setCreated($createdAt = new \DateTime('2020-06-10 18:45:44'))
+            )
+        ;
+
+        $result = $this->controller->displayCreateForm($request, $response);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('editlink', (string) $result->getBody());
+
+        static::assertSame('Edit Shaare - Shaarli', $assignedVariables['pagetitle']);
+        static::assertFalse($assignedVariables['link_is_new']);
+
+        static::assertSame($id, $assignedVariables['link']['id']);
+        static::assertSame($expectedUrl, $assignedVariables['link']['url']);
+        static::assertSame($title, $assignedVariables['link']['title']);
+        static::assertSame($description, $assignedVariables['link']['description']);
+        static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
+        static::assertTrue($assignedVariables['link']['private']);
+        static::assertSame($createdAt, $assignedVariables['link']['created']);
+    }
+
+    /**
+     * Test displaying bookmark edit form
+     * When an existing ID is provided, ensure that default workflow works properly.
+     */
+    public function testDisplayEditFormDefault(): void
+    {
+        $assignedVariables = [];
+        $this->assignTemplateVars($assignedVariables);
+
+        $id = 11;
+
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
+        $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
+
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('get')
+            ->with($id)
+            ->willReturn(
+                (new Bookmark())
+                    ->setId($id)
+                    ->setUrl($url = 'http://domain.tld')
+                    ->setTitle($title = 'Bookmark Title')
+                    ->setDescription($description = 'Bookmark description.')
+                    ->setTags($tags = ['abc', 'def'])
+                    ->setPrivate(true)
+                    ->setCreated($createdAt = new \DateTime('2020-06-10 18:45:44'))
+            )
+        ;
+
+        $result = $this->controller->displayEditForm($request, $response, ['id' => (string) $id]);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('editlink', (string) $result->getBody());
+
+        static::assertSame('Edit Shaare - Shaarli', $assignedVariables['pagetitle']);
+        static::assertFalse($assignedVariables['link_is_new']);
+
+        static::assertSame($id, $assignedVariables['link']['id']);
+        static::assertSame($url, $assignedVariables['link']['url']);
+        static::assertSame($title, $assignedVariables['link']['title']);
+        static::assertSame($description, $assignedVariables['link']['description']);
+        static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
+        static::assertTrue($assignedVariables['link']['private']);
+        static::assertSame($createdAt, $assignedVariables['link']['created']);
+    }
+
+    /**
+     * Test save a new bookmark
+     */
+    public function testSaveBookmark(): void
+    {
+        $id = 21;
+        $parameters = [
+            'lf_url' => 'http://url.tld/other?part=3#hash',
+            'lf_title' => 'Provided Title',
+            'lf_description' => 'Provided description.',
+            'lf_tags' => 'abc def',
+            'lf_private' => '1',
+            'returnurl' => 'http://shaarli.tld/subfolder/add-shaare'
+        ];
+
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($parameters): ?string {
+                return $parameters[$key] ?? null;
+            })
+        ;
+        $request->method('getUri')->willReturnCallback(function (): Uri {
+            $uri = $this->createMock(Uri::class);
+            $uri->method('getBasePath')->willReturn('/subfolder');
+
+            return $uri;
+        });
+        $response = new Response();
+
+        $checkBookmark = function (Bookmark $bookmark) use ($parameters) {
+            static::assertSame($parameters['lf_url'], $bookmark->getUrl());
+            static::assertSame($parameters['lf_title'], $bookmark->getTitle());
+            static::assertSame($parameters['lf_description'], $bookmark->getDescription());
+            static::assertSame($parameters['lf_tags'], $bookmark->getTagsString());
+            static::assertTrue($bookmark->isPrivate());
+        };
+
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('addOrSet')
+            ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
+                static::assertFalse($save);
+
+                $checkBookmark($bookmark);
+
+                $bookmark->setId($id);
+            })
+        ;
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('set')
+            ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
+                static::assertTrue($save);
+
+                $checkBookmark($bookmark);
+
+                static::assertSame($id, $bookmark->getId());
+            })
+        ;
+
+        // Make sure that PluginManager hook is triggered
+        $this->container->pluginManager
+            ->expects(static::at(0))
+            ->method('executeHooks')
+            ->willReturnCallback(function (string $hook, array $data) use ($parameters, $id): array {
+                static::assertSame('save_link', $hook);
+
+                static::assertSame($id, $data['id']);
+                static::assertSame($parameters['lf_url'], $data['url']);
+                static::assertSame($parameters['lf_title'], $data['title']);
+                static::assertSame($parameters['lf_description'], $data['description']);
+                static::assertSame($parameters['lf_tags'], $data['tags']);
+                static::assertTrue($data['private']);
+
+                return $data;
+            })
+        ;
+
+        $result = $this->controller->save($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertRegExp('@/subfolder/#\w{6}@', $result->getHeader('location')[0]);
+    }
+
+
+    /**
+     * Test save an existing bookmark
+     */
+    public function testSaveExistingBookmark(): void
+    {
+        $id = 21;
+        $parameters = [
+            'lf_id' => (string) $id,
+            'lf_url' => 'http://url.tld/other?part=3#hash',
+            'lf_title' => 'Provided Title',
+            'lf_description' => 'Provided description.',
+            'lf_tags' => 'abc def',
+            'lf_private' => '1',
+            'returnurl' => 'http://shaarli.tld/subfolder/?page=2'
+        ];
+
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($parameters): ?string {
+                return $parameters[$key] ?? null;
+            })
+        ;
+        $request->method('getUri')->willReturnCallback(function (): Uri {
+            $uri = $this->createMock(Uri::class);
+            $uri->method('getBasePath')->willReturn('/subfolder');
+
+            return $uri;
+        });
+        $response = new Response();
+
+        $checkBookmark = function (Bookmark $bookmark) use ($parameters, $id) {
+            static::assertSame($id, $bookmark->getId());
+            static::assertSame($parameters['lf_url'], $bookmark->getUrl());
+            static::assertSame($parameters['lf_title'], $bookmark->getTitle());
+            static::assertSame($parameters['lf_description'], $bookmark->getDescription());
+            static::assertSame($parameters['lf_tags'], $bookmark->getTagsString());
+            static::assertTrue($bookmark->isPrivate());
+        };
+
+        $this->container->bookmarkService->expects(static::atLeastOnce())->method('exists')->willReturn(true);
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('get')
+            ->willReturn((new Bookmark())->setId($id)->setUrl('http://other.url'))
+        ;
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('addOrSet')
+            ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
+                static::assertFalse($save);
+
+                $checkBookmark($bookmark);
+            })
+        ;
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('set')
+            ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
+                static::assertTrue($save);
+
+                $checkBookmark($bookmark);
+
+                static::assertSame($id, $bookmark->getId());
+            })
+        ;
+
+        // Make sure that PluginManager hook is triggered
+        $this->container->pluginManager
+            ->expects(static::at(0))
+            ->method('executeHooks')
+            ->willReturnCallback(function (string $hook, array $data) use ($parameters, $id): array {
+                static::assertSame('save_link', $hook);
+
+                static::assertSame($id, $data['id']);
+                static::assertSame($parameters['lf_url'], $data['url']);
+                static::assertSame($parameters['lf_title'], $data['title']);
+                static::assertSame($parameters['lf_description'], $data['description']);
+                static::assertSame($parameters['lf_tags'], $data['tags']);
+                static::assertTrue($data['private']);
+
+                return $data;
+            })
+        ;
+
+        $result = $this->controller->save($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertRegExp('@/subfolder/\?page=2#\w{6}@', $result->getHeader('location')[0]);
+    }
+
+    /**
+     * Test save a bookmark - try to retrieve the thumbnail
+     */
+    public function testSaveBookmarkWithThumbnail(): void
+    {
+        $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash'];
+
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($parameters): ?string {
+                return $parameters[$key] ?? null;
+            })
+        ;
+        $request->method('getUri')->willReturnCallback(function (): Uri {
+            $uri = $this->createMock(Uri::class);
+            $uri->method('getBasePath')->willReturn('/subfolder');
+
+            return $uri;
+        });
+        $response = new Response();
+
+        $this->container->conf = $this->createMock(ConfigManager::class);
+        $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
+            return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default;
+        });
+
+        $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
+        $this->container->thumbnailer
+            ->expects(static::once())
+            ->method('get')
+            ->with($parameters['lf_url'])
+            ->willReturn($thumb = 'http://thumb.url')
+        ;
+
+        $this->container->bookmarkService
+            ->expects(static::once())
+            ->method('addOrSet')
+            ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($thumb): void {
+                static::assertSame($thumb, $bookmark->getThumbnail());
+            })
+        ;
+
+        $result = $this->controller->save($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+    }
+
+    /**
+     * Change the password with a wrong existing password
+     */
+    public function testSaveBookmarkFromBookmarklet(): void
+    {
+        $parameters = ['source' => 'bookmarklet'];
+
+        $request = $this->createMock(Request::class);
+        $request
+            ->method('getParam')
+            ->willReturnCallback(function (string $key) use ($parameters): ?string {
+                return $parameters[$key] ?? null;
+            })
+        ;
+        $response = new Response();
+
+        $result = $this->controller->save($request, $response);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('<script>self.close();</script>', (string) $result->getBody());
+    }
+
+    /**
+     * Change the password with a wrong existing password
+     */
+    public function testSaveBookmarkWrongToken(): void
+    {
+        $this->container->sessionManager = $this->createMock(SessionManager::class);
+        $this->container->sessionManager->method('checkToken')->willReturn(false);
+
+        $this->container->bookmarkService->expects(static::never())->method('addOrSet');
+        $this->container->bookmarkService->expects(static::never())->method('set');
+
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $this->expectException(WrongTokenException::class);
+
+        $this->controller->save($request, $response);
+    }
+}
index b4b4a0ec1e3a5443d6923dc72fca9535806ba132..999d2f4d2f3e657d1255bb8dabbab7962d6bd990 100644 (file)
@@ -9,7 +9,7 @@
   <div class="pure-u-lg-1-3 pure-u-1-24"></div>
   <div id="addlink-form" class="page-form  page-form-light pure-u-lg-1-3 pure-u-22-24">
     <h2 class="window-title">{"Shaare a new link"|t}</h2>
-    <form method="GET" action="#" name="addform" class="addform">
+    <form method="GET" action="./shaare" name="addform" class="addform">
       <div>
         <label for="shaare">{'URL or leave empty to post a note'|t}</label>
         <input type="text" name="post" id="shaare" class="autofocus">
index d16059a39b2dcfa204ddbc0ddff742eb812c7710..9f6d6b74dbf672a329edeb066918c28fb48e9989 100644 (file)
@@ -7,7 +7,11 @@
   {include="page.header"}
   <div id="editlinkform" class="edit-link-container" class="pure-g">
     <div class="pure-u-lg-1-5 pure-u-1-24"></div>
-    <form method="post" name="linkform" class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light">
+    <form method="post"
+          name="linkform"
+          action="./shaare"
+          class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light"
+    >
       <h2 class="window-title">
         {if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if}
       </h2>
index bde5036d1b603dd6514e72dfaf25f2dd3a99c140..cf59e89deab5c1031edd84646d715e40d62a6877 100644 (file)
@@ -21,7 +21,7 @@
         </li>
         {if="$is_logged_in || $openshaarli"}
           <li class="pure-menu-item">
-            <a href="./?do=addlink" class="pure-menu-link" id="shaarli-menu-shaare">
+            <a href="./add-shaare" class="pure-menu-link" id="shaarli-menu-shaare">
               <i class="fa fa-plus" aria-hidden="true"></i> {'Shaare'|t}
             </a>
           </li>
index da50f45e208029a704e24dd0bf44a31c4a73ddd1..13dbb36e8963de201753540eef7fea2f7f119e2b 100644 (file)
@@ -5,7 +5,7 @@
 <div id="pageheader">
        {include="page.header"}
        <div id="headerform">
-               <form method="GET" action="" name="addform" class="addform">
+               <form method="GET" action="./shaare" name="addform" class="addform">
                        <input type="text" name="post" class="linkurl">
                        <input type="submit" value="Add link" class="bigbutton">
                </form>
index c265d6d0a8a972df53bdb111f80ef4e1cbae9c94..8f9b6cc5aec2f7e124e209102d600b9b6f8ed363 100644 (file)
     {if="$is_logged_in"}
     <li><a href="./logout">Logout</a></li>
     <li><a href="./tools">Tools</a></li>
-    <li><a href="?do=addlink">Add link</a></li>
+    <li><a href="./add-shaare">Add link</a></li>
     {elseif="$openshaarli"}
     <li><a href="./tools">Tools</a></li>
-    <li><a href="./?do=addlink">Add link</a></li>
+    <li><a href="./add-shaare">Add link</a></li>
     {else}
     <li><a href="./login">Login</a></li>
     {/if}