MediaWiki REL1_34
ImageMap.php
Go to the documentation of this file.
1<?php
21class ImageMap {
22 public static $id = 0;
23
24 const TOP_RIGHT = 0;
25 const BOTTOM_RIGHT = 1;
26 const BOTTOM_LEFT = 2;
27 const TOP_LEFT = 3;
28 const NONE = 4;
29
33 public static function onParserFirstCallInit( Parser &$parser ) {
34 $parser->setHook( 'imagemap', [ 'ImageMap', 'render' ] );
35 }
36
43 public static function render( $input, $params, $parser ) {
45 $config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
46
47 $lines = explode( "\n", $input );
48
49 $first = true;
50 $scale = 1;
51 $imageNode = null;
52 $domDoc = null;
53 $thumbWidth = 0;
54 $thumbHeight = 0;
55 $imageTitle = null;
56 $lineNum = 0;
57 $mapHTML = '';
58 $links = [];
59
60 // Define canonical desc types to allow i18n of 'imagemap_desc_types'
61 $descTypesCanonical = 'top-right, bottom-right, bottom-left, top-left, none';
62 $descType = self::BOTTOM_RIGHT;
63 $defaultLinkAttribs = false;
64 $realmap = true;
65 $extLinks = [];
66 foreach ( $lines as $line ) {
67 ++$lineNum;
68 $externLink = false;
69
70 $line = trim( $line );
71 if ( $line == '' || $line[0] == '#' ) {
72 continue;
73 }
74
75 if ( $first ) {
76 $first = false;
77
78 // The first line should have an image specification on it
79 // Extract it and render the HTML
80 $bits = explode( '|', $line, 2 );
81 if ( count( $bits ) == 1 ) {
82 $image = $bits[0];
83 $options = '';
84 } else {
85 list( $image, $options ) = $bits;
86 }
87 $imageTitle = Title::newFromText( $image );
88 if ( !$imageTitle || !$imageTitle->inNamespace( NS_FILE ) ) {
89 return self::error( 'imagemap_no_image' );
90 }
91 if ( wfIsBadImage( $imageTitle->getDBkey(), $parser->mTitle ) ) {
92 return self::error( 'imagemap_bad_image' );
93 }
94 // Parse the options so we can use links and the like in the caption
95 $parsedOptions = $parser->recursiveTagParse( $options );
96 $imageHTML = $parser->makeImage( $imageTitle, $parsedOptions );
97 $parser->replaceLinkHolders( $imageHTML );
98 $imageHTML = $parser->mStripState->unstripBoth( $imageHTML );
99 $imageHTML = Sanitizer::normalizeCharReferences( $imageHTML );
100
101 $domDoc = new DOMDocument();
102 Wikimedia\suppressWarnings();
103 $ok = $domDoc->loadXML( $imageHTML );
104 Wikimedia\restoreWarnings();
105 if ( !$ok ) {
106 return self::error( 'imagemap_invalid_image' );
107 }
108 $xpath = new DOMXPath( $domDoc );
109 $imgs = $xpath->query( '//img' );
110 if ( !$imgs->length ) {
111 return self::error( 'imagemap_invalid_image' );
112 }
113 $imageNode = $imgs->item( 0 );
114 $thumbWidth = $imageNode->getAttribute( 'width' );
115 $thumbHeight = $imageNode->getAttribute( 'height' );
116
117 $imageObj = wfFindFile( $imageTitle );
118 if ( !$imageObj || !$imageObj->exists() ) {
119 return self::error( 'imagemap_invalid_image' );
120 }
121 // Add the linear dimensions to avoid inaccuracy in the scale
122 // factor when one is much larger than the other
123 // (sx+sy)/(x+y) = s
124 $denominator = $imageObj->getWidth() + $imageObj->getHeight();
125 $numerator = $thumbWidth + $thumbHeight;
126 if ( $denominator <= 0 || $numerator <= 0 ) {
127 return self::error( 'imagemap_invalid_image' );
128 }
129 $scale = $numerator / $denominator;
130 continue;
131 }
132
133 // Handle desc spec
134 $cmd = strtok( $line, " \t" );
135 if ( $cmd == 'desc' ) {
136 $typesText = wfMessage( 'imagemap_desc_types' )->inContentLanguage()->text();
137 if ( $descTypesCanonical != $typesText ) {
138 // i18n desc types exists
139 $typesText = $descTypesCanonical . ', ' . $typesText;
140 }
141 $types = array_map( 'trim', explode( ',', $typesText ) );
142 $type = trim( strtok( '' ) );
143 $descType = array_search( $type, $types );
144 if ( $descType > 4 ) {
145 // A localized descType is used. Subtract 5 to reach the canonical desc type.
146 $descType = $descType - 5;
147 }
148 // <0? In theory never, but paranoia...
149 if ( $descType === false || $descType < 0 ) {
150 return self::error( 'imagemap_invalid_desc', $typesText );
151 }
152 continue;
153 }
154
155 $title = false;
156 // Find the link
157 $link = trim( strstr( $line, '[' ) );
158 $m = [];
159 if ( preg_match( '/^ \[\[ ([^|]*+) \| ([^\]]*+) \]\] \w* $ /x', $link, $m ) ) {
160 $title = Title::newFromText( $m[1] );
161 $alt = trim( $m[2] );
162 } elseif ( preg_match( '/^ \[\[ ([^\]]*+) \]\] \w* $ /x', $link, $m ) ) {
163 $title = Title::newFromText( $m[1] );
164 if ( is_null( $title ) ) {
165 return self::error( 'imagemap_invalid_title', $lineNum );
166 }
167 $alt = $title->getFullText();
168 } elseif ( in_array( substr( $link, 1, strpos( $link, '//' ) + 1 ), $wgUrlProtocols )
169 || in_array( substr( $link, 1, strpos( $link, ':' ) ), $wgUrlProtocols )
170 ) {
171 if ( preg_match( '/^ \[ ([^\s]*+) \s ([^\]]*+) \] \w* $ /x', $link, $m ) ) {
172 $title = $m[1];
173 $alt = trim( $m[2] );
174 $externLink = true;
175 } elseif ( preg_match( '/^ \[ ([^\]]*+) \] \w* $ /x', $link, $m ) ) {
176 $title = $alt = trim( $m[1] );
177 $externLink = true;
178 }
179 } else {
180 return self::error( 'imagemap_no_link', $lineNum );
181 }
182 if ( !$title ) {
183 return self::error( 'imagemap_invalid_title', $lineNum );
184 }
185
186 $shapeSpec = substr( $line, 0, -strlen( $link ) );
187
188 // Tokenize shape spec
189 $shape = strtok( $shapeSpec, " \t" );
190 switch ( $shape ) {
191 case 'default':
192 $coords = [];
193 break;
194 case 'rect':
195 $coords = self::tokenizeCoords( 4, $lineNum );
196 if ( !is_array( $coords ) ) {
197 return $coords;
198 }
199 break;
200 case 'circle':
201 $coords = self::tokenizeCoords( 3, $lineNum );
202 if ( !is_array( $coords ) ) {
203 return $coords;
204 }
205 break;
206 case 'poly':
207 $coords = [];
208 $coord = strtok( " \t" );
209 while ( $coord !== false ) {
210 if ( !is_numeric( $coord ) || $coord > 1e9 ) {
211 return self::error( 'imagemap_invalid_coord', $lineNum );
212 }
213 $coords[] = $coord;
214 $coord = strtok( " \t" );
215 }
216 if ( !count( $coords ) ) {
217 return self::error( 'imagemap_missing_coord', $lineNum );
218 }
219 if ( count( $coords ) % 2 !== 0 ) {
220 return self::error( 'imagemap_poly_odd', $lineNum );
221 }
222 break;
223 default:
224 return self::error( 'imagemap_unrecognised_shape', $lineNum );
225 }
226
227 // Scale the coords using the size of the source image
228 foreach ( $coords as $i => $c ) {
229 $coords[$i] = (int)round( $c * $scale );
230 }
231
232 // Construct the area tag
233 $attribs = [];
234 if ( $externLink ) {
235 $attribs['href'] = $title;
236 $attribs['class'] = 'plainlinks';
237 if ( $wgNoFollowLinks ) {
238 $attribs['rel'] = 'nofollow';
239 }
240 } elseif ( $title->getFragment() != '' && $title->getPrefixedDBkey() == '' ) {
241 // XXX: kluge to handle [[#Fragment]] links, should really fix getLocalURL()
242 // in Title.php to return an empty string in this case
243 $attribs['href'] = $title->getFragmentForURL();
244 } else {
245 $attribs['href'] = $title->getLocalURL() . $title->getFragmentForURL();
246 }
247 if ( $shape != 'default' ) {
248 $attribs['shape'] = $shape;
249 }
250 if ( $coords ) {
251 $attribs['coords'] = implode( ',', $coords );
252 }
253 if ( $alt != '' ) {
254 if ( $shape != 'default' ) {
255 $attribs['alt'] = $alt;
256 }
257 $attribs['title'] = $alt;
258 }
259 if ( $shape == 'default' ) {
260 $defaultLinkAttribs = $attribs;
261 } else {
262 $mapHTML .= Xml::element( 'area', $attribs ) . "\n";
263 }
264 if ( $externLink ) {
265 $extLinks[] = $title;
266 } else {
267 $links[] = $title;
268 }
269 }
270
271 if ( $first || !$imageNode ) {
272 return self::error( 'imagemap_no_image' );
273 }
274
275 if ( $mapHTML == '' ) {
276 // no areas defined, default only. It's not a real imagemap, so we do not need some tags
277 $realmap = false;
278 }
279
280 if ( $realmap ) {
281 // Construct the map
282 // Add a hash of the map HTML to avoid breaking cached HTML fragments that are
283 // later joined together on the one page (T18471).
284 // The only way these hashes can clash is if the map is identical, in which
285 // case it wouldn't matter that the "wrong" map was used.
286 $mapName = 'ImageMap_' . substr( md5( $mapHTML ), 0, 16 );
287 $mapHTML = "<map name=\"$mapName\">\n$mapHTML</map>\n";
288
289 // Alter the image tag
290 $imageNode->setAttribute( 'usemap', "#$mapName" );
291 }
292
293 // Add a surrounding div, remove the default link to the description page
294 $anchor = $imageNode->parentNode;
295 $parent = $anchor->parentNode;
296 $div = $parent->insertBefore( new DOMElement( 'div' ), $anchor );
297 $div->setAttribute( 'class', 'noresize' );
298 if ( $defaultLinkAttribs ) {
299 $defaultAnchor = $div->appendChild( new DOMElement( 'a' ) );
300 foreach ( $defaultLinkAttribs as $name => $value ) {
301 $defaultAnchor->setAttribute( $name, $value );
302 }
303 $imageParent = $defaultAnchor;
304 } else {
305 $imageParent = $div;
306 }
307
308 // Add the map HTML to the div
309 // We used to add it before the div, but that made tidy unhappy
310 if ( $mapHTML != '' ) {
311 $mapDoc = new DOMDocument();
312 $mapDoc->loadXML( $mapHTML );
313 $mapNode = $domDoc->importNode( $mapDoc->documentElement, true );
314 $div->appendChild( $mapNode );
315 }
316
317 $imageParent->appendChild( $imageNode->cloneNode( true ) );
318 $parent->removeChild( $anchor );
319
320 // Determine whether a "magnify" link is present
321 $xpath = new DOMXPath( $domDoc );
322 $magnify = $xpath->query( '//div[@class="magnify"]' );
323 if ( !$magnify->length && $descType != self::NONE ) {
324 // Add image description link
325 if ( $descType == self::TOP_LEFT || $descType == self::BOTTOM_LEFT ) {
326 $marginLeft = 0;
327 } else {
328 $marginLeft = $thumbWidth - 20;
329 }
330 if ( $descType == self::TOP_LEFT || $descType == self::TOP_RIGHT ) {
331 $marginTop = -$thumbHeight;
332 // 1px hack for IE, to stop it poking out the top
333 $marginTop += 1;
334 } else {
335 $marginTop = -20;
336 }
337 $div->setAttribute( 'style', "height: {$thumbHeight}px; width: {$thumbWidth}px; " );
338 $descWrapper = $div->appendChild( new DOMElement( 'div' ) );
339 $descWrapper->setAttribute( 'style',
340 "margin-left: {$marginLeft}px; " .
341 "margin-top: {$marginTop}px; " .
342 "text-align: left;"
343 );
344
345 $descAnchor = $descWrapper->appendChild( new DOMElement( 'a' ) );
346 $descAnchor->setAttribute( 'href', $imageTitle->getLocalURL() );
347 $descAnchor->setAttribute(
348 'title',
349 wfMessage( 'imagemap_description' )->inContentLanguage()->text()
350 );
351 $descImg = $descAnchor->appendChild( new DOMElement( 'img' ) );
352 $descImg->setAttribute(
353 'alt',
354 wfMessage( 'imagemap_description' )->inContentLanguage()->text()
355 );
356 $url = $config->get( 'ExtensionAssetsPath' ) . '/ImageMap/resources/desc-20.png';
357 $descImg->setAttribute(
358 'src',
359 OutputPage::transformResourcePath( $config, $url )
360 );
361 $descImg->setAttribute( 'style', 'border: none;' );
362 }
363
364 // Output the result
365 // We use saveXML() not saveHTML() because then we get XHTML-compliant output.
366 // The disadvantage is that we have to strip out the DTD
367 $output = preg_replace( '/<\?xml[^?]*\?>/', '', $domDoc->saveXML( null, LIBXML_NOEMPTYTAG ) );
368
369 // Register links
370 foreach ( $links as $title ) {
371 if ( $title->isExternal() || $title->getNamespace() == NS_SPECIAL ) {
372 // Don't register special or interwiki links...
373 } elseif ( $title->getNamespace() == NS_MEDIA ) {
374 // Regular Media: links are recorded as image usages
375 $parser->mOutput->addImage( $title->getDBkey() );
376 } else {
377 // Plain ol' link
378 $parser->mOutput->addLink( $title );
379 }
380 }
381 foreach ( $extLinks as $title ) {
382 $parser->mOutput->addExternalLink( $title );
383 }
384 // Armour output against broken parser
385 $output = str_replace( "\n", '', $output );
386 return $output;
387 }
388
394 private static function tokenizeCoords( $count, $lineNum ) {
395 $coords = [];
396 for ( $i = 0; $i < $count; $i++ ) {
397 $coord = strtok( " \t" );
398 if ( $coord === false ) {
399 return self::error( 'imagemap_missing_coord', $lineNum );
400 }
401 if ( !is_numeric( $coord ) || $coord > 1e9 || $coord < 0 ) {
402 return self::error( 'imagemap_invalid_coord', $lineNum );
403 }
404 $coords[$i] = $coord;
405 }
406 return $coords;
407 }
408
414 private static function error( $name, $line = false ) {
415 return '<p class="error">' . wfMessage( $name, $line )->parse() . '</p>';
416 }
417}
$wgNoFollowLinks
If true, external URL links in wiki text will be given the rel="nofollow" attribute as a hint to sear...
$wgUrlProtocols
URL schemes that should be recognized as valid by wfParseUrl().
wfFindFile( $title, $options=[])
Find a file.
wfIsBadImage( $name, $contextTitle=false, $blacklist=null)
Determine if an image exists on the 'bad image list'.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
$line
Definition cdb.php:59
const BOTTOM_RIGHT
Definition ImageMap.php:25
const TOP_LEFT
Definition ImageMap.php:27
static error( $name, $line=false)
Definition ImageMap.php:414
const BOTTOM_LEFT
Definition ImageMap.php:26
static $id
Definition ImageMap.php:22
const NONE
Definition ImageMap.php:28
static onParserFirstCallInit(Parser &$parser)
Definition ImageMap.php:33
static render( $input, $params, $parser)
Definition ImageMap.php:43
const TOP_RIGHT
Definition ImageMap.php:24
static tokenizeCoords( $count, $lineNum)
Definition ImageMap.php:394
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:74
setHook( $tag, callable $callback)
Create an HTML-style tag, e.g.
Definition Parser.php:5189
const NS_FILE
Definition Defines.php:75
const NS_SPECIAL
Definition Defines.php:58
const NS_MEDIA
Definition Defines.php:57
$lines
Definition router.php:61