MediaWiki master
ApiQueryLogEvents.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Api;
10
29use Wikimedia\Timestamp\TimestampFormat as TS;
30
37
39 private $formattedComments;
40
41 public function __construct(
42 ApiQuery $query,
43 string $moduleName,
44 private readonly CommentStore $commentStore,
45 private readonly RowCommentFormatter $commentFormatter,
46 private readonly NameTableStore $changeTagDefStore,
47 private readonly ChangeTagsStore $changeTagsStore,
48 private readonly UserNameUtils $userNameUtils,
49 private readonly LogFormatterFactory $logFormatterFactory,
50 ) {
51 parent::__construct( $query, $moduleName, 'le' );
52 }
53
54 private bool $fld_ids = false;
55 private bool $fld_title = false;
56 private bool $fld_type = false;
57 private bool $fld_user = false;
58 private bool $fld_userid = false;
59 private bool $fld_timestamp = false;
60 private bool $fld_comment = false;
61 private bool $fld_parsedcomment = false;
62 private bool $fld_details = false;
63 private bool $fld_tags = false;
64
65 public function execute() {
66 $params = $this->extractRequestParams();
67 $db = $this->getDB();
68 $this->requireMaxOneParameter( $params, 'title', 'prefix', 'namespace' );
69
70 $prop = array_fill_keys( $params['prop'], true );
71
72 $this->fld_ids = isset( $prop['ids'] );
73 $this->fld_title = isset( $prop['title'] );
74 $this->fld_type = isset( $prop['type'] );
75 $this->fld_user = isset( $prop['user'] );
76 $this->fld_userid = isset( $prop['userid'] );
77 $this->fld_timestamp = isset( $prop['timestamp'] );
78 $this->fld_comment = isset( $prop['comment'] );
79 $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
80 $this->fld_details = isset( $prop['details'] );
81 $this->fld_tags = isset( $prop['tags'] );
82
83 $hideLogs = LogEventsList::getExcludeClause( $db, 'user', $this->getAuthority() );
84 if ( $hideLogs !== false ) {
85 $this->addWhere( $hideLogs );
86 }
87
88 $this->addTables( 'logging' );
89
90 $this->addFields( [
91 'log_id',
92 'log_type',
93 'log_action',
94 'log_timestamp',
95 'log_deleted',
96 ] );
97
98 if ( $params['ids'] ) {
99 $this->addWhereIDsFld( 'logging', 'log_id', $params['ids'] );
100 }
101
102 $user = $params['user'];
103 if ( $this->fld_user || $this->fld_userid || $user !== null ) {
104 $this->addTables( 'actor' );
105 $this->addJoinConds( [
106 'actor' => [ 'JOIN', 'actor_id=log_actor' ],
107 ] );
108 $this->addFieldsIf( [ 'actor_name', 'actor_user' ], $this->fld_user );
109 $this->addFieldsIf( 'actor_user', $this->fld_userid );
110 if ( $user !== null ) {
111 $this->addWhereFld( 'actor_name', $user );
112 }
113 }
114
115 if ( $this->fld_ids ) {
116 $this->addTables( 'page' );
117 $this->addJoinConds( [
118 'page' => [ 'LEFT JOIN',
119 [ 'log_namespace=page_namespace',
120 'log_title=page_title' ] ]
121 ] );
122 // log_page is the page_id saved at log time, whereas page_id is from a
123 // join at query time. This leads to different results in various
124 // scenarios, e.g. deletion, recreation.
125 $this->addFields( [ 'page_id', 'log_page' ] );
126 }
127 $this->addFieldsIf(
128 [ 'log_namespace', 'log_title' ],
129 $this->fld_title || $this->fld_parsedcomment
130 );
131 $this->addFieldsIf( 'log_params', $this->fld_details || $this->fld_ids );
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( [
142 'ts_tags' => $this->changeTagsStore->makeTagSummarySubquery( 'logging', $this->getAuthority() )
143 ] );
144 }
145
146 if ( $params['tag'] !== null ) {
147 if ( !$this->changeTagsStore->canViewTag( $params['tag'], $this->getAuthority() ) ) {
148 $this->addWhere( '1=0' );
149 }
150
151 $this->addTables( 'change_tag' );
152 $this->addJoinConds( [ 'change_tag' => [ 'JOIN',
153 [ 'log_id=ct_log_id' ] ] ] );
154 try {
155 $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $params['tag'] ) );
156 } catch ( NameTableAccessException ) {
157 // Return nothing.
158 $this->addWhere( '1=0' );
159 }
160 }
161
162 if ( $params['action'] !== null ) {
163 // Do validation of action param, list of allowed actions can contains wildcards
164 // Allow the param, when the actions is in the list or a wildcard version is listed.
165 $logAction = $params['action'];
166 if ( !str_contains( $logAction, '/' ) ) {
167 // all items in the list have a slash
168 $valid = false;
169 } else {
170 $logActions = array_fill_keys( $this->getAllowedLogActions(), true );
171 [ $type, $action ] = explode( '/', $logAction, 2 );
172 $valid = isset( $logActions[$logAction] ) || isset( $logActions[$type . '/*'] );
173 }
174
175 if ( !$valid ) {
176 $encParamName = $this->encodeParamName( 'action' );
177 $this->dieWithError(
178 [ 'apierror-unrecognizedvalue', $encParamName, wfEscapeWikiText( $logAction ) ],
179 "unknown_$encParamName"
180 );
181 }
182
183 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable
184 $this->addWhereFld( 'log_type', $type );
185 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable
186 $this->addWhereFld( 'log_action', $action );
187 } elseif ( $params['type'] !== null ) {
188 $this->addWhereFld( 'log_type', $params['type'] );
189 }
190
192 'log_timestamp',
193 $params['dir'],
194 $params['start'],
195 $params['end']
196 );
197 // Include in ORDER BY for uniqueness
198 $this->addWhereRange( 'log_id', $params['dir'], null, null );
199
200 if ( $params['continue'] !== null ) {
201 $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'timestamp', 'int' ] );
202 $op = ( $params['dir'] === 'newer' ? '>=' : '<=' );
203 $this->addWhere( $db->buildComparison( $op, [
204 'log_timestamp' => $db->timestamp( $cont[0] ),
205 'log_id' => $cont[1],
206 ] ) );
207 }
208
209 $limit = $params['limit'];
210 $this->addOption( 'LIMIT', $limit + 1 );
211
212 $title = $params['title'];
213 if ( $title !== null ) {
214 $titleObj = Title::newFromText( $title );
215 if ( $titleObj === null || $titleObj->isExternal() ) {
216 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] );
217 }
218 $this->addWhereFld( 'log_namespace', $titleObj->getNamespace() );
219 $this->addWhereFld( 'log_title', $titleObj->getDBkey() );
220 }
221
222 if ( $params['namespace'] !== null ) {
223 $this->addWhereFld( 'log_namespace', $params['namespace'] );
224 }
225
226 $prefix = $params['prefix'];
227
228 if ( $prefix !== null ) {
229 if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
230 $this->dieWithError( 'apierror-prefixsearchdisabled' );
231 }
232
233 $title = Title::newFromText( $prefix );
234 if ( $title === null || $title->isExternal() ) {
235 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $prefix ) ] );
236 }
237 $this->addWhereFld( 'log_namespace', $title->getNamespace() );
238 $this->addWhere(
239 $db->expr( 'log_title', IExpression::LIKE, new LikeValue( $title->getDBkey(), $db->anyString() ) )
240 );
241 }
242
243 // Paranoia: avoid brute force searches (T19342)
244 if ( $params['namespace'] !== null || $title !== null || $user !== null ) {
245 if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
246 $titleBits = LogPage::DELETED_ACTION;
247 $userBits = LogPage::DELETED_USER;
248 } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
249 $titleBits = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
250 $userBits = LogPage::DELETED_USER | LogPage::DELETED_RESTRICTED;
251 } else {
252 $titleBits = 0;
253 $userBits = 0;
254 }
255 if ( ( $params['namespace'] !== null || $title !== null ) && $titleBits ) {
256 $this->addWhere( $db->bitAnd( 'log_deleted', $titleBits ) . " != $titleBits" );
257 }
258 if ( $user !== null && $userBits ) {
259 $this->addWhere( $db->bitAnd( 'log_deleted', $userBits ) . " != $userBits" );
260 }
261 }
262
263 // T220999: MySQL/MariaDB (10.1.37) can sometimes irrationally decide that querying `actor` before
264 // `logging` and filesorting is somehow better than querying $limit+1 rows from `logging`.
265 // Tell it not to reorder the query. But not when `letag` was used, as it seems as likely
266 // to be harmed as helped in that case.
267 // If "user" was specified, it's obviously correct to query actor first (T282122)
268 if ( $params['tag'] === null && $user === null ) {
269 $this->addOption( 'STRAIGHT_JOIN' );
270 }
271
272 $this->addOption(
273 'MAX_EXECUTION_TIME',
275 );
276
277 $count = 0;
278 $res = $this->select( __METHOD__ );
279
280 if ( $this->fld_title ) {
281 $this->executeGenderCacheFromResultWrapper( $res, __METHOD__, 'log' );
282 }
283 if ( $this->fld_parsedcomment ) {
284 $this->formattedComments = $this->commentFormatter->formatItems(
285 $this->commentFormatter->rows( $res )
286 ->commentKey( 'log_comment' )
287 ->indexField( 'log_id' )
288 ->namespaceField( 'log_namespace' )
289 ->titleField( 'log_title' )
290 );
291 }
292
293 $result = $this->getResult();
294 foreach ( $res as $row ) {
295 if ( ++$count > $limit ) {
296 // We've reached the one extra which shows that there are
297 // additional pages to be had. Stop here...
298 $this->setContinueEnumParameter( 'continue', "$row->log_timestamp|$row->log_id" );
299 break;
300 }
301
302 $vals = $this->extractRowInfo( $row );
303 $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
304 if ( !$fit ) {
305 $this->setContinueEnumParameter( 'continue', "$row->log_timestamp|$row->log_id" );
306 break;
307 }
308 }
309 $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'item' );
310 }
311
312 private function extractRowInfo( \stdClass $row ): array {
313 $logEntry = DatabaseLogEntry::newFromRow( $row );
314 $vals = [
315 ApiResult::META_TYPE => 'assoc',
316 ];
317 $anyHidden = false;
318
319 if ( $this->fld_ids ) {
320 $vals['logid'] = (int)$row->log_id;
321 }
322
323 if ( $this->fld_title ) {
324 $title = Title::makeTitle( $row->log_namespace, $row->log_title );
325 }
326
327 $authority = $this->getAuthority();
328 if ( $this->fld_title || $this->fld_ids || ( $this->fld_details && $row->log_params !== '' ) ) {
329 if ( LogEventsList::isDeleted( $row, LogPage::DELETED_ACTION ) ) {
330 $vals['actionhidden'] = true;
331 $anyHidden = true;
332 }
333 if ( LogEventsList::userCan( $row, LogPage::DELETED_ACTION, $authority ) ) {
334 if ( $this->fld_title ) {
335 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
336 // title is set when used
337 ApiQueryBase::addTitleInfo( $vals, $title );
338 }
339 if ( $this->fld_ids ) {
340 $vals['pageid'] = (int)$row->page_id;
341 $vals['logpage'] = (int)$row->log_page;
342 $revId = $logEntry->getAssociatedRevId();
343 if ( $revId ) {
344 $vals['revid'] = (int)$revId;
345 }
346 }
347 if ( $this->fld_details ) {
348 $vals['params'] = $this->logFormatterFactory->newFromEntry( $logEntry )->formatParametersForApi();
349 }
350 }
351 }
352
353 if ( $this->fld_type ) {
354 $vals['type'] = $row->log_type;
355 $vals['action'] = $row->log_action;
356 }
357
358 if ( $this->fld_user || $this->fld_userid ) {
359 if ( LogEventsList::isDeleted( $row, LogPage::DELETED_USER ) ) {
360 $vals['userhidden'] = true;
361 $anyHidden = true;
362 }
363 if ( LogEventsList::userCan( $row, LogPage::DELETED_USER, $authority ) ) {
364 if ( $this->fld_user ) {
365 $vals['user'] = $row->actor_name;
366 }
367 if ( $this->fld_userid ) {
368 $vals['userid'] = (int)$row->actor_user;
369 }
370
371 if ( isset( $vals['user'] ) && $this->userNameUtils->isTemp( $vals['user'] ) ) {
372 $vals['temp'] = true;
373 }
374
375 if ( !$row->actor_user ) {
376 $vals['anon'] = true;
377 }
378 }
379 }
380 if ( $this->fld_timestamp ) {
381 $vals['timestamp'] = wfTimestamp( TS::ISO_8601, $row->log_timestamp );
382 }
383
384 if ( $this->fld_comment || $this->fld_parsedcomment ) {
385 if ( LogEventsList::isDeleted( $row, LogPage::DELETED_COMMENT ) ) {
386 $vals['commenthidden'] = true;
387 $anyHidden = true;
388 }
389 if ( LogEventsList::userCan( $row, LogPage::DELETED_COMMENT, $authority ) ) {
390 if ( $this->fld_comment ) {
391 $vals['comment'] = $this->commentStore->getComment( 'log_comment', $row )->text;
392 }
393
394 if ( $this->fld_parsedcomment ) {
395 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
396 $vals['parsedcomment'] = $this->formattedComments[$row->log_id];
397 }
398 }
399 }
400
401 if ( $this->fld_tags ) {
402 if ( $row->ts_tags ) {
403 $tags = explode( ',', $row->ts_tags );
404 ApiResult::setIndexedTagName( $tags, 'tag' );
405 $vals['tags'] = $tags;
406 } else {
407 $vals['tags'] = [];
408 }
409 }
410
411 if ( $anyHidden && LogEventsList::isDeleted( $row, LogPage::DELETED_RESTRICTED ) ) {
412 $vals['suppressed'] = true;
413 }
414
415 return $vals;
416 }
417
421 private function getAllowedLogActions() {
422 $config = $this->getConfig();
423 return array_keys( array_merge(
424 $config->get( MainConfigNames::LogActions ),
426 ) );
427 }
428
430 public function getCacheMode( $params ) {
431 if ( $this->userCanSeeRevDel() ) {
432 return 'private';
433 }
434 return 'anon-public-user-private';
435 }
436
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',
485 'newer' => 'api-help-paramvalue-direction-newer',
486 'older' => 'api-help-paramvalue-direction-older',
487 ],
488 ],
489 'ids' => [
490 ParamValidator::PARAM_TYPE => 'integer',
491 ParamValidator::PARAM_ISMULTI => true
492 ],
493 'user' => [
494 ParamValidator::PARAM_TYPE => 'user',
495 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
496 ],
497 'title' => null,
498 'namespace' => [
499 ParamValidator::PARAM_TYPE => 'namespace',
500 NamespaceDef::PARAM_EXTRA_NAMESPACES => [ NS_MEDIA, NS_SPECIAL ],
501 ],
502 'prefix' => [],
503 'tag' => null,
504 'limit' => [
505 ParamValidator::PARAM_DEFAULT => 10,
506 ParamValidator::PARAM_TYPE => 'limit',
507 IntegerDef::PARAM_MIN => 1,
508 IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
509 IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
510 ],
511 'continue' => [
512 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
513 ],
514 ];
515
516 if ( $config->get( MainConfigNames::MiserMode ) ) {
517 $ret['prefix'][ApiBase::PARAM_HELP_MSG] = 'api-help-param-disabled-in-miser-mode';
518 }
519
520 return $ret;
521 }
522
524 protected function getExamplesMessages() {
525 return [
526 'action=query&list=logevents'
527 => 'apihelp-query+logevents-example-simple',
528 ];
529 }
530
532 public function getHelpUrls() {
533 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Logevents';
534 }
535}
536
538class_alias( ApiQueryLogEvents::class, 'ApiQueryLogEvents' );
const NS_SPECIAL
Definition Defines.php:40
const NS_MEDIA
Definition Defines.php:39
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.
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1522
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:557
parseContinueParamOrDie(string $continue, array $types)
Parse the 'continue' parameter in the usual format and validate the types of each part,...
Definition ApiBase.php:1707
getResult()
Get the result object.
Definition ApiBase.php:696
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, or 'string' with PARAM_ISMULTI,...
Definition ApiBase.php:206
requireMaxOneParameter( $params,... $required)
Dies if more than one parameter from a certain set of parameters are set and not false.
Definition ApiBase.php:1012
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:166
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition ApiBase.php:233
encodeParamName( $paramName)
This method mangles parameter name based on the prefix supplied to the constructor.
Definition ApiBase.php:815
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:837
const GET_VALUES_FOR_HELP
getAllowedParams() flag: When this is set, the result could take longer to generate,...
Definition ApiBase.php:244
const LIMIT_BIG1
Fast query, standard limit.
Definition ApiBase.php:231
This is a base class for all Query modules.
addOption( $name, $value=null)
Add an option such as LIMIT or USE INDEX.
addFieldsIf( $value, $condition)
Same as addFields(), but add the fields only if a condition is met.
static addTitleInfo(&$arr, $title, $prefix='')
Add information (title and namespace) about a Title object to a result array.
addTables( $tables, $alias=null)
Add a set of tables to the internal array.
addJoinConds( $join_conds)
Add a set of JOIN conditions to the internal array.
addWhereIDsFld( $table, $field, $ids)
Like addWhereFld for an integer list of IDs.
getDB()
Get the Query database connection (read-only).
select( $method, $extraQuery=[], ?array &$hookData=null)
Execute a SELECT query based on the values in the internal arrays.
addWhere( $value)
Add a set of WHERE clauses to the internal array.
executeGenderCacheFromResultWrapper(IResultWrapper $res, $fname=__METHOD__, $fieldPrefix='page')
Preprocess the result set to fill the GenderCache with the necessary information before using self::a...
addTimestampWhereRange( $field, $dir, $start, $end, $sort=true)
Add a WHERE clause corresponding to a range, similar to addWhereRange, but converts $start and $end t...
setContinueEnumParameter( $paramName, $paramValue)
Set a query-continue value.
addWhereFld( $field, $value)
Equivalent to addWhere( [ $field => $value ] )
addFields( $value)
Add a set of fields to select to the internal array.
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.
Query action to List the log events, with optional filtering by various parameters.
getHelpUrls()
Return links to more detailed help pages about the module.1.25, returning boolean false is deprecated...
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
__construct(ApiQuery $query, string $moduleName, private readonly CommentStore $commentStore, private readonly RowCommentFormatter $commentFormatter, private readonly NameTableStore $changeTagDefStore, private readonly ChangeTagsStore $changeTagsStore, private readonly UserNameUtils $userNameUtils, private readonly LogFormatterFactory $logFormatterFactory,)
getExamplesMessages()
Returns usage examples for this module.Return value has query strings as keys, with values being eith...
getCacheMode( $params)
Get the cache mode for the data generated by this module.Override this in the module subclass....
This is the main query class.
Definition ApiQuery.php:36
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
const META_TYPE
Key for the 'type' metadata item.
Read-write access to the change_tags table.
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.
makeTitle( $linkId)
Convert a link ID to a Title.to override Title
A value class to process existing log entries.
Class to simplify the use of log pages.
Definition LogPage.php:34
A class containing constants representing the names of configuration variables.
const MaxExecutionTimeForExpensiveQueries
Name constant for the MaxExecutionTimeForExpensiveQueries setting, for use with Config::get()
const LogActionsHandlers
Name constant for the LogActionsHandlers setting, for use with Config::get()
const LogActions
Name constant for the LogActions setting, for use with Config::get()
const MiserMode
Name constant for the MiserMode setting, for use with Config::get()
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:69
UserNameUtils service.
Service for formatting and validating API parameters.
Type definition for integer types.
Content of like value.
Definition LikeValue.php:14