PageRenderTime 63ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/includes/LocalisationCache.php

https://bitbucket.org/brunodefraine/mediawiki
PHP | 1167 lines | 672 code | 148 blank | 347 comment | 119 complexity | 5125e04e80ff38bcb205a4978361818f MD5 | raw file
Possible License(s): GPL-2.0, Apache-2.0, LGPL-3.0
  1. <?php
  2. define( 'MW_LC_VERSION', 2 );
  3. /**
  4. * Class for caching the contents of localisation files, Messages*.php
  5. * and *.i18n.php.
  6. *
  7. * An instance of this class is available using Language::getLocalisationCache().
  8. *
  9. * The values retrieved from here are merged, containing items from extension
  10. * files, core messages files and the language fallback sequence (e.g. zh-cn ->
  11. * zh-hans -> en ). Some common errors are corrected, for example namespace
  12. * names with spaces instead of underscores, but heavyweight processing, such
  13. * as grammatical transformation, is done by the caller.
  14. */
  15. class LocalisationCache {
  16. /** Configuration associative array */
  17. var $conf;
  18. /**
  19. * True if recaching should only be done on an explicit call to recache().
  20. * Setting this reduces the overhead of cache freshness checking, which
  21. * requires doing a stat() for every extension i18n file.
  22. */
  23. var $manualRecache = false;
  24. /**
  25. * True to treat all files as expired until they are regenerated by this object.
  26. */
  27. var $forceRecache = false;
  28. /**
  29. * The cache data. 3-d array, where the first key is the language code,
  30. * the second key is the item key e.g. 'messages', and the third key is
  31. * an item specific subkey index. Some items are not arrays and so for those
  32. * items, there are no subkeys.
  33. */
  34. var $data = array();
  35. /**
  36. * The persistent store object. An instance of LCStore.
  37. *
  38. * @var LCStore
  39. */
  40. var $store;
  41. /**
  42. * A 2-d associative array, code/key, where presence indicates that the item
  43. * is loaded. Value arbitrary.
  44. *
  45. * For split items, if set, this indicates that all of the subitems have been
  46. * loaded.
  47. */
  48. var $loadedItems = array();
  49. /**
  50. * A 3-d associative array, code/key/subkey, where presence indicates that
  51. * the subitem is loaded. Only used for the split items, i.e. messages.
  52. */
  53. var $loadedSubitems = array();
  54. /**
  55. * An array where presence of a key indicates that that language has been
  56. * initialised. Initialisation includes checking for cache expiry and doing
  57. * any necessary updates.
  58. */
  59. var $initialisedLangs = array();
  60. /**
  61. * An array mapping non-existent pseudo-languages to fallback languages. This
  62. * is filled by initShallowFallback() when data is requested from a language
  63. * that lacks a Messages*.php file.
  64. */
  65. var $shallowFallbacks = array();
  66. /**
  67. * An array where the keys are codes that have been recached by this instance.
  68. */
  69. var $recachedLangs = array();
  70. /**
  71. * All item keys
  72. */
  73. static public $allKeys = array(
  74. 'fallback', 'namespaceNames', 'bookstoreList',
  75. 'magicWords', 'messages', 'rtl', 'capitalizeAllNouns', 'digitTransformTable',
  76. 'separatorTransformTable', 'fallback8bitEncoding', 'linkPrefixExtension',
  77. 'linkTrail', 'namespaceAliases',
  78. 'dateFormats', 'datePreferences', 'datePreferenceMigrationMap',
  79. 'defaultDateFormat', 'extraUserToggles', 'specialPageAliases',
  80. 'imageFiles', 'preloadedMessages', 'namespaceGenderAliases',
  81. 'digitGroupingPattern'
  82. );
  83. /**
  84. * Keys for items which consist of associative arrays, which may be merged
  85. * by a fallback sequence.
  86. */
  87. static public $mergeableMapKeys = array( 'messages', 'namespaceNames',
  88. 'dateFormats', 'imageFiles', 'preloadedMessages',
  89. );
  90. /**
  91. * Keys for items which are a numbered array.
  92. */
  93. static public $mergeableListKeys = array( 'extraUserToggles' );
  94. /**
  95. * Keys for items which contain an array of arrays of equivalent aliases
  96. * for each subitem. The aliases may be merged by a fallback sequence.
  97. */
  98. static public $mergeableAliasListKeys = array( 'specialPageAliases' );
  99. /**
  100. * Keys for items which contain an associative array, and may be merged if
  101. * the primary value contains the special array key "inherit". That array
  102. * key is removed after the first merge.
  103. */
  104. static public $optionalMergeKeys = array( 'bookstoreList' );
  105. /**
  106. * Keys for items that are formatted like $magicWords
  107. */
  108. static public $magicWordKeys = array( 'magicWords' );
  109. /**
  110. * Keys for items where the subitems are stored in the backend separately.
  111. */
  112. static public $splitKeys = array( 'messages' );
  113. /**
  114. * Keys which are loaded automatically by initLanguage()
  115. */
  116. static public $preloadedKeys = array( 'dateFormats', 'namespaceNames' );
  117. var $mergeableKeys = null;
  118. /**
  119. * Constructor.
  120. * For constructor parameters, see the documentation in DefaultSettings.php
  121. * for $wgLocalisationCacheConf.
  122. *
  123. * @param $conf Array
  124. */
  125. function __construct( $conf ) {
  126. global $wgCacheDirectory;
  127. $this->conf = $conf;
  128. $storeConf = array();
  129. if ( !empty( $conf['storeClass'] ) ) {
  130. $storeClass = $conf['storeClass'];
  131. } else {
  132. switch ( $conf['store'] ) {
  133. case 'files':
  134. case 'file':
  135. $storeClass = 'LCStore_CDB';
  136. break;
  137. case 'db':
  138. $storeClass = 'LCStore_DB';
  139. break;
  140. case 'accel':
  141. $storeClass = 'LCStore_Accel';
  142. break;
  143. case 'detect':
  144. $storeClass = $wgCacheDirectory ? 'LCStore_CDB' : 'LCStore_DB';
  145. break;
  146. default:
  147. throw new MWException(
  148. 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.' );
  149. }
  150. }
  151. wfDebug( get_class( $this ) . ": using store $storeClass\n" );
  152. if ( !empty( $conf['storeDirectory'] ) ) {
  153. $storeConf['directory'] = $conf['storeDirectory'];
  154. }
  155. $this->store = new $storeClass( $storeConf );
  156. foreach ( array( 'manualRecache', 'forceRecache' ) as $var ) {
  157. if ( isset( $conf[$var] ) ) {
  158. $this->$var = $conf[$var];
  159. }
  160. }
  161. }
  162. /**
  163. * Returns true if the given key is mergeable, that is, if it is an associative
  164. * array which can be merged through a fallback sequence.
  165. * @param $key
  166. * @return bool
  167. */
  168. public function isMergeableKey( $key ) {
  169. if ( $this->mergeableKeys === null ) {
  170. $this->mergeableKeys = array_flip( array_merge(
  171. self::$mergeableMapKeys,
  172. self::$mergeableListKeys,
  173. self::$mergeableAliasListKeys,
  174. self::$optionalMergeKeys,
  175. self::$magicWordKeys
  176. ) );
  177. }
  178. return isset( $this->mergeableKeys[$key] );
  179. }
  180. /**
  181. * Get a cache item.
  182. *
  183. * Warning: this may be slow for split items (messages), since it will
  184. * need to fetch all of the subitems from the cache individually.
  185. * @param $code
  186. * @param $key
  187. * @return mixed
  188. */
  189. public function getItem( $code, $key ) {
  190. if ( !isset( $this->loadedItems[$code][$key] ) ) {
  191. wfProfileIn( __METHOD__.'-load' );
  192. $this->loadItem( $code, $key );
  193. wfProfileOut( __METHOD__.'-load' );
  194. }
  195. if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
  196. return $this->shallowFallbacks[$code];
  197. }
  198. return $this->data[$code][$key];
  199. }
  200. /**
  201. * Get a subitem, for instance a single message for a given language.
  202. * @param $code
  203. * @param $key
  204. * @param $subkey
  205. * @return null
  206. */
  207. public function getSubitem( $code, $key, $subkey ) {
  208. if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
  209. !isset( $this->loadedItems[$code][$key] ) ) {
  210. wfProfileIn( __METHOD__.'-load' );
  211. $this->loadSubitem( $code, $key, $subkey );
  212. wfProfileOut( __METHOD__.'-load' );
  213. }
  214. if ( isset( $this->data[$code][$key][$subkey] ) ) {
  215. return $this->data[$code][$key][$subkey];
  216. } else {
  217. return null;
  218. }
  219. }
  220. /**
  221. * Get the list of subitem keys for a given item.
  222. *
  223. * This is faster than array_keys($lc->getItem(...)) for the items listed in
  224. * self::$splitKeys.
  225. *
  226. * Will return null if the item is not found, or false if the item is not an
  227. * array.
  228. * @param $code
  229. * @param $key
  230. * @return bool|null|string
  231. */
  232. public function getSubitemList( $code, $key ) {
  233. if ( in_array( $key, self::$splitKeys ) ) {
  234. return $this->getSubitem( $code, 'list', $key );
  235. } else {
  236. $item = $this->getItem( $code, $key );
  237. if ( is_array( $item ) ) {
  238. return array_keys( $item );
  239. } else {
  240. return false;
  241. }
  242. }
  243. }
  244. /**
  245. * Load an item into the cache.
  246. * @param $code
  247. * @param $key
  248. */
  249. protected function loadItem( $code, $key ) {
  250. if ( !isset( $this->initialisedLangs[$code] ) ) {
  251. $this->initLanguage( $code );
  252. }
  253. // Check to see if initLanguage() loaded it for us
  254. if ( isset( $this->loadedItems[$code][$key] ) ) {
  255. return;
  256. }
  257. if ( isset( $this->shallowFallbacks[$code] ) ) {
  258. $this->loadItem( $this->shallowFallbacks[$code], $key );
  259. return;
  260. }
  261. if ( in_array( $key, self::$splitKeys ) ) {
  262. $subkeyList = $this->getSubitem( $code, 'list', $key );
  263. foreach ( $subkeyList as $subkey ) {
  264. if ( isset( $this->data[$code][$key][$subkey] ) ) {
  265. continue;
  266. }
  267. $this->data[$code][$key][$subkey] = $this->getSubitem( $code, $key, $subkey );
  268. }
  269. } else {
  270. $this->data[$code][$key] = $this->store->get( $code, $key );
  271. }
  272. $this->loadedItems[$code][$key] = true;
  273. }
  274. /**
  275. * Load a subitem into the cache
  276. * @param $code
  277. * @param $key
  278. * @param $subkey
  279. * @return
  280. */
  281. protected function loadSubitem( $code, $key, $subkey ) {
  282. if ( !in_array( $key, self::$splitKeys ) ) {
  283. $this->loadItem( $code, $key );
  284. return;
  285. }
  286. if ( !isset( $this->initialisedLangs[$code] ) ) {
  287. $this->initLanguage( $code );
  288. }
  289. // Check to see if initLanguage() loaded it for us
  290. if ( isset( $this->loadedItems[$code][$key] ) ||
  291. isset( $this->loadedSubitems[$code][$key][$subkey] ) ) {
  292. return;
  293. }
  294. if ( isset( $this->shallowFallbacks[$code] ) ) {
  295. $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
  296. return;
  297. }
  298. $value = $this->store->get( $code, "$key:$subkey" );
  299. $this->data[$code][$key][$subkey] = $value;
  300. $this->loadedSubitems[$code][$key][$subkey] = true;
  301. }
  302. /**
  303. * Returns true if the cache identified by $code is missing or expired.
  304. */
  305. public function isExpired( $code ) {
  306. if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) {
  307. wfDebug( __METHOD__."($code): forced reload\n" );
  308. return true;
  309. }
  310. $deps = $this->store->get( $code, 'deps' );
  311. $keys = $this->store->get( $code, 'list', 'messages' );
  312. $preload = $this->store->get( $code, 'preload' );
  313. // Different keys may expire separately, at least in LCStore_Accel
  314. if ( $deps === null || $keys === null || $preload === null ) {
  315. wfDebug( __METHOD__."($code): cache missing, need to make one\n" );
  316. return true;
  317. }
  318. foreach ( $deps as $dep ) {
  319. // Because we're unserializing stuff from cache, we
  320. // could receive objects of classes that don't exist
  321. // anymore (e.g. uninstalled extensions)
  322. // When this happens, always expire the cache
  323. if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
  324. wfDebug( __METHOD__."($code): cache for $code expired due to " .
  325. get_class( $dep ) . "\n" );
  326. return true;
  327. }
  328. }
  329. return false;
  330. }
  331. /**
  332. * Initialise a language in this object. Rebuild the cache if necessary.
  333. * @param $code
  334. */
  335. protected function initLanguage( $code ) {
  336. if ( isset( $this->initialisedLangs[$code] ) ) {
  337. return;
  338. }
  339. $this->initialisedLangs[$code] = true;
  340. # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
  341. if ( !Language::isValidBuiltInCode( $code ) ) {
  342. $this->initShallowFallback( $code, 'en' );
  343. return;
  344. }
  345. # Recache the data if necessary
  346. if ( !$this->manualRecache && $this->isExpired( $code ) ) {
  347. if ( file_exists( Language::getMessagesFileName( $code ) ) ) {
  348. $this->recache( $code );
  349. } elseif ( $code === 'en' ) {
  350. throw new MWException( 'MessagesEn.php is missing.' );
  351. } else {
  352. $this->initShallowFallback( $code, 'en' );
  353. }
  354. return;
  355. }
  356. # Preload some stuff
  357. $preload = $this->getItem( $code, 'preload' );
  358. if ( $preload === null ) {
  359. if ( $this->manualRecache ) {
  360. // No Messages*.php file. Do shallow fallback to en.
  361. if ( $code === 'en' ) {
  362. throw new MWException( 'No localisation cache found for English. ' .
  363. 'Please run maintenance/rebuildLocalisationCache.php.' );
  364. }
  365. $this->initShallowFallback( $code, 'en' );
  366. return;
  367. } else {
  368. throw new MWException( 'Invalid or missing localisation cache.' );
  369. }
  370. }
  371. $this->data[$code] = $preload;
  372. foreach ( $preload as $key => $item ) {
  373. if ( in_array( $key, self::$splitKeys ) ) {
  374. foreach ( $item as $subkey => $subitem ) {
  375. $this->loadedSubitems[$code][$key][$subkey] = true;
  376. }
  377. } else {
  378. $this->loadedItems[$code][$key] = true;
  379. }
  380. }
  381. }
  382. /**
  383. * Create a fallback from one language to another, without creating a
  384. * complete persistent cache.
  385. * @param $primaryCode
  386. * @param $fallbackCode
  387. */
  388. public function initShallowFallback( $primaryCode, $fallbackCode ) {
  389. $this->data[$primaryCode] =& $this->data[$fallbackCode];
  390. $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
  391. $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
  392. $this->shallowFallbacks[$primaryCode] = $fallbackCode;
  393. }
  394. /**
  395. * Read a PHP file containing localisation data.
  396. * @param $_fileName
  397. * @param $_fileType
  398. * @return array
  399. */
  400. protected function readPHPFile( $_fileName, $_fileType ) {
  401. // Disable APC caching
  402. $_apcEnabled = ini_set( 'apc.cache_by_default', '0' );
  403. include( $_fileName );
  404. ini_set( 'apc.cache_by_default', $_apcEnabled );
  405. if ( $_fileType == 'core' || $_fileType == 'extension' ) {
  406. $data = compact( self::$allKeys );
  407. } elseif ( $_fileType == 'aliases' ) {
  408. $data = compact( 'aliases' );
  409. } else {
  410. throw new MWException( __METHOD__.": Invalid file type: $_fileType" );
  411. }
  412. return $data;
  413. }
  414. /**
  415. * Merge two localisation values, a primary and a fallback, overwriting the
  416. * primary value in place.
  417. * @param $key
  418. * @param $value
  419. * @param $fallbackValue
  420. */
  421. protected function mergeItem( $key, &$value, $fallbackValue ) {
  422. if ( !is_null( $value ) ) {
  423. if ( !is_null( $fallbackValue ) ) {
  424. if ( in_array( $key, self::$mergeableMapKeys ) ) {
  425. $value = $value + $fallbackValue;
  426. } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
  427. $value = array_unique( array_merge( $fallbackValue, $value ) );
  428. } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
  429. $value = array_merge_recursive( $value, $fallbackValue );
  430. } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
  431. if ( !empty( $value['inherit'] ) ) {
  432. $value = array_merge( $fallbackValue, $value );
  433. }
  434. if ( isset( $value['inherit'] ) ) {
  435. unset( $value['inherit'] );
  436. }
  437. } elseif ( in_array( $key, self::$magicWordKeys ) ) {
  438. $this->mergeMagicWords( $value, $fallbackValue );
  439. }
  440. }
  441. } else {
  442. $value = $fallbackValue;
  443. }
  444. }
  445. /**
  446. * @param $value
  447. * @param $fallbackValue
  448. */
  449. protected function mergeMagicWords( &$value, $fallbackValue ) {
  450. foreach ( $fallbackValue as $magicName => $fallbackInfo ) {
  451. if ( !isset( $value[$magicName] ) ) {
  452. $value[$magicName] = $fallbackInfo;
  453. } else {
  454. $oldSynonyms = array_slice( $fallbackInfo, 1 );
  455. $newSynonyms = array_slice( $value[$magicName], 1 );
  456. $synonyms = array_values( array_unique( array_merge(
  457. $newSynonyms, $oldSynonyms ) ) );
  458. $value[$magicName] = array_merge( array( $fallbackInfo[0] ), $synonyms );
  459. }
  460. }
  461. }
  462. /**
  463. * Given an array mapping language code to localisation value, such as is
  464. * found in extension *.i18n.php files, iterate through a fallback sequence
  465. * to merge the given data with an existing primary value.
  466. *
  467. * Returns true if any data from the extension array was used, false
  468. * otherwise.
  469. * @param $codeSequence
  470. * @param $key
  471. * @param $value
  472. * @param $fallbackValue
  473. * @return bool
  474. */
  475. protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) {
  476. $used = false;
  477. foreach ( $codeSequence as $code ) {
  478. if ( isset( $fallbackValue[$code] ) ) {
  479. $this->mergeItem( $key, $value, $fallbackValue[$code] );
  480. $used = true;
  481. }
  482. }
  483. return $used;
  484. }
  485. /**
  486. * Load localisation data for a given language for both core and extensions
  487. * and save it to the persistent cache store and the process cache
  488. * @param $code
  489. */
  490. public function recache( $code ) {
  491. global $wgExtensionMessagesFiles;
  492. wfProfileIn( __METHOD__ );
  493. if ( !$code ) {
  494. throw new MWException( "Invalid language code requested" );
  495. }
  496. $this->recachedLangs[$code] = true;
  497. # Initial values
  498. $initialData = array_combine(
  499. self::$allKeys,
  500. array_fill( 0, count( self::$allKeys ), null ) );
  501. $coreData = $initialData;
  502. $deps = array();
  503. # Load the primary localisation from the source file
  504. $fileName = Language::getMessagesFileName( $code );
  505. if ( !file_exists( $fileName ) ) {
  506. wfDebug( __METHOD__.": no localisation file for $code, using fallback to en\n" );
  507. $coreData['fallback'] = 'en';
  508. } else {
  509. $deps[] = new FileDependency( $fileName );
  510. $data = $this->readPHPFile( $fileName, 'core' );
  511. wfDebug( __METHOD__.": got localisation for $code from source\n" );
  512. # Merge primary localisation
  513. foreach ( $data as $key => $value ) {
  514. $this->mergeItem( $key, $coreData[$key], $value );
  515. }
  516. }
  517. # Fill in the fallback if it's not there already
  518. if ( is_null( $coreData['fallback'] ) ) {
  519. $coreData['fallback'] = $code === 'en' ? false : 'en';
  520. }
  521. if ( $coreData['fallback'] === false ) {
  522. $coreData['fallbackSequence'] = array();
  523. } else {
  524. $coreData['fallbackSequence'] = array_map( 'trim', explode( ',', $coreData['fallback'] ) );
  525. $len = count( $coreData['fallbackSequence'] );
  526. # Ensure that the sequence ends at en
  527. if ( $coreData['fallbackSequence'][$len - 1] !== 'en' ) {
  528. $coreData['fallbackSequence'][] = 'en';
  529. }
  530. # Load the fallback localisation item by item and merge it
  531. foreach ( $coreData['fallbackSequence'] as $fbCode ) {
  532. # Load the secondary localisation from the source file to
  533. # avoid infinite cycles on cyclic fallbacks
  534. $fbFilename = Language::getMessagesFileName( $fbCode );
  535. if ( !file_exists( $fbFilename ) ) {
  536. continue;
  537. }
  538. $deps[] = new FileDependency( $fbFilename );
  539. $fbData = $this->readPHPFile( $fbFilename, 'core' );
  540. foreach ( self::$allKeys as $key ) {
  541. if ( !isset( $fbData[$key] ) ) {
  542. continue;
  543. }
  544. if ( is_null( $coreData[$key] ) || $this->isMergeableKey( $key ) ) {
  545. $this->mergeItem( $key, $coreData[$key], $fbData[$key] );
  546. }
  547. }
  548. }
  549. }
  550. $codeSequence = array_merge( array( $code ), $coreData['fallbackSequence'] );
  551. # Load the extension localisations
  552. # This is done after the core because we know the fallback sequence now.
  553. # But it has a higher precedence for merging so that we can support things
  554. # like site-specific message overrides.
  555. $allData = $initialData;
  556. foreach ( $wgExtensionMessagesFiles as $fileName ) {
  557. $data = $this->readPHPFile( $fileName, 'extension' );
  558. $used = false;
  559. foreach ( $data as $key => $item ) {
  560. if( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) {
  561. $used = true;
  562. }
  563. }
  564. if ( $used ) {
  565. $deps[] = new FileDependency( $fileName );
  566. }
  567. }
  568. # Merge core data into extension data
  569. foreach ( $coreData as $key => $item ) {
  570. $this->mergeItem( $key, $allData[$key], $item );
  571. }
  572. # Add cache dependencies for any referenced globals
  573. $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
  574. $deps['version'] = new ConstantDependency( 'MW_LC_VERSION' );
  575. # Add dependencies to the cache entry
  576. $allData['deps'] = $deps;
  577. # Replace spaces with underscores in namespace names
  578. $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] );
  579. # And do the same for special page aliases. $page is an array.
  580. foreach ( $allData['specialPageAliases'] as &$page ) {
  581. $page = str_replace( ' ', '_', $page );
  582. }
  583. # Decouple the reference to prevent accidental damage
  584. unset($page);
  585. # Set the list keys
  586. $allData['list'] = array();
  587. foreach ( self::$splitKeys as $key ) {
  588. $allData['list'][$key] = array_keys( $allData[$key] );
  589. }
  590. # Run hooks
  591. wfRunHooks( 'LocalisationCacheRecache', array( $this, $code, &$allData ) );
  592. if ( is_null( $allData['namespaceNames'] ) ) {
  593. throw new MWException( __METHOD__.': Localisation data failed sanity check! ' .
  594. 'Check that your languages/messages/MessagesEn.php file is intact.' );
  595. }
  596. # Set the preload key
  597. $allData['preload'] = $this->buildPreload( $allData );
  598. # Save to the process cache and register the items loaded
  599. $this->data[$code] = $allData;
  600. foreach ( $allData as $key => $item ) {
  601. $this->loadedItems[$code][$key] = true;
  602. }
  603. # Save to the persistent cache
  604. $this->store->startWrite( $code );
  605. foreach ( $allData as $key => $value ) {
  606. if ( in_array( $key, self::$splitKeys ) ) {
  607. foreach ( $value as $subkey => $subvalue ) {
  608. $this->store->set( "$key:$subkey", $subvalue );
  609. }
  610. } else {
  611. $this->store->set( $key, $value );
  612. }
  613. }
  614. $this->store->finishWrite();
  615. # Clear out the MessageBlobStore
  616. # HACK: If using a null (i.e. disabled) storage backend, we
  617. # can't write to the MessageBlobStore either
  618. if ( !$this->store instanceof LCStore_Null ) {
  619. MessageBlobStore::clear();
  620. }
  621. wfProfileOut( __METHOD__ );
  622. }
  623. /**
  624. * Build the preload item from the given pre-cache data.
  625. *
  626. * The preload item will be loaded automatically, improving performance
  627. * for the commonly-requested items it contains.
  628. * @param $data
  629. * @return array
  630. */
  631. protected function buildPreload( $data ) {
  632. $preload = array( 'messages' => array() );
  633. foreach ( self::$preloadedKeys as $key ) {
  634. $preload[$key] = $data[$key];
  635. }
  636. foreach ( $data['preloadedMessages'] as $subkey ) {
  637. if ( isset( $data['messages'][$subkey] ) ) {
  638. $subitem = $data['messages'][$subkey];
  639. } else {
  640. $subitem = null;
  641. }
  642. $preload['messages'][$subkey] = $subitem;
  643. }
  644. return $preload;
  645. }
  646. /**
  647. * Unload the data for a given language from the object cache.
  648. * Reduces memory usage.
  649. * @param $code
  650. */
  651. public function unload( $code ) {
  652. unset( $this->data[$code] );
  653. unset( $this->loadedItems[$code] );
  654. unset( $this->loadedSubitems[$code] );
  655. unset( $this->initialisedLangs[$code] );
  656. foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
  657. if ( $fbCode === $code ) {
  658. $this->unload( $shallowCode );
  659. }
  660. }
  661. }
  662. /**
  663. * Unload all data
  664. */
  665. public function unloadAll() {
  666. foreach ( $this->initialisedLangs as $lang => $unused ) {
  667. $this->unload( $lang );
  668. }
  669. }
  670. /**
  671. * Disable the storage backend
  672. */
  673. public function disableBackend() {
  674. $this->store = new LCStore_Null;
  675. $this->manualRecache = false;
  676. }
  677. }
  678. /**
  679. * Interface for the persistence layer of LocalisationCache.
  680. *
  681. * The persistence layer is two-level hierarchical cache. The first level
  682. * is the language, the second level is the item or subitem.
  683. *
  684. * Since the data for a whole language is rebuilt in one operation, it needs
  685. * to have a fast and atomic method for deleting or replacing all of the
  686. * current data for a given language. The interface reflects this bulk update
  687. * operation. Callers writing to the cache must first call startWrite(), then
  688. * will call set() a couple of thousand times, then will call finishWrite()
  689. * to commit the operation. When finishWrite() is called, the cache is
  690. * expected to delete all data previously stored for that language.
  691. *
  692. * The values stored are PHP variables suitable for serialize(). Implementations
  693. * of LCStore are responsible for serializing and unserializing.
  694. */
  695. interface LCStore {
  696. /**
  697. * Get a value.
  698. * @param $code Language code
  699. * @param $key Cache key
  700. */
  701. function get( $code, $key );
  702. /**
  703. * Start a write transaction.
  704. * @param $code Language code
  705. */
  706. function startWrite( $code );
  707. /**
  708. * Finish a write transaction.
  709. */
  710. function finishWrite();
  711. /**
  712. * Set a key to a given value. startWrite() must be called before this
  713. * is called, and finishWrite() must be called afterwards.
  714. * @param $key
  715. * @param $value
  716. */
  717. function set( $key, $value );
  718. }
  719. /**
  720. * LCStore implementation which uses PHP accelerator to store data.
  721. * This will work if one of XCache, WinCache or APC cacher is configured.
  722. * (See ObjectCache.php)
  723. */
  724. class LCStore_Accel implements LCStore {
  725. var $currentLang;
  726. var $keys;
  727. public function __construct() {
  728. $this->cache = wfGetCache( CACHE_ACCEL );
  729. }
  730. public function get( $code, $key ) {
  731. $k = wfMemcKey( 'l10n', $code, 'k', $key );
  732. $r = $this->cache->get( $k );
  733. return $r === false ? null : $r;
  734. }
  735. public function startWrite( $code ) {
  736. $k = wfMemcKey( 'l10n', $code, 'l' );
  737. $keys = $this->cache->get( $k );
  738. if ( $keys ) {
  739. foreach ( $keys as $k ) {
  740. $this->cache->delete( $k );
  741. }
  742. }
  743. $this->currentLang = $code;
  744. $this->keys = array();
  745. }
  746. public function finishWrite() {
  747. if ( $this->currentLang ) {
  748. $k = wfMemcKey( 'l10n', $this->currentLang, 'l' );
  749. $this->cache->set( $k, array_keys( $this->keys ) );
  750. }
  751. $this->currentLang = null;
  752. $this->keys = array();
  753. }
  754. public function set( $key, $value ) {
  755. if ( $this->currentLang ) {
  756. $k = wfMemcKey( 'l10n', $this->currentLang, 'k', $key );
  757. $this->keys[$k] = true;
  758. $this->cache->set( $k, $value );
  759. }
  760. }
  761. }
  762. /**
  763. * LCStore implementation which uses the standard DB functions to store data.
  764. * This will work on any MediaWiki installation.
  765. */
  766. class LCStore_DB implements LCStore {
  767. var $currentLang;
  768. var $writesDone = false;
  769. /**
  770. * @var DatabaseBase
  771. */
  772. var $dbw;
  773. var $batch;
  774. var $readOnly = false;
  775. public function get( $code, $key ) {
  776. if ( $this->writesDone ) {
  777. $db = wfGetDB( DB_MASTER );
  778. } else {
  779. $db = wfGetDB( DB_SLAVE );
  780. }
  781. $row = $db->selectRow( 'l10n_cache', array( 'lc_value' ),
  782. array( 'lc_lang' => $code, 'lc_key' => $key ), __METHOD__ );
  783. if ( $row ) {
  784. return unserialize( $row->lc_value );
  785. } else {
  786. return null;
  787. }
  788. }
  789. public function startWrite( $code ) {
  790. if ( $this->readOnly ) {
  791. return;
  792. }
  793. if ( !$code ) {
  794. throw new MWException( __METHOD__.": Invalid language \"$code\"" );
  795. }
  796. $this->dbw = wfGetDB( DB_MASTER );
  797. try {
  798. $this->dbw->begin();
  799. $this->dbw->delete( 'l10n_cache', array( 'lc_lang' => $code ), __METHOD__ );
  800. } catch ( DBQueryError $e ) {
  801. if ( $this->dbw->wasReadOnlyError() ) {
  802. $this->readOnly = true;
  803. $this->dbw->rollback();
  804. $this->dbw->ignoreErrors( false );
  805. return;
  806. } else {
  807. throw $e;
  808. }
  809. }
  810. $this->currentLang = $code;
  811. $this->batch = array();
  812. }
  813. public function finishWrite() {
  814. if ( $this->readOnly ) {
  815. return;
  816. }
  817. if ( $this->batch ) {
  818. $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
  819. }
  820. $this->dbw->commit();
  821. $this->currentLang = null;
  822. $this->dbw = null;
  823. $this->batch = array();
  824. $this->writesDone = true;
  825. }
  826. public function set( $key, $value ) {
  827. if ( $this->readOnly ) {
  828. return;
  829. }
  830. if ( is_null( $this->currentLang ) ) {
  831. throw new MWException( __CLASS__.': must call startWrite() before calling set()' );
  832. }
  833. $this->batch[] = array(
  834. 'lc_lang' => $this->currentLang,
  835. 'lc_key' => $key,
  836. 'lc_value' => serialize( $value ) );
  837. if ( count( $this->batch ) >= 100 ) {
  838. $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
  839. $this->batch = array();
  840. }
  841. }
  842. }
  843. /**
  844. * LCStore implementation which stores data as a collection of CDB files in the
  845. * directory given by $wgCacheDirectory. If $wgCacheDirectory is not set, this
  846. * will throw an exception.
  847. *
  848. * Profiling indicates that on Linux, this implementation outperforms MySQL if
  849. * the directory is on a local filesystem and there is ample kernel cache
  850. * space. The performance advantage is greater when the DBA extension is
  851. * available than it is with the PHP port.
  852. *
  853. * See Cdb.php and http://cr.yp.to/cdb.html
  854. */
  855. class LCStore_CDB implements LCStore {
  856. var $readers, $writer, $currentLang, $directory;
  857. function __construct( $conf = array() ) {
  858. global $wgCacheDirectory;
  859. if ( isset( $conf['directory'] ) ) {
  860. $this->directory = $conf['directory'];
  861. } else {
  862. $this->directory = $wgCacheDirectory;
  863. }
  864. }
  865. public function get( $code, $key ) {
  866. if ( !isset( $this->readers[$code] ) ) {
  867. $fileName = $this->getFileName( $code );
  868. if ( !file_exists( $fileName ) ) {
  869. $this->readers[$code] = false;
  870. } else {
  871. $this->readers[$code] = CdbReader::open( $fileName );
  872. }
  873. }
  874. if ( !$this->readers[$code] ) {
  875. return null;
  876. } else {
  877. $value = $this->readers[$code]->get( $key );
  878. if ( $value === false ) {
  879. return null;
  880. }
  881. return unserialize( $value );
  882. }
  883. }
  884. public function startWrite( $code ) {
  885. if ( !file_exists( $this->directory ) ) {
  886. if ( !wfMkdirParents( $this->directory, null, __METHOD__ ) ) {
  887. throw new MWException( "Unable to create the localisation store " .
  888. "directory \"{$this->directory}\"" );
  889. }
  890. }
  891. // Close reader to stop permission errors on write
  892. if( !empty($this->readers[$code]) ) {
  893. $this->readers[$code]->close();
  894. }
  895. $this->writer = CdbWriter::open( $this->getFileName( $code ) );
  896. $this->currentLang = $code;
  897. }
  898. public function finishWrite() {
  899. // Close the writer
  900. $this->writer->close();
  901. $this->writer = null;
  902. unset( $this->readers[$this->currentLang] );
  903. $this->currentLang = null;
  904. }
  905. public function set( $key, $value ) {
  906. if ( is_null( $this->writer ) ) {
  907. throw new MWException( __CLASS__.': must call startWrite() before calling set()' );
  908. }
  909. $this->writer->set( $key, serialize( $value ) );
  910. }
  911. protected function getFileName( $code ) {
  912. if ( !$code || strpos( $code, '/' ) !== false ) {
  913. throw new MWException( __METHOD__.": Invalid language \"$code\"" );
  914. }
  915. return "{$this->directory}/l10n_cache-$code.cdb";
  916. }
  917. }
  918. /**
  919. * Null store backend, used to avoid DB errors during install
  920. */
  921. class LCStore_Null implements LCStore {
  922. public function get( $code, $key ) {
  923. return null;
  924. }
  925. public function startWrite( $code ) {}
  926. public function finishWrite() {}
  927. public function set( $key, $value ) {}
  928. }
  929. /**
  930. * A localisation cache optimised for loading large amounts of data for many
  931. * languages. Used by rebuildLocalisationCache.php.
  932. */
  933. class LocalisationCache_BulkLoad extends LocalisationCache {
  934. /**
  935. * A cache of the contents of data files.
  936. * Core files are serialized to avoid using ~1GB of RAM during a recache.
  937. */
  938. var $fileCache = array();
  939. /**
  940. * Most recently used languages. Uses the linked-list aspect of PHP hashtables
  941. * to keep the most recently used language codes at the end of the array, and
  942. * the language codes that are ready to be deleted at the beginning.
  943. */
  944. var $mruLangs = array();
  945. /**
  946. * Maximum number of languages that may be loaded into $this->data
  947. */
  948. var $maxLoadedLangs = 10;
  949. /**
  950. * @param $fileName
  951. * @param $fileType
  952. * @return array|mixed
  953. */
  954. protected function readPHPFile( $fileName, $fileType ) {
  955. $serialize = $fileType === 'core';
  956. if ( !isset( $this->fileCache[$fileName][$fileType] ) ) {
  957. $data = parent::readPHPFile( $fileName, $fileType );
  958. if ( $serialize ) {
  959. $encData = serialize( $data );
  960. } else {
  961. $encData = $data;
  962. }
  963. $this->fileCache[$fileName][$fileType] = $encData;
  964. return $data;
  965. } elseif ( $serialize ) {
  966. return unserialize( $this->fileCache[$fileName][$fileType] );
  967. } else {
  968. return $this->fileCache[$fileName][$fileType];
  969. }
  970. }
  971. /**
  972. * @param $code
  973. * @param $key
  974. * @return mixed
  975. */
  976. public function getItem( $code, $key ) {
  977. unset( $this->mruLangs[$code] );
  978. $this->mruLangs[$code] = true;
  979. return parent::getItem( $code, $key );
  980. }
  981. /**
  982. * @param $code
  983. * @param $key
  984. * @param $subkey
  985. * @return
  986. */
  987. public function getSubitem( $code, $key, $subkey ) {
  988. unset( $this->mruLangs[$code] );
  989. $this->mruLangs[$code] = true;
  990. return parent::getSubitem( $code, $key, $subkey );
  991. }
  992. /**
  993. * @param $code
  994. */
  995. public function recache( $code ) {
  996. parent::recache( $code );
  997. unset( $this->mruLangs[$code] );
  998. $this->mruLangs[$code] = true;
  999. $this->trimCache();
  1000. }
  1001. /**
  1002. * @param $code
  1003. */
  1004. public function unload( $code ) {
  1005. unset( $this->mruLangs[$code] );
  1006. parent::unload( $code );
  1007. }
  1008. /**
  1009. * Unload cached languages until there are less than $this->maxLoadedLangs
  1010. */
  1011. protected function trimCache() {
  1012. while ( count( $this->data ) > $this->maxLoadedLangs && count( $this->mruLangs ) ) {
  1013. reset( $this->mruLangs );
  1014. $code = key( $this->mruLangs );
  1015. wfDebug( __METHOD__.": unloading $code\n" );
  1016. $this->unload( $code );
  1017. }
  1018. }
  1019. }