aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2020-06-20 15:14:24 +0200
committerArthurHoaro <arthur@hoa.ro>2020-07-23 21:19:21 +0200
commit1b8620b1ad4e2c647ff2d032c8e7c6687b6647a1 (patch)
tree98a76bf93f9ed84680daa06050680a7c0425e535
parent78657347c5b463d7c22bfc8c87b7db39fe058833 (diff)
downloadShaarli-1b8620b1ad4e2c647ff2d032c8e7c6687b6647a1.tar.gz
Shaarli-1b8620b1ad4e2c647ff2d032c8e7c6687b6647a1.tar.zst
Shaarli-1b8620b1ad4e2c647ff2d032c8e7c6687b6647a1.zip
Process plugins administration page through Slim controllers
-rw-r--r--application/container/ContainerBuilder.php7
-rw-r--r--application/front/controller/admin/PluginsController.php98
-rw-r--r--application/plugin/PluginManager.php2
-rw-r--r--doc/md/Translations.md2
-rw-r--r--index.php54
-rw-r--r--plugins/wallabag/README.md2
-rw-r--r--tests/front/controller/admin/PluginsControllerTest.php190
-rw-r--r--tpl/default/includes.html4
-rw-r--r--tpl/default/page.footer.html2
-rw-r--r--tpl/default/pluginsadmin.html5
-rw-r--r--tpl/default/tools.html2
-rw-r--r--tpl/vintage/pluginsadmin.html4
-rw-r--r--tpl/vintage/tools.html2
13 files changed, 312 insertions, 62 deletions
diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php
index a4fd6a0c..ba91fe8b 100644
--- a/application/container/ContainerBuilder.php
+++ b/application/container/ContainerBuilder.php
@@ -88,7 +88,12 @@ class ContainerBuilder
88 }; 88 };
89 89
90 $container['pluginManager'] = function (ShaarliContainer $container): PluginManager { 90 $container['pluginManager'] = function (ShaarliContainer $container): PluginManager {
91 return new PluginManager($container->conf); 91 $pluginManager = new PluginManager($container->conf);
92
93 // FIXME! Configuration is already injected
94 $pluginManager->load($container->conf->get('general.enabled_plugins'));
95
96 return $pluginManager;
92 }; 97 };
93 98
94 $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory { 99 $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory {
diff --git a/application/front/controller/admin/PluginsController.php b/application/front/controller/admin/PluginsController.php
new file mode 100644
index 00000000..d5ec91f0
--- /dev/null
+++ b/application/front/controller/admin/PluginsController.php
@@ -0,0 +1,98 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Exception;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Class PluginsController
13 *
14 * Slim controller used to handle Shaarli plugins configuration page (display + save new config).
15 */
16class PluginsController extends ShaarliAdminController
17{
18 /**
19 * GET /admin/plugins - Displays the configuration page
20 */
21 public function index(Request $request, Response $response): Response
22 {
23 $pluginMeta = $this->container->pluginManager->getPluginsMeta();
24
25 // Split plugins into 2 arrays: ordered enabled plugins and disabled.
26 $enabledPlugins = array_filter($pluginMeta, function ($v) {
27 return ($v['order'] ?? false) !== false;
28 });
29 $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $this->container->conf->get('plugins', []));
30 uasort(
31 $enabledPlugins,
32 function ($a, $b) {
33 return $a['order'] - $b['order'];
34 }
35 );
36 $disabledPlugins = array_filter($pluginMeta, function ($v) {
37 return ($v['order'] ?? false) === false;
38 });
39
40 $this->assignView('enabledPlugins', $enabledPlugins);
41 $this->assignView('disabledPlugins', $disabledPlugins);
42 $this->assignView(
43 'pagetitle',
44 t('Plugin Administration') .' - '. $this->container->conf->get('general.title', 'Shaarli')
45 );
46
47 return $response->write($this->render('pluginsadmin'));
48 }
49
50 /**
51 * POST /admin/plugins - Update Shaarli's configuration
52 */
53 public function save(Request $request, Response $response): Response
54 {
55 $this->checkToken($request);
56
57 try {
58 $parameters = $request->getParams() ?? [];
59
60 $this->executeHooks($parameters);
61
62 if (isset($parameters['parameters_form'])) {
63 unset($parameters['parameters_form']);
64 foreach ($parameters as $param => $value) {
65 $this->container->conf->set('plugins.'. $param, escape($value));
66 }
67 } else {
68 $this->container->conf->set('general.enabled_plugins', save_plugin_config($parameters));
69 }
70
71 $this->container->conf->write($this->container->loginManager->isLoggedIn());
72 $this->container->history->updateSettings();
73
74 $this->saveSuccessMessage(t('Setting successfully saved.'));
75 } catch (Exception $e) {
76 $this->saveErrorMessage(
77 t('ERROR while saving plugin configuration: ') . PHP_EOL . $e->getMessage()
78 );
79 }
80
81 return $this->redirect($response, '/admin/plugins');
82 }
83
84 /**
85 * @param mixed[] $data Variables passed to the template engine
86 *
87 * @return mixed[] Template data after active plugins render_picwall hook execution.
88 */
89 protected function executeHooks(array $data): array
90 {
91 $this->container->pluginManager->executeHooks(
92 'save_plugin_parameters',
93 $data
94 );
95
96 return $data;
97 }
98}
diff --git a/application/plugin/PluginManager.php b/application/plugin/PluginManager.php
index f7b24a8e..591a9180 100644
--- a/application/plugin/PluginManager.php
+++ b/application/plugin/PluginManager.php
@@ -16,7 +16,7 @@ class PluginManager
16 * 16 *
17 * @var array $authorizedPlugins 17 * @var array $authorizedPlugins
18 */ 18 */
19 private $authorizedPlugins; 19 private $authorizedPlugins = [];
20 20
21 /** 21 /**
22 * List of loaded plugins. 22 * List of loaded plugins.
diff --git a/doc/md/Translations.md b/doc/md/Translations.md
index df86f4d4..dd42bf30 100644
--- a/doc/md/Translations.md
+++ b/doc/md/Translations.md
@@ -43,7 +43,7 @@ http://<replace_domain>/admin/export
43http://<replace_domain>/admin/import 43http://<replace_domain>/admin/import
44http://<replace_domain>/login 44http://<replace_domain>/login
45http://<replace_domain>/picture-wall 45http://<replace_domain>/picture-wall
46http://<replace_domain>/?do=pluginadmin 46http://<replace_domain>/admin/plugins
47http://<replace_domain>/tags/cloud 47http://<replace_domain>/tags/cloud
48http://<replace_domain>/tags/list 48http://<replace_domain>/tags/list
49``` 49```
diff --git a/index.php b/index.php
index 47fef3ed..1571df60 100644
--- a/index.php
+++ b/index.php
@@ -584,60 +584,14 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
584 584
585 // Plugin administration page 585 // Plugin administration page
586 if ($targetPage == Router::$PAGE_PLUGINSADMIN) { 586 if ($targetPage == Router::$PAGE_PLUGINSADMIN) {
587 $pluginMeta = $pluginManager->getPluginsMeta(); 587 header('Location: ./admin/plugins');
588
589 // Split plugins into 2 arrays: ordered enabled plugins and disabled.
590 $enabledPlugins = array_filter($pluginMeta, function ($v) {
591 return $v['order'] !== false;
592 });
593 // Load parameters.
594 $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $conf->get('plugins', array()));
595 uasort(
596 $enabledPlugins,
597 function ($a, $b) {
598 return $a['order'] - $b['order'];
599 }
600 );
601 $disabledPlugins = array_filter($pluginMeta, function ($v) {
602 return $v['order'] === false;
603 });
604
605 $PAGE->assign('enabledPlugins', $enabledPlugins);
606 $PAGE->assign('disabledPlugins', $disabledPlugins);
607 $PAGE->assign('pagetitle', t('Plugin administration') .' - '. $conf->get('general.title', 'Shaarli'));
608 $PAGE->renderPage('pluginsadmin');
609 exit; 588 exit;
610 } 589 }
611 590
612 // Plugin administration form action 591 // Plugin administration form action
613 if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) { 592 if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) {
614 try { 593 // This route is no longer supported in legacy mode
615 if (isset($_POST['parameters_form'])) { 594 header('Location: ./admin/plugins');
616 $pluginManager->executeHooks('save_plugin_parameters', $_POST);
617 unset($_POST['parameters_form']);
618 foreach ($_POST as $param => $value) {
619 $conf->set('plugins.'. $param, escape($value));
620 }
621 } else {
622 $conf->set('general.enabled_plugins', save_plugin_config($_POST));
623 }
624 $conf->write($loginManager->isLoggedIn());
625 $history->updateSettings();
626 } catch (Exception $e) {
627 error_log(
628 'ERROR while saving plugin configuration:.' . PHP_EOL .
629 $e->getMessage()
630 );
631
632 // TODO: do not handle exceptions/errors in JS.
633 echo '<script>alert("'
634 . $e->getMessage()
635 .'");document.location=\'./?do='
636 . Router::$PAGE_PLUGINSADMIN
637 .'\';</script>';
638 exit;
639 }
640 header('Location: ./?do='. Router::$PAGE_PLUGINSADMIN);
641 exit; 595 exit;
642 } 596 }
643 597
@@ -1022,6 +976,8 @@ $app->group('', function () {
1022 $this->post('/admin/export', '\Shaarli\Front\Controller\Admin\ExportController:export'); 976 $this->post('/admin/export', '\Shaarli\Front\Controller\Admin\ExportController:export');
1023 $this->get('/admin/import', '\Shaarli\Front\Controller\Admin\ImportController:index'); 977 $this->get('/admin/import', '\Shaarli\Front\Controller\Admin\ImportController:index');
1024 $this->post('/admin/import', '\Shaarli\Front\Controller\Admin\ImportController:import'); 978 $this->post('/admin/import', '\Shaarli\Front\Controller\Admin\ImportController:import');
979 $this->get('/admin/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index');
980 $this->post('/admin/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save');
1025 981
1026 $this->get('/links-per-page', '\Shaarli\Front\Controller\Admin\SessionFilterController:linksPerPage'); 982 $this->get('/links-per-page', '\Shaarli\Front\Controller\Admin\SessionFilterController:linksPerPage');
1027 $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility'); 983 $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility');
diff --git a/plugins/wallabag/README.md b/plugins/wallabag/README.md
index ea21a519..c53a04d9 100644
--- a/plugins/wallabag/README.md
+++ b/plugins/wallabag/README.md
@@ -21,7 +21,7 @@ The directory structure should look like:
21 21
22To enable the plugin, you can either: 22To enable the plugin, you can either:
23 23
24 * enable it in the plugins administration page (`?do=pluginadmin`). 24 * enable it in the plugins administration page (`/admin/plugins`).
25 * add `wallabag` to your list of enabled plugins in `data/config.json.php` (`general.enabled_plugins` section). 25 * add `wallabag` to your list of enabled plugins in `data/config.json.php` (`general.enabled_plugins` section).
26 26
27### Configuration 27### Configuration
diff --git a/tests/front/controller/admin/PluginsControllerTest.php b/tests/front/controller/admin/PluginsControllerTest.php
new file mode 100644
index 00000000..700a0df2
--- /dev/null
+++ b/tests/front/controller/admin/PluginsControllerTest.php
@@ -0,0 +1,190 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Front\Exception\WrongTokenException;
10use Shaarli\Security\SessionManager;
11use Slim\Http\Request;
12use Slim\Http\Response;
13
14class PluginsControllerTest extends TestCase
15{
16 use FrontAdminControllerMockHelper;
17
18 /** @var PluginsController */
19 protected $controller;
20
21 public function setUp(): void
22 {
23 $this->createContainer();
24
25 $this->controller = new PluginsController($this->container);
26 }
27
28 /**
29 * Test displaying plugins admin page
30 */
31 public function testIndex(): void
32 {
33 $assignedVariables = [];
34 $this->assignTemplateVars($assignedVariables);
35
36 $request = $this->createMock(Request::class);
37 $response = new Response();
38
39 $data = [
40 'plugin1' => ['order' => 2, 'other' => 'field'],
41 'plugin2' => ['order' => 1],
42 'plugin3' => ['order' => false, 'abc' => 'def'],
43 'plugin4' => [],
44 ];
45
46 $this->container->pluginManager
47 ->expects(static::once())
48 ->method('getPluginsMeta')
49 ->willReturn($data);
50
51 $result = $this->controller->index($request, $response);
52
53 static::assertSame(200, $result->getStatusCode());
54 static::assertSame('pluginsadmin', (string) $result->getBody());
55
56 static::assertSame('Plugin Administration - Shaarli', $assignedVariables['pagetitle']);
57 static::assertSame(
58 ['plugin2' => $data['plugin2'], 'plugin1' => $data['plugin1']],
59 $assignedVariables['enabledPlugins']
60 );
61 static::assertSame(
62 ['plugin3' => $data['plugin3'], 'plugin4' => $data['plugin4']],
63 $assignedVariables['disabledPlugins']
64 );
65 }
66
67 /**
68 * Test save plugins admin page
69 */
70 public function testSaveEnabledPlugins(): void
71 {
72 $parameters = [
73 'plugin1' => 'on',
74 'order_plugin1' => '2',
75 'plugin2' => 'on',
76 ];
77
78 $request = $this->createMock(Request::class);
79 $request
80 ->expects(static::atLeastOnce())
81 ->method('getParams')
82 ->willReturnCallback(function () use ($parameters): array {
83 return $parameters;
84 })
85 ;
86 $response = new Response();
87
88 $this->container->pluginManager
89 ->expects(static::once())
90 ->method('executeHooks')
91 ->with('save_plugin_parameters', $parameters)
92 ;
93 $this->container->conf
94 ->expects(static::atLeastOnce())
95 ->method('set')
96 ->with('general.enabled_plugins', ['plugin1', 'plugin2'])
97 ;
98
99 $result = $this->controller->save($request, $response);
100
101 static::assertSame(302, $result->getStatusCode());
102 static::assertSame(['/subfolder/admin/plugins'], $result->getHeader('location'));
103 }
104
105 /**
106 * Test save plugin parameters
107 */
108 public function testSavePluginParameters(): void
109 {
110 $parameters = [
111 'parameters_form' => true,
112 'parameter1' => 'blip',
113 'parameter2' => 'blop',
114 ];
115
116 $request = $this->createMock(Request::class);
117 $request
118 ->expects(static::atLeastOnce())
119 ->method('getParams')
120 ->willReturnCallback(function () use ($parameters): array {
121 return $parameters;
122 })
123 ;
124 $response = new Response();
125
126 $this->container->pluginManager
127 ->expects(static::once())
128 ->method('executeHooks')
129 ->with('save_plugin_parameters', $parameters)
130 ;
131 $this->container->conf
132 ->expects(static::atLeastOnce())
133 ->method('set')
134 ->withConsecutive(['plugins.parameter1', 'blip'], ['plugins.parameter2', 'blop'])
135 ;
136
137 $result = $this->controller->save($request, $response);
138
139 static::assertSame(302, $result->getStatusCode());
140 static::assertSame(['/subfolder/admin/plugins'], $result->getHeader('location'));
141 }
142
143 /**
144 * Test save plugin parameters - error encountered
145 */
146 public function testSaveWithError(): void
147 {
148 $request = $this->createMock(Request::class);
149 $response = new Response();
150
151 $this->container->conf = $this->createMock(ConfigManager::class);
152 $this->container->conf
153 ->expects(static::atLeastOnce())
154 ->method('write')
155 ->willThrowException(new \Exception($message = 'error message'))
156 ;
157
158 $this->container->sessionManager = $this->createMock(SessionManager::class);
159 $this->container->sessionManager->method('checkToken')->willReturn(true);
160 $this->container->sessionManager
161 ->expects(static::once())
162 ->method('setSessionParameter')
163 ->with(
164 SessionManager::KEY_ERROR_MESSAGES,
165 ['ERROR while saving plugin configuration: ' . PHP_EOL . $message]
166 )
167 ;
168
169 $result = $this->controller->save($request, $response);
170
171 static::assertSame(302, $result->getStatusCode());
172 static::assertSame(['/subfolder/admin/plugins'], $result->getHeader('location'));
173 }
174
175 /**
176 * Test save plugin parameters - wrong token
177 */
178 public function testSaveWrongToken(): void
179 {
180 $this->container->sessionManager = $this->createMock(SessionManager::class);
181 $this->container->sessionManager->method('checkToken')->willReturn(false);
182
183 $request = $this->createMock(Request::class);
184 $response = new Response();
185
186 $this->expectException(WrongTokenException::class);
187
188 $this->controller->save($request, $response);
189 }
190}
diff --git a/tpl/default/includes.html b/tpl/default/includes.html
index 102314d5..227f9b52 100644
--- a/tpl/default/includes.html
+++ b/tpl/default/includes.html
@@ -12,10 +12,10 @@
12 <link type="text/css" rel="stylesheet" href="{$asset_path}/css/markdown.min.css?v={$version_hash}#" /> 12 <link type="text/css" rel="stylesheet" href="{$asset_path}/css/markdown.min.css?v={$version_hash}#" />
13{/if} 13{/if}
14{loop="$plugins_includes.css_files"} 14{loop="$plugins_includes.css_files"}
15 <link type="text/css" rel="stylesheet" href="{$value}?v={$version_hash}#"/> 15 <link type="text/css" rel="stylesheet" href="{$base_path}/{$value}?v={$version_hash}#"/>
16{/loop} 16{/loop}
17{if="is_file('data/user.css')"} 17{if="is_file('data/user.css')"}
18 <link type="text/css" rel="stylesheet" href="data/user.css#" /> 18 <link type="text/css" rel="stylesheet" href="{$base_path}/data/user.css#" />
19{/if} 19{/if}
20<link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#" 20<link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#"
21 title="Shaarli search - {$shaarlititle}" /> 21 title="Shaarli search - {$shaarlititle}" />
diff --git a/tpl/default/page.footer.html b/tpl/default/page.footer.html
index d72917de..51bdb2f0 100644
--- a/tpl/default/page.footer.html
+++ b/tpl/default/page.footer.html
@@ -25,7 +25,7 @@
25{/loop} 25{/loop}
26 26
27{loop="$plugins_footer.js_files"} 27{loop="$plugins_footer.js_files"}
28 <script src="{$value}#"></script> 28 <script src="{$base_path}/{$value}#"></script>
29{/loop} 29{/loop}
30 30
31<div id="js-translations" class="hidden"> 31<div id="js-translations" class="hidden">
diff --git a/tpl/default/pluginsadmin.html b/tpl/default/pluginsadmin.html
index 1536c311..05d13556 100644
--- a/tpl/default/pluginsadmin.html
+++ b/tpl/default/pluginsadmin.html
@@ -16,7 +16,7 @@
16 <div class="clear"></div> 16 <div class="clear"></div>
17</noscript> 17</noscript>
18 18
19<form method="POST" action="{$base_path}/?do=save_pluginadmin" name="pluginform" id="pluginform" class="pluginform-container"> 19<form method="POST" action="{$base_path}/admin/plugins" name="pluginform" id="pluginform" class="pluginform-container">
20 <div class="pure-g"> 20 <div class="pure-g">
21 <div class="pure-u-lg-1-8 pure-u-1-24"></div> 21 <div class="pure-u-lg-1-8 pure-u-1-24"></div>
22 <div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-complete"> 22 <div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-complete">
@@ -127,7 +127,7 @@
127 <input type="hidden" name="token" value="{$token}"> 127 <input type="hidden" name="token" value="{$token}">
128</form> 128</form>
129 129
130<form action="{$base_path}/?do=save_pluginadmin" method="POST"> 130<form action="{$base_path}/admin/plugins" method="POST">
131 <div class="pure-g"> 131 <div class="pure-g">
132 <div class="pure-u-lg-1-8 pure-u-1-24"></div> 132 <div class="pure-u-lg-1-8 pure-u-1-24"></div>
133 <div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-light"> 133 <div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-light">
@@ -173,6 +173,7 @@
173 </section> 173 </section>
174 </div> 174 </div>
175 </div> 175 </div>
176 <input type="hidden" name="token" value="{$token}">
176</form> 177</form>
177 178
178{include="page.footer"} 179{include="page.footer"}
diff --git a/tpl/default/tools.html b/tpl/default/tools.html
index 045defc9..31f33a09 100644
--- a/tpl/default/tools.html
+++ b/tpl/default/tools.html
@@ -16,7 +16,7 @@
16 </a> 16 </a>
17 </div> 17 </div>
18 <div class="tools-item"> 18 <div class="tools-item">
19 <a href="{$base_path}/?do=pluginadmin" title="{'Enable, disable and configure plugins'|t}"> 19 <a href="{$base_path}/admin/plugins" title="{'Enable, disable and configure plugins'|t}">
20 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Plugin administration'|t}</span> 20 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Plugin administration'|t}</span>
21 </a> 21 </a>
22 </div> 22 </div>
diff --git a/tpl/vintage/pluginsadmin.html b/tpl/vintage/pluginsadmin.html
index c94dc211..a04c77c2 100644
--- a/tpl/vintage/pluginsadmin.html
+++ b/tpl/vintage/pluginsadmin.html
@@ -16,7 +16,7 @@
16</noscript> 16</noscript>
17 17
18<div id="pluginsadmin"> 18<div id="pluginsadmin">
19 <form action="{$base_path}/?do=save_pluginadmin" method="POST"> 19 <form action="{$base_path}/admin/plugins" method="POST">
20 <section id="enabled_plugins"> 20 <section id="enabled_plugins">
21 <h1>Enabled Plugins</h1> 21 <h1>Enabled Plugins</h1>
22 22
@@ -88,7 +88,7 @@
88 </section> 88 </section>
89 </form> 89 </form>
90 90
91 <form action="{$base_path}/?do=save_pluginadmin" method="POST"> 91 <form action="{$base_path}/admin/plugins" method="POST">
92 <section id="plugin_parameters"> 92 <section id="plugin_parameters">
93 <h1>Enabled Plugin Parameters</h1> 93 <h1>Enabled Plugin Parameters</h1>
94 94
diff --git a/tpl/vintage/tools.html b/tpl/vintage/tools.html
index 95f89d8c..1125bba9 100644
--- a/tpl/vintage/tools.html
+++ b/tpl/vintage/tools.html
@@ -7,7 +7,7 @@
7 <div id="toolsdiv"> 7 <div id="toolsdiv">
8 <a href="{$base_path}/admin/configure"><b>Configure your Shaarli</b><span>: Change Title, timezone...</span></a> 8 <a href="{$base_path}/admin/configure"><b>Configure your Shaarli</b><span>: Change Title, timezone...</span></a>
9 <br><br> 9 <br><br>
10 <a href="{$base_path}/?do=pluginadmin"><b>Plugin administration</b><span>: Enable, disable and configure plugins.</span></a> 10 <a href="{$base_path}/admin/plugins"><b>Plugin administration</b><span>: Enable, disable and configure plugins.</span></a>
11 <br><br> 11 <br><br>
12 {if="!$openshaarli"}<a href="{$base_path}/admin/password"><b>Change password</b><span>: Change your password.</span></a> 12 {if="!$openshaarli"}<a href="{$base_path}/admin/password"><b>Change password</b><span>: Change your password.</span></a>
13 <br><br>{/if} 13 <br><br>{/if}