MediaWiki  master
LogEventsList.php
Go to the documentation of this file.
1 <?php
39 
41  public const NO_ACTION_LINK = 1;
42  public const NO_EXTRA_USER_LINKS = 2;
43  public const USE_CHECKBOXES = 4;
44 
45  public $flags;
46 
50  protected $showTagEditUI;
51 
55  private $linkRenderer;
56 
58  private $hookRunner;
59 
66  public function __construct( $context, $linkRenderer = null, $flags = 0 ) {
67  $this->setContext( $context );
68  $this->flags = $flags;
69  $this->showTagEditUI = ChangeTags::showTagEditingUI( $this->getAuthority() );
70  if ( $linkRenderer instanceof LinkRenderer ) {
71  $this->linkRenderer = $linkRenderer;
72  }
73  $this->hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
74  }
75 
80  protected function getLinkRenderer() {
81  if ( $this->linkRenderer !== null ) {
82  return $this->linkRenderer;
83  } else {
84  return MediaWikiServices::getInstance()->getLinkRenderer();
85  }
86  }
87 
99  public function showOptions( $type = '', $year = 0, $month = 0, $day = 0 ) {
100  $formDescriptor = [];
101 
102  // Basic selectors
103  $formDescriptor['type'] = $this->getTypeMenuDesc();
104  $formDescriptor['user'] = [
105  'class' => HTMLUserTextField::class,
106  'label-message' => 'specialloguserlabel',
107  'name' => 'user',
108  'ipallowed' => true,
109  'iprange' => true,
110  'external' => true,
111  ];
112  $formDescriptor['page'] = [
113  'class' => HTMLTitleTextField::class,
114  'label-message' => 'speciallogtitlelabel',
115  'name' => 'page',
116  'required' => false,
117  ];
118 
119  // Title pattern, if allowed
120  if ( !$this->getConfig()->get( MainConfigNames::MiserMode ) ) {
121  $formDescriptor['pattern'] = [
122  'type' => 'check',
123  'label-message' => 'log-title-wildcard',
124  'name' => 'pattern',
125  ];
126  }
127 
128  // Add extra inputs if any
129  $extraInputsDescriptor = $this->getExtraInputsDesc( $type );
130  if ( $extraInputsDescriptor ) {
131  $formDescriptor[ 'extra' ] = $extraInputsDescriptor;
132  }
133 
134  // Date menu
135  $formDescriptor['date'] = [
136  'type' => 'date',
137  'label-message' => 'date',
138  'default' => $year && $month && $day ? sprintf( "%04d-%02d-%02d", $year, $month, $day ) : '',
139  ];
140 
141  // Tag filter
142  $formDescriptor['tagfilter'] = [
143  'type' => 'tagfilter',
144  'name' => 'tagfilter',
145  'label-message' => 'tag-filter',
146  ];
147  $formDescriptor['tagInvert'] = [
148  'type' => 'check',
149  'name' => 'tagInvert',
150  'label-message' => 'invert',
151  'hide-if' => [ '===', 'tagfilter', '' ],
152  ];
153 
154  // Filter checkboxes, when work on all logs
155  if ( $type === '' ) {
156  $formDescriptor['filters'] = $this->getFiltersDesc();
157  }
158 
159  // Action filter
160  $allowedActions = $this->getConfig()->get( MainConfigNames::ActionFilteredLogs );
161  if ( isset( $allowedActions[$type] ) ) {
162  $formDescriptor['subtype'] = $this->getActionSelectorDesc( $type, $allowedActions[$type] );
163  }
164 
165  $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
166  $htmlForm
167  ->setTitle( SpecialPage::getTitleFor( 'Log' ) ) // Remove subpage
168  ->setSubmitTextMsg( 'logeventslist-submit' )
169  ->setMethod( 'GET' )
170  ->setWrapperLegendMsg( 'log' )
171  ->setFormIdentifier( 'logeventslist', true ) // T321154
172  // Set callback for data validation and log type description.
173  ->setSubmitCallback( static function ( $formData, $form ) {
174  $form->addPreHtml(
175  ( new LogPage( $formData['type'] ) )->getDescription()
176  ->setContext( $form->getContext() )->parseAsBlock()
177  );
178  return true;
179  } );
180 
181  $result = $htmlForm->prepareForm()->trySubmit();
182  $htmlForm->displayForm( $result );
183  return $result === true || ( $result instanceof Status && $result->isGood() );
184  }
185 
189  private function getFiltersDesc() {
190  $optionsMsg = [];
191  $filters = $this->getConfig()->get( MainConfigNames::FilterLogTypes );
192  foreach ( $filters as $type => $val ) {
193  $optionsMsg["logeventslist-{$type}-log"] = $type;
194  }
195  return [
196  'class' => HTMLMultiSelectField::class,
197  'label-message' => 'logeventslist-more-filters',
198  'flatlist' => true,
199  'options-messages' => $optionsMsg,
200  'default' => array_keys( array_intersect( $filters, [ false ] ) ),
201  ];
202  }
203 
207  private function getTypeMenuDesc() {
208  $typesByName = [];
209  // Load the log names
210  foreach ( LogPage::validTypes() as $type ) {
211  $page = new LogPage( $type );
212  if ( $this->getAuthority()->isAllowed( $page->getRestriction() ) ) {
213  $typesByName[$type] = $page->getName()->text();
214  }
215  }
216 
217  asort( $typesByName );
218 
219  // Always put "All public logs" on top
220  $public = $typesByName[''];
221  unset( $typesByName[''] );
222  $typesByName = [ '' => $public ] + $typesByName;
223 
224  return [
225  'class' => HTMLSelectField::class,
226  'name' => 'type',
227  'options' => array_flip( $typesByName ),
228  ];
229  }
230 
235  private function getExtraInputsDesc( $type ) {
236  if ( $type === 'suppress' ) {
237  return [
238  'type' => 'text',
239  'label-message' => 'revdelete-offender',
240  'name' => 'offender',
241  ];
242  } else {
243  // Allow extensions to add an extra input into the descriptor array.
244  $unused = ''; // Deprecated since 1.32, removed in 1.41
245  $formDescriptor = [];
246  $this->hookRunner->onLogEventsListGetExtraInputs( $type, $this, $unused, $formDescriptor );
247 
248  return $formDescriptor;
249  }
250  }
251 
258  private function getActionSelectorDesc( $type, $actions ) {
259  $actionOptions = [ 'log-action-filter-all' => '' ];
260 
261  foreach ( $actions as $value => $_ ) {
262  $msgKey = "log-action-filter-$type-$value";
263  $actionOptions[ $msgKey ] = $value;
264  }
265 
266  return [
267  'class' => HTMLSelectField::class,
268  'name' => 'subtype',
269  'options-messages' => $actionOptions,
270  'label-message' => 'log-action-filter-' . $type,
271  ];
272  }
273 
277  public function beginLogEventsList() {
278  return "<ul class='mw-logevent-loglines'>\n";
279  }
280 
284  public function endLogEventsList() {
285  return "</ul>\n";
286  }
287 
292  public function logLine( $row ) {
293  $entry = DatabaseLogEntry::newFromRow( $row );
294  $formatter = LogFormatter::newFromEntry( $entry );
295  $formatter->setContext( $this->getContext() );
296  $formatter->setLinkRenderer( $this->getLinkRenderer() );
297  $formatter->setShowUserToolLinks( !( $this->flags & self::NO_EXTRA_USER_LINKS ) );
298 
299  $time = $this->getLanguage()->userTimeAndDate(
300  $entry->getTimestamp(),
301  $this->getUser()
302  );
303  // Link the time text to the specific log entry, see T207562
304  $timeLink = $this->getLinkRenderer()->makeKnownLink(
305  SpecialPage::getTitleValueFor( 'Log' ),
306  $time,
307  [],
308  [ 'logid' => $entry->getId() ]
309  );
310 
311  $action = $formatter->getActionText();
312 
313  if ( $this->flags & self::NO_ACTION_LINK ) {
314  $revert = '';
315  } else {
316  $revert = $formatter->getActionLinks();
317  if ( $revert != '' ) {
318  $revert = '<span class="mw-logevent-actionlink">' . $revert . '</span>';
319  }
320  }
321 
322  $comment = $formatter->getComment();
323 
324  // Some user can hide log items and have review links
325  $del = $this->getShowHideLinks( $row );
326 
327  // Any tags...
328  [ $tagDisplay, $newClasses ] = ChangeTags::formatSummaryRow(
329  $row->ts_tags,
330  'logevent',
331  $this->getContext()
332  );
333  $classes = array_merge(
334  [ 'mw-logline-' . $entry->getType() ],
335  $newClasses
336  );
337  $attribs = [
338  'data-mw-logid' => $entry->getId(),
339  'data-mw-logaction' => $entry->getFullType(),
340  ];
341  $ret = "$del $timeLink $action $comment $revert $tagDisplay";
342 
343  // Let extensions add data
344  $this->hookRunner->onLogEventsListLineEnding( $this, $ret, $entry, $classes, $attribs );
345  $attribs = array_filter( $attribs,
346  [ Sanitizer::class, 'isReservedDataAttribute' ],
347  ARRAY_FILTER_USE_KEY
348  );
349  $attribs['class'] = $classes;
350 
351  return Html::rawElement( 'li', $attribs, $ret ) . "\n";
352  }
353 
358  private function getShowHideLinks( $row ) {
359  // We don't want to see the links and
360  if ( $this->flags == self::NO_ACTION_LINK ) {
361  return '';
362  }
363 
364  // If change tag editing is available to this user, return the checkbox
365  if ( $this->flags & self::USE_CHECKBOXES && $this->showTagEditUI ) {
366  return Xml::check(
367  'showhiderevisions',
368  false,
369  [ 'name' => 'ids[' . $row->log_id . ']' ]
370  );
371  }
372 
373  // no one can hide items from the suppress log.
374  if ( $row->log_type == 'suppress' ) {
375  return '';
376  }
377 
378  $del = '';
379  $authority = $this->getAuthority();
380  // Don't show useless checkbox to people who cannot hide log entries
381  if ( $authority->isAllowed( 'deletedhistory' ) ) {
382  $canHide = $authority->isAllowed( 'deletelogentry' );
383  $canViewSuppressedOnly = $authority->isAllowed( 'viewsuppressed' ) &&
384  !$authority->isAllowed( 'suppressrevision' );
385  $entryIsSuppressed = self::isDeleted( $row, LogPage::DELETED_RESTRICTED );
386  $canViewThisSuppressedEntry = $canViewSuppressedOnly && $entryIsSuppressed;
387  if ( $row->log_deleted || $canHide ) {
388  // Show checkboxes instead of links.
389  if ( $canHide && $this->flags & self::USE_CHECKBOXES && !$canViewThisSuppressedEntry ) {
390  // If event was hidden from sysops
391  if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $authority ) ) {
392  $del = Xml::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] );
393  } else {
394  $del = Xml::check(
395  'showhiderevisions',
396  false,
397  [ 'name' => 'ids[' . $row->log_id . ']' ]
398  );
399  }
400  } else {
401  // If event was hidden from sysops
402  if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $authority ) ) {
403  $del = Linker::revDeleteLinkDisabled( $canHide );
404  } else {
405  $query = [
406  'target' => SpecialPage::getTitleFor( 'Log', $row->log_type )->getPrefixedDBkey(),
407  'type' => 'logging',
408  'ids' => $row->log_id,
409  ];
410  $del = Linker::revDeleteLink(
411  $query,
412  $entryIsSuppressed,
413  $canHide && !$canViewThisSuppressedEntry
414  );
415  }
416  }
417  }
418  }
419 
420  return $del;
421  }
422 
429  public static function typeAction( $row, $type, $action ) {
430  $match = is_array( $type ) ?
431  in_array( $row->log_type, $type ) : $row->log_type == $type;
432  if ( $match ) {
433  $match = is_array( $action ) ?
434  in_array( $row->log_action, $action ) : $row->log_action == $action;
435  }
436 
437  return $match;
438  }
439 
449  public static function userCan( $row, $field, Authority $performer ) {
450  return self::userCanBitfield( $row->log_deleted, $field, $performer ) &&
451  self::userCanViewLogType( $row->log_type, $performer );
452  }
453 
463  public static function userCanBitfield( $bitfield, $field, Authority $performer ) {
464  if ( $bitfield & $field ) {
465  if ( $bitfield & LogPage::DELETED_RESTRICTED ) {
466  return $performer->isAllowedAny( 'suppressrevision', 'viewsuppressed' );
467  } else {
468  return $performer->isAllowed( 'deletedhistory' );
469  }
470  }
471  return true;
472  }
473 
482  public static function userCanViewLogType( $type, Authority $performer ) {
483  $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogRestrictions );
484  if ( isset( $logRestrictions[$type] ) && !$performer->isAllowed( $logRestrictions[$type] ) ) {
485  return false;
486  }
487  return true;
488  }
489 
495  public static function isDeleted( $row, $field ) {
496  return ( $row->log_deleted & $field ) == $field;
497  }
498 
524  public static function showLogExtract(
525  &$out, $types = [], $page = '', $user = '', $param = []
526  ) {
527  $defaultParameters = [
528  'lim' => 25,
529  'conds' => [],
530  'showIfEmpty' => true,
531  'msgKey' => [ '' ],
532  'wrap' => "$1",
533  'flags' => 0,
534  'useRequestParams' => false,
535  'useMaster' => false,
536  'extraUrlParams' => false,
537  ];
538  # The + operator appends elements of remaining keys from the right
539  # handed array to the left handed, whereas duplicated keys are NOT overwritten.
540  $param += $defaultParameters;
541  # Convert $param array to individual variables
542  $lim = $param['lim'];
543  $conds = $param['conds'];
544  $showIfEmpty = $param['showIfEmpty'];
545  $msgKey = $param['msgKey'];
546  $wrap = $param['wrap'];
547  $flags = $param['flags'];
548  $extraUrlParams = $param['extraUrlParams'];
549 
550  $useRequestParams = $param['useRequestParams'];
551  // @phan-suppress-next-line PhanRedundantCondition
552  if ( !is_array( $msgKey ) ) {
553  $msgKey = [ $msgKey ];
554  }
555 
556  if ( $out instanceof OutputPage ) {
557  $context = $out->getContext();
558  } else {
559  $context = RequestContext::getMain();
560  }
561 
562  $services = MediaWikiServices::getInstance();
563  // FIXME: Figure out how to inject this
564  $linkRenderer = $services->getLinkRenderer();
565 
566  # Insert list of top 50 (or top $lim) items
567  $loglist = new LogEventsList( $context, $linkRenderer, $flags );
568  $pager = new LogPager(
569  $loglist,
570  $types,
571  $user,
572  $page,
573  false,
574  $conds,
575  false,
576  false,
577  false,
578  '',
579  '',
580  0,
581  $services->getLinkBatchFactory(),
582  $services->getActorNormalization()
583  );
584  // @phan-suppress-next-line PhanImpossibleCondition
585  if ( !$useRequestParams ) {
586  # Reset vars that may have been taken from the request
587  $pager->mLimit = 50;
588  $pager->mDefaultLimit = 50;
589  $pager->mOffset = "";
590  $pager->mIsBackwards = false;
591  }
592 
593  // @phan-suppress-next-line PhanImpossibleCondition
594  if ( $param['useMaster'] ) {
595  $pager->mDb = $services->getDBLoadBalancerFactory()->getPrimaryDatabase();
596  }
597  // @phan-suppress-next-line PhanImpossibleCondition
598  if ( isset( $param['offset'] ) ) { # Tell pager to ignore WebRequest offset
599  $pager->setOffset( $param['offset'] );
600  }
601 
602  // @phan-suppress-next-line PhanSuspiciousValueComparison
603  if ( $lim > 0 ) {
604  $pager->mLimit = $lim;
605  }
606  // Fetch the log rows and build the HTML if needed
607  $logBody = $pager->getBody();
608  $numRows = $pager->getNumRows();
609 
610  $s = '';
611 
612  if ( $logBody ) {
613  if ( $msgKey[0] ) {
614  // @phan-suppress-next-line PhanParamTooFewUnpack Non-emptiness checked above
615  $msg = $context->msg( ...$msgKey );
616  if ( $page instanceof PageReference ) {
617  $msg->page( $page );
618  }
619  $s .= $msg->parseAsBlock();
620  }
621  $s .= $loglist->beginLogEventsList() .
622  $logBody .
623  $loglist->endLogEventsList();
624  // add styles for change tags
625  $context->getOutput()->addModuleStyles( 'mediawiki.interface.helpers.styles' );
626  // @phan-suppress-next-line PhanRedundantCondition
627  } elseif ( $showIfEmpty ) {
628  $s = Html::rawElement( 'div', [ 'class' => 'mw-warning-logempty' ],
629  $context->msg( 'logempty' )->parse() );
630  }
631 
632  if ( $page instanceof PageReference ) {
633  $titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter();
634  $pageName = $titleFormatter->getPrefixedDBkey( $page );
635  } elseif ( $page != '' ) {
636  $pageName = $page;
637  } else {
638  $pageName = null;
639  }
640 
641  if ( $numRows > $pager->mLimit ) { # Show "Full log" link
642  $urlParam = [];
643  if ( $pageName ) {
644  $urlParam['page'] = $pageName;
645  }
646 
647  if ( $user != '' ) {
648  $urlParam['user'] = $user;
649  }
650 
651  if ( !is_array( $types ) ) { # Make it an array, if it isn't
652  $types = [ $types ];
653  }
654 
655  # If there is exactly one log type, we can link to Special:Log?type=foo
656  if ( count( $types ) == 1 ) {
657  $urlParam['type'] = $types[0];
658  }
659 
660  // @phan-suppress-next-line PhanSuspiciousValueComparison
661  if ( $extraUrlParams !== false ) {
662  $urlParam = array_merge( $urlParam, $extraUrlParams );
663  }
664 
665  $s .= $linkRenderer->makeKnownLink(
666  SpecialPage::getTitleFor( 'Log' ),
667  $context->msg( 'log-fulllog' )->text(),
668  [],
669  $urlParam
670  );
671  }
672 
673  if ( $logBody && $msgKey[0] ) {
674  // TODO: The condition above is weird. Should this be done in any other cases?
675  // Or is it always true in practice?
676 
677  // Mark as interface language (T60685)
678  $dir = $context->getLanguage()->getDir();
679  $lang = $context->getLanguage()->getHtmlCode();
680  $s = Html::rawElement( 'div', [
681  'class' => "mw-content-$dir",
682  'dir' => $dir,
683  'lang' => $lang,
684  ], $s );
685 
686  // Wrap in warning box
687  $s = Html::warningBox(
688  $s,
689  'mw-warning-with-logexcerpt'
690  );
691  }
692 
693  // @phan-suppress-next-line PhanSuspiciousValueComparison, PhanRedundantCondition
694  if ( $wrap != '' ) { // Wrap message in html
695  $s = str_replace( '$1', $s, $wrap );
696  }
697 
698  /* hook can return false, if we don't want the message to be emitted (Wikia BugId:7093) */
699  $hookRunner = new HookRunner( $services->getHookContainer() );
700  if ( $hookRunner->onLogEventsListShowLogExtract( $s, $types, $pageName, $user, $param ) ) {
701  // $out can be either an OutputPage object or a String-by-reference
702  if ( $out instanceof OutputPage ) {
703  $out->addHTML( $s );
704  } else {
705  $out = $s;
706  }
707  }
708 
709  return $numRows;
710  }
711 
721  public static function getExcludeClause( $db, $audience = 'public', Authority $performer = null ) {
722  $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogRestrictions );
723 
724  if ( $audience != 'public' && $performer === null ) {
725  throw new InvalidArgumentException(
726  'A User object must be given when checking for a user audience.'
727  );
728  }
729 
730  // Reset the array, clears extra "where" clauses when $par is used
731  $hiddenLogs = [];
732 
733  // Don't show private logs to unprivileged users
734  foreach ( $logRestrictions as $logType => $right ) {
735  if ( $audience == 'public' || !$performer->isAllowed( $right ) ) {
736  $hiddenLogs[] = $logType;
737  }
738  }
739  if ( count( $hiddenLogs ) == 1 ) {
740  return 'log_type != ' . $db->addQuotes( $hiddenLogs[0] );
741  } elseif ( $hiddenLogs ) {
742  return 'log_type NOT IN (' . $db->makeList( $hiddenLogs ) . ')';
743  }
744 
745  return false;
746  }
747 }
static showTagEditingUI(Authority $performer)
Indicate whether change tag editing UI is relevant.
static formatSummaryRow( $tags, $unused, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:147
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
getContext()
Get the base IContextSource object.
setContext(IContextSource $context)
static newFromRow( $row)
Constructs new LogEntry from database result row.
static factory( $displayFormat, $descriptor, IContextSource $context, $messagePrefix='')
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:360
const NO_EXTRA_USER_LINKS
static typeAction( $row, $type, $action)
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
static getExcludeClause( $db, $audience='public', Authority $performer=null)
SQL clause to skip forbidden log types for this user.
showOptions( $type='', $year=0, $month=0, $day=0)
Show options for the log list.
static userCan( $row, $field, Authority $performer)
Determine if the current user is allowed to view a particular field of this log row,...
static userCanBitfield( $bitfield, $field, Authority $performer)
Determine if the current user is allowed to view a particular field of this log row,...
__construct( $context, $linkRenderer=null, $flags=0)
const NO_ACTION_LINK
static userCanViewLogType( $type, Authority $performer)
Determine if the current user is allowed to view a particular field of this log row,...
const USE_CHECKBOXES
static isDeleted( $row, $field)
static newFromEntry(LogEntry $entry)
Constructs a new formatter suitable for given entry.
Class to simplify the use of log pages.
Definition: LogPage.php:43
const DELETED_RESTRICTED
Definition: LogPage.php:47
static validTypes()
Get the list of valid log types.
Definition: LogPage.php:211
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:568
onLogEventsListShowLogExtract(&$s, $types, $page, $user, $param)
This hook is called before the string is added to OutputPage.
This class is a collection of static functions that serve two purposes:
Definition: Html.php:57
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition: Linker.php:65
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
This is one of the Core classes and should be read at least once by any new developers.
Definition: OutputPage.php:93
HTML sanitizer for MediaWiki.
Definition: Sanitizer.php:46
Parent class for all special pages.
Definition: SpecialPage.php:65
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:58
static getMain()
Get the RequestContext object associated with the main request.
isGood()
Returns whether the operation completed and didn't have any error or warnings.
static check( $name, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox.
Definition: Xml.php:336
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
isAllowed(string $permission, PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
isAllowedAny(... $permissions)
Checks whether this authority has any of the given permissions in general.