]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/api/ApiQueryRecentChanges.php
MediaWiki 1.30.2-scripts2
[autoinstalls/mediawiki.git] / includes / api / ApiQueryRecentChanges.php
1 <?php
2 /**
3  *
4  *
5  * Created on Oct 19, 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  * A query action to enumerate the recent changes that were done to the wiki.
29  * Various filters are supported.
30  *
31  * @ingroup API
32  */
33 class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
34
35         public function __construct( ApiQuery $query, $moduleName ) {
36                 parent::__construct( $query, $moduleName, 'rc' );
37         }
38
39         private $commentStore;
40
41         private $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_userid = false,
42                 $fld_flags = false, $fld_timestamp = false, $fld_title = false, $fld_ids = false,
43                 $fld_sizes = false, $fld_redirect = false, $fld_patrolled = false, $fld_loginfo = false,
44                 $fld_tags = false, $fld_sha1 = false, $token = [];
45
46         private $tokenFunctions;
47
48         /**
49          * Get an array mapping token names to their handler functions.
50          * The prototype for a token function is func($pageid, $title, $rc)
51          * it should return a token or false (permission denied)
52          * @deprecated since 1.24
53          * @return array [ tokenname => function ]
54          */
55         protected function getTokenFunctions() {
56                 // Don't call the hooks twice
57                 if ( isset( $this->tokenFunctions ) ) {
58                         return $this->tokenFunctions;
59                 }
60
61                 // If we're in a mode that breaks the same-origin policy, no tokens can
62                 // be obtained
63                 if ( $this->lacksSameOriginSecurity() ) {
64                         return [];
65                 }
66
67                 $this->tokenFunctions = [
68                         'patrol' => [ 'ApiQueryRecentChanges', 'getPatrolToken' ]
69                 ];
70                 Hooks::run( 'APIQueryRecentChangesTokens', [ &$this->tokenFunctions ] );
71
72                 return $this->tokenFunctions;
73         }
74
75         /**
76          * @deprecated since 1.24
77          * @param int $pageid
78          * @param Title $title
79          * @param RecentChange|null $rc
80          * @return bool|string
81          */
82         public static function getPatrolToken( $pageid, $title, $rc = null ) {
83                 global $wgUser;
84
85                 $validTokenUser = false;
86
87                 if ( $rc ) {
88                         if ( ( $wgUser->useRCPatrol() && $rc->getAttribute( 'rc_type' ) == RC_EDIT ) ||
89                                 ( $wgUser->useNPPatrol() && $rc->getAttribute( 'rc_type' ) == RC_NEW )
90                         ) {
91                                 $validTokenUser = true;
92                         }
93                 } elseif ( $wgUser->useRCPatrol() || $wgUser->useNPPatrol() ) {
94                         $validTokenUser = true;
95                 }
96
97                 if ( $validTokenUser ) {
98                         // The patrol token is always the same, let's exploit that
99                         static $cachedPatrolToken = null;
100
101                         if ( is_null( $cachedPatrolToken ) ) {
102                                 $cachedPatrolToken = $wgUser->getEditToken( 'patrol' );
103                         }
104
105                         return $cachedPatrolToken;
106                 }
107
108                 return false;
109         }
110
111         /**
112          * Sets internal state to include the desired properties in the output.
113          * @param array $prop Associative array of properties, only keys are used here
114          */
115         public function initProperties( $prop ) {
116                 $this->fld_comment = isset( $prop['comment'] );
117                 $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
118                 $this->fld_user = isset( $prop['user'] );
119                 $this->fld_userid = isset( $prop['userid'] );
120                 $this->fld_flags = isset( $prop['flags'] );
121                 $this->fld_timestamp = isset( $prop['timestamp'] );
122                 $this->fld_title = isset( $prop['title'] );
123                 $this->fld_ids = isset( $prop['ids'] );
124                 $this->fld_sizes = isset( $prop['sizes'] );
125                 $this->fld_redirect = isset( $prop['redirect'] );
126                 $this->fld_patrolled = isset( $prop['patrolled'] );
127                 $this->fld_loginfo = isset( $prop['loginfo'] );
128                 $this->fld_tags = isset( $prop['tags'] );
129                 $this->fld_sha1 = isset( $prop['sha1'] );
130         }
131
132         public function execute() {
133                 $this->run();
134         }
135
136         public function executeGenerator( $resultPageSet ) {
137                 $this->run( $resultPageSet );
138         }
139
140         /**
141          * Generates and outputs the result of this query based upon the provided parameters.
142          *
143          * @param ApiPageSet $resultPageSet
144          */
145         public function run( $resultPageSet = null ) {
146                 $user = $this->getUser();
147                 /* Get the parameters of the request. */
148                 $params = $this->extractRequestParams();
149
150                 /* Build our basic query. Namely, something along the lines of:
151                  * SELECT * FROM recentchanges WHERE rc_timestamp > $start
152                  *   AND rc_timestamp < $end AND rc_namespace = $namespace
153                  */
154                 $this->addTables( 'recentchanges' );
155                 $this->addTimestampWhereRange( 'rc_timestamp', $params['dir'], $params['start'], $params['end'] );
156
157                 if ( !is_null( $params['continue'] ) ) {
158                         $cont = explode( '|', $params['continue'] );
159                         $this->dieContinueUsageIf( count( $cont ) != 2 );
160                         $db = $this->getDB();
161                         $timestamp = $db->addQuotes( $db->timestamp( $cont[0] ) );
162                         $id = intval( $cont[1] );
163                         $this->dieContinueUsageIf( $id != $cont[1] );
164                         $op = $params['dir'] === 'older' ? '<' : '>';
165                         $this->addWhere(
166                                 "rc_timestamp $op $timestamp OR " .
167                                 "(rc_timestamp = $timestamp AND " .
168                                 "rc_id $op= $id)"
169                         );
170                 }
171
172                 $order = $params['dir'] === 'older' ? 'DESC' : 'ASC';
173                 $this->addOption( 'ORDER BY', [
174                         "rc_timestamp $order",
175                         "rc_id $order",
176                 ] );
177
178                 $this->addWhereFld( 'rc_namespace', $params['namespace'] );
179
180                 if ( !is_null( $params['type'] ) ) {
181                         try {
182                                 $this->addWhereFld( 'rc_type', RecentChange::parseToRCType( $params['type'] ) );
183                         } catch ( Exception $e ) {
184                                 ApiBase::dieDebug( __METHOD__, $e->getMessage() );
185                         }
186                 }
187
188                 if ( !is_null( $params['show'] ) ) {
189                         $show = array_flip( $params['show'] );
190
191                         /* Check for conflicting parameters. */
192                         if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) )
193                                 || ( isset( $show['bot'] ) && isset( $show['!bot'] ) )
194                                 || ( isset( $show['anon'] ) && isset( $show['!anon'] ) )
195                                 || ( isset( $show['redirect'] ) && isset( $show['!redirect'] ) )
196                                 || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) )
197                                 || ( isset( $show['patrolled'] ) && isset( $show['unpatrolled'] ) )
198                                 || ( isset( $show['!patrolled'] ) && isset( $show['unpatrolled'] ) )
199                         ) {
200                                 $this->dieWithError( 'apierror-show' );
201                         }
202
203                         // Check permissions
204                         if ( $this->includesPatrollingFlags( $show ) ) {
205                                 if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
206                                         $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
207                                 }
208                         }
209
210                         /* Add additional conditions to query depending upon parameters. */
211                         $this->addWhereIf( 'rc_minor = 0', isset( $show['!minor'] ) );
212                         $this->addWhereIf( 'rc_minor != 0', isset( $show['minor'] ) );
213                         $this->addWhereIf( 'rc_bot = 0', isset( $show['!bot'] ) );
214                         $this->addWhereIf( 'rc_bot != 0', isset( $show['bot'] ) );
215                         $this->addWhereIf( 'rc_user = 0', isset( $show['anon'] ) );
216                         $this->addWhereIf( 'rc_user != 0', isset( $show['!anon'] ) );
217                         $this->addWhereIf( 'rc_patrolled = 0', isset( $show['!patrolled'] ) );
218                         $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) );
219                         $this->addWhereIf( 'page_is_redirect = 1', isset( $show['redirect'] ) );
220
221                         if ( isset( $show['unpatrolled'] ) ) {
222                                 // See ChangesList::isUnpatrolled
223                                 if ( $user->useRCPatrol() ) {
224                                         $this->addWhere( 'rc_patrolled = 0' );
225                                 } elseif ( $user->useNPPatrol() ) {
226                                         $this->addWhere( 'rc_patrolled = 0' );
227                                         $this->addWhereFld( 'rc_type', RC_NEW );
228                                 }
229                         }
230
231                         // Don't throw log entries out the window here
232                         $this->addWhereIf(
233                                 'page_is_redirect = 0 OR page_is_redirect IS NULL',
234                                 isset( $show['!redirect'] )
235                         );
236                 }
237
238                 $this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
239
240                 if ( !is_null( $params['user'] ) ) {
241                         $this->addWhereFld( 'rc_user_text', $params['user'] );
242                 }
243
244                 if ( !is_null( $params['excludeuser'] ) ) {
245                         // We don't use the rc_user_text index here because
246                         // * it would require us to sort by rc_user_text before rc_timestamp
247                         // * the != condition doesn't throw out too many rows anyway
248                         $this->addWhere( 'rc_user_text != ' . $this->getDB()->addQuotes( $params['excludeuser'] ) );
249                 }
250
251                 /* Add the fields we're concerned with to our query. */
252                 $this->addFields( [
253                         'rc_id',
254                         'rc_timestamp',
255                         'rc_namespace',
256                         'rc_title',
257                         'rc_cur_id',
258                         'rc_type',
259                         'rc_deleted'
260                 ] );
261
262                 $showRedirects = false;
263                 /* Determine what properties we need to display. */
264                 if ( !is_null( $params['prop'] ) ) {
265                         $prop = array_flip( $params['prop'] );
266
267                         /* Set up internal members based upon params. */
268                         $this->initProperties( $prop );
269
270                         if ( $this->fld_patrolled && !$user->useRCPatrol() && !$user->useNPPatrol() ) {
271                                 $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
272                         }
273
274                         /* Add fields to our query if they are specified as a needed parameter. */
275                         $this->addFieldsIf( [ 'rc_this_oldid', 'rc_last_oldid' ], $this->fld_ids );
276                         $this->addFieldsIf( 'rc_user', $this->fld_user || $this->fld_userid );
277                         $this->addFieldsIf( 'rc_user_text', $this->fld_user );
278                         $this->addFieldsIf( [ 'rc_minor', 'rc_type', 'rc_bot' ], $this->fld_flags );
279                         $this->addFieldsIf( [ 'rc_old_len', 'rc_new_len' ], $this->fld_sizes );
280                         $this->addFieldsIf( [ 'rc_patrolled', 'rc_log_type' ], $this->fld_patrolled );
281                         $this->addFieldsIf(
282                                 [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
283                                 $this->fld_loginfo
284                         );
285                         $showRedirects = $this->fld_redirect || isset( $show['redirect'] )
286                                 || isset( $show['!redirect'] );
287                 }
288                 $this->addFieldsIf( [ 'rc_this_oldid' ],
289                         $resultPageSet && $params['generaterevisions'] );
290
291                 if ( $this->fld_tags ) {
292                         $this->addTables( 'tag_summary' );
293                         $this->addJoinConds( [ 'tag_summary' => [ 'LEFT JOIN', [ 'rc_id=ts_rc_id' ] ] ] );
294                         $this->addFields( 'ts_tags' );
295                 }
296
297                 if ( $this->fld_sha1 ) {
298                         $this->addTables( 'revision' );
299                         $this->addJoinConds( [ 'revision' => [ 'LEFT JOIN',
300                                 [ 'rc_this_oldid=rev_id' ] ] ] );
301                         $this->addFields( [ 'rev_sha1', 'rev_deleted' ] );
302                 }
303
304                 if ( $params['toponly'] || $showRedirects ) {
305                         $this->addTables( 'page' );
306                         $this->addJoinConds( [ 'page' => [ 'LEFT JOIN',
307                                 [ 'rc_namespace=page_namespace', 'rc_title=page_title' ] ] ] );
308                         $this->addFields( 'page_is_redirect' );
309
310                         if ( $params['toponly'] ) {
311                                 $this->addWhere( 'rc_this_oldid = page_latest' );
312                         }
313                 }
314
315                 if ( !is_null( $params['tag'] ) ) {
316                         $this->addTables( 'change_tag' );
317                         $this->addJoinConds( [ 'change_tag' => [ 'INNER JOIN', [ 'rc_id=ct_rc_id' ] ] ] );
318                         $this->addWhereFld( 'ct_tag', $params['tag'] );
319                 }
320
321                 // Paranoia: avoid brute force searches (T19342)
322                 if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
323                         if ( !$user->isAllowed( 'deletedhistory' ) ) {
324                                 $bitmask = Revision::DELETED_USER;
325                         } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
326                                 $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
327                         } else {
328                                 $bitmask = 0;
329                         }
330                         if ( $bitmask ) {
331                                 $this->addWhere( $this->getDB()->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask" );
332                         }
333                 }
334                 if ( $this->getRequest()->getCheck( 'namespace' ) ) {
335                         // LogPage::DELETED_ACTION hides the affected page, too.
336                         if ( !$user->isAllowed( 'deletedhistory' ) ) {
337                                 $bitmask = LogPage::DELETED_ACTION;
338                         } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
339                                 $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
340                         } else {
341                                 $bitmask = 0;
342                         }
343                         if ( $bitmask ) {
344                                 $this->addWhere( $this->getDB()->makeList( [
345                                         'rc_type != ' . RC_LOG,
346                                         $this->getDB()->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
347                                 ], LIST_OR ) );
348                         }
349                 }
350
351                 $this->token = $params['token'];
352
353                 if ( $this->fld_comment || $this->fld_parsedcomment || $this->token ) {
354                         $this->commentStore = new CommentStore( 'rc_comment' );
355                         $commentQuery = $this->commentStore->getJoin();
356                         $this->addTables( $commentQuery['tables'] );
357                         $this->addFields( $commentQuery['fields'] );
358                         $this->addJoinConds( $commentQuery['joins'] );
359                 }
360
361                 $this->addOption( 'LIMIT', $params['limit'] + 1 );
362
363                 $hookData = [];
364                 $count = 0;
365                 /* Perform the actual query. */
366                 $res = $this->select( __METHOD__, [], $hookData );
367
368                 $revids = [];
369                 $titles = [];
370
371                 $result = $this->getResult();
372
373                 /* Iterate through the rows, adding data extracted from them to our query result. */
374                 foreach ( $res as $row ) {
375                         if ( $count === 0 && $resultPageSet !== null ) {
376                                 // Set the non-continue since the list of recentchanges is
377                                 // prone to having entries added at the start frequently.
378                                 $this->getContinuationManager()->addGeneratorNonContinueParam(
379                                         $this, 'continue', "$row->rc_timestamp|$row->rc_id"
380                                 );
381                         }
382                         if ( ++$count > $params['limit'] ) {
383                                 // We've reached the one extra which shows that there are
384                                 // additional pages to be had. Stop here...
385                                 $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" );
386                                 break;
387                         }
388
389                         if ( is_null( $resultPageSet ) ) {
390                                 /* Extract the data from a single row. */
391                                 $vals = $this->extractRowInfo( $row );
392
393                                 /* Add that row's data to our final output. */
394                                 $fit = $this->processRow( $row, $vals, $hookData ) &&
395                                         $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
396                                 if ( !$fit ) {
397                                         $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" );
398                                         break;
399                                 }
400                         } elseif ( $params['generaterevisions'] ) {
401                                 $revid = (int)$row->rc_this_oldid;
402                                 if ( $revid > 0 ) {
403                                         $revids[] = $revid;
404                                 }
405                         } else {
406                                 $titles[] = Title::makeTitle( $row->rc_namespace, $row->rc_title );
407                         }
408                 }
409
410                 if ( is_null( $resultPageSet ) ) {
411                         /* Format the result */
412                         $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'rc' );
413                 } elseif ( $params['generaterevisions'] ) {
414                         $resultPageSet->populateFromRevisionIDs( $revids );
415                 } else {
416                         $resultPageSet->populateFromTitles( $titles );
417                 }
418         }
419
420         /**
421          * Extracts from a single sql row the data needed to describe one recent change.
422          *
423          * @param stdClass $row The row from which to extract the data.
424          * @return array An array mapping strings (descriptors) to their respective string values.
425          * @access public
426          */
427         public function extractRowInfo( $row ) {
428                 /* Determine the title of the page that has been changed. */
429                 $title = Title::makeTitle( $row->rc_namespace, $row->rc_title );
430                 $user = $this->getUser();
431
432                 /* Our output data. */
433                 $vals = [];
434
435                 $type = intval( $row->rc_type );
436                 $vals['type'] = RecentChange::parseFromRCType( $type );
437
438                 $anyHidden = false;
439
440                 /* Create a new entry in the result for the title. */
441                 if ( $this->fld_title || $this->fld_ids ) {
442                         if ( $type === RC_LOG && ( $row->rc_deleted & LogPage::DELETED_ACTION ) ) {
443                                 $vals['actionhidden'] = true;
444                                 $anyHidden = true;
445                         }
446                         if ( $type !== RC_LOG ||
447                                 LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user )
448                         ) {
449                                 if ( $this->fld_title ) {
450                                         ApiQueryBase::addTitleInfo( $vals, $title );
451                                 }
452                                 if ( $this->fld_ids ) {
453                                         $vals['pageid'] = intval( $row->rc_cur_id );
454                                         $vals['revid'] = intval( $row->rc_this_oldid );
455                                         $vals['old_revid'] = intval( $row->rc_last_oldid );
456                                 }
457                         }
458                 }
459
460                 if ( $this->fld_ids ) {
461                         $vals['rcid'] = intval( $row->rc_id );
462                 }
463
464                 /* Add user data and 'anon' flag, if user is anonymous. */
465                 if ( $this->fld_user || $this->fld_userid ) {
466                         if ( $row->rc_deleted & Revision::DELETED_USER ) {
467                                 $vals['userhidden'] = true;
468                                 $anyHidden = true;
469                         }
470                         if ( Revision::userCanBitfield( $row->rc_deleted, Revision::DELETED_USER, $user ) ) {
471                                 if ( $this->fld_user ) {
472                                         $vals['user'] = $row->rc_user_text;
473                                 }
474
475                                 if ( $this->fld_userid ) {
476                                         $vals['userid'] = (int)$row->rc_user;
477                                 }
478
479                                 if ( !$row->rc_user ) {
480                                         $vals['anon'] = true;
481                                 }
482                         }
483                 }
484
485                 /* Add flags, such as new, minor, bot. */
486                 if ( $this->fld_flags ) {
487                         $vals['bot'] = (bool)$row->rc_bot;
488                         $vals['new'] = $row->rc_type == RC_NEW;
489                         $vals['minor'] = (bool)$row->rc_minor;
490                 }
491
492                 /* Add sizes of each revision. (Only available on 1.10+) */
493                 if ( $this->fld_sizes ) {
494                         $vals['oldlen'] = intval( $row->rc_old_len );
495                         $vals['newlen'] = intval( $row->rc_new_len );
496                 }
497
498                 /* Add the timestamp. */
499                 if ( $this->fld_timestamp ) {
500                         $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->rc_timestamp );
501                 }
502
503                 /* Add edit summary / log summary. */
504                 if ( $this->fld_comment || $this->fld_parsedcomment ) {
505                         if ( $row->rc_deleted & Revision::DELETED_COMMENT ) {
506                                 $vals['commenthidden'] = true;
507                                 $anyHidden = true;
508                         }
509                         if ( Revision::userCanBitfield( $row->rc_deleted, Revision::DELETED_COMMENT, $user ) ) {
510                                 $comment = $this->commentStore->getComment( $row )->text;
511                                 if ( $this->fld_comment ) {
512                                         $vals['comment'] = $comment;
513                                 }
514
515                                 if ( $this->fld_parsedcomment ) {
516                                         $vals['parsedcomment'] = Linker::formatComment( $comment, $title );
517                                 }
518                         }
519                 }
520
521                 if ( $this->fld_redirect ) {
522                         $vals['redirect'] = (bool)$row->page_is_redirect;
523                 }
524
525                 /* Add the patrolled flag */
526                 if ( $this->fld_patrolled ) {
527                         $vals['patrolled'] = $row->rc_patrolled == 1;
528                         $vals['unpatrolled'] = ChangesList::isUnpatrolled( $row, $user );
529                 }
530
531                 if ( $this->fld_loginfo && $row->rc_type == RC_LOG ) {
532                         if ( $row->rc_deleted & LogPage::DELETED_ACTION ) {
533                                 $vals['actionhidden'] = true;
534                                 $anyHidden = true;
535                         }
536                         if ( LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user ) ) {
537                                 $vals['logid'] = intval( $row->rc_logid );
538                                 $vals['logtype'] = $row->rc_log_type;
539                                 $vals['logaction'] = $row->rc_log_action;
540                                 $vals['logparams'] = LogFormatter::newFromRow( $row )->formatParametersForApi();
541                         }
542                 }
543
544                 if ( $this->fld_tags ) {
545                         if ( $row->ts_tags ) {
546                                 $tags = explode( ',', $row->ts_tags );
547                                 ApiResult::setIndexedTagName( $tags, 'tag' );
548                                 $vals['tags'] = $tags;
549                         } else {
550                                 $vals['tags'] = [];
551                         }
552                 }
553
554                 if ( $this->fld_sha1 && $row->rev_sha1 !== null ) {
555                         if ( $row->rev_deleted & Revision::DELETED_TEXT ) {
556                                 $vals['sha1hidden'] = true;
557                                 $anyHidden = true;
558                         }
559                         if ( Revision::userCanBitfield( $row->rev_deleted, Revision::DELETED_TEXT, $user ) ) {
560                                 if ( $row->rev_sha1 !== '' ) {
561                                         $vals['sha1'] = Wikimedia\base_convert( $row->rev_sha1, 36, 16, 40 );
562                                 } else {
563                                         $vals['sha1'] = '';
564                                 }
565                         }
566                 }
567
568                 if ( !is_null( $this->token ) ) {
569                         $tokenFunctions = $this->getTokenFunctions();
570                         foreach ( $this->token as $t ) {
571                                 $val = call_user_func( $tokenFunctions[$t], $row->rc_cur_id,
572                                         $title, RecentChange::newFromRow( $row ) );
573                                 if ( $val === false ) {
574                                         $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] );
575                                 } else {
576                                         $vals[$t . 'token'] = $val;
577                                 }
578                         }
579                 }
580
581                 if ( $anyHidden && ( $row->rc_deleted & Revision::DELETED_RESTRICTED ) ) {
582                         $vals['suppressed'] = true;
583                 }
584
585                 return $vals;
586         }
587
588         /**
589          * @param array $flagsArray flipped array (string flags are keys)
590          * @return bool
591          */
592         private function includesPatrollingFlags( array $flagsArray ) {
593                 return isset( $flagsArray['patrolled'] ) ||
594                         isset( $flagsArray['!patrolled'] ) ||
595                         isset( $flagsArray['unpatrolled'] ) ||
596                         isset( $flagsArray['autopatrolled'] ) ||
597                         isset( $flagsArray['!autopatrolled'] );
598         }
599
600         public function getCacheMode( $params ) {
601                 if ( isset( $params['show'] ) &&
602                         $this->includesPatrollingFlags( array_flip( $params['show'] ) )
603                 ) {
604                         return 'private';
605                 }
606                 if ( isset( $params['token'] ) ) {
607                         return 'private';
608                 }
609                 if ( $this->userCanSeeRevDel() ) {
610                         return 'private';
611                 }
612                 if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) {
613                         // formatComment() calls wfMessage() among other things
614                         return 'anon-public-user-private';
615                 }
616
617                 return 'public';
618         }
619
620         public function getAllowedParams() {
621                 return [
622                         'start' => [
623                                 ApiBase::PARAM_TYPE => 'timestamp'
624                         ],
625                         'end' => [
626                                 ApiBase::PARAM_TYPE => 'timestamp'
627                         ],
628                         'dir' => [
629                                 ApiBase::PARAM_DFLT => 'older',
630                                 ApiBase::PARAM_TYPE => [
631                                         'newer',
632                                         'older'
633                                 ],
634                                 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
635                         ],
636                         'namespace' => [
637                                 ApiBase::PARAM_ISMULTI => true,
638                                 ApiBase::PARAM_TYPE => 'namespace',
639                                 ApiBase::PARAM_EXTRA_NAMESPACES => [ NS_MEDIA, NS_SPECIAL ],
640                         ],
641                         'user' => [
642                                 ApiBase::PARAM_TYPE => 'user'
643                         ],
644                         'excludeuser' => [
645                                 ApiBase::PARAM_TYPE => 'user'
646                         ],
647                         'tag' => null,
648                         'prop' => [
649                                 ApiBase::PARAM_ISMULTI => true,
650                                 ApiBase::PARAM_DFLT => 'title|timestamp|ids',
651                                 ApiBase::PARAM_TYPE => [
652                                         'user',
653                                         'userid',
654                                         'comment',
655                                         'parsedcomment',
656                                         'flags',
657                                         'timestamp',
658                                         'title',
659                                         'ids',
660                                         'sizes',
661                                         'redirect',
662                                         'patrolled',
663                                         'loginfo',
664                                         'tags',
665                                         'sha1',
666                                 ],
667                                 ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
668                         ],
669                         'token' => [
670                                 ApiBase::PARAM_DEPRECATED => true,
671                                 ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() ),
672                                 ApiBase::PARAM_ISMULTI => true
673                         ],
674                         'show' => [
675                                 ApiBase::PARAM_ISMULTI => true,
676                                 ApiBase::PARAM_TYPE => [
677                                         'minor',
678                                         '!minor',
679                                         'bot',
680                                         '!bot',
681                                         'anon',
682                                         '!anon',
683                                         'redirect',
684                                         '!redirect',
685                                         'patrolled',
686                                         '!patrolled',
687                                         'unpatrolled'
688                                 ]
689                         ],
690                         'limit' => [
691                                 ApiBase::PARAM_DFLT => 10,
692                                 ApiBase::PARAM_TYPE => 'limit',
693                                 ApiBase::PARAM_MIN => 1,
694                                 ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
695                                 ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
696                         ],
697                         'type' => [
698                                 ApiBase::PARAM_DFLT => 'edit|new|log|categorize',
699                                 ApiBase::PARAM_ISMULTI => true,
700                                 ApiBase::PARAM_TYPE => RecentChange::getChangeTypes()
701                         ],
702                         'toponly' => false,
703                         'continue' => [
704                                 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
705                         ],
706                         'generaterevisions' => false,
707                 ];
708         }
709
710         protected function getExamplesMessages() {
711                 return [
712                         'action=query&list=recentchanges'
713                                 => 'apihelp-query+recentchanges-example-simple',
714                         'action=query&generator=recentchanges&grcshow=!patrolled&prop=info'
715                                 => 'apihelp-query+recentchanges-example-generator',
716                 ];
717         }
718
719         public function getHelpUrls() {
720                 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Recentchanges';
721         }
722 }