Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
12.20% covered (danger)
12.20%
20 / 164
6.67% covered (danger)
6.67%
1 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Banner
12.20% covered (danger)
12.20%
20 / 164
6.67% covered (danger)
6.67%
1 / 15
2749.81
0.00% covered (danger)
0.00%
0 / 1
 addToc
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 addIcons
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
56
 addFocus
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
132
 extractOptions
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getBannerHtml
16.67% covered (danger)
16.67%
6 / 36
0.00% covered (danger)
0.00%
0 / 1
45.04
 getImageUrl
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
3.43
 getStandardSizeUrls
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 getAutomaticBanner
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getPageImagesBanner
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 getWikidataBanner
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
56
 isSkinDisabled
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 setOutputPageProperties
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getWPBConfig
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addCssClasses
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 validateNamespace
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace MediaWiki\Extension\WikidataPageBanner;
4
5use ExtensionRegistry;
6use MediaWiki\Config\Config;
7use MediaWiki\Html\TemplateParser;
8use MediaWiki\MediaWikiServices;
9use MediaWiki\Output\OutputPage;
10use MediaWiki\Parser\Sanitizer;
11use MediaWiki\Title\Title;
12use PageImages\PageImages;
13use Parser;
14use Skin;
15use Wikibase\Client\WikibaseClient;
16use Wikibase\DataModel\Entity\Item;
17use Wikibase\DataModel\Entity\NumericPropertyId;
18use Wikibase\DataModel\Snak\PropertyValueSnak;
19
20/**
21 * This class is responsible for generating a banner
22 * in response to Hook events.
23 *
24 * TODO: make non-static and split wikidata specific logic into a WikidataBanner class
25 */
26class Banner {
27    /**
28     * Set bannertoc variable on parser output object
29     *
30     * @param array &$paramsForBannerTemplate banner parameters array
31     * @param array $options options from parser function
32     */
33    public static function addToc( &$paramsForBannerTemplate, $options ) {
34        if ( isset( $options['toc'] ) && $options['toc'] === 'yes' ) {
35            $paramsForBannerTemplate['enable-toc'] = true;
36        }
37    }
38
39    /**
40     * Render icons using OOJS-UI for icons which are set in arguments
41     *
42     * @param array &$paramsForBannerTemplate Parameters defined for banner template
43     * @param array $argumentsFromParserFunction Arguments passed to {{PAGEBANNER}} function
44     */
45    public static function addIcons( &$paramsForBannerTemplate, $argumentsFromParserFunction ) {
46        $iconsToAdd = [];
47
48        // check all parameters and look for one's starting with icon-
49        // The old format of icons=star,unesco would not generate any icons
50        foreach ( $argumentsFromParserFunction as $key => $value ) {
51            // found a valid icon parameter, so process it
52            if ( substr( $key, 0, 5 ) === 'icon-' ) {
53                // extract iconname after 'icon-' til the end of key
54                $iconname = substr( $key, 5 );
55                if ( !isset( $iconname ) || !isset( $value ) ) {
56                    continue;
57                }
58
59                $iconName = Sanitizer::escapeClass( $iconname );
60                $iconUrl = Title::newFromText( $value );
61                $iconTitleText = $iconName;
62                $finalIcon = [ 'url' => '#' ];
63                // reference article for icons provided and is valid, then add its link
64                if ( $iconUrl ) {
65                    $finalIcon['url'] = $iconUrl->getLocalURL();
66                    // set icon title to title of referring article
67                    $iconTitleText = $iconUrl->getText();
68                }
69                $finalIcon['icon'] = $iconName;
70                $finalIcon['title'] = $iconTitleText;
71                $iconsToAdd[] = $finalIcon;
72            }
73        }
74
75        // only set hasIcons to true if parser function gives some non-empty icon names
76        if ( $iconsToAdd ) {
77            $paramsForBannerTemplate['hasIcons'] = true;
78            $paramsForBannerTemplate['icons'] = $iconsToAdd;
79        }
80    }
81
82    /**
83     * Sets focus parameter on banner templates to shift focus on banner when cropped
84     *
85     * @param array &$paramsForBannerTemplate Parameters defined for banner template
86     * @param array $argumentsFromParserFunction Arguments passed to {{PAGEBANNER}} function
87     */
88    public static function addFocus( &$paramsForBannerTemplate, $argumentsFromParserFunction ) {
89        // default centering would be 0, and -1 would represent extreme left and extreme top
90        // Allowed values for each coordinate is between 0 and 1
91        // If no value has been specified these are set to null
92        $paramsForBannerTemplate['data-pos-x'] = 0;
93        $paramsForBannerTemplate['data-pos-y'] = 0;
94        $paramsForBannerTemplate['hasPosition'] = false;
95
96        if ( isset( $argumentsFromParserFunction['origin'] ) ) {
97            // split the origin into x and y coordinates
98            $coords = explode( ',', $argumentsFromParserFunction['origin'] );
99            if ( count( $coords ) === 2 ) {
100                $paramsForBannerTemplate['hasPosition'] = true;
101                $positionx = $coords[0];
102                $positiony = $coords[1];
103                // TODO:Add a js module to use the data-pos values being set below to fine tune the
104                // position of the banner to emulate a coordinate system.
105                if ( filter_var( $positionx, FILTER_VALIDATE_FLOAT ) !== false ) {
106                    if ( $positionx >= -1 && $positionx <= 1 ) {
107                        $paramsForBannerTemplate['data-pos-x'] = $positionx;
108                        if ( $positionx <= -0.25 ) {
109                            // these are classes to be added in case js is disabled
110                            $paramsForBannerTemplate['originx'] = 'wpb-left';
111                        } elseif ( $positionx >= 0.25 ) {
112                            $paramsForBannerTemplate['originx'] = 'wpb-right';
113                        }
114                    }
115                }
116                if ( filter_var( $positiony, FILTER_VALIDATE_FLOAT ) !== false ) {
117                    if ( $positiony >= -1 && $positiony <= 1 ) {
118                        $paramsForBannerTemplate['data-pos-y'] = $positiony;
119                    }
120                }
121            }
122        }
123    }
124
125    /**
126     * Converts an array of values in form [0] => "name=value" into a real
127     * associative array in form [name] => value
128     *
129     * @param Parser $parser
130     * @param string[] $options
131     * @return array $results
132     */
133    public static function extractOptions( Parser $parser, array $options ) {
134        $results = [];
135        $langConv = MediaWikiServices::getInstance()->getLanguageConverterFactory()
136            ->getLanguageConverter( $parser->getTargetLanguage() );
137
138        foreach ( $options as $option ) {
139            $pair = explode( '=', $option, 2 );
140            if ( count( $pair ) == 2 ) {
141                $name = trim( $pair[0] );
142                // convert value to preferred language variant as
143                // done in core Parser.php
144                $value = $langConv->convert( trim( $pair[1] ) );
145                $results[$name] = $value;
146            }
147        }
148
149        return $results;
150    }
151
152    /**
153     * Hooks::getBannerHtml
154     * Returns the html code for the pagebanner
155     *
156     * @param string $bannername FileName of banner image
157     * @param array $options additional parameters passed to template
158     * @return string|null Html code of the banner or null if invalid bannername
159     */
160    public static function getBannerHtml( $bannername, $options = [] ) {
161        $config = self::getWPBConfig();
162        $urls = static::getStandardSizeUrls( $bannername );
163        $banner = null;
164        /** @var string srcset attribute for <img> element of banner image */
165        $srcset = [];
166
167        // if a valid bannername given, set banner
168        if ( $urls ) {
169            // @var int index variable
170            $i = 0;
171            foreach ( $urls as $url ) {
172                $size = $config->get( 'WPBStandardSizes' );
173                $size = $size[$i];
174                // add url with width and a comma if not adding the last url
175                if ( $i < count( $urls ) ) {
176                    $srcset[] = "$url {$size}w";
177                }
178                $i++;
179            }
180            // create full src set from individual urls, separated by comma
181            $srcset = implode( ',', $srcset );
182            // use largest image url as src attribute
183            $bannerurl = $urls[count( $urls ) - 1];
184            $bannerfile = Title::newFromText( "File:$bannername" );
185            $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $bannerfile );
186            // don't auto generate banner if image is not landscape, see bug report T131424
187            $fileWidth = $file->getWidth();
188            $fileHeight = $file->getHeight();
189            if ( !empty( $options['isAutomatic'] ) && $fileWidth < 1.5 * $fileHeight ) {
190                return null;
191            }
192            // Get the URL of the link. Can be an internal or external link, or none. Defaults to the image's page.
193            if ( isset( $options['link'] ) ) {
194                $href = $options['link'] === '' ? false : Skin::makeInternalOrExternalUrl( $options['link'] );
195            } else {
196                $href = $bannerfile->getLocalURL();
197            }
198            $templateParser = new TemplateParser( __DIR__ . '/../templates' );
199            $templateParser->enableRecursivePartials( true );
200            $options['href'] = $href;
201            $options['banner'] = $bannerurl;
202            $options['srcset'] = $srcset;
203            $options['maxWidth'] = $fileWidth;
204            // Provide information to the logic-less template about whether it is a panorama or not.
205            $options['isPanorama'] = $fileWidth > ( $fileHeight * 2 );
206            $options['isHeadingOverrideEnabled'] = $config->get( 'WPBEnableHeadingOverride' );
207            $banner = $templateParser->processTemplate(
208                'banner',
209                $options
210            );
211        }
212
213        return $banner;
214    }
215
216    /**
217     * Hooks::getImageUrl
218     * Return the full url of the banner image, stored on the wiki, given the
219     * image name. Additionally, if a width parameter is specified, it creates
220     * and returns url of an image of specified width.
221     *
222     * @param string $filename Filename of the banner image
223     * @param int|null $imagewidth
224     * @return string|null Full url of the banner image on the wiki or null
225     */
226    public static function getImageUrl( $filename, $imagewidth = null ) {
227        // make title object from image name
228        $title = Title::makeTitleSafe( NS_FILE, $filename );
229        $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title );
230        $options = [
231            'options' => [ 'min_range' => 0, 'max_range' => 3000 ]
232        ];
233        // if file not found, return null
234        if ( $file === false ) {
235            return null;
236        } elseif ( filter_var( $imagewidth, FILTER_VALIDATE_INT, $options ) !== false ) {
237            // validate $bannerwidth to be a width within 3000
238            $mto = $file->transform( [ 'width' => $imagewidth ] );
239            return wfExpandUrl( $mto->getUrl(), PROTO_CURRENT );
240        } else {
241            // return image without transforming, if width not valid
242            return $file->getFullUrl();
243        }
244    }
245
246    /**
247     * Hooks::getStandardSizeUrls
248     * returns an array of urls of standard image sizes defined by $wgWPBStandardSizes
249     *
250     * @param string $filename Name of Image file
251     * @return array
252     */
253    public static function getStandardSizeUrls( $filename ) {
254        $urlSet = [];
255
256        foreach ( self::getWPBConfig()->get( 'WPBStandardSizes' ) as $size ) {
257            $url = static::getImageUrl( $filename, $size );
258            // prevent duplication in urlSet
259            if ( $url !== null && !in_array( $url, $urlSet, true ) ) {
260                $urlSet[] = $url;
261            }
262        }
263
264        return $urlSet;
265    }
266
267    /**
268     * Fetches a banner for a given title when none has been specified by an editor
269     *
270     * @param Title $title Title of the page
271     * @return string|null file name of a suitable automatic banner or null if none found
272     */
273    public static function getAutomaticBanner( $title ) {
274        $config = self::getWPBConfig();
275        $bannername = static::getWikidataBanner( $title );
276
277        if ( $bannername === null ) {
278            $bannername = static::getPageImagesBanner( $title );
279        }
280        if ( $bannername === null ) {
281            // if Wikidata banner not found, set bannername to default banner
282            $bannername = $config->get( 'WPBImage' );
283        }
284        return $bannername;
285    }
286
287    /**
288     * Fetches banner from PageImages
289     *
290     * @param Title $title Title of the page
291     * @return string|null file name of the banner found via page images
292     * or null if none found
293     */
294    public static function getPageImagesBanner( $title ) {
295        $config = self::getWPBConfig();
296
297        if (
298            $config->get( 'WPBEnablePageImagesBanners' ) &&
299            ExtensionRegistry::getInstance()->isLoaded( 'PageImages' )
300        ) {
301            $pi = PageImages::getPageImage( $title );
302            // getPageImage returns false if no page image.
303            if ( $pi ) {
304                return $pi->getTitle()->getDBkey();
305            }
306        }
307
308        return null;
309    }
310
311    /**
312     * Hooks::getWikidataBanner Fetches banner from wikidata for the specified page
313     *
314     * @param Title $title Title of the page
315     * @return string|null file name of the banner from wikidata
316     * or null if none found
317     */
318    public static function getWikidataBanner( $title ) {
319        $banner = null;
320        $wpbBannerProperty = self::getWPBConfig()->get( 'WPBBannerProperty' );
321        if ( $wpbBannerProperty === '' ) {
322            return null;
323        }
324        if ( !ExtensionRegistry::getInstance()->isLoaded( 'WikibaseClient' ) ) {
325            return null;
326        }
327
328        $entityIdLookup = WikibaseClient::getEntityIdLookup();
329        $itemId = $entityIdLookup->getEntityIdForTitle( $title );
330        // check if this page has an associated item page
331        if ( $itemId !== null ) {
332            $entityLookup = WikibaseClient::getEntityLookup();
333            $item = $entityLookup->getEntity( $itemId );
334            if ( !( $item instanceof Item ) ) {
335                // Sometimes EntityIdLookup is not consistent/ up to date with repo
336                return null;
337            }
338            $statements = $item->getStatements()->getByPropertyId(
339                new NumericPropertyId( $wpbBannerProperty )
340            )->getBestStatements();
341            if ( !$statements->isEmpty() ) {
342                $statements = $statements->toArray();
343                $snak = $statements[0]->getMainSnak();
344                if ( $snak instanceof PropertyValueSnak ) {
345                    $banner = $snak->getDataValue()->getValue();
346                }
347            }
348        }
349
350        return $banner;
351    }
352
353    /**
354     * @param Skin $skin
355     * @return bool
356     */
357    public static function isSkinDisabled( $skin ) {
358        $skinName = $skin->getSkinName();
359        $config = $skin->getConfig();
360        $skinDisabled = (array)$config->get( 'WPBSkinDisabled' );
361        return in_array( $skinName, $skinDisabled );
362    }
363
364    /**
365     * Insert banner HTML into the page as a page property.
366     * Suppress primary page title if configured.
367     *
368     * @param OutputPage $out to inject banner into
369     * @param string $html of banner to insert
370     */
371    public static function setOutputPageProperties( $out, $html ) {
372        $config = self::getWPBConfig();
373
374        if ( $config->get( 'WPBEnableHeadingOverride' )
375            && !self::isSkinDisabled( $out->getSkin() ) ) {
376            $htmlTitle = $out->getHTMLTitle();
377            // hide primary title
378            $out->setPageTitle( '' );
379            // set html title again, because above call also empties the <title> tag
380            $out->setHTMLTitle( $htmlTitle );
381        }
382        // set articlebanner property on OutputPage for getSkinTemplateOutputPageBeforeExec hook
383        $out->setProperty( 'articlebanner', $html );
384
385        // Add common resources
386        $out->addModuleStyles( 'ext.WikidataPageBanner' );
387        $out->addModuleStyles( 'ext.WikidataPageBanner.print.styles' );
388        $out->addModules( 'ext.WikidataPageBanner.positionBanner' );
389    }
390
391    /**
392     * Returns a new or cached config object for Hooks extension.
393     *
394     * @return Config
395     */
396    public static function getWPBConfig() {
397        return MediaWikiServices::getInstance()->getConfigFactory()
398            ->makeConfig( 'wikidatapagebanner' );
399    }
400
401    /**
402     * Adds banner custom CSS classes according to extraClass parameter
403     *
404     * @param array &$paramsForBannerTemplate Parameters defined for banner template
405     * @param array $argumentsFromParserFunction Arguments passed to {{PAGEBANNER}} function
406     */
407    public static function addCssClasses( &$paramsForBannerTemplate,
408            $argumentsFromParserFunction ) {
409        $paramsForBannerTemplate['extraClass'] = '';
410        if ( isset( $argumentsFromParserFunction['extraClass'] ) ) {
411            $classes = explode( ' ', $argumentsFromParserFunction['extraClass'] );
412            foreach ( $classes as $class ) {
413                $paramsForBannerTemplate['extraClass'] .= ' ' . Sanitizer::escapeClass( $class );
414            }
415        }
416    }
417
418    /**
419     * Check if the namespace should have a banner on it by default.
420     * $wgWPBNamespaces can be an array of namespaces, or true, in which case it applies to all
421     * namespaces. If it's true, certain namespaces can be disabled with $wgWPBDisabledNamespaces.
422     *
423     * @param int $ns Namespace of page
424     * @return bool
425     */
426    public static function validateNamespace( $ns ) {
427        $config = self::getWPBConfig();
428        if ( $config->get( 'WPBNamespaces' ) === true ) {
429            return !in_array( $ns, $config->get( 'WPBDisabledNamespaces' ) );
430        } else {
431            return in_array( $ns, $config->get( 'WPBNamespaces' ) );
432        }
433    }
434}