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