MediaWiki REL1_39
ApiQueryLogEvents.php
Go to the documentation of this file.
1<?php
32
39
41 private $commentStore;
42
44 private $commentFormatter;
45
47 private $changeTagDefStore;
48
50 private $formattedComments;
51
59 public function __construct(
60 ApiQuery $query,
61 $moduleName,
62 CommentStore $commentStore,
63 RowCommentFormatter $commentFormatter,
64 NameTableStore $changeTagDefStore
65 ) {
66 parent::__construct( $query, $moduleName, 'le' );
67 $this->commentStore = $commentStore;
68 $this->commentFormatter = $commentFormatter;
69 $this->changeTagDefStore = $changeTagDefStore;
70 }
71
72 private $fld_ids = false, $fld_title = false, $fld_type = false,
73 $fld_user = false, $fld_userid = false,
74 $fld_timestamp = false, $fld_comment = false, $fld_parsedcomment = false,
75 $fld_details = false, $fld_tags = false;
76
77 public function execute() {
78 $params = $this->extractRequestParams();
79 $db = $this->getDB();
80 $this->requireMaxOneParameter( $params, 'title', 'prefix', 'namespace' );
81
82 $prop = array_fill_keys( $params['prop'], true );
83
84 $this->fld_ids = isset( $prop['ids'] );
85 $this->fld_title = isset( $prop['title'] );
86 $this->fld_type = isset( $prop['type'] );
87 $this->fld_user = isset( $prop['user'] );
88 $this->fld_userid = isset( $prop['userid'] );
89 $this->fld_timestamp = isset( $prop['timestamp'] );
90 $this->fld_comment = isset( $prop['comment'] );
91 $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
92 $this->fld_details = isset( $prop['details'] );
93 $this->fld_tags = isset( $prop['tags'] );
94
95 $hideLogs = LogEventsList::getExcludeClause( $db, 'user', $this->getAuthority() );
96 if ( $hideLogs !== false ) {
97 $this->addWhere( $hideLogs );
98 }
99
100 $this->addTables( [ 'logging', 'actor' ] );
101 $this->addJoinConds( [
102 'actor' => [ 'JOIN', 'actor_id=log_actor' ],
103 ] );
104
105 $this->addFields( [
106 'log_id',
107 'log_type',
108 'log_action',
109 'log_timestamp',
110 'log_deleted',
111 ] );
112
113 if ( $this->fld_ids ) {
114 $this->addTables( 'page' );
115 $this->addJoinConds( [
116 'page' => [ 'LEFT JOIN',
117 [ 'log_namespace=page_namespace',
118 'log_title=page_title' ] ]
119 ] );
120 // log_page is the page_id saved at log time, whereas page_id is from a
121 // join at query time. This leads to different results in various
122 // scenarios, e.g. deletion, recreation.
123 $this->addFields( [ 'page_id', 'log_page' ] );
124 }
125 $this->addFieldsIf( [ 'actor_name', 'actor_user' ], $this->fld_user );
126 $this->addFieldsIf( 'actor_user', $this->fld_userid );
127 $this->addFieldsIf(
128 [ 'log_namespace', 'log_title' ],
129 $this->fld_title || $this->fld_parsedcomment
130 );
131 $this->addFieldsIf( 'log_params', $this->fld_details );
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( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'logging' ) ] );
142 }
143
144 if ( $params['tag'] !== null ) {
145 $this->addTables( 'change_tag' );
146 $this->addJoinConds( [ 'change_tag' => [ 'JOIN',
147 [ 'log_id=ct_log_id' ] ] ] );
148 try {
149 $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $params['tag'] ) );
150 } catch ( NameTableAccessException $exception ) {
151 // Return nothing.
152 $this->addWhere( '1=0' );
153 }
154 }
155
156 if ( $params['action'] !== null ) {
157 // Do validation of action param, list of allowed actions can contains wildcards
158 // Allow the param, when the actions is in the list or a wildcard version is listed.
159 $logAction = $params['action'];
160 if ( strpos( $logAction, '/' ) === false ) {
161 // all items in the list have a slash
162 $valid = false;
163 } else {
164 $logActions = array_fill_keys( $this->getAllowedLogActions(), true );
165 list( $type, $action ) = explode( '/', $logAction, 2 );
166 $valid = isset( $logActions[$logAction] ) || isset( $logActions[$type . '/*'] );
167 }
168
169 if ( !$valid ) {
170 $encParamName = $this->encodeParamName( 'action' );
171 $this->dieWithError(
172 [ 'apierror-unrecognizedvalue', $encParamName, wfEscapeWikiText( $logAction ) ],
173 "unknown_$encParamName"
174 );
175 }
176
177 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable,PhanPossiblyUndeclaredVariable T240141
178 $this->addWhereFld( 'log_type', $type );
179 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable,PhanPossiblyUndeclaredVariable T240141
180 $this->addWhereFld( 'log_action', $action );
181 } elseif ( $params['type'] !== null ) {
182 $this->addWhereFld( 'log_type', $params['type'] );
183 }
184
186 'log_timestamp',
187 $params['dir'],
188 $params['start'],
189 $params['end']
190 );
191 // Include in ORDER BY for uniqueness
192 $this->addWhereRange( 'log_id', $params['dir'], null, null );
193
194 if ( $params['continue'] !== null ) {
195 $cont = explode( '|', $params['continue'] );
196 $this->dieContinueUsageIf( count( $cont ) != 2 );
197 $op = ( $params['dir'] === 'newer' ? '>' : '<' );
198 $continueTimestamp = $db->addQuotes( $db->timestamp( $cont[0] ) );
199 $continueId = (int)$cont[1];
200 $this->dieContinueUsageIf( $continueId != $cont[1] );
201 $this->addWhere( "log_timestamp $op $continueTimestamp OR " .
202 "(log_timestamp = $continueTimestamp AND " .
203 "log_id $op= $continueId)"
204 );
205 }
206
207 $limit = $params['limit'];
208 $this->addOption( 'LIMIT', $limit + 1 );
209
210 $user = $params['user'];
211 if ( $user !== null ) {
212 $this->addWhereFld( 'actor_name', $user );
213 }
214
215 $title = $params['title'];
216 if ( $title !== null ) {
217 $titleObj = Title::newFromText( $title );
218 if ( $titleObj === null || $titleObj->isExternal() ) {
219 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] );
220 }
221 $this->addWhereFld( 'log_namespace', $titleObj->getNamespace() );
222 $this->addWhereFld( 'log_title', $titleObj->getDBkey() );
223 }
224
225 if ( $params['namespace'] !== null ) {
226 $this->addWhereFld( 'log_namespace', $params['namespace'] );
227 }
228
229 $prefix = $params['prefix'];
230
231 if ( $prefix !== null ) {
232 if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
233 $this->dieWithError( 'apierror-prefixsearchdisabled' );
234 }
235
236 $title = Title::newFromText( $prefix );
237 if ( $title === null || $title->isExternal() ) {
238 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $prefix ) ] );
239 }
240 $this->addWhereFld( 'log_namespace', $title->getNamespace() );
241 $this->addWhere( 'log_title ' . $db->buildLike( $title->getDBkey(), $db->anyString() ) );
242 }
243
244 // Paranoia: avoid brute force searches (T19342)
245 if ( $params['namespace'] !== null || $title !== null || $user !== null ) {
246 if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
247 $titleBits = LogPage::DELETED_ACTION;
248 $userBits = LogPage::DELETED_USER;
249 } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
252 } else {
253 $titleBits = 0;
254 $userBits = 0;
255 }
256 if ( ( $params['namespace'] !== null || $title !== null ) && $titleBits ) {
257 $this->addWhere( $db->bitAnd( 'log_deleted', $titleBits ) . " != $titleBits" );
258 }
259 if ( $user !== null && $userBits ) {
260 $this->addWhere( $db->bitAnd( 'log_deleted', $userBits ) . " != $userBits" );
261 }
262 }
263
264 // T220999: MySQL/MariaDB (10.1.37) can sometimes irrationally decide that querying `actor` before
265 // `logging` and filesorting is somehow better than querying $limit+1 rows from `logging`.
266 // Tell it not to reorder the query. But not when `letag` was used, as it seems as likely
267 // to be harmed as helped in that case.
268 // If "user" was specified, it's obviously correct to query actor first (T282122)
269 if ( $params['tag'] === null && $user === null ) {
270 $this->addOption( 'STRAIGHT_JOIN' );
271 }
272
273 $this->addOption(
274 'MAX_EXECUTION_TIME',
275 $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries )
276 );
277
278 $count = 0;
279 $res = $this->select( __METHOD__ );
280
281 if ( $this->fld_title ) {
282 $this->executeGenderCacheFromResultWrapper( $res, __METHOD__, 'log' );
283 }
284 if ( $this->fld_parsedcomment ) {
285 $this->formattedComments = $this->commentFormatter->formatItems(
286 $this->commentFormatter->rows( $res )
287 ->commentKey( 'log_comment' )
288 ->indexField( 'log_id' )
289 ->namespaceField( 'log_namespace' )
290 ->titleField( 'log_title' )
291 );
292 }
293
294 $result = $this->getResult();
295 foreach ( $res as $row ) {
296 if ( ++$count > $limit ) {
297 // We've reached the one extra which shows that there are
298 // additional pages to be had. Stop here...
299 $this->setContinueEnumParameter( 'continue', "$row->log_timestamp|$row->log_id" );
300 break;
301 }
302
303 $vals = $this->extractRowInfo( $row );
304 $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
305 if ( !$fit ) {
306 $this->setContinueEnumParameter( 'continue', "$row->log_timestamp|$row->log_id" );
307 break;
308 }
309 }
310 $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'item' );
311 }
312
313 private function extractRowInfo( $row ) {
314 $logEntry = DatabaseLogEntry::newFromRow( $row );
315 $vals = [
316 ApiResult::META_TYPE => 'assoc',
317 ];
318 $anyHidden = false;
319
320 if ( $this->fld_ids ) {
321 $vals['logid'] = (int)$row->log_id;
322 }
323
324 if ( $this->fld_title ) {
325 $title = Title::makeTitle( $row->log_namespace, $row->log_title );
326 }
327
328 $authority = $this->getAuthority();
329 if ( $this->fld_title || $this->fld_ids || $this->fld_details && $row->log_params !== '' ) {
330 if ( LogEventsList::isDeleted( $row, LogPage::DELETED_ACTION ) ) {
331 $vals['actionhidden'] = true;
332 $anyHidden = true;
333 }
334 if ( LogEventsList::userCan( $row, LogPage::DELETED_ACTION, $authority ) ) {
335 if ( $this->fld_title ) {
336 // @phan-suppress-next-next-line PhanTypeMismatchArgumentNullable,PhanPossiblyUndeclaredVariable
337 // title is set when used
339 }
340 if ( $this->fld_ids ) {
341 $vals['pageid'] = (int)$row->page_id;
342 $vals['logpage'] = (int)$row->log_page;
343 }
344 if ( $this->fld_details ) {
345 $vals['params'] = LogFormatter::newFromEntry( $logEntry )->formatParametersForApi();
346 }
347 }
348 }
349
350 if ( $this->fld_type ) {
351 $vals['type'] = $row->log_type;
352 $vals['action'] = $row->log_action;
353 }
354
355 if ( $this->fld_user || $this->fld_userid ) {
356 if ( LogEventsList::isDeleted( $row, LogPage::DELETED_USER ) ) {
357 $vals['userhidden'] = true;
358 $anyHidden = true;
359 }
360 if ( LogEventsList::userCan( $row, LogPage::DELETED_USER, $authority ) ) {
361 if ( $this->fld_user ) {
362 $vals['user'] = $row->actor_name;
363 }
364 if ( $this->fld_userid ) {
365 $vals['userid'] = (int)$row->actor_user;
366 }
367
368 if ( !$row->actor_user ) {
369 $vals['anon'] = true;
370 }
371 }
372 }
373 if ( $this->fld_timestamp ) {
374 $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->log_timestamp );
375 }
376
377 if ( $this->fld_comment || $this->fld_parsedcomment ) {
378 if ( LogEventsList::isDeleted( $row, LogPage::DELETED_COMMENT ) ) {
379 $vals['commenthidden'] = true;
380 $anyHidden = true;
381 }
382 if ( LogEventsList::userCan( $row, LogPage::DELETED_COMMENT, $authority ) ) {
383 if ( $this->fld_comment ) {
384 $vals['comment'] = $this->commentStore->getComment( 'log_comment', $row )->text;
385 }
386
387 if ( $this->fld_parsedcomment ) {
388 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
389 $vals['parsedcomment'] = $this->formattedComments[$row->log_id];
390 }
391 }
392 }
393
394 if ( $this->fld_tags ) {
395 if ( $row->ts_tags ) {
396 $tags = explode( ',', $row->ts_tags );
397 ApiResult::setIndexedTagName( $tags, 'tag' );
398 $vals['tags'] = $tags;
399 } else {
400 $vals['tags'] = [];
401 }
402 }
403
404 if ( $anyHidden && LogEventsList::isDeleted( $row, LogPage::DELETED_RESTRICTED ) ) {
405 $vals['suppressed'] = true;
406 }
407
408 return $vals;
409 }
410
414 private function getAllowedLogActions() {
415 $config = $this->getConfig();
416 return array_keys( array_merge(
417 $config->get( MainConfigNames::LogActions ),
418 $config->get( MainConfigNames::LogActionsHandlers )
419 ) );
420 }
421
422 public function getCacheMode( $params ) {
423 if ( $this->userCanSeeRevDel() ) {
424 return 'private';
425 }
426 if ( $params['prop'] !== null && in_array( 'parsedcomment', $params['prop'] ) ) {
427 // formatComment() calls wfMessage() among other things
428 return 'anon-public-user-private';
429 } elseif ( LogEventsList::getExcludeClause( $this->getDB(), 'user', $this->getAuthority() )
430 === LogEventsList::getExcludeClause( $this->getDB(), 'public' )
431 ) { // Output can only contain public data.
432 return 'public';
433 } else {
434 return 'anon-public-user-private';
435 }
436 }
437
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 'user' => [
490 ParamValidator::PARAM_TYPE => 'user',
491 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
492 ],
493 'title' => null,
494 'namespace' => [
495 ParamValidator::PARAM_TYPE => 'namespace',
496 NamespaceDef::PARAM_EXTRA_NAMESPACES => [ NS_MEDIA, NS_SPECIAL ],
497 ],
498 'prefix' => [],
499 'tag' => null,
500 'limit' => [
501 ParamValidator::PARAM_DEFAULT => 10,
502 ParamValidator::PARAM_TYPE => 'limit',
503 IntegerDef::PARAM_MIN => 1,
504 IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
505 IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
506 ],
507 'continue' => [
508 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
509 ],
510 ];
511
512 if ( $config->get( MainConfigNames::MiserMode ) ) {
513 $ret['prefix'][ApiBase::PARAM_HELP_MSG] = 'api-help-param-disabled-in-miser-mode';
514 }
515
516 return $ret;
517 }
518
519 protected function getExamplesMessages() {
520 return [
521 'action=query&list=logevents'
522 => 'apihelp-query+logevents-example-simple',
523 ];
524 }
525
526 public function getHelpUrls() {
527 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Logevents';
528 }
529}
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:1454
encodeParamName( $paramName)
This method mangles parameter name based on the prefix supplied to the constructor.
Definition ApiBase.php:743
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition ApiBase.php:1643
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:196
const LIMIT_BIG1
Fast query, standard limit.
Definition ApiBase.php:221
requireMaxOneParameter( $params,... $required)
Die if more than one of a certain set of parameters is set and not false.
Definition ApiBase.php:938
getResult()
Get the result object.
Definition ApiBase.php:629
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:765
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:163
const GET_VALUES_FOR_HELP
getAllowedParams() flag: When set, the result could take longer to generate, but should be more thoro...
Definition ApiBase.php:234
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition ApiBase.php:223
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:498
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:41
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.
Handle database storage of comments such as edit summaries and log reasons.
static newFromEntry(LogEntry $entry)
Constructs a new formatter suitable for given entry.
const DELETED_USER
Definition LogPage.php:42
const DELETED_RESTRICTED
Definition LogPage.php:43
const DELETED_COMMENT
Definition LogPage.php:41
static validTypes()
Get the list of valid log types.
Definition LogPage.php:207
const DELETED_ACTION
Definition LogPage.php:40
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 ...
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.
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition Title.php:638
Service for formatting and validating API parameters.
Type definition for integer types.