Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
21.78% |
22 / 101 |
|
22.22% |
2 / 9 |
CRAP | |
0.00% |
0 / 1 |
TranslatablePageStore | |
21.78% |
22 / 101 |
|
22.22% |
2 / 9 |
232.04 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
move | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
handleNullRevisionInsert | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
delete | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
unmark | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
performStatusUpdate | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
updateStatus | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
2 | |||
moveMetadata | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
30 | |||
removeFromAggregateGroups | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\MessageGroupProcessing; |
5 | |
6 | use AggregateMessageGroup; |
7 | use DeferredUpdates; |
8 | use IDBAccessObject; |
9 | use InvalidArgumentException; |
10 | use JobQueueGroup; |
11 | use MediaWiki\Extension\Translate\MessageLoading\MessageIndex; |
12 | use MediaWiki\Extension\Translate\MessageProcessing\MessageGroupMetadata; |
13 | use MediaWiki\Extension\Translate\PageTranslation\TranslatablePage; |
14 | use MediaWiki\Extension\Translate\PageTranslation\TranslatablePageParser; |
15 | use MediaWiki\Extension\Translate\PageTranslation\UpdateTranslatablePageJob; |
16 | use MediaWiki\Page\PageIdentity; |
17 | use MediaWiki\Revision\RevisionRecord; |
18 | use MediaWiki\Revision\SlotRecord; |
19 | use MediaWiki\Title\Title; |
20 | use RuntimeException; |
21 | use TextContent; |
22 | use Wikimedia\Rdbms\ILoadBalancer; |
23 | |
24 | /** |
25 | * @author Abijeet Patro |
26 | * @author Niklas Laxström |
27 | * @since 2022.03 |
28 | * @license GPL-2.0-or-later |
29 | */ |
30 | class TranslatablePageStore implements TranslatableBundleStore { |
31 | private MessageIndex $messageIndex; |
32 | private JobQueueGroup $jobQueue; |
33 | private RevTagStore $revTagStore; |
34 | private ILoadBalancer $loadBalancer; |
35 | private TranslatableBundleStatusStore $translatableBundleStatusStore; |
36 | private TranslatablePageParser $translatablePageParser; |
37 | private MessageGroupMetadata $messageGroupMetadata; |
38 | |
39 | public function __construct( |
40 | MessageIndex $messageIndex, |
41 | JobQueueGroup $jobQueue, |
42 | RevTagStore $revTagStore, |
43 | ILoadBalancer $loadBalancer, |
44 | TranslatableBundleStatusStore $translatableBundleStatusStore, |
45 | TranslatablePageParser $translatablePageParser, |
46 | MessageGroupMetadata $messageGroupMetadata |
47 | ) { |
48 | $this->messageIndex = $messageIndex; |
49 | $this->jobQueue = $jobQueue; |
50 | $this->revTagStore = $revTagStore; |
51 | $this->loadBalancer = $loadBalancer; |
52 | $this->translatableBundleStatusStore = $translatableBundleStatusStore; |
53 | $this->translatablePageParser = $translatablePageParser; |
54 | $this->messageGroupMetadata = $messageGroupMetadata; |
55 | } |
56 | |
57 | public function move( Title $oldName, Title $newName ): void { |
58 | $oldTranslatablePage = TranslatablePage::newFromTitle( $oldName ); |
59 | $newTranslatablePage = TranslatablePage::newFromTitle( $newName ); |
60 | $oldGroupId = $oldTranslatablePage->getMessageGroupId(); |
61 | $newGroupId = $newTranslatablePage->getMessageGroupId(); |
62 | |
63 | $this->messageGroupMetadata->moveMetadata( $oldGroupId, $newGroupId, TranslatablePage::METADATA_KEYS ); |
64 | |
65 | $this->moveMetadata( $oldGroupId, $newGroupId ); |
66 | |
67 | TranslatablePage::clearSourcePageCache(); |
68 | |
69 | // Re-render the pages to get everything in sync |
70 | MessageGroups::singleton()->recache(); |
71 | // Update message index now so that, when after this job the MoveTranslationUnits hook |
72 | // runs in deferred updates, it will not run MessageIndexRebuildJob (T175834). |
73 | $this->messageIndex->rebuild(); |
74 | |
75 | $job = UpdateTranslatablePageJob::newFromPage( TranslatablePage::newFromTitle( $newName ) ); |
76 | $this->jobQueue->push( $job ); |
77 | } |
78 | |
79 | public function handleNullRevisionInsert( TranslatableBundle $bundle, RevisionRecord $revision ): void { |
80 | if ( !$bundle instanceof TranslatablePage ) { |
81 | throw new InvalidArgumentException( |
82 | 'Expected $bundle to be of type TranslatablePage, got ' . get_class( $bundle ) |
83 | ); |
84 | } |
85 | |
86 | $pageContent = $revision->getContent( SlotRecord::MAIN ); |
87 | if ( !$pageContent instanceof TextContent ) { |
88 | throw new RuntimeException( "Translatable page {$bundle->getTitle()} has non-textual content." ); |
89 | } |
90 | |
91 | // Check if the revision still has the <translate> tag |
92 | $pageText = $pageContent->getText(); |
93 | if ( $this->translatablePageParser->containsMarkup( $pageText ) ) { |
94 | $this->revTagStore->replaceTag( $bundle->getTitle(), RevTagStore::TP_READY_TAG, $revision->getId() ); |
95 | TranslatablePage::clearSourcePageCache(); |
96 | } |
97 | } |
98 | |
99 | /** Delete a translatable page */ |
100 | public function delete( Title $title ): void { |
101 | $dbw = $this->loadBalancer->getConnection( DB_PRIMARY ); |
102 | $dbw->delete( 'translate_sections', [ 'trs_page' => $title->getArticleID() ], __METHOD__ ); |
103 | |
104 | $this->unmark( $title ); |
105 | } |
106 | |
107 | /** Unmark a translatable page */ |
108 | public function unmark( PageIdentity $title ): void { |
109 | $translatablePage = TranslatablePage::newFromTitle( $title ); |
110 | $translatablePage->getTranslationPercentages(); |
111 | foreach ( $translatablePage->getTranslationPages() as $page ) { |
112 | $page->invalidateCache(); |
113 | } |
114 | |
115 | $groupId = $translatablePage->getMessageGroupId(); |
116 | $this->messageGroupMetadata->clearMetadata( $groupId, TranslatablePage::METADATA_KEYS ); |
117 | $this->removeFromAggregateGroups( $groupId ); |
118 | |
119 | // Remove tags after all group related work is done in order to avoid breaking calls to |
120 | // TranslatablePage::getMessageGroup incase the group cache is not populated |
121 | $this->revTagStore->removeTags( $title, RevTagStore::TP_MARK_TAG, RevTagStore::TP_READY_TAG ); |
122 | $this->translatableBundleStatusStore->removeStatus( $title->getId() ); |
123 | |
124 | MessageGroups::singleton()->recache(); |
125 | $this->messageIndex->rebuild(); |
126 | |
127 | TranslatablePage::clearSourcePageCache(); |
128 | $translatablePage->getTitle()->invalidateCache(); |
129 | } |
130 | |
131 | /** Queues an update for the status of the translatable page. Update is not done immediately. */ |
132 | public function performStatusUpdate( Title $title ): void { |
133 | DeferredUpdates::addCallableUpdate( |
134 | function () use ( $title ) { |
135 | $this->updateStatus( $title ); |
136 | } |
137 | ); |
138 | } |
139 | |
140 | /** @internal public only for testing. Use ::performStatusUpdate instead */ |
141 | public function updateStatus( Title $title ): ?TranslatableBundleStatus { |
142 | $revTags = $this->revTagStore->getLatestRevisionsForTags( |
143 | $title, |
144 | RevTagStore::TP_MARK_TAG, |
145 | RevTagStore::TP_READY_TAG |
146 | ); |
147 | |
148 | $status = TranslatablePage::determineStatus( |
149 | $revTags[RevTagStore::TP_READY_TAG] ?? null, |
150 | $revTags[RevTagStore::TP_MARK_TAG] ?? null, |
151 | $title->getLatestRevID( IDBAccessObject::READ_LATEST ) |
152 | ); |
153 | |
154 | if ( $status ) { |
155 | $this->translatableBundleStatusStore->setStatus( |
156 | $title, $status, TranslatablePage::class |
157 | ); |
158 | } |
159 | |
160 | return $status; |
161 | } |
162 | |
163 | private function moveMetadata( string $oldGroupId, string $newGroupId ): void { |
164 | // Make the changes in aggregate groups metadata, if present in any of them. |
165 | $aggregateGroups = MessageGroups::getGroupsByType( AggregateMessageGroup::class ); |
166 | $this->messageGroupMetadata->preloadGroups( array_keys( $aggregateGroups ), __METHOD__ ); |
167 | |
168 | foreach ( $aggregateGroups as $id => $group ) { |
169 | $subgroups = $this->messageGroupMetadata->get( $id, 'subgroups' ); |
170 | if ( $subgroups === false ) { |
171 | continue; |
172 | } |
173 | |
174 | $subgroups = explode( ',', $subgroups ); |
175 | $subgroups = array_flip( $subgroups ); |
176 | if ( isset( $subgroups[$oldGroupId] ) ) { |
177 | $subgroups[$newGroupId] = $subgroups[$oldGroupId]; |
178 | unset( $subgroups[$oldGroupId] ); |
179 | $subgroups = array_flip( $subgroups ); |
180 | $this->messageGroupMetadata->set( |
181 | $group->getId(), |
182 | 'subgroups', |
183 | implode( ',', $subgroups ) |
184 | ); |
185 | } |
186 | } |
187 | |
188 | // Move discouraged status |
189 | $priority = MessageGroups::getPriority( $oldGroupId ); |
190 | if ( $priority !== '' ) { |
191 | MessageGroups::setPriority( $newGroupId, $priority ); |
192 | MessageGroups::setPriority( $oldGroupId, '' ); |
193 | } |
194 | } |
195 | |
196 | private function removeFromAggregateGroups( string $groupId ): void { |
197 | // remove the page from aggregate groups, if present in any of them. |
198 | $aggregateGroups = MessageGroups::getGroupsByType( AggregateMessageGroup::class ); |
199 | $this->messageGroupMetadata->preloadGroups( array_keys( $aggregateGroups ), __METHOD__ ); |
200 | foreach ( $aggregateGroups as $group ) { |
201 | $subgroups = $this->messageGroupMetadata->get( $group->getId(), 'subgroups' ); |
202 | if ( $subgroups !== false ) { |
203 | $subgroups = explode( ',', $subgroups ); |
204 | $subgroups = array_flip( $subgroups ); |
205 | if ( isset( $subgroups[$groupId] ) ) { |
206 | unset( $subgroups[$groupId] ); |
207 | $subgroups = array_flip( $subgroups ); |
208 | $this->messageGroupMetadata->set( |
209 | $group->getId(), |
210 | 'subgroups', |
211 | implode( ',', $subgroups ) |
212 | ); |
213 | } |
214 | } |
215 | } |
216 | } |
217 | } |