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