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