Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 213 |
|
0.00% |
0 / 19 |
CRAP | |
0.00% |
0 / 1 |
PendingChangesPager | |
0.00% |
0 / 213 |
|
0.00% |
0 / 19 |
2862 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
72 | |||
setLimit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
formatRow | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDefaultQuery | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getQueryInfo | |
0.00% |
0 / 60 |
|
0.00% |
0 / 1 |
156 | |||
getIndexField | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doBatchLookups | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getDefaultSort | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFieldNames | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
formatValue | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isFieldSortable | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getStartBody | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
buildTableHeader | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
buildHeaderCaption | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getPendingCount | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
buildTableElement | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
buildTableHeaderCells | |
0.00% |
0 / 73 |
|
0.00% |
0 / 1 |
240 | |||
getEndBody | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
buildTableCaption | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | use MediaWiki\Html\Html; |
4 | use MediaWiki\MediaWikiServices; |
5 | use MediaWiki\Pager\TablePager; |
6 | use Wikimedia\Rdbms\RawSQLExpression; |
7 | |
8 | /** |
9 | * Query to list out outdated reviewed pages |
10 | */ |
11 | class PendingChangesPager extends TablePager { |
12 | |
13 | private PendingChanges $mForm; |
14 | private ?string $category; |
15 | /** @var int|int[] */ |
16 | private $namespace; |
17 | private ?string $size; |
18 | private bool $watched; |
19 | private bool $stable; |
20 | private ?string $tagFilter; |
21 | // Don't get too expensive |
22 | private const PAGE_LIMIT = 100; |
23 | |
24 | /** |
25 | * The unique sort fields for the sort options for unique paginate |
26 | */ |
27 | private const INDEX_FIELDS = [ |
28 | 'fp_pending_since' => [ 'fp_pending_since' ], |
29 | ]; |
30 | |
31 | /** |
32 | * @param PendingChanges $form |
33 | * @param int|null $namespace |
34 | * @param string $category |
35 | * @param int|null $size |
36 | * @param bool $watched |
37 | * @param bool $stable |
38 | * @param ?string $tagFilter |
39 | */ |
40 | public function __construct( $form, $namespace, string $category = '', |
41 | ?int $size = null, bool $watched = false, bool $stable = false, ?string $tagFilter = '' |
42 | ) { |
43 | $this->mForm = $form; |
44 | # Must be a content page... |
45 | if ( $namespace !== null ) { |
46 | $namespace = (int)$namespace; |
47 | } |
48 | # Sanity check |
49 | if ( $namespace === null || !FlaggedRevs::isReviewNamespace( $namespace ) ) { |
50 | $namespace = FlaggedRevs::getReviewNamespaces(); |
51 | } |
52 | $this->namespace = $namespace; |
53 | $this->category = $category ? str_replace( ' ', '_', $category ) : null; |
54 | $this->tagFilter = $tagFilter ? str_replace( ' ', '_', $tagFilter ) : null; |
55 | $this->size = $size; |
56 | $this->watched = $watched; |
57 | $this->stable = $stable && !FlaggedRevs::isStableShownByDefault() |
58 | && !FlaggedRevs::useOnlyIfProtected(); |
59 | |
60 | parent::__construct(); |
61 | # Don't get too expensive |
62 | $this->mLimitsShown = [ 20, 50, 100 ]; |
63 | $this->setLimit( $this->mLimit ); // apply max limit |
64 | } |
65 | |
66 | /** |
67 | * @inheritDoc |
68 | */ |
69 | public function setLimit( $limit ) { |
70 | $this->mLimit = min( $limit, self::PAGE_LIMIT ); |
71 | } |
72 | |
73 | /** |
74 | * @inheritDoc |
75 | */ |
76 | public function formatRow( $row ): string { |
77 | return $this->mForm->formatRow( $row ); |
78 | } |
79 | |
80 | /** |
81 | * @inheritDoc |
82 | */ |
83 | public function getDefaultQuery(): array { |
84 | $query = parent::getDefaultQuery(); |
85 | $query['category'] = $this->category; |
86 | $query['tagFilter'] = $this->tagFilter; |
87 | return $query; |
88 | } |
89 | |
90 | /** |
91 | * @inheritDoc |
92 | */ |
93 | public function getQueryInfo(): array { |
94 | $tables = [ 'page', 'revision', 'flaggedpages' ]; |
95 | $fields = [ |
96 | 'page_namespace', |
97 | 'page_title', |
98 | 'page_len', |
99 | 'rev_len', |
100 | 'page_latest', |
101 | 'stable' => 'fp_stable', |
102 | 'quality' => 'fp_quality', |
103 | 'pending_since' => 'fp_pending_since' |
104 | ]; |
105 | $conds = [ |
106 | 'page_id = fp_page_id', |
107 | 'rev_id = fp_stable', |
108 | $this->mDb->expr( 'fp_pending_since', '!=', null ) |
109 | ]; |
110 | |
111 | # Filter by pages configured to be stable |
112 | if ( $this->stable ) { |
113 | $tables[] = 'flaggedpage_config'; |
114 | $conds[] = 'fp_page_id = fpc_page_id'; |
115 | $conds['fpc_override'] = 1; |
116 | } |
117 | # Filter by category |
118 | if ( $this->category != '' ) { |
119 | $tables[] = 'categorylinks'; |
120 | $conds[] = 'cl_from = fp_page_id'; |
121 | $conds['cl_to'] = $this->category; |
122 | } |
123 | # Index field for sorting |
124 | $this->mIndexField = 'fp_pending_since'; |
125 | $fields[] = $this->mIndexField; // Pager needs this |
126 | # Filter namespace |
127 | if ( $this->namespace !== null ) { |
128 | $conds['page_namespace'] = $this->namespace; |
129 | } |
130 | # Filter by watchlist |
131 | if ( $this->watched ) { |
132 | $uid = $this->getUser()->getId(); |
133 | if ( $uid ) { |
134 | $tables[] = 'watchlist'; |
135 | $conds['wl_user'] = $uid; |
136 | $conds[] = 'page_namespace = wl_namespace'; |
137 | $conds[] = 'page_title = wl_title'; |
138 | } |
139 | } |
140 | # Filter by bytes changed |
141 | if ( $this->size !== null && $this->size >= 0 ) { |
142 | $conds[] = new RawSQLExpression( |
143 | "(GREATEST(page_len, rev_len) - LEAST(page_len, rev_len)) <= " . intval( $this->size ) |
144 | ); |
145 | } |
146 | # Filter by tag |
147 | if ( $this->tagFilter !== null && $this->tagFilter !== '' ) { |
148 | $tables[] = 'change_tag'; |
149 | $tables[] = 'change_tag_def'; |
150 | $conds[] = 'ct_tag_id = ctd_id'; |
151 | $conds[] = 'ct_rev_id = rev_id'; |
152 | $conds['ctd_name'] = $this->tagFilter; |
153 | } |
154 | # Don't display pages with expired protection (T350527) |
155 | if ( FlaggedRevs::useOnlyIfProtected() ) { |
156 | $tables[] = 'flaggedpage_config'; |
157 | $conds[] = 'fpc_page_id = fp_page_id'; |
158 | $conds[] = new RawSQLExpression( $this->mDb->buildComparison( '>', |
159 | [ 'fpc_expiry' => $this->mDb->timestamp() ] ) . ' OR fpc_expiry = "infinity"' |
160 | ); |
161 | } |
162 | # Set sorting options |
163 | $sortField = $this->getRequest()->getVal( 'sort', 'fp_pending_since' ); |
164 | $sortOrder = $this->getRequest()->getVal( 'asc' ) ? 'ASC' : 'DESC'; |
165 | $options = [ 'ORDER BY' => "$sortField $sortOrder" ]; |
166 | # Return query information |
167 | return [ |
168 | 'tables' => $tables, |
169 | 'fields' => $fields, |
170 | 'conds' => $conds, |
171 | 'options' => $options, |
172 | ]; |
173 | } |
174 | |
175 | /** |
176 | * @inheritDoc |
177 | */ |
178 | public function getIndexField() { |
179 | return $this->mIndexField; |
180 | } |
181 | |
182 | /** |
183 | * @inheritDoc |
184 | */ |
185 | protected function doBatchLookups() { |
186 | $this->mResult->seek( 0 ); |
187 | $lb = MediaWikiServices::getInstance()->getLinkBatchFactory(); |
188 | $batch = $lb->newLinkBatch(); |
189 | foreach ( $this->mResult as $row ) { |
190 | $batch->add( $row->page_namespace, $row->page_title ); |
191 | } |
192 | $batch->execute(); |
193 | } |
194 | |
195 | /** |
196 | * @inheritDoc |
197 | * @since 1.43 |
198 | */ |
199 | public function getDefaultSort(): string { |
200 | return 'fp_pending_since'; |
201 | } |
202 | |
203 | /** |
204 | * @inheritDoc |
205 | * @since 1.43 |
206 | */ |
207 | protected function getFieldNames(): array { |
208 | $fields = [ |
209 | 'page_title' => 'pendingchanges-table-page', |
210 | 'review' => 'pendingchanges-table-review', |
211 | 'rev_len' => 'pendingchanges-table-size', |
212 | 'fp_pending_since' => 'pendingchanges-table-pending-since', |
213 | ]; |
214 | |
215 | if ( $this->getAuthority()->isAllowed( 'unwatchedpages' ) ) { |
216 | $fields['watching'] = 'pendingchanges-table-watching'; |
217 | } |
218 | |
219 | return $fields; |
220 | } |
221 | |
222 | /** |
223 | * @inheritDoc |
224 | * @since 1.43 |
225 | */ |
226 | public function formatValue( $name, $value ): ?string { |
227 | return htmlspecialchars( $value ); |
228 | } |
229 | |
230 | /** |
231 | * @inheritDoc |
232 | * @since 1.43 |
233 | */ |
234 | protected function isFieldSortable( $field ): bool { |
235 | return isset( self::INDEX_FIELDS[$field] ); |
236 | } |
237 | |
238 | /** |
239 | * Builds and returns the start body HTML for the table. |
240 | * |
241 | * @return string HTML |
242 | * @since 1.43 |
243 | */ |
244 | protected function getStartBody(): string { |
245 | return Html::openElement( 'div', [ 'class' => 'cdx-table mw-fr-pending-changes-table' ] ) . |
246 | $this->buildTableHeader() . |
247 | Html::openElement( 'div', [ 'class' => 'cdx-table__table-wrapper' ] ) . |
248 | $this->buildTableElement(); |
249 | } |
250 | |
251 | /** |
252 | * Builds and returns the table header HTML. |
253 | * |
254 | * @return string HTML |
255 | */ |
256 | private function buildTableHeader(): string { |
257 | $headerCaption = $this->buildHeaderCaption(); |
258 | $headerContent = $this->buildTableCaption( 'cdx-table__header__header-content' ); |
259 | |
260 | return Html::rawElement( |
261 | 'div', |
262 | [ 'class' => 'cdx-table__header' ], |
263 | $headerCaption . $headerContent |
264 | ); |
265 | } |
266 | |
267 | /** |
268 | * Builds and returns the header caption HTML. |
269 | * |
270 | * @return string HTML |
271 | */ |
272 | private function buildHeaderCaption(): string { |
273 | return Html::rawElement( |
274 | 'div', |
275 | [ 'class' => 'cdx-table__header__caption', 'aria-hidden' => 'true' ], |
276 | $this->msg( 'pendingchanges-table-caption' )->text() |
277 | ); |
278 | } |
279 | |
280 | /** |
281 | * Retrieves the count of pending pages. |
282 | * |
283 | * @return int The count of pending pages. |
284 | */ |
285 | private function getPendingCount(): int { |
286 | return $this->mDb->selectRowCount( |
287 | 'flaggedpages', '*', [ $this->mDb->expr( 'fp_pending_since', '!=', null ) ], __METHOD__ |
288 | ); |
289 | } |
290 | |
291 | /** |
292 | * Builds and returns the table element HTML. |
293 | * |
294 | * @return string HTML |
295 | */ |
296 | private function buildTableElement(): string { |
297 | $caption = Html::element( 'caption', [], $this->msg( 'pendingchanges-table-caption' )->text() ); |
298 | $thead = $this->buildTableHeaderCells(); |
299 | |
300 | return Html::openElement( 'table', [ 'class' => 'cdx-table__table cdx-table__table--borders-vertical' ] ) . |
301 | $caption . |
302 | $thead . |
303 | Html::openElement( 'tbody' ); |
304 | } |
305 | |
306 | /** |
307 | * Builds and returns the table header cells HTML. |
308 | * |
309 | * @return string HTML |
310 | */ |
311 | private function buildTableHeaderCells(): string { |
312 | $fields = $this->getFieldNames(); |
313 | $headerCells = ''; |
314 | |
315 | foreach ( $fields as $field => $labelKey ) { |
316 | $class = ( $field === 'review' || $field === 'history' ) ? 'cdx-table__table__cell--align-center' : ''; |
317 | |
318 | if ( $field === 'history' ) { |
319 | $headerCells .= Html::rawElement( |
320 | 'th', |
321 | [ 'scope' => 'col', 'class' => $class ], |
322 | Html::rawElement( |
323 | 'span', |
324 | [ 'class' => 'fr-cdx-icon-clock', 'aria-hidden' => 'true' ] |
325 | ) |
326 | ); |
327 | } elseif ( $this->isFieldSortable( $field ) ) { |
328 | $isCurrentSortField = ( $this->mSort === $field ); |
329 | $currentAsc = $this->getRequest()->getVal( 'asc', '1' ); |
330 | |
331 | $newSortAsc = $isCurrentSortField && $currentAsc === '1' ? '' : '1'; |
332 | $newSortDesc = $isCurrentSortField && $currentAsc === '1' ? '1' : ''; |
333 | |
334 | $ariaSort = 'none'; |
335 | if ( $isCurrentSortField ) { |
336 | $ariaSort = $currentAsc === '1' ? 'ascending' : 'descending'; |
337 | } |
338 | |
339 | $iconClass = 'fr-cdx-icon-sort-vertical'; |
340 | if ( $isCurrentSortField ) { |
341 | $iconClass = $currentAsc === '1' ? 'fr-icon-asc' : 'fr-icon-desc'; |
342 | } |
343 | |
344 | $currentParams = $this->getRequest()->getValues(); |
345 | unset( $currentParams['title'], $currentParams['sort'], $currentParams['asc'], $currentParams['desc'] ); |
346 | $currentParams['sort'] = $field; |
347 | $currentParams['asc'] = $newSortAsc; |
348 | $currentParams['desc'] = $newSortDesc; |
349 | |
350 | $href = $this->getTitle()->getLocalURL( $currentParams ); |
351 | |
352 | $headerCells .= Html::rawElement( |
353 | 'th', |
354 | [ |
355 | 'scope' => 'col', |
356 | 'class' => 'cdx-table__table__cell--has-sort ' . $class, |
357 | 'aria-sort' => $ariaSort, |
358 | ], |
359 | Html::rawElement( |
360 | 'a', |
361 | [ 'href' => $href ], |
362 | Html::rawElement( |
363 | 'button', |
364 | [ |
365 | 'class' => 'cdx-table__table__sort-button', |
366 | 'aria-selected' => $isCurrentSortField ? 'true' : 'false' |
367 | ], |
368 | $this->msg( $labelKey )->text() . ' ' . |
369 | Html::rawElement( |
370 | 'span', |
371 | [ 'class' => 'cdx-icon cdx-icon--small cdx-table__table__sort-icon ' . |
372 | $iconClass, 'aria-hidden' => 'true' ] |
373 | ) |
374 | ) |
375 | ) |
376 | ); |
377 | } else { |
378 | $headerCells .= Html::rawElement( |
379 | 'th', |
380 | [ 'scope' => 'col', 'class' => $class ], |
381 | Html::rawElement( |
382 | 'span', |
383 | [ 'class' => 'cdx-table__th-content' ], |
384 | $this->msg( $labelKey )->text() |
385 | ) |
386 | ); |
387 | } |
388 | } |
389 | |
390 | return Html::rawElement( |
391 | 'thead', |
392 | [], |
393 | Html::rawElement( |
394 | 'tr', |
395 | [], |
396 | $headerCells |
397 | ) |
398 | ); |
399 | } |
400 | |
401 | /** |
402 | * Builds and returns the end body HTML for the table. |
403 | * |
404 | * @return string HTML |
405 | * @since 1.43 |
406 | */ |
407 | protected function getEndBody(): string { |
408 | return Html::closeElement( 'tbody' ) . |
409 | Html::closeElement( 'table' ) . |
410 | Html::closeElement( 'div' ) . |
411 | $this->buildTableCaption( 'cdx-table__footer' ) . |
412 | Html::closeElement( 'div' ); |
413 | } |
414 | |
415 | /** |
416 | * Builds and returns the table caption, currently used both in |
417 | * the header and the footer. |
418 | * |
419 | * @param string $class The class to use for the returning element |
420 | * @return string HTML |
421 | */ |
422 | private function buildTableCaption( string $class ): string { |
423 | $pendingCount = $this->getPendingCount(); |
424 | $formattedCount = $this->getLanguage()->formatNum( $pendingCount ); |
425 | $chip = Html::element( 'strong', [ 'class' => 'cdx-info-chip' ], $formattedCount ); |
426 | $message = $this->msg( 'pendingchanges-table-footer', $chip ) |
427 | ->numParams( $pendingCount )->text(); |
428 | |
429 | return Html::rawElement( |
430 | 'div', |
431 | [ 'class' => $class ], |
432 | Html::rawElement( 'span', [], $message ) |
433 | ); |
434 | } |
435 | } |