]> git.immae.eu Git - github/shaarli/Shaarli.git/blob - application/front/controller/visitor/BookmarkListController.php
Handle pagination through BookmarkService
[github/shaarli/Shaarli.git] / application / front / controller / visitor / BookmarkListController.php
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Shaarli\Front\Controller\Visitor;
6
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;
15
16 /**
17 * Class BookmarkListController
18 *
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.
21 */
22 class BookmarkListController extends ShaarliVisitorController
23 {
24 /**
25 * GET / - Displays the bookmark list, with optional filter parameters.
26 */
27 public function index(Request $request, Response $response): Response
28 {
29 $legacyResponse = $this->processLegacyController($request, $response);
30 if (null !== $legacyResponse) {
31 return $legacyResponse;
32 }
33
34 $formatter = $this->container->formatterFactory->getFormatter();
35 $formatter->addContextData('base_path', $this->container->basePath);
36
37 $searchTags = normalize_spaces($request->getParam('searchtags') ?? '');
38 $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));
39
40 // Filter bookmarks according search parameters.
41 $visibility = $this->container->sessionManager->getSessionParameter('visibility');
42 $search = [
43 'searchtags' => $searchTags,
44 'searchterm' => $searchTerm,
45 ];
46
47 // Select articles according to paging.
48 $page = (int) ($request->getParam('page') ?? 1);
49 $page = $page < 1 ? 1 : $page;
50 $linksPerPage = $this->container->sessionManager->getSessionParameter('LINKS_PER_PAGE', 20) ?: 20;
51
52 $searchResult = $this->container->bookmarkService->search(
53 $search,
54 $visibility,
55 false,
56 !!$this->container->sessionManager->getSessionParameter('untaggedonly'),
57 false,
58 ['offset' => $linksPerPage * ($page - 1), 'limit' => $linksPerPage]
59 ) ?? [];
60
61 $save = false;
62 $links = [];
63 foreach ($searchResult->getBookmarks() as $key => $bookmark) {
64 $save = $this->updateThumbnail($bookmark, false) || $save;
65 $links[$key] = $formatter->format($bookmark);
66 }
67
68 if ($save) {
69 $this->container->bookmarkService->save();
70 }
71
72 // Compute paging navigation
73 $searchtagsUrl = $searchTags === '' ? '' : '&searchtags=' . urlencode($searchTags);
74 $searchtermUrl = $searchTerm === '' ? '' : '&searchterm=' . urlencode($searchTerm);
75 $page = $searchResult->getPage();
76
77 $previousPageUrl = !$searchResult->isLastPage() ? '?page=' . ($page + 1) . $searchtermUrl . $searchtagsUrl : '';
78 $nextPageUrl = !$searchResult->isFirstPage() ? '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl : '';
79
80 $tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
81 $searchTagsUrlEncoded = array_map('urlencode', tags_str2array($searchTags, $tagsSeparator));
82 $searchTags = !empty($searchTags) ? trim($searchTags, $tagsSeparator) . $tagsSeparator : '';
83
84 // Fill all template fields.
85 $data = array_merge(
86 $this->initializeTemplateVars(),
87 [
88 'previous_page_url' => $previousPageUrl,
89 'next_page_url' => $nextPageUrl,
90 'page_current' => $page,
91 'page_max' => $searchResult->getLastPage(),
92 'result_count' => $searchResult->getTotalCount(),
93 'search_term' => escape($searchTerm),
94 'search_tags' => escape($searchTags),
95 'search_tags_url' => $searchTagsUrlEncoded,
96 'visibility' => $visibility,
97 'links' => $links,
98 ]
99 );
100
101 if (!empty($searchTerm) || !empty($searchTags)) {
102 $data['pagetitle'] = t('Search: ');
103 $data['pagetitle'] .= ! empty($searchTerm) ? $searchTerm . ' ' : '';
104 $bracketWrap = function ($tag) {
105 return '[' . $tag . ']';
106 };
107 $data['pagetitle'] .= ! empty($searchTags)
108 ? implode(' ', array_map($bracketWrap, tags_str2array($searchTags, $tagsSeparator))) . ' '
109 : ''
110 ;
111 $data['pagetitle'] .= '- ';
112 }
113
114 $data['pagetitle'] = ($data['pagetitle'] ?? '') . $this->container->conf->get('general.title', 'Shaarli');
115
116 $this->executePageHooks('render_linklist', $data, TemplatePage::LINKLIST);
117 $this->assignAllView($data);
118
119 return $response->write($this->render(TemplatePage::LINKLIST));
120 }
121
122 /**
123 * GET /shaare/{hash} - Display a single shaare
124 */
125 public function permalink(Request $request, Response $response, array $args): Response
126 {
127 $privateKey = $request->getParam('key');
128
129 try {
130 $bookmark = $this->container->bookmarkService->findByHash($args['hash'], $privateKey);
131 } catch (BookmarkNotFoundException $e) {
132 $this->assignView('error_message', $e->getMessage());
133
134 return $response->write($this->render(TemplatePage::ERROR_404));
135 }
136
137 $this->updateThumbnail($bookmark);
138
139 $formatter = $this->container->formatterFactory->getFormatter();
140 $formatter->addContextData('base_path', $this->container->basePath);
141
142 $data = array_merge(
143 $this->initializeTemplateVars(),
144 [
145 'pagetitle' => $bookmark->getTitle() . ' - ' . $this->container->conf->get('general.title', 'Shaarli'),
146 'links' => [$formatter->format($bookmark)],
147 ]
148 );
149
150 $this->executePageHooks('render_linklist', $data, TemplatePage::LINKLIST);
151 $this->assignAllView($data);
152
153 return $response->write($this->render(TemplatePage::LINKLIST));
154 }
155
156 /**
157 * Update the thumbnail of a single bookmark if necessary.
158 */
159 protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool
160 {
161 if (false === $this->container->loginManager->isLoggedIn()) {
162 return false;
163 }
164
165 // If thumbnail should be updated, we reset it to null
166 if ($bookmark->shouldUpdateThumbnail()) {
167 $bookmark->setThumbnail(null);
168
169 // Requires an update, not async retrieval, thumbnails enabled
170 if (
171 $bookmark->shouldUpdateThumbnail()
172 && true !== $this->container->conf->get('general.enable_async_metadata', true)
173 && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
174 ) {
175 $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
176 $this->container->bookmarkService->set($bookmark, $writeDatastore);
177
178 return true;
179 }
180 }
181
182 return false;
183 }
184
185 /**
186 * @return string[] Default template variables without values.
187 */
188 protected function initializeTemplateVars(): array
189 {
190 return [
191 'previous_page_url' => '',
192 'next_page_url' => '',
193 'page_max' => '',
194 'search_tags' => '',
195 'result_count' => '',
196 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true)
197 ];
198 }
199
200 /**
201 * Process legacy routes if necessary. They used query parameters.
202 * If no legacy routes is passed, return null.
203 */
204 protected function processLegacyController(Request $request, Response $response): ?Response
205 {
206 // Legacy smallhash filter
207 $queryString = $this->container->environment['QUERY_STRING'] ?? null;
208 if (null !== $queryString && 1 === preg_match('/^([a-zA-Z0-9-_@]{6})($|&|#)/', $queryString, $match)) {
209 return $this->redirect($response, '/shaare/' . $match[1]);
210 }
211
212 // Legacy controllers (mostly used for redirections)
213 if (null !== $request->getQueryParam('do')) {
214 $legacyController = new LegacyController($this->container);
215
216 try {
217 return $legacyController->process($request, $response, $request->getQueryParam('do'));
218 } catch (UnknowLegacyRouteException $e) {
219 // We ignore legacy 404
220 return null;
221 }
222 }
223
224 // Legacy GET admin routes
225 $legacyGetRoutes = array_intersect(
226 LegacyController::LEGACY_GET_ROUTES,
227 array_keys($request->getQueryParams() ?? [])
228 );
229 if (1 === count($legacyGetRoutes)) {
230 $legacyController = new LegacyController($this->container);
231
232 return $legacyController->process($request, $response, $legacyGetRoutes[0]);
233 }
234
235 return null;
236 }
237 }