From bee33239ed444f9724422fe5234cd79997500519 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 23 Jan 2020 22:26:38 +0100 Subject: Fix all relative link to work with new URL --- application/legacy/LegacyUpdater.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'application') diff --git a/application/legacy/LegacyUpdater.php b/application/legacy/LegacyUpdater.php index 3a5de79f..8d5cd071 100644 --- a/application/legacy/LegacyUpdater.php +++ b/application/legacy/LegacyUpdater.php @@ -10,9 +10,9 @@ use ReflectionMethod; use Shaarli\ApplicationUtils; use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\BookmarkArray; -use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\BookmarkFilter; use Shaarli\Bookmark\BookmarkIO; +use Shaarli\Bookmark\LinkDB; use Shaarli\Config\ConfigJson; use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigPhp; @@ -534,7 +534,7 @@ class LegacyUpdater if ($thumbnailsEnabled) { $this->session['warnings'][] = t( - 'You have enabled or changed thumbnails mode. Please synchronize them.' + 'You have enabled or changed thumbnails mode. Please synchronize them.' ); } -- cgit v1.2.3 From 485b168a9677d160b0c0426e4f282b9bd0c632c1 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 26 Jan 2020 11:15:15 +0100 Subject: Process picwall rendering through Slim controller + UT --- application/bookmark/Bookmark.php | 2 +- application/container/ContainerBuilder.php | 5 ++ application/container/ShaarliContainer.php | 2 + .../front/controllers/PictureWallController.php | 72 ++++++++++++++++++++++ .../exceptions/ThumbnailsDisabledException.php | 15 +++++ application/updater/Updater.php | 18 +++++- 6 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 application/front/controllers/PictureWallController.php create mode 100644 application/front/exceptions/ThumbnailsDisabledException.php (limited to 'application') diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php index f9b21d3d..83ddab82 100644 --- a/application/bookmark/Bookmark.php +++ b/application/bookmark/Bookmark.php @@ -346,7 +346,7 @@ class Bookmark /** * Get the Thumbnail. * - * @return string|bool + * @return string|bool|null */ public function getThumbnail() { diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index e2c78ccc..99c12334 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -7,6 +7,7 @@ namespace Shaarli\Container; use Shaarli\Bookmark\BookmarkFileService; use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Config\ConfigManager; +use Shaarli\Formatter\FormatterFactory; use Shaarli\History; use Shaarli\Plugin\PluginManager; use Shaarli\Render\PageBuilder; @@ -76,6 +77,10 @@ class ContainerBuilder return new PluginManager($container->conf); }; + $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory { + return new FormatterFactory($container->conf, $container->loginManager->isLoggedIn()); + }; + return $container; } } diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php index 3fa9116e..fdf2f77f 100644 --- a/application/container/ShaarliContainer.php +++ b/application/container/ShaarliContainer.php @@ -6,6 +6,7 @@ namespace Shaarli\Container; use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Config\ConfigManager; +use Shaarli\Formatter\FormatterFactory; use Shaarli\History; use Shaarli\Plugin\PluginManager; use Shaarli\Render\PageBuilder; @@ -23,6 +24,7 @@ use Slim\Container; * @property BookmarkServiceInterface $bookmarkService * @property PageBuilder $pageBuilder * @property PluginManager $pluginManager + * @property FormatterFactory $formatterFactory */ class ShaarliContainer extends Container { diff --git a/application/front/controllers/PictureWallController.php b/application/front/controllers/PictureWallController.php new file mode 100644 index 00000000..08d31b29 --- /dev/null +++ b/application/front/controllers/PictureWallController.php @@ -0,0 +1,72 @@ +container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) { + throw new ThumbnailsDisabledException(); + } + + $this->assignView( + 'pagetitle', + t('Picture wall') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + + // Optionally filter the results: + $links = $this->container->bookmarkService->search($request->getQueryParams()); + $linksToDisplay = []; + + // Get only bookmarks which have a thumbnail. + // Note: we do not retrieve thumbnails here, the request is too heavy. + $formatter = $this->container->formatterFactory->getFormatter('raw'); + foreach ($links as $key => $link) { + if (!empty($link->getThumbnail())) { + $linksToDisplay[] = $formatter->format($link); + } + } + + $data = $this->executeHooks($linksToDisplay); + foreach ($data as $key => $value) { + $this->assignView($key, $value); + } + + return $response->write($this->render('picwall')); + } + + /** + * @param mixed[] $linksToDisplay List of formatted bookmarks + * + * @return mixed[] Template data after active plugins render_picwall hook execution. + */ + protected function executeHooks(array $linksToDisplay): array + { + $data = [ + 'linksToDisplay' => $linksToDisplay, + ]; + $this->container->pluginManager->executeHooks( + 'render_picwall', + $data, + ['loggedin' => $this->container->loginManager->isLoggedIn()] + ); + + return $data; + } +} diff --git a/application/front/exceptions/ThumbnailsDisabledException.php b/application/front/exceptions/ThumbnailsDisabledException.php new file mode 100644 index 00000000..1b9cf5b7 --- /dev/null +++ b/application/front/exceptions/ThumbnailsDisabledException.php @@ -0,0 +1,15 @@ +doneUpdates; } + + /** + * With the Slim routing system, default header link should be `./` instead of `?`. + * Otherwise you can not go back to the home page. Example: `/picture-wall` -> `/picture-wall?` instead of `/`. + */ + public function updateMethodRelativeHomeLink(): bool + { + $link = trim($this->conf->get('general.header_link')); + if ($link[0] === '?') { + $link = './'. ltrim($link, '?'); + + $this->conf->set('general.header_link', $link, true, true); + } + + return true; + } } -- cgit v1.2.3 From b0428aa9b02b058b72c40b6e8dc2298d55bf692f Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 23 Jan 2020 21:13:41 +0100 Subject: Migrate cache purge function to a proper class And update dependencies and tests. Note that SESSION['tags'] has been removed a log ago --- application/bookmark/BookmarkFileService.php | 7 ++++- application/bookmark/BookmarkIO.php | 2 -- application/feed/Cache.php | 38 ----------------------- application/legacy/LegacyLinkDB.php | 4 ++- application/render/PageCacheManager.php | 45 ++++++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 42 deletions(-) delete mode 100644 application/feed/Cache.php create mode 100644 application/render/PageCacheManager.php (limited to 'application') diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 9c59e139..fef998fd 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -12,6 +12,7 @@ use Shaarli\Formatter\BookmarkMarkdownFormatter; use Shaarli\History; use Shaarli\Legacy\LegacyLinkDB; use Shaarli\Legacy\LegacyUpdater; +use Shaarli\Render\PageCacheManager; use Shaarli\Updater\UpdaterUtils; /** @@ -39,6 +40,9 @@ class BookmarkFileService implements BookmarkServiceInterface /** @var History instance */ protected $history; + /** @var PageCacheManager instance */ + protected $pageCacheManager; + /** @var bool true for logged in users. Default value to retrieve private bookmarks. */ protected $isLoggedIn; @@ -49,6 +53,7 @@ class BookmarkFileService implements BookmarkServiceInterface { $this->conf = $conf; $this->history = $history; + $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache')); $this->bookmarksIO = new BookmarkIO($this->conf); $this->isLoggedIn = $isLoggedIn; @@ -275,7 +280,7 @@ class BookmarkFileService implements BookmarkServiceInterface } $this->bookmarks->reorder(); $this->bookmarksIO->write($this->bookmarks); - invalidateCaches($this->conf->get('resource.page_cache')); + $this->pageCacheManager->invalidateCaches(); } /** diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php index ae9ffcb4..1026e2f9 100644 --- a/application/bookmark/BookmarkIO.php +++ b/application/bookmark/BookmarkIO.php @@ -102,7 +102,5 @@ class BookmarkIO $this->datastore, self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix ); - - invalidateCaches($this->conf->get('resource.page_cache')); } } diff --git a/application/feed/Cache.php b/application/feed/Cache.php deleted file mode 100644 index e5d43e61..00000000 --- a/application/feed/Cache.php +++ /dev/null @@ -1,38 +0,0 @@ -write(); - invalidateCaches($pageCacheDir); + $pageCacheManager = new PageCacheManager($pageCacheDir); + $pageCacheManager->invalidateCaches(); } /** diff --git a/application/render/PageCacheManager.php b/application/render/PageCacheManager.php new file mode 100644 index 00000000..bd91fe0d --- /dev/null +++ b/application/render/PageCacheManager.php @@ -0,0 +1,45 @@ +pageCacheDir = $pageCacheDir; + } + + /** + * Purges all cached pages + * + * @return string|null an error string if the directory is missing + */ + public function purgeCachedPages(): ?string + { + if (!is_dir($this->pageCacheDir)) { + $error = sprintf(t('Cannot purge %s: no directory'), $this->pageCacheDir); + error_log($error); + + return $error; + } + + array_map('unlink', glob($this->pageCacheDir . '/*.cache')); + + return null; + } + + /** + * Invalidates caches when the database is changed or the user logs out. + */ + public function invalidateCaches(): void + { + // Purge page cache shared by sessions. + $this->purgeCachedPages(); + } +} -- cgit v1.2.3 From 8e47af2b3620c920116ec056173277c039163ec1 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 23 Jan 2020 21:52:03 +0100 Subject: Process logout through Slim controller --- application/container/ContainerBuilder.php | 20 ++++++++++++-- application/container/ShaarliContainer.php | 3 +++ application/front/controllers/LogoutController.php | 31 ++++++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 application/front/controllers/LogoutController.php (limited to 'application') diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index 99c12334..c5c4a2c3 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -11,6 +11,7 @@ use Shaarli\Formatter\FormatterFactory; use Shaarli\History; use Shaarli\Plugin\PluginManager; use Shaarli\Render\PageBuilder; +use Shaarli\Render\PageCacheManager; use Shaarli\Security\LoginManager; use Shaarli\Security\SessionManager; @@ -34,19 +35,30 @@ class ContainerBuilder /** @var LoginManager */ protected $login; - public function __construct(ConfigManager $conf, SessionManager $session, LoginManager $login) - { + /** @var string */ + protected $webPath; + + public function __construct( + ConfigManager $conf, + SessionManager $session, + LoginManager $login, + string $webPath + ) { $this->conf = $conf; $this->session = $session; $this->login = $login; + $this->webPath = $webPath; } public function build(): ShaarliContainer { $container = new ShaarliContainer(); + $container['conf'] = $this->conf; $container['sessionManager'] = $this->session; $container['loginManager'] = $this->login; + $container['webPath'] = $this->webPath; + $container['plugins'] = function (ShaarliContainer $container): PluginManager { return new PluginManager($container->conf); }; @@ -81,6 +93,10 @@ class ContainerBuilder return new FormatterFactory($container->conf, $container->loginManager->isLoggedIn()); }; + $container['pageCacheManager'] = function (ShaarliContainer $container): PageCacheManager { + return new PageCacheManager($container->conf->get('resource.page_cache')); + }; + return $container; } } diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php index fdf2f77f..af62e574 100644 --- a/application/container/ShaarliContainer.php +++ b/application/container/ShaarliContainer.php @@ -10,6 +10,7 @@ use Shaarli\Formatter\FormatterFactory; use Shaarli\History; use Shaarli\Plugin\PluginManager; use Shaarli\Render\PageBuilder; +use Shaarli\Render\PageCacheManager; use Shaarli\Security\LoginManager; use Shaarli\Security\SessionManager; use Slim\Container; @@ -20,11 +21,13 @@ use Slim\Container; * @property ConfigManager $conf * @property SessionManager $sessionManager * @property LoginManager $loginManager + * @property string $webPath * @property History $history * @property BookmarkServiceInterface $bookmarkService * @property PageBuilder $pageBuilder * @property PluginManager $pluginManager * @property FormatterFactory $formatterFactory + * @property PageCacheManager $pageCacheManager */ class ShaarliContainer extends Container { diff --git a/application/front/controllers/LogoutController.php b/application/front/controllers/LogoutController.php new file mode 100644 index 00000000..aba078c3 --- /dev/null +++ b/application/front/controllers/LogoutController.php @@ -0,0 +1,31 @@ +container->pageCacheManager->invalidateCaches(); + $this->container->sessionManager->logout(); + + // TODO: switch to a simple Cookie manager allowing to check the session, and create mocks. + setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, $this->container->webPath); + + return $response->withRedirect('./'); + } +} -- cgit v1.2.3 From 03340c18ead651ef9e11f883745695f2edafbae3 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 12 May 2020 12:44:48 +0200 Subject: Slim router: handle add tag route --- application/bookmark/LinkUtils.php | 2 +- application/container/ShaarliContainer.php | 1 + .../formatter/BookmarkMarkdownFormatter.php | 4 +- application/front/controllers/TagController.php | 74 ++++++++++++++++++++++ 4 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 application/front/controllers/TagController.php (limited to 'application') diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index 88379430..98d9038a 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php @@ -220,7 +220,7 @@ function hashtag_autolink($description, $indexUrl = '') * \p{Mn} - any non marking space (accents, umlauts, etc) */ $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; - $replacement = '$1#$2'; + $replacement = '$1#$2'; return preg_replace($regex, $replacement, $description); } diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php index af62e574..3995f669 100644 --- a/application/container/ShaarliContainer.php +++ b/application/container/ShaarliContainer.php @@ -18,6 +18,7 @@ use Slim\Container; /** * Extension of Slim container to document the injected objects. * + * @property mixed[] $environment $_SERVER automatically injected by Slim * @property ConfigManager $conf * @property SessionManager $sessionManager * @property LoginManager $loginManager diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php index 077e5312..5d244d4c 100644 --- a/application/formatter/BookmarkMarkdownFormatter.php +++ b/application/formatter/BookmarkMarkdownFormatter.php @@ -114,7 +114,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter /** * Replace hashtag in Markdown links format - * E.g. `#hashtag` becomes `[#hashtag](?addtag=hashtag)` + * E.g. `#hashtag` becomes `[#hashtag](./add-tag/hashtag)` * It includes the index URL if specified. * * @param string $description @@ -133,7 +133,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter * \p{Mn} - any non marking space (accents, umlauts, etc) */ $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; - $replacement = '$1[#$2]('. $indexUrl .'?addtag=$2)'; + $replacement = '$1[#$2]('. $indexUrl .'./add-tag/$2)'; $descriptionLines = explode(PHP_EOL, $description); $descriptionOut = ''; diff --git a/application/front/controllers/TagController.php b/application/front/controllers/TagController.php new file mode 100644 index 00000000..598275b0 --- /dev/null +++ b/application/front/controllers/TagController.php @@ -0,0 +1,74 @@ +container->environment['HTTP_REFERER'] ?? null; + + // In case browser does not send HTTP_REFERER, we search a single tag + if (null === $referer) { + if (null !== $newTag) { + return $response->withRedirect('./?searchtags='. urlencode($newTag)); + } + + return $response->withRedirect('./'); + } + + $currentUrl = parse_url($this->container->environment['HTTP_REFERER']); + parse_str($currentUrl['query'] ?? '', $params); + + if (null === $newTag) { + return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); + } + + // Prevent redirection loop + if (isset($params['addtag'])) { + unset($params['addtag']); + } + + // Check if this tag is already in the search query and ignore it if it is. + // Each tag is always separated by a space + $currentTags = isset($params['searchtags']) ? explode(' ', $params['searchtags']) : []; + + $addtag = true; + foreach ($currentTags as $value) { + if ($value === $newTag) { + $addtag = false; + break; + } + } + + // Append the tag if necessary + if (true === $addtag) { + $currentTags[] = trim($newTag); + } + + $params['searchtags'] = trim(implode(' ', $currentTags)); + + // We also remove page (keeping the same page has no sense, since the results are different) + unset($params['page']); + + return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); + } +} -- cgit v1.2.3 From c266a89d0fbb0d60d2d7df0ec171b7cb022224f6 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 26 Jan 2020 14:35:25 +0100 Subject: Process tag cloud page through Slim controller --- application/Utils.php | 2 +- .../front/controllers/TagCloudController.php | 89 ++++++++++++++++++++++ application/security/SessionManager.php | 10 +++ 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 application/front/controllers/TagCloudController.php (limited to 'application') diff --git a/application/Utils.php b/application/Utils.php index 4b7fc546..4e97cdda 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -87,7 +87,7 @@ function endsWith($haystack, $needle, $case = true) * * @param mixed $input Data to escape: a single string or an array of strings. * - * @return string escaped. + * @return string|array escaped. */ function escape($input) { diff --git a/application/front/controllers/TagCloudController.php b/application/front/controllers/TagCloudController.php new file mode 100644 index 00000000..b6f4a0ce --- /dev/null +++ b/application/front/controllers/TagCloudController.php @@ -0,0 +1,89 @@ +container->loginManager->isLoggedIn() === true) { + $visibility = $this->container->sessionManager->getSessionParameter('visibility'); + } + + $searchTags = $request->getQueryParam('searchtags'); + $filteringTags = $searchTags !== null ? explode(' ', $searchTags) : []; + + $tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null); + + // We sort tags alphabetically, then choose a font size according to count. + // First, find max value. + $maxCount = 0; + foreach ($tags as $count) { + $maxCount = max($maxCount, $count); + } + + alphabetical_sort($tags, false, true); + + $logMaxCount = $maxCount > 1 ? log($maxCount, 30) : 1; + $tagList = []; + foreach ($tags as $key => $value) { + if (in_array($key, $filteringTags)) { + continue; + } + // Tag font size scaling: + // default 15 and 30 logarithm bases affect scaling, + // 2.2 and 0.8 are arbitrary font sizes in em. + $size = log($value, 15) / $logMaxCount * 2.2 + 0.8; + $tagList[$key] = [ + 'count' => $value, + 'size' => number_format($size, 2, '.', ''), + ]; + } + + $searchTags = implode(' ', escape($filteringTags)); + $data = [ + 'search_tags' => $searchTags, + 'tags' => $tagList, + ]; + $data = $this->executeHooks($data); + foreach ($data as $key => $value) { + $this->assignView($key, $value); + } + + $searchTags = !empty($searchTags) ? $searchTags .' - ' : ''; + $this->assignView( + 'pagetitle', + $searchTags. t('Tag cloud') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + + return $response->write($this->render('tag.cloud')); + } + + /** + * @param mixed[] $data Template data + * + * @return mixed[] Template data after active plugins render_picwall hook execution. + */ + protected function executeHooks(array $data): array + { + $this->container->pluginManager->executeHooks( + 'render_tagcloud', + $data, + ['loggedin' => $this->container->loginManager->isLoggedIn()] + ); + + return $data; + } +} diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php index 994fcbe5..4ae99168 100644 --- a/application/security/SessionManager.php +++ b/application/security/SessionManager.php @@ -202,4 +202,14 @@ class SessionManager { return $this->session; } + + /** + * @param mixed $default value which will be returned if the $key is undefined + * + * @return mixed Content stored in session + */ + public function getSessionParameter(string $key, $default = null) + { + return $this->session[$key] ?? $default; + } } -- cgit v1.2.3 From c79473bd84ab5aba7836d2caaf61847cabaf1e53 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 16 May 2020 13:13:00 +0200 Subject: Handle tag filtering in the Bookmark service --- application/bookmark/BookmarkFileService.php | 1 + application/front/controllers/TagCloudController.php | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) (limited to 'application') diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index fef998fd..3b3812af 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -296,6 +296,7 @@ class BookmarkFileService implements BookmarkServiceInterface if (empty($tag) || (! $this->isLoggedIn && startsWith($tag, '.')) || $tag === BookmarkMarkdownFormatter::NO_MD_TAG + || in_array($tag, $filteringTags, true) ) { continue; } diff --git a/application/front/controllers/TagCloudController.php b/application/front/controllers/TagCloudController.php index b6f4a0ce..9389c2b0 100644 --- a/application/front/controllers/TagCloudController.php +++ b/application/front/controllers/TagCloudController.php @@ -39,9 +39,6 @@ class TagCloudController extends ShaarliController $logMaxCount = $maxCount > 1 ? log($maxCount, 30) : 1; $tagList = []; foreach ($tags as $key => $value) { - if (in_array($key, $filteringTags)) { - continue; - } // Tag font size scaling: // default 15 and 30 logarithm bases affect scaling, // 2.2 and 0.8 are arbitrary font sizes in em. -- cgit v1.2.3 From 3772298ee7d8d0708f4e72798600accafa17740b Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 16 May 2020 13:33:39 +0200 Subject: Few optimizations and code readability for tag cloud controller --- .../front/controllers/TagCloudController.php | 52 +++++++++++++--------- 1 file changed, 31 insertions(+), 21 deletions(-) (limited to 'application') diff --git a/application/front/controllers/TagCloudController.php b/application/front/controllers/TagCloudController.php index 9389c2b0..93e3ae27 100644 --- a/application/front/controllers/TagCloudController.php +++ b/application/front/controllers/TagCloudController.php @@ -16,7 +16,13 @@ use Slim\Http\Response; */ class TagCloudController extends ShaarliController { - public function index(Request $request, Response $response): Response + /** + * Display the tag cloud through the template engine. + * This controller a few filters: + * - Visibility stored in the session for logged in users + * - `searchtags` query parameter: will return tags associated with filter in at least one bookmark + */ + public function cloud(Request $request, Response $response): Response { if ($this->container->loginManager->isLoggedIn() === true) { $visibility = $this->container->sessionManager->getSessionParameter('visibility'); @@ -27,27 +33,10 @@ class TagCloudController extends ShaarliController $tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null); - // We sort tags alphabetically, then choose a font size according to count. - // First, find max value. - $maxCount = 0; - foreach ($tags as $count) { - $maxCount = max($maxCount, $count); - } - + // TODO: the sorting should be handled by bookmarkService instead of the controller alphabetical_sort($tags, false, true); - $logMaxCount = $maxCount > 1 ? log($maxCount, 30) : 1; - $tagList = []; - foreach ($tags as $key => $value) { - // Tag font size scaling: - // default 15 and 30 logarithm bases affect scaling, - // 2.2 and 0.8 are arbitrary font sizes in em. - $size = log($value, 15) / $logMaxCount * 2.2 + 0.8; - $tagList[$key] = [ - 'count' => $value, - 'size' => number_format($size, 2, '.', ''), - ]; - } + $tagList = $this->formatTagsForCloud($tags); $searchTags = implode(' ', escape($filteringTags)); $data = [ @@ -62,12 +51,33 @@ class TagCloudController extends ShaarliController $searchTags = !empty($searchTags) ? $searchTags .' - ' : ''; $this->assignView( 'pagetitle', - $searchTags. t('Tag cloud') .' - '. $this->container->conf->get('general.title', 'Shaarli') + $searchTags . t('Tag cloud') .' - '. $this->container->conf->get('general.title', 'Shaarli') ); return $response->write($this->render('tag.cloud')); } + protected function formatTagsForCloud(array $tags): array + { + // We sort tags alphabetically, then choose a font size according to count. + // First, find max value. + $maxCount = count($tags) > 0 ? max($tags) : 0; + $logMaxCount = $maxCount > 1 ? log($maxCount, 30) : 1; + $tagList = []; + foreach ($tags as $key => $value) { + // Tag font size scaling: + // default 15 and 30 logarithm bases affect scaling, + // 2.2 and 0.8 are arbitrary font sizes in em. + $size = log($value, 15) / $logMaxCount * 2.2 + 0.8; + $tagList[$key] = [ + 'count' => $value, + 'size' => number_format($size, 2, '.', ''), + ]; + } + + return $tagList; + } + /** * @param mixed[] $data Template data * -- cgit v1.2.3 From 60ae241251b753fc052e50ebd95277dfcb074cb0 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 16 May 2020 14:56:22 +0200 Subject: Process tag list page through Slim controller --- .../front/controllers/TagCloudController.php | 59 ++++++++++++++++++---- 1 file changed, 48 insertions(+), 11 deletions(-) (limited to 'application') diff --git a/application/front/controllers/TagCloudController.php b/application/front/controllers/TagCloudController.php index 93e3ae27..1ff7c2e6 100644 --- a/application/front/controllers/TagCloudController.php +++ b/application/front/controllers/TagCloudController.php @@ -10,12 +10,15 @@ use Slim\Http\Response; /** * Class TagCloud * - * Slim controller used to render the tag cloud page. + * Slim controller used to render the tag cloud and tag list pages. * * @package Front\Controller */ class TagCloudController extends ShaarliController { + protected const TYPE_CLOUD = 'cloud'; + protected const TYPE_LIST = 'list'; + /** * Display the tag cloud through the template engine. * This controller a few filters: @@ -23,27 +26,54 @@ class TagCloudController extends ShaarliController * - `searchtags` query parameter: will return tags associated with filter in at least one bookmark */ public function cloud(Request $request, Response $response): Response + { + return $this->processRequest(static::TYPE_CLOUD, $request, $response); + } + + /** + * Display the tag list through the template engine. + * This controller a few filters: + * - Visibility stored in the session for logged in users + * - `searchtags` query parameter: will return tags associated with filter in at least one bookmark + * - `sort` query parameters: + * + `usage` (default): most used tags first + * + `alpha`: alphabetical order + */ + public function list(Request $request, Response $response): Response + { + return $this->processRequest(static::TYPE_LIST, $request, $response); + } + + /** + * Process the request for both tag cloud and tag list endpoints. + */ + protected function processRequest(string $type, Request $request, Response $response): Response { if ($this->container->loginManager->isLoggedIn() === true) { $visibility = $this->container->sessionManager->getSessionParameter('visibility'); } + $sort = $request->getQueryParam('sort'); $searchTags = $request->getQueryParam('searchtags'); $filteringTags = $searchTags !== null ? explode(' ', $searchTags) : []; $tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null); - // TODO: the sorting should be handled by bookmarkService instead of the controller - alphabetical_sort($tags, false, true); + if (static::TYPE_CLOUD === $type || 'alpha' === $sort) { + // TODO: the sorting should be handled by bookmarkService instead of the controller + alphabetical_sort($tags, false, true); + } - $tagList = $this->formatTagsForCloud($tags); + if (static::TYPE_CLOUD === $type) { + $tags = $this->formatTagsForCloud($tags); + } $searchTags = implode(' ', escape($filteringTags)); $data = [ 'search_tags' => $searchTags, - 'tags' => $tagList, + 'tags' => $tags, ]; - $data = $this->executeHooks($data); + $data = $this->executeHooks('tag' . $type, $data); foreach ($data as $key => $value) { $this->assignView($key, $value); } @@ -51,12 +81,19 @@ class TagCloudController extends ShaarliController $searchTags = !empty($searchTags) ? $searchTags .' - ' : ''; $this->assignView( 'pagetitle', - $searchTags . t('Tag cloud') .' - '. $this->container->conf->get('general.title', 'Shaarli') + $searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli') ); - return $response->write($this->render('tag.cloud')); + return $response->write($this->render('tag.'. $type)); } + /** + * Format the tags array for the tag cloud template. + * + * @param array $tags List of tags as key with count as value + * + * @return mixed[] List of tags as key, with count and expected font size in a subarray + */ protected function formatTagsForCloud(array $tags): array { // We sort tags alphabetically, then choose a font size according to count. @@ -81,12 +118,12 @@ class TagCloudController extends ShaarliController /** * @param mixed[] $data Template data * - * @return mixed[] Template data after active plugins render_picwall hook execution. + * @return mixed[] Template data after active plugins hook execution. */ - protected function executeHooks(array $data): array + protected function executeHooks(string $template, array $data): array { $this->container->pluginManager->executeHooks( - 'render_tagcloud', + 'render_'. $template, $data, ['loggedin' => $this->container->loginManager->isLoggedIn()] ); -- cgit v1.2.3 From 69e29ff65ef56b886748c58ba5b037cf217c4a1d Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 17 May 2020 11:06:39 +0200 Subject: Process daily page through Slim controller --- application/Utils.php | 8 +- application/bookmark/BookmarkFilter.php | 2 +- application/front/controllers/DailyController.php | 142 ++++++++++++++++++++++ 3 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 application/front/controllers/DailyController.php (limited to 'application') diff --git a/application/Utils.php b/application/Utils.php index 4e97cdda..72c90049 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -294,15 +294,15 @@ function normalize_spaces($string) * Requires php-intl to display international datetimes, * otherwise default format '%c' will be returned. * - * @param DateTime $date to format. - * @param bool $time Displays time if true. - * @param bool $intl Use international format if true. + * @param DateTimeInterface $date to format. + * @param bool $time Displays time if true. + * @param bool $intl Use international format if true. * * @return bool|string Formatted date, or false if the input is invalid. */ function format_date($date, $time = true, $intl = true) { - if (! $date instanceof DateTime) { + if (! $date instanceof DateTimeInterface) { return false; } diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php index fd556679..797a36b8 100644 --- a/application/bookmark/BookmarkFilter.php +++ b/application/bookmark/BookmarkFilter.php @@ -436,7 +436,7 @@ class BookmarkFilter throw new Exception('Invalid date format'); } - $filtered = array(); + $filtered = []; foreach ($this->bookmarks as $key => $l) { if ($l->getCreated()->format('Ymd') == $day) { $filtered[$key] = $l; diff --git a/application/front/controllers/DailyController.php b/application/front/controllers/DailyController.php new file mode 100644 index 00000000..c2fdaa55 --- /dev/null +++ b/application/front/controllers/DailyController.php @@ -0,0 +1,142 @@ +getQueryParam('day') ?? date('Ymd'); + + $availableDates = $this->container->bookmarkService->days(); + $nbAvailableDates = count($availableDates); + $index = array_search($day, $availableDates); + + if ($index === false && $nbAvailableDates > 0) { + // no bookmarks for day, but at least one day with bookmarks + $index = $nbAvailableDates - 1; + $day = $availableDates[$index]; + } + + if ($day === date('Ymd')) { + $this->assignView('dayDesc', t('Today')); + } elseif ($day === date('Ymd', strtotime('-1 days'))) { + $this->assignView('dayDesc', t('Yesterday')); + } + + if ($index !== false) { + if ($index >= 1) { + $previousDay = $availableDates[$index - 1]; + } + if ($index < $nbAvailableDates - 1) { + $nextDay = $availableDates[$index + 1]; + } + } + + try { + $linksToDisplay = $this->container->bookmarkService->filterDay($day); + } catch (\Exception $exc) { + $linksToDisplay = []; + } + + $formatter = $this->container->formatterFactory->getFormatter(); + // We pre-format some fields for proper output. + foreach ($linksToDisplay as $key => $bookmark) { + $linksToDisplay[$key] = $formatter->format($bookmark); + // This page is a bit specific, we need raw description to calculate the length + $linksToDisplay[$key]['formatedDescription'] = $linksToDisplay[$key]['description']; + $linksToDisplay[$key]['description'] = $bookmark->getDescription(); + } + + $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); + $data = [ + 'linksToDisplay' => $linksToDisplay, + 'day' => $dayDate->getTimestamp(), + 'dayDate' => $dayDate, + 'previousday' => $previousDay ?? '', + 'nextday' => $nextDay ?? '', + ]; + + // Hooks are called before column construction so that plugins don't have to deal with columns. + $this->executeHooks($data); + + $data['cols'] = $this->calculateColumns($data['linksToDisplay']); + + foreach ($data as $key => $value) { + $this->assignView($key, $value); + } + + $mainTitle = $this->container->conf->get('general.title', 'Shaarli'); + $this->assignView( + 'pagetitle', + t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle + ); + + return $response->write($this->render('daily')); + } + + /** + * We need to spread the articles on 3 columns. + * did not want to use a JavaScript lib like http://masonry.desandro.com/ + * so I manually spread entries with a simple method: I roughly evaluate the + * height of a div according to title and description length. + */ + protected function calculateColumns(array $links): array + { + // Entries to display, for each column. + $columns = [[], [], []]; + // Rough estimate of columns fill. + $fill = [0, 0, 0]; + foreach ($links as $link) { + // Roughly estimate length of entry (by counting characters) + // Title: 30 chars = 1 line. 1 line is 30 pixels height. + // Description: 836 characters gives roughly 342 pixel height. + // This is not perfect, but it's usually OK. + $length = strlen($link['title'] ?? '') + (342 * strlen($link['description'] ?? '')) / 836; + if (! empty($link['thumbnail'])) { + $length += 100; // 1 thumbnails roughly takes 100 pixels height. + } + // Then put in column which is the less filled: + $smallest = min($fill); // find smallest value in array. + $index = array_search($smallest, $fill); // find index of this smallest value. + array_push($columns[$index], $link); // Put entry in this column. + $fill[$index] += $length; + } + + return $columns; + } + + /** + * @param mixed[] $data Variables passed to the template engine + * + * @return mixed[] Template data after active plugins render_picwall hook execution. + */ + protected function executeHooks(array $data): array + { + $this->container->pluginManager->executeHooks( + 'render_daily', + $data, + ['loggedin' => $this->container->loginManager->isLoggedIn()] + ); + + return $data; + } +} -- cgit v1.2.3 From e3d28be9673a9f8404ff907b8191209729ad690c Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 17 May 2020 11:29:17 +0200 Subject: Slim daily: minor bugfix with empty data --- application/front/controllers/DailyController.php | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) (limited to 'application') diff --git a/application/front/controllers/DailyController.php b/application/front/controllers/DailyController.php index c2fdaa55..271c0ee2 100644 --- a/application/front/controllers/DailyController.php +++ b/application/front/controllers/DailyController.php @@ -30,10 +30,13 @@ class DailyController extends ShaarliController $nbAvailableDates = count($availableDates); $index = array_search($day, $availableDates); - if ($index === false && $nbAvailableDates > 0) { + if ($index === false) { // no bookmarks for day, but at least one day with bookmarks - $index = $nbAvailableDates - 1; - $day = $availableDates[$index]; + $day = $availableDates[$nbAvailableDates - 1] ?? $day; + $previousDay = $availableDates[$nbAvailableDates - 2] ?? ''; + } else { + $previousDay = $availableDates[$index - 1] ?? ''; + $nextDay = $availableDates[$index + 1] ?? ''; } if ($day === date('Ymd')) { @@ -42,15 +45,6 @@ class DailyController extends ShaarliController $this->assignView('dayDesc', t('Yesterday')); } - if ($index !== false) { - if ($index >= 1) { - $previousDay = $availableDates[$index - 1]; - } - if ($index < $nbAvailableDates - 1) { - $nextDay = $availableDates[$index + 1]; - } - } - try { $linksToDisplay = $this->container->bookmarkService->filterDay($day); } catch (\Exception $exc) { -- cgit v1.2.3 From c4d5be53c2ae503c00da3cfe6b28d0ce9d2ca7f5 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 17 May 2020 14:16:32 +0200 Subject: Process Daily RSS feed through Slim controller The daily RSS template has been entirely rewritten to handle the whole feed through the template engine. --- application/bookmark/Bookmark.php | 15 ++--- application/bookmark/BookmarkFileService.php | 2 +- application/container/ContainerBuilder.php | 5 +- application/front/controllers/DailyController.php | 74 +++++++++++++++++++++++ application/http/HttpUtils.php | 15 +++-- application/legacy/LegacyLinkDB.php | 2 +- application/render/PageCacheManager.php | 17 +++++- 7 files changed, 115 insertions(+), 15 deletions(-) (limited to 'application') diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php index 83ddab82..90ff5b16 100644 --- a/application/bookmark/Bookmark.php +++ b/application/bookmark/Bookmark.php @@ -3,6 +3,7 @@ namespace Shaarli\Bookmark; use DateTime; +use DateTimeInterface; use Shaarli\Bookmark\Exception\InvalidBookmarkException; /** @@ -42,10 +43,10 @@ class Bookmark /** @var bool Set to true if the bookmark is set as sticky */ protected $sticky; - /** @var DateTime Creation datetime */ + /** @var DateTimeInterface Creation datetime */ protected $created; - /** @var DateTime Update datetime */ + /** @var DateTimeInterface datetime */ protected $updated; /** @var bool True if the bookmark can only be seen while logged in */ @@ -100,7 +101,7 @@ class Bookmark || ! is_int($this->id) || empty($this->shortUrl) || empty($this->created) - || ! $this->created instanceof DateTime + || ! $this->created instanceof DateTimeInterface ) { throw new InvalidBookmarkException($this); } @@ -188,7 +189,7 @@ class Bookmark /** * Get the Created. * - * @return DateTime + * @return DateTimeInterface */ public function getCreated() { @@ -198,7 +199,7 @@ class Bookmark /** * Get the Updated. * - * @return DateTime + * @return DateTimeInterface */ public function getUpdated() { @@ -270,7 +271,7 @@ class Bookmark * Set the Created. * Note: you shouldn't set this manually except for special cases (like bookmark import) * - * @param DateTime $created + * @param DateTimeInterface $created * * @return Bookmark */ @@ -284,7 +285,7 @@ class Bookmark /** * Set the Updated. * - * @param DateTime $updated + * @param DateTimeInterface $updated * * @return Bookmark */ diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 3b3812af..7439d8d8 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -53,7 +53,7 @@ class BookmarkFileService implements BookmarkServiceInterface { $this->conf = $conf; $this->history = $history; - $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache')); + $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn); $this->bookmarksIO = new BookmarkIO($this->conf); $this->isLoggedIn = $isLoggedIn; diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index c5c4a2c3..199f3f67 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -94,7 +94,10 @@ class ContainerBuilder }; $container['pageCacheManager'] = function (ShaarliContainer $container): PageCacheManager { - return new PageCacheManager($container->conf->get('resource.page_cache')); + return new PageCacheManager( + $container->conf->get('resource.page_cache'), + $container->loginManager->isLoggedIn() + ); }; return $container; diff --git a/application/front/controllers/DailyController.php b/application/front/controllers/DailyController.php index 271c0ee2..4a0735aa 100644 --- a/application/front/controllers/DailyController.php +++ b/application/front/controllers/DailyController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller; use DateTime; +use DateTimeImmutable; use Shaarli\Bookmark\Bookmark; use Slim\Http\Request; use Slim\Http\Response; @@ -18,6 +19,8 @@ use Slim\Http\Response; */ class DailyController extends ShaarliController { + public static $DAILY_RSS_NB_DAYS = 8; + /** * Controller displaying all bookmarks published in a single day. * It take a `day` date query parameter (format YYYYMMDD). @@ -87,6 +90,77 @@ class DailyController extends ShaarliController return $response->write($this->render('daily')); } + /** + * Daily RSS feed: 1 RSS entry per day giving all the bookmarks on that day. + * Gives the last 7 days (which have bookmarks). + * This RSS feed cannot be filtered and does not trigger plugins yet. + */ + public function rss(Request $request, Response $response): Response + { + $response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8'); + + $pageUrl = page_url($this->container->environment); + $cache = $this->container->pageCacheManager->getCachePage($pageUrl); + + $cached = $cache->cachedVersion(); + if (!empty($cached)) { + return $response->write($cached); + } + + $days = []; + foreach ($this->container->bookmarkService->search() as $bookmark) { + $day = $bookmark->getCreated()->format('Ymd'); + + // Stop iterating after DAILY_RSS_NB_DAYS entries + if (count($days) === static::$DAILY_RSS_NB_DAYS && !isset($days[$day])) { + break; + } + + $days[$day][] = $bookmark; + } + + // Build the RSS feed. + $indexUrl = escape(index_url($this->container->environment)); + + $formatter = $this->container->formatterFactory->getFormatter(); + $formatter->addContextData('index_url', $indexUrl); + + $dataPerDay = []; + + /** @var Bookmark[] $bookmarks */ + foreach ($days as $day => $bookmarks) { + $dayDatetime = DateTimeImmutable::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); + $dataPerDay[$day] = [ + 'date' => $dayDatetime, + 'date_rss' => $dayDatetime->format(DateTime::RSS), + 'date_human' => format_date($dayDatetime, false, true), + 'absolute_url' => $indexUrl . '/daily?day=' . $day, + 'links' => [], + ]; + + foreach ($bookmarks as $key => $bookmark) { + $dataPerDay[$day]['links'][$key] = $formatter->format($bookmark); + + // Make permalink URL absolute + if ($bookmark->isNote()) { + $dataPerDay[$day]['links'][$key]['url'] = $indexUrl . $bookmark->getUrl(); + } + } + } + + $this->assignView('title', $this->container->conf->get('general.title', 'Shaarli')); + $this->assignView('index_url', $indexUrl); + $this->assignView('page_url', $pageUrl); + $this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false)); + $this->assignView('days', $dataPerDay); + + $rssContent = $this->render('dailyrss'); + + $cache->cache($rssContent); + + return $response->write($rssContent); + } + /** * We need to spread the articles on 3 columns. * did not want to use a JavaScript lib like http://masonry.desandro.com/ diff --git a/application/http/HttpUtils.php b/application/http/HttpUtils.php index 2ea9195d..f00c4336 100644 --- a/application/http/HttpUtils.php +++ b/application/http/HttpUtils.php @@ -369,7 +369,7 @@ function server_url($server) */ function index_url($server) { - $scriptname = $server['SCRIPT_NAME']; + $scriptname = $server['SCRIPT_NAME'] ?? ''; if (endsWith($scriptname, 'index.php')) { $scriptname = substr($scriptname, 0, -9); } @@ -377,7 +377,7 @@ function index_url($server) } /** - * Returns the absolute URL of the current script, with the query + * Returns the absolute URL of the current script, with current route and query * * If the resource is "index.php", then it is removed (for better-looking URLs) * @@ -387,10 +387,17 @@ function index_url($server) */ function page_url($server) { + $scriptname = $server['SCRIPT_NAME'] ?? ''; + if (endsWith($scriptname, 'index.php')) { + $scriptname = substr($scriptname, 0, -9); + } + + $route = ltrim($server['REQUEST_URI'] ?? '', $scriptname); if (! empty($server['QUERY_STRING'])) { - return index_url($server).'?'.$server['QUERY_STRING']; + return index_url($server) . $route . '?' . $server['QUERY_STRING']; } - return index_url($server); + + return index_url($server) . $route; } /** diff --git a/application/legacy/LegacyLinkDB.php b/application/legacy/LegacyLinkDB.php index 947005ad..7bf76fd4 100644 --- a/application/legacy/LegacyLinkDB.php +++ b/application/legacy/LegacyLinkDB.php @@ -353,7 +353,7 @@ You use the community supported version of the original Shaarli project, by Seba $this->write(); - $pageCacheManager = new PageCacheManager($pageCacheDir); + $pageCacheManager = new PageCacheManager($pageCacheDir, $this->loggedIn); $pageCacheManager->invalidateCaches(); } diff --git a/application/render/PageCacheManager.php b/application/render/PageCacheManager.php index bd91fe0d..97805c35 100644 --- a/application/render/PageCacheManager.php +++ b/application/render/PageCacheManager.php @@ -2,6 +2,8 @@ namespace Shaarli\Render; +use Shaarli\Feed\CachedPage; + /** * Cache utilities */ @@ -10,9 +12,13 @@ class PageCacheManager /** @var string Cache directory */ protected $pageCacheDir; - public function __construct(string $pageCacheDir) + /** @var bool */ + protected $isLoggedIn; + + public function __construct(string $pageCacheDir, bool $isLoggedIn) { $this->pageCacheDir = $pageCacheDir; + $this->isLoggedIn = $isLoggedIn; } /** @@ -42,4 +48,13 @@ class PageCacheManager // Purge page cache shared by sessions. $this->purgeCachedPages(); } + + public function getCachePage(string $pageUrl): CachedPage + { + return new CachedPage( + $this->pageCacheDir, + $pageUrl, + false === $this->isLoggedIn + ); + } } -- cgit v1.2.3 From f4929b1188b4bc5e92b925ebc44f5ad40bb1a4ed Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 18 May 2020 13:03:13 +0200 Subject: Make FeedBuilder instance creation independant of the request stack --- application/feed/FeedBuilder.php | 139 +++++++++++++++++++-------------------- 1 file changed, 66 insertions(+), 73 deletions(-) (limited to 'application') diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php index 40bd4f15..bcf27c2c 100644 --- a/application/feed/FeedBuilder.php +++ b/application/feed/FeedBuilder.php @@ -43,21 +43,9 @@ class FeedBuilder */ protected $formatter; - /** - * @var string RSS or ATOM feed. - */ - protected $feedType; - - /** - * @var array $_SERVER - */ + /** @var mixed[] $_SERVER */ protected $serverInfo; - /** - * @var array $_GET - */ - protected $userInput; - /** * @var boolean True if the user is currently logged in, false otherwise. */ @@ -77,7 +65,6 @@ class FeedBuilder * @var string server locale. */ protected $locale; - /** * @var DateTime Latest item date. */ @@ -88,37 +75,36 @@ class FeedBuilder * * @param BookmarkServiceInterface $linkDB LinkDB instance. * @param BookmarkFormatter $formatter instance. - * @param string $feedType Type of feed. * @param array $serverInfo $_SERVER. - * @param array $userInput $_GET. * @param boolean $isLoggedIn True if the user is currently logged in, false otherwise. */ - public function __construct($linkDB, $formatter, $feedType, $serverInfo, $userInput, $isLoggedIn) + public function __construct($linkDB, $formatter, array $serverInfo, $isLoggedIn) { $this->linkDB = $linkDB; $this->formatter = $formatter; - $this->feedType = $feedType; $this->serverInfo = $serverInfo; - $this->userInput = $userInput; $this->isLoggedIn = $isLoggedIn; } /** * Build data for feed templates. * + * @param string $feedType Type of feed (RSS/ATOM). + * @param array $userInput $_GET. + * * @return array Formatted data for feeds templates. */ - public function buildData() + public function buildData(string $feedType, ?array $userInput) { // Search for untagged bookmarks - if (isset($this->userInput['searchtags']) && empty($this->userInput['searchtags'])) { - $this->userInput['searchtags'] = false; + if (isset($this->userInput['searchtags']) && empty($userInput['searchtags'])) { + $userInput['searchtags'] = false; } // Optionally filter the results: - $linksToDisplay = $this->linkDB->search($this->userInput); + $linksToDisplay = $this->linkDB->search($userInput); - $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay)); + $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay), $userInput); // Can't use array_keys() because $link is a LinkDB instance and not a real array. $keys = array(); @@ -130,11 +116,11 @@ class FeedBuilder $this->formatter->addContextData('index_url', $pageaddr); $linkDisplayed = array(); for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) { - $linkDisplayed[$keys[$i]] = $this->buildItem($linksToDisplay[$keys[$i]], $pageaddr); + $linkDisplayed[$keys[$i]] = $this->buildItem($feedType, $linksToDisplay[$keys[$i]], $pageaddr); } - $data['language'] = $this->getTypeLanguage(); - $data['last_update'] = $this->getLatestDateFormatted(); + $data['language'] = $this->getTypeLanguage($feedType); + $data['last_update'] = $this->getLatestDateFormatted($feedType); $data['show_dates'] = !$this->hideDates || $this->isLoggedIn; // Remove leading slash from REQUEST_URI. $data['self_link'] = escape(server_url($this->serverInfo)) @@ -146,15 +132,46 @@ class FeedBuilder return $data; } + /** + * Set this to true to use permalinks instead of direct bookmarks. + * + * @param boolean $usePermalinks true to force permalinks. + */ + public function setUsePermalinks($usePermalinks) + { + $this->usePermalinks = $usePermalinks; + } + + /** + * Set this to true to hide timestamps in feeds. + * + * @param boolean $hideDates true to enable. + */ + public function setHideDates($hideDates) + { + $this->hideDates = $hideDates; + } + + /** + * Set the locale. Used to show feed language. + * + * @param string $locale The locale (eg. 'fr_FR.UTF8'). + */ + public function setLocale($locale) + { + $this->locale = strtolower($locale); + } + /** * Build a feed item (one per shaare). * + * @param string $feedType Type of feed (RSS/ATOM). * @param Bookmark $link Single link array extracted from LinkDB. * @param string $pageaddr Index URL. * * @return array Link array with feed attributes. */ - protected function buildItem($link, $pageaddr) + protected function buildItem(string $feedType, $link, $pageaddr) { $data = $this->formatter->format($link); $data['guid'] = $pageaddr . '?' . $data['shorturl']; @@ -165,13 +182,13 @@ class FeedBuilder } $data['description'] .= PHP_EOL . PHP_EOL . '
— ' . $permalink; - $data['pub_iso_date'] = $this->getIsoDate($data['created']); + $data['pub_iso_date'] = $this->getIsoDate($feedType, $data['created']); // atom:entry elements MUST contain exactly one atom:updated element. if (!empty($link->getUpdated())) { - $data['up_iso_date'] = $this->getIsoDate($data['updated'], DateTime::ATOM); + $data['up_iso_date'] = $this->getIsoDate($feedType, $data['updated'], DateTime::ATOM); } else { - $data['up_iso_date'] = $this->getIsoDate($data['created'], DateTime::ATOM); + $data['up_iso_date'] = $this->getIsoDate($feedType, $data['created'], DateTime::ATOM); } // Save the more recent item. @@ -185,52 +202,24 @@ class FeedBuilder return $data; } - /** - * Set this to true to use permalinks instead of direct bookmarks. - * - * @param boolean $usePermalinks true to force permalinks. - */ - public function setUsePermalinks($usePermalinks) - { - $this->usePermalinks = $usePermalinks; - } - - /** - * Set this to true to hide timestamps in feeds. - * - * @param boolean $hideDates true to enable. - */ - public function setHideDates($hideDates) - { - $this->hideDates = $hideDates; - } - - /** - * Set the locale. Used to show feed language. - * - * @param string $locale The locale (eg. 'fr_FR.UTF8'). - */ - public function setLocale($locale) - { - $this->locale = strtolower($locale); - } - /** * Get the language according to the feed type, based on the locale: * * - RSS format: en-us (default: 'en-en'). * - ATOM format: fr (default: 'en'). * + * @param string $feedType Type of feed (RSS/ATOM). + * * @return string The language. */ - public function getTypeLanguage() + protected function getTypeLanguage(string $feedType) { // Use the locale do define the language, if available. if (!empty($this->locale) && preg_match('/^\w{2}[_\-]\w{2}/', $this->locale)) { - $length = ($this->feedType === self::$FEED_RSS) ? 5 : 2; + $length = ($feedType === self::$FEED_RSS) ? 5 : 2; return str_replace('_', '-', substr($this->locale, 0, $length)); } - return ($this->feedType === self::$FEED_RSS) ? 'en-en' : 'en'; + return ($feedType === self::$FEED_RSS) ? 'en-en' : 'en'; } /** @@ -238,32 +227,35 @@ class FeedBuilder * * Return an empty string if invalid DateTime is passed. * + * @param string $feedType Type of feed (RSS/ATOM). + * * @return string Formatted date. */ - protected function getLatestDateFormatted() + protected function getLatestDateFormatted(string $feedType) { if (empty($this->latestDate) || !$this->latestDate instanceof DateTime) { return ''; } - $type = ($this->feedType == self::$FEED_RSS) ? DateTime::RSS : DateTime::ATOM; + $type = ($feedType == self::$FEED_RSS) ? DateTime::RSS : DateTime::ATOM; return $this->latestDate->format($type); } /** * Get ISO date from DateTime according to feed type. * + * @param string $feedType Type of feed (RSS/ATOM). * @param DateTime $date Date to format. * @param string|bool $format Force format. * * @return string Formatted date. */ - protected function getIsoDate(DateTime $date, $format = false) + protected function getIsoDate(string $feedType, DateTime $date, $format = false) { if ($format !== false) { return $date->format($format); } - if ($this->feedType == self::$FEED_RSS) { + if ($feedType == self::$FEED_RSS) { return $date->format(DateTime::RSS); } return $date->format(DateTime::ATOM); @@ -275,21 +267,22 @@ class FeedBuilder * If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS. * If 'nb' is set to 'all', display all filtered bookmarks (max parameter). * - * @param int $max maximum number of bookmarks to display. + * @param int $max maximum number of bookmarks to display. + * @param array $userInput $_GET. * * @return int number of bookmarks to display. */ - public function getNbLinks($max) + protected function getNbLinks($max, ?array $userInput) { - if (empty($this->userInput['nb'])) { + if (empty($userInput['nb'])) { return self::$DEFAULT_NB_LINKS; } - if ($this->userInput['nb'] == 'all') { + if ($userInput['nb'] == 'all') { return $max; } - $intNb = intval($this->userInput['nb']); + $intNb = intval($userInput['nb']); if (!is_int($intNb) || $intNb == 0) { return self::$DEFAULT_NB_LINKS; } -- cgit v1.2.3 From 7b2ba6ef820335df682fbe3dcfaceef3a62cf4a5 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 18 May 2020 17:17:36 +0200 Subject: RSS/ATOM feeds: process through Slim controller --- application/container/ContainerBuilder.php | 10 +++ application/container/ShaarliContainer.php | 2 + application/feed/FeedBuilder.php | 2 +- application/front/controllers/FeedController.php | 79 ++++++++++++++++++++++ .../front/controllers/ShaarliController.php | 14 ++++ 5 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 application/front/controllers/FeedController.php (limited to 'application') diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index 199f3f67..84406979 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -7,6 +7,7 @@ namespace Shaarli\Container; use Shaarli\Bookmark\BookmarkFileService; use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Config\ConfigManager; +use Shaarli\Feed\FeedBuilder; use Shaarli\Formatter\FormatterFactory; use Shaarli\History; use Shaarli\Plugin\PluginManager; @@ -100,6 +101,15 @@ class ContainerBuilder ); }; + $container['feedBuilder'] = function (ShaarliContainer $container): FeedBuilder { + return new FeedBuilder( + $container->bookmarkService, + $container->formatterFactory->getFormatter(), + $container->environment, + $container->loginManager->isLoggedIn() + ); + }; + return $container; } } diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php index 3995f669..deb07197 100644 --- a/application/container/ShaarliContainer.php +++ b/application/container/ShaarliContainer.php @@ -6,6 +6,7 @@ namespace Shaarli\Container; use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Config\ConfigManager; +use Shaarli\Feed\FeedBuilder; use Shaarli\Formatter\FormatterFactory; use Shaarli\History; use Shaarli\Plugin\PluginManager; @@ -29,6 +30,7 @@ use Slim\Container; * @property PluginManager $pluginManager * @property FormatterFactory $formatterFactory * @property PageCacheManager $pageCacheManager + * @property FeedBuilder $feedBuilder */ class ShaarliContainer extends Container { diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php index bcf27c2c..c97ae1ea 100644 --- a/application/feed/FeedBuilder.php +++ b/application/feed/FeedBuilder.php @@ -78,7 +78,7 @@ class FeedBuilder * @param array $serverInfo $_SERVER. * @param boolean $isLoggedIn True if the user is currently logged in, false otherwise. */ - public function __construct($linkDB, $formatter, array $serverInfo, $isLoggedIn) + public function __construct($linkDB, $formatter, $serverInfo, $isLoggedIn) { $this->linkDB = $linkDB; $this->formatter = $formatter; diff --git a/application/front/controllers/FeedController.php b/application/front/controllers/FeedController.php new file mode 100644 index 00000000..78d826d9 --- /dev/null +++ b/application/front/controllers/FeedController.php @@ -0,0 +1,79 @@ +processRequest(FeedBuilder::$FEED_ATOM, $request, $response); + } + + public function rss(Request $request, Response $response): Response + { + return $this->processRequest(FeedBuilder::$FEED_RSS, $request, $response); + } + + protected function processRequest(string $feedType, Request $request, Response $response): Response + { + $response = $response->withHeader('Content-Type', 'application/'. $feedType .'+xml; charset=utf-8'); + + $pageUrl = page_url($this->container->environment); + $cache = $this->container->pageCacheManager->getCachePage($pageUrl); + + $cached = $cache->cachedVersion(); + if (!empty($cached)) { + return $response->write($cached); + } + + // Generate data. + $this->container->feedBuilder->setLocale(strtolower(setlocale(LC_COLLATE, 0))); + $this->container->feedBuilder->setHideDates($this->container->conf->get('privacy.hide_timestamps', false)); + $this->container->feedBuilder->setUsePermalinks( + null !== $request->getParam('permalinks') || !$this->container->conf->get('feed.rss_permalinks') + ); + + $data = $this->container->feedBuilder->buildData($feedType, $request->getParams()); + + $this->executeHooks($data, $feedType); + $this->assignAllView($data); + + $content = $this->render('feed.'. $feedType); + + $cache->cache($content); + + return $response->write($content); + } + + /** + * @param mixed[] $data Template data + * + * @return mixed[] Template data after active plugins hook execution. + */ + protected function executeHooks(array $data, string $feedType): array + { + $this->container->pluginManager->executeHooks( + 'render_feed', + $data, + [ + 'loggedin' => $this->container->loginManager->isLoggedIn(), + 'target' => $feedType, + ] + ); + + return $data; + } +} diff --git a/application/front/controllers/ShaarliController.php b/application/front/controllers/ShaarliController.php index 2b828588..0c5d363e 100644 --- a/application/front/controllers/ShaarliController.php +++ b/application/front/controllers/ShaarliController.php @@ -30,6 +30,20 @@ abstract class ShaarliController return $this; } + /** + * Assign variables to RainTPL template through the PageBuilder. + * + * @param mixed $data Values to assign to the template and their keys + */ + protected function assignAllView(array $data): self + { + foreach ($data as $key => $value) { + $this->assignView($key, $value); + } + + return $this; + } + protected function render(string $template): string { $this->assignView('linkcount', $this->container->bookmarkService->count(BookmarkFilter::$ALL)); -- cgit v1.2.3 From 5ec4708ced1cdca01eddd7e52377ab5e5f8b3290 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 20 May 2020 10:47:20 +0200 Subject: Process OpenSearch controller through Slim Also it was missing on the default template feeds --- .../front/controllers/OpenSearchController.php | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 application/front/controllers/OpenSearchController.php (limited to 'application') diff --git a/application/front/controllers/OpenSearchController.php b/application/front/controllers/OpenSearchController.php new file mode 100644 index 00000000..fa32c5f1 --- /dev/null +++ b/application/front/controllers/OpenSearchController.php @@ -0,0 +1,28 @@ +withHeader('Content-Type', 'application/opensearchdescription+xml; charset=utf-8'); + + $this->assignView('serverurl', index_url($this->container->environment)); + + return $response->write($this->render('opensearch')); + } +} -- cgit v1.2.3 From 893f5159c64e5bcff505c8367e6dc22cc2a7b14d Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 20 May 2020 14:38:31 +0200 Subject: Process remove tag endpoint through Slim controller --- application/front/controllers/TagController.php | 48 ++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) (limited to 'application') diff --git a/application/front/controllers/TagController.php b/application/front/controllers/TagController.php index 598275b0..a1d5ad5b 100644 --- a/application/front/controllers/TagController.php +++ b/application/front/controllers/TagController.php @@ -35,7 +35,7 @@ class TagController extends ShaarliController return $response->withRedirect('./'); } - $currentUrl = parse_url($this->container->environment['HTTP_REFERER']); + $currentUrl = parse_url($referer); parse_str($currentUrl['query'] ?? '', $params); if (null === $newTag) { @@ -71,4 +71,50 @@ class TagController extends ShaarliController return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); } + + /** + * Remove a tag from the current search through an HTTP redirection. + * + * @param array $args Should contain `tag` key as tag to remove from current search + */ + public function removeTag(Request $request, Response $response, array $args): Response + { + $referer = $this->container->environment['HTTP_REFERER'] ?? null; + + // If the referrer is not provided, we can update the search, so we failback on the bookmark list + if (empty($referer)) { + return $response->withRedirect('./'); + } + + $tagToRemove = $args['tag'] ?? null; + $currentUrl = parse_url($referer); + parse_str($currentUrl['query'] ?? '', $params); + + if (null === $tagToRemove) { + return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); + } + + // Prevent redirection loop + if (isset($params['removetag'])) { + unset($params['removetag']); + } + + if (isset($params['searchtags'])) { + $tags = explode(' ', $params['searchtags']); + // Remove value from array $tags. + $tags = array_diff($tags, [$tagToRemove]); + $params['searchtags'] = implode(' ', $tags); + + if (empty($params['searchtags'])) { + unset($params['searchtags']); + } + + // We also remove page (keeping the same page has no sense, since the results are different) + unset($params['page']); + } + + $queryParams = count($params) > 0 ? '?' . http_build_query($params) : ''; + + return $response->withRedirect(($currentUrl['path'] ?? './') . $queryParams); + } } -- cgit v1.2.3 From af290059d10319e76d1e7d78b592cab99c26d91a Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 22 May 2020 11:02:56 +0200 Subject: Process session filters through Slim controllers Including: - visibility - links per page - untagged only --- .../front/controllers/SessionFilterController.php | 81 ++++++++++++++++++++++ .../front/controllers/ShaarliController.php | 43 ++++++++++++ application/security/SessionManager.php | 33 +++++++++ 3 files changed, 157 insertions(+) create mode 100644 application/front/controllers/SessionFilterController.php (limited to 'application') diff --git a/application/front/controllers/SessionFilterController.php b/application/front/controllers/SessionFilterController.php new file mode 100644 index 00000000..a021dc37 --- /dev/null +++ b/application/front/controllers/SessionFilterController.php @@ -0,0 +1,81 @@ +getParam('nb') ?? null; + if (null === $linksPerPage || false === is_numeric($linksPerPage)) { + $linksPerPage = $this->container->conf->get('general.links_per_page', 20); + } + + $this->container->sessionManager->setSessionParameter( + SessionManager::KEY_LINKS_PER_PAGE, + abs(intval($linksPerPage)) + ); + + return $this->redirectFromReferer($response, ['linksperpage'], ['nb']); + } + + /** + * GET /visibility: allows to display only public or only private bookmarks in linklist + */ + public function visibility(Request $request, Response $response, array $args): Response + { + if (false === $this->container->loginManager->isLoggedIn()) { + return $this->redirectFromReferer($response, ['visibility']); + } + + $newVisibility = $args['visibility'] ?? null; + if (false === in_array($newVisibility, [BookmarkFilter::$PRIVATE, BookmarkFilter::$PUBLIC], true)) { + $newVisibility = null; + } + + $currentVisibility = $this->container->sessionManager->getSessionParameter(SessionManager::KEY_VISIBILITY); + + // Visibility not set or not already expected value, set expected value, otherwise reset it + if ($newVisibility !== null && (null === $currentVisibility || $currentVisibility !== $newVisibility)) { + // See only public bookmarks + $this->container->sessionManager->setSessionParameter( + SessionManager::KEY_VISIBILITY, + $newVisibility + ); + } else { + $this->container->sessionManager->deleteSessionParameter(SessionManager::KEY_VISIBILITY); + } + + return $this->redirectFromReferer($response, ['visibility']); + } + + /** + * GET /untagged-only: allows to display only bookmarks without any tag + */ + public function untaggedOnly(Request $request, Response $response): Response + { + $this->container->sessionManager->setSessionParameter( + SessionManager::KEY_UNTAGGED_ONLY, + empty($this->container->sessionManager->getSessionParameter(SessionManager::KEY_UNTAGGED_ONLY)) + ); + + return $this->redirectFromReferer($response, ['untaggedonly', 'untagged-only']); + } +} diff --git a/application/front/controllers/ShaarliController.php b/application/front/controllers/ShaarliController.php index 0c5d363e..bfff5fcf 100644 --- a/application/front/controllers/ShaarliController.php +++ b/application/front/controllers/ShaarliController.php @@ -6,6 +6,7 @@ namespace Shaarli\Front\Controller; use Shaarli\Bookmark\BookmarkFilter; use Shaarli\Container\ShaarliContainer; +use Slim\Http\Response; abstract class ShaarliController { @@ -80,4 +81,46 @@ abstract class ShaarliController $this->assignView('plugins_' . $name, $plugin_data); } } + + /** + * Generates a redirection to the previous page, based on the HTTP_REFERER. + * It fails back to the home page. + * + * @param array $loopTerms Terms to remove from path and query string to prevent direction loop. + * @param array $clearParams List of parameter to remove from the query string of the referrer. + */ + protected function redirectFromReferer(Response $response, array $loopTerms = [], array $clearParams = []): Response + { + $defaultPath = './'; + $referer = $this->container->environment['HTTP_REFERER'] ?? null; + + if (null !== $referer) { + $currentUrl = parse_url($referer); + parse_str($currentUrl['query'] ?? '', $params); + $path = $currentUrl['path'] ?? $defaultPath; + } else { + $params = []; + $path = $defaultPath; + } + + // Prevent redirection loop + if (isset($currentUrl)) { + foreach ($clearParams as $value) { + unset($params[$value]); + } + + $checkQuery = implode('', array_keys($params)); + foreach ($loopTerms as $value) { + if (strpos($path . $checkQuery, $value) !== false) { + $params = []; + $path = $defaultPath; + break; + } + } + } + + $queryString = count($params) > 0 ? '?'. http_build_query($params) : ''; + + return $response->withRedirect($path . $queryString); + } } diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php index 4ae99168..8b77d362 100644 --- a/application/security/SessionManager.php +++ b/application/security/SessionManager.php @@ -8,6 +8,10 @@ use Shaarli\Config\ConfigManager; */ class SessionManager { + public const KEY_LINKS_PER_PAGE = 'LINKS_PER_PAGE'; + public const KEY_VISIBILITY = 'visibility'; + public const KEY_UNTAGGED_ONLY = 'untaggedonly'; + /** @var int Session expiration timeout, in seconds */ public static $SHORT_TIMEOUT = 3600; // 1 hour @@ -212,4 +216,33 @@ class SessionManager { return $this->session[$key] ?? $default; } + + /** + * Store a variable in user session. + * + * @param string $key Session key + * @param mixed $value Session value to store + * + * @return $this + */ + public function setSessionParameter(string $key, $value): self + { + $this->session[$key] = $value; + + return $this; + } + + /** + * Store a variable in user session. + * + * @param string $key Session key + * + * @return $this + */ + public function deleteSessionParameter(string $key): self + { + unset($this->session[$key]); + + return $this; + } } -- cgit v1.2.3 From 2899ebb5b5e82890c877151f5c02045266ac9973 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 22 May 2020 13:20:31 +0200 Subject: Initialize admin Slim controllers - Reorganize visitor controllers - Fix redirection with Slim's requests base path - Fix daily links --- application/front/ShaarliMiddleware.php | 7 +- .../front/controller/admin/LogoutController.php | 29 +++ .../controller/admin/SessionFilterController.php | 79 ++++++++ .../controller/admin/ShaarliAdminController.php | 21 +++ .../front/controller/visitor/DailyController.php | 208 ++++++++++++++++++++ .../front/controller/visitor/FeedController.php | 77 ++++++++ .../front/controller/visitor/LoginController.php | 46 +++++ .../controller/visitor/OpenSearchController.php | 26 +++ .../controller/visitor/PictureWallController.php | 70 +++++++ .../visitor/ShaarliVisitorController.php | 131 +++++++++++++ .../controller/visitor/TagCloudController.php | 131 +++++++++++++ .../front/controller/visitor/TagController.php | 118 ++++++++++++ application/front/controllers/DailyController.php | 210 --------------------- application/front/controllers/FeedController.php | 79 -------- application/front/controllers/LoginController.php | 48 ----- application/front/controllers/LogoutController.php | 31 --- .../front/controllers/OpenSearchController.php | 28 --- .../front/controllers/PictureWallController.php | 72 ------- .../front/controllers/SessionFilterController.php | 81 -------- .../front/controllers/ShaarliController.php | 126 ------------- .../front/controllers/TagCloudController.php | 133 ------------- application/front/controllers/TagController.php | 120 ------------ .../front/exceptions/LoginBannedException.php | 2 +- application/front/exceptions/ShaarliException.php | 23 --- .../front/exceptions/ShaarliFrontException.php | 23 +++ .../exceptions/ThumbnailsDisabledException.php | 2 +- .../front/exceptions/UnauthorizedException.php | 15 ++ 27 files changed, 981 insertions(+), 955 deletions(-) create mode 100644 application/front/controller/admin/LogoutController.php create mode 100644 application/front/controller/admin/SessionFilterController.php create mode 100644 application/front/controller/admin/ShaarliAdminController.php create mode 100644 application/front/controller/visitor/DailyController.php create mode 100644 application/front/controller/visitor/FeedController.php create mode 100644 application/front/controller/visitor/LoginController.php create mode 100644 application/front/controller/visitor/OpenSearchController.php create mode 100644 application/front/controller/visitor/PictureWallController.php create mode 100644 application/front/controller/visitor/ShaarliVisitorController.php create mode 100644 application/front/controller/visitor/TagCloudController.php create mode 100644 application/front/controller/visitor/TagController.php delete mode 100644 application/front/controllers/DailyController.php delete mode 100644 application/front/controllers/FeedController.php delete mode 100644 application/front/controllers/LoginController.php delete mode 100644 application/front/controllers/LogoutController.php delete mode 100644 application/front/controllers/OpenSearchController.php delete mode 100644 application/front/controllers/PictureWallController.php delete mode 100644 application/front/controllers/SessionFilterController.php delete mode 100644 application/front/controllers/ShaarliController.php delete mode 100644 application/front/controllers/TagCloudController.php delete mode 100644 application/front/controllers/TagController.php delete mode 100644 application/front/exceptions/ShaarliException.php create mode 100644 application/front/exceptions/ShaarliFrontException.php create mode 100644 application/front/exceptions/UnauthorizedException.php (limited to 'application') diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php index fa6c6467..f8992e0b 100644 --- a/application/front/ShaarliMiddleware.php +++ b/application/front/ShaarliMiddleware.php @@ -3,7 +3,8 @@ namespace Shaarli\Front; use Shaarli\Container\ShaarliContainer; -use Shaarli\Front\Exception\ShaarliException; +use Shaarli\Front\Exception\ShaarliFrontException; +use Shaarli\Front\Exception\UnauthorizedException; use Slim\Http\Request; use Slim\Http\Response; @@ -39,7 +40,7 @@ class ShaarliMiddleware { try { $response = $next($request, $response); - } catch (ShaarliException $e) { + } catch (ShaarliFrontException $e) { $this->container->pageBuilder->assign('message', $e->getMessage()); if ($this->container->conf->get('dev.debug', false)) { $this->container->pageBuilder->assign( @@ -50,6 +51,8 @@ class ShaarliMiddleware $response = $response->withStatus($e->getCode()); $response = $response->write($this->container->pageBuilder->render('error')); + } catch (UnauthorizedException $e) { + return $response->withRedirect($request->getUri()->getBasePath() . '/login'); } return $response; diff --git a/application/front/controller/admin/LogoutController.php b/application/front/controller/admin/LogoutController.php new file mode 100644 index 00000000..41e81984 --- /dev/null +++ b/application/front/controller/admin/LogoutController.php @@ -0,0 +1,29 @@ +container->pageCacheManager->invalidateCaches(); + $this->container->sessionManager->logout(); + + // TODO: switch to a simple Cookie manager allowing to check the session, and create mocks. + setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, $this->container->webPath); + + return $response->withRedirect('./'); + } +} diff --git a/application/front/controller/admin/SessionFilterController.php b/application/front/controller/admin/SessionFilterController.php new file mode 100644 index 00000000..69a16ec3 --- /dev/null +++ b/application/front/controller/admin/SessionFilterController.php @@ -0,0 +1,79 @@ +getParam('nb') ?? null; + if (null === $linksPerPage || false === is_numeric($linksPerPage)) { + $linksPerPage = $this->container->conf->get('general.links_per_page', 20); + } + + $this->container->sessionManager->setSessionParameter( + SessionManager::KEY_LINKS_PER_PAGE, + abs(intval($linksPerPage)) + ); + + return $this->redirectFromReferer($request, $response, ['linksperpage'], ['nb']); + } + + /** + * GET /visibility: allows to display only public or only private bookmarks in linklist + */ + public function visibility(Request $request, Response $response, array $args): Response + { + if (false === $this->container->loginManager->isLoggedIn()) { + return $this->redirectFromReferer($request, $response, ['visibility']); + } + + $newVisibility = $args['visibility'] ?? null; + if (false === in_array($newVisibility, [BookmarkFilter::$PRIVATE, BookmarkFilter::$PUBLIC], true)) { + $newVisibility = null; + } + + $currentVisibility = $this->container->sessionManager->getSessionParameter(SessionManager::KEY_VISIBILITY); + + // Visibility not set or not already expected value, set expected value, otherwise reset it + if ($newVisibility !== null && (null === $currentVisibility || $currentVisibility !== $newVisibility)) { + // See only public bookmarks + $this->container->sessionManager->setSessionParameter( + SessionManager::KEY_VISIBILITY, + $newVisibility + ); + } else { + $this->container->sessionManager->deleteSessionParameter(SessionManager::KEY_VISIBILITY); + } + + return $this->redirectFromReferer($request, $response, ['visibility']); + } + + /** + * GET /untagged-only: allows to display only bookmarks without any tag + */ + public function untaggedOnly(Request $request, Response $response): Response + { + $this->container->sessionManager->setSessionParameter( + SessionManager::KEY_UNTAGGED_ONLY, + empty($this->container->sessionManager->getSessionParameter(SessionManager::KEY_UNTAGGED_ONLY)) + ); + + return $this->redirectFromReferer($request, $response, ['untaggedonly', 'untagged-only']); + } +} diff --git a/application/front/controller/admin/ShaarliAdminController.php b/application/front/controller/admin/ShaarliAdminController.php new file mode 100644 index 00000000..ea703f62 --- /dev/null +++ b/application/front/controller/admin/ShaarliAdminController.php @@ -0,0 +1,21 @@ +container->loginManager->isLoggedIn()) { + throw new UnauthorizedException(); + } + } +} diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php new file mode 100644 index 00000000..47e2503a --- /dev/null +++ b/application/front/controller/visitor/DailyController.php @@ -0,0 +1,208 @@ +getQueryParam('day') ?? date('Ymd'); + + $availableDates = $this->container->bookmarkService->days(); + $nbAvailableDates = count($availableDates); + $index = array_search($day, $availableDates); + + if ($index === false) { + // no bookmarks for day, but at least one day with bookmarks + $day = $availableDates[$nbAvailableDates - 1] ?? $day; + $previousDay = $availableDates[$nbAvailableDates - 2] ?? ''; + } else { + $previousDay = $availableDates[$index - 1] ?? ''; + $nextDay = $availableDates[$index + 1] ?? ''; + } + + if ($day === date('Ymd')) { + $this->assignView('dayDesc', t('Today')); + } elseif ($day === date('Ymd', strtotime('-1 days'))) { + $this->assignView('dayDesc', t('Yesterday')); + } + + try { + $linksToDisplay = $this->container->bookmarkService->filterDay($day); + } catch (\Exception $exc) { + $linksToDisplay = []; + } + + $formatter = $this->container->formatterFactory->getFormatter(); + // We pre-format some fields for proper output. + foreach ($linksToDisplay as $key => $bookmark) { + $linksToDisplay[$key] = $formatter->format($bookmark); + // This page is a bit specific, we need raw description to calculate the length + $linksToDisplay[$key]['formatedDescription'] = $linksToDisplay[$key]['description']; + $linksToDisplay[$key]['description'] = $bookmark->getDescription(); + } + + $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); + $data = [ + 'linksToDisplay' => $linksToDisplay, + 'day' => $dayDate->getTimestamp(), + 'dayDate' => $dayDate, + 'previousday' => $previousDay ?? '', + 'nextday' => $nextDay ?? '', + ]; + + // Hooks are called before column construction so that plugins don't have to deal with columns. + $this->executeHooks($data); + + $data['cols'] = $this->calculateColumns($data['linksToDisplay']); + + foreach ($data as $key => $value) { + $this->assignView($key, $value); + } + + $mainTitle = $this->container->conf->get('general.title', 'Shaarli'); + $this->assignView( + 'pagetitle', + t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle + ); + + return $response->write($this->render('daily')); + } + + /** + * Daily RSS feed: 1 RSS entry per day giving all the bookmarks on that day. + * Gives the last 7 days (which have bookmarks). + * This RSS feed cannot be filtered and does not trigger plugins yet. + */ + public function rss(Request $request, Response $response): Response + { + $response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8'); + + $pageUrl = page_url($this->container->environment); + $cache = $this->container->pageCacheManager->getCachePage($pageUrl); + + $cached = $cache->cachedVersion(); + if (!empty($cached)) { + return $response->write($cached); + } + + $days = []; + foreach ($this->container->bookmarkService->search() as $bookmark) { + $day = $bookmark->getCreated()->format('Ymd'); + + // Stop iterating after DAILY_RSS_NB_DAYS entries + if (count($days) === static::$DAILY_RSS_NB_DAYS && !isset($days[$day])) { + break; + } + + $days[$day][] = $bookmark; + } + + // Build the RSS feed. + $indexUrl = escape(index_url($this->container->environment)); + + $formatter = $this->container->formatterFactory->getFormatter(); + $formatter->addContextData('index_url', $indexUrl); + + $dataPerDay = []; + + /** @var Bookmark[] $bookmarks */ + foreach ($days as $day => $bookmarks) { + $dayDatetime = DateTimeImmutable::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); + $dataPerDay[$day] = [ + 'date' => $dayDatetime, + 'date_rss' => $dayDatetime->format(DateTime::RSS), + 'date_human' => format_date($dayDatetime, false, true), + 'absolute_url' => $indexUrl . '/daily?day=' . $day, + 'links' => [], + ]; + + foreach ($bookmarks as $key => $bookmark) { + $dataPerDay[$day]['links'][$key] = $formatter->format($bookmark); + + // Make permalink URL absolute + if ($bookmark->isNote()) { + $dataPerDay[$day]['links'][$key]['url'] = $indexUrl . $bookmark->getUrl(); + } + } + } + + $this->assignView('title', $this->container->conf->get('general.title', 'Shaarli')); + $this->assignView('index_url', $indexUrl); + $this->assignView('page_url', $pageUrl); + $this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false)); + $this->assignView('days', $dataPerDay); + + $rssContent = $this->render('dailyrss'); + + $cache->cache($rssContent); + + return $response->write($rssContent); + } + + /** + * We need to spread the articles on 3 columns. + * did not want to use a JavaScript lib like http://masonry.desandro.com/ + * so I manually spread entries with a simple method: I roughly evaluate the + * height of a div according to title and description length. + */ + protected function calculateColumns(array $links): array + { + // Entries to display, for each column. + $columns = [[], [], []]; + // Rough estimate of columns fill. + $fill = [0, 0, 0]; + foreach ($links as $link) { + // Roughly estimate length of entry (by counting characters) + // Title: 30 chars = 1 line. 1 line is 30 pixels height. + // Description: 836 characters gives roughly 342 pixel height. + // This is not perfect, but it's usually OK. + $length = strlen($link['title'] ?? '') + (342 * strlen($link['description'] ?? '')) / 836; + if (! empty($link['thumbnail'])) { + $length += 100; // 1 thumbnails roughly takes 100 pixels height. + } + // Then put in column which is the less filled: + $smallest = min($fill); // find smallest value in array. + $index = array_search($smallest, $fill); // find index of this smallest value. + array_push($columns[$index], $link); // Put entry in this column. + $fill[$index] += $length; + } + + return $columns; + } + + /** + * @param mixed[] $data Variables passed to the template engine + * + * @return mixed[] Template data after active plugins render_picwall hook execution. + */ + protected function executeHooks(array $data): array + { + $this->container->pluginManager->executeHooks( + 'render_daily', + $data, + ['loggedin' => $this->container->loginManager->isLoggedIn()] + ); + + return $data; + } +} diff --git a/application/front/controller/visitor/FeedController.php b/application/front/controller/visitor/FeedController.php new file mode 100644 index 00000000..70664635 --- /dev/null +++ b/application/front/controller/visitor/FeedController.php @@ -0,0 +1,77 @@ +processRequest(FeedBuilder::$FEED_ATOM, $request, $response); + } + + public function rss(Request $request, Response $response): Response + { + return $this->processRequest(FeedBuilder::$FEED_RSS, $request, $response); + } + + protected function processRequest(string $feedType, Request $request, Response $response): Response + { + $response = $response->withHeader('Content-Type', 'application/'. $feedType .'+xml; charset=utf-8'); + + $pageUrl = page_url($this->container->environment); + $cache = $this->container->pageCacheManager->getCachePage($pageUrl); + + $cached = $cache->cachedVersion(); + if (!empty($cached)) { + return $response->write($cached); + } + + // Generate data. + $this->container->feedBuilder->setLocale(strtolower(setlocale(LC_COLLATE, 0))); + $this->container->feedBuilder->setHideDates($this->container->conf->get('privacy.hide_timestamps', false)); + $this->container->feedBuilder->setUsePermalinks( + null !== $request->getParam('permalinks') || !$this->container->conf->get('feed.rss_permalinks') + ); + + $data = $this->container->feedBuilder->buildData($feedType, $request->getParams()); + + $this->executeHooks($data, $feedType); + $this->assignAllView($data); + + $content = $this->render('feed.'. $feedType); + + $cache->cache($content); + + return $response->write($content); + } + + /** + * @param mixed[] $data Template data + * + * @return mixed[] Template data after active plugins hook execution. + */ + protected function executeHooks(array $data, string $feedType): array + { + $this->container->pluginManager->executeHooks( + 'render_feed', + $data, + [ + 'loggedin' => $this->container->loginManager->isLoggedIn(), + 'target' => $feedType, + ] + ); + + return $data; + } +} diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php new file mode 100644 index 00000000..4de2f55d --- /dev/null +++ b/application/front/controller/visitor/LoginController.php @@ -0,0 +1,46 @@ +container->loginManager->isLoggedIn() + || $this->container->conf->get('security.open_shaarli', false) + ) { + return $response->withRedirect('./'); + } + + $userCanLogin = $this->container->loginManager->canLogin($request->getServerParams()); + if ($userCanLogin !== true) { + throw new LoginBannedException(); + } + + if ($request->getParam('username') !== null) { + $this->assignView('username', escape($request->getParam('username'))); + } + + $this + ->assignView('returnurl', escape($request->getServerParam('HTTP_REFERER'))) + ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true)) + ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli')) + ; + + return $response->write($this->render('loginform')); + } +} diff --git a/application/front/controller/visitor/OpenSearchController.php b/application/front/controller/visitor/OpenSearchController.php new file mode 100644 index 00000000..0fd68db6 --- /dev/null +++ b/application/front/controller/visitor/OpenSearchController.php @@ -0,0 +1,26 @@ +withHeader('Content-Type', 'application/opensearchdescription+xml; charset=utf-8'); + + $this->assignView('serverurl', index_url($this->container->environment)); + + return $response->write($this->render('opensearch')); + } +} diff --git a/application/front/controller/visitor/PictureWallController.php b/application/front/controller/visitor/PictureWallController.php new file mode 100644 index 00000000..4e1dce8c --- /dev/null +++ b/application/front/controller/visitor/PictureWallController.php @@ -0,0 +1,70 @@ +container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) { + throw new ThumbnailsDisabledException(); + } + + $this->assignView( + 'pagetitle', + t('Picture wall') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + + // Optionally filter the results: + $links = $this->container->bookmarkService->search($request->getQueryParams()); + $linksToDisplay = []; + + // Get only bookmarks which have a thumbnail. + // Note: we do not retrieve thumbnails here, the request is too heavy. + $formatter = $this->container->formatterFactory->getFormatter('raw'); + foreach ($links as $key => $link) { + if (!empty($link->getThumbnail())) { + $linksToDisplay[] = $formatter->format($link); + } + } + + $data = $this->executeHooks($linksToDisplay); + foreach ($data as $key => $value) { + $this->assignView($key, $value); + } + + return $response->write($this->render('picwall')); + } + + /** + * @param mixed[] $linksToDisplay List of formatted bookmarks + * + * @return mixed[] Template data after active plugins render_picwall hook execution. + */ + protected function executeHooks(array $linksToDisplay): array + { + $data = [ + 'linksToDisplay' => $linksToDisplay, + ]; + $this->container->pluginManager->executeHooks( + 'render_picwall', + $data, + ['loggedin' => $this->container->loginManager->isLoggedIn()] + ); + + return $data; + } +} diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php new file mode 100644 index 00000000..655b3baa --- /dev/null +++ b/application/front/controller/visitor/ShaarliVisitorController.php @@ -0,0 +1,131 @@ +container = $container; + } + + /** + * Assign variables to RainTPL template through the PageBuilder. + * + * @param mixed $value Value to assign to the template + */ + protected function assignView(string $name, $value): self + { + $this->container->pageBuilder->assign($name, $value); + + return $this; + } + + /** + * Assign variables to RainTPL template through the PageBuilder. + * + * @param mixed $data Values to assign to the template and their keys + */ + protected function assignAllView(array $data): self + { + foreach ($data as $key => $value) { + $this->assignView($key, $value); + } + + return $this; + } + + protected function render(string $template): string + { + $this->assignView('linkcount', $this->container->bookmarkService->count(BookmarkFilter::$ALL)); + $this->assignView('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE)); + $this->assignView('plugin_errors', $this->container->pluginManager->getErrors()); + + $this->executeDefaultHooks($template); + + return $this->container->pageBuilder->render($template); + } + + /** + * Call plugin hooks for header, footer and includes, specifying which page will be rendered. + * Then assign generated data to RainTPL. + */ + protected function executeDefaultHooks(string $template): void + { + $common_hooks = [ + 'includes', + 'header', + 'footer', + ]; + + foreach ($common_hooks as $name) { + $plugin_data = []; + $this->container->pluginManager->executeHooks( + 'render_' . $name, + $plugin_data, + [ + 'target' => $template, + 'loggedin' => $this->container->loginManager->isLoggedIn() + ] + ); + $this->assignView('plugins_' . $name, $plugin_data); + } + } + + /** + * Generates a redirection to the previous page, based on the HTTP_REFERER. + * It fails back to the home page. + * + * @param array $loopTerms Terms to remove from path and query string to prevent direction loop. + * @param array $clearParams List of parameter to remove from the query string of the referrer. + */ + protected function redirectFromReferer( + Request $request, + Response $response, + array $loopTerms = [], + array $clearParams = [] + ): Response { + $defaultPath = $request->getUri()->getBasePath(); + $referer = $this->container->environment['HTTP_REFERER'] ?? null; + + if (null !== $referer) { + $currentUrl = parse_url($referer); + parse_str($currentUrl['query'] ?? '', $params); + $path = $currentUrl['path'] ?? $defaultPath; + } else { + $params = []; + $path = $defaultPath; + } + + // Prevent redirection loop + if (isset($currentUrl)) { + foreach ($clearParams as $value) { + unset($params[$value]); + } + + $checkQuery = implode('', array_keys($params)); + foreach ($loopTerms as $value) { + if (strpos($path . $checkQuery, $value) !== false) { + $params = []; + $path = $defaultPath; + break; + } + } + } + + $queryString = count($params) > 0 ? '?'. http_build_query($params) : ''; + + return $response->withRedirect($path . $queryString); + } +} diff --git a/application/front/controller/visitor/TagCloudController.php b/application/front/controller/visitor/TagCloudController.php new file mode 100644 index 00000000..15b6d7b7 --- /dev/null +++ b/application/front/controller/visitor/TagCloudController.php @@ -0,0 +1,131 @@ +processRequest(static::TYPE_CLOUD, $request, $response); + } + + /** + * Display the tag list through the template engine. + * This controller a few filters: + * - Visibility stored in the session for logged in users + * - `searchtags` query parameter: will return tags associated with filter in at least one bookmark + * - `sort` query parameters: + * + `usage` (default): most used tags first + * + `alpha`: alphabetical order + */ + public function list(Request $request, Response $response): Response + { + return $this->processRequest(static::TYPE_LIST, $request, $response); + } + + /** + * Process the request for both tag cloud and tag list endpoints. + */ + protected function processRequest(string $type, Request $request, Response $response): Response + { + if ($this->container->loginManager->isLoggedIn() === true) { + $visibility = $this->container->sessionManager->getSessionParameter('visibility'); + } + + $sort = $request->getQueryParam('sort'); + $searchTags = $request->getQueryParam('searchtags'); + $filteringTags = $searchTags !== null ? explode(' ', $searchTags) : []; + + $tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null); + + if (static::TYPE_CLOUD === $type || 'alpha' === $sort) { + // TODO: the sorting should be handled by bookmarkService instead of the controller + alphabetical_sort($tags, false, true); + } + + if (static::TYPE_CLOUD === $type) { + $tags = $this->formatTagsForCloud($tags); + } + + $searchTags = implode(' ', escape($filteringTags)); + $data = [ + 'search_tags' => $searchTags, + 'tags' => $tags, + ]; + $data = $this->executeHooks('tag' . $type, $data); + foreach ($data as $key => $value) { + $this->assignView($key, $value); + } + + $searchTags = !empty($searchTags) ? $searchTags .' - ' : ''; + $this->assignView( + 'pagetitle', + $searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + + return $response->write($this->render('tag.'. $type)); + } + + /** + * Format the tags array for the tag cloud template. + * + * @param array $tags List of tags as key with count as value + * + * @return mixed[] List of tags as key, with count and expected font size in a subarray + */ + protected function formatTagsForCloud(array $tags): array + { + // We sort tags alphabetically, then choose a font size according to count. + // First, find max value. + $maxCount = count($tags) > 0 ? max($tags) : 0; + $logMaxCount = $maxCount > 1 ? log($maxCount, 30) : 1; + $tagList = []; + foreach ($tags as $key => $value) { + // Tag font size scaling: + // default 15 and 30 logarithm bases affect scaling, + // 2.2 and 0.8 are arbitrary font sizes in em. + $size = log($value, 15) / $logMaxCount * 2.2 + 0.8; + $tagList[$key] = [ + 'count' => $value, + 'size' => number_format($size, 2, '.', ''), + ]; + } + + return $tagList; + } + + /** + * @param mixed[] $data Template data + * + * @return mixed[] Template data after active plugins hook execution. + */ + protected function executeHooks(string $template, array $data): array + { + $this->container->pluginManager->executeHooks( + 'render_'. $template, + $data, + ['loggedin' => $this->container->loginManager->isLoggedIn()] + ); + + return $data; + } +} diff --git a/application/front/controller/visitor/TagController.php b/application/front/controller/visitor/TagController.php new file mode 100644 index 00000000..a0bc1d1b --- /dev/null +++ b/application/front/controller/visitor/TagController.php @@ -0,0 +1,118 @@ +container->environment['HTTP_REFERER'] ?? null; + + // In case browser does not send HTTP_REFERER, we search a single tag + if (null === $referer) { + if (null !== $newTag) { + return $response->withRedirect('./?searchtags='. urlencode($newTag)); + } + + return $response->withRedirect('./'); + } + + $currentUrl = parse_url($referer); + parse_str($currentUrl['query'] ?? '', $params); + + if (null === $newTag) { + return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); + } + + // Prevent redirection loop + if (isset($params['addtag'])) { + unset($params['addtag']); + } + + // Check if this tag is already in the search query and ignore it if it is. + // Each tag is always separated by a space + $currentTags = isset($params['searchtags']) ? explode(' ', $params['searchtags']) : []; + + $addtag = true; + foreach ($currentTags as $value) { + if ($value === $newTag) { + $addtag = false; + break; + } + } + + // Append the tag if necessary + if (true === $addtag) { + $currentTags[] = trim($newTag); + } + + $params['searchtags'] = trim(implode(' ', $currentTags)); + + // We also remove page (keeping the same page has no sense, since the results are different) + unset($params['page']); + + return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); + } + + /** + * Remove a tag from the current search through an HTTP redirection. + * + * @param array $args Should contain `tag` key as tag to remove from current search + */ + public function removeTag(Request $request, Response $response, array $args): Response + { + $referer = $this->container->environment['HTTP_REFERER'] ?? null; + + // If the referrer is not provided, we can update the search, so we failback on the bookmark list + if (empty($referer)) { + return $response->withRedirect('./'); + } + + $tagToRemove = $args['tag'] ?? null; + $currentUrl = parse_url($referer); + parse_str($currentUrl['query'] ?? '', $params); + + if (null === $tagToRemove) { + return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); + } + + // Prevent redirection loop + if (isset($params['removetag'])) { + unset($params['removetag']); + } + + if (isset($params['searchtags'])) { + $tags = explode(' ', $params['searchtags']); + // Remove value from array $tags. + $tags = array_diff($tags, [$tagToRemove]); + $params['searchtags'] = implode(' ', $tags); + + if (empty($params['searchtags'])) { + unset($params['searchtags']); + } + + // We also remove page (keeping the same page has no sense, since the results are different) + unset($params['page']); + } + + $queryParams = count($params) > 0 ? '?' . http_build_query($params) : ''; + + return $response->withRedirect(($currentUrl['path'] ?? './') . $queryParams); + } +} diff --git a/application/front/controllers/DailyController.php b/application/front/controllers/DailyController.php deleted file mode 100644 index 4a0735aa..00000000 --- a/application/front/controllers/DailyController.php +++ /dev/null @@ -1,210 +0,0 @@ -getQueryParam('day') ?? date('Ymd'); - - $availableDates = $this->container->bookmarkService->days(); - $nbAvailableDates = count($availableDates); - $index = array_search($day, $availableDates); - - if ($index === false) { - // no bookmarks for day, but at least one day with bookmarks - $day = $availableDates[$nbAvailableDates - 1] ?? $day; - $previousDay = $availableDates[$nbAvailableDates - 2] ?? ''; - } else { - $previousDay = $availableDates[$index - 1] ?? ''; - $nextDay = $availableDates[$index + 1] ?? ''; - } - - if ($day === date('Ymd')) { - $this->assignView('dayDesc', t('Today')); - } elseif ($day === date('Ymd', strtotime('-1 days'))) { - $this->assignView('dayDesc', t('Yesterday')); - } - - try { - $linksToDisplay = $this->container->bookmarkService->filterDay($day); - } catch (\Exception $exc) { - $linksToDisplay = []; - } - - $formatter = $this->container->formatterFactory->getFormatter(); - // We pre-format some fields for proper output. - foreach ($linksToDisplay as $key => $bookmark) { - $linksToDisplay[$key] = $formatter->format($bookmark); - // This page is a bit specific, we need raw description to calculate the length - $linksToDisplay[$key]['formatedDescription'] = $linksToDisplay[$key]['description']; - $linksToDisplay[$key]['description'] = $bookmark->getDescription(); - } - - $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); - $data = [ - 'linksToDisplay' => $linksToDisplay, - 'day' => $dayDate->getTimestamp(), - 'dayDate' => $dayDate, - 'previousday' => $previousDay ?? '', - 'nextday' => $nextDay ?? '', - ]; - - // Hooks are called before column construction so that plugins don't have to deal with columns. - $this->executeHooks($data); - - $data['cols'] = $this->calculateColumns($data['linksToDisplay']); - - foreach ($data as $key => $value) { - $this->assignView($key, $value); - } - - $mainTitle = $this->container->conf->get('general.title', 'Shaarli'); - $this->assignView( - 'pagetitle', - t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle - ); - - return $response->write($this->render('daily')); - } - - /** - * Daily RSS feed: 1 RSS entry per day giving all the bookmarks on that day. - * Gives the last 7 days (which have bookmarks). - * This RSS feed cannot be filtered and does not trigger plugins yet. - */ - public function rss(Request $request, Response $response): Response - { - $response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8'); - - $pageUrl = page_url($this->container->environment); - $cache = $this->container->pageCacheManager->getCachePage($pageUrl); - - $cached = $cache->cachedVersion(); - if (!empty($cached)) { - return $response->write($cached); - } - - $days = []; - foreach ($this->container->bookmarkService->search() as $bookmark) { - $day = $bookmark->getCreated()->format('Ymd'); - - // Stop iterating after DAILY_RSS_NB_DAYS entries - if (count($days) === static::$DAILY_RSS_NB_DAYS && !isset($days[$day])) { - break; - } - - $days[$day][] = $bookmark; - } - - // Build the RSS feed. - $indexUrl = escape(index_url($this->container->environment)); - - $formatter = $this->container->formatterFactory->getFormatter(); - $formatter->addContextData('index_url', $indexUrl); - - $dataPerDay = []; - - /** @var Bookmark[] $bookmarks */ - foreach ($days as $day => $bookmarks) { - $dayDatetime = DateTimeImmutable::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); - $dataPerDay[$day] = [ - 'date' => $dayDatetime, - 'date_rss' => $dayDatetime->format(DateTime::RSS), - 'date_human' => format_date($dayDatetime, false, true), - 'absolute_url' => $indexUrl . '/daily?day=' . $day, - 'links' => [], - ]; - - foreach ($bookmarks as $key => $bookmark) { - $dataPerDay[$day]['links'][$key] = $formatter->format($bookmark); - - // Make permalink URL absolute - if ($bookmark->isNote()) { - $dataPerDay[$day]['links'][$key]['url'] = $indexUrl . $bookmark->getUrl(); - } - } - } - - $this->assignView('title', $this->container->conf->get('general.title', 'Shaarli')); - $this->assignView('index_url', $indexUrl); - $this->assignView('page_url', $pageUrl); - $this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false)); - $this->assignView('days', $dataPerDay); - - $rssContent = $this->render('dailyrss'); - - $cache->cache($rssContent); - - return $response->write($rssContent); - } - - /** - * We need to spread the articles on 3 columns. - * did not want to use a JavaScript lib like http://masonry.desandro.com/ - * so I manually spread entries with a simple method: I roughly evaluate the - * height of a div according to title and description length. - */ - protected function calculateColumns(array $links): array - { - // Entries to display, for each column. - $columns = [[], [], []]; - // Rough estimate of columns fill. - $fill = [0, 0, 0]; - foreach ($links as $link) { - // Roughly estimate length of entry (by counting characters) - // Title: 30 chars = 1 line. 1 line is 30 pixels height. - // Description: 836 characters gives roughly 342 pixel height. - // This is not perfect, but it's usually OK. - $length = strlen($link['title'] ?? '') + (342 * strlen($link['description'] ?? '')) / 836; - if (! empty($link['thumbnail'])) { - $length += 100; // 1 thumbnails roughly takes 100 pixels height. - } - // Then put in column which is the less filled: - $smallest = min($fill); // find smallest value in array. - $index = array_search($smallest, $fill); // find index of this smallest value. - array_push($columns[$index], $link); // Put entry in this column. - $fill[$index] += $length; - } - - return $columns; - } - - /** - * @param mixed[] $data Variables passed to the template engine - * - * @return mixed[] Template data after active plugins render_picwall hook execution. - */ - protected function executeHooks(array $data): array - { - $this->container->pluginManager->executeHooks( - 'render_daily', - $data, - ['loggedin' => $this->container->loginManager->isLoggedIn()] - ); - - return $data; - } -} diff --git a/application/front/controllers/FeedController.php b/application/front/controllers/FeedController.php deleted file mode 100644 index 78d826d9..00000000 --- a/application/front/controllers/FeedController.php +++ /dev/null @@ -1,79 +0,0 @@ -processRequest(FeedBuilder::$FEED_ATOM, $request, $response); - } - - public function rss(Request $request, Response $response): Response - { - return $this->processRequest(FeedBuilder::$FEED_RSS, $request, $response); - } - - protected function processRequest(string $feedType, Request $request, Response $response): Response - { - $response = $response->withHeader('Content-Type', 'application/'. $feedType .'+xml; charset=utf-8'); - - $pageUrl = page_url($this->container->environment); - $cache = $this->container->pageCacheManager->getCachePage($pageUrl); - - $cached = $cache->cachedVersion(); - if (!empty($cached)) { - return $response->write($cached); - } - - // Generate data. - $this->container->feedBuilder->setLocale(strtolower(setlocale(LC_COLLATE, 0))); - $this->container->feedBuilder->setHideDates($this->container->conf->get('privacy.hide_timestamps', false)); - $this->container->feedBuilder->setUsePermalinks( - null !== $request->getParam('permalinks') || !$this->container->conf->get('feed.rss_permalinks') - ); - - $data = $this->container->feedBuilder->buildData($feedType, $request->getParams()); - - $this->executeHooks($data, $feedType); - $this->assignAllView($data); - - $content = $this->render('feed.'. $feedType); - - $cache->cache($content); - - return $response->write($content); - } - - /** - * @param mixed[] $data Template data - * - * @return mixed[] Template data after active plugins hook execution. - */ - protected function executeHooks(array $data, string $feedType): array - { - $this->container->pluginManager->executeHooks( - 'render_feed', - $data, - [ - 'loggedin' => $this->container->loginManager->isLoggedIn(), - 'target' => $feedType, - ] - ); - - return $data; - } -} diff --git a/application/front/controllers/LoginController.php b/application/front/controllers/LoginController.php deleted file mode 100644 index ae3599e0..00000000 --- a/application/front/controllers/LoginController.php +++ /dev/null @@ -1,48 +0,0 @@ -container->loginManager->isLoggedIn() - || $this->container->conf->get('security.open_shaarli', false) - ) { - return $response->withRedirect('./'); - } - - $userCanLogin = $this->container->loginManager->canLogin($request->getServerParams()); - if ($userCanLogin !== true) { - throw new LoginBannedException(); - } - - if ($request->getParam('username') !== null) { - $this->assignView('username', escape($request->getParam('username'))); - } - - $this - ->assignView('returnurl', escape($request->getServerParam('HTTP_REFERER'))) - ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true)) - ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli')) - ; - - return $response->write($this->render('loginform')); - } -} diff --git a/application/front/controllers/LogoutController.php b/application/front/controllers/LogoutController.php deleted file mode 100644 index aba078c3..00000000 --- a/application/front/controllers/LogoutController.php +++ /dev/null @@ -1,31 +0,0 @@ -container->pageCacheManager->invalidateCaches(); - $this->container->sessionManager->logout(); - - // TODO: switch to a simple Cookie manager allowing to check the session, and create mocks. - setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, $this->container->webPath); - - return $response->withRedirect('./'); - } -} diff --git a/application/front/controllers/OpenSearchController.php b/application/front/controllers/OpenSearchController.php deleted file mode 100644 index fa32c5f1..00000000 --- a/application/front/controllers/OpenSearchController.php +++ /dev/null @@ -1,28 +0,0 @@ -withHeader('Content-Type', 'application/opensearchdescription+xml; charset=utf-8'); - - $this->assignView('serverurl', index_url($this->container->environment)); - - return $response->write($this->render('opensearch')); - } -} diff --git a/application/front/controllers/PictureWallController.php b/application/front/controllers/PictureWallController.php deleted file mode 100644 index 08d31b29..00000000 --- a/application/front/controllers/PictureWallController.php +++ /dev/null @@ -1,72 +0,0 @@ -container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) { - throw new ThumbnailsDisabledException(); - } - - $this->assignView( - 'pagetitle', - t('Picture wall') .' - '. $this->container->conf->get('general.title', 'Shaarli') - ); - - // Optionally filter the results: - $links = $this->container->bookmarkService->search($request->getQueryParams()); - $linksToDisplay = []; - - // Get only bookmarks which have a thumbnail. - // Note: we do not retrieve thumbnails here, the request is too heavy. - $formatter = $this->container->formatterFactory->getFormatter('raw'); - foreach ($links as $key => $link) { - if (!empty($link->getThumbnail())) { - $linksToDisplay[] = $formatter->format($link); - } - } - - $data = $this->executeHooks($linksToDisplay); - foreach ($data as $key => $value) { - $this->assignView($key, $value); - } - - return $response->write($this->render('picwall')); - } - - /** - * @param mixed[] $linksToDisplay List of formatted bookmarks - * - * @return mixed[] Template data after active plugins render_picwall hook execution. - */ - protected function executeHooks(array $linksToDisplay): array - { - $data = [ - 'linksToDisplay' => $linksToDisplay, - ]; - $this->container->pluginManager->executeHooks( - 'render_picwall', - $data, - ['loggedin' => $this->container->loginManager->isLoggedIn()] - ); - - return $data; - } -} diff --git a/application/front/controllers/SessionFilterController.php b/application/front/controllers/SessionFilterController.php deleted file mode 100644 index a021dc37..00000000 --- a/application/front/controllers/SessionFilterController.php +++ /dev/null @@ -1,81 +0,0 @@ -getParam('nb') ?? null; - if (null === $linksPerPage || false === is_numeric($linksPerPage)) { - $linksPerPage = $this->container->conf->get('general.links_per_page', 20); - } - - $this->container->sessionManager->setSessionParameter( - SessionManager::KEY_LINKS_PER_PAGE, - abs(intval($linksPerPage)) - ); - - return $this->redirectFromReferer($response, ['linksperpage'], ['nb']); - } - - /** - * GET /visibility: allows to display only public or only private bookmarks in linklist - */ - public function visibility(Request $request, Response $response, array $args): Response - { - if (false === $this->container->loginManager->isLoggedIn()) { - return $this->redirectFromReferer($response, ['visibility']); - } - - $newVisibility = $args['visibility'] ?? null; - if (false === in_array($newVisibility, [BookmarkFilter::$PRIVATE, BookmarkFilter::$PUBLIC], true)) { - $newVisibility = null; - } - - $currentVisibility = $this->container->sessionManager->getSessionParameter(SessionManager::KEY_VISIBILITY); - - // Visibility not set or not already expected value, set expected value, otherwise reset it - if ($newVisibility !== null && (null === $currentVisibility || $currentVisibility !== $newVisibility)) { - // See only public bookmarks - $this->container->sessionManager->setSessionParameter( - SessionManager::KEY_VISIBILITY, - $newVisibility - ); - } else { - $this->container->sessionManager->deleteSessionParameter(SessionManager::KEY_VISIBILITY); - } - - return $this->redirectFromReferer($response, ['visibility']); - } - - /** - * GET /untagged-only: allows to display only bookmarks without any tag - */ - public function untaggedOnly(Request $request, Response $response): Response - { - $this->container->sessionManager->setSessionParameter( - SessionManager::KEY_UNTAGGED_ONLY, - empty($this->container->sessionManager->getSessionParameter(SessionManager::KEY_UNTAGGED_ONLY)) - ); - - return $this->redirectFromReferer($response, ['untaggedonly', 'untagged-only']); - } -} diff --git a/application/front/controllers/ShaarliController.php b/application/front/controllers/ShaarliController.php deleted file mode 100644 index bfff5fcf..00000000 --- a/application/front/controllers/ShaarliController.php +++ /dev/null @@ -1,126 +0,0 @@ -container = $container; - } - - /** - * Assign variables to RainTPL template through the PageBuilder. - * - * @param mixed $value Value to assign to the template - */ - protected function assignView(string $name, $value): self - { - $this->container->pageBuilder->assign($name, $value); - - return $this; - } - - /** - * Assign variables to RainTPL template through the PageBuilder. - * - * @param mixed $data Values to assign to the template and their keys - */ - protected function assignAllView(array $data): self - { - foreach ($data as $key => $value) { - $this->assignView($key, $value); - } - - return $this; - } - - protected function render(string $template): string - { - $this->assignView('linkcount', $this->container->bookmarkService->count(BookmarkFilter::$ALL)); - $this->assignView('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE)); - $this->assignView('plugin_errors', $this->container->pluginManager->getErrors()); - - $this->executeDefaultHooks($template); - - return $this->container->pageBuilder->render($template); - } - - /** - * Call plugin hooks for header, footer and includes, specifying which page will be rendered. - * Then assign generated data to RainTPL. - */ - protected function executeDefaultHooks(string $template): void - { - $common_hooks = [ - 'includes', - 'header', - 'footer', - ]; - - foreach ($common_hooks as $name) { - $plugin_data = []; - $this->container->pluginManager->executeHooks( - 'render_' . $name, - $plugin_data, - [ - 'target' => $template, - 'loggedin' => $this->container->loginManager->isLoggedIn() - ] - ); - $this->assignView('plugins_' . $name, $plugin_data); - } - } - - /** - * Generates a redirection to the previous page, based on the HTTP_REFERER. - * It fails back to the home page. - * - * @param array $loopTerms Terms to remove from path and query string to prevent direction loop. - * @param array $clearParams List of parameter to remove from the query string of the referrer. - */ - protected function redirectFromReferer(Response $response, array $loopTerms = [], array $clearParams = []): Response - { - $defaultPath = './'; - $referer = $this->container->environment['HTTP_REFERER'] ?? null; - - if (null !== $referer) { - $currentUrl = parse_url($referer); - parse_str($currentUrl['query'] ?? '', $params); - $path = $currentUrl['path'] ?? $defaultPath; - } else { - $params = []; - $path = $defaultPath; - } - - // Prevent redirection loop - if (isset($currentUrl)) { - foreach ($clearParams as $value) { - unset($params[$value]); - } - - $checkQuery = implode('', array_keys($params)); - foreach ($loopTerms as $value) { - if (strpos($path . $checkQuery, $value) !== false) { - $params = []; - $path = $defaultPath; - break; - } - } - } - - $queryString = count($params) > 0 ? '?'. http_build_query($params) : ''; - - return $response->withRedirect($path . $queryString); - } -} diff --git a/application/front/controllers/TagCloudController.php b/application/front/controllers/TagCloudController.php deleted file mode 100644 index 1ff7c2e6..00000000 --- a/application/front/controllers/TagCloudController.php +++ /dev/null @@ -1,133 +0,0 @@ -processRequest(static::TYPE_CLOUD, $request, $response); - } - - /** - * Display the tag list through the template engine. - * This controller a few filters: - * - Visibility stored in the session for logged in users - * - `searchtags` query parameter: will return tags associated with filter in at least one bookmark - * - `sort` query parameters: - * + `usage` (default): most used tags first - * + `alpha`: alphabetical order - */ - public function list(Request $request, Response $response): Response - { - return $this->processRequest(static::TYPE_LIST, $request, $response); - } - - /** - * Process the request for both tag cloud and tag list endpoints. - */ - protected function processRequest(string $type, Request $request, Response $response): Response - { - if ($this->container->loginManager->isLoggedIn() === true) { - $visibility = $this->container->sessionManager->getSessionParameter('visibility'); - } - - $sort = $request->getQueryParam('sort'); - $searchTags = $request->getQueryParam('searchtags'); - $filteringTags = $searchTags !== null ? explode(' ', $searchTags) : []; - - $tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null); - - if (static::TYPE_CLOUD === $type || 'alpha' === $sort) { - // TODO: the sorting should be handled by bookmarkService instead of the controller - alphabetical_sort($tags, false, true); - } - - if (static::TYPE_CLOUD === $type) { - $tags = $this->formatTagsForCloud($tags); - } - - $searchTags = implode(' ', escape($filteringTags)); - $data = [ - 'search_tags' => $searchTags, - 'tags' => $tags, - ]; - $data = $this->executeHooks('tag' . $type, $data); - foreach ($data as $key => $value) { - $this->assignView($key, $value); - } - - $searchTags = !empty($searchTags) ? $searchTags .' - ' : ''; - $this->assignView( - 'pagetitle', - $searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli') - ); - - return $response->write($this->render('tag.'. $type)); - } - - /** - * Format the tags array for the tag cloud template. - * - * @param array $tags List of tags as key with count as value - * - * @return mixed[] List of tags as key, with count and expected font size in a subarray - */ - protected function formatTagsForCloud(array $tags): array - { - // We sort tags alphabetically, then choose a font size according to count. - // First, find max value. - $maxCount = count($tags) > 0 ? max($tags) : 0; - $logMaxCount = $maxCount > 1 ? log($maxCount, 30) : 1; - $tagList = []; - foreach ($tags as $key => $value) { - // Tag font size scaling: - // default 15 and 30 logarithm bases affect scaling, - // 2.2 and 0.8 are arbitrary font sizes in em. - $size = log($value, 15) / $logMaxCount * 2.2 + 0.8; - $tagList[$key] = [ - 'count' => $value, - 'size' => number_format($size, 2, '.', ''), - ]; - } - - return $tagList; - } - - /** - * @param mixed[] $data Template data - * - * @return mixed[] Template data after active plugins hook execution. - */ - protected function executeHooks(string $template, array $data): array - { - $this->container->pluginManager->executeHooks( - 'render_'. $template, - $data, - ['loggedin' => $this->container->loginManager->isLoggedIn()] - ); - - return $data; - } -} diff --git a/application/front/controllers/TagController.php b/application/front/controllers/TagController.php deleted file mode 100644 index a1d5ad5b..00000000 --- a/application/front/controllers/TagController.php +++ /dev/null @@ -1,120 +0,0 @@ -container->environment['HTTP_REFERER'] ?? null; - - // In case browser does not send HTTP_REFERER, we search a single tag - if (null === $referer) { - if (null !== $newTag) { - return $response->withRedirect('./?searchtags='. urlencode($newTag)); - } - - return $response->withRedirect('./'); - } - - $currentUrl = parse_url($referer); - parse_str($currentUrl['query'] ?? '', $params); - - if (null === $newTag) { - return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); - } - - // Prevent redirection loop - if (isset($params['addtag'])) { - unset($params['addtag']); - } - - // Check if this tag is already in the search query and ignore it if it is. - // Each tag is always separated by a space - $currentTags = isset($params['searchtags']) ? explode(' ', $params['searchtags']) : []; - - $addtag = true; - foreach ($currentTags as $value) { - if ($value === $newTag) { - $addtag = false; - break; - } - } - - // Append the tag if necessary - if (true === $addtag) { - $currentTags[] = trim($newTag); - } - - $params['searchtags'] = trim(implode(' ', $currentTags)); - - // We also remove page (keeping the same page has no sense, since the results are different) - unset($params['page']); - - return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); - } - - /** - * Remove a tag from the current search through an HTTP redirection. - * - * @param array $args Should contain `tag` key as tag to remove from current search - */ - public function removeTag(Request $request, Response $response, array $args): Response - { - $referer = $this->container->environment['HTTP_REFERER'] ?? null; - - // If the referrer is not provided, we can update the search, so we failback on the bookmark list - if (empty($referer)) { - return $response->withRedirect('./'); - } - - $tagToRemove = $args['tag'] ?? null; - $currentUrl = parse_url($referer); - parse_str($currentUrl['query'] ?? '', $params); - - if (null === $tagToRemove) { - return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); - } - - // Prevent redirection loop - if (isset($params['removetag'])) { - unset($params['removetag']); - } - - if (isset($params['searchtags'])) { - $tags = explode(' ', $params['searchtags']); - // Remove value from array $tags. - $tags = array_diff($tags, [$tagToRemove]); - $params['searchtags'] = implode(' ', $tags); - - if (empty($params['searchtags'])) { - unset($params['searchtags']); - } - - // We also remove page (keeping the same page has no sense, since the results are different) - unset($params['page']); - } - - $queryParams = count($params) > 0 ? '?' . http_build_query($params) : ''; - - return $response->withRedirect(($currentUrl['path'] ?? './') . $queryParams); - } -} diff --git a/application/front/exceptions/LoginBannedException.php b/application/front/exceptions/LoginBannedException.php index b31a4a14..79d0ea15 100644 --- a/application/front/exceptions/LoginBannedException.php +++ b/application/front/exceptions/LoginBannedException.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Shaarli\Front\Exception; -class LoginBannedException extends ShaarliException +class LoginBannedException extends ShaarliFrontException { public function __construct() { diff --git a/application/front/exceptions/ShaarliException.php b/application/front/exceptions/ShaarliException.php deleted file mode 100644 index 800bfbec..00000000 --- a/application/front/exceptions/ShaarliException.php +++ /dev/null @@ -1,23 +0,0 @@ - Date: Fri, 22 May 2020 13:47:02 +0200 Subject: Process tools page through Slim controller --- .../front/controller/admin/ToolsController.php | 49 ++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 application/front/controller/admin/ToolsController.php (limited to 'application') diff --git a/application/front/controller/admin/ToolsController.php b/application/front/controller/admin/ToolsController.php new file mode 100644 index 00000000..66db5ad9 --- /dev/null +++ b/application/front/controller/admin/ToolsController.php @@ -0,0 +1,49 @@ + index_url($this->container->environment), + 'sslenabled' => is_https($this->container->environment), + ]; + + $this->executeHooks($data); + + foreach ($data as $key => $value) { + $this->assignView($key, $value); + } + + $this->assignView('pagetitle', t('Tools') .' - '. $this->container->conf->get('general.title', 'Shaarli')); + + return $response->write($this->render('tools')); + } + + /** + * @param mixed[] $data Variables passed to the template engine + * + * @return mixed[] Template data after active plugins render_picwall hook execution. + */ + protected function executeHooks(array $data): array + { + $this->container->pluginManager->executeHooks( + 'render_tools', + $data + ); + + return $data; + } +} -- cgit v1.2.3 From ef00f9d2033f6de11e71bf3a909399cae6f73a9f Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 27 May 2020 13:35:48 +0200 Subject: Process password change controller through Slim --- .../front/controller/admin/PasswordController.php | 100 +++++++++++++++++++++ .../controller/admin/ShaarliAdminController.php | 59 ++++++++++++ .../visitor/ShaarliVisitorController.php | 8 ++ .../exceptions/OpenShaarliPasswordException.php | 18 ++++ .../front/exceptions/ShaarliFrontException.php | 4 +- .../front/exceptions/WrongTokenException.php | 18 ++++ application/render/PageBuilder.php | 26 ++++-- application/security/SessionManager.php | 4 + 8 files changed, 230 insertions(+), 7 deletions(-) create mode 100644 application/front/controller/admin/PasswordController.php create mode 100644 application/front/exceptions/OpenShaarliPasswordException.php create mode 100644 application/front/exceptions/WrongTokenException.php (limited to 'application') diff --git a/application/front/controller/admin/PasswordController.php b/application/front/controller/admin/PasswordController.php new file mode 100644 index 00000000..6e8f0bcb --- /dev/null +++ b/application/front/controller/admin/PasswordController.php @@ -0,0 +1,100 @@ +assignView( + 'pagetitle', + t('Change password') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + } + + /** + * GET /password - Displays the change password template + */ + public function index(Request $request, Response $response): Response + { + return $response->write($this->render('changepassword')); + } + + /** + * POST /password - Change admin password - existing and new passwords need to be provided. + */ + public function change(Request $request, Response $response): Response + { + $this->checkToken($request); + + if ($this->container->conf->get('security.open_shaarli', false)) { + throw new OpenShaarliPasswordException(); + } + + $oldPassword = $request->getParam('oldpassword'); + $newPassword = $request->getParam('setpassword'); + + if (empty($newPassword) || empty($oldPassword)) { + $this->saveErrorMessage(t('You must provide the current and new password to change it.')); + + return $response + ->withStatus(400) + ->write($this->render('changepassword')) + ; + } + + // Make sure old password is correct. + $oldHash = sha1( + $oldPassword . + $this->container->conf->get('credentials.login') . + $this->container->conf->get('credentials.salt') + ); + + if ($oldHash !== $this->container->conf->get('credentials.hash')) { + $this->saveErrorMessage(t('The old password is not correct.')); + + return $response + ->withStatus(400) + ->write($this->render('changepassword')) + ; + } + + // Save new password + // Salt renders rainbow-tables attacks useless. + $this->container->conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); + $this->container->conf->set( + 'credentials.hash', + sha1( + $newPassword + . $this->container->conf->get('credentials.login') + . $this->container->conf->get('credentials.salt') + ) + ); + + try { + $this->container->conf->write($this->container->loginManager->isLoggedIn()); + } catch (Throwable $e) { + throw new ShaarliFrontException($e->getMessage(), 500, $e); + } + + $this->saveSuccessMessage(t('Your password has been changed')); + + return $response->write($this->render('changepassword')); + } +} diff --git a/application/front/controller/admin/ShaarliAdminController.php b/application/front/controller/admin/ShaarliAdminController.php index ea703f62..3385006c 100644 --- a/application/front/controller/admin/ShaarliAdminController.php +++ b/application/front/controller/admin/ShaarliAdminController.php @@ -7,7 +7,19 @@ namespace Shaarli\Front\Controller\Admin; use Shaarli\Container\ShaarliContainer; use Shaarli\Front\Controller\Visitor\ShaarliVisitorController; use Shaarli\Front\Exception\UnauthorizedException; +use Shaarli\Front\Exception\WrongTokenException; +use Shaarli\Security\SessionManager; +use Slim\Http\Request; +/** + * Class ShaarliAdminController + * + * All admin controllers (for logged in users) MUST extend this abstract class. + * It makes sure that the user is properly logged in, and otherwise throw an exception + * which will redirect to the login page. + * + * @package Shaarli\Front\Controller\Admin + */ abstract class ShaarliAdminController extends ShaarliVisitorController { public function __construct(ShaarliContainer $container) @@ -18,4 +30,51 @@ abstract class ShaarliAdminController extends ShaarliVisitorController throw new UnauthorizedException(); } } + + /** + * Any persistent action to the config or data store must check the XSRF token validity. + */ + protected function checkToken(Request $request): void + { + if (!$this->container->sessionManager->checkToken($request->getParam('token'))) { + throw new WrongTokenException(); + } + } + + /** + * Save a SUCCESS message in user session, which will be displayed on any template page. + */ + protected function saveSuccessMessage(string $message): void + { + $this->saveMessage(SessionManager::KEY_SUCCESS_MESSAGES, $message); + } + + /** + * Save a WARNING message in user session, which will be displayed on any template page. + */ + protected function saveWarningMessage(string $message): void + { + $this->saveMessage(SessionManager::KEY_WARNING_MESSAGES, $message); + } + + /** + * Save an ERROR message in user session, which will be displayed on any template page. + */ + protected function saveErrorMessage(string $message): void + { + $this->saveMessage(SessionManager::KEY_ERROR_MESSAGES, $message); + } + + /** + * Use the sessionManager to save the provided message using the proper type. + * + * @param string $type successed/warnings/errors + */ + protected function saveMessage(string $type, string $message): void + { + $messages = $this->container->sessionManager->getSessionParameter($type) ?? []; + $messages[] = $message; + + $this->container->sessionManager->setSessionParameter($type, $messages); + } } diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php index 655b3baa..f12915c1 100644 --- a/application/front/controller/visitor/ShaarliVisitorController.php +++ b/application/front/controller/visitor/ShaarliVisitorController.php @@ -9,6 +9,14 @@ use Shaarli\Container\ShaarliContainer; use Slim\Http\Request; use Slim\Http\Response; +/** + * Class ShaarliVisitorController + * + * All controllers accessible by visitors (non logged in users) should extend this abstract class. + * Contains a few helper function for template rendering, plugins, etc. + * + * @package Shaarli\Front\Controller\Visitor + */ abstract class ShaarliVisitorController { /** @var ShaarliContainer */ diff --git a/application/front/exceptions/OpenShaarliPasswordException.php b/application/front/exceptions/OpenShaarliPasswordException.php new file mode 100644 index 00000000..a6f0b3ae --- /dev/null +++ b/application/front/exceptions/OpenShaarliPasswordException.php @@ -0,0 +1,18 @@ +tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width')); $this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height')); - if (!empty($_SESSION['warnings'])) { - $this->tpl->assign('global_warnings', $_SESSION['warnings']); - unset($_SESSION['warnings']); - } - $this->tpl->assign('formatter', $this->conf->get('formatter', 'default')); // To be removed with a proper theme configuration. $this->tpl->assign('conf', $this->conf); } + protected function finalize(): void + { + // TODO: use the SessionManager + $messageKeys = [ + SessionManager::KEY_SUCCESS_MESSAGES, + SessionManager::KEY_WARNING_MESSAGES, + SessionManager::KEY_ERROR_MESSAGES + ]; + foreach ($messageKeys as $messageKey) { + if (!empty($_SESSION[$messageKey])) { + $this->tpl->assign('global_' . $messageKey, $_SESSION[$messageKey]); + unset($_SESSION[$messageKey]); + } + } + } + /** * The following assign() method is basically the same as RainTPL (except lazy loading) * @@ -196,6 +208,8 @@ class PageBuilder $this->initialize(); } + $this->finalize(); + $this->tpl->draw($page); } @@ -213,6 +227,8 @@ class PageBuilder $this->initialize(); } + $this->finalize(); + return $this->tpl->draw($page, true); } diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php index 8b77d362..0ac17d9a 100644 --- a/application/security/SessionManager.php +++ b/application/security/SessionManager.php @@ -12,6 +12,10 @@ class SessionManager public const KEY_VISIBILITY = 'visibility'; public const KEY_UNTAGGED_ONLY = 'untaggedonly'; + public const KEY_SUCCESS_MESSAGES = 'successes'; + public const KEY_WARNING_MESSAGES = 'warnings'; + public const KEY_ERROR_MESSAGES = 'errors'; + /** @var int Session expiration timeout, in seconds */ public static $SHORT_TIMEOUT = 3600; // 1 hour -- cgit v1.2.3 From fdedbfd4a7fb547da0e0ce65c6180f74aad90691 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 27 May 2020 14:13:49 +0200 Subject: Test ShaarliAdminController --- application/front/controller/admin/ShaarliAdminController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'application') diff --git a/application/front/controller/admin/ShaarliAdminController.php b/application/front/controller/admin/ShaarliAdminController.php index 3385006c..3bc5bb6b 100644 --- a/application/front/controller/admin/ShaarliAdminController.php +++ b/application/front/controller/admin/ShaarliAdminController.php @@ -34,11 +34,13 @@ abstract class ShaarliAdminController extends ShaarliVisitorController /** * Any persistent action to the config or data store must check the XSRF token validity. */ - protected function checkToken(Request $request): void + protected function checkToken(Request $request): bool { if (!$this->container->sessionManager->checkToken($request->getParam('token'))) { throw new WrongTokenException(); } + + return true; } /** -- cgit v1.2.3 From 66063ed1a18d739b1a60bfb163d8656417a4c529 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 30 May 2020 14:00:06 +0200 Subject: Process configure page through Slim controller --- .../front/controller/admin/ConfigureController.php | 120 +++++++++++++++++++++ application/render/PageBuilder.php | 4 + 2 files changed, 124 insertions(+) create mode 100644 application/front/controller/admin/ConfigureController.php (limited to 'application') diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php new file mode 100644 index 00000000..b1d32270 --- /dev/null +++ b/application/front/controller/admin/ConfigureController.php @@ -0,0 +1,120 @@ +assignView('title', $this->container->conf->get('general.title', 'Shaarli')); + $this->assignView('theme', $this->container->conf->get('resource.theme')); + $this->assignView( + 'theme_available', + ThemeUtils::getThemes($this->container->conf->get('resource.raintpl_tpl')) + ); + $this->assignView('formatter_available', ['default', 'markdown']); + list($continents, $cities) = generateTimeZoneData( + timezone_identifiers_list(), + $this->container->conf->get('general.timezone') + ); + $this->assignView('continents', $continents); + $this->assignView('cities', $cities); + $this->assignView('retrieve_description', $this->container->conf->get('general.retrieve_description', false)); + $this->assignView('private_links_default', $this->container->conf->get('privacy.default_private_links', false)); + $this->assignView( + 'session_protection_disabled', + $this->container->conf->get('security.session_protection_disabled', false) + ); + $this->assignView('enable_rss_permalinks', $this->container->conf->get('feed.rss_permalinks', false)); + $this->assignView('enable_update_check', $this->container->conf->get('updates.check_updates', true)); + $this->assignView('hide_public_links', $this->container->conf->get('privacy.hide_public_links', false)); + $this->assignView('api_enabled', $this->container->conf->get('api.enabled', true)); + $this->assignView('api_secret', $this->container->conf->get('api.secret')); + $this->assignView('languages', Languages::getAvailableLanguages()); + $this->assignView('gd_enabled', extension_loaded('gd')); + $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)); + $this->assignView('pagetitle', t('Configure') .' - '. $this->container->conf->get('general.title', 'Shaarli')); + + return $response->write($this->render('configure')); + } + + /** + * POST /configure - Update Shaarli's configuration + */ + public function save(Request $request, Response $response): Response + { + $this->checkToken($request); + + $continent = $request->getParam('continent'); + $city = $request->getParam('city'); + $tz = 'UTC'; + if (null !== $continent && null !== $city && isTimeZoneValid($continent, $city)) { + $tz = $continent . '/' . $city; + } + + $this->container->conf->set('general.timezone', $tz); + $this->container->conf->set('general.title', escape($request->getParam('title'))); + $this->container->conf->set('general.header_link', escape($request->getParam('titleLink'))); + $this->container->conf->set('general.retrieve_description', !empty($request->getParam('retrieveDescription'))); + $this->container->conf->set('resource.theme', escape($request->getParam('theme'))); + $this->container->conf->set( + 'security.session_protection_disabled', + !empty($request->getParam('disablesessionprotection')) + ); + $this->container->conf->set( + 'privacy.default_private_links', + !empty($request->getParam('privateLinkByDefault')) + ); + $this->container->conf->set('feed.rss_permalinks', !empty($request->getParam('enableRssPermalinks'))); + $this->container->conf->set('updates.check_updates', !empty($request->getParam('updateCheck'))); + $this->container->conf->set('privacy.hide_public_links', !empty($request->getParam('hidePublicLinks'))); + $this->container->conf->set('api.enabled', !empty($request->getParam('enableApi'))); + $this->container->conf->set('api.secret', escape($request->getParam('apiSecret'))); + $this->container->conf->set('formatter', escape($request->getParam('formatter'))); + + if (!empty($request->getParam('language'))) { + $this->container->conf->set('translation.language', escape($request->getParam('language'))); + } + + $thumbnailsMode = extension_loaded('gd') ? $request->getParam('enableThumbnails') : Thumbnailer::MODE_NONE; + if ($thumbnailsMode !== Thumbnailer::MODE_NONE + && $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) + ) { + $this->saveWarningMessage(t( + 'You have enabled or changed thumbnails mode. ' + .'Please synchronize them.' + )); + } + $this->container->conf->set('thumbnails.mode', $thumbnailsMode); + + try { + $this->container->conf->write($this->container->loginManager->isLoggedIn()); + $this->container->history->updateSettings(); + $this->container->pageCacheManager->invalidateCaches(); + } catch (Throwable $e) { + // TODO: translation + stacktrace + $this->saveErrorMessage('ERROR while writing config file after configuration update.'); + } + + $this->saveSuccessMessage(t('Configuration was saved.')); + + return $response->withRedirect('./configure'); + } +} diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php index 264cd33b..d90ed58b 100644 --- a/application/render/PageBuilder.php +++ b/application/render/PageBuilder.php @@ -143,6 +143,10 @@ class PageBuilder $this->tpl->assign('conf', $this->conf); } + /** + * Affect variable after controller processing. + * Used for alert messages. + */ protected function finalize(): void { // TODO: use the SessionManager -- cgit v1.2.3 From 8eac2e54882d8adae8cbb45386dca1b465242632 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 30 May 2020 15:51:14 +0200 Subject: Process manage tags page through Slim controller --- .../front/controller/admin/ConfigureController.php | 2 +- .../front/controller/admin/ManageTagController.php | 87 ++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 application/front/controller/admin/ManageTagController.php (limited to 'application') diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php index b1d32270..5a482d8e 100644 --- a/application/front/controller/admin/ConfigureController.php +++ b/application/front/controller/admin/ConfigureController.php @@ -12,7 +12,7 @@ use Slim\Http\Response; use Throwable; /** - * Class PasswordController + * Class ConfigureController * * Slim controller used to handle Shaarli configuration page (display + save new config). */ diff --git a/application/front/controller/admin/ManageTagController.php b/application/front/controller/admin/ManageTagController.php new file mode 100644 index 00000000..e015e613 --- /dev/null +++ b/application/front/controller/admin/ManageTagController.php @@ -0,0 +1,87 @@ +getParam('fromtag') ?? ''; + + $this->assignView('fromtag', escape($fromTag)); + $this->assignView( + 'pagetitle', + t('Manage tags') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + + return $response->write($this->render('changetag')); + } + + /** + * POST /manage-tags - Update or delete provided tag + */ + public function save(Request $request, Response $response): Response + { + $this->checkToken($request); + + $isDelete = null !== $request->getParam('deletetag') && null === $request->getParam('renametag'); + + $fromTag = escape(trim($request->getParam('fromtag') ?? '')); + $toTag = escape(trim($request->getParam('totag') ?? '')); + + if (0 === strlen($fromTag) || false === $isDelete && 0 === strlen($toTag)) { + $this->saveWarningMessage(t('Invalid tags provided.')); + + return $response->withRedirect('./manage-tags'); + } + + // TODO: move this to bookmark service + $count = 0; + $bookmarks = $this->container->bookmarkService->search(['searchtags' => $fromTag], BookmarkFilter::$ALL, true); + foreach ($bookmarks as $bookmark) { + if (false === $isDelete) { + $bookmark->renameTag($fromTag, $toTag); + } else { + $bookmark->deleteTag($fromTag); + } + + $this->container->bookmarkService->set($bookmark, false); + $this->container->history->updateLink($bookmark); + $count++; + } + + $this->container->bookmarkService->save(); + + if (true === $isDelete) { + $alert = sprintf( + t('The tag was removed from %d bookmark.', 'The tag was removed from %d bookmarks.', $count), + $count + ); + } else { + $alert = sprintf( + t('The tag was renamed in %d bookmark.', 'The tag was renamed in %d bookmarks.', $count), + $count + ); + } + + $this->saveSuccessMessage($alert); + + $redirect = true === $isDelete ? './manage-tags' : './?searchtags='. urlencode($toTag); + + return $response->withRedirect($redirect); + } +} -- cgit v1.2.3 From c22fa57a5505fe95fd01860e3d3dfbb089f869cd Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 6 Jun 2020 14:01:03 +0200 Subject: Handle shaare creation/edition/deletion through Slim controllers --- application/Utils.php | 4 + application/bookmark/LinkUtils.php | 106 --------- application/container/ContainerBuilder.php | 10 + application/container/ShaarliContainer.php | 4 + .../controller/admin/PostBookmarkController.php | 258 +++++++++++++++++++++ .../front/controller/admin/ToolsController.php | 2 +- .../front/controller/visitor/DailyController.php | 2 +- .../front/controller/visitor/FeedController.php | 2 +- .../visitor/ShaarliVisitorController.php | 14 +- application/http/HttpAccess.php | 39 ++++ application/http/HttpUtils.php | 106 +++++++++ 11 files changed, 432 insertions(+), 115 deletions(-) create mode 100644 application/front/controller/admin/PostBookmarkController.php create mode 100644 application/http/HttpAccess.php (limited to 'application') diff --git a/application/Utils.php b/application/Utils.php index 72c90049..9c9eaaa2 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -91,6 +91,10 @@ function endsWith($haystack, $needle, $case = true) */ function escape($input) { + if (null === $input) { + return null; + } + if (is_bool($input)) { return $input; } diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index 98d9038a..68914fca 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php @@ -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. * diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index 84406979..85126246 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -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; } } diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php index deb07197..fec398d0 100644 --- a/application/container/ShaarliContainer.php +++ b/application/container/ShaarliContainer.php @@ -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 index 00000000..dbe570e2 --- /dev/null +++ b/application/front/controller/admin/PostBookmarkController.php @@ -0,0 +1,258 @@ +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(''); + } + + 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(''); + } + + // 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; + } +} diff --git a/application/front/controller/admin/ToolsController.php b/application/front/controller/admin/ToolsController.php index 66db5ad9..d087f2cd 100644 --- a/application/front/controller/admin/ToolsController.php +++ b/application/front/controller/admin/ToolsController.php @@ -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); diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index 47e2503a..e5c9ddac 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php @@ -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']); diff --git a/application/front/controller/visitor/FeedController.php b/application/front/controller/visitor/FeedController.php index 70664635..f76f55fd 100644 --- a/application/front/controller/visitor/FeedController.php +++ b/application/front/controller/visitor/FeedController.php @@ -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); diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php index f12915c1..98423d90 100644 --- a/application/front/controller/visitor/ShaarliVisitorController.php +++ b/application/front/controller/visitor/ShaarliVisitorController.php @@ -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 index 00000000..81d9e076 --- /dev/null +++ b/application/http/HttpAccess.php @@ -0,0 +1,39 @@ + Date: Sat, 13 Jun 2020 11:22:14 +0200 Subject: Explicitly define base and asset path in templates With the new routes, all pages are not all at the same folder level anymore (e.g. /shaare and /shaare/123), so we can't just use './' everywhere. The most consistent way to handle this is to prefix all path with the proper variable, and handle the actual path in controllers. --- application/container/ShaarliContainer.php | 1 + application/front/ShaarliMiddleware.php | 2 ++ .../front/controller/visitor/ShaarliVisitorController.php | 15 ++++++++++++++- application/render/PageBuilder.php | 4 ++++ 4 files changed, 21 insertions(+), 1 deletion(-) (limited to 'application') diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php index fec398d0..a95393cd 100644 --- a/application/container/ShaarliContainer.php +++ b/application/container/ShaarliContainer.php @@ -22,6 +22,7 @@ use Slim\Container; * Extension of Slim container to document the injected objects. * * @property mixed[] $environment $_SERVER automatically injected by Slim + * @property string $basePath Shaarli's instance base path (e.g. `/shaarli/`) * @property ConfigManager $conf * @property SessionManager $sessionManager * @property LoginManager $loginManager diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php index f8992e0b..47aa61bb 100644 --- a/application/front/ShaarliMiddleware.php +++ b/application/front/ShaarliMiddleware.php @@ -39,6 +39,8 @@ class ShaarliMiddleware public function __invoke(Request $request, Response $response, callable $next) { try { + $this->container->basePath = rtrim($request->getUri()->getBasePath(), '/'); + $response = $next($request, $response); } catch (ShaarliFrontException $e) { $this->container->pageBuilder->assign('message', $e->getMessage()); diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php index 98423d90..b90b1e8f 100644 --- a/application/front/controller/visitor/ShaarliVisitorController.php +++ b/application/front/controller/visitor/ShaarliVisitorController.php @@ -60,6 +60,19 @@ abstract class ShaarliVisitorController $this->assignView('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE)); $this->assignView('plugin_errors', $this->container->pluginManager->getErrors()); + /* + * Define base path (if Shaarli is installed in a domain's subfolder, e.g. `/shaarli`) + * and the asset path (subfolder/tpl/default for default theme). + * These MUST be used to create an internal link or to include an asset in templates. + */ + $this->assignView('base_path', $this->container->basePath); + $this->assignView( + 'asset_path', + $this->container->basePath . '/' . + rtrim($this->container->conf->get('resource.raintpl_tpl', 'tpl'), '/') . '/' . + $this->container->conf->get('resource.theme', 'default') + ); + $this->executeDefaultHooks($template); return $this->container->pageBuilder->render($template); @@ -105,7 +118,7 @@ abstract class ShaarliVisitorController array $clearParams = [], string $anchor = null ): Response { - $defaultPath = rtrim($request->getUri()->getBasePath(), '/') . '/'; + $defaultPath = $this->container->basePath . '/'; $referer = $this->container->environment['HTTP_REFERER'] ?? null; if (null !== $referer) { diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php index d90ed58b..2779eb90 100644 --- a/application/render/PageBuilder.php +++ b/application/render/PageBuilder.php @@ -149,6 +149,10 @@ class PageBuilder */ protected function finalize(): void { + //FIXME - DEV _ REMOVE ME + $this->assign('base_path', '/Shaarli'); + $this->assign('asset_path', '/Shaarli/tpl/default'); + // TODO: use the SessionManager $messageKeys = [ SessionManager::KEY_SUCCESS_MESSAGES, -- cgit v1.2.3 From 9c75f877935fa6adec951a4d8d32b328aaab314f Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 13 Jun 2020 13:08:01 +0200 Subject: Use multi-level routes for existing controllers instead of 1 level everywhere Also prefix most admin routes with /admin/ --- application/container/ContainerBuilder.php | 10 ++++------ application/container/ShaarliContainer.php | 17 ++++++++--------- application/front/ShaarliMiddleware.php | 6 +++--- .../front/controller/admin/ConfigureController.php | 6 +++--- application/front/controller/admin/LogoutController.php | 4 ++-- .../front/controller/admin/ManageTagController.php | 10 +++++----- .../front/controller/admin/PasswordController.php | 4 ++-- .../front/controller/admin/PostBookmarkController.php | 17 ++++++++++------- .../front/controller/visitor/LoginController.php | 2 +- .../controller/visitor/ShaarliVisitorController.php | 13 +++++++++++++ application/front/controller/visitor/TagController.php | 8 +++++--- 11 files changed, 56 insertions(+), 41 deletions(-) (limited to 'application') diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index 85126246..72a85710 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -38,19 +38,17 @@ class ContainerBuilder /** @var LoginManager */ protected $login; - /** @var string */ - protected $webPath; + /** @var string|null */ + protected $basePath = null; public function __construct( ConfigManager $conf, SessionManager $session, - LoginManager $login, - string $webPath + LoginManager $login ) { $this->conf = $conf; $this->session = $session; $this->login = $login; - $this->webPath = $webPath; } public function build(): ShaarliContainer @@ -60,7 +58,7 @@ class ContainerBuilder $container['conf'] = $this->conf; $container['sessionManager'] = $this->session; $container['loginManager'] = $this->login; - $container['webPath'] = $this->webPath; + $container['basePath'] = $this->basePath; $container['plugins'] = function (ShaarliContainer $container): PluginManager { return new PluginManager($container->conf); diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php index a95393cd..4b97aae2 100644 --- a/application/container/ShaarliContainer.php +++ b/application/container/ShaarliContainer.php @@ -21,21 +21,20 @@ use Slim\Container; /** * Extension of Slim container to document the injected objects. * - * @property mixed[] $environment $_SERVER automatically injected by Slim * @property string $basePath Shaarli's instance base path (e.g. `/shaarli/`) + * @property BookmarkServiceInterface $bookmarkService * @property ConfigManager $conf - * @property SessionManager $sessionManager - * @property LoginManager $loginManager - * @property string $webPath + * @property mixed[] $environment $_SERVER automatically injected by Slim + * @property FeedBuilder $feedBuilder + * @property FormatterFactory $formatterFactory * @property History $history - * @property BookmarkServiceInterface $bookmarkService + * @property HttpAccess $httpAccess + * @property LoginManager $loginManager * @property PageBuilder $pageBuilder - * @property PluginManager $pluginManager - * @property FormatterFactory $formatterFactory * @property PageCacheManager $pageCacheManager - * @property FeedBuilder $feedBuilder + * @property PluginManager $pluginManager + * @property SessionManager $sessionManager * @property Thumbnailer $thumbnailer - * @property HttpAccess $httpAccess */ class ShaarliContainer extends Container { diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php index 47aa61bb..7ad610c7 100644 --- a/application/front/ShaarliMiddleware.php +++ b/application/front/ShaarliMiddleware.php @@ -38,9 +38,9 @@ class ShaarliMiddleware */ public function __invoke(Request $request, Response $response, callable $next) { - try { - $this->container->basePath = rtrim($request->getUri()->getBasePath(), '/'); + $this->container->basePath = rtrim($request->getUri()->getBasePath(), '/'); + try { $response = $next($request, $response); } catch (ShaarliFrontException $e) { $this->container->pageBuilder->assign('message', $e->getMessage()); @@ -54,7 +54,7 @@ class ShaarliMiddleware $response = $response->withStatus($e->getCode()); $response = $response->write($this->container->pageBuilder->render('error')); } catch (UnauthorizedException $e) { - return $response->withRedirect($request->getUri()->getBasePath() . '/login'); + return $response->withRedirect($this->container->basePath . '/login'); } return $response; diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php index 5a482d8e..44971c43 100644 --- a/application/front/controller/admin/ConfigureController.php +++ b/application/front/controller/admin/ConfigureController.php @@ -19,7 +19,7 @@ use Throwable; class ConfigureController extends ShaarliAdminController { /** - * GET /configure - Displays the configuration page + * GET /admin/configure - Displays the configuration page */ public function index(Request $request, Response $response): Response { @@ -56,7 +56,7 @@ class ConfigureController extends ShaarliAdminController } /** - * POST /configure - Update Shaarli's configuration + * POST /admin/configure - Update Shaarli's configuration */ public function save(Request $request, Response $response): Response { @@ -115,6 +115,6 @@ class ConfigureController extends ShaarliAdminController $this->saveSuccessMessage(t('Configuration was saved.')); - return $response->withRedirect('./configure'); + return $this->redirect($response, '/admin/configure'); } } diff --git a/application/front/controller/admin/LogoutController.php b/application/front/controller/admin/LogoutController.php index 41e81984..c5984814 100644 --- a/application/front/controller/admin/LogoutController.php +++ b/application/front/controller/admin/LogoutController.php @@ -22,8 +22,8 @@ class LogoutController extends ShaarliAdminController $this->container->sessionManager->logout(); // TODO: switch to a simple Cookie manager allowing to check the session, and create mocks. - setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, $this->container->webPath); + setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, $this->container->basePath . '/'); - return $response->withRedirect('./'); + return $this->redirect($response, '/'); } } diff --git a/application/front/controller/admin/ManageTagController.php b/application/front/controller/admin/ManageTagController.php index e015e613..7dab288a 100644 --- a/application/front/controller/admin/ManageTagController.php +++ b/application/front/controller/admin/ManageTagController.php @@ -16,7 +16,7 @@ use Slim\Http\Response; class ManageTagController extends ShaarliAdminController { /** - * GET /manage-tags - Displays the manage tags page + * GET /admin/tags - Displays the manage tags page */ public function index(Request $request, Response $response): Response { @@ -32,7 +32,7 @@ class ManageTagController extends ShaarliAdminController } /** - * POST /manage-tags - Update or delete provided tag + * POST /admin/tags - Update or delete provided tag */ public function save(Request $request, Response $response): Response { @@ -46,7 +46,7 @@ class ManageTagController extends ShaarliAdminController if (0 === strlen($fromTag) || false === $isDelete && 0 === strlen($toTag)) { $this->saveWarningMessage(t('Invalid tags provided.')); - return $response->withRedirect('./manage-tags'); + return $this->redirect($response, '/admin/tags'); } // TODO: move this to bookmark service @@ -80,8 +80,8 @@ class ManageTagController extends ShaarliAdminController $this->saveSuccessMessage($alert); - $redirect = true === $isDelete ? './manage-tags' : './?searchtags='. urlencode($toTag); + $redirect = true === $isDelete ? '/admin/tags' : '/?searchtags='. urlencode($toTag); - return $response->withRedirect($redirect); + return $this->redirect($response, $redirect); } } diff --git a/application/front/controller/admin/PasswordController.php b/application/front/controller/admin/PasswordController.php index 6e8f0bcb..bcce01a6 100644 --- a/application/front/controller/admin/PasswordController.php +++ b/application/front/controller/admin/PasswordController.php @@ -29,7 +29,7 @@ class PasswordController extends ShaarliAdminController } /** - * GET /password - Displays the change password template + * GET /admin/password - Displays the change password template */ public function index(Request $request, Response $response): Response { @@ -37,7 +37,7 @@ class PasswordController extends ShaarliAdminController } /** - * POST /password - Change admin password - existing and new passwords need to be provided. + * POST /admin/password - Change admin password - existing and new passwords need to be provided. */ public function change(Request $request, Response $response): Response { diff --git a/application/front/controller/admin/PostBookmarkController.php b/application/front/controller/admin/PostBookmarkController.php index dbe570e2..f3ee5dea 100644 --- a/application/front/controller/admin/PostBookmarkController.php +++ b/application/front/controller/admin/PostBookmarkController.php @@ -19,7 +19,7 @@ use Slim\Http\Response; class PostBookmarkController extends ShaarliAdminController { /** - * GET /add-shaare - Displays the form used to create a new bookmark from an URL + * GET /admin/add-shaare - Displays the form used to create a new bookmark from an URL */ public function addShaare(Request $request, Response $response): Response { @@ -32,7 +32,7 @@ class PostBookmarkController extends ShaarliAdminController } /** - * GET /shaare - Displays the bookmark form for creation. + * 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 @@ -93,7 +93,7 @@ class PostBookmarkController extends ShaarliAdminController } /** - * GET /shaare-{id} - Displays the bookmark form in edition mode. + * GET /admin/shaare/{id} - Displays the bookmark form in edition mode. */ public function displayEditForm(Request $request, Response $response, array $args): Response { @@ -106,7 +106,7 @@ class PostBookmarkController extends ShaarliAdminController } catch (BookmarkNotFoundException $e) { $this->saveErrorMessage(t('Bookmark not found')); - return $response->withRedirect('./'); + return $this->redirect($response, '/'); } $formatter = $this->container->formatterFactory->getFormatter('raw'); @@ -116,7 +116,7 @@ class PostBookmarkController extends ShaarliAdminController } /** - * POST /shaare + * POST /admin/shaare */ public function save(Request $request, Response $response): Response { @@ -170,11 +170,14 @@ class PostBookmarkController extends ShaarliAdminController ); } + /** + * GET /admin/shaare/delete + */ public function deleteBookmark(Request $request, Response $response): Response { $this->checkToken($request); - $ids = escape(trim($request->getParam('lf_linkdate'))); + $ids = escape(trim($request->getParam('id'))); if (strpos($ids, ' ') !== false) { // multiple, space-separated ids provided $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'strlen')); @@ -207,7 +210,7 @@ class PostBookmarkController extends ShaarliAdminController } // Don't redirect to where we were previously because the datastore has changed. - return $response->withRedirect('./'); + return $this->redirect($response, '/'); } protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php index 4de2f55d..0db1f463 100644 --- a/application/front/controller/visitor/LoginController.php +++ b/application/front/controller/visitor/LoginController.php @@ -23,7 +23,7 @@ class LoginController extends ShaarliVisitorController if ($this->container->loginManager->isLoggedIn() || $this->container->conf->get('security.open_shaarli', false) ) { - return $response->withRedirect('./'); + return $this->redirect($response, '/'); } $userCanLogin = $this->container->loginManager->canLogin($request->getServerParams()); diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php index b90b1e8f..b494a8e6 100644 --- a/application/front/controller/visitor/ShaarliVisitorController.php +++ b/application/front/controller/visitor/ShaarliVisitorController.php @@ -104,6 +104,19 @@ abstract class ShaarliVisitorController } } + /** + * Simple helper which prepend the base path to redirect path. + * + * @param Response $response + * @param string $path Absolute path, e.g.: `/`, or `/admin/shaare/123` regardless of install directory + * + * @return Response updated + */ + protected function redirect(Response $response, string $path): Response + { + return $response->withRedirect($this->container->basePath . $path); + } + /** * Generates a redirection to the previous page, based on the HTTP_REFERER. * It fails back to the home page. diff --git a/application/front/controller/visitor/TagController.php b/application/front/controller/visitor/TagController.php index a0bc1d1b..c176f43f 100644 --- a/application/front/controller/visitor/TagController.php +++ b/application/front/controller/visitor/TagController.php @@ -11,6 +11,8 @@ use Slim\Http\Response; * Class TagController * * Slim controller handle tags. + * + * TODO: check redirections with new helper */ class TagController extends ShaarliVisitorController { @@ -27,10 +29,10 @@ class TagController extends ShaarliVisitorController // In case browser does not send HTTP_REFERER, we search a single tag if (null === $referer) { if (null !== $newTag) { - return $response->withRedirect('./?searchtags='. urlencode($newTag)); + return $this->redirect($response, '/?searchtags='. urlencode($newTag)); } - return $response->withRedirect('./'); + return $this->redirect($response, '/'); } $currentUrl = parse_url($referer); @@ -81,7 +83,7 @@ class TagController extends ShaarliVisitorController // If the referrer is not provided, we can update the search, so we failback on the bookmark list if (empty($referer)) { - return $response->withRedirect('./'); + return $this->redirect($response, '/'); } $tagToRemove = $args['tag'] ?? null; -- cgit v1.2.3 From baa6979194573855b260593094983c33ec338dc7 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 13 Jun 2020 15:37:02 +0200 Subject: Improve ManageTagController coverage and error handling --- .../controller/admin/ManageShaareController.php | 281 +++++++++++++++++++++ .../controller/admin/PostBookmarkController.php | 261 ------------------- 2 files changed, 281 insertions(+), 261 deletions(-) create mode 100644 application/front/controller/admin/ManageShaareController.php delete mode 100644 application/front/controller/admin/PostBookmarkController.php (limited to 'application') diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php new file mode 100644 index 00000000..620bbc40 --- /dev/null +++ b/application/front/controller/admin/ManageShaareController.php @@ -0,0 +1,281 @@ +assignView( + 'pagetitle', + t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + + return $response->write($this->render('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 (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 /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') ? 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(''); + } + + 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() + ); + } + + /** + * GET /admin/shaare/delete + */ + 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->container->pluginManager->executeHooks('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, '/'); + } + + /** + * 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 = [ + '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; + } +} diff --git a/application/front/controller/admin/PostBookmarkController.php b/application/front/controller/admin/PostBookmarkController.php deleted file mode 100644 index f3ee5dea..00000000 --- a/application/front/controller/admin/PostBookmarkController.php +++ /dev/null @@ -1,261 +0,0 @@ -assignView( - 'pagetitle', - t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli') - ); - - return $response->write($this->render('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 (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 /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($id); // Read database - } catch (BookmarkNotFoundException $e) { - $this->saveErrorMessage(t('Bookmark not found')); - - 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') ? 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(''); - } - - 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() - ); - } - - /** - * GET /admin/shaare/delete - */ - public function deleteBookmark(Request $request, Response $response): Response - { - $this->checkToken($request); - - $ids = escape(trim($request->getParam('id'))); - 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(''); - } - - // Don't redirect to where we were previously because the datastore has changed. - return $this->redirect($response, '/'); - } - - 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; - } -} -- cgit v1.2.3 From 7b8a6f2858248601d43c1b8247deb91b74392d2e Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 13 Jun 2020 19:40:32 +0200 Subject: Process change visibility action through Slim controller --- .../controller/admin/ManageShaareController.php | 70 +++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) (limited to 'application') diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php index 620bbc40..ff330a99 100644 --- a/application/front/controller/admin/ManageShaareController.php +++ b/application/front/controller/admin/ManageShaareController.php @@ -174,7 +174,7 @@ class ManageShaareController extends ShaarliAdminController } /** - * GET /admin/shaare/delete + * GET /admin/shaare/delete - Delete one or multiple bookmarks (depending on `id` query parameter). */ public function deleteBookmark(Request $request, Response $response): Response { @@ -228,6 +228,74 @@ class ManageShaareController extends ShaarliAdminController 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->container->pluginManager->executeHooks('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']); + } + /** * Helper function used to display the shaare form whether it's a new or existing bookmark. * -- cgit v1.2.3 From 3447d888d7881eed437117a6de2450abb96f6a76 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 15 Jun 2020 08:15:40 +0200 Subject: Pin bookmarks through Slim controller --- .../controller/admin/ManageShaareController.php | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) (limited to 'application') diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php index ff330a99..bdfc5ca7 100644 --- a/application/front/controller/admin/ManageShaareController.php +++ b/application/front/controller/admin/ManageShaareController.php @@ -296,6 +296,42 @@ class ManageShaareController extends ShaarliAdminController 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->container->pluginManager->executeHooks('save_link', $data); + $bookmark->fromArray($data); + + $this->container->bookmarkService->set($bookmark); + + return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); + } + /** * Helper function used to display the shaare form whether it's a new or existing bookmark. * -- cgit v1.2.3 From e8a10f312a5c44314292402bb44e6ee2e71f3d5d Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 17 Jun 2020 15:55:31 +0200 Subject: Use NetscapeBookmarkUtils object instance instead of static calls --- application/container/ContainerBuilder.php | 5 ++ application/container/ShaarliContainer.php | 2 + application/netscape/NetscapeBookmarkUtils.php | 118 ++++++++++++++----------- 3 files changed, 71 insertions(+), 54 deletions(-) (limited to 'application') diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index 72a85710..a4fd6a0c 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -11,6 +11,7 @@ use Shaarli\Feed\FeedBuilder; use Shaarli\Formatter\FormatterFactory; use Shaarli\History; use Shaarli\Http\HttpAccess; +use Shaarli\Netscape\NetscapeBookmarkUtils; use Shaarli\Plugin\PluginManager; use Shaarli\Render\PageBuilder; use Shaarli\Render\PageCacheManager; @@ -118,6 +119,10 @@ class ContainerBuilder return new HttpAccess(); }; + $container['netscapeBookmarkUtils'] = function (ShaarliContainer $container): NetscapeBookmarkUtils { + return new NetscapeBookmarkUtils($container->bookmarkService, $container->conf, $container->history); + }; + return $container; } } diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php index 4b97aae2..b08fa4cb 100644 --- a/application/container/ShaarliContainer.php +++ b/application/container/ShaarliContainer.php @@ -10,6 +10,7 @@ use Shaarli\Feed\FeedBuilder; use Shaarli\Formatter\FormatterFactory; use Shaarli\History; use Shaarli\Http\HttpAccess; +use Shaarli\Netscape\NetscapeBookmarkUtils; use Shaarli\Plugin\PluginManager; use Shaarli\Render\PageBuilder; use Shaarli\Render\PageCacheManager; @@ -30,6 +31,7 @@ use Slim\Container; * @property History $history * @property HttpAccess $httpAccess * @property LoginManager $loginManager + * @property NetscapeBookmarkUtils $netscapeBookmarkUtils * @property PageBuilder $pageBuilder * @property PageCacheManager $pageCacheManager * @property PluginManager $pluginManager diff --git a/application/netscape/NetscapeBookmarkUtils.php b/application/netscape/NetscapeBookmarkUtils.php index d64eef7f..8557cca2 100644 --- a/application/netscape/NetscapeBookmarkUtils.php +++ b/application/netscape/NetscapeBookmarkUtils.php @@ -16,10 +16,24 @@ use Shaarli\NetscapeBookmarkParser\NetscapeBookmarkParser; /** * Utilities to import and export bookmarks using the Netscape format - * TODO: Not static, use a container. */ class NetscapeBookmarkUtils { + /** @var BookmarkServiceInterface */ + protected $bookmarkService; + + /** @var ConfigManager */ + protected $conf; + + /** @var History */ + protected $history; + + public function __construct(BookmarkServiceInterface $bookmarkService, ConfigManager $conf, History $history) + { + $this->bookmarkService = $bookmarkService; + $this->conf = $conf; + $this->history = $history; + } /** * Filters bookmarks and adds Netscape-formatted fields @@ -28,18 +42,16 @@ class NetscapeBookmarkUtils * - timestamp link addition date, using the Unix epoch format * - taglist comma-separated tag list * - * @param BookmarkServiceInterface $bookmarkService Link datastore * @param BookmarkFormatter $formatter instance * @param string $selection Which bookmarks to export: (all|private|public) * @param bool $prependNoteUrl Prepend note permalinks with the server's URL * @param string $indexUrl Absolute URL of the Shaarli index page * * @return array The bookmarks to be exported, with additional fields - *@throws Exception Invalid export selection * + * @throws Exception Invalid export selection */ - public static function filterAndFormat( - $bookmarkService, + public function filterAndFormat( $formatter, $selection, $prependNoteUrl, @@ -51,7 +63,7 @@ class NetscapeBookmarkUtils } $bookmarkLinks = array(); - foreach ($bookmarkService->search([], $selection) as $bookmark) { + foreach ($this->bookmarkService->search([], $selection) as $bookmark) { $link = $formatter->format($bookmark); $link['taglist'] = implode(',', $bookmark->getTags()); if ($bookmark->isNote() && $prependNoteUrl) { @@ -64,53 +76,15 @@ class NetscapeBookmarkUtils return $bookmarkLinks; } - /** - * Generates an import status summary - * - * @param string $filename name of the file to import - * @param int $filesize size of the file to import - * @param int $importCount how many bookmarks were imported - * @param int $overwriteCount how many bookmarks were overwritten - * @param int $skipCount how many bookmarks were skipped - * @param int $duration how many seconds did the import take - * - * @return string Summary of the bookmark import status - */ - private static function importStatus( - $filename, - $filesize, - $importCount = 0, - $overwriteCount = 0, - $skipCount = 0, - $duration = 0 - ) { - $status = sprintf(t('File %s (%d bytes) '), $filename, $filesize); - if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) { - $status .= t('has an unknown file format. Nothing was imported.'); - } else { - $status .= vsprintf( - t( - 'was successfully processed in %d seconds: ' - . '%d bookmarks imported, %d bookmarks overwritten, %d bookmarks skipped.' - ), - [$duration, $importCount, $overwriteCount, $skipCount] - ); - } - return $status; - } - /** * Imports Web bookmarks from an uploaded Netscape bookmark dump * * @param array $post Server $_POST parameters * @param array $files Server $_FILES parameters - * @param BookmarkServiceInterface $bookmarkService Loaded LinkDB instance - * @param ConfigManager $conf instance - * @param History $history History instance * * @return string Summary of the bookmark import status */ - public static function import($post, $files, $bookmarkService, $conf, $history) + public function import($post, $files) { $start = time(); $filename = $files['filetoupload']['name']; @@ -141,11 +115,11 @@ class NetscapeBookmarkUtils true, // nested tag support $defaultTags, // additional user-specified tags strval(1 - $defaultPrivacy), // defaultPub = 1 - defaultPrivacy - $conf->get('resource.data_dir') // log path, will be overridden + $this->conf->get('resource.data_dir') // log path, will be overridden ); $logger = new Logger( - $conf->get('resource.data_dir'), - !$conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG, + $this->conf->get('resource.data_dir'), + !$this->conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG, [ 'prefix' => 'import.', 'extension' => 'log', @@ -171,7 +145,7 @@ class NetscapeBookmarkUtils $private = 0; } - $link = $bookmarkService->findByUrl($bkm['uri']); + $link = $this->bookmarkService->findByUrl($bkm['uri']); $existingLink = $link !== null; if (! $existingLink) { $link = new Bookmark(); @@ -193,20 +167,21 @@ class NetscapeBookmarkUtils } $link->setTitle($bkm['title']); - $link->setUrl($bkm['uri'], $conf->get('security.allowed_protocols')); + $link->setUrl($bkm['uri'], $this->conf->get('security.allowed_protocols')); $link->setDescription($bkm['note']); $link->setPrivate($private); $link->setTagsString($bkm['tags']); - $bookmarkService->addOrSet($link, false); + $this->bookmarkService->addOrSet($link, false); $importCount++; } - $bookmarkService->save(); - $history->importLinks(); + $this->bookmarkService->save(); + $this->history->importLinks(); $duration = time() - $start; - return self::importStatus( + + return $this->importStatus( $filename, $filesize, $importCount, @@ -215,4 +190,39 @@ class NetscapeBookmarkUtils $duration ); } + + /** + * Generates an import status summary + * + * @param string $filename name of the file to import + * @param int $filesize size of the file to import + * @param int $importCount how many bookmarks were imported + * @param int $overwriteCount how many bookmarks were overwritten + * @param int $skipCount how many bookmarks were skipped + * @param int $duration how many seconds did the import take + * + * @return string Summary of the bookmark import status + */ + protected function importStatus( + $filename, + $filesize, + $importCount = 0, + $overwriteCount = 0, + $skipCount = 0, + $duration = 0 + ) { + $status = sprintf(t('File %s (%d bytes) '), $filename, $filesize); + if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) { + $status .= t('has an unknown file format. Nothing was imported.'); + } else { + $status .= vsprintf( + t( + 'was successfully processed in %d seconds: ' + . '%d bookmarks imported, %d bookmarks overwritten, %d bookmarks skipped.' + ), + [$duration, $importCount, $overwriteCount, $skipCount] + ); + } + return $status; + } } -- cgit v1.2.3 From c70ff64a61d62cc8d35a62f30596ecc2a3c578a3 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 17 Jun 2020 16:04:18 +0200 Subject: Process bookmark exports through Slim controllers --- .../front/controller/admin/ExportController.php | 92 ++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 application/front/controller/admin/ExportController.php (limited to 'application') diff --git a/application/front/controller/admin/ExportController.php b/application/front/controller/admin/ExportController.php new file mode 100644 index 00000000..8e0e5a56 --- /dev/null +++ b/application/front/controller/admin/ExportController.php @@ -0,0 +1,92 @@ +assignView('pagetitle', t('Export') .' - '. $this->container->conf->get('general.title', 'Shaarli')); + + return $response->write($this->render('export')); + } + + /** + * POST /admin/export - Process export, and serve download file named + * bookmarks_(all|private|public)_datetime.html + */ + public function export(Request $request, Response $response): Response + { + $selection = $request->getParam('selection'); + + if (empty($selection)) { + $this->saveErrorMessage(t('Please select an export mode.')); + + return $this->redirect($response, '/admin/export'); + } + + $prependNoteUrl = filter_var($request->getParam('prepend_note_url') ?? false, FILTER_VALIDATE_BOOLEAN); + + try { + $formatter = $this->container->formatterFactory->getFormatter('raw'); + + $this->assignView( + 'links', + $this->container->netscapeBookmarkUtils->filterAndFormat( + $formatter, + $selection, + $prependNoteUrl, + index_url($this->container->environment) + ) + ); + } catch (\Exception $exc) { + $this->saveErrorMessage($exc->getMessage()); + + return $this->redirect($response, '/admin/export'); + } + + $now = new DateTime(); + $response = $response->withHeader('Content-Type', 'text/html; charset=utf-8'); + $response = $response->withHeader( + 'Content-disposition', + 'attachment; filename=bookmarks_'.$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html' + ); + + $this->assignView('date', $now->format(DateTime::RFC822)); + $this->assignView('eol', PHP_EOL); + $this->assignView('selection', $selection); + + return $response->write($this->render('export.bookmarks')); + } + + /** + * @param mixed[] $data Variables passed to the template engine + * + * @return mixed[] Template data after active plugins render_picwall hook execution. + */ + protected function executeHooks(array $data): array + { + $this->container->pluginManager->executeHooks( + 'render_tools', + $data + ); + + return $data; + } +} -- cgit v1.2.3 From 78657347c5b463d7c22bfc8c87b7db39fe058833 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 17 Jun 2020 19:08:02 +0200 Subject: Process bookmarks import through Slim controller --- .../front/controller/admin/ExportController.php | 17 +---- .../front/controller/admin/ImportController.php | 81 ++++++++++++++++++++++ application/netscape/NetscapeBookmarkUtils.php | 15 ++-- 3 files changed, 91 insertions(+), 22 deletions(-) create mode 100644 application/front/controller/admin/ImportController.php (limited to 'application') diff --git a/application/front/controller/admin/ExportController.php b/application/front/controller/admin/ExportController.php index 8e0e5a56..7afbfc23 100644 --- a/application/front/controller/admin/ExportController.php +++ b/application/front/controller/admin/ExportController.php @@ -33,6 +33,8 @@ class ExportController extends ShaarliAdminController */ public function export(Request $request, Response $response): Response { + $this->checkToken($request); + $selection = $request->getParam('selection'); if (empty($selection)) { @@ -74,19 +76,4 @@ class ExportController extends ShaarliAdminController return $response->write($this->render('export.bookmarks')); } - - /** - * @param mixed[] $data Variables passed to the template engine - * - * @return mixed[] Template data after active plugins render_picwall hook execution. - */ - protected function executeHooks(array $data): array - { - $this->container->pluginManager->executeHooks( - 'render_tools', - $data - ); - - return $data; - } } diff --git a/application/front/controller/admin/ImportController.php b/application/front/controller/admin/ImportController.php new file mode 100644 index 00000000..8c5305b9 --- /dev/null +++ b/application/front/controller/admin/ImportController.php @@ -0,0 +1,81 @@ +assignView( + 'maxfilesize', + get_max_upload_size( + ini_get('post_max_size'), + ini_get('upload_max_filesize'), + false + ) + ); + $this->assignView( + 'maxfilesizeHuman', + get_max_upload_size( + ini_get('post_max_size'), + ini_get('upload_max_filesize'), + true + ) + ); + $this->assignView('pagetitle', t('Import') .' - '. $this->container->conf->get('general.title', 'Shaarli')); + + return $response->write($this->render('import')); + } + + /** + * POST /admin/import - Process import file provided and create bookmarks + */ + public function import(Request $request, Response $response): Response + { + $this->checkToken($request); + + $file = ($request->getUploadedFiles() ?? [])['filetoupload'] ?? null; + if (!$file instanceof UploadedFileInterface) { + $this->saveErrorMessage(t('No import file provided.')); + + return $this->redirect($response, '/admin/import'); + } + + + // Import bookmarks from an uploaded file + if (0 === $file->getSize()) { + // The file is too big or some form field may be missing. + $msg = sprintf( + t( + 'The file you are trying to upload is probably bigger than what this webserver can accept' + .' (%s). Please upload in smaller chunks.' + ), + get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')) + ); + $this->saveErrorMessage($msg); + + return $this->redirect($response, '/admin/import'); + } + + $status = $this->container->netscapeBookmarkUtils->import($request->getParams(), $file); + + $this->saveSuccessMessage($status); + + return $this->redirect($response, '/admin/import'); + } +} diff --git a/application/netscape/NetscapeBookmarkUtils.php b/application/netscape/NetscapeBookmarkUtils.php index 8557cca2..b150f649 100644 --- a/application/netscape/NetscapeBookmarkUtils.php +++ b/application/netscape/NetscapeBookmarkUtils.php @@ -6,6 +6,7 @@ use DateTime; use DateTimeZone; use Exception; use Katzgrau\KLogger\Logger; +use Psr\Http\Message\UploadedFileInterface; use Psr\Log\LogLevel; use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\BookmarkServiceInterface; @@ -79,20 +80,20 @@ class NetscapeBookmarkUtils /** * Imports Web bookmarks from an uploaded Netscape bookmark dump * - * @param array $post Server $_POST parameters - * @param array $files Server $_FILES parameters + * @param array $post Server $_POST parameters + * @param UploadedFileInterface $file File in PSR-7 object format * * @return string Summary of the bookmark import status */ - public function import($post, $files) + public function import($post, UploadedFileInterface $file) { $start = time(); - $filename = $files['filetoupload']['name']; - $filesize = $files['filetoupload']['size']; - $data = file_get_contents($files['filetoupload']['tmp_name']); + $filename = $file->getClientFilename(); + $filesize = $file->getSize(); + $data = (string) $file->getStream(); if (preg_match('//i', $data) === 0) { - return self::importStatus($filename, $filesize); + return $this->importStatus($filename, $filesize); } // Overwrite existing bookmarks? -- cgit v1.2.3 From 1b8620b1ad4e2c647ff2d032c8e7c6687b6647a1 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 20 Jun 2020 15:14:24 +0200 Subject: Process plugins administration page through Slim controllers --- application/container/ContainerBuilder.php | 7 +- .../front/controller/admin/PluginsController.php | 98 ++++++++++++++++++++++ application/plugin/PluginManager.php | 2 +- 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 application/front/controller/admin/PluginsController.php (limited to 'application') diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index a4fd6a0c..ba91fe8b 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -88,7 +88,12 @@ class ContainerBuilder }; $container['pluginManager'] = function (ShaarliContainer $container): PluginManager { - return new PluginManager($container->conf); + $pluginManager = new PluginManager($container->conf); + + // FIXME! Configuration is already injected + $pluginManager->load($container->conf->get('general.enabled_plugins')); + + return $pluginManager; }; $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory { diff --git a/application/front/controller/admin/PluginsController.php b/application/front/controller/admin/PluginsController.php new file mode 100644 index 00000000..d5ec91f0 --- /dev/null +++ b/application/front/controller/admin/PluginsController.php @@ -0,0 +1,98 @@ +container->pluginManager->getPluginsMeta(); + + // Split plugins into 2 arrays: ordered enabled plugins and disabled. + $enabledPlugins = array_filter($pluginMeta, function ($v) { + return ($v['order'] ?? false) !== false; + }); + $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $this->container->conf->get('plugins', [])); + uasort( + $enabledPlugins, + function ($a, $b) { + return $a['order'] - $b['order']; + } + ); + $disabledPlugins = array_filter($pluginMeta, function ($v) { + return ($v['order'] ?? false) === false; + }); + + $this->assignView('enabledPlugins', $enabledPlugins); + $this->assignView('disabledPlugins', $disabledPlugins); + $this->assignView( + 'pagetitle', + t('Plugin Administration') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + + return $response->write($this->render('pluginsadmin')); + } + + /** + * POST /admin/plugins - Update Shaarli's configuration + */ + public function save(Request $request, Response $response): Response + { + $this->checkToken($request); + + try { + $parameters = $request->getParams() ?? []; + + $this->executeHooks($parameters); + + if (isset($parameters['parameters_form'])) { + unset($parameters['parameters_form']); + foreach ($parameters as $param => $value) { + $this->container->conf->set('plugins.'. $param, escape($value)); + } + } else { + $this->container->conf->set('general.enabled_plugins', save_plugin_config($parameters)); + } + + $this->container->conf->write($this->container->loginManager->isLoggedIn()); + $this->container->history->updateSettings(); + + $this->saveSuccessMessage(t('Setting successfully saved.')); + } catch (Exception $e) { + $this->saveErrorMessage( + t('ERROR while saving plugin configuration: ') . PHP_EOL . $e->getMessage() + ); + } + + return $this->redirect($response, '/admin/plugins'); + } + + /** + * @param mixed[] $data Variables passed to the template engine + * + * @return mixed[] Template data after active plugins render_picwall hook execution. + */ + protected function executeHooks(array $data): array + { + $this->container->pluginManager->executeHooks( + 'save_plugin_parameters', + $data + ); + + return $data; + } +} diff --git a/application/plugin/PluginManager.php b/application/plugin/PluginManager.php index f7b24a8e..591a9180 100644 --- a/application/plugin/PluginManager.php +++ b/application/plugin/PluginManager.php @@ -16,7 +16,7 @@ class PluginManager * * @var array $authorizedPlugins */ - private $authorizedPlugins; + private $authorizedPlugins = []; /** * List of loaded plugins. -- cgit v1.2.3 From 764d34a7d347d653414e5f5c632e02499edaef04 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 21 Jun 2020 12:21:31 +0200 Subject: Process token retrieve through Slim controller --- .../front/controller/admin/TokenController.php | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 application/front/controller/admin/TokenController.php (limited to 'application') diff --git a/application/front/controller/admin/TokenController.php b/application/front/controller/admin/TokenController.php new file mode 100644 index 00000000..08d68d0a --- /dev/null +++ b/application/front/controller/admin/TokenController.php @@ -0,0 +1,26 @@ +withHeader('Content-Type', 'text/plain'); + + return $response->write($this->container->sessionManager->generateToken()); + } +} -- cgit v1.2.3 From 6132d64748dfc6806ed25f71d2e078a5ed29d071 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 27 Jun 2020 12:08:26 +0200 Subject: Process thumbnail synchronize page through Slim controllers --- application/Thumbnailer.php | 3 +- .../front/controller/admin/ConfigureController.php | 2 +- .../controller/admin/ThumbnailsController.php | 79 ++++++++++++++++++++++ application/legacy/LegacyUpdater.php | 2 +- 4 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 application/front/controller/admin/ThumbnailsController.php (limited to 'application') diff --git a/application/Thumbnailer.php b/application/Thumbnailer.php index 314baf0d..5aec23c8 100644 --- a/application/Thumbnailer.php +++ b/application/Thumbnailer.php @@ -4,7 +4,6 @@ namespace Shaarli; use Shaarli\Config\ConfigManager; use WebThumbnailer\Application\ConfigManager as WTConfigManager; -use WebThumbnailer\Exception\WebThumbnailerException; use WebThumbnailer\WebThumbnailer; /** @@ -90,7 +89,7 @@ class Thumbnailer try { return $this->wt->thumbnail($url); - } catch (WebThumbnailerException $e) { + } catch (\Throwable $e) { // Exceptions are only thrown in debug mode. error_log(get_class($e) . ': ' . $e->getMessage()); } diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php index 44971c43..201a859b 100644 --- a/application/front/controller/admin/ConfigureController.php +++ b/application/front/controller/admin/ConfigureController.php @@ -99,7 +99,7 @@ class ConfigureController extends ShaarliAdminController ) { $this->saveWarningMessage(t( 'You have enabled or changed thumbnails mode. ' - .'Please synchronize them.' + .'Please synchronize them.' )); } $this->container->conf->set('thumbnails.mode', $thumbnailsMode); diff --git a/application/front/controller/admin/ThumbnailsController.php b/application/front/controller/admin/ThumbnailsController.php new file mode 100644 index 00000000..e5308510 --- /dev/null +++ b/application/front/controller/admin/ThumbnailsController.php @@ -0,0 +1,79 @@ +container->bookmarkService->search() as $bookmark) { + // A note or not HTTP(S) + if ($bookmark->isNote() || !startsWith(strtolower($bookmark->getUrl()), 'http')) { + continue; + } + + $ids[] = $bookmark->getId(); + } + + $this->assignView('ids', $ids); + $this->assignView( + 'pagetitle', + t('Thumbnails update') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + + return $response->write($this->render('thumbnails')); + } + + /** + * PATCH /admin/shaare/{id}/thumbnail-update - Route for AJAX calls + */ + public function ajaxUpdate(Request $request, Response $response, array $args): Response + { + $id = $args['id'] ?? null; + + if (false === ctype_digit($id)) { + return $response->withStatus(400); + } + + try { + $bookmark = $this->container->bookmarkService->get($id); + } catch (BookmarkNotFoundException $e) { + return $response->withStatus(404); + } + + $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); + $this->container->bookmarkService->set($bookmark); + + return $response->withJson($this->container->formatterFactory->getFormatter('raw')->format($bookmark)); + } + + /** + * @param mixed[] $data Variables passed to the template engine + * + * @return mixed[] Template data after active plugins render_picwall hook execution. + */ + protected function executeHooks(array $data): array + { + $this->container->pluginManager->executeHooks( + 'render_tools', + $data + ); + + return $data; + } +} diff --git a/application/legacy/LegacyUpdater.php b/application/legacy/LegacyUpdater.php index 8d5cd071..cbf6890f 100644 --- a/application/legacy/LegacyUpdater.php +++ b/application/legacy/LegacyUpdater.php @@ -534,7 +534,7 @@ class LegacyUpdater if ($thumbnailsEnabled) { $this->session['warnings'][] = t( - 'You have enabled or changed thumbnails mode. Please synchronize them.' + 'You have enabled or changed thumbnails mode. Please synchronize them.' ); } -- cgit v1.2.3 From 1a8ac737e52cb25a5c346232ee398f5908cee7d7 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 6 Jul 2020 08:04:35 +0200 Subject: Process main page (linklist) through Slim controller Including a bunch of improvements on the container, and helper used across new controllers. --- application/Router.php | 184 --------------- application/api/ApiMiddleware.php | 9 +- application/bookmark/BookmarkFileService.php | 2 +- application/container/ContainerBuilder.php | 11 + application/container/ShaarliContainer.php | 7 +- application/front/ShaarliMiddleware.php | 73 +++++- .../front/controller/admin/ConfigureController.php | 3 +- .../front/controller/admin/ExportController.php | 5 +- .../front/controller/admin/ImportController.php | 3 +- .../controller/admin/ManageShaareController.php | 5 +- .../front/controller/admin/ManageTagController.php | 3 +- .../front/controller/admin/PasswordController.php | 9 +- .../front/controller/admin/PluginsController.php | 3 +- .../controller/admin/ThumbnailsController.php | 18 +- .../front/controller/admin/ToolsController.php | 3 +- .../controller/visitor/BookmarkListController.php | 248 +++++++++++++++++++++ .../front/controller/visitor/DailyController.php | 5 +- .../front/controller/visitor/LoginController.php | 3 +- .../controller/visitor/OpenSearchController.php | 3 +- .../controller/visitor/PictureWallController.php | 3 +- application/legacy/LegacyController.php | 130 +++++++++++ application/legacy/LegacyRouter.php | 187 ++++++++++++++++ application/legacy/UnknowLegacyRouteException.php | 9 + application/render/PageBuilder.php | 9 + application/render/TemplatePage.php | 33 +++ application/updater/Updater.php | 43 +++- 26 files changed, 778 insertions(+), 233 deletions(-) delete mode 100644 application/Router.php create mode 100644 application/front/controller/visitor/BookmarkListController.php create mode 100644 application/legacy/LegacyController.php create mode 100644 application/legacy/LegacyRouter.php create mode 100644 application/legacy/UnknowLegacyRouteException.php create mode 100644 application/render/TemplatePage.php (limited to 'application') diff --git a/application/Router.php b/application/Router.php deleted file mode 100644 index d7187487..00000000 --- a/application/Router.php +++ /dev/null @@ -1,184 +0,0 @@ -getApiResponse(); } - return $response; + return $response + ->withHeader('Access-Control-Allow-Origin', '*') + ->withHeader( + 'Access-Control-Allow-Headers', + 'X-Requested-With, Content-Type, Accept, Origin, Authorization' + ) + ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') + ; } /** diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 7439d8d8..3d15d4c9 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -93,7 +93,7 @@ class BookmarkFileService implements BookmarkServiceInterface throw new Exception('Not authorized'); } - return $bookmark; + return $first; } /** diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index ba91fe8b..ccb87c3a 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -18,6 +18,8 @@ use Shaarli\Render\PageCacheManager; use Shaarli\Security\LoginManager; use Shaarli\Security\SessionManager; use Shaarli\Thumbnailer; +use Shaarli\Updater\Updater; +use Shaarli\Updater\UpdaterUtils; /** * Class ContainerBuilder @@ -128,6 +130,15 @@ class ContainerBuilder return new NetscapeBookmarkUtils($container->bookmarkService, $container->conf, $container->history); }; + $container['updater'] = function (ShaarliContainer $container): Updater { + return new Updater( + UpdaterUtils::read_updates_file($container->conf->get('resource.updates')), + $container->bookmarkService, + $container->conf, + $container->loginManager->isLoggedIn() + ); + }; + return $container; } } diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php index b08fa4cb..09e7d5b1 100644 --- a/application/container/ShaarliContainer.php +++ b/application/container/ShaarliContainer.php @@ -17,15 +17,17 @@ use Shaarli\Render\PageCacheManager; use Shaarli\Security\LoginManager; use Shaarli\Security\SessionManager; use Shaarli\Thumbnailer; +use Shaarli\Updater\Updater; use Slim\Container; /** * Extension of Slim container to document the injected objects. * - * @property string $basePath Shaarli's instance base path (e.g. `/shaarli/`) + * @property string $basePath Shaarli's instance base path (e.g. `/shaarli/`) * @property BookmarkServiceInterface $bookmarkService * @property ConfigManager $conf - * @property mixed[] $environment $_SERVER automatically injected by Slim + * @property mixed[] $environment $_SERVER automatically injected by Slim + * @property callable $errorHandler Overrides default Slim error display * @property FeedBuilder $feedBuilder * @property FormatterFactory $formatterFactory * @property History $history @@ -37,6 +39,7 @@ use Slim\Container; * @property PluginManager $pluginManager * @property SessionManager $sessionManager * @property Thumbnailer $thumbnailer + * @property Updater $updater */ class ShaarliContainer extends Container { diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php index 7ad610c7..baea6ef2 100644 --- a/application/front/ShaarliMiddleware.php +++ b/application/front/ShaarliMiddleware.php @@ -25,6 +25,8 @@ class ShaarliMiddleware /** * Middleware execution: + * - run updates + * - if not logged in open shaarli, redirect to login * - execute the controller * - return the response * @@ -36,27 +38,82 @@ class ShaarliMiddleware * * @return Response response. */ - public function __invoke(Request $request, Response $response, callable $next) + public function __invoke(Request $request, Response $response, callable $next): Response { $this->container->basePath = rtrim($request->getUri()->getBasePath(), '/'); try { - $response = $next($request, $response); + $this->runUpdates(); + $this->checkOpenShaarli($request, $response, $next); + + return $next($request, $response); } catch (ShaarliFrontException $e) { + // Possible functional error + $this->container->pageBuilder->reset(); $this->container->pageBuilder->assign('message', $e->getMessage()); + + $response = $response->withStatus($e->getCode()); + + return $response->write($this->container->pageBuilder->render('error')); + } catch (UnauthorizedException $e) { + return $response->withRedirect($this->container->basePath . '/login'); + } catch (\Throwable $e) { + // Unknown error encountered + $this->container->pageBuilder->reset(); if ($this->container->conf->get('dev.debug', false)) { + $this->container->pageBuilder->assign('message', $e->getMessage()); $this->container->pageBuilder->assign( 'stacktrace', - nl2br(get_class($this) .': '. $e->getTraceAsString()) + nl2br(get_class($e) .': '. PHP_EOL . $e->getTraceAsString()) ); + } else { + $this->container->pageBuilder->assign('message', t('An unexpected error occurred.')); } - $response = $response->withStatus($e->getCode()); - $response = $response->write($this->container->pageBuilder->render('error')); - } catch (UnauthorizedException $e) { - return $response->withRedirect($this->container->basePath . '/login'); + $response = $response->withStatus(500); + + return $response->write($this->container->pageBuilder->render('error')); + } + } + + /** + * Run the updater for every requests processed while logged in. + */ + protected function runUpdates(): void + { + if ($this->container->loginManager->isLoggedIn() !== true) { + return; + } + + $newUpdates = $this->container->updater->update(); + if (!empty($newUpdates)) { + $this->container->updater->writeUpdates( + $this->container->conf->get('resource.updates'), + $this->container->updater->getDoneUpdates() + ); + + $this->container->pageCacheManager->invalidateCaches(); + } + } + + /** + * Access is denied to most pages with `hide_public_links` + `force_login` settings. + */ + protected function checkOpenShaarli(Request $request, Response $response, callable $next): bool + { + if (// if the user isn't logged in + !$this->container->loginManager->isLoggedIn() + // and Shaarli doesn't have public content... + && $this->container->conf->get('privacy.hide_public_links') + // and is configured to enforce the login + && $this->container->conf->get('privacy.force_login') + // and the current page isn't already the login page + // and the user is not requesting a feed (which would lead to a different content-type as expected) + && !in_array($next->getName(), ['login', 'atom', 'rss'], true) + ) { + throw new UnauthorizedException(); } - return $response; + return true; } } diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php index 201a859b..865fc2b0 100644 --- a/application/front/controller/admin/ConfigureController.php +++ b/application/front/controller/admin/ConfigureController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Admin; use Shaarli\Languages; +use Shaarli\Render\TemplatePage; use Shaarli\Render\ThemeUtils; use Shaarli\Thumbnailer; use Slim\Http\Request; @@ -52,7 +53,7 @@ class ConfigureController extends ShaarliAdminController $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)); $this->assignView('pagetitle', t('Configure') .' - '. $this->container->conf->get('general.title', 'Shaarli')); - return $response->write($this->render('configure')); + return $response->write($this->render(TemplatePage::CONFIGURE)); } /** diff --git a/application/front/controller/admin/ExportController.php b/application/front/controller/admin/ExportController.php index 7afbfc23..2be957fa 100644 --- a/application/front/controller/admin/ExportController.php +++ b/application/front/controller/admin/ExportController.php @@ -6,6 +6,7 @@ namespace Shaarli\Front\Controller\Admin; use DateTime; use Shaarli\Bookmark\Bookmark; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -24,7 +25,7 @@ class ExportController extends ShaarliAdminController { $this->assignView('pagetitle', t('Export') .' - '. $this->container->conf->get('general.title', 'Shaarli')); - return $response->write($this->render('export')); + return $response->write($this->render(TemplatePage::EXPORT)); } /** @@ -74,6 +75,6 @@ class ExportController extends ShaarliAdminController $this->assignView('eol', PHP_EOL); $this->assignView('selection', $selection); - return $response->write($this->render('export.bookmarks')); + return $response->write($this->render(TemplatePage::NETSCAPE_EXPORT_BOOKMARKS)); } } diff --git a/application/front/controller/admin/ImportController.php b/application/front/controller/admin/ImportController.php index 8c5305b9..758d5ef9 100644 --- a/application/front/controller/admin/ImportController.php +++ b/application/front/controller/admin/ImportController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Admin; use Psr\Http\Message\UploadedFileInterface; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -39,7 +40,7 @@ class ImportController extends ShaarliAdminController ); $this->assignView('pagetitle', t('Import') .' - '. $this->container->conf->get('general.title', 'Shaarli')); - return $response->write($this->render('import')); + return $response->write($this->render(TemplatePage::IMPORT)); } /** diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php index bdfc5ca7..3aa48423 100644 --- a/application/front/controller/admin/ManageShaareController.php +++ b/application/front/controller/admin/ManageShaareController.php @@ -7,6 +7,7 @@ namespace Shaarli\Front\Controller\Admin; use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Formatter\BookmarkMarkdownFormatter; +use Shaarli\Render\TemplatePage; use Shaarli\Thumbnailer; use Slim\Http\Request; use Slim\Http\Response; @@ -28,7 +29,7 @@ class ManageShaareController extends ShaarliAdminController t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli') ); - return $response->write($this->render('addlink')); + return $response->write($this->render(TemplatePage::ADDLINK)); } /** @@ -365,7 +366,7 @@ class ManageShaareController extends ShaarliAdminController $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli') ); - return $response->write($this->render('editlink')); + return $response->write($this->render(TemplatePage::EDIT_LINK)); } /** diff --git a/application/front/controller/admin/ManageTagController.php b/application/front/controller/admin/ManageTagController.php index 7dab288a..0380ef1f 100644 --- a/application/front/controller/admin/ManageTagController.php +++ b/application/front/controller/admin/ManageTagController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Admin; use Shaarli\Bookmark\BookmarkFilter; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -28,7 +29,7 @@ class ManageTagController extends ShaarliAdminController t('Manage tags') .' - '. $this->container->conf->get('general.title', 'Shaarli') ); - return $response->write($this->render('changetag')); + return $response->write($this->render(TemplatePage::CHANGE_TAG)); } /** diff --git a/application/front/controller/admin/PasswordController.php b/application/front/controller/admin/PasswordController.php index bcce01a6..5ec0d24b 100644 --- a/application/front/controller/admin/PasswordController.php +++ b/application/front/controller/admin/PasswordController.php @@ -7,6 +7,7 @@ namespace Shaarli\Front\Controller\Admin; use Shaarli\Container\ShaarliContainer; use Shaarli\Front\Exception\OpenShaarliPasswordException; use Shaarli\Front\Exception\ShaarliFrontException; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; use Throwable; @@ -33,7 +34,7 @@ class PasswordController extends ShaarliAdminController */ public function index(Request $request, Response $response): Response { - return $response->write($this->render('changepassword')); + return $response->write($this->render(TemplatePage::CHANGE_PASSWORD)); } /** @@ -55,7 +56,7 @@ class PasswordController extends ShaarliAdminController return $response ->withStatus(400) - ->write($this->render('changepassword')) + ->write($this->render(TemplatePage::CHANGE_PASSWORD)) ; } @@ -71,7 +72,7 @@ class PasswordController extends ShaarliAdminController return $response ->withStatus(400) - ->write($this->render('changepassword')) + ->write($this->render(TemplatePage::CHANGE_PASSWORD)) ; } @@ -95,6 +96,6 @@ class PasswordController extends ShaarliAdminController $this->saveSuccessMessage(t('Your password has been changed')); - return $response->write($this->render('changepassword')); + return $response->write($this->render(TemplatePage::CHANGE_PASSWORD)); } } diff --git a/application/front/controller/admin/PluginsController.php b/application/front/controller/admin/PluginsController.php index d5ec91f0..44025395 100644 --- a/application/front/controller/admin/PluginsController.php +++ b/application/front/controller/admin/PluginsController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Admin; use Exception; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -44,7 +45,7 @@ class PluginsController extends ShaarliAdminController t('Plugin Administration') .' - '. $this->container->conf->get('general.title', 'Shaarli') ); - return $response->write($this->render('pluginsadmin')); + return $response->write($this->render(TemplatePage::PLUGINS_ADMIN)); } /** diff --git a/application/front/controller/admin/ThumbnailsController.php b/application/front/controller/admin/ThumbnailsController.php index e5308510..81c87ed0 100644 --- a/application/front/controller/admin/ThumbnailsController.php +++ b/application/front/controller/admin/ThumbnailsController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Admin; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -36,7 +37,7 @@ class ThumbnailsController extends ShaarliAdminController t('Thumbnails update') .' - '. $this->container->conf->get('general.title', 'Shaarli') ); - return $response->write($this->render('thumbnails')); + return $response->write($this->render(TemplatePage::THUMBNAILS)); } /** @@ -61,19 +62,4 @@ class ThumbnailsController extends ShaarliAdminController return $response->withJson($this->container->formatterFactory->getFormatter('raw')->format($bookmark)); } - - /** - * @param mixed[] $data Variables passed to the template engine - * - * @return mixed[] Template data after active plugins render_picwall hook execution. - */ - protected function executeHooks(array $data): array - { - $this->container->pluginManager->executeHooks( - 'render_tools', - $data - ); - - return $data; - } } diff --git a/application/front/controller/admin/ToolsController.php b/application/front/controller/admin/ToolsController.php index d087f2cd..a476e898 100644 --- a/application/front/controller/admin/ToolsController.php +++ b/application/front/controller/admin/ToolsController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Admin; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -29,7 +30,7 @@ class ToolsController extends ShaarliAdminController $this->assignView('pagetitle', t('Tools') .' - '. $this->container->conf->get('general.title', 'Shaarli')); - return $response->write($this->render('tools')); + return $response->write($this->render(TemplatePage::TOOLS)); } /** diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php new file mode 100644 index 00000000..a37a7f6b --- /dev/null +++ b/application/front/controller/visitor/BookmarkListController.php @@ -0,0 +1,248 @@ +processLegacyController($request, $response); + if (null !== $legacyResponse) { + return $legacyResponse; + } + + $formatter = $this->container->formatterFactory->getFormatter(); + + $searchTags = escape(normalize_spaces($request->getParam('searchtags') ?? '')); + $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));; + + // Filter bookmarks according search parameters. + $visibility = $this->container->sessionManager->getSessionParameter('visibility'); + $search = [ + 'searchtags' => $searchTags, + 'searchterm' => $searchTerm, + ]; + $linksToDisplay = $this->container->bookmarkService->search( + $search, + $visibility, + false, + !!$this->container->sessionManager->getSessionParameter('untaggedonly') + ) ?? []; + + // ---- Handle paging. + $keys = []; + foreach ($linksToDisplay as $key => $value) { + $keys[] = $key; + } + + $linksPerPage = $this->container->sessionManager->getSessionParameter('LINKS_PER_PAGE', 20) ?: 20; + + // Select articles according to paging. + $pageCount = (int) ceil(count($keys) / $linksPerPage) ?: 1; + $page = (int) $request->getParam('page') ?? 1; + $page = $page < 1 ? 1 : $page; + $page = $page > $pageCount ? $pageCount : $page; + + // Start index. + $i = ($page - 1) * $linksPerPage; + $end = $i + $linksPerPage; + + $linkDisp = []; + $save = false; + while ($i < $end && $i < count($keys)) { + $save = $this->updateThumbnail($linksToDisplay[$keys[$i]], false) || $save; + $link = $formatter->format($linksToDisplay[$keys[$i]]); + + $linkDisp[$keys[$i]] = $link; + $i++; + } + + if ($save) { + $this->container->bookmarkService->save(); + } + + // Compute paging navigation + $searchtagsUrl = $searchTags === '' ? '' : '&searchtags=' . urlencode($searchTags); + $searchtermUrl = $searchTerm === '' ? '' : '&searchterm=' . urlencode($searchTerm); + + $previous_page_url = ''; + if ($i !== count($keys)) { + $previous_page_url = '?page=' . ($page + 1) . $searchtermUrl . $searchtagsUrl; + } + $next_page_url = ''; + if ($page > 1) { + $next_page_url = '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl; + } + + // Fill all template fields. + $data = array_merge( + $this->initializeTemplateVars(), + [ + 'previous_page_url' => $previous_page_url, + 'next_page_url' => $next_page_url, + 'page_current' => $page, + 'page_max' => $pageCount, + 'result_count' => count($linksToDisplay), + 'search_term' => $searchTerm, + 'search_tags' => $searchTags, + 'visibility' => $visibility, + 'links' => $linkDisp, + ] + ); + + if (!empty($searchTerm) || !empty($searchTags)) { + $data['pagetitle'] = t('Search: '); + $data['pagetitle'] .= ! empty($searchTerm) ? $searchTerm . ' ' : ''; + $bracketWrap = function ($tag) { + return '[' . $tag . ']'; + }; + $data['pagetitle'] .= ! empty($searchTags) + ? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchTags))) . ' ' + : ''; + $data['pagetitle'] .= '- '; + } + + $data['pagetitle'] = ($data['pagetitle'] ?? '') . $this->container->conf->get('general.title', 'Shaarli'); + + $this->executeHooks($data); + $this->assignAllView($data); + + return $response->write($this->render(TemplatePage::LINKLIST)); + } + + /** + * GET /shaare/{hash} - Display a single shaare + */ + public function permalink(Request $request, Response $response, array $args): Response + { + try { + $bookmark = $this->container->bookmarkService->findByHash($args['hash']); + } catch (BookmarkNotFoundException $e) { + $this->assignView('error_message', $e->getMessage()); + + return $response->write($this->render(TemplatePage::ERROR_404)); + } + + $this->updateThumbnail($bookmark); + + $data = array_merge( + $this->initializeTemplateVars(), + [ + 'pagetitle' => $bookmark->getTitle() .' - '. $this->container->conf->get('general.title', 'Shaarli'), + 'links' => [$this->container->formatterFactory->getFormatter()->format($bookmark)], + ] + ); + + $this->executeHooks($data); + $this->assignAllView($data); + + return $response->write($this->render(TemplatePage::LINKLIST)); + } + + /** + * Update the thumbnail of a single bookmark if necessary. + */ + protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool + { + // Logged in, thumbnails enabled, not a note, is HTTP + // and (never retrieved yet or no valid cache file) + if ($this->container->loginManager->isLoggedIn() + && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE + && false !== $bookmark->getThumbnail() + && !$bookmark->isNote() + && (null === $bookmark->getThumbnail() || !is_file($bookmark->getThumbnail())) + && startsWith(strtolower($bookmark->getUrl()), 'http') + ) { + $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); + $this->container->bookmarkService->set($bookmark, $writeDatastore); + + return true; + } + + return false; + } + + /** + * @param mixed[] $data Template vars to process in plugins, passed as reference. + */ + protected function executeHooks(array &$data): void + { + $this->container->pluginManager->executeHooks( + 'render_linklist', + $data, + ['loggedin' => $this->container->loginManager->isLoggedIn()] + ); + } + + /** + * @return string[] Default template variables without values. + */ + protected function initializeTemplateVars(): array + { + return [ + 'previous_page_url' => '', + 'next_page_url' => '', + 'page_max' => '', + 'search_tags' => '', + 'result_count' => '', + ]; + } + + /** + * Process legacy routes if necessary. They used query parameters. + * If no legacy routes is passed, return null. + */ + protected function processLegacyController(Request $request, Response $response): ?Response + { + // Legacy smallhash filter + $queryString = $this->container->environment['QUERY_STRING'] ?? null; + if (null !== $queryString && 1 === preg_match('/^([a-zA-Z0-9-_@]{6})($|&|#)/', $queryString, $match)) { + return $this->redirect($response, '/shaare/' . $match[1]); + } + + // Legacy controllers (mostly used for redirections) + if (null !== $request->getQueryParam('do')) { + $legacyController = new LegacyController($this->container); + + try { + return $legacyController->process($request, $response, $request->getQueryParam('do')); + } catch (UnknowLegacyRouteException $e) { + // We ignore legacy 404 + return null; + } + } + + // Legacy GET admin routes + $legacyGetRoutes = array_intersect( + LegacyController::LEGACY_GET_ROUTES, + array_keys($request->getQueryParams() ?? []) + ); + if (1 === count($legacyGetRoutes)) { + $legacyController = new LegacyController($this->container); + + return $legacyController->process($request, $response, $legacyGetRoutes[0]); + } + + return null; + } +} diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index e5c9ddac..05b4f095 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php @@ -7,6 +7,7 @@ namespace Shaarli\Front\Controller\Visitor; use DateTime; use DateTimeImmutable; use Shaarli\Bookmark\Bookmark; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -85,7 +86,7 @@ class DailyController extends ShaarliVisitorController t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle ); - return $response->write($this->render('daily')); + return $response->write($this->render(TemplatePage::DAILY)); } /** @@ -152,7 +153,7 @@ class DailyController extends ShaarliVisitorController $this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false)); $this->assignView('days', $dataPerDay); - $rssContent = $this->render('dailyrss'); + $rssContent = $this->render(TemplatePage::DAILY_RSS); $cache->cache($rssContent); diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php index 0db1f463..a257766f 100644 --- a/application/front/controller/visitor/LoginController.php +++ b/application/front/controller/visitor/LoginController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Visitor; use Shaarli\Front\Exception\LoginBannedException; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -41,6 +42,6 @@ class LoginController extends ShaarliVisitorController ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli')) ; - return $response->write($this->render('loginform')); + return $response->write($this->render(TemplatePage::LOGIN)); } } diff --git a/application/front/controller/visitor/OpenSearchController.php b/application/front/controller/visitor/OpenSearchController.php index 0fd68db6..36d60acf 100644 --- a/application/front/controller/visitor/OpenSearchController.php +++ b/application/front/controller/visitor/OpenSearchController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Visitor; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -21,6 +22,6 @@ class OpenSearchController extends ShaarliVisitorController $this->assignView('serverurl', index_url($this->container->environment)); - return $response->write($this->render('opensearch')); + return $response->write($this->render(TemplatePage::OPEN_SEARCH)); } } diff --git a/application/front/controller/visitor/PictureWallController.php b/application/front/controller/visitor/PictureWallController.php index 4e1dce8c..5ef2cb17 100644 --- a/application/front/controller/visitor/PictureWallController.php +++ b/application/front/controller/visitor/PictureWallController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Visitor; use Shaarli\Front\Exception\ThumbnailsDisabledException; +use Shaarli\Render\TemplatePage; use Shaarli\Thumbnailer; use Slim\Http\Request; use Slim\Http\Response; @@ -46,7 +47,7 @@ class PictureWallController extends ShaarliVisitorController $this->assignView($key, $value); } - return $response->write($this->render('picwall')); + return $response->write($this->render(TemplatePage::PICTURE_WALL)); } /** diff --git a/application/legacy/LegacyController.php b/application/legacy/LegacyController.php new file mode 100644 index 00000000..a97b07b1 --- /dev/null +++ b/application/legacy/LegacyController.php @@ -0,0 +1,130 @@ +{$action}($request, $response); + } + + /** Legacy route: ?post= */ + public function post(Request $request, Response $response): Response + { + $parameters = count($request->getQueryParams()) > 0 ? '?' . http_build_query($request->getQueryParams()) : ''; + + if (!$this->container->loginManager->isLoggedIn()) { + return $this->redirect($response, '/login' . $parameters); + } + + return $this->redirect($response, '/admin/shaare' . $parameters); + } + + /** Legacy route: ?addlink= */ + protected function addlink(Request $request, Response $response): Response + { + if (!$this->container->loginManager->isLoggedIn()) { + return $this->redirect($response, '/login'); + } + + return $this->redirect($response, '/admin/add-shaare'); + } + + /** Legacy route: ?do=login */ + protected function login(Request $request, Response $response): Response + { + return $this->redirect($response, '/login'); + } + + /** Legacy route: ?do=logout */ + protected function logout(Request $request, Response $response): Response + { + return $this->redirect($response, '/logout'); + } + + /** Legacy route: ?do=picwall */ + protected function picwall(Request $request, Response $response): Response + { + return $this->redirect($response, '/picture-wall'); + } + + /** Legacy route: ?do=tagcloud */ + protected function tagcloud(Request $request, Response $response): Response + { + return $this->redirect($response, '/tags/cloud'); + } + + /** Legacy route: ?do=taglist */ + protected function taglist(Request $request, Response $response): Response + { + return $this->redirect($response, '/tags/list'); + } + + /** Legacy route: ?do=daily */ + protected function daily(Request $request, Response $response): Response + { + $dayParam = !empty($request->getParam('day')) ? '?day=' . escape($request->getParam('day')) : ''; + + return $this->redirect($response, '/daily' . $dayParam); + } + + /** Legacy route: ?do=rss */ + protected function rss(Request $request, Response $response): Response + { + return $this->feed($request, $response, FeedBuilder::$FEED_RSS); + } + + /** Legacy route: ?do=atom */ + protected function atom(Request $request, Response $response): Response + { + return $this->feed($request, $response, FeedBuilder::$FEED_ATOM); + } + + /** Legacy route: ?do=opensearch */ + protected function opensearch(Request $request, Response $response): Response + { + return $this->redirect($response, '/open-search'); + } + + /** Legacy route: ?do=dailyrss */ + protected function dailyrss(Request $request, Response $response): Response + { + return $this->redirect($response, '/daily-rss'); + } + + /** Legacy route: ?do=feed */ + protected function feed(Request $request, Response $response, string $feedType): Response + { + $parameters = count($request->getQueryParams()) > 0 ? '?' . http_build_query($request->getQueryParams()) : ''; + + return $this->redirect($response, '/feed/' . $feedType . $parameters); + } +} diff --git a/application/legacy/LegacyRouter.php b/application/legacy/LegacyRouter.php new file mode 100644 index 00000000..cea99154 --- /dev/null +++ b/application/legacy/LegacyRouter.php @@ -0,0 +1,187 @@ +isLoggedIn = $isLoggedIn; } + /** + * Reset current state of template rendering. + * Mostly useful for error handling. We remove everything, and display the error template. + */ + public function reset(): void + { + $this->tpl = false; + } + /** * Initialize all default tpl tags. */ diff --git a/application/render/TemplatePage.php b/application/render/TemplatePage.php new file mode 100644 index 00000000..8af8228a --- /dev/null +++ b/application/render/TemplatePage.php @@ -0,0 +1,33 @@ +doneUpdates = $doneUpdates; - $this->linkServices = $linkDB; + $this->bookmarkService = $linkDB; $this->conf = $conf; $this->isLoggedIn = $isLoggedIn; @@ -68,7 +68,7 @@ class Updater */ public function update() { - $updatesRan = array(); + $updatesRan = []; // If the user isn't logged in, exit without updating. if ($this->isLoggedIn !== true) { @@ -112,6 +112,16 @@ class Updater return $this->doneUpdates; } + public function readUpdates(string $updatesFilepath): array + { + return UpdaterUtils::read_updates_file($updatesFilepath); + } + + public function writeUpdates(string $updatesFilepath, array $updates): void + { + UpdaterUtils::write_updates_file($updatesFilepath, $updates); + } + /** * With the Slim routing system, default header link should be `./` instead of `?`. * Otherwise you can not go back to the home page. Example: `/picture-wall` -> `/picture-wall?` instead of `/`. @@ -127,4 +137,31 @@ class Updater return true; } + + /** + * With the Slim routing system, note bookmarks URL formatted `?abcdef` + * should be replaced with `/shaare/abcdef` + */ + public function updateMethodMigrateExistingNotesUrl(): bool + { + $updated = false; + + foreach ($this->bookmarkService->search() as $bookmark) { + if ($bookmark->isNote() + && startsWith($bookmark->getUrl(), '?') + && 1 === preg_match('/^\?([a-zA-Z0-9-_@]{6})($|&|#)/', $bookmark->getUrl(), $match) + ) { + $updated = true; + $bookmark = $bookmark->setUrl('/shaare/' . $match[1]); + + $this->bookmarkService->set($bookmark, false); + } + } + + if ($updated) { + $this->bookmarkService->save(); + } + + return true; + } } -- cgit v1.2.3 From c4ad3d4f061d05a01db25aa54dda830ba776792d Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 7 Jul 2020 10:15:56 +0200 Subject: Process Shaarli install through Slim controller --- application/bookmark/BookmarkFileService.php | 26 +++- application/bookmark/BookmarkInitializer.php | 12 +- application/bookmark/BookmarkServiceInterface.php | 13 ++ application/container/ContainerBuilder.php | 7 + application/container/ShaarliContainer.php | 3 + application/front/ShaarliMiddleware.php | 6 + .../front/controller/admin/LogoutController.php | 10 +- .../front/controller/visitor/InstallController.php | 173 +++++++++++++++++++++ .../front/exceptions/AlreadyInstalledException.php | 15 ++ .../exceptions/ResourcePermissionException.php | 13 ++ application/security/CookieManager.php | 33 ++++ application/security/LoginManager.php | 16 +- application/security/SessionManager.php | 16 +- application/updater/Updater.php | 30 ++-- 14 files changed, 340 insertions(+), 33 deletions(-) create mode 100644 application/front/controller/visitor/InstallController.php create mode 100644 application/front/exceptions/AlreadyInstalledException.php create mode 100644 application/front/exceptions/ResourcePermissionException.php create mode 100644 application/security/CookieManager.php (limited to 'application') diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 3d15d4c9..6e04f3b7 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -46,6 +46,9 @@ class BookmarkFileService implements BookmarkServiceInterface /** @var bool true for logged in users. Default value to retrieve private bookmarks. */ protected $isLoggedIn; + /** @var bool Allow datastore alteration from not logged in users. */ + protected $anonymousPermission = false; + /** * @inheritDoc */ @@ -64,7 +67,7 @@ class BookmarkFileService implements BookmarkServiceInterface $this->bookmarks = $this->bookmarksIO->read(); } catch (EmptyDataStoreException $e) { $this->bookmarks = new BookmarkArray(); - if ($isLoggedIn) { + if ($this->isLoggedIn) { $this->save(); } } @@ -154,7 +157,7 @@ class BookmarkFileService implements BookmarkServiceInterface */ public function set($bookmark, $save = true) { - if ($this->isLoggedIn !== true) { + if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { throw new Exception(t('You\'re not authorized to alter the datastore')); } if (! $bookmark instanceof Bookmark) { @@ -179,7 +182,7 @@ class BookmarkFileService implements BookmarkServiceInterface */ public function add($bookmark, $save = true) { - if ($this->isLoggedIn !== true) { + if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { throw new Exception(t('You\'re not authorized to alter the datastore')); } if (! $bookmark instanceof Bookmark) { @@ -204,7 +207,7 @@ class BookmarkFileService implements BookmarkServiceInterface */ public function addOrSet($bookmark, $save = true) { - if ($this->isLoggedIn !== true) { + if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { throw new Exception(t('You\'re not authorized to alter the datastore')); } if (! $bookmark instanceof Bookmark) { @@ -221,7 +224,7 @@ class BookmarkFileService implements BookmarkServiceInterface */ public function remove($bookmark, $save = true) { - if ($this->isLoggedIn !== true) { + if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { throw new Exception(t('You\'re not authorized to alter the datastore')); } if (! $bookmark instanceof Bookmark) { @@ -274,10 +277,11 @@ class BookmarkFileService implements BookmarkServiceInterface */ public function save() { - if (!$this->isLoggedIn) { + if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { // TODO: raise an Exception instead die('You are not authorized to change the database.'); } + $this->bookmarks->reorder(); $this->bookmarksIO->write($this->bookmarks); $this->pageCacheManager->invalidateCaches(); @@ -357,6 +361,16 @@ class BookmarkFileService implements BookmarkServiceInterface $initializer->initialize(); } + public function enableAnonymousPermission(): void + { + $this->anonymousPermission = true; + } + + public function disableAnonymousPermission(): void + { + $this->anonymousPermission = false; + } + /** * Handles migration to the new database format (BookmarksArray). */ diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php index 9eee9a35..479ee9a9 100644 --- a/application/bookmark/BookmarkInitializer.php +++ b/application/bookmark/BookmarkInitializer.php @@ -34,13 +34,15 @@ class BookmarkInitializer */ public function initialize() { + $this->bookmarkService->enableAnonymousPermission(); + $bookmark = new Bookmark(); $bookmark->setTitle(t('My secret stuff... - Pastebin.com')); - $bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', []); + $bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8='); $bookmark->setDescription(t('Shhhh! I\'m a private link only YOU can see. You can delete me too.')); $bookmark->setTagsString('secretstuff'); $bookmark->setPrivate(true); - $this->bookmarkService->add($bookmark); + $this->bookmarkService->add($bookmark, false); $bookmark = new Bookmark(); $bookmark->setTitle(t('The personal, minimalist, super-fast, database free, bookmarking service')); @@ -54,6 +56,10 @@ To learn how to use Shaarli, consult the link "Documentation" at the bottom of t You use the community supported version of the original Shaarli project, by Sebastien Sauvage.' )); $bookmark->setTagsString('opensource software'); - $this->bookmarkService->add($bookmark); + $this->bookmarkService->add($bookmark, false); + + $this->bookmarkService->save(); + + $this->bookmarkService->disableAnonymousPermission(); } } diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php index 7b7a4f09..37fbda89 100644 --- a/application/bookmark/BookmarkServiceInterface.php +++ b/application/bookmark/BookmarkServiceInterface.php @@ -177,4 +177,17 @@ interface BookmarkServiceInterface * Creates the default database after a fresh install. */ public function initialize(); + + /** + * Allow to write the datastore from anonymous session (not logged in). + * + * This covers a few specific use cases, such as datastore initialization, + * but it should be used carefully as it can lead to security issues. + */ + public function enableAnonymousPermission(); + + /** + * Disable anonymous permission. + */ + public function disableAnonymousPermission(); } diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index ccb87c3a..593aafb7 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -15,6 +15,7 @@ use Shaarli\Netscape\NetscapeBookmarkUtils; use Shaarli\Plugin\PluginManager; use Shaarli\Render\PageBuilder; use Shaarli\Render\PageCacheManager; +use Shaarli\Security\CookieManager; use Shaarli\Security\LoginManager; use Shaarli\Security\SessionManager; use Shaarli\Thumbnailer; @@ -38,6 +39,9 @@ class ContainerBuilder /** @var SessionManager */ protected $session; + /** @var CookieManager */ + protected $cookieManager; + /** @var LoginManager */ protected $login; @@ -47,11 +51,13 @@ class ContainerBuilder public function __construct( ConfigManager $conf, SessionManager $session, + CookieManager $cookieManager, LoginManager $login ) { $this->conf = $conf; $this->session = $session; $this->login = $login; + $this->cookieManager = $cookieManager; } public function build(): ShaarliContainer @@ -60,6 +66,7 @@ class ContainerBuilder $container['conf'] = $this->conf; $container['sessionManager'] = $this->session; + $container['cookieManager'] = $this->cookieManager; $container['loginManager'] = $this->login; $container['basePath'] = $this->basePath; diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php index 09e7d5b1..c4fe753e 100644 --- a/application/container/ShaarliContainer.php +++ b/application/container/ShaarliContainer.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Shaarli\Container; +use http\Cookie; use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Config\ConfigManager; use Shaarli\Feed\FeedBuilder; @@ -14,6 +15,7 @@ use Shaarli\Netscape\NetscapeBookmarkUtils; use Shaarli\Plugin\PluginManager; use Shaarli\Render\PageBuilder; use Shaarli\Render\PageCacheManager; +use Shaarli\Security\CookieManager; use Shaarli\Security\LoginManager; use Shaarli\Security\SessionManager; use Shaarli\Thumbnailer; @@ -25,6 +27,7 @@ use Slim\Container; * * @property string $basePath Shaarli's instance base path (e.g. `/shaarli/`) * @property BookmarkServiceInterface $bookmarkService + * @property CookieManager $cookieManager * @property ConfigManager $conf * @property mixed[] $environment $_SERVER automatically injected by Slim * @property callable $errorHandler Overrides default Slim error display diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php index baea6ef2..595182ac 100644 --- a/application/front/ShaarliMiddleware.php +++ b/application/front/ShaarliMiddleware.php @@ -43,6 +43,12 @@ class ShaarliMiddleware $this->container->basePath = rtrim($request->getUri()->getBasePath(), '/'); try { + if (!is_file($this->container->conf->getConfigFileExt()) + && !in_array($next->getName(), ['displayInstall', 'saveInstall'], true) + ) { + return $response->withRedirect($this->container->basePath . '/install'); + } + $this->runUpdates(); $this->checkOpenShaarli($request, $response, $next); diff --git a/application/front/controller/admin/LogoutController.php b/application/front/controller/admin/LogoutController.php index c5984814..28165129 100644 --- a/application/front/controller/admin/LogoutController.php +++ b/application/front/controller/admin/LogoutController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Admin; +use Shaarli\Security\CookieManager; use Shaarli\Security\LoginManager; use Slim\Http\Request; use Slim\Http\Response; @@ -20,9 +21,12 @@ class LogoutController extends ShaarliAdminController { $this->container->pageCacheManager->invalidateCaches(); $this->container->sessionManager->logout(); - - // TODO: switch to a simple Cookie manager allowing to check the session, and create mocks. - setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, $this->container->basePath . '/'); + $this->container->cookieManager->setCookieParameter( + CookieManager::STAY_SIGNED_IN, + 'false', + 0, + $this->container->basePath . '/' + ); return $this->redirect($response, '/'); } diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php new file mode 100644 index 00000000..aa032860 --- /dev/null +++ b/application/front/controller/visitor/InstallController.php @@ -0,0 +1,173 @@ +container->conf->getConfigFileExt())) { + throw new AlreadyInstalledException(); + } + } + + /** + * Display the install template page. + * Also test file permissions and sessions beforehand. + */ + public function index(Request $request, Response $response): Response + { + // Before installation, we'll make sure that permissions are set properly, and sessions are working. + $this->checkPermissions(); + + if (static::SESSION_TEST_VALUE + !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) + ) { + $this->container->sessionManager->setSessionParameter(static::SESSION_TEST_KEY, static::SESSION_TEST_VALUE); + + return $this->redirect($response, '/install/session-test'); + } + + [$continents, $cities] = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get()); + + $this->assignView('continents', $continents); + $this->assignView('cities', $cities); + $this->assignView('languages', Languages::getAvailableLanguages()); + + return $response->write($this->render('install')); + } + + /** + * Route checking that the session parameter has been properly saved between two distinct requests. + * If the session parameter is preserved, redirect to install template page, otherwise displays error. + */ + public function sessionTest(Request $request, Response $response): Response + { + // This part makes sure sessions works correctly. + // (Because on some hosts, session.save_path may not be set correctly, + // or we may not have write access to it.) + if (static::SESSION_TEST_VALUE + !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) + ) { + // Step 2: Check if data in session is correct. + $msg = t( + '
Sessions do not seem to work correctly on your server.
'. + 'Make sure the variable "session.save_path" is set correctly in your PHP config, '. + 'and that you have write access to it.
'. + 'It currently points to %s.
'. + 'On some browsers, accessing your server via a hostname like \'localhost\' '. + 'or any custom hostname without a dot causes cookie storage to fail. '. + 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.
' + ); + $msg = sprintf($msg, $this->container->sessionManager->getSavePath()); + + $this->assignView('message', $msg); + + return $response->write($this->render('error')); + } + + return $this->redirect($response, '/install'); + } + + /** + * Save installation form and initialize config file and datastore if necessary. + */ + public function save(Request $request, Response $response): Response + { + $timezone = 'UTC'; + if (!empty($request->getParam('continent')) + && !empty($request->getParam('city')) + && isTimeZoneValid($request->getParam('continent'), $request->getParam('city')) + ) { + $timezone = $request->getParam('continent') . '/' . $request->getParam('city'); + } + $this->container->conf->set('general.timezone', $timezone); + + $login = $request->getParam('setlogin'); + $this->container->conf->set('credentials.login', $login); + $salt = sha1(uniqid('', true) .'_'. mt_rand()); + $this->container->conf->set('credentials.salt', $salt); + $this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt)); + + if (!empty($request->getParam('title'))) { + $this->container->conf->set('general.title', escape($request->getParam('title'))); + } else { + $this->container->conf->set( + 'general.title', + 'Shared bookmarks on '.escape(index_url($this->container->environment)) + ); + } + + $this->container->conf->set('translation.language', escape($request->getParam('language'))); + $this->container->conf->set('updates.check_updates', !empty($request->getParam('updateCheck'))); + $this->container->conf->set('api.enabled', !empty($request->getParam('enableApi'))); + $this->container->conf->set( + 'api.secret', + generate_api_secret( + $this->container->conf->get('credentials.login'), + $this->container->conf->get('credentials.salt') + ) + ); + + try { + // Everything is ok, let's create config file. + $this->container->conf->write($this->container->loginManager->isLoggedIn()); + } catch (\Exception $e) { + $this->assignView('message', $e->getMessage()); + $this->assignView('stacktrace', $e->getTraceAsString()); + + return $response->write($this->render('error')); + } + + if ($this->container->bookmarkService->count(BookmarkFilter::$ALL) === 0) { + $this->container->bookmarkService->initialize(); + } + + $this->container->sessionManager->setSessionParameter( + SessionManager::KEY_SUCCESS_MESSAGES, + [t('Shaarli is now configured. Please login and start shaaring your bookmarks!')] + ); + + return $this->redirect($response, '/'); + } + + protected function checkPermissions(): bool + { + // Ensure Shaarli has proper access to its resources + $errors = ApplicationUtils::checkResourcePermissions($this->container->conf); + + if (empty($errors)) { + return true; + } + + // FIXME! Do not insert HTML here. + $message = '

'. t('Insufficient permissions:') .'

    '; + + foreach ($errors as $error) { + $message .= '
  • '.$error.'
  • '; + } + $message .= '
'; + + throw new ResourcePermissionException($message); + } +} diff --git a/application/front/exceptions/AlreadyInstalledException.php b/application/front/exceptions/AlreadyInstalledException.php new file mode 100644 index 00000000..4add86cf --- /dev/null +++ b/application/front/exceptions/AlreadyInstalledException.php @@ -0,0 +1,15 @@ +cookies = $cookies; + } + + public function setCookieParameter(string $key, string $value, int $expires, string $path): self + { + $this->cookies[$key] = $value; + + setcookie($key, $value, $expires, $path); + + return $this; + } + + public function getCookieParameter(string $key, string $default = null): ?string + { + return $this->cookies[$key] ?? $default; + } +} diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php index 39ec9b2e..d74c3118 100644 --- a/application/security/LoginManager.php +++ b/application/security/LoginManager.php @@ -9,9 +9,6 @@ use Shaarli\Config\ConfigManager; */ class LoginManager { - /** @var string Name of the cookie set after logging in **/ - public static $STAY_SIGNED_IN_COOKIE = 'shaarli_staySignedIn'; - /** @var array A reference to the $_GLOBALS array */ protected $globals = []; @@ -32,17 +29,21 @@ class LoginManager /** @var string User sign-in token depending on remote IP and credentials */ protected $staySignedInToken = ''; + /** @var CookieManager */ + protected $cookieManager; /** * Constructor * * @param ConfigManager $configManager Configuration Manager instance * @param SessionManager $sessionManager SessionManager instance + * @param CookieManager $cookieManager CookieManager instance */ - public function __construct($configManager, $sessionManager) + public function __construct($configManager, $sessionManager, $cookieManager) { $this->configManager = $configManager; $this->sessionManager = $sessionManager; + $this->cookieManager = $cookieManager; $this->banManager = new BanManager( $this->configManager->get('security.trusted_proxies', []), $this->configManager->get('security.ban_after'), @@ -86,10 +87,9 @@ class LoginManager /** * Check user session state and validity (expiration) * - * @param array $cookie The $_COOKIE array * @param string $clientIpId Client IP address identifier */ - public function checkLoginState($cookie, $clientIpId) + public function checkLoginState($clientIpId) { if (! $this->configManager->exists('credentials.login')) { // Shaarli is not configured yet @@ -97,9 +97,7 @@ class LoginManager return; } - if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE]) - && $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken - ) { + if ($this->staySignedInToken === $this->cookieManager->getCookieParameter(CookieManager::STAY_SIGNED_IN)) { // The user client has a valid stay-signed-in cookie // Session information is updated with the current client information $this->sessionManager->storeLoginInfo($clientIpId); diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php index 0ac17d9a..82771c24 100644 --- a/application/security/SessionManager.php +++ b/application/security/SessionManager.php @@ -31,16 +31,21 @@ class SessionManager /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */ protected $staySignedIn = false; + /** @var string */ + protected $savePath; + /** * Constructor * - * @param array $session The $_SESSION array (reference) - * @param ConfigManager $conf ConfigManager instance + * @param array $session The $_SESSION array (reference) + * @param ConfigManager $conf ConfigManager instance + * @param string $savePath Session save path returned by builtin function session_save_path() */ - public function __construct(& $session, $conf) + public function __construct(&$session, $conf, string $savePath) { $this->session = &$session; $this->conf = $conf; + $this->savePath = $savePath; } /** @@ -249,4 +254,9 @@ class SessionManager return $this; } + + public function getSavePath(): string + { + return $this->savePath; + } } diff --git a/application/updater/Updater.php b/application/updater/Updater.php index f73a7452..4c578528 100644 --- a/application/updater/Updater.php +++ b/application/updater/Updater.php @@ -38,6 +38,11 @@ class Updater */ protected $methods; + /** + * @var string $basePath Shaarli root directory (from HTTP Request) + */ + protected $basePath = null; + /** * Object constructor. * @@ -62,11 +67,13 @@ class Updater * Run all new updates. * Update methods have to start with 'updateMethod' and return true (on success). * + * @param string $basePath Shaarli root directory (from HTTP Request) + * * @return array An array containing ran updates. * * @throws UpdaterException If something went wrong. */ - public function update() + public function update(string $basePath = null) { $updatesRan = []; @@ -123,16 +130,14 @@ class Updater } /** - * With the Slim routing system, default header link should be `./` instead of `?`. - * Otherwise you can not go back to the home page. Example: `/picture-wall` -> `/picture-wall?` instead of `/`. + * With the Slim routing system, default header link should be `/subfolder/` instead of `?`. + * Otherwise you can not go back to the home page. + * Example: `/subfolder/picture-wall` -> `/subfolder/picture-wall?` instead of `/subfolder/`. */ public function updateMethodRelativeHomeLink(): bool { - $link = trim($this->conf->get('general.header_link')); - if ($link[0] === '?') { - $link = './'. ltrim($link, '?'); - - $this->conf->set('general.header_link', $link, true, true); + if ('?' === trim($this->conf->get('general.header_link'))) { + $this->conf->set('general.header_link', $this->basePath . '/', true, true); } return true; @@ -152,7 +157,7 @@ class Updater && 1 === preg_match('/^\?([a-zA-Z0-9-_@]{6})($|&|#)/', $bookmark->getUrl(), $match) ) { $updated = true; - $bookmark = $bookmark->setUrl('/shaare/' . $match[1]); + $bookmark = $bookmark->setUrl($this->basePath . '/shaare/' . $match[1]); $this->bookmarkService->set($bookmark, false); } @@ -164,4 +169,11 @@ class Updater return true; } + + public function setBasePath(string $basePath): self + { + $this->basePath = $basePath; + + return $this; + } } -- cgit v1.2.3 From a8c11451e8d885a243c1ad52012093ba8d121e2c Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 21 Jul 2020 20:33:33 +0200 Subject: Process login through Slim controller --- application/front/ShaarliMiddleware.php | 4 +- .../front/controller/visitor/LoginController.php | 127 +++++++++++++++++++-- .../front/exceptions/CantLoginException.php | 10 ++ application/security/SessionManager.php | 30 +++++ 4 files changed, 160 insertions(+), 11 deletions(-) create mode 100644 application/front/exceptions/CantLoginException.php (limited to 'application') diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php index 595182ac..e9f5552d 100644 --- a/application/front/ShaarliMiddleware.php +++ b/application/front/ShaarliMiddleware.php @@ -62,7 +62,9 @@ class ShaarliMiddleware return $response->write($this->container->pageBuilder->render('error')); } catch (UnauthorizedException $e) { - return $response->withRedirect($this->container->basePath . '/login'); + $returnUrl = urlencode($this->container->environment['REQUEST_URI']); + + return $response->withRedirect($this->container->basePath . '/login?returnurl=' . $returnUrl); } catch (\Throwable $e) { // Unknown error encountered $this->container->pageBuilder->reset(); diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php index a257766f..c40b8cc4 100644 --- a/application/front/controller/visitor/LoginController.php +++ b/application/front/controller/visitor/LoginController.php @@ -4,8 +4,12 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Visitor; +use Shaarli\Front\Exception\CantLoginException; use Shaarli\Front\Exception\LoginBannedException; +use Shaarli\Front\Exception\WrongTokenException; use Shaarli\Render\TemplatePage; +use Shaarli\Security\CookieManager; +use Shaarli\Security\SessionManager; use Slim\Http\Request; use Slim\Http\Response; @@ -19,29 +23,132 @@ use Slim\Http\Response; */ class LoginController extends ShaarliVisitorController { + /** + * GET /login - Display the login page. + */ public function index(Request $request, Response $response): Response { - if ($this->container->loginManager->isLoggedIn() - || $this->container->conf->get('security.open_shaarli', false) - ) { + try { + $this->checkLoginState(); + } catch (CantLoginException $e) { return $this->redirect($response, '/'); } - $userCanLogin = $this->container->loginManager->canLogin($request->getServerParams()); - if ($userCanLogin !== true) { - throw new LoginBannedException(); + if ($request->getParam('login') !== null) { + $this->assignView('username', escape($request->getParam('login'))); } - if ($request->getParam('username') !== null) { - $this->assignView('username', escape($request->getParam('username'))); - } + $returnUrl = $request->getParam('returnurl') ?? $this->container->environment['HTTP_REFERER'] ?? null; $this - ->assignView('returnurl', escape($request->getServerParam('HTTP_REFERER'))) + ->assignView('returnurl', escape($returnUrl)) ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true)) ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli')) ; return $response->write($this->render(TemplatePage::LOGIN)); } + + /** + * POST /login - Process login + */ + public function login(Request $request, Response $response): Response + { + if (!$this->container->sessionManager->checkToken($request->getParam('token'))) { + throw new WrongTokenException(); + } + + try { + $this->checkLoginState(); + } catch (CantLoginException $e) { + return $this->redirect($response, '/'); + } + + if (!$this->container->loginManager->checkCredentials( + $this->container->environment['REMOTE_ADDR'], + client_ip_id($this->container->environment), + $request->getParam('login'), + $request->getParam('password') + ) + ) { + $this->container->loginManager->handleFailedLogin($this->container->environment); + + $this->container->sessionManager->setSessionParameter( + SessionManager::KEY_ERROR_MESSAGES, + [t('Wrong login/password.')] + ); + + // Call controller directly instead of unnecessary redirection + return $this->index($request, $response); + } + + $this->container->loginManager->handleSuccessfulLogin($this->container->environment); + + $cookiePath = $this->container->basePath . '/'; + $expirationTime = $this->saveLongLastingSession($request, $cookiePath); + $this->renewUserSession($cookiePath, $expirationTime); + + // Force referer from given return URL + $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl'); + + return $this->redirectFromReferer($request, $response, ['login']); + } + + /** + * Make sure that the user is allowed to login and/or displaying the login page: + * - not already logged in + * - not open shaarli + * - not banned + */ + protected function checkLoginState(): bool + { + if ($this->container->loginManager->isLoggedIn() + || $this->container->conf->get('security.open_shaarli', false) + ) { + throw new CantLoginException(); + } + + if (true !== $this->container->loginManager->canLogin($this->container->environment)) { + throw new LoginBannedException(); + } + + return true; + } + + /** + * @return int Session duration in seconds + */ + protected function saveLongLastingSession(Request $request, string $cookiePath): int + { + if (empty($request->getParam('longlastingsession'))) { + // Standard session expiration (=when browser closes) + $expirationTime = 0; + } else { + // Keep the session cookie even after the browser closes + $this->container->sessionManager->setStaySignedIn(true); + $expirationTime = $this->container->sessionManager->extendSession(); + } + + $this->container->cookieManager->setCookieParameter( + CookieManager::STAY_SIGNED_IN, + $this->container->loginManager->getStaySignedInToken(), + $expirationTime, + $cookiePath + ); + + return $expirationTime; + } + + protected function renewUserSession(string $cookiePath, int $expirationTime): void + { + // Send cookie with the new expiration date to the browser + $this->container->sessionManager->destroy(); + $this->container->sessionManager->cookieParameters( + $expirationTime, + $cookiePath, + $this->container->environment['SERVER_NAME'] + ); + $this->container->sessionManager->start(); + $this->container->sessionManager->regenerateId(true); + } } diff --git a/application/front/exceptions/CantLoginException.php b/application/front/exceptions/CantLoginException.php new file mode 100644 index 00000000..cd16635d --- /dev/null +++ b/application/front/exceptions/CantLoginException.php @@ -0,0 +1,10 @@ +savePath; } + + /* + * Next public functions wrapping native PHP session API. + */ + + public function destroy(): bool + { + $this->session = []; + + return session_destroy(); + } + + public function start(): bool + { + if (session_status() === PHP_SESSION_ACTIVE) { + $this->destroy(); + } + + return session_start(); + } + + public function cookieParameters(int $lifeTime, string $path, string $domain): bool + { + return session_set_cookie_params($lifeTime, $path, $domain); + } + + public function regenerateId(bool $deleteOldSession = false): bool + { + return session_regenerate_id($deleteOldSession); + } } -- cgit v1.2.3 From fabff3835da26e6c95cea56b2a01a03749dec7c8 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 22 Jul 2020 18:12:10 +0200 Subject: Move PHP and config init to dedicated file in order to keep index.php as minimal as possible --- application/render/PageBuilder.php | 20 -------------------- application/security/SessionManager.php | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 20 deletions(-) (limited to 'application') diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php index 85e1d59d..471724c0 100644 --- a/application/render/PageBuilder.php +++ b/application/render/PageBuilder.php @@ -158,10 +158,6 @@ class PageBuilder */ protected function finalize(): void { - //FIXME - DEV _ REMOVE ME - $this->assign('base_path', '/Shaarli'); - $this->assign('asset_path', '/Shaarli/tpl/default'); - // TODO: use the SessionManager $messageKeys = [ SessionManager::KEY_SUCCESS_MESSAGES, @@ -248,20 +244,4 @@ class PageBuilder return $this->tpl->draw($page, true); } - - /** - * Render a 404 page (uses the template : tpl/404.tpl) - * usage: $PAGE->render404('The link was deleted') - * - * @param string $message A message to display what is not found - */ - public function render404($message = '') - { - if (empty($message)) { - $message = t('The page you are trying to reach does not exist or has been deleted.'); - } - header($_SERVER['SERVER_PROTOCOL'] . ' ' . t('404 Not Found')); - $this->tpl->assign('error_message', $message); - $this->renderPage('404'); - } } diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php index 46219a3d..76b0afe8 100644 --- a/application/security/SessionManager.php +++ b/application/security/SessionManager.php @@ -48,6 +48,20 @@ class SessionManager $this->savePath = $savePath; } + /** + * Initialize XSRF token and links per page session variables. + */ + public function initialize(): void + { + if (!isset($this->session['tokens'])) { + $this->session['tokens'] = []; + } + + if (!isset($this->session['LINKS_PER_PAGE'])) { + $this->session['LINKS_PER_PAGE'] = $this->conf->get('general.links_per_page', 20); + } + } + /** * Define whether the user should stay signed in across browser sessions * -- cgit v1.2.3 From 3ee8351e438f13ccf36062ce956e0b4a4d5f4a29 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 23 Jul 2020 16:41:32 +0200 Subject: Multiple small fixes --- application/config/ConfigManager.php | 4 +++- application/container/ContainerBuilder.php | 1 - application/front/ShaarliMiddleware.php | 2 +- .../front/controller/admin/ConfigureController.php | 17 +++++++++++------ .../front/controller/visitor/InstallController.php | 13 +++++-------- application/front/controller/visitor/TagController.php | 2 -- application/legacy/LegacyUpdater.php | 3 ++- 7 files changed, 22 insertions(+), 20 deletions(-) (limited to 'application') diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php index e45bb4c3..4c98be30 100644 --- a/application/config/ConfigManager.php +++ b/application/config/ConfigManager.php @@ -3,6 +3,7 @@ namespace Shaarli\Config; use Shaarli\Config\Exception\MissingFieldConfigException; use Shaarli\Config\Exception\UnauthorizedConfigException; +use Shaarli\Thumbnailer; /** * Class ConfigManager @@ -361,7 +362,7 @@ class ConfigManager $this->setEmpty('security.open_shaarli', false); $this->setEmpty('security.allowed_protocols', ['ftp', 'ftps', 'magnet']); - $this->setEmpty('general.header_link', '?'); + $this->setEmpty('general.header_link', '/'); $this->setEmpty('general.links_per_page', 20); $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS); $this->setEmpty('general.default_note_title', 'Note: '); @@ -381,6 +382,7 @@ class ConfigManager // default state of the 'remember me' checkbox of the login form $this->setEmpty('privacy.remember_user_default', true); + $this->setEmpty('thumbnails.mode', Thumbnailer::MODE_ALL); $this->setEmpty('thumbnails.width', '125'); $this->setEmpty('thumbnails.height', '90'); diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index 593aafb7..bfe93501 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -99,7 +99,6 @@ class ContainerBuilder $container['pluginManager'] = function (ShaarliContainer $container): PluginManager { $pluginManager = new PluginManager($container->conf); - // FIXME! Configuration is already injected $pluginManager->load($container->conf->get('general.enabled_plugins')); return $pluginManager; diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php index e9f5552d..fd978e99 100644 --- a/application/front/ShaarliMiddleware.php +++ b/application/front/ShaarliMiddleware.php @@ -56,7 +56,7 @@ class ShaarliMiddleware } catch (ShaarliFrontException $e) { // Possible functional error $this->container->pageBuilder->reset(); - $this->container->pageBuilder->assign('message', $e->getMessage()); + $this->container->pageBuilder->assign('message', nl2br($e->getMessage())); $response = $response->withStatus($e->getCode()); diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php index 865fc2b0..e675fcca 100644 --- a/application/front/controller/admin/ConfigureController.php +++ b/application/front/controller/admin/ConfigureController.php @@ -98,10 +98,10 @@ class ConfigureController extends ShaarliAdminController if ($thumbnailsMode !== Thumbnailer::MODE_NONE && $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) ) { - $this->saveWarningMessage(t( - 'You have enabled or changed thumbnails mode. ' - .'Please synchronize them.' - )); + $this->saveWarningMessage( + t('You have enabled or changed thumbnails mode.') . + '' . t('Please synchronize them.') .'' + ); } $this->container->conf->set('thumbnails.mode', $thumbnailsMode); @@ -110,8 +110,13 @@ class ConfigureController extends ShaarliAdminController $this->container->history->updateSettings(); $this->container->pageCacheManager->invalidateCaches(); } catch (Throwable $e) { - // TODO: translation + stacktrace - $this->saveErrorMessage('ERROR while writing config file after configuration update.'); + $this->assignView('message', t('Error while writing config file after configuration update.')); + + if ($this->container->conf->get('dev.debug', false)) { + $this->assignView('stacktrace', $e->getMessage() . PHP_EOL . $e->getTraceAsString()); + } + + return $response->write($this->render('error')); } $this->saveSuccessMessage(t('Configuration was saved.')); diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php index aa032860..94ebb4ae 100644 --- a/application/front/controller/visitor/InstallController.php +++ b/application/front/controller/visitor/InstallController.php @@ -128,13 +128,14 @@ class InstallController extends ShaarliVisitorController $this->container->conf->get('credentials.salt') ) ); + $this->container->conf->set('general.header_link', $this->container->basePath); try { // Everything is ok, let's create config file. $this->container->conf->write($this->container->loginManager->isLoggedIn()); } catch (\Exception $e) { - $this->assignView('message', $e->getMessage()); - $this->assignView('stacktrace', $e->getTraceAsString()); + $this->assignView('message', t('Error while writing config file after configuration update.')); + $this->assignView('stacktrace', $e->getMessage() . PHP_EOL . $e->getTraceAsString()); return $response->write($this->render('error')); } @@ -155,18 +156,14 @@ class InstallController extends ShaarliVisitorController { // Ensure Shaarli has proper access to its resources $errors = ApplicationUtils::checkResourcePermissions($this->container->conf); - if (empty($errors)) { return true; } - // FIXME! Do not insert HTML here. - $message = '

'. t('Insufficient permissions:') .'

    '; - + $message = t('Insufficient permissions:') . PHP_EOL; foreach ($errors as $error) { - $message .= '
  • '.$error.'
  • '; + $message .= PHP_EOL . $error; } - $message .= '
'; throw new ResourcePermissionException($message); } diff --git a/application/front/controller/visitor/TagController.php b/application/front/controller/visitor/TagController.php index c176f43f..de4e7ea2 100644 --- a/application/front/controller/visitor/TagController.php +++ b/application/front/controller/visitor/TagController.php @@ -11,8 +11,6 @@ use Slim\Http\Response; * Class TagController * * Slim controller handle tags. - * - * TODO: check redirections with new helper */ class TagController extends ShaarliVisitorController { diff --git a/application/legacy/LegacyUpdater.php b/application/legacy/LegacyUpdater.php index cbf6890f..0ab3a55b 100644 --- a/application/legacy/LegacyUpdater.php +++ b/application/legacy/LegacyUpdater.php @@ -534,7 +534,8 @@ class LegacyUpdater if ($thumbnailsEnabled) { $this->session['warnings'][] = t( - 'You have enabled or changed thumbnails mode. Please synchronize them.' + t('You have enabled or changed thumbnails mode.') . + '' . t('Please synchronize them.') . '' ); } -- cgit v1.2.3 From 8e9169cebaf5697344cb373d69fe429e8e0efd5d Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 23 Jul 2020 17:13:22 +0200 Subject: Update French translation --- application/front/controller/admin/PluginsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'application') diff --git a/application/front/controller/admin/PluginsController.php b/application/front/controller/admin/PluginsController.php index 44025395..1eb7e635 100644 --- a/application/front/controller/admin/PluginsController.php +++ b/application/front/controller/admin/PluginsController.php @@ -75,7 +75,7 @@ class PluginsController extends ShaarliAdminController $this->saveSuccessMessage(t('Setting successfully saved.')); } catch (Exception $e) { $this->saveErrorMessage( - t('ERROR while saving plugin configuration: ') . PHP_EOL . $e->getMessage() + t('Error while saving plugin configuration: ') . PHP_EOL . $e->getMessage() ); } -- cgit v1.2.3 From 87ae3c4f08431e02869376cb57add257747910d1 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 24 Jul 2020 10:30:47 +0200 Subject: Fix default link and redirection in install controller --- application/front/controller/visitor/InstallController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'application') diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php index 94ebb4ae..5e3152c7 100644 --- a/application/front/controller/visitor/InstallController.php +++ b/application/front/controller/visitor/InstallController.php @@ -128,7 +128,7 @@ class InstallController extends ShaarliVisitorController $this->container->conf->get('credentials.salt') ) ); - $this->container->conf->set('general.header_link', $this->container->basePath); + $this->container->conf->set('general.header_link', $this->container->basePath . '/'); try { // Everything is ok, let's create config file. @@ -149,7 +149,7 @@ class InstallController extends ShaarliVisitorController [t('Shaarli is now configured. Please login and start shaaring your bookmarks!')] ); - return $this->redirect($response, '/'); + return $this->redirect($response, '/login'); } protected function checkPermissions(): bool -- cgit v1.2.3 From 204035bd3c91b9a5c39fcb6fc470e108b032dbd9 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 24 Jul 2020 12:48:53 +0200 Subject: Fix: visitor are allowed to chose nb of links per page --- .../controller/admin/SessionFilterController.php | 20 +------------ .../visitor/PublicSessionFilterController.php | 33 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 19 deletions(-) create mode 100644 application/front/controller/visitor/PublicSessionFilterController.php (limited to 'application') diff --git a/application/front/controller/admin/SessionFilterController.php b/application/front/controller/admin/SessionFilterController.php index 69a16ec3..081c0ba0 100644 --- a/application/front/controller/admin/SessionFilterController.php +++ b/application/front/controller/admin/SessionFilterController.php @@ -12,28 +12,10 @@ use Slim\Http\Response; /** * Class SessionFilterController * - * Slim controller used to handle filters stored in the user session, such as visibility, links per page, etc. + * Slim controller used to handle filters stored in the user session, such as visibility, etc. */ class SessionFilterController extends ShaarliAdminController { - /** - * GET /links-per-page: set the number of bookmarks to display per page in homepage - */ - public function linksPerPage(Request $request, Response $response): Response - { - $linksPerPage = $request->getParam('nb') ?? null; - if (null === $linksPerPage || false === is_numeric($linksPerPage)) { - $linksPerPage = $this->container->conf->get('general.links_per_page', 20); - } - - $this->container->sessionManager->setSessionParameter( - SessionManager::KEY_LINKS_PER_PAGE, - abs(intval($linksPerPage)) - ); - - return $this->redirectFromReferer($request, $response, ['linksperpage'], ['nb']); - } - /** * GET /visibility: allows to display only public or only private bookmarks in linklist */ diff --git a/application/front/controller/visitor/PublicSessionFilterController.php b/application/front/controller/visitor/PublicSessionFilterController.php new file mode 100644 index 00000000..35da0c5f --- /dev/null +++ b/application/front/controller/visitor/PublicSessionFilterController.php @@ -0,0 +1,33 @@ +getParam('nb') ?? null; + if (null === $linksPerPage || false === is_numeric($linksPerPage)) { + $linksPerPage = $this->container->conf->get('general.links_per_page', 20); + } + + $this->container->sessionManager->setSessionParameter( + SessionManager::KEY_LINKS_PER_PAGE, + abs(intval($linksPerPage)) + ); + + return $this->redirectFromReferer($request, $response, ['linksperpage'], ['nb']); + } +} -- cgit v1.2.3 From 9fbc42294e7667c5ef19cafa0d1fcfbc1c0f36a9 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 26 Jul 2020 14:43:10 +0200 Subject: New basePath: fix officiel plugin paths and vintage template --- application/config/ConfigPlugin.php | 17 ++++++++++- application/front/ShaarliMiddleware.php | 4 +-- .../controller/admin/ManageShaareController.php | 25 ++++------------ .../front/controller/admin/PluginsController.php | 17 +---------- .../front/controller/admin/ToolsController.php | 17 +---------- .../controller/visitor/BookmarkListController.php | 16 ++--------- .../front/controller/visitor/DailyController.php | 22 ++------------- .../front/controller/visitor/FeedController.php | 21 +------------- .../controller/visitor/PictureWallController.php | 23 ++------------- .../visitor/ShaarliVisitorController.php | 33 ++++++++++++---------- .../controller/visitor/TagCloudController.php | 24 ++-------------- application/plugin/PluginManager.php | 4 +++ application/render/PageBuilder.php | 32 ++++++++------------- 13 files changed, 70 insertions(+), 185 deletions(-) (limited to 'application') diff --git a/application/config/ConfigPlugin.php b/application/config/ConfigPlugin.php index dbb24937..ea8dfbda 100644 --- a/application/config/ConfigPlugin.php +++ b/application/config/ConfigPlugin.php @@ -1,6 +1,7 @@ $value) { // No duplicate order allowed. - if (in_array($value, $orders)) { + if (in_array($value, $orders, true)) { return false; } diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php index fd978e99..92c0e911 100644 --- a/application/front/ShaarliMiddleware.php +++ b/application/front/ShaarliMiddleware.php @@ -60,7 +60,7 @@ class ShaarliMiddleware $response = $response->withStatus($e->getCode()); - return $response->write($this->container->pageBuilder->render('error')); + return $response->write($this->container->pageBuilder->render('error', $this->container->basePath)); } catch (UnauthorizedException $e) { $returnUrl = urlencode($this->container->environment['REQUEST_URI']); @@ -80,7 +80,7 @@ class ShaarliMiddleware $response = $response->withStatus(500); - return $response->write($this->container->pageBuilder->render('error')); + return $response->write($this->container->pageBuilder->render('error', $this->container->basePath)); } } diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php index 3aa48423..33e1188e 100644 --- a/application/front/controller/admin/ManageShaareController.php +++ b/application/front/controller/admin/ManageShaareController.php @@ -152,7 +152,7 @@ class ManageShaareController extends ShaarliAdminController // 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); + $this->executePageHooks('save_link', $data); $bookmark->fromArray($data); $this->container->bookmarkService->set($bookmark); @@ -211,7 +211,7 @@ class ManageShaareController extends ShaarliAdminController } $data = $formatter->format($bookmark); - $this->container->pluginManager->executeHooks('delete_link', $data); + $this->executePageHooks('delete_link', $data); $this->container->bookmarkService->remove($bookmark, false); ++ $count; } @@ -283,7 +283,7 @@ class ManageShaareController extends ShaarliAdminController // To preserve backward compatibility with 3rd parties, plugins still use arrays $data = $formatter->format($bookmark); - $this->container->pluginManager->executeHooks('save_link', $data); + $this->executePageHooks('save_link', $data); $bookmark->fromArray($data); $this->container->bookmarkService->set($bookmark, false); @@ -325,7 +325,7 @@ class ManageShaareController extends ShaarliAdminController // To preserve backward compatibility with 3rd parties, plugins still use arrays $data = $formatter->format($bookmark); - $this->container->pluginManager->executeHooks('save_link', $data); + $this->executePageHooks('save_link', $data); $bookmark->fromArray($data); $this->container->bookmarkService->set($bookmark); @@ -354,7 +354,7 @@ class ManageShaareController extends ShaarliAdminController 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false), ]; - $data = $this->executeHooks('render_editlink', $data); + $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK); foreach ($data as $key => $value) { $this->assignView($key, $value); @@ -368,19 +368,4 @@ class ManageShaareController extends ShaarliAdminController return $response->write($this->render(TemplatePage::EDIT_LINK)); } - - /** - * @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; - } } diff --git a/application/front/controller/admin/PluginsController.php b/application/front/controller/admin/PluginsController.php index 1eb7e635..0e09116e 100644 --- a/application/front/controller/admin/PluginsController.php +++ b/application/front/controller/admin/PluginsController.php @@ -58,7 +58,7 @@ class PluginsController extends ShaarliAdminController try { $parameters = $request->getParams() ?? []; - $this->executeHooks($parameters); + $this->executePageHooks('save_plugin_parameters', $parameters); if (isset($parameters['parameters_form'])) { unset($parameters['parameters_form']); @@ -81,19 +81,4 @@ class PluginsController extends ShaarliAdminController return $this->redirect($response, '/admin/plugins'); } - - /** - * @param mixed[] $data Variables passed to the template engine - * - * @return mixed[] Template data after active plugins render_picwall hook execution. - */ - protected function executeHooks(array $data): array - { - $this->container->pluginManager->executeHooks( - 'save_plugin_parameters', - $data - ); - - return $data; - } } diff --git a/application/front/controller/admin/ToolsController.php b/application/front/controller/admin/ToolsController.php index a476e898..a87f20d2 100644 --- a/application/front/controller/admin/ToolsController.php +++ b/application/front/controller/admin/ToolsController.php @@ -22,7 +22,7 @@ class ToolsController extends ShaarliAdminController 'sslenabled' => is_https($this->container->environment), ]; - $data = $this->executeHooks($data); + $this->executePageHooks('render_tools', $data, TemplatePage::TOOLS); foreach ($data as $key => $value) { $this->assignView($key, $value); @@ -32,19 +32,4 @@ class ToolsController extends ShaarliAdminController return $response->write($this->render(TemplatePage::TOOLS)); } - - /** - * @param mixed[] $data Variables passed to the template engine - * - * @return mixed[] Template data after active plugins render_picwall hook execution. - */ - protected function executeHooks(array $data): array - { - $this->container->pluginManager->executeHooks( - 'render_tools', - $data - ); - - return $data; - } } diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php index a37a7f6b..23c4fbae 100644 --- a/application/front/controller/visitor/BookmarkListController.php +++ b/application/front/controller/visitor/BookmarkListController.php @@ -124,7 +124,7 @@ class BookmarkListController extends ShaarliVisitorController $data['pagetitle'] = ($data['pagetitle'] ?? '') . $this->container->conf->get('general.title', 'Shaarli'); - $this->executeHooks($data); + $this->executePageHooks('render_linklist', $data, TemplatePage::LINKLIST); $this->assignAllView($data); return $response->write($this->render(TemplatePage::LINKLIST)); @@ -153,7 +153,7 @@ class BookmarkListController extends ShaarliVisitorController ] ); - $this->executeHooks($data); + $this->executePageHooks('render_linklist', $data, TemplatePage::LINKLIST); $this->assignAllView($data); return $response->write($this->render(TemplatePage::LINKLIST)); @@ -182,18 +182,6 @@ class BookmarkListController extends ShaarliVisitorController return false; } - /** - * @param mixed[] $data Template vars to process in plugins, passed as reference. - */ - protected function executeHooks(array &$data): void - { - $this->container->pluginManager->executeHooks( - 'render_linklist', - $data, - ['loggedin' => $this->container->loginManager->isLoggedIn()] - ); - } - /** * @return string[] Default template variables without values. */ diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index 05b4f095..808ca5f7 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php @@ -72,13 +72,11 @@ class DailyController extends ShaarliVisitorController ]; // Hooks are called before column construction so that plugins don't have to deal with columns. - $data = $this->executeHooks($data); + $this->executePageHooks('render_daily', $data, TemplatePage::DAILY); $data['cols'] = $this->calculateColumns($data['linksToDisplay']); - foreach ($data as $key => $value) { - $this->assignView($key, $value); - } + $this->assignAllView($data); $mainTitle = $this->container->conf->get('general.title', 'Shaarli'); $this->assignView( @@ -190,20 +188,4 @@ class DailyController extends ShaarliVisitorController return $columns; } - - /** - * @param mixed[] $data Variables passed to the template engine - * - * @return mixed[] Template data after active plugins render_picwall hook execution. - */ - protected function executeHooks(array $data): array - { - $this->container->pluginManager->executeHooks( - 'render_daily', - $data, - ['loggedin' => $this->container->loginManager->isLoggedIn()] - ); - - return $data; - } } diff --git a/application/front/controller/visitor/FeedController.php b/application/front/controller/visitor/FeedController.php index f76f55fd..da2848c2 100644 --- a/application/front/controller/visitor/FeedController.php +++ b/application/front/controller/visitor/FeedController.php @@ -46,7 +46,7 @@ class FeedController extends ShaarliVisitorController $data = $this->container->feedBuilder->buildData($feedType, $request->getParams()); - $data = $this->executeHooks($data, $feedType); + $this->executePageHooks('render_feed', $data, $feedType); $this->assignAllView($data); $content = $this->render('feed.'. $feedType); @@ -55,23 +55,4 @@ class FeedController extends ShaarliVisitorController return $response->write($content); } - - /** - * @param mixed[] $data Template data - * - * @return mixed[] Template data after active plugins hook execution. - */ - protected function executeHooks(array $data, string $feedType): array - { - $this->container->pluginManager->executeHooks( - 'render_feed', - $data, - [ - 'loggedin' => $this->container->loginManager->isLoggedIn(), - 'target' => $feedType, - ] - ); - - return $data; - } } diff --git a/application/front/controller/visitor/PictureWallController.php b/application/front/controller/visitor/PictureWallController.php index 5ef2cb17..3c57f8dd 100644 --- a/application/front/controller/visitor/PictureWallController.php +++ b/application/front/controller/visitor/PictureWallController.php @@ -42,30 +42,13 @@ class PictureWallController extends ShaarliVisitorController } } - $data = $this->executeHooks($linksToDisplay); + $data = ['linksToDisplay' => $linksToDisplay]; + $this->executePageHooks('render_picwall', $data, TemplatePage::PICTURE_WALL); + foreach ($data as $key => $value) { $this->assignView($key, $value); } return $response->write($this->render(TemplatePage::PICTURE_WALL)); } - - /** - * @param mixed[] $linksToDisplay List of formatted bookmarks - * - * @return mixed[] Template data after active plugins render_picwall hook execution. - */ - protected function executeHooks(array $linksToDisplay): array - { - $data = [ - 'linksToDisplay' => $linksToDisplay, - ]; - $this->container->pluginManager->executeHooks( - 'render_picwall', - $data, - ['loggedin' => $this->container->loginManager->isLoggedIn()] - ); - - return $data; - } } diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php index b494a8e6..47057d97 100644 --- a/application/front/controller/visitor/ShaarliVisitorController.php +++ b/application/front/controller/visitor/ShaarliVisitorController.php @@ -60,22 +60,9 @@ abstract class ShaarliVisitorController $this->assignView('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE)); $this->assignView('plugin_errors', $this->container->pluginManager->getErrors()); - /* - * Define base path (if Shaarli is installed in a domain's subfolder, e.g. `/shaarli`) - * and the asset path (subfolder/tpl/default for default theme). - * These MUST be used to create an internal link or to include an asset in templates. - */ - $this->assignView('base_path', $this->container->basePath); - $this->assignView( - 'asset_path', - $this->container->basePath . '/' . - rtrim($this->container->conf->get('resource.raintpl_tpl', 'tpl'), '/') . '/' . - $this->container->conf->get('resource.theme', 'default') - ); - $this->executeDefaultHooks($template); - return $this->container->pageBuilder->render($template); + return $this->container->pageBuilder->render($template, $this->container->basePath); } /** @@ -97,13 +84,29 @@ abstract class ShaarliVisitorController $pluginData, [ 'target' => $template, - 'loggedin' => $this->container->loginManager->isLoggedIn() + 'loggedin' => $this->container->loginManager->isLoggedIn(), + 'basePath' => $this->container->basePath, ] ); $this->assignView('plugins_' . $name, $pluginData); } } + protected function executePageHooks(string $hook, array &$data, string $template = null): void + { + $params = [ + 'target' => $template, + 'loggedin' => $this->container->loginManager->isLoggedIn(), + 'basePath' => $this->container->basePath, + ]; + + $this->container->pluginManager->executeHooks( + $hook, + $data, + $params + ); + } + /** * Simple helper which prepend the base path to redirect path. * diff --git a/application/front/controller/visitor/TagCloudController.php b/application/front/controller/visitor/TagCloudController.php index 15b6d7b7..f9c529bc 100644 --- a/application/front/controller/visitor/TagCloudController.php +++ b/application/front/controller/visitor/TagCloudController.php @@ -71,10 +71,8 @@ class TagCloudController extends ShaarliVisitorController 'search_tags' => $searchTags, 'tags' => $tags, ]; - $data = $this->executeHooks('tag' . $type, $data); - foreach ($data as $key => $value) { - $this->assignView($key, $value); - } + $this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type); + $this->assignAllView($data); $searchTags = !empty($searchTags) ? $searchTags .' - ' : ''; $this->assignView( @@ -82,7 +80,7 @@ class TagCloudController extends ShaarliVisitorController $searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli') ); - return $response->write($this->render('tag.'. $type)); + return $response->write($this->render('tag.' . $type)); } /** @@ -112,20 +110,4 @@ class TagCloudController extends ShaarliVisitorController return $tagList; } - - /** - * @param mixed[] $data Template data - * - * @return mixed[] Template data after active plugins hook execution. - */ - protected function executeHooks(string $template, array $data): array - { - $this->container->pluginManager->executeHooks( - 'render_'. $template, - $data, - ['loggedin' => $this->container->loginManager->isLoggedIn()] - ); - - return $data; - } } diff --git a/application/plugin/PluginManager.php b/application/plugin/PluginManager.php index 591a9180..b3e8b2f8 100644 --- a/application/plugin/PluginManager.php +++ b/application/plugin/PluginManager.php @@ -108,6 +108,10 @@ class PluginManager $data['_LOGGEDIN_'] = $params['loggedin']; } + if (isset($params['basePath'])) { + $data['_BASE_PATH_'] = $params['basePath']; + } + foreach ($this->loadedPlugins as $plugin) { $hookFunction = $this->buildHookName($hook, $plugin); diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php index 471724c0..7a716673 100644 --- a/application/render/PageBuilder.php +++ b/application/render/PageBuilder.php @@ -3,6 +3,7 @@ namespace Shaarli\Render; use Exception; +use exceptions\MissingBasePathException; use RainTPL; use Shaarli\ApplicationUtils; use Shaarli\Bookmark\BookmarkServiceInterface; @@ -156,7 +157,7 @@ class PageBuilder * Affect variable after controller processing. * Used for alert messages. */ - protected function finalize(): void + protected function finalize(string $basePath): void { // TODO: use the SessionManager $messageKeys = [ @@ -170,6 +171,14 @@ class PageBuilder unset($_SESSION[$messageKey]); } } + + $this->assign('base_path', $basePath); + $this->assign( + 'asset_path', + $basePath . '/' . + rtrim($this->conf->get('resource.raintpl_tpl', 'tpl'), '/') . '/' . + $this->conf->get('resource.theme', 'default') + ); } /** @@ -209,23 +218,6 @@ class PageBuilder return true; } - /** - * Render a specific page (using a template file). - * e.g. $pb->renderPage('picwall'); - * - * @param string $page Template filename (without extension). - */ - public function renderPage($page) - { - if ($this->tpl === false) { - $this->initialize(); - } - - $this->finalize(); - - $this->tpl->draw($page); - } - /** * Render a specific page as string (using a template file). * e.g. $pb->render('picwall'); @@ -234,13 +226,13 @@ class PageBuilder * * @return string Processed template content */ - public function render(string $page): string + public function render(string $page, string $basePath): string { if ($this->tpl === false) { $this->initialize(); } - $this->finalize(); + $this->finalize($basePath); return $this->tpl->draw($page, true); } -- cgit v1.2.3 From a285668ec4456c4d413c1d6dec275f1d18bf3f15 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 27 Jul 2020 12:34:17 +0200 Subject: Fix redirection after post install login --- application/front/controller/visitor/LoginController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'application') diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php index c40b8cc4..121ba40b 100644 --- a/application/front/controller/visitor/LoginController.php +++ b/application/front/controller/visitor/LoginController.php @@ -91,7 +91,7 @@ class LoginController extends ShaarliVisitorController // Force referer from given return URL $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl'); - return $this->redirectFromReferer($request, $response, ['login']); + return $this->redirectFromReferer($request, $response, ['login', 'install']); } /** -- cgit v1.2.3 From 301c7ab1a079d937ab41c6f52b8804e5731008e6 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 28 Jul 2020 20:46:11 +0200 Subject: Better support for notes permalink --- application/api/ApiUtils.php | 2 +- application/bookmark/Bookmark.php | 4 ++-- application/container/ContainerBuilder.php | 5 ++++- application/feed/FeedBuilder.php | 2 +- application/formatter/BookmarkDefaultFormatter.php | 22 ++++++++++++++-------- application/formatter/BookmarkFormatter.php | 6 ++++-- application/formatter/FormatterFactory.php | 2 +- .../controller/visitor/BookmarkListController.php | 6 +++++- .../front/controller/visitor/DailyController.php | 1 + application/netscape/NetscapeBookmarkUtils.php | 2 +- 10 files changed, 34 insertions(+), 18 deletions(-) (limited to 'application') diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php index 5156a5f7..faebb8f5 100644 --- a/application/api/ApiUtils.php +++ b/application/api/ApiUtils.php @@ -67,7 +67,7 @@ class ApiUtils if (! $bookmark->isNote()) { $out['url'] = $bookmark->getUrl(); } else { - $out['url'] = $indexUrl . $bookmark->getUrl(); + $out['url'] = rtrim($indexUrl, '/') . '/' . ltrim($bookmark->getUrl(), '/'); } $out['shorturl'] = $bookmark->getShortUrl(); $out['title'] = $bookmark->getTitle(); diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php index 90ff5b16..c6f2c515 100644 --- a/application/bookmark/Bookmark.php +++ b/application/bookmark/Bookmark.php @@ -106,7 +106,7 @@ class Bookmark throw new InvalidBookmarkException($this); } if (empty($this->url)) { - $this->url = '?'. $this->shortUrl; + $this->url = '/shaare/'. $this->shortUrl; } if (empty($this->title)) { $this->title = $this->url; @@ -406,7 +406,7 @@ class Bookmark public function isNote() { // We check empty value to get a valid result if the link has not been saved yet - return empty($this->url) || $this->url[0] === '?'; + return empty($this->url) || startsWith($this->url, '/shaare/') || $this->url[0] === '?'; } /** diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index bfe93501..2e8c1ee3 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -105,7 +105,10 @@ class ContainerBuilder }; $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory { - return new FormatterFactory($container->conf, $container->loginManager->isLoggedIn()); + return new FormatterFactory( + $container->conf, + $container->loginManager->isLoggedIn() + ); }; $container['pageCacheManager'] = function (ShaarliContainer $container): PageCacheManager { diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php index c97ae1ea..269ad877 100644 --- a/application/feed/FeedBuilder.php +++ b/application/feed/FeedBuilder.php @@ -174,7 +174,7 @@ class FeedBuilder protected function buildItem(string $feedType, $link, $pageaddr) { $data = $this->formatter->format($link); - $data['guid'] = $pageaddr . '?' . $data['shorturl']; + $data['guid'] = rtrim($pageaddr, '/') . '/shaare/' . $data['shorturl']; if ($this->usePermalinks === true) { $permalink = ''. t('Direct link') .''; } else { diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php index c6c59064..08e710eb 100644 --- a/application/formatter/BookmarkDefaultFormatter.php +++ b/application/formatter/BookmarkDefaultFormatter.php @@ -50,11 +50,10 @@ class BookmarkDefaultFormatter extends BookmarkFormatter */ public function formatUrl($bookmark) { - if (! empty($this->contextData['index_url']) && ( - startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/') - )) { - return $this->contextData['index_url'] . escape($bookmark->getUrl()); + if ($bookmark->isNote() && !empty($this->contextData['index_url'])) { + return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/')); } + return escape($bookmark->getUrl()); } @@ -63,11 +62,18 @@ class BookmarkDefaultFormatter extends BookmarkFormatter */ protected function formatRealUrl($bookmark) { - if (! empty($this->contextData['index_url']) && ( - startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/') - )) { - return $this->contextData['index_url'] . escape($bookmark->getUrl()); + if ($bookmark->isNote()) { + if (!empty($this->contextData['index_url'])) { + $prefix = rtrim($this->contextData['index_url'], '/') . '/'; + } + + if (!empty($this->contextData['base_path'])) { + $prefix = rtrim($this->contextData['base_path'], '/') . '/'; + } + + return escape($prefix ?? '') . escape(ltrim($bookmark->getUrl(), '/')); } + return escape($bookmark->getUrl()); } diff --git a/application/formatter/BookmarkFormatter.php b/application/formatter/BookmarkFormatter.php index a80d83fc..22ba7aae 100644 --- a/application/formatter/BookmarkFormatter.php +++ b/application/formatter/BookmarkFormatter.php @@ -3,8 +3,8 @@ namespace Shaarli\Formatter; use DateTime; -use Shaarli\Config\ConfigManager; use Shaarli\Bookmark\Bookmark; +use Shaarli\Config\ConfigManager; /** * Class BookmarkFormatter @@ -80,6 +80,8 @@ abstract class BookmarkFormatter public function addContextData($key, $value) { $this->contextData[$key] = $value; + + return $this; } /** @@ -128,7 +130,7 @@ abstract class BookmarkFormatter */ protected function formatRealUrl($bookmark) { - return $bookmark->getUrl(); + return $this->formatUrl($bookmark); } /** diff --git a/application/formatter/FormatterFactory.php b/application/formatter/FormatterFactory.php index 5f282f68..a029579f 100644 --- a/application/formatter/FormatterFactory.php +++ b/application/formatter/FormatterFactory.php @@ -38,7 +38,7 @@ class FormatterFactory * * @return BookmarkFormatter instance. */ - public function getFormatter(string $type = null) + public function getFormatter(string $type = null): BookmarkFormatter { $type = $type ? $type : $this->conf->get('formatter', 'default'); $className = '\\Shaarli\\Formatter\\Bookmark'. ucfirst($type) .'Formatter'; diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php index 23c4fbae..2988bee6 100644 --- a/application/front/controller/visitor/BookmarkListController.php +++ b/application/front/controller/visitor/BookmarkListController.php @@ -32,6 +32,7 @@ class BookmarkListController extends ShaarliVisitorController } $formatter = $this->container->formatterFactory->getFormatter(); + $formatter->addContextData('base_path', $this->container->basePath); $searchTags = escape(normalize_spaces($request->getParam('searchtags') ?? '')); $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));; @@ -145,11 +146,14 @@ class BookmarkListController extends ShaarliVisitorController $this->updateThumbnail($bookmark); + $formatter = $this->container->formatterFactory->getFormatter(); + $formatter->addContextData('base_path', $this->container->basePath); + $data = array_merge( $this->initializeTemplateVars(), [ 'pagetitle' => $bookmark->getTitle() .' - '. $this->container->conf->get('general.title', 'Shaarli'), - 'links' => [$this->container->formatterFactory->getFormatter()->format($bookmark)], + 'links' => [$formatter->format($bookmark)], ] ); diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index 808ca5f7..54a4778f 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php @@ -54,6 +54,7 @@ class DailyController extends ShaarliVisitorController } $formatter = $this->container->formatterFactory->getFormatter(); + $formatter->addContextData('base_path', $this->container->basePath); // We pre-format some fields for proper output. foreach ($linksToDisplay as $key => $bookmark) { $linksToDisplay[$key] = $formatter->format($bookmark); diff --git a/application/netscape/NetscapeBookmarkUtils.php b/application/netscape/NetscapeBookmarkUtils.php index b150f649..b83f16f8 100644 --- a/application/netscape/NetscapeBookmarkUtils.php +++ b/application/netscape/NetscapeBookmarkUtils.php @@ -68,7 +68,7 @@ class NetscapeBookmarkUtils $link = $formatter->format($bookmark); $link['taglist'] = implode(',', $bookmark->getTags()); if ($bookmark->isNote() && $prependNoteUrl) { - $link['url'] = $indexUrl . $link['url']; + $link['url'] = rtrim($indexUrl, '/') . '/' . ltrim($link['url'], '/'); } $bookmarkLinks[] = $link; -- cgit v1.2.3 From 624123177f8673f978c49186b43fd96c6827d8a0 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 28 Jul 2020 21:09:22 +0200 Subject: Include empty basePath in formatting --- application/formatter/BookmarkDefaultFormatter.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'application') diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php index 08e710eb..9d4a0fa0 100644 --- a/application/formatter/BookmarkDefaultFormatter.php +++ b/application/formatter/BookmarkDefaultFormatter.php @@ -50,7 +50,7 @@ class BookmarkDefaultFormatter extends BookmarkFormatter */ public function formatUrl($bookmark) { - if ($bookmark->isNote() && !empty($this->contextData['index_url'])) { + if ($bookmark->isNote() && isset($this->contextData['index_url'])) { return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/')); } @@ -63,11 +63,11 @@ class BookmarkDefaultFormatter extends BookmarkFormatter protected function formatRealUrl($bookmark) { if ($bookmark->isNote()) { - if (!empty($this->contextData['index_url'])) { + if (isset($this->contextData['index_url'])) { $prefix = rtrim($this->contextData['index_url'], '/') . '/'; } - if (!empty($this->contextData['base_path'])) { + if (isset($this->contextData['base_path'])) { $prefix = rtrim($this->contextData['base_path'], '/') . '/'; } -- cgit v1.2.3 From f7f08ceec1b218e1525153e8bd3d0199f2fb1c9d Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 28 Jul 2020 22:24:41 +0200 Subject: Fix basePath in unit tests reference DB --- application/front/ShaarliMiddleware.php | 1 + application/updater/Updater.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'application') diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php index 92c0e911..707489d0 100644 --- a/application/front/ShaarliMiddleware.php +++ b/application/front/ShaarliMiddleware.php @@ -93,6 +93,7 @@ class ShaarliMiddleware return; } + $this->container->updater->setBasePath($this->container->basePath); $newUpdates = $this->container->updater->update(); if (!empty($newUpdates)) { $this->container->updater->writeUpdates( diff --git a/application/updater/Updater.php b/application/updater/Updater.php index 4c578528..88a7bc7b 100644 --- a/application/updater/Updater.php +++ b/application/updater/Updater.php @@ -157,7 +157,7 @@ class Updater && 1 === preg_match('/^\?([a-zA-Z0-9-_@]{6})($|&|#)/', $bookmark->getUrl(), $match) ) { $updated = true; - $bookmark = $bookmark->setUrl($this->basePath . '/shaare/' . $match[1]); + $bookmark = $bookmark->setUrl('/shaare/' . $match[1]); $this->bookmarkService->set($bookmark, false); } -- cgit v1.2.3 From d6e5f04d3987e498c5cb859eed6bff33d67949df Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 1 Aug 2020 11:10:57 +0200 Subject: Remove anonymous permission and initialize bookmarks on login --- application/bookmark/BookmarkFileService.php | 36 ++++++++++------------ application/bookmark/BookmarkIO.php | 8 +++-- application/bookmark/BookmarkInitializer.php | 9 +----- application/bookmark/BookmarkServiceInterface.php | 14 --------- .../exception/DatastoreNotInitializedException.php | 10 ++++++ .../front/controller/visitor/InstallController.php | 5 --- 6 files changed, 33 insertions(+), 49 deletions(-) create mode 100644 application/bookmark/exception/DatastoreNotInitializedException.php (limited to 'application') diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 6e04f3b7..b3a90ed4 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -6,6 +6,7 @@ namespace Shaarli\Bookmark; use Exception; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; +use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; use Shaarli\Bookmark\Exception\EmptyDataStoreException; use Shaarli\Config\ConfigManager; use Shaarli\Formatter\BookmarkMarkdownFormatter; @@ -46,9 +47,6 @@ class BookmarkFileService implements BookmarkServiceInterface /** @var bool true for logged in users. Default value to retrieve private bookmarks. */ protected $isLoggedIn; - /** @var bool Allow datastore alteration from not logged in users. */ - protected $anonymousPermission = false; - /** * @inheritDoc */ @@ -65,10 +63,16 @@ class BookmarkFileService implements BookmarkServiceInterface } else { try { $this->bookmarks = $this->bookmarksIO->read(); - } catch (EmptyDataStoreException $e) { + } catch (EmptyDataStoreException|DatastoreNotInitializedException $e) { $this->bookmarks = new BookmarkArray(); + if ($this->isLoggedIn) { - $this->save(); + // Datastore file does not exists, we initialize it with default bookmarks. + if ($e instanceof DatastoreNotInitializedException) { + $this->initialize(); + } else { + $this->save(); + } } } @@ -157,7 +161,7 @@ class BookmarkFileService implements BookmarkServiceInterface */ public function set($bookmark, $save = true) { - if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { + if (true !== $this->isLoggedIn) { throw new Exception(t('You\'re not authorized to alter the datastore')); } if (! $bookmark instanceof Bookmark) { @@ -182,7 +186,7 @@ class BookmarkFileService implements BookmarkServiceInterface */ public function add($bookmark, $save = true) { - if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { + if (true !== $this->isLoggedIn) { throw new Exception(t('You\'re not authorized to alter the datastore')); } if (! $bookmark instanceof Bookmark) { @@ -207,7 +211,7 @@ class BookmarkFileService implements BookmarkServiceInterface */ public function addOrSet($bookmark, $save = true) { - if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { + if (true !== $this->isLoggedIn) { throw new Exception(t('You\'re not authorized to alter the datastore')); } if (! $bookmark instanceof Bookmark) { @@ -224,7 +228,7 @@ class BookmarkFileService implements BookmarkServiceInterface */ public function remove($bookmark, $save = true) { - if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { + if (true !== $this->isLoggedIn) { throw new Exception(t('You\'re not authorized to alter the datastore')); } if (! $bookmark instanceof Bookmark) { @@ -277,7 +281,7 @@ class BookmarkFileService implements BookmarkServiceInterface */ public function save() { - if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { + if (true !== $this->isLoggedIn) { // TODO: raise an Exception instead die('You are not authorized to change the database.'); } @@ -359,16 +363,10 @@ class BookmarkFileService implements BookmarkServiceInterface { $initializer = new BookmarkInitializer($this); $initializer->initialize(); - } - public function enableAnonymousPermission(): void - { - $this->anonymousPermission = true; - } - - public function disableAnonymousPermission(): void - { - $this->anonymousPermission = false; + if (true === $this->isLoggedIn) { + $this->save(); + } } /** diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php index 1026e2f9..6bf7f365 100644 --- a/application/bookmark/BookmarkIO.php +++ b/application/bookmark/BookmarkIO.php @@ -2,6 +2,7 @@ namespace Shaarli\Bookmark; +use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; use Shaarli\Bookmark\Exception\EmptyDataStoreException; use Shaarli\Bookmark\Exception\NotWritableDataStoreException; use Shaarli\Config\ConfigManager; @@ -52,13 +53,14 @@ class BookmarkIO * * @return BookmarkArray instance * - * @throws NotWritableDataStoreException Data couldn't be loaded - * @throws EmptyDataStoreException Datastore doesn't exist + * @throws NotWritableDataStoreException Data couldn't be loaded + * @throws EmptyDataStoreException Datastore file exists but does not contain any bookmark + * @throws DatastoreNotInitializedException File does not exists */ public function read() { if (! file_exists($this->datastore)) { - throw new EmptyDataStoreException(); + throw new DatastoreNotInitializedException(); } if (!is_writable($this->datastore)) { diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php index 479ee9a9..cd2d1724 100644 --- a/application/bookmark/BookmarkInitializer.php +++ b/application/bookmark/BookmarkInitializer.php @@ -6,8 +6,7 @@ namespace Shaarli\Bookmark; * Class BookmarkInitializer * * This class is used to initialized default bookmarks after a fresh install of Shaarli. - * It is no longer call when the data store is empty, - * because user might want to delete default bookmarks after the install. + * It should be only called if the datastore file does not exist(users might want to delete the default bookmarks). * * To prevent data corruption, it does not overwrite existing bookmarks, * even though there should not be any. @@ -34,8 +33,6 @@ class BookmarkInitializer */ public function initialize() { - $this->bookmarkService->enableAnonymousPermission(); - $bookmark = new Bookmark(); $bookmark->setTitle(t('My secret stuff... - Pastebin.com')); $bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8='); @@ -57,9 +54,5 @@ You use the community supported version of the original Shaarli project, by Seba )); $bookmark->setTagsString('opensource software'); $this->bookmarkService->add($bookmark, false); - - $this->bookmarkService->save(); - - $this->bookmarkService->disableAnonymousPermission(); } } diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php index 37fbda89..ce8bd912 100644 --- a/application/bookmark/BookmarkServiceInterface.php +++ b/application/bookmark/BookmarkServiceInterface.php @@ -6,7 +6,6 @@ namespace Shaarli\Bookmark; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Bookmark\Exception\NotWritableDataStoreException; use Shaarli\Config\ConfigManager; -use Shaarli\Exceptions\IOException; use Shaarli\History; /** @@ -177,17 +176,4 @@ interface BookmarkServiceInterface * Creates the default database after a fresh install. */ public function initialize(); - - /** - * Allow to write the datastore from anonymous session (not logged in). - * - * This covers a few specific use cases, such as datastore initialization, - * but it should be used carefully as it can lead to security issues. - */ - public function enableAnonymousPermission(); - - /** - * Disable anonymous permission. - */ - public function disableAnonymousPermission(); } diff --git a/application/bookmark/exception/DatastoreNotInitializedException.php b/application/bookmark/exception/DatastoreNotInitializedException.php new file mode 100644 index 00000000..f495049d --- /dev/null +++ b/application/bookmark/exception/DatastoreNotInitializedException.php @@ -0,0 +1,10 @@ +write($this->render('error')); } - if ($this->container->bookmarkService->count(BookmarkFilter::$ALL) === 0) { - $this->container->bookmarkService->initialize(); - } - $this->container->sessionManager->setSessionParameter( SessionManager::KEY_SUCCESS_MESSAGES, [t('Shaarli is now configured. Please login and start shaaring your bookmarks!')] -- cgit v1.2.3 From 1a68ae5a29bc33ab80c9cfbe043cb1213551533c Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 1 Aug 2020 11:14:03 +0200 Subject: Bookmark's thumbnails PHPDoc improvement --- application/bookmark/Bookmark.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'application') diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php index c6f2c515..1beb8be2 100644 --- a/application/bookmark/Bookmark.php +++ b/application/bookmark/Bookmark.php @@ -37,7 +37,7 @@ class Bookmark /** @var array List of bookmark's tags */ protected $tags; - /** @var string Thumbnail's URL - false if no thumbnail could be found */ + /** @var string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found */ protected $thumbnail; /** @var bool Set to true if the bookmark is set as sticky */ @@ -347,7 +347,7 @@ class Bookmark /** * Get the Thumbnail. * - * @return string|bool|null + * @return string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found */ public function getThumbnail() { @@ -357,7 +357,7 @@ class Bookmark /** * Set the Thumbnail. * - * @param string|bool $thumbnail + * @param string|bool $thumbnail Thumbnail's URL - false if no thumbnail could be found * * @return Bookmark */ -- cgit v1.2.3 From bedbb845eec20363b928b424143787dbe988eefe Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 13 Aug 2020 11:08:13 +0200 Subject: Move all admin controller into a dedicated group Also handle authentication check in a new middleware for the admin group. --- application/front/ShaarliAdminMiddleware.php | 27 ++++++++++++++++++++++ application/front/ShaarliMiddleware.php | 12 +++++++++- .../controller/admin/SessionFilterController.php | 13 +---------- .../controller/admin/ShaarliAdminController.php | 9 -------- .../visitor/PublicSessionFilterController.php | 13 +++++++++++ application/legacy/LegacyController.php | 2 +- 6 files changed, 53 insertions(+), 23 deletions(-) create mode 100644 application/front/ShaarliAdminMiddleware.php (limited to 'application') diff --git a/application/front/ShaarliAdminMiddleware.php b/application/front/ShaarliAdminMiddleware.php new file mode 100644 index 00000000..35ce4a3b --- /dev/null +++ b/application/front/ShaarliAdminMiddleware.php @@ -0,0 +1,27 @@ +initBasePath($request); + + if (true !== $this->container->loginManager->isLoggedIn()) { + $returnUrl = urlencode($this->container->environment['REQUEST_URI']); + + return $response->withRedirect($this->container->basePath . '/login?returnurl=' . $returnUrl); + } + + return parent::__invoke($request, $response, $next); + } +} diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php index 707489d0..a2a3837b 100644 --- a/application/front/ShaarliMiddleware.php +++ b/application/front/ShaarliMiddleware.php @@ -40,7 +40,7 @@ class ShaarliMiddleware */ public function __invoke(Request $request, Response $response, callable $next): Response { - $this->container->basePath = rtrim($request->getUri()->getBasePath(), '/'); + $this->initBasePath($request); try { if (!is_file($this->container->conf->getConfigFileExt()) @@ -125,4 +125,14 @@ class ShaarliMiddleware return true; } + + /** + * Initialize the URL base path if it hasn't been defined yet. + */ + protected function initBasePath(Request $request): void + { + if (null === $this->container->basePath) { + $this->container->basePath = rtrim($request->getUri()->getBasePath(), '/'); + } + } } diff --git a/application/front/controller/admin/SessionFilterController.php b/application/front/controller/admin/SessionFilterController.php index 081c0ba0..d9a7a2e0 100644 --- a/application/front/controller/admin/SessionFilterController.php +++ b/application/front/controller/admin/SessionFilterController.php @@ -17,7 +17,7 @@ use Slim\Http\Response; class SessionFilterController extends ShaarliAdminController { /** - * GET /visibility: allows to display only public or only private bookmarks in linklist + * GET /admin/visibility: allows to display only public or only private bookmarks in linklist */ public function visibility(Request $request, Response $response, array $args): Response { @@ -46,16 +46,5 @@ class SessionFilterController extends ShaarliAdminController return $this->redirectFromReferer($request, $response, ['visibility']); } - /** - * GET /untagged-only: allows to display only bookmarks without any tag - */ - public function untaggedOnly(Request $request, Response $response): Response - { - $this->container->sessionManager->setSessionParameter( - SessionManager::KEY_UNTAGGED_ONLY, - empty($this->container->sessionManager->getSessionParameter(SessionManager::KEY_UNTAGGED_ONLY)) - ); - return $this->redirectFromReferer($request, $response, ['untaggedonly', 'untagged-only']); - } } diff --git a/application/front/controller/admin/ShaarliAdminController.php b/application/front/controller/admin/ShaarliAdminController.php index 3bc5bb6b..3b5939bb 100644 --- a/application/front/controller/admin/ShaarliAdminController.php +++ b/application/front/controller/admin/ShaarliAdminController.php @@ -22,15 +22,6 @@ use Slim\Http\Request; */ abstract class ShaarliAdminController extends ShaarliVisitorController { - public function __construct(ShaarliContainer $container) - { - parent::__construct($container); - - if (true !== $this->container->loginManager->isLoggedIn()) { - throw new UnauthorizedException(); - } - } - /** * Any persistent action to the config or data store must check the XSRF token validity. */ diff --git a/application/front/controller/visitor/PublicSessionFilterController.php b/application/front/controller/visitor/PublicSessionFilterController.php index 35da0c5f..1a66362d 100644 --- a/application/front/controller/visitor/PublicSessionFilterController.php +++ b/application/front/controller/visitor/PublicSessionFilterController.php @@ -30,4 +30,17 @@ class PublicSessionFilterController extends ShaarliVisitorController return $this->redirectFromReferer($request, $response, ['linksperpage'], ['nb']); } + + /** + * GET /untagged-only: allows to display only bookmarks without any tag + */ + public function untaggedOnly(Request $request, Response $response): Response + { + $this->container->sessionManager->setSessionParameter( + SessionManager::KEY_UNTAGGED_ONLY, + empty($this->container->sessionManager->getSessionParameter(SessionManager::KEY_UNTAGGED_ONLY)) + ); + + return $this->redirectFromReferer($request, $response, ['untaggedonly', 'untagged-only']); + } } diff --git a/application/legacy/LegacyController.php b/application/legacy/LegacyController.php index a97b07b1..26465d2c 100644 --- a/application/legacy/LegacyController.php +++ b/application/legacy/LegacyController.php @@ -67,7 +67,7 @@ class LegacyController extends ShaarliVisitorController /** Legacy route: ?do=logout */ protected function logout(Request $request, Response $response): Response { - return $this->redirect($response, '/logout'); + return $this->redirect($response, '/admin/logout'); } /** Legacy route: ?do=picwall */ -- cgit v1.2.3 From 0c6fdbe12bbbb336348666b14b82096f24d5858b Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 21 Aug 2020 10:50:44 +0200 Subject: Move error handling to dedicated controller instead of middleware --- application/container/ContainerBuilder.php | 5 +++ application/front/ShaarliMiddleware.php | 26 +------------ .../front/controller/visitor/ErrorController.php | 45 ++++++++++++++++++++++ 3 files changed, 51 insertions(+), 25 deletions(-) create mode 100644 application/front/controller/visitor/ErrorController.php (limited to 'application') diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index 2e8c1ee3..4a1a6ea7 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -9,6 +9,7 @@ use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Config\ConfigManager; use Shaarli\Feed\FeedBuilder; use Shaarli\Formatter\FormatterFactory; +use Shaarli\Front\Controller\Visitor\ErrorController; use Shaarli\History; use Shaarli\Http\HttpAccess; use Shaarli\Netscape\NetscapeBookmarkUtils; @@ -148,6 +149,10 @@ class ContainerBuilder ); }; + $container['errorHandler'] = function (ShaarliContainer $container): ErrorController { + return new ErrorController($container); + }; + return $container; } } diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php index a2a3837b..c015c0c6 100644 --- a/application/front/ShaarliMiddleware.php +++ b/application/front/ShaarliMiddleware.php @@ -3,7 +3,6 @@ namespace Shaarli\Front; use Shaarli\Container\ShaarliContainer; -use Shaarli\Front\Exception\ShaarliFrontException; use Shaarli\Front\Exception\UnauthorizedException; use Slim\Http\Request; use Slim\Http\Response; @@ -53,35 +52,12 @@ class ShaarliMiddleware $this->checkOpenShaarli($request, $response, $next); return $next($request, $response); - } catch (ShaarliFrontException $e) { - // Possible functional error - $this->container->pageBuilder->reset(); - $this->container->pageBuilder->assign('message', nl2br($e->getMessage())); - - $response = $response->withStatus($e->getCode()); - - return $response->write($this->container->pageBuilder->render('error', $this->container->basePath)); } catch (UnauthorizedException $e) { $returnUrl = urlencode($this->container->environment['REQUEST_URI']); return $response->withRedirect($this->container->basePath . '/login?returnurl=' . $returnUrl); - } catch (\Throwable $e) { - // Unknown error encountered - $this->container->pageBuilder->reset(); - if ($this->container->conf->get('dev.debug', false)) { - $this->container->pageBuilder->assign('message', $e->getMessage()); - $this->container->pageBuilder->assign( - 'stacktrace', - nl2br(get_class($e) .': '. PHP_EOL . $e->getTraceAsString()) - ); - } else { - $this->container->pageBuilder->assign('message', t('An unexpected error occurred.')); - } - - $response = $response->withStatus(500); - - return $response->write($this->container->pageBuilder->render('error', $this->container->basePath)); } + // Other exceptions are handled by ErrorController } /** diff --git a/application/front/controller/visitor/ErrorController.php b/application/front/controller/visitor/ErrorController.php new file mode 100644 index 00000000..10aa84c8 --- /dev/null +++ b/application/front/controller/visitor/ErrorController.php @@ -0,0 +1,45 @@ +container->pageBuilder->reset(); + + if ($throwable instanceof ShaarliFrontException) { + // Functional error + $this->assignView('message', nl2br($throwable->getMessage())); + + $response = $response->withStatus($throwable->getCode()); + } else { + // Internal error (any other Throwable) + if ($this->container->conf->get('dev.debug', false)) { + $this->assignView('message', $throwable->getMessage()); + $this->assignView( + 'stacktrace', + nl2br(get_class($throwable) .': '. PHP_EOL . $throwable->getTraceAsString()) + ); + } else { + $this->assignView('message', t('An unexpected error occurred.')); + } + + $response = $response->withStatus(500); + } + + + return $response->write($this->render('error')); + } +} -- cgit v1.2.3