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