Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
74.36% covered (warning)
74.36%
58 / 78
58.33% covered (warning)
58.33%
7 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
BlockTargetFactory
74.36% covered (warning)
74.36%
58 / 78
58.33% covered (warning)
58.33%
7 / 12
57.85
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
 getWikiId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newFromString
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
7
 newFromUser
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 newFromLegacyUnion
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 newFromRowRaw
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newFromRowRedacted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newFromRowInternal
65.00% covered (warning)
65.00%
13 / 20
0.00% covered (danger)
0.00%
0 / 1
14.29
 newAutoBlockTarget
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newUserBlockTarget
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 newAnonIpBlockTarget
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 newRangeBlockTarget
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
1<?php
2
3namespace MediaWiki\Block;
4
5use InvalidArgumentException;
6use MediaWiki\Config\ServiceOptions;
7use MediaWiki\DAO\WikiAwareEntity;
8use MediaWiki\DAO\WikiAwareEntityTrait;
9use MediaWiki\MainConfigNames;
10use MediaWiki\User\UserIdentity;
11use MediaWiki\User\UserIdentityLookup;
12use MediaWiki\User\UserIdentityValue;
13use MediaWiki\User\UserNameUtils;
14use RuntimeException;
15use stdClass;
16use Wikimedia\IPUtils;
17use Wikimedia\Rdbms\IDBAccessObject;
18
19/**
20 * Factory for BlockTarget objects
21 *
22 * @since 1.44
23 */
24class BlockTargetFactory implements WikiAwareEntity {
25    use WikiAwareEntityTrait;
26
27    private UserIdentityLookup $userIdentityLookup;
28    private UserNameUtils $userNameUtils;
29
30    /** @var string|false */
31    private $wikiId;
32
33    /**
34     * @var array The range block minimum prefix lengths indexed by protocol (IPv4 or IPv6)
35     */
36    private $rangePrefixLimits;
37
38    /**
39     * @internal Only for use by ServiceWiring
40     */
41    public const CONSTRUCTOR_OPTIONS = [
42        MainConfigNames::BlockCIDRLimit,
43    ];
44
45    /**
46     * @param ServiceOptions $options
47     * @param UserIdentityLookup $userIdentityLookup
48     * @param UserNameUtils $userNameUtils
49     * @param string|false $wikiId
50     */
51    public function __construct(
52        ServiceOptions $options,
53        UserIdentityLookup $userIdentityLookup,
54        UserNameUtils $userNameUtils,
55        /* string|false */ $wikiId = Block::LOCAL
56    ) {
57        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
58        $this->rangePrefixLimits = $options->get( MainConfigNames::BlockCIDRLimit );
59        $this->userIdentityLookup = $userIdentityLookup;
60        $this->userNameUtils = $userNameUtils;
61        $this->wikiId = $wikiId;
62    }
63
64    public function getWikiId() {
65        return $this->wikiId;
66    }
67
68    /**
69     * Try to create a block target from a user input string.
70     *
71     * @param string|null $str
72     * @return BlockTarget|null
73     */
74    public function newFromString( ?string $str ): ?BlockTarget {
75        if ( $str === null ) {
76            return null;
77        }
78
79        $str = trim( $str );
80
81        if ( IPUtils::isValid( $str ) ) {
82            return new AnonIpBlockTarget( IPUtils::sanitizeIP( $str ), $this->wikiId );
83        } elseif ( IPUtils::isValidRange( $str ) ) {
84            return new RangeBlockTarget(
85                IPUtils::sanitizeRange( $str ),
86                $this->rangePrefixLimits,
87                $this->wikiId
88            );
89        }
90
91        if ( preg_match( '/^#\d+$/', $str ) ) {
92            // Autoblock reference in the form "#12345"
93            return new AutoBlockTarget(
94                (int)substr( $str, 1 ),
95                $this->wikiId
96            );
97        }
98
99        $userFromDB = $this->userIdentityLookup->getUserIdentityByName( $str );
100        if ( $userFromDB instanceof UserIdentity ) {
101            return new UserBlockTarget( $userFromDB );
102        }
103
104        // Wrap the invalid user in a UserIdentityValue.
105        // This allows validateTarget() to return a "nosuchusershort" message,
106        // which is needed for Special:Block.
107        $canonicalName = $this->userNameUtils->getCanonical( $str );
108        if ( $canonicalName !== false ) {
109            return new UserBlockTarget( new UserIdentityValue( 0, $canonicalName ) );
110        }
111
112        return null;
113    }
114
115    /**
116     * Create a BlockTarget from a UserIdentity, which may refer to a
117     * registered user, an IP address or range.
118     *
119     * @param UserIdentity $user
120     * @return BlockTarget
121     */
122    public function newFromUser( UserIdentity $user ): BlockTarget {
123        $this->assertWiki( $user->getWikiId() );
124        $name = $user->getName();
125        if ( $user->getId( $this->wikiId ) !== 0 ) {
126            // We'll trust the caller and skip IP validity checks
127            return new UserBlockTarget( $user );
128        } elseif ( IPUtils::isValidRange( $name ) ) {
129            return $this->newRangeBlockTarget( IPUtils::sanitizeRange( $name ) );
130        } elseif ( IPUtils::isValid( $name ) ) {
131            return $this->newAnonIpBlockTarget( $name );
132        } else {
133            return new UserBlockTarget( $user );
134        }
135    }
136
137    /**
138     * Try to create a BlockTarget from a UserIdentity|string|null, a union type
139     * previously used as a target by various methods.
140     *
141     * @param UserIdentity|string|null $union
142     * @return BlockTarget|null
143     */
144    public function newFromLegacyUnion( $union ): ?BlockTarget {
145        if ( $union instanceof UserIdentity ) {
146            if ( IPUtils::isValid( $union->getName() ) ) {
147                return new AnonIpBlockTarget( $union->getName(), $this->wikiId );
148            } else {
149                return new UserBlockTarget( $union );
150            }
151        } elseif ( is_string( $union ) ) {
152            return $this->newFromString( $union );
153        } else {
154            return null;
155        }
156    }
157
158    /**
159     * Try to create a BlockTarget from a row which must contain bt_user,
160     * bt_address and optionally bt_user_text.
161     *
162     * bt_auto is ignored, so this is suitable for permissions and for block
163     * creation but not for display.
164     *
165     * @param stdClass $row
166     * @return BlockTarget|null
167     */
168    public function newFromRowRaw( $row ): ?BlockTarget {
169        return $this->newFromRowInternal( $row, false );
170    }
171
172    /**
173     * Try to create a BlockTarget from a row which must contain bt_auto,
174     * bt_user, bt_address and bl_id, and optionally bt_user_text.
175     *
176     * If bt_auto is set, the address will be redacted to avoid disclosing it.
177     * The ID will be wrapped in an AutoblockTarget.
178     *
179     * @param stdClass $row
180     * @return BlockTarget|null
181     */
182    public function newFromRowRedacted( $row ): ?BlockTarget {
183        return $this->newFromRowInternal( $row, true );
184    }
185
186    /**
187     * @param stdClass $row
188     * @param bool $redact
189     * @return BlockTarget|null
190     */
191    private function newFromRowInternal( $row, $redact ): ?BlockTarget {
192        if ( $redact && $row->bt_auto ) {
193            return $this->newAutoBlockTarget( $row->bl_id );
194        } elseif ( isset( $row->bt_user ) ) {
195            if ( isset( $row->bt_user_text ) ) {
196                $user = new UserIdentityValue( $row->bt_user, $row->bt_user_text, $this->wikiId );
197            } else {
198                $user = $this->userIdentityLookup->getUserIdentityByUserId( $row->bt_user );
199                if ( !$user ) {
200                    $user = $this->userIdentityLookup->getUserIdentityByUserId(
201                        $row->bt_user, IDBAccessObject::READ_LATEST );
202                    if ( !$user ) {
203                        throw new RuntimeException(
204                            "Unable to find name for user ID {$row->bt_user}" );
205                    }
206                }
207            }
208            return new UserBlockTarget( $user );
209        } elseif ( $row->bt_address === null ) {
210            return null;
211        } elseif ( IPUtils::isValid( $row->bt_address ) ) {
212            return $this->newAnonIpBlockTarget( IPUtils::sanitizeIP( $row->bt_address ) );
213        } elseif ( IPUtils::isValidRange( $row->bt_address ) ) {
214            return $this->newRangeBlockTarget( IPUtils::sanitizeRange( $row->bt_address ) );
215        } else {
216            return null;
217        }
218    }
219
220    /**
221     * Create an AutoBlockTarget for the given ID
222     *
223     * A simple constructor proxy for pre-validated input.
224     *
225     * @param int $id
226     * @return AutoBlockTarget
227     */
228    public function newAutoBlockTarget( int $id ): AutoBlockTarget {
229        return new AutoBlockTarget( $id, $this->wikiId );
230    }
231
232    /**
233     * Create a UserBlockTarget for the given user.
234     *
235     * A simple constructor proxy for pre-validated input.
236     *
237     * The user must be a real registered user. Use newFromUser() to create a
238     * block target from a UserIdentity which may represent an IP address.
239     *
240     * @param UserIdentity $user
241     * @return UserBlockTarget
242     */
243    public function newUserBlockTarget( UserIdentity $user ): UserBlockTarget {
244        $this->assertWiki( $user->getWikiId() );
245        if ( IPUtils::isValid( $user->getName() ) ) {
246            throw new InvalidArgumentException( 'IP address passed to newUserBlockTarget' );
247        }
248        return new UserBlockTarget( $user );
249    }
250
251    /**
252     * Create an IP block target
253     *
254     * A simple constructor proxy for pre-validated input.
255     *
256     * @param string $ip
257     * @return AnonIpBlockTarget
258     */
259    public function newAnonIpBlockTarget( string $ip ): AnonIpBlockTarget {
260        if ( !IPUtils::isValid( $ip ) ) {
261            throw new InvalidArgumentException( 'Invalid IP address for block target' );
262        }
263        return new AnonIpBlockTarget( $ip, $this->wikiId );
264    }
265
266    /**
267     * Create a range block target.
268     *
269     * A simple constructor proxy for pre-validated input.
270     *
271     * @param string $cidr
272     * @return RangeBlockTarget
273     */
274    public function newRangeBlockTarget( string $cidr ): RangeBlockTarget {
275        if ( !IPUtils::isValidRange( $cidr ) ) {
276            throw new InvalidArgumentException( 'Invalid IP range for block target' );
277        }
278        return new RangeBlockTarget( $cidr, $this->rangePrefixLimits, $this->wikiId );
279    }
280}