PluginProbe
Elementor Website Builder – more than just a page builder / 3.0.7
Elementor Website Builder – more than just a page builder v3.0.7
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 4.0.8 4.1.0-beta1 4.1.0-dev1 All 452 releases
elementor / core / files / assets / svg / svg-handler.php

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

705 lines 15.4 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 // Make sure to Disable the ability to load external entities
511 $libxml_disable_entity_loader = libxml_disable_entity_loader( true );
512 // Suppress the errors
513 $libxml_use_internal_errors = libxml_use_internal_errors( true );
514
515 // Create DomDocument instance
516 $this->svg_dom = new \DOMDocument();
517 $this->svg_dom->formatOutput = false;
518 $this->svg_dom->preserveWhiteSpace = false;
519 $this->svg_dom->strictErrorChecking = false;
520
521 $open_svg = $this->svg_dom->loadXML( $content );
522 if ( ! $open_svg ) {
523 return false;
524 }
525
526 $this->strip_doctype();
527 $this->sanitize_elements();
528
529 // Export sanitized svg to string
530 // Using documentElement to strip out <?xml version="1.0" encoding="UTF-8"...
531 $sanitized = $this->svg_dom->saveXML( $this->svg_dom->documentElement, LIBXML_NOEMPTYTAG );
532
533 // Restore defaults
534 libxml_disable_entity_loader( $libxml_disable_entity_loader );
535 libxml_use_internal_errors( $libxml_use_internal_errors );
536
537 return $sanitized;
538 }
539
540 /**
541 * strip_php_tags
542 * @param $string
543 *
544 * @return string
545 */
546 private function strip_php_tags( $string ) {
547 $string = preg_replace( '/<\?(=|php)(.+?)\?>/i', '', $string );
548 // Remove XML, ASP, etc.
549 $string = preg_replace( '/<\?(.*)\?>/Us', '', $string );
550 $string = preg_replace( '/<\%(.*)\%>/Us', '', $string );
551
552 if ( ( false !== strpos( $string, '<?' ) ) || ( false !== strpos( $string, '<%' ) ) ) {
553 return '';
554 }
555 return $string;
556 }
557
558 /**
559 * strip_comments
560 * @param $string
561 *
562 * @return string
563 */
564 private function strip_comments( $string ) {
565 // Remove comments.
566 $string = preg_replace( '/<!--(.*)-->/Us', '', $string );
567 $string = preg_replace( '/\/\*(.*)\*\//Us', '', $string );
568 if ( ( false !== strpos( $string, '<!--' ) ) || ( false !== strpos( $string, '/*' ) ) ) {
569 return '';
570 }
571 return $string;
572 }
573
574 /**
575 * wp_prepare_attachment_for_js
576 * @param $attachment_data
577 * @param $attachment
578 * @param $meta
579 *
580 * @return mixed
581 */
582 public function wp_prepare_attachment_for_js( $attachment_data, $attachment, $meta ) {
583 if ( 'image' !== $attachment_data['type'] || 'svg+xml' !== $attachment_data['subtype'] || ! class_exists( 'SimpleXMLElement' ) ) {
584 return $attachment_data;
585 }
586
587 $svg = self::get_inline_svg( $attachment->ID );
588
589 if ( ! $svg ) {
590 return $attachment_data;
591 }
592
593 try {
594 $svg = new \SimpleXMLElement( $svg );
595 } catch ( \Exception $e ) {
596 return $attachment_data;
597 }
598
599 $src = $attachment_data['url'];
600 $width = (int) $svg['width'];
601 $height = (int) $svg['height'];
602
603 // Media Gallery
604 $attachment_data['image'] = compact( 'src', 'width', 'height' );
605 $attachment_data['thumb'] = compact( 'src', 'width', 'height' );
606
607 // Single Details of Image
608 $attachment_data['sizes']['full'] = [
609 'height' => $height,
610 'width' => $width,
611 'url' => $src,
612 'orientation' => $height > $width ? 'portrait' : 'landscape',
613 ];
614 return $attachment_data;
615 }
616
617 /**
618 * set_attachment_id
619 * @param $attachment_id
620 *
621 * @return int
622 */
623 public function set_attachment_id( $attachment_id ) {
624 $this->attachment_id = $attachment_id;
625 return $this->attachment_id;
626 }
627
628 /**
629 * get_attachment_id
630 * @return int
631 */
632 public function get_attachment_id() {
633 return $this->attachment_id;
634 }
635
636 /**
637 * handle_upload_prefilter
638 * @param $file
639 *
640 * @return mixed
641 */
642 public function handle_upload_prefilter( $file ) {
643 if ( ! $this->is_file_should_handled( $file ) ) {
644 return $file;
645 }
646
647 $file = parent::handle_upload_prefilter( $file );
648
649 if ( ! $file['error'] && self::file_sanitizer_can_run() && ! $this->sanitize_svg( $file['tmp_name'] ) ) {
650 $display_type = strtoupper( $this->get_file_type() );
651
652 $file['error'] = sprintf( __( 'Invalid %1$s Format, file not uploaded for security reasons', 'elementor' ), $display_type );
653 }
654
655 return $file;
656 }
657
658 /**
659 * @since 3.0.0
660 * @deprecated 3.0.0 Use Files_Upload_Handler::file_sanitizer_can_run() instead.
661 */
662 public function svg_sanitizer_can_run() {
663 _deprecated_function( __METHOD__, '3.0.0', 'Files_Upload_Handler::file_sanitizer_can_run()' );
664
665 return Files_Upload_Handler::file_sanitizer_can_run();
666 }
667
668 /**
669 * @since 3.0.0
670 * @deprecated 3.0.0
671 */
672 public function upload_mimes() {
673 _deprecated_function( __METHOD__, '3.0.0' );
674 }
675
676 /**
677 * @since 3.0.0
678 * @deprecated 3.0.0
679 */
680 public function wp_handle_upload_prefilter() {
681 _deprecated_function( __METHOD__, '3.0.0' );
682 }
683
684 /**
685 * @since 3.0.0
686 * @deprecated 3.0.0 Use Files_Upload_Handler::is_enabled() instead.
687 * @see is_enabled()
688 */
689 public function is_svg_uploads_enabled() {
690 _deprecated_function( __METHOD__, '3.0.0', 'Files_Upload_Handler::is_enabled()' );
691
692 return Files_Upload_Handler::is_enabled();
693 }
694
695 /**
696 * Svg_Handler constructor.
697 */
698 public function __construct() {
699 parent::__construct();
700
701 add_filter( 'wp_prepare_attachment_for_js', [ $this, 'wp_prepare_attachment_for_js' ], 10, 3 );
702 add_action( 'elementor/core/files/clear_cache', [ $this, 'delete_meta_cache' ] );
703 }
704 }
705