Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.11% covered (success)
92.11%
210 / 228
70.00% covered (warning)
70.00%
14 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
SetEntitySchemaLabelDescriptionAliases
92.11% covered (success)
92.11%
210 / 228
70.00% covered (warning)
70.00%
14 / 20
42.87
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 submitEditFormCallback
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
2
 getDescription
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIdFromSubpageOrRequest
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getLanguageFromSubpageOrRequestOrUI
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 displaySchemaLanguageSelectionForm
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 displayEditForm
65.52% covered (warning)
65.52%
19 / 29
0.00% covered (danger)
0.00%
0 / 1
4.66
 isSecondForm
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSchemaNameBadge
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
3.18
 getSchemaSelectionFormFields
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
2
 getEditFormFields
100.00% covered (success)
100.00%
73 / 73
100.00% covered (success)
100.00%
1 / 1
1
 isSelectionDataValid
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 displayCopyright
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 displayWarnings
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 buildLanguageAndSchemaNotice
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getSchemaDisplayLabel
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getWarnings
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 checkBlocked
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
1<?php
2
3declare( strict_types = 1 );
4
5namespace EntitySchema\MediaWiki\Specials;
6
7use EntitySchema\DataAccess\EntitySchemaStatus;
8use EntitySchema\DataAccess\MediaWikiRevisionEntitySchemaUpdater;
9use EntitySchema\Domain\Model\EntitySchemaId;
10use EntitySchema\MediaWiki\EntitySchemaRedirectTrait;
11use EntitySchema\Presentation\InputValidator;
12use EntitySchema\Services\Converter\EntitySchemaConverter;
13use EntitySchema\Services\Converter\NameBadge;
14use InvalidArgumentException;
15use MediaWiki\Exception\PermissionsError;
16use MediaWiki\Html\Html;
17use MediaWiki\HTMLForm\HTMLForm;
18use MediaWiki\Linker\LinkTarget;
19use MediaWiki\MediaWikiServices;
20use MediaWiki\Message\Message;
21use MediaWiki\Output\OutputPage;
22use MediaWiki\Request\WebRequest;
23use MediaWiki\Revision\SlotRecord;
24use MediaWiki\SpecialPage\SpecialPage;
25use MediaWiki\Status\Status;
26use MediaWiki\Title\Title;
27use MediaWiki\User\TempUser\TempUserConfig;
28use Wikibase\Lib\SettingsArray;
29use Wikibase\Repo\CopyrightMessageBuilder;
30use Wikibase\Repo\Specials\SpecialPageCopyrightView;
31
32/**
33 * Page for editing label, description and aliases of a Schema
34 *
35 * @license GPL-2.0-or-later
36 */
37class SetEntitySchemaLabelDescriptionAliases extends SpecialPage {
38
39    use EntitySchemaRedirectTrait;
40
41    public const FIELD_ID = 'ID';
42    public const FIELD_LANGUAGE = 'languagecode';
43    public const FIELD_DESCRIPTION = 'description';
44    public const FIELD_LABEL = 'label';
45    public const FIELD_ALIASES = 'aliases';
46    public const FIELD_BASE_REV = 'base-rev';
47    private const SUBMIT_SELECTION_NAME = 'submit-selection';
48    private const SUBMIT_EDIT_NAME = 'submit-edit';
49
50    private string $htmlFormProvider;
51
52    private SpecialPageCopyrightView $copyrightView;
53
54    private TempUserConfig $tempUserConfig;
55
56    public function __construct(
57        TempUserConfig $tempUserConfig,
58        SettingsArray $repoSettings,
59        string $htmlFormProvider = HTMLForm::class
60    ) {
61        parent::__construct(
62            'SetEntitySchemaLabelDescriptionAliases',
63            'edit'
64        );
65
66        $this->htmlFormProvider = $htmlFormProvider;
67        $this->copyrightView = new SpecialPageCopyrightView(
68            new CopyrightMessageBuilder(),
69            $repoSettings->getSetting( 'dataRightsUrl' ),
70            $repoSettings->getSetting( 'dataRightsText' )
71        );
72        $this->tempUserConfig = $tempUserConfig;
73    }
74
75    /** @inheritDoc */
76    public function execute( $subPage ): void {
77        parent::execute( $subPage );
78
79        $request = $this->getRequest();
80        $subPage = $subPage ?: '';
81        $id = $this->getIdFromSubpageOrRequest( $subPage, $request );
82        $language = $this->getLanguageFromSubpageOrRequestOrUI( $subPage, $request );
83
84        if ( $this->isSelectionDataValid( $id, $language ) ) {
85            $baseRevId = $request->getInt( self::FIELD_BASE_REV );
86            // @phan-suppress-next-line PhanTypeMismatchArgumentNullable isSelectionDataValid() guarantees $id !== null
87            $this->displayEditForm( new EntitySchemaId( $id ), $language, $baseRevId );
88            return;
89        }
90
91        $this->displaySchemaLanguageSelectionForm( $id, $language );
92    }
93
94    public function submitEditFormCallback( array $data ): Status {
95        try {
96            $id = new EntitySchemaId( $data[self::FIELD_ID] );
97        } catch ( InvalidArgumentException $e ) {
98            return Status::newFatal( 'entityschema-error-schemaupdate-failed' );
99        }
100        $title = Title::makeTitle( NS_ENTITYSCHEMA_JSON, $id->getId() );
101        $this->checkBlocked( $title );
102        $aliases = array_map( 'trim', explode( '|', $data[self::FIELD_ALIASES] ) );
103        $schemaUpdater = MediaWikiRevisionEntitySchemaUpdater::newFromContext( $this->getContext() );
104
105        $status = $schemaUpdater->updateSchemaNameBadge(
106            $id,
107            $data[self::FIELD_LANGUAGE],
108            $data[self::FIELD_LABEL],
109            $data[self::FIELD_DESCRIPTION],
110            $aliases,
111            (int)$data[self::FIELD_BASE_REV]
112        );
113        $status->replaceMessage( 'edit-conflict', 'entityschema-error-namebadge-conflict' );
114        return $status;
115    }
116
117    public function getDescription(): Message {
118        return $this->msg( 'entityschema-special-setlabeldescriptionaliases' );
119    }
120
121    private function getIdFromSubpageOrRequest( string $subpage, WebRequest $request ): ?string {
122        $subpageParts = array_filter( explode( '/', $subpage, 2 ) );
123        if ( count( $subpageParts ) > 0 ) {
124            return $subpageParts[0];
125        }
126        return $request->getText( self::FIELD_ID ) ?: null;
127    }
128
129    private function getLanguageFromSubpageOrRequestOrUI( string $subpage, WebRequest $request ): string {
130        $subpageParts = array_filter( explode( '/', $subpage, 2 ) );
131        if ( count( $subpageParts ) === 2 ) {
132            return $subpageParts[1];
133        }
134
135        return $request->getText( self::FIELD_LANGUAGE ) ?: $this->getLanguage()->getCode();
136    }
137
138    private function displaySchemaLanguageSelectionForm( ?string $defaultId, string $defaultLanguage ): void {
139        $formDescriptor = $this->getSchemaSelectionFormFields( $defaultId, $defaultLanguage );
140
141        $form = $this->htmlFormProvider::factory( 'ooui', $formDescriptor, $this->getContext() )
142            ->setSubmitName( self::SUBMIT_SELECTION_NAME )
143            ->setSubmitID( 'entityschema-special-schema-id-submit' )
144            ->setSubmitTextMsg( 'entityschema-special-id-submit' )
145            ->setTitle( $this->getPageTitle() );
146        $form->prepareForm();
147        $submitStatus = $form->tryAuthorizedSubmit();
148        $form->displayForm( $submitStatus ?: Status::newGood() );
149    }
150
151    private function displayEditForm( EntitySchemaId $id, string $langCode, int $baseRevId ): void {
152        $output = $this->getOutput();
153        $title = Title::makeTitle( NS_ENTITYSCHEMA_JSON, $id->getId() );
154        $schemaNameBadge = $this->getSchemaNameBadge( $title, $langCode, $baseRevId );
155        $formDescriptor = $this->getEditFormFields( $id, $langCode, $schemaNameBadge, $baseRevId );
156
157        $form = $this->htmlFormProvider::factory( 'ooui', $formDescriptor, $this->getContext() )
158            ->setSubmitName( self::SUBMIT_EDIT_NAME )
159            ->setSubmitID( 'entityschema-special-schema-id-submit' )
160            ->setSubmitTextMsg( 'entityschema-special-id-submit' )
161            ->setValidationErrorMessage( [ [
162                'entityschema-error-possibly-multiple-messages-available',
163            ] ] );
164        $form->prepareForm();
165
166        if ( !$this->isSecondForm() ) {
167            $form->setSubmitCallback( [ $this, 'submitEditFormCallback' ] );
168
169            $submitStatus = $form->tryAuthorizedSubmit();
170            if ( $submitStatus && $submitStatus->isGood() ) {
171                // wrap it, in case HTMLForm turned it into a generic Status
172                $submitStatus = EntitySchemaStatus::wrap( $submitStatus );
173                $this->redirectToEntitySchema( $submitStatus );
174                return;
175            }
176        }
177
178        $output->addModules( [
179            'ext.EntitySchema.special.setEntitySchemaLabelDescriptionAliases.edit',
180        ] );
181        $output->addJsConfigVars(
182            'wgEntitySchemaNameBadgeMaxSizeChars',
183            intval( $this->getConfig()->get( 'EntitySchemaNameBadgeMaxSizeChars' ) )
184        );
185        $this->displayWarnings( $output );
186        $form->displayForm( $submitStatus ?? Status::newGood() );
187        $this->displayCopyright( $output );
188    }
189
190    /**
191     * Check if the second form is requested.
192     */
193    private function isSecondForm(): bool {
194        return $this->getContext()->getRequest()->getCheck( self::SUBMIT_SELECTION_NAME );
195    }
196
197    /**
198     * Gets the Schema NameBadge (label, desc, aliases) by interface language
199     *
200     * @param Title $title instance of Title for a specific Schema
201     * @param string $langCode
202     * @param int &$revId the revision from which to load data, or 0 to load the latest revision
203     * of $title, in which case &$revId will be replaced with that revision's ID
204     *
205     * @return NameBadge
206     */
207    private function getSchemaNameBadge( Title $title, string $langCode, int &$revId ): NameBadge {
208        if ( $revId > 0 ) {
209            $revision = MediaWikiServices::getInstance()->getRevisionLookup()
210                ->getRevisionById( $revId );
211            if ( $revision->getPageId() !== $title->getArticleID() ) {
212                throw new InvalidArgumentException( 'revision does not match title' );
213            }
214        } else {
215            $wikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
216            $revision = $wikiPage->getRevisionRecord();
217            $revId = $revision->getId();
218        }
219        // @phan-suppress-next-line PhanUndeclaredMethod
220        $schema = $revision->getContent( SlotRecord::MAIN )->getText();
221        $converter = new EntitySchemaConverter();
222        return $converter->getMonolingualNameBadgeData( $schema, $langCode );
223    }
224
225    private function getSchemaSelectionFormFields( ?string $defaultId, string $defaultLanguage ): array {
226        $inputValidator = InputValidator::newFromGlobalState();
227        return [
228            self::FIELD_ID => [
229                'name' => self::FIELD_ID,
230                'type' => 'text',
231                'id' => 'entityschema-special-schema-id',
232                'required' => true,
233                'default' => $defaultId ?: '',
234                'placeholder-message' => 'entityschema-special-id-placeholder',
235                'label-message' => 'entityschema-special-id-inputlabel',
236                'validation-callback' => [
237                    $inputValidator,
238                    'validateIDExists',
239                ],
240            ],
241            self::FIELD_LANGUAGE => [
242                'name' => self::FIELD_LANGUAGE,
243                'type' => 'text',
244                'id' => 'entityschema-language-code',
245                'required' => true,
246                'default' => $defaultLanguage,
247                'label-message' => 'entityschema-special-language-inputlabel',
248                'validation-callback' => [
249                    $inputValidator,
250                    'validateLangCodeIsSupported',
251                ],
252            ],
253        ];
254    }
255
256    private function getEditFormFields(
257        EntitySchemaId $id,
258        string $badgeLangCode,
259        NameBadge $nameBadge,
260        int $baseRevId
261    ): array {
262        $label = $nameBadge->label;
263        $description = $nameBadge->description;
264        $aliases = implode( '|', $nameBadge->aliases );
265        $uiLangCode = $this->getLanguage()->getCode();
266        $langName = MediaWikiServices::getInstance()->getLanguageNameUtils()
267            ->getLanguageName( $badgeLangCode, $uiLangCode );
268        $inputValidator = InputValidator::newFromGlobalState();
269        return [
270            'notice' => [
271                'type' => 'info',
272                'raw' => true,
273                'default' => $this->buildLanguageAndSchemaNotice( $langName, $label, $id ),
274            ],
275            self::FIELD_ID => [
276                'name' => self::FIELD_ID,
277                'type' => 'hidden',
278                'id' => 'entityschema-id',
279                'required' => true,
280                'default' => $id->getId(),
281            ],
282            self::FIELD_LANGUAGE => [
283                'name' => self::FIELD_LANGUAGE,
284                'type' => 'hidden',
285                'id' => 'entityschema-language-code',
286                'required' => true,
287                'default' => $badgeLangCode,
288            ],
289            self::FIELD_LABEL => [
290                'name' => self::FIELD_LABEL,
291                'type' => 'text',
292                'id' => 'entityschema-title-label',
293                'default' => $label,
294                'placeholder-message' => $this->msg( 'entityschema-label-edit-placeholder' )
295                    ->params( $langName ),
296                'label-message' => 'entityschema-special-label',
297                'validation-callback' => [
298                    $inputValidator,
299                    'validateStringInputLength',
300                ],
301            ],
302            self::FIELD_DESCRIPTION => [
303                'name' => self::FIELD_DESCRIPTION,
304                'type' => 'text',
305                'default' => $description,
306                'id' => 'entityschema-heading-description',
307                'placeholder-message' => $this->msg( 'entityschema-description-edit-placeholder' )
308                    ->params( $langName ),
309                'label-message' => 'entityschema-special-description',
310                'validation-callback' => [
311                    $inputValidator,
312                    'validateStringInputLength',
313                ],
314            ],
315            self::FIELD_ALIASES => [
316                'name' => self::FIELD_ALIASES,
317                'type' => 'text',
318                'default' => $aliases,
319                'id' => 'entityschema-heading-aliases',
320                'placeholder-message' => $this->msg( 'entityschema-aliases-edit-placeholder' )
321                    ->params( $langName ),
322                'label-message' => 'entityschema-special-aliases',
323                'validation-callback' => [
324                    $inputValidator,
325                    'validateAliasesLength',
326                ],
327            ],
328            self::FIELD_BASE_REV => [
329                'name' => self::FIELD_BASE_REV,
330                'type' => 'hidden',
331                'required' => true,
332                'default' => $baseRevId,
333            ],
334        ];
335    }
336
337    /**
338     * Validate ID and Language Code values
339     *
340     * @param string|null $id ID of the Schema
341     * @param string|null $language language code of the Schema
342     *
343     * @return bool
344     */
345    private function isSelectionDataValid( ?string $id, ?string $language ): bool {
346        if ( $id === null || $language === null ) {
347            return false;
348        }
349        $inputValidator = InputValidator::newFromGlobalState();
350
351        return $inputValidator->validateIDExists( $id ) === true &&
352            $inputValidator->validateLangCodeIsSupported( $language ) === true;
353    }
354
355    private function displayCopyright( OutputPage $output ): void {
356        $output->addHTML( $this->copyrightView
357            ->getHtml( $this->getLanguage(), 'entityschema-special-id-submit' ) );
358    }
359
360    private function displayWarnings( OutputPage $output ): void {
361        foreach ( $this->getWarnings() as $warning ) {
362            $output->addHTML( Html::rawElement( 'div', [ 'class' => 'warning' ], $warning ) );
363        }
364    }
365
366    /**
367     * Build the info message atop of the second form
368     *
369     * @return string HTML
370     */
371    private function buildLanguageAndSchemaNotice(
372        string $langName,
373        string $label,
374        EntitySchemaId $entitySchemaId
375    ): string {
376        $title = Title::makeTitle( NS_ENTITYSCHEMA_JSON, $entitySchemaId->getId() );
377        return $this->msg( 'entityschema-special-setlabeldescriptionaliases-info' )
378            ->params( $langName )
379            ->params( $this->getSchemaDisplayLabel( $label, $entitySchemaId ) )
380            ->params( $title->getPrefixedText() )
381            ->parse();
382    }
383
384    private function getSchemaDisplayLabel( string $label, EntitySchemaId $entitySchemaId ): string {
385        if ( !$label ) {
386            return $entitySchemaId->getId();
387        }
388
389        return $label . ' ' . $this->msg( 'parentheses' )->params( $entitySchemaId->getId() )->escaped();
390    }
391
392    private function getWarnings(): array {
393        if ( $this->getUser()->isAnon() && !$this->tempUserConfig->isEnabled() ) {
394            return [
395                $this->msg(
396                    'entityschema-anonymouseditwarning'
397                )->parse(),
398            ];
399        }
400
401        return [];
402    }
403
404    protected function getGroupName(): string {
405        return 'wikibase';
406    }
407
408    private function checkBlocked( LinkTarget $title ): void {
409        $errors = MediaWikiServices::getInstance()->getPermissionManager()
410            ->getPermissionErrors( $this->getRestriction(), $this->getUser(), $title );
411        if ( $errors !== [] ) {
412            throw new PermissionsError( $this->getRestriction(), $errors );
413        }
414    }
415
416}