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