Completed
Push — master ( f2f357...ccb861 )
by
unknown
02:21
created

SpecialConstraintReport::buildSummary()   A

Complexity

Conditions 5
Paths 9

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.3222
c 0
b 0
f 0
cc 5
nc 9
nop 1
1
<?php
2
3
namespace WikibaseQuality\ConstraintReport\Specials;
4
5
use Config;
6
use DataValues\DataValue;
7
use Html;
8
use HTMLForm;
9
use IBufferingStatsdDataFactory;
10
use InvalidArgumentException;
11
use MediaWiki\MediaWikiServices;
12
use OOUI\IconWidget;
13
use OOUI\LabelWidget;
14
use SpecialPage;
15
use UnexpectedValueException;
16
use ValueFormatters\FormatterOptions;
17
use ValueFormatters\ValueFormatter;
18
use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\MultilingualTextViolationMessageRenderer;
19
use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessageRenderer;
20
use WikibaseQuality\ConstraintReport\ConstraintParameterRenderer;
21
use Wikibase\DataModel\Entity\EntityId;
22
use Wikibase\DataModel\Entity\EntityIdParser;
23
use Wikibase\DataModel\Entity\EntityIdParsingException;
24
use Wikibase\DataModel\Entity\EntityIdValue;
25
use Wikibase\DataModel\Entity\ItemId;
26
use Wikibase\DataModel\Entity\PropertyId;
27
use Wikibase\DataModel\Services\EntityId\EntityIdFormatter;
28
use Wikibase\DataModel\Services\Lookup\EntityLookup;
29
use Wikibase\Lib\OutputFormatValueFormatterFactory;
30
use Wikibase\Lib\SnakFormatter;
31
use Wikibase\Lib\Store\EntityTitleLookup;
32
use Wikibase\Lib\Store\LanguageFallbackLabelDescriptionLookupFactory;
33
use Wikibase\Repo\EntityIdHtmlLinkFormatterFactory;
34
use Wikibase\Repo\EntityIdLabelFormatterFactory;
35
use Wikibase\Repo\WikibaseRepo;
36
use WikibaseQuality\ConstraintReport\ConstraintCheck\DelegatingConstraintChecker;
37
use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult;
38
use WikibaseQuality\ConstraintReport\ConstraintReportFactory;
39
use WikibaseQuality\Html\HtmlTableBuilder;
40
use WikibaseQuality\Html\HtmlTableCellBuilder;
41
use WikibaseQuality\Html\HtmlTableHeaderBuilder;
42
43
/**
44
 * Special page that displays all constraints that are defined on an Entity with additional information
45
 * (whether it complied or was a violation, which parameters the constraint has etc.).
46
 *
47
 * @author BP2014N1
48
 * @license GPL-2.0-or-later
49
 */
50
class SpecialConstraintReport extends SpecialPage {
51
52
	/**
53
	 * @var EntityIdParser
54
	 */
55
	private $entityIdParser;
56
57
	/**
58
	 * @var EntityLookup
59
	 */
60
	private $entityLookup;
61
62
	/**
63
	 * @var EntityTitleLookup
64
	 */
65
	private $entityTitleLookup;
66
67
	/**
68
	 * @var ValueFormatter
69
	 */
70
	private $dataValueFormatter;
71
72
	/**
73
	 * @var EntityIdFormatter
74
	 */
75
	private $entityIdLabelFormatter;
76
77
	/**
78
	 * @var EntityIdFormatter
79
	 */
80
	private $entityIdLinkFormatter;
81
82
	/**
83
	 * @var DelegatingConstraintChecker
84
	 */
85
	private $constraintChecker;
86
87
	/**
88
	 * @var ConstraintParameterRenderer
89
	 */
90
	private $constraintParameterRenderer;
91
92
	/**
93
	 * @var ViolationMessageRenderer
94
	 */
95
	private $violationMessageRenderer;
96
97
	/**
98
	 * @var Config
99
	 */
100
	private $config;
101
102
	/**
103
	 * @var IBufferingStatsdDataFactory
104
	 */
105
	private $dataFactory;
106
107
	public static function newFromGlobalState() {
108
		$constraintReportFactory = ConstraintReportFactory::getDefaultInstance();
109
		$wikibaseRepo = WikibaseRepo::getDefaultInstance();
110
		$config = MediaWikiServices::getInstance()->getMainConfig();
111
		$dataFactory = MediaWikiServices::getInstance()->getStatsdDataFactory();
112
113
		return new self(
114
			$wikibaseRepo->getEntityLookup(),
115
			$wikibaseRepo->getEntityTitleLookup(),
116
			new EntityIdLabelFormatterFactory(),
117
			$wikibaseRepo->getEntityIdHtmlLinkFormatterFactory(),
118
			$wikibaseRepo->getLanguageFallbackLabelDescriptionLookupFactory(),
119
			$wikibaseRepo->getEntityIdParser(),
120
			$wikibaseRepo->getValueFormatterFactory(),
121
			$constraintReportFactory->getConstraintChecker(),
122
			$config,
123
			$dataFactory
124
		);
125
	}
126
127
	public function __construct(
128
		EntityLookup $entityLookup,
129
		EntityTitleLookup $entityTitleLookup,
130
		EntityIdLabelFormatterFactory $entityIdLabelFormatterFactory,
131
		EntityIdHtmlLinkFormatterFactory $entityIdHtmlLinkFormatterFactory,
132
		LanguageFallbackLabelDescriptionLookupFactory $fallbackLabelDescLookupFactory,
133
		EntityIdParser $entityIdParser,
134
		OutputFormatValueFormatterFactory $valueFormatterFactory,
135
		DelegatingConstraintChecker $constraintChecker,
136
		Config $config,
137
		IBufferingStatsdDataFactory $dataFactory
138
	) {
139
		parent::__construct( 'ConstraintReport' );
140
141
		$this->entityLookup = $entityLookup;
142
		$this->entityTitleLookup = $entityTitleLookup;
143
		$this->entityIdParser = $entityIdParser;
144
145
		$language = $this->getLanguage();
146
147
		$formatterOptions = new FormatterOptions();
148
		$formatterOptions->setOption( SnakFormatter::OPT_LANG, $language->getCode() );
149
		$this->dataValueFormatter = $valueFormatterFactory->getValueFormatter(
150
			SnakFormatter::FORMAT_HTML,
151
			$formatterOptions
152
		);
153
154
		$labelLookup = $fallbackLabelDescLookupFactory->newLabelDescriptionLookup( $language );
155
156
		$this->entityIdLabelFormatter = $entityIdLabelFormatterFactory->getEntityIdFormatter(
157
			$labelLookup
158
		);
159
160
		$this->entityIdLinkFormatter = $entityIdHtmlLinkFormatterFactory->getEntityIdFormatter(
161
			$labelLookup
162
		);
163
164
		$this->constraintChecker = $constraintChecker;
165
166
		$this->constraintParameterRenderer = new ConstraintParameterRenderer(
167
			$this->entityIdLabelFormatter,
168
			$this->dataValueFormatter,
169
			$this->getContext(),
170
			$config
171
		);
172
		$this->violationMessageRenderer = new MultilingualTextViolationMessageRenderer(
173
			$this->entityIdLinkFormatter,
174
			$this->dataValueFormatter,
175
			$this->getContext(),
176
			$config
177
		);
178
179
		$this->config = $config;
180
		$this->dataFactory = $dataFactory;
181
	}
182
183
	/**
184
	 * Returns array of modules that should be added
185
	 *
186
	 * @return array
187
	 */
188
	private function getModules() {
189
		return [
190
			'SpecialConstraintReportPage',
191
			'wikibase.quality.constraints.icon',
192
		];
193
	}
194
195
	/**
196
	 * @see SpecialPage::getGroupName
197
	 *
198
	 * @return string
199
	 */
200
	protected function getGroupName() {
201
		return 'wikibasequality';
202
	}
203
204
	/**
205
	 * @see SpecialPage::getDescription
206
	 *
207
	 * @return string
208
	 */
209
	public function getDescription() {
210
		return $this->msg( 'wbqc-constraintreport' )->escaped();
211
	}
212
213
	/**
214
	 * @see SpecialPage::execute
215
	 *
216
	 * @param string|null $subPage
217
	 *
218
	 * @throws InvalidArgumentException
219
	 * @throws EntityIdParsingException
220
	 * @throws UnexpectedValueException
221
	 */
222
	public function execute( $subPage ) {
223
		$out = $this->getOutput();
224
225
		$postRequest = $this->getContext()->getRequest()->getVal( 'entityid' );
226
		if ( $postRequest ) {
227
			$out->redirect( $this->getPageTitle( strtoupper( $postRequest ) )->getLocalURL() );
228
			return;
229
		}
230
231
		$out->enableOOUI();
232
		$out->addModules( $this->getModules() );
233
234
		$this->setHeaders();
235
236
		$out->addHTML( $this->getExplanationText() );
237
		$this->buildEntityIdForm();
238
239
		if ( !$subPage ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $subPage of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
240
			return;
241
		}
242
243
		if ( !is_string( $subPage ) ) {
244
			throw new InvalidArgumentException( '$subPage must be string.' );
245
		}
246
247
		try {
248
			$entityId = $this->entityIdParser->parse( $subPage );
249
		} catch ( EntityIdParsingException $e ) {
250
			$out->addHTML(
251
				$this->buildNotice( 'wbqc-constraintreport-invalid-entity-id', true )
252
			);
253
			return;
254
		}
255
256
		if ( !$this->entityLookup->hasEntity( $entityId ) ) {
257
			$out->addHTML(
258
				$this->buildNotice( 'wbqc-constraintreport-not-existent-entity', true )
259
			);
260
			return;
261
		}
262
263
		$this->dataFactory->increment(
264
			'wikibase.quality.constraints.specials.specialConstraintReport.executeCheck'
265
		);
266
		$results = $this->constraintChecker->checkAgainstConstraintsOnEntityId( $entityId );
267
268
		if ( $results !== [] ) {
269
			$out->addHTML(
270
				$this->buildResultHeader( $entityId )
271
				. $this->buildSummary( $results )
272
				. $this->buildResultTable( $entityId, $results )
273
			);
274
		} else {
275
			$out->addHTML(
276
				$this->buildResultHeader( $entityId )
277
				. $this->buildNotice( 'wbqc-constraintreport-empty-result' )
278
			);
279
		}
280
	}
281
282
	/**
283
	 * Builds html form for entity id input
284
	 */
285
	private function buildEntityIdForm() {
286
		$formDescriptor = [
287
			'entityid' => [
288
				'class' => 'HTMLTextField',
289
				'section' => 'section',
290
				'name' => 'entityid',
291
				'label-message' => 'wbqc-constraintreport-form-entityid-label',
292
				'cssclass' => 'wbqc-constraintreport-form-entity-id',
293
				'placeholder' => $this->msg( 'wbqc-constraintreport-form-entityid-placeholder' )->escaped()
294
			]
295
		];
296
		$htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext(), 'wbqc-constraintreport-form' );
297
		$htmlForm->setSubmitText( $this->msg( 'wbqc-constraintreport-form-submit-label' )->escaped() );
298
		$htmlForm->setSubmitCallback( function() {
299
			return false;
300
		} );
301
		$htmlForm->setMethod( 'post' );
302
		$htmlForm->show();
303
	}
304
305
	/**
306
	 * Builds notice with given message. Optionally notice can be handles as error by settings $error to true
307
	 *
308
	 * @param string $messageKey
309
	 * @param bool $error
310
	 *
311
	 * @throws InvalidArgumentException
312
	 *
313
	 * @return string HTML
314
	 */
315
	private function buildNotice( $messageKey, $error = false ) {
316
		if ( !is_string( $messageKey ) ) {
317
			throw new InvalidArgumentException( '$message must be string.' );
318
		}
319
		if ( !is_bool( $error ) ) {
320
			throw new InvalidArgumentException( '$error must be bool.' );
321
		}
322
323
		$cssClasses = 'wbqc-constraintreport-notice';
324
		if ( $error ) {
325
			$cssClasses .= ' wbqc-constraintreport-notice-error';
326
		}
327
328
		return Html::rawElement(
329
				'p',
330
				[
331
					'class' => $cssClasses
332
				],
333
				$this->msg( $messageKey )->escaped()
334
			);
335
	}
336
337
	/**
338
	 * @return string HTML
339
	 */
340
	private function getExplanationText() {
341
		return Html::rawElement(
342
			'div',
343
			[ 'class' => 'wbqc-explanation' ],
344
			Html::rawElement(
345
				'p',
346
				[],
347
				$this->msg( 'wbqc-constraintreport-explanation-part-one' )->escaped()
348
			)
349
			. Html::rawElement(
350
				'p',
351
				[],
352
				$this->msg( 'wbqc-constraintreport-explanation-part-two' )->escaped()
353
			)
354
		);
355
	}
356
357
	/**
358
	 * @param EntityId $entityId
359
	 * @param CheckResult[] $results
360
	 *
361
	 * @return string HTML
362
	 */
363
	private function buildResultTable( EntityId $entityId, array $results ) {
364
		// Set table headers
365
		$table = new HtmlTableBuilder(
366
			[
367
				new HtmlTableHeaderBuilder(
368
					$this->msg( 'wbqc-constraintreport-result-table-header-status' )->escaped(),
369
					true
370
				),
371
				new HtmlTableHeaderBuilder(
372
					$this->msg( 'wbqc-constraintreport-result-table-header-property' )->escaped(),
373
					true
374
				),
375
				new HtmlTableHeaderBuilder(
376
					$this->msg( 'wbqc-constraintreport-result-table-header-message' )->escaped(),
377
					true
378
				),
379
				new HtmlTableHeaderBuilder(
380
					$this->msg( 'wbqc-constraintreport-result-table-header-constraint' )->escaped(),
381
					true
382
				)
383
			]
384
		);
385
386
		foreach ( $results as $result ) {
387
			$table = $this->appendToResultTable( $table, $entityId, $result );
388
		}
389
390
		return $table->toHtml();
391
	}
392
393
	private function appendToResultTable( HtmlTableBuilder $table, EntityId $entityId, CheckResult $result ) {
394
		$message = $result->getMessage();
395
		if ( $message === null ) {
396
			// no row for this result
397
			return $table;
398
		}
399
400
		// Status column
401
		$statusColumn = $this->formatStatus( $result->getStatus() );
402
403
		// Property column
404
		$propertyId = new PropertyId( $result->getContextCursor()->getSnakPropertyId() );
405
		$propertyColumn = $this->getClaimLink(
406
			$entityId,
407
			$propertyId,
408
			$this->entityIdLabelFormatter->formatEntityId( $propertyId )
409
		);
410
411
		// Message column
412
		$messageColumn = $this->violationMessageRenderer->render( $message );
413
414
		// Constraint column
415
		$constraintTypeItemId = $result->getConstraint()->getConstraintTypeItemId();
416
		try {
417
			$constraintTypeLabel = $this->entityIdLabelFormatter->formatEntityId( new ItemId( $constraintTypeItemId ) );
418
		} catch ( InvalidArgumentException $e ) {
419
			$constraintTypeLabel = htmlspecialchars( $constraintTypeItemId );
420
		}
421
		$constraintLink = $this->getClaimLink(
422
			$propertyId,
423
			new PropertyId( $this->config->get( 'WBQualityConstraintsPropertyConstraintId' ) ),
424
			$constraintTypeLabel
425
		);
426
		$constraintColumn = $this->buildExpandableElement(
427
			$constraintLink,
428
			$this->constraintParameterRenderer->formatParameters( $result->getParameters() ),
0 ignored issues
show
Documentation introduced by
$result->getParameters() is of type array<integer,array>, but the function expects a array<integer,string|obj...Values\DataValue>>|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
429
			'[...]'
430
		);
431
432
		// Append cells
433
		$table->appendRow(
434
			[
435
				new HtmlTableCellBuilder(
436
					$statusColumn,
437
					[],
438
					true
439
				),
440
				new HtmlTableCellBuilder(
441
					$propertyColumn,
442
					[],
443
					true
444
				),
445
				new HtmlTableCellBuilder(
446
					$messageColumn,
447
					[],
448
					true
449
				),
450
				new HtmlTableCellBuilder(
451
					$constraintColumn,
452
					[],
453
					true
454
				)
455
			]
456
		);
457
458
		return $table;
459
	}
460
461
	/**
462
	 * Returns html text of the result header
463
	 *
464
	 * @param EntityId $entityId
465
	 *
466
	 * @return string HTML
467
	 */
468
	protected function buildResultHeader( EntityId $entityId ) {
469
		$entityLink = sprintf( '%s (%s)',
470
							   $this->entityIdLinkFormatter->formatEntityId( $entityId ),
471
							   htmlspecialchars( $entityId->getSerialization() ) );
472
473
		return Html::rawElement(
474
			'h3',
475
			[],
476
			sprintf( '%s %s', $this->msg( 'wbqc-constraintreport-result-headline' )->escaped(), $entityLink )
477
		);
478
	}
479
480
	/**
481
	 * Builds summary from given results
482
	 *
483
	 * @param CheckResult[] $results
484
	 *
485
	 * @return string HTML
486
	 */
487
	protected function buildSummary( array $results ) {
488
		$statuses = [];
489
		foreach ( $results as $result ) {
490
			$status = strtolower( $result->getStatus() );
491
			$statuses[$status] = isset( $statuses[$status] ) ? $statuses[$status] + 1 : 1;
492
		}
493
494
		$statusElements = [];
495
		foreach ( $statuses as $status => $count ) {
496
			if ( $count > 0 ) {
497
				$statusElements[] =
498
					$this->formatStatus( $status )
499
					. ': '
500
					. $count;
501
			}
502
		}
503
504
		return Html::rawElement( 'p', [], implode( ', ', $statusElements ) );
505
	}
506
507
	/**
508
	 * Builds a html div element with given content and a tooltip with given tooltip content
509
	 * If $tooltipContent is null, no tooltip will be created
510
	 *
511
	 * @param string $content
512
	 * @param string $expandableContent
513
	 * @param string $indicator
514
	 *
515
	 * @throws InvalidArgumentException
516
	 *
517
	 * @return string HTML
518
	 */
519
	protected function buildExpandableElement( $content, $expandableContent, $indicator ) {
520
		if ( !is_string( $content ) ) {
521
			throw new InvalidArgumentException( '$content has to be string.' );
522
		}
523
		if ( $expandableContent && ( !is_string( $expandableContent ) ) ) {
524
			throw new InvalidArgumentException( '$tooltipContent, if provided, has to be string.' );
525
		}
526
527
		if ( empty( $expandableContent ) ) {
528
			return $content;
529
		}
530
531
		$tooltipIndicator = Html::element(
532
			'span',
533
			[
534
				'class' => 'wbqc-expandable-content-indicator wbqc-indicator'
535
			],
536
			$indicator
537
		);
538
539
		$expandableContent = Html::element(
540
			'div',
541
			[
542
				'class' => 'wbqc-expandable-content'
543
			],
544
			$expandableContent
545
		);
546
547
		return sprintf( '%s %s %s', $content, $tooltipIndicator, $expandableContent );
548
	}
549
550
	/**
551
	 * Formats given status to html
552
	 *
553
	 * @param string $status
554
	 *
555
	 * @throws InvalidArgumentException
556
	 *
557
	 * @return string HTML
558
	 */
559
	private function formatStatus( $status ) {
560
		$messageName = "wbqc-constraintreport-status-" . strtolower( $status );
561
		$statusIcons = [
562
			CheckResult::STATUS_WARNING => [
563
				'icon' => 'non-mandatory-constraint-violation',
564
			],
565
			CheckResult::STATUS_VIOLATION => [
566
				'icon' => 'mandatory-constraint-violation',
567
			],
568
			CheckResult::STATUS_BAD_PARAMETERS => [
569
				'icon' => 'alert',
570
				'flags' => 'warning',
571
			],
572
		];
573
574
		if ( array_key_exists( $status, $statusIcons ) ) {
575
			$iconWidget = new IconWidget( $statusIcons[$status] );
576
			$iconHtml = $iconWidget->toString() . ' ';
577
		} else {
578
			$iconHtml = '';
579
		}
580
581
		$labelWidget = new LabelWidget( [
582
			'label' => $this->msg( $messageName )->text(),
583
		] );
584
		$labelHtml = $labelWidget->toString();
585
586
		$formattedStatus =
587
			Html::rawElement(
588
				'span',
589
				[
590
					'class' => 'wbqc-status wbqc-status-' . $status
591
				],
592
				$iconHtml . $labelHtml
593
			);
594
595
		return $formattedStatus;
596
	}
597
598
	/**
599
	 * Parses data values to human-readable string
600
	 *
601
	 * @param DataValue|array $dataValues
602
	 * @param string $separator
603
	 *
604
	 * @throws InvalidArgumentException
605
	 *
606
	 * @return string HTML
607
	 */
608
	protected function formatDataValues( $dataValues, $separator = ', ' ) {
609
		if ( $dataValues instanceof DataValue ) {
610
			$dataValues = [ $dataValues ];
611
		} elseif ( !is_array( $dataValues ) ) {
612
			throw new InvalidArgumentException( '$dataValues has to be instance of DataValue or an array of DataValues.' );
613
		}
614
615
		$formattedDataValues = [];
616
		foreach ( $dataValues as $dataValue ) {
617
			if ( !( $dataValue instanceof DataValue ) ) {
618
				throw new InvalidArgumentException( '$dataValues has to be instance of DataValue or an array of DataValues.' );
619
			}
620
			if ( $dataValue instanceof EntityIdValue ) {
621
				$formattedDataValues[ ] = $this->entityIdLabelFormatter->formatEntityId( $dataValue->getEntityId() );
622
			} else {
623
				$formattedDataValues[ ] = $this->dataValueFormatter->format( $dataValue );
624
			}
625
		}
626
627
		return implode( $separator, $formattedDataValues );
628
	}
629
630
	/**
631
	 * Returns html link to given entity with anchor to specified property.
632
	 *
633
	 * @param EntityId $entityId
634
	 * @param PropertyId $propertyId
635
	 * @param string $text HTML
636
	 *
637
	 * @return string HTML
638
	 */
639
	private function getClaimLink( EntityId $entityId, PropertyId $propertyId, $text ) {
640
		return Html::rawElement(
641
			'a',
642
			[
643
				'href' => $this->getClaimUrl( $entityId, $propertyId ),
644
				'target' => '_blank'
645
			],
646
			$text
647
		);
648
	}
649
650
	/**
651
	 * Returns url of given entity with anchor to specified property.
652
	 *
653
	 * @param EntityId $entityId
654
	 * @param PropertyId $propertyId
655
	 *
656
	 * @return string
657
	 */
658
	private function getClaimUrl( EntityId $entityId, PropertyId $propertyId ) {
659
		$title = $this->entityTitleLookup->getTitleForId( $entityId );
660
		$entityUrl = sprintf( '%s#%s', $title->getLocalURL(), $propertyId->getSerialization() );
661
662
		return $entityUrl;
663
	}
664
665
}
666