. * * @var array */ private $_vpi_preload_list = []; /** * The user-level next-gen format supported (''|webp|avif). * * @var string */ private $_format = ''; /** * The system-level chosen next-gen format (webp|avif). * * @var string */ private $_sys_format = ''; /** * Init. * * @since 1.4 */ public function __construct() { self::debug2( 'init' ); $this->_wp_upload_dir = wp_upload_dir(); if ( $this->conf( Base::O_IMG_OPTM_WEBP ) ) { $this->_sys_format = 'webp'; $this->_format = 'webp'; if ( 2 === $this->conf( Base::O_IMG_OPTM_WEBP ) ) { $this->_sys_format = 'avif'; $this->_format = 'avif'; } if ( ! $this->_browser_support_next_gen() ) { $this->_format = ''; } $this->_format = apply_filters( 'litespeed_next_gen_format', $this->_format ); } } /** * Hooks after user init. * * @since 7.2 * @since 7.4 Add media replace original with scaled. * @return void */ public function after_user_init() { // Hook to attachment delete action (PR#844, Issue#841) for AJAX del compatibility. add_action( 'delete_attachment', array( $this, 'delete_attachment' ), 11, 2 ); // For big images, allow to replace original with scaled image. if ( $this->conf( Base::O_MEDIA_AUTO_RESCALE_ORI ) ) { // Added priority 9 to happen before other functions added. add_filter( 'wp_update_attachment_metadata', array( $this, 'rescale_ori' ), 9, 2 ); } } /** * Init optm features. * * @since 3.0 * @access public * @return void */ public function init() { if ( is_admin() ) { return; } // Due to ajax call doesn't send correct accept header, have to limit webp to HTML only. if ( $this->webp_support() ) { // Hook to srcset. if ( function_exists( 'wp_calculate_image_srcset' ) ) { add_filter( 'wp_calculate_image_srcset', array( $this, 'webp_srcset' ), 988 ); } // Hook to mime icon // add_filter( 'wp_get_attachment_image_src', array( $this, 'webp_attach_img_src' ), 988 );// todo: need to check why not // add_filter( 'wp_get_attachment_url', array( $this, 'webp_url' ), 988 ); // disabled to avoid wp-admin display } if ( $this->conf( Base::O_MEDIA_LAZY ) && ! $this->cls( 'Metabox' )->setting( 'litespeed_no_image_lazy' ) ) { self::debug( 'Suppress default WP lazyload' ); add_filter( 'wp_lazy_loading_enabled', '__return_false' ); } /** * Replace gravatar. * * @since 3.0 */ $this->cls( 'Avatar' ); add_filter( 'litespeed_buffer_finalize', array( $this, 'finalize' ), 4 ); add_filter( 'litespeed_optm_html_head', array( $this, 'finalize_head' ) ); } /** * Handle attachment create (rescale original). * * @param array $metadata Current meta array. * @param int $attachment_id Attachment ID. * @return array Modified metadata. * @since 7.4 */ public function rescale_ori( $metadata, $attachment_id ) { // Test if create and image was resized. if ( $metadata && isset( $metadata['original_image'], $metadata['file'] ) && false !== strpos( $metadata['file'], '-scaled' ) ) { // Get rescaled file name. $path_exploded = explode( '/', strrev( $metadata['file'] ), 2 ); $rescaled_file_name = strrev( $path_exploded[0] ); // Create paths for images: resized and original. $base_path = $this->_wp_upload_dir['basedir'] . $this->_wp_upload_dir['subdir'] . '/'; $rescaled_path = $base_path . $rescaled_file_name; $new_path = $base_path . $metadata['original_image']; // Change array file key. $metadata['file'] = $this->_wp_upload_dir['subdir'] . '/' . $metadata['original_image']; if ( 0 === strpos( $metadata['file'], '/' ) ) { $metadata['file'] = substr( $metadata['file'], 1 ); } // Delete array "original_image" key. unset( $metadata['original_image'] ); if ( file_exists( $rescaled_path ) && file_exists( $new_path ) ) { // Move rescaled to original using WP_Filesystem. global $wp_filesystem; if ( ! $wp_filesystem ) { require_once ABSPATH . '/wp-admin/includes/file.php'; \WP_Filesystem(); } if ( $wp_filesystem ) { $wp_filesystem->move( $rescaled_path, $new_path, true ); } // Update meta "_wp_attached_file". update_post_meta( $attachment_id, '_wp_attached_file', $metadata['file'] ); } } return $metadata; } /** * Add featured image and VPI preloads to head. * * @param string $content Current head HTML. * @return string Modified head HTML. */ public function finalize_head( $content ) { // if ( $this->_vpi_preload_list ) { foreach ( $this->_vpi_preload_list as $v ) { $content .= ''; } } return $content; } /** * Adjust WP default JPG quality. * * @since 3.0 * @access public * * @param int $quality Current quality. * @return int Adjusted quality. */ public function adjust_jpg_quality( $quality ) { $v = $this->conf( Base::O_IMG_OPTM_JPG_QUALITY ); if ( $v ) { return $v; } return $quality; } /** * Register admin menu. * * @since 1.6.3 * @access public * @return void */ public function after_admin_init() { /** * JPG quality control. * * @since 3.0 */ add_filter( 'jpeg_quality', array( $this, 'adjust_jpg_quality' ) ); add_filter( 'manage_media_columns', array( $this, 'media_row_title' ) ); add_filter( 'manage_media_custom_column', array( $this, 'media_row_actions' ), 10, 2 ); add_action( 'litespeed_media_row', array( $this, 'media_row_con' ) ); } /** * Media delete action hook. * * @since 2.4.3 * @access public * * @param int $post_id Post ID. * @return void */ public static function delete_attachment( $post_id ) { self::debug( 'delete_attachment [pid] ' . $post_id ); Img_Optm::cls()->reset_row( $post_id ); } /** * Return media file info if exists. * * This is for remote attachment plugins. * * @since 2.9.8 * @access public * * @param string $short_file_path Relative file path under uploads. * @param int $post_id Post ID. * @return array|false Array( url, md5, size ) or false. */ public function info( $short_file_path, $post_id ) { $short_file_path = wp_normalize_path( $short_file_path ); $basedir = $this->_wp_upload_dir['basedir'] . '/'; if ( 0 === strpos( $short_file_path, $basedir ) ) { $short_file_path = substr( $short_file_path, strlen( $basedir ) ); } $real_file = $basedir . $short_file_path; if ( file_exists( $real_file ) ) { return array( 'url' => $this->_wp_upload_dir['baseurl'] . '/' . $short_file_path, 'md5' => md5_file( $real_file ), 'size' => filesize( $real_file ), ); } /** * WP Stateless compatibility #143 https://github.com/litespeedtech/lscache_wp/issues/143 * * @since 2.9.8 * Should return array( 'url', 'md5', 'size' ). */ $info = apply_filters( 'litespeed_media_info', [], $short_file_path, $post_id ); if ( ! empty( $info['url'] ) && ! empty( $info['md5'] ) && ! empty( $info['size'] ) ) { return $info; } return false; } /** * Delete media file. * * @since 2.9.8 * @access public * * @param string $short_file_path Relative file path under uploads. * @param int $post_id Post ID. * @return void */ public function del( $short_file_path, $post_id ) { $real_file = $this->_wp_upload_dir['basedir'] . '/' . $short_file_path; if ( file_exists( $real_file ) ) { wp_delete_file( $real_file ); self::debug( 'deleted ' . $real_file ); } do_action( 'litespeed_media_del', $short_file_path, $post_id ); } /** * Rename media file. * * @since 2.9.8 * @access public * * @param string $short_file_path Old relative path. * @param string $short_file_path_new New relative path. * @param int $post_id Post ID. * @return void */ public function rename( $short_file_path, $short_file_path_new, $post_id ) { $real_file = $this->_wp_upload_dir['basedir'] . '/' . $short_file_path; $real_file_new = $this->_wp_upload_dir['basedir'] . '/' . $short_file_path_new; if ( file_exists( $real_file ) ) { global $wp_filesystem; if ( ! $wp_filesystem ) { require_once ABSPATH . '/wp-admin/includes/file.php'; \WP_Filesystem(); } if ( $wp_filesystem ) { $wp_filesystem->move( $real_file, $real_file_new, true ); } self::debug( 'renamed ' . $real_file . ' to ' . $real_file_new ); } do_action( 'litespeed_media_rename', $short_file_path, $short_file_path_new, $post_id ); } /** * Media Admin Menu -> Image Optimization Column Title. * * @since 1.6.3 * @access public * * @param array $posts_columns Existing columns. * @return array Modified columns. */ public function media_row_title( $posts_columns ) { $posts_columns['imgoptm'] = esc_html__( 'LiteSpeed Optimization', 'litespeed-cache' ); return $posts_columns; } /** * Media Admin Menu -> Image Optimization Column. * * @since 1.6.2 * @access public * * @param string $column_name Current column name. * @param int $post_id Post ID. * @return void */ public function media_row_actions( $column_name, $post_id ) { if ( 'imgoptm' !== $column_name ) { return; } do_action( 'litespeed_media_row', $post_id ); } /** * Display image optimization info in the media list row. * * @since 3.0 * * @param int $post_id Attachment post ID. * @return void */ public function media_row_con( $post_id ) { $att_info = wp_get_attachment_metadata( $post_id ); if ( empty( $att_info['file'] ) ) { return; } $short_path = $att_info['file']; $size_meta = get_post_meta( $post_id, Img_Optm::DB_SIZE, true ); echo '
'; // Original image info. if ( $size_meta && ! empty( $size_meta['ori_saved'] ) ) { $percent = (int) ceil( ( (int) $size_meta['ori_saved'] * 100 ) / max( 1, (int) $size_meta['ori_total'] ) ); $extension = pathinfo( $short_path, PATHINFO_EXTENSION ); $bk_file = substr( $short_path, 0, -strlen( $extension ) ) . 'bk.' . $extension; $bk_optm_file = substr( $short_path, 0, -strlen( $extension ) ) . 'bk.optm.' . $extension; $link = Utility::build_url( Router::ACTION_IMG_OPTM, 'orig' . $post_id ); $desc = false; $cls = ''; if ( $this->info( $bk_file, $post_id ) ) { $curr_status = esc_html__( '(optm)', 'litespeed-cache' ); $desc = esc_attr__( 'Currently using optimized version of file.', 'litespeed-cache' ) . ' ' . esc_attr__( 'Click to switch to original (unoptimized) version.', 'litespeed-cache' ); } elseif ( $this->info( $bk_optm_file, $post_id ) ) { $cls .= ' litespeed-warning'; $curr_status = esc_html__( '(non-optm)', 'litespeed-cache' ); $desc = esc_attr__( 'Currently using original (unoptimized) version of file.', 'litespeed-cache' ) . ' ' . esc_attr__( 'Click to switch to optimized version.', 'litespeed-cache' ); } echo wp_kses_post( GUI::pie_tiny( $percent, 24, sprintf( esc_html__( 'Original file reduced by %1$s (%2$s)', 'litespeed-cache' ), $percent . '%', Utility::real_size( $size_meta['ori_saved'] ) ), 'left' ) ); printf( esc_html__( 'Orig saved %s', 'litespeed-cache' ), (int) $percent . '%' ); if ( $desc ) { printf( ' %4$s', esc_url( $link ), esc_attr( $cls ), wp_kses_post( $desc ), esc_html( $curr_status ) ); } else { printf( ' %2$s', esc_attr__( 'Using optimized version of file. ', 'litespeed-cache' ) . ' ' . esc_attr__( 'No backup of original file exists.', 'litespeed-cache' ), esc_html__( '(optm)', 'litespeed-cache' ) ); } } elseif ( $size_meta && 0 === (int) $size_meta['ori_saved'] ) { echo wp_kses_post( GUI::pie_tiny( 0, 24, esc_html__( 'Congratulation! Your file was already optimized', 'litespeed-cache' ), 'left' ) ); printf( esc_html__( 'Orig %s', 'litespeed-cache' ), '' . esc_html__( '(no savings)', 'litespeed-cache' ) . '' ); } else { echo esc_html__( 'Orig', 'litespeed-cache' ) . '—'; } echo '
'; echo ''; // WebP/AVIF info. if ( $size_meta && $this->webp_support( true ) && ! empty( $size_meta[ $this->_sys_format . '_saved' ] ) ) { $is_avif = 'avif' === $this->_sys_format; $size_meta_saved = $size_meta[ $this->_sys_format . '_saved' ]; $size_meta_total = $size_meta[ $this->_sys_format . '_total' ]; $percent = ceil( ( $size_meta_saved * 100 ) / max( 1, $size_meta_total ) ); $link = Utility::build_url( Router::ACTION_IMG_OPTM, $this->_sys_format . $post_id ); $desc = false; $cls = ''; if ( $this->info( $short_path . '.' . $this->_sys_format, $post_id ) ) { $curr_status = esc_html__( '(optm)', 'litespeed-cache' ); $desc = $is_avif ? esc_attr__( 'Currently using optimized version of AVIF file.', 'litespeed-cache' ) : esc_attr__( 'Currently using optimized version of WebP file.', 'litespeed-cache' ); $desc .= ' ' . esc_attr__( 'Click to switch to original (unoptimized) version.', 'litespeed-cache' ); } elseif ( $this->info( $short_path . '.optm.' . $this->_sys_format, $post_id ) ) { $cls .= ' litespeed-warning'; $curr_status = esc_html__( '(non-optm)', 'litespeed-cache' ); $desc = $is_avif ? esc_attr__( 'Currently using original (unoptimized) version of AVIF file.', 'litespeed-cache' ) : esc_attr__( 'Currently using original (unoptimized) version of WebP file.', 'litespeed-cache' ); $desc .= ' ' . esc_attr__( 'Click to switch to optimized version.', 'litespeed-cache' ); } echo wp_kses_post( GUI::pie_tiny( $percent, 24, sprintf( $is_avif ? esc_html__( 'AVIF file reduced by %1$s (%2$s)', 'litespeed-cache' ) : esc_html__( 'WebP file reduced by %1$s (%2$s)', 'litespeed-cache' ), $percent . '%', Utility::real_size( $size_meta_saved ) ), 'left' ) ); printf( $is_avif ? esc_html__( 'AVIF saved %s', 'litespeed-cache' ) : esc_html__( 'WebP saved %s', 'litespeed-cache' ), '' . esc_html( $percent ) . '%' ); if ( $desc ) { printf( ' %4$s', esc_url( $link ), esc_attr( $cls ), wp_kses_post( $desc ), esc_html( $curr_status ) ); } else { printf( ' %3$s', esc_attr__( 'Using optimized version of file. ', 'litespeed-cache' ), $is_avif ? esc_attr__( 'No backup of unoptimized AVIF file exists.', 'litespeed-cache' ) : esc_attr__( 'No backup of unoptimized WebP file exists.', 'litespeed-cache' ), esc_html__( '(optm)', 'litespeed-cache' ) ); } } else { echo esc_html( $this->next_gen_image_title() ) . '—'; } echo '
'; // Delete row btn. if ( $size_meta ) { printf( '', esc_url( Utility::build_url( Router::ACTION_IMG_OPTM, Img_Optm::TYPE_RESET_ROW, false, null, array( 'id' => $post_id ) ) ), esc_html__( 'Restore from backup', 'litespeed-cache' ) ); echo ''; } } /** * Get wp size info. * * NOTE: this is not used because it has to be after admin_init. * * @since 1.6.2 * @return array $sizes Data for all currently-registered image sizes. */ public function get_image_sizes() { global $_wp_additional_image_sizes; $sizes = []; foreach ( get_intermediate_image_sizes() as $_size ) { if ( in_array( $_size, array( 'thumbnail', 'medium', 'medium_large', 'large' ), true ) ) { $sizes[ $_size ]['width'] = get_option( $_size . '_size_w' ); $sizes[ $_size ]['height'] = get_option( $_size . '_size_h' ); $sizes[ $_size ]['crop'] = (bool) get_option( $_size . '_crop' ); } elseif ( isset( $_wp_additional_image_sizes[ $_size ] ) ) { $sizes[ $_size ] = array( 'width' => $_wp_additional_image_sizes[ $_size ]['width'], 'height' => $_wp_additional_image_sizes[ $_size ]['height'], 'crop' => $_wp_additional_image_sizes[ $_size ]['crop'], ); } } return $sizes; } /** * Exclude role from optimization filter. * * @since 1.6.2 * @access public * * @param bool $sys_level Return system-level format if true. * @return string Next-gen format name or empty string. */ public function webp_support( $sys_level = false ) { if ( $sys_level ) { return $this->_sys_format; } return $this->_format; // User level next gen support. } /** * Detect if browser supports next-gen format. * * @return bool */ private function _browser_support_next_gen() { $accept = isset( $_SERVER['HTTP_ACCEPT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT'] ) ) : ''; if ( $accept ) { if ( false !== strpos( $accept, 'image/' . $this->_sys_format ) ) { return true; } } $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; if ( $ua ) { $user_agents = array( 'chrome-lighthouse', 'googlebot', 'page speed' ); foreach ( $user_agents as $user_agent ) { if ( false !== stripos( $ua, $user_agent ) ) { return true; } } if ( preg_match( '/iPhone OS (\d+)_/i', $ua, $matches ) ) { if ( $matches[1] >= 14 ) { return true; } } if ( preg_match( '/Firefox\/(\d+)/i', $ua, $matches ) ) { if ( $matches[1] >= 65 ) { return true; } } } return false; } /** * Get next gen image title. * * @since 7.0 * @return string */ public function next_gen_image_title() { $next_gen_img = 'WebP'; if ( 2 === $this->conf( Base::O_IMG_OPTM_WEBP ) ) { $next_gen_img = 'AVIF'; } return $next_gen_img; } /** * Run lazy load process. * NOTE: As this is after cache finalized, can NOT set any cache control anymore. * * Only do for main page. Do NOT do for esi or dynamic content. * * @since 1.4 * @access public * * @param string $content Final buffer. * @return string The buffer. */ public function finalize( $content ) { if ( defined( 'LITESPEED_NO_LAZY' ) ) { self::debug2( 'bypass: NO_LAZY const' ); return $content; } if ( ! defined( 'LITESPEED_IS_HTML' ) ) { self::debug2( 'bypass: Not frontend HTML type' ); return $content; } if ( ! Control::is_cacheable() ) { self::debug( 'bypass: Not cacheable' ); return $content; } self::debug( 'finalize' ); $this->content = $content; $this->_finalize(); return $this->content; } /** * Run lazyload replacement for images in buffer. * * @since 1.4 * @access private * @return void */ private function _finalize() { /** * Use webp for optimized images. * * @since 1.6.2 */ if ( $this->webp_support() ) { $this->content = $this->_replace_buffer_img_webp( $this->content ); } /** * Check if URI is excluded. * * @since 3.0 */ $excludes = $this->conf( Base::O_MEDIA_LAZY_URI_EXC ); if ( ! defined( 'LITESPEED_GUEST_OPTM' ) ) { $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; $result = $request_uri ? Utility::str_hit_array( $request_uri, $excludes ) : false; if ( $result ) { self::debug( 'bypass lazyload: hit URI Excludes setting: ' . $result ); return; } } $cfg_lazy = ( defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_MEDIA_LAZY ) ) && ! $this->cls( 'Metabox' )->setting( 'litespeed_no_image_lazy' ); $cfg_iframe_lazy = defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_MEDIA_IFRAME_LAZY ); $cfg_js_delay = defined( 'LITESPEED_GUEST_OPTM' ) || 2 === $this->conf( Base::O_OPTM_JS_DEFER ); $cfg_trim_noscript = defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_OPTM_NOSCRIPT_RM ); $cfg_vpi = defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_MEDIA_VPI ); // Preload VPI. if ( $cfg_vpi ) { $this->_parse_img_for_preload(); } if ( $cfg_lazy ) { if ( $cfg_vpi ) { add_filter( 'litespeed_media_lazy_img_excludes', array( $this->cls( 'Metabox' ), 'lazy_img_excludes' ) ); } list( $src_list, $html_list, $placeholder_list ) = $this->_parse_img(); $html_list_ori = $html_list; } else { self::debug( 'lazyload disabled' ); } // image lazy load. if ( $cfg_lazy ) { $__placeholder = Placeholder::cls(); foreach ( $html_list as $k => $v ) { $size = $placeholder_list[ $k ]; $src = $src_list[ $k ]; $html_list[ $k ] = $__placeholder->replace( $v, $src, $size ); } } if ( $cfg_lazy ) { $this->content = str_replace( $html_list_ori, $html_list, $this->content ); } // iframe lazy load. if ( $cfg_iframe_lazy ) { $html_list = $this->_parse_iframe(); $html_list_ori = $html_list; foreach ( $html_list as $k => $v ) { $snippet = $cfg_trim_noscript ? '' : ''; if ( $cfg_js_delay ) { $v = str_replace( ' src=', ' data-litespeed-src=', $v ); } else { $v = str_replace( ' src=', ' data-src=', $v ); } $v = str_replace( '