MediaWiki REL1_37
ApiQueryLogEvents.php
Go to the documentation of this file.
1<?php
26
33
36
39
46 public function __construct(
47 ApiQuery $query,
48 $moduleName,
51 ) {
52 parent::__construct( $query, $moduleName, 'le' );
53 $this->commentStore = $commentStore;
54 $this->changeTagDefStore = $changeTagDefStore;
55 }
56
57 private $fld_ids = false, $fld_title = false, $fld_type = false,
58 $fld_user = false, $fld_userid = false,
60 $fld_details = false, $fld_tags = false;
61
62 public function execute() {
63 $params = $this->extractRequestParams();
64 $db = $this->getDB();
65 $this->requireMaxOneParameter( $params, 'title', 'prefix', 'namespace' );
66
67 $prop = array_fill_keys( $params['prop'], true );
68
69 $this->fld_ids = isset( $prop['ids'] );
70 $this->fld_title = isset( $prop['title'] );
71 $this->fld_type = isset( $prop['type'] );
72 $this->fld_user = isset( $prop['user'] );
73 $this->fld_userid = isset( $prop['userid'] );
74 $this->fld_timestamp = isset( $prop['timestamp'] );
75 $this->fld_comment = isset( $prop['comment'] );
76 $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
77 $this->fld_details = isset( $prop['details'] );
78 $this->fld_tags = isset( $prop['tags'] );
79
80 $hideLogs = LogEventsList::getExcludeClause( $db, 'user', $this->getUser() );
81 if ( $hideLogs !== false ) {
82 $this->addWhere( $hideLogs );
83 }
84
85 $this->addTables( [ 'logging', 'actor', 'page' ] );
86 $this->addJoinConds( [
87 'actor' => [ 'JOIN', 'actor_id=log_actor' ],
88 'page' => [ 'LEFT JOIN',
89 [ 'log_namespace=page_namespace',
90 'log_title=page_title' ] ] ] );
91
92 $this->addFields( [
93 'log_id',
94 'log_type',
95 'log_action',
96 'log_timestamp',
97 'log_deleted',
98 ] );
99
100 $this->addFieldsIf( 'page_id', $this->fld_ids );
101 // log_page is the page_id saved at log time, whereas page_id is from a
102 // join at query time. This leads to different results in various
103 // scenarios, e.g. deletion, recreation.
104 $this->addFieldsIf( 'log_page', $this->fld_ids );
105 $this->addFieldsIf( [ 'actor_name', 'actor_user' ], $this->fld_user );
106 $this->addFieldsIf( 'actor_user', $this->fld_userid );
107 $this->addFieldsIf(
108 [ 'log_namespace', 'log_title' ],
109 $this->fld_title || $this->fld_parsedcomment
110 );
111 $this->addFieldsIf( 'log_params', $this->fld_details );
112
113 if ( $this->fld_comment || $this->fld_parsedcomment ) {
114 $commentQuery = $this->commentStore->getJoin( 'log_comment' );
115 $this->addTables( $commentQuery['tables'] );
116 $this->addFields( $commentQuery['fields'] );
117 $this->addJoinConds( $commentQuery['joins'] );
118 }
119
120 if ( $this->fld_tags ) {
121 $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'logging' ) ] );
122 }
123
124 if ( $params['tag'] !== null ) {
125 $this->addTables( 'change_tag' );
126 $this->addJoinConds( [ 'change_tag' => [ 'JOIN',
127 [ 'log_id=ct_log_id' ] ] ] );
128 try {
129 $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $params['tag'] ) );
130 } catch ( NameTableAccessException $exception ) {
131 // Return nothing.
132 $this->addWhere( '1=0' );
133 }
134 }
135
136 if ( $params['action'] !== null ) {
137 // Do validation of action param, list of allowed actions can contains wildcards
138 // Allow the param, when the actions is in the list or a wildcard version is listed.
139 $logAction = $params['action'];
140 if ( strpos( $logAction, '/' ) === false ) {
141 // all items in the list have a slash
142 $valid = false;
143 } else {
144 $logActions = array_fill_keys( $this->getAllowedLogActions(), true );
145 list( $type, $action ) = explode( '/', $logAction, 2 );
146 $valid = isset( $logActions[$logAction] ) || isset( $logActions[$type . '/*'] );
147 }
148
149 if ( !$valid ) {
150 $encParamName = $this->encodeParamName( 'action' );
151 $this->dieWithError(
152 [ 'apierror-unrecognizedvalue', $encParamName, wfEscapeWikiText( $logAction ) ],
153 "unknown_$encParamName"
154 );
155 }
156
157 $this->addWhereFld( 'log_type', $type );
158 $this->addWhereFld( 'log_action', $action );
159 } elseif ( $params['type'] !== null ) {
160 $this->addWhereFld( 'log_type', $params['type'] );
161 }
162
164 'log_timestamp',
165 $params['dir'],
166 $params['start'],
167 $params['end']
168 );
169 // Include in ORDER BY for uniqueness
170 $this->addWhereRange( 'log_id', $params['dir'], null, null );
171
172 if ( $params['continue'] !== null ) {
173 $cont = explode( '|', $params['continue'] );
174 $this->dieContinueUsageIf( count( $cont ) != 2 );
175 $op = ( $params['dir'] === 'newer' ? '>' : '<' );
176 $continueTimestamp = $db->addQuotes( $db->timestamp( $cont[0] ) );
177 $continueId = (int)$cont[1];
178 $this->dieContinueUsageIf( $continueId != $cont[1] );
179 $this->addWhere( "log_timestamp $op $continueTimestamp OR " .
180 "(log_timestamp = $continueTimestamp AND " .
181 "log_id $op= $continueId)"
182 );
183 }
184
185 $limit = $params['limit'];
186 $this->addOption( 'LIMIT', $limit + 1 );
187
188 $user = $params['user'];
189 if ( $user !== null ) {
190 $this->addWhereFld( 'actor_name', $user );
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->getAuthority()->isAllowed( 'deletedhistory' ) ) {
225 $titleBits = LogPage::DELETED_ACTION;
226 $userBits = LogPage::DELETED_USER;
227 } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
230 } else {
231 $titleBits = 0;
232 $userBits = 0;
233 }
234 if ( ( $params['namespace'] !== null || $title !== null ) && $titleBits ) {
235 $this->addWhere( $db->bitAnd( 'log_deleted', $titleBits ) . " != $titleBits" );
236 }
237 if ( $user !== null && $userBits ) {
238 $this->addWhere( $db->bitAnd( 'log_deleted', $userBits ) . " != $userBits" );
239 }
240 }
241
242 // T220999: MySQL/MariaDB (10.1.37) can sometimes irrationally decide that querying `actor` before
243 // `logging` and filesorting is somehow better than querying $limit+1 rows from `logging`.
244 // Tell it not to reorder the query. But not when `letag` was used, as it seems as likely
245 // to be harmed as helped in that case.
246 // If "user" was specified, it's obviously correct to query actor first (T282122)
247 if ( $params['tag'] === null && $user === null ) {
248 $this->addOption( 'STRAIGHT_JOIN' );
249 }
250
251 $this->addOption(
252 'MAX_EXECUTION_TIME',
253 $this->getConfig()->get( 'MaxExecutionTimeForExpensiveQueries' )
254 );
255
256 $count = 0;
257 $res = $this->select( __METHOD__ );
258
259 if ( $this->fld_title ) {
260 $this->executeGenderCacheFromResultWrapper( $res, __METHOD__, 'log' );
261 }
262
263 $result = $this->getResult();
264 foreach ( $res as $row ) {
265 if ( ++$count > $limit ) {
266 // We've reached the one extra which shows that there are
267 // additional pages to be had. Stop here...
268 $this->setContinueEnumParameter( 'continue', "$row->log_timestamp|$row->log_id" );
269 break;
270 }
271
272 $vals = $this->extractRowInfo( $row );
273 $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
274 if ( !$fit ) {
275 $this->setContinueEnumParameter( 'continue', "$row->log_timestamp|$row->log_id" );
276 break;
277 }
278 }
279 $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'item' );
280 }
281
282 private function extractRowInfo( $row ) {
283 $logEntry = DatabaseLogEntry::newFromRow( $row );
284 $vals = [
285 ApiResult::META_TYPE => 'assoc',
286 ];
287 $anyHidden = false;
288 $user = $this->getUser();
289
290 if ( $this->fld_ids ) {
291 $vals['logid'] = (int)$row->log_id;
292 }
293
294 if ( $this->fld_title || $this->fld_parsedcomment ) {
295 $title = Title::makeTitle( $row->log_namespace, $row->log_title );
296 }
297
298 if ( $this->fld_title || $this->fld_ids || $this->fld_details && $row->log_params !== '' ) {
299 if ( LogEventsList::isDeleted( $row, LogPage::DELETED_ACTION ) ) {
300 $vals['actionhidden'] = true;
301 $anyHidden = true;
302 }
303 if ( LogEventsList::userCan( $row, LogPage::DELETED_ACTION, $user ) ) {
304 if ( $this->fld_title ) {
306 }
307 if ( $this->fld_ids ) {
308 $vals['pageid'] = (int)$row->page_id;
309 $vals['logpage'] = (int)$row->log_page;
310 }
311 if ( $this->fld_details ) {
312 $vals['params'] = LogFormatter::newFromEntry( $logEntry )->formatParametersForApi();
313 }
314 }
315 }
316
317 if ( $this->fld_type ) {
318 $vals['type'] = $row->log_type;
319 $vals['action'] = $row->log_action;
320 }
321
322 if ( $this->fld_user || $this->fld_userid ) {
323 if ( LogEventsList::isDeleted( $row, LogPage::DELETED_USER ) ) {
324 $vals['userhidden'] = true;
325 $anyHidden = true;
326 }
327 if ( LogEventsList::userCan( $row, LogPage::DELETED_USER, $user ) ) {
328 if ( $this->fld_user ) {
329 $vals['user'] = $row->actor_name;
330 }
331 if ( $this->fld_userid ) {
332 $vals['userid'] = (int)$row->actor_user;
333 }
334
335 if ( !$row->actor_user ) {
336 $vals['anon'] = true;
337 }
338 }
339 }
340 if ( $this->fld_timestamp ) {
341 $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->log_timestamp );
342 }
343
344 if ( $this->fld_comment || $this->fld_parsedcomment ) {
345 if ( LogEventsList::isDeleted( $row, LogPage::DELETED_COMMENT ) ) {
346 $vals['commenthidden'] = true;
347 $anyHidden = true;
348 }
349 if ( LogEventsList::userCan( $row, LogPage::DELETED_COMMENT, $user ) ) {
350 $comment = $this->commentStore->getComment( 'log_comment', $row )->text;
351 if ( $this->fld_comment ) {
352 $vals['comment'] = $comment;
353 }
354
355 if ( $this->fld_parsedcomment ) {
356 $vals['parsedcomment'] = Linker::formatComment( $comment, $title );
357 }
358 }
359 }
360
361 if ( $this->fld_tags ) {
362 if ( $row->ts_tags ) {
363 $tags = explode( ',', $row->ts_tags );
364 ApiResult::setIndexedTagName( $tags, 'tag' );
365 $vals['tags'] = $tags;
366 } else {
367 $vals['tags'] = [];
368 }
369 }
370
371 if ( $anyHidden && LogEventsList::isDeleted( $row, LogPage::DELETED_RESTRICTED ) ) {
372 $vals['suppressed'] = true;
373 }
374
375 return $vals;
376 }
377
381 private function getAllowedLogActions() {
382 $config = $this->getConfig();
383 return array_keys( array_merge(
384 $config->get( 'LogActions' ),
385 $config->get( 'LogActionsHandlers' )
386 ) );
387 }
388
389 public function getCacheMode( $params ) {
390 if ( $this->userCanSeeRevDel() ) {
391 return 'private';
392 }
393 if ( $params['prop'] !== null && in_array( 'parsedcomment', $params['prop'] ) ) {
394 // formatComment() calls wfMessage() among other things
395 return 'anon-public-user-private';
396 } elseif ( LogEventsList::getExcludeClause( $this->getDB(), 'user', $this->getUser() )
397 === LogEventsList::getExcludeClause( $this->getDB(), 'public' )
398 ) { // Output can only contain public data.
399 return 'public';
400 } else {
401 return 'anon-public-user-private';
402 }
403 }
404
405 public function getAllowedParams( $flags = 0 ) {
406 $config = $this->getConfig();
407 if ( $flags & ApiBase::GET_VALUES_FOR_HELP ) {
408 $logActions = $this->getAllowedLogActions();
409 sort( $logActions );
410 } else {
411 $logActions = null;
412 }
413 $ret = [
414 'prop' => [
416 ApiBase::PARAM_DFLT => 'ids|title|type|user|timestamp|comment|details',
418 'ids',
419 'title',
420 'type',
421 'user',
422 'userid',
423 'timestamp',
424 'comment',
425 'parsedcomment',
426 'details',
427 'tags'
428 ],
430 ],
431 'type' => [
433 ],
434 'action' => [
435 // validation on request is done in execute()
436 ApiBase::PARAM_TYPE => $logActions
437 ],
438 'start' => [
439 ApiBase::PARAM_TYPE => 'timestamp'
440 ],
441 'end' => [
442 ApiBase::PARAM_TYPE => 'timestamp'
443 ],
444 'dir' => [
445 ApiBase::PARAM_DFLT => 'older',
447 'newer',
448 'older'
449 ],
450 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
451 ],
452 'user' => [
453 ApiBase::PARAM_TYPE => 'user',
454 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
455 ],
456 'title' => null,
457 'namespace' => [
458 ApiBase::PARAM_TYPE => 'namespace',
460 ],
461 'prefix' => [],
462 'tag' => null,
463 'limit' => [
465 ApiBase::PARAM_TYPE => 'limit',
469 ],
470 'continue' => [
471 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
472 ],
473 ];
474
475 if ( $config->get( 'MiserMode' ) ) {
476 $ret['prefix'][ApiBase::PARAM_HELP_MSG] = 'api-help-param-disabled-in-miser-mode';
477 }
478
479 return $ret;
480 }
481
482 protected function getExamplesMessages() {
483 return [
484 'action=query&list=logevents'
485 => 'apihelp-query+logevents-example-simple',
486 ];
487 }
488
489 public function getHelpUrls() {
490 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Logevents';
491 }
492}
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:1436
const PARAM_MAX2
Definition ApiBase.php:89
encodeParamName( $paramName)
This method mangles parameter name based on the prefix supplied to the constructor.
Definition ApiBase.php:742
const PARAM_MAX
Definition ApiBase.php:85
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition ApiBase.php:1620
const PARAM_TYPE
Definition ApiBase.php:81
const PARAM_DFLT
Definition ApiBase.php:73
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:93
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:936
getResult()
Get the result object.
Definition ApiBase.php:628
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:764
const PARAM_EXTRA_NAMESPACES
Definition ApiBase.php:121
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:497
const PARAM_ISMULTI
Definition ApiBase.php:77
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.
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, CommentStore $commentStore, NameTableStore $changeTagDefStore)
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:37
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 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:1372
static newFromEntry(LogEntry $entry)
Constructs a new formatter suitable for given entry.
const DELETED_USER
Definition LogPage.php:41
const DELETED_RESTRICTED
Definition LogPage.php:42
const DELETED_COMMENT
Definition LogPage.php:40
static validTypes()
Get the list of valid log types.
Definition LogPage.php:206
const DELETED_ACTION
Definition LogPage.php:39
Type definition for user types.
Definition UserDef.php:25
Exception representing a failure to look up a row from a name table.