EntityContent   F
last analyzed

Complexity

Total Complexity 88

Size/Duplication

Total Lines 650
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 19

Importance

Changes 0
Metric Value
wmc 88
lcom 1
cbo 19
dl 0
loc 650
rs 1.95
c 0
b 0
f 0

31 Methods

Rating   Name   Duplication   Size   Complexity  
A isValid() 0 12 3
A isCountable() 0 3 2
A getEntityRedirect() 0 7 2
getEntity() 0 1 ?
getEntityHolder() 0 1 ?
A getEntityId() 0 15 4
A getParserOutput() 0 23 4
A getParserOutputForRedirect() 0 21 2
A getParserOutputFromEntityView() 0 24 1
A getValidUserLanguage() 0 7 2
A getEntityRevision() 0 12 2
A getTextForSearchIndex() 0 14 3
A getRedirectText() 0 4 1
getIgnoreKeysForFilters() 0 1 ?
A getTextForFilters() 0 14 2
A getWikitextForTransclusion() 0 3 1
A getTextForSummary() 0 30 5
A getRedirectData() 0 14 3
A getNativeData() 0 13 3
A getSize() 0 3 1
C equals() 0 39 15
A makeEmptyEntity() 0 4 1
A getDiff() 0 21 5
B getPatchedCopy() 0 31 6
A getPatchedRedirect() 0 20 3
A isEmpty() 0 8 3
A copy() 0 15 3
A prepareSave() 0 17 4
A applyValidators() 0 15 3
A applyEntityPageProperties() 0 10 3
A getEntityPageProperties() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like EntityContent often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EntityContent, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Wikibase\Repo\Content;
4
5
use AbstractContent;
6
use Article;
7
use Content;
8
use Diff\Differ\MapDiffer;
9
use Diff\DiffOp\Diff\Diff;
10
use Diff\Patcher\MapPatcher;
11
use Diff\Patcher\PatcherException;
12
use Hooks;
13
use Language;
14
use LogicException;
15
use MediaWiki\MediaWikiServices;
16
use MWException;
17
use ParserOptions;
18
use ParserOutput;
19
use RequestContext;
20
use RuntimeException;
21
use Serializers\Exceptions\SerializationException;
22
use Status;
23
use Title;
24
use User;
25
use ValueValidators\Result;
26
use Wikibase\DataModel\Entity\EntityDocument;
27
use Wikibase\DataModel\Entity\EntityId;
28
use Wikibase\DataModel\Entity\EntityRedirect;
29
use Wikibase\DataModel\Term\LabelsProvider;
30
use Wikibase\Lib\Store\EntityRevision;
31
use Wikibase\Repo\ArrayValueCollector;
32
use Wikibase\Repo\FingerprintSearchTextGenerator;
33
use Wikibase\Repo\Validators\EntityValidator;
34
use Wikibase\Repo\WikibaseRepo;
35
use WikiPage;
36
37
/**
38
 * Abstract content object for articles representing Wikibase entities.
39
 *
40
 * For more information on the relationship between entities and wiki pages, see
41
 * docs/entity-storage.wiki.
42
 *
43
 * @license GPL-2.0-or-later
44
 * @author Jeroen De Dauw < [email protected] >
45
 * @author Daniel Kinzler
46
 * @author Bene* < [email protected] >
47
 *
48
 * @method \Wikibase\Repo\Content\EntityHandler getContentHandler()
49
 */
50
abstract class EntityContent extends AbstractContent {
51
52
	/**
53
	 * Flag for use with prepareSave(), indicating that no pre-save validation should be applied.
54
	 * Can be passed in via EditEntity::attemptSave, EntityStore::saveEntity,
55
	 * as well as WikiPage::doEditContent()
56
	 *
57
	 * @note: must not collide with the EDIT_XXX flags defined by MediaWiki core in Defines.php.
58
	 */
59
	const EDIT_IGNORE_CONSTRAINTS = 1024;
60
61
	/**
62
	 * @see Content::isValid()
63
	 *
64
	 * @return bool True if this content object is valid for saving. False if there is no entity, or
65
	 *  the entity does not have an ID set.
66
	 */
67
	public function isValid() {
68
		if ( $this->isRedirect() ) {
69
			// Under some circumstances, the handler will not support redirects,
70
			// but it's still possible to construct Content objects that represent
71
			// redirects. In such a case, make sure such Content objects are considered
72
			// invalid and do not get saved.
73
			return $this->getContentHandler()->supportsRedirects();
74
		}
75
76
		$holder = $this->getEntityHolder();
77
		return $holder !== null && $holder->getEntityId() !== null;
78
	}
79
80
	/**
81
	 * @see EntityContent::isCountable
82
	 *
83
	 * @param bool|null $hasLinks
84
	 *
85
	 * @return bool True if this is not a redirect and the item is not empty.
86
	 */
87
	public function isCountable( $hasLinks = null ) {
88
		return !$this->isRedirect() && !$this->isEmpty();
89
	}
90
91
	/**
92
	 * Returns the EntityRedirect represented by this EntityContent, or null if this
93
	 * EntityContent is not a redirect.
94
	 *
95
	 * @note This default implementation will fail if isRedirect() is true.
96
	 * Subclasses that support redirects must override getEntityRedirect().
97
	 *
98
	 * @throws LogicException
99
	 * @return EntityRedirect|null
100
	 */
101
	public function getEntityRedirect() {
102
		if ( $this->isRedirect() ) {
103
			throw new LogicException( 'EntityContent subclasses that support redirects must override getEntityRedirect()' );
104
		}
105
106
		return null;
107
	}
108
109
	/**
110
	 * Returns the entity contained by this entity content.
111
	 * Deriving classes typically have a more specific get method as
112
	 * for greater clarity and type hinting.
113
	 *
114
	 * @throws MWException when it's a redirect (targets will never be resolved)
115
	 * @throws LogicException if the content object is empty and does not contain an entity.
116
	 * @return EntityDocument
117
	 */
118
	abstract public function getEntity();
119
120
	/**
121
	 * Returns a holder for the entity contained in this EntityContent object.
122
	 *
123
	 * @throws MWException when it's a redirect (targets will never be resolved)
124
	 * @return EntityHolder|null
125
	 */
126
	abstract protected function getEntityHolder();
127
128
	/**
129
	 * @throws RuntimeException if the content object is empty or no entity ID is set
130
	 * @return EntityId
131
	 */
132
	public function getEntityId(): EntityId {
133
		if ( $this->isRedirect() ) {
134
			return $this->getEntityRedirect()->getEntityId();
135
		}
136
137
		$holder = $this->getEntityHolder();
138
		if ( $holder !== null ) {
139
			$id = $holder->getEntityId();
140
			if ( $id !== null ) {
141
				return $id;
142
			}
143
		}
144
145
		throw new RuntimeException( 'EntityContent was constructed without an EntityId!' );
146
	}
147
148
	/**
149
	 * Returns a ParserOutput object containing the HTML.
150
	 *
151
	 * @note this calls ParserOutput::recordOption( 'userlang' ) to split the cache
152
	 * by user language, and ParserOutput::recordOption( 'wb' ) to split the cache on
153
	 * EntityHandler::PARSER_VERSION.
154
	 *
155
	 * @see Content::getParserOutput
156
	 *
157
	 * @param Title $title
158
	 * @param int|null $revisionId
159
	 * @param ParserOptions|null $options
160
	 * @param bool $generateHtml
161
	 *
162
	 * @return ParserOutput
163
	 */
164
	public function getParserOutput(
165
		Title $title,
166
		$revisionId = null,
167
		ParserOptions $options = null,
168
		$generateHtml = true
169
	) {
170
		if ( $this->isRedirect() ) {
171
			return $this->getParserOutputForRedirect( $generateHtml );
172
		} else {
173
			if ( $options === null ) {
174
				$options = ParserOptions::newFromContext( RequestContext::getMain() );
175
			}
176
177
			$out = $this->getParserOutputFromEntityView( $revisionId, $options, $generateHtml );
178
179
			if ( !$options->getUserLangObj()->equals( RequestContext::getMain()->getLanguage() ) ) {
180
				// HACK: Don't save to parser cache if this is not in the user's lang: T199983.
181
				$out->updateCacheExpiry( 0 );
182
			}
183
184
			return $out;
185
		}
186
	}
187
188
	/**
189
	 * @note Will fail if this EntityContent does not represent a redirect.
190
	 *
191
	 * @param bool $generateHtml
192
	 *
193
	 * @return ParserOutput
194
	 */
195
	protected function getParserOutputForRedirect( $generateHtml ) {
196
		$output = new ParserOutput();
197
		$target = $this->getRedirectTarget();
198
199
		// Make sure to include the redirect link in pagelinks
200
		$output->addLink( $target );
201
202
		// Since the output depends on the user language, we must make sure
203
		// ParserCache::getKey() includes it in the cache key.
204
		$output->recordOption( 'userlang' );
205
		// And we need to include EntityHandler::PARSER_VERSION in the cache key too
206
		$output->recordOption( 'wb' );
207
		if ( $generateHtml ) {
208
			$chain = $this->getRedirectChain();
209
			$language = $this->getContentHandler()->getPageViewLanguage( $target );
210
			$html = Article::getRedirectHeaderHtml( $language, $chain, false );
211
			$output->setText( $html );
212
		}
213
214
		return $output;
215
	}
216
217
	/**
218
	 * @note Will fail if this EntityContent represents a redirect.
219
	 *
220
	 * @param int|null $revisionId
221
	 * @param ParserOptions $options
222
	 * @param bool $generateHtml
223
	 *
224
	 * @return ParserOutput
225
	 */
226
	protected function getParserOutputFromEntityView(
227
		$revisionId,
228
		ParserOptions $options,
229
		$generateHtml = true
230
	) {
231
		// @todo: move this to the ContentHandler
232
		$outputGenerator = WikibaseRepo::getDefaultInstance()->getEntityParserOutputGenerator(
233
			$this->getValidUserLanguage( $options->getUserLangObj() )
234
		);
235
236
		$entityRevision = $this->getEntityRevision( $revisionId );
237
238
		$output = $outputGenerator->getParserOutput( $entityRevision, $generateHtml );
239
240
		// Since the output depends on the user language, we must make sure
241
		// ParserCache::getKey() includes it in the cache key.
242
		$output->recordOption( 'userlang' );
243
		// And we need to include EntityHandler::PARSER_VERSION in the cache key too
244
		$output->recordOption( 'wb' );
245
246
		$this->applyEntityPageProperties( $output );
247
248
		return $output;
249
	}
250
251
	private function getValidUserLanguage( Language $language ) {
252
		if ( !Language::isValidBuiltInCode( $language->getCode() ) ) {
253
			return Language::factory( 'und' ); // T204791
254
		}
255
256
		return $language;
257
	}
258
259
	/**
260
	 * @param int|null $revisionId
261
	 *
262
	 * @return EntityRevision
263
	 */
264
	private function getEntityRevision( $revisionId = null ) {
265
		$entity = $this->getEntity();
266
267
		if ( $revisionId !== null ) {
268
			return new EntityRevision( $entity, $revisionId );
269
		}
270
271
		// Revision defaults to 0 (latest), which is desired and suitable in cases where
272
		// getParserOutput specifies no revision. (e.g. is called during save process
273
		// when revision id is unknown or not assigned yet)
274
		return new EntityRevision( $entity );
275
	}
276
277
	/**
278
	 * @return string A string representing the content in a way useful for building a full text
279
	 *         search index.
280
	 */
281
	public function getTextForSearchIndex() {
282
		if ( $this->isRedirect() ) {
283
			return '';
284
		}
285
286
		$searchTextGenerator = new FingerprintSearchTextGenerator();
287
		$text = $searchTextGenerator->generate( $this->getEntity() );
288
289
		if ( !Hooks::run( 'WikibaseTextForSearchIndex', [ $this, &$text ] ) ) {
290
			return '';
291
		}
292
293
		return $text;
294
	}
295
296
	/**
297
	 * @return string Returns the string representation of the redirect
298
	 * represented by this EntityContent (if any).
299
	 *
300
	 * @note Will fail if this EntityContent is not a redirect.
301
	 */
302
	protected function getRedirectText() {
303
		$target = $this->getRedirectTarget();
304
		return '#REDIRECT [[' . $target->getFullText() . ']]';
305
	}
306
307
	/**
308
	 * Get the keys within this Contents Entity JSON that should be removed for
309
	 * text passed to edit filters.
310
	 *
311
	 * @return string[] Keys to ignore
312
	 */
313
	abstract protected function getIgnoreKeysForFilters();
314
315
	/**
316
	 * @return string A string representing the content in a way useful for content filtering as
317
	 *         performed by extensions like AbuseFilter.
318
	 */
319
	public function getTextForFilters() {
320
		if ( $this->isRedirect() ) {
321
			return $this->getRedirectText();
322
		}
323
324
		// @todo this text for filters stuff should be it's own class with test coverage!
325
		$codec = WikibaseRepo::getDefaultInstance()->getEntityContentDataCodec();
326
		$json = $codec->encodeEntity( $this->getEntity(), CONTENT_FORMAT_JSON );
327
		$data = json_decode( $json, true );
328
329
		$values = ArrayValueCollector::collectValues( $data, $this->getIgnoreKeysForFilters() );
330
331
		return implode( "\n", $values );
332
	}
333
334
	/**
335
	 * @return string The wikitext to include when another page includes this  content, or false if
336
	 *         the content is not includable in a wikitext page.
337
	 */
338
	public function getWikitextForTransclusion() {
339
		return false;
340
	}
341
342
	/**
343
	 * Returns a textual representation of the content suitable for use in edit summaries and log messages.
344
	 *
345
	 * @param int $maxLength maximum length of the summary text
346
	 * @return string
347
	 * @throws MWException
348
	 */
349
	public function getTextForSummary( $maxLength = 250 ) {
350
		if ( $this->isRedirect() ) {
351
			return $this->getRedirectText();
352
		}
353
354
		$entity = $this->getEntity();
355
356
		// TODO: This assumes all entities not implementing their own getTextForSummary are LabelsProvider. Fix it.
357
		if ( !( $entity instanceof LabelsProvider ) ) {
358
			throw new LogicException(
359
				"Entity type {$entity->getType()} must implement its own getTextForSummary method."
360
			);
361
		}
362
363
		$labels = $entity->getLabels();
364
		if ( $labels->isEmpty() ) {
365
			return '';
366
		}
367
368
		$language = MediaWikiServices::getInstance()->getContentLanguage();
369
370
		if ( $labels->hasTermForLanguage( $language->getCode() ) ) {
371
			$label = $labels->getByLanguage( $language->getCode() )->getText();
372
			return $language->truncateForDatabase( $label, $maxLength );
373
		}
374
375
		// Return first term it can find
376
		$term = $labels->getIterator()->current();
377
		return $language->truncateForDatabase( $term->getText(), $maxLength );
378
	}
379
380
	/**
381
	 * Returns an array structure for the redirect represented by this EntityContent, if any.
382
	 *
383
	 * @note This may or may not be consistent with what EntityContentCodec does.
384
	 *       It it intended to be used primarily for diffing.
385
	 */
386
	private function getRedirectData() {
387
		// NOTE: keep in sync with getPatchedRedirect
388
		$data = [];
389
390
		if ( $this->isValid() ) {
391
			$data['entity'] = $this->getEntityId()->getSerialization();
392
		}
393
394
		if ( $this->isRedirect() ) {
395
			$data['redirect'] = $this->getEntityRedirect()->getTargetId()->getSerialization();
396
		}
397
398
		return $data;
399
	}
400
401
	/**
402
	 * @see Content::getNativeData
403
	 *
404
	 * @note Avoid relying on this method! It bypasses EntityContentCodec, and does
405
	 *       not make any guarantees about the structure of the array returned.
406
	 *
407
	 * @return array|EntityDocument An undefined data structure representing the content. This is
408
	 *  not guaranteed to conform to any serialization structure used in the database or externally.
409
	 */
410
	public function getNativeData() {
411
		if ( $this->isRedirect() ) {
412
			return $this->getRedirectData();
413
		}
414
415
		// NOTE: this may or may not be consistent with what EntityContentCodec does!
416
		$serializer = WikibaseRepo::getDefaultInstance()->getAllTypesEntitySerializer();
417
		try {
418
			return $serializer->serialize( $this->getEntity() );
419
		} catch ( SerializationException $ex ) {
420
			return $this->getEntity();
421
		}
422
	}
423
424
	/**
425
	 * returns the content's nominal size in bogo-bytes.
426
	 *
427
	 * @return int
428
	 */
429
	public function getSize() {
430
		return strlen( serialize( $this->getNativeData() ) );
431
	}
432
433
	/**
434
	 * Both contents will be considered equal if they have the same ID and equal Entity data. If
435
	 * one of the contents is considered "new", then matching IDs is not a criteria for them to be
436
	 * considered equal.
437
	 *
438
	 * @see Content::equals
439
	 *
440
	 * @param Content|null $that
441
	 *
442
	 * @return bool
443
	 */
444
	public function equals( Content $that = null ) {
445
		if ( $that === $this ) {
446
			return true;
447
		}
448
449
		if ( !( $that instanceof self ) || $that->getModel() !== $this->getModel() ) {
450
			return false;
451
		}
452
453
		$thisRedirect = $this->getRedirectTarget();
454
		$thatRedirect = $that->getRedirectTarget();
455
456
		if ( $thisRedirect !== null ) {
457
			if ( $thatRedirect === null ) {
458
				return false;
459
			} else {
460
				return $thisRedirect->equals( $thatRedirect )
461
					&& $this->getEntityRedirect()->equals( $that->getEntityRedirect() );
462
			}
463
		} elseif ( $thatRedirect !== null ) {
464
			return false;
465
		}
466
467
		$thisHolder = $this->getEntityHolder();
468
		$thatHolder = $that->getEntityHolder();
469
		if ( !$thisHolder && !$thatHolder ) {
470
			return true;
471
		} elseif ( !$thisHolder || !$thatHolder ) {
472
			return false;
473
		}
474
475
		$thisId = $thisHolder->getEntityId();
476
		$thatId = $thatHolder->getEntityId();
477
		if ( $thisId && $thatId && !$thisId->equals( $thatId ) ) {
478
			return false;
479
		}
480
481
		return $thisHolder->getEntity()->equals( $thatHolder->getEntity() );
482
	}
483
484
	/**
485
	 * @return EntityDocument
486
	 */
487
	private function makeEmptyEntity() {
488
		$handler = $this->getContentHandler();
489
		return $handler->makeEmptyEntity();
490
	}
491
492
	/**
493
	 * Returns a diff between this EntityContent and the given EntityContent.
494
	 *
495
	 * @param self $toContent
496
	 *
497
	 * @return EntityContentDiff
498
	 */
499
	public function getDiff( EntityContent $toContent ) {
500
		$fromContent = $this;
501
502
		$differ = new MapDiffer();
503
		$redirectDiffOps = $differ->doDiff(
504
			$fromContent->getRedirectData(),
505
			$toContent->getRedirectData()
506
		);
507
508
		$redirectDiff = new Diff( $redirectDiffOps, true );
509
510
		$fromEntity = ( $fromContent->isRedirect() || $fromContent->getEntityHolder() === null ) ?
511
			$this->makeEmptyEntity() : $fromContent->getEntity();
512
		$toEntity = ( $toContent->isRedirect() || $toContent->getEntityHolder() === null ) ?
513
			$this->makeEmptyEntity() : $toContent->getEntity();
514
515
		$entityDiffer = WikibaseRepo::getDefaultInstance()->getEntityDiffer();
516
		$entityDiff = $entityDiffer->diffEntities( $fromEntity, $toEntity );
517
518
		return new EntityContentDiff( $entityDiff, $redirectDiff, $fromEntity->getType() );
519
	}
520
521
	/**
522
	 * Returns a patched copy of this Content object.
523
	 *
524
	 * @param EntityContentDiff $patch
525
	 *
526
	 * @throws PatcherException
527
	 * @return self
528
	 */
529
	public function getPatchedCopy( EntityContentDiff $patch ) {
530
		$handler = $this->getContentHandler();
531
532
		if ( $this->isRedirect() ) {
533
			$entityAfterPatch = $this->makeEmptyEntity();
534
			$entityAfterPatch->setId( $this->getEntityId() );
535
		} else {
536
			$entityAfterPatch = $this->getEntity()->copy();
537
		}
538
539
		$patcher = WikibaseRepo::getDefaultInstance()->getEntityPatcher();
540
		$patcher->patchEntity( $entityAfterPatch, $patch->getEntityDiff() );
541
542
		$redirAfterPatch = $this->getPatchedRedirect( $patch->getRedirectDiff() );
543
544
		if ( $redirAfterPatch !== null && !$entityAfterPatch->isEmpty() ) {
545
			throw new PatcherException( 'EntityContent must not contain Entity data as well as'
546
				. ' a redirect after applying the patch!' );
547
		} elseif ( $redirAfterPatch ) {
548
			$patched = $handler->makeEntityRedirectContent( $redirAfterPatch );
549
550
			if ( !$patched ) {
551
				throw new PatcherException( 'Cannot create a redirect using content model '
552
					. $this->getModel() . '!' );
553
			}
554
		} else {
555
			$patched = $handler->makeEntityContent( new EntityInstanceHolder( $entityAfterPatch ) );
556
		}
557
558
		return $patched;
559
	}
560
561
	/**
562
	 * @param Diff $redirectPatch
563
	 *
564
	 * @return EntityRedirect|null
565
	 */
566
	private function getPatchedRedirect( Diff $redirectPatch ) {
567
		// See getRedirectData() for the structure of the data array.
568
		$redirData = $this->getRedirectData();
569
570
		if ( !$redirectPatch->isEmpty() ) {
571
			$patcher = new MapPatcher();
572
			$redirData = $patcher->patch( $redirData, $redirectPatch );
573
		}
574
575
		if ( isset( $redirData['redirect'] ) ) {
576
			$handler = $this->getContentHandler();
577
578
			$entityId = $this->getEntityId();
579
			$targetId = $handler->makeEntityId( $redirData['redirect'] );
580
581
			return new EntityRedirect( $entityId, $targetId );
582
		} else {
583
			return null;
584
		}
585
	}
586
587
	/**
588
	 * @return bool True if this is not a redirect and the page is empty.
589
	 */
590
	public function isEmpty() {
591
		if ( $this->isRedirect() ) {
592
			return false;
593
		}
594
595
		$holder = $this->getEntityHolder();
596
		return $holder === null || $holder->getEntity()->isEmpty();
597
	}
598
599
	/**
600
	 * @see Content::copy
601
	 *
602
	 * @return self
603
	 */
604
	public function copy() {
605
		$handler = $this->getContentHandler();
606
607
		if ( $this->isRedirect() ) {
608
			return $handler->makeEntityRedirectContent( $this->getEntityRedirect() );
609
		}
610
611
		$holder = $this->getEntityHolder();
612
		if ( $holder !== null ) {
613
			return $handler->makeEntityContent( new DeferredCopyEntityHolder( $holder ) );
614
		}
615
616
		// There is nothing mutable on an entirely empty content object.
617
		return $this;
618
	}
619
620
	/**
621
	 * @see Content::prepareSave
622
	 *
623
	 * @param WikiPage $page
624
	 * @param int $flags
625
	 * @param int $baseRevId
626
	 * @param User $user
627
	 *
628
	 * @return Status
629
	 */
630
	public function prepareSave( WikiPage $page, $flags, $baseRevId, User $user ) {
631
		// Chain to parent
632
		$status = parent::prepareSave( $page, $flags, $baseRevId, $user );
633
634
		if ( $status->isOK() ) {
635
			if ( !$this->isRedirect() && !( $flags & self::EDIT_IGNORE_CONSTRAINTS ) ) {
636
				$handler = $this->getContentHandler();
637
				$validators = $handler->getOnSaveValidators(
638
					( $flags & EDIT_NEW ) !== 0,
639
					$this->getEntity()->getId()
640
				);
641
				$status = $this->applyValidators( $validators );
642
			}
643
		}
644
645
		return $status;
646
	}
647
648
	/**
649
	 * Apply the given validators.
650
	 *
651
	 * @param EntityValidator[] $validators
652
	 *
653
	 * @return Status
654
	 */
655
	private function applyValidators( array $validators ) {
656
		$result = Result::newSuccess();
657
658
		foreach ( $validators as $validator ) {
659
			$result = $validator->validateEntity( $this->getEntity() );
660
661
			if ( !$result->isValid() ) {
662
				break;
663
			}
664
		}
665
666
		$handler = $this->getContentHandler();
667
		$status = $handler->getValidationErrorLocalizer()->getResultStatus( $result );
668
		return $status;
669
	}
670
671
	/**
672
	 * Registers any properties returned by getEntityPageProperties()
673
	 * in $output.
674
	 *
675
	 * @param ParserOutput $output
676
	 */
677
	private function applyEntityPageProperties( ParserOutput $output ) {
678
		if ( $this->isRedirect() ) {
679
			return;
680
		}
681
682
		$properties = $this->getEntityPageProperties();
683
		foreach ( $properties as $name => $value ) {
684
			$output->setProperty( $name, $value );
685
		}
686
	}
687
688
	/**
689
	 * Returns a map of properties about the entity, to be recorded in
690
	 * MediaWiki's page_props table. The idea is to allow efficient lookups
691
	 * of entities based on such properties.
692
	 *
693
	 * @return array A map from property names to property values.
694
	 */
695
	public function getEntityPageProperties() {
696
		return [];
697
	}
698
699
}
700