--- a/robin-image-optimizer/admin/activation.php
+++ b/robin-image-optimizer/admin/activation.php
@@ -3,7 +3,7 @@
/**
* Activator for the Robin image optimizer
*
- * @see Factory480_Activator
+ * @see Factory600_Activator
* @version 1.0
*/
@@ -12,7 +12,7 @@
exit;
}
-class WIO_Activation extends Wbcr_Factory480_Activator {
+class WIO_Activation extends Wbcr_Factory600_Activator {
/**
* Runs activation actions.
@@ -52,7 +52,7 @@
RIO_Process_Queue::try_create_plugin_tables();
- WBCRFactory_Templates_134Helpers::flushPageCache();
+ WBCRFactory_Templates_759Helpers::flushPageCache();
WRIO_Plugin::app()->logger->info( 'Parent plugin installation complete!' );
}
--- a/robin-image-optimizer/admin/includes/classes/class-rio-optimize-template.php
+++ b/robin-image-optimizer/admin/includes/classes/class-rio-optimize-template.php
@@ -102,6 +102,7 @@
$attach_dimensions = $params['attach_dimensions'];
$attachment_file_size = $params['attachment_file_size'];
$is_skipped = $params['is_skipped'];
+ $is_supported_format = isset( $params['is_supported_format'] ) ? $params['is_supported_format'] : true;
$webp_size = $params['webp_size'];
$avif_size = isset( $params['avif_size'] ) ? $params['avif_size'] : 0;
$webp_percent = isset( $params['webp_percent'] ) ? $params['webp_percent'] : 0;
@@ -109,7 +110,12 @@
$backuped = isset( $params['backuped'] ) ? $params['backuped'] : false;
if ( $is_skipped ) {
- return ob_get_clean();
+ return (string) ob_get_clean();
+ }
+
+ // Don't show buttons for unsupported formats
+ if ( ! $is_supported_format ) {
+ return (string) ob_get_clean();
}
if ( $is_optimized ) {
@@ -394,6 +400,6 @@
}
}
- return ob_get_clean();
+ return (string) ob_get_clean();
}
}
--- a/robin-image-optimizer/admin/pages/class-rio-license.php
+++ b/robin-image-optimizer/admin/pages/class-rio-license.php
@@ -3,7 +3,7 @@
* License Page class
*
* Self-contained license management page without Factory Templates dependency.
- * Reuses existing factory-bootstrap-482 CSS framework.
+ * Reuses existing factory-bootstrap-500 CSS framework.
*
* @package Robin_Image_Optimizer
* @subpackage AdminPages
@@ -288,7 +288,7 @@
$min_height = $this->calculate_menu_height();
?>
<div id="WBCR" class="wrap">
- <div class="wbcr-factory-templates-134-impressive-page-template factory-bootstrap-482 factory-fontawesome-000">
+ <div class="wbcr-factory-templates-759-impressive-page-template factory-bootstrap-500 factory-fontawesome-000">
<div class="wbcr-factory-page wbcr-factory-page-<?php echo esc_attr( $this->id ); ?>">
<?php $this->render_header(); ?>
<div class="wbcr-factory-left-navigation-bar">
@@ -503,7 +503,7 @@
?>
<div id="license-manager"
- class="factory-bootstrap-482 onp-page-wrap <?php echo esc_attr( $license_type ); ?>-license-manager-content">
+ class="factory-bootstrap-500 onp-page-wrap <?php echo esc_attr( $license_type ); ?>-license-manager-content">
<?php if ( ! $has_pro ) : ?>
<?php $this->render_upgrade_banner(); ?>
--- a/robin-image-optimizer/admin/pages/class-rio-log.php
+++ b/robin-image-optimizer/admin/pages/class-rio-log.php
@@ -9,7 +9,7 @@
*
* @version 1.0
*/
-class WRIO_LogPage extends Wbcr_FactoryLogger149_PageBase {
+class WRIO_LogPage extends Wbcr_FactoryLogger359_PageBase {
/**
* {@inheritdoc}
--- a/robin-image-optimizer/admin/pages/class-rio-page.php
+++ b/robin-image-optimizer/admin/pages/class-rio-page.php
@@ -16,7 +16,7 @@
*
* @version 1.0
*/
-class WRIO_Page extends WBCRFactory_Templates_134Impressive {
+class WRIO_Page extends WBCRFactory_Templates_759Impressive {
/**
* {@inheritdoc}
--- a/robin-image-optimizer/admin/pages/class-rio-settings.php
+++ b/robin-image-optimizer/admin/pages/class-rio-settings.php
@@ -109,7 +109,7 @@
*
* @return void
* @since 1.0.0
- * @see Wbcr_FactoryPages480_AdminPage
+ * @see Wbcr_FactoryPages600_AdminPage
*/
public function assets( $scripts, $styles ) {
parent::assets( $scripts, $styles );
--- a/robin-image-optimizer/includes/class-rio-plugin.php
+++ b/robin-image-optimizer/includes/class-rio-plugin.php
@@ -9,11 +9,11 @@
*
* @version 1.0
*/
-class WRIO_Plugin extends Wbcr_Factory480_Plugin {
+class WRIO_Plugin extends Wbcr_Factory600_Plugin {
/**
* @see self::app()
- * @var Wbcr_Factory480_Plugin
+ * @var Wbcr_Factory600_Plugin
*/
private static $app;
@@ -82,12 +82,6 @@
// completely disable image size threshold
add_filter( 'big_image_size_threshold', '__return_false' );
- if ( wrio_is_license_activate() ) {
- if ( ! defined( 'FACTORY_ADVERTS_BLOCK' ) ) {
- define( 'FACTORY_ADVERTS_BLOCK', true );
- }
- }
-
if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
// Ajax files
require_once WRIO_PLUGIN_DIR . '/admin/ajax/backup.php';
@@ -173,7 +167,7 @@
/**
* The premium provider.
*
- * @var WBCRFactory_480PremiumProvider|null
+ * @var WBCRFactory_600PremiumProvider|null
*/
$license_manager = self::app()->premium;
if ( $license_manager ) {
@@ -182,7 +176,7 @@
/**
* The license data.
*
- * @var WBCRFactory_Freemius_170EntitiesLicense|null
+ * @var WBCRFactory_Freemius_Rio_600EntitiesLicense|null
*/
$license_data = $license_manager->get_license();
@@ -274,14 +268,18 @@
$page_id = $current_screen->id;
$page_slug = null;
- if ( 'settings_page_rio_general-wbcr_image_optimizer' === $page_id ) {
+ if ( 'toplevel_page_rio_general-robin-image-optimizer' === $page_id ) {
$page_slug = 'bulk-optimization';
- } elseif ( 'toplevel_page_rio_settings-wbcr_image_optimizer' === $page_id ) {
+ } elseif ( 'toplevel_page_io_folders_statistic-robin-image-optimizer' === $page_id ) {
+ $page_slug = 'custom-folders';
+ } elseif ( 'robin-image-optimizer_page_rio_settings-robin-image-optimizer' === $page_id ) {
$page_slug = 'settings';
- } elseif ( 'toplevel_page_wbcr_io_logger-wbcr_image_optimizer' === $page_id ) {
+ } elseif ( 'robin-image-optimizer_page_wbcr_io_logger-robin-image-optimizer' === $page_id ) {
$page_slug = 'error-log';
- } elseif ( 'toplevel_page_rio_license-wbcr_image_optimizer' === $page_id ) {
+ } elseif ( 'robin-image-optimizer_page_wrio_license' === $page_id ) {
$page_slug = 'license';
+ } elseif ( 'toplevel_page_io_nextgen_gallery_statistic-robin-image-optimizer' === $page_id ) {
+ $page_slug = 'nextgen-gallery';
}
if ( null === $page_slug ) {
@@ -337,7 +335,7 @@
* Используется для получения настроек плагина, информации о плагине, для доступа к вспомогательным
* классам.
*
- * @return Wbcr_Factory480_Plugin|WRIO_Plugin
+ * @return Wbcr_Factory600_Plugin|WRIO_Plugin
*/
public static function app() {
return self::$app;
@@ -386,6 +384,8 @@
require_once WRIO_PLUGIN_DIR . '/includes/classes/class-rio-media-library.php';
require_once WRIO_PLUGIN_DIR . '/includes/classes/processors/class-rio-server-abstract.php';
require_once WRIO_PLUGIN_DIR . '/includes/classes/class-rio-image-statistic.php';
+ require_once WRIO_PLUGIN_DIR . '/includes/classes/class-rio-image-query.php';
+ require_once WRIO_PLUGIN_DIR . '/includes/classes/class-rio-optimization-orchestrator.php';
require_once WRIO_PLUGIN_DIR . '/includes/classes/class-rio-backup.php';
require_once WRIO_PLUGIN_DIR . '/includes/classes/class-rio-optimization-tools.php';
@@ -406,6 +406,9 @@
// ----------------
require_once WRIO_PLUGIN_DIR . '/includes/classes/class-rio-cron.php';
new WRIO_Cron();
+
+ // Register cache invalidation hooks for WRIO_Image_Query
+ WRIO_Image_Query::register_hooks();
}
/**
--- a/robin-image-optimizer/includes/classes/class-rio-attachment.php
+++ b/robin-image-optimizer/includes/classes/class-rio-attachment.php
@@ -227,6 +227,18 @@
}
/**
+ * Read file size reliably within the current request.
+ * PHP caches stat() results; we clear cache for this path.
+ *
+ * @param mixed $file_path The file path.
+ *
+ * @return int
+ */
+ protected function get_file_size( $file_path ) {
+ return wrio_get_file_size( $file_path );
+ }
+
+ /**
* Оптимизация аттачмента.
*
* @param string $optimization_level Уровень оптимизации изображения.
@@ -315,7 +327,7 @@
$results['is_backed_up'] = $is_image_backuped;
- $original_main_size = filesize( $this->path );
+ $original_main_size = $this->get_file_size( $this->path );
// если файл большой - изменяем размер
if ( $this->isNeedResize() ) {
@@ -368,6 +380,7 @@
$results['final_size'] = 0;
$extra_data = [
+ 'original_main_size' => $original_main_size,
'main_optimized_data' => $optimized_img_data,
'thumbnails_optimized_data' => $this->optimizeImageSizes(),
];
@@ -387,7 +400,7 @@
// некоторые провайдеры не отдают оптимизированный размер, поэтому после замены файла получаем его сами
if ( ! $optimized_img_data['optimized_size'] ) {
clearstatcache();
- $optimized_img_data['optimized_size'] = filesize( $this->get( 'path' ) );
+ $optimized_img_data['optimized_size'] = $this->get_file_size( $this->get( 'path' ) );
}
// при отрицательной оптимизации ставим значение оригинала
@@ -511,8 +524,11 @@
$original_size = 0;
$optimized_size = 0;
$thumbnails_count = 0;
- $original_main_size = filesize( $this->get( 'path' ) );
- $original_size = $original_size + $original_main_size;
+ $original_main_size = (int) $extra_data->get_original_main_size();
+ if ( $original_main_size <= 0 ) {
+ $original_main_size = $this->get_file_size( $this->get( 'path' ) );
+ }
+ $original_size = $original_size + $original_main_size;
$this->replaceOriginalFile(
[
@@ -522,7 +538,7 @@
clearstatcache();
- $optimized_main_size = filesize( $this->get( 'path' ) );
+ $optimized_main_size = $this->get_file_size( $this->get( 'path' ) );
// при отрицательной оптимизации ставим значение оригинала
if ( $optimized_main_size > $original_main_size ) {
@@ -534,7 +550,7 @@
if ( is_array( $thumbnails['thumbnails'] ) ) {
foreach ( $thumbnails['thumbnails'] as $thumbnail_size => $thumbnail ) {
$thumbnail_file = $this->getImageSizePath( $thumbnail_size );
- $original_thumbnail_size = filesize( $thumbnail_file );
+ $original_thumbnail_size = $this->get_file_size( $thumbnail_file );
$original_size = $original_size + $original_thumbnail_size;
$this->replaceOriginalFile(
@@ -546,7 +562,7 @@
clearstatcache();
- $optimized_thumbnail_size = filesize( $thumbnail_file );
+ $optimized_thumbnail_size = $this->get_file_size( $thumbnail_file );
// при отрицательной оптимизации ставим значение оригинала
if ( $optimized_thumbnail_size > $original_thumbnail_size ) {
@@ -664,7 +680,7 @@
$original_file_size = 0;
if ( is_file( $path ) ) {
- $original_file_size = filesize( $path );
+ $original_file_size = $this->get_file_size( $path );
}
$optimized_img_data = $image_processor->process(
@@ -685,7 +701,7 @@
// некоторые провайдеры не отдают оптимизированный размер, поэтому после замены файла получаем его сами
if ( ! $optimized_img_data['optimized_size'] ) {
clearstatcache();
- $optimized_img_data['optimized_size'] = filesize( $path );
+ $optimized_img_data['optimized_size'] = $this->get_file_size( $path );
}
if ( ! $optimized_img_data['src_size'] ) {
$optimized_img_data['src_size'] = $original_file_size;
--- a/robin-image-optimizer/includes/classes/class-rio-backup.php
+++ b/robin-image-optimizer/includes/classes/class-rio-backup.php
@@ -422,6 +422,42 @@
}
/**
+ * Get the backup file path for an attachment size if it exists.
+ *
+ * @param int $attachment_id The attachment ID.
+ * @param string $size The image size (e.g., 'original', 'thumbnail', 'medium').
+ *
+ * @return string|null The backup file path if it exists, null otherwise.
+ * @since 1.0.0
+ */
+ public function getAttachmentBackupPath( $attachment_id, $size = 'original' ) {
+ $attachment_meta = wp_get_attachment_metadata( $attachment_id );
+
+ if ( empty( $attachment_meta ) || ! isset( $attachment_meta['file'] ) ) {
+ return null;
+ }
+
+ $backup_dir = $this->getAttachmentBackupDir( $attachment_meta );
+
+ if ( is_wp_error( $backup_dir ) ) {
+ return null;
+ }
+
+ // Get the filename based on size
+ if ( 'original' === $size ) {
+ $filename = wp_basename( $attachment_meta['file'] );
+ } elseif ( isset( $attachment_meta['sizes'][ $size ]['file'] ) ) {
+ $filename = $attachment_meta['sizes'][ $size ]['file'];
+ } else {
+ return null;
+ }
+
+ $backup_path = $backup_dir . $filename;
+
+ return file_exists( $backup_path ) ? $backup_path : null;
+ }
+
+ /**
* Удаляем резервные копии аттачмента
*
* @param int $attachment_id аттачмент id
--- a/robin-image-optimizer/includes/classes/class-rio-bulk-optimization.php
+++ b/robin-image-optimizer/includes/classes/class-rio-bulk-optimization.php
@@ -1,6 +1,6 @@
<?php
-use WBCRFactory_Processing_113WP_Background_Process;
+use WBCRFactory_Processing_759WP_Background_Process;
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
@@ -70,29 +70,9 @@
wp_die( - 1 );
}
- global $wpdb;
-
WRIO_Plugin::app()->deletePopulateOption( 'wrio_partial_total_count' );
- $allowed_formats_sql = wrio_get_allowed_formats( true );
-
- $sql = "SELECT COUNT(posts.ID) AS total_attachments
- FROM {$wpdb->posts} as posts
- WHERE post_type = 'attachment'
- AND post_status = 'inherit'
- AND post_mime_type IN ( {$allowed_formats_sql} )";
-
- // Если используется плагин WPML, исключаем дублирующие изображения
- if ( defined( 'WPML_PLUGIN_FILE' ) ) {
- $sql .= " AND NOT EXISTS
- (SELECT trnsl.element_id FROM {$wpdb->prefix}icl_translations as trnsl
- WHERE trnsl.element_id = posts.ID
- AND trnsl.element_type = 'post_attachment'
- AND source_language_code IS NOT NULL
- )";
- }
-
- $total_attachments = $wpdb->get_var( $sql );
+ $total_attachments = WRIO_Image_Query::get_instance()->count_total_attachments();
wp_send_json_success(
[
@@ -549,6 +529,34 @@
wp_die( - 1 );
}
+ // Use orchestrator for media-library scope
+ if ( 'media-library' === $scope ) {
+ if ( $reset_current_error ) {
+ $media_library = WRIO_Media_Library::get_instance();
+ $media_library->resetCurrentErrors();
+ }
+
+ $orchestrator = WRIO_Optimization_Orchestrator::get_instance();
+ $result = $orchestrator->execute_next_action( 1 );
+
+ if ( isset( $result['error'] ) ) {
+ $error_massage = $result['error'];
+
+ if ( empty( $error_massage ) ) {
+ $error_massage = __( "Unknown error. Enable error log on the plugin's settings page, then check the error report on the Error Log page. You can export the error report and send it to the support service of the plugin.", 'robin-image-optimizer' );
+ }
+
+ WRIO_Plugin::app()->logger->error( sprintf( 'Bulk optimization error: %s.', $error_massage ) );
+
+ wp_send_json_error( [ 'error_message' => $error_massage ] );
+ }
+
+ WRIO_Plugin::app()->logger->info( sprintf( 'End bulk optimization process! Scope: %s. Remain: %d', $scope, $result['remain'] ) );
+
+ wp_send_json_success( $result );
+ }
+
+ // Fall back to old behavior for custom-folders, nextgen, etc.
// Context class name. If plugin expands with add-ons
$class_name = 'WRIO_' . wrio_dashes_to_camel_case( $scope, true );
@@ -556,9 +564,7 @@
WRIO_Plugin::app()->logger->error( sprintf( 'Bulk optimization error: Context class (%s) not found.', $class_name ) );
// todo: Temporary bug fix.
- if ( 'media-library' === $scope ) {
- $class_name = 'WRIO_Media_Library';
- } elseif ( 'custom-folders' === $scope ) {
+ if ( 'custom-folders' === $scope ) {
$class_name = 'WRIO_Custom_Folders';
} elseif ( 'nextgen-gallery' == $scope ) {
$class_name = 'WRIO_Nextgen_Gallery';
@@ -573,7 +579,6 @@
* Create an instance of the class depending on the context in which scope user
* has runned optimization.
*
- * @see WRIO_Media_Library
* @see WRIO_Custom_Folders
* @see WRIO_Nextgen_Gallery
* @var WRIO_Media_Library $optimizer
@@ -584,7 +589,6 @@
$optimizer->resetCurrentErrors();
}
- // Step 1: Standard optimization
$result = $optimizer->processUnoptimizedImages( 1 );
if ( is_wp_error( $result ) ) {
@@ -599,40 +603,6 @@
wp_send_json_error( [ 'error_message' => $error_massage ] );
}
- // Step 2: WebP conversion (if enabled and scope is media-library)
- if ( 'media-library' === $scope && WRIO_Format_Converter_Factory::is_webp_enabled() ) {
- $webp_result = $optimizer->webpUnoptimizedImages( 1, 'webp' );
- if ( ! is_wp_error( $webp_result ) && ! empty( $webp_result['last_converted'] ) ) {
- // Merge WebP data into result
- if ( ! empty( $result['last_optimized'] ) ) {
- foreach ( $result['last_optimized'] as $key => $item ) {
- if ( isset( $webp_result['last_converted'][0] ) ) {
- $result['last_optimized'][ $key ]['webp_size'] = $webp_result['last_converted'][0]['webp_size'] ?? null;
- }
- }
- }
- // Update statistics with WebP data
- $result['statistic'] = $webp_result['statistic'];
- }
- }
-
- // Step 3: AVIF conversion (if enabled, licensed, and scope is media-library)
- if ( 'media-library' === $scope && WRIO_Format_Converter_Factory::is_avif_enabled() ) {
- $avif_result = $optimizer->webpUnoptimizedImages( 1, 'avif' );
- if ( ! is_wp_error( $avif_result ) && ! empty( $avif_result['last_converted'] ) ) {
- // Merge AVIF data into result
- if ( ! empty( $result['last_optimized'] ) ) {
- foreach ( $result['last_optimized'] as $key => $item ) {
- if ( isset( $avif_result['last_converted'][0] ) ) {
- $result['last_optimized'][ $key ]['avif_size'] = $avif_result['last_converted'][0]['avif_size'] ?? null;
- }
- }
- }
- // Update statistics with AVIF data
- $result['statistic'] = $avif_result['statistic'];
- }
- }
-
// If all images are processed, send completion command
if ( $result['remain'] <= 0 ) {
$result['end'] = true;
--- a/robin-image-optimizer/includes/classes/class-rio-image-query.php
+++ b/robin-image-optimizer/includes/classes/class-rio-image-query.php
@@ -0,0 +1,509 @@
+<?php
+/**
+ * Image Query class.
+ *
+ * @package Robin_Image_Optimizer
+ */
+
+// Exit if accessed directly
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * WRIO_Image_Query
+ */
+class WRIO_Image_Query {
+
+ /**
+ * The single instance of the class.
+ *
+ * @var WRIO_Image_Query|null
+ */
+ protected static $instance = null;
+
+ /**
+ * Cache for allowed formats SQL string
+ *
+ * @var string
+ */
+ protected $allowed_formats_sql;
+
+ /**
+ * Cache for required conversion types
+ *
+ * @var string[]|null
+ */
+ protected $required_types = null;
+
+ /**
+ * Cache group for all query results
+ *
+ * @var string
+ */
+ const CACHE_GROUP = 'wrio_image_query';
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ $formats = wrio_get_allowed_formats( true );
+ $this->allowed_formats_sql = is_array( $formats ) ? implode( ', ', $formats ) : $formats;
+ }
+
+ /**
+ * Register cache invalidation hooks.
+ *
+ * Call this once during plugin initialization to automatically
+ * clear query caches when images are optimized, restored, or deleted.
+ *
+ * @since 1.5.0
+ *
+ * @return void
+ */
+ public static function register_hooks() {
+ add_action( 'wbcr/riop/queue_item_saved', [ __CLASS__, 'clear_cache' ], 100 );
+ add_action( 'wbcr/rio/attachment_restored', [ __CLASS__, 'clear_cache' ], 100 );
+ add_action( 'delete_attachment', [ __CLASS__, 'clear_cache' ], 100 );
+ }
+
+ /**
+ * Get singleton instance
+ *
+ * @return WRIO_Image_Query
+ */
+ public static function get_instance() {
+ if ( null === self::$instance ) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Build list of required conversion types based on enabled formats.
+ * Computed fresh each call to avoid stale data.
+ *
+ * @since 1.5.0
+ *
+ * @return string[] Array of required item_types
+ */
+ protected function build_required_types() {
+ $types = [ 'attachment' ]; // Basic optimization always required
+
+ if ( class_exists( 'WRIO_Format_Converter_Factory' ) ) {
+ if ( WRIO_Format_Converter_Factory::is_webp_enabled() ) {
+ $types[] = 'webp';
+ }
+ if ( WRIO_Format_Converter_Factory::is_avif_enabled() ) {
+ $types[] = 'avif';
+ }
+ }
+
+ return $types;
+ }
+
+ /**
+ * Get required conversion types.
+ * Lazy loads on first access.
+ *
+ * @since 1.5.0
+ *
+ * @return string[]
+ */
+ public function get_required_types() {
+ if ( null === $this->required_types ) {
+ $this->required_types = $this->build_required_types();
+ }
+
+ return $this->required_types;
+ }
+
+ /**
+ * Get sanitized optimization order.
+ * Prevents SQL injection by validating order parameter.
+ *
+ * @since 1.5.0
+ *
+ * @return string 'ASC' or 'DESC'
+ */
+ protected function get_optimize_order() {
+ $order = WRIO_Plugin::app()->getOption( 'image_optimization_order', 'asc' );
+
+ // Whitelist validation - only allow 'DESC', all others default to 'ASC'
+ return strtolower( $order ) === 'desc' ? 'DESC' : 'ASC';
+ }
+
+ /**
+ * Get WPML exclusion clause for filtering translation duplicates.
+ *
+ * @since 1.5.0
+ *
+ * @return string SQL clause or empty string if WPML not active
+ */
+ protected function get_wpml_exclusion_clause() {
+ global $wpdb;
+
+ if ( ! defined( 'WPML_PLUGIN_FILE' ) ) {
+ return '';
+ }
+
+ return " AND NOT EXISTS (
+ SELECT trnsl.element_id
+ FROM {$wpdb->prefix}icl_translations AS trnsl
+ WHERE trnsl.element_id = posts.ID
+ AND trnsl.element_type = 'post_attachment'
+ AND trnsl.source_language_code IS NOT NULL
+ )";
+ }
+
+ /**
+ * Append pagination to SQL query.
+ *
+ * @since 1.5.0
+ *
+ * @param string $sql SQL query.
+ * @param int|null $limit Number of results to return.
+ * @param int $offset Number of results to skip.
+ *
+ * @return string SQL with pagination appended
+ */
+ protected function append_pagination( $sql, $limit = null, $offset = 0 ) {
+ if ( $limit ) {
+ $sql .= sprintf( ' LIMIT %d, %d', absint( $offset ), absint( $limit ) );
+ }
+
+ return $sql;
+ }
+
+ /**
+ * Get cached count or compute and cache result.
+ *
+ * @since 1.5.0
+ *
+ * @param string $cache_key Cache key (will be namespaced).
+ * @param callable $callback Callback that returns the count.
+ *
+ * @return int
+ */
+ protected function get_cached_count( $cache_key, $callback ) {
+ $cached = wp_cache_get( $cache_key, self::CACHE_GROUP );
+
+ if ( false !== $cached ) {
+ return (int) $cached;
+ }
+
+ $count = (int) call_user_func( $callback );
+
+ wp_cache_set( $cache_key, $count, self::CACHE_GROUP, HOUR_IN_SECONDS );
+
+ return $count;
+ }
+
+ /**
+ * Build base attachment query with common conditions.
+ *
+ * @since 1.5.0
+ *
+ * @param string $select_clause The SELECT portion (e.g., 'DISTINCT posts.ID').
+ *
+ * @return string
+ */
+ protected function get_base_query( $select_clause ) {
+ global $wpdb;
+
+ return "SELECT {$select_clause}
+ FROM {$wpdb->posts} posts
+ WHERE posts.post_type = 'attachment'
+ AND posts.post_status = 'inherit'
+ AND posts.post_mime_type IN ( {$this->allowed_formats_sql} )";
+ }
+
+ /**
+ * Build optimization status EXISTS clause.
+ *
+ * @since 1.5.0
+ *
+ * @param bool $negate Use NOT EXISTS instead of EXISTS.
+ * @param bool $all_types Check all required types are complete.
+ *
+ * @return string SQL clause
+ */
+ protected function get_optimization_exists_clause( $negate = false, $all_types = true ) {
+ $db_table = RIO_Process_Queue::table_name();
+ $exists = $negate ? 'NOT EXISTS' : 'EXISTS';
+ $types = $this->get_required_types();
+
+ $placeholders = implode( ',', array_fill( 0, count( $types ), '%s' ) );
+
+ $clause = "{$exists} (
+ SELECT 1
+ FROM {$db_table} rio
+ WHERE rio.object_id = posts.ID
+ AND rio.item_type IN ( {$placeholders} )
+ AND rio.result_status = 'success'";
+
+ if ( $all_types ) {
+ $clause .= '
+ GROUP BY rio.object_id
+ HAVING COUNT(DISTINCT rio.item_type) = ' . count( $types );
+ }
+
+ $clause .= '
+ )';
+
+ return $clause;
+ }
+
+ /**
+ * Build error status EXISTS clause.
+ *
+ * @since 1.5.0
+ *
+ * @return string SQL clause
+ */
+ protected function get_error_exists_clause() {
+ $db_table = RIO_Process_Queue::table_name();
+
+ return "EXISTS (
+ SELECT 1
+ FROM {$db_table} rio
+ WHERE rio.object_id = posts.ID
+ AND rio.result_status = 'error'
+ )";
+ }
+
+ /**
+ * Get IDs of fully optimized images.
+ *
+ * An image is optimized if it has successful conversions for ALL required types.
+ *
+ * @since 1.5.0
+ *
+ * @param int|null $limit Number of results to return. NULL for no limit.
+ * @param int $offset Number of results to skip.
+ *
+ * @return int[] Array of attachment IDs
+ */
+ public function get_optimized_ids( $limit = null, $offset = 0 ) {
+ global $wpdb;
+
+ $sql = $this->get_base_query( 'DISTINCT posts.ID' );
+ $sql .= ' AND ' . $this->get_optimization_exists_clause( false, true );
+ $sql .= ' ORDER BY posts.ID ' . $this->get_optimize_order();
+ $sql = $this->append_pagination( $sql, $limit, $offset );
+
+ $sql = $wpdb->prepare( $sql, $this->get_required_types() );
+
+ $result = $wpdb->get_col( $sql );
+
+ return array_map( 'absint', $result ?? [] );
+ }
+
+ /**
+ * Get IDs of unoptimized images.
+ *
+ * Unoptimized images are those missing ANY required conversion with success status.
+ * Includes: never queued, partial, failed, and processing images.
+ *
+ * @since 1.5.0
+ *
+ * @param int|null $limit Number of results to return. NULL for no limit.
+ * @param int $offset Number of results to skip.
+ * @param bool $exclude_wpml_dupes Whether to exclude WPML translation duplicates.
+ *
+ * @return int[] Array of attachment IDs
+ */
+ public function get_unoptimized_ids( $limit = null, $offset = 0, $exclude_wpml_dupes = true ) {
+ global $wpdb;
+
+ $sql = $this->get_base_query( 'DISTINCT posts.ID' );
+ $sql .= ' AND ' . $this->get_optimization_exists_clause( true, true );
+
+ if ( $exclude_wpml_dupes ) {
+ $sql .= $this->get_wpml_exclusion_clause();
+ }
+
+ $sql .= ' ORDER BY posts.ID ' . $this->get_optimize_order();
+ $sql = $this->append_pagination( $sql, $limit, $offset );
+
+ $sql = $wpdb->prepare( $sql, $this->get_required_types() );
+
+ $result = $wpdb->get_col( $sql );
+
+ return array_map( 'absint', $result ?? [] );
+ }
+
+ /**
+ * Get IDs of images with optimization errors.
+ *
+ * @since 1.5.0
+ *
+ * @param int|null $limit Number of results to return. NULL for no limit.
+ * @param int $offset Number of results to skip.
+ *
+ * @return int[] Array of attachment IDs
+ */
+ public function get_error_ids( $limit = null, $offset = 0 ) {
+ global $wpdb;
+
+ $sql = $this->get_base_query( 'DISTINCT posts.ID' );
+ $sql .= ' AND ' . $this->get_error_exists_clause();
+ $sql .= ' ORDER BY posts.ID ASC';
+ $sql = $this->append_pagination( $sql, $limit, $offset );
+
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query built with safe methods, no user input.
+ $result = $wpdb->get_col( $sql );
+
+ return array_map( 'absint', $result ?? [] );
+ }
+
+ /**
+ * Count fully optimized images.
+ *
+ * @since 1.5.0
+ *
+ * @return int
+ */
+ public function count_optimized() {
+ return $this->get_cached_count(
+ 'count_optimized',
+ function () {
+ global $wpdb;
+
+ $sql = $this->get_base_query( 'COUNT(DISTINCT posts.ID)' );
+ $sql .= ' AND ' . $this->get_optimization_exists_clause( false, true );
+
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query built with safe methods and prepared with types.
+ $sql = $wpdb->prepare( $sql, $this->get_required_types() );
+
+ return (int) $wpdb->get_var( $sql );
+ }
+ );
+ }
+
+ /**
+ * Count unoptimized images.
+ *
+ * @since 1.5.0
+ *
+ * @param bool $exclude_wpml_dupes Whether to exclude WPML translation duplicates.
+ *
+ * @return int
+ */
+ public function count_unoptimized( $exclude_wpml_dupes = true ) {
+ $cache_suffix = $exclude_wpml_dupes ? '1' : '0';
+ $cache_key = "count_unoptimized_{$cache_suffix}";
+
+ return $this->get_cached_count(
+ $cache_key,
+ function () use ( $exclude_wpml_dupes ) {
+ global $wpdb;
+
+ $sql = $this->get_base_query( 'COUNT(DISTINCT posts.ID)' );
+ $sql .= ' AND ' . $this->get_optimization_exists_clause( true, true );
+
+ if ( $exclude_wpml_dupes ) {
+ $sql .= $this->get_wpml_exclusion_clause();
+ }
+
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query built with safe methods and prepared with types.
+ $sql = $wpdb->prepare( $sql, $this->get_required_types() );
+
+ return (int) $wpdb->get_var( $sql );
+ }
+ );
+ }
+
+ /**
+ * Count images with optimization errors.
+ *
+ * @since 1.5.0
+ *
+ * @return int
+ */
+ public function count_error() {
+ return $this->get_cached_count(
+ 'count_error',
+ function () {
+ global $wpdb;
+
+ $sql = $this->get_base_query( 'COUNT(DISTINCT posts.ID)' );
+ $sql .= ' AND ' . $this->get_error_exists_clause();
+
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query built with safe methods, no user input.
+ return (int) $wpdb->get_var( $sql );
+ }
+ );
+ }
+
+ /**
+ * Count total attachment images with allowed formats.
+ *
+ * @since 1.5.0
+ *
+ * @param bool $exclude_wpml_dupes Whether to exclude WPML translation duplicates.
+ *
+ * @return int
+ */
+ public function count_total_attachments( $exclude_wpml_dupes = true ) {
+ $cache_suffix = $exclude_wpml_dupes ? '1' : '0';
+ $cache_key = "count_total_attachments_{$cache_suffix}";
+
+ return $this->get_cached_count(
+ $cache_key,
+ function () use ( $exclude_wpml_dupes ) {
+ global $wpdb;
+
+ $sql = $this->get_base_query( 'COUNT(DISTINCT posts.ID)' );
+
+ if ( $exclude_wpml_dupes ) {
+ $sql .= $this->get_wpml_exclusion_clause();
+ }
+
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query built with safe methods, no user input.
+ return (int) $wpdb->get_var( $sql );
+ }
+ );
+ }
+
+ /**
+ * Clear all query caches.
+ *
+ * Call after image optimization, restoration, or deletion.
+ *
+ * @since 1.5.0
+ *
+ * @return void
+ */
+ public static function clear_cache() {
+ $keys = [
+ 'count_optimized',
+ 'count_unoptimized_0',
+ 'count_unoptimized_1',
+ 'count_error',
+ 'count_total_attachments_0',
+ 'count_total_attachments_1',
+ ];
+
+ foreach ( $keys as $key ) {
+ wp_cache_delete( $key, self::CACHE_GROUP );
+ }
+ }
+
+ /**
+ * Refresh instance data after settings change.
+ *
+ * Use this if WebP/AVIF settings are changed mid-request.
+ *
+ * @since 1.5.0
+ *
+ * @return void
+ */
+ public function refresh() {
+ $this->required_types = null;
+ $formats = wrio_get_allowed_formats( true );
+ $this->allowed_formats_sql = is_array( $formats ) ? implode( ', ', $formats ) : $formats;
+ self::clear_cache();
+ }
+}
--- a/robin-image-optimizer/includes/classes/class-rio-image-statistic.php
+++ b/robin-image-optimizer/includes/classes/class-rio-image-statistic.php
@@ -76,6 +76,18 @@
}
/**
+ * Read file size reliably within the current request.
+ * PHP caches stat() results; we clear cache for this path.
+ *
+ * @param mixed $file_path The file path.
+ *
+ * @return int
+ */
+ protected function get_file_size( $file_path ) {
+ return wrio_get_file_size( $file_path );
+ }
+
+ /**
* Get the statistic data.
*
* @return array{
@@ -171,47 +183,14 @@
$webp_optimized_size = WRIO_Plugin::app()->getOption( 'webp_optimized_size', 0 );
$avif_optimized_size = WRIO_Plugin::app()->getOption( 'avif_optimized_size', 0 );
- $allowed_formats_sql = wrio_get_allowed_formats( true );
-
- global $wpdb;
- $sql = "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'attachment' AND post_status = 'inherit' AND post_mime_type IN ( {$allowed_formats_sql} );";
- $total_images = $wpdb->get_var( $sql );
- $error_count = RIO_Process_Queue::count_by_type_status( 'attachment', 'error' );
- // Count images that have ALL required conversions complete
- // Build list of required item_types based on what's enabled
- $required_types = [ 'attachment' ]; // Basic optimization always required
-
- if ( class_exists( 'WRIO_Format_Converter_Factory' ) ) {
- if ( WRIO_Format_Converter_Factory::is_webp_enabled() ) {
- $required_types[] = 'webp';
- }
- if ( WRIO_Format_Converter_Factory::is_avif_enabled() ) {
- $required_types[] = 'avif';
- }
- }
-
- $db_table = RIO_Process_Queue::table_name();
- $placeholders = implode( ',', array_fill( 0, count( $required_types ), '%s' ) );
- $sql_optimized = $wpdb->prepare(
- "SELECT COUNT(DISTINCT p.ID)
- FROM {$wpdb->posts} p
- WHERE p.post_type = 'attachment'
- AND p.post_status = 'inherit'
- AND p.post_mime_type IN ( {$allowed_formats_sql} )
- AND (
- SELECT COUNT(DISTINCT rio.item_type)
- FROM {$db_table} rio
- WHERE rio.object_id = p.ID
- AND rio.item_type IN ( {$placeholders} )
- AND rio.result_status = 'success'
- ) = %d",
- array_merge( $required_types, [ count( $required_types ) ] )
- );
- $optimized_count = (int) $wpdb->get_var( $sql_optimized );
+ $image_query = WRIO_Image_Query::get_instance();
+ $total_images = $image_query->count_total_attachments( false ); // false = don't exclude WPML dupes for total display
+ $optimized_count = $image_query->count_optimized();
+ $error_count = $image_query->count_error(); // Count images with ANY error (attachment, webp, or avif)
$webp_optimized_count = RIO_Process_Queue::count_by_type_status( 'webp', 'success' );
- $webp_error_count = RIO_Process_Queue::count_by_type_status( 'webp', 'error' );
+ $webp_error_count = (int) RIO_Process_Queue::count_by_type_status( 'webp', 'error' );
$avif_optimized_count = RIO_Process_Queue::count_by_type_status( 'avif', 'success' );
- $avif_error_count = RIO_Process_Queue::count_by_type_status( 'avif', 'error' );
+ $avif_error_count = (int) RIO_Process_Queue::count_by_type_status( 'avif', 'error' );
if ( ! $total_images ) {
$total_images = 0;
@@ -219,20 +198,11 @@
if ( ! $error_count ) {
$error_count = 0;
}
- if ( ! $webp_error_count ) {
- $webp_error_count = 0;
- }
- if ( ! $avif_error_count ) {
- $avif_error_count = 0;
- }
if ( ! $optimized_count ) {
$optimized_count = 0;
}
- // unoptimized count: all - optimized - error
- $unoptimized_count = static::get_unoptimized_count();
- if ( $unoptimized_count < 0 ) {
- $unoptimized_count = 0;
- }
+ // Unoptimized = total - optimized - errors (ensures mutual exclusivity)
+ $unoptimized_count = max( 0, $total_images - $optimized_count - $error_count );
// WebP stats
$unconverted_count = static::get_unconverted_count( 'webp' );
@@ -276,12 +246,8 @@
}
$optimized_images_percent = 0;
- if ( $total_images ) {
- if ( $optimized_count >= $total_images ) {
- $optimized_images_percent = 100;
- } else {
- $optimized_images_percent = floor( $optimized_count * 100 / $total_images );
- }
+ if ( $total_images > 0 ) {
+ $optimized_images_percent = floor( $optimized_count * 100 / $total_images );
}
$processor = WIO_OptimizationTools::getImageProcessor();
@@ -330,44 +296,7 @@
* @since 1.3.6
*/
public static function get_unoptimized_count() {
- global $wpdb;
- $db_table = RIO_Process_Queue::table_name();
-
- $allowed_formats_sql = wrio_get_allowed_formats( true );
-
- // Build list of required item_types based on what's enabled
- $required_types = [ 'attachment' ]; // Basic optimization always required
-
- if ( class_exists( 'WRIO_Format_Converter_Factory' ) ) {
- if ( WRIO_Format_Converter_Factory::is_webp_enabled() ) {
- $required_types[] = 'webp';
- }
- if ( WRIO_Format_Converter_Factory::is_avif_enabled() ) {
- $required_types[] = 'avif';
- }
- }
-
- // Count images that are missing ANY required conversion
- // An image is unoptimized if it doesn't have ALL required item_types with success status
- $placeholders = implode( ',', array_fill( 0, count( $required_types ), '%s' ) );
-
- $sql = $wpdb->prepare(
- "SELECT COUNT(DISTINCT p.ID)
- FROM {$wpdb->posts} p
- WHERE p.post_type = 'attachment'
- AND p.post_status = 'inherit'
- AND p.post_mime_type IN ( {$allowed_formats_sql} )
- AND (
- SELECT COUNT(DISTINCT rio.item_type)
- FROM {$db_table} rio
- WHERE rio.object_id = p.ID
- AND rio.item_type IN ( {$placeholders} )
- AND rio.result_status = 'success'
- ) < %d",
- array_merge( $required_types, [ count( $required_types ) ] )
- );
-
- return (int) $wpdb->get_var( $sql );
+ return WRIO_Image_Query::get_instance()->count_unoptimized();
}
/**
@@ -481,10 +410,7 @@
* @type string $file_name Имя файла
* @type string $url URL
* @type string $thumbnail_url URL превьюшки
- * @type string $original_size Размер до оптимизации
* @type string $optimized_size Размер после оптимизации
- * @type string $webp_size webP размер
- * @type string $original_saving На сколько процентов изменился главный файл
* @type string $thumbnails_count Сколько превьюшек оптимизировано
* @type string $total_saving Процент оптимизации главного файла и превьюшек
* }
@@ -492,92 +418,81 @@
public function get_last_optimized_images( $limit = 100 ) {
global $wpdb;
- $items = [];
$db_table = RIO_Process_Queue::table_name();
- if ( $limit ) {
- $sql = $wpdb->prepare(
- "SELECT * FROM {$db_table}
- WHERE item_type IN ('attachment', 'webp') AND result_status IN (%s, %s)
- ORDER BY id DESC
- LIMIT %d ;",
- RIO_Process_Queue::STATUS_SUCCESS,
- RIO_Process_Queue::STATUS_ERROR,
- $limit
- );
- } else {
- $sql = $wpdb->prepare(
- "SELECT * FROM {$db_table}
- WHERE item_type IN ('attachment', 'webp') AND result_status IN (%s, %s)
- ORDER BY id DESC;",
- RIO_Process_Queue::STATUS_SUCCESS,
- RIO_Process_Queue::STATUS_ERROR
- );
- }
- $optimized_images = $wpdb->get_results( $sql, ARRAY_A );
+ $limit = max( 0, (int) $limit );
- $items = [];
- if ( ! empty( $optimized_images ) ) {
- foreach ( $optimized_images as $row ) {
- $object_id = $row['object_id'];
- if ( $row['item_type'] == 'attachment' ) {
- $items[ $object_id ] = $this->format_for_log( new RIO_Process_Queue( $row ) );
- }
+ $sql = $wpdb->prepare(
+ "SELECT object_id FROM {$db_table}
+ WHERE result_status IN (%s, %s)
+ ORDER BY id DESC
+ LIMIT %d;",
+ RIO_Process_Queue::STATUS_SUCCESS,
+ RIO_Process_Queue::STATUS_ERROR,
+ $limit
+ );
- if ( $row['item_type'] == 'webp' && $this->is_original_webp( $row ) ) {
- if ( ! isset( $items[ $object_id ] ) ) {
- $items[ $object_id ] = $this->format_for_log( new RIO_Process_Queue( $row ) );
- }
- }
- }
- }
+ $optimized_images_logs = $wpdb->get_results( $sql, ARRAY_A );
- return $items;
- }
+ $optimized_attachment_ids = [];
+ foreach ( $optimized_images_logs as $log ) {
+ $optimized_attachment_ids[] = $log['object_id'];
+ }
+ $optimized_attachment_ids = array_unique( $optimized_attachment_ids );
- /**
- * @param $row
- *
- * @return bool
- */
- public function is_original_webp( $row ) {
- if ( $row['item_type'] == 'webp' ) {
- $extra = json_decode( $row['extra_data'], true );
- if ( isset( $extra['converted_from_size'] ) && $extra['converted_from_size'] == 'original' ) {
- return true;
+ $optimized_attachment = [];
+ foreach ( $optimized_attachment_ids as $attachment_id ) {
+ $log_data = $this->get_last_optimized_image( $attachment_id );
+ if ( ! empty( $log_data ) ) {
+ $optimized_attachment[] = $log_data[0];
}
}
- return false;
+ return $optimized_attachment;
}
/**
- * @param int $object_id
+ * Get the last optimized image record for a specific attachment.
+ * Uses the same data source as get_last_optimized_images() for consistency.
*
+ * @param int $attachment_id Attachment ID.
+ *
+ * @return array<int, array<string, mixed>>
* @since 1.3.9
*/
- public function get_last_optimized_image( $object_id ) {
- global $wpdb;
+ public function get_last_optimized_image( $attachment_id ) {
+ $info = WRIO_Media_Library::get_instance()->calculateMediaLibraryParams( $attachment_id );
- $items = [];
- $db_table = RIO_Process_Queue::table_name();
- $sql = $wpdb->prepare(
- "SELECT * FROM {$db_table}
- WHERE object_id = '%d' AND item_type = 'attachment' AND result_status IN (%s, %s)
- ORDER BY id DESC
- LIMIT 1;",
- (int) $object_id,
- RIO_Process_Queue::STATUS_SUCCESS,
- RIO_Process_Queue::STATUS_ERROR
- );
-
- $model = $wpdb->get_row( $sql, ARRAY_A );
+ $best_optimized_size = ! empty( $info['optimized_size'] ) ? $info['optimized_size'] : 0;
+ if ( ! empty( $info['webp_size'] ) ) {
+ $best_optimized_size = min( $best_optimized_size, $info['webp_size'] );
+ }
+
+ if ( ! empty( $info['avif_size'] ) ) {
+ $best_optimized_size = min( $best_optimized_size, $info['avif_size'] );
+ }
+
+ $original_size = ! empty( $info['original_size'] ) ? $info['original_size'] : 0;
+ $best_optimized_size = min( $best_optimized_size, $original_size );
+
+ $log = [
+ 'id' => $attachment_id,
+ 'file_name' => $info['original_name'],
+ 'url' => $info['edit_url'],
+ 'thumbnail_url' => $info['original_url'],
+ 'original_size' => size_format( $original_size, 2 ),
+ 'optimized_size' => size_format( $best_optimized_size, 2 ),
+ 'thumbnails_count' => ! empty( $info['thumbnails_optimized'] ) ? $info['thumbnails_optimized'] : 0,
+ 'total_saving' => ! empty( $info['diff_percent_all'] ) ? $info['diff_percent_all'] . '%' : '0%',
+ ];
- if ( ! empty( $model ) ) {
- $items[] = $this->format_for_log( new RIO_Process_Queue( $model ) );
+ // Check errors.
+ if ( ! empty( $info['error_msg'] ) ) {
+ $log['type'] = 'error';
+ $log['error_msg'] = $info['error_msg'];
}
- return $items;
+ return [ $log ];
}
/**
@@ -617,10 +532,13 @@
}
/**
- * @param RIO_Process_Queue $queue_model
+ * Format a queue record for the optimization log display.
+ * Works universally for attachment, webp, and avif item types.
*
- * @return array
- * @throws Exception
+ * @param RIO_Process_Queue $queue_model Queue model instance.
+ *
+ * @return array<string, mixed>
+ * @throws Exception If invalid model provided.
* @since 1.3.9
*/
protected function format_for_log( $queue_model ) {
@@ -628,123 +546,111 @@
throw new Exception( 'Variable $queue_model must be an instance of RIO_Process_Queue!' );
}
- if ( $queue_model->item_type === 'webp' ) {
- return $this->format_webp_for_log( $queue_model );
- }
- /**
- * @var RIO_Attachment_Extra_Data $extra_data
- */
- $extra_data = $queue_model->get_extra_data();
-
- $default_formated_data = [
- 'id' => $queue_model->get_id(),
- 'url' => admin_url( sprintf( 'post.php?post=%d&action=edit', $queue_model->get_object_id() ) ),
- 'original_url' => null,
- 'thumbnail_url' => null,
- 'file_name' => null,
- 'original_size' => 0,
- 'optimized_size' => 0,
- 'type' => 'success',
- 'webp_size' => null,
- 'avif_size' => null,
- 'original_saving' => 0,
- 'thumbnails_count' => 0,
- 'total_saving' => 0,
+ $extra_data = $queue_model->get_extra_data();
+ $object_id = $queue_model->get_object_id();
+ $item_type = $queue_model->item_type;
+ $original_size = $queue_model->get_original_size();
+ $final_size = min( $original_size, $queue_model->get_final_size() );
+
+ $formatted_data = [
+ 'id' => $queue_model->get_id(),
+ 'attachment_id' => $object_id,
+ 'item_type' => $item_type,
+ 'url' => admin_url( sprintf( 'post.php?post=%d&action=edit', $object_id ) ),
+ 'original_url' => null,
+ 'thumbnail_url' => null,
+ 'file_name' => null,
+ 'original_size' => size_format( $original_size, 2 ),
+ 'original_size_bytes' => $original_size,
+ 'optimized_size' => size_format( $final_size, 2 ),
+ 'type' => 'success',
+ 'webp_size' => null,
+ 'avif_size' => null,
+ 'original_saving' => 0,
+ 'thumbnails_count' => 0,
+ 'total_saving' => 0,
+ 'final_size_bytes' => $final_size,
+ 'converted_from' => null,
];
- $upload_dir = wp_upload_dir();
-
- $attachment_meta = wp_get_attachment_metadata( $queue_model->get_object_id() );
- $formated_data = [];
-
- if ( ! empty( $attachment_meta ) ) {
- $image_url = trailingslashit( $upload_dir['baseurl'] ) . $attachment_meta['file'];
- $thumbnail_url = $image_url;
-
- if ( isset( $attachment_meta['sizes']['thumbnail'] ) ) {
- $image_basename = wp_basename( $image_url );
- $thumbnail_url = str_replace( $image_basename, $attachment_meta['sizes']['thumbnail']['file'], $image_url );
- }
-
- $main_file = trailingslashit( $upload_dir['basedir'] ) . $attachment_meta['file'];
-
- // Use main file sizes (not combined with thumbnails) to match media library display
- $original_main_size = ! empty( $extra_data ) ? $extra_data->get_original_main_size() : 0;
- $current_main_size = is_numeric( $queue_model->get_final_size() ) ? $queue_model->get_final_size() : filesize( $main_file );
-
- $formated_data = wp_parse_args(
- [
- 'original_url' => $image_url,
- 'thumbnail_url' => $thumbnail_url,
- 'file_name' => wp_basename( $attachment_meta['file'] ),
- 'original_size' => size_format( $original_main_size ? $original_main_size : $queue_model->get_original_size(), 2 ),
- 'optimized_size' => size_format( $queue_model->get_final_size(), 2 ),
- ],
- $default_formated_data
- );
-
- // An extra data may be empty after a failed migration or an unknown error.
- if ( ! empty( $extra_data ) ) {
- if ( $original_main_size && $current_main_size ) {
- $original_saving = ( $original_main_size - $current_main_size ) * 100 / $original_main_size;
- $formated_data['original_saving'] = round( $original_saving ) . '%';
- }
-
- $formated_data['thumbnails_count'] = $extra_data->get_thumbnails_count();
- }
-
- // Query for WebP conversion record (main/full size only)
- $webp_record = $this->get_conversion_record( $queue_model->get_object_id(), 'webp' );
- if ( $webp_record ) {
- $formated_data['webp_size'] = size_format( $webp_record->get_final_size(), 2 );
+ // Get URLs and file name based on item type
+ if ( in_array( $item_type, [ 'webp', 'avif' ], true ) && $extra_data instanceof RIOP_WebP_Extra_Data ) {
+ // For webp/avif, use source_src from extra_data
+ $original_url = $extra_data->get_source_src();
+ $formatted_data['original_url'] = $original_url;
+ $formatted_data['file_name'] = wp_basename( $original_url );
+ $formatted_data['thumbnail_url'] = $original_url;
+ $formatted_data['converted_from'] = $extra_data->get_converted_from_size();
+
+ // Set the appropriate size field
+ if ( 'avif' === $item_type ) {
+ $formatted_data['avif_size'] = size_format( $final_size, 2 );
+ } else {
+ $formatted_data['webp_size'] = size_format( $final_size, 2 );
}
- // Query for AVIF conversion record (main/full size only)
- $avif_record = $this->get_conversion_record( $queue_model->get_object_id(), 'avif' );
- if ( $avif_record ) {
- $formated_data['avif_size'] = size_format( $avif_record->get_final_size(), 2 );
- }
-
- if ( $queue_model->get_original_size() ) {
- $total_saving = ( $queue_model->get_original_size() - $queue_model->get_final_size() ) * 100 / $queue_model->get_original_size();
- $formated_data['total_saving'] = round( $total_saving, 2 ) . '%';
+ if ( $extra_data->get_thumbnails_count() ) {
+ $formatted_data['thumbnails_count'] = $extra_data->get_thumbnails_count();
}
} else {
- $attachment = get_post( $queue_model->get_object_id() );
+ // For attachment type, use WordPress attachment metadata
+ $upload_dir = wp_upload_dir();
+ $attachment_meta = wp_get_attachment_metadata( $object_id );
+
+ if ( ! empty( $attachment_meta ) ) {
+ $image_url = trailingslashit( $upload_dir['baseurl'] ) . $attachment_meta['file'];
+ $formatted_data['original_url'] = $image_url;
+ $formatted_data['file_name'] = wp_basename( $attachment_meta['file'] );
+ $formatted_data['thumbnail_url'] = $image_url;
+
+ if ( isset( $attachment_meta['sizes']['thumbnail'] ) ) {
+ $image_basename = wp_basename( $image_url );
+ $formatted_data['thumbnail_url'] = str_replace( $image_basename, $attachment_meta['sizes']['thumbnail']['file'], $image_url );
+ }
- if ( ! empty( $attachment ) ) {
- $formated_data = [
- 'original_url' => $attachment->guid,
- 'thumbnail_url' => $attachment->guid,
- 'file_name' => wp_basename( $attachment->guid ),
- ];
+ if ( ! empty( $extra_data ) && method_exists( $extra_data, 'get_thumbnails_count' ) ) {
+ $formatted_data['thumbnails_count'] = $extra_data->get_thumbnails_count();
+ }
+ } else {
+ // Fallback to post guid
+ $attachment = get_post( $object_id );
+ if ( ! empty( $attachment ) ) {
+ $formatted_data['original_url'] = $attachment->guid;
+ $formatted_data['thumbnail_url'] = $attachment->guid;
+ $formatted_data['file_name'] = wp_basename( $attachment->guid );
+ }
}
+ }
- $formated_data = wp_parse_args( $formated_data, $default_formated_data );
+ // Calculate total saving directly from the row's original_size and final_size
+ if ( is_numeric( $original_size ) && $original_size > 0 && is_numeric( $final_size ) ) {
+ $total_saving = ( $original_size - $final_size ) * 100 / $original_size;
+ $total_saving = max( 0, min( $total_saving, 100 ) );
+ $formatted_data['total_saving'] = round( $total_saving, 2 ) . '%';
}
- // We collect information about errors
- if ( $queue_model->get_result_status() == RIO_Process_Queue::STATUS_ERROR ) {
+ // Handle errors
+ if ( RIO_Process_Queue::STATUS_ERROR === $queue_model->get_result_status() ) {
$error_message = null;
- if ( ! empty( $extra_data ) ) {
+ if ( ! empty( $extra_data ) && method_exists( $extra_data, 'get_error_msg' ) ) {
$error_message = $extra_data->get_error_msg();
}
- $formated_data['type'] = 'error';
- $formated_data['error_msg'] = ! empty( $error_message ) ? $error_message : __( 'Unknown error', 'robin-image-optimizer' );
-
- return $formated_data;
+ $formatted_data['type'] = 'error';
+ $formatted_data['error_msg'] = ! empty( $error_message ) ? $error_message : __( 'Unknown error', 'robin-image-optimizer' );
}
- return $formated_data;
+ return $formatted_data;
}
/**
- * @param RIO_Process_Queue $queue_model
+ * Format WebP/AVIF record for log display.
*
- * @return array
- * @throws Exception
+ * @param RIO_Process_Queue $queue_model Queue model instance.
+ *
+ * @return array<string, mixed>
+ * @throws Exception If invalid model provided.
* @since 1.5.3
*/
protected function format_webp_for_log( $queue_model ) {
@@ -759,6 +665,8 @@
$default_formated_data = [
'id' => $queue_model->get_id(),
+ 'attachment_id' => $queue_model->get_object_id(),
+ 'item_type' => $queue_model->item_type,
'url' => admin_url( sprintf( 'post.php?post=%d&action=edit', $queue_model->get_object_id() ) ),
'original_url' => null,
'thumbnail_url' => null,
@@ -811,8 +719,9 @@
if ( ! empty( $extra_data ) ) {
$original_main_size = $extra_data->get_original_main_size();
- if ( $original_main_size && file_exists( $main_file ) ) {
- $original_saving = ( $original_main_size - filesize( $main_file ) ) * 100 / $original_main_size;
+ if ( $original_main_size ) {
+ $current_main_size = $this->get_file_size( $main_file );
+ $original_saving = ( $original_main_size - $current_main_size ) * 100 / $original_main_size;
$formated_data['original_saving'] = round( $original_saving ) . '%';
}
@@ -838,10 +747,10 @@
}
// We collect information about errors
- if ( $queue_model->get_result_status() == RIO_Process_Queue::STATUS_ERROR ) {
+ if ( RIO_Process_Queue::STATUS_ERROR === $queue_model->get_result_status() ) {
$error_message = null;
- if ( ! empty( $extra_data ) ) {
+ if ( ! empty( $extra_data ) && method_exists( $extra_data, 'get_error_msg' ) ) {
$error_message = $extra_data->get_error_msg();
}
--- a/robin-image-optimizer/includes/classes/class-rio-media-library.php
+++ b/robin-image-optimizer/includes/classes/class-rio-media-library.php
@@ -126,9 +126,10 @@
$optimization_data = $wio_attachment->getOptimizationData();
$allowed_mime = wrio_get_allowed_formats();
+ $allowed_mime = is_array( $allowed_mime ) ? $allowed_mime : [];
$mime_type = get_post_mime_type( $attachment_id );
- if ( ! in_array( $mime_type, $allowed_mime ) ) {
- WRIO_Plugin::app()->logger->warning( 'This format is disabled in the plugin settings: ' . $mime_type );
+ if ( ! in_array( $mime_type, $allowed_mime, true ) ) {
+ WRIO_Plugin::app()->logger->warning( 'This format is disabled in the plugin settings: ' . $mime_type . ' (' . implode( ',', $allowed_mime ) . ')' );
return [];
}
@@ -242,38 +243,7 @@
* @return array
*/
public function getUnoptimizedImages() {
- global $wpdb;
- $db_table = RIO_Process_Queue::table_name();
-
- $allowed_formats_sql = wrio_get_allowed_formats( true );
-
- $optimize_order = WRIO_Plugin::app()->getOption( 'image_optimization_order', 'asc' );
-
- $sql = "SELECT DISTINCT posts.ID
- FROM {$wpdb->posts} AS posts
- LEFT JOIN {$db_table} AS rio ON posts.ID = rio.object_id AND rio.item_type = 'attachment'
- WHERE rio.object_id IS NULL
- AND posts.post_type = 'attachment'
- AND posts.post_status = 'inherit'
- AND posts.post_mime_type IN ( {$allowed_formats_sql} )";
-
- // If you use a WPML plugin, you need to exclude duplicate images
- if ( defined( 'WPML_PLUGIN_FILE' ) ) {
- $sql .= " AND NOT EXISTS (
- SELECT trnsl.element_id
- FROM {$wpdb->prefix}icl_translations as trnsl
- WHERE trnsl.element_id=posts.ID
- AND trnsl.element_type='post_attachment'
- AND source_language_code IS NOT NULL
- )";
- }
-
- $sql .= " ORDER BY posts.ID {$optimize_order}";
-
- // выборка не оптимизированных изображений
- $unoptimized_attachments_ids = $wpdb->get_col( $sql );
-
- return is_array( $unoptimized_attachments_ids ) ? $unoptimized_attachments_ids : [];
+ return WRIO_Image_Query::get_instance()->get_unoptimized_ids();
}
/**
@@ -295,8 +265,6 @@
* @return array|WP_Error
*/
public function processUnoptimizedImages( $max_process_per_request ) {
- global $wpdb;
-
$backup_origin_images = WRIO_Plugin::app()->getPopulateOption( 'backup_origin_images', false );
$backup = WIO_Backup::get_instance();
@@ -309,36 +277,14 @@
return new WP_Error( 'unwritable_upload_dir', __( 'No access for writing backups.', 'robin-image-optimizer' ) );
}
- $db_table = RIO_Process_Queue::table_name();
$max_process_per_request = intval( $max_process_per_request );
- $allowed_formats_sql = wrio_get_allowed_formats( true );
-
- $optimize_order = WRIO_Plugin::app()->getOption( 'image_optimization_order', 'asc' );
-
- $sql = "SELECT DISTINCT posts.ID
- FROM {$wpdb->posts} AS posts
- LEFT JOIN {$db_table} AS rio ON posts.ID = rio.object_id AND rio.item_type = 'attachment'
- WHERE rio.object_id IS NULL
- AND posts.post_type = 'attachment'
- AND posts.post_status = 'inherit'
- AND posts.post_mime_type IN ( {$allowed_formats_sql} )";
-
- // If you use a WPML plugin, you need to exclude duplicate images
- if ( defined( 'WPML_PLUGIN_FILE' ) ) {
- $sql .= " AND NOT EXISTS (
- SELECT trnsl.element_id
- FROM {$wpdb->prefix}icl_translations as trnsl
- WHERE trnsl.element_id=posts.ID
- AND trnsl.element_type='post_attachment'
- AND source_language_code IS NOT NULL
- )";
- }
-
- $sql .= " ORDER BY posts.ID {$optimize_order}
- LIMIT {$max_process_per_request}";
- // выборка неоптимизированных изображений
- $unoptimized_attachments_ids = $wpdb->get_col( $sql );
+ // Get unoptimized attachment IDs using centralized query helper
+ $unoptimized_attachments_ids = WRIO_Image_Query::get_instance()->get_unoptimized_ids(
+ $max_process_per_request,
+ 0,
+ true
+ );
// временно
$optimized_count = (int) RIO_Process_Queue::count_by_type_status( 'attachment', 'success' );
@@ -395,7 +341,7 @@
'remain' => $remain,
'end' => false,
'statistic' => $image_statistics->load(),
- 'last_optimized' => $image_statistics->get_last_optimized_image( $last_optimized_id ),
+ 'last_optimized' => $last_optimized_id ? $image_statistics->get_last_optimized_image( $last_optimized_id ) : [],
'optimized_count' => $optimized_count,
];
@@ -622,12 +568,7 @@
* @return int
*/
public function getOptimizedCount() {
- $optimized_count = RIO_Process_Queue::count_by_type_status( 'attachment', 'success' );
- if ( ! $optimized_count ) {
- $optimized_count = 0;
- }
-
- return $optimized_count;
+ return WRIO_Image_Query::get_instance()->count_optimized();
}
/**
@@ -726,6 +667,16 @@
$is_skipped = $optimization_data->is_skipped();
$attach_meta = wp_get_attachment_metadata( $attachment_id );
$attach_dimensions = '0 x 0';
+ $error_msg = '';
+ // Check if attachment format is supported
+ $allowed_mime = wrio_get_allowed_formats();
+ $allowed_mime = is_array( $allowed_mime ) ? $allowed_mime : [];
+ $mime_type = get_post_mime_type( $attachment_id );
+ $is_supported_format = in_array( $mime_type, $allowed_mime, true );
+
+ $original_url = wp_get_attachment_url( $attachment_id );
+ $edit_url = get_edit_post_link( $attachment_id );
+ $original_name = basename( $original_url );
if ( isset( $attach_meta['width'] ) && isset( $attach_meta['height'] ) ) {
$attach_dimensions = $attach_meta['width'] . ' × ' . $attach_meta['height'];
@@ -739,26 +690,71 @@
$attachment_file_size = filesize( $attachment_file );
}
+ // Check for errors in extra data.
+ $extra_data = $optimization_data->get_extra_data();
+ if ( null !== $extra_data && method_exists( $extra_data, 'get_error_msg' ) ) {
+ $error = $extra_data->get_error_msg();
+
+ if ( ! empty( $error ) ) {
+ $error_msg = $error;
+ }
+ }
+
+ $extra_data = $webp_data->get_extra_data();
+ if ( null !== $extra_data && method_exists( $extra_data, 'get_error_msg' ) ) {
+ $error = $extra_data->get_error_msg();
+
+ if ( ! empty( $error ) ) {
+ $error_msg .= ' WebP error: ' . $e