Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.04% covered (warning)
87.04%
141 / 162
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiBlock
87.04% covered (warning)
87.04%
141 / 162
50.00% covered (danger)
50.00%
4 / 8
24.15
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 execute
91.57% covered (success)
91.57%
76 / 83
0.00% covered (danger)
0.00%
0 / 1
14.12
 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
87.50% covered (warning)
87.50%
49 / 56
0.00% covered (danger)
0.00%
0 / 1
3.02
 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 * 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 */
22
23use MediaWiki\Block\AbstractBlock;
24use MediaWiki\Block\BlockActionInfo;
25use MediaWiki\Block\BlockPermissionCheckerFactory;
26use MediaWiki\Block\BlockUserFactory;
27use MediaWiki\Block\BlockUtils;
28use MediaWiki\Block\DatabaseBlock;
29use MediaWiki\Block\Restriction\ActionRestriction;
30use MediaWiki\Block\Restriction\NamespaceRestriction;
31use MediaWiki\Block\Restriction\PageRestriction;
32use MediaWiki\MainConfigNames;
33use MediaWiki\ParamValidator\TypeDef\TitleDef;
34use MediaWiki\ParamValidator\TypeDef\UserDef;
35use MediaWiki\Title\Title;
36use MediaWiki\Title\TitleFactory;
37use MediaWiki\User\Options\UserOptionsLookup;
38use MediaWiki\User\UserIdentity;
39use MediaWiki\User\UserIdentityLookup;
40use MediaWiki\Watchlist\WatchlistManager;
41use Wikimedia\ParamValidator\ParamValidator;
42use Wikimedia\ParamValidator\TypeDef\ExpiryDef;
43
44/**
45 * API module that facilitates the blocking of users. Requires API write mode
46 * to be enabled.
47 *
48 * @ingroup API
49 */
50class ApiBlock extends ApiBase {
51
52    use ApiBlockInfoTrait;
53    use ApiWatchlistTrait;
54
55    private BlockPermissionCheckerFactory $blockPermissionCheckerFactory;
56    private BlockUserFactory $blockUserFactory;
57    private TitleFactory $titleFactory;
58    private UserIdentityLookup $userIdentityLookup;
59    private WatchedItemStoreInterface $watchedItemStore;
60    private BlockUtils $blockUtils;
61    private BlockActionInfo $blockActionInfo;
62
63    /**
64     * @param ApiMain $main
65     * @param string $action
66     * @param BlockPermissionCheckerFactory $blockPermissionCheckerFactory
67     * @param BlockUserFactory $blockUserFactory
68     * @param TitleFactory $titleFactory
69     * @param UserIdentityLookup $userIdentityLookup
70     * @param WatchedItemStoreInterface $watchedItemStore
71     * @param BlockUtils $blockUtils
72     * @param BlockActionInfo $blockActionInfo
73     * @param WatchlistManager $watchlistManager
74     * @param UserOptionsLookup $userOptionsLookup
75     */
76    public function __construct(
77        ApiMain $main,
78        $action,
79        BlockPermissionCheckerFactory $blockPermissionCheckerFactory,
80        BlockUserFactory $blockUserFactory,
81        TitleFactory $titleFactory,
82        UserIdentityLookup $userIdentityLookup,
83        WatchedItemStoreInterface $watchedItemStore,
84        BlockUtils $blockUtils,
85        BlockActionInfo $blockActionInfo,
86        WatchlistManager $watchlistManager,
87        UserOptionsLookup $userOptionsLookup
88    ) {
89        parent::__construct( $main, $action );
90
91        $this->blockPermissionCheckerFactory = $blockPermissionCheckerFactory;
92        $this->blockUserFactory = $blockUserFactory;
93        $this->titleFactory = $titleFactory;
94        $this->userIdentityLookup = $userIdentityLookup;
95        $this->watchedItemStore = $watchedItemStore;
96        $this->blockUtils = $blockUtils;
97        $this->blockActionInfo = $blockActionInfo;
98
99        // Variables needed in ApiWatchlistTrait trait
100        $this->watchlistExpiryEnabled = $this->getConfig()->get( MainConfigNames::WatchlistExpiry );
101        $this->watchlistMaxDuration =
102            $this->getConfig()->get( MainConfigNames::WatchlistExpiryMaxDuration );
103        $this->watchlistManager = $watchlistManager;
104        $this->userOptionsLookup = $userOptionsLookup;
105    }
106
107    /**
108     * Blocks the user specified in the parameters for the given expiry, with the
109     * given reason, and with all other settings provided in the params. If the block
110     * succeeds, produces a result containing the details of the block and notice
111     * of success. If it fails, the result will specify the nature of the error.
112     */
113    public function execute() {
114        $this->checkUserRightsAny( 'block' );
115        $params = $this->extractRequestParams();
116        $this->requireOnlyOneParameter( $params, 'user', 'userid' );
117
118        // Make sure $target contains a parsed target
119        if ( $params['user'] !== null ) {
120            $target = $params['user'];
121        } else {
122            $target = $this->userIdentityLookup->getUserIdentityByUserId( $params['userid'] );
123            if ( !$target ) {
124                $this->dieWithError( [ 'apierror-nosuchuserid', $params['userid'] ], 'nosuchuserid' );
125            }
126        }
127        [ $target, $targetType ] = $this->blockUtils->parseBlockTarget( $target );
128
129        if (
130            $params['noemail'] &&
131            !$this->blockPermissionCheckerFactory
132                ->newBlockPermissionChecker(
133                    $target,
134                    $this->getAuthority()
135                )
136                ->checkEmailPermissions()
137        ) {
138            $this->dieWithError( 'apierror-cantblock-email' );
139        }
140
141        $restrictions = [];
142        if ( $params['partial'] ) {
143            $pageRestrictions = array_map(
144                [ PageRestriction::class, 'newFromTitle' ],
145                (array)$params['pagerestrictions']
146            );
147
148            $namespaceRestrictions = array_map( static function ( $id ) {
149                return new NamespaceRestriction( 0, $id );
150            }, (array)$params['namespacerestrictions'] );
151            $restrictions = array_merge( $pageRestrictions, $namespaceRestrictions );
152
153            if ( $this->getConfig()->get( MainConfigNames::EnablePartialActionBlocks ) ) {
154                $actionRestrictions = array_map( function ( $action ) {
155                    return new ActionRestriction( 0, $this->blockActionInfo->getIdFromAction( $action ) );
156                }, (array)$params['actionrestrictions'] );
157                $restrictions = array_merge( $restrictions, $actionRestrictions );
158            }
159        }
160
161        $status = $this->blockUserFactory->newBlockUser(
162            $target,
163            $this->getAuthority(),
164            $params['expiry'],
165            $params['reason'],
166            [
167                'isCreateAccountBlocked' => $params['nocreate'],
168                'isEmailBlocked' => $params['noemail'],
169                'isHardBlock' => !$params['anononly'],
170                'isAutoblocking' => $params['autoblock'],
171                'isUserTalkEditBlocked' => !$params['allowusertalk'],
172                'isHideUser' => $params['hidename'],
173                'isPartial' => $params['partial'],
174            ],
175            $restrictions,
176            $params['tags']
177        )->placeBlock( $params['reblock'] );
178
179        if ( !$status->isOK() ) {
180            $this->dieStatus( $status );
181        }
182
183        $block = $status->value;
184
185        $watchlistExpiry = $this->getExpiryFromParams( $params );
186        $userPage = Title::makeTitle( NS_USER, $block->getTargetName() );
187
188        if ( $params['watchuser'] && $targetType !== AbstractBlock::TYPE_RANGE ) {
189            $this->setWatch( 'watch', $userPage, $this->getUser(), null, $watchlistExpiry );
190        }
191
192        $res = [];
193
194        $res['user'] = $block->getTargetName();
195        $res['userID'] = $target instanceof UserIdentity ? $target->getId() : 0;
196
197        if ( $block instanceof DatabaseBlock ) {
198            $res['expiry'] = ApiResult::formatExpiry( $block->getExpiry(), 'infinite' );
199            $res['id'] = $block->getId();
200        } else {
201            # should be unreachable
202            $res['expiry'] = ''; // @codeCoverageIgnore
203            $res['id'] = ''; // @codeCoverageIgnore
204        }
205
206        $res['reason'] = $params['reason'];
207        $res['anononly'] = $params['anononly'];
208        $res['nocreate'] = $params['nocreate'];
209        $res['autoblock'] = $params['autoblock'];
210        $res['noemail'] = $params['noemail'];
211        $res['hidename'] = $block->getHideName();
212        $res['allowusertalk'] = $params['allowusertalk'];
213        $res['watchuser'] = $params['watchuser'];
214        if ( $watchlistExpiry ) {
215            $expiry = $this->getWatchlistExpiry(
216                $this->watchedItemStore,
217                $userPage,
218                $this->getUser()
219            );
220            $res['watchlistexpiry'] = $expiry;
221        }
222        $res['partial'] = $params['partial'];
223        $res['pagerestrictions'] = $params['pagerestrictions'];
224        $res['namespacerestrictions'] = $params['namespacerestrictions'];
225        if ( $this->getConfig()->get( MainConfigNames::EnablePartialActionBlocks ) ) {
226            $res['actionrestrictions'] = $params['actionrestrictions'];
227        }
228
229        $this->getResult()->addValue( null, $this->getModuleName(), $res );
230    }
231
232    public function mustBePosted() {
233        return true;
234    }
235
236    public function isWriteMode() {
237        return true;
238    }
239
240    public function getAllowedParams() {
241        $params = [
242            'user' => [
243                ParamValidator::PARAM_TYPE => 'user',
244                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'cidr', 'id' ],
245            ],
246            'userid' => [
247                ParamValidator::PARAM_TYPE => 'integer',
248                ParamValidator::PARAM_DEPRECATED => true,
249            ],
250            'expiry' => 'never',
251            'reason' => '',
252            'anononly' => false,
253            'nocreate' => false,
254            'autoblock' => false,
255            'noemail' => false,
256            'hidename' => false,
257            'allowusertalk' => false,
258            'reblock' => false,
259            'watchuser' => false,
260        ];
261
262        // Params appear in the docs in the order they are defined,
263        // which is why this is here and not at the bottom.
264        // @todo Find better way to support insertion at arbitrary position
265        if ( $this->watchlistExpiryEnabled ) {
266            $params += [
267                'watchlistexpiry' => [
268                    ParamValidator::PARAM_TYPE => 'expiry',
269                    ExpiryDef::PARAM_MAX => $this->watchlistMaxDuration,
270                    ExpiryDef::PARAM_USE_MAX => true,
271                ]
272            ];
273        }
274
275        $params += [
276            'tags' => [
277                ParamValidator::PARAM_TYPE => 'tags',
278                ParamValidator::PARAM_ISMULTI => true,
279            ],
280            'partial' => false,
281            'pagerestrictions' => [
282                ParamValidator::PARAM_TYPE => 'title',
283                TitleDef::PARAM_MUST_EXIST => true,
284
285                // TODO: TitleDef returns instances of TitleValue when PARAM_RETURN_OBJECT is
286                // truthy. At the time of writing,
287                // MediaWiki\Block\Restriction\PageRestriction::newFromTitle accepts either
288                // string or instance of Title.
289                //TitleDef::PARAM_RETURN_OBJECT => true,
290
291                ParamValidator::PARAM_ISMULTI => true,
292                ParamValidator::PARAM_ISMULTI_LIMIT1 => 10,
293                ParamValidator::PARAM_ISMULTI_LIMIT2 => 10,
294            ],
295            'namespacerestrictions' => [
296                ParamValidator::PARAM_ISMULTI => true,
297                ParamValidator::PARAM_TYPE => 'namespace',
298            ],
299        ];
300
301        if ( $this->getConfig()->get( MainConfigNames::EnablePartialActionBlocks ) ) {
302            $params += [
303                'actionrestrictions' => [
304                    ParamValidator::PARAM_ISMULTI => true,
305                    ParamValidator::PARAM_TYPE => array_keys(
306                        $this->blockActionInfo->getAllBlockActions()
307                    ),
308                ],
309            ];
310        }
311
312        return $params;
313    }
314
315    public function needsToken() {
316        return 'csrf';
317    }
318
319    protected function getExamplesMessages() {
320        // phpcs:disable Generic.Files.LineLength
321        return [
322            'action=block&user=192.0.2.5&expiry=3%20days&reason=First%20strike&token=123ABC'
323                => 'apihelp-block-example-ip-simple',
324            'action=block&user=Vandal&expiry=never&reason=Vandalism&nocreate=&autoblock=&noemail=&token=123ABC'
325                => 'apihelp-block-example-user-complex',
326        ];
327        // phpcs:enable
328    }
329
330    public function getHelpUrls() {
331        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Block';
332    }
333}