Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 83
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
MessageBundleStore
0.00% covered (danger)
0.00%
0 / 83
0.00% covered (danger)
0.00%
0 / 6
506
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 move
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 handleNullRevisionInsert
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 delete
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 validate
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
110
 save
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageBundleTranslation;
5
6use InvalidArgumentException;
7use JobQueueGroup;
8use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
9use MediaWiki\Extension\Translate\MessageGroupProcessing\RevTagStore;
10use MediaWiki\Extension\Translate\MessageGroupProcessing\TranslatableBundle;
11use MediaWiki\Extension\Translate\MessageGroupProcessing\TranslatableBundleStore;
12use MediaWiki\Extension\Translate\MessageLoading\MessageIndex;
13use MediaWiki\Extension\Translate\MessageProcessing\MessageGroupMetadata;
14use MediaWiki\Languages\LanguageNameUtils;
15use MediaWiki\Revision\RevisionRecord;
16use MediaWiki\Title\Title;
17use Message;
18use RequestContext;
19use SpecialPageLanguage;
20
21/**
22 * @author Abijeet Patro
23 * @author Niklas Laxström
24 * @since 2022.04
25 * @license GPL-2.0-or-later
26 */
27class MessageBundleStore implements TranslatableBundleStore {
28    private RevTagStore $revTagStore;
29    private JobQueueGroup $jobQueue;
30    private LanguageNameUtils $languageNameUtils;
31    private MessageIndex $messageIndex;
32    private MessageGroupMetadata $messageGroupMetadata;
33    private const METADATA_KEYS_DB = [
34        'priorityforce',
35        'prioritylangs'
36    ];
37
38    public function __construct(
39        RevTagStore $revTagStore,
40        JobQueueGroup $jobQueue,
41        LanguageNameUtils $languageNameUtils,
42        MessageIndex $messageIndex,
43        MessageGroupMetadata $messageGroupMetadata
44    ) {
45        $this->revTagStore = $revTagStore;
46        $this->jobQueue = $jobQueue;
47        $this->languageNameUtils = $languageNameUtils;
48        $this->messageIndex = $messageIndex;
49        $this->messageGroupMetadata = $messageGroupMetadata;
50    }
51
52    public function move( Title $oldName, Title $newName ): void {
53        $oldBundle = new MessageBundle( $oldName );
54        $newBundle = new MessageBundle( $newName );
55
56        $this->messageGroupMetadata->moveMetadata(
57            $oldBundle->getMessageGroupId(),
58            $newBundle->getMessageGroupId(),
59            self::METADATA_KEYS_DB
60        );
61
62        MessageBundle::clearSourcePageCache();
63
64        // Re-render the bundles to get everything in sync
65        MessageGroups::singleton()->recache();
66        // Update message index now so that, when after this job the MoveTranslationUnits hook
67        // runs in deferred updates, it will not run MessageIndexRebuildJob (T175834).
68        $this->messageIndex->rebuild();
69    }
70
71    public function handleNullRevisionInsert( TranslatableBundle $bundle, RevisionRecord $revision ): void {
72        if ( !$bundle instanceof MessageBundle ) {
73            throw new InvalidArgumentException(
74                'Expected $bundle to be of type MessageBundle, got ' . get_class( $bundle )
75            );
76        }
77
78        $this->revTagStore->replaceTag( $bundle->getTitle(), RevTagStore::MB_VALID_TAG, $revision->getId() );
79        MessageBundle::clearSourcePageCache();
80    }
81
82    public function delete( Title $title ): void {
83        $this->revTagStore->removeTags( $title, RevTagStore::MB_VALID_TAG );
84
85        $bundle = new MessageBundle( $title );
86        $this->messageGroupMetadata->clearMetadata( $bundle->getMessageGroupId(), self::METADATA_KEYS_DB );
87
88        MessageBundle::clearSourcePageCache();
89
90        MessageGroups::singleton()->recache();
91        $this->messageIndex->rebuild();
92    }
93
94    public function validate( Title $pageTitle, MessageBundleContent $content ): void {
95        $content->validate();
96        // Verify that the language code is valid
97        $metadata = $content->getMetadata();
98        $sourceLanguageCode = $metadata->getSourceLanguageCode();
99        if ( $sourceLanguageCode ) {
100            if ( !$this->languageNameUtils->isKnownLanguageTag( $sourceLanguageCode ) ) {
101                throw new MalformedBundle(
102                    'translate-messagebundle-error-invalid-sourcelanguage', [ $sourceLanguageCode ]
103                );
104            }
105
106            $revisionId = $this->revTagStore->getLatestRevisionWithTag( $pageTitle, RevTagStore::MB_VALID_TAG );
107            // If request wants the source language to be changed after creation, then throw an exception
108            if ( $revisionId !== null && $sourceLanguageCode !== $pageTitle->getPageLanguage()->getCode() ) {
109                throw new MalformedBundle( 'translate-messagebundle-sourcelanguage-changed' );
110            }
111
112        }
113
114        $priorityLanguageCodes = $metadata->getPriorityLanguages();
115        if ( $priorityLanguageCodes ) {
116            $invalidLanguageCodes = [];
117            foreach ( $priorityLanguageCodes as $languageCode ) {
118                if ( !is_string( $languageCode ) ) {
119                    throw new MalformedBundle( 'translate-messagebundle-error-invalid-prioritylanguage-format' );
120                }
121
122                if ( !$this->languageNameUtils->isKnownLanguageTag( $languageCode ) ) {
123                    $invalidLanguageCodes[] = $languageCode;
124                }
125            }
126
127            if ( $invalidLanguageCodes ) {
128                throw new MalformedBundle(
129                    'translate-messagebundle-error-invalid-prioritylanguage',
130                    [ Message::listParam( $invalidLanguageCodes ), count( $invalidLanguageCodes ) ]
131                );
132            }
133        }
134    }
135
136    public function save(
137        Title $pageTitle,
138        RevisionRecord $revisionRecord,
139        MessageBundleContent $content
140    ): void {
141        // Validate the content before saving
142        $this->validate( $pageTitle, $content );
143
144        $previousRevisionId = $this->revTagStore->getLatestRevisionWithTag( $pageTitle, RevTagStore::MB_VALID_TAG );
145        if ( $previousRevisionId !== null ) {
146            $this->revTagStore->removeTags( $pageTitle, RevTagStore::MB_VALID_TAG );
147        }
148
149        if ( $content->isValid() ) {
150            // Bundle is valid and contains translatable messages
151            $this->revTagStore->replaceTag( $pageTitle, RevTagStore::MB_VALID_TAG, $revisionRecord->getId() );
152            MessageBundle::clearSourcePageCache();
153
154            // Defer most of the heavy work to the job queue
155            $job = UpdateMessageBundleJob::newJob( $pageTitle, $revisionRecord->getId(), $previousRevisionId );
156
157            $this->jobQueue->push( $job );
158
159            // A new message bundle, set the source language.
160            $definedLanguageCode = $content->getMetadata()->getSourceLanguageCode();
161            $pageLanguageCode = $pageTitle->getPageLanguage()->getCode();
162            if ( $previousRevisionId === null ) {
163                if ( $definedLanguageCode !== $pageLanguageCode ) {
164                    $context = RequestContext::getMain();
165                    SpecialPageLanguage::changePageLanguage(
166                        $context,
167                        $pageTitle,
168                        $definedLanguageCode,
169                        wfMessage( 'translate-messagebundle-change-sourcelanguage' )->inContentLanguage()
170                    );
171                }
172            }
173
174            // Save the metadata
175            $messageBundle = new MessageBundle( $pageTitle );
176            $groupId = $messageBundle->getMessageGroupId();
177
178            $metadata = $content->getMetadata();
179            $priorityForce = $metadata->areOnlyPriorityLanguagesAllowed() ? 'on' : false;
180            $priorityLanguages = $metadata->getPriorityLanguages();
181            $priorityLanguages = $priorityLanguages ? implode( ',', $priorityLanguages ) : false;
182
183            $this->messageGroupMetadata->set( $groupId, 'prioritylangs', $priorityLanguages );
184            $this->messageGroupMetadata->set( $groupId, 'priorityforce', $priorityForce );
185
186            $description = $metadata->getDescription();
187            $this->messageGroupMetadata->set( $groupId, 'description', $description ?? false );
188
189            $label = $metadata->getLabel();
190            $this->messageGroupMetadata->set( $groupId, 'label', $label ?? false );
191        }
192
193        // What should we do if there are no messages? Use the previous version? Remove the group?
194        // Currently, the bundle is removed from translation.
195    }
196}