| Current File : /home/jvzmxxx/wiki1/extensions/Wikibase/repo/includes/Content/EntityHandler.php |
<?php
namespace Wikibase\Repo\Content;
use Content;
use ContentHandler;
use DataUpdate;
use Diff\Patcher\PatcherException;
use IContextSource;
use InvalidArgumentException;
use Language;
use MWContentSerializationException;
use MWException;
use ParserOptions;
use RequestContext;
use Revision;
use Title;
use User;
use Wikibase\Content\DeferredDecodingEntityHolder;
use Wikibase\Content\EntityHolder;
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\EntityContent;
use Wikibase\Lib\Store\EntityContentDataCodec;
use Wikibase\Repo\Diff\EntityContentDiffView;
use Wikibase\Repo\Store\EntityPerPage;
use Wikibase\Repo\Validators\EntityConstraintProvider;
use Wikibase\Repo\Validators\EntityValidator;
use Wikibase\Repo\Validators\ValidatorErrorLocalizer;
use Wikibase\Repo\WikibaseRepo;
use Wikibase\TermIndex;
use Wikibase\Updates\DataUpdateAdapter;
/**
* Base handler class for Entity content classes.
*
* @license GPL-2.0+
* @author Daniel Kinzler
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
* @author Daniel Kinzler
*/
abstract class EntityHandler extends ContentHandler {
/**
* Added to parser options for EntityContent.
*
* Bump the version when making incompatible changes
* to parser output.
*/
const PARSER_VERSION = 3;
/**
* @var EntityPerPage
*/
private $entityPerPage;
/**
* @var TermIndex
*/
private $termIndex;
/**
* @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 EntityPerPage $entityPerPage
* @param TermIndex $termIndex
* @param EntityContentDataCodec $contentCodec
* @param EntityConstraintProvider $constraintProvider
* @param ValidatorErrorLocalizer $errorLocalizer
* @param EntityIdParser $entityIdParser
* @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.
*
* @throws InvalidArgumentException
*/
public function __construct(
$modelId,
EntityPerPage $entityPerPage,
TermIndex $termIndex,
EntityContentDataCodec $contentCodec,
EntityConstraintProvider $constraintProvider,
ValidatorErrorLocalizer $errorLocalizer,
EntityIdParser $entityIdParser,
$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->entityPerPage = $entityPerPage;
$this->termIndex = $termIndex;
$this->contentCodec = $contentCodec;
$this->constraintProvider = $constraintProvider;
$this->errorLocalizer = $errorLocalizer;
$this->entityIdParser = $entityIdParser;
$this->legacyExportFormatDetector = $legacyExportFormatDetector;
}
/**
* 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;
}
/**
* Returns the name of the EntityContent deriving class.
*
* @since 0.3
*
* @return string
*/
abstract protected function getContentClass();
/**
* @see ContentHandler::getDiffEngineClass
*
* @return string
*/
protected function getDiffEngineClass() {
return EntityContentDiffView::class;
}
/**
* 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 ) {
if ( $forCreation ) {
$validators = $this->constraintProvider->getCreationValidators( $this->getEntityType() );
} 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
*
* @throws MWException Always. EntityContent cannot be empty.
* @return EntityContent
*/
public function makeEmptyContent() {
throw new MWException( 'Cannot make an empty EntityContent, since we require at least an ID to be set.' );
}
/**
* 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.
*
* @since 0.5
*
* @return EntityDocument
*/
abstract public function makeEmptyEntity();
/**
* Will return a new EntityContent representing the given EntityRedirect,
* or null if the Content class does not support redirects (that is, if it does
* not have a static newFromRedirect() function).
*
* @see makeRedirectContent()
* @see supportsRedirects()
*
* @since 0.5
*
* @param EntityRedirect $redirect
*
* @return EntityContent|null
*/
public function makeEntityRedirectContent( EntityRedirect $redirect ) {
$contentClass = $this->getContentClass();
if ( !$this->supportsRedirects() ) {
return null;
} else {
$title = $this->getTitleForId( $redirect->getTargetId() );
return $contentClass::newFromRedirect( $redirect, $title );
}
}
/**
* Will return true if the Content class has a static newFromRedirect() function.
*
* @see makeRedirectContent()
* @see makeEntityRedirectContent()
*
* @since 0.5
*
* @return bool
*/
public function supportsRedirects() {
$contentClass = $this->getContentClass();
return method_exists( $contentClass, 'newFromRedirect' );
}
/**
* @see ContentHandler::makeRedirectContent
*
* @warn Always throws an MWException, 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
*
* @throws MWException Always.
* @return EntityContent|null
*/
public function makeRedirectContent( Title $title, $text = '' ) {
throw new MWException( 'EntityContent does not support plain title based redirects.'
. ' Use makeEntityRedirectContent() instead.' );
}
/**
* @see ContentHandler::makeParserOptions
*
* @since 0.5
*
* @param IContextSource|User|string $context
*
* @return ParserOptions
*/
public function makeParserOptions( $context ) {
if ( $context === 'canonical' ) {
// There are no "canonical" ParserOptions for Wikibase,
// as everything is User-language dependent
$context = RequestContext::getMain();
}
$options = parent::makeParserOptions( $context );
// The html representation of entities depends on the user language, so we
// have to call ParserOptions::getUserLangObj to split the cache by user language.
$options->getUserLangObj();
// bump PARSER VERSION when making breaking changes to parser output (e.g. entity view).
$options->addExtraKey( 'wb' . self::PARSER_VERSION );
return $options;
}
/**
* @see ContentHandler::exportTransform
*
* @param string $blob
* @param string|null $format
*
* @return string|void
*/
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;
}
/**
* Creates a Content object for the given Entity object.
*
* @since 0.5
*
* @param EntityHolder $entityHolder
*
* @return EntityContent
*/
public function makeEntityContent( EntityHolder $entityHolder ) {
$contentClass = $this->getContentClass();
/* EntityContent $content */
$content = new $contentClass( $entityHolder );
//TODO: make sure the entity is valid/complete!
return $content;
}
/**
* 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.
*
* @warn This should not really be needed and may just go away!
*
* @since 0.5
*
* @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.
*
* @warn This should not really be needed and may just go away!
*
* @since 0.5
*
* @see EntityTitleLookup::getTitleForId
*
* @param EntityId $id
*
* @throws InvalidArgumentException if $id refers to an entity of the wrong type.
* @return Title
*/
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() );
}
/**
* @see EntityHandler::getEntityNamespace
*
* @return int
*/
final public function getEntityNamespace() {
$entityNamespaceLookup = WikibaseRepo::getDefaultInstance()->getEntityNamespaceLookup();
return $entityNamespaceLookup->getEntityNamespace( $this->getModelID() );
}
/**
* @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 Content::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 Content::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 ) {
global $wgContLang;
return $wgContLang;
}
/**
* 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.
*
* @since 0.2
*
* @return string|null Always null in this default implementation.
*/
public function getSpecialPageForCreation() {
return null;
}
/**
* @see ContentHandler::getUndoContent
*
* @since 0.4
*
* @param Revision $latestRevision The current text
* @param Revision $newerRevision The revision to undo
* @param Revision $olderRevision Must be an earlier revision than $undo
*
* @return Content|bool Content on success, false on failure
*/
public function getUndoContent(
Revision $latestRevision,
Revision $newerRevision,
Revision $olderRevision
) {
/**
* @var EntityContent $latestContent
* @var EntityContent $newerContent
* @var EntityContent $olderContent
*/
$latestContent = $latestRevision->getContent();
$newerContent = $newerRevision->getContent();
$olderContent = $olderRevision->getContent();
if ( $latestRevision->getId() === $newerRevision->getId() ) {
// 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();
/**
* Returns deletion updates for the given EntityContent.
*
* @see Content::getDeletionUpdates
*
* @since 0.5
*
* @param EntityContent $content
* @param Title $title
*
* @return DataUpdate[]
*/
public function getEntityDeletionUpdates( EntityContent $content, Title $title ) {
$updates = array();
$entityId = $content->getEntityId();
// Call the WikibaseEntityDeletionUpdate hook.
// Do this before doing any well-known updates.
$updates[] = new DataUpdateAdapter(
'wfRunHooks',
'WikibaseEntityDeletionUpdate',
array( $content, $title )
);
// Unregister the entity from the terms table.
$updates[] = new DataUpdateAdapter(
array( $this->termIndex, 'deleteTermsOfEntity' ),
$entityId
);
// Unregister the entity from the EntityPerPage table.
$updates[] = new DataUpdateAdapter(
array( $this->entityPerPage, 'deleteEntityPage' ),
$entityId,
$title->getArticleID()
);
return $updates;
}
/**
* Returns modification updates for the given EntityContent.
*
* @see Content::getSecondaryDataUpdates
*
* @since 0.5
*
* @param EntityContent $content
* @param Title $title
*
* @return DataUpdate[]
*/
public function getEntityModificationUpdates( EntityContent $content, Title $title ) {
$updates = array();
$entityId = $content->getEntityId();
//FIXME: we should not need this!
if ( $entityId === null ) {
$entityId = $this->getIdForTitle( $title );
}
if ( $content->isRedirect() ) {
// Remove the entity from the terms table since it's now a redirect.
$updates[] = new DataUpdateAdapter(
array( $this->termIndex, 'deleteTermsOfEntity' ),
$entityId
);
// Register the redirect from the EntityPerPage table.
$updates[] = new DataUpdateAdapter(
array( $this->entityPerPage, 'addRedirectPage' ),
$entityId,
$title->getArticleID(),
$content->getEntityRedirect()->getTargetId()
);
} else {
// Register the entity in the EntityPerPage table.
$updates[] = new DataUpdateAdapter(
array( $this->entityPerPage, 'addEntityPage' ),
$entityId,
$title->getArticleID()
);
// Register the entity in the terms table.
$updates[] = new DataUpdateAdapter(
array( $this->termIndex, 'saveTermsOfEntity' ),
$content->getEntity()
);
}
// Call the WikibaseEntityModificationUpdate hook.
// Do this after doing all well-known updates.
$updates[] = new DataUpdateAdapter(
'wfRunHooks',
'WikibaseEntityModificationUpdate',
array( $content, $title )
);
return $updates;
}
}