Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.30% covered (warning)
87.30%
55 / 63
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
HtmlPageLinkRendererEndHookHandler
87.30% covered (warning)
87.30%
55 / 63
66.67% covered (warning)
66.67%
4 / 6
17.59
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
2
 factory
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 onHtmlPageLinkRendererEnd
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
4.10
 doHtmlPageLinkRendererEnd
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 internalDoHtmlPageLinkRendererEnd
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
2
 shouldConvertNoBadTitle
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare( strict_types = 1 );
4
5namespace EntitySchema\MediaWiki\Hooks;
6
7use EntitySchema\DataAccess\SchemaDataResolvingLabelLookup;
8use HtmlArmor;
9use MediaWiki\Context\RequestContext;
10use MediaWiki\Languages\LanguageFactory;
11use MediaWiki\Linker\Hook\HtmlPageLinkRendererEndHook;
12use MediaWiki\Linker\LinkRenderer;
13use MediaWiki\Title\Title;
14use Wikimedia\Assert\Assert;
15
16/**
17 * @license GPL-2.0-or-later
18 */
19class HtmlPageLinkRendererEndHookHandler implements HtmlPageLinkRendererEndHook {
20
21    private bool $entitySchemaIsRepo;
22    private LanguageFactory $languageFactory;
23    private ?SchemaDataResolvingLabelLookup $labelLookup;
24    private RequestContext $context;
25
26    public function __construct(
27        bool $entitySchemaIsRepo,
28        LanguageFactory $languageFactory,
29        ?SchemaDataResolvingLabelLookup $labelLookup,
30        RequestContext $context
31    ) {
32        $this->entitySchemaIsRepo = $entitySchemaIsRepo;
33        $this->languageFactory = $languageFactory;
34        $this->labelLookup = $labelLookup;
35        $this->context = $context;
36        if ( $entitySchemaIsRepo ) {
37            Assert::parameterType( SchemaDataResolvingLabelLookup::class, $labelLookup, '$labelLookup' );
38        }
39    }
40
41    public static function factory(
42        LanguageFactory $languageFactory,
43        bool $entitySchemaIsRepo,
44        ?SchemaDataResolvingLabelLookup $labelLookup
45    ): self {
46        return new self(
47            $entitySchemaIsRepo,
48            $languageFactory,
49            $labelLookup,
50            RequestContext::getMain()
51        );
52    }
53
54    /**
55     * @inheritDoc
56     */
57    public function onHtmlPageLinkRendererEnd(
58        $linkRenderer,
59        $target,
60        $isKnown,
61        &$text,
62        &$extraAttribs,
63        &$ret
64    ): bool {
65        if ( !$this->entitySchemaIsRepo ) {
66            return true;
67        }
68        if ( !$this->context->hasTitle() ) {
69            // Short-circuit this hook if no title is
70            // set in the main context (T131176)
71            return true;
72        }
73
74        if ( !$target->inNamespace( NS_ENTITYSCHEMA_JSON ) ) {
75            return true;
76        }
77
78        return $this->doHtmlPageLinkRendererEnd(
79            $linkRenderer,
80            Title::newFromLinkTarget( $target ),
81            $text
82        );
83    }
84
85    /**
86     * @param LinkRenderer $linkRenderer
87     * @param Title $target
88     * @param HtmlArmor|string|null &$text
89     *
90     * @return bool always true
91     */
92    private function doHtmlPageLinkRendererEnd(
93        LinkRenderer $linkRenderer,
94        Title $target,
95        &$text
96    ): bool {
97        // if custom link text is given, there is no point in overwriting it
98        // but not if it is similar to the plain title
99        if ( $text !== null
100            && $target->getFullText() !== HtmlArmor::getHtml( $text )
101            && $target->getText() !== HtmlArmor::getHtml( $text )
102        ) {
103            return true;
104        }
105
106        $outputTitle = $this->context->getTitle();
107        '@phan-var Title $outputTitle';
108        if ( !$this->shouldConvertNoBadTitle( $outputTitle, $linkRenderer ) ) {
109            return true;
110        }
111
112        return $this->internalDoHtmlPageLinkRendererEnd(
113            $target,
114            $text,
115        );
116    }
117
118    /**
119     * Parts of the hook handling logic for the HtmlPageLinkRendererEnd hook that potentially
120     * interact with entity storage.
121     *
122     * @param Title $target
123     * @param HtmlArmor|string|null &$text
124     *
125     * @return bool always true
126     */
127    private function internalDoHtmlPageLinkRendererEnd(
128        Title $target,
129        &$text
130    ): bool {
131        $label = $this->labelLookup->getLabelForTitle( $target, $this->context->getLanguage()->getCode() );
132        if ( $label === null ) {
133            return true;
134        }
135
136        $labelLang = $this->languageFactory->getLanguage( $label->getLanguageCode() );
137
138        // $idHtml, $labelHtml and $text is closely based on Wikibase DefaultEntityLinkFormatter::getHtml()
139
140        $idHtml = '<span class="wb-itemlink-id">'
141            . $this->context->msg(
142                'wikibase-itemlink-id-wrapper',
143                $target->getText()
144            )->inContentLanguage()->escaped()
145            . '</span>';
146
147        $labelHtml = '<span class="wb-itemlink-label"'
148            . ' lang="' . htmlspecialchars( $labelLang->getHtmlCode() ) . '"'
149            . ' dir="' . htmlspecialchars( $labelLang->getDir() ) . '">'
150            . HtmlArmor::getHtml( $label->getText() )
151            . '</span>';
152
153        $text = new HtmlArmor( '<span class="wb-itemlink">'
154            . $this->context->msg( 'wikibase-itemlink' )->rawParams(
155                $labelHtml,
156                $idHtml
157            )->inContentLanguage()->escaped()
158            . '</span>' );
159
160        return true;
161    }
162
163    private function shouldConvertNoBadTitle( Title $currentTitle, LinkRenderer $linkRenderer ): bool {
164        return $linkRenderer->isForComment() ||
165            // Note: this may not work right with special page transclusion. If $out->getTitle()
166            // doesn't return the transcluded special page's title, the transcluded text will
167            // not have entity IDs resolved to labels.
168            // Also Note: Badtitle is excluded because it is used in rendering actual page content
169            // that is added to the ParserCache. See T327062#8796532 and https://www.mediawiki.org/wiki/API:Stashedit
170            ( $currentTitle->isSpecialPage() && !$currentTitle->isSpecial( 'Badtitle' ) );
171    }
172}