]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/api/ApiQueryLogEvents.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / api / ApiQueryLogEvents.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  * Query action to List the log events, with optional filtering by various parameters.
29  *
30  * @ingroup API
31  */
32 class ApiQueryLogEvents extends ApiQueryBase {
33
34         private $commentStore;
35
36         public function __construct( ApiQuery $query, $moduleName ) {
37                 parent::__construct( $query, $moduleName, 'le' );
38         }
39
40         private $fld_ids = false, $fld_title = false, $fld_type = false,
41                 $fld_user = false, $fld_userid = false,
42                 $fld_timestamp = false, $fld_comment = false, $fld_parsedcomment = false,
43                 $fld_details = false, $fld_tags = false;
44
45         public function execute() {
46                 $params = $this->extractRequestParams();
47                 $db = $this->getDB();
48                 $this->commentStore = new CommentStore( 'log_comment' );
49                 $this->requireMaxOneParameter( $params, 'title', 'prefix', 'namespace' );
50
51                 $prop = array_flip( $params['prop'] );
52
53                 $this->fld_ids = isset( $prop['ids'] );
54                 $this->fld_title = isset( $prop['title'] );
55                 $this->fld_type = isset( $prop['type'] );
56                 $this->fld_user = isset( $prop['user'] );
57                 $this->fld_userid = isset( $prop['userid'] );
58                 $this->fld_timestamp = isset( $prop['timestamp'] );
59                 $this->fld_comment = isset( $prop['comment'] );
60                 $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
61                 $this->fld_details = isset( $prop['details'] );
62                 $this->fld_tags = isset( $prop['tags'] );
63
64                 $hideLogs = LogEventsList::getExcludeClause( $db, 'user', $this->getUser() );
65                 if ( $hideLogs !== false ) {
66                         $this->addWhere( $hideLogs );
67                 }
68
69                 // Order is significant here
70                 $this->addTables( [ 'logging', 'user', 'page' ] );
71                 $this->addJoinConds( [
72                         'user' => [ 'LEFT JOIN',
73                                 'user_id=log_user' ],
74                         'page' => [ 'LEFT JOIN',
75                                 [ 'log_namespace=page_namespace',
76                                         'log_title=page_title' ] ] ] );
77
78                 $this->addFields( [
79                         'log_id',
80                         'log_type',
81                         'log_action',
82                         'log_timestamp',
83                         'log_deleted',
84                 ] );
85
86                 $this->addFieldsIf( 'page_id', $this->fld_ids );
87                 // log_page is the page_id saved at log time, whereas page_id is from a
88                 // join at query time.  This leads to different results in various
89                 // scenarios, e.g. deletion, recreation.
90                 $this->addFieldsIf( 'log_page', $this->fld_ids );
91                 $this->addFieldsIf( [ 'log_user', 'log_user_text', 'user_name' ], $this->fld_user );
92                 $this->addFieldsIf( 'log_user', $this->fld_userid );
93                 $this->addFieldsIf(
94                         [ 'log_namespace', 'log_title' ],
95                         $this->fld_title || $this->fld_parsedcomment
96                 );
97                 $this->addFieldsIf( 'log_params', $this->fld_details );
98
99                 if ( $this->fld_comment || $this->fld_parsedcomment ) {
100                         $commentQuery = $this->commentStore->getJoin();
101                         $this->addTables( $commentQuery['tables'] );
102                         $this->addFields( $commentQuery['fields'] );
103                         $this->addJoinConds( $commentQuery['joins'] );
104                 }
105
106                 if ( $this->fld_tags ) {
107                         $this->addTables( 'tag_summary' );
108                         $this->addJoinConds( [ 'tag_summary' => [ 'LEFT JOIN', 'log_id=ts_log_id' ] ] );
109                         $this->addFields( 'ts_tags' );
110                 }
111
112                 if ( !is_null( $params['tag'] ) ) {
113                         $this->addTables( 'change_tag' );
114                         $this->addJoinConds( [ 'change_tag' => [ 'INNER JOIN',
115                                 [ 'log_id=ct_log_id' ] ] ] );
116                         $this->addWhereFld( 'ct_tag', $params['tag'] );
117                 }
118
119                 if ( !is_null( $params['action'] ) ) {
120                         // Do validation of action param, list of allowed actions can contains wildcards
121                         // Allow the param, when the actions is in the list or a wildcard version is listed.
122                         $logAction = $params['action'];
123                         if ( strpos( $logAction, '/' ) === false ) {
124                                 // all items in the list have a slash
125                                 $valid = false;
126                         } else {
127                                 $logActions = array_flip( $this->getAllowedLogActions() );
128                                 list( $type, $action ) = explode( '/', $logAction, 2 );
129                                 $valid = isset( $logActions[$logAction] ) || isset( $logActions[$type . '/*'] );
130                         }
131
132                         if ( !$valid ) {
133                                 $encParamName = $this->encodeParamName( 'action' );
134                                 $this->dieWithError(
135                                         [ 'apierror-unrecognizedvalue', $encParamName, wfEscapeWikiText( $logAction ) ],
136                                         "unknown_$encParamName"
137                                 );
138                         }
139
140                         $this->addWhereFld( 'log_type', $type );
141                         $this->addWhereFld( 'log_action', $action );
142                 } elseif ( !is_null( $params['type'] ) ) {
143                         $this->addWhereFld( 'log_type', $params['type'] );
144                 }
145
146                 $this->addTimestampWhereRange(
147                         'log_timestamp',
148                         $params['dir'],
149                         $params['start'],
150                         $params['end']
151                 );
152                 // Include in ORDER BY for uniqueness
153                 $this->addWhereRange( 'log_id', $params['dir'], null, null );
154
155                 if ( !is_null( $params['continue'] ) ) {
156                         $cont = explode( '|', $params['continue'] );
157                         $this->dieContinueUsageIf( count( $cont ) != 2 );
158                         $op = ( $params['dir'] === 'newer' ? '>' : '<' );
159                         $continueTimestamp = $db->addQuotes( $db->timestamp( $cont[0] ) );
160                         $continueId = (int)$cont[1];
161                         $this->dieContinueUsageIf( $continueId != $cont[1] );
162                         $this->addWhere( "log_timestamp $op $continueTimestamp OR " .
163                                 "(log_timestamp = $continueTimestamp AND " .
164                                 "log_id $op= $continueId)"
165                         );
166                 }
167
168                 $limit = $params['limit'];
169                 $this->addOption( 'LIMIT', $limit + 1 );
170
171                 $user = $params['user'];
172                 if ( !is_null( $user ) ) {
173                         $userid = User::idFromName( $user );
174                         if ( $userid ) {
175                                 $this->addWhereFld( 'log_user', $userid );
176                         } else {
177                                 $this->addWhereFld( 'log_user_text', $user );
178                         }
179                 }
180
181                 $title = $params['title'];
182                 if ( !is_null( $title ) ) {
183                         $titleObj = Title::newFromText( $title );
184                         if ( is_null( $titleObj ) ) {
185                                 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] );
186                         }
187                         $this->addWhereFld( 'log_namespace', $titleObj->getNamespace() );
188                         $this->addWhereFld( 'log_title', $titleObj->getDBkey() );
189                 }
190
191                 if ( $params['namespace'] !== null ) {
192                         $this->addWhereFld( 'log_namespace', $params['namespace'] );
193                 }
194
195                 $prefix = $params['prefix'];
196
197                 if ( !is_null( $prefix ) ) {
198                         if ( $this->getConfig()->get( 'MiserMode' ) ) {
199                                 $this->dieWithError( 'apierror-prefixsearchdisabled' );
200                         }
201
202                         $title = Title::newFromText( $prefix );
203                         if ( is_null( $title ) ) {
204                                 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $prefix ) ] );
205                         }
206                         $this->addWhereFld( 'log_namespace', $title->getNamespace() );
207                         $this->addWhere( 'log_title ' . $db->buildLike( $title->getDBkey(), $db->anyString() ) );
208                 }
209
210                 // Paranoia: avoid brute force searches (T19342)
211                 if ( $params['namespace'] !== null || !is_null( $title ) || !is_null( $user ) ) {
212                         if ( !$this->getUser()->isAllowed( 'deletedhistory' ) ) {
213                                 $titleBits = LogPage::DELETED_ACTION;
214                                 $userBits = LogPage::DELETED_USER;
215                         } elseif ( !$this->getUser()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
216                                 $titleBits = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
217                                 $userBits = LogPage::DELETED_USER | LogPage::DELETED_RESTRICTED;
218                         } else {
219                                 $titleBits = 0;
220                                 $userBits = 0;
221                         }
222                         if ( ( $params['namespace'] !== null || !is_null( $title ) ) && $titleBits ) {
223                                 $this->addWhere( $db->bitAnd( 'log_deleted', $titleBits ) . " != $titleBits" );
224                         }
225                         if ( !is_null( $user ) && $userBits ) {
226                                 $this->addWhere( $db->bitAnd( 'log_deleted', $userBits ) . " != $userBits" );
227                         }
228                 }
229
230                 $count = 0;
231                 $res = $this->select( __METHOD__ );
232                 $result = $this->getResult();
233                 foreach ( $res as $row ) {
234                         if ( ++$count > $limit ) {
235                                 // We've reached the one extra which shows that there are
236                                 // additional pages to be had. Stop here...
237                                 $this->setContinueEnumParameter( 'continue', "$row->log_timestamp|$row->log_id" );
238                                 break;
239                         }
240
241                         $vals = $this->extractRowInfo( $row );
242                         $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
243                         if ( !$fit ) {
244                                 $this->setContinueEnumParameter( 'continue', "$row->log_timestamp|$row->log_id" );
245                                 break;
246                         }
247                 }
248                 $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'item' );
249         }
250
251         /**
252          * @deprecated since 1.25 Use LogFormatter::formatParametersForApi instead
253          * @param ApiResult $result
254          * @param array &$vals
255          * @param string $params
256          * @param string $type
257          * @param string $action
258          * @param string $ts
259          * @param bool $legacy
260          * @return array
261          */
262         public static function addLogParams( $result, &$vals, $params, $type,
263                 $action, $ts, $legacy = false
264         ) {
265                 wfDeprecated( __METHOD__, '1.25' );
266
267                 $entry = new ManualLogEntry( $type, $action );
268                 $entry->setParameters( $params );
269                 $entry->setTimestamp( $ts );
270                 $entry->setLegacy( $legacy );
271                 $formatter = LogFormatter::newFromEntry( $entry );
272                 $vals['params'] = $formatter->formatParametersForApi();
273
274                 return $vals;
275         }
276
277         private function extractRowInfo( $row ) {
278                 $logEntry = DatabaseLogEntry::newFromRow( $row );
279                 $vals = [
280                         ApiResult::META_TYPE => 'assoc',
281                 ];
282                 $anyHidden = false;
283                 $user = $this->getUser();
284
285                 if ( $this->fld_ids ) {
286                         $vals['logid'] = intval( $row->log_id );
287                 }
288
289                 if ( $this->fld_title || $this->fld_parsedcomment ) {
290                         $title = Title::makeTitle( $row->log_namespace, $row->log_title );
291                 }
292
293                 if ( $this->fld_title || $this->fld_ids || $this->fld_details && $row->log_params !== '' ) {
294                         if ( LogEventsList::isDeleted( $row, LogPage::DELETED_ACTION ) ) {
295                                 $vals['actionhidden'] = true;
296                                 $anyHidden = true;
297                         }
298                         if ( LogEventsList::userCan( $row, LogPage::DELETED_ACTION, $user ) ) {
299                                 if ( $this->fld_title ) {
300                                         ApiQueryBase::addTitleInfo( $vals, $title );
301                                 }
302                                 if ( $this->fld_ids ) {
303                                         $vals['pageid'] = intval( $row->page_id );
304                                         $vals['logpage'] = intval( $row->log_page );
305                                 }
306                                 if ( $this->fld_details ) {
307                                         $vals['params'] = LogFormatter::newFromEntry( $logEntry )->formatParametersForApi();
308                                 }
309                         }
310                 }
311
312                 if ( $this->fld_type ) {
313                         $vals['type'] = $row->log_type;
314                         $vals['action'] = $row->log_action;
315                 }
316
317                 if ( $this->fld_user || $this->fld_userid ) {
318                         if ( LogEventsList::isDeleted( $row, LogPage::DELETED_USER ) ) {
319                                 $vals['userhidden'] = true;
320                                 $anyHidden = true;
321                         }
322                         if ( LogEventsList::userCan( $row, LogPage::DELETED_USER, $user ) ) {
323                                 if ( $this->fld_user ) {
324                                         $vals['user'] = $row->user_name === null ? $row->log_user_text : $row->user_name;
325                                 }
326                                 if ( $this->fld_userid ) {
327                                         $vals['userid'] = intval( $row->log_user );
328                                 }
329
330                                 if ( !$row->log_user ) {
331                                         $vals['anon'] = true;
332                                 }
333                         }
334                 }
335                 if ( $this->fld_timestamp ) {
336                         $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->log_timestamp );
337                 }
338
339                 if ( $this->fld_comment || $this->fld_parsedcomment ) {
340                         if ( LogEventsList::isDeleted( $row, LogPage::DELETED_COMMENT ) ) {
341                                 $vals['commenthidden'] = true;
342                                 $anyHidden = true;
343                         }
344                         if ( LogEventsList::userCan( $row, LogPage::DELETED_COMMENT, $user ) ) {
345                                 $comment = $this->commentStore->getComment( $row )->text;
346                                 if ( $this->fld_comment ) {
347                                         $vals['comment'] = $comment;
348                                 }
349
350                                 if ( $this->fld_parsedcomment ) {
351                                         $vals['parsedcomment'] = Linker::formatComment( $comment, $title );
352                                 }
353                         }
354                 }
355
356                 if ( $this->fld_tags ) {
357                         if ( $row->ts_tags ) {
358                                 $tags = explode( ',', $row->ts_tags );
359                                 ApiResult::setIndexedTagName( $tags, 'tag' );
360                                 $vals['tags'] = $tags;
361                         } else {
362                                 $vals['tags'] = [];
363                         }
364                 }
365
366                 if ( $anyHidden && LogEventsList::isDeleted( $row, LogPage::DELETED_RESTRICTED ) ) {
367                         $vals['suppressed'] = true;
368                 }
369
370                 return $vals;
371         }
372
373         /**
374          * @return array
375          */
376         private function getAllowedLogActions() {
377                 $config = $this->getConfig();
378                 return array_keys( array_merge(
379                         $config->get( 'LogActions' ),
380                         $config->get( 'LogActionsHandlers' )
381                 ) );
382         }
383
384         public function getCacheMode( $params ) {
385                 if ( $this->userCanSeeRevDel() ) {
386                         return 'private';
387                 }
388                 if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) {
389                         // formatComment() calls wfMessage() among other things
390                         return 'anon-public-user-private';
391                 } elseif ( LogEventsList::getExcludeClause( $this->getDB(), 'user', $this->getUser() )
392                         === LogEventsList::getExcludeClause( $this->getDB(), 'public' )
393                 ) { // Output can only contain public data.
394                         return 'public';
395                 } else {
396                         return 'anon-public-user-private';
397                 }
398         }
399
400         public function getAllowedParams( $flags = 0 ) {
401                 $config = $this->getConfig();
402                 $ret = [
403                         'prop' => [
404                                 ApiBase::PARAM_ISMULTI => true,
405                                 ApiBase::PARAM_DFLT => 'ids|title|type|user|timestamp|comment|details',
406                                 ApiBase::PARAM_TYPE => [
407                                         'ids',
408                                         'title',
409                                         'type',
410                                         'user',
411                                         'userid',
412                                         'timestamp',
413                                         'comment',
414                                         'parsedcomment',
415                                         'details',
416                                         'tags'
417                                 ],
418                                 ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
419                         ],
420                         'type' => [
421                                 ApiBase::PARAM_TYPE => $config->get( 'LogTypes' )
422                         ],
423                         'action' => [
424                                 // validation on request is done in execute()
425                                 ApiBase::PARAM_TYPE => ( $flags & ApiBase::GET_VALUES_FOR_HELP )
426                                         ? $this->getAllowedLogActions()
427                                         : null
428                         ],
429                         'start' => [
430                                 ApiBase::PARAM_TYPE => 'timestamp'
431                         ],
432                         'end' => [
433                                 ApiBase::PARAM_TYPE => 'timestamp'
434                         ],
435                         'dir' => [
436                                 ApiBase::PARAM_DFLT => 'older',
437                                 ApiBase::PARAM_TYPE => [
438                                         'newer',
439                                         'older'
440                                 ],
441                                 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
442                         ],
443                         'user' => [
444                                 ApiBase::PARAM_TYPE => 'user',
445                         ],
446                         'title' => null,
447                         'namespace' => [
448                                 ApiBase::PARAM_TYPE => 'namespace',
449                                 ApiBase::PARAM_EXTRA_NAMESPACES => [ NS_MEDIA, NS_SPECIAL ],
450                         ],
451                         'prefix' => [],
452                         'tag' => null,
453                         'limit' => [
454                                 ApiBase::PARAM_DFLT => 10,
455                                 ApiBase::PARAM_TYPE => 'limit',
456                                 ApiBase::PARAM_MIN => 1,
457                                 ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
458                                 ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
459                         ],
460                         'continue' => [
461                                 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
462                         ],
463                 ];
464
465                 if ( $config->get( 'MiserMode' ) ) {
466                         $ret['prefix'][ApiBase::PARAM_HELP_MSG] = 'api-help-param-disabled-in-miser-mode';
467                 }
468
469                 return $ret;
470         }
471
472         protected function getExamplesMessages() {
473                 return [
474                         'action=query&list=logevents'
475                                 => 'apihelp-query+logevents-example-simple',
476                 ];
477         }
478
479         public function getHelpUrls() {
480                 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Logevents';
481         }
482 }