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