Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 84 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
DisplayTitleService | |
0.00% |
0 / 84 |
|
0.00% |
0 / 4 |
1406 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
handleLink | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
462 | |||
getDisplayTitle | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
110 | |||
setSubtitle | |
0.00% |
0 / 11 |
|
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 | |
22 | namespace MediaWiki\Extension\DisplayTitle; |
23 | |
24 | use HtmlArmor; |
25 | use MediaWiki\Config\ServiceOptions; |
26 | use MediaWiki\Page\RedirectLookup; |
27 | use MediaWiki\Page\WikiPageFactory; |
28 | use NamespaceInfo; |
29 | use OutputPage; |
30 | use PageProps; |
31 | use Title; |
32 | |
33 | class 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( ' ', '', 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 | } |