PageRenderTime 41ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/extensions/LocalisationUpdate/LocalisationUpdate.class.php

https://github.com/ChuguluGames/mediawiki-svn
PHP | 670 lines | 408 code | 98 blank | 164 comment | 55 complexity | 297de3486381d3ecb4362d4e9fa4d1d9 MD5 | raw file
  1. <?php
  2. /**
  3. * Class for localization updates.
  4. *
  5. * TODO: refactor code to remove duplication
  6. */
  7. class LocalisationUpdate {
  8. private static $newHashes = null;
  9. private static $filecache = array();
  10. /**
  11. * LocalisationCacheRecache hook handler.
  12. *
  13. * @param $lc LocalisationCache
  14. * @param $langcode String
  15. * @param $cache Array
  16. *
  17. * @return true
  18. */
  19. public static function onRecache( LocalisationCache $lc, $langcode, array &$cache ) {
  20. $cache['messages'] = array_merge(
  21. $cache['messages'],
  22. self::readFile( $langcode )
  23. );
  24. $cache['deps'][] = new FileDependency(
  25. self::filename( $langcode )
  26. );
  27. return true;
  28. }
  29. /**
  30. * Called from the cronjob to fetch new messages from SVN.
  31. *
  32. * @param $options Array
  33. *
  34. * @return true
  35. */
  36. public static function updateMessages( array $options ) {
  37. global $wgLocalisationUpdateDirectory;
  38. $verbose = !isset( $options['quiet'] );
  39. $all = isset( $options['all'] );
  40. $skipCore = isset( $options['skip-core'] );
  41. $skipExtensions = isset( $options['skip-extensions'] );
  42. if( isset( $options['outdir'] ) ) {
  43. $wgLocalisationUpdateDirectory = $options['outdir'];
  44. }
  45. $result = 0;
  46. // Update all MW core messages.
  47. if( !$skipCore ) {
  48. $result = self::updateMediawikiMessages( $verbose );
  49. }
  50. // Update all Extension messages.
  51. if( !$skipExtensions ) {
  52. if( $all ) {
  53. global $IP;
  54. $extFiles = array();
  55. // Look in extensions/ for all available items...
  56. // TODO: add support for $wgExtensionAssetsPath
  57. $dirs = new RecursiveDirectoryIterator( "$IP/extensions/" );
  58. // I ain't kidding... RecursiveIteratorIterator.
  59. foreach( new RecursiveIteratorIterator( $dirs ) as $pathname => $item ) {
  60. $filename = basename( $pathname );
  61. $matches = array();
  62. if( preg_match( '/^(.*)\.i18n\.php$/', $filename, $matches ) ) {
  63. $group = $matches[1];
  64. $extFiles[$group] = $pathname;
  65. }
  66. }
  67. } else {
  68. global $wgExtensionMessagesFiles;
  69. $extFiles = $wgExtensionMessagesFiles;
  70. }
  71. foreach ( $extFiles as $extension => $locFile ) {
  72. $result += self::updateExtensionMessages( $locFile, $extension, $verbose );
  73. }
  74. }
  75. self::writeHashes();
  76. // And output the result!
  77. self::myLog( "Updated {$result} messages in total" );
  78. self::myLog( "Done" );
  79. return true;
  80. }
  81. /**
  82. * Update Extension Messages.
  83. *
  84. * @param $file String
  85. * @param $extension String
  86. * @param $verbose Boolean
  87. *
  88. * @return Integer: the amount of updated messages
  89. */
  90. public static function updateExtensionMessages( $file, $extension, $verbose ) {
  91. global $IP, $wgLocalisationUpdateSVNURL;
  92. $relfile = wfRelativePath( $file, "$IP/extensions" );
  93. // Create a full path.
  94. // TODO: add support for $wgExtensionAssetsPath
  95. $localfile = "$IP/extensions/$relfile";
  96. // Get the full SVN directory path.
  97. // TODO: add support for $wgExtensionAssetsPath
  98. $svnfile = "$wgLocalisationUpdateSVNURL/extensions/$relfile";
  99. // Compare the 2 files.
  100. $result = self::compareExtensionFiles( $extension, $svnfile, $file, $verbose, false, true );
  101. return $result;
  102. }
  103. /**
  104. * Update the MediaWiki Core Messages.
  105. *
  106. * @param $verbose Boolean
  107. *
  108. * @return Integer: the amount of updated messages
  109. */
  110. public static function updateMediawikiMessages( $verbose ) {
  111. global $IP, $wgLocalisationUpdateSVNURL;
  112. // Create an array which will later contain all the files that we want to try to update.
  113. $files = array();
  114. // The directory which contains the files.
  115. $dirname = "languages/messages";
  116. // Get the full path to the directory.
  117. $localdir = $IP . "/" . $dirname;
  118. // Get the full SVN Path.
  119. $svndir = "$wgLocalisationUpdateSVNURL/phase3/$dirname";
  120. // Open the directory.
  121. $dir = opendir( $localdir );
  122. while ( false !== ( $file = readdir( $dir ) ) ) {
  123. $m = array();
  124. // And save all the filenames of files containing messages
  125. if ( preg_match( '/Messages([A-Z][a-z_]+)\.php$/', $file, $m ) ) {
  126. if ( $m[1] != 'En' ) { // Except for the English one.
  127. $files[] = $file;
  128. }
  129. }
  130. }
  131. closedir( $dir );
  132. // Find the changed English strings (as these messages won't be updated in ANY language).
  133. $changedEnglishStrings = self::compareFiles( $localdir . '/MessagesEn.php', $svndir . '/MessagesEn.php', $verbose );
  134. // Count the changes.
  135. $changedCount = 0;
  136. // For each language.
  137. sort( $files );
  138. foreach ( $files as $file ) {
  139. $svnfile = $svndir . '/' . $file;
  140. $localfile = $localdir . '/' . $file;
  141. // Compare the files.
  142. $result = self::compareFiles( $svnfile, $localfile, $verbose, $changedEnglishStrings, false, true );
  143. // And update the change counter.
  144. $changedCount += count( $result );
  145. }
  146. // Log some nice info.
  147. self::myLog( "{$changedCount} MediaWiki messages are updated" );
  148. return $changedCount;
  149. }
  150. /**
  151. * Removes all unneeded content from a file and returns it.
  152. *
  153. * @param $contents String
  154. *
  155. * @return String
  156. */
  157. public static function cleanupFile( $contents ) {
  158. // We don't need any PHP tags.
  159. $contents = strtr( $contents,
  160. array(
  161. '<?php' => '',
  162. '?' . '>' => ''
  163. )
  164. );
  165. $results = array();
  166. // And we only want the messages array.
  167. preg_match( "/\\\$messages(.*\s)*?\);/", $contents, $results );
  168. // If there is any!
  169. if ( !empty( $results[0] ) ) {
  170. $contents = $results[0];
  171. } else {
  172. $contents = '';
  173. }
  174. // Windows vs Unix always stinks when comparing files.
  175. $contents = preg_replace( "/\\r\\n?/", "\n", $contents );
  176. // Return the cleaned up file.
  177. return $contents;
  178. }
  179. /**
  180. * Removes all unneeded content from a file and returns it.
  181. *
  182. * FIXME: duplicated cleanupFile code
  183. *
  184. * @param $contents String
  185. *
  186. * @return string
  187. */
  188. public static function cleanupExtensionFile( $contents ) {
  189. // We don't want PHP tags.
  190. $contents = preg_replace( "/<\?php/", "", $contents );
  191. $contents = preg_replace( "/\?" . ">/", "", $contents );
  192. $results = array();
  193. // And we only want message arrays.
  194. preg_match_all( "/\\\$messages(.*\s)*?\);/", $contents, $results );
  195. // But we want them all in one string.
  196. if( !empty( $results[0] ) && is_array( $results[0] ) ) {
  197. $contents = implode( "\n\n", $results[0] );
  198. } else {
  199. $contents = '';
  200. }
  201. // And we hate the windows vs linux linebreaks.
  202. $contents = preg_replace( "/\\\r\\\n?/", "\n", $contents );
  203. return $contents;
  204. }
  205. /**
  206. * Returns the contents of a file or false on failiure.
  207. *
  208. * @param $basefile String
  209. *
  210. * @return string or false
  211. */
  212. public static function getFileContents( $basefile ) {
  213. global $wgLocalisationUpdateRetryAttempts;
  214. $attempts = 0;
  215. $basefilecontents = '';
  216. // Use cURL to get the SVN contents.
  217. if ( preg_match( "/^http/", $basefile ) ) {
  218. while( !$basefilecontents && $attempts <= $wgLocalisationUpdateRetryAttempts ) {
  219. if( $attempts > 0 ) {
  220. $delay = 1;
  221. self::myLog( 'Failed to download ' . $basefile . "; retrying in ${delay}s..." );
  222. sleep( $delay );
  223. }
  224. $basefilecontents = Http::get( $basefile );
  225. $attempts++;
  226. }
  227. if ( !$basefilecontents ) {
  228. self::myLog( 'Cannot get the contents of ' . $basefile . ' (curl)' );
  229. return false;
  230. }
  231. } else {// otherwise try file_get_contents
  232. if ( !( $basefilecontents = file_get_contents( $basefile ) ) ) {
  233. self::myLog( 'Cannot get the contents of ' . $basefile );
  234. return false;
  235. }
  236. }
  237. return $basefilecontents;
  238. }
  239. /**
  240. * Returns an array containing the differences between the files.
  241. *
  242. * @param $basefile String
  243. * @param $comparefile String
  244. * @param $verbose Boolean
  245. * @param $forbiddenKeys Array
  246. * @param $alwaysGetResult Boolean
  247. * @param $saveResults Boolean
  248. *
  249. * @return array
  250. */
  251. public static function compareFiles( $basefile, $comparefile, $verbose, array $forbiddenKeys = array(), $alwaysGetResult = true, $saveResults = false ) {
  252. // Get the languagecode.
  253. $langcode = Language::getCodeFromFileName( $basefile, 'Messages' );
  254. $basefilecontents = self::getFileContents( $basefile );
  255. if ( $basefilecontents === false || $basefilecontents === '' ) {
  256. return array(); // Failed
  257. }
  258. // Only get the part we need.
  259. $basefilecontents = self::cleanupFile( $basefilecontents );
  260. // Change the variable name.
  261. $basefilecontents = preg_replace( "/\\\$messages/", "\$base_messages", $basefilecontents );
  262. $basehash = md5( $basefilecontents );
  263. // Check if the file has changed since our last update.
  264. if ( !$alwaysGetResult ) {
  265. if ( !self::checkHash( $basefile, $basehash ) ) {
  266. self::myLog( "Skipping {$langcode} since the remote file hasn't changed since our last update", $verbose );
  267. return array();
  268. }
  269. }
  270. // Get the array with messages.
  271. $base_messages = self::parsePHP( $basefilecontents, 'base_messages' );
  272. $comparefilecontents = self::getFileContents( $comparefile );
  273. if ( $comparefilecontents === false || $comparefilecontents === '' ) {
  274. return array(); // Failed
  275. }
  276. // Only get the stuff we need.
  277. $comparefilecontents = self::cleanupFile( $comparefilecontents );
  278. // Rename the array.
  279. $comparefilecontents = preg_replace( "/\\\$messages/", "\$compare_messages", $comparefilecontents );
  280. $comparehash = md5( $comparefilecontents );
  281. // If this is the remote file check if the file has changed since our last update.
  282. if ( preg_match( "/^http/", $comparefile ) && !$alwaysGetResult ) {
  283. if ( !self::checkHash( $comparefile, $comparehash ) ) {
  284. self::myLog( "Skipping {$langcode} since the remote file has not changed since our last update", $verbose );
  285. return array();
  286. }
  287. }
  288. // Get the array.
  289. $compare_messages = self::parsePHP( $comparefilecontents, 'compare_messages' );
  290. // If the localfile and the remote file are the same, skip them!
  291. if ( $basehash == $comparehash && !$alwaysGetResult ) {
  292. self::myLog( "Skipping {$langcode} since the remote file is the same as the local file", $verbose );
  293. return array();
  294. }
  295. // Add the messages we got with our previous update(s) to the local array (as we already got these as well).
  296. $compare_messages = array_merge(
  297. $compare_messages,
  298. self::readFile( $langcode )
  299. );
  300. // Compare the remote and local message arrays.
  301. $changedStrings = array_diff_assoc( $base_messages, $compare_messages );
  302. // If we want to save the differences.
  303. if ( $saveResults && !empty( $changedStrings ) && is_array( $changedStrings ) ) {
  304. self::myLog( "--Checking languagecode {$langcode}--", $verbose );
  305. // Save the differences.
  306. $updates = self::saveChanges( $changedStrings, $forbiddenKeys, $compare_messages, $base_messages, $langcode, $verbose );
  307. self::myLog( "{$updates} messages updated for {$langcode}.", $verbose );
  308. } elseif ( $saveResults ) {
  309. self::myLog( "--{$langcode} hasn't changed--", $verbose );
  310. }
  311. self::saveHash( $basefile, $basehash );
  312. self::saveHash( $comparefile, $comparehash );
  313. return $changedStrings;
  314. }
  315. /**
  316. * Checks whether a messages file has a certain hash.
  317. *
  318. * TODO: Swap return values, this is insane
  319. *
  320. * @param $file string Filename
  321. * @param $hash string Hash
  322. *
  323. * @return bool True if $file does NOT have hash $hash, false if it does
  324. */
  325. public static function checkHash( $file, $hash ) {
  326. $hashes = self::readFile( 'hashes' );
  327. return @$hashes[$file] !== $hash;
  328. }
  329. public static function saveHash( $file, $hash ) {
  330. if ( is_null( self::$newHashes ) ) {
  331. self::$newHashes = self::readFile( 'hashes' );
  332. }
  333. self::$newHashes[$file] = $hash;
  334. }
  335. public static function writeHashes() {
  336. self::writeFile( 'hashes', self::$newHashes );
  337. }
  338. /**
  339. *
  340. *
  341. * @param $changedStrings Array
  342. * @param $forbiddenKeys Array
  343. * @param $compare_messages Array
  344. * @param $base_messages Array
  345. * @param $langcode String
  346. * @param $verbose Boolean
  347. *
  348. * @return Integer: the amount of updated messages
  349. */
  350. public static function saveChanges( $changedStrings, array $forbiddenKeys, array $compare_messages, array $base_messages, $langcode, $verbose ) {
  351. // Count the updates.
  352. $updates = 0;
  353. if( !is_array( $changedStrings ) ) {
  354. self::myLog("CRITICAL ERROR: \$changedStrings is not an array in file " . (__FILE__) . ' at line ' .( __LINE__ ) );
  355. return 0;
  356. }
  357. $new_messages = self::readFile( $langcode );
  358. foreach ( $changedStrings as $key => $value ) {
  359. // If this message wasn't changed in English.
  360. if ( !isset( $forbiddenKeys[$key] ) ) {
  361. $new_messages[$key] = $base_messages[$key];
  362. // Output extra logmessages when needed.
  363. if ( $verbose ) {
  364. $oldmsg = isset( $compare_messages[$key] ) ? "'{$compare_messages[$key]}'" : 'not set';
  365. self::myLog( "Updated message {$key} from $oldmsg to '{$base_messages[$key]}'", $verbose );
  366. }
  367. // Update the counter.
  368. $updates++;
  369. }
  370. }
  371. self::writeFile( $langcode, $new_messages );
  372. return $updates;
  373. }
  374. /**
  375. *
  376. * @param $extension String
  377. * @param $basefile String
  378. * @param $comparefile String
  379. * @param $verbose Boolean
  380. * @param $alwaysGetResult Boolean
  381. * @param $saveResults Boolean
  382. *
  383. * @return Integer: the amount of updated messages
  384. */
  385. public static function compareExtensionFiles( $extension, $basefile, $comparefile, $verbose, $alwaysGetResult = true, $saveResults = false ) {
  386. // FIXME: Factor out duplicated code?
  387. $basefilecontents = self::getFileContents( $basefile );
  388. if ( $basefilecontents === false || $basefilecontents === '' ) {
  389. return 0; // Failed
  390. }
  391. // Cleanup the file where needed.
  392. $basefilecontents = self::cleanupExtensionFile( $basefilecontents );
  393. // Rename the arrays.
  394. $basefilecontents = preg_replace( "/\\\$messages/", "\$base_messages", $basefilecontents );
  395. $basehash = md5( $basefilecontents );
  396. // If this is the remote file
  397. if ( preg_match( "/^http/", $basefile ) && !$alwaysGetResult ) {
  398. // Check if the hash has changed
  399. if ( !self::checkHash( $basefile, $basehash ) ) {
  400. self::myLog( "Skipping {$extension} since the remote file has not changed since our last update", $verbose );
  401. return 0;
  402. }
  403. }
  404. // And get the real contents
  405. $base_messages = self::parsePHP( $basefilecontents, 'base_messages' );
  406. $comparefilecontents = self::getFileContents( $comparefile );
  407. if ( $comparefilecontents === false || $comparefilecontents === '' ) {
  408. return 0; // Failed
  409. }
  410. // Only get what we need.
  411. $comparefilecontents = self::cleanupExtensionFile( $comparefilecontents );
  412. // Rename the array.
  413. $comparefilecontents = preg_replace( "/\\\$messages/", "\$compare_messages", $comparefilecontents );
  414. $comparehash = md5( $comparefilecontents );
  415. if ( preg_match( "/^http/", $comparefile ) && !$alwaysGetResult ) {
  416. // Check if the remote file has changed
  417. if ( !self::checkHash( $comparefile, $comparehash ) ) {
  418. self::myLog( "Skipping {$extension} since the remote file has not changed since our last update", $verbose );
  419. return 0;
  420. }
  421. }
  422. // Get the real array.
  423. $compare_messages = self::parsePHP( $comparefilecontents, 'compare_messages' );
  424. // If both files are the same, they can be skipped.
  425. if ( $basehash == $comparehash && !$alwaysGetResult ) {
  426. self::myLog( "Skipping {$extension} since the remote file is the same as the local file", $verbose );
  427. return 0;
  428. }
  429. // Update counter.
  430. $updates = 0;
  431. if ( !is_array( $base_messages ) ) {
  432. $base_messages = array();
  433. }
  434. if ( empty( $base_messages['en'] ) ) {
  435. $base_messages['en'] = array();
  436. }
  437. if ( !is_array( $compare_messages ) ) {
  438. $compare_messages = array();
  439. }
  440. if ( empty( $compare_messages['en'] ) ) {
  441. $compare_messages['en'] = array();
  442. }
  443. // Find the changed english strings.
  444. $forbiddenKeys = array_diff_assoc( $base_messages['en'], $compare_messages['en'] );
  445. // Do an update for each language.
  446. foreach ( $base_messages as $language => $messages ) {
  447. if ( $language == 'en' ) { // Skip english.
  448. continue;
  449. }
  450. if ( !isset( $compare_messages[$language] ) ) {
  451. $compare_messages[$language] = array();
  452. }
  453. // Add the already known messages to the array so we will only find new changes.
  454. $compare_messages[$language] = array_merge(
  455. $compare_messages[$language],
  456. self::readFile( $language )
  457. );
  458. if ( empty( $compare_messages[$language] ) || !is_array( $compare_messages[$language] ) ) {
  459. $compare_messages[$language] = array();
  460. }
  461. // Get the array of changed strings.
  462. $changedStrings = array_diff_assoc( $messages, $compare_messages[$language] );
  463. // If we want to save the changes.
  464. if ( $saveResults === true && !empty( $changedStrings ) && is_array( $changedStrings ) ) {
  465. self::myLog( "--Checking languagecode {$language}--", $verbose );
  466. // The save them
  467. $updates = self::saveChanges( $changedStrings, $forbiddenKeys, $compare_messages[$language], $messages, $language, $verbose );
  468. self::myLog( "{$updates} messages updated for {$language}.", $verbose );
  469. } elseif($saveResults === true) {
  470. self::myLog( "--{$language} hasn't changed--", $verbose );
  471. }
  472. }
  473. // And log some stuff.
  474. self::myLog( "Updated " . $updates . " messages for the '{$extension}' extension", $verbose );
  475. self::saveHash( $basefile, $basehash );
  476. self::saveHash( $comparefile, $comparehash );
  477. return $updates;
  478. }
  479. /**
  480. * Logs a message.
  481. *
  482. * @param $log String
  483. */
  484. public static function myLog( $log, $verbose = true ) {
  485. if ( !$verbose ) {
  486. return;
  487. }
  488. if ( isset( $_SERVER ) && array_key_exists( 'REQUEST_METHOD', $_SERVER ) ) {
  489. wfDebug( $log . "\n" );
  490. } else {
  491. print( $log . "\n" );
  492. }
  493. }
  494. /**
  495. * @param $php
  496. * @param $varname
  497. * @return bool|array
  498. */
  499. public static function parsePHP( $php, $varname ) {
  500. try {
  501. $reader = new QuickArrayReader("<?php $php");
  502. return $reader->getVar( $varname );
  503. } catch( Exception $e ) {
  504. self::myLog( "Failed to read file: " . $e );
  505. return false;
  506. }
  507. }
  508. public static function filename( $lang ) {
  509. global $wgLocalisationUpdateDirectory, $wgCacheDirectory;
  510. $dir = $wgLocalisationUpdateDirectory ?
  511. $wgLocalisationUpdateDirectory :
  512. $wgCacheDirectory;
  513. if ( !$dir ) {
  514. throw new MWException( 'No cache directory configured' );
  515. }
  516. return "$dir/l10nupdate-$lang.cache";
  517. }
  518. public static function readFile( $lang ) {
  519. if ( !isset( self::$filecache[$lang] ) ) {
  520. $file = self::filename( $lang );
  521. $contents = @file_get_contents( $file );
  522. if ( $contents === false ) {
  523. wfDebug( "Failed to read file '$file'\n" );
  524. $retval = array();
  525. } else {
  526. $retval = unserialize( $contents );
  527. if ( $retval === false ) {
  528. wfDebug( "Corrupted data in file '$file'\n" );
  529. $retval = array();
  530. }
  531. }
  532. self::$filecache[$lang] = $retval;
  533. }
  534. return self::$filecache[$lang];
  535. }
  536. public static function writeFile( $lang, $var ) {
  537. $file = self::filename( $lang );
  538. if ( !@file_put_contents( $file, serialize( $var ) ) ) {
  539. throw new MWException( "Failed to write to file '$file'" );
  540. }
  541. self::$filecache[$lang] = $var;
  542. }
  543. }