See <https://www.mediawiki.org/wiki/OOUI/Themes> for details.
* (T229035) The GetUserBlock hook was added. Use this instead of
GetBlockedStatus.
+* ObjectFactory is available as a service. When used as a service, the object
+ specs can now specify needed DI services.
=== External library changes in 1.34 ===
engines.
* Skin::escapeSearchLink() is deprecated. Use Skin::getSearchLink() or the skin
template option 'searchaction' instead.
+* Skin::getRevisionId() and Skin::isRevisionCurrent() have been deprecated.
+ Use OutputPage::getRevisionId() and OutputPage::isRevisionCurrent() instead.
* LoadBalancer::haveIndex() and LoadBalancer::isNonZeroLoad() have
been deprecated.
* FileBackend::getWikiId() has been deprecated.
* TempFSFile::factory() has been deprecated. Use TempFSFileFactory instead.
* wfIsBadImage() is deprecated. Use the BadFileLookup service instead.
* Language::getLocalisationCache() is deprecated. Use MediaWikiServices.
+* The following Language methods are deprecated: isSupportedLanguage,
+ isValidCode, isValidBuiltInCode, isKnownLanguageTag, fetchLanguageNames,
+ fetchLanguageName, getFileName, getMessagesFileName, getJsonMessagesFileName.
+ Use the new LanguageNameUtils class instead. (Note that fetchLanguageName(s)
+ are called getLanguageName(s) in the new class.)
=== Other changes in 1.34 ===
* …
'MediaWiki\\Languages\\Data\\CrhExceptions' => __DIR__ . '/languages/data/CrhExceptions.php',
'MediaWiki\\Languages\\Data\\Names' => __DIR__ . '/languages/data/Names.php',
'MediaWiki\\Languages\\Data\\ZhConversion' => __DIR__ . '/languages/data/ZhConversion.php',
+ 'MediaWiki\\Languages\\LanguageNameUtils' => __DIR__ . '/includes/language/LanguageNameUtils.php',
'MediaWiki\\Logger\\ConsoleLogger' => __DIR__ . '/includes/debug/logger/ConsoleLogger.php',
'MediaWiki\\Logger\\ConsoleSpi' => __DIR__ . '/includes/debug/logger/ConsoleSpi.php',
'MediaWiki\\Logger\\LegacyLogger' => __DIR__ . '/includes/debug/logger/LegacyLogger.php',
'MediumSpecificBagOStuff' => __DIR__ . '/includes/libs/objectcache/MediumSpecificBagOStuff.php',
'MemcLockManager' => __DIR__ . '/includes/libs/lockmanager/MemcLockManager.php',
'MemcachedBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedBagOStuff.php',
- 'MemcachedClient' => __DIR__ . '/includes/libs/objectcache/MemcachedClient.php',
+ 'MemcachedClient' => __DIR__ . '/includes/libs/objectcache/utils/MemcachedClient.php',
'MemcachedPeclBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedPeclBagOStuff.php',
'MemcachedPhpBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedPhpBagOStuff.php',
'MemoizedCallable' => __DIR__ . '/includes/libs/MemoizedCallable.php',
protected function addPageProtectionWarningHeaders() {
$out = $this->context->getOutput();
if ( $this->mTitle->isProtected( 'edit' ) &&
- MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels(
- $this->mTitle->getNamespace()
+ MediaWikiServices::getInstance()->getPermissionManager()->getNamespaceRestrictionLevels(
+ $this->getTitle()->getNamespace()
) !== [ '' ]
) {
# Is the title semi-protected?
* @return array
*/
public static function getRestrictionLevels( $index, User $user = null ) {
- return MediaWikiServices::getInstance()->getNamespaceInfo()->
- getRestrictionLevels( $index, $user );
+ return MediaWikiServices::getInstance()
+ ->getPermissionManager()
+ ->getNamespaceRestrictionLevels( $index, $user );
}
/**
use MediaWiki\Block\BlockRestrictionStore;
use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
use MediaWiki\Http\HttpRequestFactory;
+use MediaWiki\Languages\LanguageNameUtils;
use MediaWiki\Page\MovePageFactory;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Preferences\PreferencesFactory;
use TitleFormatter;
use TitleParser;
use VirtualRESTServiceClient;
+use Wikimedia\ObjectFactory;
use Wikimedia\Rdbms\LBFactory;
use Wikimedia\Services\SalvageableService;
use Wikimedia\Services\ServiceContainer;
return $this->getService( 'InterwikiLookup' );
}
+ /**
+ * @since 1.34
+ * @return LanguageNameUtils
+ */
+ public function getLanguageNameUtils() {
+ return $this->getService( 'LanguageNameUtils' );
+ }
+
/**
* @since 1.28
* @return LinkCache
return $this->getService( 'NameTableStoreFactory' );
}
+ /**
+ * ObjectFactory is intended for instantiating "handlers" from declarative definitions,
+ * such as Action API modules, special pages, or REST API handlers.
+ *
+ * @since 1.34
+ * @return ObjectFactory
+ */
+ public function getObjectFactory() {
+ return $this->getService( 'ObjectFactory' );
+ }
+
/**
* @since 1.32
* @return OldRevisionImporter
return $this->mRevisionId;
}
+ /**
+ * Whether the revision displayed is the latest revision of the page
+ *
+ * @since 1.34
+ * @return bool
+ */
+ public function isRevisionCurrent() {
+ return $this->mRevisionId == 0 || $this->mRevisionId == $this->getTitle()->getLatestRevID();
+ }
+
/**
* Set the timestamp of the revision which will be displayed. This is used
* to avoid a extra DB call in Skin::lastModified().
'BlockDisablesLogin',
'GroupPermissions',
'RevokePermissions',
- 'AvailableRights'
+ 'AvailableRights',
+ 'NamespaceProtection',
+ 'RestrictionLevels'
];
/** @var ServiceOptions */
* Check if user is allowed to make any action
*
* @param UserIdentity $user
- * @param string[] ...$actions
+ * // TODO: HHVM can't create mocks with variable params @param string ...$actions
* @return bool True if user is allowed to perform *any* of the given actions
* @since 1.34
*/
- public function userHasAnyRight( UserIdentity $user, ...$actions ) {
+ public function userHasAnyRight( UserIdentity $user ) {
+ $actions = array_slice( func_get_args(), 1 );
foreach ( $actions as $action ) {
if ( $this->userHasRight( $user, $action ) ) {
return true;
* Check if user is allowed to make all actions
*
* @param UserIdentity $user
- * @param string[] ...$actions
+ * // TODO: HHVM can't create mocks with variable params @param string ...$actions
* @return bool True if user is allowed to perform *all* of the given actions
* @since 1.34
*/
- public function userHasAllRights( UserIdentity $user, ...$actions ) {
+ public function userHasAllRights( UserIdentity $user ) {
+ $actions = array_slice( func_get_args(), 1 );
foreach ( $actions as $action ) {
if ( !$this->userHasRight( $user, $action ) ) {
return false;
return $this->allRights;
}
+ /**
+ * Determine which restriction levels it makes sense to use in a namespace,
+ * optionally filtered by a user's rights.
+ *
+ * @param int $index Index to check
+ * @param UserIdentity|null $user User to check
+ * @return array
+ */
+ public function getNamespaceRestrictionLevels( $index, UserIdentity $user = null ) {
+ if ( !isset( $this->options->get( 'NamespaceProtection' )[$index] ) ) {
+ // All levels are valid if there's no namespace restriction.
+ // But still filter by user, if necessary
+ $levels = $this->options->get( 'RestrictionLevels' );
+ if ( $user ) {
+ $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) {
+ $right = $level;
+ if ( $right == 'sysop' ) {
+ $right = 'editprotected'; // BC
+ }
+ if ( $right == 'autoconfirmed' ) {
+ $right = 'editsemiprotected'; // BC
+ }
+ return $this->userHasRight( $user, $right );
+ } ) );
+ }
+ return $levels;
+ }
+
+ // $wgNamespaceProtection can require one or more rights to edit the namespace, which
+ // may be satisfied by membership in multiple groups each giving a subset of those rights.
+ // A restriction level is redundant if, for any one of the namespace rights, all groups
+ // giving that right also give the restriction level's right. Or, conversely, a
+ // restriction level is not redundant if, for every namespace right, there's at least one
+ // group giving that right without the restriction level's right.
+ //
+ // First, for each right, get a list of groups with that right.
+ $namespaceRightGroups = [];
+ foreach ( (array)$this->options->get( 'NamespaceProtection' )[$index] as $right ) {
+ if ( $right == 'sysop' ) {
+ $right = 'editprotected'; // BC
+ }
+ if ( $right == 'autoconfirmed' ) {
+ $right = 'editsemiprotected'; // BC
+ }
+ if ( $right != '' ) {
+ $namespaceRightGroups[$right] = $this->getGroupsWithPermission( $right );
+ }
+ }
+
+ // Now, go through the protection levels one by one.
+ $usableLevels = [ '' ];
+ foreach ( $this->options->get( 'RestrictionLevels' ) as $level ) {
+ $right = $level;
+ if ( $right == 'sysop' ) {
+ $right = 'editprotected'; // BC
+ }
+ if ( $right == 'autoconfirmed' ) {
+ $right = 'editsemiprotected'; // BC
+ }
+
+ if ( $right != '' &&
+ !isset( $namespaceRightGroups[$right] ) &&
+ ( !$user || $this->userHasRight( $user, $right ) )
+ ) {
+ // Do any of the namespace rights imply the restriction right? (see explanation above)
+ foreach ( $namespaceRightGroups as $groups ) {
+ if ( !array_diff( $groups, $this->getGroupsWithPermission( $right ) ) ) {
+ // Yes, this one does.
+ continue 2;
+ }
+ }
+ // No, keep the restriction level
+ $usableLevels[] = $level;
+ }
+ }
+
+ return $usableLevels;
+ }
+
/**
* Add temporary user rights, only valid for the current scope.
* This is meant for making it possible to programatically trigger certain actions that
* Loads the current state of protection into the object.
*/
function loadData() {
- $levels = MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels(
+ $levels = MediaWikiServices::getInstance()->getPermissionManager()->getNamespaceRestrictionLevels(
$this->mTitle->getNamespace(), $this->mContext->getUser()
);
$this->mCascade = $this->mTitle->areRestrictionsCascading();
*/
function execute() {
if (
- MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels(
+ MediaWikiServices::getInstance()->getPermissionManager()->getNamespaceRestrictionLevels(
$this->mTitle->getNamespace()
) === [ '' ]
) {
function buildSelector( $action, $selected ) {
// If the form is disabled, display all relevant levels. Otherwise,
// just show the ones this user can use.
- $levels = MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels(
- $this->mTitle->getNamespace(),
- $this->disabled ? null : $this->mContext->getUser()
- );
+ $levels = MediaWikiServices::getInstance()
+ ->getPermissionManager()
+ ->getNamespaceRestrictionLevels(
+ $this->mTitle->getNamespace(),
+ $this->disabled ? null : $this->mContext->getUser()
+ );
$id = 'mwProtect-level-' . $action;
use MediaWiki\Http\HttpRequestFactory;
use MediaWiki\Interwiki\ClassicInterwikiLookup;
use MediaWiki\Interwiki\InterwikiLookup;
+use MediaWiki\Languages\LanguageNameUtils;
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\Linker\LinkRendererFactory;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\Storage\NameTableStoreFactory;
use MediaWiki\Storage\SqlBlobStore;
use MediaWiki\Storage\PageEditStash;
+use Wikimedia\ObjectFactory;
return [
'ActorMigration' => function ( MediaWikiServices $services ) : ActorMigration {
);
},
+ 'LanguageNameUtils' => function ( MediaWikiServices $services ) : LanguageNameUtils {
+ return new LanguageNameUtils( new ServiceOptions(
+ LanguageNameUtils::$constructorOptions,
+ $services->getMainConfig()
+ ) );
+ },
+
'LinkCache' => function ( MediaWikiServices $services ) : LinkCache {
return new LinkCache(
$services->getTitleFormatter(),
$logger,
[ function () use ( $services ) {
$services->getResourceLoader()->getMessageBlobStore()->clear();
- } ]
+ } ],
+ $services->getLanguageNameUtils()
);
},
);
},
+ 'ObjectFactory' => function ( MediaWikiServices $services ) : ObjectFactory {
+ return new ObjectFactory( $services );
+ },
+
'OldRevisionImporter' => function ( MediaWikiServices $services ) : OldRevisionImporter {
return new ImportableOldRevisionImporter(
true,
use CLDRPluralRuleParser\Evaluator;
use CLDRPluralRuleParser\Error as CLDRPluralRuleError;
use MediaWiki\Config\ServiceOptions;
+use MediaWiki\Languages\LanguageNameUtils;
use Psr\Log\LoggerInterface;
/**
/** @var callable[] See comment for parameter in constructor */
private $clearStoreCallbacks;
+ /** @var LanguageNameUtils */
+ private $langNameUtils;
+
/**
* A 2-d associative array, code/key, where presence indicates that the item
* is loaded. Value arbitrary.
* @param callable[] $clearStoreCallbacks To be called whenever the cache is cleared. Can be
* used to clear other caches that depend on this one, such as ResourceLoader's
* MessageBlobStore.
+ * @param LanguageNameUtils $langNameUtils
* @throws MWException
*/
function __construct(
ServiceOptions $options,
LCStore $store,
LoggerInterface $logger,
- array $clearStoreCallbacks = []
+ array $clearStoreCallbacks,
+ LanguageNameUtils $langNameUtils
) {
$options->assertRequiredOptions( self::$constructorOptions );
$this->store = $store;
$this->logger = $logger;
$this->clearStoreCallbacks = $clearStoreCallbacks;
+ $this->langNameUtils = $langNameUtils;
// Keep this separate from $this->options so it can be mutable
$this->manualRecache = $options->get( 'manualRecache' );
$this->initialisedLangs[$code] = true;
# If the code is of the wrong form for a Messages*.php file, do a shallow fallback
- if ( !Language::isValidBuiltInCode( $code ) ) {
+ if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) {
$this->initShallowFallback( $code, 'en' );
return;
# Recache the data if necessary
if ( !$this->manualRecache && $this->isExpired( $code ) ) {
- if ( Language::isSupportedLanguage( $code ) ) {
+ if ( $this->langNameUtils->isSupportedLanguage( $code ) ) {
$this->recache( $code );
} elseif ( $code === 'en' ) {
throw new MWException( 'MessagesEn.php is missing.' );
global $IP;
// This reads in the PHP i18n file with non-messages l10n data
- $fileName = Language::getMessagesFileName( $code );
+ $fileName = $this->langNameUtils->getMessagesFileName( $code );
if ( !file_exists( $fileName ) ) {
$data = [];
} else {
public function getTextboxProtectionCSSClasses( Title $title ) {
$classes = []; // Textarea CSS
if ( $title->isProtected( 'edit' ) &&
- MediaWikiServices::getInstance()->getNamespaceInfo()->
- getRestrictionLevels( $title->getNamespace() ) !== [ '' ]
+ MediaWikiServices::getInstance()->getPermissionManager()
+ ->getNamespaceRestrictionLevels( $title->getNamespace() ) !== [ '' ]
) {
# Is the title semi-protected?
if ( $title->isSemiProtected() ) {
/**
* Methods for dealing with language codes.
- * @todo Move some of the code-related static methods out of Language into this class
*
* @since 1.29
* @ingroup Language
--- /dev/null
+<?php
+/**
+ * Internationalisation code.
+ * See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more information.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Language
+ */
+
+/**
+ * @defgroup Language Language
+ */
+
+namespace MediaWiki\Languages;
+
+use HashBagOStuff;
+use Hooks;
+use MediaWiki\Config\ServiceOptions;
+use MediaWikiTitleCodec;
+use MWException;
+use Wikimedia\Assert\Assert;
+
+/**
+ * @ingroup Language
+ *
+ * A service that provides utilities to do with language names and codes.
+ *
+ * @since 1.34
+ */
+class LanguageNameUtils {
+ /**
+ * Return autonyms in getLanguageName(s).
+ */
+ const AUTONYMS = null;
+
+ /**
+ * Return all known languages in getLanguageName(s).
+ */
+ const ALL = 'all';
+
+ /**
+ * Return in getLanguageName(s) only the languages that are defined by MediaWiki.
+ */
+ const DEFINED = 'mw';
+
+ /**
+ * Return in getLanguageName(s) only the languages for which we have at least some localisation.
+ */
+ const SUPPORTED = 'mwfile';
+
+ /** @var ServiceOptions */
+ private $options;
+
+ /**
+ * Cache for language names
+ * @var HashBagOStuff|null
+ */
+ private $languageNameCache;
+
+ /**
+ * Cache for validity of language codes
+ * @var array
+ */
+ private $validCodeCache = [];
+
+ public static $constructorOptions = [
+ 'ExtraLanguageNames',
+ 'UsePigLatinVariant',
+ ];
+
+ /**
+ * @param ServiceOptions $options
+ */
+ public function __construct( ServiceOptions $options ) {
+ $options->assertRequiredOptions( self::$constructorOptions );
+ $this->options = $options;
+ }
+
+ /**
+ * Checks whether any localisation is available for that language tag in MediaWiki
+ * (MessagesXx.php or xx.json exists).
+ *
+ * @param string $code Language tag (in lower case)
+ * @return bool Whether language is supported
+ */
+ public function isSupportedLanguage( $code ) {
+ if ( !$this->isValidBuiltInCode( $code ) ) {
+ return false;
+ }
+
+ if ( $code === 'qqq' ) {
+ // Special code for internal use, not supported even though there is a qqq.json
+ return false;
+ }
+
+ return is_readable( $this->getMessagesFileName( $code ) ) ||
+ is_readable( $this->getJsonMessagesFileName( $code ) );
+ }
+
+ /**
+ * Returns true if a language code string is of a valid form, whether or not it exists. This
+ * includes codes which are used solely for customisation via the MediaWiki namespace.
+ *
+ * @param string $code
+ *
+ * @return bool
+ */
+ public function isValidCode( $code ) {
+ Assert::parameterType( 'string', $code, '$code' );
+ if ( !isset( $this->validCodeCache[$code] ) ) {
+ // People think language codes are HTML-safe, so enforce it. Ideally we should only
+ // allow a-zA-Z0-9- but .+ and other chars are often used for {{int:}} hacks. See bugs
+ // T39564, T39587, T38938.
+ $this->validCodeCache[$code] =
+ // Protect against path traversal
+ strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code ) &&
+ !preg_match( MediaWikiTitleCodec::getTitleInvalidRegex(), $code );
+ }
+ return $this->validCodeCache[$code];
+ }
+
+ /**
+ * Returns true if a language code is of a valid form for the purposes of internal customisation
+ * of MediaWiki, via Messages*.php or *.json.
+ *
+ * @param string $code
+ * @return bool
+ */
+ public function isValidBuiltInCode( $code ) {
+ Assert::parameterType( 'string', $code, '$code' );
+
+ return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
+ }
+
+ /**
+ * Returns true if a language code is an IETF tag known to MediaWiki.
+ *
+ * @param string $tag
+ *
+ * @return bool
+ */
+ public function isKnownLanguageTag( $tag ) {
+ // Quick escape for invalid input to avoid exceptions down the line when code tries to
+ // process tags which are not valid at all.
+ if ( !$this->isValidBuiltInCode( $tag ) ) {
+ return false;
+ }
+
+ if ( isset( Data\Names::$names[$tag] ) || $this->getLanguageName( $tag, $tag ) !== '' ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get an array of language names, indexed by code.
+ * @param null|string $inLanguage Code of language in which to return the names
+ * Use self::AUTONYMS for autonyms (native names)
+ * @param string $include One of:
+ * self::ALL all available languages
+ * self::DEFINED only if the language is defined in MediaWiki or wgExtraLanguageNames
+ * (default)
+ * self::SUPPORTED only if the language is in self::DEFINED *and* has a message file
+ * @return array Language code => language name (sorted by key)
+ */
+ public function getLanguageNames( $inLanguage = self::AUTONYMS, $include = self::DEFINED ) {
+ $cacheKey = $inLanguage === self::AUTONYMS ? 'null' : $inLanguage;
+ $cacheKey .= ":$include";
+ if ( !$this->languageNameCache ) {
+ $this->languageNameCache = new HashBagOStuff( [ 'maxKeys' => 20 ] );
+ }
+
+ $ret = $this->languageNameCache->get( $cacheKey );
+ if ( !$ret ) {
+ $ret = $this->getLanguageNamesUncached( $inLanguage, $include );
+ $this->languageNameCache->set( $cacheKey, $ret );
+ }
+ return $ret;
+ }
+
+ /**
+ * Uncached helper for getLanguageNames
+ * @param null|string $inLanguage As getLanguageNames
+ * @param string $include As getLanguageNames
+ * @return array Language code => language name (sorted by key)
+ */
+ private function getLanguageNamesUncached( $inLanguage, $include ) {
+ // If passed an invalid language code to use, fallback to en
+ if ( $inLanguage !== self::AUTONYMS && !$this->isValidCode( $inLanguage ) ) {
+ $inLanguage = 'en';
+ }
+
+ $names = [];
+
+ if ( $inLanguage !== self::AUTONYMS ) {
+ # TODO: also include for self::AUTONYMS, when this code is more efficient
+ Hooks::run( 'LanguageGetTranslatedLanguageNames', [ &$names, $inLanguage ] );
+ }
+
+ $mwNames = $this->options->get( 'ExtraLanguageNames' ) + Data\Names::$names;
+ if ( $this->options->get( 'UsePigLatinVariant' ) ) {
+ // Pig Latin (for variant development)
+ $mwNames['en-x-piglatin'] = 'Igpay Atinlay';
+ }
+
+ foreach ( $mwNames as $mwCode => $mwName ) {
+ # - Prefer own MediaWiki native name when not using the hook
+ # - For other names just add if not added through the hook
+ if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) {
+ $names[$mwCode] = $mwName;
+ }
+ }
+
+ if ( $include === self::ALL ) {
+ ksort( $names );
+ return $names;
+ }
+
+ $returnMw = [];
+ $coreCodes = array_keys( $mwNames );
+ foreach ( $coreCodes as $coreCode ) {
+ $returnMw[$coreCode] = $names[$coreCode];
+ }
+
+ if ( $include === self::SUPPORTED ) {
+ $namesMwFile = [];
+ # We do this using a foreach over the codes instead of a directory loop so that messages
+ # files in extensions will work correctly.
+ foreach ( $returnMw as $code => $value ) {
+ if ( is_readable( $this->getMessagesFileName( $code ) ) ||
+ is_readable( $this->getJsonMessagesFileName( $code ) )
+ ) {
+ $namesMwFile[$code] = $names[$code];
+ }
+ }
+
+ ksort( $namesMwFile );
+ return $namesMwFile;
+ }
+
+ ksort( $returnMw );
+ # self::DEFINED option; default if it's not one of the other two options
+ # (self::ALL/self::SUPPORTED)
+ return $returnMw;
+ }
+
+ /**
+ * @param string $code The code of the language for which to get the name
+ * @param null|string $inLanguage Code of language in which to return the name (self::AUTONYMS
+ * for autonyms)
+ * @param string $include See getLanguageNames(), except this defaults to self::ALL instead of
+ * self::DEFINED
+ * @return string Language name or empty
+ * @since 1.20
+ */
+ public function getLanguageName( $code, $inLanguage = self::AUTONYMS, $include = self::ALL ) {
+ $code = strtolower( $code );
+ $array = $this->getLanguageNames( $inLanguage, $include );
+ return $array[$code] ?? '';
+ }
+
+ /**
+ * Get the name of a file for a certain language code
+ * @param string $prefix Prepend this to the filename
+ * @param string $code Language code
+ * @param string $suffix Append this to the filename
+ * @throws MWException
+ * @return string $prefix . $mangledCode . $suffix
+ */
+ public function getFileName( $prefix, $code, $suffix = '.php' ) {
+ if ( !$this->isValidBuiltInCode( $code ) ) {
+ throw new MWException( "Invalid language code \"$code\"" );
+ }
+
+ return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
+ }
+
+ /**
+ * @param string $code
+ * @return string
+ */
+ public function getMessagesFileName( $code ) {
+ global $IP;
+ $file = $this->getFileName( "$IP/languages/messages/Messages", $code, '.php' );
+ Hooks::run( 'Language::getMessagesFileName', [ $code, &$file ] );
+ return $file;
+ }
+
+ /**
+ * @param string $code
+ * @return string
+ * @throws MWException
+ */
+ public function getJsonMessagesFileName( $code ) {
+ global $IP;
+
+ if ( !$this->isValidBuiltInCode( $code ) ) {
+ throw new MWException( "Invalid language code \"$code\"" );
+ }
+
+ return "$IP/languages/i18n/$code.json";
+ }
+}
* @file
* @ingroup FileBackend
*/
+use Wikimedia\AtEase\AtEase;
use Wikimedia\Timestamp\ConvertibleTimestamp;
/**
unset( $params['latest'] ); // sanity
// Check that the specified temp file is valid...
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$ok = ( is_file( $tmpPath ) && filesize( $tmpPath ) == 0 );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
if ( !$ok ) { // not present or not empty
$status->fatal( 'backend-fail-opentemp', $tmpPath );
protected function doGetFileContentsMulti( array $params ) {
$contents = [];
foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$contents[$path] = $fsFile ? file_get_contents( $fsFile->getPath() ) : false;
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
}
return $contents;
*
* @file
*/
+
+use Wikimedia\AtEase\AtEase;
use Wikimedia\Timestamp\ConvertibleTimestamp;
/**
is_int( $header ) ? HttpStatus::header( $header ) : header( $header );
};
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$info = stat( $this->path );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
if ( !is_array( $info ) ) {
if ( $sendErrors ) {
* @ingroup FileBackend
*/
+use Wikimedia\AtEase\AtEase;
+
/**
* Simulation of a backend storage in memory.
*
return $status;
}
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$data = file_get_contents( $params['src'] );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
if ( $data === false ) { // source doesn't exist?
$status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
* @author Russ Nelson
*/
+use Wikimedia\AtEase\AtEase;
+
/**
* @brief Class for an OpenStack Swift (or Ceph RGW) based file backend.
*
return $status;
}
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$sha1Hash = sha1_file( $params['src'] );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
if ( $sha1Hash === false ) { // source doesn't exist?
$status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
* @param FileBackendStore $backend
* @param array $params
* @param LoggerInterface $logger PSR logger instance
- * @throws FileBackendError
+ * @throws InvalidArgumentException
*/
final public function __construct(
FileBackendStore $backend, array $params, LoggerInterface $logger
* @ingroup FileBackend
*/
+use Wikimedia\AtEase\AtEase;
+
/**
* Store a file into the backend from a file on the file system.
* Parameters for this operation are outlined in FileBackend::doOperations().
}
protected function getSourceSha1Base36() {
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$hash = sha1_file( $this->params['src'] );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
if ( $hash !== false ) {
$hash = Wikimedia\base_convert( $hash, 16, 36, 31 );
}
* @ingroup FileBackend
*/
+use Wikimedia\AtEase\AtEase;
+
/**
* Class representing a non-directory file on the file system
*
* @return string|bool TS_MW timestamp or false on failure
*/
public function getTimestamp() {
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$timestamp = filemtime( $this->path );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
if ( $timestamp !== false ) {
$timestamp = wfTimestamp( TS_MW, $timestamp );
}
return $this->sha1Base36;
}
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$this->sha1Base36 = sha1_file( $this->path );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
if ( $this->sha1Base36 !== false ) {
$this->sha1Base36 = Wikimedia\base_convert( $this->sha1Base36, 16, 36, 31 );
* @ingroup FileBackend
*/
+use Wikimedia\AtEase\AtEase;
+
/**
* This class is used to hold the location and do limited manipulation
* of files stored temporarily (this will be whatever wfTempDir() returns)
protected static $pathsCollect = null;
/**
- * Do not call directly. Use TempFSFileFactory.
+ * Do not call directly. Use TempFSFileFactory
+ *
+ * @param string $path
*/
public function __construct( $path ) {
parent::__construct( $path );
*/
public function purge() {
$this->canDelete = false; // done
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$ok = unlink( $this->path );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
unset( self::$pathsCollect[$this->path] );
*/
public static function purgeAllOnShutdown() {
foreach ( self::$pathsCollect as $path => $unused ) {
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
unlink( $path );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
}
}
+++ /dev/null
-<?php
-// phpcs:ignoreFile -- It's an external lib and it isn't. Let's not bother.
-/**
- * Memcached client for PHP.
- *
- * +---------------------------------------------------------------------------+
- * | memcached client, PHP |
- * +---------------------------------------------------------------------------+
- * | Copyright (c) 2003 Ryan T. Dean <rtdean@cytherianage.net> |
- * | All rights reserved. |
- * | |
- * | Redistribution and use in source and binary forms, with or without |
- * | modification, are permitted provided that the following conditions |
- * | are met: |
- * | |
- * | 1. Redistributions of source code must retain the above copyright |
- * | notice, this list of conditions and the following disclaimer. |
- * | 2. Redistributions in binary form must reproduce the above copyright |
- * | notice, this list of conditions and the following disclaimer in the |
- * | documentation and/or other materials provided with the distribution. |
- * | |
- * | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR |
- * | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
- * | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. |
- * | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, |
- * | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
- * | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
- * | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
- * | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
- * | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
- * | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
- * +---------------------------------------------------------------------------+
- * | Author: Ryan T. Dean <rtdean@cytherianage.net> |
- * | Heavily influenced by the Perl memcached client by Brad Fitzpatrick. |
- * | Permission granted by Brad Fitzpatrick for relicense of ported Perl |
- * | client logic under 2-clause BSD license. |
- * +---------------------------------------------------------------------------+
- *
- * @file
- * $TCAnet$
- */
-
-/**
- * This is a PHP client for memcached - a distributed memory cache daemon.
- *
- * More information is available at http://www.danga.com/memcached/
- *
- * Usage example:
- *
- * $mc = new MemcachedClient(array(
- * 'servers' => array(
- * '127.0.0.1:10000',
- * array( '192.0.0.1:10010', 2 ),
- * '127.0.0.1:10020'
- * ),
- * 'debug' => false,
- * 'compress_threshold' => 10240,
- * 'persistent' => true
- * ));
- *
- * $mc->add( 'key', array( 'some', 'array' ) );
- * $mc->replace( 'key', 'some random string' );
- * $val = $mc->get( 'key' );
- *
- * @author Ryan T. Dean <rtdean@cytherianage.net>
- * @version 0.1.2
- */
-
-use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
-
-// {{{ class MemcachedClient
-/**
- * memcached client class implemented using (p)fsockopen()
- *
- * @author Ryan T. Dean <rtdean@cytherianage.net>
- * @ingroup Cache
- */
-class MemcachedClient {
- // {{{ properties
- // {{{ public
-
- // {{{ constants
- // {{{ flags
-
- /**
- * Flag: indicates data is serialized
- */
- const SERIALIZED = 1;
-
- /**
- * Flag: indicates data is compressed
- */
- const COMPRESSED = 2;
-
- /**
- * Flag: indicates data is an integer
- */
- const INTVAL = 4;
-
- // }}}
-
- /**
- * Minimum savings to store data compressed
- */
- const COMPRESSION_SAVINGS = 0.20;
-
- // }}}
-
- /**
- * Command statistics
- *
- * @var array
- * @access public
- */
- public $stats;
-
- // }}}
- // {{{ private
-
- /**
- * Cached Sockets that are connected
- *
- * @var array
- * @access private
- */
- public $_cache_sock;
-
- /**
- * Current debug status; 0 - none to 9 - profiling
- *
- * @var bool
- * @access private
- */
- public $_debug;
-
- /**
- * Dead hosts, assoc array, 'host'=>'unixtime when ok to check again'
- *
- * @var array
- * @access private
- */
- public $_host_dead;
-
- /**
- * Is compression available?
- *
- * @var bool
- * @access private
- */
- public $_have_zlib;
-
- /**
- * Do we want to use compression?
- *
- * @var bool
- * @access private
- */
- public $_compress_enable;
-
- /**
- * At how many bytes should we compress?
- *
- * @var int
- * @access private
- */
- public $_compress_threshold;
-
- /**
- * Are we using persistent links?
- *
- * @var bool
- * @access private
- */
- public $_persistent;
-
- /**
- * If only using one server; contains ip:port to connect to
- *
- * @var string
- * @access private
- */
- public $_single_sock;
-
- /**
- * Array containing ip:port or array(ip:port, weight)
- *
- * @var array
- * @access private
- */
- public $_servers;
-
- /**
- * Our bit buckets
- *
- * @var array
- * @access private
- */
- public $_buckets;
-
- /**
- * Total # of bit buckets we have
- *
- * @var int
- * @access private
- */
- public $_bucketcount;
-
- /**
- * # of total servers we have
- *
- * @var int
- * @access private
- */
- public $_active;
-
- /**
- * Stream timeout in seconds. Applies for example to fread()
- *
- * @var int
- * @access private
- */
- public $_timeout_seconds;
-
- /**
- * Stream timeout in microseconds
- *
- * @var int
- * @access private
- */
- public $_timeout_microseconds;
-
- /**
- * Connect timeout in seconds
- */
- public $_connect_timeout;
-
- /**
- * Number of connection attempts for each server
- */
- public $_connect_attempts;
-
- /**
- * @var LoggerInterface
- */
- private $_logger;
-
- // }}}
- // }}}
- // {{{ methods
- // {{{ public functions
- // {{{ memcached()
-
- /**
- * Memcache initializer
- *
- * @param array $args Associative array of settings
- */
- public function __construct( $args ) {
- $this->set_servers( $args['servers'] ?? array() );
- $this->_debug = $args['debug'] ?? false;
- $this->stats = array();
- $this->_compress_threshold = $args['compress_threshold'] ?? 0;
- $this->_persistent = $args['persistent'] ?? false;
- $this->_compress_enable = true;
- $this->_have_zlib = function_exists( 'gzcompress' );
-
- $this->_cache_sock = array();
- $this->_host_dead = array();
-
- $this->_timeout_seconds = 0;
- $this->_timeout_microseconds = $args['timeout'] ?? 500000;
-
- $this->_connect_timeout = $args['connect_timeout'] ?? 0.1;
- $this->_connect_attempts = 2;
-
- $this->_logger = $args['logger'] ?? new NullLogger();
- }
-
- // }}}
-
- /**
- * @param mixed $value
- * @return string|integer
- */
- public function serialize( $value ) {
- return serialize( $value );
- }
-
- /**
- * @param string $value
- * @return mixed
- */
- public function unserialize( $value ) {
- return unserialize( $value );
- }
-
- // {{{ add()
-
- /**
- * Adds a key/value to the memcache server if one isn't already set with
- * that key
- *
- * @param string $key Key to set with data
- * @param mixed $val Value to store
- * @param int $exp (optional) Expiration time. This can be a number of seconds
- * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or
- * longer must be the timestamp of the time at which the mapping should expire. It
- * is safe to use timestamps in all cases, regardless of expiration
- * eg: strtotime("+3 hour")
- *
- * @return bool
- */
- public function add( $key, $val, $exp = 0 ) {
- return $this->_set( 'add', $key, $val, $exp );
- }
-
- // }}}
- // {{{ decr()
-
- /**
- * Decrease a value stored on the memcache server
- *
- * @param string $key Key to decrease
- * @param int $amt (optional) amount to decrease
- *
- * @return mixed False on failure, value on success
- */
- public function decr( $key, $amt = 1 ) {
- return $this->_incrdecr( 'decr', $key, $amt );
- }
-
- // }}}
- // {{{ delete()
-
- /**
- * Deletes a key from the server, optionally after $time
- *
- * @param string $key Key to delete
- * @param int $time (optional) how long to wait before deleting
- *
- * @return bool True on success, false on failure
- */
- public function delete( $key, $time = 0 ) {
- if ( !$this->_active ) {
- return false;
- }
-
- $sock = $this->get_sock( $key );
- if ( !is_resource( $sock ) ) {
- return false;
- }
-
- $key = is_array( $key ) ? $key[1] : $key;
-
- if ( isset( $this->stats['delete'] ) ) {
- $this->stats['delete']++;
- } else {
- $this->stats['delete'] = 1;
- }
- $cmd = "delete $key $time\r\n";
- if ( !$this->_fwrite( $sock, $cmd ) ) {
- return false;
- }
- $res = $this->_fgets( $sock );
-
- if ( $this->_debug ) {
- $this->_debugprint( sprintf( "MemCache: delete %s (%s)", $key, $res ) );
- }
-
- if ( $res == "DELETED" || $res == "NOT_FOUND" ) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Changes the TTL on a key from the server to $time
- *
- * @param string $key
- * @param int $time TTL in seconds
- *
- * @return bool True on success, false on failure
- */
- public function touch( $key, $time = 0 ) {
- if ( !$this->_active ) {
- return false;
- }
-
- $sock = $this->get_sock( $key );
- if ( !is_resource( $sock ) ) {
- return false;
- }
-
- $key = is_array( $key ) ? $key[1] : $key;
-
- if ( isset( $this->stats['touch'] ) ) {
- $this->stats['touch']++;
- } else {
- $this->stats['touch'] = 1;
- }
- $cmd = "touch $key $time\r\n";
- if ( !$this->_fwrite( $sock, $cmd ) ) {
- return false;
- }
- $res = $this->_fgets( $sock );
-
- if ( $this->_debug ) {
- $this->_debugprint( sprintf( "MemCache: touch %s (%s)", $key, $res ) );
- }
-
- if ( $res == "TOUCHED" ) {
- return true;
- }
-
- return false;
- }
-
- // }}}
- // {{{ disconnect_all()
-
- /**
- * Disconnects all connected sockets
- */
- public function disconnect_all() {
- foreach ( $this->_cache_sock as $sock ) {
- fclose( $sock );
- }
-
- $this->_cache_sock = array();
- }
-
- // }}}
- // {{{ enable_compress()
-
- /**
- * Enable / Disable compression
- *
- * @param bool $enable True to enable, false to disable
- */
- public function enable_compress( $enable ) {
- $this->_compress_enable = $enable;
- }
-
- // }}}
- // {{{ forget_dead_hosts()
-
- /**
- * Forget about all of the dead hosts
- */
- public function forget_dead_hosts() {
- $this->_host_dead = array();
- }
-
- // }}}
- // {{{ get()
-
- /**
- * Retrieves the value associated with the key from the memcache server
- *
- * @param array|string $key key to retrieve
- * @param float $casToken [optional]
- *
- * @return mixed
- */
- public function get( $key, &$casToken = null ) {
- if ( $this->_debug ) {
- $this->_debugprint( "get($key)" );
- }
-
- if ( !is_array( $key ) && strval( $key ) === '' ) {
- $this->_debugprint( "Skipping key which equals to an empty string" );
- return false;
- }
-
- if ( !$this->_active ) {
- return false;
- }
-
- $sock = $this->get_sock( $key );
-
- if ( !is_resource( $sock ) ) {
- return false;
- }
-
- $key = is_array( $key ) ? $key[1] : $key;
- if ( isset( $this->stats['get'] ) ) {
- $this->stats['get']++;
- } else {
- $this->stats['get'] = 1;
- }
-
- $cmd = "gets $key\r\n";
- if ( !$this->_fwrite( $sock, $cmd ) ) {
- return false;
- }
-
- $val = array();
- $this->_load_items( $sock, $val, $casToken );
-
- if ( $this->_debug ) {
- foreach ( $val as $k => $v ) {
- $this->_debugprint(
- sprintf( "MemCache: sock %s got %s", $this->serialize( $sock ), $k ) );
- }
- }
-
- $value = false;
- if ( isset( $val[$key] ) ) {
- $value = $val[$key];
- }
- return $value;
- }
-
- // }}}
- // {{{ get_multi()
-
- /**
- * Get multiple keys from the server(s)
- *
- * @param array $keys Keys to retrieve
- *
- * @return array
- */
- public function get_multi( $keys ) {
- if ( !$this->_active ) {
- return array();
- }
-
- if ( isset( $this->stats['get_multi'] ) ) {
- $this->stats['get_multi']++;
- } else {
- $this->stats['get_multi'] = 1;
- }
- $sock_keys = array();
- $socks = array();
- foreach ( $keys as $key ) {
- $sock = $this->get_sock( $key );
- if ( !is_resource( $sock ) ) {
- continue;
- }
- $key = is_array( $key ) ? $key[1] : $key;
- if ( !isset( $sock_keys[$sock] ) ) {
- $sock_keys[intval( $sock )] = array();
- $socks[] = $sock;
- }
- $sock_keys[intval( $sock )][] = $key;
- }
-
- $gather = array();
- // Send out the requests
- foreach ( $socks as $sock ) {
- $cmd = 'gets';
- foreach ( $sock_keys[intval( $sock )] as $key ) {
- $cmd .= ' ' . $key;
- }
- $cmd .= "\r\n";
-
- if ( $this->_fwrite( $sock, $cmd ) ) {
- $gather[] = $sock;
- }
- }
-
- // Parse responses
- $val = array();
- foreach ( $gather as $sock ) {
- $this->_load_items( $sock, $val, $casToken );
- }
-
- if ( $this->_debug ) {
- foreach ( $val as $k => $v ) {
- $this->_debugprint( sprintf( "MemCache: got %s", $k ) );
- }
- }
-
- return $val;
- }
-
- // }}}
- // {{{ incr()
-
- /**
- * Increments $key (optionally) by $amt
- *
- * @param string $key Key to increment
- * @param int $amt (optional) amount to increment
- *
- * @return int|null Null if the key does not exist yet (this does NOT
- * create new mappings if the key does not exist). If the key does
- * exist, this returns the new value for that key.
- */
- public function incr( $key, $amt = 1 ) {
- return $this->_incrdecr( 'incr', $key, $amt );
- }
-
- // }}}
- // {{{ replace()
-
- /**
- * Overwrites an existing value for key; only works if key is already set
- *
- * @param string $key Key to set value as
- * @param mixed $value Value to store
- * @param int $exp (optional) Expiration time. This can be a number of seconds
- * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or
- * longer must be the timestamp of the time at which the mapping should expire. It
- * is safe to use timestamps in all cases, regardless of exipration
- * eg: strtotime("+3 hour")
- *
- * @return bool
- */
- public function replace( $key, $value, $exp = 0 ) {
- return $this->_set( 'replace', $key, $value, $exp );
- }
-
- // }}}
- // {{{ run_command()
-
- /**
- * Passes through $cmd to the memcache server connected by $sock; returns
- * output as an array (null array if no output)
- *
- * @param Resource $sock Socket to send command on
- * @param string $cmd Command to run
- *
- * @return array Output array
- */
- public function run_command( $sock, $cmd ) {
- if ( !is_resource( $sock ) ) {
- return array();
- }
-
- if ( !$this->_fwrite( $sock, $cmd ) ) {
- return array();
- }
-
- $ret = array();
- while ( true ) {
- $res = $this->_fgets( $sock );
- $ret[] = $res;
- if ( preg_match( '/^END/', $res ) ) {
- break;
- }
- if ( strlen( $res ) == 0 ) {
- break;
- }
- }
- return $ret;
- }
-
- // }}}
- // {{{ set()
-
- /**
- * Unconditionally sets a key to a given value in the memcache. Returns true
- * if set successfully.
- *
- * @param string $key Key to set value as
- * @param mixed $value Value to set
- * @param int $exp (optional) Expiration time. This can be a number of seconds
- * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or
- * longer must be the timestamp of the time at which the mapping should expire. It
- * is safe to use timestamps in all cases, regardless of exipration
- * eg: strtotime("+3 hour")
- *
- * @return bool True on success
- */
- public function set( $key, $value, $exp = 0 ) {
- return $this->_set( 'set', $key, $value, $exp );
- }
-
- // }}}
- // {{{ cas()
-
- /**
- * Sets a key to a given value in the memcache if the current value still corresponds
- * to a known, given value. Returns true if set successfully.
- *
- * @param float $casToken Current known value
- * @param string $key Key to set value as
- * @param mixed $value Value to set
- * @param int $exp (optional) Expiration time. This can be a number of seconds
- * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or
- * longer must be the timestamp of the time at which the mapping should expire. It
- * is safe to use timestamps in all cases, regardless of exipration
- * eg: strtotime("+3 hour")
- *
- * @return bool True on success
- */
- public function cas( $casToken, $key, $value, $exp = 0 ) {
- return $this->_set( 'cas', $key, $value, $exp, $casToken );
- }
-
- // }}}
- // {{{ set_compress_threshold()
-
- /**
- * Set the compression threshold
- *
- * @param int $thresh Threshold to compress if larger than
- */
- public function set_compress_threshold( $thresh ) {
- $this->_compress_threshold = $thresh;
- }
-
- // }}}
- // {{{ set_debug()
-
- /**
- * Set the debug flag
- *
- * @see __construct()
- * @param bool $dbg True for debugging, false otherwise
- */
- public function set_debug( $dbg ) {
- $this->_debug = $dbg;
- }
-
- // }}}
- // {{{ set_servers()
-
- /**
- * Set the server list to distribute key gets and puts between
- *
- * @see __construct()
- * @param array $list Array of servers to connect to
- */
- public function set_servers( $list ) {
- $this->_servers = $list;
- $this->_active = count( $list );
- $this->_buckets = null;
- $this->_bucketcount = 0;
-
- $this->_single_sock = null;
- if ( $this->_active == 1 ) {
- $this->_single_sock = $this->_servers[0];
- }
- }
-
- /**
- * Sets the timeout for new connections
- *
- * @param int $seconds Number of seconds
- * @param int $microseconds Number of microseconds
- */
- public function set_timeout( $seconds, $microseconds ) {
- $this->_timeout_seconds = $seconds;
- $this->_timeout_microseconds = $microseconds;
- }
-
- // }}}
- // }}}
- // {{{ private methods
- // {{{ _close_sock()
-
- /**
- * Close the specified socket
- *
- * @param string $sock Socket to close
- *
- * @access private
- */
- function _close_sock( $sock ) {
- $host = array_search( $sock, $this->_cache_sock );
- fclose( $this->_cache_sock[$host] );
- unset( $this->_cache_sock[$host] );
- }
-
- // }}}
- // {{{ _connect_sock()
-
- /**
- * Connects $sock to $host, timing out after $timeout
- *
- * @param int $sock Socket to connect
- * @param string $host Host:IP to connect to
- *
- * @return bool
- * @access private
- */
- function _connect_sock( &$sock, $host ) {
- list( $ip, $port ) = preg_split( '/:(?=\d)/', $host );
- $sock = false;
- $timeout = $this->_connect_timeout;
- $errno = $errstr = null;
- for ( $i = 0; !$sock && $i < $this->_connect_attempts; $i++ ) {
- Wikimedia\suppressWarnings();
- if ( $this->_persistent == 1 ) {
- $sock = pfsockopen( $ip, $port, $errno, $errstr, $timeout );
- } else {
- $sock = fsockopen( $ip, $port, $errno, $errstr, $timeout );
- }
- Wikimedia\restoreWarnings();
- }
- if ( !$sock ) {
- $this->_error_log( "Error connecting to $host: $errstr" );
- $this->_dead_host( $host );
- return false;
- }
-
- // Initialise timeout
- stream_set_timeout( $sock, $this->_timeout_seconds, $this->_timeout_microseconds );
-
- // If the connection was persistent, flush the read buffer in case there
- // was a previous incomplete request on this connection
- if ( $this->_persistent ) {
- $this->_flush_read_buffer( $sock );
- }
- return true;
- }
-
- // }}}
- // {{{ _dead_sock()
-
- /**
- * Marks a host as dead until 30-40 seconds in the future
- *
- * @param string $sock Socket to mark as dead
- *
- * @access private
- */
- function _dead_sock( $sock ) {
- $host = array_search( $sock, $this->_cache_sock );
- $this->_dead_host( $host );
- }
-
- /**
- * @param string $host
- */
- function _dead_host( $host ) {
- $ip = explode( ':', $host )[0];
- $this->_host_dead[$ip] = time() + 30 + intval( rand( 0, 10 ) );
- $this->_host_dead[$host] = $this->_host_dead[$ip];
- unset( $this->_cache_sock[$host] );
- }
-
- // }}}
- // {{{ get_sock()
-
- /**
- * get_sock
- *
- * @param string $key Key to retrieve value for;
- *
- * @return Resource|bool Resource on success, false on failure
- * @access private
- */
- function get_sock( $key ) {
- if ( !$this->_active ) {
- return false;
- }
-
- if ( $this->_single_sock !== null ) {
- return $this->sock_to_host( $this->_single_sock );
- }
-
- $hv = is_array( $key ) ? intval( $key[0] ) : $this->_hashfunc( $key );
- if ( $this->_buckets === null ) {
- $bu = array();
- foreach ( $this->_servers as $v ) {
- if ( is_array( $v ) ) {
- for ( $i = 0; $i < $v[1]; $i++ ) {
- $bu[] = $v[0];
- }
- } else {
- $bu[] = $v;
- }
- }
- $this->_buckets = $bu;
- $this->_bucketcount = count( $bu );
- }
-
- $realkey = is_array( $key ) ? $key[1] : $key;
- for ( $tries = 0; $tries < 20; $tries++ ) {
- $host = $this->_buckets[$hv % $this->_bucketcount];
- $sock = $this->sock_to_host( $host );
- if ( is_resource( $sock ) ) {
- return $sock;
- }
- $hv = $this->_hashfunc( $hv . $realkey );
- }
-
- return false;
- }
-
- // }}}
- // {{{ _hashfunc()
-
- /**
- * Creates a hash integer based on the $key
- *
- * @param string $key Key to hash
- *
- * @return int Hash value
- * @access private
- */
- function _hashfunc( $key ) {
- # Hash function must be in [0,0x7ffffff]
- # We take the first 31 bits of the MD5 hash, which unlike the hash
- # function used in a previous version of this client, works
- return hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
- }
-
- // }}}
- // {{{ _incrdecr()
-
- /**
- * Perform increment/decriment on $key
- *
- * @param string $cmd Command to perform
- * @param string|array $key Key to perform it on
- * @param int $amt Amount to adjust
- *
- * @return int New value of $key
- * @access private
- */
- function _incrdecr( $cmd, $key, $amt = 1 ) {
- if ( !$this->_active ) {
- return null;
- }
-
- $sock = $this->get_sock( $key );
- if ( !is_resource( $sock ) ) {
- return null;
- }
-
- $key = is_array( $key ) ? $key[1] : $key;
- if ( isset( $this->stats[$cmd] ) ) {
- $this->stats[$cmd]++;
- } else {
- $this->stats[$cmd] = 1;
- }
- if ( !$this->_fwrite( $sock, "$cmd $key $amt\r\n" ) ) {
- return null;
- }
-
- $line = $this->_fgets( $sock );
- $match = array();
- if ( !preg_match( '/^(\d+)/', $line, $match ) ) {
- return null;
- }
- return $match[1];
- }
-
- // }}}
- // {{{ _load_items()
-
- /**
- * Load items into $ret from $sock
- *
- * @param Resource $sock Socket to read from
- * @param array $ret returned values
- * @param float $casToken [optional]
- * @return bool True for success, false for failure
- *
- * @access private
- */
- function _load_items( $sock, &$ret, &$casToken = null ) {
- $results = array();
-
- while ( 1 ) {
- $decl = $this->_fgets( $sock );
-
- if ( $decl === false ) {
- /*
- * If nothing can be read, something is wrong because we know exactly when
- * to stop reading (right after "END") and we return right after that.
- */
- return false;
- } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+) (\d+)$/', $decl, $match ) ) {
- /*
- * Read all data returned. This can be either one or multiple values.
- * Save all that data (in an array) to be processed later: we'll first
- * want to continue reading until "END" before doing anything else,
- * to make sure that we don't leave our client in a state where it's
- * output is not yet fully read.
- */
- $results[] = array(
- $match[1], // rkey
- $match[2], // flags
- $match[3], // len
- $match[4], // casToken
- $this->_fread( $sock, $match[3] + 2 ), // data
- );
- } elseif ( $decl == "END" ) {
- if ( count( $results ) == 0 ) {
- return false;
- }
-
- /**
- * All data has been read, time to process the data and build
- * meaningful return values.
- */
- foreach ( $results as $vars ) {
- list( $rkey, $flags, $len, $casToken, $data ) = $vars;
-
- if ( $data === false || substr( $data, -2 ) !== "\r\n" ) {
- $this->_handle_error( $sock,
- 'line ending missing from data block from $1' );
- return false;
- }
- $data = substr( $data, 0, -2 );
- $ret[$rkey] = $data;
-
- if ( $this->_have_zlib && $flags & self::COMPRESSED ) {
- $ret[$rkey] = gzuncompress( $ret[$rkey] );
- }
-
- /*
- * This unserialize is the exact reason that we only want to
- * process data after having read until "END" (instead of doing
- * this right away): "unserialize" can trigger outside code:
- * in the event that $ret[$rkey] is a serialized object,
- * unserializing it will trigger __wakeup() if present. If that
- * function attempted to read from memcached (while we did not
- * yet read "END"), these 2 calls would collide.
- */
- if ( $flags & self::SERIALIZED ) {
- $ret[$rkey] = $this->unserialize( $ret[$rkey] );
- } elseif ( $flags & self::INTVAL ) {
- $ret[$rkey] = intval( $ret[$rkey] );
- }
- }
-
- return true;
- } else {
- $this->_handle_error( $sock, 'Error parsing response from $1' );
- return false;
- }
- }
- }
-
- // }}}
- // {{{ _set()
-
- /**
- * Performs the requested storage operation to the memcache server
- *
- * @param string $cmd Command to perform
- * @param string $key Key to act on
- * @param mixed $val What we need to store
- * @param int $exp (optional) Expiration time. This can be a number of seconds
- * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or
- * longer must be the timestamp of the time at which the mapping should expire. It
- * is safe to use timestamps in all cases, regardless of exipration
- * eg: strtotime("+3 hour")
- * @param float $casToken [optional]
- *
- * @return bool
- * @access private
- */
- function _set( $cmd, $key, $val, $exp, $casToken = null ) {
- if ( !$this->_active ) {
- return false;
- }
-
- $sock = $this->get_sock( $key );
- if ( !is_resource( $sock ) ) {
- return false;
- }
-
- if ( isset( $this->stats[$cmd] ) ) {
- $this->stats[$cmd]++;
- } else {
- $this->stats[$cmd] = 1;
- }
-
- $flags = 0;
-
- if ( is_int( $val ) ) {
- $flags |= self::INTVAL;
- } elseif ( !is_scalar( $val ) ) {
- $val = $this->serialize( $val );
- $flags |= self::SERIALIZED;
- if ( $this->_debug ) {
- $this->_debugprint( sprintf( "client: serializing data as it is not scalar" ) );
- }
- }
-
- $len = strlen( $val );
-
- if ( $this->_have_zlib && $this->_compress_enable
- && $this->_compress_threshold && $len >= $this->_compress_threshold
- ) {
- $c_val = gzcompress( $val, 9 );
- $c_len = strlen( $c_val );
-
- if ( $c_len < $len * ( 1 - self::COMPRESSION_SAVINGS ) ) {
- if ( $this->_debug ) {
- $this->_debugprint( sprintf( "client: compressing data; was %d bytes is now %d bytes", $len, $c_len ) );
- }
- $val = $c_val;
- $len = $c_len;
- $flags |= self::COMPRESSED;
- }
- }
-
- $command = "$cmd $key $flags $exp $len";
- if ( $casToken ) {
- $command .= " $casToken";
- }
-
- if ( !$this->_fwrite( $sock, "$command\r\n$val\r\n" ) ) {
- return false;
- }
-
- $line = $this->_fgets( $sock );
-
- if ( $this->_debug ) {
- $this->_debugprint( sprintf( "%s %s (%s)", $cmd, $key, $line ) );
- }
- if ( $line === "STORED" ) {
- return true;
- } elseif ( $line === "NOT_STORED" && $cmd === "set" ) {
- // "Not stored" is always used as the mcrouter response with AllAsyncRoute
- return true;
- }
-
- return false;
- }
-
- // }}}
- // {{{ sock_to_host()
-
- /**
- * Returns the socket for the host
- *
- * @param string $host Host:IP to get socket for
- *
- * @return Resource|bool IO Stream or false
- * @access private
- */
- function sock_to_host( $host ) {
- if ( isset( $this->_cache_sock[$host] ) ) {
- return $this->_cache_sock[$host];
- }
-
- $sock = null;
- $now = time();
- list( $ip, /* $port */) = explode( ':', $host );
- if ( isset( $this->_host_dead[$host] ) && $this->_host_dead[$host] > $now ||
- isset( $this->_host_dead[$ip] ) && $this->_host_dead[$ip] > $now
- ) {
- return null;
- }
-
- if ( !$this->_connect_sock( $sock, $host ) ) {
- return null;
- }
-
- // Do not buffer writes
- stream_set_write_buffer( $sock, 0 );
-
- $this->_cache_sock[$host] = $sock;
-
- return $this->_cache_sock[$host];
- }
-
- /**
- * @param string $text
- */
- function _debugprint( $text ) {
- $this->_logger->debug( $text );
- }
-
- /**
- * @param string $text
- */
- function _error_log( $text ) {
- $this->_logger->error( "Memcached error: $text" );
- }
-
- /**
- * Write to a stream. If there is an error, mark the socket dead.
- *
- * @param Resource $sock The socket
- * @param string $buf The string to write
- * @return bool True on success, false on failure
- */
- function _fwrite( $sock, $buf ) {
- $bytesWritten = 0;
- $bufSize = strlen( $buf );
- while ( $bytesWritten < $bufSize ) {
- $result = fwrite( $sock, $buf );
- $data = stream_get_meta_data( $sock );
- if ( $data['timed_out'] ) {
- $this->_handle_error( $sock, 'timeout writing to $1' );
- return false;
- }
- // Contrary to the documentation, fwrite() returns zero on error in PHP 5.3.
- if ( $result === false || $result === 0 ) {
- $this->_handle_error( $sock, 'error writing to $1' );
- return false;
- }
- $bytesWritten += $result;
- }
-
- return true;
- }
-
- /**
- * Handle an I/O error. Mark the socket dead and log an error.
- *
- * @param Resource $sock
- * @param string $msg
- */
- function _handle_error( $sock, $msg ) {
- $peer = stream_socket_get_name( $sock, true /** remote **/ );
- if ( strval( $peer ) === '' ) {
- $peer = array_search( $sock, $this->_cache_sock );
- if ( $peer === false ) {
- $peer = '[unknown host]';
- }
- }
- $msg = str_replace( '$1', $peer, $msg );
- $this->_error_log( "$msg" );
- $this->_dead_sock( $sock );
- }
-
- /**
- * Read the specified number of bytes from a stream. If there is an error,
- * mark the socket dead.
- *
- * @param Resource $sock The socket
- * @param int $len The number of bytes to read
- * @return string|bool The string on success, false on failure.
- */
- function _fread( $sock, $len ) {
- $buf = '';
- while ( $len > 0 ) {
- $result = fread( $sock, $len );
- $data = stream_get_meta_data( $sock );
- if ( $data['timed_out'] ) {
- $this->_handle_error( $sock, 'timeout reading from $1' );
- return false;
- }
- if ( $result === false ) {
- $this->_handle_error( $sock, 'error reading buffer from $1' );
- return false;
- }
- if ( $result === '' ) {
- // This will happen if the remote end of the socket is shut down
- $this->_handle_error( $sock, 'unexpected end of file reading from $1' );
- return false;
- }
- $len -= strlen( $result );
- $buf .= $result;
- }
- return $buf;
- }
-
- /**
- * Read a line from a stream. If there is an error, mark the socket dead.
- * The \r\n line ending is stripped from the response.
- *
- * @param Resource $sock The socket
- * @return string|bool The string on success, false on failure
- */
- function _fgets( $sock ) {
- $result = fgets( $sock );
- // fgets() may return a partial line if there is a select timeout after
- // a successful recv(), so we have to check for a timeout even if we
- // got a string response.
- $data = stream_get_meta_data( $sock );
- if ( $data['timed_out'] ) {
- $this->_handle_error( $sock, 'timeout reading line from $1' );
- return false;
- }
- if ( $result === false ) {
- $this->_handle_error( $sock, 'error reading line from $1' );
- return false;
- }
- if ( substr( $result, -2 ) === "\r\n" ) {
- $result = substr( $result, 0, -2 );
- } elseif ( substr( $result, -1 ) === "\n" ) {
- $result = substr( $result, 0, -1 );
- } else {
- $this->_handle_error( $sock, 'line ending missing in response from $1' );
- return false;
- }
- return $result;
- }
-
- /**
- * Flush the read buffer of a stream
- * @param Resource $f
- */
- function _flush_read_buffer( $f ) {
- if ( !is_resource( $f ) ) {
- return;
- }
- $r = array( $f );
- $w = null;
- $e = null;
- $n = stream_select( $r, $w, $e, 0, 0 );
- while ( $n == 1 && !feof( $f ) ) {
- fread( $f, 1024 );
- $r = array( $f );
- $w = null;
- $e = null;
- $n = stream_select( $r, $w, $e, 0, 0 );
- }
- }
-
- // }}}
- // }}}
- // }}}
-}
-
-// }}}
--- /dev/null
+<?php
+// phpcs:ignoreFile -- It's an external lib and it isn't. Let's not bother.
+/**
+ * Memcached client for PHP.
+ *
+ * +---------------------------------------------------------------------------+
+ * | memcached client, PHP |
+ * +---------------------------------------------------------------------------+
+ * | Copyright (c) 2003 Ryan T. Dean <rtdean@cytherianage.net> |
+ * | All rights reserved. |
+ * | |
+ * | Redistribution and use in source and binary forms, with or without |
+ * | modification, are permitted provided that the following conditions |
+ * | are met: |
+ * | |
+ * | 1. Redistributions of source code must retain the above copyright |
+ * | notice, this list of conditions and the following disclaimer. |
+ * | 2. Redistributions in binary form must reproduce the above copyright |
+ * | notice, this list of conditions and the following disclaimer in the |
+ * | documentation and/or other materials provided with the distribution. |
+ * | |
+ * | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR |
+ * | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
+ * | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. |
+ * | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, |
+ * | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
+ * | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
+ * | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
+ * | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
+ * | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
+ * | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
+ * +---------------------------------------------------------------------------+
+ * | Author: Ryan T. Dean <rtdean@cytherianage.net> |
+ * | Heavily influenced by the Perl memcached client by Brad Fitzpatrick. |
+ * | Permission granted by Brad Fitzpatrick for relicense of ported Perl |
+ * | client logic under 2-clause BSD license. |
+ * +---------------------------------------------------------------------------+
+ *
+ * @file
+ * $TCAnet$
+ */
+
+/**
+ * This is a PHP client for memcached - a distributed memory cache daemon.
+ *
+ * More information is available at http://www.danga.com/memcached/
+ *
+ * Usage example:
+ *
+ * $mc = new MemcachedClient(array(
+ * 'servers' => array(
+ * '127.0.0.1:10000',
+ * array( '192.0.0.1:10010', 2 ),
+ * '127.0.0.1:10020'
+ * ),
+ * 'debug' => false,
+ * 'compress_threshold' => 10240,
+ * 'persistent' => true
+ * ));
+ *
+ * $mc->add( 'key', array( 'some', 'array' ) );
+ * $mc->replace( 'key', 'some random string' );
+ * $val = $mc->get( 'key' );
+ *
+ * @author Ryan T. Dean <rtdean@cytherianage.net>
+ * @version 0.1.2
+ */
+
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
+// {{{ class MemcachedClient
+/**
+ * memcached client class implemented using (p)fsockopen()
+ *
+ * @author Ryan T. Dean <rtdean@cytherianage.net>
+ * @ingroup Cache
+ */
+class MemcachedClient {
+ // {{{ properties
+ // {{{ public
+
+ // {{{ constants
+ // {{{ flags
+
+ /**
+ * Flag: indicates data is serialized
+ */
+ const SERIALIZED = 1;
+
+ /**
+ * Flag: indicates data is compressed
+ */
+ const COMPRESSED = 2;
+
+ /**
+ * Flag: indicates data is an integer
+ */
+ const INTVAL = 4;
+
+ // }}}
+
+ /**
+ * Minimum savings to store data compressed
+ */
+ const COMPRESSION_SAVINGS = 0.20;
+
+ // }}}
+
+ /**
+ * Command statistics
+ *
+ * @var array
+ * @access public
+ */
+ public $stats;
+
+ // }}}
+ // {{{ private
+
+ /**
+ * Cached Sockets that are connected
+ *
+ * @var array
+ * @access private
+ */
+ public $_cache_sock;
+
+ /**
+ * Current debug status; 0 - none to 9 - profiling
+ *
+ * @var bool
+ * @access private
+ */
+ public $_debug;
+
+ /**
+ * Dead hosts, assoc array, 'host'=>'unixtime when ok to check again'
+ *
+ * @var array
+ * @access private
+ */
+ public $_host_dead;
+
+ /**
+ * Is compression available?
+ *
+ * @var bool
+ * @access private
+ */
+ public $_have_zlib;
+
+ /**
+ * Do we want to use compression?
+ *
+ * @var bool
+ * @access private
+ */
+ public $_compress_enable;
+
+ /**
+ * At how many bytes should we compress?
+ *
+ * @var int
+ * @access private
+ */
+ public $_compress_threshold;
+
+ /**
+ * Are we using persistent links?
+ *
+ * @var bool
+ * @access private
+ */
+ public $_persistent;
+
+ /**
+ * If only using one server; contains ip:port to connect to
+ *
+ * @var string
+ * @access private
+ */
+ public $_single_sock;
+
+ /**
+ * Array containing ip:port or array(ip:port, weight)
+ *
+ * @var array
+ * @access private
+ */
+ public $_servers;
+
+ /**
+ * Our bit buckets
+ *
+ * @var array
+ * @access private
+ */
+ public $_buckets;
+
+ /**
+ * Total # of bit buckets we have
+ *
+ * @var int
+ * @access private
+ */
+ public $_bucketcount;
+
+ /**
+ * # of total servers we have
+ *
+ * @var int
+ * @access private
+ */
+ public $_active;
+
+ /**
+ * Stream timeout in seconds. Applies for example to fread()
+ *
+ * @var int
+ * @access private
+ */
+ public $_timeout_seconds;
+
+ /**
+ * Stream timeout in microseconds
+ *
+ * @var int
+ * @access private
+ */
+ public $_timeout_microseconds;
+
+ /**
+ * Connect timeout in seconds
+ */
+ public $_connect_timeout;
+
+ /**
+ * Number of connection attempts for each server
+ */
+ public $_connect_attempts;
+
+ /**
+ * @var LoggerInterface
+ */
+ private $_logger;
+
+ // }}}
+ // }}}
+ // {{{ methods
+ // {{{ public functions
+ // {{{ memcached()
+
+ /**
+ * Memcache initializer
+ *
+ * @param array $args Associative array of settings
+ */
+ public function __construct( $args ) {
+ $this->set_servers( $args['servers'] ?? array() );
+ $this->_debug = $args['debug'] ?? false;
+ $this->stats = array();
+ $this->_compress_threshold = $args['compress_threshold'] ?? 0;
+ $this->_persistent = $args['persistent'] ?? false;
+ $this->_compress_enable = true;
+ $this->_have_zlib = function_exists( 'gzcompress' );
+
+ $this->_cache_sock = array();
+ $this->_host_dead = array();
+
+ $this->_timeout_seconds = 0;
+ $this->_timeout_microseconds = $args['timeout'] ?? 500000;
+
+ $this->_connect_timeout = $args['connect_timeout'] ?? 0.1;
+ $this->_connect_attempts = 2;
+
+ $this->_logger = $args['logger'] ?? new NullLogger();
+ }
+
+ // }}}
+
+ /**
+ * @param mixed $value
+ * @return string|integer
+ */
+ public function serialize( $value ) {
+ return serialize( $value );
+ }
+
+ /**
+ * @param string $value
+ * @return mixed
+ */
+ public function unserialize( $value ) {
+ return unserialize( $value );
+ }
+
+ // {{{ add()
+
+ /**
+ * Adds a key/value to the memcache server if one isn't already set with
+ * that key
+ *
+ * @param string $key Key to set with data
+ * @param mixed $val Value to store
+ * @param int $exp (optional) Expiration time. This can be a number of seconds
+ * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or
+ * longer must be the timestamp of the time at which the mapping should expire. It
+ * is safe to use timestamps in all cases, regardless of expiration
+ * eg: strtotime("+3 hour")
+ *
+ * @return bool
+ */
+ public function add( $key, $val, $exp = 0 ) {
+ return $this->_set( 'add', $key, $val, $exp );
+ }
+
+ // }}}
+ // {{{ decr()
+
+ /**
+ * Decrease a value stored on the memcache server
+ *
+ * @param string $key Key to decrease
+ * @param int $amt (optional) amount to decrease
+ *
+ * @return mixed False on failure, value on success
+ */
+ public function decr( $key, $amt = 1 ) {
+ return $this->_incrdecr( 'decr', $key, $amt );
+ }
+
+ // }}}
+ // {{{ delete()
+
+ /**
+ * Deletes a key from the server, optionally after $time
+ *
+ * @param string $key Key to delete
+ * @param int $time (optional) how long to wait before deleting
+ *
+ * @return bool True on success, false on failure
+ */
+ public function delete( $key, $time = 0 ) {
+ if ( !$this->_active ) {
+ return false;
+ }
+
+ $sock = $this->get_sock( $key );
+ if ( !is_resource( $sock ) ) {
+ return false;
+ }
+
+ $key = is_array( $key ) ? $key[1] : $key;
+
+ if ( isset( $this->stats['delete'] ) ) {
+ $this->stats['delete']++;
+ } else {
+ $this->stats['delete'] = 1;
+ }
+ $cmd = "delete $key $time\r\n";
+ if ( !$this->_fwrite( $sock, $cmd ) ) {
+ return false;
+ }
+ $res = $this->_fgets( $sock );
+
+ if ( $this->_debug ) {
+ $this->_debugprint( sprintf( "MemCache: delete %s (%s)", $key, $res ) );
+ }
+
+ if ( $res == "DELETED" || $res == "NOT_FOUND" ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Changes the TTL on a key from the server to $time
+ *
+ * @param string $key
+ * @param int $time TTL in seconds
+ *
+ * @return bool True on success, false on failure
+ */
+ public function touch( $key, $time = 0 ) {
+ if ( !$this->_active ) {
+ return false;
+ }
+
+ $sock = $this->get_sock( $key );
+ if ( !is_resource( $sock ) ) {
+ return false;
+ }
+
+ $key = is_array( $key ) ? $key[1] : $key;
+
+ if ( isset( $this->stats['touch'] ) ) {
+ $this->stats['touch']++;
+ } else {
+ $this->stats['touch'] = 1;
+ }
+ $cmd = "touch $key $time\r\n";
+ if ( !$this->_fwrite( $sock, $cmd ) ) {
+ return false;
+ }
+ $res = $this->_fgets( $sock );
+
+ if ( $this->_debug ) {
+ $this->_debugprint( sprintf( "MemCache: touch %s (%s)", $key, $res ) );
+ }
+
+ if ( $res == "TOUCHED" ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ // }}}
+ // {{{ disconnect_all()
+
+ /**
+ * Disconnects all connected sockets
+ */
+ public function disconnect_all() {
+ foreach ( $this->_cache_sock as $sock ) {
+ fclose( $sock );
+ }
+
+ $this->_cache_sock = array();
+ }
+
+ // }}}
+ // {{{ enable_compress()
+
+ /**
+ * Enable / Disable compression
+ *
+ * @param bool $enable True to enable, false to disable
+ */
+ public function enable_compress( $enable ) {
+ $this->_compress_enable = $enable;
+ }
+
+ // }}}
+ // {{{ forget_dead_hosts()
+
+ /**
+ * Forget about all of the dead hosts
+ */
+ public function forget_dead_hosts() {
+ $this->_host_dead = array();
+ }
+
+ // }}}
+ // {{{ get()
+
+ /**
+ * Retrieves the value associated with the key from the memcache server
+ *
+ * @param array|string $key key to retrieve
+ * @param float $casToken [optional]
+ *
+ * @return mixed
+ */
+ public function get( $key, &$casToken = null ) {
+ if ( $this->_debug ) {
+ $this->_debugprint( "get($key)" );
+ }
+
+ if ( !is_array( $key ) && strval( $key ) === '' ) {
+ $this->_debugprint( "Skipping key which equals to an empty string" );
+ return false;
+ }
+
+ if ( !$this->_active ) {
+ return false;
+ }
+
+ $sock = $this->get_sock( $key );
+
+ if ( !is_resource( $sock ) ) {
+ return false;
+ }
+
+ $key = is_array( $key ) ? $key[1] : $key;
+ if ( isset( $this->stats['get'] ) ) {
+ $this->stats['get']++;
+ } else {
+ $this->stats['get'] = 1;
+ }
+
+ $cmd = "gets $key\r\n";
+ if ( !$this->_fwrite( $sock, $cmd ) ) {
+ return false;
+ }
+
+ $val = array();
+ $this->_load_items( $sock, $val, $casToken );
+
+ if ( $this->_debug ) {
+ foreach ( $val as $k => $v ) {
+ $this->_debugprint(
+ sprintf( "MemCache: sock %s got %s", $this->serialize( $sock ), $k ) );
+ }
+ }
+
+ $value = false;
+ if ( isset( $val[$key] ) ) {
+ $value = $val[$key];
+ }
+ return $value;
+ }
+
+ // }}}
+ // {{{ get_multi()
+
+ /**
+ * Get multiple keys from the server(s)
+ *
+ * @param array $keys Keys to retrieve
+ *
+ * @return array
+ */
+ public function get_multi( $keys ) {
+ if ( !$this->_active ) {
+ return array();
+ }
+
+ if ( isset( $this->stats['get_multi'] ) ) {
+ $this->stats['get_multi']++;
+ } else {
+ $this->stats['get_multi'] = 1;
+ }
+ $sock_keys = array();
+ $socks = array();
+ foreach ( $keys as $key ) {
+ $sock = $this->get_sock( $key );
+ if ( !is_resource( $sock ) ) {
+ continue;
+ }
+ $key = is_array( $key ) ? $key[1] : $key;
+ if ( !isset( $sock_keys[$sock] ) ) {
+ $sock_keys[intval( $sock )] = array();
+ $socks[] = $sock;
+ }
+ $sock_keys[intval( $sock )][] = $key;
+ }
+
+ $gather = array();
+ // Send out the requests
+ foreach ( $socks as $sock ) {
+ $cmd = 'gets';
+ foreach ( $sock_keys[intval( $sock )] as $key ) {
+ $cmd .= ' ' . $key;
+ }
+ $cmd .= "\r\n";
+
+ if ( $this->_fwrite( $sock, $cmd ) ) {
+ $gather[] = $sock;
+ }
+ }
+
+ // Parse responses
+ $val = array();
+ foreach ( $gather as $sock ) {
+ $this->_load_items( $sock, $val, $casToken );
+ }
+
+ if ( $this->_debug ) {
+ foreach ( $val as $k => $v ) {
+ $this->_debugprint( sprintf( "MemCache: got %s", $k ) );
+ }
+ }
+
+ return $val;
+ }
+
+ // }}}
+ // {{{ incr()
+
+ /**
+ * Increments $key (optionally) by $amt
+ *
+ * @param string $key Key to increment
+ * @param int $amt (optional) amount to increment
+ *
+ * @return int|null Null if the key does not exist yet (this does NOT
+ * create new mappings if the key does not exist). If the key does
+ * exist, this returns the new value for that key.
+ */
+ public function incr( $key, $amt = 1 ) {
+ return $this->_incrdecr( 'incr', $key, $amt );
+ }
+
+ // }}}
+ // {{{ replace()
+
+ /**
+ * Overwrites an existing value for key; only works if key is already set
+ *
+ * @param string $key Key to set value as
+ * @param mixed $value Value to store
+ * @param int $exp (optional) Expiration time. This can be a number of seconds
+ * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or
+ * longer must be the timestamp of the time at which the mapping should expire. It
+ * is safe to use timestamps in all cases, regardless of exipration
+ * eg: strtotime("+3 hour")
+ *
+ * @return bool
+ */
+ public function replace( $key, $value, $exp = 0 ) {
+ return $this->_set( 'replace', $key, $value, $exp );
+ }
+
+ // }}}
+ // {{{ run_command()
+
+ /**
+ * Passes through $cmd to the memcache server connected by $sock; returns
+ * output as an array (null array if no output)
+ *
+ * @param Resource $sock Socket to send command on
+ * @param string $cmd Command to run
+ *
+ * @return array Output array
+ */
+ public function run_command( $sock, $cmd ) {
+ if ( !is_resource( $sock ) ) {
+ return array();
+ }
+
+ if ( !$this->_fwrite( $sock, $cmd ) ) {
+ return array();
+ }
+
+ $ret = array();
+ while ( true ) {
+ $res = $this->_fgets( $sock );
+ $ret[] = $res;
+ if ( preg_match( '/^END/', $res ) ) {
+ break;
+ }
+ if ( strlen( $res ) == 0 ) {
+ break;
+ }
+ }
+ return $ret;
+ }
+
+ // }}}
+ // {{{ set()
+
+ /**
+ * Unconditionally sets a key to a given value in the memcache. Returns true
+ * if set successfully.
+ *
+ * @param string $key Key to set value as
+ * @param mixed $value Value to set
+ * @param int $exp (optional) Expiration time. This can be a number of seconds
+ * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or
+ * longer must be the timestamp of the time at which the mapping should expire. It
+ * is safe to use timestamps in all cases, regardless of exipration
+ * eg: strtotime("+3 hour")
+ *
+ * @return bool True on success
+ */
+ public function set( $key, $value, $exp = 0 ) {
+ return $this->_set( 'set', $key, $value, $exp );
+ }
+
+ // }}}
+ // {{{ cas()
+
+ /**
+ * Sets a key to a given value in the memcache if the current value still corresponds
+ * to a known, given value. Returns true if set successfully.
+ *
+ * @param float $casToken Current known value
+ * @param string $key Key to set value as
+ * @param mixed $value Value to set
+ * @param int $exp (optional) Expiration time. This can be a number of seconds
+ * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or
+ * longer must be the timestamp of the time at which the mapping should expire. It
+ * is safe to use timestamps in all cases, regardless of exipration
+ * eg: strtotime("+3 hour")
+ *
+ * @return bool True on success
+ */
+ public function cas( $casToken, $key, $value, $exp = 0 ) {
+ return $this->_set( 'cas', $key, $value, $exp, $casToken );
+ }
+
+ // }}}
+ // {{{ set_compress_threshold()
+
+ /**
+ * Set the compression threshold
+ *
+ * @param int $thresh Threshold to compress if larger than
+ */
+ public function set_compress_threshold( $thresh ) {
+ $this->_compress_threshold = $thresh;
+ }
+
+ // }}}
+ // {{{ set_debug()
+
+ /**
+ * Set the debug flag
+ *
+ * @see __construct()
+ * @param bool $dbg True for debugging, false otherwise
+ */
+ public function set_debug( $dbg ) {
+ $this->_debug = $dbg;
+ }
+
+ // }}}
+ // {{{ set_servers()
+
+ /**
+ * Set the server list to distribute key gets and puts between
+ *
+ * @see __construct()
+ * @param array $list Array of servers to connect to
+ */
+ public function set_servers( $list ) {
+ $this->_servers = $list;
+ $this->_active = count( $list );
+ $this->_buckets = null;
+ $this->_bucketcount = 0;
+
+ $this->_single_sock = null;
+ if ( $this->_active == 1 ) {
+ $this->_single_sock = $this->_servers[0];
+ }
+ }
+
+ /**
+ * Sets the timeout for new connections
+ *
+ * @param int $seconds Number of seconds
+ * @param int $microseconds Number of microseconds
+ */
+ public function set_timeout( $seconds, $microseconds ) {
+ $this->_timeout_seconds = $seconds;
+ $this->_timeout_microseconds = $microseconds;
+ }
+
+ // }}}
+ // }}}
+ // {{{ private methods
+ // {{{ _close_sock()
+
+ /**
+ * Close the specified socket
+ *
+ * @param string $sock Socket to close
+ *
+ * @access private
+ */
+ function _close_sock( $sock ) {
+ $host = array_search( $sock, $this->_cache_sock );
+ fclose( $this->_cache_sock[$host] );
+ unset( $this->_cache_sock[$host] );
+ }
+
+ // }}}
+ // {{{ _connect_sock()
+
+ /**
+ * Connects $sock to $host, timing out after $timeout
+ *
+ * @param int $sock Socket to connect
+ * @param string $host Host:IP to connect to
+ *
+ * @return bool
+ * @access private
+ */
+ function _connect_sock( &$sock, $host ) {
+ list( $ip, $port ) = preg_split( '/:(?=\d)/', $host );
+ $sock = false;
+ $timeout = $this->_connect_timeout;
+ $errno = $errstr = null;
+ for ( $i = 0; !$sock && $i < $this->_connect_attempts; $i++ ) {
+ Wikimedia\suppressWarnings();
+ if ( $this->_persistent == 1 ) {
+ $sock = pfsockopen( $ip, $port, $errno, $errstr, $timeout );
+ } else {
+ $sock = fsockopen( $ip, $port, $errno, $errstr, $timeout );
+ }
+ Wikimedia\restoreWarnings();
+ }
+ if ( !$sock ) {
+ $this->_error_log( "Error connecting to $host: $errstr" );
+ $this->_dead_host( $host );
+ return false;
+ }
+
+ // Initialise timeout
+ stream_set_timeout( $sock, $this->_timeout_seconds, $this->_timeout_microseconds );
+
+ // If the connection was persistent, flush the read buffer in case there
+ // was a previous incomplete request on this connection
+ if ( $this->_persistent ) {
+ $this->_flush_read_buffer( $sock );
+ }
+ return true;
+ }
+
+ // }}}
+ // {{{ _dead_sock()
+
+ /**
+ * Marks a host as dead until 30-40 seconds in the future
+ *
+ * @param string $sock Socket to mark as dead
+ *
+ * @access private
+ */
+ function _dead_sock( $sock ) {
+ $host = array_search( $sock, $this->_cache_sock );
+ $this->_dead_host( $host );
+ }
+
+ /**
+ * @param string $host
+ */
+ function _dead_host( $host ) {
+ $ip = explode( ':', $host )[0];
+ $this->_host_dead[$ip] = time() + 30 + intval( rand( 0, 10 ) );
+ $this->_host_dead[$host] = $this->_host_dead[$ip];
+ unset( $this->_cache_sock[$host] );
+ }
+
+ // }}}
+ // {{{ get_sock()
+
+ /**
+ * get_sock
+ *
+ * @param string $key Key to retrieve value for;
+ *
+ * @return Resource|bool Resource on success, false on failure
+ * @access private
+ */
+ function get_sock( $key ) {
+ if ( !$this->_active ) {
+ return false;
+ }
+
+ if ( $this->_single_sock !== null ) {
+ return $this->sock_to_host( $this->_single_sock );
+ }
+
+ $hv = is_array( $key ) ? intval( $key[0] ) : $this->_hashfunc( $key );
+ if ( $this->_buckets === null ) {
+ $bu = array();
+ foreach ( $this->_servers as $v ) {
+ if ( is_array( $v ) ) {
+ for ( $i = 0; $i < $v[1]; $i++ ) {
+ $bu[] = $v[0];
+ }
+ } else {
+ $bu[] = $v;
+ }
+ }
+ $this->_buckets = $bu;
+ $this->_bucketcount = count( $bu );
+ }
+
+ $realkey = is_array( $key ) ? $key[1] : $key;
+ for ( $tries = 0; $tries < 20; $tries++ ) {
+ $host = $this->_buckets[$hv % $this->_bucketcount];
+ $sock = $this->sock_to_host( $host );
+ if ( is_resource( $sock ) ) {
+ return $sock;
+ }
+ $hv = $this->_hashfunc( $hv . $realkey );
+ }
+
+ return false;
+ }
+
+ // }}}
+ // {{{ _hashfunc()
+
+ /**
+ * Creates a hash integer based on the $key
+ *
+ * @param string $key Key to hash
+ *
+ * @return int Hash value
+ * @access private
+ */
+ function _hashfunc( $key ) {
+ # Hash function must be in [0,0x7ffffff]
+ # We take the first 31 bits of the MD5 hash, which unlike the hash
+ # function used in a previous version of this client, works
+ return hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
+ }
+
+ // }}}
+ // {{{ _incrdecr()
+
+ /**
+ * Perform increment/decriment on $key
+ *
+ * @param string $cmd Command to perform
+ * @param string|array $key Key to perform it on
+ * @param int $amt Amount to adjust
+ *
+ * @return int New value of $key
+ * @access private
+ */
+ function _incrdecr( $cmd, $key, $amt = 1 ) {
+ if ( !$this->_active ) {
+ return null;
+ }
+
+ $sock = $this->get_sock( $key );
+ if ( !is_resource( $sock ) ) {
+ return null;
+ }
+
+ $key = is_array( $key ) ? $key[1] : $key;
+ if ( isset( $this->stats[$cmd] ) ) {
+ $this->stats[$cmd]++;
+ } else {
+ $this->stats[$cmd] = 1;
+ }
+ if ( !$this->_fwrite( $sock, "$cmd $key $amt\r\n" ) ) {
+ return null;
+ }
+
+ $line = $this->_fgets( $sock );
+ $match = array();
+ if ( !preg_match( '/^(\d+)/', $line, $match ) ) {
+ return null;
+ }
+ return $match[1];
+ }
+
+ // }}}
+ // {{{ _load_items()
+
+ /**
+ * Load items into $ret from $sock
+ *
+ * @param Resource $sock Socket to read from
+ * @param array $ret returned values
+ * @param float $casToken [optional]
+ * @return bool True for success, false for failure
+ *
+ * @access private
+ */
+ function _load_items( $sock, &$ret, &$casToken = null ) {
+ $results = array();
+
+ while ( 1 ) {
+ $decl = $this->_fgets( $sock );
+
+ if ( $decl === false ) {
+ /*
+ * If nothing can be read, something is wrong because we know exactly when
+ * to stop reading (right after "END") and we return right after that.
+ */
+ return false;
+ } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+) (\d+)$/', $decl, $match ) ) {
+ /*
+ * Read all data returned. This can be either one or multiple values.
+ * Save all that data (in an array) to be processed later: we'll first
+ * want to continue reading until "END" before doing anything else,
+ * to make sure that we don't leave our client in a state where it's
+ * output is not yet fully read.
+ */
+ $results[] = array(
+ $match[1], // rkey
+ $match[2], // flags
+ $match[3], // len
+ $match[4], // casToken
+ $this->_fread( $sock, $match[3] + 2 ), // data
+ );
+ } elseif ( $decl == "END" ) {
+ if ( count( $results ) == 0 ) {
+ return false;
+ }
+
+ /**
+ * All data has been read, time to process the data and build
+ * meaningful return values.
+ */
+ foreach ( $results as $vars ) {
+ list( $rkey, $flags, $len, $casToken, $data ) = $vars;
+
+ if ( $data === false || substr( $data, -2 ) !== "\r\n" ) {
+ $this->_handle_error( $sock,
+ 'line ending missing from data block from $1' );
+ return false;
+ }
+ $data = substr( $data, 0, -2 );
+ $ret[$rkey] = $data;
+
+ if ( $this->_have_zlib && $flags & self::COMPRESSED ) {
+ $ret[$rkey] = gzuncompress( $ret[$rkey] );
+ }
+
+ /*
+ * This unserialize is the exact reason that we only want to
+ * process data after having read until "END" (instead of doing
+ * this right away): "unserialize" can trigger outside code:
+ * in the event that $ret[$rkey] is a serialized object,
+ * unserializing it will trigger __wakeup() if present. If that
+ * function attempted to read from memcached (while we did not
+ * yet read "END"), these 2 calls would collide.
+ */
+ if ( $flags & self::SERIALIZED ) {
+ $ret[$rkey] = $this->unserialize( $ret[$rkey] );
+ } elseif ( $flags & self::INTVAL ) {
+ $ret[$rkey] = intval( $ret[$rkey] );
+ }
+ }
+
+ return true;
+ } else {
+ $this->_handle_error( $sock, 'Error parsing response from $1' );
+ return false;
+ }
+ }
+ }
+
+ // }}}
+ // {{{ _set()
+
+ /**
+ * Performs the requested storage operation to the memcache server
+ *
+ * @param string $cmd Command to perform
+ * @param string $key Key to act on
+ * @param mixed $val What we need to store
+ * @param int $exp (optional) Expiration time. This can be a number of seconds
+ * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or
+ * longer must be the timestamp of the time at which the mapping should expire. It
+ * is safe to use timestamps in all cases, regardless of exipration
+ * eg: strtotime("+3 hour")
+ * @param float $casToken [optional]
+ *
+ * @return bool
+ * @access private
+ */
+ function _set( $cmd, $key, $val, $exp, $casToken = null ) {
+ if ( !$this->_active ) {
+ return false;
+ }
+
+ $sock = $this->get_sock( $key );
+ if ( !is_resource( $sock ) ) {
+ return false;
+ }
+
+ if ( isset( $this->stats[$cmd] ) ) {
+ $this->stats[$cmd]++;
+ } else {
+ $this->stats[$cmd] = 1;
+ }
+
+ $flags = 0;
+
+ if ( is_int( $val ) ) {
+ $flags |= self::INTVAL;
+ } elseif ( !is_scalar( $val ) ) {
+ $val = $this->serialize( $val );
+ $flags |= self::SERIALIZED;
+ if ( $this->_debug ) {
+ $this->_debugprint( sprintf( "client: serializing data as it is not scalar" ) );
+ }
+ }
+
+ $len = strlen( $val );
+
+ if ( $this->_have_zlib && $this->_compress_enable
+ && $this->_compress_threshold && $len >= $this->_compress_threshold
+ ) {
+ $c_val = gzcompress( $val, 9 );
+ $c_len = strlen( $c_val );
+
+ if ( $c_len < $len * ( 1 - self::COMPRESSION_SAVINGS ) ) {
+ if ( $this->_debug ) {
+ $this->_debugprint( sprintf( "client: compressing data; was %d bytes is now %d bytes", $len, $c_len ) );
+ }
+ $val = $c_val;
+ $len = $c_len;
+ $flags |= self::COMPRESSED;
+ }
+ }
+
+ $command = "$cmd $key $flags $exp $len";
+ if ( $casToken ) {
+ $command .= " $casToken";
+ }
+
+ if ( !$this->_fwrite( $sock, "$command\r\n$val\r\n" ) ) {
+ return false;
+ }
+
+ $line = $this->_fgets( $sock );
+
+ if ( $this->_debug ) {
+ $this->_debugprint( sprintf( "%s %s (%s)", $cmd, $key, $line ) );
+ }
+ if ( $line === "STORED" ) {
+ return true;
+ } elseif ( $line === "NOT_STORED" && $cmd === "set" ) {
+ // "Not stored" is always used as the mcrouter response with AllAsyncRoute
+ return true;
+ }
+
+ return false;
+ }
+
+ // }}}
+ // {{{ sock_to_host()
+
+ /**
+ * Returns the socket for the host
+ *
+ * @param string $host Host:IP to get socket for
+ *
+ * @return Resource|bool IO Stream or false
+ * @access private
+ */
+ function sock_to_host( $host ) {
+ if ( isset( $this->_cache_sock[$host] ) ) {
+ return $this->_cache_sock[$host];
+ }
+
+ $sock = null;
+ $now = time();
+ list( $ip, /* $port */) = explode( ':', $host );
+ if ( isset( $this->_host_dead[$host] ) && $this->_host_dead[$host] > $now ||
+ isset( $this->_host_dead[$ip] ) && $this->_host_dead[$ip] > $now
+ ) {
+ return null;
+ }
+
+ if ( !$this->_connect_sock( $sock, $host ) ) {
+ return null;
+ }
+
+ // Do not buffer writes
+ stream_set_write_buffer( $sock, 0 );
+
+ $this->_cache_sock[$host] = $sock;
+
+ return $this->_cache_sock[$host];
+ }
+
+ /**
+ * @param string $text
+ */
+ function _debugprint( $text ) {
+ $this->_logger->debug( $text );
+ }
+
+ /**
+ * @param string $text
+ */
+ function _error_log( $text ) {
+ $this->_logger->error( "Memcached error: $text" );
+ }
+
+ /**
+ * Write to a stream. If there is an error, mark the socket dead.
+ *
+ * @param Resource $sock The socket
+ * @param string $buf The string to write
+ * @return bool True on success, false on failure
+ */
+ function _fwrite( $sock, $buf ) {
+ $bytesWritten = 0;
+ $bufSize = strlen( $buf );
+ while ( $bytesWritten < $bufSize ) {
+ $result = fwrite( $sock, $buf );
+ $data = stream_get_meta_data( $sock );
+ if ( $data['timed_out'] ) {
+ $this->_handle_error( $sock, 'timeout writing to $1' );
+ return false;
+ }
+ // Contrary to the documentation, fwrite() returns zero on error in PHP 5.3.
+ if ( $result === false || $result === 0 ) {
+ $this->_handle_error( $sock, 'error writing to $1' );
+ return false;
+ }
+ $bytesWritten += $result;
+ }
+
+ return true;
+ }
+
+ /**
+ * Handle an I/O error. Mark the socket dead and log an error.
+ *
+ * @param Resource $sock
+ * @param string $msg
+ */
+ function _handle_error( $sock, $msg ) {
+ $peer = stream_socket_get_name( $sock, true /** remote **/ );
+ if ( strval( $peer ) === '' ) {
+ $peer = array_search( $sock, $this->_cache_sock );
+ if ( $peer === false ) {
+ $peer = '[unknown host]';
+ }
+ }
+ $msg = str_replace( '$1', $peer, $msg );
+ $this->_error_log( "$msg" );
+ $this->_dead_sock( $sock );
+ }
+
+ /**
+ * Read the specified number of bytes from a stream. If there is an error,
+ * mark the socket dead.
+ *
+ * @param Resource $sock The socket
+ * @param int $len The number of bytes to read
+ * @return string|bool The string on success, false on failure.
+ */
+ function _fread( $sock, $len ) {
+ $buf = '';
+ while ( $len > 0 ) {
+ $result = fread( $sock, $len );
+ $data = stream_get_meta_data( $sock );
+ if ( $data['timed_out'] ) {
+ $this->_handle_error( $sock, 'timeout reading from $1' );
+ return false;
+ }
+ if ( $result === false ) {
+ $this->_handle_error( $sock, 'error reading buffer from $1' );
+ return false;
+ }
+ if ( $result === '' ) {
+ // This will happen if the remote end of the socket is shut down
+ $this->_handle_error( $sock, 'unexpected end of file reading from $1' );
+ return false;
+ }
+ $len -= strlen( $result );
+ $buf .= $result;
+ }
+ return $buf;
+ }
+
+ /**
+ * Read a line from a stream. If there is an error, mark the socket dead.
+ * The \r\n line ending is stripped from the response.
+ *
+ * @param Resource $sock The socket
+ * @return string|bool The string on success, false on failure
+ */
+ function _fgets( $sock ) {
+ $result = fgets( $sock );
+ // fgets() may return a partial line if there is a select timeout after
+ // a successful recv(), so we have to check for a timeout even if we
+ // got a string response.
+ $data = stream_get_meta_data( $sock );
+ if ( $data['timed_out'] ) {
+ $this->_handle_error( $sock, 'timeout reading line from $1' );
+ return false;
+ }
+ if ( $result === false ) {
+ $this->_handle_error( $sock, 'error reading line from $1' );
+ return false;
+ }
+ if ( substr( $result, -2 ) === "\r\n" ) {
+ $result = substr( $result, 0, -2 );
+ } elseif ( substr( $result, -1 ) === "\n" ) {
+ $result = substr( $result, 0, -1 );
+ } else {
+ $this->_handle_error( $sock, 'line ending missing in response from $1' );
+ return false;
+ }
+ return $result;
+ }
+
+ /**
+ * Flush the read buffer of a stream
+ * @param Resource $f
+ */
+ function _flush_read_buffer( $f ) {
+ if ( !is_resource( $f ) ) {
+ return;
+ }
+ $r = array( $f );
+ $w = null;
+ $e = null;
+ $n = stream_select( $r, $w, $e, 0, 0 );
+ while ( $n == 1 && !feof( $f ) ) {
+ fread( $f, 1024 );
+ $r = array( $f );
+ $w = null;
+ $e = null;
+ $n = stream_select( $r, $w, $e, 0, 0 );
+ }
+ }
+
+ // }}}
+ // }}}
+ // }}}
+}
+
+// }}}
implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
);
- // CP-protected writes should overwhelmingly go to the master datacenter, so use a
- // DC-local lock to merge the values. Use a DC-local get() and a synchronous all-DC
- // set(). This makes it possible for the BagOStuff class to write in parallel to all
- // DCs with one RTT. The use of WRITE_SYNC avoids needing READ_LATEST for the get().
+ // CP-protected writes should overwhelmingly go to the master datacenter, so merge the
+ // positions with a DC-local lock, a DC-local get(), and an all-DC set() with WRITE_SYNC.
+ // If set() returns success, then any get() should be able to see the new positions.
if ( $store->lock( $this->key, 3 ) ) {
if ( $workCallback ) {
// Let the store run the work before blocking on a replication sync barrier.
/** @var string[] (server index => tag/host name) */
protected $serverTags;
/** @var int */
- protected $numServers;
+ protected $numServerShards;
/** @var int UNIX timestamp */
protected $lastGarbageCollect = 0;
/** @var int */
/** @var int */
protected $purgeLimit = 100;
/** @var int */
- protected $shards = 1;
+ protected $numTableShards = 1;
/** @var string */
protected $tableName = 'objectcache';
/** @var bool */
if ( isset( $params['servers'] ) ) {
$this->serverInfos = [];
$this->serverTags = [];
- $this->numServers = count( $params['servers'] );
+ $this->numServerShards = count( $params['servers'] );
$index = 0;
foreach ( $params['servers'] as $tag => $info ) {
$this->serverInfos[$index] = $info;
}
} elseif ( isset( $params['server'] ) ) {
$this->serverInfos = [ $params['server'] ];
- $this->numServers = count( $this->serverInfos );
+ $this->numServerShards = count( $this->serverInfos );
} else {
// Default to using the main wiki's database servers
$this->serverInfos = false;
- $this->numServers = 1;
+ $this->numServerShards = 1;
$this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_BE;
}
if ( isset( $params['purgePeriod'] ) ) {
$this->tableName = $params['tableName'];
}
if ( isset( $params['shards'] ) ) {
- $this->shards = intval( $params['shards'] );
+ $this->numTableShards = intval( $params['shards'] );
}
// Backwards-compatibility for < 1.34
$this->replicaOnly = $params['replicaOnly'] ?? ( $params['slaveOnly'] ?? false );
/**
* Get a connection to the specified database
*
- * @param int $serverIndex
+ * @param int $shardIndex
* @return IMaintainableDatabase
* @throws MWException
*/
- protected function getDB( $serverIndex ) {
- if ( $serverIndex >= $this->numServers ) {
- throw new MWException( __METHOD__ . ": Invalid server index \"$serverIndex\"" );
+ private function getDB( $shardIndex ) {
+ if ( $shardIndex >= $this->numServerShards ) {
+ throw new MWException( __METHOD__ . ": Invalid server index \"$shardIndex\"" );
}
# Don't keep timing out trying to connect for each call if the DB is down
if (
- isset( $this->connFailureErrors[$serverIndex] ) &&
- ( $this->getCurrentTime() - $this->connFailureTimes[$serverIndex] ) < 60
+ isset( $this->connFailureErrors[$shardIndex] ) &&
+ ( $this->getCurrentTime() - $this->connFailureTimes[$shardIndex] ) < 60
) {
- throw $this->connFailureErrors[$serverIndex];
+ throw $this->connFailureErrors[$shardIndex];
}
if ( $this->serverInfos ) {
- if ( !isset( $this->conns[$serverIndex] ) ) {
+ if ( !isset( $this->conns[$shardIndex] ) ) {
// Use custom database defined by server connection info
- $info = $this->serverInfos[$serverIndex];
+ $info = $this->serverInfos[$shardIndex];
$type = $info['type'] ?? 'mysql';
$host = $info['host'] ?? '[unknown]';
$this->logger->debug( __CLASS__ . ": connecting to $host" );
$db = Database::factory( $type, $info );
$db->clearFlag( DBO_TRX ); // auto-commit mode
- $this->conns[$serverIndex] = $db;
+ $this->conns[$shardIndex] = $db;
}
- $db = $this->conns[$serverIndex];
+ $db = $this->conns[$shardIndex];
} else {
// Use the main LB database
$lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
* @param string $key
* @return array Server index and table name
*/
- protected function getTableByKey( $key ) {
- if ( $this->shards > 1 ) {
+ private function getTableByKey( $key ) {
+ if ( $this->numTableShards > 1 ) {
$hash = hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
- $tableIndex = $hash % $this->shards;
+ $tableIndex = $hash % $this->numTableShards;
} else {
$tableIndex = 0;
}
- if ( $this->numServers > 1 ) {
+ if ( $this->numServerShards > 1 ) {
$sortedServers = $this->serverTags;
ArrayUtils::consistentHashSort( $sortedServers, $key );
reset( $sortedServers );
- $serverIndex = key( $sortedServers );
+ $shardIndex = key( $sortedServers );
} else {
- $serverIndex = 0;
+ $shardIndex = 0;
}
- return [ $serverIndex, $this->getTableNameByShard( $tableIndex ) ];
+ return [ $shardIndex, $this->getTableNameByShard( $tableIndex ) ];
}
/**
* @param int $index
* @return string
*/
- protected function getTableNameByShard( $index ) {
- if ( $this->shards > 1 ) {
- $decimals = strlen( $this->shards - 1 );
+ private function getTableNameByShard( $index ) {
+ if ( $this->numTableShards > 1 ) {
+ $decimals = strlen( $this->numTableShards - 1 );
return $this->tableName .
sprintf( "%0{$decimals}d", $index );
} else {
return $values;
}
- protected function fetchBlobMulti( array $keys, $flags = 0 ) {
+ private function fetchBlobMulti( array $keys, $flags = 0 ) {
$values = []; // array of (key => value)
$keysByTable = [];
foreach ( $keys as $key ) {
- list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
- $keysByTable[$serverIndex][$tableName][] = $key;
+ list( $shardIndex, $tableName ) = $this->getTableByKey( $key );
+ $keysByTable[$shardIndex][$tableName][] = $key;
}
$dataRows = [];
- foreach ( $keysByTable as $serverIndex => $serverKeys ) {
+ foreach ( $keysByTable as $shardIndex => $serverKeys ) {
try {
- $db = $this->getDB( $serverIndex );
+ $db = $this->getDB( $shardIndex );
foreach ( $serverKeys as $tableName => $tableKeys ) {
$res = $db->select( $tableName,
[ 'keyname', 'value', 'exptime' ],
continue;
}
foreach ( $res as $row ) {
- $row->serverIndex = $serverIndex;
+ $row->shardIndex = $shardIndex;
$row->tableName = $tableName;
$dataRows[$row->keyname] = $row;
}
}
} catch ( DBError $e ) {
- $this->handleReadError( $e, $serverIndex );
+ $this->handleReadError( $e, $shardIndex );
}
}
$this->debug( "get: retrieved data; expiry time is " . $row->exptime );
$db = null; // in case of connection failure
try {
- $db = $this->getDB( $row->serverIndex );
+ $db = $this->getDB( $row->shardIndex );
if ( $this->isExpired( $db, $row->exptime ) ) { // MISS
$this->debug( "get: key has expired" );
} else { // HIT
$values[$key] = $db->decodeBlob( $row->value );
}
} catch ( DBQueryError $e ) {
- $this->handleWriteError( $e, $db, $row->serverIndex );
+ $this->handleWriteError( $e, $db, $row->shardIndex );
}
} else { // MISS
$this->debug( 'get: no matching rows' );
private function modifyMulti( array $data, $exptime, $flags, $op ) {
$keysByTable = [];
foreach ( $data as $key => $value ) {
- list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
- $keysByTable[$serverIndex][$tableName][] = $key;
+ list( $shardIndex, $tableName ) = $this->getTableByKey( $key );
+ $keysByTable[$shardIndex][$tableName][] = $key;
}
$exptime = $this->getExpirationAsTimestamp( $exptime );
$result = true;
/** @noinspection PhpUnusedLocalVariableInspection */
$silenceScope = $this->silenceTransactionProfiler();
- foreach ( $keysByTable as $serverIndex => $serverKeys ) {
+ foreach ( $keysByTable as $shardIndex => $serverKeys ) {
$db = null; // in case of connection failure
try {
- $db = $this->getDB( $serverIndex );
+ $db = $this->getDB( $shardIndex );
$this->occasionallyGarbageCollect( $db ); // expire old entries if any
$dbExpiry = $exptime ? $db->timestamp( $exptime ) : $this->getMaxDateTime( $db );
} catch ( DBError $e ) {
- $this->handleWriteError( $e, $db, $serverIndex );
+ $this->handleWriteError( $e, $db, $shardIndex );
$result = false;
continue;
}
$dbExpiry
) && $result;
} catch ( DBError $e ) {
- $this->handleWriteError( $e, $db, $serverIndex );
+ $this->handleWriteError( $e, $db, $shardIndex );
$result = false;
}
}
protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
- list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+ list( $shardIndex, $tableName ) = $this->getTableByKey( $key );
$exptime = $this->getExpirationAsTimestamp( $exptime );
/** @noinspection PhpUnusedLocalVariableInspection */
$silenceScope = $this->silenceTransactionProfiler();
$db = null; // in case of connection failure
try {
- $db = $this->getDB( $serverIndex );
+ $db = $this->getDB( $shardIndex );
// (T26425) use a replace if the db supports it instead of
// delete/insert to avoid clashes with conflicting keynames
$db->update(
__METHOD__
);
} catch ( DBQueryError $e ) {
- $this->handleWriteError( $e, $db, $serverIndex );
+ $this->handleWriteError( $e, $db, $shardIndex );
return false;
}
}
public function incr( $key, $step = 1 ) {
- list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+ list( $shardIndex, $tableName ) = $this->getTableByKey( $key );
$newCount = false;
/** @noinspection PhpUnusedLocalVariableInspection */
$silenceScope = $this->silenceTransactionProfiler();
$db = null; // in case of connection failure
try {
- $db = $this->getDB( $serverIndex );
+ $db = $this->getDB( $shardIndex );
$encTimestamp = $db->addQuotes( $db->timestamp() );
$db->update(
$tableName,
}
}
} catch ( DBError $e ) {
- $this->handleWriteError( $e, $db, $serverIndex );
+ $this->handleWriteError( $e, $db, $shardIndex );
}
return $newCount;
* @param string $exptime
* @return bool
*/
- protected function isExpired( $db, $exptime ) {
+ private function isExpired( $db, $exptime ) {
return (
$exptime != $this->getMaxDateTime( $db ) &&
wfTimestamp( TS_UNIX, $exptime ) < $this->getCurrentTime()
* @param IDatabase $db
* @return string
*/
- protected function getMaxDateTime( $db ) {
+ private function getMaxDateTime( $db ) {
if ( (int)$this->getCurrentTime() > 0x7fffffff ) {
return $db->timestamp( 1 << 62 );
} else {
* @param IDatabase $db
* @throws DBError
*/
- protected function occasionallyGarbageCollect( IDatabase $db ) {
+ private function occasionallyGarbageCollect( IDatabase $db ) {
if (
// Random purging is enabled
$this->purgePeriod &&
/** @noinspection PhpUnusedLocalVariableInspection */
$silenceScope = $this->silenceTransactionProfiler();
- $serverIndexes = range( 0, $this->numServers - 1 );
- shuffle( $serverIndexes );
+ $shardIndexes = range( 0, $this->numServerShards - 1 );
+ shuffle( $shardIndexes );
$ok = true;
$keysDeletedCount = 0;
- foreach ( $serverIndexes as $numServersDone => $serverIndex ) {
+ foreach ( $shardIndexes as $numServersDone => $shardIndex ) {
$db = null; // in case of connection failure
try {
- $db = $this->getDB( $serverIndex );
+ $db = $this->getDB( $shardIndex );
$this->deleteServerObjectsExpiringBefore(
$db,
$timestamp,
$keysDeletedCount
);
} catch ( DBError $e ) {
- $this->handleWriteError( $e, $db, $serverIndex );
+ $this->handleWriteError( $e, $db, $shardIndex );
$ok = false;
}
}
&$keysDeletedCount = 0
) {
$cutoffUnix = wfTimestamp( TS_UNIX, $timestamp );
- $shardIndexes = range( 0, $this->shards - 1 );
+ $shardIndexes = range( 0, $this->numTableShards - 1 );
shuffle( $shardIndexes );
foreach ( $shardIndexes as $numShardsDone => $shardIndex ) {
if ( $lag ) {
$remainingLag = $cutoffUnix - wfTimestamp( TS_UNIX, $continue );
$processedLag = max( $lag - $remainingLag, 0 );
- $doneRatio = ( $numShardsDone + $processedLag / $lag ) / $this->shards;
+ $doneRatio = ( $numShardsDone + $processedLag / $lag ) / $this->numTableShards;
} else {
$doneRatio = 1;
}
- $overallRatio = ( $doneRatio / $this->numServers )
- + ( $serversDoneCount / $this->numServers );
+ $overallRatio = ( $doneRatio / $this->numServerShards )
+ + ( $serversDoneCount / $this->numServerShards );
call_user_func( $progressCallback, $overallRatio * 100 );
}
} while ( $res->numRows() && $keysDeletedCount < $limit );
public function deleteAll() {
/** @noinspection PhpUnusedLocalVariableInspection */
$silenceScope = $this->silenceTransactionProfiler();
- for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
+ for ( $shardIndex = 0; $shardIndex < $this->numServerShards; $shardIndex++ ) {
$db = null; // in case of connection failure
try {
- $db = $this->getDB( $serverIndex );
- for ( $i = 0; $i < $this->shards; $i++ ) {
+ $db = $this->getDB( $shardIndex );
+ for ( $i = 0; $i < $this->numTableShards; $i++ ) {
$db->delete( $this->getTableNameByShard( $i ), '*', __METHOD__ );
}
} catch ( DBError $e ) {
- $this->handleWriteError( $e, $db, $serverIndex );
+ $this->handleWriteError( $e, $db, $shardIndex );
return false;
}
}
}
}
- list( $serverIndex ) = $this->getTableByKey( $key );
+ list( $shardIndex ) = $this->getTableByKey( $key );
$db = null; // in case of connection failure
try {
- $db = $this->getDB( $serverIndex );
+ $db = $this->getDB( $shardIndex );
$ok = $db->lock( $key, __METHOD__, $timeout );
if ( $ok ) {
$this->locks[$key] = [ 'class' => $rclass, 'depth' => 1 ];
return $ok;
} catch ( DBError $e ) {
- $this->handleWriteError( $e, $db, $serverIndex );
+ $this->handleWriteError( $e, $db, $shardIndex );
$ok = false;
}
if ( --$this->locks[$key]['depth'] <= 0 ) {
unset( $this->locks[$key] );
- list( $serverIndex ) = $this->getTableByKey( $key );
+ list( $shardIndex ) = $this->getTableByKey( $key );
$db = null; // in case of connection failure
try {
- $db = $this->getDB( $serverIndex );
+ $db = $this->getDB( $shardIndex );
$ok = $db->unlock( $key, __METHOD__ );
if ( !$ok ) {
$this->logger->warning(
);
}
} catch ( DBError $e ) {
- $this->handleWriteError( $e, $db, $serverIndex );
+ $this->handleWriteError( $e, $db, $shardIndex );
$ok = false;
}
* Handle a DBError which occurred during a read operation.
*
* @param DBError $exception
- * @param int $serverIndex
+ * @param int $shardIndex
*/
- protected function handleReadError( DBError $exception, $serverIndex ) {
+ private function handleReadError( DBError $exception, $shardIndex ) {
if ( $exception instanceof DBConnectionError ) {
- $this->markServerDown( $exception, $serverIndex );
+ $this->markServerDown( $exception, $shardIndex );
}
$this->setAndLogDBError( $exception );
*
* @param DBError $exception
* @param IDatabase|null $db DB handle or null if connection failed
- * @param int $serverIndex
+ * @param int $shardIndex
* @throws Exception
*/
- protected function handleWriteError( DBError $exception, $db, $serverIndex ) {
+ private function handleWriteError( DBError $exception, $db, $shardIndex ) {
if ( !( $db instanceof IDatabase ) ) {
- $this->markServerDown( $exception, $serverIndex );
+ $this->markServerDown( $exception, $shardIndex );
}
$this->setAndLogDBError( $exception );
* Mark a server down due to a DBConnectionError exception
*
* @param DBError $exception
- * @param int $serverIndex
+ * @param int $shardIndex
*/
- protected function markServerDown( DBError $exception, $serverIndex ) {
- unset( $this->conns[$serverIndex] ); // bug T103435
+ private function markServerDown( DBError $exception, $shardIndex ) {
+ unset( $this->conns[$shardIndex] ); // bug T103435
$now = $this->getCurrentTime();
- if ( isset( $this->connFailureTimes[$serverIndex] ) ) {
- if ( $now - $this->connFailureTimes[$serverIndex] >= 60 ) {
- unset( $this->connFailureTimes[$serverIndex] );
- unset( $this->connFailureErrors[$serverIndex] );
+ if ( isset( $this->connFailureTimes[$shardIndex] ) ) {
+ if ( $now - $this->connFailureTimes[$shardIndex] >= 60 ) {
+ unset( $this->connFailureTimes[$shardIndex] );
+ unset( $this->connFailureErrors[$shardIndex] );
} else {
- $this->logger->debug( __METHOD__ . ": Server #$serverIndex already down" );
+ $this->logger->debug( __METHOD__ . ": Server #$shardIndex already down" );
return;
}
}
- $this->logger->info( __METHOD__ . ": Server #$serverIndex down until " . ( $now + 60 ) );
- $this->connFailureTimes[$serverIndex] = $now;
- $this->connFailureErrors[$serverIndex] = $exception;
+ $this->logger->info( __METHOD__ . ": Server #$shardIndex down until " . ( $now + 60 ) );
+ $this->connFailureTimes[$shardIndex] = $now;
+ $this->connFailureErrors[$shardIndex] = $exception;
}
/**
* Create shard tables. For use from eval.php.
*/
public function createTables() {
- for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
- $db = $this->getDB( $serverIndex );
+ for ( $shardIndex = 0; $shardIndex < $this->numServerShards; $shardIndex++ ) {
+ $db = $this->getDB( $shardIndex );
if ( $db->getType() !== 'mysql' ) {
throw new MWException( __METHOD__ . ' is not supported on this DB server' );
}
- for ( $i = 0; $i < $this->shards; $i++ ) {
+ for ( $i = 0; $i < $this->numTableShards; $i++ ) {
$db->query(
'CREATE TABLE ' . $db->tableName( $this->getTableNameByShard( $i ) ) .
' LIKE ' . $db->tableName( 'objectcache' ),
/**
* @return bool Whether the main DB is used, e.g. wfGetDB( DB_MASTER )
*/
- protected function usesMainDB() {
+ private function usesMainDB() {
return !$this->serverInfos;
}
- protected function waitForReplication() {
+ private function waitForReplication() {
if ( !$this->usesMainDB() ) {
// Custom DB server list; probably doesn't use replication
return true;
}
/**
- * Returns a ScopedCallback which resets the silence flag in the transaction profiler when it is
- * destroyed on the end of a scope, for example on return or throw
- * @return ScopedCallback
- * @since 1.32
+ * Silence the transaction profiler until the return value falls out of scope
+ *
+ * @return ScopedCallback|null
*/
- protected function silenceTransactionProfiler() {
+ private function silenceTransactionProfiler() {
+ if ( !$this->usesMainDB() ) {
+ // Custom DB is configured which either has no TransactionProfiler injected,
+ // or has one specific for cache use, which we shouldn't silence
+ return null;
+ }
+
$trxProfiler = Profiler::instance()->getTransactionProfiler();
$oldSilenced = $trxProfiler->setSilenced( true );
return new ScopedCallback( function () use ( $trxProfiler, $oldSilenced ) {
/**
* Get the current revision ID
*
+ * @deprecated since 1.34, use OutputPage::getRevisionId instead
* @return int
*/
public function getRevisionId() {
/**
* Whether the revision displayed is the latest revision of the page
*
+ * @deprecated since 1.34, use OutputPage::isRevisionCurrent instead
* @return bool
*/
public function isRevisionCurrent() {
- $revID = $this->getRevisionId();
- return $revID == 0 || $revID == $this->getTitle()->getLatestRevID();
+ return $this->getOutput()->isRevisionCurrent();
}
/**
* @return string HTML text with an URL
*/
function printSource() {
- $oldid = $this->getRevisionId();
+ $oldid = $this->getOutput()->getRevisionId();
if ( $oldid ) {
$canonicalUrl = $this->getTitle()->getCanonicalURL( 'oldid=' . $oldid );
$url = htmlspecialchars( wfExpandIRI( $canonicalUrl ) );
function getCopyright( $type = 'detect' ) {
$linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
if ( $type == 'detect' ) {
- if ( !$this->isRevisionCurrent()
+ if ( !$this->getOutput()->isRevisionCurrent()
&& !$this->msg( 'history_copyright' )->inContentLanguage()->isDisabled()
) {
$type = 'history';
# No cached timestamp, load it from the database
if ( $timestamp === null ) {
- $timestamp = Revision::getTimestampFromId( $this->getTitle(), $this->getRevisionId() );
+ $timestamp = Revision::getTimestampFromId( $this->getTitle(),
+ $this->getOutput()->getRevisionId() );
}
if ( $timestamp ) {
function editUrlOptions() {
$options = [ 'action' => 'edit' ];
- if ( !$this->isRevisionCurrent() ) {
- $options['oldid'] = intval( $this->getRevisionId() );
+ if ( !$this->getOutput()->isRevisionCurrent() ) {
+ $options['oldid'] = intval( $this->getOutput()->getRevisionId() );
}
return $options;
$tpl->set( 'credits', false );
$tpl->set( 'numberofwatchingusers', false );
if ( $title->exists() ) {
- if ( $out->isArticle() && $this->isRevisionCurrent() ) {
+ if ( $out->isArticle() && $out->isRevisionCurrent() ) {
if ( $wgMaxCredits != 0 ) {
/** @var CreditsAction $action */
$action = Action::factory(
// Whether to show the "Add a new section" tab
// Checks if this is a current rev of talk page and is not forced to be hidden
$showNewSection = !$out->forceHideNewSectionLink()
- && ( ( $isTalk && $this->isRevisionCurrent() ) || $out->showNewSectionLink() );
+ && ( ( $isTalk && $out->isRevisionCurrent() ) || $out->showNewSectionLink() );
$section = $request->getVal( 'section' );
if ( $title->exists()
}
if ( $title->quickUserCan( 'protect', $user ) && $title->getRestrictionTypes() &&
- MediaWikiServices::getInstance()->getNamespaceInfo()->
- getRestrictionLevels( $title->getNamespace(), $user ) !== [ '' ]
+ MediaWikiServices::getInstance()->getPermissionManager()
+ ->getNamespaceRestrictionLevels( $title->getNamespace(), $user ) !== [ '' ]
) {
$mode = $title->isProtected() ? 'unprotect' : 'protect';
$content_navigation['actions'][$mode] = [
if ( $out->isArticle() ) {
// Also add a "permalink" while we're at it
- $revid = $this->getRevisionId();
+ $revid = $this->getOutput()->getRevisionId();
if ( $revid ) {
$nav_urls['permalink'] = [
'text' => $this->msg( 'permalink' )->text(),
// HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
// (e.g. emojis) count for two each. This limit is overridden in JS to instead count
// Unicode codepoints.
- // "- 155" is to leave room for the auto-generated part of the log entry.
- 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT - 155,
+ 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
] ) .
'</td>' .
"</tr><tr>\n" .
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Linker\LinkTarget;
+use MediaWiki\MediaWikiServices;
/**
* This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of
'ExtraNamespaces',
'ExtraSignatureNamespaces',
'NamespaceContentModels',
- 'NamespaceProtection',
'NamespacesWithSubpages',
'NonincludableNamespaces',
- 'RestrictionLevels',
];
/**
* Determine which restriction levels it makes sense to use in a namespace,
* optionally filtered by a user's rights.
*
- * @todo Move this to PermissionManager and remove the dependency here on permissions-related
- * config settings.
- *
+ * @deprecated since 1.34 User PermissionManager::getNamespaceRestrictionLevels instead.
* @param int $index Index to check
* @param User|null $user User to check
* @return array
*/
public function getRestrictionLevels( $index, User $user = null ) {
- if ( !isset( $this->options->get( 'NamespaceProtection' )[$index] ) ) {
- // All levels are valid if there's no namespace restriction.
- // But still filter by user, if necessary
- $levels = $this->options->get( 'RestrictionLevels' );
- if ( $user ) {
- $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) {
- $right = $level;
- if ( $right == 'sysop' ) {
- $right = 'editprotected'; // BC
- }
- if ( $right == 'autoconfirmed' ) {
- $right = 'editsemiprotected'; // BC
- }
- return ( $right == '' || $user->isAllowed( $right ) );
- } ) );
- }
- return $levels;
- }
-
- // $wgNamespaceProtection can require one or more rights to edit the namespace, which
- // may be satisfied by membership in multiple groups each giving a subset of those rights.
- // A restriction level is redundant if, for any one of the namespace rights, all groups
- // giving that right also give the restriction level's right. Or, conversely, a
- // restriction level is not redundant if, for every namespace right, there's at least one
- // group giving that right without the restriction level's right.
- //
- // First, for each right, get a list of groups with that right.
- $namespaceRightGroups = [];
- foreach ( (array)$this->options->get( 'NamespaceProtection' )[$index] as $right ) {
- if ( $right == 'sysop' ) {
- $right = 'editprotected'; // BC
- }
- if ( $right == 'autoconfirmed' ) {
- $right = 'editsemiprotected'; // BC
- }
- if ( $right != '' ) {
- $namespaceRightGroups[$right] = User::getGroupsWithPermission( $right );
- }
- }
-
- // Now, go through the protection levels one by one.
- $usableLevels = [ '' ];
- foreach ( $this->options->get( 'RestrictionLevels' ) as $level ) {
- $right = $level;
- if ( $right == 'sysop' ) {
- $right = 'editprotected'; // BC
- }
- if ( $right == 'autoconfirmed' ) {
- $right = 'editsemiprotected'; // BC
- }
-
- if ( $right != '' &&
- !isset( $namespaceRightGroups[$right] ) &&
- ( !$user || $user->isAllowed( $right ) )
- ) {
- // Do any of the namespace rights imply the restriction right? (see explanation above)
- foreach ( $namespaceRightGroups as $groups ) {
- if ( !array_diff( $groups, User::getGroupsWithPermission( $right ) ) ) {
- // Yes, this one does.
- continue 2;
- }
- }
- // No, keep the restriction level
- $usableLevels[] = $level;
- }
- }
-
- return $usableLevels;
+ // PermissionManager is not injected because adding an explicit dependency
+ // breaks MW installer by adding a dependency chain on the database before
+ // it was set up. Also, the method is deprecated and will be soon removed.
+ return MediaWikiServices::getInstance()
+ ->getPermissionManager()
+ ->getNamespaceRestrictionLevels( $index, $user );
}
/**
*/
use CLDRPluralRuleParser\Evaluator;
+use MediaWiki\Languages\LanguageNameUtils;
use MediaWiki\MediaWikiServices;
-use Wikimedia\Assert\Assert;
/**
* Internationalisation code
/**
* Return autonyms in fetchLanguageName(s).
* @since 1.32
+ * @deprecated since 1.34, LanguageNameUtils::AUTONYMS
*/
- const AS_AUTONYMS = null;
+ const AS_AUTONYMS = LanguageNameUtils::AUTONYMS;
/**
* Return all known languages in fetchLanguageName(s).
* @since 1.32
+ * @deprecated since 1.34, use LanguageNameUtils::ALL
*/
- const ALL = 'all';
+ const ALL = LanguageNameUtils::ALL;
/**
* Return in fetchLanguageName(s) only the languages for which we have at
* least some localisation.
* @since 1.32
+ * @deprecated since 1.34, use LanguageNameUtils::SUPPORTED
*/
- const SUPPORTED = 'mwfile';
+ const SUPPORTED = LanguageNameUtils::SUPPORTED;
/**
* @var LanguageConverter
/** @var LocalisationCache */
private $localisationCache;
+ /** @var LanguageNameUtils */
+ private $langNameUtils;
+
public static $mLangObjCache = [];
/**
*/
const STRICT_FALLBACKS = 1;
+ // TODO Make these const once we drop HHVM support (T192166)
public static $mWeekdayMsgs = [
'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
'friday', 'saturday'
*/
private static $grammarTransformations;
- /**
- * Cache for language names
- * @var HashBagOStuff|null
- */
- private static $languageNameCache;
-
/**
* Unicode directional formatting characters, for embedBidi()
*/
* @return Language
*/
protected static function newFromCode( $code, $fallback = false ) {
- if ( !self::isValidCode( $code ) ) {
+ $langNameUtils = MediaWikiServices::getInstance()->getLanguageNameUtils();
+ if ( !$langNameUtils->isValidCode( $code ) ) {
throw new MWException( "Invalid language code \"$code\"" );
}
- if ( !self::isValidBuiltInCode( $code ) ) {
+ if ( !$langNameUtils->isValidBuiltInCode( $code ) ) {
// It's not possible to customise this code with class files, so
// just return a Language object. This is to support uselang= hacks.
$lang = new Language;
// Keep trying the fallback list until we find an existing class
$fallbacks = self::getFallbacksFor( $code );
foreach ( $fallbacks as $fallbackCode ) {
- if ( !self::isValidBuiltInCode( $fallbackCode ) ) {
+ if ( !$langNameUtils->isValidBuiltInCode( $fallbackCode ) ) {
throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
}
}
if ( !defined( 'MEDIAWIKI_INSTALL' ) ) {
MediaWikiServices::getInstance()->resetServiceForTesting( 'LocalisationCache' );
+ MediaWikiServices::getInstance()->resetServiceForTesting( 'LanguageNameUtils' );
}
self::$mLangObjCache = [];
self::$fallbackLanguageCache = [];
self::$grammarTransformations = null;
- self::$languageNameCache = null;
}
/**
* Checks whether any localisation is available for that language tag
* in MediaWiki (MessagesXx.php exists).
*
+ * @deprecated since 1.34, use LanguageNameUtils
* @param string $code Language tag (in lower case)
* @return bool Whether language is supported
* @since 1.21
*/
public static function isSupportedLanguage( $code ) {
- if ( !self::isValidBuiltInCode( $code ) ) {
- return false;
- }
-
- if ( $code === 'qqq' ) {
- return false;
- }
-
- return is_readable( self::getMessagesFileName( $code ) ) ||
- is_readable( self::getJsonMessagesFileName( $code ) );
+ return MediaWikiServices::getInstance()->getLanguageNameUtils()
+ ->isSupportedLanguage( $code );
}
/**
* not it exists. This includes codes which are used solely for
* customisation via the MediaWiki namespace.
*
+ * @deprecated since 1.34, use LanguageNameUtils
+ *
* @param string $code
*
* @return bool
*/
public static function isValidCode( $code ) {
- static $cache = [];
- Assert::parameterType( 'string', $code, '$code' );
- if ( !isset( $cache[$code] ) ) {
- // People think language codes are html safe, so enforce it.
- // Ideally we should only allow a-zA-Z0-9-
- // but, .+ and other chars are often used for {{int:}} hacks
- // see bugs T39564, T39587, T38938
- $cache[$code] =
- // Protect against path traversal
- strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
- && !preg_match( MediaWikiTitleCodec::getTitleInvalidRegex(), $code );
- }
- return $cache[$code];
+ return MediaWikiServices::getInstance()->getLanguageNameUtils()->isValidCode( $code );
}
/**
* Returns true if a language code is of a valid form for the purposes of
* internal customisation of MediaWiki, via Messages*.php or *.json.
*
+ * @deprecated since 1.34, use LanguageNameUtils
+ *
* @param string $code
*
* @since 1.18
* @return bool
*/
public static function isValidBuiltInCode( $code ) {
- Assert::parameterType( 'string', $code, '$code' );
-
- return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
+ return MediaWikiServices::getInstance()->getLanguageNameUtils()
+ ->isValidBuiltInCode( $code );
}
/**
* Returns true if a language code is an IETF tag known to MediaWiki.
*
+ * @deprecated since 1.34, use LanguageNameUtils
+ *
* @param string $tag
*
* @since 1.21
* @return bool
*/
public static function isKnownLanguageTag( $tag ) {
- // Quick escape for invalid input to avoid exceptions down the line
- // when code tries to process tags which are not valid at all.
- if ( !self::isValidBuiltInCode( $tag ) ) {
- return false;
- }
-
- if ( isset( MediaWiki\Languages\Data\Names::$names[$tag] )
- || self::fetchLanguageName( $tag, $tag ) !== ''
- ) {
- return true;
- }
-
- return false;
+ return MediaWikiServices::getInstance()->getLanguageNameUtils()
+ ->isKnownLanguageTag( $tag );
}
/**
} else {
$this->mCode = str_replace( '_', '-', strtolower( substr( static::class, 8 ) ) );
}
- $this->localisationCache = MediaWikiServices::getInstance()->getLocalisationCache();
+ $services = MediaWikiServices::getInstance();
+ $this->localisationCache = $services->getLocalisationCache();
+ $this->langNameUtils = $services->getLanguageNameUtils();
}
/**
if ( $usemsg && wfMessage( $msg )->exists() ) {
return $this->getMessageFromDB( $msg );
}
- $name = self::fetchLanguageName( $code );
+ $name = $this->langNameUtils->getLanguageName( $code );
if ( $name ) {
return $name; # if it's defined as a language name, show that
} else {
/**
* Get an array of language names, indexed by code.
+ *
+ * @deprecated since 1.34, use LanguageNameUtils::getLanguageNames
* @param null|string $inLanguage Code of language in which to return the names
* Use self::AS_AUTONYMS for autonyms (native names)
* @param string $include One of:
* @since 1.20
*/
public static function fetchLanguageNames( $inLanguage = self::AS_AUTONYMS, $include = 'mw' ) {
- $cacheKey = $inLanguage === self::AS_AUTONYMS ? 'null' : $inLanguage;
- $cacheKey .= ":$include";
- if ( self::$languageNameCache === null ) {
- self::$languageNameCache = new HashBagOStuff( [ 'maxKeys' => 20 ] );
- }
-
- $ret = self::$languageNameCache->get( $cacheKey );
- if ( !$ret ) {
- $ret = self::fetchLanguageNamesUncached( $inLanguage, $include );
- self::$languageNameCache->set( $cacheKey, $ret );
- }
- return $ret;
- }
-
- /**
- * Uncached helper for fetchLanguageNames
- * @param null|string $inLanguage Code of language in which to return the names
- * Use self::AS_AUTONYMS for autonyms (native names)
- * @param string $include One of:
- * self::ALL all available languages
- * 'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
- * self::SUPPORTED only if the language is in 'mw' *and* has a message file
- * @return array Language code => language name (sorted by key)
- */
- private static function fetchLanguageNamesUncached(
- $inLanguage = self::AS_AUTONYMS,
- $include = 'mw'
- ) {
- global $wgExtraLanguageNames, $wgUsePigLatinVariant;
-
- // If passed an invalid language code to use, fallback to en
- if ( $inLanguage !== self::AS_AUTONYMS && !self::isValidCode( $inLanguage ) ) {
- $inLanguage = 'en';
- }
-
- $names = [];
-
- if ( $inLanguage ) {
- # TODO: also include when $inLanguage is null, when this code is more efficient
- Hooks::run( 'LanguageGetTranslatedLanguageNames', [ &$names, $inLanguage ] );
- }
-
- $mwNames = $wgExtraLanguageNames + MediaWiki\Languages\Data\Names::$names;
- if ( $wgUsePigLatinVariant ) {
- // Pig Latin (for variant development)
- $mwNames['en-x-piglatin'] = 'Igpay Atinlay';
- }
-
- foreach ( $mwNames as $mwCode => $mwName ) {
- # - Prefer own MediaWiki native name when not using the hook
- # - For other names just add if not added through the hook
- if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) {
- $names[$mwCode] = $mwName;
- }
- }
-
- if ( $include === self::ALL ) {
- ksort( $names );
- return $names;
- }
-
- $returnMw = [];
- $coreCodes = array_keys( $mwNames );
- foreach ( $coreCodes as $coreCode ) {
- $returnMw[$coreCode] = $names[$coreCode];
- }
-
- if ( $include === self::SUPPORTED ) {
- $namesMwFile = [];
- # We do this using a foreach over the codes instead of a directory
- # loop so that messages files in extensions will work correctly.
- foreach ( $returnMw as $code => $value ) {
- if ( is_readable( self::getMessagesFileName( $code ) )
- || is_readable( self::getJsonMessagesFileName( $code ) )
- ) {
- $namesMwFile[$code] = $names[$code];
- }
- }
-
- ksort( $namesMwFile );
- return $namesMwFile;
- }
-
- ksort( $returnMw );
- # 'mw' option; default if it's not one of the other two options (all/mwfile)
- return $returnMw;
+ return MediaWikiServices::getInstance()->getLanguageNameUtils()
+ ->getLanguageNames( $inLanguage, $include );
}
/**
+ * @deprecated since 1.34, use LanguageNameUtils::getLanguageName
* @param string $code The code of the language for which to get the name
* @param null|string $inLanguage Code of language in which to return the name
* (SELF::AS_AUTONYMS for autonyms)
$inLanguage = self::AS_AUTONYMS,
$include = self::ALL
) {
- $code = strtolower( $code );
- $array = self::fetchLanguageNames( $inLanguage, $include );
- return !array_key_exists( $code, $array ) ? '' : $array[$code];
+ return MediaWikiServices::getInstance()->getLanguageNameUtils()
+ ->getLanguageName( $code, $inLanguage, $include );
}
/**
/**
* Get the name of a file for a certain language code
+ *
+ * @deprecated since 1.34, use LanguageNameUtils
* @param string $prefix Prepend this to the filename
* @param string $code Language code
* @param string $suffix Append this to the filename
* @return string $prefix . $mangledCode . $suffix
*/
public static function getFileName( $prefix, $code, $suffix = '.php' ) {
- if ( !self::isValidBuiltInCode( $code ) ) {
- throw new MWException( "Invalid language code \"$code\"" );
- }
-
- return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
+ return MediaWikiServices::getInstance()->getLanguageNameUtils()
+ ->getFileName( $prefix, $code, $suffix );
}
/**
+ * @deprecated since 1.34, use LanguageNameUtils
* @param string $code
* @return string
*/
public static function getMessagesFileName( $code ) {
- global $IP;
- $file = self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
- Hooks::run( 'Language::getMessagesFileName', [ $code, &$file ] );
- return $file;
+ return MediaWikiServices::getInstance()->getLanguageNameUtils()
+ ->getMessagesFileName( $code );
}
/**
+ * @deprecated since 1.34, use LanguageNameUtils
* @param string $code
* @return string
* @throws MWException
* @since 1.23
*/
public static function getJsonMessagesFileName( $code ) {
- global $IP;
-
- if ( !self::isValidBuiltInCode( $code ) ) {
- throw new MWException( "Invalid language code \"$code\"" );
- }
-
- return "$IP/languages/i18n/$code.json";
+ return MediaWikiServices::getInstance()->getLanguageNameUtils()
+ ->getJsonMessagesFileName( $code );
}
/**
* If you are adding support for such a language, add it also to
* the relevant section in shared.css.
*
- * Do not use this class directly. Use Language::fetchLanguageNames(), which
+ * Do not use this class directly. Use LanguageNameUtils::getLanguageNames(), which
* includes support for the CLDR extension.
*
* @ingroup Language
[ function () {
MediaWikiServices::getInstance()->getResourceLoader()
->getMessageBlobStore()->clear();
- } ]
+ } ],
+ MediaWikiServices::getInstance()->getLanguageNameUtils()
);
$allCodes = array_keys( Language::fetchLanguageNames( null, 'mwfile' ) );
# tests/phpunit/unit/includes
'BadFileLookupTest' => "$testDir/phpunit/unit/includes/BadFileLookupTest.php",
+ # tests/phpunit/unit/includes/language
+ 'LanguageNameUtilsTestTrait' => "$testDir/phpunit/unit/includes/language/LanguageNameUtilsTestTrait.php",
+
# tests/phpunit/unit/includes/libs/filebackend/fsfile
'TempFSFileTestTrait' => "$testDir/phpunit/unit/includes/libs/filebackend/fsfile/TempFSFileTestTrait.php",
];
}
+ /**
+ * @param int $titleLastRevision Last Title revision to set
+ * @param int $outputRevision Revision stored in OutputPage
+ * @param bool $expectedResult Expected result of $output->isRevisionCurrent call
+ * @covers OutputPage::isRevisionCurrent
+ * @dataProvider provideIsRevisionCurrent
+ */
+ public function testIsRevisionCurrent( $titleLastRevision, $outputRevision, $expectedResult ) {
+ $titleMock = $this->getMock( Title::class, [], [], '', false );
+ $titleMock->expects( $this->any() )
+ ->method( 'getLatestRevID' )
+ ->willReturn( $titleLastRevision );
+
+ $output = $this->newInstance( [], null, [ 'notitle' => true ] );
+ $output->setTitle( $titleMock );
+ $output->setRevisionId( $outputRevision );
+ $this->assertEquals( $expectedResult, $output->isRevisionCurrent() );
+ }
+
+ public function provideIsRevisionCurrent() {
+ return [
+ [ 10, null, true ],
+ [ 42, 42, true ],
+ [ null, 0, true ],
+ [ 42, 47, false ],
+ [ 47, 42, false ]
+ ];
+ }
+
/**
* @return OutputPage
*/
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Revision\MutableRevisionRecord;
use MediaWiki\Revision\RevisionLookup;
+use MWException;
use TestAllServiceOptionsUsed;
use Wikimedia\ScopedCallback;
use MediaWiki\Session\SessionId;
'BlockDisablesLogin' => false,
'GroupPermissions' => [],
'RevokePermissions' => [],
- 'AvailableRights' => []
+ 'AvailableRights' => [],
+ 'NamespaceProtection' => [],
+ 'RestrictionLevels' => []
]
),
$services->getSpecialPageFactory(),
return $revision;
}
+ public function provideGetRestrictionLevels() {
+ return [
+ 'No namespace restriction' => [ [ '', 'autoconfirmed', 'sysop' ], NS_TALK ],
+ 'Restricted to autoconfirmed' => [ [ '', 'sysop' ], NS_MAIN ],
+ 'Restricted to sysop' => [ [ '' ], NS_USER ],
+ 'Restricted to someone in two groups' => [ [ '', 'sysop' ], 101 ],
+ 'No special permissions' => [
+ [ '' ],
+ NS_TALK,
+ []
+ ],
+ 'autoconfirmed' => [
+ [ '', 'autoconfirmed' ],
+ NS_TALK,
+ [ 'autoconfirmed' ]
+ ],
+ 'autoconfirmed revoked' => [
+ [ '' ],
+ NS_TALK,
+ [ 'autoconfirmed', 'noeditsemiprotected' ]
+ ],
+ 'sysop' => [
+ [ '', 'autoconfirmed', 'sysop' ],
+ NS_TALK,
+ [ 'sysop' ]
+ ],
+ 'sysop with autoconfirmed revoked (a bit silly)' => [
+ [ '', 'sysop' ],
+ NS_TALK,
+ [ 'sysop', 'noeditsemiprotected' ]
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideGetRestrictionLevels
+ * @covers \MediaWiki\Permissions\PermissionManager::getNamespaceRestrictionLevels
+ *
+ * @param array $expected
+ * @param int $ns
+ * @param array|null $userGroups
+ * @throws MWException
+ */
+ public function testGetRestrictionLevels( array $expected, $ns, array $userGroups = null ) {
+ $this->setMwGlobals( [
+ 'wgGroupPermissions' => [
+ '*' => [ 'edit' => true ],
+ 'autoconfirmed' => [ 'editsemiprotected' => true ],
+ 'sysop' => [
+ 'editsemiprotected' => true,
+ 'editprotected' => true,
+ ],
+ 'privileged' => [ 'privileged' => true ],
+ ],
+ 'wgRevokePermissions' => [
+ 'noeditsemiprotected' => [ 'editsemiprotected' => true ],
+ ],
+ 'wgNamespaceProtection' => [
+ NS_MAIN => 'autoconfirmed',
+ NS_USER => 'sysop',
+ 101 => [ 'editsemiprotected', 'privileged' ],
+ ],
+ 'wgRestrictionLevels' => [ '', 'autoconfirmed', 'sysop' ],
+ 'wgAutopromote' => []
+ ] );
+ $this->resetServices();
+ $user = is_null( $userGroups ) ? null : $this->getTestUser( $userGroups )->getUser();
+ $this->assertSame( $expected, MediaWikiServices::getInstance()
+ ->getPermissionManager()
+ ->getNamespaceRestrictionLevels( $ns, $user ) );
+ }
}
'wgExtraInterlanguageLinkPrefixes' => [ 'self' ],
'wgExtraLanguageNames' => [ 'self' => 'Recursion' ],
] );
+ $this->resetServices();
MessageCache::singleton()->enable();
<?php
use MediaWiki\Config\ServiceOptions;
+use MediaWiki\Languages\LanguageNameUtils;
use Psr\Log\NullLogger;
/**
protected function getMockLocalisationCache() {
global $IP;
+ $mockLangNameUtils = $this->createMock( LanguageNameUtils::class );
+ $mockLangNameUtils->method( 'isValidBuiltInCode' )->will( $this->returnCallback(
+ function ( $code ) {
+ // Copy-paste, but it's only one line
+ return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
+ }
+ ) );
+ $mockLangNameUtils->method( 'isSupportedLanguage' )->will( $this->returnCallback(
+ function ( $code ) {
+ return in_array( $code, [
+ 'ar',
+ 'arz',
+ 'ba',
+ 'de',
+ 'en',
+ 'ksh',
+ 'ru',
+ ] );
+ }
+ ) );
+ $mockLangNameUtils->method( 'getMessagesFileName' )->will( $this->returnCallback(
+ function ( $code ) {
+ global $IP;
+ $code = str_replace( '-', '_', ucfirst( $code ) );
+ return "$IP/languages/messages/Messages$code.php";
+ }
+ ) );
+ $mockLangNameUtils->expects( $this->never() )->method( $this->anythingBut(
+ 'isValidBuiltInCode', 'isSupportedLanguage', 'getMessagesFileName'
+ ) );
+
$lc = $this->getMockBuilder( LocalisationCache::class )
->setConstructorArgs( [
new ServiceOptions( LocalisationCache::$constructorOptions, [
'MessagesDirs' => [],
] ),
new LCStoreDB( [] ),
- new NullLogger
+ new NullLogger,
+ [],
+ $mockLangNameUtils
] )
->setMethods( [ 'getMessagesDirs' ] )
->getMock();
'ExtraNamespaces' => [],
'ExtraSignatureNamespaces' => [],
'NamespaceContentModels' => [],
- 'NamespaceProtection' => [],
'NamespacesWithSubpages' => [
NS_TALK => true,
NS_USER => true,
NS_USER_TALK => true,
],
'NonincludableNamespaces' => [],
- 'RestrictionLevels' => [ '', 'autoconfirmed', 'sysop' ],
];
private function newObj( array $options = [] ) : NamespaceInfo {
*/
/**
- * This mock user can only have isAllowed() called on it.
- *
- * @param array $groups Groups for the mock user to have
- * @return User
- */
- private function getMockUser( array $groups = [] ) : User {
- $groups[] = '*';
-
- $mock = $this->createMock( User::class );
- $mock->method( 'isAllowed' )->will( $this->returnCallback(
- function ( $action ) use ( $groups ) {
- global $wgGroupPermissions, $wgRevokePermissions;
- if ( $action == '' ) {
- return true;
- }
- foreach ( $wgRevokePermissions as $group => $rights ) {
- if ( !in_array( $group, $groups ) ) {
- continue;
- }
- if ( isset( $rights[$action] ) && $rights[$action] ) {
- return false;
- }
- }
- foreach ( $wgGroupPermissions as $group => $rights ) {
- if ( !in_array( $group, $groups ) ) {
- continue;
- }
- if ( isset( $rights[$action] ) && $rights[$action] ) {
- return true;
- }
- }
- return false;
- }
- ) );
- $mock->expects( $this->never() )->method( $this->anythingBut( 'isAllowed' ) );
- return $mock;
- }
-
- /**
+ * TODO: This is superceeded by PermissionManagerTest::testGetNamespaceRestrictionLevels
+ * Remove when deprecated method is removed.
* @dataProvider provideGetRestrictionLevels
- * @covers NamespaceInfo::getRestrictionLevels
+ * @covers NamespaceInfo::getRestrictionLevels
*
* @param array $expected
* @param int $ns
- * @param User|null $user
+ * @param array|null $groups
+ * @throws MWException
*/
- public function testGetRestrictionLevels( array $expected, $ns, User $user = null ) {
+ public function testGetRestrictionLevels( array $expected, $ns, array $groups = null ) {
$this->setMwGlobals( [
'wgGroupPermissions' => [
'*' => [ 'edit' => true ],
'wgRevokePermissions' => [
'noeditsemiprotected' => [ 'editsemiprotected' => true ],
],
- ] );
- $obj = $this->newObj( [
- 'NamespaceProtection' => [
+ 'wgNamespaceProtection' => [
NS_MAIN => 'autoconfirmed',
NS_USER => 'sysop',
101 => [ 'editsemiprotected', 'privileged' ],
],
+ 'wgRestrictionLevels' => [ '', 'autoconfirmed', 'sysop' ],
+ 'wgAutopromote' => []
] );
+ $this->resetServices();
+ $obj = $this->newObj();
+ $user = is_null( $groups ) ? null : $this->getTestUser( $groups )->getUser();
$this->assertSame( $expected, $obj->getRestrictionLevels( $ns, $user ) );
}
'Restricted to autoconfirmed' => [ [ '', 'sysop' ], NS_MAIN ],
'Restricted to sysop' => [ [ '' ], NS_USER ],
'Restricted to someone in two groups' => [ [ '', 'sysop' ], 101 ],
- 'No special permissions' => [ [ '' ], NS_TALK, $this->getMockUser() ],
+ 'No special permissions' => [ [ '' ], NS_TALK, [] ],
'autoconfirmed' => [
[ '', 'autoconfirmed' ],
NS_TALK,
- $this->getMockUser( [ 'autoconfirmed' ] )
+ [ 'autoconfirmed' ]
],
'autoconfirmed revoked' => [
[ '' ],
NS_TALK,
- $this->getMockUser( [ 'autoconfirmed', 'noeditsemiprotected' ] )
+ [ 'autoconfirmed', 'noeditsemiprotected' ]
],
'sysop' => [
[ '', 'autoconfirmed', 'sysop' ],
NS_TALK,
- $this->getMockUser( [ 'sysop' ] )
+ [ 'sysop' ]
],
'sysop with autoconfirmed revoked (a bit silly)' => [
[ '', 'sysop' ],
NS_TALK,
- $this->getMockUser( [ 'sysop', 'noeditsemiprotected' ] )
+ [ 'sysop', 'noeditsemiprotected' ]
],
];
}
use Wikimedia\TestingAccessWrapper;
class LanguageTest extends LanguageClassesTestCase {
+ use LanguageNameUtilsTestTrait;
+
+ /** @var array Copy of $wgHooks from before we unset LanguageGetTranslatedLanguageNames */
+ private $origHooks;
+
+ public function setUp() {
+ global $wgHooks;
+
+ parent::setUp();
+
+ // Don't allow installed hooks to run, except if a test restores them via origHooks (needed
+ // for testIsKnownLanguageTag_cldr)
+ $this->origHooks = $wgHooks;
+ $newHooks = $wgHooks;
+ unset( $newHooks['LanguageGetTranslatedLanguageNames'] );
+ $this->setMwGlobals( 'wgHooks', $newHooks );
+ }
+
/**
* @covers Language::convertDoubleWidth
* @covers Language::normalizeForSearch
);
}
- /**
- * Test Language::isValidBuiltInCode()
- * @dataProvider provideLanguageCodes
- * @covers Language::isValidBuiltInCode
- */
- public function testBuiltInCodeValidation( $code, $expected, $message = '' ) {
- $this->assertEquals( $expected,
- (bool)Language::isValidBuiltInCode( $code ),
- "validating code $code $message"
- );
- }
-
- public static function provideLanguageCodes() {
- return [
- [ 'fr', true, 'Two letters, minor case' ],
- [ 'EN', false, 'Two letters, upper case' ],
- [ 'tyv', true, 'Three letters' ],
- [ 'be-tarask', true, 'With dash' ],
- [ 'be-x-old', true, 'With extension (two dashes)' ],
- [ 'be_tarask', false, 'Reject underscores' ],
- ];
- }
-
- /**
- * Test Language::isKnownLanguageTag()
- * @dataProvider provideKnownLanguageTags
- * @covers Language::isKnownLanguageTag
- */
- public function testKnownLanguageTag( $code, $message = '' ) {
- $this->assertTrue(
- (bool)Language::isKnownLanguageTag( $code ),
- "validating code $code - $message"
- );
- }
-
- public static function provideKnownLanguageTags() {
- return [
- [ 'fr', 'simple code' ],
- [ 'bat-smg', 'an MW legacy tag' ],
- [ 'sgs', 'an internal standard MW name, for which a legacy tag is used externally' ],
- ];
- }
-
- /**
- * @covers Language::isKnownLanguageTag
- */
- public function testKnownCldrLanguageTag() {
- if ( !class_exists( 'LanguageNames' ) ) {
- $this->markTestSkipped( 'The LanguageNames class is not available. '
- . 'The CLDR extension is probably not installed.' );
- }
-
- $this->assertTrue(
- (bool)Language::isKnownLanguageTag( 'pal' ),
- 'validating code "pal" an ancient language, which probably will '
- . 'not appear in Names.php, but appears in CLDR in English'
- );
- }
-
- /**
- * Negative tests for Language::isKnownLanguageTag()
- * @dataProvider provideUnKnownLanguageTags
- * @covers Language::isKnownLanguageTag
- */
- public function testUnknownLanguageTag( $code, $message = '' ) {
- $this->assertFalse(
- (bool)Language::isKnownLanguageTag( $code ),
- "checking that code $code is invalid - $message"
- );
- }
-
- public static function provideUnknownLanguageTags() {
- return [
- [ 'mw', 'non-existent two-letter code' ],
- [ 'foo"<bar', 'very invalid language code' ],
- ];
- }
-
/**
* Test too short timestamp
* @expectedException MWException
$lang->getGrammarTransformations();
$this->assertNotNull( $languageClass->grammarTransformations );
- // Populate $languageNameCache
- Language::fetchLanguageNames();
- $this->assertNotNull( $languageClass->languageNameCache );
-
Language::clearCaches();
$this->assertCount( 0, Language::$mLangObjCache );
$this->assertCount( 0, $languageClass->fallbackLanguageCache );
$this->assertNull( $languageClass->grammarTransformations );
- $this->assertNull( $languageClass->languageNameCache );
- }
-
- /**
- * @dataProvider provideIsSupportedLanguage
- * @covers Language::isSupportedLanguage
- */
- public function testIsSupportedLanguage( $code, $expected, $comment ) {
- $this->assertEquals( $expected, Language::isSupportedLanguage( $code ), $comment );
- }
-
- public static function provideIsSupportedLanguage() {
- return [
- [ 'en', true, 'is supported language' ],
- [ 'fi', true, 'is supported language' ],
- [ 'bunny', false, 'is not supported language' ],
- [ 'FI', false, 'is not supported language, input should be in lower case' ],
- ];
}
/**
[ 'èl', 'Ll' , 'Non-ASCII is overridden', [ 'è' => 'L' ] ],
];
}
+
+ // The following methods are for LanguageNameUtilsTestTrait
+
+ private function isSupportedLanguage( $code ) {
+ return Language::isSupportedLanguage( $code );
+ }
+
+ private function isValidCode( $code ) {
+ return Language::isValidCode( $code );
+ }
+
+ private function isValidBuiltInCode( $code ) {
+ return Language::isValidBuiltInCode( $code );
+ }
+
+ private function isKnownLanguageTag( $code ) {
+ return Language::isKnownLanguageTag( $code );
+ }
+
+ /**
+ * Call getLanguageName() and getLanguageNames() using the Language static methods.
+ *
+ * @param array $options To set globals for testing Language
+ * @param string $expected
+ * @param string $code
+ * @param mixed ...$otherArgs Optionally, pass $inLanguage and/or $include.
+ */
+ private function assertGetLanguageNames( array $options, $expected, $code, ...$otherArgs ) {
+ if ( $options ) {
+ foreach ( $options as $key => $val ) {
+ $this->setMwGlobals( "wg$key", $val );
+ }
+ $this->resetServices();
+ }
+ $this->assertSame( $expected,
+ Language::fetchLanguageNames( ...$otherArgs )[strtolower( $code )] ?? '' );
+ $this->assertSame( $expected, Language::fetchLanguageName( $code, ...$otherArgs ) );
+ }
+
+ private function getLanguageNames( ...$args ) {
+ return Language::fetchLanguageNames( ...$args );
+ }
+
+ private function getLanguageName( ...$args ) {
+ return Language::fetchLanguageName( ...$args );
+ }
+
+ private static function getFileName( ...$args ) {
+ return Language::getFileName( ...$args );
+ }
+
+ private static function getMessagesFileName( $code ) {
+ return Language::getMessagesFileName( $code );
+ }
+
+ private static function getJsonMessagesFileName( $code ) {
+ return Language::getJsonMessagesFileName( $code );
+ }
+
+ /**
+ * @todo This really belongs in the cldr extension's tests.
+ *
+ * @covers MediaWiki\Languages\LanguageNameUtils::isKnownLanguageTag
+ * @covers Language::isKnownLanguageTag
+ */
+ public function testIsKnownLanguageTag_cldr() {
+ if ( !class_exists( 'LanguageNames' ) ) {
+ $this->markTestSkipped( 'The LanguageNames class is not available. '
+ . 'The CLDR extension is probably not installed.' );
+ }
+
+ // We need to restore the extension's hook that we removed.
+ $this->setMwGlobals( 'wgHooks', $this->origHooks );
+
+ // "pal" is an ancient language, which probably will not appear in Names.php, but appears in
+ // CLDR in English
+ $this->assertTrue( Language::isKnownLanguageTag( 'pal' ) );
+ }
}
--- /dev/null
+<?php
+
+use MediaWiki\Config\ServiceOptions;
+use MediaWiki\Languages\LanguageNameUtils;
+
+class LanguageNameUtilsTest extends MediaWikiUnitTestCase {
+ /**
+ * @param array $optionsArray
+ */
+ private static function newObj( array $optionsArray = [] ) : LanguageNameUtils {
+ return new LanguageNameUtils( new ServiceOptions(
+ LanguageNameUtils::$constructorOptions,
+ $optionsArray,
+ [
+ 'ExtraLanguageNames' => [],
+ 'LanguageCode' => 'en',
+ 'UsePigLatinVariant' => false,
+ ]
+ ) );
+ }
+
+ use LanguageNameUtilsTestTrait;
+
+ private function isSupportedLanguage( $code ) {
+ return $this->newObj()->isSupportedLanguage( $code );
+ }
+
+ private function isValidCode( $code ) {
+ return $this->newObj()->isValidCode( $code );
+ }
+
+ private function isValidBuiltInCode( $code ) {
+ return $this->newObj()->isValidBuiltInCode( $code );
+ }
+
+ private function isKnownLanguageTag( $code ) {
+ return $this->newObj()->isKnownLanguageTag( $code );
+ }
+
+ private function assertGetLanguageNames( array $options, $expected, $code, ...$otherArgs ) {
+ $this->assertSame( $expected, $this->newObj( $options )
+ ->getLanguageNames( ...$otherArgs )[strtolower( $code )] ?? '' );
+ $this->assertSame( $expected,
+ $this->newObj( $options )->getLanguageName( $code, ...$otherArgs ) );
+ }
+
+ private function getLanguageNames( ...$args ) {
+ return $this->newObj()->getLanguageNames( ...$args );
+ }
+
+ private function getLanguageName( ...$args ) {
+ return $this->newObj()->getLanguageName( ...$args );
+ }
+
+ private static function getFileName( ...$args ) {
+ return self::newObj()->getFileName( ...$args );
+ }
+
+ private static function getMessagesFileName( $code ) {
+ return self::newObj()->getMessagesFileName( $code );
+ }
+
+ private static function getJsonMessagesFileName( $code ) {
+ return self::newObj()->getJsonMessagesFileName( $code );
+ }
+}
--- /dev/null
+<?php
+
+use MediaWiki\Languages\LanguageNameUtils;
+
+const AUTONYMS = LanguageNameUtils::AUTONYMS;
+const ALL = LanguageNameUtils::ALL;
+const DEFINED = LanguageNameUtils::DEFINED;
+const SUPPORTED = LanguageNameUtils::SUPPORTED;
+
+/**
+ * For code shared between LanguageNameUtilsTest and LanguageTest.
+ */
+trait LanguageNameUtilsTestTrait {
+ abstract protected function isSupportedLanguage( $code );
+
+ /**
+ * @dataProvider provideIsSupportedLanguage
+ * @covers MediaWiki\Languages\LanguageNameUtils::__construct
+ * @covers MediaWiki\Languages\LanguageNameUtils::isSupportedLanguage
+ * @covers Language::isSupportedLanguage
+ */
+ public function testIsSupportedLanguage( $code, $expected ) {
+ $this->assertSame( $expected, $this->isSupportedLanguage( $code ) );
+ }
+
+ public static function provideIsSupportedLanguage() {
+ return [
+ 'en' => [ 'en', true ],
+ 'fi' => [ 'fi', true ],
+ 'bunny' => [ 'bunny', false ],
+ 'qqq' => [ 'qqq', false ],
+ 'uppercase is not considered supported' => [ 'FI', false ],
+ ];
+ }
+
+ abstract protected function isValidCode( $code );
+
+ /**
+ * We don't test that the result is cached, because that should only be noticeable if the
+ * configuration changes in between calls, and 1) that should never happen in normal operation,
+ * 2) if you do it you deserve whatever you get, and 3) once the static Language method is
+ * dropped and the invalid title regex is moved to something injected instead of a static call,
+ * the cache will be undetectable.
+ *
+ * @todo Should we test changes to $wgLegalTitleChars here? Does anybody actually change that?
+ * Is it possible to change it usefully without breaking everything?
+ *
+ * @dataProvider provideIsValidCode
+ * @covers MediaWiki\Languages\LanguageNameUtils::isValidCode
+ * @covers Language::isValidCode
+ *
+ * @param string $code
+ * @param bool $expected
+ */
+ public function testIsValidCode( $code, $expected ) {
+ $this->assertSame( $expected, $this->isValidCode( $code ) );
+ }
+
+ public static function provideIsValidCode() {
+ $ret = [
+ 'en' => [ 'en', true ],
+ 'en-GB' => [ 'en-GB', true ],
+ 'Funny chars' => [ "%!$()*,-.;=?@^_`~\x80\xA2\xFF+", true ],
+ 'Percent escape not allowed' => [ 'a%aF', false ],
+ 'Percent with only one following char is okay' => [ '%a', true ],
+ 'Percent with non-hex following chars is okay' => [ '%AG', true ],
+ 'Named char reference "a"' => [ 'a&a', false ],
+ 'Named char reference "A"' => [ 'a&A', false ],
+ 'Named char reference "0"' => [ 'a&0', false ],
+ 'Named char reference non-ASCII' => [ "a&\x92", false ],
+ 'Numeric char reference' => [ "a�", false ],
+ 'Hex char reference 0' => [ "a�", false ],
+ 'Hex char reference A' => [ "a
", false ],
+ 'Lone ampersand is valid for title but not lang code' => [ '&', false ],
+ 'Ampersand followed by just # is valid for title but not lang code' => [ '&#', false ],
+ 'Ampersand followed by # and non-x/digit is valid for title but not lang code' =>
+ [ '&#a', false ],
+ ];
+ $disallowedChars = ":/\\\000&<>'\"";
+ foreach ( str_split( $disallowedChars ) as $char ) {
+ $ret["Disallowed character $char"] = [ "a{$char}a", false ];
+ }
+ return $ret;
+ }
+
+ abstract protected function isValidBuiltInCode( $code );
+
+ /**
+ * @dataProvider provideIsValidBuiltInCode
+ * @covers MediaWiki\Languages\LanguageNameUtils::isValidBuiltInCode
+ * @covers Language::isValidBuiltInCode
+ *
+ * @param string $code
+ * @param bool $expected
+ */
+ public function testIsValidBuiltInCode( $code, $expected ) {
+ $this->assertSame( $expected, $this->isValidBuiltInCode( $code ) );
+ }
+
+ public static function provideIsValidBuiltInCode() {
+ return [
+ 'Two letters, lowercase' => [ 'fr', true ],
+ 'Two letters, uppercase' => [ 'EN', false ],
+ 'Three letters' => [ 'tyv', true ],
+ 'With dash' => [ 'be-tarask', true ],
+ 'With extension (two dashes)' => [ 'be-x-old', true ],
+ 'Reject underscores' => [ 'be_tarask', false ],
+ 'One letter' => [ 'a', false ],
+ 'Only digits' => [ '00', true ],
+ 'Only dashes' => [ '--', true ],
+ 'Unreasonably long' => [ str_repeat( 'x', 100 ), true ],
+ 'qqq' => [ 'qqq', true ],
+ ];
+ }
+
+ abstract protected function isKnownLanguageTag( $code );
+
+ /**
+ * @dataProvider provideIsKnownLanguageTag
+ * @covers MediaWiki\Languages\LanguageNameUtils::isKnownLanguageTag
+ * @covers Language::isKnownLanguageTag
+ *
+ * @param string $code
+ * @param bool $expected
+ */
+ public function testIsKnownLanguageTag( $code, $expected ) {
+ $this->assertSame( $expected, $this->isKnownLanguageTag( $code ) );
+ }
+
+ public static function provideIsKnownLanguageTag() {
+ $invalidBuiltInCodes = array_filter( static::provideIsValidBuiltInCode(),
+ function ( $arr ) {
+ // If isValidBuiltInCode() returns false, we want to also, but if it returns true,
+ // we could still return false from isKnownLanguageTag(), so skip those.
+ return !$arr[1];
+ }
+ );
+ return array_merge( $invalidBuiltInCodes, [
+ 'Simple code' => [ 'fr', true ],
+ 'An MW legacy tag' => [ 'bat-smg', true ],
+ 'An internal standard MW name, for which a legacy tag is used externally' =>
+ [ 'sgs', true ],
+ 'Non-existent two-letter code' => [ 'mw', false ],
+ 'Very invalid language code' => [ 'foo"<bar', false ],
+ ] );
+ }
+
+ abstract protected function assertGetLanguageNames(
+ array $options, $expected, $code, ...$otherArgs
+ );
+
+ abstract protected function getLanguageNames( ...$args );
+
+ abstract protected function getLanguageName( ...$args );
+
+ /**
+ * @dataProvider provideGetLanguageNames
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageName
+ * @covers Language::fetchLanguageNames
+ * @covers Language::fetchLanguageName
+ *
+ * @param string $expected
+ * @param string $code
+ * @param mixed ...$otherArgs Optionally, pass $inLanguage and/or $include.
+ */
+ public function testGetLanguageNames( $expected, $code, ...$otherArgs ) {
+ $this->assertGetLanguageNames( [], $expected, $code, ...$otherArgs );
+ }
+
+ public static function provideGetLanguageNames() {
+ // @todo There are probably lots of interesting tests to add here.
+ return [
+ 'Simple code' => [ 'Deutsch', 'de' ],
+ 'Simple code in a different language (doesn\'t work without hook)' =>
+ [ 'Deutsch', 'de', 'fr' ],
+ 'Invalid code' => [ '', '&' ],
+ 'Pig Latin not enabled' => [ '', 'en-x-piglatin', AUTONYMS, ALL ],
+ 'qqq doesn\'t have a name' => [ '', 'qqq', AUTONYMS, ALL ],
+ 'An MW legacy tag is recognized' => [ 'žemaitėška', 'bat-smg' ],
+ // @todo Is the next test's result desired?
+ 'An MW legacy tag is not supported' => [ '', 'bat-smg', AUTONYMS, SUPPORTED ],
+ 'An internal standard name, for which a legacy tag is used externally, is supported' =>
+ [ 'žemaitėška', 'sgs', AUTONYMS, SUPPORTED ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideGetLanguageNames_withHook
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageName
+ * @covers Language::fetchLanguageNames
+ * @covers Language::fetchLanguageName
+ *
+ * @param string $expected Expected return value of getLanguageName()
+ * @param string $code
+ * @param mixed ...$otherArgs Optionally, pass $inLanguage and/or $include.
+ */
+ public function testGetLanguageNames_withHook( $expected, $code, ...$otherArgs ) {
+ $this->setTemporaryHook( 'LanguageGetTranslatedLanguageNames',
+ function ( &$names, $inLanguage ) {
+ switch ( $inLanguage ) {
+ case 'de':
+ $names = [
+ 'de' => 'Deutsch',
+ 'en' => 'Englisch',
+ 'fr' => 'Französisch',
+ ];
+ break;
+
+ case 'en':
+ $names = [
+ 'de' => 'German',
+ 'en' => 'English',
+ 'fr' => 'French',
+ 'sqsqsqsq' => '!!?!',
+ 'bat-smg' => 'Samogitian',
+ ];
+ break;
+
+ case 'fr':
+ $names = [
+ 'de' => 'allemand',
+ 'en' => 'anglais',
+ // Deliberate mistake (no cedilla)
+ 'fr' => 'francais',
+ ];
+ break;
+ }
+ }
+ );
+
+ // Really we could dispense with assertGetLanguageNames() and just call
+ // testGetLanguageNames() here, but it looks weird to call a test method from another test
+ // method.
+ $this->assertGetLanguageNames( [], $expected, $code, ...$otherArgs );
+ }
+
+ public static function provideGetLanguageNames_withHook() {
+ return [
+ 'Simple code in a different language' => [ 'allemand', 'de', 'fr' ],
+ 'Invalid inLanguage defaults to English' => [ 'German', 'de', '&' ],
+ 'If inLanguage not provided, default to autonym' => [ 'Deutsch', 'de' ],
+ 'Hooks ignored for explicitly-requested autonym' => [ 'français', 'fr', 'fr' ],
+ 'Hooks don\'t make a language supported' => [ '', 'bat-smg', 'en', SUPPORTED ],
+ 'Hooks don\'t make a language defined' => [ '', 'sqsqsqsq', 'en', DEFINED ],
+ 'Hooks do make a language name returned with ALL' => [ '!!?!', 'sqsqsqsq', 'en', ALL ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideGetLanguageNames_ExtraLanguageNames
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageName
+ * @covers Language::fetchLanguageNames
+ * @covers Language::fetchLanguageName
+ *
+ * @param string $expected Expected return value of getLanguageName()
+ * @param string $code
+ * @param mixed ...$otherArgs Optionally, pass $inLanguage and/or $include.
+ */
+ public function testGetLanguageNames_ExtraLanguageNames( $expected, $code, ...$otherArgs ) {
+ $this->setTemporaryHook( 'LanguageGetTranslatedLanguageNames',
+ function ( &$names ) {
+ $names['de'] = 'die deutsche Sprache';
+ }
+ );
+ $this->assertGetLanguageNames(
+ [ 'ExtraLanguageNames' => [ 'de' => 'deutsche Sprache', 'sqsqsqsq' => '!!?!' ] ],
+ $expected, $code, ...$otherArgs
+ );
+ }
+
+ public static function provideGetLanguageNames_ExtraLanguageNames() {
+ return [
+ 'Simple extra language name' => [ '!!?!', 'sqsqsqsq' ],
+ 'Extra language is defined' => [ '!!?!', 'sqsqsqsq', AUTONYMS, DEFINED ],
+ 'Extra language is not supported' => [ '', 'sqsqsqsq', AUTONYMS, SUPPORTED ],
+ 'Extra language overrides default' => [ 'deutsche Sprache', 'de' ],
+ 'Extra language overrides hook for explicitly requested autonym' =>
+ [ 'deutsche Sprache', 'de', 'de' ],
+ 'Hook overrides extra language for non-autonym' =>
+ [ 'die deutsche Sprache', 'de', 'fr' ],
+ ];
+ }
+
+ /**
+ * Test that getLanguageNames() defaults to DEFINED, and getLanguageName() defaults to ALL.
+ *
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageName
+ * @covers Language::fetchLanguageNames
+ * @covers Language::fetchLanguageName
+ */
+ public function testGetLanguageNames_parameterDefault() {
+ $this->setTemporaryHook( 'LanguageGetTranslatedLanguageNames',
+ function ( &$names ) {
+ $names = [ 'sqsqsqsq' => '!!?!' ];
+ }
+ );
+
+ // We use 'en' here because the hook is not run if we're requesting autonyms, although in
+ // this case (language that isn't defined by MediaWiki itself) that behavior seems wrong.
+ $this->assertArrayNotHasKey( 'sqsqsqsq', $this->getLanguageNames(), 'en' );
+
+ $this->assertSame( '!!?!', $this->getLanguageName( 'sqsqsqsq', 'en' ) );
+ }
+
+ /**
+ * @dataProvider provideGetLanguageNames_sorted
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
+ * @covers Language::fetchLanguageNames
+ *
+ * @param mixed ...$args To pass to method
+ */
+ public function testGetLanguageNames_sorted( ...$args ) {
+ $names = $this->getLanguageNames( ...$args );
+ $sortedNames = $names;
+ ksort( $sortedNames );
+ $this->assertSame( $sortedNames, $names );
+ }
+
+ public static function provideGetLanguageNames_sorted() {
+ return [
+ [],
+ [ AUTONYMS ],
+ [ AUTONYMS, 'mw' ],
+ [ AUTONYMS, ALL ],
+ [ AUTONYMS, SUPPORTED ],
+ [ 'he', 'mw' ],
+ [ 'he', ALL ],
+ [ 'he', SUPPORTED ],
+ ];
+ }
+
+ /**
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
+ * @covers Language::fetchLanguageNames
+ */
+ public function testGetLanguageNames_hookNotCalledForAutonyms() {
+ $count = 0;
+ $this->setTemporaryHook( 'LanguageGetTranslatedLanguageNames',
+ function () use ( &$count ) {
+ $count++;
+ }
+ );
+
+ $this->getLanguageNames();
+ $this->assertSame( 0, $count, 'Hook must not be called for autonyms' );
+
+ // We test elsewhere that the hook works, but the following verifies that our test is
+ // working and $count isn't being incremented above only because we're checking autonyms.
+ $this->getLanguageNames( 'fr' );
+ $this->assertSame( 1, $count, 'Hook must be called for non-autonyms' );
+ }
+
+ /**
+ * @dataProvider provideGetLanguageNames_pigLatin
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageName
+ * @covers Language::fetchLanguageNames
+ * @covers Language::fetchLanguageName
+ *
+ * @param string $expected
+ * @param mixed ...$otherArgs Optionally, pass $inLanguage and/or $include.
+ */
+ public function testGetLanguageNames_pigLatin( $expected, ...$otherArgs ) {
+ $this->setTemporaryHook( 'LanguageGetTranslatedLanguageNames',
+ function ( &$names, $inLanguage ) {
+ switch ( $inLanguage ) {
+ case 'fr':
+ $names = [ 'en-x-piglatin' => 'latin de cochons' ];
+ break;
+
+ case 'en-x-piglatin':
+ // Deliberately lowercase
+ $names = [ 'en-x-piglatin' => 'igpay atinlay' ];
+ break;
+ }
+ }
+ );
+
+ $this->assertGetLanguageNames(
+ [ 'UsePigLatinVariant' => true ], $expected, 'en-x-piglatin', ...$otherArgs );
+ }
+
+ public static function provideGetLanguageNames_pigLatin() {
+ return [
+ 'Simple test' => [ 'Igpay Atinlay' ],
+ 'Not supported' => [ '', AUTONYMS, SUPPORTED ],
+ 'Foreign language' => [ 'latin de cochons', 'fr' ],
+ 'Hook doesn\'t override explicit autonym' =>
+ [ 'Igpay Atinlay', 'en-x-piglatin', 'en-x-piglatin' ],
+ ];
+ }
+
+ /**
+ * Just for the sake of completeness, test that ExtraLanguageNames will not override the name
+ * for pig Latin. Nobody actually cares about this and if anything current behavior is probably
+ * wrong, but once we're testing the whole file we may as well be comprehensive.
+ *
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
+ * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageName
+ * @covers Language::fetchLanguageNames
+ * @covers Language::fetchLanguageName
+ */
+ public function testGetLanguageNames_pigLatinAndExtraLanguageNames() {
+ $this->assertGetLanguageNames(
+ [
+ 'UsePigLatinVariant' => true,
+ 'ExtraLanguageNames' => [ 'en-x-piglatin' => 'igpay atinlay' ]
+ ],
+ 'Igpay Atinlay',
+ 'en-x-piglatin'
+ );
+ }
+
+ abstract protected static function getFileName( ...$args );
+
+ /**
+ * @dataProvider provideGetFileName
+ * @covers MediaWiki\Languages\LanguageNameUtils::getFileName
+ * @covers Language::getFileName
+ *
+ * @param string $expected
+ * @param mixed ...$args To pass to method
+ */
+ public function testGetFileName( $expected, ...$args ) {
+ $this->assertSame( $expected, $this->getFileName( ...$args ) );
+ }
+
+ public static function provideGetFileName() {
+ return [
+ 'Simple case' => [ 'MessagesXx.php', 'Messages', 'xx' ],
+ 'With extension' => [ 'MessagesXx.ext', 'Messages', 'xx', '.ext' ],
+ 'Replacing dashes' => [ '!__?', '!', '--', '?' ],
+ 'Empty prefix and extension' => [ 'Xx', '', 'xx', '' ],
+ 'Uppercase only first letter' => [ 'Messages_a.php', 'Messages', '-a' ],
+ ];
+ }
+
+ abstract protected function getMessagesFileName( $code );
+
+ /**
+ * @dataProvider provideGetMessagesFileName
+ * @covers MediaWiki\Languages\LanguageNameUtils::getMessagesFileName
+ * @covers Language::getMessagesFileName
+ *
+ * @param string $code
+ * @param string $expected
+ */
+ public function testGetMessagesFileName( $code, $expected ) {
+ $this->assertSame( $expected, $this->getMessagesFileName( $code ) );
+ }
+
+ public static function provideGetMessagesFileName() {
+ global $IP;
+ return [
+ 'Simple case' => [ 'en', "$IP/languages/messages/MessagesEn.php" ],
+ 'Replacing dashes' => [ '--', "$IP/languages/messages/Messages__.php" ],
+ 'Uppercase only first letter' => [ '-a', "$IP/languages/messages/Messages_a.php" ],
+ ];
+ }
+
+ /**
+ * @covers MediaWiki\Languages\LanguageNameUtils::getMessagesFileName
+ * @covers Language::getMessagesFileName
+ */
+ public function testGetMessagesFileName_withHook() {
+ $called = 0;
+
+ $this->setTemporaryHook( 'Language::getMessagesFileName',
+ function ( $code, &$file ) use ( &$called ) {
+ global $IP;
+
+ $called++;
+
+ $this->assertSame( 'ab-cd', $code );
+ $this->assertSame( "$IP/languages/messages/MessagesAb_cd.php", $file );
+ $file = 'bye-bye';
+ }
+ );
+
+ $this->assertSame( 'bye-bye', $this->getMessagesFileName( 'ab-cd' ) );
+ $this->assertSame( 1, $called );
+ }
+
+ abstract protected function getJsonMessagesFileName( $code );
+
+ /**
+ * @covers MediaWiki\Languages\LanguageNameUtils::getJsonMessagesFileName
+ * @covers Language::getJsonMessagesFileName
+ */
+ public function testGetJsonMessagesFileName() {
+ global $IP;
+
+ // Not so much to test here, one test seems to be enough
+ $expected = "$IP/languages/i18n/en--123.json";
+ $this->assertSame( $expected, $this->getJsonMessagesFileName( 'en--123' ) );
+ }
+
+ /**
+ * getFileName, getMessagesFileName, and getJsonMessagesFileName all throw if they get an
+ * invalid code. To save boilerplate, test them all in one method.
+ *
+ * @dataProvider provideExceptionFromInvalidCode
+ * @covers MediaWiki\Languages\LanguageNameUtils::getFileName
+ * @covers MediaWiki\Languages\LanguageNameUtils::getMessagesFileName
+ * @covers MediaWiki\Languages\LanguageNameUtils::getJsonMessagesFileName
+ * @covers Language::getFileName
+ * @covers Language::getMessagesFileName
+ * @covers Language::getJsonMessagesFileName
+ *
+ * @param callable $callback Will throw when passed $code
+ * @param string $code
+ */
+ public function testExceptionFromInvalidCode( $callback, $code ) {
+ $this->setExpectedException( MWException::class, "Invalid language code \"$code\"" );
+
+ $callback( $code );
+ }
+
+ public static function provideExceptionFromInvalidCode() {
+ $ret = [];
+ foreach ( static::provideIsValidBuiltInCode() as $desc => list( $code, $valid ) ) {
+ if ( $valid ) {
+ // Won't get an exception from this one
+ continue;
+ }
+
+ // For getFileName, we define an anonymous function because of the extra first param
+ $ret["getFileName: $desc"] = [
+ function ( $code ) {
+ return static::getFileName( 'Messages', $code );
+ },
+ $code
+ ];
+
+ $ret["getMessagesFileName: $desc"] =
+ [ [ static::class, 'getMessagesFileName' ], $code ];
+
+ $ret["getJsonMessagesFileName: $desc"] =
+ [ [ static::class, 'getJsonMessagesFileName' ], $code ];
+ }
+ return $ret;
+ }
+}