Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
31.76% covered (danger)
31.76%
27 / 85
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
BlockUtils
31.76% covered (danger)
31.76%
27 / 85
16.67% covered (danger)
16.67%
1 / 6
378.98
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 parseBlockTarget
84.62% covered (warning)
84.62%
22 / 26
0.00% covered (danger)
0.00%
0 / 1
9.29
 parseBlockTargetRow
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 validateTarget
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
72
 validateIPv4Range
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 validateIPv6Range
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
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\MainConfigNames;
26use MediaWiki\Status\Status;
27use MediaWiki\User\UserIdentity;
28use MediaWiki\User\UserIdentityLookup;
29use MediaWiki\User\UserIdentityValue;
30use MediaWiki\User\UserNameUtils;
31use Wikimedia\IPUtils;
32
33/**
34 * Backend class for blocking utils
35 *
36 * This service should contain any methods that are useful
37 * to more than one blocking-related class and don't fit any
38 * other service.
39 *
40 * For now, this includes only
41 * - block target parsing
42 * - block target validation
43 * - parsing the target and type of a block in the database
44 *
45 * @since 1.36
46 */
47class BlockUtils {
48    /** @var ServiceOptions */
49    private $options;
50
51    /** @var UserIdentityLookup */
52    private $userIdentityLookup;
53
54    /** @var UserNameUtils */
55    private $userNameUtils;
56
57    /** @var string|false */
58    private $wikiId;
59
60    /**
61     * @internal Only for use by ServiceWiring
62     */
63    public const CONSTRUCTOR_OPTIONS = [
64        MainConfigNames::BlockCIDRLimit,
65    ];
66
67    /**
68     * @param ServiceOptions $options
69     * @param UserIdentityLookup $userIdentityLookup
70     * @param UserNameUtils $userNameUtils
71     * @param string|false $wikiId
72     */
73    public function __construct(
74        ServiceOptions $options,
75        UserIdentityLookup $userIdentityLookup,
76        UserNameUtils $userNameUtils,
77        $wikiId = Block::LOCAL
78    ) {
79        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
80        $this->options = $options;
81        $this->userIdentityLookup = $userIdentityLookup;
82        $this->userNameUtils = $userNameUtils;
83        $this->wikiId = $wikiId;
84    }
85
86    /**
87     * From string specification or UserIdentity, get the block target and the
88     * type of target.
89     *
90     * Note that, except for null, it is always safe to treat the target
91     * as a string; for UserIdentityValue objects this will return
92     * UserIdentityValue::__toString() which in turn gives
93     * UserIdentityValue::getName().
94     *
95     * If the type is not null, it will be an AbstractBlock::TYPE_ constant.
96     *
97     * Since 1.42, it is no longer safe to pass a value from the database field
98     * ipb_address/bt_address to this method, since the username is normalized.
99     * Use parseBlockTargetRow() instead. (T346683)
100     *
101     * @param string|UserIdentity|null $target
102     * @return array [ UserIdentity|String|null, int|null ]
103     */
104    public function parseBlockTarget( $target ): array {
105        // We may have been through this before
106        if ( $target instanceof UserIdentity ) {
107            if ( IPUtils::isValid( $target->getName() ) ) {
108                return [ $target, AbstractBlock::TYPE_IP ];
109            } else {
110                return [ $target, AbstractBlock::TYPE_USER ];
111            }
112        } elseif ( $target === null ) {
113            return [ null, null ];
114        }
115
116        $target = trim( $target );
117
118        if ( IPUtils::isValid( $target ) ) {
119            return [
120                UserIdentityValue::newAnonymous( IPUtils::sanitizeIP( $target ), $this->wikiId ),
121                AbstractBlock::TYPE_IP
122            ];
123
124        } elseif ( IPUtils::isValidRange( $target ) ) {
125            // Can't create a UserIdentity from an IP range
126            return [ IPUtils::sanitizeRange( $target ), AbstractBlock::TYPE_RANGE ];
127        }
128
129        if ( preg_match( '/^#\d+$/', $target ) ) {
130            // Autoblock reference in the form "#12345"
131            return [ substr( $target, 1 ), AbstractBlock::TYPE_AUTO ];
132        }
133
134        $userFromDB = $this->userIdentityLookup->getUserIdentityByName( $target );
135        if ( $userFromDB instanceof UserIdentity ) {
136            // Note that since numbers are valid usernames, a $target of "12345" will be
137            // considered a UserIdentity. If you want to pass a block ID, prepend a hash "#12345",
138            // since hash characters are not valid in usernames or titles generally.
139            return [ $userFromDB, AbstractBlock::TYPE_USER ];
140        }
141
142        // Wrap the invalid user in a UserIdentityValue.
143        // This allows validateTarget() to return a "nosuchusershort" message,
144        // which is needed for Special:Block.
145        $canonicalName = $this->userNameUtils->getCanonical( $target );
146        if ( $canonicalName !== false ) {
147            return [
148                new UserIdentityValue( 0, $canonicalName ),
149                AbstractBlock::TYPE_USER
150            ];
151        }
152
153        return [ null, null ];
154    }
155
156    /**
157     * From a row which must contain bt_auto, bt_user, bt_address and bl_id,
158     * and optionally bt_user_text, determine the block target and type.
159     *
160     * @since 1.42
161     * @param \stdClass $row
162     * @return array [ UserIdentity|String|null, int|null ]
163     */
164    public function parseBlockTargetRow( $row ) {
165        if ( $row->bt_auto ) {
166            return [ $row->bl_id, AbstractBlock::TYPE_AUTO ];
167        } elseif ( isset( $row->bt_user ) ) {
168            if ( isset( $row->bt_user_text ) ) {
169                $user = new UserIdentityValue( $row->bt_user, $row->bt_user_text, $this->wikiId );
170            } else {
171                $user = $this->userIdentityLookup->getUserIdentityByUserId( $row->bt_user );
172            }
173            return [ $user, AbstractBlock::TYPE_USER ];
174        } elseif ( $row->bt_address === null ) {
175            return [ null, null ];
176        } elseif ( IPUtils::isValid( $row->bt_address ) ) {
177            return [
178                UserIdentityValue::newAnonymous( IPUtils::sanitizeIP( $row->bt_address ), $this->wikiId ),
179                AbstractBlock::TYPE_IP
180            ];
181        } elseif ( IPUtils::isValidRange( $row->bt_address ) ) {
182            // Can't create a UserIdentity from an IP range
183            return [ IPUtils::sanitizeRange( $row->bt_address ), AbstractBlock::TYPE_RANGE ];
184        } else {
185            return [ null, null ];
186        }
187    }
188
189    /**
190     * Validate block target
191     *
192     * @param string|UserIdentity $value
193     *
194     * @return Status
195     */
196    public function validateTarget( $value ): Status {
197        [ $target, $type ] = $this->parseBlockTarget( $value );
198
199        $status = Status::newGood( $target );
200
201        switch ( $type ) {
202            case AbstractBlock::TYPE_USER:
203                if ( !$target->isRegistered() ) {
204                    $status->fatal(
205                        'nosuchusershort',
206                        wfEscapeWikiText( $target->getName() )
207                    );
208                }
209                break;
210
211            case AbstractBlock::TYPE_RANGE:
212                [ $ip, $range ] = explode( '/', $target, 2 );
213
214                if ( IPUtils::isIPv4( $ip ) ) {
215                    $status->merge( $this->validateIPv4Range( (int)$range ) );
216                } elseif ( IPUtils::isIPv6( $ip ) ) {
217                    $status->merge( $this->validateIPv6Range( (int)$range ) );
218                } else {
219                    // Something is FUBAR
220                    $status->fatal( 'badipaddress' );
221                }
222                break;
223
224            case AbstractBlock::TYPE_IP:
225                // All is well
226                break;
227
228            default:
229                $status->fatal( 'badipaddress' );
230                break;
231        }
232
233        return $status;
234    }
235
236    /**
237     * Validate an IPv4 range
238     *
239     * @param int $range
240     *
241     * @return Status
242     */
243    private function validateIPv4Range( int $range ): Status {
244        $status = Status::newGood();
245        $blockCIDRLimit = $this->options->get( MainConfigNames::BlockCIDRLimit );
246
247        if ( $blockCIDRLimit['IPv4'] == 32 ) {
248            // Range block effectively disabled
249            $status->fatal( 'range_block_disabled' );
250        } elseif ( $range > 32 ) {
251            // Such a range cannot exist
252            $status->fatal( 'ip_range_invalid' );
253        } elseif ( $range < $blockCIDRLimit['IPv4'] ) {
254            $status->fatal( 'ip_range_toolarge', $blockCIDRLimit['IPv4'] );
255        }
256
257        return $status;
258    }
259
260    /**
261     * Validate an IPv6 range
262     *
263     * @param int $range
264     *
265     * @return Status
266     */
267    private function validateIPv6Range( int $range ): Status {
268        $status = Status::newGood();
269        $blockCIDRLimit = $this->options->get( MainConfigNames::BlockCIDRLimit );
270
271        if ( $blockCIDRLimit['IPv6'] == 128 ) {
272            // Range block effectively disabled
273            $status->fatal( 'range_block_disabled' );
274        } elseif ( $range > 128 ) {
275            // Dodgy range - such a range cannot exist
276            $status->fatal( 'ip_range_invalid' );
277        } elseif ( $range < $blockCIDRLimit['IPv6'] ) {
278            $status->fatal( 'ip_range_toolarge', $blockCIDRLimit['IPv6'] );
279        }
280
281        return $status;
282    }
283}