mediawiki-extensions-Linter/includes/SpecialLintErrors.php
Moh'd Khier Abualruz d543552be9 Allow registration of hidden lint rules
Adding support for categories non, allowing access to category page without showing it in listing, in categories in teh special page

Bug: T334527
Change-Id: I8397a24e85ca9f5a9ce6413dec5efa8c401a9960
2023-05-04 10:41:05 -07:00

350 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, $this->getSubpagesForPrefixSearch() ) ) {
$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() {
$categoryManager = new CategoryManager();
return array_merge( $categoryManager->getVisibleCategories(), $categoryManager->getInvisibleCategories() );
}
}