Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
12.20% |
20 / 164 |
|
6.67% |
1 / 15 |
CRAP | |
0.00% |
0 / 1 |
Banner | |
12.20% |
20 / 164 |
|
6.67% |
1 / 15 |
2749.81 | |
0.00% |
0 / 1 |
addToc | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
12 | |||
addIcons | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
56 | |||
addFocus | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
132 | |||
extractOptions | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
getBannerHtml | |
16.67% |
6 / 36 |
|
0.00% |
0 / 1 |
45.04 | |||
getImageUrl | |
63.64% |
7 / 11 |
|
0.00% |
0 / 1 |
3.43 | |||
getStandardSizeUrls | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
4.07 | |||
getAutomaticBanner | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getPageImagesBanner | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
getWikidataBanner | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
56 | |||
isSkinDisabled | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
setOutputPageProperties | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
getWPBConfig | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
addCssClasses | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
validateNamespace | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\WikidataPageBanner; |
4 | |
5 | use ExtensionRegistry; |
6 | use MediaWiki\Config\Config; |
7 | use MediaWiki\Html\TemplateParser; |
8 | use MediaWiki\MediaWikiServices; |
9 | use MediaWiki\Output\OutputPage; |
10 | use MediaWiki\Parser\Sanitizer; |
11 | use MediaWiki\Title\Title; |
12 | use PageImages\PageImages; |
13 | use Parser; |
14 | use Skin; |
15 | use Wikibase\Client\WikibaseClient; |
16 | use Wikibase\DataModel\Entity\Item; |
17 | use Wikibase\DataModel\Entity\NumericPropertyId; |
18 | use 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 | */ |
26 | class 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 | } |