Phase 6: WebP image conversion - Converter for Media plugin with Nginx rewrite rules

This commit is contained in:
Hanson.xyz Dev
2025-11-28 17:16:24 -06:00
parent 91de533da4
commit 78a744ef06
260 changed files with 21138 additions and 5 deletions
@@ -0,0 +1,78 @@
<?php
namespace WebpConverter\Service;
use WebpConverter\Conversion\Directory\UploadsWebpcDirectory;
use WebpConverter\HookableInterface;
use WebpConverter\PluginData;
use WebpConverter\Settings\Option\ExtraFeaturesOption;
/**
* Excludes saving converted images in the backup.
*/
class BackupExcluder implements HookableInterface {
/**
* @var PluginData
*/
private $plugin_data;
public function __construct( PluginData $plugin_data ) {
$this->plugin_data = $plugin_data;
}
/**
* {@inheritdoc}
*/
public function init_hooks() {
add_action( 'init', [ $this, 'init_hooks_after_setup' ] );
}
/**
* @return void
* @internal
*/
public function init_hooks_after_setup() {
$plugin_settings = $this->plugin_data->get_plugin_settings();
if ( in_array( ExtraFeaturesOption::OPTION_VALUE_BACKUP_ENABLED, $plugin_settings[ ExtraFeaturesOption::OPTION_NAME ] ) ) {
return;
}
add_filter( 'ai1wm_exclude_content_from_export', [ $this, 'ai1wm_exclude_content_from_export' ], 10, 1 );
add_filter( 'updraftplus_exclude_directory', [ $this, 'updraftplus_exclude_directory' ], 10, 2 );
add_filter( 'backwpup_content_exclude_dirs', [ $this, 'backwpup_content_exclude_dirs' ], 10, 1 );
}
/**
* @param string[] $exclude_dirs .
*
* @return string[]
* @internal
*/
public function ai1wm_exclude_content_from_export( $exclude_dirs ) {
$exclude_dirs[] = UploadsWebpcDirectory::DIRECTORY_NAME;
return $exclude_dirs;
}
/**
* @param bool $status .
* @param string $directory .
*
* @return bool
* @internal
*/
public function updraftplus_exclude_directory( $status, $directory ) {
return ( $directory === UploadsWebpcDirectory::DIRECTORY_NAME ) ? true : $status;
}
/**
* @param string[] $exclude_dirs .
*
* @return string[]
* @internal
*/
public function backwpup_content_exclude_dirs( $exclude_dirs ) {
$exclude_dirs[] = UploadsWebpcDirectory::DIRECTORY_NAME;
return $exclude_dirs;
}
}
@@ -0,0 +1,86 @@
<?php
namespace WebpConverter\Service;
use WebpConverter\HookableInterface;
use WebpConverter\PluginInfo;
use WebpConverter\Settings\Option\LoaderTypeOption;
/**
* Supports cleaning cache generated by other plugins.
*/
class CacheIntegrator implements HookableInterface {
/**
* @var PluginInfo
*/
private $plugin_info;
public function __construct( PluginInfo $plugin_info ) {
$this->plugin_info = $plugin_info;
}
/**
* {@inheritdoc}
*/
public function init_hooks() {
add_action( 'webpc_settings_updated', [ $this, 'clear_after_settings_save' ], 10, 2 );
register_activation_hook( $this->plugin_info->get_plugin_file(), [ $this, 'clear_cache' ] );
register_deactivation_hook( $this->plugin_info->get_plugin_file(), [ $this, 'clear_cache' ] );
}
/**
* @param mixed[] $current_settings .
* @param mixed[] $previous_settings .
*
* @return void
* @internal
*/
public function clear_after_settings_save( array $current_settings, array $previous_settings ) {
if ( $previous_settings[ LoaderTypeOption::OPTION_NAME ] === $current_settings[ LoaderTypeOption::OPTION_NAME ] ) {
return;
}
$this->clear_cache();
}
/**
* @return void
* @internal
*/
public function clear_cache() {
if ( ! function_exists( 'is_plugin_active' ) ) {
include_once ABSPATH . '/wp-admin/includes/plugin.php';
}
if ( is_plugin_active( 'breeze/breeze.php' ) ) {
do_action( 'breeze_clear_all_cache' );
}
if ( is_plugin_active( 'cache-enabler/cache-enabler.php' ) ) {
do_action( 'cache_enabler_clear_complete_cache' );
}
if ( is_plugin_active( 'hummingbird-performance/wp-hummingbird.php' ) ) {
do_action( 'wphb_clear_page_cache' );
}
if ( is_plugin_active( 'litespeed-cache/litespeed-cache.php' ) ) {
do_action( 'litespeed_purge', '*' );
}
if ( is_plugin_active( 'sg-cachepress/sg-cachepress.php' ) && function_exists( 'sg_cachepress_purge_cache' ) ) {
sg_cachepress_purge_cache();
}
if ( is_plugin_active( 'w3-total-cache/w3-total-cache.php' ) && function_exists( 'w3tc_flush_posts' ) ) {
w3tc_flush_posts();
}
if ( is_plugin_active( 'wp-fastest-cache/wpFastestCache.php' ) ) {
do_action( 'wpfc_clear_all_cache' );
}
if ( is_plugin_active( 'wp-optimize/wp-optimize.php' ) && function_exists( 'wpo_cache_flush' ) ) {
wpo_cache_flush();
}
if ( is_plugin_active( 'wp-super-cache/wp-cache.php' ) && function_exists( 'wp_cache_clear_cache' ) ) {
wp_cache_clear_cache();
}
wp_cache_delete( 'alloptions', 'options' );
}
}
@@ -0,0 +1,159 @@
<?php
namespace WebpConverter\Service;
use WebpConverter\HookableInterface;
use WebpConverter\PluginData;
use WebpConverter\PluginInfo;
use WebpConverter\Settings\Option\CloudflareApiTokenOption;
use WebpConverter\Settings\Option\CloudflareZoneIdOption;
/**
* Manages the cache configuration for Cloudflare CDN.
*/
class CloudflareConfigurator implements HookableInterface {
const API_CACHE_CONFIG_URL = 'https://api.cloudflare.com/client/v4/zones/%s/cache/variants';
const API_CACHE_PURGE_URL = 'https://api.cloudflare.com/client/v4/zones/%s/purge_cache';
const REQUEST_CACHE_CONFIG_OPTION = 'webpc_cloudflare_cache_config';
const REQUEST_CACHE_PURGE_OPTION = 'webpc_cloudflare_cache_purge';
/**
* @var PluginInfo
*/
private $plugin_info;
/**
* @var PluginData
*/
private $plugin_data;
public function __construct( PluginInfo $plugin_info, PluginData $plugin_data ) {
$this->plugin_info = $plugin_info;
$this->plugin_data = $plugin_data;
}
/**
* {@inheritdoc}
*/
public function init_hooks() {
add_action( 'webpc_settings_updated', [ $this, 'clear_after_settings_save' ], 10, 2 );
register_activation_hook( $this->plugin_info->get_plugin_file(), [ $this, 'purge_cache' ] );
register_deactivation_hook( $this->plugin_info->get_plugin_file(), [ $this, 'purge_cache' ] );
}
/**
* @param mixed[] $current_settings .
* @param mixed[] $previous_settings .
*
* @return void
* @internal
*/
public function clear_after_settings_save( array $current_settings, array $previous_settings ) {
if ( ( $previous_settings[ CloudflareZoneIdOption::OPTION_NAME ] === $current_settings[ CloudflareZoneIdOption::OPTION_NAME ] )
&& ( $previous_settings[ CloudflareApiTokenOption::OPTION_NAME ] === $current_settings[ CloudflareApiTokenOption::OPTION_NAME ] ) ) {
return;
}
$this->set_cache_config();
$this->purge_cache();
}
/**
* @return bool
* @internal
*/
public function set_cache_config(): bool {
$this->send_request( self::API_CACHE_CONFIG_URL, 'DELETE' );
$response_code = $this->send_request(
self::API_CACHE_CONFIG_URL,
'PATCH',
[
'value' => [
'jpeg' => [ 'image/jpeg', 'image/webp', 'image/avif' ],
'jpg' => [ 'image/jpeg', 'image/webp', 'image/avif' ],
'png' => [ 'image/png', 'image/webp', 'image/avif' ],
'gif' => [ 'image/gif', 'image/webp', 'image/avif' ],
'webp' => [ 'image/webp', 'image/avif' ],
],
]
);
if ( $response_code === null ) {
return false;
}
OptionsAccessManager::update_option(
self::REQUEST_CACHE_CONFIG_OPTION,
( $response_code === 200 ) ? 'yes' : $response_code
);
return ( $response_code === 200 );
}
/**
* @return bool
* @internal
*/
public function purge_cache(): bool {
$response_code = $this->send_request(
self::API_CACHE_PURGE_URL,
'POST',
[
'purge_everything' => true,
]
);
if ( $response_code === null ) {
return false;
}
OptionsAccessManager::update_option(
self::REQUEST_CACHE_PURGE_OPTION,
( $response_code === 200 ) ? 'yes' : $response_code
);
return ( $response_code === 200 );
}
/**
* @param string $api_url .
* @param string $request_method .
* @param mixed[] $request_data .
*
* @return int|null
*/
private function send_request( string $api_url, string $request_method, array $request_data = [] ): ?int {
$plugin_setting = $this->plugin_data->get_plugin_settings();
if ( ! $plugin_setting[ CloudflareZoneIdOption::OPTION_NAME ] || ! $plugin_setting[ CloudflareApiTokenOption::OPTION_NAME ] ) {
return null;
}
$connect = curl_init( sprintf( $api_url, $plugin_setting[ CloudflareZoneIdOption::OPTION_NAME ] ) );
if ( ! $connect ) {
return null;
}
curl_setopt( $connect, CURLOPT_SSL_VERIFYPEER, false );
curl_setopt( $connect, CURLOPT_SSL_VERIFYHOST, 0 );
curl_setopt( $connect, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $connect, CURLOPT_TIMEOUT, 10 );
curl_setopt( $connect, CURLOPT_CUSTOMREQUEST, $request_method ?: 'POST' );
if ( $request_data !== [] ) {
curl_setopt( $connect, CURLOPT_POSTFIELDS, json_encode( $request_data ) ?: '' );
}
curl_setopt(
$connect,
CURLOPT_HTTPHEADER,
[
'Content-Type: application/json',
'Authorization: Bearer ' . $plugin_setting[ CloudflareApiTokenOption::OPTION_NAME ],
]
);
curl_exec( $connect );
$request_info = curl_getinfo( $connect );
curl_close( $connect );
return $request_info['http_code'];
}
}
@@ -0,0 +1,235 @@
<?php
namespace WebpConverter\Service;
use WebpConverter\Error\Notice\LibsNotInstalledNotice;
use WebpConverter\Error\Notice\LibsWithoutWebpSupportNotice;
use WebpConverter\HookableInterface;
use WebpConverter\PluginData;
use WebpConverter\PluginInfo;
use WebpConverter\Settings\Page\AdvancedSettingsPage;
use WebpConverter\Settings\Page\PageIntegrator;
use WebpConverterVendor\MattPlugins\DeactivationModal;
/**
* Initiates the popup displayed when the plugin is deactivated.
*/
class DeactivationModalLoader implements HookableInterface {
const API_URL = 'https://data.mattplugins.com/deactivations/%s';
/**
* @var PluginInfo
*/
private $plugin_info;
/**
* @var PluginData
*/
private $plugin_data;
/**
* @var StatsManager
*/
private $stats_manager;
public function __construct(
PluginInfo $plugin_info,
PluginData $plugin_data,
?StatsManager $stats_manager = null
) {
$this->plugin_info = $plugin_info;
$this->plugin_data = $plugin_data;
$this->stats_manager = $stats_manager ?: new StatsManager();
}
/**
* {@inheritdoc}
*/
public function init_hooks() {
add_action( 'load-plugins.php', [ $this, 'load_modal' ] );
}
/**
* @return void
* @throws DeactivationModal\Exception\DuplicatedFormOptionKeyException
* @throws DeactivationModal\Exception\DuplicatedFormValueKeyException
* @internal
*/
public function load_modal() {
new DeactivationModal\Modal(
$this->plugin_info->get_plugin_slug(),
new DeactivationModal\Model\FormTemplate(
sprintf( self::API_URL, $this->plugin_info->get_plugin_slug() ),
sprintf(
/* translators: %s: plugin name */
__( 'We are sorry that you are leaving our %s plugin', 'webp-converter-for-media' ),
'Converter for Media'
),
__( 'Can you, please, take a moment to tell us why you are deactivating this plugin (anonymous answer)?', 'webp-converter-for-media' ),
__( 'Submit and Deactivate', 'webp-converter-for-media' ),
__( 'Skip and Deactivate', 'webp-converter-for-media' ),
$this->load_notice_message()
),
( new DeactivationModal\Model\FormOptions() )
->set_option(
new DeactivationModal\Model\FormOption(
'server_config',
10,
sprintf(
/* translators: %s: notice title */
__( 'I have the %s notice in the plugin settings', 'webp-converter-for-media' ),
__( 'Server configuration error', 'webp-converter-for-media' )
),
function () {
$errors = apply_filters( 'webpc_server_errors', [] );
if ( ! in_array(
$errors,
[ [ LibsWithoutWebpSupportNotice::ERROR_KEY ], [ LibsNotInstalledNotice::ERROR_KEY ] ]
) ) {
return null;
}
return sprintf(
/* translators: %1$s: open anchor tag, %2$s: close anchor tag */
__( 'If your server does not meet the technical requirements, you can use "Remote server" as "Conversion method", in %1$sthe plugin settings%2$s.', 'webp-converter-for-media' ),
'<a href="' . esc_url( PageIntegrator::get_settings_page_url() ) . '">',
'</a>'
);
},
__( 'What is your error? Have you been looking for a solution to this issue?', 'webp-converter-for-media' )
)
)
->set_option(
new DeactivationModal\Model\FormOption(
'misunderstanding',
20,
__( 'Images are not displayed in the WebP format', 'webp-converter-for-media' ),
function () {
return sprintf(
/* translators: %1$s: open anchor tag, %2$s: close anchor tag */
__( 'Check out %1$sour instructions%2$s and see how to check if the plugin is working properly.', 'webp-converter-for-media' ),
'<a href="https://url.mattplugins.com/converter-deactivation-misunderstanding-instruction" target="_blank">',
'</a>'
);
},
__( 'Did you check the operation of the plugin in accordance with the instructions?', 'webp-converter-for-media' )
)
)
->set_option(
new DeactivationModal\Model\FormOption(
'website_broken',
30,
__( 'This plugin broke my website', 'webp-converter-for-media' ),
function () {
return sprintf(
/* translators: %1$s: option label, %2$s: open anchor tag, %3$s: close anchor tag */
__( 'Check the %1$s option in %2$sthe plugin settings%3$s - this should solve the problem.', 'webp-converter-for-media' ),
__( 'Disable rewrite inheritance in .htaccess files', 'webp-converter-for-media' ),
'<a href="' . esc_url( PageIntegrator::get_settings_page_url( AdvancedSettingsPage::PAGE_SLUG ) ) . '">',
'</a>'
);
},
__( 'What exactly happened?', 'webp-converter-for-media' )
)
)
->set_option(
new DeactivationModal\Model\FormOption(
'better_plugin',
40,
__( 'I found a better plugin', 'webp-converter-for-media' ),
null,
__( 'What is the name of this plugin? Why is it better?', 'webp-converter-for-media' )
)
)
->set_option(
new DeactivationModal\Model\FormOption(
'temporary_deactivation',
50,
__( 'This is a temporary deactivation', 'webp-converter-for-media' ),
null,
null
)
)
->set_option(
new DeactivationModal\Model\FormOption(
'other',
60,
__( 'Other reason', 'webp-converter-for-media' ),
null,
__( 'What is the reason? What can we improve for you?', 'webp-converter-for-media' )
)
),
( new DeactivationModal\Model\FormValues() )
->set_value(
new DeactivationModal\Model\FormValue(
'request_error_codes',
function () {
return implode( ',', apply_filters( 'webpc_server_errors', [] ) );
}
)
)
->set_value(
new DeactivationModal\Model\FormValue(
'request_plugin_settings',
function () {
$settings_json = json_encode( $this->plugin_data->get_plugin_settings_public() );
return base64_encode( $settings_json ?: '' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions
}
)
)
->set_value(
new DeactivationModal\Model\FormValue(
'request_plugin_stats',
function () {
$stats_data = [
'usage_time' => $this->stats_manager->get_plugin_usage_time(),
'first_version' => $this->stats_manager->get_plugin_first_version(),
'regeneration_images' => $this->stats_manager->get_regeneration_images(),
'webp_all' => $this->stats_manager->get_images_webp_all(),
'webp_unconverted' => $this->stats_manager->get_images_webp_unconverted(),
'avif_all' => $this->stats_manager->get_images_avif_all(),
'avif_unconverted' => $this->stats_manager->get_images_avif_unconverted(),
'rewrite_root' => PathsGenerator::get_rewrite_root(),
'rewrite_path' => PathsGenerator::get_rewrite_path(),
];
$stats_json = json_encode( $stats_data );
return base64_encode( $stats_json ?: '' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions
}
)
)
->set_value(
new DeactivationModal\Model\FormValue(
'request_plugin_version',
function () {
return $this->plugin_info->get_plugin_version();
}
)
)
);
}
/**
* @return string|null
*/
private function load_notice_message(): ?string {
if ( ( apply_filters( 'webpc_server_errors', [] ) !== [] ) || is_multisite() ) {
return null;
}
$images_all = $this->stats_manager->get_images_webp_all() ?: 0;
$images_left = $this->stats_manager->get_images_webp_unconverted() ?: 0;
if ( ( $images_all === 0 ) || ( $images_left === 0 ) ) {
return null;
}
return sprintf(
/* translators: %1$s: button label, %2$s: open anchor tag, %3$s: close anchor tag */
__( 'You have unconverted images on your website - click the %1$s button in %2$sthe plugin settings%3$s. This is all you need to do after installing the plugin.', 'webp-converter-for-media' ),
'"' . __( 'Start Bulk Optimization', 'webp-converter-for-media' ) . '"',
'<a href="' . esc_url( admin_url( 'upload.php?page=' . PageIntegrator::UPLOAD_MENU_PAGE ) ) . '">',
'</a>'
);
}
}
@@ -0,0 +1,18 @@
<?php
namespace WebpConverter\Service;
/**
* Allows to detect specific server environments.
*/
class EnvDetector {
public static function is_cdn_bunny(): bool {
if ( ! is_plugin_active( 'bunnycdn/bunnycdn.php' ) ) {
return false;
}
$status = (int) get_option( 'bunnycdn_cdn_status', 0 );
return ( $status === 1 );
}
}
@@ -0,0 +1,186 @@
<?php
namespace WebpConverter\Service;
/**
* Returns size of image downloaded based on server path or URL.
*/
class FileLoader {
const GLOBAL_LOGS_VARIABLE = 'webpc_logs';
/**
* Checks size of file by sending request using active image loader.
*
* @param string $url URL of image.
* @param bool $set_headers Whether to send headers to confirm that browser supports WebP?
* @param string|null $ver_param Additional GET param.
* @param string|null $debug_context .
*
* @return int
*/
public function get_file_size_by_url( string $url, bool $set_headers = true, ?string $ver_param = null, ?string $debug_context = null ): int {
$request_url = $this->get_curl_url( $url, $ver_param );
$request_headers = $this->get_curl_headers( $set_headers );
$connect = $this->get_curl_connection( $request_url, $request_headers );
if ( $connect === null ) {
return 0;
}
$response = curl_exec( $connect );
if ( ! is_string( $response ) ) {
$response = '';
}
$http_code = curl_getinfo( $connect, CURLINFO_HTTP_CODE );
$curl_error = curl_error( $connect );
curl_close( $connect );
if ( $debug_context !== null ) {
$this->log_request( $debug_context, $request_url, $set_headers, $http_code, $curl_error, strlen( $response ) );
}
return ( $http_code === 200 ) ? strlen( $response ) : 0;
}
/**
* Checks HTTP status of file by sending request using active image loader.
*
* @param string $url URL of image.
* @param bool $set_headers Whether to send headers to confirm that browser supports WebP?
* @param string|null $ver_param Additional GET param.
* @param string|null $debug_context .
*
* @return int
*/
public function get_file_status_by_url( string $url, bool $set_headers = true, ?string $ver_param = null, ?string $debug_context = null ): int {
$request_url = $this->get_curl_url( $url, $ver_param );
$request_headers = $this->get_curl_headers( $set_headers );
$connect = $this->get_curl_connection( $request_url, $request_headers );
if ( $connect === null ) {
return 0;
}
curl_exec( $connect );
$http_code = curl_getinfo( $connect, CURLINFO_HTTP_CODE );
$curl_error = curl_error( $connect );
curl_close( $connect );
if ( $debug_context !== null ) {
$this->log_request( $debug_context, $request_url, $set_headers, $http_code, $curl_error, null );
}
return $http_code;
}
/**
* Returns size of file.
*
* @param string $path Server path of file.
*
* @return int Size of file.
*/
public function get_file_size_by_path( string $path ): int {
return ( file_exists( $path ) ) ? ( filesize( $path ) ?: 0 ) : 0;
}
/**
* @param string $url URL of image.
* @param string|null $ver_param Additional GET param.
*
* @return string
*/
private function get_curl_url( string $url, ?string $ver_param = null ): string {
$image_url = $url;
if ( $ver_param !== null ) {
$image_url = add_query_arg( 'ver', $ver_param, $image_url );
}
if ( function_exists( 'is_plugin_active' ) && is_plugin_active( 'wccp-pro/preventer-index.php' ) ) {
$image_url = add_query_arg( 'wccp_pro_watermark_pass', '', $image_url );
}
return apply_filters( 'webpc_debug_image_url', $image_url );
}
/**
* @param bool $set_headers Whether to send headers to confirm that browser supports WebP?
*
* @return string[]
*/
private function get_curl_headers( bool $set_headers ): array {
$headers = ( $set_headers )
? [ 'Accept: image/webp,image/*' ]
: [ 'Accept: image/*' ];
foreach ( wp_get_nocache_headers() as $header_key => $header_value ) {
$headers[] = sprintf( '%s: %s', $header_key, $header_value );
}
return $headers;
}
/**
* @param string $url .
* @param string[] $headers .
*
* @return resource|null
*/
private function get_curl_connection( string $url, array $headers ) {
if ( ! function_exists( 'curl_init' ) ) {
return null;
}
$ch = curl_init( $url );
if ( $ch === false ) {
return null;
}
if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
curl_setopt( $ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC );
curl_setopt( $ch, CURLOPT_USERPWD, sprintf( '%1$s:%2$s', $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
}
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 0 );
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
curl_setopt( $ch, CURLOPT_FRESH_CONNECT, true );
curl_setopt( $ch, CURLOPT_TIMEOUT, 10 );
curl_setopt( $ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' );
curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
curl_setopt( $ch, CURLOPT_REFERER, PathsGenerator::get_site_url() );
return $ch;
}
/**
* @param string $debug_context .
* @param string $url .
* @param bool $is_webp_request .
* @param int $response_code .
* @param string|null $curl_error .
* @param int|null $response_length .
*
* @return void
*/
private function log_request(
string $debug_context,
string $url,
bool $is_webp_request,
int $response_code,
?string $curl_error = null,
?int $response_length = null
) {
if ( ! isset( $GLOBALS[ self::GLOBAL_LOGS_VARIABLE ] ) ) {
$GLOBALS[ self::GLOBAL_LOGS_VARIABLE ] = [];
}
$GLOBALS[ self::GLOBAL_LOGS_VARIABLE ][] = [
'context' => $debug_context,
'url' => $url,
'is_webp' => $is_webp_request,
'http_code' => $response_code,
'response' => $response_length,
'curl_error' => ( $curl_error === '' ) ? null : $curl_error,
];
}
}
@@ -0,0 +1,551 @@
<?php
namespace WebpConverter\Service;
use WebpConverter\Conversion\AttachmentPathsGenerator;
use WebpConverter\Conversion\Endpoint\RegenerateAttachmentEndpoint;
use WebpConverter\Conversion\Format\AvifFormat;
use WebpConverter\Conversion\Format\FormatFactory;
use WebpConverter\Conversion\Format\WebpFormat;
use WebpConverter\Conversion\LargerFilesOperator;
use WebpConverter\Conversion\OutputPathGenerator;
use WebpConverter\HookableInterface;
use WebpConverter\Model\Token;
use WebpConverter\PluginData;
use WebpConverter\Repository\TokenRepository;
use WebpConverter\Settings\Option\MediaStatsOption;
use WebpConverter\Settings\Page\PageIntegrator;
/**
* Generates information about conversion status in Media Library.
*/
class MediaStatusViewer implements HookableInterface {
/**
* @var PluginData
*/
private $plugin_data;
/**
* @var TokenRepository
*/
private $token_repository;
/**
* @var OutputPathGenerator
*/
private $output_path;
/**
* @var AttachmentPathsGenerator|null
*/
private $attachment = null;
/**
* @var Token|null
*/
private $token = null;
public function __construct(
PluginData $plugin_data,
TokenRepository $token_repository,
FormatFactory $format_factory,
?OutputPathGenerator $output_path = null
) {
$this->plugin_data = $plugin_data;
$this->token_repository = $token_repository;
$this->output_path = $output_path ?: new OutputPathGenerator( $format_factory );
}
/**
* {@inheritdoc}
*/
public function init_hooks() {
add_action( 'admin_init', [ $this, 'init_hooks_after_setup' ] );
add_filter( 'webpc_attachment_stats', [ $this, 'get_conversion_stats_for_attachment' ], 10, 3 );
}
/**
* @return void
* @internal
*/
public function init_hooks_after_setup() {
$plugin_settings = $this->plugin_data->get_plugin_settings();
if ( ! $plugin_settings[ MediaStatsOption::OPTION_NAME ] ) {
return;
}
add_filter( 'manage_media_columns', [ $this, 'add_custom_table_column' ] );
add_action( 'manage_media_custom_column', [ $this, 'print_table_column_value' ], 10, 2 );
add_action( 'attachment_submitbox_misc_actions', [ $this, 'print_attachment_sidebar_value' ], 20 );
add_filter( 'wp_prepare_attachment_for_js', [ $this, 'add_status_for_attachment_data' ], 10, 2 );
}
/**
* @param string $current_value .
* @param int $post_id .
* @param int|null $strategy_level .
*
* @return string|null
* @internal
*/
public function get_conversion_stats_for_attachment( string $current_value, int $post_id, ?int $strategy_level = null ): ?string {
$conversion_status = $this->get_conversion_status( $post_id, $strategy_level );
if ( $conversion_status === null ) {
return null;
}
return wp_kses( implode( PHP_EOL, $conversion_status ), $this->get_allowed_html_tags() );
}
/**
* @param string[] $columns .
*
* @return string[]
* @internal
*/
public function add_custom_table_column( array $columns ): array {
$columns['webpc_status'] = 'Converter for Media';
return $columns;
}
/**
* @param string $column_name .
* @param int $post_id .
*
* @return void
* @internal
*/
public function print_table_column_value( string $column_name, int $post_id ) {
if ( $column_name !== 'webpc_status' ) {
return;
}
$conversion_stats = $this->get_conversion_stats_for_attachment( '', $post_id );
if ( $conversion_stats === null ) {
return;
}
printf(
'<div id="webpc-attachment-trigger-%1$s-wrapper">%2$s</div>',
esc_attr( (string) $post_id ),
wp_kses( $conversion_stats, $this->get_allowed_html_tags() )
);
}
/**
* @param \WP_Post $post .
*
* @return void
* @internal
*/
public function print_attachment_sidebar_value( \WP_Post $post ) {
$conversion_stats = $this->get_conversion_stats_for_attachment( '', $post->ID );
if ( $conversion_stats === null ) {
return;
}
?>
<div class="misc-pub-section misc-pub-webpc">
<div id="webpc-attachment-trigger-<?php echo esc_attr( (string) $post->ID ); ?>-wrapper">
<?php echo wp_kses( $conversion_stats, $this->get_allowed_html_tags() ); ?>
</div>
<small>
<?php
echo wp_kses_post(
sprintf(
/* translators: %s: plugin name */
__( 'Optimized by: %s', 'webp-converter-for-media' ),
sprintf( '<a href="%1$s">%2$s</a>', esc_attr( PageIntegrator::get_settings_page_url() ), 'Converter for Media' )
)
);
?>
</small>
</div>
<?php
}
/**
* @param mixed[] $response .
* @param \WP_Post $attachment .
*
* @return mixed[]
* @internal
*/
public function add_status_for_attachment_data( array $response, \WP_Post $attachment ): array {
$source_post_id = (string) ( $_REQUEST['post_id'] ?? '' ); // phpcs:ignore WordPress.Security
if ( $source_post_id !== '0' ) {
return $response;
}
$conversion_stats = $this->get_conversion_stats_for_attachment( '', $attachment->ID );
if ( $conversion_stats === null ) {
return $response;
}
$response['compat'] = $response['compat'] ?? [];
$response['compat']['meta'] = $response['compat']['meta'] ?? '';
$response['compat']['meta'] .= sprintf(
'<br><div id="webpc-attachment-trigger-%1$s-wrapper">%2$s</div><small>%3$s</small>',
$attachment->ID,
$conversion_stats,
wp_kses_post(
sprintf(
/* translators: %s: plugin name */
__( 'Optimized by: %s', 'webp-converter-for-media' ),
sprintf( '<a href="%1$s">%2$s</a>', esc_attr( PageIntegrator::get_settings_page_url() ), 'Converter for Media' )
)
)
);
return $response;
}
/**
* @param int $post_id .
* @param int|null $strategy_level .
*
* @return string[]|null
*/
private function get_conversion_status( int $post_id, ?int $strategy_level = null ): ?array {
$this->attachment = $this->attachment ?: new AttachmentPathsGenerator( $this->plugin_data );
$this->token = $this->token ?: $this->token_repository->get_token();
$source_paths = $this->attachment->get_attachment_paths( $post_id );
if ( ! $source_paths ) {
return null;
}
$images_stats = $this->get_images_stats( $source_paths, $post_id );
if ( ! $images_stats ) {
return null;
}
$percent_values = array_filter(
array_column( $images_stats, 'optimized_percent' ),
function ( $value ) {
return ! is_null( $value );
}
);
$percent_average = ( $percent_values )
? -( 100 - round( array_sum( $percent_values ) / count( $percent_values ) ) )
: null;
$rows = [
sprintf(
/* translators: %s: percent value */
__( 'Average image size reduction: %s', 'webp-converter-for-media' ),
( $percent_average !== null )
?
sprintf(
'<abbr title="%1$s">%2$s</abbr>',
esc_html__( 'File size reduction of all thumbnails compared to the original ones.', 'webp-converter-for-media' ),
( '<strong>' . ( ( $percent_average > 0 ) ? ( '+' . $percent_average ) : $percent_average ) . '%</strong>' )
)
: '<strong>—</strong>'
),
'<br>',
'<div class="webpcMediaStat">',
sprintf(
'<input type="checkbox" class="webpcMediaStat__button" id="stats-webp-converter-for-media-attachment-%s">',
$post_id
),
sprintf(
'<label for="stats-webp-converter-for-media-attachment-%1$s" class="webpcMediaStat__buttonLabel webpcMediaStat__buttonLabel--unchecked button button-small">%2$s</label>',
$post_id,
sprintf(
/* translators: %s: files count */
__( 'Show stats for all thumbnails (%s)', 'webp-converter-for-media' ),
count( $images_stats )
)
),
sprintf(
'<label for="stats-webp-converter-for-media-attachment-%1$s" class="webpcMediaStat__buttonLabel webpcMediaStat__buttonLabel--checked button button-small">%2$s</label>',
$post_id,
__( 'Hide stats', 'webp-converter-for-media' )
),
'<div class="webpcMediaStat__wrapper">',
];
if ( ! $this->token->get_valid_status() ) {
$rows[] = '<div class="webpcMediaStat__notice">';
$rows[] = sprintf(
/* translators: %1$s: call to action, %2$s: format name, %3$s: percent value, %4$s: format name */
__( '%1$s and convert your images to the %2$s format, making them weigh about %3$s less than images converted only to the %4$s format.', 'webp-converter-for-media' ),
sprintf(
/* translators: %1$s: open anchor tag, %2$s: close anchor tag */
__( '%1$sUpgrade to PRO%2$s', 'webp-converter-for-media' ),
'<a href="https://url.mattplugins.com/converter-media-stats-notice-upgrade" target="_blank">',
' </a>'
),
'AVIF',
'50%',
'WebP'
);
$rows[] = '</div>';
}
foreach ( $images_stats as $images_stat ) {
$percent_value = -( 100 - $images_stat['optimized_percent'] );
$rows[] = sprintf(
'<div class="webpcMediaStat__item">
<div class="webpcMediaStat__itemProgress">
<div class="webpcMediaStat__itemProgressInner" style="width: %5$s%%;"></div>
</div>
<a href="%1$s" target="_blank" class="webpcMediaStat__itemLink">%2$s</a>
<br>
%3$s
<br>
%4$s
</div>',
$images_stat['file_url'],
basename( $images_stat['file_url'] ),
sprintf(
/* translators: %s: file size */
__( 'Original file size: %s', 'webp-converter-for-media' ),
sprintf( '<strong>%s</strong>', size_format( $images_stat['original_size'] ) )
),
( $images_stat['output_format'] )
?
sprintf(
/* translators: %1$s: format name, %2$s: file size */
__( 'Optimized file size in the %1$s format: %2$s', 'webp-converter-for-media' ),
$images_stat['output_format'],
sprintf(
'<strong>%1$s <abbr title="%2$s">(%3$s)</abbr></strong>',
size_format( $images_stat['optimized_size'] ),
sprintf(
/* translators: %s: format name */
__( 'Image size reduction after conversion to the %s format compared to the original one.', 'webp-converter-for-media' ),
$images_stat['output_format']
),
( $percent_value > 0 )
? sprintf( '+%s%%', $percent_value )
: sprintf( '%s%%', $percent_value )
)
)
:
sprintf(
/* translators: %s: file size */
__( 'Optimized file size: %s', 'webp-converter-for-media' ),
'<strong>-</strong>'
),
( $images_stat['output_format'] )
? $images_stat['optimized_percent']
: 0
);
}
$rows[] = '</div>';
$rows[] = '</div>';
$quality_levels = apply_filters( 'webpc_option_quality_levels', [ 75, 80, 85, 90, 95 ] );
$quality_levels = [
intval( $quality_levels[0] ?? 75 ),
intval( $quality_levels[1] ?? 80 ),
intval( $quality_levels[2] ?? 85 ),
intval( $quality_levels[3] ?? 90 ),
intval( $quality_levels[4] ?? 95 ),
0,
];
$rows[] = '<br>';
$rows[] = sprintf(
'<select id="webpc-attachment-trigger-%1$s" onchange="webpcConvertAttachment(this,%1$s);" data-api-path="%2$s|%3$s">%4$s</select><span id="webpc-attachment-trigger-%1$s-spinner" class="spinner no-float" hidden></span>',
$post_id,
RegenerateAttachmentEndpoint::get_route_url(),
RegenerateAttachmentEndpoint::get_route_nonce(),
implode(
'',
[
sprintf(
'<option value="%1$s" %2$s disabled>%3$s</option>',
'',
( $strategy_level === null ) ? 'selected' : '',
( $percent_average !== null )
? __( 'Re-optimize Now', 'webp-converter-for-media' )
: __( 'Optimize Now', 'webp-converter-for-media' )
),
sprintf(
'<option value="%1$s" %2$s>%3$s</option>',
$quality_levels[0],
( $strategy_level === $quality_levels[0] ) ? 'selected' : '',
sprintf(
/* translators: %s: strategy level */
'— ' . __( 'using Strategy %s', 'webp-converter-for-media' ),
sprintf( '%1$s (%2$s)', '#1', __( 'Lossy', 'webp-converter-for-media' ) )
)
),
sprintf(
'<option value="%1$s" %2$s>%3$s</option>',
$quality_levels[1],
( $strategy_level === $quality_levels[1] ) ? 'selected' : '',
sprintf(
/* translators: %s: strategy level */
'— ' . __( 'using Strategy %s', 'webp-converter-for-media' ),
'#2'
)
),
sprintf(
'<option value="%1$s" %2$s>%3$s</option>',
$quality_levels[2],
( $strategy_level === $quality_levels[2] ) ? 'selected' : '',
sprintf(
/* translators: %s: strategy level */
'— ' . __( 'using Strategy %s', 'webp-converter-for-media' ),
sprintf( '%1$s (%2$s)', '#3', __( 'Optimal', 'webp-converter-for-media' ) )
)
),
sprintf(
'<option value="%1$s" %2$s>%3$s</option>',
$quality_levels[3],
( $strategy_level === $quality_levels[3] ) ? 'selected' : '',
sprintf(
/* translators: %s: strategy level */
'— ' . __( 'using Strategy %s', 'webp-converter-for-media' ),
'#4'
)
),
sprintf(
'<option value="%1$s" %2$s>%3$s</option>',
$quality_levels[4],
( $strategy_level === $quality_levels[4] ) ? 'selected' : '',
sprintf(
/* translators: %s: strategy level */
'— ' . __( 'using Strategy %s', 'webp-converter-for-media' ),
sprintf( '%1$s (%2$s)', '#5', __( 'Lossless', 'webp-converter-for-media' ) )
)
),
( $percent_average !== null )
?
sprintf(
'<option value="%1$s" %2$s>%3$s</option>',
$quality_levels[5],
( $strategy_level === $quality_levels[5] ) ? 'selected' : '',
__( 'Restore Originals', 'webp-converter-for-media' )
)
: '',
]
)
);
return $rows;
}
/**
* @param string[] $source_paths .
* @param int $attachment_id .
*
* @return mixed[] {
* @type int $original_size .
* @type int|null $optimized_size .
* @type int|null $optimized_percent Size of optimized file compared to the original one (from >0 to <=100).
* @type string|null $output_format .
* @type string $file_url .
* }
*/
private function get_images_stats( array $source_paths, int $attachment_id ): array {
$file_url = wp_get_attachment_url( $attachment_id ) ?: null;
if ( $file_url ) {
$file_url = dirname( $file_url );
}
$items = [];
foreach ( $source_paths as $source_path ) {
$filesize_original = ( file_exists( $source_path ) ) ? ( filesize( $source_path ) ?: null ) : null;
if ( $filesize_original === null ) {
continue;
}
$output_path_webp = $this->output_path->get_path( $source_path, false, WebpFormat::FORMAT_EXTENSION );
$output_path_avif = $this->output_path->get_path( $source_path, false, AvifFormat::FORMAT_EXTENSION );
$filesize_avif = ( $output_path_avif )
? ( ( file_exists( $output_path_avif ) ) ? ( filesize( $output_path_avif ) ?: null ) : null )
: null;
$filesize_webp = ( $output_path_webp )
? ( ( file_exists( $output_path_webp ) ) ? ( filesize( $output_path_webp ) ?: null ) : null )
: null;
$status_avif = ( ( $filesize_avif !== null ) || file_exists( $output_path_avif . '.' . LargerFilesOperator::DELETED_FILE_EXTENSION ) );
$status_webp = ( ( $filesize_webp !== null ) || file_exists( $output_path_webp . '.' . LargerFilesOperator::DELETED_FILE_EXTENSION ) );
$items[] = [
'original_size' => $filesize_original,
'optimized_size' => ( $filesize_avif !== null )
? $filesize_avif
: ( ( $filesize_webp !== null )
? $filesize_webp
: ( ( $status_avif || $status_webp ) ? $filesize_original : null )
),
'optimized_percent' => ( $filesize_avif !== null )
? round( $filesize_avif / $filesize_original * 100 )
: ( ( $filesize_webp !== null )
? round( $filesize_webp / $filesize_original * 100 )
: ( ( $status_avif || $status_webp ) ? 100 : null )
),
'output_format' => ( $filesize_avif !== null )
? 'AVIF'
: ( ( ( $filesize_webp !== null ) || $status_webp )
? 'WebP'
: null
),
'file_url' => sprintf( '%1$s/%2$s', $file_url, basename( $source_path ) ),
];
}
return $items;
}
/**
* @return mixed[]
*/
private function get_allowed_html_tags(): array {
return [
'a' => [
'href' => [],
'class' => [],
'target' => [],
],
'abbr' => [
'title' => [],
],
'br' => [],
'div' => [
'id' => [],
'class' => [],
'style' => [],
],
'input' => [
'id' => [],
'type' => [],
'class' => [],
],
'label' => [
'for' => [],
'class' => [],
],
'option' => [
'value' => [],
'selected' => [],
'disabled' => [],
],
'select' => [
'id' => [],
'onchange' => [],
'data-api-path' => [],
],
'span' => [
'id' => [],
'class' => [],
'hidden' => [],
],
'strong' => [
'class' => [],
'titleyik mnb ' => [],
],
];
}
}
@@ -0,0 +1,50 @@
<?php
namespace WebpConverter\Service;
/**
* Provides access to options stored in database.
*/
class OptionsAccessManager {
/**
* @param string $option_name .
* @param mixed $default_value .
*
* @return mixed|null
*/
public static function get_option( string $option_name, $default_value = null ) {
if ( is_multisite() ) {
return get_site_option( $option_name, $default_value );
} else {
return get_option( $option_name, $default_value );
}
}
/**
* @param string $option_name .
* @param mixed $option_value .
*
* @return void
*/
public static function update_option( string $option_name, $option_value ) {
if ( is_multisite() ) {
update_site_option( $option_name, $option_value );
} else {
update_option( $option_name, $option_value );
}
}
/**
* @param string $option_name .
*
* @return void
*/
public static function delete_option( string $option_name ) {
if ( is_multisite() ) {
delete_site_option( $option_name );
} else {
delete_option( $option_name );
}
}
}
@@ -0,0 +1,69 @@
<?php
namespace WebpConverter\Service;
/**
* Manages generation of server paths.
*/
class PathsGenerator {
/**
* Returns path to root directory of WordPress installation.
*/
public static function get_wordpress_root_path(): string {
$root_dir = self::get_root_directory();
return apply_filters(
'webpc_site_root',
preg_replace( '/(\/|\\\\)/', DIRECTORY_SEPARATOR, $root_dir )
);
}
/**
* Returns path to DOCUMENT_ROOT, by default: "%{DOCUMENT_ROOT}/".
*/
public static function get_rewrite_root(): string {
$root_document = preg_replace( '/(\/|\\\\)/', '/', rtrim( self::get_document_root(), '\/' ) );
$root_document_real = preg_replace( '/(\/|\\\\)/', '/', rtrim( self::get_real_document_root(), '\/' ) );
$root_wordpress = preg_replace( '/(\/|\\\\)/', '/', rtrim( self::get_wordpress_root_path(), '\/' ) );
$root_path = trim( str_replace( $root_document_real ?: '', '', $root_wordpress ?: '' ), '\/' );
$root_suffix = str_replace( '//', '/', sprintf( '/%s/', $root_path ) );
return apply_filters(
'webpc_htaccess_rewrite_root',
( $root_document !== $root_document_real ) ? ( $root_wordpress . '/' ) : ( '%{DOCUMENT_ROOT}' . $root_suffix )
);
}
/**
* Returns prefix used before "wp-content/uploads-webpc/", by default: "/".
*/
public static function get_rewrite_path(): string {
$root_document_real = preg_replace( '/(\/|\\\\)/', '/', rtrim( self::get_real_document_root(), '\/' ) );
$root_wordpress = preg_replace( '/(\/|\\\\)/', '/', rtrim( self::get_wordpress_root_path(), '\/' ) );
$root_path = trim( str_replace( $root_document_real ?: '', '', $root_wordpress ?: '' ), '\/' );
return apply_filters(
'webpc_htaccess_rewrite_path',
str_replace( '//', '/', sprintf( '/%s/', $root_path ) )
);
}
public static function get_site_url(): string {
return apply_filters( 'webpc_site_url', ( defined( 'WP_HOME' ) ) ? WP_HOME : get_site_url() );
}
private static function get_root_directory(): string {
return ( defined( 'WP_CONTENT_DIR' ) ) ? dirname( WP_CONTENT_DIR ) : ABSPATH;
}
private static function get_document_root(): string {
return $_SERVER['DOCUMENT_ROOT']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
}
private static function get_real_document_root(): string {
return realpath( $_SERVER['DOCUMENT_ROOT'] ) ?: ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
}
}
@@ -0,0 +1,73 @@
<?php
namespace WebpConverter\Service;
use WebpConverter\Conversion\Endpoint\EndpointIntegrator;
use WebpConverter\HookableInterface;
/**
* Supports exceptions for blocked REST API endpoints.
*/
class RestApiUnlocker implements HookableInterface {
/**
* {@inheritdoc}
*/
public function init_hooks() {
add_filter( 'rest_authentication_errors', [ $this, 'clear_authentication_error' ], 9999 );
add_filter(
'option_mo_api_authentication_protectedrestapi_route_whitelist',
[ $this, 'handle_wp_rest_api_authentication_plugin' ]
);
add_filter( 'jwt_auth_whitelist', [ $this, 'handle_jwt_auth_plugin' ] );
}
/**
* @param \WP_Error|null|true $result .
*
* @return \WP_Error|null|true
* @internal
*/
public function clear_authentication_error( $result ) {
$current_route = untrailingslashit( $GLOBALS['wp']->query_vars['rest_route'] ?? '' );
if ( strpos( $current_route, '/' . EndpointIntegrator::ROUTE_NAMESPACE . '/' ) === 0 ) {
return true;
}
return $result;
}
/**
* @param array|mixed $all_routes .
*
* @return array|mixed
* @internal
*/
public function handle_wp_rest_api_authentication_plugin( $all_routes ) {
if ( ! is_array( $all_routes ) ) {
return $all_routes;
}
foreach ( $all_routes as $route_key => $route_path ) {
if ( strpos( $route_path, '/' . EndpointIntegrator::ROUTE_NAMESPACE . '/' ) === 0 ) {
unset( $all_routes[ $route_key ] );
}
}
return $all_routes;
}
/**
* @param array|mixed $white_routes .
*
* @return array|mixed
* @internal
*/
public function handle_jwt_auth_plugin( $white_routes ) {
if ( ! is_array( $white_routes ) ) {
return $white_routes;
}
$all_routes[] = '/wp-json/' . EndpointIntegrator::ROUTE_NAMESPACE . '/*';
return $all_routes;
}
}
@@ -0,0 +1,29 @@
<?php
namespace WebpConverter\Service;
/**
* Manages required server configuration.
*/
class ServerConfigurator {
/**
* @param int $value .
*
* @return void
*/
public function set_memory_limit( int $value = 2 ) {
ini_set( 'memory_limit', sprintf( '%sG', $value ) ); // phpcs:ignore
}
/**
* @param int $seconds .
*
* @return void
*/
public function set_execution_time( int $seconds = 120 ) {
if ( strpos( ini_get( 'disable_functions' ) ?: '', 'set_time_limit' ) === false ) {
set_time_limit( $seconds );
}
}
}
@@ -0,0 +1,90 @@
<?php
namespace WebpConverter\Service;
use WebpConverter\HookableInterface;
use WebpConverter\PluginData;
use WebpConverter\Settings\Option\AccessTokenOption;
use WebpConverter\Settings\Page\PageIntegrator;
/**
* Diagnoses the website and displays recommendations on the Site Health screen.
*/
class SiteHealthDetector implements HookableInterface {
const SITE_HEALTH_TEST_AVIF_FORMAT = 'webpc_avif_format';
/**
* @var PluginData
*/
private $plugin_data;
public function __construct( PluginData $plugin_data ) {
$this->plugin_data = $plugin_data;
}
/**
* {@inheritdoc}
*/
public function init_hooks() {
add_filter( 'site_status_tests', [ $this, 'add_test_to_check_avif_format' ] );
}
/**
* @param mixed[][] $tests .
*
* @return mixed[][]
* @internal
*/
public function add_test_to_check_avif_format( array $tests ): array {
$settings = $this->plugin_data->get_plugin_settings();
if ( isset( $settings[ AccessTokenOption::OPTION_NAME ] ) && $settings[ AccessTokenOption::OPTION_NAME ] ) {
return $tests;
}
$tests['direct'] = array_merge(
[
self::SITE_HEALTH_TEST_AVIF_FORMAT => [
'label' => __( 'Serve images in the AVIF format', 'webp-converter-for-media' ),
'test' => [ $this, 'perform_avif_format_test' ],
],
],
$tests['direct']
);
return $tests;
}
/**
* @return mixed[]
* @internal
*/
public function perform_avif_format_test(): array {
return [
'label' => __( 'Serve images in the AVIF format', 'webp-converter-for-media' ),
'status' => 'recommended',
'badge' => [
'label' => __( 'Performance' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
'color' => 'blue',
],
'description' => __( 'The AVIF format is the successor to the WebP format. Images converted to the AVIF format weigh about 50% less than images converted only to the WebP format, while maintaining better image quality.', 'webp-converter-for-media' ),
'actions' => sprintf(
'<p>%1$s</p><h4>%2$s</h4><p>%3$s</p><p>%4$s</p>',
sprintf(
/* translators: %1$s: plugin name, %2$s: format name */
__( 'The %1$s plugin you are using allows you to convert your images to the %2$s format.', 'webp-converter-for-media' ),
'<a href="' . PageIntegrator::get_settings_page_url() . '">Converter for Media</a>',
'AVIF'
),
__( 'How does the plugin work?', 'webp-converter-for-media' ),
__( 'When a browser tries to load an image file, the plugin checks if it supports the AVIF format (if enabled in the plugin settings). If so, the browser will receive the equivalent of the original image in the AVIF format. If it does not support AVIF, but supports the WebP format, the browser will receive the equivalent of the original image in WebP format. In case the browser does not support either WebP or AVIF, the original image is loaded. This means full support for all browsers.', 'webp-converter-for-media' ),
sprintf(
/* translators: %1$s: open anchor tag, %2$s: close anchor tag */
__( '%1$sUpgrade to PRO%2$s', 'webp-converter-for-media' ),
'<a href="https://url.mattplugins.com/converter-site-health-avif-format-upgrade" target="_blank">',
' </a>'
)
),
'test' => self::SITE_HEALTH_TEST_AVIF_FORMAT,
];
}
}
@@ -0,0 +1,154 @@
<?php
namespace WebpConverter\Service;
/**
* Manages the statistical data saved by the plugin.
*/
class StatsManager {
const STATS_INSTALLATION_DATE_OPTION = 'webpc_stats_installation_date';
const STATS_FIRST_VERSION_OPTION = 'webpc_stats_first_version';
const STATS_REGENERATION_IMAGES_OPTION = 'webpc_stats_regeneration_images';
const STATS_IMAGES_WEBP_ALL_OPTION = 'webpc_stats_webp_all';
const STATS_IMAGES_WEBP_UNCONVERTED_OPTION = 'webpc_stats_webp_unconverted';
const STATS_IMAGES_AVIF_ALL_OPTION = 'webpc_stats_avif_all';
const STATS_IMAGES_AVIF_UNCONVERTED_OPTION = 'webpc_stats_avif_unconverted';
/**
* @return void
*/
public function set_plugin_installation_date() {
if ( OptionsAccessManager::get_option( self::STATS_INSTALLATION_DATE_OPTION ) !== null ) {
return;
}
OptionsAccessManager::update_option(
self::STATS_INSTALLATION_DATE_OPTION,
gmdate( 'Y-m-d H:i:s' )
);
}
/**
* @return int|null
*/
public function get_plugin_usage_time(): ?int {
$installation_date = OptionsAccessManager::get_option( self::STATS_INSTALLATION_DATE_OPTION );
if ( $installation_date === null ) {
return null;
}
return ( strtotime( gmdate( 'Y-m-d H:i:s' ) ) - strtotime( $installation_date ) );
}
/**
* @param string $value .
*
* @return void
*/
public function set_plugin_first_version( string $value ) {
if ( OptionsAccessManager::get_option( self::STATS_FIRST_VERSION_OPTION ) !== null ) {
return;
}
OptionsAccessManager::update_option(
self::STATS_FIRST_VERSION_OPTION,
$value
);
}
/**
* @return string|null
*/
public function get_plugin_first_version(): ?string {
return OptionsAccessManager::get_option( self::STATS_FIRST_VERSION_OPTION, null );
}
/**
* @param int $value .
*
* @return void
*/
public function set_regeneration_images( int $value ) {
if ( OptionsAccessManager::get_option( self::STATS_REGENERATION_IMAGES_OPTION ) !== null ) {
return;
}
OptionsAccessManager::update_option( self::STATS_REGENERATION_IMAGES_OPTION, $value );
}
/**
* @return int|null
*/
public function get_regeneration_images(): ?int {
return OptionsAccessManager::get_option( self::STATS_REGENERATION_IMAGES_OPTION, null );
}
/**
* @param int $value .
*
* @return void
*/
public function set_images_webp_all( int $value ) {
OptionsAccessManager::update_option( self::STATS_IMAGES_WEBP_ALL_OPTION, $value );
}
/**
* @return int|null
*/
public function get_images_webp_all(): ?int {
$value = OptionsAccessManager::get_option( self::STATS_IMAGES_WEBP_ALL_OPTION, null );
return ( $value === null ) ? null : (int) $value;
}
/**
* @param int|null $value .
*
* @return void
*/
public function set_images_webp_unconverted( ?int $value = null ) {
OptionsAccessManager::update_option( self::STATS_IMAGES_WEBP_UNCONVERTED_OPTION, $value );
}
/**
* @return int|null
*/
public function get_images_webp_unconverted(): ?int {
$value = OptionsAccessManager::get_option( self::STATS_IMAGES_WEBP_UNCONVERTED_OPTION, null );
return ( $value === null ) ? null : (int) $value;
}
/**
* @param int $value .
*
* @return void
*/
public function set_images_avif_all( int $value ) {
OptionsAccessManager::update_option( self::STATS_IMAGES_AVIF_ALL_OPTION, $value );
}
/**
* @return int|null
*/
public function get_images_avif_all(): ?int {
$value = OptionsAccessManager::get_option( self::STATS_IMAGES_AVIF_ALL_OPTION, null );
return ( $value === null ) ? null : (int) $value;
}
/**
* @param int|null $value .
*
* @return void
*/
public function set_images_avif_unconverted( ?int $value = null ) {
OptionsAccessManager::update_option( self::STATS_IMAGES_AVIF_UNCONVERTED_OPTION, $value );
}
/**
* @return int|null
*/
public function get_images_avif_unconverted(): ?int {
$value = OptionsAccessManager::get_option( self::STATS_IMAGES_AVIF_UNCONVERTED_OPTION, null );
return ( $value === null ) ? null : (int) $value;
}
}
@@ -0,0 +1,86 @@
<?php
namespace WebpConverter\Service;
use WebpConverter\Model\Token;
use WebpConverter\Repository\TokenRepository;
use WebpConverter\WebpConverterConstants;
/**
* Checks the token status for the PRO version.
*/
class TokenValidator {
const API_TOKEN_SUCCESS_CODE = 200;
const REQUEST_INFO_OPTION = 'webpc_token_request_info';
/**
* @var TokenRepository
*/
private $token_repository;
/**
* @var Token
*/
private $token;
public function __construct( ?TokenRepository $token_repository = null ) {
$this->token_repository = $token_repository ?: new TokenRepository();
}
public function validate_token( ?string $token_value = null ): Token {
$this->token = $this->token_repository->get_token( $token_value );
$status = ( $token_value && $this->check_access_token( $token_value ) );
if ( $status ) {
$this->token_repository->save_token(
$this->token
->set_token_value( $token_value )
->set_valid_status( true )
);
} else {
$this->token_repository->remove_token();
}
return $this->token_repository->get_token( $token_value );
}
private function check_access_token( string $token_value ): bool {
$connect = curl_init( sprintf( WebpConverterConstants::API_TOKEN_VALIDATION_URL, $token_value ) );
if ( ! $connect ) {
return false;
}
curl_setopt( $connect, CURLOPT_SSL_VERIFYPEER, false );
curl_setopt( $connect, CURLOPT_SSL_VERIFYHOST, 0 );
curl_setopt( $connect, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $connect, CURLOPT_TIMEOUT, 10 );
curl_setopt( $connect, CURLOPT_POST, true );
curl_setopt(
$connect,
CURLOPT_POSTFIELDS,
[
'domain_host' => parse_url( get_site_url(), PHP_URL_HOST ),
]
);
$response = curl_exec( $connect );
$request_info = curl_getinfo( $connect );
curl_close( $connect );
if ( $request_info['http_code'] !== self::API_TOKEN_SUCCESS_CODE ) {
OptionsAccessManager::update_option( self::REQUEST_INFO_OPTION, $request_info );
return false;
}
$response_json = ( $response && is_string( $response ) ) ? json_decode( $response, true ) : null;
if ( ! $response_json ) {
return false;
}
$this->token->set_images_usage( $response_json[ WebpConverterConstants::API_RESPONSE_VALUE_LIMIT_USAGE ] );
$this->token->set_images_limit( $response_json[ WebpConverterConstants::API_RESPONSE_VALUE_LIMIT_MAX ] );
return true;
}
}
@@ -0,0 +1,37 @@
<?php
namespace WebpConverter\Service;
use WebpConverter\PluginInfo;
/**
* Supports loading views from /templates directory.
*/
class ViewLoader {
/**
* @var PluginInfo
*/
private $plugin_info;
public function __construct( PluginInfo $plugin_info ) {
$this->plugin_info = $plugin_info;
}
/**
* Loads view with given variables.
*
* @param string $path Server path relative to plugin root directory.
* @param mixed[] $params Variables for view.
*
* @return void
*/
public function load_view( string $path, array $params = [] ) {
extract( $params ); // phpcs:ignore
$view_path = sprintf( '%1$s/templates/%2$s', $this->plugin_info->get_plugin_directory_path(), $path );
if ( file_exists( $view_path ) ) {
/** @noinspection PhpIncludeInspection */ // phpcs:ignore
require_once $view_path;
}
}
}
@@ -0,0 +1,233 @@
<?php
namespace WebpConverter\Service;
use WebpConverter\Conversion\FilesTreeFinder;
use WebpConverter\Conversion\Format\AvifFormat;
use WebpConverter\Conversion\Format\FormatFactory;
use WebpConverter\Conversion\Format\WebpFormat;
use WebpConverter\Conversion\Method\MethodFactory;
use WebpConverter\Conversion\Method\MethodIntegrator;
use WebpConverter\Conversion\PathsFinder;
use WebpConverter\HookableInterface;
use WebpConverter\PluginData;
use WebpConverter\Repository\TokenRepository;
/**
* Registers the commands handled by WP_CLI.
*
* @see https://wp-cli.org
*/
class WpCliManager implements HookableInterface {
/**
* @var PluginData
*/
private $plugin_data;
/**
* @var TokenRepository
*/
private $token_repository;
/**
* @var MethodFactory
*/
private $method_factory;
/**
* @var FormatFactory
*/
private $format_factory;
public function __construct(
PluginData $plugin_data,
TokenRepository $token_repository,
MethodFactory $method_factory,
FormatFactory $format_factory
) {
$this->plugin_data = $plugin_data;
$this->token_repository = $token_repository;
$this->method_factory = $method_factory;
$this->format_factory = $format_factory;
}
/**
* {@inheritdoc}
*/
public function init_hooks() {
add_action( 'cli_init', [ $this, 'init_hooks_after_setup' ] );
}
/**
* @return void
* @internal
*/
public function init_hooks_after_setup() {
if ( ! class_exists( '\WP_CLI' ) ) {
return;
}
\WP_CLI::add_command(
'converter-for-media calculate',
[ $this, 'calculate_images' ],
[]
);
\WP_CLI::add_command(
'converter-for-media regenerate',
[ $this, 'regenerate_images' ],
[
'synopsis' => [
'type' => 'flag',
'name' => 'force',
'description' => __( 'Force the conversion of all images again', 'webp-converter-for-media' ),
],
]
);
\WP_CLI::add_command( 'webp-converter calculate', [ $this, 'calculate_images' ] );
\WP_CLI::add_command( 'webp-converter regenerate', [ $this, 'regenerate_images' ] );
}
/**
* @return void
*/
public function calculate_images() {
\WP_Cli::log(
__( 'How many images to convert are remaining on my website?', 'webp-converter-for-media' )
);
$stats_data = ( new FilesTreeFinder( $this->plugin_data, $this->format_factory ) )
->get_tree( [ WebpFormat::FORMAT_EXTENSION, AvifFormat::FORMAT_EXTENSION ] );
\WP_CLI::success(
sprintf(
/* translators: %1$s: images count, %2$s: images count */
__( '%1$s for AVIF and %2$s for WebP', 'webp-converter-for-media' ),
number_format( $stats_data['files_unconverted'][ AvifFormat::FORMAT_EXTENSION ], 0, '', ' ' ),
number_format( $stats_data['files_unconverted'][ WebpFormat::FORMAT_EXTENSION ], 0, '', ' ' )
)
);
}
/**
* @param string[] $args .
* @param string[] $assoc_args .
*
* @return void
*/
public function regenerate_images( array $args, array $assoc_args = [] ) {
$force_flag = ( isset( $assoc_args['force'] ) || in_array( '-force', $args ) );
$conversion_method = ( new MethodIntegrator( $this->plugin_data, $this->method_factory ) );
$method_used = $conversion_method->get_method_used();
if ( $method_used === null ) {
\WP_CLI::error(
sprintf(
/* translators: %1$s: open anchor tag, %2$s: close anchor tag */
__( 'GD or Imagick library is not installed on your server.', 'webp-converter-for-media' ) . ' ' . __( 'This means that you cannot convert images to the WebP format on your server, because it does not meet the plugin requirements described in %1$sthe plugin FAQ%2$s. This issue is not dependent on the plugin.', 'webp-converter-for-media' ),
'<a href="https://url.mattplugins.com/converter-error-libs-not-installed-faq" target="_blank">',
'</a>'
)
);
}
$paths_chunks = ( new PathsFinder( $this->plugin_data, $this->token_repository, $this->format_factory ) )
->get_paths_by_chunks( ! $force_flag );
$count = 0;
foreach ( $paths_chunks as $chunk_data ) {
$count += count( $chunk_data['files'] );
}
$progress = \WP_CLI\Utils\make_progress_bar(
__( 'Bulk Optimization', 'webp-converter-for-media' ),
$count
);
$size_before = 0;
$size_after = 0;
$files_all = 0;
$files_converted = 0;
foreach ( $paths_chunks as $chunk_data ) {
foreach ( $chunk_data['files'] as $images_paths ) {
$response = $conversion_method->init_conversion(
$this->parse_files_paths( $images_paths, $chunk_data['path'] ),
$force_flag,
true
);
if ( $response !== null ) {
foreach ( $response['errors'] as $error_message ) {
if ( ! $response['is_fatal_error'] ) {
\WP_CLI::warning( $error_message );
} else {
\WP_CLI::error( $error_message );
}
}
if ( $response['is_fatal_error'] ) {
return;
}
$size_before = $response['size']['before'];
$size_after = $response['size']['after'];
$files_all = $response['files']['webp_available'] + $response['files']['avif_available'];
$files_converted = $response['files']['webp_converted'] + $response['files']['avif_converted'];
}
$progress->tick();
}
}
$progress->finish();
\WP_CLI::success(
__( 'The process was completed successfully. Your images have been converted!', 'webp-converter-for-media' )
);
if ( $size_before > $size_after ) {
\WP_CLI::log(
sprintf(
/* translators: %s progress value */
__( 'Saving the weight of your images: %s', 'webp-converter-for-media' ),
$this->format_bytes( $size_before - $size_after )
)
);
}
\WP_CLI::log(
sprintf(
/* translators: %s images count */
__( 'Successfully converted files: %s', 'webp-converter-for-media' ),
$files_converted
)
);
\WP_CLI::log(
sprintf(
/* translators: %s images count */
__( 'Failed or skipped file conversion attempts: %s', 'webp-converter-for-media' ),
( $files_all - $files_converted )
)
);
}
/**
* @param string[] $paths .
* @param string $path_prefix .
*
* @return string[]
*/
private function parse_files_paths( array $paths, string $path_prefix ): array {
$items = [];
foreach ( $paths as $path ) {
$items[] = $path_prefix . '/' . $path;
}
return $items;
}
private function format_bytes( int $size ): string {
$suffixes = [ 'B', 'KB', 'MB', 'GB' ];
$base = floor( log( $size ) / log( 1024 ) );
return sprintf( '%.2f ' . $suffixes[ $base ], ( $size / pow( 1024, floor( $base ) ) ) );
}
}