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