Current File : /home/jvzmxxx/wiki1/extensions/Flow/includes/Import/OptInController.php
<?php

namespace Flow\Import;

use DateTime;
use DateTimeZone;
use DerivativeContext;
use Flow\Collection\HeaderCollection;
use Flow\Content\BoardContent;
use Flow\Exception\InvalidDataException;
use Flow\NotificationController;
use Flow\OccupationController;
use Flow\Conversion\Utils;
use Flow\WorkflowLoader;
use Flow\WorkflowLoaderFactory;
use IContextSource;
use MovePage;
use Parser;
use ParserOptions;
use RequestContext;
use Revision;
use Title;
use Flow\Container;
use User;
use WikiPage;
use WikitextContent;

/**
 * Entry point for enabling Flow on a page.
 */
class OptInController {

	/**
	 * @var OccupationController
	 */
	private $occupationController;

	/**
	 * @var NotificationController
	 */
	private $notificationController;

	/**
	 * @var ArchiveNameHelper
	 */
	private $archiveNameHelper;

	/**
	 * @var IContextSource
	 */
	private $context;

	/**
	 * @var User
	 */
	private $user;

	public function __construct() {
		$this->occupationController = Container::get( 'occupation_controller' );
		$this->notificationController = Container::get( 'controller.notification' );
		$this->archiveNameHelper = new ArchiveNameHelper();
		$this->user = $this->occupationController->getTalkpageManager();
		$this->context = new DerivativeContext( RequestContext::getMain() );
		$this->context->setUser( $this->user );
	}

	/**
	 * @param Title $title
	 * @param User $user
	 */
	public function enable( Title $title, User $user ) {
		if ( $this->isFlowBoard( $title ) ) {
			// already a Flow board
			return;
		}

		// archive existing wikitext talk page
		$currentTemplate = null;
		$templatesFromTalkpage = null;
		if ( $title->exists( Title::GAID_FOR_UPDATE ) ) {
			$templatesFromTalkpage = $this->extractTemplatesAboveFirstSection( $title );
			$wikitextTalkpageArchiveTitle = $this->archiveExistingTalkpage( $title );
			$currentTemplate = $this->getFormattedCurrentTemplate( $wikitextTalkpageArchiveTitle );
		}

		// create or restore flow board
		$archivedFlowPage = $this->findLatestFlowArchive( $title );
		if ( $archivedFlowPage ) {
			$this->restoreExistingFlowBoard( $archivedFlowPage, $title, $currentTemplate );
		} else {
			$this->createFlowBoard( $title, $templatesFromTalkpage . "\n\n" . $currentTemplate );
			$this->notificationController->notifyFlowEnabledOnTalkpage( $user );
		}
	}

	/**
	 * @param Title $title
	 */
	public function disable( Title $title ) {
		if ( !$this->isFlowBoard( $title ) ) {
			return;
		}

		// archive the flow board
		$flowArchiveTitle = $this->archiveFlowBoard( $title );

		// restore the original wikitext talk page
		$archivedTalkpage = $this->findLatestArchive( $title );
		if ( $archivedTalkpage ) {
			$this->removeArchiveTemplateFromWikitextTalkpage( $archivedTalkpage );
			$this->addCurrentTemplate( $archivedTalkpage, $flowArchiveTitle );
			$restoreReason = wfMessage( 'flow-optin-restore-wikitext' )->inContentLanguage()->text();
			$this->movePage( $archivedTalkpage, $title, $restoreReason );
		}
	}

	/**
	 * Check whether the current user has a flow board archived already.
	 *
	 * @param User $user
	 * @return bool Flow board archive exists
	 */
	public function hasFlowBoardArchive( User $user ) {
		return $this->findLatestFlowArchive( $user->getTalkPage() ) !== false;
	}

	/**
	 * @param Title $title
	 * @return bool
	 */
	private function isFlowBoard( Title $title ) {
		return $title->getContentModel( Title::GAID_FOR_UPDATE ) === CONTENT_MODEL_FLOW_BOARD;
	}

	/**
	 * @param Title $from
	 * @param Title $to
	 * @param string $reason
	 */
	private function movePage( Title $from, Title $to, $reason = '' ) {
		$mp = new MovePage( $from, $to );
		$mp->move( $this->user, $reason, false );

		/*
		 * Article IDs are cached inside title objects. Since we'll be
		 * reusing these objects, we have to make sure they reflect the
		 * correct IDs.
		 * We could just Title::GAID_FOR_UPDATE everywhere, but that would
		 * result in a lot of unneeded calls to master.
		 * If these IDs are wrong, we could end up associating workflows
		 * with an incorrect page (that was just moved)
		 *
		 * Anyway, the page has just been moved without redirect, so that
		 * page is no longer valid.
		 */
		$from->resetArticleID( 0 );
		$linkCache = \LinkCache::singleton();
		$linkCache->addBadLinkObj( $from );

		/*
		 * Force id cached inside $title to be updated, as well as info
		 * inside LinkCache.
		 */
		$to->getArticleID( Title::GAID_FOR_UPDATE );
	}

	/**
	 * @param $msgKey
	 * @param array $args
	 * @throws ImportException
	 */
	private function fatal( $msgKey, $args = array() ) {
		throw new ImportException( wfMessage( $msgKey, $args )->inContentLanguage()->text() );
	}

	/**
	 * @param string $str
	 * @return array
	 */
	private function fromNewlineSeparated( $str ) {
		return explode( "\n", $str );
	}

	/**
	 * @param Title $title
	 * @return Title|false
	 */
	private function findLatestArchive( Title $title ) {
		$archiveFormats = $this->fromNewlineSeparated(
			wfMessage( 'flow-conversion-archive-page-name-format' )->inContentLanguage()->plain() );
		return $this->archiveNameHelper->findLatestArchiveTitle( $title, $archiveFormats );
	}

	/**
	 * @param Title $title
	 * @return Title
	 * @throws ImportException
	 */
	private function findNextArchive( Title $title ) {
		$archiveFormats = $this->fromNewlineSeparated(
			wfMessage( 'flow-conversion-archive-page-name-format' )->inContentLanguage()->plain() );
		return $this->archiveNameHelper->decideArchiveTitle( $title, $archiveFormats );
	}

	/**
	 * @param Title $title
	 * @return Title|false
	 */
	private function findLatestFlowArchive( Title $title ) {
		$archiveFormats = $this->fromNewlineSeparated(
			wfMessage( 'flow-conversion-archive-flow-page-name-format' )->inContentLanguage()->plain() );
		return $this->archiveNameHelper->findLatestArchiveTitle( $title, $archiveFormats );
	}

	/**
	 * @param Title $title
	 * @return Title
	 * @throws ImportException
	 */
	private function findNextFlowArchive( Title $title ) {
		$archiveFormats = $this->fromNewlineSeparated(
			wfMessage( 'flow-conversion-archive-flow-page-name-format' )->inContentLanguage()->plain() );
		return $this->archiveNameHelper->decideArchiveTitle( $title, $archiveFormats );
	}

	/**
	 * @param Title $title
	 * @param string $contentText
	 * @param string $summary
	 * @throws ImportException
	 * @throws \MWException
	 */
	private function createRevision( Title $title, $contentText, $summary ) {
		$page = WikiPage::factory( $title );
		$newContent = new WikitextContent( $contentText );
		$status = $page->doEditContent(
			$newContent,
			$summary,
			EDIT_FORCE_BOT | EDIT_SUPPRESS_RC,
			false,
			$this->user
		);

		if ( !$status->isGood() ) {
			throw new ImportException( "Failed creating revision at {$title}" );
		}
	}

	/**
	 * @param Title $title
	 * @param $boardDescription
	 * @throws ImportException
	 * @throws \Flow\Exception\CrossWikiException
	 * @throws \Flow\Exception\InvalidInputException
	 */
	private function createFlowBoard( Title $title, $boardDescription ) {
		/** @var WorkflowLoaderFactory $loaderFactory */
		$loaderFactory = Container::get( 'factory.loader.workflow' );
		$page = $title->getPrefixedText();

		$creationStatus = $this->occupationController->safeAllowCreation( $title, $this->user, false );
		if ( !$creationStatus->isGood() ) {
			$this->fatal( 'flow-special-enableflow-board-creation-not-allowed', $page );
		}

		$loader = $loaderFactory->createWorkflowLoader( $title );
		$blocks = $loader->getBlocks();

		if ( !$boardDescription ) {
			$boardDescription = ' ';
		}

		$action = 'edit-header';
		$params = array(
			'header' => array(
				'content' => $boardDescription,
				'format' => 'wikitext',
			),
		);

		$blocksToCommit = $loader->handleSubmit(
			$this->context,
			$action,
			$params
		);

		foreach ( $blocks as $block ) {
			if ( $block->hasErrors() ) {
				$errors = $block->getErrors();

				foreach ( $errors as $errorKey ) {
					$this->fatal( $block->getErrorMessage( $errorKey ) );
				}
			}
		}

		$loader->commit( $blocksToCommit );
	}

	/**
	 * @param Title $title
	 * @return Title
	 */
	private function archiveExistingTalkpage( Title $title ) {
		$archiveTitle = $this->findNextArchive( $title );
		$archiveReason = wfMessage( 'flow-optin-archive-wikitext' )->inContentLanguage()->text();
		$this->movePage( $title, $archiveTitle, $archiveReason );

		$content = $this->getContent( $archiveTitle );
		$content = $this->removeCurrentTemplateFromWikitext( $content, $archiveTitle );
		$content = $this->getFormattedArchiveTemplate( $title ) . "\n\n" . $content;

		$addTemplateReason = wfMessage( 'flow-beta-feature-add-archive-template-edit-summary' )->inContentLanguage()->plain();
		$this->createRevision(
			$archiveTitle,
			$content,
			$addTemplateReason
		);

		return $archiveTitle;
	}

	/**
	 * @param Title $archivedFlowPage
	 * @param Title $title
	 * @param string|null $currentTemplate
	 */
	private function restoreExistingFlowBoard( Title $archivedFlowPage, Title $title, $currentTemplate = null ) {
		$this->editBoardDescription(
			$archivedFlowPage,
			function( $content ) use ( $currentTemplate, $archivedFlowPage ) {
				$templateName = wfMessage( 'flow-importer-wt-converted-archive-template' )->inContentLanguage()->plain();
				$content = TemplateHelper::removeFromHtml( $content, $templateName );
				if ( $currentTemplate ) {
					$content = Utils::convert( 'wikitext', 'html', $currentTemplate, $archivedFlowPage ) . "<br/><br/>" . $content;
				}
				return $content;
			},
			'html'
		);

		$restoreReason = wfMessage( 'flow-optin-restore-flow-board' )->inContentLanguage()->text();
		$this->movePage( $archivedFlowPage, $title, $restoreReason );

	}

	/**
	 * @param Title $title
	 * @return string
	 * @throws \MWException
	 */
	private function getContent( Title $title ) {
		$page = WikiPage::factory( $title );
		$page->loadPageData( 'fromdbmaster' );
		$revision = $page->getRevision();
		if ( $revision ) {
			$content = $revision->getContent( Revision::FOR_PUBLIC );
			if ( $content instanceof WikitextContent ) {
				return $content->getNativeData();
			}
		}

		return '';
	}

	/**
	 * @param Title $archiveTitle
	 * @return string
	 */
	private function getFormattedCurrentTemplate( Title $archiveTitle ) {
		$now = new DateTime( "now", new DateTimeZone( "GMT" ) );
		$arguments = array(
			'archive' => $archiveTitle->getPrefixedText(),
			'date' => $now->format( 'Y-m-d' ),
		);
		$template = wfMessage( 'flow-importer-wt-converted-template' )->inContentLanguage()->plain();
		return $this->formatTemplate( $template, $arguments );
	}

	/**
	 * @param string $name
	 * @param array $args
	 * @return string
	 */
	private function formatTemplate( $name, $args ) {
		$arguments = implode( '|',
			array_map(
				function( $key, $value ) {
					return "$key=$value";
				},
				array_keys( $args ),
				array_values( $args ) )
		);
		return "{{{$name}|$arguments}}";
	}

	/**
	 * @param Title $title
	 * @param callable $newDescriptionCallback
	 * @param string $format
	 * @throws ImportException
	 * @throws \Flow\Exception\InvalidDataException
	 */
	private function editBoardDescription( Title $title, callable $newDescriptionCallback, $format = 'html' ) {
		/*
		 * We could use WorkflowLoaderFactory::createWorkflowLoader
		 * to get to the workflow ID, but that uses WikiPage::factory
		 * to build the wikipage & get the content. For most requests,
		 * that'll be better (it reads from slaves), but we really
		 * need to read from master here.
		 * We'll need WorkflowLoader further down anyway, but we'll
		 * then have the correct workflow ID to initialize it with!
		 *
		 * $title->getLatestRevId() should be fine, it'll be read from
		 * LinkCache, which has been updated.
		 * Revision::newFromId will try slave first. If it can't find
		 * the id, it'll try to find it on master.
		 */
		$revId = $title->getLatestRevID();
		$revision = Revision::newFromId( $revId );
		$content = $revision->getContent();
		if ( !$content instanceof BoardContent ) {
			throw new InvalidDataException(
				'Could not find board page for ' . $title->getPrefixedDBkey() . ' (id: ' . $title->getArticleID() . ').' .
				'Found content: ' . var_export( $content, true )
			);
		}
		$workflowId = $content->getWorkflowId();

		$collection = HeaderCollection::newFromId( $workflowId );
		$revision = $collection->getLastRevision();

		/*
		 * We could just do $revision->getContent( $format ), but that
		 * may need to find $title in order to convert.
		 * We already know $title (and don't want to risk it being used
		 * in a way it stores lagging slave data), so let's just
		 * manually convert the content.
		 */
		$content = $revision->getContentRaw();
		$content = Utils::convert( $revision->getContentFormat(), $format, $content, $title );

		$newDescription = call_user_func( $newDescriptionCallback, $content );

		$action = 'edit-header';
		$params = array(
			'header' => array(
				'content' => $newDescription,
				'format' => $format,
				'prev_revision' => $revision->getRevisionId()->getAlphadecimal()
			),
		);

		/** @var WorkflowLoaderFactory $factory */
		$factory = Container::get( 'factory.loader.workflow' );

		/** @var WorkflowLoader $loader */
		$loader = $factory->createWorkflowLoader( $title, $workflowId );

		$blocks = $loader->getBlocks();

		$blocksToCommit = $loader->handleSubmit(
			$this->context,
			$action,
			$params
		);

		foreach ( $blocks as $block ) {
			if ( $block->hasErrors() ) {
				$errors = $block->getErrors();

				foreach ( $errors as $errorKey ) {
					$this->fatal( $block->getErrorMessage( $errorKey ) );
				}
			}
		}

		$loader->commit( $blocksToCommit );
	}

	/**
	 * @param Title $current
	 * @return string
	 */
	private function getFormattedArchiveTemplate( Title $current ) {
		$templateName = wfMessage( 'flow-importer-wt-converted-archive-template' )->inContentLanguage()->plain();
		$now = new DateTime( "now", new DateTimeZone( "GMT" ) );
		return $this->formatTemplate( $templateName, array(
			'from' => $current->getPrefixedText(),
			'date' => $now->format( 'Y-m-d' ),
		) );
	}

	/**
	 * @param Title $title
	 * @throws ImportException
	 */
	private function removeArchiveTemplateFromWikitextTalkpage( Title $title ) {
		$content = $this->getContent( $title );
		if ( !$content ) {
			return;
		}

		$content = Utils::convert( 'wikitext', 'html', $content, $title );
		$templateName = wfMessage( 'flow-importer-wt-converted-archive-template' )->inContentLanguage()->plain();

		$newContent = TemplateHelper::removeFromHtml( $content, $templateName );

		$this->createRevision(
			$title,
			Utils::convert( 'html', 'wikitext', $newContent, $title ),
			wfMessage( 'flow-beta-feature-remove-archive-template-edit-summary' )->inContentLanguage()->plain());
	}

	/**
	 * @param string $wikitextContent
	 * @param Title $title
	 * @return string
	 */
	private function removeCurrentTemplateFromWikitext( $wikitextContent, Title $title ) {
		$templateName = wfMessage( 'flow-importer-wt-converted-template' )->inContentLanguage()->plain();
		$contentAsHtml = Utils::convert( 'wikitext', 'html', $wikitextContent, $title );
		$contentWithoutTemplate = TemplateHelper::removeFromHtml( $contentAsHtml, $templateName );
		return Utils::convert( 'html', 'wikitext', $contentWithoutTemplate, $title );
	}

	/**
	 * @param Title $title
	 * @return string
	 */
	private function extractTemplatesAboveFirstSection( Title $title ) {
		$content = $this->getContent( $title );
		if ( !$content ) {
			return '';
		}

		$parser = new Parser();
		$output = $parser->parse( $content, $title, new ParserOptions );
		$sections = $output->getSections();
		if ( $sections ) {
			$content = substr( $content, 0, $sections[0]['byteoffset'] );
		}
		return TemplateHelper::extractTemplates( $content, $title );
	}

	/**
	 * @param Title $title
	 * @param $reason
	 * @param callable $newDescriptionCallback
	 * @param string $format
	 * @throws ImportException
	 * @throws InvalidDataException
	 */
	private function editWikitextContent( Title $title, $reason, callable $newDescriptionCallback, $format = 'html' ) {
		$content = Utils::convert( 'wikitext', $format, $this->getContent( $title ), $title );
		$newContent = call_user_func( $newDescriptionCallback, $content );
		$this->createRevision(
			$title,
			Utils::convert( $format, 'wikitext', $newContent, $title ),
			$reason
		);
	}

	/**
	 * Add the "current" template to the page considered the current talkpage
	 * and link to the archived talkpage.
	 *
	 * @param Title $currentTalkpageTitle
	 * @param Title $archivedTalkpageTitle
	 */
	private function addCurrentTemplate( Title $currentTalkpageTitle, Title $archivedTalkpageTitle ) {
		$template = $this->getFormattedCurrentTemplate( $archivedTalkpageTitle );
		$this->editWikitextContent(
			$currentTalkpageTitle,
			null,
			function( $content ) use ( $template ) { return $template . "\n\n" . $content; },
			'wikitext'
		);
	}

	/**
	 * @param Title $title
	 * @return Title
	 * @throws InvalidDataException
	 */
	private function archiveFlowBoard( Title $title ) {
		$flowArchiveTitle = $this->findNextFlowArchive( $title );
		$archiveReason = wfMessage( 'flow-optin-archive-flow-board' )->inContentLanguage()->text();
		$this->movePage( $title, $flowArchiveTitle, $archiveReason );

		$template = $this->getFormattedArchiveTemplate( $title );
		$template = Utils::convert( 'wikitext', 'html', $template, $title );

		$this->editBoardDescription(
			$flowArchiveTitle,
			function( $content ) use ( $template ) {
				$templateName = wfMessage( 'flow-importer-wt-converted-template' )->inContentLanguage()->plain();
				$content = TemplateHelper::removeFromHtml( $content, $templateName );
				return $template . "<br/><br/>" . $content;
			},
			'html' );

		return $flowArchiveTitle;
	}
}