a02bd6255eb5f57b733e7e24c9a78de03f0179f9
[lhc/web/wiklou.git] / includes / MessageCache.php
1 <?php
2 /**
3 *
4 * @package MediaWiki
5 * @subpackage Cache
6 */
7
8 /** */
9 require_once( 'Revision.php' );
10
11 /**
12 *
13 */
14 define( 'MSG_LOAD_TIMEOUT', 60);
15 define( 'MSG_LOCK_TIMEOUT', 10);
16 define( 'MSG_WAIT_TIMEOUT', 10);
17
18 /**
19 * Message cache
20 * Performs various useful MediaWiki namespace-related functions
21 *
22 * @package MediaWiki
23 */
24 class MessageCache
25 {
26 var $mCache, $mUseCache, $mDisable, $mExpiry;
27 var $mMemcKey, $mKeys, $mParserOptions, $mParser;
28 var $mExtensionMessages = array();
29 var $mInitialised = false;
30 var $mDeferred = true;
31
32 function initialise( &$memCached, $useDB, $expiry, $memcPrefix) {
33 global $wgLocalMessageCache;
34 $fname = 'MessageCache::initialise';
35 wfProfileIn( $fname );
36
37 $this->mUseCache = !is_null( $memCached );
38 $this->mMemc = &$memCached;
39 $this->mDisable = !$useDB;
40 $this->mExpiry = $expiry;
41 $this->mDisableTransform = false;
42 $this->mMemcKey = $memcPrefix.':messages';
43 $this->mKeys = false; # initialised on demand
44 $this->mInitialised = true;
45
46 wfProfileIn( $fname.'-parseropt' );
47 $this->mParserOptions = ParserOptions::newFromUser( $u=NULL );
48 wfProfileOut( $fname.'-parseropt' );
49 wfProfileIn( $fname.'-parser' );
50 $this->mParser = new Parser;
51 wfProfileOut( $fname.'-parser' );
52
53 # When we first get asked for a message,
54 # then we'll fill up the cache. If we
55 # can return a cache hit, this saves
56 # some extra milliseconds
57 $this->mDeferred = true;
58
59 wfProfileOut( $fname );
60 }
61
62 /**
63 * Try to load the cache from a local file
64 */
65 function loadFromLocal( $hash ) {
66 global $wgLocalMessageCache, $wgDBname;
67
68 $this->mCache = false;
69 if ( $wgLocalMessageCache === false ) {
70 return;
71 }
72
73 $filename = "$wgLocalMessageCache/messages-$wgDBname";
74
75 $file = fopen( $filename, 'r' );
76 if ( !$file ) {
77 return;
78 }
79
80 // Check to see if the file has the hash specified
81 $localHash = fread( $file, 32 );
82 if ( $hash == $localHash ) {
83 // All good, get the rest of it
84 $serialized = fread( $file, 1000000 );
85 $this->mCache = unserialize( $serialized );
86 }
87 fclose( $file );
88 }
89
90 /**
91 * Save the cache to a local file
92 */
93 function saveToLocal( $serialized, $hash ) {
94 global $wgLocalMessageCache, $wgDBname;
95
96 if ( $wgLocalMessageCache === false ) {
97 return;
98 }
99
100 $filename = "$wgLocalMessageCache/messages-$wgDBname";
101 wfMkdirParents( $wgLocalMessageCache, 0777 );
102
103 $file = fopen( $filename, 'w' );
104 if ( !$file ) {
105 wfDebug( "Unable to open local cache file for writing\n" );
106 return;
107 }
108
109 fwrite( $file, $hash . $serialized );
110 fclose( $file );
111 }
112
113
114 /**
115 * Loads messages either from memcached or the database, if not disabled
116 * On error, quietly switches to a fallback mode
117 * Returns false for a reportable error, true otherwise
118 */
119 function load() {
120 global $wgAllMessagesEn, $wgLocalMessageCache;
121
122 if ( $this->mDisable ) {
123 static $shownDisabled = false;
124 if ( !$shownDisabled ) {
125 wfDebug( "MessageCache::load(): disabled\n" );
126 $shownDisabled = true;
127 }
128 return true;
129 }
130 $fname = 'MessageCache::load';
131 wfProfileIn( $fname );
132 $success = true;
133
134 if ( $this->mUseCache ) {
135 $this->mCache = false;
136
137 # Try local cache
138 wfProfileIn( $fname.'-fromlocal' );
139 $hash = $this->mMemc->get( "{$this->mMemcKey}-hash" );
140 if ( $hash ) {
141 $this->loadFromLocal( $hash );
142 }
143 wfProfileOut( $fname.'-fromlocal' );
144
145 # Try memcached
146 if ( !$this->mCache ) {
147 wfProfileIn( $fname.'-fromcache' );
148 $this->mCache = $this->mMemc->get( $this->mMemcKey );
149
150 # Save to local cache
151 if ( $wgLocalMessageCache !== false ) {
152 $serialized = serialize( $this->mCache );
153 if ( !$hash ) {
154 $hash = md5( $serialized );
155 $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry );
156 }
157 $this->saveToLocal( $serialized, $hash );
158 }
159 wfProfileOut( $fname.'-fromcache' );
160 }
161
162
163 # If there's nothing in memcached, load all the messages from the database
164 if ( !$this->mCache ) {
165 wfDebug( "MessageCache::load(): loading all messages\n" );
166 $this->lock();
167 # Other threads don't need to load the messages if another thread is doing it.
168 $success = $this->mMemc->add( $this->mMemcKey.'-status', "loading", MSG_LOAD_TIMEOUT );
169 if ( $success ) {
170 wfProfileIn( $fname.'-load' );
171 $this->loadFromDB();
172 wfProfileOut( $fname.'-load' );
173
174 # Save in memcached
175 # Keep trying if it fails, this is kind of important
176 wfProfileIn( $fname.'-save' );
177 for ($i=0; $i<20 &&
178 !$this->mMemc->set( $this->mMemcKey, $this->mCache, $this->mExpiry );
179 $i++ ) {
180 usleep(mt_rand(500000,1500000));
181 }
182
183 # Save to local cache
184 if ( $wgLocalMessageCache !== false ) {
185 $serialized = serialize( $this->mCache );
186 $hash = md5( $serialized );
187 $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry );
188 $this->saveToLocal( $serialized, $hash );
189 }
190
191 wfProfileOut( $fname.'-save' );
192 if ( $i == 20 ) {
193 $this->mMemc->set( $this->mMemcKey.'-status', 'error', 60*5 );
194 wfDebug( "MemCached set error in MessageCache: restart memcached server!\n" );
195 }
196 }
197 $this->unlock();
198 }
199
200 if ( !is_array( $this->mCache ) ) {
201 wfDebug( "MessageCache::load(): individual message mode\n" );
202 # If it is 'loading' or 'error', switch to individual message mode, otherwise disable
203 # Causing too much DB load, disabling -- TS
204 $this->mDisable = true;
205 /*
206 if ( $this->mCache == "loading" ) {
207 $this->mUseCache = false;
208 } elseif ( $this->mCache == "error" ) {
209 $this->mUseCache = false;
210 $success = false;
211 } else {
212 $this->mDisable = true;
213 $success = false;
214 }*/
215 $this->mCache = false;
216 }
217 }
218 wfProfileOut( $fname );
219 $this->mDeferred = false;
220 return $success;
221 }
222
223 /**
224 * Loads all or main part of cacheable messages from the database
225 */
226 function loadFromDB() {
227 global $wgAllMessagesEn, $wgLang;
228
229 $fname = 'MessageCache::loadFromDB';
230 $dbr =& wfGetDB( DB_SLAVE );
231 if ( !$dbr ) {
232 wfDebugDieBacktrace( 'Invalid database object' );
233 }
234 $conditions = array( 'page_is_redirect' => 0,
235 'page_namespace' => NS_MEDIAWIKI);
236 $res = $dbr->select( array( 'page', 'revision', 'text' ),
237 array( 'page_title', 'old_text', 'old_flags' ),
238 'page_is_redirect=0 AND page_namespace='.NS_MEDIAWIKI.' AND page_latest=rev_id AND rev_text_id=old_id',
239 $fname
240 );
241
242 $this->mCache = array();
243 for ( $row = $dbr->fetchObject( $res ); $row; $row = $dbr->fetchObject( $res ) ) {
244 $this->mCache[$row->page_title] = Revision::getRevisionText( $row );
245 }
246
247 # Negative caching
248 # Go through the language array and the extension array and make a note of
249 # any keys missing from the cache
250 foreach ( $wgAllMessagesEn as $key => $value ) {
251 $uckey = $wgLang->ucfirst( $key );
252 if ( !array_key_exists( $uckey, $this->mCache ) ) {
253 $this->mCache[$uckey] = false;
254 }
255 }
256 foreach ( $this->mExtensionMessages as $key => $value ) {
257 $uckey = $wgLang->ucfirst( $key );
258 if ( !array_key_exists( $uckey, $this->mCache ) ) {
259 $this->mCache[$uckey] = false;
260 }
261 }
262
263 $dbr->freeResult( $res );
264 }
265
266 /**
267 * Not really needed anymore
268 */
269 function getKeys() {
270 global $wgAllMessagesEn, $wgContLang;
271 if ( !$this->mKeys ) {
272 $this->mKeys = array();
273 foreach ( $wgAllMessagesEn as $key => $value ) {
274 $title = $wgContLang->ucfirst( $key );
275 array_push( $this->mKeys, $title );
276 }
277 }
278 return $this->mKeys;
279 }
280
281 /**
282 * @deprecated
283 */
284 function isCacheable( $key ) {
285 return true;
286 }
287
288 function replace( $title, $text ) {
289 global $wgLocalMessageCache;
290
291 $this->lock();
292 $this->load();
293 if ( is_array( $this->mCache ) ) {
294 $this->mCache[$title] = $text;
295 $this->mMemc->set( $this->mMemcKey, $this->mCache, $this->mExpiry );
296
297 # Save to local cache
298 if ( $wgLocalMessageCache !== false ) {
299 $serialized = serialize( $this->mCache );
300 $hash = md5( $serialized );
301 $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry );
302 $this->saveToLocal( $serialized, $hash );
303 }
304
305
306 }
307 $this->unlock();
308 }
309
310 /**
311 * Returns success
312 * Represents a write lock on the messages key
313 */
314 function lock() {
315 if ( !$this->mUseCache ) {
316 return true;
317 }
318
319 $lockKey = $this->mMemcKey . 'lock';
320 for ($i=0; $i < MSG_WAIT_TIMEOUT && !$this->mMemc->add( $lockKey, 1, MSG_LOCK_TIMEOUT ); $i++ ) {
321 sleep(1);
322 }
323
324 return $i >= MSG_WAIT_TIMEOUT;
325 }
326
327 function unlock() {
328 if ( !$this->mUseCache ) {
329 return;
330 }
331
332 $lockKey = $this->mMemcKey . 'lock';
333 $this->mMemc->delete( $lockKey );
334 }
335
336 function get( $key, $useDB, $forcontent=true, $isfullkey = false ) {
337 global $wgContLanguageCode;
338 if( $forcontent ) {
339 global $wgContLang;
340 $lang =& $wgContLang;
341 $langcode = $wgContLanguageCode;
342 } else {
343 global $wgLang, $wgLanguageCode;
344 $lang =& $wgLang;
345 $langcode = $wgLanguageCode;
346 }
347 # If uninitialised, someone is trying to call this halfway through Setup.php
348 if( !$this->mInitialised ) {
349 return '&lt;' . htmlspecialchars($key) . '&gt;';
350 }
351 # If cache initialization was deferred, start it now.
352 if( $this->mDeferred && !$this->mDisable && $useDB ) {
353 $this->load();
354 }
355
356 $message = false;
357 if( !$this->mDisable && $useDB ) {
358 $title = $lang->ucfirst( $key );
359 if(!$isfullkey && ($langcode != $wgContLanguageCode) ) {
360 $title .= '/' . $langcode;
361 }
362 $message = $this->getFromCache( $title );
363 }
364 # Try the extension array
365 if( $message === false && array_key_exists( $key, $this->mExtensionMessages ) ) {
366 $message = $this->mExtensionMessages[$key];
367 }
368
369 # Try the array in the language object
370 if( $message === false ) {
371 wfSuppressWarnings();
372 $message = $lang->getMessage( $key );
373 wfRestoreWarnings();
374 if ( is_null( $message ) ) {
375 $message = false;
376 }
377 }
378
379 # Try the English array
380 if( $message === false && $langcode != 'en' ) {
381 wfSuppressWarnings();
382 $message = Language::getMessage( $key );
383 wfRestoreWarnings();
384 if ( is_null( $message ) ) {
385 $message = false;
386 }
387 }
388
389 # Is this a custom message? Try the default language in the db...
390 if( $message === false &&
391 !$this->mDisable && $useDB &&
392 !$isfullkey && ($langcode != $wgContLanguageCode) ) {
393 $message = $this->getFromCache( $lang->ucfirst( $key ) );
394 }
395
396 # Final fallback
397 if( $message === false ) {
398 return '&lt;' . htmlspecialchars($key) . '&gt;';
399 }
400
401 # Replace brace tags
402 $message = $this->transform( $message );
403 return $message;
404 }
405
406 function getFromCache( $title ) {
407 $message = false;
408
409 # Try the cache
410 if( $this->mUseCache && is_array( $this->mCache ) && array_key_exists( $title, $this->mCache ) ) {
411 return $this->mCache[$title];
412 }
413
414 # Try individual message cache
415 if ( $this->mUseCache ) {
416 $message = $this->mMemc->get( $this->mMemcKey . ':' . $title );
417 if ( $message == '###NONEXISTENT###' ) {
418 return false;
419 } elseif( !is_null( $message ) ) {
420 $this->mCache[$title] = $message;
421 return $message;
422 } else {
423 $message = false;
424 }
425 }
426
427 # If it wasn't in the cache, load each message from the DB individually
428 $revision = Revision::newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) );
429 if( $revision ) {
430 $message = $revision->getText();
431 if ($this->mUseCache) {
432 $this->mCache[$title]=$message;
433 /* individual messages may be often
434 recached until proper purge code exists
435 */
436 $this->mMemc->set( $this->mMemcKey . ':' . $title, $message, 300 );
437 }
438 } else {
439 # Negative caching
440 # Use some special text instead of false, because false gets converted to '' somewhere
441 $this->mMemc->set( $this->mMemcKey . ':' . $title, '###NONEXISTENT###', $this->mExpiry );
442 }
443
444 return $message;
445 }
446
447 function transform( $message ) {
448 if( !$this->mDisableTransform ) {
449 if( strpos( $message, '{{' ) !== false ) {
450 $message = $this->mParser->transformMsg( $message, $this->mParserOptions );
451 }
452 }
453 return $message;
454 }
455
456 function disable() { $this->mDisable = true; }
457 function enable() { $this->mDisable = false; }
458 function disableTransform() { $this->mDisableTransform = true; }
459 function enableTransform() { $this->mDisableTransform = false; }
460
461 /**
462 * Add a message to the cache
463 *
464 * @param mixed $key
465 * @param mixed $value
466 */
467 function addMessage( $key, $value ) {
468 $this->mExtensionMessages[$key] = $value;
469 }
470
471 /**
472 * Add an associative array of message to the cache
473 *
474 * @param array $messages An associative array of key => values to be added
475 */
476 function addMessages( $messages ) {
477 foreach ( $messages as $key => $value ) {
478 $this->addMessage( $key, $value );
479 }
480 }
481
482 /**
483 * Clear all stored messages. Mainly used after a mass rebuild.
484 */
485 function clear() {
486 if( $this->mUseCache ) {
487 $this->mMemc->delete( $this->mMemcKey );
488 }
489 }
490 }
491 ?>