Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.44% covered (warning)
86.44%
102 / 118
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiUnblock
87.18% covered (warning)
87.18%
102 / 117
50.00% covered (danger)
50.00%
4 / 8
25.21
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
 execute
98.59% covered (success)
98.59%
70 / 71
0.00% covered (danger)
0.00%
0 / 1
16
 mustBePosted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isWriteMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedParams
75.86% covered (warning)
75.86%
22 / 29
0.00% covered (danger)
0.00%
0 / 1
2.06
 needsToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Copyright © 2007 Roan Kattouw <roan.kattouw@gmail.com>
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Api;
10
11use MediaWiki\Block\Block;
12use MediaWiki\Block\BlockPermissionCheckerFactory;
13use MediaWiki\Block\BlockTargetFactory;
14use MediaWiki\Block\DatabaseBlockStore;
15use MediaWiki\Block\UnblockUserFactory;
16use MediaWiki\MainConfigNames;
17use MediaWiki\ParamValidator\TypeDef\UserDef;
18use MediaWiki\Title\Title;
19use MediaWiki\User\Options\UserOptionsLookup;
20use MediaWiki\User\UserIdentityLookup;
21use MediaWiki\Watchlist\WatchedItemStoreInterface;
22use MediaWiki\Watchlist\WatchlistManager;
23use RuntimeException;
24use Wikimedia\ParamValidator\ParamValidator;
25use Wikimedia\ParamValidator\TypeDef\ExpiryDef;
26
27/**
28 * API module that facilitates the unblocking of users. Requires API write mode
29 * to be enabled.
30 *
31 * @ingroup API
32 */
33class ApiUnblock extends ApiBase {
34
35    use ApiBlockInfoTrait;
36    use ApiWatchlistTrait;
37
38    public function __construct(
39        ApiMain $main,
40        string $action,
41        private readonly BlockPermissionCheckerFactory $permissionCheckerFactory,
42        private readonly UnblockUserFactory $unblockUserFactory,
43        private readonly UserIdentityLookup $userIdentityLookup,
44        WatchedItemStoreInterface $watchedItemStore,
45        WatchlistManager $watchlistManager,
46        UserOptionsLookup $userOptionsLookup,
47        private readonly DatabaseBlockStore $blockStore,
48        private readonly BlockTargetFactory $blockTargetFactory,
49    ) {
50        parent::__construct( $main, $action );
51
52        $this->watchedItemStore = $watchedItemStore;
53
54        // Variables needed in ApiWatchlistTrait trait
55        $this->watchlistExpiryEnabled = $this->getConfig()->get( MainConfigNames::WatchlistExpiry );
56        $this->watchlistMaxDuration =
57            $this->getConfig()->get( MainConfigNames::WatchlistExpiryMaxDuration );
58        $this->watchlistManager = $watchlistManager;
59        $this->userOptionsLookup = $userOptionsLookup;
60    }
61
62    /**
63     * Unblocks the specified user or provides the reason the unblock failed.
64     */
65    public function execute() {
66        $performer = $this->getUser();
67        $params = $this->extractRequestParams();
68
69        $this->requireOnlyOneParameter( $params, 'id', 'user', 'userid' );
70
71        if ( !$this->getAuthority()->isAllowed( 'block' ) ) {
72            $this->dieWithError( 'apierror-permissiondenied-unblock', 'permissiondenied' );
73        }
74
75        if ( $params['userid'] !== null ) {
76            $identity = $this->userIdentityLookup->getUserIdentityByUserId( $params['userid'] );
77            if ( !$identity ) {
78                $this->dieWithError( [ 'apierror-nosuchuserid', $params['userid'] ], 'nosuchuserid' );
79            }
80            $params['user'] = $identity;
81        }
82
83        $blockToRemove = null;
84        if ( $params['id'] !== null ) {
85            $blockToRemove = $this->blockStore->newFromID( $params['id'], true );
86            if ( !$blockToRemove ) {
87                $this->dieWithError(
88                    [ 'apierror-nosuchblockid', $params['id'] ],
89                    'nosuchblockid' );
90            }
91            $target = $blockToRemove->getRedactedTarget();
92            if ( !$target ) {
93                throw new RuntimeException( 'Block has no target' );
94            }
95        } else {
96            $target = $this->blockTargetFactory->newFromUser( $params['user'] );
97        }
98
99        # T17810: blocked admins should have limited access here
100        $status = $this->permissionCheckerFactory
101            ->newChecker(
102                $this->getAuthority()
103            )->checkBlockPermissions( $target );
104
105        if ( $status !== true ) {
106            $this->dieWithError(
107                $status,
108                null,
109                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
110                [ 'blockinfo' => $this->getBlockDetails( $performer->getBlock() ) ]
111            );
112        }
113
114        if ( $blockToRemove !== null ) {
115            $status = $this->unblockUserFactory->newRemoveBlock(
116                $blockToRemove,
117                $this->getAuthority(),
118                $params['reason'],
119                $params['tags'] ?? []
120            )->unblock();
121        } else {
122            $status = $this->unblockUserFactory->newUnblockUser(
123                $target,
124                $this->getAuthority(),
125                $params['reason'],
126                $params['tags'] ?? []
127            )->unblock();
128        }
129
130        if ( !$status->isOK() ) {
131            $this->dieStatus( $status );
132        }
133
134        $block = $status->getValue();
135        $targetType = $block->getType();
136        $targetName = $targetType === Block::TYPE_AUTO ? '' : $block->getTargetName();
137        $targetUserId = $block->getTargetUserIdentity() ? $block->getTargetUserIdentity()->getId() : 0;
138
139        $userPage = Title::makeTitle( NS_USER, $targetName );
140        $watchlistExpiry = $this->getExpiryFromParams( $params, $userPage, $this->getUser() );
141        $watchuser = $params['watchuser'];
142        if ( $watchuser && $targetType !== Block::TYPE_RANGE && $targetType !== Block::TYPE_AUTO ) {
143            $this->setWatch( 'watch', $userPage, $this->getUser(), null, $watchlistExpiry );
144        } else {
145            $watchuser = false;
146            $watchlistExpiry = null;
147        }
148
149        $res = [
150            'id' => $block->getId(),
151            'user' => $targetName,
152            'userid' => $targetUserId,
153            'reason' => $params['reason'],
154            'watchuser' => $watchuser,
155        ];
156
157        if ( $watchlistExpiry !== null ) {
158            $res['watchlistexpiry'] = $this->getWatchlistExpiry(
159                $this->watchedItemStore,
160                $userPage,
161                $this->getUser()
162            );
163        }
164
165        $this->getResult()->addValue( null, $this->getModuleName(), $res );
166    }
167
168    /** @inheritDoc */
169    public function mustBePosted() {
170        return true;
171    }
172
173    /** @inheritDoc */
174    public function isWriteMode() {
175        return true;
176    }
177
178    /** @inheritDoc */
179    public function getAllowedParams() {
180        $params = [
181            'id' => [
182                ParamValidator::PARAM_TYPE => 'integer',
183            ],
184            'user' => [
185                ParamValidator::PARAM_TYPE => 'user',
186                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'cidr', 'id' ],
187                UserDef::PARAM_RETURN_OBJECT => true,
188            ],
189            'userid' => [
190                ParamValidator::PARAM_TYPE => 'integer',
191                ParamValidator::PARAM_DEPRECATED => true,
192            ],
193            'reason' => '',
194            'tags' => [
195                ParamValidator::PARAM_TYPE => 'tags',
196                ParamValidator::PARAM_ISMULTI => true,
197            ],
198            'watchuser' => false,
199        ];
200
201        // Params appear in the docs in the order they are defined,
202        // which is why this is here and not at the bottom.
203        // @todo Find better way to support insertion at arbitrary position
204        if ( $this->watchlistExpiryEnabled ) {
205            $params += [
206                'watchlistexpiry' => [
207                    ParamValidator::PARAM_TYPE => 'expiry',
208                    ExpiryDef::PARAM_MAX => $this->watchlistMaxDuration,
209                    ExpiryDef::PARAM_USE_MAX => true,
210                ]
211            ];
212        }
213
214        return $params;
215    }
216
217    /** @inheritDoc */
218    public function needsToken() {
219        return 'csrf';
220    }
221
222    /** @inheritDoc */
223    protected function getExamplesMessages() {
224        return [
225            'action=unblock&id=105'
226                => 'apihelp-unblock-example-id',
227            'action=unblock&user=Bob&reason=Sorry%20Bob'
228                => 'apihelp-unblock-example-user',
229        ];
230    }
231
232    /** @inheritDoc */
233    public function getHelpUrls() {
234        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Block';
235    }
236}
237
238/** @deprecated class alias since 1.43 */
239class_alias( ApiUnblock::class, 'ApiUnblock' );