MediaWiki master
LogEventsList.php
Go to the documentation of this file.
1<?php
12namespace MediaWiki\Logging;
13
14use InvalidArgumentException;
45use stdClass;
46use UnexpectedValueException;
47use Wikimedia\IPUtils;
52
54 public const NO_ACTION_LINK = 1;
55 public const NO_EXTRA_USER_LINKS = 2;
56 public const USE_CHECKBOXES = 4;
57
59 public $flags;
60
64 protected $showTagEditUI;
65
69 private $linkRenderer;
70
72 private $hookRunner;
73
74 private LogFormatterFactory $logFormatterFactory;
75
77 private $tagsCache;
78
79 private TempUserConfig $tempUserConfig;
80 private ChangeTagsFormatter $changeTagsFormatter;
81
88 public function __construct( $context, $linkRenderer = null, $flags = 0 ) {
89 $this->setContext( $context );
90 $this->flags = $flags;
91 $this->showTagEditUI = ChangeTags::showTagEditingUI( $this->getAuthority() );
92 if ( $linkRenderer instanceof LinkRenderer ) {
93 $this->linkRenderer = $linkRenderer;
94 }
96 $this->hookRunner = new HookRunner( $services->getHookContainer() );
97 $this->logFormatterFactory = $services->getLogFormatterFactory();
98 $this->tagsCache = new MapCacheLRU( 50 );
99 $this->tempUserConfig = $services->getTempUserConfig();
100 $this->changeTagsFormatter = $services->getChangeTagsFormatter();
101 }
102
107 protected function getLinkRenderer() {
108 if ( $this->linkRenderer !== null ) {
109 return $this->linkRenderer;
110 } else {
111 return MediaWikiServices::getInstance()->getLinkRenderer();
112 }
113 }
114
127 public function showOptions( $type = '', $year = 0, $month = 0, $day = 0, $username = '' ) {
128 $formDescriptor = [];
129 $typesByName = $this->getTypeMenuOptions();
130
131 // Basic selectors
132 $formDescriptor['type'] = [
133 'class' => HTMLSelectField::class,
134 'name' => 'type',
135 'options' => array_flip( $typesByName ),
136 'default' => '',
137 ];
138 $formDescriptor['user'] = [
139 'class' => HTMLUserTextField::class,
140 'label-message' => 'specialloguserlabel',
141 'name' => 'user',
142 'ipallowed' => true,
143 'iprange' => true,
144 'external' => true,
145 ];
146 $formDescriptor['page'] = [
147 'class' => HTMLTitleTextField::class,
148 'label-message' => 'speciallogtitlelabel',
149 'name' => 'page',
150 'required' => false,
151 ];
152
153 // Title pattern, if allowed
154 if ( !$this->getConfig()->get( MainConfigNames::MiserMode ) ) {
155 $formDescriptor['pattern'] = [
156 'type' => 'check',
157 'label-message' => 'log-title-wildcard',
158 'name' => 'pattern',
159 ];
160 }
161
162 // Add extra inputs if any
163 foreach ( $this->getExtraInputsDesc( $typesByName, $username ) as $key => $field ) {
164 $formDescriptor[$key] = $field;
165 }
166
167 // Date menu
168 $formDescriptor['date'] = [
169 'type' => 'date',
170 'label-message' => 'date',
171 'default' => $year && $month && $day ? sprintf( "%04d-%02d-%02d", $year, $month, $day ) : '',
172 ];
173
174 // Tag filter
175 $formDescriptor['tagfilter'] = [
176 'type' => 'tagfilter',
177 'name' => 'tagfilter',
178 'label-message' => 'tag-filter',
179 ];
180 $formDescriptor['tagInvert'] = [
181 'type' => 'check',
182 'name' => 'tagInvert',
183 'label-message' => 'invert',
184 'hide-if' => [ '===', 'tagfilter', '' ],
185 ];
186
187 // Filter checkboxes to hide single log types
188 $formDescriptor['filters'] = $this->getFiltersDesc();
189
190 // Action filters
191 $allowedActions = $this->getConfig()->get( MainConfigNames::ActionFilteredLogs );
192 foreach ( $typesByName as $type => $_ ) {
193 if ( isset( $allowedActions[$type] ) ) {
194 $formDescriptor["subtype-$type"] = $this->getActionSelectorDesc( $type, $allowedActions[$type] );
195 }
196 }
197
198 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
199 $htmlForm
200 ->setTitle( SpecialPage::getTitleFor( 'Log' ) ) // Remove subpage
201 ->setSubmitTextMsg( 'logeventslist-submit' )
202 ->setMethod( 'GET' )
203 ->setWrapperLegendMsg( 'log' )
204 ->setFormIdentifier( 'logeventslist', true ) // T321154
205 // Set callback for data validation and log type description.
206 ->setSubmitCallback( static function ( $formData, $form ) {
207 $form->addPreHtml(
208 ( new LogPage( $formData['type'] ) )->getDescription()
209 ->setContext( $form->getContext() )->parseAsBlock()
210 );
211 return true;
212 } );
213
214 $result = $htmlForm->prepareForm()->trySubmit();
215 $htmlForm->displayForm( $result );
216 return $result === true || ( $result instanceof Status && $result->isGood() );
217 }
218
222 private function getFiltersDesc() {
223 $optionsMsg = [];
224 $filters = $this->getConfig()->get( MainConfigNames::FilterLogTypes );
225 foreach ( $filters as $type => $val ) {
226 $optionsMsg["logeventslist-{$type}-log"] = $type;
227 }
228 return [
229 'class' => HTMLMultiSelectField::class,
230 'label-message' => 'logeventslist-more-filters',
231 'flatlist' => true,
232 'options-messages' => $optionsMsg,
233 'default' => array_keys( array_intersect( $filters, [ false ] ) ),
234 // Only shown when displaying all logs
235 'hide-if-nojs' => [ '!==', 'type', '' ],
236 ];
237 }
238
242 private function getTypeMenuOptions() {
243 $typesByName = [];
244 // Load the log names
245 foreach ( LogPage::validTypes() as $type ) {
246 $page = new LogPage( $type );
247 $pageText = $page->getName()->text();
248 if ( in_array( $pageText, $typesByName ) ) {
249 LoggerFactory::getInstance( 'translation-problem' )->error(
250 'The log type {log_type_one} has the same translation as {log_type_two} for {lang}. ' .
251 '{log_type_one} will not be displayed in the drop down menu on Special:Log.',
252 [
253 'log_type_one' => $type,
254 'log_type_two' => array_search( $pageText, $typesByName ),
255 'lang' => $this->getLanguage()->getCode(),
256 ]
257 );
258 continue;
259 }
260 if ( $this->getAuthority()->isAllowed( $page->getRestriction() ) ) {
261 $typesByName[$type] = $pageText;
262 }
263 }
264
265 asort( $typesByName );
266
267 // Always put "All public logs" on top
268 $public = $typesByName[''];
269 unset( $typesByName[''] );
270 $typesByName = [ '' => $public ] + $typesByName;
271
272 return $typesByName;
273 }
274
280 private function getExtraInputsDesc( $typesByName, $username ) {
281 $formDescriptor = [];
282
283 if ( isset( $typesByName['suppress'] ) ) {
284 $formDescriptor['extra-suppress'] = [
285 'type' => 'text',
286 'label-message' => 'revdelete-offender',
287 'name' => 'offender',
288 'hide-if-nojs' => [ '!==', 'type', 'suppress' ],
289 ];
290 }
291
292 if ( isset( $typesByName['newusers'] ) ) {
293 // Add option to exclude/include temporary account creations in results,
294 // excluding them by default.
295 if ( $this->tempUserConfig->isKnown() ) {
296 $formDescriptor['extra-newusers'] = [
297 'type' => 'check',
298 'label-message' => 'newusers-excludetempacct',
299 'name' => 'excludetempacct',
300 'default' => !$this->tempUserConfig->isTempName( $username ),
301 'hide-if' => [ 'AND',
302 [ '!==', 'type', 'newusers' ],
303 [ '!==', 'type', '' ],
304 ],
305 ];
306 }
307 }
308
309 // Allow extensions to add an extra input into the descriptor array.
310 // This is a bit weird, because this hook used to be called only for the selected type.
311 foreach ( $typesByName as $type => $_ ) {
312 $extraInputs = [];
313 $unused = ''; // Deprecated since 1.32, removed in 1.41
314 $this->hookRunner->onLogEventsListGetExtraInputs( $type, $this, $unused, $extraInputs );
315 if ( $extraInputs ) {
316 // Single inputs (assoc. array of attributes) and multiple inputs (list of
317 // the aforementioned assoc. arrays) are supported.
318 if ( !array_is_list( $extraInputs ) ) {
319 $extraInputs = [ $extraInputs ];
320 }
321 foreach ( $extraInputs as $i => $input ) {
322 if ( isset( $input['hide-if-nojs'] ) ) {
323 $input['hide-if-nojs'] = [ 'OR',
324 $input['hide-if-nojs'],
325 [ '!==', 'type', $type ]
326 ];
327 } else {
328 $input['hide-if-nojs'] = [ '!==', 'type', $type ];
329 }
330 $formDescriptor["extra-$type-$i"] = $input;
331 }
332 }
333 }
334
335 return $formDescriptor;
336 }
337
344 private function getActionSelectorDesc( $type, $actions ) {
345 $actionOptions = [ 'log-action-filter-all' => '' ];
346
347 foreach ( $actions as $value => $_ ) {
348 $msgKey = "log-action-filter-$type-$value";
349 $actionOptions[ $msgKey ] = $value;
350 }
351
352 return [
353 'class' => HTMLSelectField::class,
354 'name' => 'subtype',
355 'id' => 'mw-log-action-filter-' . $type,
356 'options-messages' => $actionOptions,
357 'label-message' => 'log-action-filter-' . $type,
358 /*
359 The form on Special:Log has a very long list of 'hide-if' fields (which allow picking the log
360 subtype depending on the selected type), 10 of them just in core, 20+ with some extensions.
361
362 Normally all 'hide-if' fields are shown to no-JS users, but having this many of them is not very
363 usable. Hide them all; they will be shown only after selecting a type and submitting the form.
364
365 This also lets us have multiple fields with `'name' => 'subtype',` without breaking form submission,
366 since only one of them will be visible and enabled and submitted with the form.
367 */
368 'hide-if-nojs' => [ '!==', 'type', $type ],
369 ];
370 }
371
375 public function beginLogEventsList() {
376 return "<ul class='mw-logevent-loglines'>\n";
377 }
378
382 public function endLogEventsList() {
383 return "</ul>\n";
384 }
385
390 public function logLine( $row ) {
391 $entry = DatabaseLogEntry::newFromRow( $row );
392 $formatter = $this->logFormatterFactory->newFromEntry( $entry );
393 $formatter->setContext( $this->getContext() );
394 $formatter->setShowUserToolLinks( !( $this->flags & self::NO_EXTRA_USER_LINKS ) );
395
396 $time = $this->getLanguage()->userTimeAndDate(
397 $entry->getTimestamp(),
398 $this->getUser()
399 );
400 // Link the time text to the specific log entry, see T207562
401 $timeLink = $this->getLinkRenderer()->makeKnownLink(
403 $time,
404 [],
405 [ 'logid' => $entry->getId() ]
406 );
407
408 $action = $formatter->getActionText();
409
410 if ( $this->flags & self::NO_ACTION_LINK ) {
411 $revert = '';
412 } else {
413 $revert = $formatter->getActionLinks();
414 if ( $revert != '' ) {
415 $revert = '<span class="mw-logevent-actionlink">' . $revert . '</span>';
416 }
417 }
418
419 $comment = $formatter->getComment();
420
421 // Some user can hide log items and have review links
422 $del = $this->getShowHideLinks( $row );
423
424 // Any tags...
425 [ $tagDisplay, $newClasses ] = $this->tagsCache->getWithSetCallback(
426 $this->tagsCache->makeKey(
427 $row->ts_tags ?? '',
428 $this->getUser()->getName(),
429 $this->getLanguage()->getCode()
430 ),
431 fn () => $this->changeTagsFormatter->formatTagsAsSummaryList(
432 $row->ts_tags,
433 $this->getContext(),
434 $this->getAuthority()
435 )
436 );
437 $classes = [ 'mw-logline-' . $entry->getType(), ...$newClasses ];
438 $attribs = [
439 'data-mw-logid' => $entry->getId(),
440 'data-mw-logaction' => $entry->getFullType(),
441 ];
442 $ret = "$del $timeLink $action $comment $revert $tagDisplay";
443
444 // Let extensions add data
445 $ret .= Html::openElement( 'span', [ 'class' => 'mw-logevent-tool' ] );
446 // FIXME: this hook assumes that callers will only append to $ret value.
447 // In future this hook should be replaced with a new hook: LogTools that has a
448 // hook interface consistent with DiffTools and HistoryTools.
449 $this->hookRunner->onLogEventsListLineEnding( $this, $ret, $entry, $classes, $attribs );
450 $attribs = array_filter( $attribs,
451 Sanitizer::isReservedDataAttribute( ... ),
452 ARRAY_FILTER_USE_KEY
453 );
454 $ret .= Html::closeElement( 'span' );
455 $attribs['class'] = $classes;
456
457 return Html::rawElement( 'li', $attribs, $ret ) . "\n";
458 }
459
464 private function getShowHideLinks( $row ) {
465 // We don't want to see the links and
466 if ( $this->flags == self::NO_ACTION_LINK ) {
467 return '';
468 }
469
470 // If change tag editing is available to this user, return the checkbox
471 if ( $this->flags & self::USE_CHECKBOXES && $this->showTagEditUI ) {
472 return Html::check( 'ids[' . $row->log_id . ']', false );
473 }
474
475 // no one can hide items from the suppress log.
476 if ( $row->log_type == 'suppress' ) {
477 return '';
478 }
479
480 $del = '';
481 $authority = $this->getAuthority();
482 // Don't show useless checkbox to people who cannot hide log entries
483 if ( $authority->isAllowed( 'deletedhistory' ) ) {
484 $canHide = $authority->isAllowed( 'deletelogentry' );
485 $canViewSuppressedOnly = $authority->isAllowed( 'viewsuppressed' ) &&
486 !$authority->isAllowed( 'suppressrevision' );
487 $entryIsSuppressed = self::isDeleted( $row, LogPage::DELETED_RESTRICTED );
488 $canViewThisSuppressedEntry = $canViewSuppressedOnly && $entryIsSuppressed;
489 if ( $row->log_deleted || $canHide ) {
490 // Show checkboxes instead of links.
491 if ( $canHide && $this->flags & self::USE_CHECKBOXES && !$canViewThisSuppressedEntry ) {
492 // If event was hidden from sysops
493 if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $authority ) ) {
494 $del = Html::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] );
495 } else {
496 $del = Html::check( 'ids[' . $row->log_id . ']', false );
497 }
498 } else {
499 // If event was hidden from sysops
500 if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $authority ) ) {
501 $del = Linker::revDeleteLinkDisabled( $canHide );
502 } else {
503 $query = [
504 'target' => SpecialPage::getTitleFor( 'Log', $row->log_type )->getPrefixedDBkey(),
505 'type' => 'logging',
506 'ids' => $row->log_id,
507 ];
508 $del = Linker::revDeleteLink(
509 $query,
510 $entryIsSuppressed,
511 $canHide && !$canViewThisSuppressedEntry
512 );
513 }
514 }
515 }
516 }
517
518 return $del;
519 }
520
527 public static function typeAction( $row, $type, $action ) {
528 $match = is_array( $type ) ?
529 in_array( $row->log_type, $type ) : $row->log_type == $type;
530 if ( $match ) {
531 $match = is_array( $action ) ?
532 in_array( $row->log_action, $action ) : $row->log_action == $action;
533 }
534
535 return $match;
536 }
537
547 public static function userCan( $row, $field, Authority $performer ) {
548 return self::userCanBitfield( $row->log_deleted, $field, $performer ) &&
549 self::userCanViewLogType( $row->log_type, $performer );
550 }
551
561 public static function userCanBitfield( $bitfield, $field, Authority $performer ) {
562 if ( $bitfield & $field ) {
563 if ( $bitfield & LogPage::DELETED_RESTRICTED ) {
564 return $performer->isAllowedAny( 'suppressrevision', 'viewsuppressed' );
565 } else {
566 return $performer->isAllowed( 'deletedhistory' );
567 }
568 }
569 return true;
570 }
571
580 public static function userCanViewLogType( $type, Authority $performer ) {
581 $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogRestrictions );
582 if ( isset( $logRestrictions[$type] ) && !$performer->isAllowed( $logRestrictions[$type] ) ) {
583 return false;
584 }
585 return true;
586 }
587
593 public static function isDeleted( $row, $field ) {
594 return ( $row->log_deleted & $field ) == $field;
595 }
596
628 public static function showLogExtract(
629 &$out, $types = [], $pages = '', $user = '', $param = [], ?IContextSource $context = null
630 ) {
631 $defaultParameters = [
632 'lim' => 25,
633 'conds' => [],
634 'showIfEmpty' => true,
635 'msgKey' => [ '' ],
636 'wrap' => "$1",
637 'flags' => 0,
638 'useRequestParams' => false,
639 'useMaster' => false,
640 'extraUrlParams' => false,
641 'footerHtmlItems' => []
642 ];
643 # The + operator appends elements of remaining keys from the right
644 # handed array to the left handed, whereas duplicated keys are NOT overwritten.
645 $param += $defaultParameters;
646 # Convert $param array to individual variables
647 $lim = $param['lim'];
648 $conds = $param['conds'];
649 $showIfEmpty = $param['showIfEmpty'];
650 $msgKey = $param['msgKey'];
651 $wrap = $param['wrap'];
652 $flags = $param['flags'];
653 $extraUrlParams = $param['extraUrlParams'];
654
655 $useRequestParams = $param['useRequestParams'];
656 if ( !is_array( $msgKey ) ) {
657 $msgKey = [ $msgKey ];
658 }
659
660 // @phan-suppress-next-line PhanRedundantCondition
661 if ( $out instanceof OutputPage ) {
662 if ( $context ) {
663 throw new InvalidArgumentException( 'When passing $context, do not pass OutputPage as $out' );
664 }
665 $context = $out->getContext();
666 }
667 if ( !$context ) {
668 $context = RequestContext::getMain();
669 }
670
671 $services = MediaWikiServices::getInstance();
672 // FIXME: Figure out how to inject this
673 $linkRenderer = $services->getLinkRenderer();
674
675 if ( !is_array( $pages ) ) {
676 $pages = [ $pages ];
677 }
678
679 # Insert list of top 50 (or top $lim) items
680 $loglist = new LogEventsList( $context, $linkRenderer, $flags );
681 $pager = new LogPager(
682 $loglist,
683 $types,
684 $user,
685 $pages,
686 false,
687 $conds,
688 false,
689 false,
690 false,
691 '',
692 '',
693 0,
694 $services->getLinkBatchFactory(),
695 $services->getActorNormalization(),
696 $services->getLogFormatterFactory()
697 );
698 if ( !$useRequestParams ) {
699 # Reset vars that may have been taken from the request
700 $pager->mLimit = 50;
701 $pager->mDefaultLimit = 50;
702 $pager->mOffset = "";
703 $pager->mIsBackwards = false;
704 }
705
706 if ( $param['useMaster'] ) {
707 $pager->mDb = $services->getConnectionProvider()->getPrimaryDatabase();
708 }
709
710 if ( isset( $param['offset'] ) ) { # Tell pager to ignore WebRequest offset
711 $pager->setOffset( $param['offset'] );
712 }
713
714 if ( $lim > 0 ) {
715 $pager->mLimit = $lim;
716 }
717 // Fetch the log rows and build the HTML if needed
718 $logBody = $pager->getBody();
719 $numRows = $pager->getNumRows();
720
721 $s = '';
722 $footerHtmlItems = [];
723
724 if ( $logBody ) {
725 if ( $msgKey[0] ) {
726 $msg = $context->msg( ...$msgKey );
727 if ( ( $pages[0] ?? null ) instanceof PageReference ) {
728 $msg->page( $pages[0] );
729 }
730 $s .= $msg->parseAsBlock();
731 }
732 $s .= $loglist->beginLogEventsList() .
733 $logBody .
734 $loglist->endLogEventsList();
735 // add styles for change tags
736 $context->getOutput()->addModuleStyles( 'mediawiki.interface.helpers.styles' );
737 } elseif ( $showIfEmpty ) {
738 $s = Html::rawElement( 'div', [ 'class' => 'mw-warning-logempty' ],
739 $context->msg( 'logempty' )->parse() );
740 }
741
742 $pageNames = [];
743 foreach ( $pages as $page ) {
744 if ( $page instanceof PageReference ) {
745 $titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter();
746 $pageNames[] = $titleFormatter->getPrefixedDBkey( $page );
747 } elseif ( $page != '' ) {
748 $pageNames[] = $page;
749 }
750 }
751
752 if ( $numRows > $pager->mLimit ) { # Show "Full log" link
753 $urlParam = [];
754 if ( $pageNames ) {
755 $urlParam['page'] = count( $pageNames ) > 1 ? $pageNames : $pageNames[0];
756 }
757
758 if ( $user != '' ) {
759 $urlParam['user'] = $user;
760 }
761
762 if ( !is_array( $types ) ) { # Make it an array, if it isn't
763 $types = [ $types ];
764 }
765
766 # If there is exactly one log type, we can link to Special:Log?type=foo
767 if ( count( $types ) == 1 ) {
768 $urlParam['type'] = $types[0];
769 }
770
771 if ( $extraUrlParams !== false ) {
772 $urlParam = array_merge( $urlParam, $extraUrlParams );
773 }
774
775 $footerHtmlItems[] = $linkRenderer->makeKnownLink(
776 SpecialPage::getTitleFor( 'Log' ),
777 $context->msg( 'log-fulllog' )->text(),
778 [],
779 $urlParam
780 );
781 }
782 if ( $param['footerHtmlItems'] ) {
783 $footerHtmlItems = array_merge( $footerHtmlItems, $param['footerHtmlItems'] );
784 }
785 if ( $logBody && $footerHtmlItems ) {
786 $s .= '<ul class="mw-logevent-footer">';
787 foreach ( $footerHtmlItems as $item ) {
788 $s .= Html::rawElement( 'li', [], $item );
789 }
790 $s .= '</ul>';
791 }
792
793 if ( $logBody && $msgKey[0] ) {
794 // TODO: The condition above is weird. Should this be done in any other cases?
795 // Or is it always true in practice?
796
797 // Mark as interface language (T60685)
798 $dir = $context->getLanguage()->getDir();
799 $lang = $context->getLanguage()->getHtmlCode();
800 $s = Html::rawElement( 'div', [
801 'class' => "mw-content-$dir",
802 'dir' => $dir,
803 'lang' => $lang,
804 ], $s );
805
806 // Wrap in warning box
807 $s = Html::warningBox(
808 $s,
809 'mw-warning-with-logexcerpt'
810 );
811 // Add styles for warning box
812 $context->getOutput()->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
813 }
814
815 if ( $wrap != '' ) { // Wrap message in html
816 $s = str_replace( '$1', $s, $wrap );
817 }
818
819 /* hook can return false, if we don't want the message to be emitted (Wikia BugId:7093) */
820 $hookRunner = new HookRunner( $services->getHookContainer() );
821 if ( $hookRunner->onLogEventsListShowLogExtract(
822 $s, $types, $pageNames, $user, $param
823 ) ) {
824 // $out can be either an OutputPage object or a String-by-reference
825 if ( $out instanceof OutputPage ) {
826 $out->addHTML( $s );
827 } else {
828 $out = $s;
829 }
830 }
831
832 return $numRows;
833 }
834
844 public static function getExcludeClause( $db, $audience = 'public', ?Authority $performer = null ) {
845 $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogRestrictions );
846
847 if ( $audience != 'public' && $performer === null ) {
848 throw new InvalidArgumentException(
849 'A User object must be given when checking for a user audience.'
850 );
851 }
852
853 // Reset the array, clears extra "where" clauses when $par is used
854 $hiddenLogs = [];
855
856 // Don't show private logs to unprivileged users
857 foreach ( $logRestrictions as $logType => $right ) {
858 if ( $audience == 'public' || !$performer->isAllowed( $right ) ) {
859 $hiddenLogs[] = $logType;
860 }
861 }
862 if ( count( $hiddenLogs ) == 1 ) {
863 return 'log_type != ' . $db->addQuotes( $hiddenLogs[0] );
864 } elseif ( $hiddenLogs ) {
865 return 'log_type NOT IN (' . $db->makeList( $hiddenLogs ) . ')';
866 }
867
868 return false;
869 }
870
897 public static function getBlockLogWarningBox(
898 DatabaseBlockStore $blockStore,
899 NamespaceInfo $namespaceInfo,
900 MessageLocalizer $localizer,
901 LinkRenderer $linkRenderer,
902 $user,
903 ?Title $title,
904 array|callable $additionalParams = [],
905 ?IContextSource $context = null
906 ) {
907 if ( !$user ) {
908 return null;
909 }
910
911 // For IP ranges we must give DatabaseBlock::newFromTarget the CIDR string
912 // and not a user object
913 $userOrRange = IPUtils::isValidRange( $user->getName() ) ? $user->getName() : $user;
914 $blocks = $blockStore->newListFromTarget(
915 // Do not expose the autoblocks, since that may lead to a leak of accounts' IPs,
916 // and also that will display a totally irrelevant log entry as a current block.
917 $userOrRange, $userOrRange, false, DatabaseBlockStore::AUTO_NONE
918 );
919 if ( !count( $blocks ) ) {
920 return null;
921 }
922
923 $isAnon = !$user->isRegistered();
924 $appliesToTitle = false;
925 $logTargetPages = [];
926 $sitewide = false;
927 $matchingIpFound = false;
928 $newestBlockTimestamp = null;
929 $blockId = null;
930 foreach ( $blocks as $block ) {
931 if ( $title === null || $block->appliesToTitle( $title ) ) {
932 $appliesToTitle = true;
933 }
934 $blockTargetName = $block->getTargetName();
935 $logTargetPages[] =
936 $namespaceInfo->getCanonicalName( NS_USER ) . ':' . $blockTargetName;
937 if ( $block->isSitewide() ) {
938 $sitewide = true;
939 }
940
941 // Track the most recent active block. Prefer newer timestamps; if two blocks
942 // share the same timestamp, fall back to the larger block ID to break ties.
943 // This avoids issues where overridden blocks may reuse smaller IDs.
944 //
945 // IP blocks are a bit tricky here:
946 // - Prioritize direct blocks where $user and $block share the same IP.
947 // - The same IP can be directly blocked multiple times, in which case
948 // the timestamp priority logic should work the same way.
949 // Once an exact IP match is found, it takes precedence over range blocks
950 // even if the range is newer or has a bigger ID, since it represents a more
951 // specific and directly applicable restriction.
952 $isExactIpMatch = $isAnon && $user->getName() === $blockTargetName;
953 if ( ( $isExactIpMatch || !$matchingIpFound ) && (
954 $newestBlockTimestamp === null ||
955 $block->getTimestamp() > $newestBlockTimestamp ||
956 ( $block->getTimestamp() === $newestBlockTimestamp && $block->getId() > $blockId )
957 ) ) {
958 $newestBlockTimestamp = $block->getTimestamp();
959 $blockId = $block->getId();
960
961 // If this block is an exact IP match, mark it so future range blocks don't
962 // override it, regardless of newer timestamps or bigger IDs
963 if ( $isExactIpMatch ) {
964 $matchingIpFound = true;
965 }
966 }
967 }
968
969 // Show nothing if no active block applies to the given title
970 // (practically, whether the target user is allowed to edit their user/user_talk page)
971 if ( !$appliesToTitle ) {
972 return null;
973 }
974
975 if ( count( $blocks ) === 1 ) {
976 if ( $isAnon ) {
977 $msgKey = $sitewide ?
978 'blocked-notice-logextract-anon' :
979 'blocked-notice-logextract-anon-partial';
980 } else {
981 $msgKey = $sitewide ?
982 'blocked-notice-logextract' :
983 'blocked-notice-logextract-partial';
984 }
985 } else {
986 if ( $isAnon ) {
987 $msgKey = 'blocked-notice-logextract-anon-multi';
988 } else {
989 $msgKey = 'blocked-notice-logextract-multi';
990 }
991 }
992
993 // While $blocks already contains only active blocks, LogEventsList::showLogExtract
994 // by default fetches the most recent log entries regardless of block status.
995 // To ensure the newest ACTIVE block log is shown, add explicit LIKE conditions
996 // here to filter block log entries.
997 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
998 $orCondsForBlockId = [];
999 $orCondsForBlockId[] = $dbr->expr(
1000 // Before MW 1.44, log_params did not contain blockId. Always include such older
1001 // log entries for backwards compatibility
1002 'log_params',
1003 IExpression::NOT_LIKE,
1004 new LikeValue( new LikeMatch( '%"blockId"%' ) )
1005 );
1006 if ( $blockId !== null ) {
1007 $orCondsForBlockId[] = $dbr->expr(
1008 'log_params',
1009 IExpression::LIKE,
1010 new LikeValue( new LikeMatch( "%\"blockId\";i:$blockId;%" ) )
1011 );
1012 }
1013 $conds = [ $dbr->makeList( $orCondsForBlockId, LIST_OR ) ];
1014
1015 $params = [
1016 'lim' => 1,
1017 'conds' => $conds,
1018 'showIfEmpty' => false,
1019 'msgKey' => [
1020 $msgKey,
1021 $user->getName(), // Support GENDER in $msgKey
1022 count( $blocks )
1023 ],
1024 'offset' => '' // Don't use WebRequest parameter offset
1025 ];
1026
1027 if ( count( $blocks ) > 1 ) {
1028 $params['footerHtmlItems'] = [
1029 $linkRenderer->makeKnownLink(
1030 SpecialPage::getTitleFor( 'BlockList' ),
1031 $localizer->msg( 'blocked-notice-list-link' )->text(),
1032 [],
1033 [ 'wpTarget' => $user->getName() ]
1034 ),
1035 ];
1036 }
1037
1038 if ( is_callable( $additionalParams ) ) {
1039 $extraParams = $additionalParams( [
1040 // Add values to this callback array depending on the needs
1041 // Don't forget to also update the method documentation
1042 'blocks' => $blocks,
1043 'sitewide' => $sitewide,
1044 'logTargetPages' => $logTargetPages
1045 ] );
1046 if ( !is_array( $extraParams ) ) {
1047 throw new UnexpectedValueException(
1048 'The callable $additionalParams must return an array, ' . gettype( $extraParams ) . ' given'
1049 );
1050 }
1051 $params += $extraParams;
1052 } else {
1053 $params += $additionalParams;
1054 }
1055
1056 $outString = '';
1057 self::showLogExtract( $outString, 'block', $logTargetPages, '', $params, $context );
1058 return $outString ?: null;
1059 }
1060}
1061
1063class_alias( LogEventsList::class, 'LogEventsList' );
const NS_USER
Definition Defines.php:53
const LIST_OR
Definition Defines.php:33
newListFromTarget( $specificTarget, $vagueTarget=null, $fromPrimary=false, $auto=self::AUTO_ALL)
This is similar to DatabaseBlockStore::newFromTarget, but it returns all the relevant blocks.
Formats change tags for display in HTML and use filter dropdown menus.
Recent changes tagging.
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:214
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.1.35bool|void True or no return value to...
This class is a collection of static functions that serve two purposes:
Definition Html.php:44
Class that generates HTML for internal links.
makeKnownLink( $target, $text=null, array $extraAttribs=[], array $query=[])
Make a link that's styled as if the target page exists (usually a "blue link", although the styling m...
Some internal bits split of from Skin.php.
Definition Linker.php:48
Create PSR-3 logger objects.
static newFromRow( $row, string|false $wikiId=WikiAwareEntity::LOCAL)
Constructs new LogEntry from database result row.
static isDeleted( $row, $field)
static getExcludeClause( $db, $audience='public', ?Authority $performer=null)
SQL clause to skip forbidden log types for this user.
static getBlockLogWarningBox(DatabaseBlockStore $blockStore, NamespaceInfo $namespaceInfo, MessageLocalizer $localizer, LinkRenderer $linkRenderer, $user, ?Title $title, array|callable $additionalParams=[], ?IContextSource $context=null)
showOptions( $type='', $year=0, $month=0, $day=0, $username='')
Show options for the log list.
__construct( $context, $linkRenderer=null, $flags=0)
static typeAction( $row, $type, $action)
static userCanBitfield( $bitfield, $field, Authority $performer)
Determine if the current user is allowed to view a particular field of this log row,...
static showLogExtract(&$out, $types=[], $pages='', $user='', $param=[], ?IContextSource $context=null)
Show log extract.
static userCanViewLogType( $type, Authority $performer)
Determine if the current user is allowed to view a particular field of this log row,...
static userCan( $row, $field, Authority $performer)
Determine if the current user is allowed to view a particular field of this log row,...
Class to simplify the use of log pages.
Definition LogPage.php:34
static validTypes()
Get the list of valid log types.
Definition LogPage.php:207
A class containing constants representing the names of configuration variables.
const LogRestrictions
Name constant for the LogRestrictions setting, for use with Config::get()
const ActionFilteredLogs
Name constant for the ActionFilteredLogs setting, for use with Config::get()
const FilterLogTypes
Name constant for the FilterLogTypes setting, for use with Config::get()
const MiserMode
Name constant for the MiserMode setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
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:34
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form,...
Parent class for all special pages.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
static getTitleValueFor( $name, $subpage=false, $fragment='')
Get a localised TitleValue object for a specified special page name.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
getCanonicalName(int $index)
Returns the canonical (English) name for a given index.
Represents a title within MediaWiki.
Definition Title.php:69
isGood()
Returns whether the operation completed and didn't have any error or warnings.
Store key-value entries in a size-limited in-memory LRU cache.
Used by Database::buildLike() to represent characters that have special meaning in SQL LIKE clauses a...
Definition LikeMatch.php:10
Content of like value.
Definition LikeValue.php:14
Interface for objects which can provide a MediaWiki context on request.
Interface for localizing messages in MediaWiki.
msg( $key,... $params)
This is the method for getting translated interface messages.
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:23
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.
Interface for temporary user creation config and name matching.
Interface for objects representing user identity.