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