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 + composer.json | 3 +- doc/md/Translations.md | 2 +- index.php | 35 +- tests/bootstrap.php | 3 +- tests/front/controller/DailyControllerTest.php | 497 --------------------- tests/front/controller/FeedControllerTest.php | 151 ------- .../front/controller/FrontControllerMockHelper.php | 114 ----- tests/front/controller/LoginControllerTest.php | 144 ------ tests/front/controller/LogoutControllerTest.php | 57 --- .../front/controller/OpenSearchControllerTest.php | 48 -- .../front/controller/PictureWallControllerTest.php | 125 ------ .../controller/SessionFilterControllerTest.php | 290 ------------ tests/front/controller/ShaarliControllerTest.php | 225 ---------- tests/front/controller/TagCloudControllerTest.php | 381 ---------------- tests/front/controller/TagControllerTest.php | 242 ---------- .../admin/FrontAdminControllerMockHelper.php | 34 ++ .../controller/admin/LogoutControllerTest.php | 57 +++ .../admin/SessionFilterControllerTest.php | 348 +++++++++++++++ .../controller/visitor/DailyControllerTest.php | 497 +++++++++++++++++++++ .../controller/visitor/FeedControllerTest.php | 151 +++++++ .../visitor/FrontControllerMockHelper.php | 114 +++++ .../controller/visitor/LoginControllerTest.php | 144 ++++++ .../visitor/OpenSearchControllerTest.php | 46 ++ .../visitor/PictureWallControllerTest.php | 125 ++++++ .../visitor/ShaarliPublicControllerTest.php | 239 ++++++++++ .../controller/visitor/TagCloudControllerTest.php | 381 ++++++++++++++++ .../front/controller/visitor/TagControllerTest.php | 241 ++++++++++ tpl/default/page.header.html | 2 +- tpl/vintage/daily.html | 4 +- tpl/vintage/page.header.html | 2 +- 57 files changed, 3386 insertions(+), 3252 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 delete mode 100644 tests/front/controller/DailyControllerTest.php delete mode 100644 tests/front/controller/FeedControllerTest.php delete mode 100644 tests/front/controller/FrontControllerMockHelper.php delete mode 100644 tests/front/controller/LoginControllerTest.php delete mode 100644 tests/front/controller/LogoutControllerTest.php delete mode 100644 tests/front/controller/OpenSearchControllerTest.php delete mode 100644 tests/front/controller/PictureWallControllerTest.php delete mode 100644 tests/front/controller/SessionFilterControllerTest.php delete mode 100644 tests/front/controller/ShaarliControllerTest.php delete mode 100644 tests/front/controller/TagCloudControllerTest.php delete mode 100644 tests/front/controller/TagControllerTest.php create mode 100644 tests/front/controller/admin/FrontAdminControllerMockHelper.php create mode 100644 tests/front/controller/admin/LogoutControllerTest.php create mode 100644 tests/front/controller/admin/SessionFilterControllerTest.php create mode 100644 tests/front/controller/visitor/DailyControllerTest.php create mode 100644 tests/front/controller/visitor/FeedControllerTest.php create mode 100644 tests/front/controller/visitor/FrontControllerMockHelper.php create mode 100644 tests/front/controller/visitor/LoginControllerTest.php create mode 100644 tests/front/controller/visitor/OpenSearchControllerTest.php create mode 100644 tests/front/controller/visitor/PictureWallControllerTest.php create mode 100644 tests/front/controller/visitor/ShaarliPublicControllerTest.php create mode 100644 tests/front/controller/visitor/TagCloudControllerTest.php create mode 100644 tests/front/controller/visitor/TagControllerTest.php 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 @@ -/?do=changepasswd http:///?do=changetag http:///?do=configure http:///?do=tools -http:///?do=daily +http:///daily http:///?post http:///?do=export http:///?do=import diff --git a/index.php b/index.php index a31cbeab..4cd6d5f4 100644 --- a/index.php +++ b/index.php @@ -1498,30 +1498,33 @@ $app->group('/api/v1', function () { })->add('\Shaarli\Api\ApiMiddleware'); $app->group('', function () { - $this->get('/login', '\Shaarli\Front\Controller\LoginController:index')->setName('login'); - $this->get('/logout', '\Shaarli\Front\Controller\LogoutController:index')->setName('logout'); - $this->get('/picture-wall', '\Shaarli\Front\Controller\PictureWallController:index')->setName('picwall'); - $this->get('/tag-cloud', '\Shaarli\Front\Controller\TagCloudController:cloud')->setName('tagcloud'); - $this->get('/tag-list', '\Shaarli\Front\Controller\TagCloudController:list')->setName('taglist'); - $this->get('/daily', '\Shaarli\Front\Controller\DailyController:index')->setName('daily'); - $this->get('/daily-rss', '\Shaarli\Front\Controller\DailyController:rss')->setName('dailyrss'); - $this->get('/feed-atom', '\Shaarli\Front\Controller\FeedController:atom')->setName('feedatom'); - $this->get('/feed-rss', '\Shaarli\Front\Controller\FeedController:rss')->setName('feedrss'); - $this->get('/open-search', '\Shaarli\Front\Controller\OpenSearchController:index')->setName('opensearch'); - - $this->get('/add-tag/{newTag}', '\Shaarli\Front\Controller\TagController:addTag')->setName('add-tag'); - $this->get('/remove-tag/{tag}', '\Shaarli\Front\Controller\TagController:removeTag')->setName('remove-tag'); + /* -- PUBLIC --*/ + $this->get('/login', '\Shaarli\Front\Controller\Visitor\LoginController:index')->setName('login'); + $this->get('/picture-wall', '\Shaarli\Front\Controller\Visitor\PictureWallController:index')->setName('picwall'); + $this->get('/tag-cloud', '\Shaarli\Front\Controller\Visitor\TagCloudController:cloud')->setName('tagcloud'); + $this->get('/tag-list', '\Shaarli\Front\Controller\Visitor\TagCloudController:list')->setName('taglist'); + $this->get('/daily', '\Shaarli\Front\Controller\Visitor\DailyController:index')->setName('daily'); + $this->get('/daily-rss', '\Shaarli\Front\Controller\Visitor\DailyController:rss')->setName('dailyrss'); + $this->get('/feed-atom', '\Shaarli\Front\Controller\Visitor\FeedController:atom')->setName('feedatom'); + $this->get('/feed-rss', '\Shaarli\Front\Controller\Visitor\FeedController:rss')->setName('feedrss'); + $this->get('/open-search', '\Shaarli\Front\Controller\Visitor\OpenSearchController:index')->setName('opensearch'); + + $this->get('/add-tag/{newTag}', '\Shaarli\Front\Controller\Visitor\TagController:addTag')->setName('add-tag'); + $this->get('/remove-tag/{tag}', '\Shaarli\Front\Controller\Visitor\TagController:removeTag')->setName('remove-tag'); + + /* -- LOGGED IN -- */ + $this->get('/logout', '\Shaarli\Front\Controller\Admin\LogoutController:index')->setName('logout'); $this - ->get('/links-per-page', '\Shaarli\Front\Controller\SessionFilterController:linksPerPage') + ->get('/links-per-page', '\Shaarli\Front\Controller\Admin\SessionFilterController:linksPerPage') ->setName('filter-links-per-page') ; $this - ->get('/visibility/{visibility}', '\Shaarli\Front\Controller\SessionFilterController:visibility') + ->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility') ->setName('visibility') ; $this - ->get('/untagged-only', '\Shaarli\Front\Controller\SessionFilterController:untaggedOnly') + ->get('/untagged-only', '\Shaarli\Front\Controller\Admin\SessionFilterController:untaggedOnly') ->setName('untagged-only') ; })->add('\Shaarli\Front\ShaarliMiddleware'); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 6bb345c2..511698ff 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -22,4 +22,5 @@ require_once 'tests/utils/ReferenceLinkDB.php'; require_once 'tests/utils/ReferenceHistory.php'; require_once 'tests/utils/FakeBookmarkService.php'; require_once 'tests/container/ShaarliTestContainer.php'; -require_once 'tests/front/controller/FrontControllerMockHelper.php'; +require_once 'tests/front/controller/visitor/FrontControllerMockHelper.php'; +require_once 'tests/front/controller/admin/FrontAdminControllerMockHelper.php'; diff --git a/tests/front/controller/DailyControllerTest.php b/tests/front/controller/DailyControllerTest.php deleted file mode 100644 index 7ec99030..00000000 --- a/tests/front/controller/DailyControllerTest.php +++ /dev/null @@ -1,497 +0,0 @@ -createContainer(); - - $this->controller = new DailyController($this->container); - DailyController::$DAILY_RSS_NB_DAYS = 2; - } - - public function testValidIndexControllerInvokeDefault(): void - { - $this->createValidContainerMockSet(); - - $currentDay = new \DateTimeImmutable('2020-05-13'); - - $request = $this->createMock(Request::class); - $request->method('getQueryParam')->willReturn($currentDay->format('Ymd')); - $response = new Response(); - - // Save RainTPL assigned variables - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - // Links dataset: 2 links with thumbnails - $this->container->bookmarkService - ->expects(static::once()) - ->method('days') - ->willReturnCallback(function () use ($currentDay): array { - return [ - '20200510', - $currentDay->format('Ymd'), - '20200516', - ]; - }) - ; - $this->container->bookmarkService - ->expects(static::once()) - ->method('filterDay') - ->willReturnCallback(function (): array { - return [ - (new Bookmark()) - ->setId(1) - ->setUrl('http://url.tld') - ->setTitle(static::generateContent(50)) - ->setDescription(static::generateContent(500)) - , - (new Bookmark()) - ->setId(2) - ->setUrl('http://url2.tld') - ->setTitle(static::generateContent(50)) - ->setDescription(static::generateContent(500)) - , - (new Bookmark()) - ->setId(3) - ->setUrl('http://url3.tld') - ->setTitle(static::generateContent(50)) - ->setDescription(static::generateContent(500)) - , - ]; - }) - ; - - // Make sure that PluginManager hook is triggered - $this->container->pluginManager - ->expects(static::at(0)) - ->method('executeHooks') - ->willReturnCallback(function (string $hook, array $data, array $param) use ($currentDay): array { - static::assertSame('render_daily', $hook); - - static::assertArrayHasKey('linksToDisplay', $data); - static::assertCount(3, $data['linksToDisplay']); - static::assertSame(1, $data['linksToDisplay'][0]['id']); - static::assertSame($currentDay->getTimestamp(), $data['day']); - static::assertSame('20200510', $data['previousday']); - static::assertSame('20200516', $data['nextday']); - - static::assertArrayHasKey('loggedin', $param); - - return $data; - }) - ; - - $result = $this->controller->index($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertSame('daily', (string) $result->getBody()); - static::assertSame( - 'Daily - '. format_date($currentDay, false, true) .' - Shaarli', - $assignedVariables['pagetitle'] - ); - static::assertEquals($currentDay, $assignedVariables['dayDate']); - static::assertEquals($currentDay->getTimestamp(), $assignedVariables['day']); - static::assertCount(3, $assignedVariables['linksToDisplay']); - - $link = $assignedVariables['linksToDisplay'][0]; - - static::assertSame(1, $link['id']); - static::assertSame('http://url.tld', $link['url']); - static::assertNotEmpty($link['title']); - static::assertNotEmpty($link['description']); - static::assertNotEmpty($link['formatedDescription']); - - $link = $assignedVariables['linksToDisplay'][1]; - - static::assertSame(2, $link['id']); - static::assertSame('http://url2.tld', $link['url']); - static::assertNotEmpty($link['title']); - static::assertNotEmpty($link['description']); - static::assertNotEmpty($link['formatedDescription']); - - $link = $assignedVariables['linksToDisplay'][2]; - - static::assertSame(3, $link['id']); - static::assertSame('http://url3.tld', $link['url']); - static::assertNotEmpty($link['title']); - static::assertNotEmpty($link['description']); - static::assertNotEmpty($link['formatedDescription']); - - static::assertCount(3, $assignedVariables['cols']); - static::assertCount(1, $assignedVariables['cols'][0]); - static::assertCount(1, $assignedVariables['cols'][1]); - static::assertCount(1, $assignedVariables['cols'][2]); - - $link = $assignedVariables['cols'][0][0]; - - static::assertSame(1, $link['id']); - static::assertSame('http://url.tld', $link['url']); - static::assertNotEmpty($link['title']); - static::assertNotEmpty($link['description']); - static::assertNotEmpty($link['formatedDescription']); - - $link = $assignedVariables['cols'][1][0]; - - static::assertSame(2, $link['id']); - static::assertSame('http://url2.tld', $link['url']); - static::assertNotEmpty($link['title']); - static::assertNotEmpty($link['description']); - static::assertNotEmpty($link['formatedDescription']); - - $link = $assignedVariables['cols'][2][0]; - - static::assertSame(3, $link['id']); - static::assertSame('http://url3.tld', $link['url']); - static::assertNotEmpty($link['title']); - static::assertNotEmpty($link['description']); - static::assertNotEmpty($link['formatedDescription']); - } - - /** - * Daily page - test that everything goes fine with no future or past bookmarks - */ - public function testValidIndexControllerInvokeNoFutureOrPast(): void - { - $this->createValidContainerMockSet(); - - $currentDay = new \DateTimeImmutable('2020-05-13'); - - $request = $this->createMock(Request::class); - $response = new Response(); - - // Save RainTPL assigned variables - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - // Links dataset: 2 links with thumbnails - $this->container->bookmarkService - ->expects(static::once()) - ->method('days') - ->willReturnCallback(function () use ($currentDay): array { - return [ - $currentDay->format($currentDay->format('Ymd')), - ]; - }) - ; - $this->container->bookmarkService - ->expects(static::once()) - ->method('filterDay') - ->willReturnCallback(function (): array { - return [ - (new Bookmark()) - ->setId(1) - ->setUrl('http://url.tld') - ->setTitle(static::generateContent(50)) - ->setDescription(static::generateContent(500)) - , - ]; - }) - ; - - // Make sure that PluginManager hook is triggered - $this->container->pluginManager - ->expects(static::at(0)) - ->method('executeHooks') - ->willReturnCallback(function (string $hook, array $data, array $param) use ($currentDay): array { - static::assertSame('render_daily', $hook); - - static::assertArrayHasKey('linksToDisplay', $data); - static::assertCount(1, $data['linksToDisplay']); - static::assertSame(1, $data['linksToDisplay'][0]['id']); - static::assertSame($currentDay->getTimestamp(), $data['day']); - static::assertEmpty($data['previousday']); - static::assertEmpty($data['nextday']); - - static::assertArrayHasKey('loggedin', $param); - - return $data; - }); - - $result = $this->controller->index($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertSame('daily', (string) $result->getBody()); - static::assertSame( - 'Daily - '. format_date($currentDay, false, true) .' - Shaarli', - $assignedVariables['pagetitle'] - ); - static::assertCount(1, $assignedVariables['linksToDisplay']); - - $link = $assignedVariables['linksToDisplay'][0]; - static::assertSame(1, $link['id']); - } - - /** - * Daily page - test that height adjustment in columns is working - */ - public function testValidIndexControllerInvokeHeightAdjustment(): void - { - $this->createValidContainerMockSet(); - - $currentDay = new \DateTimeImmutable('2020-05-13'); - - $request = $this->createMock(Request::class); - $response = new Response(); - - // Save RainTPL assigned variables - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - // Links dataset: 2 links with thumbnails - $this->container->bookmarkService - ->expects(static::once()) - ->method('days') - ->willReturnCallback(function () use ($currentDay): array { - return [ - $currentDay->format($currentDay->format('Ymd')), - ]; - }) - ; - $this->container->bookmarkService - ->expects(static::once()) - ->method('filterDay') - ->willReturnCallback(function (): array { - return [ - (new Bookmark())->setId(1)->setUrl('http://url.tld')->setTitle('title'), - (new Bookmark()) - ->setId(2) - ->setUrl('http://url.tld') - ->setTitle(static::generateContent(50)) - ->setDescription(static::generateContent(5000)) - , - (new Bookmark())->setId(3)->setUrl('http://url.tld')->setTitle('title'), - (new Bookmark())->setId(4)->setUrl('http://url.tld')->setTitle('title'), - (new Bookmark())->setId(5)->setUrl('http://url.tld')->setTitle('title'), - (new Bookmark())->setId(6)->setUrl('http://url.tld')->setTitle('title'), - (new Bookmark())->setId(7)->setUrl('http://url.tld')->setTitle('title'), - ]; - }) - ; - - // Make sure that PluginManager hook is triggered - $this->container->pluginManager - ->expects(static::at(0)) - ->method('executeHooks') - ->willReturnCallback(function (string $hook, array $data, array $param): array { - return $data; - }) - ; - - $result = $this->controller->index($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertSame('daily', (string) $result->getBody()); - static::assertCount(7, $assignedVariables['linksToDisplay']); - - $columnIds = function (array $column): array { - return array_map(function (array $item): int { return $item['id']; }, $column); - }; - - static::assertSame([1, 4, 6], $columnIds($assignedVariables['cols'][0])); - static::assertSame([2], $columnIds($assignedVariables['cols'][1])); - static::assertSame([3, 5, 7], $columnIds($assignedVariables['cols'][2])); - } - - /** - * Daily page - no bookmark - */ - public function testValidIndexControllerInvokeNoBookmark(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $response = new Response(); - - // Save RainTPL assigned variables - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - // Links dataset: 2 links with thumbnails - $this->container->bookmarkService - ->expects(static::once()) - ->method('days') - ->willReturnCallback(function (): array { - return []; - }) - ; - $this->container->bookmarkService - ->expects(static::once()) - ->method('filterDay') - ->willReturnCallback(function (): array { - return []; - }) - ; - - // Make sure that PluginManager hook is triggered - $this->container->pluginManager - ->expects(static::at(0)) - ->method('executeHooks') - ->willReturnCallback(function (string $hook, array $data, array $param): array { - return $data; - }) - ; - - $result = $this->controller->index($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertSame('daily', (string) $result->getBody()); - static::assertCount(0, $assignedVariables['linksToDisplay']); - static::assertSame('Today', $assignedVariables['dayDesc']); - static::assertEquals((new \DateTime())->setTime(0, 0)->getTimestamp(), $assignedVariables['day']); - static::assertEquals((new \DateTime())->setTime(0, 0), $assignedVariables['dayDate']); - } - - /** - * Daily RSS - default behaviour - */ - public function testValidRssControllerInvokeDefault(): void - { - $this->createValidContainerMockSet(); - - $dates = [ - new \DateTimeImmutable('2020-05-17'), - new \DateTimeImmutable('2020-05-15'), - new \DateTimeImmutable('2020-05-13'), - ]; - - $request = $this->createMock(Request::class); - $response = new Response(); - - $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([ - (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'), - (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), - (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), - (new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'), - ]); - - $this->container->pageCacheManager - ->expects(static::once()) - ->method('getCachePage') - ->willReturnCallback(function (): CachedPage { - $cachedPage = $this->createMock(CachedPage::class); - $cachedPage->expects(static::once())->method('cache')->with('dailyrss'); - - return $cachedPage; - } - ); - - // Save RainTPL assigned variables - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - $result = $this->controller->rss($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]); - static::assertSame('dailyrss', (string) $result->getBody()); - static::assertSame('Shaarli', $assignedVariables['title']); - static::assertSame('http://shaarli', $assignedVariables['index_url']); - static::assertSame('http://shaarli/daily-rss', $assignedVariables['page_url']); - static::assertFalse($assignedVariables['hide_timestamps']); - static::assertCount(2, $assignedVariables['days']); - - $day = $assignedVariables['days'][$dates[0]->format('Ymd')]; - - static::assertEquals($dates[0], $day['date']); - static::assertSame($dates[0]->format(\DateTime::RSS), $day['date_rss']); - static::assertSame(format_date($dates[0], false), $day['date_human']); - static::assertSame('http://shaarli/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']); - static::assertCount(1, $day['links']); - static::assertSame(1, $day['links'][0]['id']); - static::assertSame('http://domain.tld/1', $day['links'][0]['url']); - static::assertEquals($dates[0], $day['links'][0]['created']); - - $day = $assignedVariables['days'][$dates[1]->format('Ymd')]; - - static::assertEquals($dates[1], $day['date']); - static::assertSame($dates[1]->format(\DateTime::RSS), $day['date_rss']); - static::assertSame(format_date($dates[1], false), $day['date_human']); - static::assertSame('http://shaarli/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']); - static::assertCount(2, $day['links']); - - static::assertSame(2, $day['links'][0]['id']); - static::assertSame('http://domain.tld/2', $day['links'][0]['url']); - static::assertEquals($dates[1], $day['links'][0]['created']); - static::assertSame(3, $day['links'][1]['id']); - static::assertSame('http://domain.tld/3', $day['links'][1]['url']); - static::assertEquals($dates[1], $day['links'][1]['created']); - } - - /** - * Daily RSS - trigger cache rendering - */ - public function testValidRssControllerInvokeTriggerCache(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $response = new Response(); - - $this->container->pageCacheManager->method('getCachePage')->willReturnCallback(function (): CachedPage { - $cachedPage = $this->createMock(CachedPage::class); - $cachedPage->method('cachedVersion')->willReturn('this is cache!'); - - return $cachedPage; - }); - - $this->container->bookmarkService->expects(static::never())->method('search'); - - $result = $this->controller->rss($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]); - static::assertSame('this is cache!', (string) $result->getBody()); - } - - /** - * Daily RSS - No bookmark - */ - public function testValidRssControllerInvokeNoBookmark(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $response = new Response(); - - $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([]); - - // Save RainTPL assigned variables - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - $result = $this->controller->rss($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]); - static::assertSame('dailyrss', (string) $result->getBody()); - static::assertSame('Shaarli', $assignedVariables['title']); - static::assertSame('http://shaarli', $assignedVariables['index_url']); - static::assertSame('http://shaarli/daily-rss', $assignedVariables['page_url']); - static::assertFalse($assignedVariables['hide_timestamps']); - static::assertCount(0, $assignedVariables['days']); - } - - protected static function generateContent(int $length): string - { - // bin2hex(random_bytes) generates string twice as long as given parameter - $length = (int) ceil($length / 2); - return bin2hex(random_bytes($length)); - } -} diff --git a/tests/front/controller/FeedControllerTest.php b/tests/front/controller/FeedControllerTest.php deleted file mode 100644 index 7e8657e2..00000000 --- a/tests/front/controller/FeedControllerTest.php +++ /dev/null @@ -1,151 +0,0 @@ -createContainer(); - - $this->container->feedBuilder = $this->createMock(FeedBuilder::class); - - $this->controller = new FeedController($this->container); - } - - /** - * Feed Controller - RSS default behaviour - */ - public function testDefaultRssController(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $response = new Response(); - - $this->container->feedBuilder->expects(static::once())->method('setLocale'); - $this->container->feedBuilder->expects(static::once())->method('setHideDates')->with(false); - $this->container->feedBuilder->expects(static::once())->method('setUsePermalinks')->with(true); - - // Save RainTPL assigned variables - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - $this->container->feedBuilder->method('buildData')->willReturn(['content' => 'data']); - - // Make sure that PluginManager hook is triggered - $this->container->pluginManager - ->expects(static::at(0)) - ->method('executeHooks') - ->willReturnCallback(function (string $hook, array $data, array $param): void { - static::assertSame('render_feed', $hook); - static::assertSame('data', $data['content']); - - static::assertArrayHasKey('loggedin', $param); - static::assertSame('rss', $param['target']); - }) - ; - - $result = $this->controller->rss($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]); - static::assertSame('feed.rss', (string) $result->getBody()); - static::assertSame('data', $assignedVariables['content']); - } - - /** - * Feed Controller - ATOM default behaviour - */ - public function testDefaultAtomController(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $response = new Response(); - - $this->container->feedBuilder->expects(static::once())->method('setLocale'); - $this->container->feedBuilder->expects(static::once())->method('setHideDates')->with(false); - $this->container->feedBuilder->expects(static::once())->method('setUsePermalinks')->with(true); - - // Save RainTPL assigned variables - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - $this->container->feedBuilder->method('buildData')->willReturn(['content' => 'data']); - - // Make sure that PluginManager hook is triggered - $this->container->pluginManager - ->expects(static::at(0)) - ->method('executeHooks') - ->willReturnCallback(function (string $hook, array $data, array $param): void { - static::assertSame('render_feed', $hook); - static::assertSame('data', $data['content']); - - static::assertArrayHasKey('loggedin', $param); - static::assertSame('atom', $param['target']); - }) - ; - - $result = $this->controller->atom($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertStringContainsString('application/atom', $result->getHeader('Content-Type')[0]); - static::assertSame('feed.atom', (string) $result->getBody()); - static::assertSame('data', $assignedVariables['content']); - } - - /** - * Feed Controller - ATOM with parameters - */ - public function testAtomControllerWithParameters(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $request->method('getParams')->willReturn(['parameter' => 'value']); - $response = new Response(); - - // Save RainTPL assigned variables - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - $this->container->feedBuilder - ->method('buildData') - ->with('atom', ['parameter' => 'value']) - ->willReturn(['content' => 'data']) - ; - - // Make sure that PluginManager hook is triggered - $this->container->pluginManager - ->expects(static::at(0)) - ->method('executeHooks') - ->willReturnCallback(function (string $hook, array $data, array $param): void { - static::assertSame('render_feed', $hook); - static::assertSame('data', $data['content']); - - static::assertArrayHasKey('loggedin', $param); - static::assertSame('atom', $param['target']); - }) - ; - - $result = $this->controller->atom($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertStringContainsString('application/atom', $result->getHeader('Content-Type')[0]); - static::assertSame('feed.atom', (string) $result->getBody()); - static::assertSame('data', $assignedVariables['content']); - } -} diff --git a/tests/front/controller/FrontControllerMockHelper.php b/tests/front/controller/FrontControllerMockHelper.php deleted file mode 100644 index b65607e7..00000000 --- a/tests/front/controller/FrontControllerMockHelper.php +++ /dev/null @@ -1,114 +0,0 @@ -container = $this->createMock(ShaarliTestContainer::class); - } - - /** - * Initialize container's services used by tests - */ - protected function createValidContainerMockSet(): void - { - $this->container->loginManager = $this->createMock(LoginManager::class); - - // Config - $this->container->conf = $this->createMock(ConfigManager::class); - $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) { - return $default; - }); - - // PageBuilder - $this->container->pageBuilder = $this->createMock(PageBuilder::class); - $this->container->pageBuilder - ->method('render') - ->willReturnCallback(function (string $template): string { - return $template; - }) - ; - - // Plugin Manager - $this->container->pluginManager = $this->createMock(PluginManager::class); - - // BookmarkService - $this->container->bookmarkService = $this->createMock(BookmarkServiceInterface::class); - - // Formatter - $this->container->formatterFactory = $this->createMock(FormatterFactory::class); - $this->container->formatterFactory - ->method('getFormatter') - ->willReturnCallback(function (): BookmarkFormatter { - return new BookmarkRawFormatter($this->container->conf, true); - }) - ; - - // CacheManager - $this->container->pageCacheManager = $this->createMock(PageCacheManager::class); - - // SessionManager - $this->container->sessionManager = $this->createMock(SessionManager::class); - - // $_SERVER - $this->container->environment = [ - 'SERVER_NAME' => 'shaarli', - 'SERVER_PORT' => '80', - 'REQUEST_URI' => '/daily-rss', - ]; - } - - /** - * Pass a reference of an array which will be populated by `pageBuilder->assign` calls during execution. - * - * @param mixed $variables Array reference to populate. - */ - protected function assignTemplateVars(array &$variables): void - { - $this->container->pageBuilder - ->expects(static::atLeastOnce()) - ->method('assign') - ->willReturnCallback(function ($key, $value) use (&$variables) { - $variables[$key] = $value; - - return $this; - }) - ; - } - - /** - * Force to be used in PHPUnit context. - */ - protected abstract function createMock($originalClassName): MockObject; -} diff --git a/tests/front/controller/LoginControllerTest.php b/tests/front/controller/LoginControllerTest.php deleted file mode 100644 index 21937f3c..00000000 --- a/tests/front/controller/LoginControllerTest.php +++ /dev/null @@ -1,144 +0,0 @@ -createContainer(); - - $this->controller = new LoginController($this->container); - } - - public function testValidControllerInvoke(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $request->expects(static::once())->method('getServerParam')->willReturn('> referer'); - $response = new Response(); - - $assignedVariables = []; - $this->container->pageBuilder - ->method('assign') - ->willReturnCallback(function ($key, $value) use (&$assignedVariables) { - $assignedVariables[$key] = $value; - - return $this; - }) - ; - - $this->container->loginManager->method('canLogin')->willReturn(true); - - $result = $this->controller->index($request, $response); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(200, $result->getStatusCode()); - static::assertSame('loginform', (string) $result->getBody()); - - static::assertSame('> referer', $assignedVariables['returnurl']); - static::assertSame(true, $assignedVariables['remember_user_default']); - static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']); - } - - public function testValidControllerInvokeWithUserName(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $request->expects(static::once())->method('getServerParam')->willReturn('> referer'); - $request->expects(static::exactly(2))->method('getParam')->willReturn('myUser>'); - $response = new Response(); - - $assignedVariables = []; - $this->container->pageBuilder - ->method('assign') - ->willReturnCallback(function ($key, $value) use (&$assignedVariables) { - $assignedVariables[$key] = $value; - - return $this; - }) - ; - - $this->container->loginManager->expects(static::once())->method('canLogin')->willReturn(true); - - $result = $this->controller->index($request, $response); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(200, $result->getStatusCode()); - static::assertSame('loginform', (string) $result->getBody()); - - static::assertSame('myUser>', $assignedVariables['username']); - static::assertSame('> referer', $assignedVariables['returnurl']); - static::assertSame(true, $assignedVariables['remember_user_default']); - static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']); - } - - public function testLoginControllerWhileLoggedIn(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $response = new Response(); - - $this->container->loginManager->expects(static::once())->method('isLoggedIn')->willReturn(true); - - $result = $this->controller->index($request, $response); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['./'], $result->getHeader('Location')); - } - - public function testLoginControllerOpenShaarli(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $response = new Response(); - - $conf = $this->createMock(ConfigManager::class); - $conf->method('get')->willReturnCallback(function (string $parameter, $default) { - if ($parameter === 'security.open_shaarli') { - return true; - } - return $default; - }); - $this->container->conf = $conf; - - $result = $this->controller->index($request, $response); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['./'], $result->getHeader('Location')); - } - - public function testLoginControllerWhileBanned(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $response = new Response(); - - $this->container->loginManager->method('isLoggedIn')->willReturn(false); - $this->container->loginManager->method('canLogin')->willReturn(false); - - $this->expectException(LoginBannedException::class); - - $this->controller->index($request, $response); - } -} diff --git a/tests/front/controller/LogoutControllerTest.php b/tests/front/controller/LogoutControllerTest.php deleted file mode 100644 index 8e01c367..00000000 --- a/tests/front/controller/LogoutControllerTest.php +++ /dev/null @@ -1,57 +0,0 @@ -createContainer(); - - $this->controller = new LogoutController($this->container); - - setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, $cookie = 'hi there'); - } - - public function testValidControllerInvoke(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $response = new Response(); - - $this->container->pageCacheManager->expects(static::once())->method('invalidateCaches'); - - $this->container->sessionManager = $this->createMock(SessionManager::class); - $this->container->sessionManager->expects(static::once())->method('logout'); - - static::assertSame('hi there', $_COOKIE[LoginManager::$STAY_SIGNED_IN_COOKIE]); - - $result = $this->controller->index($request, $response); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertContains('./', $result->getHeader('Location')); - static::assertSame('false', $_COOKIE[LoginManager::$STAY_SIGNED_IN_COOKIE]); - } -} diff --git a/tests/front/controller/OpenSearchControllerTest.php b/tests/front/controller/OpenSearchControllerTest.php deleted file mode 100644 index f3b6f439..00000000 --- a/tests/front/controller/OpenSearchControllerTest.php +++ /dev/null @@ -1,48 +0,0 @@ -createContainer(); - - $this->controller = new OpenSearchController($this->container); - } - - public function testOpenSearchController(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $response = new Response(); - - // Save RainTPL assigned variables - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - $result = $this->controller->index($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertStringContainsString( - 'application/opensearchdescription+xml', - $result->getHeader('Content-Type')[0] - ); - static::assertSame('opensearch', (string) $result->getBody()); - static::assertSame('http://shaarli', $assignedVariables['serverurl']); - } -} diff --git a/tests/front/controller/PictureWallControllerTest.php b/tests/front/controller/PictureWallControllerTest.php deleted file mode 100644 index 8160bb38..00000000 --- a/tests/front/controller/PictureWallControllerTest.php +++ /dev/null @@ -1,125 +0,0 @@ -createContainer(); - - $this->controller = new PictureWallController($this->container); - } - - public function testValidControllerInvokeDefault(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $request->expects(static::once())->method('getQueryParams')->willReturn([]); - $response = new Response(); - - // ConfigManager: thumbnails are enabled - $this->container->conf = $this->createMock(ConfigManager::class); - $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) { - if ($parameter === 'thumbnails.mode') { - return Thumbnailer::MODE_COMMON; - } - - return $default; - }); - - // Save RainTPL assigned variables - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - // Links dataset: 2 links with thumbnails - $this->container->bookmarkService - ->expects(static::once()) - ->method('search') - ->willReturnCallback(function (array $parameters, ?string $visibility): array { - // Visibility is set through the container, not the call - static::assertNull($visibility); - - // No query parameters - if (count($parameters) === 0) { - return [ - (new Bookmark())->setId(1)->setUrl('http://url.tld')->setThumbnail('thumb1'), - (new Bookmark())->setId(2)->setUrl('http://url2.tld'), - (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setThumbnail('thumb2'), - ]; - } - }) - ; - - // Make sure that PluginManager hook is triggered - $this->container->pluginManager - ->expects(static::at(0)) - ->method('executeHooks') - ->willReturnCallback(function (string $hook, array $data, array $param): array { - static::assertSame('render_picwall', $hook); - static::assertArrayHasKey('linksToDisplay', $data); - static::assertCount(2, $data['linksToDisplay']); - static::assertSame(1, $data['linksToDisplay'][0]['id']); - static::assertSame(3, $data['linksToDisplay'][1]['id']); - static::assertArrayHasKey('loggedin', $param); - - return $data; - }); - - $result = $this->controller->index($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertSame('picwall', (string) $result->getBody()); - static::assertSame('Picture wall - Shaarli', $assignedVariables['pagetitle']); - static::assertCount(2, $assignedVariables['linksToDisplay']); - - $link = $assignedVariables['linksToDisplay'][0]; - - static::assertSame(1, $link['id']); - static::assertSame('http://url.tld', $link['url']); - static::assertSame('thumb1', $link['thumbnail']); - - $link = $assignedVariables['linksToDisplay'][1]; - - static::assertSame(3, $link['id']); - static::assertSame('http://url3.tld', $link['url']); - static::assertSame('thumb2', $link['thumbnail']); - } - - public function testControllerWithThumbnailsDisabled(): void - { - $this->expectException(ThumbnailsDisabledException::class); - - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $response = new Response(); - - // ConfigManager: thumbnails are disabled - $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) { - if ($parameter === 'thumbnails.mode') { - return Thumbnailer::MODE_NONE; - } - - return $default; - }); - - $this->controller->index($request, $response); - } -} diff --git a/tests/front/controller/SessionFilterControllerTest.php b/tests/front/controller/SessionFilterControllerTest.php deleted file mode 100644 index f541de03..00000000 --- a/tests/front/controller/SessionFilterControllerTest.php +++ /dev/null @@ -1,290 +0,0 @@ -createContainer(); - - $this->controller = new SessionFilterController($this->container); - } - - /** - * Link per page - Default call with valid parameter and a referer. - */ - public function testLinksPerPage(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc']; - - $request = $this->createMock(Request::class); - $request->method('getParam')->with('nb')->willReturn('8'); - $response = new Response(); - - $this->container->sessionManager - ->expects(static::once()) - ->method('setSessionParameter') - ->with(SessionManager::KEY_LINKS_PER_PAGE, 8) - ; - - $result = $this->controller->linksPerPage($request, $response); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location')); - } - - /** - * Link per page - Invalid value, should use default value (20) - */ - public function testLinksPerPageNotValid(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $request->method('getParam')->with('nb')->willReturn('test'); - $response = new Response(); - - $this->container->sessionManager - ->expects(static::once()) - ->method('setSessionParameter') - ->with(SessionManager::KEY_LINKS_PER_PAGE, 20) - ; - - $result = $this->controller->linksPerPage($request, $response); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['./'], $result->getHeader('location')); - } - - /** - * Visibility - Default call for private filter while logged in without current value - */ - public function testVisibility(): void - { - $this->createValidContainerMockSet(); - - $arg = ['visibility' => 'private']; - - $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc']; - - $this->container->loginManager->method('isLoggedIn')->willReturn(true); - $this->container->sessionManager - ->expects(static::once()) - ->method('setSessionParameter') - ->with(SessionManager::KEY_VISIBILITY, 'private') - ; - - $request = $this->createMock(Request::class); - $response = new Response(); - - $result = $this->controller->visibility($request, $response, $arg); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location')); - } - - /** - * Visibility - Toggle off private visibility - */ - public function testVisibilityToggleOff(): void - { - $this->createValidContainerMockSet(); - - $arg = ['visibility' => 'private']; - - $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc']; - - $this->container->loginManager->method('isLoggedIn')->willReturn(true); - $this->container->sessionManager - ->method('getSessionParameter') - ->with(SessionManager::KEY_VISIBILITY) - ->willReturn('private') - ; - $this->container->sessionManager - ->expects(static::never()) - ->method('setSessionParameter') - ; - $this->container->sessionManager - ->expects(static::once()) - ->method('deleteSessionParameter') - ->with(SessionManager::KEY_VISIBILITY) - ; - - $request = $this->createMock(Request::class); - $response = new Response(); - - $result = $this->controller->visibility($request, $response, $arg); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location')); - } - - /** - * Visibility - Change private to public - */ - public function testVisibilitySwitch(): void - { - $this->createValidContainerMockSet(); - - $arg = ['visibility' => 'private']; - - $this->container->loginManager->method('isLoggedIn')->willReturn(true); - $this->container->sessionManager - ->method('getSessionParameter') - ->with(SessionManager::KEY_VISIBILITY) - ->willReturn('public') - ; - $this->container->sessionManager - ->expects(static::once()) - ->method('setSessionParameter') - ->with(SessionManager::KEY_VISIBILITY, 'private') - ; - - $request = $this->createMock(Request::class); - $response = new Response(); - - $result = $this->controller->visibility($request, $response, $arg); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['./'], $result->getHeader('location')); - } - - /** - * Visibility - With invalid value - should remove any visibility setting - */ - public function testVisibilityInvalidValue(): void - { - $this->createValidContainerMockSet(); - - $arg = ['visibility' => 'test']; - - $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc']; - - $this->container->loginManager->method('isLoggedIn')->willReturn(true); - $this->container->sessionManager - ->expects(static::never()) - ->method('setSessionParameter') - ; - $this->container->sessionManager - ->expects(static::once()) - ->method('deleteSessionParameter') - ->with(SessionManager::KEY_VISIBILITY) - ; - - $request = $this->createMock(Request::class); - $response = new Response(); - - $result = $this->controller->visibility($request, $response, $arg); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location')); - } - - /** - * Visibility - Try to change visibility while logged out - */ - public function testVisibilityLoggedOut(): void - { - $this->createValidContainerMockSet(); - - $arg = ['visibility' => 'test']; - - $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc']; - - $this->container->loginManager->method('isLoggedIn')->willReturn(false); - $this->container->sessionManager - ->expects(static::never()) - ->method('setSessionParameter') - ; - $this->container->sessionManager - ->expects(static::never()) - ->method('deleteSessionParameter') - ->with(SessionManager::KEY_VISIBILITY) - ; - - $request = $this->createMock(Request::class); - $response = new Response(); - - $result = $this->controller->visibility($request, $response, $arg); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location')); - } - - /** - * Untagged only - valid call - */ - public function testUntaggedOnly(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc']; - - $request = $this->createMock(Request::class); - $response = new Response(); - - $this->container->sessionManager - ->expects(static::once()) - ->method('setSessionParameter') - ->with(SessionManager::KEY_UNTAGGED_ONLY, true) - ; - - $result = $this->controller->untaggedOnly($request, $response); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location')); - } - - /** - * Untagged only - toggle off - */ - public function testUntaggedOnlyToggleOff(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc']; - - $request = $this->createMock(Request::class); - $response = new Response(); - - $this->container->sessionManager - ->method('getSessionParameter') - ->with(SessionManager::KEY_UNTAGGED_ONLY) - ->willReturn(true) - ; - $this->container->sessionManager - ->expects(static::once()) - ->method('setSessionParameter') - ->with(SessionManager::KEY_UNTAGGED_ONLY, false) - ; - - $result = $this->controller->untaggedOnly($request, $response); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location')); - } -} diff --git a/tests/front/controller/ShaarliControllerTest.php b/tests/front/controller/ShaarliControllerTest.php deleted file mode 100644 index a6011b49..00000000 --- a/tests/front/controller/ShaarliControllerTest.php +++ /dev/null @@ -1,225 +0,0 @@ -createContainer(); - - $this->controller = new class($this->container) extends ShaarliController - { - public function assignView(string $key, $value): ShaarliController - { - return parent::assignView($key, $value); - } - - public function render(string $template): string - { - return parent::render($template); - } - - public function redirectFromReferer( - Response $response, - array $loopTerms = [], - array $clearParams = [] - ): Response { - return parent::redirectFromReferer($response, $loopTerms, $clearParams); - } - }; - $this->assignedValues = []; - } - - public function testAssignView(): void - { - $this->createValidContainerMockSet(); - - $this->assignTemplateVars($this->assignedValues); - - $self = $this->controller->assignView('variableName', 'variableValue'); - - static::assertInstanceOf(ShaarliController::class, $self); - static::assertSame('variableValue', $this->assignedValues['variableName']); - } - - public function testRender(): void - { - $this->createValidContainerMockSet(); - - $this->assignTemplateVars($this->assignedValues); - - $this->container->bookmarkService - ->method('count') - ->willReturnCallback(function (string $visibility): int { - return $visibility === BookmarkFilter::$PRIVATE ? 5 : 10; - }) - ; - - $this->container->pluginManager - ->method('executeHooks') - ->willReturnCallback(function (string $hook, array &$data, array $params): array { - return $data[$hook] = $params; - }); - $this->container->pluginManager->method('getErrors')->willReturn(['error']); - - $this->container->loginManager->method('isLoggedIn')->willReturn(true); - - $render = $this->controller->render('templateName'); - - static::assertSame('templateName', $render); - - static::assertSame(10, $this->assignedValues['linkcount']); - static::assertSame(5, $this->assignedValues['privateLinkcount']); - static::assertSame(['error'], $this->assignedValues['plugin_errors']); - - static::assertSame('templateName', $this->assignedValues['plugins_includes']['render_includes']['target']); - static::assertTrue($this->assignedValues['plugins_includes']['render_includes']['loggedin']); - static::assertSame('templateName', $this->assignedValues['plugins_header']['render_header']['target']); - static::assertTrue($this->assignedValues['plugins_header']['render_header']['loggedin']); - static::assertSame('templateName', $this->assignedValues['plugins_footer']['render_footer']['target']); - static::assertTrue($this->assignedValues['plugins_footer']['render_footer']['loggedin']); - } - - /** - * Test redirectFromReferer() - Default behaviour - */ - public function testRedirectFromRefererDefault(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2'; - - $response = new Response(); - - $result = $this->controller->redirectFromReferer($response); - - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location')); - } - - /** - * Test redirectFromReferer() - With a loop term not matched in the referer - */ - public function testRedirectFromRefererWithUnmatchedLoopTerm(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2'; - - $response = new Response(); - - $result = $this->controller->redirectFromReferer($response, ['nope']); - - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location')); - } - - /** - * Test redirectFromReferer() - With a loop term matching the referer in its path -> redirect to default - */ - public function testRedirectFromRefererWithMatchingLoopTermInPath(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2'; - - $response = new Response(); - - $result = $this->controller->redirectFromReferer($response, ['nope', 'controller']); - - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['./'], $result->getHeader('location')); - } - - /** - * Test redirectFromReferer() - With a loop term matching the referer in its query parameters -> redirect to default - */ - public function testRedirectFromRefererWithMatchingLoopTermInQueryParam(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2'; - - $response = new Response(); - - $result = $this->controller->redirectFromReferer($response, ['nope', 'other']); - - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['./'], $result->getHeader('location')); - } - - /** - * Test redirectFromReferer() - With a loop term matching the referer in its query value - * -> we do not block redirection for query parameter values. - */ - public function testRedirectFromRefererWithMatchingLoopTermInQueryValue(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2'; - - $response = new Response(); - - $result = $this->controller->redirectFromReferer($response, ['nope', 'param']); - - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location')); - } - - /** - * Test redirectFromReferer() - With a loop term matching the referer in its domain name - * -> we do not block redirection for shaarli's hosts - */ - public function testRedirectFromRefererWithLoopTermInDomain(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2'; - - $response = new Response(); - - $result = $this->controller->redirectFromReferer($response, ['shaarli']); - - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location')); - } - - /** - * Test redirectFromReferer() - With a loop term matching a query parameter AND clear this query param - * -> the param should be cleared before checking if it matches the redir loop terms - */ - public function testRedirectFromRefererWithMatchingClearedParam(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2'; - - $response = new Response(); - - $result = $this->controller->redirectFromReferer($response, ['query'], ['query']); - - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/subfolder/controller?other=2'], $result->getHeader('location')); - } -} diff --git a/tests/front/controller/TagCloudControllerTest.php b/tests/front/controller/TagCloudControllerTest.php deleted file mode 100644 index 8c27900d..00000000 --- a/tests/front/controller/TagCloudControllerTest.php +++ /dev/null @@ -1,381 +0,0 @@ -createContainer(); - - $this->controller = new TagCloudController($this->container); - } - - /** - * Tag Cloud - default parameters - */ - public function testValidCloudControllerInvokeDefault(): void - { - $this->createValidContainerMockSet(); - - $allTags = [ - 'ghi' => 1, - 'abc' => 3, - 'def' => 12, - ]; - $expectedOrder = ['abc', 'def', 'ghi']; - - $request = $this->createMock(Request::class); - $response = new Response(); - - // Save RainTPL assigned variables - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - $this->container->bookmarkService - ->expects(static::once()) - ->method('bookmarksCountPerTag') - ->with([], null) - ->willReturnCallback(function () use ($allTags): array { - return $allTags; - }) - ; - - // Make sure that PluginManager hook is triggered - $this->container->pluginManager - ->expects(static::at(0)) - ->method('executeHooks') - ->willReturnCallback(function (string $hook, array $data, array $param): array { - static::assertSame('render_tagcloud', $hook); - static::assertSame('', $data['search_tags']); - static::assertCount(3, $data['tags']); - - static::assertArrayHasKey('loggedin', $param); - - return $data; - }) - ; - - $result = $this->controller->cloud($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertSame('tag.cloud', (string) $result->getBody()); - static::assertSame('Tag cloud - Shaarli', $assignedVariables['pagetitle']); - - static::assertSame('', $assignedVariables['search_tags']); - static::assertCount(3, $assignedVariables['tags']); - static::assertSame($expectedOrder, array_keys($assignedVariables['tags'])); - - foreach ($allTags as $tag => $count) { - static::assertArrayHasKey($tag, $assignedVariables['tags']); - static::assertSame($count, $assignedVariables['tags'][$tag]['count']); - static::assertGreaterThan(0, $assignedVariables['tags'][$tag]['size']); - static::assertLessThan(5, $assignedVariables['tags'][$tag]['size']); - } - } - - /** - * Tag Cloud - Additional parameters: - * - logged in - * - visibility private - * - search tags: `ghi` and `def` (note that filtered tags are not displayed anymore) - */ - public function testValidCloudControllerInvokeWithParameters(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $request - ->method('getQueryParam') - ->with() - ->willReturnCallback(function (string $key): ?string { - if ('searchtags' === $key) { - return 'ghi def'; - } - - return null; - }) - ; - $response = new Response(); - - // Save RainTPL assigned variables - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - $this->container->loginManager->method('isLoggedin')->willReturn(true); - $this->container->sessionManager->expects(static::once())->method('getSessionParameter')->willReturn('private'); - - $this->container->bookmarkService - ->expects(static::once()) - ->method('bookmarksCountPerTag') - ->with(['ghi', 'def'], BookmarkFilter::$PRIVATE) - ->willReturnCallback(function (): array { - return ['abc' => 3]; - }) - ; - - // Make sure that PluginManager hook is triggered - $this->container->pluginManager - ->expects(static::at(0)) - ->method('executeHooks') - ->willReturnCallback(function (string $hook, array $data, array $param): array { - static::assertSame('render_tagcloud', $hook); - static::assertSame('ghi def', $data['search_tags']); - static::assertCount(1, $data['tags']); - - static::assertArrayHasKey('loggedin', $param); - - return $data; - }) - ; - - $result = $this->controller->cloud($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertSame('tag.cloud', (string) $result->getBody()); - static::assertSame('ghi def - Tag cloud - Shaarli', $assignedVariables['pagetitle']); - - static::assertSame('ghi def', $assignedVariables['search_tags']); - static::assertCount(1, $assignedVariables['tags']); - - static::assertArrayHasKey('abc', $assignedVariables['tags']); - static::assertSame(3, $assignedVariables['tags']['abc']['count']); - static::assertGreaterThan(0, $assignedVariables['tags']['abc']['size']); - static::assertLessThan(5, $assignedVariables['tags']['abc']['size']); - } - - /** - * Tag Cloud - empty - */ - public function testEmptyCloud(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $response = new Response(); - - // Save RainTPL assigned variables - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - $this->container->bookmarkService - ->expects(static::once()) - ->method('bookmarksCountPerTag') - ->with([], null) - ->willReturnCallback(function (array $parameters, ?string $visibility): array { - return []; - }) - ; - - // Make sure that PluginManager hook is triggered - $this->container->pluginManager - ->expects(static::at(0)) - ->method('executeHooks') - ->willReturnCallback(function (string $hook, array $data, array $param): array { - static::assertSame('render_tagcloud', $hook); - static::assertSame('', $data['search_tags']); - static::assertCount(0, $data['tags']); - - static::assertArrayHasKey('loggedin', $param); - - return $data; - }) - ; - - $result = $this->controller->cloud($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertSame('tag.cloud', (string) $result->getBody()); - static::assertSame('Tag cloud - Shaarli', $assignedVariables['pagetitle']); - - static::assertSame('', $assignedVariables['search_tags']); - static::assertCount(0, $assignedVariables['tags']); - } - - /** - * Tag List - Default sort is by usage DESC - */ - public function testValidListControllerInvokeDefault(): void - { - $this->createValidContainerMockSet(); - - $allTags = [ - 'def' => 12, - 'abc' => 3, - 'ghi' => 1, - ]; - - $request = $this->createMock(Request::class); - $response = new Response(); - - // Save RainTPL assigned variables - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - $this->container->bookmarkService - ->expects(static::once()) - ->method('bookmarksCountPerTag') - ->with([], null) - ->willReturnCallback(function () use ($allTags): array { - return $allTags; - }) - ; - - // Make sure that PluginManager hook is triggered - $this->container->pluginManager - ->expects(static::at(0)) - ->method('executeHooks') - ->willReturnCallback(function (string $hook, array $data, array $param): array { - static::assertSame('render_taglist', $hook); - static::assertSame('', $data['search_tags']); - static::assertCount(3, $data['tags']); - - static::assertArrayHasKey('loggedin', $param); - - return $data; - }) - ; - - $result = $this->controller->list($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertSame('tag.list', (string) $result->getBody()); - static::assertSame('Tag list - Shaarli', $assignedVariables['pagetitle']); - - static::assertSame('', $assignedVariables['search_tags']); - static::assertCount(3, $assignedVariables['tags']); - - foreach ($allTags as $tag => $count) { - static::assertSame($count, $assignedVariables['tags'][$tag]); - } - } - - /** - * Tag List - Additional parameters: - * - logged in - * - visibility private - * - search tags: `ghi` and `def` (note that filtered tags are not displayed anymore) - * - sort alphabetically - */ - public function testValidListControllerInvokeWithParameters(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $request - ->method('getQueryParam') - ->with() - ->willReturnCallback(function (string $key): ?string { - if ('searchtags' === $key) { - return 'ghi def'; - } elseif ('sort' === $key) { - return 'alpha'; - } - - return null; - }) - ; - $response = new Response(); - - // Save RainTPL assigned variables - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - $this->container->loginManager->method('isLoggedin')->willReturn(true); - $this->container->sessionManager->expects(static::once())->method('getSessionParameter')->willReturn('private'); - - $this->container->bookmarkService - ->expects(static::once()) - ->method('bookmarksCountPerTag') - ->with(['ghi', 'def'], BookmarkFilter::$PRIVATE) - ->willReturnCallback(function (): array { - return ['abc' => 3]; - }) - ; - - // Make sure that PluginManager hook is triggered - $this->container->pluginManager - ->expects(static::at(0)) - ->method('executeHooks') - ->willReturnCallback(function (string $hook, array $data, array $param): array { - static::assertSame('render_taglist', $hook); - static::assertSame('ghi def', $data['search_tags']); - static::assertCount(1, $data['tags']); - - static::assertArrayHasKey('loggedin', $param); - - return $data; - }) - ; - - $result = $this->controller->list($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertSame('tag.list', (string) $result->getBody()); - static::assertSame('ghi def - Tag list - Shaarli', $assignedVariables['pagetitle']); - - static::assertSame('ghi def', $assignedVariables['search_tags']); - static::assertCount(1, $assignedVariables['tags']); - static::assertSame(3, $assignedVariables['tags']['abc']); - } - - /** - * Tag List - empty - */ - public function testEmptyList(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $response = new Response(); - - // Save RainTPL assigned variables - $assignedVariables = []; - $this->assignTemplateVars($assignedVariables); - - $this->container->bookmarkService - ->expects(static::once()) - ->method('bookmarksCountPerTag') - ->with([], null) - ->willReturnCallback(function (array $parameters, ?string $visibility): array { - return []; - }) - ; - - // Make sure that PluginManager hook is triggered - $this->container->pluginManager - ->expects(static::at(0)) - ->method('executeHooks') - ->willReturnCallback(function (string $hook, array $data, array $param): array { - static::assertSame('render_taglist', $hook); - static::assertSame('', $data['search_tags']); - static::assertCount(0, $data['tags']); - - static::assertArrayHasKey('loggedin', $param); - - return $data; - }) - ; - - $result = $this->controller->list($request, $response); - - static::assertSame(200, $result->getStatusCode()); - static::assertSame('tag.list', (string) $result->getBody()); - static::assertSame('Tag list - Shaarli', $assignedVariables['pagetitle']); - - static::assertSame('', $assignedVariables['search_tags']); - static::assertCount(0, $assignedVariables['tags']); - } -} diff --git a/tests/front/controller/TagControllerTest.php b/tests/front/controller/TagControllerTest.php deleted file mode 100644 index 2184cb11..00000000 --- a/tests/front/controller/TagControllerTest.php +++ /dev/null @@ -1,242 +0,0 @@ -createContainer(); - - $this->controller = new TagController($this->container); - } - - public function testAddTagWithReferer(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/']; - - $request = $this->createMock(Request::class); - $response = new Response(); - - $tags = ['newTag' => 'abc']; - - $result = $this->controller->addTag($request, $response, $tags); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/controller/?searchtags=abc'], $result->getHeader('location')); - } - - public function testAddTagWithRefererAndExistingSearch(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def']; - - $request = $this->createMock(Request::class); - $response = new Response(); - - $tags = ['newTag' => 'abc']; - - $result = $this->controller->addTag($request, $response, $tags); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location')); - } - - public function testAddTagWithoutRefererAndExistingSearch(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $response = new Response(); - - $tags = ['newTag' => 'abc']; - - $result = $this->controller->addTag($request, $response, $tags); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['./?searchtags=abc'], $result->getHeader('location')); - } - - public function testAddTagRemoveLegacyQueryParam(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def&addtag=abc']; - - $request = $this->createMock(Request::class); - $response = new Response(); - - $tags = ['newTag' => 'abc']; - - $result = $this->controller->addTag($request, $response, $tags); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location')); - } - - public function testAddTagResetPagination(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def&page=12']; - - $request = $this->createMock(Request::class); - $response = new Response(); - - $tags = ['newTag' => 'abc']; - - $result = $this->controller->addTag($request, $response, $tags); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location')); - } - - public function testAddTagWithRefererAndEmptySearch(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=']; - - $request = $this->createMock(Request::class); - $response = new Response(); - - $tags = ['newTag' => 'abc']; - - $result = $this->controller->addTag($request, $response, $tags); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/controller/?searchtags=abc'], $result->getHeader('location')); - } - - public function testAddTagWithoutNewTagWithReferer(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def']; - - $request = $this->createMock(Request::class); - $response = new Response(); - - $result = $this->controller->addTag($request, $response, []); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/controller/?searchtags=def'], $result->getHeader('location')); - } - - public function testAddTagWithoutNewTagWithoutReferer(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $response = new Response(); - - $result = $this->controller->addTag($request, $response, []); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['./'], $result->getHeader('location')); - } - - public function testRemoveTagWithoutMatchingTag(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def']; - - $request = $this->createMock(Request::class); - $response = new Response(); - - $tags = ['tag' => 'abc']; - - $result = $this->controller->removeTag($request, $response, $tags); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/controller/?searchtags=def'], $result->getHeader('location')); - } - - public function testRemoveTagWithoutTagsearch(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/']; - - $request = $this->createMock(Request::class); - $response = new Response(); - - $tags = ['tag' => 'abc']; - - $result = $this->controller->removeTag($request, $response, $tags); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/controller/'], $result->getHeader('location')); - } - - public function testRemoveTagWithoutReferer(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $response = new Response(); - - $tags = ['tag' => 'abc']; - - $result = $this->controller->removeTag($request, $response, $tags); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['./'], $result->getHeader('location')); - } - - public function testRemoveTagWithoutTag(): void - { - $this->createValidContainerMockSet(); - - $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtag=abc']; - - $request = $this->createMock(Request::class); - $response = new Response(); - - $result = $this->controller->removeTag($request, $response, []); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['/controller/?searchtag=abc'], $result->getHeader('location')); - } - - public function testRemoveTagWithoutTagWithoutReferer(): void - { - $this->createValidContainerMockSet(); - - $request = $this->createMock(Request::class); - $response = new Response(); - - $result = $this->controller->removeTag($request, $response, []); - - static::assertInstanceOf(Response::class, $result); - static::assertSame(302, $result->getStatusCode()); - static::assertSame(['./'], $result->getHeader('location')); - } -} diff --git a/tests/front/controller/admin/FrontAdminControllerMockHelper.php b/tests/front/controller/admin/FrontAdminControllerMockHelper.php new file mode 100644 index 00000000..94581c09 --- /dev/null +++ b/tests/front/controller/admin/FrontAdminControllerMockHelper.php @@ -0,0 +1,34 @@ +parentCreateContainer(); + + $this->container->loginManager = $this->createMock(LoginManager::class); + $this->container->loginManager->method('isLoggedIn')->willReturn(true); + } +} diff --git a/tests/front/controller/admin/LogoutControllerTest.php b/tests/front/controller/admin/LogoutControllerTest.php new file mode 100644 index 00000000..239e39b2 --- /dev/null +++ b/tests/front/controller/admin/LogoutControllerTest.php @@ -0,0 +1,57 @@ +createContainer(); + + $this->controller = new LogoutController($this->container); + + setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, $cookie = 'hi there'); + } + + public function testValidControllerInvoke(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $response = new Response(); + + $this->container->pageCacheManager->expects(static::once())->method('invalidateCaches'); + + $this->container->sessionManager = $this->createMock(SessionManager::class); + $this->container->sessionManager->expects(static::once())->method('logout'); + + static::assertSame('hi there', $_COOKIE[LoginManager::$STAY_SIGNED_IN_COOKIE]); + + $result = $this->controller->index($request, $response); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertContains('./', $result->getHeader('Location')); + static::assertSame('false', $_COOKIE[LoginManager::$STAY_SIGNED_IN_COOKIE]); + } +} diff --git a/tests/front/controller/admin/SessionFilterControllerTest.php b/tests/front/controller/admin/SessionFilterControllerTest.php new file mode 100644 index 00000000..f50f2fc2 --- /dev/null +++ b/tests/front/controller/admin/SessionFilterControllerTest.php @@ -0,0 +1,348 @@ +createContainer(); + + $this->controller = new SessionFilterController($this->container); + } + + /** + * Link per page - Default call with valid parameter and a referer. + */ + public function testLinksPerPage(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc']; + + $request = $this->createMock(Request::class); + $request->method('getUri')->willReturnCallback(function (): Uri { + $uri = $this->createMock(Uri::class); + $uri->method('getBasePath')->willReturn('/subfolder'); + + return $uri; + }); + $request->method('getParam')->with('nb')->willReturn('8'); + $response = new Response(); + + $this->container->sessionManager + ->expects(static::once()) + ->method('setSessionParameter') + ->with(SessionManager::KEY_LINKS_PER_PAGE, 8) + ; + + $result = $this->controller->linksPerPage($request, $response); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location')); + } + + /** + * Link per page - Invalid value, should use default value (20) + */ + public function testLinksPerPageNotValid(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $request->method('getUri')->willReturnCallback(function (): Uri { + $uri = $this->createMock(Uri::class); + $uri->method('getBasePath')->willReturn('/subfolder'); + + return $uri; + }); + $request->method('getParam')->with('nb')->willReturn('test'); + $response = new Response(); + + $this->container->sessionManager + ->expects(static::once()) + ->method('setSessionParameter') + ->with(SessionManager::KEY_LINKS_PER_PAGE, 20) + ; + + $result = $this->controller->linksPerPage($request, $response); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/subfolder'], $result->getHeader('location')); + } + + /** + * Visibility - Default call for private filter while logged in without current value + */ + public function testVisibility(): void + { + $this->createValidContainerMockSet(); + + $arg = ['visibility' => 'private']; + + $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc']; + + $this->container->loginManager->method('isLoggedIn')->willReturn(true); + $this->container->sessionManager + ->expects(static::once()) + ->method('setSessionParameter') + ->with(SessionManager::KEY_VISIBILITY, 'private') + ; + + $request = $this->createMock(Request::class); + $request->method('getUri')->willReturnCallback(function (): Uri { + $uri = $this->createMock(Uri::class); + $uri->method('getBasePath')->willReturn('/subfolder'); + + return $uri; + }); + $response = new Response(); + + $result = $this->controller->visibility($request, $response, $arg); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location')); + } + + /** + * Visibility - Toggle off private visibility + */ + public function testVisibilityToggleOff(): void + { + $this->createValidContainerMockSet(); + + $arg = ['visibility' => 'private']; + + $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc']; + + $this->container->loginManager->method('isLoggedIn')->willReturn(true); + $this->container->sessionManager + ->method('getSessionParameter') + ->with(SessionManager::KEY_VISIBILITY) + ->willReturn('private') + ; + $this->container->sessionManager + ->expects(static::never()) + ->method('setSessionParameter') + ; + $this->container->sessionManager + ->expects(static::once()) + ->method('deleteSessionParameter') + ->with(SessionManager::KEY_VISIBILITY) + ; + + $request = $this->createMock(Request::class); + $request->method('getUri')->willReturnCallback(function (): Uri { + $uri = $this->createMock(Uri::class); + $uri->method('getBasePath')->willReturn('/subfolder'); + + return $uri; + }); + $response = new Response(); + + $result = $this->controller->visibility($request, $response, $arg); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location')); + } + + /** + * Visibility - Change private to public + */ + public function testVisibilitySwitch(): void + { + $this->createValidContainerMockSet(); + + $arg = ['visibility' => 'private']; + + $this->container->loginManager->method('isLoggedIn')->willReturn(true); + $this->container->sessionManager + ->method('getSessionParameter') + ->with(SessionManager::KEY_VISIBILITY) + ->willReturn('public') + ; + $this->container->sessionManager + ->expects(static::once()) + ->method('setSessionParameter') + ->with(SessionManager::KEY_VISIBILITY, 'private') + ; + + $request = $this->createMock(Request::class); + $request->method('getUri')->willReturnCallback(function (): Uri { + $uri = $this->createMock(Uri::class); + $uri->method('getBasePath')->willReturn('/subfolder'); + + return $uri; + }); + $response = new Response(); + + $result = $this->controller->visibility($request, $response, $arg); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/subfolder'], $result->getHeader('location')); + } + + /** + * Visibility - With invalid value - should remove any visibility setting + */ + public function testVisibilityInvalidValue(): void + { + $this->createValidContainerMockSet(); + + $arg = ['visibility' => 'test']; + + $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc']; + + $this->container->loginManager->method('isLoggedIn')->willReturn(true); + $this->container->sessionManager + ->expects(static::never()) + ->method('setSessionParameter') + ; + $this->container->sessionManager + ->expects(static::once()) + ->method('deleteSessionParameter') + ->with(SessionManager::KEY_VISIBILITY) + ; + + $request = $this->createMock(Request::class); + $request->method('getUri')->willReturnCallback(function (): Uri { + $uri = $this->createMock(Uri::class); + $uri->method('getBasePath')->willReturn('/subfolder'); + + return $uri; + }); + $response = new Response(); + + $result = $this->controller->visibility($request, $response, $arg); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location')); + } + + /** + * Visibility - Try to change visibility while logged out + */ + public function testVisibilityLoggedOut(): void + { + $this->createValidContainerMockSet(); + + $arg = ['visibility' => 'test']; + + $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc']; + + $this->container->loginManager = $this->createMock(LoginManager::class); + $this->container->loginManager->method('isLoggedIn')->willReturn(false); + $this->container->sessionManager + ->expects(static::never()) + ->method('setSessionParameter') + ; + $this->container->sessionManager + ->expects(static::never()) + ->method('deleteSessionParameter') + ->with(SessionManager::KEY_VISIBILITY) + ; + + $request = $this->createMock(Request::class); + $request->method('getUri')->willReturnCallback(function (): Uri { + $uri = $this->createMock(Uri::class); + $uri->method('getBasePath')->willReturn('/subfolder'); + + return $uri; + }); + $response = new Response(); + + $result = $this->controller->visibility($request, $response, $arg); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location')); + } + + /** + * Untagged only - valid call + */ + public function testUntaggedOnly(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc']; + + $request = $this->createMock(Request::class); + $request->method('getUri')->willReturnCallback(function (): Uri { + $uri = $this->createMock(Uri::class); + $uri->method('getBasePath')->willReturn('/subfolder'); + + return $uri; + }); + $response = new Response(); + + $this->container->sessionManager + ->expects(static::once()) + ->method('setSessionParameter') + ->with(SessionManager::KEY_UNTAGGED_ONLY, true) + ; + + $result = $this->controller->untaggedOnly($request, $response); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location')); + } + + /** + * Untagged only - toggle off + */ + public function testUntaggedOnlyToggleOff(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc']; + + $request = $this->createMock(Request::class); + $request->method('getUri')->willReturnCallback(function (): Uri { + $uri = $this->createMock(Uri::class); + $uri->method('getBasePath')->willReturn('/subfolder'); + + return $uri; + }); + + $response = new Response(); + + $this->container->sessionManager + ->method('getSessionParameter') + ->with(SessionManager::KEY_UNTAGGED_ONLY) + ->willReturn(true) + ; + $this->container->sessionManager + ->expects(static::once()) + ->method('setSessionParameter') + ->with(SessionManager::KEY_UNTAGGED_ONLY, false) + ; + + $result = $this->controller->untaggedOnly($request, $response); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location')); + } +} diff --git a/tests/front/controller/visitor/DailyControllerTest.php b/tests/front/controller/visitor/DailyControllerTest.php new file mode 100644 index 00000000..6ff769fc --- /dev/null +++ b/tests/front/controller/visitor/DailyControllerTest.php @@ -0,0 +1,497 @@ +createContainer(); + + $this->controller = new DailyController($this->container); + DailyController::$DAILY_RSS_NB_DAYS = 2; + } + + public function testValidIndexControllerInvokeDefault(): void + { + $this->createValidContainerMockSet(); + + $currentDay = new \DateTimeImmutable('2020-05-13'); + + $request = $this->createMock(Request::class); + $request->method('getQueryParam')->willReturn($currentDay->format('Ymd')); + $response = new Response(); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + // Links dataset: 2 links with thumbnails + $this->container->bookmarkService + ->expects(static::once()) + ->method('days') + ->willReturnCallback(function () use ($currentDay): array { + return [ + '20200510', + $currentDay->format('Ymd'), + '20200516', + ]; + }) + ; + $this->container->bookmarkService + ->expects(static::once()) + ->method('filterDay') + ->willReturnCallback(function (): array { + return [ + (new Bookmark()) + ->setId(1) + ->setUrl('http://url.tld') + ->setTitle(static::generateContent(50)) + ->setDescription(static::generateContent(500)) + , + (new Bookmark()) + ->setId(2) + ->setUrl('http://url2.tld') + ->setTitle(static::generateContent(50)) + ->setDescription(static::generateContent(500)) + , + (new Bookmark()) + ->setId(3) + ->setUrl('http://url3.tld') + ->setTitle(static::generateContent(50)) + ->setDescription(static::generateContent(500)) + , + ]; + }) + ; + + // Make sure that PluginManager hook is triggered + $this->container->pluginManager + ->expects(static::at(0)) + ->method('executeHooks') + ->willReturnCallback(function (string $hook, array $data, array $param) use ($currentDay): array { + static::assertSame('render_daily', $hook); + + static::assertArrayHasKey('linksToDisplay', $data); + static::assertCount(3, $data['linksToDisplay']); + static::assertSame(1, $data['linksToDisplay'][0]['id']); + static::assertSame($currentDay->getTimestamp(), $data['day']); + static::assertSame('20200510', $data['previousday']); + static::assertSame('20200516', $data['nextday']); + + static::assertArrayHasKey('loggedin', $param); + + return $data; + }) + ; + + $result = $this->controller->index($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('daily', (string) $result->getBody()); + static::assertSame( + 'Daily - '. format_date($currentDay, false, true) .' - Shaarli', + $assignedVariables['pagetitle'] + ); + static::assertEquals($currentDay, $assignedVariables['dayDate']); + static::assertEquals($currentDay->getTimestamp(), $assignedVariables['day']); + static::assertCount(3, $assignedVariables['linksToDisplay']); + + $link = $assignedVariables['linksToDisplay'][0]; + + static::assertSame(1, $link['id']); + static::assertSame('http://url.tld', $link['url']); + static::assertNotEmpty($link['title']); + static::assertNotEmpty($link['description']); + static::assertNotEmpty($link['formatedDescription']); + + $link = $assignedVariables['linksToDisplay'][1]; + + static::assertSame(2, $link['id']); + static::assertSame('http://url2.tld', $link['url']); + static::assertNotEmpty($link['title']); + static::assertNotEmpty($link['description']); + static::assertNotEmpty($link['formatedDescription']); + + $link = $assignedVariables['linksToDisplay'][2]; + + static::assertSame(3, $link['id']); + static::assertSame('http://url3.tld', $link['url']); + static::assertNotEmpty($link['title']); + static::assertNotEmpty($link['description']); + static::assertNotEmpty($link['formatedDescription']); + + static::assertCount(3, $assignedVariables['cols']); + static::assertCount(1, $assignedVariables['cols'][0]); + static::assertCount(1, $assignedVariables['cols'][1]); + static::assertCount(1, $assignedVariables['cols'][2]); + + $link = $assignedVariables['cols'][0][0]; + + static::assertSame(1, $link['id']); + static::assertSame('http://url.tld', $link['url']); + static::assertNotEmpty($link['title']); + static::assertNotEmpty($link['description']); + static::assertNotEmpty($link['formatedDescription']); + + $link = $assignedVariables['cols'][1][0]; + + static::assertSame(2, $link['id']); + static::assertSame('http://url2.tld', $link['url']); + static::assertNotEmpty($link['title']); + static::assertNotEmpty($link['description']); + static::assertNotEmpty($link['formatedDescription']); + + $link = $assignedVariables['cols'][2][0]; + + static::assertSame(3, $link['id']); + static::assertSame('http://url3.tld', $link['url']); + static::assertNotEmpty($link['title']); + static::assertNotEmpty($link['description']); + static::assertNotEmpty($link['formatedDescription']); + } + + /** + * Daily page - test that everything goes fine with no future or past bookmarks + */ + public function testValidIndexControllerInvokeNoFutureOrPast(): void + { + $this->createValidContainerMockSet(); + + $currentDay = new \DateTimeImmutable('2020-05-13'); + + $request = $this->createMock(Request::class); + $response = new Response(); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + // Links dataset: 2 links with thumbnails + $this->container->bookmarkService + ->expects(static::once()) + ->method('days') + ->willReturnCallback(function () use ($currentDay): array { + return [ + $currentDay->format($currentDay->format('Ymd')), + ]; + }) + ; + $this->container->bookmarkService + ->expects(static::once()) + ->method('filterDay') + ->willReturnCallback(function (): array { + return [ + (new Bookmark()) + ->setId(1) + ->setUrl('http://url.tld') + ->setTitle(static::generateContent(50)) + ->setDescription(static::generateContent(500)) + , + ]; + }) + ; + + // Make sure that PluginManager hook is triggered + $this->container->pluginManager + ->expects(static::at(0)) + ->method('executeHooks') + ->willReturnCallback(function (string $hook, array $data, array $param) use ($currentDay): array { + static::assertSame('render_daily', $hook); + + static::assertArrayHasKey('linksToDisplay', $data); + static::assertCount(1, $data['linksToDisplay']); + static::assertSame(1, $data['linksToDisplay'][0]['id']); + static::assertSame($currentDay->getTimestamp(), $data['day']); + static::assertEmpty($data['previousday']); + static::assertEmpty($data['nextday']); + + static::assertArrayHasKey('loggedin', $param); + + return $data; + }); + + $result = $this->controller->index($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('daily', (string) $result->getBody()); + static::assertSame( + 'Daily - '. format_date($currentDay, false, true) .' - Shaarli', + $assignedVariables['pagetitle'] + ); + static::assertCount(1, $assignedVariables['linksToDisplay']); + + $link = $assignedVariables['linksToDisplay'][0]; + static::assertSame(1, $link['id']); + } + + /** + * Daily page - test that height adjustment in columns is working + */ + public function testValidIndexControllerInvokeHeightAdjustment(): void + { + $this->createValidContainerMockSet(); + + $currentDay = new \DateTimeImmutable('2020-05-13'); + + $request = $this->createMock(Request::class); + $response = new Response(); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + // Links dataset: 2 links with thumbnails + $this->container->bookmarkService + ->expects(static::once()) + ->method('days') + ->willReturnCallback(function () use ($currentDay): array { + return [ + $currentDay->format($currentDay->format('Ymd')), + ]; + }) + ; + $this->container->bookmarkService + ->expects(static::once()) + ->method('filterDay') + ->willReturnCallback(function (): array { + return [ + (new Bookmark())->setId(1)->setUrl('http://url.tld')->setTitle('title'), + (new Bookmark()) + ->setId(2) + ->setUrl('http://url.tld') + ->setTitle(static::generateContent(50)) + ->setDescription(static::generateContent(5000)) + , + (new Bookmark())->setId(3)->setUrl('http://url.tld')->setTitle('title'), + (new Bookmark())->setId(4)->setUrl('http://url.tld')->setTitle('title'), + (new Bookmark())->setId(5)->setUrl('http://url.tld')->setTitle('title'), + (new Bookmark())->setId(6)->setUrl('http://url.tld')->setTitle('title'), + (new Bookmark())->setId(7)->setUrl('http://url.tld')->setTitle('title'), + ]; + }) + ; + + // Make sure that PluginManager hook is triggered + $this->container->pluginManager + ->expects(static::at(0)) + ->method('executeHooks') + ->willReturnCallback(function (string $hook, array $data, array $param): array { + return $data; + }) + ; + + $result = $this->controller->index($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('daily', (string) $result->getBody()); + static::assertCount(7, $assignedVariables['linksToDisplay']); + + $columnIds = function (array $column): array { + return array_map(function (array $item): int { return $item['id']; }, $column); + }; + + static::assertSame([1, 4, 6], $columnIds($assignedVariables['cols'][0])); + static::assertSame([2], $columnIds($assignedVariables['cols'][1])); + static::assertSame([3, 5, 7], $columnIds($assignedVariables['cols'][2])); + } + + /** + * Daily page - no bookmark + */ + public function testValidIndexControllerInvokeNoBookmark(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $response = new Response(); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + // Links dataset: 2 links with thumbnails + $this->container->bookmarkService + ->expects(static::once()) + ->method('days') + ->willReturnCallback(function (): array { + return []; + }) + ; + $this->container->bookmarkService + ->expects(static::once()) + ->method('filterDay') + ->willReturnCallback(function (): array { + return []; + }) + ; + + // Make sure that PluginManager hook is triggered + $this->container->pluginManager + ->expects(static::at(0)) + ->method('executeHooks') + ->willReturnCallback(function (string $hook, array $data, array $param): array { + return $data; + }) + ; + + $result = $this->controller->index($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('daily', (string) $result->getBody()); + static::assertCount(0, $assignedVariables['linksToDisplay']); + static::assertSame('Today', $assignedVariables['dayDesc']); + static::assertEquals((new \DateTime())->setTime(0, 0)->getTimestamp(), $assignedVariables['day']); + static::assertEquals((new \DateTime())->setTime(0, 0), $assignedVariables['dayDate']); + } + + /** + * Daily RSS - default behaviour + */ + public function testValidRssControllerInvokeDefault(): void + { + $this->createValidContainerMockSet(); + + $dates = [ + new \DateTimeImmutable('2020-05-17'), + new \DateTimeImmutable('2020-05-15'), + new \DateTimeImmutable('2020-05-13'), + ]; + + $request = $this->createMock(Request::class); + $response = new Response(); + + $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([ + (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'), + (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), + (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), + (new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'), + ]); + + $this->container->pageCacheManager + ->expects(static::once()) + ->method('getCachePage') + ->willReturnCallback(function (): CachedPage { + $cachedPage = $this->createMock(CachedPage::class); + $cachedPage->expects(static::once())->method('cache')->with('dailyrss'); + + return $cachedPage; + } + ); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $result = $this->controller->rss($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]); + static::assertSame('dailyrss', (string) $result->getBody()); + static::assertSame('Shaarli', $assignedVariables['title']); + static::assertSame('http://shaarli', $assignedVariables['index_url']); + static::assertSame('http://shaarli/daily-rss', $assignedVariables['page_url']); + static::assertFalse($assignedVariables['hide_timestamps']); + static::assertCount(2, $assignedVariables['days']); + + $day = $assignedVariables['days'][$dates[0]->format('Ymd')]; + + static::assertEquals($dates[0], $day['date']); + static::assertSame($dates[0]->format(\DateTime::RSS), $day['date_rss']); + static::assertSame(format_date($dates[0], false), $day['date_human']); + static::assertSame('http://shaarli/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']); + static::assertCount(1, $day['links']); + static::assertSame(1, $day['links'][0]['id']); + static::assertSame('http://domain.tld/1', $day['links'][0]['url']); + static::assertEquals($dates[0], $day['links'][0]['created']); + + $day = $assignedVariables['days'][$dates[1]->format('Ymd')]; + + static::assertEquals($dates[1], $day['date']); + static::assertSame($dates[1]->format(\DateTime::RSS), $day['date_rss']); + static::assertSame(format_date($dates[1], false), $day['date_human']); + static::assertSame('http://shaarli/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']); + static::assertCount(2, $day['links']); + + static::assertSame(2, $day['links'][0]['id']); + static::assertSame('http://domain.tld/2', $day['links'][0]['url']); + static::assertEquals($dates[1], $day['links'][0]['created']); + static::assertSame(3, $day['links'][1]['id']); + static::assertSame('http://domain.tld/3', $day['links'][1]['url']); + static::assertEquals($dates[1], $day['links'][1]['created']); + } + + /** + * Daily RSS - trigger cache rendering + */ + public function testValidRssControllerInvokeTriggerCache(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $response = new Response(); + + $this->container->pageCacheManager->method('getCachePage')->willReturnCallback(function (): CachedPage { + $cachedPage = $this->createMock(CachedPage::class); + $cachedPage->method('cachedVersion')->willReturn('this is cache!'); + + return $cachedPage; + }); + + $this->container->bookmarkService->expects(static::never())->method('search'); + + $result = $this->controller->rss($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]); + static::assertSame('this is cache!', (string) $result->getBody()); + } + + /** + * Daily RSS - No bookmark + */ + public function testValidRssControllerInvokeNoBookmark(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $response = new Response(); + + $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([]); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $result = $this->controller->rss($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]); + static::assertSame('dailyrss', (string) $result->getBody()); + static::assertSame('Shaarli', $assignedVariables['title']); + static::assertSame('http://shaarli', $assignedVariables['index_url']); + static::assertSame('http://shaarli/daily-rss', $assignedVariables['page_url']); + static::assertFalse($assignedVariables['hide_timestamps']); + static::assertCount(0, $assignedVariables['days']); + } + + protected static function generateContent(int $length): string + { + // bin2hex(random_bytes) generates string twice as long as given parameter + $length = (int) ceil($length / 2); + return bin2hex(random_bytes($length)); + } +} diff --git a/tests/front/controller/visitor/FeedControllerTest.php b/tests/front/controller/visitor/FeedControllerTest.php new file mode 100644 index 00000000..fd4679ea --- /dev/null +++ b/tests/front/controller/visitor/FeedControllerTest.php @@ -0,0 +1,151 @@ +createContainer(); + + $this->container->feedBuilder = $this->createMock(FeedBuilder::class); + + $this->controller = new FeedController($this->container); + } + + /** + * Feed Controller - RSS default behaviour + */ + public function testDefaultRssController(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $response = new Response(); + + $this->container->feedBuilder->expects(static::once())->method('setLocale'); + $this->container->feedBuilder->expects(static::once())->method('setHideDates')->with(false); + $this->container->feedBuilder->expects(static::once())->method('setUsePermalinks')->with(true); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $this->container->feedBuilder->method('buildData')->willReturn(['content' => 'data']); + + // Make sure that PluginManager hook is triggered + $this->container->pluginManager + ->expects(static::at(0)) + ->method('executeHooks') + ->willReturnCallback(function (string $hook, array $data, array $param): void { + static::assertSame('render_feed', $hook); + static::assertSame('data', $data['content']); + + static::assertArrayHasKey('loggedin', $param); + static::assertSame('rss', $param['target']); + }) + ; + + $result = $this->controller->rss($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]); + static::assertSame('feed.rss', (string) $result->getBody()); + static::assertSame('data', $assignedVariables['content']); + } + + /** + * Feed Controller - ATOM default behaviour + */ + public function testDefaultAtomController(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $response = new Response(); + + $this->container->feedBuilder->expects(static::once())->method('setLocale'); + $this->container->feedBuilder->expects(static::once())->method('setHideDates')->with(false); + $this->container->feedBuilder->expects(static::once())->method('setUsePermalinks')->with(true); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $this->container->feedBuilder->method('buildData')->willReturn(['content' => 'data']); + + // Make sure that PluginManager hook is triggered + $this->container->pluginManager + ->expects(static::at(0)) + ->method('executeHooks') + ->willReturnCallback(function (string $hook, array $data, array $param): void { + static::assertSame('render_feed', $hook); + static::assertSame('data', $data['content']); + + static::assertArrayHasKey('loggedin', $param); + static::assertSame('atom', $param['target']); + }) + ; + + $result = $this->controller->atom($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertStringContainsString('application/atom', $result->getHeader('Content-Type')[0]); + static::assertSame('feed.atom', (string) $result->getBody()); + static::assertSame('data', $assignedVariables['content']); + } + + /** + * Feed Controller - ATOM with parameters + */ + public function testAtomControllerWithParameters(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $request->method('getParams')->willReturn(['parameter' => 'value']); + $response = new Response(); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $this->container->feedBuilder + ->method('buildData') + ->with('atom', ['parameter' => 'value']) + ->willReturn(['content' => 'data']) + ; + + // Make sure that PluginManager hook is triggered + $this->container->pluginManager + ->expects(static::at(0)) + ->method('executeHooks') + ->willReturnCallback(function (string $hook, array $data, array $param): void { + static::assertSame('render_feed', $hook); + static::assertSame('data', $data['content']); + + static::assertArrayHasKey('loggedin', $param); + static::assertSame('atom', $param['target']); + }) + ; + + $result = $this->controller->atom($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertStringContainsString('application/atom', $result->getHeader('Content-Type')[0]); + static::assertSame('feed.atom', (string) $result->getBody()); + static::assertSame('data', $assignedVariables['content']); + } +} diff --git a/tests/front/controller/visitor/FrontControllerMockHelper.php b/tests/front/controller/visitor/FrontControllerMockHelper.php new file mode 100644 index 00000000..bc3266b5 --- /dev/null +++ b/tests/front/controller/visitor/FrontControllerMockHelper.php @@ -0,0 +1,114 @@ +container = $this->createMock(ShaarliTestContainer::class); + } + + /** + * Initialize container's services used by tests + */ + protected function createValidContainerMockSet(): void + { + $this->container->loginManager = $this->createMock(LoginManager::class); + + // Config + $this->container->conf = $this->createMock(ConfigManager::class); + $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) { + return $default; + }); + + // PageBuilder + $this->container->pageBuilder = $this->createMock(PageBuilder::class); + $this->container->pageBuilder + ->method('render') + ->willReturnCallback(function (string $template): string { + return $template; + }) + ; + + // Plugin Manager + $this->container->pluginManager = $this->createMock(PluginManager::class); + + // BookmarkService + $this->container->bookmarkService = $this->createMock(BookmarkServiceInterface::class); + + // Formatter + $this->container->formatterFactory = $this->createMock(FormatterFactory::class); + $this->container->formatterFactory + ->method('getFormatter') + ->willReturnCallback(function (): BookmarkFormatter { + return new BookmarkRawFormatter($this->container->conf, true); + }) + ; + + // CacheManager + $this->container->pageCacheManager = $this->createMock(PageCacheManager::class); + + // SessionManager + $this->container->sessionManager = $this->createMock(SessionManager::class); + + // $_SERVER + $this->container->environment = [ + 'SERVER_NAME' => 'shaarli', + 'SERVER_PORT' => '80', + 'REQUEST_URI' => '/daily-rss', + ]; + } + + /** + * Pass a reference of an array which will be populated by `pageBuilder->assign` calls during execution. + * + * @param mixed $variables Array reference to populate. + */ + protected function assignTemplateVars(array &$variables): void + { + $this->container->pageBuilder + ->expects(static::atLeastOnce()) + ->method('assign') + ->willReturnCallback(function ($key, $value) use (&$variables) { + $variables[$key] = $value; + + return $this; + }) + ; + } + + /** + * Force to be used in PHPUnit context. + */ + protected abstract function createMock($originalClassName): MockObject; +} diff --git a/tests/front/controller/visitor/LoginControllerTest.php b/tests/front/controller/visitor/LoginControllerTest.php new file mode 100644 index 00000000..9d223316 --- /dev/null +++ b/tests/front/controller/visitor/LoginControllerTest.php @@ -0,0 +1,144 @@ +createContainer(); + + $this->controller = new LoginController($this->container); + } + + public function testValidControllerInvoke(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $request->expects(static::once())->method('getServerParam')->willReturn('> referer'); + $response = new Response(); + + $assignedVariables = []; + $this->container->pageBuilder + ->method('assign') + ->willReturnCallback(function ($key, $value) use (&$assignedVariables) { + $assignedVariables[$key] = $value; + + return $this; + }) + ; + + $this->container->loginManager->method('canLogin')->willReturn(true); + + $result = $this->controller->index($request, $response); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(200, $result->getStatusCode()); + static::assertSame('loginform', (string) $result->getBody()); + + static::assertSame('> referer', $assignedVariables['returnurl']); + static::assertSame(true, $assignedVariables['remember_user_default']); + static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']); + } + + public function testValidControllerInvokeWithUserName(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $request->expects(static::once())->method('getServerParam')->willReturn('> referer'); + $request->expects(static::exactly(2))->method('getParam')->willReturn('myUser>'); + $response = new Response(); + + $assignedVariables = []; + $this->container->pageBuilder + ->method('assign') + ->willReturnCallback(function ($key, $value) use (&$assignedVariables) { + $assignedVariables[$key] = $value; + + return $this; + }) + ; + + $this->container->loginManager->expects(static::once())->method('canLogin')->willReturn(true); + + $result = $this->controller->index($request, $response); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(200, $result->getStatusCode()); + static::assertSame('loginform', (string) $result->getBody()); + + static::assertSame('myUser>', $assignedVariables['username']); + static::assertSame('> referer', $assignedVariables['returnurl']); + static::assertSame(true, $assignedVariables['remember_user_default']); + static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']); + } + + public function testLoginControllerWhileLoggedIn(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $response = new Response(); + + $this->container->loginManager->expects(static::once())->method('isLoggedIn')->willReturn(true); + + $result = $this->controller->index($request, $response); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['./'], $result->getHeader('Location')); + } + + public function testLoginControllerOpenShaarli(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $response = new Response(); + + $conf = $this->createMock(ConfigManager::class); + $conf->method('get')->willReturnCallback(function (string $parameter, $default) { + if ($parameter === 'security.open_shaarli') { + return true; + } + return $default; + }); + $this->container->conf = $conf; + + $result = $this->controller->index($request, $response); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['./'], $result->getHeader('Location')); + } + + public function testLoginControllerWhileBanned(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $response = new Response(); + + $this->container->loginManager->method('isLoggedIn')->willReturn(false); + $this->container->loginManager->method('canLogin')->willReturn(false); + + $this->expectException(LoginBannedException::class); + + $this->controller->index($request, $response); + } +} diff --git a/tests/front/controller/visitor/OpenSearchControllerTest.php b/tests/front/controller/visitor/OpenSearchControllerTest.php new file mode 100644 index 00000000..52475318 --- /dev/null +++ b/tests/front/controller/visitor/OpenSearchControllerTest.php @@ -0,0 +1,46 @@ +createContainer(); + + $this->controller = new OpenSearchController($this->container); + } + + public function testOpenSearchController(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $response = new Response(); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $result = $this->controller->index($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertStringContainsString( + 'application/opensearchdescription+xml', + $result->getHeader('Content-Type')[0] + ); + static::assertSame('opensearch', (string) $result->getBody()); + static::assertSame('http://shaarli', $assignedVariables['serverurl']); + } +} diff --git a/tests/front/controller/visitor/PictureWallControllerTest.php b/tests/front/controller/visitor/PictureWallControllerTest.php new file mode 100644 index 00000000..7ac842cb --- /dev/null +++ b/tests/front/controller/visitor/PictureWallControllerTest.php @@ -0,0 +1,125 @@ +createContainer(); + + $this->controller = new PictureWallController($this->container); + } + + public function testValidControllerInvokeDefault(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $request->expects(static::once())->method('getQueryParams')->willReturn([]); + $response = new Response(); + + // ConfigManager: thumbnails are enabled + $this->container->conf = $this->createMock(ConfigManager::class); + $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) { + if ($parameter === 'thumbnails.mode') { + return Thumbnailer::MODE_COMMON; + } + + return $default; + }); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + // Links dataset: 2 links with thumbnails + $this->container->bookmarkService + ->expects(static::once()) + ->method('search') + ->willReturnCallback(function (array $parameters, ?string $visibility): array { + // Visibility is set through the container, not the call + static::assertNull($visibility); + + // No query parameters + if (count($parameters) === 0) { + return [ + (new Bookmark())->setId(1)->setUrl('http://url.tld')->setThumbnail('thumb1'), + (new Bookmark())->setId(2)->setUrl('http://url2.tld'), + (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setThumbnail('thumb2'), + ]; + } + }) + ; + + // Make sure that PluginManager hook is triggered + $this->container->pluginManager + ->expects(static::at(0)) + ->method('executeHooks') + ->willReturnCallback(function (string $hook, array $data, array $param): array { + static::assertSame('render_picwall', $hook); + static::assertArrayHasKey('linksToDisplay', $data); + static::assertCount(2, $data['linksToDisplay']); + static::assertSame(1, $data['linksToDisplay'][0]['id']); + static::assertSame(3, $data['linksToDisplay'][1]['id']); + static::assertArrayHasKey('loggedin', $param); + + return $data; + }); + + $result = $this->controller->index($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('picwall', (string) $result->getBody()); + static::assertSame('Picture wall - Shaarli', $assignedVariables['pagetitle']); + static::assertCount(2, $assignedVariables['linksToDisplay']); + + $link = $assignedVariables['linksToDisplay'][0]; + + static::assertSame(1, $link['id']); + static::assertSame('http://url.tld', $link['url']); + static::assertSame('thumb1', $link['thumbnail']); + + $link = $assignedVariables['linksToDisplay'][1]; + + static::assertSame(3, $link['id']); + static::assertSame('http://url3.tld', $link['url']); + static::assertSame('thumb2', $link['thumbnail']); + } + + public function testControllerWithThumbnailsDisabled(): void + { + $this->expectException(ThumbnailsDisabledException::class); + + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $response = new Response(); + + // ConfigManager: thumbnails are disabled + $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) { + if ($parameter === 'thumbnails.mode') { + return Thumbnailer::MODE_NONE; + } + + return $default; + }); + + $this->controller->index($request, $response); + } +} diff --git a/tests/front/controller/visitor/ShaarliPublicControllerTest.php b/tests/front/controller/visitor/ShaarliPublicControllerTest.php new file mode 100644 index 00000000..e2e88da3 --- /dev/null +++ b/tests/front/controller/visitor/ShaarliPublicControllerTest.php @@ -0,0 +1,239 @@ +createContainer(); + + $this->controller = new class($this->container) extends ShaarliVisitorController + { + public function assignView(string $key, $value): ShaarliVisitorController + { + return parent::assignView($key, $value); + } + + public function render(string $template): string + { + return parent::render($template); + } + + public function redirectFromReferer( + Request $request, + Response $response, + array $loopTerms = [], + array $clearParams = [] + ): Response { + return parent::redirectFromReferer($request, $response, $loopTerms, $clearParams); + } + }; + $this->assignedValues = []; + + $this->request = $this->createMock(Request::class); + $this->request->method('getUri')->willReturnCallback(function (): Uri { + $uri = $this->createMock(Uri::class); + $uri->method('getBasePath')->willReturn('/subfolder'); + + return $uri; + }); + } + + public function testAssignView(): void + { + $this->createValidContainerMockSet(); + + $this->assignTemplateVars($this->assignedValues); + + $self = $this->controller->assignView('variableName', 'variableValue'); + + static::assertInstanceOf(ShaarliVisitorController::class, $self); + static::assertSame('variableValue', $this->assignedValues['variableName']); + } + + public function testRender(): void + { + $this->createValidContainerMockSet(); + + $this->assignTemplateVars($this->assignedValues); + + $this->container->bookmarkService + ->method('count') + ->willReturnCallback(function (string $visibility): int { + return $visibility === BookmarkFilter::$PRIVATE ? 5 : 10; + }) + ; + + $this->container->pluginManager + ->method('executeHooks') + ->willReturnCallback(function (string $hook, array &$data, array $params): array { + return $data[$hook] = $params; + }); + $this->container->pluginManager->method('getErrors')->willReturn(['error']); + + $this->container->loginManager->method('isLoggedIn')->willReturn(true); + + $render = $this->controller->render('templateName'); + + static::assertSame('templateName', $render); + + static::assertSame(10, $this->assignedValues['linkcount']); + static::assertSame(5, $this->assignedValues['privateLinkcount']); + static::assertSame(['error'], $this->assignedValues['plugin_errors']); + + static::assertSame('templateName', $this->assignedValues['plugins_includes']['render_includes']['target']); + static::assertTrue($this->assignedValues['plugins_includes']['render_includes']['loggedin']); + static::assertSame('templateName', $this->assignedValues['plugins_header']['render_header']['target']); + static::assertTrue($this->assignedValues['plugins_header']['render_header']['loggedin']); + static::assertSame('templateName', $this->assignedValues['plugins_footer']['render_footer']['target']); + static::assertTrue($this->assignedValues['plugins_footer']['render_footer']['loggedin']); + } + + /** + * Test redirectFromReferer() - Default behaviour + */ + public function testRedirectFromRefererDefault(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2'; + + $response = new Response(); + + $result = $this->controller->redirectFromReferer($this->request, $response); + + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location')); + } + + /** + * Test redirectFromReferer() - With a loop term not matched in the referer + */ + public function testRedirectFromRefererWithUnmatchedLoopTerm(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2'; + + $response = new Response(); + + $result = $this->controller->redirectFromReferer($this->request, $response, ['nope']); + + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location')); + } + + /** + * Test redirectFromReferer() - With a loop term matching the referer in its path -> redirect to default + */ + public function testRedirectFromRefererWithMatchingLoopTermInPath(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2'; + + $response = new Response(); + + $result = $this->controller->redirectFromReferer($this->request, $response, ['nope', 'controller']); + + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/subfolder'], $result->getHeader('location')); + } + + /** + * Test redirectFromReferer() - With a loop term matching the referer in its query parameters -> redirect to default + */ + public function testRedirectFromRefererWithMatchingLoopTermInQueryParam(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2'; + + $response = new Response(); + + $result = $this->controller->redirectFromReferer($this->request, $response, ['nope', 'other']); + + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/subfolder'], $result->getHeader('location')); + } + + /** + * Test redirectFromReferer() - With a loop term matching the referer in its query value + * -> we do not block redirection for query parameter values. + */ + public function testRedirectFromRefererWithMatchingLoopTermInQueryValue(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2'; + + $response = new Response(); + + $result = $this->controller->redirectFromReferer($this->request, $response, ['nope', 'param']); + + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location')); + } + + /** + * Test redirectFromReferer() - With a loop term matching the referer in its domain name + * -> we do not block redirection for shaarli's hosts + */ + public function testRedirectFromRefererWithLoopTermInDomain(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2'; + + $response = new Response(); + + $result = $this->controller->redirectFromReferer($this->request, $response, ['shaarli']); + + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location')); + } + + /** + * Test redirectFromReferer() - With a loop term matching a query parameter AND clear this query param + * -> the param should be cleared before checking if it matches the redir loop terms + */ + public function testRedirectFromRefererWithMatchingClearedParam(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2'; + + $response = new Response(); + + $result = $this->controller->redirectFromReferer($this->request, $response, ['query'], ['query']); + + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/subfolder/controller?other=2'], $result->getHeader('location')); + } +} diff --git a/tests/front/controller/visitor/TagCloudControllerTest.php b/tests/front/controller/visitor/TagCloudControllerTest.php new file mode 100644 index 00000000..e636d496 --- /dev/null +++ b/tests/front/controller/visitor/TagCloudControllerTest.php @@ -0,0 +1,381 @@ +createContainer(); + + $this->controller = new TagCloudController($this->container); + } + + /** + * Tag Cloud - default parameters + */ + public function testValidCloudControllerInvokeDefault(): void + { + $this->createValidContainerMockSet(); + + $allTags = [ + 'ghi' => 1, + 'abc' => 3, + 'def' => 12, + ]; + $expectedOrder = ['abc', 'def', 'ghi']; + + $request = $this->createMock(Request::class); + $response = new Response(); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $this->container->bookmarkService + ->expects(static::once()) + ->method('bookmarksCountPerTag') + ->with([], null) + ->willReturnCallback(function () use ($allTags): array { + return $allTags; + }) + ; + + // Make sure that PluginManager hook is triggered + $this->container->pluginManager + ->expects(static::at(0)) + ->method('executeHooks') + ->willReturnCallback(function (string $hook, array $data, array $param): array { + static::assertSame('render_tagcloud', $hook); + static::assertSame('', $data['search_tags']); + static::assertCount(3, $data['tags']); + + static::assertArrayHasKey('loggedin', $param); + + return $data; + }) + ; + + $result = $this->controller->cloud($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('tag.cloud', (string) $result->getBody()); + static::assertSame('Tag cloud - Shaarli', $assignedVariables['pagetitle']); + + static::assertSame('', $assignedVariables['search_tags']); + static::assertCount(3, $assignedVariables['tags']); + static::assertSame($expectedOrder, array_keys($assignedVariables['tags'])); + + foreach ($allTags as $tag => $count) { + static::assertArrayHasKey($tag, $assignedVariables['tags']); + static::assertSame($count, $assignedVariables['tags'][$tag]['count']); + static::assertGreaterThan(0, $assignedVariables['tags'][$tag]['size']); + static::assertLessThan(5, $assignedVariables['tags'][$tag]['size']); + } + } + + /** + * Tag Cloud - Additional parameters: + * - logged in + * - visibility private + * - search tags: `ghi` and `def` (note that filtered tags are not displayed anymore) + */ + public function testValidCloudControllerInvokeWithParameters(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $request + ->method('getQueryParam') + ->with() + ->willReturnCallback(function (string $key): ?string { + if ('searchtags' === $key) { + return 'ghi def'; + } + + return null; + }) + ; + $response = new Response(); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $this->container->loginManager->method('isLoggedin')->willReturn(true); + $this->container->sessionManager->expects(static::once())->method('getSessionParameter')->willReturn('private'); + + $this->container->bookmarkService + ->expects(static::once()) + ->method('bookmarksCountPerTag') + ->with(['ghi', 'def'], BookmarkFilter::$PRIVATE) + ->willReturnCallback(function (): array { + return ['abc' => 3]; + }) + ; + + // Make sure that PluginManager hook is triggered + $this->container->pluginManager + ->expects(static::at(0)) + ->method('executeHooks') + ->willReturnCallback(function (string $hook, array $data, array $param): array { + static::assertSame('render_tagcloud', $hook); + static::assertSame('ghi def', $data['search_tags']); + static::assertCount(1, $data['tags']); + + static::assertArrayHasKey('loggedin', $param); + + return $data; + }) + ; + + $result = $this->controller->cloud($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('tag.cloud', (string) $result->getBody()); + static::assertSame('ghi def - Tag cloud - Shaarli', $assignedVariables['pagetitle']); + + static::assertSame('ghi def', $assignedVariables['search_tags']); + static::assertCount(1, $assignedVariables['tags']); + + static::assertArrayHasKey('abc', $assignedVariables['tags']); + static::assertSame(3, $assignedVariables['tags']['abc']['count']); + static::assertGreaterThan(0, $assignedVariables['tags']['abc']['size']); + static::assertLessThan(5, $assignedVariables['tags']['abc']['size']); + } + + /** + * Tag Cloud - empty + */ + public function testEmptyCloud(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $response = new Response(); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $this->container->bookmarkService + ->expects(static::once()) + ->method('bookmarksCountPerTag') + ->with([], null) + ->willReturnCallback(function (array $parameters, ?string $visibility): array { + return []; + }) + ; + + // Make sure that PluginManager hook is triggered + $this->container->pluginManager + ->expects(static::at(0)) + ->method('executeHooks') + ->willReturnCallback(function (string $hook, array $data, array $param): array { + static::assertSame('render_tagcloud', $hook); + static::assertSame('', $data['search_tags']); + static::assertCount(0, $data['tags']); + + static::assertArrayHasKey('loggedin', $param); + + return $data; + }) + ; + + $result = $this->controller->cloud($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('tag.cloud', (string) $result->getBody()); + static::assertSame('Tag cloud - Shaarli', $assignedVariables['pagetitle']); + + static::assertSame('', $assignedVariables['search_tags']); + static::assertCount(0, $assignedVariables['tags']); + } + + /** + * Tag List - Default sort is by usage DESC + */ + public function testValidListControllerInvokeDefault(): void + { + $this->createValidContainerMockSet(); + + $allTags = [ + 'def' => 12, + 'abc' => 3, + 'ghi' => 1, + ]; + + $request = $this->createMock(Request::class); + $response = new Response(); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $this->container->bookmarkService + ->expects(static::once()) + ->method('bookmarksCountPerTag') + ->with([], null) + ->willReturnCallback(function () use ($allTags): array { + return $allTags; + }) + ; + + // Make sure that PluginManager hook is triggered + $this->container->pluginManager + ->expects(static::at(0)) + ->method('executeHooks') + ->willReturnCallback(function (string $hook, array $data, array $param): array { + static::assertSame('render_taglist', $hook); + static::assertSame('', $data['search_tags']); + static::assertCount(3, $data['tags']); + + static::assertArrayHasKey('loggedin', $param); + + return $data; + }) + ; + + $result = $this->controller->list($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('tag.list', (string) $result->getBody()); + static::assertSame('Tag list - Shaarli', $assignedVariables['pagetitle']); + + static::assertSame('', $assignedVariables['search_tags']); + static::assertCount(3, $assignedVariables['tags']); + + foreach ($allTags as $tag => $count) { + static::assertSame($count, $assignedVariables['tags'][$tag]); + } + } + + /** + * Tag List - Additional parameters: + * - logged in + * - visibility private + * - search tags: `ghi` and `def` (note that filtered tags are not displayed anymore) + * - sort alphabetically + */ + public function testValidListControllerInvokeWithParameters(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $request + ->method('getQueryParam') + ->with() + ->willReturnCallback(function (string $key): ?string { + if ('searchtags' === $key) { + return 'ghi def'; + } elseif ('sort' === $key) { + return 'alpha'; + } + + return null; + }) + ; + $response = new Response(); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $this->container->loginManager->method('isLoggedin')->willReturn(true); + $this->container->sessionManager->expects(static::once())->method('getSessionParameter')->willReturn('private'); + + $this->container->bookmarkService + ->expects(static::once()) + ->method('bookmarksCountPerTag') + ->with(['ghi', 'def'], BookmarkFilter::$PRIVATE) + ->willReturnCallback(function (): array { + return ['abc' => 3]; + }) + ; + + // Make sure that PluginManager hook is triggered + $this->container->pluginManager + ->expects(static::at(0)) + ->method('executeHooks') + ->willReturnCallback(function (string $hook, array $data, array $param): array { + static::assertSame('render_taglist', $hook); + static::assertSame('ghi def', $data['search_tags']); + static::assertCount(1, $data['tags']); + + static::assertArrayHasKey('loggedin', $param); + + return $data; + }) + ; + + $result = $this->controller->list($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('tag.list', (string) $result->getBody()); + static::assertSame('ghi def - Tag list - Shaarli', $assignedVariables['pagetitle']); + + static::assertSame('ghi def', $assignedVariables['search_tags']); + static::assertCount(1, $assignedVariables['tags']); + static::assertSame(3, $assignedVariables['tags']['abc']); + } + + /** + * Tag List - empty + */ + public function testEmptyList(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $response = new Response(); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $this->container->bookmarkService + ->expects(static::once()) + ->method('bookmarksCountPerTag') + ->with([], null) + ->willReturnCallback(function (array $parameters, ?string $visibility): array { + return []; + }) + ; + + // Make sure that PluginManager hook is triggered + $this->container->pluginManager + ->expects(static::at(0)) + ->method('executeHooks') + ->willReturnCallback(function (string $hook, array $data, array $param): array { + static::assertSame('render_taglist', $hook); + static::assertSame('', $data['search_tags']); + static::assertCount(0, $data['tags']); + + static::assertArrayHasKey('loggedin', $param); + + return $data; + }) + ; + + $result = $this->controller->list($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('tag.list', (string) $result->getBody()); + static::assertSame('Tag list - Shaarli', $assignedVariables['pagetitle']); + + static::assertSame('', $assignedVariables['search_tags']); + static::assertCount(0, $assignedVariables['tags']); + } +} diff --git a/tests/front/controller/visitor/TagControllerTest.php b/tests/front/controller/visitor/TagControllerTest.php new file mode 100644 index 00000000..9a2b1f71 --- /dev/null +++ b/tests/front/controller/visitor/TagControllerTest.php @@ -0,0 +1,241 @@ +createContainer(); + + $this->controller = new TagController($this->container); + } + + public function testAddTagWithReferer(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/']; + + $request = $this->createMock(Request::class); + $response = new Response(); + + $tags = ['newTag' => 'abc']; + + $result = $this->controller->addTag($request, $response, $tags); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/controller/?searchtags=abc'], $result->getHeader('location')); + } + + public function testAddTagWithRefererAndExistingSearch(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def']; + + $request = $this->createMock(Request::class); + $response = new Response(); + + $tags = ['newTag' => 'abc']; + + $result = $this->controller->addTag($request, $response, $tags); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location')); + } + + public function testAddTagWithoutRefererAndExistingSearch(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $response = new Response(); + + $tags = ['newTag' => 'abc']; + + $result = $this->controller->addTag($request, $response, $tags); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['./?searchtags=abc'], $result->getHeader('location')); + } + + public function testAddTagRemoveLegacyQueryParam(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def&addtag=abc']; + + $request = $this->createMock(Request::class); + $response = new Response(); + + $tags = ['newTag' => 'abc']; + + $result = $this->controller->addTag($request, $response, $tags); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location')); + } + + public function testAddTagResetPagination(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def&page=12']; + + $request = $this->createMock(Request::class); + $response = new Response(); + + $tags = ['newTag' => 'abc']; + + $result = $this->controller->addTag($request, $response, $tags); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location')); + } + + public function testAddTagWithRefererAndEmptySearch(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=']; + + $request = $this->createMock(Request::class); + $response = new Response(); + + $tags = ['newTag' => 'abc']; + + $result = $this->controller->addTag($request, $response, $tags); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/controller/?searchtags=abc'], $result->getHeader('location')); + } + + public function testAddTagWithoutNewTagWithReferer(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def']; + + $request = $this->createMock(Request::class); + $response = new Response(); + + $result = $this->controller->addTag($request, $response, []); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/controller/?searchtags=def'], $result->getHeader('location')); + } + + public function testAddTagWithoutNewTagWithoutReferer(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $response = new Response(); + + $result = $this->controller->addTag($request, $response, []); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['./'], $result->getHeader('location')); + } + + public function testRemoveTagWithoutMatchingTag(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def']; + + $request = $this->createMock(Request::class); + $response = new Response(); + + $tags = ['tag' => 'abc']; + + $result = $this->controller->removeTag($request, $response, $tags); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/controller/?searchtags=def'], $result->getHeader('location')); + } + + public function testRemoveTagWithoutTagsearch(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/']; + + $request = $this->createMock(Request::class); + $response = new Response(); + + $tags = ['tag' => 'abc']; + + $result = $this->controller->removeTag($request, $response, $tags); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/controller/'], $result->getHeader('location')); + } + + public function testRemoveTagWithoutReferer(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $response = new Response(); + + $tags = ['tag' => 'abc']; + + $result = $this->controller->removeTag($request, $response, $tags); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['./'], $result->getHeader('location')); + } + + public function testRemoveTagWithoutTag(): void + { + $this->createValidContainerMockSet(); + + $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtag=abc']; + + $request = $this->createMock(Request::class); + $response = new Response(); + + $result = $this->controller->removeTag($request, $response, []); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['/controller/?searchtag=abc'], $result->getHeader('location')); + } + + public function testRemoveTagWithoutTagWithoutReferer(): void + { + $this->createValidContainerMockSet(); + + $request = $this->createMock(Request::class); + $response = new Response(); + + $result = $this->controller->removeTag($request, $response, []); + + static::assertInstanceOf(Response::class, $result); + static::assertSame(302, $result->getStatusCode()); + static::assertSame(['./'], $result->getHeader('location')); + } +} diff --git a/tpl/default/page.header.html b/tpl/default/page.header.html index 2d015b26..624367e4 100644 --- a/tpl/default/page.header.html +++ b/tpl/default/page.header.html @@ -38,7 +38,7 @@ {/if}
  • - {'Daily'|t} + {'Daily'|t}
  • {loop="$plugins_header.buttons_toolbar"}
  • diff --git a/tpl/vintage/daily.html b/tpl/vintage/daily.html index adcdf6ab..a459e21a 100644 --- a/tpl/vintage/daily.html +++ b/tpl/vintage/daily.html @@ -14,9 +14,9 @@
    All links of one day
    in a single page.
    - {if="$previousday"} <Previous day{else}<Previous day{/if} + {if="$previousday"} <Previous day{else}<Previous day{/if} - - {if="$nextday"}Next day>{else}Next day>{/if} + {if="$nextday"}Next day>{else}Next day>{/if}
    {loop="$daily_about_plugin"} diff --git a/tpl/vintage/page.header.html b/tpl/vintage/page.header.html index 0a8392b6..9268ced9 100644 --- a/tpl/vintage/page.header.html +++ b/tpl/vintage/page.header.html @@ -33,7 +33,7 @@ {/if}
  • Tag cloud
  • Picture wall
  • -
  • Daily
  • +
  • Daily
  • {loop="$plugins_header.buttons_toolbar"}