Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
41.81% |
97 / 232 |
|
45.00% |
9 / 20 |
CRAP | |
0.00% |
0 / 1 |
SetEntitySchemaLabelDescriptionAliases | |
41.81% |
97 / 232 |
|
45.00% |
9 / 20 |
425.45 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
70.00% |
7 / 10 |
|
0.00% |
0 / 1 |
3.24 | |||
submitEditFormCallback | |
80.00% |
16 / 20 |
|
0.00% |
0 / 1 |
4.13 | |||
getDescription | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getIdFromSubpageOrRequest | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
getLanguageFromSubpageOrRequestOrUI | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
displaySchemaLanguageSelectionForm | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
displayEditForm | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
20 | |||
isSecondForm | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSchemaNameBadge | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
getSchemaSelectionFormFields | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
2 | |||
getEditFormFields | |
0.00% |
0 / 73 |
|
0.00% |
0 / 1 |
2 | |||
isSelectionDataValid | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
displayCopyright | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
displayWarnings | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
buildLanguageAndSchemaNotice | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getSchemaDisplayLabel | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getWarnings | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
checkBlocked | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 |
1 | <?php |
2 | |
3 | declare( strict_types = 1 ); |
4 | |
5 | namespace EntitySchema\MediaWiki\Specials; |
6 | |
7 | use EntitySchema\DataAccess\EditConflict; |
8 | use EntitySchema\DataAccess\MediaWikiRevisionEntitySchemaUpdater; |
9 | use EntitySchema\Domain\Model\EntitySchemaId; |
10 | use EntitySchema\Presentation\InputValidator; |
11 | use EntitySchema\Services\Converter\EntitySchemaConverter; |
12 | use EntitySchema\Services\Converter\NameBadge; |
13 | use HTMLForm; |
14 | use InvalidArgumentException; |
15 | use MediaWiki\Html\Html; |
16 | use MediaWiki\Linker\LinkTarget; |
17 | use MediaWiki\MediaWikiServices; |
18 | use MediaWiki\Output\OutputPage; |
19 | use MediaWiki\Request\WebRequest; |
20 | use MediaWiki\Revision\SlotRecord; |
21 | use MediaWiki\SpecialPage\SpecialPage; |
22 | use MediaWiki\Status\Status; |
23 | use MediaWiki\Title\Title; |
24 | use MediaWiki\User\TempUser\TempUserConfig; |
25 | use Message; |
26 | use PermissionsError; |
27 | use RuntimeException; |
28 | use Wikibase\Lib\SettingsArray; |
29 | use Wikibase\Repo\CopyrightMessageBuilder; |
30 | use 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 | */ |
37 | class 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 | } |