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