'POST', 'permission_callback' => array( $this, 'permission' ), 'callback' => array( $this, 'upload' ), ) ); } public function permission( WP_REST_Request $request ) { $permission = Security::rest_can_edit_request( $request ); if ( true !== $permission ) { return $permission; } if ( ! current_user_can( 'upload_files' ) ) { return new WP_Error( 'rest_upload_forbidden', __( 'Sorry, you are not allowed to upload media.', 'advanced-animation' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } public function upload( WP_REST_Request $request ) { $source = $request->get_param( 'svg' ); if ( ! is_string( $source ) || '' === trim( $source ) ) { return new WP_Error( 'svg_required', __( 'An SVG data URI is required.', 'advanced-animation' ), array( 'status' => 400 ) ); } $decoded = $this->decode_data_uri( trim( $source ) ); if ( is_wp_error( $decoded ) ) { return $decoded; } $result = self::store_svg( $decoded ); return is_wp_error( $result ) ? $result : rest_ensure_response( $result ); } /** Validate and store raw SVG XML for authenticated media import callers. */ public static function store_svg( string $source ) { $max_size = (int) wp_max_upload_size(); if ( $max_size <= 0 ) { $max_size = 20 * 1024 * 1024; } if ( strlen( $source ) > $max_size ) { return new WP_Error( 'svg_too_large', __( 'The SVG exceeds the WordPress upload limit.', 'advanced-animation' ), array( 'status' => 413 ) ); } $dimensions = array(); $svg = self::sanitize_svg( $source, $dimensions ); if ( is_wp_error( $svg ) ) { return $svg; } if ( strlen( $svg ) > $max_size ) { return new WP_Error( 'svg_too_large', __( 'The SVG exceeds the WordPress upload limit.', 'advanced-animation' ), array( 'status' => 413 ) ); } $hash = hash( 'sha256', $svg ); $existing = get_posts( array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'numberposts' => 1, 'fields' => 'ids', 'meta_key' => self::HASH_META_KEY, 'meta_value' => $hash, ) ); if ( ! empty( $existing ) ) { $url = wp_get_attachment_url( (int) $existing[0] ); if ( $url ) { self::ensure_dimensions( (int) $existing[0], $dimensions ); return array( 'id' => (int) $existing[0], 'url' => $url, 'source_url' => $url, 'reused' => true, ); } } if ( ! function_exists( 'wp_upload_bits' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } if ( ! function_exists( 'wp_insert_attachment' ) ) { require_once ABSPATH . 'wp-admin/includes/image.php'; } $filename = 'advanced-addons-svg-' . substr( $hash, 0, 16 ) . '.svg'; // Only validated SVGs use this scoped exception to WordPress's MIME list. $allow_svg = static function ( $mimes ) { $mimes['svg'] = 'image/svg+xml'; return $mimes; }; add_filter( 'upload_mimes', $allow_svg ); try { $upload = wp_upload_bits( $filename, null, $svg ); } finally { remove_filter( 'upload_mimes', $allow_svg ); } if ( ! empty( $upload['error'] ) ) { return new WP_Error( 'svg_upload_failed', $upload['error'], array( 'status' => 500 ) ); } $attachment_id = wp_insert_attachment( array( 'post_mime_type' => 'image/svg+xml', 'post_title' => sanitize_file_name( pathinfo( $filename, PATHINFO_FILENAME ) ), 'post_status' => 'inherit', ), $upload['file'], 0, true ); if ( is_wp_error( $attachment_id ) ) { wp_delete_file( $upload['file'] ); return new WP_Error( 'svg_attachment_failed', $attachment_id->get_error_message(), array( 'status' => 500 ) ); } update_post_meta( $attachment_id, self::HASH_META_KEY, $hash ); self::ensure_dimensions( (int) $attachment_id, $dimensions ); return array( 'id' => (int) $attachment_id, 'url' => $upload['url'], 'source_url' => $upload['url'], 'reused' => false, ); } private function decode_data_uri( string $source ) { $comma = strpos( $source, ',' ); $header = false === $comma ? '' : substr( $source, 0, $comma ); if ( false === $comma || ! preg_match( '/^data:image\/svg\+xml(?:;[^,]*)*$/i', $header ) ) { return new WP_Error( 'svg_invalid_data_uri', __( 'Only image/svg+xml data URIs are accepted.', 'advanced-animation' ), array( 'status' => 400 ) ); } $payload = substr( $source, $comma + 1 ); if ( preg_match( '/;base64/i', $header ) ) { $decoded = base64_decode( preg_replace( '/\s+/', '', $payload ), true ); } else { $decoded = rawurldecode( $payload ); } if ( false === $decoded || '' === trim( (string) $decoded ) ) { return new WP_Error( 'svg_invalid_payload', __( 'The SVG data URI is invalid.', 'advanced-animation' ), array( 'status' => 400 ) ); } return (string) $decoded; } private static function sanitize_svg( string $svg, array &$dimensions ) { if ( ! class_exists( 'DOMDocument' ) ) { return new WP_Error( 'svg_parser_unavailable', __( 'SVG validation is unavailable on this server.', 'advanced-animation' ), array( 'status' => 500 ) ); } if ( preg_match( '/ 400 ) ); } $previous = libxml_use_internal_errors( true ); $dom = new \DOMDocument(); $loaded = $dom->loadXML( $svg, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING ); $errors = libxml_get_errors(); libxml_clear_errors(); libxml_use_internal_errors( $previous ); if ( ! $loaded || ! empty( $errors ) || ! $dom->documentElement || 'svg' !== strtolower( $dom->documentElement->tagName ) ) { return new WP_Error( 'svg_invalid_xml', __( 'The SVG document is invalid.', 'advanced-animation' ), array( 'status' => 400 ) ); } $validation = self::validate_node( $dom->documentElement ); if ( is_wp_error( $validation ) ) { return $validation; } $dimensions = self::svg_dimensions( $dom->documentElement ); $serialized = $dom->saveXML( $dom->documentElement ); return preg_replace( '/^<\?xml[^>]+>\s*/', '', (string) $serialized ); } /** SVGs are not rasterized by WordPress, so supply their intrinsic dimensions. */ private static function svg_dimensions( \DOMElement $root ): array { $length = static function ( string $value ): float { if ( ! preg_match( '/^([0-9]+(?:\.[0-9]*)?|\.[0-9]+)(px|pt|pc|in|cm|mm|q)?$/i', trim( $value ), $match ) ) { return 0; } $units = array( '' => 1, 'px' => 1, 'pt' => 96 / 72, 'pc' => 16, 'in' => 96, 'cm' => 96 / 2.54, 'mm' => 96 / 25.4, 'q' => 96 / 101.6 ); return (float) $match[1] * $units[ strtolower( $match[2] ?? '' ) ]; }; $width = $length( $root->getAttribute( 'width' ) ); $height = $length( $root->getAttribute( 'height' ) ); $box = preg_split( '/[\s,]+/', trim( $root->getAttribute( 'viewBox' ) ) ); if ( 4 === count( $box ) && count( array_filter( $box, 'is_numeric' ) ) === 4 && (float) $box[2] > 0 && (float) $box[3] > 0 ) { $ratio = (float) $box[2] / (float) $box[3]; if ( is_finite( $ratio ) && $ratio > 0 ) { if ( $width <= 0 && $height > 0 ) { $width = $height * $ratio; } if ( $height <= 0 && $width > 0 ) { $height = $width / $ratio; } if ( $width <= 0 && $height <= 0 ) { $width = (float) $box[2]; $height = (float) $box[3]; } } } return array( 'width' => is_finite( $width ) && $width > 0 && $width < PHP_INT_MAX ? max( 1, (int) round( $width ) ) : 300, 'height' => is_finite( $height ) && $height > 0 && $height < PHP_INT_MAX ? max( 1, (int) round( $height ) ) : 150, ); } private static function ensure_dimensions( int $attachment_id, array $dimensions ): void { $metadata = wp_get_attachment_metadata( $attachment_id ); $metadata = is_array( $metadata ) ? $metadata : array(); if ( ! empty( $metadata['width'] ) && ! empty( $metadata['height'] ) ) { return; } $metadata = array_merge( $metadata, $dimensions ); $metadata['file'] = get_post_meta( $attachment_id, '_wp_attached_file', true ); wp_update_attachment_metadata( $attachment_id, $metadata ); } private static function validate_node( \DOMElement $node ) { $tag = strtolower( $node->tagName ); if ( ! in_array( $tag, self::ALLOWED_TAGS, true ) ) { return new WP_Error( 'svg_unsafe_element', __( 'The SVG contains an unsupported element.', 'advanced-animation' ), array( 'status' => 400, 'element' => $tag ) ); } foreach ( iterator_to_array( $node->attributes ) as $attribute ) { $name = $attribute->name; $value = (string) $attribute->value; if ( 0 === stripos( $name, 'on' ) || ! in_array( $name, self::ALLOWED_ATTRIBUTES, true ) ) { return new WP_Error( 'svg_unsafe_attribute', __( 'The SVG contains an unsupported attribute.', 'advanced-animation' ), array( 'status' => 400, 'attribute' => $name ) ); } if ( preg_match( '/(?:javascript|vbscript|data:text|expression\s*\()/i', $value ) ) { return new WP_Error( 'svg_unsafe_value', __( 'The SVG contains an unsafe attribute value.', 'advanced-animation' ), array( 'status' => 400, 'attribute' => $name ) ); } if ( in_array( strtolower( $name ), array( 'href', 'xlink:href' ), true ) && preg_match( '#^(?:https?:|//|data:)#i', trim( $value ) ) ) { return new WP_Error( 'svg_external_reference', __( 'External SVG references are not allowed.', 'advanced-animation' ), array( 'status' => 400, 'attribute' => $name ) ); } if ( 'filter' === strtolower( $name ) && ! preg_match( '/^(?:none|url\(\s*(["\']?)#[^\s"\'()]+\1\s*\))$/i', trim( $value ) ) ) { return new WP_Error( 'svg_external_filter_reference', __( 'Only local SVG filter references are allowed.', 'advanced-animation' ), array( 'status' => 400, 'attribute' => $name ) ); } if ( 'style' === strtolower( $name ) && preg_match( '#url\(\s*["\']?(?:https?:|//|data:)#i', $value ) ) { return new WP_Error( 'svg_external_style_reference', __( 'External SVG style references are not allowed.', 'advanced-animation' ), array( 'status' => 400, 'attribute' => $name ) ); } } foreach ( $node->childNodes as $child ) { if ( $child instanceof \DOMElement ) { $result = self::validate_node( $child ); if ( is_wp_error( $result ) ) { return $result; } } elseif ( ! in_array( $child->nodeType, array( XML_TEXT_NODE, XML_CDATA_SECTION_NODE ), true ) ) { return new WP_Error( 'svg_unsafe_node', __( 'The SVG contains an unsupported node.', 'advanced-animation' ), array( 'status' => 400 ) ); } } return true; } }