]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/api/ApiQueryUserContributions.php
MediaWiki 1.30.2
[autoinstalls/mediawiki.git] / includes / api / ApiQueryUserContributions.php
1 <?php
2 /**
3  *
4  *
5  * Created on Oct 16, 2006
6  *
7  * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
8  *
9  * This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 2 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License along
20  * with this program; if not, write to the Free Software Foundation, Inc.,
21  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22  * http://www.gnu.org/copyleft/gpl.html
23  *
24  * @file
25  */
26
27 /**
28  * This query action adds a list of a specified user's contributions to the output.
29  *
30  * @ingroup API
31  */
32 class ApiQueryContributions extends ApiQueryBase {
33
34         public function __construct( ApiQuery $query, $moduleName ) {
35                 parent::__construct( $query, $moduleName, 'uc' );
36         }
37
38         private $params, $prefixMode, $userprefix, $multiUserMode, $idMode, $usernames, $userids,
39                 $parentLens, $commentStore;
40         private $fld_ids = false, $fld_title = false, $fld_timestamp = false,
41                 $fld_comment = false, $fld_parsedcomment = false, $fld_flags = false,
42                 $fld_patrolled = false, $fld_tags = false, $fld_size = false, $fld_sizediff = false;
43
44         public function execute() {
45                 // Parse some parameters
46                 $this->params = $this->extractRequestParams();
47
48                 $this->commentStore = new CommentStore( 'rev_comment' );
49
50                 $prop = array_flip( $this->params['prop'] );
51                 $this->fld_ids = isset( $prop['ids'] );
52                 $this->fld_title = isset( $prop['title'] );
53                 $this->fld_comment = isset( $prop['comment'] );
54                 $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
55                 $this->fld_size = isset( $prop['size'] );
56                 $this->fld_sizediff = isset( $prop['sizediff'] );
57                 $this->fld_flags = isset( $prop['flags'] );
58                 $this->fld_timestamp = isset( $prop['timestamp'] );
59                 $this->fld_patrolled = isset( $prop['patrolled'] );
60                 $this->fld_tags = isset( $prop['tags'] );
61
62                 // Most of this code will use the 'contributions' group DB, which can map to replica DBs
63                 // with extra user based indexes or partioning by user. The additional metadata
64                 // queries should use a regular replica DB since the lookup pattern is not all by user.
65                 $dbSecondary = $this->getDB(); // any random replica DB
66
67                 // TODO: if the query is going only against the revision table, should this be done?
68                 $this->selectNamedDB( 'contributions', DB_REPLICA, 'contributions' );
69
70                 $this->requireOnlyOneParameter( $this->params, 'userprefix', 'userids', 'user' );
71
72                 $this->idMode = false;
73                 if ( isset( $this->params['userprefix'] ) ) {
74                         $this->prefixMode = true;
75                         $this->multiUserMode = true;
76                         $this->userprefix = $this->params['userprefix'];
77                 } elseif ( isset( $this->params['userids'] ) ) {
78                         $this->userids = [];
79
80                         if ( !count( $this->params['userids'] ) ) {
81                                 $encParamName = $this->encodeParamName( 'userids' );
82                                 $this->dieWithError( [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName" );
83                         }
84
85                         foreach ( $this->params['userids'] as $uid ) {
86                                 if ( $uid <= 0 ) {
87                                         $this->dieWithError( [ 'apierror-invaliduserid', $uid ], 'invaliduserid' );
88                                 }
89
90                                 $this->userids[] = $uid;
91                         }
92
93                         $this->prefixMode = false;
94                         $this->multiUserMode = ( count( $this->params['userids'] ) > 1 );
95                         $this->idMode = true;
96                 } else {
97                         $anyIPs = false;
98                         $this->userids = [];
99                         $this->usernames = [];
100                         if ( !count( $this->params['user'] ) ) {
101                                 $encParamName = $this->encodeParamName( 'user' );
102                                 $this->dieWithError(
103                                         [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName"
104                                 );
105                         }
106                         foreach ( $this->params['user'] as $u ) {
107                                 if ( $u === '' ) {
108                                         $encParamName = $this->encodeParamName( 'user' );
109                                         $this->dieWithError(
110                                                 [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName"
111                                         );
112                                 }
113
114                                 if ( User::isIP( $u ) ) {
115                                         $anyIPs = true;
116                                         $this->usernames[] = $u;
117                                 } else {
118                                         $name = User::getCanonicalName( $u, 'valid' );
119                                         if ( $name === false ) {
120                                                 $encParamName = $this->encodeParamName( 'user' );
121                                                 $this->dieWithError(
122                                                         [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $u ) ], "baduser_$encParamName"
123                                                 );
124                                         }
125                                         $this->usernames[] = $name;
126                                 }
127                         }
128                         $this->prefixMode = false;
129                         $this->multiUserMode = ( count( $this->params['user'] ) > 1 );
130
131                         if ( !$anyIPs ) {
132                                 $dbr = $this->getDB();
133                                 $res = $dbr->select( 'user', 'user_id', [ 'user_name' => $this->usernames ], __METHOD__ );
134                                 foreach ( $res as $row ) {
135                                         $this->userids[] = $row->user_id;
136                                 }
137                                 $this->idMode = count( $this->userids ) === count( $this->usernames );
138                         }
139                 }
140
141                 $this->prepareQuery();
142
143                 $hookData = [];
144                 // Do the actual query.
145                 $res = $this->select( __METHOD__, [], $hookData );
146
147                 if ( $this->fld_sizediff ) {
148                         $revIds = [];
149                         foreach ( $res as $row ) {
150                                 if ( $row->rev_parent_id ) {
151                                         $revIds[] = $row->rev_parent_id;
152                                 }
153                         }
154                         $this->parentLens = Revision::getParentLengths( $dbSecondary, $revIds );
155                         $res->rewind(); // reset
156                 }
157
158                 // Initialise some variables
159                 $count = 0;
160                 $limit = $this->params['limit'];
161
162                 // Fetch each row
163                 foreach ( $res as $row ) {
164                         if ( ++$count > $limit ) {
165                                 // We've reached the one extra which shows that there are
166                                 // additional pages to be had. Stop here...
167                                 $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
168                                 break;
169                         }
170
171                         $vals = $this->extractRowInfo( $row );
172                         $fit = $this->processRow( $row, $vals, $hookData ) &&
173                                 $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals );
174                         if ( !$fit ) {
175                                 $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
176                                 break;
177                         }
178                 }
179
180                 $this->getResult()->addIndexedTagName(
181                         [ 'query', $this->getModuleName() ],
182                         'item'
183                 );
184         }
185
186         /**
187          * Prepares the query and returns the limit of rows requested
188          */
189         private function prepareQuery() {
190                 // We're after the revision table, and the corresponding page
191                 // row for anything we retrieve. We may also need the
192                 // recentchanges row and/or tag summary row.
193                 $user = $this->getUser();
194                 $tables = [ 'page', 'revision' ]; // Order may change
195                 $this->addWhere( 'page_id=rev_page' );
196
197                 // Handle continue parameter
198                 if ( !is_null( $this->params['continue'] ) ) {
199                         $continue = explode( '|', $this->params['continue'] );
200                         $db = $this->getDB();
201                         if ( $this->multiUserMode ) {
202                                 $this->dieContinueUsageIf( count( $continue ) != 4 );
203                                 $modeFlag = array_shift( $continue );
204                                 $this->dieContinueUsageIf( !in_array( $modeFlag, [ 'id', 'name' ] ) );
205                                 if ( $this->idMode && $modeFlag === 'name' ) {
206                                         // The users were created since this query started, but we
207                                         // can't go back and change modes now. So just keep on with
208                                         // name mode.
209                                         $this->idMode = false;
210                                 }
211                                 $this->dieContinueUsageIf( ( $modeFlag === 'id' ) !== $this->idMode );
212                                 $userField = $this->idMode ? 'rev_user' : 'rev_user_text';
213                                 $encUser = $db->addQuotes( array_shift( $continue ) );
214                         } else {
215                                 $this->dieContinueUsageIf( count( $continue ) != 2 );
216                         }
217                         $encTS = $db->addQuotes( $db->timestamp( $continue[0] ) );
218                         $encId = (int)$continue[1];
219                         $this->dieContinueUsageIf( $encId != $continue[1] );
220                         $op = ( $this->params['dir'] == 'older' ? '<' : '>' );
221                         if ( $this->multiUserMode ) {
222                                 $this->addWhere(
223                                         "$userField $op $encUser OR " .
224                                         "($userField = $encUser AND " .
225                                         "(rev_timestamp $op $encTS OR " .
226                                         "(rev_timestamp = $encTS AND " .
227                                         "rev_id $op= $encId)))"
228                                 );
229                         } else {
230                                 $this->addWhere(
231                                         "rev_timestamp $op $encTS OR " .
232                                         "(rev_timestamp = $encTS AND " .
233                                         "rev_id $op= $encId)"
234                                 );
235                         }
236                 }
237
238                 // Don't include any revisions where we're not supposed to be able to
239                 // see the username.
240                 if ( !$user->isAllowed( 'deletedhistory' ) ) {
241                         $bitmask = Revision::DELETED_USER;
242                 } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
243                         $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
244                 } else {
245                         $bitmask = 0;
246                 }
247                 if ( $bitmask ) {
248                         $this->addWhere( $this->getDB()->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" );
249                 }
250
251                 // We only want pages by the specified users.
252                 if ( $this->prefixMode ) {
253                         $this->addWhere( 'rev_user_text' .
254                                 $this->getDB()->buildLike( $this->userprefix, $this->getDB()->anyString() ) );
255                 } elseif ( $this->idMode ) {
256                         $this->addWhereFld( 'rev_user', $this->userids );
257                 } else {
258                         $this->addWhereFld( 'rev_user_text', $this->usernames );
259                 }
260                 // ... and in the specified timeframe.
261                 // Ensure the same sort order for rev_user/rev_user_text and rev_timestamp
262                 // so our query is indexed
263                 if ( $this->multiUserMode ) {
264                         $this->addWhereRange( $this->idMode ? 'rev_user' : 'rev_user_text',
265                                 $this->params['dir'], null, null );
266                 }
267                 $this->addTimestampWhereRange( 'rev_timestamp',
268                         $this->params['dir'], $this->params['start'], $this->params['end'] );
269                 // Include in ORDER BY for uniqueness
270                 $this->addWhereRange( 'rev_id', $this->params['dir'], null, null );
271
272                 $this->addWhereFld( 'page_namespace', $this->params['namespace'] );
273
274                 $show = $this->params['show'];
275                 if ( $this->params['toponly'] ) { // deprecated/old param
276                         $show[] = 'top';
277                 }
278                 if ( !is_null( $show ) ) {
279                         $show = array_flip( $show );
280
281                         if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) )
282                                 || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) )
283                                 || ( isset( $show['top'] ) && isset( $show['!top'] ) )
284                                 || ( isset( $show['new'] ) && isset( $show['!new'] ) )
285                         ) {
286                                 $this->dieWithError( 'apierror-show' );
287                         }
288
289                         $this->addWhereIf( 'rev_minor_edit = 0', isset( $show['!minor'] ) );
290                         $this->addWhereIf( 'rev_minor_edit != 0', isset( $show['minor'] ) );
291                         $this->addWhereIf( 'rc_patrolled = 0', isset( $show['!patrolled'] ) );
292                         $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) );
293                         $this->addWhereIf( 'rev_id != page_latest', isset( $show['!top'] ) );
294                         $this->addWhereIf( 'rev_id = page_latest', isset( $show['top'] ) );
295                         $this->addWhereIf( 'rev_parent_id != 0', isset( $show['!new'] ) );
296                         $this->addWhereIf( 'rev_parent_id = 0', isset( $show['new'] ) );
297                 }
298                 $this->addOption( 'LIMIT', $this->params['limit'] + 1 );
299
300                 // Mandatory fields: timestamp allows request continuation
301                 // ns+title checks if the user has access rights for this page
302                 // user_text is necessary if multiple users were specified
303                 $this->addFields( [
304                         'rev_id',
305                         'rev_timestamp',
306                         'page_namespace',
307                         'page_title',
308                         'rev_user',
309                         'rev_user_text',
310                         'rev_deleted'
311                 ] );
312
313                 if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ||
314                         $this->fld_patrolled
315                 ) {
316                         if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
317                                 $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
318                         }
319
320                         // Use a redundant join condition on both
321                         // timestamp and ID so we can use the timestamp
322                         // index
323                         $index['recentchanges'] = 'rc_user_text';
324                         if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ) {
325                                 // Put the tables in the right order for
326                                 // STRAIGHT_JOIN
327                                 $tables = [ 'revision', 'recentchanges', 'page' ];
328                                 $this->addOption( 'STRAIGHT_JOIN' );
329                                 $this->addWhere( 'rc_user_text=rev_user_text' );
330                                 $this->addWhere( 'rc_timestamp=rev_timestamp' );
331                                 $this->addWhere( 'rc_this_oldid=rev_id' );
332                         } else {
333                                 $tables[] = 'recentchanges';
334                                 $this->addJoinConds( [ 'recentchanges' => [
335                                         'LEFT JOIN', [
336                                                 'rc_user_text=rev_user_text',
337                                                 'rc_timestamp=rev_timestamp',
338                                                 'rc_this_oldid=rev_id' ] ] ] );
339                         }
340                 }
341
342                 $this->addTables( $tables );
343                 $this->addFieldsIf( 'rev_page', $this->fld_ids );
344                 $this->addFieldsIf( 'page_latest', $this->fld_flags );
345                 // $this->addFieldsIf( 'rev_text_id', $this->fld_ids ); // Should this field be exposed?
346                 $this->addFieldsIf( 'rev_len', $this->fld_size || $this->fld_sizediff );
347                 $this->addFieldsIf( 'rev_minor_edit', $this->fld_flags );
348                 $this->addFieldsIf( 'rev_parent_id', $this->fld_flags || $this->fld_sizediff || $this->fld_ids );
349                 $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled );
350
351                 if ( $this->fld_comment || $this->fld_parsedcomment ) {
352                         $commentQuery = $this->commentStore->getJoin();
353                         $this->addTables( $commentQuery['tables'] );
354                         $this->addFields( $commentQuery['fields'] );
355                         $this->addJoinConds( $commentQuery['joins'] );
356                 }
357
358                 if ( $this->fld_tags ) {
359                         $this->addTables( 'tag_summary' );
360                         $this->addJoinConds(
361                                 [ 'tag_summary' => [ 'LEFT JOIN', [ 'rev_id=ts_rev_id' ] ] ]
362                         );
363                         $this->addFields( 'ts_tags' );
364                 }
365
366                 if ( isset( $this->params['tag'] ) ) {
367                         $this->addTables( 'change_tag' );
368                         $this->addJoinConds(
369                                 [ 'change_tag' => [ 'INNER JOIN', [ 'rev_id=ct_rev_id' ] ] ]
370                         );
371                         $this->addWhereFld( 'ct_tag', $this->params['tag'] );
372                 }
373
374                 if ( isset( $index ) ) {
375                         $this->addOption( 'USE INDEX', $index );
376                 }
377         }
378
379         /**
380          * Extract fields from the database row and append them to a result array
381          *
382          * @param stdClass $row
383          * @return array
384          */
385         private function extractRowInfo( $row ) {
386                 $vals = [];
387                 $anyHidden = false;
388
389                 if ( $row->rev_deleted & Revision::DELETED_TEXT ) {
390                         $vals['texthidden'] = true;
391                         $anyHidden = true;
392                 }
393
394                 // Any rows where we can't view the user were filtered out in the query.
395                 $vals['userid'] = (int)$row->rev_user;
396                 $vals['user'] = $row->rev_user_text;
397                 if ( $row->rev_deleted & Revision::DELETED_USER ) {
398                         $vals['userhidden'] = true;
399                         $anyHidden = true;
400                 }
401                 if ( $this->fld_ids ) {
402                         $vals['pageid'] = intval( $row->rev_page );
403                         $vals['revid'] = intval( $row->rev_id );
404                         // $vals['textid'] = intval( $row->rev_text_id ); // todo: Should this field be exposed?
405
406                         if ( !is_null( $row->rev_parent_id ) ) {
407                                 $vals['parentid'] = intval( $row->rev_parent_id );
408                         }
409                 }
410
411                 $title = Title::makeTitle( $row->page_namespace, $row->page_title );
412
413                 if ( $this->fld_title ) {
414                         ApiQueryBase::addTitleInfo( $vals, $title );
415                 }
416
417                 if ( $this->fld_timestamp ) {
418                         $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->rev_timestamp );
419                 }
420
421                 if ( $this->fld_flags ) {
422                         $vals['new'] = $row->rev_parent_id == 0 && !is_null( $row->rev_parent_id );
423                         $vals['minor'] = (bool)$row->rev_minor_edit;
424                         $vals['top'] = $row->page_latest == $row->rev_id;
425                 }
426
427                 if ( $this->fld_comment || $this->fld_parsedcomment ) {
428                         if ( $row->rev_deleted & Revision::DELETED_COMMENT ) {
429                                 $vals['commenthidden'] = true;
430                                 $anyHidden = true;
431                         }
432
433                         $userCanView = Revision::userCanBitfield(
434                                 $row->rev_deleted,
435                                 Revision::DELETED_COMMENT, $this->getUser()
436                         );
437
438                         if ( $userCanView ) {
439                                 $comment = $this->commentStore->getComment( $row )->text;
440                                 if ( $this->fld_comment ) {
441                                         $vals['comment'] = $comment;
442                                 }
443
444                                 if ( $this->fld_parsedcomment ) {
445                                         $vals['parsedcomment'] = Linker::formatComment( $comment, $title );
446                                 }
447                         }
448                 }
449
450                 if ( $this->fld_patrolled ) {
451                         $vals['patrolled'] = (bool)$row->rc_patrolled;
452                 }
453
454                 if ( $this->fld_size && !is_null( $row->rev_len ) ) {
455                         $vals['size'] = intval( $row->rev_len );
456                 }
457
458                 if ( $this->fld_sizediff
459                         && !is_null( $row->rev_len )
460                         && !is_null( $row->rev_parent_id )
461                 ) {
462                         $parentLen = isset( $this->parentLens[$row->rev_parent_id] )
463                                 ? $this->parentLens[$row->rev_parent_id]
464                                 : 0;
465                         $vals['sizediff'] = intval( $row->rev_len - $parentLen );
466                 }
467
468                 if ( $this->fld_tags ) {
469                         if ( $row->ts_tags ) {
470                                 $tags = explode( ',', $row->ts_tags );
471                                 ApiResult::setIndexedTagName( $tags, 'tag' );
472                                 $vals['tags'] = $tags;
473                         } else {
474                                 $vals['tags'] = [];
475                         }
476                 }
477
478                 if ( $anyHidden && $row->rev_deleted & Revision::DELETED_RESTRICTED ) {
479                         $vals['suppressed'] = true;
480                 }
481
482                 return $vals;
483         }
484
485         private function continueStr( $row ) {
486                 if ( $this->multiUserMode ) {
487                         if ( $this->idMode ) {
488                                 return "id|$row->rev_user|$row->rev_timestamp|$row->rev_id";
489                         } else {
490                                 return "name|$row->rev_user_text|$row->rev_timestamp|$row->rev_id";
491                         }
492                 } else {
493                         return "$row->rev_timestamp|$row->rev_id";
494                 }
495         }
496
497         public function getCacheMode( $params ) {
498                 // This module provides access to deleted revisions and patrol flags if
499                 // the requester is logged in
500                 return 'anon-public-user-private';
501         }
502
503         public function getAllowedParams() {
504                 return [
505                         'limit' => [
506                                 ApiBase::PARAM_DFLT => 10,
507                                 ApiBase::PARAM_TYPE => 'limit',
508                                 ApiBase::PARAM_MIN => 1,
509                                 ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
510                                 ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
511                         ],
512                         'start' => [
513                                 ApiBase::PARAM_TYPE => 'timestamp'
514                         ],
515                         'end' => [
516                                 ApiBase::PARAM_TYPE => 'timestamp'
517                         ],
518                         'continue' => [
519                                 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
520                         ],
521                         'user' => [
522                                 ApiBase::PARAM_TYPE => 'user',
523                                 ApiBase::PARAM_ISMULTI => true
524                         ],
525                         'userids' => [
526                                 ApiBase::PARAM_TYPE => 'integer',
527                                 ApiBase::PARAM_ISMULTI => true
528                         ],
529                         'userprefix' => null,
530                         'dir' => [
531                                 ApiBase::PARAM_DFLT => 'older',
532                                 ApiBase::PARAM_TYPE => [
533                                         'newer',
534                                         'older'
535                                 ],
536                                 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
537                         ],
538                         'namespace' => [
539                                 ApiBase::PARAM_ISMULTI => true,
540                                 ApiBase::PARAM_TYPE => 'namespace'
541                         ],
542                         'prop' => [
543                                 ApiBase::PARAM_ISMULTI => true,
544                                 ApiBase::PARAM_DFLT => 'ids|title|timestamp|comment|size|flags',
545                                 ApiBase::PARAM_TYPE => [
546                                         'ids',
547                                         'title',
548                                         'timestamp',
549                                         'comment',
550                                         'parsedcomment',
551                                         'size',
552                                         'sizediff',
553                                         'flags',
554                                         'patrolled',
555                                         'tags'
556                                 ],
557                                 ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
558                         ],
559                         'show' => [
560                                 ApiBase::PARAM_ISMULTI => true,
561                                 ApiBase::PARAM_TYPE => [
562                                         'minor',
563                                         '!minor',
564                                         'patrolled',
565                                         '!patrolled',
566                                         'top',
567                                         '!top',
568                                         'new',
569                                         '!new',
570                                 ],
571                                 ApiBase::PARAM_HELP_MSG => [
572                                         'apihelp-query+usercontribs-param-show',
573                                         $this->getConfig()->get( 'RCMaxAge' )
574                                 ],
575                         ],
576                         'tag' => null,
577                         'toponly' => [
578                                 ApiBase::PARAM_DFLT => false,
579                                 ApiBase::PARAM_DEPRECATED => true,
580                         ],
581                 ];
582         }
583
584         protected function getExamplesMessages() {
585                 return [
586                         'action=query&list=usercontribs&ucuser=Example'
587                                 => 'apihelp-query+usercontribs-example-user',
588                         'action=query&list=usercontribs&ucuserprefix=192.0.2.'
589                                 => 'apihelp-query+usercontribs-example-ipprefix',
590                 ];
591         }
592
593         public function getHelpUrls() {
594                 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Usercontribs';
595         }
596 }