Current File : /home/jvzmxxx/wiki1/extensions/Wikibase/lib/includes/LanguageFallbackChainFactory.php
<?php

namespace Wikibase;

use Babel;
use IContextSource;
use InvalidArgumentException;
use Language;
use LanguageConverter;
use MWException;
use User;

/**
 * Object creating LanguageFallbackChain objects in Wikibase.
 *
 * @since 0.4
 *
 * @license GPL-2.0+
 * @author Liangent < liangent@gmail.com >
 */
class LanguageFallbackChainFactory {

	/**
	 * Fallback levels
	 */
	const FALLBACK_ALL = 0xff;

	/**
	 * The language itself, e.g. 'en' for 'en'.
	 */
	const FALLBACK_SELF = 1;

	/**
	 * Other compatible languages that can be translated into the requested language
	 * (and translation is automatically done), e.g. 'sr', 'sr-ec' and 'sr-el' for 'sr'.
	 */
	const FALLBACK_VARIANTS = 2;

	/**
	 * All other language from the system fallback chain, e.g. 'de' and 'en' for 'de-formal'.
	 */
	const FALLBACK_OTHERS = 4;

	/**
	 * @var array[]
	 */
	private $languageCache;

	/**
	 * @var array[]
	 */
	private $userLanguageCache;

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

	/**
	 * @param bool $anonymousPageViewCached Whether full page outputs are cached for anons, so some
	 *                                      fine-grained fallbacks shouldn't be used for them.
	 */
	public function __construct( $anonymousPageViewCached = false ) {
		// @fixme fix instantiation of factory in various lib classes
		$this->anonymousPageViewCached = $anonymousPageViewCached;
	}

	/**
	 * Get the fallback chain based a single language, and specified fallback level.
	 *
	 * @param Language $language
	 * @param int $mode Bitfield of self::FALLBACK_*
	 *
	 * @return LanguageFallbackChain
	 */
	public function newFromLanguage( Language $language, $mode = self::FALLBACK_ALL ) {
		$languageCode = $language->getCode();

		if ( !isset( $this->languageCache[$languageCode][$mode] ) ) {
			$chain = $this->buildFromLanguage( $language, $mode );
			$this->languageCache[$languageCode][$mode] = new LanguageFallbackChain( $chain );
		}

		return $this->languageCache[$languageCode][$mode];
	}

	/**
	 * Get the fallback chain based a single language code, and specified fallback level.
	 *
	 * @param string $languageCode
	 * @param int $mode Bitfield of self::FALLBACK_*
	 *
	 * @return LanguageFallbackChain
	 */
	public function newFromLanguageCode( $languageCode, $mode = self::FALLBACK_ALL ) {
		$languageCode = LanguageWithConversion::validateLanguageCode( $languageCode );

		if ( !isset( $this->languageCache[$languageCode][$mode] ) ) {
			$chain = $this->buildFromLanguage( $languageCode, $mode );
			$this->languageCache[$languageCode][$mode] = new LanguageFallbackChain( $chain );
		}

		return $this->languageCache[$languageCode][$mode];
	}

	/**
	 * Build fallback chain array for a given language or validated language code.
	 *
	 * @param Language|string $language Language object or language code as string
	 * @param int $mode Bitfield of self::FALLBACK_*
	 * @param LanguageFallbackChain[] $chain for recursive calls
	 * @param array $fetched for recursive calls
	 *
	 * @throws InvalidArgumentException
	 * @return LanguageWithConversion[]
	 */
	private function buildFromLanguage( $language, $mode, array &$chain = array(), array &$fetched = array() ) {
		if ( !is_int( $mode ) ) {
			throw new InvalidArgumentException( '$mode must be an integer' );
		}

		if ( is_string( $language ) ) {
			$languageCode = $language;
		} else {
			$languageCode = $language->getCode();
		}

		if ( $mode & self::FALLBACK_SELF ) {
			if ( !isset( $fetched[$languageCode] ) ) {
				$chain[] = LanguageWithConversion::factory( $language );
				$fetched[$languageCode] = true;
			}
		}

		if ( $mode & self::FALLBACK_VARIANTS ) {
			/** @var Language $parentLanguage */
			$pieces = explode( '-', $languageCode );
			if ( !in_array( $pieces[0], LanguageConverter::$languagesWithVariants ) ) {
				$parentLanguage = null;
			} else {
				if ( is_string( $language ) ) {
					$language = Language::factory( $language );
				}
				$parentLanguage = $language->getParentLanguage();
			}
			if ( $parentLanguage ) {
				// It's less likely to trigger conversion mistakes by converting
				// zh-tw to zh-hk first instead of converting zh-cn to zh-tw.
				$variantFallbacks = $parentLanguage->getConverter()
					->getVariantFallbacks( $languageCode );
				if ( is_array( $variantFallbacks ) ) {
					$variants = array_unique( array_merge(
						$variantFallbacks, $parentLanguage->getVariants()
					) );
				} else {
					$variants = $parentLanguage->getVariants();
				}

				foreach ( $variants as $variant ) {
					if ( isset( $fetched[$variant] ) || !$parentLanguage->hasVariant( $variant ) ) {
						continue;
					}

					$chain[] = LanguageWithConversion::factory( $language, $variant );
					$fetched[$variant] = true;
				}
			}
		}

		if ( $mode & self::FALLBACK_OTHERS ) {
			// Regarding $mode in recursive calls:
			// * self is a must to have the fallback item itself included;
			// * respect the original caller about whether to include variants or not;
			// * others should be excluded as they'll be handled here in loops.
			$recursiveMode = $mode;
			$recursiveMode &= self::FALLBACK_VARIANTS;
			$recursiveMode |= self::FALLBACK_SELF;
			foreach ( Language::getFallbacksFor( $languageCode ) as $other ) {
				$this->buildFromLanguage( $other, $recursiveMode, $chain, $fetched );
			}
		}

		return $chain;
	}

	/**
	 * Construct the fallback chain based on a context. Currently it just uses user and language info in it.
	 *
	 * @param IContextSource $context
	 *
	 * @return LanguageFallbackChain
	 */
	public function newFromContext( IContextSource $context ) {
		return $this->newFromUserAndLanguageCode( $context->getUser(), $context->getLanguage()->getCode() );
	}

	/**
	 * Construct the fallback chain based on a context, but ignore the language info in it and use a specified one instead.
	 *
	 * @param IContextSource $context
	 * @param string $languageCode
	 *
	 * @return LanguageFallbackChain
	 */
	public function newFromContextAndLanguageCode( IContextSource $context, $languageCode ) {
		return $this->newFromUserAndLanguageCode( $context->getUser(), $languageCode );
	}

	/**
	 * Construct the fallback chain based on a user and a language, currently from data provided by Extension:Babel.
	 *
	 * @param User $user
	 * @param string $languageCode
	 *
	 * @return LanguageFallbackChain
	 */
	public function newFromUserAndLanguageCode( User $user, $languageCode ) {
		if ( !class_exists( Babel::class ) || $user->isAnon() ) {
			return $this->newFromLanguageCode( $languageCode, self::FALLBACK_ALL );
		}

		$languageCode = LanguageWithConversion::validateLanguageCode( $languageCode );

		if ( isset( $this->userLanguageCache[$user->getName()][$languageCode] ) ) {
			return $this->userLanguageCache[$user->getName()][$languageCode];
		}

		$babel = $this->getBabel( $languageCode, $user );

		$chain = $this->buildFromBabel( $babel );
		$languageFallbackChain = new LanguageFallbackChain( $chain );

		$this->userLanguageCache[$user->getName()][$languageCode] = $languageFallbackChain;

		return $languageFallbackChain;
	}

	private function getBabel( $languageCode, $user ) {
		$babel = array();

		$babelCategoryNames = $this->getBabelCategoryNames();

		if ( count( $babelCategoryNames ) ) {
			// A little redundant but it's the only way to get required information with current Babel API.
			$previousLevelBabel = array();

			foreach ( $babelCategoryNames as $level => $_ ) {
				// Make the current language at the top of the chain.
				$levelBabel = array_unique( array_merge(
					array( $languageCode ),
					Babel::getUserLanguages( $user, $level )
				) );

				$babel[$level] = array_diff( $levelBabel, $previousLevelBabel );
				$previousLevelBabel = $levelBabel;
			}
		} else {
			$babel['N'] = array( $languageCode );
		}

		return $babel;
	}

	private function getBabelCategoryNames() {
		global $wgBabelCategoryNames;

		$babelCategoryNames = array_filter(
			$wgBabelCategoryNames,
			function( $category ) {
				return $category !== false;
			}
		);

		krsort( $babelCategoryNames );

		return $babelCategoryNames;
	}

	/**
	 * Build fallback chain array for a given babel array.
	 *
	 * @param array $babel
	 *
	 * @return LanguageWithConversion[]
	 */
	public function buildFromBabel( array $babel ) {
		$chain = array();
		$fetched = array();

		// First pass to get "compatible" languages (self and variants)
		foreach ( $babel as $languageCodes ) { // Already sorted when added
			foreach ( array( self::FALLBACK_SELF, self::FALLBACK_VARIANTS ) as $mode ) {
				foreach ( $languageCodes as $languageCode ) {
					try {
						$languageCode = LanguageWithConversion::validateLanguageCode( $languageCode );
					} catch ( MWException $e ) {
						continue;
					}
					$this->buildFromLanguage( $languageCode, $mode, $chain, $fetched );
				}
			}
		}

		// Second pass to get other languages from system fallback chain
		foreach ( $babel as $languageCodes ) {
			foreach ( $languageCodes as $languageCode ) {
				try {
					$languageCode = LanguageWithConversion::validateLanguageCode( $languageCode );
				} catch ( MWException $e ) {
					continue;
				}
				$this->buildFromLanguage(
					$languageCode,
					self::FALLBACK_OTHERS | self::FALLBACK_VARIANTS,
					$chain,
					$fetched
				);
			}
		}

		return $chain;
	}

}