*/ final class Optml_Tag_Replacer extends Optml_App_Replacer { use Optml_Normalizer; use Optml_Validator; /** * Cached object instance. * * @var Optml_Tag_Replacer */ protected static $instance = null; /** * Class instance method. * * @codeCoverageIgnore * @static * @since 1.0.0 * @access public * @return Optml_Tag_Replacer */ public static function instance() { if ( null === self::$instance ) { self::$instance = new self(); add_action( 'optml_replacer_setup', [ self::$instance, 'init' ] ); } return self::$instance; } /** * The initialize method. */ public function init() { parent::init(); add_filter( 'optml_content_images_tags', [ $this, 'process_image_tags' ], 1, 2 ); if ( $this->settings->use_lazyload() ) { return; } add_filter( 'optml_tag_replace', [ $this, 'regular_tag_replace' ], 1, 6 ); add_filter( 'image_downsize', [ $this, 'filter_image_downsize' ], PHP_INT_MAX, 3 ); add_filter( 'wp_calculate_image_srcset', [ $this, 'filter_srcset_attr' ], PHP_INT_MAX, 5 ); add_filter( 'wp_calculate_image_sizes', [ $this, 'filter_sizes_attr' ], 1, 2 ); } /** * Called by hook to replace image tags in content. * * @param string $content The content to process. * @param array $images List of image tags. * * @return mixed */ /** * Replace in given content the given image tags with video tags is image is gif * * @param string $image_url Image url to process. * @param string $image_tag The tag to replace. * @param string $content The content to process. * * @return bool */ public function img_to_video( $image_url, $image_tag, &$content ) { if ( $this->settings->get( 'img_to_video' ) === 'disabled' ) { return false; } if ( ! $this->is_valid_gif( $image_url ) ) { return false; } $link_mp4 = apply_filters( 'optml_content_url', $image_url, ['width' => 'auto', 'height' => 'auto', 'format' => 'mp4', ] ); $link_png = apply_filters( 'optml_content_url', $image_url, ['width' => 'auto', 'height' => 'auto', 'quality' => 'eco', ] ); $video_tag = $image_tag; $video_tag = str_replace( [ 'src=', '', ], [ 'original-src=', '', ], $video_tag ); $content = str_replace( $image_tag, $video_tag, $content ); return true; } /** * Method invoked by `optml_content_images_tags` filter. * * @param string $content The content to be processed. * @param array $images A list of images. * * @return mixed */ public function process_image_tags( $content, $images = [] ) { $image_sizes = self::image_sizes(); $sizes2crop = self::size_to_crop(); foreach ( $images[0] as $index => $tag ) { $width = $height = false; $crop = null; $image_tag = $images['img_tag'][ $index ]; $is_slashed = strpos( $images['img_url'][ $index ], '\/' ) !== false; $src = $tmp = $is_slashed ? $this->strip_slashes( $images['img_url'][ $index ] ) : $images['img_url'][ $index ]; if ( strpos( $src, $this->upload_resource['content_path'] ) === 0 ) { $src = $tmp = untrailingslashit( $this->upload_resource['content_host'] ) . $src; $new_src = $is_slashed ? addcslashes( $src, '/' ) : $src; $image_tag = str_replace( [ '"' . $images['img_url'][ $index ], "'" . $images['img_url'][ $index ], ], [ '"' . $new_src, "'" . $new_src, ], $image_tag ); $images['img_url'][ $index ] = $new_src; } if ( ( apply_filters( 'optml_ignore_image_link', false, $src ) || false !== strpos( $src, Optml_Config::$service_url ) || ! $this->can_replace_url( $src ) || ! $this->can_replace_tag( $images['img_url'][ $index ], $tag ) ) && ! Optml_Media_Offload::is_not_processed_image( $src ) ) { continue; // @codeCoverageIgnore } if ( $this->img_to_video( $images['img_url'][ $index ], $images['img_tag'][ $index ], $content ) ) { continue; } $resize = apply_filters( 'optml_default_crop', [] ); list( $width, $height, $resize ) = self::parse_dimensions_from_tag( $images['img_tag'][ $index ], $image_sizes, [ 'width' => $width, 'height' => $height, 'resize' => $resize, ] ); if ( false === $width && false === $height ) { list( $width, $height, $crop ) = $this->parse_dimensions_from_filename( $tmp ); } if ( empty( $resize ) && isset( $sizes2crop[ $width . $height ] ) ) { $resize = $this->to_optml_crop( $sizes2crop[ $width . $height ] ); } elseif ( $crop === true ) { $resize = $this->to_optml_crop( $crop ); } $optml_args = [ 'width' => $width, 'height' => $height, 'resize' => $resize ]; $tmp = $this->strip_image_size_from_url( $tmp ); $new_url = apply_filters( 'optml_content_url', $tmp, $optml_args ); if ( $new_url === $tmp && ! Optml_Media_Offload::is_not_processed_image( $src ) ) { continue; // @codeCoverageIgnore } $image_tag = str_replace( [ 'width="' . $width . '"', 'width=\"' . $width . '\"', 'height="' . $height . '"', 'height=\"' . $height . '\"', ], [ 'width="' . $optml_args['width'] . '"', 'width=\"' . $optml_args['width'] . '\"', 'height="' . $optml_args['height'] . '"', 'height=\"' . $optml_args['height'] . '\"', ], $image_tag ); // If the image is in header or has a class excluded from lazyload, we need to do the regular replace. if ( $images['in_header'][ $index ] ) { $image_tag = $this->regular_tag_replace( $image_tag, $images['img_url'][ $index ], $new_url, $optml_args, $is_slashed, $tag ); } else { $image_tag = apply_filters( 'optml_tag_replace', $image_tag, $images['img_url'][ $index ], $new_url, $optml_args, $is_slashed, $tag ); } $content = str_replace( $images['img_tag'][ $index ], $image_tag, $content ); } return $content; } /** * Check replacement is allowed for this tag. * * @param string $url Url. * @param string $tag Html tag. * * @return bool We can replace? */ public function can_replace_tag( $url, $tag = '' ) { foreach ( self::possible_tag_flags() as $banned_string ) { if ( strpos( $tag, $banned_string ) !== false ) { self::$ignored_url_map[ crc32( $url ) ] = true; return false; } } return true; } /** * Extract image dimensions from img tag. * * @param string $tag The HTML img tag. * @param array $image_sizes WordPress supported image sizes. * @param array $args Default args to use. * * @return array */ private function parse_dimensions_from_tag( $tag, $image_sizes, $args = [] ) { if ( preg_match( '#width=["|\']?([\d%]+)["|\']?#i', $tag, $width_string ) ) { $args['width'] = $width_string[1]; } if ( preg_match( '#height=["|\']?([\d%]+)["|\']?#i', $tag, $height_string ) ) { $args['height'] = $height_string[1]; } if ( preg_match( '#class=["|\']?[^"\']*size-([^"\'\s]+)[^"\']*["|\']?#i', $tag, $size ) ) { $size = array_pop( $size ); if ( false === $args['width'] && false === $args['height'] && 'full' !== $size && array_key_exists( $size, $image_sizes ) ) { $args['width'] = (int) $image_sizes[ $size ]['width']; $args['height'] = (int) $image_sizes[ $size ]['height']; } if ( 'full' !== $size && array_key_exists( $size, $image_sizes ) ) { $args['resize'] = $this->to_optml_crop( $image_sizes[ $size ]['crop'] ); } } else { $args['resize'] = apply_filters( 'optml_parse_resize_from_tag', [], $tag ); } return [ $args['width'], $args['height'], $args['resize'] ]; } /** * Replaces the tags by default. * * @param string $new_tag The new tag. * @param string $original_url The original URL. * @param string $new_url The optimized URL. * @param array $optml_args Options passed for URL optimization. * @param bool $is_slashed Url needs to slashed. * @param string $full_tag Full tag, wrapper included. * * @return string */ public function regular_tag_replace( $new_tag, $original_url, $new_url, $optml_args, $is_slashed = false, $full_tag = '' ) { $pattern = '/(?to_optml_crop( $sizes[ $size_array[0] . $size_array[1] ] ) : null; } foreach ( $sources as $i => $source ) { $url = $source['url']; if ( Optml_Media_Offload::is_uploaded_image( $url ) ) { continue; } list( $width, $height, $file_crop ) = $this->parse_dimensions_from_filename( $url ); if ( empty( $width ) ) { $width = $image_meta['width']; } if ( empty( $height ) ) { $height = $image_meta['height']; } if ( $original_url === null ) { if ( ! empty( $attachment_id ) ) { $original_url = wp_get_attachment_url( $attachment_id ); } else { $original_url = $this->strip_image_size_from_url( $source['url'] ); } } $args = []; if ( 'w' === $source['descriptor'] ) { if ( $height && ( $source['value'] === $width ) ) { $args['width'] = $width; $args['height'] = $height; } else { $args['width'] = $source['value']; } } if ( $cropping !== null ) { $args['resize'] = $cropping; } else { $args['resize'] = $this->to_optml_crop( $file_crop ); } $sources[ $i ]['url'] = apply_filters( 'optml_content_url', $original_url, $args ); } return $sources; } /** * Filters sizes attribute of the images. * * @param array $sizes An array of media query breakpoints. * @param array $size Width and height of the image. * * @return mixed An array of media query breakpoints. */ public function filter_sizes_attr( $sizes, $size ) { if ( ! doing_filter( 'the_content' ) ) { return $sizes; } $content_width = false; if ( isset( $GLOBALS['content_width'] ) ) { $content_width = $GLOBALS['content_width']; } if ( ! $content_width ) { $content_width = 1000; } if ( is_array( $size ) && $size[0] < $content_width ) { return $sizes; } return sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $content_width ); } /** * This filter will replace all the images retrieved via "wp_get_image" type of functions. * * @param array $image The filtered value. * @param int $attachment_id The related attachment id. * @param array|string $size This could be the name of the thumbnail size or an array of custom dimensions. * * @return array */ public function filter_image_downsize( $image, $attachment_id, $size ) { $image_url = wp_get_attachment_url( $attachment_id ); if ( Optml_Media_Offload::is_uploaded_image( $image_url ) ) { return $image; } if ( $image_url === false ) { return $image; } $image_meta = wp_get_attachment_metadata( $attachment_id ); $image_args = self::image_sizes(); // default size $sizes = [ 'width' => isset( $image_meta['width'] ) ? intval( $image_meta['width'] ) : false, 'height' => isset( $image_meta['height'] ) ? intval( $image_meta['height'] ) : false, ]; switch ( $size ) { case is_array( $size ): $width = isset( $size[0] ) ? (int) $size[0] : false; $height = isset( $size[1] ) ? (int) $size[1] : false; if ( ! $width || ! $height ) { break; } $image_resized = image_resize_dimensions( $sizes['width'], $sizes['height'], $width, $height ); if ( $image_resized ) { $width = $image_resized[6]; $height = $image_resized[7]; } else { $width = $image_meta['width']; $height = $image_meta['height']; } list( $sizes['width'], $sizes['height'] ) = image_constrain_size_for_editor( $width, $height, $size ); break; case 'full' !== $size && isset( $image_args[ $size ] ): $image_resized = image_resize_dimensions( $sizes['width'], $sizes['height'], $image_args[ $size ]['width'], $image_args[ $size ]['height'], $image_args[ $size ]['crop'] ); if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image. $sizes['width'] = $image_resized[6]; $sizes['height'] = $image_resized[7]; } list( $sizes['width'], $sizes['height'] ) = image_constrain_size_for_editor( $sizes['width'], $sizes['height'], $size, 'display' ); $sizes['resize'] = $this->to_optml_crop( $image_args[ $size ]['crop'] ); break; } $image_url = $this->strip_image_size_from_url( $image_url ); $new_url = apply_filters( 'optml_content_url', $image_url, $sizes ); if ( $new_url === $image_url ) { return $image; } return [ $new_url, $sizes['width'], $sizes['height'], $size === 'full', ]; } /** * Throw error on object clone * * The whole idea of the singleton design pattern is that there is a single * object therefore, we don't want the object to be cloned. * * @codeCoverageIgnore * @access public * @since 1.0.0 * @return void */ public function __clone() { // Cloning instances of the class is forbidden. _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); } /** * Disable unserializing of the class * * @codeCoverageIgnore * @access public * @since 1.0.0 * @return void */ public function __wakeup() { // Unserializing instances of the class is forbidden. _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); } }