MediaWiki  master
LogPager.php
Go to the documentation of this file.
1 <?php
32 
38  private $types = [];
39 
41  private $performer = '';
42 
44  private $page = '';
45 
47  private $pattern = false;
48 
50  private $typeCGI = '';
51 
53  private $action = '';
54 
57 
59  private $actionRestrictionsEnforced = false;
60 
62  private $mConds;
63 
65  private $mTagFilter;
66 
69 
72 
75 
93  public function __construct( $list, $types = [], $performer = '', $page = '',
94  $pattern = false, $conds = [], $year = false, $month = false, $day = false,
95  $tagFilter = '', $action = '', $logId = 0,
97  ILoadBalancer $loadBalancer = null,
99  ) {
100  $services = MediaWikiServices::getInstance();
101  // Set database before parent constructor to avoid setting it there with wfGetDB
102  $this->mDb = ( $loadBalancer ?? $services->getDBLoadBalancer() )
103  ->getConnectionRef( ILoadBalancer::DB_REPLICA, 'logpager' );
104  parent::__construct( $list->getContext() );
105  $this->mConds = $conds;
106 
107  $this->mLogEventsList = $list;
108 
109  // Class is used directly in extensions - T266480
110  $this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
111  $this->actorNormalization = $actorNormalization ?? $services->getActorNormalization();
112 
113  $this->limitLogId( $logId ); // set before types per T269761
114  $this->limitType( $types ); // also excludes hidden types
115  $this->limitFilterTypes();
116  $this->limitPerformer( $performer );
117  $this->limitTitle( $page, $pattern );
118  $this->limitAction( $action );
119  $this->getDateCond( $year, $month, $day );
120  $this->mTagFilter = (string)$tagFilter;
121  }
122 
123  public function getDefaultQuery() {
124  $query = parent::getDefaultQuery();
125  $query['type'] = $this->typeCGI; // arrays won't work here
126  $query['user'] = $this->performer;
127  $query['day'] = $this->mDay;
128  $query['month'] = $this->mMonth;
129  $query['year'] = $this->mYear;
130 
131  return $query;
132  }
133 
134  private function limitFilterTypes() {
135  if ( $this->hasEqualsClause( 'log_id' ) ) { // T220834
136  return;
137  }
138  $filterTypes = $this->getFilterParams();
139  foreach ( $filterTypes as $type => $hide ) {
140  if ( $hide ) {
141  $this->mConds[] = 'log_type != ' . $this->mDb->addQuotes( $type );
142  }
143  }
144  }
145 
146  public function getFilterParams() {
147  $filters = [];
148  if ( count( $this->types ) ) {
149  return $filters;
150  }
151 
152  $wpfilters = $this->getRequest()->getArray( "wpfilters" );
153  $filterLogTypes = $this->getConfig()->get( MainConfigNames::FilterLogTypes );
154 
155  foreach ( $filterLogTypes as $type => $default ) {
156  // Back-compat: Check old URL params if the new param wasn't passed
157  if ( $wpfilters === null ) {
158  $hide = $this->getRequest()->getBool( "hide_{$type}_log", $default );
159  } else {
160  $hide = !in_array( $type, $wpfilters );
161  }
162 
163  $filters[$type] = $hide;
164  }
165 
166  return $filters;
167  }
168 
176  private function limitType( $types ) {
177  $user = $this->getUser();
178  $restrictions = $this->getConfig()->get( MainConfigNames::LogRestrictions );
179  // If $types is not an array, make it an array
180  $types = ( $types === '' ) ? [] : (array)$types;
181  // Don't even show header for private logs; don't recognize it...
182  $needReindex = false;
183  foreach ( $types as $type ) {
184  if ( isset( $restrictions[$type] )
185  && !$this->getAuthority()->isAllowed( $restrictions[$type] )
186  ) {
187  $needReindex = true;
188  $types = array_diff( $types, [ $type ] );
189  }
190  }
191  if ( $needReindex ) {
192  // Lots of this code makes assumptions that
193  // the first entry in the array is $types[0].
194  $types = array_values( $types );
195  }
196  $this->types = $types;
197  // Don't show private logs to unprivileged users.
198  // Also, only show them upon specific request to avoid surprises.
199  // Exception: if we are showing only a single log entry based on the log id,
200  // we don't require that "specific request" so that the links-in-logs feature
201  // works. See T269761
202  $audience = ( $types || $this->hasEqualsClause( 'log_id' ) ) ? 'user' : 'public';
203  $hideLogs = LogEventsList::getExcludeClause( $this->mDb, $audience, $user );
204  if ( $hideLogs !== false ) {
205  $this->mConds[] = $hideLogs;
206  }
207  if ( count( $types ) ) {
208  $this->mConds['log_type'] = $types;
209  // Set typeCGI; used in url param for paging
210  if ( count( $types ) == 1 ) {
211  $this->typeCGI = $types[0];
212  }
213  }
214  }
215 
222  private function limitPerformer( $name ) {
223  if ( $name == '' ) {
224  return;
225  }
226 
227  $actorId = $this->actorNormalization->findActorIdByName( $name, $this->mDb );
228 
229  if ( !$actorId ) {
230  // Unknown user, match nothing.
231  $this->mConds[] = '1 = 0';
232  return;
233  }
234 
235  $this->mConds[ 'log_actor' ] = $actorId;
236 
238 
239  $this->performer = $name;
240  }
241 
250  private function limitTitle( $page, $pattern ) {
251  if ( !$page instanceof PageReference ) {
252  // NOTE: For some types of logs, the title may be something strange, like "User:#12345"!
254  if ( !$page ) {
255  return;
256  }
257  }
258 
259  $titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter();
260  $this->page = $titleFormatter->getPrefixedDBkey( $page );
261  $ns = $page->getNamespace();
262  $db = $this->mDb;
263 
264  $interwikiDelimiter = $this->getConfig()->get( MainConfigNames::UserrightsInterwikiDelimiter );
265 
266  $doUserRightsLogLike = false;
267  if ( $this->types == [ 'rights' ] ) {
268  $parts = explode( $interwikiDelimiter, $page->getDBkey() );
269  if ( count( $parts ) == 2 ) {
270  list( $name, $database ) = array_map( 'trim', $parts );
271  if ( strstr( $database, '*' ) ) { // Search for wildcard in database name
272  $doUserRightsLogLike = true;
273  }
274  }
275  }
276 
290  $this->mConds['log_namespace'] = $ns;
291  if ( $doUserRightsLogLike ) {
292  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $name is set when reached here
293  $params = [ $name . $interwikiDelimiter ];
294  // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable $database is set when reached here
295  // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal $database is set when reached here
296  foreach ( explode( '*', $database ) as $databasepart ) {
297  $params[] = $databasepart;
298  $params[] = $db->anyString();
299  }
300  array_pop( $params ); // Get rid of the last % we added.
301  $this->mConds[] = 'log_title' . $db->buildLike( ...$params );
302  } elseif ( $pattern && !$this->getConfig()->get( MainConfigNames::MiserMode ) ) {
303  $this->mConds[] = 'log_title' . $db->buildLike( $page->getDBkey(), $db->anyString() );
304  $this->pattern = $pattern;
305  } else {
306  $this->mConds['log_title'] = $page->getDBkey();
307  }
308  $this->enforceActionRestrictions();
309  }
310 
316  private function limitAction( $action ) {
317  // Allow to filter the log by actions
319  if ( $type === '' ) {
320  // nothing to do
321  return;
322  }
323  $actions = $this->getConfig()->get( MainConfigNames::ActionFilteredLogs );
324  if ( isset( $actions[$type] ) ) {
325  // log type can be filtered by actions
326  $this->mLogEventsList->setAllowedActions( array_keys( $actions[$type] ) );
327  if ( $action !== '' && isset( $actions[$type][$action] ) ) {
328  // add condition to query
329  $this->mConds['log_action'] = $actions[$type][$action];
330  $this->action = $action;
331  }
332  }
333  }
334 
339  protected function limitLogId( $logId ) {
340  if ( !$logId ) {
341  return;
342  }
343  $this->mConds['log_id'] = $logId;
344  }
345 
351  public function getQueryInfo() {
353 
354  $tables = $basic['tables'];
355  $fields = $basic['fields'];
356  $conds = $basic['conds'];
357  $options = $basic['options'];
358  $joins = $basic['join_conds'];
359 
360  # Add log_search table if there are conditions on it.
361  # This filters the results to only include log rows that have
362  # log_search records with the specified ls_field and ls_value values.
363  if ( array_key_exists( 'ls_field', $this->mConds ) ) {
364  $tables[] = 'log_search';
365  $options['IGNORE INDEX'] = [ 'log_search' => 'ls_log_id' ];
366  $options['USE INDEX'] = [ 'logging' => 'PRIMARY' ];
367  if ( !$this->hasEqualsClause( 'ls_field' )
368  || !$this->hasEqualsClause( 'ls_value' )
369  ) {
370  # Since (ls_field,ls_value,ls_logid) is unique, if the condition is
371  # to match a specific (ls_field,ls_value) tuple, then there will be
372  # no duplicate log rows. Otherwise, we need to remove the duplicates.
373  $options[] = 'DISTINCT';
374  }
375  } elseif ( array_key_exists( 'log_actor', $this->mConds ) ) {
376  // Optimizer doesn't pick the right index when a user has lots of log actions (T303089)
377  $index = 'log_actor_time';
378  foreach ( $this->getFilterParams() as $type => $hide ) {
379  if ( !$hide ) {
380  $index = 'log_actor_type_time';
381  break;
382  }
383  }
384  $options['USE INDEX'] = [ 'logging' => $index ];
385  }
386  # Don't show duplicate rows when using log_search
387  $joins['log_search'] = [ 'JOIN', 'ls_log_id=log_id' ];
388 
389  // T221458: MySQL/MariaDB (10.1.37) can sometimes irrationally decide that querying `actor` before
390  // `logging` and filesorting is somehow better than querying $limit+1 rows from `logging`.
391  // Tell it not to reorder the query. But not when tag filtering or log_search was used, as it
392  // seems as likely to be harmed as helped in that case.
393  if ( $this->mTagFilter === '' && !array_key_exists( 'ls_field', $this->mConds ) ) {
394  $options[] = 'STRAIGHT_JOIN';
395  }
396 
397  $options['MAX_EXECUTION_TIME'] = $this->getConfig()
398  ->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries );
399 
400  $info = [
401  'tables' => $tables,
402  'fields' => $fields,
403  'conds' => array_merge( $conds, $this->mConds ),
404  'options' => $options,
405  'join_conds' => $joins,
406  ];
407  # Add ChangeTags filter query
408  ChangeTags::modifyDisplayQuery( $info['tables'], $info['fields'], $info['conds'],
409  $info['join_conds'], $info['options'], $this->mTagFilter );
410 
411  return $info;
412  }
413 
419  protected function hasEqualsClause( $field ) {
420  return (
421  array_key_exists( $field, $this->mConds ) &&
422  ( !is_array( $this->mConds[$field] ) || count( $this->mConds[$field] ) == 1 )
423  );
424  }
425 
426  public function getIndexField() {
427  return 'log_timestamp';
428  }
429 
430  protected function getStartBody() {
431  # Do a link batch query
432  if ( $this->getNumRows() > 0 ) {
433  $lb = $this->linkBatchFactory->newLinkBatch();
434  foreach ( $this->mResult as $row ) {
435  $lb->add( $row->log_namespace, $row->log_title );
436  $lb->add( NS_USER, $row->log_user_text );
437  $lb->add( NS_USER_TALK, $row->log_user_text );
438  $formatter = LogFormatter::newFromRow( $row );
439  foreach ( $formatter->getPreloadTitles() as $title ) {
440  $lb->addObj( $title );
441  }
442  }
443  $lb->execute();
444  $this->mResult->seek( 0 );
445  }
446 
447  return '';
448  }
449 
450  public function formatRow( $row ) {
451  return $this->mLogEventsList->logLine( $row );
452  }
453 
454  public function getType() {
455  return $this->types;
456  }
457 
463  public function getPerformer() {
464  return $this->performer;
465  }
466 
470  public function getPage() {
471  return $this->page;
472  }
473 
477  public function getPattern() {
478  return $this->pattern;
479  }
480 
481  public function getYear() {
482  return $this->mYear;
483  }
484 
485  public function getMonth() {
486  return $this->mMonth;
487  }
488 
489  public function getDay() {
490  return $this->mDay;
491  }
492 
493  public function getTagFilter() {
494  return $this->mTagFilter;
495  }
496 
497  public function getAction() {
498  return $this->action;
499  }
500 
501  public function doQuery() {
502  // Workaround MySQL optimizer bug
503  $this->mDb->setBigSelects();
504  parent::doQuery();
505  $this->mDb->setBigSelects( 'default' );
506  }
507 
511  private function enforceActionRestrictions() {
512  if ( $this->actionRestrictionsEnforced ) {
513  return;
514  }
515  $this->actionRestrictionsEnforced = true;
516  if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
517  $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_ACTION ) . ' = 0';
518  } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
519  $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_ACTION ) .
520  ' != ' . LogPage::SUPPRESSED_USER;
521  }
522  }
523 
527  private function enforcePerformerRestrictions() {
528  // Same as enforceActionRestrictions(), except for _USER instead of _ACTION bits.
529  if ( $this->performerRestrictionsEnforced ) {
530  return;
531  }
532  $this->performerRestrictionsEnforced = true;
533  if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
534  $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_USER ) . ' = 0';
535  } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
536  $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_USER ) .
538  }
539  }
540 }
const NS_USER
Definition: Defines.php:66
const NS_USER_TALK
Definition: Defines.php:67
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='', bool $exclude=false)
Applies all tags-related changes to a query.
Definition: ChangeTags.php:896
static getSelectQueryData()
Returns array of information that is needed for querying log entries.
IDatabase $mDb
Definition: IndexPager.php:100
getNumRows()
Get the number of rows in the result set.
Definition: IndexPager.php:777
static getExcludeClause( $db, $audience='public', Authority $performer=null)
SQL clause to skip forbidden log types for this user.
static newFromRow( $row)
Handy shortcut for constructing a formatter directly from database row.
const SUPPRESSED_USER
Definition: LogPage.php:46
const DELETED_USER
Definition: LogPage.php:42
const DELETED_ACTION
Definition: LogPage.php:40
const SUPPRESSED_ACTION
Definition: LogPage.php:47
getMonth()
Definition: LogPager.php:485
hasEqualsClause( $field)
Checks if $this->mConds has $field matched to a single value.
Definition: LogPager.php:419
bool $actionRestrictionsEnforced
Definition: LogPager.php:59
bool $performerRestrictionsEnforced
Definition: LogPager.php:56
ActorNormalization $actorNormalization
Definition: LogPager.php:74
formatRow( $row)
Returns an HTML string representing the result row $row.
Definition: LogPager.php:450
limitType( $types)
Set the log reader to return only entries of the given type.
Definition: LogPager.php:176
getStartBody()
Hook into getBody(), allows text to be inserted at the start.
Definition: LogPager.php:430
limitLogId( $logId)
Limit to the (single) specified log ID.
Definition: LogPager.php:339
string $performer
Events limited to those by performer when set.
Definition: LogPager.php:41
array $mConds
Definition: LogPager.php:62
bool $pattern
Definition: LogPager.php:47
getFilterParams()
Definition: LogPager.php:146
limitFilterTypes()
Definition: LogPager.php:134
enforcePerformerRestrictions()
Paranoia: avoid brute force searches (T19342)
Definition: LogPager.php:527
LinkBatchFactory $linkBatchFactory
Definition: LogPager.php:71
enforceActionRestrictions()
Paranoia: avoid brute force searches (T19342)
Definition: LogPager.php:511
getDefaultQuery()
Get an array of query parameters that should be put into self-links.
Definition: LogPager.php:123
limitTitle( $page, $pattern)
Set the log reader to return only entries affecting the given page.
Definition: LogPager.php:250
string $mTagFilter
Definition: LogPager.php:65
string $action
Definition: LogPager.php:53
getTagFilter()
Definition: LogPager.php:493
LogEventsList $mLogEventsList
Definition: LogPager.php:68
getQueryInfo()
Constructs the most part of the query.
Definition: LogPager.php:351
doQuery()
Do the query, using information from the object context.
Definition: LogPager.php:501
getPerformer()
Guaranteed to either return a valid title string or a Zero-Length String.
Definition: LogPager.php:463
string $typeCGI
Definition: LogPager.php:50
limitPerformer( $name)
Set the log reader to return only entries by the given user.
Definition: LogPager.php:222
getAction()
Definition: LogPager.php:497
getPattern()
Definition: LogPager.php:477
array $types
Log types.
Definition: LogPager.php:38
getIndexField()
Returns the name of the index field.
Definition: LogPager.php:426
limitAction( $action)
Set the log_action field to a specified value (or values)
Definition: LogPager.php:316
__construct( $list, $types=[], $performer='', $page='', $pattern=false, $conds=[], $year=false, $month=false, $day=false, $tagFilter='', $action='', $logId=0, LinkBatchFactory $linkBatchFactory=null, ILoadBalancer $loadBalancer=null, ActorNormalization $actorNormalization=null)
Definition: LogPager.php:93
string $page
Events limited to those about this page when set.
Definition: LogPager.php:44
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Efficient paging for SQL queries.
getDateCond( $year, $month, $day=-1)
Set and return the mOffset timestamp such that we can get all revisions with a timestamp up to the sp...
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:370
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
Service for dealing with the actor table.
Database cluster connection, tracking, load balancing, and transaction manager interface.
const DB_REPLICA
Definition: defines.php:26