MediaWiki master
LogEventsList.php
Go to the documentation of this file.
1<?php
44
46 public const NO_ACTION_LINK = 1;
47 public const NO_EXTRA_USER_LINKS = 2;
48 public const USE_CHECKBOXES = 4;
49
50 public $flags;
51
55 protected $showTagEditUI;
56
60 private $linkRenderer;
61
63 private $hookRunner;
64
66 private $tagsCache;
67
74 public function __construct( $context, $linkRenderer = null, $flags = 0 ) {
75 $this->setContext( $context );
76 $this->flags = $flags;
77 $this->showTagEditUI = ChangeTags::showTagEditingUI( $this->getAuthority() );
78 if ( $linkRenderer instanceof LinkRenderer ) {
79 $this->linkRenderer = $linkRenderer;
80 }
81 $this->hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
82 $this->tagsCache = new MapCacheLRU( 50 );
83 }
84
89 protected function getLinkRenderer() {
90 if ( $this->linkRenderer !== null ) {
91 return $this->linkRenderer;
92 } else {
93 return MediaWikiServices::getInstance()->getLinkRenderer();
94 }
95 }
96
108 public function showOptions( $type = '', $year = 0, $month = 0, $day = 0 ) {
109 $formDescriptor = [];
110
111 // Basic selectors
112 $formDescriptor['type'] = $this->getTypeMenuDesc();
113 $formDescriptor['user'] = [
114 'class' => HTMLUserTextField::class,
115 'label-message' => 'specialloguserlabel',
116 'name' => 'user',
117 'ipallowed' => true,
118 'iprange' => true,
119 'external' => true,
120 ];
121 $formDescriptor['page'] = [
122 'class' => HTMLTitleTextField::class,
123 'label-message' => 'speciallogtitlelabel',
124 'name' => 'page',
125 'required' => false,
126 ];
127
128 // Title pattern, if allowed
129 if ( !$this->getConfig()->get( MainConfigNames::MiserMode ) ) {
130 $formDescriptor['pattern'] = [
131 'type' => 'check',
132 'label-message' => 'log-title-wildcard',
133 'name' => 'pattern',
134 ];
135 }
136
137 // Add extra inputs if any
138 $extraInputsDescriptor = $this->getExtraInputsDesc( $type );
139 if ( $extraInputsDescriptor ) {
140 $formDescriptor[ 'extra' ] = $extraInputsDescriptor;
141 }
142
143 // Date menu
144 $formDescriptor['date'] = [
145 'type' => 'date',
146 'label-message' => 'date',
147 'default' => $year && $month && $day ? sprintf( "%04d-%02d-%02d", $year, $month, $day ) : '',
148 ];
149
150 // Tag filter
151 $formDescriptor['tagfilter'] = [
152 'type' => 'tagfilter',
153 'name' => 'tagfilter',
154 'label-message' => 'tag-filter',
155 ];
156 $formDescriptor['tagInvert'] = [
157 'type' => 'check',
158 'name' => 'tagInvert',
159 'label-message' => 'invert',
160 'hide-if' => [ '===', 'tagfilter', '' ],
161 ];
162
163 // Filter checkboxes, when work on all logs
164 if ( $type === '' ) {
165 $formDescriptor['filters'] = $this->getFiltersDesc();
166 }
167
168 // Action filter
169 $allowedActions = $this->getConfig()->get( MainConfigNames::ActionFilteredLogs );
170 if ( isset( $allowedActions[$type] ) ) {
171 $formDescriptor['subtype'] = $this->getActionSelectorDesc( $type, $allowedActions[$type] );
172 }
173
174 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
175 $htmlForm
176 ->setTitle( SpecialPage::getTitleFor( 'Log' ) ) // Remove subpage
177 ->setSubmitTextMsg( 'logeventslist-submit' )
178 ->setMethod( 'GET' )
179 ->setWrapperLegendMsg( 'log' )
180 ->setFormIdentifier( 'logeventslist', true ) // T321154
181 // Set callback for data validation and log type description.
182 ->setSubmitCallback( static function ( $formData, $form ) {
183 $form->addPreHtml(
184 ( new LogPage( $formData['type'] ) )->getDescription()
185 ->setContext( $form->getContext() )->parseAsBlock()
186 );
187 return true;
188 } );
189
190 $result = $htmlForm->prepareForm()->trySubmit();
191 $htmlForm->displayForm( $result );
192 return $result === true || ( $result instanceof Status && $result->isGood() );
193 }
194
198 private function getFiltersDesc() {
199 $optionsMsg = [];
200 $filters = $this->getConfig()->get( MainConfigNames::FilterLogTypes );
201 foreach ( $filters as $type => $val ) {
202 $optionsMsg["logeventslist-{$type}-log"] = $type;
203 }
204 return [
205 'class' => HTMLMultiSelectField::class,
206 'label-message' => 'logeventslist-more-filters',
207 'flatlist' => true,
208 'options-messages' => $optionsMsg,
209 'default' => array_keys( array_intersect( $filters, [ false ] ) ),
210 ];
211 }
212
216 private function getTypeMenuDesc() {
217 $typesByName = [];
218 // Load the log names
219 foreach ( LogPage::validTypes() as $type ) {
220 $page = new LogPage( $type );
221 $pageText = $page->getName()->text();
222 if ( in_array( $pageText, $typesByName ) ) {
223 LoggerFactory::getInstance( 'error' )->error(
224 'The log type {log_type_one} has the same translation as {log_type_two} for {lang}. ' .
225 '{log_type_one} will not be displayed in the drop down menu on Special:Log.',
226 [
227 'log_type_one' => $type,
228 'log_type_two' => array_search( $pageText, $typesByName ),
229 'lang' => $this->getLanguage()->getCode(),
230 ]
231 );
232 continue;
233 }
234 if ( $this->getAuthority()->isAllowed( $page->getRestriction() ) ) {
235 $typesByName[$type] = $pageText;
236 }
237 }
238
239 asort( $typesByName );
240
241 // Always put "All public logs" on top
242 $public = $typesByName[''];
243 unset( $typesByName[''] );
244 $typesByName = [ '' => $public ] + $typesByName;
245
246 return [
247 'class' => HTMLSelectField::class,
248 'name' => 'type',
249 'options' => array_flip( $typesByName ),
250 ];
251 }
252
257 private function getExtraInputsDesc( $type ) {
258 if ( $type === 'suppress' ) {
259 return [
260 'type' => 'text',
261 'label-message' => 'revdelete-offender',
262 'name' => 'offender',
263 ];
264 } else {
265 // Allow extensions to add an extra input into the descriptor array.
266 $unused = ''; // Deprecated since 1.32, removed in 1.41
267 $formDescriptor = [];
268 $this->hookRunner->onLogEventsListGetExtraInputs( $type, $this, $unused, $formDescriptor );
269
270 return $formDescriptor;
271 }
272 }
273
280 private function getActionSelectorDesc( $type, $actions ) {
281 $actionOptions = [ 'log-action-filter-all' => '' ];
282
283 foreach ( $actions as $value => $_ ) {
284 $msgKey = "log-action-filter-$type-$value";
285 $actionOptions[ $msgKey ] = $value;
286 }
287
288 return [
289 'class' => HTMLSelectField::class,
290 'name' => 'subtype',
291 'options-messages' => $actionOptions,
292 'label-message' => 'log-action-filter-' . $type,
293 ];
294 }
295
299 public function beginLogEventsList() {
300 return "<ul class='mw-logevent-loglines'>\n";
301 }
302
306 public function endLogEventsList() {
307 return "</ul>\n";
308 }
309
314 public function logLine( $row ) {
315 $entry = DatabaseLogEntry::newFromRow( $row );
316 $formatter = LogFormatter::newFromEntry( $entry );
317 $formatter->setContext( $this->getContext() );
318 $formatter->setLinkRenderer( $this->getLinkRenderer() );
319 $formatter->setShowUserToolLinks( !( $this->flags & self::NO_EXTRA_USER_LINKS ) );
320
321 $time = $this->getLanguage()->userTimeAndDate(
322 $entry->getTimestamp(),
323 $this->getUser()
324 );
325 // Link the time text to the specific log entry, see T207562
326 $timeLink = $this->getLinkRenderer()->makeKnownLink(
327 SpecialPage::getTitleValueFor( 'Log' ),
328 $time,
329 [],
330 [ 'logid' => $entry->getId() ]
331 );
332
333 $action = $formatter->getActionText();
334
335 if ( $this->flags & self::NO_ACTION_LINK ) {
336 $revert = '';
337 } else {
338 $revert = $formatter->getActionLinks();
339 if ( $revert != '' ) {
340 $revert = '<span class="mw-logevent-actionlink">' . $revert . '</span>';
341 }
342 }
343
344 $comment = $formatter->getComment();
345
346 // Some user can hide log items and have review links
347 $del = $this->getShowHideLinks( $row );
348
349 // Any tags...
350 [ $tagDisplay, $newClasses ] = $this->tagsCache->getWithSetCallback(
351 $this->tagsCache->makeKey(
352 $row->ts_tags ?? '',
353 $this->getUser()->getName(),
354 $this->getLanguage()->getCode()
355 ),
357 $row->ts_tags,
358 'logevent',
359 $this->getContext()
360 )
361 );
362 $classes = array_merge(
363 [ 'mw-logline-' . $entry->getType() ],
364 $newClasses
365 );
366 $attribs = [
367 'data-mw-logid' => $entry->getId(),
368 'data-mw-logaction' => $entry->getFullType(),
369 ];
370 $ret = "$del $timeLink $action $comment $revert $tagDisplay";
371
372 // Let extensions add data
373 $this->hookRunner->onLogEventsListLineEnding( $this, $ret, $entry, $classes, $attribs );
374 $attribs = array_filter( $attribs,
375 [ Sanitizer::class, 'isReservedDataAttribute' ],
376 ARRAY_FILTER_USE_KEY
377 );
378 $attribs['class'] = $classes;
379
380 return Html::rawElement( 'li', $attribs, $ret ) . "\n";
381 }
382
387 private function getShowHideLinks( $row ) {
388 // We don't want to see the links and
389 if ( $this->flags == self::NO_ACTION_LINK ) {
390 return '';
391 }
392
393 // If change tag editing is available to this user, return the checkbox
394 if ( $this->flags & self::USE_CHECKBOXES && $this->showTagEditUI ) {
395 return Xml::check(
396 'showhiderevisions',
397 false,
398 [ 'name' => 'ids[' . $row->log_id . ']' ]
399 );
400 }
401
402 // no one can hide items from the suppress log.
403 if ( $row->log_type == 'suppress' ) {
404 return '';
405 }
406
407 $del = '';
408 $authority = $this->getAuthority();
409 // Don't show useless checkbox to people who cannot hide log entries
410 if ( $authority->isAllowed( 'deletedhistory' ) ) {
411 $canHide = $authority->isAllowed( 'deletelogentry' );
412 $canViewSuppressedOnly = $authority->isAllowed( 'viewsuppressed' ) &&
413 !$authority->isAllowed( 'suppressrevision' );
414 $entryIsSuppressed = self::isDeleted( $row, LogPage::DELETED_RESTRICTED );
415 $canViewThisSuppressedEntry = $canViewSuppressedOnly && $entryIsSuppressed;
416 if ( $row->log_deleted || $canHide ) {
417 // Show checkboxes instead of links.
418 if ( $canHide && $this->flags & self::USE_CHECKBOXES && !$canViewThisSuppressedEntry ) {
419 // If event was hidden from sysops
420 if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $authority ) ) {
421 $del = Xml::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] );
422 } else {
423 $del = Xml::check(
424 'showhiderevisions',
425 false,
426 [ 'name' => 'ids[' . $row->log_id . ']' ]
427 );
428 }
429 } else {
430 // If event was hidden from sysops
431 if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $authority ) ) {
432 $del = Linker::revDeleteLinkDisabled( $canHide );
433 } else {
434 $query = [
435 'target' => SpecialPage::getTitleFor( 'Log', $row->log_type )->getPrefixedDBkey(),
436 'type' => 'logging',
437 'ids' => $row->log_id,
438 ];
439 $del = Linker::revDeleteLink(
440 $query,
441 $entryIsSuppressed,
442 $canHide && !$canViewThisSuppressedEntry
443 );
444 }
445 }
446 }
447 }
448
449 return $del;
450 }
451
458 public static function typeAction( $row, $type, $action ) {
459 $match = is_array( $type ) ?
460 in_array( $row->log_type, $type ) : $row->log_type == $type;
461 if ( $match ) {
462 $match = is_array( $action ) ?
463 in_array( $row->log_action, $action ) : $row->log_action == $action;
464 }
465
466 return $match;
467 }
468
478 public static function userCan( $row, $field, Authority $performer ) {
479 return self::userCanBitfield( $row->log_deleted, $field, $performer ) &&
480 self::userCanViewLogType( $row->log_type, $performer );
481 }
482
492 public static function userCanBitfield( $bitfield, $field, Authority $performer ) {
493 if ( $bitfield & $field ) {
494 if ( $bitfield & LogPage::DELETED_RESTRICTED ) {
495 return $performer->isAllowedAny( 'suppressrevision', 'viewsuppressed' );
496 } else {
497 return $performer->isAllowed( 'deletedhistory' );
498 }
499 }
500 return true;
501 }
502
511 public static function userCanViewLogType( $type, Authority $performer ) {
512 $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogRestrictions );
513 if ( isset( $logRestrictions[$type] ) && !$performer->isAllowed( $logRestrictions[$type] ) ) {
514 return false;
515 }
516 return true;
517 }
518
524 public static function isDeleted( $row, $field ) {
525 return ( $row->log_deleted & $field ) == $field;
526 }
527
553 public static function showLogExtract(
554 &$out, $types = [], $page = '', $user = '', $param = []
555 ) {
556 $defaultParameters = [
557 'lim' => 25,
558 'conds' => [],
559 'showIfEmpty' => true,
560 'msgKey' => [ '' ],
561 'wrap' => "$1",
562 'flags' => 0,
563 'useRequestParams' => false,
564 'useMaster' => false,
565 'extraUrlParams' => false,
566 ];
567 # The + operator appends elements of remaining keys from the right
568 # handed array to the left handed, whereas duplicated keys are NOT overwritten.
569 $param += $defaultParameters;
570 # Convert $param array to individual variables
571 $lim = $param['lim'];
572 $conds = $param['conds'];
573 $showIfEmpty = $param['showIfEmpty'];
574 $msgKey = $param['msgKey'];
575 $wrap = $param['wrap'];
576 $flags = $param['flags'];
577 $extraUrlParams = $param['extraUrlParams'];
578
579 $useRequestParams = $param['useRequestParams'];
580 // @phan-suppress-next-line PhanRedundantCondition
581 if ( !is_array( $msgKey ) ) {
582 $msgKey = [ $msgKey ];
583 }
584
585 // ???
586 // @phan-suppress-next-line PhanRedundantCondition
587 if ( $out instanceof OutputPage ) {
588 $context = $out->getContext();
589 } else {
590 $context = RequestContext::getMain();
591 }
592
593 $services = MediaWikiServices::getInstance();
594 // FIXME: Figure out how to inject this
595 $linkRenderer = $services->getLinkRenderer();
596
597 # Insert list of top 50 (or top $lim) items
598 $loglist = new LogEventsList( $context, $linkRenderer, $flags );
599 $pager = new LogPager(
600 $loglist,
601 $types,
602 $user,
603 $page,
604 false,
605 $conds,
606 false,
607 false,
608 false,
609 '',
610 '',
611 0,
612 $services->getLinkBatchFactory(),
613 $services->getActorNormalization()
614 );
615 // @phan-suppress-next-line PhanImpossibleCondition
616 if ( !$useRequestParams ) {
617 # Reset vars that may have been taken from the request
618 $pager->mLimit = 50;
619 $pager->mDefaultLimit = 50;
620 $pager->mOffset = "";
621 $pager->mIsBackwards = false;
622 }
623
624 // @phan-suppress-next-line PhanImpossibleCondition
625 if ( $param['useMaster'] ) {
626 $pager->mDb = $services->getConnectionProvider()->getPrimaryDatabase();
627 }
628 // @phan-suppress-next-line PhanImpossibleCondition
629 if ( isset( $param['offset'] ) ) { # Tell pager to ignore WebRequest offset
630 $pager->setOffset( $param['offset'] );
631 }
632
633 // @phan-suppress-next-line PhanSuspiciousValueComparison
634 if ( $lim > 0 ) {
635 $pager->mLimit = $lim;
636 }
637 // Fetch the log rows and build the HTML if needed
638 $logBody = $pager->getBody();
639 $numRows = $pager->getNumRows();
640
641 $s = '';
642
643 if ( $logBody ) {
644 if ( $msgKey[0] ) {
645 // @phan-suppress-next-line PhanParamTooFewUnpack Non-emptiness checked above
646 $msg = $context->msg( ...$msgKey );
647 if ( $page instanceof PageReference ) {
648 $msg->page( $page );
649 }
650 $s .= $msg->parseAsBlock();
651 }
652 $s .= $loglist->beginLogEventsList() .
653 $logBody .
654 $loglist->endLogEventsList();
655 // add styles for change tags
656 $context->getOutput()->addModuleStyles( 'mediawiki.interface.helpers.styles' );
657 // @phan-suppress-next-line PhanRedundantCondition
658 } elseif ( $showIfEmpty ) {
659 $s = Html::rawElement( 'div', [ 'class' => 'mw-warning-logempty' ],
660 $context->msg( 'logempty' )->parse() );
661 }
662
663 if ( $page instanceof PageReference ) {
664 $titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter();
665 $pageName = $titleFormatter->getPrefixedDBkey( $page );
666 } elseif ( $page != '' ) {
667 $pageName = $page;
668 } else {
669 $pageName = null;
670 }
671
672 if ( $numRows > $pager->mLimit ) { # Show "Full log" link
673 $urlParam = [];
674 if ( $pageName ) {
675 $urlParam['page'] = $pageName;
676 }
677
678 if ( $user != '' ) {
679 $urlParam['user'] = $user;
680 }
681
682 if ( !is_array( $types ) ) { # Make it an array, if it isn't
683 $types = [ $types ];
684 }
685
686 # If there is exactly one log type, we can link to Special:Log?type=foo
687 if ( count( $types ) == 1 ) {
688 $urlParam['type'] = $types[0];
689 }
690
691 // @phan-suppress-next-line PhanSuspiciousValueComparison
692 if ( $extraUrlParams !== false ) {
693 $urlParam = array_merge( $urlParam, $extraUrlParams );
694 }
695
696 $s .= $linkRenderer->makeKnownLink(
697 SpecialPage::getTitleFor( 'Log' ),
698 $context->msg( 'log-fulllog' )->text(),
699 [],
700 $urlParam
701 );
702 }
703
704 if ( $logBody && $msgKey[0] ) {
705 // TODO: The condition above is weird. Should this be done in any other cases?
706 // Or is it always true in practice?
707
708 // Mark as interface language (T60685)
709 $dir = $context->getLanguage()->getDir();
710 $lang = $context->getLanguage()->getHtmlCode();
711 $s = Html::rawElement( 'div', [
712 'class' => "mw-content-$dir",
713 'dir' => $dir,
714 'lang' => $lang,
715 ], $s );
716
717 // Wrap in warning box
718 $s = Html::warningBox(
719 $s,
720 'mw-warning-with-logexcerpt'
721 );
722 }
723
724 // @phan-suppress-next-line PhanSuspiciousValueComparison, PhanRedundantCondition
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 $hookRunner = new HookRunner( $services->getHookContainer() );
731 if ( $hookRunner->onLogEventsListShowLogExtract( $s, $types, $pageName, $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
752 public static function getExcludeClause( $db, $audience = 'public', Authority $performer = null ) {
753 $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogRestrictions );
754
755 if ( $audience != 'public' && $performer === null ) {
756 throw new InvalidArgumentException(
757 'A User object must be given when checking for a user audience.'
758 );
759 }
760
761 // Reset the array, clears extra "where" clauses when $par is used
762 $hiddenLogs = [];
763
764 // Don't show private logs to unprivileged users
765 foreach ( $logRestrictions as $logType => $right ) {
766 if ( $audience == 'public' || !$performer->isAllowed( $right ) ) {
767 $hiddenLogs[] = $logType;
768 }
769 }
770 if ( count( $hiddenLogs ) == 1 ) {
771 return 'log_type != ' . $db->addQuotes( $hiddenLogs[0] );
772 } elseif ( $hiddenLogs ) {
773 return 'log_type NOT IN (' . $db->makeList( $hiddenLogs ) . ')';
774 }
775
776 return false;
777 }
778}
getAuthority()
getContext()
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.
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)
static userCanViewLogType( $type, Authority $performer)
Determine if the current user is allowed to view a particular field of this log row,...
static isDeleted( $row, $field)
Class to simplify the use of log pages.
Definition LogPage.php:44
Store key-value entries in a size-limited in-memory LRU cache.
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
setContext(IContextSource $context)
Group all the pieces relevant to the context of a request into one instance.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
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:56
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition Linker.php:63
Create PSR-3 logger objects.
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.
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:46
Parent class for all special pages.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Module of static functions for generating XML.
Definition Xml.php:37
isGood()
Returns whether the operation completed and didn't have any error or warnings.
Interface for objects which can provide a MediaWiki context on request.
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
This interface represents the authority associated with the current execution context,...
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.