sync-group.php

Go to the documentation of this file.
00001 <?php
00014 // Standard boilerplate to define $IP
00015 if ( getenv( 'MW_INSTALL_PATH' ) !== false ) {
00016     $IP = getenv( 'MW_INSTALL_PATH' );
00017 } else {
00018     $dir = __DIR__;
00019     $IP = "$dir/../../..";
00020 }
00021 require_once "$IP/maintenance/Maintenance.php";
00022 
00023 # Override the memory limit for wfShellExec, 100 MB seems to be too little for svn
00024 $wgMaxShellMemory = 1024 * 200;
00025 
00026 class SyncGroup extends Maintenance {
00027     public function __construct() {
00028         parent::__construct();
00029         $this->mDescription = 'Import or update source messages and translations into ' .
00030             'the wiki database.';
00031         $this->addOption(
00032             'git',
00033             '(optional) Use git to retrieve last modified date of i18n files. Will use subversion ' .
00034             'by default and fallback on filesystem timestamp',
00035             false, /*required*/
00036             false /*has arg*/
00037         );
00038         $this->addOption(
00039             'group',
00040             'Comma separated list of group IDs (can use * as wildcard).',
00041             true, /*required*/
00042             true /*has arg*/
00043         );
00044         $this->addOption(
00045             'lang',
00046             '(optional) Comma separated list of language codes or *',
00047             false, /*required*/
00048             true /*has arg*/
00049         );
00050         $this->addOption(
00051             'norc',
00052             '(optional) Do not add entries to recent changes table',
00053             false, /*required*/
00054             false /*has arg*/
00055         );
00056         $this->addOption(
00057             'noask',
00058             '(optional) Skip all conflicts',
00059             false, /*required*/
00060             false /*has arg*/
00061         );
00062         $this->addOption(
00063             'start',
00064             '(optional) Start of the last export (changes in wiki after will conflict)',
00065             false, /*required*/
00066             true /*has arg*/
00067         );
00068         $this->addOption(
00069             'end',
00070             '(optional) End of the last export (changes in source after will conflict)',
00071             false, /*required*/
00072             true /*has arg*/
00073         );
00074         $this->addOption(
00075             'nocolor',
00076             '(optional) Without colors',
00077             false, /*required*/
00078             false /*has arg*/
00079         );
00080         $this->addOption(
00081             'core-meta',
00082             '(optional) Allow export of specific MediaWiki core meta groups ' .
00083             '(translatewiki.net specific)',
00084             false, /*required*/
00085             false /*has arg*/
00086         );
00087     }
00088 
00089     public function execute() {
00090         $groupIds = explode( ',', trim( $this->getOption( 'group' ) ) );
00091         $groupIds = MessageGroups::expandWildcards( $groupIds );
00092         $groups = MessageGroups::getGroupsById( $groupIds );
00093 
00094         if ( !count( $groups ) ) {
00095             $this->error( "ESG2: No valid message groups identified.", 1 );
00096         }
00097 
00098         $start = $this->getOption( 'start' ) ? strtotime( $this->getOption( 'start' ) ) : false;
00099         $end = $this->getOption( 'end' ) ? strtotime( $this->getOption( 'end' ) ) : false;
00100 
00101         $this->output( "Conflict times: " . wfTimestamp( TS_ISO_8601, $start ) . " - " .
00102             wfTimestamp( TS_ISO_8601, $end ) . "\n" );
00103 
00104         $codes = array_filter( array_map( 'trim', explode( ',', $this->getOption( 'lang' ) ) ) );
00105 
00106         $supportedCodes = array_keys( TranslateUtils::getLanguageNames( 'en' ) );
00107         ksort( $supportedCodes );
00108 
00109         if ( $codes[0] === '*' ) {
00110             $codes = $supportedCodes;
00111         }
00112 
00113         $coreMeta = $this->hasOption( 'core-meta' );
00114 
00116         foreach ( $groups as $groupId => &$group ) {
00117             if ( $group->isMeta() ) {
00118                 if ( !$coreMeta ) {
00119                     $this->output( "Skipping meta message group $groupId.\n" );
00120                     continue;
00121                 }
00122 
00123                 // Special case for MediaWiki core branches with pattern "core-1*"
00124                 if ( strstr( $group->getId(), 'core-1', true ) !== '' ) {
00125                     $this->output( "Skipping meta message group $groupId.\n" );
00126                     continue;
00127                 }
00128             }
00129 
00130             $this->output( "{$group->getLabel()} ", $group );
00131 
00132             foreach ( $codes as $code ) {
00133                 // No sync possible for unsupported language codes.
00134                 if ( !in_array( $code, $supportedCodes ) ) {
00135                     $this->output( "Unsupported code " . $code . ": skipping.\n" );
00136                     continue;
00137                 }
00138 
00139                 if ( $group instanceof FileBasedMessageGroup ) {
00141                     $file = $group->getSourceFilePath( $code );
00142                 } else {
00144                     $file = $group->getMessageFileWithPath( $code );
00145                 }
00146 
00147                 if ( !$file ) {
00148                     continue;
00149                 }
00150 
00151                 if ( !file_exists( $file ) ) {
00152                     continue;
00153                 }
00154 
00155                 $cs = new ChangeSyncer( $group, $this );
00156                 $cs->setProgressCallback( array( $this, 'myOutput' ) );
00157                 $cs->interactive = !$this->hasOption( 'noask' );
00158                 $cs->nocolor = $this->hasOption( 'nocolor' );
00159                 $cs->norc = $this->hasOption( 'norc' );
00160 
00161                 # @todo FIXME: Make this auto detect.
00162                 # Guess last modified date of the file from either git, svn or filesystem
00163                 if ( $this->hasOption( 'git' ) ) {
00164                     $ts = $cs->getTimestampsFromGit( $file );
00165                 } else {
00166                     $ts = $cs->getTimestampsFromSvn( $file );
00167                 }
00168                 if ( !$ts ) {
00169                     $ts = $cs->getTimestampsFromFs( $file );
00170                 }
00171 
00172                 $this->output( "Modify time for $code: " . wfTimestamp( TS_ISO_8601, $ts ) . "\n" );
00173 
00174                 $cs->checkConflicts( $code, $start, $end, $ts );
00175             }
00176 
00177             unset( $group );
00178         }
00179         // Print timestamp if the user wants to store it
00180         $this->output( wfTimestamp( TS_RFC2822 ) . "\n" );
00181     }
00182 
00190     public function myOutput( $text, $channel = null, $error = false ) {
00191         if ( $error ) {
00192             $this->error( $text, $channel );
00193         } else {
00194             $this->output( $text, $channel );
00195         }
00196     }
00197 }
00198 
00202 class ChangeSyncer {
00204     protected $progressCallback;
00205 
00207     public $norc = false;
00208 
00210     public $interactive = true;
00211 
00213     public $nocolor = false;
00214 
00216     protected $group;
00217 
00222     public function __construct( MessageGroup $group ) {
00223         $this->group = $group;
00224     }
00225 
00226     public function setProgressCallback( $callback ) {
00227         $this->progressCallback = $callback;
00228     }
00229 
00231     protected function reportProgress( $text, $channel, $severity = 'status' ) {
00232         if ( is_callable( $this->progressCallback ) ) {
00233             $useErrorOutput = $severity === 'error';
00234             call_user_func( $this->progressCallback, $text, $channel, $useErrorOutput );
00235         }
00236     }
00237 
00238     // svn component from pecl doesn't seem to have this in quick sight
00244     public function getTimestampsFromSvn( $file ) {
00245         $file = escapeshellarg( $file );
00246         $retval = 0;
00247         $output = wfShellExec( "svn info $file 2>/dev/null", $retval );
00248 
00249         if ( $retval ) {
00250             return false;
00251         }
00252 
00253         $matches = array();
00254         // PHP doesn't allow foo || return false;
00255         // Thank
00256         // you
00257         // PHP (for being an ass)!
00258         $regex = '^Last Changed Date: (.*) \(';
00259         $ok = preg_match( "~$regex~m", $output, $matches );
00260         if ( $ok ) {
00261             return strtotime( $matches[1] );
00262         }
00263 
00264         return false;
00265     }
00266 
00272     public function getTimestampsFromGit( $file ) {
00273         $file = escapeshellarg( $file );
00274         $retval = 0;
00275         $output = wfShellExec( "git log -n 1 --format=%cd $file", $retval );
00276 
00277         if ( $retval ) {
00278             return false;
00279         }
00280 
00281         return strtotime( $output );
00282     }
00283 
00289     public function getTimestampsFromFs( $file ) {
00290         if ( !file_exists( $file ) ) {
00291             return false;
00292         }
00293 
00294         $stat = stat( $file );
00295 
00296         return $stat['mtime'];
00297     }
00298 
00308     public function checkConflicts( $code, $startTs = false, $endTs = false, $changeTs = false ) {
00309         $messages = $this->group->load( $code );
00310 
00311         if ( !count( $messages ) ) {
00312             return;
00313         }
00314 
00315         $collection = $this->group->initCollection( $code );
00316         $collection->filter( 'ignored' );
00317         $collection->loadTranslations();
00318 
00319         foreach ( $messages as $key => $translation ) {
00320             if ( !isset( $collection[$key] ) ) {
00321                 continue;
00322             }
00323 
00324             // @todo Temporary exception. Should be fixed elsewhere more generically.
00325             if ( $translation == '{{PLURAL:GETTEXT|}}' ) {
00326                 return;
00327             }
00328 
00329             $title = Title::makeTitleSafe( $this->group->getNamespace(), "$key/$code" );
00330 
00331             $page = $title->getPrefixedText();
00332 
00333             if ( $collection[$key]->translation() === null ) {
00334                 $this->reportProgress( "Importing $page as a new translation\n", 'importing' );
00335                 $this->import( $title, $translation, 'Importing a new translation' );
00336                 continue;
00337             }
00338 
00339             $current = str_replace( TRANSLATE_FUZZY, '', $collection[$key]->translation() );
00340             $translation = str_replace( TRANSLATE_FUZZY, '', $translation );
00341             if ( $translation === $current ) {
00342                 continue;
00343             }
00344 
00345             $this->reportProgress( "Conflict in " . $this->color( 'bold', $page ) . "!", $page );
00346 
00347             $iso = 'xnY-xnm-xnd"T"xnH:xni:xns';
00348             $lang = RequestContext::getMain()->getLanguage();
00349 
00350             // Finally all is ok, now lets start comparing timestamps
00351             // Make sure we are comparing timestamps in same format
00352             $wikiTs = $this->getLastGoodChange( $title, $startTs );
00353             if ( $wikiTs ) {
00354                 $wikiTs = wfTimestamp( TS_UNIX, $wikiTs );
00355                 $wikiDate = $lang->sprintfDate( $iso, wfTimestamp( TS_MW, $wikiTs ) );
00356             } else {
00357                 $wikiDate = 'Unknown';
00358             }
00359 
00360             if ( $startTs ) {
00361                 $startTs = wfTimestamp( TS_UNIX, $startTs );
00362             }
00363 
00364             if ( $endTs ) {
00365                 $endTs = wfTimestamp( TS_UNIX, $endTs );
00366             }
00367             if ( $changeTs ) {
00368                 $changeTs = wfTimestamp( TS_UNIX, $changeTs );
00369                 $changeDate = $lang->sprintfDate( $iso, wfTimestamp( TS_MW, $changeTs ) );
00370             } else {
00371                 $changeDate = 'Unknown';
00372             }
00373 
00374             if ( $changeTs ) {
00375                 if ( $wikiTs > $startTs && $changeTs <= $endTs ) {
00376                     $this->reportProgress( " →Changed in wiki after export: IGNORE", $page );
00377                     continue;
00378                 } elseif ( !$wikiTs || ( $changeTs > $endTs && $wikiTs < $startTs ) ) {
00379                     $this->reportProgress( " →Changed in source after export: IMPORT", $page );
00380                     $this->import(
00381                         $title,
00382                         $translation,
00383                         'Updating translation from external source'
00384                     );
00385                     continue;
00386                 }
00387             }
00388 
00389             if ( !$this->interactive ) {
00390                 continue;
00391             }
00392 
00393             $this->reportProgress( " →Needs manual resolution", $page );
00394             $this->reportProgress( "Source translation at $changeDate:", 'source' );
00395             $this->reportProgress( $this->color( 'blue', $translation ), 'source' );
00396             $this->reportProgress( "Wiki translation at $wikiDate:", 'translation' );
00397             $this->reportProgress( $this->color( 'green', $current ), 'translation' );
00398 
00399             do {
00400                 $this->reportProgress( "Resolution: [S]kip [I]mport [C]onflict: ", 'foo' );
00401                 // @todo Find an elegant way to use Maintenance::readconsole().
00402                 $action = fgets( STDIN );
00403                 $action = strtoupper( trim( $action ) );
00404 
00405                 if ( $action === 'S' ) {
00406                     break;
00407                 }
00408 
00409                 if ( $action === 'I' ) {
00410                     $this->import(
00411                         $title,
00412                         $translation,
00413                         'Updating translation from external source'
00414                     );
00415                     break;
00416                 }
00417 
00418                 if ( $action === 'C' ) {
00419                     $this->import(
00420                         $title,
00421                         TRANSLATE_FUZZY . $translation,
00422                         'Edit conflict between wiki and source'
00423                     );
00424                     break;
00425                 }
00426             } while ( true );
00427         }
00428     }
00429 
00436     public function color( $color, $text ) {
00437         switch ( $color ) {
00438             case 'blue':
00439                 return "\033[1;34m$text\033[0m";
00440             case 'green':
00441                 return "\033[1;32m$text\033[0m";
00442             case 'bold':
00443                 return "\033[1m$text\033[0m";
00444             default:
00445                 return $text;
00446         }
00447     }
00448 
00455     public function getLastGoodChange( $title, $startTs = false ) {
00456         global $wgTranslateFuzzyBotName;
00457 
00458         $wikiTs = false;
00459         $revision = Revision::newFromTitle( $title );
00460         while ( $revision ) {
00461             // No need to go back further
00462             if ( $startTs && $wikiTs && ( $wikiTs < $startTs ) ) {
00463                 break;
00464             }
00465 
00466             if ( $revision->getRawUserText() === $wgTranslateFuzzyBotName ) {
00467                 $revision = $revision->getPrevious();
00468                 continue;
00469             }
00470 
00471             $wikiTs = wfTimestamp( TS_UNIX, $revision->getTimestamp() );
00472             break;
00473         }
00474 
00475         return $wikiTs;
00476     }
00477 
00484     public function import( $title, $translation, $comment ) {
00485         $flags = EDIT_FORCE_BOT;
00486         if ( $this->norc ) {
00487             $flags |= EDIT_SUPPRESS_RC;
00488         }
00489 
00490         $wikipage = new WikiPage( $title );
00491         $this->reportProgress( "Importing {$title->getPrefixedText()}: ", $title );
00492         $status = $wikipage->doEdit(
00493             $translation, $comment, $flags, false, FuzzyBot::getUser()
00494         );
00495         $success = $status === true || ( is_object( $status ) && $status->isOK() );
00496         $this->reportProgress( $success ? 'OK' : 'FAILED', $title );
00497     }
00498 }
00499 
00500 $maintClass = 'SyncGroup';
00501 require_once RUN_MAINTENANCE_IF_MAIN;
Generated on Tue Oct 29 00:00:24 2013 for MediaWiki Translate Extension by  doxygen 1.6.3