Completed
Push — master ( 7c8455...4fdd6e )
by
unknown
02:48
created

SpecialCrossCheck   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 429
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 10
Bugs 2 Features 1
Metric Value
wmc 33
c 10
b 2
f 1
lcom 1
cbo 11
dl 0
loc 429
rs 9.3999

15 Methods

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