OwlCyberSecurity - MANAGER
Edit File: class-amp-image-dimension-extractor.php
<?php /** * Class AMP_Image_Dimension_Extractor * * @package AMP */ /** * Class with static methods to extract image dimensions. * * @internal */ class AMP_Image_Dimension_Extractor { const STATUS_FAILED_LAST_ATTEMPT = 'failed'; const STATUS_IMAGE_EXTRACTION_FAILED = 'failed'; /** * Internal flag whether callbacks have been registered. * * @var bool */ private static $callbacks_registered = false; /** * Extracts dimensions from image URLs. * * @since 0.2 * * @param array|string $urls Array of URLs to extract dimensions from, or a single URL string. * @return array|string Extracted dimensions keyed by original URL, or else the single set of dimensions if one URL string is passed. */ public static function extract( $urls ) { if ( ! self::$callbacks_registered ) { self::register_callbacks(); } $return_dimensions = []; // Back-compat for users calling this method directly. $is_single = is_string( $urls ); if ( $is_single ) { $urls = [ $urls ]; } // Normalize URLs and also track a map of normalized-to-original as we'll need it to reformat things when returning the data. $url_map = []; $normalized_urls = []; foreach ( $urls as $original_url ) { $normalized_url = self::normalize_url( $original_url ); if ( false !== $normalized_url ) { $url_map[ $original_url ] = $normalized_url; $normalized_urls[] = $normalized_url; } else { // This is not a URL we can extract dimensions from, so default to false. $return_dimensions[ $original_url ] = false; } } $extracted_dimensions = array_fill_keys( $normalized_urls, false ); /** * Filters the dimensions extracted from image URLs. * * @since 0.5.1 * * @param array $extracted_dimensions Extracted dimensions, initially mapping images URLs to false. */ $extracted_dimensions = apply_filters( 'amp_extract_image_dimensions_batch', $extracted_dimensions ); // We need to return a map with the original (un-normalized URL) as we that to match nodes that need dimensions. foreach ( $url_map as $original_url => $normalized_url ) { $return_dimensions[ $original_url ] = $extracted_dimensions[ $normalized_url ]; } // Back-compat: just return the dimensions, not the full mapped array. if ( $is_single ) { return current( $return_dimensions ); } return $return_dimensions; } /** * Normalizes the given URL. * * This method ensures the URL has a scheme and, if relative, is prepended the WordPress site URL. * * @param string $url URL to normalize. * @return string|false Normalized URL, or false if normalization failed. */ public static function normalize_url( $url ) { if ( empty( $url ) ) { return false; } if ( 0 === strpos( $url, 'data:' ) ) { return false; } $normalized_url = $url; if ( 0 === strpos( $url, '//' ) ) { $normalized_url = set_url_scheme( $url, 'http' ); } else { $parsed = wp_parse_url( $url ); if ( ! isset( $parsed['host'] ) ) { $path = ''; if ( isset( $parsed['path'] ) ) { $path .= $parsed['path']; } if ( isset( $parsed['query'] ) ) { $path .= '?' . $parsed['query']; } $home = home_url(); $home_path = wp_parse_url( $home, PHP_URL_PATH ); if ( ! empty( $home_path ) ) { $home = substr( $home, 0, - strlen( $home_path ) ); } $normalized_url = $home . $path; } } /** * Apply filters on the normalized image URL for dimension extraction. * * @since 1.1 * * @param string $normalized_url Normalized image URL. * @param string $url Original image URL. */ $normalized_url = apply_filters( 'amp_normalized_dimension_extractor_image_url', $normalized_url, $url ); return $normalized_url; } /** * Registers the necessary callbacks. */ private static function register_callbacks() { self::$callbacks_registered = true; add_filter( 'amp_extract_image_dimensions_batch', [ __CLASS__, 'extract_by_filename_or_filesystem' ], 100 ); add_filter( 'amp_extract_image_dimensions_batch', [ __CLASS__, 'extract_by_downloading_images' ], 999, 1 ); /** * Fires after the amp_extract_image_dimensions_batch filter has been added to extract by downloading images. * * @since 0.5.1 */ do_action( 'amp_extract_image_dimensions_batch_callbacks_registered' ); } /** * Extract dimensions from filename if dimension exists or from file system. * * @param array $dimensions Image urls mapped to dimensions. * @return array Dimensions mapped to image urls, or false if they could not be retrieved */ public static function extract_by_filename_or_filesystem( $dimensions ) { if ( empty( $dimensions ) || ! is_array( $dimensions ) ) { return []; } $using_ext_object_cache = wp_using_ext_object_cache(); $ext_types = wp_get_ext_types(); if ( empty( $ext_types['image'] ) ) { return $dimensions; } $image_ext_types = $ext_types['image']; unset( $ext_types ); $upload_dir = wp_get_upload_dir(); $base_upload_uri = trailingslashit( $upload_dir['baseurl'] ); $base_upload_dir = trailingslashit( $upload_dir['basedir'] ); foreach ( $dimensions as $url => $value ) { // Check whether some other callback attached to the filter already provided dimensions for this image. if ( ! empty( $value ) && is_array( $value ) ) { continue; } $url_without_query_fragment = strtok( $url, '?#' ); // Parse info out of the URL, including the file extension and (optionally) the dimensions. if ( ! preg_match( '/(?:-(?<width>[1-9][0-9]*)x(?<height>[1-9][0-9]*))?\.(?<ext>\w+)$/', $url_without_query_fragment, $matches ) ) { continue; } // Skip images don't have recognized extensions. if ( ! in_array( strtolower( $matches['ext'] ), $image_ext_types, true ) ) { continue; } // Use image dimension from the file name. if ( ! empty( $matches['width'] ) && ! empty( $matches['height'] ) ) { $dimensions[ $url ] = [ 'width' => (int) $matches['width'], 'height' => (int) $matches['height'], ]; continue; } // Verify that the URL is for an uploaded file. if ( 0 !== strpos( $url_without_query_fragment, $base_upload_uri ) ) { continue; } $upload_relative_path = substr( $url_without_query_fragment, strlen( $base_upload_uri ) ); // Bail if the URL contains relative paths. if ( 0 !== validate_file( $upload_relative_path ) ) { continue; } // Get image dimension from file system. $image_file = $base_upload_dir . $upload_relative_path; $image_size = []; list( $transient_name ) = self::get_transient_names( $url ); // When using an external object cache, try to first see if dimensions have already been obtained. This is // not done for a non-external object cache (i.e. when wp_options is used for transients) because then // we are not storing the dimensions in a transient, because it is more performant to read the dimensions // from the filesystem--both in terms of time and storage--than to store dimensions in wp_options. if ( $using_ext_object_cache ) { $image_size = get_transient( $transient_name ); $image_size = ( ! empty( $image_size ) && is_array( $image_size ) ) ? $image_size : []; } if ( empty( $image_size ) && file_exists( $image_file ) ) { if ( function_exists( 'wp_getimagesize' ) ) { $image_size = wp_getimagesize( $image_file ); } elseif ( function_exists( 'getimagesize' ) ) { $image_size = @getimagesize( $image_file ); // phpcs:ignore WordPress.PHP.NoSilencedErrors } if ( $using_ext_object_cache && ! empty( $image_size ) && is_array( $image_size ) ) { set_transient( $transient_name, $image_size ); } } if ( ! empty( $image_size ) && is_array( $image_size ) ) { $dimensions[ $url ] = [ 'width' => (int) $image_size[0], 'height' => (int) $image_size[1], ]; } } return $dimensions; } /** * Get transient names. * * @param string $url Image URL. * @return array { * @type string $0 Transient name for storing dimensions. * @type string $1 Transient name for image fetching lock. * } */ private static function get_transient_names( $url ) { $url_hash = md5( $url ); return [ sprintf( 'amp_img_%s', $url_hash ), sprintf( 'amp_lock_%s', $url_hash ), ]; } /** * Extract dimensions from downloaded images (or transient/cached dimensions from downloaded images) * * @param array $dimensions Image urls mapped to dimensions. * @param bool $mode Deprecated. * @return array Dimensions mapped to image urls, or false if they could not be retrieved */ public static function extract_by_downloading_images( $dimensions, $mode = false ) { if ( $mode ) { _deprecated_argument( __METHOD__, 'AMP 1.1' ); } $transient_expiration = 30 * DAY_IN_SECONDS; $urls_to_fetch = []; $images = []; self::determine_which_images_to_fetch( $dimensions, $urls_to_fetch ); try { self::fetch_images( $urls_to_fetch, $images ); self::process_fetched_images( $urls_to_fetch, $images, $dimensions, $transient_expiration ); } catch ( Exception $exception ) { trigger_error( esc_html( $exception->getMessage() ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error } return $dimensions; } /** * Determine which images to fetch by checking for dimensions in transient/cache. * Creates a short lived transient that acts as a semaphore so that another visitor * doesn't trigger a remote fetch for the same image at the same time. * * @param array $dimensions Image urls mapped to dimensions. * @param array $urls_to_fetch Urls of images to fetch because dimensions are not in transient/cache. */ private static function determine_which_images_to_fetch( &$dimensions, &$urls_to_fetch ) { foreach ( $dimensions as $url => $value ) { // Check whether some other callback attached to the filter already provided dimensions for this image. if ( is_array( $value ) || empty( $url ) ) { continue; } list( $transient_name, $transient_lock_name ) = self::get_transient_names( $url ); $cached_dimensions = get_transient( $transient_name ); // If we're able to retrieve the dimensions from a transient, set them and move on. if ( is_array( $cached_dimensions ) ) { $dimensions[ $url ] = [ 'width' => $cached_dimensions[0], 'height' => $cached_dimensions[1], ]; continue; } // If the value in the transient reflects we couldn't get dimensions for this image the last time we tried, move on. if ( self::STATUS_FAILED_LAST_ATTEMPT === $cached_dimensions ) { $dimensions[ $url ] = false; continue; } // If somebody is already trying to extract dimensions for this transient right now, move on. if ( false !== get_transient( $transient_lock_name ) ) { $dimensions[ $url ] = false; continue; } // Include the image as a url to fetch. $urls_to_fetch[ $url ] = []; $urls_to_fetch[ $url ]['url'] = $url; $urls_to_fetch[ $url ]['transient_name'] = $transient_name; $urls_to_fetch[ $url ]['transient_lock_name'] = $transient_lock_name; set_transient( $transient_lock_name, 1, MINUTE_IN_SECONDS ); } } /** * Fetch dimensions of remote images * * @throws Exception When cURL handle cannot be added. * * @param array $urls_to_fetch Image src urls to fetch. * @param array $images Array to populate with results of image/dimension inspection. */ private static function fetch_images( $urls_to_fetch, &$images ) { $urls = array_keys( $urls_to_fetch ); $client = new \FasterImage\FasterImage(); /** * Filters the user agent for obtaining the image dimensions. * * @param string $user_agent User agent. */ $client->setUserAgent( apply_filters( 'amp_extract_image_dimensions_get_user_agent', self::get_default_user_agent() ) ); $client->setBufferSize( 1024 ); $client->setSslVerifyHost( true ); $client->setSslVerifyPeer( true ); $images = $client->batch( $urls ); } /** * Determine success or failure of remote fetch, integrate fetched dimensions into url to dimension mapping, * cache fetched dimensions via transient and release/delete semaphore transient * * @param array $urls_to_fetch List of image urls that were fetched and transient names corresponding to each (for unlocking semaphore, setting "real" transient). * @param array $images Results of remote fetch mapping fetched image url to dimensions. * @param array $dimensions Map of image url to dimensions to be updated with results of remote fetch. * @param int $transient_expiration Duration image dimensions should exist in transient/cache. */ private static function process_fetched_images( $urls_to_fetch, $images, &$dimensions, $transient_expiration ) { foreach ( $urls_to_fetch as $url_data ) { $image_data = $images[ $url_data['url'] ]; if ( self::STATUS_IMAGE_EXTRACTION_FAILED === $image_data['size'] ) { $dimensions[ $url_data['url'] ] = false; set_transient( $url_data['transient_name'], self::STATUS_FAILED_LAST_ATTEMPT, $transient_expiration ); } else { $dimensions[ $url_data['url'] ] = [ 'width' => $image_data['size'][0], 'height' => $image_data['size'][1], ]; set_transient( $url_data['transient_name'], [ $image_data['size'][0], $image_data['size'][1], ], $transient_expiration ); } delete_transient( $url_data['transient_lock_name'] ); } } /** * Get default user agent * * @return string */ public static function get_default_user_agent() { return 'amp-wp, v' . AMP__VERSION . ', ' . home_url(); } }