by Chris Smith
This plugin allows wiki administrators to easily alter its configuration settings online from the comfort of their favourite webbrowser.
Settings are shown with different backgrounds to highlight their source. A blue background is used to show default values (conf/dokuwiki.php). A white background to show local changes (conf/local.php) and a light red background to show protected settings (conf/local.protected.php).
On saving changed settings this plugin will copy the current local settings file (conf/local.php) to conf/local.php.bak and save the updated settings to conf/local.php. It will never make any changes to the default settings stored in conf/dokuwiki.php. However, any settings found in conf/local.php will override the default settings as explained in the manual.
The plugin adds the following lines to the top of conf/local.php when it updates it …
/* * Dokuwiki's Main Configuration File - Local Settings * Auto-generated by config plugin * Run for user: <username> * Date: <current date/time, rfc 2822 format (day, dd MMM YYYY hh:mm:ss TZ)> */
You can protect certain settings by placing them in conf/local.protected.php. This plugin adds the following line to the bottom of conf/local.php —
@include(DOKU_CONF.'local.protected.php');— thereby ensuring the protected settings are included and will override any previously set values from
conf/dokuwiki.phpandconf/local.php. Any settings found in this file will be displayed by the plugin surrounded in light-red to indicated their protected status. Editing of protected values is disabled.
Plugin sources: zip format (15k), tar.gz format (12k), darcs repository
The first two links may be used with the plugin manager and the third with the darcs plugin.
To install the plugin manually, download the package from either of the first two links to your plugin folder, lib/plugins and extract its contents. That will create a new plugin folder, lib/plugins/config, and install the plugin.
The folder will contain:
admin.php the main plugin script style.css styles to assist in display of settings data settings/ folder containing scripts & data to manipulate dokuwiki settings settings/config.class.php contains configuration class and generic setting classes settings/extra.class.php contains additional setting classes specific to some dokuwiki settings settings/config.metadata.php metadata describing dokuwiki's configuration settings lang/ localisation root folder lang/xx/ folder for language xx, at least en (english) will be present lang/xx/intro.txt lang/xx/lang.php localised language strings
The plugin is now installed.
The plugin has been structured to work to separate knowledge of Dokuwiki settings from the scripts which handle the settings.
Of the remaining files,
admin.php
<?php /** * Configuration Manager admin plugin * * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) * @author Christopher Smith <chris@jalakai.co.uk> */ if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/'); if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); require_once(DOKU_PLUGIN.'admin.php'); define('CM_KEYMARKER','____'); // used for settings with multiple dimensions of array indices define('PLUGIN_SELF',dirname(__FILE__).'/'); define('PLUGIN_METADATA',PLUGIN_SELF.'settings/config.metadata.php'); require_once(PLUGIN_SELF.'settings/config.class.php'); // main configuration class and generic settings classes require_once(PLUGIN_SELF.'settings/extra.class.php'); // settings classes specific to these settings /** * All DokuWiki plugins to extend the admin function * need to inherit from this class */ class admin_plugin_config extends DokuWiki_Admin_Plugin { var $_file = PLUGIN_METADATA; var $_config = null; var $_input = null; var $_changed = false; // set to true if configuration has altered var $_error = false; var $_session_started = false; /** * return some info */ function getInfo(){ return array( 'author' => 'Christopher Smith', 'email' => 'chris@jalakai.co.uk', 'date' => '2005-11-11', 'name' => 'Configuration Manager', 'desc' => "Manage Dokuwiki's Configuration Settings", 'url' => 'http://wiki.splitbrain.org/plugin:config', ); } function getMenuSort() { return 100; } /** * handle user request */ function handle() { if (!$this->_restore_session()) return $this->_close_session(); if (!isset($_REQUEST['save']) || ($_REQUEST['save'] != 1)) return $this->_close_session(); if (is_null($this->_config)) { $this->_config = new configuration($this->_file); } // don't go any further if the configuration is locked if ($this->_config->_locked) return $this->_close_session(); $this->_input = $_REQUEST['config']; while (list($key) = each($this->_config->setting)) { $input = isset($this->_input[$key]) ? $this->_input[$key] : NULL; if ($this->_config->setting[$key]->update($input)) { $this->_changed = true; } if ($this->_config->setting[$key]->error()) $this->_error = true; } if ($this->_changed && !$this->_error) { $this->_config->save_settings($this->getPluginName()); // save state & force a page reload to get the new settings to take effect $_SESSION['PLUGIN_CONFIG'] = array('state' => 'updated', 'time' => time()); $this->_close_session(); header("Location: ".wl($ID)."?do=admin&page=config"); exit(); } $this->_close_session(); } /** * output appropriate html */ function html() { global $lang; if (is_null($this->_config)) { $this->_config = new configuration($this->_file); } print $this->locale_xhtml('intro'); ptln('<div id="configmanager">'); if ($this->_config->locked) ptln('<p class="info">'.$this->getLang('locked').'</p>'); elseif ($this->_error) ptln('<p class="error">'.$this->getLang('error').'</p>'); elseif ($this->_changed) ptln('<p class="ok">'.$this->getLang('updated').'</p>'); ptln('<form action="'.wl($id).'" method="post">'); ptln(' <table class="inline">'); foreach($this->_config->setting as $setting) { list($label,$input) = $setting->html($this, $this->_error); $class = $setting->is_default() ? ' class="default"' : ($setting->is_protected() ? ' class="protected"' : ''); $error = $setting->error() ? ' class="error"' : ''; ptln(' <tr'.$class.'>'); ptln(' <td>'.$label.'</td>'); ptln(' <td'.$error.'>'.$input.'</td>'); ptln(' </tr>'); } ptln(' </table>'); ptln('<p>'); ptln(' <input type="hidden" name="do" value="admin" />'); ptln(' <input type="hidden" name="page" value="config" />'); if (!$this->_config->locked) { ptln(' <input type="hidden" name="save" value="1" />'); ptln(' <input type="submit" name="submit" value="'.$lang['btn_save'].'" />'); ptln(' <input type="reset" value="'.$lang['btn_reset'].'" />'); } ptln('</p>'); ptln('</form>'); ptln('</div>'); } /** * @return boolean true - proceed with handle, false - don't proceed */ function _restore_session() { // dokuwiki closes the session before act_dispatch. $_SESSION variables are all set, // however they can't be changed without starting the session again if (!headers_sent()) { session_start(); $this->_session_started = true; } if (!isset($_SESSION['PLUGIN_CONFIG'])) return true; $session = $_SESSION['PLUGIN_CONFIG']; unset($_SESSION['PLUGIN_CONFIG']); // still valid? if (time() - $session['time'] > 120) return true; switch ($session['state']) { case 'updated' : $this->_changed = true; return false; } return true; } function _close_session() { if ($this->_session_started) session_write_close(); } }
settings/config.class.php
<?php /* * Configuration Class and generic setting classes * * @author Chris Smith <chris@jalakai.co.uk> */ if (!class_exists('configuration')) { class configuration { var $_name = 'conf'; // name of the config variable found in the files (overridden by $config['varname']) var $_format = 'php'; // format of the config file, supported formats - php (overridden by $config['format']) var $_heading = ''; // heading string written at top of config file - don't include comment indicators var $_loaded = false; // set to true after configuration files are loaded var $_metadata = array(); // holds metadata describing the settings var $setting = array(); // array of setting objects var $locked = false; // configuration is considered locked if it can't be updated // filenames, these will be eval()'d prior to use so maintain any constants in output var $_default_file = ''; var $_local_file = ''; var $_protected_file = ''; /** * constructor */ function configuration($datafile) { if (!@file_exists($datafile)) { msg('No configuration metadata found at - '.htmlspecialchars($datafile),-1); return; } include($datafile); if (isset($config['varname'])) $this->_name = $config['varname']; if (isset($config['format'])) $this->_format = $config['format']; if (isset($config['heading'])) $this->_heading = $config['heading']; if (isset($file['default'])) $this->_default_file = $file['default']; if (isset($file['local'])) $this->_local_file = $file['local']; if (isset($file['protected'])) $this->_protected_file = $file['protected']; $this->locked = $this->_is_locked(); $this->_metadata = $meta; $this->retrieve_settings(); } function retrieve_settings() { if (!$this->_loaded) { $default = $this->_read_config($this->_default_file); $local = $this->_read_config($this->_local_file); $protected = $this->_read_config($this->_protected_file); $keys = array_merge(array_keys($this->_metadata),array_keys($default), array_keys($local), array_keys($protected)); $keys = array_unique($keys); foreach ($keys as $key) { if (isset($this->_metadata[$key])) { $class = $this->_metadata[$key][0]; $class = ($class && class_exists('setting_'.$class)) ? 'setting_'.$class : 'setting'; $param = $this->_metadata[$key]; array_shift($param); } else { $class = 'setting'; $param = NULL; } $this->setting[$key] = new $class($key,$param); $this->setting[$key]->initialize($default[$key],$local[$key],$protected[$key]); } $this->_loaded = true; } } function save_settings($id, $header='', $backup=true) { if ($this->locked) return false; $file = eval('return '.$this->_local_file.';'); // backup current file (remove any existing backup) if (@file_exists($file) && $backup) { if (@file_exists($file.'.bak')) @unlink($file.'.bak'); if (!@rename($file, $file.'.bak')) return false; } if (!$fh = @fopen($file, 'wb')) { @rename($file.'.bak', $file); // problem opening, restore the backup return false; } if (empty($header)) $header = $this->_heading; $out = '<'.'?php'."\n". "/*\n". " * ".$header." \n". " * Auto-generated by ".$id." plugin \n". " * Run for user: ".$_SERVER['REMOTE_USER']."\n". " * Date: ".date('r')."\n". " */\n\n"; foreach ($this->setting as $setting) { $out .= $setting->out($this->_name, $this->_format); } if ($this->_protected_file) { $out .= "\n@include(".$this->_protected_file.");\n"; } $out .= "\n// end auto-generated content\n"; @fwrite($fh, $out); fclose($fh); return true; } /** * return an array of config settings */ function _read_config($file) { if (!$file) return array(); $config = array(); $file = eval('return '.$file.';'); if ($this->_format == 'php') { $contents = php_strip_whitespace($file); $pattern = '/\$'.$this->_name.'\[[\'"]([^=]+)[\'"]\]=(.*?);/'; preg_match_all($pattern,$contents,$matches=array(),PREG_SET_ORDER); for ($i=0; $i<count($matches); $i++) { // correct issues with the incoming data // FIXME ... for now merge multi-dimensional array indices using _ $key = preg_replace('/.\]\[./',CM_KEYMARKER,$matches[$i][1]); // remove quotes from quoted strings & unescape escaped data (FIXME) $value = preg_replace('/^(\'|")(.*)(?<!\\\\)\1$/','$2',$matches[$i][2]); $value = strtr($value, array('\\\\'=>'\\','\\\''=>'\'','\\"'=>'"')); $config[$key] = $value; } } return $config; } // configuration is considered locked if there is no local settings filename // or the directory its in is not writable or the file exists and is not writable function _is_locked() { if (!$this->_local_file) return true; $local = eval('return '.$this->_local_file.';'); if (!is_writable(dirname($local))) return true; if (file_exists($local) && !is_writable($local)) return true; return false; } /** * not used ... conf's contents are an array! * reduce any multidimensional settings to one dimension using CM_KEYMARKER */ function _flatten($conf,$prefix='') { $out = array(); foreach($conf as $key => $value) { if (!is_array($value)) { $out[$prefix.$key] = $value; continue; } $tmp = $this->_flatten($value,$prefix.$key.CM_KEYMARKER); $out = array_merge($out,$tmp); } return $out; } } } if (!class_exists('setting')) { class setting { var $_key = ''; var $_default = NULL; var $_local = NULL; var $_protected = NULL; var $_pattern = ''; var $_error = false; // only used by those classes which error check var $_input = NULL; // only used by those classes which error check function setting($key, $params=NULL) { $this->_key = $key; if (is_array($params)) { foreach($params as $property => $value) { $this->$property = $value; } } } /** * recieves current values for the setting $key */ function initialize($default, $local, $protected) { if (isset($default)) $this->_default = $default; if (isset($local)) $this->_local = $local; if (isset($protected)) $this->_protected = $protected; } /* * update setting with user provided value $input * if value fails error check, save it * * FIXME value must be cleaned and validated */ function update($input) { if (is_null($input)) return false; if ($this->is_protected()) return false; $value = is_null($this->_local) ? $this->_default : $this->_local; if ($value == $input) return false; if ($this->_pattern && !preg_match($this->_pattern,$input)) { $this->_error = true; $this->_input = $input; return false; } $this->_local = $input; return true; } /** * @return array(string $label_html, string $input_html) */ function html(&$plugin, $echo=false) { $value = ''; $disable = ''; if ($this->is_protected()) { $value = $this->_protected; $disable = 'disabled="disabled"'; } else { if ($echo && $this->_error) { $value = $this->_input; } else { $value = is_null($this->_local) ? $this->_default : $this->_local; } } $key = htmlspecialchars($this->_key); $value = htmlspecialchars($value); $label = '<label for="config_'.$key.'">'.$this->prompt($plugin).'</label>'; $input = '<input id="config_'.$key.'" name="config['.$key.']" type="text" class="text" value="'.$value.'" '.$disable.'/>'; return array($label,$input); } /** * generate string to save setting value to file according to $fmt */ function out($var, $fmt='php') { if ($this->is_protected()) return ''; if (is_null($this->_local) || ($this->_default == $this->_local)) return ''; if ($fmt=='php') { // translation string needs to be improved FIXME $tr = array("\n"=>'\n', "\r"=>'\r', "\t"=>'\t', "\\" => '\\\\', "'" => '\\\''); $out = '$'.$var."['".$this->_out_key()."'] = '".strtr($this->_local, $tr)."';\n"; return $out; } } function prompt(&$plugin) { $prompt = $plugin->getLang($this->_key); if (!$prompt) $prompt = str_replace(array('____','_'),' ',$this->_key); return htmlspecialchars($prompt); } function is_protected() { return !is_null($this->_protected); } function is_default() { return !$this->is_protected() && is_null($this->_local); } function error() { return $this->_error; } function _out_key() { return str_replace(CM_KEYMARKER,"']['",$this->_key); } } } if (!class_exists('setting_password')) { class setting_password extends setting { function update($input) { if ($this->is_protected()) return false; if (!$input) return false; if ($this->_pattern && !preg_match($this->_pattern,$input)) { $this->_error = true; $this->_input = $input; return false; } $this->_local = $input; return true; } function html(&$plugin, $echo=false) { $value = ''; $disable = $this->is_protected() ? 'disabled="disabled"' : ''; $key = htmlspecialchars($this->_key); $label = '<label for="config_'.$key.'">'.$this->prompt($plugin).'</label>'; $input = '<input id="config_'.$key.'" name="config['.$key.']" type="password" class="text" value="" '.$disable.'/>'; return array($label,$input); } } } if (!class_exists('setting_email')) { class setting_email extends setting { var $_pattern = '#([a-z0-9\-_.]+?)@([\w\-]+\.([\w\-\.]+\.)*[\w]+)#i'; } } if (!class_exists('setting_numeric')) { class setting_numeric extends setting { var $_pattern = '/^[-+\/*0-9 ]*$/'; function out($var, $fmt='php') { if ($this->is_protected()) return ''; if (is_null($this->_local) || ($this->_default == $this->_local)) return ''; if ($fmt=='php') { $out .= '$'.$var."['".$this->_out_key()."'] = ".$this->_local.";\n"; return $out; } } } } if (!class_exists('setting_onoff')) { class setting_onoff extends setting_numeric { function html(&$plugin) { $value = ''; $disable = ''; if ($this->is_protected()) { $value = $this->_protected; $disable = ' disabled="disabled"'; } else { $value = is_null($this->_local) ? $this->_default : $this->_local; } $key = htmlspecialchars($this->_key); $checked = ($value) ? ' checked="checked"' : ''; $label = '<label for="config_'.$key.'">'.$this->prompt($plugin).'</label>'; $input = '<div class="input"><input id="config_'.$key.'" name="config['.$key.']" type="checkbox" class="checkbox" value="1"'.$checked.$disable.'/></div>'; return array($label,$input); } function update($input) { if ($this->is_protected()) return false; $input = ($input) ? 1 : 0; $value = is_null($this->_local) ? $this->_default : $this->_local; if ($value == $input) return false; $this->_local = $input; return true; } } } if (!class_exists('setting_mulitchoice')) { class setting_multichoice extends setting { var $_choices = array(); function html(&$plugin) { $value = ''; $disable = ''; $nochoice = ''; if ($this->is_protected()) { $value = $this->_protected; $disable = ' disabled="disabled"'; } else { $value = is_null($this->_local) ? $this->_default : $this->_local; } // ensure current value is included if (!in_array($value, $this->_choices)) { $this->_choices[] = $value; } // disable if no other choices if (!$this->is_protected() && count($this->_choices) <= 1) { $disable = ' disabled="disabled"'; $nochoice = $plugin->getLang('nochoice'); } $key = htmlspecialchars($this->_key); $label = '<label for="config_'.$key.'">'.$this->prompt($plugin).'</label>'; $input = "<div class=\"input\">\n"; $input .= '<select id="config_'.$key.'" name="config['.$key.']"'.$disable.'>'."\n"; foreach ($this->_choices as $choice) { $selected = ($value == $choice) ? ' selected="selected"' : ''; $option = $plugin->getLang($this->_key.'_o_'.$choice); if (!$option) $option = $choice; $choice = htmlspecialchars($choice); $option = htmlspecialchars($option); $input .= ' <option value="'.$choice.'"'.$selected.' >'.$option.'</option>'."\n"; } $input .= "</select> $nochoice \n"; $input .= "</div>\n"; return array($label,$input); } function update($input) { if (is_null($input)) return false; if ($this->is_protected()) return false; $value = is_null($this->_local) ? $this->_default : $this->_local; if ($value == $input) return false; if (!in_array($input, $this->_choices)) return false; $this->_local = $input; return true; } } } if (!class_exists('setting_dirchoice')) { class setting_dirchoice extends setting_multichoice { var $_dir = ''; function initialize($default,$local,$protected) { // populate $this->_choices with a list of available templates $list = array(); if ($dh = @opendir($this->_dir)) { while (false !== ($entry = readdir($dh))) { if ($entry == '.' || $entry == '..') continue; $file = (is_link($this->_dir.$entry)) ? readlink($this->_dir.$entry) : $entry; if (is_dir($this->_dir.$file)) $list[] = $entry; } closedir($dh); } sort($list); $this->_choices = $list; parent::initialize($default,$local,$protected);