Publish MaintenanceShell v0.4.0: Rewrite
* Fix various security issues
- Use in_array on the directory scan instead of
sanitising $script and doing file_exists.
- Catch the output of the maintenance script with AJAX
and output it as a text node instead of echo'ing it raw
unescaped between <pre> and </pre>, which can cause html
escape issues with the output of the maintenance script.
* Phase out hacks in main php file, in favour of using regular
MediaWiki interfaces for:
- i18n messages
- user tokens
- user permissions
* Use FormSpecialPage to automatically take care of:
- Visual layout and creation of HTML <form> output
- User token handling
- Input validation
- User block status, rights and permissions checking
- Simple <select> drop down menu instead of <table>
of <a> links.
* Use ResourceLoader for delivery of javascript/css
* Implement basic parser for the cli arguments instead of
the manual regexing, also adding unit tests for many
edge cases that the old regex didn't support.
To run:
$ cd mw/core/tests/phpunit;
$ php phpunit.php ../../extensions/MaintenanceShell/;
* Config changes:
- Removed obsolete $wgMaintenanceShellLang
- Renamed $maintenance_path to $wgMaintenanceShellPath
* Cleaned up directory structure and file naming to
latest MediaWiki extension recommendations and coding
style conventions.
diff --git a/MaintenanceShell.alias.php b/MaintenanceShell.alias.php
old mode 100755
new mode 100644
diff --git a/MaintenanceShell.hooks.php b/MaintenanceShell.hooks.php
new file mode 100644
index 0000000..67085e7
--- /dev/null
+++ b/MaintenanceShell.hooks.php
@@ -0,0 +1,26 @@
+<?php
+/**
+ * Hooks for MaintenanceShell extension
+ *
+ * @file
+ * @ingroup Extensions
+ */
+
+class MaintenanceShellHooks {
+
+ /**
+ * Set default values.
+ * Set from this hook so that core Setup and LocaLSettings apply first.
+ */
+ public static function onSetup() {
+ global $wgMaintenanceShellPath, $IP;
+
+ if ( $wgMaintenanceShellPath === false ) {
+ $wgMaintenanceShellPath = $IP . '/maintenance';
+ }
+ }
+
+ public static function onUnitTestsList( Array &$files ) {
+ $files[] = __DIR__ . '/tests/MaintenanceShellArgumentsParserTest.php';
+ }
+}
diff --git a/MaintenanceShell.i18n.php b/MaintenanceShell.i18n.php
old mode 100755
new mode 100644
index 55f43a7..aaab66a
--- a/MaintenanceShell.i18n.php
+++ b/MaintenanceShell.i18n.php
@@ -4,27 +4,28 @@
/** English
* @author Andrew Fitzgerald
*/
-
$messages['en'] = array(
'maintenanceshell' => 'Maintenance Shell',
'maintenanceshell-desc' => 'Wiki interface for maintenance scripts',
- 'maintshell-pagename' => 'Special:MaintenanceShell',
- 'right-maintenanceshell' => 'Access the Maintenance Shell',
- 'maintshell-installfail' => 'MaintenanceShell did not detect the user right <b>maintenanceshell</b> assigned to any group.<br /><br /> Please see <a href="https://www.mediawiki.org/wiki/Extension:MaintenanceShell">the Extension:MaintenanceShell documentation</a> for more details.',
- 'maintshell-installfail2' =>'<b>MaintenanceShell is configured incorrectly.</b><br />The user right <b>maintenanceshell</b> must be assigned to a user group before the extension MaintanceShell is called in <b>LocalSettings.php</b>.<br /><br />Please see the <a href="https://www.mediawiki.org/wiki/Extension:MaintenanceShell">Extension:MaintenanceShell</a> documentation for more details.',
- 'maintshell-installfail3' => 'To install MaintenanceShell, put the following code on the <b>very last line</b> of LocalSettings.php:</br />require_once( "$IP/extensions/MaintenanceShell/MaintenanceShell.php" );',
- 'maintshell-return' => 'Return to the Maintenance Shell',
- 'maintshell-noexist' => "Script '$1.php' does not exist!",
- 'maintshell-warning' => 'Warning: Use these scripts with care. They are intended for administrators and other advanced users only.',
- 'maintshell-links' => '<ul style="padding-top: 1em;">
- <li><a class="external" href="https://www.mediawiki.org/wiki/Manual:Maintenance_scripts">Manual:Maintenance scripts</a></li>
- <li><a class="external" href="https://www.mediawiki.org/wiki/Extension:MaintenanceShell">MaintenanceShell Homepage</a></li>
-</ul>',
- 'maintshell-available' => 'Available maintenance scripts:',
- 'maintshell-scriptname' => 'Script name',
- 'maintshell-commandline' => 'Command line options',
- 'maintshell-runscript' => 'Run script',
+ # Special:ListGroupRights
+ 'right-maintenanceshell' => 'Execute maintenance scripts',
+ # FormSpecialPage "You don't have access to {$action}, for the following reason:"
+ 'action-maintenanceshell' => 'the maintenance shell',
+
+ # Special:MaintenanceShell
+ 'maintenanceshell-legend' => 'Maintenance Shell',
+ 'maintenanceshell-text' => "'''Warning:''' Use these scripts with care. They are intended for developers only.
+* [//www.mediawiki.org/wiki/Manual:Maintenance_scripts Manual:Maintenance scripts]
+* [//www.mediawiki.org/wiki/Extension:MaintenanceShell Extension:MaintenanceShell]
+",
+ 'maintenanceshell-return' => 'Return to [[{{#special:maintenanceshell}}]].',
+ 'maintenanceshell-error-scriptname' => 'Script not found',
+ 'maintenanceshell-error-rawsubmit' => 'For security reasons, this page requires javascript to be enabled.',
+ 'maintenanceshell-available' => 'Available maintenance scripts:',
+ 'maintenanceshell-field-scriptname' => 'Script name:',
+ 'maintenanceshell-field-args' => 'Command line options:',
+ 'maintenanceshell-field-submit' => 'Run script',
);
@@ -32,29 +33,22 @@
* @author kghbln
*/
$messages['de'] = array(
- 'maintenanceshell' => 'Wartungs-Shell',
- 'maintenanceshell-desc' => 'Ergänzt eine [[Special:MaintenanceShell|Spezialseite]] mit hilfreichen Links zu Wartungsskripten für die Systemadministration.',
- 'maintshell-pagename' => 'Special:Wartungs-Shell',
- 'right-maintenanceshell' => 'Wartungsskripte über die Wartungs-Shell ausführen.',
- 'maintshell-installfail' => 'Das Benutzergruppenrecht <b>maintenanceshell</b>, das für die Verwendung der Wartungs-Shell benötigt wird, wurde keiner Benutzergruppe zugeordnet.<br /><br />Weitere Hinweise hierzu gibt es in der <a href="https://www.mediawiki.org/wiki/Extension:MaintenanceShell">Dokumentation zur Wartungs-Shell</a>.',
- 'maintshell-installfail2' => '<b>Die Wartungs-Shell wurde fehlerhaft konfiguriert.</b><br />Das Benutzergruppenrecht <b>maintenanceshell</b> muss in der Datei <b>LocalSettings.php</b> einer Benutzergruppe zugewiesen werden, bevor dort die Softwareerweiterung Wartungs-Shell aufgerufen wird.<br /><br />Weitere Hinweise hierzu gibt es in der <a href="https://www.mediawiki.org/wiki/Extension:MaintenanceShell">Dokumentation zur Wartungs-Shell</a>.',
- 'maintshell-installfail3' => 'Um die Wartungs-Shell zu aktivieren, muss folgender Code in der <b>allerletzten Zeile</b> der Datei <b>LocalSettings.php</b> eingefügt werden:</br />require_once( "$IP/extensions/MaintenanceShell/MaintenanceShell.php" );',
- 'maintshell-return' => 'Rückkehr zur Wartungs-Shell',
- 'maintshell-noexist' => "Das Skript '$1.php' ist nicht vorhanden!",
- 'maintshell-warning' => '<b><u>Achtung:</u> Setze diese Skripte sorgfältig ein. Dies wird zudem nur Systemadministratoren und fortgeschrittenen Nutzern empfohlen.</b>',
- 'maintshell-links' => '<ul style="padding-top: 1em;">
- <li><a class="external" href="https://www.mediawiki.org/wiki/Manual:Maintenance_scripts">Nutzeranleitung für die Wartungs-Shell (englisch)</a></li>
- <li><a class="external" href="https://www.mediawiki.org/wiki/Extension:MaintenanceShell">Hompage zur Wartungs-Shell (englisch) </a></li>
-</ul><br />',
- 'maintshell-available' => '<br /><b>Verfügbare Wartungsskripte:</b>',
- 'maintshell-scriptname' => 'Name des Skrips',
- 'maintshell-commandline' => 'Zusätzliche Kommandos',
- 'maintshell-runscript' => 'Skript ausführen',
+ 'maintenanceshell' => 'Wartungs-Shell',
+ 'maintenanceshell-desc' => 'Ergänzt eine [[Special:MaintenanceShell|Spezialseite]] mit hilfreichen Links zu Wartungsskripten für die Systemadministration.',
+ 'maintenanceshell-pagename' => 'Special:Wartungs-Shell',
+ 'right-maintenanceshell' => 'Wartungsskripte über die Wartungs-Shell ausführen.',
+ 'maintenanceshell-warning' => "'''Achtung:''' Setze diese Skripte sorgfältig ein. Dies wird zudem nur Systemadministratoren und fortgeschrittenen Nutzern empfohlen.",
+ 'maintenanceshell-return' => 'Rückkehr zur Wartungs-Shell',
+ 'maintenanceshell-noexist' => 'Das Skript ist nicht vorhanden',
+ 'maintenanceshell-available' => 'Verfügbare Wartungsskripte:',
+ 'maintenanceshell-field-script' => 'Name des Skrips:',
+ 'maintenanceshell-commandline' => 'Zusätzliche Kommandos:',
+ 'maintenanceshell-field-args' => 'Skript ausführen',
);
/** German (formal address) (Deutsch (Sie-Form))
* @author kghbln
*/
$messages['de-formal'] = array(
- 'maintshell-warning' => '<b><u>Achtung:</u> Setzen Sie diese Skripte sorgfältig ein. Dies wird zudem nur Systemadministratoren und fortgeschrittenen Nutzern empfohlen.</b>',
+ 'maintenanceshell-warning' => "'''Achtung:''' Setzen Sie diese Skripte sorgfältig ein. Dies wird zudem nur Systemadministratoren und fortgeschrittenen Nutzern empfohlen.",
);
diff --git a/MaintenanceShell.php b/MaintenanceShell.php
old mode 100755
new mode 100644
index 23b8af9..5859797
--- a/MaintenanceShell.php
+++ b/MaintenanceShell.php
@@ -1,220 +1,64 @@
<?php
-/***********************************************************
- * Name: Maintenance Shell
- * Desc: Adds a special page to provide access to maintenance scripts
+/**
+ * Maintenance Shell extension.
+ * Adds a special page to provide access to maintenance scripts
*
- * Version: 0.3.2
- *
- * Author: Andrew Fitzgerald ([email protected])
- * Homepage: https://www.mediawiki.org/wiki/Extension:MaintenanceShell
- * http://www.swiftlytilting.com/
- *
- * License: GNU GPL
- *
- ***********************************************************
+ * @file
+ * @ingroup Extensions
+ * @copyright 2009-2013 Andrew Fitzgerald <[email protected]>
+ * @license GNU GPL
*/
-
-# Alert the user that this is not a valid entry point to MediaWiki if they try to access the special pages file directly.
-if (!defined('MEDIAWIKI')) {
- echo wfMsg_MS('maintshell-installfail3');
- exit( 1 );
-}
-
$wgExtensionCredits['specialpage'][] = array(
'name' => 'MaintenanceShell',
- 'author' => '[http://swiftlytilting.com Andrew Fitzgerald]',
+ 'author' => array(
+ '[http://swiftlytilting.com Andrew Fitzgerald]',
+ 'Timo Tijhof',
+ ),
'url' => 'https://www.mediawiki.org/wiki/Extension:MaintenanceShell',
'description' => 'Adds a special page to provide access to maintenance scripts.',
'descriptionmsg' => 'maintenanceshell-desc',
- 'version' => '0.3.2',
+ 'version' => '0.4.0',
);
-$dir = dirname(__FILE__) . '/';
-$wgAutoloadClasses['MaintenanceShell'] = $dir . 'MaintenanceShell_body.php'; # Tell MediaWiki to load the extension body.
-$wgExtensionMessagesFiles['MaintenanceShell'] = $dir . 'MaintenanceShell.i18n.php';
-$wgExtensionAliasesFiles['MaintenanceShell'] = $dir . 'MaintenanceShell.alias.php';
-$wgSpecialPages['MaintenanceShell'] = 'MaintenanceShell'; # Let MediaWiki know about your new special page.
+/* Setup */
-// Special page group for MW 1.13+
+$dir = dirname( __FILE__ );
+
+// Register files
+$wgAutoloadClasses['MaintenanceShellHooks'] = $dir . '/MaintenanceShell.hooks.php';
+$wgAutoloadClasses['SpecialMaintenanceShell'] = $dir . '/includes/SpecialMaintenanceShell.php';
+$wgAutoloadClasses['MaintenanceShellArgumentsParser'] = $dir . '/includes/MaintenanceShellArgumentsParser.php';
+$wgExtensionMessagesFiles['MaintenanceShell'] = $dir . '/MaintenanceShell.i18n.php';
+$wgExtensionMessagesFiles['MaintenanceShellAlias'] = $dir . '/MaintenanceShell.alias.php';
+
+// Register special pages
+$wgSpecialPages['MaintenanceShell'] = 'SpecialMaintenanceShell';
$wgSpecialPageGroups['MaintenanceShell'] = 'wiki';
-// New user right - required to access Special:MaintenanceShell
+// Register user rights
$wgAvailableRights[] = 'maintenanceshell';
-// check that there is a group assigned to maintenanceshell
-$wgMaintShellPermissions = 0;
-foreach ($wgGroupPermissions as $v)
-{ $wgMaintShellPermissions += array_key_exists('maintenanceshell', $v) ? 1 : 0;
-}
+// Register hooks
+$wgExtensionFunctions[] = 'MaintenanceShellHooks::onSetup';
+$wgHooks['UnitTestsList'][] = 'MaintenanceShellHooks::onUnitTestsList';
-// load language file
-require_once($IP . '/extensions/MaintenanceShell/MaintenanceShell.i18n.php');
+// Register modules
+$wgResourceModules['ext.maintenanceShell'] = array(
+ 'scripts' => 'resources/ext.maintenanceShell.js',
+ 'styles' => 'resources/ext.maintenanceShell.css',
+ 'localBasePath' => $dir,
+ 'remoteExtPath' => 'MaintenanceShell',
+ 'dependencies' => 'jquery.spinner'
+);
-// check for custom settings
+// Not granted to anyone by default.
+// To grant to "developer" group, use:
+// $wgGroupPermissions['developer']['maintenanceshell'] = true;
+// Or create a new user group, use:
+// $wgGroupPermissions['maintainer']['maintenanceshell'] = true;
-$wgMaintenanceShellLang = isset($wgLanguageCode) ? $wgLanguageCode : 'en';
-$maintenance_path = isset( $wgMaintenancePath) ? $wgMaintenancePath : $IP . '/maintenance/';
+/* Configuration */
-
-// catch operations before wiki does anything, so we can act like we're coming from the command line
-// we use $_POST because $wgRequest isn't initialized, not really needed anyways
-
-if (!array_key_exists('commandline', $_POST)
- && !array_key_exists('token', $_POST) // make sure it was really from the user and not a fake
-) {
- return; // bail if we're not coming from the command line form
-}
-else {
- // define some functions we'll need. could load then from MW but it's faster to write them
- // than figure out how to cleanly load them from MW
- // at least now they don't load unless we're actually using the maintenance shell
-
- function wfMsg_MS($key) {
- global $wgMaintenanceShellLang, $messages, $wgRequest;
- $args = func_get_args();
- array_shift( $args );
-
-
- if (array_key_exists($wgMaintenanceShellLang, $messages)
- && array_key_exists($key, $messages[$wgMaintenanceShellLang])
- ) {
- $ret = $messages[$wgMaintenanceShellLang][$key];
- }
- elseif (array_key_exists('en', $messages)) {
- $ret = $messages['en'][$key];
- } else {
- $ret = '[ERROR: string not found for this language]';
- }
-
- return wfMsgReplaceArgs_MS($ret, $args);
- }
-
- function wfMsgReplaceArgs_MS( $message, $args ) {
- # Fix windows line-endings
- # Some messages are split with explode("\n", $msg)
- $message = str_replace( "\r", '', $message );
-
- // Replace arguments
- if ( count( $args ) ) {
- if ( is_array( $args[0] ) ) {
- $args = array_values( $args[0] );
- }
- $replacementKeys = array();
- foreach( $args as $n => $param ) {
- $replacementKeys['$' . ($n + 1)] = $param;
- }
- $message = strtr( $message, $replacementKeys );
- }
-
- return $message;
- }
-}
-
-
-$maintshell_pagename = wfMsg_MS( 'maintshell-pagename');
-
-if (array_key_exists('title', $_REQUEST)
- && ($_REQUEST['title'] == $maintshell_pagename)
-) {
- // first lets check to see if we're installed correctly
- if ($wgMaintShellPermissions === 0) {
- echo wfMsg_MS('maintshell-installfail');
- exit;
- }
-
- // set up system to verify user permissions
- require_once('./includes/Setup.php');
-
- if ( $wgUser->isBlocked()
- || wfReadOnly()
- || !$wgUser->isAllowed( 'maintenanceshell' )
- || $_POST['token'] !== $wgUser->getToken()
- ) {
-
- $head_redirect = 'Location: '
- . ($_SERVER['SERVER_PORT'] == "443" ? "https" : "http")
- . "://"
- . $_SERVER['SERVER_NAME']
- . ($_SERVER['SERVER_PORT'] == "80" ? "" : $_SERVER['SERVER_PORT'])
- . $_SERVER['SCRIPT_NAME']
- . '?title='
- . $maintshell_pagename;
-
- header($head_redirect);
- return;
- }
-
- echo '<a href="' . $_SERVER['SCRIPT_NAME'] . '?title=' . $maintshell_pagename . '">'. wfMsg_MS('maintshell-return') . '</a>';
-
- echo '<hr />';
- $script = trim($_POST['script']);
-
- if ($script) {
- $script = str_replace(array('.', '/'), '', $script);
- $scriptHTML = MaintenanceShell::HTMLescape($script);
-
- $commandline = (array_key_exists('commandline', $_POST) ? trim($_POST['commandline']) : '');
- $commandlineHTML = MaintenanceShell::HTMLescape($commandline);
-
-
- if (file_exists(trim($maintenance_path . $script . '.php'))) {
- // display shell frame
- echo '<div style="border: 5px solid gray; background-color: black; color: green; padding: 1em;">';
- echo '<pre style="white-space: -moz-pre-wrap; white-space: -pre-wrap; white-space: -o-pre-wrap; white-space: pre-wrap; word-wrap: break-word;"><b>';
- chdir(isset($wgMaintenanceShellDir) ? $wgMaintenanceShellDir : $maintenance_path);
- echo getcwd() . '$ php '. (isset($wgMaintenanceShellDir) ? $maintenance_path : '') .$script . '.php ' . $commandlineHTML . "</b>\n\n";
-
- // make commandline.inc think we're coming from the command line
- // Unset request method and build $argv
- unset($_SERVER['REQUEST_METHOD']);
-
- $commandline = str_ireplace('{{root}}', $_SERVER['DOCUMENT_ROOT'], $commandline);
-
- // handle quote marks in command line
-
- preg_match_all('%\"(.*)\"%Us', $commandline, $matches);
-
- foreach ($matches[1] as $n => $v) {
- $commandline = str_replace($v, str_replace(' ', "\n", $v), $temp_command );
- }
-
- $temp_command = preg_replace('% +%', ' ', $commandline);
- $argv = explode(' ',basename($_SERVER["PHP_SELF"]) . ' ' . $commandline);
-
- $search = array( '"', "\n");
- $replace = array('', ' ');
- foreach($argv as $n => $v) {
- $argv[$n] = str_replace($search, $replace, $v);
- }
-
- $argc = count($argv);
-
- // catch exit calls from within the called script
- function exit_callback($param = false) {
- echo '</pre></div>';
- exit;
- }
-
- register_shutdown_function('exit_callback');
-
- // needed for MW 1.15
- // apparently sometimes 2 ob_starts are needed?! not sure why..
- // it's best when the output isn't buffered at all, then in firefox it displays as the form
- // exports it
- ob_start(); ob_start();
-
- // call the script
- include_once($maintenance_path . $script . '.php');
-
- // exit, if the script doesn't explictly exit. Will call the exit callback function
- exit;
-
-
- } else {
- echo wfMsg_MS('maintshell-noexist', $maintenance_path .$scriptHTML);
- }
- }
- exit;
-}
+$wgMaintenanceShellPath = false;
diff --git a/MaintenanceShell_body.php b/MaintenanceShell_body.php
deleted file mode 100755
index e7d631d..0000000
--- a/MaintenanceShell_body.php
+++ /dev/null
@@ -1,90 +0,0 @@
-<?php
-
-
-/**
- * Special page interface for Maintenance Shell
- */
-class MaintenanceShell extends SpecialPage {
- function __construct() {
- parent::__construct( 'MaintenanceShell', 'maintenanceshell' );
- wfLoadExtensionMessages('MaintenanceShell');
- }
-
- function execute( $par ) {
- global $wgRequest, $wgOut, $IP, $wgUser, $wgMaintShellPermissions;
-
- # If user is blocked, s/he doesn't need to access this page
- if ( $wgUser->isBlocked() ) {
- $wgOut->blockedPage();
- return;
- }
-
- # Show a message if the database is in read-only mode
- if ( wfReadOnly() ) {
- $wgOut->readOnlyPage();
- return;
- }
-
- # If the user doesn't have the required 'maintenanceshell' permission, display an error
- if( !$wgUser->isAllowed( 'maintenanceshell' )) {
- $wgOut->permissionRequired( 'maintenanceshell' );
- return;
- }
-
- $this->setHeaders();
-
- $token = $wgUser->getToken();
-
-
- if ($wgMaintShellPermissions === 0) {
- $wgOut->addHTML(wfMsg('maintshell-installfail2'));
- return;
- }
-
- $scriptNameHTML = self::HTMLescape($wgRequest->getText('script'));
- $commandlineHTML = self::HTMLescape($wgRequest->getText('commandline'));
-
- $maintenance_path = $IP.'/maintenance/';
- $str = '<b>' . wfMsg('maintshell-warning'). '</b>' .
- "<br /><br /><form action='$_SERVER[SCRIPT_NAME]' method='post'>" .
- '<input name="title" value="Special:MaintenanceShell" type="hidden">' .
- "<table><tr><td><b>". wfMsg('maintshell-scriptname') ."</b>:</td><td> <input name='script' value='" . $scriptNameHTML . "'/>.php</td></tr>" .
- '<tr><td><b>'. wfMsg('maintshell-commandline') .'</b>:</td><td> <input name="commandline" size="70" value="' . $commandlineHTML .'"/></td></tr>' .
- '</table><br /><input name="submit" type="submit" value="' . wfMsg('maintshell-runscript') . '"/>'
- .'<input type="hidden" name="token" value="' .$token . '" /> </form>'. wfMsg('maintshell-links') .'<hr />';
-
-
- $wgOut->addHTML($str);
-
- $files = array_diff( scandir($maintenance_path ), array( ".", ".." ) );
- if (count($files)) {
- $wgOut->addHTML('<b>'. wfMsg('maintshell-available') .'</b><pre><table cellpadding=5><tr>');
- $count = 0;
- foreach ($files as $v) {
- if (($arr = pathinfo($v)) && array_key_exists('extension', $arr) && ($arr['extension'] == 'php') && (strpos($v, '.inc') === false)) {
- $v = str_replace('.php','',$v);
- $wgOut->addHTML( "<td><a href='$_SERVER[SCRIPT_NAME]?title=". wfMsg('maintshell-pagename') ."&script=$v'>$v</a></td>");
- $count++;
- if ($count == 4) {
- $wgOut->addHTML( '</tr><tr>');
- $count = 0;
- }
- }
- }
- $wgOut->addHTML('</tr></table>');
-
- }
-
- $wgOut->addHTML('</pre>');
-
- }
-
- /**
- * aggressively escape everything besides letters and numbers
- */
- static function HTMLescape($string) {
- return preg_replace_callback('%([^A-Za-z0-9 ])%',
- create_function('$matches', 'return "&#" . ord($matches[1]) .";" ;'),
- $string);
- }
-}
diff --git a/includes/AlteredSelectedMaintenanceScript.php b/includes/AlteredSelectedMaintenanceScript.php
new file mode 100644
index 0000000..844857a
--- /dev/null
+++ b/includes/AlteredSelectedMaintenanceScript.php
@@ -0,0 +1,40 @@
+<?php
+
+class AlteredSelectedMaintenanceScript extends SelectedMaintenanceScript {
+
+ /**
+ * Copied from core Maintenance::setup.
+ *
+ * Overriding it to exclude these checks:
+ *
+ * - `ini_get( 'register_argc_argv' )`
+ * > Disabled by default in many PHP configurations for Apache servers.
+ *
+ * - `$_SERVER['REQUEST_METHOD']`
+ * > Because we're not actually on the command line. This can be simulated
+ * using unset(), but since we're overriding this anyway, might as well remove it.
+ *
+ * - `define( 'MEDIAWIKI', true )`
+ * > This would throw an E_NOTICE, since we're already in MediaWiki request context.
+ */
+ public function setup() {
+ global $wgCommandLineMode, $wgRequestTime;
+
+ if ( ini_get( 'display_errors' ) ) {
+ ini_set( 'display_errors', 'stderr' );
+ }
+
+ $this->loadParamsAndArgs();
+ $this->maybeHelp();
+ $this->adjustMemoryLimit();
+
+ ini_set( 'max_execution_time', 0 );
+
+ $wgRequestTime = microtime( true );
+ $wgCommandLineMode = true;
+
+ @ob_end_flush();
+
+ $this->validateParamsAndArgs();
+ }
+}
diff --git a/includes/MaintenanceShellArgumentsParser.php b/includes/MaintenanceShellArgumentsParser.php
new file mode 100644
index 0000000..a4e510f
--- /dev/null
+++ b/includes/MaintenanceShellArgumentsParser.php
@@ -0,0 +1,156 @@
+<?php
+
+class MaintenanceShellArgumentsParser {
+ private $raw;
+ private $arguments;
+
+ public function __construct( $str ) {
+ $this->raw = strval( $str );
+ }
+
+ /**
+ * Get the supposed-be value of $argv given a string that
+ * would be written literally on the command line.
+ * This basically simulates the bash intepreter
+ * and (where relevant) PHPs specific perception of it
+ * as exposed in $arv. See also <http://php.net/argv/>.
+ *
+ * @return Array
+ */
+ public function getArgv() {
+ return $this->parseArguments();
+ }
+
+ public function getArgc() {
+ return count( $this->parseArguments() );
+ }
+
+ private function parseArguments() {
+ if ( $this->arguments !== null ) {
+ return $this->arguments;
+ }
+
+ // Trim leading whitespace (not after backslash escape or in quotes)
+ $chars = ltrim( $this->raw );
+
+ // Stable results
+ $values = array();
+
+ // Continuous state
+ $value = false;
+ $inSingleWrap = false;
+ $inDoubleWrap = false;
+ $escapeNext = false;
+
+ // Helper variables
+ $inEscape = false;
+ $inWrap = false;
+
+ $chars_len = strlen( $chars );
+ for ( $i = 0; $i < $chars_len; $i++ ) {
+ $char = $chars[$i];
+
+ $inEscape = $escapeNext;
+ $escapeNext = false;
+ $inWrap = $inSingleWrap || $inDoubleWrap;
+
+ if ( $char === '\\' ) {
+ // Plain or wrapped
+ if ( !$inWrap ) {
+ if ( !$inEscape ) {
+ $escapeNext = true;
+ // Beginning of a plain value of which the first character
+ // (next iteration) will be escaped.
+ if ( $value === false ) {
+ $value = '';
+ }
+ } else {
+ if ( $value === false ) {
+ // XXX: Is it even possible to be in escape with value being false?
+ $value = $char;
+ } else {
+ $value .= $char;
+ }
+ }
+ // Wrapped
+ } else {
+ if ( $value === false ) {
+ $value = $char;
+ } else {
+ $value .= $char;
+ }
+ }
+
+ } elseif ( $char === '"' ) {
+ // Plain: Begin wrap
+ if ( !$inWrap ) {
+ $inDoubleWrap = true;
+
+ // Double wrap: End of wrap
+ } elseif ( $inDoubleWrap ) {
+ $inDoubleWrap = false;
+
+ // Single wrap: Literal value
+ } elseif ( $inSingleWrap ) {
+ $value .= $char;
+ }
+ } elseif ( $char === "'" ) {
+ // Plain: Begin wrap
+ if ( !$inWrap ) {
+ $inSingleWrap = true;
+
+ // Single wrap: End of wrap
+ } elseif ( $inSingleWrap ) {
+ $inSingleWrap = false;
+
+ // Double wrap: Literal value
+ } elseif ( $inDoubleWrap ) {
+ $value .= $char;
+ }
+ } elseif ( $char === ' ' ) {
+ // Plain
+ if ( !$inWrap ) {
+ if ( !$inEscape ) {
+ if ( $value !== false ) {
+ array_push( $values, $value );
+ $value = false;
+ }
+ // else: do nothing.
+ // We're in plain context with no escape and no value yet.
+ // Additional separator spaces are just ignored.
+ } else {
+ if ( $value === false ) {
+ $value = $char;
+ } else {
+ $value .= $char;
+ }
+ }
+ // Wrapped
+ } else {
+ // XXX: Should escaping be considered here?
+ if ( $value === false ) {
+ $value = $char;
+ } else {
+ $value .= $char;
+ }
+ }
+ } else {
+ if ( $value === false ) {
+ $value = $char;
+ } else {
+ $value .= $char;
+ }
+ }
+ }
+
+ // If there was no trailing space, make sure we push in the
+ // last value as well.
+ if ( $value !== false ) {
+ array_push( $values, $value );
+ }
+
+ $this->arguments = $values;
+ return $this->arguments;
+ }
+
+}
diff --git a/includes/SpecialMaintenanceShell.php b/includes/SpecialMaintenanceShell.php
new file mode 100644
index 0000000..9f16957
--- /dev/null
+++ b/includes/SpecialMaintenanceShell.php
@@ -0,0 +1,170 @@
+<?php
+
+/**
+ * Special page interface for Maintenance Shell.
+ *
+ * TODO: FormSpecialPage implements a token protection.
+ * That's good but ideally we'd use a custom token (Like FormAction allows).
+ */
+class SpecialMaintenanceShell extends FormSpecialPage {
+ private $maintshellOutput = '';
+
+ public function __construct() {
+ parent::__construct( 'MaintenanceShell', 'maintenanceshell' );
+ $out = $this->getOutput();
+ $out->addModules( 'ext.maintenanceShell' );
+ }
+
+ public function execute( $par ) {
+ $out = $this->getOutput();
+ $out->addHTML( '<div class="mw-sp-maintenanceShell">' );
+ parent::execute( $par );
+ $out->addHTML( '</div>' );
+ }
+
+ /**
+ * @return Array
+ */
+ protected function getFormFields() {
+ global $wgMaintenanceShellPath;
+
+ $files = array_map( 'basename', glob( $wgMaintenanceShellPath . '/*.php' ) );
+ $options = array_combine( $files, $files );
+
+ // Blacklist
+ unset( $options['Maintenance.php'] );
+ unset( $options['doMaintenance.php'] );
+
+ // Add empty value to top of the list
+ // Note: HTMLForm automatically considers '' to be an invalid
+ // value if required:true is set.
+ $options[''] = '';
+
+ ksort( $options );
+
+ return array(
+ 'Script' => array(
+ 'type' => 'select',
+ 'label-message' => 'maintenanceshell-field-scriptname',
+ 'tabindex' => '1',
+ 'size' => '45',
+ 'required' => true,
+ 'options' => $options,
+ 'default' => '',
+ ),
+ 'Arguments' => array(
+ 'type' => 'text',
+ 'label-message' => 'maintenanceshell-field-args',
+ 'tabindex' => '1',
+ 'size' => '70',
+ )
+ );
+ }
+
+ protected function alterForm( HTMLForm $form ) {
+ $form->setSubmitTextMsg( 'maintenanceshell-field-submit' );
+ /* Control field to prevent accidential submissions
+ * in browsers without javascript support.
+ *
+ * We require the javascript module to have successfully
+ * done its work, as otherwise there is a potential of the
+ * form being submitted directly which in IE can cause
+ * the output to be interpretted as html.
+ */
+ $form->addHiddenField( 'controlfield', '0', array( 'class' => 'mw-sp-maintenanceShell-controlfield' ) );
+ }
+
+ /**
+ * After form input is validated, we run the maintenance script.
+ * This is before the page output is generated.
+ *
+ * @param Array $data
+ * @return Bool|Array true on success, array of errors on failure
+ */
+ public function onSubmit( Array $data ) {
+ global $wgMaintenanceShellPath;
+
+ $filePath = $wgMaintenanceShellPath . '/' . $data['Script'];
+ // Verify one more time, in case of race condition or forged submission.
+ if ( !is_file( $filePath ) ) {
+ return array( 'maintenanceshell-error-scriptname' );
+ }
+
+ if ( $this->getRequest()->getVal('controlfield') !== '1' ) {
+ return array( 'maintenanceshell-error-rawsubmit' );
+ }
+
+ $this->mainshellExec( $filePath, $data['Arguments'] );
+
+ #$this->maintshellPrompt = getcwd() . '$ php ' . $data['Script'] . ' ' . $data['Arguments'];
+ #$this->maintshellOutput = $this->mainshellExec( $filePath, $data['Arguments'] );
+ #return true;
+ }
+
+
+ public function onSuccess() {
+ // Never happens because onSubmit callback to mainshellExec
+ // will exit before. We handle this in the front-end instead
+ // because maintenance scripts are hard to catch the output of.
+ // We instead let the script output and exit, but we'lll make the
+ // request itself go through AJAX, so the javascript module can
+ // safely catch the output and present it to the user.
+ }
+
+ /**
+ * Execute the script and echo output
+ * to the browser as plain text.
+ */
+ private function mainshellExec( $filePath, $arguments ) {
+ global $wgMaintenanceShellPath, $IP, $wgTitle;
+
+ // Replace placeholders
+ $arguments = str_ireplace( '{{root}}', $_SERVER['DOCUMENT_ROOT'], $arguments );
+
+ // Simulate working directory
+ chdir( $wgMaintenanceShellPath );
+
+ // Build $argv and $argc
+ $arguments = basename( $filePath ) . ' ' . $arguments;
+ $parser = new MaintenanceShellArgumentsParser( $arguments );
+ $GLOBALS['argv'] = $parser->getArgv();
+ $GLOBALS['argc'] = $parser->getArgc();
+
+ // Output plain text header to avoid output being misintepreted as html
+ header( 'Content-Type: text/plain; charset=utf-8' );
+
+ require_once( $filePath );
+
+ // We could eval the entire extension, but lets only eval the part
+ // we need, namely the variable class extension.
+ eval( "class SelectedMaintenanceScript extends $maintClass {}" );
+
+ // We need some alterations to the maintenance class to make it
+ // work in a web request context.
+ require_once( __DIR__ . '/AlteredSelectedMaintenanceScript.php' );
+
+ // "DoMaintenance" (loaded from the maintenance script file) will have refused
+ // to run the script, as Maintenance::shouldExecute returns false because
+ // we're not on the command line and not in the global context.
+ // So, reverse-engineer its logic here, using our altered version of the class.
+ if ( !Maintenance::shouldExecute() ) {
+ $maintenance = new AlteredSelectedMaintenanceScript();
+ $maintenance->setup();
+ // Already handled by our current request context:
+ # require( $maintenance->loadSettings() );
+ # AdminSettings.php
+ # Setup.php
+ $maintenance->finalSetup();
+ $wgTitle = null;
+ try {
+ $maintenance->execute();
+ $maintenance->globals();
+ } catch ( MWException $mwe ) {
+ echo $mwe->getText();
+ }
+ }
+
+ // If the script doesn't explictly exit, we'll exit anyway
+ exit;
+ }
+}
diff --git a/resources/ext.maintenanceShell.css b/resources/ext.maintenanceShell.css
new file mode 100644
index 0000000..287c195
--- /dev/null
+++ b/resources/ext.maintenanceShell.css
@@ -0,0 +1,13 @@
+.mw-sp-maintenanceShell-shell {
+ padding: 1em;
+ background: #000;
+ color: #f9f9f9;
+ font: normal 12px/1.4 Menlo, Monaco, 'Courier', monospace;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+.mw-sp-maintenanceShell-shell-error b {
+ color: red;
+ font-weight: bold;
+}
diff --git a/resources/ext.maintenanceShell.js b/resources/ext.maintenanceShell.js
new file mode 100644
index 0000000..777c1a0
--- /dev/null
+++ b/resources/ext.maintenanceShell.js
@@ -0,0 +1,91 @@
+( function ( $ ) {
+
+ function err( $shell, errorMsg ) {
+ $shell.append(
+ $('<p><b>></b> </p>' )
+ .addClass( 'mw-sp-maintenanceShell-shell-error' )
+ .append( document.createTextNode( errorMsg ) )
+ );
+ }
+
+ function init( $pageWrap ) {
+ $pageWrap
+ .find( '.mw-sp-maintenanceShell-controlfield' )
+ .val( '1' )
+ .end()
+ .find( 'select[name="wpScript"]' )
+ .prop( 'required', true )
+ .end()
+ .find( 'form' )
+ .on( 'submit', function ( e ) {
+ // We're going the ajax way!
+ e.preventDefault();
+
+ // Get data
+ var $spinner,
+ $tmpWrap,
+ $form = $( this ),
+ postData = $form.serialize(),
+ $inputs = $form.find( ':input' ), // select, input, textarea, button
+ $wrap = $form.closest( '.mw-sp-maintenanceShell' ),
+ $shell = $wrap.find( '.mw-sp-maintenanceShell-shell' ).empty();
+ if ( !$shell.length ) {
+ $shell = $( '<div>' ).addClass( 'mw-sp-maintenanceShell-shell' ).appendTo( $wrap );
+ }
+ // Remove stale errors from $tmpWrap
+ $wrap.find( '.error' ).remove();
+
+ // Disable form and show spinner
+ $inputs.prop( 'disabled', true ); // Could store original state, but we don't have disabled fields..
+ $spinner = $.createSpinner({ size: 'large', type: 'block' });
+ $form.find( 'fieldset' ).eq( 0 ).append( $spinner );
+
+ // So far the only known restriction/limitation: --wiki doens't work
+ // (which could be, but is purposely not supported. If you run a
+ // wiki farm, you're either expected to have command line access or
+ // just access this SpecialPage from the correct wiki)
+ if ( postData.indexOf( '--wiki' ) !== -1 ) {
+ err( $shell, 'Usage of the --wiki option is not allowed by MaintenanceShell.' );
+ $inputs.prop( 'disabled', false );
+ $spinner.remove();
+ return;
+ }
+
+ // Submission
+ $.ajax( {
+ type: $form.attr( 'method' ) || 'POST',
+ url: $form.attr( 'action' ) || '',
+ data: postData,
+ cache: false,
+ dataType: 'text'
+ } )
+ .done( function ( data ) {
+ // In case of error, the server will respond with a full html page.
+ // Extract the SpecialPage wrapper, and replace our current one to show the error.
+ // Then re-run our init handlers on the wrapper.
+ if ( data.indexOf( 'mw-sp-maintenanceShell' ) !== -1 || data.indexOf( 'controlfield' ) !== -1 ) {
+ $tmpWrap = $(data).find( '.mw-sp-maintenanceShell' ).eq( 0 );
+ $wrap.replaceWith( $tmpWrap );
+ init( $tmpWrap );
+ } else {
+ $shell.text( data );
+ }
+ } )
+ .fail( function ( jqXHR, textStatus, errorThrown ) {
+ err( $shell, errorThrown || 'Request failed.' );
+ } )
+ .complete( function () {
+ // Re-enable form and hide spinner
+ $inputs.prop( 'disabled', false );
+ $spinner.remove();
+ } );
+
+ } );
+ }
+
+ // Kick it off
+ $( function () {
+ init( $( '.mw-sp-maintenanceShell' ) );
+ } );
+
+}( jQuery ) );
diff --git a/tests/MaintenanceShellArgumentsParserTest.php b/tests/MaintenanceShellArgumentsParserTest.php
new file mode 100644
index 0000000..faf6722
--- /dev/null
+++ b/tests/MaintenanceShellArgumentsParserTest.php
@@ -0,0 +1,117 @@
+<?php
+
+class MaintenanceShellArgumentsParserTest extends MediaWikiTestCase {
+
+ public static function provideArguments() {
+ return array(
+ array(
+ 'foo',
+ array( 'foo' ),
+ 'One plain argument'
+ ),
+ array(
+ 'foo bar',
+ array( 'foo', 'bar' ),
+ 'Two plain arguments'
+ ),
+ array(
+ 'foo bar baz',
+ array( 'foo', 'bar', 'baz' ),
+ 'Three plain arguments'
+ ),
+ array(
+ 'foo\ bar baz\ quux',
+ array( 'foo bar', 'baz quux' ),
+ 'Plain argument with escaping'
+ ),
+ array(
+ '\ foo \ bar \ baz',
+ array( ' foo', ' bar', ' baz' ),
+ 'Plain argument with escaping at start of value'
+ ),
+ array(
+ ' \ foo \ bar \ baz ',
+ array( ' foo', ' bar', ' baz' ),
+ 'Plain arguments with escaping and extra spacing'
+ ),
+ array(
+ "foo\nbar",
+ array( "foo\nbar" ),
+ 'Plain argument with line break'
+ ),
+ array(
+ '"foo" "bar" "baz"',
+ array( 'foo', 'bar', 'baz' ),
+ 'Double quoted arguments'
+ ),
+ array(
+ '" foo " " bar " " baz "',
+ array( ' foo ', ' bar ', ' baz ' ),
+ 'Double quoted arguments with spacing'
+ ),
+ array(
+ '" foo\ " " bar\ " " baz\ "',
+ array( ' foo\ ', ' bar\ ', ' baz\ ' ),
+ 'Double quoted arguments with spacing and backslashes'
+ ),
+ array(
+ // one more backslash for each one because of PHP's escaping.
+ // Actual input: "foo\\" "\bar\"
+ '"foo\\\\" "\bar\"',
+ array( 'foo\\\\', '\bar\\' ),
+ 'Double quoted arguments with multple backslashes'
+ ),
+ array(
+ "\"foo\nbar\"",
+ array( "foo\nbar" ),
+ 'Double quoted argument with line break'
+ ),
+ array(
+ "'foo' 'bar' 'baz'",
+ array( 'foo', 'bar', 'baz' ),
+ 'Single quoted arguments'
+ ),
+ array(
+ "' foo ' ' bar ' ' baz '",
+ array( ' foo ', ' bar ', ' baz ' ),
+ 'Single quoted arguments with spaces'
+ ),
+ array(
+ "'foo\nbar'",
+ array( "foo\nbar" ),
+ 'Single quoted argument with line break'
+ ),
+ array(
+ '"foo""bar"\'quux\'',
+ array( 'foobarquux' ),
+ 'Quotes only provide wrapping context, they are not value seperators'
+ ),
+ array(
+ ' path/to/something\ in/this\ directory.png --baz="quux" -abc="c++" -d e -- -f "' . "line\nbreak". '" ',
+ array(
+ 'path/to/something in/this directory.png',
+ '--baz=quux',
+ '-abc=c++',
+ '-d',
+ 'e',
+ '--',
+ '-f',
+ "line\nbreak",
+ ),
+ 'Everything thrown together in one giant example (using double quotes)'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider provideArguments
+ */
+ public function testParseArguments( $input, Array $expected, $message ) {
+ $parser = new MaintenanceShellArgumentsParser( $input );
+ $this->assertEquals(
+ $expected,
+ $parser->getArgv(),
+ $message
+ );
+ }
+}