MediaWiki  master
ApiQueryLogEvents.php
Go to the documentation of this file.
1 <?php
34 
41 
43  private $commentStore;
44 
46  private $commentFormatter;
47 
49  private $changeTagDefStore;
50 
52  private $formattedComments;
53 
61  public function __construct(
62  ApiQuery $query,
63  $moduleName,
64  CommentStore $commentStore,
65  RowCommentFormatter $commentFormatter,
66  NameTableStore $changeTagDefStore
67  ) {
68  parent::__construct( $query, $moduleName, 'le' );
69  $this->commentStore = $commentStore;
70  $this->commentFormatter = $commentFormatter;
71  $this->changeTagDefStore = $changeTagDefStore;
72  }
73 
74  private $fld_ids = false, $fld_title = false, $fld_type = false,
75  $fld_user = false, $fld_userid = false,
76  $fld_timestamp = false, $fld_comment = false, $fld_parsedcomment = false,
77  $fld_details = false, $fld_tags = false;
78 
79  public function execute() {
80  $params = $this->extractRequestParams();
81  $db = $this->getDB();
82  $this->requireMaxOneParameter( $params, 'title', 'prefix', 'namespace' );
83 
84  $prop = array_fill_keys( $params['prop'], true );
85 
86  $this->fld_ids = isset( $prop['ids'] );
87  $this->fld_title = isset( $prop['title'] );
88  $this->fld_type = isset( $prop['type'] );
89  $this->fld_user = isset( $prop['user'] );
90  $this->fld_userid = isset( $prop['userid'] );
91  $this->fld_timestamp = isset( $prop['timestamp'] );
92  $this->fld_comment = isset( $prop['comment'] );
93  $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
94  $this->fld_details = isset( $prop['details'] );
95  $this->fld_tags = isset( $prop['tags'] );
96 
97  $hideLogs = LogEventsList::getExcludeClause( $db, 'user', $this->getAuthority() );
98  if ( $hideLogs !== false ) {
99  $this->addWhere( $hideLogs );
100  }
101 
102  $this->addTables( [ 'logging', 'actor' ] );
103  $this->addJoinConds( [
104  'actor' => [ 'JOIN', 'actor_id=log_actor' ],
105  ] );
106 
107  $this->addFields( [
108  'log_id',
109  'log_type',
110  'log_action',
111  'log_timestamp',
112  'log_deleted',
113  ] );
114 
115  if ( $this->fld_ids ) {
116  $this->addTables( 'page' );
117  $this->addJoinConds( [
118  'page' => [ 'LEFT JOIN',
119  [ 'log_namespace=page_namespace',
120  'log_title=page_title' ] ]
121  ] );
122  // log_page is the page_id saved at log time, whereas page_id is from a
123  // join at query time. This leads to different results in various
124  // scenarios, e.g. deletion, recreation.
125  $this->addFields( [ 'page_id', 'log_page' ] );
126  }
127  $this->addFieldsIf( [ 'actor_name', 'actor_user' ], $this->fld_user );
128  $this->addFieldsIf( 'actor_user', $this->fld_userid );
129  $this->addFieldsIf(
130  [ 'log_namespace', 'log_title' ],
131  $this->fld_title || $this->fld_parsedcomment
132  );
133  $this->addFieldsIf( 'log_params', $this->fld_details || $this->fld_ids );
134 
135  if ( $this->fld_comment || $this->fld_parsedcomment ) {
136  $commentQuery = $this->commentStore->getJoin( 'log_comment' );
137  $this->addTables( $commentQuery['tables'] );
138  $this->addFields( $commentQuery['fields'] );
139  $this->addJoinConds( $commentQuery['joins'] );
140  }
141 
142  if ( $this->fld_tags ) {
143  $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'logging' ) ] );
144  }
145 
146  if ( $params['tag'] !== null ) {
147  $this->addTables( 'change_tag' );
148  $this->addJoinConds( [ 'change_tag' => [ 'JOIN',
149  [ 'log_id=ct_log_id' ] ] ] );
150  try {
151  $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $params['tag'] ) );
152  } catch ( NameTableAccessException $exception ) {
153  // Return nothing.
154  $this->addWhere( '1=0' );
155  }
156  }
157 
158  if ( $params['action'] !== null ) {
159  // Do validation of action param, list of allowed actions can contains wildcards
160  // Allow the param, when the actions is in the list or a wildcard version is listed.
161  $logAction = $params['action'];
162  if ( !str_contains( $logAction, '/' ) ) {
163  // all items in the list have a slash
164  $valid = false;
165  } else {
166  $logActions = array_fill_keys( $this->getAllowedLogActions(), true );
167  [ $type, $action ] = explode( '/', $logAction, 2 );
168  $valid = isset( $logActions[$logAction] ) || isset( $logActions[$type . '/*'] );
169  }
170 
171  if ( !$valid ) {
172  $encParamName = $this->encodeParamName( 'action' );
173  $this->dieWithError(
174  [ 'apierror-unrecognizedvalue', $encParamName, wfEscapeWikiText( $logAction ) ],
175  "unknown_$encParamName"
176  );
177  }
178 
179  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable,PhanPossiblyUndeclaredVariable T240141
180  $this->addWhereFld( 'log_type', $type );
181  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable,PhanPossiblyUndeclaredVariable T240141
182  $this->addWhereFld( 'log_action', $action );
183  } elseif ( $params['type'] !== null ) {
184  $this->addWhereFld( 'log_type', $params['type'] );
185  }
186 
187  $this->addTimestampWhereRange(
188  'log_timestamp',
189  $params['dir'],
190  $params['start'],
191  $params['end']
192  );
193  // Include in ORDER BY for uniqueness
194  $this->addWhereRange( 'log_id', $params['dir'], null, null );
195 
196  if ( $params['continue'] !== null ) {
197  $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'timestamp', 'int' ] );
198  $op = ( $params['dir'] === 'newer' ? '>=' : '<=' );
199  $this->addWhere( $db->buildComparison( $op, [
200  'log_timestamp' => $db->timestamp( $cont[0] ),
201  'log_id' => $cont[1],
202  ] ) );
203  }
204 
205  $limit = $params['limit'];
206  $this->addOption( 'LIMIT', $limit + 1 );
207 
208  $user = $params['user'];
209  if ( $user !== null ) {
210  $this->addWhereFld( 'actor_name', $user );
211  }
212 
213  $title = $params['title'];
214  if ( $title !== null ) {
215  $titleObj = Title::newFromText( $title );
216  if ( $titleObj === null || $titleObj->isExternal() ) {
217  $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] );
218  }
219  $this->addWhereFld( 'log_namespace', $titleObj->getNamespace() );
220  $this->addWhereFld( 'log_title', $titleObj->getDBkey() );
221  }
222 
223  if ( $params['namespace'] !== null ) {
224  $this->addWhereFld( 'log_namespace', $params['namespace'] );
225  }
226 
227  $prefix = $params['prefix'];
228 
229  if ( $prefix !== null ) {
230  if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
231  $this->dieWithError( 'apierror-prefixsearchdisabled' );
232  }
233 
234  $title = Title::newFromText( $prefix );
235  if ( $title === null || $title->isExternal() ) {
236  $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $prefix ) ] );
237  }
238  $this->addWhereFld( 'log_namespace', $title->getNamespace() );
239  $this->addWhere( 'log_title ' . $db->buildLike( $title->getDBkey(), $db->anyString() ) );
240  }
241 
242  // Paranoia: avoid brute force searches (T19342)
243  if ( $params['namespace'] !== null || $title !== null || $user !== null ) {
244  if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
245  $titleBits = LogPage::DELETED_ACTION;
246  $userBits = LogPage::DELETED_USER;
247  } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
250  } else {
251  $titleBits = 0;
252  $userBits = 0;
253  }
254  if ( ( $params['namespace'] !== null || $title !== null ) && $titleBits ) {
255  $this->addWhere( $db->bitAnd( 'log_deleted', $titleBits ) . " != $titleBits" );
256  }
257  if ( $user !== null && $userBits ) {
258  $this->addWhere( $db->bitAnd( 'log_deleted', $userBits ) . " != $userBits" );
259  }
260  }
261 
262  // T220999: MySQL/MariaDB (10.1.37) can sometimes irrationally decide that querying `actor` before
263  // `logging` and filesorting is somehow better than querying $limit+1 rows from `logging`.
264  // Tell it not to reorder the query. But not when `letag` was used, as it seems as likely
265  // to be harmed as helped in that case.
266  // If "user" was specified, it's obviously correct to query actor first (T282122)
267  if ( $params['tag'] === null && $user === null ) {
268  $this->addOption( 'STRAIGHT_JOIN' );
269  }
270 
271  $this->addOption(
272  'MAX_EXECUTION_TIME',
273  $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries )
274  );
275 
276  $count = 0;
277  $res = $this->select( __METHOD__ );
278 
279  if ( $this->fld_title ) {
280  $this->executeGenderCacheFromResultWrapper( $res, __METHOD__, 'log' );
281  }
282  if ( $this->fld_parsedcomment ) {
283  $this->formattedComments = $this->commentFormatter->formatItems(
284  $this->commentFormatter->rows( $res )
285  ->commentKey( 'log_comment' )
286  ->indexField( 'log_id' )
287  ->namespaceField( 'log_namespace' )
288  ->titleField( 'log_title' )
289  );
290  }
291 
292  $result = $this->getResult();
293  foreach ( $res as $row ) {
294  if ( ++$count > $limit ) {
295  // We've reached the one extra which shows that there are
296  // additional pages to be had. Stop here...
297  $this->setContinueEnumParameter( 'continue', "$row->log_timestamp|$row->log_id" );
298  break;
299  }
300 
301  $vals = $this->extractRowInfo( $row );
302  $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
303  if ( !$fit ) {
304  $this->setContinueEnumParameter( 'continue', "$row->log_timestamp|$row->log_id" );
305  break;
306  }
307  }
308  $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'item' );
309  }
310 
311  private function extractRowInfo( $row ) {
312  $logEntry = DatabaseLogEntry::newFromRow( $row );
313  $vals = [
314  ApiResult::META_TYPE => 'assoc',
315  ];
316  $anyHidden = false;
317 
318  if ( $this->fld_ids ) {
319  $vals['logid'] = (int)$row->log_id;
320  }
321 
322  if ( $this->fld_title ) {
323  $title = Title::makeTitle( $row->log_namespace, $row->log_title );
324  }
325 
326  $authority = $this->getAuthority();
327  if ( $this->fld_title || $this->fld_ids || $this->fld_details && $row->log_params !== '' ) {
329  $vals['actionhidden'] = true;
330  $anyHidden = true;
331  }
332  if ( LogEventsList::userCan( $row, LogPage::DELETED_ACTION, $authority ) ) {
333  if ( $this->fld_title ) {
334  // @phan-suppress-next-next-line PhanTypeMismatchArgumentNullable,PhanPossiblyUndeclaredVariable
335  // title is set when used
337  }
338  if ( $this->fld_ids ) {
339  $vals['pageid'] = (int)$row->page_id;
340  $vals['logpage'] = (int)$row->log_page;
341  $revId = $logEntry->getAssociatedRevId();
342  if ( $revId ) {
343  $vals['revid'] = (int)$revId;
344  }
345  }
346  if ( $this->fld_details ) {
347  $vals['params'] = LogFormatter::newFromEntry( $logEntry )->formatParametersForApi();
348  }
349  }
350  }
351 
352  if ( $this->fld_type ) {
353  $vals['type'] = $row->log_type;
354  $vals['action'] = $row->log_action;
355  }
356 
357  if ( $this->fld_user || $this->fld_userid ) {
359  $vals['userhidden'] = true;
360  $anyHidden = true;
361  }
362  if ( LogEventsList::userCan( $row, LogPage::DELETED_USER, $authority ) ) {
363  if ( $this->fld_user ) {
364  $vals['user'] = $row->actor_name;
365  }
366  if ( $this->fld_userid ) {
367  $vals['userid'] = (int)$row->actor_user;
368  }
369 
370  if ( !$row->actor_user ) {
371  $vals['anon'] = true;
372  }
373  }
374  }
375  if ( $this->fld_timestamp ) {
376  $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->log_timestamp );
377  }
378 
379  if ( $this->fld_comment || $this->fld_parsedcomment ) {
381  $vals['commenthidden'] = true;
382  $anyHidden = true;
383  }
384  if ( LogEventsList::userCan( $row, LogPage::DELETED_COMMENT, $authority ) ) {
385  if ( $this->fld_comment ) {
386  $vals['comment'] = $this->commentStore->getComment( 'log_comment', $row )->text;
387  }
388 
389  if ( $this->fld_parsedcomment ) {
390  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
391  $vals['parsedcomment'] = $this->formattedComments[$row->log_id];
392  }
393  }
394  }
395 
396  if ( $this->fld_tags ) {
397  if ( $row->ts_tags ) {
398  $tags = explode( ',', $row->ts_tags );
399  ApiResult::setIndexedTagName( $tags, 'tag' );
400  $vals['tags'] = $tags;
401  } else {
402  $vals['tags'] = [];
403  }
404  }
405 
406  if ( $anyHidden && LogEventsList::isDeleted( $row, LogPage::DELETED_RESTRICTED ) ) {
407  $vals['suppressed'] = true;
408  }
409 
410  return $vals;
411  }
412 
416  private function getAllowedLogActions() {
417  $config = $this->getConfig();
418  return array_keys( array_merge(
419  $config->get( MainConfigNames::LogActions ),
420  $config->get( MainConfigNames::LogActionsHandlers )
421  ) );
422  }
423 
424  public function getCacheMode( $params ) {
425  if ( $this->userCanSeeRevDel() ) {
426  return 'private';
427  }
428  if ( $params['prop'] !== null && in_array( 'parsedcomment', $params['prop'] ) ) {
429  // MediaWiki\CommentFormatter\CommentFormatter::formatItems() calls wfMessage() among other things
430  return 'anon-public-user-private';
431  } elseif ( LogEventsList::getExcludeClause( $this->getDB(), 'user', $this->getAuthority() )
432  === LogEventsList::getExcludeClause( $this->getDB(), 'public' )
433  ) { // Output can only contain public data.
434  return 'public';
435  } else {
436  return 'anon-public-user-private';
437  }
438  }
439 
440  public function getAllowedParams( $flags = 0 ) {
441  $config = $this->getConfig();
442  if ( $flags & ApiBase::GET_VALUES_FOR_HELP ) {
443  $logActions = $this->getAllowedLogActions();
444  sort( $logActions );
445  } else {
446  $logActions = null;
447  }
448  $ret = [
449  'prop' => [
450  ParamValidator::PARAM_ISMULTI => true,
451  ParamValidator::PARAM_DEFAULT => 'ids|title|type|user|timestamp|comment|details',
452  ParamValidator::PARAM_TYPE => [
453  'ids',
454  'title',
455  'type',
456  'user',
457  'userid',
458  'timestamp',
459  'comment',
460  'parsedcomment',
461  'details',
462  'tags'
463  ],
465  ],
466  'type' => [
467  ParamValidator::PARAM_TYPE => LogPage::validTypes(),
468  ],
469  'action' => [
470  // validation on request is done in execute()
471  ParamValidator::PARAM_TYPE => $logActions
472  ],
473  'start' => [
474  ParamValidator::PARAM_TYPE => 'timestamp'
475  ],
476  'end' => [
477  ParamValidator::PARAM_TYPE => 'timestamp'
478  ],
479  'dir' => [
480  ParamValidator::PARAM_DEFAULT => 'older',
481  ParamValidator::PARAM_TYPE => [
482  'newer',
483  'older'
484  ],
485  ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
486  ],
487  'user' => [
488  ParamValidator::PARAM_TYPE => 'user',
489  UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
490  ],
491  'title' => null,
492  'namespace' => [
493  ParamValidator::PARAM_TYPE => 'namespace',
494  NamespaceDef::PARAM_EXTRA_NAMESPACES => [ NS_MEDIA, NS_SPECIAL ],
495  ],
496  'prefix' => [],
497  'tag' => null,
498  'limit' => [
499  ParamValidator::PARAM_DEFAULT => 10,
500  ParamValidator::PARAM_TYPE => 'limit',
501  IntegerDef::PARAM_MIN => 1,
502  IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
503  IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
504  ],
505  'continue' => [
506  ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
507  ],
508  ];
509 
510  if ( $config->get( MainConfigNames::MiserMode ) ) {
511  $ret['prefix'][ApiBase::PARAM_HELP_MSG] = 'api-help-param-disabled-in-miser-mode';
512  }
513 
514  return $ret;
515  }
516 
517  protected function getExamplesMessages() {
518  return [
519  'action=query&list=logevents'
520  => 'apihelp-query+logevents-example-simple',
521  ];
522  }
523 
524  public function getHelpUrls() {
525  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Logevents';
526  }
527 }
const NS_SPECIAL
Definition: Defines.php:53
const NS_MEDIA
Definition: Defines.php:52
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition: ApiBase.php:1460
encodeParamName( $paramName)
This method mangles parameter name based on the prefix supplied to the constructor.
Definition: ApiBase.php:751
parseContinueParamOrDie(string $continue, array $types)
Parse the 'continue' parameter in the usual format and validate the types of each part,...
Definition: ApiBase.php:1649
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, or 'string' with PARAM_ISMULTI,...
Definition: ApiBase.php:204
const LIMIT_BIG1
Fast query, standard limit.
Definition: ApiBase.php:229
requireMaxOneParameter( $params,... $required)
Die if more than one of a certain set of parameters is set and not false.
Definition: ApiBase.php:946
getResult()
Get the result object.
Definition: ApiBase.php:637
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:773
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition: ApiBase.php:166
const GET_VALUES_FOR_HELP
getAllowedParams() flag: When set, the result could take longer to generate, but should be more thoro...
Definition: ApiBase.php:242
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition: ApiBase.php:231
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:506
This is a base class for all Query modules.
static addTitleInfo(&$arr, $title, $prefix='')
Add information (title and namespace) about a Title object to a result array.
setContinueEnumParameter( $paramName, $paramValue)
Set a query-continue value.
addWhereRange( $field, $dir, $start, $end, $sort=true)
Add a WHERE clause corresponding to a range, and an ORDER BY clause to sort in the right direction.
addFields( $value)
Add a set of fields to select to the internal array.
addOption( $name, $value=null)
Add an option such as LIMIT or USE INDEX.
addTables( $tables, $alias=null)
Add a set of tables to the internal array.
addTimestampWhereRange( $field, $dir, $start, $end, $sort=true)
Add a WHERE clause corresponding to a range, similar to addWhereRange, but converts $start and $end t...
getDB()
Get the Query database connection (read-only)
executeGenderCacheFromResultWrapper(IResultWrapper $res, $fname=__METHOD__, $fieldPrefix='page')
Preprocess the result set to fill the GenderCache with the necessary information before using self::a...
select( $method, $extraQuery=[], array &$hookData=null)
Execute a SELECT query based on the values in the internal arrays.
addFieldsIf( $value, $condition)
Same as addFields(), but add the fields only if a condition is met.
addJoinConds( $join_conds)
Add a set of JOIN conditions to the internal array.
addWhereFld( $field, $value)
Equivalent to addWhere( [ $field => $value ] )
addWhere( $value)
Add a set of WHERE clauses to the internal array.
userCanSeeRevDel()
Check whether the current user has permission to view revision-deleted fields.
Query action to List the log events, with optional filtering by various parameters.
getExamplesMessages()
Returns usage examples for this module.
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
__construct(ApiQuery $query, $moduleName, CommentStore $commentStore, RowCommentFormatter $commentFormatter, NameTableStore $changeTagDefStore)
getCacheMode( $params)
Get the cache mode for the data generated by this module.
getHelpUrls()
Return links to more detailed help pages about the module.
This is the main query class.
Definition: ApiQuery.php:42
const META_TYPE
Key for the 'type' metadata item.
Definition: ApiResult.php:110
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:604
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
static newFromRow( $row)
Constructs new LogEntry from database result row.
static getExcludeClause( $db, $audience='public', Authority $performer=null)
SQL clause to skip forbidden log types for this user.
static userCan( $row, $field, Authority $performer)
Determine if the current user is allowed to view a particular field of this log row,...
static isDeleted( $row, $field)
static newFromEntry(LogEntry $entry)
Constructs a new formatter suitable for given entry.
const DELETED_USER
Definition: LogPage.php:44
const DELETED_RESTRICTED
Definition: LogPage.php:45
const DELETED_COMMENT
Definition: LogPage.php:43
static validTypes()
Get the list of valid log types.
Definition: LogPage.php:209
const DELETED_ACTION
Definition: LogPage.php:42
This is the main service interface for converting single-line comments from various DB comment fields...
This is basically a CommentFormatter with a CommentStore dependency, allowing it to retrieve comment ...
Handle database storage of comments such as edit summaries and log reasons.
A class containing constants representing the names of configuration variables.
Exception representing a failure to look up a row from a name table.
Represents a title within MediaWiki.
Definition: Title.php:82
Service for formatting and validating API parameters.
Type definition for integer types.
Definition: IntegerDef.php:23