: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* @package Smush\Core\Modules
namespace Smush\Core\Modules;
use Smush\Core\Stats\Global_Stats;
if ( ! defined( 'WPINC' ) ) {
* Class WebP extends Abstract_Module.
class WebP extends Abstract_Module {
protected $slug = 'webp_mod';
* Whether module is pro or not.
protected $is_pro = true;
* If server is configured for webp
* @var bool $is_configured
// Show success message after deleting all webp images.
add_action( 'wp_smush_header_notices', array( $this, 'maybe_show_notices' ) );
// Only apply filters for PRO + activated Webp.
if ( $this->is_active() ) {
// Add a filter to check if the image should resmush.
//add_filter( 'wp_smush_should_resmush', array( $this, 'should_resmush' ), 10, 2 );
* Enables and disables the WebP module.
* @param boolean $enable Whether to enable or disable WebP.
public function toggle_webp( $enable = true ) {
$this->settings->set( $this->slug, $enable );
if ( is_null( $wp_filesystem ) ) {
// These aren't included when applying a config from the Hub.
if ( ! function_exists( 'WP_Filesystem' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
$parsed_udir = $this->get_upload_dir();
$flag_file_path = $parsed_udir['webp_path'] . '/disable_smush_webp';
// Handle the file used as a flag by the server rules.
$wp_filesystem->delete( $flag_file_path, true );
$wp_filesystem->put_contents( $flag_file_path, '' );
* Gets whether WebP is configured, returning a message to display when it's not.
* This is a wrapper for displaying a message on failure which is used in three places.
* Moved here to reduce the redundancy.
* @param bool $force Force check.
* @return true|string True when it's configured. String when it's not.
public function get_is_configured_with_error_message( $force = false ) {
$is_configured = $this->is_configured( $force );
if ( true === $is_configured ) {
if ( is_wp_error( $is_configured ) ) {
return $is_configured->get_error_message();
if ( 'apache' === $this->get_server_type() && $this->is_htaccess_written() ) {
return __( "The server rules have been applied but the server doesn't seem to be serving your images as WebP. We recommend contacting your hosting provider to learn more about the cause of this issue.", 'wp-smushit' );
return __( "Server configurations haven't been applied yet. Make configurations to start serving images in WebP format.", 'wp-smushit' );
* Get status of server configuration for webp.
* @param bool $force force to recheck.
public function is_configured( $force = false ) {
if ( ! is_null( $this->is_configured ) && ! $force ) {
return $this->is_configured;
$this->is_configured = $this->check_server_config();
return $this->is_configured;
* Check if server is configured to serve webp image.
private function check_server_config() {
$files_created = $this->create_test_files();
// WebP test images couldn't be created.
if ( true !== $files_created ) {
/* translators: path that couldn't be written */
__( 'We couldn\'t create the WebP test files. This is probably due to your current folder permissions. Please adjust the permissions for "%s" to 755 and try again.', 'wp-smushit' ),
return new WP_Error( 'test_files_not_created', $message );
$udir = $this->get_upload_dir();
$test_image = $udir['upload_url'] . '/smush-webp-test.png';
'Accept' => 'image/webp',
// Add support for basic auth in WPMU DEV staging.
if ( isset( $_SERVER['WPMUDEV_HOSTING_ENV'] ) && 'staging' === $_SERVER['WPMUDEV_HOSTING_ENV'] && isset( $_SERVER['PHP_AUTH_USER'] ) ) {
$args['headers']['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
$response = wp_remote_get( $test_image, $args );
// If there is an error, return.
if ( is_wp_error( $response ) ) {
$code = wp_remote_retrieve_response_code( $response );
// Check the image's format when the request was successful.
$content_type = wp_remote_retrieve_header( $response, 'content-type' );
return 'image/webp' === $content_type;
// Return the response code and message otherwise.
$error_message = sprintf(
/* translators: 1. error code, 2. error message. */
__( "We couldn't check the WebP server rules status because there was an error with the test request. Please contact support for assistance. Code %1\$s: %2\$s.", 'wp-smushit' ),
wp_remote_retrieve_response_message( $response )
return new WP_Error( $code, $error_message );
* Code to use on Nginx servers.
* @param bool $marker whether to wrap code with marker comment lines.
public function get_nginx_code( $marker = true ) {
$udir = $this->get_upload_dir();
$base = trailingslashit( dirname( $udir['upload_rel_path'] ) );
$directory = trailingslashit( basename( $udir['upload_rel_path'] ) );
$regex_base = $base . '(' . $directory . ')';
* We often need to remove WebP file extension from Nginx cache rule in order to make Smush WebP work,
* so always add expiry header rule for Nginx.
* @see https://incsub.atlassian.net/browse/SMUSH-1072
$code = 'location ~* "' . str_replace( '/', '\/', $regex_base ) . '(.*\.(?:png|jpe?g))" {
if (-f "' . $udir['webp_path'] . '/disable_smush_webp") {
if ($http_accept !~* "webp") {
try_files /' . trailingslashit( $udir['webp_rel_path'] ) . '$image_path.webp $uri =404;
if ( true === $marker ) {
$code = $this->marker_line() . "\n" . $code;
$code = $code . "\n" . $this->marker_line( true );
return apply_filters( 'smush_nginx_webp_rules', $code );
* Code to use on Apache servers.
* @todo Find out what's wrong with the rules. We shouldn't need these two different RewriteRule.
* @param string $location Where the .htaccess file is.
private function get_apache_code( $location ) {
$udir = $this->get_upload_dir();
$rewrite_path = '%{DOCUMENT_ROOT}/' . $udir['webp_rel_path'];
$code = '<IfModule mod_rewrite.c>
RewriteCond ' . $rewrite_path . '/disable_smush_webp !-f
RewriteCond %{HTTP_ACCEPT} image/webp' . "\n";
if ( 'root' === $location ) {
// This works on single sites at root.
$code .= ' RewriteCond ' . $rewrite_path . '/$1.webp -f
RewriteRule ' . $udir['upload_rel_path'] . '/(.*\.(?:png|jpe?g))$ ' . $udir['webp_rel_path'] . '/$1.webp [NC,T=image/webp]';
// This works at /uploads/.
$code .= ' RewriteCond ' . $rewrite_path . '/$1.$2.webp -f
RewriteRule ^/?(.+)\.(jpe?g|png)$ /' . $udir['webp_rel_path'] . '/$1.$2.webp [NC,T=image/webp]';
$code .= "\n" . '</IfModule>
Header append Vary Accept env=WEBP_image
return apply_filters( 'smush_apache_webp_rules', $code );
* Gets the apache rules for printing them in the config tab.
public function get_apache_code_to_print() {
$location = is_multisite() ? 'uploads' : 'root';
$code = $this->marker_line() . "\n";
$code .= $this->get_apache_code( $location );
$code .= "\n" . $this->marker_line( true );
* Retrieves uploads directory and WebP directory information.
* All paths and urls do not have trailing slash.
public function get_upload_dir() {
if ( isset( $upload_dir_info ) ) {
if ( ! is_multisite() || is_main_site() ) {
$upload = wp_upload_dir();
// Use the main site's upload directory for all subsite's webp converted images.
// This makes it easier to have a single rule on the server configs for serving webp in mu.
$blog_id = get_main_site_id();
switch_to_blog( $blog_id );
$upload = wp_upload_dir();
// Is it possible that none of the following conditions are met?
// Get the Document root path. There must be a better way to do this.
// For example, /srv/www/site/public_html for /srv/www/site/public_html/wp-content/uploads.
if ( 0 === strpos( $upload['basedir'], ABSPATH ) ) {
// Environments like Flywheel have an ABSPATH that's not used in the paths.
$root_path_base = ABSPATH;
} elseif ( ! empty( $_SERVER['DOCUMENT_ROOT'] ) && 0 === strpos( $upload['basedir'], wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) ) {
* This gets called when scanning for uncompressed images.
* When ran from certain contexts, $_SERVER['DOCUMENT_ROOT'] might not be set.
* We are removing this part from the path later on.
$root_path_base = realpath( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
} elseif ( 0 === strpos( $upload['basedir'], dirname( WP_CONTENT_DIR ) ) ) {
// We're assuming WP_CONTENT_DIR is only one level deep into the document root.
// This might not be true in customized sites. A bit edgy.
$root_path_base = dirname( WP_CONTENT_DIR );
* Filters the Document root path used to get relative paths for webp rules.
* Hopefully of help for debugging and SLS.
$root_path_base = apply_filters( 'smush_webp_rules_root_path_base', $root_path_base );
// Get the upload path relative to the Document root.
// For example, wp-content/uploads for /srv/www/site/public_html/wp-content/uploads.
$upload_root_rel_path = ltrim( str_replace( $root_path_base, '', $upload['basedir'] ), '/' );
// Get the relative path for the directory containing the webp files.
// This directory is a sibling of the 'uploads' directory.
// For example, wp-content/smush-webp for wp-content/uploads.
$webp_root_rel_path = dirname( $upload_root_rel_path ) . '/smush-webp';
* Add a hook for user custom webp address.
'upload_path' => $upload['basedir'],
'upload_rel_path' => $upload_root_rel_path,
'upload_url' => $upload['baseurl'],
'webp_path' => dirname( $upload['basedir'] ) . '/smush-webp',
'webp_rel_path' => $webp_root_rel_path,
'webp_url' => dirname( $upload['baseurl'] ) . '/smush-webp',
* Create test files and required directory.
* @return true|string String with the path that couldn't be written on failure.
public function create_test_files() {
$udir = $this->get_upload_dir();
$test_png_file = $udir['upload_path'] . '/smush-webp-test.png';
$test_webp_file = $udir['webp_path'] . '/smush-webp-test.png.webp';
// Create the png file to be requested if it doesn't exist. Bail out if it fails.
! file_exists( $test_png_file ) &&
! copy( WP_SMUSH_DIR . 'app/assets/images/smush-webp-test.png', $test_png_file )
Helper::logger()->webp()->error( 'Cannot create test PNG file [%s].', $test_png_file );
return $udir['upload_path'];
// Create the WebP file that should be sent in the response if the rules work.
if ( ! file_exists( $test_webp_file ) ) {
$directory_created = is_dir( $udir['webp_path'] ) || wp_mkdir_p( $udir['webp_path'] );
! copy( WP_SMUSH_DIR . 'app/assets/images/smush-webp-test.png.webp', $test_webp_file )
Helper::logger()->webp()->error( 'Cannot create test Webp file [%s].', $test_webp_file );
return $udir['webp_path'];
* Retrieves related webp image file path for a given non webp image file path.
* Also create required directories for webp image if not exists.
* @param string $file_path Non webp image file path.
* @param bool $make Weather to create required directories.
public function get_webp_file_path( $file_path, $make = false ) {
$udir = $this->get_upload_dir();
$file_rel_path = substr( $file_path, strlen( $udir['upload_path'] ) );
$webp_file_path = $udir['webp_path'] . $file_rel_path . '.webp';
$webp_file_dir = dirname( $webp_file_path );
if ( ! is_dir( $webp_file_dir ) ) {
wp_mkdir_p( $webp_file_dir );
* Check whether the given attachment id or mime type can be converted to WebP.
* @param string $id Atachment ID.
* @param string $mime Mime type.
private function can_be_converted( $id = '', $mime = '' ) {
if ( empty( $id ) && empty( $mime ) ) {
$mime = empty( $mime ) ? get_post_mime_type( $id ) : $mime;
// This image can not be converted to webp.
if ( ! ( 'image/jpeg' === $mime || 'image/png' === $mime ) ) {
* Checks whether an attachment should be converted to WebP.
* Returns false if WebP isn't configured, the attachment was already converted,
* or if the attachment can't be converted ( @see self::can_be_converted() ).
* @param string $id Attachment ID.
public function should_be_converted( $id ) {
// Avoid conversion when webp disabled, or when Smush is Free.
if ( ! $this->is_active() || ! Helper::is_smushable( $id ) ) {
$meta = get_post_meta( $id, Smush::$smushed_meta_key, true );
$webp_udir = $this->get_upload_dir();
// The image was already converted to WebP.
if ( ! empty( $meta['webp_flag'] ) && file_exists( $webp_udir['webp_path'] . '/' . $meta['webp_flag'] ) ) {
Helper::logger()->webp()->info( sprintf( 'The image [%d] is already converted to Webp: [%s]', $id, $meta['webp_flag'] ) );
return $this->can_be_converted( $id );
* Check whether to resmush image or not.