]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - extensions/ImageMap/ImageMap_body.php
MediaWiki 1.30.2 renames
[autoinstalls/mediawiki.git] / extensions / ImageMap / ImageMap_body.php
1 <?php
2 /**
3  * Main file for extension ImageMap.
4  *
5  * @file
6  * @ingroup Extensions
7  *
8  * Syntax:
9  * <imagemap>
10  * Image:Foo.jpg | 100px | picture of a foo
11  *
12  * rect    0  0  50 50  [[Foo type A]]
13  * circle  50 50 20     [[Foo type B]]
14  *
15  * desc bottom-left
16  * </imagemap>
17  *
18  * Coordinates are relative to the source image, not the thumbnail.
19  */
20
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
30         /**
31          * @param Parser $parser
32          */
33         public static function onParserFirstCallInit( Parser &$parser ) {
34                 $parser->setHook( 'imagemap', [ 'ImageMap', 'render' ] );
35         }
36
37         /**
38          * @param $input
39          * @param $params
40          * @param $parser Parser
41          * @return string HTML (Image map, or error message)
42          */
43         public static function render( $input, $params, $parser ) {
44                 global $wgUrlProtocols, $wgNoFollowLinks;
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();
96                                 wfSuppressWarnings();
97                                 $ok = $domDoc->loadXML( $imageHTML );
98                                 wfRestoreWarnings();
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
378         /**
379          * @param $count int
380          * @param $lineNum int|string
381          * @return array|string String with error (HTML), or array of coordinates
382          */
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
398         /**
399          * @param $name string
400          * @param $line string|int|bool
401          * @return string HTML
402          */
403         static function error( $name, $line = false ) {
404                 return '<p class="error">' . wfMessage( $name, $line )->text() . '</p>';
405         }
406 }