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