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