diff options
Diffstat (limited to 'maintenance')
66 files changed, 3761 insertions, 645 deletions
diff --git a/maintenance/Doxyfile b/maintenance/Doxyfile index df67ba60..cdc748d8 100644 --- a/maintenance/Doxyfile +++ b/maintenance/Doxyfile @@ -1,4 +1,4 @@ -# Doxyfile 1.5.1 +# Doxyfile 1.5.6 # # Some placeholders have been added for MediaWiki usage: @@ -7,6 +7,8 @@ # {{STRIP_FROM_PATH}} # {{SVNSTAT}} # {{INPUT}} +# +# A number of MediaWiki-specific aliases are near the end of this file. #--------------------------------------------------------------------------- # Project related configuration options @@ -16,7 +18,6 @@ PROJECT_NUMBER = {{CURRENT_VERSION}} OUTPUT_DIRECTORY = {{OUTPUT_DIRECTORY}} CREATE_SUBDIRS = NO OUTPUT_LANGUAGE = English -USE_WINDOWS_ENCODING = NO BRIEF_MEMBER_DESC = YES REPEAT_BRIEF = YES ABBREVIATE_BRIEF = "The $name class" \ @@ -41,12 +42,11 @@ MULTILINE_CPP_IS_BRIEF = NO DETAILS_AT_TOP = NO INHERIT_DOCS = YES SEPARATE_MEMBER_PAGES = NO -TAB_SIZE = 4 -ALIASES = +TAB_SIZE = 8 OPTIMIZE_OUTPUT_FOR_C = NO OPTIMIZE_OUTPUT_JAVA = NO BUILTIN_STL_SUPPORT = NO -DISTRIBUTE_GROUP_DOC = NO +DISTRIBUTE_GROUP_DOC = YES SUBGROUPING = YES #--------------------------------------------------------------------------- # Build related configuration options @@ -59,7 +59,7 @@ EXTRACT_LOCAL_METHODS = NO HIDE_UNDOC_MEMBERS = NO HIDE_UNDOC_CLASSES = NO HIDE_FRIEND_COMPOUNDS = NO -HIDE_IN_BODY_DOCS = NO +HIDE_IN_BODY_DOCS = YES INTERNAL_DOCS = NO CASE_SENSE_NAMES = YES HIDE_SCOPE_NAMES = NO @@ -174,7 +174,7 @@ CHM_FILE = HHC_LOCATION = GENERATE_CHI = NO BINARY_TOC = NO -TOC_EXPAND = NO +TOC_EXPAND = YES DISABLE_INDEX = NO ENUM_VALUES_PER_LINE = 4 GENERATE_TREEVIEW = YES @@ -190,8 +190,8 @@ COMPACT_LATEX = NO PAPER_TYPE = a4wide EXTRA_PACKAGES = LATEX_HEADER = -PDF_HYPERLINKS = NO -USE_PDFLATEX = NO +PDF_HYPERLINKS = YES +USE_PDFLATEX = YES LATEX_BATCHMODE = NO LATEX_HIDE_INDICES = NO #--------------------------------------------------------------------------- @@ -206,7 +206,7 @@ RTF_EXTENSIONS_FILE = #--------------------------------------------------------------------------- # configuration options related to the man page output #--------------------------------------------------------------------------- -GENERATE_MAN = YES +GENERATE_MAN = NO MAN_OUTPUT = man MAN_EXTENSION = .3 MAN_LINKS = NO @@ -268,8 +268,6 @@ DIRECTORY_GRAPH = YES DOT_IMAGE_FORMAT = png DOT_PATH = DOTFILE_DIRS = -MAX_DOT_GRAPH_WIDTH = 1024 -MAX_DOT_GRAPH_HEIGHT = 1024 MAX_DOT_GRAPH_DEPTH = 1000 DOT_TRANSPARENT = NO DOT_MULTI_TARGETS = NO @@ -279,3 +277,20 @@ DOT_CLEANUP = YES # Configuration::additions related to the search engine #--------------------------------------------------------------------------- SEARCHENGINE = NO + +ALIASES = "type{1}=<b> \1 </b>:" \ + "types{2}=<b> \1 </b> or <b> \2 </b>:" \ + "types{3}=<b> \1 </b>, <b> \2 </b>, or <b> \3 </b>:" \ + "arrayof{2}=<b> Array </b> of \2" \ + "null=\type{Null}" \ + "boolean=\type{Boolean}" \ + "bool=\boolean" \ + "integer=\type{Integer}" \ + "int=\integer" \ + "string=\type{String}" \ + "str=\string" \ + "mixed=\type{Mixed}" \ + "access=\par Access:\n" \ + "private=\access private" \ + "protected=\access protected" \ + "public=\access public"
\ No newline at end of file diff --git a/maintenance/FiveUpgrade.inc b/maintenance/FiveUpgrade.inc index 7ae8f5d0..5632241a 100644 --- a/maintenance/FiveUpgrade.inc +++ b/maintenance/FiveUpgrade.inc @@ -20,8 +20,9 @@ class FiveUpgrade { function FiveUpgrade() { $this->conversionTables = $this->prepareWindows1252(); - $this->dbw =& $this->newConnection(); - $this->dbr =& $this->streamConnection(); + $this->loadBalancers = array(); + $this->dbw = wfGetDB( DB_MASTER ); + $this->dbr = $this->streamConnection(); $this->cleanupSwaps = array(); $this->emailAuth = false; # don't preauthenticate emails @@ -67,13 +68,23 @@ class FiveUpgrade { * @return Database * @access private */ - function &newConnection() { - global $wgDBadminuser, $wgDBadminpassword, $wgDBtype; - global $wgDBserver, $wgDBname; - $dbclass = 'Database' . ucfirst( $wgDBtype ) ; - $db = new $dbclass( $wgDBserver, $wgDBadminuser, $wgDBadminpassword, $wgDBname ); + function newConnection() { + $lb = wfGetLBFactory()->newMainLB(); + $db = $lb->getConnection( DB_MASTER ); + + $this->loadBalancers[] = $lb; return $db; } + + /** + * Commit transactions and close the connections when we're done... + */ + function close() { + foreach( $this->loadBalancers as $lb ) { + $lb->commitMasterChanges(); + $lb->closeAll(); + } + } /** * Open a second connection to the master server, with buffering off. @@ -82,13 +93,13 @@ class FiveUpgrade { * @return Database * @access private */ - function &streamConnection() { + function streamConnection() { global $wgDBtype; $timeout = 3600 * 24; $db =& $this->newConnection(); $db->bufferResults( false ); - if ($wgDBtype == 'mysql') { + if ($wgDBtype == 'mysql') { $db->query( "SET net_read_timeout=$timeout" ); $db->query( "SET net_write_timeout=$timeout" ); } @@ -877,17 +888,17 @@ END; $add = array(); while( $row = $this->dbr->fetchObject( $result ) ) { $add[] = array( - 'wl_user' => $row->wl_user, - 'wl_namespace' => Namespace::getSubject( $row->wl_namespace ), - 'wl_title' => $this->conv( $row->wl_title ), - 'wl_notificationtimestamp' => '0' ); + 'wl_user' => $row->wl_user, + 'wl_namespace' => MWNamespace::getSubject( $row->wl_namespace ), + 'wl_title' => $this->conv( $row->wl_title ), + 'wl_notificationtimestamp' => '0' ); $this->addChunk( $add ); $add[] = array( - 'wl_user' => $row->wl_user, - 'wl_namespace' => Namespace::getTalk( $row->wl_namespace ), - 'wl_title' => $this->conv( $row->wl_title ), - 'wl_notificationtimestamp' => '0' ); + 'wl_user' => $row->wl_user, + 'wl_namespace' => MWNamespace::getTalk( $row->wl_namespace ), + 'wl_title' => $this->conv( $row->wl_title ), + 'wl_notificationtimestamp' => '0' ); $this->addChunk( $add ); } $this->lastChunk( $add ); @@ -988,7 +999,7 @@ CREATE TABLE $1 ( -- Filename of target image. -- This is also the page_title of the file's description page; - -- all such pages are in namespace 6 (NS_IMAGE). + -- all such pages are in namespace 6 (NS_FILE). il_to varchar(255) binary NOT NULL default '', UNIQUE KEY il_from(il_from,il_to), diff --git a/maintenance/addwiki.php b/maintenance/addwiki.php index b9c48506..ebe52f2e 100644 --- a/maintenance/addwiki.php +++ b/maintenance/addwiki.php @@ -49,6 +49,7 @@ function addWiki( $lang, $site, $dbName ) dbsource( "$IP/extensions/CheckUser/cu_log.sql", $dbw ); dbsource( "$IP/extensions/TitleKey/titlekey.sql", $dbw ); dbsource( "$IP/extensions/Oversight/hidden.sql", $dbw ); + dbsource( "$IP/extensions/GlobalBlocking/localdb_patches/setup-global_block_whitelist.sql", $dbw ); $dbw->query( "INSERT INTO site_stats(ss_row_id) VALUES (1)" ); diff --git a/maintenance/archives/patch-ipb_allow_usertalk.sql b/maintenance/archives/patch-ipb_allow_usertalk.sql new file mode 100644 index 00000000..92e7d9a4 --- /dev/null +++ b/maintenance/archives/patch-ipb_allow_usertalk.sql @@ -0,0 +1,3 @@ +-- Adding ipb_allow_usertalk for blocks +ALTER TABLE /*$wgDBprefix*/ipblocks + ADD ipb_allow_usertalk bool NOT NULL default 1; diff --git a/maintenance/archives/patch-linktables.sql b/maintenance/archives/patch-linktables.sql index ae9768a8..b15878c3 100644 --- a/maintenance/archives/patch-linktables.sql +++ b/maintenance/archives/patch-linktables.sql @@ -49,7 +49,7 @@ CREATE TABLE /*$wgDBprefix*/imagelinks ( -- Filename of target image. -- This is also the page_title of the file's description page; - -- all such pages are in namespace 6 (NS_IMAGE). + -- all such pages are in namespace 6 (NS_FILE). il_to varchar(255) binary NOT NULL default '', UNIQUE KEY il_from(il_from,il_to), diff --git a/maintenance/archives/patch-log_user_text.sql b/maintenance/archives/patch-log_user_text.sql new file mode 100644 index 00000000..f89a35f6 --- /dev/null +++ b/maintenance/archives/patch-log_user_text.sql @@ -0,0 +1,5 @@ +ALTER TABLE /*$wgDBprefix*/logging + ADD log_user_text varchar(255) binary NOT NULL default '', + ADD log_target_id int unsigned NULL, + CHANGE `log_type` `log_type` VARBINARY( 15 ) NOT NULL, + CHANGE `log_action` `log_action` VARBINARY( 15 ) NOT NULL; diff --git a/maintenance/archives/patch-ss_active_users.sql b/maintenance/archives/patch-ss_active_users.sql new file mode 100644 index 00000000..a583cdc8 --- /dev/null +++ b/maintenance/archives/patch-ss_active_users.sql @@ -0,0 +1,3 @@ +-- More statistics, for version 1.14 + +ALTER TABLE /*$wgDBprefix*/site_stats ADD ss_active_users bigint default '-1'; diff --git a/maintenance/backup.inc b/maintenance/backup.inc index bf52c1f3..e2e5363e 100644 --- a/maintenance/backup.inc +++ b/maintenance/backup.inc @@ -175,7 +175,7 @@ class BackupDumper { // extension point for subclasses to add options } - function dump( $history, $text = MW_EXPORT_TEXT ) { + function dump( $history, $text = WikiExporter::TEXT ) { # Notice messages will foul up your XML output even if they're # relatively harmless. if( ini_get( 'display_errors' ) ) @@ -192,13 +192,21 @@ class BackupDumper { if( !$this->skipHeader ) $exporter->openStream(); - - if( is_null( $this->pages ) ) { + # Log item dumps: all or by range + if( $history & WikiExporter::LOGS ) { + if( $this->startId || $this->endId ) { + $exporter->logsByRange( $this->startId, $this->endId ); + } else { + $exporter->allLogs(); + } + # Page dumps: all or by page ID range + } else if( is_null( $this->pages ) ) { if( $this->startId || $this->endId ) { $exporter->pagesByRange( $this->startId, $this->endId ); } else { $exporter->allPages(); } + # Dump of specific pages } else { $exporter->pagesByName( $this->pages ); } diff --git a/maintenance/benchmarkPurge.php b/maintenance/benchmarkPurge.php index 76302a01..796e1da2 100644 --- a/maintenance/benchmarkPurge.php +++ b/maintenance/benchmarkPurge.php @@ -9,7 +9,12 @@ /** */ require_once( "commandLine.inc" ); -/** @todo document */ +/** + * Run a bunch of URLs through SquidUpdate::purge() + * to benchmark Squid response times. + * @param $urls array A bunch of URLs to purge + * @param $trials int How many times to run the test? + */ function benchSquid( $urls, $trials = 1 ) { $start = wfTime(); for( $i = 0; $i < $trials; $i++) { @@ -22,7 +27,10 @@ function benchSquid( $urls, $trials = 1 ) { count( $urls ), $pertrial * 1000.0, $pertitle * 1000.0 ); } -/** @todo document */ +/** + * Get an array of randomUrl()'s. + * @param $length int How many urls to add to the array + */ function randomUrlList( $length ) { $list = array(); for( $i = 0; $i < $length; $i++ ) { @@ -31,13 +39,19 @@ function randomUrlList( $length ) { return $list; } -/** @todo document */ +/** + * Return a random URL of the wiki. Not necessarily an actual title in the + * database, but at least a URL that looks like one. + */ function randomUrl() { global $wgServer, $wgArticlePath; return $wgServer . str_replace( '$1', randomTitle(), $wgArticlePath ); } -/** @todo document */ +/** + * Create a random title string (not necessarily a Title object). + * For use with randomUrl(). + */ function randomTitle() { $str = ''; $length = mt_rand( 1, 20 ); diff --git a/maintenance/checkAutoLoader.php b/maintenance/checkAutoLoader.php index c2909ef7..554395ca 100644 --- a/maintenance/checkAutoLoader.php +++ b/maintenance/checkAutoLoader.php @@ -3,17 +3,24 @@ if ( php_sapi_name() != 'cli' ) exit; $IP = dirname(__FILE__) .'/..'; require( "$IP/includes/AutoLoader.php" ); -$files = array_unique( AutoLoader::$localClasses ); +$files = array_unique( $wgAutoloadLocalClasses ); foreach ( $files as $file ) { - $parseInfo = parsekit_compile_file( "$IP/$file" ); - $classes = array_keys( $parseInfo['class_table'] ); + if( function_exists( 'parsekit_compile_file' ) ){ + $parseInfo = parsekit_compile_file( "$IP/$file" ); + $classes = array_keys( $parseInfo['class_table'] ); + } else { + $contents = file_get_contents( "$IP/$file" ); + $m = array(); + preg_match_all( '/\n\s*class\s+([a-zA-Z0-9_]+)/', $contents, $m, PREG_PATTERN_ORDER ); + $classes = $m[1]; + } foreach ( $classes as $class ) { - if ( !isset( AutoLoader::$localClasses[$class] ) ) { + if ( !isset( $wgAutoloadLocalClasses[$class] ) ) { //printf( "%-50s Unlisted, in %s\n", $class, $file ); echo " '$class' => '$file',\n"; - } elseif ( AutoLoader::$localClasses[$class] !== $file ) { - echo "$class: Wrong file: found in $file, listed in " . AutoLoader::$localClasses[$class] . "\n"; + } elseif ( $wgAutoloadLocalClasses[$class] !== $file ) { + echo "$class: Wrong file: found in $file, listed in " . $wgAutoloadLocalClasses[$class] . "\n"; } } diff --git a/maintenance/checkBadRedirects.php b/maintenance/checkBadRedirects.php new file mode 100644 index 00000000..48a4b0e6 --- /dev/null +++ b/maintenance/checkBadRedirects.php @@ -0,0 +1,30 @@ +<?php + +require "commandLine.inc"; + +echo "Fetching redirects...\n"; +$dbr = wfGetDB( DB_SLAVE ); +$result = $dbr->select( + array( 'page' ), + array( 'page_namespace','page_title', 'page_latest' ), + array( 'page_is_redirect' => 1 ) ); + +$count = $result->numRows(); +echo "Found $count total redirects.\n"; +echo "Looking for bad redirects:\n"; +echo "\n"; + +foreach( $result as $row ) { + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $rev = Revision::newFromId( $row->page_latest ); + if( $rev ) { + $target = Title::newFromRedirect( $rev->getText() ); + if( !$target ) { + echo $title->getPrefixedText(); + echo "\n"; + } + } +} + +echo "\n"; +echo "done.\n"; diff --git a/maintenance/checkImages.php b/maintenance/checkImages.php new file mode 100644 index 00000000..994cd5b9 --- /dev/null +++ b/maintenance/checkImages.php @@ -0,0 +1,45 @@ +<?php + +require( 'commandLine.inc' ); + +$batchSize = 1000; +$start = ''; +$dbr = wfGetDB( DB_SLAVE ); +$localRepo = RepoGroup::singleton()->getLocalRepo(); + +$numImages = 0; +$numGood = 0; + +do { + $res = $dbr->select( 'image', '*', array( 'img_name > ' . $dbr->addQuotes( $start ) ) ); + foreach ( $res as $row ) { + $numImages++; + $start = $row->img_name; + $file = $localRepo->newFileFromRow( $row ); + $path = $file->getPath(); + if ( !$path ) { + echo "{$row->img_name}: not locally accessible\n"; + continue; + } + $stat = @stat( $file->getPath() ); + if ( !$stat ) { + echo "{$row->img_name}: missing\n"; + continue; + } + + if ( $stat['size'] == 0 && $row->img_size != 0 ) { + echo "{$row->img_name}: truncated, was {$row->img_size}\n"; + continue; + } + + if ( $stat['size'] != $row->img_size ) { + echo "{$row->img_name}: size mismatch DB={$row->img_size}, actual={$stat['size']}\n"; + continue; + } + + $numGood++; + } + +} while ( $res->numRows() ); + +echo "Good images: $numGood/$numImages\n"; diff --git a/maintenance/cleanupImages.php b/maintenance/cleanupImages.php index 79ff54e8..00903f22 100644 --- a/maintenance/cleanupImages.php +++ b/maintenance/cleanupImages.php @@ -54,6 +54,9 @@ class ImageCleanup extends TableCleanup { // About half of old bad image names have percent-codes $cleaned = rawurldecode( $cleaned ); + + // We also have some HTML entities there + $cleaned = Sanitizer::decodeCharReferences( $cleaned ); // Some are old latin-1 $cleaned = $wgContLang->checkTitleEncoding( $cleaned ); @@ -61,11 +64,13 @@ class ImageCleanup extends TableCleanup { // Many of remainder look like non-normalized unicode $cleaned = UtfNormal::cleanUp( $cleaned ); - $title = Title::makeTitleSafe( NS_IMAGE, $cleaned ); + $title = Title::makeTitleSafe( NS_FILE, $cleaned ); if( is_null( $title ) ) { $this->log( "page $source ($cleaned) is illegal." ); $safe = $this->buildSafeTitle( $cleaned ); + if( $safe === false ) + return $this->progress( 0 ); $this->pokeFile( $source, $safe ); return $this->progress( 1 ); } @@ -110,8 +115,8 @@ class ImageCleanup extends TableCleanup { $version = 0; $final = $new; - while( $db->selectField( 'image', 'img_name', - array( 'img_name' => $final ), __METHOD__ ) ) { + while( $db->selectField( 'image', 'img_name', array( 'img_name' => $final ), __METHOD__ ) || + Title::makeTitle( NS_FILE, $final )->exists() ) { $this->log( "Rename conflicts with '$final'..." ); $version++; $final = $this->appendTitle( $new, "_$version" ); @@ -123,14 +128,23 @@ class ImageCleanup extends TableCleanup { $this->log( "DRY RUN: would rename $path to $finalPath" ); } else { $this->log( "renaming $path to $finalPath" ); + // XXX: should this use File::move()? FIXME? $db->begin(); $db->update( 'image', array( 'img_name' => $final ), array( 'img_name' => $orig ), __METHOD__ ); + $db->update( 'oldimage', + array( 'oi_name' => $final ), + array( 'oi_name' => $orig ), + __METHOD__ ); + $db->update( 'page', + array( 'page_title' => $final ), + array( 'page_title' => $orig, 'page_namespace' => NS_FILE ), + __METHOD__ ); $dir = dirname( $finalPath ); if( !file_exists( $dir ) ) { - if( !mkdir( $dir, 0777, true ) ) { + if( !wfMkdirParents( $dir ) ) { $this->log( "RENAME FAILED, COULD NOT CREATE $dir" ); $db->rollback(); return; @@ -153,11 +167,11 @@ class ImageCleanup extends TableCleanup { function buildSafeTitle( $name ) { global $wgLegalTitleChars; $x = preg_replace_callback( - "/([^$wgLegalTitleChars])/", + "/([^$wgLegalTitleChars]|~)/", array( $this, 'hexChar' ), $name ); - $test = Title::makeTitleSafe( NS_IMAGE, $x ); + $test = Title::makeTitleSafe( NS_FILE, $x ); if( is_null( $test ) || $test->getDBkey() !== $x ) { $this->log( "Unable to generate safe title from '$name', got '$x'" ); return false; diff --git a/maintenance/cleanupTitles.php b/maintenance/cleanupTitles.php index a6991829..4d76ac7a 100644 --- a/maintenance/cleanupTitles.php +++ b/maintenance/cleanupTitles.php @@ -48,19 +48,30 @@ class TitleCleanup extends TableCleanup { $title = Title::newFromText( $verified ); - if( is_null( $title ) ) { + if( !is_null( $title ) && $title->equals( $current ) && $title->canExist() ) { + return $this->progress( 0 ); // all is fine + } + + if( $row->page_namespace == NS_FILE && $this->fileExists( $row->page_title ) ) { + $this->log( "file $row->page_title needs cleanup, please run cleanupImages.php." ); + return $this->progress( 0 ); + } elseif( is_null( $title ) ) { $this->log( "page $row->page_id ($display) is illegal." ); $this->moveIllegalPage( $row ); return $this->progress( 1 ); - } - - if( !$title->equals( $current ) ) { + } else { $this->log( "page $row->page_id ($display) doesn't match self." ); $this->moveInconsistentPage( $row, $title ); return $this->progress( 1 ); } + } - $this->progress( 0 ); + function fileExists( $name ) { + // XXX: Doesn't actually check for file existence, just presence of image record. + // This is reasonable, since cleanupImages.php only iterates over the image table. + $dbr = wfGetDB( DB_SLAVE ); + $row = $dbr->selectRow( 'image', array( 'img_name' ), array( 'img_name' => $name ), __METHOD__ ); + return $row !== false; } function moveIllegalPage( $row ) { diff --git a/maintenance/deleteBatch.php b/maintenance/deleteBatch.php index d10948a0..5aeea781 100644 --- a/maintenance/deleteBatch.php +++ b/maintenance/deleteBatch.php @@ -2,9 +2,10 @@ /** * Deletes a batch of pages - * Usage: php deleteBatch.php [-u <user>] [-r <reason>] [-i <interval>] <listfile> + * Usage: php deleteBatch.php [-u <user>] [-r <reason>] [-i <interval>] [listfile] * where - * <listfile> is a file where each line contains the title of a page to be deleted. + * [listfile] is a file where each line contains the title of a page to be + * deleted, standard input is used if listfile is not given. * <user> is the username * <reason> is the delete reason * <interval> is the number of seconds to sleep for after each delete @@ -70,7 +71,7 @@ for ( $linenum = 1; !feof( $file ); $linenum++ ) { print $page->getPrefixedText(); $dbw->begin(); - if( $page->getNamespace() == NS_IMAGE ) { + if( $page->getNamespace() == NS_FILE ) { $art = new ImagePage( $page ); $img = wfFindFile( $art->mTitle ); if( !$img || !$img->delete( $reason ) ) { diff --git a/maintenance/dumpBackup.php b/maintenance/dumpBackup.php index bb431242..de7ce655 100644 --- a/maintenance/dumpBackup.php +++ b/maintenance/dumpBackup.php @@ -63,6 +63,8 @@ if( isset( $options['full'] ) ) { $dumper->dump( WikiExporter::FULL, $textMode ); } elseif( isset( $options['current'] ) ) { $dumper->dump( WikiExporter::CURRENT, $textMode ); +} elseif( isset( $options['logs'] ) ) { + $dumper->dump( WikiExporter::LOGS ); } else { $dumper->progress( <<<ENDS This script dumps the wiki page database into an XML interchange wrapper @@ -74,6 +76,7 @@ Usage: php dumpBackup.php <action> [<options>] Actions: --full Dump complete history of every page. --current Includes only the latest revision of each page. + --logs Dump action logs for every page. Options: --quiet Don't dump status reports to stderr. diff --git a/maintenance/dumpTextPass.php b/maintenance/dumpTextPass.php index eb4cc072..e85fe421 100644 --- a/maintenance/dumpTextPass.php +++ b/maintenance/dumpTextPass.php @@ -487,7 +487,7 @@ class TextPassDumper extends BackupDumper { function clearOpenElement( $style ) { if( $this->openElement ) { - $this->buffer .= wfElement( $this->openElement[0], $this->openElement[1], $style ); + $this->buffer .= Xml::element( $this->openElement[0], $this->openElement[1], $style ); $this->openElement = false; } } diff --git a/maintenance/edit.php b/maintenance/edit.php index 037f9a9a..64178045 100644 --- a/maintenance/edit.php +++ b/maintenance/edit.php @@ -58,15 +58,20 @@ $text = file_get_contents( 'php://stdin' ); # Do the edit print "Saving... "; -$success = $wgArticle->doEdit( $text, $summary, +$status = $wgArticle->doEdit( $text, $summary, ( $minor ? EDIT_MINOR : 0 ) | ( $bot ? EDIT_FORCE_BOT : 0 ) | ( $autoSummary ? EDIT_AUTOSUMMARY : 0 ) | ( $noRC ? EDIT_SUPPRESS_RC : 0 ) ); -if ( $success ) { +if ( $status->isOK() ) { print "done\n"; + $exit = 0; } else { print "failed\n"; - exit( 1 ); + $exit = 1; +} +if ( !$status->isGood() ) { + print $status->getWikiText() . "\n"; } +exit( $exit ); diff --git a/maintenance/findhooks.php b/maintenance/findhooks.php index 7a2ba53f..d7cad253 100644 --- a/maintenance/findhooks.php +++ b/maintenance/findhooks.php @@ -31,6 +31,7 @@ $pathinc = array( $IP.'/includes/', $IP.'/includes/api/', $IP.'/includes/db/', + $IP.'/includes/diff/', $IP.'/includes/filerepo/', $IP.'/includes/parser/', $IP.'/includes/specials/', diff --git a/maintenance/generateSitemap.php b/maintenance/generateSitemap.php index cc3f523a..52dc33ae 100644 --- a/maintenance/generateSitemap.php +++ b/maintenance/generateSitemap.php @@ -80,8 +80,8 @@ class GenerateSitemap { NS_USER_TALK => '0.1', NS_PROJECT => '0.5', NS_PROJECT_TALK => '0.1', - NS_IMAGE => '0.5', - NS_IMAGE_TALK => '0.1', + NS_FILE => '0.5', + NS_FILE_TALK => '0.1', NS_MEDIAWIKI => '0.0', NS_MEDIAWIKI_TALK => '0.1', NS_TEMPLATE => '0.0', @@ -424,7 +424,7 @@ class GenerateSitemap { * * @static * - * @param string $url An RFC 2396 compilant URL + * @param string $url An RFC 2396 compliant URL * @param string $date A ISO 8601 date * @param string $priority A priority indicator, 0.0 - 1.0 inclusive with a 0.1 stepsize * diff --git a/maintenance/importDump.php b/maintenance/importDump.php index 99e69ce8..eb51126a 100644 --- a/maintenance/importDump.php +++ b/maintenance/importDump.php @@ -48,13 +48,10 @@ class BackupReader { function handleRevision( $rev ) { $title = $rev->getTitle(); - if (!$title) { + if( !$title ) { $this->progress( "Got bogus revision with null title!" ); return; } - #$timestamp = $rev->getTimestamp(); - #$display = $title->getPrefixedText(); - #echo "$display $timestamp\n"; $this->revCount++; $this->report(); @@ -79,6 +76,15 @@ class BackupReader { } } + function handleLogItem( $rev ) { + $this->revCount++; + $this->report(); + + if( !$this->dryRun ) { + call_user_func( $this->logItemCallback, $rev ); + } + } + function report( $final = false ) { if( $final xor ( $this->pageCount % $this->reportingInterval == 0 ) ) { $this->showReport(); @@ -95,7 +101,11 @@ class BackupReader { $rate = '-'; $revrate = '-'; } - $this->progress( "$this->pageCount ($rate pages/sec $revrate revs/sec)" ); + # Logs dumps don't have page tallies + if( $this->pageCount ) + $this->progress( "$this->pageCount ($rate pages/sec $revrate revs/sec)" ); + else + $this->progress( "$this->revCount ($revrate revs/sec)" ); } wfWaitForSlaves(5); } @@ -129,6 +139,8 @@ class BackupReader { array( &$this, 'handleRevision' ) ); $this->uploadCallback = $importer->setUploadCallback( array( &$this, 'handleUpload' ) ); + $this->logItemCallback = $importer->setLogItemCallback( + array( &$this, 'handleLogItem' ) ); return $importer->doImport(); } diff --git a/maintenance/importImages.inc.php b/maintenance/importImages.inc.php index 53895778..290f3c07 100644 --- a/maintenance/importImages.inc.php +++ b/maintenance/importImages.inc.php @@ -46,4 +46,43 @@ function splitFilename( $filename ) { unset( $parts[ count( $parts ) - 1 ] ); $fname = implode( '.', $parts ); return array( $fname, $ext ); +} + +/** + * Find an auxilliary file with the given extension, matching + * the give base file path. $maxStrip determines how many extensions + * may be stripped from the original file name before appending the + * new extension. For example, with $maxStrip = 1 (the default), + * file files acme.foo.bar.txt and acme.foo.txt would be auxilliary + * files for acme.foo.bar and the extension ".txt". With $maxStrip = 2, + * acme.txt would also be acceptable. + * + * @param $file base path + * @param $auxExtension the extension to be appended to the base path + * @param $maxStrip the maximum number of extensions to strip from the base path (default: 1) + * @return string or false + */ +function findAuxFile( $file, $auxExtension, $maxStrip = 1 ) { + if ( strpos( $auxExtension, '.' ) !== 0 ) { + $auxExtension = '.' . $auxExtension; + } + + $d = dirname( $file ); + $n = basename( $file ); + + while ( $maxStrip >= 0 ) { + $f = $d . '/' . $n . $auxExtension; + + if ( file_exists( $f ) ) { + return $f; + } + + $idx = strrpos( $n, '.' ); + if ( !$idx ) break; + + $n = substr( $n, 0, $idx ); + $maxStrip -= 1; + } + + return false; }
\ No newline at end of file diff --git a/maintenance/importImages.php b/maintenance/importImages.php index 63bbec5f..4c6082b2 100644 --- a/maintenance/importImages.php +++ b/maintenance/importImages.php @@ -9,7 +9,7 @@ * @author Rob Church <robchur@gmail.com> */ -$optionsWithArguments = array( 'extensions', 'overwrite' ); +$optionsWithArgs = array( 'extensions', 'comment', 'comment-file', 'comment-ext', 'user', 'license' ); require_once( 'commandLine.inc' ); require_once( 'importImages.inc.php' ); $added = $skipped = $overwritten = 0; @@ -39,9 +39,19 @@ if( count( $args ) > 0 ) { $wgUser = $user; # Get the upload comment - $comment = isset( $options['comment'] ) - ? $options['comment'] - : 'Importing image file'; + $comment = 'Importing image file'; + + if ( isset( $options['comment-file'] ) ) { + $comment = file_get_contents( $options['comment-file'] ); + if ( $comment === false || $comment === NULL ) { + die( "failed to read comment file: {$options['comment-file']}\n" ); + } + } + else if ( isset( $options['comment'] ) ) { + $comment = $options['comment']; + } + + $commentExt = isset( $options['comment-ext'] ) ? $options['comment-ext'] : false; # Get the license specifier $license = isset( $options['license'] ) ? $options['license'] : ''; @@ -53,7 +63,7 @@ if( count( $args ) > 0 ) { $base = wfBaseName( $file ); # Validate a title - $title = Title::makeTitleSafe( NS_IMAGE, $base ); + $title = Title::makeTitleSafe( NS_FILE, $base ); if( !is_object( $title ) ) { echo( "{$base} could not be imported; a valid title cannot be produced\n" ); continue; @@ -75,15 +85,40 @@ if( count( $args ) > 0 ) { $svar = 'added'; } + # Find comment text + $commentText = false; + + if ( $commentExt ) { + $f = findAuxFile( $file, $commentExt ); + if ( !$f ) { + echo( " No comment file with extension {$commentExt} found for {$file}, using default comment. " ); + } else { + $commentText = file_get_contents( $f ); + if ( !$f ) { + echo( " Failed to load comment file {$f}, using default comment. " ); + } + } + } + + if ( !$commentText ) { + $commentText = $comment; + } + # Import the file - $archive = $image->publish( $file ); - if( WikiError::isError( $archive ) || !$archive->isGood() ) { - echo( "failed.\n" ); - continue; + if ( isset( $options['dry'] ) ) { + echo( " publishing {$file}... " ); + } else { + $archive = $image->publish( $file ); + if( WikiError::isError( $archive ) || !$archive->isGood() ) { + echo( "failed.\n" ); + continue; + } } $$svar++; - if ( $image->recordUpload( $archive->value, $comment, $license ) ) { + if ( isset( $options['dry'] ) ) { + echo( "done.\n" ); + } else if ( $image->recordUpload( $archive->value, $commentText, $license ) ) { # We're done! echo( "done.\n" ); } else { @@ -123,10 +158,14 @@ USAGE: php importImages.php [options] <dir> Options: --extensions=<exts> Comma-separated list of allowable extensions, defaults to \$wgFileExtensions ---overwrite Overwrite existing images if a conflicting-named image is found +--overwrite Overwrite existing images if a conflicting-named image is found --user=<username> Set username of uploader, default 'Maintenance script' --comment=<text> Set upload summary comment, default 'Importing image file' +--comment-file=<file> Set upload summary comment the the content of <file>. +--comment-ext=<ext> Causes the comment for each file to be loaded from a file with the same name + but the extension <ext>. --license=<code> Use an optional license template +--dry Dry run, don't import anything END; exit(); diff --git a/maintenance/importTextFile.php b/maintenance/importTextFile.php index 5004c3c0..0a0068d7 100644 --- a/maintenance/importTextFile.php +++ b/maintenance/importTextFile.php @@ -24,19 +24,19 @@ if( count( $args ) < 1 || isset( $options['help'] ) ) { $title = isset( $options['title'] ) ? $options['title'] : titleFromFilename( $filename ); $title = Title::newFromUrl( $title ); - echo( "\nUsing title '" . $title->getPrefixedText() . "'..." ); if( is_object( $title ) ) { + echo( "\nUsing title '" . $title->getPrefixedText() . "'..." ); if( !$title->exists() || !isset( $options['nooverwrite'] ) ) { $text = file_get_contents( $filename ); $user = isset( $options['user'] ) ? $options['user'] : 'Maintenance script'; $user = User::newFromName( $user ); - echo( "\nUsing username '" . $user->getName() . "'..." ); if( is_object( $user ) ) { + echo( "\nUsing username '" . $user->getName() . "'..." ); $wgUser =& $user; $comment = isset( $options['comment'] ) ? $options['comment'] : 'Importing text file'; $flags = 0 | ( isset( $options['norc'] ) ? EDIT_SUPPRESS_RC : 0 ); diff --git a/maintenance/interwiki.sql b/maintenance/interwiki.sql index c8e088f5..2521d381 100644 --- a/maintenance/interwiki.sql +++ b/maintenance/interwiki.sql @@ -8,22 +8,16 @@ REPLACE INTO /*$wgDBprefix*/interwiki (iw_prefix,iw_url,iw_local) VALUES ('arxiv','http://www.arxiv.org/abs/$1',0), ('c2find','http://c2.com/cgi/wiki?FindPage&value=$1',0), ('cache','http://www.google.com/search?q=cache:$1',0), -('codersbase','http://www.codersbase.com/index.php/$1',0), # 2008-02-27: Fatal error ('commons','http://commons.wikimedia.org/wiki/$1',0), ('corpknowpedia','http://corpknowpedia.org/wiki/index.php/$1',0), ('dictionary','http://www.dict.org/bin/Dict?Database=*&Form=Dict1&Strategy=*&Query=$1',0), ('disinfopedia','http://www.disinfopedia.org/wiki.phtml?title=$1',0), ('docbook','http://wiki.docbook.org/topic/$1',0), +('doi','http://dx.doi.org/$1',0), ('drumcorpswiki','http://www.drumcorpswiki.com/index.php/$1',0), ('dwjwiki','http://www.suberic.net/cgi-bin/dwj/wiki.cgi?$1',0), -('efnetceewiki','http://purl.net/wiki/c/$1',0), # 2008-02-27: does not appear to be working -('efnetcppwiki','http://purl.net/wiki/cpp/$1',0), # 2008-02-27: does not appear to be working -('efnetpythonwiki','http://purl.net/wiki/python/$1',0), # 2008-02-27: does not appear to be working -('efnetxmlwiki','http://purl.net/wiki/xml/$1',0), # 2008-02-27: does not appear to be working -('eljwiki','http://elj.sourceforge.net/phpwiki/index.php/$1',0), # 2008-02-27: Fatal PhpWiki Error ('emacswiki','http://www.emacswiki.org/cgi-bin/wiki.pl?$1',0), ('elibre','http://enciclopedia.us.es/index.php/$1',0), -('eokulturcentro','http://esperanto.toulouse.free.fr/wakka.php?wiki=$1',0), # 2007-02-27: no access to database ('foldoc','http://foldoc.org/?$1',0), ('foxwiki','http://fox.wikis.com/wc.dll?Wiki~$1',0), ('freebsdman','http://www.FreeBSD.org/cgi/man.cgi?apropos=1&query=$1',0), @@ -31,12 +25,10 @@ REPLACE INTO /*$wgDBprefix*/interwiki (iw_prefix,iw_url,iw_local) VALUES ('gentoo-wiki','http://gentoo-wiki.com/$1',0), ('google','http://www.google.com/search?q=$1',0), ('googlegroups','http://groups.google.com/groups?q=$1',0), -('gotamac','http://www.got-a-mac.org/$1',0), # 2008-02-27: appears ill maintained; loads of spambots ('hammondwiki','http://www.dairiki.org/HammondWiki/$1',0), ('hewikisource','http://he.wikisource.org/wiki/$1',1), ('hrwiki','http://www.hrwiki.org/index.php/$1',0), ('imdb','http://us.imdb.com/Title?$1',0), -('infosecpedia','http://www.infosecpedia.org/pedia/index.php/$1',0), # 2008-02-27: lot of spambot activity ('jargonfile','http://sunir.org/apps/meta.pl?wiki=JargonFile&redirect=$1',0), ('jspwiki','http://www.jspwiki.org/wiki/$1',0), ('keiki','http://kei.ki/en/$1',0), @@ -57,7 +49,6 @@ REPLACE INTO /*$wgDBprefix*/interwiki (iw_prefix,iw_url,iw_local) VALUES ('oeis','http://www.research.att.com/cgi-bin/access.cgi/as/njas/sequences/eisA.cgi?Anum=$1',0), ('openfacts','http://openfacts.berlios.de/index.phtml?title=$1',0), ('openwiki','http://openwiki.com/?$1',0), -('orgpatterns','http://www.bell-labs.com/cgi-user/OrgPatterns/OrgPatterns?$1',0), # 2008-02-27: may not be working. Please double check ('patwiki','http://gauss.ffii.org/$1',0), # 2008-02-27: lots of spambots ('pmeg','http://www.bertilow.com/pmeg/$1.php',0), ('ppr','http://c2.com/cgi/wiki?$1',0), diff --git a/maintenance/language/checkLanguage.inc b/maintenance/language/checkLanguage.inc index 2cfd1b04..52281b57 100644 --- a/maintenance/language/checkLanguage.inc +++ b/maintenance/language/checkLanguage.inc @@ -13,41 +13,36 @@ class CheckLanguageCLI { protected $checks = array(); protected $L = null; - protected $defaultChecks = array( - 'untranslated', 'obsolete', 'variables', 'empty', 'plural', - 'whitespace', 'xhtml', 'chars', 'links', 'unbalanced' - ); - protected $results = array(); private $includeExif = false; /** - * GLOBALS: $wgLanguageCode; + * Constructor. + * @param $options Options for script. */ public function __construct( Array $options ) { - if ( isset( $options['help'] ) ) { echo $this->help(); exit(); } - if ( isset($options['lang']) ) { + if ( isset( $options['lang'] ) ) { $this->code = $options['lang']; } else { global $wgLanguageCode; $this->code = $wgLanguageCode; } - if ( isset($options['level']) ) { + if ( isset( $options['level'] ) ) { $this->level = $options['level']; } - $this->doLinks = isset($options['links']); - $this->includeExif = !isset($options['noexif']); - $this->checkAll = isset($options['all']); + $this->doLinks = isset( $options['links'] ); + $this->includeExif = !isset( $options['noexif'] ); + $this->checkAll = isset( $options['all'] ); - if ( isset($options['wikilang']) ) { + if ( isset( $options['wikilang'] ) ) { $this->wikiCode = $options['wikilang']; } @@ -55,57 +50,136 @@ class CheckLanguageCLI { $this->checks = explode( ',', $options['whitelist'] ); } elseif ( isset( $options['blacklist'] ) ) { $this->checks = array_diff( - $this->defaultChecks, + isset( $options['easy'] ) ? $this->easyChecks() : $this->defaultChecks(), explode( ',', $options['blacklist'] ) ); + } elseif ( isset( $options['easy'] ) ) { + $this->checks = $this->easyChecks(); } else { - $this->checks = $this->defaultChecks; + $this->checks = $this->defaultChecks(); } - if ( isset($options['output']) ) { + if ( isset( $options['output'] ) ) { $this->output = $options['output']; } - # Some additional checks not enabled by default - if ( isset( $options['duplicate'] ) ) { - $this->checks[] = 'duplicate'; - } - $this->L = new languages( $this->includeExif ); } + /** + * Get the default checks. + * @return A list of the default checks. + */ + protected function defaultChecks() { + return array( + 'untranslated', 'duplicate', 'obsolete', 'variables', 'empty', 'plural', + 'whitespace', 'xhtml', 'chars', 'links', 'unbalanced', 'namespace', + 'projecttalk', 'magic', 'magic-old', 'magic-over', 'magic-case', + 'special', 'special-old', + ); + } + + /** + * Get the checks which check other things than messages. + * @return A list of the non-message checks. + */ + protected function nonMessageChecks() { + return array( + 'namespace', 'projecttalk', 'magic', 'magic-old', 'magic-over', + 'magic-case', 'special', 'special-old', + ); + } + + /** + * Get the checks that can easily be treated by non-speakers of the language. + * @return A list of the easy checks. + */ + protected function easyChecks() { + return array( + 'duplicate', 'obsolete', 'empty', 'whitespace', 'xhtml', 'chars', 'magic-old', + 'magic-over', 'magic-case', 'special-old', + ); + } + + /** + * Get all checks. + * @return An array of all check names mapped to their function names. + */ protected function getChecks() { - $checks = array(); - $checks['untranslated'] = 'getUntranslatedMessages'; - $checks['duplicate'] = 'getDuplicateMessages'; - $checks['obsolete'] = 'getObsoleteMessages'; - $checks['variables'] = 'getMessagesWithoutVariables'; - $checks['plural'] = 'getMessagesWithoutPlural'; - $checks['empty'] = 'getEmptyMessages'; - $checks['whitespace'] = 'getMessagesWithWhitespace'; - $checks['xhtml'] = 'getNonXHTMLMessages'; - $checks['chars'] = 'getMessagesWithWrongChars'; - $checks['links'] = 'getMessagesWithDubiousLinks'; - $checks['unbalanced'] = 'getMessagesWithUnbalanced'; - return $checks; + return array( + 'untranslated' => 'getUntranslatedMessages', + 'duplicate' => 'getDuplicateMessages', + 'obsolete' => 'getObsoleteMessages', + 'variables' => 'getMessagesWithMismatchVariables', + 'plural' => 'getMessagesWithoutPlural', + 'empty' => 'getEmptyMessages', + 'whitespace' => 'getMessagesWithWhitespace', + 'xhtml' => 'getNonXHTMLMessages', + 'chars' => 'getMessagesWithWrongChars', + 'links' => 'getMessagesWithDubiousLinks', + 'unbalanced' => 'getMessagesWithUnbalanced', + 'namespace' => 'getUntranslatedNamespaces', + 'projecttalk' => 'getProblematicProjectTalks', + 'magic' => 'getUntranslatedMagicWords', + 'magic-old' => 'getObsoleteMagicWords', + 'magic-over' => 'getOverridingMagicWords', + 'magic-case' => 'getCaseMismatchMagicWords', + 'special' => 'getUntraslatedSpecialPages', + 'special-old' => 'getObsoleteSpecialPages', + ); + } + + /** + * Get total count for each check non-messages check. + * @return An array of all check names mapped to a two-element array: + * function name to get the total count and language code or null + * for checked code. + */ + protected function getTotalCount() { + return array( + 'namespace' => array( 'getNamespaceNames', 'en' ), + 'projecttalk' => null, + 'magic' => array( 'getMagicWords', 'en' ), + 'magic-old' => array( 'getMagicWords', null ), + 'magic-over' => array( 'getMagicWords', null ), + 'magic-case' => array( 'getMagicWords', null ), + 'special' => array( 'getSpecialPageAliases', 'en' ), + 'special-old' => array( 'getSpecialPageAliases', null ), + ); } + /** + * Get all check descriptions. + * @return An array of all check names mapped to their descriptions. + */ protected function getDescriptions() { - $descriptions = array(); - $descriptions['untranslated'] = '$1 message(s) of $2 are not translated to $3, but exist in en:'; - $descriptions['duplicate'] = '$1 message(s) of $2 are translated the same in en and $3:'; - $descriptions['obsolete'] = '$1 message(s) of $2 do not exist in en or are in the ignore list, but are in $3'; - $descriptions['variables'] = '$1 message(s) of $2 in $3 don\'t use some variables that en uses:'; - $descriptions['plural'] = '$1 message(s) of $2 in $3 don\'t use {{plural}} while en uses:'; - $descriptions['empty'] = '$1 message(s) of $2 in $3 are empty or -:'; - $descriptions['whitespace'] = '$1 message(s) of $2 in $3 have trailing whitespace:'; - $descriptions['xhtml'] = '$1 message(s) of $2 in $3 contain illegal XHTML:'; - $descriptions['chars'] = '$1 message(s) of $2 in $3 include hidden chars which should not be used in the messages:'; - $descriptions['links'] = '$1 message(s) of $2 in $3 have problematic link(s):'; - $descriptions['unbalanced'] = '$1 message(s) of $2 in $3 have unbalanced {[]}:'; - return $descriptions; + return array( + 'untranslated' => '$1 message(s) of $2 are not translated to $3, but exist in en:', + 'duplicate' => '$1 message(s) of $2 are translated the same in en and $3:', + 'obsolete' => '$1 message(s) of $2 do not exist in en or are in the ignore list, but exist in $3:', + 'variables' => '$1 message(s) of $2 in $3 don\'t match the variables used in en:', + 'plural' => '$1 message(s) of $2 in $3 don\'t use {{plural}} while en uses:', + 'empty' => '$1 message(s) of $2 in $3 are empty or -:', + 'whitespace' => '$1 message(s) of $2 in $3 have trailing whitespace:', + 'xhtml' => '$1 message(s) of $2 in $3 contain illegal XHTML:', + 'chars' => '$1 message(s) of $2 in $3 include hidden chars which should not be used in the messages:', + 'links' => '$1 message(s) of $2 in $3 have problematic link(s):', + 'unbalanced' => '$1 message(s) of $2 in $3 have unbalanced {[]}:', + 'namespace' => '$1 namespace name(s) of $2 are not translated to $3, but exist in en:', + 'projecttalk' => '$1 namespace name(s) and alias(es) in $3 are project talk namespaces without the parameter:', + 'magic' => '$1 magic word(s) of $2 are not translated to $3, but exist in en:', + 'magic-old' => '$1 magic word(s) of $2 do not exist in en, but exist in $3:', + 'magic-over' => '$1 magic word(s) of $2 in $3 do not contain the original en word(s):', + 'magic-case' => '$1 magic word(s) of $2 in $3 change the case-sensitivity of the original en word:', + 'special' => '$1 special page alias(es) of $2 are not translated to $3, but exist in en:', + 'special-old' => '$1 special page alias(es) of $2 do not exist in en, but exist in $3:', + ); } + /** + * Get help. + * @return The help string. + */ protected function help() { return <<<ENDS Run this script to check a specific language file, or all of them. @@ -114,24 +188,32 @@ Parameters: * lang: Language code (default: the installation default language). * all: Check all customized languages. * help: Show this help. - * level: Show the following level (default: 2). + * level: Show the following display level (default: 2). * links: Link the message values (default off). * wikilang: For the links, what is the content language of the wiki to display the output in (default en). * whitelist: Do only the following checks (form: code,code). * blacklist: Don't do the following checks (form: code,code). - * duplicate: Additionally check for messages which are translated the same to English (default off). + * easy: Do only the easy checks, which can be treated by non-speakers of the language. * noexif: Don't check for EXIF messages (a bit hard and boring to translate), if you know that they are currently not translated and want to focus on other problems (default off). -Check codes (ideally, all of them should result 0; all the checks are executed by default (except duplicate and language specific check blacklists in checkLanguage.inc): +Check codes (ideally, all of them should result 0; all the checks are executed by default (except language-specific check blacklists in checkLanguage.inc): * untranslated: Messages which are required to translate, but are not translated. * duplicate: Messages which translation equal to fallback - * obsolete: Messages which are untranslatable, but translated. - * variables: Messages without variables which should be used. - * empty: Empty messages. + * obsolete: Messages which are untranslatable or do not exist, but are translated. + * variables: Messages without variables which should be used, or with variables which shouldn't be used. + * empty: Empty messages and messages that contain only -. * whitespace: Messages which have trailing whitespace. * xhtml: Messages which are not well-formed XHTML (checks only few common errors). * chars: Messages with hidden characters. * links: Messages which contains broken links to pages (does not find all). * unbalanced: Messages which contains unequal numbers of opening {[ and closing ]}. + * namespace: Namespace names that were not translated. + * projecttalk: Namespace names and aliases where the project talk does not contain $1. + * magic: Magic words that were not translated. + * magic-old: Magic words which do not exist. + * magic-over: Magic words that override the original English word. + * magic-case: Magic words whose translation changes the case-sensitivity of the original English word. + * special: Special page names that were not translated. + * special-old: Special page names which do not exist. Display levels (default: 2): * 0: Skip the checks (useful for checking syntax). * 1: Show only the stub headers and number of wrong messages, without list of messages. @@ -141,10 +223,13 @@ Display levels (default: 2): ENDS; } + /** + * Execute the script. + */ public function execute() { $this->doChecks(); if ( $this->level > 0 ) { - switch ($this->output) { + switch ( $this->output ) { case 'plain': $this->outputText(); break; @@ -152,11 +237,14 @@ ENDS; $this->outputWiki(); break; default: - throw new MWException( "Invalid output type $this->output"); + throw new MWException( "Invalid output type $this->output" ); } } } + /** + * Execute the checks. + */ protected function doChecks() { $ignoredCodes = array( 'en', 'enRTL' ); @@ -164,24 +252,33 @@ ENDS; # Check the language if ( $this->checkAll ) { foreach ( $this->L->getLanguages() as $language ) { - if ( !in_array($language, $ignoredCodes) ) { + if ( !in_array( $language, $ignoredCodes ) ) { $this->results[$language] = $this->checkLanguage( $language ); } } } else { - if ( in_array($this->code, $ignoredCodes) ) { - throw new MWException("Cannot check code $this->code."); + if ( in_array( $this->code, $ignoredCodes ) ) { + throw new MWException( "Cannot check code $this->code." ); } else { $this->results[$this->code] = $this->checkLanguage( $this->code ); } } } + /** + * Get the check blacklist. + * @return The list of checks which should not be executed. + */ protected function getCheckBlacklist() { global $checkBlacklist; return $checkBlacklist; } + /** + * Check a language. + * @param $code The language code. + * @return The results. + */ protected function checkLanguage( $code ) { # Syntax check only if ( $this->level === 0 ) { @@ -193,22 +290,28 @@ ENDS; $checkFunctions = $this->getChecks(); $checkBlacklist = $this->getCheckBlacklist(); foreach ( $this->checks as $check ) { - if ( isset($checkBlacklist[$code]) && - in_array($check, $checkBlacklist[$code]) ) { + if ( isset( $checkBlacklist[$code] ) && + in_array( $check, $checkBlacklist[$code] ) ) { $result[$check] = array(); continue; } $callback = array( $this->L, $checkFunctions[$check] ); - if ( !is_callable($callback ) ) { + if ( !is_callable( $callback ) ) { throw new MWException( "Unkown check $check." ); } - $results[$check] = call_user_func( $callback , $code ); + $results[$check] = call_user_func( $callback, $code ); } return $results; } + /** + * Format a message key. + * @param $key The message key. + * @param $code The language code. + * @return The formatted message key. + */ protected function formatKey( $key, $code ) { if ( $this->doLinks ) { $displayKey = ucfirst( $key ); @@ -222,28 +325,44 @@ ENDS; } } + /** + * Output the checks results as plain text. + * @return The checks results as plain text. + */ protected function outputText() { foreach ( $this->results as $code => $results ) { $translated = $this->L->getMessages( $code ); $translated = count( $translated['translated'] ); - $translatable = $this->L->getGeneralMessages(); - $translatable = count( $translatable['translatable'] ); foreach ( $results as $check => $messages ) { $count = count( $messages ); if ( $count ) { + if ( $check == 'untranslated' ) { + $translatable = $this->L->getGeneralMessages(); + $total = count( $translatable['translatable'] ); + } elseif ( in_array( $check, $this->nonMessageChecks() ) ) { + $totalCount = $this->getTotalCount(); + $totalCount = $totalCount[$check]; + $callback = array( $this->L, $totalCount[0] ); + $callCode = $totalCount[1] ? $totalCount[1] : $code; + $total = count( call_user_func( $callback, $callCode ) ); + } else { + $total = $translated; + } $search = array( '$1', '$2', '$3' ); - $replace = array( $count, $check == 'untranslated' ? $translatable: $translated, $code ); + $replace = array( $count, $total, $code ); $descriptions = $this->getDescriptions(); echo "\n" . str_replace( $search, $replace, $descriptions[$check] ) . "\n"; if ( $this->level == 1 ) { echo "[messages are hidden]\n"; } else { foreach ( $messages as $key => $value ) { - $displayKey = $this->formatKey( $key, $code ); - if ( $this->level == 2 ) { - echo "* $displayKey\n"; + if( !in_array( $check, $this->nonMessageChecks() ) ) { + $key = $this->formatKey( $key, $code ); + } + if ( $this->level == 2 || empty( $value ) ) { + echo "* $key\n"; } else { - echo "* $displayKey: '$value'\n"; + echo "* $key: '$value'\n"; } } } @@ -253,7 +372,8 @@ ENDS; } /** - * Globals: $wgContLang, $IP + * Output the checks results as wiki text. + * @return The checks results as wiki text. */ function outputWiki() { global $wgContLang, $IP; @@ -265,6 +385,9 @@ ENDS; $problems = 0; $detailTextForLangChecks = array(); foreach ( $results as $check => $messages ) { + if( in_array( $check, $this->nonMessageChecks() ) ) { + continue; + } $count = count( $messages ); if ( $count ) { $problems += $count; @@ -273,7 +396,7 @@ ENDS; $displayKey = $this->formatKey( $key, $code ); $messageDetails[] = $displayKey; } - $detailTextForLangChecks[] = "===$code-$check===\n* " . implode( ', ', $messageDetails ); + $detailTextForLangChecks[] = "=== $code-$check ===\n* " . implode( ', ', $messageDetails ); $numbers[] = "'''[[#$code-$check|$count]]'''"; } else { $numbers[] = $count; @@ -285,7 +408,10 @@ ENDS; $detailText .= $detailTextForLang . implode( "\n", $detailTextForLangChecks ) . "\n"; } - if ( !$problems ) { continue; } // Don't list languages without problems + if ( !$problems ) { + # Don't list languages without problems + continue; + } $language = $wgContLang->getLanguageName( $code ); $rows[] = "| $language || $code || $problems || " . implode( ' || ', $numbers ); } @@ -297,7 +423,7 @@ ENDS; '''Check results are for:''' <code>$version</code> -{| class="sortable wikitable" border="2" cellpadding="4" cellspacing="0" style="background-color: #F9F9F9; border: 1px #AAAAAA solid; border-collapse: collapse; clear:both;" +{| class="sortable wikitable" border="2" cellpadding="4" cellspacing="0" style="background-color: #F9F9F9; border: 1px #AAAAAA solid; border-collapse: collapse; clear: both;" $tableRows |} @@ -306,46 +432,50 @@ $detailText EOL; } + /** + * Check if there are any results for the checks, in any language. + * @return True if there are any results, false if not. + */ protected function isEmpty() { - $empty = true; foreach( $this->results as $code => $results ) { foreach( $results as $check => $messages ) { if( !empty( $messages ) ) { - $empty = false; - break; + return false; } } - if( !$empty ) { - break; - } } - return $empty; + return true; } } class CheckExtensionsCLI extends CheckLanguageCLI { private $extensions; + /** + * Constructor. + * @param $options Options for script. + * @param $extension The extension name (or names). + */ public function __construct( Array $options, $extension ) { if ( isset( $options['help'] ) ) { echo $this->help(); exit(); } - if ( isset($options['lang']) ) { + if ( isset( $options['lang'] ) ) { $this->code = $options['lang']; } else { global $wgLanguageCode; $this->code = $wgLanguageCode; } - if ( isset($options['level']) ) { + if ( isset( $options['level'] ) ) { $this->level = $options['level']; } - $this->doLinks = isset($options['links']); + $this->doLinks = isset( $options['links'] ); - if ( isset($options['wikilang']) ) { + if ( isset( $options['wikilang'] ) ) { $this->wikiCode = $options['wikilang']; } @@ -353,14 +483,16 @@ class CheckExtensionsCLI extends CheckLanguageCLI { $this->checks = explode( ',', $options['whitelist'] ); } elseif ( isset( $options['blacklist'] ) ) { $this->checks = array_diff( - $this->defaultChecks, + isset( $options['easy'] ) ? $this->easyChecks() : $this->defaultChecks(), explode( ',', $options['blacklist'] ) ); + } elseif ( isset( $options['easy'] ) ) { + $this->checks = $this->easyChecks(); } else { - $this->checks = $this->defaultChecks; + $this->checks = $this->defaultChecks(); } - if ( isset($options['output']) ) { + if ( isset( $options['output'] ) ) { $this->output = $options['output']; } @@ -372,23 +504,29 @@ class CheckExtensionsCLI extends CheckLanguageCLI { $this->extensions = array(); $extensions = new PremadeMediawikiExtensionGroups(); $extensions->addAll(); - if( $extension == 'all' ) { - foreach( MessageGroups::singleton()->getGroups() as $group ) { - if( strpos( $group->getId(), 'ext-' ) === 0 && !$group->isMeta() ) { + if ( $extension == 'all' ) { + foreach ( MessageGroups::singleton()->getGroups() as $group ) { + if ( strpos( $group->getId(), 'ext-' ) === 0 && !$group->isMeta() ) { $this->extensions[] = new extensionLanguages( $group ); } } - } elseif( $extension == 'wikimedia' ) { + } elseif ( $extension == 'wikimedia' ) { $wikimedia = MessageGroups::getGroup( 'ext-0-wikimedia' ); - foreach( $wikimedia->wmfextensions() as $extension ) { + foreach ( $wikimedia->wmfextensions() as $extension ) { $group = MessageGroups::getGroup( $extension ); $this->extensions[] = new extensionLanguages( $group ); } + } elseif ( $extension == 'flaggedrevs' ) { + foreach ( MessageGroups::singleton()->getGroups() as $group ) { + if ( strpos( $group->getId(), 'ext-flaggedrevs-' ) === 0 && !$group->isMeta() ) { + $this->extensions[] = new extensionLanguages( $group ); + } + } } else { $extensions = explode( ',', $extension ); - foreach( $extensions as $extension ) { + foreach ( $extensions as $extension ) { $group = MessageGroups::getGroup( 'ext-' . $extension ); - if( $group ) { + if ( $group ) { $extension = new extensionLanguages( $group ); $this->extensions[] = $extension; } else { @@ -398,25 +536,58 @@ class CheckExtensionsCLI extends CheckLanguageCLI { } } + /** + * Get the default checks. + * @return A list of the default checks. + */ + protected function defaultChecks() { + return array( + 'untranslated', 'duplicate', 'obsolete', 'variables', 'empty', 'plural', + 'whitespace', 'xhtml', 'chars', 'links', 'unbalanced', + ); + } + + /** + * Get the checks which check other things than messages. + * @return A list of the non-message checks. + */ + protected function nonMessageChecks() { + return array(); + } + + /** + * Get the checks that can easily be treated by non-speakers of the language. + * @return A list of the easy checks. + */ + protected function easyChecks() { + return array( + 'duplicate', 'obsolete', 'empty', 'whitespace', 'xhtml', 'chars', + ); + } + + /** + * Get help. + * @return The help string. + */ protected function help() { return <<<ENDS Run this script to check the status of a specific language in extensions, or all of them. Command line settings are in form --parameter[=value], except for the first one. Parameters: - * First parameter (mandatory): Extension name, multiple extension names (separated by commas), "all" for all the extensions or "wikimedia" for extensions used by Wikimedia. + * First parameter (mandatory): Extension name, multiple extension names (separated by commas), "all" for all the extensions, "wikimedia" for extensions used by Wikimedia or "flaggedrevs" for all FLaggedRevs extension messages. * lang: Language code (default: the installation default language). * help: Show this help. - * level: Show the following level (default: 2). + * level: Show the following display level (default: 2). * links: Link the message values (default off). * wikilang: For the links, what is the content language of the wiki to display the output in (default en). * whitelist: Do only the following checks (form: code,code). * blacklist: Do not perform the following checks (form: code,code). - * duplicate: Additionally check for messages which are translated the same to English (default off). -Check codes (ideally, all of them should result 0; all the checks are executed by default (except duplicate and language specific check blacklists in checkLanguage.inc): + * easy: Do only the easy checks, which can be treated by non-speakers of the language. +Check codes (ideally, all of them should result 0; all the checks are executed by default (except language-specific check blacklists in checkLanguage.inc): * untranslated: Messages which are required to translate, but are not translated. * duplicate: Messages which translation equal to fallback * obsolete: Messages which are untranslatable, but translated. - * variables: Messages without variables which should be used. + * variables: Messages without variables which should be used, or with variables which shouldn't be used. * empty: Empty messages. * whitespace: Messages which have trailing whitespace. * xhtml: Messages which are not well-formed XHTML (checks only few common errors). @@ -432,10 +603,17 @@ Display levels (default: 2): ENDS; } + /** + * Execute the script. + */ public function execute() { $this->doChecks(); } + /** + * Check a language and show the results. + * @param $code The language code. + */ protected function checkLanguage( $code ) { foreach( $this->extensions as $extension ) { $this->L = $extension; diff --git a/maintenance/language/checkLanguage.php b/maintenance/language/checkLanguage.php index f8553a1e..7a4d3dd2 100644 --- a/maintenance/language/checkLanguage.php +++ b/maintenance/language/checkLanguage.php @@ -11,4 +11,9 @@ require_once( 'checkLanguage.inc' ); require_once( 'languages.inc' ); $cli = new CheckLanguageCLI( $options ); -$cli->execute(); + +try { + $cli->execute(); +} catch( MWException $e ) { + print 'Error: ' . $e->getMessage() . "\n"; +} diff --git a/maintenance/language/countMessages.php b/maintenance/language/countMessages.php new file mode 100644 index 00000000..7d16915a --- /dev/null +++ b/maintenance/language/countMessages.php @@ -0,0 +1,40 @@ +<?php + +require_once( dirname(__FILE__).'/../commandLine.inc' ); + +global $IP; + +if ( !isset( $args[0] ) ) { + $dir = "$IP/languages/messages"; +} else { + $dir = $args[0]; +} + +$total = 0; +$nonZero = 0; +foreach ( glob( "$dir/*.php" ) as $file ) { + $baseName = basename( $file ); + if( !preg_match( '/Messages([A-Z][a-z_]+)\.php$/', $baseName, $m ) ) { + continue; + } + $code = str_replace( '_', '-', strtolower( $m[1] ) ); + $numMessages = wfGetNumMessages( $file ); + //print "$code: $numMessages\n"; + $total += $numMessages; + if ( $numMessages > 0 ) { + $nonZero ++; + } +} +print "\nTotal: $total\n"; +print "Languages: $nonZero\n"; + +function wfGetNumMessages( $file ) { + // Separate function to limit scope + require( $file ); + if ( isset( $messages ) ) { + return count( $messages ); + } else { + return 0; + } +} + diff --git a/maintenance/language/diffLanguage.php b/maintenance/language/diffLanguage.php index 389b01d5..9d395b3c 100644 --- a/maintenance/language/diffLanguage.php +++ b/maintenance/language/diffLanguage.php @@ -84,7 +84,6 @@ function getMediawikiMessages($languageCode = 'En') { $langFile = $IP.'/languages/classes/Language'.$languageCode.'.php'; if (file_exists( $langFile ) ) { print "Including $langFile\n"; - global $wgNamespaceNamesEn; // potentially unused global declaration? include($langFile); } else wfDie("ERROR: The file $langFile does not exist !\n"); } diff --git a/maintenance/language/languages.inc b/maintenance/language/languages.inc index 6d16f80c..6159e844 100644 --- a/maintenance/language/languages.inc +++ b/maintenance/language/languages.inc @@ -11,12 +11,18 @@ */ class languages { protected $mLanguages; # List of languages + protected $mRawMessages; # Raw list of the messages in each language protected $mMessages; # Messages in each language (except for English), divided to groups protected $mGeneralMessages; # General messages in English, divided to groups protected $mIgnoredMessages; # All the messages which should be exist only in the English file protected $mOptionalMessages; # All the messages which may be translated or not, depending on the language + protected $mNamespaceNames; # Namespace names + protected $mNamespaceAliases; # Namespace aliases + protected $mMagicWords; # Magic words + protected $mSpecialPageAliases; # Special page aliases + /** * Load the list of languages: all the Messages*.php * files in the languages directory. @@ -64,24 +70,41 @@ class languages { } /** - * Load the raw messages for a specific language from the messages file. + * Load the language file. * * @param $code The language code. */ - protected function loadRawMessages( $code ) { - if ( isset( $this->mRawMessages[$code] ) ) { + protected function loadFile( $code ) { + if ( isset( $this->mRawMessages[$code] ) && + isset( $this->mNamespaceNames[$code] ) && + isset( $this->mNamespaceAliases[$code] ) && + isset( $this->mMagicWords[$code] ) && + isset( $this->mSpecialPageAliases[$code] ) ) { return; } + $this->mRawMessages[$code] = array(); + $this->mNamespaceNames[$code] = array(); + $this->mNamespaceAliases[$code] = array(); + $this->mMagicWords[$code] = array(); + $this->mSpecialPageAliases[$code] = array(); $filename = Language::getMessagesFileName( $code ); if ( file_exists( $filename ) ) { require( $filename ); if ( isset( $messages ) ) { $this->mRawMessages[$code] = $messages; - } else { - $this->mRawMessages[$code] = array(); } - } else { - $this->mRawMessages[$code] = array(); + if ( isset( $namespaceNames ) ) { + $this->mNamespaceNames[$code] = $namespaceNames; + } + if ( isset( $namespaceAliases ) ) { + $this->mNamespaceAliases[$code] = $namespaceAliases; + } + if ( isset( $magicWords ) ) { + $this->mMagicWords[$code] = $magicWords; + } + if ( isset( $specialPageAliases ) ) { + $this->mSpecialPageAliases[$code] = $specialPageAliases; + } } } @@ -90,7 +113,7 @@ class languages { * all - all the messages. * required - messages which should be translated in order to get a complete translation. * optional - messages which can be translated, the fallback translation is used if not translated. - * obsolete - messages which should not be translated, either because they are not exist, or they are ignored messages. + * obsolete - messages which should not be translated, either because they do not exist, or they are ignored messages. * translated - messages which are either required or optional, but translated from English and needed. * * @param $code The language code. @@ -99,7 +122,7 @@ class languages { if ( isset( $this->mMessages[$code] ) ) { return; } - $this->loadRawMessages( $code ); + $this->loadFile( $code ); $this->loadGeneralMessages(); $this->mMessages[$code]['all'] = $this->mRawMessages[$code]; $this->mMessages[$code]['required'] = array(); @@ -131,7 +154,7 @@ class languages { if ( isset( $this->mGeneralMessages ) ) { return; } - $this->loadRawMessages( 'en' ); + $this->loadFile( 'en' ); $this->mGeneralMessages['all'] = $this->mRawMessages['en']; $this->mGeneralMessages['required'] = array(); $this->mGeneralMessages['optional'] = array(); @@ -156,7 +179,7 @@ class languages { * all - all the messages. * required - messages which should be translated in order to get a complete translation. * optional - messages which can be translated, the fallback translation is used if not translated. - * obsolete - messages which should not be translated, either because they are not exist, or they are ignored messages. + * obsolete - messages which should not be translated, either because they do not exist, or they are ignored messages. * translated - messages which are either required or optional, but translated from English and needed. * * @param $code The language code. @@ -184,6 +207,54 @@ class languages { } /** + * Get namespace names for a specific language. + * + * @param $code The language code. + * + * @return Namespace names. + */ + public function getNamespaceNames( $code ) { + $this->loadFile( $code ); + return $this->mNamespaceNames[$code]; + } + + /** + * Get namespace aliases for a specific language. + * + * @param $code The language code. + * + * @return Namespace aliases. + */ + public function getNamespaceAliases( $code ) { + $this->loadFile( $code ); + return $this->mNamespaceAliases[$code]; + } + + /** + * Get magic words for a specific language. + * + * @param $code The language code. + * + * @return Magic words. + */ + public function getMagicWords( $code ) { + $this->loadFile( $code ); + return $this->mMagicWords[$code]; + } + + /** + * Get special page aliases for a specific language. + * + * @param $code The language code. + * + * @return Special page aliases. + */ + public function getSpecialPageAliases( $code ) { + $this->loadFile( $code ); + return $this->mSpecialPageAliases[$code]; + } + + /** * Get the untranslated messages for a specific language. * * @param $code The language code. @@ -193,13 +264,7 @@ class languages { public function getUntranslatedMessages( $code ) { $this->loadGeneralMessages(); $this->loadMessages( $code ); - $requiredGeneralMessages = array_keys( $this->mGeneralMessages['required'] ); - $requiredMessages = array_keys( $this->mMessages[$code]['required'] ); - $untranslatedMessages = array(); - foreach ( array_diff( $requiredGeneralMessages, $requiredMessages ) as $key ) { - $untranslatedMessages[$key] = $this->mGeneralMessages['required'][$key]; - } - return $untranslatedMessages; + return array_diff_key( $this->mGeneralMessages['required'], $this->mMessages[$code]['required'] ); } /** @@ -221,6 +286,13 @@ class languages { return $duplicateMessages; } + /** + * Get the obsolete messages for a specific language. + * + * @param $code The language code. + * + * @return The obsolete messages for this language. + */ public function getObsoleteMessages( $code ) { $this->loadGeneralMessages(); $this->loadMessages( $code ); @@ -228,17 +300,17 @@ class languages { } /** - * Get the messages which do not use some variables. + * Get the messages whose variables do not match the original ones. * * @param $code The language code. * - * @return The messages which do not use some variables in this language. + * @return The messages whose variables do not match the original ones. */ - public function getMessagesWithoutVariables( $code ) { + public function getMessagesWithMismatchVariables( $code ) { $this->loadGeneralMessages(); $this->loadMessages( $code ); $variables = array( '\$1', '\$2', '\$3', '\$4', '\$5', '\$6', '\$7', '\$8', '\$9' ); - $messagesWithoutVariables = array(); + $mismatchMessages = array(); foreach ( $this->mMessages[$code]['translated'] as $key => $value ) { $missing = false; foreach ( $variables as $var ) { @@ -246,12 +318,16 @@ class languages { !preg_match( "/$var/sU", $value ) ) { $missing = true; } + if ( !preg_match( "/$var/sU", $this->mGeneralMessages['translatable'][$key] ) && + preg_match( "/$var/sU", $value ) ) { + $missing = true; + } } if ( $missing ) { - $messagesWithoutVariables[$key] = $value; + $mismatchMessages[$key] = $value; } } - return $messagesWithoutVariables; + return $mismatchMessages; } /** @@ -376,6 +452,13 @@ class languages { return $wrongCharsMessages; } + /** + * Get the messages which include dubious links. + * + * @param $code The language code. + * + * @return The messages which include dubious links in this language. + */ public function getMessagesWithDubiousLinks( $code ) { $this->loadGeneralMessages(); $this->loadMessages( $code ); @@ -383,9 +466,9 @@ class languages { $messages = array(); foreach ( $this->mMessages[$code]['translated'] as $key => $value ) { $matches = array(); - preg_match_all( "/\[\[([{$tc}]+)(?:\\|(.+?))?]]/sDu", $value, $matches); + preg_match_all( "/\[\[([{$tc}]+)(?:\\|(.+?))?]]/sDu", $value, $matches ); for ($i = 0; $i < count($matches[0]); $i++ ) { - if ( preg_match( "/.*project.*/isDu", $matches[1][$i]) ) { + if ( preg_match( "/.*project.*/isDu", $matches[1][$i] ) ) { $messages[$key][] = $matches[0][$i]; } } @@ -398,19 +481,33 @@ class languages { return $messages; } + /** + * Get the messages which include unbalanced brackets. + * + * @param $code The language code. + * + * @return The messages which include unbalanced brackets in this language. + */ public function getMessagesWithUnbalanced( $code ) { $this->loadGeneralMessages(); $this->loadMessages( $code ); $messages = array(); foreach ( $this->mMessages[$code]['translated'] as $key => $value ) { - $a = $b = $c = $d = 0; - foreach ( preg_split('//', $value) as $char ) { - switch ($char) { - case '[': $a++; break; - case ']': $b++; break; - case '{': $c++; break; - case '}': $d++; break; + foreach ( preg_split( '//', $value ) as $char ) { + switch ( $char ) { + case '[': + $a++; + break; + case ']': + $b++; + break; + case '{': + $c++; + break; + case '}': + $d++; + break; } } @@ -422,6 +519,175 @@ class languages { return $messages; } + /** + * Get the untranslated namespace names. + * + * @param $code The language code. + * + * @return The untranslated namespace names in this language. + */ + public function getUntranslatedNamespaces( $code ) { + $this->loadFile( 'en' ); + $this->loadFile( $code ); + return array_flip( array_diff_key( $this->mNamespaceNames['en'], $this->mNamespaceNames[$code] ) ); + } + + /** + * Get the project talk namespace names with no $1. + * + * @param $code The language code. + * + * @return The problematic project talk namespaces in this language. + */ + public function getProblematicProjectTalks( $code ) { + $this->loadFile( $code ); + $namespaces = array(); + + # Check default namespace name + if( isset( $this->mNamespaceNames[$code][NS_PROJECT_TALK] ) ) { + $default = $this->mNamespaceNames[$code][NS_PROJECT_TALK]; + if ( strpos( $default, '$1' ) === FALSE ) { + $namespaces[$default] = 'default'; + } + } + + # Check namespace aliases + foreach( $this->mNamespaceAliases[$code] as $key => $value ) { + if ( $value == NS_PROJECT_TALK && strpos( $key, '$1' ) === FALSE ) { + $namespaces[$key] = ''; + } + } + + return $namespaces; + } + + /** + * Get the untranslated magic words. + * + * @param $code The language code. + * + * @return The untranslated magic words in this language. + */ + public function getUntranslatedMagicWords( $code ) { + $this->loadFile( 'en' ); + $this->loadFile( $code ); + $magicWords = array(); + foreach ( $this->mMagicWords['en'] as $key => $value ) { + if ( !isset( $this->mMagicWords[$code][$key] ) ) { + $magicWords[$key] = $value[1]; + } + } + return $magicWords; + } + + /** + * Get the obsolete magic words. + * + * @param $code The language code. + * + * @return The obsolete magic words in this language. + */ + public function getObsoleteMagicWords( $code ) { + $this->loadFile( 'en' ); + $this->loadFile( $code ); + $magicWords = array(); + foreach ( $this->mMagicWords[$code] as $key => $value ) { + if ( !isset( $this->mMagicWords['en'][$key] ) ) { + $magicWords[$key] = $value[1]; + } + } + return $magicWords; + } + + /** + * Get the magic words that override the original English magic word. + * + * @param $code The language code. + * + * @return The overriding magic words in this language. + */ + public function getOverridingMagicWords( $code ) { + $this->loadFile( 'en' ); + $this->loadFile( $code ); + $magicWords = array(); + foreach ( $this->mMagicWords[$code] as $key => $local ) { + if ( !isset( $this->mMagicWords['en'][$key] ) ) { + # Unrecognized magic word + continue; + } + $en = $this->mMagicWords['en'][$key]; + array_shift( $local ); + array_shift( $en ); + foreach ( $en as $word ) { + if ( !in_array( $word, $local ) ) { + $magicWords[$key] = $word; + break; + } + } + } + return $magicWords; + } + + /** + * Get the magic words which do not match the case-sensitivity of the original words. + * + * @param $code The language code. + * + * @return The magic words whose case does not match in this language. + */ + public function getCaseMismatchMagicWords( $code ) { + $this->loadFile( 'en' ); + $this->loadFile( $code ); + $magicWords = array(); + foreach ( $this->mMagicWords[$code] as $key => $local ) { + if ( !isset( $this->mMagicWords['en'][$key] ) ) { + # Unrecognized magic word + continue; + } + if ( $local[0] != $this->mMagicWords['en'][$key][0] ) { + $magicWords[$key] = $local[0]; + } + } + return $magicWords; + } + + /** + * Get the untranslated special page names. + * + * @param $code The language code. + * + * @return The untranslated special page names in this language. + */ + public function getUntraslatedSpecialPages( $code ) { + $this->loadFile( 'en' ); + $this->loadFile( $code ); + $specialPageAliases = array(); + foreach ( $this->mSpecialPageAliases['en'] as $key => $value ) { + if ( !isset( $this->mSpecialPageAliases[$code][$key] ) ) { + $specialPageAliases[$key] = $value[0]; + } + } + return $specialPageAliases; + } + + /** + * Get the obsolete special page names. + * + * @param $code The language code. + * + * @return The obsolete special page names in this language. + */ + public function getObsoleteSpecialPages( $code ) { + $this->loadFile( 'en' ); + $this->loadFile( $code ); + $specialPageAliases = array(); + foreach ( $this->mSpecialPageAliases[$code] as $key => $value ) { + if ( !isset( $this->mSpecialPageAliases['en'][$key] ) ) { + $specialPageAliases[$key] = $value[0]; + } + } + return $specialPageAliases; + } } class extensionLanguages extends languages { @@ -449,11 +715,11 @@ class extensionLanguages extends languages { } /** - * Load the raw messages for a specific language. + * Load the language file. * * @param $code The language code. */ - protected function loadRawMessages( $code ) { + protected function loadFile( $code ) { if( !isset( $this->mRawMessages[$code] ) ) { $this->mRawMessages[$code] = $this->mMessageGroup->load( $code ); if( empty( $this->mRawMessages[$code] ) ) { diff --git a/maintenance/language/messageTypes.inc b/maintenance/language/messageTypes.inc index 67caaddd..1b95fe98 100644 --- a/maintenance/language/messageTypes.inc +++ b/maintenance/language/messageTypes.inc @@ -65,11 +65,13 @@ $wgIgnoredMessages = array( 'accesskey-preview', 'accesskey-diff', 'accesskey-compareselectedversions', + 'accesskey-visualcomparison', 'accesskey-watch', 'accesskey-upload', 'addsection', 'anonnotice', 'autoblock_whitelist', + 'searchmenu-help', 'googlesearch', 'opensearch-desc', 'exif-make-value', @@ -83,7 +85,7 @@ $wgIgnoredMessages = array( 'markaspatrolledlink', 'newarticletextanon', 'newsectionheaderdefaultlevel', - 'newtalkseperator', + 'newtalkseparator', 'noarticletextanon', 'number_of_watching_users_RCview', 'pagecategorieslink', @@ -103,6 +105,7 @@ $wgIgnoredMessages = array( 'sitetitle', 'sp-contributions-footer', 'sp-contributions-footer-anon', + 'statistics-summary', 'statistics-footer', 'talkpagetext', 'trackback', @@ -127,6 +130,7 @@ $wgIgnoredMessages = array( 'uncategorizedtemplates-summary', 'popularpages-summary', 'wantedcategories-summary', + 'wantedfiles-summary', 'wantedpages-summary', 'mostlinked-summary', 'mostlinkedcategories-summary', @@ -148,8 +152,11 @@ $wgIgnoredMessages = array( 'lonelypages-summary', 'unusedtemplates-summary', 'fewestrevisions-summary', - 'missingfiles-summary', 'upload-summary', + 'pagetitle-view-mainpage', + 'newuserlogentry', + 'restrictlogpage', + 'wantedtemplates-summary', ); /** Optional messages, which may be translated only if changed in the target language. */ @@ -162,6 +169,7 @@ $wgOptionalMessages = array( 'unit-pixel', 'userrights-irreversible-marker', 'tog-nolangconversion', + 'tog-noconvertlink', 'yourvariant', 'variantname-zh-hans', 'variantname-zh-hant', @@ -192,6 +200,14 @@ $wgOptionalMessages = array( 'resetpass_text', 'image_sample', 'media_sample', + 'skinname-standard', + 'skinname-nostalgia', + 'skinname-cologneblue', + 'skinname-monobook', + 'skinname-myskin', + 'skinname-chick', + 'skinname-simple', + 'skinname-modern', 'common.css', 'standard.css', 'nostalgia.css', @@ -201,6 +217,8 @@ $wgOptionalMessages = array( 'chick.css', 'simple.css', 'modern.css', + 'print.css', + 'handheld.css', 'common.js', 'standard.js', 'nostalgia.js', @@ -307,10 +325,15 @@ $wgOptionalMessages = array( 'semicolon-separator', 'comma-separator', 'colon-separator', + 'pipe-separator', + 'word-separator', + 'ellipsis', 'autocomment-prefix', 'listgrouprights-right-display', 'timezone-utc', - 'whatlinkshere-barrow', + 'whatlinkshere-backlink', + 'recentchangeslinked-backlink', + 'diff-with-additional', ); /** EXIF messages, which may be set as optional in several checks, but are generally mandatory */ @@ -481,6 +504,16 @@ $wgEXIFMessages = array( 'exif-lightsource-19', 'exif-lightsource-24', 'exif-lightsource-255', + 'exif-flash-fired-0' , + 'exif-flash-fired-1' , + 'exif-flash-return-0' , + 'exif-flash-return-2' , + 'exif-flash-return-3' , + 'exif-flash-mode-1' , + 'exif-flash-mode-2' , + 'exif-flash-mode-3' , + 'exif-flash-function-1' , + 'exif-flash-redeye-1' , 'exif-focalplaneresolutionunit-2', 'exif-sensingmethod-1', 'exif-sensingmethod-2', diff --git a/maintenance/language/messages.inc b/maintenance/language/messages.inc index d99f2e45..d7475428 100644 --- a/maintenance/language/messages.inc +++ b/maintenance/language/messages.inc @@ -48,19 +48,20 @@ $wgMessageStructure = array( 'tog-watchlisthideown', 'tog-watchlisthidebots', 'tog-watchlisthideminor', + 'tog-watchlisthideliu', + 'tog-watchlisthideanons', 'tog-nolangconversion', 'tog-ccmeonemails', 'tog-diffonly', 'tog-showhiddencats', + 'tog-noconvertlink', + 'tog-norollbackdiff', ), 'underline' => array( 'underline-always', 'underline-never', 'underline-default', ), - 'skinpreview' => array( - 'skinpreview', - ), 'dates' => array( 'sunday', 'monday', @@ -217,8 +218,6 @@ $wgMessageStructure = array( 'links' => array( 'aboutsite', 'aboutpage', - 'bugreports', - 'bugreportspage', 'copyright', 'copyrightpagename', 'copyrightpage', @@ -243,8 +242,6 @@ $wgMessageStructure = array( 'badaccess' => array( 'badaccess', 'badaccess-group0', - 'badaccess-group1', - 'badaccess-group2', 'badaccess-groups', ), 'versionrequired' => array( @@ -255,17 +252,20 @@ $wgMessageStructure = array( 'ok', 'sitetitle', 'pagetitle', + 'pagetitle-view-mainpage', 'sitesubtitle', 'retrievedfrom', 'youhavenewmessages', 'newmessageslink', 'newmessagesdifflink', 'youhavenewmessagesmulti', - 'newtalkseperator', + 'newtalkseparator', 'editsection', 'editsection-brackets', 'editold', 'viewsourceold', + 'editlink', + 'viewsourcelink', 'editsectionhint', 'toc', 'showtoc', @@ -335,7 +335,6 @@ $wgMessageStructure = array( 'cannotdelete', 'badtitle', 'badtitletext', - 'perfdisabled', 'perfcached', 'perfcachedts', 'querypage-no-updates', @@ -371,7 +370,6 @@ $wgMessageStructure = array( 'remembermypassword', 'yourdomainname', 'externaldberror', - 'loginproblem', 'login', 'nav-login-createaccount', 'loginprompt', @@ -435,6 +433,7 @@ $wgMessageStructure = array( 'accountcreatedtext', 'createaccount-title', 'createaccount-text', + 'login-throttled', 'loginlanguagelabel', 'loginlanguagelinks', ), @@ -443,11 +442,17 @@ $wgMessageStructure = array( 'resetpass_announce', 'resetpass_text', 'resetpass_header', + 'oldpassword', + 'newpassword', + 'retypenew', 'resetpass_submit', 'resetpass_success', 'resetpass_bad_temporary', 'resetpass_forbidden', - 'resetpass_missing', + 'resetpass-no-info', + 'resetpass-submit-loggedin', + 'resetpass-wrong-oldpass', + 'resetpass-temp-password', ), 'toolbar' => array( 'bold_sample', @@ -495,10 +500,6 @@ $wgMessageStructure = array( 'blockededitsource', 'whitelistedittitle', 'whitelistedittext', - 'whitelistreadtitle', - 'whitelistreadtext', - 'whitelistacctitle', - 'whitelistacctext', 'confirmedittitle', 'confirmedittext', 'nosuchsectiontitle', @@ -561,6 +562,13 @@ $wgMessageStructure = array( 'permissionserrorstext', 'permissionserrorstext-withaction', 'recreate-deleted-warn', + 'deleted-notice', + 'deletelog-fulllog', + 'edit-hook-aborted', + 'edit-gone-missing', + 'edit-conflict', + 'edit-no-change', + 'edit-already-exists', ), 'parserwarnings' => array( 'expensive-parserfunction-warning', @@ -569,6 +577,8 @@ $wgMessageStructure = array( 'post-expand-template-inclusion-category', 'post-expand-template-argument-warning', 'post-expand-template-argument-category', + 'parser-template-loop-warning', + 'parser-template-recursion-depth-warning', ), 'undo' => array( 'undo-success', @@ -584,9 +594,8 @@ $wgMessageStructure = array( 'history' => array( 'viewpagelogs', 'nohistory', - 'revnotfound', - 'revnotfoundtext', 'currentrev', + 'currentrev-asof', 'revisionasof', 'revision-info', 'revision-info-current', @@ -600,6 +609,7 @@ $wgMessageStructure = array( 'page_first', 'page_last', 'histlegend', + 'history-fieldset-title', 'history_copyright', 'deletedrev', 'histfirst', @@ -679,6 +689,7 @@ $wgMessageStructure = array( 'mergehistory-invalid-destination', 'mergehistory-autocomment', 'mergehistory-comment', + 'mergehistory-same-destination', ), 'mergelog' => array( 'mergelog', @@ -691,11 +702,68 @@ $wgMessageStructure = array( 'difference', 'lineno', 'compareselectedversions', + 'visualcomparison', + 'wikicodecomparison', 'editundo', 'diff-multi', + 'diff-movedto', + 'diff-styleadded', + 'diff-added', + 'diff-changedto', + 'diff-movedoutof', + 'diff-styleremoved', + 'diff-removed', + 'diff-changedfrom', + 'diff-src', + 'diff-withdestination', + 'diff-with', + 'diff-with-additional', + 'diff-with-final', + 'diff-width', + 'diff-height', + 'diff-p', + 'diff-blockquote', + 'diff-h1', + 'diff-h2', + 'diff-h3', + 'diff-h4', + 'diff-h5', + 'diff-pre', + 'diff-div', + 'diff-ul', + 'diff-ol', + 'diff-li', + 'diff-table', + 'diff-tbody', + 'diff-tr', + 'diff-td', + 'diff-th', + 'diff-br', + 'diff-hr', + 'diff-code', + 'diff-dl', + 'diff-dt', + 'diff-dd', + 'diff-input', + 'diff-form', + 'diff-img', + 'diff-span', + 'diff-a', + 'diff-i', + 'diff-b', + 'diff-strong', + 'diff-em', + 'diff-font', + 'diff-big', + 'diff-del', + 'diff-tt', + 'diff-sub', + 'diff-sup', + 'diff-strike', ), 'search' => array( 'searchresults', + 'searchresults-title', 'searchresulttext', 'searchsubtitle', 'searchsubtitleinvalid', @@ -709,6 +777,25 @@ $wgMessageStructure = array( 'prevn', 'nextn', 'viewprevnext', + 'searchmenu-legend', + 'searchmenu-exists', + 'searchmenu-new', + 'searchhelp-url', + 'searchmenu-prefix', + 'searchmenu-help', + 'searchprofile-articles', + 'searchprofile-articles-and-proj', + 'searchprofile-project', + 'searchprofile-images', + 'searchprofile-everything', + 'searchprofile-advanced', + 'searchprofile-articles-tooltip', + 'searchprofile-project-tooltip', + 'searchprofile-images-tooltip', + 'searchprofile-everything-tooltip', + 'searchprofile-advanced-tooltip', + 'prefs-search-nsdefault', + 'prefs-search-nscustom', 'search-result-size', 'search-result-score', 'search-redirect', @@ -719,15 +806,16 @@ $wgMessageStructure = array( 'search-interwiki-custom', 'search-interwiki-more', 'search-mwsuggest-enabled', - 'search-mwsuggest-disabled', + 'search-mwsuggest-disabled', 'search-relatedarticle', 'mwsuggest-disable', - 'searchrelated', + 'searchrelated', 'searchall', 'showingresults', 'showingresultsnum', 'showingresultstotal', 'nonefound', + 'search-nonefound', 'powersearch', 'powersearch-legend', 'powersearch-ns', @@ -756,6 +844,7 @@ $wgMessageStructure = array( 'qbsettings-floatingright', 'changepassword', 'skin', + 'skin-preview', 'math', 'dateformat', 'datedefault', @@ -773,14 +862,15 @@ $wgMessageStructure = array( 'prefs-rc', 'prefs-watchlist', 'prefs-watchlist-days', + 'prefs-watchlist-days-max', 'prefs-watchlist-edits', + 'prefs-watchlist-edits-max', 'prefs-misc', + 'prefs-resetpass', 'saveprefs', 'resetprefs', - 'oldpassword', - 'newpassword', - 'retypenew', 'textboxsize', + 'prefs-edit-boxsize', 'rows', 'columns', 'searchresultshead', @@ -789,11 +879,15 @@ $wgMessageStructure = array( 'contextchars', 'stub-threshold', 'recentchangesdays', + 'recentchangesdays-max', 'recentchangescount', 'savedprefs', 'timezonelegend', 'timezonetext', 'localtime', + 'timezoneselect', + 'timezoneuseserverdefault', + 'timezoneuseoffset', 'timezoneoffset', 'servertime', 'guesstimezone', @@ -802,6 +896,7 @@ $wgMessageStructure = array( 'prefs-namespaces', 'defaultns', 'default', + 'defaultns', 'files', ), 'userrights' => array( @@ -859,6 +954,8 @@ $wgMessageStructure = array( 'right-minoredit', 'right-move', 'right-move-subpages', + 'right-move-rootuserpages', + 'right-movefile', 'right-suppressredirect', 'right-upload', 'right-reupload', @@ -909,10 +1006,47 @@ $wgMessageStructure = array( 'rightslogentry', 'rightsnone', ), + 'action' => array( + 'action-read', + 'action-edit', + 'action-createpage', + 'action-createtalk', + 'action-createaccount', + 'action-minoredit', + 'action-move', + 'action-move-subpages', + 'action-move-rootuserpages', + 'action-movefile', + 'action-upload', + 'action-reupload', + 'action-reupload-shared', + 'action-upload_by_url', + 'action-writeapi', + 'action-delete', + 'action-deleterevision', + 'action-deletedhistory', + 'action-browsearchive', + 'action-undelete', + 'action-suppressrevision', + 'action-suppressionlog', + 'action-block', + 'action-protect', + 'action-import', + 'action-importupload', + 'action-patrol', + 'action-autopatrol', + 'action-unwatchedpages', + 'action-trackback', + 'action-mergehistory', + 'action-userrights', + 'action-userrights-interwiki', + 'action-siteadmin', + ), 'recentchanges' => array( 'nchanges', 'recentchanges', 'recentchanges-url', + 'recentchanges-legend', 'recentchangestext', 'recentchanges-feed-description', 'rcnote', @@ -939,10 +1073,13 @@ $wgMessageStructure = array( 'rc_categories_any', 'rc-change-size', 'newsectionsummary', + 'rc-enhanced-expand', + 'rc-enhanced-hide', ), 'recentchangeslinked' => array( 'recentchangeslinked', 'recentchangeslinked-title', + 'recentchangeslinked-backlink', 'recentchangeslinked-noresult', 'recentchangeslinked-summary', 'recentchangeslinked-page', @@ -995,6 +1132,7 @@ $wgMessageStructure = array( 'fileexists-forbidden', 'fileexists-shared-forbidden', 'file-exists-duplicate', + 'file-deleted-duplicate', 'successfulupload', 'uploadwarning', 'savefile', @@ -1036,18 +1174,19 @@ $wgMessageStructure = array( 'upload_source_url', 'upload_source_file', ), - 'imagelist' => array( - 'imagelist-summary', - 'imagelist_search_for', + 'filelist' => array( + 'listfiles-summary', + 'listfiles_search_for', 'imgfile', - 'imagelist', - 'imagelist_date', - 'imagelist_name', - 'imagelist_user', - 'imagelist_size', - 'imagelist_description', - ), - 'imagedesciption' => array( + 'listfiles', + 'listfiles_date', + 'listfiles_name', + 'listfiles_user', + 'listfiles_size', + 'listfiles_description', + 'listfiles_count', + ), + 'filedescription' => array( 'filehist', 'filehist-help', 'filehist-deleteall', @@ -1055,12 +1194,16 @@ $wgMessageStructure = array( 'filehist-revert', 'filehist-current', 'filehist-datetime', + 'filehist-thumb', + 'filehist-thumbtext', + 'filehist-nothumb', 'filehist-user', 'filehist-dimensions', 'filehist-filesize', 'filehist-comment', 'imagelinks', 'linkstoimage', + 'linkstoimage-more', 'nolinkstoimage', 'morelinkstoimage', 'redirectstofile', @@ -1102,7 +1245,6 @@ $wgMessageStructure = array( 'filedelete-success-old', 'filedelete-nofile', 'filedelete-nofile-old', - 'filedelete-iscurrent', 'filedelete-otherreason', 'filedelete-reason-otherlist', 'filedelete-reason-dropdown', @@ -1139,10 +1281,23 @@ $wgMessageStructure = array( ), 'statistics' => array( 'statistics', - 'sitestats', - 'userstats', - 'sitestatstext', - 'userstatstext', + 'statistics-summary', + 'statistics-header-pages', + 'statistics-header-edits', + 'statistics-header-views', + 'statistics-header-users', + 'statistics-articles', + 'statistics-pages', + 'statistics-pages-desc', + 'statistics-files', + 'statistics-edits', + 'statistics-edits-average', + 'statistics-views-total', + 'statistics-views-peredit', + 'statistics-jobqueue', + 'statistics-users', + 'statistics-users-active', + 'statistics-users-active-desc', 'statistics-mostpopular', 'statistics-footer', ), @@ -1204,8 +1359,10 @@ $wgMessageStructure = array( 'wantedcategories-summary', 'wantedpages', 'wantedpages-summary', - 'missingfiles', - 'missingfiles-summary', + 'wantedfiles', + 'wantedfiles-summary', + 'wantedtemplates', + 'wantedtemplates-summary', 'mostlinked', 'mostlinked-summary', 'mostlinkedcategories', @@ -1230,6 +1387,7 @@ $wgMessageStructure = array( 'protectedpages', 'protectedpages-indef', 'protectedpages-summary', + 'protectedpages-cascade', 'protectedpagestext', 'protectedpagesempty', 'protectedtitles', @@ -1238,6 +1396,8 @@ $wgMessageStructure = array( 'protectedtitlesempty', 'listusers', 'listusers-summary', + 'listusers-editsonly', + 'usereditcount', 'newpages', 'newpages-summary', 'newpages-username', @@ -1262,6 +1422,7 @@ $wgMessageStructure = array( 'booksources-isbn', 'booksources-go', 'booksources-text', + 'booksources-invalid-isbn', ), 'magicwords' => array( 'rfcurl', @@ -1272,8 +1433,6 @@ $wgMessageStructure = array( 'speciallogtitlelabel', 'log', 'all-logs-page', - 'log-search-legend', - 'log-search-submit', 'alllogstext', 'logempty', 'log-title-wildcard', @@ -1285,6 +1444,7 @@ $wgMessageStructure = array( 'nextpage', 'prevpage', 'allpagesfrom', + 'allpagesto', 'allarticles', 'allinnamespace', 'allnotinnamespace', @@ -1303,11 +1463,32 @@ $wgMessageStructure = array( 'special-categories-sort-count', 'special-categories-sort-abc', ), + 'deletedcontribs' => array( + 'deletedcontributions', + ), + 'linksearch' => array( + 'linksearch', + 'linksearch-pat', + 'linksearch-ns', + 'linksearch-ok', + 'linksearch-text', + 'linksearch-line', + 'linksearch-error', + ), 'listusers' => array( 'listusersfrom', 'listusers-submit', 'listusers-noresult', ), + 'newuserlog' => array( + 'newuserlogpage', + 'newuserlogpagetext', + 'newuserlogentry', + 'newuserlog-byemail', + 'newuserlog-create-entry', + 'newuserlog-create2-entry', + 'newuserlog-autocreate-entry', + ), 'listgrouprights' => array( 'listgrouprights', 'listgrouprights-summary', @@ -1316,6 +1497,10 @@ $wgMessageStructure = array( 'listgrouprights-helppage', 'listgrouprights-members', 'listgrouprights-right-display', + 'listgrouprights-addgroup', + 'listgrouprights-removegroup', + 'listgrouprights-addgroup-all', + 'listgrouprights-removegroup-all', ), 'emailuser' => array( 'mailnologin', @@ -1327,6 +1512,9 @@ $wgMessageStructure = array( 'defemailsubject', 'noemailtitle', 'noemailtext', + 'nowikiemailtitle', + 'nowikiemailtext', + 'email-legend', 'emailfrom', 'emailto', 'emailsubject', @@ -1366,12 +1554,7 @@ $wgMessageStructure = array( 'iteminvalidname', 'wlnote', 'wlshowlast', - 'watchlist-show-bots', - 'watchlist-hide-bots', - 'watchlist-show-own', - 'watchlist-hide-own', - 'watchlist-show-minor', - 'watchlist-hide-minor', + 'watchlist-options', ), 'watching' => array( 'watching', @@ -1390,7 +1573,7 @@ $wgMessageStructure = array( 'enotif_anon_editor', 'enotif_body', ), - 'deleteprotectrev' => array( + 'delete' => array( 'deletepage', 'confirm', 'excontent', @@ -1417,6 +1600,8 @@ $wgMessageStructure = array( 'delete-edit-reasonlist', 'delete-toobig', 'delete-warning-toobig', + ), + 'rollback' => array( 'rollback', 'rollback_short', 'rollbacklink', @@ -1427,15 +1612,18 @@ $wgMessageStructure = array( 'revertpage', 'rollback-success', 'sessionfailure', + ), + 'protect' => array( 'protectlogpage', 'protectlogtext', 'protectedarticle', 'modifiedarticleprotection', 'unprotectedarticle', + 'movedarticleprotection', 'protect-title', + 'prot_1movedto2', 'protect-backlink', 'protect-legend', - 'confirmprotect', 'protectcomment', 'protectexpiry', 'protect_expiry_invalid', @@ -1452,8 +1640,17 @@ $wgMessageStructure = array( 'protect-level-sysop', 'protect-summary-cascade', 'protect-expiring', + 'protect-expiry-indefinite', 'protect-cascade', 'protect-cantedit', + 'protect-othertime', + 'protect-othertime-op', + 'protect-existing-expiry', + 'protect-otherreason', + 'protect-otherreason-op', + 'protect-dropdown', + 'protect-edit-reasonlist', + 'protect-expiry-options', 'restriction-type', 'restriction-level', 'minimum-size', @@ -1489,6 +1686,7 @@ $wgMessageStructure = array( 'undeletebtn', 'undeletelink', 'undeletereset', + 'undeleteinvert', 'undeletecomment', 'undeletedarticle', 'undeletedrevisions', @@ -1517,6 +1715,7 @@ $wgMessageStructure = array( ), 'contributions' => array( 'contributions', + 'contributions-title', 'mycontris', 'contribsub2', 'nocontribs', @@ -1527,6 +1726,7 @@ $wgMessageStructure = array( 'sp-contributions' => array( 'sp-contributions-newbies', 'sp-contributions-newbies-sub', + 'sp-contributions-newbies-title', 'sp-contributions-blocklog', 'sp-contributions-search', 'sp-contributions-username', @@ -1540,8 +1740,7 @@ $wgMessageStructure = array( 'whatlinkshere-title', 'whatlinkshere-summary', 'whatlinkshere-page', - 'whatlinkshere-barrow', - 'linklistsub', + 'whatlinkshere-backlink', 'linkshere', 'nolinkshere', 'nolinkshere-ns', @@ -1578,6 +1777,8 @@ $wgMessageStructure = array( 'ipbotherreason', 'ipbhidename', 'ipbwatchuser', + 'ipballowusertalk', + 'ipb-change-block', 'badipaddress', 'blockipsuccesssub', 'blockipsuccesstext', @@ -1586,6 +1787,7 @@ $wgMessageStructure = array( 'ipb-unblock', 'ipb-blocklist-addr', 'ipb-blocklist', + 'ipb-blocklist-contribs', 'unblockip', 'unblockiptext', 'ipusubmit', @@ -1594,6 +1796,9 @@ $wgMessageStructure = array( 'ipblocklist', 'ipblocklist-legend', 'ipblocklist-username', + 'ipblocklist-sh-userblocks', + 'ipblocklist-sh-tempblocks', + 'ipblocklist-sh-addressblocks', 'ipblocklist-summary', 'ipblocklist-submit', 'blocklistline', @@ -1603,25 +1808,31 @@ $wgMessageStructure = array( 'noautoblockblock', 'createaccountblock', 'emailblock', + 'blocklist-nousertalk', 'ipblocklist-empty', 'ipblocklist-no-results', 'blocklink', 'unblocklink', + 'change-blocklink', 'contribslink', 'autoblocker', 'blocklogpage', + 'blocklog-fulllog', 'blocklogentry', + 'reblock-logentry', 'blocklogtext', 'unblocklogentry', 'block-log-flags-anononly', 'block-log-flags-nocreate', 'block-log-flags-noautoblock', 'block-log-flags-noemail', + 'block-log-flags-nousertalk', 'block-log-flags-angry-autoblock', 'range_block_disabled', 'ipb_expiry_invalid', 'ipb_expiry_temp', 'ipb_already_blocked', + 'ipb-needreblock', 'ipb_cant_unblock', 'ipb_blocked_as_range', 'ip_range_invalid', @@ -1633,6 +1844,7 @@ $wgMessageStructure = array( 'sorbs', 'sorbsreason', 'sorbs_create_account_reason', + 'cant-block-while-blocked', ), 'developertools' => array( 'lockdb', @@ -1661,11 +1873,16 @@ $wgMessageStructure = array( 'movenologin', 'movenologintext', 'movenotallowed', + 'movenotallowedfile', + 'cant-move-user-page', + 'cant-move-to-user-page', 'newtitle', 'move-watch', 'movepagebtn', 'pagemovedsub', 'movepage-moved', + 'movepage-moved-redirect', + 'movepage-moved-noredirect', 'articleexists', 'cantmove-titleprotected', 'talkexists', @@ -1679,6 +1896,7 @@ $wgMessageStructure = array( 'movepage-max-pages', '1movedto2', '1movedto2_redir', + 'move-redirect-suppressed', 'movelogpage', 'movelogpagetext', 'movereason', @@ -1688,11 +1906,17 @@ $wgMessageStructure = array( 'delete_and_move_confirm', 'delete_and_move_reason', 'selfmove', + 'immobile-source-namespace', + 'immobile-target-namespace', + 'immobile-target-namespace-iw', + 'immobile-source-page', + 'immobile-target-page', 'immobile_namespace', 'imagenocrossnamespace', 'imagetypemismatch', 'imageinvalidfilename', 'fix-double-redirects', + 'move-leave-redirect', ), 'export' => array( 'export', @@ -1728,9 +1952,12 @@ $wgMessageStructure = array( 'import', 'importinterwiki', 'import-interwiki-text', + 'import-interwiki-source', 'import-interwiki-history', 'import-interwiki-submit', 'import-interwiki-namespace', + 'import-upload-filename', + 'import-comment', 'importtext', 'importstart', 'import-revision-count', @@ -1821,6 +2048,7 @@ $wgMessageStructure = array( 'accesskey-preview', 'accesskey-diff', 'accesskey-compareselectedversions', + 'accesskey-visualcomparison', 'accesskey-watch', 'accesskey-upload', ), @@ -1885,6 +2113,8 @@ $wgMessageStructure = array( 'tooltip-watch', 'tooltip-recreate', 'tooltip-upload', + 'tooltip-rollback', + 'tooltip-undo', ), 'stylesheets' => array( 'common.css', @@ -1896,6 +2126,8 @@ $wgMessageStructure = array( 'chick.css', 'simple.css', 'modern.css', + 'print.css', + 'handheld.css', ), 'scripts' => array( 'common.js', @@ -1939,6 +2171,16 @@ $wgMessageStructure = array( 'numauthors', 'numtalkauthors', ), + 'skin' => array( + 'skinname-standard', + 'skinname-nostalgia', + 'skinname-cologneblue', + 'skinname-monobook', + 'skinname-myskin', + 'skinname-chick', + 'skinname-simple', + 'skinname-modern', + ), 'math' => array( 'mw_math_png', 'mw_math_simple', @@ -1965,6 +2207,7 @@ $wgMessageStructure = array( 'patrol-log-line', 'patrol-log-auto', 'patrol-log-diff', + 'log-show-hide-patrol', ), 'imagedeletion' => array( 'deletedrevision', @@ -1979,6 +2222,9 @@ $wgMessageStructure = array( 'previousdiff', 'nextdiff', ), + 'visual-comparison' => array( + 'visual-comparison', + ), 'media-info' => array( 'mediawarning', 'imagemaxsize', @@ -1992,10 +2238,12 @@ $wgMessageStructure = array( 'show-big-image', 'show-big-image-thumb', ), - 'newimages' => array( + 'newfiles' => array( 'newimages', 'imagelisttext', 'newimages-summary', + 'newimages-legend', + 'newimages-label', 'showhidebots', 'noimages', 'ilsubmit', @@ -2269,6 +2517,18 @@ $wgMessageStructure = array( 'exif-lightsource-24', 'exif-lightsource-255', ), + 'exif-flash' => array( + 'exif-flash-fired-0' , + 'exif-flash-fired-1' , + 'exif-flash-return-0' , + 'exif-flash-return-2' , + 'exif-flash-return-3' , + 'exif-flash-mode-1' , + 'exif-flash-mode-2' , + 'exif-flash-mode-3' , + 'exif-flash-function-1' , + 'exif-flash-redeye-1' , + ), 'exif-focalplaneresolutionunit' => array( 'exif-focalplaneresolutionunit-2', ), @@ -2410,19 +2670,10 @@ $wgMessageStructure = array( 'unit-pixel' => array( 'unit-pixel', ), - 'htmldump' => array( - 'redirectingto', - ), 'purge' => array( - 'confirm_purge', 'confirm_purge_button', - ), - 'search2' => array( - 'searchcontaining', - 'searchnamed', - 'articletitles', - 'hideresults', - 'useajaxsearch', + 'confirm-purge-top', + 'confirm-purge-bottom', ), 'separators' => array( 'catseparator', @@ -2430,6 +2681,9 @@ $wgMessageStructure = array( 'comma-separator', 'colon-separator', 'autocomment-prefix', + 'pipe-separator', + 'word-separator', + 'ellipsis', ), 'imgmulti' => array( 'imgmultipageprev', @@ -2560,6 +2814,7 @@ $wgMessageStructure = array( ), 'CoreParserFunctions' => array( 'unknown_extension_tag', + 'duplicate-defaultsort', ), 'version' => array( 'version', @@ -2619,6 +2874,9 @@ $wgMessageStructure = array( 'blankpage', 'intentionallyblankpage', ), + 'external_images' => array( + 'external_image_whitelist', + ), ); /** Comments for each block */ @@ -2631,7 +2889,6 @@ XHTML id it should only appear once and include characters that are legal XHTML id names.", 'toggles' => 'User preference toggles', 'underline' => '', - 'skinpreview' => '', 'dates' => 'Dates', 'categorypages' => 'Categories related messages', 'mainpage' => '', @@ -2668,6 +2925,7 @@ XHTML id names.", 'group-member' => '', 'grouppage' => '', 'right' => 'Rights', + 'action' => 'Associated actions - in the sentence "You do not have permission to X"', 'rightslog' => 'User rights log', 'recentchanges' => 'Recent changes', 'recentchangeslinked' => 'Recent changes linked', @@ -2675,8 +2933,8 @@ XHTML id names.", 'upload-errors' => '', 'upload-curl-errors' => 'Some likely curl errors. More could be added from <http://curl.haxx.se/libcurl/c/libcurl-errors.html>', 'licenses' => '', - 'imagelist' => 'Special:ImageList', - 'imagedesciption' => 'Image description page', + 'filelist' => 'Special:ListFiles', + 'filedescription' => 'File description page', 'filerevert' => 'File reversion', 'filedelete' => 'File deletion', 'mimesearch' => 'MIME search', @@ -2697,13 +2955,18 @@ XHTML id names.", 'logpages' => 'Special:Log', 'allpages' => 'Special:AllPages', 'categories' => 'Special:Categories', + 'deletedcontribs' => 'Special:DeletedContributions', + 'linksearch' => 'Special:LinkSearch', 'listusers' => 'Special:ListUsers', + 'newuserlog' => 'Special:Log/newusers', 'listgrouprights' => 'Special:ListGroupRights', 'emailuser' => 'E-mail user', 'watchlist' => 'Watchlist', 'watching' => 'Displayed when you click the "watch" button and it is in the process of watching', 'enotif' => '', - 'deleteprotectrev' => 'Delete/protect/revert', + 'delete' => 'Delete', + 'rollback' => 'Rollback', + 'protect' => 'Protect', 'restrictions' => 'Restrictions (nouns)', 'restriction-levels' => 'Restriction levels', 'undelete' => 'Undelete', @@ -2727,12 +2990,13 @@ XHTML id names.", 'attribution' => 'Attribution', 'spamprotection' => 'Spam protection', 'info' => 'Info page', + 'skin' => 'Skin names', 'math' => 'Math options', 'patrolling' => 'Patrolling', 'patrol-log' => 'Patrol log', 'imagedeletion' => 'Image deletion', 'browsediffs' => 'Browsing diffs', - 'newimages' => 'Special:NewImages', + 'newfiles' => 'Special:NewFiles', 'video-info' => 'Video information, used by Language::formatTimePeriod() to format lengths in the above messages', 'badimagelist' => 'Bad image list', 'variantname-zh' => "Short names for language variants used for language conversion links. @@ -2743,6 +3007,7 @@ Variants for Chinese language", 'variantname-kk' => 'Variants for Kazakh language', 'variantname-ku' => 'Variants for Kurdish language', 'variantname-tg' => 'Variants for Tajiki language', + 'visual-comparison' => 'Visual comparison', 'media-info' => 'Media information', 'metadata' => 'Metadata', 'exif' => 'EXIF tags', @@ -2759,6 +3024,7 @@ Variants for Chinese language", 'exif-subjectdistance-value' => '', 'exif-meteringmode' => '', 'exif-lightsource' => '', + 'exif-flash' => 'Flash modes', 'exif-focalplaneresolutionunit' => '', 'exif-sensingmethod' => '', 'exif-filesource' => '', @@ -2785,9 +3051,7 @@ Variants for Chinese language", 'trackbacks' => 'Trackbacks', 'deleteconflict' => 'Delete conflict', 'unit-pixel' => '', - 'htmldump' => 'HTML dump', 'purge' => 'action=purge', - 'search2' => 'AJAX search', 'separators' => 'Separators for various lists, etc.', 'imgmulti' => 'Multipage image navigation', 'tablepager' => 'Table pager', @@ -2808,6 +3072,7 @@ Variants for Chinese language", 'fileduplicatesearch' => 'Special:FileDuplicateSearch', 'special-specialpages' => 'Special:SpecialPages', 'special-blank' => 'Special:BlankPage', + 'external_images' => 'External image whitelist', ); /** Short comments for standalone messages */ @@ -2817,7 +3082,7 @@ $wgMessageComments = array( 'sitenotice' => 'the equivalent to wgSiteNotice', 'history-feed-item-nocomment' => 'user at time', 'editcomment' => 'only shown if there is an edit comment', - 'revertpage' => 'Additional available: $3: revid of the revision reverted to, $4: timestamp of the revision reverted to, $5: revid of the revision reverted from, $6: timestamp of the revision reverted from', + 'revertpage' => 'Additionally available: $3: revid of the revision reverted to, $4: timestamp of the revision reverted to, $5: revid of the revision reverted from, $6: timestamp of the revision reverted from', 'lastmodifiedatby' => '$1 date, $2 time, $3 user', 'exif-orientation-1' => '0th row: top; 0th column: left', 'exif-orientation-2' => '0th row: top; 0th column: right', @@ -2829,7 +3094,10 @@ $wgMessageComments = array( 'exif-orientation-8' => '0th row: left; 0th column: bottom', 'movepage-moved' => 'The two titles are passed in plain text as $3 and $4 to allow additional goodies in the message.', 'ipboptions' => 'display1:time1,display2:time2,...', + 'protect-expiry-options' => 'display1:time1,display2:time2,...', 'metadata-fields' => 'Do not translate list items', 'version' => 'Not used as normal message but as header for the special page itself', 'userrights' => 'Not used as normal message but as header for the special page itself', + 'revision-info' => 'Additionally available: $3: revision id', + 'revision-info-current' => 'Available parameters: $1: timestamp; $2: userlinks; $3: revision id', ); diff --git a/maintenance/language/transstat.php b/maintenance/language/transstat.php index ee2b844c..b433abb4 100644 --- a/maintenance/language/transstat.php +++ b/maintenance/language/transstat.php @@ -96,12 +96,12 @@ foreach ( $wgLanguages->getLanguages() as $code ) { $requiredMessagesPercent = $wgOut->formatPercent( $requiredMessagesNumber, $wgRequiredMessagesNumber ); $obsoleteMessagesNumber = count( $messages['obsolete'] ); $obsoleteMessagesPercent = $wgOut->formatPercent( $obsoleteMessagesNumber, $messagesNumber, true ); - $messagesWithoutVariables = $wgLanguages->getMessagesWithoutVariables( $code ); + $messagesWithMismatchVariables = $wgLanguages->getMessagesWithMismatchVariables( $code ); $emptyMessages = $wgLanguages->getEmptyMessages( $code ); $messagesWithWhitespace = $wgLanguages->getMessagesWithWhitespace( $code ); $nonXHTMLMessages = $wgLanguages->getNonXHTMLMessages( $code ); $messagesWithWrongChars = $wgLanguages->getMessagesWithWrongChars( $code ); - $problematicMessagesNumber = count( array_unique( array_merge( $messagesWithoutVariables, $emptyMessages, $messagesWithWhitespace, $nonXHTMLMessages, $messagesWithWrongChars ) ) ); + $problematicMessagesNumber = count( array_unique( array_merge( $messagesWithMismatchVariables, $emptyMessages, $messagesWithWhitespace, $nonXHTMLMessages, $messagesWithWrongChars ) ) ); $problematicMessagesPercent = $wgOut->formatPercent( $problematicMessagesNumber, $messagesNumber, true ); # Output them diff --git a/maintenance/moveBatch.php b/maintenance/moveBatch.php index 52e6ddc6..427e5d09 100644 --- a/maintenance/moveBatch.php +++ b/maintenance/moveBatch.php @@ -7,10 +7,11 @@ * @ingroup Maintenance * @author Tim Starling * - * USAGE: php moveBatch.php [-u <user>] [-r <reason>] [-i <interval>] <listfile> + * USAGE: php moveBatch.php [-u <user>] [-r <reason>] [-i <interval>] [listfile] * - * <listfile> - file with two titles per line, separated with pipe characters; - * the first title is the source, the second is the destination + * [listfile] - file with two titles per line, separated with pipe characters; + * the first title is the source, the second is the destination. + * Standard input is used if listfile is not given. * <user> - username to perform moves as * <reason> - reason to be given for moves * <interval> - number of seconds to sleep after each move diff --git a/maintenance/namespaceDupes.php b/maintenance/namespaceDupes.php index 92296eb9..fcc7d3a1 100644 --- a/maintenance/namespaceDupes.php +++ b/maintenance/namespaceDupes.php @@ -195,7 +195,7 @@ class NamespaceConflictChecker { function reportConflict( $row, $suffix ) { $newTitle = Title::makeTitleSafe( $row->namespace, $row->title ); - if( !$newTitle ) { + if( is_null($newTitle) || !$newTitle->canExist() ) { // Title is also an illegal title... // For the moment we'll let these slide to cleanupTitles or whoever. printf( "... %d (0,\"%s\")\n", diff --git a/maintenance/nukePage.inc b/maintenance/nukePage.inc index 9ac28034..a19c6df6 100644 --- a/maintenance/nukePage.inc +++ b/maintenance/nukePage.inc @@ -25,6 +25,7 @@ function NukePage( $name, $delete = false ) { if( $title ) { $id = $title->getArticleID(); $real = $title->getPrefixedText(); + $isGoodArticle = $title->isContentPage(); echo( "found \"$real\" with ID $id.\n" ); # Get corresponding revisions @@ -56,6 +57,16 @@ function NukePage( $name, $delete = false ) { PurgeRedundantText( true ); } + # Update stats as appropriate + if ( $delete ) { + echo( "Updating site stats..." ); + $ga = $isGoodArticle ? -1 : 0; // if it was good, decrement that too + $stats = new SiteStatsUpdate( 0, -$count, $ga, -1 ); + $stats->doUpdate(); + echo( "done.\n" ); + } + + } else { echo( "not found in database.\n" ); $dbw->commit(); @@ -74,14 +85,6 @@ function DeleteRevisions( $ids ) { $dbw->query( "DELETE FROM $tbl_rev WHERE rev_id IN ( $set )" ); $dbw->commit(); - - #TODO: see if this is a "good" page, to decrement that as well. - $pages = $dbw->selectField('site_stats', 'ss_total_pages'); - $pages--; - $dbw->update( 'site_stats', - array('ss_total_pages' => $pages ), - array( 'ss_row_id' => 1), - __METHOD__ ); } diff --git a/maintenance/ourusers.php b/maintenance/ourusers.php index 620393fb..a7a3132b 100644 --- a/maintenance/ourusers.php +++ b/maintenance/ourusers.php @@ -14,42 +14,14 @@ /** */ $wikiuser_pass = `wikiuser_pass`; $wikiadmin_pass = `wikiadmin_pass`; -$wikisql_pass = `wikisql_pass`; +$nagios_pass = `nagios_sql_pass`; -if ( @$argv[1] == 'yaseo' ) { - $hosts = array( - 'localhost', - '211.115.107.158', - '211.115.107.159', - '211.115.107.160', - '211.115.107.138', - '211.115.107.139', - '211.115.107.140', - '211.115.107.141', - '211.115.107.142', - '211.115.107.143', - '211.115.107.144', - '211.115.107.145', - '211.115.107.146', - '211.115.107.147', - '211.115.107.148', - '211.115.107.149', - '211.115.107.150', - '211.115.107.152', - '211.115.107.153', - '211.115.107.154', - '211.115.107.155', - '211.115.107.156', - '211.115.107.157', - ); -} else { - $hosts = array( - 'localhost', - '10.0.%', - '66.230.200.%', - '208.80.152.%', - ); -} +$hosts = array( + 'localhost', + '10.0.%', + '66.230.200.%', + '208.80.152.%', +); $databases = array( '%wik%', @@ -60,7 +32,8 @@ print "/*!40100 set old_passwords=1 */;\n"; print "/*!40100 set global old_passwords=1 */;\n"; foreach( $hosts as $host ) { - print "--\n-- $host\n--\n\n-- wikiuser\n\n"; + print "--\n-- $host\n--\n"; + print "\n-- wikiuser\n\n"; print "GRANT REPLICATION CLIENT,PROCESS ON *.* TO 'wikiuser'@'$host' IDENTIFIED BY '$wikiuser_pass';\n"; print "GRANT ALL PRIVILEGES ON `boardvote%`.* TO 'wikiuser'@'$host' IDENTIFIED BY '$wikiuser_pass';\n"; foreach( $databases as $db ) { @@ -73,6 +46,9 @@ foreach( $hosts as $host ) { foreach ( $databases as $db ) { print "GRANT ALL PRIVILEGES ON `$db`.* TO wikiadmin@'$host' IDENTIFIED BY '$wikiadmin_pass';\n"; } + print "\n-- nagios\n\n"; + print "GRANT REPLICATION CLIENT ON *.* TO 'nagios'@'$host' IDENTIFIED BY '$nagios_pass';\n"; + print "\n"; } diff --git a/maintenance/parserTests.inc b/maintenance/parserTests.inc index 2cb85d2c..7971e64e 100644 --- a/maintenance/parserTests.inc +++ b/maintenance/parserTests.inc @@ -26,7 +26,7 @@ /** */ $options = array( 'quick', 'color', 'quiet', 'help', 'show-output', 'record' ); -$optionsWithArgs = array( 'regex' ); +$optionsWithArgs = array( 'regex', 'seed' ); require_once( 'commandLine.inc' ); require_once( "$IP/maintenance/parserTestsParserHook.php" ); @@ -62,6 +62,10 @@ class ParserTest { */ private $oldTablePrefix; + private $maxFuzzTestLength = 300; + private $fuzzSeed = 0; + private $memoryLimit = 50; + /** * Sets terminal colorization and diff/quick modes depending on OS and * command-line options (--color and --quick). @@ -117,6 +121,10 @@ class ParserTest { } $this->keepUploads = isset( $options['keep-uploads'] ); + if ( isset( $options['seed'] ) ) { + $this->fuzzSeed = intval( $options['seed'] ) - 1; + } + $this->hooks = array(); $this->functionHooks = array(); } @@ -134,6 +142,119 @@ class ParserTest { } /** + * Run a fuzz test series + * Draw input from a set of test files + */ + function fuzzTest( $filenames ) { + $dict = $this->getFuzzInput( $filenames ); + $dictSize = strlen( $dict ); + $logMaxLength = log( $this->maxFuzzTestLength ); + $this->setupDatabase(); + ini_set( 'memory_limit', $this->memoryLimit * 1048576 ); + + $numTotal = 0; + $numSuccess = 0; + $user = new User; + $opts = ParserOptions::newFromUser( $user ); + $title = Title::makeTitle( NS_MAIN, 'Parser_test' ); + + while ( true ) { + // Generate test input + mt_srand( ++$this->fuzzSeed ); + $totalLength = mt_rand( 1, $this->maxFuzzTestLength ); + $input = ''; + while ( strlen( $input ) < $totalLength ) { + $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength; + $hairLength = min( intval( exp( $logHairLength ) ), $dictSize ); + $offset = mt_rand( 0, $dictSize - $hairLength ); + $input .= substr( $dict, $offset, $hairLength ); + } + + $this->setupGlobals(); + $parser = $this->getParser(); + // Run the test + try { + $parser->parse( $input, $title, $opts ); + $fail = false; + } catch ( Exception $exception ) { + $fail = true; + } + + if ( $fail ) { + echo "Test failed with seed {$this->fuzzSeed}\n"; + echo "Input:\n"; + var_dump( $input ); + echo "\n\n"; + echo "$exception\n"; + } else { + $numSuccess++; + } + $numTotal++; + $this->teardownGlobals(); + $parser->__destruct(); + + if ( $numTotal % 100 == 0 ) { + $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 ); + echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n"; + if ( $usage > 90 ) { + echo "Out of memory:\n"; + $memStats = $this->getMemoryBreakdown(); + foreach ( $memStats as $name => $usage ) { + echo "$name: $usage\n"; + } + $this->abort(); + } + } + } + } + + /** + * Get an input dictionary from a set of parser test files + */ + function getFuzzInput( $filenames ) { + $dict = ''; + foreach( $filenames as $filename ) { + $contents = file_get_contents( $filename ); + preg_match_all( '/!!\s*input\n(.*?)\n!!\s*result/s', $contents, $matches ); + foreach ( $matches[1] as $match ) { + $dict .= $match . "\n"; + } + } + return $dict; + } + + /** + * Get a memory usage breakdown + */ + function getMemoryBreakdown() { + $memStats = array(); + foreach ( $GLOBALS as $name => $value ) { + $memStats['$'.$name] = strlen( serialize( $value ) ); + } + $classes = get_declared_classes(); + foreach ( $classes as $class ) { + $rc = new ReflectionClass( $class ); + $props = $rc->getStaticProperties(); + $memStats[$class] = strlen( serialize( $props ) ); + $methods = $rc->getMethods(); + foreach ( $methods as $method ) { + $memStats[$class] += strlen( serialize( $method->getStaticVariables() ) ); + } + } + $functions = get_defined_functions(); + foreach ( $functions['user'] as $function ) { + $rf = new ReflectionFunction( $function ); + $memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) ); + } + asort( $memStats ); + return $memStats; + } + + function abort() { + $this->abort(); + } + + /** * Run a series of tests listed in the given text files. * Each test consists of a brief description, wikitext input, * and the expected HTML output. @@ -267,6 +388,24 @@ class ParserTest { } /** + * Get a Parser object + */ + function getParser() { + global $wgParserConf; + $class = $wgParserConf['class']; + $parser = new $class( $wgParserConf ); + foreach( $this->hooks as $tag => $callback ) { + $parser->setHook( $tag, $callback ); + } + foreach( $this->functionHooks as $tag => $bits ) { + list( $callback, $flags ) = $bits; + $parser->setFunctionHook( $tag, $callback, $flags ); + } + wfRunHooks( 'ParserTestParser', array( &$parser ) ); + return $parser; + } + + /** * Run a given wikitext input through a freshly-constructed wiki parser, * and compare the output against the expected results. * Prints status and explanatory messages to stdout. @@ -276,7 +415,6 @@ class ParserTest { * @return bool */ private function runTest( $desc, $input, $result, $opts ) { - global $wgParserConf; if( $this->showProgress ) { $this->showTesting( $desc ); } @@ -300,18 +438,7 @@ class ParserTest { } $noxml = (bool)preg_match( '~\\b noxml \\b~x', $opts ); - - $class = $wgParserConf['class']; - $parser = new $class( $wgParserConf ); - foreach( $this->hooks as $tag => $callback ) { - $parser->setHook( $tag, $callback ); - } - foreach( $this->functionHooks as $tag => $bits ) { - list( $callback, $flags ) = $bits; - $parser->setFunctionHook( $tag, $callback, $flags ); - } - wfRunHooks( 'ParserTestParser', array( &$parser ) ); - + $parser = $this->getParser(); $title =& Title::makeTitle( NS_MAIN, $titleText ); $matches = array(); @@ -336,7 +463,11 @@ class ParserTest { global $wgOut; $wgOut->addCategoryLinks($output->getCategories()); $cats = $wgOut->getCategoryLinks(); - $out = $this->tidy( implode( ' ', $cats['normal'] ) ); + if ( isset( $cats['normal'] ) ) { + $out = $this->tidy( implode( ' ', $cats['normal'] ) ); + } else { + $out = ''; + } } $result = $this->tidy($result); @@ -383,6 +514,8 @@ class ParserTest { self::getOptionValue( '/variant=([a-z]+(?:-[a-z]+)?)/', $opts, false ); $maxtoclevel = self::getOptionValue( '/wgMaxTocLevel=(\d+)/', $opts, 999 ); + $linkHolderBatchSize = + self::getOptionValue( '/wgLinkHolderBatchSize=(\d+)/', $opts, 1000 ); $settings = array( 'wgServer' => 'http://localhost', @@ -426,8 +559,11 @@ class ParserTest { 'createpage' => true, 'createtalk' => true, ) ), + 'wgNamespaceProtection' => array( NS_MEDIAWIKI => 'editinterface' ), 'wgDefaultExternalStore' => array(), 'wgForeignFileRepos' => array(), + 'wgLinkHolderBatchSize' => $linkHolderBatchSize, + 'wgEnforceHtmlIds' => true, ); $this->savedGlobals = array(); foreach( $settings as $var => $val ) { @@ -437,6 +573,7 @@ class ParserTest { $langObj = Language::factory( $lang ); $GLOBALS['wgLang'] = $langObj; $GLOBALS['wgContLang'] = $langObj; + $GLOBALS['wgMemc'] = new FakeMemCachedClient; //$GLOBALS['wgMessageCache'] = new MessageCache( new BagOStuff(), false, 0, $GLOBALS['wgDBname'] ); @@ -484,9 +621,12 @@ class ParserTest { throw new MWException( 'setupDatabase should be called before setupGlobals' ); } $this->databaseSetupDone = true; + $this->oldTablePrefix = $wgDBprefix; # CREATE TEMPORARY TABLE breaks if there is more than one server - if ( wfGetLB()->getServerCount() != 1 ) { + # FIXME: r40209 makes temporary tables break even with just one server + # FIXME: (bug 15892); disabling the feature entirely as a temporary fix + if ( true || wfGetLB()->getServerCount() != 1 ) { $this->useTemporaryTables = false; } @@ -504,19 +644,28 @@ class ParserTest { $def = ''; } foreach ($tables as $tbl) { + # Clean up from previous aborted run. So that table escaping + # works correctly across DB engines, we need to change the pre- + # fix back and forth so tableName() works right. + $this->changePrefix( $this->oldTablePrefix ); $oldTableName = $db->tableName( $tbl ); - # Clean up from previous aborted run - if ( $db->tableExists( "`parsertest_$tbl`" ) ) { - $db->query("DROP TABLE `parsertest_$tbl`"); + $this->changePrefix( 'parsertest_' ); + $newTableName = $db->tableName( $tbl ); + + if ( $db->tableExists( $tbl ) ) { + $db->query("DROP TABLE $newTableName"); } # Create new table - $db->query("CREATE $temporary TABLE `parsertest_$tbl` (LIKE $oldTableName $def)"); + $db->query("CREATE $temporary TABLE $newTableName (LIKE $oldTableName $def)"); } } else { # Hack for MySQL versions < 4.1, which don't support # "CREATE TABLE ... LIKE". Note that # "CREATE TEMPORARY TABLE ... SELECT * FROM ... LIMIT 0" # would not create the indexes we need.... + # + # Note that we don't bother changing around the prefixes here be- + # cause we know we're using MySQL anyway. foreach ($tables as $tbl) { $oldTableName = $db->tableName( $tbl ); $res = $db->query("SHOW CREATE TABLE $oldTableName"); @@ -532,13 +681,15 @@ class ParserTest { } } + $this->changePrefix( 'parsertest_' ); + # Hack: insert a few Wikipedia in-project interwiki prefixes, # for testing inter-language links - $db->insert( '`parsertest_interwiki`', array( - array( 'iw_prefix' => 'Wikipedia', + $db->insert( 'interwiki', array( + array( 'iw_prefix' => 'wikipedia', 'iw_url' => 'http://en.wikipedia.org/wiki/$1', 'iw_local' => 0 ), - array( 'iw_prefix' => 'MeatBall', + array( 'iw_prefix' => 'meatball', 'iw_url' => 'http://www.usemod.com/cgi-bin/mb.pl?$1', 'iw_local' => 0 ), array( 'iw_prefix' => 'zh', @@ -556,7 +707,7 @@ class ParserTest { ) ); # Hack: Insert an image to work with - $db->insert( '`parsertest_image`', array( + $db->insert( 'image', array( 'img_name' => 'Foobar.jpg', 'img_size' => 12345, 'img_description' => 'Some lame file', @@ -573,11 +724,7 @@ class ParserTest { ) ); # Update certain things in site_stats - $db->insert( '`parsertest_site_stats`', array( 'ss_row_id' => 1, 'ss_images' => 1, 'ss_good_articles' => 1 ) ); - - # Change the table prefix - $this->oldTablePrefix = $wgDBprefix; - $this->changePrefix( 'parsertest_' ); + $db->insert( 'site_stats', array( 'ss_row_id' => 1, 'ss_images' => 1, 'ss_good_articles' => 1 ) ); } /** @@ -609,11 +756,12 @@ class ParserTest { return; } + /* $tables = $this->listTables(); $db = wfGetDB( DB_MASTER ); foreach ( $tables as $table ) { $db->query( "DROP TABLE `parsertest_$table`" ); - } + }*/ } /** @@ -633,9 +781,11 @@ class ParserTest { } wfDebug( "Creating upload directory $dir\n" ); - mkdir( $dir ); - mkdir( $dir . '/3' ); - mkdir( $dir . '/3/3a' ); + if ( file_exists( $dir ) ) { + wfDebug( "Already exists!\n" ); + return $dir; + } + wfMkdirParents( $dir . '/3/3a' ); copy( "$IP/skins/monobook/headbg.jpg", "$dir/3/3a/Foobar.jpg" ); return $dir; } @@ -646,6 +796,8 @@ class ParserTest { */ private function teardownGlobals() { RepoGroup::destroySingleton(); + FileCache::destroySingleton(); + LinkCache::singleton()->clear(); foreach( $this->savedGlobals as $var => $val ) { $GLOBALS[$var] = $val; } @@ -1085,6 +1237,8 @@ class DbTestPreviewer extends TestRecorder { 'ff' => 'still FAILING test(s) :(', ); + $prevResults = array(); + $res = $this->db->select( 'testitem', array( 'ti_name', 'ti_success' ), array( 'ti_run' => $this->prevRun ), __METHOD__ ); foreach ( $res as $row ) { diff --git a/maintenance/parserTests.php b/maintenance/parserTests.php index 192eeaa8..0d50feb1 100644 --- a/maintenance/parserTests.php +++ b/maintenance/parserTests.php @@ -28,22 +28,21 @@ require('parserTests.inc'); if( isset( $options['help'] ) ) { echo <<<ENDS MediaWiki $wgVersion parser test suite -Usage: php parserTests.php [--quick] [--quiet] [--show-output] - [--color[=(yes|no)]] - [--regex=<expression>] [--file=<testfile>] - [--record] [--compare] - [--help] +Usage: php parserTests.php [options...] + Options: --quick Suppress diff output of failed tests --quiet Suppress notification of passed tests (shows only failed tests) --show-output Show expected and actual output - --color Override terminal detection and force color output on or off + --color[=yes|no] Override terminal detection and force color output on or off use wgCommandLineDarkBg = true; if your term is dark --regex Only run tests whose descriptions which match given regex - --file Run test cases from a custom file instead of parserTests.txt + --file=<testfile> Run test cases from a custom file instead of parserTests.txt --record Record tests in database --compare Compare with recorded results, without updating the database. --keep-uploads Re-use the same upload directory for each test, don't delete it + --fuzz Do a fuzz test instead of a normal test + --seed <n> Start the fuzz test from the specified seed --help Show this help message @@ -67,7 +66,10 @@ if( isset( $options['file'] ) ) { # Print out software version to assist with locating regressions $version = SpecialVersion::getVersion(); echo( "This is MediaWiki version {$version}.\n\n" ); -$ok = $tester->runTestsFromFiles( $files ); - -exit ($ok ? 0 : -1); +if ( isset( $options['fuzz'] ) ) { + $tester->fuzzTest( $files ); +} else { + $ok = $tester->runTestsFromFiles( $files ); + exit ($ok ? 0 : -1); +} diff --git a/maintenance/parserTests.txt b/maintenance/parserTests.txt index 07c897f1..c84e0e35 100644 --- a/maintenance/parserTests.txt +++ b/maintenance/parserTests.txt @@ -1027,7 +1027,7 @@ URL-encoding in URL functions (single parameter) !! input {{localurl:Some page|amp=&}} !! result -<p>/index.php?title=Some_page&amp=%26 +<p>/index.php?title=Some_page&amp=& </p> !! end @@ -1036,7 +1036,7 @@ URL-encoding in URL functions (multiple parameters) !! input {{localurl:Some page|q=?&=&}} !! result -<p>/index.php?title=Some_page&q=%3F&amp=%26 +<p>/index.php?title=Some_page&q=?&amp=& </p> !! end @@ -1264,7 +1264,9 @@ Invalid attributes in table cell (bug 1830) !! end -# FIXME: this one has incorrect tag nesting still. +# FIXME: It's not clear at all that this is the result we want, but the actual +# output right now is invalid XML, so clearly something is wrong. The result +# specified here is now valid XML, which is an improvement . . . !! test Table security: embedded pipes (http://lists.wikimedia.org/mailman/htdig/wikitech-l/2006-April/022293.html) !! input @@ -1273,7 +1275,7 @@ Table security: embedded pipes (http://lists.wikimedia.org/mailman/htdig/wikitec !! result <table> <tr> -<td><a href="ftp://|x||" class="external autonumber" title="ftp://|x||" rel="nofollow">[1]</td><td></a>" onmouseover="alert(document.cookie)">test +<td><a href="ftp://|x||" class="external autonumber" title="ftp://|x||" rel="nofollow">[1]</a></td><td>" onmouseover="alert(document.cookie)">test </td> </tr> </table> @@ -1321,6 +1323,33 @@ Broken link !! end !! test +Broken link with fragment +!! input +[[Zigzagzogzagzig#zug]] +!! result +<p><a href="/index.php?title=Zigzagzogzagzig&action=edit&redlink=1" class="new" title="Zigzagzogzagzig (not yet written)">Zigzagzogzagzig#zug</a> +</p> +!! end + +!! test +Special page link with fragment +!! input +[[Special:Version#anchor]] +!! result +<p><a href="/wiki/Special:Version#anchor" title="Special:Version">Special:Version#anchor</a> +</p> +!! end + +!! test +Nonexistent special page link with fragment +!! input +[[Special:ThisNameWillHopefullyNeverBeUsed#anchor]] +!! result +<p><a href="/wiki/Special:ThisNameWillHopefullyNeverBeUsed" class="new" title="Special:ThisNameWillHopefullyNeverBeUsed (not yet written)">Special:ThisNameWillHopefullyNeverBeUsed#anchor</a> +</p> +!! end + +!! test Link with prefix !! input xxx[[main Page]], xxx[[Main Page]], Xxx[[main Page]] XXX[[main Page]], XXX[[Main Page]] @@ -1334,7 +1363,7 @@ Link with suffix !! input [[Main Page]]xxx, [[Main Page]]XXX, [[Main Page]]!!! !! result -<p><a href="/wiki/Main_Page" title="Main Page">Main Pagexxx</a>, <a href="/wiki/Main_Page" title="Main Page">Main PageXXX</a>, <a href="/wiki/Main_Page" title="Main Page">Main Page</a>!!! +<p><a href="/wiki/Main_Page" title="Main Page">Main Pagexxx</a>, <a href="/wiki/Main_Page" title="Main Page">Main Page</a>XXX, <a href="/wiki/Main_Page" title="Main Page">Main Page</a>!!! </p> !! end @@ -2067,7 +2096,7 @@ Namespace -1 {{ns:-1}} !! end !! test -Namespace Project {{ns:User}} +Namespace User {{ns:User}} !! input {{ns:User}} !! result @@ -2075,6 +2104,64 @@ Namespace Project {{ns:User}} </p> !! end +!! test +Namespace User talk {{ns:User_talk}} +!! input +{{ns:User_talk}} +!! result +<p>User talk +</p> +!! end + +!! test +Namespace User talk {{ns:uSeR tAlK}} +!! input +{{ns:uSeR tAlK}} +!! result +<p>User talk +</p> +!! end + +!! test +Namespace File {{ns:File}} +!! input +{{ns:File}} +!! result +<p>File +</p> +!! end + +!! test +Namespace File {{ns:Image}} +!! input +{{ns:Image}} +!! result +<p>File +</p> +!! end + +!! test +Namespace (lang=de) Benutzer {{ns:User}} +!! options +language=de +!! input +{{ns:User}} +!! result +<p>Benutzer +</p> +!! end + +!! test +Namespace (lang=de) Benutzer Diskussion {{ns:3}} +!! options +language=de +!! input +{{ns:3}} +!! result +<p>Benutzer Diskussion +</p> +!! end + ### ### Magic links @@ -2120,7 +2207,7 @@ PMID 1234 #### !! test -Nonexistant template +Nonexistent template !! input {{thistemplatedoesnotexist}} !! result @@ -2306,7 +2393,7 @@ Template with thumb image (with link in description) {{paramtest| param =[[Image:noimage.png|thumb|[[no link|link]] [[no link|caption]]]]}} !! result -This is a test template with parameter <div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/index.php?title=Special:Upload&wpDestFile=Noimage.png" class="new" title="Image:Noimage.png">Image:Noimage.png</a> <div class="thumbcaption"><a href="/index.php?title=No_link&action=edit&redlink=1" class="new" title="No link (not yet written)">link</a> <a href="/index.php?title=No_link&action=edit&redlink=1" class="new" title="No link (not yet written)">caption</a></div></div></div> +This is a test template with parameter <div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/index.php?title=Special:Upload&wpDestFile=Noimage.png" class="new" title="File:Noimage.png">File:Noimage.png</a> <div class="thumbcaption"><a href="/index.php?title=No_link&action=edit&redlink=1" class="new" title="No link (not yet written)">link</a> <a href="/index.php?title=No_link&action=edit&redlink=1" class="new" title="No link (not yet written)">caption</a></div></div></div> !! end @@ -2594,8 +2681,8 @@ Bug 6563: Edit link generation for section shown by <includeonly> !! input {{includeonly section}} !! result -<a name="Includeonly_section"></a><h2><span class="editsection">[<a href="/index.php?title=Template:Includeonly_section&action=edit&section=T-1" title="Template:Includeonly section">edit</a>]</span> <span class="mw-headline">Includeonly section</span></h2> -<a name="Section_T-1"></a><h2><span class="editsection">[<a href="/index.php?title=Template:Includeonly_section&action=edit&section=T-2" title="Template:Includeonly section">edit</a>]</span> <span class="mw-headline">Section T-1</span></h2> +<a name="Includeonly_section" id="Includeonly_section"></a><h2><span class="editsection">[<a href="/index.php?title=Template:Includeonly_section&action=edit&section=T-1" title="Template:Includeonly section">edit</a>]</span> <span class="mw-headline">Includeonly section</span></h2> +<a name="Section_T-1" id="Section_T-1"></a><h2><span class="editsection">[<a href="/index.php?title=Template:Includeonly_section&action=edit&section=T-2" title="Template:Includeonly section">edit</a>]</span> <span class="mw-headline">Section T-1</span></h2> !! end @@ -2621,7 +2708,7 @@ Bug 6563: Edit link generation for section suppressed by <includeonly> </includeonly> ==Section 1== !! result -<a name="Section_1"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Section 1">edit</a>]</span> <span class="mw-headline">Section 1</span></h2> +<a name="Section_1" id="Section_1"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Section 1">edit</a>]</span> <span class="mw-headline">Section 1</span></h2> !! end @@ -2662,7 +2749,7 @@ PST !! end !! test -pre-save transform: nonexistant template +pre-save transform: nonexistent template !! options PST !! input @@ -3043,7 +3130,7 @@ Simple image !! input [[Image:foobar.jpg]] !! result -<p><a href="/wiki/Image:Foobar.jpg" class="image" title="Image:foobar.jpg"><img alt="Image:foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> +<p><a href="/wiki/File:Foobar.jpg" class="image" title="Image:foobar.jpg"><img alt="Image:foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> </p> !! end @@ -3052,36 +3139,101 @@ Right-aligned image !! input [[Image:foobar.jpg|right]] !! result -<div class="floatright"><span><a href="/wiki/Image:Foobar.jpg" class="image" title="Foobar.jpg"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a></span></div> +<div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image" title="Foobar.jpg"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a></div> !! end !! test +Simple image (using File: namespace, now canonical) +!! input +[[File:foobar.jpg]] +!! result +<p><a href="/wiki/File:Foobar.jpg" class="image" title="File:foobar.jpg"><img alt="File:foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> +</p> +!! end + +!! test Image with caption !! input [[Image:foobar.jpg|right|Caption text]] !! result -<div class="floatright"><span><a href="/wiki/Image:Foobar.jpg" class="image" title="Caption text"><img alt="Caption text" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a></span></div> +<div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image" title="Caption text"><img alt="Caption text" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a></div> + +!! end + +!! test +Image with link parameter, wiki target +!! input +[[Image:foobar.jpg|link=Target page]] +!! result +<p><a href="/wiki/Target_page" title="Target page"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> +</p> +!! end +!! test +Image with link parameter, URL target +!! input +[[Image:foobar.jpg|link=http://example.com/]] +!! result +<p><a href="http://example.com/"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> +</p> !! end !! test +Image with empty link parameter +!! input +[[Image:foobar.jpg|link=]] +!! result +<p><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /> +</p> +!! end + + + +!! test Image with frame and link !! input [[Image:Foobar.jpg|frame|left|This is a test image [[Main Page]]]] !! result -<div class="thumb tleft"><div class="thumbinner" style="width:1943px;"><a href="/wiki/Image:Foobar.jpg" class="image" title="This is a test image Main Page"><img alt="This is a test image Main Page" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" class="thumbimage" /></a> <div class="thumbcaption">This is a test image <a href="/wiki/Main_Page" title="Main Page">Main Page</a></div></div></div> +<div class="thumb tleft"><div class="thumbinner" style="width:1943px;"><a href="/wiki/File:Foobar.jpg" class="image" title="This is a test image Main Page"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" class="thumbimage" /></a> <div class="thumbcaption">This is a test image <a href="/wiki/Main_Page" title="Main Page">Main Page</a></div></div></div> !! end !! test +Image with frame and link and explicit alt +!! input +[[Image:Foobar.jpg|frame|left|This is a test image [[Main Page]]|alt=Altitude]] +!! result +<div class="thumb tleft"><div class="thumbinner" style="width:1943px;"><a href="/wiki/File:Foobar.jpg" class="image" title="This is a test image Main Page"><img alt="Altitude" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" class="thumbimage" /></a> <div class="thumbcaption">This is a test image <a href="/wiki/Main_Page" title="Main Page">Main Page</a></div></div></div> + +!! end + +!! test +Image with wiki markup in implicit alt +!! input +[[Image:Foobar.jpg|testing '''bold''' in alt]] +!! result +<p><a href="/wiki/File:Foobar.jpg" class="image" title="testing bold in alt"><img alt="testing bold in alt" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> +</p> +!! end + +!! test +Image with wiki markup in explicit alt +!! input +[[Image:Foobar.jpg|alt=testing '''bold''' in alt]] +!! result +<p><a href="/wiki/File:Foobar.jpg" class="image" title="Foobar.jpg"><img alt="testing bold in alt" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> +</p> +!! end + +!! test Link to image page- image page normally doesn't exists, hence edit link Add test with existing image page -#<p><a href="/wiki/Image:Test" title="Image:Test">Image:test</a> +#<p><a href="/wiki/File:Test" title="Image:Test">Image:test</a> !! input [[:Image:test]] !! result -<p><a href="/index.php?title=Image:Test&action=edit&redlink=1" class="new" title="Image:Test (not yet written)">Image:test</a> +<p><a href="/index.php?title=File:Test&action=edit&redlink=1" class="new" title="File:Test (not yet written)">Image:test</a> </p> !! end @@ -3090,7 +3242,7 @@ Frameless image caption with a free URL !! input [[Image:foobar.jpg|http://example.com]] !! result -<p><a href="/wiki/Image:Foobar.jpg" class="image" title="http://example.com"><img alt="http://example.com" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> +<p><a href="/wiki/File:Foobar.jpg" class="image" title="http://example.com"><img alt="http://example.com" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> </p> !! end @@ -3099,7 +3251,16 @@ Thumbnail image caption with a free URL !! input [[Image:foobar.jpg|thumb|http://example.com]] !! result -<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/Image:Foobar.jpg" class="image" title="http://example.com"><img alt="http://example.com" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/Image:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div><a href="http://example.com" class="external free" title="http://example.com" rel="nofollow">http://example.com</a></div></div></div> +<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image" title="http://example.com"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div><a href="http://example.com" class="external free" title="http://example.com" rel="nofollow">http://example.com</a></div></div></div> + +!! end + +!! test +Thumbnail image caption with a free URL and explicit alt +!! input +[[Image:foobar.jpg|thumb|http://example.com|alt=Alteration]] +!! result +<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image" title="http://example.com"><img alt="Alteration" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div><a href="http://example.com" class="external free" title="http://example.com" rel="nofollow">http://example.com</a></div></div></div> !! end @@ -3108,7 +3269,7 @@ BUG 1887: A ISBN with a thumbnail !! input [[Image:foobar.jpg|thumb|ISBN 1235467890]] !! result -<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/Image:Foobar.jpg" class="image" title="ISBN 1235467890"><img alt="ISBN 1235467890" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/Image:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div><a href="/wiki/Special:BookSources/1235467890" class="internal">ISBN 1235467890</a></div></div></div> +<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image" title="ISBN 1235467890"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div><a href="/wiki/Special:BookSources/1235467890" class="internal">ISBN 1235467890</a></div></div></div> !! end @@ -3117,7 +3278,7 @@ BUG 1887: A RFC with a thumbnail !! input [[Image:foobar.jpg|thumb|This is RFC 12354]] !! result -<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/Image:Foobar.jpg" class="image" title="This is RFC 12354"><img alt="This is RFC 12354" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/Image:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>This is <a href="http://tools.ietf.org/html/rfc12354" class="external" title="http://tools.ietf.org/html/rfc12354">RFC 12354</a></div></div></div> +<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image" title="This is RFC 12354"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>This is <a href="http://tools.ietf.org/html/rfc12354" class="external" title="http://tools.ietf.org/html/rfc12354">RFC 12354</a></div></div></div> !! end @@ -3126,7 +3287,7 @@ BUG 1887: A mailto link with a thumbnail !! input [[Image:foobar.jpg|thumb|Please mailto:nobody@example.com]] !! result -<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/Image:Foobar.jpg" class="image" title="Please mailto:nobody@example.com"><img alt="Please mailto:nobody@example.com" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/Image:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>Please <a href="mailto:nobody@example.com" class="external free" title="mailto:nobody@example.com" rel="nofollow">mailto:nobody@example.com</a></div></div></div> +<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image" title="Please mailto:nobody@example.com"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>Please <a href="mailto:nobody@example.com" class="external free" title="mailto:nobody@example.com" rel="nofollow">mailto:nobody@example.com</a></div></div></div> !! end @@ -3136,7 +3297,7 @@ so math is not stripped and turns up as escaped <math> tags. !! input [[Image:foobar.jpg|thumb|<math>2+2</math>]] !! result -<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/Image:Foobar.jpg" class="image" title="<math>2+2</math>"><img alt="<math>2+2</math>" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/Image:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div><math>2+2</math></div></div></div> +<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image" title="<math>2+2</math>"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div><math>2+2</math></div></div></div> !! end @@ -3147,7 +3308,7 @@ math !! input [[Image:foobar.jpg|thumb|<math>2+2</math>]] !! result -<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/Image:Foobar.jpg" class="image" title="2 + 2"><img alt="2 + 2" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/Image:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div><span class="texhtml">2 + 2</span></div></div></div> +<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image" title="2 + 2"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div><span class="texhtml">2 + 2</span></div></div></div> !! end @@ -3157,7 +3318,7 @@ BUG 648: Frameless image caption with a link !! input [[Image:foobar.jpg|text with a [[link]] in it]] !! result -<p><a href="/wiki/Image:Foobar.jpg" class="image" title="text with a link in it"><img alt="text with a link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> +<p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a link in it"><img alt="text with a link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> </p> !! end @@ -3166,7 +3327,7 @@ BUG 648: Frameless image caption with a link (suffix) !! input [[Image:foobar.jpg|text with a [[link]]foo in it]] !! result -<p><a href="/wiki/Image:Foobar.jpg" class="image" title="text with a linkfoo in it"><img alt="text with a linkfoo in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> +<p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a linkfoo in it"><img alt="text with a linkfoo in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> </p> !! end @@ -3175,7 +3336,7 @@ BUG 648: Frameless image caption with an interwiki link !! input [[Image:foobar.jpg|text with a [[MeatBall:Link]] in it]] !! result -<p><a href="/wiki/Image:Foobar.jpg" class="image" title="text with a MeatBall:Link in it"><img alt="text with a MeatBall:Link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> +<p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a MeatBall:Link in it"><img alt="text with a MeatBall:Link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> </p> !! end @@ -3184,7 +3345,7 @@ BUG 648: Frameless image caption with a piped interwiki link !! input [[Image:foobar.jpg|text with a [[MeatBall:Link|link]] in it]] !! result -<p><a href="/wiki/Image:Foobar.jpg" class="image" title="text with a link in it"><img alt="text with a link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> +<p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a link in it"><img alt="text with a link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> </p> !! end @@ -3193,7 +3354,7 @@ Escape HTML special chars in image alt text !! input [[Image:foobar.jpg|& < > "]] !! result -<p><a href="/wiki/Image:Foobar.jpg" class="image" title="& < > ""><img alt="& < > "" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> +<p><a href="/wiki/File:Foobar.jpg" class="image" title="& < > ""><img alt="& < > "" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> </p> !! end @@ -3202,7 +3363,7 @@ BUG 499: Alt text should have Ӓ, not &1234; !! input [[Image:foobar.jpg|♀]] !! result -<p><a href="/wiki/Image:Foobar.jpg" class="image" title="♀"><img alt="♀" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> +<p><a href="/wiki/File:Foobar.jpg" class="image" title="♀"><img alt="♀" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> </p> !! end @@ -3220,7 +3381,7 @@ Image caption containing another image !! input [[Image:Foobar.jpg|thumb|This is a caption with another [[Image:icon.png|image]] inside it!]] !! result -<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/Image:Foobar.jpg" class="image" title="This is a caption with another Image:Icon.png inside it!"><img alt="This is a caption with another Image:Icon.png inside it!" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/Image:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>This is a caption with another <a href="/index.php?title=Special:Upload&wpDestFile=Icon.png" class="new" title="Image:Icon.png">Image:Icon.png</a> inside it!</div></div></div> +<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image" title="This is a caption with another File:Icon.png inside it!"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>This is a caption with another <a href="/index.php?title=Special:Upload&wpDestFile=Icon.png" class="new" title="File:Icon.png">File:Icon.png</a> inside it!</div></div></div> !! end @@ -3230,7 +3391,7 @@ Image caption containing a newline [[Image:Foobar.jpg|This *is some text]] !! result -<p><a href="/wiki/Image:Foobar.jpg" class="image" title="This *is some text"><img alt="This *is some text" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> +<p><a href="/wiki/File:Foobar.jpg" class="image" title="This *is some text"><img alt="This *is some text" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> </p> !!end @@ -3240,7 +3401,7 @@ Bug 3090: External links other than http: in image captions !! input [[Image:Foobar.jpg|thumb|200px|This caption has [irc://example.net irc] and [https://example.com Secure] ext links in it.]] !! result -<div class="thumb tright"><div class="thumbinner" style="width:202px;"><a href="/wiki/Image:Foobar.jpg" class="image" title="This caption has irc and Secure ext links in it."><img alt="This caption has irc and Secure ext links in it." src="http://example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" width="200" height="23" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/Image:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>This caption has <a href="irc://example.net" class="external text" title="irc://example.net" rel="nofollow">irc</a> and <a href="https://example.com" class="external text" title="https://example.com" rel="nofollow">Secure</a> ext links in it.</div></div></div> +<div class="thumb tright"><div class="thumbinner" style="width:202px;"><a href="/wiki/File:Foobar.jpg" class="image" title="This caption has irc and Secure ext links in it."><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" width="200" height="23" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>This caption has <a href="irc://example.net" class="external text" title="irc://example.net" rel="nofollow">irc</a> and <a href="https://example.com" class="external text" title="https://example.com" rel="nofollow">Secure</a> ext links in it.</div></div></div> !! end @@ -3310,7 +3471,7 @@ Link to category !! input [[:Category:MediaWiki User's Guide]] !! result -<p><a href="/wiki/Category:MediaWiki_User%27s_Guide" title="Category:MediaWiki User's Guide">Category:MediaWiki User's Guide</a> +<p><a href="/wiki/Category:MediaWiki_User%27s_Guide" title="Category:MediaWiki User's Guide">Category:MediaWiki User's Guide</a> </p> !! end @@ -3321,7 +3482,7 @@ cat !! input [[Category:MediaWiki User's Guide]] !! result -<a href="/wiki/Category:MediaWiki_User%27s_Guide" title="Category:MediaWiki User's Guide">MediaWiki User's Guide</a> +<a href="/wiki/Category:MediaWiki_User%27s_Guide" title="Category:MediaWiki User's Guide">MediaWiki User's Guide</a> !! end !! test @@ -3362,13 +3523,13 @@ More ===Smaller headline=== Blah blah !! result -<a name="Headline_1"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Headline 1">edit</a>]</span> <span class="mw-headline"> Headline 1 </span></h2> +<a name="Headline_1" id="Headline_1"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Headline 1">edit</a>]</span> <span class="mw-headline"> Headline 1 </span></h2> <p>Some text </p> -<a name="Headline_2"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Headline 2">edit</a>]</span> <span class="mw-headline">Headline 2</span></h2> +<a name="Headline_2" id="Headline_2"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Headline 2">edit</a>]</span> <span class="mw-headline">Headline 2</span></h2> <p>More </p> -<a name="Smaller_headline"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: Smaller headline">edit</a>]</span> <span class="mw-headline">Smaller headline</span></h3> +<a name="Smaller_headline" id="Smaller_headline"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: Smaller headline">edit</a>]</span> <span class="mw-headline">Smaller headline</span></h3> <p>Blah blah </p> !! end @@ -3407,14 +3568,14 @@ Some text </li> </ul> </td></tr></table><script type="text/javascript"> if (window.showTocToggle) { var tocShowText = "show"; var tocHideText = "hide"; showTocToggle(); } </script> -<a name="Headline_1"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Headline 1">edit</a>]</span> <span class="mw-headline"> Headline 1 </span></h2> -<a name="Subheadline_1"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Subheadline 1">edit</a>]</span> <span class="mw-headline"> Subheadline 1 </span></h3> -<a name="Skipping_a_level"></a><h5><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: Skipping a level">edit</a>]</span> <span class="mw-headline"> Skipping a level </span></h5> -<a name="Skipping_a_level_2"></a><h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: Skipping a level">edit</a>]</span> <span class="mw-headline"> Skipping a level </span></h6> -<a name="Headline_2"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: Headline 2">edit</a>]</span> <span class="mw-headline"> Headline 2 </span></h2> +<a name="Headline_1" id="Headline_1"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Headline 1">edit</a>]</span> <span class="mw-headline"> Headline 1 </span></h2> +<a name="Subheadline_1" id="Subheadline_1"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Subheadline 1">edit</a>]</span> <span class="mw-headline"> Subheadline 1 </span></h3> +<a name="Skipping_a_level" id="Skipping_a_level"></a><h5><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: Skipping a level">edit</a>]</span> <span class="mw-headline"> Skipping a level </span></h5> +<a name="Skipping_a_level_2" id="Skipping_a_level_2"></a><h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: Skipping a level">edit</a>]</span> <span class="mw-headline"> Skipping a level </span></h6> +<a name="Headline_2" id="Headline_2"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: Headline 2">edit</a>]</span> <span class="mw-headline"> Headline 2 </span></h2> <p>Some text </p> -<a name="Another_headline"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=6" title="Edit section: Another headline">edit</a>]</span> <span class="mw-headline">Another headline</span></h3> +<a name="Another_headline" id="Another_headline"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=6" title="Edit section: Another headline">edit</a>]</span> <span class="mw-headline">Another headline</span></h3> !! end @@ -3462,16 +3623,16 @@ Handling of sections up to level 6 and beyond </li> </ul> </td></tr></table><script type="text/javascript"> if (window.showTocToggle) { var tocShowText = "show"; var tocHideText = "hide"; showTocToggle(); } </script> -<a name="Level_1_Heading"></a><h1><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Level 1 Heading">edit</a>]</span> <span class="mw-headline"> Level 1 Heading</span></h1> -<a name="Level_2_Heading"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Level 2 Heading">edit</a>]</span> <span class="mw-headline"> Level 2 Heading</span></h2> -<a name="Level_3_Heading"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: Level 3 Heading">edit</a>]</span> <span class="mw-headline"> Level 3 Heading</span></h3> -<a name="Level_4_Heading"></a><h4><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: Level 4 Heading">edit</a>]</span> <span class="mw-headline"> Level 4 Heading</span></h4> -<a name="Level_5_Heading"></a><h5><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: Level 5 Heading">edit</a>]</span> <span class="mw-headline"> Level 5 Heading</span></h5> -<a name="Level_6_Heading"></a><h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=6" title="Edit section: Level 6 Heading">edit</a>]</span> <span class="mw-headline"> Level 6 Heading</span></h6> -<a name=".3D_Level_7_Heading.3D"></a><h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=7" title="Edit section: = Level 7 Heading=">edit</a>]</span> <span class="mw-headline">= Level 7 Heading=</span></h6> -<a name=".3D.3D_Level_8_Heading.3D.3D"></a><h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=8" title="Edit section: == Level 8 Heading==">edit</a>]</span> <span class="mw-headline">== Level 8 Heading==</span></h6> -<a name=".3D.3D.3D_Level_9_Heading.3D.3D.3D"></a><h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=9" title="Edit section: === Level 9 Heading===">edit</a>]</span> <span class="mw-headline">=== Level 9 Heading===</span></h6> -<a name=".3D.3D.3D.3D_Level_10_Heading.3D.3D.3D.3D"></a><h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=10" title="Edit section: ==== Level 10 Heading====">edit</a>]</span> <span class="mw-headline">==== Level 10 Heading====</span></h6> +<a name="Level_1_Heading" id="Level_1_Heading"></a><h1><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Level 1 Heading">edit</a>]</span> <span class="mw-headline"> Level 1 Heading</span></h1> +<a name="Level_2_Heading" id="Level_2_Heading"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Level 2 Heading">edit</a>]</span> <span class="mw-headline"> Level 2 Heading</span></h2> +<a name="Level_3_Heading" id="Level_3_Heading"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: Level 3 Heading">edit</a>]</span> <span class="mw-headline"> Level 3 Heading</span></h3> +<a name="Level_4_Heading" id="Level_4_Heading"></a><h4><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: Level 4 Heading">edit</a>]</span> <span class="mw-headline"> Level 4 Heading</span></h4> +<a name="Level_5_Heading" id="Level_5_Heading"></a><h5><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: Level 5 Heading">edit</a>]</span> <span class="mw-headline"> Level 5 Heading</span></h5> +<a name="Level_6_Heading" id="Level_6_Heading"></a><h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=6" title="Edit section: Level 6 Heading">edit</a>]</span> <span class="mw-headline"> Level 6 Heading</span></h6> +<a name=".3D_Level_7_Heading.3D" id=".3D_Level_7_Heading.3D"></a><h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=7" title="Edit section: = Level 7 Heading=">edit</a>]</span> <span class="mw-headline">= Level 7 Heading=</span></h6> +<a name=".3D.3D_Level_8_Heading.3D.3D" id=".3D.3D_Level_8_Heading.3D.3D"></a><h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=8" title="Edit section: == Level 8 Heading==">edit</a>]</span> <span class="mw-headline">== Level 8 Heading==</span></h6> +<a name=".3D.3D.3D_Level_9_Heading.3D.3D.3D" id=".3D.3D.3D_Level_9_Heading.3D.3D.3D"></a><h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=9" title="Edit section: === Level 9 Heading===">edit</a>]</span> <span class="mw-headline">=== Level 9 Heading===</span></h6> +<a name=".3D.3D.3D.3D_Level_10_Heading.3D.3D.3D.3D" id=".3D.3D.3D.3D_Level_10_Heading.3D.3D.3D.3D"></a><h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=10" title="Edit section: ==== Level 10 Heading====">edit</a>]</span> <span class="mw-headline">==== Level 10 Heading====</span></h6> !! end @@ -3504,12 +3665,12 @@ TOC regression (bug 9764) </li> </ul> </td></tr></table><script type="text/javascript"> if (window.showTocToggle) { var tocShowText = "show"; var tocHideText = "hide"; showTocToggle(); } </script> -<a name="title_1"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: title 1">edit</a>]</span> <span class="mw-headline"> title 1 </span></h2> -<a name="title_1.1"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: title 1.1">edit</a>]</span> <span class="mw-headline"> title 1.1 </span></h3> -<a name="title_1.1.1"></a><h4><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: title 1.1.1">edit</a>]</span> <span class="mw-headline"> title 1.1.1 </span></h4> -<a name="title_1.2"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: title 1.2">edit</a>]</span> <span class="mw-headline"> title 1.2 </span></h3> -<a name="title_2"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: title 2">edit</a>]</span> <span class="mw-headline"> title 2 </span></h2> -<a name="title_2.1"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=6" title="Edit section: title 2.1">edit</a>]</span> <span class="mw-headline"> title 2.1 </span></h3> +<a name="title_1" id="title_1"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: title 1">edit</a>]</span> <span class="mw-headline"> title 1 </span></h2> +<a name="title_1.1" id="title_1.1"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: title 1.1">edit</a>]</span> <span class="mw-headline"> title 1.1 </span></h3> +<a name="title_1.1.1" id="title_1.1.1"></a><h4><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: title 1.1.1">edit</a>]</span> <span class="mw-headline"> title 1.1.1 </span></h4> +<a name="title_1.2" id="title_1.2"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: title 1.2">edit</a>]</span> <span class="mw-headline"> title 1.2 </span></h3> +<a name="title_2" id="title_2"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: title 2">edit</a>]</span> <span class="mw-headline"> title 2 </span></h2> +<a name="title_2.1" id="title_2.1"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=6" title="Edit section: title 2.1">edit</a>]</span> <span class="mw-headline"> title 2.1 </span></h3> !! end @@ -3540,12 +3701,12 @@ wgMaxTocLevel=3 </li> </ul> </td></tr></table><script type="text/javascript"> if (window.showTocToggle) { var tocShowText = "show"; var tocHideText = "hide"; showTocToggle(); } </script> -<a name="title_1"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: title 1">edit</a>]</span> <span class="mw-headline"> title 1 </span></h2> -<a name="title_1.1"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: title 1.1">edit</a>]</span> <span class="mw-headline"> title 1.1 </span></h3> -<a name="title_1.1.1"></a><h4><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: title 1.1.1">edit</a>]</span> <span class="mw-headline"> title 1.1.1 </span></h4> -<a name="title_1.2"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: title 1.2">edit</a>]</span> <span class="mw-headline"> title 1.2 </span></h3> -<a name="title_2"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: title 2">edit</a>]</span> <span class="mw-headline"> title 2 </span></h2> -<a name="title_2.1"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=6" title="Edit section: title 2.1">edit</a>]</span> <span class="mw-headline"> title 2.1 </span></h3> +<a name="title_1" id="title_1"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: title 1">edit</a>]</span> <span class="mw-headline"> title 1 </span></h2> +<a name="title_1.1" id="title_1.1"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: title 1.1">edit</a>]</span> <span class="mw-headline"> title 1.1 </span></h3> +<a name="title_1.1.1" id="title_1.1.1"></a><h4><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: title 1.1.1">edit</a>]</span> <span class="mw-headline"> title 1.1.1 </span></h4> +<a name="title_1.2" id="title_1.2"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: title 1.2">edit</a>]</span> <span class="mw-headline"> title 1.2 </span></h3> +<a name="title_2" id="title_2"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: title 2">edit</a>]</span> <span class="mw-headline"> title 2 </span></h2> +<a name="title_2.1" id="title_2.1"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=6" title="Edit section: title 2.1">edit</a>]</span> <span class="mw-headline"> title 2.1 </span></h3> !! end @@ -3555,8 +3716,8 @@ Resolving duplicate section names == Foo bar == == Foo bar == !! result -<a name="Foo_bar"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Foo bar">edit</a>]</span> <span class="mw-headline"> Foo bar </span></h2> -<a name="Foo_bar_2"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Foo bar">edit</a>]</span> <span class="mw-headline"> Foo bar </span></h2> +<a name="Foo_bar" id="Foo_bar"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Foo bar">edit</a>]</span> <span class="mw-headline"> Foo bar </span></h2> +<a name="Foo_bar_2" id="Foo_bar_2"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Foo bar">edit</a>]</span> <span class="mw-headline"> Foo bar </span></h2> !! end @@ -3566,8 +3727,8 @@ Resolving duplicate section names with differing case (bug 10721) == Foo bar == == Foo Bar == !! result -<a name="Foo_bar"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Foo bar">edit</a>]</span> <span class="mw-headline"> Foo bar </span></h2> -<a name="Foo_Bar_2"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Foo Bar">edit</a>]</span> <span class="mw-headline"> Foo Bar </span></h2> +<a name="Foo_bar" id="Foo_bar"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Foo bar">edit</a>]</span> <span class="mw-headline"> Foo bar </span></h2> +<a name="Foo_Bar_2" id="Foo_Bar_2"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Foo Bar">edit</a>]</span> <span class="mw-headline"> Foo Bar </span></h2> !! end @@ -3586,10 +3747,10 @@ __NOTOC__ {{sections}} ==Section 4== !! result -<a name="Section_0"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Section 0">edit</a>]</span> <span class="mw-headline">Section 0</span></h2> -<a name="Section_1"></a><h3><span class="editsection">[<a href="/index.php?title=Template:Sections&action=edit&section=T-1" title="Template:Sections">edit</a>]</span> <span class="mw-headline">Section 1</span></h3> -<a name="Section_2"></a><h2><span class="editsection">[<a href="/index.php?title=Template:Sections&action=edit&section=T-2" title="Template:Sections">edit</a>]</span> <span class="mw-headline">Section 2</span></h2> -<a name="Section_4"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Section 4">edit</a>]</span> <span class="mw-headline">Section 4</span></h2> +<a name="Section_0" id="Section_0"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Section 0">edit</a>]</span> <span class="mw-headline">Section 0</span></h2> +<a name="Section_1" id="Section_1"></a><h3><span class="editsection">[<a href="/index.php?title=Template:Sections&action=edit&section=T-1" title="Template:Sections">edit</a>]</span> <span class="mw-headline">Section 1</span></h3> +<a name="Section_2" id="Section_2"></a><h2><span class="editsection">[<a href="/index.php?title=Template:Sections&action=edit&section=T-2" title="Template:Sections">edit</a>]</span> <span class="mw-headline">Section 2</span></h2> +<a name="Section_4" id="Section_4"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Section 4">edit</a>]</span> <span class="mw-headline">Section 4</span></h2> !! end @@ -3600,8 +3761,8 @@ __NOEDITSECTION__ ==Section 1== ==Section 2== !! result -<a name="Section_1"></a><h2> <span class="mw-headline">Section 1</span></h2> -<a name="Section_2"></a><h2> <span class="mw-headline">Section 2</span></h2> +<a name="Section_1" id="Section_1"></a><h2> <span class="mw-headline">Section 1</span></h2> +<a name="Section_2" id="Section_2"></a><h2> <span class="mw-headline">Section 2</span></h2> !! end @@ -3610,7 +3771,7 @@ Link inside a section heading !! input ==Section with a [[Main Page|link]] in it== !! result -<a name="Section_with_a_link_in_it"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Section with a link in it">edit</a>]</span> <span class="mw-headline">Section with a <a href="/wiki/Main_Page" title="Main Page">link</a> in it</span></h2> +<a name="Section_with_a_link_in_it" id="Section_with_a_link_in_it"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Section with a link in it">edit</a>]</span> <span class="mw-headline">Section with a <a href="/wiki/Main_Page" title="Main Page">link</a> in it</span></h2> !! end @@ -3632,9 +3793,9 @@ __TOC__ <li class="toclevel-1"><a href="#title_2"><span class="tocnumber">2</span> <span class="toctext">title 2</span></a></li> </ul> </td></tr></table><script type="text/javascript"> if (window.showTocToggle) { var tocShowText = "show"; var tocHideText = "hide"; showTocToggle(); } </script> -<a name="title_1"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: title 1">edit</a>]</span> <span class="mw-headline"> title 1 </span></h2> -<a name="title_1.1"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: title 1.1">edit</a>]</span> <span class="mw-headline"> title 1.1 </span></h3> -<a name="title_2"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: title 2">edit</a>]</span> <span class="mw-headline"> title 2 </span></h2> +<a name="title_1" id="title_1"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: title 1">edit</a>]</span> <span class="mw-headline"> title 1 </span></h2> +<a name="title_1.1" id="title_1.1"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: title 1.1">edit</a>]</span> <span class="mw-headline"> title 1.1 </span></h3> +<a name="title_2" id="title_2"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: title 2">edit</a>]</span> <span class="mw-headline"> title 2 </span></h2> !! end @@ -3643,7 +3804,7 @@ BUG 1219 URL next to image (good) !! input http://example.com [[Image:foobar.jpg]] !! result -<p><a href="http://example.com" class="external free" title="http://example.com" rel="nofollow">http://example.com</a> <a href="/wiki/Image:Foobar.jpg" class="image" title="Image:foobar.jpg"><img alt="Image:foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> +<p><a href="http://example.com" class="external free" title="http://example.com" rel="nofollow">http://example.com</a> <a href="/wiki/File:Foobar.jpg" class="image" title="Image:foobar.jpg"><img alt="Image:foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> </p> !!end @@ -3652,7 +3813,7 @@ BUG 1219 URL next to image (broken) !! input http://example.com[[Image:foobar.jpg]] !! result -<p><a href="http://example.com" class="external free" title="http://example.com" rel="nofollow">http://example.com</a><a href="/wiki/Image:Foobar.jpg" class="image" title="Image:foobar.jpg"><img alt="Image:foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> +<p><a href="http://example.com" class="external free" title="http://example.com" rel="nofollow">http://example.com</a><a href="/wiki/File:Foobar.jpg" class="image" title="Image:foobar.jpg"><img alt="Image:foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a> </p> !!end @@ -3838,7 +3999,7 @@ Image link to nonexistent file (bug 1850 - good) !! input [[Image:No such.jpg]] !! result -<p><a href="/index.php?title=Special:Upload&wpDestFile=No_such.jpg" class="new" title="Image:No such.jpg">Image:No such.jpg</a> +<p><a href="/index.php?title=Special:Upload&wpDestFile=No_such.jpg" class="new" title="File:No such.jpg">File:No such.jpg</a> </p> !! end @@ -3847,7 +4008,7 @@ Image link to nonexistent file (bug 1850 - good) !! input [[:Image:No such.jpg]] !! result -<p><a href="/index.php?title=Image:No_such.jpg&action=edit&redlink=1" class="new" title="Image:No such.jpg (not yet written)">Image:No such.jpg</a> +<p><a href="/index.php?title=File:No_such.jpg&action=edit&redlink=1" class="new" title="File:No such.jpg (not yet written)">Image:No such.jpg</a> </p> !! end @@ -4802,7 +4963,7 @@ Fuzz testing: Parser14 == onmouseover= == http://__TOC__ !! result -<a name="onmouseover.3D"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: onmouseover=">edit</a>]</span> <span class="mw-headline"> onmouseover= </span></h2> +<a name="onmouseover.3D" id="onmouseover.3D"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: onmouseover=">edit</a>]</span> <span class="mw-headline"> onmouseover= </span></h2> http://<table id="toc" class="toc" summary="Contents"><tr><td><div id="toctitle"><h2>Contents</h2></div> <ul> <li class="toclevel-1"><a href="#onmouseover.3D"><span class="tocnumber">1</span> <span class="toctext">onmouseover=</span></a></li> @@ -4817,7 +4978,7 @@ Fuzz testing: Parser14-table ==a== {| STYLE=__TOC__ !! result -<a name="a"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: a">edit</a>]</span> <span class="mw-headline">a</span></h2> +<a name="a" id="a"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: a">edit</a>]</span> <span class="mw-headline">a</span></h2> <table style="__TOC__"> <tr><td></td></tr> </table> @@ -4903,11 +5064,16 @@ MOVE YOUR MOUSE CURSOR OVER THIS TEXT !! end -# Known to produce bad XML for now +# Note: the current result listed for this is not what the original one was, +# but the original bug was JavaScript injection, which is fixed in any case. +# It's not clear that the original result listed was any more correct than the +# current one. Original result: +# <p>{{{| +# </p> +# <li class="||"> +# }}}blah" onmouseover="alert('hello world');" align="left"<b>MOVE MOUSE CURSOR OVER HERE</b> !!test Fuzz testing: Parser25 (bug 6055) -!! options -noxml !! input {{{ | @@ -4915,11 +5081,8 @@ noxml > }}}blah" onmouseover="alert('hello world');" align="left"'''MOVE MOUSE CURSOR OVER HERE !! result -<p>{{{| +<p><LI CLASS=blah" onmouseover="alert('hello world');" align="left"<b>MOVE MOUSE CURSOR OVER HERE</b> </p> -<li class="||"> -}}}blah" onmouseover="alert('hello world');" align="left"<b>MOVE MOUSE CURSOR OVER HERE</b> - !! end !!test @@ -5048,14 +5211,19 @@ New wiki paragraph </p> !! end - +# Original result was this: +# <p><b>bold</b><b>bold<i>bolditalics</i></b> +# </p> +# While that might be marginally more intuitive, maybe, the six-apostrophe +# construct is clearly pathological and the result stated here (which is what +# the parser actually does) is about as reasonable as anything. !!test Mixing markup for italics and bold !! options !! input '''bold''''''bold''bolditalics''''' !! result -<p><b>bold</b><b>bold<i>bolditalics</i></b> +<p>'<i>bold'</i><b>bold<i>bolditalics</i></b> </p> !! end @@ -6153,7 +6321,7 @@ Centre-aligned image !! input [[Image:foobar.jpg|centre]] !! result -<div class="center"><div class="floatnone"><span><a href="/wiki/Image:Foobar.jpg" class="image" title="Foobar.jpg"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a></span></div></div> +<div class="center"><div class="floatnone"><a href="/wiki/File:Foobar.jpg" class="image" title="Foobar.jpg"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a></div></div> !!end @@ -6162,7 +6330,7 @@ None-aligned image !! input [[Image:foobar.jpg|none]] !! result -<div class="floatnone"><span><a href="/wiki/Image:Foobar.jpg" class="image" title="Foobar.jpg"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a></span></div> +<div class="floatnone"><a href="/wiki/File:Foobar.jpg" class="image" title="Foobar.jpg"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" border="0" /></a></div> !!end @@ -6171,7 +6339,34 @@ Width + Height sized image (using px) (height is ignored) !! input [[Image:foobar.jpg|640x480px]] !! result -<p><a href="/wiki/Image:Foobar.jpg" class="image" title="Foobar.jpg"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg" width="640" height="73" border="0" /></a> +<p><a href="/wiki/File:Foobar.jpg" class="image" title="Foobar.jpg"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg" width="640" height="73" border="0" /></a> +</p> +!!end + +!! test +Width-sized image (using px, no following whitespace) +!! input +[[Image:foobar.jpg|640px]] +!! result +<p><a href="/wiki/File:Foobar.jpg" class="image" title="Foobar.jpg"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg" width="640" height="73" border="0" /></a> +</p> +!!end + +!! test +Width-sized image (using px, with following whitespace - test regression from r39467) +!! input +[[Image:foobar.jpg|640px ]] +!! result +<p><a href="/wiki/File:Foobar.jpg" class="image" title="Foobar.jpg"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg" width="640" height="73" border="0" /></a> +</p> +!!end + +!! test +Width-sized image (using px, with preceding whitespace - test regression from r39467) +!! input +[[Image:foobar.jpg| 640px]] +!! result +<p><a href="/wiki/File:Foobar.jpg" class="image" title="Foobar.jpg"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg" width="640" height="73" border="0" /></a> </p> !!end @@ -6209,7 +6404,7 @@ Images with the "|" character in the comment !! input [[image:Foobar.jpg|thumb|An [http://test/?param1=|left|¶m2=|x external] URL]] !! result -<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/Image:Foobar.jpg" class="image" title="An external URL"><img alt="An external URL" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/Image:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>An <a href="http://test/?param1=|left|&param2=|x" class="external text" title="http://test/?param1=|left|&param2=|x" rel="nofollow">external</a> URL</div></div></div> +<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image" title="An external URL"><img alt="An external URL" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>An <a href="http://test/?param1=|left|&param2=|x" class="external text" title="http://test/?param1=|left|&param2=|x" rel="nofollow">external</a> URL</div></div></div> !!end @@ -6270,8 +6465,6 @@ subpage title=[[Subpage test/L1/L2/L3]] </p> !! end - -# Question: should result be "/index.php?title=Subpage_test/L1&action=edit" instead? !! test Parents of subpages, two levels up, without trailing slash or name. !! options @@ -6279,12 +6472,10 @@ subpage title=[[Subpage test/L1/L2/L3]] !! input [[../..]] !! result -<p><a href="/index.php?title=Subpage_test/L1/L2/..&action=edit&redlink=1" class="new" title="Subpage test/L1 (not yet written)">../..</a> +<p>[[../..]] </p> !! end -# Question: Why should the link text in the above test be "../..", yet in this test the "../.." part is silently dropped? -# Current result: <p><a href="/index.php?title=Subpage_test/L1////&action=edit" class="new" title="Subpage test/L1////">/// !! test Parents of subpages, two levels up, with lots of extra trailing slashes. !! options @@ -6292,7 +6483,7 @@ subpage title=[[Subpage test/L1/L2/L3]] !! input [[../../////]] !! result -<p><a href="/index.php?title=Subpage_test/L1&action=edit&redlink=1" class="new" title="Subpage test/L1 (not yet written)">Subpage test/L1</a> +<p><a href="/index.php?title=Subpage_test/L1////&action=edit&redlink=1" class="new" title="Subpage test/L1//// (not yet written)">///</a> </p> !! end @@ -6370,7 +6561,7 @@ Inclusion of !userCanEdit() content !! input {{MediaWiki:Fake}} !! result -<a name="header"></a><h2><span class="editsection">[<a href="/index.php?title=MediaWiki:Fake&action=edit&section=T-1" title="MediaWiki:Fake">edit</a>]</span> <span class="mw-headline">header</span></h2> +<a name="header" id="header"></a><h2><span class="editsection">[<a href="/index.php?title=MediaWiki:Fake&action=edit&section=T-1" title="MediaWiki:Fake">edit</a>]</span> <span class="mw-headline">header</span></h2> !! end @@ -6401,12 +6592,12 @@ Out-of-order TOC heading levels </li> </ul> </td></tr></table><script type="text/javascript"> if (window.showTocToggle) { var tocShowText = "show"; var tocHideText = "hide"; showTocToggle(); } </script> -<a name="2"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: 2">edit</a>]</span> <span class="mw-headline">2</span></h2> -<a name="6"></a><h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: 6">edit</a>]</span> <span class="mw-headline">6</span></h6> -<a name="3"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: 3">edit</a>]</span> <span class="mw-headline">3</span></h3> -<a name="1"></a><h1><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: 1">edit</a>]</span> <span class="mw-headline">1</span></h1> -<a name="5"></a><h5><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: 5">edit</a>]</span> <span class="mw-headline">5</span></h5> -<a name="2_2"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=6" title="Edit section: 2">edit</a>]</span> <span class="mw-headline">2</span></h2> +<a name="2" id="2"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: 2">edit</a>]</span> <span class="mw-headline">2</span></h2> +<a name="6" id="6"></a><h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: 6">edit</a>]</span> <span class="mw-headline">6</span></h6> +<a name="3" id="3"></a><h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: 3">edit</a>]</span> <span class="mw-headline">3</span></h3> +<a name="1" id="1"></a><h1><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: 1">edit</a>]</span> <span class="mw-headline">1</span></h1> +<a name="5" id="5"></a><h5><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: 5">edit</a>]</span> <span class="mw-headline">5</span></h5> +<a name="2_2" id="2_2"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=6" title="Edit section: 2">edit</a>]</span> <span class="mw-headline">2</span></h2> !! end @@ -6593,7 +6784,7 @@ language=sr cat !! input [[:Category:МедиаWики Усер'с Гуиде]] !! result -<a href="/wiki/Category:MediaWiki_User%27s_Guide" title="Category:MediaWiki User's Guide">MediaWiki User's Guide</a> +<a href="/wiki/Category:MediaWiki_User%27s_Guide" title="Category:MediaWiki User's Guide">MediaWiki User's Guide</a> !! end @@ -6652,7 +6843,7 @@ language=sr variant=sr-ec !! input == -{Naslov}- == !! result -<a name="-.7BNaslov.7D-"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Уреди део: Naslov">уреди</a>]</span> <span class="mw-headline"> Naslov </span></h2> +<a name="-.7BNaslov.7D-" id="-.7BNaslov.7D-"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Уреди део: Naslov">уреди</a>]</span> <span class="mw-headline"> Naslov </span></h2> !! end @@ -6791,7 +6982,7 @@ Morwen/13: Unclosed link followed by heading !! result <p>[[link </p> -<a name="heading"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: heading">edit</a>]</span> <span class="mw-headline">heading</span></h2> +<a name="heading" id="heading"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: heading">edit</a>]</span> <span class="mw-headline">heading</span></h2> !! end @@ -6803,7 +6994,7 @@ HHP2.1: Heuristics for headings in preprocessor parenthetical structures !! result <p>{{foo| </p> -<a name="heading"></a><h1> <span class="mw-headline">heading</span></h1> +<a name="heading" id="heading"></a><h1> <span class="mw-headline">heading</span></h1> !! end @@ -6815,7 +7006,7 @@ HHP2.2: Heuristics for headings in preprocessor parenthetical structures !! result <p>{{foo| </p> -<a name="heading"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: heading">edit</a>]</span> <span class="mw-headline">heading</span></h2> +<a name="heading" id="heading"></a><h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: heading">edit</a>]</span> <span class="mw-headline">heading</span></h2> !! end @@ -6829,6 +7020,219 @@ pst <!-- ~~~~ --> !! end +!! test +Paragraphs inside divs (no extra line breaks) +!! input +<div>Line one + +Line two</div> +!! result +<div>Line one +Line two</div> + +!! end + +!! test +Paragraphs inside divs (extra line break on open) +!! input +<div> +Line one + +Line two</div> +!! result +<div> +<p>Line one +</p> +Line two</div> + +!! end + +!! test +Paragraphs inside divs (extra line break on close) +!! input +<div>Line one + +Line two +</div> +!! result +<div>Line one +<p>Line two +</p> +</div> + +!! end + +!! test +Paragraphs inside divs (extra line break on open and close) +!! input +<div> +Line one + +Line two +</div> +!! result +<div> +<p>Line one +</p><p>Line two +</p> +</div> + +!! end + +# Bug 6200: <blockquote> should behave like <div> with respect to line breaks +!! test +Bug 6200: paragraphs inside blockquotes (no extra line breaks) +!! input +<blockquote>Line one + +Line two</blockquote> +!! result +<blockquote>Line one +Line two</blockquote> + +!! end + +!! test +Bug 6200: paragraphs inside blockquotes (extra line break on open) +!! input +<blockquote> +Line one + +Line two</blockquote> +!! result +<blockquote> +<p>Line one +</p> +Line two</blockquote> + +!! end + +!! test +Bug 6200: paragraphs inside blockquotes (extra line break on close) +!! input +<blockquote>Line one + +Line two +</blockquote> +!! result +<blockquote>Line one +<p>Line two +</p> +</blockquote> + +!! end + +!! test +Bug 6200: paragraphs inside blockquotes (extra line break on open and close) +!! input +<blockquote> +Line one + +Line two +</blockquote> +!! result +<blockquote> +<p>Line one +</p><p>Line two +</p> +</blockquote> + +!! end + +!! test +Paragraphs inside blockquotes/divs (no extra line breaks) +!! input +<blockquote><div>Line one + +Line two</div></blockquote> +!! result +<blockquote><div>Line one +Line two</div></blockquote> + +!! end + +!! test +Paragraphs inside blockquotes/divs (extra line break on open) +!! input +<blockquote><div> +Line one + +Line two</div></blockquote> +!! result +<blockquote><div> +<p>Line one +</p> +Line two</div></blockquote> + +!! end + +!! test +Paragraphs inside blockquotes/divs (extra line break on close) +!! input +<blockquote><div>Line one + +Line two +</div></blockquote> +!! result +<blockquote><div>Line one +<p>Line two +</p> +</div></blockquote> + +!! end + +!! test +Paragraphs inside blockquotes/divs (extra line break on open and close) +!! input +<blockquote><div> +Line one + +Line two +</div></blockquote> +!! result +<blockquote><div> +<p>Line one +</p><p>Line two +</p> +</div></blockquote> + +!! end + +!! test +Interwiki links trounced by replaceExternalLinks after early LinkHolderArray expansion +!! options +wgLinkHolderBatchSize=0 +!! input +[[meatball:1]] +[[meatball:2]] +[[meatball:3]] +!! result +<p><a href="http://www.usemod.com/cgi-bin/mb.pl?1" class="extiw" title="meatball:1">meatball:1</a> +<a href="http://www.usemod.com/cgi-bin/mb.pl?2" class="extiw" title="meatball:2">meatball:2</a> +<a href="http://www.usemod.com/cgi-bin/mb.pl?3" class="extiw" title="meatball:3">meatball:3</a> +</p> +!! end + +!! test +Free external link invading image caption +!! input +[[Image:Foobar.jpg|thumb|http://x|hello]] +!! result +<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image" title="hello"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>hello</div></div></div> + +!! end + +!! test +Bug 15196: localised external link numbers +!! options +language=fa +!! input +[http://en.wikipedia.org/] +!! result +<p><a href="http://en.wikipedia.org/" class="external autonumber" title="http://en.wikipedia.org/" rel="nofollow">[۱]</a> +</p> +!! end + # # # diff --git a/maintenance/parserTestsStaticParserHook.php b/maintenance/parserTestsStaticParserHook.php index 5a98a89d..98c4bba1 100644 --- a/maintenance/parserTestsStaticParserHook.php +++ b/maintenance/parserTestsStaticParserHook.php @@ -21,24 +21,27 @@ function wfParserTestStaticParserHookSetup( &$parser ) { return true; } -function wfParserTestStaticParserHookHook( $in, $argv ) { - static $buf = null; - +function wfParserTestStaticParserHookHook( $in, $argv, $parser ) { if ( ! count( $argv ) ) { - $buf = $in; + $parser->static_tag_buf = $in; return ''; - } else if ( count( $argv ) === 1 && $argv['action'] === 'flush' && $in === null ) { + } else if ( count( $argv ) === 1 && isset( $argv['action'] ) + && $argv['action'] === 'flush' && $in === null ) + { // Clear the buffer, we probably don't need to - $tmp = $buf; - $buf = null; + if ( isset( $parser->static_tag_buf ) ) { + $tmp = $parser->static_tag_buf; + } else { + $tmp = ''; + } + $parser->static_tag_buf = null; return $tmp; } else // wtf? - die( + return "\nCall this extension as <statictag>string</statictag> or as" . " <statictag action=flush/>, not in any other way.\n" . "text: " . var_export( $in, true ) . "\n" . - "argv: " . var_export( $argv, true ) . "\n" - ); + "argv: " . var_export( $argv, true ) . "\n"; } diff --git a/maintenance/postgres/archives/patch-ipb_address_unique.sql b/maintenance/postgres/archives/patch-ipb_address_unique.sql index 9cfc6318..e618f99c 100644 --- a/maintenance/postgres/archives/patch-ipb_address_unique.sql +++ b/maintenance/postgres/archives/patch-ipb_address_unique.sql @@ -1,2 +1 @@ -DROP INDEX IF EXISTS ipb_address; CREATE UNIQUE INDEX ipb_address_unique ON ipblocks (ipb_address,ipb_user,ipb_auto,ipb_anon_only); diff --git a/maintenance/postgres/compare_schemas.pl b/maintenance/postgres/compare_schemas.pl index 84415d79..144663df 100644 --- a/maintenance/postgres/compare_schemas.pl +++ b/maintenance/postgres/compare_schemas.pl @@ -379,6 +379,7 @@ tl_namespace int SMALLINT wl_namespace int SMALLINT ## Easy enough to change if a wiki ever does grow this big: +ss_active_users bigint INTEGER ss_good_articles bigint INTEGER ss_total_edits bigint INTEGER ss_total_pages bigint INTEGER @@ -479,7 +480,7 @@ sub scan_dir { my $dir = shift; opendir my $dh, $dir or die qq{Could not opendir $dir: $!\n}; - print "Scanning $dir...\n"; + #print "Scanning $dir...\n"; for my $file (grep { -f "$dir/$_" and /\.php$/ } readdir $dh) { find_problems("$dir/$file"); } diff --git a/maintenance/postgres/mediawiki_mysql2postgres.pl b/maintenance/postgres/mediawiki_mysql2postgres.pl index 47fa3c0c..a3b17f94 100644 --- a/maintenance/postgres/mediawiki_mysql2postgres.pl +++ b/maintenance/postgres/mediawiki_mysql2postgres.pl @@ -1,7 +1,7 @@ #!/usr/bin/perl ## Convert data from a MySQL mediawiki database into a Postgres mediawiki database -## svn: $Id: mediawiki_mysql2postgres.pl 33556 2008-04-18 16:27:57Z greg $ +## svn: $Id: mediawiki_mysql2postgres.pl 43845 2008-11-22 06:44:45Z greg $ ## NOTE: It is probably easier to dump your wiki using maintenance/dumpBackup.php ## and then import it with maintenance/importDump.php @@ -181,7 +181,7 @@ $MYSQLSOCKET and $conninfo .= "\n-- socket $MYSQLSOCKET"; print qq{ -- Dump of MySQL Mediawiki tables for import into a Postgres Mediawiki schema -- Performed by the program: $0 --- Version: $VERSION (subversion }.q{$LastChangedRevision: 33556 $}.qq{) +-- Version: $VERSION (subversion }.q{$LastChangedRevision: 43845 $}.qq{) -- Author: Greg Sabino Mullane <greg\@turnstep.com> Comments welcome -- -- This file was created: $now @@ -275,7 +275,7 @@ $verbose and warn qq{Writing truncates to empty existing tables\n}; for my $t (@torder, 'objectcache', 'querycache') { next if $t eq '---'; my $tname = $special{$t}||$t; - printf qq{TRUNCATE TABLE %-20s;\n}, qq{"$tname"}; + printf qq{TRUNCATE TABLE %-20s CASCADE;\n}, qq{"$tname"}; } print "\n\n"; diff --git a/maintenance/postgres/tables.sql b/maintenance/postgres/tables.sql index cd8dd8a7..93cee862 100644 --- a/maintenance/postgres/tables.sql +++ b/maintenance/postgres/tables.sql @@ -173,6 +173,7 @@ CREATE TABLE templatelinks ( tl_title TEXT NOT NULL ); CREATE UNIQUE INDEX templatelinks_unique ON templatelinks (tl_namespace,tl_title,tl_from); +CREATE INDEX templatelinks_from ON templatelinks (tl_from); CREATE TABLE imagelinks ( il_from INTEGER NOT NULL REFERENCES page(page_id) ON DELETE CASCADE, @@ -210,9 +211,10 @@ CREATE TABLE site_stats ( ss_row_id INTEGER NOT NULL UNIQUE, ss_total_views INTEGER DEFAULT 0, ss_total_edits INTEGER DEFAULT 0, - ss_good_articles INTEGER DEFAULT 0, + ss_good_articles INTEGER DEFAULT 0, ss_total_pages INTEGER DEFAULT -1, ss_users INTEGER DEFAULT -1, + ss_active_users INTEGER DEFAULT -1, ss_admins INTEGER DEFAULT -1, ss_images INTEGER DEFAULT 0 ); @@ -239,7 +241,9 @@ CREATE TABLE ipblocks ( ipb_range_start TEXT, ipb_range_end TEXT, ipb_deleted SMALLINT NOT NULL DEFAULT 0, - ipb_block_email SMALLINT NOT NULL DEFAULT 0 + ipb_block_email SMALLINT NOT NULL DEFAULT 0, + ipb_allow_usertalk SMALLINT NOT NULL DEFAULT 0 + ); CREATE UNIQUE INDEX ipb_address_unique ON ipblocks (ipb_address,ipb_user,ipb_auto,ipb_anon_only); CREATE INDEX ipb_user ON ipblocks (ipb_user); @@ -350,6 +354,7 @@ CREATE TABLE recentchanges ( rc_params TEXT ); CREATE INDEX rc_timestamp ON recentchanges (rc_timestamp); +CREATE INDEX rc_timestamp_bot ON recentchanges (rc_timestamp) WHERE rc_bot = 0; CREATE INDEX rc_namespace_title ON recentchanges (rc_namespace, rc_title); CREATE INDEX rc_cur_id ON recentchanges (rc_cur_id); CREATE INDEX new_name_timestamp ON recentchanges (rc_new, rc_namespace, rc_timestamp); @@ -363,7 +368,7 @@ CREATE TABLE watchlist ( wl_notificationtimestamp TIMESTAMPTZ ); CREATE UNIQUE INDEX wl_user_namespace_title ON watchlist (wl_namespace, wl_title, wl_user); - +CREATE INDEX wl_user ON watchlist (wl_user); CREATE TABLE math ( math_inputhash BYTEA NOT NULL UNIQUE, @@ -568,5 +573,5 @@ CREATE TABLE mediawiki_version ( ); INSERT INTO mediawiki_version (type,mw_version,sql_version,sql_date) - VALUES ('Creation','??','$LastChangedRevision: 40517 $','$LastChangedDate: 2008-09-06 02:14:20 -0500 (Sat, 06 Sep 2008) $'); + VALUES ('Creation','??','$LastChangedRevision: 41967 $','$LastChangedDate: 2008-10-11 07:08:10 -0500 (Sat, 11 Oct 2008) $'); diff --git a/maintenance/rebuildFileCache.php b/maintenance/rebuildFileCache.php new file mode 100644 index 00000000..125b8842 --- /dev/null +++ b/maintenance/rebuildFileCache.php @@ -0,0 +1,91 @@ +<?php +/** + * Build file cache for content pages + * + * @file + * @ingroup Maintenance + */ + +/** */ +require_once( "commandLine.inc" ); +if( !$wgUseFileCache ) { + echo "Nothing to do -- \$wgUseFileCache is disabled.\n"; + exit(0); +} +$wgDisableCounters = false; // no real hits here + +$start = isset($args[0]) ? intval($args[0]) : 0; +$overwrite = isset( $args[1] ) && $args[1] === 'overwrite'; +echo "Building content page file cache from page {$start}!\n"; + +$dbr = wfGetDB( DB_SLAVE ); +$start = $start > 0 ? $start : $dbr->selectField( 'page', 'MIN(page_id)', false, __FUNCTION__ ); +$end = $dbr->selectField( 'page', 'MAX(page_id)', false, __FUNCTION__ ); +if( !$start ) { + die("Nothing to do.\n"); +} + +$_SERVER['HTTP_ACCEPT_ENCODING'] = 'bgzip'; // hack, no real client +OutputPage::setEncodings(); # Not really used yet + +$BATCH_SIZE = 100; +# Do remaining chunk +$end += $BATCH_SIZE - 1; +$blockStart = $start; +$blockEnd = $start + $BATCH_SIZE - 1; + +// Go through each page and save the output +while( $blockEnd <= $end ) { + // Get the pages + $res = $dbr->select( 'page', array('page_namespace','page_title','page_id'), + array('page_namespace' => $wgContentNamespaces, + "page_id BETWEEN $blockStart AND $blockEnd" ), + array('ORDER BY' => 'page_id ASC','USE INDEX' => 'PRIMARY') + ); + while( $row = $dbr->fetchObject( $res ) ) { + $rebuilt = false; + $wgTitle = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); + if( null == $wgTitle ) { + echo "Page {$row->page_id} bad title\n"; + continue; // broken title? + } + $wgArticle = new Article( $wgTitle ); + // If the article is cacheable, then load it + if( $wgArticle->isFileCacheable() ) { + $cache = new HTMLFileCache( $wgTitle ); + if( $cache->isFileCacheGood() ) { + if( $overwrite ) { + $rebuilt = true; + } else { + echo "Page {$row->page_id} already cached\n"; + continue; // done already! + } + } else { + echo "Page {$row->page_id} not cached\n"; + } + ob_start( array(&$cache, 'saveToFileCache' ) ); // save on ob_end_clean() + $wgUseFileCache = false; // hack, we don't want $wgArticle fiddling with filecache + $wgArticle->view(); + @$wgOut->output(); // header notices + $wgUseFileCache = true; + ob_end_clean(); // clear buffer + $wgOut = new OutputPage(); // empty out any output page garbage + if( $rebuilt ) + echo "Re-cached page {$row->page_id}\n"; + else + echo "Cached page {$row->page_id}\n"; + } else { + echo "Page {$row->page_id} not cacheable\n"; + } + } + $blockStart += $BATCH_SIZE; + $blockEnd += $BATCH_SIZE; + wfWaitForSlaves( 5 ); +} +echo "Done!\n"; + +// Remove these to be safe +if( isset($wgTitle) ) + unset($wgTitle); +if( isset($wgArticle) ) + unset($wgArticle); diff --git a/maintenance/refreshLinks.inc b/maintenance/refreshLinks.inc index 6d68e277..036d4109 100644 --- a/maintenance/refreshLinks.inc +++ b/maintenance/refreshLinks.inc @@ -17,7 +17,9 @@ function refreshLinks( $start, $newOnly = false, $maxLag = false, $end = 0, $red $wgUser->setOption('math', MW_MATH_SOURCE); # Don't generate extension images (e.g. Timeline) - $wgParser->clearTagHooks(); + if( method_exists( $wgParser, "clearTagHooks" ) ) { + $wgParser->clearTagHooks(); + } # Don't use HTML tidy $wgUseTidy = false; @@ -110,13 +112,13 @@ function fixRedirect( $id ){ function fixLinksFromArticle( $id ) { global $wgTitle, $wgParser; - + $wgTitle = Title::newFromID( $id ); $dbw = wfGetDB( DB_MASTER ); $linkCache =& LinkCache::singleton(); $linkCache->clear(); - + if ( is_null( $wgTitle ) ) { return; } diff --git a/maintenance/removeUnusedAccounts.inc b/maintenance/removeUnusedAccounts.inc index 8f900272..02c07c1f 100644 --- a/maintenance/removeUnusedAccounts.inc +++ b/maintenance/removeUnusedAccounts.inc @@ -39,6 +39,8 @@ function isInactiveAccount( $id, $master = false ) { function showHelp() { echo( "Delete unused user accounts from the database.\n\n" ); echo( "USAGE: php removeUnusedAccounts.php [--delete]\n\n" ); - echo( " --delete : Delete accounts which are discovered to be inactive\n" ); + echo( " --delete : Delete accounts which are discovered to be inactive\n" ); + echo( " --ignore-touched=x : Ignore accounts touched within the lasts x days\n" ); + echo( " --ignore-groups=x,y : Ignore accounts within these groups\n" ); echo( "\n" ); } diff --git a/maintenance/removeUnusedAccounts.php b/maintenance/removeUnusedAccounts.php index 636832cb..419955b0 100644 --- a/maintenance/removeUnusedAccounts.php +++ b/maintenance/removeUnusedAccounts.php @@ -8,10 +8,6 @@ * @author Rob Church <robchur@gmail.com> */ -/** - * @todo Don't delete sysops or bureaucrats - */ - $options = array( 'help', 'delete' ); require_once( 'commandLine.inc' ); require_once( 'removeUnusedAccounts.inc' ); @@ -27,10 +23,29 @@ if( isset( $options['help'] ) ) { echo( "Checking for unused user accounts...\n" ); $del = array(); $dbr = wfGetDB( DB_SLAVE ); -$res = $dbr->select( 'user', array( 'user_id', 'user_name' ), '', $fname ); +$res = $dbr->select( 'user', array( 'user_id', 'user_name', 'user_touched' ), '', $fname ); +if( isset( $options['ignore-groups'] ) ) { + $excludedGroups = explode( ',', $options['ignore-groups'] ); +} else { $excludedGroups = array(); } +$touchedSeconds = 0; +if( isset( $options['ignore-touched'] ) ) { + $touchedParamError = 0; + if( ctype_digit( $options['ignore-touched'] ) ) { + if( $options['ignore-touched'] <= 0 ) { + $touchedParamError = 1; + } + } else { $touchedParamError = 1; } + if( $touchedParamError == 1 ) { + die( "Please put a valid positive integer on the --ignore-touched parameter.\n" ); + } else { $touchedSeconds = 86400 * $options['ignore-touched']; } +} while( $row = $dbr->fetchObject( $res ) ) { - # Check the account, but ignore it if it's the primary administrator - if( $row->user_id > 1 && isInactiveAccount( $row->user_id, true ) ) { + # Check the account, but ignore it if it's within a $excludedGroups group or if it's touched within the $touchedSeconds seconds. + $instance = User::newFromId( $row->user_id ); + if( count( array_intersect( $instance->getEffectiveGroups(), $excludedGroups ) ) == 0 + && isInactiveAccount( $row->user_id, true ) + && wfTimestamp( TS_UNIX, $row->user_touched ) < wfTimestamp( TS_UNIX, time() - $touchedSeconds ) + ) { # Inactive; print out the name and flag it $del[] = $row->user_id; echo( $row->user_name . "\n" ); @@ -53,5 +68,3 @@ if( $count > 0 && isset( $options['delete'] ) ) { echo( "\nRun the script again with --delete to remove them from the database.\n" ); } echo( "\n" ); - - diff --git a/maintenance/renameDbPrefix.php b/maintenance/renameDbPrefix.php new file mode 100644 index 00000000..17568b4a --- /dev/null +++ b/maintenance/renameDbPrefix.php @@ -0,0 +1,68 @@ +<?php +/** + * Run this script to after changing $wgDBPrefix on a wiki. + * The wiki will have to get downtime to do this correctly. + * + * @file + * @ingroup Maintenance + */ +$optionsWithArgs = array('old','new','help'); + +require_once( 'commandLine.inc' ); + +if( @$options['help'] || !isset($options['old']) || !isset($options['new']) ) { + print "usage:updateSpecialPages.php [--help] [--old x] [new y]\n"; + print " --help : this help message\n"; + print " --old x : old db prefix x\n"; + print " --old 0 : EMPTY old db prefix x\n"; + print " --new y : new db prefix y\n"; + print " --new 0 : EMPTY new db prefix\n"; + wfDie(); +} + +// Allow for no old prefix +if( $options['old'] === '0' ) { + $old = ''; +} else { + // Use nice safe, sane, prefixes + preg_match( '/^[a-zA-Z]+_$/', $options['old'], $m ); + $old = isset($m[0]) ? $m[0] : false; +} +// Allow for no new prefix +if( $options['new'] === '0' ) { + $new = ''; +} else { + // Use nice safe, sane, prefixes + preg_match( '/^[a-zA-Z]+_$/', $options['new'], $m ); + $new = isset($m[0]) ? $m[0] : false; +} + +if( $old===false || $new===false ) { + print "Invalid prefix!\n"; + wfDie(); +} +if( $old === $new ) { + print "Same prefix. Nothing to rename!\n"; + wfDie(); +} + +print "Renaming DB prefix for tables of $wgDBname from '$old' to '$new'\n"; +$count = 0; + +$dbw = wfGetDB( DB_MASTER ); +$res = $dbw->query( "SHOW TABLES LIKE '".$dbw->escapeLike($old)."%'" ); +foreach( $res as $row ) { + // XXX: odd syntax. MySQL outputs an oddly cased "Tables of X" + // sort of message. Best not to try $row->x stuff... + $fields = get_object_vars( $row ); + // Silly for loop over one field... + foreach( $fields as $resName => $table ) { + // $old should be regexp safe ([a-zA-Z_]) + $newTable = preg_replace( '/^'.$old.'/',$new,$table); + print "Renaming table $table to $newTable\n"; + $dbw->query( "RENAME TABLE $table TO $newTable" ); + } + $count++; +} +print "Done! [$count tables]\n"; + diff --git a/maintenance/runJobs.php b/maintenance/runJobs.php index 14d08091..cee9cb1e 100644 --- a/maintenance/runJobs.php +++ b/maintenance/runJobs.php @@ -34,7 +34,7 @@ $conds = ''; if ($type !== false) $conds = "job_cmd = " . $dbw->addQuotes($type); -while ( $dbw->selectField( 'job', 'count(*)', $conds, 'runJobs.php' ) ) { +while ( $dbw->selectField( 'job', 'job_id', $conds, 'runJobs.php' ) ) { $offset=0; for (;;) { $job = ($type == false) ? diff --git a/maintenance/sql.php b/maintenance/sql.php index 38c995ac..ab6546b9 100644 --- a/maintenance/sql.php +++ b/maintenance/sql.php @@ -53,15 +53,15 @@ class SqlPromptPrinter { } } -function sqlPrintResult( $res ) { +function sqlPrintResult( $res, $db ) { if ( !$res ) { // Do nothing - } elseif ( $res->numRows() ) { + } elseif ( is_object( $res ) && $res->numRows() ) { while ( $row = $res->fetchObject() ) { print_r( $row ); } } else { - $affected = $res->db->affectedRows(); + $affected = $db->affectedRows(); echo "Query OK, $affected row(s) affected\n"; } } diff --git a/maintenance/stats.php b/maintenance/stats.php index 9c16e12d..00f79ded 100644 --- a/maintenance/stats.php +++ b/maintenance/stats.php @@ -11,11 +11,13 @@ require_once('commandLine.inc'); if( get_class( $wgMemc ) == 'FakeMemCachedClient' ) { die("You are running FakeMemCachedClient, I can not provide any statistics.\n"); } - -print "Requests\n"; $session = intval($wgMemc->get(wfMemcKey('stats','request_with_session'))); $noSession = intval($wgMemc->get(wfMemcKey('stats','request_without_session'))); $total = $session + $noSession; +if ( $total == 0 ) { + die("You either have no stats or memcached isn't running. Aborting.\n"); +} +print "Requests\n"; printf( "with session: %-10d %6.2f%%\n", $session, $session/$total*100 ); printf( "without session: %-10d %6.2f%%\n", $noSession, $noSession/$total*100 ); printf( "total: %-10d %6.2f%%\n", $total, 100 ); diff --git a/maintenance/storage/blob_tracking.sql b/maintenance/storage/blob_tracking.sql new file mode 100644 index 00000000..6cac9a38 --- /dev/null +++ b/maintenance/storage/blob_tracking.sql @@ -0,0 +1,57 @@ + +-- Table for tracking blobs prior to recompression or similar maintenance operations + +CREATE TABLE /*$wgDBprefix*/blob_tracking ( + -- page.page_id + -- This may be zero for orphan or deleted text + -- Note that this is for compression grouping only -- it doesn't need to be + -- accurate at the time recompressTracked is run. Operations such as a + -- delete/undelete cycle may make it inaccurate. + bt_page integer not null, + + -- revision.rev_id + -- This may be zero for orphan or deleted text + -- Like bt_page, it does not need to be accurate when recompressTracked is run. + bt_rev_id integer not null, + + -- text.old_id + bt_text_id integer not null, + + -- The ES cluster + bt_cluster varbinary(255), + + -- The ES blob ID + bt_blob_id integer not null, + + -- The CGZ content hash, or null + bt_cgz_hash varbinary(255), + + -- The URL this blob is to be moved to + bt_new_url varbinary(255), + + -- True if the text table has been updated to point to bt_new_url + bt_moved bool not null default 0, + + -- Primary key + -- Note that text_id is not unique due to null edits (protection, move) + -- moveTextRow(), commit(), trackOrphanText() + PRIMARY KEY (bt_text_id, bt_rev_id), + + -- Sort by page for easy CGZ recompression + -- doAllPages(), doAllOrphans(), doPage(), finishIncompleteMoves() + KEY (bt_moved, bt_page, bt_text_id), + + -- Key for determining the revisions using a given blob + -- Not used by any scripts yet + KEY (bt_cluster, bt_blob_id, bt_cgz_hash) + +) /*$wgDBTableOptions*/; + +-- Tracking table for blob rows that aren't tracked by the text table +CREATE TABLE /*$wgDBprefix*/blob_orphans ( + bo_cluster varbinary(255), + bo_blob_id integer not null, + + PRIMARY KEY (bo_cluster, bo_blob_id) +) /*$wgDBTableOptions*/; + diff --git a/maintenance/storage/compressOld.inc b/maintenance/storage/compressOld.inc index 52b9c40b..fb8cc422 100644 --- a/maintenance/storage/compressOld.inc +++ b/maintenance/storage/compressOld.inc @@ -4,10 +4,6 @@ * @ingroup Maintenance ExternalStorage */ -/** */ -require_once( 'Revision.php' ); -require_once( 'ExternalStoreDB.php' ); - /** @todo document */ function compressOldPages( $start = 0, $extdb = '' ) { $fname = 'compressOldPages'; @@ -70,7 +66,7 @@ define( 'LS_INDIVIDUAL', 0 ); define( 'LS_CHUNKED', 1 ); /** @todo document */ -function compressWithConcat( $startId, $maxChunkSize, $maxChunkFactor, $factorThreshold, $beginDate, +function compressWithConcat( $startId, $maxChunkSize, $beginDate, $endDate, $extdb="", $maxPageId = false ) { $fname = 'compressWithConcat'; @@ -198,7 +194,7 @@ function compressWithConcat( $startId, $maxChunkSize, $maxChunkFactor, $factorTh $primaryOldid = $revs[$i]->rev_text_id; # Get the text of each revision and add it to the object - for ( $j = 0; $j < $thisChunkSize && $chunk->isHappy( $maxChunkFactor, $factorThreshold ); $j++ ) { + for ( $j = 0; $j < $thisChunkSize && $chunk->isHappy(); $j++ ) { $oldid = $revs[$i + $j]->rev_text_id; # Get text @@ -229,7 +225,7 @@ function compressWithConcat( $startId, $maxChunkSize, $maxChunkFactor, $factorTh $stub = false; print 'x'; } else { - $stub = $chunk->addItem( $text ); + $stub = new HistoryBlobStub( $chunk->addItem( $text ) ); $stub->setLocation( $primaryOldid ); $stub->setReferrer( $oldid ); print '.'; diff --git a/maintenance/storage/compressOld.php b/maintenance/storage/compressOld.php index dda765d7..6f8b48eb 100644 --- a/maintenance/storage/compressOld.php +++ b/maintenance/storage/compressOld.php @@ -18,8 +18,6 @@ * -b <begin-date> earliest date to check for uncompressed revisions * -e <end-date> latest revision date to compress * -s <start-id> the old_id to start from - * -f <max-factor> the maximum ratio of compressed chunk bytes to uncompressed avg. revision bytes - * -h <threshold> is a minimum number of KB, where <max-factor> cuts in * --extdb <cluster> store specified revisions in an external cluster (untested) * * @file @@ -40,8 +38,6 @@ $defaults = array( 't' => 'concat', 'c' => 20, 's' => 0, - 'f' => 5, - 'h' => 100, 'b' => '', 'e' => '', 'extdb' => '', @@ -62,7 +58,7 @@ if ( $options['extdb'] != '' ) { $success = true; if ( $options['t'] == 'concat' ) { - $success = compressWithConcat( $options['s'], $options['c'], $options['f'], $options['h'], $options['b'], + $success = compressWithConcat( $options['s'], $options['c'], $options['b'], $options['e'], $options['extdb'], $options['endid'] ); } else { compressOldPages( $options['s'], $options['extdb'] ); diff --git a/maintenance/storage/dumpRev.php b/maintenance/storage/dumpRev.php index 720eb958..c84d8aa5 100644 --- a/maintenance/storage/dumpRev.php +++ b/maintenance/storage/dumpRev.php @@ -6,13 +6,51 @@ require_once( dirname(__FILE__) . '/../commandLine.inc' ); +$wgDebugLogFile = '/dev/stdout'; + + $dbr = wfGetDB( DB_SLAVE ); -$row = $dbr->selectRow( 'text', array( 'old_flags', 'old_text' ), array( 'old_id' => $args[0] ) ); -$obj = unserialize( $row->old_text ); +$row = $dbr->selectRow( + array( 'text', 'revision' ), + array( 'old_flags', 'old_text' ), + array( 'old_id=rev_text_id', 'rev_id' => $args[0] ) +); +if ( !$row ) { + print "Row not found\n"; + exit; +} -if ( get_class( $obj ) == 'concatenatedgziphistoryblob' ) { - print_r( array_keys( $obj->mItems ) ); -} else { - var_dump( $obj ); +$flags = explode( ',', $row->old_flags ); +$text = $row->old_text; +if ( in_array( 'external', $flags ) ) { + print "External $text\n"; + if ( preg_match( '!^DB://(\w+)/(\w+)/(\w+)$!', $text, $m ) ) { + $es = ExternalStore::getStoreObject( 'DB' ); + $blob = $es->fetchBlob( $m[1], $m[2], $m[3] ); + if ( strtolower( get_class( $blob ) ) == 'concatenatedgziphistoryblob' ) { + print "Found external CGZ\n"; + $blob->uncompress(); + print "Items: (" . implode( ', ', array_keys( $blob->mItems ) ) . ")\n"; + $text = $blob->getItem( $m[3] ); + } else { + print "CGZ expected at $text, got " . gettype( $blob ) . "\n"; + $text = $blob; + } + } else { + print "External plain $text\n"; + $text = ExternalStore::fetchFromURL( $text ); + } +} +if ( in_array( 'gzip', $flags ) ) { + $text = gzinflate( $text ); +} +if ( in_array( 'object', $flags ) ) { + $text = unserialize( $text ); } +if ( is_object( $text ) ) { + print "Unexpectedly got object of type: " . get_class( $text ) . "\n"; +} else { + print "Text length: " . strlen( $text ) ."\n"; + print substr( $text, 0, 100 ) . "\n"; +} diff --git a/maintenance/storage/orphanStats.php b/maintenance/storage/orphanStats.php new file mode 100644 index 00000000..afea815e --- /dev/null +++ b/maintenance/storage/orphanStats.php @@ -0,0 +1,46 @@ +<?php + +/** + * Show some statistics on the blob_orphans table, created with trackBlobs.php + */ +require_once( dirname(__FILE__).'/../commandLine.inc' ); + +$stats = new OrphanStats; +$stats->execute(); + +class OrphanStats { + function getDB( $cluster ) { + $lb = wfGetLBFactory()->getExternalLB( $cluster ); + return $lb->getConnection( DB_SLAVE ); + } + + function execute() { + $extDBs = array(); + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'blob_orphans', '*', false, __METHOD__ ); + + $num = 0; + $totalSize = 0; + $hashes = array(); + $maxSize = 0; + + foreach ( $res as $boRow ) { + $extDB = $this->getDB( $boRow->bo_cluster ); + $blobRow = $extDB->selectRow( 'blobs', '*', array( 'blob_id' => $boRow->bo_blob_id ), __METHOD__ ); + + $num++; + $size = strlen( $blobRow->blob_text ); + $totalSize += $size; + $hashes[ sha1( $blobRow->blob_text ) ] = true; + $maxSize = max( $size, $maxSize ); + } + unset( $res ); + + echo "Number of orphans: $num\n"; + if ( $num > 0 ) { + echo "Average size: " . round( $totalSize / $num, 0 ) . " bytes\n" . + "Max size: $maxSize\n" . + "Number of unique texts: " . count( $hashes ) . "\n"; + } + } +} diff --git a/maintenance/storage/recompressTracked.php b/maintenance/storage/recompressTracked.php new file mode 100644 index 00000000..7e4ed1b4 --- /dev/null +++ b/maintenance/storage/recompressTracked.php @@ -0,0 +1,742 @@ +<?php + +$optionsWithArgs = RecompressTracked::getOptionsWithArgs(); +require( dirname( __FILE__ ) .'/../commandLine.inc' ); + +if ( count( $args ) < 1 ) { + echo "Usage: php recompressTracked.php [options] <cluster> [... <cluster>...] +Moves blobs indexed by trackBlobs.php to a specified list of destination clusters, and recompresses them in the process. Restartable. + +Options: + --procs <procs> Set the number of child processes (default 1) + --copy-only Copy only, do not update the text table. Restart without this option to complete. + --debug-log <file> Log debugging data to the specified file + --info-log <file> Log progress messages to the specified file + --critical-log <file> Log error messages to the specified file +"; + exit( 1 ); +} + +$job = RecompressTracked::newFromCommandLine( $args, $options ); +$job->execute(); + +class RecompressTracked { + var $destClusters; + var $batchSize = 1000; + var $orphanBatchSize = 1000; + var $reportingInterval = 10; + var $numProcs = 1; + var $useDiff, $pageBlobClass, $orphanBlobClass; + var $slavePipes, $slaveProcs, $prevSlaveId; + var $copyOnly = false; + var $isChild = false; + var $slaveId = false; + var $debugLog, $infoLog, $criticalLog; + var $store; + + static $optionsWithArgs = array( 'procs', 'slave-id', 'debug-log', 'info-log', 'critical-log' ); + static $cmdLineOptionMap = array( + 'procs' => 'numProcs', + 'copy-only' => 'copyOnly', + 'child' => 'isChild', + 'slave-id' => 'slaveId', + 'debug-log' => 'debugLog', + 'info-log' => 'infoLog', + 'critical-log' => 'criticalLog', + ); + + static function getOptionsWithArgs() { + return self::$optionsWithArgs; + } + + static function newFromCommandLine( $args, $options ) { + $jobOptions = array( 'destClusters' => $args ); + foreach ( self::$cmdLineOptionMap as $cmdOption => $classOption ) { + if ( isset( $options[$cmdOption] ) ) { + $jobOptions[$classOption] = $options[$cmdOption]; + } + } + return new self( $jobOptions ); + } + + function __construct( $options ) { + foreach ( $options as $name => $value ) { + $this->$name = $value; + } + $this->store = new ExternalStoreDB; + if ( !$this->isChild ) { + $GLOBALS['wgDebugLogPrefix'] = "RCT M: "; + } elseif ( $this->slaveId !== false ) { + $GLOBALS['wgDebugLogPrefix'] = "RCT {$this->slaveId}: "; + } + $this->useDiff = function_exists( 'xdiff_string_bdiff' ); + $this->pageBlobClass = $this->useDiff ? 'DiffHistoryBlob' : 'ConcatenatedGzipHistoryBlob'; + $this->orphanBlobClass = 'ConcatenatedGzipHistoryBlob'; + } + + function debug( $msg ) { + wfDebug( "$msg\n" ); + if ( $this->debugLog ) { + $this->logToFile( $msg, $this->debugLog ); + } + + } + + function info( $msg ) { + echo "$msg\n"; + if ( $this->infoLog ) { + $this->logToFile( $msg, $this->infoLog ); + } + } + + function critical( $msg ) { + echo "$msg\n"; + if ( $this->criticalLog ) { + $this->logToFile( $msg, $this->criticalLog ); + } + } + + function logToFile( $msg, $file ) { + $header = '[' . date('d\TH:i:s') . '] ' . wfHostname() . ' ' . posix_getpid(); + if ( $this->slaveId !== false ) { + $header .= "({$this->slaveId})"; + } + $header .= ' ' . wfWikiID(); + wfErrorLog( sprintf( "%-50s %s\n", $header, $msg ), $file ); + } + + /** + * Wait until the selected slave has caught up to the master. + * This allows us to use the slave for things that were committed in a + * previous part of this batch process. + */ + function syncDBs() { + $dbw = wfGetDB( DB_MASTER ); + $dbr = wfGetDB( DB_SLAVE ); + $pos = $dbw->getMasterPos(); + $dbr->masterPosWait( $pos, 100000 ); + } + + /** + * Execute parent or child depending on the isChild option + */ + function execute() { + if ( $this->isChild ) { + $this->executeChild(); + } else { + $this->executeParent(); + } + } + + /** + * Execute the parent process + */ + function executeParent() { + if ( !$this->checkTrackingTable() ) { + return; + } + + $this->syncDBs(); + $this->startSlaveProcs(); + $this->doAllPages(); + $this->doAllOrphans(); + $this->killSlaveProcs(); + } + + /** + * Make sure the tracking table exists and isn't empty + */ + function checkTrackingTable() { + $dbr = wfGetDB( DB_SLAVE ); + if ( !$dbr->tableExists( 'blob_tracking' ) ) { + $this->critical( "Error: blob_tracking table does not exist" ); + return false; + } + $row = $dbr->selectRow( 'blob_tracking', '*', false, __METHOD__ ); + if ( !$row ) { + $this->info( "Warning: blob_tracking table contains no rows, skipping this wiki." ); + return false; + } + return true; + } + + /** + * Start the worker processes. + * These processes will listen on stdin for commands. + * This necessary because text recompression is slow: loading, compressing and + * writing are all slow. + */ + function startSlaveProcs() { + $cmd = 'php ' . wfEscapeShellArg( __FILE__ ); + foreach ( self::$cmdLineOptionMap as $cmdOption => $classOption ) { + if ( $cmdOption == 'slave-id' ) { + continue; + } elseif ( in_array( $cmdOption, self::$optionsWithArgs ) && isset( $this->$classOption ) ) { + $cmd .= " --$cmdOption " . wfEscapeShellArg( $this->$classOption ); + } elseif ( $this->$classOption ) { + $cmd .= " --$cmdOption"; + } + } + $cmd .= ' --child' . + ' --wiki ' . wfEscapeShellArg( wfWikiID() ) . + ' ' . call_user_func_array( 'wfEscapeShellArg', $this->destClusters ); + + $this->slavePipes = $this->slaveProcs = array(); + for ( $i = 0; $i < $this->numProcs; $i++ ) { + $pipes = false; + $spec = array( + array( 'pipe', 'r' ), + array( 'file', 'php://stdout', 'w' ), + array( 'file', 'php://stderr', 'w' ) + ); + wfSuppressWarnings(); + $proc = proc_open( "$cmd --slave-id $i", $spec, $pipes ); + wfRestoreWarnings(); + if ( !$proc ) { + $this->critical( "Error opening slave process: $cmd" ); + exit( 1 ); + } + $this->slaveProcs[$i] = $proc; + $this->slavePipes[$i] = $pipes[0]; + } + $this->prevSlaveId = -1; + } + + /** + * Gracefully terminate the child processes + */ + function killSlaveProcs() { + $this->info( "Waiting for slave processes to finish..." ); + for ( $i = 0; $i < $this->numProcs; $i++ ) { + $this->dispatchToSlave( $i, 'quit' ); + } + for ( $i = 0; $i < $this->numProcs; $i++ ) { + $status = proc_close( $this->slaveProcs[$i] ); + if ( $status ) { + $this->critical( "Warning: child #$i exited with status $status" ); + } + } + $this->info( "Done." ); + } + + /** + * Dispatch a command to the next available slave. + * This may block until a slave finishes its work and becomes available. + */ + function dispatch( /*...*/ ) { + $args = func_get_args(); + $pipes = $this->slavePipes; + $numPipes = stream_select( $x=array(), $pipes, $y=array(), 3600 ); + if ( !$numPipes ) { + $this->critical( "Error waiting to write to slaves. Aborting" ); + exit( 1 ); + } + for ( $i = 0; $i < $this->numProcs; $i++ ) { + $slaveId = ( $i + $this->prevSlaveId + 1 ) % $this->numProcs; + if ( isset( $pipes[$slaveId] ) ) { + $this->prevSlaveId = $slaveId; + $this->dispatchToSlave( $slaveId, $args ); + return; + } + } + $this->critical( "Unreachable" ); + exit( 1 ); + } + + /** + * Dispatch a command to a specified slave + */ + function dispatchToSlave( $slaveId, $args ) { + $args = (array)$args; + $cmd = implode( ' ', $args ); + fwrite( $this->slavePipes[$slaveId], "$cmd\n" ); + } + + /** + * Move all tracked pages to the new clusters + */ + function doAllPages() { + $dbr = wfGetDB( DB_SLAVE ); + $i = 0; + $startId = 0; + $numPages = $dbr->selectField( 'blob_tracking', + 'COUNT(DISTINCT bt_page)', + # A condition is required so that this query uses the index + array( 'bt_moved' => 0 ), + __METHOD__ + ); + if ( $this->copyOnly ) { + $this->info( "Copying pages..." ); + } else { + $this->info( "Moving pages..." ); + } + while ( true ) { + $res = $dbr->select( 'blob_tracking', + array( 'bt_page' ), + array( + 'bt_moved' => 0, + 'bt_page > ' . $dbr->addQuotes( $startId ) + ), + __METHOD__, + array( + 'DISTINCT', + 'ORDER BY' => 'bt_page', + 'LIMIT' => $this->batchSize, + ) + ); + if ( !$res->numRows() ) { + break; + } + foreach ( $res as $row ) { + $this->dispatch( 'doPage', $row->bt_page ); + $i++; + } + $startId = $row->bt_page; + $this->report( 'pages', $i, $numPages ); + } + $this->report( 'pages', $i, $numPages ); + if ( $this->copyOnly ) { + $this->info( "All page copies queued." ); + } else { + $this->info( "All page moves queued." ); + } + } + + /** + * Display a progress report + */ + function report( $label, $current, $end ) { + $this->numBatches++; + if ( $current == $end || $this->numBatches >= $this->reportingInterval ) { + $this->numBatches = 0; + $this->info( "$label: $current / $end" ); + wfWaitForSlaves( 5 ); + } + } + + /** + * Move all orphan text to the new clusters + */ + function doAllOrphans() { + $dbr = wfGetDB( DB_SLAVE ); + $startId = 0; + $i = 0; + $numOrphans = $dbr->selectField( 'blob_tracking', + 'COUNT(DISTINCT bt_text_id)', + array( 'bt_moved' => 0, 'bt_page' => 0 ), + __METHOD__ ); + if ( !$numOrphans ) { + return; + } + if ( $this->copyOnly ) { + $this->info( "Copying orphans..." ); + } else { + $this->info( "Moving orphans..." ); + } + $ids = array(); + + while ( true ) { + $res = $dbr->select( 'blob_tracking', + array( 'bt_text_id' ), + array( + 'bt_moved' => 0, + 'bt_page' => 0, + 'bt_text_id > ' . $dbr->addQuotes( $startId ) + ), + __METHOD__, + array( + 'DISTINCT', + 'ORDER BY' => 'bt_text_id', + 'LIMIT' => $this->batchSize + ) + ); + if ( !$res->numRows() ) { + break; + } + foreach ( $res as $row ) { + $ids[] = $row->bt_text_id; + $i++; + } + // Need to send enough orphan IDs to the child at a time to fill a blob, + // so orphanBatchSize needs to be at least ~100. + // batchSize can be smaller or larger. + while ( count( $ids ) > $this->orphanBatchSize ) { + $args = array_slice( $ids, 0, $this->orphanBatchSize ); + $ids = array_slice( $ids, $this->orphanBatchSize ); + array_unshift( $args, 'doOrphanList' ); + call_user_func_array( array( $this, 'dispatch' ), $args ); + } + $startId = $row->bt_text_id; + $this->report( 'orphans', $i, $numOrphans ); + } + $this->report( 'orphans', $i, $numOrphans ); + $this->info( "All orphans queued." ); + } + + /** + * Main entry point for worker processes + */ + function executeChild() { + $this->debug( 'starting' ); + $this->syncDBs(); + + while ( !feof( STDIN ) ) { + $line = rtrim( fgets( STDIN ) ); + if ( $line == '' ) { + continue; + } + $this->debug( $line ); + $args = explode( ' ', $line ); + $cmd = array_shift( $args ); + switch ( $cmd ) { + case 'doPage': + $this->doPage( intval( $args[0] ) ); + break; + case 'doOrphanList': + $this->doOrphanList( array_map( 'intval', $args ) ); + break; + case 'quit': + return; + } + wfWaitForSlaves( 5 ); + } + } + + /** + * Move tracked text in a given page + */ + function doPage( $pageId ) { + $title = Title::newFromId( $pageId ); + if ( $title ) { + $titleText = $title->getPrefixedText(); + } else { + $titleText = '[deleted]'; + } + $dbr = wfGetDB( DB_SLAVE ); + + // Finish any incomplete transactions + if ( !$this->copyOnly ) { + $this->finishIncompleteMoves( array( 'bt_page' => $pageId ) ); + $this->syncDBs(); + } + + $startId = 0; + $trx = new CgzCopyTransaction( $this, $this->pageBlobClass ); + + while ( true ) { + $res = $dbr->select( + array( 'blob_tracking', 'text' ), + '*', + array( + 'bt_page' => $pageId, + 'bt_text_id > ' . $dbr->addQuotes( $startId ), + 'bt_moved' => 0, + 'bt_new_url IS NULL', + 'bt_text_id=old_id', + ), + __METHOD__, + array( + 'ORDER BY' => 'bt_text_id', + 'LIMIT' => $this->batchSize + ) + ); + if ( !$res->numRows() ) { + break; + } + + $lastTextId = 0; + foreach ( $res as $row ) { + if ( $lastTextId == $row->bt_text_id ) { + // Duplicate (null edit) + continue; + } + $lastTextId = $row->bt_text_id; + // Load the text + $text = Revision::getRevisionText( $row ); + if ( $text === false ) { + $this->critical( "Error loading {$row->bt_rev_id}/{$row->bt_text_id}" ); + continue; + } + + // Queue it + if ( !$trx->addItem( $text, $row->bt_text_id ) ) { + $this->debug( "$titleText: committing blob with " . $trx->getSize() . " items" ); + $trx->commit(); + $trx = new CgzCopyTransaction( $this, $this->pageBlobClass ); + } + } + $startId = $row->bt_text_id; + } + + $this->debug( "$titleText: committing blob with " . $trx->getSize() . " items" ); + $trx->commit(); + } + + /** + * Atomic move operation. + * + * Write the new URL to the text table and set the bt_moved flag. + * + * This is done in a single transaction to provide restartable behaviour + * without data loss. + * + * The transaction is kept short to reduce locking. + */ + function moveTextRow( $textId, $url ) { + if ( $this->copyOnly ) { + $this->critical( "Internal error: can't call moveTextRow() in --copy-only mode" ); + exit( 1 ); + } + $dbw = wfGetDB( DB_MASTER ); + $dbw->begin(); + $dbw->update( 'text', + array( // set + 'old_text' => $url, + 'old_flags' => 'external,utf-8', + ), + array( // where + 'old_id' => $textId + ), + __METHOD__ + ); + $dbw->update( 'blob_tracking', + array( 'bt_moved' => 1 ), + array( 'bt_text_id' => $textId ), + __METHOD__ + ); + $dbw->commit(); + } + + /** + * Moves are done in two phases: bt_new_url and then bt_moved. + * - bt_new_url indicates that the text has been copied to the new cluster. + * - bt_moved indicates that the text table has been updated. + * + * This function completes any moves that only have done bt_new_url. This + * can happen when the script is interrupted, or when --copy-only is used. + */ + function finishIncompleteMoves( $conds ) { + $dbr = wfGetDB( DB_SLAVE ); + + $startId = 0; + $conds = array_merge( $conds, array( + 'bt_moved' => 0, + 'bt_new_url IS NOT NULL' + )); + while ( true ) { + $res = $dbr->select( 'blob_tracking', + '*', + array_merge( $conds, array( 'bt_text_id > ' . $dbr->addQuotes( $startId ) ) ), + __METHOD__, + array( + 'ORDER BY' => 'bt_text_id', + 'LIMIT' => $this->batchSize, + ) + ); + if ( !$res->numRows() ) { + break; + } + $this->debug( 'Incomplete: ' . $res->numRows() . ' rows' ); + foreach ( $res as $row ) { + $this->moveTextRow( $row->bt_text_id, $row->bt_new_url ); + } + $startId = $row->bt_text_id; + } + } + + /** + * Returns the name of the next target cluster + */ + function getTargetCluster() { + $cluster = next( $this->destClusters ); + if ( $cluster === false ) { + $cluster = reset( $this->destClusters ); + } + return $cluster; + } + + /** + * Gets a DB master connection for the given external cluster name + */ + function getExtDB( $cluster ) { + $lb = wfGetLBFactory()->getExternalLB( $cluster ); + return $lb->getConnection( DB_MASTER ); + } + + /** + * Move an orphan text_id to the new cluster + */ + function doOrphanList( $textIds ) { + // Finish incomplete moves + if ( !$this->copyOnly ) { + $this->finishIncompleteMoves( array( 'bt_text_id' => $textIds ) ); + $this->syncDBs(); + } + + $trx = new CgzCopyTransaction( $this, $this->orphanBlobClass ); + + $res = wfGetDB( DB_SLAVE )->select( + array( 'text', 'blob_tracking' ), + array( 'old_id', 'old_text', 'old_flags' ), + array( + 'old_id' => $textIds, + 'bt_text_id=old_id', + 'bt_moved' => 0, + ), + __METHOD__, + array( 'DISTINCT' ) + ); + + foreach ( $res as $row ) { + $text = Revision::getRevisionText( $row ); + if ( $text === false ) { + $this->critical( "Error: cannot load revision text for old_id=$textId" ); + continue; + } + + if ( !$trx->addItem( $text, $row->old_id ) ) { + $this->debug( "[orphan]: committing blob with " . $trx->getSize() . " rows" ); + $trx->commit(); + $trx = new CgzCopyTransaction( $this, $this->orphanBlobClass ); + } + } + $this->debug( "[orphan]: committing blob with " . $trx->getSize() . " rows" ); + $trx->commit(); + } +} + +/** + * Class to represent a recompression operation for a single CGZ blob + */ +class CgzCopyTransaction { + var $parent; + var $blobClass; + var $cgz; + var $referrers; + + /** + * Create a transaction from a RecompressTracked object + */ + function __construct( $parent, $blobClass ) { + $this->blobClass = $blobClass; + $this->cgz = false; + $this->texts = array(); + $this->parent = $parent; + } + + /** + * Add text. + * Returns false if it's ready to commit. + */ + function addItem( $text, $textId ) { + if ( !$this->cgz ) { + $class = $this->blobClass; + $this->cgz = new $class; + } + $hash = $this->cgz->addItem( $text ); + $this->referrers[$textId] = $hash; + $this->texts[$textId] = $text; + return $this->cgz->isHappy(); + } + + function getSize() { + return count( $this->texts ); + } + + /** + * Recompress text after some aberrant modification + */ + function recompress() { + $class = $this->blobClass; + $this->cgz = new $class; + $this->referrers = array(); + foreach ( $this->texts as $textId => $text ) { + $hash = $this->cgz->addItem( $text ); + $this->referrers[$textId] = $hash; + } + } + + /** + * Commit the blob. + * Does nothing if no text items have been added. + * May skip the move if --copy-only is set. + */ + function commit() { + $originalCount = count( $this->texts ); + if ( !$originalCount ) { + return; + } + + // Check to see if the target text_ids have been moved already. + // + // We originally read from the slave, so this can happen when a single + // text_id is shared between multiple pages. It's rare, but possible + // if a delete/move/undelete cycle splits up a null edit. + // + // We do a locking read to prevent closer-run race conditions. + $dbw = wfGetDB( DB_MASTER ); + $dbw->begin(); + $res = $dbw->select( 'blob_tracking', + array( 'bt_text_id', 'bt_moved' ), + array( 'bt_text_id' => array_keys( $this->referrers ) ), + __METHOD__, array( 'FOR UPDATE' ) ); + $dirty = false; + foreach ( $res as $row ) { + if ( $row->bt_moved ) { + # This row has already been moved, remove it + $this->parent->debug( "TRX: conflict detected in old_id={$row->bt_text_id}" ); + unset( $this->texts[$row->bt_text_id] ); + $dirty = true; + } + } + + // Recompress the blob if necessary + if ( $dirty ) { + if ( !count( $this->texts ) ) { + // All have been moved already + if ( $originalCount > 1 ) { + // This is suspcious, make noise + $this->critical( "Warning: concurrent operation detected, are there two conflicting " . + "processes running, doing the same job?" ); + } + return; + } + $this->recompress(); + } + + // Insert the data into the destination cluster + $targetCluster = $this->parent->getTargetCluster(); + $store = $this->parent->store; + $targetDB = $store->getMaster( $targetCluster ); + $targetDB->clearFlag( DBO_TRX ); // we manage the transactions + $targetDB->begin(); + $baseUrl = $this->parent->store->store( $targetCluster, serialize( $this->cgz ) ); + + // Write the new URLs to the blob_tracking table + foreach ( $this->referrers as $textId => $hash ) { + $url = $baseUrl . '/' . $hash; + $dbw->update( 'blob_tracking', + array( 'bt_new_url' => $url ), + array( + 'bt_text_id' => $textId, + 'bt_moved' => 0, # Check for concurrent conflicting update + ), + __METHOD__ + ); + } + + $targetDB->commit(); + // Critical section here: interruption at this point causes blob duplication + // Reversing the order of the commits would cause data loss instead + $dbw->commit(); + + // Write the new URLs to the text table and set the moved flag + if ( !$this->parent->copyOnly ) { + foreach ( $this->referrers as $textId => $hash ) { + $url = $baseUrl . '/' . $hash; + $this->parent->moveTextRow( $textId, $url ); + } + } + } +} + diff --git a/maintenance/storage/testCompression.php b/maintenance/storage/testCompression.php new file mode 100644 index 00000000..9c96c9f8 --- /dev/null +++ b/maintenance/storage/testCompression.php @@ -0,0 +1,81 @@ +<?php + +$optionsWithArgs = array( 'start', 'limit', 'type' ); +require( dirname(__FILE__).'/../commandLine.inc' ); + +if ( !isset( $args[0] ) ) { + echo "Usage: php testCompression.php [--type=<type>] [--start=<start-date>] [--limit=<num-revs>] <page-title>\n"; + exit( 1 ); +} + +$title = Title::newFromText( $args[0] ); +if ( isset( $options['start'] ) ) { + $start = wfTimestamp( TS_MW, strtotime( $options['start'] ) ); + echo "Starting from " . $wgLang->timeanddate( $start ) . "\n"; +} else { + $start = '19700101000000'; +} +if ( isset( $options['limit'] ) ) { + $limit = $options['limit']; + $untilHappy = false; +} else { + $limit = 1000; + $untilHappy = true; +} +$type = isset( $options['type'] ) ? $options['type'] : 'ConcatenatedGzipHistoryBlob'; + + +$dbr = wfGetDB( DB_SLAVE ); +$res = $dbr->select( + array( 'page', 'revision', 'text' ), + '*', + array( + 'page_namespace' => $title->getNamespace(), + 'page_title' => $title->getDBkey(), + 'page_id=rev_page', + 'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $start ) ), + 'rev_text_id=old_id' + ), __FILE__, array( 'LIMIT' => $limit ) +); + +$blob = new $type; +$hashes = array(); +$keys = array(); +$uncompressedSize = 0; +$t = -microtime( true ); +foreach ( $res as $row ) { + $revision = new Revision( $row ); + $text = $revision->getText(); + $uncompressedSize += strlen( $text ); + $hashes[$row->rev_id] = md5( $text ); + $keys[$row->rev_id] = $blob->addItem( $text ); + if ( $untilHappy && !$blob->isHappy() ) { + break; + } +} + +$serialized = serialize( $blob ); +$t += microtime( true ); +#print_r( $blob->mDiffMap ); + +printf( "%s\nCompression ratio for %d revisions: %5.2f, %s -> %d\n", + $type, + count( $hashes ), + $uncompressedSize / strlen( $serialized ), + $wgLang->formatSize( $uncompressedSize ), + strlen( $serialized ) +); +printf( "Compression time: %5.2f ms\n", $t * 1000 ); + +$t = -microtime( true ); +$blob = unserialize( $serialized ); +foreach ( $keys as $id => $key ) { + $text = $blob->getItem( $key ); + if ( md5( $text ) != $hashes[$id] ) { + echo "Content hash mismatch for rev_id $id\n"; + #var_dump( $text ); + } +} +$t += microtime( true ); +printf( "Decompression time: %5.2f ms\n", $t * 1000 ); + diff --git a/maintenance/storage/trackBlobs.php b/maintenance/storage/trackBlobs.php new file mode 100644 index 00000000..b13faa00 --- /dev/null +++ b/maintenance/storage/trackBlobs.php @@ -0,0 +1,316 @@ +<?php + +require( dirname( __FILE__ ) .'/../commandLine.inc' ); + + +if ( count( $args ) < 1 ) { + echo "Usage: php trackBlobs.php <cluster> [... <cluster>]\n"; + echo "Adds blobs from a given ES cluster to the blob_tracking table\n"; + echo "Automatically deletes the tracking table and starts from the start again when restarted.\n"; + + exit( 1 ); +} +$tracker = new TrackBlobs( $args ); +$tracker->trackBlobs(); + +class TrackBlobs { + var $clusters, $textClause; + var $doBlobOrphans; + var $trackedBlobs = array(); + + var $batchSize = 1000; + var $reportingInterval = 10; + + function __construct( $clusters ) { + $this->clusters = $clusters; + if ( extension_loaded( 'gmp' ) ) { + $this->doBlobOrphans = true; + foreach ( $clusters as $cluster ) { + $this->trackedBlobs[$cluster] = gmp_init( 0 ); + } + } else { + echo "Warning: the gmp extension is needed to find orphan blobs\n"; + } + } + + function trackBlobs() { + $this->initTrackingTable(); + $this->trackRevisions(); + $this->trackOrphanText(); + if ( $this->doBlobOrphans ) { + $this->findOrphanBlobs(); + } + } + + function initTrackingTable() { + $dbw = wfGetDB( DB_MASTER ); + if ( $dbw->tableExists( 'blob_tracking' ) ) { + $dbw->query( 'DROP TABLE ' . $dbw->tableName( 'blob_tracking' ) ); + $dbw->query( 'DROP TABLE ' . $dbw->tableName( 'blob_orphans' ) ); + } + $dbw->sourceFile( dirname( __FILE__ ) . '/blob_tracking.sql' ); + } + + function getTextClause() { + if ( !$this->textClause ) { + $dbr = wfGetDB( DB_SLAVE ); + $this->textClause = ''; + foreach ( $this->clusters as $cluster ) { + if ( $this->textClause != '' ) { + $this->textClause .= ' OR '; + } + $this->textClause .= 'old_text LIKE ' . $dbr->addQuotes( $dbr->escapeLike( "DB://$cluster/" ) . '%' ); + } + } + return $this->textClause; + } + + function interpretPointer( $text ) { + if ( !preg_match( '!^DB://(\w+)/(\d+)(?:/([0-9a-fA-F]+)|)$!', $text, $m ) ) { + return false; + } + return array( + 'cluster' => $m[1], + 'id' => intval( $m[2] ), + 'hash' => isset( $m[3] ) ? $m[2] : null + ); + } + + /** + * Scan the revision table for rows stored in the specified clusters + */ + function trackRevisions() { + $dbw = wfGetDB( DB_MASTER ); + $dbr = wfGetDB( DB_SLAVE ); + + $textClause = $this->getTextClause(); + $startId = 0; + $endId = $dbr->selectField( 'revision', 'MAX(rev_id)', false, __METHOD__ ); + $batchesDone = 0; + $rowsInserted = 0; + + echo "Finding revisions...\n"; + + while ( true ) { + $res = $dbr->select( array( 'revision', 'text' ), + array( 'rev_id', 'rev_page', 'old_id', 'old_flags', 'old_text' ), + array( + 'rev_id > ' . $dbr->addQuotes( $startId ), + 'rev_text_id=old_id', + $textClause, + "old_flags LIKE '%external%'", + ), + __METHOD__, + array( + 'ORDER BY' => 'rev_id', + 'LIMIT' => $this->batchSize + ) + ); + if ( !$res->numRows() ) { + break; + } + + $insertBatch = array(); + foreach ( $res as $row ) { + $startId = $row->rev_id; + $info = $this->interpretPointer( $row->old_text ); + if ( !$info ) { + echo "Invalid DB:// URL in rev_id {$row->rev_id}\n"; + continue; + } + if ( !in_array( $info['cluster'], $this->clusters ) ) { + echo "Invalid cluster returned in SQL query: {$info['cluster']}\n"; + continue; + } + $insertBatch[] = array( + 'bt_page' => $row->rev_page, + 'bt_rev_id' => $row->rev_id, + 'bt_text_id' => $row->old_id, + 'bt_cluster' => $info['cluster'], + 'bt_blob_id' => $info['id'], + 'bt_cgz_hash' => $info['hash'] + ); + if ( $this->doBlobOrphans ) { + gmp_setbit( $this->trackedBlobs[$info['cluster']], $info['id'] ); + } + } + $dbw->insert( 'blob_tracking', $insertBatch, __METHOD__ ); + $rowsInserted += count( $insertBatch ); + + ++$batchesDone; + if ( $batchesDone >= $this->reportingInterval ) { + $batchesDone = 0; + echo "$startId / $endId\n"; + wfWaitForSlaves( 5 ); + } + } + echo "Found $rowsInserted revisions\n"; + } + + /** + * Scan the text table for orphan text + * Orphan text here does not imply DB corruption -- deleted text tracked by the + * archive table counts as orphan for our purposes. + */ + function trackOrphanText() { + # Wait until the blob_tracking table is available in the slave + $dbw = wfGetDB( DB_MASTER ); + $dbr = wfGetDB( DB_SLAVE ); + $pos = $dbw->getMasterPos(); + $dbr->masterPosWait( $pos, 100000 ); + + $textClause = $this->getTextClause( $this->clusters ); + $startId = 0; + $endId = $dbr->selectField( 'text', 'MAX(old_id)', false, __METHOD__ ); + $rowsInserted = 0; + $batchesDone = 0; + + echo "Finding orphan text...\n"; + + # Scan the text table for orphan text + while ( true ) { + $res = $dbr->select( array( 'text', 'blob_tracking' ), + array( 'old_id', 'old_flags', 'old_text' ), + array( + 'old_id>' . $dbr->addQuotes( $startId ), + $textClause, + "old_flags LIKE '%external%'", + 'bt_text_id IS NULL' + ), + __METHOD__, + array( + 'ORDER BY' => 'old_id', + 'LIMIT' => $this->batchSize + ), + array( 'blob_tracking' => array( 'LEFT JOIN', 'bt_text_id=old_id' ) ) + ); + $ids = array(); + foreach ( $res as $row ) { + $ids[] = $row->old_id; + } + + if ( !$res->numRows() ) { + break; + } + + $insertBatch = array(); + foreach ( $res as $row ) { + $startId = $row->old_id; + $info = $this->interpretPointer( $row->old_text ); + if ( !$info ) { + echo "Invalid DB:// URL in old_id {$row->old_id}\n"; + continue; + } + if ( !in_array( $info['cluster'], $this->clusters ) ) { + echo "Invalid cluster returned in SQL query\n"; + continue; + } + + $insertBatch[] = array( + 'bt_page' => 0, + 'bt_rev_id' => 0, + 'bt_text_id' => $row->old_id, + 'bt_cluster' => $info['cluster'], + 'bt_blob_id' => $info['id'], + 'bt_cgz_hash' => $info['hash'] + ); + if ( $this->doBlobOrphans ) { + gmp_setbit( $this->trackedBlobs[$info['cluster']], $info['id'] ); + } + } + $dbw->insert( 'blob_tracking', $insertBatch, __METHOD__ ); + + $rowsInserted += count( $insertBatch ); + ++$batchesDone; + if ( $batchesDone >= $this->reportingInterval ) { + $batchesDone = 0; + echo "$startId / $endId\n"; + wfWaitForSlaves( 5 ); + } + } + echo "Found $rowsInserted orphan text rows\n"; + } + + /** + * Scan the blobs table for rows not registered in blob_tracking (and thus not + * registered in the text table). + * + * Orphan blobs are indicative of DB corruption. They are inaccessible and + * should probably be deleted. + */ + function findOrphanBlobs() { + if ( !extension_loaded( 'gmp' ) ) { + echo "Can't find orphan blobs, need bitfield support provided by GMP.\n"; + return; + } + + $dbw = wfGetDB( DB_MASTER ); + + foreach ( $this->clusters as $cluster ) { + echo "Searching for orphan blobs in $cluster...\n"; + $lb = wfGetLBFactory()->getExternalLB( $cluster ); + try { + $extDB = $lb->getConnection( DB_SLAVE ); + } catch ( DBConnectionError $e ) { + if ( strpos( $e->error, 'Unknown database' ) !== false ) { + echo "No database on $cluster\n"; + } else { + echo "Error on $cluster: " . $e->getMessage() . "\n"; + } + continue; + } + $startId = 0; + $batchesDone = 0; + $actualBlobs = gmp_init( 0 ); + $endId = $extDB->selectField( 'blobs', 'MAX(blob_id)', false, __METHOD__ ); + + // Build a bitmap of actual blob rows + while ( true ) { + $res = $extDB->select( 'blobs', + array( 'blob_id' ), + array( 'blob_id > ' . $extDB->addQuotes( $startId ) ), + __METHOD__, + array( 'LIMIT' => $this->batchSize, 'ORDER BY' => 'blob_id' ) + ); + + if ( !$res->numRows() ) { + break; + } + + foreach ( $res as $row ) { + gmp_setbit( $actualBlobs, $row->blob_id ); + } + $startId = $row->blob_id; + + ++$batchesDone; + if ( $batchesDone >= $this->reportingInterval ) { + $batchesDone = 0; + echo "$startId / $endId\n"; + } + } + + // Find actual blobs that weren't tracked by the previous passes + // This is a set-theoretic difference A \ B, or in bitwise terms, A & ~B + $orphans = gmp_and( $actualBlobs, gmp_com( $this->trackedBlobs[$cluster] ) ); + + // Traverse the orphan list + $insertBatch = array(); + $id = 0; + while ( true ) { + $id = gmp_scan1( $orphans, $id ); + if ( $id == -1 ) { + break; + } + $insertBatch[] = array( + 'bo_cluster' => $cluster, + 'bo_blob_id' => $id + ); + ++$id; + } + + // Insert the batch + echo "Found " . count( $insertBatch ) . " orphan(s) in $cluster\n"; + $dbw->insert( 'blob_orphans', $insertBatch, __METHOD__ ); + } + } +} diff --git a/maintenance/tables.sql b/maintenance/tables.sql index 11ed7cb2..28f496eb 100644 --- a/maintenance/tables.sql +++ b/maintenance/tables.sql @@ -446,7 +446,7 @@ CREATE TABLE /*$wgDBprefix*/imagelinks ( -- Filename of target image. -- This is also the page_title of the file's description page; - -- all such pages are in namespace 6 (NS_IMAGE). + -- all such pages are in namespace 6 (NS_FILE). il_to varchar(255) binary NOT NULL default '', UNIQUE KEY il_from (il_from,il_to), @@ -596,6 +596,9 @@ CREATE TABLE /*$wgDBprefix*/site_stats ( -- Number of users, theoretically equal to SELECT COUNT(*) FROM user; ss_users bigint default '-1', + + -- Number of users that still edit + ss_active_users bigint default '-1', -- Deprecated, no longer updated as of 1.5 ss_admins int default '-1', @@ -675,6 +678,9 @@ CREATE TABLE /*$wgDBprefix*/ipblocks ( -- Block prevents user from accessing Special:Emailuser ipb_block_email bool NOT NULL default 0, + -- Block allows user to edit their own talk page + ipb_allow_usertalk bool NOT NULL default 0, + PRIMARY KEY ipb_id (ipb_id), -- Unique index to support "user already blocked" messages @@ -695,7 +701,7 @@ CREATE TABLE /*$wgDBprefix*/ipblocks ( CREATE TABLE /*$wgDBprefix*/image ( -- Filename. -- This is also the title of the associated description page, - -- which will be in namespace 6 (NS_IMAGE). + -- which will be in namespace 6 (NS_FILE). img_name varchar(255) binary NOT NULL default '', -- File size in bytes. @@ -904,7 +910,7 @@ CREATE TABLE /*$wgDBprefix*/recentchanges ( rc_old_len int, rc_new_len int, - -- Visibility of deleted revisions, bitfield + -- Visibility of recent changes items, bitfield rc_deleted tinyint unsigned NOT NULL default '0', -- Value corresonding to log_id, specific log entries @@ -1044,7 +1050,7 @@ CREATE TABLE /*$wgDBprefix*/objectcache ( keyname varbinary(255) NOT NULL default '', value mediumblob, exptime datetime, - UNIQUE KEY (keyname), + PRIMARY KEY (keyname), KEY (exptime) ) /*$wgDBTableOptions*/; diff --git a/maintenance/updateArticleCount.inc.php b/maintenance/updateArticleCount.inc.php index de19191e..a847a2ed 100644 --- a/maintenance/updateArticleCount.inc.php +++ b/maintenance/updateArticleCount.inc.php @@ -38,7 +38,8 @@ class ArticleCounter { function makeSql() { list( $page, $pagelinks ) = $this->dbr->tableNamesN( 'page', 'pagelinks' ); $nsset = $this->makeNsSet(); - return "SELECT DISTINCT page_namespace,page_title FROM $page,$pagelinks " . + return "SELECT COUNT(DISTINCT page_namespace, page_title) AS pagecount " . + "FROM $page, $pagelinks " . "WHERE pl_from=page_id and page_namespace IN ( $nsset ) " . "AND page_is_redirect = 0 AND page_len > 0"; } @@ -50,15 +51,9 @@ class ArticleCounter { */ function count() { $res = $this->dbr->query( $this->makeSql(), __METHOD__ ); - if( $res ) { - $count = $this->dbr->numRows( $res ); - $this->dbr->freeResult( $res ); - return $count; - } else { - # Look out for this when handling the result - # - Actually it's unreachable, !$res throws an exception -- TS - return false; - } + $row = $this->dbr->fetchObject( $res ); + $this->dbr->freeResult( $res ); + return $row->pagecount; } } diff --git a/maintenance/updateRestrictions.php b/maintenance/updateRestrictions.php index c2d256e3..f0567b5b 100644 --- a/maintenance/updateRestrictions.php +++ b/maintenance/updateRestrictions.php @@ -25,6 +25,11 @@ function migrate_page_restrictions( $db ) { $start = $db->selectField( 'page', 'MIN(page_id)', false, __FUNCTION__ ); $end = $db->selectField( 'page', 'MAX(page_id)', false, __FUNCTION__ ); + + if( !$start ) { + die("Nothing to do.\n"); + } + # Do remaining chunk $end += BATCH_SIZE - 1; $blockStart = $start; @@ -32,18 +37,19 @@ function migrate_page_restrictions( $db ) { $encodedExpiry = 'infinity'; while ( $blockEnd <= $end ) { echo "...doing page_id from $blockStart to $blockEnd\n"; - $cond = "page_id BETWEEN $blockStart AND $blockEnd AND page_restrictions !='' AND page_restrictions !='edit=:move='"; + $cond = "page_id BETWEEN $blockStart AND $blockEnd AND page_restrictions !=''"; $res = $db->select( 'page', array('page_id', 'page_restrictions'), $cond, __FUNCTION__ ); $batch = array(); while ( $row = $db->fetchObject( $res ) ) { $oldRestrictions = array(); foreach( explode( ':', trim( $row->page_restrictions ) ) as $restrict ) { $temp = explode( '=', trim( $restrict ) ); - if(count($temp) == 1) { + // Make sure we are not settings restrictions to "" + if( count($temp) == 1 && $temp[0] ) { // old old format should be treated as edit/move restriction $oldRestrictions["edit"] = trim( $temp[0] ); $oldRestrictions["move"] = trim( $temp[0] ); - } else { + } else if( $temp[1] ) { $oldRestrictions[$temp[0]] = trim( $temp[1] ); } } @@ -61,12 +67,21 @@ function migrate_page_restrictions( $db ) { # We use insert() and not replace() as Article.php replaces # page_restrictions with '' when protected in the restrictions table if ( count( $batch ) ) { - $db->insert( 'page_restrictions', $batch, __FUNCTION__, array( 'IGNORE' ) ); + $ok = $db->deadlockLoop( + array( $db, 'insert' ), + 'page_restrictions', $batch, __FUNCTION__, array( 'IGNORE' ) ); + if( !$ok ) { + throw new MWException( "Deadlock loop failed wtf :(" ); + } } $blockStart += BATCH_SIZE - 1; $blockEnd += BATCH_SIZE - 1; wfWaitForSlaves( 5 ); } + echo "...removing dead rows from page_restrictions\n"; + // Kill any broken rows from previous imports + $db->delete( 'page_restrictions', array( 'pr_level' => '' ) ); + echo "...Done!\n"; } diff --git a/maintenance/updateSpecialPages.php b/maintenance/updateSpecialPages.php index ac7ee11f..3eaa6205 100644 --- a/maintenance/updateSpecialPages.php +++ b/maintenance/updateSpecialPages.php @@ -25,7 +25,31 @@ if(@$options['help']) { $wgOut->disable(); $dbw = wfGetDB( DB_MASTER ); -foreach ( $wgQueryPages as $page ) { +foreach( $wgSpecialPageCacheUpdates as $special => $call ) { + if( !is_callable($call) ) { + print "Uncallable function $call!\n"; + continue; + } + $t1 = explode( ' ', microtime() ); + call_user_func( $call, $dbw ); + $t2 = explode( ' ', microtime() ); + printf( '%-30s ', $special ); + $elapsed = ($t2[0] - $t1[0]) + ($t2[1] - $t1[1]); + $hours = intval( $elapsed / 3600 ); + $minutes = intval( $elapsed % 3600 / 60 ); + $seconds = $elapsed - $hours * 3600 - $minutes * 60; + if ( $hours ) { + print $hours . 'h '; + } + if ( $minutes ) { + print $minutes . 'm '; + } + printf( "completed in %.2fs\n", $seconds ); + # Wait for the slave to catch up + wfWaitForSlaves( 5 ); +} + +foreach( $wgQueryPages as $page ) { @list( $class, $special, $limit ) = $page; # --list : just show the name of pages @@ -50,33 +74,30 @@ foreach ( $wgQueryPages as $page ) { } $queryPage = new $class; - if( !(isset($options['only'])) or ($options['only'] == $queryPage->getName()) ) { - printf( '%-30s ', $special ); - - if ( $queryPage->isExpensive() ) { - $t1 = explode( ' ', microtime() ); - # Do the query - $num = $queryPage->recache( $limit === null ? $wgQueryCacheLimit : $limit ); - $t2 = explode( ' ', microtime() ); - - if ( $num === false ) { - print "FAILED: database error\n"; - } else { - print "got $num rows in "; - - $elapsed = ($t2[0] - $t1[0]) + ($t2[1] - $t1[1]); - $hours = intval( $elapsed / 3600 ); - $minutes = intval( $elapsed % 3600 / 60 ); - $seconds = $elapsed - $hours * 3600 - $minutes * 60; - if ( $hours ) { - print $hours . 'h '; - } - if ( $minutes ) { - print $minutes . 'm '; - } - printf( "%.2fs\n", $seconds ); + if( !isset($options['only']) or $options['only'] == $queryPage->getName() ) { + printf( '%-30s ', $special ); + if ( $queryPage->isExpensive() ) { + $t1 = explode( ' ', microtime() ); + # Do the query + $num = $queryPage->recache( $limit === null ? $wgQueryCacheLimit : $limit ); + $t2 = explode( ' ', microtime() ); + if ( $num === false ) { + print "FAILED: database error\n"; + } else { + print "got $num rows in "; + + $elapsed = ($t2[0] - $t1[0]) + ($t2[1] - $t1[1]); + $hours = intval( $elapsed / 3600 ); + $minutes = intval( $elapsed % 3600 / 60 ); + $seconds = $elapsed - $hours * 3600 - $minutes * 60; + if ( $hours ) { + print $hours . 'h '; + } + if ( $minutes ) { + print $minutes . 'm '; + } + printf( "%.2fs\n", $seconds ); } - # Reopen any connections that have closed if ( !wfGetLB()->pingAll()) { print "\n"; @@ -89,22 +110,10 @@ foreach ( $wgQueryPages as $page ) { # Commit the results $dbw->immediateCommit(); } - # Wait for the slave to catch up - /* - $slaveDB = wfGetDB( DB_SLAVE, array('QueryPage::recache', 'vslow' ) ); - while( $slaveDB->getLag() > 600 ) { - print "Slave lagged, waiting...\n"; - sleep(30); - - } - */ wfWaitForSlaves( 5 ); - - } else { - print "cheap, skipped\n"; - } + } else { + print "cheap, skipped\n"; + } } } - - diff --git a/maintenance/updaters.inc b/maintenance/updaters.inc index 13cdc4c8..e671efe5 100644 --- a/maintenance/updaters.inc +++ b/maintenance/updaters.inc @@ -143,6 +143,11 @@ $wgMysqlUpdates = array( array( 'maybe_do_profiling_memory_update' ), array( 'do_filearchive_indices_update' ), array( 'update_password_format' ), + + // 1.14 + array( 'add_field', 'site_stats', 'ss_active_users', 'patch-ss_active_users.sql' ), + array( 'do_active_users_init' ), + array( 'add_field', 'ipblocks', 'ipb_allow_usertalk', 'patch-ipb_allow_usertalk.sql' ) ); @@ -1015,12 +1020,32 @@ function do_stats_init() { $row = $wgDatabase->selectRow( 'site_stats', '*', array( 'ss_row_id' => 1 ), __METHOD__ ); if( $row === false ) { wfOut( "data is missing! rebuilding...\n" ); - global $IP; - require_once "$IP/maintenance/initStats.inc"; - wfInitStats(); + } elseif ( isset( $row->site_stats ) && $row->ss_total_pages == -1 ) { + wfOut( "missing ss_total_pages, rebuilding...\n" ); } else { wfOut( "ok.\n" ); + return; + } + + global $IP; + require_once "$IP/maintenance/initStats.inc"; + wfInitStats(); +} + +function do_active_users_init() { + global $wgDatabase; + $activeUsers = $wgDatabase->selectField( 'site_stats', 'ss_active_users', false, __METHOD__ ); + if( $activeUsers == -1 ) { + $activeUsers = $wgDatabase->selectField( 'recentchanges', + 'COUNT( DISTINCT rc_user_text )', + array( 'rc_user != 0', 'rc_bot' => 0, "rc_log_type != 'newusers'" ), __METHOD__ + ); + $wgDatabase->update( 'site_stats', + array( 'ss_active_users' => intval($activeUsers) ), + array( 'ss_row_id' => 1 ), __METHOD__, array( 'LIMIT' => 1 ) + ); } + wfOut( "...ss_active_users user count set...\n" ); } function purge_cache() { @@ -1431,12 +1456,13 @@ function do_postgres_updates() { array("archive", "ar_page_id", "INTEGER"), array("archive", "ar_parent_id", "INTEGER"), array("image", "img_sha1", "TEXT NOT NULL DEFAULT ''"), - array("ipblocks", "ipb_anon_only", "CHAR NOT NULL DEFAULT '0'"), + array("ipblocks", "ipb_anon_only", "SMALLINT NOT NULL DEFAULT 0"), array("ipblocks", "ipb_by_text", "TEXT NOT NULL DEFAULT ''"), - array("ipblocks", "ipb_block_email", "CHAR NOT NULL DEFAULT '0'"), - array("ipblocks", "ipb_create_account", "CHAR NOT NULL DEFAULT '1'"), + array("ipblocks", "ipb_block_email", "SMALLINT NOT NULL DEFAULT 0"), + array("ipblocks", "ipb_create_account", "SMALLINT NOT NULL DEFAULT 1"), array("ipblocks", "ipb_deleted", "SMALLINT NOT NULL DEFAULT 0"), - array("ipblocks", "ipb_enable_autoblock", "CHAR NOT NULL DEFAULT '1'"), + array("ipblocks", "ipb_enable_autoblock", "SMALLINT NOT NULL DEFAULT 1"), + array("ipblocks", "ipb_allow_usertalk", "SMALLINT NOT NULL DEFAULT 0"), array("filearchive", "fa_deleted", "SMALLINT NOT NULL DEFAULT 0"), array("logging", "log_deleted", "SMALLINT NOT NULL DEFAULT 0"), array("logging", "log_id", "INTEGER NOT NULL PRIMARY KEY DEFAULT nextval('log_log_id_seq')"), @@ -1461,6 +1487,8 @@ function do_postgres_updates() { array("revision", "rev_len", "INTEGER"), array("revision", "rev_deleted", "SMALLINT NOT NULL DEFAULT 0"), array("user_newtalk", "user_last_timestamp", "TIMESTAMPTZ"), + array("site_stats", "ss_active_users", "INTEGER DEFAULT '-1'"), + array("revision", "rev_parent_id", "INTEGER DEFAULT NULL"), ); @@ -1494,6 +1522,7 @@ function do_postgres_updates() { array("mwuser", "user_email_token","text", ""), array("objectcache", "keyname", "text", ""), array("oldimage", "oi_height", "integer", ""), + array("oldimage", "oi_metadata", "bytea", "decode(img_metadata,'escape')"), array("oldimage", "oi_size", "integer", ""), array("oldimage", "oi_width", "integer", ""), array("page", "page_is_redirect","smallint", "page_is_redirect::smallint DEFAULT 0"), @@ -1513,10 +1542,13 @@ function do_postgres_updates() { ); $newindexes = array( - array("archive", "archive_user_text", "(ar_user_text)"), - array("image", "img_sha1", "(img_sha1)"), - array("oldimage", "oi_sha1", "(oi_sha1)"), - array("revision", "rev_text_id_idx", "(rev_text_id)"), + array("archive", "archive_user_text", "(ar_user_text)"), + array("image", "img_sha1", "(img_sha1)"), + array("oldimage", "oi_sha1", "(oi_sha1)"), + array("revision", "rev_text_id_idx", "(rev_text_id)"), + array("recentchanges", "rc_timestamp_nobot", "(rc_timestamp) WHERE rc_bot = 0"), + array("templatelinks", "templatelinks_from", "(tl_from)"), + array("watchlist", "wl_user", "(wl_user)"), ); $newrules = array( @@ -1672,9 +1704,14 @@ function do_postgres_updates() { } # Fix ipb_address index + if (pg_index_exists('ipblocks', 'ipb_address' )) { + wfOut( "Removing deprecated index 'ipb_address'...\n" ); + $wgDatabase->query('DROP INDEX ipb_address'); + } if (pg_index_exists('ipblocks', 'ipb_address_unique' )) { wfOut( "... have ipb_address_unique\n" ); - } else { + } + else { wfOut( "Adding ipb_address_unique index\n" ); dbsource(archive('patch-ipb_address_unique.sql')); } @@ -1713,11 +1750,20 @@ function do_postgres_updates() { # This is create or replace, so harmless to call if not needed dbsource(archive('patch-ts2pagetitle.sql')); - ## If the server is 8.3 or higher, rewrite teh tsearch2 triggers + ## If the server is 8.3 or higher, rewrite the tsearch2 triggers ## in case they have the old 'default' versions if ( $numver >= 8.3 ) dbsource(archive('patch-tsearch2funcs.sql')); + ## Put a new row in the mediawiki_version table + $wgDatabase->insert( 'mediawiki_version', + array( + 'type' => 'Update', + 'ctype' => 'U', + 'mw_version' => $wgVersion, + 'pg_version' => $version, + 'sql_version' => '$LastChangedRevision: 46891 $', + 'sql_date' => '$LastChangedDate: 2009-02-05 22:54:47 -0600 (Thu, 05 Feb 2009) $', + ) ); return; } - |