PluginProbe
Elementor Website Builder – more than just a page builder / 3.4.7
Elementor Website Builder – more than just a page builder v3.4.7
4.3.2 4.3.1 4.3.0 4.3.0-beta3 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 All 455 releases
elementor / core / files / assets / svg / svg-handler.php

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

767 lines 17.9 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 /**
340 * Allowed attributes in SVG file.
341 *
342 * Filters the list of allowed attributes in SVG files.
343 *
344 * Since SVG files can run JS code that may inject malicious code, all attributes
345 * are removed except the allowed attributes.
346 *
347 * This hook can be used to manage allowed SVG attributes. To either add new
348 * attributes or delete existing attributes. To strengthen or weaken site security.
349 *
350 * @param array $allowed_attributes A list of allowed attributes.
351 */
352 $allowed_attributes = apply_filters( 'elementor/files/svg/allowed_attributes', $allowed_attributes );
353
354 return $allowed_attributes;
355 }
356
357 /**
358 * get_allowed_elements
359 * @return array
360 */
361 private function get_allowed_elements() {
362 $allowed_elements = [
363 'a',
364 'circle',
365 'clippath',
366 'defs',
367 'style',
368 'desc',
369 'ellipse',
370 'fegaussianblur',
371 'filter',
372 'foreignobject',
373 'g',
374 'image',
375 'line',
376 'lineargradient',
377 'marker',
378 'mask',
379 'metadata',
380 'path',
381 'pattern',
382 'polygon',
383 'polyline',
384 'radialgradient',
385 'rect',
386 'stop',
387 'svg',
388 'switch',
389 'symbol',
390 'text',
391 'textpath',
392 'title',
393 'tspan',
394 'use',
395 ];
396
397 /**
398 * Allowed elements in SVG file.
399 *
400 * Filters the list of allowed elements in SVG files.
401 *
402 * Since SVG files can run JS code that may inject malicious code, all elements
403 * are removed except the allowed elements.
404 *
405 * This hook can be used to manage SVG elements. To either add new elements or
406 * delete existing elements. To strengthen or weaken site security.
407 *
408 * @param array $allowed_elements A list of allowed elements.
409 */
410 $allowed_elements = apply_filters( 'elementor/files/svg/allowed_elements', $allowed_elements );
411
412 return $allowed_elements;
413 }
414
415 /**
416 * validate_allowed_attributes
417 * @param \DOMElement $element
418 */
419 private function validate_allowed_attributes( $element ) {
420 static $allowed_attributes = false;
421 if ( false === $allowed_attributes ) {
422 $allowed_attributes = $this->get_allowed_attributes();
423 }
424
425 for ( $index = $element->attributes->length - 1; $index >= 0; $index-- ) {
426 // get attribute name
427 $attr_name = $element->attributes->item( $index )->name;
428 $attr_name_lowercase = strtolower( $attr_name );
429 // Remove attribute if not in whitelist
430 if ( ! in_array( $attr_name_lowercase, $allowed_attributes ) && ! $this->is_a_attribute( $attr_name_lowercase, 'aria' ) && ! $this->is_a_attribute( $attr_name_lowercase, 'data' ) ) {
431 $element->removeAttribute( $attr_name );
432 continue;
433 }
434
435 $attr_value = $element->attributes->item( $index )->value;
436
437 // Remove attribute if it has a remote reference or js or data-URI/base64
438 if ( ! empty( $attr_value ) && ( $this->is_remote_value( $attr_value ) || $this->has_js_value( $attr_value ) ) ) {
439 $element->removeAttribute( $attr_name );
440 continue;
441 }
442 }
443 }
444
445 /**
446 * strip_xlinks
447 * @param \DOMElement $element
448 */
449 private function strip_xlinks( $element ) {
450 $xlinks = $element->getAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
451
452 if ( ! $xlinks ) {
453 return;
454 }
455
456 $allowed_links = [
457 'data:image/png', // PNG
458 'data:image/gif', // GIF
459 'data:image/jpg', // JPG
460 'data:image/jpe', // JPEG
461 'data:image/pjp', // PJPEG
462 ];
463 if ( 1 === preg_match( self::SCRIPT_REGEX, $xlinks ) ) {
464 if ( ! in_array( substr( $xlinks, 0, 14 ), $allowed_links ) ) {
465 $element->removeAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
466 }
467 }
468 }
469
470 /**
471 * validate_use_tag
472 * @param $element
473 */
474 private function validate_use_tag( $element ) {
475 $xlinks = $element->getAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
476 if ( $xlinks && '#' !== substr( $xlinks, 0, 1 ) ) {
477 $element->parentNode->removeChild( $element ); // phpcs:ignore -- php DomNode
478 }
479 }
480
481 /**
482 * strip_docktype
483 */
484 private function strip_doctype() {
485 foreach ( $this->svg_dom->childNodes as $child ) {
486 if ( XML_DOCUMENT_TYPE_NODE === $child->nodeType ) { // phpcs:ignore -- php DomDocument
487 $child->parentNode->removeChild( $child ); // phpcs:ignore -- php DomDocument
488 }
489 }
490 }
491
492 /**
493 * sanitize_elements
494 */
495 private function sanitize_elements() {
496 $elements = $this->svg_dom->getElementsByTagName( '*' );
497 // loop through all elements
498 // we do this backwards so we don't skip anything if we delete a node
499 // see comments at: http://php.net/manual/en/class.domnamednodemap.php
500 for ( $index = $elements->length - 1; $index >= 0; $index-- ) {
501 /**
502 * @var \DOMElement $current_element
503 */
504 $current_element = $elements->item( $index );
505 // If the tag isn't in the whitelist, remove it and continue with next iteration
506 if ( ! $this->is_allowed_tag( $current_element ) ) {
507 continue;
508 }
509
510 //validate element attributes
511 $this->validate_allowed_attributes( $current_element );
512
513 $this->strip_xlinks( $current_element );
514
515 if ( 'use' === strtolower( $current_element->tagName ) ) { // phpcs:ignore -- php DomDocument
516 $this->validate_use_tag( $current_element );
517 }
518 }
519 }
520
521 /**
522 * sanitizer
523 * @param $content
524 *
525 * @return bool|string
526 */
527 public function sanitizer( $content ) {
528 // Strip php tags
529 $content = $this->strip_comments( $content );
530 $content = $this->strip_php_tags( $content );
531
532 // Find the start and end tags so we can cut out miscellaneous garbage.
533 $start = strpos( $content, '<svg' );
534 $end = strrpos( $content, '</svg>' );
535 if ( false === $start || false === $end ) {
536 return false;
537 }
538
539 $content = substr( $content, $start, ( $end - $start + 6 ) );
540
541 // If the server's PHP version is 8 or up, make sure to Disable the ability to load external entities
542 $php_version_under_eight = version_compare( PHP_VERSION, '8.0.0', '<' );
543 if ( $php_version_under_eight ) {
544 $libxml_disable_entity_loader = libxml_disable_entity_loader( true ); // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated
545 }
546 // Suppress the errors
547 $libxml_use_internal_errors = libxml_use_internal_errors( true );
548
549 // Create DomDocument instance
550 $this->svg_dom = new \DOMDocument();
551 $this->svg_dom->formatOutput = false;
552 $this->svg_dom->preserveWhiteSpace = false;
553 $this->svg_dom->strictErrorChecking = false;
554
555 $open_svg = $this->svg_dom->loadXML( $content );
556 if ( ! $open_svg ) {
557 return false;
558 }
559
560 $this->strip_doctype();
561 $this->sanitize_elements();
562
563 // Export sanitized svg to string
564 // Using documentElement to strip out <?xml version="1.0" encoding="UTF-8"...
565 $sanitized = $this->svg_dom->saveXML( $this->svg_dom->documentElement, LIBXML_NOEMPTYTAG );
566
567 // Restore defaults
568 if ( $php_version_under_eight ) {
569 libxml_disable_entity_loader( $libxml_disable_entity_loader ); // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated
570 }
571 libxml_use_internal_errors( $libxml_use_internal_errors );
572
573 return $sanitized;
574 }
575
576 /**
577 * strip_php_tags
578 * @param $string
579 *
580 * @return string
581 */
582 private function strip_php_tags( $string ) {
583 $string = preg_replace( '/<\?(=|php)(.+?)\?>/i', '', $string );
584 // Remove XML, ASP, etc.
585 $string = preg_replace( '/<\?(.*)\?>/Us', '', $string );
586 $string = preg_replace( '/<\%(.*)\%>/Us', '', $string );
587
588 if ( ( false !== strpos( $string, '<?' ) ) || ( false !== strpos( $string, '<%' ) ) ) {
589 return '';
590 }
591 return $string;
592 }
593
594 /**
595 * strip_comments
596 * @param $string
597 *
598 * @return string
599 */
600 private function strip_comments( $string ) {
601 // Remove comments.
602 $string = preg_replace( '/<!--(.*)-->/Us', '', $string );
603 $string = preg_replace( '/\/\*(.*)\*\//Us', '', $string );
604 if ( ( false !== strpos( $string, '<!--' ) ) || ( false !== strpos( $string, '/*' ) ) ) {
605 return '';
606 }
607 return $string;
608 }
609
610 /**
611 * wp_prepare_attachment_for_js
612 * @param $attachment_data
613 * @param $attachment
614 * @param $meta
615 *
616 * @return mixed
617 */
618 public function wp_prepare_attachment_for_js( $attachment_data, $attachment, $meta ) {
619 if ( 'image' !== $attachment_data['type'] || 'svg+xml' !== $attachment_data['subtype'] || ! class_exists( 'SimpleXMLElement' ) ) {
620 return $attachment_data;
621 }
622
623 $svg = self::get_inline_svg( $attachment->ID );
624
625 if ( ! $svg ) {
626 return $attachment_data;
627 }
628
629 try {
630 $svg = new \SimpleXMLElement( $svg );
631 } catch ( \Exception $e ) {
632 return $attachment_data;
633 }
634
635 $src = $attachment_data['url'];
636 $width = (int) $svg['width'];
637 $height = (int) $svg['height'];
638
639 // Media Gallery
640 $attachment_data['image'] = compact( 'src', 'width', 'height' );
641 $attachment_data['thumb'] = compact( 'src', 'width', 'height' );
642
643 // Single Details of Image
644 $attachment_data['sizes']['full'] = [
645 'height' => $height,
646 'width' => $width,
647 'url' => $src,
648 'orientation' => $height > $width ? 'portrait' : 'landscape',
649 ];
650 return $attachment_data;
651 }
652
653 /**
654 * set_attachment_id
655 * @param $attachment_id
656 *
657 * @return int
658 */
659 public function set_attachment_id( $attachment_id ) {
660 $this->attachment_id = $attachment_id;
661 return $this->attachment_id;
662 }
663
664 /**
665 * get_attachment_id
666 * @return int
667 */
668 public function get_attachment_id() {
669 return $this->attachment_id;
670 }
671
672 /**
673 * set_svg_meta_data
674 * @return mixed
675 */
676 public function set_svg_meta_data( $data, $id ) {
677 $attachment = get_post( $id ); // Filter makes sure that the post is an attachment.
678 $mime_type = $attachment->post_mime_type;
679
680 // If the attachment is an svg
681 if ( 'image/svg+xml' === $mime_type ) {
682 // If the svg metadata are empty or the width is empty or the height is empty.
683 // then get the attributes from xml.
684 if ( empty( $data ) || empty( $data['width'] ) || empty( $data['height'] ) ) {
685 $xml = simplexml_load_file( get_attached_file( $id ) );
686 $attr = $xml->attributes();
687 $view_box = explode( ' ', $attr->viewBox );// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
688 $data['width'] = isset( $attr->width ) && preg_match( '/\d+/', $attr->width, $value ) ? (int) $value[0] : ( 4 === count( $view_box ) ? (int) $view_box[2] : null );
689 $data['height'] = isset( $attr->height ) && preg_match( '/\d+/', $attr->height, $value ) ? (int) $value[0] : ( 4 === count( $view_box ) ? (int) $view_box[3] : null );
690 }
691 }
692
693 return $data;
694 }
695
696 /**
697 * handle_upload_prefilter
698 * @param $file
699 *
700 * @return mixed
701 */
702 public function handle_upload_prefilter( $file ) {
703 if ( ! $this->is_file_should_handled( $file ) ) {
704 return $file;
705 }
706
707 $file = parent::handle_upload_prefilter( $file );
708
709 if ( ! $file['error'] && self::file_sanitizer_can_run() && ! $this->sanitize_svg( $file['tmp_name'] ) ) {
710 $display_type = strtoupper( $this->get_file_type() );
711
712 /* translators: %s: File type. */
713 $file['error'] = sprintf( esc_html__( 'Invalid %1$s Format, file not uploaded for security reasons', 'elementor' ), $display_type );
714 }
715
716 return $file;
717 }
718
719 /**
720 * @since 3.0.0
721 * @deprecated 3.0.0 Use Files_Upload_Handler::file_sanitizer_can_run() instead.
722 */
723 public function svg_sanitizer_can_run() {
724 _deprecated_function( __METHOD__, '3.0.0', 'Files_Upload_Handler::file_sanitizer_can_run()' );
725
726 return Files_Upload_Handler::file_sanitizer_can_run();
727 }
728
729 /**
730 * @since 3.0.0
731 * @deprecated 3.0.0
732 */
733 public function upload_mimes() {
734 _deprecated_function( __METHOD__, '3.0.0' );
735 }
736
737 /**
738 * @since 3.0.0
739 * @deprecated 3.0.0
740 */
741 public function wp_handle_upload_prefilter() {
742 _deprecated_function( __METHOD__, '3.0.0' );
743 }
744
745 /**
746 * @since 3.0.0
747 * @deprecated 3.0.0 Use Files_Upload_Handler::is_enabled() instead.
748 * @see is_enabled()
749 */
750 public function is_svg_uploads_enabled() {
751 _deprecated_function( __METHOD__, '3.0.0', 'Files_Upload_Handler::is_enabled()' );
752
753 return Files_Upload_Handler::is_enabled();
754 }
755
756 /**
757 * Svg_Handler constructor.
758 */
759 public function __construct() {
760 parent::__construct();
761
762 add_filter( 'wp_update_attachment_metadata', [ $this, 'set_svg_meta_data' ], 10, 2 );
763 add_filter( 'wp_prepare_attachment_for_js', [ $this, 'wp_prepare_attachment_for_js' ], 10, 3 );
764 add_action( 'elementor/core/files/clear_cache', [ $this, 'delete_meta_cache' ] );
765 }
766 }
767