Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
90.80% |
227 / 250 |
|
68.75% |
11 / 16 |
CRAP | |
0.00% |
0 / 1 |
SpecialConstraintReport | |
90.80% |
227 / 250 |
|
68.75% |
11 / 16 |
33.85 | |
0.00% |
0 / 1 |
factory | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
__construct | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
1 | |||
getModules | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDescription | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
84.78% |
39 / 46 |
|
0.00% |
0 / 1 |
9.29 | |||
buildEntityIdForm | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
1.00 | |||
buildNotice | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
getExplanationText | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
buildResultTable | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
2 | |||
appendToResultTable | |
94.59% |
35 / 37 |
|
0.00% |
0 / 1 |
3.00 | |||
buildResultHeader | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
buildSummary | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
formatStatus | |
100.00% |
33 / 33 |
|
100.00% |
1 / 1 |
2 | |||
getClaimLink | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
getClaimUrl | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | declare( strict_types = 1 ); |
4 | |
5 | namespace WikibaseQuality\ConstraintReport\Specials; |
6 | |
7 | use HtmlArmor; |
8 | use InvalidArgumentException; |
9 | use MediaWiki\Config\Config; |
10 | use MediaWiki\Html\Html; |
11 | use MediaWiki\HTMLForm\Field\HTMLTextField; |
12 | use MediaWiki\HTMLForm\HTMLForm; |
13 | use MediaWiki\SpecialPage\SpecialPage; |
14 | use OOUI\IconWidget; |
15 | use OOUI\LabelWidget; |
16 | use UnexpectedValueException; |
17 | use Wikibase\DataModel\Entity\EntityId; |
18 | use Wikibase\DataModel\Entity\EntityIdParser; |
19 | use Wikibase\DataModel\Entity\EntityIdParsingException; |
20 | use Wikibase\DataModel\Entity\ItemId; |
21 | use Wikibase\DataModel\Entity\NumericPropertyId; |
22 | use Wikibase\DataModel\Services\EntityId\EntityIdFormatter; |
23 | use Wikibase\DataModel\Services\Lookup\EntityLookup; |
24 | use Wikibase\Lib\LanguageFallbackChainFactory; |
25 | use Wikibase\Lib\Store\EntityTitleLookup; |
26 | use Wikibase\Repo\EntityIdLabelFormatterFactory; |
27 | use Wikibase\View\EntityIdFormatterFactory; |
28 | use WikibaseQuality\ConstraintReport\ConstraintCheck\DelegatingConstraintChecker; |
29 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessageRenderer; |
30 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessageRendererFactory; |
31 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult; |
32 | use WikibaseQuality\ConstraintReport\Html\HtmlTableBuilder; |
33 | use WikibaseQuality\ConstraintReport\Html\HtmlTableCellBuilder; |
34 | use WikibaseQuality\ConstraintReport\Html\HtmlTableHeaderBuilder; |
35 | use Wikimedia\Stats\IBufferingStatsdDataFactory; |
36 | |
37 | /** |
38 | * Special page that displays all constraints that are defined on an Entity with additional information |
39 | * (whether it complied or was a violation, which parameters the constraint has etc.). |
40 | * |
41 | * @author BP2014N1 |
42 | * @license GPL-2.0-or-later |
43 | */ |
44 | class SpecialConstraintReport extends SpecialPage { |
45 | |
46 | private EntityIdParser $entityIdParser; |
47 | private EntityLookup $entityLookup; |
48 | private EntityTitleLookup $entityTitleLookup; |
49 | private EntityIdFormatter $entityIdLabelFormatter; |
50 | private EntityIdFormatter $entityIdLinkFormatter; |
51 | private DelegatingConstraintChecker $constraintChecker; |
52 | private ViolationMessageRenderer $violationMessageRenderer; |
53 | private Config $config; |
54 | private IBufferingStatsdDataFactory $dataFactory; |
55 | |
56 | public static function factory( |
57 | Config $config, |
58 | IBufferingStatsdDataFactory $dataFactory, |
59 | EntityIdFormatterFactory $entityIdHtmlLinkFormatterFactory, |
60 | EntityIdLabelFormatterFactory $entityIdLabelFormatterFactory, |
61 | EntityIdParser $entityIdParser, |
62 | EntityTitleLookup $entityTitleLookup, |
63 | LanguageFallbackChainFactory $languageFallbackChainFactory, |
64 | EntityLookup $entityLookup, |
65 | DelegatingConstraintChecker $delegatingConstraintChecker, |
66 | ViolationMessageRendererFactory $violationMessageRendererFactory |
67 | ): self { |
68 | return new self( |
69 | $entityLookup, |
70 | $entityTitleLookup, |
71 | $entityIdLabelFormatterFactory, |
72 | $entityIdHtmlLinkFormatterFactory, |
73 | $entityIdParser, |
74 | $languageFallbackChainFactory, |
75 | $delegatingConstraintChecker, |
76 | $violationMessageRendererFactory, |
77 | $config, |
78 | $dataFactory |
79 | ); |
80 | } |
81 | |
82 | public function __construct( |
83 | EntityLookup $entityLookup, |
84 | EntityTitleLookup $entityTitleLookup, |
85 | EntityIdLabelFormatterFactory $entityIdLabelFormatterFactory, |
86 | EntityIdFormatterFactory $entityIdHtmlLinkFormatterFactory, |
87 | EntityIdParser $entityIdParser, |
88 | LanguageFallbackChainFactory $languageFallbackChainFactory, |
89 | DelegatingConstraintChecker $constraintChecker, |
90 | ViolationMessageRendererFactory $violationMessageRendererFactory, |
91 | Config $config, |
92 | IBufferingStatsdDataFactory $dataFactory |
93 | ) { |
94 | parent::__construct( 'ConstraintReport' ); |
95 | |
96 | $this->entityLookup = $entityLookup; |
97 | $this->entityTitleLookup = $entityTitleLookup; |
98 | $this->entityIdParser = $entityIdParser; |
99 | |
100 | $language = $this->getLanguage(); |
101 | |
102 | $this->entityIdLabelFormatter = $entityIdLabelFormatterFactory->getEntityIdFormatter( |
103 | $language |
104 | ); |
105 | |
106 | $this->entityIdLinkFormatter = $entityIdHtmlLinkFormatterFactory->getEntityIdFormatter( |
107 | $language |
108 | ); |
109 | |
110 | $this->constraintChecker = $constraintChecker; |
111 | |
112 | $this->violationMessageRenderer = $violationMessageRendererFactory->getViolationMessageRenderer( |
113 | $language, |
114 | $languageFallbackChainFactory->newFromLanguage( $language ), |
115 | $this->getContext() |
116 | ); |
117 | |
118 | $this->config = $config; |
119 | $this->dataFactory = $dataFactory; |
120 | } |
121 | |
122 | /** |
123 | * Returns array of modules that should be added |
124 | * |
125 | * @return string[] |
126 | */ |
127 | private function getModules(): array { |
128 | return [ |
129 | 'SpecialConstraintReportPage', |
130 | 'wikibase.quality.constraints.icon', |
131 | 'wikibase.alltargets', |
132 | ]; |
133 | } |
134 | |
135 | /** |
136 | * @see SpecialPage::getGroupName |
137 | * |
138 | * @return string |
139 | */ |
140 | protected function getGroupName() { |
141 | return 'wikibase'; |
142 | } |
143 | |
144 | /** |
145 | * @inheritDoc |
146 | */ |
147 | public function getDescription() { |
148 | return $this->msg( 'wbqc-constraintreport' ); |
149 | } |
150 | |
151 | /** |
152 | * @see SpecialPage::execute |
153 | * |
154 | * @param string|null $subPage |
155 | * |
156 | * @throws InvalidArgumentException |
157 | * @throws EntityIdParsingException |
158 | * @throws UnexpectedValueException |
159 | */ |
160 | public function execute( $subPage ) { |
161 | $out = $this->getOutput(); |
162 | |
163 | $postRequest = $this->getContext()->getRequest()->getVal( 'entityid' ); |
164 | if ( $postRequest ) { |
165 | try { |
166 | $entityId = $this->entityIdParser->parse( $postRequest ); |
167 | $out->redirect( $this->getPageTitle( $entityId->getSerialization() )->getLocalURL() ); |
168 | return; |
169 | } catch ( EntityIdParsingException $e ) { |
170 | // fall through, error is shown later |
171 | } |
172 | } |
173 | |
174 | $out->enableOOUI(); |
175 | $out->addModules( $this->getModules() ); |
176 | |
177 | $this->setHeaders(); |
178 | |
179 | $out->addHTML( $this->getExplanationText() ); |
180 | $this->buildEntityIdForm(); |
181 | |
182 | if ( $postRequest ) { |
183 | // must be an invalid entity ID (otherwise we would have redirected and returned above) |
184 | $out->addHTML( |
185 | $this->buildNotice( 'wbqc-constraintreport-invalid-entity-id', true ) |
186 | ); |
187 | return; |
188 | } |
189 | |
190 | if ( !$subPage ) { |
191 | return; |
192 | } |
193 | |
194 | if ( !is_string( $subPage ) ) { |
195 | throw new InvalidArgumentException( '$subPage must be string.' ); |
196 | } |
197 | |
198 | try { |
199 | $entityId = $this->entityIdParser->parse( $subPage ); |
200 | } catch ( EntityIdParsingException $e ) { |
201 | $out->addHTML( |
202 | $this->buildNotice( 'wbqc-constraintreport-invalid-entity-id', true ) |
203 | ); |
204 | return; |
205 | } |
206 | |
207 | if ( !$this->entityLookup->hasEntity( $entityId ) ) { |
208 | $out->addHTML( |
209 | $this->buildNotice( 'wbqc-constraintreport-not-existent-entity', true ) |
210 | ); |
211 | return; |
212 | } |
213 | |
214 | $this->dataFactory->increment( |
215 | 'wikibase.quality.constraints.specials.specialConstraintReport.executeCheck' |
216 | ); |
217 | $results = $this->constraintChecker->checkAgainstConstraintsOnEntityId( $entityId ); |
218 | |
219 | if ( $results !== [] ) { |
220 | $out->addHTML( |
221 | $this->buildResultHeader( $entityId ) |
222 | . $this->buildSummary( $results ) |
223 | . $this->buildResultTable( $entityId, $results ) |
224 | ); |
225 | } else { |
226 | $out->addHTML( |
227 | $this->buildResultHeader( $entityId ) |
228 | . $this->buildNotice( 'wbqc-constraintreport-empty-result' ) |
229 | ); |
230 | } |
231 | } |
232 | |
233 | /** |
234 | * Builds html form for entity id input |
235 | */ |
236 | private function buildEntityIdForm(): void { |
237 | $formDescriptor = [ |
238 | 'entityid' => [ |
239 | 'class' => HTMLTextField::class, |
240 | 'section' => 'section', |
241 | 'name' => 'entityid', |
242 | 'label-message' => 'wbqc-constraintreport-form-entityid-label', |
243 | 'cssclass' => 'wbqc-constraintreport-form-entity-id', |
244 | 'placeholder' => $this->msg( 'wbqc-constraintreport-form-entityid-placeholder' )->escaped(), |
245 | 'required' => true, |
246 | ], |
247 | ]; |
248 | $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext(), 'wbqc-constraintreport-form' ); |
249 | $htmlForm->setSubmitText( $this->msg( 'wbqc-constraintreport-form-submit-label' )->escaped() ); |
250 | $htmlForm->setSubmitCallback( static function () { |
251 | return false; |
252 | } ); |
253 | $htmlForm->setMethod( 'post' ); |
254 | $htmlForm->show(); |
255 | } |
256 | |
257 | /** |
258 | * Builds notice with given message. Optionally notice can be handles as error by settings $error to true |
259 | * |
260 | * @param string $messageKey |
261 | * @param bool $error |
262 | * |
263 | * @throws InvalidArgumentException |
264 | * |
265 | * @return string HTML |
266 | */ |
267 | private function buildNotice( string $messageKey, bool $error = false ): string { |
268 | $cssClasses = 'wbqc-constraintreport-notice'; |
269 | if ( $error ) { |
270 | $cssClasses .= ' wbqc-constraintreport-notice-error'; |
271 | } |
272 | |
273 | return Html::rawElement( |
274 | 'p', |
275 | [ |
276 | 'class' => $cssClasses, |
277 | ], |
278 | $this->msg( $messageKey )->escaped() |
279 | ); |
280 | } |
281 | |
282 | /** |
283 | * @return string HTML |
284 | */ |
285 | private function getExplanationText(): string { |
286 | return Html::rawElement( |
287 | 'div', |
288 | [ 'class' => 'wbqc-explanation' ], |
289 | Html::rawElement( |
290 | 'p', |
291 | [], |
292 | $this->msg( 'wbqc-constraintreport-explanation-part-one' )->escaped() |
293 | ) |
294 | . Html::rawElement( |
295 | 'p', |
296 | [], |
297 | $this->msg( 'wbqc-constraintreport-explanation-part-two' )->escaped() |
298 | ) |
299 | ); |
300 | } |
301 | |
302 | /** |
303 | * @param EntityId $entityId |
304 | * @param CheckResult[] $results |
305 | * |
306 | * @return string HTML |
307 | */ |
308 | private function buildResultTable( EntityId $entityId, array $results ): string { |
309 | // Set table headers |
310 | $table = new HtmlTableBuilder( |
311 | [ |
312 | new HtmlTableHeaderBuilder( |
313 | $this->msg( 'wbqc-constraintreport-result-table-header-status' )->text(), |
314 | true |
315 | ), |
316 | new HtmlTableHeaderBuilder( |
317 | $this->msg( 'wbqc-constraintreport-result-table-header-property' )->text(), |
318 | true |
319 | ), |
320 | new HtmlTableHeaderBuilder( |
321 | $this->msg( 'wbqc-constraintreport-result-table-header-message' )->text(), |
322 | true |
323 | ), |
324 | new HtmlTableHeaderBuilder( |
325 | $this->msg( 'wbqc-constraintreport-result-table-header-constraint' )->text(), |
326 | true |
327 | ), |
328 | ] |
329 | ); |
330 | |
331 | foreach ( $results as $result ) { |
332 | $table = $this->appendToResultTable( $table, $entityId, $result ); |
333 | } |
334 | |
335 | return $table->toHtml(); |
336 | } |
337 | |
338 | private function appendToResultTable( |
339 | HtmlTableBuilder $table, |
340 | EntityId $entityId, |
341 | CheckResult $result |
342 | ): HtmlTableBuilder { |
343 | $message = $result->getMessage(); |
344 | if ( $message === null ) { |
345 | // no row for this result |
346 | return $table; |
347 | } |
348 | |
349 | // Status column |
350 | $statusColumn = $this->formatStatus( $result->getStatus() ); |
351 | |
352 | // Property column |
353 | $propertyId = new NumericPropertyId( $result->getContextCursor()->getSnakPropertyId() ); |
354 | $propertyColumn = $this->getClaimLink( |
355 | $entityId, |
356 | $propertyId, |
357 | $this->entityIdLabelFormatter->formatEntityId( $propertyId ) |
358 | ); |
359 | |
360 | // Message column |
361 | $messageColumn = $this->violationMessageRenderer->render( $message ); |
362 | |
363 | // Constraint column |
364 | $constraintTypeItemId = $result->getConstraint()->getConstraintTypeItemId(); |
365 | try { |
366 | $constraintTypeLabel = $this->entityIdLabelFormatter->formatEntityId( new ItemId( $constraintTypeItemId ) ); |
367 | } catch ( InvalidArgumentException $e ) { |
368 | $constraintTypeLabel = htmlspecialchars( $constraintTypeItemId ); |
369 | } |
370 | $constraintColumn = $this->getClaimLink( |
371 | $propertyId, |
372 | new NumericPropertyId( $this->config->get( 'WBQualityConstraintsPropertyConstraintId' ) ), |
373 | $constraintTypeLabel |
374 | ); |
375 | |
376 | // Append cells |
377 | $table->appendRow( |
378 | [ |
379 | new HtmlTableCellBuilder( |
380 | new HtmlArmor( $statusColumn ) |
381 | ), |
382 | new HtmlTableCellBuilder( |
383 | new HtmlArmor( $propertyColumn ) |
384 | ), |
385 | new HtmlTableCellBuilder( |
386 | new HtmlArmor( $messageColumn ) |
387 | ), |
388 | new HtmlTableCellBuilder( |
389 | new HtmlArmor( $constraintColumn ) |
390 | ), |
391 | ] |
392 | ); |
393 | |
394 | return $table; |
395 | } |
396 | |
397 | /** |
398 | * Returns html text of the result header |
399 | * |
400 | * @param EntityId $entityId |
401 | * |
402 | * @return string HTML |
403 | */ |
404 | protected function buildResultHeader( EntityId $entityId ): string { |
405 | $entityLink = sprintf( '%s (%s)', |
406 | $this->entityIdLinkFormatter->formatEntityId( $entityId ), |
407 | htmlspecialchars( $entityId->getSerialization() ) ); |
408 | |
409 | return Html::rawElement( |
410 | 'h3', |
411 | [], |
412 | sprintf( '%s %s', $this->msg( 'wbqc-constraintreport-result-headline' )->escaped(), $entityLink ) |
413 | ); |
414 | } |
415 | |
416 | /** |
417 | * Builds summary from given results |
418 | * |
419 | * @param CheckResult[] $results |
420 | * |
421 | * @return string HTML |
422 | */ |
423 | protected function buildSummary( array $results ): string { |
424 | $statuses = []; |
425 | foreach ( $results as $result ) { |
426 | $status = strtolower( $result->getStatus() ); |
427 | $statuses[$status] = isset( $statuses[$status] ) ? $statuses[$status] + 1 : 1; |
428 | } |
429 | |
430 | $statusElements = []; |
431 | foreach ( $statuses as $status => $count ) { |
432 | if ( $count > 0 ) { |
433 | $statusElements[] = |
434 | $this->formatStatus( $status ) |
435 | . ': ' |
436 | . $count; |
437 | } |
438 | } |
439 | |
440 | return Html::rawElement( 'p', [], implode( ', ', $statusElements ) ); |
441 | } |
442 | |
443 | /** |
444 | * Formats given status to html |
445 | * |
446 | * @param string $status |
447 | * |
448 | * @throws InvalidArgumentException |
449 | * |
450 | * @return string HTML |
451 | */ |
452 | private function formatStatus( string $status ): string { |
453 | $messageName = "wbqc-constraintreport-status-" . strtolower( $status ); |
454 | $statusIcons = [ |
455 | CheckResult::STATUS_SUGGESTION => [ |
456 | 'icon' => 'suggestion-constraint-violation', |
457 | ], |
458 | CheckResult::STATUS_WARNING => [ |
459 | 'icon' => 'non-mandatory-constraint-violation', |
460 | ], |
461 | CheckResult::STATUS_VIOLATION => [ |
462 | 'icon' => 'mandatory-constraint-violation', |
463 | ], |
464 | CheckResult::STATUS_BAD_PARAMETERS => [ |
465 | 'icon' => 'alert', |
466 | 'flags' => 'warning', |
467 | ], |
468 | ]; |
469 | |
470 | if ( array_key_exists( $status, $statusIcons ) ) { |
471 | $iconWidget = new IconWidget( $statusIcons[$status] ); |
472 | $iconHtml = $iconWidget->toString() . ' '; |
473 | } else { |
474 | $iconHtml = ''; |
475 | } |
476 | |
477 | $labelWidget = new LabelWidget( [ |
478 | 'label' => $this->msg( $messageName )->text(), |
479 | ] ); |
480 | $labelHtml = $labelWidget->toString(); |
481 | |
482 | $formattedStatus = |
483 | Html::rawElement( |
484 | 'span', |
485 | [ |
486 | 'class' => 'wbqc-status wbqc-status-' . $status, |
487 | ], |
488 | $iconHtml . $labelHtml |
489 | ); |
490 | |
491 | return $formattedStatus; |
492 | } |
493 | |
494 | /** |
495 | * Returns html link to given entity with anchor to specified property. |
496 | * |
497 | * @param EntityId $entityId |
498 | * @param NumericPropertyId $propertyId |
499 | * @param string $text HTML |
500 | * |
501 | * @return string HTML |
502 | */ |
503 | private function getClaimLink( |
504 | EntityId $entityId, |
505 | NumericPropertyId $propertyId, |
506 | string $text |
507 | ): string { |
508 | return Html::rawElement( |
509 | 'a', |
510 | [ |
511 | 'href' => $this->getClaimUrl( $entityId, $propertyId ), |
512 | 'target' => '_blank', |
513 | ], |
514 | $text |
515 | ); |
516 | } |
517 | |
518 | /** |
519 | * Returns url of given entity with anchor to specified property. |
520 | */ |
521 | private function getClaimUrl( |
522 | EntityId $entityId, |
523 | NumericPropertyId $propertyId |
524 | ): string { |
525 | $title = $this->entityTitleLookup->getTitleForId( $entityId ); |
526 | $entityUrl = sprintf( '%s#%s', $title->getLocalURL(), $propertyId->getSerialization() ); |
527 | |
528 | return $entityUrl; |
529 | } |
530 | |
531 | } |