This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * Extraction of SVG image metadata. |
||
4 | * |
||
5 | * This program is free software; you can redistribute it and/or modify |
||
6 | * it under the terms of the GNU General Public License as published by |
||
7 | * the Free Software Foundation; either version 2 of the License, or |
||
8 | * (at your option) any later version. |
||
9 | * |
||
10 | * This program is distributed in the hope that it will be useful, |
||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
13 | * GNU General Public License for more details. |
||
14 | * |
||
15 | * You should have received a copy of the GNU General Public License along |
||
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
||
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
18 | * http://www.gnu.org/copyleft/gpl.html |
||
19 | * |
||
20 | * @file |
||
21 | * @ingroup Media |
||
22 | * @author "Derk-Jan Hartman <hartman _at_ videolan d0t org>" |
||
23 | * @author Brion Vibber |
||
24 | * @copyright Copyright © 2010-2010 Brion Vibber, Derk-Jan Hartman |
||
25 | * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License |
||
26 | */ |
||
27 | |||
28 | /** |
||
29 | * @ingroup Media |
||
30 | */ |
||
31 | class SVGMetadataExtractor { |
||
32 | static function getMetadata( $filename ) { |
||
33 | $svg = new SVGReader( $filename ); |
||
34 | |||
35 | return $svg->getMetadata(); |
||
36 | } |
||
37 | } |
||
38 | |||
39 | /** |
||
40 | * @ingroup Media |
||
41 | */ |
||
42 | class SVGReader { |
||
43 | const DEFAULT_WIDTH = 512; |
||
44 | const DEFAULT_HEIGHT = 512; |
||
45 | const NS_SVG = 'http://www.w3.org/2000/svg'; |
||
46 | const LANG_PREFIX_MATCH = 1; |
||
47 | const LANG_FULL_MATCH = 2; |
||
48 | |||
49 | /** @var null|XMLReader */ |
||
50 | private $reader = null; |
||
51 | |||
52 | /** @var bool */ |
||
53 | private $mDebug = false; |
||
54 | |||
55 | /** @var array */ |
||
56 | private $metadata = []; |
||
57 | private $languages = []; |
||
58 | private $languagePrefixes = []; |
||
59 | |||
60 | /** |
||
61 | * Constructor |
||
62 | * |
||
63 | * Creates an SVGReader drawing from the source provided |
||
64 | * @param string $source URI from which to read |
||
65 | * @throws MWException|Exception |
||
66 | */ |
||
67 | function __construct( $source ) { |
||
68 | global $wgSVGMetadataCutoff; |
||
69 | $this->reader = new XMLReader(); |
||
70 | |||
71 | // Don't use $file->getSize() since file object passed to SVGHandler::getMetadata is bogus. |
||
72 | $size = filesize( $source ); |
||
73 | if ( $size === false ) { |
||
74 | throw new MWException( "Error getting filesize of SVG." ); |
||
75 | } |
||
76 | |||
77 | if ( $size > $wgSVGMetadataCutoff ) { |
||
78 | $this->debug( "SVG is $size bytes, which is bigger than $wgSVGMetadataCutoff. Truncating." ); |
||
79 | $contents = file_get_contents( $source, false, null, -1, $wgSVGMetadataCutoff ); |
||
80 | if ( $contents === false ) { |
||
81 | throw new MWException( 'Error reading SVG file.' ); |
||
82 | } |
||
83 | $this->reader->XML( $contents, null, LIBXML_NOERROR | LIBXML_NOWARNING ); |
||
84 | } else { |
||
85 | $this->reader->open( $source, null, LIBXML_NOERROR | LIBXML_NOWARNING ); |
||
86 | } |
||
87 | |||
88 | // Expand entities, since Adobe Illustrator uses them for xmlns |
||
89 | // attributes (bug 31719). Note that libxml2 has some protection |
||
90 | // against large recursive entity expansions so this is not as |
||
91 | // insecure as it might appear to be. However, it is still extremely |
||
92 | // insecure. It's necessary to wrap any read() calls with |
||
93 | // libxml_disable_entity_loader() to avoid arbitrary local file |
||
94 | // inclusion, or even arbitrary code execution if the expect |
||
95 | // extension is installed (bug 46859). |
||
96 | $oldDisable = libxml_disable_entity_loader( true ); |
||
97 | $this->reader->setParserProperty( XMLReader::SUBST_ENTITIES, true ); |
||
98 | |||
99 | $this->metadata['width'] = self::DEFAULT_WIDTH; |
||
100 | $this->metadata['height'] = self::DEFAULT_HEIGHT; |
||
101 | |||
102 | // The size in the units specified by the SVG file |
||
103 | // (for the metadata box) |
||
104 | // Per the SVG spec, if unspecified, default to '100%' |
||
105 | $this->metadata['originalWidth'] = '100%'; |
||
106 | $this->metadata['originalHeight'] = '100%'; |
||
107 | |||
108 | // Because we cut off the end of the svg making an invalid one. Complicated |
||
109 | // try catch thing to make sure warnings get restored. Seems like there should |
||
110 | // be a better way. |
||
111 | MediaWiki\suppressWarnings(); |
||
112 | try { |
||
113 | $this->read(); |
||
114 | } catch ( Exception $e ) { |
||
115 | // Note, if this happens, the width/height will be taken to be 0x0. |
||
116 | // Should we consider it the default 512x512 instead? |
||
117 | MediaWiki\restoreWarnings(); |
||
118 | libxml_disable_entity_loader( $oldDisable ); |
||
119 | throw $e; |
||
120 | } |
||
121 | MediaWiki\restoreWarnings(); |
||
122 | libxml_disable_entity_loader( $oldDisable ); |
||
123 | } |
||
124 | |||
125 | /** |
||
126 | * @return array Array with the known metadata |
||
127 | */ |
||
128 | public function getMetadata() { |
||
129 | return $this->metadata; |
||
130 | } |
||
131 | |||
132 | /** |
||
133 | * Read the SVG |
||
134 | * @throws MWException |
||
135 | * @return bool |
||
136 | */ |
||
137 | protected function read() { |
||
138 | $keepReading = $this->reader->read(); |
||
139 | |||
140 | /* Skip until first element */ |
||
141 | while ( $keepReading && $this->reader->nodeType != XMLReader::ELEMENT ) { |
||
142 | $keepReading = $this->reader->read(); |
||
143 | } |
||
144 | |||
145 | if ( $this->reader->localName != 'svg' || $this->reader->namespaceURI != self::NS_SVG ) { |
||
146 | throw new MWException( "Expected <svg> tag, got " . |
||
147 | $this->reader->localName . " in NS " . $this->reader->namespaceURI ); |
||
148 | } |
||
149 | $this->debug( "<svg> tag is correct." ); |
||
150 | $this->handleSVGAttribs(); |
||
151 | |||
152 | $exitDepth = $this->reader->depth; |
||
153 | $keepReading = $this->reader->read(); |
||
154 | while ( $keepReading ) { |
||
155 | $tag = $this->reader->localName; |
||
156 | $type = $this->reader->nodeType; |
||
157 | $isSVG = ( $this->reader->namespaceURI == self::NS_SVG ); |
||
158 | |||
159 | $this->debug( "$tag" ); |
||
160 | |||
161 | if ( $isSVG && $tag == 'svg' && $type == XMLReader::END_ELEMENT |
||
162 | && $this->reader->depth <= $exitDepth |
||
163 | ) { |
||
164 | break; |
||
165 | } elseif ( $isSVG && $tag == 'title' ) { |
||
166 | $this->readField( $tag, 'title' ); |
||
167 | } elseif ( $isSVG && $tag == 'desc' ) { |
||
168 | $this->readField( $tag, 'description' ); |
||
169 | } elseif ( $isSVG && $tag == 'metadata' && $type == XMLReader::ELEMENT ) { |
||
170 | $this->readXml( $tag, 'metadata' ); |
||
171 | } elseif ( $isSVG && $tag == 'script' ) { |
||
172 | // We normally do not allow scripted svgs. |
||
173 | // However its possible to configure MW to let them |
||
174 | // in, and such files should be considered animated. |
||
175 | $this->metadata['animated'] = true; |
||
176 | } elseif ( $tag !== '#text' ) { |
||
177 | $this->debug( "Unhandled top-level XML tag $tag" ); |
||
178 | |||
179 | // Recurse into children of current tag, looking for animation and languages. |
||
180 | $this->animateFilterAndLang( $tag ); |
||
181 | } |
||
182 | |||
183 | // Goto next element, which is sibling of current (Skip children). |
||
184 | $keepReading = $this->reader->next(); |
||
185 | } |
||
186 | |||
187 | $this->reader->close(); |
||
188 | |||
189 | $this->metadata['translations'] = $this->languages + $this->languagePrefixes; |
||
190 | |||
191 | return true; |
||
192 | } |
||
193 | |||
194 | /** |
||
195 | * Read a textelement from an element |
||
196 | * |
||
197 | * @param string $name Name of the element that we are reading from |
||
198 | * @param string $metafield Field that we will fill with the result |
||
199 | */ |
||
200 | private function readField( $name, $metafield = null ) { |
||
201 | $this->debug( "Read field $metafield" ); |
||
202 | if ( !$metafield || $this->reader->nodeType != XMLReader::ELEMENT ) { |
||
0 ignored issues
–
show
|
|||
203 | return; |
||
204 | } |
||
205 | $keepReading = $this->reader->read(); |
||
206 | while ( $keepReading ) { |
||
207 | if ( $this->reader->localName == $name |
||
208 | && $this->reader->namespaceURI == self::NS_SVG |
||
209 | && $this->reader->nodeType == XMLReader::END_ELEMENT |
||
210 | ) { |
||
211 | break; |
||
212 | } elseif ( $this->reader->nodeType == XMLReader::TEXT ) { |
||
213 | $this->metadata[$metafield] = trim( $this->reader->value ); |
||
214 | } |
||
215 | $keepReading = $this->reader->read(); |
||
216 | } |
||
217 | } |
||
218 | |||
219 | /** |
||
220 | * Read an XML snippet from an element |
||
221 | * |
||
222 | * @param string $metafield Field that we will fill with the result |
||
223 | * @throws MWException |
||
224 | */ |
||
225 | private function readXml( $metafield = null ) { |
||
226 | $this->debug( "Read top level metadata" ); |
||
227 | if ( !$metafield || $this->reader->nodeType != XMLReader::ELEMENT ) { |
||
0 ignored issues
–
show
The expression
$metafield of type string|null is loosely compared to false ; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.
In PHP, under loose comparison (like For '' == false // true
'' == null // true
'ab' == false // false
'ab' == null // false
// It is often better to use strict comparison
'' === false // false
'' === null // false
![]() |
|||
228 | return; |
||
229 | } |
||
230 | // @todo Find and store type of xml snippet. metadata['metadataType'] = "rdf" |
||
231 | if ( method_exists( $this->reader, 'readInnerXML' ) ) { |
||
232 | $this->metadata[$metafield] = trim( $this->reader->readInnerXml() ); |
||
233 | } else { |
||
234 | throw new MWException( "The PHP XMLReader extension does not come " . |
||
235 | "with readInnerXML() method. Your libxml is probably out of " . |
||
236 | "date (need 2.6.20 or later)." ); |
||
237 | } |
||
238 | $this->reader->next(); |
||
239 | } |
||
240 | |||
241 | /** |
||
242 | * Filter all children, looking for animated elements. |
||
243 | * Also get a list of languages that can be targeted. |
||
244 | * |
||
245 | * @param string $name Name of the element that we are reading from |
||
246 | */ |
||
247 | private function animateFilterAndLang( $name ) { |
||
248 | $this->debug( "animate filter for tag $name" ); |
||
249 | if ( $this->reader->nodeType != XMLReader::ELEMENT ) { |
||
250 | return; |
||
251 | } |
||
252 | if ( $this->reader->isEmptyElement ) { |
||
253 | return; |
||
254 | } |
||
255 | $exitDepth = $this->reader->depth; |
||
256 | $keepReading = $this->reader->read(); |
||
257 | while ( $keepReading ) { |
||
258 | if ( $this->reader->localName == $name && $this->reader->depth <= $exitDepth |
||
259 | && $this->reader->nodeType == XMLReader::END_ELEMENT |
||
260 | ) { |
||
261 | break; |
||
262 | } elseif ( $this->reader->namespaceURI == self::NS_SVG |
||
263 | && $this->reader->nodeType == XMLReader::ELEMENT |
||
264 | ) { |
||
265 | $sysLang = $this->reader->getAttribute( 'systemLanguage' ); |
||
266 | if ( !is_null( $sysLang ) && $sysLang !== '' ) { |
||
267 | // See https://www.w3.org/TR/SVG/struct.html#SystemLanguageAttribute |
||
268 | $langList = explode( ',', $sysLang ); |
||
269 | foreach ( $langList as $langItem ) { |
||
270 | $langItem = trim( $langItem ); |
||
271 | if ( Language::isWellFormedLanguageTag( $langItem ) ) { |
||
272 | $this->languages[$langItem] = self::LANG_FULL_MATCH; |
||
273 | } |
||
274 | // Note, the standard says that any prefix should work, |
||
275 | // here we do only the initial prefix, since that will catch |
||
276 | // 99% of cases, and we are going to compare against fallbacks. |
||
277 | // This differs mildly from how the spec says languages should be |
||
278 | // handled, however it matches better how the MediaWiki language |
||
279 | // preference is generally handled. |
||
280 | $dash = strpos( $langItem, '-' ); |
||
281 | // Intentionally checking both !false and > 0 at the same time. |
||
282 | if ( $dash ) { |
||
283 | $itemPrefix = substr( $langItem, 0, $dash ); |
||
284 | if ( Language::isWellFormedLanguageTag( $itemPrefix ) ) { |
||
285 | $this->languagePrefixes[$itemPrefix] = self::LANG_PREFIX_MATCH; |
||
286 | } |
||
287 | } |
||
288 | } |
||
289 | } |
||
290 | switch ( $this->reader->localName ) { |
||
291 | case 'script': |
||
292 | // Normally we disallow files with |
||
293 | // <script>, but its possible |
||
294 | // to configure MW to disable |
||
295 | // such checks. |
||
296 | case 'animate': |
||
297 | case 'set': |
||
298 | case 'animateMotion': |
||
299 | case 'animateColor': |
||
300 | case 'animateTransform': |
||
301 | $this->debug( "HOUSTON WE HAVE ANIMATION" ); |
||
302 | $this->metadata['animated'] = true; |
||
303 | break; |
||
304 | } |
||
305 | } |
||
306 | $keepReading = $this->reader->read(); |
||
307 | } |
||
308 | } |
||
309 | |||
310 | // @todo FIXME: Unused, remove? |
||
311 | private function throwXmlError( $err ) { |
||
0 ignored issues
–
show
|
|||
312 | $this->debug( "FAILURE: $err" ); |
||
313 | wfDebug( "SVGReader XML error: $err\n" ); |
||
314 | } |
||
315 | |||
316 | private function debug( $data ) { |
||
317 | if ( $this->mDebug ) { |
||
318 | wfDebug( "SVGReader: $data\n" ); |
||
319 | } |
||
320 | } |
||
321 | |||
322 | /** |
||
323 | * Parse the attributes of an SVG element |
||
324 | * |
||
325 | * The parser has to be in the start element of "<svg>" |
||
326 | */ |
||
327 | private function handleSVGAttribs() { |
||
328 | $defaultWidth = self::DEFAULT_WIDTH; |
||
329 | $defaultHeight = self::DEFAULT_HEIGHT; |
||
330 | $aspect = 1.0; |
||
331 | $width = null; |
||
332 | $height = null; |
||
333 | |||
334 | if ( $this->reader->getAttribute( 'viewBox' ) ) { |
||
335 | // min-x min-y width height |
||
336 | $viewBox = preg_split( '/\s+/', trim( $this->reader->getAttribute( 'viewBox' ) ) ); |
||
337 | if ( count( $viewBox ) == 4 ) { |
||
338 | $viewWidth = $this->scaleSVGUnit( $viewBox[2] ); |
||
339 | $viewHeight = $this->scaleSVGUnit( $viewBox[3] ); |
||
340 | if ( $viewWidth > 0 && $viewHeight > 0 ) { |
||
341 | $aspect = $viewWidth / $viewHeight; |
||
342 | $defaultHeight = $defaultWidth / $aspect; |
||
343 | } |
||
344 | } |
||
345 | } |
||
346 | View Code Duplication | if ( $this->reader->getAttribute( 'width' ) ) { |
|
347 | $width = $this->scaleSVGUnit( $this->reader->getAttribute( 'width' ), $defaultWidth ); |
||
348 | $this->metadata['originalWidth'] = $this->reader->getAttribute( 'width' ); |
||
349 | } |
||
350 | View Code Duplication | if ( $this->reader->getAttribute( 'height' ) ) { |
|
351 | $height = $this->scaleSVGUnit( $this->reader->getAttribute( 'height' ), $defaultHeight ); |
||
352 | $this->metadata['originalHeight'] = $this->reader->getAttribute( 'height' ); |
||
353 | } |
||
354 | |||
355 | if ( !isset( $width ) && !isset( $height ) ) { |
||
356 | $width = $defaultWidth; |
||
357 | $height = $width / $aspect; |
||
358 | } elseif ( isset( $width ) && !isset( $height ) ) { |
||
359 | $height = $width / $aspect; |
||
360 | } elseif ( isset( $height ) && !isset( $width ) ) { |
||
361 | $width = $height * $aspect; |
||
362 | } |
||
363 | |||
364 | if ( $width > 0 && $height > 0 ) { |
||
365 | $this->metadata['width'] = intval( round( $width ) ); |
||
366 | $this->metadata['height'] = intval( round( $height ) ); |
||
367 | } |
||
368 | } |
||
369 | |||
370 | /** |
||
371 | * Return a rounded pixel equivalent for a labeled CSS/SVG length. |
||
372 | * https://www.w3.org/TR/SVG11/coords.html#Units |
||
373 | * |
||
374 | * @param string $length CSS/SVG length. |
||
375 | * @param float|int $viewportSize Optional scale for percentage units... |
||
376 | * @return float Length in pixels |
||
377 | */ |
||
378 | static function scaleSVGUnit( $length, $viewportSize = 512 ) { |
||
379 | static $unitLength = [ |
||
380 | 'px' => 1.0, |
||
381 | 'pt' => 1.25, |
||
382 | 'pc' => 15.0, |
||
383 | 'mm' => 3.543307, |
||
384 | 'cm' => 35.43307, |
||
385 | 'in' => 90.0, |
||
386 | 'em' => 16.0, // fake it? |
||
387 | 'ex' => 12.0, // fake it? |
||
388 | '' => 1.0, // "User units" pixels by default |
||
389 | ]; |
||
390 | $matches = []; |
||
391 | if ( preg_match( '/^\s*(\d+(?:\.\d+)?)(em|ex|px|pt|pc|cm|mm|in|%|)\s*$/', $length, $matches ) ) { |
||
392 | $length = floatval( $matches[1] ); |
||
393 | $unit = $matches[2]; |
||
394 | if ( $unit == '%' ) { |
||
395 | return $length * 0.01 * $viewportSize; |
||
396 | } else { |
||
397 | return $length * $unitLength[$unit]; |
||
398 | } |
||
399 | } else { |
||
400 | // Assume pixels |
||
401 | return floatval( $length ); |
||
402 | } |
||
403 | } |
||
404 | } |
||
405 |
In PHP, under loose comparison (like
==
, or!=
, orswitch
conditions), values of different types might be equal.For
string
values, the empty string''
is a special case, in particular the following results might be unexpected: