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