Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 140
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
LintErrorsPager
0.00% covered (danger)
0.00%
0 / 140
0.00% covered (danger)
0.00%
0 / 8
2652
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 fillQueryBuilder
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
156
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 doBatchLookups
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 isFieldSortable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 formatValue
0.00% covered (danger)
0.00%
0 / 72
0.00% covered (danger)
0.00%
0 / 1
756
 getDefaultSort
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFieldNames
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
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 */
20
21namespace MediaWiki\Linter;
22
23use IContextSource;
24use InvalidArgumentException;
25use MediaWiki\Cache\LinkCache;
26use MediaWiki\Html\Html;
27use MediaWiki\Linker\LinkRenderer;
28use MediaWiki\MediaWikiServices;
29use MediaWiki\Pager\TablePager;
30use MediaWiki\Permissions\PermissionManager;
31use MediaWiki\Title\Title;
32use MediaWiki\Title\TitleValue;
33use Wikimedia\Rdbms\IExpression;
34use Wikimedia\Rdbms\LikeValue;
35use Wikimedia\Rdbms\SelectQueryBuilder;
36
37class LintErrorsPager extends TablePager {
38
39    private CategoryManager $categoryManager;
40    private LinkCache $linkCache;
41    private LinkRenderer $linkRenderer;
42    private PermissionManager $permissionManager;
43
44    private ?string $category;
45    /** @var mixed */
46    private $categoryId;
47    private array $namespaces;
48    private bool $exactMatch;
49    private string $title;
50    private string $tag;
51
52    /**
53     * Allowed values are keys 'all', 'with' or 'without'
54     */
55    private string $throughTemplate;
56
57    /**
58     * @param IContextSource $context
59     * @param CategoryManager $categoryManager
60     * @param LinkCache $linkCache
61     * @param LinkRenderer $linkRenderer
62     * @param PermissionManager $permissionManager
63     * @param ?string $category
64     * @param array $namespaces
65     * @param bool $exactMatch
66     * @param string $title
67     * @param string $throughTemplate
68     * @param string $tag
69     */
70    public function __construct(
71        IContextSource $context,
72        CategoryManager $categoryManager,
73        LinkCache $linkCache,
74        LinkRenderer $linkRenderer,
75        PermissionManager $permissionManager,
76        ?string $category,
77        array $namespaces,
78        bool $exactMatch,
79        string $title,
80        string $throughTemplate,
81        string $tag
82    ) {
83        $this->categoryManager = $categoryManager;
84        $this->linkCache = $linkCache;
85        $this->linkRenderer = $linkRenderer;
86        $this->permissionManager = $permissionManager;
87
88        $this->category = $category;
89        if ( $category !== null ) {
90            $this->categoryId = $categoryManager->getCategoryId( $category );
91        } else {
92            $this->categoryId = array_values( $this->categoryManager->getCategoryIds(
93                $this->categoryManager->getVisibleCategories()
94            ) );
95        }
96
97        $this->namespaces = $namespaces;
98        $this->exactMatch = $exactMatch;
99        $this->title = $title;
100        $this->throughTemplate = $throughTemplate ?: 'all';
101        $this->tag = $tag ?: 'all';
102        parent::__construct( $context );
103    }
104
105    private function fillQueryBuilder( SelectQueryBuilder $queryBuilder ): void {
106        $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
107        $queryBuilder
108            ->table( 'page' )
109            ->join( 'linter', null, 'page_id=linter_page' )
110            ->fields( LinkCache::getSelectFields() )
111            ->fields( [
112                'page_namespace', 'page_title',
113                'linter_id', 'linter_params',
114                'linter_start', 'linter_end',
115                'linter_cat'
116            ] )
117            ->where( [ 'linter_cat' => $this->categoryId ] );
118
119        if ( $this->title !== '' ) {
120            $namespaces = $this->namespaces ?: [ NS_MAIN ];
121            // Specify page_namespace so that the index can be used (T360865)
122            $queryBuilder->where( [ 'page_namespace' => $namespaces ] );
123            if ( $mainConfig->get( 'LinterUseNamespaceColumnStage' ) ) {
124                // Also put a condition on linter_namespace, in case the DB
125                // decides to put the linter table first
126                $queryBuilder->where( [ 'linter_namespace' => $namespaces ] );
127            }
128            if ( $this->exactMatch ) {
129                $queryBuilder->where( [
130                    'page_title' => $this->title
131                ] );
132            } else {
133                $queryBuilder->where( $this->mDb->expr(
134                    'page_title', IExpression::LIKE, new LikeValue( $this->title, $this->mDb->anyString() )
135                ) );
136            }
137        } elseif ( $this->namespaces ) {
138            $namespaceCol = $mainConfig->get( 'LinterUseNamespaceColumnStage' )
139                ? "linter_namespace" : "page_namespace";
140            $queryBuilder->where( [ $namespaceCol => $this->namespaces ] );
141        }
142
143        if ( $mainConfig->get( 'LinterUserInterfaceTagAndTemplateStage' ) ) {
144            if ( $this->throughTemplate !== 'all' ) {
145                $op = ( $this->throughTemplate === 'with' ) ? '!=' : '=';
146                $queryBuilder->where( $this->mDb->expr( 'linter_template', $op, '' ) );
147            }
148            if ( $this->tag !== 'all' && ( new HtmlTags( $this ) )->checkAllowedHTMLTags( $this->tag ) ) {
149                $queryBuilder->where( [ 'linter_tag'  => $this->tag ] );
150            }
151        }
152    }
153
154    /**
155     * @inheritDoc
156     */
157    public function getQueryInfo() {
158        $queryBuilder = $this->mDb->newSelectQueryBuilder();
159        $this->fillQueryBuilder( $queryBuilder );
160        return $queryBuilder->getQueryInfo();
161    }
162
163    protected function doBatchLookups() {
164        foreach ( $this->mResult as $row ) {
165            $titleValue = new TitleValue( (int)$row->page_namespace, $row->page_title );
166            $this->linkCache->addGoodLinkObjFromRow( $titleValue, $row );
167        }
168    }
169
170    /** @inheritDoc */
171    public function isFieldSortable( $field ) {
172        return false;
173    }
174
175    /**
176     * @param string $name
177     * @param string $value
178     * @return string
179     * @throws InvalidArgumentException
180     */
181    public function formatValue( $name, $value ) {
182        $row = $this->mCurrentRow;
183
184        // To support multiple lint errors of varying types for a single page, the
185        // category is set each time based on the category set in the lint error $row
186        // not by the class when lints are being reported by type for many pages
187        $category = $this->category;
188        if ( $category === null ) {
189            // Assert $row->linter_cat !== null ?
190            $category = $this->categoryManager->getCategoryName( $row->linter_cat );
191        } else {
192            $row->linter_cat = $this->categoryId;
193        }
194        $lintError = Database::makeLintError( $this->categoryManager, $row );
195
196        if ( !$lintError ) {
197            return '';
198        }
199
200        switch ( $name ) {
201            case 'title':
202                $title = Title::makeTitle( $row->page_namespace, $row->page_title );
203                $viewLink = $this->linkRenderer->makeLink( $title );
204                $editMsgKey = $this->permissionManager->quickUserCan( 'edit', $this->getUser(), $title ) ?
205                    'linter-page-edit' : 'linter-page-viewsource';
206                $editLink = $this->linkRenderer->makeLink(
207                    $title,
208                    $this->msg( $editMsgKey )->text(),
209                    [],
210                    [ 'action' => 'edit', 'lintid' => $lintError->lintId, ]
211                );
212
213                $historyLink = $this->linkRenderer->makeLink(
214                    $title,
215                    $this->msg( 'linter-page-history' )->text(),
216                    [],
217                    [ 'action' => 'history' ]
218                );
219
220                $editHistLinks = $this->getLanguage()->pipeList( [ $editLink, $historyLink ] );
221                return $this->msg( 'linter-page-title-edit' )
222                    ->rawParams( $viewLink, $editHistLinks )
223                    ->escaped();
224            case 'details':
225                if ( $category !== null && $this->categoryManager->hasNameParam( $category ) &&
226                    isset( $lintError->params['name'] ) ) {
227                    return Html::element( 'code', [], $lintError->params['name'] );
228                } elseif ( $category === 'bogus-image-options' && isset( $lintError->params['items'] ) ) {
229                    $list = array_map( static function ( $in ) {
230                        return Html::element( 'code', [], $in );
231                    }, $lintError->params['items'] );
232                    return $this->getLanguage()->commaList( $list );
233                } elseif ( $category === 'pwrap-bug-workaround' &&
234                    isset( $lintError->params['root'] ) &&
235                    isset( $lintError->params['child'] ) ) {
236                    return Html::element( 'code', [],
237                        $lintError->params['root'] . " > " . $lintError->params['child'] );
238                } elseif ( $category === 'tidy-whitespace-bug' &&
239                    isset( $lintError->params['node'] ) &&
240                    isset( $lintError->params['sibling'] ) ) {
241                    return Html::element( 'code', [],
242                        $lintError->params['node'] . " + " . $lintError->params['sibling'] );
243                } elseif ( $category === 'multi-colon-escape' &&
244                    isset( $lintError->params['href'] ) ) {
245                    return Html::element( 'code', [], $lintError->params['href'] );
246                } elseif ( $category === 'multiline-html-table-in-list' ) {
247                    /* ancestor and name will be set */
248                    return Html::element( 'code', [],
249                        $lintError->params['ancestorName'] . " > " . $lintError->params['name'] );
250                } elseif ( $category === 'misc-tidy-replacement-issues' ) {
251                    /* There will be a 'subtype' param to disambiguate */
252                    return Html::element( 'code', [], $lintError->params['subtype'] );
253                }
254                return '';
255            case 'template':
256                if ( !$lintError->templateInfo ) {
257                    return '&mdash;';
258                }
259
260                if ( isset( $lintError->templateInfo['multiPartTemplateBlock'] ) ) {
261                    return $this->msg( 'multi-part-template-block' )->escaped();
262                } else {
263                    // @phan-suppress-next-line PhanTypeArraySuspiciousNullable Null checked above
264                    $templateName = $lintError->templateInfo['name'];
265                    // Parsoid provides us with fully qualified template title
266                    // So, fallback to the default main namespace
267                    $templateTitle = Title::newFromText( $templateName );
268                    if ( !$templateTitle ) {
269                        // Shouldn't be possible...???
270                        return '&mdash;';
271                    }
272                }
273
274                return $this->linkRenderer->makeLink(
275                    $templateTitle
276                );
277            case 'category':
278                return Html::element( 'code', [], $category ?? '' );
279            default:
280                throw new InvalidArgumentException( "Unexpected name: $name" );
281        }
282    }
283
284    /** @inheritDoc */
285    public function getDefaultSort() {
286        return 'linter_id';
287    }
288
289    /**
290     * @return string[]
291     */
292    public function getFieldNames() {
293        $names = [
294            'title' => $this->msg( 'linter-pager-title-header' )->text(),
295        ];
296        if ( !$this->category ) {
297            $names['category'] = $this->msg( 'linter-pager-category-header' )->text();
298            $names['details'] = $this->msg( "linter-pager-details-header" )->text();
299        } elseif ( !$this->categoryManager->hasNoParams( $this->category ) ) {
300            $names['details'] = $this->msg( "linter-pager-{$this->category}-details" )->text();
301        }
302        $names['template'] = $this->msg( "linter-pager-template-header" )->text();
303        return $names;
304    }
305}