'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', 'image/avif' => 'avif', ); public function __construct() { add_action( 'rest_api_init', array( $this, 'routes' ) ); } public function routes(): void { register_rest_route( 'advanced-animation/v1', '/elementor/import-media', array( 'methods' => 'POST', 'permission_callback' => array( $this, 'permission' ), 'callback' => array( $this, 'import' ), ) ); } public function permission( WP_REST_Request $request ) { 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 import( WP_REST_Request $request ) { $source = $this->resolve_source_attachment( $request ); if ( is_wp_error( $source ) ) { return $source; } $url = $source ? $source['url'] : $request->get_param( 'url' ); $local_host = $source ? ( $source['local_image'] ? $source['local_host'] : null ) : $this->local_upload_host( $url ); if ( ! is_string( $url ) || strlen( $url ) > 2048 || ! preg_match( '#^https?://#i', $url ) || ( ! $local_host && ! wp_http_validate_url( $url ) ) ) { return new WP_Error( 'invalid_image_url', __( 'A public HTTP image URL is required.', 'advanced-animation' ), array( 'status' => 400 ) ); } if ( ! function_exists( 'wp_tempnam' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } if ( ! function_exists( 'media_handle_sideload' ) ) { require_once ABSPATH . 'wp-admin/includes/media.php'; } if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) { require_once ABSPATH . 'wp-admin/includes/image.php'; } $temp_file = wp_tempnam( 'elementor-image' ); if ( ! $temp_file ) { return new WP_Error( 'image_temp_file_failed', __( 'Could not create a temporary image file.', 'advanced-animation' ), array( 'status' => 500 ) ); } $max_size = (int) wp_max_upload_size(); if ( $max_size <= 0 ) { $max_size = 20 * 1024 * 1024; } try { // Safe HTTP validates every redirect and blocks private network hosts. $args = array( 'timeout' => 30, 'stream' => true, 'filename' => $temp_file, 'limit_response_size' => $max_size + 1, ); $response = $local_host ? $this->fetch_local_url( $url, $args, $local_host ) : wp_safe_remote_get( $url, $args ); if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { return new WP_Error( 'image_download_failed', __( 'The remote image could not be downloaded.', 'advanced-animation' ), array( 'status' => 502 ) ); } $size = filesize( $temp_file ); if ( false === $size || 0 === $size || $size > $max_size ) { return new WP_Error( 'image_size_invalid', __( 'The remote image is empty or exceeds the upload limit.', 'advanced-animation' ), array( 'status' => 413 ) ); } $mime = wp_get_image_mime( $temp_file ); if ( ! isset( self::MIME_EXTENSIONS[ $mime ] ) ) { // Validate the downloaded XML itself, regardless of its URL or MIME header. $xml = file_get_contents( $temp_file ); $svg = false === $xml ? null : Elementor_SVG_Media_Controller::store_svg( $xml ); if ( null === $svg || ( is_wp_error( $svg ) && 'svg_invalid_xml' === $svg->get_error_code() ) ) { return new WP_Error( 'image_type_invalid', __( 'The remote file is not a supported image.', 'advanced-animation' ), array( 'status' => 415 ) ); } if ( is_wp_error( $svg ) ) { return $svg; } return rest_ensure_response( array( 'id' => $svg['id'], 'source_url' => $svg['source_url'] ) ); } $path = wp_parse_url( $url, PHP_URL_PATH ); $basename = is_string( $path ) ? wp_basename( rawurldecode( $path ) ) : ''; $name = sanitize_file_name( pathinfo( $basename, PATHINFO_FILENAME ) ); if ( '' === $name ) { $name = 'elementor-image'; } $filename = $name . '.' . self::MIME_EXTENSIONS[ $mime ]; $id = media_handle_sideload( array( 'name' => $filename, 'type' => $mime, 'tmp_name' => $temp_file, 'size' => $size, 'error' => 0, ), 0 ); if ( is_wp_error( $id ) ) { return $id; } $media_url = wp_get_attachment_url( $id ); if ( ! $media_url ) { return new WP_Error( 'image_attachment_url_missing', __( 'The uploaded image has no URL.', 'advanced-animation' ), array( 'status' => 500 ) ); } return rest_ensure_response( array( 'id' => (int) $id, 'source_url' => $media_url ) ); } finally { if ( file_exists( $temp_file ) ) { wp_delete_file( $temp_file ); } } } /** Resolve an ID-only Elementor clipboard image on its source WordPress site. */ private function resolve_source_attachment( WP_REST_Request $request ) { $source_root = $request->get_param( 'source_rest_url' ); $source_id = $request->get_param( 'attachment_id' ); if ( null === $source_root && null === $source_id ) { return null; } if ( ! is_string( $source_root ) || strlen( $source_root ) > 2048 || ! is_numeric( $source_id ) || (int) $source_id < 1 ) { return new WP_Error( 'invalid_source_attachment', __( 'A source REST URL and attachment ID are required.', 'advanced-animation' ), array( 'status' => 400 ) ); } $parts = wp_parse_url( $source_root ); if ( ! is_array( $parts ) || ! in_array( $parts['scheme'] ?? '', array( 'http', 'https' ), true ) || empty( $parts['host'] ) || ! preg_match( '#/wp-json/?$#', $parts['path'] ?? '' ) || isset( $parts['user'] ) || isset( $parts['pass'] ) || isset( $parts['query'] ) || isset( $parts['fragment'] ) ) { return new WP_Error( 'invalid_source_rest_url', __( 'The source WordPress REST URL is invalid.', 'advanced-animation' ), array( 'status' => 400 ) ); } $local_source = 'local' === wp_get_environment_type() && current_user_can( 'manage_options' ) && (bool) preg_match( '/^[a-z0-9][a-z0-9-]*\.local$/i', $parts['host'] ) && ! isset( $parts['port'] ); $endpoint = rtrim( $source_root, '/' ) . '/wp/v2/media/' . (int) $source_id; if ( ! $local_source && ! wp_http_validate_url( $endpoint ) ) { return new WP_Error( 'invalid_source_rest_url', __( 'The source WordPress REST URL is not public.', 'advanced-animation' ), array( 'status' => 400 ) ); } $args = array( 'timeout' => 10, 'limit_response_size' => 1024 * 1024 ); $response = $local_source ? $this->fetch_local_url( $endpoint, $args, $parts['host'] ) : wp_safe_remote_get( $endpoint, $args ); if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { return new WP_Error( 'source_attachment_unavailable', __( 'The source image could not be found.', 'advanced-animation' ), array( 'status' => 502 ) ); } $media = json_decode( wp_remote_retrieve_body( $response ), true ); $url = is_array( $media ) && is_string( $media['source_url'] ?? null ) ? $media['source_url'] : ''; if ( ! $url || (int) ( $media['id'] ?? 0 ) !== (int) $source_id || 0 !== strpos( (string) ( $media['mime_type'] ?? '' ), 'image/' ) ) { return new WP_Error( 'source_attachment_invalid', __( 'The source media item is not an image.', 'advanced-animation' ), array( 'status' => 415 ) ); } $asset = wp_parse_url( $url ); $local_image = $local_source && is_array( $asset ) && ( $asset['scheme'] ?? '' ) === $parts['scheme'] && 0 === strcasecmp( $this->local_upload_host( $url ) ?? '', $parts['host'] ); return array( 'url' => $url, 'local_image' => $local_image, 'local_host' => $parts['host'] ); } /** Restrict private-host imports to uploads on a local WordPress site. */ private function local_upload_host( $url ): ?string { if ( 'local' !== wp_get_environment_type() || ! current_user_can( 'manage_options' ) || ! is_string( $url ) ) { return null; } $parts = wp_parse_url( $url ); $path = is_array( $parts ) ? ( $parts['path'] ?? '' ) : ''; if ( ! is_array( $parts ) || ! in_array( $parts['scheme'] ?? '', array( 'http', 'https' ), true ) || empty( $parts['host'] ) || ! preg_match( '/^[a-z0-9][a-z0-9-]*\.local$/i', $parts['host'] ) || isset( $parts['port'] ) || isset( $parts['user'] ) || isset( $parts['pass'] ) || false === strpos( $path, '/wp-content/uploads/' ) || preg_match( '#(?:^|/)\.\.(?:/|$)#', rawurldecode( $path ) ) ) { return null; } return $parts['host']; } /** Allow one exact .local URL through WordPress safe HTTP in local admin sessions. */ private function fetch_local_url( string $url, array $args, string $host ) { $allow = static function ( $external, $requested_host, $requested_url ) use ( $host, $url ) { return $external || ( 0 === strcasecmp( $requested_host, $host ) && $requested_url === $url ); }; add_filter( 'http_request_host_is_external', $allow, 10, 3 ); try { $args['redirection'] = 0; return wp_safe_remote_get( $url, $args ); } finally { remove_filter( 'http_request_host_is_external', $allow, 10 ); } } }