Completed
Push — master ( 82291d...b2711b )
by
unknown
09:34
created

SpecialCrossCheck::buildResultHeader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 14
rs 9.4285
cc 1
eloc 10
nc 1
nop 1
1
<?php
2
3
namespace WikibaseQuality\ExternalValidation\Specials;
4
5
use DataValues\DataValue;
6
use InvalidArgumentException;
7
use UnexpectedValueException;
8
use Html;
9
use HTMLForm;
10
use Linker;
11
use SpecialPage;
12
use ValueFormatters\FormatterOptions;
13
use ValueFormatters\ValueFormatter;
14
use Wikibase\DataModel\Entity\EntityDocument;
15
use Wikibase\DataModel\Entity\EntityId;
16
use Wikibase\DataModel\Entity\EntityIdParser;
17
use Wikibase\DataModel\Entity\EntityIdParsingException;
18
use Wikibase\DataModel\Entity\EntityIdValue;
19
use Wikibase\DataModel\Services\EntityId\EntityIdFormatter;
20
use Wikibase\DataModel\Services\Lookup\EntityLookup;
21
use Wikibase\DataModel\Services\Lookup\LanguageLabelDescriptionLookup;
22
use Wikibase\DataModel\Services\Lookup\TermLookup;
23
use Wikibase\DataModel\Statement\StatementListProvider;
24
use Wikibase\Lib\OutputFormatValueFormatterFactory;
25
use Wikibase\Lib\SnakFormatter;
26
use Wikibase\Repo\EntityIdHtmlLinkFormatterFactory;
27
use Wikibase\Repo\EntityIdLabelFormatterFactory;
28
use Wikibase\Repo\WikibaseRepo;
29
use WikibaseQuality\ExternalValidation\CrossCheck\CrossCheckInteractor;
30
use WikibaseQuality\ExternalValidation\CrossCheck\Result\CrossCheckResult;
31
use WikibaseQuality\ExternalValidation\ExternalValidationServices;
32
use WikibaseQuality\Html\HtmlTableBuilder;
33
use WikibaseQuality\Html\HtmlTableCellBuilder;
34
use WikibaseQuality\Html\HtmlTableHeaderBuilder;
35
36
class SpecialCrossCheck extends SpecialPage {
37
38
	/**
39
	 * @var EntityIdParser
40
	 */
41
	private $entityIdParser;
42
43
	/**
44
	 * @var EntityLookup
45
	 */
46
	private $entityLookup;
47
48
	/**
49
	 * @var ValueFormatter
50
	 */
51
	private $dataValueFormatter;
52
53
	/**
54
	 * @var EntityIdFormatter
55
	 */
56
	private $entityIdLabelFormatter;
57
58
	/**
59
	 * @var EntityIdFormatter
60
	 */
61
	private $entityIdLinkFormatter;
62
63
	/**
64
	 * @var CrossCheckInteractor
65
	 */
66
	private $crossCheckInteractor;
67
68
	/**
69
	 * Creates new instance from global state.
70
	 * @return self
71
	 */
72
	public static function newFromGlobalState() {
73
		$repo = WikibaseRepo::getDefaultInstance();
74
		$externalValidationServices = ExternalValidationServices::getDefaultInstance();
75
76
		return new self(
77
			$repo->getEntityLookup(),
78
			$repo->getTermLookup(),
79
			new EntityIdLabelFormatterFactory(),
80
			$repo->getEntityIdHtmlLinkFormatterFactory(),
81
			$repo->getEntityIdParser(),
82
			$repo->getValueFormatterFactory(),
83
			$externalValidationServices->getCrossCheckInteractor()
84
		);
85
	}
86
87
	/**
88
	 * @param EntityLookup $entityLookup
89
	 * @param TermLookup $termLookup
90
	 * @param EntityIdLabelFormatterFactory $entityIdLabelFormatterFactory
91
	 * @param EntityIdHtmlLinkFormatterFactory $entityIdHtmlLinkFormatterFactory
92
	 * @param EntityIdParser $entityIdParser
93
	 * @param OutputFormatValueFormatterFactory $valueFormatterFactory
94
	 * @param CrossCheckInteractor $crossCheckInteractor
95
	 */
96
	public function __construct(
97
		EntityLookup $entityLookup,
98
		TermLookup $termLookup,
99
		EntityIdLabelFormatterFactory $entityIdLabelFormatterFactory,
100
		EntityIdHtmlLinkFormatterFactory $entityIdHtmlLinkFormatterFactory,
101
		EntityIdParser $entityIdParser,
102
		OutputFormatValueFormatterFactory $valueFormatterFactory,
103
		CrossCheckInteractor $crossCheckInteractor
104
	) {
105
		parent::__construct( 'CrossCheck' );
106
107
		$this->entityLookup = $entityLookup;
108
		$this->entityIdParser = $entityIdParser;
109
110
		$formatterOptions = new FormatterOptions();
111
		$formatterOptions->setOption( SnakFormatter::OPT_LANG, $this->getLanguage()->getCode() );
112
		$this->dataValueFormatter = $valueFormatterFactory->getValueFormatter( SnakFormatter::FORMAT_HTML, $formatterOptions );
113
114
		$labelLookup = new LanguageLabelDescriptionLookup( $termLookup, $this->getLanguage()->getCode() );
115
		$this->entityIdLabelFormatter = $entityIdLabelFormatterFactory->getEntityIdFormatter( $labelLookup );
116
		$this->entityIdLinkFormatter = $entityIdHtmlLinkFormatterFactory->getEntityIdFormatter( $labelLookup );
117
118
		$this->crossCheckInteractor = $crossCheckInteractor;
119
	}
120
121
	/**
122
	 * @see SpecialPage::getGroupName
123
	 *
124
	 * @return string
125
	 */
126
	public function getGroupName() {
127
		return 'wikibasequality';
128
	}
129
130
	/**
131
	 * @see SpecialPage::getDescription
132
	 *
133
	 * @return string (plain text)
134
	 */
135
	public function getDescription() {
136
		return $this->msg( 'wbqev-crosscheck' )->text();
137
	}
138
139
	/**
140
	 * @see SpecialPage::execute
141
	 *
142
	 * @param string|null $subPage
143
	 *
144
	 * @throws InvalidArgumentException
145
	 * @throws EntityIdParsingException
146
	 * @throws UnexpectedValueException
147
	 */
148
	public function execute( $subPage ) {
149
		$out = $this->getOutput();
150
		$postRequest = $this->getContext()->getRequest()->getVal( 'entityid' );
151
		if ( $postRequest ) {
152
			$out->redirect( $this->getPageTitle( strtoupper( $postRequest ) )->getLocalURL() );
153
			return;
154
		}
155
156
		$out->addModules( 'SpecialCrossCheckPage' );
157
158
		$this->setHeaders();
159
160
		$out->addHTML( $this->buildInfoBox() );
161
		$this->buildEntityIdForm();
162
163
		if ( $subPage ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $subPage of type string|null is loosely compared to true; 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...
164
			$this->buildResult( $subPage );
165
		}
166
	}
167
168
	/**
169
	 * @param string $idSerialization
170
	 */
171
	private function buildResult( $idSerialization ) {
172
		$out = $this->getOutput();
173
174
		try {
175
			$entityId = $this->entityIdParser->parse( $idSerialization );
176
		} catch ( EntityIdParsingException $ex ) {
177
			$out->addHTML( $this->buildNotice( 'wbqev-crosscheck-invalid-entity-id', true ) );
178
			return;
179
		}
180
181
		$out->addHTML( $this->buildResultHeader( $entityId ) );
182
183
		$entity = $this->entityLookup->getEntity( $entityId );
184
		if ( $entity === null ) {
185
			$out->addHTML( $this->buildNotice( 'wbqev-crosscheck-not-existent-entity', true ) );
186
			return;
187
		}
188
189
		$results = $this->getCrossCheckResultsFromEntity( $entity );
190
191
		if ( $results === null || $results->toArray() === array() ) {
192
			$out->addHTML( $this->buildNotice( 'wbqev-crosscheck-empty-result' ) );
193
		} else {
194
			$out->addHTML(
195
				$this->buildSummary( $results )
196
				. $this->buildResultTable( $results )
197
			);
198
		}
199
	}
200
201
	/**
202
	 * @param EntityDocument $entity
203
	 *
204
	 * @return CrossCheckResultList|null
205
	 */
206
	private function getCrossCheckResultsFromEntity( EntityDocument $entity ) {
207
		if ( $entity instanceof StatementListProvider ) {
208
			return $this->crossCheckInteractor->crossCheckStatements( $entity->getStatements() );
209
		}
210
211
		return null;
212
	}
213
214
	/**
215
	 * Builds html form for entity id input
216
	 */
217
	private function buildEntityIdForm() {
218
		$formDescriptor = array(
219
			'entityid' => array(
220
				'class' => 'HTMLTextField',
221
				'section' => 'section',
222
				'name' => 'entityid',
223
				'label-message' => 'wbqev-crosscheck-form-entityid-label',
224
				'cssclass' => 'wbqev-crosscheck-form-entity-id',
225
				'placeholder' => $this->msg( 'wbqev-crosscheck-form-entityid-placeholder' )->escaped()
226
			)
227
		);
228
		$htmlForm = new HTMLForm( $formDescriptor, $this->getContext(), 'wbqev-crosscheck-form' );
229
		$htmlForm->setSubmitText( $this->msg( 'wbqev-crosscheck-form-submit-label' )->escaped() );
230
		$htmlForm->setSubmitCallback( function() {
231
			return false;
232
		} );
233
		$htmlForm->setMethod( 'post' );
234
		$htmlForm->show();
235
	}
236
237
	/**
238
	 * Builds infobox with explanation for this special page
239
	 *
240
	 * @return string HTML
241
	 */
242
	private function buildInfoBox() {
243
		$externalDbLink = Linker::specialLink( 'ExternalDbs', 'wbqev-externaldbs' );
244
		$infoBox =
245
			Html::openElement(
246
				'div',
247
				array( 'class' => 'wbqev-infobox' )
248
			)
249
			. $this->msg( 'wbqev-crosscheck-explanation-general' )->parse()
250
			. sprintf( ' %s.', $externalDbLink )
251
			. Html::element( 'br' )
252
			. Html::element( 'br' )
253
			. $this->msg( 'wbqev-crosscheck-explanation-detail' )->parse()
254
			. Html::closeElement( 'div' );
255
256
		return $infoBox;
257
	}
258
259
	/**
260
	 * Builds notice with given message. Optionally notice can be handles as error by settings $error to true
261
	 *
262
	 * @param string $messageKey
263
	 * @param bool $error
264
	 *
265
	 * @throws InvalidArgumentException
266
	 *
267
	 * @return string HTML
268
	 */
269
	private function buildNotice( $messageKey, $error = false ) {
270
		$cssClasses = 'wbqev-crosscheck-notice';
271
		if ( $error ) {
272
			$cssClasses .= ' wbqev-crosscheck-notice-error';
273
		}
274
275
		return
276
			Html::element(
277
				'p',
278
				array( 'class' => $cssClasses ),
279
				$this->msg( $messageKey )->text()
280
			);
281
	}
282
283
	/**
284
	 * Returns html text of the result header
285
	 *
286
	 * @param EntityId $entityId
287
	 *
288
	 * @return string HTML
289
	 */
290
	private function buildResultHeader( EntityId $entityId ) {
291
		$entityLink = sprintf(
292
			'%s (%s)',
293
			$this->entityIdLinkFormatter->formatEntityId( $entityId ),
294
			htmlspecialchars( $entityId->getSerialization() )
295
		);
296
297
		return
298
			Html::rawElement(
299
				'h3',
300
				array(),
301
				sprintf( '%s %s', $this->msg( 'wbqev-crosscheck-result-headline' )->escaped(), $entityLink )
302
			);
303
	}
304
305
	/**
306
	 * Builds summary from given results
307
	 *
308
	 * @param CrossCheckResult[] $results
309
	 *
310
	 * @return string HTML
311
	 */
312
	private function buildSummary( $results ) {
313
		$statuses = array();
314
		foreach ( $results as $result ) {
315
			$status = strtolower( $result->getComparisonResult()->getStatus() );
316
			if ( array_key_exists( $status, $statuses ) ) {
317
				$statuses[$status]++;
318
			} else {
319
				$statuses[$status] = 1;
320
			}
321
		}
322
323
		$statusElements = array();
324
		foreach ( $statuses as $status => $count ) {
325
			if ( $count > 0 ) {
326
				$statusElements[] = $this->formatStatus( $status ) . ': ' . $count;
327
			}
328
		}
329
		$summary =
330
			Html::openElement( 'p' )
331
			. implode( ', ', $statusElements )
332
			. Html::closeElement( 'p' );
333
334
		return $summary;
335
	}
336
337
	/**
338
	 * Formats given status to html
339
	 *
340
	 * @param string $status (plain text)
341
	 *
342
	 * @throws InvalidArgumentException
343
	 *
344
	 * @return string HTML
345
	 */
346
	private function formatStatus( $status ) {
347
		$messageKey = 'wbqev-crosscheck-status-' . strtolower( $status );
348
349
		$formattedStatus =
350
			Html::element(
351
				'span',
352
				array (
353
					'class' => 'wbqev-status wbqev-status-' . htmlspecialchars( $status )
354
				),
355
				$this->msg( $messageKey )->text()
356
			);
357
358
		return $formattedStatus;
359
	}
360
361
	/**
362
	 * Parses data values to human-readable string
363
	 *
364
	 * @param DataValue|array $dataValues
365
	 * @param bool $linking
366
	 * @param string $separator HTML
367
	 *
368
	 * @throws InvalidArgumentException
369
	 *
370
	 * @return string HTML
371
	 */
372
	private function formatDataValues( $dataValues, $linking = true, $separator = null ) {
373
		if ( $dataValues instanceof DataValue ) {
374
			$dataValues = array( $dataValues );
375
		}
376
377
		$formattedDataValues = array();
378
		foreach ( $dataValues as $dataValue ) {
379
			if ( $dataValue instanceof EntityIdValue ) {
380
				if ( $linking ) {
381
					$formattedDataValues[] = $this->entityIdLinkFormatter->formatEntityId( $dataValue->getEntityId() );
382
				} else {
383
					$formattedDataValues[] = $this->entityIdLabelFormatter->formatEntityId( $dataValue->getEntityId() );
384
				}
385
			} else {
386
				$formattedDataValues[] = $this->dataValueFormatter->format( $dataValue );
387
			}
388
		}
389
390
		if ( $separator ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $separator of type string|null is loosely compared to true; 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...
391
			return implode( $separator, $formattedDataValues );
392
		}
393
394
		return $this->getLanguage()->commaList( $formattedDataValues );
395
	}
396
397
	/**
398
	 * @param CrossCheckResult[] $results
399
	 *
400
	 * @return string HTML
401
	 */
402
	private function buildResultTable( $results ) {
403
		$table = new HtmlTableBuilder(
404
			array(
405
				new HtmlTableHeaderBuilder(
406
					$this->msg( 'wbqev-crosscheck-result-table-header-status' )->escaped(),
407
					true
408
				),
409
				new HtmlTableHeaderBuilder(
410
					$this->msg( 'datatypes-type-wikibase-property' )->escaped(),
411
					true
412
				),
413
				new HtmlTableHeaderBuilder(
414
					$this->msg( 'wbqev-crosscheck-result-table-header-local-value' )->escaped()
415
				),
416
				new HtmlTableHeaderBuilder(
417
					$this->msg( 'wbqev-crosscheck-result-table-header-external-value' )->escaped()
418
				),
419
				new HtmlTableHeaderBuilder(
420
					$this->msg( 'wbqev-crosscheck-result-table-header-references' )->escaped(),
421
					true
422
				),
423
				new HtmlTableHeaderBuilder(
424
					Linker::linkKnown(
425
						self::getTitleFor( 'ExternalDbs' ),
426
						$this->msg( 'wbqev-crosscheck-result-table-header-external-source' )->escaped()
427
					),
428
					true,
429
					true
430
				)
431
			),
432
			true
433
		);
434
435
		foreach ( $results as $result ) {
436
			$status = $this->formatStatus( $result->getComparisonResult()->getStatus() );
437
			$propertyId = $this->entityIdLinkFormatter->formatEntityId( $result->getPropertyId() );
438
			$localValue = $this->formatDataValues( $result->getComparisonResult()->getLocalValue() );
439
			$externalValue = $this->formatDataValues(
440
				$result->getComparisonResult()->getExternalValues(),
441
				true,
442
				Html::element( 'br' )
443
			);
444
			$referenceStatus = $this->msg(
445
				'wbqev-crosscheck-status-' . $result->getReferenceResult()->getStatus()
446
			)->text();
447
			$dataSource = $this->entityIdLinkFormatter->formatEntityId( $result->getDumpMetaInformation()->getSourceItemId() );
448
449
			$table->appendRow(
450
				array(
451
					new HtmlTableCellBuilder( $status, array(), true ),
452
					new HtmlTableCellBuilder( $propertyId, array(), true ),
453
					new HtmlTableCellBuilder( $localValue, array(), true ),
454
					new HtmlTableCellBuilder( $externalValue, array(), true ),
455
					new HtmlTableCellBuilder( $referenceStatus, array() ),
456
					new HtmlTableCellBuilder( $dataSource, array(), true )
457
				)
458
			);
459
		}
460
461
		return $table->toHtml();
462
	}
463
464
}
465