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