aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/Config.php114
-rw-r--r--application/PluginManager.php51
-rw-r--r--application/Router.php12
-rw-r--r--inc/plugin_admin.js67
-rw-r--r--inc/shaarli.css60
-rw-r--r--index.php48
-rw-r--r--plugins/addlink_toolbar/addlink_toolbar.meta1
-rw-r--r--plugins/archiveorg/archiveorg.meta1
-rw-r--r--plugins/demo_plugin/demo_plugin.meta1
-rw-r--r--plugins/playvideos/playvideos.meta1
-rw-r--r--plugins/qrcode/qrcode.meta1
-rw-r--r--plugins/readityourself/readityourself.meta2
-rw-r--r--plugins/readityourself/readityourself.php2
-rw-r--r--plugins/wallabag/wallabag.meta2
-rw-r--r--plugins/wallabag/wallabag.php2
-rw-r--r--tests/ConfigTest.php109
-rw-r--r--tests/PluginManagerTest.php19
-rw-r--r--tests/plugins/test/test.meta2
-rw-r--r--tpl/pluginsadmin.html131
-rw-r--r--tpl/tools.html17
20 files changed, 636 insertions, 7 deletions
diff --git a/application/Config.php b/application/Config.php
index c71ef68c..9af5a535 100644
--- a/application/Config.php
+++ b/application/Config.php
@@ -74,6 +74,106 @@ function writeConfig($config, $isLoggedIn)
74} 74}
75 75
76/** 76/**
77 * Process plugin administration form data and save it in an array.
78 *
79 * @param array $formData Data sent by the plugin admin form.
80 *
81 * @return array New list of enabled plugin, ordered.
82 *
83 * @throws PluginConfigOrderException Plugins can't be sorted because their order is invalid.
84 */
85function save_plugin_config($formData)
86{
87 // Make sure there are no duplicates in orders.
88 if (!validate_plugin_order($formData)) {
89 throw new PluginConfigOrderException();
90 }
91
92 $plugins = array();
93 $newEnabledPlugins = array();
94 foreach ($formData as $key => $data) {
95 if (startsWith($key, 'order')) {
96 continue;
97 }
98
99 // If there is no order, it means a disabled plugin has been enabled.
100 if (isset($formData['order_' . $key])) {
101 $plugins[(int) $formData['order_' . $key]] = $key;
102 }
103 else {
104 $newEnabledPlugins[] = $key;
105 }
106 }
107
108 // New enabled plugins will be added at the end of order.
109 $plugins = array_merge($plugins, $newEnabledPlugins);
110
111 // Sort plugins by order.
112 if (!ksort($plugins)) {
113 throw new PluginConfigOrderException();
114 }
115
116 $finalPlugins = array();
117 // Make plugins order continuous.
118 foreach ($plugins as $plugin) {
119 $finalPlugins[] = $plugin;
120 }
121
122 return $finalPlugins;
123}
124
125/**
126 * Validate plugin array submitted.
127 * Will fail if there is duplicate orders value.
128 *
129 * @param array $formData Data from submitted form.
130 *
131 * @return bool true if ok, false otherwise.
132 */
133function validate_plugin_order($formData)
134{
135 $orders = array();
136 foreach ($formData as $key => $value) {
137 // No duplicate order allowed.
138 if (in_array($value, $orders)) {
139 return false;
140 }
141
142 if (startsWith($key, 'order')) {
143 $orders[] = $value;
144 }
145 }
146
147 return true;
148}
149
150/**
151 * Affect plugin parameters values into plugins array.
152 *
153 * @param mixed $plugins Plugins array ($plugins[<plugin_name>]['parameters']['param_name'] = <value>.
154 * @param mixed $config Plugins configuration.
155 *
156 * @return mixed Updated $plugins array.
157 */
158function load_plugin_parameter_values($plugins, $config)
159{
160 $out = $plugins;
161 foreach ($plugins as $name => $plugin) {
162 if (empty($plugin['parameters'])) {
163 continue;
164 }
165
166 foreach ($plugin['parameters'] as $key => $param) {
167 if (!empty($config[$key])) {
168 $out[$name]['parameters'][$key] = $config[$key];
169 }
170 }
171 }
172
173 return $out;
174}
175
176/**
77 * Milestone 0.9 - shaarli/Shaarli#41: options.php is not supported anymore. 177 * Milestone 0.9 - shaarli/Shaarli#41: options.php is not supported anymore.
78 * ==> if user is loggedIn, merge its content with config.php, then delete options.php. 178 * ==> if user is loggedIn, merge its content with config.php, then delete options.php.
79 * 179 *
@@ -132,3 +232,17 @@ class UnauthorizedConfigException extends Exception
132 $this->message = 'You are not authorized to alter config.'; 232 $this->message = 'You are not authorized to alter config.';
133 } 233 }
134} 234}
235
236/**
237 * Exception used if an error occur while saving plugin configuration.
238 */
239class PluginConfigOrderException extends Exception
240{
241 /**
242 * Construct exception.
243 */
244 public function __construct()
245 {
246 $this->message = 'An error occurred while trying to save plugins loading order.';
247 }
248}
diff --git a/application/PluginManager.php b/application/PluginManager.php
index 803f11b4..787ac6a9 100644
--- a/application/PluginManager.php
+++ b/application/PluginManager.php
@@ -34,6 +34,12 @@ class PluginManager
34 public static $PLUGINS_PATH = 'plugins'; 34 public static $PLUGINS_PATH = 'plugins';
35 35
36 /** 36 /**
37 * Plugins meta files extension.
38 * @var string $META_EXT
39 */
40 public static $META_EXT = 'meta';
41
42 /**
37 * Private constructor: new instances not allowed. 43 * Private constructor: new instances not allowed.
38 */ 44 */
39 private function __construct() 45 private function __construct()
@@ -162,6 +168,51 @@ class PluginManager
162 { 168 {
163 return 'hook_' . $pluginName . '_' . $hook; 169 return 'hook_' . $pluginName . '_' . $hook;
164 } 170 }
171
172 /**
173 * Retrieve plugins metadata from *.meta (INI) files into an array.
174 * Metadata contains:
175 * - plugin description [description]
176 * - parameters split with ';' [parameters]
177 *
178 * Respects plugins order from settings.
179 *
180 * @return array plugins metadata.
181 */
182 public function getPluginsMeta()
183 {
184 $metaData = array();
185 $dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR | GLOB_MARK);
186
187 // Browse all plugin directories.
188 foreach ($dirs as $pluginDir) {
189 $plugin = basename($pluginDir);
190 $metaFile = $pluginDir . $plugin . '.' . self::$META_EXT;
191 if (!is_file($metaFile) || !is_readable($metaFile)) {
192 continue;
193 }
194
195 $metaData[$plugin] = parse_ini_file($metaFile);
196 $metaData[$plugin]['order'] = array_search($plugin, $this->authorizedPlugins);
197
198 // Read parameters and format them into an array.
199 if (isset($metaData[$plugin]['parameters'])) {
200 $params = explode(';', $metaData[$plugin]['parameters']);
201 } else {
202 $params = array();
203 }
204 $metaData[$plugin]['parameters'] = array();
205 foreach ($params as $param) {
206 if (empty($param)) {
207 continue;
208 }
209
210 $metaData[$plugin]['parameters'][$param] = '';
211 }
212 }
213
214 return $metaData;
215 }
165} 216}
166 217
167/** 218/**
diff --git a/application/Router.php b/application/Router.php
index 0c813847..6185f08e 100644
--- a/application/Router.php
+++ b/application/Router.php
@@ -35,6 +35,10 @@ class Router
35 35
36 public static $PAGE_LINKLIST = 'linklist'; 36 public static $PAGE_LINKLIST = 'linklist';
37 37
38 public static $PAGE_PLUGINSADMIN = 'pluginadmin';
39
40 public static $PAGE_SAVE_PLUGINSADMIN = 'save_pluginadmin';
41
38 /** 42 /**
39 * Reproducing renderPage() if hell, to avoid regression. 43 * Reproducing renderPage() if hell, to avoid regression.
40 * 44 *
@@ -112,6 +116,14 @@ class Router
112 return self::$PAGE_IMPORT; 116 return self::$PAGE_IMPORT;
113 } 117 }
114 118
119 if (startswith($query, 'do='. self::$PAGE_PLUGINSADMIN)) {
120 return self::$PAGE_PLUGINSADMIN;
121 }
122
123 if (startswith($query, 'do='. self::$PAGE_SAVE_PLUGINSADMIN)) {
124 return self::$PAGE_SAVE_PLUGINSADMIN;
125 }
126
115 return self::$PAGE_LINKLIST; 127 return self::$PAGE_LINKLIST;
116 } 128 }
117} \ No newline at end of file 129} \ No newline at end of file
diff --git a/inc/plugin_admin.js b/inc/plugin_admin.js
new file mode 100644
index 00000000..134ffb33
--- /dev/null
+++ b/inc/plugin_admin.js
@@ -0,0 +1,67 @@
1/**
2 * Change the position counter of a row.
3 *
4 * @param elem Element Node to change.
5 * @param toPos int New position.
6 */
7function changePos(elem, toPos)
8{
9 var elemName = elem.getAttribute('data-line')
10
11 elem.setAttribute('data-order', toPos);
12 var hiddenInput = document.querySelector('[name="order_'+ elemName +'"]');
13 hiddenInput.setAttribute('value', toPos);
14}
15
16/**
17 * Move a row up or down.
18 *
19 * @param pos Element Node to move.
20 * @param move int Move: +1 (down) or -1 (up)
21 */
22function changeOrder(pos, move)
23{
24 var newpos = parseInt(pos) + move;
25 var line = document.querySelector('[data-order="'+ pos +'"]');
26 var changeline = document.querySelector('[data-order="'+ newpos +'"]');
27 var parent = changeline.parentNode;
28
29 changePos(line, newpos);
30 changePos(changeline, parseInt(pos));
31 var changeItem = move < 0 ? changeline : changeline.nextSibling;
32 parent.insertBefore(line, changeItem);
33}
34
35/**
36 * Move a row up in the table.
37 *
38 * @param pos int row counter.
39 *
40 * @returns false
41 */
42function orderUp(pos)
43{
44 if (pos == 0) {
45 return false;
46 }
47 changeOrder(pos, -1);
48 return false;
49}
50
51/**
52 * Move a row down in the table.
53 *
54 * @param pos int row counter.
55 *
56 * @returns false
57 */
58function orderDown(pos)
59{
60 var lastpos = document.querySelector('[data-order]:last-child').getAttribute('data-order');
61 if (pos == lastpos) {
62 return false;
63 }
64
65 changeOrder(pos, +1);
66 return false;
67}
diff --git a/inc/shaarli.css b/inc/shaarli.css
index 7a69d575..8a7409b2 100644
--- a/inc/shaarli.css
+++ b/inc/shaarli.css
@@ -1102,6 +1102,66 @@ ul.errors {
1102 float: left; 1102 float: left;
1103} 1103}
1104 1104
1105#pluginsadmin {
1106 width: 80%;
1107 padding: 20px 0 0 20px;
1108}
1109
1110#pluginsadmin section {
1111 padding: 20px 0;
1112}
1113
1114#pluginsadmin .plugin_parameters {
1115 margin: 10px 0;
1116}
1117
1118#pluginsadmin h1 {
1119 font-style: normal;
1120}
1121
1122#pluginsadmin h2 {
1123 font-size: 1.4em;
1124 font-weight: bold;
1125}
1126
1127#pluginsadmin table {
1128 width: 100%;
1129}
1130
1131#pluginsadmin table, #pluginsadmin th, #pluginsadmin td {
1132 border-width: 1px 0;
1133 border-style: solid;
1134 border-color: #c0c0c0;
1135}
1136
1137#pluginsadmin table th {
1138 font-weight: bold;
1139 padding: 10px 0;
1140}
1141
1142#pluginsadmin table td {
1143 padding: 5px 0;
1144}
1145
1146#pluginsadmin input[type=submit] {
1147 margin: 10px 0;
1148}
1149
1150#pluginsadmin .plugin_parameter {
1151 padding: 5px 0;
1152 border-width: 1px 0;
1153 border-style: solid;
1154 border-color: #c0c0c0;
1155}
1156
1157#pluginsadmin .float_label {
1158 float: left;
1159 width: 20%;
1160}
1161
1162#pluginsadmin a {
1163 color: black;
1164}
1105/* 404 page */ 1165/* 404 page */
1106.error-container { 1166.error-container {
1107 1167
diff --git a/index.php b/index.php
index beba9c32..51b7b391 100644
--- a/index.php
+++ b/index.php
@@ -1770,6 +1770,54 @@ HTML;
1770 exit; 1770 exit;
1771 } 1771 }
1772 1772
1773 // Plugin administration page
1774 if ($targetPage == Router::$PAGE_PLUGINSADMIN) {
1775 $pluginMeta = $pluginManager->getPluginsMeta();
1776
1777 // Split plugins into 2 arrays: ordered enabled plugins and disabled.
1778 $enabledPlugins = array_filter($pluginMeta, function($v) { return $v['order'] !== false; });
1779 // Load parameters.
1780 $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $GLOBALS['plugins']);
1781 uasort(
1782 $enabledPlugins,
1783 function($a, $b) { return $a['order'] - $b['order']; }
1784 );
1785 $disabledPlugins = array_filter($pluginMeta, function($v) { return $v['order'] === false; });
1786
1787 $PAGE->assign('enabledPlugins', $enabledPlugins);
1788 $PAGE->assign('disabledPlugins', $disabledPlugins);
1789 $PAGE->renderPage('pluginsadmin');
1790 exit;
1791 }
1792
1793 // Plugin administration form action
1794 if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) {
1795 try {
1796 if (isset($_POST['parameters_form'])) {
1797 unset($_POST['parameters_form']);
1798 foreach ($_POST as $param => $value) {
1799 $GLOBALS['plugins'][$param] = escape($value);
1800 }
1801 }
1802 else {
1803 $GLOBALS['config']['ENABLED_PLUGINS'] = save_plugin_config($_POST);
1804 }
1805 writeConfig($GLOBALS, isLoggedIn());
1806 }
1807 catch (Exception $e) {
1808 error_log(
1809 'ERROR while saving plugin configuration:.' . PHP_EOL .
1810 $e->getMessage()
1811 );
1812
1813 // TODO: do not handle exceptions/errors in JS.
1814 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=pluginsadmin\';</script>';
1815 exit;
1816 }
1817 header('Location: ?do='. Router::$PAGE_PLUGINSADMIN);
1818 exit;
1819 }
1820
1773 // -------- Otherwise, simply display search form and links: 1821 // -------- Otherwise, simply display search form and links:
1774 showLinkList($PAGE, $LINKSDB); 1822 showLinkList($PAGE, $LINKSDB);
1775 exit; 1823 exit;
diff --git a/plugins/addlink_toolbar/addlink_toolbar.meta b/plugins/addlink_toolbar/addlink_toolbar.meta
new file mode 100644
index 00000000..2f0b5866
--- /dev/null
+++ b/plugins/addlink_toolbar/addlink_toolbar.meta
@@ -0,0 +1 @@
description="Adds the addlink input on the linklist page."
diff --git a/plugins/archiveorg/archiveorg.meta b/plugins/archiveorg/archiveorg.meta
new file mode 100644
index 00000000..8b5703e1
--- /dev/null
+++ b/plugins/archiveorg/archiveorg.meta
@@ -0,0 +1 @@
description="For each link, add an Archive.org icon."
diff --git a/plugins/demo_plugin/demo_plugin.meta b/plugins/demo_plugin/demo_plugin.meta
new file mode 100644
index 00000000..b063ecb7
--- /dev/null
+++ b/plugins/demo_plugin/demo_plugin.meta
@@ -0,0 +1 @@
description="A demo plugin covering all use cases for template designers and plugin developers."
diff --git a/plugins/playvideos/playvideos.meta b/plugins/playvideos/playvideos.meta
new file mode 100644
index 00000000..c2b0908e
--- /dev/null
+++ b/plugins/playvideos/playvideos.meta
@@ -0,0 +1 @@
description="Add a button in the toolbar allowing to watch all videos."
diff --git a/plugins/qrcode/qrcode.meta b/plugins/qrcode/qrcode.meta
new file mode 100644
index 00000000..cbf371ea
--- /dev/null
+++ b/plugins/qrcode/qrcode.meta
@@ -0,0 +1 @@
description="For each link, add a QRCode icon ."
diff --git a/plugins/readityourself/readityourself.meta b/plugins/readityourself/readityourself.meta
new file mode 100644
index 00000000..bd611dd0
--- /dev/null
+++ b/plugins/readityourself/readityourself.meta
@@ -0,0 +1,2 @@
1description="For each link, add a ReadItYourself icon to save the shaared URL."
2parameters=READITYOUSELF_URL; \ No newline at end of file
diff --git a/plugins/readityourself/readityourself.php b/plugins/readityourself/readityourself.php
index 1b030bc8..c8df4c4f 100644
--- a/plugins/readityourself/readityourself.php
+++ b/plugins/readityourself/readityourself.php
@@ -13,7 +13,7 @@ if (is_file(PluginManager::$PLUGINS_PATH . '/readityourself/config.php')) {
13 include PluginManager::$PLUGINS_PATH . '/readityourself/config.php'; 13 include PluginManager::$PLUGINS_PATH . '/readityourself/config.php';
14} 14}
15 15
16if (!isset($GLOBALS['plugins']['READITYOUSELF_URL'])) { 16if (empty($GLOBALS['plugins']['READITYOUSELF_URL'])) {
17 $GLOBALS['plugin_errors'][] = 'Readityourself plugin error: '. 17 $GLOBALS['plugin_errors'][] = 'Readityourself plugin error: '.
18 'Please define "$GLOBALS[\'plugins\'][\'READITYOUSELF_URL\']" '. 18 'Please define "$GLOBALS[\'plugins\'][\'READITYOUSELF_URL\']" '.
19 'in "plugins/readityourself/config.php" or in your Shaarli config.php file.'; 19 'in "plugins/readityourself/config.php" or in your Shaarli config.php file.';
diff --git a/plugins/wallabag/wallabag.meta b/plugins/wallabag/wallabag.meta
new file mode 100644
index 00000000..8763c4a2
--- /dev/null
+++ b/plugins/wallabag/wallabag.meta
@@ -0,0 +1,2 @@
1description="For each link, add a Wallabag icon to save it in your instance."
2parameters="WALLABAG_URL" \ No newline at end of file
diff --git a/plugins/wallabag/wallabag.php b/plugins/wallabag/wallabag.php
index e3c399a9..0d6fc66d 100644
--- a/plugins/wallabag/wallabag.php
+++ b/plugins/wallabag/wallabag.php
@@ -11,7 +11,7 @@ if (is_file(PluginManager::$PLUGINS_PATH . '/wallabag/config.php')) {
11 include PluginManager::$PLUGINS_PATH . '/wallabag/config.php'; 11 include PluginManager::$PLUGINS_PATH . '/wallabag/config.php';
12} 12}
13 13
14if (!isset($GLOBALS['plugins']['WALLABAG_URL'])) { 14if (empty($GLOBALS['plugins']['WALLABAG_URL'])) {
15 $GLOBALS['plugin_errors'][] = 'Wallabag plugin error: '. 15 $GLOBALS['plugin_errors'][] = 'Wallabag plugin error: '.
16 'Please define "$GLOBALS[\'plugins\'][\'WALLABAG_URL\']" '. 16 'Please define "$GLOBALS[\'plugins\'][\'WALLABAG_URL\']" '.
17 'in "plugins/wallabag/config.php" or in your Shaarli config.php file.'; 17 'in "plugins/wallabag/config.php" or in your Shaarli config.php file.';
diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php
index adebfcc3..492ddd3b 100644
--- a/tests/ConfigTest.php
+++ b/tests/ConfigTest.php
@@ -174,4 +174,113 @@ class ConfigTest extends PHPUnit_Framework_TestCase
174 include self::$configFields['config']['CONFIG_FILE']; 174 include self::$configFields['config']['CONFIG_FILE'];
175 $this->assertEquals(self::$configFields['login'], $GLOBALS['login']); 175 $this->assertEquals(self::$configFields['login'], $GLOBALS['login']);
176 } 176 }
177
178 /**
179 * Test save_plugin_config with valid data.
180 *
181 * @throws PluginConfigOrderException
182 */
183 public function testSavePluginConfigValid()
184 {
185 $data = array(
186 'order_plugin1' => 2, // no plugin related
187 'plugin2' => 0, // new - at the end
188 'plugin3' => 0, // 2nd
189 'order_plugin3' => 8,
190 'plugin4' => 0, // 1st
191 'order_plugin4' => 5,
192 );
193
194 $expected = array(
195 'plugin3',
196 'plugin4',
197 'plugin2',
198 );
199
200 $out = save_plugin_config($data);
201 $this->assertEquals($expected, $out);
202 }
203
204 /**
205 * Test save_plugin_config with invalid data.
206 *
207 * @expectedException PluginConfigOrderException
208 */
209 public function testSavePluginConfigInvalid()
210 {
211 $data = array(
212 'plugin2' => 0,
213 'plugin3' => 0,
214 'order_plugin3' => 0,
215 'plugin4' => 0,
216 'order_plugin4' => 0,
217 );
218
219 save_plugin_config($data);
220 }
221
222 /**
223 * Test save_plugin_config without data.
224 */
225 public function testSavePluginConfigEmpty()
226 {
227 $this->assertEquals(array(), save_plugin_config(array()));
228 }
229
230 /**
231 * Test validate_plugin_order with valid data.
232 */
233 public function testValidatePluginOrderValid()
234 {
235 $data = array(
236 'order_plugin1' => 2,
237 'plugin2' => 0,
238 'plugin3' => 0,
239 'order_plugin3' => 1,
240 'plugin4' => 0,
241 'order_plugin4' => 5,
242 );
243
244 $this->assertTrue(validate_plugin_order($data));
245 }
246
247 /**
248 * Test validate_plugin_order with invalid data.
249 */
250 public function testValidatePluginOrderInvalid()
251 {
252 $data = array(
253 'order_plugin1' => 2,
254 'order_plugin3' => 1,
255 'order_plugin4' => 1,
256 );
257
258 $this->assertFalse(validate_plugin_order($data));
259 }
260
261 /**
262 * Test load_plugin_parameter_values.
263 */
264 public function testLoadPluginParameterValues()
265 {
266 $plugins = array(
267 'plugin_name' => array(
268 'parameters' => array(
269 'param1' => true,
270 'param2' => false,
271 'param3' => '',
272 )
273 )
274 );
275
276 $parameters = array(
277 'param1' => 'value1',
278 'param2' => 'value2',
279 );
280
281 $result = load_plugin_parameter_values($plugins, $parameters);
282 $this->assertEquals('value1', $result['plugin_name']['parameters']['param1']);
283 $this->assertEquals('value2', $result['plugin_name']['parameters']['param2']);
284 $this->assertEquals('', $result['plugin_name']['parameters']['param3']);
285 }
177} 286}
diff --git a/tests/PluginManagerTest.php b/tests/PluginManagerTest.php
index df2614b5..348082c7 100644
--- a/tests/PluginManagerTest.php
+++ b/tests/PluginManagerTest.php
@@ -63,4 +63,23 @@ class PluginManagerTest extends PHPUnit_Framework_TestCase
63 63
64 $pluginManager->load(array('nope', 'renope')); 64 $pluginManager->load(array('nope', 'renope'));
65 } 65 }
66
67 /**
68 * Test plugin metadata loading.
69 */
70 public function testGetPluginsMeta()
71 {
72 $pluginManager = PluginManager::getInstance();
73
74 PluginManager::$PLUGINS_PATH = self::$pluginPath;
75 $pluginManager->load(array(self::$pluginName));
76
77 $expectedParameters = array(
78 'pop' => '',
79 'hip' => '',
80 );
81 $meta = $pluginManager->getPluginsMeta();
82 $this->assertEquals('test plugin', $meta[self::$pluginName]['description']);
83 $this->assertEquals($expectedParameters, $meta[self::$pluginName]['parameters']);
84 }
66} \ No newline at end of file 85} \ No newline at end of file
diff --git a/tests/plugins/test/test.meta b/tests/plugins/test/test.meta
new file mode 100644
index 00000000..ab999ed4
--- /dev/null
+++ b/tests/plugins/test/test.meta
@@ -0,0 +1,2 @@
1description="test plugin"
2parameters="pop;hip" \ No newline at end of file
diff --git a/tpl/pluginsadmin.html b/tpl/pluginsadmin.html
new file mode 100644
index 00000000..4f7d091e
--- /dev/null
+++ b/tpl/pluginsadmin.html
@@ -0,0 +1,131 @@
1<!DOCTYPE html>
2<html>
3<head>{include="includes"}</head>
4<body>
5<div id="pageheader">
6 {include="page.header"}
7</div>
8
9<noscript>
10 <div>
11 <ul class="errors">
12 <li>You need to enable Javascript to change plugin loading order.</li>
13 </ul>
14 </div>
15 <div class="clear"></div>
16</noscript>
17
18<div id="pluginsadmin">
19 <form action="?do=save_pluginadmin" method="POST">
20 <section id="enabled_plugins">
21 <h1>Enabled Plugins</h1>
22
23 <div>
24 {if="count($enabledPlugins)==0"}
25 <p>No plugin enabled.</p>
26 {else}
27 <table id="plugin_table">
28 <thead>
29 <tr>
30 <th class="center">Disable</th>
31 <th class="center">Order</th>
32 <th>Name</th>
33 <th>Description</th>
34 </tr>
35 </thead>
36 <tbody>
37 {loop="$enabledPlugins"}
38 <tr data-line="{$key}" data-order="{$counter}">
39 <td class="center"><input type="checkbox" name="{$key}" checked="checked"></td>
40 <td class="center">
41 <a href="#"
42 onclick="return orderUp(this.parentNode.parentNode.getAttribute('data-order'));">
43
44 </a>
45 <a href="#"
46 onclick="return orderDown(this.parentNode.parentNode.getAttribute('data-order'));">
47
48 </a>
49 <input type="hidden" name="order_{$key}" value="{$counter}">
50 </td>
51 <td>{$key}</td>
52 <td>{$value.description}</td>
53 </tr>
54 {/loop}
55 </tbody>
56 </table>
57 {/if}
58 </div>
59 </section>
60
61 <section id="disabled_plugins">
62 <h1>Disabled Plugins</h1>
63
64 <div>
65 {if="count($disabledPlugins)==0"}
66 <p>No plugin disabled.</p>
67 {else}
68 <table>
69 <tr>
70 <th class="center">Enable</th>
71 <th>Name</th>
72 <th>Description</th>
73 </tr>
74 {loop="$disabledPlugins"}
75 <tr>
76 <td class="center"><input type="checkbox" name="{$key}"></td>
77 <td>{$key}</td>
78 <td>{$value.description}</td>
79 </tr>
80 {/loop}
81 </table>
82 {/if}
83 </div>
84
85 <div class="center">
86 <input type="submit" value="Save"/>
87 </div>
88 </section>
89 </form>
90
91 <form action="?do=save_pluginadmin" method="POST">
92 <section id="plugin_parameters">
93 <h1>Enabled Plugin Parameters</h1>
94
95 <div>
96 {if="count($enabledPlugins)==0"}
97 <p>No plugin enabled.</p>
98 {else}
99 {loop="$enabledPlugins"}
100 {if="count($value.parameters) > 0"}
101 <div class="plugin_parameters">
102 <h2>{$key}</h2>
103 {loop="$value.parameters"}
104 <div class="plugin_parameter">
105 <div class="float_label">
106 <label for="{$key}">
107 <code>{$key}</code>
108 </label>
109 </div>
110 <div class="float_input">
111 <input name="{$key}" value="{$value}" id="{$key}"/>
112 </div>
113 </div>
114 {/loop}
115 </div>
116 {/if}
117 {/loop}
118 {/if}
119 <div class="center">
120 <input type="submit" name="parameters_form" value="Save"/>
121 </div>
122 </div>
123 </section>
124 </form>
125
126</div>
127{include="page.footer"}
128
129<script src="inc/plugin_admin.js#"></script>
130</body>
131</html> \ No newline at end of file
diff --git a/tpl/tools.html b/tpl/tools.html
index c13f4f16..78b81663 100644
--- a/tpl/tools.html
+++ b/tpl/tools.html
@@ -5,11 +5,18 @@
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 {if="!$GLOBALS['config']['OPEN_SHAARLI']"}<a href="?do=changepasswd"><b>Change password</b> <span>: Change your password.</span></a><br><br>{/if} 8 <a href="?do=configure"><b>Configure your Shaarli</b><span>: Change Title, timezone...</span></a>
9 <a href="?do=configure"><b>Configure your Shaarli</b> <span>: Change Title, timezone...</span></a><br><br> 9 <br><br>
10 <a href="?do=changetag"><b>Rename/delete tags</b> <span>: Rename or delete a tag in all links</span></a><br><br> 10 <a href="?do=pluginadmin"><b>Plugin administration</b><span>: Enable, disable and configure plugins.</span></a>
11 <a href="?do=import"><b>Import</b> <span>: Import Netscape html bookmarks (as exported from Firefox, Chrome, Opera, delicious...)</span></a> <br><br> 11 <br><br>
12 <a href="?do=export"><b>Export</b> <span>: Export Netscape html bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)</span></a><br><br> 12 {if="!$GLOBALS['config']['OPEN_SHAARLI']"}<a href="?do=changepasswd"><b>Change password</b><span>: Change your password.</span></a>
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>
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>
17 <br><br>
18 <a href="?do=export"><b>Export</b><span>: Export Netscape html bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)</span></a>
19 <br><br>
13 <a class="smallbutton" 20 <a class="smallbutton"
14 onclick="return alertBookmarklet();" 21 onclick="return alertBookmarklet();"
15 href="javascript:( 22 href="javascript:(