Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.60% covered (success)
98.60%
424 / 430
88.89% covered (warning)
88.89%
24 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialNewLexeme
98.60% covered (success)
98.60%
424 / 430
88.89% covered (warning)
88.89%
24 / 27
61
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 getRestriction
100.00% covered (success)
100.00%
1 / 1
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%
78 / 78
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
 showAnonymousEditWarning
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\User\TempUser\TempUserConfig;
17use OOUI\IconWidget;
18use StatusValue;
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( 'NewLexeme' );
108
109        $this->tags = $tags;
110        $this->linkRenderer = $linkRenderer;
111        $this->statsFactory = $statsFactory;
112        $this->editEntityFactory = $editEntityFactory;
113        $this->entityNamespaceLookup = $entityNamespaceLookup;
114        $this->entityTitleLookup = $entityTitleLookup;
115        $this->entityLookup = $entityLookup;
116        $this->entityIdParser = $entityIdParser;
117        $this->summaryFormatter = $summaryFormatter;
118        $this->entityIdFormatterFactory = $entityIdFormatterFactory;
119        $this->labelDescriptionLookupFactory = $labelDescriptionLookupFactory;
120        $this->validatorErrorLocalizer = $validatorErrorLocalizer;
121        $this->lemmaTermValidator = $lemmaTermValidator;
122        $this->copyrightView = $copyrightView;
123        $this->anonymousEditWarningBuilder = $anonymousEditWarningBuilder;
124        $this->tempUserConfig = $tempUserConfig;
125    }
126
127    /** @inheritDoc */
128    public function getRestriction(): string {
129        return 'createpage';
130    }
131
132    public static function factory(
133        LinkRenderer $linkRenderer,
134        StatsFactory $statsFactory,
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            $statsFactory,
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        $metric = $this->statsFactory->getCounter( 'special_new_lexeme_views_total' );
184        $metric->copyToStatsdAt( 'wikibase.lexeme.special.NewLexeme.views' )->increment();
185
186        parent::execute( $subPage );
187
188        $this->checkBlocked();
189        $this->checkBlockedOnNamespace();
190        $this->checkReadOnly();
191
192        $config = $this->getConfig();
193        $output = $this->getOutput();
194        $this->setHeaders();
195        $searchUrl = SpecialPage::getTitleFor( 'Search' )
196            ->getFullURL( [
197                'ns' . $config->get( 'LexemeNamespace' ) => '',
198                'search' => $this->getRequest()->getText( self::FIELD_LEMMA ),
199            ] );
200        $searchExisting = $this->msg( 'wikibaselexeme-newlexeme-search-existing' )
201            ->params( $searchUrl )
202            ->parse();
203        $output->addHTML(
204            '<div id="wbl-snl-intro-text-wrapper">'
205            . '<p class="wbl-snl-search-existing-no-js">' . $searchExisting . '</p>'
206            . '</div>'
207        );
208        $output->enableOOUI();
209        $this->showAnonymousEditWarning();
210        $output->addHTML( '<div class="wbl-snl-main-content">' );
211        $output->addHTML( '<div id="special-newlexeme-root"></div>' );
212        $output->addModules( [
213            'wikibase.lexeme.special.NewLexeme',
214            'wikibase.lexeme.special.NewLexeme.legacyBrowserFallback',
215            ] );
216        $output->addModuleStyles( [
217            'wikibase.lexeme.special.NewLexeme.styles',
218            'wikibase.alltargets', // T322687
219        ] );
220
221        $exampleLexemeParams = $this->createExampleParameters();
222        $form = $this->createForm( $exampleLexemeParams );
223        $form->setSubmitTextMsg( $this->msg( 'wikibaselexeme-newlexeme-submit' ) );
224
225        // handle submit (submit callback may create form, see below)
226        // or show form (possibly with errors); status represents submit result
227        $status = $form->show();
228        $output->addModuleStyles( [
229            'oojs-ui.styles.icons-content', // info icon
230            'oojs-ui.styles.icons-alert', // alert icon
231        ] );
232        $output->addHTML(
233            $this->processInfoPanelTemplate( $exampleLexemeParams )
234        );
235        $output->addHTML( '</div>' ); // .wbl-snl-main-content
236        $output->addHTML(
237            '<noscript>'
238            . '<style type="text/css">#special-newlexeme-root {display:none;}</style>'
239            . '</noscript>'
240        );
241
242        if ( $status instanceof StatusValue && $status->isGood() ) {
243            // cast it, in case HTMLForm turned it into a generic Status
244            $status = EditEntityStatus::cast( $status );
245            $this->redirectToEntityPage( $status );
246            return;
247        }
248
249        $output->addJsConfigVars(
250            'LexemeLanguageCodePropertyId',
251            $config->get( 'LexemeLanguageCodePropertyId' )
252        );
253        $output->addJsConfigVars( 'wblSpecialNewLexemeParams',
254            $this->getUrlParamsForConfig()
255        );
256        $output->addJsConfigVars(
257            'wblSpecialNewLexemeLexicalCategorySuggestions',
258            $this->getLexicalCategorySuggestions()
259        );
260        $output->addJsConfigVars( 'wblSpecialNewLexemeTempUserEnabled',
261            $this->tempUserConfig->isEnabled()
262        );
263        $output->addJSConfigVars(
264            'wblSpecialNewLexemeExampleData',
265            [
266                'languageLabel' => $exampleLexemeParams['language_item_label'],
267                'lexicalCategoryLabel' => $exampleLexemeParams['lexical_category_item_label'],
268                'lemma' => $exampleLexemeParams['lemma_text'],
269                'spellingVariant' => $exampleLexemeParams['lemma_language'],
270            ]
271        );
272    }
273
274    private function getUrlParamsForConfig(): array {
275        $params = [];
276        $lemma = $this->getRequest()->getText( self::FIELD_LEMMA );
277        if ( $lemma ) {
278            $params['lemma'] = $lemma;
279        }
280
281        $spellVarCode = $this->getRequest()->getText( self::FIELD_LEMMA_LANGUAGE );
282        if ( $spellVarCode ) {
283            $params['spellVarCode'] = $spellVarCode;
284        }
285
286        try {
287            $languageId = $this->entityIdParser->parse(
288                $this->getRequest()->getText( self::FIELD_LEXEME_LANGUAGE )
289            );
290        } catch ( EntityIdParsingException ) {
291            $languageId = null;
292        }
293        try {
294            $lexCatId = $this->entityIdParser->parse(
295                $this->getRequest()->getText( self::FIELD_LEXICAL_CATEGORY )
296            );
297        } catch ( EntityIdParsingException ) {
298            $lexCatId = null;
299        }
300
301        $idsToPrefetch = array_filter( [ $languageId, $lexCatId ] );
302        if ( !$idsToPrefetch ) {
303            return $params;
304        }
305
306        $labelDescriptionLookup = $this->labelDescriptionLookupFactory->newLabelDescriptionLookup(
307            $this->getLanguage(),
308            $idsToPrefetch,
309            [ TermTypes::TYPE_LABEL, TermTypes::TYPE_DESCRIPTION ]
310        );
311
312        if ( $languageId ) {
313            $params['language'] = $this->getItemIdLabelDesc( $languageId, $labelDescriptionLookup );
314            $params['language']['languageCode'] = $this->extractLanguageCode( $languageId );
315        }
316
317        if ( $lexCatId ) {
318            $params['lexicalCategory'] = $this->getItemIdLabelDesc( $lexCatId, $labelDescriptionLookup );
319        }
320
321        return $params;
322    }
323
324    private function getItemIdLabelDesc(
325        EntityId $itemId,
326        FallbackLabelDescriptionLookup $labelDescriptionLookup
327    ): array {
328        $params = [ 'display' => [] ];
329        $params['id'] = $itemId->getSerialization();
330        $label = $labelDescriptionLookup->getLabel( $itemId );
331        if ( $label !== null ) {
332            $params['display']['label'] = self::termToArrayForJs( $label );
333        }
334        $description = $labelDescriptionLookup->getDescription( $itemId );
335        if ( $description !== null ) {
336            $params['display']['description'] = self::termToArrayForJs( $description );
337        }
338
339        return $params;
340    }
341
342    /** @return mixed|null */
343    private function extractLanguageCode( EntityId $languageId ) {
344        $lexemeLanguageCodePropertyIdString = $this->getConfig()->get( 'LexemeLanguageCodePropertyId' );
345        if ( !$lexemeLanguageCodePropertyIdString ) {
346            return null;
347        }
348        $languageItem = $this->entityLookup->getEntity( $languageId );
349        if ( !( $languageItem instanceof Item ) ) {
350            return null;
351        }
352        $lexemeLanguageCodePropertyId = $this->entityIdParser->parse(
353            $lexemeLanguageCodePropertyIdString
354        );
355        if ( !( $lexemeLanguageCodePropertyId instanceof PropertyId ) ) {
356            throw new ConfigException(
357                'LexemeLanguageCodePropertyId must be a property ID, but isn’t: ' . $lexemeLanguageCodePropertyIdString
358            );
359        }
360        $languageCodeStatements = $languageItem->getStatements()->getByPropertyId(
361            $lexemeLanguageCodePropertyId
362        )->getBestStatements();
363        if ( !$languageCodeStatements->isEmpty() ) {
364            $firstBestSnak = $languageCodeStatements->getMainSnaks()[0];
365            if ( $firstBestSnak instanceof PropertyValueSnak ) {
366                return $firstBestSnak->getDataValue()->getValue();
367            }
368            if ( $firstBestSnak instanceof PropertySomeValueSnak ) {
369                return false;
370            }
371        }
372        return null;
373    }
374
375    private function createExampleParameters(): array {
376        $exampleMessage = $this->msg( 'wikibaselexeme-newlexeme-info-panel-example-lexeme-id' );
377        if ( $exampleMessage->exists() ) {
378            $lexemeIdString = trim( $exampleMessage->text() );
379        } else {
380            $lexemeIdString = 'L1';
381        }
382        try {
383            return $this->createTemplateParamsFromLexemeId( $lexemeIdString );
384        } catch ( Exception ) {
385            return [
386                'lexeme_id_HTML' => 'L1',
387                'lemma_text' => 'speak',
388                'lemma_language' => 'en',
389                'language_item_id' => 'Q1',
390                'language_item_label' => self::FALLBACK_LANGUAGE_LABEL,
391                'language_link_HTML' => self::FALLBACK_LANGUAGE_LABEL,
392                'lexical_category_item_id' => 'Q2',
393                'lexical_category_item_label' => self::FALLBACK_LEXICAL_CATEGORY_LABEL,
394                'lexical_category_link_HTML' => self::FALLBACK_LEXICAL_CATEGORY_LABEL,
395            ];
396        }
397    }
398
399    private function createTemplateParamsFromLexemeId( string $lexemeIdString ): array {
400        try {
401            $lexemeId = $this->entityIdParser->parse( $lexemeIdString );
402            $lexeme = $this->entityLookup->getEntity( $lexemeId );
403        } catch ( EntityIdParsingException ) {
404            $lexeme = null;
405        }
406        if ( !( $lexeme instanceof Lexeme ) ) {
407            throw new ConfigException(
408                'MediaWiki:wikibaselexeme-newlexeme-info-panel-example-lexeme-id must be ' .
409                'the ID of an existing lexeme, but isn’t: ' . $lexemeIdString
410            );
411        }
412
413        $lemma = $lexeme->getLemmas()->getIterator()->current();
414        $lexemeIdLink = $this->linkRenderer->makeKnownLink(
415            // @phan-suppress-next-line PhanPossiblyUndeclaredVariable @phpstan-ignore-next-line
416            $this->entityTitleLookup->getTitleForId( $lexemeId ),
417            $lexemeIdString
418        );
419
420        $labelDescriptionLookup = $this->labelDescriptionLookupFactory->newLabelDescriptionLookup(
421            $this->getLanguage(),
422            [ $lexeme->getLanguage(), $lexeme->getLexicalCategory() ],
423            [ TermTypes::TYPE_LABEL ]
424        );
425
426        $entityIdFormatter = $this->entityIdFormatterFactory->getEntityIdFormatter( $this->getLanguage() );
427        $languageLabel = $labelDescriptionLookup->getLabel( $lexeme->getLanguage() );
428        $lexicalCategoryLabel = $labelDescriptionLookup->getLabel( $lexeme->getLexicalCategory() );
429
430        return [
431            'lexeme_id_HTML' => $lexemeIdLink,
432            'lemma_text' => $lemma->getText(),
433            'lemma_language' => $lemma->getLanguageCode(),
434            'language_item_id' => $lexeme->getLanguage()->getSerialization(),
435            'language_item_label' => $languageLabel ?
436                $languageLabel->getText() :
437                self::FALLBACK_LANGUAGE_LABEL,
438            'language_link_HTML' => $entityIdFormatter->formatEntityId( $lexeme->getLanguage() ),
439            'lexical_category_item_id' => $lexeme->getLexicalCategory()->getSerialization(),
440            'lexical_category_item_label' => $lexicalCategoryLabel ?
441                $lexicalCategoryLabel->getText() :
442                self::FALLBACK_LEXICAL_CATEGORY_LABEL,
443            'lexical_category_link_HTML' => $entityIdFormatter->formatEntityId( $lexeme->getLexicalCategory() ),
444        ];
445    }
446
447    private function processInfoPanelTemplate( array $params ): string {
448        $staticTemplateParams = [
449            'header' => $this->msg( 'wikibaselexeme-newlexeme-info-panel-heading' )->text(),
450            'lexicographical-data_HTML' => $this->msg(
451                'wikibaselexeme-newlexeme-info-panel-lexicographical-data'
452            )->parse(),
453            'no-general-data_HTML' => $this->msg( 'wikibaselexeme-newlexeme-info-panel-no-general-data' )->parse(),
454            'info_icon_HTML' => ( new IconWidget( [ 'icon' => 'infoFilled' ] ) )->toString(),
455            'language_label' => $this->msg( 'wikibaselexeme-field-language-label' )->text(),
456            'lexical_category_label' => $this->msg(
457                'wikibaselexeme-field-lexical-category-label'
458            )->text(),
459            'colon_separator' => $this->msg( 'colon-separator' )->text(),
460        ];
461        $params['lemma_language_HTML'] = LanguageCode::bcp47( $params['lemma_language'] );
462
463        return ( new TemplateParser( __DIR__ ) )->processTemplate(
464            'SpecialNewLexeme-infopanel',
465            $staticTemplateParams + $params
466        );
467    }
468
469    /**
470     * Get the suggested lexical category items with their labels and descriptions.
471     *
472     * @return array[]
473     */
474    private function getLexicalCategorySuggestions(): array {
475        $itemIds = array_map(
476            [ $this->entityIdParser, 'parse' ],
477            $this->getConfig()->get( 'LexemeLexicalCategoryItemIds' )
478        );
479        $labelDescriptionLookup = $this->labelDescriptionLookupFactory->newLabelDescriptionLookup(
480            $this->getLanguage(),
481            $itemIds, // prefetch labels and descriptions of all these item IDs
482            [ TermTypes::TYPE_LABEL, TermTypes::TYPE_DESCRIPTION ]
483        );
484
485        return array_map( static function ( EntityId $entityId ) use ( $labelDescriptionLookup ) {
486            $label = $labelDescriptionLookup->getLabel( $entityId );
487            $description = $labelDescriptionLookup->getDescription( $entityId );
488            $suggestion = [
489                'id' => $entityId->getSerialization(),
490                'display' => [],
491            ];
492            if ( $label !== null ) {
493                $suggestion['display']['label'] = self::termToArrayForJs( $label );
494            }
495            if ( $description !== null ) {
496                $suggestion['display']['description'] = self::termToArrayForJs( $description );
497            }
498            return $suggestion;
499        }, $itemIds );
500    }
501
502    private static function termToArrayForJs( TermFallback $term ): array {
503        return [
504            'language' => LanguageCode::bcp47( $term->getActualLanguageCode() ),
505            'value' => $term->getText(),
506        ];
507    }
508
509    private function createForm( array $exampleLexemeParams ): HTMLForm {
510        return HTMLForm::factory( 'ooui', $this->getFormFields( $exampleLexemeParams ), $this->getContext() )
511            ->setSubmitCallback(
512                function ( $data, HTMLForm $form ) {
513                    // $data is already validated at this point (according to the field definitions)
514
515                    $entity = $this->createEntityFromFormData( $data );
516
517                    $summary = $this->createSummary( $entity );
518
519                    $saveStatus = $this->saveEntity(
520                        $entity,
521                        $summary,
522                        $form->getRequest()->getVal( 'wpEditToken' )
523                    );
524
525                    if ( !$saveStatus->isGood() ) {
526                        return $saveStatus;
527                    }
528
529                    $metric = $this->statsFactory->getCounter( 'special_new_lexeme_nojs_create_total' );
530                    $metric->copyToStatsdAt( 'wikibase.lexeme.special.NewLexeme.nojs.create' )->increment();
531
532                    return $saveStatus;
533                }
534            )->addPreHtml( '<noscript>' )
535            ->addPostHtml( '</noscript>' );
536    }
537
538    private function createEntityFromFormData( array $formData ): Lexeme {
539        $entity = new Lexeme();
540        $lemmaLanguage = $formData[self::FIELD_LEMMA_LANGUAGE];
541
542        $lemmas = new TermList( [ new Term( $lemmaLanguage, $formData[self::FIELD_LEMMA] ) ] );
543        $entity->setLemmas( $lemmas );
544
545        $entity->setLexicalCategory( new ItemId( $formData[self::FIELD_LEXICAL_CATEGORY] ) );
546
547        $entity->setLanguage( new ItemId( $formData[self::FIELD_LEXEME_LANGUAGE] ) );
548
549        return $entity;
550    }
551
552    private function createSummary( Lexeme $lexeme ): Summary {
553        $uiLanguageCode = $this->getLanguage()->getCode();
554
555        $summary = new Summary( 'wbeditentity', 'create' );
556        $summary->setLanguage( $uiLanguageCode );
557
558        $lemmaIterator = $lexeme->getLemmas()->getIterator();
559        /** @var Term|null $lemmaTerm */
560        $lemmaTerm = $lemmaIterator->current();
561        $summary->addAutoSummaryArgs( $lemmaTerm->getText() );
562
563        return $summary;
564    }
565
566    private function redirectToEntityPage( EditEntityStatus $status ) {
567        $entity = $status->getRevision()->getEntity();
568        $title = $this->entityTitleLookup->getTitleForId( $entity->getId() );
569        $savedTempUser = $status->getSavedTempUser();
570        $redirectUrl = '';
571        if ( $savedTempUser !== null ) {
572            $this->getHookRunner()->onTempUserCreatedRedirect(
573                $this->getRequest()->getSession(),
574                $savedTempUser,
575                $title->getPrefixedDBkey(),
576                '',
577                '',
578                $redirectUrl
579            );
580        }
581        if ( !$redirectUrl ) {
582            $redirectUrl = $title->getFullURL();
583        }
584        $this->getOutput()->redirect( $redirectUrl );
585    }
586
587    private function newEditEntity(): EditEntity {
588        return $this->editEntityFactory->newEditEntity(
589            $this->getContext(),
590            null,
591            0,
592            $this->getRequest()->wasPosted()
593        );
594    }
595
596    private function saveEntity(
597        EntityDocument $entity,
598        FormatableSummary $summary,
599        string $token
600    ): EditEntityStatus {
601        return $this->newEditEntity()->attemptSave(
602            $entity,
603            $this->summaryFormatter->formatSummary( $summary ),
604            EDIT_NEW,
605            $token,
606            null,
607            $this->tags
608        );
609    }
610
611    private function getFormFields( array $exampleLexemeParams ): array {
612        return [
613            self::FIELD_LEMMA => [
614                'name' => self::FIELD_LEMMA,
615                'class' => HTMLTrimmedTextField::class,
616                'id' => 'wb-newlexeme-lemma',
617                'required' => true,
618                'placeholder-message' => [
619                    'wikibaselexeme-newlexeme-lemma-placeholder-with-example',
620                    Message::plaintextParam( $exampleLexemeParams['lemma_text'] ),
621                ],
622                'label-message' => 'wikibaselexeme-newlexeme-lemma',
623                'validation-callback' => function ( string $lemma ) {
624                    $result = $this->lemmaTermValidator->validate( $lemma );
625                    return $result->isValid() ?:
626                        $this->validatorErrorLocalizer->getErrorMessage( $result->getErrors()[0] );
627                },
628            ],
629            self::FIELD_LEMMA_LANGUAGE => [
630                'name' => self::FIELD_LEMMA_LANGUAGE,
631                'class' => LemmaLanguageField::class,
632                'cssclass' => 'lemma-language',
633                'id' => 'wb-newlexeme-lemma-language',
634                'label-message' => 'wikibaselexeme-newlexeme-lemma-language',
635                'placeholder-message' => [
636                    'wikibaselexeme-newlexeme-lemma-language-placeholder-with-example',
637                    Message::plaintextParam( $exampleLexemeParams['lemma_language'] ),
638                ],
639            ],
640            self::FIELD_LEXEME_LANGUAGE => [
641                'name' => self::FIELD_LEXEME_LANGUAGE,
642                'labelFieldName' => self::FIELD_LEXEME_LANGUAGE . '-label',
643                'class' => HTMLItemReferenceField::class,
644                'id' => 'wb-newlexeme-lexeme-language',
645                'label-message' => 'wikibaselexeme-newlexeme-language',
646                'required' => true,
647                'placeholder-message' => [
648                    'wikibaselexeme-newlexeme-language-placeholder-with-example',
649                    Message::plaintextParam( $exampleLexemeParams['language_item_id'] ),
650                ],
651            ],
652            self::FIELD_LEXICAL_CATEGORY => [
653                'name' => self::FIELD_LEXICAL_CATEGORY,
654                'labelFieldName' => self::FIELD_LEXICAL_CATEGORY . '-label',
655                'class' => HTMLItemReferenceField::class,
656                'id' => 'wb-newlexeme-lexicalCategory',
657                'label-message' => 'wikibaselexeme-newlexeme-lexicalcategory',
658                'required' => true,
659                'placeholder-message' => [
660                    'wikibaselexeme-newlexeme-lexicalcategory-placeholder-with-example',
661                    Message::plaintextParam( $exampleLexemeParams['lexical_category_item_id'] ),
662                ],
663            ],
664            'copyright-message' => [
665                'name' => 'copyright-message',
666                'type' => 'info',
667                'raw' => true,
668                'id' => 'wb-newlexeme-copyright',
669                'default' => $this->getCopyrightHTML(),
670            ],
671        ];
672    }
673
674    public function setHeaders(): void {
675        $out = $this->getOutput();
676        $out->setPageTitleMsg( $this->getDescription() );
677    }
678
679    /** @see \Wikibase\Repo\Specials\SpecialWikibasePage::getGroupName() */
680    protected function getGroupName(): string {
681        return 'wikibase';
682    }
683
684    public function getDescription(): Message {
685        return $this->msg( 'special-newlexeme' );
686    }
687
688    /**
689     * @throws UserBlockedError
690     */
691    private function checkBlocked(): void {
692        $block = $this->getUser()->getBlock();
693        if ( $block && $block->isSitewide() ) {
694            throw new UserBlockedError( $block );
695        }
696    }
697
698    /**
699     * @throws UserBlockedError
700     */
701    private function checkBlockedOnNamespace(): void {
702        $namespace = $this->entityNamespaceLookup->getEntityNamespace( Lexeme::ENTITY_TYPE );
703        $block = $this->getUser()->getBlock();
704        if ( $block && $block->appliesToNamespace( $namespace ) ) {
705            throw new UserBlockedError( $block );
706        }
707    }
708
709    /**
710     * @return string HTML
711     */
712    private function getCopyrightHTML() {
713        return $this->copyrightView->getHtml(
714            $this->getLanguage(),
715            'wikibaselexeme-newlexeme-submit'
716        );
717    }
718
719    private function showAnonymousEditWarning(): void {
720        if ( !$this->getUser()->isRegistered() ) {
721            $fullTitle = $this->getPageTitle();
722            $this->getOutput()->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
723            $this->getOutput()->addHTML(
724                Html::rawElement(
725                    'noscript',
726                    [],
727                    Html::warningBox(
728                        $this->msg( $this->anonymousEditWarningBuilder->buildAnonymousEditWarningMessage( $fullTitle ) )
729                            ->parse(),
730                        'wbl-snl-anonymous-edit-warning-no-js'
731                    )
732                )
733            );
734        }
735    }
736}