MessageCache.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870
  1. <?php
  2. /**
  3. * @file
  4. * @ingroup Cache
  5. */
  6. /**
  7. *
  8. */
  9. define( 'MSG_LOAD_TIMEOUT', 60);
  10. define( 'MSG_LOCK_TIMEOUT', 10);
  11. define( 'MSG_WAIT_TIMEOUT', 10);
  12. define( 'MSG_CACHE_VERSION', 1 );
  13. /**
  14. * Message cache
  15. * Performs various MediaWiki namespace-related functions
  16. * @ingroup Cache
  17. */
  18. class MessageCache {
  19. // Holds loaded messages that are defined in MediaWiki namespace.
  20. var $mCache;
  21. var $mUseCache, $mDisable, $mExpiry;
  22. var $mKeys, $mParserOptions, $mParser;
  23. var $mExtensionMessages = array();
  24. var $mInitialised = false;
  25. var $mAllMessagesLoaded = array(); // Extension messages
  26. // Variable for tracking which variables are loaded
  27. var $mLoadedLanguages = array();
  28. function __construct( &$memCached, $useDB, $expiry, /*ignored*/ $memcPrefix ) {
  29. $this->mUseCache = !is_null( $memCached );
  30. $this->mMemc = &$memCached;
  31. $this->mDisable = !$useDB;
  32. $this->mExpiry = $expiry;
  33. $this->mDisableTransform = false;
  34. $this->mKeys = false; # initialised on demand
  35. $this->mInitialised = true;
  36. $this->mParser = null;
  37. }
  38. /**
  39. * ParserOptions is lazy initialised.
  40. */
  41. function getParserOptions() {
  42. if ( !$this->mParserOptions ) {
  43. $this->mParserOptions = new ParserOptions;
  44. }
  45. return $this->mParserOptions;
  46. }
  47. /**
  48. * Try to load the cache from a local file.
  49. * Actual format of the file depends on the $wgLocalMessageCacheSerialized
  50. * setting.
  51. *
  52. * @param $hash String: the hash of contents, to check validity.
  53. * @param $code Mixed: Optional language code, see documenation of load().
  54. * @return false on failure.
  55. */
  56. function loadFromLocal( $hash, $code ) {
  57. global $wgLocalMessageCache, $wgLocalMessageCacheSerialized;
  58. $filename = "$wgLocalMessageCache/messages-" . wfWikiID() . "-$code";
  59. # Check file existence
  60. wfSuppressWarnings();
  61. $file = fopen( $filename, 'r' );
  62. wfRestoreWarnings();
  63. if ( !$file ) {
  64. return false; // No cache file
  65. }
  66. if ( $wgLocalMessageCacheSerialized ) {
  67. // Check to see if the file has the hash specified
  68. $localHash = fread( $file, 32 );
  69. if ( $hash === $localHash ) {
  70. // All good, get the rest of it
  71. $serialized = '';
  72. while ( !feof( $file ) ) {
  73. $serialized .= fread( $file, 100000 );
  74. }
  75. fclose( $file );
  76. return $this->setCache( unserialize( $serialized ), $code );
  77. } else {
  78. fclose( $file );
  79. return false; // Wrong hash
  80. }
  81. } else {
  82. $localHash=substr(fread($file,40),8);
  83. fclose($file);
  84. if ($hash!=$localHash) {
  85. return false; // Wrong hash
  86. }
  87. # Require overwrites the member variable or just shadows it?
  88. require( $filename );
  89. return $this->setCache( $this->mCache, $code );
  90. }
  91. }
  92. /**
  93. * Save the cache to a local file.
  94. */
  95. function saveToLocal( $serialized, $hash, $code ) {
  96. global $wgLocalMessageCache;
  97. $filename = "$wgLocalMessageCache/messages-" . wfWikiID() . "-$code";
  98. wfMkdirParents( $wgLocalMessageCache ); // might fail
  99. wfSuppressWarnings();
  100. $file = fopen( $filename, 'w' );
  101. wfRestoreWarnings();
  102. if ( !$file ) {
  103. wfDebug( "Unable to open local cache file for writing\n" );
  104. return;
  105. }
  106. fwrite( $file, $hash . $serialized );
  107. fclose( $file );
  108. @chmod( $filename, 0666 );
  109. }
  110. function saveToScript( $array, $hash, $code ) {
  111. global $wgLocalMessageCache;
  112. $filename = "$wgLocalMessageCache/messages-" . wfWikiID() . "-$code";
  113. $tempFilename = $filename . '.tmp';
  114. wfMkdirParents( $wgLocalMessageCache ); // might fail
  115. wfSuppressWarnings();
  116. $file = fopen( $tempFilename, 'w');
  117. wfRestoreWarnings();
  118. if ( !$file ) {
  119. wfDebug( "Unable to open local cache file for writing\n" );
  120. return;
  121. }
  122. fwrite($file,"<?php\n//$hash\n\n \$this->mCache = array(");
  123. foreach ($array as $key => $message) {
  124. $key = $this->escapeForScript($key);
  125. $messages = $this->escapeForScript($message);
  126. fwrite($file, "'$key' => '$message',\n");
  127. }
  128. fwrite($file,");\n?>");
  129. fclose($file);
  130. rename($tempFilename, $filename);
  131. }
  132. function escapeForScript($string) {
  133. $string = str_replace( '\\', '\\\\', $string );
  134. $string = str_replace( '\'', '\\\'', $string );
  135. return $string;
  136. }
  137. /**
  138. * Set the cache to $cache, if it is valid. Otherwise set the cache to false.
  139. */
  140. function setCache( $cache, $code ) {
  141. if ( isset( $cache['VERSION'] ) && $cache['VERSION'] == MSG_CACHE_VERSION ) {
  142. $this->mCache[$code] = $cache;
  143. return true;
  144. } else {
  145. return false;
  146. }
  147. }
  148. /**
  149. * Loads messages from caches or from database in this order:
  150. * (1) local message cache (if $wgLocalMessageCache is enabled)
  151. * (2) memcached
  152. * (3) from the database.
  153. *
  154. * When succesfully loading from (2) or (3), all higher level caches are
  155. * updated for the newest version.
  156. *
  157. * Nothing is loaded if member variable mDisabled is true, either manually
  158. * set by calling code or if message loading fails (is this possible?).
  159. *
  160. * Returns true if cache is already populated or it was succesfully populated,
  161. * or false if populating empty cache fails. Also returns true if MessageCache
  162. * is disabled.
  163. *
  164. * @param $code String: language to which load messages
  165. */
  166. function load( $code = false ) {
  167. global $wgLocalMessageCache;
  168. if ( !$this->mUseCache ) {
  169. return true;
  170. }
  171. if( !is_string( $code ) ) {
  172. # This isn't really nice, so at least make a note about it and try to
  173. # fall back
  174. wfDebug( __METHOD__ . " called without providing a language code\n" );
  175. $code = 'en';
  176. }
  177. # Don't do double loading...
  178. if ( isset($this->mLoadedLanguages[$code]) ) return true;
  179. # 8 lines of code just to say (once) that message cache is disabled
  180. if ( $this->mDisable ) {
  181. static $shownDisabled = false;
  182. if ( !$shownDisabled ) {
  183. wfDebug( __METHOD__ . ": disabled\n" );
  184. $shownDisabled = true;
  185. }
  186. return true;
  187. }
  188. # Loading code starts
  189. wfProfileIn( __METHOD__ );
  190. $success = false; # Keep track of success
  191. $where = array(); # Debug info, delayed to avoid spamming debug log too much
  192. $cacheKey = wfMemcKey( 'messages', $code ); # Key in memc for messages
  193. # (1) local cache
  194. # Hash of the contents is stored in memcache, to detect if local cache goes
  195. # out of date (due to update in other thread?)
  196. if ( $wgLocalMessageCache !== false ) {
  197. wfProfileIn( __METHOD__ . '-fromlocal' );
  198. $hash = $this->mMemc->get( wfMemcKey( 'messages', $code, 'hash' ) );
  199. if ( $hash ) {
  200. $success = $this->loadFromLocal( $hash, $code );
  201. if ( $success ) $where[] = 'got from local cache';
  202. }
  203. wfProfileOut( __METHOD__ . '-fromlocal' );
  204. }
  205. # (2) memcache
  206. # Fails if nothing in cache, or in the wrong version.
  207. if ( !$success ) {
  208. wfProfileIn( __METHOD__ . '-fromcache' );
  209. $cache = $this->mMemc->get( $cacheKey );
  210. $success = $this->setCache( $cache, $code );
  211. if ( $success ) {
  212. $where[] = 'got from global cache';
  213. $this->saveToCaches( $cache, false, $code );
  214. }
  215. wfProfileOut( __METHOD__ . '-fromcache' );
  216. }
  217. # (3)
  218. # Nothing in caches... so we need create one and store it in caches
  219. if ( !$success ) {
  220. $where[] = 'cache is empty';
  221. $where[] = 'loading from database';
  222. $this->lock($cacheKey);
  223. # Limit the concurrency of loadFromDB to a single process
  224. # This prevents the site from going down when the cache expires
  225. $statusKey = wfMemcKey( 'messages', $code, 'status' );
  226. $success = $this->mMemc->add( $statusKey, 'loading', MSG_LOAD_TIMEOUT );
  227. if ( $success ) {
  228. $cache = $this->loadFromDB( $code );
  229. $success = $this->setCache( $cache, $code );
  230. }
  231. if ( $success ) {
  232. $success = $this->saveToCaches( $cache, true, $code );
  233. if ( $success ) {
  234. $this->mMemc->delete( $statusKey );
  235. } else {
  236. $this->mMemc->set( $statusKey, 'error', 60*5 );
  237. wfDebug( "MemCached set error in MessageCache: restart memcached server!\n" );
  238. }
  239. }
  240. $this->unlock($cacheKey);
  241. }
  242. if ( !$success ) {
  243. # Bad luck... this should not happen
  244. $where[] = 'loading FAILED - cache is disabled';
  245. $info = implode( ', ', $where );
  246. wfDebug( __METHOD__ . ": Loading $code... $info\n" );
  247. $this->mDisable = true;
  248. $this->mCache = false;
  249. } else {
  250. # All good, just record the success
  251. $info = implode( ', ', $where );
  252. wfDebug( __METHOD__ . ": Loading $code... $info\n" );
  253. $this->mLoadedLanguages[$code] = true;
  254. }
  255. wfProfileOut( __METHOD__ );
  256. return $success;
  257. }
  258. /**
  259. * Loads cacheable messages from the database. Messages bigger than
  260. * $wgMaxMsgCacheEntrySize are assigned a special value, and are loaded
  261. * on-demand from the database later.
  262. *
  263. * @param $code Optional language code, see documenation of load().
  264. * @return Array: Loaded messages for storing in caches.
  265. */
  266. function loadFromDB( $code = false ) {
  267. wfProfileIn( __METHOD__ );
  268. global $wgMaxMsgCacheEntrySize, $wgContLanguageCode;
  269. $dbr = wfGetDB( DB_SLAVE );
  270. $cache = array();
  271. # Common conditions
  272. $conds = array(
  273. 'page_is_redirect' => 0,
  274. 'page_namespace' => NS_MEDIAWIKI,
  275. );
  276. if ( $code ) {
  277. # Is this fast enough. Should not matter if the filtering is done in the
  278. # database or in code.
  279. if ( $code !== $wgContLanguageCode ) {
  280. # Messages for particular language
  281. $escapedCode = $dbr->escapeLike( $code );
  282. $conds[] = "page_title like '%%/$escapedCode'";
  283. } else {
  284. # Effectively disallows use of '/' character in NS_MEDIAWIKI for uses
  285. # other than language code.
  286. $conds[] = "page_title not like '%%/%%'";
  287. }
  288. }
  289. # Conditions to fetch oversized pages to ignore them
  290. $bigConds = $conds;
  291. $bigConds[] = 'page_len > ' . intval( $wgMaxMsgCacheEntrySize );
  292. # Load titles for all oversized pages in the MediaWiki namespace
  293. $res = $dbr->select( 'page', 'page_title', $bigConds, __METHOD__ );
  294. while ( $row = $dbr->fetchObject( $res ) ) {
  295. $cache[$row->page_title] = '!TOO BIG';
  296. }
  297. $dbr->freeResult( $res );
  298. # Conditions to load the remaining pages with their contents
  299. $smallConds = $conds;
  300. $smallConds[] = 'page_latest=rev_id';
  301. $smallConds[] = 'rev_text_id=old_id';
  302. $smallConds[] = 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize );
  303. $res = $dbr->select( array( 'page', 'revision', 'text' ),
  304. array( 'page_title', 'old_text', 'old_flags' ),
  305. $smallConds, __METHOD__ );
  306. for ( $row = $dbr->fetchObject( $res ); $row; $row = $dbr->fetchObject( $res ) ) {
  307. $cache[$row->page_title] = ' ' . Revision::getRevisionText( $row );
  308. }
  309. $dbr->freeResult( $res );
  310. $cache['VERSION'] = MSG_CACHE_VERSION;
  311. wfProfileOut( __METHOD__ );
  312. return $cache;
  313. }
  314. /**
  315. * Updates cache as necessary when message page is changed
  316. *
  317. * @param $title String: name of the page changed.
  318. * @param $text Mixed: new contents of the page.
  319. */
  320. public function replace( $title, $text ) {
  321. global $wgMaxMsgCacheEntrySize;
  322. wfProfileIn( __METHOD__ );
  323. list( , $code ) = $this->figureMessage( $title );
  324. $cacheKey = wfMemcKey( 'messages', $code );
  325. $this->load($code);
  326. $this->lock($cacheKey);
  327. if ( is_array($this->mCache[$code]) ) {
  328. $titleKey = wfMemcKey( 'messages', 'individual', $title );
  329. if ( $text === false ) {
  330. # Article was deleted
  331. unset( $this->mCache[$code][$title] );
  332. $this->mMemc->delete( $titleKey );
  333. } elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) {
  334. # Check for size
  335. $this->mCache[$code][$title] = '!TOO BIG';
  336. $this->mMemc->set( $titleKey, ' ' . $text, $this->mExpiry );
  337. } else {
  338. $this->mCache[$code][$title] = ' ' . $text;
  339. $this->mMemc->delete( $titleKey );
  340. }
  341. # Update caches
  342. $this->saveToCaches( $this->mCache[$code], true, $code );
  343. }
  344. $this->unlock($cacheKey);
  345. // Also delete cached sidebar... just in case it is affected
  346. global $parserMemc;
  347. $sidebarKey = wfMemcKey( 'sidebar', $code );
  348. $parserMemc->delete( $sidebarKey );
  349. wfProfileOut( __METHOD__ );
  350. }
  351. /**
  352. * Shortcut to update caches.
  353. *
  354. * @param $cache Array: cached messages with a version.
  355. * @param $cacheKey String: Identifier for the cache.
  356. * @param $memc Bool: Wether to update or not memcache.
  357. * @param $code String: Language code.
  358. * @return False on somekind of error.
  359. */
  360. protected function saveToCaches( $cache, $memc = true, $code = false ) {
  361. wfProfileIn( __METHOD__ );
  362. global $wgLocalMessageCache, $wgLocalMessageCacheSerialized;
  363. $cacheKey = wfMemcKey( 'messages', $code );
  364. $i = 0;
  365. if ( $memc ) {
  366. # Save in memcached
  367. # Keep trying if it fails, this is kind of important
  368. for ($i=0; $i<20 &&
  369. !$this->mMemc->set( $cacheKey, $cache, $this->mExpiry );
  370. $i++ ) {
  371. usleep(mt_rand(500000,1500000));
  372. }
  373. }
  374. # Save to local cache
  375. if ( $wgLocalMessageCache !== false ) {
  376. $serialized = serialize( $cache );
  377. $hash = md5( $serialized );
  378. $this->mMemc->set( wfMemcKey( 'messages', $code, 'hash' ), $hash, $this->mExpiry );
  379. if ($wgLocalMessageCacheSerialized) {
  380. $this->saveToLocal( $serialized, $hash, $code );
  381. } else {
  382. $this->saveToScript( $cache, $hash, $code );
  383. }
  384. }
  385. if ( $i == 20 ) {
  386. $success = false;
  387. } else {
  388. $success = true;
  389. }
  390. wfProfileOut( __METHOD__ );
  391. return $success;
  392. }
  393. /**
  394. * Returns success
  395. * Represents a write lock on the messages key
  396. */
  397. function lock($key) {
  398. if ( !$this->mUseCache ) {
  399. return true;
  400. }
  401. $lockKey = $key . ':lock';
  402. for ($i=0; $i < MSG_WAIT_TIMEOUT && !$this->mMemc->add( $lockKey, 1, MSG_LOCK_TIMEOUT ); $i++ ) {
  403. sleep(1);
  404. }
  405. return $i >= MSG_WAIT_TIMEOUT;
  406. }
  407. function unlock($key) {
  408. if ( !$this->mUseCache ) {
  409. return;
  410. }
  411. $lockKey = $key . ':lock';
  412. $this->mMemc->delete( $lockKey );
  413. }
  414. /**
  415. * Get a message from either the content language or the user language.
  416. *
  417. * @param string $key The message cache key
  418. * @param bool $useDB Get the message from the DB, false to use only the localisation
  419. * @param string $langcode Code of the language to get the message for, if
  420. * it is a valid code create a language for that
  421. * language, if it is a string but not a valid code
  422. * then make a basic language object, if it is a
  423. * false boolean then use the current users
  424. * language (as a fallback for the old parameter
  425. * functionality), or if it is a true boolean then
  426. * use the wikis content language (also as a
  427. * fallback).
  428. * @param bool $isFullKey Specifies whether $key is a two part key "lang/msg".
  429. */
  430. function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) {
  431. global $wgContLanguageCode, $wgContLang;
  432. $lang = wfGetLangObj( $langcode );
  433. $langcode = $lang->getCode();
  434. # If uninitialised, someone is trying to call this halfway through Setup.php
  435. if( !$this->mInitialised ) {
  436. return '&lt;' . htmlspecialchars($key) . '&gt;';
  437. }
  438. $message = false;
  439. # Normalise title-case input
  440. $lckey = $wgContLang->lcfirst( $key );
  441. $lckey = str_replace( ' ', '_', $lckey );
  442. # Try the MediaWiki namespace
  443. if( !$this->mDisable && $useDB ) {
  444. $title = $wgContLang->ucfirst( $lckey );
  445. if(!$isFullKey && ($langcode != $wgContLanguageCode) ) {
  446. $title .= '/' . $langcode;
  447. }
  448. $message = $this->getMsgFromNamespace( $title, $langcode );
  449. }
  450. # Try the extension array
  451. if ( $message === false && isset( $this->mExtensionMessages[$langcode][$lckey] ) ) {
  452. $message = $this->mExtensionMessages[$langcode][$lckey];
  453. }
  454. if ( $message === false && isset( $this->mExtensionMessages['en'][$lckey] ) ) {
  455. $message = $this->mExtensionMessages['en'][$lckey];
  456. }
  457. # Try the array in the language object
  458. if ( $message === false ) {
  459. $message = $lang->getMessage( $lckey );
  460. if ( is_null( $message ) ) {
  461. $message = false;
  462. }
  463. }
  464. # Try the array of another language
  465. $pos = strrpos( $lckey, '/' );
  466. if( $message === false && $pos !== false) {
  467. $mkey = substr( $lckey, 0, $pos );
  468. $code = substr( $lckey, $pos+1 );
  469. if ( $code ) {
  470. # We may get calls for things that are http-urls from sidebar
  471. # Let's not load nonexistent languages for those
  472. $validCodes = array_keys( Language::getLanguageNames() );
  473. if ( in_array( $code, $validCodes ) ) {
  474. $message = Language::getMessageFor( $mkey, $code );
  475. if ( is_null( $message ) ) {
  476. $message = false;
  477. }
  478. }
  479. }
  480. }
  481. # Is this a custom message? Try the default language in the db...
  482. if( ($message === false || $message === '-' ) &&
  483. !$this->mDisable && $useDB &&
  484. !$isFullKey && ($langcode != $wgContLanguageCode) ) {
  485. $message = $this->getMsgFromNamespace( $wgContLang->ucfirst( $lckey ), $wgContLanguageCode );
  486. }
  487. # Final fallback
  488. if( $message === false ) {
  489. return '&lt;' . htmlspecialchars($key) . '&gt;';
  490. }
  491. return $message;
  492. }
  493. /**
  494. * Get a message from the MediaWiki namespace, with caching. The key must
  495. * first be converted to two-part lang/msg form if necessary.
  496. *
  497. * @param $title String: Message cache key with initial uppercase letter.
  498. * @param $code String: code denoting the language to try.
  499. */
  500. function getMsgFromNamespace( $title, $code ) {
  501. $type = false;
  502. $message = false;
  503. if ( $this->mUseCache ) {
  504. $this->load( $code );
  505. if (isset( $this->mCache[$code][$title] ) ) {
  506. $entry = $this->mCache[$code][$title];
  507. $type = substr( $entry, 0, 1 );
  508. if ( $type == ' ' ) {
  509. return substr( $entry, 1 );
  510. }
  511. }
  512. }
  513. # Call message hooks, in case they are defined
  514. wfRunHooks('MessagesPreLoad', array( $title, &$message ) );
  515. if ( $message !== false ) {
  516. return $message;
  517. }
  518. # If there is no cache entry and no placeholder, it doesn't exist
  519. if ( $type !== '!' ) {
  520. return false;
  521. }
  522. $titleKey = wfMemcKey( 'messages', 'individual', $title );
  523. # Try the individual message cache
  524. if ( $this->mUseCache ) {
  525. $entry = $this->mMemc->get( $titleKey );
  526. if ( $entry ) {
  527. $type = substr( $entry, 0, 1 );
  528. if ( $type === ' ' ) {
  529. # Ok!
  530. $message = substr( $entry, 1 );
  531. $this->mCache[$code][$title] = $entry;
  532. return $message;
  533. } elseif ( $entry === '!NONEXISTENT' ) {
  534. return false;
  535. } else {
  536. # Corrupt/obsolete entry, delete it
  537. $this->mMemc->delete( $titleKey );
  538. }
  539. }
  540. }
  541. # Try loading it from the DB
  542. $revision = Revision::newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) );
  543. if( $revision ) {
  544. $message = $revision->getText();
  545. if ($this->mUseCache) {
  546. $this->mCache[$code][$title] = ' ' . $message;
  547. $this->mMemc->set( $titleKey, $message, $this->mExpiry );
  548. }
  549. } else {
  550. # Negative caching
  551. # Use some special text instead of false, because false gets converted to '' somewhere
  552. $this->mMemc->set( $titleKey, '!NONEXISTENT', $this->mExpiry );
  553. $this->mCache[$code][$title] = false;
  554. }
  555. return $message;
  556. }
  557. function transform( $message, $interface = false, $language = null ) {
  558. // Avoid creating parser if nothing to transfrom
  559. if( strpos( $message, '{{' ) === false ) {
  560. return $message;
  561. }
  562. global $wgParser, $wgParserConf;
  563. if ( !$this->mParser && isset( $wgParser ) ) {
  564. # Do some initialisation so that we don't have to do it twice
  565. $wgParser->firstCallInit();
  566. # Clone it and store it
  567. $class = $wgParserConf['class'];
  568. if ( $class == 'Parser_DiffTest' ) {
  569. # Uncloneable
  570. $this->mParser = new $class( $wgParserConf );
  571. } else {
  572. $this->mParser = clone $wgParser;
  573. }
  574. #wfDebug( __METHOD__ . ": following contents triggered transform: $message\n" );
  575. }
  576. if ( $this->mParser ) {
  577. $popts = $this->getParserOptions();
  578. $popts->setInterfaceMessage( $interface );
  579. $popts->setTargetLanguage( $language );
  580. $message = $this->mParser->transformMsg( $message, $popts );
  581. }
  582. return $message;
  583. }
  584. function disable() { $this->mDisable = true; }
  585. function enable() { $this->mDisable = false; }
  586. /** @deprecated */
  587. function disableTransform(){
  588. wfDeprecated( __METHOD__ );
  589. }
  590. function enableTransform() {
  591. wfDeprecated( __METHOD__ );
  592. }
  593. function setTransform( $x ) {
  594. wfDeprecated( __METHOD__ );
  595. }
  596. function getTransform() {
  597. wfDeprecated( __METHOD__ );
  598. return false;
  599. }
  600. /**
  601. * Add a message to the cache
  602. *
  603. * @param mixed $key
  604. * @param mixed $value
  605. * @param string $lang The messages language, English by default
  606. */
  607. function addMessage( $key, $value, $lang = 'en' ) {
  608. global $wgContLang;
  609. # Normalise title-case input
  610. $lckey = str_replace( ' ', '_', $wgContLang->lcfirst( $key ) );
  611. $this->mExtensionMessages[$lang][$lckey] = $value;
  612. }
  613. /**
  614. * Add an associative array of message to the cache
  615. *
  616. * @param array $messages An associative array of key => values to be added
  617. * @param string $lang The messages language, English by default
  618. */
  619. function addMessages( $messages, $lang = 'en' ) {
  620. wfProfileIn( __METHOD__ );
  621. if ( !is_array( $messages ) ) {
  622. throw new MWException( __METHOD__.': Invalid message array' );
  623. }
  624. if ( isset( $this->mExtensionMessages[$lang] ) ) {
  625. $this->mExtensionMessages[$lang] = $messages + $this->mExtensionMessages[$lang];
  626. } else {
  627. $this->mExtensionMessages[$lang] = $messages;
  628. }
  629. wfProfileOut( __METHOD__ );
  630. }
  631. /**
  632. * Add a 2-D array of messages by lang. Useful for extensions.
  633. *
  634. * @param array $messages The array to be added
  635. */
  636. function addMessagesByLang( $messages ) {
  637. wfProfileIn( __METHOD__ );
  638. foreach ( $messages as $key => $value ) {
  639. $this->addMessages( $value, $key );
  640. }
  641. wfProfileOut( __METHOD__ );
  642. }
  643. /**
  644. * Get the extension messages for a specific language. Only English, interface
  645. * and content language are guaranteed to be loaded.
  646. *
  647. * @param string $lang The messages language, English by default
  648. */
  649. function getExtensionMessagesFor( $lang = 'en' ) {
  650. wfProfileIn( __METHOD__ );
  651. $messages = array();
  652. if ( isset( $this->mExtensionMessages[$lang] ) ) {
  653. $messages = $this->mExtensionMessages[$lang];
  654. }
  655. if ( $lang != 'en' ) {
  656. $messages = $messages + $this->mExtensionMessages['en'];
  657. }
  658. wfProfileOut( __METHOD__ );
  659. return $messages;
  660. }
  661. /**
  662. * Clear all stored messages. Mainly used after a mass rebuild.
  663. */
  664. function clear() {
  665. if( $this->mUseCache ) {
  666. $langs = Language::getLanguageNames( false );
  667. foreach ( array_keys($langs) as $code ) {
  668. # Global cache
  669. $this->mMemc->delete( wfMemcKey( 'messages', $code ) );
  670. # Invalidate all local caches
  671. $this->mMemc->delete( wfMemcKey( 'messages', $code, 'hash' ) );
  672. }
  673. }
  674. }
  675. function loadAllMessages( $lang = false ) {
  676. global $wgExtensionMessagesFiles;
  677. $key = $lang === false ? '*' : $lang;
  678. if ( isset( $this->mAllMessagesLoaded[$key] ) ) {
  679. return;
  680. }
  681. $this->mAllMessagesLoaded[$key] = true;
  682. # Some extensions will load their messages when you load their class file
  683. wfLoadAllExtensions();
  684. # Others will respond to this hook
  685. wfRunHooks( 'LoadAllMessages' );
  686. # Some register their messages in $wgExtensionMessagesFiles
  687. foreach ( $wgExtensionMessagesFiles as $name => $file ) {
  688. wfLoadExtensionMessages( $name, $lang );
  689. }
  690. # Still others will respond to neither, they are EVIL. We sometimes need to know!
  691. }
  692. /**
  693. * Load messages from a given file
  694. *
  695. * @param string $filename Filename of file to load.
  696. * @param string $langcode Language to load messages for, or false for
  697. * default behvaiour (en, content language and user
  698. * language).
  699. */
  700. function loadMessagesFile( $filename, $langcode = false ) {
  701. global $wgLang, $wgContLang;
  702. wfProfileIn( __METHOD__ );
  703. $messages = $magicWords = false;
  704. require( $filename );
  705. $validCodes = Language::getLanguageNames();
  706. if( is_string( $langcode ) && array_key_exists( $langcode, $validCodes ) ) {
  707. # Load messages for given language code.
  708. $this->processMessagesArray( $messages, $langcode );
  709. } elseif( is_string( $langcode ) && !array_key_exists( $langcode, $validCodes ) ) {
  710. wfDebug( "Invalid language '$langcode' code passed to MessageCache::loadMessagesFile()" );
  711. } else {
  712. # Load only languages that are usually used, and merge all
  713. # fallbacks, except English.
  714. $langs = array_unique( array( 'en', $wgContLang->getCode(), $wgLang->getCode() ) );
  715. foreach( $langs as $code ) {
  716. $this->processMessagesArray( $messages, $code );
  717. }
  718. }
  719. if ( $magicWords !== false ) {
  720. global $wgContLang;
  721. $wgContLang->addMagicWordsByLang( $magicWords );
  722. }
  723. wfProfileOut( __METHOD__ );
  724. }
  725. /**
  726. * Process an array of messages, loading it into the message cache.
  727. *
  728. * @param array $messages Messages array.
  729. * @param string $langcode Language code to process.
  730. */
  731. function processMessagesArray( $messages, $langcode ) {
  732. wfProfileIn( __METHOD__ );
  733. $fallbackCode = $langcode;
  734. $mergedMessages = array();
  735. do {
  736. if ( isset($messages[$fallbackCode]) ) {
  737. $mergedMessages += $messages[$fallbackCode];
  738. }
  739. $fallbackCode = Language::getFallbackfor( $fallbackCode );
  740. } while( $fallbackCode && $fallbackCode !== 'en' );
  741. if ( !empty($mergedMessages) )
  742. $this->addMessages( $mergedMessages, $langcode );
  743. wfProfileOut( __METHOD__ );
  744. }
  745. public function figureMessage( $key ) {
  746. global $wgContLanguageCode;
  747. $pieces = explode( '/', $key );
  748. if( count( $pieces ) < 2 )
  749. return array( $key, $wgContLanguageCode );
  750. $lang = array_pop( $pieces );
  751. $validCodes = Language::getLanguageNames();
  752. if( !array_key_exists( $lang, $validCodes ) )
  753. return array( $key, $wgContLanguageCode );
  754. $message = implode( '/', $pieces );
  755. return array( $message, $lang );
  756. }
  757. }