Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 374
0.00% covered (danger)
0.00%
0 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
LogEventsList
0.00% covered (danger)
0.00%
0 / 374
0.00% covered (danger)
0.00%
0 / 18
8190
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getLinkRenderer
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 showOptions
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 1
110
 getFiltersDesc
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 getTypeMenuDesc
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
20
 getExtraInputsDesc
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 getActionSelectorDesc
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 beginLogEventsList
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 endLogEventsList
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 logLine
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
12
 getShowHideLinks
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
272
 typeAction
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 userCan
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 userCanBitfield
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 userCanViewLogType
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 isDeleted
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 showLogExtract
0.00% covered (danger)
0.00%
0 / 117
0.00% covered (danger)
0.00%
0 / 1
600
 getExcludeClause
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2/**
3 * Contain classes to list log entries
4 *
5 * Copyright © 2004 Brooke Vibber <bvibber@wikimedia.org>
6 * https://www.mediawiki.org/
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License along
19 * with this program; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 * http://www.gnu.org/copyleft/gpl.html
22 *
23 * @file
24 */
25
26use MediaWiki\Context\ContextSource;
27use MediaWiki\Context\IContextSource;
28use MediaWiki\Context\RequestContext;
29use MediaWiki\HookContainer\HookRunner;
30use MediaWiki\Html\Html;
31use MediaWiki\HTMLForm\Field\HTMLMultiSelectField;
32use MediaWiki\HTMLForm\Field\HTMLSelectField;
33use MediaWiki\HTMLForm\Field\HTMLTitleTextField;
34use MediaWiki\HTMLForm\Field\HTMLUserTextField;
35use MediaWiki\HTMLForm\HTMLForm;
36use MediaWiki\Linker\Linker;
37use MediaWiki\Linker\LinkRenderer;
38use MediaWiki\Logger\LoggerFactory;
39use MediaWiki\MainConfigNames;
40use MediaWiki\MediaWikiServices;
41use MediaWiki\Output\OutputPage;
42use MediaWiki\Page\PageReference;
43use MediaWiki\Pager\LogPager;
44use MediaWiki\Parser\Sanitizer;
45use MediaWiki\Permissions\Authority;
46use MediaWiki\SpecialPage\SpecialPage;
47use MediaWiki\Status\Status;
48use MediaWiki\Xml\Xml;
49
50class LogEventsList extends ContextSource {
51    public const NO_ACTION_LINK = 1;
52    public const NO_EXTRA_USER_LINKS = 2;
53    public const USE_CHECKBOXES = 4;
54
55    /** @var int */
56    public $flags;
57
58    /**
59     * @var bool
60     */
61    protected $showTagEditUI;
62
63    /**
64     * @var LinkRenderer|null
65     */
66    private $linkRenderer;
67
68    /** @var HookRunner */
69    private $hookRunner;
70
71    private LogFormatterFactory $logFormatterFactory;
72
73    /** @var MapCacheLRU */
74    private $tagsCache;
75
76    /**
77     * @param IContextSource $context
78     * @param LinkRenderer|null $linkRenderer
79     * @param int $flags Can be a combination of self::NO_ACTION_LINK,
80     *   self::NO_EXTRA_USER_LINKS or self::USE_CHECKBOXES.
81     */
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
95    /**
96     * @since 1.30
97     * @return LinkRenderer
98     */
99    protected function getLinkRenderer() {
100        if ( $this->linkRenderer !== null ) {
101            return $this->linkRenderer;
102        } else {
103            return MediaWikiServices::getInstance()->getLinkRenderer();
104        }
105    }
106
107    /**
108     * Show options for the log list
109     *
110     * @param string $type Log type
111     * @param int|string $year Use 0 to start with no year preselected.
112     * @param int|string $month A month in the 1..12 range. Use 0 to start with no month
113     *  preselected.
114     * @param int|string $day A day in the 1..31 range. Use 0 to start with no month
115     *  preselected.
116     * @return bool Whether the options are valid
117     */
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
205    /**
206     * @return array Form descriptor
207     */
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
223    /**
224     * @return array Form descriptor
225     */
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
263    /**
264     * @param string $type
265     * @return array Form descriptor
266     */
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
284    /**
285     * Drop down menu for selection of actions that can be used to filter the log
286     * @param string $type
287     * @param array $actions
288     * @return array Form descriptor
289     */
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
306    /**
307     * @return string
308     */
309    public function beginLogEventsList() {
310        return "<ul class='mw-logevent-loglines'>\n";
311    }
312
313    /**
314     * @return string
315     */
316    public function endLogEventsList() {
317        return "</ul>\n";
318    }
319
320    /**
321     * @param stdClass $row A single row from the result set
322     * @return string Formatted HTML list item
323     */
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            ),
365            fn () => ChangeTags::formatSummaryRow(
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
392    /**
393     * @param stdClass $row
394     * @return string
395     */
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
461    /**
462     * @param stdClass $row
463     * @param string|array $type
464     * @param string|array $action
465     * @return bool
466     */
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
478    /**
479     * Determine if the current user is allowed to view a particular
480     * field of this log row, if it's marked as deleted and/or restricted log type.
481     *
482     * @param stdClass $row
483     * @param int $field One of LogPage::DELETED_ACTION, ::DELETED_COMMENT, ::DELETED_USER, ::DELETED_RESTRICTED
484     * @param Authority $performer User to check
485     * @return bool
486     */
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
492    /**
493     * Determine if the current user is allowed to view a particular
494     * field of this log row, if it's marked as deleted.
495     *
496     * @param int $bitfield Current field
497     * @param int $field One of LogPage::DELETED_ACTION, ::DELETED_COMMENT, ::DELETED_USER, ::DELETED_RESTRICTED
498     * @param Authority $performer User to check
499     * @return bool
500     */
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
512    /**
513     * Determine if the current user is allowed to view a particular
514     * field of this log row, if it's marked as restricted log type.
515     *
516     * @param string $type
517     * @param Authority $performer User to check
518     * @return bool
519     */
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
528    /**
529     * @param stdClass $row
530     * @param int $field One of LogPage::DELETED_ACTION, ::DELETED_COMMENT, ::DELETED_USER, ::DELETED_RESTRICTED
531     * @return bool
532     */
533    public static function isDeleted( $row, $field ) {
534        return ( $row->log_deleted & $field ) == $field;
535    }
536
537    /**
538     * Show log extract. Either with text and a box (set $msgKey) or without (don't set $msgKey)
539     *
540     * @param OutputPage|string &$out
541     * @param string|array $types Log types to show
542     * @param string|PageReference $page The page title to show log entries for
543     * @param string $user The user who made the log entries
544     * @param array $param Associative Array with the following additional options:
545     * - lim Integer Limit of items to show, default is 50
546     * - conds Array Extra conditions for the query
547     *   (e.g. $dbr->expr( 'log_action', '!=', 'revision' ))
548     * - showIfEmpty boolean Set to false if you don't want any output in case the loglist is empty
549     *   if set to true (default), "No matching items in log" is displayed if loglist is empty
550     * - msgKey Array If you want a nice box with a message, set this to the key of the message.
551     *   First element is the message key, additional optional elements are parameters for the key
552     *   that are processed with wfMessage
553     * - offset Set to overwrite offset parameter in WebRequest
554     *   set to '' to unset offset
555     * - wrap String Wrap the message in html (usually something like "<div ...>$1</div>").
556     * - flags Integer display flags (NO_ACTION_LINK,NO_EXTRA_USER_LINKS)
557     * - useRequestParams boolean Set true to use Pager-related parameters in the WebRequest
558     * - useMaster boolean Use primary DB
559     * - extraUrlParams array|bool Additional url parameters for "full log" link (if it is shown)
560     * @return int Number of total log items (not limited by $lim)
561     */
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
755    /**
756     * SQL clause to skip forbidden log types for this user
757     *
758     * @param \Wikimedia\Rdbms\IReadableDatabase $db
759     * @param string $audience Public/user
760     * @param Authority|null $performer User to check, required when audience isn't public
761     * @return string|false String on success, false on failure.
762     * @throws InvalidArgumentException
763     */
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}