Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
92.65% |
63 / 68 |
|
92.31% |
12 / 13 |
CRAP | |
0.00% |
0 / 1 |
MessageBlobStore | |
92.65% |
63 / 68 |
|
92.31% |
12 / 13 |
25.25 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
setLogger | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getBlob | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getBlobs | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
6 | |||
makeGlobalPurgeKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
makeModulePurgeKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
makeBlobCacheKey | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
recacheMessageBlob | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
updateMessage | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
clear | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
clearGlobalCacheEntry | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
fetchMessage | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
generateMessageBlob | |
61.54% |
8 / 13 |
|
0.00% |
0 / 1 |
4.91 |
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 | * @file |
19 | * @author Roan Kattouw |
20 | * @author Trevor Parscal |
21 | */ |
22 | |
23 | namespace MediaWiki\ResourceLoader; |
24 | |
25 | use FormatJson; |
26 | use MediaWiki\MediaWikiServices; |
27 | use Psr\Log\LoggerAwareInterface; |
28 | use Psr\Log\LoggerInterface; |
29 | use Psr\Log\NullLogger; |
30 | use WANObjectCache; |
31 | use Wikimedia\Rdbms\Database; |
32 | |
33 | /** |
34 | * This class generates message blobs for use by ResourceLoader. |
35 | * |
36 | * A message blob is a JSON object containing the interface messages for a |
37 | * certain module in a certain language. |
38 | * |
39 | * @ingroup ResourceLoader |
40 | * @since 1.17 |
41 | */ |
42 | class MessageBlobStore implements LoggerAwareInterface { |
43 | /** @var ResourceLoader */ |
44 | private $resourceloader; |
45 | |
46 | /** @var LoggerInterface */ |
47 | protected $logger; |
48 | |
49 | /** @var WANObjectCache */ |
50 | protected $wanCache; |
51 | |
52 | /** |
53 | * @param ResourceLoader $rl |
54 | * @param LoggerInterface|null $logger |
55 | * @param WANObjectCache|null $wanObjectCache |
56 | */ |
57 | public function __construct( |
58 | ResourceLoader $rl, |
59 | ?LoggerInterface $logger, |
60 | ?WANObjectCache $wanObjectCache |
61 | ) { |
62 | $this->resourceloader = $rl; |
63 | $this->logger = $logger ?: new NullLogger(); |
64 | |
65 | // NOTE: when changing this assignment, make sure the code in the instantiator for |
66 | // LocalisationCache which calls MessageBlobStore::clearGlobalCacheEntry() uses the |
67 | // same cache object. |
68 | $this->wanCache = $wanObjectCache ?: MediaWikiServices::getInstance() |
69 | ->getMainWANObjectCache(); |
70 | } |
71 | |
72 | /** |
73 | * @since 1.27 |
74 | * @param LoggerInterface $logger |
75 | */ |
76 | public function setLogger( LoggerInterface $logger ) { |
77 | $this->logger = $logger; |
78 | } |
79 | |
80 | /** |
81 | * Get the message blob for a module |
82 | * |
83 | * @since 1.27 |
84 | * @param Module $module |
85 | * @param string $lang Language code |
86 | * @return string JSON |
87 | */ |
88 | public function getBlob( Module $module, $lang ) { |
89 | $blobs = $this->getBlobs( [ $module->getName() => $module ], $lang ); |
90 | return $blobs[$module->getName()]; |
91 | } |
92 | |
93 | /** |
94 | * Get the message blobs for a set of modules |
95 | * |
96 | * @since 1.27 |
97 | * @param Module[] $modules Array of module objects keyed by name |
98 | * @param string $lang Language code |
99 | * @return string[] An array mapping module names to message blobs |
100 | */ |
101 | public function getBlobs( array $modules, $lang ) { |
102 | // Each cache key for a message blob by module name and language code also has a generic |
103 | // check key without language code. This is used to invalidate any and all language subkeys |
104 | // that exist for a module from the updateMessage() method. |
105 | $checkKeys = [ |
106 | self::makeGlobalPurgeKey( $this->wanCache ) |
107 | ]; |
108 | $cacheKeys = []; |
109 | foreach ( $modules as $name => $module ) { |
110 | $cacheKey = $this->makeBlobCacheKey( $name, $lang, $module ); |
111 | $cacheKeys[$name] = $cacheKey; |
112 | $checkKeys[$cacheKey][] = $this->makeModulePurgeKey( $name ); |
113 | } |
114 | $curTTLs = []; |
115 | $result = $this->wanCache->getMulti( array_values( $cacheKeys ), $curTTLs, $checkKeys ); |
116 | |
117 | $blobs = []; |
118 | foreach ( $modules as $name => $module ) { |
119 | $key = $cacheKeys[$name]; |
120 | if ( !isset( $result[$key] ) || $curTTLs[$key] === null || $curTTLs[$key] < 0 ) { |
121 | $blobs[$name] = $this->recacheMessageBlob( $key, $module, $lang ); |
122 | } else { |
123 | // Use unexpired cache |
124 | $blobs[$name] = $result[$key]; |
125 | } |
126 | } |
127 | return $blobs; |
128 | } |
129 | |
130 | /** |
131 | * Global check key for ::clear() |
132 | * |
133 | * @param WANObjectCache $cache |
134 | * @return string Cache key |
135 | */ |
136 | private static function makeGlobalPurgeKey( WANObjectCache $cache ) { |
137 | return $cache->makeGlobalKey( 'resourceloader-messageblob' ); |
138 | } |
139 | |
140 | /** |
141 | * Per-module check key, for ::updateMessage() |
142 | * |
143 | * @param string $name |
144 | * @return string Cache key |
145 | */ |
146 | private function makeModulePurgeKey( $name ) { |
147 | return $this->wanCache->makeKey( 'resourceloader-messageblob', $name ); |
148 | } |
149 | |
150 | /** |
151 | * @param string $name |
152 | * @param string $lang |
153 | * @param Module $module |
154 | * @return string Cache key |
155 | */ |
156 | private function makeBlobCacheKey( $name, $lang, Module $module ) { |
157 | $messages = array_values( array_unique( $module->getMessages() ) ); |
158 | sort( $messages ); |
159 | return $this->wanCache->makeKey( 'resourceloader-messageblob', |
160 | $name, |
161 | $lang, |
162 | md5( json_encode( $messages ) ) |
163 | ); |
164 | } |
165 | |
166 | /** |
167 | * @since 1.27 |
168 | * @param string $cacheKey |
169 | * @param Module $module |
170 | * @param string $lang |
171 | * @return string JSON blob |
172 | */ |
173 | protected function recacheMessageBlob( $cacheKey, Module $module, $lang ) { |
174 | $blob = $this->generateMessageBlob( $module, $lang ); |
175 | $cache = $this->wanCache; |
176 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
177 | $cache->set( $cacheKey, $blob, |
178 | // Add part of a day to TTL to avoid all modules expiring at once |
179 | $cache::TTL_WEEK + mt_rand( 0, $cache::TTL_DAY ), |
180 | Database::getCacheSetOptions( $dbr ) |
181 | ); |
182 | return $blob; |
183 | } |
184 | |
185 | /** |
186 | * Invalidate cache keys for modules using this message key. |
187 | * Called by MessageCache when a message has changed. |
188 | * |
189 | * @param string $key Message key |
190 | */ |
191 | public function updateMessage( $key ): void { |
192 | $moduleNames = $this->resourceloader->getModulesByMessage( $key ); |
193 | foreach ( $moduleNames as $moduleName ) { |
194 | // Use the default holdoff TTL to account for database replica DB lag |
195 | // which can affect MessageCache. |
196 | $this->wanCache->touchCheckKey( $this->makeModulePurgeKey( $moduleName ) ); |
197 | } |
198 | } |
199 | |
200 | /** |
201 | * Invalidate cache keys for all known modules. |
202 | * |
203 | * Used by purgeMessageBlobStore.php |
204 | */ |
205 | public function clear() { |
206 | self::clearGlobalCacheEntry( $this->wanCache ); |
207 | } |
208 | |
209 | /** |
210 | * Invalidate cache keys for all known modules. |
211 | * |
212 | * Used by LocalisationCache and DatabaseUpdater after regenerating l10n cache. |
213 | * |
214 | * @param WANObjectCache $cache |
215 | */ |
216 | public static function clearGlobalCacheEntry( WANObjectCache $cache ) { |
217 | // Disable holdoff TTL because: |
218 | // - LocalisationCache is populated by messages on-disk and don't have DB lag, |
219 | // thus there is no need for hold off. We only clear it after new localisation |
220 | // updates are known to be deployed to all servers. |
221 | // - This global check key invalidates message blobs for all modules for all wikis |
222 | // in cache contexts (e.g. languages, skins). Setting a hold-off on this key could |
223 | // cause a cache stampede since no values would be stored for several seconds. |
224 | $cache->touchCheckKey( self::makeGlobalPurgeKey( $cache ), $cache::HOLDOFF_TTL_NONE ); |
225 | } |
226 | |
227 | /** |
228 | * @since 1.27 |
229 | * @param string $key Message key |
230 | * @param string $lang Language code |
231 | * @return string|null |
232 | */ |
233 | protected function fetchMessage( $key, $lang ) { |
234 | $message = wfMessage( $key )->inLanguage( $lang ); |
235 | if ( !$message->exists() ) { |
236 | $this->logger->warning( 'Failed to find {messageKey} ({lang})', [ |
237 | 'messageKey' => $key, |
238 | 'lang' => $lang, |
239 | ] ); |
240 | $value = null; |
241 | } else { |
242 | $value = $message->plain(); |
243 | } |
244 | return $value; |
245 | } |
246 | |
247 | /** |
248 | * Generate the message blob for a given module in a given language. |
249 | * |
250 | * @param Module $module |
251 | * @param string $lang Language code |
252 | * @return string JSON blob |
253 | */ |
254 | private function generateMessageBlob( Module $module, $lang ) { |
255 | $messages = []; |
256 | foreach ( $module->getMessages() as $key ) { |
257 | $value = $this->fetchMessage( $key, $lang ); |
258 | // If the message does not exist, omit it from the blob so that |
259 | // client-side mw.message may do its own existence handling. |
260 | if ( $value !== null ) { |
261 | $messages[$key] = $value; |
262 | } |
263 | } |
264 | |
265 | $json = FormatJson::encode( (object)$messages, false, FormatJson::UTF8_OK ); |
266 | // @codeCoverageIgnoreStart |
267 | if ( $json === false ) { |
268 | $this->logger->warning( 'Failed to encode message blob for {module} ({lang})', [ |
269 | 'module' => $module->getName(), |
270 | 'lang' => $lang, |
271 | ] ); |
272 | $json = '{}'; |
273 | } |
274 | // codeCoverageIgnoreEnd |
275 | return $json; |
276 | } |
277 | } |