Configuration plugin

by Chris Smith

experimental

This plugin allows wiki administrators to easily alter its configuration settings online from the comfort of their favourite webbrowser.

Usage Notes

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)>
 */

Protected Settings

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.php and conf/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.

Installation

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.

Details

The plugin has been structured to work to separate knowledge of Dokuwiki settings from the scripts which handle the settings.

  • admin.php provides the interface to dokuwiki's admin menu, the user and instantiates the configuration object.
  • settings/config.class.php contains the configuration class and several generic setting classes. It reads the metadata which describes the settings (settings/config.metadata.php) and loads dokuwiki's settings into objects capable of understanding each setting.
  • settings/extra.class.php provides some extra setting classes where a particular setting can not be handled by one of the generic classes.

Of the remaining files,

  • style.css has styles to assist in displaying settings, particular to indicated errors;
  • intro.txt & lang.php contain locale dependent strings displayed to the user. These two can be translated these into your local language. The translations should be stored in a sub-folder of lang with the same code you have chosen for your Dokuwiki installation.

admin.php

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

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);