Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 156 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
ApiSectionTranslationSave | |
0.00% |
0 / 156 |
|
0.00% |
0 / 10 |
552 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
validateRequest | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
90 | |||
execute | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
20 | |||
createNewTranslationFromPayload | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
saveTranslation | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
6 | |||
saveSectionTranslation | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
6 | |||
getAllowedParams | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
2 | |||
needsToken | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isWriteMode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isInternal | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * @copyright See AUTHORS.txt |
4 | * @license GPL-2.0-or-later |
5 | */ |
6 | |
7 | namespace ContentTranslation\ActionApi; |
8 | |
9 | use ContentTranslation\Entity\SectionTranslation; |
10 | use ContentTranslation\Exception\InvalidSectionDataException; |
11 | use ContentTranslation\LoadBalancer; |
12 | use ContentTranslation\Manager\TranslationCorporaManager; |
13 | use ContentTranslation\Service\SandboxTitleMaker; |
14 | use ContentTranslation\SiteMapper; |
15 | use ContentTranslation\Store\SectionTranslationStore; |
16 | use ContentTranslation\Store\TranslationStore; |
17 | use ContentTranslation\Translation; |
18 | use ContentTranslation\Translator; |
19 | use MediaWiki\Api\ApiBase; |
20 | use MediaWiki\Api\ApiMain; |
21 | use MediaWiki\Api\ApiUsageException; |
22 | use MediaWiki\Languages\LanguageNameUtils; |
23 | use MediaWiki\Title\TitleFactory; |
24 | use MediaWiki\User\User; |
25 | use Wikimedia\ParamValidator\ParamValidator; |
26 | |
27 | class ApiSectionTranslationSave extends ApiBase { |
28 | private TranslationCorporaManager $corporaManager; |
29 | private LoadBalancer $lb; |
30 | private SectionTranslationStore $sectionTranslationStore; |
31 | private SandboxTitleMaker $sandboxTitleMaker; |
32 | private TitleFactory $titleFactory; |
33 | private LanguageNameUtils $languageNameUtils; |
34 | private TranslationStore $translationStore; |
35 | |
36 | public function __construct( |
37 | ApiMain $mainModule, |
38 | string $action, |
39 | TranslationCorporaManager $corporaManager, |
40 | LoadBalancer $loadBalancer, |
41 | SectionTranslationStore $sectionTranslationStore, |
42 | SandboxTitleMaker $sandboxTitleMaker, |
43 | TitleFactory $titleFactory, |
44 | LanguageNameUtils $languageNameUtils, |
45 | TranslationStore $translationStore |
46 | ) { |
47 | parent::__construct( $mainModule, $action ); |
48 | $this->corporaManager = $corporaManager; |
49 | $this->lb = $loadBalancer; |
50 | $this->sectionTranslationStore = $sectionTranslationStore; |
51 | $this->sandboxTitleMaker = $sandboxTitleMaker; |
52 | $this->titleFactory = $titleFactory; |
53 | $this->languageNameUtils = $languageNameUtils; |
54 | $this->translationStore = $translationStore; |
55 | } |
56 | |
57 | private function validateRequest() { |
58 | if ( $this->lb->getConnection( DB_PRIMARY )->isReadOnly() ) { |
59 | $this->dieReadOnly(); |
60 | } |
61 | |
62 | $user = $this->getUser(); |
63 | |
64 | if ( !$user->isNamed() ) { |
65 | $this->dieWithError( 'apierror-sxsave-anon-user' ); |
66 | } |
67 | |
68 | $block = $user->getBlock(); |
69 | if ( $block && $block->isSitewide() ) { |
70 | $this->dieBlocked( $block ); |
71 | } |
72 | |
73 | if ( $user->pingLimiter( 'sxsave' ) ) { |
74 | $this->dieWithError( 'apierror-ratelimited' ); |
75 | } |
76 | |
77 | $params = $this->extractRequestParams(); |
78 | if ( !$this->languageNameUtils->isKnownLanguageTag( $params['sourcelanguage'] ) ) { |
79 | $this->dieWithError( 'apierror-cx-invalidsourcelanguage', 'invalidsourcelanguage' ); |
80 | } |
81 | |
82 | if ( !$this->languageNameUtils->isKnownLanguageTag( $params['targetlanguage'] ) ) { |
83 | $this->dieWithError( 'apierror-cx-invalidtargetlanguage', 'invalidtargetlanguage' ); |
84 | } |
85 | |
86 | if ( trim( $params['content'] ) === '' ) { |
87 | $this->dieWithError( [ 'apierror-paramempty', 'content' ], 'invalidcontent' ); |
88 | } |
89 | } |
90 | |
91 | /** |
92 | * @throws ApiUsageException |
93 | */ |
94 | public function execute() { |
95 | $this->validateRequest(); |
96 | $params = $this->extractRequestParams(); |
97 | $user = $this->getUser(); |
98 | $targetTitleRaw = $params['targettitle']; |
99 | $isSandbox = $params['issandbox']; |
100 | if ( $isSandbox ) { |
101 | $targetTitle = $this->sandboxTitleMaker->makeSandboxTitle( $user, $targetTitleRaw ); |
102 | } else { |
103 | $targetTitle = $this->titleFactory->newFromText( $targetTitleRaw ); |
104 | } |
105 | |
106 | if ( !$targetTitle ) { |
107 | $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $targetTitleRaw ) ] ); |
108 | } |
109 | |
110 | $translation = $this->saveTranslation( |
111 | $user, |
112 | $params['sourcelanguage'], |
113 | $params['targetlanguage'], |
114 | $params['sourcetitle'], |
115 | $targetTitle->getPrefixedText(), |
116 | $params['sourcerevision'] |
117 | ); |
118 | $translationId = $translation->getTranslationId(); |
119 | |
120 | try { |
121 | $this->corporaManager->saveTranslationUnits( $translation, $params['content'] ); |
122 | } catch ( InvalidSectionDataException $exception ) { |
123 | $this->dieWithError( 'apierror-cx-invalidsectiondata', 'invalidcontent' ); |
124 | } |
125 | $sectionTranslationId = $this->saveSectionTranslation( |
126 | $translationId, |
127 | $params['sectionid'], |
128 | $params['sourcesectiontitle'], |
129 | $params['targetsectiontitle'], |
130 | $params['progress'] |
131 | ); |
132 | $result = [ |
133 | 'result' => 'success', |
134 | 'sectiontranslationid' => $sectionTranslationId, |
135 | 'translationid' => $translationId |
136 | ]; |
137 | $this->getResult()->addValue( null, $this->getModuleName(), $result ); |
138 | } |
139 | |
140 | /** |
141 | * This method creates a new Translation model for the saved translation and returns it |
142 | * |
143 | * @param string $sourceLanguage |
144 | * @param string $sourceTitle |
145 | * @param string $targetLanguage |
146 | * @param string $targetTitle |
147 | * @param string $sourceRevision |
148 | * @return Translation |
149 | */ |
150 | private function createNewTranslationFromPayload( |
151 | string $sourceLanguage, |
152 | string $sourceTitle, |
153 | string $targetLanguage, |
154 | string $targetTitle, |
155 | string $sourceRevision |
156 | ): Translation { |
157 | $translationData = [ |
158 | 'sourceTitle' => $sourceTitle, |
159 | 'targetTitle' => $targetTitle, |
160 | 'sourceLanguage' => $sourceLanguage, |
161 | 'targetLanguage' => $targetLanguage, |
162 | 'sourceRevisionId' => $sourceRevision, |
163 | 'sourceURL' => SiteMapper::getPageURL( $sourceLanguage, $sourceTitle ), |
164 | 'status' => TranslationStore::TRANSLATION_STATUS_DRAFT, |
165 | 'progress' => json_encode( [ "any" => null, "mt" => null, "human" => null ] ), |
166 | 'cxVersion' => 3, |
167 | ]; |
168 | |
169 | return new Translation( $translationData ); |
170 | } |
171 | |
172 | protected function saveTranslation( |
173 | User $user, |
174 | string $sourceLanguage, |
175 | string $targetLanguage, |
176 | string $sourceTitle, |
177 | string $targetTitle, |
178 | string $sourceRevision |
179 | ): Translation { |
180 | $translation = $this->translationStore->findTranslationByUser( |
181 | $user, |
182 | $sourceTitle, |
183 | $sourceLanguage, |
184 | $targetLanguage |
185 | ); |
186 | |
187 | if ( !$translation ) { |
188 | $translation = $this->createNewTranslationFromPayload( |
189 | $sourceLanguage, |
190 | $sourceTitle, |
191 | $targetLanguage, |
192 | $targetTitle, |
193 | $sourceRevision |
194 | ); |
195 | } else { |
196 | $translation->translation['sourceRevisionId'] = $sourceRevision; |
197 | // target title can be changed any time during translation |
198 | $translation->translation['targetTitle'] = $targetTitle; |
199 | } |
200 | $this->translationStore->saveTranslation( $translation, $user ); |
201 | |
202 | // Associate the translation with the translator |
203 | $translator = new Translator( $user ); |
204 | $translationId = $translation->getTranslationId(); |
205 | $translator->addTranslation( $translationId ); |
206 | |
207 | return $translation; |
208 | } |
209 | |
210 | /** |
211 | * Given a translation id (corresponding to a row inside "cx_translations" table), this |
212 | * method creates a new SectionTranslation model and stores it inside "cx_section_translations" |
213 | * table. |
214 | * |
215 | * Lead sections are also stored inside the table. For such sections we set empty strings as |
216 | * values for "cxsx_source_section_title" and "cxsx_target_section_title" values, as empty |
217 | * strings are considered valid values for non-nullable fields in MySQL. |
218 | * |
219 | * @param int $translationId |
220 | * @param string $sectionId |
221 | * @param string $sourceSectionTitle |
222 | * @param string $targetSectionTitle |
223 | * @return int the id (cxsx_id) of the saved section translation |
224 | */ |
225 | private function saveSectionTranslation( |
226 | int $translationId, |
227 | string $sectionId, |
228 | string $sourceSectionTitle, |
229 | string $targetSectionTitle, |
230 | string $progress |
231 | ): int { |
232 | $sectionTranslation = $this->sectionTranslationStore->findTranslation( $translationId, $sectionId ); |
233 | $draftStatusIndex = SectionTranslationStore::getStatusIndexByStatus( |
234 | SectionTranslationStore::TRANSLATION_STATUS_DRAFT |
235 | ); |
236 | |
237 | if ( !$sectionTranslation ) { |
238 | $sectionTranslation = new SectionTranslation( |
239 | null, |
240 | $translationId, |
241 | $sectionId, |
242 | $sourceSectionTitle, |
243 | $targetSectionTitle, |
244 | $draftStatusIndex, |
245 | $progress |
246 | ); |
247 | $this->sectionTranslationStore->insertTranslation( $sectionTranslation ); |
248 | } else { |
249 | // update updatable fields |
250 | $sectionTranslation->setTargetSectionTitle( $targetSectionTitle ); |
251 | $sectionTranslation->setTranslationStatus( $draftStatusIndex ); |
252 | $sectionTranslation->setProgress( $progress ); |
253 | $this->sectionTranslationStore->updateTranslation( $sectionTranslation ); |
254 | } |
255 | |
256 | // the id of the section translation is always set, since the entity has been stored in the database |
257 | // @phan-suppress-next-line PhanTypeMismatchReturnNullable |
258 | return $sectionTranslation->getId(); |
259 | } |
260 | |
261 | public function getAllowedParams() { |
262 | return [ |
263 | 'sourcelanguage' => [ |
264 | ParamValidator::PARAM_REQUIRED => true, |
265 | ], |
266 | 'targetlanguage' => [ |
267 | ParamValidator::PARAM_REQUIRED => true, |
268 | ], |
269 | 'sourcetitle' => [ |
270 | ParamValidator::PARAM_REQUIRED => true, |
271 | ], |
272 | 'targettitle' => [ |
273 | ParamValidator::PARAM_REQUIRED => true, |
274 | ], |
275 | 'content' => [ |
276 | ParamValidator::PARAM_REQUIRED => true, |
277 | ], |
278 | 'sourcerevision' => [ |
279 | ParamValidator::PARAM_TYPE => 'integer', |
280 | ParamValidator::PARAM_REQUIRED => true, |
281 | ], |
282 | 'sourcesectiontitle' => [ |
283 | ParamValidator::PARAM_TYPE => 'string', |
284 | ParamValidator::PARAM_REQUIRED => true, |
285 | ], |
286 | 'targetsectiontitle' => [ |
287 | ParamValidator::PARAM_TYPE => 'string', |
288 | ParamValidator::PARAM_REQUIRED => true, |
289 | ], |
290 | 'sectionid' => [ |
291 | ParamValidator::PARAM_TYPE => 'string', |
292 | ParamValidator::PARAM_REQUIRED => true |
293 | ], |
294 | 'issandbox' => [ |
295 | ParamValidator::PARAM_TYPE => 'boolean', |
296 | ParamValidator::PARAM_REQUIRED => false, |
297 | ], |
298 | 'progress' => [ |
299 | ParamValidator::PARAM_REQUIRED => true, |
300 | ], |
301 | ]; |
302 | } |
303 | |
304 | public function needsToken() { |
305 | return 'csrf'; |
306 | } |
307 | |
308 | public function isWriteMode() { |
309 | return true; |
310 | } |
311 | |
312 | public function isInternal() { |
313 | return true; |
314 | } |
315 | } |