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