From 086ae52d12011746a75f5588e877347bc0457352 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Fri, 21 Mar 2008 11:49:34 +0100 Subject: Update auf MediaWiki 1.12.0 --- maintenance/addwiki.php | 31 +- maintenance/archives/patch-image_reditects.sql | 0 maintenance/archives/patch-protected_titles.sql | 12 + maintenance/archives/populateSha1.php | 22 +- maintenance/backup.inc | 3 +- maintenance/cleanupImages.php | 6 +- maintenance/cleanupSpam.php | 3 +- maintenance/cleanupTitles.php | 6 +- maintenance/commandLine.inc | 14 + maintenance/createAndPromote.php | 2 +- maintenance/deleteBatch.php | 6 +- maintenance/deleteDefaultMessages.php | 4 +- maintenance/deleteOldRevisions.inc | 17 +- maintenance/deleteOldRevisions.php | 4 +- maintenance/dumpHTML.php | 163 +------ maintenance/dumpTextPass.php | 150 ++++++- maintenance/dumpUploads.php | 95 +++-- maintenance/fetchText.php | 36 ++ maintenance/findhooks.php | 98 +++-- maintenance/interwiki.sql | 4 +- maintenance/language/StatOutputs.php | 103 +++++ maintenance/language/checkLanguage.inc | 128 +----- maintenance/language/checkLanguage.php | 323 +++++++++++--- maintenance/language/lang2po.php | 14 +- maintenance/language/languages.inc | 113 ++--- maintenance/language/messageTypes.inc | 70 ++++ maintenance/language/messages.inc | 297 ++++++++++--- maintenance/language/rebuildLanguage.php | 2 +- maintenance/language/splitLanguageFiles.inc | 1 - maintenance/language/transstat.php | 96 +---- maintenance/language/writeMessagesArray.inc | 360 +++++++++------- maintenance/namespaceDupes.php | 54 ++- maintenance/nextJobDB.php | 6 +- maintenance/parserTests.inc | 64 ++- maintenance/parserTests.php | 1 + maintenance/parserTests.txt | 442 +++++++++++-------- .../postgres/archives/patch-protected_titles.sql | 10 + .../postgres/archives/patch-ts2pagetitle.sql | 13 + maintenance/postgres/compare_schemas.pl | 221 +++++++++- maintenance/postgres/mediawiki_mysql2postgres.pl | 11 +- maintenance/postgres/tables.sql | 112 ++--- maintenance/preprocessorFuzzTest.php | 233 +++++++++++ maintenance/rebuildInterwiki.inc | 9 +- maintenance/rebuildInterwiki.php | 16 +- maintenance/rebuildall.php | 3 +- maintenance/rebuildmessages.php | 17 + maintenance/rebuildrecentchanges.inc | 34 +- maintenance/rebuildrecentchanges.php | 4 +- maintenance/refreshLinks.inc | 2 +- maintenance/refreshLinks.php | 18 +- maintenance/runJobs.php | 8 +- maintenance/storage/compressOld.inc | 2 +- maintenance/tables.sql | 13 + maintenance/testRunner.postgres.sql | 30 ++ maintenance/updateRestrictions.php | 2 +- maintenance/updateSpecialPages.php | 7 +- maintenance/updaters.inc | 466 ++++++++++++--------- maintenance/wikipedia-interwiki.sql | 2 +- 58 files changed, 2700 insertions(+), 1283 deletions(-) create mode 100644 maintenance/archives/patch-image_reditects.sql create mode 100644 maintenance/archives/patch-protected_titles.sql create mode 100644 maintenance/fetchText.php create mode 100644 maintenance/language/StatOutputs.php create mode 100644 maintenance/postgres/archives/patch-protected_titles.sql create mode 100644 maintenance/postgres/archives/patch-ts2pagetitle.sql create mode 100644 maintenance/preprocessorFuzzTest.php create mode 100644 maintenance/rebuildmessages.php create mode 100644 maintenance/testRunner.postgres.sql (limited to 'maintenance') diff --git a/maintenance/addwiki.php b/maintenance/addwiki.php index 3b6bb5d6..a19b24ce 100644 --- a/maintenance/addwiki.php +++ b/maintenance/addwiki.php @@ -71,23 +71,32 @@ function addWiki( $lang, $site, $dbName ) } } + global $wgTitle, $wgArticle; $wgTitle = Title::newMainPage(); $wgArticle = new Article( $wgTitle ); $ucsite = ucfirst( $site ); - $wgArticle->insertNewArticle( " -==This subdomain is reserved for the creation of a $ucsite in '''[[:en:{$name}|{$name}]]''' language== + $wgArticle->insertNewArticle( << +[http://www.wikipedia.org Wikipedia] | +[http://www.wiktionary.org Wiktonary] | +[http://www.wikibooks.org Wikibooks] | +[http://www.wikinews.org Wikinews] | +[http://www.wikiquote.org Wikiquote] | +[http://www.wikisource.org Wikisource] +[http://www.wikiversity.org Wikiversity] + -See the [http://www.wikipedia.org Wikipedia portal] for other language Wikipedias. +See Wikimedia's [[m:|Meta-Wiki]] for the coordination of these projects. [[aa:]] [[af:]] @@ -99,6 +108,7 @@ See the [http://www.wikipedia.org Wikipedia portal] for other language Wikipedia [[ast:]] [[ay:]] [[az:]] +[[bcl:]] [[be:]] [[bg:]] [[bn:]] @@ -125,6 +135,7 @@ See the [http://www.wikipedia.org Wikipedia portal] for other language Wikipedia [[he:]] [[hi:]] [[hr:]] +[[hsb:]] [[hy:]] [[ia:]] [[id:]] @@ -195,7 +206,9 @@ See the [http://www.wikipedia.org Wikipedia portal] for other language Wikipedia [[za:]] [[zh:]] [[zu:]] -", '', false, false ); + +EOT +, '', false, false ); print "Adding to dblists\n"; diff --git a/maintenance/archives/patch-image_reditects.sql b/maintenance/archives/patch-image_reditects.sql new file mode 100644 index 00000000..e69de29b diff --git a/maintenance/archives/patch-protected_titles.sql b/maintenance/archives/patch-protected_titles.sql new file mode 100644 index 00000000..5307cbdd --- /dev/null +++ b/maintenance/archives/patch-protected_titles.sql @@ -0,0 +1,12 @@ +-- Protected titles - nonexistent pages that have been protected +CREATE TABLE /*$wgDBprefix*/protected_titles ( + pt_namespace int NOT NULL, + pt_title varchar(255) NOT NULL, + pt_user int unsigned NOT NULL, + pt_reason tinyblob, + pt_timestamp binary(14) NOT NULL, + pt_expiry varbinary(14) NOT NULL default '', + pt_create_perm varbinary(60) NOT NULL, + PRIMARY KEY (pt_namespace,pt_title), + KEY pt_timestamp (pt_timestamp) +) /*$wgDBTableOptions*/; diff --git a/maintenance/archives/populateSha1.php b/maintenance/archives/populateSha1.php index a34d36d3..45f29c43 100644 --- a/maintenance/archives/populateSha1.php +++ b/maintenance/archives/populateSha1.php @@ -4,7 +4,7 @@ $optionsWithArgs = array( 'method' ); require_once( dirname(__FILE__).'/../commandLine.inc' ); -$method = isset( $args['method'] ) ? $args['method'] : 'normal'; +$method = isset( $options['method'] ) ? $options['method'] : 'normal'; $t = -microtime( true ); $fname = 'populateSha1.php'; @@ -14,30 +14,42 @@ $imageTable = $dbw->tableName( 'image' ); $oldimageTable = $dbw->tableName( 'oldimage' ); $batch = array(); -$cmd = 'mysql -u ' . wfEscapeShellArg( $wgDBuser ) . ' -p' . wfEscapeShellArg( $wgDBpassword, $wgDBname ); +$cmd = 'mysql -u' . wfEscapeShellArg( $wgDBuser ) . + ' -h' . wfEscapeShellArg( $wgDBserver ) . + ' -p' . wfEscapeShellArg( $wgDBpassword, $wgDBname ); if ( $method == 'pipe' ) { + echo "Using pipe method\n"; $pipe = popen( $cmd, 'w' ); - fwrite( $pipe, "-- hello\n" ); } +$numRows = $res->numRows(); +$i = 0; foreach ( $res as $row ) { + if ( $i % 100 == 0 ) { + printf( "Done %d of %d, %5.3f%% \r", $i, $numRows, $i / $numRows * 100 ); + wfWaitForSlaves( 5 ); + } $file = wfLocalFile( $row->img_name ); + if ( !$file ) { + continue; + } $sha1 = File::sha1Base36( $file->getPath() ); if ( strval( $sha1 ) !== '' ) { $sql = "UPDATE $imageTable SET img_sha1=" . $dbw->addQuotes( $sha1 ) . " WHERE img_name=" . $dbw->addQuotes( $row->img_name ); if ( $method == 'pipe' ) { - fwrite( $pipe, $sql ); + fwrite( $pipe, "$sql;\n" ); } else { $dbw->query( $sql, $fname ); } } + $i++; } if ( $method == 'pipe' ) { fflush( $pipe ); pclose( $pipe ); } $t += microtime( true ); -print "Done in $t seconds\n"; +printf( "\nDone %d files in %.1f seconds\n", $numRows, $t ); ?> diff --git a/maintenance/backup.inc b/maintenance/backup.inc index ee44954c..94fb48c9 100644 --- a/maintenance/backup.inc +++ b/maintenance/backup.inc @@ -170,7 +170,8 @@ class BackupDumper { function dump( $history, $text = MW_EXPORT_TEXT ) { # Notice messages will foul up your XML output even if they're # relatively harmless. - ini_set( 'display_errors', false ); + if( ini_get( 'display_errors' ) ) + ini_set( 'display_errors', 'stderr' ); $this->initProgress( $history ); diff --git a/maintenance/cleanupImages.php b/maintenance/cleanupImages.php index 1c0edeb5..d6ed5a7a 100644 --- a/maintenance/cleanupImages.php +++ b/maintenance/cleanupImages.php @@ -66,8 +66,8 @@ class ImageCleanup extends TableCleanup { return $this->progress( 1 ); } - if( $title->getDbKey() !== $source ) { - $munged = $title->getDbKey(); + if( $title->getDBkey() !== $source ) { + $munged = $title->getDBkey(); $this->log( "page $source ($munged) doesn't match self." ); $this->pokeFile( $source, $munged ); return $this->progress( 1 ); @@ -154,7 +154,7 @@ class ImageCleanup extends TableCleanup { $name ); $test = Title::makeTitleSafe( NS_IMAGE, $x ); - if( is_null( $test ) || $test->getDbKey() !== $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/cleanupSpam.php b/maintenance/cleanupSpam.php index 2c269b34..36d8b258 100644 --- a/maintenance/cleanupSpam.php +++ b/maintenance/cleanupSpam.php @@ -14,9 +14,8 @@ function cleanupArticle( $id, $domain ) { $rev = Revision::newFromTitle( $title ); $revId = $rev->getId(); $currentRevId = $revId; - $regex = LinkFilter::makeRegex( $domain ); - while ( $rev && preg_match( $regex, $rev->getText() ) ) { + while ( $rev && LinkFilter::matchEntry( $rev->getText() , $domain ) ) { # Revision::getPrevious can't be used in this way before MW 1.6 (Revision.php 1.26) #$rev = $rev->getPrevious(); $revId = $title->getPreviousRevisionID( $revId ); diff --git a/maintenance/cleanupTitles.php b/maintenance/cleanupTitles.php index 0ec57d4e..1f06b165 100644 --- a/maintenance/cleanupTitles.php +++ b/maintenance/cleanupTitles.php @@ -79,7 +79,7 @@ class TitleCleanup extends TableCleanup { $title = Title::newFromText( $clean ); } - $dest = $title->getDbKey(); + $dest = $title->getDBkey(); if( $this->dryrun ) { $this->log( "DRY RUN: would rename $row->page_id ($row->page_namespace,'$row->page_title') to ($row->page_namespace,'$dest')" ); } else { @@ -97,7 +97,7 @@ class TitleCleanup extends TableCleanup { if( $title->getInterwiki() ) { $prior = $title->getPrefixedDbKey(); } else { - $prior = $title->getDbKey(); + $prior = $title->getDBkey(); } $clean = 'Broken/' . $prior; $verified = Title::makeTitleSafe( $row->page_namespace, $clean ); @@ -112,7 +112,7 @@ class TitleCleanup extends TableCleanup { wfDie( "Something awry; empty title.\n" ); } $ns = $title->getNamespace(); - $dest = $title->getDbKey(); + $dest = $title->getDBkey(); if( $this->dryrun ) { $this->log( "DRY RUN: would rename $row->page_id ($row->page_namespace,'$row->page_title') to ($row->page_namespace,'$dest')" ); } else { diff --git a/maintenance/commandLine.inc b/maintenance/commandLine.inc index 4466344f..f7bb53ff 100644 --- a/maintenance/commandLine.inc +++ b/maintenance/commandLine.inc @@ -216,6 +216,20 @@ if ( defined( 'MW_CMDLINE_CALLBACK' ) ) { ini_set( 'memory_limit', -1 ); +if( version_compare( phpversion(), '5.2.4' ) >= 0 ) { + // Send PHP warnings and errors to stderr instead of stdout. + // This aids in diagnosing problems, while keeping messages + // out of redirected output. + if( ini_get( 'display_errors' ) ) { + ini_set( 'display_errors', 'stderr' ); + } + + // Don't touch the setting on earlier versions of PHP, + // as setting it would disable output if you'd wanted it. + + // Note that exceptions are also sent to stderr when + // command-line mode is on, regardless of PHP version. +} $wgShowSQLErrors = true; require_once( "$IP/includes/Setup.php" ); diff --git a/maintenance/createAndPromote.php b/maintenance/createAndPromote.php index af4a1dab..0d30fe73 100644 --- a/maintenance/createAndPromote.php +++ b/maintenance/createAndPromote.php @@ -38,7 +38,7 @@ if( !is_object( $user ) ) { # Insert the account into the database $user->addToDatabase(); $user->setPassword( $password ); -$user->setToken(); +$user->saveSettings(); # Promote user $user->addGroup( 'sysop' ); diff --git a/maintenance/deleteBatch.php b/maintenance/deleteBatch.php index 6821ee29..62169641 100644 --- a/maintenance/deleteBatch.php +++ b/maintenance/deleteBatch.php @@ -67,6 +67,10 @@ for ( $linenum = 1; !feof( $file ); $linenum++ ) { $dbw->begin(); if( $page->getNamespace() == NS_IMAGE ) { $art = new ImagePage( $page ); + $img = wfFindFile( $art->mTitle ); + if( !$img || !$img->delete( $reason ) ) { + print "FAILED to delete image file... "; + } } else { $art = new Article( $page ); } @@ -75,7 +79,7 @@ for ( $linenum = 1; !feof( $file ); $linenum++ ) { if ( $success ) { print "\n"; } else { - print " FAILED\n"; + print " FAILED to delete image page\n"; } if ( $interval ) { diff --git a/maintenance/deleteDefaultMessages.php b/maintenance/deleteDefaultMessages.php index 32e03494..9a7f5c6a 100644 --- a/maintenance/deleteDefaultMessages.php +++ b/maintenance/deleteDefaultMessages.php @@ -1,7 +1,7 @@ addGroup( 'bot' ); - + $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( array( 'page', 'revision' ), array( 'page_namespace', 'page_title' ), diff --git a/maintenance/deleteOldRevisions.inc b/maintenance/deleteOldRevisions.inc index 8d8ca9a1..03fb2306 100644 --- a/maintenance/deleteOldRevisions.inc +++ b/maintenance/deleteOldRevisions.inc @@ -9,7 +9,7 @@ require_once( 'purgeOldText.inc' ); -function DeleteOldRevisions( $delete = false ) { +function DeleteOldRevisions( $delete = false, $args = array() ) { # Data should come off the master, wrapped in a transaction $dbw = wfGetDB( DB_MASTER ); @@ -18,9 +18,20 @@ function DeleteOldRevisions( $delete = false ) { $tbl_pag = $dbw->tableName( 'page' ); $tbl_rev = $dbw->tableName( 'revision' ); + $pageIdClause = ''; + $revPageClause = ''; + + # If a list of page_ids was provided, limit results to that set of page_ids + if ( sizeof( $args ) > 0 ) { + $pageIdList = implode( ',', $args ); + $pageIdClause = " WHERE page_id IN ({$pageIdList})"; + $revPageClause = " AND rev_page IN ({$pageIdList})"; + echo( "Limiting to {$tbl_pag}.page_id IN ({$pageIdList})\n" ); + } + # Get "active" revisions from the page table echo( "Searching for active revisions..." ); - $res = $dbw->query( "SELECT page_latest FROM $tbl_pag" ); + $res = $dbw->query( "SELECT page_latest FROM $tbl_pag{$pageIdClause}" ); while( $row = $dbw->fetchObject( $res ) ) { $cur[] = $row->page_latest; } @@ -29,7 +40,7 @@ function DeleteOldRevisions( $delete = false ) { # Get all revisions that aren't in this set echo( "Searching for inactive revisions..." ); $set = implode( ', ', $cur ); - $res = $dbw->query( "SELECT rev_id FROM $tbl_rev WHERE rev_id NOT IN ( $set )" ); + $res = $dbw->query( "SELECT rev_id FROM $tbl_rev WHERE rev_id NOT IN ( $set ){$revPageClause}" ); while( $row = $dbw->fetchObject( $res ) ) { $old[] = $row->rev_id; } diff --git a/maintenance/deleteOldRevisions.php b/maintenance/deleteOldRevisions.php index 8d676ab1..8454479b 100644 --- a/maintenance/deleteOldRevisions.php +++ b/maintenance/deleteOldRevisions.php @@ -16,12 +16,12 @@ echo( "Delete Old Revisions\n\n" ); if( @$options['help'] ) { ShowUsage(); } else { - DeleteOldRevisions( @$options['delete'] ); + DeleteOldRevisions( @$options['delete'], $args ); } function ShowUsage() { echo( "Deletes non-current revisions from the database.\n\n" ); - echo( "Usage: php deleteOldRevisions.php [--delete|--help]\n\n" ); + echo( "Usage: php deleteOldRevisions.php [--delete|--help] [ ...]\n\n" ); echo( "delete : Performs the deletion\n" ); echo( " help : Show this usage information\n" ); } diff --git a/maintenance/dumpHTML.php b/maintenance/dumpHTML.php index 26e914ff..bd94958e 100644 --- a/maintenance/dumpHTML.php +++ b/maintenance/dumpHTML.php @@ -1,160 +1,7 @@ - destination directory - -s start ID - -e end ID - -k skin to use (defaults to htmldump) - --no-overwrite skip existing HTML files - --checkpoint use a checkpoint file to allow restarting of interrupted dumps - --slice split the job into m segments and do the n'th one - --images only do image description pages - --shared-desc only do shared (commons) image description pages - --no-shared-desc don't do shared image description pages - --categories only do category pages - --redirects only do redirects - --special only do miscellaneous stuff - --force-copy copy commons instead of symlink, needed for Wikimedia - --interlang allow interlanguage links - --image-snapshot copy all images used to the destination directory - --compress generate compressed version of the html pages - --udp-profile profile 1/N rendering operations using ProfilerSimpleUDP - -END; -} - -$optionsWithArgs = array( 's', 'd', 'e', 'k', 'checkpoint', 'slice', 'udp-profile' ); -$options = array( 'help' ); -$profiling = false; - -if ( $profiling ) { - define( 'MW_CMDLINE_CALLBACK', 'wfSetupDump' ); - function wfSetupDump() { - global $wgProfiling, $wgProfileToDatabase, $wgProfileSampleRate; - $wgProfiling = true; - $wgProfileToDatabase = false; - $wgProfileSampleRate = 1; - } -} - -if ( in_array( '--udp-profile', $argv ) ) { - define( 'MW_FORCE_PROFILE', 1 ); -} - -require_once( "commandLine.inc" ); -require_once( "dumpHTML.inc" ); - -error_reporting( E_ALL & (~E_NOTICE) ); - -if( isset( $options['help'] ) ) { - ShowUsage(); - exit(); -} - -if ( !empty( $options['s'] ) ) { - $start = $options['s']; -} else { - $start = 1; -} - -if ( !empty( $options['e'] ) ) { - $end = $options['e']; -} else { - $dbr = wfGetDB( DB_SLAVE ); - $end = $dbr->selectField( 'page', 'max(page_id)', false ); -} - -if ( !empty( $options['d'] ) ) { - $dest = $options['d']; -} else { - $dest = "$IP/static"; -} - -$skin = isset( $options['k'] ) ? $options['k'] : 'htmldump'; - -if ( $options['slice'] ) { - $bits = explode( '/', $options['slice'] ); - if ( count( $bits ) != 2 || $bits[0] < 1 || $bits[0] > $bits[1] ) { - print "Invalid slice specification"; - exit; - } - $sliceNumerator = $bits[0]; - $sliceDenominator = $bits[1]; -} else { - $sliceNumerator = $sliceDenominator = 1; -} - -$wgHTMLDump = new DumpHTML( array( - 'dest' => $dest, - 'forceCopy' => $options['force-copy'], - 'alternateScriptPath' => $options['interlang'], - 'interwiki' => $options['interlang'], - 'skin' => $skin, - 'makeSnapshot' => $options['image-snapshot'], - 'checkpointFile' => $options['checkpoint'], - 'startID' => $start, - 'endID' => $end, - 'sliceNumerator' => $sliceNumerator, - 'sliceDenominator' => $sliceDenominator, - 'noOverwrite' => $options['no-overwrite'], - 'compress' => $options['compress'], - 'noSharedDesc' => $options['no-shared-desc'], - 'udpProfile' => $options['udp-profile'], -)); - - -if ( $options['special'] ) { - $wgHTMLDump->doSpecials(); -} elseif ( $options['images'] ) { - $wgHTMLDump->doImageDescriptions(); -} elseif ( $options['categories'] ) { - $wgHTMLDump->doCategories(); -} elseif ( $options['redirects'] ) { - $wgHTMLDump->doRedirects(); -} elseif ( $options['shared-desc'] ) { - $wgHTMLDump->doSharedImageDescriptions(); -} else { - print "Creating static HTML dump in directory $dest. \n"; - $dbr = wfGetDB( DB_SLAVE ); - $server = $dbr->getProperty( 'mServer' ); - print "Using database {$server}\n"; - - if ( !isset( $options['e'] ) ) { - $wgHTMLDump->doEverything(); - } else { - $wgHTMLDump->doArticles(); - } -} - -if ( isset( $options['debug'] ) ) { - #print_r($GLOBALS); - # Workaround for bug #36957 - $globals = array_keys( $GLOBALS ); - #sort( $globals ); - $sizes = array(); - foreach ( $globals as $name ) { - $sizes[$name] = strlen( serialize( $GLOBALS[$name] ) ); - } - arsort($sizes); - $sizes = array_slice( $sizes, 0, 20 ); - foreach ( $sizes as $name => $size ) { - printf( "%9d %s\n", $size, $name ); - } -} - -if ( $profiling ) { - echo $wgProfiler->getOutput(); -} +dumpHTML has moved to the DumpHTML extension. +WebDAV/SVN: +http://svn.wikimedia.org/svnroot/mediawiki/trunk/extensions/DumpHTML/ +Web: +http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/DumpHTML/ diff --git a/maintenance/dumpTextPass.php b/maintenance/dumpTextPass.php index 92ab4b4e..440f6d27 100644 --- a/maintenance/dumpTextPass.php +++ b/maintenance/dumpTextPass.php @@ -104,6 +104,13 @@ class TextPassDumper extends BackupDumper { var $failures = 0; var $maxFailures = 200; var $failureTimeout = 5; // Seconds to sleep after db failure + + var $php = "php"; + var $spawn = false; + var $spawnProc = false; + var $spawnWrite = false; + var $spawnRead = false; + var $spawnErr = false; function dump() { # This shouldn't happen if on console... ;) @@ -111,7 +118,8 @@ class TextPassDumper extends BackupDumper { # Notice messages will foul up your XML output even if they're # relatively harmless. -// ini_set( 'display_errors', false ); + if( ini_get( 'display_errors' ) ) + ini_set( 'display_errors', 'stderr' ); $this->initProgress( $this->history ); @@ -125,6 +133,10 @@ class TextPassDumper extends BackupDumper { if( WikiError::isError( $result ) ) { wfDie( $result->getMessage() ); } + + if( $this->spawnProc ) { + $this->closeSpawn(); + } $this->report( true ); } @@ -134,7 +146,8 @@ class TextPassDumper extends BackupDumper { switch( $opt ) { case 'prefetch': - require_once 'maintenance/backupPrefetch.inc'; + global $IP; + require_once "$IP/maintenance/backupPrefetch.inc"; $this->prefetch = new BaseDump( $url ); break; case 'stub': @@ -146,6 +159,12 @@ class TextPassDumper extends BackupDumper { case 'full': $this->history = WikiExporter::FULL; break; + case 'spawn': + $this->spawn = true; + if( $val ) { + $this->php = $val; + } + break; } } @@ -237,9 +256,26 @@ class TextPassDumper extends BackupDumper { return $text; } } + return $this->doGetText( $id ); + } + + private function doGetText( $id ) { + if( $this->spawn ) { + return $this->getTextSpawned( $id ); + } else { + return $this->getTextDbSafe( $id ); + } + } + + /** + * Fetch a text revision from the database, retrying in case of failure. + * This may survive some transitory errors by reconnecting, but + * may not survive a long-term server outage. + */ + private function getTextDbSafe( $id ) { while( true ) { try { - $text = $this->doGetText( $id ); + $text = $this->getTextDb( $id ); $ex = new MWException("Graceful storage failure"); } catch (DBQueryError $ex) { $text = false; @@ -263,7 +299,7 @@ class TextPassDumper extends BackupDumper { /** * May throw a database error if, say, the server dies during query. */ - private function doGetText( $id ) { + private function getTextDb( $id ) { $id = intval( $id ); $row = $this->db->selectRow( 'text', array( 'old_text', 'old_flags' ), @@ -277,6 +313,111 @@ class TextPassDumper extends BackupDumper { $normalized = UtfNormal::cleanUp( $stripped ); return $normalized; } + + private function getTextSpawned( $id ) { + wfSuppressWarnings(); + if( !$this->spawnProc ) { + // First time? + $this->openSpawn(); + } + while( true ) { + + $text = $this->getTextSpawnedOnce( $id ); + if( !is_string( $text ) ) { + $this->progress("Database subprocess failed. Respawning..."); + + $this->closeSpawn(); + sleep( $this->failureTimeout ); + $this->openSpawn(); + + continue; + } + wfRestoreWarnings(); + return $text; + } + } + + function openSpawn() { + global $IP, $wgDBname; + + $cmd = implode( " ", + array_map( 'wfEscapeShellArg', + array( + $this->php, + "$IP/maintenance/fetchText.php", + $wgDBname ) ) ); + $spec = array( + 0 => array( "pipe", "r" ), + 1 => array( "pipe", "w" ), + 2 => array( "file", "/dev/null", "a" ) ); + $pipes = array(); + + $this->progress( "Spawning database subprocess: $cmd" ); + $this->spawnProc = proc_open( $cmd, $spec, $pipes ); + if( !$this->spawnProc ) { + // shit + $this->progress( "Subprocess spawn failed." ); + return false; + } + list( + $this->spawnWrite, // -> stdin + $this->spawnRead, // <- stdout + ) = $pipes; + + return true; + } + + private function closeSpawn() { + wfSuppressWarnings(); + if( $this->spawnRead ) + fclose( $this->spawnRead ); + $this->spawnRead = false; + if( $this->spawnWrite ) + fclose( $this->spawnWrite ); + $this->spawnWrite = false; + if( $this->spawnErr ) + fclose( $this->spawnErr ); + $this->spawnErr = false; + if( $this->spawnProc ) + pclose( $this->spawnProc ); + $this->spawnProc = false; + wfRestoreWarnings(); + } + + private function getTextSpawnedOnce( $id ) { + $ok = fwrite( $this->spawnWrite, "$id\n" ); + //$this->progress( ">> $id" ); + if( !$ok ) return false; + + $ok = fflush( $this->spawnWrite ); + //$this->progress( ">> [flush]" ); + if( !$ok ) return false; + + $len = fgets( $this->spawnRead ); + //$this->progress( "<< " . trim( $len ) ); + if( $len === false ) return false; + + $nbytes = intval( $len ); + $text = ""; + + // Subprocess may not send everything at once, we have to loop. + while( $nbytes > strlen( $text ) ) { + $buffer = fread( $this->spawnRead, $nbytes - strlen( $text ) ); + if( $text === false ) break; + $text .= $buffer; + } + + $gotbytes = strlen( $text ); + if( $gotbytes != $nbytes ) { + $this->progress( "Expected $nbytes bytes from database subprocess, got $gotbytes "); + return false; + } + + // Do normalization in the dump thread... + $stripped = str_replace( "\r", "", $text ); + $normalized = UtfNormal::cleanUp( $stripped ); + return $normalized; + } function startElement( $parser, $name, $attribs ) { $this->clearOpenElement( null ); @@ -371,6 +512,7 @@ Options: (Default: 100) --server=h Force reading from MySQL server h --current Base ETA on number of pages in database instead of all revisions + --spawn Spawn a subprocess for loading text records END ); } diff --git a/maintenance/dumpUploads.php b/maintenance/dumpUploads.php index 74a28380..50d03ae1 100644 --- a/maintenance/dumpUploads.php +++ b/maintenance/dumpUploads.php @@ -3,12 +3,12 @@ require_once 'commandLine.inc'; class UploadDumper { - function __construct( $args ) { global $IP, $wgUseSharedUploads; - $this->mAction = 'fetchUsed'; + $this->mAction = 'fetchLocal'; $this->mBasePath = $IP; - $this->mShared = $wgUseSharedUploads; + $this->mShared = false; + $this->mSharedSupplement = false; if( isset( $args['help'] ) ) { $this->mAction = 'help'; @@ -17,10 +17,31 @@ class UploadDumper { if( isset( $args['base'] ) ) { $this->mBasePath = $args['base']; } + + if( isset( $args['local'] ) ) { + $this->mAction = 'fetchLocal'; + } + + if( isset( $args['used'] ) ) { + $this->mAction = 'fetchUsed'; + } + + if( isset( $args['shared'] ) ) { + if( isset( $args['used'] ) ) { + // Include shared-repo files in the used check + $this->mShared = true; + } else { + // Grab all local *plus* used shared + $this->mSharedSupplement = true; + } + } } function run() { - $this->{$this->mAction}(); + $this->{$this->mAction}( $this->mShared ); + if( $this->mSharedSupplement ) { + $this->fetchUsed( true ); + } } function help() { @@ -35,8 +56,6 @@ php dumpUploads.php [options] > list-o-files.txt Options: --base= Set base relative path instead of wiki include root -FIXME: other options not implemented yet ;) - --local List all local files, used or not. No shared files included. --used Skip local images that are not used --shared Include images used from shared repository @@ -50,7 +69,7 @@ END; * @param string $directory Base directory where files are located * @param bool $shared true to pass shared-dir settings to hash func */ - function fetchUsed() { + function fetchUsed( $shared ) { $dbr = wfGetDB( DB_SLAVE ); $image = $dbr->tableName( 'image' ); $imagelinks = $dbr->tableName( 'imagelinks' ); @@ -61,52 +80,38 @@ END; ON il_to=img_name"; $result = $dbr->query( $sql ); - while( $row = $dbr->fetchObject( $result ) ) { - if( is_null( $row->img_name ) ) { - if( $this->mShared ) { - $this->outputShared( $row->il_to ); - } - } else { - $this->outputLocal( $row->il_to ); - } + foreach( $result as $row ) { + $this->outputItem( $row->il_to, $shared ); } $dbr->freeResult( $result ); } - function outputLocal( $name ) { - global $wgUploadDirectory; - return $this->outputItem( $name, $wgUploadDirectory, false ); + function fetchLocal( $shared ) { + $dbr = wfGetDB( DB_SLAVE ); + $result = $dbr->select( 'image', + array( 'img_name' ), + '', + __METHOD__ ); + + foreach( $result as $row ) { + $this->outputItem( $row->img_name, $shared ); + } + $dbr->freeResult( $result ); } - function outputShared( $name ) { - global $wgSharedUploadDirectory; - return $this->outputItem( $name, $wgSharedUploadDirectory, true ); + function outputItem( $name, $shared ) { + $file = wfFindFile( $name ); + if( $file && $this->filterItem( $file, $shared ) ) { + $filename = $file->getFullPath(); + $rel = wfRelativePath( $filename, $this->mBasePath ); + echo "$rel\n"; + } else { + wfDebug( __METHOD__ . ": base file? $name\n" ); + } } - function outputItem( $name, $directory, $shared ) { - $filename = $directory . - wfGetHashPath( $name, $shared ) . - $name; - $rel = $this->relativePath( $filename, $this->mBasePath ); - echo "$rel\n"; - } - - /** - * Return a relative path to $path from the base directory $base - * For instance relativePath( '/foo/bar/baz', '/foo' ) should return - * 'bar/baz'. - */ - function relativePath( $path, $base) { - $path = explode( DIRECTORY_SEPARATOR, $path ); - $base = explode( DIRECTORY_SEPARATOR, $base ); - while( count( $base ) && $path[0] == $base[0] ) { - array_shift( $path ); - array_shift( $base ); - } - foreach( $base as $prefix ) { - array_unshift( $path, '..' ); - } - return implode( DIRECTORY_SEPARATOR, $path ); + function filterItem( $file, $shared ) { + return $shared || $file->isLocal(); } } diff --git a/maintenance/fetchText.php b/maintenance/fetchText.php new file mode 100644 index 00000000..3b745c0a --- /dev/null +++ b/maintenance/fetchText.php @@ -0,0 +1,36 @@ +selectRow( 'text', + array( 'old_text', 'old_flags' ), + array( 'old_id' => $id ), + 'TextPassDumper::getText' ); + $text = Revision::getRevisionText( $row ); + if( $text === false ) { + return false; + } + return $text; +} + + +?> \ No newline at end of file diff --git a/maintenance/findhooks.php b/maintenance/findhooks.php index 284e7906..8433571d 100644 --- a/maintenance/findhooks.php +++ b/maintenance/findhooks.php @@ -7,25 +7,26 @@ * - hooks names in hooks.txt are at the beginning of a line and single quoted. * - hooks names in code are the first parameter of wfRunHooks. * + * Any instance of wfRunHooks that doesn't meet these parameters will be noted. + * * @addtogroup Maintenance * * @author Ashar Voultoiz * @copyright Copyright © Ashar voultoiz * @license http://www.gnu.org/copyleft/gpl.html GNU General Public Licence 2.0 or later */ - + /** This is a command line script*/ include('commandLine.inc'); - - + + # GLOBALS - + $doc = $IP . '/docs/hooks.txt'; -$pathinc = $IP . '/includes/'; - - +$pathinc = array( $IP.'/includes/', $IP.'/includes/api/', $IP.'/includes/filerepo/', $IP.'/languages/', $IP.'/maintenance/', $IP.'/skins/' ); + # FUNCTIONS - + /** * @return array of documented hooks */ @@ -36,19 +37,19 @@ function getHooksFromDoc() { preg_match_all( "/\n'(.*?)'/", $content, $m); return $m[1]; } - + /** - * Get hooks from a php file + * Get hooks from a PHP file * @param $file Full filename to the PHP file. * @return array of hooks found. */ function getHooksFromFile( $file ) { $content = file_get_contents( $file ); $m = array(); - preg_match_all( "/wfRunHooks\(\s*\'(.*?)\'/", $content, $m); - return $m[1]; + preg_match_all( '/wfRunHooks\(\s*([\'"])(.*?)\1/', $content, $m); + return $m[2]; } - + /** * Get hooks from the source code. * @param $path Directory where the include files can be found @@ -66,7 +67,43 @@ function getHooksFromPath( $path ) { } return $hooks; } - + +/** + * Get bad hooks (where the hook name could not be determined) from a PHP file + * @param $file Full filename to the PHP file. + * @return array of bad wfRunHooks() lines + */ +function getBadHooksFromFile( $file ) { + $content = file_get_contents( $file ); + $m = array(); + # We want to skip the "function wfRunHooks()" one. :) + preg_match_all( '/(? + * @author Ashar Voultoiz + */ + +/** A general output object. Need to be overriden */ +class statsOutput { + function formatPercent( $subset, $total, $revert = false, $accuracy = 2 ) { + return @sprintf( '%.' . $accuracy . 'f%%', 100 * $subset / $total ); + } + + # Override the following methods + function heading() { + } + function footer() { + } + function blockstart() { + } + function blockend() { + } + function element( $in, $heading = false ) { + } +} + +/** Outputs WikiText */ +class wikiStatsOutput extends statsOutput { + function heading() { + global $IP; + $version = SpecialVersion::getVersion( $IP ); + echo "'''Statistics are based on:''' " . $version . "\n\n"; + echo "'''Note:''' These statistics can be generated by running php maintenance/language/transstat.php.\n\n"; + echo "For additional information on specific languages (the message names, the actual problems, etc.), run php maintenance/language/checkLanguage.php --lang=foo.\n\n"; + echo '{| class="sortable wikitable" border="2" cellpadding="4" cellspacing="0" style="background-color: #F9F9F9; border: 1px #AAAAAA solid; border-collapse: collapse; clear:both;" width="100%"'."\n"; + } + function footer() { + echo "|}\n"; + } + function blockstart() { + echo "|-\n"; + } + function blockend() { + echo ''; + } + function element( $in, $heading = false ) { + echo ($heading ? '!' : '|') . " $in\n"; + } + function formatPercent( $subset, $total, $revert = false, $accuracy = 2 ) { + $v = @round(255 * $subset / $total); + if ( $revert ) { + $v = 255 - $v; + } + if ( $v < 128 ) { + # Red to Yellow + $red = 'FF'; + $green = sprintf( '%02X', 2 * $v ); + } else { + # Yellow to Green + $red = sprintf('%02X', 2 * ( 255 - $v ) ); + $green = 'FF'; + } + $blue = '00'; + $color = $red . $green . $blue; + + $percent = statsOutput::formatPercent( $subset, $total, $revert, $accuracy ); + return 'bgcolor="#'. $color .'" | '. $percent; + } +} + +/** Outputs WikiText and appends category and text only used for Meta-Wiki */ +class metawikiStatsOutput extends wikiStatsOutput { + function heading() { + echo "See [[MediaWiki localisation]] to learn how you can help translating MediaWiki.\n\n"; + parent::heading(); + } + function footer() { + parent::footer(); + echo "\n[[Category:Localisation|Statistics]]\n"; + } +} + +/** Output text. To be used on a terminal for example. */ +class textStatsOutput extends statsOutput { + function element( $in, $heading = false ) { + echo $in."\t"; + } + function blockend() { + echo "\n"; + } +} + +/** csv output. Some people love excel */ +class csvStatsOutput extends statsOutput { + function element( $in, $heading = false ) { + echo $in . ";"; + } + function blockend() { + echo "\n"; + } +} diff --git a/maintenance/language/checkLanguage.inc b/maintenance/language/checkLanguage.inc index 468db550..51de8014 100644 --- a/maintenance/language/checkLanguage.inc +++ b/maintenance/language/checkLanguage.inc @@ -1,109 +1,23 @@ getMessages( $code ); - $messagesNumber = count( $messages['translated'] ); - # Skip the checks if told so - if ( $displayLevel == 0 ) { - return; - } - - # Initialize counts - $problems = 0; - - # Set default language code and checks - if ( !$code ) { - global $wgContLang; - $code = $wgContLang->getCode(); - } - if ( !$checks ) { - $checks = array( 'untranslated', 'obsolete', 'variables', 'empty', 'whitespace', 'xhtml', 'chars' ); - } - - # Untranslated messages - if ( in_array( 'untranslated', $checks ) ) { - $generalMessages = $languages->getGeneralMessages(); - $requiredMessagesNumber = count( $generalMessages['required'] ); - $untranslatedMessages = $languages->getUntranslatedMessages( $code ); - $untranslatedMessagesNumber = count( $untranslatedMessages ); - $languages->outputMessagesList( $untranslatedMessages, $code, "\n$untranslatedMessagesNumber messages of $requiredMessagesNumber are not translated to $code, but exist in en:", $displayLevel, $links, $wikiLanguage ); - $problems += $untranslatedMessagesNumber; - } - - # Duplicate messages - if ( in_array( 'duplicate', $checks ) ) { - $duplicateMessages = $languages->getDuplicateMessages( $code ); - $duplicateMessagesNumber = count( $duplicateMessages ); - $languages->outputMessagesList( $duplicateMessages, $code, "\n$duplicateMessagesNumber messages of $messagesNumber are translated the same in en and $code:", $displayLevel, $links, $wikiLanguage ); - $problems += $duplicateMessagesNumber; - } - - # Obsolete messages - if ( in_array( 'obsolete', $checks ) ) { - $obsoleteMessages = $messages['obsolete']; - $obsoleteMessagesNumber = count( $obsoleteMessages ); - $languages->outputMessagesList( $obsoleteMessages, $code, "\n$obsoleteMessagesNumber messages of $messagesNumber do not exist in en (or are in the ignored list), but still exist in $code:", $displayLevel, $links, $wikiLanguage ); - $problems += $obsoleteMessagesNumber; - } - - # Messages without variables - if ( in_array( 'variables', $checks ) ) { - $messagesWithoutVariables = $languages->getMessagesWithoutVariables( $code ); - $messagesWithoutVariablesNumber = count( $messagesWithoutVariables ); - $languages->outputMessagesList( $messagesWithoutVariables, $code, "\n$messagesWithoutVariablesNumber messages of $messagesNumber in $code don't use some variables while en uses them:", $displayLevel, $links, $wikiLanguage ); - $problems += $messagesWithoutVariablesNumber; - } - - # Messages without plural - if ( in_array( 'plural', $checks ) ) { - $messagesWithoutPlural = $languages->getMessagesWithoutPlural( $code ); - $messagesWithoutPluralNumber = count( $messagesWithoutPlural ); - $languages->outputMessagesList( $messagesWithoutPlural, $code, "\n$messagesWithoutPluralNumber messages of $messagesNumber in $code don't use {{plural}} while en uses it:", $displayLevel, $links, $wikiLanguage ); - $problems += $messagesWithoutPluralNumber; - } - - # Empty messages - if ( in_array( 'empty', $checks ) ) { - $emptyMessages = $languages->getEmptyMessages( $code ); - $emptyMessagesNumber = count( $emptyMessages ); - $languages->outputMessagesList( $emptyMessages, $code, "\n$emptyMessagesNumber messages of $messagesNumber in $code are empty or -:", $displayLevel, $links, $wikiLanguage ); - $problems += $emptyMessagesNumber; - } - - # Messages with whitespace - if ( in_array( 'whitespace', $checks ) ) { - $messagesWithWhitespace = $languages->getMessagesWithWhitespace( $code ); - $messagesWithWhitespaceNumber = count( $messagesWithWhitespace ); - $languages->outputMessagesList( $messagesWithWhitespace, $code, "\n$messagesWithWhitespaceNumber messages of $messagesNumber in $code have a trailing whitespace:", $displayLevel, $links, $wikiLanguage ); - $problems += $messagesWithWhitespaceNumber; - } - - # Non-XHTML messages - if ( in_array( 'xhtml', $checks ) ) { - $nonXHTMLMessages = $languages->getNonXHTMLMessages( $code ); - $nonXHTMLMessagesNumber = count( $nonXHTMLMessages ); - $languages->outputMessagesList( $nonXHTMLMessages, $code, "\n$nonXHTMLMessagesNumber messages of $messagesNumber in $code are not well-formed XHTML:", $displayLevel, $links, $wikiLanguage ); - $problems += $nonXHTMLMessagesNumber; - } - - # Messages with wrong characters - if ( in_array( 'chars', $checks ) ) { - $messagesWithWrongChars = $languages->getMessagesWithWrongChars( $code ); - $messagesWithWrongCharsNumber = count( $messagesWithWrongChars ); - $languages->outputMessagesList( $messagesWithWrongChars, $code, "\n$messagesWithWrongCharsNumber messages of $messagesNumber in $code include hidden chars which should not be used in the messages:", $displayLevel, $links, $wikiLanguage ); - $problems += $messagesWithWrongCharsNumber; - } - - return $problems; -} +# Blacklist some checks for some languages +$checkBlacklist = array( +#'code' => array( 'check1', 'check2' ... ) +'gan' => array( 'plural' ), +'hak' => array( 'plural' ), +'ja' => array( 'plural' ), // Does not use plural +'my' => array( 'chars' ), // Uses a lot zwnj +'tet' => array( 'plural' ), +'th' => array( 'plural' ), +'wuu' => array( 'plural' ), +'yue' => array( 'plural' ), +'zh' => array( 'plural' ), +'zh-classical' => array( 'plural' ), +'zh-cn' => array( 'plural' ), +'zh-hans' => array( 'plural' ), +'zh-hant' => array( 'plural' ), +'zh-hk' => array( 'plural' ), +'zh-sg' => array( 'plural' ), +'zh-tw' => array( 'plural' ), +'zh-yue' => array( 'plural' ), +); diff --git a/maintenance/language/checkLanguage.php b/maintenance/language/checkLanguage.php index 42a43c02..36d32a48 100644 --- a/maintenance/language/checkLanguage.php +++ b/maintenance/language/checkLanguage.php @@ -7,31 +7,137 @@ require_once( dirname(__FILE__).'/../commandLine.inc' ); require_once( 'languages.inc' ); -require_once( 'checkLanguage.inc' ); -# Show help -if ( isset( $options['help'] ) ) { - echo <<execute(); + +class CheckLanguageCLI { + private $code = null; + private $level = 2; + private $doLinks = false; + private $wikiCode = 'en'; + private $includeExif = false; + private $checkAll = false; + private $output = 'plain'; + private $checks = array(); + + private $defaultChecks = array( + 'untranslated', 'obsolete', 'variables', 'empty', 'plural', + 'whitespace', 'xhtml', 'chars', 'links', 'unbalanced' + ); + + private $L = null; + + /** + * GLOBALS: $wgLanguageCode; + */ + public function __construct( Array $options ) { + + if ( isset( $options['help'] ) ) { + echo $this->help(); + exit(); + } + + if ( isset($options['lang']) ) { + $this->code = $options['lang']; + } else { + global $wgLanguageCode; + $this->code = $wgLanguageCode; + } + + if ( isset($options['level']) ) { + $this->level = $options['level']; + } + + $this->doLinks = isset($options['links']); + $this->includeExif = !isset($options['noexif']); + $this->checkAll = isset($options['all']); + + if ( isset($options['wikilang']) ) { + $this->wikiCode = $options['wikilang']; + } + + if ( isset( $options['whitelist'] ) ) { + $this->checks = explode( ',', $options['whitelist'] ); + } elseif ( isset( $options['blacklist'] ) ) { + $this->checks = array_diff( + $this->defaultChecks, + explode( ',', $options['blacklist'] ) + ); + } else { + $this->checks = $this->defaultChecks; + } + + 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 ); + } + + 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; + } + + 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; + } + + protected function help() { + return <<