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 Iterator;
8use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
9use MediaWiki\Config\ConfigException;
10use MediaWiki\Html\Html;
11use MediaWiki\Html\TemplateParser;
12use MediaWiki\HTMLForm\HTMLForm;
13use MediaWiki\Language\LanguageCode;
14use MediaWiki\Linker\LinkRenderer;
15use MediaWiki\Message\Message;
16use MediaWiki\SpecialPage\SpecialPage;
17use MediaWiki\Status\Status;
18use MediaWiki\User\TempUser\TempUserConfig;
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 array $tags;
75    private LinkRenderer $linkRenderer;
76    private StatsdDataFactoryInterface $statsDataFactory;
77    private MediaWikiEditEntityFactory $editEntityFactory;
78    private EntityNamespaceLookup $entityNamespaceLookup;
79    private EntityTitleStoreLookup $entityTitleLookup;
80    private EntityLookup $entityLookup;
81    private EntityIdParser $entityIdParser;
82    private SummaryFormatter $summaryFormatter;
83    private EntityIdFormatterFactory $entityIdFormatterFactory;
84    private FallbackLabelDescriptionLookupFactory $labelDescriptionLookupFactory;
85    private ValidatorErrorLocalizer $validatorErrorLocalizer;
86    private LemmaTermValidator $lemmaTermValidator;
87    private SpecialPageCopyrightView $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,