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 --- .../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 ++++++++++++ 11 files changed, 936 insertions(+) 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 (limited to 'application/front/controller') 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); + } +} -- cgit v1.2.3 From ba43064ddb7771fc97df135a32f9b0d5e373dd36 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 22 May 2020 13:47:02 +0200 Subject: Process tools page through Slim controller --- .../front/controller/admin/ToolsController.php | 49 ++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 application/front/controller/admin/ToolsController.php (limited to 'application/front/controller') diff --git a/application/front/controller/admin/ToolsController.php b/application/front/controller/admin/ToolsController.php new file mode 100644 index 00000000..66db5ad9 --- /dev/null +++ b/application/front/controller/admin/ToolsController.php @@ -0,0 +1,49 @@ + index_url($this->container->environment), + 'sslenabled' => is_https($this->container->environment), + ]; + + $this->executeHooks($data); + + foreach ($data as $key => $value) { + $this->assignView($key, $value); + } + + $this->assignView('pagetitle', t('Tools') .' - '. $this->container->conf->get('general.title', 'Shaarli')); + + return $response->write($this->render('tools')); + } + + /** + * @param mixed[] $data Variables passed to the template engine + * + * @return mixed[] Template data after active plugins render_picwall hook execution. + */ + protected function executeHooks(array $data): array + { + $this->container->pluginManager->executeHooks( + 'render_tools', + $data + ); + + return $data; + } +} -- cgit v1.2.3 From ef00f9d2033f6de11e71bf3a909399cae6f73a9f Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 27 May 2020 13:35:48 +0200 Subject: Process password change controller through Slim --- .../front/controller/admin/PasswordController.php | 100 +++++++++++++++++++++ .../controller/admin/ShaarliAdminController.php | 59 ++++++++++++ .../visitor/ShaarliVisitorController.php | 8 ++ 3 files changed, 167 insertions(+) create mode 100644 application/front/controller/admin/PasswordController.php (limited to 'application/front/controller') diff --git a/application/front/controller/admin/PasswordController.php b/application/front/controller/admin/PasswordController.php new file mode 100644 index 00000000..6e8f0bcb --- /dev/null +++ b/application/front/controller/admin/PasswordController.php @@ -0,0 +1,100 @@ +assignView( + 'pagetitle', + t('Change password') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + } + + /** + * GET /password - Displays the change password template + */ + public function index(Request $request, Response $response): Response + { + return $response->write($this->render('changepassword')); + } + + /** + * POST /password - Change admin password - existing and new passwords need to be provided. + */ + public function change(Request $request, Response $response): Response + { + $this->checkToken($request); + + if ($this->container->conf->get('security.open_shaarli', false)) { + throw new OpenShaarliPasswordException(); + } + + $oldPassword = $request->getParam('oldpassword'); + $newPassword = $request->getParam('setpassword'); + + if (empty($newPassword) || empty($oldPassword)) { + $this->saveErrorMessage(t('You must provide the current and new password to change it.')); + + return $response + ->withStatus(400) + ->write($this->render('changepassword')) + ; + } + + // Make sure old password is correct. + $oldHash = sha1( + $oldPassword . + $this->container->conf->get('credentials.login') . + $this->container->conf->get('credentials.salt') + ); + + if ($oldHash !== $this->container->conf->get('credentials.hash')) { + $this->saveErrorMessage(t('The old password is not correct.')); + + return $response + ->withStatus(400) + ->write($this->render('changepassword')) + ; + } + + // Save new password + // Salt renders rainbow-tables attacks useless. + $this->container->conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); + $this->container->conf->set( + 'credentials.hash', + sha1( + $newPassword + . $this->container->conf->get('credentials.login') + . $this->container->conf->get('credentials.salt') + ) + ); + + try { + $this->container->conf->write($this->container->loginManager->isLoggedIn()); + } catch (Throwable $e) { + throw new ShaarliFrontException($e->getMessage(), 500, $e); + } + + $this->saveSuccessMessage(t('Your password has been changed')); + + return $response->write($this->render('changepassword')); + } +} diff --git a/application/front/controller/admin/ShaarliAdminController.php b/application/front/controller/admin/ShaarliAdminController.php index ea703f62..3385006c 100644 --- a/application/front/controller/admin/ShaarliAdminController.php +++ b/application/front/controller/admin/ShaarliAdminController.php @@ -7,7 +7,19 @@ namespace Shaarli\Front\Controller\Admin; use Shaarli\Container\ShaarliContainer; use Shaarli\Front\Controller\Visitor\ShaarliVisitorController; use Shaarli\Front\Exception\UnauthorizedException; +use Shaarli\Front\Exception\WrongTokenException; +use Shaarli\Security\SessionManager; +use Slim\Http\Request; +/** + * Class ShaarliAdminController + * + * All admin controllers (for logged in users) MUST extend this abstract class. + * It makes sure that the user is properly logged in, and otherwise throw an exception + * which will redirect to the login page. + * + * @package Shaarli\Front\Controller\Admin + */ abstract class ShaarliAdminController extends ShaarliVisitorController { public function __construct(ShaarliContainer $container) @@ -18,4 +30,51 @@ abstract class ShaarliAdminController extends ShaarliVisitorController throw new UnauthorizedException(); } } + + /** + * Any persistent action to the config or data store must check the XSRF token validity. + */ + protected function checkToken(Request $request): void + { + if (!$this->container->sessionManager->checkToken($request->getParam('token'))) { + throw new WrongTokenException(); + } + } + + /** + * Save a SUCCESS message in user session, which will be displayed on any template page. + */ + protected function saveSuccessMessage(string $message): void + { + $this->saveMessage(SessionManager::KEY_SUCCESS_MESSAGES, $message); + } + + /** + * Save a WARNING message in user session, which will be displayed on any template page. + */ + protected function saveWarningMessage(string $message): void + { + $this->saveMessage(SessionManager::KEY_WARNING_MESSAGES, $message); + } + + /** + * Save an ERROR message in user session, which will be displayed on any template page. + */ + protected function saveErrorMessage(string $message): void + { + $this->saveMessage(SessionManager::KEY_ERROR_MESSAGES, $message); + } + + /** + * Use the sessionManager to save the provided message using the proper type. + * + * @param string $type successed/warnings/errors + */ + protected function saveMessage(string $type, string $message): void + { + $messages = $this->container->sessionManager->getSessionParameter($type) ?? []; + $messages[] = $message; + + $this->container->sessionManager->setSessionParameter($type, $messages); + } } diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php index 655b3baa..f12915c1 100644 --- a/application/front/controller/visitor/ShaarliVisitorController.php +++ b/application/front/controller/visitor/ShaarliVisitorController.php @@ -9,6 +9,14 @@ use Shaarli\Container\ShaarliContainer; use Slim\Http\Request; use Slim\Http\Response; +/** + * Class ShaarliVisitorController + * + * All controllers accessible by visitors (non logged in users) should extend this abstract class. + * Contains a few helper function for template rendering, plugins, etc. + * + * @package Shaarli\Front\Controller\Visitor + */ abstract class ShaarliVisitorController { /** @var ShaarliContainer */ -- cgit v1.2.3 From fdedbfd4a7fb547da0e0ce65c6180f74aad90691 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 27 May 2020 14:13:49 +0200 Subject: Test ShaarliAdminController --- application/front/controller/admin/ShaarliAdminController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'application/front/controller') diff --git a/application/front/controller/admin/ShaarliAdminController.php b/application/front/controller/admin/ShaarliAdminController.php index 3385006c..3bc5bb6b 100644 --- a/application/front/controller/admin/ShaarliAdminController.php +++ b/application/front/controller/admin/ShaarliAdminController.php @@ -34,11 +34,13 @@ abstract class ShaarliAdminController extends ShaarliVisitorController /** * Any persistent action to the config or data store must check the XSRF token validity. */ - protected function checkToken(Request $request): void + protected function checkToken(Request $request): bool { if (!$this->container->sessionManager->checkToken($request->getParam('token'))) { throw new WrongTokenException(); } + + return true; } /** -- cgit v1.2.3 From 66063ed1a18d739b1a60bfb163d8656417a4c529 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 30 May 2020 14:00:06 +0200 Subject: Process configure page through Slim controller --- .../front/controller/admin/ConfigureController.php | 120 +++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 application/front/controller/admin/ConfigureController.php (limited to 'application/front/controller') diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php new file mode 100644 index 00000000..b1d32270 --- /dev/null +++ b/application/front/controller/admin/ConfigureController.php @@ -0,0 +1,120 @@ +assignView('title', $this->container->conf->get('general.title', 'Shaarli')); + $this->assignView('theme', $this->container->conf->get('resource.theme')); + $this->assignView( + 'theme_available', + ThemeUtils::getThemes($this->container->conf->get('resource.raintpl_tpl')) + ); + $this->assignView('formatter_available', ['default', 'markdown']); + list($continents, $cities) = generateTimeZoneData( + timezone_identifiers_list(), + $this->container->conf->get('general.timezone') + ); + $this->assignView('continents', $continents); + $this->assignView('cities', $cities); + $this->assignView('retrieve_description', $this->container->conf->get('general.retrieve_description', false)); + $this->assignView('private_links_default', $this->container->conf->get('privacy.default_private_links', false)); + $this->assignView( + 'session_protection_disabled', + $this->container->conf->get('security.session_protection_disabled', false) + ); + $this->assignView('enable_rss_permalinks', $this->container->conf->get('feed.rss_permalinks', false)); + $this->assignView('enable_update_check', $this->container->conf->get('updates.check_updates', true)); + $this->assignView('hide_public_links', $this->container->conf->get('privacy.hide_public_links', false)); + $this->assignView('api_enabled', $this->container->conf->get('api.enabled', true)); + $this->assignView('api_secret', $this->container->conf->get('api.secret')); + $this->assignView('languages', Languages::getAvailableLanguages()); + $this->assignView('gd_enabled', extension_loaded('gd')); + $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)); + $this->assignView('pagetitle', t('Configure') .' - '. $this->container->conf->get('general.title', 'Shaarli')); + + return $response->write($this->render('configure')); + } + + /** + * POST /configure - Update Shaarli's configuration + */ + public function save(Request $request, Response $response): Response + { + $this->checkToken($request); + + $continent = $request->getParam('continent'); + $city = $request->getParam('city'); + $tz = 'UTC'; + if (null !== $continent && null !== $city && isTimeZoneValid($continent, $city)) { + $tz = $continent . '/' . $city; + } + + $this->container->conf->set('general.timezone', $tz); + $this->container->conf->set('general.title', escape($request->getParam('title'))); + $this->container->conf->set('general.header_link', escape($request->getParam('titleLink'))); + $this->container->conf->set('general.retrieve_description', !empty($request->getParam('retrieveDescription'))); + $this->container->conf->set('resource.theme', escape($request->getParam('theme'))); + $this->container->conf->set( + 'security.session_protection_disabled', + !empty($request->getParam('disablesessionprotection')) + ); + $this->container->conf->set( + 'privacy.default_private_links', + !empty($request->getParam('privateLinkByDefault')) + ); + $this->container->conf->set('feed.rss_permalinks', !empty($request->getParam('enableRssPermalinks'))); + $this->container->conf->set('updates.check_updates', !empty($request->getParam('updateCheck'))); + $this->container->conf->set('privacy.hide_public_links', !empty($request->getParam('hidePublicLinks'))); + $this->container->conf->set('api.enabled', !empty($request->getParam('enableApi'))); + $this->container->conf->set('api.secret', escape($request->getParam('apiSecret'))); + $this->container->conf->set('formatter', escape($request->getParam('formatter'))); + + if (!empty($request->getParam('language'))) { + $this->container->conf->set('translation.language', escape($request->getParam('language'))); + } + + $thumbnailsMode = extension_loaded('gd') ? $request->getParam('enableThumbnails') : Thumbnailer::MODE_NONE; + if ($thumbnailsMode !== Thumbnailer::MODE_NONE + && $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) + ) { + $this->saveWarningMessage(t( + 'You have enabled or changed thumbnails mode. ' + .'Please synchronize them.' + )); + } + $this->container->conf->set('thumbnails.mode', $thumbnailsMode); + + try { + $this->container->conf->write($this->container->loginManager->isLoggedIn()); + $this->container->history->updateSettings(); + $this->container->pageCacheManager->invalidateCaches(); + } catch (Throwable $e) { + // TODO: translation + stacktrace + $this->saveErrorMessage('ERROR while writing config file after configuration update.'); + } + + $this->saveSuccessMessage(t('Configuration was saved.')); + + return $response->withRedirect('./configure'); + } +} -- cgit v1.2.3 From 8eac2e54882d8adae8cbb45386dca1b465242632 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 30 May 2020 15:51:14 +0200 Subject: Process manage tags page through Slim controller --- .../front/controller/admin/ConfigureController.php | 2 +- .../front/controller/admin/ManageTagController.php | 87 ++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 application/front/controller/admin/ManageTagController.php (limited to 'application/front/controller') diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php index b1d32270..5a482d8e 100644 --- a/application/front/controller/admin/ConfigureController.php +++ b/application/front/controller/admin/ConfigureController.php @@ -12,7 +12,7 @@ use Slim\Http\Response; use Throwable; /** - * Class PasswordController + * Class ConfigureController * * Slim controller used to handle Shaarli configuration page (display + save new config). */ diff --git a/application/front/controller/admin/ManageTagController.php b/application/front/controller/admin/ManageTagController.php new file mode 100644 index 00000000..e015e613 --- /dev/null +++ b/application/front/controller/admin/ManageTagController.php @@ -0,0 +1,87 @@ +getParam('fromtag') ?? ''; + + $this->assignView('fromtag', escape($fromTag)); + $this->assignView( + 'pagetitle', + t('Manage tags') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + + return $response->write($this->render('changetag')); + } + + /** + * POST /manage-tags - Update or delete provided tag + */ + public function save(Request $request, Response $response): Response + { + $this->checkToken($request); + + $isDelete = null !== $request->getParam('deletetag') && null === $request->getParam('renametag'); + + $fromTag = escape(trim($request->getParam('fromtag') ?? '')); + $toTag = escape(trim($request->getParam('totag') ?? '')); + + if (0 === strlen($fromTag) || false === $isDelete && 0 === strlen($toTag)) { + $this->saveWarningMessage(t('Invalid tags provided.')); + + return $response->withRedirect('./manage-tags'); + } + + // TODO: move this to bookmark service + $count = 0; + $bookmarks = $this->container->bookmarkService->search(['searchtags' => $fromTag], BookmarkFilter::$ALL, true); + foreach ($bookmarks as $bookmark) { + if (false === $isDelete) { + $bookmark->renameTag($fromTag, $toTag); + } else { + $bookmark->deleteTag($fromTag); + } + + $this->container->bookmarkService->set($bookmark, false); + $this->container->history->updateLink($bookmark); + $count++; + } + + $this->container->bookmarkService->save(); + + if (true === $isDelete) { + $alert = sprintf( + t('The tag was removed from %d bookmark.', 'The tag was removed from %d bookmarks.', $count), + $count + ); + } else { + $alert = sprintf( + t('The tag was renamed in %d bookmark.', 'The tag was renamed in %d bookmarks.', $count), + $count + ); + } + + $this->saveSuccessMessage($alert); + + $redirect = true === $isDelete ? './manage-tags' : './?searchtags='. urlencode($toTag); + + return $response->withRedirect($redirect); + } +} -- cgit v1.2.3 From c22fa57a5505fe95fd01860e3d3dfbb089f869cd Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 6 Jun 2020 14:01:03 +0200 Subject: Handle shaare creation/edition/deletion through Slim controllers --- .../controller/admin/PostBookmarkController.php | 258 +++++++++++++++++++++ .../front/controller/admin/ToolsController.php | 2 +- .../front/controller/visitor/DailyController.php | 2 +- .../front/controller/visitor/FeedController.php | 2 +- .../visitor/ShaarliVisitorController.php | 14 +- 5 files changed, 269 insertions(+), 9 deletions(-) create mode 100644 application/front/controller/admin/PostBookmarkController.php (limited to 'application/front/controller') diff --git a/application/front/controller/admin/PostBookmarkController.php b/application/front/controller/admin/PostBookmarkController.php new file mode 100644 index 00000000..dbe570e2 --- /dev/null +++ b/application/front/controller/admin/PostBookmarkController.php @@ -0,0 +1,258 @@ +assignView( + 'pagetitle', + t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + + return $response->write($this->render('addlink')); + } + + /** + * GET /shaare - Displays the bookmark form for creation. + * Note that if the URL is found in existing bookmarks, then it will be in edit mode. + */ + public function displayCreateForm(Request $request, Response $response): Response + { + $url = cleanup_url($request->getParam('post')); + + $linkIsNew = false; + // Check if URL is not already in database (in this case, we will edit the existing link) + $bookmark = $this->container->bookmarkService->findByUrl($url); + if (null === $bookmark) { + $linkIsNew = true; + // Get shaare data if it was provided in URL (e.g.: by the bookmarklet). + $title = $request->getParam('title'); + $description = $request->getParam('description'); + $tags = $request->getParam('tags'); + $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN); + + // If this is an HTTP(S) link, we try go get the page to extract + // the title (otherwise we will to straight to the edit form.) + if (empty($title) && strpos(get_url_scheme($url) ?: '', 'http') !== false) { + $retrieveDescription = $this->container->conf->get('general.retrieve_description'); + // Short timeout to keep the application responsive + // The callback will fill $charset and $title with data from the downloaded page. + $this->container->httpAccess->getHttpResponse( + $url, + $this->container->conf->get('general.download_timeout', 30), + $this->container->conf->get('general.download_max_size', 4194304), + $this->container->httpAccess->getCurlDownloadCallback( + $charset, + $title, + $description, + $tags, + $retrieveDescription + ) + ); + if (! empty($title) && strtolower($charset) !== 'utf-8') { + $title = mb_convert_encoding($title, 'utf-8', $charset); + } + } + + if (empty($url) && empty($title)) { + $title = $this->container->conf->get('general.default_note_title', t('Note: ')); + } + + $link = escape([ + 'title' => $title, + 'url' => $url ?? '', + 'description' => $description ?? '', + 'tags' => $tags ?? '', + 'private' => $private, + ]); + } else { + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $link = $formatter->format($bookmark); + } + + return $this->displayForm($link, $linkIsNew, $request, $response); + } + + /** + * GET /shaare-{id} - Displays the bookmark form in edition mode. + */ + public function displayEditForm(Request $request, Response $response, array $args): Response + { + $id = $args['id']; + try { + if (false === ctype_digit($id)) { + throw new BookmarkNotFoundException(); + } + $bookmark = $this->container->bookmarkService->get($id); // Read database + } catch (BookmarkNotFoundException $e) { + $this->saveErrorMessage(t('Bookmark not found')); + + return $response->withRedirect('./'); + } + + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $link = $formatter->format($bookmark); + + return $this->displayForm($link, false, $request, $response); + } + + /** + * POST /shaare + */ + public function save(Request $request, Response $response): Response + { + $this->checkToken($request); + + // lf_id should only be present if the link exists. + $id = $request->getParam('lf_id') ? intval(escape($request->getParam('lf_id'))) : null; + if (null !== $id && true === $this->container->bookmarkService->exists($id)) { + // Edit + $bookmark = $this->container->bookmarkService->get($id); + } else { + // New link + $bookmark = new Bookmark(); + } + + $bookmark->setTitle($request->getParam('lf_title')); + $bookmark->setDescription($request->getParam('lf_description')); + $bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', [])); + $bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN)); + $bookmark->setTagsString($request->getParam('lf_tags')); + + if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE + && false === $bookmark->isNote() + ) { + $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); + } + $this->container->bookmarkService->addOrSet($bookmark, false); + + // To preserve backward compatibility with 3rd parties, plugins still use arrays + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $data = $formatter->format($bookmark); + $data = $this->executeHooks('save_link', $data); + + $bookmark->fromArray($data); + $this->container->bookmarkService->set($bookmark); + + // If we are called from the bookmarklet, we must close the popup: + if ($request->getParam('source') === 'bookmarklet') { + return $response->write(''); + } + + if (!empty($request->getParam('returnurl'))) { + $this->container->environment['HTTP_REFERER'] = escape($request->getParam('returnurl')); + } + + return $this->redirectFromReferer( + $request, + $response, + ['add-shaare', 'shaare'], ['addlink', 'post', 'edit_link'], + $bookmark->getShortUrl() + ); + } + + public function deleteBookmark(Request $request, Response $response): Response + { + $this->checkToken($request); + + $ids = escape(trim($request->getParam('lf_linkdate'))); + if (strpos($ids, ' ') !== false) { + // multiple, space-separated ids provided + $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'strlen')); + } else { + $ids = [$ids]; + } + + // assert at least one id is given + if (0 === count($ids)) { + $this->saveErrorMessage(t('Invalid bookmark ID provided.')); + + return $this->redirectFromReferer($request, $response, [], ['delete-shaare']); + } + + $formatter = $this->container->formatterFactory->getFormatter('raw'); + foreach ($ids as $id) { + $id = (int) $id; + // TODO: check if it exists + $bookmark = $this->container->bookmarkService->get($id); + $data = $formatter->format($bookmark); + $this->container->pluginManager->executeHooks('delete_link', $data); + $this->container->bookmarkService->remove($bookmark, false); + } + + $this->container->bookmarkService->save(); + + // If we are called from the bookmarklet, we must close the popup: + if ($request->getParam('source') === 'bookmarklet') { + return $response->write(''); + } + + // Don't redirect to where we were previously because the datastore has changed. + return $response->withRedirect('./'); + } + + protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response + { + $tags = $this->container->bookmarkService->bookmarksCountPerTag(); + if ($this->container->conf->get('formatter') === 'markdown') { + $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; + } + + $data = [ + 'link' => $link, + 'link_is_new' => $isNew, + 'http_referer' => escape($this->container->environment['HTTP_REFERER'] ?? ''), + 'source' => $request->getParam('source') ?? '', + 'tags' => $tags, + 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false), + ]; + + $data = $this->executeHooks('render_editlink', $data); + + foreach ($data as $key => $value) { + $this->assignView($key, $value); + } + + $editLabel = false === $isNew ? t('Edit') .' ' : ''; + $this->assignView( + 'pagetitle', + $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + + return $response->write($this->render('editlink')); + } + + /** + * @param mixed[] $data Variables passed to the template engine + * + * @return mixed[] Template data after active plugins render_picwall hook execution. + */ + protected function executeHooks(string $hook, array $data): array + { + $this->container->pluginManager->executeHooks( + $hook, + $data + ); + + return $data; + } +} diff --git a/application/front/controller/admin/ToolsController.php b/application/front/controller/admin/ToolsController.php index 66db5ad9..d087f2cd 100644 --- a/application/front/controller/admin/ToolsController.php +++ b/application/front/controller/admin/ToolsController.php @@ -21,7 +21,7 @@ class ToolsController extends ShaarliAdminController 'sslenabled' => is_https($this->container->environment), ]; - $this->executeHooks($data); + $data = $this->executeHooks($data); foreach ($data as $key => $value) { $this->assignView($key, $value); diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index 47e2503a..e5c9ddac 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php @@ -71,7 +71,7 @@ class DailyController extends ShaarliVisitorController ]; // Hooks are called before column construction so that plugins don't have to deal with columns. - $this->executeHooks($data); + $data = $this->executeHooks($data); $data['cols'] = $this->calculateColumns($data['linksToDisplay']); diff --git a/application/front/controller/visitor/FeedController.php b/application/front/controller/visitor/FeedController.php index 70664635..f76f55fd 100644 --- a/application/front/controller/visitor/FeedController.php +++ b/application/front/controller/visitor/FeedController.php @@ -46,7 +46,7 @@ class FeedController extends ShaarliVisitorController $data = $this->container->feedBuilder->buildData($feedType, $request->getParams()); - $this->executeHooks($data, $feedType); + $data = $this->executeHooks($data, $feedType); $this->assignAllView($data); $content = $this->render('feed.'. $feedType); diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php index f12915c1..98423d90 100644 --- a/application/front/controller/visitor/ShaarliVisitorController.php +++ b/application/front/controller/visitor/ShaarliVisitorController.php @@ -78,16 +78,16 @@ abstract class ShaarliVisitorController ]; foreach ($common_hooks as $name) { - $plugin_data = []; + $pluginData = []; $this->container->pluginManager->executeHooks( 'render_' . $name, - $plugin_data, + $pluginData, [ 'target' => $template, 'loggedin' => $this->container->loginManager->isLoggedIn() ] ); - $this->assignView('plugins_' . $name, $plugin_data); + $this->assignView('plugins_' . $name, $pluginData); } } @@ -102,9 +102,10 @@ abstract class ShaarliVisitorController Request $request, Response $response, array $loopTerms = [], - array $clearParams = [] + array $clearParams = [], + string $anchor = null ): Response { - $defaultPath = $request->getUri()->getBasePath(); + $defaultPath = rtrim($request->getUri()->getBasePath(), '/') . '/'; $referer = $this->container->environment['HTTP_REFERER'] ?? null; if (null !== $referer) { @@ -133,7 +134,8 @@ abstract class ShaarliVisitorController } $queryString = count($params) > 0 ? '?'. http_build_query($params) : ''; + $anchor = $anchor ? '#' . $anchor : ''; - return $response->withRedirect($path . $queryString); + return $response->withRedirect($path . $queryString . $anchor); } } -- cgit v1.2.3 From 818b3193ffabec57501e3bdfa997206e3c0671ef Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 13 Jun 2020 11:22:14 +0200 Subject: Explicitly define base and asset path in templates With the new routes, all pages are not all at the same folder level anymore (e.g. /shaare and /shaare/123), so we can't just use './' everywhere. The most consistent way to handle this is to prefix all path with the proper variable, and handle the actual path in controllers. --- .../front/controller/visitor/ShaarliVisitorController.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'application/front/controller') diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php index 98423d90..b90b1e8f 100644 --- a/application/front/controller/visitor/ShaarliVisitorController.php +++ b/application/front/controller/visitor/ShaarliVisitorController.php @@ -60,6 +60,19 @@ abstract class ShaarliVisitorController $this->assignView('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE)); $this->assignView('plugin_errors', $this->container->pluginManager->getErrors()); + /* + * Define base path (if Shaarli is installed in a domain's subfolder, e.g. `/shaarli`) + * and the asset path (subfolder/tpl/default for default theme). + * These MUST be used to create an internal link or to include an asset in templates. + */ + $this->assignView('base_path', $this->container->basePath); + $this->assignView( + 'asset_path', + $this->container->basePath . '/' . + rtrim($this->container->conf->get('resource.raintpl_tpl', 'tpl'), '/') . '/' . + $this->container->conf->get('resource.theme', 'default') + ); + $this->executeDefaultHooks($template); return $this->container->pageBuilder->render($template); @@ -105,7 +118,7 @@ abstract class ShaarliVisitorController array $clearParams = [], string $anchor = null ): Response { - $defaultPath = rtrim($request->getUri()->getBasePath(), '/') . '/'; + $defaultPath = $this->container->basePath . '/'; $referer = $this->container->environment['HTTP_REFERER'] ?? null; if (null !== $referer) { -- cgit v1.2.3 From 9c75f877935fa6adec951a4d8d32b328aaab314f Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 13 Jun 2020 13:08:01 +0200 Subject: Use multi-level routes for existing controllers instead of 1 level everywhere Also prefix most admin routes with /admin/ --- .../front/controller/admin/ConfigureController.php | 6 +++--- application/front/controller/admin/LogoutController.php | 4 ++-- .../front/controller/admin/ManageTagController.php | 10 +++++----- .../front/controller/admin/PasswordController.php | 4 ++-- .../front/controller/admin/PostBookmarkController.php | 17 ++++++++++------- .../front/controller/visitor/LoginController.php | 2 +- .../controller/visitor/ShaarliVisitorController.php | 13 +++++++++++++ application/front/controller/visitor/TagController.php | 8 +++++--- 8 files changed, 41 insertions(+), 23 deletions(-) (limited to 'application/front/controller') diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php index 5a482d8e..44971c43 100644 --- a/application/front/controller/admin/ConfigureController.php +++ b/application/front/controller/admin/ConfigureController.php @@ -19,7 +19,7 @@ use Throwable; class ConfigureController extends ShaarliAdminController { /** - * GET /configure - Displays the configuration page + * GET /admin/configure - Displays the configuration page */ public function index(Request $request, Response $response): Response { @@ -56,7 +56,7 @@ class ConfigureController extends ShaarliAdminController } /** - * POST /configure - Update Shaarli's configuration + * POST /admin/configure - Update Shaarli's configuration */ public function save(Request $request, Response $response): Response { @@ -115,6 +115,6 @@ class ConfigureController extends ShaarliAdminController $this->saveSuccessMessage(t('Configuration was saved.')); - return $response->withRedirect('./configure'); + return $this->redirect($response, '/admin/configure'); } } diff --git a/application/front/controller/admin/LogoutController.php b/application/front/controller/admin/LogoutController.php index 41e81984..c5984814 100644 --- a/application/front/controller/admin/LogoutController.php +++ b/application/front/controller/admin/LogoutController.php @@ -22,8 +22,8 @@ class LogoutController extends ShaarliAdminController $this->container->sessionManager->logout(); // TODO: switch to a simple Cookie manager allowing to check the session, and create mocks. - setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, $this->container->webPath); + setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, $this->container->basePath . '/'); - return $response->withRedirect('./'); + return $this->redirect($response, '/'); } } diff --git a/application/front/controller/admin/ManageTagController.php b/application/front/controller/admin/ManageTagController.php index e015e613..7dab288a 100644 --- a/application/front/controller/admin/ManageTagController.php +++ b/application/front/controller/admin/ManageTagController.php @@ -16,7 +16,7 @@ use Slim\Http\Response; class ManageTagController extends ShaarliAdminController { /** - * GET /manage-tags - Displays the manage tags page + * GET /admin/tags - Displays the manage tags page */ public function index(Request $request, Response $response): Response { @@ -32,7 +32,7 @@ class ManageTagController extends ShaarliAdminController } /** - * POST /manage-tags - Update or delete provided tag + * POST /admin/tags - Update or delete provided tag */ public function save(Request $request, Response $response): Response { @@ -46,7 +46,7 @@ class ManageTagController extends ShaarliAdminController if (0 === strlen($fromTag) || false === $isDelete && 0 === strlen($toTag)) { $this->saveWarningMessage(t('Invalid tags provided.')); - return $response->withRedirect('./manage-tags'); + return $this->redirect($response, '/admin/tags'); } // TODO: move this to bookmark service @@ -80,8 +80,8 @@ class ManageTagController extends ShaarliAdminController $this->saveSuccessMessage($alert); - $redirect = true === $isDelete ? './manage-tags' : './?searchtags='. urlencode($toTag); + $redirect = true === $isDelete ? '/admin/tags' : '/?searchtags='. urlencode($toTag); - return $response->withRedirect($redirect); + return $this->redirect($response, $redirect); } } diff --git a/application/front/controller/admin/PasswordController.php b/application/front/controller/admin/PasswordController.php index 6e8f0bcb..bcce01a6 100644 --- a/application/front/controller/admin/PasswordController.php +++ b/application/front/controller/admin/PasswordController.php @@ -29,7 +29,7 @@ class PasswordController extends ShaarliAdminController } /** - * GET /password - Displays the change password template + * GET /admin/password - Displays the change password template */ public function index(Request $request, Response $response): Response { @@ -37,7 +37,7 @@ class PasswordController extends ShaarliAdminController } /** - * POST /password - Change admin password - existing and new passwords need to be provided. + * POST /admin/password - Change admin password - existing and new passwords need to be provided. */ public function change(Request $request, Response $response): Response { diff --git a/application/front/controller/admin/PostBookmarkController.php b/application/front/controller/admin/PostBookmarkController.php index dbe570e2..f3ee5dea 100644 --- a/application/front/controller/admin/PostBookmarkController.php +++ b/application/front/controller/admin/PostBookmarkController.php @@ -19,7 +19,7 @@ use Slim\Http\Response; class PostBookmarkController extends ShaarliAdminController { /** - * GET /add-shaare - Displays the form used to create a new bookmark from an URL + * GET /admin/add-shaare - Displays the form used to create a new bookmark from an URL */ public function addShaare(Request $request, Response $response): Response { @@ -32,7 +32,7 @@ class PostBookmarkController extends ShaarliAdminController } /** - * GET /shaare - Displays the bookmark form for creation. + * GET /admin/shaare - Displays the bookmark form for creation. * Note that if the URL is found in existing bookmarks, then it will be in edit mode. */ public function displayCreateForm(Request $request, Response $response): Response @@ -93,7 +93,7 @@ class PostBookmarkController extends ShaarliAdminController } /** - * GET /shaare-{id} - Displays the bookmark form in edition mode. + * GET /admin/shaare/{id} - Displays the bookmark form in edition mode. */ public function displayEditForm(Request $request, Response $response, array $args): Response { @@ -106,7 +106,7 @@ class PostBookmarkController extends ShaarliAdminController } catch (BookmarkNotFoundException $e) { $this->saveErrorMessage(t('Bookmark not found')); - return $response->withRedirect('./'); + return $this->redirect($response, '/'); } $formatter = $this->container->formatterFactory->getFormatter('raw'); @@ -116,7 +116,7 @@ class PostBookmarkController extends ShaarliAdminController } /** - * POST /shaare + * POST /admin/shaare */ public function save(Request $request, Response $response): Response { @@ -170,11 +170,14 @@ class PostBookmarkController extends ShaarliAdminController ); } + /** + * GET /admin/shaare/delete + */ public function deleteBookmark(Request $request, Response $response): Response { $this->checkToken($request); - $ids = escape(trim($request->getParam('lf_linkdate'))); + $ids = escape(trim($request->getParam('id'))); if (strpos($ids, ' ') !== false) { // multiple, space-separated ids provided $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'strlen')); @@ -207,7 +210,7 @@ class PostBookmarkController extends ShaarliAdminController } // Don't redirect to where we were previously because the datastore has changed. - return $response->withRedirect('./'); + return $this->redirect($response, '/'); } protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php index 4de2f55d..0db1f463 100644 --- a/application/front/controller/visitor/LoginController.php +++ b/application/front/controller/visitor/LoginController.php @@ -23,7 +23,7 @@ class LoginController extends ShaarliVisitorController if ($this->container->loginManager->isLoggedIn() || $this->container->conf->get('security.open_shaarli', false) ) { - return $response->withRedirect('./'); + return $this->redirect($response, '/'); } $userCanLogin = $this->container->loginManager->canLogin($request->getServerParams()); diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php index b90b1e8f..b494a8e6 100644 --- a/application/front/controller/visitor/ShaarliVisitorController.php +++ b/application/front/controller/visitor/ShaarliVisitorController.php @@ -104,6 +104,19 @@ abstract class ShaarliVisitorController } } + /** + * Simple helper which prepend the base path to redirect path. + * + * @param Response $response + * @param string $path Absolute path, e.g.: `/`, or `/admin/shaare/123` regardless of install directory + * + * @return Response updated + */ + protected function redirect(Response $response, string $path): Response + { + return $response->withRedirect($this->container->basePath . $path); + } + /** * Generates a redirection to the previous page, based on the HTTP_REFERER. * It fails back to the home page. diff --git a/application/front/controller/visitor/TagController.php b/application/front/controller/visitor/TagController.php index a0bc1d1b..c176f43f 100644 --- a/application/front/controller/visitor/TagController.php +++ b/application/front/controller/visitor/TagController.php @@ -11,6 +11,8 @@ use Slim\Http\Response; * Class TagController * * Slim controller handle tags. + * + * TODO: check redirections with new helper */ class TagController extends ShaarliVisitorController { @@ -27,10 +29,10 @@ class TagController extends ShaarliVisitorController // In case browser does not send HTTP_REFERER, we search a single tag if (null === $referer) { if (null !== $newTag) { - return $response->withRedirect('./?searchtags='. urlencode($newTag)); + return $this->redirect($response, '/?searchtags='. urlencode($newTag)); } - return $response->withRedirect('./'); + return $this->redirect($response, '/'); } $currentUrl = parse_url($referer); @@ -81,7 +83,7 @@ class TagController extends ShaarliVisitorController // If the referrer is not provided, we can update the search, so we failback on the bookmark list if (empty($referer)) { - return $response->withRedirect('./'); + return $this->redirect($response, '/'); } $tagToRemove = $args['tag'] ?? null; -- cgit v1.2.3 From baa6979194573855b260593094983c33ec338dc7 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 13 Jun 2020 15:37:02 +0200 Subject: Improve ManageTagController coverage and error handling --- .../controller/admin/ManageShaareController.php | 281 +++++++++++++++++++++ .../controller/admin/PostBookmarkController.php | 261 ------------------- 2 files changed, 281 insertions(+), 261 deletions(-) create mode 100644 application/front/controller/admin/ManageShaareController.php delete mode 100644 application/front/controller/admin/PostBookmarkController.php (limited to 'application/front/controller') diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php new file mode 100644 index 00000000..620bbc40 --- /dev/null +++ b/application/front/controller/admin/ManageShaareController.php @@ -0,0 +1,281 @@ +assignView( + 'pagetitle', + t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + + return $response->write($this->render('addlink')); + } + + /** + * GET /admin/shaare - Displays the bookmark form for creation. + * Note that if the URL is found in existing bookmarks, then it will be in edit mode. + */ + public function displayCreateForm(Request $request, Response $response): Response + { + $url = cleanup_url($request->getParam('post')); + + $linkIsNew = false; + // Check if URL is not already in database (in this case, we will edit the existing link) + $bookmark = $this->container->bookmarkService->findByUrl($url); + if (null === $bookmark) { + $linkIsNew = true; + // Get shaare data if it was provided in URL (e.g.: by the bookmarklet). + $title = $request->getParam('title'); + $description = $request->getParam('description'); + $tags = $request->getParam('tags'); + $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN); + + // If this is an HTTP(S) link, we try go get the page to extract + // the title (otherwise we will to straight to the edit form.) + if (empty($title) && strpos(get_url_scheme($url) ?: '', 'http') !== false) { + $retrieveDescription = $this->container->conf->get('general.retrieve_description'); + // Short timeout to keep the application responsive + // The callback will fill $charset and $title with data from the downloaded page. + $this->container->httpAccess->getHttpResponse( + $url, + $this->container->conf->get('general.download_timeout', 30), + $this->container->conf->get('general.download_max_size', 4194304), + $this->container->httpAccess->getCurlDownloadCallback( + $charset, + $title, + $description, + $tags, + $retrieveDescription + ) + ); + if (! empty($title) && strtolower($charset) !== 'utf-8') { + $title = mb_convert_encoding($title, 'utf-8', $charset); + } + } + + if (empty($url) && empty($title)) { + $title = $this->container->conf->get('general.default_note_title', t('Note: ')); + } + + $link = escape([ + 'title' => $title, + 'url' => $url ?? '', + 'description' => $description ?? '', + 'tags' => $tags ?? '', + 'private' => $private, + ]); + } else { + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $link = $formatter->format($bookmark); + } + + return $this->displayForm($link, $linkIsNew, $request, $response); + } + + /** + * GET /admin/shaare/{id} - Displays the bookmark form in edition mode. + */ + public function displayEditForm(Request $request, Response $response, array $args): Response + { + $id = $args['id'] ?? ''; + try { + if (false === ctype_digit($id)) { + throw new BookmarkNotFoundException(); + } + $bookmark = $this->container->bookmarkService->get((int) $id); // Read database + } catch (BookmarkNotFoundException $e) { + $this->saveErrorMessage(sprintf( + t('Bookmark with identifier %s could not be found.'), + $id + )); + + return $this->redirect($response, '/'); + } + + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $link = $formatter->format($bookmark); + + return $this->displayForm($link, false, $request, $response); + } + + /** + * POST /admin/shaare + */ + public function save(Request $request, Response $response): Response + { + $this->checkToken($request); + + // lf_id should only be present if the link exists. + $id = $request->getParam('lf_id') ? intval(escape($request->getParam('lf_id'))) : null; + if (null !== $id && true === $this->container->bookmarkService->exists($id)) { + // Edit + $bookmark = $this->container->bookmarkService->get($id); + } else { + // New link + $bookmark = new Bookmark(); + } + + $bookmark->setTitle($request->getParam('lf_title')); + $bookmark->setDescription($request->getParam('lf_description')); + $bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', [])); + $bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN)); + $bookmark->setTagsString($request->getParam('lf_tags')); + + if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE + && false === $bookmark->isNote() + ) { + $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); + } + $this->container->bookmarkService->addOrSet($bookmark, false); + + // To preserve backward compatibility with 3rd parties, plugins still use arrays + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $data = $formatter->format($bookmark); + $data = $this->executeHooks('save_link', $data); + + $bookmark->fromArray($data); + $this->container->bookmarkService->set($bookmark); + + // If we are called from the bookmarklet, we must close the popup: + if ($request->getParam('source') === 'bookmarklet') { + return $response->write(''); + } + + if (!empty($request->getParam('returnurl'))) { + $this->container->environment['HTTP_REFERER'] = escape($request->getParam('returnurl')); + } + + return $this->redirectFromReferer( + $request, + $response, + ['add-shaare', 'shaare'], ['addlink', 'post', 'edit_link'], + $bookmark->getShortUrl() + ); + } + + /** + * GET /admin/shaare/delete + */ + public function deleteBookmark(Request $request, Response $response): Response + { + $this->checkToken($request); + + $ids = escape(trim($request->getParam('id') ?? '')); + if (empty($ids) || strpos($ids, ' ') !== false) { + // multiple, space-separated ids provided + $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); + } else { + $ids = [$ids]; + } + + // assert at least one id is given + if (0 === count($ids)) { + $this->saveErrorMessage(t('Invalid bookmark ID provided.')); + + return $this->redirectFromReferer($request, $response, [], ['delete-shaare']); + } + + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $count = 0; + foreach ($ids as $id) { + try { + $bookmark = $this->container->bookmarkService->get((int) $id); + } catch (BookmarkNotFoundException $e) { + $this->saveErrorMessage(sprintf( + t('Bookmark with identifier %s could not be found.'), + $id + )); + + continue; + } + + $data = $formatter->format($bookmark); + $this->container->pluginManager->executeHooks('delete_link', $data); + $this->container->bookmarkService->remove($bookmark, false); + ++ $count; + } + + if ($count > 0) { + $this->container->bookmarkService->save(); + } + + // If we are called from the bookmarklet, we must close the popup: + if ($request->getParam('source') === 'bookmarklet') { + return $response->write(''); + } + + // Don't redirect to where we were previously because the datastore has changed. + return $this->redirect($response, '/'); + } + + /** + * Helper function used to display the shaare form whether it's a new or existing bookmark. + * + * @param array $link data used in template, either from parameters or from the data store + */ + protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response + { + $tags = $this->container->bookmarkService->bookmarksCountPerTag(); + if ($this->container->conf->get('formatter') === 'markdown') { + $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; + } + + $data = [ + 'link' => $link, + 'link_is_new' => $isNew, + 'http_referer' => escape($this->container->environment['HTTP_REFERER'] ?? ''), + 'source' => $request->getParam('source') ?? '', + 'tags' => $tags, + 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false), + ]; + + $data = $this->executeHooks('render_editlink', $data); + + foreach ($data as $key => $value) { + $this->assignView($key, $value); + } + + $editLabel = false === $isNew ? t('Edit') .' ' : ''; + $this->assignView( + 'pagetitle', + $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + + return $response->write($this->render('editlink')); + } + + /** + * @param mixed[] $data Variables passed to the template engine + * + * @return mixed[] Template data after active plugins render_picwall hook execution. + */ + protected function executeHooks(string $hook, array $data): array + { + $this->container->pluginManager->executeHooks( + $hook, + $data + ); + + return $data; + } +} diff --git a/application/front/controller/admin/PostBookmarkController.php b/application/front/controller/admin/PostBookmarkController.php deleted file mode 100644 index f3ee5dea..00000000 --- a/application/front/controller/admin/PostBookmarkController.php +++ /dev/null @@ -1,261 +0,0 @@ -assignView( - 'pagetitle', - t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli') - ); - - return $response->write($this->render('addlink')); - } - - /** - * GET /admin/shaare - Displays the bookmark form for creation. - * Note that if the URL is found in existing bookmarks, then it will be in edit mode. - */ - public function displayCreateForm(Request $request, Response $response): Response - { - $url = cleanup_url($request->getParam('post')); - - $linkIsNew = false; - // Check if URL is not already in database (in this case, we will edit the existing link) - $bookmark = $this->container->bookmarkService->findByUrl($url); - if (null === $bookmark) { - $linkIsNew = true; - // Get shaare data if it was provided in URL (e.g.: by the bookmarklet). - $title = $request->getParam('title'); - $description = $request->getParam('description'); - $tags = $request->getParam('tags'); - $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN); - - // If this is an HTTP(S) link, we try go get the page to extract - // the title (otherwise we will to straight to the edit form.) - if (empty($title) && strpos(get_url_scheme($url) ?: '', 'http') !== false) { - $retrieveDescription = $this->container->conf->get('general.retrieve_description'); - // Short timeout to keep the application responsive - // The callback will fill $charset and $title with data from the downloaded page. - $this->container->httpAccess->getHttpResponse( - $url, - $this->container->conf->get('general.download_timeout', 30), - $this->container->conf->get('general.download_max_size', 4194304), - $this->container->httpAccess->getCurlDownloadCallback( - $charset, - $title, - $description, - $tags, - $retrieveDescription - ) - ); - if (! empty($title) && strtolower($charset) !== 'utf-8') { - $title = mb_convert_encoding($title, 'utf-8', $charset); - } - } - - if (empty($url) && empty($title)) { - $title = $this->container->conf->get('general.default_note_title', t('Note: ')); - } - - $link = escape([ - 'title' => $title, - 'url' => $url ?? '', - 'description' => $description ?? '', - 'tags' => $tags ?? '', - 'private' => $private, - ]); - } else { - $formatter = $this->container->formatterFactory->getFormatter('raw'); - $link = $formatter->format($bookmark); - } - - return $this->displayForm($link, $linkIsNew, $request, $response); - } - - /** - * GET /admin/shaare/{id} - Displays the bookmark form in edition mode. - */ - public function displayEditForm(Request $request, Response $response, array $args): Response - { - $id = $args['id']; - try { - if (false === ctype_digit($id)) { - throw new BookmarkNotFoundException(); - } - $bookmark = $this->container->bookmarkService->get($id); // Read database - } catch (BookmarkNotFoundException $e) { - $this->saveErrorMessage(t('Bookmark not found')); - - return $this->redirect($response, '/'); - } - - $formatter = $this->container->formatterFactory->getFormatter('raw'); - $link = $formatter->format($bookmark); - - return $this->displayForm($link, false, $request, $response); - } - - /** - * POST /admin/shaare - */ - public function save(Request $request, Response $response): Response - { - $this->checkToken($request); - - // lf_id should only be present if the link exists. - $id = $request->getParam('lf_id') ? intval(escape($request->getParam('lf_id'))) : null; - if (null !== $id && true === $this->container->bookmarkService->exists($id)) { - // Edit - $bookmark = $this->container->bookmarkService->get($id); - } else { - // New link - $bookmark = new Bookmark(); - } - - $bookmark->setTitle($request->getParam('lf_title')); - $bookmark->setDescription($request->getParam('lf_description')); - $bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', [])); - $bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN)); - $bookmark->setTagsString($request->getParam('lf_tags')); - - if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE - && false === $bookmark->isNote() - ) { - $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); - } - $this->container->bookmarkService->addOrSet($bookmark, false); - - // To preserve backward compatibility with 3rd parties, plugins still use arrays - $formatter = $this->container->formatterFactory->getFormatter('raw'); - $data = $formatter->format($bookmark); - $data = $this->executeHooks('save_link', $data); - - $bookmark->fromArray($data); - $this->container->bookmarkService->set($bookmark); - - // If we are called from the bookmarklet, we must close the popup: - if ($request->getParam('source') === 'bookmarklet') { - return $response->write(''); - } - - if (!empty($request->getParam('returnurl'))) { - $this->container->environment['HTTP_REFERER'] = escape($request->getParam('returnurl')); - } - - return $this->redirectFromReferer( - $request, - $response, - ['add-shaare', 'shaare'], ['addlink', 'post', 'edit_link'], - $bookmark->getShortUrl() - ); - } - - /** - * GET /admin/shaare/delete - */ - public function deleteBookmark(Request $request, Response $response): Response - { - $this->checkToken($request); - - $ids = escape(trim($request->getParam('id'))); - if (strpos($ids, ' ') !== false) { - // multiple, space-separated ids provided - $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'strlen')); - } else { - $ids = [$ids]; - } - - // assert at least one id is given - if (0 === count($ids)) { - $this->saveErrorMessage(t('Invalid bookmark ID provided.')); - - return $this->redirectFromReferer($request, $response, [], ['delete-shaare']); - } - - $formatter = $this->container->formatterFactory->getFormatter('raw'); - foreach ($ids as $id) { - $id = (int) $id; - // TODO: check if it exists - $bookmark = $this->container->bookmarkService->get($id); - $data = $formatter->format($bookmark); - $this->container->pluginManager->executeHooks('delete_link', $data); - $this->container->bookmarkService->remove($bookmark, false); - } - - $this->container->bookmarkService->save(); - - // If we are called from the bookmarklet, we must close the popup: - if ($request->getParam('source') === 'bookmarklet') { - return $response->write(''); - } - - // Don't redirect to where we were previously because the datastore has changed. - return $this->redirect($response, '/'); - } - - protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response - { - $tags = $this->container->bookmarkService->bookmarksCountPerTag(); - if ($this->container->conf->get('formatter') === 'markdown') { - $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; - } - - $data = [ - 'link' => $link, - 'link_is_new' => $isNew, - 'http_referer' => escape($this->container->environment['HTTP_REFERER'] ?? ''), - 'source' => $request->getParam('source') ?? '', - 'tags' => $tags, - 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false), - ]; - - $data = $this->executeHooks('render_editlink', $data); - - foreach ($data as $key => $value) { - $this->assignView($key, $value); - } - - $editLabel = false === $isNew ? t('Edit') .' ' : ''; - $this->assignView( - 'pagetitle', - $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli') - ); - - return $response->write($this->render('editlink')); - } - - /** - * @param mixed[] $data Variables passed to the template engine - * - * @return mixed[] Template data after active plugins render_picwall hook execution. - */ - protected function executeHooks(string $hook, array $data): array - { - $this->container->pluginManager->executeHooks( - $hook, - $data - ); - - return $data; - } -} -- cgit v1.2.3 From 7b8a6f2858248601d43c1b8247deb91b74392d2e Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 13 Jun 2020 19:40:32 +0200 Subject: Process change visibility action through Slim controller --- .../controller/admin/ManageShaareController.php | 70 +++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) (limited to 'application/front/controller') diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php index 620bbc40..ff330a99 100644 --- a/application/front/controller/admin/ManageShaareController.php +++ b/application/front/controller/admin/ManageShaareController.php @@ -174,7 +174,7 @@ class ManageShaareController extends ShaarliAdminController } /** - * GET /admin/shaare/delete + * GET /admin/shaare/delete - Delete one or multiple bookmarks (depending on `id` query parameter). */ public function deleteBookmark(Request $request, Response $response): Response { @@ -228,6 +228,74 @@ class ManageShaareController extends ShaarliAdminController return $this->redirect($response, '/'); } + /** + * GET /admin/shaare/visibility + * + * Change visibility (public/private) of one or multiple bookmarks (depending on `id` query parameter). + */ + public function changeVisibility(Request $request, Response $response): Response + { + $this->checkToken($request); + + $ids = trim(escape($request->getParam('id') ?? '')); + if (empty($ids) || strpos($ids, ' ') !== false) { + // multiple, space-separated ids provided + $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit')); + } else { + // only a single id provided + $ids = [$ids]; + } + + // assert at least one id is given + if (0 === count($ids)) { + $this->saveErrorMessage(t('Invalid bookmark ID provided.')); + + return $this->redirectFromReferer($request, $response, [], ['change_visibility']); + } + + // assert that the visibility is valid + $visibility = $request->getParam('newVisibility'); + if (null === $visibility || false === in_array($visibility, ['public', 'private'], true)) { + $this->saveErrorMessage(t('Invalid visibility provided.')); + + return $this->redirectFromReferer($request, $response, [], ['change_visibility']); + } else { + $isPrivate = $visibility === 'private'; + } + + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $count = 0; + + foreach ($ids as $id) { + try { + $bookmark = $this->container->bookmarkService->get((int) $id); + } catch (BookmarkNotFoundException $e) { + $this->saveErrorMessage(sprintf( + t('Bookmark with identifier %s could not be found.'), + $id + )); + + continue; + } + + $bookmark->setPrivate($isPrivate); + + // To preserve backward compatibility with 3rd parties, plugins still use arrays + $data = $formatter->format($bookmark); + $this->container->pluginManager->executeHooks('save_link', $data); + $bookmark->fromArray($data); + + $this->container->bookmarkService->set($bookmark, false); + ++$count; + } + + if ($count > 0) { + $this->container->bookmarkService->save(); + } + + return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']); + } + /** * Helper function used to display the shaare form whether it's a new or existing bookmark. * -- cgit v1.2.3 From 3447d888d7881eed437117a6de2450abb96f6a76 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 15 Jun 2020 08:15:40 +0200 Subject: Pin bookmarks through Slim controller --- .../controller/admin/ManageShaareController.php | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) (limited to 'application/front/controller') diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php index ff330a99..bdfc5ca7 100644 --- a/application/front/controller/admin/ManageShaareController.php +++ b/application/front/controller/admin/ManageShaareController.php @@ -296,6 +296,42 @@ class ManageShaareController extends ShaarliAdminController return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']); } + /** + * GET /admin/shaare/{id}/pin - Pin or unpin a bookmark. + */ + public function pinBookmark(Request $request, Response $response, array $args): Response + { + $this->checkToken($request); + + $id = $args['id'] ?? ''; + try { + if (false === ctype_digit($id)) { + throw new BookmarkNotFoundException(); + } + $bookmark = $this->container->bookmarkService->get((int) $id); // Read database + } catch (BookmarkNotFoundException $e) { + $this->saveErrorMessage(sprintf( + t('Bookmark with identifier %s could not be found.'), + $id + )); + + return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); + } + + $formatter = $this->container->formatterFactory->getFormatter('raw'); + + $bookmark->setSticky(!$bookmark->isSticky()); + + // To preserve backward compatibility with 3rd parties, plugins still use arrays + $data = $formatter->format($bookmark); + $this->container->pluginManager->executeHooks('save_link', $data); + $bookmark->fromArray($data); + + $this->container->bookmarkService->set($bookmark); + + return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']); + } + /** * Helper function used to display the shaare form whether it's a new or existing bookmark. * -- cgit v1.2.3 From c70ff64a61d62cc8d35a62f30596ecc2a3c578a3 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 17 Jun 2020 16:04:18 +0200 Subject: Process bookmark exports through Slim controllers --- .../front/controller/admin/ExportController.php | 92 ++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 application/front/controller/admin/ExportController.php (limited to 'application/front/controller') diff --git a/application/front/controller/admin/ExportController.php b/application/front/controller/admin/ExportController.php new file mode 100644 index 00000000..8e0e5a56 --- /dev/null +++ b/application/front/controller/admin/ExportController.php @@ -0,0 +1,92 @@ +assignView('pagetitle', t('Export') .' - '. $this->container->conf->get('general.title', 'Shaarli')); + + return $response->write($this->render('export')); + } + + /** + * POST /admin/export - Process export, and serve download file named + * bookmarks_(all|private|public)_datetime.html + */ + public function export(Request $request, Response $response): Response + { + $selection = $request->getParam('selection'); + + if (empty($selection)) { + $this->saveErrorMessage(t('Please select an export mode.')); + + return $this->redirect($response, '/admin/export'); + } + + $prependNoteUrl = filter_var($request->getParam('prepend_note_url') ?? false, FILTER_VALIDATE_BOOLEAN); + + try { + $formatter = $this->container->formatterFactory->getFormatter('raw'); + + $this->assignView( + 'links', + $this->container->netscapeBookmarkUtils->filterAndFormat( + $formatter, + $selection, + $prependNoteUrl, + index_url($this->container->environment) + ) + ); + } catch (\Exception $exc) { + $this->saveErrorMessage($exc->getMessage()); + + return $this->redirect($response, '/admin/export'); + } + + $now = new DateTime(); + $response = $response->withHeader('Content-Type', 'text/html; charset=utf-8'); + $response = $response->withHeader( + 'Content-disposition', + 'attachment; filename=bookmarks_'.$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html' + ); + + $this->assignView('date', $now->format(DateTime::RFC822)); + $this->assignView('eol', PHP_EOL); + $this->assignView('selection', $selection); + + return $response->write($this->render('export.bookmarks')); + } + + /** + * @param mixed[] $data Variables passed to the template engine + * + * @return mixed[] Template data after active plugins render_picwall hook execution. + */ + protected function executeHooks(array $data): array + { + $this->container->pluginManager->executeHooks( + 'render_tools', + $data + ); + + return $data; + } +} -- cgit v1.2.3 From 78657347c5b463d7c22bfc8c87b7db39fe058833 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 17 Jun 2020 19:08:02 +0200 Subject: Process bookmarks import through Slim controller --- .../front/controller/admin/ExportController.php | 17 +---- .../front/controller/admin/ImportController.php | 81 ++++++++++++++++++++++ 2 files changed, 83 insertions(+), 15 deletions(-) create mode 100644 application/front/controller/admin/ImportController.php (limited to 'application/front/controller') diff --git a/application/front/controller/admin/ExportController.php b/application/front/controller/admin/ExportController.php index 8e0e5a56..7afbfc23 100644 --- a/application/front/controller/admin/ExportController.php +++ b/application/front/controller/admin/ExportController.php @@ -33,6 +33,8 @@ class ExportController extends ShaarliAdminController */ public function export(Request $request, Response $response): Response { + $this->checkToken($request); + $selection = $request->getParam('selection'); if (empty($selection)) { @@ -74,19 +76,4 @@ class ExportController extends ShaarliAdminController return $response->write($this->render('export.bookmarks')); } - - /** - * @param mixed[] $data Variables passed to the template engine - * - * @return mixed[] Template data after active plugins render_picwall hook execution. - */ - protected function executeHooks(array $data): array - { - $this->container->pluginManager->executeHooks( - 'render_tools', - $data - ); - - return $data; - } } diff --git a/application/front/controller/admin/ImportController.php b/application/front/controller/admin/ImportController.php new file mode 100644 index 00000000..8c5305b9 --- /dev/null +++ b/application/front/controller/admin/ImportController.php @@ -0,0 +1,81 @@ +assignView( + 'maxfilesize', + get_max_upload_size( + ini_get('post_max_size'), + ini_get('upload_max_filesize'), + false + ) + ); + $this->assignView( + 'maxfilesizeHuman', + get_max_upload_size( + ini_get('post_max_size'), + ini_get('upload_max_filesize'), + true + ) + ); + $this->assignView('pagetitle', t('Import') .' - '. $this->container->conf->get('general.title', 'Shaarli')); + + return $response->write($this->render('import')); + } + + /** + * POST /admin/import - Process import file provided and create bookmarks + */ + public function import(Request $request, Response $response): Response + { + $this->checkToken($request); + + $file = ($request->getUploadedFiles() ?? [])['filetoupload'] ?? null; + if (!$file instanceof UploadedFileInterface) { + $this->saveErrorMessage(t('No import file provided.')); + + return $this->redirect($response, '/admin/import'); + } + + + // Import bookmarks from an uploaded file + if (0 === $file->getSize()) { + // The file is too big or some form field may be missing. + $msg = sprintf( + t( + 'The file you are trying to upload is probably bigger than what this webserver can accept' + .' (%s). Please upload in smaller chunks.' + ), + get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')) + ); + $this->saveErrorMessage($msg); + + return $this->redirect($response, '/admin/import'); + } + + $status = $this->container->netscapeBookmarkUtils->import($request->getParams(), $file); + + $this->saveSuccessMessage($status); + + return $this->redirect($response, '/admin/import'); + } +} -- cgit v1.2.3 From 1b8620b1ad4e2c647ff2d032c8e7c6687b6647a1 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 20 Jun 2020 15:14:24 +0200 Subject: Process plugins administration page through Slim controllers --- .../front/controller/admin/PluginsController.php | 98 ++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 application/front/controller/admin/PluginsController.php (limited to 'application/front/controller') diff --git a/application/front/controller/admin/PluginsController.php b/application/front/controller/admin/PluginsController.php new file mode 100644 index 00000000..d5ec91f0 --- /dev/null +++ b/application/front/controller/admin/PluginsController.php @@ -0,0 +1,98 @@ +container->pluginManager->getPluginsMeta(); + + // Split plugins into 2 arrays: ordered enabled plugins and disabled. + $enabledPlugins = array_filter($pluginMeta, function ($v) { + return ($v['order'] ?? false) !== false; + }); + $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $this->container->conf->get('plugins', [])); + uasort( + $enabledPlugins, + function ($a, $b) { + return $a['order'] - $b['order']; + } + ); + $disabledPlugins = array_filter($pluginMeta, function ($v) { + return ($v['order'] ?? false) === false; + }); + + $this->assignView('enabledPlugins', $enabledPlugins); + $this->assignView('disabledPlugins', $disabledPlugins); + $this->assignView( + 'pagetitle', + t('Plugin Administration') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + + return $response->write($this->render('pluginsadmin')); + } + + /** + * POST /admin/plugins - Update Shaarli's configuration + */ + public function save(Request $request, Response $response): Response + { + $this->checkToken($request); + + try { + $parameters = $request->getParams() ?? []; + + $this->executeHooks($parameters); + + if (isset($parameters['parameters_form'])) { + unset($parameters['parameters_form']); + foreach ($parameters as $param => $value) { + $this->container->conf->set('plugins.'. $param, escape($value)); + } + } else { + $this->container->conf->set('general.enabled_plugins', save_plugin_config($parameters)); + } + + $this->container->conf->write($this->container->loginManager->isLoggedIn()); + $this->container->history->updateSettings(); + + $this->saveSuccessMessage(t('Setting successfully saved.')); + } catch (Exception $e) { + $this->saveErrorMessage( + t('ERROR while saving plugin configuration: ') . PHP_EOL . $e->getMessage() + ); + } + + return $this->redirect($response, '/admin/plugins'); + } + + /** + * @param mixed[] $data Variables passed to the template engine + * + * @return mixed[] Template data after active plugins render_picwall hook execution. + */ + protected function executeHooks(array $data): array + { + $this->container->pluginManager->executeHooks( + 'save_plugin_parameters', + $data + ); + + return $data; + } +} -- cgit v1.2.3 From 764d34a7d347d653414e5f5c632e02499edaef04 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 21 Jun 2020 12:21:31 +0200 Subject: Process token retrieve through Slim controller --- .../front/controller/admin/TokenController.php | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 application/front/controller/admin/TokenController.php (limited to 'application/front/controller') diff --git a/application/front/controller/admin/TokenController.php b/application/front/controller/admin/TokenController.php new file mode 100644 index 00000000..08d68d0a --- /dev/null +++ b/application/front/controller/admin/TokenController.php @@ -0,0 +1,26 @@ +withHeader('Content-Type', 'text/plain'); + + return $response->write($this->container->sessionManager->generateToken()); + } +} -- cgit v1.2.3 From 6132d64748dfc6806ed25f71d2e078a5ed29d071 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 27 Jun 2020 12:08:26 +0200 Subject: Process thumbnail synchronize page through Slim controllers --- .../front/controller/admin/ConfigureController.php | 2 +- .../controller/admin/ThumbnailsController.php | 79 ++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 application/front/controller/admin/ThumbnailsController.php (limited to 'application/front/controller') diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php index 44971c43..201a859b 100644 --- a/application/front/controller/admin/ConfigureController.php +++ b/application/front/controller/admin/ConfigureController.php @@ -99,7 +99,7 @@ class ConfigureController extends ShaarliAdminController ) { $this->saveWarningMessage(t( 'You have enabled or changed thumbnails mode. ' - .'Please synchronize them.' + .'Please synchronize them.' )); } $this->container->conf->set('thumbnails.mode', $thumbnailsMode); diff --git a/application/front/controller/admin/ThumbnailsController.php b/application/front/controller/admin/ThumbnailsController.php new file mode 100644 index 00000000..e5308510 --- /dev/null +++ b/application/front/controller/admin/ThumbnailsController.php @@ -0,0 +1,79 @@ +container->bookmarkService->search() as $bookmark) { + // A note or not HTTP(S) + if ($bookmark->isNote() || !startsWith(strtolower($bookmark->getUrl()), 'http')) { + continue; + } + + $ids[] = $bookmark->getId(); + } + + $this->assignView('ids', $ids); + $this->assignView( + 'pagetitle', + t('Thumbnails update') .' - '. $this->container->conf->get('general.title', 'Shaarli') + ); + + return $response->write($this->render('thumbnails')); + } + + /** + * PATCH /admin/shaare/{id}/thumbnail-update - Route for AJAX calls + */ + public function ajaxUpdate(Request $request, Response $response, array $args): Response + { + $id = $args['id'] ?? null; + + if (false === ctype_digit($id)) { + return $response->withStatus(400); + } + + try { + $bookmark = $this->container->bookmarkService->get($id); + } catch (BookmarkNotFoundException $e) { + return $response->withStatus(404); + } + + $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); + $this->container->bookmarkService->set($bookmark); + + return $response->withJson($this->container->formatterFactory->getFormatter('raw')->format($bookmark)); + } + + /** + * @param mixed[] $data Variables passed to the template engine + * + * @return mixed[] Template data after active plugins render_picwall hook execution. + */ + protected function executeHooks(array $data): array + { + $this->container->pluginManager->executeHooks( + 'render_tools', + $data + ); + + return $data; + } +} -- cgit v1.2.3 From 1a8ac737e52cb25a5c346232ee398f5908cee7d7 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 6 Jul 2020 08:04:35 +0200 Subject: Process main page (linklist) through Slim controller Including a bunch of improvements on the container, and helper used across new controllers. --- .../front/controller/admin/ConfigureController.php | 3 +- .../front/controller/admin/ExportController.php | 5 +- .../front/controller/admin/ImportController.php | 3 +- .../controller/admin/ManageShaareController.php | 5 +- .../front/controller/admin/ManageTagController.php | 3 +- .../front/controller/admin/PasswordController.php | 9 +- .../front/controller/admin/PluginsController.php | 3 +- .../controller/admin/ThumbnailsController.php | 18 +- .../front/controller/admin/ToolsController.php | 3 +- .../controller/visitor/BookmarkListController.php | 248 +++++++++++++++++++++ .../front/controller/visitor/DailyController.php | 5 +- .../front/controller/visitor/LoginController.php | 3 +- .../controller/visitor/OpenSearchController.php | 3 +- .../controller/visitor/PictureWallController.php | 3 +- 14 files changed, 280 insertions(+), 34 deletions(-) create mode 100644 application/front/controller/visitor/BookmarkListController.php (limited to 'application/front/controller') diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php index 201a859b..865fc2b0 100644 --- a/application/front/controller/admin/ConfigureController.php +++ b/application/front/controller/admin/ConfigureController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Admin; use Shaarli\Languages; +use Shaarli\Render\TemplatePage; use Shaarli\Render\ThemeUtils; use Shaarli\Thumbnailer; use Slim\Http\Request; @@ -52,7 +53,7 @@ class ConfigureController extends ShaarliAdminController $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)); $this->assignView('pagetitle', t('Configure') .' - '. $this->container->conf->get('general.title', 'Shaarli')); - return $response->write($this->render('configure')); + return $response->write($this->render(TemplatePage::CONFIGURE)); } /** diff --git a/application/front/controller/admin/ExportController.php b/application/front/controller/admin/ExportController.php index 7afbfc23..2be957fa 100644 --- a/application/front/controller/admin/ExportController.php +++ b/application/front/controller/admin/ExportController.php @@ -6,6 +6,7 @@ namespace Shaarli\Front\Controller\Admin; use DateTime; use Shaarli\Bookmark\Bookmark; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -24,7 +25,7 @@ class ExportController extends ShaarliAdminController { $this->assignView('pagetitle', t('Export') .' - '. $this->container->conf->get('general.title', 'Shaarli')); - return $response->write($this->render('export')); + return $response->write($this->render(TemplatePage::EXPORT)); } /** @@ -74,6 +75,6 @@ class ExportController extends ShaarliAdminController $this->assignView('eol', PHP_EOL); $this->assignView('selection', $selection); - return $response->write($this->render('export.bookmarks')); + return $response->write($this->render(TemplatePage::NETSCAPE_EXPORT_BOOKMARKS)); } } diff --git a/application/front/controller/admin/ImportController.php b/application/front/controller/admin/ImportController.php index 8c5305b9..758d5ef9 100644 --- a/application/front/controller/admin/ImportController.php +++ b/application/front/controller/admin/ImportController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Admin; use Psr\Http\Message\UploadedFileInterface; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -39,7 +40,7 @@ class ImportController extends ShaarliAdminController ); $this->assignView('pagetitle', t('Import') .' - '. $this->container->conf->get('general.title', 'Shaarli')); - return $response->write($this->render('import')); + return $response->write($this->render(TemplatePage::IMPORT)); } /** diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php index bdfc5ca7..3aa48423 100644 --- a/application/front/controller/admin/ManageShaareController.php +++ b/application/front/controller/admin/ManageShaareController.php @@ -7,6 +7,7 @@ namespace Shaarli\Front\Controller\Admin; use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Formatter\BookmarkMarkdownFormatter; +use Shaarli\Render\TemplatePage; use Shaarli\Thumbnailer; use Slim\Http\Request; use Slim\Http\Response; @@ -28,7 +29,7 @@ class ManageShaareController extends ShaarliAdminController t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli') ); - return $response->write($this->render('addlink')); + return $response->write($this->render(TemplatePage::ADDLINK)); } /** @@ -365,7 +366,7 @@ class ManageShaareController extends ShaarliAdminController $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli') ); - return $response->write($this->render('editlink')); + return $response->write($this->render(TemplatePage::EDIT_LINK)); } /** diff --git a/application/front/controller/admin/ManageTagController.php b/application/front/controller/admin/ManageTagController.php index 7dab288a..0380ef1f 100644 --- a/application/front/controller/admin/ManageTagController.php +++ b/application/front/controller/admin/ManageTagController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Admin; use Shaarli\Bookmark\BookmarkFilter; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -28,7 +29,7 @@ class ManageTagController extends ShaarliAdminController t('Manage tags') .' - '. $this->container->conf->get('general.title', 'Shaarli') ); - return $response->write($this->render('changetag')); + return $response->write($this->render(TemplatePage::CHANGE_TAG)); } /** diff --git a/application/front/controller/admin/PasswordController.php b/application/front/controller/admin/PasswordController.php index bcce01a6..5ec0d24b 100644 --- a/application/front/controller/admin/PasswordController.php +++ b/application/front/controller/admin/PasswordController.php @@ -7,6 +7,7 @@ namespace Shaarli\Front\Controller\Admin; use Shaarli\Container\ShaarliContainer; use Shaarli\Front\Exception\OpenShaarliPasswordException; use Shaarli\Front\Exception\ShaarliFrontException; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; use Throwable; @@ -33,7 +34,7 @@ class PasswordController extends ShaarliAdminController */ public function index(Request $request, Response $response): Response { - return $response->write($this->render('changepassword')); + return $response->write($this->render(TemplatePage::CHANGE_PASSWORD)); } /** @@ -55,7 +56,7 @@ class PasswordController extends ShaarliAdminController return $response ->withStatus(400) - ->write($this->render('changepassword')) + ->write($this->render(TemplatePage::CHANGE_PASSWORD)) ; } @@ -71,7 +72,7 @@ class PasswordController extends ShaarliAdminController return $response ->withStatus(400) - ->write($this->render('changepassword')) + ->write($this->render(TemplatePage::CHANGE_PASSWORD)) ; } @@ -95,6 +96,6 @@ class PasswordController extends ShaarliAdminController $this->saveSuccessMessage(t('Your password has been changed')); - return $response->write($this->render('changepassword')); + return $response->write($this->render(TemplatePage::CHANGE_PASSWORD)); } } diff --git a/application/front/controller/admin/PluginsController.php b/application/front/controller/admin/PluginsController.php index d5ec91f0..44025395 100644 --- a/application/front/controller/admin/PluginsController.php +++ b/application/front/controller/admin/PluginsController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Admin; use Exception; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -44,7 +45,7 @@ class PluginsController extends ShaarliAdminController t('Plugin Administration') .' - '. $this->container->conf->get('general.title', 'Shaarli') ); - return $response->write($this->render('pluginsadmin')); + return $response->write($this->render(TemplatePage::PLUGINS_ADMIN)); } /** diff --git a/application/front/controller/admin/ThumbnailsController.php b/application/front/controller/admin/ThumbnailsController.php index e5308510..81c87ed0 100644 --- a/application/front/controller/admin/ThumbnailsController.php +++ b/application/front/controller/admin/ThumbnailsController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Admin; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -36,7 +37,7 @@ class ThumbnailsController extends ShaarliAdminController t('Thumbnails update') .' - '. $this->container->conf->get('general.title', 'Shaarli') ); - return $response->write($this->render('thumbnails')); + return $response->write($this->render(TemplatePage::THUMBNAILS)); } /** @@ -61,19 +62,4 @@ class ThumbnailsController extends ShaarliAdminController return $response->withJson($this->container->formatterFactory->getFormatter('raw')->format($bookmark)); } - - /** - * @param mixed[] $data Variables passed to the template engine - * - * @return mixed[] Template data after active plugins render_picwall hook execution. - */ - protected function executeHooks(array $data): array - { - $this->container->pluginManager->executeHooks( - 'render_tools', - $data - ); - - return $data; - } } diff --git a/application/front/controller/admin/ToolsController.php b/application/front/controller/admin/ToolsController.php index d087f2cd..a476e898 100644 --- a/application/front/controller/admin/ToolsController.php +++ b/application/front/controller/admin/ToolsController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Admin; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -29,7 +30,7 @@ class ToolsController extends ShaarliAdminController $this->assignView('pagetitle', t('Tools') .' - '. $this->container->conf->get('general.title', 'Shaarli')); - return $response->write($this->render('tools')); + return $response->write($this->render(TemplatePage::TOOLS)); } /** diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php new file mode 100644 index 00000000..a37a7f6b --- /dev/null +++ b/application/front/controller/visitor/BookmarkListController.php @@ -0,0 +1,248 @@ +processLegacyController($request, $response); + if (null !== $legacyResponse) { + return $legacyResponse; + } + + $formatter = $this->container->formatterFactory->getFormatter(); + + $searchTags = escape(normalize_spaces($request->getParam('searchtags') ?? '')); + $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));; + + // Filter bookmarks according search parameters. + $visibility = $this->container->sessionManager->getSessionParameter('visibility'); + $search = [ + 'searchtags' => $searchTags, + 'searchterm' => $searchTerm, + ]; + $linksToDisplay = $this->container->bookmarkService->search( + $search, + $visibility, + false, + !!$this->container->sessionManager->getSessionParameter('untaggedonly') + ) ?? []; + + // ---- Handle paging. + $keys = []; + foreach ($linksToDisplay as $key => $value) { + $keys[] = $key; + } + + $linksPerPage = $this->container->sessionManager->getSessionParameter('LINKS_PER_PAGE', 20) ?: 20; + + // Select articles according to paging. + $pageCount = (int) ceil(count($keys) / $linksPerPage) ?: 1; + $page = (int) $request->getParam('page') ?? 1; + $page = $page < 1 ? 1 : $page; + $page = $page > $pageCount ? $pageCount : $page; + + // Start index. + $i = ($page - 1) * $linksPerPage; + $end = $i + $linksPerPage; + + $linkDisp = []; + $save = false; + while ($i < $end && $i < count($keys)) { + $save = $this->updateThumbnail($linksToDisplay[$keys[$i]], false) || $save; + $link = $formatter->format($linksToDisplay[$keys[$i]]); + + $linkDisp[$keys[$i]] = $link; + $i++; + } + + if ($save) { + $this->container->bookmarkService->save(); + } + + // Compute paging navigation + $searchtagsUrl = $searchTags === '' ? '' : '&searchtags=' . urlencode($searchTags); + $searchtermUrl = $searchTerm === '' ? '' : '&searchterm=' . urlencode($searchTerm); + + $previous_page_url = ''; + if ($i !== count($keys)) { + $previous_page_url = '?page=' . ($page + 1) . $searchtermUrl . $searchtagsUrl; + } + $next_page_url = ''; + if ($page > 1) { + $next_page_url = '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl; + } + + // Fill all template fields. + $data = array_merge( + $this->initializeTemplateVars(), + [ + 'previous_page_url' => $previous_page_url, + 'next_page_url' => $next_page_url, + 'page_current' => $page, + 'page_max' => $pageCount, + 'result_count' => count($linksToDisplay), + 'search_term' => $searchTerm, + 'search_tags' => $searchTags, + 'visibility' => $visibility, + 'links' => $linkDisp, + ] + ); + + if (!empty($searchTerm) || !empty($searchTags)) { + $data['pagetitle'] = t('Search: '); + $data['pagetitle'] .= ! empty($searchTerm) ? $searchTerm . ' ' : ''; + $bracketWrap = function ($tag) { + return '[' . $tag . ']'; + }; + $data['pagetitle'] .= ! empty($searchTags) + ? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchTags))) . ' ' + : ''; + $data['pagetitle'] .= '- '; + } + + $data['pagetitle'] = ($data['pagetitle'] ?? '') . $this->container->conf->get('general.title', 'Shaarli'); + + $this->executeHooks($data); + $this->assignAllView($data); + + return $response->write($this->render(TemplatePage::LINKLIST)); + } + + /** + * GET /shaare/{hash} - Display a single shaare + */ + public function permalink(Request $request, Response $response, array $args): Response + { + try { + $bookmark = $this->container->bookmarkService->findByHash($args['hash']); + } catch (BookmarkNotFoundException $e) { + $this->assignView('error_message', $e->getMessage()); + + return $response->write($this->render(TemplatePage::ERROR_404)); + } + + $this->updateThumbnail($bookmark); + + $data = array_merge( + $this->initializeTemplateVars(), + [ + 'pagetitle' => $bookmark->getTitle() .' - '. $this->container->conf->get('general.title', 'Shaarli'), + 'links' => [$this->container->formatterFactory->getFormatter()->format($bookmark)], + ] + ); + + $this->executeHooks($data); + $this->assignAllView($data); + + return $response->write($this->render(TemplatePage::LINKLIST)); + } + + /** + * Update the thumbnail of a single bookmark if necessary. + */ + protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool + { + // Logged in, thumbnails enabled, not a note, is HTTP + // and (never retrieved yet or no valid cache file) + if ($this->container->loginManager->isLoggedIn() + && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE + && false !== $bookmark->getThumbnail() + && !$bookmark->isNote() + && (null === $bookmark->getThumbnail() || !is_file($bookmark->getThumbnail())) + && startsWith(strtolower($bookmark->getUrl()), 'http') + ) { + $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); + $this->container->bookmarkService->set($bookmark, $writeDatastore); + + return true; + } + + return false; + } + + /** + * @param mixed[] $data Template vars to process in plugins, passed as reference. + */ + protected function executeHooks(array &$data): void + { + $this->container->pluginManager->executeHooks( + 'render_linklist', + $data, + ['loggedin' => $this->container->loginManager->isLoggedIn()] + ); + } + + /** + * @return string[] Default template variables without values. + */ + protected function initializeTemplateVars(): array + { + return [ + 'previous_page_url' => '', + 'next_page_url' => '', + 'page_max' => '', + 'search_tags' => '', + 'result_count' => '', + ]; + } + + /** + * Process legacy routes if necessary. They used query parameters. + * If no legacy routes is passed, return null. + */ + protected function processLegacyController(Request $request, Response $response): ?Response + { + // Legacy smallhash filter + $queryString = $this->container->environment['QUERY_STRING'] ?? null; + if (null !== $queryString && 1 === preg_match('/^([a-zA-Z0-9-_@]{6})($|&|#)/', $queryString, $match)) { + return $this->redirect($response, '/shaare/' . $match[1]); + } + + // Legacy controllers (mostly used for redirections) + if (null !== $request->getQueryParam('do')) { + $legacyController = new LegacyController($this->container); + + try { + return $legacyController->process($request, $response, $request->getQueryParam('do')); + } catch (UnknowLegacyRouteException $e) { + // We ignore legacy 404 + return null; + } + } + + // Legacy GET admin routes + $legacyGetRoutes = array_intersect( + LegacyController::LEGACY_GET_ROUTES, + array_keys($request->getQueryParams() ?? []) + ); + if (1 === count($legacyGetRoutes)) { + $legacyController = new LegacyController($this->container); + + return $legacyController->process($request, $response, $legacyGetRoutes[0]); + } + + return null; + } +} diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index e5c9ddac..05b4f095 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php @@ -7,6 +7,7 @@ namespace Shaarli\Front\Controller\Visitor; use DateTime; use DateTimeImmutable; use Shaarli\Bookmark\Bookmark; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -85,7 +86,7 @@ class DailyController extends ShaarliVisitorController t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle ); - return $response->write($this->render('daily')); + return $response->write($this->render(TemplatePage::DAILY)); } /** @@ -152,7 +153,7 @@ class DailyController extends ShaarliVisitorController $this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false)); $this->assignView('days', $dataPerDay); - $rssContent = $this->render('dailyrss'); + $rssContent = $this->render(TemplatePage::DAILY_RSS); $cache->cache($rssContent); diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php index 0db1f463..a257766f 100644 --- a/application/front/controller/visitor/LoginController.php +++ b/application/front/controller/visitor/LoginController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Visitor; use Shaarli\Front\Exception\LoginBannedException; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -41,6 +42,6 @@ class LoginController extends ShaarliVisitorController ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli')) ; - return $response->write($this->render('loginform')); + return $response->write($this->render(TemplatePage::LOGIN)); } } diff --git a/application/front/controller/visitor/OpenSearchController.php b/application/front/controller/visitor/OpenSearchController.php index 0fd68db6..36d60acf 100644 --- a/application/front/controller/visitor/OpenSearchController.php +++ b/application/front/controller/visitor/OpenSearchController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Visitor; +use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -21,6 +22,6 @@ class OpenSearchController extends ShaarliVisitorController $this->assignView('serverurl', index_url($this->container->environment)); - return $response->write($this->render('opensearch')); + return $response->write($this->render(TemplatePage::OPEN_SEARCH)); } } diff --git a/application/front/controller/visitor/PictureWallController.php b/application/front/controller/visitor/PictureWallController.php index 4e1dce8c..5ef2cb17 100644 --- a/application/front/controller/visitor/PictureWallController.php +++ b/application/front/controller/visitor/PictureWallController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Visitor; use Shaarli\Front\Exception\ThumbnailsDisabledException; +use Shaarli\Render\TemplatePage; use Shaarli\Thumbnailer; use Slim\Http\Request; use Slim\Http\Response; @@ -46,7 +47,7 @@ class PictureWallController extends ShaarliVisitorController $this->assignView($key, $value); } - return $response->write($this->render('picwall')); + return $response->write($this->render(TemplatePage::PICTURE_WALL)); } /** -- cgit v1.2.3 From c4ad3d4f061d05a01db25aa54dda830ba776792d Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 7 Jul 2020 10:15:56 +0200 Subject: Process Shaarli install through Slim controller --- .../front/controller/admin/LogoutController.php | 10 +- .../front/controller/visitor/InstallController.php | 173 +++++++++++++++++++++ 2 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 application/front/controller/visitor/InstallController.php (limited to 'application/front/controller') diff --git a/application/front/controller/admin/LogoutController.php b/application/front/controller/admin/LogoutController.php index c5984814..28165129 100644 --- a/application/front/controller/admin/LogoutController.php +++ b/application/front/controller/admin/LogoutController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Admin; +use Shaarli\Security\CookieManager; use Shaarli\Security\LoginManager; use Slim\Http\Request; use Slim\Http\Response; @@ -20,9 +21,12 @@ class LogoutController extends ShaarliAdminController { $this->container->pageCacheManager->invalidateCaches(); $this->container->sessionManager->logout(); - - // TODO: switch to a simple Cookie manager allowing to check the session, and create mocks. - setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, $this->container->basePath . '/'); + $this->container->cookieManager->setCookieParameter( + CookieManager::STAY_SIGNED_IN, + 'false', + 0, + $this->container->basePath . '/' + ); return $this->redirect($response, '/'); } diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php new file mode 100644 index 00000000..aa032860 --- /dev/null +++ b/application/front/controller/visitor/InstallController.php @@ -0,0 +1,173 @@ +container->conf->getConfigFileExt())) { + throw new AlreadyInstalledException(); + } + } + + /** + * Display the install template page. + * Also test file permissions and sessions beforehand. + */ + public function index(Request $request, Response $response): Response + { + // Before installation, we'll make sure that permissions are set properly, and sessions are working. + $this->checkPermissions(); + + if (static::SESSION_TEST_VALUE + !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) + ) { + $this->container->sessionManager->setSessionParameter(static::SESSION_TEST_KEY, static::SESSION_TEST_VALUE); + + return $this->redirect($response, '/install/session-test'); + } + + [$continents, $cities] = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get()); + + $this->assignView('continents', $continents); + $this->assignView('cities', $cities); + $this->assignView('languages', Languages::getAvailableLanguages()); + + return $response->write($this->render('install')); + } + + /** + * Route checking that the session parameter has been properly saved between two distinct requests. + * If the session parameter is preserved, redirect to install template page, otherwise displays error. + */ + public function sessionTest(Request $request, Response $response): Response + { + // This part makes sure sessions works correctly. + // (Because on some hosts, session.save_path may not be set correctly, + // or we may not have write access to it.) + if (static::SESSION_TEST_VALUE + !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) + ) { + // Step 2: Check if data in session is correct. + $msg = t( + '
Sessions do not seem to work correctly on your server.
'. + 'Make sure the variable "session.save_path" is set correctly in your PHP config, '. + 'and that you have write access to it.
'. + 'It currently points to %s.
'. + 'On some browsers, accessing your server via a hostname like \'localhost\' '. + 'or any custom hostname without a dot causes cookie storage to fail. '. + 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.
' + ); + $msg = sprintf($msg, $this->container->sessionManager->getSavePath()); + + $this->assignView('message', $msg); + + return $response->write($this->render('error')); + } + + return $this->redirect($response, '/install'); + } + + /** + * Save installation form and initialize config file and datastore if necessary. + */ + public function save(Request $request, Response $response): Response + { + $timezone = 'UTC'; + if (!empty($request->getParam('continent')) + && !empty($request->getParam('city')) + && isTimeZoneValid($request->getParam('continent'), $request->getParam('city')) + ) { + $timezone = $request->getParam('continent') . '/' . $request->getParam('city'); + } + $this->container->conf->set('general.timezone', $timezone); + + $login = $request->getParam('setlogin'); + $this->container->conf->set('credentials.login', $login); + $salt = sha1(uniqid('', true) .'_'. mt_rand()); + $this->container->conf->set('credentials.salt', $salt); + $this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt)); + + if (!empty($request->getParam('title'))) { + $this->container->conf->set('general.title', escape($request->getParam('title'))); + } else { + $this->container->conf->set( + 'general.title', + 'Shared bookmarks on '.escape(index_url($this->container->environment)) + ); + } + + $this->container->conf->set('translation.language', escape($request->getParam('language'))); + $this->container->conf->set('updates.check_updates', !empty($request->getParam('updateCheck'))); + $this->container->conf->set('api.enabled', !empty($request->getParam('enableApi'))); + $this->container->conf->set( + 'api.secret', + generate_api_secret( + $this->container->conf->get('credentials.login'), + $this->container->conf->get('credentials.salt') + ) + ); + + try { + // Everything is ok, let's create config file. + $this->container->conf->write($this->container->loginManager->isLoggedIn()); + } catch (\Exception $e) { + $this->assignView('message', $e->getMessage()); + $this->assignView('stacktrace', $e->getTraceAsString()); + + return $response->write($this->render('error')); + } + + if ($this->container->bookmarkService->count(BookmarkFilter::$ALL) === 0) { + $this->container->bookmarkService->initialize(); + } + + $this->container->sessionManager->setSessionParameter( + SessionManager::KEY_SUCCESS_MESSAGES, + [t('Shaarli is now configured. Please login and start shaaring your bookmarks!')] + ); + + return $this->redirect($response, '/'); + } + + protected function checkPermissions(): bool + { + // Ensure Shaarli has proper access to its resources + $errors = ApplicationUtils::checkResourcePermissions($this->container->conf); + + if (empty($errors)) { + return true; + } + + // FIXME! Do not insert HTML here. + $message = '

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

    '; + + foreach ($errors as $error) { + $message .= '
  • '.$error.'
  • '; + } + $message .= '
'; + + throw new ResourcePermissionException($message); + } +} -- cgit v1.2.3 From a8c11451e8d885a243c1ad52012093ba8d121e2c Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 21 Jul 2020 20:33:33 +0200 Subject: Process login through Slim controller --- .../front/controller/visitor/LoginController.php | 127 +++++++++++++++++++-- 1 file changed, 117 insertions(+), 10 deletions(-) (limited to 'application/front/controller') diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php index a257766f..c40b8cc4 100644 --- a/application/front/controller/visitor/LoginController.php +++ b/application/front/controller/visitor/LoginController.php @@ -4,8 +4,12 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Visitor; +use Shaarli\Front\Exception\CantLoginException; use Shaarli\Front\Exception\LoginBannedException; +use Shaarli\Front\Exception\WrongTokenException; use Shaarli\Render\TemplatePage; +use Shaarli\Security\CookieManager; +use Shaarli\Security\SessionManager; use Slim\Http\Request; use Slim\Http\Response; @@ -19,29 +23,132 @@ use Slim\Http\Response; */ class LoginController extends ShaarliVisitorController { + /** + * GET /login - Display the login page. + */ public function index(Request $request, Response $response): Response { - if ($this->container->loginManager->isLoggedIn() - || $this->container->conf->get('security.open_shaarli', false) - ) { + try { + $this->checkLoginState(); + } catch (CantLoginException $e) { return $this->redirect($response, '/'); } - $userCanLogin = $this->container->loginManager->canLogin($request->getServerParams()); - if ($userCanLogin !== true) { - throw new LoginBannedException(); + if ($request->getParam('login') !== null) { + $this->assignView('username', escape($request->getParam('login'))); } - if ($request->getParam('username') !== null) { - $this->assignView('username', escape($request->getParam('username'))); - } + $returnUrl = $request->getParam('returnurl') ?? $this->container->environment['HTTP_REFERER'] ?? null; $this - ->assignView('returnurl', escape($request->getServerParam('HTTP_REFERER'))) + ->assignView('returnurl', escape($returnUrl)) ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true)) ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli')) ; return $response->write($this->render(TemplatePage::LOGIN)); } + + /** + * POST /login - Process login + */ + public function login(Request $request, Response $response): Response + { + if (!$this->container->sessionManager->checkToken($request->getParam('token'))) { + throw new WrongTokenException(); + } + + try { + $this->checkLoginState(); + } catch (CantLoginException $e) { + return $this->redirect($response, '/'); + } + + if (!$this->container->loginManager->checkCredentials( + $this->container->environment['REMOTE_ADDR'], + client_ip_id($this->container->environment), + $request->getParam('login'), + $request->getParam('password') + ) + ) { + $this->container->loginManager->handleFailedLogin($this->container->environment); + + $this->container->sessionManager->setSessionParameter( + SessionManager::KEY_ERROR_MESSAGES, + [t('Wrong login/password.')] + ); + + // Call controller directly instead of unnecessary redirection + return $this->index($request, $response); + } + + $this->container->loginManager->handleSuccessfulLogin($this->container->environment); + + $cookiePath = $this->container->basePath . '/'; + $expirationTime = $this->saveLongLastingSession($request, $cookiePath); + $this->renewUserSession($cookiePath, $expirationTime); + + // Force referer from given return URL + $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl'); + + return $this->redirectFromReferer($request, $response, ['login']); + } + + /** + * Make sure that the user is allowed to login and/or displaying the login page: + * - not already logged in + * - not open shaarli + * - not banned + */ + protected function checkLoginState(): bool + { + if ($this->container->loginManager->isLoggedIn() + || $this->container->conf->get('security.open_shaarli', false) + ) { + throw new CantLoginException(); + } + + if (true !== $this->container->loginManager->canLogin($this->container->environment)) { + throw new LoginBannedException(); + } + + return true; + } + + /** + * @return int Session duration in seconds + */ + protected function saveLongLastingSession(Request $request, string $cookiePath): int + { + if (empty($request->getParam('longlastingsession'))) { + // Standard session expiration (=when browser closes) + $expirationTime = 0; + } else { + // Keep the session cookie even after the browser closes + $this->container->sessionManager->setStaySignedIn(true); + $expirationTime = $this->container->sessionManager->extendSession(); + } + + $this->container->cookieManager->setCookieParameter( + CookieManager::STAY_SIGNED_IN, + $this->container->loginManager->getStaySignedInToken(), + $expirationTime, + $cookiePath + ); + + return $expirationTime; + } + + protected function renewUserSession(string $cookiePath, int $expirationTime): void + { + // Send cookie with the new expiration date to the browser + $this->container->sessionManager->destroy(); + $this->container->sessionManager->cookieParameters( + $expirationTime, + $cookiePath, + $this->container->environment['SERVER_NAME'] + ); + $this->container->sessionManager->start(); + $this->container->sessionManager->regenerateId(true); + } } -- cgit v1.2.3 From 3ee8351e438f13ccf36062ce956e0b4a4d5f4a29 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 23 Jul 2020 16:41:32 +0200 Subject: Multiple small fixes --- .../front/controller/admin/ConfigureController.php | 17 +++++++++++------ .../front/controller/visitor/InstallController.php | 13 +++++-------- application/front/controller/visitor/TagController.php | 2 -- 3 files changed, 16 insertions(+), 16 deletions(-) (limited to 'application/front/controller') diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php index 865fc2b0..e675fcca 100644 --- a/application/front/controller/admin/ConfigureController.php +++ b/application/front/controller/admin/ConfigureController.php @@ -98,10 +98,10 @@ class ConfigureController extends ShaarliAdminController if ($thumbnailsMode !== Thumbnailer::MODE_NONE && $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) ) { - $this->saveWarningMessage(t( - 'You have enabled or changed thumbnails mode. ' - .'Please synchronize them.' - )); + $this->saveWarningMessage( + t('You have enabled or changed thumbnails mode.') . + '' . t('Please synchronize them.') .'' + ); } $this->container->conf->set('thumbnails.mode', $thumbnailsMode); @@ -110,8 +110,13 @@ class ConfigureController extends ShaarliAdminController $this->container->history->updateSettings(); $this->container->pageCacheManager->invalidateCaches(); } catch (Throwable $e) { - // TODO: translation + stacktrace - $this->saveErrorMessage('ERROR while writing config file after configuration update.'); + $this->assignView('message', t('Error while writing config file after configuration update.')); + + if ($this->container->conf->get('dev.debug', false)) { + $this->assignView('stacktrace', $e->getMessage() . PHP_EOL . $e->getTraceAsString()); + } + + return $response->write($this->render('error')); } $this->saveSuccessMessage(t('Configuration was saved.')); diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php index aa032860..94ebb4ae 100644 --- a/application/front/controller/visitor/InstallController.php +++ b/application/front/controller/visitor/InstallController.php @@ -128,13 +128,14 @@ class InstallController extends ShaarliVisitorController $this->container->conf->get('credentials.salt') ) ); + $this->container->conf->set('general.header_link', $this->container->basePath); try { // Everything is ok, let's create config file. $this->container->conf->write($this->container->loginManager->isLoggedIn()); } catch (\Exception $e) { - $this->assignView('message', $e->getMessage()); - $this->assignView('stacktrace', $e->getTraceAsString()); + $this->assignView('message', t('Error while writing config file after configuration update.')); + $this->assignView('stacktrace', $e->getMessage() . PHP_EOL . $e->getTraceAsString()); return $response->write($this->render('error')); } @@ -155,18 +156,14 @@ class InstallController extends ShaarliVisitorController { // Ensure Shaarli has proper access to its resources $errors = ApplicationUtils::checkResourcePermissions($this->container->conf); - if (empty($errors)) { return true; } - // FIXME! Do not insert HTML here. - $message = '

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

    '; - + $message = t('Insufficient permissions:') . PHP_EOL; foreach ($errors as $error) { - $message .= '
  • '.$error.'
  • '; + $message .= PHP_EOL . $error; } - $message .= '
'; throw new ResourcePermissionException($message); } diff --git a/application/front/controller/visitor/TagController.php b/application/front/controller/visitor/TagController.php index c176f43f..de4e7ea2 100644 --- a/application/front/controller/visitor/TagController.php +++ b/application/front/controller/visitor/TagController.php @@ -11,8 +11,6 @@ use Slim\Http\Response; * Class TagController * * Slim controller handle tags. - * - * TODO: check redirections with new helper */ class TagController extends ShaarliVisitorController { -- cgit v1.2.3 From 8e9169cebaf5697344cb373d69fe429e8e0efd5d Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 23 Jul 2020 17:13:22 +0200 Subject: Update French translation --- application/front/controller/admin/PluginsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'application/front/controller') diff --git a/application/front/controller/admin/PluginsController.php b/application/front/controller/admin/PluginsController.php index 44025395..1eb7e635 100644 --- a/application/front/controller/admin/PluginsController.php +++ b/application/front/controller/admin/PluginsController.php @@ -75,7 +75,7 @@ class PluginsController extends ShaarliAdminController $this->saveSuccessMessage(t('Setting successfully saved.')); } catch (Exception $e) { $this->saveErrorMessage( - t('ERROR while saving plugin configuration: ') . PHP_EOL . $e->getMessage() + t('Error while saving plugin configuration: ') . PHP_EOL . $e->getMessage() ); } -- cgit v1.2.3 From 87ae3c4f08431e02869376cb57add257747910d1 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 24 Jul 2020 10:30:47 +0200 Subject: Fix default link and redirection in install controller --- application/front/controller/visitor/InstallController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'application/front/controller') diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php index 94ebb4ae..5e3152c7 100644 --- a/application/front/controller/visitor/InstallController.php +++ b/application/front/controller/visitor/InstallController.php @@ -128,7 +128,7 @@ class InstallController extends ShaarliVisitorController $this->container->conf->get('credentials.salt') ) ); - $this->container->conf->set('general.header_link', $this->container->basePath); + $this->container->conf->set('general.header_link', $this->container->basePath . '/'); try { // Everything is ok, let's create config file. @@ -149,7 +149,7 @@ class InstallController extends ShaarliVisitorController [t('Shaarli is now configured. Please login and start shaaring your bookmarks!')] ); - return $this->redirect($response, '/'); + return $this->redirect($response, '/login'); } protected function checkPermissions(): bool -- cgit v1.2.3 From 204035bd3c91b9a5c39fcb6fc470e108b032dbd9 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 24 Jul 2020 12:48:53 +0200 Subject: Fix: visitor are allowed to chose nb of links per page --- .../controller/admin/SessionFilterController.php | 20 +------------ .../visitor/PublicSessionFilterController.php | 33 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 19 deletions(-) create mode 100644 application/front/controller/visitor/PublicSessionFilterController.php (limited to 'application/front/controller') diff --git a/application/front/controller/admin/SessionFilterController.php b/application/front/controller/admin/SessionFilterController.php index 69a16ec3..081c0ba0 100644 --- a/application/front/controller/admin/SessionFilterController.php +++ b/application/front/controller/admin/SessionFilterController.php @@ -12,28 +12,10 @@ use Slim\Http\Response; /** * Class SessionFilterController * - * Slim controller used to handle filters stored in the user session, such as visibility, links per page, etc. + * Slim controller used to handle filters stored in the user session, such as visibility, etc. */ class SessionFilterController extends ShaarliAdminController { - /** - * GET /links-per-page: set the number of bookmarks to display per page in homepage - */ - public function linksPerPage(Request $request, Response $response): Response - { - $linksPerPage = $request->getParam('nb') ?? null; - if (null === $linksPerPage || false === is_numeric($linksPerPage)) { - $linksPerPage = $this->container->conf->get('general.links_per_page', 20); - } - - $this->container->sessionManager->setSessionParameter( - SessionManager::KEY_LINKS_PER_PAGE, - abs(intval($linksPerPage)) - ); - - return $this->redirectFromReferer($request, $response, ['linksperpage'], ['nb']); - } - /** * GET /visibility: allows to display only public or only private bookmarks in linklist */ diff --git a/application/front/controller/visitor/PublicSessionFilterController.php b/application/front/controller/visitor/PublicSessionFilterController.php new file mode 100644 index 00000000..35da0c5f --- /dev/null +++ b/application/front/controller/visitor/PublicSessionFilterController.php @@ -0,0 +1,33 @@ +getParam('nb') ?? null; + if (null === $linksPerPage || false === is_numeric($linksPerPage)) { + $linksPerPage = $this->container->conf->get('general.links_per_page', 20); + } + + $this->container->sessionManager->setSessionParameter( + SessionManager::KEY_LINKS_PER_PAGE, + abs(intval($linksPerPage)) + ); + + return $this->redirectFromReferer($request, $response, ['linksperpage'], ['nb']); + } +} -- cgit v1.2.3 From 9fbc42294e7667c5ef19cafa0d1fcfbc1c0f36a9 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 26 Jul 2020 14:43:10 +0200 Subject: New basePath: fix officiel plugin paths and vintage template --- .../controller/admin/ManageShaareController.php | 25 ++++------------ .../front/controller/admin/PluginsController.php | 17 +---------- .../front/controller/admin/ToolsController.php | 17 +---------- .../controller/visitor/BookmarkListController.php | 16 ++--------- .../front/controller/visitor/DailyController.php | 22 ++------------- .../front/controller/visitor/FeedController.php | 21 +------------- .../controller/visitor/PictureWallController.php | 23 ++------------- .../visitor/ShaarliVisitorController.php | 33 ++++++++++++---------- .../controller/visitor/TagCloudController.php | 24 ++-------------- 9 files changed, 36 insertions(+), 162 deletions(-) (limited to 'application/front/controller') diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php index 3aa48423..33e1188e 100644 --- a/application/front/controller/admin/ManageShaareController.php +++ b/application/front/controller/admin/ManageShaareController.php @@ -152,7 +152,7 @@ class ManageShaareController extends ShaarliAdminController // To preserve backward compatibility with 3rd parties, plugins still use arrays $formatter = $this->container->formatterFactory->getFormatter('raw'); $data = $formatter->format($bookmark); - $data = $this->executeHooks('save_link', $data); + $this->executePageHooks('save_link', $data); $bookmark->fromArray($data); $this->container->bookmarkService->set($bookmark); @@ -211,7 +211,7 @@ class ManageShaareController extends ShaarliAdminController } $data = $formatter->format($bookmark); - $this->container->pluginManager->executeHooks('delete_link', $data); + $this->executePageHooks('delete_link', $data); $this->container->bookmarkService->remove($bookmark, false); ++ $count; } @@ -283,7 +283,7 @@ class ManageShaareController extends ShaarliAdminController // To preserve backward compatibility with 3rd parties, plugins still use arrays $data = $formatter->format($bookmark); - $this->container->pluginManager->executeHooks('save_link', $data); + $this->executePageHooks('save_link', $data); $bookmark->fromArray($data); $this->container->bookmarkService->set($bookmark, false); @@ -325,7 +325,7 @@ class ManageShaareController extends ShaarliAdminController // To preserve backward compatibility with 3rd parties, plugins still use arrays $data = $formatter->format($bookmark); - $this->container->pluginManager->executeHooks('save_link', $data); + $this->executePageHooks('save_link', $data); $bookmark->fromArray($data); $this->container->bookmarkService->set($bookmark); @@ -354,7 +354,7 @@ class ManageShaareController extends ShaarliAdminController 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false), ]; - $data = $this->executeHooks('render_editlink', $data); + $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK); foreach ($data as $key => $value) { $this->assignView($key, $value); @@ -368,19 +368,4 @@ class ManageShaareController extends ShaarliAdminController return $response->write($this->render(TemplatePage::EDIT_LINK)); } - - /** - * @param mixed[] $data Variables passed to the template engine - * - * @return mixed[] Template data after active plugins render_picwall hook execution. - */ - protected function executeHooks(string $hook, array $data): array - { - $this->container->pluginManager->executeHooks( - $hook, - $data - ); - - return $data; - } } diff --git a/application/front/controller/admin/PluginsController.php b/application/front/controller/admin/PluginsController.php index 1eb7e635..0e09116e 100644 --- a/application/front/controller/admin/PluginsController.php +++ b/application/front/controller/admin/PluginsController.php @@ -58,7 +58,7 @@ class PluginsController extends ShaarliAdminController try { $parameters = $request->getParams() ?? []; - $this->executeHooks($parameters); + $this->executePageHooks('save_plugin_parameters', $parameters); if (isset($parameters['parameters_form'])) { unset($parameters['parameters_form']); @@ -81,19 +81,4 @@ class PluginsController extends ShaarliAdminController return $this->redirect($response, '/admin/plugins'); } - - /** - * @param mixed[] $data Variables passed to the template engine - * - * @return mixed[] Template data after active plugins render_picwall hook execution. - */ - protected function executeHooks(array $data): array - { - $this->container->pluginManager->executeHooks( - 'save_plugin_parameters', - $data - ); - - return $data; - } } diff --git a/application/front/controller/admin/ToolsController.php b/application/front/controller/admin/ToolsController.php index a476e898..a87f20d2 100644 --- a/application/front/controller/admin/ToolsController.php +++ b/application/front/controller/admin/ToolsController.php @@ -22,7 +22,7 @@ class ToolsController extends ShaarliAdminController 'sslenabled' => is_https($this->container->environment), ]; - $data = $this->executeHooks($data); + $this->executePageHooks('render_tools', $data, TemplatePage::TOOLS); foreach ($data as $key => $value) { $this->assignView($key, $value); @@ -32,19 +32,4 @@ class ToolsController extends ShaarliAdminController return $response->write($this->render(TemplatePage::TOOLS)); } - - /** - * @param mixed[] $data Variables passed to the template engine - * - * @return mixed[] Template data after active plugins render_picwall hook execution. - */ - protected function executeHooks(array $data): array - { - $this->container->pluginManager->executeHooks( - 'render_tools', - $data - ); - - return $data; - } } diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php index a37a7f6b..23c4fbae 100644 --- a/application/front/controller/visitor/BookmarkListController.php +++ b/application/front/controller/visitor/BookmarkListController.php @@ -124,7 +124,7 @@ class BookmarkListController extends ShaarliVisitorController $data['pagetitle'] = ($data['pagetitle'] ?? '') . $this->container->conf->get('general.title', 'Shaarli'); - $this->executeHooks($data); + $this->executePageHooks('render_linklist', $data, TemplatePage::LINKLIST); $this->assignAllView($data); return $response->write($this->render(TemplatePage::LINKLIST)); @@ -153,7 +153,7 @@ class BookmarkListController extends ShaarliVisitorController ] ); - $this->executeHooks($data); + $this->executePageHooks('render_linklist', $data, TemplatePage::LINKLIST); $this->assignAllView($data); return $response->write($this->render(TemplatePage::LINKLIST)); @@ -182,18 +182,6 @@ class BookmarkListController extends ShaarliVisitorController return false; } - /** - * @param mixed[] $data Template vars to process in plugins, passed as reference. - */ - protected function executeHooks(array &$data): void - { - $this->container->pluginManager->executeHooks( - 'render_linklist', - $data, - ['loggedin' => $this->container->loginManager->isLoggedIn()] - ); - } - /** * @return string[] Default template variables without values. */ diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index 05b4f095..808ca5f7 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php @@ -72,13 +72,11 @@ class DailyController extends ShaarliVisitorController ]; // Hooks are called before column construction so that plugins don't have to deal with columns. - $data = $this->executeHooks($data); + $this->executePageHooks('render_daily', $data, TemplatePage::DAILY); $data['cols'] = $this->calculateColumns($data['linksToDisplay']); - foreach ($data as $key => $value) { - $this->assignView($key, $value); - } + $this->assignAllView($data); $mainTitle = $this->container->conf->get('general.title', 'Shaarli'); $this->assignView( @@ -190,20 +188,4 @@ class DailyController extends ShaarliVisitorController return $columns; } - - /** - * @param mixed[] $data Variables passed to the template engine - * - * @return mixed[] Template data after active plugins render_picwall hook execution. - */ - protected function executeHooks(array $data): array - { - $this->container->pluginManager->executeHooks( - 'render_daily', - $data, - ['loggedin' => $this->container->loginManager->isLoggedIn()] - ); - - return $data; - } } diff --git a/application/front/controller/visitor/FeedController.php b/application/front/controller/visitor/FeedController.php index f76f55fd..da2848c2 100644 --- a/application/front/controller/visitor/FeedController.php +++ b/application/front/controller/visitor/FeedController.php @@ -46,7 +46,7 @@ class FeedController extends ShaarliVisitorController $data = $this->container->feedBuilder->buildData($feedType, $request->getParams()); - $data = $this->executeHooks($data, $feedType); + $this->executePageHooks('render_feed', $data, $feedType); $this->assignAllView($data); $content = $this->render('feed.'. $feedType); @@ -55,23 +55,4 @@ class FeedController extends ShaarliVisitorController return $response->write($content); } - - /** - * @param mixed[] $data Template data - * - * @return mixed[] Template data after active plugins hook execution. - */ - protected function executeHooks(array $data, string $feedType): array - { - $this->container->pluginManager->executeHooks( - 'render_feed', - $data, - [ - 'loggedin' => $this->container->loginManager->isLoggedIn(), - 'target' => $feedType, - ] - ); - - return $data; - } } diff --git a/application/front/controller/visitor/PictureWallController.php b/application/front/controller/visitor/PictureWallController.php index 5ef2cb17..3c57f8dd 100644 --- a/application/front/controller/visitor/PictureWallController.php +++ b/application/front/controller/visitor/PictureWallController.php @@ -42,30 +42,13 @@ class PictureWallController extends ShaarliVisitorController } } - $data = $this->executeHooks($linksToDisplay); + $data = ['linksToDisplay' => $linksToDisplay]; + $this->executePageHooks('render_picwall', $data, TemplatePage::PICTURE_WALL); + foreach ($data as $key => $value) { $this->assignView($key, $value); } return $response->write($this->render(TemplatePage::PICTURE_WALL)); } - - /** - * @param mixed[] $linksToDisplay List of formatted bookmarks - * - * @return mixed[] Template data after active plugins render_picwall hook execution. - */ - protected function executeHooks(array $linksToDisplay): array - { - $data = [ - 'linksToDisplay' => $linksToDisplay, - ]; - $this->container->pluginManager->executeHooks( - 'render_picwall', - $data, - ['loggedin' => $this->container->loginManager->isLoggedIn()] - ); - - return $data; - } } diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php index b494a8e6..47057d97 100644 --- a/application/front/controller/visitor/ShaarliVisitorController.php +++ b/application/front/controller/visitor/ShaarliVisitorController.php @@ -60,22 +60,9 @@ abstract class ShaarliVisitorController $this->assignView('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE)); $this->assignView('plugin_errors', $this->container->pluginManager->getErrors()); - /* - * Define base path (if Shaarli is installed in a domain's subfolder, e.g. `/shaarli`) - * and the asset path (subfolder/tpl/default for default theme). - * These MUST be used to create an internal link or to include an asset in templates. - */ - $this->assignView('base_path', $this->container->basePath); - $this->assignView( - 'asset_path', - $this->container->basePath . '/' . - rtrim($this->container->conf->get('resource.raintpl_tpl', 'tpl'), '/') . '/' . - $this->container->conf->get('resource.theme', 'default') - ); - $this->executeDefaultHooks($template); - return $this->container->pageBuilder->render($template); + return $this->container->pageBuilder->render($template, $this->container->basePath); } /** @@ -97,13 +84,29 @@ abstract class ShaarliVisitorController $pluginData, [ 'target' => $template, - 'loggedin' => $this->container->loginManager->isLoggedIn() + 'loggedin' => $this->container->loginManager->isLoggedIn(), + 'basePath' => $this->container->basePath, ] ); $this->assignView('plugins_' . $name, $pluginData); } } + protected function executePageHooks(string $hook, array &$data, string $template = null): void + { + $params = [ + 'target' => $template, + 'loggedin' => $this->container->loginManager->isLoggedIn(), + 'basePath' => $this->container->basePath, + ]; + + $this->container->pluginManager->executeHooks( + $hook, + $data, + $params + ); + } + /** * Simple helper which prepend the base path to redirect path. * diff --git a/application/front/controller/visitor/TagCloudController.php b/application/front/controller/visitor/TagCloudController.php index 15b6d7b7..f9c529bc 100644 --- a/application/front/controller/visitor/TagCloudController.php +++ b/application/front/controller/visitor/TagCloudController.php @@ -71,10 +71,8 @@ class TagCloudController extends ShaarliVisitorController 'search_tags' => $searchTags, 'tags' => $tags, ]; - $data = $this->executeHooks('tag' . $type, $data); - foreach ($data as $key => $value) { - $this->assignView($key, $value); - } + $this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type); + $this->assignAllView($data); $searchTags = !empty($searchTags) ? $searchTags .' - ' : ''; $this->assignView( @@ -82,7 +80,7 @@ class TagCloudController extends ShaarliVisitorController $searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli') ); - return $response->write($this->render('tag.'. $type)); + return $response->write($this->render('tag.' . $type)); } /** @@ -112,20 +110,4 @@ class TagCloudController extends ShaarliVisitorController return $tagList; } - - /** - * @param mixed[] $data Template data - * - * @return mixed[] Template data after active plugins hook execution. - */ - protected function executeHooks(string $template, array $data): array - { - $this->container->pluginManager->executeHooks( - 'render_'. $template, - $data, - ['loggedin' => $this->container->loginManager->isLoggedIn()] - ); - - return $data; - } } -- cgit v1.2.3 From a285668ec4456c4d413c1d6dec275f1d18bf3f15 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 27 Jul 2020 12:34:17 +0200 Subject: Fix redirection after post install login --- application/front/controller/visitor/LoginController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'application/front/controller') diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php index c40b8cc4..121ba40b 100644 --- a/application/front/controller/visitor/LoginController.php +++ b/application/front/controller/visitor/LoginController.php @@ -91,7 +91,7 @@ class LoginController extends ShaarliVisitorController // Force referer from given return URL $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl'); - return $this->redirectFromReferer($request, $response, ['login']); + return $this->redirectFromReferer($request, $response, ['login', 'install']); } /** -- cgit v1.2.3 From 301c7ab1a079d937ab41c6f52b8804e5731008e6 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 28 Jul 2020 20:46:11 +0200 Subject: Better support for notes permalink --- application/front/controller/visitor/BookmarkListController.php | 6 +++++- application/front/controller/visitor/DailyController.php | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'application/front/controller') diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php index 23c4fbae..2988bee6 100644 --- a/application/front/controller/visitor/BookmarkListController.php +++ b/application/front/controller/visitor/BookmarkListController.php @@ -32,6 +32,7 @@ class BookmarkListController extends ShaarliVisitorController } $formatter = $this->container->formatterFactory->getFormatter(); + $formatter->addContextData('base_path', $this->container->basePath); $searchTags = escape(normalize_spaces($request->getParam('searchtags') ?? '')); $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));; @@ -145,11 +146,14 @@ class BookmarkListController extends ShaarliVisitorController $this->updateThumbnail($bookmark); + $formatter = $this->container->formatterFactory->getFormatter(); + $formatter->addContextData('base_path', $this->container->basePath); + $data = array_merge( $this->initializeTemplateVars(), [ 'pagetitle' => $bookmark->getTitle() .' - '. $this->container->conf->get('general.title', 'Shaarli'), - 'links' => [$this->container->formatterFactory->getFormatter()->format($bookmark)], + 'links' => [$formatter->format($bookmark)], ] ); diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index 808ca5f7..54a4778f 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php @@ -54,6 +54,7 @@ class DailyController extends ShaarliVisitorController } $formatter = $this->container->formatterFactory->getFormatter(); + $formatter->addContextData('base_path', $this->container->basePath); // We pre-format some fields for proper output. foreach ($linksToDisplay as $key => $bookmark) { $linksToDisplay[$key] = $formatter->format($bookmark); -- cgit v1.2.3 From d6e5f04d3987e498c5cb859eed6bff33d67949df Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 1 Aug 2020 11:10:57 +0200 Subject: Remove anonymous permission and initialize bookmarks on login --- application/front/controller/visitor/InstallController.php | 5 ----- 1 file changed, 5 deletions(-) (limited to 'application/front/controller') diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php index 5e3152c7..7cb32777 100644 --- a/application/front/controller/visitor/InstallController.php +++ b/application/front/controller/visitor/InstallController.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Visitor; use Shaarli\ApplicationUtils; -use Shaarli\Bookmark\BookmarkFilter; use Shaarli\Container\ShaarliContainer; use Shaarli\Front\Exception\AlreadyInstalledException; use Shaarli\Front\Exception\ResourcePermissionException; @@ -140,10 +139,6 @@ class InstallController extends ShaarliVisitorController return $response->write($this->render('error')); } - if ($this->container->bookmarkService->count(BookmarkFilter::$ALL) === 0) { - $this->container->bookmarkService->initialize(); - } - $this->container->sessionManager->setSessionParameter( SessionManager::KEY_SUCCESS_MESSAGES, [t('Shaarli is now configured. Please login and start shaaring your bookmarks!')] -- cgit v1.2.3 From bedbb845eec20363b928b424143787dbe988eefe Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 13 Aug 2020 11:08:13 +0200 Subject: Move all admin controller into a dedicated group Also handle authentication check in a new middleware for the admin group. --- .../front/controller/admin/SessionFilterController.php | 13 +------------ .../front/controller/admin/ShaarliAdminController.php | 9 --------- .../controller/visitor/PublicSessionFilterController.php | 13 +++++++++++++ 3 files changed, 14 insertions(+), 21 deletions(-) (limited to 'application/front/controller') diff --git a/application/front/controller/admin/SessionFilterController.php b/application/front/controller/admin/SessionFilterController.php index 081c0ba0..d9a7a2e0 100644 --- a/application/front/controller/admin/SessionFilterController.php +++ b/application/front/controller/admin/SessionFilterController.php @@ -17,7 +17,7 @@ use Slim\Http\Response; class SessionFilterController extends ShaarliAdminController { /** - * GET /visibility: allows to display only public or only private bookmarks in linklist + * GET /admin/visibility: allows to display only public or only private bookmarks in linklist */ public function visibility(Request $request, Response $response, array $args): Response { @@ -46,16 +46,5 @@ class SessionFilterController extends ShaarliAdminController return $this->redirectFromReferer($request, $response, ['visibility']); } - /** - * GET /untagged-only: allows to display only bookmarks without any tag - */ - public function untaggedOnly(Request $request, Response $response): Response - { - $this->container->sessionManager->setSessionParameter( - SessionManager::KEY_UNTAGGED_ONLY, - empty($this->container->sessionManager->getSessionParameter(SessionManager::KEY_UNTAGGED_ONLY)) - ); - return $this->redirectFromReferer($request, $response, ['untaggedonly', 'untagged-only']); - } } diff --git a/application/front/controller/admin/ShaarliAdminController.php b/application/front/controller/admin/ShaarliAdminController.php index 3bc5bb6b..3b5939bb 100644 --- a/application/front/controller/admin/ShaarliAdminController.php +++ b/application/front/controller/admin/ShaarliAdminController.php @@ -22,15 +22,6 @@ use Slim\Http\Request; */ abstract class ShaarliAdminController extends ShaarliVisitorController { - public function __construct(ShaarliContainer $container) - { - parent::__construct($container); - - if (true !== $this->container->loginManager->isLoggedIn()) { - throw new UnauthorizedException(); - } - } - /** * Any persistent action to the config or data store must check the XSRF token validity. */ diff --git a/application/front/controller/visitor/PublicSessionFilterController.php b/application/front/controller/visitor/PublicSessionFilterController.php index 35da0c5f..1a66362d 100644 --- a/application/front/controller/visitor/PublicSessionFilterController.php +++ b/application/front/controller/visitor/PublicSessionFilterController.php @@ -30,4 +30,17 @@ class PublicSessionFilterController extends ShaarliVisitorController return $this->redirectFromReferer($request, $response, ['linksperpage'], ['nb']); } + + /** + * GET /untagged-only: allows to display only bookmarks without any tag + */ + public function untaggedOnly(Request $request, Response $response): Response + { + $this->container->sessionManager->setSessionParameter( + SessionManager::KEY_UNTAGGED_ONLY, + empty($this->container->sessionManager->getSessionParameter(SessionManager::KEY_UNTAGGED_ONLY)) + ); + + return $this->redirectFromReferer($request, $response, ['untaggedonly', 'untagged-only']); + } } -- cgit v1.2.3 From 0c6fdbe12bbbb336348666b14b82096f24d5858b Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 21 Aug 2020 10:50:44 +0200 Subject: Move error handling to dedicated controller instead of middleware --- .../front/controller/visitor/ErrorController.php | 45 ++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 application/front/controller/visitor/ErrorController.php (limited to 'application/front/controller') diff --git a/application/front/controller/visitor/ErrorController.php b/application/front/controller/visitor/ErrorController.php new file mode 100644 index 00000000..10aa84c8 --- /dev/null +++ b/application/front/controller/visitor/ErrorController.php @@ -0,0 +1,45 @@ +container->pageBuilder->reset(); + + if ($throwable instanceof ShaarliFrontException) { + // Functional error + $this->assignView('message', nl2br($throwable->getMessage())); + + $response = $response->withStatus($throwable->getCode()); + } else { + // Internal error (any other Throwable) + if ($this->container->conf->get('dev.debug', false)) { + $this->assignView('message', $throwable->getMessage()); + $this->assignView( + 'stacktrace', + nl2br(get_class($throwable) .': '. PHP_EOL . $throwable->getTraceAsString()) + ); + } else { + $this->assignView('message', t('An unexpected error occurred.')); + } + + $response = $response->withStatus(500); + } + + + return $response->write($this->render('error')); + } +} -- cgit v1.2.3