Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 151
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialPage
0.00% covered (danger)
0.00%
0 / 151
0.00% covered (danger)
0.00%
0 / 12
1332
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
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDescription
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
30
 outputResults
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
 getTableHeader
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 linkParameters
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 formatResult
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 getOrderFields
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 sortDescending
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getForm
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
56
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 Extensions
20 */
21
22namespace MediaWiki\Extension\PageAssessments;
23
24use MediaWiki\Html\Html;
25use MediaWiki\HTMLForm\Field\HTMLTextField;
26use MediaWiki\HTMLForm\HTMLForm;
27use MediaWiki\Output\OutputPage;
28use MediaWiki\SpecialPage\QueryPage;
29use MediaWiki\Status\Status;
30use MediaWiki\Title\Title;
31use Skin;
32use Wikimedia\Rdbms\IReadableDatabase;
33use Wikimedia\Rdbms\IResultWrapper;
34
35/**
36 * A special page for searching Page Assessments. Can also be transcluded (in which case the
37 * search results' sorting links will be disabled).
38 */
39class SpecialPage extends QueryPage {
40
41    /**
42     * Create this special page, giving it a name and making it transcludable.
43     */
44    public function __construct() {
45        parent::__construct();
46        $this->mName = 'PageAssessments';
47        $this->mIncludable = true;
48    }
49
50    /**
51     * List the page under "Page tools" at Special:SpecialPages
52     * @return string
53     */
54    protected function getGroupName() {
55        return 'pagetools';
56    }
57
58    /**
59     * Returns the name that goes in the \<h1\> in the special page itself, and
60     * also the name that will be listed in Special:Specialpages.
61     *
62     * Overridden here because we want proper sentence casing, rather than 'PageAssessments'.
63     *
64     * @inheritDoc
65     */
66    public function getDescription() {
67        return $this->msg( 'pageassessments-special' );
68    }
69
70    /**
71     * The information for the database query. Don't include an ORDER or LIMIT clause, they will
72     * be added.
73     * @return array[]
74     */
75    public function getQueryInfo() {
76        $info = [
77            'tables' => [ 'page_assessments', 'page_assessments_projects', 'page', 'revision' ],
78            'fields' => [
79                'project' => 'pap_project_title',
80                'class' => 'pa_class',
81                'importance' => 'pa_importance',
82                'timestamp' => 'rev_timestamp',
83                'page_title' => 'page_title',
84                'page_revision' => 'pa_page_revision',
85                'page_namespace' => 'page_namespace',
86            ],
87            'conds' => [],
88            'options' => [],
89            'join_conds' => [
90                'page_assessments_projects' => [ 'JOIN', 'pa_project_id = pap_project_id' ],
91                'page' => [ 'JOIN', 'pa_page_id = page_id' ],
92                'revision' => [ 'JOIN', 'page_id = rev_page AND pa_page_revision = rev_id' ],
93            ],
94        ];
95        $request = $this->getRequest();
96        // Project.
97        $project = $request->getVal( 'project', '' );
98        if ( $project !== '' ) {
99            $info['conds']['pap_project_title'] = $project;
100        }
101        // Page title.
102        $pageTitle = $request->getVal( 'page_title', '' );
103        $pageTitle = Title::newFromText( $pageTitle );
104
105        if ( $pageTitle ) {
106            $info['conds']['page_title'] = $pageTitle->getDBkey();
107        }
108        // Namespace (if it's set, it's either an integer >= 0, 'all', or the empty string).
109        $namespace = $request->getVal( 'namespace', '' );
110        if ( $namespace !== '' && $namespace !== 'all' ) {
111            $info['conds']['page_namespace'] = $namespace;
112        }
113        return $info;
114    }
115
116    /**
117     * Format and output report results.
118     *
119     * @param OutputPage $out OutputPage to print to
120     * @param Skin $skin User skin to use
121     * @param IReadableDatabase $dbr Database (read) connection to use
122     * @param IResultWrapper $res Result pointer
123     * @param int $num Number of available result rows
124     * @param int $offset Paging offset
125     * @return bool False if no results are displayed, true otherwise.
126     */
127    protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
128        // Don't display anything if there are no results.
129        if ( $num < 1 ) {
130            return false;
131        }
132        $out->addModuleStyles( 'mediawiki.pager.styles' );
133        $tableClasses = 'mw-datatable page-assessments TablePager';
134        $html = Html::openElement( 'table', [ 'class' => $tableClasses ] )
135            . Html::openElement( 'thead' )
136            . Html::openElement( 'tr' )
137            . $this->getTableHeader( 'project', 'project' )
138            . $this->getTableHeader( 'page_title', 'page-title' )
139            . Html::element( 'th', [], $this->msg( 'pageassessments-importance' )->text() )
140            . Html::element( 'th', [], $this->msg( 'pageassessments-class' )->text() )
141            . Html::element( 'th', [], $this->msg( 'pageassessments-timestamp' )->text() )
142            . Html::closeElement( 'tr' )
143            . Html::closeElement( 'thead' )
144            . Html::openElement( 'tbody' );
145
146        foreach ( $res as $row ) {
147            $html .= $this->formatResult( $skin, $row );
148        }
149        $html .= Html::closeElement( 'tbody' )
150            . Html::closeElement( 'table' );
151        $out->addHTML( $html );
152        return true;
153    }
154
155    /**
156     * Get a HTML TH element, with a link to sort the column.
157     * @param string $field The field that this column contains.
158     * @param string $messageKey The i18n message (suffix) for the column header.
159     * @return string HTML table header.
160     */
161    protected function getTableHeader( $field, $messageKey ) {
162        // message keys: pageassessments-project, pageassessments-page-title
163        $text = $this->msg( 'pageassessments-' . $messageKey )->text();
164
165        // If this special page is being included, don't enable header sorting.
166        if ( $this->including() ) {
167            return Html::element( 'th', [], $text );
168        }
169
170        // Otherwise, set up the correct link.
171        $query = $this->getRequest()->getValues();
172        $cellAttrs = [];
173        if ( $this->getOrderFields() == [ $field ] ) {
174            // Currently sorted by this field.
175            $query['dir'] = $this->sortDescending() ? 'asc' : 'desc';
176            $currentDir = $this->sortDescending() ? 'descending' : 'ascending';
177            $cellAttrs['class'] = 'TablePager_sort-' . $currentDir;
178        } else {
179            $query['dir'] = 'asc';
180            $query['sort'] = $field;
181        }
182        $link = $this->getLinkRenderer()->makeLink( $this->getPageTitle(), $text, [], $query );
183        return Html::rawElement( 'th', $cellAttrs, $link );
184    }
185
186    /**
187     * Add the query and sort parameters to the paging links (prev/next/lengths).
188     * @return string[]
189     */
190    public function linkParameters() {
191        $params = [];
192        $request = $this->getRequest();
193        foreach ( [ 'project', 'namespace', 'page_title', 'sort', 'dir' ] as $key ) {
194            $val = $request->getVal( $key, '' );
195            if ( $val !== '' ) {
196                $params[$key] = $val;
197            }
198        }
199        return $params;
200    }
201
202    /**
203     * Formats the results of the query for display. The skin is the current
204     * skin; you can use it for making links. The result is a single row of
205     * result data. You should be able to grab SQL results off of it.
206     * If the function returns false, the line output will be skipped.
207     * @param Skin $skin The current skin
208     * @param \stdClass $result Result row
209     * @return string
210     */
211    public function formatResult( $skin, $result ) {
212        $renderer = $this->getLinkRenderer();
213        $pageTitle = Title::newFromText(
214            $result->page_title,
215            $result->page_namespace
216        );
217
218        // Link to the page.
219        $pageLink = $renderer->makeKnownLink( $pageTitle );
220
221        // Timestamp of assessed revision.
222        $lang = $this->getLanguage();
223        $ts = $lang->userTimeAndDate( $result->timestamp, $this->getUser() );
224        $linkQuery = [ 'oldid' => $result->page_revision ];
225        $timestampLink = $renderer->makeKnownLink( $pageTitle, $ts, [], $linkQuery );
226
227        // HTML table row.
228        return Html::rawElement( 'tr', [],
229            Html::element( 'td', [], $result->project ) .
230            Html::rawElement( 'td', [], $pageLink ) .
231            Html::element( 'td', [], $result->importance ) .
232            Html::element( 'td', [], $result->class ) .
233            Html::rawElement( 'td', [], $timestampLink )
234        );
235    }
236
237    /**
238     * Output the special page.
239     * @param string|null $par
240     */
241    public function execute( $par ) {
242        // Set up.
243        $this->setHeaders();
244        $this->addHelpLink( 'Extension:PageAssessments' );
245
246        // Output form.
247        if ( !$this->including() ) {
248            $form = $this->getForm();
249            $form->show();
250        }
251
252        $request = $this->getRequest();
253        $ns = $request->getVal( 'namespace', '' );
254        $project = $request->getVal( 'project', '' );
255        $pageTitle = $request->getVal( 'page_title', '' );
256
257        // Require namespace and either project name or page title to execute query.
258        // Note that this also prevents execution on initial page load.
259        // See T168599 and discussion in T248280.
260        if ( ( $ns !== '' && $ns !== 'all' ) &&
261            ( $project !== '' || $pageTitle !== '' )
262        ) {
263            parent::execute( $par );
264        }
265    }
266
267    /**
268     * Get the fields that the results are currently ordered by.
269     * @return string[]
270     */
271    public function getOrderFields() {
272        $permitted = [ 'project', 'page_title' ];
273        $requested = $this->getRequest()->getVal( 'sort', '' );
274        if ( in_array( $requested, $permitted ) ) {
275            return [ $requested ];
276        }
277        return [];
278    }
279
280    /**
281     * Whether we're currently sorting descending, or ascending. Based on the request 'dir'
282     * value; anything starting with 'desc' is considered 'desecending'.
283     * @return bool
284     */
285    public function sortDescending() {
286        return stripos( $this->getRequest()->getVal( 'dir', 'asc' ), 'desc' ) === 0;
287    }
288
289    /**
290     * Get the search form. This also loads the required Javascript module and global JS variable.
291     * @return HTMLForm
292     */
293    protected function getForm() {
294        $this->getOutput()->addModules( 'ext.pageassessments.special' );
295
296        // Add a list of all projects to the page's JS.
297        $projects = $this->getDBLoadBalancer()->getConnection( DB_REPLICA )->newSelectQueryBuilder()
298            ->select( 'pap_project_title' )
299            ->from( 'page_assessments_projects' )
300            ->orderBy( 'pap_project_title' )
301            ->caller( __METHOD__ )
302            ->fetchFieldValues();
303        $this->getOutput()->addJsConfigVars( 'wgPageAssessmentProjects', $projects );
304
305        // Define the form fields.
306        $formDescriptor = [
307            'project' => [
308                'id' => 'pageassessments-project',
309                'class' => HTMLTextField::class,
310                'name' => 'project',
311                'label-message' => 'pageassessments-project',
312            ],
313            'namespace' => [
314                'id' => 'pageassessments-namespace',
315                'class' => NamespaceSelect::class,
316                'name' => 'namespace',
317                'label-message' => 'pageassessments-page-namespace',
318            ],
319            'page_title' => [
320                'id' => 'pageassessments-page-title',
321                'class' => HTMLTextField::class,
322                'name' => 'page_title',
323                'label-message' => 'pageassessments-page-title',
324            ],
325        ];
326
327        // Construct and return the form.
328        $form = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
329        $form->setMethod( 'get' );
330        $form->setSubmitTextMsg( 'pageassessments-search' );
331        $form->setSubmitCallback( static function ( array $data, HTMLForm $form ) {
332            // Filtering only by namespace can be slow, disallow it:
333            // https://phabricator.wikimedia.org/T168599
334            if ( $data['namespace'] !== null
335                && $data['namespace'] !== 'all'
336                && ( $data['project'] === null || $data['project'] === '' )
337                && ( $data['page_title'] === null || $data['page_title'] === '' )
338            ) {
339                return Status::newFatal( 'pageassessments-error-namespace-filter' );
340            }
341        } );
342        return $form;
343    }
344}