diff options
author | ArthurHoaro <arthur@hoa.ro> | 2020-05-22 13:20:31 +0200 |
---|---|---|
committer | ArthurHoaro <arthur@hoa.ro> | 2020-07-23 21:19:21 +0200 |
commit | 2899ebb5b5e82890c877151f5c02045266ac9973 (patch) | |
tree | 0c4e2684c7f6d161f92a21181bfa4b2f78d6a82f /application/front/controller | |
parent | af290059d10319e76d1e7d78b592cab99c26d91a (diff) | |
download | Shaarli-2899ebb5b5e82890c877151f5c02045266ac9973.tar.gz Shaarli-2899ebb5b5e82890c877151f5c02045266ac9973.tar.zst Shaarli-2899ebb5b5e82890c877151f5c02045266ac9973.zip |
Initialize admin Slim controllers
- Reorganize visitor controllers
- Fix redirection with Slim's requests base path
- Fix daily links
Diffstat (limited to 'application/front/controller')
11 files changed, 936 insertions, 0 deletions
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 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use Shaarli\Security\LoginManager; | ||
8 | use Slim\Http\Request; | ||
9 | use Slim\Http\Response; | ||
10 | |||
11 | /** | ||
12 | * Class LogoutController | ||
13 | * | ||
14 | * Slim controller used to logout the user. | ||
15 | * It invalidates page cache and terminate the user session. Then it redirects to the homepage. | ||
16 | */ | ||
17 | class LogoutController extends ShaarliAdminController | ||
18 | { | ||
19 | public function index(Request $request, Response $response): Response | ||
20 | { | ||
21 | $this->container->pageCacheManager->invalidateCaches(); | ||
22 | $this->container->sessionManager->logout(); | ||
23 | |||
24 | // TODO: switch to a simple Cookie manager allowing to check the session, and create mocks. | ||
25 | setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, $this->container->webPath); | ||
26 | |||
27 | return $response->withRedirect('./'); | ||
28 | } | ||
29 | } | ||
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 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use Shaarli\Bookmark\BookmarkFilter; | ||
8 | use Shaarli\Security\SessionManager; | ||
9 | use Slim\Http\Request; | ||
10 | use Slim\Http\Response; | ||
11 | |||
12 | /** | ||
13 | * Class SessionFilterController | ||
14 | * | ||
15 | * Slim controller used to handle filters stored in the user session, such as visibility, links per page, etc. | ||
16 | */ | ||
17 | class SessionFilterController extends ShaarliAdminController | ||
18 | { | ||
19 | /** | ||
20 | * GET /links-per-page: set the number of bookmarks to display per page in homepage | ||
21 | */ | ||
22 | public function linksPerPage(Request $request, Response $response): Response | ||
23 | { | ||
24 | $linksPerPage = $request->getParam('nb') ?? null; | ||
25 | if (null === $linksPerPage || false === is_numeric($linksPerPage)) { | ||
26 | $linksPerPage = $this->container->conf->get('general.links_per_page', 20); | ||
27 | } | ||
28 | |||
29 | $this->container->sessionManager->setSessionParameter( | ||
30 | SessionManager::KEY_LINKS_PER_PAGE, | ||
31 | abs(intval($linksPerPage)) | ||
32 | ); | ||
33 | |||
34 | return $this->redirectFromReferer($request, $response, ['linksperpage'], ['nb']); | ||
35 | } | ||
36 | |||
37 | /** | ||
38 | * GET /visibility: allows to display only public or only private bookmarks in linklist | ||
39 | */ | ||
40 | public function visibility(Request $request, Response $response, array $args): Response | ||
41 | { | ||
42 | if (false === $this->container->loginManager->isLoggedIn()) { | ||
43 | return $this->redirectFromReferer($request, $response, ['visibility']); | ||
44 | } | ||
45 | |||
46 | $newVisibility = $args['visibility'] ?? null; | ||
47 | if (false === in_array($newVisibility, [BookmarkFilter::$PRIVATE, BookmarkFilter::$PUBLIC], true)) { | ||
48 | $newVisibility = null; | ||
49 | } | ||
50 | |||
51 | $currentVisibility = $this->container->sessionManager->getSessionParameter(SessionManager::KEY_VISIBILITY); | ||
52 | |||
53 | // Visibility not set or not already expected value, set expected value, otherwise reset it | ||
54 | if ($newVisibility !== null && (null === $currentVisibility || $currentVisibility !== $newVisibility)) { | ||
55 | // See only public bookmarks | ||
56 | $this->container->sessionManager->setSessionParameter( | ||
57 | SessionManager::KEY_VISIBILITY, | ||
58 | $newVisibility | ||
59 | ); | ||
60 | } else { | ||
61 | $this->container->sessionManager->deleteSessionParameter(SessionManager::KEY_VISIBILITY); | ||
62 | } | ||
63 | |||
64 | return $this->redirectFromReferer($request, $response, ['visibility']); | ||
65 | } | ||
66 | |||
67 | /** | ||
68 | * GET /untagged-only: allows to display only bookmarks without any tag | ||
69 | */ | ||
70 | public function untaggedOnly(Request $request, Response $response): Response | ||
71 | { | ||
72 | $this->container->sessionManager->setSessionParameter( | ||
73 | SessionManager::KEY_UNTAGGED_ONLY, | ||
74 | empty($this->container->sessionManager->getSessionParameter(SessionManager::KEY_UNTAGGED_ONLY)) | ||
75 | ); | ||
76 | |||
77 | return $this->redirectFromReferer($request, $response, ['untaggedonly', 'untagged-only']); | ||
78 | } | ||
79 | } | ||
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 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use Shaarli\Container\ShaarliContainer; | ||
8 | use Shaarli\Front\Controller\Visitor\ShaarliVisitorController; | ||
9 | use Shaarli\Front\Exception\UnauthorizedException; | ||
10 | |||
11 | abstract class ShaarliAdminController extends ShaarliVisitorController | ||
12 | { | ||
13 | public function __construct(ShaarliContainer $container) | ||
14 | { | ||
15 | parent::__construct($container); | ||
16 | |||
17 | if (true !== $this->container->loginManager->isLoggedIn()) { | ||
18 | throw new UnauthorizedException(); | ||
19 | } | ||
20 | } | ||
21 | } | ||
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 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Visitor; | ||
6 | |||
7 | use DateTime; | ||
8 | use DateTimeImmutable; | ||
9 | use Shaarli\Bookmark\Bookmark; | ||
10 | use Slim\Http\Request; | ||
11 | use Slim\Http\Response; | ||
12 | |||
13 | /** | ||
14 | * Class DailyController | ||
15 | * | ||
16 | * Slim controller used to render the daily page. | ||
17 | */ | ||
18 | class DailyController extends ShaarliVisitorController | ||
19 | { | ||
20 | public static $DAILY_RSS_NB_DAYS = 8; | ||
21 | |||
22 | /** | ||
23 | * Controller displaying all bookmarks published in a single day. | ||
24 | * It take a `day` date query parameter (format YYYYMMDD). | ||
25 | */ | ||
26 | public function index(Request $request, Response $response): Response | ||
27 | { | ||
28 | $day = $request->getQueryParam('day') ?? date('Ymd'); | ||
29 | |||
30 | $availableDates = $this->container->bookmarkService->days(); | ||
31 | $nbAvailableDates = count($availableDates); | ||
32 | $index = array_search($day, $availableDates); | ||
33 | |||
34 | if ($index === false) { | ||
35 | // no bookmarks for day, but at least one day with bookmarks | ||
36 | $day = $availableDates[$nbAvailableDates - 1] ?? $day; | ||
37 | $previousDay = $availableDates[$nbAvailableDates - 2] ?? ''; | ||
38 | } else { | ||
39 | $previousDay = $availableDates[$index - 1] ?? ''; | ||
40 | $nextDay = $availableDates[$index + 1] ?? ''; | ||
41 | } | ||
42 | |||
43 | if ($day === date('Ymd')) { | ||
44 | $this->assignView('dayDesc', t('Today')); | ||
45 | } elseif ($day === date('Ymd', strtotime('-1 days'))) { | ||
46 | $this->assignView('dayDesc', t('Yesterday')); | ||
47 | } | ||
48 | |||
49 | try { | ||
50 | $linksToDisplay = $this->container->bookmarkService->filterDay($day); | ||
51 | } catch (\Exception $exc) { | ||
52 | $linksToDisplay = []; | ||
53 | } | ||
54 | |||
55 | $formatter = $this->container->formatterFactory->getFormatter(); | ||
56 | // We pre-format some fields for proper output. | ||
57 | foreach ($linksToDisplay as $key => $bookmark) { | ||
58 | $linksToDisplay[$key] = $formatter->format($bookmark); | ||
59 | // This page is a bit specific, we need raw description to calculate the length | ||
60 | $linksToDisplay[$key]['formatedDescription'] = $linksToDisplay[$key]['description']; | ||
61 | $linksToDisplay[$key]['description'] = $bookmark->getDescription(); | ||
62 | } | ||
63 | |||
64 | $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); | ||
65 | $data = [ | ||
66 | 'linksToDisplay' => $linksToDisplay, | ||
67 | 'day' => $dayDate->getTimestamp(), | ||
68 | 'dayDate' => $dayDate, | ||
69 | 'previousday' => $previousDay ?? '', | ||
70 | 'nextday' => $nextDay ?? '', | ||
71 | ]; | ||
72 | |||
73 | // Hooks are called before column construction so that plugins don't have to deal with columns. | ||
74 | $this->executeHooks($data); | ||
75 | |||
76 | $data['cols'] = $this->calculateColumns($data['linksToDisplay']); | ||
77 | |||
78 | foreach ($data as $key => $value) { | ||
79 | $this->assignView($key, $value); | ||
80 | } | ||
81 | |||
82 | $mainTitle = $this->container->conf->get('general.title', 'Shaarli'); | ||
83 | $this->assignView( | ||
84 | 'pagetitle', | ||
85 | t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle | ||
86 | ); | ||
87 | |||
88 | return $response->write($this->render('daily')); | ||
89 | } | ||
90 | |||
91 | /** | ||
92 | * Daily RSS feed: 1 RSS entry per day giving all the bookmarks on that day. | ||
93 | * Gives the last 7 days (which have bookmarks). | ||
94 | * This RSS feed cannot be filtered and does not trigger plugins yet. | ||
95 | */ | ||
96 | public function rss(Request $request, Response $response): Response | ||
97 | { | ||
98 | $response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8'); | ||
99 | |||
100 | $pageUrl = page_url($this->container->environment); | ||
101 | $cache = $this->container->pageCacheManager->getCachePage($pageUrl); | ||
102 | |||
103 | $cached = $cache->cachedVersion(); | ||
104 | if (!empty($cached)) { | ||
105 | return $response->write($cached); | ||
106 | } | ||
107 | |||
108 | $days = []; | ||
109 | foreach ($this->container->bookmarkService->search() as $bookmark) { | ||
110 | $day = $bookmark->getCreated()->format('Ymd'); | ||
111 | |||
112 | // Stop iterating after DAILY_RSS_NB_DAYS entries | ||
113 | if (count($days) === static::$DAILY_RSS_NB_DAYS && !isset($days[$day])) { | ||
114 | break; | ||
115 | } | ||
116 | |||
117 | $days[$day][] = $bookmark; | ||
118 | } | ||
119 | |||
120 | // Build the RSS feed. | ||
121 | $indexUrl = escape(index_url($this->container->environment)); | ||
122 | |||
123 | $formatter = $this->container->formatterFactory->getFormatter(); | ||
124 | $formatter->addContextData('index_url', $indexUrl); | ||
125 | |||
126 | $dataPerDay = []; | ||
127 | |||
128 | /** @var Bookmark[] $bookmarks */ | ||
129 | foreach ($days as $day => $bookmarks) { | ||
130 | $dayDatetime = DateTimeImmutable::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); | ||
131 | $dataPerDay[$day] = [ | ||
132 | 'date' => $dayDatetime, | ||
133 | 'date_rss' => $dayDatetime->format(DateTime::RSS), | ||
134 | 'date_human' => format_date($dayDatetime, false, true), | ||
135 | 'absolute_url' => $indexUrl . '/daily?day=' . $day, | ||
136 | 'links' => [], | ||
137 | ]; | ||
138 | |||
139 | foreach ($bookmarks as $key => $bookmark) { | ||
140 | $dataPerDay[$day]['links'][$key] = $formatter->format($bookmark); | ||
141 | |||
142 | // Make permalink URL absolute | ||
143 | if ($bookmark->isNote()) { | ||
144 | $dataPerDay[$day]['links'][$key]['url'] = $indexUrl . $bookmark->getUrl(); | ||
145 | } | ||
146 | } | ||
147 | } | ||
148 | |||
149 | $this->assignView('title', $this->container->conf->get('general.title', 'Shaarli')); | ||
150 | $this->assignView('index_url', $indexUrl); | ||
151 | $this->assignView('page_url', $pageUrl); | ||
152 | $this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false)); | ||
153 | $this->assignView('days', $dataPerDay); | ||
154 | |||
155 | $rssContent = $this->render('dailyrss'); | ||
156 | |||
157 | $cache->cache($rssContent); | ||
158 | |||
159 | return $response->write($rssContent); | ||
160 | } | ||
161 | |||
162 | /** | ||
163 | * We need to spread the articles on 3 columns. | ||
164 | * did not want to use a JavaScript lib like http://masonry.desandro.com/ | ||
165 | * so I manually spread entries with a simple method: I roughly evaluate the | ||
166 | * height of a div according to title and description length. | ||
167 | */ | ||
168 | protected function calculateColumns(array $links): array | ||
169 | { | ||
170 | // Entries to display, for each column. | ||
171 | $columns = [[], [], []]; | ||
172 | // Rough estimate of columns fill. | ||
173 | $fill = [0, 0, 0]; | ||
174 | foreach ($links as $link) { | ||
175 | // Roughly estimate length of entry (by counting characters) | ||
176 | // Title: 30 chars = 1 line. 1 line is 30 pixels height. | ||
177 | // Description: 836 characters gives roughly 342 pixel height. | ||
178 | // This is not perfect, but it's usually OK. | ||
179 | $length = strlen($link['title'] ?? '') + (342 * strlen($link['description'] ?? '')) / 836; | ||
180 | if (! empty($link['thumbnail'])) { | ||
181 | $length += 100; // 1 thumbnails roughly takes 100 pixels height. | ||
182 | } | ||
183 | // Then put in column which is the less filled: | ||
184 | $smallest = min($fill); // find smallest value in array. | ||
185 | $index = array_search($smallest, $fill); // find index of this smallest value. | ||
186 | array_push($columns[$index], $link); // Put entry in this column. | ||
187 | $fill[$index] += $length; | ||
188 | } | ||
189 | |||
190 | return $columns; | ||
191 | } | ||
192 | |||
193 | /** | ||
194 | * @param mixed[] $data Variables passed to the template engine | ||
195 | * | ||
196 | * @return mixed[] Template data after active plugins render_picwall hook execution. | ||
197 | */ | ||
198 | protected function executeHooks(array $data): array | ||
199 | { | ||
200 | $this->container->pluginManager->executeHooks( | ||
201 | 'render_daily', | ||
202 | $data, | ||
203 | ['loggedin' => $this->container->loginManager->isLoggedIn()] | ||
204 | ); | ||
205 | |||
206 | return $data; | ||
207 | } | ||
208 | } | ||
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 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Visitor; | ||
6 | |||
7 | use Shaarli\Feed\FeedBuilder; | ||
8 | use Slim\Http\Request; | ||
9 | use Slim\Http\Response; | ||
10 | |||
11 | /** | ||
12 | * Class FeedController | ||
13 | * | ||
14 | * Slim controller handling ATOM and RSS feed. | ||
15 | */ | ||
16 | class FeedController extends ShaarliVisitorController | ||
17 | { | ||
18 | public function atom(Request $request, Response $response): Response | ||
19 | { | ||
20 | return $this->processRequest(FeedBuilder::$FEED_ATOM, $request, $response); | ||
21 | } | ||
22 | |||
23 | public function rss(Request $request, Response $response): Response | ||
24 | { | ||
25 | return $this->processRequest(FeedBuilder::$FEED_RSS, $request, $response); | ||
26 | } | ||
27 | |||
28 | protected function processRequest(string $feedType, Request $request, Response $response): Response | ||
29 | { | ||
30 | $response = $response->withHeader('Content-Type', 'application/'. $feedType .'+xml; charset=utf-8'); | ||
31 | |||
32 | $pageUrl = page_url($this->container->environment); | ||
33 | $cache = $this->container->pageCacheManager->getCachePage($pageUrl); | ||
34 | |||
35 | $cached = $cache->cachedVersion(); | ||
36 | if (!empty($cached)) { | ||
37 | return $response->write($cached); | ||
38 | } | ||
39 | |||
40 | // Generate data. | ||
41 | $this->container->feedBuilder->setLocale(strtolower(setlocale(LC_COLLATE, 0))); | ||
42 | $this->container->feedBuilder->setHideDates($this->container->conf->get('privacy.hide_timestamps', false)); | ||
43 | $this->container->feedBuilder->setUsePermalinks( | ||
44 | null !== $request->getParam('permalinks') || !$this->container->conf->get('feed.rss_permalinks') | ||
45 | ); | ||
46 | |||
47 | $data = $this->container->feedBuilder->buildData($feedType, $request->getParams()); | ||
48 | |||
49 | $this->executeHooks($data, $feedType); | ||
50 | $this->assignAllView($data); | ||
51 | |||
52 | $content = $this->render('feed.'. $feedType); | ||
53 | |||
54 | $cache->cache($content); | ||
55 | |||
56 | return $response->write($content); | ||
57 | } | ||
58 | |||
59 | /** | ||
60 | * @param mixed[] $data Template data | ||
61 | * | ||
62 | * @return mixed[] Template data after active plugins hook execution. | ||
63 | */ | ||
64 | protected function executeHooks(array $data, string $feedType): array | ||
65 | { | ||
66 | $this->container->pluginManager->executeHooks( | ||
67 | 'render_feed', | ||
68 | $data, | ||
69 | [ | ||
70 | 'loggedin' => $this->container->loginManager->isLoggedIn(), | ||
71 | 'target' => $feedType, | ||
72 | ] | ||
73 | ); | ||
74 | |||
75 | return $data; | ||
76 | } | ||
77 | } | ||
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 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Visitor; | ||
6 | |||
7 | use Shaarli\Front\Exception\LoginBannedException; | ||
8 | use Slim\Http\Request; | ||
9 | use Slim\Http\Response; | ||
10 | |||
11 | /** | ||
12 | * Class LoginController | ||
13 | * | ||
14 | * Slim controller used to render the login page. | ||
15 | * | ||
16 | * The login page is not available if the user is banned | ||
17 | * or if open shaarli setting is enabled. | ||
18 | */ | ||
19 | class LoginController extends ShaarliVisitorController | ||
20 | { | ||
21 | public function index(Request $request, Response $response): Response | ||
22 | { | ||
23 | if ($this->container->loginManager->isLoggedIn() | ||
24 | || $this->container->conf->get('security.open_shaarli', false) | ||
25 | ) { | ||
26 | return $response->withRedirect('./'); | ||
27 | } | ||
28 | |||
29 | $userCanLogin = $this->container->loginManager->canLogin($request->getServerParams()); | ||
30 | if ($userCanLogin !== true) { | ||
31 | throw new LoginBannedException(); | ||
32 | } | ||
33 | |||
34 | if ($request->getParam('username') !== null) { | ||
35 | $this->assignView('username', escape($request->getParam('username'))); | ||
36 | } | ||
37 | |||
38 | $this | ||
39 | ->assignView('returnurl', escape($request->getServerParam('HTTP_REFERER'))) | ||
40 | ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true)) | ||
41 | ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli')) | ||
42 | ; | ||
43 | |||
44 | return $response->write($this->render('loginform')); | ||
45 | } | ||
46 | } | ||
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 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Visitor; | ||
6 | |||
7 | use Slim\Http\Request; | ||
8 | use Slim\Http\Response; | ||
9 | |||
10 | /** | ||
11 | * Class OpenSearchController | ||
12 | * | ||
13 | * Slim controller used to render open search template. | ||
14 | * This allows to add Shaarli as a search engine within the browser. | ||
15 | */ | ||
16 | class OpenSearchController extends ShaarliVisitorController | ||
17 | { | ||
18 | public function index(Request $request, Response $response): Response | ||
19 | { | ||
20 | $response = $response->withHeader('Content-Type', 'application/opensearchdescription+xml; charset=utf-8'); | ||
21 | |||
22 | $this->assignView('serverurl', index_url($this->container->environment)); | ||
23 | |||
24 | return $response->write($this->render('opensearch')); | ||
25 | } | ||
26 | } | ||
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 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Visitor; | ||
6 | |||
7 | use Shaarli\Front\Exception\ThumbnailsDisabledException; | ||
8 | use Shaarli\Thumbnailer; | ||
9 | use Slim\Http\Request; | ||
10 | use Slim\Http\Response; | ||
11 | |||
12 | /** | ||
13 | * Class PicturesWallController | ||
14 | * | ||
15 | * Slim controller used to render the pictures wall page. | ||
16 | * If thumbnails mode is set to NONE, we just render the template without any image. | ||
17 | */ | ||
18 | class PictureWallController extends ShaarliVisitorController | ||
19 | { | ||
20 | public function index(Request $request, Response $response): Response | ||
21 | { | ||
22 | if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) { | ||
23 | throw new ThumbnailsDisabledException(); | ||
24 | } | ||
25 | |||
26 | $this->assignView( | ||
27 | 'pagetitle', | ||
28 | t('Picture wall') .' - '. $this->container->conf->get('general.title', 'Shaarli') | ||
29 | ); | ||
30 | |||
31 | // Optionally filter the results: | ||
32 | $links = $this->container->bookmarkService->search($request->getQueryParams()); | ||
33 | $linksToDisplay = []; | ||
34 | |||
35 | // Get only bookmarks which have a thumbnail. | ||
36 | // Note: we do not retrieve thumbnails here, the request is too heavy. | ||
37 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
38 | foreach ($links as $key => $link) { | ||
39 | if (!empty($link->getThumbnail())) { | ||
40 | $linksToDisplay[] = $formatter->format($link); | ||
41 | } | ||
42 | } | ||
43 | |||
44 | $data = $this->executeHooks($linksToDisplay); | ||
45 | foreach ($data as $key => $value) { | ||
46 | $this->assignView($key, $value); | ||
47 | } | ||
48 | |||
49 | return $response->write($this->render('picwall')); | ||
50 | } | ||
51 | |||
52 | /** | ||
53 | * @param mixed[] $linksToDisplay List of formatted bookmarks | ||
54 | * | ||
55 | * @return mixed[] Template data after active plugins render_picwall hook execution. | ||
56 | */ | ||
57 | protected function executeHooks(array $linksToDisplay): array | ||
58 | { | ||
59 | $data = [ | ||
60 | 'linksToDisplay' => $linksToDisplay, | ||
61 | ]; | ||
62 | $this->container->pluginManager->executeHooks( | ||
63 | 'render_picwall', | ||
64 | $data, | ||
65 | ['loggedin' => $this->container->loginManager->isLoggedIn()] | ||
66 | ); | ||
67 | |||
68 | return $data; | ||
69 | } | ||
70 | } | ||
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 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Visitor; | ||
6 | |||
7 | use Shaarli\Bookmark\BookmarkFilter; | ||
8 | use Shaarli\Container\ShaarliContainer; | ||
9 | use Slim\Http\Request; | ||
10 | use Slim\Http\Response; | ||
11 | |||
12 | abstract class ShaarliVisitorController | ||
13 | { | ||
14 | /** @var ShaarliContainer */ | ||
15 | protected $container; | ||
16 | |||
17 | /** @param ShaarliContainer $container Slim container (extended for attribute completion). */ | ||
18 | public function __construct(ShaarliContainer $container) | ||
19 | { | ||
20 | $this->container = $container; | ||
21 | } | ||
22 | |||
23 | /** | ||
24 | * Assign variables to RainTPL template through the PageBuilder. | ||
25 | * | ||
26 | * @param mixed $value Value to assign to the template | ||
27 | */ | ||
28 | protected function assignView(string $name, $value): self | ||
29 | { | ||
30 | $this->container->pageBuilder->assign($name, $value); | ||
31 | |||
32 | return $this; | ||
33 | } | ||
34 | |||
35 | /** | ||
36 | * Assign variables to RainTPL template through the PageBuilder. | ||
37 | * | ||
38 | * @param mixed $data Values to assign to the template and their keys | ||
39 | */ | ||
40 | protected function assignAllView(array $data): self | ||
41 | { | ||
42 | foreach ($data as $key => $value) { | ||
43 | $this->assignView($key, $value); | ||
44 | } | ||
45 | |||
46 | return $this; | ||
47 | } | ||
48 | |||
49 | protected function render(string $template): string | ||
50 | { | ||
51 | $this->assignView('linkcount', $this->container->bookmarkService->count(BookmarkFilter::$ALL)); | ||
52 | $this->assignView('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE)); | ||
53 | $this->assignView('plugin_errors', $this->container->pluginManager->getErrors()); | ||
54 | |||
55 | $this->executeDefaultHooks($template); | ||
56 | |||
57 | return $this->container->pageBuilder->render($template); | ||
58 | } | ||
59 | |||
60 | /** | ||
61 | * Call plugin hooks for header, footer and includes, specifying which page will be rendered. | ||
62 | * Then assign generated data to RainTPL. | ||
63 | */ | ||
64 | protected function executeDefaultHooks(string $template): void | ||
65 | { | ||
66 | $common_hooks = [ | ||
67 | 'includes', | ||
68 | 'header', | ||
69 | 'footer', | ||
70 | ]; | ||
71 | |||
72 | foreach ($common_hooks as $name) { | ||
73 | $plugin_data = []; | ||
74 | $this->container->pluginManager->executeHooks( | ||
75 | 'render_' . $name, | ||
76 | $plugin_data, | ||
77 | [ | ||
78 | 'target' => $template, | ||
79 | 'loggedin' => $this->container->loginManager->isLoggedIn() | ||
80 | ] | ||
81 | ); | ||
82 | $this->assignView('plugins_' . $name, $plugin_data); | ||
83 | } | ||
84 | } | ||
85 | |||
86 | /** | ||
87 | * Generates a redirection to the previous page, based on the HTTP_REFERER. | ||
88 | * It fails back to the home page. | ||
89 | * | ||
90 | * @param array $loopTerms Terms to remove from path and query string to prevent direction loop. | ||
91 | * @param array $clearParams List of parameter to remove from the query string of the referrer. | ||
92 | */ | ||
93 | protected function redirectFromReferer( | ||
94 | Request $request, | ||
95 | Response $response, | ||
96 | array $loopTerms = [], | ||
97 | array $clearParams = [] | ||
98 | ): Response { | ||
99 | $defaultPath = $request->getUri()->getBasePath(); | ||
100 | $referer = $this->container->environment['HTTP_REFERER'] ?? null; | ||
101 | |||
102 | if (null !== $referer) { | ||
103 | $currentUrl = parse_url($referer); | ||
104 | parse_str($currentUrl['query'] ?? '', $params); | ||
105 | $path = $currentUrl['path'] ?? $defaultPath; | ||
106 | } else { | ||
107 | $params = []; | ||
108 | $path = $defaultPath; | ||
109 | } | ||
110 | |||
111 | // Prevent redirection loop | ||
112 | if (isset($currentUrl)) { | ||
113 | foreach ($clearParams as $value) { | ||
114 | unset($params[$value]); | ||
115 | } | ||
116 | |||
117 | $checkQuery = implode('', array_keys($params)); | ||
118 | foreach ($loopTerms as $value) { | ||
119 | if (strpos($path . $checkQuery, $value) !== false) { | ||
120 | $params = []; | ||
121 | $path = $defaultPath; | ||
122 | break; | ||
123 | } | ||
124 | } | ||
125 | } | ||
126 | |||
127 | $queryString = count($params) > 0 ? '?'. http_build_query($params) : ''; | ||
128 | |||
129 | return $response->withRedirect($path . $queryString); | ||
130 | } | ||
131 | } | ||
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 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Visitor; | ||
6 | |||
7 | use Slim\Http\Request; | ||
8 | use Slim\Http\Response; | ||
9 | |||
10 | /** | ||
11 | * Class TagCloud | ||
12 | * | ||
13 | * Slim controller used to render the tag cloud and tag list pages. | ||
14 | */ | ||
15 | class TagCloudController extends ShaarliVisitorController | ||
16 | { | ||
17 | protected const TYPE_CLOUD = 'cloud'; | ||
18 | protected const TYPE_LIST = 'list'; | ||
19 | |||
20 | /** | ||
21 | * Display the tag cloud through the template engine. | ||
22 | * This controller a few filters: | ||
23 | * - Visibility stored in the session for logged in users | ||
24 | * - `searchtags` query parameter: will return tags associated with filter in at least one bookmark | ||
25 | */ | ||
26 | public function cloud(Request $request, Response $response): Response | ||
27 | { | ||
28 | return $this->processRequest(static::TYPE_CLOUD, $request, $response); | ||
29 | } | ||
30 | |||
31 | /** | ||
32 | * Display the tag list through the template engine. | ||
33 | * This controller a few filters: | ||
34 | * - Visibility stored in the session for logged in users | ||
35 | * - `searchtags` query parameter: will return tags associated with filter in at least one bookmark | ||
36 | * - `sort` query parameters: | ||
37 | * + `usage` (default): most used tags first | ||
38 | * + `alpha`: alphabetical order | ||
39 | */ | ||
40 | public function list(Request $request, Response $response): Response | ||
41 | { | ||
42 | return $this->processRequest(static::TYPE_LIST, $request, $response); | ||
43 | } | ||
44 | |||
45 | /** | ||
46 | * Process the request for both tag cloud and tag list endpoints. | ||
47 | */ | ||
48 | protected function processRequest(string $type, Request $request, Response $response): Response | ||
49 | { | ||
50 | if ($this->container->loginManager->isLoggedIn() === true) { | ||
51 | $visibility = $this->container->sessionManager->getSessionParameter('visibility'); | ||
52 | } | ||
53 | |||
54 | $sort = $request->getQueryParam('sort'); | ||
55 | $searchTags = $request->getQueryParam('searchtags'); | ||
56 | $filteringTags = $searchTags !== null ? explode(' ', $searchTags) : []; | ||
57 | |||
58 | $tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null); | ||
59 | |||
60 | if (static::TYPE_CLOUD === $type || 'alpha' === $sort) { | ||
61 | // TODO: the sorting should be handled by bookmarkService instead of the controller | ||
62 | alphabetical_sort($tags, false, true); | ||
63 | } | ||
64 | |||
65 | if (static::TYPE_CLOUD === $type) { | ||
66 | $tags = $this->formatTagsForCloud($tags); | ||
67 | } | ||
68 | |||
69 | $searchTags = implode(' ', escape($filteringTags)); | ||
70 | $data = [ | ||
71 | 'search_tags' => $searchTags, | ||
72 | 'tags' => $tags, | ||
73 | ]; | ||
74 | $data = $this->executeHooks('tag' . $type, $data); | ||
75 | foreach ($data as $key => $value) { | ||
76 | $this->assignView($key, $value); | ||
77 | } | ||
78 | |||
79 | $searchTags = !empty($searchTags) ? $searchTags .' - ' : ''; | ||
80 | $this->assignView( | ||
81 | 'pagetitle', | ||
82 | $searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli') | ||
83 | ); | ||
84 | |||
85 | return $response->write($this->render('tag.'. $type)); | ||
86 | } | ||
87 | |||
88 | /** | ||
89 | * Format the tags array for the tag cloud template. | ||
90 | * | ||
91 | * @param array<string, int> $tags List of tags as key with count as value | ||
92 | * | ||
93 | * @return mixed[] List of tags as key, with count and expected font size in a subarray | ||
94 | */ | ||
95 | protected function formatTagsForCloud(array $tags): array | ||
96 | { | ||
97 | // We sort tags alphabetically, then choose a font size according to count. | ||
98 | // First, find max value. | ||
99 | $maxCount = count($tags) > 0 ? max($tags) : 0; | ||
100 | $logMaxCount = $maxCount > 1 ? log($maxCount, 30) : 1; | ||
101 | $tagList = []; | ||
102 | foreach ($tags as $key => $value) { | ||
103 | // Tag font size scaling: | ||
104 | // default 15 and 30 logarithm bases affect scaling, | ||
105 | // 2.2 and 0.8 are arbitrary font sizes in em. | ||
106 | $size = log($value, 15) / $logMaxCount * 2.2 + 0.8; | ||
107 | $tagList[$key] = [ | ||
108 | 'count' => $value, | ||
109 | 'size' => number_format($size, 2, '.', ''), | ||
110 | ]; | ||
111 | } | ||
112 | |||
113 | return $tagList; | ||
114 | } | ||
115 | |||
116 | /** | ||
117 | * @param mixed[] $data Template data | ||
118 | * | ||
119 | * @return mixed[] Template data after active plugins hook execution. | ||
120 | */ | ||
121 | protected function executeHooks(string $template, array $data): array | ||
122 | { | ||
123 | $this->container->pluginManager->executeHooks( | ||
124 | 'render_'. $template, | ||
125 | $data, | ||
126 | ['loggedin' => $this->container->loginManager->isLoggedIn()] | ||
127 | ); | ||
128 | |||
129 | return $data; | ||
130 | } | ||
131 | } | ||
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 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Visitor; | ||
6 | |||
7 | use Slim\Http\Request; | ||
8 | use Slim\Http\Response; | ||
9 | |||
10 | /** | ||
11 | * Class TagController | ||
12 | * | ||
13 | * Slim controller handle tags. | ||
14 | */ | ||
15 | class TagController extends ShaarliVisitorController | ||
16 | { | ||
17 | /** | ||
18 | * Add another tag in the current search through an HTTP redirection. | ||
19 | * | ||
20 | * @param array $args Should contain `newTag` key as tag to add to current search | ||
21 | */ | ||
22 | public function addTag(Request $request, Response $response, array $args): Response | ||
23 | { | ||
24 | $newTag = $args['newTag'] ?? null; | ||
25 | $referer = $this->container->environment['HTTP_REFERER'] ?? null; | ||
26 | |||
27 | // In case browser does not send HTTP_REFERER, we search a single tag | ||
28 | if (null === $referer) { | ||
29 | if (null !== $newTag) { | ||
30 | return $response->withRedirect('./?searchtags='. urlencode($newTag)); | ||
31 | } | ||
32 | |||
33 | return $response->withRedirect('./'); | ||
34 | } | ||
35 | |||
36 | $currentUrl = parse_url($referer); | ||
37 | parse_str($currentUrl['query'] ?? '', $params); | ||
38 | |||
39 | if (null === $newTag) { | ||
40 | return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); | ||
41 | } | ||
42 | |||
43 | // Prevent redirection loop | ||
44 | if (isset($params['addtag'])) { | ||
45 | unset($params['addtag']); | ||
46 | } | ||
47 | |||
48 | // Check if this tag is already in the search query and ignore it if it is. | ||
49 | // Each tag is always separated by a space | ||
50 | $currentTags = isset($params['searchtags']) ? explode(' ', $params['searchtags']) : []; | ||
51 | |||
52 | $addtag = true; | ||
53 | foreach ($currentTags as $value) { | ||
54 | if ($value === $newTag) { | ||
55 | $addtag = false; | ||
56 | break; | ||
57 | } | ||
58 | } | ||
59 | |||
60 | // Append the tag if necessary | ||
61 | if (true === $addtag) { | ||
62 | $currentTags[] = trim($newTag); | ||
63 | } | ||
64 | |||
65 | $params['searchtags'] = trim(implode(' ', $currentTags)); | ||
66 | |||
67 | // We also remove page (keeping the same page has no sense, since the results are different) | ||
68 | unset($params['page']); | ||
69 | |||
70 | return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); | ||
71 | } | ||
72 | |||
73 | /** | ||
74 | * Remove a tag from the current search through an HTTP redirection. | ||
75 | * | ||
76 | * @param array $args Should contain `tag` key as tag to remove from current search | ||
77 | */ | ||
78 | public function removeTag(Request $request, Response $response, array $args): Response | ||
79 | { | ||
80 | $referer = $this->container->environment['HTTP_REFERER'] ?? null; | ||
81 | |||
82 | // If the referrer is not provided, we can update the search, so we failback on the bookmark list | ||
83 | if (empty($referer)) { | ||
84 | return $response->withRedirect('./'); | ||
85 | } | ||
86 | |||
87 | $tagToRemove = $args['tag'] ?? null; | ||
88 | $currentUrl = parse_url($referer); | ||
89 | parse_str($currentUrl['query'] ?? '', $params); | ||
90 | |||
91 | if (null === $tagToRemove) { | ||
92 | return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); | ||
93 | } | ||
94 | |||
95 | // Prevent redirection loop | ||
96 | if (isset($params['removetag'])) { | ||
97 | unset($params['removetag']); | ||
98 | } | ||
99 | |||
100 | if (isset($params['searchtags'])) { | ||
101 | $tags = explode(' ', $params['searchtags']); | ||
102 | // Remove value from array $tags. | ||
103 | $tags = array_diff($tags, [$tagToRemove]); | ||
104 | $params['searchtags'] = implode(' ', $tags); | ||
105 | |||
106 | if (empty($params['searchtags'])) { | ||
107 | unset($params['searchtags']); | ||
108 | } | ||
109 | |||
110 | // We also remove page (keeping the same page has no sense, since the results are different) | ||
111 | unset($params['page']); | ||
112 | } | ||
113 | |||
114 | $queryParams = count($params) > 0 ? '?' . http_build_query($params) : ''; | ||
115 | |||
116 | return $response->withRedirect(($currentUrl['path'] ?? './') . $queryParams); | ||
117 | } | ||
118 | } | ||