Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 240
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
UnreviewedPages
0.00% covered (danger)
0.00%
0 / 240
0.00% covered (danger)
0.00%
0 / 8
992
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 showForm
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 1
42
 showPageList
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 formatRow
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
72
 getLineClass
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 updateQueryCache
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 1
42
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3use MediaWiki\Exception\PermissionsError;
4use MediaWiki\Html\Html;
5use MediaWiki\MainConfigNames;
6use MediaWiki\MediaWikiServices;
7use MediaWiki\Permissions\PermissionManager;
8use MediaWiki\SpecialPage\SpecialPage;
9use MediaWiki\Title\Title;
10use Wikimedia\Rdbms\IConnectionProvider;
11
12class UnreviewedPages extends SpecialPage {
13    /** @var UnreviewedPagesPager */
14    private $pager = null;
15
16    /** @var int */
17    private $currentUnixTS;
18
19    /** @var int */
20    private $namespace;
21
22    /** @var string */
23    private $category;
24
25    /** @var bool */
26    private $hideRedirs;
27
28    /** @var bool */
29    private $isMiser;
30
31    /** How many entries are at most stored in the cache */
32    private const CACHE_SIZE = 5000;
33
34    public function __construct(
35        private readonly IConnectionProvider $dbProvider,
36        private readonly PermissionManager $permissionManager,
37    ) {
38        parent::__construct( 'UnreviewedPages', 'unreviewedpages' );
39    }
40
41    /**
42     * @inheritDoc
43     */
44    public function execute( $par ) {
45        $request = $this->getRequest();
46
47        $this->isMiser = $this->getConfig()->get( MainConfigNames::MiserMode );
48
49        $this->setHeaders();
50        $this->addHelpLink( 'Help:Extension:FlaggedRevs' );
51        if ( !$this->permissionManager->userHasRight( $this->getUser(), 'unreviewedpages' ) ) {
52            throw new PermissionsError( 'unreviewedpages' );
53        }
54
55        $this->currentUnixTS = (int)wfTimestamp();
56
57        # Get default namespace
58        $this->namespace = $request->getInt( 'namespace', FlaggedRevs::getFirstReviewNamespace() );
59        $category = trim( $request->getVal( 'category', '' ) );
60        $catTitle = Title::makeTitleSafe( NS_CATEGORY, $category );
61        $this->category = $catTitle === null ? '' : $catTitle->getText();
62        $level = $request->getInt( 'level' );
63        $this->hideRedirs = $request->getBool( 'hideredirs', true );
64
65        $this->pager = new UnreviewedPagesPager( $this, !$this->isMiser,
66            $this->namespace, !$this->hideRedirs, $this->category, $level );
67
68        $this->showForm();
69        $this->showPageList();
70    }
71
72    private function showForm() {
73        # Add explanatory text
74        $this->getOutput()->addWikiMsg( 'unreviewedpages-list',
75            $this->getLanguage()->formatNum( $this->pager->getNumRows() ) );
76
77        # show/hide links
78        $link = $this->getLinkRenderer()->makeLink(
79            $this->getPageTitle(),
80            $this->msg( $this->hideRedirs ? 'show' : 'hide' )->text(),
81            [ 'class' => 'cdx-docs-link' ],
82            [
83                'hideredirs' => $this->hideRedirs ? '0' : '1',
84                'category' => $this->category,
85                'namespace' => $this->namespace,
86            ]
87        );
88        $showhideredirs = $this->msg( 'unreviewedpages-showhide-redirect' )->rawParams( $link )->escaped();
89
90        # Add form...
91        $form = Html::openElement( 'form', [
92                'name' => 'unreviewedpages',
93                'action' => $this->getConfig()->get( MainConfigNames::Script ),
94                'method' => 'get',
95                'class' => 'cdx-form mw-fr-form-container'
96            ] ) . "\n";
97
98        $form .= Html::openElement( 'fieldset', [ 'class' => 'cdx-field' ] ) . "\n";
99
100        $form .= Html::openElement( 'legend', [ 'class' => 'cdx-label' ] ) . "\n";
101        $form .= Html::rawElement( 'span', [ 'class' => 'cdx-label__label' ],
102            Html::element( 'span', [ 'class' => 'cdx-label__label__text' ],
103                $this->msg( 'unreviewedpages-legend' )->text() )
104        );
105        $form .= Html::closeElement( 'legend' ) . "\n";
106
107        $form .= Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) . "\n";
108
109        $form .= Html::openElement( 'div', [ 'class' => 'cdx-field__control cdx-align-right' ] ) . "\n";
110
111        # Namespace selector
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                Html::label( $this->msg( 'unreviewedpages-category' )->text(), 'category',
124                    [ 'class' => 'cdx-label__label' ] ) .
125                Html::input( 'category', $this->category, 'text', [
126                    'id' => 'category',
127                    'class' => 'cdx-text-input__input',
128                    'size' => 30
129                ] )
130            ) . "\n";
131
132        $form .= Html::rawElement(
133                'div',
134                [ 'class' => 'cdx-field__item' ],
135                $showhideredirs
136            ) . "\n";
137
138        $form .= Html::closeElement( 'div' ) . "\n";
139
140        $form .= Html::rawElement(
141                'div',
142                [ 'class' => 'cdx-field__control' ],
143                Html::submitButton( $this->msg( 'allpagessubmit' )->text(), [
144                    'class' => 'cdx-button cdx-button--action-progressive'
145                ] )
146            ) . "\n";
147
148        $form .= Html::closeElement( 'fieldset' ) . "\n";
149        $form .= Html::closeElement( 'form' ) . "\n";
150
151        # Query may get too slow to be live...
152        if ( $this->isMiser ) {
153            $dbr = $this->dbProvider->getReplicaDatabase();
154
155            $ts = $dbr->newSelectQueryBuilder()
156                ->select( 'qci_timestamp' )
157                ->from( 'querycache_info' )
158                ->where( [ 'qci_type' => 'fr_unreviewedpages' ] )
159                ->caller( __METHOD__ )
160                ->fetchField();
161            if ( $ts ) {
162                $ts = wfTimestamp( TS_MW, $ts );
163                $td = $this->getLanguage()->timeanddate( $ts );
164                $d = $this->getLanguage()->date( $ts );
165                $t = $this->getLanguage()->time( $ts );
166                $form .= $this->msg( 'perfcachedts', $td, $d, $t, self::CACHE_SIZE )->parseAsBlock();
167            } else {
168                $form .= $this->msg( 'perfcached', self::CACHE_SIZE )->parseAsBlock();
169            }
170        }
171
172        $this->getOutput()->addHTML( $form );
173    }
174
175    private function showPageList() {
176        $out = $this->getOutput();
177        if ( $this->pager->getNumRows() ) {
178            $out->addHTML( $this->pager->getNavigationBar() );
179            $out->addHTML( $this->pager->getBody() );
180            $out->addHTML( $this->pager->getNavigationBar() );
181        } else {
182            $out->addWikiMsg( 'unreviewedpages-none' );
183        }
184    }
185
186    /**
187     * @param stdClass $row
188     * @return string HTML
189     */
190    public function formatRow( $row ) {
191        $title = Title::newFromRow( $row );
192
193        $stxt = '';
194        $linkRenderer = $this->getLinkRenderer();
195        $link = $linkRenderer->makeLink( $title, null, [], [ 'redirect' => 'no' ] );
196        $dirmark = $this->getLanguage()->getDirMark();
197        $hist = $linkRenderer->makeKnownLink(
198            $title,
199            $this->msg( 'hist' )->text(),
200            [],
201            [ 'action' => 'history' ]
202        );
203        $size = $row->page_len;
204        if ( $size !== null ) {
205            $stxt = ( $size == 0 )
206                ? $this->msg( 'historyempty' )->escaped()
207                : $this->msg( 'historysize' )->numParams( $size )->escaped();
208            $stxt = " <small>$stxt</small>";
209        }
210        # Get how long the first unreviewed edit has been waiting...
211        $firstPendingTime = (int)wfTimestamp( TS_UNIX, $row->creation );
212        $hours = ( $this->currentUnixTS - $firstPendingTime ) / 3600;
213        $days = round( $hours / 24 );
214        if ( $days >= 3 ) {
215            $age = ' ' . $this->msg( 'unreviewedpages-days' )->numParams( $days )->escaped();
216        } elseif ( $hours >= 1 ) {
217            $age = ' ' . $this->msg( 'unreviewedpages-hours' )->numParams( round( $hours ) )->escaped();
218        } else {
219            $age = ' ' . $this->msg( 'unreviewedpages-recent' )->escaped(); // hot off the press :)
220        }
221        if ( $this->permissionManager->userHasRight( $this->getUser(), 'unwatchedpages' ) ) {
222            $uw = FRUserActivity::numUsersWatchingPage( $title );
223            $watching = ' ';
224            $watching .= $uw
225                ? $this->msg( 'unreviewedpages-watched' )->numParams( $uw )->escaped()
226                : $this->msg( 'unreviewedpages-unwatched' )->escaped();
227        } else {
228            $uw = -1;
229            $watching = '';
230        }
231        $css = $this->getLineClass( $hours, $uw );
232        $css = $css ? " class='$css'" : "";
233
234        return ( "<li{$css}>{$link} $dirmark {$stxt} ({$hist})" .
235            "{$age}{$watching}</li>" );
236    }
237
238    /**
239     * @param float $hours
240     * @param int $numUsersWatching Number of users or -1 when not allowed to see the number
241     * @return string
242     */
243    private function getLineClass( $hours, $numUsersWatching ) {
244        $days = $hours / 24;
245        if ( $numUsersWatching == 0 ) {
246            return 'fr-unreviewed-unwatched';
247        } elseif ( $days > 20 ) {
248            return 'fr-pending-long2';
249        } elseif ( $days > 7 ) {
250            return 'fr-pending-long';
251        } else {
252            return '';
253        }
254    }
255
256    /**
257     * Run an update to the cached query rows
258     * @return void
259     */
260    public static function updateQueryCache() {
261        $rNamespaces = FlaggedRevs::getReviewNamespaces();
262        if ( !$rNamespaces ) {
263            return;
264        }
265        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
266
267        $insertRows = [];
268        // Find pages that were never reviewed at all...
269        $res = $dbr->newSelectQueryBuilder()
270            ->select( [ 'page_namespace', 'page_title', 'page_id' ] )
271            ->from( 'page' )
272            ->leftJoin( 'flaggedpages', null, 'fp_page_id = page_id' )
273            ->where( [
274                'page_namespace' => $rNamespaces,
275                'page_is_redirect' => 0, // no redirects
276                'fp_page_id' => null,
277            ] )
278            ->limit( self::CACHE_SIZE )
279            ->caller( __METHOD__ )
280            ->fetchResultSet();
281        foreach ( $res as $row ) {
282            $insertRows[] = [
283                'qc_type'       => 'fr_unreviewedpages',
284                'qc_namespace'  => $row->page_namespace,
285                'qc_title'      => $row->page_title,
286                'qc_value'      => $row->page_id
287            ];
288        }
289
290        $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
291
292        $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
293        $dbw->startAtomic( __METHOD__ );
294        # Clear out any old cached data
295        $dbw->newDeleteQueryBuilder()
296            ->deleteFrom( 'querycache' )
297            ->where( [ 'qc_type' => 'fr_unreviewedpages' ] )
298            ->caller( __METHOD__ )
299            ->execute();
300        # Insert new data...
301        if ( $insertRows ) {
302            $dbw->newInsertQueryBuilder()
303                ->insertInto( 'querycache' )
304                ->rows( $insertRows )
305                ->caller( __METHOD__ )
306                ->execute();
307        }
308        # Update the querycache_info record for the page
309        $dbw->newDeleteQueryBuilder()
310            ->deleteFrom( 'querycache_info' )
311            ->where( [ 'qci_type' => 'fr_unreviewedpages' ] )
312            ->caller( __METHOD__ )
313            ->execute();
314        $dbw->newInsertQueryBuilder()
315            ->insertInto( 'querycache_info' )
316            ->row( [ 'qci_type' => 'fr_unreviewedpages', 'qci_timestamp' => $dbw->timestamp() ] )
317            ->caller( __METHOD__ )
318            ->execute();
319        $dbw->endAtomic( __METHOD__ );
320        $lbFactory->commitPrimaryChanges( __METHOD__ );
321
322        $insertRows = [];
323        // Find pages that were never marked as "quality"...
324        $res = $dbr->newSelectQueryBuilder()
325            ->select( [ 'page_namespace', 'page_title', 'page_id' ] )
326            ->from( 'page' )
327            ->leftJoin( 'flaggedpages', null, 'fp_page_id = page_id' )
328            ->where( [
329                'page_namespace' => $rNamespaces,
330                'page_is_redirect' => 0, // no redirects
331                $dbr->expr( 'fp_page_id', '=', null )->or( 'fp_quality', '=', 0 ),
332            ] )
333            ->limit( self::CACHE_SIZE )
334            ->caller( __METHOD__ )
335            ->fetchResultSet();
336        foreach ( $res as $row ) {
337            $insertRows[] = [
338                'qc_type'       => 'fr_unreviewedpages_q',
339                'qc_namespace'  => $row->page_namespace,
340                'qc_title'      => $row->page_title,
341                'qc_value'      => $row->page_id
342            ];
343        }
344
345        $dbw->startAtomic( __METHOD__ );
346        # Clear out any old cached data
347        $dbw->newDeleteQueryBuilder()
348            ->deleteFrom( 'querycache' )
349            ->where( [ 'qc_type' => 'fr_unreviewedpages_q' ] )
350            ->caller( __METHOD__ )
351            ->execute();
352        # Insert new data...
353        if ( $insertRows ) {
354            $dbw->newInsertQueryBuilder()
355                ->insertInto( 'querycache' )
356                ->rows( $insertRows )
357                ->caller( __METHOD__ )
358                ->execute();
359        }
360        # Update the querycache_info record for the page
361        $dbw->newDeleteQueryBuilder()
362            ->deleteFrom( 'querycache_info' )
363            ->where( [ 'qci_type' => 'fr_unreviewedpages_q' ] )
364            ->caller( __METHOD__ )
365            ->execute();
366        $dbw->newInsertQueryBuilder()
367            ->insertInto( 'querycache_info' )
368            ->row( [ 'qci_type' => 'fr_unreviewedpages_q', 'qci_timestamp' => $dbw->timestamp() ] )
369            ->caller( __METHOD__ )
370            ->execute();
371        $dbw->endAtomic( __METHOD__ );
372        $lbFactory->commitPrimaryChanges( __METHOD__ );
373    }
374
375    /**
376     * @return string
377     */
378    protected function getGroupName() {
379        return 'quality';
380    }
381}