Hosts that refused or timed out during this request. */
private static $failed_hosts = [];
/** @var float Seconds spent downloading during this request. */
private static $time_spent = 0.0;
/**
* @param string $url Image URL taken from the export file.
* @param int $parent_id Post the new attachment is attached to.
* @return int Attachment ID, or 0 when the image could not be imported.
*/
public static function import_image_from_url( $url, $parent_id = 0 ) {
$url = esc_url_raw( trim( (string) $url ), [ 'http', 'https' ] );
if ( empty( $url ) ) {
return 0;
}
// Same site (or already imported once): reuse the existing attachment.
$attachment_id = attachment_url_to_postid( $url );
if ( $attachment_id && wp_attachment_is_image( $attachment_id ) ) {
return (int) $attachment_id;
}
$attachment_id = self::get_previously_imported( $url );
if ( $attachment_id ) {
return $attachment_id;
}
$host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
$budget = (float) apply_filters( 'academy/import/media_time_budget', self::TIME_BUDGET );
if ( isset( self::$failed_hosts[ $host ] ) || self::$time_spent >= $budget ) {
return 0;
}
if ( ! function_exists( 'media_sideload_image' ) ) {
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
}
$cap_timeout = function ( $args ) {
$args['timeout'] = min( (float) ( $args['timeout'] ?? self::HTTP_TIMEOUT ), self::HTTP_TIMEOUT );
return $args;
};
add_filter( 'http_request_args', $cap_timeout );
$started = microtime( true );
$attachment_id = media_sideload_image( $url, absint( $parent_id ), null, 'id' );
self::$time_spent += microtime( true ) - $started;
remove_filter( 'http_request_args', $cap_timeout );
if ( is_wp_error( $attachment_id ) || ! $attachment_id ) {
// Connection-level failure: the host is down, skip its other images. A 404 only loses this one.
if ( is_wp_error( $attachment_id ) && 'http_request_failed' === $attachment_id->get_error_code() ) {
self::$failed_hosts[ $host ] = true;
}
return 0;
}
update_post_meta( $attachment_id, self::SOURCE_URL_META, $url );
return (int) $attachment_id;
}
/**
* @param int $post_id
* @param string $url
* @return int Attachment ID set as the thumbnail, or 0.
*/
public static function set_post_thumbnail_from_url( $post_id, $url ) {
$attachment_id = self::import_image_from_url( $url, $post_id );
if ( $attachment_id && set_post_thumbnail( $post_id, $attachment_id ) ) {
return $attachment_id;
}
return 0;
}
/**
* Imports images that an HTML body hotlinks from another site, and points
* the
tags (and image block ids) at the local copies.
* Images that are already local or cannot be imported are left untouched.
*
* @param string $content Sanitized HTML.
* @param int $parent_id Post the new attachments are attached to.
* @return string
*/
public static function import_content_images( $content, $parent_id = 0 ) {
if ( ! is_string( $content ) || false === stripos( $content, '
next_tag( 'img' ) ) {
$src = $processor->get_attribute( 'src' );
if ( ! is_string( $src ) || self::is_local_url( $src ) ) {
continue;
}
$attachment_id = self::import_image_from_url( $src, $parent_id );
if ( ! $attachment_id ) {
continue;
}
$processor->set_attribute( 'src', wp_get_attachment_url( $attachment_id ) );
// srcset/sizes still list the source site's resized copies.
$processor->remove_attribute( 'srcset' );
$processor->remove_attribute( 'sizes' );
$class = $processor->get_attribute( 'class' );
if ( is_string( $class ) && preg_match( '/\bwp-image-(\d+)\b/', $class, $matches ) ) {
$id_map[ (int) $matches[1] ] = $attachment_id;
$processor->set_attribute( 'class', preg_replace( '/\bwp-image-\d+\b/', 'wp-image-' . $attachment_id, $class ) );
}
}//end while
$content = $processor->get_updated_html();
if ( $id_map ) {
// Keep `` in sync so the block editor doesn't flag the block.
$content = preg_replace_callback(
'/(