Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 111
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialBrokenRedirects
0.00% covered (danger)
0.00%
0 / 110
0.00% covered (danger)
0.00%
0 / 12
600
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 isExpensive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isSyndicated
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sortDescending
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPageHeader
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
2
 getOrderFields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 preprocessResults
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
42
 getRedirectTarget
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 formatResult
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
56
 execute
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Specials;
8
9use MediaWiki\Content\IContentHandlerFactory;
10use MediaWiki\Page\LinkBatchFactory;
11use MediaWiki\Skin\Skin;
12use MediaWiki\SpecialPage\QueryPage;
13use MediaWiki\Title\Title;
14use stdClass;
15use Wikimedia\Rdbms\IConnectionProvider;
16use Wikimedia\Rdbms\IReadableDatabase;
17use Wikimedia\Rdbms\IResultWrapper;
18
19/**
20 * List of redirects to non-existent pages.
21 *
22 * Editors are encouraged to fix these by editing them to redirect to
23 * an existing page instead.
24 *
25 * How it works, from a performance perspective:
26 *
27 * 1. Identify source pages,
28 *    in doQuery (cached for MiserMode wikis).
29 *
30 * 2. Render source links,
31 *    in formatResult(). Pages may change between cache and now, and
32 *    LinkRenderer doesn't know anyway, so we batch preload page info
33 *    for all source pages in preprocessResults(),
34 *    consumed by LinkRenderer calls in formatResult().
35 *
36 * 3. Identify redirect destination.
37 *    For uncached, this happens in doQuery() by adding extra fields.
38 *    For MiserMode, the redirect target is loaded from database
39 *    and added to the batch as well.
40 *
41 * 4. Render destination links,
42 *    in formatResult(). Pages may change between cache and now.
43 *    So we batch preload page for all destination pages in
44 *    preprocessResults(), consumed by LinkRenderer in formatResult().
45 *
46 * @ingroup SpecialPage
47 */
48class SpecialBrokenRedirects extends QueryPage {
49
50    /** @var array<int,array<string,Title>> namespace and title map to redirect targets */
51    private array $redirectTargets = [];
52
53    public function __construct(
54        private readonly IContentHandlerFactory $contentHandlerFactory,
55        IConnectionProvider $dbProvider,
56        LinkBatchFactory $linkBatchFactory
57    ) {
58        parent::__construct( 'BrokenRedirects' );
59        $this->setDatabaseProvider( $dbProvider );
60        $this->setLinkBatchFactory( $linkBatchFactory );
61    }
62
63    /** @inheritDoc */
64    public function isExpensive() {
65        return true;
66    }
67
68    /** @inheritDoc */
69    public function isSyndicated() {
70        return false;
71    }
72
73    /** @inheritDoc */
74    protected function sortDescending() {
75        return false;
76    }
77
78    /** @inheritDoc */
79    protected function getPageHeader() {
80        return $this->msg( 'brokenredirectstext' )->parseAsBlock();
81    }
82
83    /** @inheritDoc */
84    public function getQueryInfo() {
85        $dbr = $this->getDatabaseProvider()->getReplicaDatabase();
86
87        return [
88            'tables' => [
89                'redirect',
90                'p1' => 'page',
91                'p2' => 'page',
92            ],
93            'fields' => [
94                'namespace' => 'p1.page_namespace',
95                'title' => 'p1.page_title',
96                'rd_namespace',
97                'rd_title',
98                'rd_fragment',
99            ],
100            'conds' => [
101                // Exclude pages that don't exist locally as wiki pages, but aren't "broken" either: special
102                // pages and interwiki links.
103                $dbr->expr( 'rd_namespace', '>=', 0 ),
104                'rd_interwiki' => '',
105                'p2.page_namespace' => null,
106            ],
107            'join_conds' => [
108                'p1' => [ 'JOIN', [
109                    'rd_from=p1.page_id',
110                ] ],
111                'p2' => [ 'LEFT JOIN', [
112                    'rd_namespace=p2.page_namespace',
113                    'rd_title=p2.page_title'
114                ] ],
115            ],
116        ];
117    }
118
119    /**
120     * @return array
121     */
122    protected function getOrderFields() {
123        return [ 'rd_namespace', 'rd_title', 'rd_from' ];
124    }
125
126    /**
127     * Preload LinkRenderer for source and destination
128     *
129     * @param IReadableDatabase $db
130     * @param IResultWrapper $res
131     */
132    public function preprocessResults( $db, $res ) {
133        if ( !$res->numRows() ) {
134            return;
135        }
136
137        $batch = $this->getLinkBatchFactory()->newLinkBatch()->setCaller( __METHOD__ );
138        $cached = $this->isCached();
139        foreach ( $res as $row ) {
140            // Preload LinkRenderer data for source links
141            $batch->add( $row->namespace, $row->title );
142            if ( !$cached ) {
143                // Preload LinkRenderer data for destination links
144                $batch->add( $row->rd_namespace, $row->rd_title );
145            }
146        }
147        if ( $cached ) {
148            // Preload redirect targets and LinkRenderer data for destination links
149            $rdRes = $db->newSelectQueryBuilder()
150                ->select( [ 'page_namespace', 'page_title', 'rd_namespace', 'rd_title', 'rd_fragment' ] )
151                ->from( 'page' )
152                ->join( 'redirect', null, 'page_id = rd_from' )
153                ->where( $batch->constructSet( 'page', $db ) )
154                ->caller( __METHOD__ )
155                ->fetchResultSet();
156
157            foreach ( $rdRes as $rdRow ) {
158                $batch->add( $rdRow->rd_namespace, $rdRow->rd_title );
159                $this->redirectTargets[$rdRow->page_namespace][$rdRow->page_title] =
160                    Title::makeTitle( $rdRow->rd_namespace, $rdRow->rd_title, $rdRow->rd_fragment );
161            }
162        }
163        $batch->execute();
164        // Rewind for display
165        $res->seek( 0 );
166    }
167
168    protected function getRedirectTarget( stdClass $result ): ?Title {
169        if ( isset( $result->rd_title ) ) {
170            return Title::makeTitle(
171                $result->rd_namespace,
172                $result->rd_title,
173                $result->rd_fragment
174            );
175        } else {
176            return $this->redirectTargets[$result->namespace][$result->title] ?? null;
177        }
178    }
179
180    /**
181     * @param Skin $skin
182     * @param \stdClass $result Result row
183     * @return string
184     */
185    public function formatResult( $skin, $result ) {
186        $fromObj = Title::makeTitle( $result->namespace, $result->title );
187        $toObj = $this->getRedirectTarget( $result );
188
189        $linkRenderer = $this->getLinkRenderer();
190
191        if ( $toObj === null || $toObj->isKnown() ) {
192            return '<del>' . $linkRenderer->makeLink( $fromObj ) . '</del>';
193        }
194
195        $from = $linkRenderer->makeKnownLink(
196            $fromObj,
197            null,
198            [],
199            [ 'redirect' => 'no' ]
200        );
201        $links = [];
202        // if the page is editable, add an edit link
203        if (
204            // check user permissions
205            $this->getAuthority()->isAllowed( 'edit' ) &&
206            // check, if the content model is editable through action=edit
207            $this->contentHandlerFactory->getContentHandler( $fromObj->getContentModel() )
208                ->supportsDirectEditing()
209        ) {
210            $links[] = $linkRenderer->makeKnownLink(
211                $fromObj,
212                $this->msg( 'brokenredirects-edit' )->text(),
213                [],
214                [ 'action' => 'edit' ]
215            );
216        }
217        $to = $linkRenderer->makeBrokenLink( $toObj, $toObj->getFullText() );
218        $arr = $this->getLanguage()->getArrow();
219
220        $out = $from . $this->msg( 'word-separator' )->escaped();
221
222        if ( $this->getAuthority()->isAllowed( 'delete' ) ) {
223            $links[] = $linkRenderer->makeKnownLink(
224                $fromObj,
225                $this->msg( 'brokenredirects-delete' )->text(),
226                [],
227                [
228                    'action' => 'delete',
229                    'wpReason' => $this->msg( 'brokenredirects-delete-reason' )
230                        ->inContentLanguage()
231                        ->text()
232                ]
233            );
234        }
235
236        if ( $links ) {
237            $out .= $this->msg( 'parentheses' )->rawParams( $this->getLanguage()
238                ->pipeList( $links ) )->escaped();
239        }
240        $out .= " {$arr} {$to}";
241
242        return $out;
243    }
244
245    /** @inheritDoc */
246    public function execute( $par ) {
247        $this->addHelpLink( 'Help:Redirects' );
248        parent::execute( $par );
249    }
250
251    /** @inheritDoc */
252    protected function getGroupName() {
253        return 'maintenance';
254    }
255}
256
257/** @deprecated class alias since 1.41 */
258class_alias( SpecialBrokenRedirects::class, 'SpecialBrokenRedirects' );