MediaWiki master
LogEventsList.php
Go to the documentation of this file.
1<?php
12namespace MediaWiki\Logging;
13
14use InvalidArgumentException;
34use MediaWiki\Pager\LogPager;
44use stdClass;
45use UnexpectedValueException;
46use Wikimedia\IPUtils;
51
53 public const NO_ACTION_LINK = 1;
54 public const NO_EXTRA_USER_LINKS = 2;
55 public const USE_CHECKBOXES = 4;
56
58 public $flags;
59
63 protected $showTagEditUI;
64
68 private $linkRenderer;
69
71 private $hookRunner;
72
73 private LogFormatterFactory $logFormatterFactory;
74
76 private $tagsCache;
77
78 private TempUserConfig $tempUserConfig;
79
86 public function __construct( $context, $linkRenderer = null, $flags = 0 ) {
87 $this->setContext( $context );
88 $this->flags = $flags;
89 $this->showTagEditUI = ChangeTags::showTagEditingUI( $this->getAuthority() );
90 if ( $linkRenderer instanceof LinkRenderer ) {
91 $this->linkRenderer = $linkRenderer;
92 }
94 $this->hookRunner = new HookRunner( $services->getHookContainer() );
95 $this->logFormatterFactory = $services->getLogFormatterFactory();
96 $this->tagsCache = new MapCacheLRU( 50 );
97 $this->tempUserConfig = $services->getTempUserConfig();
98 }
99
104 protected function getLinkRenderer() {
105 if ( $this->linkRenderer !== null ) {
106 return $this->linkRenderer;
107 } else {
108 return MediaWikiServices::getInstance()->getLinkRenderer();
109 }
110 }
111
124 public function showOptions( $type = '', $year = 0, $month = 0, $day = 0, $username = '' ) {
125 $formDescriptor = [];
126
127 // Basic selectors
128 $formDescriptor['type'] = $this->getTypeMenuDesc();
129 $formDescriptor['user'] = [
130 'class' => HTMLUserTextField::class,
131 'label-message' => 'specialloguserlabel',
132 'name' => 'user',
133 'ipallowed' => true,
134 'iprange' => true,
135 'external' => true,
136 ];
137 $formDescriptor['page'] = [
138 'class' => HTMLTitleTextField::class,
139 'label-message' => 'speciallogtitlelabel',
140 'name' => 'page',
141 'required' => false,
142 ];
143
144 // Title pattern, if allowed
145 if ( !$this->getConfig()->get( MainConfigNames::MiserMode ) ) {
146 $formDescriptor['pattern'] = [
147 'type' => 'check',
148 'label-message' => 'log-title-wildcard',
149 'name' => 'pattern',
150 ];
151 }
152
153 // Add extra inputs if any
154 $extraInputsDescriptor = $this->getExtraInputsDesc( $type, $username );
155
156 // Single inputs (array of attributes) and multiple inputs (array of arrays)
157 // are supported. Distinguish between the two by checking if the first element
158 // is an array or not.
159 if ( $extraInputsDescriptor ) {
160 if ( isset( $extraInputsDescriptor[0] ) && is_array( $extraInputsDescriptor[0] ) ) {
161 foreach ( $extraInputsDescriptor as $i => $input ) {
162 $formDescriptor[ 'extra_' . $i ] = $input;
163 }
164 } else {
165 $formDescriptor[ 'extra' ] = $extraInputsDescriptor;
166 }
167 }
168
169 // Date menu
170 $formDescriptor['date'] = [
171 'type' => 'date',
172 'label-message' => 'date',
173 'default' => $year && $month && $day ? sprintf( "%04d-%02d-%02d", $year, $month, $day ) : '',
174 ];
175
176 // Tag filter
177 $formDescriptor['tagfilter'] = [
178 'type' => 'tagfilter',
179 'name' => 'tagfilter',
180 'label-message' => 'tag-filter',
181 ];
182 $formDescriptor['tagInvert'] = [
183 'type' => 'check',
184 'name' => 'tagInvert',
185 'label-message' => 'invert',
186 'hide-if' => [ '===', 'tagfilter', '' ],
187 ];
188
189 // Filter checkboxes, when work on all logs
190 if ( $type === '' ) {
191 $formDescriptor['filters'] = $this->getFiltersDesc();
192 }
193
194 // Action filter
195 $allowedActions = $this->getConfig()->get( MainConfigNames::ActionFilteredLogs );
196 if ( isset( $allowedActions[$type] ) ) {
197 $formDescriptor['subtype'] = $this->getActionSelectorDesc( $type, $allowedActions[$type] );
198 }
199
200 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
201 $htmlForm
202 ->setTitle( SpecialPage::getTitleFor( 'Log' ) ) // Remove subpage
203 ->setSubmitTextMsg( 'logeventslist-submit' )
204 ->setMethod( 'GET' )
205 ->setWrapperLegendMsg( 'log' )
206 ->setFormIdentifier( 'logeventslist', true ) // T321154
207 // Set callback for data validation and log type description.
208 ->setSubmitCallback( static function ( $formData, $form ) {
209 $form->addPreHtml(
210 ( new LogPage( $formData['type'] ) )->getDescription()
211 ->setContext( $form->getContext() )->parseAsBlock()
212 );
213 return true;
214 } );
215
216 $result = $htmlForm->prepareForm()->trySubmit();
217 $htmlForm->displayForm( $result );
218 return $result === true || ( $result instanceof Status && $result->isGood() );
219 }
220
224 private function getFiltersDesc() {
225 $optionsMsg = [];
226 $filters = $this->getConfig()->get( MainConfigNames::FilterLogTypes );
227 foreach ( $filters as $type => $val ) {
228 $optionsMsg["logeventslist-{$type}-log"] = $type;
229 }
230 return [
231 'class' => HTMLMultiSelectField::class,
232 'label-message' => 'logeventslist-more-filters',
233 'flatlist' => true,
234 'options-messages' => $optionsMsg,
235 'default' => array_keys( array_intersect( $filters, [ false ] ) ),
236 ];
237 }
238
242 private function getTypeMenuDesc() {
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 [
273 'class' => HTMLSelectField::class,
274 'name' => 'type',
275 'options' => array_flip( $typesByName ),
276 ];
277 }
278
284 private function getExtraInputsDesc( $type, $username ) {
285 $formDescriptor = [];
286
287 if ( $type === 'suppress' ) {
288 $formDescriptor[] = [
289 'type' => 'text',
290 'label-message' => 'revdelete-offender',
291 'name' => 'offender',
292 ];
293 return $formDescriptor;
294 }
295
296 if ( $this->tempUserConfig->isKnown() ) {
297 // Add option to exclude/include temporary account creations in results,
298 // excluding them by default. If we're on a different log, use a hidden field
299 // to preserve the checked by default behavior.
300 $fieldType = 'hidden';
301 if ( $type === 'newusers' || $type === '' ) {
302 $fieldType = 'check';
303 }
304 $formDescriptor[] = [
305 'type' => $fieldType,
306 'label-message' => 'newusers-excludetempacct',
307 'name' => 'excludetempacct',
308 'default' => !$this->tempUserConfig->isTempName( $username ),
309 ];
310 }
311
312 // Allow extensions to add an extra input into the descriptor array.
313 $unused = ''; // Deprecated since 1.32, removed in 1.41
314 $this->hookRunner->onLogEventsListGetExtraInputs( $type, $this, $unused, $formDescriptor );
315
316 return $formDescriptor;
317 }
318
325 private function getActionSelectorDesc( $type, $actions ) {
326 $actionOptions = [ 'log-action-filter-all' => '' ];
327
328 foreach ( $actions as $value => $_ ) {
329 $msgKey = "log-action-filter-$type-$value";
330 $actionOptions[ $msgKey ] = $value;
331 }
332
333 return [
334 'class' => HTMLSelectField::class,
335 'name' => 'subtype',
336 'options-messages' => $actionOptions,
337 'label-message' => 'log-action-filter-' . $type,
338 ];
339 }
340
344 public function beginLogEventsList() {
345 return "<ul class='mw-logevent-loglines'>\n";
346 }
347
351 public function endLogEventsList() {
352 return "</ul>\n";
353 }
354
359 public function logLine( $row ) {
360 $entry = DatabaseLogEntry::newFromRow( $row );
361 $formatter = $this->logFormatterFactory->newFromEntry( $entry );
362 $formatter->setContext( $this->getContext() );
363 $formatter->setShowUserToolLinks( !( $this->flags & self::NO_EXTRA_USER_LINKS ) );
364
365 $time = $this->getLanguage()->userTimeAndDate(
366 $entry->getTimestamp(),
367 $this->getUser()
368 );
369 // Link the time text to the specific log entry, see T207562
370 $timeLink = $this->getLinkRenderer()->makeKnownLink(
372 $time,
373 [],
374 [ 'logid' => $entry->getId() ]
375 );
376
377 $action = $formatter->getActionText();
378
379 if ( $this->flags & self::NO_ACTION_LINK ) {
380 $revert = '';
381 } else {
382 $revert = $formatter->getActionLinks();
383 if ( $revert != '' ) {
384 $revert = '<span class="mw-logevent-actionlink">' . $revert . '</span>';
385 }
386 }
387
388 $comment = $formatter->getComment();
389
390 // Some user can hide log items and have review links
391 $del = $this->getShowHideLinks( $row );
392
393 // Any tags...
394 [ $tagDisplay, $newClasses ] = $this->tagsCache->getWithSetCallback(
395 $this->tagsCache->makeKey(
396 $row->ts_tags ?? '',
397 $this->getUser()->getName(),
398 $this->getLanguage()->getCode()
399 ),
400 fn () => ChangeTags::formatSummaryRow(
401 $row->ts_tags,
402 'logevent',
403 $this->getContext()
404 )
405 );
406 $classes = [ 'mw-logline-' . $entry->getType(), ...$newClasses ];
407 $attribs = [
408 'data-mw-logid' => $entry->getId(),
409 'data-mw-logaction' => $entry->getFullType(),
410 ];
411 $ret = "$del $timeLink $action $comment $revert $tagDisplay";
412
413 // Let extensions add data
414 $ret .= Html::openElement( 'span', [ 'class' => 'mw-logevent-tool' ] );
415 // FIXME: this hook assumes that callers will only append to $ret value.
416 // In future this hook should be replaced with a new hook: LogTools that has a
417 // hook interface consistent with DiffTools and HistoryTools.
418 $this->hookRunner->onLogEventsListLineEnding( $this, $ret, $entry, $classes, $attribs );
419 $attribs = array_filter( $attribs,
420 Sanitizer::isReservedDataAttribute( ... ),
421 ARRAY_FILTER_USE_KEY
422 );
423 $ret .= Html::closeElement( 'span' );
424 $attribs['class'] = $classes;
425
426 return Html::rawElement( 'li', $attribs, $ret ) . "\n";
427 }
428
433 private function getShowHideLinks( $row ) {
434 // We don't want to see the links and
435 if ( $this->flags == self::NO_ACTION_LINK ) {
436 return '';
437 }
438
439 // If change tag editing is available to this user, return the checkbox
440 if ( $this->flags & self::USE_CHECKBOXES && $this->showTagEditUI ) {
441 return Html::check( 'ids[' . $row->log_id . ']', false );
442 }
443
444 // no one can hide items from the suppress log.
445 if ( $row->log_type == 'suppress' ) {
446 return '';
447 }
448
449 $del = '';
450 $authority = $this->getAuthority();
451 // Don't show useless checkbox to people who cannot hide log entries
452 if ( $authority->isAllowed( 'deletedhistory' ) ) {
453 $canHide = $authority->isAllowed( 'deletelogentry' );
454 $canViewSuppressedOnly = $authority->isAllowed( 'viewsuppressed' ) &&
455 !$authority->isAllowed( 'suppressrevision' );
456 $entryIsSuppressed = self::isDeleted( $row, LogPage::DELETED_RESTRICTED );
457 $canViewThisSuppressedEntry = $canViewSuppressedOnly && $entryIsSuppressed;
458 if ( $row->log_deleted || $canHide ) {
459 // Show checkboxes instead of links.
460 if ( $canHide && $this->flags & self::USE_CHECKBOXES && !$canViewThisSuppressedEntry ) {
461 // If event was hidden from sysops
462 if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $authority ) ) {
463 $del = Html::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] );
464 } else {
465 $del = Html::check( 'ids[' . $row->log_id . ']', false );
466 }
467 } else {
468 // If event was hidden from sysops
469 if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $authority ) ) {
470 $del = Linker::revDeleteLinkDisabled( $canHide );
471 } else {
472 $query = [
473 'target' => SpecialPage::getTitleFor( 'Log', $row->log_type )->getPrefixedDBkey(),
474 'type' => 'logging',
475 'ids' => $row->log_id,
476 ];
477 $del = Linker::revDeleteLink(
478 $query,
479 $entryIsSuppressed,
480 $canHide && !$canViewThisSuppressedEntry
481 );
482 }
483 }
484 }
485 }
486
487 return $del;
488 }
489
496 public static function typeAction( $row, $type, $action ) {
497 $match = is_array( $type ) ?
498 in_array( $row->log_type, $type ) : $row->log_type == $type;
499 if ( $match ) {
500 $match = is_array( $action ) ?
501 in_array( $row->log_action, $action ) : $row->log_action == $action;
502 }
503
504 return $match;
505 }
506
516 public static function userCan( $row, $field, Authority $performer ) {
517 return self::userCanBitfield( $row->log_deleted, $field, $performer ) &&
518 self::userCanViewLogType( $row->log_type, $performer );
519 }
520
530 public static function userCanBitfield( $bitfield, $field, Authority $performer ) {
531 if ( $bitfield & $field ) {
532 if ( $bitfield & LogPage::DELETED_RESTRICTED ) {
533 return $performer->isAllowedAny( 'suppressrevision', 'viewsuppressed' );
534 } else {
535 return $performer->isAllowed( 'deletedhistory' );
536 }
537 }
538 return true;
539 }
540
549 public static function userCanViewLogType( $type, Authority $performer ) {
550 $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogRestrictions );
551 if ( isset( $logRestrictions[$type] ) && !$performer->isAllowed( $logRestrictions[$type] ) ) {
552 return false;
553 }
554 return true;
555 }
556
562 public static function isDeleted( $row, $field ) {
563 return ( $row->log_deleted & $field ) == $field;
564 }
565
594 public static function showLogExtract(
595 &$out, $types = [], $pages = '', $user = '', $param = []
596 ) {
597 $defaultParameters = [
598 'lim' => 25,
599 'conds' => [],
600 'showIfEmpty' => true,
601 'msgKey' => [ '' ],
602 'wrap' => "$1",
603 'flags' => 0,
604 'useRequestParams' => false,
605 'useMaster' => false,
606 'extraUrlParams' => false,
607 'footerHtmlItems' => []
608 ];
609 # The + operator appends elements of remaining keys from the right
610 # handed array to the left handed, whereas duplicated keys are NOT overwritten.
611 $param += $defaultParameters;
612 # Convert $param array to individual variables
613 $lim = $param['lim'];
614 $conds = $param['conds'];
615 $showIfEmpty = $param['showIfEmpty'];
616 $msgKey = $param['msgKey'];
617 $wrap = $param['wrap'];
618 $flags = $param['flags'];
619 $extraUrlParams = $param['extraUrlParams'];
620
621 $useRequestParams = $param['useRequestParams'];
622 if ( !is_array( $msgKey ) ) {
623 $msgKey = [ $msgKey ];
624 }
625
626 // ???
627 // @phan-suppress-next-line PhanRedundantCondition
628 if ( $out instanceof OutputPage ) {
629 $context = $out->getContext();
630 } else {
631 $context = RequestContext::getMain();
632 }
633
634 $services = MediaWikiServices::getInstance();
635 // FIXME: Figure out how to inject this
636 $linkRenderer = $services->getLinkRenderer();
637
638 if ( !is_array( $pages ) ) {
639 $pages = [ $pages ];
640 }
641
642 # Insert list of top 50 (or top $lim) items
643 $loglist = new LogEventsList( $context, $linkRenderer, $flags );
644 $pager = new LogPager(
645 $loglist,
646 $types,
647 $user,
648 $pages,
649 false,
650 $conds,
651 false,
652 false,
653 false,
654 '',
655 '',
656 0,
657 $services->getLinkBatchFactory(),
658 $services->getActorNormalization(),
659 $services->getLogFormatterFactory()
660 );
661 if ( !$useRequestParams ) {
662 # Reset vars that may have been taken from the request
663 $pager->mLimit = 50;
664 $pager->mDefaultLimit = 50;
665 $pager->mOffset = "";
666 $pager->mIsBackwards = false;
667 }
668
669 if ( $param['useMaster'] ) {
670 $pager->mDb = $services->getConnectionProvider()->getPrimaryDatabase();
671 }
672
673 if ( isset( $param['offset'] ) ) { # Tell pager to ignore WebRequest offset
674 $pager->setOffset( $param['offset'] );
675 }
676
677 if ( $lim > 0 ) {
678 $pager->mLimit = $lim;
679 }
680 // Fetch the log rows and build the HTML if needed
681 $logBody = $pager->getBody();
682 $numRows = $pager->getNumRows();
683
684 $s = '';
685 $footerHtmlItems = [];
686
687 if ( $logBody ) {
688 if ( $msgKey[0] ) {
689 // @phan-suppress-next-line PhanParamTooFewUnpack Non-emptiness checked above
690 $msg = $context->msg( ...$msgKey );
691 if ( ( $pages[0] ?? null ) instanceof PageReference ) {
692 $msg->page( $pages[0] );
693 }
694 $s .= $msg->parseAsBlock();
695 }
696 $s .= $loglist->beginLogEventsList() .
697 $logBody .
698 $loglist->endLogEventsList();
699 // add styles for change tags
700 $context->getOutput()->addModuleStyles( 'mediawiki.interface.helpers.styles' );
701 } elseif ( $showIfEmpty ) {
702 $s = Html::rawElement( 'div', [ 'class' => 'mw-warning-logempty' ],
703 $context->msg( 'logempty' )->parse() );
704 }
705
706 $pageNames = [];
707 foreach ( $pages as $page ) {
708 if ( $page instanceof PageReference ) {
709 $titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter();
710 $pageNames[] = $titleFormatter->getPrefixedDBkey( $page );
711 } elseif ( $page != '' ) {
712 $pageNames[] = $page;
713 }
714 }
715
716 if ( $numRows > $pager->mLimit ) { # Show "Full log" link
717 $urlParam = [];
718 if ( $pageNames ) {
719 $urlParam['page'] = count( $pageNames ) > 1 ? $pageNames : $pageNames[0];
720 }
721
722 if ( $user != '' ) {
723 $urlParam['user'] = $user;
724 }
725
726 if ( !is_array( $types ) ) { # Make it an array, if it isn't
727 $types = [ $types ];
728 }
729
730 # If there is exactly one log type, we can link to Special:Log?type=foo
731 if ( count( $types ) == 1 ) {
732 $urlParam['type'] = $types[0];
733 }
734
735 if ( $extraUrlParams !== false ) {
736 $urlParam = array_merge( $urlParam, $extraUrlParams );
737 }
738
739 $footerHtmlItems[] = $linkRenderer->makeKnownLink(
740 SpecialPage::getTitleFor( 'Log' ),
741 $context->msg( 'log-fulllog' )->text(),
742 [],
743 $urlParam
744 );
745 }
746 if ( $param['footerHtmlItems'] ) {
747 $footerHtmlItems = array_merge( $footerHtmlItems, $param['footerHtmlItems'] );
748 }
749 if ( $logBody && $footerHtmlItems ) {
750 $s .= '<ul class="mw-logevent-footer">';
751 foreach ( $footerHtmlItems as $item ) {
752 $s .= Html::rawElement( 'li', [], $item );
753 }
754 $s .= '</ul>';
755 }
756
757 if ( $logBody && $msgKey[0] ) {
758 // TODO: The condition above is weird. Should this be done in any other cases?
759 // Or is it always true in practice?
760
761 // Mark as interface language (T60685)
762 $dir = $context->getLanguage()->getDir();
763 $lang = $context->getLanguage()->getHtmlCode();
764 $s = Html::rawElement( 'div', [
765 'class' => "mw-content-$dir",
766 'dir' => $dir,
767 'lang' => $lang,
768 ], $s );
769
770 // Wrap in warning box
771 $s = Html::warningBox(
772 $s,
773 'mw-warning-with-logexcerpt'
774 );
775 // Add styles for warning box
776 $context->getOutput()->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
777 }
778
779 if ( $wrap != '' ) { // Wrap message in html
780 $s = str_replace( '$1', $s, $wrap );
781 }
782
783 /* hook can return false, if we don't want the message to be emitted (Wikia BugId:7093) */
784 $hookRunner = new HookRunner( $services->getHookContainer() );
785 if ( $hookRunner->onLogEventsListShowLogExtract(
786 $s, $types, $pageNames, $user, $param
787 ) ) {
788 // $out can be either an OutputPage object or a String-by-reference
789 if ( $out instanceof OutputPage ) {
790 $out->addHTML( $s );
791 } else {
792 $out = $s;
793 }
794 }
795
796 return $numRows;
797 }
798
808 public static function getExcludeClause( $db, $audience = 'public', ?Authority $performer = null ) {
809 $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogRestrictions );
810
811 if ( $audience != 'public' && $performer === null ) {
812 throw new InvalidArgumentException(
813 'A User object must be given when checking for a user audience.'
814 );
815 }
816
817 // Reset the array, clears extra "where" clauses when $par is used
818 $hiddenLogs = [];
819
820 // Don't show private logs to unprivileged users
821 foreach ( $logRestrictions as $logType => $right ) {
822 if ( $audience == 'public' || !$performer->isAllowed( $right ) ) {
823 $hiddenLogs[] = $logType;
824 }
825 }
826 if ( count( $hiddenLogs ) == 1 ) {
827 return 'log_type != ' . $db->addQuotes( $hiddenLogs[0] );
828 } elseif ( $hiddenLogs ) {
829 return 'log_type NOT IN (' . $db->makeList( $hiddenLogs ) . ')';
830 }
831
832 return false;
833 }
834
859 public static function getBlockLogWarningBox(
860 DatabaseBlockStore $blockStore,
861 NamespaceInfo $namespaceInfo,
862 MessageLocalizer $localizer,
863 LinkRenderer $linkRenderer,
864 $user,
865 ?Title $title,
866 array|callable $additionalParams = []
867 ) {
868 if ( !$user ) {
869 return null;
870 }
871
872 // For IP ranges we must give DatabaseBlock::newFromTarget the CIDR string
873 // and not a user object
874 $userOrRange = IPUtils::isValidRange( $user->getName() ) ? $user->getName() : $user;
875 $blocks = $blockStore->newListFromTarget(
876 // Do not expose the autoblocks, since that may lead to a leak of accounts' IPs,
877 // and also that will display a totally irrelevant log entry as a current block.
878 $userOrRange, $userOrRange, false, DatabaseBlockStore::AUTO_NONE
879 );
880 if ( !count( $blocks ) ) {
881 return null;
882 }
883
884 $isAnon = !$user->isRegistered();
885 $appliesToTitle = false;
886 $logTargetPages = [];
887 $sitewide = false;
888 $matchingIpFound = false;
889 $newestBlockTimestamp = null;
890 $blockId = null;
891 foreach ( $blocks as $block ) {
892 if ( $title === null || $block->appliesToTitle( $title ) ) {
893 $appliesToTitle = true;
894 }
895 $blockTargetName = $block->getTargetName();
896 $logTargetPages[] =
897 $namespaceInfo->getCanonicalName( NS_USER ) . ':' . $blockTargetName;
898 if ( $block->isSitewide() ) {
899 $sitewide = true;
900 }
901
902 // Track the most recent active block. Prefer newer timestamps; if two blocks
903 // share the same timestamp, fall back to the larger block ID to break ties.
904 // This avoids issues where overridden blocks may reuse smaller IDs.
905 //
906 // IP blocks are a bit tricky here:
907 // - Prioritize direct blocks where $user and $block share the same IP.
908 // - The same IP can be directly blocked multiple times, in which case
909 // the timestamp priority logic should work the same way.
910 // Once an exact IP match is found, it takes precedence over range blocks
911 // even if the range is newer or has a bigger ID, since it represents a more
912 // specific and directly applicable restriction.
913 $isExactIpMatch = $isAnon && $user->getName() === $blockTargetName;
914 if ( ( $isExactIpMatch || !$matchingIpFound ) && (
915 $newestBlockTimestamp === null ||
916 $block->getTimestamp() > $newestBlockTimestamp ||
917 ( $block->getTimestamp() === $newestBlockTimestamp && $block->getId() > $blockId )
918 ) ) {
919 $newestBlockTimestamp = $block->getTimestamp();
920 $blockId = $block->getId();
921
922 // If this block is an exact IP match, mark it so future range blocks don't
923 // override it, regardless of newer timestamps or bigger IDs
924 if ( $isExactIpMatch ) {
925 $matchingIpFound = true;
926 }
927 }
928 }
929
930 // Show nothing if no active block applies to the given title
931 // (practically, whether the target user is allowed to edit their user/user_talk page)
932 if ( !$appliesToTitle ) {
933 return null;
934 }
935
936 if ( count( $blocks ) === 1 ) {
937 if ( $isAnon ) {
938 $msgKey = $sitewide ?
939 'blocked-notice-logextract-anon' :
940 'blocked-notice-logextract-anon-partial';
941 } else {
942 $msgKey = $sitewide ?
943 'blocked-notice-logextract' :
944 'blocked-notice-logextract-partial';
945 }
946 } else {
947 if ( $isAnon ) {
948 $msgKey = 'blocked-notice-logextract-anon-multi';
949 } else {
950 $msgKey = 'blocked-notice-logextract-multi';
951 }
952 }
953
954 // While $blocks already contains only active blocks, LogEventsList::showLogExtract
955 // by default fetches the most recent log entries regardless of block status.
956 // To ensure the newest ACTIVE block log is shown, add explicit LIKE conditions
957 // here to filter block log entries.
958 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
959 $orCondsForBlockId = [];
960 $orCondsForBlockId[] = $dbr->expr(
961 // Before MW 1.44, log_params did not contain blockId. Always include such older
962 // log entries for backwards compatibility
963 'log_params',
964 IExpression::NOT_LIKE,
965 new LikeValue( new LikeMatch( '%"blockId"%' ) )
966 );
967 if ( $blockId !== null ) {
968 $orCondsForBlockId[] = $dbr->expr(
969 'log_params',
970 IExpression::LIKE,
971 new LikeValue( new LikeMatch( "%\"blockId\";i:$blockId;%" ) )
972 );
973 }
974 $conds = [ $dbr->makeList( $orCondsForBlockId, LIST_OR ) ];
975
976 $params = [
977 'lim' => 1,
978 'conds' => $conds,
979 'showIfEmpty' => false,
980 'msgKey' => [
981 $msgKey,
982 $user->getName(), // Support GENDER in $msgKey
983 count( $blocks )
984 ],
985 'offset' => '' // Don't use WebRequest parameter offset
986 ];
987
988 if ( count( $blocks ) > 1 ) {
989 $params['footerHtmlItems'] = [
990 $linkRenderer->makeKnownLink(
991 SpecialPage::getTitleFor( 'BlockList' ),
992 $localizer->msg( 'blocked-notice-list-link' )->text(),
993 [],
994 [ 'wpTarget' => $user->getName() ]
995 ),
996 ];
997 }
998
999 if ( is_callable( $additionalParams ) ) {
1000 $extraParams = $additionalParams( [
1001 // Add values to this callback array depending on the needs
1002 // Don't forget to also update the method documentation
1003 'blocks' => $blocks,
1004 'sitewide' => $sitewide,
1005 'logTargetPages' => $logTargetPages
1006 ] );
1007 if ( !is_array( $extraParams ) ) {
1008 throw new UnexpectedValueException(
1009 'The callable $additionalParams must return an array, ' . gettype( $extraParams ) . ' given'
1010 );
1011 }
1012 $params += $extraParams;
1013 } else {
1014 $params += $additionalParams;
1015 }
1016
1017 $outString = '';
1018 self::showLogExtract( $outString, 'block', $logTargetPages, '', $params );
1019 return $outString ?: null;
1020 }
1021}
1022
1024class_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.
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:195
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:43
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:47
Create PSR-3 logger objects.
static newFromRow( $row)
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.
showOptions( $type='', $year=0, $month=0, $day=0, $username='')
Show options for the log list.
__construct( $context, $linkRenderer=null, $flags=0)
static showLogExtract(&$out, $types=[], $pages='', $user='', $param=[])
Show log extract.
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 userCanViewLogType( $type, Authority $performer)
Determine if the current user is allowed to view a particular field of this log row,...
static getBlockLogWarningBox(DatabaseBlockStore $blockStore, NamespaceInfo $namespaceInfo, MessageLocalizer $localizer, LinkRenderer $linkRenderer, $user, ?Title $title, array|callable $additionalParams=[])
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:35
static validTypes()
Get the list of valid log types.
Definition LogPage.php:208
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:32
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( $index)
Returns the canonical (English) name for a given index.
Represents a title within MediaWiki.
Definition Title.php:70
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 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.
Interface for localizing messages in MediaWiki.
msg( $key,... $params)
This is the method for getting translated interface messages.