Файловый менеджер - Редактировать - /var/www/html/user.zip
Ðазад
PK ! �(�F� � test_user_copy.shnu ȯ�� #!/bin/sh # SPDX-License-Identifier: GPL-2.0 # Runs copy_to/from_user infrastructure using test_user_copy kernel module # Kselftest framework requirement - SKIP code is 4. ksft_skip=4 if ! /sbin/modprobe -q -n test_user_copy; then echo "user: module test_user_copy is not found [SKIP]" exit $ksft_skip fi if /sbin/modprobe -q test_user_copy; then /sbin/modprobe -q -r test_user_copy echo "user_copy: ok" else echo "user_copy: [FAIL]" exit 1 fi PK ! �h\�� � Makefilenu �[��� # SPDX-License-Identifier: GPL-2.0-only # Makefile for user memory selftests # No binaries, but make sure arg-less "make" doesn't trigger "run_tests" all: TEST_PROGS := test_user_copy.sh include ../lib.mk PK ! �, �� � ActorStoreFactory.phpnu �Iw�� <?php /** * 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 */ namespace MediaWiki\User; use MediaWiki\Block\HideUserUtils; use MediaWiki\Config\ServiceOptions; use MediaWiki\DAO\WikiAwareEntity; use MediaWiki\MainConfigNames; use MediaWiki\User\TempUser\TempUserConfig; use Psr\Log\LoggerInterface; use Wikimedia\Rdbms\ILBFactory; use Wikimedia\Rdbms\ILoadBalancer; /** * ActorStore factory for various domains. * * @package MediaWiki\User * @since 1.36 */ class ActorStoreFactory { /** @internal */ public const CONSTRUCTOR_OPTIONS = [ MainConfigNames::SharedDB, MainConfigNames::SharedTables, ]; private ILBFactory $loadBalancerFactory; private UserNameUtils $userNameUtils; private TempUserConfig $tempUserConfig; private LoggerInterface $logger; private HideUserUtils $hideUserUtils; /** @var string|false */ private $sharedDB; /** @var string[] */ private $sharedTables; /** @var ActorStore[] */ private $storeCache = []; /** * @param ServiceOptions $options * @param ILBFactory $loadBalancerFactory * @param UserNameUtils $userNameUtils * @param TempUserConfig $tempUserConfig * @param LoggerInterface $logger * @param HideUserUtils $hideUserUtils */ public function __construct( ServiceOptions $options, ILBFactory $loadBalancerFactory, UserNameUtils $userNameUtils, TempUserConfig $tempUserConfig, LoggerInterface $logger, HideUserUtils $hideUserUtils ) { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->loadBalancerFactory = $loadBalancerFactory; $this->sharedDB = $options->get( MainConfigNames::SharedDB ); $this->sharedTables = $options->get( MainConfigNames::SharedTables ); $this->userNameUtils = $userNameUtils; $this->tempUserConfig = $tempUserConfig; $this->logger = $logger; $this->hideUserUtils = $hideUserUtils; } /** * @param string|false $wikiId * @return ActorNormalization */ public function getActorNormalization( $wikiId = WikiAwareEntity::LOCAL ): ActorNormalization { return $this->getActorStore( $wikiId ); } /** * @since 1.42 * @param string|false $wikiId * @return ActorNormalization */ public function getActorNormalizationForImport( $wikiId = WikiAwareEntity::LOCAL ): ActorNormalization { return $this->getActorStoreForImport( $wikiId ); } /** * @param string|false $wikiId * @return ActorStore */ public function getActorStore( $wikiId = WikiAwareEntity::LOCAL ): ActorStore { return $this->getStore( $wikiId, false ); } /** * @since 1.42 * @param string|false $wikiId * @return ActorStore */ public function getActorStoreForImport( $wikiId = WikiAwareEntity::LOCAL ): ActorStore { return $this->getStore( $wikiId, true ); } /** * @since 1.43 * @param string|false $wikiId * @return ActorStore */ public function getActorStoreForUndelete( $wikiId = WikiAwareEntity::LOCAL ): ActorStore { return $this->getStore( $wikiId, true ); } /** * @param string|false $wikiId * @param bool $allowingIpActorCreation * @return ActorStore */ private function getStore( $wikiId, bool $allowingIpActorCreation ): ActorStore { // During the transition from User, we still have old User objects // representing users from a different wiki, so we still have IDatabase::getDomainId // passed as $wikiId, so we need to remap it back to LOCAL. if ( is_string( $wikiId ) && $this->loadBalancerFactory->getLocalDomainID() === $wikiId ) { $wikiId = WikiAwareEntity::LOCAL; } $storeCacheKey = ( $allowingIpActorCreation ? 'allowing-ip-actor-creation-' : '' ) . ( $wikiId === WikiAwareEntity::LOCAL ? 'LOCAL' : $wikiId ); if ( !isset( $this->storeCache[$storeCacheKey] ) ) { $store = new ActorStore( $this->getLoadBalancerForTable( 'actor', $wikiId ), $this->userNameUtils, $this->tempUserConfig, $this->logger, $this->hideUserUtils, $wikiId ); if ( $allowingIpActorCreation ) { $store->setAllowCreateIpActors( true ); } $this->storeCache[$storeCacheKey] = $store; } return $this->storeCache[$storeCacheKey]; } /** * @param string|false $wikiId * @return UserIdentityLookup */ public function getUserIdentityLookup( $wikiId = WikiAwareEntity::LOCAL ): UserIdentityLookup { return $this->getActorStore( $wikiId ); } /** * Returns a load balancer for the database that has the $table * for the given $wikiId. * * @param string $table * @param string|false $wikiId * @return ILoadBalancer */ private function getLoadBalancerForTable( string $table, $wikiId = WikiAwareEntity::LOCAL ): ILoadBalancer { if ( $this->sharedDB && in_array( $table, $this->sharedTables ) ) { // The main LB is already properly set up for shared databases early in Setup.php return $this->loadBalancerFactory->getMainLB(); } return $this->loadBalancerFactory->getMainLB( $wikiId ); } } PK ! ۢ[O� � Options/UserOptionsLookup.phpnu �Iw�� <?php /** * 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 */ namespace MediaWiki\User\Options; use MediaWiki\User\UserIdentity; use Wikimedia\Rdbms\IDBAccessObject; /** * Provides access to user options * @since 1.35 */ abstract class UserOptionsLookup { /** * Exclude user options that are set to their default value. */ public const EXCLUDE_DEFAULTS = 1; /** * The suffix appended to preference names for the associated preference * that tracks whether they have a local override. * @since 1.43 */ public const LOCAL_EXCEPTION_SUFFIX = '-local-exception'; /** * Combine the language default options with any site-specific and user-specific defaults * and add the default language variants. * * @param UserIdentity|null $userIdentity User to look the default up for; set to null to * ignore any user-specific defaults (since 1.42) * @return array */ abstract public function getDefaultOptions( ?UserIdentity $userIdentity = null ): array; /** * Get a given default option value. * * @param string $opt Name of option to retrieve * @param UserIdentity|null $userIdentity User to look the defaults up for; set to null to * ignore any user-specific defaults (since 1.42) * @return mixed|null Default option value */ public function getDefaultOption( string $opt, ?UserIdentity $userIdentity = null ) { $defaultOptions = $this->getDefaultOptions( $userIdentity ); return $defaultOptions[$opt] ?? null; } /** * Get the user's current setting for a given option. * * @param UserIdentity $user The user to get the option for * @param string $oname The option to check * @param mixed|null $defaultOverride A default value returned if the option does not exist * @param bool $ignoreHidden Whether to ignore the effects of $wgHiddenPrefs * @param int $queryFlags A bit field composed of READ_XXX flags * @return mixed|null User's current value for the option, * Note that while option values retrieved from the database are always strings, default * values and values set within the current request and not yet saved may be of another type. * @see getBoolOption() * @see getIntOption() */ abstract public function getOption( UserIdentity $user, string $oname, $defaultOverride = null, bool $ignoreHidden = false, int $queryFlags = IDBAccessObject::READ_NORMAL ); /** * Get all user's options * * @param UserIdentity $user The user to get the option for * @param int $flags Bitwise combination of: * UserOptionsManager::EXCLUDE_DEFAULTS Exclude user options that are set * to the default value. Options * that are set to their conditionally * default value are not excluded. * @param int $queryFlags A bit field composed of READ_XXX flags * @return array */ abstract public function getOptions( UserIdentity $user, int $flags = 0, int $queryFlags = IDBAccessObject::READ_NORMAL ): array; /** * Get the user's current setting for a given option, as a boolean value. * * @param UserIdentity $user The user to get the option for * @param string $oname The option to check * @param int $queryFlags A bit field composed of READ_XXX flags * @return bool User's current value for the option * @see getOption() */ public function getBoolOption( UserIdentity $user, string $oname, int $queryFlags = IDBAccessObject::READ_NORMAL ): bool { return (bool)$this->getOption( $user, $oname, null, false, $queryFlags ); } /** * Get the user's current setting for a given option, as an integer value. * * @param UserIdentity $user The user to get the option for * @param string $oname The option to check * @param int $defaultOverride A default value returned if the option does not exist * @param int $queryFlags A bit field composed of READ_XXX flags * @return int User's current value for the option * @see getOption() */ public function getIntOption( UserIdentity $user, string $oname, int $defaultOverride = 0, int $queryFlags = IDBAccessObject::READ_NORMAL ): int { $val = $this->getOption( $user, $oname, $defaultOverride, false, $queryFlags ); if ( $val == '' ) { $val = $defaultOverride; } return intval( $val ); } /** * Determine if a user option came from a source other than the local store * or the defaults. If this is true, setting the option will be ignored * unless GLOBAL_OVERRIDE or GLOBAL_UPDATE is passed to setOption(). * * @param UserIdentity $user * @param string $key * @return bool */ public function isOptionGlobal( UserIdentity $user, string $key ) { return false; } } /** @deprecated class alias since 1.42 */ class_alias( UserOptionsLookup::class, 'MediaWiki\\User\\UserOptionsLookup' ); PK ! }q�\ \ Options/UserOptionsManager.phpnu �Iw�� <?php /** * 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 */ namespace MediaWiki\User\Options; use InvalidArgumentException; use MediaWiki\Config\ServiceOptions; use MediaWiki\Context\IContextSource; use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; use MediaWiki\Language\LanguageCode; use MediaWiki\Language\LanguageConverter; use MediaWiki\Languages\LanguageConverterFactory; use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; use MediaWiki\User\UserFactory; use MediaWiki\User\UserIdentity; use MediaWiki\User\UserNameUtils; use MediaWiki\User\UserTimeCorrection; use Psr\Log\LoggerInterface; use Wikimedia\ObjectFactory\ObjectFactory; use Wikimedia\Rdbms\IConnectionProvider; use Wikimedia\Rdbms\IDBAccessObject; /** * A service class to control user options * @since 1.35 */ class UserOptionsManager extends UserOptionsLookup { /** * @internal For use by ServiceWiring */ public const CONSTRUCTOR_OPTIONS = [ MainConfigNames::HiddenPrefs, MainConfigNames::LocalTZoffset, ]; /** * @since 1.39.5, 1.40 */ public const MAX_BYTES_OPTION_VALUE = 65530; /** * If the option was set globally, ignore the update. * @since 1.43 */ public const GLOBAL_IGNORE = 'ignore'; /** * If the option was set globally, add a local override. * @since 1.43 */ public const GLOBAL_OVERRIDE = 'override'; /** * If the option was set globally, update the global value. * @since 1.43 */ public const GLOBAL_UPDATE = 'update'; private const LOCAL_STORE_KEY = 'local'; private ServiceOptions $serviceOptions; private DefaultOptionsLookup $defaultOptionsLookup; private LanguageConverterFactory $languageConverterFactory; private IConnectionProvider $dbProvider; private UserFactory $userFactory; private LoggerInterface $logger; private HookRunner $hookRunner; private UserNameUtils $userNameUtils; private array $storeProviders; private ObjectFactory $objectFactory; /** @var UserOptionsCacheEntry[] */ private $cache = []; /** @var UserOptionsStore[]|null */ private $stores; /** * @param ServiceOptions $options * @param DefaultOptionsLookup $defaultOptionsLookup * @param LanguageConverterFactory $languageConverterFactory * @param IConnectionProvider $dbProvider * @param LoggerInterface $logger * @param HookContainer $hookContainer * @param UserFactory $userFactory * @param UserNameUtils $userNameUtils * @param ObjectFactory $objectFactory * @param array $storeProviders */ public function __construct( ServiceOptions $options, DefaultOptionsLookup $defaultOptionsLookup, LanguageConverterFactory $languageConverterFactory, IConnectionProvider $dbProvider, LoggerInterface $logger, HookContainer $hookContainer, UserFactory $userFactory, UserNameUtils $userNameUtils, ObjectFactory $objectFactory, array $storeProviders ) { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->serviceOptions = $options; $this->defaultOptionsLookup = $defaultOptionsLookup; $this->languageConverterFactory = $languageConverterFactory; $this->dbProvider = $dbProvider; $this->logger = $logger; $this->hookRunner = new HookRunner( $hookContainer ); $this->userFactory = $userFactory; $this->userNameUtils = $userNameUtils; $this->objectFactory = $objectFactory; $this->storeProviders = $storeProviders; } /** * @inheritDoc */ public function getDefaultOptions( ?UserIdentity $userIdentity = null ): array { return $this->defaultOptionsLookup->getDefaultOptions( $userIdentity ); } /** * @inheritDoc */ public function getDefaultOption( string $opt, ?UserIdentity $userIdentity = null ) { return $this->defaultOptionsLookup->getDefaultOption( $opt, $userIdentity ); } /** * @inheritDoc */ public function getOption( UserIdentity $user, string $oname, $defaultOverride = null, bool $ignoreHidden = false, int $queryFlags = IDBAccessObject::READ_NORMAL ) { # We want 'disabled' preferences to always behave as the default value for # users, even if they have set the option explicitly in their settings (ie they # set it, and then it was disabled removing their ability to change it). But # we don't want to erase the preferences in the database in case the preference # is re-enabled again. So don't touch $mOptions, just override the returned value if ( !$ignoreHidden && in_array( $oname, $this->serviceOptions->get( MainConfigNames::HiddenPrefs ) ) ) { return $this->defaultOptionsLookup->getDefaultOption( $oname, $user ); } $options = $this->loadUserOptions( $user, $queryFlags ); if ( array_key_exists( $oname, $options ) ) { return $options[$oname]; } return $defaultOverride; } /** * @inheritDoc */ public function getOptions( UserIdentity $user, int $flags = 0, int $queryFlags = IDBAccessObject::READ_NORMAL ): array { $options = $this->loadUserOptions( $user, $queryFlags ); # We want 'disabled' preferences to always behave as the default value for # users, even if they have set the option explicitly in their settings (ie they # set it, and then it was disabled removing their ability to change it). But # we don't want to erase the preferences in the database in case the preference # is re-enabled again. So don't touch $mOptions, just override the returned value foreach ( $this->serviceOptions->get( MainConfigNames::HiddenPrefs ) as $pref ) { $default = $this->defaultOptionsLookup->getDefaultOption( $pref, $user ); if ( $default !== null ) { $options[$pref] = $default; } } if ( $flags & self::EXCLUDE_DEFAULTS ) { // NOTE: This intentionally ignores conditional defaults, so that `mw.user.options` // work correctly for options with conditional defaults. $defaultOptions = $this->defaultOptionsLookup->getDefaultOptions( null ); foreach ( $options as $option => $value ) { if ( array_key_exists( $option, $defaultOptions ) && self::isValueEqual( $value, $defaultOptions[$option] ) ) { unset( $options[$option] ); } } } return $options; } public function isOptionGlobal( UserIdentity $user, string $key ) { $this->getOptions( $user ); $source = $this->cache[ $this->getCacheKey( $user ) ]->sources[$key] ?? self::LOCAL_STORE_KEY; return $source !== self::LOCAL_STORE_KEY; } /** * Set the given option for a user. * * You need to call saveOptions() to actually write to the database. * * $val should be null or a string. Other types are accepted for B/C with legacy * code but can result in surprising behavior and are discouraged. Values are always * stored as strings in the database, so if you pass a non-string value, it will be * eventually converted; but before the call to saveOptions(), getOption() will return * the passed value from instance cache without any type conversion. * * A null value means resetting the option to its default value (removing the user_properties * row). Passing in the same value as the default value fo the user has the same result. * This behavior supports some level of type juggling - e.g. if the default value is 1, * and you pass in '1', the option will be reset to its default value. * * When an option is reset to its default value, that means whenever the default value * is changed in the site configuration, the user preference for this user will also change. * There is no way to set a user preference to be the same as the default but avoid it * changing when the default changes. You can instead use $wgConditionalUserOptions to * split the default based on user registration date. * * If a global user option exists with the given name, the behaviour depends on the value * of $global. * * @param UserIdentity $user * @param string $oname The option to set * @param mixed $val New value to set. * @param string $global Since 1.43. What to do if the option was set * globally using the GlobalPreferences extension. One of the * self::GLOBAL_* constants: * - GLOBAL_IGNORE: Do nothing. The option remains with its previous value. * - GLOBAL_OVERRIDE: Add a local override. * - GLOBAL_UPDATE: Update the option globally. * The UI should typically ask for the user's consent before setting a global * option. */ public function setOption( UserIdentity $user, string $oname, $val, $global = self::GLOBAL_IGNORE ) { // Explicitly NULL values should refer to defaults $val ??= $this->defaultOptionsLookup->getDefaultOption( $oname, $user ); $userKey = $this->getCacheKey( $user ); $info = $this->cache[$userKey] ??= new UserOptionsCacheEntry; $info->modifiedValues[$oname] = $val; $info->globalUpdateActions[$oname] = $global; } /** * Reset certain (or all) options to the site defaults * * The optional parameter determines which kinds of preferences will be reset. * Supported values are everything that can be reported by getOptionKinds() * and 'all', which forces a reset of *all* preferences and overrides everything else. * * @note You need to call saveOptions() to actually write to the database. * * @deprecated since 1.43 use resetOptionsByName() with PreferencesFactory::getOptionNamesForReset() * * @param UserIdentity $user * @param IContextSource $context Context source used when $resetKinds does not contain 'all'. * @param array|string $resetKinds Which kinds of preferences to reset. * Defaults to [ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ] */ public function resetOptions( UserIdentity $user, IContextSource $context, $resetKinds = [ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ] ) { wfDeprecated( __METHOD__, '1.43' ); $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory(); $optionsToReset = $preferencesFactory->getOptionNamesForReset( $this->userFactory->newFromUserIdentity( $user ), $context, $resetKinds ); $this->resetOptionsByName( $user, $optionsToReset ); } /** * Reset a list of options to the site defaults * * @note You need to call saveOptions() to actually write to the database. * * @param UserIdentity $user * @param string[] $optionNames */ public function resetOptionsByName( UserIdentity $user, array $optionNames ) { foreach ( $optionNames as $name ) { $this->setOption( $user, $name, null ); } } /** * Reset all options that were set to a non-default value by the given user * * @note You need to call saveOptions() to actually write to the database. * * @param UserIdentity $user */ public function resetAllOptions( UserIdentity $user ) { foreach ( $this->loadUserOptions( $user ) as $name => $value ) { $this->setOption( $user, $name, null ); } } /** * @deprecated since 1.43 use PreferencesFactory::listResetKinds() * * @return string[] Option kinds */ public function listOptionKinds(): array { wfDeprecated( __METHOD__, '1.43' ); $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory(); return $preferencesFactory->listResetKinds(); } /** * @deprecated since 1.43 use PreferencesFactory::getResetKinds * * @param UserIdentity $userIdentity * @param IContextSource $context * @param array|null $options * @return string[] */ public function getOptionKinds( UserIdentity $userIdentity, IContextSource $context, $options = null ): array { wfDeprecated( __METHOD__, '1.43' ); $user = $this->userFactory->newFromUserIdentity( $userIdentity ); $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory(); return $preferencesFactory->getResetKinds( $user, $context, $options ); } /** * Saves the non-default options for this user, as previously set e.g. via * setOption(), in the database's "user_properties" (preferences) table. * * @since 1.38, this method was internal before that. * @param UserIdentity $user */ public function saveOptions( UserIdentity $user ) { $dbw = $this->dbProvider->getPrimaryDatabase(); $changed = $this->saveOptionsInternal( $user ); $legacyUser = $this->userFactory->newFromUserIdentity( $user ); // Before UserOptionsManager, User::saveSettings was used for user options // saving. Some extensions might depend on UserSaveSettings hook being run // when options are saved, so run this hook for legacy reasons. // Once UserSaveSettings hook is deprecated and replaced with a different hook // with more modern interface, extensions should use 'SaveUserOptions' hook. $this->hookRunner->onUserSaveSettings( $legacyUser ); if ( $changed ) { $dbw->onTransactionCommitOrIdle( static function () use ( $legacyUser ) { $legacyUser->checkAndSetTouched(); }, __METHOD__ ); } } /** * Saves the non-default options for this user, as previously set e.g. via * setOption(), in the database's "user_properties" (preferences) table. * * @param UserIdentity $user * @return bool true if options were changed and new options successfully saved. * @internal only public for use in User::saveSettings */ public function saveOptionsInternal( UserIdentity $user ): bool { if ( $this->userNameUtils->isIP( $user->getName() ) || $this->userNameUtils->isTemp( $user->getName() ) ) { throw new InvalidArgumentException( __METHOD__ . ' was called on IP or temporary user' ); } $userKey = $this->getCacheKey( $user ); $cache = $this->cache[$userKey] ?? new UserOptionsCacheEntry; $modifiedOptions = $cache->modifiedValues; // FIXME: should probably use READ_LATEST here $originalOptions = $this->loadOriginalOptions( $user ); if ( !$this->hookRunner->onSaveUserOptions( $user, $modifiedOptions, $originalOptions ) ) { return false; } $updatesByStore = []; foreach ( $modifiedOptions as $key => $value ) { // Don't store unchanged or default values $defaultValue = $this->defaultOptionsLookup->getDefaultOption( $key, $user ); if ( $value === null || self::isValueEqual( $value, $defaultValue ) ) { $valOrNull = null; } else { $valOrNull = (string)$value; } $source = $cache->sources[$key] ?? self::LOCAL_STORE_KEY; if ( $source === self::LOCAL_STORE_KEY ) { $updatesByStore[self::LOCAL_STORE_KEY][$key] = $valOrNull; } else { $updateAction = $cache->globalUpdateActions[$key] ?? self::GLOBAL_IGNORE; if ( $updateAction === self::GLOBAL_UPDATE ) { $updatesByStore[$source][$key] = $valOrNull; } elseif ( $updateAction === self::GLOBAL_OVERRIDE ) { $updatesByStore[self::LOCAL_STORE_KEY][$key] = $valOrNull; $updatesByStore[self::LOCAL_STORE_KEY][$key . self::LOCAL_EXCEPTION_SUFFIX] = '1'; } } } $changed = false; $stores = $this->getStores(); foreach ( $updatesByStore as $source => $updates ) { $changed = $stores[$source]->store( $user, $updates ) || $changed; } if ( !$changed ) { return false; } // Clear the cache and the update queue unset( $this->cache[$userKey] ); return true; } /** * Loads user options either from cache or from the database. * * @note Query flags are ignored for anons, since they do not have any * options stored in the database. If the UserIdentity was itself * obtained from a replica and doesn't have ID set due to replication lag, * it will be treated as anon regardless of the query flags passed here. * * @internal * * @param UserIdentity $user * @param int $queryFlags * @return array */ public function loadUserOptions( UserIdentity $user, int $queryFlags = IDBAccessObject::READ_NORMAL ): array { $userKey = $this->getCacheKey( $user ); $originalOptions = $this->loadOriginalOptions( $user, $queryFlags ); $cache = $this->cache[$userKey] ?? null; if ( $cache ) { return array_merge( $originalOptions, $cache->modifiedValues ); } else { return $originalOptions; } } /** * Clears cached user options. * @internal To be used by User::clearInstanceCache * @param UserIdentity $user */ public function clearUserOptionsCache( UserIdentity $user ) { unset( $this->cache[ $this->getCacheKey( $user ) ] ); } /** * Fetches the options directly from the database with no caches. * * @param UserIdentity $user * @param int $queryFlags a bit field composed of READ_XXX flags * @return array */ private function loadOptionsFromStore( UserIdentity $user, int $queryFlags ): array { $this->logger->debug( 'Loading options from database', [ 'user_id' => $user->getId(), 'user_name' => $user->getName() ] ); $mergedOptions = []; $cache = $this->cache[ $this->getCacheKey( $user ) ] ??= new UserOptionsCacheEntry; foreach ( $this->getStores() as $storeName => $store ) { $options = $store->fetch( $user, $queryFlags ); foreach ( $options as $name => $value ) { // Handle a local exception which is the default if ( str_ends_with( $name, self::LOCAL_EXCEPTION_SUFFIX ) && $value ) { $baseName = substr( $name, 0, -strlen( self::LOCAL_EXCEPTION_SUFFIX ) ); if ( !isset( $options[$baseName] ) ) { // T368595: The source should always be set to local for local exceptions $cache->sources[$baseName] = self::LOCAL_STORE_KEY; unset( $mergedOptions[$baseName] ); } } // Handle a non-default option or non-default local exception if ( !isset( $mergedOptions[$name] ) || !empty( $options[$name . self::LOCAL_EXCEPTION_SUFFIX] ) ) { $cache->sources[$name] = $storeName; $mergedOptions[$name] = $this->normalizeValueType( $value ); } } } return $mergedOptions; } /** * Convert '0' to 0. PHP's boolean conversion considers them both * false, but e.g. JavaScript considers the former as true. * * @todo T54542 Somehow determine the desired type (string/int/bool) * and convert all values here. * * @param string $value * @return mixed */ private function normalizeValueType( $value ) { if ( $value === '0' ) { $value = 0; } return $value; } /** * Loads the original user options from the database and applies various transforms, * like timecorrection. Runs hooks. * * @param UserIdentity $user * @param int $queryFlags * @return array */ private function loadOriginalOptions( UserIdentity $user, int $queryFlags = IDBAccessObject::READ_NORMAL ): array { $userKey = $this->getCacheKey( $user ); $defaultOptions = $this->defaultOptionsLookup->getDefaultOptions( $user ); $cache = $this->cache[$userKey] ??= new UserOptionsCacheEntry; if ( $this->userNameUtils->isIP( $user->getName() ) || $this->userNameUtils->isTemp( $user->getName() ) ) { // For unlogged-in users, load language/variant options from request. // There's no need to do it for logged-in users: they can set preferences, // and handling of page content is done by $pageLang->getPreferredVariant() and such, // so don't override user's choice (especially when the user chooses site default). $variant = $this->languageConverterFactory->getLanguageConverter()->getDefaultVariant(); $defaultOptions['variant'] = $variant; $defaultOptions['language'] = $variant; $cache->originalValues = $defaultOptions; return $defaultOptions; } // In case options were already loaded from the database before and no options // changes were saved to the database, we can use the cached original options. if ( $cache->canUseCachedValues( $queryFlags ) && $cache->originalValues !== null ) { return $cache->originalValues; } $options = $this->loadOptionsFromStore( $user, $queryFlags ) + $defaultOptions; // Replace deprecated language codes $options['language'] = LanguageCode::replaceDeprecatedCodes( $options['language'] ); $options['variant'] = LanguageCode::replaceDeprecatedCodes( $options['variant'] ); foreach ( LanguageConverter::$languagesWithVariants as $langCode ) { $variant = "variant-$langCode"; if ( isset( $options[$variant] ) ) { $options[$variant] = LanguageCode::replaceDeprecatedCodes( $options[$variant] ); } } // Fix up timezone offset (Due to DST it can change from what was stored in the DB) // ZoneInfo|offset|TimeZoneName if ( isset( $options['timecorrection'] ) ) { $options['timecorrection'] = ( new UserTimeCorrection( $options['timecorrection'], null, $this->serviceOptions->get( MainConfigNames::LocalTZoffset ) ) )->toString(); } // Need to store what we have so far before the hook to prevent // infinite recursion if the hook attempts to reload options $cache->originalValues = $options; $cache->recency = $queryFlags; $this->hookRunner->onLoadUserOptions( $user, $options ); $cache->originalValues = $options; return $options; } /** * Get a cache key for a user * @param UserIdentity $user * @return string */ private function getCacheKey( UserIdentity $user ): string { $name = $user->getName(); if ( $this->userNameUtils->isIP( $name ) || $this->userNameUtils->isTemp( $name ) ) { // IP and temporary users may not have custom preferences, so they can share a key return 'anon'; } elseif ( $user->isRegistered() ) { return "u:{$user->getId()}"; } else { // Allow users with no local account to have preferences provided by alternative // UserOptionsStore implementations (e.g. in GlobalPreferences) $canonical = $this->userNameUtils->getCanonical( $name ) ?: $name; return "a:$canonical"; } } /** * Determines whether two values are sufficiently similar that the database * does not need to be updated to reflect the change. This is basically the * same as comparing the result of Database::addQuotes(). * * @since 1.43 * * @param mixed $a * @param mixed $b * @return bool */ public static function isValueEqual( $a, $b ) { // null is only equal to another null (T355086) if ( $a === null || $b === null ) { return $a === $b; } if ( is_bool( $a ) ) { $a = (int)$a; } if ( is_bool( $b ) ) { $b = (int)$b; } return (string)$a === (string)$b; } /** * Get the storage backends in descending order of priority * * @return UserOptionsStore[] */ private function getStores() { if ( !$this->stores ) { $stores = [ self::LOCAL_STORE_KEY => new LocalUserOptionsStore( $this->dbProvider ) ]; foreach ( $this->storeProviders as $name => $spec ) { $store = $this->objectFactory->createObject( $spec ); if ( !$store instanceof UserOptionsStore ) { throw new \RuntimeException( "Invalid type for extension store \"$name\"" ); } $stores[$name] = $store; } // Query global providers first, preserve keys $this->stores = array_reverse( $stores, true ); } return $this->stores; } } /** @deprecated class alias since 1.42 */ class_alias( UserOptionsManager::class, 'MediaWiki\\User\\UserOptionsManager' ); PK ! �P�) ) ! Options/LocalUserOptionsStore.phpnu �Iw�� <?php namespace MediaWiki\User\Options; use MediaWiki\User\UserIdentity; use Wikimedia\Rdbms\DBAccessObjectUtils; use Wikimedia\Rdbms\IConnectionProvider; use Wikimedia\Rdbms\IDBAccessObject; class LocalUserOptionsStore implements UserOptionsStore { private IConnectionProvider $dbProvider; /** @var array[] Cached options for each user, by user ID */ private $optionsFromDb; public function __construct( IConnectionProvider $dbProvider ) { $this->dbProvider = $dbProvider; } public function fetch( UserIdentity $user, int $recency ): array { // In core, only users with local accounts may have preferences if ( !$user->getId() ) { return []; } $dbr = DBAccessObjectUtils::getDBFromRecency( $this->dbProvider, $recency ); $res = $dbr->newSelectQueryBuilder() ->select( [ 'up_property', 'up_value' ] ) ->from( 'user_properties' ) ->where( [ 'up_user' => $user->getId() ] ) ->recency( $recency ) ->caller( __METHOD__ )->fetchResultSet(); $options = []; foreach ( $res as $row ) { $options[$row->up_property] = (string)$row->up_value; } $this->optionsFromDb[$user->getId()] = $options; return $options; } public function store( UserIdentity $user, array $updates ) { // In core, only users with local accounts may have preferences if ( !$user->getId() ) { return false; } $oldOptions = $this->optionsFromDb[ $user->getId() ] ?? $this->fetch( $user, IDBAccessObject::READ_LATEST ); $newOptions = $oldOptions; $keysToDelete = []; $rowsToInsert = []; foreach ( $updates as $key => $value ) { if ( !UserOptionsManager::isValueEqual( $value, $oldOptions[$key] ?? null ) ) { // Update by deleting and reinserting if ( array_key_exists( $key, $oldOptions ) ) { $keysToDelete[] = $key; unset( $newOptions[$key] ); } if ( $value !== null ) { $truncValue = mb_strcut( $value, 0, UserOptionsManager::MAX_BYTES_OPTION_VALUE ); $rowsToInsert[] = [ 'up_user' => $user->getId(), 'up_property' => $key, 'up_value' => $truncValue, ]; $newOptions[$key] = $truncValue; } } } if ( !count( $keysToDelete ) && !count( $rowsToInsert ) ) { // Nothing to do return false; } // Do the DELETE $dbw = $this->dbProvider->getPrimaryDatabase(); if ( $keysToDelete ) { $dbw->newDeleteQueryBuilder() ->deleteFrom( 'user_properties' ) ->where( [ 'up_user' => $user->getId() ] ) ->andWhere( [ 'up_property' => $keysToDelete ] ) ->caller( __METHOD__ )->execute(); } if ( $rowsToInsert ) { // Insert the new preference rows $dbw->newInsertQueryBuilder() ->insertInto( 'user_properties' ) ->ignore() ->rows( $rowsToInsert ) ->caller( __METHOD__ )->execute(); } // Update cache $this->optionsFromDb[$user->getId()] = $newOptions; return true; } } PK ! �^4� � # Options/StaticUserOptionsLookup.phpnu �Iw�� <?php namespace MediaWiki\User\Options; use MediaWiki\User\UserIdentity; use Wikimedia\Rdbms\IDBAccessObject; /** * A UserOptionsLookup that's just an array. Useful for testing and creating staging environments. * Note that unlike UserOptionsManager, no attempt is made to canonicalize user names. * @since 1.36 */ class StaticUserOptionsLookup extends UserOptionsLookup { /** @var array[] */ private $userMap; /** @var mixed[] */ private $defaults; /** * @param array[] $userMap User options, username => [ option name => value ] * @param mixed[] $defaults Defaults for each option, option name => value */ public function __construct( array $userMap, array $defaults = [] ) { $this->userMap = $userMap; $this->defaults = $defaults; } /** @inheritDoc */ public function getDefaultOptions( ?UserIdentity $userIdentity = null ): array { return $this->defaults; } /** @inheritDoc */ public function getOption( UserIdentity $user, string $oname, $defaultOverride = null, bool $ignoreHidden = false, int $queryFlags = IDBAccessObject::READ_NORMAL ) { $userOptions = $this->getOptions( $user ); if ( array_key_exists( $oname, $userOptions ) ) { return $userOptions[$oname]; } else { return $defaultOverride; } } /** @inheritDoc */ public function getOptions( UserIdentity $user, int $flags = 0, int $queryFlags = IDBAccessObject::READ_NORMAL ): array { $userOptions = []; if ( $user->isRegistered() ) { $userOptions = $this->userMap[$user->getName()] ?? []; } if ( !( $flags & self::EXCLUDE_DEFAULTS ) ) { $userOptions += $this->defaults; } return $userOptions; } } /** @deprecated class alias since 1.42 */ class_alias( StaticUserOptionsLookup::class, 'MediaWiki\User\StaticUserOptionsLookup' ); PK ! 5ou�m m ! Options/UserOptionsCacheEntry.phpnu �Iw�� <?php namespace MediaWiki\User\Options; use Wikimedia\Rdbms\IDBAccessObject; /** * @internal */ class UserOptionsCacheEntry { /** * @var array<string,mixed> Values modified by setOption(), queued for update */ public $modifiedValues = []; /** * @var array<string,string> The source name for each value in $originalValues */ public $sources = []; /** * @var array<string,string> The value of the $global parameter to setOption() */ public $globalUpdateActions = []; /** * @var array<string,string>|null Cached original user options with all the * adjustments like time correction and hook changes applied. Ready to be * returned. Null if the original values have not been loaded */ public $originalValues; /** @var int|null Query flags used to retrieve options from database */ public $recency; /** * Determine if it's OK to use cached options values for a given user and query flags * * @param int $recency * @return bool */ public function canUseCachedValues( $recency ) { $recencyUsed = $this->recency ?? IDBAccessObject::READ_NONE; return $recencyUsed >= $recency; } } PK ! "Gֵ� � $ Options/Hook/LoadUserOptionsHook.phpnu �Iw�� <?php namespace MediaWiki\User\Options\Hook; use MediaWiki\User\UserIdentity; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "LoadUserOptions" to register handlers implementing this interface. * * @stable to implement * @ingroup Hooks */ interface LoadUserOptionsHook { /** * This hook is called when user options/preferences are being loaded from the database. * * @since 1.37 * * @param UserIdentity $user * @param array &$options Options, can be modified. * @return void This hook must not abort, it must return no value */ public function onLoadUserOptions( UserIdentity $user, array &$options ): void; } PK ! .w�}a a $ Options/Hook/SaveUserOptionsHook.phpnu �Iw�� <?php namespace MediaWiki\User\Options\Hook; use MediaWiki\User\UserIdentity; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "SaveUserOptions" to register handlers implementing this interface. * * @stable to implement * @ingroup Hooks */ interface SaveUserOptionsHook { /** * This hook is called just before saving user preferences. * * Hook handlers can either add or manipulate options, or reset one back to its default * to block changing it. Hook handlers are also allowed to abort the process by returning * false, e.g. to save to a global profile instead. Compare to the UserSaveSettings * hook, which is called after the preferences have been saved. * * @since 1.37 * * @param UserIdentity $user The user for which the options are going to be saved * @param array &$modifiedOptions The user's options as an associative array, modifiable. * To reset the preference value to default, set the preference to null. * To block the preference from changing, unset the key from the array. * To modify a preference value, set a new value. * @param array $originalOptions The user's original options being replaced * @return bool|void True or no return value to continue or false to abort */ public function onSaveUserOptions( UserIdentity $user, array &$modifiedOptions, array $originalOptions ); } PK ! ��7 7 : Options/Hook/ConditionalDefaultOptionsAddConditionHook.phpnu �Iw�� <?php namespace MediaWiki\User\Options\Hook; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "ConditionalDefaultOptionsAddCondition" to register handlers implementing * this interface. It runs virtually on any request, although it should run once, performance of * the hook run should be taken in account. * * @stable to implement * @ingroup Hooks */ interface ConditionalDefaultOptionsAddConditionHook { /** * This hook is called when ConditionalDefaultsLookup service is created. * Important: $extraConditions must be added and used in a property defined by the same extension. Using * a condition from a missing extension will result in fatal error. * See also Manual:$wgConditionalUserOptions. * * @since 1.43 * * @param array<string,callable> &$extraConditions An empty array to add checker functions to * evaluate conditions set in $wgConditionalUserOptions. The key is the name of the condition * and the value a callable which will take two arguments: * - UserIdentity $user: the user which option is being evaluated * - array $args: the rest of arguments in the relevant condition configuration * @return void This hook must not abort, it must return no value */ public function onConditionalDefaultOptionsAddCondition( array &$extraConditions ): void; } PK ! }+ �, , Options/DefaultOptionsLookup.phpnu �Iw�� <?php /** * 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 */ namespace MediaWiki\User\Options; use MediaWiki\Config\ServiceOptions; use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; use MediaWiki\Language\LanguageCode; use MediaWiki\Language\LanguageConverter; use MediaWiki\MainConfigNames; use MediaWiki\Title\NamespaceInfo; use MediaWiki\User\UserIdentity; use Skin; use Wikimedia\Assert\Assert; use Wikimedia\Rdbms\IDBAccessObject; /** * A service class to control default user options * @since 1.35 */ class DefaultOptionsLookup extends UserOptionsLookup { /** * @internal For use by ServiceWiring */ public const CONSTRUCTOR_OPTIONS = [ MainConfigNames::DefaultSkin, MainConfigNames::DefaultUserOptions, MainConfigNames::NamespacesToBeSearchedDefault ]; private ServiceOptions $serviceOptions; private LanguageCode $contentLang; private NamespaceInfo $nsInfo; private ConditionalDefaultsLookup $conditionalDefaultsLookup; /** @var array|null Cached default options */ private $defaultOptions = null; private HookRunner $hookRunner; /** * @param ServiceOptions $options * @param LanguageCode $contentLang * @param HookContainer $hookContainer * @param NamespaceInfo $nsInfo * @param ConditionalDefaultsLookup $conditionalUserOptionsDefaultsLookup */ public function __construct( ServiceOptions $options, LanguageCode $contentLang, HookContainer $hookContainer, NamespaceInfo $nsInfo, ConditionalDefaultsLookup $conditionalUserOptionsDefaultsLookup ) { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->serviceOptions = $options; $this->contentLang = $contentLang; $this->hookRunner = new HookRunner( $hookContainer ); $this->nsInfo = $nsInfo; $this->conditionalDefaultsLookup = $conditionalUserOptionsDefaultsLookup; } /** * Get default user options from $wgDefaultUserOptions (ignoring any conditional defaults) * * @return array */ private function getGenericDefaultOptions(): array { if ( $this->defaultOptions !== null ) { return $this->defaultOptions; } $this->defaultOptions = $this->serviceOptions->get( MainConfigNames::DefaultUserOptions ); // Default language setting // NOTE: don't use the content language code since the static default variant would // NOT always be the same as the content language code. $contentLangCode = $this->contentLang->toString(); $LangsWithStaticDefaultVariant = LanguageConverter::$languagesWithStaticDefaultVariant; $staticDefaultVariant = $LangsWithStaticDefaultVariant[$contentLangCode] ?? $contentLangCode; $this->defaultOptions['language'] = $contentLangCode; $this->defaultOptions['variant'] = $staticDefaultVariant; foreach ( LanguageConverter::$languagesWithVariants as $langCode ) { $staticDefaultVariant = $LangsWithStaticDefaultVariant[$langCode] ?? $langCode; $this->defaultOptions["variant-$langCode"] = $staticDefaultVariant; } // NOTE: don't use SearchEngineConfig::getSearchableNamespaces here, // since extensions may change the set of searchable namespaces depending // on user groups/permissions. $nsSearchDefault = $this->serviceOptions->get( MainConfigNames::NamespacesToBeSearchedDefault ); foreach ( $this->nsInfo->getValidNamespaces() as $n ) { $this->defaultOptions['searchNs' . $n] = ( $nsSearchDefault[$n] ?? false ) ? 1 : 0; } $this->defaultOptions['skin'] = Skin::normalizeKey( $this->serviceOptions->get( MainConfigNames::DefaultSkin ) ); $this->hookRunner->onUserGetDefaultOptions( $this->defaultOptions ); return $this->defaultOptions; } /** * @inheritDoc */ public function getDefaultOptions( ?UserIdentity $userIdentity = null ): array { $defaultOptions = $this->getGenericDefaultOptions(); // If requested, process any conditional defaults if ( $userIdentity ) { $conditionallyDefaultOptions = $this->conditionalDefaultsLookup->getConditionallyDefaultOptions(); foreach ( $conditionallyDefaultOptions as $optionName ) { $conditionalDefault = $this->conditionalDefaultsLookup->getOptionDefaultForUser( $optionName, $userIdentity ); if ( $conditionalDefault !== null ) { $defaultOptions[$optionName] = $conditionalDefault; } } } return $defaultOptions; } /** * @inheritDoc */ public function getOption( UserIdentity $user, string $oname, $defaultOverride = null, bool $ignoreHidden = false, int $queryFlags = IDBAccessObject::READ_NORMAL ) { $this->verifyUsable( $user, __METHOD__ ); return $this->getDefaultOption( $oname ) ?? $defaultOverride; } /** * @inheritDoc */ public function getOptions( UserIdentity $user, int $flags = 0, int $queryFlags = IDBAccessObject::READ_NORMAL ): array { $this->verifyUsable( $user, __METHOD__ ); if ( $flags & self::EXCLUDE_DEFAULTS ) { return []; } return $this->getDefaultOptions(); } /** * Checks if the DefaultOptionsLookup is usable as an instance of UserOptionsLookup. * * It only makes sense in an installer context when UserOptionsManager cannot be yet instantiated * as the database is not available. Thus, this can only be called for an anon user, * calling under different circumstances indicates a bug, or that a system user is being used. * * The only exception to this is database-less PHPUnit tests, where sometimes fake registered users are * used and end up being passed to this class. This should not be considered a bug, and using the default * preferences in this scenario is probably the intended behaviour. * * @param UserIdentity $user * @param string $fname */ private function verifyUsable( UserIdentity $user, string $fname ) { if ( defined( 'MEDIAWIKI_INSTALL' ) ) { return; } Assert::precondition( !$user->isRegistered(), "$fname called on a registered user" ); } } /** @deprecated class alias since 1.42 */ class_alias( DefaultOptionsLookup::class, 'MediaWiki\\User\\DefaultOptionsLookup' ); PK ! pUE�P P Options/UserOptionsStore.phpnu �Iw�� <?php namespace MediaWiki\User\Options; use MediaWiki\User\UserIdentity; /** * @since 1.43 * @stable to implement */ interface UserOptionsStore { /** * Fetch all options for a given user from the store. * * Note that OptionsStore does not handle fallback to default. Options are * either present or absent. * * @param UserIdentity $user A user with a non-zero ID * @param int $recency a bit field composed of READ_XXX flags * @return array<string,string> */ public function fetch( UserIdentity $user, int $recency ); /** * Process a batch of option updates. * * The store may assume that fetch() was previously called with a recency * sufficient to provide reference values for a differential update. It is * the caller's responsibility to manage recency. * * Note that OptionsStore does not have a concept of defaults. The store is * not required to check whether the value matches the default. * * @param UserIdentity $user A user with a non-zero ID * @param array<string,string|null> $updates A map of option names to new * values. If the value is null, the key should be deleted from the store * and subsequently not returned from fetch(). Absent keys should be left * unchanged. * @return bool Whether any change was made */ public function store( UserIdentity $user, array $updates ); } PK ! |��� � % Options/ConditionalDefaultsLookup.phpnu �Iw�� <?php namespace MediaWiki\User\Options; use InvalidArgumentException; use MediaWiki\Config\ServiceOptions; use MediaWiki\MainConfigNames; use MediaWiki\User\Registration\UserRegistrationLookup; use MediaWiki\User\UserGroupManager; use MediaWiki\User\UserIdentity; use MediaWiki\User\UserIdentityUtils; use Wikimedia\Timestamp\ConvertibleTimestamp; class ConditionalDefaultsLookup { /** * @internal Exposed for ServiceWiring only */ public const CONSTRUCTOR_OPTIONS = [ MainConfigNames::ConditionalUserOptions, ]; private ServiceOptions $options; private UserRegistrationLookup $userRegistrationLookup; private UserIdentityUtils $userIdentityUtils; /** * UserGroupManager must be provided as a callback function to avoid circular dependency * @var callable */ private $userGroupManagerCallback; private array $extraConditions; public function __construct( ServiceOptions $options, UserRegistrationLookup $userRegistrationLookup, UserIdentityUtils $userIdentityUtils, callable $userGroupManagerCallback, array $extraConditions = [] ) { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->options = $options; $this->userRegistrationLookup = $userRegistrationLookup; $this->userIdentityUtils = $userIdentityUtils; $this->userGroupManagerCallback = $userGroupManagerCallback; $this->extraConditions = $extraConditions; } /** * Does the option support conditional defaults? * * @param string $option * @return bool */ public function hasConditionalDefault( string $option ): bool { return array_key_exists( $option, $this->options->get( MainConfigNames::ConditionalUserOptions ) ); } /** * Get all conditionally default user options * * @return string[] */ public function getConditionallyDefaultOptions(): array { return array_keys( $this->options->get( MainConfigNames::ConditionalUserOptions ) ); } /** * Get the conditional default for user and option * * @param string $optionName * @param UserIdentity $userIdentity * @return mixed|null The default value if set, or null if it cannot be determined * conditionally (default from DefaultOptionsLookup should be used in that case). */ public function getOptionDefaultForUser( string $optionName, UserIdentity $userIdentity ) { $conditionalDefaults = $this->options ->get( MainConfigNames::ConditionalUserOptions )[$optionName] ?? []; foreach ( $conditionalDefaults as $conditionalDefault ) { // At the zeroth index of the conditional case, the intended value is found; the rest // of the array are conditions, which are evaluated in checkConditionsForUser(). $value = array_shift( $conditionalDefault ); if ( $this->checkConditionsForUser( $userIdentity, $conditionalDefault ) ) { return $value; } } return null; } /** * Are ALL conditions satisfied for the given user? * * @param UserIdentity $userIdentity * @param array $conditions * @return bool */ private function checkConditionsForUser( UserIdentity $userIdentity, array $conditions ): bool { foreach ( $conditions as $condition ) { if ( !$this->checkConditionForUser( $userIdentity, $condition ) ) { return false; } } return true; } /** * Is ONE condition satisfied for the given user? * * @param UserIdentity $userIdentity * @param array|int $cond Either [ CUDCOND_*, args ] or CUDCOND_*, depending on whether the * condition has any arguments. * @return bool */ private function checkConditionForUser( UserIdentity $userIdentity, $cond ): bool { if ( !is_array( $cond ) ) { $cond = [ $cond ]; } if ( $cond === [] ) { throw new InvalidArgumentException( 'Empty condition' ); } $condName = array_shift( $cond ); switch ( $condName ) { case CUDCOND_AFTER: $registration = $this->userRegistrationLookup->getRegistration( $userIdentity ); if ( $registration === null || $registration === false ) { return false; } return ( (int)ConvertibleTimestamp::convert( TS_UNIX, $registration ) - (int)ConvertibleTimestamp::convert( TS_UNIX, $cond[0] ) ) > 0; case CUDCOND_ANON: return !$userIdentity->isRegistered(); case CUDCOND_NAMED: return $this->userIdentityUtils->isNamed( $userIdentity ); case CUDCOND_USERGROUP: $userGroupManagerCallback = $this->userGroupManagerCallback; /** @var UserGroupManager */ $userGroupManager = $userGroupManagerCallback(); return in_array( $cond[0], $userGroupManager->getUserEffectiveGroups( $userIdentity ) ); default: if ( array_key_exists( $condName, $this->extraConditions ) ) { return call_user_func( $this->extraConditions[$condName], $userIdentity, $cond ); } throw new InvalidArgumentException( 'Unsupported condition ' . $condName ); } } } PK ! �R�� UserSelectQueryBuilder.phpnu �Iw�� <?php /** * 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 */ namespace MediaWiki\User; use Iterator; use MediaWiki\Block\HideUserUtils; use MediaWiki\User\TempUser\TempUserConfig; use Wikimedia\Assert\Assert; use Wikimedia\Assert\PreconditionException; use Wikimedia\Rdbms\IExpression; use Wikimedia\Rdbms\IReadableDatabase; use Wikimedia\Rdbms\LikeValue; use Wikimedia\Rdbms\SelectQueryBuilder; class UserSelectQueryBuilder extends SelectQueryBuilder { /** @var ActorStore */ private $actorStore; private TempUserConfig $tempUserConfig; private HideUserUtils $hideUserUtils; private bool $userJoined = false; /** * @internal * @param IReadableDatabase $db * @param ActorStore $actorStore * @param TempUserConfig $tempUserConfig */ public function __construct( IReadableDatabase $db, ActorStore $actorStore, TempUserConfig $tempUserConfig, HideUserUtils $hideUserUtils ) { parent::__construct( $db ); $this->actorStore = $actorStore; $this->tempUserConfig = $tempUserConfig; $this->hideUserUtils = $hideUserUtils; $this->table( 'actor' ); } /** * Find by provided user ids. * * @param int|int[] $userIds * @return UserSelectQueryBuilder */ public function whereUserIds( $userIds ): self { Assert::parameterType( [ 'integer', 'array' ], $userIds, '$userIds' ); $this->conds( [ 'actor_user' => $userIds ] ); return $this; } /** * Find by provided user ids. * @deprecated since 1.37, use whereUserIds instead * @param int|int[] $userIds * @return UserSelectQueryBuilder */ public function userIds( $userIds ): self { return $this->whereUserIds( $userIds ); } /** * Find by provided user names. * * @param string|string[] $userNames * @return UserSelectQueryBuilder */ public function whereUserNames( $userNames ): self { Assert::parameterType( [ 'string', 'array' ], $userNames, '$userIds' ); $userNames = array_map( function ( $name ) { return $this->actorStore->normalizeUserName( (string)$name ); }, (array)$userNames ); $this->conds( [ 'actor_name' => $userNames ] ); return $this; } /** * Find by provided user names. * @deprecated since 1.37, use whereUserNames instead * @param string|string[] $userNames * @return UserSelectQueryBuilder */ public function userNames( $userNames ): self { return $this->whereUserNames( $userNames ); } /** * Find users with names starting from the provided prefix. * * @note this could produce a huge number of results, like User00000 ... User99999, * so you must set a limit when using this condition. * * @param string $prefix * @return UserSelectQueryBuilder */ public function whereUserNamePrefix( string $prefix ): self { if ( !isset( $this->options['LIMIT'] ) ) { throw new PreconditionException( 'Must set a limit when using a user name prefix' ); } $this->conds( $this->db->expr( 'actor_name', IExpression::LIKE, new LikeValue( $prefix, $this->db->anyString() ) ) ); return $this; } /** * Find users with names starting from the provided prefix. * * @note this could produce a huge number of results, like User00000 ... User99999, * so you must set a limit when using this condition. * @deprecated since 1.37 use whereUserNamePrefix instead * @param string $prefix * @return UserSelectQueryBuilder */ public function userNamePrefix( string $prefix ): self { return $this->whereUserNamePrefix( $prefix ); } /** * Find registered users who registered * * @param string $timestamp * @param bool $direction Direction flag (if true, user_registration must be before $timestamp) * @since 1.42 * @return UserSelectQueryBuilder */ public function whereRegisteredTimestamp( string $timestamp, bool $direction ): self { if ( !$this->userJoined ) { $this->join( 'user', null, [ "actor_user=user_id" ] ); $this->userJoined = true; } $this->conds( $this->db->expr( 'user_registration', ( $direction ? '<' : '>' ), $this->db->timestamp( $timestamp ) ) ); return $this; } /** * Order results by name in $direction * * @param string $dir one of self::SORT_ASC or self::SORT_DESC * @return UserSelectQueryBuilder */ public function orderByName( string $dir = self::SORT_ASC ): self { $this->orderBy( 'actor_name', $dir ); return $this; } /** * Order results by user id. * * @param string $dir one of self::SORT_ASC or self::SORT_DESC * @return UserSelectQueryBuilder */ public function orderByUserId( string $dir = self::SORT_ASC ): self { $this->orderBy( 'actor_user', $dir ); return $this; } /** * Only return registered users. * * @return UserSelectQueryBuilder */ public function registered(): self { $this->conds( $this->db->expr( 'actor_user', '!=', null ) ); return $this; } /** * Only return anonymous users. * * @return UserSelectQueryBuilder */ public function anon(): self { $this->conds( [ 'actor_user' => null ] ); return $this; } /** * Only return named users. * * @return UserSelectQueryBuilder */ public function named(): self { if ( !$this->tempUserConfig->isKnown() ) { // nothing to do: getMatchCondition throws if temp accounts aren't known return $this; } $this->conds( $this->tempUserConfig->getMatchCondition( $this->db, 'actor_name', IExpression::NOT_LIKE ) ); return $this; } /** * Only return temp users * * @return UserSelectQueryBuilder */ public function temp(): self { if ( !$this->tempUserConfig->isKnown() ) { $this->conds( '1=0' ); return $this; } $this->conds( $this->tempUserConfig->getMatchCondition( $this->db, 'actor_name', IExpression::LIKE ) ); return $this; } /** * Filter based on user hidden status * * @since 1.38 * @param bool $hidden True - only hidden users, false - no hidden users * @return $this */ public function hidden( bool $hidden ): self { $this->conds( $this->hideUserUtils->getExpression( $this->db, 'actor_user', $hidden ? HideUserUtils::HIDDEN_USERS : HideUserUtils::SHOWN_USERS ) ); return $this; } /** * Fetch a single UserIdentity that matches specified criteria. * * @return UserIdentity|null */ public function fetchUserIdentity(): ?UserIdentity { $this->fields( [ 'actor_id', 'actor_name', 'actor_user' ] ); $row = $this->fetchRow(); if ( !$row ) { return null; } return $this->actorStore->newActorFromRow( $row ); } /** * Fetch UserIdentities for the specified query. * * @return Iterator<UserIdentity> */ public function fetchUserIdentities(): Iterator { $this->fields( [ 'actor_id', 'actor_name', 'actor_user' ] ); $result = $this->fetchResultSet(); foreach ( $result as $row ) { yield $this->actorStore->newActorFromRow( $row ); } $result->free(); } /** * Returns an array of user names matching the query. * * @return string[] */ public function fetchUserNames(): array { $this->field( 'actor_name' ); return $this->fetchFieldValues(); } } PK ! ɘ�< < # UserNamePrefixSearch_deprecated.phpnu �Iw�� <?php /** * Prefix search of user names. * * 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 */ use MediaWiki\MediaWikiServices; use MediaWiki\User\User; // phpcs:disable MediaWiki.Files.ClassMatchesFilename.NotMatch /** * Handles searching prefixes of user names * * @note There are two classes called UserNamePrefixSearch. You should use the first one, in * namespace MediaWiki\User, which is a service. \UserNamePrefixSearch is a deprecated static wrapper * that forwards to the global service. * * @deprecated since 1.36, use the MediaWiki\User\UserNamePrefixSearch service; hard deprecated * since 1.41 * * @since 1.27 */ class UserNamePrefixSearch { /** * Do a prefix search of user names and return a list of matching user names. * * @deprecated since 1.36, use the MediaWiki\User\UserNamePrefixSearch service instead; hard * deprecated since 1.41 * * @param string|User $audience The string 'public' or a user object to show the search for * @param string $search * @param int $limit * @param int $offset How many results to offset from the beginning * @return string[] */ public static function search( $audience, $search, $limit, $offset = 0 ) { wfDeprecated( __METHOD__, '1.36' ); return MediaWikiServices::getInstance() ->getUserNamePrefixSearch() ->search( $audience, (string)$search, (int)$limit, (int)$offset ); } } PK ! �j|�8 8 LoggedOutEditToken.phpnu �Iw�� <?php /** * MediaWiki edit token * * 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 Session */ namespace MediaWiki\User; use MediaWiki\Session\Token; /** * Value object representing a logged-out user's edit token * * This exists so that code generically dealing with MediaWiki\Session\Token * (i.e. the API) doesn't have to have so many special cases for anon edit * tokens. * * @newable * * @since 1.27 */ class LoggedOutEditToken extends Token { /** * @stable to call */ public function __construct() { parent::__construct( '', '', false ); } protected function toStringAtTimestamp( $timestamp ) { return self::SUFFIX; } public function match( $userToken, $maxAge = null ) { return $userToken === self::SUFFIX; } } /** @deprecated class alias since 1.41 */ class_alias( LoggedOutEditToken::class, 'LoggedOutEditToken' ); PK ! ?�!t� � ActorCache.phpnu �Iw�� <?php /** * 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 */ namespace MediaWiki\User; /** * Simple in-memory cache for UserIdentity objects indexed by user ID, * actor ID and user name. * * We cannot just use MapCacheLRU for this because of eviction semantics: * we need to be able to remove UserIdentity from the cache even if * user ID or user name has changed, so we track the most accessed VALUES * in the cache, not keys, and evict them alongside with all their indexes. * * @since 1.37 * @internal for use by ActorStore * @package MediaWiki\User */ class ActorCache { /** @var string Key by actor ID */ public const KEY_ACTOR_ID = 'actorId'; /** @var string Key by user ID */ public const KEY_USER_ID = 'userId'; /** @var string Key by user name */ public const KEY_USER_NAME = 'name'; private int $maxSize; /** * @var array[][] Contains 3 keys, KEY_ACTOR_ID, KEY_USER_ID, and KEY_USER_NAME, * each of which has a value of an array of arrays with actor ids and UserIdentity objects, * keyed with the corresponding actor id/user id/user name */ private $cache = [ self::KEY_ACTOR_ID => [], self::KEY_USER_NAME => [], self::KEY_USER_ID => [] ]; /** * @param int $maxSize hold up to this many UserIdentity values */ public function __construct( int $maxSize ) { $this->maxSize = $maxSize; } /** * Get user identity which has $keyType equal to $keyValue * @param string $keyType one of self::KEY_* constants. * @param string|int $keyValue * @return UserIdentity|null */ public function getActor( string $keyType, $keyValue ): ?UserIdentity { return $this->getCachedValue( $keyType, $keyValue )['actor'] ?? null; } /** * Get actor ID of the user which has $keyType equal to $keyValue. * @param string $keyType one of self::KEY_* constants. * @param string|int $keyValue * @return int|null */ public function getActorId( string $keyType, $keyValue ): ?int { return $this->getCachedValue( $keyType, $keyValue )['actorId'] ?? null; } /** * Add $actor with $actorId to the cache. * @param int $actorId * @param UserIdentity $actor */ public function add( int $actorId, UserIdentity $actor ) { while ( count( $this->cache[self::KEY_ACTOR_ID] ) >= $this->maxSize ) { $evictId = array_key_first( $this->cache[self::KEY_ACTOR_ID] ); $this->remove( $this->cache[self::KEY_ACTOR_ID][$evictId]['actor'] ); } $value = [ 'actorId' => $actorId, 'actor' => $actor ]; $this->cache[self::KEY_ACTOR_ID][$actorId] = $value; $userId = $actor->getId( $actor->getWikiId() ); if ( $userId ) { $this->cache[self::KEY_USER_ID][$userId] = $value; } $this->cache[self::KEY_USER_NAME][$actor->getName()] = $value; } /** * Remove $actor from cache. * @param UserIdentity $actor */ public function remove( UserIdentity $actor ) { $oldByName = $this->cache[self::KEY_USER_NAME][$actor->getName()] ?? null; $oldByUserId = $this->cache[self::KEY_USER_ID][$actor->getId( $actor->getWikiId() )] ?? null; if ( $oldByName ) { unset( $this->cache[self::KEY_USER_ID][$oldByName['actor']->getId( $oldByName['actor']->getWikiId() )] ); unset( $this->cache[self::KEY_ACTOR_ID][$oldByName['actorId']] ); } if ( $oldByUserId ) { unset( $this->cache[self::KEY_USER_NAME][$oldByUserId['actor']->getName()] ); unset( $this->cache[self::KEY_ACTOR_ID][$oldByUserId['actorId']] ); } unset( $this->cache[self::KEY_USER_NAME][$actor->getName()] ); unset( $this->cache[self::KEY_USER_ID][$actor->getId( $actor->getWikiId() )] ); } /** * Remove everything from the cache. * @internal */ public function clear() { $this->cache = [ self::KEY_ACTOR_ID => [], self::KEY_USER_NAME => [], self::KEY_USER_ID => [] ]; } /** * @param string $keyType one of self::KEY_* constants. * @param string|int $keyValue * @return array|null [ 'actor' => UserIdentity, 'actorId' => int ] */ private function getCachedValue( string $keyType, $keyValue ): ?array { if ( isset( $this->cache[$keyType][$keyValue] ) ) { $cached = $this->cache[$keyType][$keyValue]; $actorId = $cached['actorId']; // Record the actor with $actorId was recently used. $item = $this->cache[self::KEY_ACTOR_ID][$actorId]; unset( $this->cache[self::KEY_ACTOR_ID][$actorId] ); $this->cache[self::KEY_ACTOR_ID][$actorId] = $item; return $cached; } return null; } } PK ! �kj�� � UserTimeCorrection.phpnu �Iw�� <?php /** * 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 * @author Derk-Jan Hartman <hartman.wiki@gmail.com> */ namespace MediaWiki\User; use DateInterval; use DateTime; use DateTimeZone; use Exception; use MediaWiki\Utils\MWTimestamp; use Stringable; use Wikimedia\RequestTimeout\TimeoutException; /** * Utility class to parse the TimeCorrection string value. * * These values are used to specify the time offset for a user and are stored in * the database as a user preference and returned by the preferences APIs * * The class will correct invalid input and adjusts timezone offsets to applicable dates, * taking into account DST etc. * * @since 1.37 */ class UserTimeCorrection implements Stringable { /** * @var string (default) Time correction based on the MediaWiki's system offset from UTC. * The System offset can be configured with wgLocalTimezone and/or wgLocalTZoffset */ public const SYSTEM = 'System'; /** @var string Time correction based on a user defined offset from UTC */ public const OFFSET = 'Offset'; /** @var string Time correction based on a user defined timezone */ public const ZONEINFO = 'ZoneInfo'; /** @var DateTime */ private $date; /** @var bool */ private $valid; /** @var string */ private $correctionType; /** @var int Offset in minutes */ private $offset; /** @var DateTimeZone|null */ private $timeZone; /** * @param string $timeCorrection Original time correction string * @param DateTime|null $relativeToDate The date used to calculate the time zone offset of. * This defaults to the current date and time. * @param int $systemOffset Offset for self::SYSTEM in minutes */ public function __construct( string $timeCorrection, ?DateTime $relativeToDate = null, int $systemOffset = 0 ) { $this->date = $relativeToDate ?? new DateTime( '@' . MWTimestamp::time() ); $this->valid = false; $this->parse( $timeCorrection, $systemOffset ); } /** * Get time offset for a user * * @return string Offset that was applied to the user */ public function getCorrectionType(): string { return $this->correctionType; } /** * Get corresponding time offset for this correction * Note: When correcting dates/times, apply only the offset OR the time zone, not both. * @return int Offset in minutes */ public function getTimeOffset(): int { return $this->offset; } /** * Get corresponding time offset for this correction * Note: When correcting dates/times, apply only the offset OR the time zone, not both. * @return DateInterval Offset in minutes as a DateInterval */ public function getTimeOffsetInterval(): DateInterval { $offset = abs( $this->offset ); $interval = new DateInterval( "PT{$offset}M" ); if ( $this->offset < 1 ) { $interval->invert = 1; } return $interval; } /** * The time zone if known * Note: When correcting dates/times, apply only the offset OR the time zone, not both. * @return DateTimeZone|null */ public function getTimeZone(): ?DateTimeZone { return $this->timeZone; } /** * Was the original correction specification valid * @return bool */ public function isValid(): bool { return $this->valid; } /** * Parse the timecorrection string as stored in the database for a user * or as entered into the Preferences form field * * There can be two forms of these strings: * 1. A pipe separated tuple of a maximum of 3 fields * - Field 1 is the type of offset definition * - Field 2 is the offset in minutes from UTC (ignored for System type) * FIXME Since it's ignored, remove the offset from System everywhere. * - Field 3 is a timezone identifier from the tz database (only required for ZoneInfo type) * - The offset for a ZoneInfo type is unreliable because of DST. * After retrieving it from the database, it should be recalculated based on the TZ identifier. * Examples: * - System * - System|60 * - Offset|60 * - ZoneInfo|60|Europe/Amsterdam * * 2. The following form provides an offset in hours and minutes * This currently should only be used by the preferences input field, * but historically they were present in the database. * TODO: write a maintenance script to migrate these old db values * Examples: * - 16:00 * - 10 * * @param string $timeCorrection * @param int $systemOffset */ private function parse( string $timeCorrection, int $systemOffset ) { $data = explode( '|', $timeCorrection, 3 ); // First handle the case of an actual timezone being specified. if ( $data[0] === self::ZONEINFO ) { try { $this->correctionType = self::ZONEINFO; $this->timeZone = new DateTimeZone( $data[2] ); $this->offset = (int)floor( $this->timeZone->getOffset( $this->date ) / 60 ); $this->valid = true; return; } catch ( TimeoutException $e ) { throw $e; } catch ( Exception $e ) { // Not a valid/known timezone. // Fall back to any specified offset } } // If $timeCorrection is in fact a pipe-separated value, check the // first value. switch ( $data[0] ) { case self::OFFSET: case self::ZONEINFO: $this->correctionType = self::OFFSET; // First value is Offset, so use the specified offset $this->offset = (int)( $data[1] ?? 0 ); // If this is ZoneInfo, then we didn't recognize the TimeZone $this->valid = isset( $data[1] ) && $data[0] === self::OFFSET; break; case self::SYSTEM: $this->correctionType = self::SYSTEM; $this->offset = $systemOffset; $this->valid = true; break; default: // $timeCorrection actually isn't a pipe separated value, but instead // a colon separated value. This is only used by the HTMLTimezoneField userinput // but can also still be present in the Db. (but shouldn't be) $this->correctionType = self::OFFSET; $data = explode( ':', $timeCorrection, 2 ); if ( count( $data ) >= 2 ) { // Combination hours and minutes. $this->offset = abs( (int)$data[0] ) * 60 + (int)$data[1]; if ( (int)$data[0] < 0 ) { $this->offset *= -1; } $this->valid = true; } elseif ( preg_match( '/^[+-]?\d+$/', $data[0] ) ) { // Just hours. $this->offset = (int)$data[0] * 60; $this->valid = true; } else { // We really don't know this. Fallback to System $this->correctionType = self::SYSTEM; $this->offset = $systemOffset; return; } break; } // Max is +14:00 and min is -12:00, see: // https://en.wikipedia.org/wiki/Timezone if ( $this->offset < -12 * 60 || $this->offset > 14 * 60 ) { $this->valid = false; } // 14:00 $this->offset = min( $this->offset, 14 * 60 ); // -12:00 $this->offset = max( $this->offset, -12 * 60 ); } /** * Converts a timezone offset in minutes (e.g., "120") to an hh:mm string like "+02:00". * @param int $offset * @return string */ public static function formatTimezoneOffset( int $offset ): string { $hours = $offset > 0 ? floor( $offset / 60 ) : ceil( $offset / 60 ); return sprintf( '%+03d:%02d', $hours, abs( $offset ) % 60 ); } /** * Note: The string value of this object might not be equal to the original value * @return string a timecorrection string representing this value */ public function toString(): string { switch ( $this->correctionType ) { case self::ZONEINFO: if ( $this->timeZone ) { return "ZoneInfo|{$this->offset}|{$this->timeZone->getName()}"; } // If not, fallback: case self::SYSTEM: case self::OFFSET: default: return "{$this->correctionType}|{$this->offset}"; } } public function __toString() { return $this->toString(); } } PK ! 7s�7�. �. BotPasswordStore.phpnu �Iw�� <?php /** * BotPassword interaction with databases * * 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 */ namespace MediaWiki\User; use MediaWiki\Config\ServiceOptions; use MediaWiki\Json\FormatJson; use MediaWiki\MainConfigNames; use MediaWiki\Password\Password; use MediaWiki\Password\PasswordFactory; use MediaWiki\User\CentralId\CentralIdLookup; use MWCryptRand; use MWRestrictions; use StatusValue; use Wikimedia\Rdbms\IConnectionProvider; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\IDBAccessObject; use Wikimedia\Rdbms\IReadableDatabase; /** * @author DannyS712 * @since 1.37 */ class BotPasswordStore { /** * @internal For use by ServiceWiring */ public const CONSTRUCTOR_OPTIONS = [ MainConfigNames::EnableBotPasswords, ]; private ServiceOptions $options; private IConnectionProvider $dbProvider; private CentralIdLookup $centralIdLookup; /** * @param ServiceOptions $options * @param CentralIdLookup $centralIdLookup * @param IConnectionProvider $dbProvider */ public function __construct( ServiceOptions $options, CentralIdLookup $centralIdLookup, IConnectionProvider $dbProvider ) { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->options = $options; $this->centralIdLookup = $centralIdLookup; $this->dbProvider = $dbProvider; } /** * Get a database connection for the bot passwords database * @return IReadableDatabase * @internal */ public function getReplicaDatabase(): IReadableDatabase { return $this->dbProvider->getReplicaDatabase( 'virtual-botpasswords' ); } /** * Get a database connection for the bot passwords database * @return IDatabase * @internal */ public function getPrimaryDatabase(): IDatabase { return $this->dbProvider->getPrimaryDatabase( 'virtual-botpasswords' ); } /** * Load a BotPassword from the database based on a UserIdentity object * @param UserIdentity $userIdentity * @param string $appId * @param int $flags IDBAccessObject read flags * @return BotPassword|null */ public function getByUser( UserIdentity $userIdentity, string $appId, int $flags = IDBAccessObject::READ_NORMAL ): ?BotPassword { if ( !$this->options->get( MainConfigNames::EnableBotPasswords ) ) { return null; } $centralId = $this->centralIdLookup->centralIdFromLocalUser( $userIdentity, CentralIdLookup::AUDIENCE_RAW, $flags ); return $centralId ? $this->getByCentralId( $centralId, $appId, $flags ) : null; } /** * Load a BotPassword from the database * @param int $centralId from CentralIdLookup * @param string $appId * @param int $flags IDBAccessObject read flags * @return BotPassword|null */ public function getByCentralId( int $centralId, string $appId, int $flags = IDBAccessObject::READ_NORMAL ): ?BotPassword { if ( !$this->options->get( MainConfigNames::EnableBotPasswords ) ) { return null; } if ( ( $flags & IDBAccessObject::READ_LATEST ) == IDBAccessObject::READ_LATEST ) { $db = $this->dbProvider->getPrimaryDatabase( 'virtual-botpasswords' ); } else { $db = $this->dbProvider->getReplicaDatabase( 'virtual-botpasswords' ); } $row = $db->newSelectQueryBuilder() ->select( [ 'bp_user', 'bp_app_id', 'bp_token', 'bp_restrictions', 'bp_grants' ] ) ->from( 'bot_passwords' ) ->where( [ 'bp_user' => $centralId, 'bp_app_id' => $appId ] ) ->recency( $flags ) ->caller( __METHOD__ )->fetchRow(); return $row ? new BotPassword( $row, true, $flags ) : null; } /** * Create an unsaved BotPassword * @param array $data Data to use to create the bot password. Keys are: * - user: (UserIdentity) UserIdentity to create the password for. Overrides username and centralId. * - username: (string) Username to create the password for. Overrides centralId. * - centralId: (int) User central ID to create the password for. * - appId: (string, required) App ID for the password. * - restrictions: (MWRestrictions, optional) Restrictions. * - grants: (string[], optional) Grants. * @param int $flags IDBAccessObject read flags * @return BotPassword|null */ public function newUnsavedBotPassword( array $data, int $flags = IDBAccessObject::READ_NORMAL ): ?BotPassword { if ( isset( $data['user'] ) && ( !$data['user'] instanceof UserIdentity ) ) { return null; } $row = (object)[ 'bp_user' => 0, 'bp_app_id' => trim( $data['appId'] ?? '' ), 'bp_token' => '**unsaved**', 'bp_restrictions' => $data['restrictions'] ?? MWRestrictions::newDefault(), 'bp_grants' => $data['grants'] ?? [], ]; if ( $row->bp_app_id === '' || strlen( $row->bp_app_id ) > BotPassword::APPID_MAXLENGTH || !$row->bp_restrictions instanceof MWRestrictions || !is_array( $row->bp_grants ) ) { return null; } $row->bp_restrictions = $row->bp_restrictions->toJson(); $row->bp_grants = FormatJson::encode( $row->bp_grants ); if ( isset( $data['user'] ) ) { // Must be a UserIdentity object, already checked above $row->bp_user = $this->centralIdLookup->centralIdFromLocalUser( $data['user'], CentralIdLookup::AUDIENCE_RAW, $flags ); } elseif ( isset( $data['username'] ) ) { $row->bp_user = $this->centralIdLookup->centralIdFromName( $data['username'], CentralIdLookup::AUDIENCE_RAW, $flags ); } elseif ( isset( $data['centralId'] ) ) { $row->bp_user = $data['centralId']; } if ( !$row->bp_user ) { return null; } return new BotPassword( $row, false, $flags ); } /** * Save the new BotPassword to the database * * @internal * * @param BotPassword $botPassword * @param Password|null $password Use null for an invalid password * @return StatusValue if everything worked, the value of the StatusValue is the new token */ public function insertBotPassword( BotPassword $botPassword, ?Password $password = null ): StatusValue { $res = $this->validateBotPassword( $botPassword ); if ( !$res->isGood() ) { return $res; } $password ??= PasswordFactory::newInvalidPassword(); $dbw = $this->getPrimaryDatabase(); $dbw->newInsertQueryBuilder() ->insertInto( 'bot_passwords' ) ->ignore() ->row( [ 'bp_user' => $botPassword->getUserCentralId(), 'bp_app_id' => $botPassword->getAppId(), 'bp_token' => MWCryptRand::generateHex( User::TOKEN_LENGTH ), 'bp_restrictions' => $botPassword->getRestrictions()->toJson(), 'bp_grants' => FormatJson::encode( $botPassword->getGrants() ), 'bp_password' => $password->toString(), ] ) ->caller( __METHOD__ )->execute(); $ok = (bool)$dbw->affectedRows(); if ( $ok ) { $token = $dbw->newSelectQueryBuilder() ->select( 'bp_token' ) ->from( 'bot_passwords' ) ->where( [ 'bp_user' => $botPassword->getUserCentralId(), 'bp_app_id' => $botPassword->getAppId(), ] ) ->caller( __METHOD__ )->fetchField(); return StatusValue::newGood( $token ); } return StatusValue::newFatal( 'botpasswords-insert-failed', $botPassword->getAppId() ); } /** * Update an existing BotPassword in the database * * @internal * * @param BotPassword $botPassword * @param Password|null $password Use null for an invalid password * @return StatusValue if everything worked, the value of the StatusValue is the new token */ public function updateBotPassword( BotPassword $botPassword, ?Password $password = null ): StatusValue { $res = $this->validateBotPassword( $botPassword ); if ( !$res->isGood() ) { return $res; } $conds = [ 'bp_user' => $botPassword->getUserCentralId(), 'bp_app_id' => $botPassword->getAppId(), ]; $fields = [ 'bp_token' => MWCryptRand::generateHex( User::TOKEN_LENGTH ), 'bp_restrictions' => $botPassword->getRestrictions()->toJson(), 'bp_grants' => FormatJson::encode( $botPassword->getGrants() ), ]; if ( $password !== null ) { $fields['bp_password'] = $password->toString(); } $dbw = $this->getPrimaryDatabase(); $dbw->newUpdateQueryBuilder() ->update( 'bot_passwords' ) ->set( $fields ) ->where( $conds ) ->caller( __METHOD__ )->execute(); $ok = (bool)$dbw->affectedRows(); if ( $ok ) { $token = $dbw->newSelectQueryBuilder() ->select( 'bp_token' ) ->from( 'bot_passwords' ) ->where( $conds ) ->caller( __METHOD__ )->fetchField(); return StatusValue::newGood( $token ); } return StatusValue::newFatal( 'botpasswords-update-failed', $botPassword->getAppId() ); } /** * Check if a BotPassword is valid to save in the database (either inserting a new * one or updating an existing one) based on the size of the restrictions and grants * * @param BotPassword $botPassword * @return StatusValue */ private function validateBotPassword( BotPassword $botPassword ): StatusValue { $res = StatusValue::newGood(); $restrictions = $botPassword->getRestrictions()->toJson(); if ( strlen( $restrictions ) > BotPassword::RESTRICTIONS_MAXLENGTH ) { $res->fatal( 'botpasswords-toolong-restrictions' ); } $grants = FormatJson::encode( $botPassword->getGrants() ); if ( strlen( $grants ) > BotPassword::GRANTS_MAXLENGTH ) { $res->fatal( 'botpasswords-toolong-grants' ); } return $res; } /** * Delete an existing BotPassword in the database * * @param BotPassword $botPassword * @return bool */ public function deleteBotPassword( BotPassword $botPassword ): bool { $dbw = $this->getPrimaryDatabase(); $dbw->newDeleteQueryBuilder() ->deleteFrom( 'bot_passwords' ) ->where( [ 'bp_user' => $botPassword->getUserCentralId() ] ) ->andWhere( [ 'bp_app_id' => $botPassword->getAppId() ] ) ->caller( __METHOD__ )->execute(); return (bool)$dbw->affectedRows(); } /** * Invalidate all passwords for a user, by name * @param string $username * @return bool Whether any passwords were invalidated */ public function invalidateUserPasswords( string $username ): bool { if ( !$this->options->get( MainConfigNames::EnableBotPasswords ) ) { return false; } $centralId = $this->centralIdLookup->centralIdFromName( $username, CentralIdLookup::AUDIENCE_RAW, IDBAccessObject::READ_LATEST ); if ( !$centralId ) { return false; } $dbw = $this->getPrimaryDatabase(); $dbw->newUpdateQueryBuilder() ->update( 'bot_passwords' ) ->set( [ 'bp_password' => PasswordFactory::newInvalidPassword()->toString() ] ) ->where( [ 'bp_user' => $centralId ] ) ->caller( __METHOD__ )->execute(); return (bool)$dbw->affectedRows(); } /** * Remove all passwords for a user, by name * @param string $username * @return bool Whether any passwords were removed */ public function removeUserPasswords( string $username ): bool { if ( !$this->options->get( MainConfigNames::EnableBotPasswords ) ) { return false; } $centralId = $this->centralIdLookup->centralIdFromName( $username, CentralIdLookup::AUDIENCE_RAW, IDBAccessObject::READ_LATEST ); if ( !$centralId ) { return false; } $dbw = $this->getPrimaryDatabase(); $dbw->newDeleteQueryBuilder() ->deleteFrom( 'bot_passwords' ) ->where( [ 'bp_user' => $centralId ] ) ->caller( __METHOD__ )->execute(); return (bool)$dbw->affectedRows(); } } PK ! �'��? ? UserIdentityLookup.phpnu �Iw�� <?php /** * 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 */ namespace MediaWiki\User; use InvalidArgumentException; use Wikimedia\Rdbms\IDBAccessObject; use Wikimedia\Rdbms\IReadableDatabase; /** * Service for looking up UserIdentity * * @package MediaWiki\User * @since 1.36 */ interface UserIdentityLookup { /** * Find an identity of a user by $name * * @param string $name * @param int $queryFlags one of IDBAccessObject constants * @return UserIdentity|null * @throws InvalidArgumentException if non-normalizable actor name is passed. */ public function getUserIdentityByName( string $name, int $queryFlags = IDBAccessObject::READ_NORMAL ): ?UserIdentity; /** * Find an identity of a user by $userId * * @param int $userId * @param int $queryFlags one of IDBAccessObject constants * @return UserIdentity|null */ public function getUserIdentityByUserId( int $userId, int $queryFlags = IDBAccessObject::READ_NORMAL ): ?UserIdentity; /** * Returns a specialized SelectQueryBuilder for querying the UserIdentity objects. * * @param IReadableDatabase|int $dbOrQueryFlags The database connection to perform the query on, * or one of the IDBAccessObject::READ_* constants. * @return UserSelectQueryBuilder */ public function newSelectQueryBuilder( $dbOrQueryFlags = IDBAccessObject::READ_NORMAL ): UserSelectQueryBuilder; } PK ! ��1U U TempUser/CreateStatus.phpnu �Iw�� <?php namespace MediaWiki\User\TempUser; use MediaWiki\Status\Status; use MediaWiki\User\User; /** * Status object with strongly typed value, for TempUserManager::createUser() * * @since 1.39 * @internal */ class CreateStatus extends Status { /** * @return User */ public function getUser(): User { return $this->value; } } PK ! �*!�@ @ TempUser/TempUserConfig.phpnu �Iw�� <?php namespace MediaWiki\User\TempUser; use MediaWiki\Permissions\Authority; use Wikimedia\Rdbms\IExpression; use Wikimedia\Rdbms\IReadableDatabase; /** * Interface for temporary user creation config and name matching. * * This is separate from TempUserCreator to avoid dependency loops during * service construction, since TempUserCreator needs UserNameUtils which * needs TempUserConfig. * * @since 1.39 */ interface TempUserConfig { /** * Is temp user creation enabled? * * @return bool */ public function isEnabled(); /** * Are temporary accounts a known concept on the wiki? * This should return true if any temporary accounts exist. * * @return bool */ public function isKnown(); /** * Is the action valid for user auto-creation? * * @param string $action * @return bool */ public function isAutoCreateAction( string $action ); /** * Should/would auto-create be performed if the user attempts to perform * the given action? * * @since 1.41 * @param Authority $authority * @param string $action * @return bool */ public function shouldAutoCreate( Authority $authority, string $action ); /** * Does the name match the configured pattern indicating that it is a * temporary auto-created user? * * @param string $name * @return bool */ public function isTempName( string $name ); /** * Does the name match a configured pattern which indicates that it * conflicts with temporary user names? Should manual user creation * be denied? * * @param string $name * @return mixed */ public function isReservedName( string $name ); /** * Get a placeholder name which matches the reserved prefix * * @return string */ public function getPlaceholderName(): string; /** * Get a Pattern indicating how temporary account can be detected * * Used to avoid selecting a temp account via select queries. * * @deprecated since 1.42. Use ::getMatchPatterns as multiple patterns may be defined. * @return Pattern */ public function getMatchPattern(): Pattern; /** * Get Patterns indicating how temporary account can be detected * * Used to avoid selecting a temp account via select queries. * * @since 1.42 * @return Pattern[] */ public function getMatchPatterns(): array; /** * Get a SQL query condition that will match (or not match) temporary accounts. * * @since 1.42 * @param IReadableDatabase $db * @param string $field Database field to match against * @param string $op Operator: IExpression::LIKE or IExpression::NOT_LIKE * @return IExpression */ public function getMatchCondition( IReadableDatabase $db, string $field, string $op ): IExpression; /** * After how many days do temporary users expire? * * @note expireTemporaryAccounts.php maintenance script needs to be periodically executed for * temp account expiry to work. * @since 1.42 * @return int|null Null if temp accounts should never expire */ public function getExpireAfterDays(): ?int; /** * How many days before expiration should temporary users be notified? * * @note expireTemporaryAccounts.php maintenance script needs to be periodically executed for * temp account expiry to work. * @since 1.42 * @return int|null Null if temp accounts should never be notified before expiration */ public function getNotifyBeforeExpirationDays(): ?int; } PK ! `�j� � TempUser/DBSerialProvider.phpnu �Iw�� <?php namespace MediaWiki\User\TempUser; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\RawSQLValue; /** * Base class for serial acquisition code shared between core and CentralAuth. * * @since 1.39 */ abstract class DBSerialProvider implements SerialProvider { /** @var int */ private $numShards; /** * @param array $config * - numShards (int, default 1): A small integer. This can be set to a * value greater than 1 to avoid acquiring a global lock when * allocating IDs, at the expense of making the IDs be non-monotonic. */ public function __construct( $config ) { $this->numShards = $config['numShards'] ?? 1; } public function acquireIndex( int $year = 0 ): int { if ( $this->numShards ) { $shard = mt_rand( 0, $this->numShards - 1 ); } else { $shard = 0; } $dbw = $this->getDB(); $table = $this->getTableName(); $dbw->startAtomic( __METHOD__ ); $dbw->newInsertQueryBuilder() ->insertInto( $table ) ->row( [ 'uas_shard' => $shard, 'uas_year' => $year, 'uas_value' => 1 ] ) ->onDuplicateKeyUpdate() ->uniqueIndexFields( [ 'uas_shard', 'uas_year' ] ) ->set( [ 'uas_value' => new RawSQLValue( 'uas_value+1' ) ] ) ->caller( __METHOD__ )->execute(); $value = $dbw->newSelectQueryBuilder() ->select( 'uas_value' ) ->from( $table ) ->where( [ 'uas_shard' => $shard ] ) ->andWhere( [ 'uas_year' => $year ] ) ->caller( __METHOD__ ) ->fetchField(); $dbw->endAtomic( __METHOD__ ); return $value * $this->numShards + $shard; } /** * @return IDatabase */ abstract protected function getDB(); /** * @return string */ abstract protected function getTableName(); } PK ! � �� � TempUser/ScrambleMapping.phpnu �Iw�� <?php namespace MediaWiki\User\TempUser; use LogicException; use OutOfBoundsException; use RuntimeException; /** * A mapping which converts sequential input into an output sequence that looks * pseudo-random, but preserves the base-10 length of the input number. * * Take a sequence generated by multiplying the previous element of the * sequence by a fixed number "g", then applying the modulus "p": * * X(0) = 1 * X(i) = ( g X(i-1) ) mod p * * If g is a primitive root modulo p, then this sequence will cover all values * from 1 to p-1 before it repeats. X(i) is a modular exponential function * (g^i mod p) and algorithms are available to calculate it efficiently. * * Loosely speaking, we choose a sequence based on the number of digits N in the * input, with the period being approximately 10^N, so that the number of digits * in the output will be approximately the same. * * More precisely, after offsetting the subsequent sequences to avoid colliding * with the previous sequences, the period ends up being about 0.9 * 10^N * * The modulo p is always a prime number because that makes the maths easier. * We use a value for g close to p/sqrt(3) since that seems to stir the digits * better than the largest or smallest primitive root. * * @internal */ class ScrambleMapping implements SerialMapping { /** * Appropriately sized prime moduli and primitive roots. Generated with * this GP/PARI script: * s=0; \ * for(q = 2, 10, \ * p=precprime(10^q - s); \ * s = s + p; \ * forstep(i = floor(p/sqrt(3)), 1, -1, \ * if(znorder(Mod(i, p)) == p-1, \ * print("[ ", i, ", ", p, " ],"); \ * break ))) */ private const GENERATORS = [ [ 56, 97 ], [ 511, 887 ], [ 5203, 9013 ], [ 51947, 90001 ], [ 519612, 900001 ], [ 5196144, 8999993 ], [ 51961523, 89999999 ], [ 519615218, 899999963 ], [ 5196152444, 9000000043 ], ]; /** @var int */ private $offset; /** @var bool */ private $hasGmp; /** @var bool */ private $hasBcm; public function __construct( $config ) { $this->offset = $config['offset'] ?? 0; $this->hasGmp = extension_loaded( 'gmp' ); $this->hasBcm = extension_loaded( 'bcmath' ); if ( !$this->hasGmp && !$this->hasBcm ) { throw new RuntimeException( __CLASS__ . ' requires the bcmath or gmp extension' ); } } public function getSerialIdForIndex( int $index ): string { if ( $index <= 0 ) { return (string)$index; } $offset = $this->offset; if ( $index - $offset < 0 ) { throw new OutOfBoundsException( __METHOD__ . ": The configured offset $offset is too large." ); } foreach ( self::GENERATORS as [ $g, $p ] ) { if ( $index - $offset < $p ) { return (string)( $offset + $this->powmod( $g, $index - $offset, $p ) ); } $offset += $p - 1; } throw new RuntimeException( __METHOD__ . ": The index $index is too large" ); } private function powmod( $num, $exponent, $modulus ) { if ( $this->hasGmp ) { return \gmp_intval( \gmp_powm( $num, $exponent, $modulus ) ); } elseif ( $this->hasBcm ) { return (int)\bcpowmod( (string)$num, (string)$exponent, (string)$modulus ); } else { throw new LogicException( __CLASS__ . ' requires the bcmath or gmp extension' ); } } } PK ! 9��7� � &