00001 <?php
00047 class MessageChecker {
00048 protected $checks = array();
00049 protected $group = null;
00050 private static $globalBlacklist = null;
00051
00056 public function __construct( MessageGroup $group ) {
00057 global $wgTranslateCheckBlacklist;
00058
00059 if ( $wgTranslateCheckBlacklist === false ) {
00060 self::$globalBlacklist = array();
00061 } elseif ( self::$globalBlacklist === null ) {
00062 $file = $wgTranslateCheckBlacklist;
00063 $list = PHPVariableLoader::loadVariableFromPHPFile( $file, 'checkBlacklist' );
00064 $keys = array( 'group', 'check', 'subcheck', 'code', 'message' );
00065
00066 foreach ( $list as $key => $pattern ) {
00067 foreach ( $keys as $checkKey ) {
00068 if ( !isset( $pattern[$checkKey] ) ) {
00069 $list[$key][$checkKey] = '#';
00070 } elseif ( is_array( $pattern[$checkKey] ) ) {
00071 $list[$key][$checkKey] =
00072 array_map( array( $this, 'foldValue' ), $pattern[$checkKey] );
00073 } else {
00074 $list[$key][$checkKey] = $this->foldValue( $pattern[$checkKey] );
00075 }
00076 }
00077 }
00078
00079 self::$globalBlacklist = $list;
00080 }
00081
00082 $this->group = $group;
00083 }
00084
00090 protected function foldValue( $value ) {
00091 return str_replace( ' ', '_', strtolower( $value ) );
00092 }
00093
00098 public function setChecks( $checks ) {
00099 foreach ( $checks as $k => $c ) {
00100 if ( !is_callable( $c ) ) {
00101 unset( $checks[$k] );
00102 wfWarn( "Check function for check $k is not callable" );
00103 }
00104 }
00105 $this->checks = $checks;
00106 }
00107
00113 public function addCheck( $check ) {
00114 if ( is_callable( $check ) ) {
00115 $this->checks[] = $check;
00116 }
00117 }
00118
00127 public function checkMessage( TMessage $message, $code ) {
00128 $warningsArray = array();
00129 $messages = array( $message );
00130
00131 foreach ( $this->checks as $check ) {
00132 call_user_func_array( $check, array( $messages, $code, &$warningsArray ) );
00133 }
00134
00135 $warningsArray = $this->filterWarnings( $warningsArray );
00136 if ( !count( $warningsArray ) ) {
00137 return array();
00138 }
00139
00140 $warnings = $warningsArray[$message->key()];
00141 $warnings = $this->fixMessageParams( $warnings );
00142
00143 return $warnings;
00144 }
00145
00152 public function checkMessageFast( TMessage $message, $code ) {
00153 $warningsArray = array();
00154 $messages = array( $message );
00155
00156 foreach ( $this->checks as $check ) {
00157 call_user_func_array( $check, array( $messages, $code, &$warningsArray ) );
00158 if ( count( $warningsArray ) ) {
00159 return true;
00160 }
00161 }
00162
00163 return false;
00164 }
00165
00171 protected function filterWarnings( $warningsArray ) {
00172 $groupId = $this->group->getId();
00173
00174
00175 foreach ( $warningsArray as $mkey => $warnings ) {
00176
00177 foreach ( $warnings as $wkey => $warning ) {
00178 $check = array_shift( $warning );
00179
00180 foreach ( self::$globalBlacklist as $pattern ) {
00181 if ( !$this->match( $pattern['group'], $groupId ) ) {
00182 continue;
00183 }
00184 if ( !$this->match( $pattern['check'], $check[0] ) ) {
00185 continue;
00186 }
00187 if ( !$this->match( $pattern['subcheck'], $check[1] ) ) {
00188 continue;
00189 }
00190 if ( !$this->match( $pattern['message'], $check[2] ) ) {
00191 continue;
00192 }
00193 if ( !$this->match( $pattern['code'], $check[3] ) ) {
00194 continue;
00195 }
00196
00197
00198 unset( $warningsArray[$mkey][$wkey] );
00199 }
00200 }
00201 }
00202
00203 return $warningsArray;
00204 }
00205
00212 protected function match( $pattern, $value ) {
00213 if ( $pattern === '#' ) {
00214 return true;
00215 } elseif ( is_array( $pattern ) ) {
00216 return in_array( strtolower( $value ), $pattern, true );
00217 } else {
00218 return strtolower( $value ) === $pattern;
00219 }
00220 }
00221
00229 protected function fixMessageParams( $warnings ) {
00230 $lang = RequestContext::getMain()->getLanguage();
00231
00232 foreach ( $warnings as $wkey => $warning ) {
00233 array_shift( $warning );
00234 $message = array( array_shift( $warning ) );
00235
00236 foreach ( $warning as $param ) {
00237 if ( !is_array( $param ) ) {
00238 $message[] = $param;
00239 } else {
00240 list( $type, $value ) = $param;
00241 if ( $type === 'COUNT' ) {
00242 $message[] = $lang->formatNum( $value );
00243 } elseif ( $type === 'PARAMS' ) {
00244 $message[] = $lang->commaList( $value );
00245 } else {
00246 throw new MWException( "Unknown type $type" );
00247 }
00248 }
00249 }
00250 $warnings[$wkey] = $message;
00251 }
00252
00253 return $warnings;
00254 }
00255
00262 protected static function compareArrays( $defs, $trans ) {
00263 $missing = array();
00264
00265 foreach ( $defs as $defVar ) {
00266 if ( !in_array( $defVar, $trans ) ) {
00267 $missing[] = $defVar;
00268 }
00269 }
00270
00271 return $missing;
00272 }
00273
00281 protected function printfCheck( $messages, $code, array &$warnings ) {
00282 $this->parameterCheck( $messages, $code, $warnings, '/%(\d+\$)?[sduf]/U' );
00283 }
00284
00292 protected function rubyVariableCheck( $messages, $code, array &$warnings ) {
00293 $this->parameterCheck( $messages, $code, $warnings, '/%{[a-zA-Z_]+}/' );
00294 }
00295
00303 protected function pythonInterpolationCheck( $messages, $code, array &$warnings ) {
00304 $pattern = '/\%\([a-zA-Z0-9]*?\)[diouxXeEfFgGcrs]/U';
00305 $this->parameterCheck( $messages, $code, $warnings, $pattern );
00306 }
00307
00315 protected function braceBalanceCheck( $messages, $code, array &$warnings ) {
00316 foreach ( $messages as $message ) {
00317 $key = $message->key();
00318 $translation = $message->translation();
00319 $translation = preg_replace( '/[^{}[\]()]/u', '', $translation );
00320
00321 $subcheck = 'brace';
00322 $counts = array(
00323 '{' => 0, '}' => 0,
00324 '[' => 0, ']' => 0,
00325 '(' => 0, ')' => 0,
00326 );
00327
00328 $len = strlen( $translation );
00329 for ( $i = 0; $i < $len; $i++ ) {
00330 $char = $translation[$i];
00331 $counts[$char]++;
00332 }
00333
00334 $balance = array();
00335 if ( $counts['['] !== $counts[']'] ) {
00336 $balance[] = '[]: ' . ( $counts['['] - $counts[']'] );
00337 }
00338
00339 if ( $counts['{'] !== $counts['}'] ) {
00340 $balance[] = '{}: ' . ( $counts['{'] - $counts['}'] );
00341 }
00342
00343 if ( $counts['('] !== $counts[')'] ) {
00344 $balance[] = '(): ' . ( $counts['('] - $counts[')'] );
00345 }
00346
00347 if ( count( $balance ) ) {
00348 $warnings[$key][] = array(
00349 array( 'balance', $subcheck, $key, $code ),
00350 'translate-checks-balance',
00351 array( 'PARAMS', $balance ),
00352 array( 'COUNT', count( $balance ) ),
00353 );
00354 }
00355 }
00356 }
00357
00366 protected function parameterCheck( $messages, $code, array &$warnings, $pattern ) {
00367 foreach ( $messages as $message ) {
00368 $key = $message->key();
00369 $definition = $message->definition();
00370 $translation = $message->translation();
00371
00372 preg_match_all( $pattern, $definition, $defVars );
00373 preg_match_all( $pattern, $translation, $transVars );
00374
00375
00376 $subcheck = 'missing';
00377 $params = self::compareArrays( $defVars[0], $transVars[0] );
00378
00379 if ( count( $params ) ) {
00380 $warnings[$key][] = array(
00381 array( 'variable', $subcheck, $key, $code ),
00382 'translate-checks-parameters',
00383 array( 'PARAMS', $params ),
00384 array( 'COUNT', count( $params ) ),
00385 );
00386 }
00387
00388
00389 $subcheck = 'unknown';
00390 $params = self::compareArrays( $transVars[0], $defVars[0] );
00391
00392 if ( count( $params ) ) {
00393 $warnings[$key][] = array(
00394 array( 'variable', $subcheck, $key, $code ),
00395 'translate-checks-parameters-unknown',
00396 array( 'PARAMS', $params ),
00397 array( 'COUNT', count( $params ) ),
00398 );
00399 }
00400 }
00401 }
00402
00408 protected function balancedTagsCheck( $messages, $code, array &$warnings ) {
00409 foreach ( $messages as $message ) {
00410 $key = $message->key();
00411 $translation = $message->translation();
00412
00413 libxml_use_internal_errors( true );
00414 libxml_clear_errors();
00415 $doc = simplexml_load_string( Xml::tags( 'root', null, $translation ) );
00416 if ( $doc ) {
00417 continue;
00418 }
00419
00420 $errors = libxml_get_errors();
00421 $params = array();
00422 foreach ( $errors as $error ) {
00423 if ( $error->code !== 76 && $error->code !== 73 ) {
00424 continue;
00425 }
00426 $params[] = "<br />• [{$error->code}] $error->message";
00427 }
00428
00429 if ( !count( $params ) ) {
00430 continue;
00431 }
00432
00433 $warnings[$key][] = array(
00434 array( 'tags', 'balance', $key, $code ),
00435 'translate-checks-format',
00436 array( 'PARAMS', $params ),
00437 array( 'COUNT', count( $params ) ),
00438 );
00439 }
00440
00441 libxml_clear_errors();
00442 }
00443 }