Current File : /home/jvzmxxx/wiki/extensions/Wikibase/repo/includes/Content/EntityContent.php
<?php

namespace Wikibase;

use AbstractContent;
use Article;
use Content;
use DataUpdate;
use Diff\Differ\MapDiffer;
use Diff\DiffOp\Diff\Diff;
use Diff\Patcher\MapPatcher;
use Diff\Patcher\PatcherException;
use Hooks;
use LogicException;
use MWException;
use ParserOptions;
use ParserOutput;
use RuntimeException;
use Status;
use Title;
use User;
use ValueValidators\Result;
use Wikibase\Content\DeferredCopyEntityHolder;
use Wikibase\Content\EntityHolder;
use Wikibase\Content\EntityInstanceHolder;
use Wikibase\DataModel\Entity\EntityDocument;
use Wikibase\DataModel\Entity\EntityId;
use Wikibase\DataModel\Entity\EntityRedirect;
use Wikibase\DataModel\Services\Diff\EntityDiffer;
use Wikibase\DataModel\Services\Diff\EntityPatcher;
use Wikibase\DataModel\Term\FingerprintProvider;
use Wikibase\Repo\Content\EntityContentDiff;
use Wikibase\Repo\Content\EntityHandler;
use Wikibase\Repo\FingerprintSearchTextGenerator;
use Wikibase\Repo\Validators\EntityValidator;
use Wikibase\Repo\WikibaseRepo;
use WikiPage;

/**
 * Abstract content object for articles representing Wikibase entities.
 *
 * @since 0.1
 *
 * @license GPL-2.0+
 * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 * @author Daniel Kinzler
 * @author Bene* < benestar.wikimedia@gmail.com >
 */
abstract class EntityContent extends AbstractContent {

	/**
	 * For use in the wb-status page property to indicate that the entity has no special
	 * status, to indicate. STATUS_NONE will not be recorded in the database.
	 *
	 * @see getEntityStatus()
	 */
	const STATUS_NONE = 0;

	/**
	 * For use in the wb-status page property to indicate that the entity is a stub,
	 * i.e. it's empty except for terms (labels, descriptions, and aliases).
	 *
	 * @see getEntityStatus()
	 */
	const STATUS_STUB = 100;

	/**
	 * For use in the wb-status page property to indicate that the entity is empty.
	 *
	 * @see getEntityStatus()
	 */
	const STATUS_EMPTY = 200;

	/**
	 * Flag for use with prepareSave(), indicating that no pre-save validation should be applied.
	 * Can be passed in via EditEntity::attemptSave, EntityStore::saveEntity,
	 * as well as WikiPage::doEditContent()
	 *
	 * @note: must not collide with the EDIT_XXX flags defined by MediaWiki core in Defines.php.
	 */
	const EDIT_IGNORE_CONSTRAINTS = 1024;

	/**
	 * Checks if this EntityContent is valid for saving.
	 *
	 * Returns false if the entity does not have an ID set.
	 *
	 * @see Content::isValid()
	 */
	public function isValid() {
		if ( $this->isRedirect() ) {
			// Under some circumstances, the handler will not support redirects,
			// but it's still possible to construct Content objects that represent
			// redirects. In such a case, make sure such Content objects are considered
			// invalid and do not get saved.
			return $this->getContentHandler()->supportsRedirects();
		}

		return $this->getEntityId() !== null;
	}

	/**
	 * Returns the EntityRedirect represented by this EntityContent, or null if this
	 * EntityContent is not a redirect.
	 *
	 * @note This default implementation will fail if isRedirect() is true.
	 * Subclasses that support redirects must override getEntityRedirect().
	 *
	 * @throws LogicException
	 * @return EntityRedirect|null
	 */
	public function getEntityRedirect() {
		if ( $this->isRedirect() ) {
			throw new LogicException( 'EntityContent subclasses that support redirects must override getEntityRedirect()' );
		}

		return null;
	}

	/**
	 * Returns the entity contained by this entity content.
	 * Deriving classes typically have a more specific get method as
	 * for greater clarity and type hinting.
	 *
	 * @throws MWException when it's a redirect (targets will never be resolved)
	 * @return EntityDocument
	 */
	abstract public function getEntity();

	/**
	 * Returns a holder for the entity contained in this EntityContent object.
	 *
	 * @throws MWException when it's a redirect (targets will never be resolved)
	 * @return EntityHolder
	 */
	abstract protected function getEntityHolder();

	/**
	 * Returns the ID of the entity represented by this EntityContent;
	 *
	 * @throws RuntimeException if no entity ID is set
	 * @return EntityId
	 */
	public function getEntityId() {
		if ( $this->isRedirect() ) {
			return $this->getEntityRedirect()->getEntityId();
		} else {
			$id = $this->getEntityHolder()->getEntityId();
			if ( !$id ) {
				// @todo: Force an ID to be present; Entity objects without an ID make sense,
				// EntityContent objects with no entity ID don't.
				throw new RuntimeException( 'EntityContent was constructed without an EntityId!' );
			}

			return $id;
		}
	}

	/**
	 * @see Content::getDeletionUpdates
	 * @see EntityHandler::getEntityDeletionUpdates
	 *
	 * @param WikiPage $page
	 * @param ParserOutput|null $parserOutput
	 *
	 * @return DataUpdate[]
	 */
	public function getDeletionUpdates( WikiPage $page, ParserOutput $parserOutput = null ) {
		/* @var EntityHandler $handler */
		$handler = $this->getContentHandler();
		$updates = $handler->getEntityDeletionUpdates( $this, $page->getTitle() );

		return array_merge(
			parent::getDeletionUpdates( $page, $parserOutput ),
			$updates
		);
	}

	/**
	 * @see Content::getSecondaryDataUpdates
	 * @see EntityHandler::getEntityModificationUpdates
	 *
	 * @param Title $title
	 * @param Content|null $oldContent
	 * @param bool $recursive
	 * @param ParserOutput|null $parserOutput
	 *
	 * @return DataUpdate[]
	 */
	public function getSecondaryDataUpdates(
		Title $title,
		Content $oldContent = null,
		$recursive = false,
		ParserOutput $parserOutput = null
	) {
		/** @var EntityHandler $handler */
		$handler = $this->getContentHandler();
		$updates = $handler->getEntityModificationUpdates( $this, $title );

		return array_merge(
			parent::getSecondaryDataUpdates( $title, $oldContent, $recursive, $parserOutput ),
			$updates
		);
	}

	/**
	 * Returns a ParserOutput object containing the HTML.
	 *
	 * @note: this calls ParserOutput::recordOption( 'userlang' ) to split the cache
	 * by user language.
	 *
	 * @see Content::getParserOutput
	 *
	 * @param Title $title
	 * @param int|null $revisionId
	 * @param ParserOptions|null $options
	 * @param bool $generateHtml
	 *
	 * @return ParserOutput
	 */
	public function getParserOutput(
		Title $title,
		$revisionId = null,
		ParserOptions $options = null,
		$generateHtml = true
	) {
		if ( $this->isRedirect() ) {
			return $this->getParserOutputForRedirect( $generateHtml );
		} else {
			if ( $options === null ) {
				$options = $this->getContentHandler()->makeParserOptions( 'canonical' );
			}

			return $this->getParserOutputFromEntityView( $title, $revisionId, $options, $generateHtml );
		}
	}

	/**
	 * @since 0.5
	 *
	 * @note Will fail if this EntityContent does not represent a redirect.
	 *
	 * @param bool $generateHtml
	 *
	 * @return ParserOutput
	 */
	protected function getParserOutputForRedirect( $generateHtml ) {
		$output = new ParserOutput();
		/** @var Title $target */
		$target = $this->getRedirectTarget();

		// Make sure to include the redirect link in pagelinks
		$output->addLink( $target );

		// Since the output depends on the user language, we must make sure
		// ParserCache::getKey() includes it in the cache key.
		$output->recordOption( 'userlang' );
		if ( $generateHtml ) {
			$chain = $this->getRedirectChain();
			$language = $this->getContentHandler()->getPageViewLanguage( $target );
			$html = Article::getRedirectHeaderHtml( $language, $chain, false );
			$output->setText( $html );
		}

		return $output;
	}

	/**
	 * @since 0.5
	 *
	 * @note Will fail if this EntityContent represents a redirect.
	 *
	 * @param Title $title
	 * @param int|null $revisionId
	 * @param ParserOptions $options
	 * @param bool $generateHtml
	 *
	 * @return ParserOutput
	 */
	protected function getParserOutputFromEntityView(
		Title $title,
		$revisionId = null,
		ParserOptions $options,
		$generateHtml = true
	) {
		// @todo: move this to the ContentHandler
		$entityParserOutputGeneratorFactory = WikibaseRepo::getDefaultInstance()->getEntityParserOutputGeneratorFactory();

		$outputGenerator = $entityParserOutputGeneratorFactory->getEntityParserOutputGenerator(
			$options->getUserLang(),
			$options->getEditSection()
		);

		$entityRevision = $this->getEntityRevision( $revisionId );

		$output = $outputGenerator->getParserOutput( $entityRevision->getEntity(), $generateHtml );

		// Force parser cache split by whether edit links are show.
		// MediaWiki core has the ability to split on editsection, but does not trigger it
		// automatically when $parserOptions->getEditSection() is called. Presumably this
		// is because core uses <mw:editsection> tags that are substituted by ParserOutput::getText
		// using the info from ParserOutput::getEditSectionTokens.
		$output->recordOption( 'editsection' );

		// Since the output depends on the user language, we must make sure
		// ParserCache::getKey() includes it in the cache key.
		$output->recordOption( 'userlang' );

		$this->applyEntityPageProperties( $output );

		return $output;
	}

	/**
	 * @param int|null $revisionId
	 *
	 * @return EntityRevision
	 */
	private function getEntityRevision( $revisionId = null ) {
		$entity = $this->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 );
	}

	/**
	 * @return String a string representing the content in a way useful for building a full text
	 *         search index.
	 */
	public function getTextForSearchIndex() {
		if ( $this->isRedirect() ) {
			return '';
		}

		$text = '';
		$entity = $this->getEntity();

		if ( $entity instanceof FingerprintProvider ) {
			$searchTextGenerator = new FingerprintSearchTextGenerator();
			$text = $searchTextGenerator->generate( $entity->getFingerprint() );
		}

		if ( !Hooks::run( 'WikibaseTextForSearchIndex', array( $this, &$text ) ) ) {
			return '';
		}

		return $text;
	}

	/**
	 * @return string Returns the string representation of the redirect
	 * represented by this EntityContent (if any).
	 *
	 * @note Will fail if this EntityContent is not a redirect.
	 */
	protected function getRedirectText() {
		/** @var Title $target */
		$target = $this->getRedirectTarget();
		return '#REDIRECT [[' . $target->getFullText() . ']]';
	}

	/**
	 * @return String a string representing the content in a way useful for content filtering as
	 *         performed by extensions like AbuseFilter.
	 */
	public function getTextForFilters() {
		if ( $this->isRedirect() ) {
			return $this->getRedirectText();
		}

		//XXX: $ignore contains knowledge about the Entity's internal representation.
		//     This list should therefore rather be maintained in the Entity class.
		static $ignore = array(
			'language',
			'site',
			'type',
		);

		// @todo this text for filters stuff should be it's own class with test coverage!
		$codec = WikibaseRepo::getDefaultInstance()->getEntityContentDataCodec();
		$json = $codec->encodeEntity( $this->getEntity(), CONTENT_FORMAT_JSON );
		$data = json_decode( $json, true );

		$values = self::collectValues( $data, $ignore );

		return implode( "\n", $values );
	}

	/**
	 * Recursively collects values from nested arrays.
	 *
	 * @param array $data The array structure to process.
	 * @param array $ignore A list of keys to skip.
	 *
	 * @return array The values found in the array structure.
	 * @todo needs unit test
	 */
	protected static function collectValues( array $data, array $ignore = array() ) {
		$values = array();

		$erongi = array_flip( $ignore );
		foreach ( $data as $key => $value ) {
			if ( isset( $erongi[$key] ) ) {
				continue;
			}

			if ( is_array( $value ) ) {
				$values = array_merge( $values, self::collectValues( $value, $ignore ) );
			} else {
				$values[] = $value;
			}
		}

		return $values;
	}

	/**
	 * @return String the wikitext to include when another page includes this  content, or false if
	 *         the content is not includable in a wikitext page.
	 */
	public function getWikitextForTransclusion() {
		return false;
	}

	/**
	 * Returns a textual representation of the content suitable for use in edit summaries and log
	 * messages.
	 *
	 * @param int $maxLength maximum length of the summary text
	 * @return String the summary text
	 */
	public function getTextForSummary( $maxLength = 250 ) {
		if ( $this->isRedirect() ) {
			return $this->getRedirectText();
		}

		global $wgLang;
		$entity = $this->getEntity();

		// TODO use DescriptionProvider
		if ( $entity instanceof FingerprintProvider ) {
			$fingerprint = $entity->getFingerprint();
			$languageCode = $wgLang->getCode();

			if ( $fingerprint->hasDescription( $languageCode ) ) {
				$description = $fingerprint->getDescription( $languageCode )->getText();
				return substr( $description, 0, $maxLength );
			}
		}

		return '';
	}

	/**
	 * Returns an array structure for the redirect represented by this EntityContent, if any.
	 *
	 * @note This may or may not be consistent with what EntityContentCodec does.
	 *       It it intended to be used primarily for diffing.
	 */
	private function getRedirectData() {
		// NOTE: keep in sync with getPatchedRedirect
		$data = array(
			'entity' => $this->getEntityId()->getSerialization(),
		);

		if ( $this->isRedirect() ) {
			$data['redirect'] = $this->getEntityRedirect()->getTargetId()->getSerialization();
		}

		return $data;
	}

	/**
	 * @see Content::getNativeData
	 *
	 * @note Avoid relying on this method! It bypasses EntityContentCodec, and does
	 *       not make any guarantees about the structure of the array returned.
	 *
	 * @return array An undefined data structure representing the content. This is not guaranteed
	 *         to conform to any serialization structure used in the database or externally.
	 */
	public function getNativeData() {
		if ( $this->isRedirect() ) {
			return $this->getRedirectData();
		}

		// NOTE: this may or may not be consistent with what EntityContentCodec does!
		$serializer = WikibaseRepo::getDefaultInstance()->getEntitySerializer();
		return $serializer->serialize( $this->getEntity() );
	}

	/**
	 * returns the content's nominal size in bogo-bytes.
	 *
	 * @return int
	 */
	public function getSize() {
		return strlen( serialize( $this->getNativeData() ) );
	}

	/**
	 * Both contents will be considered equal if they have the same ID and equal Entity data. If
	 * one of the contents is considered "new", then matching IDs is not a criteria for them to be
	 * considered equal.
	 *
	 * @see Content::equals
	 */
	public function equals( Content $that = null ) {
		if ( $that === $this ) {
			return true;
		}

		if ( !( $that instanceof self ) || $that->getModel() !== $this->getModel() ) {
			return false;
		}

		/** @var Title $thisRedirect */
		$thisRedirect = $this->getRedirectTarget();
		$thatRedirect = $that->getRedirectTarget();

		if ( $thisRedirect !== null ) {
			if ( $thatRedirect === null ) {
				return false;
			} else {
				return $thisRedirect->equals( $thatRedirect )
					&& $this->getEntityRedirect()->equals( $that->getEntityRedirect() );
			}
		} elseif ( $thatRedirect !== null ) {
			return false;
		}

		$thisId = $this->getEntityHolder()->getEntityId();
		$thatId = $that->getEntityHolder()->getEntityId();

		if ( $thisId !== null && $thatId !== null
			&& !$thisId->equals( $thatId )
		) {
			return false;
		}

		$thisEntity = $this->getEntity();
		$thatEntity = $that->getEntity();

		return $thisEntity->equals( $thatEntity );
	}

	/**
	 * @return EntityDocument
	 */
	private function makeEmptyEntity() {
		/** @var EntityHandler $handler */
		$handler = $this->getContentHandler();
		return $handler->makeEmptyEntity();
	}

	/**
	 * Returns a diff between this EntityContent and the given EntityContent.
	 *
	 * @param EntityContent $toContent
	 *
	 * @return EntityContentDiff
	 */
	public function getDiff( EntityContent $toContent ) {
		$fromContent = $this;

		$differ = new MapDiffer();
		$redirectDiffOps = $differ->doDiff(
			$fromContent->getRedirectData(),
			$toContent->getRedirectData()
		);

		$redirectDiff = new Diff( $redirectDiffOps, true );

		$fromEntity = $fromContent->isRedirect() ? $this->makeEmptyEntity() : $fromContent->getEntity();
		$toEntity = $toContent->isRedirect() ? $this->makeEmptyEntity() : $toContent->getEntity();

		$entityDiffer = new EntityDiffer();
		$entityDiff = $entityDiffer->diffEntities( $fromEntity, $toEntity );

		return new EntityContentDiff( $entityDiff, $redirectDiff );
	}

	/**
	 * Returns a patched copy of this Content object.
	 *
	 * @param EntityContentDiff $patch
	 *
	 * @throws PatcherException
	 * @return EntityContent
	 */
	public function getPatchedCopy( EntityContentDiff $patch ) {
		/* @var EntityHandler $handler */
		$handler = $this->getContentHandler();

		if ( $this->isRedirect() ) {
			$entityAfterPatch = $this->makeEmptyEntity();
		} else {
			$entityAfterPatch = $this->getEntity()->copy();
		}

		// FIXME: this should either be done in the derivatives, or the patcher
		// should be injected, so the application can add support for additional entity types.
		$patcher = new EntityPatcher();
		$patcher->patchEntity( $entityAfterPatch, $patch->getEntityDiff() );

		$redirAfterPatch = $this->getPatchedRedirect( $patch->getRedirectDiff() );

		if ( $redirAfterPatch !== null && !$entityAfterPatch->isEmpty() ) {
			throw new PatcherException( 'EntityContent must not contain Entity data as well as'
				. ' a redirect after applying the patch!' );
		} elseif ( $redirAfterPatch ) {
			$patched = $handler->makeEntityRedirectContent( $redirAfterPatch );

			if ( !$patched ) {
				throw new PatcherException( 'Cannot create a redirect using content model '
					. $this->getModel() . '!' );
			}
		} else {
			$patched = $handler->makeEntityContent( new EntityInstanceHolder( $entityAfterPatch ) );
		}

		return $patched;
	}

	/**
	 * @param Diff $redirectPatch
	 *
	 * @return EntityRedirect|null
	 */
	private function getPatchedRedirect( Diff $redirectPatch ) {
		// See getRedirectData() for the structure of the data array.
		$redirData = $this->getRedirectData();

		if ( !$redirectPatch->isEmpty() ) {
			$patcher = new MapPatcher();
			$redirData = $patcher->patch( $redirData, $redirectPatch );
		}

		if ( isset( $redirData['redirect'] ) ) {
			/* @var EntityHandler $handler */
			$handler = $this->getContentHandler();

			$entityId = $this->getEntityId();
			$targetId = $handler->makeEntityId( $redirData['redirect'] );

			return new EntityRedirect( $entityId, $targetId );
		} else {
			return null;
		}
	}

	/**
	 * @return bool True if this is not a redirect and the page is empty.
	 */
	public function isEmpty() {
		return !$this->isRedirect() && parent::isEmpty();
	}

	/**
	 * @return bool
	 */
	abstract public function isStub();

	/**
	 * @see Content::copy
	 *
	 * @return ItemContent
	 */
	public function copy() {
		/* @var EntityHandler $handler */
		$handler = $this->getContentHandler();
		if ( $this->isRedirect() ) {
			return $handler->makeEntityRedirectContent( $this->getEntityRedirect() );
		} else {
			return $handler->makeEntityContent( new DeferredCopyEntityHolder( $this->getEntityHolder() ) );
		}
	}

	/**
	 * @see Content::prepareSave
	 *
	 * @param WikiPage $page
	 * @param int $flags
	 * @param int $baseRevId
	 * @param User $user
	 *
	 * @return Status
	 */
	public function prepareSave( WikiPage $page, $flags, $baseRevId, User $user ) {
		// Chain to parent
		$status = parent::prepareSave( $page, $flags, $baseRevId, $user );

		if ( $status->isOK() ) {
			if ( !$this->isRedirect() && ( $flags & self::EDIT_IGNORE_CONSTRAINTS ) === 0 ) {
				/* @var EntityHandler $handler */
				$handler = $this->getContentHandler();
				$validators = $handler->getOnSaveValidators( ( $flags & EDIT_NEW ) > 0 );
				$status = $this->applyValidators( $validators );
			}
		}

		return $status;
	}

	/**
	 * Apply the given validators.
	 *
	 * @param EntityValidator[] $validators
	 *
	 * @return Result
	 */
	private function applyValidators( array $validators ) {
		$result = Result::newSuccess();

		/* @var EntityValidator $validator */
		foreach ( $validators as $validator ) {
			$result = $validator->validateEntity( $this->getEntity() );

			if ( !$result->isValid() ) {
				break;
			}
		}

		/* @var EntityHandler $handler */
		$handler = $this->getContentHandler();
		$status = $handler->getValidationErrorLocalizer()->getResultStatus( $result );
		return $status;
	}

	/**
	 * Registers any properties returned by getEntityPageProperties()
	 * in $output.
	 *
	 * @param ParserOutput $output
	 */
	private function applyEntityPageProperties( ParserOutput $output ) {
		if ( $this->isRedirect() ) {
			return;
		}

		$properties = $this->getEntityPageProperties();
		foreach ( $properties as $name => $value ) {
			$output->setProperty( $name, $value );
		}
	}

	/**
	 * Returns a map of properties about the entity, to be recorded in
	 * MediaWiki's page_props table. The idea is to allow efficient lookups
	 * of entities based on such properties.
	 *
	 * @see getEntityStatus()
	 *
	 * Records the entity's status in the 'wb-status' key.
	 *
	 * @return array A map from property names to property values.
	 */
	public function getEntityPageProperties() {
		$properties = array();

		$status = $this->getEntityStatus();
		if ( $status !== self::STATUS_NONE ) {
			$properties['wb-status'] = $status;
		}

		return $properties;
	}

	/**
	 * Returns an identifier representing the status of the entity,
	 * e.g. STATUS_EMPTY or STATUS_NONE.
	 * Used by getEntityPageProperties().
	 *
	 * @note Will fail if this EntityContent is a redirect.
	 *
	 * @see getEntityPageProperties()
	 * @see EntityContent::STATUS_NONE
	 * @see EntityContent::STATUS_STUB
	 * @see EntityContent::STATUS_EMPTY
	 *
	 * @return int
	 */
	public function getEntityStatus() {
		if ( $this->isEmpty() ) {
			return self::STATUS_EMPTY;
		} elseif ( $this->isStub() ) {
			return self::STATUS_STUB;
		} else {
			return self::STATUS_NONE;
		}
	}

}