MediaWiki  1.34.0
ImageMap.php
Go to the documentation of this file.
1 <?php
21 class 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 }
Title\newFromText
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:316
ImageMap\TOP_RIGHT
const TOP_RIGHT
Definition: ImageMap.php:24
NS_FILE
const NS_FILE
Definition: Defines.php:66
ImageMap
Definition: ImageMap.php:21
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1264
ImageMap\$id
static $id
Definition: ImageMap.php:22
NS_SPECIAL
const NS_SPECIAL
Definition: Defines.php:49
ConfigFactory\getDefaultInstance
static getDefaultInstance()
Definition: ConfigFactory.php:51
Xml\element
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:41
$lines
$lines
Definition: router.php:61
$title
$title
Definition: testCompression.php:34
$output
$output
Definition: SyntaxHighlight.php:335
ImageMap\tokenizeCoords
static tokenizeCoords( $count, $lineNum)
Definition: ImageMap.php:394
ImageMap\error
static error( $name, $line=false)
Definition: ImageMap.php:414
ImageMap\BOTTOM_RIGHT
const BOTTOM_RIGHT
Definition: ImageMap.php:25
ImageMap\TOP_LEFT
const TOP_LEFT
Definition: ImageMap.php:27
$line
$line
Definition: cdb.php:59
wfIsBadImage
wfIsBadImage( $name, $contextTitle=false, $blacklist=null)
Determine if an image exists on the 'bad image list'.
Definition: GlobalFunctions.php:2924
$wgUrlProtocols
$wgUrlProtocols
URL schemes that should be recognized as valid by wfParseUrl().
Definition: DefaultSettings.php:4185
$wgNoFollowLinks
$wgNoFollowLinks
If true, external URL links in wiki text will be given the rel="nofollow" attribute as a hint to sear...
Definition: DefaultSettings.php:4284
NS_MEDIA
const NS_MEDIA
Definition: Defines.php:48
ImageMap\NONE
const NONE
Definition: ImageMap.php:28
wfFindFile
wfFindFile( $title, $options=[])
Find a file.
Definition: GlobalFunctions.php:2604
ImageMap\onParserFirstCallInit
static onParserFirstCallInit(Parser &$parser)
Definition: ImageMap.php:33
ImageMap\render
static render( $input, $params, $parser)
Definition: ImageMap.php:43
ImageMap\BOTTOM_LEFT
const BOTTOM_LEFT
Definition: ImageMap.php:26
$type
$type
Definition: testCompression.php:48