Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.71% covered (success)
95.71%
67 / 70
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
TimelinePager
95.71% covered (success)
95.71%
67 / 70
87.50% covered (warning)
87.50%
7 / 8
15
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 reallyDoQuery
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getQueryInfo
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getIndexField
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 formatRow
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
1 / 1
6
 getPagingQueries
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getFullOutput
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getEndBody
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace MediaWiki\CheckUser\Investigate\Pagers;
4
5use IContextSource;
6use MediaWiki\CheckUser\Hook\CheckUserFormatRowHook;
7use MediaWiki\CheckUser\Investigate\Services\TimelineService;
8use MediaWiki\CheckUser\Investigate\Utilities\DurationManager;
9use MediaWiki\CheckUser\Services\TokenQueryManager;
10use MediaWiki\Html\Html;
11use MediaWiki\Linker\LinkRenderer;
12use MediaWiki\Pager\ReverseChronologicalPager;
13use ParserOutput;
14use Psr\Log\LoggerInterface;
15use Wikimedia\Rdbms\FakeResultWrapper;
16
17class TimelinePager extends ReverseChronologicalPager {
18    private CheckUserFormatRowHook $formatRowHookRunner;
19    private TimelineService $timelineService;
20    private TimelineRowFormatter $timelineRowFormatter;
21    private TokenQueryManager $tokenQueryManager;
22
23    /** @var string */
24    private $start;
25
26    /** @var string|null */
27    private $lastDateHeader;
28
29    /**
30     * Targets whose results should not be included in the investigation.
31     * Targets in this list may or may not also be in the $targets list.
32     * Either way, no activity related to these targets will appear in the
33     * results.
34     *
35     * @var string[]
36     */
37    private $excludeTargets;
38
39    /**
40     * Targets that have been added to the investigation but that are not
41     * present in $excludeTargets. These are the targets that will actually
42     * be investigated.
43     *
44     * @var string[]
45     */
46    private $filteredTargets;
47
48    private LoggerInterface $logger;
49
50    /**
51     * @param IContextSource $context
52     * @param LinkRenderer $linkRenderer
53     * @param CheckUserFormatRowHook $formatRowHookRunner
54     * @param TokenQueryManager $tokenQueryManager
55     * @param DurationManager $durationManager
56     * @param TimelineService $timelineService
57     * @param TimelineRowFormatter $timelineRowFormatter
58     * @param LoggerInterface $logger
59     */
60    public function __construct(
61        IContextSource $context,
62        LinkRenderer $linkRenderer,
63        CheckUserFormatRowHook $formatRowHookRunner,
64        TokenQueryManager $tokenQueryManager,
65        DurationManager $durationManager,
66        TimelineService $timelineService,
67        TimelineRowFormatter $timelineRowFormatter,
68        LoggerInterface $logger
69    ) {
70        parent::__construct( $context, $linkRenderer );
71        $this->formatRowHookRunner = $formatRowHookRunner;
72        $this->timelineService = $timelineService;
73        $this->timelineRowFormatter = $timelineRowFormatter;
74        $this->tokenQueryManager = $tokenQueryManager;
75        $this->logger = $logger;
76
77        $tokenData = $tokenQueryManager->getDataFromRequest( $context->getRequest() );
78        $this->mOffset = $tokenData['offset'] ?? '';
79        $this->excludeTargets = $tokenData['exclude-targets'] ?? [];
80        $this->filteredTargets = array_diff(
81            $tokenData['targets'] ?? [],
82            $this->excludeTargets
83        );
84        $this->start = $durationManager->getTimestampFromRequest( $context->getRequest() );
85    }
86
87    /**
88     * @inheritDoc
89     *
90     * Handle special case where all targets are filtered.
91     */
92    public function reallyDoQuery( $offset, $limit, $order ) {
93        // If there are no targets, there is no need to run the query and an empty result can be used.
94        if ( $this->filteredTargets === [] ) {
95            return new FakeResultWrapper( [] );
96        }
97        return parent::reallyDoQuery( $offset, $limit, $order );
98    }
99
100    /**
101     * @inheritDoc
102     */
103    public function getQueryInfo() {
104        return $this->timelineService->getQueryInfo(
105            $this->filteredTargets,
106            $this->excludeTargets,
107            $this->start,
108            $this->mLimit
109        );
110    }
111
112    /**
113     * @inheritDoc
114     */
115    public function getIndexField() {
116        return [ [ 'timestamp', 'id' ] ];
117    }
118
119    /**
120     * @inheritDoc
121     */
122    public function formatRow( $row ) {
123        $line = '';
124        $dateHeader = $this->getLanguage()->userDate( wfTimestamp( TS_MW, $row->timestamp ), $this->getUser() );
125        if ( $this->lastDateHeader === null ) {
126            $this->lastDateHeader = $dateHeader;
127            $line .= Html::element( 'h4', [], $dateHeader );
128            $line .= Html::openElement( 'ul' );
129        } elseif ( $this->lastDateHeader !== $dateHeader ) {
130            $this->lastDateHeader = $dateHeader;
131
132            // Start a new list with a new date header
133            $line .= Html::closeElement( 'ul' );
134            $line .= Html::element( 'h4', [], $dateHeader );
135            $line .= Html::openElement( 'ul' );
136        }
137
138        $rowItems = $this->timelineRowFormatter->getFormattedRowItems( $row );
139
140        $this->formatRowHookRunner->onCheckUserFormatRow( $this->getContext(), $row, $rowItems );
141
142        if ( !is_array( $rowItems ) || !isset( $rowItems['links'] ) || !isset( $rowItems['info'] ) ) {
143            $this->logger->warning(
144                __METHOD__ . ': Expected array with keys \'links\' and \'info\''
145                    . ' from CheckUserFormatRow $rowItems param'
146            );
147            return '';
148        }
149
150        $formattedLinks = implode( ' ', array_filter(
151            $rowItems['links'],
152            static function ( $item ) {
153                return $item !== '';
154            } )
155        );
156
157        $formatted = implode( ' . . ', array_filter(
158            array_merge(
159                [ $formattedLinks ],
160                $rowItems['info']
161            ), static function ( $item ) {
162                return $item !== '';
163            } )
164        );
165
166        $line .= Html::rawElement(
167            'li',
168            [],
169            $formatted
170        );
171
172        return $line;
173    }
174
175    /**
176     * @inheritDoc
177     *
178     * Conceal the offset which may reveal private data.
179     */
180    public function getPagingQueries() {
181        return $this->tokenQueryManager->getPagingQueries(
182            $this->getRequest(), parent::getPagingQueries()
183        );
184    }
185
186    /**
187     * Get the formatted result list, with navigation bars.
188     *
189     * @return ParserOutput
190     */
191    public function getFullOutput(): ParserOutput {
192        return new ParserOutput(
193            $this->getNavigationBar() . $this->getBody() . $this->getNavigationBar()
194        );
195    }
196
197    /**
198     * @inheritDoc
199     */
200    public function getEndBody() {
201        return $this->getNumRows() ? Html::closeElement( 'ul' ) : '';
202    }
203}