mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Linter
synced 2024-11-24 07:53:51 +00:00
6aa4cdeba9
* Tag and Template search is enabled using config variable 'LinterUserInterfaceTagAndTemplateStage' and also checks for the linter table column 'linter_tag' to exist to protect the report code from error if the column is absent. As the linter table alter maintenance added both the linter_tag and linter_template at the same time, there is no reason to check both. The user interface code does not check for the field presence only the config variable. * This code depends on the recordLintJob code writing the tag and template data which is enabled by the config variable 'LinterWriteTagAndTemplateColumnsStage' and also assumes the data migration maintenance script migrateTagTemplate.php has been run to populate linter error records created prior to the table alter and the write code being enabled. Bug: T175177 Change-Id: I2f951dfcd34e3dc6ca17e8754cfaeba8baa3e835
349 lines
12 KiB
PHP
349 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
*
|
|
* @file
|
|
*/
|
|
|
|
namespace MediaWiki\Linter;
|
|
|
|
use Html;
|
|
use HTMLForm;
|
|
use MalformedTitleException;
|
|
use MediaWiki\MediaWikiServices;
|
|
use OutputPage;
|
|
use SpecialPage;
|
|
|
|
class SpecialLintErrors extends SpecialPage {
|
|
|
|
/**
|
|
* @var string|null
|
|
*/
|
|
private $category;
|
|
|
|
public function __construct() {
|
|
parent::__construct( 'LintErrors' );
|
|
}
|
|
|
|
/**
|
|
* @param int|null $ns
|
|
* @param bool $invert
|
|
* @param string $titleLabel
|
|
*/
|
|
protected function showFilterForm( $ns, $invert, $titleLabel ) {
|
|
$selectOptions = [
|
|
(string)$this->msg( 'linter-form-exact-match' )->escaped() => true,
|
|
(string)$this->msg( 'linter-form-prefix-match' )->escaped() => false,
|
|
];
|
|
$fields = [
|
|
'namespace' => [
|
|
'type' => 'namespaceselect',
|
|
'name' => 'namespace',
|
|
'label-message' => 'linter-form-namespace',
|
|
'default' => $ns,
|
|
'id' => 'namespace',
|
|
'all' => '',
|
|
'cssclass' => 'namespaceselector'
|
|
],
|
|
'invertnamespace' => [
|
|
'type' => 'check',
|
|
'name' => 'invert',
|
|
'label-message' => 'invert',
|
|
'default' => $invert,
|
|
'tooltip' => 'invert'
|
|
],
|
|
'titlefield' => [
|
|
'type' => 'title',
|
|
'name' => $titleLabel,
|
|
'label-message' => 'linter-form-title-prefix',
|
|
'exists' => true,
|
|
'required' => false
|
|
],
|
|
'exactmatchradio' => [
|
|
'type' => 'radio',
|
|
'name' => 'exactmatch',
|
|
'options' => $selectOptions,
|
|
'label-message' => 'linter-form-exact-or-prefix',
|
|
'default' => true
|
|
]
|
|
];
|
|
|
|
$mwServices = MediaWikiServices::getInstance();
|
|
$config = $mwServices->getMainConfig();
|
|
$enableUserInterfaceTagAndTemplateStage = $config->get( 'LinterUserInterfaceTagAndTemplateStage' );
|
|
if ( $enableUserInterfaceTagAndTemplateStage ) {
|
|
$selectTemplateOptions = [
|
|
(string)$this->msg( 'linter-form-template-option-all' )->escaped() => 'all',
|
|
(string)$this->msg( 'linter-form-template-option-with' )->escaped() => 'with',
|
|
(string)$this->msg( 'linter-form-template-option-without' )->escaped() => 'without',
|
|
];
|
|
$htmlTags = new HtmlTags( $this );
|
|
$tagAndTemplateFields = [
|
|
'tag' => [
|
|
'type' => 'select',
|
|
'name' => 'tag',
|
|
'label-message' => 'linter-form-tag',
|
|
'options' => $htmlTags->getAllowedHTMLTags()
|
|
],
|
|
'template' => [
|
|
'type' => 'select',
|
|
'name' => 'template',
|
|
'label-message' => 'linter-form-template',
|
|
'options' => $selectTemplateOptions
|
|
]
|
|
];
|
|
$fields = array_merge( $fields, $tagAndTemplateFields );
|
|
}
|
|
|
|
$form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
|
|
$form->setWrapperLegend( true );
|
|
if ( $this->category !== null ) {
|
|
$form->addHeaderHtml( $this->msg( "linter-category-{$this->category}-desc" )->parse() );
|
|
}
|
|
$form->setMethod( 'get' );
|
|
$form->prepareForm()->displayForm( false );
|
|
}
|
|
|
|
/**
|
|
* cleanTitle parses a title and handles a malformed titles, namespaces that are mismatched
|
|
* and exact title searches that find no matching records, and produce appropriate error messages
|
|
*
|
|
* @param string $title
|
|
* @param int|null $namespace
|
|
* @param bool $invert
|
|
* @return array
|
|
*/
|
|
public function cleanTitle( $title, $namespace, $invert ) {
|
|
// Check all titles for malformation regardless of exact match or prefix match
|
|
try {
|
|
$titleElements = MediaWikiServices::getInstance()->getTitleParser()->parseTitle( $title );
|
|
} catch ( MalformedTitleException $e ) {
|
|
return [ 'titlefield' => null, 'error' => 'linter-invalid-title' ];
|
|
}
|
|
|
|
// The drop-down namespace defaults to 'all' which is returned as a null, indicating match all namespaces.
|
|
// If 'main' is selected in the drop-down, int 0 is returned. Other namespaces are returned as int values > 0.
|
|
//
|
|
// If the user does not specify a namespace in the title text box, parseTitle sets it to int 0 as the default.
|
|
// If the user entered ':' (main) namespace as the namespace prefix of a title such as ":MyPageTitle",
|
|
// parseTitle will also return int 0 as the namespace. Other valid system namespaces entered as prefixes
|
|
// in the title text box are returned by parseTitle as int values > 0.
|
|
// To determine if the user entered the ':' (main) namespace when int 0 is returned, a separate check for
|
|
// the substring ':' at offset 0 must be performed.
|
|
|
|
$titleNamespace = $titleElements->getNamespace();
|
|
// Determine if the user entered ':' (resolves to main) as the namespace part of the title,
|
|
// or was it was set by default by parseTitle() to 0, but the user intended to search across 'all' namespaces.
|
|
if ( $titleNamespace === 0 && $title[0] !== ':' ) {
|
|
$titleNamespace = null;
|
|
}
|
|
|
|
if ( $namespace === null && $titleNamespace === null ) {
|
|
// if invert checkbox is set while the namespace drop-down is set to 'all' and no namespace was set in the
|
|
// title text box, then the invert check box being set is invalid as it would exclude all namespaces.
|
|
if ( $invert ) {
|
|
return [ 'titlefield' => null, 'error' => 'linter-namespace-invert-error' ];
|
|
}
|
|
}
|
|
|
|
if ( $namespace !== null && $titleNamespace !== null ) {
|
|
// Show the namespace mismatch error if the namespaces specified in drop-down and title text do not match.
|
|
if ( $namespace !== $titleNamespace ) {
|
|
return [ 'titlefield' => null, 'error' => 'linter-namespace-mismatch' ];
|
|
}
|
|
}
|
|
|
|
// If the namespace drop-down selection is 'all' (null), return the namespace from the title text
|
|
$ns = ( $namespace === null ) ? $titleNamespace : $namespace;
|
|
|
|
return [ 'titlefield' => $titleElements->getDBkey(), 'namespace' => $ns ];
|
|
}
|
|
|
|
/**
|
|
* @param OutputPage $out
|
|
* @param string|null $message
|
|
*/
|
|
private function displayError( $out, $message ) {
|
|
$out->addHTML(
|
|
Html::element( 'span', [ 'class' => 'error' ],
|
|
$this->msg( $message )->text() )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param string|null $par
|
|
*/
|
|
public function execute( $par ) {
|
|
$request = $this->getRequest();
|
|
$out = $this->getOutput();
|
|
|
|
$params = $request->getQueryValues();
|
|
|
|
$this->setHeaders();
|
|
$this->outputHeader( $par || isset( $params[ 'titlesearch' ] ) ? 'disable-summary' : '' );
|
|
|
|
$ns = $request->getIntOrNull( 'namespace' );
|
|
$invert = $request->getBool( 'invert' );
|
|
$exactMatch = $request->getBool( 'exactmatch', true );
|
|
$tagName = $this->getRequest()->getText( 'tag' );
|
|
// map command line tag name through associative array to protect request from a SQL injection security risk
|
|
$htmlTags = new HtmlTags( $this );
|
|
$allowedHtmlTags = $htmlTags->getAllowedHTMLTags();
|
|
$tag = $allowedHtmlTags[ $tagName ] ?? 'all';
|
|
$template = $this->getRequest()->getText( 'template' );
|
|
|
|
// If the request contains a 'titlesearch' parameter, then the user entered a page title
|
|
// or just the first few characters of the title. They also may have entered the first few characters
|
|
// of a custom namespace (just text before a :) to search for and pressed the associated Submit button.
|
|
// Added the pageback parameter to inform the code that the '<- Special:LintErrors' link had be used to allow
|
|
// the UI to redisplay with previous form values, instead of just resubmitting the query.
|
|
if ( $par === null && isset( $params[ 'titlesearch' ] ) && !isset( $params[ 'pageback'] ) ) {
|
|
array_shift( $params );
|
|
$params = array_merge( [ 'pageback' => true ], $params );
|
|
$out->addBacklinkSubtitle( $this->getPageTitle(), $params );
|
|
|
|
$title = $request->getText( 'titlesearch' );
|
|
$titleSearch = $this->cleanTitle( $title, $ns, $invert );
|
|
|
|
if ( $titleSearch[ 'titlefield' ] !== null ) {
|
|
$out->setPageTitle( $this->msg( 'linter-prefix-search-subpage', $titleSearch[ 'titlefield' ] ) );
|
|
|
|
$catManager = new CategoryManager();
|
|
$pager = new LintErrorsPager(
|
|
$this->getContext(), null, $this->getLinkRenderer(),
|
|
$catManager, $titleSearch[ 'namespace' ], $invert, $exactMatch,
|
|
$titleSearch[ 'titlefield' ], $template, $tag
|
|
);
|
|
$out->addParserOutput( $pager->getFullOutput() );
|
|
} else {
|
|
$this->displayError( $out, $titleSearch[ 'error' ] );
|
|
}
|
|
return;
|
|
}
|
|
|
|
$catManager = new CategoryManager();
|
|
if ( in_array( $par, $catManager->getVisibleCategories() ) ) {
|
|
$this->category = $par;
|
|
}
|
|
|
|
if ( !$this->category ) {
|
|
$this->addHelpLink( 'Help:Extension:Linter' );
|
|
$this->showCategoryListings( $catManager );
|
|
} else {
|
|
$this->addHelpLink( "Help:Extension:Linter/{$this->category}" );
|
|
$out->setPageTitle(
|
|
$this->msg( 'linterrors-subpage',
|
|
$this->msg( "linter-category-{$this->category}" )->text()
|
|
)
|
|
);
|
|
$out->addBacklinkSubtitle( $this->getPageTitle() );
|
|
|
|
$title = $request->getText( 'titlecategorysearch' );
|
|
// For category based searches, allow an undefined title to display all records
|
|
if ( $title === '' ) {
|
|
$titleCategorySearch = [ 'titlefield' => '', 'namespace' => $ns, 'pageid' => null ];
|
|
} else {
|
|
$titleCategorySearch = $this->cleanTitle( $title, $ns, $invert );
|
|
}
|
|
|
|
if ( $titleCategorySearch[ 'titlefield' ] !== null ) {
|
|
$this->showFilterForm( null, $invert, 'titlecategorysearch' );
|
|
$pager = new LintErrorsPager(
|
|
$this->getContext(), $this->category, $this->getLinkRenderer(),
|
|
$catManager, $titleCategorySearch[ 'namespace' ], $invert, $exactMatch,
|
|
$titleCategorySearch[ 'titlefield' ], $template, $tag
|
|
);
|
|
$out->addParserOutput( $pager->getFullOutput() );
|
|
} else {
|
|
$this->displayError( $out, $titleCategorySearch[ 'error' ] );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $priority
|
|
* @param int[] $totals name => count
|
|
* @param string[] $categories
|
|
*/
|
|
private function displayList( $priority, $totals, array $categories ) {
|
|
$out = $this->getOutput();
|
|
$msgName = 'linter-heading-' . $priority . '-priority';
|
|
$out->addHTML( Html::element( 'h2', [], $this->msg( $msgName )->text() ) );
|
|
$out->addHTML( $this->buildCategoryList( $categories, $totals ) );
|
|
}
|
|
|
|
/**
|
|
*/
|
|
private function displaySearchPage() {
|
|
$out = $this->getOutput();
|
|
$out->addHTML( Html::element( 'h2', [],
|
|
$this->msg( "linter-lints-prefix-search-page-desc" )->text() ) );
|
|
$this->showFilterForm( null, false, 'titlesearch' );
|
|
}
|
|
|
|
/**
|
|
* @param CategoryManager $catManager
|
|
*/
|
|
private function showCategoryListings( CategoryManager $catManager ) {
|
|
$lookup = new TotalsLookup(
|
|
$catManager,
|
|
MediaWikiServices::getInstance()->getMainWANObjectCache()
|
|
);
|
|
$totals = $lookup->getTotals();
|
|
|
|
// Display lint issues by priority
|
|
$this->displayList( 'high', $totals, $catManager->getHighPriority() );
|
|
$this->displayList( 'medium', $totals, $catManager->getMediumPriority() );
|
|
$this->displayList( 'low', $totals, $catManager->getLowPriority() );
|
|
|
|
$this->displaySearchPage();
|
|
}
|
|
|
|
/**
|
|
* @param string[] $cats
|
|
* @param int[] $totals name => count
|
|
* @return string
|
|
*/
|
|
private function buildCategoryList( array $cats, array $totals ) {
|
|
$linkRenderer = $this->getLinkRenderer();
|
|
$html = Html::openElement( 'ul' ) . "\n";
|
|
foreach ( $cats as $cat ) {
|
|
$html .= Html::rawElement( 'li', [], $linkRenderer->makeKnownLink(
|
|
$this->getPageTitle( $cat ),
|
|
$this->msg( "linter-category-$cat" )->text()
|
|
) . ' ' . Html::element( 'bdi', [],
|
|
$this->msg( "linter-numerrors" )->numParams( $totals[$cat] )->text()
|
|
) ) . "\n";
|
|
}
|
|
$html .= Html::closeElement( 'ul' );
|
|
|
|
return $html;
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function getGroupName() {
|
|
return 'maintenance';
|
|
}
|
|
|
|
/**
|
|
* @return string[]
|
|
*/
|
|
protected function getSubpagesForPrefixSearch() {
|
|
return ( new CategoryManager() )->getVisibleCategories();
|
|
}
|
|
|
|
}
|