'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; } $svg = $this->sanitize_svg( $decoded ); if ( is_wp_error( $svg ) ) { return $svg; } if ( strlen( $svg ) > wp_max_upload_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 ) { return rest_ensure_response( 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'; $upload = wp_upload_bits( $filename, null, $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 ) ) { 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 ); return rest_ensure_response( 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 function sanitize_svg( string $svg ) { 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 = $this->validate_node( $dom->documentElement ); if ( is_wp_error( $validation ) ) { return $validation; } $serialized = $dom->saveXML( $dom->documentElement ); return preg_replace( '/^<\?xml[^>]+>\s*/', '', (string) $serialized ); } private 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 ( '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 = $this->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; } }