Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 301 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 1 |
PendingChanges | |
0.00% |
0 / 301 |
|
0.00% |
0 / 14 |
2550 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
56 | |||
setSyndicated | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
showForm | |
0.00% |
0 / 84 |
|
0.00% |
0 / 1 |
6 | |||
getLimitSelector | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
6 | |||
showPageList | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
parseParams | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
56 | |||
feed | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
30 | |||
feedTitle | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
feedItem | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
20 | |||
formatRow | |
0.00% |
0 / 73 |
|
0.00% |
0 / 1 |
156 | |||
getLineClass | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getWatchingFormatted | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | use MediaWiki\Exception\MWException; |
4 | use MediaWiki\Feed\FeedItem; |
5 | use MediaWiki\Feed\FeedUtils; |
6 | use MediaWiki\Html\Html; |
7 | use MediaWiki\MainConfigNames; |
8 | use MediaWiki\RecentChanges\ChangesList; |
9 | use MediaWiki\Revision\RevisionLookup; |
10 | use MediaWiki\SpecialPage\SpecialPage; |
11 | use MediaWiki\SpecialPage\SpecialPageFactory; |
12 | use MediaWiki\Title\NamespaceInfo; |
13 | use MediaWiki\Title\Title; |
14 | |
15 | class PendingChanges extends SpecialPage { |
16 | |
17 | private ?PendingChangesPager $pager = null; |
18 | private int $currentUnixTS; |
19 | private ?int $namespace; |
20 | private string $category; |
21 | private ?string $tagFilter; |
22 | private ?int $size; |
23 | private bool $watched; |
24 | |
25 | private NamespaceInfo $namespaceInfo; |
26 | private RevisionLookup $revisionLookup; |
27 | private SpecialPageFactory $specialPageFactory; |
28 | |
29 | public function __construct( |
30 | NamespaceInfo $namespaceInfo, |
31 | RevisionLookup $revisionLookup, |
32 | SpecialPageFactory $specialPageFactory |
33 | ) { |
34 | parent::__construct( 'PendingChanges' ); |
35 | $this->mIncludable = true; |
36 | $this->namespaceInfo = $namespaceInfo; |
37 | $this->revisionLookup = $revisionLookup; |
38 | $this->specialPageFactory = $specialPageFactory; |
39 | } |
40 | |
41 | /** |
42 | * @inheritDoc |
43 | * @throws MWException |
44 | */ |
45 | public function execute( $subPage ) { |
46 | $request = $this->getRequest(); |
47 | |
48 | $this->setHeaders(); |
49 | $this->addHelpLink( 'Help:Extension:FlaggedRevs' ); |
50 | $this->currentUnixTS = (int)wfTimestamp(); |
51 | |
52 | $this->namespace = $request->getIntOrNull( 'namespace' ); |
53 | $this->tagFilter = $request->getVal( 'tagFilter' ); |
54 | $category = trim( $request->getVal( 'category', '' ) ); |
55 | $catTitle = Title::makeTitleSafe( NS_CATEGORY, $category ); |
56 | $this->category = $catTitle === null ? '' : $catTitle->getText(); |
57 | $this->size = $request->getIntOrNull( 'size' ); |
58 | $this->watched = $request->getCheck( 'watched' ); |
59 | $stable = $request->getCheck( 'stable' ); |
60 | $feedType = $request->getVal( 'feed' ); |
61 | $limit = $request->getInt( 'limit', 50 ); |
62 | |
63 | $incLimit = 0; |
64 | if ( $this->including() && $subPage !== null ) { |
65 | $incLimit = $this->parseParams( $subPage ); // apply non-URL params |
66 | } |
67 | |
68 | $this->pager = new PendingChangesPager( $this, $this->namespace, |
69 | $this->category, $this->size, $this->watched, $stable, $this->tagFilter ); |
70 | $this->pager->setLimit( $limit ); |
71 | |
72 | # Output appropriate format... |
73 | if ( $feedType != null ) { |
74 | $this->feed( $feedType ); |
75 | } else { |
76 | if ( $this->including() ) { |
77 | if ( $incLimit ) { // limit provided |
78 | $this->pager->setLimit( $incLimit ); // apply non-URL limit |
79 | } |
80 | } else { |
81 | $this->setSyndicated(); |
82 | $this->showForm(); |
83 | } |
84 | $this->showPageList(); |
85 | } |
86 | } |
87 | |
88 | private function setSyndicated() { |
89 | $request = $this->getRequest(); |
90 | $queryParams = [ |
91 | 'namespace' => $request->getIntOrNull( 'namespace' ), |
92 | 'category' => $request->getVal( 'category' ), |
93 | ]; |
94 | $this->getOutput()->setSyndicated(); |
95 | $this->getOutput()->setFeedAppendQuery( wfArrayToCgi( $queryParams ) ); |
96 | } |
97 | |
98 | private function showForm() { |
99 | $form = Html::openElement( 'form', [ |
100 | 'name' => 'pendingchanges', |
101 | 'action' => $this->getConfig()->get( MainConfigNames::Script ), |
102 | 'method' => 'get', |
103 | 'class' => 'mw-fr-form-container' |
104 | ] ) . "\n"; |
105 | |
106 | $form .= Html::openElement( 'fieldset', [ 'class' => 'cdx-field' ] ) . "\n"; |
107 | |
108 | $form .= Html::openElement( 'legend', [ 'class' => 'cdx-label' ] ) . "\n"; |
109 | $form .= Html::rawElement( 'span', [ 'class' => 'cdx-label__label' ], |
110 | Html::element( 'span', [ 'class' => 'cdx-label__label__text' ], |
111 | $this->msg( 'pendingchanges-legend' )->text() ) |
112 | ); |
113 | |
114 | # Explanatory text |
115 | $form .= Html::rawElement( 'span', [ 'class' => 'cdx-label__description' ], |
116 | $this->msg( 'pendingchanges-list' )->params( |
117 | $this->getLanguage()->formatNum( $this->pager->getNumRows() ) |
118 | )->parse() |
119 | ); |
120 | |
121 | $form .= Html::closeElement( 'legend' ) . "\n"; |
122 | |
123 | $form .= Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) . "\n"; |
124 | |
125 | $form .= Html::openElement( 'div', [ 'class' => 'cdx-field__control' ] ) . "\n"; |
126 | |
127 | if ( count( FlaggedRevs::getReviewNamespaces() ) > 1 ) { |
128 | $form .= Html::rawElement( |
129 | 'div', |
130 | [ 'class' => 'cdx-field__item' ], |
131 | FlaggedRevsHTML::getNamespaceMenu( $this->namespace, '' ) |
132 | ); |
133 | } |
134 | |
135 | $form .= Html::rawElement( |
136 | 'div', |
137 | [ 'class' => 'cdx-field__item' ], |
138 | FlaggedRevsHTML::getEditTagFilterMenu( $this->tagFilter ) |
139 | ); |
140 | |
141 | $form .= Html::rawElement( |
142 | 'div', |
143 | [ 'class' => 'cdx-field__item' ], |
144 | $this->getLimitSelector( $this->pager->mLimit ) |
145 | ); |
146 | |
147 | $form .= Html::rawElement( |
148 | 'div', |
149 | [ 'class' => 'cdx-field__item' ], |
150 | Html::label( $this->msg( 'pendingchanges-category' )->text(), 'wpCategory', |
151 | [ 'class' => 'cdx-label__label' ] ) . |
152 | Html::input( 'category', $this->category, 'text', [ |
153 | 'id' => 'wpCategory', |
154 | 'class' => 'cdx-text-input__input' |
155 | ] ) |
156 | ) . "\n"; |
157 | |
158 | $form .= Html::rawElement( |
159 | 'div', |
160 | [ 'class' => 'cdx-field__item' ], |
161 | Html::label( $this->msg( 'pendingchanges-size' )->text(), 'wpSize', |
162 | [ 'class' => 'cdx-label__label' ] ) . |
163 | Html::input( 'size', (string)$this->size, 'number', [ |
164 | 'id' => 'wpSize', |
165 | 'class' => 'cdx-text-input__input' |
166 | ] ) |
167 | ) . "\n"; |
168 | |
169 | $form .= Html::closeElement( 'div' ) . "\n"; |
170 | |
171 | $form .= Html::rawElement( |
172 | 'div', |
173 | [ 'class' => 'cdx-field__control' ], |
174 | Html::rawElement( 'span', [ 'class' => 'cdx-checkbox cdx-checkbox--inline' ], |
175 | Html::check( 'watched', $this->watched, [ |
176 | 'id' => 'wpWatched', |
177 | 'class' => 'cdx-checkbox__input' |
178 | ] ) . |
179 | Html::rawElement( 'span', [ 'class' => 'cdx-checkbox__icon' ] ) . |
180 | Html::rawElement( |
181 | 'div', |
182 | [ 'class' => 'cdx-checkbox__label cdx-label' ], |
183 | Html::label( $this->msg( 'pendingchanges-onwatchlist' )->text(), 'wpWatched', |
184 | [ 'class' => 'cdx-label__label' ] ) |
185 | ) |
186 | ) |
187 | ) . "\n"; |
188 | |
189 | $form .= Html::rawElement( |
190 | 'div', |
191 | [ 'class' => 'cdx-field__control' ], |
192 | Html::submitButton( $this->msg( 'pendingchanges-filter-submit-button-text' )->text(), [ |
193 | 'class' => 'cdx-button cdx-button--action-progressive' |
194 | ] ) |
195 | ) . "\n"; |
196 | |
197 | $form .= Html::closeElement( 'fieldset' ) . "\n"; |
198 | $form .= Html::closeElement( 'form' ) . "\n"; |
199 | |
200 | $this->getOutput()->addHTML( $form ); |
201 | } |
202 | |
203 | /** |
204 | * Get a selector for limit options |
205 | * |
206 | * @param int $selected The currently selected limit |
207 | */ |
208 | private function getLimitSelector( int $selected = 20 ): string { |
209 | $s = Html::rawElement( 'div', [ 'class' => 'cdx-field__item' ], |
210 | Html::rawElement( 'div', [ 'class' => 'cdx-label' ], |
211 | Html::label( |
212 | $this->msg( 'pendingchanges-limit' )->text(), |
213 | 'wpLimit', |
214 | [ 'class' => 'cdx-label__label' ] |
215 | ) |
216 | ) |
217 | ); |
218 | |
219 | $options = [ 20, 50, 100 ]; |
220 | $selectOptions = ''; |
221 | foreach ( $options as $option ) { |
222 | $selectOptions .= Html::element( 'option', [ |
223 | 'value' => $option, |
224 | 'selected' => $selected == $option |
225 | ], $this->getLanguage()->formatNum( $option ) ); |
226 | } |
227 | |
228 | $s .= Html::rawElement( 'select', [ |
229 | 'name' => 'limit', |
230 | 'id' => 'wpLimit', |
231 | 'class' => 'cdx-select' |
232 | ], $selectOptions ); |
233 | |
234 | return $s; |
235 | } |
236 | |
237 | private function showPageList() { |
238 | $out = $this->getOutput(); |
239 | |
240 | if ( !$this->pager->getNumRows() ) { |
241 | $out->addWikiMsg( 'pendingchanges-none' ); |
242 | return; |
243 | } |
244 | |
245 | // To style output of ChangesList::showCharacterDifference |
246 | $out->addModuleStyles( 'mediawiki.special.changeslist' ); |
247 | $out->addModuleStyles( 'mediawiki.interface.helpers.styles' ); |
248 | |
249 | if ( $this->including() ) { |
250 | // If this list is transcluded... |
251 | $out->addHTML( $this->pager->getBody() ); |
252 | } else { |
253 | // Viewing the list normally... |
254 | $navigationBar = $this->pager->getNavigationBar(); |
255 | $out->addHTML( $navigationBar ); |
256 | $out->addHTML( $this->pager->getBody() ); |
257 | $out->addHTML( $navigationBar ); |
258 | } |
259 | } |
260 | |
261 | /** |
262 | * Set pager parameters from $subPage, return pager limit |
263 | * @param string $subPage |
264 | * @return bool|int |
265 | */ |
266 | private function parseParams( string $subPage ) { |
267 | $bits = preg_split( '/\s*,\s*/', trim( $subPage ) ); |
268 | $limit = false; |
269 | foreach ( $bits as $bit ) { |
270 | if ( is_numeric( $bit ) ) { |
271 | $limit = intval( $bit ); |
272 | } |
273 | $m = []; |
274 | if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) { |
275 | $limit = intval( $m[1] ); |
276 | } |
277 | if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) { |
278 | $ns = $this->getLanguage()->getNsIndex( $m[1] ); |
279 | if ( $ns !== false ) { |
280 | $this->namespace = $ns; |
281 | } |
282 | } |
283 | if ( preg_match( '/^category=(.+)$/', $bit, $m ) ) { |
284 | $this->category = $m[1]; |
285 | } |
286 | } |
287 | return $limit; |
288 | } |
289 | |
290 | /** |
291 | * Output a subscription feed listing recent edits to this page. |
292 | * @param string $type |
293 | * @throws MWException |
294 | */ |
295 | private function feed( string $type ) { |
296 | if ( !$this->getConfig()->get( MainConfigNames::Feed ) ) { |
297 | $this->getOutput()->addWikiMsg( 'feed-unavailable' ); |
298 | return; |
299 | } |
300 | |
301 | $feedClasses = $this->getConfig()->get( MainConfigNames::FeedClasses ); |
302 | if ( !isset( $feedClasses[$type] ) ) { |
303 | $this->getOutput()->addWikiMsg( 'feed-invalid' ); |
304 | return; |
305 | } |
306 | |
307 | $feed = new $feedClasses[$type]( |
308 | $this->feedTitle(), |
309 | $this->msg( 'tagline' )->text(), |
310 | $this->getPageTitle()->getFullURL() |
311 | ); |
312 | $this->pager->mLimit = min( $this->getConfig()->get( MainConfigNames::FeedLimit ), $this->pager->mLimit ); |
313 | |
314 | $feed->outHeader(); |
315 | if ( $this->pager->getNumRows() > 0 ) { |
316 | foreach ( $this->pager->mResult as $row ) { |
317 | $feed->outItem( $this->feedItem( $row ) ); |
318 | } |
319 | } |
320 | $feed->outFooter(); |
321 | } |
322 | |
323 | private function feedTitle(): string { |
324 | $languageCode = $this->getConfig()->get( MainConfigNames::LanguageCode ); |
325 | $sitename = $this->getConfig()->get( MainConfigNames::Sitename ); |
326 | |
327 | $page = $this->specialPageFactory->getPage( 'PendingChanges' ); |
328 | $desc = $page->getDescription(); |
329 | return "$sitename - $desc [$languageCode]"; |
330 | } |
331 | |
332 | /** |
333 | * @param stdClass $row |
334 | * @return FeedItem|null |
335 | * @throws MWException |
336 | */ |
337 | private function feedItem( stdClass $row ): ?FeedItem { |
338 | $title = Title::makeTitle( $row->page_namespace, $row->page_title ); |
339 | if ( !$title ) { |
340 | return null; |
341 | } |
342 | |
343 | $date = $row->pending_since; |
344 | $comments = $this->namespaceInfo->getTalkPage( $title ); |
345 | $curRevRecord = $this->revisionLookup->getRevisionByTitle( $title ); |
346 | $currentComment = $curRevRecord->getComment() ? $curRevRecord->getComment()->text : ''; |
347 | $currentUserText = $curRevRecord->getUser() ? $curRevRecord->getUser()->getName() : ''; |
348 | return new FeedItem( |
349 | $title->getPrefixedText(), |
350 | FeedUtils::formatDiffRow2( |
351 | $title, |
352 | $row->stable, |
353 | $curRevRecord->getId(), |
354 | $row->pending_since, |
355 | $currentComment |
356 | ), |
357 | $title->getFullURL(), |
358 | $date, |
359 | $currentUserText, |
360 | $comments |
361 | ); |
362 | } |
363 | |
364 | public function formatRow( stdClass $row ): string { |
365 | $css = ''; |
366 | $title = Title::newFromRow( $row ); |
367 | $size = ChangesList::showCharacterDifference( $row->rev_len, $row->page_len ); |
368 | # Page links... |
369 | $linkRenderer = $this->getLinkRenderer(); |
370 | |
371 | $query = $title->isRedirect() ? [ 'redirect' => 'no' ] : []; |
372 | |
373 | $link = $linkRenderer->makeKnownLink( |
374 | $title, |
375 | null, |
376 | [ 'class' => 'mw-fr-pending-changes-page-title' ], |
377 | $query |
378 | ); |
379 | $linkArr = []; |
380 | $linkArr[] = $linkRenderer->makeKnownLink( |
381 | $title, |
382 | $this->msg( 'hist' )->text(), |
383 | [ 'class' => 'mw-fr-pending-changes-page-history' ], |
384 | [ 'action' => 'history' ] |
385 | ); |
386 | if ( $this->getAuthority()->isAllowed( 'edit' ) ) { |
387 | $linkArr[] = $linkRenderer->makeKnownLink( |
388 | $title, |
389 | $this->msg( 'editlink' )->text(), |
390 | [ 'class' => 'mw-fr-pending-changes-page-edit' ], |
391 | [ 'action' => 'edit' ] |
392 | ); |
393 | } |
394 | if ( $this->getAuthority()->isAllowed( 'delete' ) ) { |
395 | $linkArr[] = $linkRenderer->makeKnownLink( |
396 | $title, |
397 | $this->msg( 'tags-delete' )->text(), |
398 | [ 'class' => 'mw-fr-pending-changes-page-delete' ], |
399 | [ 'action' => 'delete' ] |
400 | ); |
401 | } |
402 | $links = $this->msg( 'parentheses' )->rawParams( $this->getLanguage() |
403 | ->pipeList( $linkArr ) )->escaped(); |
404 | $review = Html::rawElement( |
405 | 'a', |
406 | [ |
407 | 'class' => 'cdx-docs-link', |
408 | 'href' => $title->getFullURL( [ 'diff' => 'cur', 'oldid' => $row->stable ] ) |
409 | ], |
410 | $this->msg( 'pendingchanges-diff' )->text() |
411 | ); |
412 | # Is anybody watching? |
413 | // Only show information to users with the `unwatchedpages` who could find this |
414 | // information elsewhere anyway, T281065 |
415 | if ( !$this->including() && $this->getAuthority()->isAllowed( 'unwatchedpages' ) ) { |
416 | $uw = FRUserActivity::numUsersWatchingPage( $title ); |
417 | $watching = ' '; |
418 | $watching .= $uw |
419 | ? $this->getWatchingFormatted( $uw ) |
420 | : $this->msg( 'pendingchanges-unwatched' )->escaped(); |
421 | } else { |
422 | $uw = -1; |
423 | $watching = ''; |
424 | } |
425 | # Get how long the first unreviewed edit has been waiting... |
426 | if ( $row->pending_since ) { |
427 | $firstPendingTime = (int)wfTimestamp( TS_UNIX, $row->pending_since ); |
428 | $hours = ( $this->currentUnixTS - $firstPendingTime ) / 3600; |
429 | $days = round( $hours / 24 ); |
430 | if ( $days >= 3 ) { |
431 | $age = $this->msg( 'pendingchanges-days' )->numParams( $days )->text(); |
432 | } elseif ( $hours >= 1 ) { |
433 | $age = $this->msg( 'pendingchanges-hours' )->numParams( round( $hours ) )->text(); |
434 | } else { |
435 | $age = $this->msg( 'pendingchanges-recent' )->text(); // hot off the press :) |
436 | } |
437 | $age = Html::element( 'span', [], $age ); |
438 | // Oh-noes! |
439 | $class = $this->getLineClass( $uw ); |
440 | $css = $class ? " $class" : ""; |
441 | } else { |
442 | $age = ""; |
443 | } |
444 | $watchingColumn = $watching ? "<td>$watching</td>" : ''; |
445 | return ( |
446 | "<tr class='$css'> |
447 | <td>$link $links</td> |
448 | <td class='cdx-table__table__cell--align-center'>$review</td> |
449 | <td>$size</td> |
450 | <td>$age</td> |
451 | $watchingColumn |
452 | </tr>" |
453 | ); |
454 | } |
455 | |
456 | /** |
457 | * @param int $numUsersWatching Number of users or -1 when not allowed to see the number |
458 | * @return string |
459 | */ |
460 | private function getLineClass( int $numUsersWatching ): string { |
461 | return $numUsersWatching == 0 ? 'fr-unreviewed-unwatched' : ''; |
462 | } |
463 | |
464 | /** |
465 | * @return string |
466 | */ |
467 | protected function getGroupName(): string { |
468 | return 'quality'; |
469 | } |
470 | |
471 | /** |
472 | * Get formatted text for the watching value |
473 | * |
474 | * @param int $watching |
475 | * @return string |
476 | * @since 1.43 |
477 | */ |
478 | public function getWatchingFormatted( int $watching ): string { |
479 | return $watching > 0 |
480 | ? Html::element( 'span', [], $this->getLanguage()->formatNum( $watching ) ) |
481 | : Html::rawElement( |
482 | 'div', |
483 | [ 'class' => 'cdx-info-chip' ], |
484 | Html::element( 'span', [ 'class' => 'cdx-info-chip--text' ], |
485 | $this->msg( 'pendingchanges-unwatched' )->text() ) |
486 | ); |
487 | } |
488 | } |