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