Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 151 |
|
0.00% |
0 / 12 |
CRAP | |
0.00% |
0 / 1 |
SpecialPage | |
0.00% |
0 / 151 |
|
0.00% |
0 / 12 |
1332 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDescription | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getQueryInfo | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
30 | |||
outputResults | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
12 | |||
getTableHeader | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
linkParameters | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
formatResult | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 | |||
getOrderFields | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
sortDescending | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getForm | |
0.00% |
0 / 39 |
|
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 | |
22 | namespace MediaWiki\Extension\PageAssessments; |
23 | |
24 | use MediaWiki\Html\Html; |
25 | use MediaWiki\HTMLForm\Field\HTMLTextField; |
26 | use MediaWiki\HTMLForm\HTMLForm; |
27 | use MediaWiki\Output\OutputPage; |
28 | use MediaWiki\SpecialPage\QueryPage; |
29 | use MediaWiki\Status\Status; |
30 | use MediaWiki\Title\Title; |
31 | use Skin; |
32 | use Wikimedia\Rdbms\IReadableDatabase; |
33 | use 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 | */ |
39 | class 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 | } |