MediaWiki REL1_32
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 $lineNum = 0;
51 $mapHTML = '';
52 $links = [];
53
54 // Define canonical desc types to allow i18n of 'imagemap_desc_types'
55 $descTypesCanonical = 'top-right, bottom-right, bottom-left, top-left, none';
56 $descType = self::BOTTOM_RIGHT;
57 $defaultLinkAttribs = false;
58 $realmap = true;
59 $extLinks = [];
60 foreach ( $lines as $line ) {
61 ++$lineNum;
62 $externLink = false;
63
64 $line = trim( $line );
65 if ( $line == '' || $line[0] == '#' ) {
66 continue;
67 }
68
69 if ( $first ) {
70 $first = false;
71
72 // The first line should have an image specification on it
73 // Extract it and render the HTML
74 $bits = explode( '|', $line, 2 );
75 if ( count( $bits ) == 1 ) {
76 $image = $bits[0];
77 $options = '';
78 } else {
79 list( $image, $options ) = $bits;
80 }
81 $imageTitle = Title::newFromText( $image );
82 if ( !$imageTitle || !$imageTitle->inNamespace( NS_FILE ) ) {
83 return self::error( 'imagemap_no_image' );
84 }
85 if ( wfIsBadImage( $imageTitle->getDBkey(), $parser->mTitle ) ) {
86 return self::error( 'imagemap_bad_image' );
87 }
88 // Parse the options so we can use links and the like in the caption
89 $parsedOptions = $parser->recursiveTagParse( $options );
90 $imageHTML = $parser->makeImage( $imageTitle, $parsedOptions );
91 $parser->replaceLinkHolders( $imageHTML );
92 $imageHTML = $parser->mStripState->unstripBoth( $imageHTML );
93 $imageHTML = Sanitizer::normalizeCharReferences( $imageHTML );
94
95 $domDoc = new DOMDocument();
97 $ok = $domDoc->loadXML( $imageHTML );
99 if ( !$ok ) {
100 return self::error( 'imagemap_invalid_image' );
101 }
102 $xpath = new DOMXPath( $domDoc );
103 $imgs = $xpath->query( '//img' );
104 if ( !$imgs->length ) {
105 return self::error( 'imagemap_invalid_image' );
106 }
107 $imageNode = $imgs->item( 0 );
108 $thumbWidth = $imageNode->getAttribute( 'width' );
109 $thumbHeight = $imageNode->getAttribute( 'height' );
110
111 $imageObj = wfFindFile( $imageTitle );
112 if ( !$imageObj || !$imageObj->exists() ) {
113 return self::error( 'imagemap_invalid_image' );
114 }
115 // Add the linear dimensions to avoid inaccuracy in the scale
116 // factor when one is much larger than the other
117 // (sx+sy)/(x+y) = s
118 $denominator = $imageObj->getWidth() + $imageObj->getHeight();
119 $numerator = $thumbWidth + $thumbHeight;
120 if ( $denominator <= 0 || $numerator <= 0 ) {
121 return self::error( 'imagemap_invalid_image' );
122 }
123 $scale = $numerator / $denominator;
124 continue;
125 }
126
127 // Handle desc spec
128 $cmd = strtok( $line, " \t" );
129 if ( $cmd == 'desc' ) {
130 $typesText = wfMessage( 'imagemap_desc_types' )->inContentLanguage()->text();
131 if ( $descTypesCanonical != $typesText ) {
132 // i18n desc types exists
133 $typesText = $descTypesCanonical . ', ' . $typesText;
134 }
135 $types = array_map( 'trim', explode( ',', $typesText ) );
136 $type = trim( strtok( '' ) );
137 $descType = array_search( $type, $types );
138 if ( $descType > 4 ) {
139 // A localized descType is used. Subtract 5 to reach the canonical desc type.
140 $descType = $descType - 5;
141 }
142 // <0? In theory never, but paranoia...
143 if ( $descType === false || $descType < 0 ) {
144 return self::error( 'imagemap_invalid_desc', $typesText );
145 }
146 continue;
147 }
148
149 $title = false;
150 // Find the link
151 $link = trim( strstr( $line, '[' ) );
152 $m = [];
153 if ( preg_match( '/^ \[\[ ([^|]*+) \| ([^\]]*+) \]\] \w* $ /x', $link, $m ) ) {
154 $title = Title::newFromText( $m[1] );
155 $alt = trim( $m[2] );
156 } elseif ( preg_match( '/^ \[\[ ([^\]]*+) \]\] \w* $ /x', $link, $m ) ) {
157 $title = Title::newFromText( $m[1] );
158 if ( is_null( $title ) ) {
159 return self::error( 'imagemap_invalid_title', $lineNum );
160 }
161 $alt = $title->getFullText();
162 } elseif ( in_array( substr( $link, 1, strpos( $link, '//' ) + 1 ), $wgUrlProtocols )
163 || in_array( substr( $link, 1, strpos( $link, ':' ) ), $wgUrlProtocols )
164 ) {
165 if ( preg_match( '/^ \[ ([^\s]*+) \s ([^\]]*+) \] \w* $ /x', $link, $m ) ) {
166 $title = $m[1];
167 $alt = trim( $m[2] );
168 $externLink = true;
169 } elseif ( preg_match( '/^ \[ ([^\]]*+) \] \w* $ /x', $link, $m ) ) {
170 $title = $alt = trim( $m[1] );
171 $externLink = true;
172 }
173 } else {
174 return self::error( 'imagemap_no_link', $lineNum );
175 }
176 if ( !$title ) {
177 return self::error( 'imagemap_invalid_title', $lineNum );
178 }
179
180 $shapeSpec = substr( $line, 0, -strlen( $link ) );
181
182 // Tokenize shape spec
183 $shape = strtok( $shapeSpec, " \t" );
184 switch ( $shape ) {
185 case 'default':
186 $coords = [];
187 break;
188 case 'rect':
189 $coords = self::tokenizeCoords( 4, $lineNum );
190 if ( !is_array( $coords ) ) {
191 return $coords;
192 }
193 break;
194 case 'circle':
195 $coords = self::tokenizeCoords( 3, $lineNum );
196 if ( !is_array( $coords ) ) {
197 return $coords;
198 }
199 break;
200 case 'poly':
201 $coords = [];
202 $coord = strtok( " \t" );
203 while ( $coord !== false ) {
204 $coords[] = $coord;
205 $coord = strtok( " \t" );
206 }
207 if ( !count( $coords ) ) {
208 return self::error( 'imagemap_missing_coord', $lineNum );
209 }
210 if ( count( $coords ) % 2 !== 0 ) {
211 return self::error( 'imagemap_poly_odd', $lineNum );
212 }
213 break;
214 default:
215 return self::error( 'imagemap_unrecognised_shape', $lineNum );
216 }
217
218 // Scale the coords using the size of the source image
219 foreach ( $coords as $i => $c ) {
220 $coords[$i] = intval( round( $c * $scale ) );
221 }
222
223 // Construct the area tag
224 $attribs = [];
225 if ( $externLink ) {
226 $attribs['href'] = $title;
227 $attribs['class'] = 'plainlinks';
228 if ( $wgNoFollowLinks ) {
229 $attribs['rel'] = 'nofollow';
230 }
231 } elseif ( $title->getFragment() != '' && $title->getPrefixedDBkey() == '' ) {
232 // XXX: kluge to handle [[#Fragment]] links, should really fix getLocalURL()
233 // in Title.php to return an empty string in this case
234 $attribs['href'] = $title->getFragmentForURL();
235 } else {
236 $attribs['href'] = $title->getLocalURL() . $title->getFragmentForURL();
237 }
238 if ( $shape != 'default' ) {
239 $attribs['shape'] = $shape;
240 }
241 if ( $coords ) {
242 $attribs['coords'] = implode( ',', $coords );
243 }
244 if ( $alt != '' ) {
245 if ( $shape != 'default' ) {
246 $attribs['alt'] = $alt;
247 }
248 $attribs['title'] = $alt;
249 }
250 if ( $shape == 'default' ) {
251 $defaultLinkAttribs = $attribs;
252 } else {
253 $mapHTML .= Xml::element( 'area', $attribs ) . "\n";
254 }
255 if ( $externLink ) {
256 $extLinks[] = $title;
257 } else {
258 $links[] = $title;
259 }
260 }
261
262 if ( $first ) {
263 return self::error( 'imagemap_no_image' );
264 }
265
266 if ( $mapHTML == '' ) {
267 // no areas defined, default only. It's not a real imagemap, so we do not need some tags
268 $realmap = false;
269 }
270
271 if ( $realmap ) {
272 // Construct the map
273 // Add random number to avoid breaking cached HTML fragments that are
274 // later joined together on the one page (bug 16471)
275 $mapName = "ImageMap_" . ++self::$id . '_' . mt_rand( 0, 0x7fffffff );
276 $mapHTML = "<map name=\"$mapName\">\n$mapHTML</map>\n";
277
278 // Alter the image tag
279 $imageNode->setAttribute( 'usemap', "#$mapName" );
280 }
281
282 // Add a surrounding div, remove the default link to the description page
283 $anchor = $imageNode->parentNode;
284 $parent = $anchor->parentNode;
285 $div = $parent->insertBefore( new DOMElement( 'div' ), $anchor );
286 $div->setAttribute( 'class', 'noresize' );
287 if ( $defaultLinkAttribs ) {
288 $defaultAnchor = $div->appendChild( new DOMElement( 'a' ) );
289 foreach ( $defaultLinkAttribs as $name => $value ) {
290 $defaultAnchor->setAttribute( $name, $value );
291 }
292 $imageParent = $defaultAnchor;
293 } else {
294 $imageParent = $div;
295 }
296
297 // Add the map HTML to the div
298 // We used to add it before the div, but that made tidy unhappy
299 if ( $mapHTML != '' ) {
300 $mapDoc = new DOMDocument();
301 $mapDoc->loadXML( $mapHTML );
302 $mapNode = $domDoc->importNode( $mapDoc->documentElement, true );
303 $div->appendChild( $mapNode );
304 }
305
306 $imageParent->appendChild( $imageNode->cloneNode( true ) );
307 $parent->removeChild( $anchor );
308
309 // Determine whether a "magnify" link is present
310 $xpath = new DOMXPath( $domDoc );
311 $magnify = $xpath->query( '//div[@class="magnify"]' );
312 if ( !$magnify->length && $descType != self::NONE ) {
313 // Add image description link
314 if ( $descType == self::TOP_LEFT || $descType == self::BOTTOM_LEFT ) {
315 $marginLeft = 0;
316 } else {
317 $marginLeft = $thumbWidth - 20;
318 }
319 if ( $descType == self::TOP_LEFT || $descType == self::TOP_RIGHT ) {
320 $marginTop = -$thumbHeight;
321 // 1px hack for IE, to stop it poking out the top
322 $marginTop += 1;
323 } else {
324 $marginTop = -20;
325 }
326 $div->setAttribute( 'style', "height: {$thumbHeight}px; width: {$thumbWidth}px; " );
327 $descWrapper = $div->appendChild( new DOMElement( 'div' ) );
328 $descWrapper->setAttribute( 'style',
329 "margin-left: {$marginLeft}px; " .
330 "margin-top: {$marginTop}px; " .
331 "text-align: left;"
332 );
333
334 $descAnchor = $descWrapper->appendChild( new DOMElement( 'a' ) );
335 $descAnchor->setAttribute( 'href', $imageTitle->getLocalURL() );
336 $descAnchor->setAttribute(
337 'title',
338 wfMessage( 'imagemap_description' )->inContentLanguage()->text()
339 );
340 $descImg = $descAnchor->appendChild( new DOMElement( 'img' ) );
341 $descImg->setAttribute(
342 'alt',
343 wfMessage( 'imagemap_description' )->inContentLanguage()->text()
344 );
345 $url = $config->get( 'ExtensionAssetsPath' ) . '/ImageMap/desc-20.png';
346 $descImg->setAttribute(
347 'src',
348 OutputPage::transformResourcePath( $config, $url )
349 );
350 $descImg->setAttribute( 'style', 'border: none;' );
351 }
352
353 // Output the result
354 // We use saveXML() not saveHTML() because then we get XHTML-compliant output.
355 // The disadvantage is that we have to strip out the DTD
356 $output = preg_replace( '/<\?xml[^?]*\?>/', '', $domDoc->saveXML( null, LIBXML_NOEMPTYTAG ) );
357
358 // Register links
359 foreach ( $links as $title ) {
360 if ( $title->isExternal() || $title->getNamespace() == NS_SPECIAL ) {
361 // Don't register special or interwiki links...
362 } elseif ( $title->getNamespace() == NS_MEDIA ) {
363 // Regular Media: links are recorded as image usages
364 $parser->mOutput->addImage( $title->getDBkey() );
365 } else {
366 // Plain ol' link
367 $parser->mOutput->addLink( $title );
368 }
369 }
370 foreach ( $extLinks as $title ) {
371 $parser->mOutput->addExternalLink( $title );
372 }
373 // Armour output against broken parser
374 $output = str_replace( "\n", '', $output );
375 return $output;
376 }
377
383 static function tokenizeCoords( $count, $lineNum ) {
384 $coords = [];
385 for ( $i = 0; $i < $count; $i++ ) {
386 $coord = strtok( " \t" );
387 if ( $coord === false ) {
388 return self::error( 'imagemap_missing_coord', $lineNum );
389 }
390 if ( !is_numeric( $coord ) || $coord > 1e9 || $coord < 0 ) {
391 return self::error( 'imagemap_invalid_coord', $lineNum );
392 }
393 $coords[$i] = $coord;
394 }
395 return $coords;
396 }
397
403 static function error( $name, $line = false ) {
404 return '<p class="error">' . wfMessage( $name, $line )->parse() . '</p>';
405 }
406}
This list may contain false positives That usually means there is additional text with links below the first Each row contains links to the first and second as well as the first line of the second redirect text
$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().
wfRestoreWarnings()
wfFindFile( $title, $options=[])
Find a file.
wfSuppressWarnings( $end=false)
Reference-counted warning suppression.
wfIsBadImage( $name, $contextTitle=false, $blacklist=null)
Determine if an image exists on the 'bad image list'.
$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:403
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:383
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:68
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
see documentation in includes Linker php for Linker::makeImageLink or false for current used if you return false $parser
Definition hooks.txt:1873
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check $image
Definition hooks.txt:925
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults error
Definition hooks.txt:2683
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:2050
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:994
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt;div ...>$1&lt;/div>"). - flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException':Called before an exception(or PHP error) is logged. This is meant for integration with external error aggregation services
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition hooks.txt:3106
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing & $attribs
Definition hooks.txt:2063
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title e g db for database replication lag or jobqueue for job queue size converted to pseudo seconds It is possible to add more fields and they will be returned to the user in the API response after the basic globals have been set but before ordinary actions take place $output
Definition hooks.txt:2317
const NS_FILE
Definition Defines.php:70
const NS_SPECIAL
Definition Defines.php:53
const NS_MEDIA
Definition Defines.php:52
$parent
if(is_array($mode)) switch( $mode) $input
$lines
Definition router.php:61
$params