Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 110
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialDoubleRedirects
0.00% covered (danger)
0.00%
0 / 109
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 / 2
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
 reallyGetQueryInfo
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
12
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 1
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
 formatResult
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
42
 execute
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 preprocessResults
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 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\Html\Html;
11use MediaWiki\Page\LinkBatchFactory;
12use MediaWiki\Skin\Skin;
13use MediaWiki\SpecialPage\QueryPage;
14use MediaWiki\Title\Title;
15use stdClass;
16use Wikimedia\Rdbms\IConnectionProvider;
17use Wikimedia\Rdbms\IReadableDatabase;
18use Wikimedia\Rdbms\IResultWrapper;
19
20/**
21 * List of redirects to another redirecting page.
22 *
23 * The software will by default not follow double redirects, to prevent loops.
24 * Editors are encouraged to fix these, and can discover them via this page.
25 *
26 * @ingroup SpecialPage
27 */
28class SpecialDoubleRedirects extends QueryPage {
29
30    public function __construct(
31        private readonly IContentHandlerFactory $contentHandlerFactory,
32        private readonly LinkBatchFactory $linkBatchFactory,
33        IConnectionProvider $dbProvider
34    ) {
35        parent::__construct( 'DoubleRedirects' );
36        $this->setDatabaseProvider( $dbProvider );
37    }
38
39    /** @inheritDoc */
40    public function isExpensive() {
41        return true;
42    }
43
44    /** @inheritDoc */
45    public function isSyndicated() {
46        return false;
47    }
48
49    /** @inheritDoc */
50    protected function sortDescending() {
51        return false;
52    }
53
54    /** @inheritDoc */
55    protected function getPageHeader() {
56        return $this->msg( 'doubleredirectstext' )->parseAsBlock();
57    }
58
59    private function reallyGetQueryInfo( ?int $namespace = null, ?string $title = null ): array {
60        $limitToTitle = !( $namespace === null && $title === null );
61        $retval = [
62            'tables' => [
63                'ra' => 'redirect',
64                'rb' => 'redirect',
65                'pa' => 'page',
66                'pb' => 'page'
67            ],
68            'fields' => [
69                'namespace' => 'pa.page_namespace',
70                'title' => 'pa.page_title',
71
72                'b_namespace' => 'pb.page_namespace',
73                'b_title' => 'pb.page_title',
74                'b_fragment' => 'ra.rd_fragment',
75
76                // Select fields from redirect instead of page. Because there may
77                // not actually be a page table row for this target (e.g. for interwiki redirects)
78                'c_namespace' => 'rb.rd_namespace',
79                'c_title' => 'rb.rd_title',
80                'c_fragment' => 'rb.rd_fragment',
81                'c_interwiki' => 'rb.rd_interwiki',
82            ],
83            'conds' => [
84                'ra.rd_from = pa.page_id',
85
86                // Filter out redirects where the target goes interwiki (T42353).
87                // This isn't an optimization, it is required for correct results,
88                // otherwise a non-double redirect like Bar -> w:Foo will show up
89                // like "Bar -> Foo -> w:Foo".
90                'ra.rd_interwiki' => '',
91
92                'pb.page_namespace = ra.rd_namespace',
93                'pb.page_title = ra.rd_title',
94
95                'rb.rd_from = pb.page_id',
96            ]
97        ];
98
99        if ( $limitToTitle ) {
100            $retval['conds']['pa.page_namespace'] = $namespace;
101            $retval['conds']['pa.page_title'] = $title;
102        }
103
104        return $retval;
105    }
106
107    /** @inheritDoc */
108    public function getQueryInfo() {
109        return $this->reallyGetQueryInfo();
110    }
111
112    /** @inheritDoc */
113    protected function getOrderFields() {
114        return [ 'ra.rd_namespace', 'ra.rd_title' ];
115    }
116
117    /**
118     * @param Skin $skin
119     * @param stdClass $result Result row
120     * @return string
121     */
122    public function formatResult( $skin, $result ) {
123        // If no Title B or C is in the query, it means this came from
124        // querycache (which only saves the 3 columns for title A).
125        // That does save the bulk of the query cost, but now we need to
126        // get a little more detail about each individual entry quickly
127        // using the filter of reallyGetQueryInfo.
128        $deep = false;
129        if ( $result ) {
130            if ( isset( $result->b_namespace ) ) {
131                $deep = $result;
132            } else {
133                $qi = $this->reallyGetQueryInfo(
134                    $result->namespace,
135                    $result->title
136                );
137                $deep = $this->getDatabaseProvider()->getReplicaDatabase()->newSelectQueryBuilder()
138                    ->queryInfo( $qi )
139                    ->caller( __METHOD__ )
140                    ->fetchRow();
141            }
142        }
143
144        $titleA = Title::makeTitle( $result->namespace, $result->title );
145
146        $linkRenderer = $this->getLinkRenderer();
147        if ( !$deep ) {
148            return '<del>' . $linkRenderer->makeLink( $titleA, null, [], [ 'redirect' => 'no' ] ) . '</del>';
149        }
150
151        // if the page is editable, add an edit link
152        if (
153            // check user permissions
154            $this->getAuthority()->isAllowed( 'edit' ) &&
155            // check, if the content model is editable through action=edit
156            $this->contentHandlerFactory->getContentHandler( $titleA->getContentModel() )
157                ->supportsDirectEditing()
158        ) {
159            $edit = $linkRenderer->makeKnownLink(
160                $titleA,
161                $this->msg( 'parentheses', $this->msg( 'editlink' )->text() )->text(),
162                [],
163                [ 'action' => 'edit' ]
164            );
165        } else {
166            $edit = '';
167        }
168
169        $arrow = $this->getLanguage()->getArrow();
170        $contentLanguage = $this->getContentLanguage();
171        $bdiAttrs = [
172            'dir' => $contentLanguage->getDir(),
173            'lang' => $contentLanguage->getHtmlCode(),
174        ];
175        $linkA = Html::rawElement( 'bdi', $bdiAttrs, $linkRenderer->makeKnownLink(
176            $titleA,
177            null,
178            [],
179            [ 'redirect' => 'no' ]
180        ) );
181
182        $titleB = Title::makeTitle( $deep->b_namespace, $deep->b_title );
183        // We show fragment, but don't link to it, as it probably doesn't exist anymore.
184        $titleBFrag = Title::makeTitle( $deep->b_namespace, $deep->b_title, $deep->b_fragment );
185        $linkB = Html::rawElement( 'bdi', $bdiAttrs, $linkRenderer->makeKnownLink(
186            $titleB,
187            $titleBFrag->getFullText(),
188            [],
189            [ 'redirect' => 'no' ]
190        ) );
191
192        $titleC = Title::makeTitle(
193            $deep->c_namespace,
194            $deep->c_title,
195            $deep->c_fragment,
196            $deep->c_interwiki
197        );
198        $linkC = Html::rawElement( 'bdi', $bdiAttrs,
199            $linkRenderer->makeKnownLink( $titleC, $titleC->getFullText() )
200        );
201
202        return ( "{$linkA} {$edit} {$arrow} {$linkB} {$arrow} {$linkC}" );
203    }
204
205    /** @inheritDoc */
206    public function execute( $par ) {
207        $this->addHelpLink( 'Help:Redirects' );
208        parent::execute( $par );
209    }
210
211    /**
212     * Cache page content model and gender distinction for performance
213     *
214     * @param IReadableDatabase $db
215     * @param IResultWrapper $res
216     */
217    public function preprocessResults( $db, $res ) {
218        if ( !$res->numRows() ) {
219            return;
220        }
221
222        $batch = $this->linkBatchFactory->newLinkBatch()->setCaller( __METHOD__ );
223        foreach ( $res as $row ) {
224            $batch->add( $row->namespace, $row->title );
225            if ( isset( $row->b_namespace ) ) {
226                // lazy loaded when using cached results
227                $batch->add( $row->b_namespace, $row->b_title );
228            }
229            if ( isset( $row->c_interwiki ) && !$row->c_interwiki ) {
230                // lazy loaded when using cached result, not added when interwiki link
231                $batch->add( $row->c_namespace, $row->c_title );
232            }
233        }
234        $batch->execute();
235
236        // Back to start for display
237        $res->seek( 0 );
238    }
239
240    /** @inheritDoc */
241    protected function getGroupName() {
242        return 'maintenance';
243    }
244}
245
246/** @deprecated class alias since 1.41 */
247class_alias( SpecialDoubleRedirects::class, 'SpecialDoubleRedirects' );