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