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