MediaWiki REL1_35
ApiQueryLogEvents.php
Go to the documentation of this file.
1<?php
26
33
36
37 public function __construct( ApiQuery $query, $moduleName ) {
38 parent::__construct( $query, $moduleName, 'le' );
39 }
40
41 private $fld_ids = false, $fld_title = false, $fld_type = false,
42 $fld_user = false, $fld_userid = false,
44 $fld_details = false, $fld_tags = false;
45
46 public function execute() {
47 $params = $this->extractRequestParams();
48 $db = $this->getDB();
49 $this->commentStore = CommentStore::getStore();
50 $this->requireMaxOneParameter( $params, 'title', 'prefix', 'namespace' );
51
52 $prop = array_flip( $params['prop'] );
53
54 $this->fld_ids = isset( $prop['ids'] );
55 $this->fld_title = isset( $prop['title'] );
56 $this->fld_type = isset( $prop['type'] );
57 $this->fld_user = isset( $prop['user'] );
58 $this->fld_userid = isset( $prop['userid'] );
59 $this->fld_timestamp = isset( $prop['timestamp'] );
60 $this->fld_comment = isset( $prop['comment'] );
61 $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
62 $this->fld_details = isset( $prop['details'] );
63 $this->fld_tags = isset( $prop['tags'] );
64
65 $hideLogs = LogEventsList::getExcludeClause( $db, 'user', $this->getUser() );
66 if ( $hideLogs !== false ) {
67 $this->addWhere( $hideLogs );
68 }
69
70 $actorMigration = ActorMigration::newMigration();
71 $actorQuery = $actorMigration->getJoin( 'log_user' );
72 $this->addTables( 'logging' );
73 $this->addTables( $actorQuery['tables'] );
74 $this->addTables( [ 'user', 'page' ] );
75 $this->addJoinConds( $actorQuery['joins'] );
76 $this->addJoinConds( [
77 'user' => [ 'LEFT JOIN',
78 'user_id=' . $actorQuery['fields']['log_user'] ],
79 'page' => [ 'LEFT JOIN',
80 [ 'log_namespace=page_namespace',
81 'log_title=page_title' ] ] ] );
82
83 $this->addFields( [
84 'log_id',
85 'log_type',
86 'log_action',
87 'log_timestamp',
88 'log_deleted',
89 ] );
90
91 $this->addFieldsIf( 'page_id', $this->fld_ids );
92 // log_page is the page_id saved at log time, whereas page_id is from a
93 // join at query time. This leads to different results in various
94 // scenarios, e.g. deletion, recreation.
95 $this->addFieldsIf( 'log_page', $this->fld_ids );
96 $this->addFieldsIf( $actorQuery['fields'] + [ 'user_name' ], $this->fld_user );
97 $this->addFieldsIf( $actorQuery['fields'], $this->fld_userid );
98 $this->addFieldsIf(
99 [ 'log_namespace', 'log_title' ],
100 $this->fld_title || $this->fld_parsedcomment
101 );
102 $this->addFieldsIf( 'log_params', $this->fld_details );
103
104 if ( $this->fld_comment || $this->fld_parsedcomment ) {
105 $commentQuery = $this->commentStore->getJoin( 'log_comment' );
106 $this->addTables( $commentQuery['tables'] );
107 $this->addFields( $commentQuery['fields'] );
108 $this->addJoinConds( $commentQuery['joins'] );
109 }
110
111 if ( $this->fld_tags ) {
112 $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'logging' ) ] );
113 }
114
115 if ( $params['tag'] !== null ) {
116 $this->addTables( 'change_tag' );
117 $this->addJoinConds( [ 'change_tag' => [ 'JOIN',
118 [ 'log_id=ct_log_id' ] ] ] );
119 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
120 try {
121 $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) );
122 } catch ( NameTableAccessException $exception ) {
123 // Return nothing.
124 $this->addWhere( '1=0' );
125 }
126 }
127
128 if ( $params['action'] !== null ) {
129 // Do validation of action param, list of allowed actions can contains wildcards
130 // Allow the param, when the actions is in the list or a wildcard version is listed.
131 $logAction = $params['action'];
132 if ( strpos( $logAction, '/' ) === false ) {
133 // all items in the list have a slash
134 $valid = false;
135 } else {
136 $logActions = array_flip( $this->getAllowedLogActions() );
137 list( $type, $action ) = explode( '/', $logAction, 2 );
138 $valid = isset( $logActions[$logAction] ) || isset( $logActions[$type . '/*'] );
139 }
140
141 if ( !$valid ) {
142 $encParamName = $this->encodeParamName( 'action' );
143 $this->dieWithError(
144 [ 'apierror-unrecognizedvalue', $encParamName, wfEscapeWikiText( $logAction ) ],
145 "unknown_$encParamName"
146 );
147 }
148
149 $this->addWhereFld( 'log_type', $type );
150 $this->addWhereFld( 'log_action', $action );
151 } elseif ( $params['type'] !== null ) {
152 $this->addWhereFld( 'log_type', $params['type'] );
153 }
154
156 'log_timestamp',
157 $params['dir'],
158 $params['start'],
159 $params['end']
160 );
161 // Include in ORDER BY for uniqueness
162 $this->addWhereRange( 'log_id', $params['dir'], null, null );
163
164 if ( $params['continue'] !== null ) {
165 $cont = explode( '|', $params['continue'] );
166 $this->dieContinueUsageIf( count( $cont ) != 2 );
167 $op = ( $params['dir'] === 'newer' ? '>' : '<' );
168 $continueTimestamp = $db->addQuotes( $db->timestamp( $cont[0] ) );
169 $continueId = (int)$cont[1];
170 $this->dieContinueUsageIf( $continueId != $cont[1] );
171 $this->addWhere( "log_timestamp $op $continueTimestamp OR " .
172 "(log_timestamp = $continueTimestamp AND " .
173 "log_id $op= $continueId)"
174 );
175 }
176
177 $limit = $params['limit'];
178 $this->addOption( 'LIMIT', $limit + 1 );
179
180 $user = $params['user'];
181 if ( $user !== null ) {
182 // Note the joins in $q are the same as those from ->getJoin() above
183 // so we only need to add 'conds' here.
184 $q = $actorMigration->getWhere( $db, 'log_user', $params['user'] );
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 ( $title !== null ) {
195 $titleObj = Title::newFromText( $title );
196 if ( $titleObj === null ) {
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 ( $prefix !== null ) {
210 if ( $this->getConfig()->get( 'MiserMode' ) ) {
211 $this->dieWithError( 'apierror-prefixsearchdisabled' );
212 }
213
214 $title = Title::newFromText( $prefix );
215 if ( $title === null ) {
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 || $title !== null || $user !== null ) {
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 || $title !== null ) && $titleBits ) {
237 $this->addWhere( $db->bitAnd( 'log_deleted', $titleBits ) . " != $titleBits" );
238 }
239 if ( $user !== null && $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 $this->addOption(
253 'MAX_EXECUTION_TIME',
254 $this->getConfig()->get( 'MaxExecutionTimeForExpensiveQueries' )
255 );
256
257 $count = 0;
258 $res = $this->select( __METHOD__ );
259
260 if ( $this->fld_title ) {
261 $this->executeGenderCacheFromResultWrapper( $res, __METHOD__, 'log' );
262 }
263
264 $result = $this->getResult();
265 foreach ( $res as $row ) {
266 if ( ++$count > $limit ) {
267 // We've reached the one extra which shows that there are
268 // additional pages to be had. Stop here...
269 $this->setContinueEnumParameter( 'continue', "$row->log_timestamp|$row->log_id" );
270 break;
271 }
272
273 $vals = $this->extractRowInfo( $row );
274 $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
275 if ( !$fit ) {
276 $this->setContinueEnumParameter( 'continue', "$row->log_timestamp|$row->log_id" );
277 break;
278 }
279 }
280 $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'item' );
281 }
282
283 private function extractRowInfo( $row ) {
284 $logEntry = DatabaseLogEntry::newFromRow( $row );
285 $vals = [
286 ApiResult::META_TYPE => 'assoc',
287 ];
288 $anyHidden = false;
289 $user = $this->getUser();
290
291 if ( $this->fld_ids ) {
292 $vals['logid'] = (int)$row->log_id;
293 }
294
295 if ( $this->fld_title || $this->fld_parsedcomment ) {
296 $title = Title::makeTitle( $row->log_namespace, $row->log_title );
297 }
298
299 if ( $this->fld_title || $this->fld_ids || $this->fld_details && $row->log_params !== '' ) {
300 if ( LogEventsList::isDeleted( $row, LogPage::DELETED_ACTION ) ) {
301 $vals['actionhidden'] = true;
302 $anyHidden = true;
303 }
304 if ( LogEventsList::userCan( $row, LogPage::DELETED_ACTION, $user ) ) {
305 if ( $this->fld_title ) {
307 }
308 if ( $this->fld_ids ) {
309 $vals['pageid'] = (int)$row->page_id;
310 $vals['logpage'] = (int)$row->log_page;
311 }
312 if ( $this->fld_details ) {
313 $vals['params'] = LogFormatter::newFromEntry( $logEntry )->formatParametersForApi();
314 }
315 }
316 }
317
318 if ( $this->fld_type ) {
319 $vals['type'] = $row->log_type;
320 $vals['action'] = $row->log_action;
321 }
322
323 if ( $this->fld_user || $this->fld_userid ) {
324 if ( LogEventsList::isDeleted( $row, LogPage::DELETED_USER ) ) {
325 $vals['userhidden'] = true;
326 $anyHidden = true;
327 }
328 if ( LogEventsList::userCan( $row, LogPage::DELETED_USER, $user ) ) {
329 if ( $this->fld_user ) {
330 $vals['user'] = $row->user_name ?? $row->log_user_text;
331 }
332 if ( $this->fld_userid ) {
333 $vals['userid'] = (int)$row->log_user;
334 }
335
336 if ( !$row->log_user ) {
337 $vals['anon'] = true;
338 }
339 }
340 }
341 if ( $this->fld_timestamp ) {
342 $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->log_timestamp );
343 }
344
345 if ( $this->fld_comment || $this->fld_parsedcomment ) {
346 if ( LogEventsList::isDeleted( $row, LogPage::DELETED_COMMENT ) ) {
347 $vals['commenthidden'] = true;
348 $anyHidden = true;
349 }
350 if ( LogEventsList::userCan( $row, LogPage::DELETED_COMMENT, $user ) ) {
351 $comment = $this->commentStore->getComment( 'log_comment', $row )->text;
352 if ( $this->fld_comment ) {
353 $vals['comment'] = $comment;
354 }
355
356 if ( $this->fld_parsedcomment ) {
357 $vals['parsedcomment'] = Linker::formatComment( $comment, $title );
358 }
359 }
360 }
361
362 if ( $this->fld_tags ) {
363 if ( $row->ts_tags ) {
364 $tags = explode( ',', $row->ts_tags );
365 ApiResult::setIndexedTagName( $tags, 'tag' );
366 $vals['tags'] = $tags;
367 } else {
368 $vals['tags'] = [];
369 }
370 }
371
372 if ( $anyHidden && LogEventsList::isDeleted( $row, LogPage::DELETED_RESTRICTED ) ) {
373 $vals['suppressed'] = true;
374 }
375
376 return $vals;
377 }
378
382 private function getAllowedLogActions() {
383 $config = $this->getConfig();
384 return array_keys( array_merge(
385 $config->get( 'LogActions' ),
386 $config->get( 'LogActionsHandlers' )
387 ) );
388 }
389
390 public function getCacheMode( $params ) {
391 if ( $this->userCanSeeRevDel() ) {
392 return 'private';
393 }
394 if ( $params['prop'] !== null && in_array( 'parsedcomment', $params['prop'] ) ) {
395 // formatComment() calls wfMessage() among other things
396 return 'anon-public-user-private';
397 } elseif ( LogEventsList::getExcludeClause( $this->getDB(), 'user', $this->getUser() )
398 === LogEventsList::getExcludeClause( $this->getDB(), 'public' )
399 ) { // Output can only contain public data.
400 return 'public';
401 } else {
402 return 'anon-public-user-private';
403 }
404 }
405
406 public function getAllowedParams( $flags = 0 ) {
407 $config = $this->getConfig();
408 if ( $flags & ApiBase::GET_VALUES_FOR_HELP ) {
409 $logActions = $this->getAllowedLogActions();
410 sort( $logActions );
411 } else {
412 $logActions = null;
413 }
414 $ret = [
415 'prop' => [
417 ApiBase::PARAM_DFLT => 'ids|title|type|user|timestamp|comment|details',
419 'ids',
420 'title',
421 'type',
422 'user',
423 'userid',
424 'timestamp',
425 'comment',
426 'parsedcomment',
427 'details',
428 'tags'
429 ],
431 ],
432 'type' => [
434 ],
435 'action' => [
436 // validation on request is done in execute()
437 ApiBase::PARAM_TYPE => $logActions
438 ],
439 'start' => [
440 ApiBase::PARAM_TYPE => 'timestamp'
441 ],
442 'end' => [
443 ApiBase::PARAM_TYPE => 'timestamp'
444 ],
445 'dir' => [
446 ApiBase::PARAM_DFLT => 'older',
448 'newer',
449 'older'
450 ],
451 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
452 ],
453 'user' => [
454 ApiBase::PARAM_TYPE => 'user',
455 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
456 UserDef::PARAM_RETURN_OBJECT => true,
457 ],
458 'title' => null,
459 'namespace' => [
460 ApiBase::PARAM_TYPE => 'namespace',
462 ],
463 'prefix' => [],
464 'tag' => null,
465 'limit' => [
467 ApiBase::PARAM_TYPE => 'limit',
471 ],
472 'continue' => [
473 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
474 ],
475 ];
476
477 if ( $config->get( 'MiserMode' ) ) {
478 $ret['prefix'][ApiBase::PARAM_HELP_MSG] = 'api-help-param-disabled-in-miser-mode';
479 }
480
481 return $ret;
482 }
483
484 protected function getExamplesMessages() {
485 return [
486 'action=query&list=logevents'
487 => 'apihelp-query+logevents-example-simple',
488 ];
489 }
490
491 public function getHelpUrls() {
492 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Logevents';
493 }
494}
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:1437
const PARAM_MAX2
Definition ApiBase.php:86
encodeParamName( $paramName)
This method mangles parameter name based on the prefix supplied to the constructor.
Definition ApiBase.php:750
const PARAM_MAX
Definition ApiBase.php:82
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition ApiBase.php:1617
const PARAM_TYPE
Definition ApiBase.php:78
const PARAM_DFLT
Definition ApiBase.php:70
getPermissionManager()
Obtain a PermissionManager instance that subclasses may use in their authorization checks.
Definition ApiBase.php:692
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:195
const PARAM_MIN
Definition ApiBase.php:90
const LIMIT_BIG1
Fast query, standard limit.
Definition ApiBase.php:220
requireMaxOneParameter( $params,... $required)
Die if more than one of a certain set of parameters is set and not false.
Definition ApiBase.php:944
getResult()
Get the result object.
Definition ApiBase.php:620
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:772
const PARAM_EXTRA_NAMESPACES
Definition ApiBase.php:118
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:162
const GET_VALUES_FOR_HELP
getAllowedParams() flag: When set, the result could take longer to generate, but should be more thoro...
Definition ApiBase.php:233
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition ApiBase.php:222
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:499
const PARAM_ISMULTI
Definition ApiBase.php:74
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) Stable to override.
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)
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:37
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
CommentStore handles storage of comments (edit summaries, log reasons, etc) in the database.
getUser()
Stable to override.
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:1209
static newFromEntry(LogEntry $entry)
Constructs a new formatter suitable for given entry.
const DELETED_USER
Definition LogPage.php:40
const DELETED_RESTRICTED
Definition LogPage.php:41
const DELETED_COMMENT
Definition LogPage.php:39
static validTypes()
Get the list of valid log types.
Definition LogPage.php:203
const DELETED_ACTION
Definition LogPage.php:38
MediaWikiServices is the service locator for the application scope of MediaWiki.
Type definition for user types.
Definition UserDef.php:23
Exception representing a failure to look up a row from a name table.
const NS_SPECIAL
Definition Defines.php:59
const NS_MEDIA
Definition Defines.php:58