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