ApiQueryBlocks.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  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. use Wikimedia\Rdbms\IResultWrapper;
  23. use MediaWiki\MediaWikiServices;
  24. /**
  25. * Query module to enumerate all user blocks
  26. *
  27. * @ingroup API
  28. */
  29. class ApiQueryBlocks extends ApiQueryBase {
  30. public function __construct( ApiQuery $query, $moduleName ) {
  31. parent::__construct( $query, $moduleName, 'bk' );
  32. }
  33. public function execute() {
  34. $db = $this->getDB();
  35. $commentStore = CommentStore::getStore();
  36. $params = $this->extractRequestParams();
  37. $this->requireMaxOneParameter( $params, 'users', 'ip' );
  38. $prop = array_flip( $params['prop'] );
  39. $fld_id = isset( $prop['id'] );
  40. $fld_user = isset( $prop['user'] );
  41. $fld_userid = isset( $prop['userid'] );
  42. $fld_by = isset( $prop['by'] );
  43. $fld_byid = isset( $prop['byid'] );
  44. $fld_timestamp = isset( $prop['timestamp'] );
  45. $fld_expiry = isset( $prop['expiry'] );
  46. $fld_reason = isset( $prop['reason'] );
  47. $fld_range = isset( $prop['range'] );
  48. $fld_flags = isset( $prop['flags'] );
  49. $fld_restrictions = isset( $prop['restrictions'] );
  50. $result = $this->getResult();
  51. $this->addTables( 'ipblocks' );
  52. $this->addFields( [ 'ipb_auto', 'ipb_id', 'ipb_timestamp' ] );
  53. $this->addFieldsIf( [ 'ipb_address', 'ipb_user' ], $fld_user || $fld_userid );
  54. if ( $fld_by || $fld_byid ) {
  55. $actorQuery = ActorMigration::newMigration()->getJoin( 'ipb_by' );
  56. $this->addTables( $actorQuery['tables'] );
  57. $this->addFields( $actorQuery['fields'] );
  58. $this->addJoinConds( $actorQuery['joins'] );
  59. }
  60. $this->addFieldsIf( 'ipb_expiry', $fld_expiry );
  61. $this->addFieldsIf( [ 'ipb_range_start', 'ipb_range_end' ], $fld_range );
  62. $this->addFieldsIf( [ 'ipb_anon_only', 'ipb_create_account', 'ipb_enable_autoblock',
  63. 'ipb_block_email', 'ipb_deleted', 'ipb_allow_usertalk', 'ipb_sitewide' ],
  64. $fld_flags );
  65. $this->addFieldsIf( 'ipb_sitewide', $fld_restrictions );
  66. if ( $fld_reason ) {
  67. $commentQuery = $commentStore->getJoin( 'ipb_reason' );
  68. $this->addTables( $commentQuery['tables'] );
  69. $this->addFields( $commentQuery['fields'] );
  70. $this->addJoinConds( $commentQuery['joins'] );
  71. }
  72. $this->addOption( 'LIMIT', $params['limit'] + 1 );
  73. $this->addTimestampWhereRange(
  74. 'ipb_timestamp',
  75. $params['dir'],
  76. $params['start'],
  77. $params['end']
  78. );
  79. // Include in ORDER BY for uniqueness
  80. $this->addWhereRange( 'ipb_id', $params['dir'], null, null );
  81. if ( !is_null( $params['continue'] ) ) {
  82. $cont = explode( '|', $params['continue'] );
  83. $this->dieContinueUsageIf( count( $cont ) != 2 );
  84. $op = ( $params['dir'] == 'newer' ? '>' : '<' );
  85. $continueTimestamp = $db->addQuotes( $db->timestamp( $cont[0] ) );
  86. $continueId = (int)$cont[1];
  87. $this->dieContinueUsageIf( $continueId != $cont[1] );
  88. $this->addWhere( "ipb_timestamp $op $continueTimestamp OR " .
  89. "(ipb_timestamp = $continueTimestamp AND " .
  90. "ipb_id $op= $continueId)"
  91. );
  92. }
  93. if ( isset( $params['ids'] ) ) {
  94. $this->addWhereIDsFld( 'ipblocks', 'ipb_id', $params['ids'] );
  95. }
  96. if ( isset( $params['users'] ) ) {
  97. $usernames = [];
  98. foreach ( (array)$params['users'] as $u ) {
  99. $usernames[] = $this->prepareUsername( $u );
  100. }
  101. $this->addWhereFld( 'ipb_address', $usernames );
  102. $this->addWhereFld( 'ipb_auto', 0 );
  103. }
  104. if ( isset( $params['ip'] ) ) {
  105. $blockCIDRLimit = $this->getConfig()->get( 'BlockCIDRLimit' );
  106. if ( IP::isIPv4( $params['ip'] ) ) {
  107. $type = 'IPv4';
  108. $cidrLimit = $blockCIDRLimit['IPv4'];
  109. $prefixLen = 0;
  110. } elseif ( IP::isIPv6( $params['ip'] ) ) {
  111. $type = 'IPv6';
  112. $cidrLimit = $blockCIDRLimit['IPv6'];
  113. $prefixLen = 3; // IP::toHex output is prefixed with "v6-"
  114. } else {
  115. $this->dieWithError( 'apierror-badip', 'param_ip' );
  116. }
  117. # Check range validity, if it's a CIDR
  118. list( $ip, $range ) = IP::parseCIDR( $params['ip'] );
  119. if ( $ip !== false && $range !== false && $range < $cidrLimit ) {
  120. $this->dieWithError( [ 'apierror-cidrtoobroad', $type, $cidrLimit ] );
  121. }
  122. # Let IP::parseRange handle calculating $upper, instead of duplicating the logic here.
  123. list( $lower, $upper ) = IP::parseRange( $params['ip'] );
  124. # Extract the common prefix to any rangeblock affecting this IP/CIDR
  125. $prefix = substr( $lower, 0, $prefixLen + floor( $cidrLimit / 4 ) );
  126. # Fairly hard to make a malicious SQL statement out of hex characters,
  127. # but it is good practice to add quotes
  128. $lower = $db->addQuotes( $lower );
  129. $upper = $db->addQuotes( $upper );
  130. $this->addWhere( [
  131. 'ipb_range_start' . $db->buildLike( $prefix, $db->anyString() ),
  132. 'ipb_range_start <= ' . $lower,
  133. 'ipb_range_end >= ' . $upper,
  134. 'ipb_auto' => 0
  135. ] );
  136. }
  137. if ( !is_null( $params['show'] ) ) {
  138. $show = array_flip( $params['show'] );
  139. /* Check for conflicting parameters. */
  140. if ( ( isset( $show['account'] ) && isset( $show['!account'] ) )
  141. || ( isset( $show['ip'] ) && isset( $show['!ip'] ) )
  142. || ( isset( $show['range'] ) && isset( $show['!range'] ) )
  143. || ( isset( $show['temp'] ) && isset( $show['!temp'] ) )
  144. ) {
  145. $this->dieWithError( 'apierror-show' );
  146. }
  147. $this->addWhereIf( 'ipb_user = 0', isset( $show['!account'] ) );
  148. $this->addWhereIf( 'ipb_user != 0', isset( $show['account'] ) );
  149. $this->addWhereIf( 'ipb_user != 0 OR ipb_range_end > ipb_range_start', isset( $show['!ip'] ) );
  150. $this->addWhereIf( 'ipb_user = 0 AND ipb_range_end = ipb_range_start', isset( $show['ip'] ) );
  151. $this->addWhereIf( 'ipb_expiry = ' .
  152. $db->addQuotes( $db->getInfinity() ), isset( $show['!temp'] ) );
  153. $this->addWhereIf( 'ipb_expiry != ' .
  154. $db->addQuotes( $db->getInfinity() ), isset( $show['temp'] ) );
  155. $this->addWhereIf( 'ipb_range_end = ipb_range_start', isset( $show['!range'] ) );
  156. $this->addWhereIf( 'ipb_range_end > ipb_range_start', isset( $show['range'] ) );
  157. }
  158. if ( !$this->getPermissionManager()->userHasRight( $this->getUser(), 'hideuser' ) ) {
  159. $this->addWhereFld( 'ipb_deleted', 0 );
  160. }
  161. # Filter out expired rows
  162. $this->addWhere( 'ipb_expiry > ' . $db->addQuotes( $db->timestamp() ) );
  163. $res = $this->select( __METHOD__ );
  164. $restrictions = [];
  165. if ( $fld_restrictions ) {
  166. $restrictions = self::getRestrictionData( $res, $params['limit'] );
  167. }
  168. $count = 0;
  169. foreach ( $res as $row ) {
  170. if ( ++$count > $params['limit'] ) {
  171. // We've had enough
  172. $this->setContinueEnumParameter( 'continue', "$row->ipb_timestamp|$row->ipb_id" );
  173. break;
  174. }
  175. $block = [
  176. ApiResult::META_TYPE => 'assoc',
  177. ];
  178. if ( $fld_id ) {
  179. $block['id'] = (int)$row->ipb_id;
  180. }
  181. if ( $fld_user && !$row->ipb_auto ) {
  182. $block['user'] = $row->ipb_address;
  183. }
  184. if ( $fld_userid && !$row->ipb_auto ) {
  185. $block['userid'] = (int)$row->ipb_user;
  186. }
  187. if ( $fld_by ) {
  188. $block['by'] = $row->ipb_by_text;
  189. }
  190. if ( $fld_byid ) {
  191. $block['byid'] = (int)$row->ipb_by;
  192. }
  193. if ( $fld_timestamp ) {
  194. $block['timestamp'] = wfTimestamp( TS_ISO_8601, $row->ipb_timestamp );
  195. }
  196. if ( $fld_expiry ) {
  197. $block['expiry'] = ApiResult::formatExpiry( $row->ipb_expiry );
  198. }
  199. if ( $fld_reason ) {
  200. $block['reason'] = $commentStore->getComment( 'ipb_reason', $row )->text;
  201. }
  202. if ( $fld_range && !$row->ipb_auto ) {
  203. $block['rangestart'] = IP::formatHex( $row->ipb_range_start );
  204. $block['rangeend'] = IP::formatHex( $row->ipb_range_end );
  205. }
  206. if ( $fld_flags ) {
  207. // For clarity, these flags use the same names as their action=block counterparts
  208. $block['automatic'] = (bool)$row->ipb_auto;
  209. $block['anononly'] = (bool)$row->ipb_anon_only;
  210. $block['nocreate'] = (bool)$row->ipb_create_account;
  211. $block['autoblock'] = (bool)$row->ipb_enable_autoblock;
  212. $block['noemail'] = (bool)$row->ipb_block_email;
  213. $block['hidden'] = (bool)$row->ipb_deleted;
  214. $block['allowusertalk'] = (bool)$row->ipb_allow_usertalk;
  215. $block['partial'] = !(bool)$row->ipb_sitewide;
  216. }
  217. if ( $fld_restrictions ) {
  218. $block['restrictions'] = [];
  219. if ( !$row->ipb_sitewide && isset( $restrictions[$row->ipb_id] ) ) {
  220. $block['restrictions'] = $restrictions[$row->ipb_id];
  221. }
  222. }
  223. $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $block );
  224. if ( !$fit ) {
  225. $this->setContinueEnumParameter( 'continue', "$row->ipb_timestamp|$row->ipb_id" );
  226. break;
  227. }
  228. }
  229. $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'block' );
  230. }
  231. protected function prepareUsername( $user ) {
  232. if ( !$user ) {
  233. $encParamName = $this->encodeParamName( 'users' );
  234. $this->dieWithError( [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $user ) ],
  235. "baduser_{$encParamName}"
  236. );
  237. }
  238. $name = User::isIP( $user )
  239. ? $user
  240. : User::getCanonicalName( $user, 'valid' );
  241. if ( $name === false ) {
  242. $encParamName = $this->encodeParamName( 'users' );
  243. $this->dieWithError( [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $user ) ],
  244. "baduser_{$encParamName}"
  245. );
  246. }
  247. return $name;
  248. }
  249. /**
  250. * Retrieves the restrictions based on the query result.
  251. *
  252. * @param IResultWrapper $result
  253. * @param int $limit
  254. *
  255. * @return array
  256. */
  257. private static function getRestrictionData( IResultWrapper $result, $limit ) {
  258. $partialIds = [];
  259. $count = 0;
  260. foreach ( $result as $row ) {
  261. if ( ++$count <= $limit && !$row->ipb_sitewide ) {
  262. $partialIds[] = (int)$row->ipb_id;
  263. }
  264. }
  265. $blockRestrictionStore = MediaWikiServices::getInstance()->getBlockRestrictionStore();
  266. $restrictions = $blockRestrictionStore->loadByBlockId( $partialIds );
  267. $data = [];
  268. $keys = [
  269. 'page' => 'pages',
  270. 'ns' => 'namespaces',
  271. ];
  272. foreach ( $restrictions as $restriction ) {
  273. $key = $keys[$restriction->getType()];
  274. $id = $restriction->getBlockId();
  275. switch ( $restriction->getType() ) {
  276. case 'page':
  277. /** @var \MediaWiki\Block\Restriction\PageRestriction $restriction */
  278. '@phan-var \MediaWiki\Block\Restriction\PageRestriction $restriction';
  279. $value = [ 'id' => $restriction->getValue() ];
  280. if ( $restriction->getTitle() ) {
  281. self::addTitleInfo( $value, $restriction->getTitle() );
  282. }
  283. break;
  284. default:
  285. $value = $restriction->getValue();
  286. }
  287. if ( !isset( $data[$id][$key] ) ) {
  288. $data[$id][$key] = [];
  289. ApiResult::setIndexedTagName( $data[$id][$key], $restriction->getType() );
  290. }
  291. $data[$id][$key][] = $value;
  292. }
  293. return $data;
  294. }
  295. public function getAllowedParams() {
  296. $blockCIDRLimit = $this->getConfig()->get( 'BlockCIDRLimit' );
  297. return [
  298. 'start' => [
  299. ApiBase::PARAM_TYPE => 'timestamp'
  300. ],
  301. 'end' => [
  302. ApiBase::PARAM_TYPE => 'timestamp',
  303. ],
  304. 'dir' => [
  305. ApiBase::PARAM_TYPE => [
  306. 'newer',
  307. 'older'
  308. ],
  309. ApiBase::PARAM_DFLT => 'older',
  310. ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
  311. ],
  312. 'ids' => [
  313. ApiBase::PARAM_TYPE => 'integer',
  314. ApiBase::PARAM_ISMULTI => true
  315. ],
  316. 'users' => [
  317. ApiBase::PARAM_TYPE => 'user',
  318. ApiBase::PARAM_ISMULTI => true
  319. ],
  320. 'ip' => [
  321. ApiBase::PARAM_HELP_MSG => [
  322. 'apihelp-query+blocks-param-ip',
  323. $blockCIDRLimit['IPv4'],
  324. $blockCIDRLimit['IPv6'],
  325. ],
  326. ],
  327. 'limit' => [
  328. ApiBase::PARAM_DFLT => 10,
  329. ApiBase::PARAM_TYPE => 'limit',
  330. ApiBase::PARAM_MIN => 1,
  331. ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
  332. ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
  333. ],
  334. 'prop' => [
  335. ApiBase::PARAM_DFLT => 'id|user|by|timestamp|expiry|reason|flags',
  336. ApiBase::PARAM_TYPE => [
  337. 'id',
  338. 'user',
  339. 'userid',
  340. 'by',
  341. 'byid',
  342. 'timestamp',
  343. 'expiry',
  344. 'reason',
  345. 'range',
  346. 'flags',
  347. 'restrictions',
  348. ],
  349. ApiBase::PARAM_ISMULTI => true,
  350. ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
  351. ],
  352. 'show' => [
  353. ApiBase::PARAM_TYPE => [
  354. 'account',
  355. '!account',
  356. 'temp',
  357. '!temp',
  358. 'ip',
  359. '!ip',
  360. 'range',
  361. '!range',
  362. ],
  363. ApiBase::PARAM_ISMULTI => true
  364. ],
  365. 'continue' => [
  366. ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
  367. ],
  368. ];
  369. }
  370. protected function getExamplesMessages() {
  371. return [
  372. 'action=query&list=blocks'
  373. => 'apihelp-query+blocks-example-simple',
  374. 'action=query&list=blocks&bkusers=Alice|Bob'
  375. => 'apihelp-query+blocks-example-users',
  376. ];
  377. }
  378. public function getHelpUrls() {
  379. return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Blocks';
  380. }
  381. }