Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
92.11% |
210 / 228 |
|
70.00% |
14 / 20 |
CRAP | |
0.00% |
0 / 1 |
SetEntitySchemaLabelDescriptionAliases | |
92.11% |
210 / 228 |
|
70.00% |
14 / 20 |
42.87 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
submitEditFormCallback | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
2 | |||
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 | |
65.52% |
19 / 29 |
|
0.00% |
0 / 1 |
4.66 | |||
isSecondForm | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSchemaNameBadge | |
72.73% |
8 / 11 |
|
0.00% |
0 / 1 |
3.18 | |||
getSchemaSelectionFormFields | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
2 | |||
getEditFormFields | |
100.00% |
73 / 73 |
|
100.00% |
1 / 1 |
1 | |||
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 | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getSchemaDisplayLabel | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
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\EntitySchemaStatus; |
8 | use EntitySchema\DataAccess\MediaWikiRevisionEntitySchemaUpdater; |
9 | use EntitySchema\Domain\Model\EntitySchemaId; |
10 | use EntitySchema\MediaWiki\EntitySchemaRedirectTrait; |
11 | use EntitySchema\Presentation\InputValidator; |
12 | use EntitySchema\Services\Converter\EntitySchemaConverter; |
13 | use EntitySchema\Services\Converter\NameBadge; |
14 | use InvalidArgumentException; |
15 | use MediaWiki\Exception\PermissionsError; |
16 | use MediaWiki\Html\Html; |
17 | use MediaWiki\HTMLForm\HTMLForm; |
18 | use MediaWiki\Linker\LinkTarget; |
19 | use MediaWiki\MediaWikiServices; |
20 | use MediaWiki\Message\Message; |
21 | use MediaWiki\Output\OutputPage; |
22 | use MediaWiki\Request\WebRequest; |
23 | use MediaWiki\Revision\SlotRecord; |
24 | use MediaWiki\SpecialPage\SpecialPage; |
25 | use MediaWiki\Status\Status; |
26 | use MediaWiki\Title\Title; |
27 | use MediaWiki\User\TempUser\TempUserConfig; |
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 | 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 | } |