Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 301
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 / 301
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 / 5
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 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 feedItem
0.00% covered (danger)
0.00%
0 / 22
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\Exception\MWException;
4use MediaWiki\Feed\FeedItem;
5use MediaWiki\Feed\FeedUtils;
6use MediaWiki\Html\Html;
7use MediaWiki\MainConfigNames;
8use MediaWiki\RecentChanges\ChangesList;
9use MediaWiki\Revision\RevisionLookup;
10use MediaWiki\SpecialPage\SpecialPage;
11use MediaWiki\SpecialPage\SpecialPageFactory;
12use MediaWiki\Title\NamespaceInfo;
13use MediaWiki\Title\Title;
14
15class 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}