Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
SyncTranslatableBundleStatusMaintenanceScript.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\Diagnostics;
5
6use LoggedUpdateMaintenance;
15use MediaWiki\MediaWikiServices;
16use MediaWiki\Title\Title;
17use RuntimeException;
18
23class SyncTranslatableBundleStatusMaintenanceScript extends LoggedUpdateMaintenance {
24 private const INDENT_SPACER = ' ';
25 private const STATUS_NAME_MAPPING = [
26 TranslatablePageStatus::PROPOSED => 'Proposed',
27 TranslatablePageStatus::ACTIVE => 'Active',
28 TranslatablePageStatus::OUTDATED => 'Outdated',
29 TranslatablePageStatus::BROKEN => 'Broken'
30 ];
31 private const SYNC_BATCH_STATUS = 15;
32 private const SCRIPT_VERSION = 1;
33
34 public function __construct() {
35 parent::__construct();
36 $this->addDescription( 'Sync translatable bundle status with values from the rev_tag table' );
37 $this->requireExtension( 'Translate' );
38 }
39
41 protected function getUpdateKey(): string {
42 return __CLASS__ . '_v' . self::SCRIPT_VERSION;
43 }
44
46 protected function doDBUpdates(): bool {
47 $this->output( "... Syncing translatable bundle status ...\n\n" );
48
49 $this->output( "Fetching translatable bundles and their statues\n\n" );
50 $translatableBundles = $this->fetchTranslatableBundles();
51 $translatableBundleStatuses = Services::getInstance()
52 ->getTranslatableBundleStatusStore()
53 ->getAllWithStatus();
54
55 $differences = $this->identifyDifferences( $translatableBundles, $translatableBundleStatuses );
56
57 $this->outputDifferences( $differences['missing'], 'Missing' );
58 $this->outputDifferences( $differences['incorrect'], 'Incorrect' );
59 $this->outputDifferences( $differences['extra'], 'Extra' );
60
61 $this->output( "\nSynchronizing...\n\n" );
62
63 $this->syncStatus( $differences['missing'], 'Missing' );
64 $this->syncStatus( $differences['incorrect'], 'Incorrect' );
65 $this->removeStatus( $differences['extra'] );
66
67 $this->output( "\n...Done syncing translatable status...\n" );
68
69 return true;
70 }
71
72 private function fetchTranslatableBundles(): array {
73 // Fetch the translatable pages
74 $resultWrapper = PageTranslationSpecialPage::loadPagesFromDB();
75 return PageTranslationSpecialPage::buildPageArray( $resultWrapper );
76
77 // TODO: Fetch message bundles
78 }
79
87 private function identifyDifferences(
88 array $translatableBundles,
89 array $translatableBundleStatuses
90 ): array {
91 $result = [
92 'missing' => [],
93 'extra' => [],
94 'incorrect' => []
95 ];
96
97 $bundleFactory = Services::getInstance()->getTranslatableBundleFactory();
98 foreach ( $translatableBundles as $bundleId => $bundleInfo ) {
99 $title = $bundleInfo['title'];
100 $bundle = $this->getTranslatableBundle( $bundleFactory, $title );
101 $bundleStatus = $this->determineStatus( $bundle, $bundleInfo );
102
103 if ( !$bundleStatus ) {
104 // Ignore pages for which status could not be determined.
105 continue;
106 }
107
108 if ( !isset( $translatableBundleStatuses[$bundleId] ) ) {
109 // Identify missing records in translatable_bundles
110 $response = [
111 'title' => $title,
112 'status' => $bundleStatus,
113 'page_id' => $bundleId
114 ];
115 $result['missing'][] = $response;
116 } elseif ( !$bundleStatus->isEqual( $translatableBundleStatuses[$bundleId] ) ) {
117 // Identify incorrect records in translatable_bundles
118 $response = [
119 'title' => $title,
120 'status' => $bundleStatus,
121 'page_id' => $bundleId
122 ];
123 $result['incorrect'][] = $response;
124 }
125 }
126
127 // Identify extra records in translatable_bundles
128 $extraStatusBundleIds = array_diff_key( $translatableBundleStatuses, $translatableBundles );
129 foreach ( $extraStatusBundleIds as $extraBundleId => $statusId ) {
130 $title = Title::newFromID( $extraBundleId );
131 $response = [
132 'title' => $title,
133 // TODO: This should be determined dynamically when we start supporting MessageBundles
134 'status' => new TranslatablePageStatus( $statusId ),
135 'page_id' => $extraBundleId
136 ];
137
138 $result['extra'][] = $response;
139 }
140
141 return $result;
142 }
143
144 private function determineStatus(
145 TranslatableBundle $bundle,
146 array $bundleInfo
147 ): ?TranslatableBundleStatus {
148 if ( $bundle instanceof TranslatablePage ) {
149 return $bundle::determineStatus(
150 $bundleInfo[RevTagStore::TP_READY_TAG] ?? null,
151 $bundleInfo[RevTagStore::TP_MARK_TAG] ?? null,
152 $bundleInfo['latest']
153 );
154 } else {
155 // TODO: Add determineStatus as a function to TranslatableBundle abstract class and then
156 // implement it in MessageBundle. It may not take the same set of parameters though.
157 throw new RuntimeException( 'Method determineStatus not implemented for MessageBundle' );
158 }
159 }
160
161 private function getTranslatableBundle(
162 TranslatableBundleFactory $tbFactory,
163 Title $title
164 ): TranslatableBundle {
165 $bundle = $tbFactory->getBundle( $title );
166 if ( $bundle ) {
167 return $bundle;
168 }
169
170 // This page has a revision tag, lets assume that this is a translatable page
171 // Broken pages for example will not be in the cache
172 // TODO: Is there a better way to handle this?
173 return TranslatablePage::newFromTitle( $title );
174 }
175
176 private function syncStatus( array $bundlesWithDifference, string $differenceType ): void {
177 if ( !$bundlesWithDifference ) {
178 $this->output( "No \"$differenceType\" bundle statuses\n" );
179 return;
180 }
181
182 $this->output( "Syncing \"$differenceType\" bundle statuses\n" );
183
184 $bundleFactory = Services::getInstance()->getTranslatableBundleFactory();
185 $tpStore = Services::getInstance()->getTranslatablePageStore();
186 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
187
188 $bundleCountProcessed = 0;
189 foreach ( $bundlesWithDifference as $bundleInfo ) {
190 $pageId = $bundleInfo['page_id'];
191 $bundleTitle = $bundleInfo['title'] ?? null;
192 if ( !$bundleTitle instanceof Title ) {
193 $this->fatalError( "No title for page with id: $pageId \n" );
194 }
195
196 $bundle = $this->getTranslatableBundle( $bundleFactory, $bundleTitle );
197 if ( $bundle instanceof TranslatablePage ) {
198 // TODO: Eventually we want to add this method to the TranslatableBundleStore
199 // and then call updateStatus on it. After that we won't have to check for the
200 // type of the translatable bundle.
201 $tpStore->updateStatus( $bundleTitle );
202 }
203
204 if ( $bundleCountProcessed % self::SYNC_BATCH_STATUS === 0 ) {
205 $lbFactory->waitForReplication();
206 }
207
208 ++$bundleCountProcessed;
209 }
210
211 $this->output( "Completed sync for \"$differenceType\" bundle statuses\n" );
212 }
213
214 private function removeStatus( array $extraBundleInfo ): void {
215 if ( !$extraBundleInfo ) {
216 $this->output( "No \"extra\" bundle statuses\n" );
217 return;
218 }
219 $this->output( "Removing \"extra\" bundle statuses\n" );
220 $pageIds = [];
221 foreach ( $extraBundleInfo as $bundleInfo ) {
222 $pageIds[] = $bundleInfo['page_id'];
223 }
224
225 $tbStatusStore = Services::getInstance()->getTranslatableBundleStatusStore();
226 $tbStatusStore->removeStatus( ...$pageIds );
227 $this->output( "Removed \"extra\" bundle statuses\n" );
228 }
229
230 private function outputDifferences( array $bundlesWithDifference, string $differenceType ): void {
231 if ( $bundlesWithDifference ) {
232 $this->output( "$differenceType translatable bundles statuses:\n" );
233 foreach ( $bundlesWithDifference as $bundle ) {
234 $this->outputBundleInfo( $bundle );
235 }
236 } else {
237 $this->output( "No \"$differenceType\" translatable bundle statuses found!\n" );
238 }
239 }
240
241 private function outputBundleInfo( array $bundle ): void {
242 $titlePrefixedDbKey = $bundle['title'] instanceof Title ?
243 $bundle['title']->getPrefixedDBkey() : '<Title not available>';
244 $id = str_pad( (string)$bundle['page_id'], 7, ' ', STR_PAD_LEFT );
245 $status = self::STATUS_NAME_MAPPING[ $bundle['status']->getId() ];
246 $this->output( self::INDENT_SPACER . "* [Id: $id] $titlePrefixedDbKey: $status\n" );
247 }
248}
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
Script to identify the status of the translatable bundles in the rev_tag table and update them in the...
Class to manage revision tags for translatable bundles.
Create instances of various classes based on the type of TranslatableBundle.
Translatable bundle represents a message group where its translatable content is defined on a wiki pa...
A special page for marking revisions of pages for translation.
Stores and validates possible statuses for TranslatablePage.
Mixed bag of methods related to translatable pages.
Minimal service container.
Definition Services.php:58