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 
107  public function showOptions( $types = [], $user = '', $page = '', $pattern = '', $year = 0,
108  $month = 0, $filter = null, $tagFilter = '', $action = null
109  ) {
111 
112  $title = SpecialPage::getTitleFor( 'Log' );
113 
114  // For B/C, we take strings, but make sure they are converted...
115  $types = ( $types === '' ) ? [] : (array)$types;
116 
117  $tagSelector = ChangeTags::buildTagFilterSelector( $tagFilter, false, $this->getContext() );
118 
119  $html = Html::hidden( 'title', $title->getPrefixedDBkey() );
120 
121  // Basic selectors
122  $html .= $this->getTypeMenu( $types ) . "\n";
123  $html .= $this->getUserInput( $user ) . "\n";
124  $html .= $this->getTitleInput( $page ) . "\n";
125  $html .= $this->getExtraInputs( $types ) . "\n";
126 
127  // Title pattern, if allowed
128  if ( !$wgMiserMode ) {
129  $html .= $this->getTitlePattern( $pattern ) . "\n";
130  }
131 
132  // date menu
133  $html .= Xml::tags( 'p', null, Xml::dateMenu( (int)$year, (int)$month ) );
134 
135  // Tag filter
136  if ( $tagSelector ) {
137  $html .= Xml::tags( 'p', null, implode( '&#160;', $tagSelector ) );
138  }
139 
140  // Filter links
141  if ( $filter ) {
142  $html .= Xml::tags( 'p', null, $this->getFilterLinks( $filter ) );
143  }
144 
145  // Action filter
146  if ( $action !== null ) {
147  $html .= Xml::tags( 'p', null, $this->getActionSelector( $types, $action ) );
148  }
149 
150  // Submit button
151  $html .= Xml::submitButton( $this->msg( 'logeventslist-submit' )->text() );
152 
153  // Fieldset
154  $html = Xml::fieldset( $this->msg( 'log' )->text(), $html );
155 
156  // Form wrapping
157  $html = Xml::tags( 'form', [ 'action' => $wgScript, 'method' => 'get' ], $html );
158 
159  $this->getOutput()->addHTML( $html );
160  }
161 
166  private function getFilterLinks( $filter ) {
167  // show/hide links
168  $messages = [ $this->msg( 'show' )->text(), $this->msg( 'hide' )->text() ];
169  // Option value -> message mapping
170  $links = [];
171  $hiddens = ''; // keep track for "go" button
172  $linkRenderer = $this->getLinkRenderer();
173  foreach ( $filter as $type => $val ) {
174  // Should the below assignment be outside the foreach?
175  // Then it would have to be copied. Not certain what is more expensive.
176  $query = $this->getDefaultQuery();
177  $queryKey = "hide_{$type}_log";
178 
179  $hideVal = 1 - intval( $val );
180  $query[$queryKey] = $hideVal;
181 
182  $link = $linkRenderer->makeKnownLink(
183  $this->getTitle(),
184  $messages[$hideVal],
185  [],
186  $query
187  );
188 
189  // Message: log-show-hide-patrol
190  $links[$type] = $this->msg( "log-show-hide-{$type}" )->rawParams( $link )->escaped();
191  $hiddens .= Html::hidden( "hide_{$type}_log", $val ) . "\n";
192  }
193 
194  // Build links
195  return '<small>' . $this->getLanguage()->pipeList( $links ) . '</small>' . $hiddens;
196  }
197 
198  private function getDefaultQuery() {
199  if ( !isset( $this->mDefaultQuery ) ) {
200  $this->mDefaultQuery = $this->getRequest()->getQueryValues();
201  unset( $this->mDefaultQuery['title'] );
202  unset( $this->mDefaultQuery['dir'] );
203  unset( $this->mDefaultQuery['offset'] );
204  unset( $this->mDefaultQuery['limit'] );
205  unset( $this->mDefaultQuery['order'] );
206  unset( $this->mDefaultQuery['month'] );
207  unset( $this->mDefaultQuery['year'] );
208  }
209 
210  return $this->mDefaultQuery;
211  }
212 
217  private function getTypeMenu( $queryTypes ) {
218  $queryType = count( $queryTypes ) == 1 ? $queryTypes[0] : '';
219  $selector = $this->getTypeSelector();
220  $selector->setDefault( $queryType );
221 
222  return $selector->getHTML();
223  }
224 
230  public function getTypeSelector() {
231  $typesByName = []; // Temporary array
232  // First pass to load the log names
233  foreach ( LogPage::validTypes() as $type ) {
234  $page = new LogPage( $type );
235  $restriction = $page->getRestriction();
236  if ( $this->getUser()->isAllowed( $restriction ) ) {
237  $typesByName[$type] = $page->getName()->text();
238  }
239  }
240 
241  // Second pass to sort by name
242  asort( $typesByName );
243 
244  // Always put "All public logs" on top
245  $public = $typesByName[''];
246  unset( $typesByName[''] );
247  $typesByName = [ '' => $public ] + $typesByName;
248 
249  $select = new XmlSelect( 'type' );
250  foreach ( $typesByName as $type => $name ) {
251  $select->addOption( $name, $type );
252  }
253 
254  return $select;
255  }
256 
261  private function getUserInput( $user ) {
262  $label = Xml::inputLabel(
263  $this->msg( 'specialloguserlabel' )->text(),
264  'user',
265  'mw-log-user',
266  15,
267  $user,
268  [ 'class' => 'mw-autocomplete-user' ]
269  );
270 
271  return '<span class="mw-input-with-label">' . $label . '</span>';
272  }
273 
278  private function getTitleInput( $title ) {
279  $label = Xml::inputLabel(
280  $this->msg( 'speciallogtitlelabel' )->text(),
281  'page',
282  'mw-log-page',
283  20,
284  $title
285  );
286 
287  return '<span class="mw-input-with-label">' . $label . '</span>';
288  }
289 
294  private function getTitlePattern( $pattern ) {
295  return '<span class="mw-input-with-label">' .
296  Xml::checkLabel( $this->msg( 'log-title-wildcard' )->text(), 'pattern', 'pattern', $pattern ) .
297  '</span>';
298  }
299 
304  private function getExtraInputs( $types ) {
305  if ( count( $types ) == 1 ) {
306  if ( $types[0] == 'suppress' ) {
307  $offender = $this->getRequest()->getVal( 'offender' );
308  $user = User::newFromName( $offender, false );
309  if ( !$user || ( $user->getId() == 0 && !IP::isIPAddress( $offender ) ) ) {
310  $offender = ''; // Blank field if invalid
311  }
312  return Xml::inputLabel( $this->msg( 'revdelete-offender' )->text(), 'offender',
313  'mw-log-offender', 20, $offender );
314  } else {
315  // Allow extensions to add their own extra inputs
316  $input = '';
317  Hooks::run( 'LogEventsListGetExtraInputs', [ $types[0], $this, &$input ] );
318  return $input;
319  }
320  }
321 
322  return '';
323  }
324 
332  private function getActionSelector( $types, $action ) {
333  if ( $this->allowedActions === null || !count( $this->allowedActions ) ) {
334  return '';
335  }
336  $html = '';
337  $html .= Xml::label( wfMessage( 'log-action-filter-' . $types[0] )->text(),
338  'action-filter-' .$types[0] ) . "\n";
339  $select = new XmlSelect( 'subtype' );
340  $select->addOption( wfMessage( 'log-action-filter-all' )->text(), '' );
341  foreach ( $this->allowedActions as $value ) {
342  $msgKey = 'log-action-filter-' . $types[0] . '-' . $value;
343  $select->addOption( wfMessage( $msgKey )->text(), $value );
344  }
345  $select->setDefault( $action );
346  $html .= $select->getHTML();
347  return $html;
348  }
349 
356  public function setAllowedActions( $actions ) {
357  $this->allowedActions = $actions;
358  }
359 
363  public function beginLogEventsList() {
364  return "<ul>\n";
365  }
366 
370  public function endLogEventsList() {
371  return "</ul>\n";
372  }
373 
378  public function logLine( $row ) {
379  $entry = DatabaseLogEntry::newFromRow( $row );
380  $formatter = LogFormatter::newFromEntry( $entry );
381  $formatter->setContext( $this->getContext() );
382  $formatter->setLinkRenderer( $this->getLinkRenderer() );
383  $formatter->setShowUserToolLinks( !( $this->flags & self::NO_EXTRA_USER_LINKS ) );
384 
385  $time = htmlspecialchars( $this->getLanguage()->userTimeAndDate(
386  $entry->getTimestamp(), $this->getUser() ) );
387 
388  $action = $formatter->getActionText();
389 
390  if ( $this->flags & self::NO_ACTION_LINK ) {
391  $revert = '';
392  } else {
393  $revert = $formatter->getActionLinks();
394  if ( $revert != '' ) {
395  $revert = '<span class="mw-logevent-actionlink">' . $revert . '</span>';
396  }
397  }
398 
399  $comment = $formatter->getComment();
400 
401  // Some user can hide log items and have review links
402  $del = $this->getShowHideLinks( $row );
403 
404  // Any tags...
405  list( $tagDisplay, $newClasses ) = ChangeTags::formatSummaryRow(
406  $row->ts_tags,
407  'logevent',
408  $this->getContext()
409  );
410  $classes = array_merge(
411  [ 'mw-logline-' . $entry->getType() ],
412  $newClasses
413  );
414  $attribs = [
415  'data-mw-logid' => $entry->getId(),
416  'data-mw-logaction' => $entry->getFullType(),
417  ];
418  $ret = "$del $time $action $comment $revert $tagDisplay";
419 
420  // Let extensions add data
421  Hooks::run( 'LogEventsListLineEnding', [ $this, &$ret, $entry, &$classes, &$attribs ] );
422  $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] );
423  $attribs['class'] = implode( ' ', $classes );
424 
425  return Html::rawElement( 'li', $attribs, $ret ) . "\n";
426  }
427 
432  private function getShowHideLinks( $row ) {
433  // We don't want to see the links and
434  if ( $this->flags == self::NO_ACTION_LINK ) {
435  return '';
436  }
437 
438  $user = $this->getUser();
439 
440  // If change tag editing is available to this user, return the checkbox
441  if ( $this->flags & self::USE_CHECKBOXES && $this->showTagEditUI ) {
442  return Xml::check(
443  'showhiderevisions',
444  false,
445  [ 'name' => 'ids[' . $row->log_id . ']' ]
446  );
447  }
448 
449  // no one can hide items from the suppress log.
450  if ( $row->log_type == 'suppress' ) {
451  return '';
452  }
453 
454  $del = '';
455  // Don't show useless checkbox to people who cannot hide log entries
456  if ( $user->isAllowed( 'deletedhistory' ) ) {
457  $canHide = $user->isAllowed( 'deletelogentry' );
458  $canViewSuppressedOnly = $user->isAllowed( 'viewsuppressed' ) &&
459  !$user->isAllowed( 'suppressrevision' );
460  $entryIsSuppressed = self::isDeleted( $row, LogPage::DELETED_RESTRICTED );
461  $canViewThisSuppressedEntry = $canViewSuppressedOnly && $entryIsSuppressed;
462  if ( $row->log_deleted || $canHide ) {
463  // Show checkboxes instead of links.
464  if ( $canHide && $this->flags & self::USE_CHECKBOXES && !$canViewThisSuppressedEntry ) {
465  // If event was hidden from sysops
466  if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $user ) ) {
467  $del = Xml::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] );
468  } else {
469  $del = Xml::check(
470  'showhiderevisions',
471  false,
472  [ 'name' => 'ids[' . $row->log_id . ']' ]
473  );
474  }
475  } else {
476  // If event was hidden from sysops
477  if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $user ) ) {
478  $del = Linker::revDeleteLinkDisabled( $canHide );
479  } else {
480  $query = [
481  'target' => SpecialPage::getTitleFor( 'Log', $row->log_type )->getPrefixedDBkey(),
482  'type' => 'logging',
483  'ids' => $row->log_id,
484  ];
485  $del = Linker::revDeleteLink(
486  $query,
487  $entryIsSuppressed,
488  $canHide && !$canViewThisSuppressedEntry
489  );
490  }
491  }
492  }
493  }
494 
495  return $del;
496  }
497 
505  public static function typeAction( $row, $type, $action, $right = '' ) {
506  $match = is_array( $type ) ?
507  in_array( $row->log_type, $type ) : $row->log_type == $type;
508  if ( $match ) {
509  $match = is_array( $action ) ?
510  in_array( $row->log_action, $action ) : $row->log_action == $action;
511  if ( $match && $right ) {
512  global $wgUser;
513  $match = $wgUser->isAllowed( $right );
514  }
515  }
516 
517  return $match;
518  }
519 
529  public static function userCan( $row, $field, User $user = null ) {
530  return self::userCanBitfield( $row->log_deleted, $field, $user );
531  }
532 
542  public static function userCanBitfield( $bitfield, $field, User $user = null ) {
543  if ( $bitfield & $field ) {
544  if ( $user === null ) {
545  global $wgUser;
546  $user = $wgUser;
547  }
548  if ( $bitfield & LogPage::DELETED_RESTRICTED ) {
549  $permissions = [ 'suppressrevision', 'viewsuppressed' ];
550  } else {
551  $permissions = [ 'deletedhistory' ];
552  }
553  $permissionlist = implode( ', ', $permissions );
554  wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
555  return call_user_func_array( [ $user, 'isAllowedAny' ], $permissions );
556  }
557  return true;
558  }
559 
565  public static function isDeleted( $row, $field ) {
566  return ( $row->log_deleted & $field ) == $field;
567  }
568 
594  public static function showLogExtract(
595  &$out, $types = [], $page = '', $user = '', $param = []
596  ) {
597  $defaultParameters = [
598  'lim' => 25,
599  'conds' => [],
600  'showIfEmpty' => true,
601  'msgKey' => [ '' ],
602  'wrap' => "$1",
603  'flags' => 0,
604  'useRequestParams' => false,
605  'useMaster' => false,
606  'extraUrlParams' => false,
607  ];
608  # The + operator appends elements of remaining keys from the right
609  # handed array to the left handed, whereas duplicated keys are NOT overwritten.
610  $param += $defaultParameters;
611  # Convert $param array to individual variables
612  $lim = $param['lim'];
613  $conds = $param['conds'];
614  $showIfEmpty = $param['showIfEmpty'];
615  $msgKey = $param['msgKey'];
616  $wrap = $param['wrap'];
617  $flags = $param['flags'];
618  $extraUrlParams = $param['extraUrlParams'];
619 
620  $useRequestParams = $param['useRequestParams'];
621  if ( !is_array( $msgKey ) ) {
622  $msgKey = [ $msgKey ];
623  }
624 
625  if ( $out instanceof OutputPage ) {
626  $context = $out->getContext();
627  } else {
629  }
630 
631  // FIXME: Figure out how to inject this
632  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
633 
634  # Insert list of top 50 (or top $lim) items
635  $loglist = new LogEventsList( $context, $linkRenderer, $flags );
636  $pager = new LogPager( $loglist, $types, $user, $page, '', $conds );
637  if ( !$useRequestParams ) {
638  # Reset vars that may have been taken from the request
639  $pager->mLimit = 50;
640  $pager->mDefaultLimit = 50;
641  $pager->mOffset = "";
642  $pager->mIsBackwards = false;
643  }
644 
645  if ( $param['useMaster'] ) {
646  $pager->mDb = wfGetDB( DB_MASTER );
647  }
648  if ( isset( $param['offset'] ) ) { # Tell pager to ignore WebRequest offset
649  $pager->setOffset( $param['offset'] );
650  }
651 
652  if ( $lim > 0 ) {
653  $pager->mLimit = $lim;
654  }
655  // Fetch the log rows and build the HTML if needed
656  $logBody = $pager->getBody();
657  $numRows = $pager->getNumRows();
658 
659  $s = '';
660 
661  if ( $logBody ) {
662  if ( $msgKey[0] ) {
663  $dir = $context->getLanguage()->getDir();
664  $lang = $context->getLanguage()->getHtmlCode();
665 
666  $s = Xml::openElement( 'div', [
667  'class' => "mw-warning-with-logexcerpt mw-content-$dir",
668  'dir' => $dir,
669  'lang' => $lang,
670  ] );
671 
672  if ( count( $msgKey ) == 1 ) {
673  $s .= $context->msg( $msgKey[0] )->parseAsBlock();
674  } else { // Process additional arguments
675  $args = $msgKey;
676  array_shift( $args );
677  $s .= $context->msg( $msgKey[0], $args )->parseAsBlock();
678  }
679  }
680  $s .= $loglist->beginLogEventsList() .
681  $logBody .
682  $loglist->endLogEventsList();
683  } elseif ( $showIfEmpty ) {
684  $s = Html::rawElement( 'div', [ 'class' => 'mw-warning-logempty' ],
685  $context->msg( 'logempty' )->parse() );
686  }
687 
688  if ( $numRows > $pager->mLimit ) { # Show "Full log" link
689  $urlParam = [];
690  if ( $page instanceof Title ) {
691  $urlParam['page'] = $page->getPrefixedDBkey();
692  } elseif ( $page != '' ) {
693  $urlParam['page'] = $page;
694  }
695 
696  if ( $user != '' ) {
697  $urlParam['user'] = $user;
698  }
699 
700  if ( !is_array( $types ) ) { # Make it an array, if it isn't
701  $types = [ $types ];
702  }
703 
704  # If there is exactly one log type, we can link to Special:Log?type=foo
705  if ( count( $types ) == 1 ) {
706  $urlParam['type'] = $types[0];
707  }
708 
709  if ( $extraUrlParams !== false ) {
710  $urlParam = array_merge( $urlParam, $extraUrlParams );
711  }
712 
713  $s .= $linkRenderer->makeKnownLink(
714  SpecialPage::getTitleFor( 'Log' ),
715  $context->msg( 'log-fulllog' )->text(),
716  [],
717  $urlParam
718  );
719  }
720 
721  if ( $logBody && $msgKey[0] ) {
722  $s .= '</div>';
723  }
724 
725  if ( $wrap != '' ) { // Wrap message in html
726  $s = str_replace( '$1', $s, $wrap );
727  }
728 
729  /* hook can return false, if we don't want the message to be emitted (Wikia BugId:7093) */
730  if ( Hooks::run( 'LogEventsListShowLogExtract', [ &$s, $types, $page, $user, $param ] ) ) {
731  // $out can be either an OutputPage object or a String-by-reference
732  if ( $out instanceof OutputPage ) {
733  $out->addHTML( $s );
734  } else {
735  $out = $s;
736  }
737  }
738 
739  return $numRows;
740  }
741 
750  public static function getExcludeClause( $db, $audience = 'public', User $user = null ) {
751  global $wgLogRestrictions;
752 
753  if ( $audience != 'public' && $user === null ) {
754  global $wgUser;
755  $user = $wgUser;
756  }
757 
758  // Reset the array, clears extra "where" clauses when $par is used
759  $hiddenLogs = [];
760 
761  // Don't show private logs to unprivileged users
762  foreach ( $wgLogRestrictions as $logType => $right ) {
763  if ( $audience == 'public' || !$user->isAllowed( $right ) ) {
764  $hiddenLogs[] = $logType;
765  }
766  }
767  if ( count( $hiddenLogs ) == 1 ) {
768  return 'log_type != ' . $db->addQuotes( $hiddenLogs[0] );
769  } elseif ( $hiddenLogs ) {
770  return 'log_type NOT IN (' . $db->makeList( $hiddenLogs ) . ')';
771  }
772 
773  return false;
774  }
775 }
static newFromName($name, $validate= 'valid')
Static factory method for creation from username.
Definition: User.php:550
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:1966
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
Interface for objects which can provide a MediaWiki context on request.
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:782
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:1582
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:1966
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:715
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:746
Class for generating HTML