Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 300
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
PendingChanges
0.00% covered (danger)
0.00%
0 / 300
0.00% covered (danger)
0.00%
0 / 14
2550
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
56
 setSyndicated
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 showForm
0.00% covered (danger)
0.00%
0 / 84
0.00% covered (danger)
0.00%
0 / 1
6
 getLimitSelector
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
6
 showPageList
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 parseParams
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
56
 feed
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 feedTitle
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 feedItem
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
20
 formatRow
0.00% covered (danger)
0.00%
0 / 73
0.00% covered (danger)
0.00%
0 / 1
156
 getLineClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getWatchingFormatted
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3use MediaWiki\Feed\FeedItem;
4use MediaWiki\Feed\FeedUtils;
5use MediaWiki\Html\Html;
6use MediaWiki\MainConfigNames;
7use MediaWiki\MediaWikiServices;
8use MediaWiki\SpecialPage\SpecialPage;
9use MediaWiki\Title\Title;
10
11class 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}