Page MenuHomeWickedGov Phorge

GlobalBlockLookup.php
No OneTemporary

Size
24 KB
Referenced Files
None
Subscribers
None

GlobalBlockLookup.php

<?php
namespace MediaWiki\Extension\GlobalBlocking\Services;
use InvalidArgumentException;
use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Context\RequestContext;
use MediaWiki\Extension\GlobalBlocking\GlobalBlock;
use MediaWiki\User\CentralId\CentralIdLookup;
use MediaWiki\User\TempUser\TempUserConfig;
use MediaWiki\User\User;
use MediaWiki\User\UserFactory;
use MediaWiki\User\UserIdentity;
use stdClass;
use Wikimedia\IPUtils;
use Wikimedia\Rdbms\FakeResultWrapper;
use Wikimedia\Rdbms\IExpression;
use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\LikeValue;
/**
* Allows looking up global blocks in the globalblocks table.
*
* @since 1.42
*/
class GlobalBlockLookup {
public const CONSTRUCTOR_OPTIONS = [
'GlobalBlockingAllowedRanges',
'GlobalBlockingBlockXFF',
'GlobalBlockingCIDRLimit',
'GlobalBlockingCentralWikiContentLanguage',
];
private const TYPE_USER = 1;
private const TYPE_IP = 2;
private const TYPE_RANGE = 3;
private const TYPE_AUTOBLOCK = 4;
/** @var int Flag to ignore blocks on IP addresses which are marked as anon-only. */
public const SKIP_SOFT_IP_BLOCKS = 1;
/** @var int Flag to ignore all blocks on IP addresses. */
public const SKIP_IP_BLOCKS = 2;
/** @var int Flag to skip checking if the blocks that affect a target are locally disabled. */
public const SKIP_LOCAL_DISABLE_CHECK = 4;
/** @var int Flag to skip the excluding of IP blocks in the GlobalBlockingAllowedRanges config. */
public const SKIP_ALLOWED_RANGES_CHECK = 8;
/** @var int Flag to ignore all autoblocks. Is implicitly set if ::SKIP_IP_BLOCKS is set. */
public const SKIP_AUTOBLOCKS = 16;
private ServiceOptions $options;
private GlobalBlockingConnectionProvider $globalBlockingConnectionProvider;
private StatsdDataFactoryInterface $statsdFactory;
private CentralIdLookup $centralIdLookup;
private GlobalBlockLocalStatusLookup $globalBlockLocalStatusLookup;
private TempUserConfig $tempUserConfig;
private UserFactory $userFactory;
private array $getUserBlockDetailsCache = [];
public function __construct(
ServiceOptions $options,
GlobalBlockingConnectionProvider $globalBlockingConnectionProvider,
StatsdDataFactoryInterface $statsdFactory,
CentralIdLookup $centralIdLookup,
GlobalBlockLocalStatusLookup $globalBlockLocalStatusLookup,
TempUserConfig $tempUserConfig,
UserFactory $userFactory
) {
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
$this->options = $options;
$this->globalBlockingConnectionProvider = $globalBlockingConnectionProvider;
$this->statsdFactory = $statsdFactory;
$this->centralIdLookup = $centralIdLookup;
$this->globalBlockLocalStatusLookup = $globalBlockLocalStatusLookup;
$this->tempUserConfig = $tempUserConfig;
$this->userFactory = $userFactory;
}
/**
* Given a target and the IP address being used to make the request, get an existing
* GlobalBlock object that applies to the target or IP address being used. If no
* block exists, then this method returns null.
*
* @param User $user Filter for GlobalBlock objects that target this user or IP address
* @param string|null $ip The IP address being used by the user, used to apply global blocks
* on IPs or IP ranges that are not anon-only. Specifying null when $user is an IP address
* and is not the session user will cause the value to be autogenerated.
* @return GlobalBlock|null The GlobalBlock that applies to the given user or IP, or null if no block applies.
*/
public function getUserBlock( User $user, ?string $ip ): ?GlobalBlock {
$details = $this->getUserBlockDetails( $user, $ip );
if ( $details['block'] ) {
$row = $details['block'];
if ( $row->gb_autoblock_parent_id ) {
// If the block is an autoblock, then replace the reason used for the autoblock with the autoblock
// reason but in the content language.
// This means that the user seeing the block notice will get the reason for the block in a language
// that they understand (as opposed to seeing it in English).
$parentBlock = $this->globalBlockingConnectionProvider->getReplicaGlobalBlockingDatabase()
->newSelectQueryBuilder()
->select( [ 'gb_reason', 'gb_target_central_id', 'gb_id' ] )
->from( 'globalblocks' )
->where( [ 'gb_id' => $row->gb_autoblock_parent_id ] )
->caller( __METHOD__ )
->fetchRow();
$row->gb_reason = $this->getAutoblockReason( $parentBlock, true );
}
return GlobalBlock::newFromRow( $row, $details['xff'] );
}
return null;
}
/**
* Get the reason for the autoblock suitable for use in a "globalblocks" table database row, or for
* display to the user in a global auto block notice.
*
* @since 1.43
* @param stdClass $block The globalblock row of the parent block that is causing the autoblock.
* Should include at least the gb_id, gb_target_central_id, and gb_reason fields.
* @param bool $forDisplayInBlockNotice Whether the autoblock reason will be displayed in a block
* notice? If so, then the reason is always set to use the content language of the wiki.
* @return string
*/
public function getAutoblockReason( stdClass $block, bool $forDisplayInBlockNotice ): string {
// Hide the target username of the parent block if it is not publicly viewable. This is to prevent leaking
// hidden usernames. Users can refer to the parent global block via the ID in this case.
$target = $this->centralIdLookup->nameFromCentralId( $block->gb_target_central_id ) ?? '';
if ( $target ) {
$autoBlockReasonMsg = wfMessage( 'globalblocking-autoblocker', $target, $block->gb_reason );
} else {
$autoBlockReasonMsg = wfMessage( 'globalblocking-autoblocker-hidden-block', $block->gb_id );
}
if ( $this->options->get( 'GlobalBlockingCentralWikiContentLanguage' ) && !$forDisplayInBlockNotice ) {
$autoBlockReasonMsg->inLanguage( $this->options->get( 'GlobalBlockingCentralWikiContentLanguage' ) );
} else {
$autoBlockReasonMsg->inContentLanguage();
}
return $autoBlockReasonMsg->plain();
}
/**
* Gets a key to access ->getUserBlockDetailsCache for a given user and IP.
*
* @param UserIdentity $userIdentity
* @param string|null $ip
* @return string The associated cache key
*/
private function getUserBlockDetailsCacheKey( UserIdentity $userIdentity, ?string $ip ): string {
return $userIdentity->getName() . ( $ip ?? '' );
}
/**
* Add the $result to the instance cache under the username of the given $user.
*
* @param array $result
* @param UserIdentity $user
* @param string|null $ip
* @return array The value of $result
*/
private function addToUserBlockDetailsCache( array $result, UserIdentity $user, ?string $ip ): array {
$this->getUserBlockDetailsCache[$this->getUserBlockDetailsCacheKey( $user, $ip )] = $result;
return $result;
}
/**
* Get the cached result of ::getUserBlockDetails for the given user and IP.
*
* @param UserIdentity $userIdentity
* @param string|null $ip
* @return array|null Array if the result is cached, null if there is no cached result
*/
protected function getUserBlockDetailsCacheResult( UserIdentity $userIdentity, ?string $ip ): ?array {
return $this->getUserBlockDetailsCache[$this->getUserBlockDetailsCacheKey( $userIdentity, $ip )] ?? null;
}
/**
* Given a target and the IP address being used to make the request, get the
* most specific block that applies along with a human readable error message
* associated with the block. If no block exists, this returns an array with
* no block and an empty array of error messages.
*
* @param User $user See ::getUserBlock. Note this may not be the session user.
* @param string|null $ip See ::getUserBlock.
* @return array An array with the key 'block' for the DB row of the block that applies. May include a
* xff key if the block was applied due to the X-Forwarded-For header value.
* @phan-return array{block:stdClass|null,xff:bool}
*/
private function getUserBlockDetails( User $user, ?string $ip ): array {
// Check first if the instance cache has the result.
$cachedResult = $this->getUserBlockDetailsCacheResult( $user, $ip );
if ( $cachedResult !== null ) {
return $cachedResult;
}
$this->statsdFactory->increment( 'global_blocking.get_user_block' );
// We have callers from different code paths which may leave $ip as null when providing an
// IP address as the $user where the IP address is not the session user. In this case, populate
// the $ip argument with the IP provided in $user to get all the blocks that apply to the IP.
$context = RequestContext::getMain();
$isSessionUser = $user->equals( $context->getUser() );
if ( $ip === null && !$isSessionUser && IPUtils::isIPAddress( $user->getName() ) ) {
// Populate the IP for checking blocks against non-session users.
$ip = $user->getName();
}
$flags = 0;
if ( $user->isAllowedAny( 'ipblock-exempt', 'globalblock-exempt' ) ) {
// User is exempt from IP blocks.
$flags |= self::SKIP_IP_BLOCKS;
}
if ( $user->isNamed() ) {
// User is a named account, so skip anon-only (soft) IP blocks.
$flags |= self::SKIP_SOFT_IP_BLOCKS;
}
$centralId = 0;
if ( $user->isRegistered() ) {
$centralId = $this->centralIdLookup->centralIdFromLocalUser( $user, CentralIdLookup::AUDIENCE_RAW );
}
$this->statsdFactory->increment( 'global_blocking.get_user_block_db_query' );
$block = $this->getGlobalBlockingBlock( $ip, $centralId, $flags );
if ( $block ) {
return $this->addToUserBlockDetailsCache( [ 'block' => $block, 'xff' => false ], $user, $ip );
}
// We should only check XFF blocks if we are checking blocks for the session user. The exception to this is
// that we should also check XFF blocks when the name is the temporary account placeholder, as this is used
// when a logged out user is making edit on a wiki with temporary accounts enabled (T353564).
if (
$this->options->get( 'GlobalBlockingBlockXFF' ) &&
(
$isSessionUser ||
( $this->tempUserConfig->isEnabled() && $this->userFactory->newTempPlaceholder()->equals( $user ) )
)
) {
$xffIps = $context->getRequest()->getHeader( 'X-Forwarded-For' );
if ( $xffIps ) {
$xffIps = array_map( 'trim', explode( ',', $xffIps ) );
// Always skip the allowed ranges check when checking the XFF IPs as the value of this header
// is easy to spoof.
$xffFlags = $flags | self::SKIP_ALLOWED_RANGES_CHECK;
$block = $this->chooseMostSpecificBlock( $this->checkIpsForBlock( $xffIps, $xffFlags ), $xffFlags );
if ( $block !== null ) {
return $this->addToUserBlockDetailsCache( [ 'block' => $block, 'xff' => true ], $user, $ip );
}
}
}
return $this->addToUserBlockDetailsCache( [ 'block' => null, 'xff' => false ], $user, $ip );
}
/**
* Returns the ::TYPE_* constant for the given block.
*
* @param stdClass $block
* @return int
*/
private function getTargetType( stdClass $block ): int {
if ( $block->gb_autoblock_parent_id ) {
return self::TYPE_AUTOBLOCK;
}
$target = $block->gb_address;
if ( IPUtils::isValid( $target ) ) {
return self::TYPE_IP;
} elseif ( IPUtils::isValidRange( $target ) ) {
return self::TYPE_RANGE;
} else {
return self::TYPE_USER;
}
}
/**
* Choose the most specific block from some combination of user, IP and IP range
* blocks. Decreasing order of specificity: IP > narrower IP range > wider IP
* range. A range that encompasses one IP address is ranked equally to a single IP.
*
* Note that DatabaseBlock::chooseBlocks chooses blocks in a different way.
*
* This is based on DatabaseBlock::chooseMostSpecificBlock
*
* @param IResultWrapper $blocks These should not include autoblocks or ID blocks
* @param int $flags The $flags provided. This method only checks for BLOCK_FLAG_SKIP_LOCAL_DISABLE_CHECK,
* and callers are in charge of checking for other relevant flags.
* @return stdClass|null The block with the most specific target
*/
private function chooseMostSpecificBlock( IResultWrapper $blocks, int $flags ): ?stdClass {
// This result could contain a block on the user, a block on the IP, and a russian-doll
// set of rangeblocks. We want to choose the most specific one, so keep a leader board.
$bestBlock = null;
// Lower will be better
$bestBlockScore = 100;
foreach ( $blocks as $block ) {
// Check for local whitelisting, unless the flag is set to skip the check.
if (
!( $flags & self::SKIP_LOCAL_DISABLE_CHECK ) &&
$this->globalBlockLocalStatusLookup->isGlobalBlockLocallyDisabledForBlockApplication( $block->gb_id )
) {
continue;
}
$target = $block->gb_address;
$type = $this->getTargetType( $block );
if ( $type == self::TYPE_RANGE ) {
// This is the number of bits that are allowed to vary in the block, give
// or take some floating point errors
$max = IPUtils::isIPv6( $target ) ? 128 : 32;
[ $network, $bits ] = IPUtils::parseCIDR( $target );
$size = $max - $bits;
// Rank a range block covering a single IP equally with a single-IP block
$score = self::TYPE_RANGE - 1 + ( $size / $max );
} else {
$score = $type;
}
// Always prioritise blocks that deny account creation over all other blocks.
// This is done by adding the maximum possible score for a single block to the current block score
// (currently the score for an autoblock), such that blocks which don't disable account creation always
// have a higher score to those which disable account creation.
if ( !$block->gb_create_account ) {
$score += self::TYPE_AUTOBLOCK;
}
if ( $bestBlock === null || $score < $bestBlockScore ) {
$bestBlockScore = $score;
$bestBlock = $block;
}
}
return $bestBlock;
}
/**
* Get the most specific row from the `globalblocks` table that applies to the given IP address
* or the central user.
*
* This does not check if the user is exempt from IP blocks. As such it should not be used to determine
* if a block should be applied to a user. Use ::getUserBlock for that.
*
* @param string|null $ip The IP address used by the user. If null, then no IP blocks will be checked.
* @param int $centralId The central ID of the user. 0 if the user is anonymous. Setting this as
* a boolean is soft deprecated and will be treated as 0.
* @param int $flags Flags to control the behavior of the block lookup
* @return stdClass|null The most specific row from the `globalblocks` table, or null if no row was found
*/
public function getGlobalBlockingBlock( ?string $ip, int $centralId, int $flags = 0 ): ?stdClass {
$conds = $this->getGlobalBlockLookupConditions( $ip, $centralId, $flags );
if ( $conds === null ) {
// No conditions, so don't perform the query and assume the user is not targeted by any block
return null;
}
$blocks = $this->globalBlockingConnectionProvider
->getReplicaGlobalBlockingDatabase()
->newSelectQueryBuilder()
->select( self::selectFields() )
->from( 'globalblocks' )
->where( $conds )
->caller( __METHOD__ )
->fetchResultSet();
// Get the most specific block for the global blocks that apply to the user.
return $this->chooseMostSpecificBlock( $blocks, $flags );
}
/**
* Get the SQL WHERE conditions that allow looking up all blocks from the
* `globalblocks` table that apply to the given IP address or range.
*
* @param string $ip The IP address or range
* @deprecated Since 1.42. Use ::getGlobalBlockLookupConditions.
* @return IExpression
*/
public function getRangeCondition( string $ip ): IExpression {
wfDeprecated( __METHOD__, '1.42' );
// This method does not return null if an IP is provided and the allowed ranges check is skipped.
// @phan-suppress-next-line PhanTypeMismatchReturnNullable
return $this->getGlobalBlockLookupConditions( $ip, 0, self::SKIP_ALLOWED_RANGES_CHECK );
}
/**
* Get the SQL WHERE conditions that allow looking up all blocks from the `globalblocks` table.
*
* @param ?string $ip The IP address or range. If null, then no IP blocks will be checked.
* @param int $centralId The central ID of the user. 0 if the user is anonymous and 0 will skip
* checking user specific blocks.
* @param int $flags Flags which control what conditions are returned. Ignores the
* ::BLOCK_FLAG_SKIP_LOCAL_DISABLE_CHECK flag and callers are expected to check if the block is
* locally disabled if this is needed.
* @return IExpression|null The conditions to be used in a SQL query to look up global blocks, or null if no valid
* conditions could be generated.
*/
public function getGlobalBlockLookupConditions( ?string $ip, int $centralId = 0, int $flags = 0 ): ?IExpression {
$dbr = $this->globalBlockingConnectionProvider->getReplicaGlobalBlockingDatabase();
$ipExpr = null;
$userExpr = null;
if ( $ip !== null ) {
$sanitisedIp = IPUtils::sanitizeIP( $ip );
if ( !IPUtils::isIPAddress( $ip ) || !$sanitisedIp ) {
// The provided IP is invalid, so throw.
throw new InvalidArgumentException(
"Invalid IP address or range provided to GlobalBlockLookup::getGlobalBlockLookupConditions."
);
}
// Use the sanitised version of the IP address, incase an IPv4 address is provided that has leading 0s.
// If leading 0s are present, then IPUtils::parseRange will fail to parse the range properly.
$ip = $sanitisedIp;
}
if ( $ip !== null && !( $flags & self::SKIP_ALLOWED_RANGES_CHECK ) ) {
$ranges = $this->options->get( 'GlobalBlockingAllowedRanges' );
foreach ( $ranges as $range ) {
if ( IPUtils::isInRange( $ip, $range ) ) {
// IP is in a range that is exempt from IP blocks, so treat the user as having
// global IP block exemption for this specific IP address
$flags |= self::SKIP_IP_BLOCKS;
break;
}
}
}
if ( $ip !== null && !( $flags & self::SKIP_IP_BLOCKS ) ) {
// If we have been provided an IP address or range in $ip, then
// add conditions to the query to lookup blocks that apply to the IP address / range.
[ $start, $end ] = IPUtils::parseRange( $ip );
$chunk = $this->getIpFragment( $start );
$ipExpr = $dbr->expr( 'gb_range_start', IExpression::LIKE, new LikeValue( $chunk, $dbr->anyString() ) )
->and( 'gb_range_start', '<=', $start )
->and( 'gb_range_end', '>=', $end );
if ( $flags & self::SKIP_SOFT_IP_BLOCKS ) {
// If the flags say to skip soft IP blocks, then exclude blocks with gb_anon_only
// set to 1 (which should only be soft blocks on IP addresses or ranges).
$ipExpr = $ipExpr->and( 'gb_anon_only', '!=', 1 );
}
if ( $flags & self::SKIP_AUTOBLOCKS ) {
$ipExpr = $ipExpr->and( 'gb_autoblock_parent_id', '=', 0 );
}
}
if ( $centralId !== 0 ) {
// If we have been provided a non-zero central ID, then also look for blocks that target the
// given central ID.
$userExpr = $dbr->expr( 'gb_target_central_id', '=', $centralId );
}
// Combine the IP conditions and user IExpressions
if ( $userExpr !== null && $ipExpr !== null ) {
// If we have conditions for both the IP and the user, then combine them with an OR
// to allow selecting blocks that apply to either the IP or the user.
$targetExpr = $userExpr->orExpr( $ipExpr );
} elseif ( $userExpr !== null ) {
// If we only have conditions for the user, then use that IExpression.
$targetExpr = $userExpr;
} elseif ( $ipExpr !== null ) {
// If we only have conditions for the IP, then use that IExpression.
$targetExpr = $ipExpr;
} else {
// No conditions, so don't perform the query otherwise we will select all blocks from the DB.
// In this case, we can assume the user or their IP is not affected by any global block.
return null;
}
// @todo expiry shouldn't be in this function
return $dbr->expr( 'gb_expiry', '>', $dbr->timestamp() )
->andExpr( $targetExpr );
}
/**
* Get the component of an IP address which is certain to be the same between an IP
* address and a range block containing that IP address.
*
* This mostly duplicates the logic in DatabaseStoreBlock::getIpFragment, but with the
* CIDR limit config being the GlobalBlocking extension specific one.
*
* @param string $hex Hexadecimal IP representation
* @return string
*/
private function getIpFragment( string $hex ): string {
$blockCIDRLimit = $this->options->get( 'GlobalBlockingCIDRLimit' );
if ( str_starts_with( $hex, 'v6-' ) ) {
return 'v6-' . substr( substr( $hex, 3 ), 0, (int)floor( $blockCIDRLimit['IPv6'] / 4 ) );
} else {
return substr( $hex, 0, (int)floor( $blockCIDRLimit['IPv4'] / 4 ) );
}
}
/**
* Find all rows from the `globalblocks` table that target at least one of
* the given IP addresses.
*
* Does not filter out locally disabled blocks. You probably want to pass the
* result to {@link self::chooseMostSpecificBlock}.
*
* @param string[] $ips The array of IP addresses to be checked
* @param int $flags Flags which control what blocks are returned.
* @return IResultWrapper Applicable blocks as rows from the `globalblocks` table
*/
private function checkIpsForBlock( array $ips, int $flags = 0 ): IResultWrapper {
if ( $flags & self::SKIP_IP_BLOCKS ) {
// If the flags say to skip IP blocks, then don't even make the query.
return new FakeResultWrapper( [] );
}
$dbr = $this->globalBlockingConnectionProvider->getReplicaGlobalBlockingDatabase();
$conds = [];
foreach ( $ips as $ip ) {
if ( IPUtils::isValid( $ip ) ) {
$ipConds = $this->getGlobalBlockLookupConditions( $ip, 0, $flags );
if ( $ipConds !== null ) {
$conds[] = $ipConds;
}
}
}
if ( !$conds ) {
// No valid IPs provided so don't even make the query. Bug 59705
return new FakeResultWrapper( [] );
}
return $dbr->newSelectQueryBuilder()
->select( self::selectFields() )
->from( 'globalblocks' )
->where( $dbr->orExpr( $conds ) )
->caller( __METHOD__ )
->fetchResultSet();
}
/**
* Given a specific target, find the ID for the global block that applies to it.
* If no global block exists for this target, then this method returns 0.
*
* @param string $target The specific target which can be a username, IP address, range, or global block ID that
* may or may not exist. The target being specific means that if you provide a single IP which is covered by a
* range block, the range block will not be returned. This also means that autoblocks cannot be queried by the
* IP that they target. Use ::getGlobalBlockingBlock to include these blocks.
* @param int $dbtype Either DB_REPLICA or DB_PRIMARY.
* @return int
*/
public function getGlobalBlockId( string $target, int $dbtype = DB_REPLICA ): int {
if ( $dbtype === DB_PRIMARY ) {
$db = $this->globalBlockingConnectionProvider->getPrimaryGlobalBlockingDatabase();
} else {
$db = $this->globalBlockingConnectionProvider->getReplicaGlobalBlockingDatabase();
}
$queryBuilder = $db->newSelectQueryBuilder()
->select( 'gb_id' )
->from( 'globalblocks' )
->where( $db->expr( 'gb_expiry', '>', $db->timestamp() ) );
$globalBlockId = self::isAGlobalBlockId( $target );
if ( $globalBlockId ) {
$queryBuilder->where( [ 'gb_id' => $globalBlockId ] );
} elseif ( IPUtils::isIPAddress( $target ) ) {
$queryBuilder->where( [ 'gb_address' => $target, 'gb_autoblock_parent_id' => 0 ] );
} else {
$centralId = $this->centralIdLookup->centralIdFromName( $target, CentralIdLookup::AUDIENCE_RAW );
if ( !$centralId ) {
// If we are looking up a block by a central ID of a user, then the user must have a central ID
// for a block to apply to them.
return 0;
}
$queryBuilder->where( [ 'gb_target_central_id' => $centralId ] );
}
return (int)$queryBuilder
->caller( __METHOD__ )
->fetchField();
}
/**
* Determines if a given string is in the format of a global block ID.
*
* This method does not validate that the global block ID actually exists. Use
* {@link GlobalBlockLookup::getGlobalBlockId} for that.
*
* @param string $target The string to check
* @return int|false False if the string is not in the format of a global block ID, or the ID of the global
* block if it is in the format of a global block ID.
*/
public static function isAGlobalBlockId( string $target ) {
$isTargetABlockId = preg_match( '/^#\d+$/', $target );
if ( $isTargetABlockId ) {
return intval( substr( $target, 1 ) );
}
return false;
}
/**
* @return string[] The fields needed to construct a GlobalBlock object
*/
public static function selectFields(): array {
return [
'gb_id', 'gb_address', 'gb_target_central_id', 'gb_by_central_id', 'gb_by_wiki', 'gb_reason',
'gb_timestamp', 'gb_anon_only', 'gb_expiry', 'gb_range_start', 'gb_range_end', 'gb_create_account',
'gb_enable_autoblock', 'gb_autoblock_parent_id',
];
}
}

File Metadata

Mime Type
text/x-php
Expires
Sat, May 16, 21:29 (1 d, 5 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
47/8e/7496cfc8b98c2158152e6f721249
Default Alt Text
GlobalBlockLookup.php (24 KB)

Event Timeline