Current File : /home/jvzmxxx/wiki1/extensions/Flow/includes/Formatter/AbstractQuery.php
<?php

namespace Flow\Formatter;

use Flow\Data\ManagerGroup;
use Flow\Exception\FlowException;
use Flow\Model\AbstractRevision;
use Flow\Model\Header;
use Flow\Model\PostRevision;
use Flow\Model\PostSummary;
use Flow\Model\UUID;
use Flow\Model\Workflow;
use Flow\Repository\TreeRepository;
use ResultWrapper;

/**
 * Base class that collects the data necessary to utilize AbstractFormatter
 * based on a list of revisions. In some cases formatters will not utilize
 * this query and will instead receive data from a table such as the core
 * recentchanges.
 */
abstract class AbstractQuery {
	/**
	 * @var ManagerGroup
	 */
	protected $storage;

	/**
	 * @var TreeRepository
	 */
	protected $treeRepository;

	// Consider converting these in-process caches to MapCacheLRU to avoid
	// memory leaks.  Should only be an issue if a batch is repeatedly doing queries.
	/**
	 * @var UUID[] Associative array of post ID to root post's UUID object.
	 */
	protected $rootPostIdCache = array();

	/**
	 * @var PostRevision[] Associative array of post ID to PostRevision object.
	 */
	protected $postCache = array();

	/**
	 * @var AbstractRevision[] Associative array of revision ID to AbstractRevision object
	 */
	protected $revisionCache = array();

	/**
	 * @var Workflow[] Associative array of workflow ID to Workflow object.
	 */
	protected $workflowCache = array();

	/**
	 * Array of collection ids mapping to their most recent revision ids.
	 *
	 * @var UUID[]
	 */
	protected $currentRevisionsCache = array();

	protected $identityMap = array();

	/**
	 * @param ManagerGroup $storage
	 * @param TreeRepository $treeRepository
	 */
	public function __construct( ManagerGroup $storage, TreeRepository $treeRepository ) {
		$this->storage = $storage;
		$this->treeRepository = $treeRepository;
	}

	/**
	 * Entry point for batch loading metadata for a variety of revisions
	 * into the internal cache.
	 *
	 * @param AbstractRevision[]|ResultWrapper $results
	 */
	protected function loadMetadataBatch( $results ) {
		// Batch load data related to a list of revisions
		$postIds = array();
		$workflowIds = array();
		$revisions = array();
		$previousRevisionIds = array();
		$collectionIds = array();
		foreach( $results as $result ) {
			if ( $result instanceof PostRevision ) {
				// If top-level, then just get the workflow.
				// Otherwise we need to find the root post.
				$id = $result->getPostId();
				$alpha = $id->getAlphadecimal();
				if ( $result->isTopicTitle() ) {
					$workflowIds[] = $id;
				} else {
					$postIds[$alpha] = $id;
				}
				$this->postCache[$alpha] = $result;
			} elseif ( $result instanceof Header ) {
				$workflowIds[] = $result->getWorkflowId();
			} elseif ( $result instanceof PostSummary ) {
				// This would be the post id for the summary
				$id = $result->getSummaryTargetId();
				$postIds[$id->getAlphadecimal()] = $id;
			}

			$revisions[$result->getRevisionId()->getAlphadecimal()] = $result;
			if ( $this->needsPreviousRevision( $result ) ) {
				$previousRevisionIds[get_class( $result )][] = $result->getPrevRevisionId();
			}

			$collection = $result->getCollection();
			$collectionIds[get_class( $result )][] = $collection->getId();
		}

		// map from post Id to the related root post id
		$rootPostIds = array_filter( $this->treeRepository->findRoots( $postIds ) );
		$rootPostRequests = array();
		foreach( $rootPostIds as $postId ) {
			$rootPostRequests[] = array( 'rev_type_id' => $postId );
		}

		// these tree identity maps are required for determining where a reply goes when
		//
		// replying to a specific post.
		$identityMap = $this->treeRepository->fetchSubtreeIdentityMap(
			array_unique( $rootPostIds, SORT_REGULAR )
		);

		$rootPostResult = $this->storage->findMulti(
			'PostRevision',
			$rootPostRequests,
			array(
				'SORT' => 'rev_id',
				'ORDER' => 'DESC',
				'LIMIT' => 1,
			)
		);

		$rootPosts = array();
		if ( count( $rootPostResult ) > 0 ) {
			foreach ( $rootPostResult as $found ) {
				$root = reset( $found );
				$rootPosts[$root->getPostId()->getAlphadecimal()] = $root;
				$revisions[$root->getRevisionId()->getAlphadecimal()] = $root;
			}
		}

		// Workflow IDs are the same as root post IDs
		// So any post IDs that *are* root posts + found root post IDs + header workflow IDs
		// should cover the lot.
		$workflows = $this->storage->getMulti( 'Workflow', array_merge( $rootPostIds, $workflowIds ) );
		$workflows = $workflows ?: array();

		// preload all requested previous revisions
		foreach ( $previousRevisionIds as $revisionType => $ids ) {
			// get rid of null-values (for original revisions, without previous revision)
			$ids = array_filter( $ids );
			/** @var AbstractRevision[] $found */
			$found = $this->storage->getMulti( $revisionType, $ids );
			foreach ( $found as $rev ) {
				$revisions[$rev->getRevisionId()->getAlphadecimal()] = $rev;
			}
		}

		// preload all current versions
		foreach ( $collectionIds as $revisionType => $ids ) {
			$queries = array();
			foreach ( $ids as $uuid ) {
				$queries[] = array( 'rev_type_id' => $uuid );
			}

			$found = $this->storage->findMulti( $revisionType,
				$queries,
				array( 'sort' => 'rev_id', 'order' => 'DESC', 'limit' => 1 )
			);

			/** @var AbstractRevision[] $result */
			foreach ( $found as $result ) {
				$rev = reset( $result );
				$cacheKey = $this->getCurrentRevisionCacheKey( $rev );
				$this->currentRevisionsCache[$cacheKey] = $rev->getRevisionId();
				$revisions[$rev->getRevisionId()->getAlphadecimal()] = $rev;
			}
		}

		$this->revisionCache = array_merge( $this->revisionCache, $revisions );
		$this->postCache = array_merge( $this->postCache, $rootPosts );
		$this->rootPostIdCache = array_merge( $this->rootPostIdCache, $rootPostIds );
		$this->workflowCache = array_merge( $this->workflowCache, $workflows );
		$this->identityMap = array_merge( $this->identityMap, $identityMap );
	}

	/**
	 * Build a stdClass object that contains all related data models necessary
	 * for rendering a revision.
	 *
	 * @param AbstractRevision $revision
	 * @param string $indexField The field used for pagination
	 * @param FormatterRow|null $row Row to populate
	 * @return FormatterRow
	 * @throws FlowException
	 */
	protected function buildResult( AbstractRevision $revision, $indexField, FormatterRow $row = null ) {
		$uuid = $revision->getRevisionId();
		$timestamp = $uuid->getTimestamp();

		$workflow = $this->getWorkflow( $revision );
		if ( !$workflow ) {
			throw new FlowException( "could not locate workflow for revision " . $revision->getRevisionId()->getAlphadecimal() );
		}

		$row = $row ?: new FormatterRow;
		$row->revision = $revision;
		if ( $this->needsPreviousRevision( $revision ) ) {
			$row->previousRevision = $this->getPreviousRevision( $revision );
		}
		$row->currentRevision = $this->getCurrentRevision( $revision );
		$row->workflow = $workflow;

		// some core classes that process this row before our formatter
		// require a specific field to handle pagination
		if ( property_exists( $row, $indexField ) ) {
			$row->$indexField = $timestamp;
		}

		if ( $revision instanceof PostRevision ) {
			$row->rootPost = $this->getRootPost( $revision );
			$revision->setRootPost( $row->rootPost );
			$row->isFirstReply = $this->isFirstReply( $revision, $row->rootPost );
			$row->isLastReply = $this->isLastReply( $revision );
		}

		return $row;
	}

	/**
	 * @param PostRevision $revision
	 * @param PostRevision $root
	 * @return bool
	 */
	protected function isFirstReply( PostRevision $revision, PostRevision $root ) {
		// check if it's a first-level reply (not topic title, but the level just below that)
		if ( !$root->getPostId()->equals( $revision->getReplyToId() ) ) {
			return false;
		}

		// we can use the timestamps to check if the reply was created at roughly the same time the topic was created
		// if they're 0 or 1 seconds apart, they must have been created in the same request
		// unless our servers are extremely slow and can't create topic + first reply in < 1 seconds, this should be a pretty safe method to detect first reply
		if ( $revision->getPostId()->getTimestamp( TS_UNIX ) - $root->getPostId()->getTimestamp( TS_UNIX ) >= 2 ) {
			return false;
		}

		return true;
	}

	/**
	 * @param PostRevision $revision
	 * @return bool
	 */
	protected function isLastReply( PostRevision $revision ) {
		if ( $revision->isTopicTitle() ) {
			return false;
		}
		$reply = $revision->getReplyToId()->getAlphadecimal();
		if ( !isset( $this->identityMap[$reply] ) ) {
			wfDebugLog( 'Flow', __METHOD__ . ": Missing $reply in identity map" );
			return false;
		}
		$parent = $this->identityMap[$revision->getReplyToId()->getAlphadecimal()];
		$keys = array_keys( $parent['children'] );
		return end( $keys ) === $revision->getPostId()->getAlphadecimal();
	}

	/**
	 * @param AbstractRevision $revision
	 * @return Workflow
	 * @throws \MWException
	 */
	protected function getWorkflow( AbstractRevision $revision ) {
		if ( $revision instanceof PostRevision ) {
			$rootPostId = $this->getRootPostId( $revision );
			return $this->getWorkflowById( $rootPostId );
		} elseif ( $revision instanceof Header ) {
			return $this->getWorkflowById( $revision->getWorkflowId() );
		} elseif ( $revision instanceof PostSummary ) {
			return $this->getWorkflowById( $revision->getCollection()->getWorkflowId() );
		} else {
			throw new \MWException( 'Unsupported revision type ' . get_class( $revision ) );
		}
	}

	/**
	 * Decides if the given abstract revision needs its prior revision for formatting
	 * @param AbstractRevision $revision
	 * @return boolean true when the previous revision to this should be loaded
	 */
	protected function needsPreviousRevision( AbstractRevision $revision ) {
		// crappy special case needs the previous object so it can show the title
		// but only when outputting a full history api result(we don't know that here)
		return $revision instanceof PostRevision
			&& $revision->getChangeType() === 'edit-title';
	}

	/**
	 * Retrieves the previous revision for a given AbstractRevision
	 * @param  AbstractRevision $revision The revision to retrieve the previous revision for.
	 * @return AbstractRevision|null      AbstractRevision of the previous revision or null if no previous revision.
	 */
	protected function getPreviousRevision( AbstractRevision $revision ) {
		$previousRevisionId = $revision->getPrevRevisionId();

		// original post; no previous revision
		if ( $previousRevisionId === null ) {
			return null;
		}

		if ( !isset( $this->revisionCache[$previousRevisionId->getAlphadecimal()] ) ) {
			$this->revisionCache[$previousRevisionId->getAlphadecimal()] =
				$this->storage->get( 'PostRevision', $previousRevisionId );
		}

		return $this->revisionCache[$previousRevisionId->getAlphadecimal()];
	}

	/**
	 * Retrieves the current revision for a given AbstractRevision
	 * @param  AbstractRevision $revision The revision to retrieve the current revision for.
	 * @return AbstractRevision|null      AbstractRevision of the current revision.
	 */
	protected function getCurrentRevision( AbstractRevision $revision ) {
		$cacheKey = $this->getCurrentRevisionCacheKey( $revision );
		if ( !isset( $this->currentRevisionsCache[$cacheKey] ) ) {
			$currentRevision = $revision->getCollection()->getLastRevision();

			$this->currentRevisionsCache[$cacheKey] = $currentRevision->getRevisionId();
			$this->revisionCache[$currentRevision->getRevisionId()->getAlphadecimal()] = $currentRevision;
		}

		$currentRevisionId = $this->currentRevisionsCache[$cacheKey];
		return $this->revisionCache[$currentRevisionId->getAlphaDecimal()];
	}

	/**
	 * Retrieves the root post for a given PostRevision
	 * @param  PostRevision $revision The revision to retrieve the root post for.
	 * @return PostRevision           PostRevision of the root post.
	 * @throws \MWException
	 */
	protected function getRootPost( PostRevision $revision ) {
		if ( $revision->isTopicTitle() ) {
			return $revision;
		}
		$rootPostId = $this->getRootPostId( $revision );

		if ( !isset( $this->postCache[$rootPostId->getAlphadecimal()] ) ) {
			throw new \MwException( 'Did not load root post ' . $rootPostId->getAlphadecimal() );
		}

		$rootPost = $this->postCache[$rootPostId->getAlphadecimal()];
		if ( !$rootPost ) {
			throw new \MWException( 'Did not locate root post ' . $rootPostId->getAlphadecimal() );
		}
		if ( !$rootPost->isTopicTitle() ) {
			throw new \MWException( "Not a topic title: " . $rootPost->getRevisionId()->getAlphadecimal() );
		}

		return $rootPost;
	}

	/**
	 * Gets the root post ID for a given PostRevision
	 * @param  PostRevision $revision The revision to get the root post ID for.
	 * @return UUID                   The UUID for the root post.
	 * @throws \MWException
	 */
	protected function getRootPostId( PostRevision $revision ) {
		$postId = $revision->getPostId();
		if ( $revision->isTopicTitle() ) {
			return $postId;
		} elseif ( isset( $this->rootPostIdCache[$postId->getAlphadecimal()] ) ) {
			return $this->rootPostIdCache[$postId->getAlphadecimal()];
		} else {
			throw new \MWException( "Unable to find root post ID for post " . $postId->getAlphadecimal() );
		}
	}

	/**
	 * Gets a Workflow object given its ID
	 * @param  UUID   $workflowId The Workflow ID to retrieve.
	 * @return Workflow           The Workflow.
	 */
	protected function getWorkflowById( UUID $workflowId ) {
		$alpha = $workflowId->getAlphadecimal();
		if ( isset( $this->workflowCache[$alpha] ) ) {
			return $this->workflowCache[$alpha];
		} else {
			return $this->workflowCache[$alpha] = $this->storage->get( 'Workflow', $workflowId );
		}
	}

	/**
	 * @param AbstractRevision $revision
	 * @return string
	 */
	protected function getCurrentRevisionCacheKey( AbstractRevision $revision ) {
		return $revision->getRevisionType() . '-' . $revision->getCollectionId()->getAlphadecimal();
	}
}

/**
 * Helper class represents a row of data from AbstractQuery
 */
class FormatterRow {
	/** @var AbstractRevision */
	public $revision;
	/** @var AbstractRevision|null */
	public $previousRevision;
	/** @var AbstractRevision */
	public $currentRevision;
	/** @var Workflow */
	public $workflow;
	/** @var string */
	public $indexFieldName;
	/** @var string */
	public $indexFieldValue;
	/** @var PostRevision|null */
	public $rootPost;
	/** @var bool */
	public $isLastReply = false;
	/** @var bool */
	public $isFirstReply = false;

	// protect against typos
	public function __get( $attribute ) {
		throw new \MWException( "Accessing non-existent parameter: $attribute" );
	}

	// protect against typos
	public function __set( $attribute, $value ) {
		throw new \MWException( "Accessing non-existent parameter: $attribute" );
	}
}