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