aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/front/controller/admin/ExportController.php92
-rw-r--r--doc/md/Translations.md2
-rw-r--r--index.php47
-rw-r--r--tests/front/controller/admin/ExportControllerTest.php167
-rw-r--r--tpl/default/export.html3
-rw-r--r--tpl/default/tools.html2
-rw-r--r--tpl/vintage/export.html3
-rw-r--r--tpl/vintage/tools.html2
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
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use DateTime;
8use Shaarli\Bookmark\Bookmark;
9use Slim\Http\Request;
10use 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 */
18class 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
39http://<replace_domain>/admin/tools 39http://<replace_domain>/admin/tools
40http://<replace_domain>/daily 40http://<replace_domain>/daily
41http://<replace_domain>/?post 41http://<replace_domain>/?post
42http://<replace_domain>/?do=export 42http://<replace_domain>/admin/export
43http://<replace_domain>/?do=import 43http://<replace_domain>/?do=import
44http://<replace_domain>/login 44http://<replace_domain>/login
45http://<replace_domain>/picture-wall 45http://<replace_domain>/picture-wall
diff --git a/index.php b/index.php
index 7c49bc8d..030fdfa3 100644
--- a/index.php
+++ b/index.php
@@ -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
3declare(strict_types=1);
4
5namespace front\controller\admin;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Formatter\BookmarkFormatter;
10use Shaarli\Formatter\BookmarkRawFormatter;
11use Shaarli\Front\Controller\Admin\ExportController;
12use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
13use Shaarli\Netscape\NetscapeBookmarkUtils;
14use Shaarli\Security\SessionManager;
15use Slim\Http\Request;
16use Slim\Http\Response;
17
18class 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();"