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 |