repo/includes/Content/EntityHandler.php
<?php
namespace Wikibase\Repo\Content;
use Article;
use Diff\Patcher\PatcherException;
use InvalidArgumentException;
use LogicException;
use MediaWiki\Content\Content;
use MediaWiki\Content\ContentHandler;
use MediaWiki\Content\Renderer\ContentParseParams;
use MediaWiki\Content\ValidationParams;
use MediaWiki\Context\IContextSource;
use MediaWiki\Context\RequestContext;
use MediaWiki\Html\Html;
use MediaWiki\Language\Language;
use MediaWiki\MediaWikiServices;
use MediaWiki\Parser\ParserCache;
use MediaWiki\Parser\ParserOptions;
use MediaWiki\Parser\ParserOutput;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\Status\Status;
use MediaWiki\Title\Title;
use MWContentSerializationException;
use SearchEngine;
use ValueValidators\Result;
use Wikibase\DataModel\Entity\EntityDocument;
use Wikibase\DataModel\Entity\EntityId;
use Wikibase\DataModel\Entity\EntityIdParser;
use Wikibase\DataModel\Entity\EntityIdParsingException;
use Wikibase\DataModel\Entity\EntityRedirect;
use Wikibase\Lib\Store\EntityContentDataCodec;
use Wikibase\Lib\Store\EntityRevision;
use Wikibase\Repo\Diff\DispatchingEntityDiffVisualizer;
use Wikibase\Repo\Diff\EntityContentDiffView;
use Wikibase\Repo\Diff\EntitySlotDiffRenderer;
use Wikibase\Repo\Search\Fields\FieldDefinitions;
use Wikibase\Repo\Validators\EntityConstraintProvider;
use Wikibase\Repo\Validators\EntityValidator;
use Wikibase\Repo\Validators\ValidatorErrorLocalizer;
use Wikibase\Repo\WikibaseRepo;
use Wikimedia\Assert\Assert;
use WikiPage;
/**
* Base handler class for Entity content classes.
* @license GPL-2.0-or-later
* @author Daniel Kinzler
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
*/
abstract class EntityHandler extends ContentHandler {
/**
* Added to parser options for EntityContent.
*
* Bump the version when making incompatible changes
* to parser output.
*/
public const PARSER_VERSION = 3;
/**
* @var FieldDefinitions
*/
protected $fieldDefinitions;
/**
* @var EntityContentDataCodec
*/
protected $contentCodec;
/**
* @var EntityConstraintProvider
*/
protected $constraintProvider;
/**
* @var ValidatorErrorLocalizer
*/
private $errorLocalizer;
/**
* @var EntityIdParser
*/
private $entityIdParser;
/**
* @var callable|null Callback to determine whether a serialized
* blob needs to be re-serialized on export.
*/
private $legacyExportFormatDetector;
/**
* @param string $modelId
* @param mixed $unused @todo Get rid of me
* @param EntityContentDataCodec $contentCodec
* @param EntityConstraintProvider $constraintProvider
* @param ValidatorErrorLocalizer $errorLocalizer
* @param EntityIdParser $entityIdParser
* @param FieldDefinitions $fieldDefinitions
* @param callable|null $legacyExportFormatDetector Callback to determine whether a serialized
* blob needs to be re-serialized on export. The callback must take two parameters,
* the blob an the serialization format. It must return true if re-serialization is needed.
* False positives are acceptable, false negatives are not.
*
*/
public function __construct(
$modelId,
$unused,
EntityContentDataCodec $contentCodec,
EntityConstraintProvider $constraintProvider,
ValidatorErrorLocalizer $errorLocalizer,
EntityIdParser $entityIdParser,
FieldDefinitions $fieldDefinitions,
$legacyExportFormatDetector = null
) {
$formats = $contentCodec->getSupportedFormats();
parent::__construct( $modelId, $formats );
if ( $legacyExportFormatDetector && !is_callable( $legacyExportFormatDetector ) ) {
throw new InvalidArgumentException( '$legacyExportFormatDetector must be a callable (or null)' );
}
$this->contentCodec = $contentCodec;
$this->constraintProvider = $constraintProvider;
$this->errorLocalizer = $errorLocalizer;
$this->entityIdParser = $entityIdParser;
$this->legacyExportFormatDetector = $legacyExportFormatDetector;
$this->fieldDefinitions = $fieldDefinitions;
}
/**
* Returns the callback used to determine whether a serialized blob needs
* to be re-serialized on export (or null of re-serialization is disabled).
*
* @return callable|null
*/
public function getLegacyExportFormatDetector() {
return $this->legacyExportFormatDetector;
}
/**
* Handle the fact that a given page does not contain an Entity, even though it could.
* Per default, this behaves similarly to Article::showMissingArticle: it shows
* a message to the user.
*
* @see Article::showMissingArticle
*
* @param Title $title The title of the page that potentially could, but does not,
* contain an entity.
* @param IContextSource $context Context to use for reporting. In particular, output
* will be written to $context->getOutput().
*/
public function showMissingEntity( Title $title, IContextSource $context ) {
$text = wfMessage( 'wikibase-noentity' )->setContext( $context )->plain();
$dir = $context->getLanguage()->getDir();
$lang = $context->getLanguage()->getHtmlCode();
$outputPage = $context->getOutput();
$outputPage->addWikiTextAsInterface( Html::openElement( 'div', [
'class' => "noarticletext mw-content-$dir",
'dir' => $dir,
'lang' => $lang,
] ) . "\n$text\n</div>" );
}
/** @inheritDoc */
protected function getDiffEngineClass() {
return EntityContentDiffView::class;
}
protected function getSlotDiffRendererWithOptions( IContextSource $context, $options = [] ) {
$entityDiffVisualizerFactory = WikibaseRepo::getEntityDiffVisualizerFactory();
$diffVisualizer = new DispatchingEntityDiffVisualizer( $entityDiffVisualizerFactory, $context );
return new EntitySlotDiffRenderer( $diffVisualizer, $context->getLanguage()->getCode() );
}
/**
* Get EntityValidators for on-save validation.
*
* @see getValidationErrorLocalizer()
*
* @param bool $forCreation Whether the entity is created (true) or updated (false).
*
* @return EntityValidator[]
*/
public function getOnSaveValidators( $forCreation, EntityId $entityId ) {
if ( $forCreation ) {
$validators = $this->constraintProvider->getCreationValidators( $this->getEntityType(), $entityId );
} else {
$validators = $this->constraintProvider->getUpdateValidators( $this->getEntityType() );
}
return $validators;
}
/**
* Error localizer for use together with getOnSaveValidators().
*
* @see getOnSaveValidators()
*
* @return ValidatorErrorLocalizer
*/
public function getValidationErrorLocalizer() {
return $this->errorLocalizer;
}
/**
* @see ContentHandler::makeEmptyContent
*
* @return EntityContent
*/
public function makeEmptyContent() {
return $this->newEntityContent( null );
}
/**
* Returns an empty Entity object of the type supported by this handler.
* This is intended to provide a baseline for diffing and related operations.
*
* @note The Entity returned here will not have an ID set, and is thus not
* suitable for use in an EntityContent object.
*
* @return EntityDocument
*/
abstract public function makeEmptyEntity();
/**
* @param EntityRedirect $redirect Unused in this default implementation.
*
* @return EntityContent|null Either a new EntityContent representing the given EntityRedirect,
* or null if the entity type does not support redirects. Always null in this default
* implementation.
*/
public function makeEntityRedirectContent( EntityRedirect $redirect ) {
return null;
}
/**
* None of the Entity content models support categories.
*
* @return bool Always false.
*/
public function supportsCategories() {
return false;
}
/**
* Do not render HTML on edit (T285987)
*/
public function generateHTMLOnEdit(): bool {
return false;
}
/**
* @see ContentHandler::getAutosummary
*
* We never want to use MediaWiki's autosummaries, used e.g. for new page creation. Override this
* to make sure they never overwrite our autosummaries (which look like the automatic summary
* prefixes with a section title, and so could be overwritten).
*
* @param Content|null $oldContent
* @param Content|null $newContent
* @param int $flags
*
* @return string Empty string
*/
public function getAutosummary(
Content $oldContent = null,
Content $newContent = null,
$flags = 0
) {
return '';
}
/**
* @see ContentHandler::makeRedirectContent
*
* @warning This always throws an exception, since an EntityRedirects needs to know it's own
* ID in addition to the target ID. We have no way to guess that in makeRedirectContent().
* Use makeEntityRedirectContent() instead.
*
* @see makeEntityRedirectContent()
*
* @param Title $title
* @param string $text
*
* @return never
*/
public function makeRedirectContent( Title $title, $text = '' ) {
throw new LogicException( 'EntityContent does not support plain title based redirects.'
. ' Use makeEntityRedirectContent() instead.' );
}
/**
* @see ContentHandler::exportTransform
*
* @param string $blob
* @param string|null $format
*
* @return string
*/
public function exportTransform( $blob, $format = null ) {
if ( !$this->legacyExportFormatDetector ) {
return $blob;
}
$needsTransform = call_user_func( $this->legacyExportFormatDetector, $blob, $format );
if ( $needsTransform ) {
$format = ( $format === null ) ? $this->getDefaultFormat() : $format;
$content = $this->unserializeContent( $blob, $format );
$blob = $this->serializeContent( $content );
}
return $blob;
}
/**
* @param EntityHolder $entityHolder
*
* @return EntityContent
*/
public function makeEntityContent( EntityHolder $entityHolder ) {
return $this->newEntityContent( $entityHolder );
}
/**
* @param EntityHolder|null $entityHolder
*
* @return EntityContent
*/
abstract protected function newEntityContent( EntityHolder $entityHolder = null );
/**
* Parses the given ID string into an EntityId for the type of entity
* supported by this EntityHandler. If the string is not a valid
* serialization of the correct type of entity ID, an exception is thrown.
*
* @param string $id String representation the entity ID
*
* @return EntityId
* @throws InvalidArgumentException
*/
abstract public function makeEntityId( $id );
/**
* @return string
*/
public function getDefaultFormat() {
return $this->contentCodec->getDefaultFormat();
}
/**
* @param Content $content
* @param string|null $format
*
* @throws InvalidArgumentException
* @throws MWContentSerializationException
* @return string
*/
public function serializeContent( Content $content, $format = null ) {
if ( !( $content instanceof EntityContent ) ) {
throw new InvalidArgumentException( '$content must be an instance of EntityContent' );
}
if ( $content->isRedirect() ) {
$redirect = $content->getEntityRedirect();
return $this->contentCodec->encodeRedirect( $redirect, $format );
} else {
// TODO: If we have an un-decoded Entity in a DeferredDecodingEntityHolder, just re-use
// the encoded form.
$entity = $content->getEntity();
return $this->contentCodec->encodeEntity( $entity, $format );
}
}
/**
* @see ContentHandler::unserializeContent
*
* @param string $blob
* @param string|null $format
*
* @throws MWContentSerializationException
* @return EntityContent
*/
public function unserializeContent( $blob, $format = null ) {
$redirect = $this->contentCodec->decodeRedirect( $blob, $format );
if ( $redirect !== null ) {
return $this->makeEntityRedirectContent( $redirect );
} else {
$holder = new DeferredDecodingEntityHolder(
$this->contentCodec,
$blob,
$format,
$this->getEntityType()
);
$entityContent = $this->makeEntityContent( $holder );
return $entityContent;
}
}
/**
* Returns the ID of the entity contained by the page of the given title.
*
* @warning This should not really be needed and may just go away!
*
* @param Title $target
*
* @throws EntityIdParsingException
* @return EntityId
*/
public function getIdForTitle( Title $target ) {
return $this->entityIdParser->parse( $target->getText() );
}
/**
* Returns the appropriate page Title for the given EntityId.
*
* @warning This should not really be needed and may just go away!
*
* @see EntityTitleStoreLookup::getTitleForId
*
* @param EntityId $id
*
* @throws InvalidArgumentException if $id refers to an entity of the wrong type.
* @return Title|null
*/
public function getTitleForId( EntityId $id ) {
if ( $id->getEntityType() !== $this->getEntityType() ) {
throw new InvalidArgumentException( 'The given ID does not refer to an entity of type '
. $this->getEntityType() );
}
return Title::makeTitle( $this->getEntityNamespace(), $id->getSerialization() );
}
/**
* Returns the appropriate page Titles for the given EntityIds
*
* @param EntityId[] $ids
* @return Title[] Array of Title objects indexed by the entity id serializations
*/
public function getTitlesForIds( array $ids ) {
$titles = [];
foreach ( $ids as $id ) {
if ( $id->getEntityType() !== $this->getEntityType() ) {
throw new InvalidArgumentException(
'The given ID does not refer to an entity of type ' . $this->getEntityType()
);
}
$titles[ $id->getSerialization() ] =
Title::makeTitle( $this->getEntityNamespace(), $id->getSerialization() );
}
return $titles;
}
/**
* Returns the namespace that is to be used for this kind of entities.
*
* @return int
*/
final public function getEntityNamespace() {
$entityNamespaceLookup = WikibaseRepo::getEntityNamespaceLookup();
$ns = $entityNamespaceLookup->getEntityNamespace( $this->getEntityType() );
Assert::postcondition(
$ns !== null,
'Namespace for entity type ' . $this->getEntityType() . ' must be defined!'
);
return $ns;
}
/**
* Returns the slot that is to be used for this kind of entities.
*
* @return string the role name of the slot
*/
final public function getEntitySlotRole() {
$entityNamespaceLookup = WikibaseRepo::getEntityNamespaceLookup();
return $entityNamespaceLookup->getEntitySlotRole( $this->getEntityType() );
}
/**
* @see ContentHandler::canBeUsedOn
*
* This implementation returns true if and only if the given title's namespace
* is the same as the one returned by $this->getEntityNamespace().
*
* @param Title $title
*
* @return bool true if $title represents a page in the appropriate entity namespace.
*/
public function canBeUsedOn( Title $title ) {
if ( !parent::canBeUsedOn( $title ) ) {
return false;
}
$namespace = $this->getEntityNamespace();
return $namespace === $title->getNamespace();
}
/**
* Returns true to indicate that the parser cache can be used for data items.
*
* @note The html representation of entities depends on the user language, so
* EntityContent::getParserOutput needs to make sure ParserOutput::recordOption( 'userlang' )
* is called to split the cache by user language.
*
* @see ContentHandler::isParserCacheSupported
*
* @return bool Always true in this default implementation.
*/
public function isParserCacheSupported() {
return true;
}
/**
* @see ContentHandler::getPageViewLanguage
*
* This implementation returns the user language, because entities get rendered in
* the user's language. The PageContentLanguage hook is bypassed.
*
* @param Title $title the page to determine the language for.
* @param Content|null $content the page's content, if you have it handy, to avoid reloading it.
*
* @return Language The page's language
*/
public function getPageViewLanguage( Title $title, Content $content = null ) {
global $wgLang;
return $wgLang;
}
/**
* @see ContentHandler::getPageLanguage
*
* This implementation unconditionally returns the wiki's content language.
* The PageContentLanguage hook is bypassed.
*
* @note Ideally, this would return 'mul' to indicate multilingual content. But MediaWiki
* currently doesn't support that.
*
* @note in several places in mediawiki, most importantly the parser cache, getPageLanguage
* is used in places where getPageViewLanguage would be more appropriate.
*
* @param Title $title the page to determine the language for.
* @param Content|null $content the page's content, if you have it handy, to avoid reloading it.
*
* @return Language The page's language
*/
public function getPageLanguage( Title $title, Content $content = null ) {
return MediaWikiServices::getInstance()->getContentLanguage();
}
/**
* Returns the name of the special page responsible for creating a page
* for this type of entity content.
* Returns null if there is no such special page.
*
* @return string|null Always null in this default implementation.
*/
public function getSpecialPageForCreation() {
return null;
}
/**
* @see ContentHandler::getUndoContent
*
* @param Content $latestContent The current text
* @param Content $newerContent The revision to undo
* @param Content $olderContent Must be an earlier revision than $newer
* @param bool $undoIsLatest Set to true if $newer is from the current revision (since 1.32)
*
* @return EntityContent|bool Content on success, false on failure
*/
public function getUndoContent(
Content $latestContent,
Content $newerContent,
Content $olderContent,
$undoIsLatest = false
) {
if (
!$latestContent instanceof EntityContent
|| !$newerContent instanceof EntityContent
|| !$olderContent instanceof EntityContent
) {
return false;
}
if ( $undoIsLatest ) {
// no patching needed, just roll back
return $olderContent;
}
// diff from new to base
$patch = $newerContent->getDiff( $olderContent );
try {
// apply the patch( new -> old ) to the current revision.
$patchedCurrent = $latestContent->getPatchedCopy( $patch );
} catch ( PatcherException $ex ) {
return false;
}
// detect conflicts against current revision
$cleanPatch = $latestContent->getDiff( $patchedCurrent );
$conflicts = $patch->count() - $cleanPatch->count();
if ( $conflicts > 0 ) {
return false;
} else {
return $patchedCurrent;
}
}
/**
* Returns the entity type ID for the kind of entity managed by this EntityContent implementation.
*
* @return string
*/
abstract public function getEntityType();
/**
* Whether IDs can automatically be assigned to entities
* of the kind supported by this EntityHandler.
*
* @return bool
*/
public function allowAutomaticIds() {
return true;
}
/**
* Whether the given custom ID is valid for creating a new entity
* of the kind supported by this EntityHandler.
*
* Implementations are not required to check if an entity with the given ID already exists.
* If this method returns true, this means that an entity with the given ID could be
* created (or already existed) at the time the method was called. There is no guarantee
* that this continues to be true after the method call returned. Callers must be careful
* to handle race conditions.
*
* @note For entity types that cannot be created with custom IDs (that is,
* entity types that are defined to use automatic IDs), this should always
* return false.
*
* @see EntityStore::canCreateWithCustomId()
*
* @param EntityId $id
*
* @return bool
*/
public function canCreateWithCustomId( EntityId $id ) {
return false;
}
/**
* @param SearchEngine $engine
* @return \SearchIndexField[] List of fields this content handler can provide.
*/
public function getFieldsForSearchIndex( SearchEngine $engine ) {
$fields = [];
foreach ( $this->fieldDefinitions->getFields() as $name => $field ) {
$mappingField = $field->getMappingField( $engine, $name );
if ( $mappingField ) {
$fields[$name] = $mappingField;
}
}
return $fields;
}
/**
* @inheritDoc
*/
public function getDataForSearchIndex(
WikiPage $page,
ParserOutput $parserOutput,
SearchEngine $engine,
RevisionRecord $revision = null
) {
$fieldsData = parent::getDataForSearchIndex( $page, $parserOutput, $engine, $revision );
$content = $revision != null ? $revision->getContent( SlotRecord::MAIN ) : $page->getContent();
return $this->getContentDataForSearchIndex( $content ) + $fieldsData;
}
/**
* Extract fields data for the search index but only the fields
* related to the slot content.
* Useful for EntityHandlers that may work on non-main slot contents.
*
* @stable to override
* @param Content $content the Content to extract search data from
* @return array fields to be indexed by the search engine
*/
public function getContentDataForSearchIndex( Content $content ): array {
$fieldsData = [];
if ( ( $content instanceof EntityContent ) && !$content->isRedirect() ) {
$entity = $content->getEntity();
$fields = $this->fieldDefinitions->getFields();
foreach ( $fields as $fieldName => $field ) {
$fieldsData[$fieldName] = $field->getFieldData( $entity );
}
}
return $fieldsData;
}
/**
* Produce page output suitable for indexing.
* Does not include HTML.
*
* @inheritDoc
*/
public function getParserOutputForIndexing( WikiPage $page, ParserCache $cache = null, RevisionRecord $revision = null ) {
$parserOptions = $page->makeParserOptions( 'canonical' );
$renderer = MediaWikiServices::getInstance()->getRevisionRenderer();
$revisionRecord = $this->latestRevision( $page );
$parserOutput = $renderer->getRenderedRevision( $revisionRecord, $parserOptions )
// this will call EntityContent::getParserOutput() with $generateHtml = false
->getRevisionParserOutput( [
'generate-html' => false,
] );
// since we didn’t generate HTML, don’t call $cache->save()
return $parserOutput;
}
/**
* @inheritDoc
*/
public function validateSave(
Content $content,
ValidationParams $validationParams
) {
'@phan-var EntityContent $content';
// Chain to parent
$status = parent::validateSave( $content, $validationParams );
$flags = $validationParams->getFlags();
if ( $status->isOK() ) {
if ( !$content->isRedirect() && !( $flags & EntityContent::EDIT_IGNORE_CONSTRAINTS ) ) {
$validators = $this->getOnSaveValidators(
( $flags & EDIT_NEW ) !== 0,
$content->getEntity()->getId()
);
$status = $this->applyValidators( $content, $validators );
}
}
return $status;
}
/**
* @note this calls ParserOutput::recordOption( 'userlang' ) to split the cache
* by user language, and ParserOutput::recordOption( 'wb' ) to split the cache on
* EntityHandler::PARSER_VERSION.
*
* @inheritDoc
*/
protected function fillParserOutput(
Content $content,
ContentParseParams $cpoParams,
ParserOutput &$parserOutput
) {
'@phan-var EntityContent $content';
$generateHtml = $cpoParams->getGenerateHtml();
$parserOptions = $cpoParams->getParserOptions();
$revId = $cpoParams->getRevId();
if ( $content->isRedirect() ) {
$parserOutput = $this->getParserOutputForRedirect( $content, $generateHtml );
} elseif ( !$content->getEntityHolder() ) {
// NOTE: There is no entity to render, but fillParserOutput() must work for all Content objects.
// NOTE: isEmpty() will return true when there is an entity, but that entity is empty. In
// that case, we must not bail out, but call getParserOutputFromEntityView() as normal.
} else {
$parserOutput = $this->getParserOutputFromEntityView(
$content,
$revId,
$parserOptions,
$generateHtml
);
if ( !$parserOptions->getUserLangObj()->equals( RequestContext::getMain()->getLanguage() ) ) {
// HACK: Don't save to parser cache if this is not in the user's lang: T199983.
$parserOutput->updateCacheExpiry( 0 );
}
}
}
/**
* @note Will fail if this EntityContent does not represent a redirect.
*
* @param EntityContent $content
* @param bool $generateHtml
*
* @return ParserOutput
*/
protected function getParserOutputForRedirect( EntityContent $content, bool $generateHtml ) {
$parserOutput = new ParserOutput();
$parserOutput->resetParseStartTime();
$target = $content->getRedirectTarget();
// Make sure to include the redirect link in pagelinks
$parserOutput->addLink( $target );
// Since the output depends on the user language, we must make sure
// ParserCache::getKey() includes it in the cache key.
$parserOutput->recordOption( 'userlang' );
// And we need to include EntityHandler::PARSER_VERSION in the cache key too
$parserOutput->recordOption( 'wb' );
if ( $generateHtml ) {
$language = $this->getPageViewLanguage( $target );
$services = MediaWikiServices::getInstance();
$html = $services->getLinkRenderer()->makeRedirectHeader(
$language, $target, false
);
$parserOutput->setRedirectHeader( $html );
$parserOutput->setText( '' );
}
return $parserOutput;
}
/**
* @note Will fail if this EntityContent represents a redirect.
*
* @param EntityContent $content
* @param int|null $revisionId
* @param ParserOptions $options
* @param bool $generateHtml
*
* @return ParserOutput
*/
protected function getParserOutputFromEntityView(
EntityContent $content,
$revisionId,
ParserOptions $options,
$generateHtml = true
) {
$outputGenerator = WikibaseRepo::getEntityParserOutputGeneratorFactory()
->getEntityParserOutputGenerator(
$this->getValidUserLanguage( $options->getUserLangObj() )
);
$entityRevision = $this->getEntityRevision( $content, $revisionId );
$parserOutput = $outputGenerator->getParserOutput( $entityRevision, $generateHtml );
// Since the output depends on the user language, we must make sure
// ParserCache::getKey() includes it in the cache key.
$parserOutput->recordOption( 'userlang' );
// And we need to include EntityHandler::PARSER_VERSION in the cache key too
$parserOutput->recordOption( 'wb' );
$this->applyEntityPageProperties( $content, $parserOutput );
return $parserOutput;
}
private function getValidUserLanguage( Language $language ) {
$services = MediaWikiServices::getInstance();
if ( !$services->getLanguageNameUtils()->isValidBuiltInCode( $language->getCode() ) ) {
return $services->getLanguageFactory()->getLanguage( 'und' ); // T204791
}
return $language;
}
/**
* @param EntityContent $content
* @param int|null $revisionId
*
* @return EntityRevision
*/
private function getEntityRevision( EntityContent $content, $revisionId = null ) {
$entity = $content->getEntity();
if ( $revisionId !== null ) {
return new EntityRevision( $entity, $revisionId );
}
// Revision defaults to 0 (latest), which is desired and suitable in cases where
// getParserOutput specifies no revision. (e.g. is called during save process
// when revision id is unknown or not assigned yet)
return new EntityRevision( $entity );
}
/**
* Registers any properties returned by getEntityPageProperties()
* in $parserOutput.
*
* @param EntityContent $content
* @param ParserOutput $parserOutput
*/
private function applyEntityPageProperties( EntityContent $content, ParserOutput $parserOutput ) {
if ( $content->isRedirect() ) {
return;
}
$properties = $content->getEntityPageProperties();
foreach ( $properties as $name => $value ) {
if ( is_numeric( $value ) ) {
$parserOutput->setNumericPageProperty( $name, $value );
} elseif ( is_bool( $value ) ) {
$parserOutput->setNumericPageProperty( $name, (int)$value );
} else {
$parserOutput->setUnsortedPageProperty( $name, $value );
}
}
}
/**
* Apply the given validators.
*
* @param EntityContent $content
* @param EntityValidator[] $validators
*
* @return Status
*/
private function applyValidators( EntityContent $content, array $validators ) {
$result = Result::newSuccess();
foreach ( $validators as $validator ) {
$result = $validator->validateEntity( $content->getEntity() );
if ( !$result->isValid() ) {
break;
}
}
$status = $this->getValidationErrorLocalizer()->getResultStatus( $result );
return $status;
}
}