Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
MessageGroupCache.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
6use Cdb\Reader;
7use Cdb\Writer;
9use RuntimeException;
10
24 public const NO_SOURCE = 1;
25 public const NO_CACHE = 2;
26 public const CHANGED = 3;
27 private const VERSION = '4';
28 private FileBasedMessageGroup $group;
29 private ?Reader $cache = null;
30 private string $languageCode;
31 private string $cacheFilePath;
32
34 public function __construct(
36 string $languageCode,
37 string $cacheFilePath
38 ) {
39 $this->group = $group;
40 $this->languageCode = $languageCode;
41 $this->cacheFilePath = $cacheFilePath;
42 }
43
45 public function exists(): bool {
46 return file_exists( $this->getCacheFilePath() );
47 }
48
53 public function getKeys(): array {
54 $reader = $this->open();
55 $keys = [];
56
57 $key = $reader->firstkey();
58 while ( $key !== false ) {
59 if ( ( $key[0] ?? '' ) !== '#' ) {
60 $keys[] = $key;
61 }
62
63 $key = $reader->nextkey();
64 }
65
66 return $keys;
67 }
68
73 public function getTimestamp() {
74 return $this->open()->get( '#created' );
75 }
76
78 public function getUpdateTimestamp() {
79 return $this->open()->get( '#updated' );
80 }
81
86 public function get( string $key ) {
87 return $this->open()->get( $key );
88 }
89
94 public function getAuthors(): array {
95 $cache = $this->open();
96 return $cache->exists( '#authors' ) ?
97 $this->unserialize( $cache->get( '#authors' ) ) : [];
98 }
99
101 public function getExtra(): array {
102 $cache = $this->open();
103 return $cache->exists( '#extra' ) ? $this->unserialize( $cache->get( '#extra' ) ) : [];
104 }
105
110 public function create( $created = false ): void {
111 $this->close(); // Close the reader instance just to be sure
112
113 $parseOutput = $this->group->parseExternal( $this->languageCode );
114 $messages = $parseOutput['MESSAGES'];
115 if ( $messages === [] ) {
116 if ( $this->exists() ) {
117 // Delete stale cache files
118 unlink( $this->getCacheFilePath() );
119 }
120
121 return; // Don't create empty caches
122 }
123 $hash = md5( file_get_contents( $this->group->getSourceFilePath( $this->languageCode ) ) );
124
125 wfMkdirParents( dirname( $this->getCacheFilePath() ) );
126 $cache = Writer::open( $this->getCacheFilePath() );
127
128 foreach ( $messages as $key => $value ) {
129 $cache->set( $key, $value );
130 }
131 $cache->set( '#authors', $this->serialize( $parseOutput['AUTHORS'] ) );
132 $cache->set( '#extra', $this->serialize( $parseOutput['EXTRA'] ) );
133 $cache->set( '#created', $created ?: wfTimestamp() );
134 $cache->set( '#updated', wfTimestamp() );
135 $cache->set( '#filehash', $hash );
136 $cache->set( '#msghash', md5( serialize( $parseOutput ) ) );
137 $cache->set( '#version', self::VERSION );
138 $cache->close();
139 }
140
151 public function isValid( &$reason ): bool {
152 $group = $this->group;
153 $pattern = $group->getSourceFilePath( '*' );
154 $filename = $group->getSourceFilePath( $this->languageCode );
155
156 $parseOutput = null;
157
158 // If the file pattern is not dependent on the language, we will assume
159 // that all translations are stored in one file. This means we need to
160 // actually parse the file to know if a language is present.
161 if ( !str_contains( $pattern, '*' ) ) {
162 $parseOutput = $group->parseExternal( $this->languageCode );
163 $source = $parseOutput['MESSAGES'] !== [];
164 } else {
165 static $globCache = [];
166 if ( !isset( $globCache[$pattern] ) ) {
167 $globCache[$pattern] = array_flip( glob( $pattern, GLOB_NOESCAPE ) );
168 // Definition file might not match the above pattern
169 $globCache[$pattern][$group->getSourceFilePath( 'en' )] = true;
170 }
171 $source = isset( $globCache[$pattern][$filename] );
172 }
173
174 $cache = $this->exists();
175
176 // Timestamp and existence checks
177 if ( !$cache && !$source ) {
178 return true;
179 } elseif ( !$cache && $source ) {
180 $reason = self::NO_CACHE;
181
182 return false;
183 } elseif ( $cache && !$source ) {
184 $reason = self::NO_SOURCE;
185
186 return false;
187 }
188
189 if ( $this->get( '#version' ) !== self::VERSION ) {
190 $reason = self::CHANGED;
191 return false;
192 }
193
194 if ( filemtime( $filename ) <= $this->get( '#updated' ) ) {
195 return true;
196 }
197
198 // From now on cache and source file exists, but source file mtime is newer
199 $created = $this->get( '#created' );
200
201 // File hash check
202 $newhash = md5( file_get_contents( $filename ) );
203 if ( $this->get( '#filehash' ) === $newhash ) {
204 // Update cache so that we don't need to compare hashes next time
205 $this->create( $created );
206
207 return true;
208 }
209
210 // Parse output hash check
211 $parseOutput ??= $group->parseExternal( $this->languageCode );
212 if ( $this->get( '#msghash' ) === md5( serialize( $parseOutput ) ) ) {
213 // Update cache so that we don't need to do slow checks next time
214 $this->create( $created );
215
216 return true;
217 }
218
219 $reason = self::CHANGED;
220
221 return false;
222 }
223
224 public function invalidate(): void {
225 $this->close();
226 unlink( $this->getCacheFilePath() );
227 }
228
229 private function serialize( array $data ): string {
230 // Using simple prefix for easy future extension
231 return 'J' . json_encode( $data );
232 }
233
234 private function unserialize( string $serialized ): array {
235 $type = $serialized[0];
236
237 if ( $type !== 'J' ) {
238 throw new RuntimeException( 'Unknown serialization format' );
239 }
240
241 return json_decode( substr( $serialized, 1 ), true );
242 }
243
245 protected function open(): Reader {
246 $this->cache ??= Reader::open( $this->getCacheFilePath() );
247
248 return $this->cache;
249 }
250
252 protected function close(): void {
253 if ( $this->cache !== null ) {
254 $this->cache->close();
255 $this->cache = null;
256 }
257 }
258
260 protected function getCacheFilePath(): string {
261 return $this->cacheFilePath;
262 }
263}
return[ 'Translate:AggregateGroupManager'=> static function(MediaWikiServices $services):AggregateGroupManager { return new AggregateGroupManager( $services->getTitleFactory());}, 'Translate:AggregateGroupMessageGroupFactory'=> static function(MediaWikiServices $services):AggregateGroupMessageGroupFactory { return new AggregateGroupMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'));}, '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:ExternalMessageSourceStateComparator'=> static function(MediaWikiServices $services):ExternalMessageSourceStateComparator { return new ExternalMessageSourceStateComparator(new SimpleStringComparator(), $services->getRevisionLookup(), $services->getPageStore());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), $services->get( 'Translate:MessageIndex'), $services->getTitleFactory(), new ServiceOptions(ExternalMessageSourceStateImporter::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:FileBasedMessageGroupFactory'=> static function(MediaWikiServices $services):FileBasedMessageGroupFactory { return new FileBasedMessageGroupFactory(new MessageGroupConfigurationParser(), new ServiceOptions(FileBasedMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, '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:HookDefinedMessageGroupFactory'=> static function(MediaWikiServices $services):HookDefinedMessageGroupFactory { return new HookDefinedMessageGroupFactory( $services->get( 'Translate:HookRunner'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleMessageGroupFactory'=> static function(MediaWikiServices $services):MessageBundleMessageGroupFactory { return new MessageBundleMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'), new ServiceOptions(MessageBundleMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:MessageBundleTranslationLoader'=> static function(MediaWikiServices $services):MessageBundleTranslationLoader { return new MessageBundleTranslationLoader( $services->getLanguageFallback());}, 'Translate:MessageGroupMetadata'=> static function(MediaWikiServices $services):MessageGroupMetadata { return new MessageGroupMetadata( $services->getDBLoadBalancer());}, '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->get( 'Translate:MessageGroupMetadata'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageGroupSubscription'=> static function(MediaWikiServices $services):MessageGroupSubscription { return new MessageGroupSubscription($services->get( 'Translate:MessageGroupSubscriptionStore'), $services->getJobQueueGroup(), $services->getUserIdentityLookup(), LoggerFactory::getInstance( 'Translate.MessageGroupSubscription'), new ServiceOptions(MessageGroupSubscription::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:MessageGroupSubscriptionHookHandler'=> static function(MediaWikiServices $services):MessageGroupSubscriptionHookHandler { return new MessageGroupSubscriptionHookHandler($services->get( 'Translate:MessageGroupSubscription'), $services->getUserFactory());}, 'Translate:MessageGroupSubscriptionStore'=> static function(MediaWikiServices $services):MessageGroupSubscriptionStore { return new MessageGroupSubscriptionStore( $services->getDBLoadBalancerFactory());}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=(array) $services->getMainConfig() ->get( 'TranslateMessageIndex');$class=array_shift( $params);$implementationMap=['HashMessageIndex'=> HashMessageIndex::class, 'CDBMessageIndex'=> CDBMessageIndex::class, 'DatabaseMessageIndex'=> DatabaseMessageIndex::class, 'hash'=> HashMessageIndex::class, 'cdb'=> CDBMessageIndex::class, 'database'=> DatabaseMessageIndex::class,];$messageIndexStoreClass=$implementationMap[$class] ?? $implementationMap['database'];return new MessageIndex(new $messageIndexStoreClass, $services->getMainWANObjectCache(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), LoggerFactory::getInstance( 'Translate'), $services->getMainObjectStash(), $services->getDBLoadBalancerFactory(), $services->get( 'Translate:MessageGroupSubscription'), new ServiceOptions(MessageIndex::SERVICE_OPTIONS, $services->getMainConfig()),);}, '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'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore( $services->getDBLoadBalancer());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleDeleter'=> static function(MediaWikiServices $services):TranslatableBundleDeleter { return new TranslatableBundleDeleter($services->getMainObjectStash(), $services->getJobQueueGroup(), $services->get( 'Translate:SubpageListBuilder'), $services->get( 'Translate:TranslatableBundleFactory'));}, '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(), $services->getNamespaceInfo(), $services->getTitleFactory());}, '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->getDBLoadBalancerFactory(), $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:TranslatablePageMarker'=> static function(MediaWikiServices $services):TranslatablePageMarker { return new TranslatablePageMarker($services->getDBLoadBalancer(), $services->getJobQueueGroup(), $services->getLinkRenderer(), MessageGroups::singleton(), $services->get( 'Translate:MessageIndex'), $services->getTitleFormatter(), $services->getTitleParser(), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:TranslatablePageStateStore'), $services->get( 'Translate:TranslationUnitStoreFactory'), $services->get( 'Translate:MessageGroupMetadata'), $services->getWikiPageFactory(), $services->get( 'Translate:TranslatablePageView'));}, 'Translate:TranslatablePageMessageGroupFactory'=> static function(MediaWikiServices $services):TranslatablePageMessageGroupFactory { return new TranslatablePageMessageGroupFactory(new ServiceOptions(TranslatablePageMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStateStore'=> static function(MediaWikiServices $services):TranslatablePageStateStore { return new TranslatablePageStateStore($services->get( 'Translate:PersistentCache'), $services->getPageStore());}, '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'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:TranslatablePageView'=> static function(MediaWikiServices $services):TranslatablePageView { return new TranslatablePageView($services->getDBLoadBalancerFactory(), $services->get( 'Translate:TranslatablePageStateStore'), new ServiceOptions(TranslatablePageView::SERVICE_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslateSandbox'=> static function(MediaWikiServices $services):TranslateSandbox { return new TranslateSandbox($services->getUserFactory(), $services->getDBLoadBalancer(), $services->getPermissionManager(), $services->getAuthManager(), $services->getUserGroupManager(), $services->getActorStore(), $services->getUserOptionsManager(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), new ServiceOptions(TranslateSandbox::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, '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.
create( $created=false)
Populates the cache from current state of the source file.
isValid(&$reason)
Checks whether the cache still reflects the source file.
getTimestamp()
Returns timestamp in unix-format about when this cache was first created.
exists()
Returns whether cache exists for this language and group.
__construct(FileBasedMessageGroup $group, string $languageCode, string $cacheFilePath)
Contructs a new cache object for given group and language code.
getExtra()
Get other data cached from the file format class.