3 declare(strict_types
=1);
5 namespace Shaarli\Front\Controller\Visitor
;
7 use Shaarli\Bookmark\Bookmark
;
8 use Shaarli\Bookmark\Exception\BookmarkNotFoundException
;
9 use Shaarli\Legacy\LegacyController
;
10 use Shaarli\Legacy\UnknowLegacyRouteException
;
11 use Shaarli\Render\TemplatePage
;
12 use Shaarli\Thumbnailer
;
13 use Slim\Http\Request
;
14 use Slim\Http\Response
;
17 * Class BookmarkListController
19 * Slim controller used to render the bookmark list, the home page of Shaarli.
20 * It also displays permalinks, and process legacy routes based on GET parameters.
22 class BookmarkListController
extends ShaarliVisitorController
25 * GET / - Displays the bookmark list, with optional filter parameters.
27 public function index(Request
$request, Response
$response): Response
29 $legacyResponse = $this->processLegacyController($request, $response);
30 if (null !== $legacyResponse) {
31 return $legacyResponse;
34 $formatter = $this->container
->formatterFactory
->getFormatter();
36 $searchTags = escape(normalize_spaces($request->getParam('searchtags') ?? ''));
37 $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));;
39 // Filter bookmarks according search parameters.
40 $visibility = $this->container
->sessionManager
->getSessionParameter('visibility');
42 'searchtags' => $searchTags,
43 'searchterm' => $searchTerm,
45 $linksToDisplay = $this->container
->bookmarkService
->search(
49 !!$this->container
->sessionManager
->getSessionParameter('untaggedonly')
52 // ---- Handle paging.
54 foreach ($linksToDisplay as $key => $value) {
58 $linksPerPage = $this->container
->sessionManager
->getSessionParameter('LINKS_PER_PAGE', 20) ?: 20;
60 // Select articles according to paging.
61 $pageCount = (int) ceil(count($keys) / $linksPerPage) ?: 1;
62 $page = (int) $request->getParam('page') ?? 1;
63 $page = $page < 1 ? 1 : $page;
64 $page = $page > $pageCount ? $pageCount : $page;
67 $i = ($page - 1) * $linksPerPage;
68 $end = $i +
$linksPerPage;
72 while ($i < $end && $i < count($keys)) {
73 $save = $this->updateThumbnail($linksToDisplay[$keys[$i]], false) || $save;
74 $link = $formatter->format($linksToDisplay[$keys[$i]]);
76 $linkDisp[$keys[$i]] = $link;
81 $this->container
->bookmarkService
->save();
84 // Compute paging navigation
85 $searchtagsUrl = $searchTags === '' ? '' : '&searchtags=' . urlencode($searchTags);
86 $searchtermUrl = $searchTerm === '' ? '' : '&searchterm=' . urlencode($searchTerm);
88 $previous_page_url = '';
89 if ($i !== count($keys)) {
90 $previous_page_url = '?page=' . ($page +
1) . $searchtermUrl . $searchtagsUrl;
94 $next_page_url = '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl;
97 // Fill all template fields.
99 $this->initializeTemplateVars(),
101 'previous_page_url' => $previous_page_url,
102 'next_page_url' => $next_page_url,
103 'page_current' => $page,
104 'page_max' => $pageCount,
105 'result_count' => count($linksToDisplay),
106 'search_term' => $searchTerm,
107 'search_tags' => $searchTags,
108 'visibility' => $visibility,
109 'links' => $linkDisp,
113 if (!empty($searchTerm) || !empty($searchTags)) {
114 $data['pagetitle'] = t('Search: ');
115 $data['pagetitle'] .= ! empty($searchTerm) ? $searchTerm . ' ' : '';
116 $bracketWrap = function ($tag) {
117 return '[' . $tag . ']';
119 $data['pagetitle'] .= ! empty($searchTags)
120 ? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchTags))) . ' '
122 $data['pagetitle'] .= '- ';
125 $data['pagetitle'] = ($data['pagetitle'] ?? '') . $this->container
->conf
->get('general.title', 'Shaarli');
127 $this->executeHooks($data);
128 $this->assignAllView($data);
130 return $response->write($this->render(TemplatePage
::LINKLIST
));
134 * GET /shaare/{hash} - Display a single shaare
136 public function permalink(Request
$request, Response
$response, array $args): Response
139 $bookmark = $this->container
->bookmarkService
->findByHash($args['hash']);
140 } catch (BookmarkNotFoundException
$e) {
141 $this->assignView('error_message', $e->getMessage());
143 return $response->write($this->render(TemplatePage
::ERROR_404
));
146 $this->updateThumbnail($bookmark);
149 $this->initializeTemplateVars(),
151 'pagetitle' => $bookmark->getTitle() .' - '. $this->container
->conf
->get('general.title', 'Shaarli'),
152 'links' => [$this->container
->formatterFactory
->getFormatter()->format($bookmark)],
156 $this->executeHooks($data);
157 $this->assignAllView($data);
159 return $response->write($this->render(TemplatePage
::LINKLIST
));
163 * Update the thumbnail of a single bookmark if necessary.
165 protected function updateThumbnail(Bookmark
$bookmark, bool $writeDatastore = true): bool
167 // Logged in, thumbnails enabled, not a note, is HTTP
168 // and (never retrieved yet or no valid cache file)
169 if ($this->container
->loginManager
->isLoggedIn()
170 && $this->container
->conf
->get('thumbnails.mode', Thumbnailer
::MODE_NONE
) !== Thumbnailer
::MODE_NONE
171 && false !== $bookmark->getThumbnail()
172 && !$bookmark->isNote()
173 && (null === $bookmark->getThumbnail() || !is_file($bookmark->getThumbnail()))
174 && startsWith(strtolower($bookmark->getUrl()), 'http')
176 $bookmark->setThumbnail($this->container
->thumbnailer
->get($bookmark->getUrl()));
177 $this->container
->bookmarkService
->set($bookmark, $writeDatastore);
186 * @param mixed[] $data Template vars to process in plugins, passed as reference.
188 protected function executeHooks(array &$data): void
190 $this->container
->pluginManager
->executeHooks(
193 ['loggedin' => $this->container
->loginManager
->isLoggedIn()]
198 * @return string[] Default template variables without values.
200 protected function initializeTemplateVars(): array
203 'previous_page_url' => '',
204 'next_page_url' => '',
207 'result_count' => '',
212 * Process legacy routes if necessary. They used query parameters.
213 * If no legacy routes is passed, return null.
215 protected function processLegacyController(Request
$request, Response
$response): ?Response
217 // Legacy smallhash filter
218 $queryString = $this->container
->environment
['QUERY_STRING'] ?? null;
219 if (null !== $queryString && 1 === preg_match('/^([a-zA-Z0-9-_@]{6})($|&|#)/', $queryString, $match)) {
220 return $this->redirect($response, '/shaare/' . $match[1]);
223 // Legacy controllers (mostly used for redirections)
224 if (null !== $request->getQueryParam('do')) {
225 $legacyController = new LegacyController($this->container
);
228 return $legacyController->process($request, $response, $request->getQueryParam('do'));
229 } catch (UnknowLegacyRouteException
$e) {
230 // We ignore legacy 404
235 // Legacy GET admin routes
236 $legacyGetRoutes = array_intersect(
237 LegacyController
::LEGACY_GET_ROUTES
,
238 array_keys($request->getQueryParams() ?? [])
240 if (1 === count($legacyGetRoutes)) {
241 $legacyController = new LegacyController($this->container
);
243 return $legacyController->process($request, $response, $legacyGetRoutes[0]);