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