Extension:Configurator

From MediaWiki.org
Jump to navigation Jump to search
This script is dangerous. It changes files on your server and has the potential to do all kinds of bad stuff. For advanced users only.
MediaWiki extensions manual
Crystal Clear action run.svg
Configurator
Release status: experimental
Implementation Special page
Description Allows a user with the siteadmin right to change variables in LocalSettings.php through a web interface
Author(s) Christian Neubauer (modified Thomas Candrian) (cneubauertalk)
Latest version 0.5.1 (2013-08-12)
MediaWiki 1.12+
Database changes No
License Creative Commons Attribution 3.0
Download Copy this code
Hooks used
LanguageGetSpecialPageAliases
Translate the Configurator extension if it is available at translatewiki.net
Check usage and version matrix.

The Configurator extension reads the LocalSettings.php file and displays a form on a custom special page where the user can modify configuration variables that have previously been defined in the file. It only shows MediaWiki variables defined in LocalSettings.php that appear on their own line and have values that are a "string", int, bool, CONSTANT, or $variable. It excludes passwords. When the user submits the form, the extension backs up the current LocalSettings.php file and writes a new one.

It also has some support for extensions but the enabling/disabling stuff doesn't work yet.

Usage[edit]

  1. Log in as a user with the 'siteadmin' permission.
  2. Browse to Special Pages -> Configurator.
  3. Add/Modify values.
  4. Click Submit.

Download instructions[edit]

Cut and paste the code found below and place it in $IP/extensions/Configurator/Configurator.php. Note: $IP stands for the root directory of your MediaWiki installation, the same directory that holds LocalSettings.php.

Installation[edit]

To install this extension, add the following to LocalSettings.php:

require_once("$IP/extensions/Configurator/Configurator.php");

You also have to make the files LocalSettings.php and LocalSettings.old.php writable by your web server.

Code[edit]

Configurator.php[edit]

<?php
if ( ! defined( 'MEDIAWIKI' ) ) {
  echo <<<EOT
To install Configurator, put the following line in LocalSettings.php:
require_once( "$IP/extensions/Configurator/Configurator.php" );
EOT;
  exit( 1 );
}

$wgExtensionCredits['specialpage'][] = array( 
	'name'   => 'Configurator',
	'version'=> '0.5.1',
	'url'    => 'https://www.mediawiki.org/wiki/Extension:Configurator',
	'author' => 'Christian Neubauer, Thomas Candrian',
	'descriptionmsg' => 'configurator-desc',
	'license-name' => 'CC-BY-3.0'
);

$dir = dirname(__FILE__) . '/';
$wgAutoloadClasses['ConfiguratorSpecialPage'] = $dir . 'Configurator.body.php';
$wgExtensionMessagesFiles['ConfiguratorSpecialPage'] = $dir . 'Configurator.i18n.php';
$wgSpecialPages['ConfiguratorSpecialPage'] = 'ConfiguratorSpecialPage';
$wgHooks['LanguageGetSpecialPageAliases'][] = 'ConfiguratorSpecialPage_Setup';
 
function ConfiguratorSpecialPage_Setup(&$specialPageArray, $code) {
  wfLoadExtensionMessages('ConfiguratorSpecialPage');
  $text = wfMsg('configurator-specialpage');
  $title = Title::newFromText($text);
  $specialPageArray['ConfiguratorSpecialPage'][] = $title->getDBKey();
  return true;
}

Configurator.body.php[edit]

<?php
if ( ! defined( 'MEDIAWIKI' ) )
    die('This extension is intended for use via MediaWiki.');
 
/**
 * Configurator 0.5.1
 *
 * WARNING: This script is dangerous.  It changes files on your server and has the
 * potential to do all kinds of bad stuff.  For advanced users only.
 *
 * This is a MediaWiki extension to allow an admin to change configuration options
 * via a web interface without having to open a shell on the machine and change
 * LocalSettings.php directly.  It changes variables that are described in
 * LocalSettings.php, not everything in DefaultSettings.php.
 *
 * The script opens LocalSettings.php and reads all the variables that look
 * like this: $wgVariableName = value; where value = true | false | $variable |
 * 'string' | "string" | CONSTANT_VAL. It then opens DefaultSettings.php and
 * looks for comment blocks that start with a slash dot and end with a dot slash
 * on the line preceding the variable definition.
 *
 * It displays each variable with a description if one was found in
 * DefaultSettings.php.  It only displays the limited value types described above.
 * It excludes complex php statements, objects, etc.
 *
 * You can change some of these values and submit the form.  The script will
 * backup your existing LocalSettings.php file to LocalSettings.old.php and
 * attempt to overwrite your LocalSettings.php file with the new values.  It will
 * only overwrite lines that look exactly like this:
$wg[variable_name] = [value];
 * Notice that the variable has to be on the beginning of the line and cannot have
 * any trailing comments after it.  Sorry Tennessee.
 *
 * DO THIS FIRST:
 * 
 * Before you run this script you have to do some stuff on your host to allow
 * the script to change your LocalSettings file.  If you run Windows, you can
 * probably skip these steps.  Otherwise you are going to have to do some touching,
 * chowning, and chmoding.
 * 
 * 1. Make LocalSettings.php writable by your web server.
 * 2. Make a LocalSettings.old.php file and make it writable by your web server.
 *
 * @package MediaWiki
 * @subpackage Extensions
 *
 * @author Christian Neubauer
 * @link http://www.mediawiki.org/wiki/Extension:Configurator
 * @copyright Copyright © 2008, Christian Neubauer
 * @license http://creativecommons.org/licenses/by/3.0/ Creative Commons By 3.0
 */
 
#------------------------------------------------------------------------------
# ConfiguratorSpecialPage Class
#------------------------------------------------------------------------------

/*
 * Class containing a form for manipulating a Configurator.
 */
class ConfiguratorSpecialPage extends SpecialPage {
    var $config;
 
    function ConfiguratorSpecialPage() {
        SpecialPage::SpecialPage('ConfiguratorSpecialPage', 'siteadmin');
        wfLoadExtensionMessages('ConfiguratorSpecialPage');
        $this->config = new Configurator();
    }
 
    /*
     * Basically the entry point.  If the form was submitted, handle the post.
     * Otherwise, create the form.
     */
    function execute( $par ) {
            global $wgOut, $wgRequest;
 
            $this->setHeaders();
            if($wgRequest->wasPosted()) {
                $extensions = array();
                foreach($wgRequest->getValues() as $key => $value) {
                    if(strpos($key, 'extension-') === 0) {
                        list(,$name,$key) = explode('-', $key);
                        if($extensions[$name] == null) {
                            $extensions[$name] = array();
                        }
                        $extensions[$name][$key] = $value;
                    } else {
                        $this->config->setValue($key, $value);
                    }
                }
                $this->config->setExtensions($extensions);
                $ok = $this->config->writeConfig($error);
                $result = '<pre>' . $error . '</pre>';
                $wgOut->addHTML( $result );
            } else {
                $ok = $this->config->readConfig($error);
                $result = '<pre>' . $error . '</pre>';
                if( $ok === false ) {
                    $wgOut->addHTML( $result );
                } else {
                    $wgOut->addHTML( $this->showForm() );
                }
            }
    }
 
    /*
     * Show the form for manipulating a LocalSettings file.
     */
    function showForm() {
        $html = $this->_doHeader() . '<h2>Core Config</h2>';
        $values = $this->config->getValues();
        foreach($values as $key => $value) {
            $value = htmlspecialchars($value);
            $comment = htmlspecialchars($this->config->getComment($key));
            if($comment) {
                $comment = '<div class="comment">' . $comment . '</div>';
            }
            $html .= '<div class="config_section">'.$comment.'<div class="config"><span class="key">$wg'.$key.'</span> = <input name="'.$key.'" class="value" type="text" value="'.$value.'" /></div></div>';
        }
        $html .= '<h2>Extensions</h2>';
        $extensions = $this->config->getExtensions();
        foreach($extensions as $name => $extension) {
            $html .= '<div class="extension_section"><h3>'.$name.'</h3>';
            foreach($extension as $key => $value) {
                if($key == 'path') {
                    $key = 'require_once';
                    $value = '<span class="value">'.htmlspecialchars($value).'</span>';
                } else {
                    $key = htmlspecialchars($key);
                    $value = '<input name="extension-'.$name.'-'.$key.'" class="value" type="text" value="'.htmlspecialchars($value).'" />';
                }
                $html .= '<div class="config"><span class="key">'.$key.'</span> = '.$value.'</div>';
            }
            $html .= '<div class="config"><span class="key">Enabled</span> = <input checked="true" name="'.$name.'-enabled" type="checkbox" value="enabled" /></div>';
            $html .= '</div>';
        }
        $html .= $this->_doFooter();
        return $html;
    }
 
    #--------------------------------------------------------------------------
    # Private Functions
    #--------------------------------------------------------------------------

    private function _doHeader() {
        return <<<EOT
    <style type="text/css">
      div.config_section, div.extension_section div.config {
        margin-top: 1px;
      }
      div.config_section pre {
        margin: 0;
      }
      div.config_section input[type="text"], div.extension_section input[type="text"] {
        width: 35em;
      }
      div.config_section div.comment, div.extension_section span.value {
        color: #808080;
        white-space: pre;
      }
      div.config_section span.key, div.extension_section span.key {
        color: #0000FF;
      }
      div.config_section div.config span.key, div.extension_section div.config span.key {
        float: left;
        width: 15em;
      }
    </style>
    <script type="text/javascript">
      function toggleComments() {
        var divs = document.getElementsByTagName('div');
        for(var i = 0, len = divs.length; i < len; i++) {
          if(divs[i].className == 'comment') {
            var display = divs[i].style.display;
            divs[i].style.display = (display == 'none') ? '' : 'none';
          }
        }
      }
    </script>
    <p>The following settings are defined in your LocalSettings.php file.  You can adjust the values, but be careful; Your computer might explode.
    <br /><strong>Note:</strong> This script will overwrite your LocalSettings.php file.  Make sure you back it up in a useable state first.
    <br /><strong>Note:</strong> This script won't work unless the LocalSettings file is writeable by the user that your web server runs as.  This is probably a very bad idea!.
    <br /><strong>Note:</strong> Make sure you add quotes around stuff that should be a string value or you'll probably brick your wiki.
    <br /><strong>Note:</strong> Variables that aren't a string, true, false, a variable, or a constant are not shown.  Also, passwords are excluded.</p><hr />
    <button onclick="toggleComments();return false;">Toggle Comments</button>
    <form id="config_form" method="POST">
EOT;
    }
 
    private function _doFooter() {
        return '<input id="config_submit" type="submit" value="Submit" /></form>';
    }
}
 
#------------------------------------------------------------------------------
# Configurator Class
#------------------------------------------------------------------------------

/*
 * Class to manipulate the LocalSettings.php file.
 */
class Configurator {
    var $data; # an array of $wg[keys] => [values]
    var $comments; # an array of $wg[keys] => [comments]
    var $extensions; # an array of extensions
    var $filter; # true = filter out bad values before writing LocalSettings.php
    
    /*
     * @param $filter True to filter out odd values before writing the LocalSettings.php file.
     */
    function __construct($filter=true) {
        $this->data = array();
        $this->comments = array();
        $this->extensions = array();
        $this->filter = $filter;
    }
 
    /*
     * Read the configuration files, LocalSettings then DefaultSettings for the
     * comments associated with the variables.
     * @return true on success, false on error
     */
    public function readConfig(&$error) {
        $ok = $this->_readLocalSettings($error1);
        if($ok === false) {
            return $ok;
        }
        $ok = $this->_readDefaultSettings($error2);
        if($ok === false) {
            return $ok;
        }
        $error = $error1 . $error2;
        return true;
    }
 
    /*
     * Backup the old LocalSettings file, then write a new one.
     * @return true on success, false on error
     */
    public function writeConfig(&$error) {
        $settings = $this->_prepareLocalSettings($error);
        if($settings === false) {
            return false;
        }
        $ok = $this->_verifyLocalSettings($settings, $error);
        if($ok === false) {
            return false;
        }
        $ok = $this->_writeLocalSettings($settings, $error);
        if($ok === false) {
            return false;
        }
        return true;
    }
 
    /*
     * Various getters and setters for keys, values, and comments.
     */
    public function getKeys() {
        return array_keys($this->data);
    }
    public function getValue($key) {
        return $this->data[$key];
    }
    public function setValue($key, $value) {
        if( $this->filter == false ) {
            $this->data[$key] = $value;
            return true;
        } else if( $this->filter == true && $this->_isGoodValue($value) ) {
            $this->data[$key] = $value;
            return true;
        }
        return false;
    }
    public function getComment($key) {
		if (isset($this->comments[$key])) {
			return $this->comments[$key];}
		else {
			return $key;
		}
    }
    public function setComment($key, $comment) {
        $this->comments[$key] = $comment;
    }
    public function setExtension($name, $extension) {
        return $this->extensions[$name] = $extension;
    }
    public function addExtensionSetting($name, $var, $val) {
        return $this->extensions[$name][$var] = $val;
    }
 
    /*
     * Get an array of values.
     * @param $keys If defined, only return the values corresponding to these keys.
     */
    public function getValues($keys=null) {
        if($keys == null) {
            return $this->data;
        }
        $partial_data = array();
        foreach($keys as $key) {
            $partial_data[$key] = $this->data[$key];
        }
        return $partial_data;
    }
    /*
     * Get an array of comments.
     * @param $keys If defined, only return the comments corresponding to these keys.
     */
    public function getComments($keys=null) {
        if($keys == null) {
            return $this->comments;
        }
        $partial_data = array();
        foreach($keys as $key) {
            $partial_data[] = $this->comments[$key];
        }
        return $partial_data;
    }
    /*
     * Get an array of extensions.
     * @param $names If defined, only return the comments extensions to these names.
     */
    public function getExtensions($names=null) {
        if($names == null) {
            return $this->extensions;
        }
        $partial_data = array();
        foreach($names as $name) {
            $partial_data[] = $this->extensions[$name];
        }
        return $partial_data;
    }
    /*
     * Set values.
     * @param $array An array of $key => $value mappings to set.
     */
    public function setValues($array) {
        if($this->filter) {
            $array = $this->_cleanValues($array);
        }
        foreach($array as $key => $value) {
            $this->data[$key] = $value;
        }
    }
    /*
     * Set extensions.
     * @param $array An array of $name => array($key => $value) mappings to set.
     */
    public function setExtensions($array) {
        foreach($array as $name => $extension) {
            if($this->filter) {
                $extension = $this->_cleanValues($extension);
            }
            $this->extensions[$name] = $extension;
        }
    }
 
    #--------------------------------------------------------------------------
    # Private Functions
    #--------------------------------------------------------------------------
    
    private function _readDefaultSettings(&$error) {
        $settings = @file_get_contents('includes/DefaultSettings.php');
        if($settings) {
            $this->_processComments($settings);
            $error .= "Read and processsed DefaultSettings.php\r\n";
            return true;
        }
        $error .= "Failed to read DefaultSettings.php\r\n";
        return false;
    }
 
    private function _readLocalSettings(&$error) {
        $settings = @file('LocalSettings.php', FILE_SKIP_EMPTY_LINES);
        if($settings) {
            $this->_processSettings($settings);
            $this->_processExtensions($settings);
            $error .= "Read and processsed LocalSettings.php\r\n";
            return true;
        }
        $error .= "Failed to read LocalSettings.php\r\n";
        return false;
    }
 
    private function _processSettings($settings) {
        foreach($settings as $setting) {
            if(preg_match('/^\s*\$wg([_\w]*)\s*=\s*([^;]*);/', $setting, $matches) != false) {
                if( strpos($matches[1], 'password') === false ) {
                    $this->setValue($matches[1], $matches[2]);
                }
            }
        }
    }
 
    private function _processExtensions($settings) {
        $cur_extension = "";
        foreach($settings as $setting) {
            if(preg_match('/require_once\(\s*[\"\'](.*?)[\"\']\s*\)/', $setting, $matches) != false) {
                if( strpos($matches[1], 'DefaultSettings') === false  ) {
                    $path = $matches[1];
                    $start = strrpos($path, '/')+1;
                    $end = strrpos($path, '.php');
                    $name = substr($path, $start, $end-$start);
                    $this->setExtension($name, array('path' => $path));
                    $cur_extension = $name;
                }
            } else if(preg_match('/^\s*(\$[_\w]*)\s*=\s*([^;]*);/', $setting, $matches) != false) {
                if( $cur_extension != '' && strpos($matches[1], 'password') === false  ) {
                    $this->addExtensionSetting($cur_extension, $matches[1], $matches[2]);
                }
            }
        }
    }
 
    private function _processComments($settings) {
        foreach($this->getKeys() as $key) {
            if(preg_match('#(/\*[^/]*\*/)[\s]*\$wg'.$key.'[\s]*=#', $settings, $matches) != false) {
                $this->setComment($key, $matches[1]);
            }
        }
    }
 
    private function _prepareLocalSettings(&$error) {
        if(!file_exists('LocalSettings.php')) {
            $error .= "Could not find LocalSettings.php.\r\n";
            return false;
        }
        $error .= "Backing up LocalSettings file.\r\n";
        if(@copy('LocalSettings.php', 'LocalSettings.old.php') === false) {
            $error .= "Failed to backup LocalSettings.php.  Please make sure the file is readable by the web server and that LocalSettings.old.php is writable by the web server.\r\n";
            return false;
        }
        $error .= "Reading LocalSettings file.\r\n";
        $settings = @file_get_contents('LocalSettings.php');
        if($settings === false) {
            $error .= "Failed to read LocalSettings.php.  Please make sure the file is readable by the web server.\r\n";
            return false;
        }
        foreach($this->getValues() as $key => $value) {
            $error .= " - Setting $key to $value\r\n";
            $settings = preg_replace('#^\$wg'.$key.'[\s]*=\s*[^;]*;$#m', '$wg'.$key.' = '.$value.';', $settings);
        }
        foreach($this->getExtensions() as $name => $extension) {
            foreach($extension as $key => $value) {
                $error .= " - Setting $name extension variable $key to $value\r\n";
                $settings = preg_replace('#^'."\\".$key.'[\s]*=\s*[^;]*;$#m', $key.' = '.$value.';', $settings);
            }
        }
        return $settings;
    }
 
    private function _verifyLocalSettings($settings, &$error) {
        if(substr($settings, 0, 5) == '<?php') {
            $settings = substr($settings, 5);
        }
        $IP = dirname( dirname( dirname( __FILE__ ) ) );
        define( 'MW_INSTALL_PATH', $IP );
                $ok = eval( $settings );
                if( $ok === false ) {
                        $error .= "Errors in generated LocalSettings file; " .
                                "most likely due to a bug in this extension " .
                                "Config file was: " .
                                "<pre>" .
                                htmlspecialchars( $settings ) .
                                "</pre>";
            return false;
                }
        $error .= "Verified new LocalSettings.\r\n";
        return true;
    }
 
    private function _writeLocalSettings($settings, &$error) {
        $error .= "Writing new LocalSettings.\r\n";
        if(@file_put_contents('LocalSettings.php', $settings) === false) {
            $error .= "Failed to write LocalSettings.php.  Please make sure the file is writable by the web server.\r\n";
            return false;
        }
        $error .= 'Done. Click <a href="./">here</a> see your changes.';
        return true;
    }
 
    private function _isGoodValue($value) {
        return ( is_string($value) && (
                    ( $value{0} == '"' && $value{strlen($value)-1} == '"' ) || # "string"
                    ( $value{0} == "'" && $value{strlen($value)-1} == "'" ) || # 'string'
                    ( preg_match('/^\$[\w_]+$/',$value) ) ||                   # $wgVariable
                    ( preg_match('/^[A-Z_]+$/',$value) ) ||                    # CONSTANT_VALUE
                    $value == 'true' || $value == 'false' || is_numeric($value) ) ) ||
               is_bool($value) ||
               is_int($value);
    }
 
    private function _cleanValues($values) {
        $clean_values = array();
        foreach($values as $key => $value) {
            $clean = preg_replace('#^\\\"(.*)\\\"$#', '"$1"', $value, -1, $count );
            if($count == 1) {
                $clean_values[$key] = $clean;
                continue;
            }
            $clean = preg_replace("#^\\\'(.*)\\\'$#", "'$1'", $value, -1, $count );
            if($count == 1) {
                $clean_values[$key] = $clean;
                continue;
            }
            if( $this->_isGoodValue($value) ) {
                $clean_values[$key] = $value;
                continue;
            }
        }
        return $clean_values;
    }
}

Configurator.i18n.php[edit]

<?php
$messages = array();
$messages['en'] = array( 
    'configurator-desc' => 'Reads the LocalSettings.php file and displays a form to modify configuration variables'
    'configurator-specialpage' => 'Configurator',
);

$messages['de'] = array(
    'configurator-desc' => 'Ermöglicht das formulargestützte Bearbeiten der Datei LocalSettings.php zur Anpassung der Konfiguration'
    'configurator-specialpage' => 'Konfigurator'
);