1<?php 2 3/** 4 * This program is free software; you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation; either version 2 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License along 15 * with this program; if not, write to the Free Software Foundation, Inc., 16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 * http://www.gnu.org/copyleft/gpl.html 18 * 19 * @file 20 */ 21 22namespace MediaWiki\Block; 23 24use MediaWiki\Config\ServiceOptions; 25use MediaWiki\User\UserIdentity; 26use MediaWiki\User\UserIdentityLookup; 27use MediaWiki\User\UserIdentityValue; 28use MediaWiki\User\UserNameUtils; 29use Status; 30use Wikimedia\IPUtils; 31 32/** 33 * Backend class for blocking utils 34 * 35 * This service should contain any methods that are useful 36 * to more than one blocking-related class and doesn't fit any 37 * other service. 38 * 39 * For now, this includes only 40 * - block target parsing 41 * - block target validation 42 * 43 * @since 1.36 44 */ 45class BlockUtils { 46 /** @var ServiceOptions */ 47 private $options; 48 49 /** @var UserIdentityLookup */ 50 private $userIdentityLookup; 51 52 /** @var UserNameUtils */ 53 private $userNameUtils; 54 55 /** 56 * @internal Only for use by ServiceWiring 57 */ 58 public const CONSTRUCTOR_OPTIONS = [ 59 'BlockCIDRLimit', 60 ]; 61 62 /** 63 * @param ServiceOptions $options 64 * @param UserIdentityLookup $userIdentityLookup 65 * @param UserNameUtils $userNameUtils 66 */ 67 public function __construct( 68 ServiceOptions $options, 69 UserIdentityLookup $userIdentityLookup, 70 UserNameUtils $userNameUtils 71 ) { 72 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); 73 $this->options = $options; 74 $this->userIdentityLookup = $userIdentityLookup; 75 $this->userNameUtils = $userNameUtils; 76 } 77 78 /** 79 * From an existing block, get the target and the type of target. 80 * 81 * Note that, except for null, it is always safe to treat the target 82 * as a string; for UserIdentityValue objects this will return 83 * UserIdentityValue::__toString() which in turn gives 84 * UserIdentityValue::getName(). 85 * 86 * If the type is not null, it will be an AbstractBlock::TYPE_ constant. 87 * 88 * @param string|UserIdentity|null $target 89 * @return array [ UserIdentity|String|null, int|null ] 90 */ 91 public function parseBlockTarget( $target ): array { 92 // We may have been through this before 93 if ( $target instanceof UserIdentity ) { 94 if ( IPUtils::isValid( $target->getName() ) ) { 95 return [ $target, AbstractBlock::TYPE_IP ]; 96 } else { 97 return [ $target, AbstractBlock::TYPE_USER ]; 98 } 99 } elseif ( $target === null ) { 100 return [ null, null ]; 101 } 102 103 $target = trim( $target ); 104 105 if ( IPUtils::isValid( $target ) ) { 106 return [ 107 UserIdentityValue::newAnonymous( IPUtils::sanitizeIP( $target ) ), 108 AbstractBlock::TYPE_IP 109 ]; 110 111 } elseif ( IPUtils::isValidRange( $target ) ) { 112 // Can't create a UserIdentity from an IP range 113 return [ IPUtils::sanitizeRange( $target ), AbstractBlock::TYPE_RANGE ]; 114 } 115 116 // Consider the possibility that this is not a username at all 117 // but actually an old subpage (T31797) 118 if ( strpos( $target, '/' ) !== false ) { 119 // An old subpage, drill down to the user behind it 120 $target = explode( '/', $target )[0]; 121 } 122 123 if ( preg_match( '/^#\d+$/', $target ) ) { 124 // Autoblock reference in the form "#12345" 125 return [ substr( $target, 1 ), AbstractBlock::TYPE_AUTO ]; 126 } 127 128 $userFromDB = $this->userIdentityLookup->getUserIdentityByName( $target ); 129 if ( $userFromDB instanceof UserIdentity ) { 130 // Note that since numbers are valid usernames, a $target of "12345" will be 131 // considered a UserIdentity. If you want to pass a block ID, prepend a hash "#12345", 132 // since hash characters are not valid in usernames or titles generally. 133 return [ $userFromDB, AbstractBlock::TYPE_USER ]; 134 } 135 136 // TODO: figure out if it makes sense to have users that do not exist in the DB here 137 $canonicalName = $this->userNameUtils->getCanonical( $target ); 138 if ( $canonicalName ) { 139 return [ 140 new UserIdentityValue( 0, $canonicalName ), 141 AbstractBlock::TYPE_USER 142 ]; 143 } 144 145 return [ null, null ]; 146 } 147 148 /** 149 * Validate block target 150 * 151 * @param string|UserIdentity $value 152 * 153 * @return Status 154 */ 155 public function validateTarget( $value ): Status { 156 list( $target, $type ) = $this->parseBlockTarget( $value ); 157 158 $status = Status::newGood( $target ); 159 160 switch ( $type ) { 161 case AbstractBlock::TYPE_USER: 162 if ( !$target->isRegistered() ) { 163 $status->fatal( 164 'nosuchusershort', 165 wfEscapeWikiText( $target->getName() ) 166 ); 167 } 168 break; 169 170 case AbstractBlock::TYPE_RANGE: 171 list( $ip, $range ) = explode( '/', $target, '2' ); 172 173 if ( IPUtils::isIPv4( $ip ) ) { 174 $status->merge( $this->validateIPv4Range( $range ) ); 175 } elseif ( IPUtils::isIPv6( $ip ) ) { 176 $status->merge( $this->validateIPv6Range( $range ) ); 177 } else { 178 // Something is FUBAR 179 $status->fatal( 'badipaddress' ); 180 } 181 break; 182 183 case AbstractBlock::TYPE_IP: 184 // All is well 185 break; 186 187 default: 188 $status->fatal( 'badipaddress' ); 189 break; 190 } 191 192 return $status; 193 } 194 195 /** 196 * Validate an IPv4 range 197 * 198 * @param int $range 199 * 200 * @return Status 201 */ 202 private function validateIPv4Range( int $range ): Status { 203 $status = Status::newGood(); 204 $blockCIDRLimit = $this->options->get( 'BlockCIDRLimit' ); 205 206 if ( $blockCIDRLimit['IPv4'] == 32 ) { 207 // Range block effectively disabled 208 $status->fatal( 'range_block_disabled' ); 209 } elseif ( $range > 32 ) { 210 // Such a range cannot exist 211 $status->fatal( 'ip_range_invalid' ); 212 } elseif ( $range < $blockCIDRLimit['IPv4'] ) { 213 $status->fatal( 'ip_range_toolarge', $blockCIDRLimit['IPv4'] ); 214 } 215 216 return $status; 217 } 218 219 /** 220 * Validate an IPv6 range 221 * 222 * @param int $range 223 * 224 * @return Status 225 */ 226 private function validateIPv6Range( int $range ): Status { 227 $status = Status::newGood(); 228 $blockCIDRLimit = $this->options->get( 'BlockCIDRLimit' ); 229 230 if ( $blockCIDRLimit['IPv6'] == 128 ) { 231 // Range block effectively disabled 232 $status->fatal( 'range_block_disabled' ); 233 } elseif ( $range > 128 ) { 234 // Dodgy range - such a range cannot exist 235 $status->fatal( 'ip_range_invalid' ); 236 } elseif ( $range < $blockCIDRLimit['IPv6'] ) { 237 $status->fatal( 'ip_range_toolarge', $blockCIDRLimit['IPv6'] ); 238 } 239 240 return $status; 241 } 242} 243