aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/front/controller/admin/ManageShaareController.php70
-rw-r--r--assets/default/js/base.js2
-rw-r--r--index.php51
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php418
4 files changed, 492 insertions, 49 deletions
diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php
index 620bbc40..ff330a99 100644
--- a/application/front/controller/admin/ManageShaareController.php
+++ b/application/front/controller/admin/ManageShaareController.php
@@ -174,7 +174,7 @@ class ManageShaareController extends ShaarliAdminController
174 } 174 }
175 175
176 /** 176 /**
177 * GET /admin/shaare/delete 177 * GET /admin/shaare/delete - Delete one or multiple bookmarks (depending on `id` query parameter).
178 */ 178 */
179 public function deleteBookmark(Request $request, Response $response): Response 179 public function deleteBookmark(Request $request, Response $response): Response
180 { 180 {
@@ -229,6 +229,74 @@ class ManageShaareController extends ShaarliAdminController
229 } 229 }
230 230
231 /** 231 /**
232 * GET /admin/shaare/visibility
233 *
234 * Change visibility (public/private) of one or multiple bookmarks (depending on `id` query parameter).
235 */
236 public function changeVisibility(Request $request, Response $response): Response
237 {
238 $this->checkToken($request);
239
240 $ids = trim(escape($request->getParam('id') ?? ''));
241 if (empty($ids) || strpos($ids, ' ') !== false) {
242 // multiple, space-separated ids provided
243 $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit'));
244 } else {
245 // only a single id provided
246 $ids = [$ids];
247 }
248
249 // assert at least one id is given
250 if (0 === count($ids)) {
251 $this->saveErrorMessage(t('Invalid bookmark ID provided.'));
252
253 return $this->redirectFromReferer($request, $response, [], ['change_visibility']);
254 }
255
256 // assert that the visibility is valid
257 $visibility = $request->getParam('newVisibility');
258 if (null === $visibility || false === in_array($visibility, ['public', 'private'], true)) {
259 $this->saveErrorMessage(t('Invalid visibility provided.'));
260
261 return $this->redirectFromReferer($request, $response, [], ['change_visibility']);
262 } else {
263 $isPrivate = $visibility === 'private';
264 }
265
266 $formatter = $this->container->formatterFactory->getFormatter('raw');
267 $count = 0;
268
269 foreach ($ids as $id) {
270 try {
271 $bookmark = $this->container->bookmarkService->get((int) $id);
272 } catch (BookmarkNotFoundException $e) {
273 $this->saveErrorMessage(sprintf(
274 t('Bookmark with identifier %s could not be found.'),
275 $id
276 ));
277
278 continue;
279 }
280
281 $bookmark->setPrivate($isPrivate);
282
283 // To preserve backward compatibility with 3rd parties, plugins still use arrays
284 $data = $formatter->format($bookmark);
285 $this->container->pluginManager->executeHooks('save_link', $data);
286 $bookmark->fromArray($data);
287
288 $this->container->bookmarkService->set($bookmark, false);
289 ++$count;
290 }
291
292 if ($count > 0) {
293 $this->container->bookmarkService->save();
294 }
295
296 return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']);
297 }
298
299 /**
232 * Helper function used to display the shaare form whether it's a new or existing bookmark. 300 * Helper function used to display the shaare form whether it's a new or existing bookmark.
233 * 301 *
234 * @param array $link data used in template, either from parameters or from the data store 302 * @param array $link data used in template, either from parameters or from the data store
diff --git a/assets/default/js/base.js b/assets/default/js/base.js
index 9f67d980..af3d650c 100644
--- a/assets/default/js/base.js
+++ b/assets/default/js/base.js
@@ -486,7 +486,7 @@ function init(description) {
486 486
487 const ids = links.map(item => item.id); 487 const ids = links.map(item => item.id);
488 window.location = 488 window.location =
489 `${basePath}/?change_visibility&token=${token.value}&newVisibility=${visibility}&ids=${ids.join('+')}`; 489 `${basePath}/admin/shaare/visibility?token=${token.value}&newVisibility=${visibility}&id=${ids.join('+')}`;
490 }); 490 });
491 }); 491 });
492 } 492 }
diff --git a/index.php b/index.php
index 12c7a8f1..93e5590b 100644
--- a/index.php
+++ b/index.php
@@ -499,6 +499,8 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
499 499
500 // -------- All other functions are reserved for the registered user: 500 // -------- All other functions are reserved for the registered user:
501 501
502 // TODO: Remove legacy admin route redirections. We'll only keep public URL.
503
502 // -------- Display the Tools menu if requested (import/export/bookmarklet...) 504 // -------- Display the Tools menu if requested (import/export/bookmarklet...)
503 if ($targetPage == Router::$PAGE_TOOLS) { 505 if ($targetPage == Router::$PAGE_TOOLS) {
504 header('Location: ./admin/tools'); 506 header('Location: ./admin/tools');
@@ -547,53 +549,7 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
547 549
548 // -------- User clicked either "Set public" or "Set private" bulk operation 550 // -------- User clicked either "Set public" or "Set private" bulk operation
549 if ($targetPage == Router::$PAGE_CHANGE_VISIBILITY) { 551 if ($targetPage == Router::$PAGE_CHANGE_VISIBILITY) {
550 if (! $sessionManager->checkToken($_GET['token'])) { 552 header('Location: ./admin/shaare/visibility?id=' . $_GET['token']);
551 die(t('Wrong token.'));
552 }
553
554 $ids = trim($_GET['ids']);
555 if (strpos($ids, ' ') !== false) {
556 // multiple, space-separated ids provided
557 $ids = array_values(array_filter(preg_split('/\s+/', escape($ids))));
558 } else {
559 // only a single id provided
560 $ids = [$ids];
561 }
562
563 // assert at least one id is given
564 if (!count($ids)) {
565 die('no id provided');
566 }
567 // assert that the visibility is valid
568 if (!isset($_GET['newVisibility']) || !in_array($_GET['newVisibility'], ['public', 'private'])) {
569 die('invalid visibility');
570 } else {
571 $private = $_GET['newVisibility'] === 'private';
572 }
573 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
574 $formatter = $factory->getFormatter('raw');
575 foreach ($ids as $id) {
576 $id = (int) escape($id);
577 $bookmark = $bookmarkService->get($id);
578 $bookmark->setPrivate($private);
579
580 // To preserve backward compatibility with 3rd parties, plugins still use arrays
581 $data = $formatter->format($bookmark);
582 $pluginManager->executeHooks('save_link', $data);
583 $bookmark->fromArray($data);
584
585 $bookmarkService->set($bookmark);
586 }
587 $bookmarkService->save();
588
589 $location = '?';
590 if (isset($_SERVER['HTTP_REFERER'])) {
591 $location = generateLocation(
592 $_SERVER['HTTP_REFERER'],
593 $_SERVER['HTTP_HOST']
594 );
595 }
596 header('Location: ' . $location); // After deleting the link, redirect to appropriate location
597 exit; 553 exit;
598 } 554 }
599 555
@@ -1164,6 +1120,7 @@ $app->group('', function () {
1164 $this->get('/admin/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayEditForm'); 1120 $this->get('/admin/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayEditForm');
1165 $this->post('/admin/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:save'); 1121 $this->post('/admin/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:save');
1166 $this->get('/admin/shaare/delete', '\Shaarli\Front\Controller\Admin\ManageShaareController:deleteBookmark'); 1122 $this->get('/admin/shaare/delete', '\Shaarli\Front\Controller\Admin\ManageShaareController:deleteBookmark');
1123 $this->get('/admin/shaare/visibility', '\Shaarli\Front\Controller\Admin\ManageShaareController:changeVisibility');
1167 1124
1168 $this->get('/links-per-page', '\Shaarli\Front\Controller\Admin\SessionFilterController:linksPerPage'); 1125 $this->get('/links-per-page', '\Shaarli\Front\Controller\Admin\SessionFilterController:linksPerPage');
1169 $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility'); 1126 $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility');
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php b/tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php
new file mode 100644
index 00000000..5a615791
--- /dev/null
+++ b/tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php
@@ -0,0 +1,418 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
10use Shaarli\Formatter\BookmarkFormatter;
11use Shaarli\Formatter\BookmarkRawFormatter;
12use Shaarli\Formatter\FormatterFactory;
13use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
14use Shaarli\Front\Controller\Admin\ManageShaareController;
15use Shaarli\Http\HttpAccess;
16use Shaarli\Security\SessionManager;
17use Slim\Http\Request;
18use Slim\Http\Response;
19
20class ChangeVisibilityBookmarkTest extends TestCase
21{
22 use FrontAdminControllerMockHelper;
23
24 /** @var ManageShaareController */
25 protected $controller;
26
27 public function setUp(): void
28 {
29 $this->createContainer();
30
31 $this->container->httpAccess = $this->createMock(HttpAccess::class);
32 $this->controller = new ManageShaareController($this->container);
33 }
34
35 /**
36 * Change bookmark visibility - Set private - Single public bookmark with valid parameters
37 */
38 public function testSetSingleBookmarkPrivate(): void
39 {
40 $parameters = ['id' => '123', 'newVisibility' => 'private'];
41
42 $request = $this->createMock(Request::class);
43 $request
44 ->method('getParam')
45 ->willReturnCallback(function (string $key) use ($parameters): ?string {
46 return $parameters[$key] ?? null;
47 })
48 ;
49 $response = new Response();
50
51 $bookmark = (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(false);
52
53 static::assertFalse($bookmark->isPrivate());
54
55 $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
56 $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, false);
57 $this->container->bookmarkService->expects(static::once())->method('save');
58 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
59 $this->container->formatterFactory
60 ->expects(static::once())
61 ->method('getFormatter')
62 ->with('raw')
63 ->willReturnCallback(function () use ($bookmark): BookmarkFormatter {
64 return new BookmarkRawFormatter($this->container->conf, true);
65 })
66 ;
67
68 // Make sure that PluginManager hook is triggered
69 $this->container->pluginManager
70 ->expects(static::once())
71 ->method('executeHooks')
72 ->with('save_link')
73 ;
74
75 $result = $this->controller->changeVisibility($request, $response);
76
77 static::assertTrue($bookmark->isPrivate());
78
79 static::assertSame(302, $result->getStatusCode());
80 static::assertSame(['/subfolder/'], $result->getHeader('location'));
81 }
82
83 /**
84 * Change bookmark visibility - Set public - Single private bookmark with valid parameters
85 */
86 public function testSetSingleBookmarkPublic(): void
87 {
88 $parameters = ['id' => '123', 'newVisibility' => 'public'];
89
90 $request = $this->createMock(Request::class);
91 $request
92 ->method('getParam')
93 ->willReturnCallback(function (string $key) use ($parameters): ?string {
94 return $parameters[$key] ?? null;
95 })
96 ;
97 $response = new Response();
98
99 $bookmark = (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(true);
100
101 static::assertTrue($bookmark->isPrivate());
102
103 $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
104 $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, false);
105 $this->container->bookmarkService->expects(static::once())->method('save');
106 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
107 $this->container->formatterFactory
108 ->expects(static::once())
109 ->method('getFormatter')
110 ->with('raw')
111 ->willReturn(new BookmarkRawFormatter($this->container->conf, true))
112 ;
113
114 // Make sure that PluginManager hook is triggered
115 $this->container->pluginManager
116 ->expects(static::once())
117 ->method('executeHooks')
118 ->with('save_link')
119 ;
120
121 $result = $this->controller->changeVisibility($request, $response);
122
123 static::assertFalse($bookmark->isPrivate());
124
125 static::assertSame(302, $result->getStatusCode());
126 static::assertSame(['/subfolder/'], $result->getHeader('location'));
127 }
128
129 /**
130 * Change bookmark visibility - Set private on single already private bookmark
131 */
132 public function testSetSinglePrivateBookmarkPrivate(): void
133 {
134 $parameters = ['id' => '123', 'newVisibility' => 'private'];
135
136 $request = $this->createMock(Request::class);
137 $request
138 ->method('getParam')
139 ->willReturnCallback(function (string $key) use ($parameters): ?string {
140 return $parameters[$key] ?? null;
141 })
142 ;
143 $response = new Response();
144
145 $bookmark = (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(true);
146
147 static::assertTrue($bookmark->isPrivate());
148
149 $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
150 $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, false);
151 $this->container->bookmarkService->expects(static::once())->method('save');
152 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
153 $this->container->formatterFactory
154 ->expects(static::once())
155 ->method('getFormatter')
156 ->with('raw')
157 ->willReturn(new BookmarkRawFormatter($this->container->conf, true))
158 ;
159
160 // Make sure that PluginManager hook is triggered
161 $this->container->pluginManager
162 ->expects(static::once())
163 ->method('executeHooks')
164 ->with('save_link')
165 ;
166
167 $result = $this->controller->changeVisibility($request, $response);
168
169 static::assertTrue($bookmark->isPrivate());
170
171 static::assertSame(302, $result->getStatusCode());
172 static::assertSame(['/subfolder/'], $result->getHeader('location'));
173 }
174
175 /**
176 * Change bookmark visibility - Set multiple bookmarks private
177 */
178 public function testSetMultipleBookmarksPrivate(): void
179 {
180 $parameters = ['id' => '123 456 789', 'newVisibility' => 'private'];
181
182 $request = $this->createMock(Request::class);
183 $request
184 ->method('getParam')
185 ->willReturnCallback(function (string $key) use ($parameters): ?string {
186 return $parameters[$key] ?? null;
187 })
188 ;
189 $response = new Response();
190
191 $bookmarks = [
192 (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(false),
193 (new Bookmark())->setId(456)->setUrl('http://domain.tld')->setTitle('Title 456')->setPrivate(true),
194 (new Bookmark())->setId(789)->setUrl('http://domain.tld')->setTitle('Title 789')->setPrivate(false),
195 ];
196
197 $this->container->bookmarkService
198 ->expects(static::exactly(3))
199 ->method('get')
200 ->withConsecutive([123], [456], [789])
201 ->willReturnOnConsecutiveCalls(...$bookmarks)
202 ;
203 $this->container->bookmarkService
204 ->expects(static::exactly(3))
205 ->method('set')
206 ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
207 return [$bookmark, false];
208 }, $bookmarks))
209 ;
210 $this->container->bookmarkService->expects(static::once())->method('save');
211 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
212 $this->container->formatterFactory
213 ->expects(static::once())
214 ->method('getFormatter')
215 ->with('raw')
216 ->willReturn(new BookmarkRawFormatter($this->container->conf, true))
217 ;
218
219 // Make sure that PluginManager hook is triggered
220 $this->container->pluginManager
221 ->expects(static::exactly(3))
222 ->method('executeHooks')
223 ->with('save_link')
224 ;
225
226 $result = $this->controller->changeVisibility($request, $response);
227
228 static::assertTrue($bookmarks[0]->isPrivate());
229 static::assertTrue($bookmarks[1]->isPrivate());
230 static::assertTrue($bookmarks[2]->isPrivate());
231
232 static::assertSame(302, $result->getStatusCode());
233 static::assertSame(['/subfolder/'], $result->getHeader('location'));
234 }
235
236 /**
237 * Change bookmark visibility - Single bookmark not found.
238 */
239 public function testChangeVisibilitySingleBookmarkNotFound(): void
240 {
241 $parameters = ['id' => '123', 'newVisibility' => 'private'];
242
243 $request = $this->createMock(Request::class);
244 $request
245 ->method('getParam')
246 ->willReturnCallback(function (string $key) use ($parameters): ?string {
247 return $parameters[$key] ?? null;
248 })
249 ;
250 $response = new Response();
251
252 $this->container->bookmarkService
253 ->expects(static::once())
254 ->method('get')
255 ->willThrowException(new BookmarkNotFoundException())
256 ;
257 $this->container->bookmarkService->expects(static::never())->method('set');
258 $this->container->bookmarkService->expects(static::never())->method('save');
259 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
260 $this->container->formatterFactory
261 ->expects(static::once())
262 ->method('getFormatter')
263 ->with('raw')
264 ->willReturn(new BookmarkRawFormatter($this->container->conf, true))
265 ;
266
267 // Make sure that PluginManager hook is not triggered
268 $this->container->pluginManager
269 ->expects(static::never())
270 ->method('executeHooks')
271 ->with('save_link')
272 ;
273
274 $result = $this->controller->changeVisibility($request, $response);
275
276 static::assertSame(302, $result->getStatusCode());
277 static::assertSame(['/subfolder/'], $result->getHeader('location'));
278 }
279
280 /**
281 * Change bookmark visibility - Multiple bookmarks with one not found.
282 */
283 public function testChangeVisibilityMultipleBookmarksOneNotFound(): void
284 {
285 $parameters = ['id' => '123 456 789', 'newVisibility' => 'public'];
286
287 $request = $this->createMock(Request::class);
288 $request
289 ->method('getParam')
290 ->willReturnCallback(function (string $key) use ($parameters): ?string {
291 return $parameters[$key] ?? null;
292 })
293 ;
294 $response = new Response();
295
296 $bookmarks = [
297 (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(true),
298 (new Bookmark())->setId(789)->setUrl('http://domain.tld')->setTitle('Title 789')->setPrivate(false),
299 ];
300
301 $this->container->bookmarkService
302 ->expects(static::exactly(3))
303 ->method('get')
304 ->withConsecutive([123], [456], [789])
305 ->willReturnCallback(function (int $id) use ($bookmarks): Bookmark {
306 if ($id === 123) {
307 return $bookmarks[0];
308 }
309 if ($id === 789) {
310 return $bookmarks[1];
311 }
312 throw new BookmarkNotFoundException();
313 })
314 ;
315 $this->container->bookmarkService
316 ->expects(static::exactly(2))
317 ->method('set')
318 ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
319 return [$bookmark, false];
320 }, $bookmarks))
321 ;
322 $this->container->bookmarkService->expects(static::once())->method('save');
323
324 // Make sure that PluginManager hook is not triggered
325 $this->container->pluginManager
326 ->expects(static::exactly(2))
327 ->method('executeHooks')
328 ->with('save_link')
329 ;
330
331 $this->container->sessionManager
332 ->expects(static::once())
333 ->method('setSessionParameter')
334 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier 456 could not be found.'])
335 ;
336
337 $result = $this->controller->changeVisibility($request, $response);
338
339 static::assertSame(302, $result->getStatusCode());
340 static::assertSame(['/subfolder/'], $result->getHeader('location'));
341 }
342
343 /**
344 * Change bookmark visibility - Invalid ID
345 */
346 public function testChangeVisibilityInvalidId(): void
347 {
348 $parameters = ['id' => 'nope not an ID', 'newVisibility' => 'private'];
349
350 $request = $this->createMock(Request::class);
351 $request
352 ->method('getParam')
353 ->willReturnCallback(function (string $key) use ($parameters): ?string {
354 return $parameters[$key] ?? null;
355 })
356 ;
357 $response = new Response();
358
359 $this->container->sessionManager
360 ->expects(static::once())
361 ->method('setSessionParameter')
362 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid bookmark ID provided.'])
363 ;
364
365 $result = $this->controller->changeVisibility($request, $response);
366
367 static::assertSame(302, $result->getStatusCode());
368 static::assertSame(['/subfolder/'], $result->getHeader('location'));
369 }
370
371 /**
372 * Change bookmark visibility - Empty ID
373 */
374 public function testChangeVisibilityEmptyId(): void
375 {
376 $request = $this->createMock(Request::class);
377 $response = new Response();
378
379 $this->container->sessionManager
380 ->expects(static::once())
381 ->method('setSessionParameter')
382 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid bookmark ID provided.'])
383 ;
384
385 $result = $this->controller->changeVisibility($request, $response);
386
387 static::assertSame(302, $result->getStatusCode());
388 static::assertSame(['/subfolder/'], $result->getHeader('location'));
389 }
390
391 /**
392 * Change bookmark visibility - with invalid visibility
393 */
394 public function testChangeVisibilityWithInvalidVisibility(): void
395 {
396 $parameters = ['id' => '123', 'newVisibility' => 'invalid'];
397
398 $request = $this->createMock(Request::class);
399 $request
400 ->method('getParam')
401 ->willReturnCallback(function (string $key) use ($parameters): ?string {
402 return $parameters[$key] ?? null;
403 })
404 ;
405 $response = new Response();
406
407 $this->container->sessionManager
408 ->expects(static::once())
409 ->method('setSessionParameter')
410 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid visibility provided.'])
411 ;
412
413 $result = $this->controller->changeVisibility($request, $response);
414
415 static::assertSame(302, $result->getStatusCode());
416 static::assertSame(['/subfolder/'], $result->getHeader('location'));
417 }
418}