ImageMap_body.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. <?php
  2. /**
  3. * Image map extension.
  4. *
  5. * Syntax:
  6. * <imagemap>
  7. * Image:Foo.jpg | 100px | picture of a foo
  8. *
  9. * rect 0 0 50 50 [[Foo type A]]
  10. * circle 50 50 20 [[Foo type B]]
  11. *
  12. * desc bottom-left
  13. * </imagemap>
  14. *
  15. * Coordinates are relative to the source image, not the thumbnail
  16. *
  17. */
  18. class ImageMap {
  19. static public $id = 0;
  20. const TOP_RIGHT = 0;
  21. const BOTTOM_RIGHT = 1;
  22. const BOTTOM_LEFT = 2;
  23. const TOP_LEFT = 3;
  24. const NONE = 4;
  25. static function render( $input, $params, $parser ) {
  26. global $wgScriptPath, $wgUser, $wgUrlProtocols, $wgNoFollowLinks;
  27. wfLoadExtensionMessages( 'ImageMap' );
  28. $lines = explode( "\n", $input );
  29. $first = true;
  30. $lineNum = 0;
  31. $mapHTML = '';
  32. $links = array();
  33. # Define canonical desc types to allow i18n of 'imagemap_desc_types'
  34. $descTypesCanonical = 'top-right, bottom-right, bottom-left, top-left, none';
  35. $descType = self::BOTTOM_RIGHT;
  36. $defaultLinkAttribs = false;
  37. $realmap = true;
  38. foreach ( $lines as $line ) {
  39. ++$lineNum;
  40. $externLink = false;
  41. $line = trim( $line );
  42. if ( $line == '' || $line[0] == '#' ) {
  43. continue;
  44. }
  45. if ( $first ) {
  46. $first = false;
  47. # The first line should have an image specification on it
  48. # Extract it and render the HTML
  49. $bits = explode( '|', $line, 2 );
  50. if ( count( $bits ) == 1 ) {
  51. $image = $bits[0];
  52. $options = '';
  53. } else {
  54. list( $image, $options ) = $bits;
  55. }
  56. $imageTitle = Title::newFromText( $image );
  57. if ( !$imageTitle || $imageTitle->getNamespace() != NS_IMAGE ) {
  58. return self::error( 'imagemap_no_image' );
  59. }
  60. if ( wfIsBadImage( $imageTitle->getDBkey() , $parser->mTitle ) ) {
  61. return self::error( 'imagemap_bad_image' );
  62. }
  63. // Parse the options so we can use links and the like in the caption
  64. $parsedOptions = $parser->recursiveTagParse( $options );
  65. $imageHTML = $parser->makeImage( $imageTitle, $parsedOptions );
  66. $parser->replaceLinkHolders( $imageHTML );
  67. $imageHTML = $parser->mStripState->unstripBoth( $imageHTML );
  68. $imageHTML = Sanitizer::normalizeCharReferences( $imageHTML );
  69. $parser->mOutput->addImage( $imageTitle->getDBkey() );
  70. $domDoc = new DOMDocument();
  71. wfSuppressWarnings();
  72. $ok = $domDoc->loadXML( $imageHTML );
  73. wfRestoreWarnings();
  74. if ( !$ok ) {
  75. return self::error( 'imagemap_invalid_image' );
  76. }
  77. $xpath = new DOMXPath( $domDoc );
  78. $imgs = $xpath->query( '//img' );
  79. if ( !$imgs->length ) {
  80. return self::error( 'imagemap_invalid_image' );
  81. }
  82. $imageNode = $imgs->item(0);
  83. $thumbWidth = $imageNode->getAttribute('width');
  84. $thumbHeight = $imageNode->getAttribute('height');
  85. if( function_exists( 'wfFindFile' ) ) {
  86. $imageObj = wfFindFile( $imageTitle );
  87. } else {
  88. // Old MW
  89. $imageObj = wfFindFile( $imageTitle );
  90. }
  91. if ( !$imageObj || !$imageObj->exists() ) {
  92. return self::error( 'imagemap_invalid_image' );
  93. }
  94. # Add the linear dimensions to avoid inaccuracy in the scale
  95. # factor when one is much larger than the other
  96. # (sx+sy)/(x+y) = s
  97. $denominator = $imageObj->getWidth() + $imageObj->getHeight();
  98. $numerator = $thumbWidth + $thumbHeight;
  99. if ( $denominator <= 0 || $numerator <= 0 ) {
  100. return self::error( 'imagemap_invalid_image' );
  101. }
  102. $scale = $numerator / $denominator;
  103. continue;
  104. }
  105. # Handle desc spec
  106. $cmd = strtok( $line, " \t" );
  107. if ( $cmd == 'desc' ) {
  108. $typesText = wfMsgForContent( 'imagemap_desc_types' );
  109. if ( $descTypesCanonical != $typesText ) {
  110. // i18n desc types exists
  111. $typesText = $descTypesCanonical . ', ' . $typesText;
  112. }
  113. $types = array_map( 'trim', explode( ',', $typesText ) );
  114. $type = trim( strtok( '' ) );
  115. $descType = array_search( $type, $types );
  116. if ( $descType > 4 ) {
  117. // A localized descType is used. Subtract 5 to reach the canonical desc type.
  118. $descType = $descType - 5;
  119. }
  120. if ( $descType === false || $descType < 0 ) { // <0? In theory never, but paranoia...
  121. return self::error( 'imagemap_invalid_desc', $typesText );
  122. }
  123. continue;
  124. }
  125. # Find the link
  126. $link = trim( strstr( $line, '[' ) );
  127. if ( preg_match( '/^ \[\[ ([^|]*+) \| ([^\]]*+) \]\] \w* $ /x', $link, $m ) ) {
  128. $title = Title::newFromText( $m[1] );
  129. $alt = trim( $m[2] );
  130. } elseif ( preg_match( '/^ \[\[ ([^\]]*+) \]\] \w* $ /x', $link, $m ) ) {
  131. $title = Title::newFromText( $m[1] );
  132. if (is_null($title))
  133. return self::error('imagemap_invalid_title', $lineNum);
  134. $alt = $title->getFullText();
  135. } elseif ( in_array( substr( $link , 1 , strpos($link, '//' )+1 ) , $wgUrlProtocols ) || in_array( substr( $link , 1 , strpos($link, ':' ) ) , $wgUrlProtocols ) ) {
  136. if ( preg_match( '/^ \[ ([^\s]*+) \s ([^\]]*+) \] \w* $ /x', $link, $m ) ) {
  137. $title = $m[1];
  138. $alt = trim( $m[2] );
  139. $externLink = true;
  140. } elseif ( preg_match( '/^ \[ ([^\]]*+) \] \w* $ /x', $link, $m ) ) {
  141. $title = $alt = trim( $m[1] );
  142. $externLink = true;
  143. }
  144. } else {
  145. return self::error( 'imagemap_no_link', $lineNum );
  146. }
  147. if ( !$title ) {
  148. return self::error( 'imagemap_invalid_title', $lineNum );
  149. }
  150. $shapeSpec = substr( $line, 0, -strlen( $link ) );
  151. # Tokenize shape spec
  152. $shape = strtok( $shapeSpec, " \t" );
  153. switch ( $shape ) {
  154. case 'default':
  155. $coords = array();
  156. break;
  157. case 'rect':
  158. $coords = self::tokenizeCoords( 4, $lineNum );
  159. if ( !is_array( $coords ) ) {
  160. return $coords;
  161. }
  162. break;
  163. case 'circle':
  164. $coords = self::tokenizeCoords( 3, $lineNum );
  165. if ( !is_array( $coords ) ) {
  166. return $coords;
  167. }
  168. break;
  169. case 'poly':
  170. $coords = array();
  171. $coord = strtok( " \t" );
  172. while ( $coord !== false ) {
  173. $coords[] = $coord;
  174. $coord = strtok( " \t" );
  175. }
  176. if ( !count( $coords ) ) {
  177. return self::error( 'imagemap_missing_coord', $lineNum );
  178. }
  179. if ( count( $coords ) % 2 !== 0 ) {
  180. return self::error( 'imagemap_poly_odd', $lineNum );
  181. }
  182. break;
  183. default:
  184. return self::error( 'imagemap_unrecognised_shape', $lineNum );
  185. }
  186. # Scale the coords using the size of the source image
  187. foreach ( $coords as $i => $c ) {
  188. $coords[$i] = intval( round( $c * $scale ) );
  189. }
  190. # Construct the area tag
  191. $attribs = array();
  192. if ( $externLink ) {
  193. $attribs['href'] = $title;
  194. $attribs['class'] = 'plainlinks';
  195. if ( $wgNoFollowLinks ) {
  196. $attribs['rel'] = 'nofollow';
  197. }
  198. } else if ( $title->getFragment() != '' && $title->getPrefixedDBkey() == '' ) {
  199. # XXX: kluge to handle [[#Fragment]] links, should really fix getLocalURL()
  200. # in Title.php to return an empty string in this case
  201. $attribs['href'] = $title->getFragmentForURL();
  202. } else {
  203. $attribs['href'] = $title->escapeLocalURL() . $title->getFragmentForURL();
  204. }
  205. if ( $shape != 'default' ) {
  206. $attribs['shape'] = $shape;
  207. }
  208. if ( $coords ) {
  209. $attribs['coords'] = implode( ',', $coords );
  210. }
  211. if ( $alt != '' ) {
  212. if ( $shape != 'default' ) {
  213. $attribs['alt'] = $alt;
  214. }
  215. $attribs['title'] = $alt;
  216. }
  217. if ( $shape == 'default' ) {
  218. $defaultLinkAttribs = $attribs;
  219. } else {
  220. $mapHTML .= Xml::element( 'area', $attribs ) . "\n";
  221. }
  222. if ( $externLink ) {
  223. $extLinks[] = $title;
  224. } else {
  225. $links[] = $title;
  226. }
  227. }
  228. if ( $first ) {
  229. return self::error( 'imagemap_no_image' );
  230. }
  231. if ( $mapHTML == '' && $defaultLinkAttribs == '' ) {
  232. return self::error( 'imagemap_no_areas' );
  233. } elseif ( $mapHTML == '' && $defaultLinkAttribs != '' ) {
  234. // no areas defined, default only. It's not a real imagemap, so we do not need some tags
  235. $realmap = false;
  236. }
  237. if ( $realmap ) {
  238. # Construct the map
  239. # Add random number to avoid breaking cached HTML fragments that are
  240. # later joined together on the one page (bug 16471)
  241. $mapName = "ImageMap_" . ++self::$id . '_' . mt_rand( 0, 0x7fffffff );
  242. $mapHTML = "<map name=\"$mapName\">\n$mapHTML</map>\n";
  243. # Alter the image tag
  244. $imageNode->setAttribute( 'usemap', "#$mapName" );
  245. }
  246. # Add a surrounding div, remove the default link to the description page
  247. $anchor = $imageNode->parentNode;
  248. $parent = $anchor->parentNode;
  249. $div = $parent->insertBefore( new DOMElement( 'div' ), $anchor );
  250. if ( $defaultLinkAttribs ) {
  251. $defaultAnchor = $div->appendChild( new DOMElement( 'a' ) );
  252. foreach ( $defaultLinkAttribs as $name => $value ) {
  253. $defaultAnchor->setAttribute( $name, $value );
  254. }
  255. $imageParent = $defaultAnchor;
  256. } else {
  257. $imageParent = $div;
  258. }
  259. # Add the map HTML to the div
  260. # We used to add it before the div, but that made tidy unhappy
  261. if ( $mapHTML != '' ) {
  262. $mapDoc = new DOMDocument();
  263. $mapDoc->loadXML( $mapHTML );
  264. $mapNode = $domDoc->importNode( $mapDoc->documentElement, true );
  265. $div->appendChild( $mapNode );
  266. }
  267. $imageParent->appendChild( $imageNode->cloneNode( true ) );
  268. $parent->removeChild( $anchor );
  269. # Determine whether a "magnify" link is present
  270. $xpath = new DOMXPath( $domDoc );
  271. $magnify = $xpath->query( '//div[@class="magnify"]' );
  272. if ( !$magnify->length && $descType != self::NONE ) {
  273. # Add image description link
  274. if ( $descType == self::TOP_LEFT || $descType == self::BOTTOM_LEFT ) {
  275. $marginLeft = 0;
  276. } else {
  277. $marginLeft = $thumbWidth - 20;
  278. }
  279. if ( $descType == self::TOP_LEFT || $descType == self::TOP_RIGHT ) {
  280. $marginTop = -$thumbHeight;
  281. // 1px hack for IE, to stop it poking out the top
  282. $marginTop += 1;
  283. } else {
  284. $marginTop = -20;
  285. }
  286. $div->setAttribute( 'style', "height: {$thumbHeight}px; width: {$thumbWidth}px; " );
  287. $descWrapper = $div->appendChild( new DOMElement( 'div' ) );
  288. $descWrapper->setAttribute( 'style',
  289. "margin-left: {$marginLeft}px; " .
  290. "margin-top: {$marginTop}px; " .
  291. "text-align: left;"
  292. );
  293. $descAnchor = $descWrapper->appendChild( new DOMElement( 'a' ) );
  294. $descAnchor->setAttribute( 'href', $imageTitle->escapeLocalURL() );
  295. $descAnchor->setAttribute( 'title', wfMsgForContent( 'imagemap_description' ) );
  296. $descImg = $descAnchor->appendChild( new DOMElement( 'img' ) );
  297. $descImg->setAttribute( 'alt', wfMsgForContent( 'imagemap_description' ) );
  298. $descImg->setAttribute( 'src', "$wgScriptPath/extensions/ImageMap/desc-20.png" );
  299. $descImg->setAttribute( 'style', 'border: none;' );
  300. }
  301. # Output the result
  302. # We use saveXML() not saveHTML() because then we get XHTML-compliant output.
  303. # The disadvantage is that we have to strip out the DTD
  304. $output = preg_replace( '/<\?xml[^?]*\?>/', '', $domDoc->saveXML() );
  305. # Register links
  306. foreach ( $links as $title ) {
  307. if( $title->isExternal() || $title->getNamespace() == NS_SPECIAL ) {
  308. // Don't register special or interwiki links...
  309. } elseif( $title->getNamespace() == NS_MEDIA ) {
  310. // Regular Media: links are recorded as image usages
  311. $parser->mOutput->addImage( $title->getDBkey() );
  312. } else {
  313. // Plain ol' link
  314. $parser->mOutput->addLink( $title );
  315. }
  316. }
  317. if ( isset( $extLinks ) ) {
  318. foreach ( $extLinks as $title ) {
  319. $parser->mOutput->addExternalLink( $title );
  320. }
  321. }
  322. # Armour output against broken parser
  323. $output = str_replace( "\n", '', $output );
  324. return $output;
  325. }
  326. static function tokenizeCoords( $count, $lineNum ) {
  327. $coords = array();
  328. for ( $i = 0; $i < $count; $i++ ) {
  329. $coord = strtok( " \t" );
  330. if ( $coord === false ) {
  331. return self::error( 'imagemap_missing_coord', $lineNum );
  332. }
  333. if ( !is_numeric( $coord ) || $coord > 1e9 || $coord < 0 ) {
  334. return self::error( 'imagemap_invalid_coord', $lineNum );
  335. }
  336. $coords[$i] = $coord;
  337. }
  338. return $coords;
  339. }
  340. static function error( $name, $line = false ) {
  341. return '<p class="error">' . wfMsgForContent( $name, $line ) . '</p>';
  342. }
  343. }