Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 84
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
DisplayTitleService
0.00% covered (danger)
0.00%
0 / 84
0.00% covered (danger)
0.00%
0 / 4
1406
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 handleLink
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
462
 getDisplayTitle
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
110
 setSubtitle
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2/*
3 * Permission is hereby granted, free of charge, to any person obtaining a
4 * copy of this software and associated documentation files (the "Software"),
5 * to deal in the Software without restriction, including without limitation
6 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
7 * and/or sell copies of the Software, and to permit persons to whom the
8 * Software is furnished to do so, subject to the following conditions:
9 *
10 * The above copyright notice and this permission notice shall be included in
11 * all copies or substantial portions of the Software.
12 *
13 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 * DEALINGS IN THE SOFTWARE.
20 */
21
22namespace MediaWiki\Extension\DisplayTitle;
23
24use HtmlArmor;
25use MediaWiki\Config\ServiceOptions;
26use MediaWiki\Page\RedirectLookup;
27use MediaWiki\Page\WikiPageFactory;
28use NamespaceInfo;
29use OutputPage;
30use PageProps;
31use Title;
32
33class DisplayTitleService {
34    public const CONSTRUCTOR_OPTIONS = [
35        'DisplayTitleHideSubtitle',
36        'DisplayTitleExcludes',
37        'DisplayTitleFollowRedirects'
38    ];
39
40    /**
41     * @var bool
42     */
43    private $hideSubtitle;
44
45    /**
46     * @var array
47     */
48    private $excludes;
49
50    /**
51     * @var bool
52     */
53    private $followRedirects;
54
55    /**
56     * @var NamespaceInfo
57     */
58    private $namespaceInfo;
59
60    /**
61     * @var RedirectLookup
62     */
63    private $redirectLookup;
64
65    /**
66     * @var PageProps
67     */
68    private $pageProps;
69
70    /**
71     * @var WikiPageFactory
72     */
73    private $wikiPageFactory;
74
75    /**
76     * @param ServiceOptions $options
77     * @param NamespaceInfo $namespaceInfo
78     * @param RedirectLookup $redirectLookup
79     * @param PageProps $pageProps
80     * @param WikiPageFactory $wikiPageFactory
81     */
82    public function __construct(
83        ServiceOptions $options,
84        NamespaceInfo $namespaceInfo,
85        RedirectLookup $redirectLookup,
86        PageProps $pageProps,
87        WikiPageFactory $wikiPageFactory
88    ) {
89        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
90        $this->hideSubtitle = $options->get( 'DisplayTitleHideSubtitle' );
91        $this->excludes = $options->get( 'DisplayTitleExcludes' );
92        $this->followRedirects = $options->get( 'DisplayTitleFollowRedirects' );
93        $this->namespaceInfo = $namespaceInfo;
94        $this->redirectLookup = $redirectLookup;
95        $this->pageProps = $pageProps;
96        $this->wikiPageFactory = $wikiPageFactory;
97    }
98
99    /**
100     * Determines link text for self-links and standard links.
101     * If a link is customized by a user (e. g. [[Target|Text]])
102     * it should remain intact. Let us assume a link is not customized if its
103     * html is the prefixed or (to support Semantic MediaWiki queries)
104     * non-prefixed title of the target page.
105     *
106     * @since 1.3
107     * @param string $pageTitle
108     * @param Title $target the Title object that the link is pointing to
109     * @param string|HtmlArmor &$html the HTML of the link text
110     * @param bool $wrap whether to wrap result in HtmlArmor
111     */
112    public function handleLink( string $pageTitle, Title $target, &$html, bool $wrap ) {
113        // Do not use DisplayTitle if current page is defined in $wgDisplayTitleExcludes
114        if ( in_array( $pageTitle, $this->excludes ) ) {
115            return;
116        }
117
118        // Do not use DisplayTitle if the current page is a redirect to the page being linked
119        $title = Title::newFromText( $pageTitle );
120        if ( $title->canExist() ) {
121            $wikipage = $this->wikiPageFactory->newFromTitle( $title );
122            $redirectTarget = $this->redirectLookup->getRedirectTarget( $wikipage );
123            if ( $redirectTarget && $pageTitle === $target->getPrefixedText() ) {
124                return;
125            }
126        }
127
128        $customized = false;
129        if ( isset( $html ) ) {
130            $text = null;
131            if ( is_string( $html ) ) {
132                $text = str_replace( '_', ' ', $html );
133            } elseif ( is_int( $html ) ) {
134                $text = (string)$html;
135            } elseif ( $html instanceof HtmlArmor ) {
136                $text = HtmlArmor::getHtml( $html );
137                // Remove html tags used for highlighting matched words in the title, see T355481
138                $text = strip_tags( $text );
139                $text = str_replace( '_', ' ', $text );
140            }
141
142            // handle named Semantic MediaWiki subobjects (see T275984) by removing trailing fragment
143            // skip fragment detection on category pages
144            $fragment = '#' . $target->getFragment();
145            if ( $text !== null && $fragment !== '#' && $target->getNamespace() !== NS_CATEGORY ) {
146                $fragmentLength = strlen( $fragment );
147                if ( substr( $text, -$fragmentLength ) === $fragment ) {
148                    // Remove fragment text from the link text
149                    $textTitle = substr( $text, 0, -$fragmentLength );
150                    $textFragment = substr( $fragment, 1 );
151                } else {
152                    $textTitle = $text;
153                    $textFragment = '';
154                }
155                if ( $textTitle === '' || $textFragment === '' ) {
156                    $customized = true;
157                } else {
158                    $text = $textTitle;
159                    if ( $wrap ) {
160                        $html = new HtmlArmor( $text );
161                    }
162                    $customized = $text !== $target->getPrefixedText() && $text !== $target->getText();
163                }
164            } else {
165                $customized = $text !== null
166                    && $text !== $target->getPrefixedText()
167                    && $text !== $target->getSubpageText();
168            }
169        }
170        if ( !$customized && $html !== null ) {
171            $this->getDisplayTitle( $target, $html, $wrap );
172        }
173    }
174
175    /**
176     * Get displaytitle page property text.
177     *
178     * @since 1.0
179     * @param Title $title the Title object for the page
180     * @param string|HtmlArmor &$displaytitle to return the display title, if set
181     * @param bool $wrap whether to wrap result in HtmlArmor
182     * @return bool true if the page has a displaytitle page property that is
183     * different from the prefixed page name, false otherwise
184     */
185    public function getDisplayTitle( Title $title, &$displaytitle, bool $wrap = false ): bool {
186        $title = $title->createFragmentTarget( '' );
187
188        if ( !$title->canExist() ) {
189            // If the Title isn't a valid content page (e.g. Special:UserLogin), just return.
190            return false;
191        }
192
193        $originalPageName = $title->getPrefixedText();
194        $wikipage = $this->wikiPageFactory->newFromTitle( $title );
195        $redirect = false;
196        if ( $this->followRedirects ) {
197            $redirectTarget = $this->redirectLookup->getRedirectTarget( $wikipage );
198            if ( $redirectTarget !== null ) {
199                $redirect = true;
200                $title = Title::newFromLinkTarget( $redirectTarget );
201            }
202        }
203        $id = $title->getArticleID();
204        $values = $this->pageProps->getProperties( $title, 'displaytitle' );
205        if ( array_key_exists( $id, $values ) ) {
206            $value = $values[$id];
207            if ( trim( str_replace( '&#160;', '', strip_tags( $value ) ) ) !== '' &&
208                $value !== $originalPageName ) {
209                $displaytitle = $value;
210                if ( $wrap ) {
211                    // @phan-suppress-next-line SecurityCheck-XSS
212                    $displaytitle = new HtmlArmor( $displaytitle );
213                }
214                return true;
215            }
216        } elseif ( $redirect ) {
217            $displaytitle = $title->getPrefixedText();
218            if ( $wrap ) {
219                $displaytitle = new HtmlArmor( $displaytitle );
220            }
221            return true;
222        }
223        return false;
224    }
225
226    /**
227     * Display subtitle if requested
228     *
229     * @since 4.0
230     * @param OutputPage $out
231     * @return void
232     */
233    public function setSubtitle( OutputPage $out ): void {
234        if ( $this->hideSubtitle ) {
235            return;
236        }
237        $title = $out->getTitle();
238        if ( !$title->isTalkPage() ) {
239            $found = $this->getDisplayTitle( $title, $displaytitle );
240        } else {
241            $subjectPage = Title::castFromLinkTarget( $this->namespaceInfo->getSubjectPage( $title ) );
242            if ( $subjectPage->exists() ) {
243                $found = $this->getDisplayTitle( $subjectPage, $displaytitle );
244            } else {
245                $found = false;
246            }
247        }
248        if ( $found ) {
249            $out->addSubtitle( "<span class=\"mw-displaytitle-subtitle\">" . $title->getPrefixedText() . "</span>" );
250        }
251    }
252}