Current File : /home/jvzmxxx/wiki1/extensions/Flow/includes/Model/AbstractRevision.php
<?php

namespace Flow\Model;

use Flow\Collection\AbstractCollection;
use Flow\Exception\DataModelException;
use Flow\Exception\InvalidDataException;
use Flow\Exception\PermissionException;
use Flow\Conversion\Utils;
use ContentHandler;
use Hooks;
use Title;
use User;
use RecentChange;
use WikiPage;

abstract class AbstractRevision {
	const MODERATED_NONE = '';
	const MODERATED_HIDDEN = 'hide';
	const MODERATED_DELETED = 'delete';
	const MODERATED_SUPPRESSED = 'suppress';
	const MODERATED_LOCKED = 'lock';

	/**
	 * List of available permission levels.
	 *
	 * @var string[]
	 **/
	static public $perms = array(
		self::MODERATED_NONE,
		self::MODERATED_HIDDEN,
		self::MODERATED_DELETED,
		self::MODERATED_SUPPRESSED,
		self::MODERATED_LOCKED,
	);

	/**
	 * List of moderation change types
	 *
	 * @var array|null
	 */
	static protected $moderationChangeTypes = null;

	/**
	 * @var UUID
	 */
	protected $revId;

	/**
	 * @var UserTuple
	 */
	protected $user;

	/**
	 * Array of flags strictly related to the content. Flags are reset when
	 * content changes.
	 *
	 * @var string[]
	 */
	protected $flags = array();

	/**
	 * Name of the action performed that generated this revision.
	 *
	 * @see FlowActions.php
	 * @var string
	 */
	protected $changeType;

	/**
	 * @var UUID|null The id of the revision prior to this one, or null if this is first revision
	 */
	protected $prevRevision;

	/**
	 * @var string Raw content of revision
	 */
	protected $content;

	/**
	 * @var string|null Only populated when external store is in use
	 */
	protected $contentUrl;

	/**
	 * @var string|null This is decompressed on-demand from $this->content in self::getContent()
	 */
	protected $decompressedContent;

	/**
	 * @var string[] Converted (wikitext|html) content, based off of $this->decompressedContent
	 */
	protected $convertedContent = array();

	/**
	 * html content has been allowed by the xss check.  When we find the next xss
	 * in the parser this hook allows preventing any display of hostile html. True
	 * means the content is allowed. False means not allowed. Null means unchecked
	 *
	 * @var boolean
	 */
	protected $xssCheck;

	/**
	 * moderation states for the revision.  This is technically denormalized data
	 * since it can be overwritten and does not provide a full history.
	 * The tricky part is updating moderation is a new revision for hide and
	 * delete, but adjusts an existing revision for full suppression.
	 *
	 * @var string
	 */
	protected $moderationState = self::MODERATED_NONE;

	/**
	 * @var string|null
	 */
	protected $moderationTimestamp;

	/**
	 * @var UserTuple|null
	 */
	protected $moderatedBy;

	/**
	 * @var string|null
	 */
	protected $moderatedReason;

	/**
	 * @var UUID|null The id of the last content edit revision
	 */
	protected $lastEditId;

	/**
	 * @var UserTuple|null
	 */
	protected $lastEditUser;

	/**
	 * @var integer Size of previous revision wikitext
	 */
	protected $previousContentLength = 0;

	/**
	 * @var integer Size of current revision wikitext
	 */
	protected $contentLength = 0;

	/**
	 * Author of the first revision
	 *
	 * @var UserTuple
	 */
	protected $creator;

	/**
	 * @param string[] $row
	 * @param AbstractRevision|null $obj
	 * @return AbstractRevision
	 * @throws DataModelException
	 */
	static public function fromStorageRow( array $row, $obj = null ) {
		if ( $obj === null ) {
			/** @var AbstractRevision $obj */
			$obj = new static;
		} elseif ( !$obj instanceof static ) {
			throw new DataModelException( 'wrong object type', 'process-data' );
		}
		$obj->revId = UUID::create( $row['rev_id'] );
		$obj->user = UserTuple::newFromArray( $row, 'rev_user_' );
		if ( $obj->user === null ) {
			throw new DataModelException( 'Could not load UserTuple for rev_user_' );
		}
		$obj->prevRevision = $row['rev_parent_id'] ? UUID::create( $row['rev_parent_id'] ) : null;
		$obj->changeType = $row['rev_change_type'];
	 	$obj->flags = array_filter( explode( ',', $row['rev_flags'] ) );
		$obj->content = $row['rev_content'];
		// null if external store is not being used
		$obj->contentUrl = isset( $row['rev_content_url'] ) ? $row['rev_content_url'] : null;
		$obj->decompressedContent = null;

		$obj->moderationState = $row['rev_mod_state'];
		$obj->moderatedBy = UserTuple::newFromArray( $row, 'rev_mod_user_' );
		$obj->moderationTimestamp = $row['rev_mod_timestamp'] ?: null;
		$obj->moderatedReason = isset( $row['rev_mod_reason'] ) && $row['rev_mod_reason'] ? $row['rev_mod_reason'] : null;

		// BC: 'suppress' used to be called 'censor' & 'lock' was 'close'
		$bc = array(
			'censor' => AbstractRevision::MODERATED_SUPPRESSED,
			'close' => AbstractRevision::MODERATED_LOCKED,
		);
		$obj->moderationState = str_replace( array_keys( $bc ), array_values( $bc ), $obj->moderationState );

		// isset required because there is a possible db migration, cached data will not have it
		$obj->lastEditId = isset( $row['rev_last_edit_id'] ) && $row['rev_last_edit_id'] ? UUID::create( $row['rev_last_edit_id'] ) : null;
		$obj->lastEditUser = UserTuple::newFromArray( $row, 'rev_edit_user_' );

		$obj->contentLength = isset( $row['rev_content_length'] ) ? $row['rev_content_length'] : 0;
		$obj->previousContentLength = isset( $row['rev_previous_content_length'] ) ? $row['rev_previous_content_length'] : 0;

		return $obj;
	}

	/**
	 * @param AbstractRevision $obj
	 * @return string[]
	 */
	static public function toStorageRow( $obj ) {
		return array(
			'rev_id' => $obj->revId->getAlphadecimal(),
			'rev_user_id' => $obj->user->id,
			'rev_user_ip' => $obj->user->ip,
			'rev_user_wiki' => $obj->user->wiki,
			'rev_parent_id' => $obj->prevRevision ? $obj->prevRevision->getAlphadecimal() : null,
			'rev_change_type' => $obj->changeType,
			'rev_type' => $obj->getRevisionType(),
			'rev_type_id' => $obj->getCollectionId()->getAlphadecimal(),

			'rev_content' => $obj->content,
			'rev_content_url' => $obj->contentUrl,
			'rev_flags' => implode( ',', $obj->flags ),

			'rev_mod_state' => $obj->moderationState,
			'rev_mod_user_id' => $obj->moderatedBy ? $obj->moderatedBy->id : null,
			'rev_mod_user_ip' => $obj->moderatedBy ? $obj->moderatedBy->ip : null,
			'rev_mod_user_wiki' => $obj->moderatedBy ? $obj->moderatedBy->wiki : null,
			'rev_mod_timestamp' => $obj->moderationTimestamp,
			'rev_mod_reason' => $obj->moderatedReason,

			'rev_last_edit_id' => $obj->lastEditId ? $obj->lastEditId->getAlphadecimal() : null,
			'rev_edit_user_id' => $obj->lastEditUser ? $obj->lastEditUser->id : null,
			'rev_edit_user_ip' => $obj->lastEditUser ? $obj->lastEditUser->ip : null,
			'rev_edit_user_wiki' => $obj->lastEditUser ? $obj->lastEditUser->wiki : null,

			'rev_content_length' => $obj->contentLength,
			'rev_previous_content_length' => $obj->previousContentLength,
		);
	}

	/**
	 * NOTE: No guarantee is made here regarding if $this is the newest revision.  Validation
	 * must happen externally.  DB *will* throw an exception if this attempts to write to db
	 * and it is not the most recent revision.
	 *
	 * @param User $user
	 * @return AbstractRevision
	 * @throws PermissionException
	 */
	public function newNullRevision( User $user ) {
		if ( !$user->isAllowed( 'edit' ) ) {
			throw new PermissionException( 'User does not have core edit permission', 'insufficient-permission' );
		}
		$obj = clone $this;
		$obj->revId = UUID::create();
		$obj->user = UserTuple::newFromUser( $user );
		$obj->prevRevision = $this->revId;
		$obj->changeType = '';
		$obj->previousContentLength = $obj->contentLength;

		return $obj;
	}

	/**
	 * Create the next revision with new content
	 * or return itself when content is the same
	 *
	 * @param User $user
	 * @param string $content
	 * @param string $format wikitext|html
	 * @param string $changeType
	 * @param Title $title The article title of the related workflow
	 * @return AbstractRevision
	 */
	public function newNextRevision( User $user, $content, $format, $changeType, Title $title ) {
		$obj = $this->newNullRevision( $user );
		$obj->setNextContent( $user, $content, $format, $title );
		$obj->changeType = $changeType;
		return $this->hasSameContentAs( $obj ) ? $this : $obj;
	}

	/**
	 * @param User $user
	 * @param string $state
	 * @param string $changeType
	 * @param string $reason
	 * @return AbstractRevision
	 */
	public function moderate( User $user, $state, $changeType, $reason ) {
		if ( ! $this->isValidModerationState( $state ) ) {
			wfWarn( __METHOD__ . ': Provided moderation state does not exist : ' . $state );
			return null;
		}

		$obj = $this->newNullRevision( $user );
		$obj->changeType = $changeType;

		// This is a bit hacky, but we store the restore reason
		// in the "moderated reason" field. Hmmph.
		$obj->moderatedReason = $reason;
		$obj->moderationState = $state;

		if ( $state === self::MODERATED_NONE ) {
			$obj->moderatedBy = null;
			$obj->moderationTimestamp = null;
		} else {
			$obj->moderatedBy = UserTuple::newFromUser( $user );
			$obj->moderationTimestamp = $obj->revId->getTimestamp();
		}

		// all moderation levels past lock report a size of 0
		if ( $obj->isModerated() && !$obj->isLocked() ) {
			$obj->contentLength = 0;
		} else {
			// reset content length (we may be restoring, in which case $obj's
			// current length will be 0)
			$obj->contentLength = $this->calculateContentLength();
		}

		return $obj;
	}

	/**
	 * @param string $state
	 * @return boolean
	 */
	public function isValidModerationState( $state ) {
		return in_array( $state, self::$perms );
	}

	/**
	 * @return UUID
	 */
	public function getRevisionId() {
		return $this->revId;
	}

	/**
	 * @return boolean
	 */
	public function hasHiddenContent() {
		return $this->moderationState === self::MODERATED_HIDDEN;
	}

	/**
	 * @return string
	 */
	public function getContentRaw() {
		if ( $this->decompressedContent === null ) {
			$this->decompressedContent = \Revision::decompressRevisionText( $this->content, $this->flags );
		}

		return $this->decompressedContent;
	}

	/**
	 * DO NOT USE THIS METHOD to output the content; use
	 * Templating::getContent, which will do additional (permissions-based)
	 * checks to make sure it outputs something the user can see.
	 *
	 * @param string[optional] $format Format to output content in (html|wikitext|topic-title-wikitext|topic-title-html|topic-title-plaintext)
	 * @return string
	 * @throws InvalidDataException
	 */
	public function getContent( $format = 'html' ) {
		if ( $this->content === false ) {
			throw new InvalidDataException( 'Failed to load the content' );
		}
		if ( $this->xssCheck === false ) {
			return '';
		}
		$raw = $this->getContentRaw();
		$sourceFormat = $this->getContentFormat();
		if ( $this->xssCheck === null && $sourceFormat === 'html' ) {
			// returns true if no handler aborted the hook
			$this->xssCheck = Hooks::run( 'FlowCheckHtmlContentXss', array( $raw ) );
			if ( !$this->xssCheck ) {
				wfDebugLog( 'Flow', __METHOD__ . ': XSS check prevented display of revision ' . $this->revId->getAlphadecimal() );
				return '';
			}
		}

		if ( !isset( $this->convertedContent[$format] ) ) {
			if ( $sourceFormat === $format ) {
				$this->convertedContent[$format] = $raw;
			} else {
				$this->convertedContent[$format] = Utils::convert(
					$sourceFormat,
					$format,
					$raw,
					$this->getCollection()->getTitle()
				);
			}
		}

		return $this->convertedContent[$format];
	}

	/**
	 * Gets the content in a wikitext format.  In this class, it will be 'wikitext',
	 * but this can be overriden in sub-classes (e.g. to 'topic-title-wikitext' for topic titles).
	 *
	 * DO NOT USE THIS METHOD to output the content; use Templating::getContent for security reasons.
	 *
	 * @return string Text in a wikitext-based format.
	 */
	public function getContentInWikitext() {
		return $this->getContent( $this->getWikitextFormat() );
	}

	/**
	 * Gets a wikitext format that is suitable for this revision.
	 * In this class, it will be 'wikitext', but this can be overriden in sub-classes
	 * (e.g. to 'topic-title-wikitext' for topic titles).
	 *
	 * @return string Format name
	 */
	public function getWikitextFormat() {
		return 'wikitext';
	}

	/**
	 * Gets the content in an HTML format.  In this class, it will be 'html',
	 * but this can be overriden in sub-classes (e.g. to 'topic-title-html' for topic titles).
	 *
	 * DO NOT USE THIS METHOD to output the content; use Templating::getContent for security reasons.
	 *
	 * @return string Text in an HTML-based format.
	 */
	public function getContentInHtml() {
		return $this->getContent( $this->getHtmlFormat() );
	}

	/**
	 * Gets an HTML format that is suitable for this revision.
	 * In this class, it will be 'html', but this can be overriden in sub-classes
	 * (e.g. to 'topic-title-html' for topic titles).
	 *
	 * @return string Format name
	 */
	public function getHtmlFormat() {
		return 'html';
	}

	/**
	 * @return UserTuple
	 */
	public function getUserTuple() {
		return $this->user;
	}

	/**
	 * @return integer
	 */
	public function getUserId() {
		return $this->user->id;
	}

	/**
	 * @return string|null
	 */
	public function getUserIp() {
		return $this->user->ip;
	}

	/**
	 * @return string
	 */
	public function getUserWiki() {
		return $this->user->wiki;
	}

	/**
	 * @return User
	 */
	public function getUser() {
		return $this->user->createUser();
	}

	/**
	 * Should only be used for setting the initial content.  To set subsequent content
	 * use self::setNextContent
	 *
	 * @param string $content
	 * @param string $format wikitext|html|topic-title-wikitext
	 * @param Title|null $title When null the related workflow will be lazy-loaded to locate the title
	 * @throws DataModelException
	 */
	protected function setContent( $content, $format, Title $title = null ) {
		if ( $this->moderationState !== self::MODERATED_NONE ) {
			throw new DataModelException( 'TODO: Cannot change content of restricted revision', 'process-data' );
		}

		if ( $this->content !== null ) {
			throw new DataModelException( 'Updating content must use setNextContent method', 'process-data' );
		}

		if ( !$title ) {
			$title = $this->getCollection()->getTitle();
		}

		if ( $format !== 'wikitext' && $format !== 'html' && $format !== 'topic-title-wikitext' ) {
			throw new DataModelException( 'Invalid format: Supported formats for new content are \'wikitext\', \'html\', and \'topic-title-wikitext\'' );
		}

		// never trust incoming html - roundtrip to wikitext first
		if ( $format === 'html' ) {
			$content = Utils::convert( $format, 'wikitext', $content, $title  );
			$format = 'wikitext';
		}

		if ( $format === 'wikitext' ) {
			// Run pre-save transform
			$content = ContentHandler::makeContent( $content, $title, CONTENT_MODEL_WIKITEXT )
				->preSaveTransform(
					$title,
					$this->getUser(),
					WikiPage::factory( $title )->makeParserOptions( $this->getUser() )
				)
				->serialize( 'text/x-wiki' );
		}

		// Keep consistent with normal edit page, trim only trailing whitespaces
		$content = rtrim( $content );
		$this->convertedContent = array( $format => $content );

		// convert content to desired storage format
		$storageFormat = $this->getStorageFormat();
		if ( $storageFormat !== $format ) {
			$this->convertedContent[$storageFormat] = Utils::convert( $format, $storageFormat, $content, $title );
		}

		$this->content = $this->decompressedContent = $this->convertedContent[$storageFormat];
		$this->contentUrl = null;

		// should this only remove a subset of flags?
		$this->flags = array_filter( explode( ',', \Revision::compressRevisionText( $this->content ) ) );
		$this->flags[] = $storageFormat;

		$this->contentLength = $this->calculateContentLength();
	}

	/**
	 * Apply new content to a revision.
	 *
	 * @param User $user
	 * @param string $content
	 * @param string $format wikitext|html|topic-title-wikitext
	 * @param Title|null $title When null the related workflow will be lazy-loaded to locate the title
	 * @throws DataModelException
	 */
	protected function setNextContent( User $user, $content, $format, Title $title = null ) {
		if ( $this->moderationState !== self::MODERATED_NONE ) {
			throw new DataModelException( 'Cannot change content of restricted revision', 'process-data' );
		}

		// Do we need this if check, or just the one in newNextRevision against the prior revision?
		if ( $content !== $this->getContent( $format ) ) {
			$this->content = null;
			$this->setContent( $content, $format, $title );
			$this->lastEditId = $this->getRevisionId();
			$this->lastEditUser = UserTuple::newFromUser( $user );
		}
	}

	/**
	 * @return string The content format of this revision
	 */
	public function getContentFormat() {
		return in_array( 'html', $this->flags ) ? 'html' : 'wikitext';
	}

	/**
	 * Determines the appropriate format to store content in.
	 * NOTE: The format of the current content is retrieved with getContentFormat
	 *
	 * @return string The name of the storage format.
	 */
	protected function getStorageFormat() {
		global $wgFlowContentFormat;

		return $wgFlowContentFormat;
	}

	/**
	 * @return UUID|null
	 */
	public function getPrevRevisionId() {
		return $this->prevRevision;
	}

	/**
	 * @return string
	 */
	public function getChangeType() {
		return $this->changeType;
	}

	/**
	 * @return string
	 */
	public function getModerationState() {
		return $this->moderationState;
	}

	/**
	 * @return string|null
	 */
	public function getModeratedReason() {
		return $this->moderatedReason;
	}

	/**
	 * @return boolean
	 */
	public function isModerated() {
		return $this->moderationState !== self::MODERATED_NONE;
	}

	/**
	 * @return boolean
	 */
	public function isHidden() {
		return $this->moderationState === self::MODERATED_HIDDEN;
	}

	/**
	 * @return boolean
	 */
	public function isDeleted() {
		return $this->moderationState === self::MODERATED_DELETED;
	}

	/**
	 * @return boolean
	 */
	public function isSuppressed() {
		return $this->moderationState === self::MODERATED_SUPPRESSED;
	}

	/**
	 * @return boolean
	 */
	public function isLocked() {
		return $this->moderationState === self::MODERATED_LOCKED;
	}

	/**
	 * @return string|null Timestamp in TS_MW format
	 */
	public function getModerationTimestamp() {
		return $this->moderationTimestamp;
	}

	/**
	 * @param string|array $flags
	 * @return boolean True when at least one flag in $flags is set
	 */
	public function isFlaggedAny( $flags ) {
		foreach ( (array) $flags as $flag ) {
			if ( false !== array_search( $flag, $this->flags ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * @param string|array $flags
	 * @return boolean
	 */
	public function isFlaggedAll( $flags ) {
		foreach ( (array) $flags as $flag ) {
			if ( false === array_search( $flag, $this->flags ) ) {
				return false;
			}
		}
		return true;
	}

	/**
	 * @return boolean
	 */
	public function isFirstRevision() {
		return $this->prevRevision === null;
	}

	/**
	 * @return boolean
	 */
	public function isOriginalContent() {
		return $this->lastEditId === null;
	}

	/**
	 * @return UUID
	 */
	public function getLastContentEditId() {
		return $this->lastEditId;
	}

	/**
	 * @return UserTuple
	 */
	public function getLastContentEditUserTuple() {
		return $this->lastEditUser;
	}

	/**
	 * @return integer
	 */
	public function getLastContentEditUserId() {
		return $this->lastEditUser ? $this->lastEditUser->id : null;
	}

	/**
	 * @return string|null
	 */
	public function getLastContentEditUserIp() {
		return $this->lastEditUser ? $this->lastEditUser->ip : null;
	}

	/**
	 * @return string|null
	 */
	public function getLastContentEditUserWiki() {
		return $this->lastEditUser ? $this->lastEditUser->wiki : null;
	}

	/**
	 * @return UserTuple
	 */
	public function getModeratedByTuple() {
		return $this->moderatedBy;
	}

	/**
	 * @return integer|null
	 */
	public function getModeratedByUserId() {
		return $this->moderatedBy ? $this->moderatedBy->id : null;
	}

	/**
	 * @return string|null
	 */
	public function getModeratedByUserIp() {
		return $this->moderatedBy ? $this->moderatedBy->ip : null;
	}

	/**
	 * @return string|null
	 */
	public function getModeratedByUserWiki() {
		return $this->moderatedBy ? $this->moderatedBy->wiki : null;
	}

	public static function getModerationChangeTypes() {
		if ( self::$moderationChangeTypes === null ) {
			self::$moderationChangeTypes = array();
			foreach( self::$perms as $perm ) {
				if ( $perm != '' ) {
					self::$moderationChangeTypes[] = "{$perm}-topic";
					self::$moderationChangeTypes[] = "{$perm}-post";
				}
			}

			self::$moderationChangeTypes[] = 'restore-topic';
			self::$moderationChangeTypes[] = 'restore-post';
		}

		return self::$moderationChangeTypes;
	}

	public function isModerationChange() {
		 return in_array( $this->getChangeType(), self::getModerationChangeTypes() );
	}

	/**
	 * @return integer
	 */
	public function getContentLength() {
		return $this->contentLength;
	}

	// Only public for FlowUpdateRevisionContentLength.
	/**
	 * Determines the content length by measuring the actual content.
	 *
	 * @return integer
	 */
	public function calculateContentLength() {
		return mb_strlen( $this->getContentInWikitext() );
	}

	/**
	 * @return integer
	 */
	public function getPreviousContentLength() {
		return $this->previousContentLength;
	}

	/**
	 * Finds the RecentChange object associated with this flow revision.
	 *
	 * @return null|RecentChange
	 */
	public function getRecentChange() {
		$timestamp = $this->revId->getTimestamp();

		if ( !RecentChange::isInRCLifespan( $timestamp ) ) {
			// Too old to be in RC, don't even bother checking
			return null;
		}
		$workflow = $this->getCollection()->getWorkflow();
		if ( $this->changeType === 'new-post' ) {
			$title = $workflow->getOwnerTitle();
		} else {
			$title = $workflow->getArticleTitle();
		}
		$namespace = $title->getNamespace();

		$conditions = array(
			'rc_title' => $title->getDBkey(),
			'rc_timestamp' => $timestamp,
			'rc_namespace' => $namespace
		);
		$options = array( 'USE INDEX' => 'rc_timestamp' );

		$dbr = wfGetDB( DB_SLAVE );
		$rows = $dbr->select( 'recentchanges', RecentChange::selectFields(), $conditions, __METHOD__, $options );

		if ( $rows === false ) {
			return null;
		}

		if ( $rows->numRows() === 1 ) {
			return RecentChange::newFromRow( $rows->fetchObject() );
		}

		// it is possible that more than 1 changes on the same page have the same timestamp
		// the revision id is hidden in rc_params['flow-workflow-change']['revision']
		$revId = $this->revId->getAlphadecimal();
		while ( $row = $rows->next() ) {
			$rc = RecentChange::newFromRow( $row );
			$params = $rc->parseParams();
			if ( $params && $params['flow-workflow-change']['revision'] === $revId ) {
				return $rc;
			}
		}

		return null;
	}

	/**
	 * @return UserTuple
	 */
	public function getCreatorTuple() {
		if ( !$this->creator ) {
			if ( $this->isFirstRevision() ) {
				$this->creator = $this->user;
			} else {
				$this->creator = $this->getCollection()->getFirstRevision()->getUserTuple();
			}
		}

		return $this->creator;
	}

	/**
	 * Get the user ID of the user who created this summary.
	 *
	 * @return integer The user ID
	 */
	public function getCreatorId() {
		return $this->getCreatorTuple()->id;
	}

	/**
	 * @return string
	 */
	public function getCreatorWiki() {
		return $this->getCreatorTuple()->wiki;
	}

	/**
	 * Get the user ip of the user who created this summary if it
	 * was created by an anonymous user
	 *
	 * @return string|null String if an creator is anon, or null if not.
	 */
	public function getCreatorIp() {
		return $this->getCreatorTuple()->ip;
	}

	/**
	 * @param AbstractRevision $revision
	 * @return bool
	 * @throws InvalidDataException
	 */
	protected function hasSameContentAs( AbstractRevision $revision ) {
		return $this->getContentInWikitext() === $revision->getContentInWikitext();
	}

	/**
	 * @return string
	 */
	abstract public function getRevisionType();

	/**
	 * @return UUID
	 */
	abstract public function getCollectionId();

	/**
	 * @return AbstractCollection
	 */
	abstract public function getCollection();
}