Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.59% covered (success)
98.59%
421 / 427
88.46% covered (warning)
88.46%
23 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialNewLexeme
98.59% covered (success)
98.59%
421 / 427
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%
73 / 73
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%
35 / 35
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%
18 / 18
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%
7 / 7
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 MediaWiki\Config\ConfigException;
8use MediaWiki\Exception\UserBlockedError;
9use MediaWiki\Html\Html;
10use MediaWiki\Html\TemplateParser;
11use MediaWiki\HTMLForm\HTMLForm;
12use MediaWiki\Language\LanguageCode;
13use MediaWiki\Linker\LinkRenderer;
14use MediaWiki\Message\Message;
15use MediaWiki\SpecialPage\SpecialPage;
16use MediaWiki\Status\Status;
17use MediaWiki\User\TempUser\TempUserConfig;
18use OOUI\IconWidget;
19use Wikibase\DataModel\Entity\EntityDocument;
20use Wikibase\DataModel\Entity\EntityId;
21use Wikibase\DataModel\Entity\EntityIdParser;
22use Wikibase\DataModel\Entity\EntityIdParsingException;
23use Wikibase\DataModel\Entity\Item;
24use Wikibase\DataModel\Entity\ItemId;
25use Wikibase\DataModel\Entity\PropertyId;
26use Wikibase\DataModel\Services\Lookup\EntityLookup;
27use Wikibase\DataModel\Snak\PropertySomeValueSnak;
28use Wikibase\DataModel\Snak\PropertyValueSnak;
29use Wikibase\DataModel\Term\Term;
30use Wikibase\DataModel\Term\TermFallback;
31use Wikibase\DataModel\Term\TermList;
32use Wikibase\DataModel\Term\TermTypes;
33use Wikibase\Lexeme\DataAccess\ChangeOp\Validation\LemmaTermValidator;
34use Wikibase\Lexeme\Domain\Model\Lexeme;
35use Wikibase\Lexeme\MediaWiki\Specials\HTMLForm\LemmaLanguageField;
36use Wikibase\Lib\FormatableSummary;
37use Wikibase\Lib\SettingsArray;
38use Wikibase\Lib\Store\EntityNamespaceLookup;
39use Wikibase\Lib\Store\FallbackLabelDescriptionLookup;
40use Wikibase\Lib\Store\FallbackLabelDescriptionLookupFactory;
41use Wikibase\Lib\Summary;
42use Wikibase\Repo\AnonymousEditWarningBuilder;
43use Wikibase\Repo\CopyrightMessageBuilder;
44use Wikibase\Repo\EditEntity\EditEntity;
45use Wikibase\Repo\EditEntity\EditEntityStatus;
46use Wikibase\Repo\EditEntity\MediaWikiEditEntityFactory;
47use Wikibase\Repo\Specials\HTMLForm\HTMLItemReferenceField;
48use Wikibase\Repo\Specials\HTMLForm\HTMLTrimmedTextField;
49use Wikibase\Repo\Specials\SpecialPageCopyrightView;
50use Wikibase\Repo\Store\EntityTitleStoreLookup;
51use Wikibase\Repo\SummaryFormatter;
52use Wikibase\Repo\Validators\ValidatorErrorLocalizer;
53use Wikibase\View\EntityIdFormatterFactory;
54use Wikimedia\Stats\StatsFactory;
55
56/**
57 * New page for creating new Lexeme entities.
58 *
59 * @license GPL-2.0-or-later
60 */
61class SpecialNewLexeme extends SpecialPage {
62
63    public const FIELD_LEXEME_LANGUAGE = 'lexeme-language';
64    public const FIELD_LEXICAL_CATEGORY = 'lexicalcategory';
65    public const FIELD_LEMMA = 'lemma';
66    public const FIELD_LEMMA_LANGUAGE = 'lemma-language';
67
68    // used for the info panel and placeholders if the example lexeme is incomplete/missing
69    private const FALLBACK_LANGUAGE_LABEL = 'English';
70    private const FALLBACK_LEXICAL_CATEGORY_LABEL = 'verb';
71
72    private array $tags;
73    private LinkRenderer $linkRenderer;
74    private StatsFactory $statsFactory;
75    private MediaWikiEditEntityFactory $editEntityFactory;
76    private EntityNamespaceLookup $entityNamespaceLookup;
77    private EntityTitleStoreLookup $entityTitleLookup;
78    private EntityLookup $entityLookup;
79    private EntityIdParser $entityIdParser;
80    private SummaryFormatter $summaryFormatter;
81    private EntityIdFormatterFactory $entityIdFormatterFactory;
82    private FallbackLabelDescriptionLookupFactory $labelDescriptionLookupFactory;
83    private ValidatorErrorLocalizer $validatorErrorLocalizer;
84    private LemmaTermValidator $lemmaTermValidator;
85    private SpecialPageCopyrightView $copyrightView;
86    private AnonymousEditWarningBuilder $anonymousEditWarningBuilder;
87    private TempUserConfig $tempUserConfig;
88
89    public function __construct(
90        array $tags,
91        SpecialPageCopyrightView $copyrightView,
92        LinkRenderer $linkRenderer,
93        StatsFactory $statsFactory,
94        MediaWikiEditEntityFactory $editEntityFactory,
95        EntityNamespaceLookup $entityNamespaceLookup,
96        EntityTitleStoreLookup $entityTitleLookup,
97        EntityLookup $entityLookup,
98        EntityIdParser $entityIdParser,
99        SummaryFormatter $summaryFormatter,
100        EntityIdFormatterFactory $entityIdFormatterFactory,
101        FallbackLabelDescriptionLookupFactory $labelDescriptionLookupFactory,
102        ValidatorErrorLocalizer $validatorErrorLocalizer,
103        LemmaTermValidator $lemmaTermValidator,
104        AnonymousEditWarningBuilder $anonymousEditWarningBuilder,
105        TempUserConfig $tempUserConfig
106    ) {
107        parent::__construct(
108            'NewLexeme',
109            'createpage'
110        );
111
112        $this->tags = $tags;
113        $this->linkRenderer = $linkRenderer;
114        $this->statsFactory = $statsFactory;
115        $this->editEntityFactory = $editEntityFactory;
116        $this->entityNamespaceLookup = $entityNamespaceLookup;
117        $this->entityTitleLookup = $entityTitleLookup;
118        $this->entityLookup = $entityLookup;
119        $this->entityIdParser = $entityIdParser;
120        $this->summaryFormatter = $summaryFormatter;
121        $this->entityIdFormatterFactory = $entityIdFormatterFactory;
122        $this->labelDescriptionLookupFactory = $labelDescriptionLookupFactory;
123        $this->validatorErrorLocalizer = $validatorErrorLocalizer;
124        $this->lemmaTermValidator = $lemmaTermValidator;
125        $this->copyrightView = $copyrightView;
126        $this->anonymousEditWarningBuilder = $anonymousEditWarningBuilder;
127        $this->tempUserConfig = $tempUserConfig;
128    }
129
130    public static function factory(
131        LinkRenderer $linkRenderer,
132        StatsFactory $statsFactory,
133        TempUserConfig $tempUserConfig,
134        AnonymousEditWarningBuilder $anonymousEditWarningBuilder,
135        MediaWikiEditEntityFactory $editEntityFactory,
136        EntityNamespaceLookup $entityNamespaceLookup,
137        EntityTitleStoreLookup $entityTitleLookup,
138        EntityLookup $entityLookup,
139        EntityIdParser $entityIdParser,
140        SettingsArray $repoSettings,
141        SummaryFormatter $summaryFormatter,
142        EntityIdFormatterFactory $entityIdFormatterFactory,
143        FallbackLabelDescriptionLookupFactory $labelDescriptionLookupFactory,
144        ValidatorErrorLocalizer $validatorErrorLocalizer,
145        LemmaTermValidator $lemmaTermValidator
146    ): self {
147        $copyrightView = new SpecialPageCopyrightView(
148            new CopyrightMessageBuilder(),
149            $repoSettings->getSetting( 'dataRightsUrl' ),
150            $repoSettings->getSetting( 'dataRightsText' )
151        );
152
153        return new self(
154            $repoSettings->getSetting( 'specialPageTags' ),
155            $copyrightView,
156            $linkRenderer,
157            $statsFactory,
158            $editEntityFactory,
159            $entityNamespaceLookup,
160            $entityTitleLookup,
161            $entityLookup,
162            $entityIdParser,
163            $summaryFormatter,
164            $entityIdFormatterFactory,
165            $labelDescriptionLookupFactory,
166            $validatorErrorLocalizer,
167            $lemmaTermValidator,
168            $anonymousEditWarningBuilder,
169            $tempUserConfig
170        );
171    }
172
173    public function doesWrites(): bool {
174        return true;
175    }
176
177    /**
178     * @param string|null $subPage
179     */
180    public function execute( $subPage ): void {
181        $metric = $this->statsFactory->getCounter( 'special_new_lexeme_views_total' );
182        $metric->copyToStatsdAt( 'wikibase.lexeme.special.NewLexeme.views' )->increment();
183
184        parent::execute( $subPage );
185
186        $this->checkBlocked();
187        $this->checkBlockedOnNamespace();
188        $this->checkReadOnly();
189
190        $output = $this->getOutput();
191        $this->setHeaders();
192        $searchUrl = SpecialPage::getTitleFor( 'Search' )
193            ->getFullURL( [
194                'ns' . $this->getConfig()->get( 'LexemeNamespace' ) => '',
195                'search' => $this->getRequest()->getText( self::FIELD_LEMMA ),
196            ] );
197        $searchExisting = $this->msg( 'wikibaselexeme-newlexeme-search-existing' )
198            ->params( $searchUrl )
199            ->parse();
200        $output->addHTML(
201            '<div id="wbl-snl-intro-text-wrapper">'
202            . '<p class="wbl-snl-search-existing-no-js">' . $searchExisting . '</p>'
203            . '</div>'
204        );
205        $output->enableOOUI();
206        $output->addHTML( $this->anonymousEditWarning() );
207        $output->addHTML( '<div class="wbl-snl-main-content">' );
208        $output->addHTML( '<div id="special-newlexeme-root"></div>' );
209        $output->addModules( [
210            'wikibase.lexeme.special.NewLexeme',
211            'wikibase.lexeme.special.NewLexeme.legacyBrowserFallback',
212            ] );
213        $output->addModuleStyles( [
214            'wikibase.lexeme.special.NewLexeme.styles',
215            'wikibase.alltargets', // T322687
216        ] );
217
218        $exampleLexemeParams = $this->createExampleParameters();
219        $form = $this->createForm( $exampleLexemeParams );
220        $form->setSubmitText( $this->msg( 'wikibaselexeme-newlexeme-submit' ) );
221
222        // handle submit (submit callback may create form, see below)
223        // or show form (possibly with errors); status represents submit result
224        $status = $form->show();
225        $output->addModuleStyles( [
226            'oojs-ui.styles.icons-content', // info icon
227            'oojs-ui.styles.icons-alert', // alert icon
228        ] );
229        $output->addHTML(
230            $this->processInfoPanelTemplate( $exampleLexemeParams )
231        );
232        $output->addHTML( '</div>' ); // .wbl-snl-main-content
233        $output->addHTML(
234            '<noscript>'
235            . '<style type="text/css">#special-newlexeme-root {display:none;}</style>'
236            . '</noscript>'
237        );
238
239        if ( $status instanceof Status && $status->isGood() ) {
240            // wrap it, in case HTMLForm turned it into a generic Status
241            $status = EditEntityStatus::wrap( $status );
242            $this->redirectToEntityPage( $status );
243            return;
244        }
245
246        $output->addJsConfigVars( 'wblSpecialNewLexemeParams',
247            $this->getUrlParamsForConfig()
248        );
249        $output->addJsConfigVars(
250            'wblSpecialNewLexemeLexicalCategorySuggestions',
251            $this->getLexicalCategorySuggestions()
252        );
253        $output->addJsConfigVars( 'wblSpecialNewLexemeTempUserEnabled',
254            $this->tempUserConfig->isEnabled()
255        );
256        $output->addJSConfigVars(
257            'wblSpecialNewLexemeExampleData',
258            [
259                'languageLabel' => $exampleLexemeParams['language_item_label'],
260                'lexicalCategoryLabel' => $exampleLexemeParams['lexical_category_item_label'],
261                'lemma' => $exampleLexemeParams['lemma_text'],
262                'spellingVariant' => $exampleLexemeParams['lemma_language'],
263            ]
264        );
265    }
266
267    private function getUrlParamsForConfig(): array {
268        $params = [];
269        $lemma = $this->getRequest()->getText( self::FIELD_LEMMA );
270        if ( $lemma ) {
271            $params['lemma'] = $lemma;
272        }
273
274        $spellVarCode = $this->getRequest()->getText( self::FIELD_LEMMA_LANGUAGE );
275        if ( $spellVarCode ) {
276            $params['spellVarCode'] = $spellVarCode;
277        }
278
279        try {
280            $languageId = $this->entityIdParser->parse(
281                $this->getRequest()->getText( self::FIELD_LEXEME_LANGUAGE )
282            );
283        } catch ( EntityIdParsingException $e ) {
284            $languageId = null;
285        }
286        try {
287            $lexCatId = $this->entityIdParser->parse(
288                $this->getRequest()->getText( self::FIELD_LEXICAL_CATEGORY )
289            );
290        } catch ( EntityIdParsingException $e ) {
291            $lexCatId = null;
292        }
293
294        $idsToPrefetch = array_filter( [ $languageId, $lexCatId ] );
295        if ( !$idsToPrefetch ) {
296            return $params;
297        }
298
299        $labelDescriptionLookup = $this->labelDescriptionLookupFactory->newLabelDescriptionLookup(
300            $this->getLanguage(),
301            $idsToPrefetch,
302            [ TermTypes::TYPE_LABEL, TermTypes::TYPE_DESCRIPTION ]
303        );
304
305        if ( $languageId ) {
306            $params['language'] = $this->getItemIdLabelDesc( $languageId, $labelDescriptionLookup );
307            $params['language']['languageCode'] = $this->extractLanguageCode( $languageId );
308        }
309
310        if ( $lexCatId ) {
311            $params['lexicalCategory'] = $this->getItemIdLabelDesc( $lexCatId, $labelDescriptionLookup );
312        }
313
314        return $params;
315    }
316
317    private function getItemIdLabelDesc(
318        EntityId $itemId,
319        FallbackLabelDescriptionLookup $labelDescriptionLookup
320    ): array {
321        $params = [ 'display' => [] ];
322        $params['id'] = $itemId->getSerialization();
323        $label = $labelDescriptionLookup->getLabel( $itemId );
324        if ( $label !== null ) {
325            $params['display']['label'] = self::termToArrayForJs( $label );
326        }
327        $description = $labelDescriptionLookup->getDescription( $itemId );
328        if ( $description !== null ) {
329            $params['display']['description'] = self::termToArrayForJs( $description );
330        }
331
332        return $params;
333    }
334
335    /** @return mixed|null */
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                    $metric = $this->statsFactory->getCounter( 'special_new_lexeme_nojs_create_total' );
522                    $metric->copyToStatsdAt( 'wikibase.lexeme.special.NewLexeme.nojs.create' )->increment();
523
524                    return $saveStatus;
525                }
526            )->addPreHtml( '<noscript>' )
527            ->addPostHtml( '</noscript>' );
528    }
529
530    private function createEntityFromFormData( array $formData ): Lexeme {
531        $entity = new Lexeme();
532        $lemmaLanguage = $formData[self::FIELD_LEMMA_LANGUAGE];
533
534        $lemmas = new TermList( [ new Term( $lemmaLanguage, $formData[self::FIELD_LEMMA] ) ] );
535        $entity->setLemmas( $lemmas );
536
537        $entity->setLexicalCategory( new ItemId( $formData[self::FIELD_LEXICAL_CATEGORY] ) );
538
539        $entity->setLanguage( new ItemId( $formData[self::FIELD_LEXEME_LANGUAGE] ) );
540
541        return $entity;
542    }
543
544    private function createSummary( Lexeme $lexeme ): Summary {
545        $uiLanguageCode = $this->getLanguage()->getCode();
546
547        $summary = new Summary( 'wbeditentity', 'create' );
548        $summary->setLanguage( $uiLanguageCode );
549
550        $lemmaIterator = $lexeme->getLemmas()->getIterator();
551        /** @var Term|null $lemmaTerm */
552        $lemmaTerm = $lemmaIterator->current();
553        $summary->addAutoSummaryArgs( $lemmaTerm->getText() );
554
555        return $summary;
556    }
557
558    private function redirectToEntityPage( EditEntityStatus $status ) {
559        $entity = $status->getRevision()->getEntity();
560        $title = $this->entityTitleLookup->getTitleForId( $entity->getId() );
561        $savedTempUser = $status->getSavedTempUser();
562        $redirectUrl = '';
563        if ( $savedTempUser !== null ) {
564            $this->getHookRunner()->onTempUserCreatedRedirect(
565                $this->getRequest()->getSession(),
566                $savedTempUser,
567                $title->getPrefixedDBkey(),
568                '',
569                '',
570                $redirectUrl
571            );
572        }
573        if ( !$redirectUrl ) {
574            $redirectUrl = $title->getFullURL();
575        }
576        $this->getOutput()->redirect( $redirectUrl );
577    }
578
579    private function newEditEntity(): EditEntity {
580        return $this->editEntityFactory->newEditEntity(
581            $this->getContext(),
582            null,
583            0,
584            $this->getRequest()->wasPosted()
585        );
586    }
587
588    private function saveEntity(
589        EntityDocument $entity,
590        FormatableSummary $summary,
591        string $token
592    ): EditEntityStatus {
593        return $this->newEditEntity()->attemptSave(
594            $entity,
595            $this->summaryFormatter->formatSummary( $summary ),
596            EDIT_NEW,
597            $token,
598            null,
599            $this->tags
600        );
601    }
602
603    private function getFormFields( array $exampleLexemeParams ): array {
604        return [
605            self::FIELD_LEMMA => [
606                'name' => self::FIELD_LEMMA,
607                'class' => HTMLTrimmedTextField::class,
608                'id' => 'wb-newlexeme-lemma',
609                'required' => true,
610                'placeholder-message' => [
611                    'wikibaselexeme-newlexeme-lemma-placeholder-with-example',
612                    Message::plaintextParam( $exampleLexemeParams['lemma_text'] ),
613                ],
614                'label-message' => 'wikibaselexeme-newlexeme-lemma',
615                'validation-callback' => function ( string $lemma ) {
616                    $result = $this->lemmaTermValidator->validate( $lemma );
617                    return $result->isValid() ?:
618                        $this->validatorErrorLocalizer->getErrorMessage( $result->getErrors()[0] );
619                },
620            ],
621            self::FIELD_LEMMA_LANGUAGE => [
622                'name' => self::FIELD_LEMMA_LANGUAGE,
623                'class' => LemmaLanguageField::class,
624                'cssclass' => 'lemma-language',
625                'id' => 'wb-newlexeme-lemma-language',
626                'label-message' => 'wikibaselexeme-newlexeme-lemma-language',
627                'placeholder-message' => [
628                    'wikibaselexeme-newlexeme-lemma-language-placeholder-with-example',
629                    Message::plaintextParam( $exampleLexemeParams['lemma_language'] ),
630                ],
631            ],
632            self::FIELD_LEXEME_LANGUAGE => [
633                'name' => self::FIELD_LEXEME_LANGUAGE,
634                'labelFieldName' => self::FIELD_LEXEME_LANGUAGE . '-label',
635                'class' => HTMLItemReferenceField::class,
636                'id' => 'wb-newlexeme-lexeme-language',
637                'label-message' => 'wikibaselexeme-newlexeme-language',
638                'required' => true,
639                'placeholder-message' => [
640                    'wikibaselexeme-newlexeme-language-placeholder-with-example',
641                    Message::plaintextParam( $exampleLexemeParams['language_item_id'] ),
642                ],
643            ],
644            self::FIELD_LEXICAL_CATEGORY => [
645                'name' => self::FIELD_LEXICAL_CATEGORY,
646                'labelFieldName' => self::FIELD_LEXICAL_CATEGORY . '-label',
647                'class' => HTMLItemReferenceField::class,
648                'id' => 'wb-newlexeme-lexicalCategory',
649                'label-message' => 'wikibaselexeme-newlexeme-lexicalcategory',
650                'required' => true,
651                'placeholder-message' => [
652                    'wikibaselexeme-newlexeme-lexicalcategory-placeholder-with-example',
653                    Message::plaintextParam( $exampleLexemeParams['lexical_category_item_id'] ),
654                ],
655            ],
656            'copyright-message' => [
657                'name' => 'copyright-message',
658                'type' => 'info',
659                'raw' => true,
660                'id' => 'wb-newlexeme-copyright',
661                'default' => $this->getCopyrightHTML(),
662            ],
663        ];
664    }
665
666    public function setHeaders(): void {
667        $out = $this->getOutput();
668        $out->setPageTitleMsg( $this->getDescription() );
669    }
670
671    /** @see \Wikibase\Repo\Specials\SpecialWikibasePage::getGroupName() */
672    protected function getGroupName(): string {
673        return 'wikibase';
674    }
675
676    public function getDescription(): Message {
677        return $this->msg( 'special-newlexeme' );
678    }
679
680    /**
681     * @throws UserBlockedError
682     */
683    private function checkBlocked(): void {
684        $block = $this->getUser()->getBlock();
685        if ( $block && $block->isSitewide() ) {
686            throw new UserBlockedError( $block );
687        }
688    }
689
690    /**
691     * @throws UserBlockedError
692     */
693    private function checkBlockedOnNamespace(): void {
694        $namespace = $this->entityNamespaceLookup->getEntityNamespace( Lexeme::ENTITY_TYPE );
695        $block = $this->getUser()->getBlock();
696        if ( $block && $block->appliesToNamespace( $namespace ) ) {
697            throw new UserBlockedError( $block );
698        }
699    }
700
701    /**
702     * @return string HTML
703     */
704    private function getCopyrightHTML() {
705        return $this->copyrightView->getHtml(
706            $this->getLanguage(),
707            'wikibaselexeme-newlexeme-submit'
708        );
709    }
710
711    /**
712     * @return string HTML
713     */
714    private function anonymousEditWarning() {
715        $warningIconHtml = ( new IconWidget( [ 'icon' => 'alert' ] ) )->toString();
716
717        if ( !$this->getUser()->isRegistered() ) {
718            $fullTitle = $this->getPageTitle();
719            $messageSpan = Html::rawElement(
720                'span',
721                [ 'class' => 'warning' ],
722                $this->anonymousEditWarningBuilder->buildAnonymousEditWarningHTML( $fullTitle->getPrefixedText() )
723            );
724            return '<noscript>
725                <div class="wbl-snl-anonymous-edit-warning-no-js wbl-snl-message-warning">'
726                . $warningIconHtml
727                . $messageSpan
728                . '</div></noscript>';
729        }
730
731        return '';
732    }
733}