Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.61% covered (success)
98.61%
425 / 431
88.46% covered (warning)
88.46%
23 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialNewLexeme
98.61% covered (success)
98.61%
425 / 431
88.46% covered (warning)
88.46%
23 / 26
60
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
1
 factory
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
1
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
100.00% covered (success)
100.00%
72 / 72
100.00% covered (success)
100.00%
1 / 1
3
 getUrlParamsForConfig
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
8
 getItemIdLabelDesc
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 extractLanguageCode
82.61% covered (warning)
82.61%
19 / 23
0.00% covered (danger)
0.00%
0 / 1
7.26
 createExampleParameters
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 createTemplateParamsFromLexemeId
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
5
 processInfoPanelTemplate
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
1
 getLexicalCategorySuggestions
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
3
 termToArrayForJs
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 createForm
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
2
 createEntityFromFormData
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 createSummary
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 redirectToEntityPage
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
 newEditEntity
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 saveEntity
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 getFormFields
100.00% covered (success)
100.00%
60 / 60
100.00% covered (success)
100.00%
1 / 1
2
 setHeaders
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDescription
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkBlocked
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 checkBlockedOnNamespace
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getCopyrightHTML
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 anonymousEditWarning
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace Wikibase\Lexeme\MediaWiki\Specials;
5
6use Exception;
7use HTMLForm;
8use Iterator;
9use LanguageCode;
10use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
11use MediaWiki\Config\ConfigException;
12use MediaWiki\Html\Html;
13use MediaWiki\Html\TemplateParser;
14use MediaWiki\Linker\LinkRenderer;
15use MediaWiki\SpecialPage\SpecialPage;
16use MediaWiki\Status\Status;
17use MediaWiki\User\TempUser\TempUserConfig;
18use Message;
19use OOUI\IconWidget;
20use UserBlockedError;
21use Wikibase\DataModel\Entity\EntityDocument;
22use Wikibase\DataModel\Entity\EntityId;
23use Wikibase\DataModel\Entity\EntityIdParser;
24use Wikibase\DataModel\Entity\EntityIdParsingException;
25use Wikibase\DataModel\Entity\Item;
26use Wikibase\DataModel\Entity\ItemId;
27use Wikibase\DataModel\Entity\PropertyId;
28use Wikibase\DataModel\Services\Lookup\EntityLookup;
29use Wikibase\DataModel\Snak\PropertySomeValueSnak;
30use Wikibase\DataModel\Snak\PropertyValueSnak;
31use Wikibase\DataModel\Term\Term;
32use Wikibase\DataModel\Term\TermFallback;
33use Wikibase\DataModel\Term\TermList;
34use Wikibase\DataModel\Term\TermTypes;
35use Wikibase\Lexeme\DataAccess\ChangeOp\Validation\LemmaTermValidator;
36use Wikibase\Lexeme\Domain\Model\Lexeme;
37use Wikibase\Lexeme\MediaWiki\Specials\HTMLForm\LemmaLanguageField;
38use Wikibase\Lib\FormatableSummary;
39use Wikibase\Lib\SettingsArray;
40use Wikibase\Lib\Store\EntityNamespaceLookup;
41use Wikibase\Lib\Store\FallbackLabelDescriptionLookup;
42use Wikibase\Lib\Store\FallbackLabelDescriptionLookupFactory;
43use Wikibase\Lib\Summary;
44use Wikibase\Repo\AnonymousEditWarningBuilder;
45use Wikibase\Repo\CopyrightMessageBuilder;
46use Wikibase\Repo\EditEntity\EditEntity;
47use Wikibase\Repo\EditEntity\EditEntityStatus;
48use Wikibase\Repo\EditEntity\MediaWikiEditEntityFactory;
49use Wikibase\Repo\Specials\HTMLForm\HTMLItemReferenceField;
50use Wikibase\Repo\Specials\HTMLForm\HTMLTrimmedTextField;
51use Wikibase\Repo\Specials\SpecialPageCopyrightView;
52use Wikibase\Repo\Store\EntityTitleStoreLookup;
53use Wikibase\Repo\SummaryFormatter;
54use Wikibase\Repo\Validators\ValidatorErrorLocalizer;
55use Wikibase\View\EntityIdFormatterFactory;
56use Wikimedia\Assert\Assert;
57
58/**
59 * New page for creating new Lexeme entities.
60 *
61 * @license GPL-2.0-or-later
62 */
63class SpecialNewLexeme extends SpecialPage {
64
65    public const FIELD_LEXEME_LANGUAGE = 'lexeme-language';
66    public const FIELD_LEXICAL_CATEGORY = 'lexicalcategory';
67    public const FIELD_LEMMA = 'lemma';
68    public const FIELD_LEMMA_LANGUAGE = 'lemma-language';
69
70    // used for the info panel and placeholders if the example lexeme is incomplete/missing
71    private const FALLBACK_LANGUAGE_LABEL = 'English';
72    private const FALLBACK_LEXICAL_CATEGORY_LABEL = 'verb';
73
74    private $tags;
75    private $linkRenderer;
76    private $statsDataFactory;
77    private $editEntityFactory;
78    private $entityNamespaceLookup;
79    private $entityTitleLookup;
80    private $entityLookup;
81    private $entityIdParser;
82    private $summaryFormatter;
83    private $entityIdFormatterFactory;
84    private $labelDescriptionLookupFactory;
85    private $validatorErrorLocalizer;
86    private $lemmaTermValidator;
87    private $copyrightView;
88    private AnonymousEditWarningBuilder $anonymousEditWarningBuilder;
89    private TempUserConfig $tempUserConfig;
90
91    public function __construct(
92        array $tags,
93        SpecialPageCopyrightView $copyrightView,
94        LinkRenderer $linkRenderer,
95        StatsdDataFactoryInterface $statsDataFactory,
96        MediaWikiEditEntityFactory $editEntityFactory,
97        EntityNamespaceLookup $entityNamespaceLookup,
98        EntityTitleStoreLookup $entityTitleLookup,
99        EntityLookup $entityLookup,
100        EntityIdParser $entityIdParser,
101        SummaryFormatter $summaryFormatter,
102        EntityIdFormatterFactory $entityIdFormatterFactory,
103        FallbackLabelDescriptionLookupFactory $labelDescriptionLookupFactory,
104        ValidatorErrorLocalizer $validatorErrorLocalizer,
105        LemmaTermValidator $lemmaTermValidator,
106        AnonymousEditWarningBuilder $anonymousEditWarningBuilder,
107        TempUserConfig $tempUserConfig
108    ) {
109        parent::__construct(
110            'NewLexeme',
111            'createpage'
112        );
113
114        $this->tags = $tags;
115        $this->linkRenderer = $linkRenderer;
116        $this->statsDataFactory = $statsDataFactory;
117        $this->editEntityFactory = $editEntityFactory;
118        $this->entityNamespaceLookup = $entityNamespaceLookup;
119        $this->entityTitleLookup = $entityTitleLookup;
120        $this->entityLookup = $entityLookup;
121        $this->entityIdParser = $entityIdParser;
122        $this->summaryFormatter = $summaryFormatter;
123        $this->entityIdFormatterFactory = $entityIdFormatterFactory;
124        $this->labelDescriptionLookupFactory = $labelDescriptionLookupFactory;
125        $this->validatorErrorLocalizer = $validatorErrorLocalizer;
126        $this->lemmaTermValidator = $lemmaTermValidator;
127        $this->copyrightView = $copyrightView;
128        $this->anonymousEditWarningBuilder = $anonymousEditWarningBuilder;
129        $this->tempUserConfig = $tempUserConfig;
130    }
131
132    public static function factory(
133        LinkRenderer $linkRenderer,
134        StatsdDataFactoryInterface $statsDataFactory,
135        TempUserConfig $tempUserConfig,
136        AnonymousEditWarningBuilder $anonymousEditWarningBuilder,
137        MediaWikiEditEntityFactory $editEntityFactory,
138        EntityNamespaceLookup $entityNamespaceLookup,
139        EntityTitleStoreLookup $entityTitleLookup,
140        EntityLookup $entityLookup,
141        EntityIdParser $entityIdParser,
142        SettingsArray $repoSettings,
143        SummaryFormatter $summaryFormatter,
144        EntityIdFormatterFactory $entityIdFormatterFactory,
145        FallbackLabelDescriptionLookupFactory $labelDescriptionLookupFactory,
146        ValidatorErrorLocalizer $validatorErrorLocalizer,
147        LemmaTermValidator $lemmaTermValidator
148    ): self {
149        $copyrightView = new SpecialPageCopyrightView(
150            new CopyrightMessageBuilder(),
151            $repoSettings->getSetting( 'dataRightsUrl' ),
152            $repoSettings->getSetting( 'dataRightsText' )
153        );
154
155        return new self(
156            $repoSettings->getSetting( 'specialPageTags' ),
157            $copyrightView,
158            $linkRenderer,
159            $statsDataFactory,
160            $editEntityFactory,
161            $entityNamespaceLookup,
162            $entityTitleLookup,
163            $entityLookup,
164            $entityIdParser,
165            $summaryFormatter,
166            $entityIdFormatterFactory,
167            $labelDescriptionLookupFactory,
168            $validatorErrorLocalizer,
169            $lemmaTermValidator,
170            $anonymousEditWarningBuilder,
171            $tempUserConfig
172        );
173    }
174
175    public function doesWrites(): bool {
176        return true;
177    }
178
179    /**
180     * @param string|null $subPage
181     */
182    public function execute( $subPage ): void {
183        $this->statsDataFactory->increment( 'wikibase.lexeme.special.NewLexeme.views' );
184
185        parent::execute( $subPage );
186
187        $this->checkBlocked();
188        $this->checkBlockedOnNamespace();
189        $this->checkReadOnly();
190
191        $output = $this->getOutput();
192        $this->setHeaders();
193        $searchUrl = SpecialPage::getTitleFor( 'Search' )
194            ->getFullURL( [
195                'ns' . $this->getConfig()->get( 'LexemeNamespace' ) => '',
196                'search' => $this->getRequest()->getText( self::FIELD_LEMMA ),
197            ] );
198        $searchExisting = $this->msg( 'wikibaselexeme-newlexeme-search-existing' )
199            ->params( $searchUrl )
200            ->parse();
201        $output->addHTML(
202            '<div id="wbl-snl-intro-text-wrapper">'
203            . '<p class="wbl-snl-search-existing-no-js">' . $searchExisting . '</p>'
204            . '</div>'
205        );
206        $output->enableOOUI();
207        $output->addHTML( $this->anonymousEditWarning() );
208        $output->addHTML( '<div class="wbl-snl-main-content">' );
209        $output->addHTML( '<div id="special-newlexeme-root"></div>' );
210        $output->addModules( [
211            'wikibase.lexeme.special.NewLexeme',
212            'wikibase.lexeme.special.NewLexeme.legacyBrowserFallback',
213            ] );
214        $output->addModuleStyles( [
215            'wikibase.lexeme.special.NewLexeme.styles',
216            'wikibase.alltargets', // T322687
217        ] );
218
219        $exampleLexemeParams = $this->createExampleParameters();
220        $form = $this->createForm( $exampleLexemeParams );
221        $form->setSubmitText( $this->msg( 'wikibaselexeme-newlexeme-submit' ) );
222
223        // handle submit (submit callback may create form, see below)
224        // or show form (possibly with errors); status represents submit result
225        $status = $form->show();
226        $output->addModuleStyles( [
227            'oojs-ui.styles.icons-content', // info icon
228            'oojs-ui.styles.icons-alert', // alert icon
229        ] );
230        $output->addHTML(
231            $this->processInfoPanelTemplate( $exampleLexemeParams )
232        );
233        $output->addHTML( '</div>' ); // .wbl-snl-main-content
234        $output->addHTML(
235            '<noscript>'
236            . '<style type="text/css">#special-newlexeme-root {display:none;}</style>'
237            . '</noscript>'
238        );
239
240        if ( $status instanceof Status && $status->isGood() ) {
241            // wrap it, in case HTMLForm turned it into a generic Status
242            $status = EditEntityStatus::wrap( $status );
243            $this->redirectToEntityPage( $status );
244            return;
245        }
246
247        $output->addJsConfigVars( 'wblSpecialNewLexemeParams',
248            $this->getUrlParamsForConfig()
249        );
250        $output->addJsConfigVars(
251            'wblSpecialNewLexemeLexicalCategorySuggestions',
252            $this->getLexicalCategorySuggestions()
253        );
254        $output->addJsConfigVars( 'wblSpecialNewLexemeTempUserEnabled',
255            $this->tempUserConfig->isEnabled()
256        );
257        $output->addJSConfigVars(
258            'wblSpecialNewLexemeExampleData',
259            [
260                'languageLabel' => $exampleLexemeParams['language_item_label'],
261                'lexicalCategoryLabel' => $exampleLexemeParams['lexical_category_item_label'],
262                'lemma' => $exampleLexemeParams['lemma_text'],
263                'spellingVariant' => $exampleLexemeParams['lemma_language'],
264            ]
265        );
266    }
267
268    private function getUrlParamsForConfig(): array {
269        $params = [];
270        $lemma = $this->getRequest()->getText( self::FIELD_LEMMA );
271        if ( $lemma ) {
272            $params['lemma'] = $lemma;
273        }
274
275        $spellVarCode = $this->getRequest()->getText( self::FIELD_LEMMA_LANGUAGE );
276        if ( $spellVarCode ) {
277            $params['spellVarCode'] = $spellVarCode;
278        }
279
280        try {
281            $languageId = $this->entityIdParser->parse(
282                $this->getRequest()->getText( self::FIELD_LEXEME_LANGUAGE )
283            );
284        } catch ( EntityIdParsingException $e ) {
285            $languageId = null;
286        }
287        try {
288            $lexCatId = $this->entityIdParser->parse(
289                $this->getRequest()->getText( self::FIELD_LEXICAL_CATEGORY )
290            );
291        } catch ( EntityIdParsingException $e ) {
292            $lexCatId = null;
293        }
294
295        $idsToPrefetch = array_filter( [ $languageId, $lexCatId ] );
296        if ( !$idsToPrefetch ) {
297            return $params;
298        }
299
300        $labelDescriptionLookup = $this->labelDescriptionLookupFactory->newLabelDescriptionLookup(
301            $this->getLanguage(),
302            $idsToPrefetch,
303            [ TermTypes::TYPE_LABEL, TermTypes::TYPE_DESCRIPTION ]
304        );
305
306        if ( $languageId ) {
307            $params['language'] = $this->getItemIdLabelDesc( $languageId, $labelDescriptionLookup );
308            $params['language']['languageCode'] = $this->extractLanguageCode( $languageId );
309        }
310
311        if ( $lexCatId ) {
312            $params['lexicalCategory'] = $this->getItemIdLabelDesc( $lexCatId, $labelDescriptionLookup );
313        }
314
315        return $params;
316    }
317
318    private function getItemIdLabelDesc(
319        EntityId $itemId,
320        FallbackLabelDescriptionLookup $labelDescriptionLookup
321    ): array {
322        $params = [ 'display' => [] ];
323        $params['id'] = $itemId->getSerialization();
324        $label = $labelDescriptionLookup->getLabel( $itemId );
325        if ( $label !== null ) {
326            $params['display']['label'] = self::termToArrayForJs( $label );
327        }
328        $description = $labelDescriptionLookup->getDescription( $itemId );
329        if ( $description !== null ) {
330            $params['display']['description'] = self::termToArrayForJs( $description );
331        }
332
333        return $params;
334    }
335
336    private function extractLanguageCode( EntityId $languageId ) {
337        $lexemeLanguageCodePropertyIdString = $this->getConfig()->get( 'LexemeLanguageCodePropertyId' );
338        if ( !$lexemeLanguageCodePropertyIdString ) {
339            return null;
340        }
341        $languageItem = $this->entityLookup->getEntity( $languageId );
342        if ( !( $languageItem instanceof Item ) ) {
343            return null;
344        }
345        $lexemeLanguageCodePropertyId = $this->entityIdParser->parse(
346            $lexemeLanguageCodePropertyIdString
347        );
348        if ( !( $lexemeLanguageCodePropertyId instanceof PropertyId ) ) {
349            throw new ConfigException(
350                'LexemeLanguageCodePropertyId must be a property ID, but isn’t: ' . $lexemeLanguageCodePropertyIdString
351            );
352        }
353        $languageCodeStatements = $languageItem->getStatements()->getByPropertyId(
354            $lexemeLanguageCodePropertyId
355        )->getBestStatements();
356        if ( !$languageCodeStatements->isEmpty() ) {
357            $firstBestSnak = $languageCodeStatements->getMainSnaks()[0];
358            if ( $firstBestSnak instanceof PropertyValueSnak ) {
359                return $firstBestSnak->getDataValue()->getValue();
360            }
361            if ( $firstBestSnak instanceof PropertySomeValueSnak ) {
362                return false;
363            }
364        }
365        return null;
366    }
367
368    private function createExampleParameters(): array {
369        $exampleMessage = $this->msg( 'wikibaselexeme-newlexeme-info-panel-example-lexeme-id' );
370        if ( $exampleMessage->exists() ) {
371            $lexemeIdString = trim( $exampleMessage->text() );
372        } else {
373            $lexemeIdString = 'L1';
374        }
375        try {
376            return $this->createTemplateParamsFromLexemeId( $lexemeIdString );
377        } catch ( Exception $_ ) {
378            return [
379                'lexeme_id_HTML' => 'L1',
380                'lemma_text' => 'speak',
381                'lemma_language' => 'en',
382                'language_item_id' => 'Q1',
383                'language_item_label' => self::FALLBACK_LANGUAGE_LABEL,
384                'language_link_HTML' => self::FALLBACK_LANGUAGE_LABEL,
385                'lexical_category_item_id' => 'Q2',
386                'lexical_category_item_label' => self::FALLBACK_LEXICAL_CATEGORY_LABEL,
387                'lexical_category_link_HTML' => self::FALLBACK_LEXICAL_CATEGORY_LABEL,
388            ];
389        }
390    }
391
392    private function createTemplateParamsFromLexemeId( string $lexemeIdString ): array {
393        try {
394            $lexemeId = $this->entityIdParser->parse( $lexemeIdString );
395            $lexeme = $this->entityLookup->getEntity( $lexemeId );
396        } catch ( EntityIdParsingException $e ) {
397            $lexeme = null;
398        }
399        if ( !( $lexeme instanceof Lexeme ) ) {
400            throw new ConfigException(
401                'MediaWiki:wikibaselexeme-newlexeme-info-panel-example-lexeme-id must be ' .
402                'the ID of an existing lexeme, but isn’t: ' . $lexemeIdString
403            );
404        }
405
406        $lemma = $lexeme->getLemmas()->getIterator()->current();
407        $lexemeIdLink = $this->linkRenderer->makeKnownLink(
408            $this->entityTitleLookup->getTitleForId( $lexemeId ),
409            $lexemeIdString
410        );
411
412        $labelDescriptionLookup = $this->labelDescriptionLookupFactory->newLabelDescriptionLookup(
413            $this->getLanguage(),
414            [ $lexeme->getLanguage(), $lexeme->getLexicalCategory() ],
415            [ TermTypes::TYPE_LABEL ]
416        );
417
418        $entityIdFormatter = $this->entityIdFormatterFactory->getEntityIdFormatter( $this->getLanguage() );
419        $languageLabel = $labelDescriptionLookup->getLabel( $lexeme->getLanguage() );
420        $lexicalCategoryLabel = $labelDescriptionLookup->getLabel( $lexeme->getLexicalCategory() );
421
422        return [
423            'lexeme_id_HTML' => $lexemeIdLink,
424            'lemma_text' => $lemma->getText(),
425            'lemma_language' => $lemma->getLanguageCode(),
426            'language_item_id' => $lexeme->getLanguage()->getSerialization(),
427            'language_item_label' => $languageLabel ?
428                $languageLabel->getText() :
429                self::FALLBACK_LANGUAGE_LABEL,
430            'language_link_HTML' => $entityIdFormatter->formatEntityId( $lexeme->getLanguage() ),
431            'lexical_category_item_id' => $lexeme->getLexicalCategory()->getSerialization(),
432            'lexical_category_item_label' => $lexicalCategoryLabel ?
433                $lexicalCategoryLabel->getText() :
434                self::FALLBACK_LEXICAL_CATEGORY_LABEL,
435            'lexical_category_link_HTML' => $entityIdFormatter->formatEntityId( $lexeme->getLexicalCategory() ),
436        ];
437    }
438
439    private function processInfoPanelTemplate( array $params ): string {
440        $staticTemplateParams = [
441            'header' => $this->msg( 'wikibaselexeme-newlexeme-info-panel-heading' )->text(),
442            'lexicographical-data_HTML' => $this->msg(
443                'wikibaselexeme-newlexeme-info-panel-lexicographical-data'
444            )->parse(),
445            'no-general-data_HTML' => $this->msg( 'wikibaselexeme-newlexeme-info-panel-no-general-data' )->parse(),
446            'info_icon_HTML' => ( new IconWidget( [ 'icon' => 'infoFilled' ] ) )->toString(),
447            'language_label' => $this->msg( 'wikibaselexeme-field-language-label' )->text(),
448            'lexical_category_label' => $this->msg(
449                'wikibaselexeme-field-lexical-category-label'
450            )->text(),
451            'colon_separator' => $this->msg( 'colon-separator' )->text(),
452        ];
453        $params['lemma_language_HTML'] = LanguageCode::bcp47( $params['lemma_language'] );
454
455        return ( new TemplateParser( __DIR__ ) )->processTemplate(
456            'SpecialNewLexeme-infopanel',
457            $staticTemplateParams + $params
458        );
459    }
460
461    /**
462     * Get the suggested lexical category items with their labels and descriptions.
463     *
464     * @return array[]
465     */
466    private function getLexicalCategorySuggestions(): array {
467        $itemIds = array_map(
468            [ $this->entityIdParser, 'parse' ],
469            $this->getConfig()->get( 'LexemeLexicalCategoryItemIds' )
470        );
471        $labelDescriptionLookup = $this->labelDescriptionLookupFactory->newLabelDescriptionLookup(
472            $this->getLanguage(),
473            $itemIds, // prefetch labels and descriptions of all these item IDs
474            [ TermTypes::TYPE_LABEL, TermTypes::TYPE_DESCRIPTION ]
475        );
476
477        return array_map( static function ( EntityId $entityId ) use ( $labelDescriptionLookup ) {
478            $label = $labelDescriptionLookup->getLabel( $entityId );
479            $description = $labelDescriptionLookup->getDescription( $entityId );
480            $suggestion = [
481                'id' => $entityId->getSerialization(),
482                'display' => [],
483            ];
484            if ( $label !== null ) {
485                $suggestion['display']['label'] = self::termToArrayForJs( $label );
486            }
487            if ( $description !== null ) {
488                $suggestion['display']['description'] = self::termToArrayForJs( $description );
489            }
490            return $suggestion;
491        }, $itemIds );
492    }
493
494    private static function termToArrayForJs( TermFallback $term ): array {
495        return [
496            'language' => LanguageCode::bcp47( $term->getActualLanguageCode() ),
497            'value' => $term->getText(),
498        ];
499    }
500
501    private function createForm( array $exampleLexemeParams ): HTMLForm {
502        return HTMLForm::factory( 'ooui', $this->getFormFields( $exampleLexemeParams ), $this->getContext() )
503            ->setSubmitCallback(
504                function ( $data, HTMLForm $form ) {
505                    // $data is already validated at this point (according to the field definitions)
506
507                    $entity = $this->createEntityFromFormData( $data );
508
509                    $summary = $this->createSummary( $entity );
510
511                    $saveStatus = $this->saveEntity(
512                        $entity,
513                        $summary,
514                        $form->getRequest()->getVal( 'wpEditToken' )
515                    );
516
517                    if ( !$saveStatus->isGood() ) {
518                        return $saveStatus;
519                    }
520
521                    $this->statsDataFactory->increment( 'wikibase.lexeme.special.NewLexeme.nojs.create' );
522
523                    return $saveStatus;
524                }
525            )->addPreHtml( '<noscript>' )
526            ->addPostHtml( '</noscript>' );
527    }
528
529    private function createEntityFromFormData( array $formData ): Lexeme {
530        $entity = new Lexeme();
531        $lemmaLanguage = $formData[self::FIELD_LEMMA_LANGUAGE];
532
533        $lemmas = new TermList( [ new Term( $lemmaLanguage, $formData[self::FIELD_LEMMA] ) ] );
534        $entity->setLemmas( $lemmas );
535
536        $entity->setLexicalCategory( new ItemId( $formData[self::FIELD_LEXICAL_CATEGORY] ) );
537
538        $entity->setLanguage( new ItemId( $formData[self::FIELD_LEXEME_LANGUAGE] ) );
539
540        return $entity;
541    }
542
543    private function createSummary( Lexeme $lexeme ): Summary {
544        $uiLanguageCode = $this->getLanguage()->getCode();
545
546        $summary = new Summary( 'wbeditentity', 'create' );
547        $summary->setLanguage( $uiLanguageCode );
548
549        $lemmaIterator = $lexeme->getLemmas()->getIterator();
550        // As getIterator can also in theory return a Traversable, guard against that
551        Assert::invariant(
552            $lemmaIterator instanceof Iterator,
553            'TermList::getIterator did not return an instance of Iterator'
554        );
555        /** @var Term|null $lemmaTerm */
556        $lemmaTerm = $lemmaIterator->current();
557        $summary->addAutoSummaryArgs( $lemmaTerm->getText() );
558
559        return $summary;
560    }
561
562    private function redirectToEntityPage( EditEntityStatus $status ) {
563        $entity = $status->getRevision()->getEntity();
564        $title = $this->entityTitleLookup->getTitleForId( $entity->getId() );
565        $savedTempUser = $status->getSavedTempUser();
566        $redirectUrl = '';
567        if ( $savedTempUser !== null ) {
568            $this->getHookRunner()->onTempUserCreatedRedirect(
569                $this->getRequest()->getSession(),
570                $savedTempUser,
571                $title->getPrefixedDBkey(),
572                '',
573                '',
574                $redirectUrl
575            );
576        }
577        if ( !$redirectUrl ) {
578            $redirectUrl = $title->getFullURL();
579        }
580        $this->getOutput()->redirect( $redirectUrl );
581    }
582
583    private function newEditEntity(): EditEntity {
584        return $this->editEntityFactory->newEditEntity(
585            $this->getContext(),
586            null,
587            0,
588            $this->getRequest()->wasPosted()
589        );
590    }
591
592    private function saveEntity(
593        EntityDocument $entity,
594        FormatableSummary $summary,
595        string $token
596    ): EditEntityStatus {
597        return $this->newEditEntity()->attemptSave(
598            $entity,
599            $this->summaryFormatter->formatSummary( $summary ),
600            EDIT_NEW,
601            $token,
602            null,
603            $this->tags
604        );
605    }
606
607    private function getFormFields( array $exampleLexemeParams ): array {
608        return [
609            self::FIELD_LEMMA => [
610                'name' => self::FIELD_LEMMA,
611                'class' => HTMLTrimmedTextField::class,
612                'id' => 'wb-newlexeme-lemma',
613                'required' => true,
614                'placeholder-message' => [
615                    'wikibaselexeme-newlexeme-lemma-placeholder-with-example',
616                    Message::plaintextParam( $exampleLexemeParams['lemma_text'] ),
617                ],
618                'label-message' => 'wikibaselexeme-newlexeme-lemma',
619                'validation-callback' => function ( string $lemma ) {
620                    $result = $this->lemmaTermValidator->validate( $lemma );
621                    return $result->isValid() ?:
622                        $this->validatorErrorLocalizer->getErrorMessage( $result->getErrors()[0] );
623                },
624            ],
625            self::FIELD_LEMMA_LANGUAGE => [
626                'name' => self::FIELD_LEMMA_LANGUAGE,
627                'class' => LemmaLanguageField::class,
628                'cssclass' => 'lemma-language',
629                'id' => 'wb-newlexeme-lemma-language',
630                'label-message' => 'wikibaselexeme-newlexeme-lemma-language',
631                'placeholder-message' => [
632                    'wikibaselexeme-newlexeme-lemma-language-placeholder-with-example',
633                    Message::plaintextParam( $exampleLexemeParams['lemma_language'] ),
634                ],
635            ],
636            self::FIELD_LEXEME_LANGUAGE => [
637                'name' => self::FIELD_LEXEME_LANGUAGE,
638                'labelFieldName' => self::FIELD_LEXEME_LANGUAGE . '-label',
639                'class' => HTMLItemReferenceField::class,
640                'id' => 'wb-newlexeme-lexeme-language',
641                'label-message' => 'wikibaselexeme-newlexeme-language',
642                'required' => true,
643                'placeholder-message' => [
644                    'wikibaselexeme-newlexeme-language-placeholder-with-example',
645                    Message::plaintextParam( $exampleLexemeParams['language_item_id'] ),
646                ],
647            ],
648            self::FIELD_LEXICAL_CATEGORY => [
649                'name' => self::FIELD_LEXICAL_CATEGORY,
650                'labelFieldName' => self::FIELD_LEXICAL_CATEGORY . '-label',
651                'class' => HTMLItemReferenceField::class,
652                'id' => 'wb-newlexeme-lexicalCategory',
653                'label-message' => 'wikibaselexeme-newlexeme-lexicalcategory',
654                'required' => true,
655                'placeholder-message' => [
656                    'wikibaselexeme-newlexeme-lexicalcategory-placeholder-with-example',
657                    Message::plaintextParam( $exampleLexemeParams['lexical_category_item_id'] ),
658                ],
659            ],
660            'copyright-message' => [
661                'name' => 'copyright-message',
662                'type' => 'info',
663                'raw' => true,
664                'id' => 'wb-newlexeme-copyright',
665                'default' => $this->getCopyrightHTML(),
666            ],
667        ];
668    }
669
670    public function setHeaders(): void {
671        $out = $this->getOutput();
672        $out->setPageTitleMsg( $this->getDescription() );
673    }
674
675    /** @see \Wikibase\Repo\Specials\SpecialWikibasePage::getGroupName() */
676    protected function getGroupName(): string {
677        return 'wikibase';
678    }
679
680    public function getDescription(): Message {
681        return $this->msg( 'special-newlexeme' );
682    }
683
684    /**
685     * @throws UserBlockedError
686     */
687    private function checkBlocked(): void {
688        $block = $this->getUser()->getBlock();
689        if ( $block && $block->isSitewide() ) {
690            throw new UserBlockedError( $block );
691        }
692    }
693
694    /**
695     * @throws UserBlockedError
696     */
697    private function checkBlockedOnNamespace(): void {
698        $namespace = $this->entityNamespaceLookup->getEntityNamespace( Lexeme::ENTITY_TYPE );
699        $block = $this->getUser()->getBlock();
700        if ( $block && $block->appliesToNamespace( $namespace ) ) {
701            throw new UserBlockedError( $block );
702        }
703    }
704
705    /**
706     * @return string HTML
707     */
708    private function getCopyrightHTML() {
709        return $this->copyrightView->getHtml(
710            $this->getLanguage(),
711            'wikibaselexeme-newlexeme-submit'
712        );
713    }
714
715    /**
716     * @return string HTML
717     */
718    private function anonymousEditWarning() {
719        $warningIconHtml = ( new IconWidget( [ 'icon' => 'alert' ] ) )->toString();
720
721        if ( !$this->getUser()->isRegistered() ) {
722            $fullTitle = $this->getPageTitle();
723            $messageSpan = Html::rawElement(
724                'span',
725                [ 'class' => 'warning' ],
726                $this->anonymousEditWarningBuilder->buildAnonymousEditWarningHTML( $fullTitle->getPrefixedText() )
727            );
728            return '<noscript>
729                <div class="wbl-snl-anonymous-edit-warning-no-js wbl-snl-message-warning">'
730                . $warningIconHtml
731                . $messageSpan
732                . '</div></noscript>';
733        }
734
735        return '';
736    }
737}