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