Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
42.00% |
63 / 150 |
|
0.00% |
0 / 15 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
42.00% |
63 / 150 |
|
0.00% |
0 / 15 |
738.18 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
expandIconTemplateOptions | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
isSiteNoticeSkin | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
isBannerPermitted | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
addBannerToSkinOutput | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
onSiteNoticeAfter | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
onBeforePageDisplay | |
90.62% |
29 / 32 |
|
0.00% |
0 / 1 |
13.14 | |||
getBannerOptions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onParserOutputPostCacheTransform | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
getSectionsDataInternal | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
getRecursiveTocData | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
onOutputPageParserOutput | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
addBadParserFunctionArgsWarning | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
addCustomBanner | |
89.47% |
34 / 38 |
|
0.00% |
0 / 1 |
10.12 | |||
onParserFirstCallInit | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\WikidataPageBanner; |
4 | |
5 | use MediaWiki\Hook\ParserFirstCallInitHook; |
6 | use MediaWiki\Hook\ParserOutputPostCacheTransformHook; |
7 | use MediaWiki\Hook\SiteNoticeAfterHook; |
8 | use MediaWiki\Languages\LanguageConverterFactory; |
9 | use MediaWiki\Message\Message; |
10 | use MediaWiki\Output\Hook\BeforePageDisplayHook; |
11 | use MediaWiki\Output\Hook\OutputPageParserOutputHook; |
12 | use MediaWiki\Output\OutputPage; |
13 | use MediaWiki\Parser\Parser; |
14 | use MediaWiki\Parser\ParserOutput; |
15 | use MediaWiki\Title\Title; |
16 | use OOUI\IconWidget; |
17 | use Skin; |
18 | |
19 | /** |
20 | * This class implements the hookhandlers for WikidataPageBanner |
21 | * |
22 | * TODO: It also currently handles a lot of the actual construction of the Banner. |
23 | * and this should be moved into Banner, WikidataBanner, BannerFactory classes |
24 | */ |
25 | class Hooks implements |
26 | BeforePageDisplayHook, |
27 | OutputPageParserOutputHook, |
28 | ParserFirstCallInitHook, |
29 | ParserOutputPostCacheTransformHook, |
30 | SiteNoticeAfterHook |
31 | { |
32 | |
33 | /** |
34 | * Singleton instance for helper class functions |
35 | * This variable holds the class name for helper functions and is used to make calls to those |
36 | * functions |
37 | * Note that this variable is also used by tests to store a mock classname of helper functions |
38 | * in it externally |
39 | * @var string |
40 | */ |
41 | public static $wpbBannerClass = Banner::class; |
42 | |
43 | /** |
44 | * Holds an array of valid parameters for PAGEBANNER hook. |
45 | * @var string[] |
46 | */ |
47 | private static $allowedParameters = [ |
48 | 'pgname', |
49 | 'tooltip', |
50 | 'toc', |
51 | 'bottomtoc', |
52 | 'origin', |
53 | 'icon-*', |
54 | 'extraClass', |
55 | 'link', |
56 | ]; |
57 | |
58 | private LanguageConverterFactory $languageConverterFactory; |
59 | |
60 | public function __construct( |
61 | LanguageConverterFactory $languageConverterFactory |
62 | ) { |
63 | $this->languageConverterFactory = $languageConverterFactory; |
64 | } |
65 | |
66 | /** |
67 | * Expands icons for rendering via template |
68 | * |
69 | * @param array[] $icons of options for IconWidget |
70 | * @return array[] |
71 | */ |
72 | protected function expandIconTemplateOptions( array $icons ) { |
73 | foreach ( $icons as $key => $iconData ) { |
74 | $widget = new IconWidget( $iconData ); |
75 | $iconData['html'] = $widget->toString(); |
76 | $icons[$key] = $iconData; |
77 | } |
78 | |
79 | return $icons; |
80 | } |
81 | |
82 | /** |
83 | * Checks if the skin should output the wikidata banner before the |
84 | * site subtitle, in which case it should use the sitenotice container. |
85 | * @param Skin $skin |
86 | * @return bool |
87 | */ |
88 | private function isSiteNoticeSkin( Skin $skin ) { |
89 | $currentSkin = $skin->getSkinName(); |
90 | $skins = $skin->getConfig()->get( 'WPBDisplaySubtitleAfterBannerSkins' ); |
91 | return in_array( $currentSkin, $skins ); |
92 | } |
93 | |
94 | /** |
95 | * Determine whether a banner should be shown on the given page. |
96 | * @param Title $title |
97 | * @return bool |
98 | */ |
99 | private function isBannerPermitted( Title $title ) { |
100 | $config = Banner::getWPBConfig(); |
101 | $ns = $title->getNamespace(); |
102 | $enabledMainPage = $title->isMainPage() ? $config->get( 'WPBEnableMainPage' ) : true; |
103 | return self::$wpbBannerClass::validateNamespace( $ns ) && $enabledMainPage; |
104 | } |
105 | |
106 | /** |
107 | * Modifies the template to add the banner html for rendering by the skin to the subtitle |
108 | * if a banner exists and the skin is configured via WPBDisplaySubtitleAfterBannerSkins; |
109 | * Any existing subtitle is made part of the banner and the subtitle is reset. |
110 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforePageDisplay |
111 | * |
112 | * @param OutputPage $out |
113 | * @return bool indicating whether it was added or not |
114 | */ |
115 | public function addBannerToSkinOutput( OutputPage $out ) { |
116 | $skin = $out->getSkin(); |
117 | $isSkinDisabled = self::$wpbBannerClass::isSkinDisabled( $skin ); |
118 | |
119 | // If the skin is using SiteNoticeAfter abort. |
120 | if ( $isSkinDisabled || $this->isSiteNoticeSkin( $skin ) ) { |
121 | return false; |
122 | } |
123 | $banner = $out->getProperty( 'articlebanner' ); |
124 | if ( $banner ) { |
125 | // Insert banner |
126 | $out->addSubtitle( $banner ); |
127 | } |
128 | |
129 | return true; |
130 | } |
131 | |
132 | /** |
133 | * Add banner to skins which output banners into the site notice area. |
134 | * @param string|bool &$siteNotice of the page. |
135 | * @param Skin $skin being used. |
136 | */ |
137 | public function onSiteNoticeAfter( &$siteNotice, $skin ) { |
138 | if ( !self::$wpbBannerClass::isSkinDisabled( $skin ) && |
139 | $this->isSiteNoticeSkin( $skin ) |
140 | ) { |
141 | $out = $skin->getOutput(); |
142 | $banner = $out->getProperty( 'articlebanner' ); |
143 | |
144 | if ( $siteNotice ) { |
145 | $siteNotice .= $banner; |
146 | } else { |
147 | $siteNotice = $banner; |
148 | } |
149 | } |
150 | } |
151 | |
152 | /** |
153 | * Hooks::addBanner Generates banner from given options and adds it and its styles |
154 | * to Output Page. If no options defined through {{PAGEBANNER}}, tries to add a wikidata banner |
155 | * or an image as defined by the PageImages extension or a default one |
156 | * dependent on how extension is configured. |
157 | * |
158 | * @param OutputPage $out |
159 | * @param Skin $skin Skin object being rendered |
160 | */ |
161 | public function onBeforePageDisplay( $out, $skin ): void { |
162 | $config = Banner::getWPBConfig(); |
163 | $title = $out->getTitle(); |
164 | $isDiff = $out->getRequest()->getCheck( 'diff' ); |
165 | $wpbBannerClass = self::$wpbBannerClass; |
166 | |
167 | // if banner-options are set and not a diff page, add banner anyway |
168 | if ( $out->getProperty( 'wpb-banner-options' ) !== null && !$isDiff ) { |
169 | $params = $out->getProperty( 'wpb-banner-options' ); |
170 | $bannername = $params['name']; |
171 | if ( isset( $params['icons'] ) ) { |
172 | $out->enableOOUI(); |
173 | $params['icons'] = $this->expandIconTemplateOptions( $params['icons'] ); |
174 | } |
175 | $banner = $wpbBannerClass::getBannerHtml( $bannername, $params ); |
176 | // attempt to get an automatic banner |
177 | if ( $banner === null ) { |
178 | $params['isAutomatic'] = true; |
179 | $bannername = $wpbBannerClass::getAutomaticBanner( $title ); |
180 | $banner = $wpbBannerClass::getBannerHtml( $bannername, $params ); |
181 | } |
182 | // only add banner and styling if valid banner generated |
183 | if ( $banner !== null ) { |
184 | if ( isset( $params['data-toc'] ) ) { |
185 | $out->addModuleStyles( 'ext.WikidataPageBanner.toc.styles' ); |
186 | } |
187 | $wpbBannerClass::setOutputPageProperties( $out, $banner ); |
188 | |
189 | // FIXME: This is currently only needed to support testing |
190 | $out->setProperty( 'articlebanner-name', $bannername ); |
191 | } |
192 | } elseif ( |
193 | $title->isKnown() && |
194 | $out->isArticle() && |
195 | $this->isBannerPermitted( $title ) && |
196 | $config->get( 'WPBEnableDefaultBanner' ) && |
197 | !$isDiff |
198 | ) { |
199 | // if the page uses no 'PAGEBANNER' invocation and if article page, insert default banner |
200 | // first try to obtain bannername from Wikidata |
201 | $bannername = $wpbBannerClass::getAutomaticBanner( $title ); |
202 | // add title and whether the banner is auto generated to template parameters |
203 | $paramsForBannerTemplate = [ 'title' => $title, 'isAutomatic' => true ]; |
204 | $banner = $wpbBannerClass::getBannerHtml( $bannername, $paramsForBannerTemplate ); |
205 | // only add banner and styling if valid banner generated |
206 | if ( $banner !== null ) { |
207 | $wpbBannerClass::setOutputPageProperties( $out, $banner ); |
208 | |
209 | // set articlebanner property on OutputPage |
210 | // FIXME: This is currently only needed to support testing |
211 | $out->setProperty( 'articlebanner-name', $bannername ); |
212 | } |
213 | } |
214 | $this->addBannerToSkinOutput( $out ); |
215 | } |
216 | |
217 | /** |
218 | * @param ParserOutput $parserOutput |
219 | * @return array|null |
220 | */ |
221 | private function getBannerOptions( ParserOutput $parserOutput ) { |
222 | return $parserOutput->getExtensionData( 'wpb-banner-options' ); |
223 | } |
224 | |
225 | /** |
226 | * Disables the primary table of contents in the article. |
227 | * @param ParserOutput $parserOutput |
228 | * @param string &$text |
229 | * @param array &$options |
230 | */ |
231 | public function onParserOutputPostCacheTransform( $parserOutput, &$text, &$options ): void { |
232 | // Disable table of contents in article. |
233 | $bannerOptions = $this->getBannerOptions( $parserOutput ); |
234 | if ( $bannerOptions !== null && isset( $bannerOptions['enable-toc'] ) ) { |
235 | $enableTocInBanner = $bannerOptions['enable-toc']; |
236 | $options['injectTOC'] = !$enableTocInBanner; |
237 | } |
238 | } |
239 | |
240 | /** |
241 | * Nests child sections within their parent sections. |
242 | * Based on code in SkinComponentTableOfContents. |
243 | * |
244 | * @param array $sections |
245 | * @param int $toclevel |
246 | * @return array |
247 | */ |
248 | private function getSectionsDataInternal( array $sections, int $toclevel = 1 ): array { |
249 | $data = []; |
250 | foreach ( $sections as $i => $section ) { |
251 | // Child section belongs to a higher parent. |
252 | if ( $section->tocLevel < $toclevel ) { |
253 | return $data; |
254 | } |
255 | |
256 | // Set all the parent sections at the current top level. |
257 | if ( $section->tocLevel === $toclevel ) { |
258 | $childSections = $this->getSectionsDataInternal( |
259 | array_slice( $sections, $i + 1 ), |
260 | $toclevel + 1 |
261 | ); |
262 | $data[] = $section->toLegacy() + [ |
263 | 'array-sections' => $childSections, |
264 | ]; |
265 | } |
266 | } |
267 | return $data; |
268 | } |
269 | |
270 | /** |
271 | * @param OutputPage $out |
272 | * @return array |
273 | */ |
274 | private function getRecursiveTocData( OutputPage $out ) { |
275 | $tocData = $out->getTOCData(); |
276 | $sections = $this->getSectionsDataInternal( |
277 | $tocData ? $tocData->getSections() : [] |
278 | ); |
279 | // Since the banner is outside of #mw-content-text, it |
280 | // will be in the 'user language' (set on the root <html> |
281 | // tag) not the 'content language'. Record the proper |
282 | // page language so that we can reset lang/dir in HTML. |
283 | $title = $out->getTitle(); |
284 | // Note that OutputPage::getLanguage() returns user language |
285 | // *not* the page/content language -- but title should always be |
286 | // set for pages we're interested in. |
287 | $lang = $title ? $title->getPageLanguage() : $out->getLanguage(); |
288 | return [ |
289 | 'array-sections' => $sections, |
290 | 'lang' => $lang->getHtmlCode(), |
291 | 'dir' => $lang->getDir(), |
292 | ]; |
293 | } |
294 | |
295 | /** |
296 | * Hooks::onOutputPageParserOutput add banner parameters from ParserOutput to |
297 | * Output page |
298 | * |
299 | * @param OutputPage $outputPage |
300 | * @param ParserOutput $parserOutput |
301 | */ |
302 | public function onOutputPageParserOutput( $outputPage, $parserOutput ): void { |
303 | $options = $this->getBannerOptions( $parserOutput ); |
304 | if ( $options !== null ) { |
305 | // if toc parameter set, then remove original classes and add banner class |
306 | if ( isset( $options['enable-toc'] ) ) { |
307 | $tocData = $outputPage->getTOCData(); |
308 | if ( $tocData ) { |
309 | $options['data-toc'] = $this->getRecursiveTocData( $outputPage ); |
310 | } |
311 | $options['msg-toc'] = $outputPage->msg( 'toc' )->text(); |
312 | } |
313 | |
314 | // set banner properties as an OutputPage property |
315 | $outputPage->setProperty( 'wpb-banner-options', $options ); |
316 | } |
317 | } |
318 | |
319 | /** |
320 | * Validates a given array of parameters against a set of allowed parameters and adds a |
321 | * warning message with a list of unknown parameters and a tracking category, if there are any. |
322 | * |
323 | * @param array $args Array of parameters to check |
324 | * @param Parser $parser ParserOutput object to add the warning message |
325 | */ |
326 | public function addBadParserFunctionArgsWarning( array $args, Parser $parser ) { |
327 | $badParams = []; |
328 | $allowedParams = array_flip( self::$allowedParameters ); |
329 | foreach ( $args as $param => $value ) { |
330 | // manually check for icons, they can have any name with the "icon-" prefix |
331 | if ( !isset( $allowedParams[$param] ) && substr( $param, 0, 5 ) !== 'icon-' ) { |
332 | $badParams[] = $param; |
333 | } |
334 | } |
335 | |
336 | if ( $badParams ) { |
337 | // if there are unknown parameters, add a tracking category |
338 | $parser->addTrackingCategory( 'wikidatapagebanner-invalid-arguments-cat' ); |
339 | |
340 | // this message will be visible when the page preview button is used, but not when the page is |
341 | // saved. It contains a list of unknown parameters. |
342 | $parser->getOutput()->addWarningMsg( |
343 | 'wikidatapagebanner-invalid-arguments', |
344 | Message::listParam( $badParams, 'comma' ) |
345 | ); |
346 | } |
347 | } |
348 | |
349 | /** |
350 | * Hooks::addCustomBanner |
351 | * Parser function hooked to 'PAGEBANNER' magic word, to define a custom banner and options to |
352 | * customize banner such as icons,horizontal TOC,etc. The method does not return any content but |
353 | * sets the banner parameters in ParserOutput object for use at a later stage to generate banner |
354 | * |
355 | * @param Parser $parser |
356 | * @param string $bannername Name of custom banner |
357 | * @param string ...$args |
358 | */ |
359 | public function addCustomBanner( Parser $parser, $bannername, ...$args ) { |
360 | // @var array to hold parameters to be passed to banner template |
361 | $paramsForBannerTemplate = []; |
362 | // Convert $argumentsFromParserFunction into an associative array |
363 | $wpbFunctionsClass = self::$wpbBannerClass; |
364 | $argumentsFromParserFunction = $wpbFunctionsClass::extractOptions( $parser, $args ); |
365 | // if given banner does not exist, return |
366 | $title = $parser->getTitle(); |
367 | |
368 | if ( $this->isBannerPermitted( $title ) ) { |
369 | // check for unknown parameters used in the parser hook and add a warning if there is any |
370 | $this->addBadParserFunctionArgsWarning( $argumentsFromParserFunction, $parser ); |
371 | |
372 | // set title and tooltip attribute to default title |
373 | // convert title to preferred language variant as done in core Parser.php |
374 | $langConv = $this->languageConverterFactory |
375 | ->getLanguageConverter( $parser->getTargetLanguage() ); |
376 | $paramsForBannerTemplate['tooltip'] = $langConv->convert( $title->getText() ); |
377 | $paramsForBannerTemplate['title'] = $langConv->convert( $title->getText() ); |
378 | if ( isset( $argumentsFromParserFunction['pgname'] ) ) { |
379 | // set tooltip attribute to parameter 'pgname', if set |
380 | $paramsForBannerTemplate['tooltip'] = $argumentsFromParserFunction['pgname']; |
381 | // set title attribute to 'pgname' if set |
382 | $paramsForBannerTemplate['title'] = $argumentsFromParserFunction['pgname']; |
383 | } |
384 | // set extra CSS classes added with extraClass attribute |
385 | $wpbFunctionsClass::addCssClasses( $paramsForBannerTemplate, |
386 | $argumentsFromParserFunction ); |
387 | // set tooltip attribute to parameter 'tooltip', if set, which takes highest preference |
388 | if ( isset( $argumentsFromParserFunction['tooltip'] ) ) { |
389 | $paramsForBannerTemplate['tooltip'] = $argumentsFromParserFunction['tooltip']; |
390 | } |
391 | // Add link attribute, to change the target of the banner link. |
392 | if ( isset( $argumentsFromParserFunction['link'] ) ) { |
393 | $paramsForBannerTemplate['link'] = $argumentsFromParserFunction['link']; |
394 | } |
395 | // set 'bottomtoc' parameter to allow TOC completely below the banner |
396 | if ( isset( $argumentsFromParserFunction['bottomtoc'] ) && |
397 | $argumentsFromParserFunction['bottomtoc'] === 'yes' ) { |
398 | $paramsForBannerTemplate['bottomtoc'] = true; |
399 | } |
400 | Banner::addToc( $paramsForBannerTemplate, |
401 | $argumentsFromParserFunction ); |
402 | Banner::addIcons( $paramsForBannerTemplate, |
403 | $argumentsFromParserFunction ); |
404 | Banner::addFocus( $paramsForBannerTemplate, |
405 | $argumentsFromParserFunction ); |
406 | $paramsForBannerTemplate['name'] = $bannername; |
407 | |
408 | // add the valid banner to image links |
409 | // @FIXME:Since bannernames which are to be added are generated here, getBannerHtml can |
410 | // be cleaned to only accept a valid title object pointing to a banner file |
411 | // Default banner is not added to imagelinks as that is the property of this extension |
412 | // and is uniform across all pages |
413 | $bannerTitle = null; |
414 | if ( $wpbFunctionsClass::getImageUrl( $paramsForBannerTemplate['name'] ) !== null ) { |
415 | $bannerTitle = Title::makeTitleSafe( NS_FILE, $paramsForBannerTemplate['name'] ); |
416 | } else { |
417 | $fallbackBanner = $wpbFunctionsClass::getAutomaticBanner( $title ); |
418 | if ( $wpbFunctionsClass::getImageUrl( $fallbackBanner ) !== null ) { |
419 | $bannerTitle = Title::makeTitleSafe( NS_FILE, $fallbackBanner ); |
420 | } |
421 | } |
422 | // add custom or wikidata banner properties to page_props table if a valid banner exists |
423 | // in, checking for custom banner first, then wikidata banner |
424 | if ( $bannerTitle !== null ) { |
425 | $parser->getOutput()->setExtensionData( 'wpb-banner-options', $paramsForBannerTemplate ); |
426 | $parser->fetchFileAndTitle( $bannerTitle ); |
427 | } |
428 | } |
429 | } |
430 | |
431 | /** |
432 | * Hooks::onParserFirstCallInit |
433 | * Hooks the parser function addCustomBanner to the magic word 'PAGEBANNER' |
434 | * |
435 | * @param Parser $parser |
436 | * @return bool |
437 | */ |
438 | public function onParserFirstCallInit( $parser ) { |
439 | $parser->setFunctionHook( |
440 | 'PAGEBANNER', [ $this, 'addCustomBanner' ], Parser::SFH_NO_HASH |
441 | ); |
442 | return true; |
443 | } |
444 | |
445 | } |