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