aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/front/controller/admin/ConfigureController.php2
-rw-r--r--application/front/controller/admin/ManageTagController.php87
-rw-r--r--assets/default/js/base.js6
-rw-r--r--assets/default/scss/shaarli.scss4
-rw-r--r--index.php35
-rw-r--r--tests/front/controller/admin/ManageTagControllerTest.php272
-rw-r--r--tpl/default/page.header.html6
-rw-r--r--tpl/default/tag.list.html2
-rw-r--r--tpl/default/tools.html2
-rw-r--r--tpl/vintage/tools.html2
10 files changed, 376 insertions, 42 deletions
diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php
index b1d32270..5a482d8e 100644
--- a/application/front/controller/admin/ConfigureController.php
+++ b/application/front/controller/admin/ConfigureController.php
@@ -12,7 +12,7 @@ use Slim\Http\Response;
12use Throwable; 12use Throwable;
13 13
14/** 14/**
15 * Class PasswordController 15 * Class ConfigureController
16 * 16 *
17 * Slim controller used to handle Shaarli configuration page (display + save new config). 17 * Slim controller used to handle Shaarli configuration page (display + save new config).
18 */ 18 */
diff --git a/application/front/controller/admin/ManageTagController.php b/application/front/controller/admin/ManageTagController.php
new file mode 100644
index 00000000..e015e613
--- /dev/null
+++ b/application/front/controller/admin/ManageTagController.php
@@ -0,0 +1,87 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Bookmark\BookmarkFilter;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Class ManageTagController
13 *
14 * Slim controller used to handle Shaarli manage tags page (rename and delete tags).
15 */
16class ManageTagController extends ShaarliAdminController
17{
18 /**
19 * GET /manage-tags - Displays the manage tags page
20 */
21 public function index(Request $request, Response $response): Response
22 {
23 $fromTag = $request->getParam('fromtag') ?? '';
24
25 $this->assignView('fromtag', escape($fromTag));
26 $this->assignView(
27 'pagetitle',
28 t('Manage tags') .' - '. $this->container->conf->get('general.title', 'Shaarli')
29 );
30
31 return $response->write($this->render('changetag'));
32 }
33
34 /**
35 * POST /manage-tags - Update or delete provided tag
36 */
37 public function save(Request $request, Response $response): Response
38 {
39 $this->checkToken($request);
40
41 $isDelete = null !== $request->getParam('deletetag') && null === $request->getParam('renametag');
42
43 $fromTag = escape(trim($request->getParam('fromtag') ?? ''));
44 $toTag = escape(trim($request->getParam('totag') ?? ''));
45
46 if (0 === strlen($fromTag) || false === $isDelete && 0 === strlen($toTag)) {
47 $this->saveWarningMessage(t('Invalid tags provided.'));
48
49 return $response->withRedirect('./manage-tags');
50 }
51
52 // TODO: move this to bookmark service
53 $count = 0;
54 $bookmarks = $this->container->bookmarkService->search(['searchtags' => $fromTag], BookmarkFilter::$ALL, true);
55 foreach ($bookmarks as $bookmark) {
56 if (false === $isDelete) {
57 $bookmark->renameTag($fromTag, $toTag);
58 } else {
59 $bookmark->deleteTag($fromTag);
60 }
61
62 $this->container->bookmarkService->set($bookmark, false);
63 $this->container->history->updateLink($bookmark);
64 $count++;
65 }
66
67 $this->container->bookmarkService->save();
68
69 if (true === $isDelete) {
70 $alert = sprintf(
71 t('The tag was removed from %d bookmark.', 'The tag was removed from %d bookmarks.', $count),
72 $count
73 );
74 } else {
75 $alert = sprintf(
76 t('The tag was renamed in %d bookmark.', 'The tag was renamed in %d bookmarks.', $count),
77 $count
78 );
79 }
80
81 $this->saveSuccessMessage($alert);
82
83 $redirect = true === $isDelete ? './manage-tags' : './?searchtags='. urlencode($toTag);
84
85 return $response->withRedirect($redirect);
86 }
87}
diff --git a/assets/default/js/base.js b/assets/default/js/base.js
index f61cfa92..8cc7eed5 100644
--- a/assets/default/js/base.js
+++ b/assets/default/js/base.js
@@ -546,7 +546,7 @@ function init(description) {
546 const refreshedToken = document.getElementById('token').value; 546 const refreshedToken = document.getElementById('token').value;
547 const fromtag = block.getAttribute('data-tag'); 547 const fromtag = block.getAttribute('data-tag');
548 const xhr = new XMLHttpRequest(); 548 const xhr = new XMLHttpRequest();
549 xhr.open('POST', './?do=changetag'); 549 xhr.open('POST', './manage-tags');
550 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 550 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
551 xhr.onload = () => { 551 xhr.onload = () => {
552 if (xhr.status !== 200) { 552 if (xhr.status !== 200) {
@@ -559,7 +559,7 @@ function init(description) {
559 findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none'; 559 findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none';
560 block.querySelector('a.tag-link').innerHTML = htmlEntities(totag); 560 block.querySelector('a.tag-link').innerHTML = htmlEntities(totag);
561 block.querySelector('a.tag-link').setAttribute('href', `./?searchtags=${encodeURIComponent(totag)}`); 561 block.querySelector('a.tag-link').setAttribute('href', `./?searchtags=${encodeURIComponent(totag)}`);
562 block.querySelector('a.rename-tag').setAttribute('href', `./?do=changetag&fromtag=${encodeURIComponent(totag)}`); 562 block.querySelector('a.rename-tag').setAttribute('href', `./manage-tags?fromtag=${encodeURIComponent(totag)}`);
563 563
564 // Refresh awesomplete values 564 // Refresh awesomplete values
565 existingTags = existingTags.map(tag => (tag === fromtag ? totag : tag)); 565 existingTags = existingTags.map(tag => (tag === fromtag ? totag : tag));
@@ -593,7 +593,7 @@ function init(description) {
593 593
594 if (confirm(`Are you sure you want to delete the tag "${tag}"?`)) { 594 if (confirm(`Are you sure you want to delete the tag "${tag}"?`)) {
595 const xhr = new XMLHttpRequest(); 595 const xhr = new XMLHttpRequest();
596 xhr.open('POST', './?do=changetag'); 596 xhr.open('POST', './manage-tags');
597 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 597 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
598 xhr.onload = () => { 598 xhr.onload = () => {
599 block.remove(); 599 block.remove();
diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss
index 243ab1b2..759dff29 100644
--- a/assets/default/scss/shaarli.scss
+++ b/assets/default/scss/shaarli.scss
@@ -490,6 +490,10 @@ body,
490 } 490 }
491} 491}
492 492
493.header-alert-message {
494 text-align: center;
495}
496
493// CONTENT - GENERAL 497// CONTENT - GENERAL
494.container { 498.container {
495 position: relative; 499 position: relative;
diff --git a/index.php b/index.php
index 50c0634a..00e4a40b 100644
--- a/index.php
+++ b/index.php
@@ -519,38 +519,7 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
519 519
520 // -------- User wants to rename a tag or delete it 520 // -------- User wants to rename a tag or delete it
521 if ($targetPage == Router::$PAGE_CHANGETAG) { 521 if ($targetPage == Router::$PAGE_CHANGETAG) {
522 if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) { 522 header('./manage-tags');
523 $PAGE->assign('fromtag', ! empty($_GET['fromtag']) ? escape($_GET['fromtag']) : '');
524 $PAGE->assign('pagetitle', t('Manage tags') .' - '. $conf->get('general.title', 'Shaarli'));
525 $PAGE->renderPage('changetag');
526 exit;
527 }
528
529 if (!$sessionManager->checkToken($_POST['token'])) {
530 die(t('Wrong token.'));
531 }
532
533 $toTag = isset($_POST['totag']) ? escape($_POST['totag']) : null;
534 $fromTag = escape($_POST['fromtag']);
535 $count = 0;
536 $bookmarks = $bookmarkService->search(['searchtags' => $fromTag], BookmarkFilter::$ALL, true);
537 foreach ($bookmarks as $bookmark) {
538 if ($toTag) {
539 $bookmark->renameTag($fromTag, $toTag);
540 } else {
541 $bookmark->deleteTag($fromTag);
542 }
543 $bookmarkService->set($bookmark, false);
544 $history->updateLink($bookmark);
545 $count++;
546 }
547 $bookmarkService->save();
548 $delete = empty($_POST['totag']);
549 $redirect = $delete ? './do=changetag' : 'searchtags='. urlencode(escape($_POST['totag']));
550 $alert = $delete
551 ? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d bookmarks.', $count), $count)
552 : sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d bookmarks.', $count), $count);
553 echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>';
554 exit; 523 exit;
555 } 524 }
556 525
@@ -1380,6 +1349,8 @@ $app->group('', function () {
1380 $this->post('/password', '\Shaarli\Front\Controller\Admin\PasswordController:change')->setName('changePassword'); 1349 $this->post('/password', '\Shaarli\Front\Controller\Admin\PasswordController:change')->setName('changePassword');
1381 $this->get('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:index')->setName('configure'); 1350 $this->get('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:index')->setName('configure');
1382 $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save')->setName('saveConfigure'); 1351 $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save')->setName('saveConfigure');
1352 $this->get('/manage-tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index')->setName('manageTag');
1353 $this->post('/manage-tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save')->setName('saveManageTag');
1383 1354
1384 $this 1355 $this
1385 ->get('/links-per-page', '\Shaarli\Front\Controller\Admin\SessionFilterController:linksPerPage') 1356 ->get('/links-per-page', '\Shaarli\Front\Controller\Admin\SessionFilterController:linksPerPage')
diff --git a/tests/front/controller/admin/ManageTagControllerTest.php b/tests/front/controller/admin/ManageTagControllerTest.php
new file mode 100644
index 00000000..eed99231
--- /dev/null
+++ b/tests/front/controller/admin/ManageTagControllerTest.php
@@ -0,0 +1,272 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Bookmark\BookmarkFilter;
10use Shaarli\Front\Exception\WrongTokenException;
11use Shaarli\Security\SessionManager;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15class ManageTagControllerTest extends TestCase
16{
17 use FrontAdminControllerMockHelper;
18
19 /** @var ManageTagController */
20 protected $controller;
21
22 public function setUp(): void
23 {
24 $this->createContainer();
25
26 $this->controller = new ManageTagController($this->container);
27 }
28
29 /**
30 * Test displaying manage tag page
31 */
32 public function testIndex(): void
33 {
34 $assignedVariables = [];
35 $this->assignTemplateVars($assignedVariables);
36
37 $request = $this->createMock(Request::class);
38 $request->method('getParam')->with('fromtag')->willReturn('fromtag');
39 $response = new Response();
40
41 $result = $this->controller->index($request, $response);
42
43 static::assertSame(200, $result->getStatusCode());
44 static::assertSame('changetag', (string) $result->getBody());
45
46 static::assertSame('fromtag', $assignedVariables['fromtag']);
47 static::assertSame('Manage tags - Shaarli', $assignedVariables['pagetitle']);
48 }
49
50 /**
51 * Test posting a tag update - rename tag - valid info provided.
52 */
53 public function testSaveRenameTagValid(): void
54 {
55 $session = [];
56 $this->assignSessionVars($session);
57
58 $requestParameters = [
59 'renametag' => 'rename',
60 'fromtag' => 'old-tag',
61 'totag' => 'new-tag',
62 ];
63 $request = $this->createMock(Request::class);
64 $request
65 ->expects(static::atLeastOnce())
66 ->method('getParam')
67 ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
68 return $requestParameters[$key] ?? null;
69 })
70 ;
71 $response = new Response();
72
73 $bookmark1 = $this->createMock(Bookmark::class);
74 $bookmark2 = $this->createMock(Bookmark::class);
75 $this->container->bookmarkService
76 ->expects(static::once())
77 ->method('search')
78 ->with(['searchtags' => 'old-tag'], BookmarkFilter::$ALL, true)
79 ->willReturnCallback(function () use ($bookmark1, $bookmark2): array {
80 $bookmark1->expects(static::once())->method('renameTag')->with('old-tag', 'new-tag');
81 $bookmark2->expects(static::once())->method('renameTag')->with('old-tag', 'new-tag');
82
83 return [$bookmark1, $bookmark2];
84 })
85 ;
86 $this->container->bookmarkService
87 ->expects(static::exactly(2))
88 ->method('set')
89 ->withConsecutive([$bookmark1, false], [$bookmark2, false])
90 ;
91 $this->container->bookmarkService->expects(static::once())->method('save');
92
93 $result = $this->controller->save($request, $response);
94
95 static::assertSame(302, $result->getStatusCode());
96 static::assertSame(['./?searchtags=new-tag'], $result->getHeader('location'));
97
98 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
99 static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
100 static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
101 static::assertSame(['The tag was renamed in 2 bookmarks.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
102 }
103
104 /**
105 * Test posting a tag update - delete tag - valid info provided.
106 */
107 public function testSaveDeleteTagValid(): void
108 {
109 $session = [];
110 $this->assignSessionVars($session);
111
112 $requestParameters = [
113 'deletetag' => 'delete',
114 'fromtag' => 'old-tag',
115 ];
116 $request = $this->createMock(Request::class);
117 $request
118 ->expects(static::atLeastOnce())
119 ->method('getParam')
120 ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
121 return $requestParameters[$key] ?? null;
122 })
123 ;
124 $response = new Response();
125
126 $bookmark1 = $this->createMock(Bookmark::class);
127 $bookmark2 = $this->createMock(Bookmark::class);
128 $this->container->bookmarkService
129 ->expects(static::once())
130 ->method('search')
131 ->with(['searchtags' => 'old-tag'], BookmarkFilter::$ALL, true)
132 ->willReturnCallback(function () use ($bookmark1, $bookmark2): array {
133 $bookmark1->expects(static::once())->method('deleteTag')->with('old-tag');
134 $bookmark2->expects(static::once())->method('deleteTag')->with('old-tag');
135
136 return [$bookmark1, $bookmark2];
137 })
138 ;
139 $this->container->bookmarkService
140 ->expects(static::exactly(2))
141 ->method('set')
142 ->withConsecutive([$bookmark1, false], [$bookmark2, false])
143 ;
144 $this->container->bookmarkService->expects(static::once())->method('save');
145
146 $result = $this->controller->save($request, $response);
147
148 static::assertSame(302, $result->getStatusCode());
149 static::assertSame(['./manage-tags'], $result->getHeader('location'));
150
151 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
152 static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
153 static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
154 static::assertSame(['The tag was removed from 2 bookmarks.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
155 }
156
157 /**
158 * Test posting a tag update - wrong token.
159 */
160 public function testSaveWrongToken(): void
161 {
162 $this->container->sessionManager = $this->createMock(SessionManager::class);
163 $this->container->sessionManager->method('checkToken')->willReturn(false);
164
165 $this->container->conf->expects(static::never())->method('set');
166 $this->container->conf->expects(static::never())->method('write');
167
168 $request = $this->createMock(Request::class);
169 $response = new Response();
170
171 $this->expectException(WrongTokenException::class);
172
173 $this->controller->save($request, $response);
174 }
175
176 /**
177 * Test posting a tag update - rename tag - missing "FROM" tag.
178 */
179 public function testSaveRenameTagMissingFrom(): void
180 {
181 $session = [];
182 $this->assignSessionVars($session);
183
184 $requestParameters = [
185 'renametag' => 'rename',
186 ];
187 $request = $this->createMock(Request::class);
188 $request
189 ->expects(static::atLeastOnce())
190 ->method('getParam')
191 ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
192 return $requestParameters[$key] ?? null;
193 })
194 ;
195 $response = new Response();
196
197 $result = $this->controller->save($request, $response);
198
199 static::assertSame(302, $result->getStatusCode());
200 static::assertSame(['./manage-tags'], $result->getHeader('location'));
201
202 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
203 static::assertArrayHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
204 static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
205 static::assertSame(['Invalid tags provided.'], $session[SessionManager::KEY_WARNING_MESSAGES]);
206 }
207
208 /**
209 * Test posting a tag update - delete tag - missing "FROM" tag.
210 */
211 public function testSaveDeleteTagMissingFrom(): void
212 {
213 $session = [];
214 $this->assignSessionVars($session);
215
216 $requestParameters = [
217 'deletetag' => 'delete',
218 ];
219 $request = $this->createMock(Request::class);
220 $request
221 ->expects(static::atLeastOnce())
222 ->method('getParam')
223 ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
224 return $requestParameters[$key] ?? null;
225 })
226 ;
227 $response = new Response();
228
229 $result = $this->controller->save($request, $response);
230
231 static::assertSame(302, $result->getStatusCode());
232 static::assertSame(['./manage-tags'], $result->getHeader('location'));
233
234 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
235 static::assertArrayHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
236 static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
237 static::assertSame(['Invalid tags provided.'], $session[SessionManager::KEY_WARNING_MESSAGES]);
238 }
239
240 /**
241 * Test posting a tag update - rename tag - missing "TO" tag.
242 */
243 public function testSaveRenameTagMissingTo(): void
244 {
245 $session = [];
246 $this->assignSessionVars($session);
247
248 $requestParameters = [
249 'renametag' => 'rename',
250 'fromtag' => 'old-tag'
251 ];
252 $request = $this->createMock(Request::class);
253 $request
254 ->expects(static::atLeastOnce())
255 ->method('getParam')
256 ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
257 return $requestParameters[$key] ?? null;
258 })
259 ;
260 $response = new Response();
261
262 $result = $this->controller->save($request, $response);
263
264 static::assertSame(302, $result->getStatusCode());
265 static::assertSame(['./manage-tags'], $result->getHeader('location'));
266
267 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
268 static::assertArrayHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
269 static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
270 static::assertSame(['Invalid tags provided.'], $session[SessionManager::KEY_WARNING_MESSAGES]);
271 }
272}
diff --git a/tpl/default/page.header.html b/tpl/default/page.header.html
index 4afcca73..bde5036d 100644
--- a/tpl/default/page.header.html
+++ b/tpl/default/page.header.html
@@ -185,7 +185,7 @@
185{/if} 185{/if}
186 186
187{if="!empty($global_errors) && $is_logged_in"} 187{if="!empty($global_errors) && $is_logged_in"}
188 <div class="pure-g new-version-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert"> 188 <div class="pure-g header-alert-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert">
189 <div class="pure-u-2-24"></div> 189 <div class="pure-u-2-24"></div>
190 <div class="pure-u-20-24"> 190 <div class="pure-u-20-24">
191 {loop="$global_errors"} 191 {loop="$global_errors"}
@@ -199,7 +199,7 @@
199{/if} 199{/if}
200 200
201{if="!empty($global_warnings) && $is_logged_in"} 201{if="!empty($global_warnings) && $is_logged_in"}
202 <div class="pure-g pure-alert pure-alert-warning pure-alert-closable" id="shaarli-warnings-alert"> 202 <div class="pure-g header-alert-message pure-alert pure-alert-warning pure-alert-closable" id="shaarli-warnings-alert">
203 <div class="pure-u-2-24"></div> 203 <div class="pure-u-2-24"></div>
204 <div class="pure-u-20-24"> 204 <div class="pure-u-20-24">
205 {loop="global_warnings"} 205 {loop="global_warnings"}
@@ -213,7 +213,7 @@
213{/if} 213{/if}
214 214
215{if="!empty($global_successes) && $is_logged_in"} 215{if="!empty($global_successes) && $is_logged_in"}
216 <div class="pure-g new-version-message pure-alert pure-alert-success pure-alert-closable" id="shaarli-success-alert"> 216 <div class="pure-g header-alert-message new-version-message pure-alert pure-alert-success pure-alert-closable" id="shaarli-success-alert">
217 <div class="pure-u-2-24"></div> 217 <div class="pure-u-2-24"></div>
218 <div class="pure-u-20-24"> 218 <div class="pure-u-20-24">
219 {loop="$global_successes"} 219 {loop="$global_successes"}
diff --git a/tpl/default/tag.list.html b/tpl/default/tag.list.html
index 3e498f89..3adcfd1f 100644
--- a/tpl/default/tag.list.html
+++ b/tpl/default/tag.list.html
@@ -51,7 +51,7 @@
51 <div class="pure-u-1"> 51 <div class="pure-u-1">
52 {if="$is_logged_in===true"} 52 {if="$is_logged_in===true"}
53 <a href="#" class="delete-tag" aria-label="{'Delete'|t}"><i class="fa fa-trash" aria-hidden="true"></i></a>&nbsp;&nbsp; 53 <a href="#" class="delete-tag" aria-label="{'Delete'|t}"><i class="fa fa-trash" aria-hidden="true"></i></a>&nbsp;&nbsp;
54 <a href="./?do=changetag&fromtag={$key|urlencode}" class="rename-tag" aria-label="{'Rename tag'|t}"> 54 <a href="./manage-tags?fromtag={$key|urlencode}" class="rename-tag" aria-label="{'Rename tag'|t}">
55 <i class="fa fa-pencil-square-o {$key}" aria-hidden="true"></i> 55 <i class="fa fa-pencil-square-o {$key}" aria-hidden="true"></i>
56 </a> 56 </a>
57 {/if} 57 {/if}
diff --git a/tpl/default/tools.html b/tpl/default/tools.html
index 0135c480..6e432e00 100644
--- a/tpl/default/tools.html
+++ b/tpl/default/tools.html
@@ -28,7 +28,7 @@
28 </div> 28 </div>
29 {/if} 29 {/if}
30 <div class="tools-item"> 30 <div class="tools-item">
31 <a href="./?do=changetag" title="{'Rename or delete a tag in all links'|t}"> 31 <a href="./manage-tags" title="{'Rename or delete a tag in all links'|t}">
32 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Manage tags'|t}</span> 32 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Manage tags'|t}</span>
33 </a> 33 </a>
34 </div> 34 </div>
diff --git a/tpl/vintage/tools.html b/tpl/vintage/tools.html
index 0d8fcdec..8f606efb 100644
--- a/tpl/vintage/tools.html
+++ b/tpl/vintage/tools.html
@@ -11,7 +11,7 @@
11 <br><br> 11 <br><br>
12 {if="!$openshaarli"}<a href="?do=changepasswd"><b>Change password</b><span>: Change your password.</span></a> 12 {if="!$openshaarli"}<a href="?do=changepasswd"><b>Change password</b><span>: Change your password.</span></a>
13 <br><br>{/if} 13 <br><br>{/if}
14 <a href="./?do=changetag"><b>Rename/delete tags</b><span>: Rename or delete a tag in all links</span></a> 14 <a href="./manage-tags"><b>Rename/delete tags</b><span>: Rename or delete a tag in all links</span></a>
15 <br><br> 15 <br><br>
16 <a href="./?do=import"><b>Import</b><span>: Import Netscape html bookmarks (as exported from Firefox, Chrome, Opera, delicious...)</span></a> 16 <a href="./?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>