]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/api/ApiQueryRecentChanges.php
MediaWiki 1.16.0
[autoinstallsdev/mediawiki.git] / includes / api / ApiQueryRecentChanges.php
1 <?php
2
3 /*
4  * Created on Oct 19, 2006
5  *
6  * API for MediaWiki 1.8+
7  *
8  * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
9  *
10  * This program is free software; you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by
12  * the Free Software Foundation; either version 2 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the GNU General Public License along
21  * with this program; if not, write to the Free Software Foundation, Inc.,
22  * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
23  * http://www.gnu.org/copyleft/gpl.html
24  */
25
26 if ( !defined( 'MEDIAWIKI' ) ) {
27         // Eclipse helper - will be ignored in production
28         require_once ( 'ApiQueryBase.php' );
29 }
30
31 /**
32  * A query action to enumerate the recent changes that were done to the wiki.
33  * Various filters are supported.
34  *
35  * @ingroup API
36  */
37 class ApiQueryRecentChanges extends ApiQueryBase {
38
39         public function __construct( $query, $moduleName ) {
40                 parent :: __construct( $query, $moduleName, 'rc' );
41         }
42
43         private $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_flags = false,
44                         $fld_timestamp = false, $fld_title = false, $fld_ids = false,
45                         $fld_sizes = false;
46         /**
47          * Get an array mapping token names to their handler functions.
48          * The prototype for a token function is func($pageid, $title, $rc)
49          * it should return a token or false (permission denied)
50          * @return array(tokenname => function)
51          */
52         protected function getTokenFunctions() {
53                 // Don't call the hooks twice
54                 if ( isset( $this->tokenFunctions ) )
55                         return $this->tokenFunctions;
56
57                 // If we're in JSON callback mode, no tokens can be obtained
58                 if ( !is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) )
59                         return array();
60
61                 $this->tokenFunctions = array(
62                         'patrol' => array( 'ApiQueryRecentChanges', 'getPatrolToken' )
63                 );
64                 wfRunHooks( 'APIQueryRecentChangesTokens', array( &$this->tokenFunctions ) );
65                 return $this->tokenFunctions;
66         }
67         
68         public static function getPatrolToken( $pageid, $title, $rc )
69         {
70                 global $wgUser;
71                 if ( !$wgUser->useRCPatrol() && ( !$wgUser->useNPPatrol() ||
72                                  $rc->getAttribute( 'rc_type' ) != RC_NEW ) )
73                         return false;
74                 
75                 // The patrol token is always the same, let's exploit that
76                 static $cachedPatrolToken = null;
77                 if ( !is_null( $cachedPatrolToken ) )
78                         return $cachedPatrolToken;
79
80                 $cachedPatrolToken = $wgUser->editToken();
81                 return $cachedPatrolToken;
82         }
83
84         /**
85          * Sets internal state to include the desired properties in the output.
86          * @param $prop associative array of properties, only keys are used here
87          */
88         public function initProperties( $prop ) {
89                 $this->fld_comment = isset ( $prop['comment'] );
90                 $this->fld_parsedcomment = isset ( $prop['parsedcomment'] );
91                 $this->fld_user = isset ( $prop['user'] );
92                 $this->fld_flags = isset ( $prop['flags'] );
93                 $this->fld_timestamp = isset ( $prop['timestamp'] );
94                 $this->fld_title = isset ( $prop['title'] );
95                 $this->fld_ids = isset ( $prop['ids'] );
96                 $this->fld_sizes = isset ( $prop['sizes'] );
97                 $this->fld_redirect = isset( $prop['redirect'] );
98                 $this->fld_patrolled = isset( $prop['patrolled'] );
99                 $this->fld_loginfo = isset( $prop['loginfo'] );
100                 $this->fld_tags = isset( $prop['tags'] );
101         }
102
103         /**
104          * Generates and outputs the result of this query based upon the provided parameters.
105          */
106         public function execute() {
107                 /* Get the parameters of the request. */
108                 $params = $this->extractRequestParams();
109
110                 /* Build our basic query. Namely, something along the lines of:
111                  * SELECT * FROM recentchanges WHERE rc_timestamp > $start
112                  *              AND rc_timestamp < $end AND rc_namespace = $namespace
113                  *              AND rc_deleted = '0'
114                  */
115                 $db = $this->getDB();
116                 $this->addTables( 'recentchanges' );
117                 $index = array( 'recentchanges' => 'rc_timestamp' ); // May change
118                 $this->addWhereRange( 'rc_timestamp', $params['dir'], $params['start'], $params['end'] );
119                 $this->addWhereFld( 'rc_namespace', $params['namespace'] );
120                 $this->addWhereFld( 'rc_deleted', 0 );
121
122                 if ( !is_null( $params['type'] ) )
123                                 $this->addWhereFld( 'rc_type', $this->parseRCType( $params['type'] ) );
124
125                 if ( !is_null( $params['show'] ) ) {
126                         $show = array_flip( $params['show'] );
127
128                         /* Check for conflicting parameters. */
129                         if ( ( isset ( $show['minor'] ) && isset ( $show['!minor'] ) )
130                                         || ( isset ( $show['bot'] ) && isset ( $show['!bot'] ) )
131                                         || ( isset ( $show['anon'] ) && isset ( $show['!anon'] ) )
132                                         || ( isset ( $show['redirect'] ) && isset ( $show['!redirect'] ) )
133                                         || ( isset ( $show['patrolled'] ) && isset ( $show['!patrolled'] ) ) ) {
134
135                                 $this->dieUsageMsg( array( 'show' ) );
136                         }
137                         
138                         // Check permissions
139                         global $wgUser;
140                         if ( ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ) && !$wgUser->useRCPatrol() && !$wgUser->useNPPatrol() )
141                                 $this->dieUsage( "You need the patrol right to request the patrolled flag", 'permissiondenied' );
142
143                         /* Add additional conditions to query depending upon parameters. */
144                         $this->addWhereIf( 'rc_minor = 0', isset ( $show['!minor'] ) );
145                         $this->addWhereIf( 'rc_minor != 0', isset ( $show['minor'] ) );
146                         $this->addWhereIf( 'rc_bot = 0', isset ( $show['!bot'] ) );
147                         $this->addWhereIf( 'rc_bot != 0', isset ( $show['bot'] ) );
148                         $this->addWhereIf( 'rc_user = 0', isset ( $show['anon'] ) );
149                         $this->addWhereIf( 'rc_user != 0', isset ( $show['!anon'] ) );
150                         $this->addWhereIf( 'rc_patrolled = 0', isset( $show['!patrolled'] ) );
151                         $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) );
152                         $this->addWhereIf( 'page_is_redirect = 1', isset ( $show['redirect'] ) );
153                         
154                         // Don't throw log entries out the window here
155                         $this->addWhereIf( 'page_is_redirect = 0 OR page_is_redirect IS NULL', isset ( $show['!redirect'] ) );
156                 }
157                 
158                 if ( !is_null( $params['user'] ) && !is_null( $param['excludeuser'] ) )
159                         $this->dieUsage( 'user and excludeuser cannot be used together', 'user-excludeuser' );
160                         
161                 if ( !is_null( $params['user'] ) )
162                 {
163                         $this->addWhereFld( 'rc_user_text', $params['user'] );
164                         $index['recentchanges'] = 'rc_user_text';
165                 }
166                 
167                 if ( !is_null( $params['excludeuser'] ) )
168                         // We don't use the rc_user_text index here because
169                         // * it would require us to sort by rc_user_text before rc_timestamp
170                         // * the != condition doesn't throw out too many rows anyway
171                         $this->addWhere( 'rc_user_text != ' . $this->getDB()->addQuotes( $params['excludeuser'] ) );
172
173                 /* Add the fields we're concerned with to our query. */
174                 $this->addFields( array (
175                         'rc_timestamp',
176                         'rc_namespace',
177                         'rc_title',
178                         'rc_cur_id',
179                         'rc_type',
180                         'rc_moved_to_ns',
181                         'rc_moved_to_title'
182                 ) );
183
184                 /* Determine what properties we need to display. */
185                 if ( !is_null( $params['prop'] ) ) {
186                         $prop = array_flip( $params['prop'] );
187
188                         /* Set up internal members based upon params. */
189                         $this->initProperties( $prop );
190
191                         global $wgUser;
192                         if ( $this->fld_patrolled && !$wgUser->useRCPatrol() && !$wgUser->useNPPatrol() )
193                                 $this->dieUsage( "You need the patrol right to request the patrolled flag", 'permissiondenied' );
194
195                         /* Add fields to our query if they are specified as a needed parameter. */
196                         $this->addFieldsIf( 'rc_id', $this->fld_ids );
197                         $this->addFieldsIf( 'rc_this_oldid', $this->fld_ids );
198                         $this->addFieldsIf( 'rc_last_oldid', $this->fld_ids );
199                         $this->addFieldsIf( 'rc_comment', $this->fld_comment || $this->fld_parsedcomment );
200                         $this->addFieldsIf( 'rc_user', $this->fld_user );
201                         $this->addFieldsIf( 'rc_user_text', $this->fld_user );
202                         $this->addFieldsIf( 'rc_minor', $this->fld_flags );
203                         $this->addFieldsIf( 'rc_bot', $this->fld_flags );
204                         $this->addFieldsIf( 'rc_new', $this->fld_flags );
205                         $this->addFieldsIf( 'rc_old_len', $this->fld_sizes );
206                         $this->addFieldsIf( 'rc_new_len', $this->fld_sizes );
207                         $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled );
208                         $this->addFieldsIf( 'rc_logid', $this->fld_loginfo );
209                         $this->addFieldsIf( 'rc_log_type', $this->fld_loginfo );
210                         $this->addFieldsIf( 'rc_log_action', $this->fld_loginfo );
211                         $this->addFieldsIf( 'rc_params', $this->fld_loginfo );
212                         if ( $this->fld_redirect || isset( $show['redirect'] ) || isset( $show['!redirect'] ) )
213                         {
214                                 $this->addTables( 'page' );
215                                 $this->addJoinConds( array( 'page' => array( 'LEFT JOIN', array( 'rc_namespace=page_namespace', 'rc_title=page_title' ) ) ) );
216                                 $this->addFields( 'page_is_redirect' );
217                         }
218                 }
219                 
220                 if ( $this->fld_tags ) {
221                         $this->addTables( 'tag_summary' );
222                         $this->addJoinConds( array( 'tag_summary' => array( 'LEFT JOIN', array( 'rc_id=ts_rc_id' ) ) ) );
223                         $this->addFields( 'ts_tags' );
224                 }
225                         
226                 if ( !is_null( $params['tag'] ) ) {
227                         $this->addTables( 'change_tag' );
228                         $this->addJoinConds( array( 'change_tag' => array( 'INNER JOIN', array( 'rc_id=ct_rc_id' ) ) ) );
229                         $this->addWhereFld( 'ct_tag' , $params['tag'] );
230                         global $wgOldChangeTagsIndex;
231                         $index['change_tag'] = $wgOldChangeTagsIndex ?  'ct_tag' : 'change_tag_tag_id';
232                 }
233                 
234                 $this->token = $params['token'];
235                 $this->addOption( 'LIMIT', $params['limit'] + 1 );
236                 $this->addOption( 'USE INDEX', $index );
237
238                 $count = 0;
239                 /* Perform the actual query. */
240                 $db = $this->getDB();
241                 $res = $this->select( __METHOD__ );
242
243                 /* Iterate through the rows, adding data extracted from them to our query result. */
244                 while ( $row = $db->fetchObject( $res ) ) {
245                         if ( ++ $count > $params['limit'] ) {
246                                 // We've reached the one extra which shows that there are additional pages to be had. Stop here...
247                                 $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->rc_timestamp ) );
248                                 break;
249                         }
250
251                         /* Extract the data from a single row. */
252                         $vals = $this->extractRowInfo( $row );
253
254                         /* Add that row's data to our final output. */
255                         if ( !$vals )
256                                 continue;
257                         $fit = $this->getResult()->addValue( array( 'query', $this->getModuleName() ), null, $vals );
258                         if ( !$fit )
259                         {
260                                 $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->rc_timestamp ) );
261                                 break;
262                         }
263                 }
264
265                 $db->freeResult( $res );
266
267                 /* Format the result */
268                 $this->getResult()->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'rc' );
269         }
270
271         /**
272          * Extracts from a single sql row the data needed to describe one recent change.
273          *
274          * @param $row The row from which to extract the data.
275          * @return An array mapping strings (descriptors) to their respective string values.
276          * @access public
277          */
278         public function extractRowInfo( $row ) {
279                 /* If page was moved somewhere, get the title of the move target. */
280                 $movedToTitle = false;
281                 if ( isset( $row->rc_moved_to_title ) && $row->rc_moved_to_title !== '' )
282                         $movedToTitle = Title :: makeTitle( $row->rc_moved_to_ns, $row->rc_moved_to_title );
283
284                 /* Determine the title of the page that has been changed. */
285                 $title = Title :: makeTitle( $row->rc_namespace, $row->rc_title );
286
287                 /* Our output data. */
288                 $vals = array ();
289
290                 $type = intval ( $row->rc_type );
291
292                 /* Determine what kind of change this was. */
293                 switch ( $type ) {
294                         case RC_EDIT:
295                                 $vals['type'] = 'edit';
296                                 break;
297                         case RC_NEW:
298                                 $vals['type'] = 'new';
299                                 break;
300                         case RC_MOVE:
301                                 $vals['type'] = 'move';
302                                 break;
303                         case RC_LOG:
304                                 $vals['type'] = 'log';
305                                 break;
306                         case RC_MOVE_OVER_REDIRECT:
307                                 $vals['type'] = 'move over redirect';
308                                 break;
309                         default:
310                                 $vals['type'] = $type;
311                 }
312
313                 /* Create a new entry in the result for the title. */
314                 if ( $this->fld_title ) {
315                         ApiQueryBase :: addTitleInfo( $vals, $title );
316                         if ( $movedToTitle )
317                                 ApiQueryBase :: addTitleInfo( $vals, $movedToTitle, "new_" );
318                 }
319
320                 /* Add ids, such as rcid, pageid, revid, and oldid to the change's info. */
321                 if ( $this->fld_ids ) {
322                         $vals['rcid'] = intval( $row->rc_id );
323                         $vals['pageid'] = intval( $row->rc_cur_id );
324                         $vals['revid'] = intval( $row->rc_this_oldid );
325                         $vals['old_revid'] = intval( $row->rc_last_oldid );
326                 }
327
328                 /* Add user data and 'anon' flag, if use is anonymous. */
329                 if ( $this->fld_user ) {
330                         $vals['user'] = $row->rc_user_text;
331                         if ( !$row->rc_user )
332                                 $vals['anon'] = '';
333                 }
334
335                 /* Add flags, such as new, minor, bot. */
336                 if ( $this->fld_flags ) {
337                         if ( $row->rc_bot )
338                                 $vals['bot'] = '';
339                         if ( $row->rc_new )
340                                 $vals['new'] = '';
341                         if ( $row->rc_minor )
342                                 $vals['minor'] = '';
343                 }
344
345                 /* Add sizes of each revision. (Only available on 1.10+) */
346                 if ( $this->fld_sizes ) {
347                         $vals['oldlen'] = intval( $row->rc_old_len );
348                         $vals['newlen'] = intval( $row->rc_new_len );
349                 }
350
351                 /* Add the timestamp. */
352                 if ( $this->fld_timestamp )
353                         $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->rc_timestamp );
354
355                 /* Add edit summary / log summary. */
356                 if ( $this->fld_comment && isset( $row->rc_comment ) )
357                         $vals['comment'] = $row->rc_comment;
358                 
359                 if ( $this->fld_parsedcomment && isset( $row->rc_comment ) ) {
360                         global $wgUser;
361                         $vals['parsedcomment'] = $wgUser->getSkin()->formatComment( $row->rc_comment, $title );
362                 }
363
364                 if ( $this->fld_redirect )
365                         if ( $row->page_is_redirect )
366                                 $vals['redirect'] = '';
367
368                 /* Add the patrolled flag */
369                 if ( $this->fld_patrolled && $row->rc_patrolled == 1 )
370                         $vals['patrolled'] = '';
371                         
372                 if ( $this->fld_loginfo && $row->rc_type == RC_LOG ) {
373                         $vals['logid'] = intval( $row->rc_logid );
374                         $vals['logtype'] = $row->rc_log_type;
375                         $vals['logaction'] = $row->rc_log_action;
376                         ApiQueryLogEvents::addLogParams( $this->getResult(),
377                                 $vals, $row->rc_params,
378                                 $row->rc_log_type, $row->rc_timestamp );
379                 }
380                 
381                 if ( $this->fld_tags ) {
382                         if ( $row->ts_tags ) {
383                                 $tags = explode( ',', $row->ts_tags );
384                                 $this->getResult()->setIndexedTagName( $tags, 'tag' );
385                                 $vals['tags'] = $tags;
386                         } else {
387                                 $vals['tags'] = array();
388                         }
389                 }
390                         
391                 if ( !is_null( $this->token ) )
392                 {
393                         $tokenFunctions = $this->getTokenFunctions();
394                         foreach ( $this->token as $t )
395                         {
396                                 $val = call_user_func( $tokenFunctions[$t], $row->rc_cur_id,
397                                         $title, RecentChange::newFromRow( $row ) );
398                                 if ( $val === false )
399                                         $this->setWarning( "Action '$t' is not allowed for the current user" );
400                                 else
401                                         $vals[$t . 'token'] = $val;
402                         }
403                 }
404
405                 return $vals;
406         }
407
408         private function parseRCType( $type )
409         {
410                         if ( is_array( $type ) )
411                         {
412                                         $retval = array();
413                                         foreach ( $type as $t )
414                                                         $retval[] = $this->parseRCType( $t );
415                                         return $retval;
416                         }
417                         switch( $type )
418                         {
419                                         case 'edit': return RC_EDIT;
420                                         case 'new': return RC_NEW;
421                                         case 'log': return RC_LOG;
422                         }
423         }
424
425         public function getCacheMode( $params ) {
426                 if ( isset( $params['show'] ) ) {
427                         foreach ( $params['show'] as $show ) {
428                                 if ( $show === 'patrolled' || $show === '!patrolled' ) {
429                                         return 'private';
430                                 }
431                         }
432                 }
433                 if ( isset( $params['token'] ) ) {
434                         return 'private';
435                 }
436                 if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) {
437                         // formatComment() calls wfMsg() among other things
438                         return 'anon-public-user-private';
439                 }
440                 return 'public';
441         }
442
443         public function getAllowedParams() {
444                 return array (
445                         'start' => array (
446                                 ApiBase :: PARAM_TYPE => 'timestamp'
447                         ),
448                         'end' => array (
449                                 ApiBase :: PARAM_TYPE => 'timestamp'
450                         ),
451                         'dir' => array (
452                                 ApiBase :: PARAM_DFLT => 'older',
453                                 ApiBase :: PARAM_TYPE => array (
454                                         'newer',
455                                         'older'
456                                 )
457                         ),
458                         'namespace' => array (
459                                 ApiBase :: PARAM_ISMULTI => true,
460                                 ApiBase :: PARAM_TYPE => 'namespace'
461                         ),
462                         'user' => array(
463                                 ApiBase :: PARAM_TYPE => 'user'
464                         ),
465                         'excludeuser' => array(
466                                 ApiBase :: PARAM_TYPE => 'user'
467                         ),
468                         'tag' => null,
469                         'prop' => array (
470                                 ApiBase :: PARAM_ISMULTI => true,
471                                 ApiBase :: PARAM_DFLT => 'title|timestamp|ids',
472                                 ApiBase :: PARAM_TYPE => array (
473                                         'user',
474                                         'comment',
475                                         'parsedcomment',
476                                         'flags',
477                                         'timestamp',
478                                         'title',
479                                         'ids',
480                                         'sizes',
481                                         'redirect',
482                                         'patrolled',
483                                         'loginfo',
484                                         'tags'
485                                 )
486                         ),
487                         'token' => array(
488                                 ApiBase :: PARAM_TYPE => array_keys( $this->getTokenFunctions() ),
489                                 ApiBase :: PARAM_ISMULTI => true
490                         ),
491                         'show' => array (
492                                 ApiBase :: PARAM_ISMULTI => true,
493                                 ApiBase :: PARAM_TYPE => array (
494                                         'minor',
495                                         '!minor',
496                                         'bot',
497                                         '!bot',
498                                         'anon',
499                                         '!anon',
500                                         'redirect',
501                                         '!redirect',
502                                         'patrolled',
503                                         '!patrolled'
504                                 )
505                         ),
506                         'limit' => array (
507                                 ApiBase :: PARAM_DFLT => 10,
508                                 ApiBase :: PARAM_TYPE => 'limit',
509                                 ApiBase :: PARAM_MIN => 1,
510                                 ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
511                                 ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
512                         ),
513                         'type' => array (
514                                 ApiBase :: PARAM_ISMULTI => true,
515                                 ApiBase :: PARAM_TYPE => array (
516                                         'edit',
517                                         'new',
518                                         'log'
519                                 )
520                         )
521                 );
522         }
523
524         public function getParamDescription() {
525                 return array (
526                         'start' => 'The timestamp to start enumerating from.',
527                         'end' => 'The timestamp to end enumerating.',
528                         'dir' => 'In which direction to enumerate.',
529                         'namespace' => 'Filter log entries to only this namespace(s)',
530                         'user' => 'Only list changes by this user',
531                         'excludeuser' => 'Don\'t list changes by this user',
532                         'prop' => 'Include additional pieces of information',
533                         'token' => 'Which tokens to obtain for each change',
534                         'show' => array (
535                                 'Show only items that meet this criteria.',
536                                 'For example, to see only minor edits done by logged-in users, set show=minor|!anon'
537                         ),
538                         'type' => 'Which types of changes to show.',
539                         'limit' => 'How many total changes to return.',
540                         'tag' => 'Only list changes tagged with this tag.',
541                 );
542         }
543
544         public function getDescription() {
545                 return 'Enumerate recent changes';
546         }
547         
548         public function getPossibleErrors() {
549                 return array_merge( parent::getPossibleErrors(), array(
550                         array( 'show' ),
551                         array( 'code' => 'permissiondenied', 'info' => 'You need the patrol right to request the patrolled flag' ),
552                         array( 'code' => 'user-excludeuser', 'info' => 'user and excludeuser cannot be used together' ),
553                 ) );
554         }
555
556         protected function getExamples() {
557                 return array (
558                         'api.php?action=query&list=recentchanges'
559                 );
560         }
561
562         public function getVersion() {
563                 return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 69932 2010-07-26 08:03:21Z tstarling $';
564         }
565 }