}
}
+/**
+ * Process plugin administration form data and save it in an array.
+ *
+ * @param array $formData Data sent by the plugin admin form.
+ *
+ * @return array New list of enabled plugin, ordered.
+ *
+ * @throws PluginConfigOrderException Plugins can't be sorted because their order is invalid.
+ */
+function save_plugin_config($formData)
+{
+ // Make sure there are no duplicates in orders.
+ if (!validate_plugin_order($formData)) {
+ throw new PluginConfigOrderException();
+ }
+
+ $plugins = array();
+ $newEnabledPlugins = array();
+ foreach ($formData as $key => $data) {
+ if (startsWith($key, 'order')) {
+ continue;
+ }
+
+ // If there is no order, it means a disabled plugin has been enabled.
+ if (isset($formData['order_' . $key])) {
+ $plugins[(int) $formData['order_' . $key]] = $key;
+ }
+ else {
+ $newEnabledPlugins[] = $key;
+ }
+ }
+
+ // New enabled plugins will be added at the end of order.
+ $plugins = array_merge($plugins, $newEnabledPlugins);
+
+ // Sort plugins by order.
+ if (!ksort($plugins)) {
+ throw new PluginConfigOrderException();
+ }
+
+ $finalPlugins = array();
+ // Make plugins order continuous.
+ foreach ($plugins as $plugin) {
+ $finalPlugins[] = $plugin;
+ }
+
+ return $finalPlugins;
+}
+
+/**
+ * Validate plugin array submitted.
+ * Will fail if there is duplicate orders value.
+ *
+ * @param array $formData Data from submitted form.
+ *
+ * @return bool true if ok, false otherwise.
+ */
+function validate_plugin_order($formData)
+{
+ $orders = array();
+ foreach ($formData as $key => $value) {
+ // No duplicate order allowed.
+ if (in_array($value, $orders)) {
+ return false;
+ }
+
+ if (startsWith($key, 'order')) {
+ $orders[] = $value;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Affect plugin parameters values into plugins array.
+ *
+ * @param mixed $plugins Plugins array ($plugins[<plugin_name>]['parameters']['param_name'] = <value>.
+ * @param mixed $config Plugins configuration.
+ *
+ * @return mixed Updated $plugins array.
+ */
+function load_plugin_parameter_values($plugins, $config)
+{
+ $out = $plugins;
+ foreach ($plugins as $name => $plugin) {
+ if (empty($plugin['parameters'])) {
+ continue;
+ }
+
+ foreach ($plugin['parameters'] as $key => $param) {
+ if (!empty($config[$key])) {
+ $out[$name]['parameters'][$key] = $config[$key];
+ }
+ }
+ }
+
+ return $out;
+}
+
/**
* Milestone 0.9 - shaarli/Shaarli#41: options.php is not supported anymore.
* ==> if user is loggedIn, merge its content with config.php, then delete options.php.
$this->message = 'You are not authorized to alter config.';
}
}
+
+/**
+ * Exception used if an error occur while saving plugin configuration.
+ */
+class PluginConfigOrderException extends Exception
+{
+ /**
+ * Construct exception.
+ */
+ public function __construct()
+ {
+ $this->message = 'An error occurred while trying to save plugins loading order.';
+ }
+}
*/
public static $PLUGINS_PATH = 'plugins';
+ /**
+ * Plugins meta files extension.
+ * @var string $META_EXT
+ */
+ public static $META_EXT = 'meta';
+
/**
* Private constructor: new instances not allowed.
*/
{
return 'hook_' . $pluginName . '_' . $hook;
}
+
+ /**
+ * Retrieve plugins metadata from *.meta (INI) files into an array.
+ * Metadata contains:
+ * - plugin description [description]
+ * - parameters split with ';' [parameters]
+ *
+ * Respects plugins order from settings.
+ *
+ * @return array plugins metadata.
+ */
+ public function getPluginsMeta()
+ {
+ $metaData = array();
+ $dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR | GLOB_MARK);
+
+ // Browse all plugin directories.
+ foreach ($dirs as $pluginDir) {
+ $plugin = basename($pluginDir);
+ $metaFile = $pluginDir . $plugin . '.' . self::$META_EXT;
+ if (!is_file($metaFile) || !is_readable($metaFile)) {
+ continue;
+ }
+
+ $metaData[$plugin] = parse_ini_file($metaFile);
+ $metaData[$plugin]['order'] = array_search($plugin, $this->authorizedPlugins);
+
+ // Read parameters and format them into an array.
+ if (isset($metaData[$plugin]['parameters'])) {
+ $params = explode(';', $metaData[$plugin]['parameters']);
+ } else {
+ $params = array();
+ }
+ $metaData[$plugin]['parameters'] = array();
+ foreach ($params as $param) {
+ if (empty($param)) {
+ continue;
+ }
+
+ $metaData[$plugin]['parameters'][$param] = '';
+ }
+ }
+
+ return $metaData;
+ }
}
/**
public static $PAGE_LINKLIST = 'linklist';
+ public static $PAGE_PLUGINSADMIN = 'pluginadmin';
+
+ public static $PAGE_SAVE_PLUGINSADMIN = 'save_pluginadmin';
+
/**
* Reproducing renderPage() if hell, to avoid regression.
*
return self::$PAGE_IMPORT;
}
+ if (startswith($query, 'do='. self::$PAGE_PLUGINSADMIN)) {
+ return self::$PAGE_PLUGINSADMIN;
+ }
+
+ if (startswith($query, 'do='. self::$PAGE_SAVE_PLUGINSADMIN)) {
+ return self::$PAGE_SAVE_PLUGINSADMIN;
+ }
+
return self::$PAGE_LINKLIST;
}
}
\ No newline at end of file
--- /dev/null
+/**
+ * Change the position counter of a row.
+ *
+ * @param elem Element Node to change.
+ * @param toPos int New position.
+ */
+function changePos(elem, toPos)
+{
+ var elemName = elem.getAttribute('data-line')
+
+ elem.setAttribute('data-order', toPos);
+ var hiddenInput = document.querySelector('[name="order_'+ elemName +'"]');
+ hiddenInput.setAttribute('value', toPos);
+}
+
+/**
+ * Move a row up or down.
+ *
+ * @param pos Element Node to move.
+ * @param move int Move: +1 (down) or -1 (up)
+ */
+function changeOrder(pos, move)
+{
+ var newpos = parseInt(pos) + move;
+ var line = document.querySelector('[data-order="'+ pos +'"]');
+ var changeline = document.querySelector('[data-order="'+ newpos +'"]');
+ var parent = changeline.parentNode;
+
+ changePos(line, newpos);
+ changePos(changeline, parseInt(pos));
+ var changeItem = move < 0 ? changeline : changeline.nextSibling;
+ parent.insertBefore(line, changeItem);
+}
+
+/**
+ * Move a row up in the table.
+ *
+ * @param pos int row counter.
+ *
+ * @returns false
+ */
+function orderUp(pos)
+{
+ if (pos == 0) {
+ return false;
+ }
+ changeOrder(pos, -1);
+ return false;
+}
+
+/**
+ * Move a row down in the table.
+ *
+ * @param pos int row counter.
+ *
+ * @returns false
+ */
+function orderDown(pos)
+{
+ var lastpos = document.querySelector('[data-order]:last-child').getAttribute('data-order');
+ if (pos == lastpos) {
+ return false;
+ }
+
+ changeOrder(pos, +1);
+ return false;
+}
float: left;
}
+#pluginsadmin {
+ width: 80%;
+ padding: 20px 0 0 20px;
+}
+
+#pluginsadmin section {
+ padding: 20px 0;
+}
+
+#pluginsadmin .plugin_parameters {
+ margin: 10px 0;
+}
+
+#pluginsadmin h1 {
+ font-style: normal;
+}
+
+#pluginsadmin h2 {
+ font-size: 1.4em;
+ font-weight: bold;
+}
+
+#pluginsadmin table {
+ width: 100%;
+}
+
+#pluginsadmin table, #pluginsadmin th, #pluginsadmin td {
+ border-width: 1px 0;
+ border-style: solid;
+ border-color: #c0c0c0;
+}
+
+#pluginsadmin table th {
+ font-weight: bold;
+ padding: 10px 0;
+}
+
+#pluginsadmin table td {
+ padding: 5px 0;
+}
+
+#pluginsadmin input[type=submit] {
+ margin: 10px 0;
+}
+
+#pluginsadmin .plugin_parameter {
+ padding: 5px 0;
+ border-width: 1px 0;
+ border-style: solid;
+ border-color: #c0c0c0;
+}
+
+#pluginsadmin .float_label {
+ float: left;
+ width: 20%;
+}
+
+#pluginsadmin a {
+ color: black;
+}
/* 404 page */
.error-container {
exit;
}
+ // Plugin administration page
+ if ($targetPage == Router::$PAGE_PLUGINSADMIN) {
+ $pluginMeta = $pluginManager->getPluginsMeta();
+
+ // Split plugins into 2 arrays: ordered enabled plugins and disabled.
+ $enabledPlugins = array_filter($pluginMeta, function($v) { return $v['order'] !== false; });
+ // Load parameters.
+ $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $GLOBALS['plugins']);
+ uasort(
+ $enabledPlugins,
+ function($a, $b) { return $a['order'] - $b['order']; }
+ );
+ $disabledPlugins = array_filter($pluginMeta, function($v) { return $v['order'] === false; });
+
+ $PAGE->assign('enabledPlugins', $enabledPlugins);
+ $PAGE->assign('disabledPlugins', $disabledPlugins);
+ $PAGE->renderPage('pluginsadmin');
+ exit;
+ }
+
+ // Plugin administration form action
+ if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) {
+ try {
+ if (isset($_POST['parameters_form'])) {
+ unset($_POST['parameters_form']);
+ foreach ($_POST as $param => $value) {
+ $GLOBALS['plugins'][$param] = escape($value);
+ }
+ }
+ else {
+ $GLOBALS['config']['ENABLED_PLUGINS'] = save_plugin_config($_POST);
+ }
+ writeConfig($GLOBALS, isLoggedIn());
+ }
+ catch (Exception $e) {
+ error_log(
+ 'ERROR while saving plugin configuration:.' . PHP_EOL .
+ $e->getMessage()
+ );
+
+ // TODO: do not handle exceptions/errors in JS.
+ echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=pluginsadmin\';</script>';
+ exit;
+ }
+ header('Location: ?do='. Router::$PAGE_PLUGINSADMIN);
+ exit;
+ }
+
// -------- Otherwise, simply display search form and links:
showLinkList($PAGE, $LINKSDB);
exit;
--- /dev/null
+description="Adds the addlink input on the linklist page."
--- /dev/null
+description="For each link, add an Archive.org icon."
--- /dev/null
+description="A demo plugin covering all use cases for template designers and plugin developers."
--- /dev/null
+description="Add a button in the toolbar allowing to watch all videos."
--- /dev/null
+description="For each link, add a QRCode icon ."
--- /dev/null
+description="For each link, add a ReadItYourself icon to save the shaared URL."
+parameters=READITYOUSELF_URL;
\ No newline at end of file
include PluginManager::$PLUGINS_PATH . '/readityourself/config.php';
}
-if (!isset($GLOBALS['plugins']['READITYOUSELF_URL'])) {
+if (empty($GLOBALS['plugins']['READITYOUSELF_URL'])) {
$GLOBALS['plugin_errors'][] = 'Readityourself plugin error: '.
'Please define "$GLOBALS[\'plugins\'][\'READITYOUSELF_URL\']" '.
'in "plugins/readityourself/config.php" or in your Shaarli config.php file.';
--- /dev/null
+description="For each link, add a Wallabag icon to save it in your instance."
+parameters="WALLABAG_URL"
\ No newline at end of file
include PluginManager::$PLUGINS_PATH . '/wallabag/config.php';
}
-if (!isset($GLOBALS['plugins']['WALLABAG_URL'])) {
+if (empty($GLOBALS['plugins']['WALLABAG_URL'])) {
$GLOBALS['plugin_errors'][] = 'Wallabag plugin error: '.
'Please define "$GLOBALS[\'plugins\'][\'WALLABAG_URL\']" '.
'in "plugins/wallabag/config.php" or in your Shaarli config.php file.';
include self::$configFields['config']['CONFIG_FILE'];
$this->assertEquals(self::$configFields['login'], $GLOBALS['login']);
}
+
+ /**
+ * Test save_plugin_config with valid data.
+ *
+ * @throws PluginConfigOrderException
+ */
+ public function testSavePluginConfigValid()
+ {
+ $data = array(
+ 'order_plugin1' => 2, // no plugin related
+ 'plugin2' => 0, // new - at the end
+ 'plugin3' => 0, // 2nd
+ 'order_plugin3' => 8,
+ 'plugin4' => 0, // 1st
+ 'order_plugin4' => 5,
+ );
+
+ $expected = array(
+ 'plugin3',
+ 'plugin4',
+ 'plugin2',
+ );
+
+ $out = save_plugin_config($data);
+ $this->assertEquals($expected, $out);
+ }
+
+ /**
+ * Test save_plugin_config with invalid data.
+ *
+ * @expectedException PluginConfigOrderException
+ */
+ public function testSavePluginConfigInvalid()
+ {
+ $data = array(
+ 'plugin2' => 0,
+ 'plugin3' => 0,
+ 'order_plugin3' => 0,
+ 'plugin4' => 0,
+ 'order_plugin4' => 0,
+ );
+
+ save_plugin_config($data);
+ }
+
+ /**
+ * Test save_plugin_config without data.
+ */
+ public function testSavePluginConfigEmpty()
+ {
+ $this->assertEquals(array(), save_plugin_config(array()));
+ }
+
+ /**
+ * Test validate_plugin_order with valid data.
+ */
+ public function testValidatePluginOrderValid()
+ {
+ $data = array(
+ 'order_plugin1' => 2,
+ 'plugin2' => 0,
+ 'plugin3' => 0,
+ 'order_plugin3' => 1,
+ 'plugin4' => 0,
+ 'order_plugin4' => 5,
+ );
+
+ $this->assertTrue(validate_plugin_order($data));
+ }
+
+ /**
+ * Test validate_plugin_order with invalid data.
+ */
+ public function testValidatePluginOrderInvalid()
+ {
+ $data = array(
+ 'order_plugin1' => 2,
+ 'order_plugin3' => 1,
+ 'order_plugin4' => 1,
+ );
+
+ $this->assertFalse(validate_plugin_order($data));
+ }
+
+ /**
+ * Test load_plugin_parameter_values.
+ */
+ public function testLoadPluginParameterValues()
+ {
+ $plugins = array(
+ 'plugin_name' => array(
+ 'parameters' => array(
+ 'param1' => true,
+ 'param2' => false,
+ 'param3' => '',
+ )
+ )
+ );
+
+ $parameters = array(
+ 'param1' => 'value1',
+ 'param2' => 'value2',
+ );
+
+ $result = load_plugin_parameter_values($plugins, $parameters);
+ $this->assertEquals('value1', $result['plugin_name']['parameters']['param1']);
+ $this->assertEquals('value2', $result['plugin_name']['parameters']['param2']);
+ $this->assertEquals('', $result['plugin_name']['parameters']['param3']);
+ }
}
$pluginManager->load(array('nope', 'renope'));
}
+
+ /**
+ * Test plugin metadata loading.
+ */
+ public function testGetPluginsMeta()
+ {
+ $pluginManager = PluginManager::getInstance();
+
+ PluginManager::$PLUGINS_PATH = self::$pluginPath;
+ $pluginManager->load(array(self::$pluginName));
+
+ $expectedParameters = array(
+ 'pop' => '',
+ 'hip' => '',
+ );
+ $meta = $pluginManager->getPluginsMeta();
+ $this->assertEquals('test plugin', $meta[self::$pluginName]['description']);
+ $this->assertEquals($expectedParameters, $meta[self::$pluginName]['parameters']);
+ }
}
\ No newline at end of file
--- /dev/null
+description="test plugin"
+parameters="pop;hip"
\ No newline at end of file
--- /dev/null
+<!DOCTYPE html>
+<html>
+<head>{include="includes"}</head>
+<body>
+<div id="pageheader">
+ {include="page.header"}
+</div>
+
+<noscript>
+ <div>
+ <ul class="errors">
+ <li>You need to enable Javascript to change plugin loading order.</li>
+ </ul>
+ </div>
+ <div class="clear"></div>
+</noscript>
+
+<div id="pluginsadmin">
+ <form action="?do=save_pluginadmin" method="POST">
+ <section id="enabled_plugins">
+ <h1>Enabled Plugins</h1>
+
+ <div>
+ {if="count($enabledPlugins)==0"}
+ <p>No plugin enabled.</p>
+ {else}
+ <table id="plugin_table">
+ <thead>
+ <tr>
+ <th class="center">Disable</th>
+ <th class="center">Order</th>
+ <th>Name</th>
+ <th>Description</th>
+ </tr>
+ </thead>
+ <tbody>
+ {loop="$enabledPlugins"}
+ <tr data-line="{$key}" data-order="{$counter}">
+ <td class="center"><input type="checkbox" name="{$key}" checked="checked"></td>
+ <td class="center">
+ <a href="#"
+ onclick="return orderUp(this.parentNode.parentNode.getAttribute('data-order'));">
+ ▲
+ </a>
+ <a href="#"
+ onclick="return orderDown(this.parentNode.parentNode.getAttribute('data-order'));">
+ ▼
+ </a>
+ <input type="hidden" name="order_{$key}" value="{$counter}">
+ </td>
+ <td>{$key}</td>
+ <td>{$value.description}</td>
+ </tr>
+ {/loop}
+ </tbody>
+ </table>
+ {/if}
+ </div>
+ </section>
+
+ <section id="disabled_plugins">
+ <h1>Disabled Plugins</h1>
+
+ <div>
+ {if="count($disabledPlugins)==0"}
+ <p>No plugin disabled.</p>
+ {else}
+ <table>
+ <tr>
+ <th class="center">Enable</th>
+ <th>Name</th>
+ <th>Description</th>
+ </tr>
+ {loop="$disabledPlugins"}
+ <tr>
+ <td class="center"><input type="checkbox" name="{$key}"></td>
+ <td>{$key}</td>
+ <td>{$value.description}</td>
+ </tr>
+ {/loop}
+ </table>
+ {/if}
+ </div>
+
+ <div class="center">
+ <input type="submit" value="Save"/>
+ </div>
+ </section>
+ </form>
+
+ <form action="?do=save_pluginadmin" method="POST">
+ <section id="plugin_parameters">
+ <h1>Enabled Plugin Parameters</h1>
+
+ <div>
+ {if="count($enabledPlugins)==0"}
+ <p>No plugin enabled.</p>
+ {else}
+ {loop="$enabledPlugins"}
+ {if="count($value.parameters) > 0"}
+ <div class="plugin_parameters">
+ <h2>{$key}</h2>
+ {loop="$value.parameters"}
+ <div class="plugin_parameter">
+ <div class="float_label">
+ <label for="{$key}">
+ <code>{$key}</code>
+ </label>
+ </div>
+ <div class="float_input">
+ <input name="{$key}" value="{$value}" id="{$key}"/>
+ </div>
+ </div>
+ {/loop}
+ </div>
+ {/if}
+ {/loop}
+ {/if}
+ <div class="center">
+ <input type="submit" name="parameters_form" value="Save"/>
+ </div>
+ </div>
+ </section>
+ </form>
+
+</div>
+{include="page.footer"}
+
+<script src="inc/plugin_admin.js#"></script>
+</body>
+</html>
\ No newline at end of file
<div id="pageheader">
{include="page.header"}
<div id="toolsdiv">
- {if="!$GLOBALS['config']['OPEN_SHAARLI']"}<a href="?do=changepasswd"><b>Change password</b> <span>: Change your password.</span></a><br><br>{/if}
- <a href="?do=configure"><b>Configure your Shaarli</b> <span>: Change Title, timezone...</span></a><br><br>
- <a href="?do=changetag"><b>Rename/delete tags</b> <span>: Rename or delete a tag in all links</span></a><br><br>
- <a href="?do=import"><b>Import</b> <span>: Import Netscape html bookmarks (as exported from Firefox, Chrome, Opera, delicious...)</span></a> <br><br>
- <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>
+ <a href="?do=configure"><b>Configure your Shaarli</b><span>: Change Title, timezone...</span></a>
+ <br><br>
+ <a href="?do=pluginadmin"><b>Plugin administration</b><span>: Enable, disable and configure plugins.</span></a>
+ <br><br>
+ {if="!$GLOBALS['config']['OPEN_SHAARLI']"}<a href="?do=changepasswd"><b>Change password</b><span>: Change your password.</span></a>
+ <br><br>{/if}
+ <a href="?do=changetag"><b>Rename/delete tags</b><span>: Rename or delete a tag in all links</span></a>
+ <br><br>
+ <a href="?do=import"><b>Import</b><span>: Import Netscape html bookmarks (as exported from Firefox, Chrome, Opera, delicious...)</span></a>
+ <br><br>
+ <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>
<a class="smallbutton"
onclick="return alertBookmarklet();"
href="javascript:(