Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.35% covered (success)
99.35%
153 / 154
90.91% covered (success)
90.91%
10 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
NewEntitySchema
99.35% covered (success)
99.35%
153 / 154
90.91% covered (success)
90.91%
10 / 11
19
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%
21 / 21
100.00% covered (success)
100.00%
1 / 1
4
 submitCallback
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
1
 getDescription
100.00% covered (success)
100.00%
1 / 1
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
 getFormFields
100.00% covered (success)
100.00%
64 / 64
100.00% covered (success)
100.00%
1 / 1
1
 displayBeforeForm
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getCopyrightHTML
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getWarnings
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 addJavaScript
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 checkPermissionsWithSubpage
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare( strict_types = 1 );
4
5namespace EntitySchema\MediaWiki\Specials;
6
7use EntitySchema\DataAccess\MediaWikiPageUpdaterFactory;
8use EntitySchema\DataAccess\MediaWikiRevisionEntitySchemaInserter;
9use EntitySchema\DataAccess\WatchlistUpdater;
10use EntitySchema\Domain\Storage\IdGenerator;
11use EntitySchema\Presentation\InputValidator;
12use HTMLForm;
13use MediaWiki\Html\Html;
14use MediaWiki\MediaWikiServices;
15use MediaWiki\Output\OutputPage;
16use MediaWiki\SpecialPage\SpecialPage;
17use MediaWiki\Status\Status;
18use MediaWiki\Title\Title;
19use MediaWiki\User\TempUser\TempUserConfig;
20use Message;
21use PermissionsError;
22use Wikibase\Lib\SettingsArray;
23use Wikibase\Repo\CopyrightMessageBuilder;
24use Wikibase\Repo\Specials\SpecialPageCopyrightView;
25
26/**
27 * Page for creating a new EntitySchema.
28 *
29 * @license GPL-2.0-or-later
30 */
31class NewEntitySchema extends SpecialPage {
32
33    public const FIELD_DESCRIPTION = 'description';
34
35    public const FIELD_LABEL = 'label';
36
37    public const FIELD_ALIASES = 'aliases';
38
39    public const FIELD_SCHEMA_TEXT = 'schema-text';
40
41    public const FIELD_LANGUAGE = 'languagecode';
42
43    private IdGenerator $idGenerator;
44
45    private SpecialPageCopyrightView $copyrightView;
46
47    private TempUserConfig $tempUserConfig;
48
49    public function __construct(
50        TempUserConfig $tempUserConfig,
51        SettingsArray $repoSettings,
52        IdGenerator $idGenerator
53    ) {
54        parent::__construct(
55            'NewEntitySchema',
56            'createpage'
57        );
58        $this->idGenerator = $idGenerator;
59        $this->copyrightView = new SpecialPageCopyrightView(
60            new CopyrightMessageBuilder(),
61            $repoSettings->getSetting( 'dataRightsUrl' ),
62            $repoSettings->getSetting( 'dataRightsText' )
63        );
64        $this->tempUserConfig = $tempUserConfig;
65    }
66
67    public function execute( $subPage ): void {
68        parent::execute( $subPage );
69
70        $this->checkPermissionsWithSubpage( $subPage );
71        $this->checkReadOnly();
72
73        $form = HTMLForm::factory( 'ooui', $this->getFormFields(), $this->getContext() )
74            ->setSubmitName( 'submit' )
75            ->setSubmitID( 'entityschema-newschema-submit' )
76            ->setSubmitTextMsg( 'entityschema-newschema-submit' )
77            ->setValidationErrorMessage( [ [
78                'entityschema-error-possibly-multiple-messages-available',
79            ] ] )
80            ->setSubmitCallback( [ $this, 'submitCallback' ] );
81        $form->prepareForm();
82
83        /** @var Status|false $submitStatus `false` if form was not submitted */
84        $submitStatus = $form->tryAuthorizedSubmit();
85
86        if ( $submitStatus && $submitStatus->isGood() ) {
87            $this->getOutput()->redirect(
88                $submitStatus->getValue()
89            );
90            return;
91        }
92
93        $this->addJavaScript();
94        $this->displayBeforeForm( $this->getOutput() );
95
96        $form->displayForm( $submitStatus ?: Status::newGood() );
97    }
98
99    public function submitCallback( array $data, HTMLForm $form ): Status {
100        // TODO: no form data validation??
101
102        $pageUpdaterFactory = new MediaWikiPageUpdaterFactory( $this->getUser() );
103
104        $services = MediaWikiServices::getInstance();
105        $schemaInserter = new MediaWikiRevisionEntitySchemaInserter(
106            $pageUpdaterFactory,
107            new WatchlistUpdater( $this->getUser(), NS_ENTITYSCHEMA_JSON ),
108            $this->idGenerator,
109            $this->getContext(),
110            $services->getLanguageFactory(),
111            $services->getHookContainer(),
112            $services->getTitleFactory()
113        );
114        $newId = $schemaInserter->insertSchema(
115            $data[self::FIELD_LANGUAGE],
116            $data[self::FIELD_LABEL],
117            $data[self::FIELD_DESCRIPTION],
118            array_filter( array_map( 'trim', explode( '|', $data[self::FIELD_ALIASES] ) ) ),
119            $data[self::FIELD_SCHEMA_TEXT]
120        );
121
122        $title = Title::makeTitle( NS_ENTITYSCHEMA_JSON, $newId->getId() );
123
124        return Status::newGood( $title->getFullURL() );
125    }
126
127    public function getDescription(): Message {
128        return $this->msg( 'special-newschema' );
129    }
130
131    protected function getGroupName(): string {
132        return 'wikibase';
133    }
134
135    private function getFormFields(): array {
136        $langCode = $this->getLanguage()->getCode();
137        $langName = MediaWikiServices::getInstance()->getLanguageNameUtils()
138            ->getLanguageName( $langCode, $langCode );
139        $inputValidator = InputValidator::newFromGlobalState();
140        return [
141            self::FIELD_LABEL => [
142                'name' => self::FIELD_LABEL,
143                'type' => 'text',
144                'id' => 'entityschema-newschema-label',
145                'required' => true,
146                'default' => '',
147                'placeholder-message' => $this->msg( 'entityschema-label-edit-placeholder' )
148                    ->params( $langName ),
149                'label-message' => 'entityschema-newschema-label',
150                'validation-callback' => [
151                    $inputValidator,
152                    'validateStringInputLength',
153                ],
154            ],
155            self::FIELD_DESCRIPTION => [
156                'name' => self::FIELD_DESCRIPTION,
157                'type' => 'text',
158                'default' => '',
159                'id' => 'entityschema-newschema-description',
160                'placeholder-message' => $this->msg( 'entityschema-description-edit-placeholder' )
161                    ->params( $langName ),
162                'label-message' => 'entityschema-newschema-description',
163                'validation-callback' => [
164                    $inputValidator,
165                    'validateStringInputLength',
166                ],
167            ],
168            self::FIELD_ALIASES => [
169                'name' => self::FIELD_ALIASES,
170                'type' => 'text',
171                'default' => '',
172                'id' => 'entityschema-newschema-aliases',
173                'placeholder-message' => $this->msg( 'entityschema-aliases-edit-placeholder' )
174                    ->params( $langName ),
175                'label-message' => 'entityschema-newschema-aliases',
176                'validation-callback' => [
177                    $inputValidator,
178                    'validateAliasesLength',
179                ],
180            ],
181            self::FIELD_SCHEMA_TEXT => [
182                'name' => self::FIELD_SCHEMA_TEXT,
183                'type' => 'textarea',
184                'default' => '',
185                'id' => 'entityschema-newschema-schema-text',
186                'placeholder' => "<human> {\n  wdt:P31 [wd:Q5]\n}",
187                'label-message' => 'entityschema-newschema-schema-shexc',
188                'validation-callback' => [
189                    $inputValidator,
190                    'validateSchemaTextLength',
191                ],
192                'useeditfont' => true,
193            ],
194            self::FIELD_LANGUAGE => [
195                'name' => self::FIELD_LANGUAGE,
196                'type' => 'hidden',
197                'default' => $langCode,
198            ],
199        ];
200    }
201
202    private function displayBeforeForm( OutputPage $output ): void {
203        $output->addHTML( $this->getCopyrightHTML() );
204
205        foreach ( $this->getWarnings() as $warning ) {
206            $output->addHTML( Html::rawElement( 'div', [ 'class' => 'warning' ], $warning ) );
207        }
208    }
209
210    /**
211     * @return string HTML
212     */
213    private function getCopyrightHTML() {
214        return $this->copyrightView
215            ->getHtml( $this->getLanguage(), 'entityschema-newschema-submit' );
216    }
217
218    private function getWarnings(): array {
219        if ( $this->getUser()->isAnon() && !$this->tempUserConfig->isEnabled() ) {
220            return [
221                $this->msg(
222                    'entityschema-anonymouseditwarning'
223                )->parse(),
224            ];
225        }
226
227        return [];
228    }
229
230    private function addJavaScript(): void {
231        $output = $this->getOutput();
232        $output->addModules( [
233            'ext.EntitySchema.special.newEntitySchema',
234        ] );
235        $output->addJsConfigVars( [
236            'wgEntitySchemaSchemaTextMaxSizeBytes' =>
237                intval( $this->getConfig()->get( 'EntitySchemaSchemaTextMaxSizeBytes' ) ),
238            'wgEntitySchemaNameBadgeMaxSizeChars' =>
239                intval( $this->getConfig()->get( 'EntitySchemaNameBadgeMaxSizeChars' ) ),
240        ] );
241    }
242
243    /**
244     * Checks if the user has permissions to perform this page’s action,
245     * and throws a {@link PermissionsError} if they don’t.
246     *
247     * @throws PermissionsError
248     */
249    protected function checkPermissionsWithSubpage( ?string $subPage ): void {
250        $pm = MediaWikiServices::getInstance()->getPermissionManager();
251        $checkReplica = !$this->getRequest()->wasPosted();
252        $permissionErrors = $pm->getPermissionErrors(
253            $this->getRestriction(),
254            $this->getUser(),
255            $this->getPageTitle( $subPage ),
256            $checkReplica ? $pm::RIGOR_FULL : $pm::RIGOR_SECURE,
257            [
258                'ns-specialprotected', // ignore “special pages cannot be edited”
259            ]
260        );
261        if ( $permissionErrors !== [] ) {
262            // reindex $permissionErrors:
263            // the ignoreErrors param (ns-specialprotected) may have left holes,
264            // but PermissionsError expects $errors[0] to exist
265            $permissionErrors = array_values( $permissionErrors );
266            throw new PermissionsError( $this->getRestriction(), $permissionErrors );
267        }
268    }
269
270}