Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.29% covered (warning)
75.29%
128 / 170
28.57% covered (danger)
28.57%
2 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialUnblock
75.74% covered (warning)
75.74%
128 / 169
28.57% covered (danger)
28.57%
2 / 7
42.85
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
71.11% covered (warning)
71.11%
64 / 90
0.00% covered (danger)
0.00%
0 / 1
17.07
 getTargetFromRequest
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 getFields
85.19% covered (warning)
85.19%
46 / 54
0.00% covered (danger)
0.00%
0 / 1
9.26
 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 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Specials;
8
9use MediaWiki\Block\Block;
10use MediaWiki\Block\BlockTarget;
11use MediaWiki\Block\BlockTargetFactory;
12use MediaWiki\Block\BlockTargetWithUserPage;
13use MediaWiki\Block\DatabaseBlock;
14use MediaWiki\Block\DatabaseBlockStore;
15use MediaWiki\Block\UnblockUserFactory;
16use MediaWiki\HTMLForm\HTMLForm;
17use MediaWiki\Logging\LogEventsList;
18use MediaWiki\MainConfigNames;
19use MediaWiki\Request\WebRequest;
20use MediaWiki\SpecialPage\SpecialPage;
21use MediaWiki\Title\Title;
22use MediaWiki\Title\TitleValue;
23use MediaWiki\User\UserNamePrefixSearch;
24use MediaWiki\User\UserNameUtils;
25use MediaWiki\Watchlist\WatchlistManager;
26
27/**
28 * A special page for unblocking users
29 *
30 * @ingroup SpecialPage
31 */
32class SpecialUnblock extends SpecialPage {
33
34    /** @var BlockTarget|null */
35    protected $target;
36
37    /** @var DatabaseBlock|null */
38    protected $block;
39
40    private UnblockUserFactory $unblockUserFactory;
41    private BlockTargetFactory $blockTargetFactory;
42    private DatabaseBlockStore $blockStore;
43    private UserNameUtils $userNameUtils;
44    private UserNamePrefixSearch $userNamePrefixSearch;
45    private WatchlistManager $watchlistManager;
46
47    public function __construct(
48        UnblockUserFactory $unblockUserFactory,
49        BlockTargetFactory $blockTargetFactory,
50        DatabaseBlockStore $blockStore,
51        UserNameUtils $userNameUtils,
52        UserNamePrefixSearch $userNamePrefixSearch,
53        WatchlistManager $watchlistManager
54    ) {
55        parent::__construct( 'Unblock', 'block' );
56        $this->unblockUserFactory = $unblockUserFactory;
57        $this->blockTargetFactory = $blockTargetFactory;
58        $this->blockStore = $blockStore;
59        $this->userNameUtils = $userNameUtils;
60        $this->userNamePrefixSearch = $userNamePrefixSearch;
61        $this->watchlistManager = $watchlistManager;
62    }
63
64    /** @inheritDoc */
65    public function doesWrites() {
66        return true;
67    }
68
69    /** @inheritDoc */
70    public function execute( $par ) {
71        $this->checkPermissions();
72        $this->checkReadOnly();
73
74        $this->target = $this->getTargetFromRequest( $par, $this->getRequest() );
75
76        // T382539
77        if ( $this->getConfig()->get( MainConfigNames::UseCodexSpecialBlock )
78            || $this->getRequest()->getBool( 'usecodex' )
79        ) {
80            // If target is null, redirect to Special:Block
81            if ( $this->target === null ) {
82                // Use 301 (Moved Permanently) as this is a deprecation
83                $this->getOutput()->redirect(
84                    SpecialPage::getTitleFor( 'Block' )->getFullURL( 'redirected=1' ),
85                    '301'
86                );
87                return;
88            }
89        }
90
91        $this->block = $this->blockStore->newFromTarget( $this->target );
92        if ( $this->target instanceof BlockTargetWithUserPage ) {
93            // Set the 'relevant user' in the skin, so it displays links like Contributions,
94            // User logs, UserRights, etc.
95            $this->getSkin()->setRelevantUser( $this->target->getUserIdentity() );
96        }
97
98        $this->setHeaders();
99        $this->outputHeader();
100        $this->addHelpLink( 'Help:Blocking users' );
101
102        $out = $this->getOutput();
103        $out->setPageTitleMsg( $this->msg( 'unblock-target' ) );
104        $out->addModules( [ 'mediawiki.userSuggest', 'mediawiki.special.block' ] );
105
106        $form = HTMLForm::factory( 'ooui', $this->getFields(), $this->getContext() )
107            ->setWrapperLegendMsg( 'unblock-target' )
108            ->setSubmitCallback( function ( array $data, HTMLForm $form ) {
109                if ( $this->target instanceof BlockTargetWithUserPage && $data['Watch'] ) {
110                    $this->watchlistManager->addWatchIgnoringRights(
111                        $form->getUser(),
112                        Title::newFromPageReference( $this->target->getUserPage() )
113                    );
114                }
115                $status = $this->unblockUserFactory->newUnblockUser(
116                    $this->target,
117                    $form->getContext()->getAuthority(),
118                    $data['Reason'],
119                    $data['Tags'] ?? []
120                )->unblock();
121
122                if ( $status->hasMessage( 'ipb_cant_unblock_multiple_blocks' ) ) {
123                    // Add additional message sending users to [[Special:Block/Username]]
124                    $status->error( 'unblock-error-multiblocks', $this->target->toString() );
125                }
126                return $status;
127            } )
128            ->setSubmitTextMsg( 'ipusubmit' )
129            ->addPreHtml( $this->msg( 'unblockiptext' )->parseAsBlock() );
130
131        if ( $this->target ) {
132            $userPage = $this->target->getLogPage();
133            $targetName = (string)$this->target;
134            // Get relevant extracts from the block and suppression logs, if possible
135            $logExtract = '';
136            LogEventsList::showLogExtract(
137                $logExtract,
138                'block',
139                $userPage,
140                '',
141                [
142                    'lim' => 10,
143                    'msgKey' => [
144                        'unblocklog-showlog',
145                        $targetName,
146                    ],
147                    'showIfEmpty' => false
148                ]
149            );
150            if ( $logExtract !== '' ) {
151                $form->addPostHtml( $logExtract );
152            }
153
154            // Add suppression block entries if allowed
155            if ( $this->getAuthority()->isAllowed( 'suppressionlog' ) ) {
156                $logExtract = '';
157                LogEventsList::showLogExtract(
158                    $logExtract,
159                    'suppress',
160                    $userPage,
161                    '',
162                    [
163                        'lim' => 10,
164                        'conds' => [ 'log_action' => [ 'block', 'reblock', 'unblock' ] ],
165                        'msgKey' => [
166                            'unblocklog-showsuppresslog',
167                            $targetName,
168                        ],
169                        'showIfEmpty' => false
170                    ]
171                );
172                if ( $logExtract !== '' ) {
173                    $form->addPostHtml( $logExtract );
174                }
175            }
176        }
177
178        if ( $form->show() ) {
179            $msgsByType = [
180                Block::TYPE_IP => 'unblocked-ip',
181                Block::TYPE_USER => 'unblocked',
182                Block::TYPE_RANGE => 'unblocked-range',
183                Block::TYPE_AUTO => 'unblocked-id'
184            ];
185            $out->addWikiMsg(
186                $msgsByType[$this->target->getType()],
187                wfEscapeWikiText( (string)$this->target )
188            );
189        }
190    }
191
192    /**
193     * Get the target and type, given the request and the subpage parameter.
194     * Several parameters are handled for backwards compatability. 'wpTarget' is
195     * prioritized, since it matches the HTML form.
196     *
197     * @param string|null $par Subpage parameter
198     * @param WebRequest $request
199     * @return BlockTarget|null
200     */
201    private function getTargetFromRequest( ?string $par, WebRequest $request ) {
202        $possibleTargets = [
203            $request->getVal( 'wpTarget', null ),
204            $par,
205            $request->getVal( 'ip', null ),
206            // B/C @since 1.18
207            $request->getVal( 'wpBlockAddress', null ),
208        ];
209        foreach ( $possibleTargets as $possibleTarget ) {
210            $target = $this->blockTargetFactory->newFromString( $possibleTarget );
211            // If type is not null then target is valid
212            if ( $target ) {
213                break;
214            }
215        }
216        return $target;
217    }
218
219    protected function getFields(): array {
220        $fields = [
221            'Target' => [
222                'type' => 'text',
223                'label-message' => 'unblock-target-label',
224                'autofocus' => true,
225                'size' => '45',
226                'required' => true,
227                'cssclass' => 'mw-autocomplete-user', // used by mediawiki.userSuggest
228            ],
229            'Name' => [
230                'type' => 'info',
231                'label-message' => 'unblock-target-label',
232            ],
233            'Reason' => [
234                'type' => 'text',
235                'label-message' => 'ipbreason',
236            ]
237        ];
238
239        if ( $this->block instanceof Block ) {
240            $type = $this->block->getType();
241            $targetName = $this->block->getTargetName();
242
243            // Autoblocks are logged as "autoblock #123 because the IP was recently used by
244            // User:Foo, and we've just got any block, auto or not, that applies to a target
245            // the user has specified.  Someone could be fishing to connect IPs to autoblocks,
246            // so don't show any distinction between unblocked IPs and autoblocked IPs
247            if ( $type == Block::TYPE_AUTO && $this->target->getType() == Block::TYPE_IP ) {
248                $fields['Target']['default'] = (string)$this->target;
249                unset( $fields['Name'] );
250            } else {
251                $fields['Target']['default'] = $targetName;
252                $fields['Target']['type'] = 'hidden';
253                switch ( $type ) {
254                    case Block::TYPE_IP:
255                        $fields['Name']['default'] = $this->getLinkRenderer()->makeKnownLink(
256                            $this->getSpecialPageFactory()->getTitleForAlias( 'Contributions/' . $targetName ),
257                            $targetName
258                        );
259                        $fields['Name']['raw'] = true;
260                        break;
261                    case Block::TYPE_USER:
262                        $fields['Name']['default'] = $this->getLinkRenderer()->makeLink(
263                            new TitleValue( NS_USER, $targetName ),
264                            $targetName
265                        );
266                        $fields['Name']['raw'] = true;
267                        break;
268
269                    case Block::TYPE_RANGE:
270                        $fields['Name']['default'] = $targetName;
271                        break;
272
273                    case Block::TYPE_AUTO:
274                        // Don't expose the real target of the autoblock
275                        $fields['Name']['default'] = $this->block->getRedactedName();
276                        $fields['Name']['raw'] = true;
277                        $fields['Target']['default'] = $this->block->getRedactedTarget()->toString();
278                        break;
279                }
280                // Target is hidden, so the reason is the first element
281                $fields['Target']['autofocus'] = false;
282                $fields['Reason']['autofocus'] = true;
283            }
284        } else {
285            $fields['Target']['default'] = $this->target;
286            unset( $fields['Name'] );
287        }
288        // Watchlist their user page? (Only if user is logged in)
289        if ( $this->getUser()->isRegistered() ) {
290            $fields['Watch'] = [
291                'type' => 'check',
292                'label-message' => 'ipbwatchuser',
293            ];
294        }
295
296        return $fields;
297    }
298
299    /**
300     * Return an array of subpages beginning with $search that this special page will accept.
301     *
302     * @param string $search Prefix to search for
303     * @param int $limit Maximum number of results to return (usually 10)
304     * @param int $offset Number of results to skip (usually 0)
305     * @return string[] Matching subpages
306     */
307    public function prefixSearchSubpages( $search, $limit, $offset ) {
308        $search = $this->userNameUtils->getCanonical( $search );
309        if ( !$search ) {
310            // No prefix suggestion for invalid user
311            return [];
312        }
313        // Autocomplete subpage as user list - public to allow caching
314        return $this->userNamePrefixSearch
315            ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
316    }
317
318    /** @inheritDoc */
319    protected function getGroupName() {
320        return 'users';
321    }
322}
323
324/**
325 * Retain the old class name for backwards compatibility.
326 * @deprecated since 1.41
327 */
328class_alias( SpecialUnblock::class, 'SpecialUnblock' );