Completed
Push — master ( 0dfa46...acf0fe )
by
unknown
02:06 queued 10s
created

SpecialConstraintReport::buildNotice()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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