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

namespace Flow\Import;

use DatabaseBase;
use Flow\Exception\FlowException;
use MovePage;
use MWExceptionHandler;
use Psr\Log\LoggerInterface;
use Revision;
use Title;
use User;
use WikiPage;
use WikitextContent;

/**
 * Converts provided titles to Flow. This converter is idempotent when
 * used with an appropriate SourceStoreInterface, and may be run many times
 * without worry for duplicate imports.
 *
 * Flow does not currently support viewing the history of its page prior
 * to being flow enabled.  Because of this prior to conversion the current
 * wikitext page will be moved to an archive location.
 *
 * Implementing classes must choose a name for their archive page and
 * be able to create an IImportSource when provided a Title. On successful
 * import of a page a 'cleanup archive' edit is optionally performed.
 *
 * Any content changes to the imported content should be provided as part
 * of the IImportSource.
 */
class Converter {
	/**
	 * @var DatabaseBase Master database of the current wiki. Required
	 *  to lookup past page moves.
	 */
	protected $dbw;

	/**
	 * @var Importer Service capable of turning an IImportSource into
	 *  flow revisions.
	 */
	protected $importer;

	/**
	 * @var LoggerInterface
	 */
	protected $logger;

	/**
	 * @var User The user for performing maintenance actions like moving
	 *  pages or editing templates onto an archived page. This should be
	 *  a system account and not a normal user.
	 */
	protected $user;

	/**
	 * @var IConversionStrategy Interface between this converter and an
	 *  IImportSource implementation.
	 */
	protected $strategy;

	/**
	 * @param DatabaseBase $dbw Master wiki database to read from
	 * @param Importer $importer
	 * @param LoggerInterface $logger
	 * @param User $user Administrative user for moves and edits related
	 *  to the conversion process.
	 * @param IConversionStrategy $strategy
	 * @throws ImportException When $user does not have an Id
	 */
	public function __construct(
		DatabaseBase $dbw,
		Importer $importer,
		LoggerInterface $logger,
		User $user,
		IConversionStrategy $strategy
	) {
		if ( !$user->getId() ) {
			throw new ImportException( 'User must have id' );
		}
		$this->dbw = $dbw;
		$this->importer = $importer;
		$this->logger = $logger;
		$this->user = $user;
		$this->strategy = $strategy;

		$postprocessor = $strategy->getPostprocessor();
		if ( $postprocessor !== null ) {
			// @todo assert we cant cause duplicate postprocessors
			$this->importer->addPostprocessor( $postprocessor );
		}

		// Force the importer to use our logger for consistent output.
		$this->importer->setLogger( $logger );
	}

	/**
	 * Converts multiple pages into Flow boards
	 *
	 * @param Traversable<Title>|array $titles
	 */
	public function convertAll( $titles ) {
		/** @var Title $title */
		foreach ( $titles as $title ) {
			try {
				$this->convert( $title );
			} catch ( \Exception $e ) {
				MWExceptionHandler::logException( $e );
				$this->logger->error( "Exception while importing: {$title}" );
				$this->logger->error( (string)$e );
			}
		}
	}

	/**
	 * Converts a page into a Flow board
	 *
	 * @param Title $title
	 * @throws FlowException
	 */
	public function convert( Title $title ) {
		/*
		 * $title is the title we're currently considering to import.
		 * It could be a page we need to import, but could also e.g.
		 * be an archive page of a previous import run (in which case
		 * $movedFrom will be the Title object of that original page)
		 */
		$movedFrom = $this->getPageMovedFrom( $title );
		if ( $this->strategy->isConversionFinished( $title, $movedFrom ) ) {
			return;
		}

		if ( !$this->isAllowed( $title ) ) {
			throw new FlowException( "Not allowed to convert: {$title}" );
		}

		$this->doConversion( $title, $movedFrom );
	}

	/**
	 * Returns a boolean indicating if we're allowed to import $title.
	 *
	 * @param Title $title
	 * @return bool
	 */
	protected function isAllowed( Title $title ) {
		// Only make changes to wikitext pages
		if ( $title->getContentModel() !== CONTENT_MODEL_WIKITEXT ) {
			$this->logger->warning( "WARNING: The title '" . $title->getPrefixedDBkey() . "' is being skipped because it has content model '" . $title->getContentModel() . "''." );
			return false;
		}

		if ( !$title->exists() ) {
			$this->logger->warning( "WARNING: The title '" . $title->getPrefixedDBkey() . "' is being skipped because it does not exist." );
			return false;
		}

		// At some point we may want to handle these, but for now just
		// let them be
		if ( $title->isRedirect() ) {
			$this->logger->warning( "WARNING: The title '" . $title->getPrefixedDBkey() . "' is being skipped because it is a redirect." );
			return false;
		}

		// Finally, check strategy-specific logic
		return $this->strategy->shouldConvert( $title );
	}

	protected function doConversion( Title $title, Title $movedFrom = null ) {
		if ( $movedFrom ) {
			// If the page is moved but has not completed conversion that
			// means the previous import failed to complete. Try again.
			$archiveTitle = $title;
			$title = $movedFrom;
			$this->logger->info( "Page previously archived from $title to $archiveTitle" );
		} else {
			// The move needs to happen prior to the import because upon starting the
			// import the top revision will be a flow-board revision.
			$archiveTitle = $this->strategy->decideArchiveTitle( $title );
			$this->logger->info( "Archiving page from $title to $archiveTitle" );
			$this->movePage( $title, $archiveTitle );
			wfWaitForSlaves(); // Wait for slaves to pick up the move
		}

		$source = $this->strategy->createImportSource( $archiveTitle );
		if ( $this->importer->import( $source, $title, $this->strategy->getSourceStore() ) ) {
			$this->createArchiveCleanupRevision( $title, $archiveTitle );
			$this->logger->info( "Completed import to $title from $archiveTitle" );
		} else {
			$this->logger->error( "Failed to complete import to $title from $archiveTitle" );
		}
	}

	/**
	 * Looks in the logging table to see if the provided title was last moved
	 * there by the user provided in the constructor. The provided user should
	 * be a system user for this task, as this assumes that user has never
	 * moved these pages outside the conversion process.
	 *
	 * This only considers the most recent move and not prior moves.  This allows
	 * for edge cases such as starting an import, canceling it, and manually
	 * reverting the move by a normal user.
	 *
	 * @param Title $title
	 * @return Title|null
	 */
	protected function getPageMovedFrom( Title $title ) {
		$row = $this->dbw->selectRow(
			array( 'logging', 'page' ),
			array( 'log_namespace', 'log_title', 'log_user' ),
			array(
				'page_namespace' => $title->getNamespace(),
				'page_title' => $title->getDBkey(),
				'log_page = page_id',
				'log_type' => 'move',
			),
			__METHOD__,
			array(
				'LIMIT' => 1,
				'ORDER BY' => 'log_timestamp DESC'
			)
		);

		// The page has never been moved
		if ( !$row ) {
			return null;
		}

		// The most recent move was not by our user
		if ( $row->log_user != $this->user->getId() ) {
			return null;
		}

		return Title::makeTitle( $row->log_namespace, $row->log_title );
	}

	/**
	 * Moves the source page to the destination. Does not leave behind a
	 * redirect, intending that flow will place a revision there for its new
	 * board.
	 *
	 * @param Title $from
	 * @param Title $to
	 * @throws ImportException on failed import
	 */
	protected function movePage( Title $from, Title $to ) {
		$mp = new MovePage( $from, $to );
		$valid = $mp->isValidMove();
		if ( !$valid->isOK() ) {
			$this->logger->error( $valid->getMessage()->text() );
			throw new ImportException( "It is not valid to move {$from} to {$to}" );
		}

		// Note that this comment must match the regex in self::getPageMovedFrom
		$status = $mp->move(
			/* user */ $this->user,
			/* reason */ $this->strategy->getMoveComment( $from, $to ),
			/* create redirect */ false
		);

		if ( !$status->isGood() ) {
			$this->logger->error( $status->getMessage()->text() );
			throw new ImportException( "Failed moving {$from} to {$to}" );
		}
	}

	/**
	 * Creates a new revision of the archived page with strategy-specific changes.
	 *
	 * @param Title $title Previous location of the page, before moving
	 * @param Title $archiveTitle Current location of the page, after moving
	 * @throws ImportException
	 */
	protected function createArchiveCleanupRevision( Title $title, Title $archiveTitle ) {
		$page = WikiPage::factory( $archiveTitle );
		 // doEditContent will do this anyway, but we need to now for the revision.
		$page->loadPageData( 'fromdbmaster' );
		$revision = $page->getRevision();
		if ( $revision === null ) {
			throw new ImportException( "Expected a revision at {$archiveTitle}" );
		}

		// Do not create revisions based on rev_deleted revisions.
		$content = $revision->getContent( Revision::FOR_PUBLIC );
		if ( !$content instanceof WikitextContent ) {
			throw new ImportException( "Expected wikitext content at: {$archiveTitle}" );
		}

		$newContent = $this->strategy->createArchiveCleanupRevisionContent( $content, $title );
		if ( $newContent === null ) {
			return;
		}

		$status = $page->doEditContent(
			$newContent,
			$this->strategy->getCleanupComment( $title, $archiveTitle ),
			EDIT_FORCE_BOT | EDIT_SUPPRESS_RC,
			false,
			$this->user
		);

		if ( !$status->isGood() ) {
			$this->logger->error( $status->getMessage()->text() );
			throw new ImportException( "Failed creating archive cleanup revision at {$archiveTitle}" );
		}
	}
}