MediaWiki  master
LogEventsList.php
Go to the documentation of this file.
1 <?php
29 
31  const NO_ACTION_LINK = 1;
33  const USE_CHECKBOXES = 4;
34 
35  public $flags;
36 
40  protected $mDefaultQuery;
41 
45  protected $showTagEditUI;
46 
50  protected $allowedActions = null;
51 
55  private $linkRenderer;
56 
67  public function __construct( $context, $linkRenderer = null, $flags = 0 ) {
68  if ( $context instanceof IContextSource ) {
69  $this->setContext( $context );
70  } else {
71  // Old parameters, $context should be a Skin object
72  $this->setContext( $context->getContext() );
73  }
74 
75  $this->flags = $flags;
76  $this->showTagEditUI = ChangeTags::showTagEditingUI( $this->getUser() );
77  if ( $linkRenderer instanceof LinkRenderer ) {
78  $this->linkRenderer = $linkRenderer;
79  }
80  }
81 
86  protected function getLinkRenderer() {
87  if ( $this->linkRenderer !== null ) {
88  return $this->linkRenderer;
89  } else {
90  return MediaWikiServices::getInstance()->getLinkRenderer();
91  }
92  }
93 
108  public function showOptions( $types = [], $user = '', $page = '', $pattern = '', $year = 0,
109  $month = 0, $filter = null, $tagFilter = '', $action = null
110  ) {
112 
113  $title = SpecialPage::getTitleFor( 'Log' );
114 
115  // For B/C, we take strings, but make sure they are converted...
116  $types = ( $types === '' ) ? [] : (array)$types;
117 
118  $tagSelector = ChangeTags::buildTagFilterSelector( $tagFilter, false, $this->getContext() );
119 
120  $html = Html::hidden( 'title', $title->getPrefixedDBkey() );
121 
122  // Basic selectors
123  $html .= $this->getTypeMenu( $types ) . "\n";
124  $html .= $this->getUserInput( $user ) . "\n";
125  $html .= $this->getTitleInput( $page ) . "\n";
126  $html .= $this->getExtraInputs( $types ) . "\n";
127 
128  // Title pattern, if allowed
129  if ( !$wgMiserMode ) {
130  $html .= $this->getTitlePattern( $pattern ) . "\n";
131  }
132 
133  // date menu
134  $html .= Xml::tags( 'p', null, Xml::dateMenu( (int)$year, (int)$month ) );
135 
136  // Tag filter
137  if ( $tagSelector ) {
138  $html .= Xml::tags( 'p', null, implode( '&#160;', $tagSelector ) );
139  }
140 
141  // Filter links
142  if ( $filter ) {
143  $html .= Xml::tags( 'p', null, $this->getFilterLinks( $filter ) );
144  }
145 
146  // Action filter
147  if ( $action !== null ) {
148  $html .= Xml::tags( 'p', null, $this->getActionSelector( $types, $action ) );
149  }
150 
151  // Submit button
152  $html .= Xml::submitButton( $this->msg( 'logeventslist-submit' )->text() );
153 
154  // Fieldset
155  $html = Xml::fieldset( $this->msg( 'log' )->text(), $html );
156 
157  // Form wrapping
158  $html = Xml::tags( 'form', [ 'action' => $wgScript, 'method' => 'get' ], $html );
159 
160  $this->getOutput()->addHTML( $html );
161  }
162 
167  private function getFilterLinks( $filter ) {
168  // show/hide links
169  $messages = [ $this->msg( 'show' )->text(), $this->msg( 'hide' )->text() ];
170  // Option value -> message mapping
171  $links = [];
172  $hiddens = ''; // keep track for "go" button
173  $linkRenderer = $this->getLinkRenderer();
174  foreach ( $filter as $type => $val ) {
175  // Should the below assignment be outside the foreach?
176  // Then it would have to be copied. Not certain what is more expensive.
177  $query = $this->getDefaultQuery();
178  $queryKey = "hide_{$type}_log";
179 
180  $hideVal = 1 - intval( $val );
181  $query[$queryKey] = $hideVal;
182 
183  $link = $linkRenderer->makeKnownLink(
184  $this->getTitle(),
185  $messages[$hideVal],
186  [],
187  $query
188  );
189 
190  // Message: log-show-hide-patrol
191  $links[$type] = $this->msg( "log-show-hide-{$type}" )->rawParams( $link )->escaped();
192  $hiddens .= Html::hidden( "hide_{$type}_log", $val ) . "\n";
193  }
194 
195  // Build links
196  return '<small>' . $this->getLanguage()->pipeList( $links ) . '</small>' . $hiddens;
197  }
198 
199  private function getDefaultQuery() {
200  if ( !isset( $this->mDefaultQuery ) ) {
201  $this->mDefaultQuery = $this->getRequest()->getQueryValues();
202  unset( $this->mDefaultQuery['title'] );
203  unset( $this->mDefaultQuery['dir'] );
204  unset( $this->mDefaultQuery['offset'] );
205  unset( $this->mDefaultQuery['limit'] );
206  unset( $this->mDefaultQuery['order'] );
207  unset( $this->mDefaultQuery['month'] );
208  unset( $this->mDefaultQuery['year'] );
209  }
210 
211  return $this->mDefaultQuery;
212  }
213 
218  private function getTypeMenu( $queryTypes ) {
219  $queryType = count( $queryTypes ) == 1 ? $queryTypes[0] : '';
220  $selector = $this->getTypeSelector();
221  $selector->setDefault( $queryType );
222 
223  return $selector->getHTML();
224  }
225 
231  public function getTypeSelector() {
232  $typesByName = []; // Temporary array
233  // First pass to load the log names
234  foreach ( LogPage::validTypes() as $type ) {
235  $page = new LogPage( $type );
236  $restriction = $page->getRestriction();
237  if ( $this->getUser()->isAllowed( $restriction ) ) {
238  $typesByName[$type] = $page->getName()->text();
239  }
240  }
241 
242  // Second pass to sort by name
243  asort( $typesByName );
244 
245  // Always put "All public logs" on top
246  $public = $typesByName[''];
247  unset( $typesByName[''] );
248  $typesByName = [ '' => $public ] + $typesByName;
249 
250  $select = new XmlSelect( 'type' );
251  foreach ( $typesByName as $type => $name ) {
252  $select->addOption( $name, $type );
253  }
254 
255  return $select;
256  }
257 
262  private function getUserInput( $user ) {
263  $label = Xml::inputLabel(
264  $this->msg( 'specialloguserlabel' )->text(),
265  'user',
266  'mw-log-user',
267  15,
268  $user,
269  [ 'class' => 'mw-autocomplete-user' ]
270  );
271 
272  return '<span class="mw-input-with-label">' . $label . '</span>';
273  }
274 
279  private function getTitleInput( $title ) {
280  $label = Xml::inputLabel(
281  $this->msg( 'speciallogtitlelabel' )->text(),
282  'page',
283  'mw-log-page',
284  20,
285  $title
286  );
287 
288  return '<span class="mw-input-with-label">' . $label . '</span>';
289  }
290 
295  private function getTitlePattern( $pattern ) {
296  return '<span class="mw-input-with-label">' .
297  Xml::checkLabel( $this->msg( 'log-title-wildcard' )->text(), 'pattern', 'pattern', $pattern ) .
298  '</span>';
299  }
300 
305  private function getExtraInputs( $types ) {
306  if ( count( $types ) == 1 ) {
307  if ( $types[0] == 'suppress' ) {
308  $offender = $this->getRequest()->getVal( 'offender' );
309  $user = User::newFromName( $offender, false );
310  if ( !$user || ( $user->getId() == 0 && !IP::isIPAddress( $offender ) ) ) {
311  $offender = ''; // Blank field if invalid
312  }
313  return Xml::inputLabel( $this->msg( 'revdelete-offender' )->text(), 'offender',
314  'mw-log-offender', 20, $offender );
315  } else {
316  // Allow extensions to add their own extra inputs
317  $input = '';
318  Hooks::run( 'LogEventsListGetExtraInputs', [ $types[0], $this, &$input ] );
319  return $input;
320  }
321  }
322 
323  return '';
324  }
325 
333  private function getActionSelector( $types, $action ) {
334  if ( $this->allowedActions === null || !count( $this->allowedActions ) ) {
335  return '';
336  }
337  $html = '';
338  $html .= Xml::label( wfMessage( 'log-action-filter-' . $types[0] )->text(),
339  'action-filter-' .$types[0] ) . "\n";
340  $select = new XmlSelect( 'subtype' );
341  $select->addOption( wfMessage( 'log-action-filter-all' )->text(), '' );
342  foreach ( $this->allowedActions as $value ) {
343  $msgKey = 'log-action-filter-' . $types[0] . '-' . $value;
344  $select->addOption( wfMessage( $msgKey )->text(), $value );
345  }
346  $select->setDefault( $action );
347  $html .= $select->getHTML();
348  return $html;
349  }
350 
357  public function setAllowedActions( $actions ) {
358  $this->allowedActions = $actions;
359  }
360 
364  public function beginLogEventsList() {
365  return "<ul>\n";
366  }
367 
371  public function endLogEventsList() {
372  return "</ul>\n";
373  }
374 
379  public function logLine( $row ) {
380  $entry = DatabaseLogEntry::newFromRow( $row );
381  $formatter = LogFormatter::newFromEntry( $entry );
382  $formatter->setContext( $this->getContext() );
383  $formatter->setLinkRenderer( $this->getLinkRenderer() );
384  $formatter->setShowUserToolLinks( !( $this->flags & self::NO_EXTRA_USER_LINKS ) );
385 
386  $time = htmlspecialchars( $this->getLanguage()->userTimeAndDate(
387  $entry->getTimestamp(), $this->getUser() ) );
388 
389  $action = $formatter->getActionText();
390 
391  if ( $this->flags & self::NO_ACTION_LINK ) {
392  $revert = '';
393  } else {
394  $revert = $formatter->getActionLinks();
395  if ( $revert != '' ) {
396  $revert = '<span class="mw-logevent-actionlink">' . $revert . '</span>';
397  }
398  }
399 
400  $comment = $formatter->getComment();
401 
402  // Some user can hide log items and have review links
403  $del = $this->getShowHideLinks( $row );
404 
405  // Any tags...
406  list( $tagDisplay, $newClasses ) = ChangeTags::formatSummaryRow(
407  $row->ts_tags,
408  'logevent',
409  $this->getContext()
410  );
411  $classes = array_merge(
412  [ 'mw-logline-' . $entry->getType() ],
413  $newClasses
414  );
415  $attribs = [
416  'data-mw-logid' => $entry->getId(),
417  'data-mw-logaction' => $entry->getFullType(),
418  ];
419  $ret = "$del $time $action $comment $revert $tagDisplay";
420 
421  // Let extensions add data
422  Hooks::run( 'LogEventsListLineEnding', [ $this, &$ret, $entry, &$classes, &$attribs ] );
423  $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] );
424  $attribs['class'] = implode( ' ', $classes );
425 
426  return Html::rawElement( 'li', $attribs, $ret ) . "\n";
427  }
428 
433  private function getShowHideLinks( $row ) {
434  // We don't want to see the links and
435  if ( $this->flags == self::NO_ACTION_LINK ) {
436  return '';
437  }
438 
439  $user = $this->getUser();
440 
441  // If change tag editing is available to this user, return the checkbox
442  if ( $this->flags & self::USE_CHECKBOXES && $this->showTagEditUI ) {
443  return Xml::check(
444  'showhiderevisions',
445  false,
446  [ 'name' => 'ids[' . $row->log_id . ']' ]
447  );
448  }
449 
450  // no one can hide items from the suppress log.
451  if ( $row->log_type == 'suppress' ) {
452  return '';
453  }
454 
455  $del = '';
456  // Don't show useless checkbox to people who cannot hide log entries
457  if ( $user->isAllowed( 'deletedhistory' ) ) {
458  $canHide = $user->isAllowed( 'deletelogentry' );
459  $canViewSuppressedOnly = $user->isAllowed( 'viewsuppressed' ) &&
460  !$user->isAllowed( 'suppressrevision' );
461  $entryIsSuppressed = self::isDeleted( $row, LogPage::DELETED_RESTRICTED );
462  $canViewThisSuppressedEntry = $canViewSuppressedOnly && $entryIsSuppressed;
463  if ( $row->log_deleted || $canHide ) {
464  // Show checkboxes instead of links.
465  if ( $canHide && $this->flags & self::USE_CHECKBOXES && !$canViewThisSuppressedEntry ) {
466  // If event was hidden from sysops
467  if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $user ) ) {
468  $del = Xml::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] );
469  } else {
470  $del = Xml::check(
471  'showhiderevisions',
472  false,
473  [ 'name' => 'ids[' . $row->log_id . ']' ]
474  );
475  }
476  } else {
477  // If event was hidden from sysops
478  if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $user ) ) {
479  $del = Linker::revDeleteLinkDisabled( $canHide );
480  } else {
481  $query = [
482  'target' => SpecialPage::getTitleFor( 'Log', $row->log_type )->getPrefixedDBkey(),
483  'type' => 'logging',
484  'ids' => $row->log_id,
485  ];
486  $del = Linker::revDeleteLink(
487  $query,
488  $entryIsSuppressed,
489  $canHide && !$canViewThisSuppressedEntry
490  );
491  }
492  }
493  }
494  }
495 
496  return $del;
497  }
498 
506  public static function typeAction( $row, $type, $action, $right = '' ) {
507  $match = is_array( $type ) ?
508  in_array( $row->log_type, $type ) : $row->log_type == $type;
509  if ( $match ) {
510  $match = is_array( $action ) ?
511  in_array( $row->log_action, $action ) : $row->log_action == $action;
512  if ( $match && $right ) {
513  global $wgUser;
514  $match = $wgUser->isAllowed( $right );
515  }
516  }
517 
518  return $match;
519  }
520 
530  public static function userCan( $row, $field, User $user = null ) {
531  return self::userCanBitfield( $row->log_deleted, $field, $user );
532  }
533 
543  public static function userCanBitfield( $bitfield, $field, User $user = null ) {
544  if ( $bitfield & $field ) {
545  if ( $user === null ) {
546  global $wgUser;
547  $user = $wgUser;
548  }
549  if ( $bitfield & LogPage::DELETED_RESTRICTED ) {
550  $permissions = [ 'suppressrevision', 'viewsuppressed' ];
551  } else {
552  $permissions = [ 'deletedhistory' ];
553  }
554  $permissionlist = implode( ', ', $permissions );
555  wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
556  return call_user_func_array( [ $user, 'isAllowedAny' ], $permissions );
557  }
558  return true;
559  }
560 
566  public static function isDeleted( $row, $field ) {
567  return ( $row->log_deleted & $field ) == $field;
568  }
569 
595  public static function showLogExtract(
596  &$out, $types = [], $page = '', $user = '', $param = []
597  ) {
598  $defaultParameters = [
599  'lim' => 25,
600  'conds' => [],
601  'showIfEmpty' => true,
602  'msgKey' => [ '' ],
603  'wrap' => "$1",
604  'flags' => 0,
605  'useRequestParams' => false,
606  'useMaster' => false,
607  'extraUrlParams' => false,
608  ];
609  # The + operator appends elements of remaining keys from the right
610  # handed array to the left handed, whereas duplicated keys are NOT overwritten.
611  $param += $defaultParameters;
612  # Convert $param array to individual variables
613  $lim = $param['lim'];
614  $conds = $param['conds'];
615  $showIfEmpty = $param['showIfEmpty'];
616  $msgKey = $param['msgKey'];
617  $wrap = $param['wrap'];
618  $flags = $param['flags'];
619  $extraUrlParams = $param['extraUrlParams'];
620 
621  $useRequestParams = $param['useRequestParams'];
622  if ( !is_array( $msgKey ) ) {
623  $msgKey = [ $msgKey ];
624  }
625 
626  if ( $out instanceof OutputPage ) {
627  $context = $out->getContext();
628  } else {
630  }
631 
632  // FIXME: Figure out how to inject this
633  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
634 
635  # Insert list of top 50 (or top $lim) items
636  $loglist = new LogEventsList( $context, $linkRenderer, $flags );
637  $pager = new LogPager( $loglist, $types, $user, $page, '', $conds );
638  if ( !$useRequestParams ) {
639  # Reset vars that may have been taken from the request
640  $pager->mLimit = 50;
641  $pager->mDefaultLimit = 50;
642  $pager->mOffset = "";
643  $pager->mIsBackwards = false;
644  }
645 
646  if ( $param['useMaster'] ) {
647  $pager->mDb = wfGetDB( DB_MASTER );
648  }
649  if ( isset( $param['offset'] ) ) { # Tell pager to ignore WebRequest offset
650  $pager->setOffset( $param['offset'] );
651  }
652 
653  if ( $lim > 0 ) {
654  $pager->mLimit = $lim;
655  }
656  // Fetch the log rows and build the HTML if needed
657  $logBody = $pager->getBody();
658  $numRows = $pager->getNumRows();
659 
660  $s = '';
661 
662  if ( $logBody ) {
663  if ( $msgKey[0] ) {
664  $dir = $context->getLanguage()->getDir();
665  $lang = $context->getLanguage()->getHtmlCode();
666 
667  $s = Xml::openElement( 'div', [
668  'class' => "mw-warning-with-logexcerpt mw-content-$dir",
669  'dir' => $dir,
670  'lang' => $lang,
671  ] );
672 
673  if ( count( $msgKey ) == 1 ) {
674  $s .= $context->msg( $msgKey[0] )->parseAsBlock();
675  } else { // Process additional arguments
676  $args = $msgKey;
677  array_shift( $args );
678  $s .= $context->msg( $msgKey[0], $args )->parseAsBlock();
679  }
680  }
681  $s .= $loglist->beginLogEventsList() .
682  $logBody .
683  $loglist->endLogEventsList();
684  } elseif ( $showIfEmpty ) {
685  $s = Html::rawElement( 'div', [ 'class' => 'mw-warning-logempty' ],
686  $context->msg( 'logempty' )->parse() );
687  }
688 
689  if ( $numRows > $pager->mLimit ) { # Show "Full log" link
690  $urlParam = [];
691  if ( $page instanceof Title ) {
692  $urlParam['page'] = $page->getPrefixedDBkey();
693  } elseif ( $page != '' ) {
694  $urlParam['page'] = $page;
695  }
696 
697  if ( $user != '' ) {
698  $urlParam['user'] = $user;
699  }
700 
701  if ( !is_array( $types ) ) { # Make it an array, if it isn't
702  $types = [ $types ];
703  }
704 
705  # If there is exactly one log type, we can link to Special:Log?type=foo
706  if ( count( $types ) == 1 ) {
707  $urlParam['type'] = $types[0];
708  }
709 
710  if ( $extraUrlParams !== false ) {
711  $urlParam = array_merge( $urlParam, $extraUrlParams );
712  }
713 
714  $s .= $linkRenderer->makeKnownLink(
715  SpecialPage::getTitleFor( 'Log' ),
716  $context->msg( 'log-fulllog' )->text(),
717  [],
718  $urlParam
719  );
720  }
721 
722  if ( $logBody && $msgKey[0] ) {
723  $s .= '</div>';
724  }
725 
726  if ( $wrap != '' ) { // Wrap message in html
727  $s = str_replace( '$1', $s, $wrap );
728  }
729 
730  /* hook can return false, if we don't want the message to be emitted (Wikia BugId:7093) */
731  if ( Hooks::run( 'LogEventsListShowLogExtract', [ &$s, $types, $page, $user, $param ] ) ) {
732  // $out can be either an OutputPage object or a String-by-reference
733  if ( $out instanceof OutputPage ) {
734  $out->addHTML( $s );
735  } else {
736  $out = $s;
737  }
738  }
739 
740  return $numRows;
741  }
742 
751  public static function getExcludeClause( $db, $audience = 'public', User $user = null ) {
752  global $wgLogRestrictions;
753 
754  if ( $audience != 'public' && $user === null ) {
755  global $wgUser;
756  $user = $wgUser;
757  }
758 
759  // Reset the array, clears extra "where" clauses when $par is used
760  $hiddenLogs = [];
761 
762  // Don't show private logs to unprivileged users
763  foreach ( $wgLogRestrictions as $logType => $right ) {
764  if ( $audience == 'public' || !$user->isAllowed( $right ) ) {
765  $hiddenLogs[] = $logType;
766  }
767  }
768  if ( count( $hiddenLogs ) == 1 ) {
769  return 'log_type != ' . $db->addQuotes( $hiddenLogs[0] );
770  } elseif ( $hiddenLogs ) {
771  return 'log_type NOT IN (' . $db->makeList( $hiddenLogs ) . ')';
772  }
773 
774  return false;
775  }
776 }
static newFromName($name, $validate= 'valid')
Static factory method for creation from username.
Definition: User.php:551
setContext(IContextSource $context)
Set the IContextSource object.
getTitlePattern($pattern)
const USE_CHECKBOXES
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses & $html
Definition: hooks.txt:1973
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
getTypeMenu($queryTypes)
wfGetDB($db, $groups=[], $wiki=false)
Get a Database object.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition: hooks.txt:784
the array() calling protocol came about after MediaWiki 1.4rc1.
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition: hooks.txt:1584
getLanguage()
Get the Language object.
$wgScript
The URL path to index.php.
static userCan($row, $field, User $user=null)
Determine if the current user is allowed to view a particular field of this log row, if it's marked as deleted.
static isDeleted($row, $field)
if(is_array($mode)) switch($mode) $input
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition: hooks.txt:1973
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
static getTitleFor($name, $subpage=false, $fragment= '')
Get a localised Title object for a specified special page name If you don't need a full Title object...
Definition: SpecialPage.php:82
static buildTagFilterSelector($selected= '', $ooui=false, IContextSource $context=null)
Build a text box to select a change tag.
Definition: ChangeTags.php:750
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
static rawElement($element, $attribs=[], $contents= '')
Returns an HTML element in a string.
Definition: Html.php:209
if(!isset($args[0])) $lang
const NO_EXTRA_USER_LINKS
static newFromEntry(LogEntry $entry)
Constructs a new formatter suitable for given entry.
static hidden($name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:771
Class for generating HTML