Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 151 |
|
0.00% |
0 / 13 |
CRAP | |
0.00% |
0 / 1 |
| SpecialPage | |
0.00% |
0 / 151 |
|
0.00% |
0 / 13 |
1406 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| isIncludable | |
0.00% |
0 / 1 |
|
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\Skin\Skin; |
| 29 | use MediaWiki\SpecialPage\QueryPage; |
| 30 | use MediaWiki\Status\Status; |
| 31 | use MediaWiki\Title\Title; |
| 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 | } |
| 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 | } |