Current File : /home/jvzmxxx/wiki1/extensions/Wikibase/client/includes/Changes/AffectedPagesFinder.php
<?php

namespace Wikibase\Client\Changes;

use ArrayIterator;
use Diff\DiffOp\Diff\Diff;
use Diff\DiffOp\DiffOp;
use Diff\DiffOp\DiffOpAdd;
use Diff\DiffOp\DiffOpChange;
use Diff\DiffOp\DiffOpRemove;
use InvalidArgumentException;
use Title;
use Traversable;
use UnexpectedValueException;
use Wikibase\Change;
use Wikibase\Client\Store\TitleFactory;
use Wikibase\Client\Usage\EntityUsage;
use Wikibase\Client\Usage\PageEntityUsages;
use Wikibase\Client\Usage\UsageAspectTransformer;
use Wikibase\Client\Usage\UsageLookup;
use Wikibase\DataModel\Entity\EntityId;
use Wikibase\DataModel\Services\Diff\EntityDiff;
use Wikibase\DataModel\Services\Diff\ItemDiff;
use Wikibase\EntityChange;
use Wikibase\ItemChange;
use Wikibase\Lib\Store\StorageException;

/**
 * @license GPL-2.0+
 * @author Daniel Kinzler
 * @author Katie Filbert < aude.wiki@gmail.com >
 */
class AffectedPagesFinder {

	/**
	 * @var UsageLookup
	 */
	private $usageLookup;

	/**
	 * @var TitleFactory
	 */
	private $titleFactory;

	/**
	 * @var string
	 */
	private $siteId;

	/**
	 * @var string
	 */
	private $contentLanguageCode;

	/**
	 * @var bool
	 */
	private $checkPageExistence;

	/**
	 * @param UsageLookup $usageLookup
	 * @param TitleFactory $titleFactory
	 * @param string $siteId
	 * @param string $contentLanguageCode
	 * @param bool $checkPageExistence
	 *
	 * @throws InvalidArgumentException
	 */
	public function __construct(
		UsageLookup $usageLookup,
		TitleFactory $titleFactory,
		$siteId,
		$contentLanguageCode,
		$checkPageExistence = true
	) {
		if ( !is_string( $siteId ) ) {
			throw new InvalidArgumentException( '$siteId must be a string' );
		}

		if ( !is_string( $contentLanguageCode ) ) {
			throw new InvalidArgumentException( '$contentLanguageCode must be a string' );
		}

		if ( !is_bool( $checkPageExistence ) ) {
			throw new InvalidArgumentException( '$checkPageExistence must be a boolean' );
		}

		$this->usageLookup = $usageLookup;
		$this->titleFactory = $titleFactory;
		$this->siteId = $siteId;
		$this->contentLanguageCode = $contentLanguageCode;
		$this->checkPageExistence = $checkPageExistence;
	}

	/**
	 * @since 0.5
	 *
	 * @param Change $change
	 *
	 * @return ArrayIterator of PageEntityUsage
	 */
	public function getAffectedUsagesByPage( Change $change ) {
		if ( $change instanceof EntityChange ) {
			$usages = $this->getAffectedPages( $change );
			return $this->filterUpdates( $usages );
		}

		return new ArrayIterator();
	}

	/**
	 * @param EntityChange $change
	 *
	 * @return string[]
	 */
	public function getChangedAspects( EntityChange $change ) {
		$aspects = array();

		$diff = $change->getDiff();
		$remainingDiffOps = count( $diff ); // this is a "deep" count!

		if ( $remainingDiffOps === 0 ) {
			// HACK: assume an empty diff implies that some "other" aspect of the entity was changed.
			// This is needed since EntityChangeFactory::newFromUpdate suppresses statement, description
			// and alias diffs for performance reasons.
			// For a better solution, see T113468.
			$aspects[] = EntityUsage::OTHER_USAGE;
			return $aspects;
		}

		if ( $diff instanceof ItemDiff && !$diff->getSiteLinkDiff()->isEmpty() ) {
			$siteLinkDiff = $diff->getSiteLinkDiff();

			$aspects[] = EntityUsage::SITELINK_USAGE;
			$remainingDiffOps -= count( $siteLinkDiff );

			if ( isset( $siteLinkDiff[$this->siteId] )
				&& !$this->isBadgesOnlyChange( $siteLinkDiff[$this->siteId] )
			) {
				$aspects[] = EntityUsage::TITLE_USAGE;
			}
		}

		if ( $diff instanceof EntityDiff && !$diff->getLabelsDiff()->isEmpty() ) {
			$labelsDiff = $diff->getLabelsDiff();

			if ( !empty( $labelsDiff ) ) {
				$labelAspects = $this->getChangedLabelAspects( $labelsDiff );
				$aspects = array_merge( $aspects, $labelAspects );
				$remainingDiffOps -= count( $labelAspects );
			}
		}
		// FIXME: EntityChange suppresses various kinds of diffs (see above). T113468.

		if ( $remainingDiffOps > 0 ) {
			$aspects[] = EntityUsage::OTHER_USAGE;
		}

		return $aspects;
	}

	/**
	 * @param Diff $labelsDiff
	 *
	 * @return string[]
	 */
	private function getChangedLabelAspects( Diff $labelsDiff ) {
		$aspects = array();

		foreach ( $labelsDiff as $lang => $diffOp ) {
			$aspects[] = EntityUsage::makeAspectKey( EntityUsage::LABEL_USAGE, $lang );
		}

		return $aspects;
	}

	/**
	 * Returns the page updates implied by the given the change.
	 *
	 * @param EntityChange $change
	 *
	 * @return Traversable of PageEntityUsages
	 */
	private function getAffectedPages( EntityChange $change ) {
		$entityId = $change->getEntityId();
		$changedAspects = $this->getChangedAspects( $change );

		$usages = $this->usageLookup->getPagesUsing(
			// @todo: more than one entity at once!
			array( $entityId ),
			// Look up pages that are marked as either using one of the changed or all aspects
			array_merge( $changedAspects, array( EntityUsage::ALL_USAGE ) )
		);

		// @todo: use iterators throughout!
		$usages = iterator_to_array( $usages, true );

		$usages = $this->transformAllPageEntityUsages( $usages, $entityId, $changedAspects );

		if ( $change instanceof ItemChange && in_array( EntityUsage::TITLE_USAGE, $changedAspects ) ) {
			$siteLinkDiff = $change->getSiteLinkDiff();
			$namesFromDiff = $this->getPagesReferencedInDiff( $siteLinkDiff );
			$titlesFromDiff = $this->getTitlesFromTexts( $namesFromDiff );
			$usagesFromDiff = $this->makeVirtualUsages( $titlesFromDiff, $entityId, array( EntityUsage::SITELINK_USAGE ) );

			//FIXME: we can't really merge if $usages is an iterator, not an array.
			//TODO: Inject $usagesFromDiff "on the fly" while streaming other usages.
			//NOTE: $usages must pass through mergeUsagesInto for re-indexing
			$mergedUsages = array();
			$this->mergeUsagesInto( $usages, $mergedUsages );
			$this->mergeUsagesInto( $usagesFromDiff, $mergedUsages );
			$usages = $mergedUsages;
		}

		return new ArrayIterator( $usages );
	}

	/**
	 * @param PageEntityUsages[] $from
	 * @param PageEntityUsages[] &$into Array to merge into
	 */
	private function mergeUsagesInto( array $from, array &$into ) {
		foreach ( $from as $pageEntityUsages ) {
			$key = $pageEntityUsages->getPageId();

			if ( isset( $into[$key] ) ) {
				$into[$key]->addUsages( $pageEntityUsages->getUsages() );
			} else {
				$into[$key] = $pageEntityUsages;
			}
		}
	}

	/**
	 * @param Diff $siteLinkDiff
	 *
	 * @throws UnexpectedValueException
	 * @return string[]
	 */
	private function getPagesReferencedInDiff( Diff $siteLinkDiff ) {
		$pagesToUpdate = array();

		// $siteLinkDiff changed from containing atomic diffs to
		// containing map diffs. For B/C, handle both cases.
		$siteLinkDiffOp = $siteLinkDiff[$this->siteId];

		if ( $siteLinkDiffOp instanceof Diff && array_key_exists( 'name', $siteLinkDiffOp ) ) {
			$siteLinkDiffOp = $siteLinkDiffOp['name'];
		}

		if ( $siteLinkDiffOp instanceof DiffOpAdd ) {
			$pagesToUpdate[] = $siteLinkDiffOp->getNewValue();
		} elseif ( $siteLinkDiffOp instanceof DiffOpRemove ) {
			$pagesToUpdate[] = $siteLinkDiffOp->getOldValue();
		} elseif ( $siteLinkDiffOp instanceof DiffOpChange ) {
			$pagesToUpdate[] = $siteLinkDiffOp->getNewValue();
			$pagesToUpdate[] = $siteLinkDiffOp->getOldValue();
		} else {
			throw new UnexpectedValueException(
				"Unknown change operation: " . get_class( $siteLinkDiffOp ) . ")"
			);
		}

		return $pagesToUpdate;
	}

	/**
	 * @param DiffOp $siteLinkDiffOp
	 *
	 * @return bool
	 */
	private function isBadgesOnlyChange( DiffOp $siteLinkDiffOp ) {
		return $siteLinkDiffOp instanceof Diff && !array_key_exists( 'name', $siteLinkDiffOp );
	}

	/**
	 * Filters updates based on namespace. This removes duplicates, non-existing pages, and pages from
	 * namespaces that are not considered "enabled" by the namespace checker.
	 *
	 * @param Traversable $usages A traversable of PageEntityUsages.
	 *
	 * @return ArrayIterator of PageEntityUsages
	 */
	private function filterUpdates( Traversable $usages ) {
		$titlesToUpdate = array();

		/** @var PageEntityUsages $pageEntityUsages */
		foreach ( $usages as $pageEntityUsages ) {
			try {
				$title = $this->titleFactory->newFromID( $pageEntityUsages->getPageId() );
			} catch ( StorageException $ex ) {
				// page not found, skip
				continue;
			}

			if ( $this->checkPageExistence && !$title->exists() ) {
				continue;
			}

			$key = $pageEntityUsages->getPageId();
			$titlesToUpdate[$key] = $pageEntityUsages;
		}

		return new ArrayIterator( $titlesToUpdate );
	}

	/**
	 * @param string[] $names
	 *
	 * @return Title[]
	 */
	private function getTitlesFromTexts( array $names ) {
		$titles = array();

		foreach ( $names as $name ) {
			try {
				$titles[] = $this->titleFactory->newFromText( $name );
			} catch ( StorageException $ex ) {
				// Invalid title in the diff? Skip.
			}
		}

		return $titles;
	}

	/**
	 * @param Title[] $titles
	 * @param EntityId $entityId
	 * @param string[] $aspects
	 *
	 * @return PageEntityUsages[]
	 */
	private function makeVirtualUsages( array $titles, EntityId $entityId, array $aspects ) {
		$usagesForItem = array();
		foreach ( $aspects as $aspect ) {
			list( $aspect, $modifier ) = EntityUsage::splitAspectKey( $aspect );
			$usagesForItem[] = new EntityUsage( $entityId, $aspect, $modifier );
		}

		$usagesPerPage = array();
		foreach ( $titles as $title ) {
			$pageId = $title->getArticleID();

			if ( $pageId === 0 ) {
				wfDebugLog( 'WikibaseChangeNotification', __METHOD__ . ': Article ID for '
					. $title->getFullText() . ' is 0.' );

				continue;
			}

			$usagesPerPage[$pageId] = new PageEntityUsages( $pageId, $usagesForItem );
		}

		return $usagesPerPage;
	}

	/**
	 * @param PageEntityUsages[] $usages
	 * @param EntityId $entityId
	 * @param string[] $changedAspects
	 *
	 * @return PageEntityUsages[]
	 */
	private function transformAllPageEntityUsages( array $usages, EntityId $entityId, array $changedAspects ) {
		$aspectTransformer = new UsageAspectTransformer();
		$aspectTransformer->setRelevantAspects( $entityId, $changedAspects );

		$transformed = array();

		foreach ( $usages as $key => $usagesOnPage ) {
			$transformedUsagesOnPage = $aspectTransformer->transformPageEntityUsages( $usagesOnPage );

			if ( !$transformedUsagesOnPage->isEmpty() ) {
				$transformed[$key] = $transformedUsagesOnPage;
			}
		}

		return $transformed;
	}

}