14use InvalidArgumentException;
46use UnexpectedValueException;
69 private $linkRenderer;
91 $this->showTagEditUI = ChangeTags::showTagEditingUI( $this->
getAuthority() );
93 $this->linkRenderer = $linkRenderer;
96 $this->hookRunner =
new HookRunner( $services->getHookContainer() );
97 $this->logFormatterFactory = $services->getLogFormatterFactory();
99 $this->tempUserConfig = $services->getTempUserConfig();
100 $this->changeTagsFormatter = $services->getChangeTagsFormatter();
108 if ( $this->linkRenderer !==
null ) {
109 return $this->linkRenderer;
127 public function showOptions( $type =
'', $year = 0, $month = 0, $day = 0, $username =
'' ) {
128 $formDescriptor = [];
129 $typesByName = $this->getTypeMenuOptions();
132 $formDescriptor[
'type'] = [
133 'class' => HTMLSelectField::class,
135 'options' => array_flip( $typesByName ),
138 $formDescriptor[
'user'] = [
139 'class' => HTMLUserTextField::class,
140 'label-message' =>
'specialloguserlabel',
146 $formDescriptor[
'page'] = [
147 'class' => HTMLTitleTextField::class,
148 'label-message' =>
'speciallogtitlelabel',
155 $formDescriptor[
'pattern'] = [
157 'label-message' =>
'log-title-wildcard',
163 foreach ( $this->getExtraInputsDesc( $typesByName, $username ) as $key => $field ) {
164 $formDescriptor[$key] = $field;
168 $formDescriptor[
'date'] = [
170 'label-message' =>
'date',
171 'default' => $year && $month && $day ? sprintf(
"%04d-%02d-%02d", $year, $month, $day ) :
'',
175 $formDescriptor[
'tagfilter'] = [
176 'type' =>
'tagfilter',
177 'name' =>
'tagfilter',
178 'label-message' =>
'tag-filter',
180 $formDescriptor[
'tagInvert'] = [
182 'name' =>
'tagInvert',
183 'label-message' =>
'invert',
184 'hide-if' => [
'===',
'tagfilter',
'' ],
188 $formDescriptor[
'filters'] = $this->getFiltersDesc();
192 foreach ( $typesByName as $type => $_ ) {
193 if ( isset( $allowedActions[$type] ) ) {
194 $formDescriptor[
"subtype-$type"] = $this->getActionSelectorDesc( $type, $allowedActions[$type] );
198 $htmlForm = HTMLForm::factory(
'ooui', $formDescriptor, $this->
getContext() );
201 ->setSubmitTextMsg(
'logeventslist-submit' )
203 ->setWrapperLegendMsg(
'log' )
204 ->setFormIdentifier(
'logeventslist',
true )
206 ->setSubmitCallback(
static function ( $formData, $form ) {
208 (
new LogPage( $formData[
'type'] ) )->getDescription()
209 ->
setContext( $form->getContext() )->parseAsBlock()
214 $result = $htmlForm->prepareForm()->trySubmit();
215 $htmlForm->displayForm( $result );
216 return $result ===
true || ( $result instanceof
Status && $result->
isGood() );
222 private function getFiltersDesc() {
225 foreach ( $filters as $type => $val ) {
226 $optionsMsg[
"logeventslist-{$type}-log"] = $type;
229 'class' => HTMLMultiSelectField::class,
230 'label-message' =>
'logeventslist-more-filters',
232 'options-messages' => $optionsMsg,
233 'default' => array_keys( array_intersect( $filters, [
false ] ) ),
235 'hide-if-nojs' => [
'!==',
'type',
'' ],
242 private function getTypeMenuOptions() {
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.',
253 'log_type_one' => $type,
254 'log_type_two' => array_search( $pageText, $typesByName ),
260 if ( $this->
getAuthority()->isAllowed( $page->getRestriction() ) ) {
261 $typesByName[$type] = $pageText;
265 asort( $typesByName );
268 $public = $typesByName[
''];
269 unset( $typesByName[
''] );
270 $typesByName = [
'' => $public ] + $typesByName;
280 private function getExtraInputsDesc( $typesByName, $username ) {
281 $formDescriptor = [];
283 if ( isset( $typesByName[
'suppress'] ) ) {
284 $formDescriptor[
'extra-suppress'] = [
286 'label-message' =>
'revdelete-offender',
287 'name' =>
'offender',
288 'hide-if-nojs' => [
'!==',
'type',
'suppress' ],
292 if ( isset( $typesByName[
'newusers'] ) ) {
295 if ( $this->tempUserConfig->isKnown() ) {
296 $formDescriptor[
'extra-newusers'] = [
298 'label-message' =>
'newusers-excludetempacct',
299 'name' =>
'excludetempacct',
300 'default' => !$this->tempUserConfig->isTempName( $username ),
301 'hide-if' => [
'AND',
302 [
'!==',
'type',
'newusers' ],
303 [
'!==',
'type',
'' ],
311 foreach ( $typesByName as $type => $_ ) {
314 $this->hookRunner->onLogEventsListGetExtraInputs( $type, $this, $unused, $extraInputs );
315 if ( $extraInputs ) {
318 if ( !array_is_list( $extraInputs ) ) {
319 $extraInputs = [ $extraInputs ];
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 ]
328 $input[
'hide-if-nojs'] = [
'!==',
'type', $type ];
330 $formDescriptor[
"extra-$type-$i"] = $input;
335 return $formDescriptor;
344 private function getActionSelectorDesc( $type, $actions ) {
345 $actionOptions = [
'log-action-filter-all' =>
'' ];
347 foreach ( $actions as $value => $_ ) {
348 $msgKey =
"log-action-filter-$type-$value";
349 $actionOptions[ $msgKey ] = $value;
353 'class' => HTMLSelectField::class,
355 'id' =>
'mw-log-action-filter-' . $type,
356 'options-messages' => $actionOptions,
357 'label-message' =>
'log-action-filter-' . $type,
368 'hide-if-nojs' => [
'!==',
'type', $type ],
376 return "<ul class='mw-logevent-loglines'>\n";
392 $formatter = $this->logFormatterFactory->newFromEntry( $entry );
393 $formatter->setContext( $this->
getContext() );
394 $formatter->setShowUserToolLinks( !( $this->flags & self::NO_EXTRA_USER_LINKS ) );
397 $entry->getTimestamp(),
405 [
'logid' => $entry->getId() ]
408 $action = $formatter->getActionText();
410 if ( $this->flags & self::NO_ACTION_LINK ) {
413 $revert = $formatter->getActionLinks();
414 if ( $revert !=
'' ) {
415 $revert =
'<span class="mw-logevent-actionlink">' . $revert .
'</span>';
419 $comment = $formatter->getComment();
422 $del = $this->getShowHideLinks( $row );
425 [ $tagDisplay, $newClasses ] = $this->tagsCache->getWithSetCallback(
426 $this->tagsCache->makeKey(
428 $this->getUser()->getName(),
429 $this->getLanguage()->getCode()
431 fn () => $this->changeTagsFormatter->formatTagsAsSummaryList(
434 $this->getAuthority()
437 $classes = [
'mw-logline-' . $entry->getType(), ...$newClasses ];
439 'data-mw-logid' => $entry->getId(),
440 'data-mw-logaction' => $entry->getFullType(),
442 $ret =
"$del $timeLink $action $comment $revert $tagDisplay";
445 $ret .= Html::openElement(
'span', [
'class' =>
'mw-logevent-tool' ] );
449 $this->hookRunner->onLogEventsListLineEnding( $this, $ret, $entry, $classes, $attribs );
450 $attribs = array_filter( $attribs,
451 Sanitizer::isReservedDataAttribute( ... ),
454 $ret .= Html::closeElement(
'span' );
455 $attribs[
'class'] = $classes;
457 return Html::rawElement(
'li', $attribs, $ret ) .
"\n";
464 private function getShowHideLinks( $row ) {
466 if ( $this->flags == self::NO_ACTION_LINK ) {
471 if ( $this->flags & self::USE_CHECKBOXES && $this->showTagEditUI ) {
472 return Html::check(
'ids[' . $row->log_id .
']',
false );
476 if ( $row->log_type ==
'suppress' ) {
483 if ( $authority->isAllowed(
'deletedhistory' ) ) {
484 $canHide = $authority->isAllowed(
'deletelogentry' );
485 $canViewSuppressedOnly = $authority->isAllowed(
'viewsuppressed' ) &&
486 !$authority->isAllowed(
'suppressrevision' );
488 $canViewThisSuppressedEntry = $canViewSuppressedOnly && $entryIsSuppressed;
489 if ( $row->log_deleted || $canHide ) {
491 if ( $canHide && $this->flags & self::USE_CHECKBOXES && !$canViewThisSuppressedEntry ) {
494 $del = Html::check(
'deleterevisions',
false, [
'disabled' =>
'disabled' ] );
496 $del = Html::check(
'ids[' . $row->log_id .
']',
false );
501 $del = Linker::revDeleteLinkDisabled( $canHide );
506 'ids' => $row->log_id,
508 $del = Linker::revDeleteLink(
511 $canHide && !$canViewThisSuppressedEntry
528 $match = is_array( $type ) ?
529 in_array( $row->log_type, $type ) : $row->log_type == $type;
531 $match = is_array( $action ) ?
532 in_array( $row->log_action, $action ) : $row->log_action == $action;
562 if ( $bitfield & $field ) {
564 return $performer->
isAllowedAny(
'suppressrevision',
'viewsuppressed' );
566 return $performer->
isAllowed(
'deletedhistory' );
582 if ( isset( $logRestrictions[$type] ) && !$performer->
isAllowed( $logRestrictions[$type] ) ) {
594 return ( $row->log_deleted & $field ) == $field;
629 &$out, $types = [], $pages =
'', $user =
'', $param = [], ?
IContextSource $context =
null
631 $defaultParameters = [
634 'showIfEmpty' =>
true,
638 'useRequestParams' =>
false,
639 'useMaster' =>
false,
640 'extraUrlParams' =>
false,
641 'footerHtmlItems' => []
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'];
653 $extraUrlParams = $param[
'extraUrlParams'];
655 $useRequestParams = $param[
'useRequestParams'];
656 if ( !is_array( $msgKey ) ) {
657 $msgKey = [ $msgKey ];
663 throw new InvalidArgumentException(
'When passing $context, do not pass OutputPage as $out' );
665 $context = $out->getContext();
668 $context = RequestContext::getMain();
673 $linkRenderer = $services->getLinkRenderer();
675 if ( !is_array( $pages ) ) {
679 # Insert list of top 50 (or top $lim) items
694 $services->getLinkBatchFactory(),
695 $services->getActorNormalization(),
696 $services->getLogFormatterFactory()
698 if ( !$useRequestParams ) {
699 # Reset vars that may have been taken from the request
701 $pager->mDefaultLimit = 50;
702 $pager->mOffset =
"";
703 $pager->mIsBackwards =
false;
706 if ( $param[
'useMaster'] ) {
707 $pager->mDb = $services->getConnectionProvider()->getPrimaryDatabase();
710 if ( isset( $param[
'offset'] ) ) { # Tell pager to ignore
WebRequest offset
711 $pager->setOffset( $param[
'offset'] );
715 $pager->mLimit = $lim;
718 $logBody = $pager->getBody();
719 $numRows = $pager->getNumRows();
722 $footerHtmlItems = [];
726 $msg = $context->msg( ...$msgKey );
728 $msg->page( $pages[0] );
730 $s .= $msg->parseAsBlock();
732 $s .= $loglist->beginLogEventsList() .
734 $loglist->endLogEventsList();
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() );
743 foreach ( $pages as $page ) {
746 $pageNames[] = $titleFormatter->getPrefixedDBkey( $page );
747 } elseif ( $page !=
'' ) {
748 $pageNames[] = $page;
752 if ( $numRows > $pager->mLimit ) { # Show
"Full log" link
755 $urlParam[
'page'] = count( $pageNames ) > 1 ? $pageNames : $pageNames[0];
759 $urlParam[
'user'] = $user;
762 if ( !is_array( $types ) ) { # Make it an array,
if it isn
't
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];
771 if ( $extraUrlParams !== false ) {
772 $urlParam = array_merge( $urlParam, $extraUrlParams );
775 $footerHtmlItems[] = $linkRenderer->makeKnownLink(
776 SpecialPage::getTitleFor( 'Log
' ),
777 $context->msg( 'log-fulllog
' )->text(),
782 if ( $param['footerHtmlItems
'] ) {
783 $footerHtmlItems = array_merge( $footerHtmlItems, $param['footerHtmlItems
'] );
785 if ( $logBody && $footerHtmlItems ) {
786 $s .= '<ul
class=
"mw-logevent-footer">
';
787 foreach ( $footerHtmlItems as $item ) {
788 $s .= Html::rawElement( 'li
', [], $item );
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?
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",
806 // Wrap in warning box
807 $s = Html::warningBox(
809 'mw-warning-with-logexcerpt
'
811 // Add styles for warning box
812 $context->getOutput()->addModuleStyles( 'mediawiki.codex.messagebox.styles
' );
815 if ( $wrap != '' ) { // Wrap message in html
816 $s = str_replace( '$1
', $s, $wrap );
819 /* hook can return false, if we don't want the message to be emitted (Wikia BugId:7093) */
820 $hookRunner =
new HookRunner( $services->getHookContainer() );
822 $s, $types, $pageNames, $user, $param
847 if ( $audience !=
'public' && $performer ===
null ) {
848 throw new InvalidArgumentException(
849 'A User object must be given when checking for a user audience.'
857 foreach ( $logRestrictions as $logType => $right ) {
858 if ( $audience ==
'public' || !$performer->isAllowed( $right ) ) {
859 $hiddenLogs[] = $logType;
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 ) .
')';
904 array|callable $additionalParams = [],
913 $userOrRange = IPUtils::isValidRange( $user->getName() ) ? $user->getName() : $user;
917 $userOrRange, $userOrRange,
false, DatabaseBlockStore::AUTO_NONE
919 if ( !count( $blocks ) ) {
923 $isAnon = !$user->isRegistered();
924 $appliesToTitle =
false;
925 $logTargetPages = [];
927 $matchingIpFound =
false;
928 $newestBlockTimestamp =
null;
930 foreach ( $blocks as $block ) {
931 if ( $title ===
null || $block->appliesToTitle( $title ) ) {
932 $appliesToTitle =
true;
934 $blockTargetName = $block->getTargetName();
937 if ( $block->isSitewide() ) {
952 $isExactIpMatch = $isAnon && $user->getName() === $blockTargetName;
953 if ( ( $isExactIpMatch || !$matchingIpFound ) && (
954 $newestBlockTimestamp ===
null ||
955 $block->getTimestamp() > $newestBlockTimestamp ||
956 ( $block->getTimestamp() === $newestBlockTimestamp && $block->getId() > $blockId )
958 $newestBlockTimestamp = $block->getTimestamp();
959 $blockId = $block->getId();
963 if ( $isExactIpMatch ) {
964 $matchingIpFound =
true;
971 if ( !$appliesToTitle ) {
975 if ( count( $blocks ) === 1 ) {
977 $msgKey = $sitewide ?
978 'blocked-notice-logextract-anon' :
979 'blocked-notice-logextract-anon-partial';
981 $msgKey = $sitewide ?
982 'blocked-notice-logextract' :
983 'blocked-notice-logextract-partial';
987 $msgKey =
'blocked-notice-logextract-anon-multi';
989 $msgKey =
'blocked-notice-logextract-multi';
998 $orCondsForBlockId = [];
999 $orCondsForBlockId[] = $dbr->expr(
1003 IExpression::NOT_LIKE,
1006 if ( $blockId !==
null ) {
1007 $orCondsForBlockId[] = $dbr->expr(
1013 $conds = [ $dbr->makeList( $orCondsForBlockId,
LIST_OR ) ];
1018 'showIfEmpty' =>
false,
1027 if ( count( $blocks ) > 1 ) {
1028 $params[
'footerHtmlItems'] = [
1031 $localizer->
msg(
'blocked-notice-list-link' )->text(),
1033 [
'wpTarget' => $user->getName() ]
1038 if ( is_callable( $additionalParams ) ) {
1039 $extraParams = $additionalParams( [
1042 'blocks' => $blocks,
1043 'sitewide' => $sitewide,
1044 'logTargetPages' => $logTargetPages
1046 if ( !is_array( $extraParams ) ) {
1047 throw new UnexpectedValueException(
1048 'The callable $additionalParams must return an array, ' . gettype( $extraParams ) .
' given'
1051 $params += $extraParams;
1053 $params += $additionalParams;
1058 return $outString ?:
null;
1063class_alias( LogEventsList::class,
'LogEventsList' );
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.
static newFromRow( $row, string|false $wikiId=WikiAwareEntity::LOCAL)
Constructs new LogEntry from database result row.
Class to simplify the use of log pages.
static validTypes()
Get the list of valid log types.
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()
This is one of the Core classes and should be read at least once by any new developers.
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.
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.