Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 109 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
SyncTranslatableBundleStatusMaintenanceScript | |
0.00% |
0 / 109 |
|
0.00% |
0 / 11 |
812 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getUpdateKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doDBUpdates | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
2 | |||
fetchTranslatableBundles | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
identifyDifferences | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
42 | |||
determineStatus | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getTranslatableBundle | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
syncStatus | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
42 | |||
removeStatus | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
outputDifferences | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
outputBundleInfo | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\Diagnostics; |
5 | |
6 | use MediaWiki\Extension\Translate\MessageGroupProcessing\RevTagStore; |
7 | use MediaWiki\Extension\Translate\MessageGroupProcessing\TranslatableBundle; |
8 | use MediaWiki\Extension\Translate\MessageGroupProcessing\TranslatableBundleFactory; |
9 | use MediaWiki\Extension\Translate\MessageGroupProcessing\TranslatableBundleStatus; |
10 | use MediaWiki\Extension\Translate\PageTranslation\PageTranslationSpecialPage; |
11 | use MediaWiki\Extension\Translate\PageTranslation\TranslatablePage; |
12 | use MediaWiki\Extension\Translate\PageTranslation\TranslatablePageStatus; |
13 | use MediaWiki\Extension\Translate\Services; |
14 | use MediaWiki\Maintenance\LoggedUpdateMaintenance; |
15 | use MediaWiki\MediaWikiServices; |
16 | use MediaWiki\Title\Title; |
17 | use RuntimeException; |
18 | |
19 | /** |
20 | * Script to identify the status of the translatable bundles in the rev_tag table |
21 | * and update them in the translatable_bundles page. |
22 | */ |
23 | class 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 | /** @inheritDoc */ |
41 | protected function getUpdateKey(): string { |
42 | return __CLASS__ . '_v' . self::SCRIPT_VERSION; |
43 | } |
44 | |
45 | /** @inheritDoc */ |
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 | |
80 | /** |
81 | * This function compares the bundles and bundles statuses to identify, |
82 | * - Missing bundles in translatable statuses |
83 | * - Extra bundles in translatable statuses |
84 | * - Incorrect statuses in translatable statuses |
85 | * The data from the rev_tag table is treated as the source of truth. |
86 | */ |
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 | } |