Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
38.25% |
96 / 251 |
|
18.18% |
2 / 11 |
CRAP | |
0.00% |
0 / 1 |
SpecialEditLexicon | |
38.25% |
96 / 251 |
|
18.18% |
2 / 11 |
544.30 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
19.40% |
13 / 67 |
|
0.00% |
0 / 1 |
116.62 | |||
redirectToLoginPage | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
4.00 | |||
getLookupFields | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
2 | |||
getSelectFields | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
20 | |||
getAddFields | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
2 | |||
getEditFields | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
12 | |||
submit | |
94.55% |
52 / 55 |
|
0.00% |
0 / 1 |
14.03 | |||
purgeOriginPageUtterances | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getLanguageOptions | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
success | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Wikispeech\Specials; |
4 | |
5 | /** |
6 | * @file |
7 | * @ingroup Extensions |
8 | * @license GPL-2.0-or-later |
9 | */ |
10 | |
11 | use Exception; |
12 | use HTMLForm; |
13 | use MediaWiki\Html\Html; |
14 | use MediaWiki\Languages\LanguageNameUtils; |
15 | use MediaWiki\Logger\LoggerFactory; |
16 | use MediaWiki\Wikispeech\Lexicon\LexiconEntry; |
17 | use MediaWiki\Wikispeech\Lexicon\LexiconEntryItem; |
18 | use MediaWiki\Wikispeech\Lexicon\LexiconStorage; |
19 | use MediaWiki\Wikispeech\SpeechoidConnector; |
20 | use MediaWiki\Wikispeech\Utterance\UtteranceStore; |
21 | use MWException; |
22 | use Psr\Log\LoggerInterface; |
23 | use SpecialPage; |
24 | |
25 | /** |
26 | * Special page for editing the lexicon. |
27 | * |
28 | * @since 0.1.8 |
29 | */ |
30 | |
31 | class SpecialEditLexicon extends SpecialPage { |
32 | |
33 | /** @var LanguageNameUtils */ |
34 | private $languageNameUtils; |
35 | |
36 | /** @var LexiconStorage */ |
37 | private $lexiconStorage; |
38 | |
39 | /** @var SpeechoidConnector */ |
40 | private $speechoidConnector; |
41 | |
42 | /** @var LexiconEntryItem */ |
43 | private $modifiedItem; |
44 | |
45 | /** @var LoggerInterface */ |
46 | private $logger; |
47 | |
48 | /** @var string */ |
49 | private $postHtml; |
50 | |
51 | /** |
52 | * @since 0.1.11 Removed ConfigFactory |
53 | * @since 0.1.8 |
54 | * @param LanguageNameUtils $languageNameUtils |
55 | * @param LexiconStorage $lexiconStorage |
56 | * @param SpeechoidConnector $speechoidConnector |
57 | */ |
58 | public function __construct( |
59 | $languageNameUtils, |
60 | $lexiconStorage, |
61 | $speechoidConnector |
62 | ) { |
63 | parent::__construct( 'EditLexicon', 'wikispeech-edit-lexicon' ); |
64 | $this->languageNameUtils = $languageNameUtils; |
65 | $this->lexiconStorage = $lexiconStorage; |
66 | $this->speechoidConnector = $speechoidConnector; |
67 | $this->logger = LoggerFactory::getInstance( 'Wikispeech' ); |
68 | $this->postHtml = ''; |
69 | } |
70 | |
71 | /** |
72 | * @since 0.1.8 |
73 | * @param string|null $subpage |
74 | */ |
75 | public function execute( $subpage ) { |
76 | $this->setHeaders(); |
77 | if ( $this->redirectToLoginPage() ) { |
78 | return; |
79 | } |
80 | |
81 | $this->checkPermissions(); |
82 | $this->addHelpLink( 'Help:Extension:Wikispeech/Lexicon editor' ); |
83 | try { |
84 | $this->speechoidConnector->requestLexicons(); |
85 | } catch ( Exception $e ) { |
86 | $this->logger->error( 'Speechoid is down.' ); |
87 | $this->getOutput()->showErrorPage( |
88 | 'error', |
89 | 'wikispeech-edit-lexicon-speechoid-down' |
90 | ); |
91 | return; |
92 | } |
93 | $request = $this->getRequest(); |
94 | $language = $request->getText( 'language' ); |
95 | $word = $request->getText( 'word' ); |
96 | if ( $request->getText( 'id' ) === '' ) { |
97 | $id = ''; |
98 | } else { |
99 | $id = $request->getIntOrNull( 'id' ); |
100 | } |
101 | $entry = $this->lexiconStorage->getEntry( $language, $word ); |
102 | $copyrightNote = $this->msg( 'wikispeech-lexicon-copyrightnote' )->parse(); |
103 | $this->postHtml = Html::rawElement( 'p', [], $copyrightNote ); |
104 | $successMessage = ''; |
105 | |
106 | $formId = ''; |
107 | $submitMessage = 'wikispeech-lexicon-next'; |
108 | if ( !$language || !$word ) { |
109 | $formId = 'lookup'; |
110 | $fields = $this->getLookupFields(); |
111 | } elseif ( $entry === null ) { |
112 | $formId = 'newEntry'; |
113 | $fields = $this->getAddFields( $language, $word ); |
114 | $submitMessage = 'wikispeech-lexicon-save'; |
115 | $successMessage = 'wikispeech-lexicon-add-entry-success'; |
116 | } elseif ( !in_array( 'id', $request->getValueNames() ) ) { |
117 | $formId = 'selectItem'; |
118 | $fields = $this->getSelectFields( $language, $word, $entry ); |
119 | } elseif ( $id ) { |
120 | $formId = 'editItem'; |
121 | $fields = $this->getEditFields( $language, $word, $id ); |
122 | $submitMessage = 'wikispeech-lexicon-save'; |
123 | $successMessage = 'wikispeech-lexicon-edit-entry-success'; |
124 | } elseif ( $id === '' ) { |
125 | $formId = 'newItem'; |
126 | $fields = $this->getAddFields( $language, $word ); |
127 | $submitMessage = 'wikispeech-lexicon-save'; |
128 | $successMessage = 'wikispeech-lexicon-add-entry-success'; |
129 | } else { |
130 | // We have a set of parameters that we can't do anything |
131 | // with. Show the first page. |
132 | $formId = 'lookup'; |
133 | $fields = $this->getLookupFields(); |
134 | } |
135 | |
136 | // Set default values from the parameters. |
137 | foreach ( $fields as $field ) { |
138 | $name = $field['name']; |
139 | $value = $request->getVal( $name ); |
140 | if ( $value !== null ) { |
141 | // There's no extra conversion logic so default values |
142 | // are set to strings and handled down the |
143 | // line. E.g. boolean values are true for "false" or |
144 | // "no". |
145 | $fields[$name]['default'] = $value; |
146 | } |
147 | } |
148 | $form = HTMLForm::factory( |
149 | 'ooui', |
150 | $fields, |
151 | $this->getContext() |
152 | ); |
153 | $form->setFormIdentifier( $formId ); |
154 | $form->setSubmitCallback( [ $this, 'submit' ] ); |
155 | $form->setSubmitTextMsg( $submitMessage ); |
156 | $form->setPostHtml( $this->postHtml ); |
157 | if ( $form->show() && $successMessage ) { |
158 | $this->success( $successMessage ); |
159 | } |
160 | |
161 | $this->getOutput()->addModules( [ |
162 | 'ext.wikispeech.specialEditLexicon' |
163 | ] ); |
164 | } |
165 | |
166 | /** |
167 | * Redirect the user to login page when appropriate |
168 | * |
169 | * If the the user is not logged in and config variable |
170 | * `WikispeechEditLexiconAutoLogin` is true this adds a redirect |
171 | * to the output. Returns to this special page after login and |
172 | * keeps any URL parameters that were originally given. |
173 | * |
174 | * @since 0.1.11 |
175 | * @return bool True if a redirect was added, else false. |
176 | */ |
177 | private function redirectToLoginPage(): bool { |
178 | if ( $this->getUser()->isNamed() ) { |
179 | // User already logged in. |
180 | return false; |
181 | } |
182 | |
183 | if ( !$this->getConfig()->get( 'WikispeechEditLexiconAutoLogin' ) ) { |
184 | return false; |
185 | } |
186 | |
187 | $lexiconUrl = $this->getPageTitle()->getPrefixedText(); |
188 | $lexiconRequest = $this->getRequest(); |
189 | |
190 | $loginParemeters = [ |
191 | 'returnto' => $lexiconUrl |
192 | ]; |
193 | if ( $lexiconRequest->getValues() ) { |
194 | // Add any parameters to this page to include after |
195 | // logging in. |
196 | $lexiconParametersString = http_build_query( $lexiconRequest->getValues() ); |
197 | $loginParemeters['returntoquery'] = $lexiconParametersString; |
198 | } |
199 | $title = SpecialPage::getTitleFor( 'Userlogin' ); |
200 | $loginUrl = $title->getFullURL( $loginParemeters ); |
201 | $this->getOutput()->redirect( $loginUrl ); |
202 | |
203 | return true; |
204 | } |
205 | |
206 | /** |
207 | * Create a field descriptor for looking up a word |
208 | * |
209 | * Has one field for language and one for word. |
210 | * |
211 | * @since 0.1.10 |
212 | * @return array |
213 | */ |
214 | private function getLookupFields(): array { |
215 | $fields = [ |
216 | 'language' => [ |
217 | 'name' => 'language', |
218 | 'type' => 'select', |
219 | 'label' => $this->msg( 'wikispeech-language' )->text(), |
220 | 'options' => $this->getLanguageOptions(), |
221 | 'id' => 'ext-wikispeech-language' |
222 | ], |
223 | 'word' => [ |
224 | 'name' => 'word', |
225 | 'type' => 'text', |
226 | 'label' => $this->msg( 'wikispeech-word' )->text(), |
227 | 'required' => true |
228 | ], |
229 | 'page' => [ |
230 | 'name' => 'page', |
231 | 'type' => 'hidden' |
232 | ] |
233 | ]; |
234 | return $fields; |
235 | } |
236 | |
237 | /** |
238 | * Create a field descriptor for selecting an item |
239 | * |
240 | * Has a field for selecting the id of the item to edit or "new" |
241 | * for creating a new item. Also shows fields for language and |
242 | * word from previous page, but readonly. |
243 | * |
244 | * @since 0.1.10 |
245 | * @param string $language |
246 | * @param string $word |
247 | * @param LexiconEntry|null $entry |
248 | * @return array |
249 | */ |
250 | private function getSelectFields( |
251 | string $language, |
252 | string $word, |
253 | ?LexiconEntry $entry = null |
254 | ): array { |
255 | $fields = $this->getLookupFields(); |
256 | $fields['language']['readonly'] = true; |
257 | $fields['language']['type'] = 'text'; |
258 | $fields['word']['readonly'] = true; |
259 | $fields['word']['required'] = false; |
260 | |
261 | $newLabel = $this->msg( 'wikispeech-lexicon-new' )->text(); |
262 | $itemOptions = [ $newLabel => '' ]; |
263 | if ( $entry ) { |
264 | foreach ( $entry->getItems() as $item ) { |
265 | $properties = $item->getProperties(); |
266 | if ( !isset( $properties->id ) ) { |
267 | $this->logger->warning( |
268 | __METHOD__ . ': Skipping item with no id.' |
269 | ); |
270 | continue; |
271 | } |
272 | $id = $properties->id; |
273 | // Add item id as option for selection. |
274 | $itemOptions[$id] = $id; |
275 | // Add item to info text. |
276 | $this->postHtml .= Html::element( 'pre', [], $item ); |
277 | } |
278 | } |
279 | |
280 | $fields['id'] = [ |
281 | 'name' => 'id', |
282 | 'type' => 'select', |
283 | 'label' => $this->msg( 'wikispeech-item-id' )->text(), |
284 | 'options' => $itemOptions, |
285 | 'default' => '' |
286 | ]; |
287 | return $fields; |
288 | } |
289 | |
290 | /** |
291 | * Create a field descriptor for adding an entry or item |
292 | * |
293 | * Has fields for transcription and preferred. Item id is held by |
294 | * a hidden field. Also shows fields for language and word from |
295 | * previous page, but readonly. |
296 | * |
297 | * @since 0.1.10 |
298 | * @param string $language |
299 | * @param string $word |
300 | * @return array |
301 | */ |
302 | private function getAddFields( string $language, string $word ): array { |
303 | $fields = $this->getSelectFields( $language, $word ); |
304 | $fields['id']['type'] = 'hidden'; |
305 | $fields += [ |
306 | 'transcription' => [ |
307 | 'name' => 'transcription', |
308 | 'type' => 'textwithbutton', |
309 | 'label' => $this->msg( 'wikispeech-transcription' )->text(), |
310 | 'required' => true, |
311 | 'id' => 'ext-wikispeech-transcription', |
312 | 'buttontype' => 'button', |
313 | 'buttondefault' => $this->msg( 'wikispeech-preview' )->text(), |
314 | 'buttonid' => 'ext-wikispeech-preview-button' |
315 | ], |
316 | 'preferred' => [ |
317 | 'name' => 'preferred', |
318 | 'type' => 'check', |
319 | 'label' => $this->msg( 'wikispeech-preferred' )->text() |
320 | ] |
321 | ]; |
322 | return $fields; |
323 | } |
324 | |
325 | /** |
326 | * Create a field descriptor for editing an item |
327 | * |
328 | * Has fields for transcription and preferred with default values |
329 | * from the lexicon. Item id is held by a hidden field. Also shows |
330 | * fields for language and word from previous page, but readonly. |
331 | * |
332 | * @since 0.1.10 |
333 | * @param string $language |
334 | * @param string $word |
335 | * @param int $id |
336 | * @return array |
337 | */ |
338 | private function getEditFields( string $language, string $word, int $id ): array { |
339 | $fields = $this->getAddFields( $language, $word ); |
340 | $entry = $this->lexiconStorage->getEntry( $language, $word ); |
341 | $item = $entry->findItemBySpeechoidIdentity( $id ); |
342 | if ( $item === null ) { |
343 | $this->getOutput()->addHTML( Html::errorBox( |
344 | $this->getOutput()->msg( 'wikispeech-edit-lexicon-no-item-found' )->params( $id )->parse() |
345 | ) ); |
346 | return $this->getSelectFields( $language, $word, $entry ); |
347 | } |
348 | $transcriptionStatus = $this->speechoidConnector->toIpa( |
349 | $item->getTranscription(), |
350 | $language |
351 | ); |
352 | if ( $transcriptionStatus->isOk() ) { |
353 | $transcription = $transcriptionStatus->getValue(); |
354 | } else { |
355 | $transcription = ''; |
356 | $this->getOutput()->addHTML( Html::errorBox( |
357 | $this->getOutput()->msg( 'wikispeech-edit-lexicon-transcription-unretrievable' )->params( $id )->parse() |
358 | ) ); |
359 | } |
360 | |
361 | $fields['transcription']['default'] = $transcription; |
362 | $fields['preferred']['default'] = $item->getPreferred(); |
363 | return $fields; |
364 | } |
365 | |
366 | /** |
367 | * Handle submit request |
368 | * |
369 | * If there is no entry for the given word a new one is created |
370 | * with a new item. If the request contains an id that item is |
371 | * updated or, if id is empty, a new item is created. If there |
372 | * isn't enough information to do any of the above this returns |
373 | * false which sends the user to the appropriate page via |
374 | * `execute()`. |
375 | * |
376 | * @since 0.1.9 |
377 | * @param array $data |
378 | * @return bool |
379 | */ |
380 | public function submit( array $data ): bool { |
381 | if ( |
382 | !array_key_exists( 'language', $data ) || |
383 | !array_key_exists( 'word', $data ) || |
384 | !array_key_exists( 'id', $data ) || |
385 | !array_key_exists( 'transcription', $data ) || |
386 | $data['transcription'] === null || |
387 | !array_key_exists( 'preferred', $data ) |
388 | ) { |
389 | // We don't have all the information we need to make an |
390 | // edit yet. |
391 | return false; |
392 | } |
393 | |
394 | $language = $data['language']; |
395 | $transcription = $data['transcription']; |
396 | $sampaStatus = $this->speechoidConnector->fromIpa( |
397 | $transcription, |
398 | $language |
399 | ); |
400 | if ( !$sampaStatus->isOk() ) { |
401 | // TODO: Show error message (T308562). |
402 | return false; |
403 | } |
404 | |
405 | $sampa = $sampaStatus->getValue(); |
406 | $word = $data['word']; |
407 | $id = $data['id']; |
408 | $preferred = $data['preferred']; |
409 | if ( $id === '' ) { |
410 | // Empty id, create new item. |
411 | $item = new LexiconEntryItem(); |
412 | $properties = [ |
413 | 'strn' => $word, |
414 | 'transcriptions' => [ (object)[ 'strn' => $sampa ] ], |
415 | // Status is required by Speechoid. |
416 | 'status' => (object)[ |
417 | 'name' => 'ok' |
418 | ] |
419 | ]; |
420 | if ( $preferred ) { |
421 | $properties['preferred'] = true; |
422 | } |
423 | $item->setProperties( (object)$properties ); |
424 | $this->lexiconStorage->createEntryItem( |
425 | $language, |
426 | $word, |
427 | $item |
428 | ); |
429 | } else { |
430 | // Id already exists, update item. |
431 | $entry = $this->lexiconStorage->getEntry( $language, $word ); |
432 | $item = $entry->findItemBySpeechoidIdentity( intval( $id ) ); |
433 | if ( $item === null ) { |
434 | throw new MWException( "No item with id '$id' found." ); |
435 | } |
436 | $properties = $item->getProperties(); |
437 | $properties->transcriptions = [ (object)[ 'strn' => $sampa ] ]; |
438 | if ( $preferred ) { |
439 | $properties->preferred = true; |
440 | } else { |
441 | unset( $properties->preferred ); |
442 | } |
443 | |
444 | $item->setProperties( $properties ); |
445 | $this->lexiconStorage->updateEntryItem( |
446 | $language, |
447 | $word, |
448 | $item |
449 | ); |
450 | } |
451 | // Item is updated by createEntryItem(), so we just need to |
452 | // store it. |
453 | $this->modifiedItem = $item; |
454 | |
455 | if ( array_key_exists( 'page', $data ) && $data['page'] ) { |
456 | // @todo Introduce $consumerUrl to request parameters and |
457 | // @todo pass it down here. Currently we're passing null, |
458 | // @todo meaning it only support flushing local wiki |
459 | // @todo utterances. |
460 | $this->purgeOriginPageUtterances( $data['page'], null ); |
461 | } |
462 | |
463 | return true; |
464 | } |
465 | |
466 | /** |
467 | * Immediately removes any utterance from the origin page. |
468 | * @since 0.1.8 |
469 | * @param int $pageId |
470 | * @param string|null $consumerUrl |
471 | */ |
472 | private function purgeOriginPageUtterances( int $pageId, ?string $consumerUrl ) { |
473 | $utteranceStore = new UtteranceStore(); |
474 | $utteranceStore->flushUtterancesByPage( $consumerUrl, $pageId ); |
475 | } |
476 | |
477 | /** |
478 | * Make options to be used by in a select field |
479 | * |
480 | * Each language that is specified in the config variable |
481 | * "WikispeechVoices" is included in the options. The labels are |
482 | * of the format "code - autonym". |
483 | * |
484 | * @since 0.1.8 |
485 | * @return array Keys are labels and values are language codes. |
486 | */ |
487 | private function getLanguageOptions(): array { |
488 | $voices = $this->getConfig()->get( 'WikispeechVoices' ); |
489 | $languages = array_keys( $voices ); |
490 | sort( $languages ); |
491 | $options = []; |
492 | foreach ( $languages as $code ) { |
493 | $name = $this->languageNameUtils->getLanguageName( $code ); |
494 | $label = "$code - $name"; |
495 | $options[$label] = $code; |
496 | } |
497 | ksort( $options ); |
498 | return $options; |
499 | } |
500 | |
501 | /** |
502 | * Show success page containing the properties of the added/edited item |
503 | * |
504 | * @since 0.1.9 |
505 | * @param string $message Success message. |
506 | */ |
507 | private function success( $message ) { |
508 | $this->getOutput()->addHtml( |
509 | Html::successBox( |
510 | $this->msg( $message )->parse() |
511 | ) |
512 | ); |
513 | $this->getOutput()->addHtml( |
514 | Html::element( 'pre', [], $this->modifiedItem ) |
515 | ); |
516 | } |
517 | } |