Current File : /home/jvzmxxx/wiki1/extensions/Flow/includes/Block/TopicList.php
<?php

namespace Flow\Block;

use Flow\Container;
use Flow\Data\Pager\Pager;
use Flow\Data\Pager\PagerPage;
use Flow\Exception\FlowException;
use Flow\Formatter\TocTopicListFormatter;
use Flow\Formatter\TopicListFormatter;
use Flow\Formatter\TopicListQuery;
use Flow\Model\PostRevision;
use Flow\Model\TopicListEntry;
use Flow\Model\UUID;
use Flow\Model\Workflow;
use Flow\Exception\FailCommitException;

class TopicListBlock extends AbstractBlock {

	/**
	 * @var array
	 */
	protected $supportedPostActions = array( 'new-topic' );

	/**
	 * @var array
	 */
	protected $supportedGetActions = array( 'view', 'view-topiclist' );

	// @Todo - fill in the template names
	protected $templates = array(
		'view' => '',
		'new-topic' => 'newtopic',
	);

	/**
	 * @var Workflow|null
	 */
	protected $topicWorkflow;

	/**
	 * @var TopicListEntry|null
	 */
	protected $topicListEntry;

	/**
	 * @var PostRevision|null
	 */
	protected $topicTitle;

	/**
	 * @var PostRevision|null
	 */
	protected $firstPost;

	/**
	 * @var array
	 *
	 * Associative array mapping topic ID (in alphadecimal form) to PostRevision for the topic root.
	 */
	protected $topicRootRevisionCache = array();

	/**
	 * @constant(TOCLIMIT)
	 *
	 * The limit of Table of Contents topics that are rendered per request
	 */
	const TOCLIMIT = 50;

	protected function validate() {
		// for now, new topic is considered a new post; perhaps some day topic creation should get it's own permissions?
		if (
			!$this->permissions->isRevisionAllowed( null, 'new-post' ) ||
			!$this->permissions->isBoardAllowed( $this->workflow, 'new-post' )
		) {
			$this->addError( 'permissions', $this->context->msg( 'flow-error-not-allowed' ) );
			return;
		}
		if ( !isset( $this->submitted['topic'] ) || !is_string( $this->submitted['topic'] ) ) {
			$this->addError( 'topic', $this->context->msg( 'flow-error-missing-title' ) );
			return;
		}
		$this->submitted['topic'] = trim( $this->submitted['topic'] );
		if ( strlen( $this->submitted['topic'] ) === 0 ) {
			$this->addError( 'topic', $this->context->msg( 'flow-error-missing-title' ) );
			return;
		}
		if ( mb_strlen( $this->submitted['topic'] ) > PostRevision::MAX_TOPIC_LENGTH ) {
			$this->addError( 'topic', $this->context->msg( 'flow-error-title-too-long', PostRevision::MAX_TOPIC_LENGTH ) );
			return;
		}

		if ( trim( $this->submitted['content'] ) === '' ) {
			$this->addError( 'content', $this->context->msg( 'flow-error-missing-content' ) );
			return;
		}

		// creates Workflow, Revision & TopicListEntry objects to be inserted into storage
		list( $this->topicWorkflow, $this->topicListEntry, $this->topicTitle, $this->firstPost ) = $this->create();

		if ( !$this->checkSpamFilters( null, $this->topicTitle ) ) {
			return;
		}
		if ( $this->firstPost && !$this->checkSpamFilters( null, $this->firstPost ) ) {
			return;
		}
	}

	/**
	 * Creates the objects about to be inserted into storage:
	 * * $this->topicWorkflow
	 * * $this->topicListEntry
	 * * $this->topicTitle
	 * * $this->firstPost
	 *
	 * @throws \MWException
	 * @throws \Flow\Exception\FailCommitException
	 * @return array Array of [$topicWorkflow, $topicListEntry, $topicTitle, $firstPost]
	 */
	protected function create() {
		$title = $this->workflow->getArticleTitle();
		$user = $this->context->getUser();
		$topicWorkflow = Workflow::create( 'topic', $title );
		$topicListEntry = TopicListEntry::create( $this->workflow, $topicWorkflow );
		$topicTitle = PostRevision::createTopicPost(
			$topicWorkflow,
			$user,
			$this->submitted['topic']
		);

		$firstPost = null;
		if ( !empty( $this->submitted['content'] ) ) {
			$firstPost = $topicTitle->reply(
				$topicWorkflow,
				$user,
				$this->submitted['content'],
				// default to wikitext when not specified, for old API requests
				isset( $this->submitted['format'] ) ? $this->submitted['format'] : 'wikitext'
			);
			$topicTitle->setChildren( array( $firstPost ) );
		}

		return array( $topicWorkflow, $topicListEntry, $topicTitle, $firstPost );
	}

	public function commit() {
		if ( $this->action !== 'new-topic' ) {
			throw new FailCommitException( 'Unknown commit action', 'fail-commit' );
		}

		$metadata = array(
			'workflow' => $this->topicWorkflow,
			'board-workflow' => $this->workflow,
			'topic-title' => $this->topicTitle,
			'first-post' => $this->firstPost,
		);

		/*
		 * Order of storage is important! We've been changing when we stored
		 * workflow a couple of times. For now, it needs to be stored first:
		 * * TopicPageCreationListener.php (post listener) must first create the
		 *   Topic:Xyz page before NotificationListener.php (topic/post
		 *   listeners) creates notifications (& mails) that link to it
		 * * ReferenceExtractor.php (run from ReferenceRecorder.php, a post
		 *   listener) needs to parse content with Parsoid & for that it needs
		 *   the board title. AbstractRevision::getContent() will figure out
		 *   the title from the workflow: $this->getCollection()->getTitle()
		 * If you even feel the need to change the order, make sure you come
		 * up with a fix for the above things ;)
		 */
		$this->storage->put( $this->workflow, array() ); // 'discussion' workflow
		$this->storage->put( $this->topicWorkflow, $metadata ); // 'topic' workflow
		$this->storage->put( $this->topicListEntry, $metadata );
		$this->storage->put( $this->topicTitle, $metadata );
		if ( $this->firstPost !== null ) {
			$this->storage->put( $this->firstPost, $metadata + array(
				'reply-to' => $this->topicTitle
			) );
		}

		$output = array(
			'topic-page' => $this->topicWorkflow->getArticleTitle()->getPrefixedText(),
			'topic-id' => $this->topicTitle->getPostId(),
			'topic-revision-id' => $this->topicTitle->getRevisionId(),
			'post-id' => $this->firstPost ? $this->firstPost->getPostId() : null,
			'post-revision-id' => $this->firstPost ? $this->firstPost->getRevisionId() : null,
		);

		return $output;
	}

	public function renderTocApi( array $topicList, array $options ) {
		global $wgFlowDefaultLimit;

		$tocApiParams = array_merge(
			$options,
			array(
				'toconly' => true,
				'limit' => self::TOCLIMIT
			)
		);

		$findOptions = $this->getFindOptions( $options );

		// include the current sortby option.  Note that when 'user' is either
		// submitted or defaulted to this is the resulting sort. ex: newest
		$tocApiParams['sortby'] = $findOptions['sortby'];

		// In the case of 'newest' sort, we could save ourselves trouble and only
		// produce the necessary 40 topics that are missing from the ToC, by taking
		// the latest UUID from the topic list.
		// This is a bit harder for the case of 'updated' which requires a timestamp,
		// so in that case, we can stick to having repeated topics and letting the
		// data model sort through which ones it needs to update and which ones it
		// may ignore.
		if ( $tocApiParams['sortby'] === 'newest' ) {
			// Make sure we found topiclist block
			// and that it actually has roots in it
			$existingRoots = isset( $topicList['roots'] ) && is_array( $topicList['roots'] ) ?
				$topicList['roots'] : array();

			if ( count( $existingRoots ) > 0 ) {
				// Add new offset-id and limit to the api parameters and change the limit
				$tocApiParams['offset-id'] = end( $existingRoots );
				$tocApiParams['limit'] = self::TOCLIMIT - $wgFlowDefaultLimit;
			}
		}

		return $this->renderApi( $tocApiParams );
	}

	public function renderApi( array $options ) {
		$options = $this->preloadTexts( $options );

		$response = array(
			'submitted' => $this->wasSubmitted() ? $this->submitted : $options,
			'errors' => $this->errors,
		);

		// Repeating the default until we use the API for everything (bug 72659)
		// Also, if this is removed other APIs (i.e. ApiFlowNewTopic) may need
		// to be adjusted if they trigger a rendering of this block.
		$isTocOnly = isset( $options['toconly'] ) ? $options['toconly'] : false;

		if ( $isTocOnly ) {
			/** @var TocTopicListFormatter $serializer */
			$serializer = Container::get( 'formatter.topiclist.toc' );
		} else {
			/** @var TopicListFormatter $serializer */
			$serializer = Container::get( 'formatter.topiclist' );
			$format = isset( $options['format'] ) ? $options['format'] : 'fixed-html';
			$serializer->setContentFormat( $format );
		}

		// @todo remove the 'api' => true, its always api
		$findOptions = $this->getFindOptions( $options + array( 'api' => true ) );

		// include the current sortby option.  Note that when 'user' is either
		// submitted or defaulted to this is the resulting sort. ex: newest
		$response['sortby'] = $findOptions['sortby'];

		if ( $this->workflow->isNew() ) {
			return $response + $serializer->buildEmptyResult( $this->workflow );
		}

		$page = $this->getPage( $findOptions );
		$workflowIds = array();
		/** @var TopicListEntry $topicListEntry */
		foreach ( $page->getResults() as $topicListEntry ) {
			$workflowIds[] = $topicListEntry->getId();
		}

		$workflows = $this->storage->getMulti( 'Workflow', $workflowIds );

		if ( $isTocOnly ) {
			// We don't need any further data, so we skip the TopicListQuery.

			$topicRootRevisionsByWorkflowId = array();
			$workflowsByWorkflowId = array();

			foreach ( $workflows as $workflow ) {
				$alphaWorkflowId = $workflow->getId()->getAlphadecimal();
				$topicRootRevisionsByWorkflowId[$alphaWorkflowId] = $this->topicRootRevisionCache[$alphaWorkflowId];
				$workflowsByWorkflowId[$alphaWorkflowId] = $workflow;
			}

			return $response + $serializer->formatApi( $this->workflow, $topicRootRevisionsByWorkflowId, $workflowsByWorkflowId, $page );
		}

		/** @var TopicListQuery $query */
		$query = Container::get( 'query.topiclist' );
		$found = $query->getResults( $page->getResults() );
		wfDebugLog( 'FlowDebug', 'Rendering topiclist for ids: ' . implode( ', ', array_map( function( UUID $id ) {
			return $id->getAlphadecimal();
		}, $workflowIds ) ) );

		return $response + $serializer->formatApi( $this->workflow, $workflows, $found, $page, $this->context );
	}

	/**
	 * Transforms preload params into proper options we can assign to template.
	 *
	 * @param array $options
	 * @return array
	 * @throws \MWException
	 */
	protected function preloadTexts( $options ) {
		if ( isset( $options['preload'] ) && !empty( $options['preload'] ) ) {
			$title = \Title::newFromText( $options['preload'] );
			$page = \WikiPage::factory( $title );
			if ( $page->isRedirect() ) {
				$title = $page->getRedirectTarget();
				$page = \WikiPage::factory( $title );
			}

			if ( $page->exists() ) {
				$content = $page->getContent( \Revision::RAW );
				$options['content'] = $content->serialize();
				$options['format'] = 'wikitext';
			}
		}

		if ( isset( $options['preloadtitle'] ) ) {
			$options['topic'] = $options['preloadtitle'];
		}

		return $options;
	}

	public function getName() {
		return 'topiclist';
	}

	protected function getLimit( array $options ) {
		global $wgFlowDefaultLimit, $wgFlowMaxLimit;
		$limit = $wgFlowDefaultLimit;
		if ( isset( $options['limit'] ) ) {
			$requestedLimit = intval( $options['limit'] );
			$limit = min( $requestedLimit, $wgFlowMaxLimit );
			$limit = max( 0, $limit );
		}

		return $limit;
	}

	protected function getFindOptions( array $requestOptions ) {
		$findOptions = array();

		// Compute offset/limit
		$limit = $this->getLimit( $requestOptions );

		// @todo Once we migrate View.php to use the API directly
		// all defaults will be handled by API and not here.
		$requestOptions += array(
			'include-offset' => false,
			'offset-id' => false,
			'offset-dir' => 'fwd',
			'offset' => false,
			'api' => true,
			'sortby' => 'user',
			'savesortby' => false,
		);

		$user = $this->context->getUser();
		if ( strlen( $requestOptions['sortby'] ) === 0 ) {
			$requestOptions['sortby'] = 'user';
		}
		// the sortby option in $findOptions is not directly used for querying,
		// but is needed by the pager to generate appropriate pagination links.
		if ( $requestOptions['sortby'] === 'user' ) {
			$requestOptions['sortby'] = $user->getOption( 'flow-topiclist-sortby' );
		}
		switch( $requestOptions['sortby'] ) {
		case 'updated':
			$findOptions = array(
				'sortby' => 'updated',
				'sort' => 'workflow_last_update_timestamp',
				'order' => 'desc',
			) + $findOptions;

			if ( $requestOptions['offset-id'] ) {
				throw new FlowException( 'The `updated` sort order does not allow the `offset-id` parameter. Please use `offset`.' );
			}
			break;

		case 'newest':
		default:
			$findOptions = array(
				'sortby' => 'newest',
				'sort' => 'topic_id',
				'order' => 'desc',
			) + $findOptions;

			if ( $requestOptions['offset'] ) {
				throw new FlowException( 'The `newest` sort order does not allow the `offset` parameter.  Please use `offset-id`.' );
			}
		}

		if ( $requestOptions['offset-id'] ) {
			$findOptions['pager-offset'] = UUID::create( $requestOptions['offset-id'] );
		} elseif ( $requestOptions['offset'] ) {
			$findOptions['pager-offset'] = intval( $requestOptions['offset'] );
		}

		if ( $requestOptions['offset-dir'] ) {
			$findOptions['pager-dir'] = $requestOptions['offset-dir'];
		}

		if ( $requestOptions['include-offset'] ) {
			$findOptions['pager-include-offset'] = $requestOptions['include-offset'];
		}

		$findOptions['pager-limit'] = $limit;

		if (
			$requestOptions['savesortby']
			&& !$user->isAnon()
			&& $user->getOption( 'flow-topiclist-sortby' ) != $findOptions['sortby']
		) {
			$user->setOption( 'flow-topiclist-sortby', $findOptions['sortby'] );
			// Save the user preferences post-send
			\DeferredUpdates::addCallableUpdate( function() use ( $user ) {
				$user->saveSettings();
			} );
		}

		return $findOptions;
	}

	/**
	 * Gets a set of workflow IDs
	 * This filters result to only include unmoderated and locked topics.
	 *
	 * Also populates topicRootRevisionCache with a mapping from topic ID to the
	 * PostRevision for the topic root.
	 *
	 * @param array $findOptions
	 * @return PagerPage
	 */
	protected function getPage( array $findOptions ) {
		$pager = new Pager(
			$this->storage->getStorage( 'TopicListEntry' ),
			array( 'topic_list_id' => $this->workflow->getId() ),
			$findOptions
		);

		$postStorage = $this->storage->getStorage( 'PostRevision' );

		// Work around lack of $this in closures until we can use PHP 5.4+ features.
		$topicRootRevisionCache =& $this->topicRootRevisionCache;

		return $pager->getPage( function( array $found ) use ( $postStorage, &$topicRootRevisionCache ) {
			$queries = array();
			/** @var TopicListEntry[] $found */
			foreach ( $found as $entry ) {
				$queries[] = array( 'rev_type_id' => $entry->getId() );
			}
			$posts = $postStorage->findMulti( $queries, array(
				'sort' => 'rev_id',
				'order' => 'DESC',
				'limit' => 1,
			) );
			$allowed = array();
			foreach ( $posts as $queryResult ) {
				$post = reset( $queryResult );
				if ( !$post->isModerated() || $post->isLocked() ) {
					$allowed[$post->getPostId()->getAlphadecimal()] = $post;
				}
			}
			foreach ( $found as $idx => $entry ) {
				if ( isset( $allowed[$entry->getId()->getAlphadecimal()] ) ) {
					$topicRootRevisionCache[$entry->getId()->getAlphadecimal()] = $allowed[$entry->getId()->getAlphadecimal()];
				} else {
					unset( $found[$idx] );
				}
			}

			return $found;
		} );
	}

	/**
	 * @param \OutputPage $out
	 */
	public function setPageTitle( \OutputPage $out ) {
		if ( $this->action !== 'new-topic' ) {
			// Only new-topic should override page title, rest should default
			parent::setPageTitle( $out );
			return;
		}

		$title = $this->workflow->getOwnerTitle();
		$message = $out->msg( 'flow-newtopic-first-heading', $title->getPrefixedText() );
		$out->setPageTitle( $message );
		$out->setHtmlTitle( $message );
		$out->setSubtitle( '&lt; ' . \Linker::link( $title ) );
	}
}