Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 197
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialProofreadPages
0.00% covered (danger)
0.00%
0 / 197
0.00% covered (danger)
0.00%
0 / 14
2070
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 / 44
0.00% covered (danger)
0.00%
0 / 1
132
 reallyDoQuery
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 preprocessResults
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isExpensive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isSyndicated
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isCacheable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 linkParameters
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 displaySearchForm
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
2
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
72
 buildValueField
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
56
 sortDescending
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 formatResult
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
30
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 * @ingroup SpecialPage
20 */
21
22namespace ProofreadPage\Special;
23
24use ISearchResultSet;
25use MediaWiki\Html\Html;
26use MediaWiki\HTMLForm\HTMLForm;
27use MediaWiki\MediaWikiServices;
28use MediaWiki\SpecialPage\QueryPage;
29use MediaWiki\Title\Title;
30use ProofreadPage\Context;
31use SearchResult;
32
33class SpecialProofreadPages extends QueryPage {
34    /** @var string|null */
35    protected $searchTerm;
36    /** @var string[]|null */
37    protected $searchList;
38    /** @var bool|null */
39    protected $suppressSqlOffset;
40    /** @var string|null */
41    protected $queryFilter;
42    /** @var string|null */
43    protected $queryOrder;
44    /** @var bool|null */
45    protected $sortAscending;
46    /** @var true|null */
47    protected $addOne;
48
49    /**
50     * @param string $name
51     */
52    public function __construct( $name = 'IndexPages' ) {
53        parent::__construct( $name );
54        $this->mIncludable = true;
55    }
56
57    /**
58     * @inheritDoc
59     */
60    public function execute( $parameters ) {
61        $this->setHeaders();
62        if ( $this->limit == 0 && $this->offset == 0 ) {
63            [ $this->limit, $this->offset ] = $this->getRequest()
64                ->getLimitOffsetForUser( $this->getUser() );
65        }
66        $output = $this->getOutput();
67        $request = $this->getRequest();
68        $output->addModules( 'ext.proofreadpage.special.indexpages' );
69        $output->addWikiTextAsContent(
70            $this->msg( 'proofreadpage_specialpage_text' )->inContentLanguage()->plain()
71        );
72
73        $this->searchList = null;
74        $this->searchTerm = $request->getText( 'key' );
75        $this->queryFilter = $request->getText( 'filter' );
76        $this->queryOrder = $request->getText( 'order' );
77        $this->sortAscending = $request->getBool( 'sortascending' );
78        $this->suppressSqlOffset = false;
79
80        // don't show navigation if included in another page
81        $this->shownavigation = !$this->including();
82
83        if ( !$this->getConfig()->get( 'DisableTextSearch' ) ) {
84            if ( !$this->including() ) {
85                // Only show the search form when not including in another page.
86                $this->displaySearchForm();
87            }
88
89            if ( $this->searchTerm ) {
90                $indexNamespaceId = Context::getDefaultContext()->getIndexNamespaceId();
91                $searchEngine = MediaWikiServices::getInstance()->getSearchEngineFactory()->create();
92                $searchEngine->setLimitOffset( $this->limit + 1, $this->offset );
93                $searchEngine->setNamespaces( [ $indexNamespaceId ] );
94                $status = $searchEngine->searchText( $this->searchTerm );
95                if ( $status instanceof ISearchResultSet ) {
96                    $textMatches = $status;
97                    $status = null;
98                } elseif ( $status->isOK() ) {
99                    $textMatches = $status->getValue();
100                } else {
101                    $textMatches = null;
102                }
103                if ( !( $textMatches instanceof ISearchResultSet ) ) {
104                    // TODO: $searchEngine->searchText() can return status objects
105                    // Might want to extract some information from them
106                    $output->showErrorPage(
107                        'proofreadpage_specialpage_searcherror',
108                        'proofreadpage_specialpage_searcherrortext'
109                    );
110                } else {
111                    $this->searchList = [];
112                    /** @var SearchResult $result */
113                    foreach ( $textMatches as $result ) {
114                        $title = $result->getTitle();
115                        if ( $title->inNamespace( $indexNamespaceId ) ) {
116                            array_push( $this->searchList, $title->getDBkey() );
117                        }
118                    }
119                    $this->suppressSqlOffset = true;
120                }
121            }
122        }
123
124        parent::execute( $parameters );
125    }
126
127    /**
128     * Wrapper function for parent function in QueryPage class
129     * @param int|false $limit
130     * @param int|false $offset
131     * @return \Wikimedia\Rdbms\IResultWrapper
132     */
133    public function reallyDoQuery( $limit, $offset = false ) {
134        if ( $this->searchList && count( $this->searchList ) > $this->limit ) {
135            // Delete the last item to avoid the sort done by reallyDoQuery move it
136            // to another position than the last
137            $this->addOne = true;
138            array_pop( $this->searchList );
139        }
140        if ( $this->suppressSqlOffset ) {
141            // Bug #27678: Do not use offset here, because it was already used in
142            // search performed by execute method
143            return parent::reallyDoQuery( $limit, false );
144        }
145        return parent::reallyDoQuery( $limit, $offset );
146    }
147
148    /**
149     * Increments $numRows if the last item of the result has been deleted
150     * @param \Wikimedia\Rdbms\IDatabase $dbr [optional] (unused parameter)
151     * @param \Wikimedia\Rdbms\IResultWrapper $res [optional] (unused parameter)
152     */
153    public function preprocessResults( $dbr, $res ) {
154        if ( $this->addOne !== null ) {
155            // there is a deleted item
156            $this->numRows++;
157        }
158    }
159
160    /** @inheritDoc */
161    public function isExpensive() {
162        // FIXME: the query does filesort, so we're kinda lying here right now
163        return false;
164    }
165
166    /** @inheritDoc */
167    public function isSyndicated() {
168        return false;
169    }
170
171    /** @inheritDoc */
172    public function isCacheable() {
173        // The page is not cacheable due to its search capabilities
174        return false;
175    }
176
177    public function linkParameters() {
178        return [
179            'key' => $this->searchTerm,
180            'filter' => $this->queryFilter,
181            'order' => $this->queryOrder,
182            'sortascending' => $this->sortAscending
183        ];
184    }
185
186    /**
187     * Display the HTMLForm of the search form.
188     */
189    protected function displaySearchForm() {
190        $formDescriptor = [
191            'key' => [
192                'type' => 'text',
193                'name' => 'key',
194                'id' => 'key',
195                'label-message' => 'proofreadpage_specialpage_label_key',
196                'size' => 50,
197                'default' => $this->searchTerm,
198            ],
199            'filter' => [
200                'type' => 'select',
201                'name' => 'filter',
202                'id' => 'filter',
203                'label-message' => 'proofreadpage_specialpage_label_filterby',
204                'default' => $this->queryFilter,
205                'options' => [
206                    $this->msg( 'proofreadpage_specialpage_filterby_all' )->text() => 'all',
207                    $this->msg( 'proofreadpage_specialpage_filterby_incomplete' )->text() => 'incomplete',
208                    $this->msg( 'proofreadpage_specialpage_filterby_proofread' )->text() => 'proofread',
209                    $this->msg( 'proofreadpage_specialpage_filterby_proofread_or_validated' )->text() =>
210                        'proofreadOrValidated',
211                    $this->msg( 'proofreadpage_specialpage_filterby_validated' )->text() => 'validated'
212                ]
213            ],
214            'order' => [
215                'type' => 'select',
216                'name' => 'order',
217                'id' => 'order',
218                'label-message' => 'proofreadpage_specialpage_label_orderby',
219                'default' => $this->queryOrder,
220                'options' => [
221                    $this->msg( 'proofreadpage_index_status' )->text() => 'quality',
222                    $this->msg( 'proofreadpage_index_size' )->text() => 'size',
223                    $this->msg( 'proofreadpage_pages_to_proofread' )->text() => 'toProofread',
224                    $this->msg( 'proofreadpage_pages_to_validate' )->text() => 'toValidate',
225                    $this->msg( 'proofreadpage_pages_to_proofread_or_validate' )->text() =>
226                        'toProofreadOrValidate',
227                    $this->msg( 'proofreadpage_alphabeticalorder' )->text() => 'alpha',
228                ]
229            ],
230            'sortascending' => [
231                'type' => 'check',
232                'name' => 'sortascending',
233                'id' => 'sortascending',
234                'label-message' => 'proofreadpage_specialpage_label_sortascending',
235                'selected' => $this->sortAscending,
236            ]
237        ];
238
239        $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
240        $htmlForm
241            ->addHiddenField( 'limit', $this->limit )
242            ->setMethod( 'get' )
243            ->setSubmitTextMsg( 'ilsubmit' )
244            ->setWrapperLegendMsg( 'proofreadpage_specialpage_legend' )
245            ->prepareForm()
246            ->displayForm( false );
247    }
248
249    /**
250     * @return mixed[]
251     */
252    public function getQueryInfo() {
253        $conds = [];
254        if ( $this->searchTerm ) {
255            if ( $this->searchList !== null ) {
256                $conds = [ 'page_namespace' => Context::getDefaultContext()->getIndexNamespaceId() ];
257                if ( $this->searchList ) {
258                    $conds['page_title'] = $this->searchList;
259                } else {
260                    // If not pages were found do not return results
261                    $conds[] = '1=0';
262                }
263            }
264        }
265
266        switch ( $this->queryFilter ) {
267            case 'incomplete':
268                $conds[] = 'pr_count - pr_q0 - pr_q3 - pr_q4 > 0';
269                break;
270            case 'proofread':
271                $conds[] = 'pr_count > 0 and pr_q3 > 0 and pr_count - pr_q0 - pr_q3 - pr_q4 = 0';
272                break;
273            case 'proofreadOrValidated':
274                $conds[] = 'pr_count > 0 and pr_count - pr_q0 - pr_q3 - pr_q4 = 0';
275                break;
276            case 'validated':
277                $conds[] = 'pr_count > 0 and pr_count - pr_q0 - pr_q4 = 0';
278                break;
279        }
280
281        return [
282            'tables' => [ 'pr_index', 'page' ],
283            'fields' => [
284                'namespace' => 'page_namespace',
285                'title' => 'page_title',
286                'value' => $this->buildValueField(),
287                'pr_count', 'pr_q0', 'pr_q1', 'pr_q2', 'pr_q3', 'pr_q4'
288            ],
289            'conds' => $conds,
290            'options' => [],
291            'join_conds' => [ 'page' => [ 'INNER JOIN', 'page_id=pr_page_id' ] ]
292        ];
293    }
294
295    private function buildValueField() {
296        switch ( $this->queryOrder ) {
297            case 'size':
298                return 'pr_count';
299            case 'alpha':
300                return 'page_title';
301            case 'toProofread':
302                return 'pr_count - pr_q4 - pr_q3 - pr_q0';
303            case 'toValidate':
304                return 'pr_q3';
305            case 'toProofreadOrValidate':
306                return 'pr_count - pr_q4 - pr_q0';
307            default:
308                return '2 * pr_q4 + pr_q3';
309        }
310    }
311
312    public function sortDescending() {
313        return !$this->sortAscending;
314    }
315
316    /**
317     * @param \Skin $skin
318     * @param \stdClass $result Result row
319     * @return string|false false to skip the row
320     */
321    public function formatResult( $skin, $result ) {
322        $title = Title::makeTitleSafe( $result->namespace, $result->title );
323        if ( !$title ) {
324            return '<!-- Invalid title ' .
325                htmlspecialchars( "{$result->namespace}:{$result->title}" ) . '-->';
326        }
327        $plink = $this->isCached()
328            ? $this->getLinkRenderer()->makeLink( $title, $title->getText() )
329            : $this->getLinkRenderer()->makeKnownLink( $title, $title->getText() );
330
331        if ( !$title->exists() ) {
332            return "<del>{$plink}</del>";
333        }
334
335        $size = $result->pr_count;
336        $q0 = $result->pr_q0;
337        $q1 = $result->pr_q1;
338        $q2 = $result->pr_q2;
339        $q3 = $result->pr_q3;
340        $q4 = $result->pr_q4;
341        $num_void = $size - $q1 - $q2 - $q3 - $q4 - $q0;
342        $void_cell = $num_void
343            ? Html::element( 'td', [ 'class' => 'qualitye', 'style' => "width: {$num_void}px" ] )
344            : '';
345
346        $qualityOutput = Html::rawElement( 'table', [
347                'class' => 'pr_quality',
348                'title' => $this->msg( 'proofreadpage-indexquality-alt' )->numParams( $q4, $q3, $q1 )->text()
349            ], Html::rawElement( 'tr', [], implode( [
350                Html::element( 'td', [ 'class' => 'quality4', 'style' => "width: {$q4}px" ] ),
351                Html::element( 'td', [ 'class' => 'quality3', 'style' => "width: {$q3}px" ] ),
352                Html::element( 'td', [ 'class' => 'quality2', 'style' => "width: {$q2}px" ] ),
353                Html::element( 'td', [ 'class' => 'quality1', 'style' => "width: {$q1}px" ] ),
354                Html::element( 'td', [ 'class' => 'quality0', 'style' => "width: {$q0}px" ] ),
355            ] ) . $void_cell ) );
356
357        $pages = $this->msg( 'proofreadpage_pages', $size )->numParams( $size )->text();
358
359        return "<div class=\"prp-indexpages-row\"><span><bdi>{$plink}</bdi> [$pages]</span>" .
360            "<div>{$qualityOutput}</div></div>";
361    }
362
363    /**
364     * @return string
365     */
366    protected function getGroupName() {
367        return 'pages';
368    }
369}