Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
22.64% covered (danger)
22.64%
24 / 106
22.22% covered (danger)
22.22%
2 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
TranslatablePageStore
22.64% covered (danger)
22.64%
24 / 106
22.22% covered (danger)
22.22%
2 / 9
225.16
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 move
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 handleNullRevisionInsert
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 delete
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 unmark
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 performStatusUpdate
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 updateStatus
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
2
 moveMetadata
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
30
 removeFromAggregateGroups
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
6use AggregateMessageGroup;
7use InvalidArgumentException;
8use JobQueueGroup;
9use MediaWiki\Content\TextContent;
10use MediaWiki\Deferred\DeferredUpdates;
11use MediaWiki\Extension\Translate\MessageLoading\MessageIndex;
12use MediaWiki\Extension\Translate\MessageLoading\RebuildMessageIndexJob;
13use MediaWiki\Extension\Translate\MessageProcessing\MessageGroupMetadata;
14use MediaWiki\Extension\Translate\PageTranslation\TranslatablePage;
15use MediaWiki\Extension\Translate\PageTranslation\TranslatablePageParser;
16use MediaWiki\Extension\Translate\PageTranslation\UpdateTranslatablePageJob;
17use MediaWiki\Page\PageIdentity;
18use MediaWiki\Revision\RevisionRecord;
19use MediaWiki\Revision\SlotRecord;
20use MediaWiki\Title\Title;
21use RuntimeException;
22use Wikimedia\Rdbms\IConnectionProvider;
23use 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 */
31class 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}