Revision: 43669
Author: tstarling
Date: 2008-11-18 11:36:09 +0000 (Tue, 18 Nov 2008)
Log Message:
-----------
Backported r43621, r43622, r43623, r43624, r43625, r43627, r43660, r43661.
Needs testing.
Modified Paths:
--------------
branches/REL1_13/phase3/RELEASE-NOTES
branches/REL1_13/phase3/img_auth.php
branches/REL1_13/phase3/includes/DefaultSettings.php
branches/REL1_13/phase3/includes/Exception.php
branches/REL1_13/phase3/includes/StreamFile.php
branches/REL1_13/phase3/includes/Title.php
branches/REL1_13/phase3/includes/XmlTypeCheck.php
branches/REL1_13/phase3/includes/specials/SpecialImport.php
branches/REL1_13/phase3/includes/specials/SpecialUndelete.php
branches/REL1_13/phase3/includes/specials/SpecialUpload.php
branches/REL1_13/phase3/languages/messages/MessagesEn.php
branches/REL1_13/phase3/profileinfo.php
Modified: branches/REL1_13/phase3/RELEASE-NOTES
===================================================================
--- branches/REL1_13/phase3/RELEASE-NOTES 2008-11-18 10:51:44 UTC (rev
43668)
+++ branches/REL1_13/phase3/RELEASE-NOTES 2008-11-18 11:36:09 UTC (rev
43669)
@@ -3,9 +3,9 @@
Security reminder: MediaWiki does not require PHP's register_globals
setting since version 1.2.0. If you have it on, turn it *off* if you can.
-== MediaWiki 1.13.2 ==
+== MediaWiki 1.13.3 ==
-October 2, 2008
+November 18, 2008
This is a security and bugfix release of the Summer 2008 snapshot release of
MediaWiki.
@@ -21,6 +21,18 @@
Those wishing to use the latest code instead of a branch release can obtain
it from source control: http://www.mediawiki.org/wiki/Download_from_SVN
+== Changes since 1.13.2 ==
+
+* Safer handling of non-MediaWiki exceptions -- now obeys our settings for
formatting and path exposure. (Rem1)
+* Less verbose errors from profileinfo.php when not configured (Rem8)
+* Blacklist redirects via Special:Filepath, hard to use. (Rem7)
+* Improved input validation on Special:Import form (Rem10, Rem11)
+* Add a .htaccess to deleted images directory for additional protection
against exposure of deleted files with known SHA-1 hashes on default
installations. (Rem13)
+* Improved scripting safety heuristics for IE 5/6 content-type detection.
(Rem14)
+* Improved scripting safety heuristics on SVG uploads. (Rem2, Rem3, Rem5, Rem6)
+* Improved the security of file streaming (Special:Undelete, img_auth.php and
thumb.php): use the extension to determine the type, check it against the
blacklist. (Rem12.2)
+* Restrict img_auth.php to private wikis only. Require a session token before
streaming out Special:Undelete. If uploads are hosted on a different domain,
then these changes reduce the chance that an upload containing a script might
steal cookies from the wiki. (Rem12.1)
+
== Changes since 1.13.1 ==
* Security: Work around misconfiguration by requiring strict comparisons for
Modified: branches/REL1_13/phase3/img_auth.php
===================================================================
--- branches/REL1_13/phase3/img_auth.php 2008-11-18 10:51:44 UTC (rev
43668)
+++ branches/REL1_13/phase3/img_auth.php 2008-11-18 11:36:09 UTC (rev
43669)
@@ -17,6 +17,12 @@
wfProfileIn( 'img_auth.php' );
require_once( dirname( __FILE__ ) . '/includes/StreamFile.php' );
+$perms = User::getGroupPermissions( array( '*' ) );
+if ( in_array( 'read', $perms, true ) ) {
+ wfDebugLog( 'img_auth', 'Public wiki' );
+ wfPublicError();
+}
+
// Extract path and image information
if( !isset( $_SERVER['PATH_INFO'] ) ) {
wfDebugLog( 'img_auth', 'Missing PATH_INFO' );
@@ -88,3 +94,25 @@
wfLogProfilingData();
exit();
}
+
+/**
+ * Show a 403 error for use when the wiki is public
+ */
+function wfPublicError() {
+ header( 'HTTP/1.0 403 Forbidden' );
+ header( 'Content-Type: text/html; charset=utf-8' );
+ echo <<<ENDS
+<html>
+<body>
+<h1>Access Denied</h1>
+<p>The function of img_auth.php is to output files from a private wiki. This
wiki
+is configured as a public wiki. For optimal security, img_auth.php is disabled
in
+this case.
+</p>
+</body>
+</html>
+ENDS;
+ wfLogProfilingData();
+ exit;
+}
+
Modified: branches/REL1_13/phase3/includes/DefaultSettings.php
===================================================================
--- branches/REL1_13/phase3/includes/DefaultSettings.php 2008-11-18
10:51:44 UTC (rev 43668)
+++ branches/REL1_13/phase3/includes/DefaultSettings.php 2008-11-18
11:36:09 UTC (rev 43669)
@@ -31,7 +31,7 @@
$wgConf = new SiteConfiguration;
/** MediaWiki version number */
-$wgVersion = '1.13.2';
+$wgVersion = '1.13.3';
/** Name of the site. It must be changed in LocalSettings.php */
$wgSitename = 'MediaWiki';
Modified: branches/REL1_13/phase3/includes/Exception.php
===================================================================
--- branches/REL1_13/phase3/includes/Exception.php 2008-11-18 10:51:44 UTC
(rev 43668)
+++ branches/REL1_13/phase3/includes/Exception.php 2008-11-18 11:36:09 UTC
(rev 43669)
@@ -274,7 +274,16 @@
}
}
} else {
- echo $e->__toString();
+ $message = "Unexpected non-MediaWiki exception encountered, of
type \"" . get_class( $e ) . "\"\n" .
+ $e->__toString() . "\n";
+ if ( $GLOBALS['wgShowExceptionDetails'] ) {
+ $message .= "\n" . $e->getTraceAsString() ."\n";
+ }
+ if ( !empty( $GLOBALS['wgCommandLineMode'] ) ) {
+ wfPrintError( $message );
+ } else {
+ echo nl2br( htmlspecialchars( $message ) ). "\n";
+ }
}
}
Modified: branches/REL1_13/phase3/includes/StreamFile.php
===================================================================
--- branches/REL1_13/phase3/includes/StreamFile.php 2008-11-18 10:51:44 UTC
(rev 43668)
+++ branches/REL1_13/phase3/includes/StreamFile.php 2008-11-18 11:36:09 UTC
(rev 43669)
@@ -31,6 +31,12 @@
header('Content-type: application/x-wiki');
}
+ // Don't stream it out as text/html if there was a PHP error
+ if ( headers_sent() ) {
+ echo "Headers already sent, terminating.\n";
+ return;
+ }
+
global $wgContLanguageCode;
header( "Content-Disposition:
inline;filename*=utf-8'$wgContLanguageCode'" . urlencode( basename( $fname ) )
);
@@ -53,25 +59,51 @@
}
/** */
-function wfGetType( $filename ) {
+function wfGetType( $filename, $safe = true ) {
global $wgTrivialMimeDetection;
+ $ext = strrchr($filename, '.');
+ $ext = $ext === false ? '' : strtolower( substr( $ext, 1 ) );
+
# trivial detection by file extension,
# used for thumbnails (thumb.php)
if ($wgTrivialMimeDetection) {
- $ext= strtolower(strrchr($filename, '.'));
-
switch ($ext) {
- case '.gif': return 'image/gif';
- case '.png': return 'image/png';
- case '.jpg': return 'image/jpeg';
- case '.jpeg': return 'image/jpeg';
+ case 'gif': return 'image/gif';
+ case 'png': return 'image/png';
+ case 'jpg': return 'image/jpeg';
+ case 'jpeg': return 'image/jpeg';
}
return 'unknown/unknown';
}
- else {
- $magic = MimeMagic::singleton();
- return $magic->guessMimeType($filename); //full fancy mime
detection
+
+ $magic = MimeMagic::singleton();
+ // Use the extension only, rather than magic numbers, to avoid opening
+ // up vulnerabilities due to uploads of files with allowed extensions
+ // but disallowed types.
+ $type = $magic->guessTypesForExtension( $ext );
+
+ /**
+ * Double-check some security settings that were done on upload but
might
+ * have changed since.
+ */
+ if ( $safe ) {
+ global $wgFileBlacklist, $wgCheckFileExtensions,
$wgStrictFileExtensions,
+ $wgFileExtensions, $wgVerifyMimeType,
$wgMimeTypeBlacklist, $wgRequest;
+ $form = new UploadForm( $wgRequest );
+ list( $partName, $extList ) = $form->splitExtensions( $filename
);
+ if ( $form->checkFileExtensionList( $extList, $wgFileBlacklist
) ) {
+ return 'unknown/unknown';
+ }
+ if ( $wgCheckFileExtensions && $wgStrictFileExtensions
+ && !$form->checkFileExtensionList( $extList,
$wgFileExtensions ) )
+ {
+ return 'unknown/unknown';
+ }
+ if ( $wgVerifyMimeType && in_array( strtolower( $type ),
$wgMimeTypeBlacklist ) ) {
+ return 'unknown/unknown';
+ }
}
+ return $type;
}
Modified: branches/REL1_13/phase3/includes/Title.php
===================================================================
--- branches/REL1_13/phase3/includes/Title.php 2008-11-18 10:51:44 UTC (rev
43668)
+++ branches/REL1_13/phase3/includes/Title.php 2008-11-18 11:36:09 UTC (rev
43669)
@@ -320,9 +320,13 @@
$m[1] = urldecode( ltrim( $m[1], ':' )
);
}
$title = Title::newFromText( $m[1] );
- // Redirects to Special:Userlogout are not
permitted
- if( $title instanceof Title &&
!$title->isSpecial( 'Userlogout' ) )
+ // Redirects to some special pages are not
permitted
+ if( $title instanceof Title
+ && !$title->isSpecial(
'Userlogout' )
+ && !$title->isSpecial(
'Filepath' ) )
+ {
return $title;
+ }
}
}
return null;
Modified: branches/REL1_13/phase3/includes/XmlTypeCheck.php
===================================================================
--- branches/REL1_13/phase3/includes/XmlTypeCheck.php 2008-11-18 10:51:44 UTC
(rev 43668)
+++ branches/REL1_13/phase3/includes/XmlTypeCheck.php 2008-11-18 11:36:09 UTC
(rev 43669)
@@ -6,6 +6,12 @@
* well-formed XML. Note that this doesn't check schema validity.
*/
public $wellFormed = false;
+
+ /**
+ * Will be set to true if the optional element filter returned
+ * a match at some point.
+ */
+ public $filterMatch = false;
/**
* Name of the document's root element, including any namespace
@@ -13,33 +19,26 @@
*/
public $rootElement = '';
- private $softNamespaces;
- private $namespaces = array();
-
/**
* @param $file string filename
- * @param $softNamespaces bool
- * If set to true, use of undeclared XML namespaces will be
ignored.
- * This matches the behavior of rsvg, but more compliant
consumers
- * such as Firefox will reject such files.
- * Leave off for the default, stricter checks.
+ * @param $filterCallback callable (optional)
+ * Function to call to do additional custom validity checks from
the
+ * SAX element handler event. This gives you access to the
element
+ * namespace, name, and attributes, but not to text contents.
+ * Filter should return 'true' to toggle on $this->filterMatch
*/
- function __construct( $file, $softNamespaces=false ) {
- $this->softNamespaces = $softNamespaces;
+ function __construct( $file, $filterCallback=null ) {
+ $this->filterCallback = $filterCallback;
$this->run( $file );
}
private function run( $fname ) {
- if( $this->softNamespaces ) {
- $parser = xml_parser_create( 'UTF-8' );
- } else {
- $parser = xml_parser_create_ns( 'UTF-8' );
- }
+ $parser = xml_parser_create_ns( 'UTF-8' );
// case folding violates XML standard, turn it off
xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false
);
- xml_set_element_handler( $parser, array( $this, 'elementOpen'
), false );
+ xml_set_element_handler( $parser, array( $this,
'rootElementOpen' ), false );
$file = fopen( $fname, "rb" );
do {
@@ -59,35 +58,22 @@
xml_parser_free( $parser );
}
+ private function rootElementOpen( $parser, $name, $attribs ) {
+ $this->rootElement = $name;
+
+ if( is_callable( $this->filterCallback ) ) {
+ xml_set_element_handler( $parser, array( $this,
'elementOpen' ), false );
+ $this->elementOpen( $parser, $name, $attribs );
+ } else {
+ // We only need the first open element
+ xml_set_element_handler( $parser, false, false );
+ }
+ }
+
private function elementOpen( $parser, $name, $attribs ) {
- if( $this->softNamespaces ) {
- // Check namespaces manually, so expat doesn't throw
- // errors on use of undeclared namespaces.
- foreach( $attribs as $attrib => $val ) {
- if( $attrib == 'xmlns' ) {
- $this->namespaces[''] = $val;
- } elseif( substr( $attrib, 0, strlen( 'xmlns:'
) ) == 'xmlns:' ) {
- $this->namespaces[substr( $attrib,
strlen( 'xmlns:' ) )] = $val;
- }
- }
-
- if( strpos( $name, ':' ) === false ) {
- $ns = '';
- $subname = $name;
- } else {
- list( $ns, $subname ) = explode( ':', $name, 2
);
- }
-
- if( isset( $this->namespaces[$ns] ) ) {
- $name = $this->namespaces[$ns] . ':' . $subname;
- } else {
- // Technically this is invalid for XML with
Namespaces.
- // But..... we'll just let it slide in soft
mode.
- }
+ if( call_user_func( $this->filterCallback, $name, $attribs ) ) {
+ // Filter hit!
+ $this->filterMatch = true;
}
-
- // We only need the first open element
- $this->rootElement = $name;
- xml_set_element_handler( $parser, false, false );
}
}
Modified: branches/REL1_13/phase3/includes/specials/SpecialImport.php
===================================================================
--- branches/REL1_13/phase3/includes/specials/SpecialImport.php 2008-11-18
10:51:44 UTC (rev 43668)
+++ branches/REL1_13/phase3/includes/specials/SpecialImport.php 2008-11-18
11:36:09 UTC (rev 43669)
@@ -43,26 +43,30 @@
if( $wgRequest->wasPosted() && $wgRequest->getVal( 'action' ) ==
'submit') {
$isUpload = false;
$namespace = $wgRequest->getIntOrNull( 'namespace' );
+ $sourceName = $wgRequest->getVal( "source" );
- switch( $wgRequest->getVal( "source" ) ) {
- case "upload":
+ if ( !$wgUser->matchEditToken( $wgRequest->getVal( 'editToken'
) ) ) {
+ $source = new WikiErrorMsg( 'import-token-mismatch' );
+ } elseif ( $sourceName == 'upload' ) {
$isUpload = true;
if( $wgUser->isAllowed( 'importupload' ) ) {
$source = ImportStreamSource::newFromUpload(
"xmlimport" );
} else {
return $wgOut->permissionRequired(
'importupload' );
}
- break;
- case "interwiki":
+ } elseif ( $sourceName == "interwiki" ) {
$interwiki = $wgRequest->getVal( 'interwiki' );
- $history = $wgRequest->getCheck( 'interwikiHistory' );
- $frompage = $wgRequest->getText( "frompage" );
- $source = ImportStreamSource::newFromInterwiki(
- $interwiki,
- $frompage,
- $history );
- break;
- default:
+ if ( !in_array( $interwiki, $wgImportSources ) ) {
+ $source = new WikiErrorMsg(
"import-invalid-interwiki" );
+ } else {
+ $history = $wgRequest->getCheck(
'interwikiHistory' );
+ $frompage = $wgRequest->getText( "frompage" );
+ $source = ImportStreamSource::newFromInterwiki(
+ $interwiki,
+ $frompage,
+ $history );
+ }
+ } else {
$source = new WikiErrorMsg( "importunknownsource" );
}
@@ -106,6 +110,7 @@
Xml::hidden( 'action', 'submit' ) .
Xml::hidden( 'source', 'upload' ) .
Xml::input( 'xmlimport', 50, '', array( 'type' =>
'file' ) ) . ' ' .
+ Xml::hidden( 'editToken', $wgUser->editToken() ) .
Xml::submitButton( wfMsg( 'uploadbtn' ) ) .
Xml::closeElement( 'form' ) .
Xml::closeElement( 'fieldset' )
@@ -124,6 +129,7 @@
wfMsgExt( 'import-interwiki-text', array( 'parse' ) ) .
Xml::hidden( 'action', 'submit' ) .
Xml::hidden( 'source', 'interwiki' ) .
+ Xml::hidden( 'editToken', $wgUser->editToken() ) .
Xml::openElement( 'table', array( 'id' =>
'mw-import-table' ) ) .
"<tr>
<td>" .
Modified: branches/REL1_13/phase3/includes/specials/SpecialUndelete.php
===================================================================
--- branches/REL1_13/phase3/includes/specials/SpecialUndelete.php
2008-11-18 10:51:44 UTC (rev 43668)
+++ branches/REL1_13/phase3/includes/specials/SpecialUndelete.php
2008-11-18 11:36:09 UTC (rev 43669)
@@ -571,7 +571,7 @@
*/
class UndeleteForm {
var $mAction, $mTarget, $mTimestamp, $mRestore, $mTargetObj;
- var $mTargetTimestamp, $mAllowed, $mComment;
+ var $mTargetTimestamp, $mAllowed, $mComment, $mToken;
function UndeleteForm( $request, $par = "" ) {
global $wgUser;
@@ -589,6 +589,7 @@
$this->mDiff = $request->getCheck( 'diff' );
$this->mComment = $request->getText( 'wpComment' );
$this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) &&
$wgUser->isAllowed( 'suppressrevision' );
+ $this->mToken = $request->getVal( 'token' );
if( $par != "" ) {
$this->mTarget = $par;
@@ -655,6 +656,9 @@
if( !$file->userCan( File::DELETED_FILE ) ) {
$wgOut->permissionRequired( 'suppressrevision'
);
return false;
+ } elseif ( !$wgUser->matchEditToken( $this->mToken,
$this->mFile ) ) {
+ $this->showFileConfirmationForm( $this->mFile );
+ return false;
} else {
return $this->showFile( $this->mFile );
}
@@ -880,6 +884,29 @@
}
/**
+ * Show a form confirming whether a tokenless user really wants to see
a file
+ */
+ private function showFileConfirmationForm( $key ) {
+ global $wgOut, $wgUser, $wgLang;
+ $file = new ArchivedFile( $this->mTargetObj, '', $this->mFile );
+ $wgOut->addWikiMsg( 'undelete-show-file-confirm',
+ $this->mTargetObj->getText(),
+ $wgLang->timeanddate( $file->getTimestamp() ) );
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array(
+ 'method' => 'POST',
+ 'action' => SpecialPage::getTitleFor(
'Undelete' )->getLocalUrl(
+ 'target=' . urlencode( $this->mTarget )
.
+ '&file=' . urlencode( $key ) .
+ '&token=' . urlencode(
$wgUser->editToken( $key ) ) )
+ )
+ ) .
+ Xml::submitButton( wfMsg( 'undelete-show-file-submit' )
) .
+ '</form>'
+ );
+ }
+
+ /**
* Show a deleted file version requested by the visitor.
*/
private function showFile( $key ) {
@@ -1191,13 +1218,15 @@
* @return string
*/
function getFileLink( $file, $titleObj, $ts, $key, $sk ) {
- global $wgLang;
+ global $wgLang, $wgUser;
if( !$file->userCan(File::DELETED_FILE) ) {
return '<span class="history-deleted">' .
$wgLang->timeanddate( $ts, true ) . '</span>';
} else {
$link = $sk->makeKnownLinkObj( $titleObj,
$wgLang->timeanddate( $ts, true ),
-
"target=".$this->mTargetObj->getPrefixedUrl()."&file=$key" );
+ "target=".$this->mTargetObj->getPrefixedUrl().
+ "&file=$key" .
+ "&token=" . urlencode( $wgUser->editToken( $key
) ) );
if( $file->isDeleted(File::DELETED_FILE) )
$link = '<span class="history-deleted">' .
$link . '</span>';
return $link;
Modified: branches/REL1_13/phase3/includes/specials/SpecialUpload.php
===================================================================
--- branches/REL1_13/phase3/includes/specials/SpecialUpload.php 2008-11-18
10:51:44 UTC (rev 43668)
+++ branches/REL1_13/phase3/includes/specials/SpecialUpload.php 2008-11-18
11:36:09 UTC (rev 43669)
@@ -1348,6 +1348,11 @@
if( $this->detectScript ( $tmpfile, $mime, $extension ) ) {
return new WikiErrorMsg( 'uploadscripted' );
}
+ if( $extension == 'svg' || $mime == 'image/svg+xml' ) {
+ if( $this->detectScriptInSvg( $tmpfile ) ) {
+ return new WikiErrorMsg( 'uploadscripted' );
+ }
+ }
/**
* Scan the uploaded file for viruses
@@ -1459,6 +1464,7 @@
*/
$tags = array(
+ '<a href',
'<body',
'<head',
'<html', #also in safari
@@ -1497,7 +1503,42 @@
return false;
}
+ function detectScriptInSvg( $filename ) {
+ $check = new XmlTypeCheck( $filename, array( $this,
'checkSvgScriptCallback' ) );
+ return $check->filterMatch;
+ }
+
/**
+ * @todo Replace this with a whitelist filter!
+ */
+ function checkSvgScriptCallback( $element, $attribs ) {
+ $stripped = $this->stripXmlNamespace( $element );
+
+ if( $stripped == 'script' ) {
+ wfDebug( __METHOD__ . ": Found script element
'$element' in uploaded file.\n" );
+ return true;
+ }
+
+ foreach( $attribs as $attrib => $value ) {
+ $stripped = $this->stripXmlNamespace( $attrib );
+ if( substr( $stripped, 0, 2 ) == 'on' ) {
+ wfDebug( __METHOD__ . ": Found script attribute
'$attrib'='value' in uploaded file.\n" );
+ return true;
+ }
+ if( $stripped == 'href' && strpos( strtolower( $value
), 'javascript:' ) !== false ) {
+ wfDebug( __METHOD__ . ": Found script href
attribute '$attrib'='$value' in uploaded file.\n" );
+ return true;
+ }
+ }
+ }
+
+ private function stripXmlNamespace( $name ) {
+ // 'http://www.w3.org/2000/svg:script' -> 'script'
+ $parts = explode( ':', strtolower( $name ) );
+ return array_pop( $parts );
+ }
+
+ /**
* Generic wrapper function for a virus scanner program.
* This relies on the $wgAntivirus and $wgAntivirusSetup variables.
* $wgAntivirusRequired may be used to deny upload if the scan fails.
Modified: branches/REL1_13/phase3/languages/messages/MessagesEn.php
===================================================================
--- branches/REL1_13/phase3/languages/messages/MessagesEn.php 2008-11-18
10:51:44 UTC (rev 43668)
+++ branches/REL1_13/phase3/languages/messages/MessagesEn.php 2008-11-18
11:36:09 UTC (rev 43669)
@@ -2288,6 +2288,8 @@
'undelete-error-long' => 'Errors were encountered while undeleting
the file:
$1',
+'undelete-show-file-confirm' => 'Are you sure you want to view a deleted
revision of the file "<nowiki>$1</nowiki>" from $2?',
+'undelete-show-file-submit' => 'Yes',
# Namespace form on various pages
'namespace' => 'Namespace:',
@@ -2583,6 +2585,8 @@
'import-nonewrevisions' => 'All revisions were previously imported.',
'xml-error-string' => '$1 at line $2, col $3 (byte $4): $5',
'import-upload' => 'Upload XML data',
+'import-token-mismatch' => 'Loss of session data. Please try again.',
+'import-invalid-interwiki' => 'Cannot import from the specified wiki.',
# Import log
'importlogpage' => 'Import log',
Modified: branches/REL1_13/phase3/profileinfo.php
===================================================================
--- branches/REL1_13/phase3/profileinfo.php 2008-11-18 10:51:44 UTC (rev
43668)
+++ branches/REL1_13/phase3/profileinfo.php 2008-11-18 11:36:09 UTC (rev
43669)
@@ -60,7 +60,7 @@
define( 'MW_NO_SETUP', 1 );
require_once( './includes/WebStart.php' );
-require_once("./AdminSettings.php");
[EMAIL PROTECTED]("./AdminSettings.php");
require_once( './includes/GlobalFunctions.php' );
if (!$wgEnableProfileInfo) {
_______________________________________________
MediaWiki-CVS mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-cvs