PluginProbe
Elementor Website Builder – more than just a page builder / 3.2.0
Elementor Website Builder – more than just a page builder v3.2.0
4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 4.0.7 All 451 releases
elementor / core / files / assets / svg / svg-handler.php

svg-handler.php in Elementor Website Builder – more than just a page builder 3.2.0, at core/files/assets/svg/svg-handler.php

710 lines 15.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Elementor\Core\Files\Assets\Svg;
3
4 use Elementor\Core\Files\Assets\Files_Upload_Handler;
5
6 if ( ! defined( 'ABSPATH' ) ) {
7 exit; // Exit if accessed directly.
8 }
9
10 class Svg_Handler extends Files_Upload_Handler {
11 /**
12 * Inline svg attachment meta key
13 */
14 const META_KEY = '_elementor_inline_svg';
15
16 const SCRIPT_REGEX = '/(?:\w+script|data):/xi';
17
18 /**
19 * @var \DOMDocument
20 */
21 private $svg_dom = null;
22
23 /**
24 * Attachment ID.
25 *
26 * Holds the current attachment ID.
27 *
28 * @var int
29 */
30 private $attachment_id;
31
32 public static function get_name() {
33 return 'svg-handler';
34 }
35
36 /**
37 * get_meta
38 * @return mixed
39 */
40 protected function get_meta() {
41 return get_post_meta( $this->attachment_id, self::META_KEY, true );
42 }
43
44 /**
45 * update_meta
46 * @param $meta
47 */
48 protected function update_meta( $meta ) {
49 update_post_meta( $this->attachment_id, self::META_KEY, $meta );
50 }
51
52 /**
53 * delete_meta
54 */
55 protected function delete_meta() {
56 delete_post_meta( $this->attachment_id, self::META_KEY );
57 }
58
59 public function get_mime_type() {
60 return 'image/svg+xml';
61 }
62
63 public function get_file_type() {
64 return 'svg';
65 }
66
67 /**
68 * delete_meta_cache
69 */
70 public function delete_meta_cache() {
71 delete_post_meta_by_key( self::META_KEY );
72 }
73
74 /**
75 * read_from_file
76 * @return bool|string
77 */
78 public function read_from_file() {
79 return file_get_contents( get_attached_file( $this->attachment_id ) );
80 }
81
82 /**
83 * get_inline_svg
84 * @param $attachment_id
85 *
86 * @return bool|mixed|string
87 */
88 public static function get_inline_svg( $attachment_id ) {
89 $svg = get_post_meta( $attachment_id, self::META_KEY, true );
90
91 if ( ! empty( $svg ) ) {
92 return $svg;
93 }
94
95 $attachment_file = get_attached_file( $attachment_id );
96
97 if ( ! $attachment_file ) {
98 return '';
99 }
100
101 $svg = file_get_contents( $attachment_file );
102
103 if ( ! empty( $svg ) ) {
104 update_post_meta( $attachment_id, self::META_KEY, $svg );
105 }
106
107 return $svg;
108 }
109
110 /**
111 * decode_svg
112 * @param $content
113 *
114 * @return string
115 */
116 private function decode_svg( $content ) {
117 return gzdecode( $content );
118 }
119
120 /**
121 * encode_svg
122 * @param $content
123 *
124 * @return string
125 */
126 private function encode_svg( $content ) {
127 return gzencode( $content );
128 }
129
130 /**
131 * sanitize_svg
132 * @param $filename
133 *
134 * @return bool
135 */
136 public function sanitize_svg( $filename ) {
137 $original_content = file_get_contents( $filename );
138 $is_encoded = $this->is_encoded( $original_content );
139
140 if ( $is_encoded ) {
141 $decoded = $this->decode_svg( $original_content );
142 if ( false === $decoded ) {
143 return false;
144 }
145 $original_content = $decoded;
146 }
147
148 $valid_svg = $this->sanitizer( $original_content );
149
150 if ( false === $valid_svg ) {
151 return false;
152 }
153
154 // If we were gzipped, we need to re-zip
155 if ( $is_encoded ) {
156 $valid_svg = $this->encode_svg( $valid_svg );
157 }
158 file_put_contents( $filename, $valid_svg );
159
160 return true;
161 }
162
163 /**
164 * Check if the contents are gzipped
165 * @see http://www.gzip.org/zlib/rfc-gzip.html#member-format
166 *
167 * @param $contents
168 * @return bool
169 */
170 private function is_encoded( $contents ) {
171 $needle = "\x1f\x8b\x08";
172 if ( function_exists( 'mb_strpos' ) ) {
173 return 0 === mb_strpos( $contents, $needle );
174 } else {
175 return 0 === strpos( $contents, $needle );
176 }
177 }
178
179 /**
180 * is_allowed_tag
181 * @param $element
182 *
183 * @return bool
184 */
185 private function is_allowed_tag( $element ) {
186 static $allowed_tags = false;
187 if ( false === $allowed_tags ) {
188 $allowed_tags = $this->get_allowed_elements();
189 }
190
191 $tag_name = $element->tagName; // phpcs:ignore -- php DomDocument
192
193 if ( ! in_array( strtolower( $tag_name ), $allowed_tags ) ) {
194 $this->remove_element( $element );
195 return false;
196 }
197
198 return true;
199 }
200
201 private function remove_element( $element ) {
202 $element->parentNode->removeChild( $element ); // phpcs:ignore -- php DomDocument
203 }
204
205 /**
206 * is_a_attribute
207 * @param $name
208 * @param $check
209 *
210 * @return bool
211 */
212 private function is_a_attribute( $name, $check ) {
213 return 0 === strpos( $name, $check . '-' );
214 }
215
216 /**
217 * is_remote_value
218 * @param $value
219 *
220 * @return string
221 */
222 private function is_remote_value( $value ) {
223 $value = trim( preg_replace( '/[^ -~]/xu', '', $value ) );
224 $wrapped_in_url = preg_match( '~^url\(\s*[\'"]\s*(.*)\s*[\'"]\s*\)$~xi', $value, $match );
225 if ( ! $wrapped_in_url ) {
226 return false;
227 }
228
229 $value = trim( $match[1], '\'"' );
230 return preg_match( '~^((https?|ftp|file):)?//~xi', $value );
231 }
232
233 /**
234 * has_js_value
235 * @param $value
236 *
237 * @return false|int
238 */
239 private function has_js_value( $value ) {
240 return preg_match( '/base64|data|(?:java)?script|alert\(|window\.|document/i', $value );
241 }
242
243 /**
244 * get_allowed_attributes
245 * @return array
246 */
247 private function get_allowed_attributes() {
248 $allowed_attributes = [
249 'class',
250 'clip-path',
251 'clip-rule',
252 'fill',
253 'fill-opacity',
254 'fill-rule',
255 'filter',
256 'id',
257 'mask',
258 'opacity',
259 'stroke',
260 'stroke-dasharray',
261 'stroke-dashoffset',
262 'stroke-linecap',
263 'stroke-linejoin',
264 'stroke-miterlimit',
265 'stroke-opacity',
266 'stroke-width',
267 'style',
268 'systemlanguage',
269 'transform',
270 'href',
271 'xlink:href',
272 'xlink:title',
273 'cx',
274 'cy',
275 'r',
276 'requiredfeatures',
277 'clippathunits',
278 'type',
279 'rx',
280 'ry',
281 'color-interpolation-filters',
282 'stddeviation',
283 'filterres',
284 'filterunits',
285 'height',
286 'primitiveunits',
287 'width',
288 'x',
289 'y',
290 'font-size',
291 'display',
292 'font-family',
293 'font-style',
294 'font-weight',
295 'text-anchor',
296 'marker-end',
297 'marker-mid',
298 'marker-start',
299 'x1',
300 'x2',
301 'y1',
302 'y2',
303 'gradienttransform',
304 'gradientunits',
305 'spreadmethod',
306 'markerheight',
307 'markerunits',
308 'markerwidth',
309 'orient',
310 'preserveaspectratio',
311 'refx',
312 'refy',
313 'viewbox',
314 'maskcontentunits',
315 'maskunits',
316 'd',
317 'patterncontentunits',
318 'patterntransform',
319 'patternunits',
320 'points',
321 'fx',
322 'fy',
323 'offset',
324 'stop-color',
325 'stop-opacity',
326 'xmlns',
327 'xmlns:se',
328 'xmlns:xlink',
329 'xml:space',
330 'method',
331 'spacing',
332 'startoffset',
333 'dx',
334 'dy',
335 'rotate',
336 'textlength',
337 ];
338
339 return apply_filters( 'elementor/files/svg/allowed_attributes', $allowed_attributes );
340 }
341
342 /**
343 * get_allowed_elements
344 * @return array
345 */
346 private function get_allowed_elements() {
347 $allowed_elements = [
348 'a',
349 'circle',
350 'clippath',
351 'defs',
352 'style',
353 'desc',
354 'ellipse',
355 'fegaussianblur',
356 'filter',
357 'foreignobject',
358 'g',
359 'image',
360 'line',
361 'lineargradient',
362 'marker',
363 'mask',
364 'metadata',
365 'path',
366 'pattern',
367 'polygon',
368 'polyline',
369 'radialgradient',
370 'rect',
371 'stop',
372 'svg',
373 'switch',
374 'symbol',
375 'text',
376 'textpath',
377 'title',
378 'tspan',
379 'use',
380 ];
381 return apply_filters( 'elementor/files/svg/allowed_elements', $allowed_elements );
382 }
383
384 /**
385 * validate_allowed_attributes
386 * @param \DOMElement $element
387 */
388 private function validate_allowed_attributes( $element ) {
389 static $allowed_attributes = false;
390 if ( false === $allowed_attributes ) {
391 $allowed_attributes = $this->get_allowed_attributes();
392 }
393
394 for ( $index = $element->attributes->length - 1; $index >= 0; $index-- ) {
395 // get attribute name
396 $attr_name = $element->attributes->item( $index )->name;
397 $attr_name_lowercase = strtolower( $attr_name );
398 // Remove attribute if not in whitelist
399 if ( ! in_array( $attr_name_lowercase, $allowed_attributes ) && ! $this->is_a_attribute( $attr_name_lowercase, 'aria' ) && ! $this->is_a_attribute( $attr_name_lowercase, 'data' ) ) {
400 $element->removeAttribute( $attr_name );
401 continue;
402 }
403
404 $attr_value = $element->attributes->item( $index )->value;
405
406 // Remove attribute if it has a remote reference or js or data-URI/base64
407 if ( ! empty( $attr_value ) && ( $this->is_remote_value( $attr_value ) || $this->has_js_value( $attr_value ) ) ) {
408 $element->removeAttribute( $attr_name );
409 continue;
410 }
411 }
412 }
413
414 /**
415 * strip_xlinks
416 * @param \DOMElement $element
417 */
418 private function strip_xlinks( $element ) {
419 $xlinks = $element->getAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
420
421 if ( ! $xlinks ) {
422 return;
423 }
424
425 $allowed_links = [
426 'data:image/png', // PNG
427 'data:image/gif', // GIF
428 'data:image/jpg', // JPG
429 'data:image/jpe', // JPEG
430 'data:image/pjp', // PJPEG
431 ];
432 if ( 1 === preg_match( self::SCRIPT_REGEX, $xlinks ) ) {
433 if ( ! in_array( substr( $xlinks, 0, 14 ), $allowed_links ) ) {
434 $element->removeAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
435 }
436 }
437 }
438
439 /**
440 * validate_use_tag
441 * @param $element
442 */
443 private function validate_use_tag( $element ) {
444 $xlinks = $element->getAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
445 if ( $xlinks && '#' !== substr( $xlinks, 0, 1 ) ) {
446 $element->parentNode->removeChild( $element ); // phpcs:ignore -- php DomNode
447 }
448 }
449
450 /**
451 * strip_docktype
452 */
453 private function strip_doctype() {
454 foreach ( $this->svg_dom->childNodes as $child ) {
455 if ( XML_DOCUMENT_TYPE_NODE === $child->nodeType ) { // phpcs:ignore -- php DomDocument
456 $child->parentNode->removeChild( $child ); // phpcs:ignore -- php DomDocument
457 }
458 }
459 }
460
461 /**
462 * sanitize_elements
463 */
464 private function sanitize_elements() {
465 $elements = $this->svg_dom->getElementsByTagName( '*' );
466 // loop through all elements
467 // we do this backwards so we don't skip anything if we delete a node
468 // see comments at: http://php.net/manual/en/class.domnamednodemap.php
469 for ( $index = $elements->length - 1; $index >= 0; $index-- ) {
470 /**
471 * @var \DOMElement $current_element
472 */
473 $current_element = $elements->item( $index );
474 // If the tag isn't in the whitelist, remove it and continue with next iteration
475 if ( ! $this->is_allowed_tag( $current_element ) ) {
476 continue;
477 }
478
479 //validate element attributes
480 $this->validate_allowed_attributes( $current_element );
481
482 $this->strip_xlinks( $current_element );
483
484 if ( 'use' === strtolower( $current_element->tagName ) ) { // phpcs:ignore -- php DomDocument
485 $this->validate_use_tag( $current_element );
486 }
487 }
488 }
489
490 /**
491 * sanitizer
492 * @param $content
493 *
494 * @return bool|string
495 */
496 public function sanitizer( $content ) {
497 // Strip php tags
498 $content = $this->strip_comments( $content );
499 $content = $this->strip_php_tags( $content );
500
501 // Find the start and end tags so we can cut out miscellaneous garbage.
502 $start = strpos( $content, '<svg' );
503 $end = strrpos( $content, '</svg>' );
504 if ( false === $start || false === $end ) {
505 return false;
506 }
507
508 $content = substr( $content, $start, ( $end - $start + 6 ) );
509
510 // If the server's PHP version is 8 or up, make sure to Disable the ability to load external entities
511 $php_version_under_eight = version_compare( PHP_VERSION, '8.0.0', '<' );
512 if ( $php_version_under_eight ) {
513 $libxml_disable_entity_loader = libxml_disable_entity_loader( true ); // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated
514 }
515 // Suppress the errors
516 $libxml_use_internal_errors = libxml_use_internal_errors( true );
517
518 // Create DomDocument instance
519 $this->svg_dom = new \DOMDocument();
520 $this->svg_dom->formatOutput = false;
521 $this->svg_dom->preserveWhiteSpace = false;
522 $this->svg_dom->strictErrorChecking = false;
523
524 $open_svg = $this->svg_dom->loadXML( $content );
525 if ( ! $open_svg ) {
526 return false;
527 }
528
529 $this->strip_doctype();
530 $this->sanitize_elements();
531
532 // Export sanitized svg to string
533 // Using documentElement to strip out <?xml version="1.0" encoding="UTF-8"...
534 $sanitized = $this->svg_dom->saveXML( $this->svg_dom->documentElement, LIBXML_NOEMPTYTAG );
535
536 // Restore defaults
537 if ( $php_version_under_eight ) {
538 libxml_disable_entity_loader( $libxml_disable_entity_loader ); // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated
539 }
540 libxml_use_internal_errors( $libxml_use_internal_errors );
541
542 return $sanitized;
543 }
544
545 /**
546 * strip_php_tags
547 * @param $string
548 *
549 * @return string
550 */
551 private function strip_php_tags( $string ) {
552 $string = preg_replace( '/<\?(=|php)(.+?)\?>/i', '', $string );
553 // Remove XML, ASP, etc.
554 $string = preg_replace( '/<\?(.*)\?>/Us', '', $string );
555 $string = preg_replace( '/<\%(.*)\%>/Us', '', $string );
556
557 if ( ( false !== strpos( $string, '<?' ) ) || ( false !== strpos( $string, '<%' ) ) ) {
558 return '';
559 }
560 return $string;
561 }
562
563 /**
564 * strip_comments
565 * @param $string
566 *
567 * @return string
568 */
569 private function strip_comments( $string ) {
570 // Remove comments.
571 $string = preg_replace( '/<!--(.*)-->/Us', '', $string );
572 $string = preg_replace( '/\/\*(.*)\*\//Us', '', $string );
573 if ( ( false !== strpos( $string, '<!--' ) ) || ( false !== strpos( $string, '/*' ) ) ) {
574 return '';
575 }
576 return $string;
577 }
578
579 /**
580 * wp_prepare_attachment_for_js
581 * @param $attachment_data
582 * @param $attachment
583 * @param $meta
584 *
585 * @return mixed
586 */
587 public function wp_prepare_attachment_for_js( $attachment_data, $attachment, $meta ) {
588 if ( 'image' !== $attachment_data['type'] || 'svg+xml' !== $attachment_data['subtype'] || ! class_exists( 'SimpleXMLElement' ) ) {
589 return $attachment_data;
590 }
591
592 $svg = self::get_inline_svg( $attachment->ID );
593
594 if ( ! $svg ) {
595 return $attachment_data;
596 }
597
598 try {
599 $svg = new \SimpleXMLElement( $svg );
600 } catch ( \Exception $e ) {
601 return $attachment_data;
602 }
603
604 $src = $attachment_data['url'];
605 $width = (int) $svg['width'];
606 $height = (int) $svg['height'];
607
608 // Media Gallery
609 $attachment_data['image'] = compact( 'src', 'width', 'height' );
610 $attachment_data['thumb'] = compact( 'src', 'width', 'height' );
611
612 // Single Details of Image
613 $attachment_data['sizes']['full'] = [
614 'height' => $height,
615 'width' => $width,
616 'url' => $src,
617 'orientation' => $height > $width ? 'portrait' : 'landscape',
618 ];
619 return $attachment_data;
620 }
621
622 /**
623 * set_attachment_id
624 * @param $attachment_id
625 *
626 * @return int
627 */
628 public function set_attachment_id( $attachment_id ) {
629 $this->attachment_id = $attachment_id;
630 return $this->attachment_id;
631 }
632
633 /**
634 * get_attachment_id
635 * @return int
636 */
637 public function get_attachment_id() {
638 return $this->attachment_id;
639 }
640
641 /**
642 * handle_upload_prefilter
643 * @param $file
644 *
645 * @return mixed
646 */
647 public function handle_upload_prefilter( $file ) {
648 if ( ! $this->is_file_should_handled( $file ) ) {
649 return $file;
650 }
651
652 $file = parent::handle_upload_prefilter( $file );
653
654 if ( ! $file['error'] && self::file_sanitizer_can_run() && ! $this->sanitize_svg( $file['tmp_name'] ) ) {
655 $display_type = strtoupper( $this->get_file_type() );
656
657 $file['error'] = sprintf( __( 'Invalid %1$s Format, file not uploaded for security reasons', 'elementor' ), $display_type );
658 }
659
660 return $file;
661 }
662
663 /**
664 * @since 3.0.0
665 * @deprecated 3.0.0 Use Files_Upload_Handler::file_sanitizer_can_run() instead.
666 */
667 public function svg_sanitizer_can_run() {
668 _deprecated_function( __METHOD__, '3.0.0', 'Files_Upload_Handler::file_sanitizer_can_run()' );
669
670 return Files_Upload_Handler::file_sanitizer_can_run();
671 }
672
673 /**
674 * @since 3.0.0
675 * @deprecated 3.0.0
676 */
677 public function upload_mimes() {
678 _deprecated_function( __METHOD__, '3.0.0' );
679 }
680
681 /**
682 * @since 3.0.0
683 * @deprecated 3.0.0
684 */
685 public function wp_handle_upload_prefilter() {
686 _deprecated_function( __METHOD__, '3.0.0' );
687 }
688
689 /**
690 * @since 3.0.0
691 * @deprecated 3.0.0 Use Files_Upload_Handler::is_enabled() instead.
692 * @see is_enabled()
693 */
694 public function is_svg_uploads_enabled() {
695 _deprecated_function( __METHOD__, '3.0.0', 'Files_Upload_Handler::is_enabled()' );
696
697 return Files_Upload_Handler::is_enabled();
698 }
699
700 /**
701 * Svg_Handler constructor.
702 */
703 public function __construct() {
704 parent::__construct();
705
706 add_filter( 'wp_prepare_attachment_for_js', [ $this, 'wp_prepare_attachment_for_js' ], 10, 3 );
707 add_action( 'elementor/core/files/clear_cache', [ $this, 'delete_meta_cache' ] );
708 }
709 }
710