diff options
-rw-r--r-- | application/front/controller/admin/ExportController.php | 92 | ||||
-rw-r--r-- | doc/md/Translations.md | 2 | ||||
-rw-r--r-- | index.php | 47 | ||||
-rw-r--r-- | tests/front/controller/admin/ExportControllerTest.php | 167 | ||||
-rw-r--r-- | tpl/default/export.html | 3 | ||||
-rw-r--r-- | tpl/default/tools.html | 2 | ||||
-rw-r--r-- | tpl/vintage/export.html | 3 | ||||
-rw-r--r-- | tpl/vintage/tools.html | 2 |
8 files changed, 267 insertions, 51 deletions
diff --git a/application/front/controller/admin/ExportController.php b/application/front/controller/admin/ExportController.php new file mode 100644 index 00000000..8e0e5a56 --- /dev/null +++ b/application/front/controller/admin/ExportController.php | |||
@@ -0,0 +1,92 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use DateTime; | ||
8 | use Shaarli\Bookmark\Bookmark; | ||
9 | use Slim\Http\Request; | ||
10 | use Slim\Http\Response; | ||
11 | |||
12 | /** | ||
13 | * Class ExportController | ||
14 | * | ||
15 | * Slim controller used to display Shaarli data export page, | ||
16 | * and process the bookmarks export as a Netscape Bookmarks file. | ||
17 | */ | ||
18 | class ExportController extends ShaarliAdminController | ||
19 | { | ||
20 | /** | ||
21 | * GET /admin/export - Display export page | ||
22 | */ | ||
23 | public function index(Request $request, Response $response): Response | ||
24 | { | ||
25 | $this->assignView('pagetitle', t('Export') .' - '. $this->container->conf->get('general.title', 'Shaarli')); | ||
26 | |||
27 | return $response->write($this->render('export')); | ||
28 | } | ||
29 | |||
30 | /** | ||
31 | * POST /admin/export - Process export, and serve download file named | ||
32 | * bookmarks_(all|private|public)_datetime.html | ||
33 | */ | ||
34 | public function export(Request $request, Response $response): Response | ||
35 | { | ||
36 | $selection = $request->getParam('selection'); | ||
37 | |||
38 | if (empty($selection)) { | ||
39 | $this->saveErrorMessage(t('Please select an export mode.')); | ||
40 | |||
41 | return $this->redirect($response, '/admin/export'); | ||
42 | } | ||
43 | |||
44 | $prependNoteUrl = filter_var($request->getParam('prepend_note_url') ?? false, FILTER_VALIDATE_BOOLEAN); | ||
45 | |||
46 | try { | ||
47 | $formatter = $this->container->formatterFactory->getFormatter('raw'); | ||
48 | |||
49 | $this->assignView( | ||
50 | 'links', | ||
51 | $this->container->netscapeBookmarkUtils->filterAndFormat( | ||
52 | $formatter, | ||
53 | $selection, | ||
54 | $prependNoteUrl, | ||
55 | index_url($this->container->environment) | ||
56 | ) | ||
57 | ); | ||
58 | } catch (\Exception $exc) { | ||
59 | $this->saveErrorMessage($exc->getMessage()); | ||
60 | |||
61 | return $this->redirect($response, '/admin/export'); | ||
62 | } | ||
63 | |||
64 | $now = new DateTime(); | ||
65 | $response = $response->withHeader('Content-Type', 'text/html; charset=utf-8'); | ||
66 | $response = $response->withHeader( | ||
67 | 'Content-disposition', | ||
68 | 'attachment; filename=bookmarks_'.$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html' | ||
69 | ); | ||
70 | |||
71 | $this->assignView('date', $now->format(DateTime::RFC822)); | ||
72 | $this->assignView('eol', PHP_EOL); | ||
73 | $this->assignView('selection', $selection); | ||
74 | |||
75 | return $response->write($this->render('export.bookmarks')); | ||
76 | } | ||
77 | |||
78 | /** | ||
79 | * @param mixed[] $data Variables passed to the template engine | ||
80 | * | ||
81 | * @return mixed[] Template data after active plugins render_picwall hook execution. | ||
82 | */ | ||
83 | protected function executeHooks(array $data): array | ||
84 | { | ||
85 | $this->container->pluginManager->executeHooks( | ||
86 | 'render_tools', | ||
87 | $data | ||
88 | ); | ||
89 | |||
90 | return $data; | ||
91 | } | ||
92 | } | ||
diff --git a/doc/md/Translations.md b/doc/md/Translations.md index 75eeed7d..af2c3daa 100644 --- a/doc/md/Translations.md +++ b/doc/md/Translations.md | |||
@@ -39,7 +39,7 @@ http://<replace_domain>/admin/configure | |||
39 | http://<replace_domain>/admin/tools | 39 | http://<replace_domain>/admin/tools |
40 | http://<replace_domain>/daily | 40 | http://<replace_domain>/daily |
41 | http://<replace_domain>/?post | 41 | http://<replace_domain>/?post |
42 | http://<replace_domain>/?do=export | 42 | http://<replace_domain>/admin/export |
43 | http://<replace_domain>/?do=import | 43 | http://<replace_domain>/?do=import |
44 | http://<replace_domain>/login | 44 | http://<replace_domain>/login |
45 | http://<replace_domain>/picture-wall | 45 | http://<replace_domain>/picture-wall |
@@ -573,50 +573,7 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM | |||
573 | } | 573 | } |
574 | 574 | ||
575 | if ($targetPage == Router::$PAGE_EXPORT) { | 575 | if ($targetPage == Router::$PAGE_EXPORT) { |
576 | // Export bookmarks as a Netscape Bookmarks file | 576 | header('Location: ./admin/export'); |
577 | |||
578 | if (empty($_GET['selection'])) { | ||
579 | $PAGE->assign('pagetitle', t('Export') .' - '. $conf->get('general.title', 'Shaarli')); | ||
580 | $PAGE->renderPage('export'); | ||
581 | exit; | ||
582 | } | ||
583 | |||
584 | // export as bookmarks_(all|private|public)_YYYYmmdd_HHMMSS.html | ||
585 | $selection = $_GET['selection']; | ||
586 | if (isset($_GET['prepend_note_url'])) { | ||
587 | $prependNoteUrl = $_GET['prepend_note_url']; | ||
588 | } else { | ||
589 | $prependNoteUrl = false; | ||
590 | } | ||
591 | |||
592 | try { | ||
593 | $factory = new FormatterFactory($conf, $loginManager->isLoggedIn()); | ||
594 | $formatter = $factory->getFormatter('raw'); | ||
595 | $PAGE->assign( | ||
596 | 'links', | ||
597 | NetscapeBookmarkUtils::filterAndFormat( | ||
598 | $bookmarkService, | ||
599 | $formatter, | ||
600 | $selection, | ||
601 | $prependNoteUrl, | ||
602 | index_url($_SERVER) | ||
603 | ) | ||
604 | ); | ||
605 | } catch (Exception $exc) { | ||
606 | header('Content-Type: text/plain; charset=utf-8'); | ||
607 | echo $exc->getMessage(); | ||
608 | exit; | ||
609 | } | ||
610 | $now = new DateTime(); | ||
611 | header('Content-Type: text/html; charset=utf-8'); | ||
612 | header( | ||
613 | 'Content-disposition: attachment; filename=bookmarks_' | ||
614 | .$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html' | ||
615 | ); | ||
616 | $PAGE->assign('date', $now->format(DateTime::RFC822)); | ||
617 | $PAGE->assign('eol', PHP_EOL); | ||
618 | $PAGE->assign('selection', $selection); | ||
619 | $PAGE->renderPage('export.bookmarks'); | ||
620 | exit; | 577 | exit; |
621 | } | 578 | } |
622 | 579 | ||
@@ -1105,6 +1062,8 @@ $app->group('', function () { | |||
1105 | $this->get('/admin/shaare/delete', '\Shaarli\Front\Controller\Admin\ManageShaareController:deleteBookmark'); | 1062 | $this->get('/admin/shaare/delete', '\Shaarli\Front\Controller\Admin\ManageShaareController:deleteBookmark'); |
1106 | $this->get('/admin/shaare/visibility', '\Shaarli\Front\Controller\Admin\ManageShaareController:changeVisibility'); | 1063 | $this->get('/admin/shaare/visibility', '\Shaarli\Front\Controller\Admin\ManageShaareController:changeVisibility'); |
1107 | $this->get('/admin/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ManageShaareController:pinBookmark'); | 1064 | $this->get('/admin/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ManageShaareController:pinBookmark'); |
1065 | $this->get('/admin/export', '\Shaarli\Front\Controller\Admin\ExportController:index'); | ||
1066 | $this->post('/admin/export', '\Shaarli\Front\Controller\Admin\ExportController:export'); | ||
1108 | 1067 | ||
1109 | $this->get('/links-per-page', '\Shaarli\Front\Controller\Admin\SessionFilterController:linksPerPage'); | 1068 | $this->get('/links-per-page', '\Shaarli\Front\Controller\Admin\SessionFilterController:linksPerPage'); |
1110 | $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility'); | 1069 | $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility'); |
diff --git a/tests/front/controller/admin/ExportControllerTest.php b/tests/front/controller/admin/ExportControllerTest.php new file mode 100644 index 00000000..e43a9626 --- /dev/null +++ b/tests/front/controller/admin/ExportControllerTest.php | |||
@@ -0,0 +1,167 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace front\controller\admin; | ||
6 | |||
7 | use PHPUnit\Framework\TestCase; | ||
8 | use Shaarli\Bookmark\Bookmark; | ||
9 | use Shaarli\Formatter\BookmarkFormatter; | ||
10 | use Shaarli\Formatter\BookmarkRawFormatter; | ||
11 | use Shaarli\Front\Controller\Admin\ExportController; | ||
12 | use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper; | ||
13 | use Shaarli\Netscape\NetscapeBookmarkUtils; | ||
14 | use Shaarli\Security\SessionManager; | ||
15 | use Slim\Http\Request; | ||
16 | use Slim\Http\Response; | ||
17 | |||
18 | class ExportControllerTest extends TestCase | ||
19 | { | ||
20 | use FrontAdminControllerMockHelper; | ||
21 | |||
22 | /** @var ExportController */ | ||
23 | protected $controller; | ||
24 | |||
25 | public function setUp(): void | ||
26 | { | ||
27 | $this->createContainer(); | ||
28 | |||
29 | $this->controller = new ExportController($this->container); | ||
30 | } | ||
31 | |||
32 | /** | ||
33 | * Test displaying export page | ||
34 | */ | ||
35 | public function testIndex(): void | ||
36 | { | ||
37 | $assignedVariables = []; | ||
38 | $this->assignTemplateVars($assignedVariables); | ||
39 | |||
40 | $request = $this->createMock(Request::class); | ||
41 | $response = new Response(); | ||
42 | |||
43 | $result = $this->controller->index($request, $response); | ||
44 | |||
45 | static::assertSame(200, $result->getStatusCode()); | ||
46 | static::assertSame('export', (string) $result->getBody()); | ||
47 | |||
48 | static::assertSame('Export - Shaarli', $assignedVariables['pagetitle']); | ||
49 | } | ||
50 | |||
51 | /** | ||
52 | * Test posting an export request | ||
53 | */ | ||
54 | public function testExportDefault(): void | ||
55 | { | ||
56 | $assignedVariables = []; | ||
57 | $this->assignTemplateVars($assignedVariables); | ||
58 | |||
59 | $parameters = [ | ||
60 | 'selection' => 'all', | ||
61 | 'prepend_note_url' => 'on', | ||
62 | ]; | ||
63 | |||
64 | $request = $this->createMock(Request::class); | ||
65 | $request->method('getParam')->willReturnCallback(function (string $key) use ($parameters) { | ||
66 | return $parameters[$key] ?? null; | ||
67 | }); | ||
68 | $response = new Response(); | ||
69 | |||
70 | $bookmarks = [ | ||
71 | (new Bookmark())->setUrl('http://link1.tld')->setTitle('Title 1'), | ||
72 | (new Bookmark())->setUrl('http://link2.tld')->setTitle('Title 2'), | ||
73 | ]; | ||
74 | |||
75 | $this->container->netscapeBookmarkUtils = $this->createMock(NetscapeBookmarkUtils::class); | ||
76 | $this->container->netscapeBookmarkUtils | ||
77 | ->expects(static::once()) | ||
78 | ->method('filterAndFormat') | ||
79 | ->willReturnCallback( | ||
80 | function ( | ||
81 | BookmarkFormatter $formatter, | ||
82 | string $selection, | ||
83 | bool $prependNoteUrl, | ||
84 | string $indexUrl | ||
85 | ) use ($parameters, $bookmarks): array { | ||
86 | static::assertInstanceOf(BookmarkRawFormatter::class, $formatter); | ||
87 | static::assertSame($parameters['selection'], $selection); | ||
88 | static::assertTrue($prependNoteUrl); | ||
89 | static::assertSame('http://shaarli', $indexUrl); | ||
90 | |||
91 | return $bookmarks; | ||
92 | } | ||
93 | ) | ||
94 | ; | ||
95 | |||
96 | $result = $this->controller->export($request, $response); | ||
97 | |||
98 | static::assertSame(200, $result->getStatusCode()); | ||
99 | static::assertSame('export.bookmarks', (string) $result->getBody()); | ||
100 | static::assertSame(['text/html; charset=utf-8'], $result->getHeader('content-type')); | ||
101 | static::assertRegExp( | ||
102 | '/attachment; filename=bookmarks_all_[\d]{8}_[\d]{6}\.html/', | ||
103 | $result->getHeader('content-disposition')[0] | ||
104 | ); | ||
105 | |||
106 | static::assertNotEmpty($assignedVariables['date']); | ||
107 | static::assertSame(PHP_EOL, $assignedVariables['eol']); | ||
108 | static::assertSame('all', $assignedVariables['selection']); | ||
109 | static::assertSame($bookmarks, $assignedVariables['links']); | ||
110 | } | ||
111 | |||
112 | /** | ||
113 | * Test posting an export request - without selection parameter | ||
114 | */ | ||
115 | public function testExportSelectionMissing(): void | ||
116 | { | ||
117 | $request = $this->createMock(Request::class); | ||
118 | $response = new Response(); | ||
119 | |||
120 | $this->container->sessionManager = $this->createMock(SessionManager::class); | ||
121 | $this->container->sessionManager | ||
122 | ->expects(static::once()) | ||
123 | ->method('setSessionParameter') | ||
124 | ->with(SessionManager::KEY_ERROR_MESSAGES, ['Please select an export mode.']) | ||
125 | ; | ||
126 | |||
127 | $result = $this->controller->export($request, $response); | ||
128 | |||
129 | static::assertSame(302, $result->getStatusCode()); | ||
130 | static::assertSame(['/subfolder/admin/export'], $result->getHeader('location')); | ||
131 | } | ||
132 | |||
133 | /** | ||
134 | * Test posting an export request - without selection parameter | ||
135 | */ | ||
136 | public function testExportErrorEncountered(): void | ||
137 | { | ||
138 | $parameters = [ | ||
139 | 'selection' => 'all', | ||
140 | ]; | ||
141 | |||
142 | $request = $this->createMock(Request::class); | ||
143 | $request->method('getParam')->willReturnCallback(function (string $key) use ($parameters) { | ||
144 | return $parameters[$key] ?? null; | ||
145 | }); | ||
146 | $response = new Response(); | ||
147 | |||
148 | $this->container->netscapeBookmarkUtils = $this->createMock(NetscapeBookmarkUtils::class); | ||
149 | $this->container->netscapeBookmarkUtils | ||
150 | ->expects(static::once()) | ||
151 | ->method('filterAndFormat') | ||
152 | ->willThrowException(new \Exception($message = 'error message')); | ||
153 | ; | ||
154 | |||
155 | $this->container->sessionManager = $this->createMock(SessionManager::class); | ||
156 | $this->container->sessionManager | ||
157 | ->expects(static::once()) | ||
158 | ->method('setSessionParameter') | ||
159 | ->with(SessionManager::KEY_ERROR_MESSAGES, [$message]) | ||
160 | ; | ||
161 | |||
162 | $result = $this->controller->export($request, $response); | ||
163 | |||
164 | static::assertSame(302, $result->getStatusCode()); | ||
165 | static::assertSame(['/subfolder/admin/export'], $result->getHeader('location')); | ||
166 | } | ||
167 | } | ||
diff --git a/tpl/default/export.html b/tpl/default/export.html index 91cf78b6..c9c92943 100644 --- a/tpl/default/export.html +++ b/tpl/default/export.html | |||
@@ -6,14 +6,13 @@ | |||
6 | <body> | 6 | <body> |
7 | {include="page.header"} | 7 | {include="page.header"} |
8 | 8 | ||
9 | <form method="GET" action="{$base_path}/?do=export" name="exportform" id="exportform"> | 9 | <form method="POST" action="{$base_path}/admin/export" name="exportform" id="exportform"> |
10 | <div class="pure-g"> | 10 | <div class="pure-g"> |
11 | <div class="pure-u-lg-1-4 pure-u-1-24"></div> | 11 | <div class="pure-u-lg-1-4 pure-u-1-24"></div> |
12 | <div class="pure-u-lg-1-2 pure-u-22-24 page-form page-form-complete"> | 12 | <div class="pure-u-lg-1-2 pure-u-22-24 page-form page-form-complete"> |
13 | <div> | 13 | <div> |
14 | <h2 class="window-title">{"Export Database"|t}</h2> | 14 | <h2 class="window-title">{"Export Database"|t}</h2> |
15 | </div> | 15 | </div> |
16 | <input type="hidden" name="do" value="export"> | ||
17 | <input type="hidden" name="token" value="{$token}"> | 16 | <input type="hidden" name="token" value="{$token}"> |
18 | 17 | ||
19 | <div class="pure-g"> | 18 | <div class="pure-g"> |
diff --git a/tpl/default/tools.html b/tpl/default/tools.html index d07dabd0..fa007460 100644 --- a/tpl/default/tools.html +++ b/tpl/default/tools.html | |||
@@ -39,7 +39,7 @@ | |||
39 | </a> | 39 | </a> |
40 | </div> | 40 | </div> |
41 | <div class="tools-item"> | 41 | <div class="tools-item"> |
42 | <a href="{$base_path}/?do=export" | 42 | <a href="{$base_path}/admin/export" |
43 | title="{'Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)'|t}"> | 43 | title="{'Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)'|t}"> |
44 | <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Export database'|t}</span> | 44 | <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Export database'|t}</span> |
45 | </a> | 45 | </a> |
diff --git a/tpl/vintage/export.html b/tpl/vintage/export.html index 67c3d05f..feee307c 100644 --- a/tpl/vintage/export.html +++ b/tpl/vintage/export.html | |||
@@ -5,8 +5,7 @@ | |||
5 | <div id="pageheader"> | 5 | <div id="pageheader"> |
6 | {include="page.header"} | 6 | {include="page.header"} |
7 | <div id="toolsdiv"> | 7 | <div id="toolsdiv"> |
8 | <form method="GET"> | 8 | <form method="POST" action="{$base_path}/admin/export"> |
9 | <input type="hidden" name="do" value="export"> | ||
10 | Selection:<br> | 9 | Selection:<br> |
11 | <input type="radio" name="selection" value="all" checked="true"> All<br> | 10 | <input type="radio" name="selection" value="all" checked="true"> All<br> |
12 | <input type="radio" name="selection" value="private"> Private<br> | 11 | <input type="radio" name="selection" value="private"> Private<br> |
diff --git a/tpl/vintage/tools.html b/tpl/vintage/tools.html index 7b69f43e..67ebd175 100644 --- a/tpl/vintage/tools.html +++ b/tpl/vintage/tools.html | |||
@@ -15,7 +15,7 @@ | |||
15 | <br><br> | 15 | <br><br> |
16 | <a href="{$base_path}/?do=import"><b>Import</b><span>: Import Netscape html bookmarks (as exported from Firefox, Chrome, Opera, delicious...)</span></a> | 16 | <a href="{$base_path}/?do=import"><b>Import</b><span>: Import Netscape html bookmarks (as exported from Firefox, Chrome, Opera, delicious...)</span></a> |
17 | <br><br> | 17 | <br><br> |
18 | <a href="{$base_path}/?do=export"><b>Export</b><span>: Export Netscape html bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)</span></a> | 18 | <a href="{$base_path}/admin/export"><b>Export</b><span>: Export Netscape html bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)</span></a> |
19 | <br><br> | 19 | <br><br> |
20 | <a class="smallbutton" | 20 | <a class="smallbutton" |
21 | onclick="return alertBookmarklet();" | 21 | onclick="return alertBookmarklet();" |