Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
MessageGroupCache.php
Go to the documentation of this file.
1<?php
8use Cdb\Reader;
9use Cdb\Writer;
10
21 public const NO_SOURCE = 1;
22 public const NO_CACHE = 2;
23 public const CHANGED = 3;
24 private const VERSION = '4';
26 protected $group;
28 protected $cache;
30 protected $code;
32 private $cacheFilePath;
33
40 public function __construct(
42 string $code,
43 string $cacheFilePath
44 ) {
45 $this->group = $group;
46 $this->code = $code;
47 $this->cacheFilePath = $cacheFilePath;
48 }
49
54 public function exists() {
55 return file_exists( $this->getCacheFilePath() );
56 }
57
62 public function getKeys() {
63 $reader = $this->open();
64 $keys = [];
65
66 $key = $reader->firstkey();
67 while ( $key !== false ) {
68 if ( ( $key[0] ?? '' ) !== '#' ) {
69 $keys[] = $key;
70 }
71
72 $key = $reader->nextkey();
73 }
74
75 return $keys;
76 }
77
82 public function getTimestamp() {
83 return $this->open()->get( '#created' );
84 }
85
90 public function getUpdateTimestamp() {
91 return $this->open()->get( '#updated' );
92 }
93
99 public function get( $key ) {
100 return $this->open()->get( $key );
101 }
102
108 public function getAuthors(): array {
109 $cache = $this->open();
110 return $cache->exists( '#authors' ) ?
111 $this->unserialize( $cache->get( '#authors' ) ) : [];
112 }
113
119 public function getExtra(): array {
120 $cache = $this->open();
121 return $cache->exists( '#extra' ) ? $this->unserialize( $cache->get( '#extra' ) ) : [];
122 }
123
128 public function create( $created = false ) {
129 $this->close(); // Close the reader instance just to be sure
130
131 $parseOutput = $this->group->parseExternal( $this->code );
132 $messages = $parseOutput['MESSAGES'];
133 if ( $messages === [] ) {
134 if ( $this->exists() ) {
135 // Delete stale cache files
136 unlink( $this->getCacheFilePath() );
137 }
138
139 return; // Don't create empty caches
140 }
141 $hash = md5( file_get_contents( $this->group->getSourceFilePath( $this->code ) ) );
142
143 wfMkdirParents( dirname( $this->getCacheFilePath() ) );
144 $cache = Writer::open( $this->getCacheFilePath() );
145
146 foreach ( $messages as $key => $value ) {
147 $cache->set( $key, $value );
148 }
149 $cache->set( '#authors', $this->serialize( $parseOutput['AUTHORS'] ) );
150 $cache->set( '#extra', $this->serialize( $parseOutput['EXTRA'] ) );
151 $cache->set( '#created', $created ?: wfTimestamp() );
152 $cache->set( '#updated', wfTimestamp() );
153 $cache->set( '#filehash', $hash );
154 $cache->set( '#msghash', md5( serialize( $parseOutput ) ) );
155 $cache->set( '#version', self::VERSION );
156 $cache->close();
157 }
158
166 public function isValid( &$reason ) {
167 $group = $this->group;
168 $pattern = $group->getSourceFilePath( '*' );
169 $filename = $group->getSourceFilePath( $this->code );
170
171 $parseOutput = null;
172
173 // If the file pattern is not dependent on the language, we will assume
174 // that all translations are stored in one file. This means we need to
175 // actually parse the file to know if a language is present.
176 if ( !str_contains( $pattern, '*' ) ) {
177 $parseOutput = $group->parseExternal( $this->code );
178 $source = $parseOutput['MESSAGES'] !== [];
179 } else {
180 static $globCache = [];
181 if ( !isset( $globCache[$pattern] ) ) {
182 $globCache[$pattern] = array_flip( glob( $pattern, GLOB_NOESCAPE ) );
183 // Definition file might not match the above pattern
184 $globCache[$pattern][$group->getSourceFilePath( 'en' )] = true;
185 }
186 $source = isset( $globCache[$pattern][$filename] );
187 }
188
189 $cache = $this->exists();
190
191 // Timestamp and existence checks
192 if ( !$cache && !$source ) {
193 return true;
194 } elseif ( !$cache && $source ) {
195 $reason = self::NO_CACHE;
196
197 return false;
198 } elseif ( $cache && !$source ) {
199 $reason = self::NO_SOURCE;
200
201 return false;
202 }
203
204 if ( $this->get( '#version' ) !== self::VERSION ) {
205 $reason = self::CHANGED;
206 return false;
207 }
208
209 if ( filemtime( $filename ) <= $this->get( '#updated' ) ) {
210 return true;
211 }
212
213 // From now on cache and source file exists, but source file mtime is newer
214 $created = $this->get( '#created' );
215
216 // File hash check
217 $newhash = md5( file_get_contents( $filename ) );
218 if ( $this->get( '#filehash' ) === $newhash ) {
219 // Update cache so that we don't need to compare hashes next time
220 $this->create( $created );
221
222 return true;
223 }
224
225 // Parse output hash check
226 $parseOutput ??= $group->parseExternal( $this->code );
227 if ( $this->get( '#msghash' ) === md5( serialize( $parseOutput ) ) ) {
228 // Update cache so that we don't need to do slow checks next time
229 $this->create( $created );
230
231 return true;
232 }
233
234 $reason = self::CHANGED;
235
236 return false;
237 }
238
239 public function invalidate(): void {
240 $this->close();
241 unlink( $this->getCacheFilePath() );
242 }
243
244 private function serialize( array $data ): string {
245 // Using simple prefix for easy future extension
246 return 'J' . json_encode( $data );
247 }
248
249 private function unserialize( string $serialized ): array {
250 $type = $serialized[0];
251
252 if ( $type !== 'J' ) {
253 throw new RuntimeException( 'Unknown serialization format' );
254 }
255
256 return json_decode( substr( $serialized, 1 ), true );
257 }
258
263 protected function open() {
264 if ( $this->cache === null ) {
265 $this->cache = Reader::open( $this->getCacheFilePath() );
266 }
267
268 return $this->cache;
269 }
270
274 protected function close() {
275 if ( $this->cache !== null ) {
276 $this->cache->close();
277 $this->cache = null;
278 }
279 }
280
285 protected function getCacheFilePath(): string {
286 return $this->cacheFilePath;
287 }
288}
return[ 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->getMainConfig(), $services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), $services->get( 'Translate:MessageIndex'));}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getDBLoadBalancer(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=$services->getMainConfig() ->get( 'TranslateMessageIndex');if(is_string( $params)) { $params=(array) $params;} $class=array_shift( $params);return new $class( $params);}, 'Translate:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getDBLoadBalancer(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore($services->getDBLoadBalancerFactory());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup());}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getDBLoadBalancer() ->getConnection(DB_PRIMARY), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'),);}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnection(DB_REPLICA);return new TranslationStashStorage( $db);}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array
This class implements default behavior for file based message groups.
Caches messages of file based message group source file.
getCacheFilePath()
Returns full path to the cache file.
getAuthors()
Get a list of authors.
isValid(&$reason)
Checks whether the cache still reflects the source file.
getKeys()
Returns list of message keys that are stored.
__construct(FileBasedMessageGroup $group, string $code, string $cacheFilePath)
Contructs a new cache object for given group and language code.
getTimestamp()
Returns timestamp in unix-format about when this cache was first created.
getExtra()
Get other data cached from the file format class.
exists()
Returns whether cache exists for this language and group.
open()
Open the cache for reading.
close()
Close the cache from reading.
create( $created=false)
Populates the cache from current state of the source file.