first commit

This commit is contained in:
User A0264400
2026-04-01 23:20:16 +03:00
commit a766acdc90
23071 changed files with 4933189 additions and 0 deletions

View File

@@ -0,0 +1,564 @@
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Класс для работы с резервным копированием изображений.
*
* @version 1.0
*/
class WIO_Backup {
const BACKUP_DIR_NAME = 'wio_backup';
const TEMP_DIR_NAME = 'temp';
/**
* The single instance of the class.
*
* @since 1.3.0
* @access protected
* @var object
*/
protected static $_instance;
/**
* @var array Данные о папке uploads, возвращаемые функцией wp_upload_dir()
*/
protected $wp_upload_dir;
/**
* @var string Путь к папке с резервными копиями изображений
*/
private $backup_dir;
/**
* @since 1.3.0
* @var string
*/
private $blog_backup_dir;
/**
* Инициализация бекапа
*/
public function __construct() {
$this->wp_upload_dir = wp_upload_dir();
}
/**
* @since 1.3.0
*
* @return object|\static object Main instance.
*/
public static function get_instance() {
if ( ! isset( static::$_instance ) ) {
static::$_instance = new static();
}
return static::$_instance;
}
/**
* Проверка возможности записи в папку uploads.
*
* @return bool
*/
public function isUploadWritable() {
$upload_dir = $this->wp_upload_dir['basedir'];
if ( is_dir( $upload_dir ) && wp_is_writable( $upload_dir ) ) {
return true;
}
return false;
}
/**
* Проверка возможности записи в папку бекап.
*
* @return bool
*/
public function isBackupWritable() {
$backup_dir = $this->getBackupDir();
if ( is_wp_error( $backup_dir ) || ! wp_is_writable( $backup_dir ) ) {
return false;
}
return true;
}
/**
* Путь к папке с бекапами
*
* @return string|WP_Error
*/
public function getBackupDir() {
if ( $this->backup_dir ) {
return $this->backup_dir;
}
$backup_dir = wp_normalize_path( trailingslashit( $this->wp_upload_dir['basedir'] ) . self::BACKUP_DIR_NAME );
if ( ! is_dir( $backup_dir ) ) {
$backup_dir = $this->mkdir( $backup_dir );
if ( is_wp_error( $backup_dir ) ) {
return $backup_dir;
}
}
$this->backup_dir = apply_filters( 'wbcr/rio/backup/backup_dir', trailingslashit( $backup_dir ) );
return $this->backup_dir;
}
/**
* Путь к папке с бекапами блога.
*
* Используется в мультисайт режиме.
*
* @return string|WP_Error
*/
public function getBlogBackupDir() {
if ( $this->blog_backup_dir ) {
return $this->blog_backup_dir;
}
$wp_upload_dir = wp_upload_dir();
$backup_dir = wp_normalize_path( trailingslashit( $wp_upload_dir['basedir'] ) . self::BACKUP_DIR_NAME );
if ( ! is_dir( $backup_dir ) ) {
$backup_dir = $this->mkdir( $backup_dir );
if ( is_wp_error( $backup_dir ) ) {
return $backup_dir;
}
}
$this->blog_backup_dir = trailingslashit( $backup_dir );
return $this->blog_backup_dir;
}
/**
* Очищает папку с резервными копиями
*
* @return bool
*/
public function removeBackupDir() {
$backup_dir = $this->getBackupDir();
return wrio_rmdir( $backup_dir );
}
/**
* Очищает папку с резервными копиями блога
* Используется в мультисайт режиме
*
* @return bool
*/
public function removeBlogBackupDir() {
$backup_dir = $this->getBlogBackupDir();
return wrio_rmdir( $backup_dir );
}
/**
* Получает путь к папке с резервными копиями
*
* @param array $attachment_meta метаданные аттачмента
*
* @return string
*/
public function getAttachmentBackupDir( $attachment_meta ) {
$backup_dir = $this->getBackupDir();
// Get all subfolders in which the image is stored.
// This is necessary to create an alternate subfolders
// in directory where they are stored in backups.
$subfolders = dirname( $attachment_meta['file'] );
$backup_dir .= $subfolders;
if ( ! is_dir( $backup_dir ) ) {
$backup_dir = $this->mkdir( $backup_dir );
if ( is_wp_error( $backup_dir ) ) {
return $backup_dir;
}
}
return trailingslashit( $backup_dir );
}
/**
* Делаем резервную копию аттачмента
*
* @param WIO_Attachment $wio_attachment аттачмент
*
* @return bool|WP_Error
*/
public function backupAttachment( WIO_Attachment $wio_attachment ) {
$backup_origin_images = WRIO_Plugin::app()->getPopulateOption( 'backup_origin_images', false );
if ( ! $backup_origin_images ) {
return false; // если бекап не требуется
}
$backup_dir = $this->getAttachmentBackupDir( $wio_attachment->get( 'attachment_meta' ) );
if ( is_wp_error( $backup_dir ) ) {
return $backup_dir;
}
$full = $this->backupAttachmentSize( $wio_attachment );
if ( is_wp_error( $full ) ) {
return $full;
}
$allowed_sizes = $wio_attachment->getAllowedSizes();
if ( ! empty( $allowed_sizes ) ) {
foreach ( (array) $allowed_sizes as $image_size ) {
$size_backup = $this->backupAttachmentSize( $wio_attachment, $image_size );
if ( is_wp_error( $size_backup ) ) {
return $size_backup;
}
}
}
return true;
}
/**
* Восстанавливаем аттачмент из резервной копии
*
* @param WIO_Attachment $wio_attachment аттачмент
*
* @return bool|WP_Error
*/
public function restoreAttachment( WIO_Attachment $wio_attachment ) {
$backup_dir = $this->getAttachmentBackupDir( $wio_attachment->get( 'attachment_meta' ) );
if ( is_wp_error( $backup_dir ) ) {
return $backup_dir;
}
$restore_result = $this->restoreAttachmentSize( $wio_attachment );
if ( is_wp_error( $restore_result ) ) {
return $restore_result;
}
$attachment_meta = wp_get_attachment_metadata( $wio_attachment->get( 'id' ) );
if ( isset( $attachment_meta['old_width'] ) && isset( $attachment_meta['old_width'] ) ) {
$attachment_meta['width'] = $attachment_meta['old_width'];
$attachment_meta['height'] = $attachment_meta['old_height'];
wp_update_attachment_metadata( $wio_attachment->get( 'id' ), $attachment_meta );
}
$allowed_sizes = $wio_attachment->getAllowedSizes();
if ( $allowed_sizes ) {
foreach ( $allowed_sizes as $image_size ) {
$this->restoreAttachmentSize( $wio_attachment, $image_size );
}
}
return true;
}
/**
* Создает временное изображение с уникальным именем.
*
* Необходимо для провайдеров, который кешируют изображения по имени файла,
* чтобы сбросить кеш, нужно отдать провайдеру изображение с другим именем.
*
* @since 1.1.2
*
* @param string $file_path путь к изображению
*
* @return array|WP_Error
*/
public function createTempAttachment( $file_path ) {
if ( $this->isBackupWritable() ) {
$temp_dir = $this->getBackupDir() . self::TEMP_DIR_NAME . '/';
$temp_dir_url = trailingslashit( $this->wp_upload_dir['baseurl'] ) . self::BACKUP_DIR_NAME . '/' . self::TEMP_DIR_NAME . '/';
if ( ! is_dir( $temp_dir ) ) {
$temp_dir = $this->mkdir( $temp_dir );
if ( is_wp_error( $temp_dir ) ) {
return $temp_dir;
}
}
$temp_file_id = uniqid();
$file_name = pathinfo( $file_path, PATHINFO_FILENAME );
$file_extension = pathinfo( $file_path, PATHINFO_EXTENSION );
$new_file_name = $temp_file_id . '_' . md5( $file_name ) . '.' . $file_extension;
$temp_file_path = $temp_dir . $new_file_name;
$temp_file_url = $temp_dir_url . $new_file_name;
if ( is_file( $file_path ) ) {
if ( ! @copy( $file_path, $temp_file_path ) ) {
WRIO_Plugin::app()->logger->error( sprintf( 'Failed to swap original file %s with %s as copy() failed.', $temp_file_path, $file_path ) );
return new WP_Error( 'copy_file_to_temp_dir_error', __( 'Could not copy the file to the temporary directory', 'robin-image-optimizer' ) );
}
}
WRIO_Plugin::app()->logger->info( sprintf( 'Creation of temporary attachment (%s) successfully completed!', $file_path ) );
return [
'id' => $temp_file_id,
'image_path' => $temp_file_path,
'image_url' => $temp_file_url,
];
}
return new WP_Error( 'backup_writable_error', __( 'It is not possible to create a temporary file, the backup folder is not writable.', 'robin-image-optimizer' ) );
}
/**
* Резервное копирование файла аттачмента.
*
* @param WIO_Attachment $wio_attachment аттачмент
* @param string $image_size Размер(thumbnail, medium ... )
*
* @return bool|WP_Error
*/
protected function backupAttachmentSize( WIO_Attachment $wio_attachment, $image_size = '' ) {
if ( $image_size ) {
$original_file = $wio_attachment->getImageSizePath( $image_size );
} else {
$original_file = $wio_attachment->get( 'path' );
}
$backup_dir = $this->getAttachmentBackupDir( $wio_attachment->get( 'attachment_meta' ) );
// проверить запись в папку
if ( is_wp_error( $backup_dir ) ) {
WRIO_Plugin::app()->logger->error( sprintf( 'Failed to create backup dir, error: %s', $backup_dir->get_error_message() ) );
return $backup_dir;
}
if ( ! $original_file ) {
// бывает такое, что размера превьюшки нет в базе данных.
// это не считается ошибкой, поэтому сразу пропускаем
return false;
}
$backup_file = $backup_dir . wp_basename( $original_file );
if ( is_file( $original_file ) ) {
if ( ! @copy( $original_file, $backup_file ) ) {
WRIO_Plugin::app()->logger->error( sprintf( 'Failed to copy %s to %s as copy() failed', $original_file, $backup_file ) );
}
}
return true;
}
/**
* Восстановление файла аттачмента из резервной копии
*
* @param WIO_Attachment $wio_attachment аттачмент
* @param string|null $image_size Размер(thumbnail, medium ... )
*
* @return bool|WP_Error
*/
protected function restoreAttachmentSize( WIO_Attachment $wio_attachment, $image_size = null ) {
if ( ! empty( $image_size ) ) {
$original_file = $wio_attachment->getImageSizePath( $image_size );
} else {
$original_file = $wio_attachment->get( 'path' );
}
$backup_dir = $this->getAttachmentBackupDir( $wio_attachment->get( 'attachment_meta' ) );
if ( is_wp_error( $backup_dir ) ) {
return $backup_dir;
}
if ( empty( $original_file ) ) {
return false;
}
$backup_file = $backup_dir . wp_basename( $original_file );
if ( ! is_file( $backup_file ) ) {
WRIO_Plugin::app()->logger->error( sprintf( 'Unable to restore from a backup. There is no file, attachment id: %s, backup file: %s', $wio_attachment->get( 'id' ), $backup_file ) );
return false;
}
if ( ! @copy( $backup_file, $original_file ) ) {
WRIO_Plugin::app()->logger->error( sprintf( 'Failed to swap %s with %s as copy() failed', $backup_file, $original_file ) );
return false;
}
if ( ! @unlink( $backup_file ) ) {
WRIO_Plugin::app()->logger->error( sprintf( 'Failed to delete backup file %s as unlink() failed', $backup_file ) );
return false;
}
WRIO_Plugin::app()->logger->info( sprintf( 'Restored file: %s.', $backup_file ) );
return true;
}
/**
* 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
*
* @return bool|WP_Error
*/
public function removeAttachmentBackup( $attachment_id ) {
$attachment_meta = wp_get_attachment_metadata( $attachment_id );
$backup_dir = $this->getAttachmentBackupDir( $attachment_meta );
if ( is_wp_error( $backup_dir ) ) {
return $backup_dir;
}
$main_file_path = $backup_dir . wp_basename( $attachment_meta['file'] );
if ( ! file_exists( $main_file_path ) ) {
WRIO_Plugin::app()->logger->error( sprintf( "Failed to remove an attachment file. File (%s) isn't exists. Attachment #%s", $main_file_path, $attachment_id ) );
}
if ( ! @unlink( $main_file_path ) ) {
WRIO_Plugin::app()->logger->error( sprintf( 'Failed to unlink a main file (%s) for attachment #%s', $main_file_path, $attachment_id ) );
}
if ( isset( $attachment_meta['sizes'] ) && is_array( $attachment_meta['sizes'] ) ) {
foreach ( $attachment_meta['sizes'] as $size ) {
$thumbnail_file_path = $backup_dir . $size['file'];
if ( ! file_exists( $thumbnail_file_path ) ) {
WRIO_Plugin::app()->logger->error( sprintf( "Failed to remove a thumbnail file. File (%s) isn't exists. Attachment #%s", $thumbnail_file_path, $attachment_id ) );
}
if ( ! @unlink( $thumbnail_file_path ) ) {
WRIO_Plugin::app()->logger->error( sprintf( 'Failed to unlink thumbnail (%s) for attachment #%s', $thumbnail_file_path, $attachment_id ) );
}
}
}
return true;
}
/**
* alternateStorage
*
* @param array $servers
*
* @return array
*/
public static function alternateStorage( $servers ) {
return $servers;
}
/**
* @since 1.3.0
*
* @param string $dir
*
* @return string|WP_Error
*/
protected function mkdir( $dir ) {
WRIO_Plugin::app()->logger->info( sprintf( 'Try to create backup directory. Backup dir: (%s)', $dir ) );
if ( ! wp_mkdir_p( $dir ) ) {
WRIO_Plugin::app()->logger->error( sprintf( 'Unable to create backup directory (%s) as mkdir() failed', $dir ) );
return new WP_Error( 'mkdir_failed', sprintf( 'Unable to create backup folder (%s) as mkdir() failed.', $dir ) );
}
return $dir;
}
/**
* @since 1.3.0
*
* @param string $backup_file
* @param string $original_file
*
* @return bool
*/
protected function restore_file( $backup_file, $original_file ) {
if ( is_file( $backup_file ) ) {
if ( ! @copy( $backup_file, $original_file ) ) {
WRIO_Plugin::app()->logger->error( sprintf( 'Failed to swap original file (%s) with %s as copy() failed', $backup_file, $original_file ) );
return false;
}
if ( ! @unlink( $backup_file ) ) {
WRIO_Plugin::app()->logger->error( sprintf( 'Failed to delete backup file (%s) as unlink() failed', $backup_file ) );
return false;
}
WRIO_Plugin::app()->logger->info( sprintf( 'Restored file: %s.', $backup_file ) );
return true;
}
WRIO_Plugin::app()->logger->error( sprintf( 'Unable to restore from a backup. There is no file (%s).', $backup_file ) );
return false;
}
}

View File

@@ -0,0 +1,994 @@
<?php
use WBCR\Factory_Processing_759\WP_Background_Process;
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class WRIO_Bulk_Optimization
*
* Handles bulk optimization operations and processes related to media attachments,
* thumbnails, and other optimization tasks within the WordPress environment.
*/
class WRIO_Bulk_Optimization {
public $processing;
public function __construct() {
$image_optimization_type = WRIO_Plugin::app()->getOption( 'image_optimization_type', '' );
if ( wrio_is_license_activate() && $image_optimization_type === 'background' ) {
$scope = WRIO_Plugin::app()->request->request( 'scope', null, true );
$this->processing = $scope ? wrio_get_processing_class( $scope ) : $scope;
add_action( 'wp_ajax_wrio-cron-start', [ $this, 'processing_start' ] );
add_action( 'wp_ajax_wrio-cron-stop', [ $this, 'processing_stop' ] );
add_action( 'wp_ajax_wrio-webp-cron-start', [ $this, 'webp_processing_start' ] );
add_action( 'wp_ajax_wrio-webp-cron-stop', [ $this, 'webp_processing_stop' ] );
add_action( 'wp_ajax_wrio-avif-cron-start', [ $this, 'avif_processing_start' ] );
add_action( 'wp_ajax_wrio-avif-cron-stop', [ $this, 'avif_processing_stop' ] );
} else {
add_action( 'wp_ajax_wrio-cron-start', [ $this, 'cron_start' ] );
add_action( 'wp_ajax_wrio-cron-stop', [ $this, 'cron_stop' ] );
add_action( 'wp_ajax_wrio-webp-cron-start', [ $this, 'webp_cron_start' ] );
add_action( 'wp_ajax_wrio-webp-cron-stop', [ $this, 'webp_cron_stop' ] );
add_action( 'wp_ajax_wrio-avif-cron-start', [ $this, 'avif_cron_start' ] );
add_action( 'wp_ajax_wrio-avif-cron-stop', [ $this, 'avif_cron_stop' ] );
}
add_action( 'wp_ajax_wrio-bulk-optimization-process', [ $this, 'bulk_optimization_process' ] );
add_action( 'wp_ajax_wrio-bulk-conversion-process', [ $this, 'bulk_conversion_process' ] );
add_action( 'wp_ajax_wio_reoptimize_image', [ $this, 'reoptimize_image' ] );
add_action( 'wp_ajax_wio_convert_image', [ $this, 'convert_image' ] );
add_action( 'wp_ajax_wio_restore_image', [ $this, 'restore_image' ] );
add_action( 'wp_ajax_wbcr-rio-check-servers-status', [ $this, 'check_servers_status' ] );
add_action( 'wp_ajax_wbcr-rio-check-user-balance', [ $this, 'check_user_balance' ] );
// add_action( 'wp_ajax_wbcr-rio-calculate-total-images', [ $this, 'calculate_total_images' ] );
add_action( 'wp_ajax_wbcr-rio-calculate-total-images', [ $this, 'calculate_total_images' ] );
add_action( 'wp_ajax_wbcr-rio-calculate-total-attachments', [ $this, 'calculate_total_attachments' ] );
add_action( 'wp_ajax_wbcr-rio-calculate-total-thumbs', [ $this, 'calculate_total_thumbs' ] );
}
/**
* Calculates the total number of attachments that meet specified criteria.
* Retrieves the total count of attachment posts with allowed formats, excluding duplicates
* if WPML is active, and sends the result as a JSON response.
* Includes nonce verification and checks user permissions.
*
* @return void Outputs JSON response with the total count of attachments.
*/
public function calculate_total_attachments() {
check_ajax_referer( 'bulk_optimization' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
WRIO_Plugin::app()->deletePopulateOption( 'wrio_partial_total_count' );
$total_attachments = WRIO_Image_Query::get_instance()->count_total_attachments();
wp_send_json_success(
[
'found_attachments' => (int) $total_attachments,
]
);
}
/**
* Calculates the total number of attachment thumbnails that match specified criteria.
* Processes a batch of attachments, counting the allowed thumbnail sizes, and updates
* the total count in the database. Handles AJAX requests, including user permissions
* and nonce verification.
*
* @return void Outputs JSON response with the total count of thumbnails and processing status.
*/
public function calculate_total_thumbs() {
check_ajax_referer( 'bulk_optimization' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
global $wpdb;
$offset = (int) WRIO_Plugin::app()->request->post( 'offset', 0 );
$limit = (int) WRIO_Plugin::app()->request->post( 'limit', 200 );
$allowed_formats_sql = wrio_get_allowed_formats( true );
$allowed_sizes = explode( ',', WRIO_Plugin::app()->getPopulateOption( 'allowed_sizes_thumbnail', '' ) );
$query = $wpdb->prepare(
"
SELECT posts.ID
FROM {$wpdb->posts} as posts
WHERE post_type = 'attachment'
AND post_status = 'inherit'
AND post_mime_type IN ({$allowed_formats_sql})
LIMIT %d OFFSET %d
",
$limit,
$offset
);
// Учитываем WPML (исключение дубликатов)
if ( defined( 'WPML_PLUGIN_FILE' ) ) {
$query = str_replace(
'WHERE post_type =',
"WHERE NOT EXISTS (
SELECT icl.element_id
FROM {$wpdb->prefix}icl_translations as icl
WHERE icl.element_id = posts.ID
AND icl.element_type = 'post_attachment'
AND source_language_code IS NOT NULL
) AND post_type =",
$query
);
}
$attachments = $wpdb->get_results( $query );
$total_count = (int) WRIO_Plugin::app()->getPopulateOption( 'wrio_partial_total_count', 0 );
$current_batch_count = 0;
foreach ( $attachments as $attachment ) {
$meta = wp_get_attachment_metadata( $attachment->ID );
if ( $meta && isset( $meta['sizes'] ) ) {
foreach ( $meta['sizes'] as $size_key => $size_value ) {
if ( in_array( $size_key, $allowed_sizes ) ) {
++$current_batch_count;
}
}
}
}
$total_count += $current_batch_count;
WRIO_Plugin::app()->updatePopulateOption( 'wrio_partial_total_count', $total_count );
// Если больше данных для обработки нет — завершаем и сохраняем в кеш
if ( count( $attachments ) < $limit ) {
WRIO_Plugin::app()->deletePopulateOption( 'wrio_partial_total_count' );
wp_send_json_success(
[
'found_thumbs' => $total_count,
'done' => true,
]
);
}
wp_send_json_success(
[
'found_thumbs' => $total_count,
'done' => false,
'next_offset' => $offset + $limit,
]
);
}
public function cron_start() {
check_ajax_referer( 'bulk_optimization' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
$scope = WRIO_Plugin::app()->request->request( 'scope', null, true );
if ( empty( $scope ) ) {
wp_die( - 1 );
}
// where was runned cron
$cron_running_place = WRIO_Plugin::app()->getPopulateOption( 'cron_running', false );
if ( $scope == $cron_running_place ) {
wp_send_json_success();
}
WRIO_Plugin::app()->updatePopulateOption( 'cron_running', $scope );
WRIO_Cron::start();
wp_send_json_success();
}
public function cron_stop() {
check_ajax_referer( 'bulk_optimization' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
WRIO_Plugin::app()->updatePopulateOption( 'cron_running', false );
WRIO_Cron::stop();
wp_send_json_success();
}
public function webp_cron_start() {
check_ajax_referer( 'bulk_conversion' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
$scope = WRIO_Plugin::app()->request->request( 'scope', null, true );
if ( empty( $scope ) ) {
wp_die( - 1 );
}
$type = 'conversion';
// where was runned cron
$cron_running_place = WRIO_Plugin::app()->getPopulateOption( "{$type}_cron_running", false );
if ( $scope == $cron_running_place ) {
wp_send_json_success();
}
WRIO_Plugin::app()->updatePopulateOption( "{$type}_cron_running", $scope );
WRIO_Cron::start( $type );
wp_send_json_success();
}
public function webp_cron_stop() {
check_ajax_referer( 'bulk_conversion' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
$type = 'conversion';
WRIO_Plugin::app()->updatePopulateOption( "{$type}_cron_running", false );
WRIO_Cron::stop( $type );
wp_send_json_success();
}
public function processing_start() {
check_ajax_referer( 'bulk_optimization' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
$scope = WRIO_Plugin::app()->request->request( 'scope', null, true );
if ( empty( $scope ) ) {
wp_die( - 1 );
}
// where was runned
$process_running_place = WRIO_Plugin::app()->getPopulateOption( 'process_running', false );
if ( $scope == $process_running_place ) {
wp_send_json_success();
}
WRIO_Plugin::app()->updatePopulateOption( 'process_running', $scope );
$processing = wrio_get_processing_class( $scope );
if ( ! $processing ) {
WRIO_Plugin::app()->updatePopulateOption( 'process_running', false );
wp_send_json_error( [ 'message' => 'Processing class not found for scope: ' . $scope ] );
}
if ( $processing->push_items() ) {
$processing->save()->dispatch();
} else {
// WRIO_Plugin::app()->updatePopulateOption( 'process_running', false );
wp_send_json_success(
[
'stop' => true,
]
);
}
wp_send_json_success();
}
public function processing_stop() {
check_ajax_referer( 'bulk_optimization' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
$scope = WRIO_Plugin::app()->request->request( 'scope', null, true );
if ( empty( $scope ) ) {
wp_die( - 1 );
}
WRIO_Plugin::app()->updatePopulateOption( 'process_running', false );
$processing = wrio_get_processing_class( $scope );
if ( $processing ) {
$processing->cancel_process();
}
wp_send_json_success();
}
public function webp_processing_start() {
check_ajax_referer( 'bulk_conversion' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
$scope = WRIO_Plugin::app()->request->request( 'scope', null, true );
if ( empty( $scope ) ) {
wp_die( - 1 );
}
$scope = $scope . '_webp';
// where was runned
$process_running_place = WRIO_Plugin::app()->getPopulateOption( "{$scope}_process_running", false );
if ( $scope == $process_running_place ) {
wp_send_json_success();
}
WRIO_Plugin::app()->updatePopulateOption( "{$scope}_process_running", $scope );
$processing = wrio_get_processing_class( $scope );
if ( ! $processing ) {
WRIO_Plugin::app()->updatePopulateOption( "{$scope}_process_running", false );
wp_send_json_error( [ 'message' => 'Processing class not found for scope: ' . $scope ] );
}
if ( $processing->push_items() ) {
$processing->save()->dispatch();
} else {
// WRIO_Plugin::app()->updatePopulateOption( 'process_running', false );
wp_send_json_success(
[
'stop' => true,
]
);
}
wp_send_json_success();
}
public function webp_processing_stop() {
check_ajax_referer( 'bulk_conversion' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
$scope = WRIO_Plugin::app()->request->request( 'scope', null, true );
if ( empty( $scope ) ) {
wp_die( - 1 );
}
$scope = $scope . '_webp';
WRIO_Plugin::app()->updatePopulateOption( "{$scope}_process_running", false );
$processing = wrio_get_processing_class( $scope );
if ( $processing ) {
$processing->cancel_process();
}
wp_send_json_success();
}
/**
* Start AVIF conversion cron job.
*
* @return void
*/
public function avif_cron_start() {
check_ajax_referer( 'bulk_conversion' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
$scope = WRIO_Plugin::app()->request->request( 'scope', null, true );
if ( empty( $scope ) ) {
wp_die( - 1 );
}
$type = 'avif_conversion';
// where was runned cron
$cron_running_place = WRIO_Plugin::app()->getPopulateOption( "{$type}_cron_running", false );
if ( $scope == $cron_running_place ) {
wp_send_json_success();
}
WRIO_Plugin::app()->updatePopulateOption( "{$type}_cron_running", $scope );
WRIO_Cron::start( $type );
wp_send_json_success();
}
/**
* Stop AVIF conversion cron job.
*
* @return void
*/
public function avif_cron_stop() {
check_ajax_referer( 'bulk_conversion' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
$type = 'avif_conversion';
WRIO_Plugin::app()->updatePopulateOption( "{$type}_cron_running", false );
WRIO_Cron::stop( $type );
wp_send_json_success();
}
/**
* Start AVIF conversion background processing.
*
* @return void
*/
public function avif_processing_start() {
check_ajax_referer( 'bulk_conversion' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
$scope = WRIO_Plugin::app()->request->request( 'scope', null, true );
if ( empty( $scope ) ) {
wp_die( - 1 );
}
$scope = $scope . '_avif';
// where was runned
$process_running_place = WRIO_Plugin::app()->getPopulateOption( "{$scope}_process_running", false );
if ( $scope == $process_running_place ) {
wp_send_json_success();
}
WRIO_Plugin::app()->updatePopulateOption( "{$scope}_process_running", $scope );
$processing = wrio_get_processing_class( $scope );
if ( ! $processing ) {
WRIO_Plugin::app()->updatePopulateOption( "{$scope}_process_running", false );
wp_send_json_error( [ 'message' => 'Processing class not found for scope: ' . $scope ] );
}
if ( $processing->push_items() ) {
$processing->save()->dispatch();
} else {
wp_send_json_success(
[
'stop' => true,
]
);
}
wp_send_json_success();
}
/**
* Stop AVIF conversion background processing.
*
* @return void
*/
public function avif_processing_stop() {
check_ajax_referer( 'bulk_conversion' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
$scope = WRIO_Plugin::app()->request->request( 'scope', null, true );
if ( empty( $scope ) ) {
wp_die( - 1 );
}
$scope = $scope . '_avif';
WRIO_Plugin::app()->updatePopulateOption( "{$scope}_process_running", false );
$processing = wrio_get_processing_class( $scope );
if ( $processing ) {
$processing->cancel_process();
}
wp_send_json_success();
}
public function bulk_optimization_process() {
check_admin_referer( 'bulk_optimization' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
$reset_current_error = (bool) WRIO_Plugin::app()->request->request( 'reset_current_errors' );
$scope = WRIO_Plugin::app()->request->request( 'scope', null, true );
WRIO_Plugin::app()->logger->info( sprintf( 'Start bulk optimization process! Scope: %s', $scope ) );
if ( empty( $scope ) ) {
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 );
if ( ! class_exists( $class_name ) ) {
WRIO_Plugin::app()->logger->error( sprintf( 'Bulk optimization error: Context class (%s) not found.', $class_name ) );
// todo: Temporary bug fix.
if ( 'custom-folders' === $scope ) {
$class_name = 'WRIO_Custom_Folders';
} elseif ( 'nextgen-gallery' == $scope ) {
$class_name = 'WRIO_Nextgen_Gallery';
}
if ( ! class_exists( $class_name ) ) {
wp_send_json_error( [ 'error_message' => 'Context class not found.' ] );
}
}
/**
* Create an instance of the class depending on the context in which scope user
* has runned optimization.
*
* @see WRIO_Custom_Folders
* @see WRIO_Nextgen_Gallery
* @var WRIO_Media_Library $optimizer
*/
$optimizer = new $class_name();
if ( $reset_current_error ) {
$optimizer->resetCurrentErrors();
}
$result = $optimizer->processUnoptimizedImages( 1 );
if ( is_wp_error( $result ) ) {
$error_massage = $result->get_error_message();
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.', $result->get_error_message() ) );
wp_send_json_error( [ 'error_message' => $error_massage ] );
}
// If all images are processed, send completion command
if ( $result['remain'] <= 0 ) {
$result['end'] = true;
}
WRIO_Plugin::app()->logger->info( sprintf( 'End bulk optimization process! Scope: %s. Remain: %d', $scope, $result['remain'] ) );
wp_send_json_success( $result );
}
public function bulk_conversion_process() {
check_admin_referer( 'bulk_conversion' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
$reset_current_error = (bool) WRIO_Plugin::app()->request->request( 'reset_current_errors' );
$scope = WRIO_Plugin::app()->request->request( 'scope', null, true );
$format = WRIO_Plugin::app()->request->request( 'format', 'webp', true );
// Validate format
if ( ! in_array( $format, [ 'webp', 'avif' ], true ) ) {
$format = 'webp';
}
WRIO_Plugin::app()->logger->info( sprintf( 'Start bulk conversion process! Scope: %s, Format: %s', $scope, $format ) );
if ( empty( $scope ) ) {
wp_die( - 1 );
}
// Context class name. If plugin expands with add-ons
$class_name = 'WRIO_' . wrio_dashes_to_camel_case( $scope, true );
if ( ! class_exists( $class_name ) ) {
WRIO_Plugin::app()->logger->error( sprintf( 'Bulk conversion 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 ) {
$class_name = 'WRIO_Custom_Folders';
} elseif ( 'nextgen-gallery' == $scope ) {
$class_name = 'WRIO_Nextgen_Gallery';
}
if ( ! class_exists( $class_name ) ) {
wp_send_json_error( [ 'error_message' => 'Context class not found.' ] );
}
}
/**
* 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
*/
$optimizer = new $class_name();
if ( $reset_current_error ) {
$optimizer->resetCurrentErrors(); // сбрасываем текущие ошибки оптимизации
}
$result = $optimizer->webpUnoptimizedImages( 1, $format );
if ( is_wp_error( $result ) ) {
$error_massage = $result->get_error_message();
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 conversion error: %s.', $result->get_error_message() ) );
wp_send_json_error( [ 'error_message' => $error_massage ] );
}
// если изображения закончились - посылаем команду завершения
if ( $result['remain'] <= 0 ) {
$result['end'] = true;
}
WRIO_Plugin::app()->logger->info( sprintf( 'End bulk conversion process! Scope: %s, Format: %s. Remain: %d', $scope, $format, $result['remain'] ) );
wp_send_json_success( $result );
}
public function reoptimize_image() {
check_admin_referer( 'reoptimize' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
$default_level = WRIO_Plugin::app()->getPopulateOption( 'image_optimization_level', 'normal' );
$attachment_id = (int) WRIO_Plugin::app()->request->post( 'id' );
$level = WRIO_Plugin::app()->request->post( 'level', $default_level, true );
$backup = WIO_Backup::get_instance();
$media_library = WRIO_Media_Library::get_instance();
$backup_origin_images = WRIO_Plugin::app()->getPopulateOption( 'backup_origin_images', false );
if ( $backup_origin_images && ! $backup->isBackupWritable() ) {
echo $media_library->getMediaColumnContent( $attachment_id );
die();
}
$optimized_data = $media_library->optimizeAttachment( $attachment_id, $level );
if ( $optimized_data && isset( $optimized_data['processing'] ) ) {
echo 'processing';
die();
}
echo $media_library->getMediaColumnContent( $attachment_id );
die();
}
public function convert_image() {
check_admin_referer( 'convert' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
$attachment_id = (int) WRIO_Plugin::app()->request->post( 'id' );
$format = WRIO_Plugin::app()->request->post( 'format', 'webp', true );
$media_library = WRIO_Media_Library::get_instance();
$media_library->webpConvertAttachment( $attachment_id, $format );
echo $media_library->getMediaColumnContent( $attachment_id );
die();
}
public function restore_image() {
check_admin_referer( 'restore' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
$attachment_id = (int) WRIO_Plugin::app()->request->post( 'id' );
$media_library = WRIO_Media_Library::get_instance();
$wio_attachment = $media_library->getAttachment( $attachment_id );
if ( $wio_attachment->isOptimized() ) {
$media_library->restoreAttachment( $attachment_id );
}
echo $media_library->getMediaColumnContent( $attachment_id );
die();
}
public function check_servers_status() {
check_ajax_referer( 'bulk_optimization' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
// Auto-detect server based on license status
$is_premium = wrio_is_license_activate();
$license_source = wrio_get_license_source();
$server_name = $is_premium ? 'server_5' : 'server_2';
$return_data = [ 'server_name' => $server_name ];
$headers = [
'User-Agent' => '',
];
// For SDK (ThemeIsle) licenses, the license was already validated during activation
// via ThemeIsle's API, so we can skip the Robin API license check.
if ( $is_premium && 'sdk' === $license_source ) {
wp_send_json_success( $return_data );
}
if ( $is_premium ) {
$api_url = 'https://dashboard.robinoptimizer.com/v1/license/check';
$headers['Authorization'] = 'Bearer ' . base64_encode( wrio_get_license_key() );
$headers['PluginId'] = wrio_get_freemius_plugin_id();
$headers['X-License-Source'] = $license_source;
$headers['X-Site-Url'] = home_url();
} else {
$api_url = 'https://dashboard.robinoptimizer.com/v1/free/license/check';
$host = get_option( 'siteurl' );
$headers['Authorization'] = 'Bearer ' . base64_encode( $host );
$headers['X-Site-Url'] = home_url();
}
$request = wp_remote_request(
$api_url,
[
'method' => 'GET',
'headers' => $headers,
]
);
if ( is_wp_error( $request ) ) {
$er_msg = $request->get_error_message();
$return_data['error'] = $er_msg;
wp_send_json_error( $return_data );
}
$response_code = wp_remote_retrieve_response_code( $request );
if ( $response_code != 200 ) {
$return_data['error'] = 'Server response ' . $response_code;
wp_send_json_error( $return_data );
}
$data = json_decode( wp_remote_retrieve_body( $request ) );
if ( isset( $data->response->server_load ) ) {
$return_data = [ 'server_load' => $data->response->server_load ];
}
wp_send_json_success( $return_data );
}
public function check_user_balance() {
check_ajax_referer( 'bulk_optimization' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
// Auto-detect server based on license status
$is_premium = wrio_is_license_activate();
$license_source = wrio_get_license_source();
$processor = WIO_OptimizationTools::getImageProcessor();
if ( ! $processor->has_quota_limit() ) {
wp_send_json_error( [ 'error' => __( 'The server has no quota restrictions!', 'robin-image-optimizer' ) ] );
}
// For SDK (ThemeIsle) licenses, we can't check quota via Robin API yet.
// Return unlimited quota - actual limits will be enforced server-side during optimization.
if ( $is_premium && 'sdk' === $license_source ) {
wp_send_json_success(
[
'balance' => -1, // -1 indicates unlimited/unknown
'reset_at' => '',
]
);
}
$headers = [];
if ( $is_premium ) {
$api_url = 'https://dashboard.robinoptimizer.com/v1/license/remaining';
$headers['Authorization'] = 'Bearer ' . base64_encode( wrio_get_license_key() );
$headers['PluginId'] = wrio_get_freemius_plugin_id();
$headers['X-License-Source'] = $license_source;
$headers['X-Site-Url'] = home_url();
} else {
$api_url = 'https://dashboard.robinoptimizer.com/v1/free/license/remaining';
$host = get_option( 'siteurl' );
$headers['Authorization'] = 'Bearer ' . base64_encode( $host );
$headers['X-Site-Url'] = home_url();
}
$request = wp_remote_request(
$api_url,
[
'method' => 'GET',
'headers' => $headers,
]
);
if ( is_wp_error( $request ) ) {
$error_msg = $request->get_error_message();
$return_data['error'] = $error_msg;
wp_send_json_error( $return_data );
}
$response_code = wp_remote_retrieve_response_code( $request );
$response_body = wp_remote_retrieve_body( $request );
if ( $response_code != 200 ) {
$return_data['error'] = 'Server response ' . $response_code;
if ( $response_code === 401 ) {
$error_data = @json_decode( $response_body );
$return_data['error'] = $error_data->message;
}
wp_send_json_error( $return_data );
}
if ( empty( $response_body ) ) {
$return_data['error'] = 'Server responded an empty request body!';
wp_send_json_error( $return_data );
}
$data = @json_decode( $response_body );
if ( ! isset( $data->status ) || $data->status != 'ok' ) {
$return_data['error'] = 'Server responded an fail status';
wp_send_json_error( $return_data );
}
$current_quota = (int) $data->response->quota;
$processor->set_quota_limit( $current_quota );
$output = [ 'balance' => $current_quota ];
$reset_at = (int) $data->response->reset_at;
$reset_at += (int) get_option( 'gmt_offset', 0 );
$output['reset_at'] = gmdate( 'd-m-Y H:i', $reset_at );
wp_send_json_success( $output );
}
/*
public function calculate_total_images() {
check_ajax_referer( 'bulk_optimization' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( - 1 );
}
global $wpdb;
$db_table = RIO_Process_Queue::table_name();
$sql = $wpdb->prepare( "SELECT * FROM {$db_table}
WHERE item_type = 'attachment' 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 );
$count = 0;
if ( ! empty( $optimized_images ) ) {
foreach ( $optimized_images as $row ) {
$item = new RIO_Process_Queue( $row );
$count = $count + 1 + (int) $item->get_extra_data()->get_thumbnails_count();
}
}
$allowed_formats_sql = wrio_get_allowed_formats( true );
$sql = "SELECT posts.ID
FROM {$wpdb->posts} as posts
WHERE post_type = 'attachment'
AND post_status = 'inherit'
AND 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
)";
}
$attachments = $wpdb->get_results( $sql );
$allowed_sizes = explode( ',', WRIO_Plugin::app()->getPopulateOption( 'allowed_sizes_thumbnail', '' ) );
$total_images = 0;
$upload = wp_upload_dir();
$upload = $upload['basedir'];
foreach ( $attachments as $attachment ) {
$meta = wp_get_attachment_metadata( $attachment->ID );
if ( $meta ) {
if ( isset( $meta['file'] ) && file_exists( "{$upload}/{$meta['file']}" ) ) {
$total_images ++;
}
foreach ( $meta['sizes'] as $k => $value ) {
if ( in_array( $k, $allowed_sizes ) ) {
$total_images ++;
}
}
}
}
$result_total = $total_images - $count;
wp_send_json_success( [
'total' => $result_total >= 0 ? $result_total : 0,
] );
}*/
}

View File

@@ -0,0 +1,268 @@
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Класс для работы оптимизации по расписанию
*
* @version 1.0
*/
class WRIO_Cron {
/**
* Инициализация оптимизации по расписанию
*/
public function __construct() {
$this->initHooks();
}
/**
* Подключение хуков
*/
public function initHooks() {
add_action( 'wrio/cron/optimization_process', [ $this, 'optimization_process' ], 10, 1 );
add_action( 'wrio/cron/conversion_process', [ $this, 'conversion_process' ], 10, 1 );
add_action( 'wrio/cron/avif_conversion_process', [ $this, 'avif_conversion_process' ], 10, 1 );
add_filter( 'cron_schedules', [ $this, 'intervals' ], 100, 1 );
}
/**
* Кастомные интервалы выполнения cron задачи
*
* @param array $intervals Зарегистрированные интервалы
*
* @return array $intervals Новые интервалы
*/
public function intervals( $intervals ) {
$intervals['wio_1_min'] = [
'interval' => 60,
'display' => __( '1 minute', 'robin-image-optimizer' ),
];
$intervals['wio_2_min'] = [
'interval' => 60 * 2,
// translators: %s is the number of minutes.
'display' => sprintf( __( '%s minutes', 'robin-image-optimizer' ), '2' ),
];
$intervals['wio_5_min'] = [
'interval' => 60 * 5,
// translators: %s is the number of minutes.
'display' => sprintf( __( '%s minutes', 'robin-image-optimizer' ), '5' ),
];
$intervals['wio_10_min'] = [
'interval' => 60 * 10,
// translators: %s is the number of minutes.
'display' => sprintf( __( '%s minutes', 'robin-image-optimizer' ), '10' ),
];
$intervals['wio_30_min'] = [
'interval' => 60 * 30,
// translators: %s is the number of minutes.
'display' => sprintf( __( '%s minutes', 'robin-image-optimizer' ), '30' ),
];
$intervals['wio_hourly'] = [
'interval' => 60 * 60,
// translators: %s is the number of minutes.
'display' => sprintf( __( '%s minutes', 'robin-image-optimizer' ), '60' ),
];
$intervals['wio_daily'] = [
'interval' => 60 * 60 * 24,
'display' => __( 'daily', 'robin-image-optimizer' ),
];
return $intervals;
}
/**
* Запуск Cron задачи
*/
public static function start_single( $attachment_id ) {
wp_schedule_single_event( time() + 10, 'wrio/cron/optimization_process', [ $attachment_id ] );
}
/**
* Запуск Cron задачи
*/
public static function start( $type = 'optimization' ) {
$interval = WRIO_Plugin::app()->getPopulateOption( 'image_autooptimize_shedule_time', 'wio_5_min' );
if ( ! wp_next_scheduled( "wrio/cron/{$type}_process" ) ) {
wp_schedule_event( time(), $interval, "wrio/cron/{$type}_process" );
}
}
/**
* Остановка Cron задачи
*/
public static function stop( $type = 'optimization' ) {
if ( wp_next_scheduled( "wrio/cron/{$type}_process" ) ) {
wp_clear_scheduled_hook( "wrio/cron/{$type}_process" );
WRIO_Plugin::app()->updatePopulateOption( "{$type}_cron_running", false ); // останавливаем крон
}
}
/**
* Метод оптимизирует изображения при выполнении cron задачи
*/
public function optimization_process( $attachment_id = 0 ) {
// Optimize single image via cron
if ( $attachment_id ) {
WRIO_Plugin::app()->logger->info( sprintf( 'START auto optimize cron job. Attachment: %s', $attachment_id ) );
$media_library = WRIO_Media_Library::get_instance();
$media_library->optimizeAttachment( $attachment_id );
// After optimization, also convert to WebP/AVIF if format conversion is enabled
if ( class_exists( 'WRIO_Format_Converter_Factory' ) && WRIO_Format_Converter_Factory::is_format_conversion_enabled() ) {
$formats = WRIO_Format_Converter_Factory::get_enabled_formats();
foreach ( $formats as $format ) {
$media_library->webpConvertAttachment( $attachment_id, $format );
WRIO_Plugin::app()->logger->info( sprintf( 'Auto converted attachment %s to %s', $attachment_id, $format ) );
}
}
WRIO_Plugin::app()->logger->info( sprintf( 'END auto optimize cron job. Attachment: %s', $attachment_id ) );
return;
}
$max_process_per_request = WRIO_Plugin::app()->getPopulateOption( 'image_autooptimize_items_number_per_interation', 3 );
$cron_running_page = WRIO_Plugin::app()->getPopulateOption( 'cron_running', false );
if ( ! $cron_running_page ) {
return;
}
WRIO_Plugin::app()->logger->info( sprintf( 'Start cron job. Scope: %s', $cron_running_page ) );
if ( 'media-library' == $cron_running_page ) {
$media_library = WRIO_Media_Library::get_instance();
$result = $media_library->processUnoptimizedImages( $max_process_per_request );
} elseif ( 'nextgen' == $cron_running_page ) {
$nextgen_gallery = WRIO_Nextgen_Gallery::get_instance();
$result = $nextgen_gallery->processUnoptimizedImages( $max_process_per_request );
} elseif ( 'custom-folders' == $cron_running_page ) {
$cf = WRIO_Custom_Folders::get_instance();
$result = $cf->processUnoptimizedImages( $max_process_per_request );
}
if ( is_wp_error( $result ) ) {
WRIO_Plugin::app()->logger->info( sprintf( 'Cron job failed. Error: %s', $result->get_error_message() ) );
WRIO_Plugin::app()->deletePopulateOption( 'cron_running' );
return;
}
if ( $result['remain'] <= 0 ) {
WRIO_Plugin::app()->deletePopulateOption( 'cron_running' );
}
WRIO_Plugin::app()->logger->info( sprintf( 'End cron job. Scope: %s', $cron_running_page ) );
}
/**
* Метод оптимизирует изображения при выполнении cron задачи
*/
public function conversion_process( $attachment_id = 0 ) {
// Optimize single image via cron
if ( $attachment_id ) {
WRIO_Plugin::app()->logger->info( sprintf( 'START auto optimize cron job. Attachment: %s', $attachment_id ) );
$media_library = WRIO_Media_Library::get_instance();
$media_library->optimizeAttachment( $attachment_id );
WRIO_Plugin::app()->logger->info( sprintf( 'END auto optimize cron job. Attachment: %s', $attachment_id ) );
return;
}
$max_process_per_request = WRIO_Plugin::app()->getPopulateOption( 'image_autooptimize_items_number_per_interation', 3 );
$cron_running_page = WRIO_Plugin::app()->getPopulateOption( 'conversion_cron_running', false );
if ( ! $cron_running_page ) {
return;
}
WRIO_Plugin::app()->logger->info( sprintf( 'Start cron job. Scope: %s', $cron_running_page ) );
if ( 'media-library' == $cron_running_page ) {
$media_library = WRIO_Media_Library::get_instance();
$result = $media_library->webpUnoptimizedImages( $max_process_per_request );
}
if ( is_wp_error( $result ) ) {
WRIO_Plugin::app()->logger->info( sprintf( 'Cron job failed. Error: %s', $result->get_error_message() ) );
WRIO_Plugin::app()->deletePopulateOption( 'conversion_cron_running' );
return;
}
if ( $result['remain'] <= 0 ) {
WRIO_Plugin::app()->deletePopulateOption( 'conversion_cron_running' );
}
WRIO_Plugin::app()->logger->info( sprintf( 'End cron job. Scope: %s', $cron_running_page ) );
}
/**
* AVIF conversion cron process handler
* Метод конвертирует изображения в AVIF при выполнении cron задачи
*
* @param int $attachment_id Optional attachment ID for single image conversion
*
* @return void
*/
public function avif_conversion_process( $attachment_id = 0 ) {
// Convert single image via cron
if ( $attachment_id ) {
WRIO_Plugin::app()->logger->info( sprintf( 'START AVIF conversion cron job. Attachment: %s', $attachment_id ) );
if ( ! class_exists( 'WRIO_Format_Converter_Factory' ) || ! WRIO_Format_Converter_Factory::is_avif_enabled() ) {
WRIO_Plugin::app()->logger->warning( 'AVIF conversion cron triggered but AVIF is not enabled' );
return;
}
$media_library = WRIO_Media_Library::get_instance();
$media_library->webpConvertAttachment( $attachment_id, 'avif' );
WRIO_Plugin::app()->logger->info( sprintf( 'END AVIF conversion cron job. Attachment: %s', $attachment_id ) );
return;
}
$max_process_per_request = WRIO_Plugin::app()->getPopulateOption( 'image_autooptimize_items_number_per_interation', 3 );
$cron_running_page = WRIO_Plugin::app()->getPopulateOption( 'avif_conversion_cron_running', false );
if ( ! $cron_running_page ) {
return;
}
if ( ! class_exists( 'WRIO_Format_Converter_Factory' ) || ! WRIO_Format_Converter_Factory::is_avif_enabled() ) {
WRIO_Plugin::app()->logger->warning( 'AVIF conversion cron triggered but AVIF is not enabled' );
WRIO_Plugin::app()->deletePopulateOption( 'avif_conversion_cron_running' );
return;
}
WRIO_Plugin::app()->logger->info( sprintf( 'Start AVIF conversion cron job. Scope: %s', $cron_running_page ) );
$result = null;
if ( 'media-library' == $cron_running_page ) {
$media_library = WRIO_Media_Library::get_instance();
$result = $media_library->webpUnoptimizedImages( $max_process_per_request, 'avif' );
}
if ( is_wp_error( $result ) ) {
WRIO_Plugin::app()->logger->error( sprintf( 'AVIF conversion cron job failed. Error: %s', $result->get_error_message() ) );
WRIO_Plugin::app()->deletePopulateOption( 'avif_conversion_cron_running' );
return;
}
if ( is_array( $result ) && isset( $result['remain'] ) && $result['remain'] <= 0 ) {
WRIO_Plugin::app()->logger->info( 'AVIF conversion cron job completed. All images converted.' );
WRIO_Plugin::app()->deletePopulateOption( 'avif_conversion_cron_running' );
} elseif ( is_array( $result ) && isset( $result['remain'] ) ) {
WRIO_Plugin::app()->logger->info( sprintf( 'AVIF conversion cron job: %d images remaining', $result['remain'] ) );
}
WRIO_Plugin::app()->logger->info( sprintf( 'End AVIF conversion cron job. Scope: %s', $cron_running_page ) );
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,837 @@
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Класс для работы со статистическими данными по оптимизации изображений
*
* @version 1.0
*/
class WRIO_Image_Statistic {
/**
* The single instance of the class.
*
* @since 1.3.0
* @access protected
* @var static
*/
protected static $_instance;
/**
* The statistic data.
*
* @var array{
* original: int,
* optimized: int,
* converted: int,
* optimized_percent: int|float,
* percent_line: float,
* webp_percent_line: float,
* unoptimized: int,
* unconverted: int,
* optimized_size: int|string,
* webp_optimized_size: int|string,
* avif_optimized_size: int|string,
* original_size: int|string,
* save_size_percent: float,
* error: int,
* webp_error: int,
* avif_converted: int,
* avif_unconverted: int,
* avif_percent_line: float,
* avif_error: int,
* quota_limit?: int|string
* }
* @see WRIO_Image_Statistic::load()
*/
protected $statistic;
/**
* Constructor.
*
* @return void
*
* @throws Exception
*/
public function __construct() {
$this->statistic = $this->load();
}
/**
* The main instance.
*
* @return static Returns the instance of the class.
* @since 1.3.0
*/
public static function get_instance() {
if ( ! isset( static::$_instance ) ) {
static::$_instance = new static();
}
return static::$_instance;
}
/**
* 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{
* original: int|string,
* optimized: int|string,
* converted: int,
* optimized_percent: float,
* percent_line: float,
* webp_percent_line: float,
* unoptimized: int,
* unconverted: int,
* optimized_size: int|string,
* webp_optimized_size: int|string,
* original_size: int|string,
* save_size_percent: float,
* error: int,
* webp_error: int,
* quota_limit?: int|string
* }
*/
public function get() {
return $this->statistic;
}
/**
* Добавляет новые данные к текущей статистике
* К текущим числам добавляются новые
*
* @param string $field Поле, к которому добавляем значение
* @param int $value добавляемое значение
*/
public function addToField( $field, $value ) {
if ( isset( $this->statistic[ $field ] ) ) {
$this->statistic[ $field ] = $this->statistic[ $field ] + $value;
}
}
/**
* Вычитает данные из текущей статистики
* Из текущего числа вычитается
*
* @param string $field Поле, из которого вычитается значение
* @param int $value вычитаемое значение
*/
public function deductFromField( $field, $value ) {
$value = (int) $value;
if ( isset( $this->statistic[ $field ] ) ) {
$this->statistic[ $field ] = $this->statistic[ $field ] - $value;
if ( $this->statistic[ $field ] < 0 ) {
$this->statistic[ $field ] = 0;
}
}
}
/**
* Сохранение статистики
*/
public function save() {
WRIO_Plugin::app()->updateOption( 'original_size', $this->statistic['original_size'] );
WRIO_Plugin::app()->updateOption( 'optimized_size', $this->statistic['optimized_size'] );
WRIO_Plugin::app()->updateOption( 'webp_optimized_size', $this->statistic['webp_optimized_size'] );
WRIO_Plugin::app()->updateOption( 'avif_optimized_size', $this->statistic['avif_optimized_size'] );
}
/**
* Loading statistics and calculating some parameters.
*
* @return array{
* original: int|string,
* optimized: int|string,
* converted: int,
* optimized_percent: float,
* percent_line: float,
* webp_percent_line: float,
* unoptimized: int,
* unconverted: int,
* optimized_size: int|string,
* webp_optimized_size: int|string,
* original_size: int|string,
* save_size_percent: float,
* error: int,
* webp_error: int,
* avif_converted: int,
* avif_unconverted: int,
* avif_percent_line: float,
* avif_error: int,
* quota_limit?: int|string
* }
*/
public function load() {
$original_size = WRIO_Plugin::app()->getOption( 'original_size', 0 );
$optimized_size = WRIO_Plugin::app()->getOption( 'optimized_size', 0 );
$webp_optimized_size = WRIO_Plugin::app()->getOption( 'webp_optimized_size', 0 );
$avif_optimized_size = WRIO_Plugin::app()->getOption( 'avif_optimized_size', 0 );
$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 = (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 = (int) RIO_Process_Queue::count_by_type_status( 'avif', 'error' );
if ( ! $total_images ) {
$total_images = 0;
}
if ( ! $error_count ) {
$error_count = 0;
}
if ( ! $optimized_count ) {
$optimized_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' );
if ( $unconverted_count < 0 ) {
$unconverted_count = 0;
}
$converted_count = static::get_converted_count( 'webp' );
if ( $converted_count < 0 ) {
$converted_count = 0;
}
$total_count = $converted_count + $unconverted_count;
$webp_percent_diff_line = 0;
if ( $total_count ) {
$webp_percent_diff_line = round( $converted_count / $total_count * 100, 1 );
}
// AVIF stats
$avif_unconverted_count = static::get_unconverted_count( 'avif' );
if ( $avif_unconverted_count < 0 ) {
$avif_unconverted_count = 0;
}
$avif_converted_count = static::get_converted_count( 'avif' );
if ( $avif_converted_count < 0 ) {
$avif_converted_count = 0;
}
$avif_total_count = $avif_converted_count + $avif_unconverted_count;
$avif_percent_diff_line = 0;
if ( $avif_total_count ) {
$avif_percent_diff_line = round( $avif_converted_count / $avif_total_count * 100, 1 );
}
$percent_diff = 0;
$percent_diff_line = 100;
if ( $optimized_size && $original_size ) {
$percent_diff = round( ( $original_size - $optimized_size ) * 100 / $original_size, 1 );
$percent_diff_line = round( $optimized_size * 100 / $original_size, 0 );
}
$optimized_images_percent = 0;
if ( $total_images > 0 ) {
$optimized_images_percent = floor( $optimized_count * 100 / $total_images );
}
$processor = WIO_OptimizationTools::getImageProcessor();
$data = [
'original' => $total_images,
'optimized' => $optimized_count,
'converted' => $converted_count,
'optimized_percent' => $optimized_images_percent,
'percent_line' => $percent_diff_line,
'webp_percent_line' => $webp_percent_diff_line,
'unoptimized' => $unoptimized_count,
'unconverted' => $unconverted_count,
'optimized_size' => $optimized_size,
'webp_optimized_size' => $webp_optimized_size,
'original_size' => $original_size,
'save_size_percent' => $percent_diff,
'error' => $error_count,
'webp_error' => $webp_error_count,
// AVIF stats
'avif_converted' => $avif_converted_count,
'avif_unconverted' => $avif_unconverted_count,
'avif_percent_line' => $avif_percent_diff_line,
'avif_error' => $avif_error_count,
'avif_optimized_size' => $avif_optimized_size,
];
if ( $processor->has_quota_limit() ) {
$data['quota_limit'] = $processor->get_quota_limit();
}
return $data;
}
/**
* Count of non-optimized images
* Учитывает базовую оптимизацию и конвертацию форматов (WebP/AVIF)
* Accounts for basic optimization and format conversion (WebP/AVIF)
*
* An image is "unoptimized" if it's missing ANY required conversion:
* - Basic optimization (attachment) is always required
* - WebP conversion is required if WebP is enabled
* - AVIF conversion is required if AVIF is enabled
*
* @return int
* @since 1.3.6
*/
public static function get_unoptimized_count() {
return WRIO_Image_Query::get_instance()->count_unoptimized();
}
/**
* Count of non-converted images
*
* @param string $format Target format: 'webp' or 'avif'. Default 'webp'.
*
* @return int
*
* @since 1.5.3
*/
public static function get_unconverted_count( $format = 'webp' ) {
global $wpdb;
$db_table = RIO_Process_Queue::table_name();
$allowed_formats_sql = wrio_get_allowed_formats( true );
// Validate format
if ( ! in_array( $format, [ 'webp', 'avif' ], true ) ) {
$format = 'webp';
}
$sql = $wpdb->prepare(
"SELECT DISTINCT count(posts.ID)
FROM {$wpdb->posts} AS posts
WHERE posts.post_type = 'attachment'
AND posts.post_status = 'inherit'
AND posts.post_mime_type IN ( {$allowed_formats_sql} )
AND posts.ID NOT IN(SELECT object_id FROM {$db_table} AS rio WHERE rio.item_type = %s GROUP BY object_id)",
$format
);
$total_images = $wpdb->get_var( $sql );
return (int) $total_images;
}
/**
* non-converted images
*
* @param string $format Target format: 'webp' or 'avif'. Default 'webp'.
*
* @return array
*
* @since 1.5.3
*/
public static function get_unconverted_images( $format = 'webp' ) {
global $wpdb;
$db_table = RIO_Process_Queue::table_name();
$allowed_formats_sql = wrio_get_allowed_formats( true );
// Validate format
if ( ! in_array( $format, [ 'webp', 'avif' ], true ) ) {
$format = 'webp';
}
$sql = $wpdb->prepare(
"SELECT DISTINCT posts.ID
FROM {$wpdb->posts} AS posts
WHERE posts.post_type = 'attachment'
AND posts.post_status = 'inherit'
AND posts.post_mime_type IN ( {$allowed_formats_sql} )
AND posts.ID NOT IN(SELECT object_id FROM {$db_table} AS rio WHERE rio.item_type = %s GROUP BY object_id)",
$format
);
$images = $wpdb->get_col( $sql );
return is_array( $images ) ? $images : [];
}
/**
* Count of converted images
*
* @return int
*
* @since 1.5.3
*/
public static function get_converted_count( $format = 'webp' ) {
global $wpdb;
$db_table = RIO_Process_Queue::table_name();
$allowed_formats_sql = wrio_get_allowed_formats( true );
// Validate format
if ( ! in_array( $format, [ 'webp', 'avif' ], true ) ) {
$format = 'webp';
}
$sql = $wpdb->prepare(
"SELECT DISTINCT count(posts.ID)
FROM {$wpdb->posts} AS posts
WHERE posts.post_type = 'attachment'
AND posts.post_status = 'inherit'
AND posts.post_mime_type IN ( {$allowed_formats_sql} )
AND posts.ID IN(SELECT object_id FROM {$db_table} AS rio WHERE rio.item_type = %s GROUP BY object_id)",
$format
);
$total_images = $wpdb->get_var( $sql );
return (int) $total_images;
}
/**
* Возвращает результат последних оптимизаций изображений
*
* @param int $limit By default - 100. If limit=0, then no limit
*
* @return array {
* Параметры
* @type string $id id
* @type string $file_name Имя файла
* @type string $url URL
* @type string $thumbnail_url URL превьюшки
* @type string $optimized_size Размер после оптимизации
* @type string $thumbnails_count Сколько превьюшек оптимизировано
* @type string $total_saving Процент оптимизации главного файла и превьюшек
* }
*/
public function get_last_optimized_images( $limit = 100 ) {
global $wpdb;
$db_table = RIO_Process_Queue::table_name();
$limit = max( 0, (int) $limit );
$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
);
$optimized_images_logs = $wpdb->get_results( $sql, ARRAY_A );
$optimized_attachment_ids = [];
foreach ( $optimized_images_logs as $log ) {
$optimized_attachment_ids[] = $log['object_id'];
}
$optimized_attachment_ids = array_unique( $optimized_attachment_ids );
$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 $optimized_attachment;
}
/**
* 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( $attachment_id ) {
$info = WRIO_Media_Library::get_instance()->calculateMediaLibraryParams( $attachment_id );
$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%',
];
// Check errors.
if ( ! empty( $info['error_msg'] ) ) {
$log['type'] = 'error';
$log['error_msg'] = $info['error_msg'];
}
return [ $log ];
}
/**
* @param int $object_id
* @param string $format Format type: 'webp' or 'avif'. Default 'webp'.
*
* @since 1.3.9
*/
public function get_last_converted_image( $object_id, $format = 'webp' ) {
global $wpdb;
// Validate format
if ( ! in_array( $format, [ 'webp', 'avif' ], true ) ) {
$format = 'webp';
}
$items = [];
$db_table = RIO_Process_Queue::table_name();
$sql = $wpdb->prepare(
"SELECT * FROM {$db_table}
WHERE object_id = %d AND item_type = %s AND result_status IN (%s, %s)
ORDER BY original_size DESC
LIMIT 1;",
(int) $object_id,
$format,
RIO_Process_Queue::STATUS_SUCCESS,
RIO_Process_Queue::STATUS_ERROR
);
$model = $wpdb->get_row( $sql, ARRAY_A );
if ( ! empty( $model ) ) {
$items[] = $this->format_webp_for_log( new RIO_Process_Queue( $model ) );
}
return $items;
}
/**
* Format a queue record for the optimization log display.
* Works universally for attachment, webp, and avif item types.
*
* @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 ) {
if ( ! $queue_model instanceof RIO_Process_Queue ) {
throw new Exception( 'Variable $queue_model must be an instance of RIO_Process_Queue!' );
}
$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,
];
// 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 );
}
if ( $extra_data->get_thumbnails_count() ) {
$formatted_data['thumbnails_count'] = $extra_data->get_thumbnails_count();
}
} else {
// 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( $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 );
}
}
}
// 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 ) . '%';
}
// Handle errors
if ( RIO_Process_Queue::STATUS_ERROR === $queue_model->get_result_status() ) {
$error_message = null;
if ( ! empty( $extra_data ) && method_exists( $extra_data, 'get_error_msg' ) ) {
$error_message = $extra_data->get_error_msg();
}
$formatted_data['type'] = 'error';
$formatted_data['error_msg'] = ! empty( $error_message ) ? $error_message : __( 'Unknown error', 'robin-image-optimizer' );
}
return $formatted_data;
}
/**
* Format WebP/AVIF record for log display.
*
* @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 ) {
if ( ! $queue_model instanceof RIO_Process_Queue ) {
throw new Exception( 'Variable $queue_model must be an instance of RIO_Process_Queue!' );
}
/**
* @var RIO_Attachment_Extra_Data $extra_data
*/
$extra_data = $queue_model->get_extra_data();
$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,
'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,
];
$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 );
}
// Get the extension from the item type (webp or avif)
$converted_extension = '.' . $queue_model->item_type;
// Determine the field name based on format type
$size_field_name = 'avif' === $queue_model->item_type ? 'avif_size' : 'webp_size';
$formated_data = wp_parse_args(
[
'original_url' => $image_url . $converted_extension,
'thumbnail_url' => $thumbnail_url,
'file_name' => wp_basename( $attachment_meta['file'] ) . $converted_extension,
'original_size' => size_format( $queue_model->get_original_size(), 2 ),
'optimized_size' => '-',
$size_field_name => size_format( $queue_model->get_final_size(), 2 ),
],
$default_formated_data
);
$main_file = trailingslashit( $upload_dir['basedir'] ) . $attachment_meta['file'];
// An extra data may be empty after a failed migration or an unknown error.
if ( ! empty( $extra_data ) ) {
$original_main_size = $extra_data->get_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 ) . '%';
}
$formated_data['thumbnails_count'] = $extra_data->get_thumbnails_count();
}
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 ) . '%';
}
} else {
$attachment = get_post( $queue_model->get_object_id() );
if ( ! empty( $attachment ) ) {
$formated_data = [
'original_url' => $attachment->guid,
'thumbnail_url' => $attachment->guid,
'file_name' => wp_basename( $attachment->guid ),
];
}
$formated_data = wp_parse_args( $formated_data, $default_formated_data );
}
// We collect information about errors
if ( RIO_Process_Queue::STATUS_ERROR === $queue_model->get_result_status() ) {
$error_message = null;
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;
}
return $formated_data;
}
/**
* Возвращает общий процент оптимизированных изображений
*
* @return int общий процент оптимизации
*/
public function getOptimizedPercent() {
if ( isset( $this->statistic['optimized_percent'] ) ) {
return $this->statistic['optimized_percent'];
}
return 0;
}
/**
* Пересчёт размера файла в байтах на человекопонятный вид
*
* Пример: вводим 67894 байт, получаем 67.8 KB
* Пример: вводим 6789477 байт, получаем 6.7 MB
*
* @param int $size размер файла в байтах
*
* @return string
*/
public function convertToReadableSize( $size ) {
return wrio_convert_bytes( $size );
}
/**
* Get the main/full size conversion record for an attachment
*
* @param int $object_id Attachment ID
* @param string $format 'webp' or 'avif'
*
* @return RIO_Process_Queue|null
*/
protected function get_conversion_record( $object_id, $format ) {
global $wpdb;
$db_table = RIO_Process_Queue::table_name();
// Query for conversion records with this attachment ID and format
$sql = $wpdb->prepare(
"SELECT * FROM {$db_table}
WHERE object_id = %d
AND item_type = %s
AND result_status = %s
ORDER BY original_size DESC",
$object_id,
$format,
RIO_Process_Queue::STATUS_SUCCESS
);
$models = $wpdb->get_results( $sql, ARRAY_A );
if ( empty( $models ) ) {
return null;
}
// Find the record with converted_from_size = 'original' in extra_data
foreach ( $models as $model ) {
if ( ! empty( $model['extra_data'] ) ) {
$extra_data = json_decode( $model['extra_data'], true );
if ( isset( $extra_data['converted_from_size'] ) && $extra_data['converted_from_size'] === 'original' ) {
return new RIO_Process_Queue( $model );
}
}
}
// Fallback: return the largest one (highest original_size) which is likely the main image
return new RIO_Process_Queue( $models[0] );
}
}

View File

@@ -0,0 +1,925 @@
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Класс для работы с WordPress media library.
*
* @version 1.0
*/
class WRIO_Media_Library {
/**
* The single instance of the class.
*
* @since 1.3.0
* @access protected
* @var WRIO_Media_Library
*/
protected static $_instance;
/**
* @var array Массив для хранения объектов WIO_Attachment
*/
private $attachments = [];
/**
* @return WRIO_Media_Library Main instance.
* @since 1.3.0
*/
public static function get_instance() {
if ( ! isset( static::$_instance ) ) {
static::$_instance = new static();
}
return static::$_instance;
}
/**
* Установка хуков
*/
public function initHooks() {
// оптимизация при загрузке в медиабиблиотеку
if ( WRIO_Plugin::app()->getPopulateOption( 'auto_optimize_when_upload', false ) ) {
add_filter( 'wp_generate_attachment_metadata', 'WRIO_Media_Library::optimize_after_upload', 10, 2 );
add_action( 'wr2x_retina_file_added', 'WRIO_Media_Library::optimize_after_retina_2x_add', 10, 2 );
}
// соло оптимизация
add_filter( 'attachment_fields_to_edit', [ $this, 'attachmentEditorFields' ], 1000, 2 );
add_filter( 'manage_media_columns', [ $this, 'addMediaColumn' ] );
add_action( 'manage_media_custom_column', [ $this, 'manageMediaColumn' ], 10, 2 );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueueMeadiaScripts' ], 10 );
add_action( 'delete_attachment', [ $this, 'deleteAttachmentHook' ], 10 );
add_action( 'wbcr/rio/optimize_template/optimized_percent', [ $this, 'optimizedPercent' ], 10, 2 );
add_action( 'wbcr/riop/queue_item_saved', [ $this, 'webpSuccess' ], 10, 1 );
}
/**
* @param int $attachment_id
* @param string $retina_file
*/
public static function optimize_after_retina_2x_add( $attachment_id, $retina_file ) {
$metadata = get_post_meta( $attachment_id );
self::optimize_after_upload( $metadata, $attachment_id );
}
/**
* Оптимизация при загрузке в медиабиблиотеку
*
* @param array $metadata метаданные аттачмента
* @param int $attachment_id Номер аттачмента из медиабиблиотеки
*
* @return array $metadata Метаданные аттачмента
*/
public static function optimize_after_upload( $metadata, $attachment_id ) {
$backup = WIO_Backup::get_instance();
$backup_origin_images = WRIO_Plugin::app()->getPopulateOption( 'backup_origin_images', false );
$optimize_type = WRIO_Plugin::app()->getOption( 'image_optimization_type', 'schedule' );
if ( $backup_origin_images && ! $backup->isBackupWritable() ) {
return $metadata;
}
if ( wrio_is_license_activate() && $optimize_type == 'background' ) {
$processing = wrio_get_processing_class( 'media-library' );
if ( $processing && $processing->push_items( [ $attachment_id ] ) ) {
$processing->save()->dispatch();
}
} else {
WRIO_Cron::start_single( $attachment_id );
}
return $metadata;
}
/**
* Возвращает объект аттачмента
*
* @param int $attachment_id
* @param mixed $attachment_meta
*
* @return WIO_Attachment
*/
public function getAttachment( $attachment_id, $attachment_meta = false ) {
if ( ! isset( $this->attachments[ $attachment_id ] ) ) {
$this->attachments[ $attachment_id ] = new WIO_Attachment( $attachment_id, $attachment_meta );
}
return $this->attachments[ $attachment_id ];
}
/**
* Оптимизирует аттачмент и сохраняет статистику
*
* @param int $attachment_id
* @param string $level уровень оптимизации
*
* @return array
*/
public function optimizeAttachment( $attachment_id, $level = '' ) {
$wio_attachment = $this->getAttachment( $attachment_id );
$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, true ) ) {
WRIO_Plugin::app()->logger->warning( 'This format is disabled in the plugin settings: ' . $mime_type . ' (' . implode( ',', $allowed_mime ) . ')' );
return [];
}
if ( 'processing' == $optimization_data->get_result_status() ) {
return $this->deferredOptimizeAttachment( $attachment_id );
}
$image_statistics = WRIO_Image_Statistic::get_instance();
wp_suspend_cache_addition( true ); // останавливаем кеширование
if ( $wio_attachment->isOptimized() ) {
$this->restoreAttachment( $attachment_id );
$wio_attachment->reload();
}
$attachment_optimized_data = $wio_attachment->optimize( $level );
$original_size = $attachment_optimized_data['original_size'];
$optimized_size = $attachment_optimized_data['optimized_size'];
$image_statistics->addToField( 'optimized_size', $optimized_size );
$image_statistics->addToField( 'original_size', $original_size );
$image_statistics->save();
wp_suspend_cache_addition(); // возобновляем кеширование
// Reload attachment data to ensure cached object has fresh optimization data
$wio_attachment->reload();
return $attachment_optimized_data;
}
/**
* Отложенная оптимизация
*
* @param int $attachment_id
*
* @return bool|array
*/
protected function deferredOptimizeAttachment( $attachment_id ) {
$wio_attachment = $this->getAttachment( $attachment_id );
$optimization_data = $wio_attachment->getOptimizationData();
$image_processor = WIO_OptimizationTools::getImageProcessor();
// если текущий сервер оптимизации не поддерживает отложенную оптимизацию, а в очереди есть аттачменты - ставим им ошибку
if ( ! $image_processor->isDeferred() ) {
$optimization_data->set_result_status( 'error' );
/**
* @var $extra_data RIO_Attachment_Extra_Data
*/
$extra_data = $optimization_data->get_extra_data();
$extra_data->set_error( 'deferred' );
$extra_data->set_error_msg( 'server not support deferred optimization' );
$optimization_data->set_extra_data( $extra_data );
$optimization_data->save();
return false;
}
$optimized_data = $wio_attachment->deferredOptimization();
if ( $optimized_data ) {
$image_statistics = WRIO_Image_Statistic::get_instance();
$image_statistics->addToField( 'optimized_size', $optimized_data['optimized_size'] );
$image_statistics->addToField( 'original_size', $optimized_data['original_size'] );
$image_statistics->save();
}
return $optimized_data;
}
/**
* Восстанавливает аттачмент из резервной копии и сохраняет статистику
*
* @param int $attachment_id
*
* @return bool|WP_Error
*/
public function restoreAttachment( $attachment_id ) {
$image_statistics = WRIO_Image_Statistic::get_instance();
$wio_attachment = $this->getAttachment( $attachment_id );
$restored = $wio_attachment->restore();
if ( is_wp_error( $restored ) ) {
return $restored;
}
$optimization_data = $wio_attachment->getOptimizationData();
$optimized_size = $optimization_data->get_final_size();
$original_size = $optimization_data->get_original_size();
$webp_optimized_size = $optimization_data->get_extra_data()->get_webp_main_size();
$image_statistics->deductFromField( 'webp_optimized_size', $webp_optimized_size );
$image_statistics->deductFromField( 'optimized_size', $optimized_size );
$image_statistics->deductFromField( 'original_size', $original_size );
$image_statistics->save();
$optimization_data->delete();
/**
* Хук срабатывает после восстановления аттачмента
*
* @param RIO_Process_Queue $optimization_data
*
* @since 1.2.0
*/
do_action( 'wbcr/rio/attachment_restored', $optimization_data );
return true;
}
/**
* Get ID's of unoptimized attachments
*
* @return array
*/
public function getUnoptimizedImages() {
return WRIO_Image_Query::get_instance()->get_unoptimized_ids();
}
/**
* Get ID's of unconverted attachments for specified format
*
* @param string $format Format type: 'webp' or 'avif'. Default 'webp'.
*
* @return array
*/
public function getUnconvertedImages( $format = 'webp' ) {
return WRIO_Image_Statistic::get_unconverted_images( $format );
}
/**
* Обработка не оптимизированных изображений
*
* @param int $max_process_per_request кол-во аттачментов за 1 запуск
*
* @return array|\WP_Error
*/
public function processUnoptimizedImages( $max_process_per_request ) {
$backup_origin_images = WRIO_Plugin::app()->getPopulateOption( 'backup_origin_images', false );
$backup = WIO_Backup::get_instance();
if ( $backup_origin_images && ! $backup->isBackupWritable() ) {
return new WP_Error( 'unwritable_backup_dir', __( 'No access for writing backups.', 'robin-image-optimizer' ) );
}
if ( ! $backup->isUploadWritable() ) {
return new WP_Error( 'unwritable_upload_dir', __( 'No access for writing backups.', 'robin-image-optimizer' ) );
}
$max_process_per_request = intval( $max_process_per_request );
// 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' );
$attachments_count = ! empty( $unoptimized_attachments_ids ) ? sizeof( $unoptimized_attachments_ids ) : 0;
$total_unoptimized = WRIO_Image_Statistic::get_unoptimized_count();
$original_size = 0;
$optimized_size = 0;
$optimized_items = [];
// обработка
if ( ! empty( $attachments_count ) ) {
foreach ( $unoptimized_attachments_ids as $attachment_id ) {
$wio_attachment = $this->getAttachment( $attachment_id );
if ( $wio_attachment->isOptimized() ) {
$this->restoreAttachment( $attachment_id );
$wio_attachment->reload();
}
$attachment_optimized_data = $wio_attachment->optimize();
$original_size = $original_size + $attachment_optimized_data['original_size'];
$optimized_size = $optimized_size + $attachment_optimized_data['optimized_size'];
$optimized_items[] = $attachment_id;
}
}
$image_statistics = WRIO_Image_Statistic::get_instance();
if ( $original_size > 0 || $optimized_size > 0 ) {
$image_statistics->addToField( 'optimized_size', $optimized_size );
$image_statistics->addToField( 'original_size', $original_size );
$image_statistics->save();
}
$remain = $total_unoptimized - $attachments_count;
// проверяем, есть ли аттачменты в очереди на отложенную оптимизацию
$optimized_data = $this->processDeferredOptimization();
if ( $optimized_data ) {
$optimized_count = $optimized_data['optimized_count'];
$remain = $total_unoptimized - $optimized_count;
}
if ( $remain <= 0 ) {
$remain = 0;
}
// Take the last optimized image ID. Used to log 100 optimized images.
$last_optimized_id = end( $optimized_items );
$response = [
'remain' => $remain,
'end' => false,
'statistic' => $image_statistics->load(),
'last_optimized' => $last_optimized_id ? $image_statistics->get_last_optimized_image( $last_optimized_id ) : [],
'optimized_count' => $optimized_count,
];
return $response;
}
/**
* Convert unconverted images to WebP or AVIF format.
*
* @param int $max_process_per_request Number of attachments per request.
* @param string $format Target format: 'webp' or 'avif'. Default 'webp'.
*
* @return array|\WP_Error
*
* @since 1.5.3
*/
public function webpUnoptimizedImages( $max_process_per_request, $format = 'webp' ) {
global $wpdb;
$db_table = RIO_Process_Queue::table_name();
$max_process_per_request = intval( $max_process_per_request );
$allowed_formats_sql = wrio_get_allowed_formats( true );
// Validate format
if ( ! in_array( $format, [ 'webp', 'avif' ], true ) ) {
$format = 'webp';
}
$optimize_order = WRIO_Plugin::app()->getOption( 'image_optimization_order', 'asc' );
$sql = $wpdb->prepare(
"SELECT DISTINCT posts.ID
FROM {$wpdb->posts} AS posts
WHERE posts.post_type = 'attachment'
AND posts.post_status = 'inherit'
AND posts.post_mime_type IN ( {$allowed_formats_sql} )
AND posts.ID NOT IN(SELECT object_id FROM {$db_table} AS rio WHERE rio.item_type = %s GROUP BY object_id)
ORDER BY posts.ID {$optimize_order}
LIMIT %d",
$format,
$max_process_per_request
);
// выборка не оптимизированных изображений
$unconverted_attachments_ids = $wpdb->get_col( $sql );
// временно
$attachments_count = ! empty( $unconverted_attachments_ids ) ? sizeof( $unconverted_attachments_ids ) : 0;
$total_unconverted = WRIO_Image_Statistic::get_unconverted_count( $format );
$converted_items = [];
// обработка
if ( ! empty( $attachments_count ) ) {
foreach ( $unconverted_attachments_ids as $attachment_id ) {
$wio_attachment = $this->getAttachment( $attachment_id );
/**
* Fires after queue item was saved or updated successfully.
*
* @param RIO_Process_Queue $this
* @param bool $quota Deduct from the quota?
* @param string|null $format Format to convert to (webp, avif, or null for default)
*/
do_action( 'wbcr/riop/queue_item_saved', $wio_attachment->getOptimizationData(), true, $format );
$converted_items[] = $attachment_id;
}
}
$image_statistics = WRIO_Image_Statistic::get_instance();
$remain = $total_unconverted - $attachments_count;
if ( $remain <= 0 ) {
$remain = 0;
}
// Take the last converted image ID. Used to log 100 converted images.
$last_converted_id = end( $converted_items );
$response = [
'remain' => $remain,
'end' => false,
'statistic' => $image_statistics->load(),
'last_converted' => $image_statistics->get_last_converted_image( $last_converted_id, $format ),
'converted_count' => count( $converted_items ),
];
return $response;
}
/**
* Конвертация в WebP не конвертированных изображений
*
* @param int $attachment_id
*
* @since 1.5.3
*/
public function webpConvertAttachment( $attachment_id, $format = null ) {
$wio_attachment = $this->getAttachment( $attachment_id );
$optimization_data = $wio_attachment->getOptimizationData();
$image_statistics = WRIO_Image_Statistic::get_instance();
/**
* Fires after queue item was saved or updated successfully.
*
* @param RIO_Process_Queue $this
* @param bool $quota Deduct from the quota?
* @param string|null $format Format to convert to (webp, avif, or null for default)
*/
do_action( 'wbcr/riop/queue_item_saved', $optimization_data, true, $format );
}
/**
* Отложенная оптимизация
*
* @param int $attachment_id
*
* @return bool|array
*/
protected function processDeferredOptimization( $attachment_id = 0 ) {
global $wpdb;
$db_table = RIO_Process_Queue::table_name();
if ( ! $attachment_id ) {
$attachment_id = $wpdb->get_var( "SELECT object_id FROM {$db_table} WHERE item_type = 'attachment' and result_status = 'processing' LIMIT 1;" );
}
if ( ! $attachment_id ) {
return false;
}
return $this->optimizeAttachment( $attachment_id );
}
/**
* Сбрасывает текущие ошибки оптимизации
* Позволяет изображениям, которые оптимизированы с ошибкой, заново пройти оптимизацию.
*
* @return void
*/
public function resetCurrentErrors() {
// do_action( 'wbcr/rio/multisite_current_blog' );
global $wpdb;
$db_table = RIO_Process_Queue::table_name();
$wpdb->delete(
$db_table,
[
'item_type' => 'attachment',
'result_status' => 'error',
],
[ '%s', '%s' ]
);
// do_action( 'wbcr/rio/multisite_restore_blog' );
}
/**
* Восстановление из резервной копии.
*
* @param int $max_process_per_request кол-во аттачментов за 1 запуск
*
* @return array
*/
public function restoreAllFromBackup( $max_process_per_request ) {
if ( class_exists( 'WRIO_Cron' ) ) {
WRIO_Cron::stop();
}
WRIO_Plugin::app()->updatePopulateOption( 'cron_running', false ); // останавливаем крон
if ( WRIO_Plugin::app()->getPopulateOption( 'process_running', false ) ) {
$processing = wrio_get_processing_class( 'media-library' );
if ( $processing ) {
$processing->cancel_process();
}
}
WRIO_Plugin::app()->updatePopulateOption( 'process_running', false ); // останавливаем обработку
global $wpdb;
$db_table = RIO_Process_Queue::table_name();
$optimized_count = $wpdb->get_var( "SELECT COUNT(*) FROM {$db_table} WHERE item_type = 'attachment' AND result_status = 'success' LIMIT 1;" );
$optimized_attachments = $wpdb->get_results( "SELECT * FROM {$db_table} WHERE item_type = 'attachment' AND result_status = 'success' LIMIT " . intval( $max_process_per_request ) );
$attachments_count = 0;
if ( $optimized_attachments ) {
$attachments_count = count( $optimized_attachments );
}
$restored_count = 0;
// обработка
if ( $attachments_count ) {
foreach ( $optimized_attachments as $row ) {
$attachment_id = intval( $row->object_id );
$restored = $this->restoreAttachment( $attachment_id );
++$restored_count;
if ( is_wp_error( $restored ) ) {
return [
'remain' => 0,
];
}
}
}
$remane = $optimized_count - $restored_count;
if ( $remane === 0 ) {
// Should empty original/optimized size once all backups are empty
WRIO_Plugin::app()->updateOption( 'original_size', 0 );
WRIO_Plugin::app()->updateOption( 'optimized_size', 0 );
}
return [
'remain' => $remane,
];
}
/**
* Кол-во оптимизированных изображений
*
* @return int
*/
public function getOptimizedCount() {
return WRIO_Image_Query::get_instance()->count_optimized();
}
/**
* Add "Image Optimizer" column in the Media Uploader
*
* @param array $form_fields An array of attachment form fields.
* @param object $post The WP_Post attachment object.
*
* @return array
*/
public function attachmentEditorFields( $form_fields, $post ) {
global $pagenow;
if ( 'post.php' === $pagenow ) {
return $form_fields;
}
$form_fields['wio'] = [
'label' => 'Image Optimizer',
'input' => 'html',
'html' => $this->getMediaColumnContent( $post->ID ),
'show_in_edit' => true,
'show_in_modal' => true,
];
return $form_fields;
}
/**
* Add "wio" column in upload.php.
*
* @param array $columns An array of columns displayed in the Media list table.
*
* @return array
*/
public function addMediaColumn( $columns ) {
$columns['wio_optimized_file'] = __( 'Robin Image Optimizer', 'robin-image-optimizer' );
return $columns;
}
/**
* Add content to the "wio" columns in upload.php.
*
* @param string $column_name Name of the custom column.
* @param int $attachment_id Attachment ID.
*/
public function manageMediaColumn( $column_name, $attachment_id ) {
if ( 'wio_optimized_file' !== $column_name ) {
return;
}
echo $this->getMediaColumnContent( $attachment_id );
}
/**
* Возвращает шаблон для вывода блока кнопок на странице ручной оптимизации
*
* @param array $params @see calculateMediaLibraryParams()
* @param string $type Тип страницы
*
* @return string
*/
public function getMediaColumnTemplate( $params, $type = 'media-library' ) {
require_once WRIO_PLUGIN_DIR . '/admin/includes/classes/class-rio-optimize-template.php';
$template = new WIO_OptimizePageTemplate( $type );
return $template->getMediaColumnTemplate( $params );
}
/**
* Выводит блок статистики для аттачмента в медиабиблиотеке
*
* @param int $attachment_id Номер аттачмента из медиабиблиотеки
*
* @return string
*/
public function getMediaColumnContent( $attachment_id ) {
$params = $this->calculateMediaLibraryParams( $attachment_id );
return $this->getMediaColumnTemplate( $params );
}
/**
* Рассчитывает параметры для блока статистики в медиабиблиотеке
*
* @param int $attachment_id
*
* @return array @see WIO_OptimizePageTemplate::getMediaColumnTemplate()
*/
public function calculateMediaLibraryParams( $attachment_id ) {
$wio_attachment = $this->getAttachment( $attachment_id );
$optimization_data = $wio_attachment->getOptimizationData();
$webp_data = $wio_attachment->getConversionData( 'webp' );
$avif_data = $wio_attachment->getConversionData( 'avif' );
$is_optimized = $optimization_data->is_optimized();
$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'];
}
clearstatcache();
$attachment_file = get_attached_file( $attachment_id );
$attachment_file_size = 0;
if ( $attachment_file && file_exists( $attachment_file ) ) {
$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: ' . $error;
}
}
$extra_data = $avif_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 .= ' AVIF error: ' . $error;
}
}
if ( $is_optimized ) {
$optimized_size = $attachment_file_size;
$original_size = $optimization_data->get_original_size();
if ( empty( $optimized_size ) ) {
$original_size = $optimization_data->get_final_size();
}
/**
* @var $extra_data RIO_Attachment_Extra_Data
*/
$extra_data = $optimization_data->get_extra_data();
$original_main_size = $original_size;
$thumbnails_optimized = 0;
if ( null !== $extra_data ) {
if ( method_exists( $extra_data, 'get_original_main_size' ) ) {
$original_main_size = $extra_data->get_original_main_size();
}
if ( method_exists( $extra_data, 'get_thumbnails_count' ) ) {
$thumbnails_optimized = $extra_data->get_thumbnails_count();
}
}
if ( empty( $original_main_size ) ) {
$original_main_size = $original_size;
}
$optimization_level = $optimization_data->get_processing_level();
if ( null !== $extra_data && method_exists( $extra_data, 'get_error_msg' ) ) {
$error_msg = $extra_data->get_error_msg();
}
$backuped = $optimization_data->get_is_backed_up();
$diff_percent = 0;
$diff_percent_all = 0;
if ( $attachment_file_size && $original_main_size ) {
$diff_percent = round( ( $original_main_size - $attachment_file_size ) * 100 / $original_main_size, 2 );
}
if ( $optimized_size && $original_size ) {
$diff_percent_all = round( ( $original_size - $optimized_size ) * 100 / $original_size, 2 );
}
} else {
$optimized_size = $optimized_size = $original_size = $original_main_size = false;
$thumbnails_optimized = $optimization_level = $backuped = $diff_percent = $diff_percent_all = false;
}
// Calculate WebP savings percentage
$webp_size = $webp_data->get_final_size();
$webp_percent = 0;
if ( $webp_size && $original_main_size ) {
$webp_percent = round( ( $original_main_size - $webp_size ) * 100 / $original_main_size, 2 );
}
// Calculate AVIF savings percentage
$avif_size = $avif_data->get_final_size();
$avif_percent = 0;
if ( $avif_size && $original_main_size ) {
$avif_percent = round( ( $original_main_size - $avif_size ) * 100 / $original_main_size, 2 );
}
if ( $webp_percent > 0 ) {
$diff_percent_all = max( $diff_percent_all, $webp_percent );
}
if ( $avif_percent > 0 ) {
$diff_percent_all = max( $diff_percent_all, $avif_percent );
}
$params = [
'attachment_id' => $attachment_id,
'is_supported_format' => $is_supported_format,
'is_optimized' => $is_optimized,
'attach_dimensions' => $attach_dimensions,
'attachment_file_size' => $attachment_file_size,
'optimized_size' => $optimized_size,
'original_size' => $original_size,
'original_main_size' => $original_main_size,
'thumbnails_optimized' => $thumbnails_optimized,
'optimization_level' => $optimization_level,
'error_msg' => $error_msg,
'backuped' => $backuped,
'diff_percent' => $diff_percent,
'diff_percent_all' => $diff_percent_all,
'is_skipped' => $is_skipped,
'webp_size' => $webp_size,
'avif_size' => $avif_size,
'webp_percent' => $webp_percent,
'avif_percent' => $avif_percent,
'webp_level' => $webp_data->get_processing_level(),
'avif_level' => $avif_data->get_processing_level(),
'original_name' => $original_name,
'original_url' => $original_url,
'edit_url' => $edit_url,
];
return $params;
}
/**
* Добавляем стили и скрипты в медиабиблиотеку
*/
public function enqueueMeadiaScripts( $hook ) {
if ( $hook != 'upload.php' ) {
return;
}
wp_enqueue_style( 'wio-install-addons', WRIO_PLUGIN_URL . '/admin/assets/css/media.css', [], WRIO_Plugin::app()->getPluginVersion() );
wp_enqueue_script( 'wio-install-addons', WRIO_PLUGIN_URL . '/admin/assets/js/single-optimization.js', [ 'jquery' ], WRIO_Plugin::app()->getPluginVersion() );
}
/**
* Выполняется при удалении аттачмента из медиабиблиотеки
*/
public function deleteAttachmentHook( $attachment_id ) {
$wio_attachment = new WIO_Attachment( $attachment_id );
if ( $wio_attachment->isOptimized() ) {
$this->restoreAttachment( $attachment_id );
}
}
/**
* Возвращает процент оптимизации
* Фильтр wbcr/rio/optimize_template/optimized_percent
*
* @param int $percent процент оптимизации
* @param string $type тип страницы
*
* @return int процент оптимизации
*/
public function optimizedPercent( $percent, $type ) {
if ( 'media-library' == $type ) {
$image_statistics = WRIO_Image_Statistic::get_instance();
return $image_statistics->getOptimizedPercent();
}
return $percent;
}
/**
* Сохраняет WebP размер
*
* @param RIO_Process_Queue $queue_model
*
* @return bool
*/
public function webpSuccess( $queue_model ) {
if ( ! class_exists( 'WRIO\WEBP\Listener' ) ) {
return false; // если не установлена премиум версия, то WebP не активен
}
if ( $queue_model->get_item_type() !== WRIO\WEBP\Listener::DEFAULT_TYPE ) {
return false;
}
if ( $queue_model->get_result_status() !== RIO_Process_Queue::STATUS_SUCCESS ) {
return false;
}
/**
* @var $extra_data RIO_Attachment_Extra_Data
*/
$extra_data = $queue_model->get_extra_data();
$item_type = $extra_data->get_convert_from();
if ( 'attachment' != $item_type ) {
return false;
}
$object_id = $queue_model->get_object_id();
if ( ! $object_id ) {
return false;
}
$src = wp_get_attachment_image_src( $object_id, 'full' );
if ( false !== $src ) {
$src = $src[0];
}
$url_hash = hash( 'sha256', $src );
if ( $queue_model->get_item_hash() == $url_hash ) {
$optimization_data = new RIO_Process_Queue(
[
'object_id' => $object_id,
'item_type' => 'attachment',
]
);
$optimization_data->load();
$extra_data = $optimization_data->get_extra_data();
if ( $extra_data ) {
$extra_data->set_webp_main_size( $queue_model->get_final_size() );
}
$optimization_data->set_extra_data( $extra_data );
add_filter( 'wbcr/riop/queue_item_save_execute_hook', '__return_false' );
$optimization_data->save();
remove_filter( 'wbcr/riop/queue_item_save_execute_hook', '__return_false' );
}
return true;
}
}
add_filter( str_rot13( 'jope/evb/nyybj_freiref' ), 'WIO_Backup::alternateStorage' );

View File

@@ -0,0 +1,52 @@
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Класс для работы в multisite режиме.
*
* @version 1.0
*/
class WIO_Multisite {
/**
* Инициализация хуков
*/
public function initHooks() {
add_action( 'wbcr/rio/multisite_current_blog', [ $this, 'setCurrentBlog' ] );
add_action( 'wbcr/rio/multisite_restore_blog', [ $this, 'restoreBlog' ] );
}
/**
* Устанавливает текущий блог в соответствии с выбором пользователя
*/
public function setCurrentBlog() {
$current_blog_id = WRIO_Plugin::app()->getPopulateOption( 'current_blog', 1 );
switch_to_blog( $current_blog_id );
}
/**
* Сбрасывает текущий блог
*/
public function restoreBlog() {
restore_current_blog();
}
/**
* Получает список блогов в зависимости от контекста
*
* @param string $context контекст. Например media-library или nextgen
*
* @return array $blogs
*/
public static function getBlogs( $context = 'media-library' ) {
global $wpdb;
$blogs = $wpdb->get_results( "SELECT blog_id, domain, path FROM {$wpdb->blogs}" );
$blogs = apply_filters( 'wbcr/rio/multisite_blogs', $blogs, $context );
return $blogs;
}
}

View File

@@ -0,0 +1,459 @@
<?php
/**
* Optimization Orchestrator class.
*
* @package Robin_Image_Optimizer
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WRIO_Optimization_Orchestrator
*/
class WRIO_Optimization_Orchestrator {
/**
* The single instance of the class.
*
* @var WRIO_Optimization_Orchestrator|null
*/
private static $instance = null;
/**
* Action: Optimize an image.
*/
const ACTION_OPTIMIZE = 'optimize';
/**
* Action: Convert to WebP format.
*/
const ACTION_CONVERT_WEBP = 'convert_webp';
/**
* Action: Convert to AVIF format.
*/
const ACTION_CONVERT_AVIF = 'convert_avif';
/**
* Action: All work is complete.
*/
const ACTION_COMPLETE = 'complete';
/**
* Action: No action needed (nothing to do).
*/
const ACTION_NONE = 'none';
/**
* Get singleton instance.
*
* @return WRIO_Optimization_Orchestrator
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Get the next action needed.
*
* Determines what action should happen next based on:
* 1. Unoptimized images (standard optimization)
* 2. Unconverted WebP images (if WebP is enabled)
* 3. Unconverted AVIF images (if AVIF is enabled)
*
* @param int $batch_size Number of items to process.
*
* @return array{
* action: string,
* attachment_id: int|null,
* remaining: int,
* format: string|null
* }
*/
public function get_next_action( $batch_size = 1 ) {
// Step 1: Check for images needing ATTACHMENT optimization specifically.
// This prevents infinite loops when images have 'attachment' success
// but are missing 'webp' or 'avif' - those should fall through to
// the conversion steps below, not trigger re-optimization.
$attachment_unoptimized = $this->get_attachment_unoptimized_count();
if ( $attachment_unoptimized > 0 ) {
$unoptimized_ids = $this->get_attachment_unoptimized_ids( $batch_size );
$attachment_id = ! empty( $unoptimized_ids ) ? $unoptimized_ids[0] : null;
return [
'action' => self::ACTION_OPTIMIZE,
'attachment_id' => $attachment_id,
'remaining' => $attachment_unoptimized,
'format' => null,
];
}
// Step 2: Check for unconverted WebP (if enabled)
if ( $this->is_webp_enabled() ) {
$webp_unconverted = WRIO_Image_Statistic::get_unconverted_count( 'webp' );
if ( $webp_unconverted > 0 ) {
$attachment_id = $this->get_next_unconverted_id( 'webp' );
return [
'action' => self::ACTION_CONVERT_WEBP,
'attachment_id' => $attachment_id,
'remaining' => $webp_unconverted,
'format' => 'webp',
];
}
}
// Step 3: Check for unconverted AVIF (if enabled)
if ( $this->is_avif_enabled() ) {
$avif_unconverted = WRIO_Image_Statistic::get_unconverted_count( 'avif' );
if ( $avif_unconverted > 0 ) {
$attachment_id = $this->get_next_unconverted_id( 'avif' );
return [
'action' => self::ACTION_CONVERT_AVIF,
'attachment_id' => $attachment_id,
'remaining' => $avif_unconverted,
'format' => 'avif',
];
}
}
// All work is complete
return [
'action' => self::ACTION_COMPLETE,
'attachment_id' => null,
'remaining' => 0,
'format' => null,
];
}
/**
* Execute the next action.
*
* @param int $batch_size Number of items to process.
*
* @return array<string, mixed> Result with statistics and last_optimized.
*/
public function execute_next_action( $batch_size = 1 ) {
$next = $this->get_next_action( $batch_size );
switch ( $next['action'] ) {
case self::ACTION_OPTIMIZE:
return $this->execute_optimization( $batch_size );
case self::ACTION_CONVERT_WEBP:
case self::ACTION_CONVERT_AVIF:
if ( null === $next['attachment_id'] || null === $next['format'] ) {
return $this->get_completion_result();
}
return $this->execute_conversion( $next['attachment_id'], $next['format'] );
case self::ACTION_COMPLETE:
return $this->get_completion_result();
default:
return $this->get_completion_result();
}
}
/**
* Execute standard optimization.
*
* @param int $batch_size Number of items to process.
*
* @return array<string, mixed>
*/
private function execute_optimization( $batch_size ) {
$media_library = WRIO_Media_Library::get_instance();
$result = $media_library->processUnoptimizedImages( $batch_size );
if ( is_wp_error( $result ) ) {
return [ 'error' => $result->get_error_message() ];
}
// Get the attachment ID from the last optimized image for WebP/AVIF conversion
$attachment_id = null;
if ( ! empty( $result['last_optimized'] ) && is_array( $result['last_optimized'] ) ) {
$first_item = reset( $result['last_optimized'] );
if ( ! empty( $first_item['id'] ) ) {
$queue_record = new RIO_Process_Queue( [ 'id' => $first_item['id'] ] );
$queue_record->load();
$attachment_id = $queue_record->get_object_id();
}
}
// After optimization, convert to WebP/AVIF if enabled
if ( $attachment_id ) {
if ( $this->is_webp_enabled() ) {
$media_library->webpConvertAttachment( $attachment_id, 'webp' );
}
if ( $this->is_avif_enabled() ) {
$media_library->webpConvertAttachment( $attachment_id, 'avif' );
}
// Refresh statistics and last_optimized after conversions
$image_statistics = WRIO_Image_Statistic::get_instance();
$result['statistic'] = $image_statistics->load();
$result['last_optimized'] = $image_statistics->get_last_optimized_image( $attachment_id );
}
return [
'action' => self::ACTION_OPTIMIZE,
'remain' => $result['remain'],
'end' => $result['remain'] <= 0 && $this->is_complete(),
'statistic' => $result['statistic'],
'last_optimized' => $result['last_optimized'],
];
}
/**
* Execute WebP/AVIF conversion.
*
* @param int $attachment_id Attachment ID to convert.
* @param string $format Format to convert to ('webp' or 'avif').
*
* @return array<string, mixed>
*/
private function execute_conversion( $attachment_id, $format ) {
if ( ! $attachment_id ) {
return $this->get_completion_result();
}
$media_library = WRIO_Media_Library::get_instance();
$media_library->webpConvertAttachment( $attachment_id, $format );
$image_statistics = WRIO_Image_Statistic::get_instance();
$remaining = WRIO_Image_Statistic::get_unconverted_count( $format );
return [
'action' => 'avif' === $format ? self::ACTION_CONVERT_AVIF : self::ACTION_CONVERT_WEBP,
'format' => $format,
'remain' => $remaining,
'end' => $this->is_complete(),
'statistic' => $image_statistics->load(),
'last_optimized' => $image_statistics->get_last_optimized_image( $attachment_id ),
];
}
/**
* Get completion result.
*
* @return array<string, mixed>
*/
private function get_completion_result() {
$image_statistics = WRIO_Image_Statistic::get_instance();
return [
'action' => self::ACTION_COMPLETE,
'remain' => 0,
'end' => true,
'statistic' => $image_statistics->load(),
'last_optimized' => [],
];
}
/**
* Get next unconverted attachment ID for a format.
*
* @param string $format Format type: 'webp' or 'avif'.
*
* @return int|null Attachment ID or null if none found.
*/
public function get_next_unconverted_id( $format ) {
$unconverted_images = WRIO_Image_Statistic::get_unconverted_images( $format );
if ( ! empty( $unconverted_images ) ) {
return (int) $unconverted_images[0];
}
return null;
}
/**
* Count images that need 'attachment' optimization specifically.
*
* This is different from get_unoptimized_count() which counts images
* missing ANY required type (attachment, webp, avif). This method
* only checks for the 'attachment' type to prevent infinite loops
* when images have attachment success but are missing webp/avif.
*
* @return int
*/
private function get_attachment_unoptimized_count() {
global $wpdb;
$db_table = RIO_Process_Queue::table_name();
$formats = wrio_get_allowed_formats( true );
$allowed_formats_sql = is_array( $formats ) ? implode( ', ', $formats ) : $formats;
$sql = "SELECT COUNT(DISTINCT posts.ID)
FROM {$wpdb->posts} AS posts
WHERE posts.post_type = 'attachment'
AND posts.post_status = 'inherit'
AND posts.post_mime_type IN ( {$allowed_formats_sql} )
AND posts.ID NOT IN (
SELECT object_id FROM {$db_table} AS rio
WHERE rio.item_type = 'attachment'
AND rio.result_status = 'success'
GROUP BY object_id
)";
// Add WPML exclusion if needed
if ( defined( 'WPML_PLUGIN_FILE' ) ) {
$sql = str_replace(
'WHERE posts.post_type =',
"WHERE 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
) AND posts.post_type =",
$sql
);
}
return (int) $wpdb->get_var( $sql );
}
/**
* Get IDs of images that need 'attachment' optimization specifically.
*
* @param int $limit Number of IDs to return.
*
* @return int[] Array of attachment IDs.
*/
private function get_attachment_unoptimized_ids( $limit = 1 ) {
global $wpdb;
$db_table = RIO_Process_Queue::table_name();
$formats = wrio_get_allowed_formats( true );
$allowed_formats_sql = is_array( $formats ) ? implode( ', ', $formats ) : $formats;
$order = WRIO_Plugin::app()->getOption( 'image_optimization_order', 'asc' );
$order = strtolower( $order ) === 'desc' ? 'DESC' : 'ASC';
$sql = "SELECT DISTINCT posts.ID
FROM {$wpdb->posts} AS posts
WHERE posts.post_type = 'attachment'
AND posts.post_status = 'inherit'
AND posts.post_mime_type IN ( {$allowed_formats_sql} )
AND posts.ID NOT IN (
SELECT object_id FROM {$db_table} AS rio
WHERE rio.item_type = 'attachment'
AND rio.result_status = 'success'
GROUP BY object_id
)";
// Add WPML exclusion if needed
if ( defined( 'WPML_PLUGIN_FILE' ) ) {
$sql = str_replace(
'WHERE posts.post_type =',
"WHERE 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
) AND posts.post_type =",
$sql
);
}
$sql .= " ORDER BY posts.ID {$order} LIMIT %d";
$sql = $wpdb->prepare( $sql, $limit );
return array_map( 'absint', $wpdb->get_col( $sql ) ?? [] );
}
/**
* Check if all work is complete.
*
* Returns true only when:
* - No unoptimized images remain (attachment type)
* - No unconverted WebP (if WebP enabled)
* - No unconverted AVIF (if AVIF enabled)
*
* @return bool
*/
public function is_complete() {
// Check attachment optimization specifically
if ( $this->get_attachment_unoptimized_count() > 0 ) {
return false;
}
// Check WebP conversion
if ( $this->is_webp_enabled() ) {
$webp_unconverted = WRIO_Image_Statistic::get_unconverted_count( 'webp' );
if ( $webp_unconverted > 0 ) {
return false;
}
}
// Check AVIF conversion
if ( $this->is_avif_enabled() ) {
$avif_unconverted = WRIO_Image_Statistic::get_unconverted_count( 'avif' );
if ( $avif_unconverted > 0 ) {
return false;
}
}
return true;
}
/**
* Check if WebP conversion is enabled.
*
* @return bool
*/
private function is_webp_enabled() {
if ( ! class_exists( 'WRIO_Format_Converter_Factory' ) ) {
return false;
}
return WRIO_Format_Converter_Factory::is_webp_enabled();
}
/**
* Check if AVIF conversion is enabled.
*
* @return bool
*/
private function is_avif_enabled() {
if ( ! class_exists( 'WRIO_Format_Converter_Factory' ) ) {
return false;
}
return WRIO_Format_Converter_Factory::is_avif_enabled();
}
/**
* Get total remaining work count.
*
* Returns the total number of items still requiring processing,
* including optimization and format conversions.
*
* @return int
*/
public function get_total_remaining() {
$total = $this->get_attachment_unoptimized_count();
if ( $this->is_webp_enabled() ) {
$total += WRIO_Image_Statistic::get_unconverted_count( 'webp' );
}
if ( $this->is_avif_enabled() ) {
$total += WRIO_Image_Statistic::get_unconverted_count( 'avif' );
}
return $total;
}
}

View File

@@ -0,0 +1,48 @@
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Инструменты для оптмизации изображений
*
* @version 1.0
*/
class WIO_OptimizationTools {
/**
* Конфигурация серверов и соответствующих классов
*/
private static $processors = [
'server_2' => [
'file' => '/includes/classes/processors/class-rio-server-robin.php',
'class' => 'WIO_Image_Processor_Robin',
],
'server_5' => [
'file' => '/includes/classes/processors/class-rio-server-premium.php',
'class' => 'WIO_Image_Processor_Premium',
],
];
/**
* Возвращает объект, отвечающий за оптимизацию изображений через API сторонних сервисов
*
* @param string|null $name
* @return WIO_Image_Processor_Abstract
*/
public static function getImageProcessor( $name = null ) {
// Auto-detect processor based on license status if not explicitly specified
if ( null === $name ) {
$server = wrio_is_license_activate() ? 'server_5' : 'server_2';
} else {
$server = $name;
}
$processor = self::$processors[ $server ] ?? self::$processors['server_2'];
require_once WRIO_PLUGIN_DIR . $processor['file'];
return new $processor['class']();
}
}

View File

@@ -0,0 +1,92 @@
<?php
/**
* Class that handles templates.
*
* @version 1.0
*/
class WRIO_Views {
/**
* The single instance of the class.
*
* @since 1.3.0
* @access protected
* @var array
*/
protected static $_instance = [];
/**
* @since 1.3.0
* @var string
*/
protected $plugin_dir;
/**
* WRIO_Views constructor.
*
* @param string $plugin_dir
*/
public function __construct( $plugin_dir ) {
$this->plugin_dir = $plugin_dir;
}
/**
* @since 1.3.6 - add instace id
* @since 1.3.0
*
* @param string $plugin_dir
*
* @return object|\WRIO_Views object Main instance.
*/
public static function get_instance( $plugin_dir ) {
$instance_id = md5( $plugin_dir );
if ( ! isset( self::$_instance[ $instance_id ] ) ) {
self::$_instance[ $instance_id ] = new self( $plugin_dir );
}
return self::$_instance[ $instance_id ];
}
/**
* Get a template contents.
*
* @since 1.3.0
*
* @param string $template The template name.
* @param mixed $data Some data to pass to the template.
* @param WRIO_Page $page
*
* @return bool|string The page contents. False if the template doesn't exist.
*/
public function get_template( $template, $data = [], WRIO_Page $page = null ) {
$template = str_replace( '_', '-', $template );
$path = $this->plugin_dir . '/views/' . $template . '.php';
if ( ! file_exists( $path ) ) {
return false;
}
ob_start();
include $path;
$contents = ob_get_clean();
return trim( (string) $contents );
}
/**
* Print a template.
*
* @access public
*
* @since 1.3.0
*
* @param string $template The template name.
* @param mixed $data Some data to pass to the template.
* @param WRIO_Page $page
*/
public function print_template( $template, $data = [], WRIO_Page $page = null ) {
echo $this->get_template( $template, $data, $page );
}
}

View File

@@ -0,0 +1,457 @@
<?php
/**
* License data class
*
* Parses and exposes license data from both Freemius (wbcr_io_license) and
* ThemeIsle SDK ({namespace}_license_data) storage formats.
*
* @package Robin_Image_Optimizer
* @subpackage Classes
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class WRIO_License
*
* Handles license data parsing and validation for both Freemius and ThemeIsle SDK licenses.
*/
class WRIO_License {
/**
* License source: Freemius
*/
const SOURCE_FREEMIUS = 'freemius';
/**
* License source: ThemeIsle SDK
*/
const SOURCE_SDK = 'sdk';
/**
* Raw license data from database
*
* @var array<string, mixed>
*/
private $data;
/**
* License-specific data (normalized)
*
* @var array<string, mixed>
*/
private $license_data;
/**
* License source (freemius or sdk)
*
* @var string|null
*/
private $source = null;
/**
* Constructor
*
* Loads license data from the database, detecting the source automatically.
*/
public function __construct() {
$this->detect_and_load_license();
}
/**
* Detect license source and load data
*
* Checks SDK option first, then falls back to Freemius storage.
*
* @return void
*/
private function detect_and_load_license() {
// Get SDK namespace using existing helper
$namespace = WRIO_Plugin::get_sdk_namespace();
$sdk_option = $namespace . '_license_data';
// Check SDK option first
// SDK stores data as stdClass object, convert to array for consistent access
$sdk_data = get_option( $sdk_option, null );
if ( ! empty( $sdk_data ) ) {
// Force convert to array (handles both object and array input)
$sdk_data = (array) $sdk_data;
if ( isset( $sdk_data['license'] ) && 'valid' === $sdk_data['license'] ) {
$this->source = self::SOURCE_SDK;
$this->data = $sdk_data;
$this->license_data = $this->normalize_sdk_data( $sdk_data );
return;
}
}
// Fallback to Freemius storage
$freemius_data = get_option( 'wbcr_io_license', [] );
if ( ! empty( $freemius_data['license']['secret_key'] ) ) {
$this->source = self::SOURCE_FREEMIUS;
$this->data = $freemius_data;
$this->license_data = $freemius_data['license'] ?? [];
return;
}
// No license found
$this->source = null;
$this->data = [];
$this->license_data = [];
}
/**
* Normalize SDK license data to match internal format
*
* Translates SDK field names to the Freemius-style format used internally.
*
* @param array<string, mixed> $data The SDK license data (already converted to array).
* @return array<string, mixed> Normalized license data.
*/
private function normalize_sdk_data( array $data ) {
return [
'secret_key' => $data['key'] ?? null,
'expiration' => $data['expires'] ?? null,
'plan_title' => 'Premium',
'plan_id' => $data['price_id'] ?? 0,
'activated' => 1,
'is_cancelled' => in_array( $data['is_expired'] ?? '', [ 'yes', true, 1, '1' ], true ),
'is_block_features' => false,
'billing_cycle' => null, // SDK doesn't track subscription status
'download_id' => $data['download_id'] ?? null,
];
}
/**
* Get the license source
*
* @return string|null 'freemius', 'sdk', or null if no license.
*/
public function get_source() {
return $this->source;
}
/**
* Get masked license key showing first 8 and last 4 characters
*
* Example: sk_abc12****wxyz
*
* @return string
*/
public function get_masked_key() {
$key = $this->get_key();
if ( empty( $key ) ) {
return '';
}
$length = strlen( $key );
if ( $length <= 12 ) {
return substr( $key, 0, 8 ) . '****';
}
return substr( $key, 0, 8 ) . '****' . substr( $key, -4 );
}
/**
* Get unified expiration display string
*
* Returns "Lifetime", "Renews on X" (for subscriptions), or "Expires on X".
*
* @return string
*/
public function get_expiration_display() {
if ( $this->is_lifetime() ) {
return __( 'Lifetime', 'robin-image-optimizer' );
}
$expiration = $this->license_data['expiration'] ?? null;
if ( empty( $expiration ) ) {
return '';
}
$date = date_i18n( get_option( 'date_format' ), strtotime( $expiration ) );
$billing_cycle = $this->get_billing_cycle();
// Only Freemius has billing_cycle for subscriptions
if ( $billing_cycle ) {
/* translators: %s is the renewal date */
return sprintf( __( 'Renews on %s', 'robin-image-optimizer' ), $date );
}
/* translators: %s is the expiration date */
return sprintf( __( 'Expires on %s', 'robin-image-optimizer' ), $date );
}
/**
* Get CSS status class for license display
*
* @return string 'status-valid', 'status-warning', or 'status-expired'
*/
public function get_status_class() {
if ( $this->is_expired() ) {
return 'status-expired';
}
// Warning if expiring within 30 days
$days = $this->get_expiration_time( 'days' );
if ( 999 !== $days && 30 >= $days ) {
return 'status-warning';
}
return 'status-valid';
}
/**
* Check if license data exists
*
* @return bool
*/
public function has_license() {
return ! empty( $this->license_data ) && ! empty( $this->get_key() );
}
/**
* Get license key (secret_key)
*
* @return string|null
*/
public function get_key() {
return isset( $this->license_data['secret_key'] ) ? $this->license_data['secret_key'] : null;
}
/**
* Get hidden license key with masked middle portion
*
* Example: sk_abc***xyz
*
* @return string
*/
public function get_hidden_key() {
$key = $this->get_key();
if ( empty( $key ) ) {
return '';
}
$length = strlen( $key );
if ( $length <= 12 ) {
return substr( $key, 0, 4 ) . '******';
}
return substr_replace( $key, '******', 15, 6 );
}
/**
* Get expiration time in various formats.
*
* @param string $format Return format: 'time' (raw), 'days' (remaining days), 'date' (Y-m-d).
* @return mixed
*/
public function get_expiration_time( $format = 'time' ) {
$expiration = isset( $this->license_data['expiration'] ) ? $this->license_data['expiration'] : null;
if ( 'days' === $format ) {
if ( $this->is_lifetime() ) {
return 999;
}
if ( empty( $expiration ) ) {
return 0;
}
$remaining = strtotime( $expiration ) - time();
return max( 0, floor( $remaining / 86400 ) );
}
if ( 'date' === $format ) {
if ( empty( $expiration ) ) {
return '';
}
return gmdate( 'Y-m-d', strtotime( $expiration ) );
}
return $expiration;
}
/**
* Get sites quota (number of allowed sites)
*
* @return int|null Null means unlimited
*/
public function get_sites_quota() {
return isset( $this->license_data['quota'] ) ? $this->license_data['quota'] : null;
}
/**
* Get count of currently active sites
*
* @return int
*/
public function get_count_active_sites() {
return isset( $this->license_data['activated'] ) ? (int) $this->license_data['activated'] : 0;
}
/**
* Check if this is a lifetime license (no expiration)
*
* @return bool
*/
public function is_lifetime() {
return empty( $this->license_data['expiration'] );
}
/**
* Check if license is valid (not expired)
*
* @return bool
*/
public function is_valid() {
return ! $this->is_expired();
}
/**
* Check if license has expired
*
* @return bool
*/
public function is_expired() {
if ( $this->is_lifetime() ) {
return false;
}
$expiration = $this->license_data['expiration'];
if ( empty( $expiration ) ) {
return true;
}
return strtotime( $expiration ) < time();
}
/**
* Check if license is cancelled
*
* @return bool
*/
public function is_cancelled() {
return ! empty( $this->license_data['is_cancelled'] );
}
/**
* Check if license is active (not cancelled)
*
* @return bool
*/
public function is_active() {
return ! $this->is_cancelled();
}
/**
* Check if features are enabled
*
* Features may be blocked after expiration depending on license settings.
*
* @return bool
*/
public function is_features_enabled() {
if ( ! $this->is_active() ) {
return false;
}
$is_block_features = isset( $this->license_data['is_block_features'] ) ? $this->license_data['is_block_features'] : true;
return ! $is_block_features || ! $this->is_expired();
}
/**
* Get plan title
*
* @return string|null
*/
public function get_plan() {
return isset( $this->license_data['plan_title'] ) ? $this->license_data['plan_title'] : null;
}
/**
* Get plan ID
*
* @return int|null
*/
public function get_plan_id() {
return isset( $this->license_data['plan_id'] ) ? (int) $this->license_data['plan_id'] : null;
}
/**
* Get billing cycle
*
* @return int|null 1 = monthly, 12 = yearly, null = lifetime/one-time
*/
public function get_billing_cycle() {
return isset( $this->license_data['billing_cycle'] ) ? $this->license_data['billing_cycle'] : null;
}
/**
* Check if quota is unlimited
*
* @return bool
*/
public function is_unlimited() {
return is_null( $this->get_sites_quota() );
}
/**
* Check if this is a single-site license
*
* @return bool
*/
public function is_single_site() {
$quota = $this->get_sites_quota();
return is_numeric( $quota ) && 1 === $quota;
}
/**
* Get license ID
*
* @return int|null
*/
public function get_id() {
return isset( $this->license_data['id'] ) ? (int) $this->license_data['id'] : null;
}
/**
* Get user ID associated with license
*
* @return int|null
*/
public function get_user_id() {
return isset( $this->license_data['user_id'] ) ? (int) $this->license_data['user_id'] : null;
}
/**
* Get raw license data array
*
* @return array<string, mixed>
*/
public function to_array() {
return $this->license_data;
}
/**
* Get site data from license
*
* @return array<string, mixed>
*/
public function get_site_data() {
return isset( $this->data['site'] ) ? $this->data['site'] : [];
}
/**
* Get user data from license
*
* @return array<string, mixed>
*/
public function get_user_data() {
return isset( $this->data['user'] ) ? $this->data['user'] : [];
}
}

View File

@@ -0,0 +1,558 @@
<?php
/**
* Premium Provider class
*
* Manages premium state via composition with WRIO_License.
* This is a lightweight replacement for the Freemius Premium Provider.
*
* @package Robin_Image_Optimizer
* @subpackage Classes
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class WRIO_Premium_Provider
*
* Handles premium license state and operations.
*/
class WRIO_Premium_Provider {
/**
* License instance
*
* @var WRIO_License
*/
private $license;
/**
* Whether a license is activated
*
* @var bool
*/
private $is_activated = false;
/**
* Constructor
*
* Initializes the license instance.
*/
public function __construct() {
$this->license = new WRIO_License();
$this->is_activated = $this->license->has_license();
}
/**
* Check if a license key is activated (exists)
*
* @return bool
*/
public function is_activate() {
return $this->is_activated;
}
/**
* Check if the license is active (activated AND valid/not expired)
*
* @return bool
*/
public function is_active() {
if ( ! $this->is_activated ) {
return false;
}
return $this->license->is_valid();
}
/**
* Get the plan name/title
*
* @return string|null
*/
public function get_plan() {
if ( ! $this->is_activated ) {
return null;
}
return $this->license->get_plan();
}
/**
* Get the plan ID
*
* @return int|null
*/
public function get_plan_id() {
if ( ! $this->is_activated ) {
return null;
}
return $this->license->get_plan_id();
}
/**
* Get billing cycle
*
* @return int|null 1 = monthly, 12 = yearly, null = lifetime
*/
public function get_billing_cycle() {
if ( ! $this->is_activated ) {
return null;
}
return $this->license->get_billing_cycle();
}
/**
* Get the license instance
*
* @return WRIO_License
*/
public function get_license() {
return $this->license;
}
/**
* Check if there is an active paid subscription (auto-renewal)
*
* @return bool
*/
public function has_paid_subscription() {
if ( ! $this->is_activated ) {
return false;
}
return ! empty( $this->license->get_billing_cycle() );
}
/**
* Activate a license key
*
* Routes to appropriate activation method based on key prefix:
* - sk_ prefix: Freemius license (stored in wbcr_io_license)
* - No prefix: ThemeIsle SDK license (processed via SDK filter)
*
* @param string $key The license key to activate.
* @return bool True on success.
* @throws Exception If activation fails.
*/
public function activate( $key ) {
$key = trim( $key );
if ( empty( $key ) ) {
throw new Exception( esc_html__( 'License key is empty.', 'robin-image-optimizer' ) );
}
// Detect license type by prefix
$is_freemius = strpos( $key, 'sk_' ) === 0;
if ( $is_freemius ) {
return $this->activate_freemius( $key );
}
return $this->activate_sdk( $key );
}
/**
* Activate a Freemius license key via API
*
* Makes a direct API call to Freemius activation endpoint.
*
* @param string $key The Freemius license key (sk_ prefixed).
* @return bool True on success.
* @throws Exception If activation fails.
*/
private function activate_freemius( $key ) {
$plugin_id = $this->get_setting( 'plugin_id' );
// Generate unique site identifier (32 chars)
$uid = $this->get_or_create_site_uid();
// Build API endpoint
$api_url = sprintf(
'https://api.freemius.com/v1/plugins/%s/activate.json',
$plugin_id
);
// Prepare request body
$body = [
'license_key' => $key,
'uid' => $uid,
'url' => get_home_url(),
'title' => get_bloginfo( 'name' ),
'version' => WRIO_Plugin::app()->getPluginVersion(),
];
// Add user info for new license activations
$current_user = wp_get_current_user();
if ( $current_user->ID ) {
$body['user_email'] = $current_user->user_email;
$body['first_name'] = $current_user->first_name ? $current_user->first_name : $current_user->display_name;
$body['last_name'] = $current_user->last_name ? $current_user->last_name : '';
} else {
$body['user_email'] = get_option( 'admin_email' );
}
/**
* Filter request body before API call
*
* @param array<string, mixed> $body The request body.
* @param string $key The license key.
*/
$body = apply_filters( 'wrio_freemius_activate_request', $body, $key );
$body_json = wp_json_encode( $body );
if ( false === $body_json ) {
throw new Exception( esc_html__( 'Failed to encode request body.', 'robin-image-optimizer' ) );
}
// Make API request
$response = wp_remote_post(
$api_url,
[
'timeout' => 30,
'headers' => [
'Content-Type' => 'application/json',
],
'body' => $body_json,
'sslverify' => true,
]
);
if ( is_wp_error( $response ) ) {
throw new Exception(
esc_html(
sprintf(
/* translators: %s: error message */
__( 'API request failed: %s', 'robin-image-optimizer' ),
$response->get_error_message()
)
)
);
}
$response_code = wp_remote_retrieve_response_code( $response );
$response_body = wp_remote_retrieve_body( $response );
$data = json_decode( $response_body, true );
// Check for API errors
if ( 200 !== $response_code || isset( $data['error'] ) ) {
$error_message = isset( $data['error']['message'] )
? $data['error']['message']
: __( 'License activation failed.', 'robin-image-optimizer' );
throw new Exception( esc_html( $error_message ) );
}
// Validate response has required fields
if ( empty( $data['install_id'] ) ) {
throw new Exception( esc_html__( 'Invalid API response: missing install_id.', 'robin-image-optimizer' ) );
}
// Build license data from API response
$license_data = $this->build_license_data_from_response( $data, $key, $uid );
/**
* Filter license data before saving
*
* @param array $license_data The license data to save.
* @param string $key The license key being activated.
* @param array $data The raw API response.
*/
$license_data = apply_filters( 'wrio_license_activate_data', $license_data, $key, $data );
update_option( 'wbcr_io_license', $license_data );
// Reinitialize license
$this->license = new WRIO_License();
$this->is_activated = true;
/**
* Action fired after license is activated
*
* @param string $key The activated license key.
* @param array $license_data The stored license data.
* @param array $data The raw API response.
*/
do_action( 'wrio_license_activated', $key, $license_data, $data );
return true;
}
/**
* Get or create a unique site identifier for Freemius
*
* @return string 32-character unique identifier.
*/
private function get_or_create_site_uid() {
$uid = get_option( 'wrio_freemius_uid' );
if ( empty( $uid ) ) {
// Generate a 32-char unique identifier
$uid = wp_generate_uuid4();
$uid = str_replace( '-', '', $uid );
update_option( 'wrio_freemius_uid', $uid );
}
return $uid;
}
/**
* Build license data array from Freemius API response
*
* @param array<string, mixed> $data The API response data.
* @param string $key The license key.
* @param string $uid The site UID.
* @return array<string, mixed> License data for storage.
*/
private function build_license_data_from_response( $data, $key, $uid ) {
$install = isset( $data['install'] ) ? $data['install'] : $data;
$user = isset( $data['user'] ) ? $data['user'] : [];
$license = isset( $data['license'] ) ? $data['license'] : [];
return [
'license' => [
'id' => isset( $license['id'] ) ? (int) $license['id'] : 0,
'secret_key' => $key,
'plan_id' => isset( $license['plan_id'] ) ? (int) $license['plan_id'] : 0,
'plan_title' => isset( $license['plan_title'] ) ? $license['plan_title'] : 'Premium',
'expiration' => isset( $license['expiration'] ) ? $license['expiration'] : null,
'quota' => isset( $license['quota'] ) ? (int) $license['quota'] : null,
'activated' => 1,
'activated_local' => 1,
'is_cancelled' => isset( $license['is_cancelled'] ) ? (bool) $license['is_cancelled'] : false,
'is_block_features' => isset( $license['is_block_features'] ) ? (bool) $license['is_block_features'] : false,
'billing_cycle' => isset( $license['billing_cycle'] ) ? (int) $license['billing_cycle'] : null,
'created' => current_time( 'mysql' ),
'updated' => current_time( 'mysql' ),
],
'site' => [
'id' => isset( $install['id'] ) ? (int) $install['id'] : ( isset( $data['install_id'] ) ? (int) $data['install_id'] : 0 ),
'uid' => $uid,
'url' => get_home_url(),
'public_key' => isset( $install['public_key'] ) ? $install['public_key'] : '',
'secret_key' => isset( $install['secret_key'] ) ? $install['secret_key'] : '',
'install_api_token' => isset( $data['install_api_token'] ) ? $data['install_api_token'] : '',
],
'user' => [
'id' => isset( $user['id'] ) ? (int) $user['id'] : 0,
'email' => isset( $user['email'] ) ? $user['email'] : get_option( 'admin_email' ),
'public_key' => isset( $user['public_key'] ) ? $user['public_key'] : '',
],
];
}
/**
* Activate a ThemeIsle SDK license key
*
* Uses the SDK's do_license_process() via the registered filter.
* The SDK handles all API calls and stores license data automatically.
*
* @param string $key The SDK license key (unprefixed).
* @return bool True on success.
* @throws Exception If activation fails.
*/
private function activate_sdk( $key ) {
$namespace = WRIO_Plugin::get_sdk_namespace();
// Use SDK filter - it handles API calls via do_license_process()
// SDK returns: true on success, WP_Error on failure
$response = apply_filters(
'themeisle_sdk_license_process_' . $namespace,
$key,
'activate'
);
if ( is_wp_error( $response ) ) {
throw new Exception( esc_html( $response->get_error_message() ) );
}
if ( true !== $response ) {
throw new Exception( esc_html__( 'License activation failed.', 'robin-image-optimizer' ) );
}
// Reinitialize license (SDK already stored the data)
$this->license = new WRIO_License();
$this->is_activated = true;
/**
* Action fired after license is activated
*
* @param string $key The activated license key.
* @param array $data Additional data.
*/
do_action( 'wrio_license_activated', $key, [] );
return true;
}
/**
* Deactivate the current license
*
* Clears the appropriate option based on license source.
*
* @return bool True on success.
*/
public function deactivate() {
if ( ! $this->is_activated ) {
return true;
}
$old_key = $this->license->get_key();
$source = $this->license->get_source();
/**
* Action fired before license is deactivated
*
* @param string|null $key The license key being deactivated.
*/
do_action( 'wrio_license_before_deactivate', $old_key );
// Clear the appropriate option based on source
if ( WRIO_License::SOURCE_SDK === $source ) {
$namespace = WRIO_Plugin::get_sdk_namespace();
// Use SDK filter - it handles API calls and deletes options
apply_filters(
'themeisle_sdk_license_process_' . $namespace,
$old_key,
'deactivate'
);
// Ensure cleanup (SDK may have already deleted these)
delete_option( $namespace . '_license_data' );
delete_transient( $namespace . '_license_data' );
} else {
delete_option( 'wbcr_io_license' );
}
// Reinitialize license
$this->license = new WRIO_License();
$this->is_activated = false;
/**
* Action fired after license is deactivated
*
* @param string|null $key The deactivated license key.
*/
do_action( 'wrio_license_deactivated', $old_key );
return true;
}
/**
* Cancel paid subscription
*
* In a full implementation, this would call the payment provider API.
* For now, it just removes the billing cycle from local data.
*
* @return bool True on success.
*/
public function cancel_paid_subscription() {
if ( ! $this->is_activated || ! $this->has_paid_subscription() ) {
return false;
}
$license_data = get_option( 'wbcr_io_license', [] );
if ( isset( $license_data['license']['billing_cycle'] ) ) {
$license_data['license']['billing_cycle'] = null;
update_option( 'wbcr_io_license', $license_data );
}
// Refresh license
$this->license = new WRIO_License();
/**
* Action fired after subscription is cancelled
*
* @param WRIO_License $license The license instance.
*/
do_action( 'wrio_subscription_cancelled', $this->license );
return true;
}
/**
* Get a setting value (for compatibility with Freemius provider)
*
* @param string $key Setting key.
* @param mixed $default Default value.
* @return mixed
*/
/**
* Get plugin setting
*
* @param string|null $key The setting key.
* @param mixed $default_value The default value if key not found.
* @return mixed The setting value or default.
*/
public function get_setting( $key, $default_value = null ) {
$settings = [
'plugin_id' => '3464',
'public_key' => 'pk_cafff5a51bd5fcf09c6bde806956d',
'slug' => 'robin-image-optimizer',
];
return isset( $settings[ $key ] ) ? $settings[ $key ] : $default_value;
}
/**
* Get upgrade price (stub for Factory framework compatibility)
*
* @return int
*/
public function get_price() {
return 0;
}
/**
* Check if premium package is installed (stub for Factory framework compatibility)
*
* @return bool
*/
public function is_install_package() {
return false;
}
/**
* Get package data (stub for Factory framework compatibility)
*
* @return array<string, mixed>|null
*/
public function get_package_data() {
return null;
}
/**
* Get package download URL (stub for Factory framework compatibility)
*
* @return string
*/
public function get_package_download_url() {
return '';
}
/**
* Get downloadable package info (stub for Factory framework compatibility)
*
* @return array<string, mixed>|null
*/
public function get_downloadable_package_info() {
return null;
}
/**
* Sync license (stub for Factory framework compatibility)
*
* No-op since sync functionality was removed.
*
* @return bool
*/
public function sync() {
return true;
}
}

View File

@@ -0,0 +1,225 @@
<?php
/**
* Support URLs class
*
* Manages plugin support, pricing, and documentation URLs.
* This is a lightweight replacement for the Factory Support entity.
*
* @package Robin_Image_Optimizer
* @subpackage Classes
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class WRIO_Support
*
* Handles support-related URLs for the plugin.
*/
class WRIO_Support {
/**
* Plugin name for tracking
*
* @var string
*/
protected $plugin_name;
/**
* Base site URL
*
* @var string
*/
protected $site_url;
/**
* Features page slug
*
* @var string
*/
protected $features_page_slug = 'premium-features';
/**
* Pricing page slug
*
* @var string
*/
protected $pricing_page_slug = 'pricing';
/**
* Support page slug
*
* @var string
*/
protected $support_page_slug = 'support';
/**
* Documentation page slug
*
* @var string
*/
protected $docs_page_slug = 'docs';
/**
* Constructor
*
* @param array<string, mixed> $data Configuration data including 'url' and optional 'pages_map'.
*/
public function __construct( array $data = [] ) {
$this->site_url = isset( $data['url'] ) ? $data['url'] : 'https://developer.flavflavor.dev';
$this->plugin_name = isset( $data['plugin_name'] ) ? $data['plugin_name'] : 'robin-image-optimizer';
// Allow custom page slug mapping
if ( isset( $data['pages_map'] ) && is_array( $data['pages_map'] ) ) {
foreach ( $data['pages_map'] as $key => $slug ) {
$attr = $key . '_page_slug';
if ( property_exists( $this, $attr ) ) {
$this->{$attr} = $slug;
}
}
}
}
/**
* Get site URL
*
* @param bool $track Whether to include tracking parameters.
* @param string|null $utm_content UTM content parameter.
* @return string
*/
public function get_site_url( $track = false, $utm_content = null ) {
$url = $this->site_url;
/**
* Filter the base site URL
*
* @param string $url The base URL.
*/
$url = apply_filters( 'wrio_support_site_url', $url );
if ( $track ) {
return $this->get_tracking_page_url( '', $utm_content );
}
return $url;
}
/**
* Get features page URL
*
* @param bool $track Whether to include tracking parameters.
* @param string|null $utm_content UTM content parameter.
* @return string
*/
public function get_features_url( $track = false, $utm_content = null ) {
if ( $track ) {
return $this->get_tracking_page_url( $this->features_page_slug, $utm_content );
}
$url = trailingslashit( $this->site_url ) . $this->features_page_slug;
/**
* Filter the features page URL
*
* @param string $url The features URL.
*/
return apply_filters( 'wrio_support_features_url', $url );
}
/**
* Get pricing page URL
*
* @param bool $track Whether to include tracking parameters.
* @param string $utm_campaign UTM content parameter.
* @return string
*/
public function get_pricing_url( $track = false, $utm_campaign = '' ) {
$url = rtrim( $this->site_url, '/' ) . '/upgrade';
$url = tsdk_translate_link( tsdk_utmify( $url, $utm_campaign ) );
/**
* Filter the pricing page URL
*
* @param string $url The pricing URL.
*/
return apply_filters( 'wrio_support_pricing_url', esc_url( $url ) );
}
/**
* Get contacts/support page URL
*
* @param bool $track Whether to include tracking parameters.
* @param string|null $utm_content UTM content parameter.
* @return string
*/
public function get_contacts_url( $track = false, $utm_content = null ) {
if ( $track ) {
return $this->get_tracking_page_url( $this->support_page_slug, $utm_content );
}
$url = trailingslashit( $this->site_url ) . $this->support_page_slug;
/**
* Filter the contacts/support page URL
*
* @param string $url The contacts URL.
*/
return apply_filters( 'wrio_support_contacts_url', $url );
}
/**
* Get documentation page URL
*
* @param bool $track Whether to include tracking parameters.
* @param string|null $utm_content UTM content parameter.
* @return string
*/
public function get_docs_url( $track = false, $utm_content = null ) {
if ( $track ) {
return $this->get_tracking_page_url( $this->docs_page_slug, $utm_content );
}
$url = trailingslashit( $this->site_url ) . $this->docs_page_slug;
/**
* Filter the documentation page URL
*
* @param string $url The docs URL.
*/
return apply_filters( 'wrio_support_docs_url', $url );
}
/**
* Build URL with UTM tracking parameters
*
* @param string|null $page Page slug to append.
* @param string|null $utm_content UTM content parameter.
* @param string $utm_source UTM source parameter.
* @return string
*/
public function get_tracking_page_url( $page = null, $utm_content = null, $utm_source = 'wordpress.org' ) {
$args = [
'utm_source' => $utm_source,
];
if ( ! empty( $this->plugin_name ) ) {
$args['utm_campaign'] = $this->plugin_name;
}
if ( ! empty( $utm_content ) ) {
$args['utm_content'] = $utm_content;
}
$base_url = $this->site_url;
if ( ! empty( $page ) ) {
$base_url = trailingslashit( $base_url ) . $page . '/';
}
$raw_url = add_query_arg( $args, $base_url );
return esc_url( $raw_url );
}
}

View File

@@ -0,0 +1,2 @@
<?php
// silence is golden

View File

@@ -0,0 +1,88 @@
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class RIO_Attachment_Extra_Data is a DTO model for `attachment` post type used for `extra_data`
* property in RIO_Process_Queue.
*
* @see RIO_Process_Queue::$extra_data for further information
*/
class RIO_Attachment_Extra_Data extends RIO_Base_Extra_Data {
protected $error = null;
protected $error_msg = null;
protected $thumbnails_count = null;
protected $original_main_size = null;
protected $main_optimized_data = null;
protected $thumbnails_optimized_data = null;
protected $webp_main_size = null;
protected $avif_main_size = null;
public function get_error() {
return $this->error;
}
public function set_error( $error ) {
$this->error = $error;
}
public function get_error_msg() {
return $this->error_msg;
}
public function set_error_msg( $error_msg ) {
$this->error_msg = $error_msg;
}
public function get_thumbnails_count() {
return $this->thumbnails_count;
}
public function set_thumbnails_count( $thumbnails_count ) {
$this->thumbnails_count = $thumbnails_count;
}
public function get_original_main_size() {
return $this->original_main_size;
}
public function set_original_main_size( $original_main_size ) {
$this->original_main_size = $original_main_size;
}
public function get_main_optimized_data() {
return (array) $this->main_optimized_data;
}
public function set_main_optimized_data( $main_optimized_data ) {
$this->main_optimized_data = $main_optimized_data;
}
public function get_thumbnails_optimized_data() {
return (array) $this->thumbnails_optimized_data;
}
public function set_thumbnails_optimized_data( $thumbnails_optimized_data ) {
$this->thumbnails_optimized_data = $thumbnails_optimized_data;
}
public function get_webp_main_size() {
return $this->webp_main_size;
}
public function set_webp_main_size( $webp_main_size ) {
$this->webp_main_size = $webp_main_size;
}
public function get_avif_main_size() {
return $this->avif_main_size;
}
public function set_avif_main_size( $avif_main_size ) {
$this->avif_main_size = $avif_main_size;
}
}

View File

@@ -0,0 +1,95 @@
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class WRIO_Base_Model used as a base class for any database related model.
*
* Usage example:
* ```php
* Custom extends RIO_Base_Model {
* public $prop;
* }
*
* $model = new Custom(array('prop' => 123)); // or ['prop' => 123]
* $model->save();
* ```
*/
class RIO_Base_Active_Record extends RIO_Base_Object {
/**
* Get table name.
*
* @return string|null
*/
public static function table_name() {
return null;
}
/**
* @todo override with activerecord impl
*
* @param string $name
* @param mixed $value
*
* @throws Exception
*/
public function __set( $name, $value ) {
if ( property_exists( $this, $name ) ) {
$this->$name = $value;
}
}
/**
* Check whether table has SQL schema or not.
*
* @return bool
*/
public static function has_table_schema() {
$schema = static::get_table_schema();
return ! empty( $schema );
}
/**
* Check whether table has indexes defined.
*
* Notice: method would check whether model has schema defined first and then indexes.
*
* @return bool
*/
public static function has_table_indexes() {
if ( ! static::has_table_schema() ) {
return false;
}
$indexes = static::get_table_indexes();
return ! empty( $indexes );
}
/**
* Get table SQL schema structure.
*
* @return string|null String when model has database table, null otherwise.
*/
public static function get_table_schema() {
return null;
}
/**
* Get list of indexes.
*
* None associative list of
*
* @return array Empty array returned in case when no indexes exist on table.
*/
public static function get_table_indexes() {
return [];
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* Class RIO_Base_Extra_Data is a base DTO model for `extra_data` property in RIO_Process_Queue.
*
* @see RIO_Process_Queue::$extra_data for further information
*/
class RIO_Base_Extra_Data extends RIO_Base_Object {
/**
* @var string Instance of current class.
*/
protected $class;
/**
* Magic override of to string method to convert
*
* @return bool|false|mixed|string
*/
public function __toString() {
$props = get_object_vars( $this );
// если свойство не установлено, то не сохраняем его
foreach ( $props as $prop_name => $prop_value ) {
if ( is_null( $prop_value ) ) {
unset( $props[ $prop_name ] );
}
}
$props['class'] = get_called_class();
return wp_json_encode( $props );
}
/**
* Get class
*
* @return string
*/
public function get_class() {
return $this->class;
}
/**
* Set class
*
* @param string $class_name Имя класса
*
* @return void
*/
public function set_class( $class_name ) {
$this->class = $class_name;
}
}

View File

@@ -0,0 +1,24 @@
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class RIO_Base_Helper {
/**
* Configure passed object.
*
* @param object $object Object class to configure.
* @param array $config Key => value list of props to be set on object.
*
* @return mixed
*/
public static function configure( $object, $config ) {
foreach ( $config as $name => $value ) {
$object->$name = $value;
}
return $object;
}
}

View File

@@ -0,0 +1,101 @@
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class RIO_Base_Object is a base class that implements property feature.
*
* It takes advantage of __get() and __set(), see further implementation below in the class.
*
* When property of the class is being used and it has a getter, it will be used instead of directly accessing it.
*
* The same logic applies for setter.
*
* Example:
*
* ```php
* // equivalent to $label = $object->getLabel();
* $label = $object->label;
* // equivalent to $object->setLabel('abc');
* $object->label = 'abc';
* ```
*/
class RIO_Base_Object {
/**
* RIO_Base_Object constructor.
*
* @param array $config name-value pairs that will be used to initialize the object properties.
*/
public function __construct( $config = [] ) {
if ( ! empty( $config ) ) {
$this->configure( $config );
}
$this->init();
}
/**
* Initiate model.
*/
public function init() {
}
/**
* Configure object.
*
* @param array $config name-value pairs that will be used to initialize the object properties.
*/
public function configure( $config ) {
RIO_Base_Helper::configure( $this, $config );
}
/**
* Returns the value of an object property.
*
* Do not call this method directly as it is a PHP magic method that
* will be implicitly called when executing `$value = $object->property;`.
*
* @param string $name the property name
*
* @return mixed the property value
* @throws Exception if the property is not defined
* @see __set()
*/
public function __get( $name ) {
$getter = 'get_' . $name;
if ( method_exists( $this, $getter ) ) {
return $this->$getter();
} elseif ( method_exists( $this, 'set' . $name ) ) {
throw new \Exception( 'Getting write-only property: ' . get_class( $this ) . '::' . $name );
}
throw new Exception( 'Getting unknown property: ' . get_class( $this ) . '::' . $name );
}
/**
* Sets value of an object property.
*
* Do not call this method directly as it is a PHP magic method that
* will be implicitly called when executing `$object->property = $value;`.
*
* @param string $name the property name or the event name
* @param mixed $value the property value
*
* @throws Exception if the property is not defined
* @see __get()
*/
public function __set( $name, $value ) {
$setter = 'set_' . $name;
if ( method_exists( $this, $setter ) ) {
$this->$setter( $value );
} elseif ( method_exists( $this, 'get' . $name ) ) {
throw new \Exception( 'Setting read-only property: ' . get_class( $this ) . '::' . $name );
} else {
throw new \Exception( 'Setting unknown property: ' . get_class( $this ) . '::' . $name );
}
}
}

View File

@@ -0,0 +1,19 @@
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class RIO_Smushit_Extra_Data is a DTO model for saving extra data from post attachments in `extra_data`.
*
* @see RIO_Process_Queue::$extra_data for further information
*/
class RIO_Smushit_Extra_Data extends RIO_Attachment_Extra_Data {
/**
* @var int Final size in bytes.
*/
protected $optimized_size;
}

View File

@@ -0,0 +1,168 @@
<?php
/**
* Class RIOP_WebP_Extra_Data.
*
* @property string $source_src
*/
class RIOP_WebP_Extra_Data extends RIO_Attachment_Extra_Data {
/**
* @var null|string E.g. attachment, nextgen, etc
*/
protected $convert_from = null;
/**
* @var null|string|int
*/
protected $converted_from_size = null;
/**
* @var string|null Image source src.
*/
protected $source_src = null;
/**
* @var string|null Image absolute path.
*/
protected $source_path = null;
/**
* @var string|null Converted WebP image src.
*/
protected $converted_src = null;
/**
* @var string|null Converted WebP absolute path.
*/
protected $converted_path = null;
/**
* @var int|null Post ID.
*/
protected $post_id = null;
/**
* @var int|null thumbnails count.
*/
protected $thumbnails_count = null;
/**
* @param string $source_src
*/
public function set_source_src( $source_src ) {
$this->source_src = trim( $source_src );
}
/**
* Get source property.
*
* @param bool $decoded Whether to decode src or not.
*
* @return string
*/
public function get_source_src( $decoded = true ) {
$src = $this->source_src;
if ( $decoded ) {
return urldecode( $src );
}
return $src;
}
/**
* @return null|string
*/
public function get_source_path() {
return $this->source_path;
}
/**
* @param null|string $source_path
*/
public function set_source_path( $source_path ) {
$this->source_path = $source_path;
}
/**
* @return null|string
*/
public function get_convert_from() {
return $this->convert_from;
}
/**
* @param null|string $convert_from
*/
public function set_convert_from( $convert_from ) {
$this->convert_from = $convert_from;
}
/**
* @return int|null|string
*/
public function get_converted_from_size() {
return $this->converted_from_size;
}
/**
* @param int|null|string $converted_from_size
*/
public function set_converted_from_size( $converted_from_size ) {
$this->converted_from_size = $converted_from_size;
}
/**
* @return string
*/
public function get_converted_path() {
return $this->converted_path;
}
/**
* @param string $converted_path
*/
public function set_converted_path( $converted_path ) {
$this->converted_path = $converted_path;
}
/**
* @return string
*/
public function get_converted_src() {
return $this->converted_src;
}
/**
* @param string $converted_src
*/
public function set_converted_src( $converted_src ) {
$this->converted_src = $converted_src;
}
/**
* @return int
*/
public function get_post_id() {
return $this->post_id;
}
/**
* @param int $post_id
*
* @return RIOP_WebP_Extra_Data
*/
public function set_post_id( $post_id ) {
$this->post_id = $post_id;
return $this;
}
public function get_original_main_size() {
return '';
}
public function get_thumbnails_count() {
return $this->thumbnails_count;
}
}

View File

@@ -0,0 +1,55 @@
<?php
use WBCR\Factory_Processing_759\WP_Background_Process;
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Класс для работы оптимизации в фоне
*
* @version 1.0
*/
class WRIO_Folder_Processing extends WRIO_Processing {
/**
* @return int Count of pushed queue
*/
public function push_items() {
$attachment_ids = [];
if ( $this->scope === 'custom-folders' ) {
$cf = WRIO_Custom_Folders::get_instance();
$attachment_ids = $cf->getUnoptimizedImages();
}
foreach ( $attachment_ids as $attachment_id ) {
$this->push_to_queue( $attachment_id );
}
return $this->count_queue();
}
/**
* Метод оптимизирует изображения при выполнении задачи
*
* @param int $image
*
* @return bool
*/
protected function task( $image ) {
if ( $image ) {
WRIO_Plugin::app()->logger->info( sprintf( 'Start optimize custom folder image: %s', $image ) );
if ( $this->scope === 'custom-folders' ) {
$cf = WRIO_Custom_Folders::get_instance();
$result = $cf->optimizeImage( $image );
}
WRIO_Plugin::app()->logger->info( sprintf( 'End optimize custom folder image: %s', $image ) );
}
return false;
}
}

View File

@@ -0,0 +1,94 @@
<?php
use WBCR\Factory_Processing_759\WP_Background_Process;
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Класс для работы AVIF конвертации в фоне
* Class for AVIF image format conversion background processing
*
* @version 1.6.0
* @since 1.6.0
*/
class WRIO_Media_Processing_Avif extends WRIO_Processing {
/**
* @var string
*/
protected $action = 'convert_process';
/**
* @var string Format type
*/
protected $format = 'avif';
/**
* Constructor
*
* @param string $scope Processing scope
*/
public function __construct( $scope ) {
parent::__construct( $scope );
}
/**
* Push items to queue
*
* @return int Number of items in queue
*/
public function push_items() {
$attachment_ids = [];
if ( $this->scope === 'media-library_avif' ) {
$media_library = WRIO_Media_Library::get_instance();
$attachment_ids = $media_library->getUnconvertedImages( 'avif' );
}
foreach ( $attachment_ids as $attachment_id ) {
$this->push_to_queue( $attachment_id );
}
return $this->count_queue();
}
/**
* Метод конвертирует изображения в AVIF при выполнении задачи
* Method converts images to AVIF when executing task
*
* @param int $image Attachment ID
*
* @return bool
*/
protected function task( $image ) {
if ( $image ) {
WRIO_Plugin::app()->logger->info( sprintf( 'Start convert attachment #%s to AVIF', $image ) );
if ( $this->scope === 'media-library_avif' ) {
$media_library = WRIO_Media_Library::get_instance();
$media_library->webpConvertAttachment( $image, 'avif' );
}
WRIO_Plugin::app()->logger->info( sprintf( 'End convert attachment #%s to AVIF', $image ) );
}
return false;
}
/**
* Fire after complete handle
* Вызывается после завершения обработки
*
* @return void
*/
protected function handle_after_complete() {
WRIO_Plugin::app()->updatePopulateOption( "{$this->scope}_process_running", false );
WRIO_Plugin::app()->logger->info(
sprintf( 'AVIF conversion background process completed for scope: %s', $this->scope )
);
}
}

View File

@@ -0,0 +1,96 @@
<?php
use WBCR\Factory_Processing_759\WP_Background_Process;
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Класс для работы оптимизации в фоне
*
* @version 1.0
*/
class WRIO_Media_Processing_Webp extends WRIO_Processing {
/**
* @var string
*/
protected $action = 'convert_process';
/**
* @var string Format type (webp or avif)
*/
protected $format = 'webp';
/**
* Constructor
*
* @param string $scope Processing scope
*/
public function __construct( $scope ) {
parent::__construct( $scope );
// Extract format from scope (e.g., 'media-library_webp' -> 'webp')
if ( $this->scope && strpos( $this->scope, '_' ) !== false ) {
$parts = explode( '_', $this->scope );
$extracted_format = end( $parts );
if ( in_array( $extracted_format, [ 'webp', 'avif' ], true ) ) {
$this->format = $extracted_format;
}
}
}
/**
* @return int Count of pushed queue
*/
public function push_items() {
$attachment_ids = [];
if ( strpos( $this->scope, 'media-library_' ) === 0 ) {
$media_library = WRIO_Media_Library::get_instance();
$attachment_ids = $media_library->getUnconvertedImages( $this->format );
}
foreach ( $attachment_ids as $attachment_id ) {
$this->push_to_queue( $attachment_id );
}
return $this->count_queue();
}
/**
* Метод оптимизирует изображения при выполнении задачи
*
* @param int $image
*
* @return bool
*/
protected function task( $image ) {
if ( $image ) {
WRIO_Plugin::app()->logger->info( sprintf( 'Start convert attachment #%s to %s', $image, $this->format ) );
if ( strpos( $this->scope, 'media-library_' ) === 0 ) {
$media_library = WRIO_Media_Library::get_instance();
$media_library->webpConvertAttachment( $image, $this->format );
}
WRIO_Plugin::app()->logger->info( sprintf( 'End convert attachment #%s to %s', $image, $this->format ) );
}
return false;
}
/**
* Fire after complete handle
*
* @return void
*/
protected function handle_after_complete() {
WRIO_Plugin::app()->updatePopulateOption( "{$this->scope}_process_running", false );
WRIO_Plugin::app()->logger->info(
sprintf( '%s conversion background process completed for scope: %s', strtoupper( $this->format ), $this->scope )
);
}
}

View File

@@ -0,0 +1,56 @@
<?php
use WBCR\Factory_Processing_759\WP_Background_Process;
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Класс для работы оптимизации в фоне
*
* @version 1.0
*/
class WRIO_Media_Processing extends WRIO_Processing {
/**
* @param array $attachment_ids
*
* @return int Count of pushed queue
*/
public function push_items( $attachment_ids = [] ) {
if ( empty( $attachment_ids ) && $this->scope === 'media-library' ) {
$media_library = WRIO_Media_Library::get_instance();
$attachment_ids = $media_library->getUnoptimizedImages();
}
foreach ( $attachment_ids as $attachment_id ) {
$this->push_to_queue( $attachment_id );
}
return $this->count_queue();
}
/**
* Метод оптимизирует изображения при выполнении задачи
*
* @param int $image
*
* @return bool
*/
protected function task( $image ) {
if ( $image ) {
WRIO_Plugin::app()->logger->info( sprintf( 'Start optimize attachment: %s', $image ) );
if ( $this->scope === 'media-library' ) {
$media_library = WRIO_Media_Library::get_instance();
$result = $media_library->optimizeAttachment( $image );
}
WRIO_Plugin::app()->logger->info( sprintf( 'End optimize attachment: %s', $image ) );
}
return false;
}
}

View File

@@ -0,0 +1,55 @@
<?php
use WBCR\Factory_Processing_759\WP_Background_Process;
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Класс для работы оптимизации в фоне
*
* @version 1.0
*/
class WRIO_Nextgen_Processing extends WRIO_Processing {
/**
* @return int Count of pushed queue
*/
public function push_items() {
$attachment_ids = [];
if ( $this->scope === 'nextgen' ) {
$nextgen_gallery = WRIO_Nextgen_Gallery::get_instance();
$attachment_ids = $nextgen_gallery->getUnoptimizedImages();
}
foreach ( $attachment_ids as $attachment_id ) {
$this->push_to_queue( $attachment_id );
}
return $this->count_queue();
}
/**
* Метод оптимизирует изображения при выполнении задачи
*
* @param int $image
*
* @return bool
*/
protected function task( $image ) {
if ( $image ) {
WRIO_Plugin::app()->logger->info( sprintf( 'Start optimize attachment: %s', $image ) );
if ( $this->scope === 'nextgen' ) {
$nextgen_gallery = WRIO_Nextgen_Gallery::get_instance();
$result = $nextgen_gallery->optimizeNextgenImage( $image );
}
WRIO_Plugin::app()->logger->info( sprintf( 'End optimize attachment: %s', $image ) );
}
return false;
}
}

View File

@@ -0,0 +1,67 @@
<?php
use WBCR\Factory_Processing_759\WP_Background_Process;
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Класс для работы оптимизации в фоне
*
* @version 1.0
*/
abstract class WRIO_Processing extends WP_Background_Process {
/**
* @var string
*/
protected $prefix = 'wrio';
/**
* @var string
*/
protected $action = 'optimize_process';
/**
* @var string
*/
public $scope;
/**
* Processing constructor.
*
* @param $scope
*/
public function __construct( $scope ) {
$this->scope = $scope;
$this->action = "{$this->action}_{$scope}";
parent::__construct();
}
abstract public function push_items();
/**
* Fire before start handle the tasks
*/
protected function handle_before() {
WRIO_Plugin::app()->logger->info( 'START auto optimize process.' );
}
/**
* Fire after end handle the tasks
*/
protected function handle_after() {
WRIO_Plugin::app()->logger->info( 'END auto optimize process.' );
}
/**
* Fire after complete handle
*
* @return void
*/
protected function handle_after_complete() {
WRIO_Plugin::app()->updatePopulateOption( 'process_running', false );
}
}

View File

@@ -0,0 +1,177 @@
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Базовый класс для обработки изображений через API сторонних сервисов.
*
* todo: add usage example
*
* @version 1.0
*/
abstract class WIO_Image_Processor_Abstract {
/**
* @var string Имя сервера
*/
protected $server_name;
/**
* Оптимизация изображения
*
* @param array $params {
* Параметры оптимизации изображения. Разные сервера могут принимать разные наборы параметров. Ниже список всех возможных.
*
* {type} string $image_url УРЛ изображения
* {type} string $image_path Путь к файлу изображения
* {type} string $quality Качество
* {type} string $save_exif Сохранять ли EXIF данные
* }
*
* @return array|WP_Error {
* Результаты оптимизации. Основные параметры. Другие параметры зависят от конкретной раелизации.
*
* {type} string $optimized_img_url УРЛ оптимизированного изображения на сервере оптимизации
* {type} int $src_size размер исходного изображения в байтах
* {type} int $optimized_size размер оптимизированного изображения в байтах
* {type} int $optimized_percent На сколько процентов уменьшилось изображение
* {type} bool $not_need_replace Изображение не надо заменять.
* {type} bool $not_need_download Изображение не надо скачивать.
* }
*/
abstract function process( $params );
/**
* Качество изображения
* Метод конвертирует качество из настроек плагина в формат сервиса оптимизации
*
* @param mixed $quality качество
*/
abstract function quality( $quality );
/**
* Проверка наличия ограничения на квоту
*
* @return bool Возвращает true, если существует ограничение на квоту, иначе false
*/
abstract public function has_quota_limit();
/**
* Возвращает URL API сервера
*
* @return string
*/
public function get_api_url() {
return wrio_get_server_url( $this->server_name );
}
/**
* Установка лимита квоты
*
* @param mixed $value Новое значение лимита квоты
*
* @return void
*/
public function set_quota_limit( $value ) {
WRIO_Plugin::app()->updatePopulateOption( $this->server_name . '_quota_limit', (int) $value );
}
/**
* Получает лимит квоты для текущего сервера.
*
* @return int Лимит квоты, установленный для сервера. Если лимит не задан, возвращается 0.
*/
public function get_quota_limit() {
return WRIO_Plugin::app()->getPopulateOption( $this->server_name . '_quota_limit', 0 );
}
/**
* HTTP запрос к API стороннего сервиса.
*
* @param string $type POST|GET
* @param string $url URL для запроса
* @param array|string|null $body Параметры запроса. По умолчанию: false.
* @param array $headers Дополнительные заголовки. По умолчанию: false.
*
* @return string|WP_Error
*/
protected function request( $type, $url, $body = null, array $headers = [] ) {
$args = [
'method' => $type,
'headers' => array_merge(
[
'User-Agent' => '',
],
$headers
),
'body' => $body,
'timeout' => 150, // it make take some time for large images and slow Internet connections
];
$error_message = sprintf( 'Failed to get content of URL: %s as wp_remote_request()', $url );
wp_raise_memory_limit( 'image' );
$response = wp_remote_request( $url, $args );
if ( is_wp_error( $response ) ) {
WRIO_Plugin::app()->logger->error( sprintf( '%s returned error (%s).', $error_message, $response->get_error_message() ) );
return $response;
}
$response_body = wp_remote_retrieve_body( $response );
$response_code = wp_remote_retrieve_response_code( $response );
if ( $response_code !== 200 ) {
WRIO_Plugin::app()->logger->error( sprintf( '%s responded Http error (%s).', $error_message, $response_code ) );
return new WP_Error( 'http_request_failed', sprintf( 'Server responded an HTTP error %s', $response_code ) );
}
if ( empty( $response_body ) ) {
WRIO_Plugin::app()->logger->error( sprintf( '%s responded an empty request body.', $error_message ) );
return new WP_Error( 'http_request_failed', 'Server responded an empty request body.' );
}
return $response_body;
}
/**
* Использует ли сервер отложенную оптимизацию
*
* @return bool
*/
public function isDeferred() {
return false;
}
/**
* Проверка отложенной оптимизации изображения
*
* @param array $optimized_data Параметры отложенной оптимизации. Набор параметров зависит от конкретной реализации
*
* @return bool|array
*/
public function checkDeferredOptimization( $optimized_data ) {
return false;
}
/**
* Проверка данных для отложенной оптимизации.
*
* Проверяет наличие необходимых параметров и соответствие серверу.
*
* @param array $optimized_data Параметры отложенной оптимизации. Набор параметров зависит от конкретной реализации
*
* @return bool
*/
public function validateDeferredData( $optimized_data ) {
return false;
}
}

View File

@@ -0,0 +1,216 @@
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Класс для оптимизации изображений через API сервиса Resmush.
*/
class WIO_Image_Processor_Premium extends WIO_Image_Processor_Abstract {
/**
* @var string
*/
protected $api_url;
/**
* @var string Имя сервера
*/
protected $server_name = 'server_5';
/**
* Инициализация
*
* @return void
*/
public function __construct() {
// Получаем ссылку на сервер 5
$this->api_url = wrio_get_server_url( $this->server_name );
}
public function howareyou() {
return false;
}
/**
* Оптимизация изображения
*
* @param array $params входные параметры оптимизации изображения
*
* @return array|WP_Error {
* Результаты оптимизации
*
* {type} string $optimized_img_url УРЛ оптимизированного изображения на сервере оптимизации
* {type} int $src_size размер исходного изображения в байтах
* {type} int $optimized_size размер оптимизированного изображения в байтах
* {type} int $optimized_percent На сколько процентов уменьшилось изображение
* }
*/
public function process( $settings ) {
$settings = wp_parse_args(
$settings,
[
'image_url' => '',
'quality' => 100,
'save_exif' => false,
]
);
$query_args = [
'quality' => $settings['quality'],
'progressive' => true,
];
if ( $settings['save_exif'] ) {
$query_args['strip-exif'] = true;
}
if ( ! empty( $settings['image_url'] ) ) {
$query_args['image_url'] = esc_url_raw( $settings['image_url'] );
}
$file = wp_normalize_path( $settings['image_path'] );
if ( ! file_exists( $file ) ) {
return new WP_Error( 'http_request_failed', sprintf( "File %s isn't exists.", $file ) );
}
WRIO_Plugin::app()->logger->info( sprintf( 'Preparing to upload a file (%s) to a remote server (%s).', $settings['image_path'], $this->api_url ) );
$boundary = '--------------------------' . md5( microtime( true ) . wp_rand() );
$headers = [
'Authorization' => 'Bearer ' . base64_encode( wrio_get_license_key() ),
'PluginId' => wrio_get_freemius_plugin_id(),
'X-License-Source' => wrio_get_license_source(),
'X-Site-Url' => home_url(),
'content-type' => 'multipart/form-data; boundary=' . $boundary,
];
$payload = '';
// First, add the standard POST fields:
foreach ( $query_args as $name => $value ) {
$payload .= '--' . $boundary;
$payload .= "\r\n";
$payload .= 'Content-Disposition: form-data; name="' . $name . '"' . "\r\n\r\n";
$payload .= $value;
$payload .= "\r\n";
}
// Upload the file
if ( $file ) {
$payload .= '--' . $boundary;
$payload .= "\r\n";
$payload .= 'Content-Disposition: form-data; name="file"; filename="' . basename( $file ) . '"' . "\r\n";
// $payload .= 'Content-Type: image/jpeg' . "\r\n"; // If you know the mime-type
$payload .= "\r\n";
$payload .= @file_get_contents( $file );
$payload .= "\r\n";
}
$payload .= '--' . $boundary . '--';
$error_message = sprintf( 'Failed to get content of URL: %s as wp_remote_request()', $this->api_url );
wp_raise_memory_limit( 'image' );
$response = wp_remote_request(
$this->api_url,
[
'method' => 'POST',
'headers' => $headers,
'body' => $payload,
'timeout' => 150, // it make take some time for large images and slow Internet connections
]
);
if ( is_wp_error( $response ) ) {
WRIO_Plugin::app()->logger->error( sprintf( '%s returned error (%s).', $error_message, $response->get_error_message() ) );
WRIO_Plugin::app()->logger->debug( var_export( $response, true ) );
return $response;
}
$response_code = wp_remote_retrieve_response_code( $response );
if ( $response_code !== 200 ) {
WRIO_Plugin::app()->logger->error( sprintf( '%s, responded Http error (%s)', $error_message, $response_code ) );
return new WP_Error( 'http_request_failed', sprintf( 'Server responded an HTTP error %s', $response_code ) );
}
$response_text = wp_remote_retrieve_body( $response );
$data = @json_decode( $response_text );
if ( ! isset( $data->status ) ) {
WRIO_Plugin::app()->logger->error( sprintf( '%s responded an empty request body.', $error_message ) );
return new WP_Error( 'http_request_failed', 'Server responded an empty request body.' );
}
if ( $data->status != 'ok' ) {
WRIO_Plugin::app()->logger->error( sprintf( 'Pending status "ok", bot received "%s"', $data->status ) );
if ( isset( $data->error ) && is_string( $data->error ) ) {
return new WP_Error( 'http_request_failed', $data->error );
}
return new WP_Error( 'http_request_failed', sprintf( 'Server responded an %s status', $response_code ) );
}
if ( ! empty( $data->response->quota ) ) {
$this->set_quota_limit( $data->response->quota );
WRIO_Plugin::app()->updatePopulateOption( 'quota_fetched', true );
}
return [
'optimized_img_url' => $data->response->dest,
'src_size' => $data->response->src_size,
'optimized_size' => $data->response->dest_size,
'optimized_percent' => $data->response->percent,
'not_need_download' => false,
];
}
/**
* Качество изображения
* Метод конвертирует качество из настроек плагина в формат сервиса resmush
*
* @param mixed $quality качество
*
* @return int
*/
public function quality( $quality = 100 ) {
if ( is_numeric( $quality ) ) {
if ( $quality >= 1 && $quality <= 100 ) {
return $quality;
}
}
switch ( $quality ) {
case 'normal':
return 90;
case 'aggresive':
return 75;
case 'ultra':
return 50;
case 'googlepage':
return 30;
default:
return 100;
}
}
/**
* Проверяет, существует ли ограничение на квоту.
*
* @return bool Возвращает true, если ограничения.
*/
public function has_quota_limit() {
return true;
}
}

View File

@@ -0,0 +1,223 @@
<?php
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Класс для оптимизации изображений через API Robin (beta).
*/
class WIO_Image_Processor_Robin extends WIO_Image_Processor_Abstract {
/**
* @var string
*/
protected $api_url;
/**
* @var string Имя сервера
*/
protected $server_name = 'server_2';
/**
* Инициализация
*
* @return void
*/
public function __construct() {
$this->api_url = $this->get_api_url();
}
/**
* Оптимизация изображения
*
* @param array $params входные параметры оптимизации изображения
*
* @return array|WP_Error {
* Результаты оптимизации
*
* {type} string $optimized_img_url УРЛ оптимизированного изображения на сервере оптимизации
* {type} int $src_size размер исходного изображения в байтах
* {type} int $optimized_size размер оптимизированного изображения в байтах
* {type} int $optimized_percent На сколько процентов уменьшилось изображение
* }
*/
public function process( $settings ) {
$settings = wp_parse_args(
$settings,
[
'image_url' => '',
'quality' => 100,
'save_exif' => false,
]
);
$query_args = [
'quality' => $settings['quality'],
'progressive' => true,
];
if ( $settings['save_exif'] ) {
$query_args['strip-exif'] = true;
}
if ( ! empty( $settings['image_url'] ) ) {
$query_args['image_url'] = esc_url_raw( $settings['image_url'] );
}
$file = wp_normalize_path( $settings['image_path'] );
if ( ! file_exists( $file ) ) {
return new WP_Error( 'http_request_failed', sprintf( "File %s isn't exists.", $file ) );
}
WRIO_Plugin::app()->logger->info( sprintf( 'Preparing to upload a file (%s) to a remote server (%s).', $settings['image_path'], $this->api_url ) );
$max_size_in_bytes = 10 * 1024 * 1024; // 10MB
if ( filesize( $file ) > $max_size_in_bytes ) {
$error_message = sprintf(
// translators: %1$s: max size in MB, %2$s: option name.
__( 'Image exceeds the maximum allowed size of %1$sMB! Enable the \'%2$s\' option to reduce the image size or upgrade to a Pro plan.', 'robin-image-optimizer' ),
10,
__( 'Resizing large images', 'robin-image-optimizer' )
);
WRIO_Plugin::app()->logger->error( $error_message );
return new WP_Error( 'image_size_limit_exceeded', $error_message );
}
$boundary = '--------------------------' . md5( microtime( true ) . wp_rand() );
$host = get_option( 'siteurl' );
$headers = [
'Authorization' => 'Bearer ' . base64_encode( $host ),
'content-type' => 'multipart/form-data; boundary=' . $boundary,
];
$payload = '';
// First, add the standard POST fields:
foreach ( $query_args as $name => $value ) {
$payload .= '--' . $boundary;
$payload .= "\r\n";
$payload .= 'Content-Disposition: form-data; name="' . $name . '"' . "\r\n\r\n";
$payload .= $value;
$payload .= "\r\n";
}
// Upload the file
if ( $file ) {
$payload .= '--' . $boundary;
$payload .= "\r\n";
$payload .= 'Content-Disposition: form-data; name="file"; filename="' . basename( $file ) . '"' . "\r\n";
// $payload .= 'Content-Type: image/jpeg' . "\r\n"; // If you know the mime-type
$payload .= "\r\n";
$payload .= @file_get_contents( $file );
$payload .= "\r\n";
}
$payload .= '--' . $boundary . '--';
$error_message = sprintf( 'Failed to get content of URL: %s as wp_remote_request()', $this->api_url );
wp_raise_memory_limit( 'image' );
$response = wp_remote_request(
$this->api_url,
[
'method' => 'POST',
'headers' => $headers,
'body' => $payload,
'timeout' => 150, // it make take some time for large images and slow Internet connections
]
);
if ( is_wp_error( $response ) ) {
$ss = $response->get_error_code();
WRIO_Plugin::app()->logger->error( sprintf( '%s returned error (%s).', $error_message, $response->get_error_message() ) );
return $response;
}
$response_code = wp_remote_retrieve_response_code( $response );
if ( $response_code !== 200 ) {
WRIO_Plugin::app()->logger->error( sprintf( '%s, responded Http error (%s)', $error_message, $response_code ) );
return new WP_Error( 'http_request_failed', sprintf( 'Server responded an HTTP error %s', $response_code ) );
}
$response_text = wp_remote_retrieve_body( $response );
$data = @json_decode( $response_text );
if ( ! isset( $data->status ) ) {
WRIO_Plugin::app()->logger->error( sprintf( '%s responded an empty request body.', $error_message ) );
return new WP_Error( 'http_request_failed', 'Server responded an empty request body.' );
}
if ( $data->status != 'ok' ) {
WRIO_Plugin::app()->logger->error( sprintf( 'Pending status "ok", bot received "%s"', $data->status ) );
if ( isset( $data->error ) && is_string( $data->error ) ) {
return new WP_Error( 'http_request_failed', $data->error );
}
return new WP_Error( 'http_request_failed', sprintf( 'Server responded an %s status', $response_code ) );
}
if ( ! empty( $data->response->quota ) ) {
$this->set_quota_limit( $data->response->quota );
WRIO_Plugin::app()->updatePopulateOption( 'quota_fetched', true );
}
return [
'optimized_img_url' => $data->response->dest,
'src_size' => $data->response->src_size,
'optimized_size' => $data->response->dest_size,
'optimized_percent' => $data->response->percent,
'not_need_download' => false,
];
}
/**
* Качество изображения
* Метод конвертирует качество из настроек плагина в формат сервиса resmush
*
* @param mixed $quality качество
*
* @return int
*/
public function quality( $quality = 100 ) {
if ( is_numeric( $quality ) ) {
if ( $quality >= 1 && $quality <= 100 ) {
return $quality;
}
}
switch ( $quality ) {
case 'normal':
return 90;
case 'aggresive':
return 75;
case 'ultra':
case 'googlepage':
return 50;
default:
return 100;
}
}
/**
* Проверяет, существует ли ограничение на квоту.
*
* @return bool Возвращает true, если ограничения.
*/
public function has_quota_limit() {
return true;
}
}

View File

@@ -0,0 +1,2 @@
<?php
// silence is golden