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