Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.24% covered (warning)
80.24%
134 / 167
45.00% covered (danger)
45.00%
9 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
LinkRenderer
80.24% covered (warning)
80.24%
134 / 167
45.00% covered (danger)
45.00%
9 / 20
85.86
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 setForceArticlePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getForceArticlePath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setExpandURLs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExpandURLs
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isForComment
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeLink
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 runBeginHook
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 makePreloadedLink
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
3.00
 makeKnownLink
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
4.03
 makeBrokenLink
95.65% covered (success)
95.65%
22 / 23
0.00% covered (danger)
0.00%
0 / 1
7
 makeExternalLink
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
30
 makeRedirectHeader
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
5
 buildAElement
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 getLinkText
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 getLinkURL
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 normalizeTarget
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 getLinkClasses
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
6.04
 castToTitle
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 castToLinkTarget
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
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 * @author Kunal Mehta <legoktm@debian.org>
20 */
21namespace MediaWiki\Linker;
22
23use MediaWiki\Cache\LinkCache;
24use MediaWiki\Config\ServiceOptions;
25use MediaWiki\HookContainer\HookContainer;
26use MediaWiki\HookContainer\HookRunner;
27use MediaWiki\Html\Html;
28use MediaWiki\Language\Language;
29use MediaWiki\Linker\LinkTarget as MWLinkTarget;
30use MediaWiki\Message\Message;
31use MediaWiki\Page\PageReference;
32use MediaWiki\Parser\Parser;
33use MediaWiki\SpecialPage\SpecialPageFactory;
34use MediaWiki\Title\Title;
35use MediaWiki\Title\TitleFormatter;
36use MediaWiki\Title\TitleValue;
37use Wikimedia\Assert\Assert;
38use Wikimedia\HtmlArmor\HtmlArmor;
39use Wikimedia\Parsoid\Core\LinkTarget;
40
41/**
42 * Class that generates HTML for internal links.
43 * See the Linker class for other kinds of links.
44 *
45 * @see https://www.mediawiki.org/wiki/Manual:LinkRenderer
46 * @since 1.28
47 */
48class LinkRenderer {
49
50    public const CONSTRUCTOR_OPTIONS = [
51        'renderForComment',
52    ];
53
54    /**
55     * Whether to force the pretty article path
56     *
57     * @var bool
58     */
59    private $forceArticlePath = false;
60
61    /**
62     * A PROTO_* constant or false
63     *
64     * @var string|bool|int
65     */
66    private $expandUrls = false;
67
68    /**
69     * Whether links are being rendered for comments.
70     *
71     * @var bool
72     */
73    private $comment = false;
74
75    /**
76     * @var TitleFormatter
77     */
78    private $titleFormatter;
79
80    /**
81     * @var LinkCache
82     */
83    private $linkCache;
84
85    /** @var HookRunner */
86    private $hookRunner;
87
88    /**
89     * @var SpecialPageFactory
90     */
91    private $specialPageFactory;
92
93    /**
94     * @internal For use by LinkRendererFactory
95     *
96     * @param TitleFormatter $titleFormatter
97     * @param LinkCache $linkCache
98     * @param SpecialPageFactory $specialPageFactory
99     * @param HookContainer $hookContainer
100     * @param ServiceOptions $options
101     */
102    public function __construct(
103        TitleFormatter $titleFormatter,
104        LinkCache $linkCache,
105        SpecialPageFactory $specialPageFactory,
106        HookContainer $hookContainer,
107        ServiceOptions $options
108    ) {
109        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
110        $this->comment = $options->get( 'renderForComment' );
111
112        $this->titleFormatter = $titleFormatter;
113        $this->linkCache = $linkCache;
114        $this->specialPageFactory = $specialPageFactory;
115        $this->hookRunner = new HookRunner( $hookContainer );
116    }
117
118    /**
119     * Whether to force the link to use the article path ($wgArticlePath) even if
120     * a query string is present, resulting in URLs like /wiki/Main_Page?action=foobar.
121     *
122     * @param bool $force
123     */
124    public function setForceArticlePath( $force ) {
125        $this->forceArticlePath = $force;
126    }
127
128    /**
129     * @return bool
130     * @see setForceArticlePath()
131     */
132    public function getForceArticlePath() {
133        return $this->forceArticlePath;
134    }
135
136    /**
137     * Whether/how to expand URLs.
138     *
139     * @param string|bool|int $expand A PROTO_* constant or false for no expansion
140     * @see UrlUtils::expand()
141     */
142    public function setExpandURLs( $expand ) {
143        $this->expandUrls = $expand;
144    }
145
146    /**
147     * @return string|bool|int a PROTO_* constant or false for no expansion
148     * @see setExpandURLs()
149     */
150    public function getExpandURLs() {
151        return $this->expandUrls;
152    }
153
154    /**
155     * True when the links will be rendered in an edit summary or log comment.
156     */
157    public function isForComment(): bool {
158        // This option only exists to power a hack in Wikibase's onHtmlPageLinkRendererEnd hook.
159        return $this->comment;
160    }
161
162    /**
163     * Render a wikilink.
164     * Will call makeKnownLink() or makeBrokenLink() as appropriate.
165     *
166     * @param LinkTarget|PageReference $target Page that will be visited when the user clicks on the link.
167     * @param-taint $target none
168     * @param string|HtmlArmor|null $text Text that the user can click on to visit the link.
169     * @param-taint $text escapes_html
170     * @param array $extraAttribs Attributes you would like to add to the <a> tag. For example, if
171     * you would like to add title="Text when hovering!", you would set this to [ 'title' => 'Text
172     * when hovering!' ]
173     * @param-taint $extraAttribs none
174     * @param array $query Parameters you would like to add to the URL. For example, if you would
175     * like to add ?redirect=no&debug=1, you would set this to [ 'redirect' => 'no', 'debug' => '1' ]
176     * @param-taint $query none
177     * @return string HTML
178     * @return-taint escaped
179     */
180    public function makeLink(
181        $target, $text = null, array $extraAttribs = [], array $query = []
182    ) {
183        Assert::parameterType( [ LinkTarget::class, PageReference::class ], $target, '$target' );
184        if ( $this->castToTitle( $target )->isKnown() ) {
185            return $this->makeKnownLink( $target, $text, $extraAttribs, $query );
186        } else {
187            return $this->makeBrokenLink( $target, $text, $extraAttribs, $query );
188        }
189    }
190
191    /**
192     * @param LinkTarget $target $target
193     * @param string|HtmlArmor|null &$text
194     * @param array &$extraAttribs
195     * @param array &$query
196     * @return string|null|void
197     */
198    private function runBeginHook( $target, &$text, array &$extraAttribs, array &$query ) {
199        $ret = null;
200        if ( !$this->hookRunner->onHtmlPageLinkRendererBegin(
201            // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
202            $this, $this->castToTitle( $target ), $text, $extraAttribs, $query, $ret )
203        ) {
204            return $ret;
205        }
206    }
207
208    /**
209     * Make a link that's styled as if the target page exists (a "blue link"), with a specified
210     * class attribute.
211     *
212     * Usually you should use makeLink() or makeKnownLink() instead, which will select the CSS
213     * classes automatically. Use this method if the exact styling doesn't matter and you want
214     * to ensure no extra DB lookup happens, e.g. for links generated by the skin.
215     *
216     * @param LinkTarget|PageReference $target Page that will be visited when the user clicks on the link.
217     * @param-taint $target none
218     * @param string|HtmlArmor|null $text Text that the user can click on to visit the link.
219     * @param-taint $text escapes_html
220     * @param string|array $classes CSS classes to add
221     * @param-taint $classes none
222     * @param array $extraAttribs Attributes you would like to add to the <a> tag. For example, if
223     * you would like to add title="Text when hovering!", you would set this to [ 'title' => 'Text
224     * when hovering!' ]
225     * @param-taint $extraAttribs none
226     * @param array $query Parameters you would like to add to the URL. For example, if you would
227     * like to add ?redirect=no&debug=1, you would set this to [ 'redirect' => 'no', 'debug' => '1' ]
228     * @param-taint $query none
229     * @return string
230     * @return-taint escaped
231     */
232    public function makePreloadedLink(
233        $target, $text = null, $classes = [], array $extraAttribs = [], array $query = []
234    ) {
235        Assert::parameterType( [ LinkTarget::class, PageReference::class ], $target, '$target' );
236
237        // Run begin hook
238        $ret = $this->runBeginHook( $target, $text, $extraAttribs, $query );
239        if ( $ret !== null ) {
240            return $ret;
241        }
242        $target = $this->normalizeTarget( $target );
243        $url = $this->getLinkURL( $target, $query );
244        // Define empty attributes here for consistent order in the output
245        $attribs = [ 'href' => null, 'class' => [], 'title' => null ];
246
247        $prefixedText = $this->titleFormatter->getPrefixedText( $target );
248        if ( $prefixedText !== '' ) {
249            $attribs['title'] = $prefixedText;
250        }
251
252        $attribs = array_merge( $attribs, $extraAttribs, [ 'href' => $url ] );
253
254        Html::addClass( $classes, Html::expandClassList( $extraAttribs['class'] ?? [] ) );
255        // Stringify attributes for hook compatibility
256        $attribs['class'] = Html::expandClassList( $classes );
257
258        $text ??= $this->getLinkText( $target );
259
260        return $this->buildAElement( $target, $text, $attribs, true );
261    }
262
263    /**
264     * Make a link that's styled as if the target page exists (usually a "blue link", although the
265     * styling might depend on e.g. whether the target is a redirect).
266     *
267     * This will result in a DB lookup if the title wasn't cached yet. If you want to avoid that,
268     * and don't care about matching the exact styling of links within page content, you can use
269     * makePreloadedLink() instead.
270     *
271     * @param LinkTarget|PageReference $target Page that will be visited when the user clicks on the link.
272     * @param-taint $target none
273     * @param string|HtmlArmor|null $text Text that the user can click on to visit the link.
274     * @param-taint $text escapes_html
275     * @param array $extraAttribs Attributes you would like to add to the <a> tag. For example, if
276     * you would like to add title="Text when hovering!", you would set this to [ 'title' => 'Text
277     * when hovering!' ]
278     * @param-taint $extraAttribs none
279     * @param array $query Parameters you would like to add to the URL. For example, if you would
280     * like to add ?redirect=no&debug=1, you would set this to [ 'redirect' => 'no', 'debug' => '1' ]
281     * @param-taint $query none
282     * @return string HTML
283     * @return-taint escaped
284     */
285    public function makeKnownLink(
286        $target, $text = null, array $extraAttribs = [], array $query = []
287    ) {
288        Assert::parameterType( [ LinkTarget::class, PageReference::class ], $target, '$target' );
289        if ( $target instanceof LinkTarget ) {
290            $isExternal = $target->isExternal();
291        } else {
292            // $target instanceof PageReference
293            // treat all PageReferences as local for now
294            $isExternal = false;
295        }
296        $classes = [];
297        if ( $isExternal ) {
298            $classes[] = 'extiw';
299        }
300        $colour = $this->getLinkClasses( $target );
301        if ( $colour !== '' ) {
302            $classes[] = $colour;
303        }
304
305        return $this->makePreloadedLink(
306            $target,
307            $text,
308            $classes,
309            $extraAttribs,
310            $query
311        );
312    }
313
314    /**
315     * Make a link that's styled as if the target page doesn't exist (a "red link").
316     *
317     * @param LinkTarget|PageReference $target Page that will be visited when the user clicks on the link.
318     * @param-taint $target none
319     * @param string|HtmlArmor|null $text Text that the user can click on to visit the link.
320     * @param-taint $text escapes_html
321     * @param array $extraAttribs Attributes you would like to add to the <a> tag. For example, if
322     * you would like to add title="Text when hovering!", you would set this to [ 'title' => 'Text
323     * when hovering!' ]
324     * @param-taint $extraAttribs none
325     * @param array $query Parameters you would like to add to the URL. For example, if you would
326     * like to add ?redirect=no&debug=1, you would set this to [ 'redirect' => 'no', 'debug' => '1' ]
327     * @param-taint $query none
328     * @return string
329     * @return-taint escaped
330     */
331    public function makeBrokenLink(
332        $target, $text = null, array $extraAttribs = [], array $query = []
333    ) {
334        Assert::parameterType( [ LinkTarget::class, PageReference::class ], $target, '$target' );
335        // Run legacy hook
336        $ret = $this->runBeginHook( $target, $text, $extraAttribs, $query );
337        if ( $ret !== null ) {
338            return $ret;
339        }
340
341        if ( $target instanceof LinkTarget ) {
342            # We don't want to include fragments for broken links, because they
343            # generally make no sense.
344            if ( $target->hasFragment() ) {
345                $target = $target->createFragmentTarget( '' );
346            }
347        }
348        $target = $this->normalizeTarget( $target );
349
350        if ( !isset( $query['action'] ) && $target->getNamespace() !== NS_SPECIAL ) {
351            $query['action'] = 'edit';
352            $query['redlink'] = '1';
353        }
354
355        $url = $this->getLinkURL( $target, $query );
356        // Define empty attributes here for consistent order in the output
357        $attribs = [ 'href' => null, 'class' => [], 'title' => null ];
358
359        $prefixedText = $this->titleFormatter->getPrefixedText( $target );
360        if ( $prefixedText !== '' ) {
361            // This ends up in parser cache!
362            $attribs['title'] = wfMessage( 'red-link-title', $prefixedText )
363                ->inContentLanguage()
364                ->text();
365        }
366
367        $attribs = array_merge( $attribs, $extraAttribs, [ 'href' => $url ] );
368
369        Html::addClass( $attribs['class'], 'new' );
370        // Stringify attributes for hook compatibility
371        $attribs['class'] = Html::expandClassList( $attribs['class'] ?? [] );
372
373        $text ??= $this->getLinkText( $target );
374
375        return $this->buildAElement( $target, $text, $attribs, false );
376    }
377
378    /**
379     * Make an external link
380     *
381     * @since 1.43
382     * @param string $url URL to link to
383     * @param-taint $url escapes_html
384     * @param string|HtmlArmor|Message $text Text of link; will be escaped if
385     *  a string.
386     * @param-taint $text escapes_html
387     * @param LinkTarget|PageReference $title Where the link is being rendered, used for title specific link attributes
388     * @param-taint $title none
389     * @param string $linktype Type of external link. Gets added to the classes
390     * @param-taint $linktype escapes_html
391     * @param array $attribs Array of extra attributes to <a>
392     * @param-taint $attribs escapes_html
393     * @return string
394     */
395    public function makeExternalLink(
396        string $url, $text, $title, $linktype = '', $attribs = []
397    ) {
398        $attribs['class'] ??= [];
399        Html::addClass( $attribs['class'], 'external' );
400        if ( $linktype ) {
401            Html::addClass( $attribs['class'], $linktype );
402        }
403        // Stringify attributes for hook compatibility
404        $attribs['class'] = Html::expandClassList( $attribs['class'] );
405
406        if ( $text instanceof Message ) {
407            $text = $text->escaped();
408        } else {
409            $text = HtmlArmor::getHtml( $text );
410            // Tell phan that $text is non-null after ::getHtml()
411            '@phan-var string $text';
412        }
413
414        $newRel = Parser::getExternalLinkRel( $url, $title );
415        if ( $newRel !== null ) {
416            $attribs['rel'] ??= [];
417            Html::addClass( $attribs['rel'], $newRel );
418            $attribs['rel'] = Html::expandClassList( $attribs['rel'] );
419        }
420        $link = '';
421        $success = $this->hookRunner->onLinkerMakeExternalLink(
422            $url, $text, $link, $attribs, $linktype );
423        if ( !$success ) {
424            wfDebug( "Hook LinkerMakeExternalLink changed the output of link "
425                . "with url {$url} and text {$text} to {$link}" );
426            return $link;
427        }
428        $attribs['href'] = $url;
429        return Html::rawElement( 'a', $attribs, $text );
430    }
431
432    /**
433     * Return the HTML for the top of a redirect page
434     *
435     * Chances are you should just be using the ParserOutput from
436     * WikitextContent::getParserOutput (which will have this header added
437     * automatically) instead of calling this for redirects.
438     *
439     * If creating your own redirect-alike, please use return value of
440     * this method to set the 'core:redirect-header' extension data field
441     * in your ParserOutput, rather than concatenating HTML directly.
442     * See WikitextContentHandler::fillParserOutput().
443     *
444     * @since 1.41
445     * @param Language $lang
446     * @param Title $target Destination to redirect
447     * @param bool $forceKnown Should the image be shown as a bluelink regardless of existence?
448     * @param bool $addLinkTag Should a <link> tag be added?
449     * @return string Containing HTML with redirect link
450     */
451    public function makeRedirectHeader(
452        Language $lang, Title $target,
453        bool $forceKnown = false, bool $addLinkTag = false
454    ) {
455        $html = '<ul class="redirectText">';
456        if ( $forceKnown ) {
457            $link = $this->makeKnownLink(
458                $target,
459                $target->getFullText(),
460                [],
461                // Make sure wiki page redirects are not followed
462                $target->isRedirect() ? [ 'redirect' => 'no' ] : []
463            );
464        } else {
465            $link = $this->makeLink(
466                $target,
467                $target->getFullText(),
468                [],
469                // Make sure wiki page redirects are not followed
470                $target->isRedirect() ? [ 'redirect' => 'no' ] : []
471            );
472        }
473
474        $redirectToText = wfMessage( 'redirectto' )->inLanguage( $lang )->escaped();
475        $linkTag = '';
476        if ( $addLinkTag ) {
477            $linkTag = Html::rawElement( 'link', [ 'rel' => 'mw:PageProp/redirect' ] );
478        }
479
480        return Html::rawElement(
481            'div', [ 'class' => 'redirectMsg' ],
482            Html::rawElement( 'p', [], $redirectToText ) .
483            Html::rawElement( 'ul', [ 'class' => 'redirectText' ],
484                Html::rawElement( 'li', [], $link ) )
485        ) . $linkTag;
486    }
487
488    /**
489     * Builds the final <a> element
490     *
491     * @param LinkTarget|PageReference $target Page that will be visited when the user clicks on the link.
492     * @param-taint $target none
493     * @param string|HtmlArmor $text
494     * @param-taint $text escapes_html
495     * @param array $attribs
496     * @param-taint $attribs none
497     * @param bool $isKnown
498     * @param-taint $isKnown none
499     * @return null|string
500     * @return-taint escaped
501     */
502    private function buildAElement( $target, $text, array $attribs, $isKnown ) {
503        $ret = null;
504        if ( !$this->hookRunner->onHtmlPageLinkRendererEnd(
505            // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
506            $this, $this->castToLinkTarget( $target ), $isKnown, $text, $attribs, $ret )
507        ) {
508            return $ret;
509        }
510
511        return Html::rawElement( 'a', $attribs, HtmlArmor::getHtml( $text ) );
512    }
513
514    /**
515     * @param LinkTarget|PageReference $target Page that will be visited when the user clicks on the link.
516     * @return string
517     */
518    private function getLinkText( $target ) {
519        $prefixedText = $this->titleFormatter->getPrefixedText( $target );
520        // If the target is just a fragment, with no title, we return the fragment
521        // text.  Otherwise, we return the title text itself.
522        if ( $prefixedText === '' && $target instanceof LinkTarget && $target->hasFragment() ) {
523            return $target->getFragment();
524        }
525
526        return $prefixedText;
527    }
528
529    /**
530     * @param LinkTarget|PageReference $target Page that will be visited when the user clicks on the link.
531     * @param array $query Parameters you would like to add to the URL. For example, if you would
532     * like to add ?redirect=no&debug=1, you would set this to [ 'redirect' => 'no', 'debug' => '1' ]
533     * @return string non-escaped text
534     */
535    private function getLinkURL( $target, $query = [] ) {
536        if ( $this->forceArticlePath ) {
537            $realQuery = $query;
538            $query = [];
539        } else {
540            $realQuery = [];
541        }
542        $url = $this->castToTitle( $target )->getLinkURL( $query, false, $this->expandUrls );
543
544        if ( $this->forceArticlePath && $realQuery ) {
545            $url = wfAppendQuery( $url, $realQuery );
546        }
547
548        return $url;
549    }
550
551    /**
552     * Normalizes the provided target
553     *
554     * @internal For use by Linker::getImageLinkMTOParams()
555     * @param LinkTarget|PageReference $target Page that will be visited when the user clicks on the link.
556     * @return MWLinkTarget
557     */
558    public function normalizeTarget( $target ): MWLinkTarget {
559        $target = $this->castToLinkTarget( $target );
560        if ( $target->getNamespace() === NS_SPECIAL && !$target->isExternal() ) {
561            [ $name, $subpage ] = $this->specialPageFactory->resolveAlias(
562                $target->getDBkey()
563            );
564            if ( $name ) {
565                return new TitleValue(
566                    NS_SPECIAL,
567                    $this->specialPageFactory->getLocalNameFor( $name, $subpage ),
568                    $target->getFragment()
569                );
570            }
571        }
572
573        return $target;
574    }
575
576    /**
577     * Returns CSS classes to add to a known link.
578     *
579     * Note that most CSS classes set on wikilinks are actually handled elsewhere (e.g. in
580     * makeKnownLink() or in LinkHolderArray).
581     *
582     * @param LinkTarget|PageReference $target Page that will be visited when the user clicks on the link.
583     * @return string CSS class
584     */
585    public function getLinkClasses( $target ) {
586        Assert::parameterType( [ LinkTarget::class, PageReference::class ], $target, '$target' );
587        $target = $this->castToLinkTarget( $target );
588        // Don't call LinkCache if the target is "non-proper"
589        if ( $target->isExternal() || $target->getText() === '' || $target->getNamespace() < 0 ) {
590            return '';
591        }
592        // Make sure the target is in the cache
593        $id = $this->linkCache->addLinkObj( $target );
594        if ( $id == 0 ) {
595            // Doesn't exist
596            return '';
597        }
598
599        if ( $this->linkCache->getGoodLinkFieldObj( $target, 'redirect' ) ) {
600            # Page is a redirect
601            return 'mw-redirect';
602        }
603
604        return '';
605    }
606
607    /**
608     * @param LinkTarget|PageReference $target Page that will be visited when the user clicks on the link.
609     * @return Title
610     */
611    private function castToTitle( $target ): Title {
612        if ( $target instanceof LinkTarget ) {
613            return Title::newFromLinkTarget( $target );
614        }
615        // $target instanceof PageReference
616        return Title::newFromPageReference( $target );
617    }
618
619    /**
620     * @param LinkTarget|PageReference $target Page that will be visited when the user clicks on the link.
621     * @return MWLinkTarget
622     */
623    private function castToLinkTarget( $target ): MWLinkTarget {
624        if ( $target instanceof PageReference ) {
625            return Title::newFromPageReference( $target );
626        }
627        if ( $target instanceof MWLinkTarget ) {
628            return $target;
629        }
630        return TitleValue::newFromLinkTarget( $target );
631    }
632}