Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
53.66% covered (warning)
53.66%
88 / 164
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialUnblock
53.99% covered (warning)
53.99%
88 / 163
0.00% covered (danger)
0.00%
0 / 8
162.25
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
68.35% covered (warning)
68.35%
54 / 79
0.00% covered (danger)
0.00%
0 / 1
22.13
 getTargetAndType
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 getTargetUserTitle
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 getFields
62.96% covered (warning)
62.96%
34 / 54
0.00% covered (danger)
0.00%
0 / 1
13.12
 prefixSearchSubpages
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Implements Special:Unblock
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup SpecialPage
22 */
23
24namespace MediaWiki\Specials;
25
26use LogEventsList;
27use MediaWiki\Block\Block;
28use MediaWiki\Block\BlockUtils;
29use MediaWiki\Block\DatabaseBlockStore;
30use MediaWiki\Block\UnblockUserFactory;
31use MediaWiki\HTMLForm\HTMLForm;
32use MediaWiki\Request\WebRequest;
33use MediaWiki\SpecialPage\SpecialPage;
34use MediaWiki\Title\Title;
35use MediaWiki\Title\TitleValue;
36use MediaWiki\User\UserIdentity;
37use MediaWiki\User\UserNamePrefixSearch;
38use MediaWiki\User\UserNameUtils;
39use MediaWiki\Watchlist\WatchlistManager;
40use Wikimedia\IPUtils;
41
42/**
43 * A special page for unblocking users
44 *
45 * @ingroup SpecialPage
46 */
47class SpecialUnblock extends SpecialPage {
48
49    /** @var UserIdentity|string|null */
50    protected $target;
51
52    /** @var int|null Block::TYPE_ constant */
53    protected $type;
54
55    protected $block;
56
57    private UnblockUserFactory $unblockUserFactory;
58    private BlockUtils $blockUtils;
59    private DatabaseBlockStore $blockStore;
60    private UserNameUtils $userNameUtils;
61    private UserNamePrefixSearch $userNamePrefixSearch;
62    private WatchlistManager $watchlistManager;
63
64    /**
65     * @param UnblockUserFactory $unblockUserFactory
66     * @param BlockUtils $blockUtils
67     * @param DatabaseBlockStore $blockStore
68     * @param UserNameUtils $userNameUtils
69     * @param UserNamePrefixSearch $userNamePrefixSearch
70     * @param WatchlistManager $watchlistManager
71     */
72    public function __construct(
73        UnblockUserFactory $unblockUserFactory,
74        BlockUtils $blockUtils,
75        DatabaseBlockStore $blockStore,
76        UserNameUtils $userNameUtils,
77        UserNamePrefixSearch $userNamePrefixSearch,
78        WatchlistManager $watchlistManager
79    ) {
80        parent::__construct( 'Unblock', 'block' );
81        $this->unblockUserFactory = $unblockUserFactory;
82        $this->blockUtils = $blockUtils;
83        $this->blockStore = $blockStore;
84        $this->userNameUtils = $userNameUtils;
85        $this->userNamePrefixSearch = $userNamePrefixSearch;
86        $this->watchlistManager = $watchlistManager;
87    }
88
89    public function doesWrites() {
90        return true;
91    }
92
93    public function execute( $par ) {
94        $this->checkPermissions();
95        $this->checkReadOnly();
96
97        [ $this->target, $this->type ] = $this->getTargetAndType( $par, $this->getRequest() );
98        $this->block = $this->blockStore->newFromTarget( $this->target );
99        if ( $this->target instanceof UserIdentity ) {
100            // Set the 'relevant user' in the skin, so it displays links like Contributions,
101            // User logs, UserRights, etc.
102            $this->getSkin()->setRelevantUser( $this->target );
103        }
104
105        $this->setHeaders();
106        $this->outputHeader();
107        $this->addHelpLink( 'Help:Blocking users' );
108
109        $out = $this->getOutput();
110        $out->setPageTitleMsg( $this->msg( 'unblockip' ) );
111        $out->addModules( [ 'mediawiki.userSuggest', 'mediawiki.special.block' ] );
112
113        $form = HTMLForm::factory( 'ooui', $this->getFields(), $this->getContext() )
114            ->setWrapperLegendMsg( 'unblockip' )
115            ->setSubmitCallback( function ( array $data, HTMLForm $form ) {
116                if ( $this->type != Block::TYPE_RANGE
117                    && $this->type != Block::TYPE_AUTO
118                    && $data['Watch']
119                ) {
120                    $this->watchlistManager->addWatchIgnoringRights(
121                        $form->getUser(),
122                        Title::makeTitle( NS_USER, $this->target )
123                    );
124                }
125                return $this->unblockUserFactory->newUnblockUser(
126                    $data['Target'],
127                    $form->getContext()->getAuthority(),
128                    $data['Reason'],
129                    $data['Tags'] ?? []
130                )->unblock();
131            } )
132            ->setSubmitTextMsg( 'ipusubmit' )
133            ->addPreHtml( $this->msg( 'unblockiptext' )->parseAsBlock() );
134
135        $userPage = $this->getTargetUserTitle( $this->target );
136        if ( $userPage ) {
137            // Get relevant extracts from the block and suppression logs, if possible
138            $logExtract = '';
139            LogEventsList::showLogExtract(
140                $logExtract,
141                'block',
142                $userPage,
143                '',
144                [
145                    'lim' => 10,
146                    'msgKey' => [
147                        'unblocklog-showlog',
148                        $userPage->getText(),
149                    ],
150                    'showIfEmpty' => false
151                ]
152            );
153            if ( $logExtract !== '' ) {
154                $form->addPostHtml( $logExtract );
155            }
156
157            // Add suppression block entries if allowed
158            if ( $this->getAuthority()->isAllowed( 'suppressionlog' ) ) {
159                $logExtract = '';
160                LogEventsList::showLogExtract(
161                    $logExtract,
162                    'suppress',
163                    $userPage,
164                    '',
165                    [
166                        'lim' => 10,
167                        'conds' => [ 'log_action' => [ 'block', 'reblock', 'unblock' ] ],
168                        'msgKey' => [
169                            'unblocklog-showsuppresslog',
170                            $userPage->getText(),
171                        ],
172                        'showIfEmpty' => false
173                    ]
174                );
175                if ( $logExtract !== '' ) {
176                    $form->addPostHtml( $logExtract );
177                }
178            }
179        }
180
181        if ( $form->show() ) {
182            switch ( $this->type ) {
183                case Block::TYPE_IP:
184                    $out->addWikiMsg( 'unblocked-ip', wfEscapeWikiText( $this->target ) );
185                    break;
186                case Block::TYPE_USER:
187                    $out->addWikiMsg( 'unblocked', wfEscapeWikiText( $this->target ) );
188                    break;
189                case Block::TYPE_RANGE:
190                    $out->addWikiMsg( 'unblocked-range', wfEscapeWikiText( $this->target ) );
191                    break;
192                case Block::TYPE_ID:
193                case Block::TYPE_AUTO:
194                    $out->addWikiMsg( 'unblocked-id', wfEscapeWikiText( $this->target ) );
195                    break;
196            }
197        }
198    }
199
200    /**
201     * Get the target and type, given the request and the subpage parameter.
202     * Several parameters are handled for backwards compatability. 'wpTarget' is
203     * prioritized, since it matches the HTML form.
204     *
205     * @param string|null $par Subpage parameter
206     * @param WebRequest $request
207     * @return array [ UserIdentity|string|null, DatabaseBlock::TYPE_ constant|null ]
208     * @phan-return array{0:UserIdentity|string|null,1:int|null}
209     */
210    private function getTargetAndType( ?string $par, WebRequest $request ) {
211        $possibleTargets = [
212            $request->getVal( 'wpTarget', null ),
213            $par,
214            $request->getVal( 'ip', null ),
215            // B/C @since 1.18
216            $request->getVal( 'wpBlockAddress', null ),
217        ];
218        foreach ( $possibleTargets as $possibleTarget ) {
219            $targetAndType = $this->blockUtils->parseBlockTarget( $possibleTarget );
220            // If type is not null then target is valid
221            if ( $targetAndType[ 1 ] !== null ) {
222                break;
223            }
224        }
225        return $targetAndType;
226    }
227
228    /**
229     * Get a user page target for things like logs.
230     * This handles account and IP range targets.
231     * @param UserIdentity|string|null $target
232     * @return Title|null
233     */
234    private function getTargetUserTitle( $target ): ?Title {
235        if ( $target instanceof UserIdentity ) {
236            return Title::makeTitle( NS_USER, $target->getName() );
237        }
238
239        if ( is_string( $target ) && IPUtils::isIPAddress( $target ) ) {
240            return Title::makeTitle( NS_USER, $target );
241        }
242
243        return null;
244    }
245
246    protected function getFields() {
247        $fields = [
248            'Target' => [
249                'type' => 'text',
250                'label-message' => 'ipaddressorusername',
251                'autofocus' => true,
252                'size' => '45',
253                'required' => true,
254                'cssclass' => 'mw-autocomplete-user', // used by mediawiki.userSuggest
255            ],
256            'Name' => [
257                'type' => 'info',
258                'label-message' => 'ipaddressorusername',
259            ],
260            'Reason' => [
261                'type' => 'text',
262                'label-message' => 'ipbreason',
263            ]
264        ];
265
266        if ( $this->block instanceof Block ) {
267            $type = $this->block->getType();
268            $targetName = $this->block->getTargetName();
269
270            // Autoblocks are logged as "autoblock #123 because the IP was recently used by
271            // User:Foo, and we've just got any block, auto or not, that applies to a target
272            // the user has specified.  Someone could be fishing to connect IPs to autoblocks,
273            // so don't show any distinction between unblocked IPs and autoblocked IPs
274            if ( $type == Block::TYPE_AUTO && $this->type == Block::TYPE_IP ) {
275                $fields['Target']['default'] = $this->target;
276                unset( $fields['Name'] );
277            } else {
278                $fields['Target']['default'] = $targetName;
279                $fields['Target']['type'] = 'hidden';
280                switch ( $type ) {
281                    case Block::TYPE_IP:
282                        $fields['Name']['default'] = $this->getLinkRenderer()->makeKnownLink(
283                            $this->getSpecialPageFactory()->getTitleForAlias( 'Contributions/' . $targetName ),
284                            $targetName
285                        );
286                        $fields['Name']['raw'] = true;
287                        break;
288                    case Block::TYPE_USER:
289                        $fields['Name']['default'] = $this->getLinkRenderer()->makeLink(
290                            new TitleValue( NS_USER, $targetName ),
291                            $targetName
292                        );
293                        $fields['Name']['raw'] = true;
294                        break;
295
296                    case Block::TYPE_RANGE:
297                        $fields['Name']['default'] = $targetName;
298                        break;
299
300                    case Block::TYPE_AUTO:
301                        $fields['Name']['default'] = $this->block->getRedactedName();
302                        $fields['Name']['raw'] = true;
303                        // Don't expose the real target of the autoblock
304                        $fields['Target']['default'] = "#{$this->target}";
305                        break;
306                }
307                // Target is hidden, so the reason is the first element
308                $fields['Target']['autofocus'] = false;
309                $fields['Reason']['autofocus'] = true;
310            }
311        } else {
312            $fields['Target']['default'] = $this->target;
313            unset( $fields['Name'] );
314        }
315        // Watchlist their user page? (Only if user is logged in)
316        if ( $this->getUser()->isRegistered() ) {
317            $fields['Watch'] = [
318                'type' => 'check',
319                'label-message' => 'ipbwatchuser',
320            ];
321        }
322
323        return $fields;
324    }
325
326    /**
327     * Return an array of subpages beginning with $search that this special page will accept.
328     *
329     * @param string $search Prefix to search for
330     * @param int $limit Maximum number of results to return (usually 10)
331     * @param int $offset Number of results to skip (usually 0)
332     * @return string[] Matching subpages
333     */
334    public function prefixSearchSubpages( $search, $limit, $offset ) {
335        $search = $this->userNameUtils->getCanonical( $search );
336        if ( !$search ) {
337            // No prefix suggestion for invalid user
338            return [];
339        }
340        // Autocomplete subpage as user list - public to allow caching
341        return $this->userNamePrefixSearch
342            ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
343    }
344
345    protected function getGroupName() {
346        return 'users';
347    }
348}
349
350/**
351 * Retain the old class name for backwards compatibility.
352 * @deprecated since 1.41
353 */
354class_alias( SpecialUnblock::class, 'SpecialUnblock' );