MediaWiki  master
ApiQueryLogEvents.php
Go to the documentation of this file.
1 <?php
25 
32 
33  private $commentStore;
34 
35  public function __construct( ApiQuery $query, $moduleName ) {
36  parent::__construct( $query, $moduleName, 'le' );
37  }
38 
39  private $fld_ids = false, $fld_title = false, $fld_type = false,
40  $fld_user = false, $fld_userid = false,
41  $fld_timestamp = false, $fld_comment = false, $fld_parsedcomment = false,
42  $fld_details = false, $fld_tags = false;
43 
44  public function execute() {
45  $params = $this->extractRequestParams();
46  $db = $this->getDB();
47  $this->commentStore = CommentStore::getStore();
48  $this->requireMaxOneParameter( $params, 'title', 'prefix', 'namespace' );
49 
50  $prop = array_flip( $params['prop'] );
51 
52  $this->fld_ids = isset( $prop['ids'] );
53  $this->fld_title = isset( $prop['title'] );
54  $this->fld_type = isset( $prop['type'] );
55  $this->fld_user = isset( $prop['user'] );
56  $this->fld_userid = isset( $prop['userid'] );
57  $this->fld_timestamp = isset( $prop['timestamp'] );
58  $this->fld_comment = isset( $prop['comment'] );
59  $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
60  $this->fld_details = isset( $prop['details'] );
61  $this->fld_tags = isset( $prop['tags'] );
62 
63  $hideLogs = LogEventsList::getExcludeClause( $db, 'user', $this->getUser() );
64  if ( $hideLogs !== false ) {
65  $this->addWhere( $hideLogs );
66  }
67 
68  $actorMigration = ActorMigration::newMigration();
69  $actorQuery = $actorMigration->getJoin( 'log_user' );
70  $this->addTables( 'logging' );
71  $this->addTables( $actorQuery['tables'] );
72  $this->addTables( [ 'user', 'page' ] );
73  $this->addJoinConds( $actorQuery['joins'] );
74  $this->addJoinConds( [
75  'user' => [ 'LEFT JOIN',
76  'user_id=' . $actorQuery['fields']['log_user'] ],
77  'page' => [ 'LEFT JOIN',
78  [ 'log_namespace=page_namespace',
79  'log_title=page_title' ] ] ] );
80 
81  $this->addFields( [
82  'log_id',
83  'log_type',
84  'log_action',
85  'log_timestamp',
86  'log_deleted',
87  ] );
88 
89  $this->addFieldsIf( 'page_id', $this->fld_ids );
90  // log_page is the page_id saved at log time, whereas page_id is from a
91  // join at query time. This leads to different results in various
92  // scenarios, e.g. deletion, recreation.
93  $this->addFieldsIf( 'log_page', $this->fld_ids );
94  $this->addFieldsIf( $actorQuery['fields'] + [ 'user_name' ], $this->fld_user );
95  $this->addFieldsIf( $actorQuery['fields'], $this->fld_userid );
96  $this->addFieldsIf(
97  [ 'log_namespace', 'log_title' ],
98  $this->fld_title || $this->fld_parsedcomment
99  );
100  $this->addFieldsIf( 'log_params', $this->fld_details );
101 
102  if ( $this->fld_comment || $this->fld_parsedcomment ) {
103  $commentQuery = $this->commentStore->getJoin( 'log_comment' );
104  $this->addTables( $commentQuery['tables'] );
105  $this->addFields( $commentQuery['fields'] );
106  $this->addJoinConds( $commentQuery['joins'] );
107  }
108 
109  if ( $this->fld_tags ) {
110  $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'logging' ) ] );
111  }
112 
113  if ( !is_null( $params['tag'] ) ) {
114  $this->addTables( 'change_tag' );
115  $this->addJoinConds( [ 'change_tag' => [ 'JOIN',
116  [ 'log_id=ct_log_id' ] ] ] );
117  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
118  try {
119  $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) );
120  } catch ( NameTableAccessException $exception ) {
121  // Return nothing.
122  $this->addWhere( '1=0' );
123  }
124  }
125 
126  if ( !is_null( $params['action'] ) ) {
127  // Do validation of action param, list of allowed actions can contains wildcards
128  // Allow the param, when the actions is in the list or a wildcard version is listed.
129  $logAction = $params['action'];
130  if ( strpos( $logAction, '/' ) === false ) {
131  // all items in the list have a slash
132  $valid = false;
133  } else {
134  $logActions = array_flip( $this->getAllowedLogActions() );
135  list( $type, $action ) = explode( '/', $logAction, 2 );
136  $valid = isset( $logActions[$logAction] ) || isset( $logActions[$type . '/*'] );
137  }
138 
139  if ( !$valid ) {
140  $encParamName = $this->encodeParamName( 'action' );
141  $this->dieWithError(
142  [ 'apierror-unrecognizedvalue', $encParamName, wfEscapeWikiText( $logAction ) ],
143  "unknown_$encParamName"
144  );
145  }
146 
147  $this->addWhereFld( 'log_type', $type );
148  $this->addWhereFld( 'log_action', $action );
149  } elseif ( !is_null( $params['type'] ) ) {
150  $this->addWhereFld( 'log_type', $params['type'] );
151  }
152 
153  $this->addTimestampWhereRange(
154  'log_timestamp',
155  $params['dir'],
156  $params['start'],
157  $params['end']
158  );
159  // Include in ORDER BY for uniqueness
160  $this->addWhereRange( 'log_id', $params['dir'], null, null );
161 
162  if ( !is_null( $params['continue'] ) ) {
163  $cont = explode( '|', $params['continue'] );
164  $this->dieContinueUsageIf( count( $cont ) != 2 );
165  $op = ( $params['dir'] === 'newer' ? '>' : '<' );
166  $continueTimestamp = $db->addQuotes( $db->timestamp( $cont[0] ) );
167  $continueId = (int)$cont[1];
168  $this->dieContinueUsageIf( $continueId != $cont[1] );
169  $this->addWhere( "log_timestamp $op $continueTimestamp OR " .
170  "(log_timestamp = $continueTimestamp AND " .
171  "log_id $op= $continueId)"
172  );
173  }
174 
175  $limit = $params['limit'];
176  $this->addOption( 'LIMIT', $limit + 1 );
177 
178  $user = $params['user'];
179  if ( !is_null( $user ) ) {
180  // Note the joins in $q are the same as those from ->getJoin() above
181  // so we only need to add 'conds' here.
182  $q = $actorMigration->getWhere(
183  $db, 'log_user', User::newFromName( $params['user'], false )
184  );
185  $this->addWhere( $q['conds'] );
186 
187  // T71222: MariaDB's optimizer, at least 10.1.37 and .38, likes to choose a wildly bad plan for
188  // some reason for this code path. Tell it not to use the wrong index it wants to pick.
189  // @phan-suppress-next-line PhanTypeMismatchArgument
190  $this->addOption( 'IGNORE INDEX', [ 'logging' => [ 'times' ] ] );
191  }
192 
193  $title = $params['title'];
194  if ( !is_null( $title ) ) {
195  $titleObj = Title::newFromText( $title );
196  if ( is_null( $titleObj ) ) {
197  $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] );
198  }
199  $this->addWhereFld( 'log_namespace', $titleObj->getNamespace() );
200  $this->addWhereFld( 'log_title', $titleObj->getDBkey() );
201  }
202 
203  if ( $params['namespace'] !== null ) {
204  $this->addWhereFld( 'log_namespace', $params['namespace'] );
205  }
206 
207  $prefix = $params['prefix'];
208 
209  if ( !is_null( $prefix ) ) {
210  if ( $this->getConfig()->get( 'MiserMode' ) ) {
211  $this->dieWithError( 'apierror-prefixsearchdisabled' );
212  }
213 
214  $title = Title::newFromText( $prefix );
215  if ( is_null( $title ) ) {
216  $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $prefix ) ] );
217  }
218  $this->addWhereFld( 'log_namespace', $title->getNamespace() );
219  $this->addWhere( 'log_title ' . $db->buildLike( $title->getDBkey(), $db->anyString() ) );
220  }
221 
222  // Paranoia: avoid brute force searches (T19342)
223  if ( $params['namespace'] !== null || !is_null( $title ) || !is_null( $user ) ) {
224  if ( !$this->getPermissionManager()->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
225  $titleBits = LogPage::DELETED_ACTION;
226  $userBits = LogPage::DELETED_USER;
227  } elseif ( !$this->getPermissionManager()
228  ->userHasAnyRight( $this->getUser(), 'suppressrevision', 'viewsuppressed' )
229  ) {
232  } else {
233  $titleBits = 0;
234  $userBits = 0;
235  }
236  if ( ( $params['namespace'] !== null || !is_null( $title ) ) && $titleBits ) {
237  $this->addWhere( $db->bitAnd( 'log_deleted', $titleBits ) . " != $titleBits" );
238  }
239  if ( !is_null( $user ) && $userBits ) {
240  $this->addWhere( $db->bitAnd( 'log_deleted', $userBits ) . " != $userBits" );
241  }
242  }
243 
244  // T220999: MySQL/MariaDB (10.1.37) can sometimes irrationally decide that querying `actor` before
245  // `logging` and filesorting is somehow better than querying $limit+1 rows from `logging`.
246  // Tell it not to reorder the query. But not when `letag` was used, as it seems as likely
247  // to be harmed as helped in that case.
248  if ( $params['tag'] === null ) {
249  $this->addOption( 'STRAIGHT_JOIN' );
250  }
251 
252  $count = 0;
253  $res = $this->select( __METHOD__ );
254  $result = $this->getResult();
255  foreach ( $res as $row ) {
256  if ( ++$count > $limit ) {
257  // We've reached the one extra which shows that there are
258  // additional pages to be had. Stop here...
259  $this->setContinueEnumParameter( 'continue', "$row->log_timestamp|$row->log_id" );
260  break;
261  }
262 
263  $vals = $this->extractRowInfo( $row );
264  $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
265  if ( !$fit ) {
266  $this->setContinueEnumParameter( 'continue', "$row->log_timestamp|$row->log_id" );
267  break;
268  }
269  }
270  $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'item' );
271  }
272 
273  private function extractRowInfo( $row ) {
274  $logEntry = DatabaseLogEntry::newFromRow( $row );
275  $vals = [
276  ApiResult::META_TYPE => 'assoc',
277  ];
278  $anyHidden = false;
279  $user = $this->getUser();
280 
281  if ( $this->fld_ids ) {
282  $vals['logid'] = (int)$row->log_id;
283  }
284 
285  if ( $this->fld_title || $this->fld_parsedcomment ) {
286  $title = Title::makeTitle( $row->log_namespace, $row->log_title );
287  }
288 
289  if ( $this->fld_title || $this->fld_ids || $this->fld_details && $row->log_params !== '' ) {
291  $vals['actionhidden'] = true;
292  $anyHidden = true;
293  }
294  if ( LogEventsList::userCan( $row, LogPage::DELETED_ACTION, $user ) ) {
295  if ( $this->fld_title ) {
297  }
298  if ( $this->fld_ids ) {
299  $vals['pageid'] = (int)$row->page_id;
300  $vals['logpage'] = (int)$row->log_page;
301  }
302  if ( $this->fld_details ) {
303  $vals['params'] = LogFormatter::newFromEntry( $logEntry )->formatParametersForApi();
304  }
305  }
306  }
307 
308  if ( $this->fld_type ) {
309  $vals['type'] = $row->log_type;
310  $vals['action'] = $row->log_action;
311  }
312 
313  if ( $this->fld_user || $this->fld_userid ) {
315  $vals['userhidden'] = true;
316  $anyHidden = true;
317  }
318  if ( LogEventsList::userCan( $row, LogPage::DELETED_USER, $user ) ) {
319  if ( $this->fld_user ) {
320  $vals['user'] = $row->user_name ?? $row->log_user_text;
321  }
322  if ( $this->fld_userid ) {
323  $vals['userid'] = (int)$row->log_user;
324  }
325 
326  if ( !$row->log_user ) {
327  $vals['anon'] = true;
328  }
329  }
330  }
331  if ( $this->fld_timestamp ) {
332  $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->log_timestamp );
333  }
334 
335  if ( $this->fld_comment || $this->fld_parsedcomment ) {
337  $vals['commenthidden'] = true;
338  $anyHidden = true;
339  }
340  if ( LogEventsList::userCan( $row, LogPage::DELETED_COMMENT, $user ) ) {
341  $comment = $this->commentStore->getComment( 'log_comment', $row )->text;
342  if ( $this->fld_comment ) {
343  $vals['comment'] = $comment;
344  }
345 
346  if ( $this->fld_parsedcomment ) {
347  $vals['parsedcomment'] = Linker::formatComment( $comment, $title );
348  }
349  }
350  }
351 
352  if ( $this->fld_tags ) {
353  if ( $row->ts_tags ) {
354  $tags = explode( ',', $row->ts_tags );
355  ApiResult::setIndexedTagName( $tags, 'tag' );
356  $vals['tags'] = $tags;
357  } else {
358  $vals['tags'] = [];
359  }
360  }
361 
362  if ( $anyHidden && LogEventsList::isDeleted( $row, LogPage::DELETED_RESTRICTED ) ) {
363  $vals['suppressed'] = true;
364  }
365 
366  return $vals;
367  }
368 
372  private function getAllowedLogActions() {
373  $config = $this->getConfig();
374  return array_keys( array_merge(
375  $config->get( 'LogActions' ),
376  $config->get( 'LogActionsHandlers' )
377  ) );
378  }
379 
380  public function getCacheMode( $params ) {
381  if ( $this->userCanSeeRevDel() ) {
382  return 'private';
383  }
384  if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) {
385  // formatComment() calls wfMessage() among other things
386  return 'anon-public-user-private';
387  } elseif ( LogEventsList::getExcludeClause( $this->getDB(), 'user', $this->getUser() )
388  === LogEventsList::getExcludeClause( $this->getDB(), 'public' )
389  ) { // Output can only contain public data.
390  return 'public';
391  } else {
392  return 'anon-public-user-private';
393  }
394  }
395 
396  public function getAllowedParams( $flags = 0 ) {
397  $config = $this->getConfig();
398  if ( $flags & ApiBase::GET_VALUES_FOR_HELP ) {
399  $logActions = $this->getAllowedLogActions();
400  sort( $logActions );
401  } else {
402  $logActions = null;
403  }
404  $ret = [
405  'prop' => [
406  ApiBase::PARAM_ISMULTI => true,
407  ApiBase::PARAM_DFLT => 'ids|title|type|user|timestamp|comment|details',
409  'ids',
410  'title',
411  'type',
412  'user',
413  'userid',
414  'timestamp',
415  'comment',
416  'parsedcomment',
417  'details',
418  'tags'
419  ],
421  ],
422  'type' => [
424  ],
425  'action' => [
426  // validation on request is done in execute()
427  ApiBase::PARAM_TYPE => $logActions
428  ],
429  'start' => [
430  ApiBase::PARAM_TYPE => 'timestamp'
431  ],
432  'end' => [
433  ApiBase::PARAM_TYPE => 'timestamp'
434  ],
435  'dir' => [
436  ApiBase::PARAM_DFLT => 'older',
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',
450  ],
451  'prefix' => [],
452  'tag' => null,
453  'limit' => [
454  ApiBase::PARAM_DFLT => 10,
455  ApiBase::PARAM_TYPE => 'limit',
456  ApiBase::PARAM_MIN => 1,
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 }
select( $method, $extraQuery=[], array &$hookData=null)
Execute a SELECT query based on the values in the internal arrays.
const PARAM_TYPE
(string|string[]) Either an array of allowed value strings, or a string type as described below...
Definition: ApiBase.php:94
getDB()
Get the Query database connection (read-only)
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition: ApiBase.php:261
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
static addTitleInfo(&$arr, $title, $prefix='')
Add information (title and namespace) about a Title object to a result array.
getResult()
Get the result object.
Definition: ApiBase.php:640
addJoinConds( $join_conds)
Add a set of JOIN conditions to the internal array.
const PARAM_DFLT
(null|boolean|integer|string) Default value of the parameter.
Definition: ApiBase.php:55
const GET_VALUES_FOR_HELP
getAllowedParams() flag: When set, the result could take longer to generate, but should be more thoro...
Definition: ApiBase.php:272
const LIMIT_BIG1
Fast query, standard limit.
Definition: ApiBase.php:259
static newFromRow( $row)
Constructs new LogEntry from database result row.
Exception representing a failure to look up a row from a name table.
static newFromEntry(LogEntry $entry)
Constructs a new formatter suitable for given entry.
const PARAM_MAX
(integer) Max value allowed for the parameter, for PARAM_TYPE &#39;integer&#39; and &#39;limit&#39;.
Definition: ApiBase.php:97
This is a base class for all Query modules.
const NS_SPECIAL
Definition: Defines.php:49
dieWithError( $msg, $code=null, $data=null, $httpCode=null)
Abort execution with an error.
Definition: ApiBase.php:2005
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user...
Definition: ApiBase.php:761
const META_TYPE
Key for the &#39;type&#39; metadata item.
Definition: ApiResult.php:110
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, this is an array mapping those values to $msg...
Definition: ApiBase.php:164
__construct(ApiQuery $query, $moduleName)
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:616
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
const DELETED_COMMENT
Definition: LogPage.php:35
static newMigration()
Static constructor.
addTables( $tables, $alias=null)
Add a set of tables to the internal array.
static formatComment( $comment, $title=null, $local=false, $wikiId=null)
This function is called by all recent changes variants, by the page history, and by the user contribu...
Definition: Linker.php:1165
userCanSeeRevDel()
Check whether the current user has permission to view revision-deleted fields.
const NS_MEDIA
Definition: Defines.php:48
dieContinueUsageIf( $condition)
Die with the &#39;badcontinue&#39; error.
Definition: ApiBase.php:2199
static isDeleted( $row, $field)
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:520
addFields( $value)
Add a set of fields to select to the internal array.
const PARAM_MAX2
(integer) Max value allowed for the parameter for users with the apihighlimits right, for PARAM_TYPE &#39;limit&#39;.
Definition: ApiBase.php:103
This is the main query class.
Definition: ApiQuery.php:37
const DELETED_RESTRICTED
Definition: LogPage.php:37
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter...
Definition: ApiBase.php:131
const DELETED_USER
Definition: LogPage.php:36
addTimestampWhereRange( $field, $dir, $start, $end, $sort=true)
Add a WHERE clause corresponding to a range, similar to addWhereRange, but converts $start and $end t...
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:586
encodeParamName( $paramName)
This method mangles parameter name based on the prefix supplied to the constructor.
Definition: ApiBase.php:739
static getStore()
static userCan( $row, $field, User $user=null)
Determine if the current user is allowed to view a particular field of this log row, if it&#39;s marked as deleted and/or restricted log type.
addWhere( $value)
Add a set of WHERE clauses to the internal array.
static getExcludeClause( $db, $audience='public', User $user=null)
SQL clause to skip forbidden log types for this user.
const PARAM_ISMULTI
(boolean) Accept multiple pipe-separated values for this parameter (e.g.
Definition: ApiBase.php:58
getPermissionManager()
Obtain a PermissionManager instance that subclasses may use in their authorization checks...
Definition: ApiBase.php:710
addWhereFld( $field, $value)
Equivalent to addWhere( [ $field => $value ] )
Query action to List the log events, with optional filtering by various parameters.
addFieldsIf( $value, $condition)
Same as addFields(), but add the fields only if a condition is met.
const PARAM_EXTRA_NAMESPACES
(int[]) When PARAM_TYPE is &#39;namespace&#39;, include these as additional possible values.
Definition: ApiBase.php:193
addOption( $name, $value=null)
Add an option such as LIMIT or USE INDEX.
const DELETED_ACTION
Definition: LogPage.php:34
const PARAM_MIN
(integer) Lowest value allowed for the parameter, for PARAM_TYPE &#39;integer&#39; and &#39;limit&#39;.
Definition: ApiBase.php:106
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:515
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
Definition: ChangeTags.php:837
requireMaxOneParameter( $params,... $required)
Die if more than one of a certain set of parameters is set and not false.
Definition: ApiBase.php:928
static validTypes()
Get the list of valid log types.
Definition: LogPage.php:198
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:319
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...
setContinueEnumParameter( $paramName, $paramValue)
Set a query-continue value.