Current File : /home/jvzmxxx/wiki1/extensions/Flow/includes/Dump/Exporter.php
<?php

namespace Flow\Dump;

use BatchRowIterator;
use DatabaseBase;
use Exception;
use Flow\Collection\PostSummaryCollection;
use Flow\Container;
use Flow\Data\ManagerGroup;
use Flow\Model\AbstractRevision;
use Flow\Model\Header;
use Flow\Model\PostRevision;
use Flow\Model\PostSummary;
use Flow\Model\Workflow;
use Flow\RevisionActionPermissions;
use Flow\Search\Iterators\AbstractIterator;
use Flow\Search\Iterators\HeaderIterator;
use Flow\Search\Iterators\TopicIterator;
use ReflectionProperty;
use TimestampException;
use User;
use WikiExporter;
use Xml;

class Exporter extends WikiExporter {
	/**
	 * Map of [db column name => xml attribute name]
	 *
	 * @var array
	 */
	public static $map = array(
		'rev_id' => 'id',
		'rev_user_id' => 'userid',
		'rev_user_ip' => 'userip',
		'rev_user_wiki' => 'userwiki',
		'rev_parent_id' => 'parentid',
		'rev_change_type' => 'changetype',
		'rev_type' => 'type',
		'rev_type_id' => 'typeid',
		'rev_content' => 'content',
		'rev_content_url' => 'contenturl',
		'rev_flags' => 'flags',
		'rev_mod_state' => 'modstate',
		'rev_mod_user_id' => 'moduserid',
		'rev_mod_user_ip' => 'moduserip',
		'rev_mod_user_wiki' => 'moduserwiki',
		'rev_mod_timestamp' => 'modtimestamp',
		'rev_mod_reason' => 'modreason',
		'rev_last_edit_id' => 'lasteditid',
		'rev_edit_user_id' => 'edituserid',
		'rev_edit_user_ip' => 'edituserip',
		'rev_edit_user_wiki' => 'edituserwiki',
		'rev_content_length' => 'contentlength',
		'rev_previous_content_length' => 'previouscontentlength',

		'tree_parent_id' => 'treeparentid',
		'tree_rev_descendant_id' => 'treedescendantid',
		'tree_rev_id' => 'treerevid',
		'tree_orig_user_id' => 'treeoriguserid',
		'tree_orig_user_ip' => 'treeoriguserip',
		'tree_orig_user_wiki' => 'treeoriguserwiki',
	);

	/**
	   @var ReflectionProperty $prevRevisionProperty Previous revision property
	*/
	protected $prevRevisionProperty;

	/**
	   @var ReflectionProperty $changeTypeProperty Change type property
	*/
	protected $changeTypeProperty;

	/**
	 * {@inheritDoc}
	 */
	function __construct( $db, $history = WikiExporter::CURRENT,
		$buffer = WikiExporter::BUFFER, $text = WikiExporter::TEXT ) {

		parent::__construct( $db, $history, $buffer, $text );
		$this->prevRevisionProperty = new ReflectionProperty( 'Flow\Model\AbstractRevision', 'prevRevision' );
		$this->prevRevisionProperty->setAccessible( true );

		$this->changeTypeProperty = new ReflectionProperty( 'Flow\Model\AbstractRevision', 'changeType' );
		$this->changeTypeProperty->setAccessible( true );
	}

	public static function schemaVersion() {
		/*
		 * Be sure to also update the schema/namespace on mediawiki.org when
		 * making any changes:
		 * @see https://gerrit.wikimedia.org/r/#/c/281640/
		 */
		return '1.0';
	}

	public function openStream() {
		global $wgLanguageCode;
		$version = static::schemaVersion();

		$output = Xml::openElement(
			'mediawiki',
			array(
				'xmlns' => "http://www.mediawiki.org/xml/flow-$version/",
				'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
				'xsi:schemaLocation' => "http://www.mediawiki.org/xml/flow-$version/ http://www.mediawiki.org/xml/flow-$version.xsd",
				'version' => $version,
				'xml:lang' => $wgLanguageCode
			)
		) . "\n";
		$this->sink->write( $output );
	}

	/**
	 * @param string[]|null $pages Array of DB-prefixed page titles
	 * @param int|null $startId page_id to start from (inclusive)
	 * @param int|null $endId page_id to end (exclusive)
	 * @return BatchRowIterator
	 */
	public function getWorkflowIterator( array $pages = null, $startId = null, $endId = null ) {
		/** @var DatabaseBase $dbr */
		$dbr = Container::get( 'db.factory' )->getDB( DB_SLAVE );

		$iterator = new BatchRowIterator( $dbr, 'flow_workflow', 'workflow_id', 300 );
		$iterator->setFetchColumns( array( '*' ) );
		$iterator->addConditions( array( 'workflow_wiki' => wfWikiID() ) );
		$iterator->addConditions( array( 'workflow_type' => 'discussion' ) );

		if ( $pages ) {
			$pageConds = array();
			foreach ( $pages as $page ) {
				$title = \Title::newFromDBkey( $page );
				$pageConds[] = $dbr->makeList(
					array(
						'workflow_namespace' => $title->getNamespace(),
						'workflow_title_text' => $title->getDBkey()
					),
					LIST_AND
				);
			}

			$iterator->addConditions( array( $dbr->makeList( $pageConds, LIST_OR ) ) );
		}
		if ( $startId ) {
			$iterator->addConditions( array( 'workflow_page_id >= ' . $dbr->addQuotes( $startId ) ) );
		}
		if ( $endId ) {
			$iterator->addConditions( array( 'workflow_page_id < ' . $dbr->addQuotes( $endId ) ) );
		}

		return $iterator;
	}

	/**
	 * @param BatchRowIterator $workflowIterator
	 * @throws Exception
	 * @throws TimestampException
	 * @throws \Flow\Exception\InvalidInputException
	 */
	public function dump( BatchRowIterator $workflowIterator ) {
		foreach ( $workflowIterator as $rows ) {
			foreach ( $rows as $row ) {
				$workflow = Workflow::fromStorageRow( (array) $row );

				$headerIterator = Container::get( 'search.index.iterators.header' );
				$topicIterator = Container::get( 'search.index.iterators.topic' );
				/** @var AbstractIterator $iterator */
				foreach ( array( $headerIterator, $topicIterator ) as $iterator ) {
					$iterator->setPage( $row->workflow_page_id );
				}

				$this->formatWorkflow( $workflow, $headerIterator, $topicIterator );
			}
		}
	}

	protected function formatWorkflow( Workflow $workflow, HeaderIterator $headerIterator, TopicIterator $topicIterator ) {
		if ( $workflow->isDeleted() ) {
			return;
		}

		$output = Xml::openElement( 'board', array(
			'id' => $workflow->getId()->getAlphadecimal(),
			'title' => $workflow->getOwnerTitle()->getPrefixedDBkey(),
		) ) . "\n";
		$this->sink->write( $output );

		foreach ( $headerIterator as $revision ) {
			/** @var Header $revision */
			$this->formatHeader( $revision );
		}
		foreach ( $topicIterator as $revision ) {
			/** @var PostRevision $revision */
			$this->formatTopic( $revision );
		}

		$output = Xml::closeElement( 'board' ) . "\n";
		$this->sink->write( $output );
	}

	protected function formatTopic( PostRevision $revision ) {
		if ( !$this->isAllowed( $revision ) ) {
			return;
		}

		$output = Xml::openElement( 'topic', array(
			'id' => $revision->getCollectionId()->getAlphadecimal(),
		) ) . "\n";
		$this->sink->write( $output );

		$this->formatPost( $revision );

		// find summary for this topic & add it as revision
		$summaryCollection = PostSummaryCollection::newFromId( $revision->getCollectionId() );
		try {
			/** @var PostSummary $summary */
			$summary = $summaryCollection->getLastRevision();
			$this->formatSummary( $summary );
		} catch ( \Exception $e ) {
			// no summary - that's ok!
		}

		$output = Xml::closeElement( 'topic' ) . "\n";
		$this->sink->write( $output );
	}

	protected function formatHeader( Header $revision ) {
		if ( !$this->isAllowed( $revision ) ) {
			return;
		}

		$output = Xml::openElement( 'description', array(
			'id' => $revision->getCollectionId()->getAlphadecimal()
		) ) . "\n";
		$this->sink->write( $output );

		$this->formatRevisions( $revision );

		$output = Xml::closeElement( 'description' ) . "\n";
		$this->sink->write( $output );
	}

	protected function formatPost( PostRevision $revision ) {
		if ( !$this->isAllowed( $revision ) ) {
			return;
		}

		$output = Xml::openElement( 'post', array(
			'id' => $revision->getCollectionId()->getAlphadecimal()
		) ) . "\n";
		$this->sink->write( $output );

		$this->formatRevisions( $revision );

		if ( $revision->getChildren() ) {
			$output = Xml::openElement( 'children' ) . "\n";
			$this->sink->write( $output );

			foreach ( $revision->getChildren() as $child ) {
				$this->formatPost( $child );
			}

			$output = Xml::closeElement( 'children' ) . "\n";
			$this->sink->write( $output );
		}

		$output = Xml::closeElement( 'post' ) . "\n";
		$this->sink->write( $output );
	}

	protected function formatSummary( PostSummary $revision ) {
		if ( !$this->isAllowed( $revision ) ) {
			return;
		}

		$output = Xml::openElement( 'summary', array(
			'id' => $revision->getCollectionId()->getAlphadecimal()
		) ) . "\n";
		$this->sink->write( $output );

		$this->formatRevisions( $revision );

		$output = Xml::closeElement( 'summary' ) . "\n";
		$this->sink->write( $output );
	}

	protected function formatRevisions( AbstractRevision $revision ) {
		$output = Xml::openElement( 'revisions' ) . "\n";
		$this->sink->write( $output );

		$collection = $revision->getCollection();
		if ( $this->history === WikiExporter::FULL ) {
			/** @var AbstractRevision[] $revisions */
			$revisions = array_reverse( $collection->getAllRevisions() );
			$prevId = null;

			foreach ( $revisions as $revision ) {
				if ( $this->isAllowed( $revision ) ) {
					if ( $prevId !== null ) {
						// override parent id: this is used to get rid of gaps
						// that are caused by moderated items, where the
						// revision tree would be incorrect
						$this->prevRevisionProperty->setValue( $revision, $prevId );

						// Since $prevId is set, we know
						// there was a gap, and the original
						// hide-topic/delete-topic/suppress-topic
						// was removed. Since that is used for
						// listeners in FlowActions.php, we replace
						// restore-topic with edit-title and make a
						// null edit (we don't do null edits in the
						// normal application flow, but this
						// provides a way to replace restore).
						$oldChangeType = $revision->getChangeType();

						if ( $oldChangeType === 'restore-topic' ) {
							$this->changeTypeProperty->setValue( $revision, 'edit-title' );
						}

						if ( $oldChangeType === 'restore-post' ) {
							$this->changeTypeProperty->setValue( $revision, 'edit-post' );
						}

						$prevId = null;
					}
					$this->formatRevision( $revision );
				} elseif ( $prevId === null ) {
					// if revision can't be dumped, store its parent id so we
					// can re-apply it to the next one that can be displayed, so
					// we don't have gaps
					$prevId = $revision->getPrevRevisionId();
				}
			}
		} elseif ( $this->history === WikiExporter::CURRENT ) {
			$first = $collection->getFirstRevision();

			// storing only last revision won't work (it'll reference non-existing
			// parents): we'll construct a bogus revision with most of the original
			// metadata, but with the current content & id (= timestamp)
			$first = $first->toStorageRow( $first );
			$last = $revision->toStorageRow( $revision );
			$first['rev_id'] = $last['rev_id'];
			$first['rev_content'] = $last['rev_content'];
			$first['rev_flags'] = $last['rev_flags'];
			if ( isset( $first['tree_rev_id'] ) ) {
				// PostRevision-only: tree_rev_id must match rev_id
				$first['tree_rev_id'] = $first['rev_id'];
			}

			// clear buffered cache, to make sure it doesn't serve the existing (already
			// loaded) revision when trying to turn our bogus mixed data into a revision
			/** @var ManagerGroup $storage */
			$storage = Container::get( 'storage' );
			$storage->clear();

			$mix = $revision->fromStorageRow( $first );

			$this->formatRevision( $mix );
		}

		$output = Xml::closeElement( 'revisions' ) . "\n";
		$this->sink->write( $output );
	}

	protected function formatRevision( AbstractRevision $revision ) {
		if ( !$this->isAllowed( $revision ) ) {
			return;
		}

		$attribs = $revision->toStorageRow( $revision );

		// make sure there are no leftover key columns (unknown to $attribs)
		$keys = array_intersect_key(static::$map, $attribs );
		// now make sure $values columns are in the same order as $keys are
		// (array_merge) and there are no leftover columns (array_intersect_key)
		$values = array_intersect_key( array_merge( $keys, $attribs ), $keys );
		// combine them
		$attribs = array_combine( $keys, $values );
		// and get rid of columns with null values
		$attribs = array_filter( $attribs, function ( $value ) {
			return $value !== null;
		} );

		// references to external store etc. are useless; we'll include the real
		// content as node text
		unset($attribs['content'], $attribs['contenturl']);
		$format = $revision->getContentFormat();
		$attribs['flags'] = 'utf-8,' . $format;

		$output = Xml::element(
			'revision',
			$attribs,
			$revision->getContent( $format )
		) . "\n";
		$this->sink->write( $output );
	}

	/**
	 * Test if anon users are allowed to view a particular revision.
	 *
	 * @param AbstractRevision $revision
	 * @return bool
	 */
	protected function isAllowed( AbstractRevision $revision ) {
		$user = User::newFromId( 0 );
		$actions = Container::get( 'flow_actions' );
		$permissions = new RevisionActionPermissions( $actions, $user );

		return $permissions->isAllowed( $revision, 'view' );
	}
}