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