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