Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
95.71% |
67 / 70 |
|
87.50% |
7 / 8 |
CRAP | |
0.00% |
0 / 1 |
TimelinePager | |
95.71% |
67 / 70 |
|
87.50% |
7 / 8 |
15 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
reallyDoQuery | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getQueryInfo | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getIndexField | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
formatRow | |
100.00% |
39 / 39 |
|
100.00% |
1 / 1 |
6 | |||
getPagingQueries | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getFullOutput | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getEndBody | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\CheckUser\Investigate\Pagers; |
4 | |
5 | use IContextSource; |
6 | use MediaWiki\CheckUser\Hook\CheckUserFormatRowHook; |
7 | use MediaWiki\CheckUser\Investigate\Services\TimelineService; |
8 | use MediaWiki\CheckUser\Investigate\Utilities\DurationManager; |
9 | use MediaWiki\CheckUser\Services\TokenQueryManager; |
10 | use MediaWiki\Html\Html; |
11 | use MediaWiki\Linker\LinkRenderer; |
12 | use MediaWiki\Pager\ReverseChronologicalPager; |
13 | use ParserOutput; |
14 | use Psr\Log\LoggerInterface; |
15 | use Wikimedia\Rdbms\FakeResultWrapper; |
16 | |
17 | class 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 | } |