aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/front/controllers/SessionFilterController.php81
-rw-r--r--application/front/controllers/ShaarliController.php43
-rw-r--r--application/security/SessionManager.php33
-rw-r--r--index.php57
-rw-r--r--tests/front/controller/SessionFilterControllerTest.php290
-rw-r--r--tests/front/controller/ShaarliControllerTest.php131
-rw-r--r--tests/security/SessionManagerTest.php57
-rw-r--r--tpl/default/linklist.paging.html16
-rw-r--r--tpl/vintage/linklist.paging.html11
9 files changed, 667 insertions, 52 deletions
diff --git a/application/front/controllers/SessionFilterController.php b/application/front/controllers/SessionFilterController.php
new file mode 100644
index 00000000..a021dc37
--- /dev/null
+++ b/application/front/controllers/SessionFilterController.php
@@ -0,0 +1,81 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller;
6
7use Shaarli\Bookmark\BookmarkFilter;
8use Shaarli\Security\SessionManager;
9use Slim\Http\Request;
10use 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 * @package Shaarli\Front\Controller
18 */
19class SessionFilterController extends ShaarliController
20{
21 /**
22 * GET /links-per-page: set the number of bookmarks to display per page in homepage
23 */
24 public function linksPerPage(Request $request, Response $response): Response
25 {
26 $linksPerPage = $request->getParam('nb') ?? null;
27 if (null === $linksPerPage || false === is_numeric($linksPerPage)) {
28 $linksPerPage = $this->container->conf->get('general.links_per_page', 20);
29 }
30
31 $this->container->sessionManager->setSessionParameter(
32 SessionManager::KEY_LINKS_PER_PAGE,
33 abs(intval($linksPerPage))
34 );
35
36 return $this->redirectFromReferer($response, ['linksperpage'], ['nb']);
37 }
38
39 /**
40 * GET /visibility: allows to display only public or only private bookmarks in linklist
41 */
42 public function visibility(Request $request, Response $response, array $args): Response
43 {
44 if (false === $this->container->loginManager->isLoggedIn()) {
45 return $this->redirectFromReferer($response, ['visibility']);
46 }
47
48 $newVisibility = $args['visibility'] ?? null;
49 if (false === in_array($newVisibility, [BookmarkFilter::$PRIVATE, BookmarkFilter::$PUBLIC], true)) {
50 $newVisibility = null;
51 }
52
53 $currentVisibility = $this->container->sessionManager->getSessionParameter(SessionManager::KEY_VISIBILITY);
54
55 // Visibility not set or not already expected value, set expected value, otherwise reset it
56 if ($newVisibility !== null && (null === $currentVisibility || $currentVisibility !== $newVisibility)) {
57 // See only public bookmarks
58 $this->container->sessionManager->setSessionParameter(
59 SessionManager::KEY_VISIBILITY,
60 $newVisibility
61 );
62 } else {
63 $this->container->sessionManager->deleteSessionParameter(SessionManager::KEY_VISIBILITY);
64 }
65
66 return $this->redirectFromReferer($response, ['visibility']);
67 }
68
69 /**
70 * GET /untagged-only: allows to display only bookmarks without any tag
71 */
72 public function untaggedOnly(Request $request, Response $response): Response
73 {
74 $this->container->sessionManager->setSessionParameter(
75 SessionManager::KEY_UNTAGGED_ONLY,
76 empty($this->container->sessionManager->getSessionParameter(SessionManager::KEY_UNTAGGED_ONLY))
77 );
78
79 return $this->redirectFromReferer($response, ['untaggedonly', 'untagged-only']);
80 }
81}
diff --git a/application/front/controllers/ShaarliController.php b/application/front/controllers/ShaarliController.php
index 0c5d363e..bfff5fcf 100644
--- a/application/front/controllers/ShaarliController.php
+++ b/application/front/controllers/ShaarliController.php
@@ -6,6 +6,7 @@ namespace Shaarli\Front\Controller;
6 6
7use Shaarli\Bookmark\BookmarkFilter; 7use Shaarli\Bookmark\BookmarkFilter;
8use Shaarli\Container\ShaarliContainer; 8use Shaarli\Container\ShaarliContainer;
9use Slim\Http\Response;
9 10
10abstract class ShaarliController 11abstract class ShaarliController
11{ 12{
@@ -80,4 +81,46 @@ abstract class ShaarliController
80 $this->assignView('plugins_' . $name, $plugin_data); 81 $this->assignView('plugins_' . $name, $plugin_data);
81 } 82 }
82 } 83 }
84
85 /**
86 * Generates a redirection to the previous page, based on the HTTP_REFERER.
87 * It fails back to the home page.
88 *
89 * @param array $loopTerms Terms to remove from path and query string to prevent direction loop.
90 * @param array $clearParams List of parameter to remove from the query string of the referrer.
91 */
92 protected function redirectFromReferer(Response $response, array $loopTerms = [], array $clearParams = []): Response
93 {
94 $defaultPath = './';
95 $referer = $this->container->environment['HTTP_REFERER'] ?? null;
96
97 if (null !== $referer) {
98 $currentUrl = parse_url($referer);
99 parse_str($currentUrl['query'] ?? '', $params);
100 $path = $currentUrl['path'] ?? $defaultPath;
101 } else {
102 $params = [];
103 $path = $defaultPath;
104 }
105
106 // Prevent redirection loop
107 if (isset($currentUrl)) {
108 foreach ($clearParams as $value) {
109 unset($params[$value]);
110 }
111
112 $checkQuery = implode('', array_keys($params));
113 foreach ($loopTerms as $value) {
114 if (strpos($path . $checkQuery, $value) !== false) {
115 $params = [];
116 $path = $defaultPath;
117 break;
118 }
119 }
120 }
121
122 $queryString = count($params) > 0 ? '?'. http_build_query($params) : '';
123
124 return $response->withRedirect($path . $queryString);
125 }
83} 126}
diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php
index 4ae99168..8b77d362 100644
--- a/application/security/SessionManager.php
+++ b/application/security/SessionManager.php
@@ -8,6 +8,10 @@ use Shaarli\Config\ConfigManager;
8 */ 8 */
9class SessionManager 9class SessionManager
10{ 10{
11 public const KEY_LINKS_PER_PAGE = 'LINKS_PER_PAGE';
12 public const KEY_VISIBILITY = 'visibility';
13 public const KEY_UNTAGGED_ONLY = 'untaggedonly';
14
11 /** @var int Session expiration timeout, in seconds */ 15 /** @var int Session expiration timeout, in seconds */
12 public static $SHORT_TIMEOUT = 3600; // 1 hour 16 public static $SHORT_TIMEOUT = 3600; // 1 hour
13 17
@@ -212,4 +216,33 @@ class SessionManager
212 { 216 {
213 return $this->session[$key] ?? $default; 217 return $this->session[$key] ?? $default;
214 } 218 }
219
220 /**
221 * Store a variable in user session.
222 *
223 * @param string $key Session key
224 * @param mixed $value Session value to store
225 *
226 * @return $this
227 */
228 public function setSessionParameter(string $key, $value): self
229 {
230 $this->session[$key] = $value;
231
232 return $this;
233 }
234
235 /**
236 * Store a variable in user session.
237 *
238 * @param string $key Session key
239 *
240 * @return $this
241 */
242 public function deleteSessionParameter(string $key): self
243 {
244 unset($this->session[$key]);
245
246 return $this;
247 }
215} 248}
diff --git a/index.php b/index.php
index c0e0c66d..a31cbeab 100644
--- a/index.php
+++ b/index.php
@@ -457,57 +457,19 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
457 457
458 // -------- User wants to change the number of bookmarks per page (linksperpage=...) 458 // -------- User wants to change the number of bookmarks per page (linksperpage=...)
459 if (isset($_GET['linksperpage'])) { 459 if (isset($_GET['linksperpage'])) {
460 if (is_numeric($_GET['linksperpage'])) { 460 header('Location: ./links-per-page?nb='. $_GET['linksperpage']);
461 $_SESSION['LINKS_PER_PAGE']=abs(intval($_GET['linksperpage']));
462 }
463
464 if (! empty($_SERVER['HTTP_REFERER'])) {
465 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('linksperpage'));
466 } else {
467 $location = '?';
468 }
469 header('Location: '. $location);
470 exit; 461 exit;
471 } 462 }
472 463
473 // -------- User wants to see only private bookmarks (toggle) 464 // -------- User wants to see only private bookmarks (toggle)
474 if (isset($_GET['visibility'])) { 465 if (isset($_GET['visibility'])) {
475 if ($_GET['visibility'] === 'private') { 466 header('Location: ./visibility/'. $_GET['visibility']);
476 // Visibility not set or not already private, set private, otherwise reset it
477 if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'private') {
478 // See only private bookmarks
479 $_SESSION['visibility'] = 'private';
480 } else {
481 unset($_SESSION['visibility']);
482 }
483 } elseif ($_GET['visibility'] === 'public') {
484 if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'public') {
485 // See only public bookmarks
486 $_SESSION['visibility'] = 'public';
487 } else {
488 unset($_SESSION['visibility']);
489 }
490 }
491
492 if (! empty($_SERVER['HTTP_REFERER'])) {
493 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('visibility'));
494 } else {
495 $location = '?';
496 }
497 header('Location: '. $location);
498 exit; 467 exit;
499 } 468 }
500 469
501 // -------- User wants to see only untagged bookmarks (toggle) 470 // -------- User wants to see only untagged bookmarks (toggle)
502 if (isset($_GET['untaggedonly'])) { 471 if (isset($_GET['untaggedonly'])) {
503 $_SESSION['untaggedonly'] = empty($_SESSION['untaggedonly']); 472 header('Location: ./untagged-only');
504
505 if (! empty($_SERVER['HTTP_REFERER'])) {
506 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('untaggedonly'));
507 } else {
508 $location = '?';
509 }
510 header('Location: '. $location);
511 exit; 473 exit;
512 } 474 }
513 475
@@ -1549,6 +1511,19 @@ $app->group('', function () {
1549 1511
1550 $this->get('/add-tag/{newTag}', '\Shaarli\Front\Controller\TagController:addTag')->setName('add-tag'); 1512 $this->get('/add-tag/{newTag}', '\Shaarli\Front\Controller\TagController:addTag')->setName('add-tag');
1551 $this->get('/remove-tag/{tag}', '\Shaarli\Front\Controller\TagController:removeTag')->setName('remove-tag'); 1513 $this->get('/remove-tag/{tag}', '\Shaarli\Front\Controller\TagController:removeTag')->setName('remove-tag');
1514
1515 $this
1516 ->get('/links-per-page', '\Shaarli\Front\Controller\SessionFilterController:linksPerPage')
1517 ->setName('filter-links-per-page')
1518 ;
1519 $this
1520 ->get('/visibility/{visibility}', '\Shaarli\Front\Controller\SessionFilterController:visibility')
1521 ->setName('visibility')
1522 ;
1523 $this
1524 ->get('/untagged-only', '\Shaarli\Front\Controller\SessionFilterController:untaggedOnly')
1525 ->setName('untagged-only')
1526 ;
1552})->add('\Shaarli\Front\ShaarliMiddleware'); 1527})->add('\Shaarli\Front\ShaarliMiddleware');
1553 1528
1554$response = $app->run(true); 1529$response = $app->run(true);
diff --git a/tests/front/controller/SessionFilterControllerTest.php b/tests/front/controller/SessionFilterControllerTest.php
new file mode 100644
index 00000000..f541de03
--- /dev/null
+++ b/tests/front/controller/SessionFilterControllerTest.php
@@ -0,0 +1,290 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Security\SessionManager;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12class SessionFilterControllerTest extends TestCase
13{
14 use FrontControllerMockHelper;
15
16 /** @var SessionFilterController */
17 protected $controller;
18
19 public function setUp(): void
20 {
21 $this->createContainer();
22
23 $this->controller = new SessionFilterController($this->container);
24 }
25
26 /**
27 * Link per page - Default call with valid parameter and a referer.
28 */
29 public function testLinksPerPage(): void
30 {
31 $this->createValidContainerMockSet();
32
33 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
34
35 $request = $this->createMock(Request::class);
36 $request->method('getParam')->with('nb')->willReturn('8');
37 $response = new Response();
38
39 $this->container->sessionManager
40 ->expects(static::once())
41 ->method('setSessionParameter')
42 ->with(SessionManager::KEY_LINKS_PER_PAGE, 8)
43 ;
44
45 $result = $this->controller->linksPerPage($request, $response);
46
47 static::assertInstanceOf(Response::class, $result);
48 static::assertSame(302, $result->getStatusCode());
49 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
50 }
51
52 /**
53 * Link per page - Invalid value, should use default value (20)
54 */
55 public function testLinksPerPageNotValid(): void
56 {
57 $this->createValidContainerMockSet();
58
59 $request = $this->createMock(Request::class);
60 $request->method('getParam')->with('nb')->willReturn('test');
61 $response = new Response();
62
63 $this->container->sessionManager
64 ->expects(static::once())
65 ->method('setSessionParameter')
66 ->with(SessionManager::KEY_LINKS_PER_PAGE, 20)
67 ;
68
69 $result = $this->controller->linksPerPage($request, $response);
70
71 static::assertInstanceOf(Response::class, $result);
72 static::assertSame(302, $result->getStatusCode());
73 static::assertSame(['./'], $result->getHeader('location'));
74 }
75
76 /**
77 * Visibility - Default call for private filter while logged in without current value
78 */
79 public function testVisibility(): void
80 {
81 $this->createValidContainerMockSet();
82
83 $arg = ['visibility' => 'private'];
84
85 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
86
87 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
88 $this->container->sessionManager
89 ->expects(static::once())
90 ->method('setSessionParameter')
91 ->with(SessionManager::KEY_VISIBILITY, 'private')
92 ;
93
94 $request = $this->createMock(Request::class);
95 $response = new Response();
96
97 $result = $this->controller->visibility($request, $response, $arg);
98
99 static::assertInstanceOf(Response::class, $result);
100 static::assertSame(302, $result->getStatusCode());
101 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
102 }
103
104 /**
105 * Visibility - Toggle off private visibility
106 */
107 public function testVisibilityToggleOff(): void
108 {
109 $this->createValidContainerMockSet();
110
111 $arg = ['visibility' => 'private'];
112
113 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
114
115 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
116 $this->container->sessionManager
117 ->method('getSessionParameter')
118 ->with(SessionManager::KEY_VISIBILITY)
119 ->willReturn('private')
120 ;
121 $this->container->sessionManager
122 ->expects(static::never())
123 ->method('setSessionParameter')
124 ;
125 $this->container->sessionManager
126 ->expects(static::once())
127 ->method('deleteSessionParameter')
128 ->with(SessionManager::KEY_VISIBILITY)
129 ;
130
131 $request = $this->createMock(Request::class);
132 $response = new Response();
133
134 $result = $this->controller->visibility($request, $response, $arg);
135
136 static::assertInstanceOf(Response::class, $result);
137 static::assertSame(302, $result->getStatusCode());
138 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
139 }
140
141 /**
142 * Visibility - Change private to public
143 */
144 public function testVisibilitySwitch(): void
145 {
146 $this->createValidContainerMockSet();
147
148 $arg = ['visibility' => 'private'];
149
150 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
151 $this->container->sessionManager
152 ->method('getSessionParameter')
153 ->with(SessionManager::KEY_VISIBILITY)
154 ->willReturn('public')
155 ;
156 $this->container->sessionManager
157 ->expects(static::once())
158 ->method('setSessionParameter')
159 ->with(SessionManager::KEY_VISIBILITY, 'private')
160 ;
161
162 $request = $this->createMock(Request::class);
163 $response = new Response();
164
165 $result = $this->controller->visibility($request, $response, $arg);
166
167 static::assertInstanceOf(Response::class, $result);
168 static::assertSame(302, $result->getStatusCode());
169 static::assertSame(['./'], $result->getHeader('location'));
170 }
171
172 /**
173 * Visibility - With invalid value - should remove any visibility setting
174 */
175 public function testVisibilityInvalidValue(): void
176 {
177 $this->createValidContainerMockSet();
178
179 $arg = ['visibility' => 'test'];
180
181 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
182
183 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
184 $this->container->sessionManager
185 ->expects(static::never())
186 ->method('setSessionParameter')
187 ;
188 $this->container->sessionManager
189 ->expects(static::once())
190 ->method('deleteSessionParameter')
191 ->with(SessionManager::KEY_VISIBILITY)
192 ;
193
194 $request = $this->createMock(Request::class);
195 $response = new Response();
196
197 $result = $this->controller->visibility($request, $response, $arg);
198
199 static::assertInstanceOf(Response::class, $result);
200 static::assertSame(302, $result->getStatusCode());
201 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
202 }
203
204 /**
205 * Visibility - Try to change visibility while logged out
206 */
207 public function testVisibilityLoggedOut(): void
208 {
209 $this->createValidContainerMockSet();
210
211 $arg = ['visibility' => 'test'];
212
213 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
214
215 $this->container->loginManager->method('isLoggedIn')->willReturn(false);
216 $this->container->sessionManager
217 ->expects(static::never())
218 ->method('setSessionParameter')
219 ;
220 $this->container->sessionManager
221 ->expects(static::never())
222 ->method('deleteSessionParameter')
223 ->with(SessionManager::KEY_VISIBILITY)
224 ;
225
226 $request = $this->createMock(Request::class);
227 $response = new Response();
228
229 $result = $this->controller->visibility($request, $response, $arg);
230
231 static::assertInstanceOf(Response::class, $result);
232 static::assertSame(302, $result->getStatusCode());
233 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
234 }
235
236 /**
237 * Untagged only - valid call
238 */
239 public function testUntaggedOnly(): void
240 {
241 $this->createValidContainerMockSet();
242
243 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
244
245 $request = $this->createMock(Request::class);
246 $response = new Response();
247
248 $this->container->sessionManager
249 ->expects(static::once())
250 ->method('setSessionParameter')
251 ->with(SessionManager::KEY_UNTAGGED_ONLY, true)
252 ;
253
254 $result = $this->controller->untaggedOnly($request, $response);
255
256 static::assertInstanceOf(Response::class, $result);
257 static::assertSame(302, $result->getStatusCode());
258 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
259 }
260
261 /**
262 * Untagged only - toggle off
263 */
264 public function testUntaggedOnlyToggleOff(): void
265 {
266 $this->createValidContainerMockSet();
267
268 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
269
270 $request = $this->createMock(Request::class);
271 $response = new Response();
272
273 $this->container->sessionManager
274 ->method('getSessionParameter')
275 ->with(SessionManager::KEY_UNTAGGED_ONLY)
276 ->willReturn(true)
277 ;
278 $this->container->sessionManager
279 ->expects(static::once())
280 ->method('setSessionParameter')
281 ->with(SessionManager::KEY_UNTAGGED_ONLY, false)
282 ;
283
284 $result = $this->controller->untaggedOnly($request, $response);
285
286 static::assertInstanceOf(Response::class, $result);
287 static::assertSame(302, $result->getStatusCode());
288 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
289 }
290}
diff --git a/tests/front/controller/ShaarliControllerTest.php b/tests/front/controller/ShaarliControllerTest.php
index 3efe4d95..a6011b49 100644
--- a/tests/front/controller/ShaarliControllerTest.php
+++ b/tests/front/controller/ShaarliControllerTest.php
@@ -6,6 +6,7 @@ namespace Shaarli\Front\Controller;
6 6
7use PHPUnit\Framework\TestCase; 7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\BookmarkFilter; 8use Shaarli\Bookmark\BookmarkFilter;
9use Slim\Http\Response;
9 10
10/** 11/**
11 * Class ShaarliControllerTest 12 * Class ShaarliControllerTest
@@ -38,6 +39,14 @@ class ShaarliControllerTest extends TestCase
38 { 39 {
39 return parent::render($template); 40 return parent::render($template);
40 } 41 }
42
43 public function redirectFromReferer(
44 Response $response,
45 array $loopTerms = [],
46 array $clearParams = []
47 ): Response {
48 return parent::redirectFromReferer($response, $loopTerms, $clearParams);
49 }
41 }; 50 };
42 $this->assignedValues = []; 51 $this->assignedValues = [];
43 } 52 }
@@ -91,4 +100,126 @@ class ShaarliControllerTest extends TestCase
91 static::assertSame('templateName', $this->assignedValues['plugins_footer']['render_footer']['target']); 100 static::assertSame('templateName', $this->assignedValues['plugins_footer']['render_footer']['target']);
92 static::assertTrue($this->assignedValues['plugins_footer']['render_footer']['loggedin']); 101 static::assertTrue($this->assignedValues['plugins_footer']['render_footer']['loggedin']);
93 } 102 }
103
104 /**
105 * Test redirectFromReferer() - Default behaviour
106 */
107 public function testRedirectFromRefererDefault(): void
108 {
109 $this->createValidContainerMockSet();
110
111 $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
112
113 $response = new Response();
114
115 $result = $this->controller->redirectFromReferer($response);
116
117 static::assertSame(302, $result->getStatusCode());
118 static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location'));
119 }
120
121 /**
122 * Test redirectFromReferer() - With a loop term not matched in the referer
123 */
124 public function testRedirectFromRefererWithUnmatchedLoopTerm(): void
125 {
126 $this->createValidContainerMockSet();
127
128 $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
129
130 $response = new Response();
131
132 $result = $this->controller->redirectFromReferer($response, ['nope']);
133
134 static::assertSame(302, $result->getStatusCode());
135 static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location'));
136 }
137
138 /**
139 * Test redirectFromReferer() - With a loop term matching the referer in its path -> redirect to default
140 */
141 public function testRedirectFromRefererWithMatchingLoopTermInPath(): void
142 {
143 $this->createValidContainerMockSet();
144
145 $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
146
147 $response = new Response();
148
149 $result = $this->controller->redirectFromReferer($response, ['nope', 'controller']);
150
151 static::assertSame(302, $result->getStatusCode());
152 static::assertSame(['./'], $result->getHeader('location'));
153 }
154
155 /**
156 * Test redirectFromReferer() - With a loop term matching the referer in its query parameters -> redirect to default
157 */
158 public function testRedirectFromRefererWithMatchingLoopTermInQueryParam(): void
159 {
160 $this->createValidContainerMockSet();
161
162 $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
163
164 $response = new Response();
165
166 $result = $this->controller->redirectFromReferer($response, ['nope', 'other']);
167
168 static::assertSame(302, $result->getStatusCode());
169 static::assertSame(['./'], $result->getHeader('location'));
170 }
171
172 /**
173 * Test redirectFromReferer() - With a loop term matching the referer in its query value
174 * -> we do not block redirection for query parameter values.
175 */
176 public function testRedirectFromRefererWithMatchingLoopTermInQueryValue(): void
177 {
178 $this->createValidContainerMockSet();
179
180 $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
181
182 $response = new Response();
183
184 $result = $this->controller->redirectFromReferer($response, ['nope', 'param']);
185
186 static::assertSame(302, $result->getStatusCode());
187 static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location'));
188 }
189
190 /**
191 * Test redirectFromReferer() - With a loop term matching the referer in its domain name
192 * -> we do not block redirection for shaarli's hosts
193 */
194 public function testRedirectFromRefererWithLoopTermInDomain(): void
195 {
196 $this->createValidContainerMockSet();
197
198 $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
199
200 $response = new Response();
201
202 $result = $this->controller->redirectFromReferer($response, ['shaarli']);
203
204 static::assertSame(302, $result->getStatusCode());
205 static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location'));
206 }
207
208 /**
209 * Test redirectFromReferer() - With a loop term matching a query parameter AND clear this query param
210 * -> the param should be cleared before checking if it matches the redir loop terms
211 */
212 public function testRedirectFromRefererWithMatchingClearedParam(): void
213 {
214 $this->createValidContainerMockSet();
215
216 $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
217
218 $response = new Response();
219
220 $result = $this->controller->redirectFromReferer($response, ['query'], ['query']);
221
222 static::assertSame(302, $result->getStatusCode());
223 static::assertSame(['/subfolder/controller?other=2'], $result->getHeader('location'));
224 }
94} 225}
diff --git a/tests/security/SessionManagerTest.php b/tests/security/SessionManagerTest.php
index f264505e..d9db775e 100644
--- a/tests/security/SessionManagerTest.php
+++ b/tests/security/SessionManagerTest.php
@@ -269,4 +269,61 @@ class SessionManagerTest extends TestCase
269 $this->session['ip'] = 'ip_id_one'; 269 $this->session['ip'] = 'ip_id_one';
270 $this->assertTrue($this->sessionManager->hasClientIpChanged('ip_id_two')); 270 $this->assertTrue($this->sessionManager->hasClientIpChanged('ip_id_two'));
271 } 271 }
272
273 /**
274 * Test creating an entry in the session array
275 */
276 public function testSetSessionParameterCreate(): void
277 {
278 $this->sessionManager->setSessionParameter('abc', 'def');
279
280 static::assertSame('def', $this->session['abc']);
281 }
282
283 /**
284 * Test updating an entry in the session array
285 */
286 public function testSetSessionParameterUpdate(): void
287 {
288 $this->session['abc'] = 'ghi';
289
290 $this->sessionManager->setSessionParameter('abc', 'def');
291
292 static::assertSame('def', $this->session['abc']);
293 }
294
295 /**
296 * Test updating an entry in the session array with null value
297 */
298 public function testSetSessionParameterUpdateNull(): void
299 {
300 $this->session['abc'] = 'ghi';
301
302 $this->sessionManager->setSessionParameter('abc', null);
303
304 static::assertArrayHasKey('abc', $this->session);
305 static::assertNull($this->session['abc']);
306 }
307
308 /**
309 * Test deleting an existing entry in the session array
310 */
311 public function testDeleteSessionParameter(): void
312 {
313 $this->session['abc'] = 'def';
314
315 $this->sessionManager->deleteSessionParameter('abc');
316
317 static::assertArrayNotHasKey('abc', $this->session);
318 }
319
320 /**
321 * Test deleting a non existent entry in the session array
322 */
323 public function testDeleteSessionParameterNotExisting(): void
324 {
325 $this->sessionManager->deleteSessionParameter('abc');
326
327 static::assertArrayNotHasKey('abc', $this->session);
328 }
272} 329}
diff --git a/tpl/default/linklist.paging.html b/tpl/default/linklist.paging.html
index 68947f92..2b601725 100644
--- a/tpl/default/linklist.paging.html
+++ b/tpl/default/linklist.paging.html
@@ -6,14 +6,14 @@
6 {'Filters'|t} 6 {'Filters'|t}
7 </span> 7 </span>
8 {if="$is_logged_in"} 8 {if="$is_logged_in"}
9 <a href="?visibility=private" aria-label="{'Only display private links'|t}" title="{'Only display private links'|t}" 9 <a href="./visibility/private" aria-label="{'Only display private links'|t}" title="{'Only display private links'|t}"
10 class="{if="$visibility==='private'"}filter-on{else}filter-off{/if}" 10 class="{if="$visibility==='private'"}filter-on{else}filter-off{/if}"
11 ><i class="fa fa-user-secret" aria-hidden="true"></i></a> 11 ><i class="fa fa-user-secret" aria-hidden="true"></i></a>
12 <a href="?visibility=public" aria-label="{'Only display public links'|t}" title="{'Only display public links'|t}" 12 <a href="./visibility/public" aria-label="{'Only display public links'|t}" title="{'Only display public links'|t}"
13 class="{if="$visibility==='public'"}filter-on{else}filter-off{/if}" 13 class="{if="$visibility==='public'"}filter-on{else}filter-off{/if}"
14 ><i class="fa fa-globe" aria-hidden="true"></i></a> 14 ><i class="fa fa-globe" aria-hidden="true"></i></a>
15 {/if} 15 {/if}
16 <a href="?untaggedonly" aria-label="{'Filter untagged links'|t}" title="{'Filter untagged links'|t}" 16 <a href="./untagged-only" aria-label="{'Filter untagged links'|t}" title="{'Filter untagged links'|t}"
17 class={if="$untaggedonly"}"filter-on"{else}"filter-off"{/if} 17 class={if="$untaggedonly"}"filter-on"{else}"filter-off"{/if}
18 ><i class="fa fa-tag" aria-hidden="true"></i></a> 18 ><i class="fa fa-tag" aria-hidden="true"></i></a>
19 <a href="#" aria-label="{'Select all'|t}" title="{'Select all'|t}" 19 <a href="#" aria-label="{'Select all'|t}" title="{'Select all'|t}"
@@ -53,11 +53,11 @@
53 53
54 <div class="linksperpage pure-u-1-3"> 54 <div class="linksperpage pure-u-1-3">
55 <div class="pure-u-0 pure-u-lg-visible">{'Links per page'|t}</div> 55 <div class="pure-u-0 pure-u-lg-visible">{'Links per page'|t}</div>
56 <a href="?linksperpage=20">20</a> 56 <a href="./links-per-page?nb=20">20</a>
57 <a href="?linksperpage=50">50</a> 57 <a href="./links-per-page?nb=50">50</a>
58 <a href="?linksperpage=100">100</a> 58 <a href="./links-per-page?nb=100">100</a>
59 <form method="GET" class="pure-u-0 pure-u-lg-visible"> 59 <form method="GET" class="pure-u-0 pure-u-lg-visible" action="./links-per-page">
60 <input type="text" name="linksperpage" placeholder="133"> 60 <input type="text" name="nb" placeholder="133">
61 </form> 61 </form>
62 <a href="#" class="filter-off fold-all pure-u-0 pure-u-lg-visible" aria-label="{'Fold all'|t}" title="{'Fold all'|t}"> 62 <a href="#" class="filter-off fold-all pure-u-0 pure-u-lg-visible" aria-label="{'Fold all'|t}" title="{'Fold all'|t}">
63 <i class="fa fa-chevron-up" aria-hidden="true"></i> 63 <i class="fa fa-chevron-up" aria-hidden="true"></i>
diff --git a/tpl/vintage/linklist.paging.html b/tpl/vintage/linklist.paging.html
index 35149a6b..797104dc 100644
--- a/tpl/vintage/linklist.paging.html
+++ b/tpl/vintage/linklist.paging.html
@@ -1,7 +1,7 @@
1<div class="paging"> 1<div class="paging">
2{if="$is_logged_in"} 2{if="$is_logged_in"}
3 <div class="paging_privatelinks"> 3 <div class="paging_privatelinks">
4 <a href="?visibility=private"> 4 <a href="./visibility/private">
5 {if="$visibility=='private'"} 5 {if="$visibility=='private'"}
6 <img src="img/private_16x16_active.png" width="16" height="16" title="Click to see all links" alt="Click to see all links"> 6 <img src="img/private_16x16_active.png" width="16" height="16" title="Click to see all links" alt="Click to see all links">
7 {else} 7 {else}
@@ -23,8 +23,13 @@
23 </div> 23 </div>
24 {/loop} 24 {/loop}
25 <div class="paging_linksperpage"> 25 <div class="paging_linksperpage">
26 Links per page: <a href="?linksperpage=20">20</a> <a href="?linksperpage=50">50</a> <a href="?linksperpage=100">100</a> 26 Links per page:
27 <form method="GET" class="linksperpage"><input type="text" name="linksperpage" size="2"></form> 27 <a href="./links-per-page?nb=20">20</a>
28 <a href="./links-per-page?nb=50">50</a>
29 <a href="./links-per-page?nb=100">100</a>
30 <form method="GET" class="linksperpage" action="./links-per-page">
31 <input type="text" name="nb" size="2">
32 </form>
28 </div> 33 </div>
29 {if="$previous_page_url"} <a href="{$previous_page_url}" class="paging_older">&#x25C4;Older</a> {/if} 34 {if="$previous_page_url"} <a href="{$previous_page_url}" class="paging_older">&#x25C4;Older</a> {/if}
30 <div class="paging_current">page {$page_current} / {$page_max} </div> 35 <div class="paging_current">page {$page_current} / {$page_max} </div>