MediaWiki 1.41.2
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
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' ) ) {
243 $titleBits = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
244 $userBits = LogPage::DELETED_USER | LogPage::DELETED_RESTRICTED;
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 !== '' ) {
323 if ( LogEventsList::isDeleted( $row, LogPage::DELETED_ACTION ) ) {
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 ) {
353 if ( LogEventsList::isDeleted( $row, LogPage::DELETED_USER ) ) {
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 ) {
375 if ( LogEventsList::isDeleted( $row, LogPage::DELETED_COMMENT ) ) {
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
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
static newFromEntry(LogEntry $entry)
Constructs a new formatter suitable for given entry.
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.
Type definition for namespace types.
Type definition for user types.
Definition UserDef.php:27
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.