]> git.immae.eu Git - github/shaarli/Shaarli.git/blob - application/front/controller/visitor/BookmarkListController.php
Process main page (linklist) through Slim controller
[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
36 $searchTags = escape(normalize_spaces($request->getParam('searchtags') ?? ''));
37 $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));;
38
39 // Filter bookmarks according search parameters.
40 $visibility = $this->container->sessionManager->getSessionParameter('visibility');
41 $search = [
42 'searchtags' => $searchTags,
43 'searchterm' => $searchTerm,
44 ];
45 $linksToDisplay = $this->container->bookmarkService->search(
46 $search,
47 $visibility,
48 false,
49 !!$this->container->sessionManager->getSessionParameter('untaggedonly')
50 ) ?? [];
51
52 // ---- Handle paging.
53 $keys = [];
54 foreach ($linksToDisplay as $key => $value) {
55 $keys[] = $key;
56 }
57
58 $linksPerPage = $this->container->sessionManager->getSessionParameter('LINKS_PER_PAGE', 20) ?: 20;
59
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;
65
66 // Start index.
67 $i = ($page - 1) * $linksPerPage;
68 $end = $i + $linksPerPage;
69
70 $linkDisp = [];
71 $save = false;
72 while ($i < $end && $i < count($keys)) {
73 $save = $this->updateThumbnail($linksToDisplay[$keys[$i]], false) || $save;
74 $link = $formatter->format($linksToDisplay[$keys[$i]]);
75
76 $linkDisp[$keys[$i]] = $link;
77 $i++;
78 }
79
80 if ($save) {
81 $this->container->bookmarkService->save();
82 }
83
84 // Compute paging navigation
85 $searchtagsUrl = $searchTags === '' ? '' : '&searchtags=' . urlencode($searchTags);
86 $searchtermUrl = $searchTerm === '' ? '' : '&searchterm=' . urlencode($searchTerm);
87
88 $previous_page_url = '';
89 if ($i !== count($keys)) {
90 $previous_page_url = '?page=' . ($page + 1) . $searchtermUrl . $searchtagsUrl;
91 }
92 $next_page_url = '';
93 if ($page > 1) {
94 $next_page_url = '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl;
95 }
96
97 // Fill all template fields.
98 $data = array_merge(
99 $this->initializeTemplateVars(),
100 [
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,
110 ]
111 );
112
113 if (!empty($searchTerm) || !empty($searchTags)) {
114 $data['pagetitle'] = t('Search: ');
115 $data['pagetitle'] .= ! empty($searchTerm) ? $searchTerm . ' ' : '';
116 $bracketWrap = function ($tag) {
117 return '[' . $tag . ']';
118 };
119 $data['pagetitle'] .= ! empty($searchTags)
120 ? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchTags))) . ' '
121 : '';
122 $data['pagetitle'] .= '- ';
123 }
124
125 $data['pagetitle'] = ($data['pagetitle'] ?? '') . $this->container->conf->get('general.title', 'Shaarli');
126
127 $this->executeHooks($data);
128 $this->assignAllView($data);
129
130 return $response->write($this->render(TemplatePage::LINKLIST));
131 }
132
133 /**
134 * GET /shaare/{hash} - Display a single shaare
135 */
136 public function permalink(Request $request, Response $response, array $args): Response
137 {
138 try {
139 $bookmark = $this->container->bookmarkService->findByHash($args['hash']);
140 } catch (BookmarkNotFoundException $e) {
141 $this->assignView('error_message', $e->getMessage());
142
143 return $response->write($this->render(TemplatePage::ERROR_404));
144 }
145
146 $this->updateThumbnail($bookmark);
147
148 $data = array_merge(
149 $this->initializeTemplateVars(),
150 [
151 'pagetitle' => $bookmark->getTitle() .' - '. $this->container->conf->get('general.title', 'Shaarli'),
152 'links' => [$this->container->formatterFactory->getFormatter()->format($bookmark)],
153 ]
154 );
155
156 $this->executeHooks($data);
157 $this->assignAllView($data);
158
159 return $response->write($this->render(TemplatePage::LINKLIST));
160 }
161
162 /**
163 * Update the thumbnail of a single bookmark if necessary.
164 */
165 protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool
166 {
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')
175 ) {
176 $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
177 $this->container->bookmarkService->set($bookmark, $writeDatastore);
178
179 return true;
180 }
181
182 return false;
183 }
184
185 /**
186 * @param mixed[] $data Template vars to process in plugins, passed as reference.
187 */
188 protected function executeHooks(array &$data): void
189 {
190 $this->container->pluginManager->executeHooks(
191 'render_linklist',
192 $data,
193 ['loggedin' => $this->container->loginManager->isLoggedIn()]
194 );
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 ];
209 }
210
211 /**
212 * Process legacy routes if necessary. They used query parameters.
213 * If no legacy routes is passed, return null.
214 */
215 protected function processLegacyController(Request $request, Response $response): ?Response
216 {
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]);
221 }
222
223 // Legacy controllers (mostly used for redirections)
224 if (null !== $request->getQueryParam('do')) {
225 $legacyController = new LegacyController($this->container);
226
227 try {
228 return $legacyController->process($request, $response, $request->getQueryParam('do'));
229 } catch (UnknowLegacyRouteException $e) {
230 // We ignore legacy 404
231 return null;
232 }
233 }
234
235 // Legacy GET admin routes
236 $legacyGetRoutes = array_intersect(
237 LegacyController::LEGACY_GET_ROUTES,
238 array_keys($request->getQueryParams() ?? [])
239 );
240 if (1 === count($legacyGetRoutes)) {
241 $legacyController = new LegacyController($this->container);
242
243 return $legacyController->process($request, $response, $legacyGetRoutes[0]);
244 }
245
246 return null;
247 }
248 }