]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/MessageBlobStore.php
MediaWiki 1.17.4
[autoinstalls/mediawiki.git] / includes / MessageBlobStore.php
1 <?php
2 /**
3  * This program is free software; you can redistribute it and/or modify
4  * it under the terms of the GNU General Public License as published by
5  * the Free Software Foundation; either version 2 of the License, or
6  * (at your option) any later version.
7  *
8  * This program is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  * GNU General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License along
14  * with this program; if not, write to the Free Software Foundation, Inc.,
15  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16  * http://www.gnu.org/copyleft/gpl.html
17  *
18  * @author Roan Kattouw
19  * @author Trevor Parscal
20  */
21
22 /**
23  * This class provides access to the resource message blobs storage used by
24  * the ResourceLoader.
25  *
26  * A message blob is a JSON object containing the interface messages for a
27  * certain resource in a certain language. These message blobs are cached
28  * in the msg_resource table and automatically invalidated when one of their
29  * consistuent messages or the resource itself is changed.
30  */
31 class MessageBlobStore {
32
33         /**
34          * Get the message blobs for a set of modules
35          *
36          * @param $resourceLoader ResourceLoader object
37          * @param $modules array Array of module objects keyed by module name
38          * @param $lang string Language code
39          * @return array An array mapping module names to message blobs
40          */
41         public static function get( ResourceLoader $resourceLoader, $modules, $lang ) {
42                 wfProfileIn( __METHOD__ );
43                 if ( !count( $modules ) ) {
44                         wfProfileOut( __METHOD__ );
45                         return array();
46                 }
47                 // Try getting from the DB first
48                 $blobs = self::getFromDB( $resourceLoader, array_keys( $modules ), $lang );
49
50                 // Generate blobs for any missing modules and store them in the DB
51                 $missing = array_diff( array_keys( $modules ), array_keys( $blobs ) );
52                 foreach ( $missing as $name ) {
53                         $blob = self::insertMessageBlob( $name, $modules[$name], $lang );
54                         if ( $blob ) {
55                                 $blobs[$name] = $blob;
56                         }
57                 }
58
59                 wfProfileOut( __METHOD__ );
60                 return $blobs;
61         }
62
63         /**
64          * Generate and insert a new message blob. If the blob was already
65          * present, it is not regenerated; instead, the preexisting blob
66          * is fetched and returned.
67          *
68          * @param $name String: module name
69          * @param $module ResourceLoaderModule object
70          * @param $lang String: language code
71          * @return mixed Message blob or false if the module has no messages
72          */
73         public static function insertMessageBlob( $name, ResourceLoaderModule $module, $lang ) {
74                 $blob = self::generateMessageBlob( $module, $lang );
75
76                 if ( !$blob ) {
77                         return false;
78                 }
79
80                 $dbw = wfGetDB( DB_MASTER );
81                 $success = $dbw->insert( 'msg_resource', array(
82                                 'mr_lang' => $lang,
83                                 'mr_resource' => $name,
84                                 'mr_blob' => $blob,
85                                 'mr_timestamp' => $dbw->timestamp()
86                         ),
87                         __METHOD__,
88                         array( 'IGNORE' )
89                 );
90
91                 if ( $success ) {
92                         if ( $dbw->affectedRows() == 0 ) {
93                                 // Blob was already present, fetch it
94                                 $blob = $dbw->selectField( 'msg_resource', 'mr_blob', array(
95                                                 'mr_resource' => $name,
96                                                 'mr_lang' => $lang,
97                                         ),
98                                         __METHOD__
99                                 );
100                         } else {
101                                 // Update msg_resource_links
102                                 $rows = array();
103
104                                 foreach ( $module->getMessages() as $key ) {
105                                         $rows[] = array(
106                                                 'mrl_resource' => $name,
107                                                 'mrl_message' => $key
108                                         );
109                                 }
110                                 $dbw->insert( 'msg_resource_links', $rows,
111                                         __METHOD__, array( 'IGNORE' )
112                                 );
113                         }
114                 }
115
116                 return $blob;
117         }
118
119         /**
120          * Update all message blobs for a given module.
121          *
122          * @param $name String: module name
123          * @param $module ResourceLoaderModule object
124          * @param $lang String: language code (optional)
125          * @return Mixed: if $lang is set, the new message blob for that language is 
126          *    returned if present. Otherwise, null is returned.
127          */
128         public static function updateModule( $name, ResourceLoaderModule $module, $lang = null ) {
129                 $retval = null;
130
131                 // Find all existing blobs for this module
132                 $dbw = wfGetDB( DB_MASTER );
133                 $res = $dbw->select( 'msg_resource',
134                         array( 'mr_lang', 'mr_blob' ),
135                         array( 'mr_resource' => $name ),
136                         __METHOD__
137                 );
138
139                 // Build the new msg_resource rows
140                 $newRows = array();
141                 $now = $dbw->timestamp();
142                 // Save the last-processed old and new blobs for later
143                 $oldBlob = $newBlob = null;
144
145                 foreach ( $res as $row ) {
146                         $oldBlob = $row->mr_blob;
147                         $newBlob = self::generateMessageBlob( $module, $row->mr_lang );
148
149                         if ( $row->mr_lang === $lang ) {
150                                 $retval = $newBlob;
151                         }
152                         $newRows[] = array(
153                                 'mr_resource' => $name,
154                                 'mr_lang' => $row->mr_lang,
155                                 'mr_blob' => $newBlob,
156                                 'mr_timestamp' => $now
157                         );
158                 }
159
160                 $dbw->replace( 'msg_resource',
161                         array( array( 'mr_resource', 'mr_lang' ) ),
162                         $newRows, __METHOD__
163                 );
164
165                 // Figure out which messages were added and removed
166                 $oldMessages = array_keys( FormatJson::decode( $oldBlob, true ) );
167                 $newMessages = array_keys( FormatJson::decode( $newBlob, true ) );
168                 $added = array_diff( $newMessages, $oldMessages );
169                 $removed = array_diff( $oldMessages, $newMessages );
170
171                 // Delete removed messages, insert added ones
172                 if ( $removed ) {
173                         $dbw->delete( 'msg_resource_links', array(
174                                         'mrl_resource' => $name,
175                                         'mrl_message' => $removed
176                                 ), __METHOD__
177                         );
178                 }
179
180                 $newLinksRows = array();
181
182                 foreach ( $added as $message ) {
183                         $newLinksRows[] = array(
184                                 'mrl_resource' => $name,
185                                 'mrl_message' => $message
186                         );
187                 }
188
189                 if ( $newLinksRows ) {
190                         $dbw->insert( 'msg_resource_links', $newLinksRows, __METHOD__,
191                                  array( 'IGNORE' ) // just in case
192                         );
193                 }
194
195                 return $retval;
196         }
197
198         /**
199          * Update a single message in all message blobs it occurs in.
200          *
201          * @param $key String: message key
202          */
203         public static function updateMessage( $key ) {
204                 $dbw = wfGetDB( DB_MASTER );
205
206                 // Keep running until the updates queue is empty.
207                 // Due to update conflicts, the queue might not be emptied
208                 // in one iteration.
209                 $updates = null;
210                 do {
211                         $updates = self::getUpdatesForMessage( $key, $updates );
212
213                         foreach ( $updates as $k => $update ) {
214                                 // Update the row on the condition that it
215                                 // didn't change since we fetched it by putting
216                                 // the timestamp in the WHERE clause.
217                                 $success = $dbw->update( 'msg_resource',
218                                         array(
219                                                 'mr_blob' => $update['newBlob'],
220                                                 'mr_timestamp' => $dbw->timestamp() ),
221                                         array(
222                                                 'mr_resource' => $update['resource'],
223                                                 'mr_lang' => $update['lang'],
224                                                 'mr_timestamp' => $update['timestamp'] ),
225                                         __METHOD__
226                                 );
227
228                                 // Only requeue conflicted updates.
229                                 // If update() returned false, don't retry, for
230                                 // fear of getting into an infinite loop
231                                 if ( !( $success && $dbw->affectedRows() == 0 ) ) {
232                                         // Not conflicted
233                                         unset( $updates[$k] );
234                                 }
235                         }
236                 } while ( count( $updates ) );
237
238                 // No need to update msg_resource_links because we didn't add
239                 // or remove any messages, we just changed their contents.
240         }
241
242         public static function clear() {
243                 // TODO: Give this some more thought
244                 // TODO: Is TRUNCATE better?
245                 $dbw = wfGetDB( DB_MASTER );
246                 $dbw->delete( 'msg_resource', '*', __METHOD__ );
247                 $dbw->delete( 'msg_resource_links', '*', __METHOD__ );
248         }
249
250         /**
251          * Create an update queue for updateMessage()
252          *
253          * @param $key String: message key
254          * @param $prevUpdates Array: updates queue to refresh or null to build a fresh update queue
255          * @return Array: updates queue
256          */
257         private static function getUpdatesForMessage( $key, $prevUpdates = null ) {
258                 $dbw = wfGetDB( DB_MASTER );
259
260                 if ( is_null( $prevUpdates ) ) {
261                         // Fetch all blobs referencing $key
262                         $res = $dbw->select(
263                                 array( 'msg_resource', 'msg_resource_links' ),
264                                 array( 'mr_resource', 'mr_lang', 'mr_blob', 'mr_timestamp' ),
265                                 array( 'mrl_message' => $key, 'mr_resource=mrl_resource' ),
266                                 __METHOD__
267                         );
268                 } else {
269                         // Refetch the blobs referenced by $prevUpdates
270
271                         // Reorganize the (resource, lang) pairs in the format
272                         // expected by makeWhereFrom2d()
273                         $twoD = array();
274
275                         foreach ( $prevUpdates as $update ) {
276                                 $twoD[$update['resource']][$update['lang']] = true;
277                         }
278
279                         $res = $dbw->select( 'msg_resource',
280                                 array( 'mr_resource', 'mr_lang', 'mr_blob', 'mr_timestamp' ),
281                                 $dbw->makeWhereFrom2d( $twoD, 'mr_resource', 'mr_lang' ),
282                                 __METHOD__
283                         );
284                 }
285
286                 // Build the new updates queue
287                 $updates = array();
288
289                 foreach ( $res as $row ) {
290                         $updates[] = array(
291                                 'resource' => $row->mr_resource,
292                                 'lang' => $row->mr_lang,
293                                 'timestamp' => $row->mr_timestamp,
294                                 'newBlob' => self::reencodeBlob( $row->mr_blob, $key, $row->mr_lang )
295                         );
296                 }
297
298                 return $updates;
299         }
300
301         /**
302          * Reencode a message blob with the updated value for a message
303          *
304          * @param $blob String: message blob (JSON object)
305          * @param $key String: message key
306          * @param $lang String: language code
307          * @return Message blob with $key replaced with its new value
308          */
309         private static function reencodeBlob( $blob, $key, $lang ) {
310                 $decoded = FormatJson::decode( $blob, true );
311                 $decoded[$key] = wfMsgExt( $key, array( 'language' => $lang ) );
312
313                 return FormatJson::encode( (object)$decoded );
314         }
315
316         /**
317          * Get the message blobs for a set of modules from the database.
318          * Modules whose blobs are not in the database are silently dropped.
319          *
320          * @param $resourceLoader ResourceLoader object
321          * @param $modules Array of module names
322          * @param $lang String: language code
323          * @return array Array mapping module names to blobs
324          */
325         private static function getFromDB( ResourceLoader $resourceLoader, $modules, $lang ) {
326                 global $wgCacheEpoch;
327                 $retval = array();
328                 $dbr = wfGetDB( DB_SLAVE );
329                 $res = $dbr->select( 'msg_resource',
330                         array( 'mr_blob', 'mr_resource', 'mr_timestamp' ),
331                         array( 'mr_resource' => $modules, 'mr_lang' => $lang ),
332                         __METHOD__
333                 );
334
335                 foreach ( $res as $row ) {
336                         $module = $resourceLoader->getModule( $row->mr_resource );
337                         if ( !$module ) {
338                                 // This shouldn't be possible
339                                 throw new MWException( __METHOD__ . ' passed an invalid module name' );
340                         }
341                         // Update the module's blobs if the set of messages changed or if the blob is
342                         // older than $wgCacheEpoch
343                         if ( array_keys( FormatJson::decode( $row->mr_blob, true ) ) !== $module->getMessages() ||
344                                         wfTimestamp( TS_MW, $row->mr_timestamp ) <= $wgCacheEpoch ) {
345                                 $retval[$row->mr_resource] = self::updateModule( $row->mr_resource, $module, $lang );
346                         } else {
347                                 $retval[$row->mr_resource] = $row->mr_blob;
348                         }
349                 }
350
351                 return $retval;
352         }
353
354         /**
355          * Generate the message blob for a given module in a given language.
356          *
357          * @param $module ResourceLoaderModule object
358          * @param $lang String: language code
359          * @return String: JSON object
360          */
361         private static function generateMessageBlob( ResourceLoaderModule $module, $lang ) {
362                 $messages = array();
363
364                 foreach ( $module->getMessages() as $key ) {
365                         $messages[$key] = wfMsgExt( $key, array( 'language' => $lang ) );
366                 }
367
368                 return FormatJson::encode( (object)$messages );
369         }
370 }