PluginProbe ʕ •ᴥ•ʔ
Safe SVG / 2.3.2
Safe SVG v2.3.2
trunk 1.0.0 1.1.0 1.1.1 1.2.0 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.5.0 1.5.1 1.5.2 1.5.3 1.6.0 1.6.1 1.7.1 1.8.0 1.8.1 1.9.0 1.9.1 1.9.10 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0
safe-svg / safe-svg.php
safe-svg Last commit date
assets 2 years ago dist 10 months ago includes 10 months ago languages 3 years ago vendor 10 months ago readme.txt 10 months ago safe-svg.php 10 months ago
safe-svg.php
782 lines
1 <?php
2 /**
3 * Plugin Name: Safe SVG
4 * Plugin URI: https://wordpress.org/plugins/safe-svg/
5 * Description: Enable SVG uploads and sanitize them to stop XML/SVG vulnerabilities in your WordPress website
6 * Version: 2.3.2
7 * Requires at least: 6.6
8 * Requires PHP: 7.4
9 * Author: 10up
10 * Author URI: https://10up.com
11 * License: GPL-2.0-or-later
12 * License URI: https://spdx.org/licenses/GPL-2.0-or-later.html
13 * Text Domain: safe-svg
14 * Domain Path: /languages
15 *
16 * @package safe-svg
17 */
18
19 namespace SafeSvg;
20
21 use enshrined\svgSanitize\Sanitizer;
22
23 if ( ! defined( 'ABSPATH' ) ) {
24 exit; // Exit if accessed directly.
25 }
26
27 define( 'SAFE_SVG_VERSION', '2.3.2' );
28 define( 'SAFE_SVG_PLUGIN_DIR', __DIR__ );
29 define( 'SAFE_SVG_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
30
31 /**
32 * Get the minimum version of PHP required by this plugin.
33 *
34 * @since 2.1.1
35 *
36 * @return string Minimum version required.
37 */
38 function minimum_php_requirement() {
39 return '7.4';
40 }
41
42 /**
43 * Whether PHP installation meets the minimum requirements
44 *
45 * @since 2.1.1
46 *
47 * @return bool True if meets minimum requirements, false otherwise.
48 */
49 function site_meets_php_requirements() {
50 return version_compare( phpversion(), minimum_php_requirement(), '>=' );
51 }
52
53 // Try and include our autoloader, ensuring our PHP version is met first.
54 if ( ! site_meets_php_requirements() ) {
55 add_action(
56 'admin_notices',
57 function() {
58 ?>
59 <div class="notice notice-error">
60 <p>
61 <?php
62 echo wp_kses_post(
63 sprintf(
64 /* translators: %s: Minimum required PHP version */
65 __( 'Safe SVG requires PHP version %s or later. Please upgrade PHP or disable the plugin.', 'safe-svg' ),
66 esc_html( minimum_php_requirement() )
67 )
68 );
69 ?>
70 </p>
71 </div>
72 <?php
73 }
74 );
75 return;
76 } elseif ( is_readable( __DIR__ . '/vendor/autoload.php' ) ) {
77 require __DIR__ . '/vendor/autoload.php';
78 } elseif ( ! class_exists( Sanitizer::class ) ) {
79 add_action(
80 'admin_notices',
81 function() {
82 ?>
83 <div class="notice notice-error">
84 <p>
85 <?php
86 echo wp_kses_post(
87 sprintf(
88 /* translators: %1$s is the command that needs to be run. */
89 __( 'You appear to be running a development version of Safe SVG. Please run %1$s in order for things to work properly.', 'safe-svg' ),
90 '<code>composer install</code>'
91 )
92 );
93 ?>
94 </p>
95 </div>
96 <?php
97 }
98 );
99 return;
100 }
101
102 require __DIR__ . '/includes/safe-svg-tags.php';
103 require __DIR__ . '/includes/safe-svg-attributes.php';
104 require __DIR__ . '/includes/safe-svg-settings.php';
105 require __DIR__ . '/includes/blocks.php';
106 require __DIR__ . '/includes/optimizer.php';
107
108 new \SafeSVG\Optimizer();
109
110 if ( ! class_exists( 'SafeSvg\\safe_svg' ) ) {
111
112 /**
113 * Class safe_svg
114 */
115 class safe_svg {
116
117 /**
118 * The sanitizer
119 *
120 * @var \enshrined\svgSanitize\Sanitizer
121 */
122 protected $sanitizer;
123
124 /**
125 * Set up the class
126 */
127 public function __construct() {
128 $this->sanitizer = new Sanitizer();
129 $this->sanitizer->minify( true );
130
131 // Allow SVG uploads from specific contexts.
132 add_action( 'load-upload.php', array( $this, 'allow_svg_from_upload' ) );
133 add_action( 'load-post-new.php', array( $this, 'allow_svg_from_upload' ) );
134 add_action( 'load-post.php', array( $this, 'allow_svg_from_upload' ) );
135 add_action( 'load-site-editor.php', array( $this, 'allow_svg_from_upload' ) );
136
137 // Init all the things.
138 add_action( 'init', array( $this, 'setup_blocks' ) );
139 add_filter( 'wp_handle_sideload_prefilter', array( $this, 'check_for_svg' ) );
140 add_filter( 'wp_handle_upload_prefilter', array( $this, 'check_for_svg' ) );
141 add_filter( 'wp_prepare_attachment_for_js', array( $this, 'fix_admin_preview' ), 10, 3 );
142 add_filter( 'wp_get_attachment_image_src', array( $this, 'one_pixel_fix' ), 10, 4 );
143 add_filter( 'admin_post_thumbnail_html', array( $this, 'featured_image_fix' ), 10, 3 );
144 add_action( 'admin_enqueue_scripts', array( $this, 'load_custom_admin_style' ) );
145 add_action( 'get_image_tag', array( $this, 'get_image_tag_override' ), 10, 6 );
146 add_filter( 'wp_generate_attachment_metadata', array( $this, 'skip_svg_regeneration' ), 10, 2 );
147 add_filter( 'wp_get_attachment_metadata', array( $this, 'metadata_error_fix' ), 10, 2 );
148 add_filter( 'wp_calculate_image_srcset_meta', array( $this, 'disable_srcset' ), 10, 4 );
149
150 new safe_svg_settings();
151 }
152
153 /**
154 * Allow SVG uploads from the wp-admin/upload.php screen. Without this, you cannot upload SVGs from the media grid view.
155 *
156 * @return void
157 */
158 public function allow_svg_from_upload() {
159 add_filter( 'upload_mimes', array( $this, 'allow_svg' ) );
160 add_filter( 'wp_check_filetype_and_ext', array( $this, 'fix_mime_type_svg' ), 75, 4 );
161 }
162
163 /**
164 * Custom function to check if user can upload svg.
165 *
166 * Use core caps if setting hasn't every been updated.
167 *
168 * @return bool
169 */
170 public function current_user_can_upload_svg() {
171 $upload_roles = get_option( 'safe_svg_upload_roles', [] );
172 $can_upload = false;
173
174 if ( empty( $upload_roles ) ) {
175 // Fallback to upload_files check for backwards compatibility.
176 $can_upload = current_user_can( 'upload_files' );
177 } else {
178 // Use our custom capability if some upload roles are set.
179 $can_upload = current_user_can( 'safe_svg_upload_svg' );
180 }
181
182 /**
183 * Determine if the current user can upload an svg.
184 *
185 * @param bool $can_upload Can the current user upload an svg?
186 *
187 * @return bool
188 */
189 return (bool) apply_filters( 'safe_svg_current_user_can_upload', $can_upload );
190 }
191
192 /**
193 * Setup the blocks.
194 */
195 public function setup_blocks() {
196 // Setup blocks.
197 Blocks\setup();
198 }
199
200 /**
201 * Allow SVG Uploads
202 *
203 * @param array $mimes Mime types keyed by the file extension regex corresponding to those types.
204 *
205 * @return mixed
206 */
207 public function allow_svg( $mimes ) {
208 if ( $this->current_user_can_upload_svg() ) {
209 $mimes['svg'] = 'image/svg+xml';
210 $mimes['svgz'] = 'image/svg+xml';
211 }
212
213 return $mimes;
214 }
215
216 /**
217 * Fixes the issue in WordPress 4.7.1 being unable to correctly identify SVGs
218 *
219 * @thanks @lewiscowles
220 *
221 * @param array $data Values for the extension, mime type, and corrected filename.
222 * @param string $file Full path to the file.
223 * @param string $filename The name of the file.
224 * @param string[] $mimes Array of mime types keyed by their file extension regex.
225 *
226 * @return null
227 */
228 public function fix_mime_type_svg( $data = null, $file = null, $filename = null, $mimes = null ) {
229 $ext = isset( $data['ext'] ) ? $data['ext'] : '';
230 if ( strlen( $ext ) < 1 ) {
231 $exploded = explode( '.', $filename );
232 $ext = strtolower( end( $exploded ) );
233 }
234 if ( 'svg' === $ext ) {
235 $data['type'] = 'image/svg+xml';
236 $data['ext'] = 'svg';
237 } elseif ( 'svgz' === $ext ) {
238 $data['type'] = 'image/svg+xml';
239 $data['ext'] = 'svgz';
240 }
241
242 return $data;
243 }
244
245 /**
246 * Check if the file is an SVG, if so handle appropriately
247 *
248 * @param array $file An array of data for a single file.
249 *
250 * @return mixed
251 */
252 public function check_for_svg( $file ) {
253
254 // Ensure we have a proper file path before processing
255 if ( ! isset( $file['tmp_name'] ) ) {
256 return $file;
257 }
258
259 $file_name = isset( $file['name'] ) ? $file['name'] : '';
260
261 // Allow SVGs to be uploaded when this function runs.
262 add_filter( 'upload_mimes', array( $this, 'allow_svg' ) );
263 add_filter( 'wp_check_filetype_and_ext', array( $this, 'fix_mime_type_svg' ), 75, 4 );
264
265 $wp_filetype = wp_check_filetype_and_ext( $file['tmp_name'], $file_name );
266
267 // Remove the SVG mime type after we've sanitized the file.
268 // We need to utilize the pre_move_uploaded_file filter to ensure we can remove the filters after the file has been full-processed.
269 // This is because wp_check_filetype_and_ext() is called multiple times during the upload process.
270 add_filter( 'pre_move_uploaded_file', array( $this, 'pre_move_uploaded_file' ) );
271
272 $type = ! empty( $wp_filetype['type'] ) ? $wp_filetype['type'] : '';
273
274 if ( 'image/svg+xml' === $type ) {
275 if ( ! $this->current_user_can_upload_svg() ) {
276 $file['error'] = __(
277 'Sorry, you are not allowed to upload SVG files.',
278 'safe-svg'
279 );
280
281 return $file;
282 }
283
284 if ( ! $this->sanitize( $file['tmp_name'] ) ) {
285 $file['error'] = __(
286 "Sorry, this file couldn't be sanitized so for security reasons wasn't uploaded",
287 'safe-svg'
288 );
289 }
290 }
291
292 return $file;
293 }
294
295 /**
296 * Remove the filters after the file has been processed.
297 *
298 * We need to utilize the pre_move_uploaded_file filter to ensure we can remove the filters after the file has been full-processed.
299 * This is because wp_check_filetype_and_ext() is called multiple times during the upload process.
300 *
301 * @param string $move_new_file The new file path. We don't touch this, just return it.
302 *
303 * @return string
304 */
305 public function pre_move_uploaded_file( $move_new_file ) {
306 remove_filter( 'wp_check_filetype_and_ext', array( $this, 'fix_mime_type_svg' ), 75 );
307 remove_filter( 'upload_mimes', array( $this, 'allow_svg' ) );
308
309 return $move_new_file;
310 }
311
312 /**
313 * Sanitize the SVG
314 *
315 * @param string $file Temp file path.
316 *
317 * @return bool|int
318 */
319 protected function sanitize( $file ) {
320 $dirty = file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
321
322 // Is the SVG gzipped? If so we try and decode the string
323 $is_zipped = $this->is_gzipped( $dirty );
324 if ( $is_zipped ) {
325 $dirty = gzdecode( $dirty );
326
327 // If decoding fails, bail as we're not secure
328 if ( false === $dirty ) {
329 return false;
330 }
331 }
332
333 // Allow large SVGs if the setting is on.
334 if ( get_option( 'safe_svg_large_svg' ) ) {
335 $this->sanitizer->setAllowHugeFiles( true );
336 }
337
338 /**
339 * Load extra filters to allow devs to access the safe tags and attrs by themselves.
340 */
341 $this->sanitizer->setAllowedTags( new SafeSvgTags\safe_svg_tags() );
342 $this->sanitizer->setAllowedAttrs( new SafeSvgAttr\safe_svg_attributes() );
343
344 $clean = $this->sanitizer->sanitize( $dirty );
345
346 if ( false === $clean ) {
347 return false;
348 }
349
350 // If we were gzipped, we need to re-zip
351 if ( $is_zipped ) {
352 $clean = gzencode( $clean );
353 }
354
355 file_put_contents( $file, $clean ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents
356
357 return true;
358 }
359
360 /**
361 * Check if the contents are gzipped
362 *
363 * @see http://www.gzip.org/zlib/rfc-gzip.html#member-format
364 *
365 * @param string $contents Content to check.
366 *
367 * @return bool
368 */
369 protected function is_gzipped( $contents ) {
370 // phpcs:disable Generic.Strings.UnnecessaryStringConcat.Found
371 if ( function_exists( 'mb_strpos' ) ) {
372 return 0 === mb_strpos( $contents, "\x1f" . "\x8b" . "\x08" );
373 } else {
374 return 0 === strpos( $contents, "\x1f" . "\x8b" . "\x08" );
375 }
376 // phpcs:enable
377 }
378
379 /**
380 * Filters the attachment data prepared for JavaScript to add the sizes array to the response
381 *
382 * @param array $response Array of prepared attachment data.
383 * @param int|object $attachment Attachment ID or object.
384 * @param array $meta Array of attachment meta data.
385 *
386 * @return array
387 */
388 public function fix_admin_preview( $response, $attachment, $meta ) {
389
390 if ( 'image/svg+xml' === $response['mime'] ) {
391 $dimensions = $this->svg_dimensions( $attachment->ID );
392
393 if ( $dimensions ) {
394 $response = array_merge( $response, $dimensions );
395 }
396
397 $possible_sizes = apply_filters(
398 'image_size_names_choose',
399 array(
400 'full' => __( 'Full Size' ),
401 'thumbnail' => __( 'Thumbnail' ),
402 'medium' => __( 'Medium' ),
403 'large' => __( 'Large' ),
404 )
405 );
406
407 $sizes = array();
408
409 foreach ( $possible_sizes as $size => $label ) {
410 $default_height = 2000;
411 $default_width = 2000;
412
413 if ( 'full' === $size && $dimensions ) {
414 $default_height = $dimensions['height'];
415 $default_width = $dimensions['width'];
416 }
417
418 $sizes[ $size ] = array(
419 'height' => get_option( "{$size}_size_w", $default_height ),
420 'width' => get_option( "{$size}_size_h", $default_width ),
421 'url' => $response['url'],
422 'orientation' => 'portrait',
423 );
424 }
425
426 $response['sizes'] = $sizes;
427 $response['icon'] = $response['url'];
428 }
429
430 return $response;
431 }
432
433 /**
434 * Filters the image src result.
435 * If the image size doesn't exist, set a default size of 100 for width and height
436 *
437 * @param array|false $image Either array with src, width & height, icon src, or false.
438 * @param int $attachment_id Image attachment ID.
439 * @param string|array $size Size of image. Image size or array of width and height values
440 * (in that order). Default 'thumbnail'.
441 * @param bool $icon Whether the image should be treated as an icon. Default false.
442 *
443 * @return array
444 */
445 public function one_pixel_fix( $image, $attachment_id, $size, $icon ) {
446 if ( get_post_mime_type( $attachment_id ) === 'image/svg+xml' ) {
447 $dimensions = $this->svg_dimensions( $attachment_id, $size );
448
449 if ( $dimensions ) {
450 $image[1] = $dimensions['width'];
451 $image[2] = $dimensions['height'];
452 } else {
453 $image[1] = 100;
454 $image[2] = 100;
455 }
456 }
457
458 return $image;
459 }
460
461 /**
462 * If the featured image is an SVG we wrap it in an SVG class so we can apply our CSS fix.
463 *
464 * @param string $content Admin post thumbnail HTML markup.
465 * @param int $post_id Post ID.
466 * @param int|null $thumbnail_id Thumbnail attachment ID, or null if there isn't one.
467 *
468 * @return string
469 */
470 public function featured_image_fix( $content, $post_id, $thumbnail_id = null ) {
471 $mime = get_post_mime_type( $thumbnail_id );
472
473 if ( 'image/svg+xml' === $mime ) {
474 $content = sprintf( '<span class="svg">%s</span>', $content );
475 }
476
477 return $content;
478 }
479
480 /**
481 * Load our custom CSS sheet.
482 */
483 public function load_custom_admin_style() {
484 wp_enqueue_style( 'safe-svg-css', plugins_url( 'assets/safe-svg.css', __FILE__ ), array(), SAFE_SVG_VERSION );
485 }
486
487 /**
488 * Override the default height and width string on an SVG
489 *
490 * @param string $html HTML content for the image.
491 * @param int $id Attachment ID.
492 * @param string $alt Alternate text.
493 * @param string $title Attachment title.
494 * @param string $align Part of the class name for aligning the image.
495 * @param string|array $size Size of image. Image size or array of width and height values (in that order).
496 * Default 'medium'.
497 *
498 * @return mixed
499 */
500 public function get_image_tag_override( $html, $id, $alt, $title, $align, $size ) {
501 $mime = get_post_mime_type( $id );
502
503 if ( 'image/svg+xml' === $mime ) {
504 if ( is_array( $size ) ) {
505 $width = $size[0];
506 $height = $size[1];
507 } elseif ( 'full' === $size && $dimensions = $this->svg_dimensions( $id ) ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.Found, Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure
508 $width = $dimensions['width'];
509 $height = $dimensions['height'];
510 } else {
511 $width = get_option( "{$size}_size_w", false );
512 $height = get_option( "{$size}_size_h", false );
513 }
514
515 if ( $height && $width ) {
516 $html = str_replace( 'width="1" ', sprintf( 'width="%s" ', $width ), $html );
517 $html = str_replace( 'height="1" ', sprintf( 'height="%s" ', $height ), $html );
518 } else {
519 $html = str_replace( 'width="1" ', '', $html );
520 $html = str_replace( 'height="1" ', '', $html );
521 }
522
523 $html = str_replace( '/>', ' role="img" />', $html );
524 }
525
526 return $html;
527 }
528
529 /**
530 * Skip regenerating SVGs
531 *
532 * @param array $metadata An array of attachment meta data.
533 * @param int $attachment_id Attachment Id to process.
534 *
535 * @return mixed Metadata for attachment.
536 */
537 public function skip_svg_regeneration( $metadata, $attachment_id ) {
538 $mime = get_post_mime_type( $attachment_id );
539 if ( 'image/svg+xml' === $mime ) {
540 $additional_image_sizes = wp_get_additional_image_sizes();
541 $svg_path = get_attached_file( $attachment_id );
542 $upload_dir = wp_upload_dir();
543 // get the path relative to /uploads/ - found no better way:
544 $relative_path = str_replace( trailingslashit( $upload_dir['basedir'] ), '', $svg_path );
545 $filename = basename( $svg_path );
546
547 $dimensions = $this->svg_dimensions( $attachment_id );
548
549 if ( ! $dimensions ) {
550 return $metadata;
551 }
552
553 $metadata = array(
554 'width' => intval( $dimensions['width'] ),
555 'height' => intval( $dimensions['height'] ),
556 'file' => $relative_path,
557 );
558
559 // Might come handy to create the sizes array too - But it's not needed for this workaround! Always links to original svg-file => Hey, it's a vector graphic! ;)
560 $sizes = array();
561 foreach ( get_intermediate_image_sizes() as $s ) {
562 $sizes[ $s ] = array(
563 'width' => '',
564 'height' => '',
565 'crop' => false,
566 );
567
568 if ( isset( $additional_image_sizes[ $s ]['width'] ) ) {
569 // For theme-added sizes
570 $sizes[ $s ]['width'] = intval( $additional_image_sizes[ $s ]['width'] );
571 } else {
572 // For default sizes set in options
573 $sizes[ $s ]['width'] = get_option( "{$s}_size_w" );
574 }
575
576 if ( isset( $additional_image_sizes[ $s ]['height'] ) ) {
577 // For theme-added sizes
578 $sizes[ $s ]['height'] = intval( $additional_image_sizes[ $s ]['height'] );
579 } else {
580 // For default sizes set in options
581 $sizes[ $s ]['height'] = get_option( "{$s}_size_h" );
582 }
583
584 if ( isset( $additional_image_sizes[ $s ]['crop'] ) ) {
585 // For theme-added sizes
586 $sizes[ $s ]['crop'] = intval( $additional_image_sizes[ $s ]['crop'] );
587 } else {
588 // For default sizes set in options
589 $sizes[ $s ]['crop'] = get_option( "{$s}_crop" );
590 }
591
592 $sizes[ $s ]['file'] = $filename;
593 $sizes[ $s ]['mime-type'] = $mime;
594 }
595 $metadata['sizes'] = $sizes;
596 }
597
598 return $metadata;
599 }
600
601 /**
602 * Filters the attachment meta data.
603 *
604 * @param array|bool $data Array of meta data for the given attachment, or false
605 * if the object does not exist.
606 * @param int $post_id Attachment ID.
607 */
608 public function metadata_error_fix( $data, $post_id ) {
609
610 // If it's a WP_Error regenerate metadata and save it
611 if ( is_wp_error( $data ) ) {
612 $data = wp_generate_attachment_metadata( $post_id, get_attached_file( $post_id ) );
613 wp_update_attachment_metadata( $post_id, $data );
614 }
615
616 return $data;
617 }
618
619 /**
620 * Get SVG size from the width/height or viewport.
621 *
622 * @param integer $attachment_id The attachment ID of the SVG being processed.
623 *
624 * @return array|bool
625 */
626 protected function svg_dimensions( $attachment_id ) {
627 /**
628 * Calculate SVG dimensions and orientation.
629 *
630 * This filter allows you to implement your own sizing. By returning a non-false
631 * value, it will short-circuit this function and return your set value.
632 *
633 * @param boolean Default value of the filter.
634 * @param integer $attachment_id The attachment ID of the SVG being processed.
635 *
636 * @return array|false An array of SVG dimensions and orientation or false.
637 */
638 $short_circuit = apply_filters( 'safe_svg_pre_dimensions', false, $attachment_id );
639
640 if ( false !== $short_circuit ) {
641 return $short_circuit;
642 }
643
644 if ( ! function_exists( 'simplexml_load_file' ) ) {
645 return false;
646 }
647
648 $svg = get_attached_file( $attachment_id );
649 $metadata = wp_get_attachment_metadata( $attachment_id );
650 $width = 0;
651 $height = 0;
652
653 if ( $svg && ! empty( $metadata['width'] ) && ! empty( $metadata['height'] ) ) {
654 $width = floatval( $metadata['width'] );
655 $height = floatval( $metadata['height'] );
656 } elseif ( $svg ) {
657 $svg = @simplexml_load_file( $svg ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
658
659 // Ensure the svg could be loaded.
660 if ( ! $svg ) {
661 return false;
662 }
663
664 $attributes = $svg->attributes();
665
666 if ( isset( $attributes->viewBox ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
667 $sizes = explode( ' ', $attributes->viewBox ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
668 if ( isset( $sizes[2], $sizes[3] ) ) {
669 $viewbox_width = floatval( $sizes[2] );
670 $viewbox_height = floatval( $sizes[3] );
671 }
672 }
673
674 if ( isset( $attributes->width, $attributes->height ) && is_numeric( (float) $attributes->width ) && is_numeric( (float) $attributes->height ) && ! $this->str_ends_with( (string) $attributes->width, '%' ) && ! $this->str_ends_with( (string) $attributes->height, '%' ) ) {
675 $attr_width = floatval( $attributes->width );
676 $attr_height = floatval( $attributes->height );
677 }
678
679 /**
680 * Decide which attributes of the SVG we use first for image tag dimensions.
681 *
682 * We default to using the parameters in the viewbox attribute but
683 * that can be overridden using this filter if you'd prefer to use
684 * the width and height attributes.
685 *
686 * @hook safe_svg_use_width_height_attributes
687 *
688 * @param bool $use_width_height_attributes If the width & height attributes should be used first. Default false.
689 * @param string $svg The file path to the SVG.
690 *
691 * @return bool If we should use the width & height attributes first or not.
692 */
693 $use_width_height = (bool) apply_filters( 'safe_svg_use_width_height_attributes', false, $svg );
694
695 if ( $use_width_height ) {
696 if ( isset( $attr_width, $attr_height ) ) {
697 $width = $attr_width;
698 $height = $attr_height;
699 } elseif ( isset( $viewbox_width, $viewbox_height ) ) {
700 $width = $viewbox_width;
701 $height = $viewbox_height;
702 }
703 } else {
704 if ( isset( $viewbox_width, $viewbox_height ) ) {
705 $width = $viewbox_width;
706 $height = $viewbox_height;
707 } elseif ( isset( $attr_width, $attr_height ) ) {
708 $width = $attr_width;
709 $height = $attr_height;
710 }
711 }
712
713 if ( ! $width && ! $height ) {
714 return false;
715 }
716 }
717
718 $dimensions = array(
719 'width' => $width,
720 'height' => $height,
721 'orientation' => ( $width > $height ) ? 'landscape' : 'portrait',
722 );
723
724 /**
725 * Calculate SVG dimensions and orientation.
726 *
727 * @param array $dimensions An array containing width, height, and orientation.
728 * @param string $svg The file path to the SVG.
729 *
730 * @return array An array of SVG dimensions and orientation.
731 */
732 return apply_filters( 'safe_svg_dimensions', $dimensions, $svg );
733 }
734
735 /**
736 * Disable the creation of srcset on SVG images.
737 *
738 * @param array $image_meta The image meta data.
739 * @param int[] $size_array {
740 * An array of requested width and height values.
741 *
742 * @type int $0 The width in pixels.
743 * @type int $1 The height in pixels.
744 * }
745 * @param string $image_src The 'src' of the image.
746 * @param int $attachment_id The image attachment ID.
747 */
748 public function disable_srcset( $image_meta, $size_array, $image_src, $attachment_id ) {
749 if ( $attachment_id && 'image/svg+xml' === get_post_mime_type( $attachment_id ) && is_array( $image_meta ) ) {
750 $image_meta['sizes'] = array();
751 }
752
753 return $image_meta;
754 }
755
756 /**
757 * Polyfill for `str_ends_with()` function added in PHP 8.0.
758 *
759 * Performs a case-sensitive check indicating if
760 * the haystack ends with needle.
761 *
762 * @param string $haystack The string to search in.
763 * @param string $needle The substring to search for in the `$haystack`.
764 * @return bool True if `$haystack` ends with `$needle`, otherwise false.
765 */
766 protected function str_ends_with( $haystack, $needle ) {
767 if ( function_exists( 'str_ends_with' ) ) {
768 return str_ends_with( $haystack, $needle );
769 }
770
771 if ( '' === $haystack && '' !== $needle ) {
772 return false;
773 }
774
775 $len = strlen( $needle );
776 return 0 === substr_compare( $haystack, $needle, -$len, $len );
777 }
778 }
779 }
780
781 new safe_svg();
782