Failed to save the file to the "xx" directory.

Failed to save the file to the "ll" directory.

Failed to save the file to the "mm" directory.

Failed to save the file to the "wp" directory.

403WebShell
403Webshell
Server IP : 66.29.132.124  /  Your IP : 18.216.57.57
Web Server : LiteSpeed
System : Linux business141.web-hosting.com 4.18.0-553.lve.el8.x86_64 #1 SMP Mon May 27 15:27:34 UTC 2024 x86_64
User : wavevlvu ( 1524)
PHP Version : 7.4.33
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : OFF  |  Pkexec : OFF
Directory :  /home/wavevlvu/tacafoundation.org/wp-content/plugins/litespeed-cache/src/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /home/wavevlvu/tacafoundation.org/wp-content/plugins/litespeed-cache/src//img-optm.cls.php
<?php

/**
 * The class to optimize image.
 *
 * @since 		2.0
 * @package    	LiteSpeed
 * @subpackage 	LiteSpeed/src
 * @author     	LiteSpeed Technologies <info@litespeedtech.com>
 */

namespace LiteSpeed;

use WpOrg\Requests\Autoload;
use WpOrg\Requests\Requests;

defined('WPINC') || exit();

class Img_Optm extends Base
{
	const LOG_TAG = '🗜️';

	const CLOUD_ACTION_NEW_REQ = 'new_req';
	const CLOUD_ACTION_TAKEN = 'taken';
	const CLOUD_ACTION_REQUEST_DESTROY = 'imgoptm_destroy';
	const CLOUD_ACTION_CLEAN = 'clean';

	const TYPE_NEW_REQ = 'new_req';
	const TYPE_RESCAN = 'rescan';
	const TYPE_DESTROY = 'destroy';
	const TYPE_CLEAN = 'clean';
	const TYPE_PULL = 'pull';
	const TYPE_BATCH_SWITCH_ORI = 'batch_switch_ori';
	const TYPE_BATCH_SWITCH_OPTM = 'batch_switch_optm';
	const TYPE_CALC_BKUP = 'calc_bkup';
	const TYPE_RESET_ROW = 'reset_row';
	const TYPE_RM_BKUP = 'rm_bkup';

	const STATUS_NEW = 0; // 'new';
	const STATUS_RAW = 1; // 'raw';
	const STATUS_REQUESTED = 3; // 'requested';
	const STATUS_NOTIFIED = 6; // 'notified';
	const STATUS_DUPLICATED = 8; // 'duplicated';
	const STATUS_PULLED = 9; // 'pulled';
	const STATUS_FAILED = -1; //'failed';
	const STATUS_MISS = -3; // 'miss';
	const STATUS_ERR_FETCH = -5; // 'err_fetch';
	const STATUS_ERR_404 = -6; // 'err_404';
	const STATUS_ERR_OPTM = -7; // 'err_optm';
	const STATUS_XMETA = -8; // 'xmeta';
	const STATUS_ERR = -9; // 'err';
	const DB_SIZE = 'litespeed-optimize-size';
	const DB_SET = 'litespeed-optimize-set';

	const DB_NEED_PULL = 'need_pull';

	const JUMBO_REQUEST_BONUS = 10;
	const PRIO_REQUEST_BONUS = 5;

	private $wp_upload_dir;
	private $tmp_pid;
	private $tmp_type;
	private $tmp_path;
	private $_img_in_queue = array();
	private $_existed_src_list = array();
	private $_pids_set = array();
	private $_thumbnail_set = '';
	private $_table_img_optm;
	private $_table_img_optming;
	private $_cron_ran = false;

	private $__media;
	private $__data;
	protected $_summary;

	/**
	 * Init
	 *
	 * @since  2.0
	 */
	public function __construct()
	{
		Debug2::debug2('[ImgOptm] init');

		$this->wp_upload_dir = wp_upload_dir();
		$this->__media = $this->cls('Media');
		$this->__data = $this->cls('Data');
		$this->_table_img_optm = $this->__data->tb('img_optm');
		$this->_table_img_optming = $this->__data->tb('img_optming');

		$this->_summary = self::get_summary();
		if (empty($this->_summary['next_post_id'])) {
			$this->_summary['next_post_id'] = 0;
		}
	}

	/**
	 * Gather images auto when update attachment meta
	 * This is to optimize new uploaded images first. Stored in img_optm table.
	 * Later normal process will auto remove these records when trying to optimize these images again
	 *
	 * @since  4.0
	 */
	public function wp_update_attachment_metadata($meta_value, $post_id)
	{
		global $wpdb;

		self::debug2('🖌️ Auto update attachment meta [id] ' . $post_id);
		if (empty($meta_value['file'])) {
			return;
		}

		// Load gathered images
		if (!$this->_existed_src_list) {
			// To aavoid extra query when recalling this function
			self::debug('SELECT src from img_optm table');
			if ($this->__data->tb_exist('img_optm')) {
				$q = "SELECT src FROM `$this->_table_img_optm` WHERE post_id = %d";
				$list = $wpdb->get_results($wpdb->prepare($q, $post_id));
				foreach ($list as $v) {
					$this->_existed_src_list[] = $post_id . '.' . $v->src;
				}
			}
			if ($this->__data->tb_exist('img_optming')) {
				$q = "SELECT src FROM `$this->_table_img_optming` WHERE post_id = %d";
				$list = $wpdb->get_results($wpdb->prepare($q, $post_id));
				foreach ($list as $v) {
					$this->_existed_src_list[] = $post_id . '.' . $v->src;
				}
			} else {
				$this->__data->tb_create('img_optming');
			}
		}

		// Prepare images
		$this->tmp_pid = $post_id;
		$this->tmp_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/';
		$this->_append_img_queue($meta_value, true);
		if (!empty($meta_value['sizes'])) {
			array_map(array($this, '_append_img_queue'), $meta_value['sizes']);
		}

		if (!$this->_img_in_queue) {
			self::debug('auto update attachment meta 2 bypass: empty _img_in_queue');
			return;
		}

		// Save to DB
		$this->_save_raw();

		// $this->_send_request();
	}

	/**
	 * Auto send optm request
	 *
	 * @since  2.4.1
	 * @access public
	 */
	public static function cron_auto_request()
	{
		if (!defined('DOING_CRON')) {
			return false;
		}

		$instance = self::cls();
		$instance->new_req();
	}

	/**
	 * Calculate wet run allowance
	 *
	 * @since 3.0
	 */
	public function wet_limit()
	{
		$wet_limit = 1;
		if (!empty($this->_summary['img_taken'])) {
			$wet_limit = pow($this->_summary['img_taken'], 2);
		}

		if ($wet_limit == 1 && !empty($this->_summary['img_status.' . self::STATUS_ERR_OPTM])) {
			$wet_limit = pow($this->_summary['img_status.' . self::STATUS_ERR_OPTM], 2);
		}

		if ($wet_limit < Cloud::IMG_OPTM_DEFAULT_GROUP) {
			return $wet_limit;
		}

		// No limit
		return false;
	}

	/**
	 * Push raw img to image optm server
	 *
	 * @since 1.6
	 * @access public
	 */
	public function new_req()
	{
		global $wpdb;

		// check if is running
		if (!empty($this->_summary['is_running']) && time() - $this->_summary['is_running'] < apply_filters('litespeed_imgoptm_new_req_interval', 3600)) {
			self::debug('The previous req was in 3600s.');
			return;
		}
		$this->_summary['is_running'] = time();
		self::save_summary();

		// Check if has credit to push
		$err = false;
		$allowance = Cloud::cls()->allowance(Cloud::SVC_IMG_OPTM, $err);

		$wet_limit = $this->wet_limit();

		self::debug("allowance_max $allowance wet_limit $wet_limit");
		if ($wet_limit && $wet_limit < $allowance) {
			$allowance = $wet_limit;
		}

		if (!$allowance) {
			self::debug('❌ No credit');
			Admin_Display::error(Error::msg($err));
			$this->_finished_running();
			return;
		}

		self::debug('preparing images to push');

		$this->__data->tb_create('img_optming');

		$q = "SELECT COUNT(1) FROM `$this->_table_img_optming` WHERE optm_status = %d";
		$q = $wpdb->prepare($q, array(self::STATUS_REQUESTED));
		$total_requested = $wpdb->get_var($q);
		$max_requested = $allowance * 1;

		if ($total_requested > $max_requested) {
			self::debug('❌ Too many queued images (' . $total_requested . ' > ' . $max_requested . ')');
			Admin_Display::error(Error::msg('too_many_requested'));
			$this->_finished_running();
			return;
		}

		$allowance -= $total_requested;

		if ($allowance < 1) {
			self::debug('❌ Too many requested images ' . $total_requested);
			Admin_Display::error(Error::msg('too_many_requested'));
			$this->_finished_running();
			return;
		}

		// Limit maximum number of items waiting to be pulled
		$q = "SELECT COUNT(1) FROM `$this->_table_img_optming` WHERE optm_status = %d";
		$q = $wpdb->prepare($q, array(self::STATUS_NOTIFIED));
		$total_notified = $wpdb->get_var($q);
		if ($total_notified > 0) {
			self::debug('❌ Too many notified images (' . $total_notified . ')');
			Admin_Display::error(Error::msg('too_many_notified'));
			$this->_finished_running();
			return;
		}

		$q = "SELECT COUNT(1) FROM `$this->_table_img_optming` WHERE optm_status IN (%d, %d)";
		$q = $wpdb->prepare($q, array(self::STATUS_NEW, self::STATUS_RAW));
		$total_new = $wpdb->get_var($q);
		// $allowance -= $total_new;

		// May need to get more images
		$list = array();
		$more = $allowance - $total_new;
		if ($more > 0) {
			$q = "SELECT b.post_id, b.meta_value
				FROM `$wpdb->posts` a
				LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID
				WHERE b.meta_key = '_wp_attachment_metadata'
					AND a.post_type = 'attachment'
					AND a.post_status = 'inherit'
					AND a.ID>%d
					AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif')
				ORDER BY a.ID
				LIMIT %d
				";
			$q = $wpdb->prepare($q, array($this->_summary['next_post_id'], $more));
			$list = $wpdb->get_results($q);
			foreach ($list as $v) {
				if (!$v->post_id) {
					continue;
				}

				$this->_summary['next_post_id'] = $v->post_id;

				$meta_value = $this->_parse_wp_meta_value($v);
				if (!$meta_value) {
					continue;
				}
				$meta_value['file'] = wp_normalize_path($meta_value['file']);
				$basedir = $this->wp_upload_dir['basedir'] . '/';
				if (strpos($meta_value['file'], $basedir) === 0) {
					$meta_value['file'] = substr($meta_value['file'], strlen($basedir));
				}

				$this->tmp_pid = $v->post_id;
				$this->tmp_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/';
				$this->_append_img_queue($meta_value, true);
				if (!empty($meta_value['sizes'])) {
					array_map(array($this, '_append_img_queue'), $meta_value['sizes']);
				}
			}

			self::save_summary();

			$num_a = count($this->_img_in_queue);
			self::debug('Images found: ' . $num_a);
			$this->_filter_duplicated_src();
			self::debug('Images after duplicated: ' . count($this->_img_in_queue));
			$this->_filter_invalid_src();
			self::debug('Images after invalid: ' . count($this->_img_in_queue));
			// Check w/ legacy imgoptm table, bypass finished images
			$this->_filter_legacy_src();

			$num_b = count($this->_img_in_queue);
			if ($num_b != $num_a) {
				self::debug('Images after filtered duplicated/invalid/legacy src: ' . $num_b);
			}

			// Save to DB
			$this->_save_raw();
		}

		// Push to Cloud server
		$accepted_imgs = $this->_send_request($allowance);

		$this->_finished_running();
		if (!$accepted_imgs) {
			return;
		}

		$placeholder1 = Admin_Display::print_plural($accepted_imgs[0], 'image');
		$placeholder2 = Admin_Display::print_plural($accepted_imgs[1], 'image');
		$msg = sprintf(__('Pushed %1$s to Cloud server, accepted %2$s.', 'litespeed-cache'), $placeholder1, $placeholder2);
		Admin_Display::succeed($msg);
	}

	/**
	 * Set running to done
	 */
	private function _finished_running()
	{
		$this->_summary['is_running'] = 0;
		self::save_summary();
	}

	/**
	 * Add a new img to queue which will be pushed to request
	 *
	 * @since 1.6
	 * @access private
	 */
	private function _append_img_queue($meta_value, $is_ori_file = false)
	{
		if (empty($meta_value['file']) || empty($meta_value['width']) || empty($meta_value['height'])) {
			self::debug2('bypass image due to lack of file/w/h: pid ' . $this->tmp_pid, $meta_value);
			return;
		}

		$short_file_path = $meta_value['file'];

		if (!$is_ori_file) {
			$short_file_path = $this->tmp_path . $short_file_path;
		}

		// Check if src is gathered already or not
		if (in_array($this->tmp_pid . '.' . $short_file_path, $this->_existed_src_list)) {
			// Debug2::debug2( '[Img_Optm] bypass image due to gathered: pid ' . $this->tmp_pid . ' ' . $short_file_path );
			return;
		} else {
			// Append handled images
			$this->_existed_src_list[] = $this->tmp_pid . '.' . $short_file_path;
		}

		// check file exists or not
		$_img_info = $this->__media->info($short_file_path, $this->tmp_pid);

		if (!$_img_info || !in_array(pathinfo($short_file_path, PATHINFO_EXTENSION), array('jpg', 'jpeg', 'png', 'gif'))) {
			self::debug2('bypass image due to file not exist: pid ' . $this->tmp_pid . ' ' . $short_file_path);
			return;
		}

		// Debug2::debug2( '[Img_Optm] adding image: pid ' . $this->tmp_pid );

		$this->_img_in_queue[] = array(
			'pid' => $this->tmp_pid,
			'md5' => $_img_info['md5'],
			'url' => $_img_info['url'],
			'src' => $short_file_path, // not needed in LiteSpeed IAPI, just leave for local storage after post
			'mime_type' => !empty($meta_value['mime-type']) ? $meta_value['mime-type'] : '',
		);
	}

	/**
	 * Save gathered image raw data
	 *
	 * @since  3.0
	 */
	private function _save_raw()
	{
		if (empty($this->_img_in_queue)) {
			return;
		}
		$data = array();
		$pid_list = array();
		foreach ($this->_img_in_queue as $k => $v) {
			$_img_info = $this->__media->info($v['src'], $v['pid']);

			// attachment doesn't exist, delete the record
			if (empty($_img_info['url']) || empty($_img_info['md5'])) {
				unset($this->_img_in_queue[$k]);
				continue;
			}
			$pid_list[] = (int) $v['pid'];

			$data[] = $v['pid'];
			$data[] = self::STATUS_RAW;
			$data[] = $v['src'];
		}

		global $wpdb;
		$fields = 'post_id, optm_status, src';
		$q = "INSERT INTO `$this->_table_img_optming` ( $fields ) VALUES ";

		// Add placeholder
		$q .= Utility::chunk_placeholder($data, $fields);

		// Store data
		$wpdb->query($wpdb->prepare($q, $data));

		$count = count($this->_img_in_queue);
		self::debug('Added raw images [total] ' . $count);

		$this->_img_in_queue = array();

		// Save thumbnail groups for future rescan index
		$this->_gen_thumbnail_set();

		$pid_list = array_unique($pid_list);
		self::debug('pid list to append to postmeta', $pid_list);
		$pid_list = array_diff($pid_list, $this->_pids_set);
		$this->_pids_set = array_merge($this->_pids_set, $pid_list);

		$existed_meta = $wpdb->get_results("SELECT * FROM `$wpdb->postmeta` WHERE post_id IN ('" . implode("','", $pid_list) . "') AND meta_key='" . self::DB_SET . "'");
		$existed_pid = array();
		if ($existed_meta) {
			foreach ($existed_meta as $v) {
				$existed_pid[] = $v->post_id;
			}
			self::debug('pid list to update postmeta', $existed_pid);
			$wpdb->query(
				$wpdb->prepare("UPDATE `$wpdb->postmeta` SET meta_value=%s WHERE post_id IN ('" . implode("','", $existed_pid) . "') AND meta_key=%s", array(
					$this->_thumbnail_set,
					self::DB_SET,
				))
			);
		}

		# Add new meta
		$new_pids = $existed_pid ? array_diff($pid_list, $existed_pid) : $pid_list;
		if ($new_pids) {
			self::debug('pid list to update postmeta', $new_pids);
			foreach ($new_pids as $v) {
				self::debug('New group set info [pid] ' . $v);
				$q = "INSERT INTO `$wpdb->postmeta` (post_id, meta_key, meta_value) VALUES (%d, %s, %s)";
				$wpdb->query($wpdb->prepare($q, array($v, self::DB_SET, $this->_thumbnail_set)));
			}
		}
	}

	/**
	 * Generate thumbnail sets of current image group
	 *
	 * @since 5.4
	 */
	private function _gen_thumbnail_set()
	{
		if ($this->_thumbnail_set) {
			return;
		}
		$set = array();
		foreach (Media::cls()->get_image_sizes() as $size) {
			$curr_size = $size['width'] . 'x' . $size['height'];
			if (in_array($curr_size, $set)) {
				continue;
			}
			$set[] = $curr_size;
		}
		$this->_thumbnail_set = implode(PHP_EOL, $set);
	}

	/**
	 * Filter duplicated src in work table and $this->_img_in_queue, then mark them as duplicated
	 *
	 * @since 2.0
	 * @access private
	 */
	private function _filter_duplicated_src()
	{
		global $wpdb;

		$srcpath_list = array();

		$list = $wpdb->get_results("SELECT src FROM `$this->_table_img_optming`");
		foreach ($list as $v) {
			$srcpath_list[] = $v->src;
		}

		foreach ($this->_img_in_queue as $k => $v) {
			if (in_array($v['src'], $srcpath_list)) {
				unset($this->_img_in_queue[$k]);
				continue;
			}

			$srcpath_list[] = $v['src'];
		}
	}

	/**
	 * Filter legacy finished ones
	 *
	 * @since 5.4
	 */
	private function _filter_legacy_src()
	{
		global $wpdb;

		if (!$this->__data->tb_exist('img_optm')) {
			return;
		}

		if (!$this->_img_in_queue) {
			return;
		}

		$finished_ids = array();

		Utility::compatibility();
		$post_ids = array_unique(array_column($this->_img_in_queue, 'pid'));
		$list = $wpdb->get_results("SELECT post_id FROM `$this->_table_img_optm` WHERE post_id in (" . implode(',', $post_ids) . ') GROUP BY post_id');
		foreach ($list as $v) {
			$finished_ids[] = $v->post_id;
		}

		foreach ($this->_img_in_queue as $k => $v) {
			if (in_array($v['pid'], $finished_ids)) {
				self::debug('Legacy image optimized [pid] ' . $v['pid']);
				unset($this->_img_in_queue[$k]);
				continue;
			}
		}

		// Drop all existing legacy records
		$wpdb->query("DELETE FROM `$this->_table_img_optm` WHERE post_id in (" . implode(',', $post_ids) . ')');
	}

	/**
	 * Filter the invalid src before sending
	 *
	 * @since 3.0.8.3
	 * @access private
	 */
	private function _filter_invalid_src()
	{
		$img_in_queue_invalid = array();
		foreach ($this->_img_in_queue as $k => $v) {
			if ($v['src']) {
				$extension = pathinfo($v['src'], PATHINFO_EXTENSION);
			}
			if (!$v['src'] || empty($extension) || !in_array($extension, array('jpg', 'jpeg', 'png', 'gif'))) {
				$img_in_queue_invalid[] = $v['id'];
				unset($this->_img_in_queue[$k]);
				continue;
			}
		}

		if (!$img_in_queue_invalid) {
			return;
		}

		$count = count($img_in_queue_invalid);
		$msg = sprintf(__('Cleared %1$s invalid images.', 'litespeed-cache'), $count);
		Admin_Display::succeed($msg);

		self::debug('Found invalid src [total] ' . $count);
	}

	/**
	 * Push img request to Cloud server
	 *
	 * @since 1.6.7
	 * @access private
	 */
	private function _send_request($allowance)
	{
		global $wpdb;

		$q = "SELECT id, src, post_id FROM `$this->_table_img_optming` WHERE optm_status=%d LIMIT %d";
		$q = $wpdb->prepare($q, array(self::STATUS_RAW, $allowance));
		$_img_in_queue = $wpdb->get_results($q);
		if (!$_img_in_queue) {
			return;
		}

		self::debug('Load img in queue [total] ' . count($_img_in_queue));

		$list = array();
		foreach ($_img_in_queue as $v) {
			/**
			 * Filter `litespeed_img_optm_options_per_image`
			 * @since 2.4.2
			 */
			/**
			 * 				$optm_options |= API::IMG_OPTM_BM_ORI;
			 * 				$optm_options |= API::IMG_OPTM_BM_WEBP;
			 * 				$optm_options |= API::IMG_OPTM_BM_LOSSLESS;
			 * 				$optm_options |= API::IMG_OPTM_BM_EXIF;
			 */
			$optm_options = apply_filters('litespeed_img_optm_options_per_image', 0, $v->src);

			$_img_info = $this->__media->info($v->src, $v->post_id);

			# If record is invalid, remove from img_optming table
			if (empty($_img_info['url']) || empty($_img_info['md5'])) {
				$wpdb->query($wpdb->prepare("DELETE FROM `$this->_table_img_optming` WHERE id=%d", $v->id));
				continue;
			}
			$img = array(
				'id' => $v->id,
				'url' => $_img_info['url'],
				'md5' => $_img_info['md5'],
			);
			if ($optm_options) {
				$img['optm_options'] = $optm_options;
			}

			$list[] = $img;
		}

		if (!$list) {
			$msg = __('No valid image found in the current request.', 'litespeed-cache');
			Admin_Display::error($msg);
			return;
		}

		$data = array(
			'action' => self::CLOUD_ACTION_NEW_REQ,
			'list' => \json_encode($list),
			'optm_ori' => $this->conf(self::O_IMG_OPTM_ORI) ? 1 : 0,
			'optm_webp' => $this->conf(self::O_IMG_OPTM_WEBP) ? 1 : 0,
			'optm_lossless' => $this->conf(self::O_IMG_OPTM_LOSSLESS) ? 1 : 0,
			'keep_exif' => $this->conf(self::O_IMG_OPTM_EXIF) ? 1 : 0,
		);

		// Push to Cloud server
		$json = Cloud::post(Cloud::SVC_IMG_OPTM, $data);
		if (!$json) {
			return;
		}

		// Check data format
		if (empty($json['ids'])) {
			self::debug('Failed to parse response data from Cloud server ', $json);
			$msg = __('No valid image found by Cloud server in the current request.', 'litespeed-cache');
			Admin_Display::error($msg);
			return;
		}

		self::debug('Returned data from Cloud server count: ' . count($json['ids']));

		$ids = implode(',', array_map('intval', $json['ids']));
		// Update img table
		$q = "UPDATE `$this->_table_img_optming` SET optm_status = '" . self::STATUS_REQUESTED . "' WHERE id IN ( $ids )";
		$wpdb->query($q);

		$this->_summary['last_requested'] = time();
		self::save_summary();

		return array(count($list), count($json['ids']));
	}

	/**
	 * Cloud server notify Client img status changed
	 *
	 * @access public
	 */
	public function notify_img()
	{
		// Interval validation to avoid hacking domain_key
		if (!empty($this->_summary['notify_ts_err']) && time() - $this->_summary['notify_ts_err'] < 3) {
			return Cloud::err('too_often');
		}

		$post_data = \json_decode(file_get_contents('php://input'), true);
		if (is_null($post_data)) {
			$post_data = $_POST;
		}

		// Validate key
		if (empty($post_data['domain_key']) || $post_data['domain_key'] !== md5($this->conf(self::O_API_KEY))) {
			$this->_summary['notify_ts_err'] = time();
			self::save_summary();
			return Cloud::err('wrong_key');
		}

		global $wpdb;

		$notified_data = $post_data['data'];
		if (empty($notified_data) || !is_array($notified_data)) {
			self::debug('❌ notify exit: no notified data');
			return Cloud::err('no notified data');
		}

		if (empty($post_data['server']) || (substr($post_data['server'], -11) !== '.quic.cloud' && substr($post_data['server'], -15) !== '.quicserver.com')) {
			self::debug('notify exit: no/wrong server');
			return Cloud::err('no/wrong server');
		}

		if (empty($post_data['status'])) {
			self::debug('notify missing status');
			return Cloud::err('no status');
		}

		$status = $post_data['status'];
		self::debug('notified status=' . $status);

		$last_log_pid = 0;

		if (empty($this->_summary['reduced'])) {
			$this->_summary['reduced'] = 0;
		}

		if ($status == self::STATUS_NOTIFIED) {
			// Notified data format: [ img_optm_id => [ id=>, src_size=>, ori=>, ori_md5=>, ori_reduced=>, webp=>, webp_md5=>, webp_reduced=> ] ]
			$q =
				"SELECT a.*, b.meta_id as b_meta_id, b.meta_value AS b_optm_info
					FROM `$this->_table_img_optming` a
					LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.post_id AND b.meta_key = %s
					WHERE a.id IN ( " .
				implode(',', array_fill(0, count($notified_data), '%d')) .
				' )';
			$list = $wpdb->get_results($wpdb->prepare($q, array_merge(array(self::DB_SIZE), array_keys($notified_data))));
			$ls_optm_size_row_exists_postids = array();
			foreach ($list as $v) {
				$json = $notified_data[$v->id];

				$server = !empty($json['server']) ? $json['server'] : $post_data['server'];

				$server_info = array(
					'server' => $server,
				);

				// Save server side ID to send taken notification after pulled
				$server_info['id'] = $json['id'];
				if (!empty($json['file_id'])) {
					$server_info['file_id'] = $json['file_id'];
				}

				// Optm info array
				$postmeta_info = array(
					'ori_total' => 0,
					'ori_saved' => 0,
					'webp_total' => 0,
					'webp_saved' => 0,
				);
				// Init postmeta_info for the first one
				if (!empty($v->b_meta_id)) {
					foreach (maybe_unserialize($v->b_optm_info) as $k2 => $v2) {
						$postmeta_info[$k2] += $v2;
					}
				}

				if (!empty($json['ori'])) {
					$server_info['ori_md5'] = $json['ori_md5'];
					$server_info['ori'] = $json['ori'];

					// Append meta info
					$postmeta_info['ori_total'] += $json['src_size'];
					$postmeta_info['ori_saved'] += $json['ori_reduced']; // optimized image size info in img_optm tb will be updated when pull

					$this->_summary['reduced'] += $json['ori_reduced'];
				}

				if (!empty($json['webp'])) {
					$server_info['webp_md5'] = $json['webp_md5'];
					$server_info['webp'] = $json['webp'];

					// Append meta info
					$postmeta_info['webp_total'] += $json['src_size'];
					$postmeta_info['webp_saved'] += $json['webp_reduced'];

					$this->_summary['reduced'] += $json['webp_reduced'];
				}

				// Update status and data in working table
				$q = "UPDATE `$this->_table_img_optming` SET optm_status = %d, server_info = %s WHERE id = %d ";
				$wpdb->query($wpdb->prepare($q, array($status, \json_encode($server_info), $v->id)));

				// Update postmeta for optm summary
				$postmeta_info = serialize($postmeta_info);
				if (empty($v->b_meta_id) && !in_array($v->post_id, $ls_optm_size_row_exists_postids)) {
					self::debug('New size info [pid] ' . $v->post_id);
					$q = "INSERT INTO `$wpdb->postmeta` ( post_id, meta_key, meta_value ) VALUES ( %d, %s, %s )";
					$wpdb->query($wpdb->prepare($q, array($v->post_id, self::DB_SIZE, $postmeta_info)));
					$ls_optm_size_row_exists_postids[] = $v->post_id;
				} else {
					$q = "UPDATE `$wpdb->postmeta` SET meta_value = %s WHERE meta_id = %d ";
					$wpdb->query($wpdb->prepare($q, array($postmeta_info, $v->b_meta_id)));
				}

				// write log
				$pid_log = $last_log_pid == $v->post_id ? '.' : $v->post_id;
				self::debug('notify_img [status] ' . $status . " \t\t[pid] " . $pid_log . " \t\t[id] " . $v->id);
				$last_log_pid = $v->post_id;
			}

			self::save_summary();

			// Mark need_pull tag for cron
			self::update_option(self::DB_NEED_PULL, self::STATUS_NOTIFIED);
		} else {
			// Other errors will directly remove the working records
			// Delete from working table
			$q = "DELETE FROM `$this->_table_img_optming` WHERE id IN ( " . implode(',', array_fill(0, count($notified_data), '%d')) . ' ) ';
			$wpdb->query($wpdb->prepare($q, $notified_data));
		}

		return Cloud::ok(array('count' => count($notified_data)));
	}

	/**
	 * Cron start async req
	 *
	 * @since 5.5
	 */
	public static function start_async_cron()
	{
		Task::async_call('imgoptm');
	}

	/**
	 * Manually start async req
	 *
	 * @since 5.5
	 */
	public static function start_async()
	{
		Task::async_call('imgoptm_force');

		$msg = __('Started async image optimization request', 'litespeed-cache');
		Admin_Display::success($msg);
	}

	/**
	 * Ajax req handler
	 *
	 * @since 5.5
	 */
	public static function async_handler($force = false)
	{
		self::debug('------------async-------------start_async_handler');

		$tag = self::get_option(self::DB_NEED_PULL);
		if (!$tag || $tag != self::STATUS_NOTIFIED) {
			self::debug('❌ no need pull [tag] ' . $tag);
			return;
		}

		self::cls()->pull($force);
	}

	/**
	 * Calculate pull theads
	 *
	 * @since  5.8
	 * @access private
	 */
	private function _calc_pull_threads()
	{
		global $wpdb;

		// Tune number of images per request based on number of images waiting and cloud packages
		$imgs_per_req = 1; // base 1, ramp up to ~50 max

		// Ramp up the request rate based on how many images are waiting
		$c = "SELECT count(id) FROM `$this->_table_img_optming` WHERE optm_status = %d";
		$_c = $wpdb->prepare($c, array(self::STATUS_NOTIFIED));
		$images_waiting = $wpdb->get_var($_c);
		if ($images_waiting && $images_waiting > 0) {
			$imgs_per_req = ceil($images_waiting / 1000); //ie. download 5/request if 5000 images are waiting
		}

		// Increase the request rate if the user has purchased addon packages
		$has_jumbo_pkg = Cloud::cls()->has_pkg(Cloud::SVC_IMG_OPTM, Cloud::BM_IMG_OPTM_JUMBO_GROUP);
		$has_prio_pkg = Cloud::cls()->has_pkg(Cloud::SVC_IMG_OPTM, Cloud::BM_IMG_OPTM_PRIO);

		if ($has_jumbo_pkg) {
			self::debug('Jumbo package detected.');
			$imgs_per_req += self::JUMBO_REQUEST_BONUS;
		}
		if ($has_prio_pkg) {
			self::debug('Priority Line package detected.');
			$imgs_per_req += self::PRIO_REQUEST_BONUS;
		}

		// Cap the request rate at 50 images per request
		$imgs_per_req = min(50, $imgs_per_req);

		self::debug('Pulling images at rate: ' . $imgs_per_req . ' Images per request.');

		return $imgs_per_req;
	}

	/**
	 * Pull optimized img
	 *
	 * @since  1.6
	 * @access public
	 */
	public function pull($manual = false)
	{
		global $wpdb;
		$timeoutLimit = ini_get('max_execution_time');
		$endts = time() + $timeoutLimit;

		self::debug('' . ($manual ? 'Manually' : 'Cron') . ' pull started [timeout: ' . $timeoutLimit . 's]');

		if ($this->cron_running()) {
			self::debug('Pull cron is running');

			$msg = __('Pull Cron is running', 'litespeed-cache');
			Admin_Display::note($msg);
			return;
		}

		$this->_summary['last_pulled'] = time();
		$this->_summary['last_pulled_by_cron'] = !$manual;
		self::save_summary();

		$imgs_per_req = $this->_calc_pull_threads();
		$q = "SELECT * FROM `$this->_table_img_optming` WHERE optm_status = %d ORDER BY id LIMIT %d";
		$_q = $wpdb->prepare($q, array(self::STATUS_NOTIFIED, $imgs_per_req));

		$rm_ori_bkup = $this->conf(self::O_IMG_OPTM_RM_BKUP);

		$total_pulled_ori = 0;
		$total_pulled_webp = 0;

		$server_list = array();

		try {
			while ($img_rows = $wpdb->get_results($_q)) {
				self::debug('timeout left: ' . ($endts - time()) . 's');
				if (function_exists('set_time_limit')) {
					$endts += 600;
					self::debug('Endtime extended to ' . date('Ymd H:i:s', $endts));
					set_time_limit(600); // This will be no more important as we use noabort now
				}
				// Disabled as we use noabort
				// if ($endts - time() < 10) {
				// 	self::debug("🚨 End loop due to timeout limit reached " . $timeoutLimit . "s");
				// 	break;
				// }

				/**
				 * Update cron timestamp to avoid duplicated running
				 * @since  1.6.2
				 */
				$this->_update_cron_running();

				// Run requests in parallel
				$requests = array(); // store each request URL for Requests::request_multiple()
				$imgs_by_req = array(); // store original request data so that we can reference it in the response
				$req_counter = 0;
				foreach ($img_rows as $row_img) {
					// request original image
					$server_info = \json_decode($row_img->server_info, true);
					if (!empty($server_info['ori'])) {
						$image_url = $server_info['server'] . '/' . $server_info['ori'];
						self::debug('Queueing pull: ' . $image_url);
						$requests[$req_counter] = array(
							'url' => $image_url,
							'type' => 'GET',
						);
						$imgs_by_req[$req_counter++] = array(
							'type' => 'ori',
							'data' => $row_img,
						);
					}

					// request webp image
					$webp_size = 0;
					if (!empty($server_info['webp'])) {
						$image_url = $server_info['server'] . '/' . $server_info['webp'];
						self::debug('Queueing pull WebP: ' . $image_url);
						$requests[$req_counter] = array(
							'url' => $image_url,
							'type' => 'GET',
						);
						$imgs_by_req[$req_counter++] = array(
							'type' => 'webp',
							'data' => $row_img,
						);
					}
				}
				self::debug('Loaded images count: ' . $req_counter);

				$complete_action = function ($response, $req_count) use ($imgs_by_req, $rm_ori_bkup, &$total_pulled_ori, &$total_pulled_webp, &$server_list) {
					global $wpdb;
					$row_data = isset($imgs_by_req[$req_count]) ? $imgs_by_req[$req_count] : false;
					if (false === $row_data) {
						self::debug('❌ failed to pull image: Request not found in lookup variable.');
						return;
					}
					$row_type = isset($row_data['type']) ? $row_data['type'] : 'ori';
					$row_img = $row_data['data'];
					$local_file = $this->wp_upload_dir['basedir'] . '/' . $row_img->src;
					$server_info = \json_decode($row_img->server_info, true);

					if (empty($response->success)) {
						if (!empty($response->status_code) && 404 == $response->status_code) {
							$this->_step_back_image($row_img->id);

							$msg = __('Some optimized image file(s) has expired and was cleared.', 'litespeed-cache');
							Admin_Display::error($msg);
							return;
						} else {
							// handle error
							$image_url = $server_info['server'] . '/' . $server_info[$row_type];
							self::debug(
								'❌ failed to pull image (' .
									$row_type .
									'): ' .
									(!empty($response->status_code) ? $response->status_code : '') .
									' [Local: ' .
									$row_img->src .
									'] / [remote: ' .
									$image_url .
									']'
							);
							throw new \Exception('Failed to pull image ' . (!empty($response->status_code) ? $response->status_code : '') . ' [url] ' . $image_url);
							return;
						}
					}
					// Handle wp_remote_get 404 as its success=true
					if (!empty($response->status_code)) {
						if ($response->status_code == 404) {
							$this->_step_back_image($row_img->id);

							$msg = __('Some optimized image file(s) has expired and was cleared.', 'litespeed-cache');
							Admin_Display::error($msg);
							return;
						}
						// Note: if there is other error status code found in future, handle here
					}

					if ('webp' === $row_type) {
						file_put_contents($local_file . '.webp', $response->body);

						if (!file_exists($local_file . '.webp') || !filesize($local_file . '.webp') || md5_file($local_file . '.webp') !== $server_info['webp_md5']) {
							self::debug('❌ Failed to pull optimized webp img: file md5 mismatch, server md5: ' . $server_info['webp_md5']);

							// Delete working table
							$q = "DELETE FROM `$this->_table_img_optming` WHERE id = %d ";
							$wpdb->query($wpdb->prepare($q, $row_img->id));

							$msg = __('Pulled WebP image md5 does not match the notified WebP image md5.', 'litespeed-cache');
							Admin_Display::error($msg);
							return;
						}

						self::debug('Pulled optimized img WebP: ' . $local_file . '.webp');

						$webp_size = filesize($local_file . '.webp');

						/**
						 * API for WebP
						 * @since 2.9.5
						 * @since  3.0 $row_img less elements (see above one)
						 * @see #751737  - API docs for WEBP generation
						 */
						do_action('litespeed_img_pull_webp', $row_img, $local_file . '.webp');

						$total_pulled_webp++;
					} else {
						// "ori" image type
						file_put_contents($local_file . '.tmp', $response->body);

						if (!file_exists($local_file . '.tmp') || !filesize($local_file . '.tmp') || md5_file($local_file . '.tmp') !== $server_info['ori_md5']) {
							self::debug(
								'❌ Failed to pull optimized img: file md5 mismatch [url] ' .
									$server_info['server'] .
									'/' .
									$server_info['ori'] .
									' [server_md5] ' .
									$server_info['ori_md5']
							);

							// Delete working table
							$q = "DELETE FROM `$this->_table_img_optming` WHERE id = %d ";
							$wpdb->query($wpdb->prepare($q, $row_img->id));

							$msg = __('One or more pulled images does not match with the notified image md5', 'litespeed-cache');
							Admin_Display::error($msg);
							return;
						}

						// Backup ori img
						if (!$rm_ori_bkup) {
							$extension = pathinfo($local_file, PATHINFO_EXTENSION);
							$bk_file = substr($local_file, 0, -strlen($extension)) . 'bk.' . $extension;
							file_exists($local_file) && rename($local_file, $bk_file);
						}

						// Replace ori img
						rename($local_file . '.tmp', $local_file);

						self::debug('Pulled optimized img: ' . $local_file);

						/**
						 * API Hook
						 * @since  2.9.5
						 * @since  3.0 $row_img has less elements now. Most useful ones are `post_id`/`src`
						 */
						do_action('litespeed_img_pull_ori', $row_img, $local_file);

						self::debug2('Remove _table_img_optming record [id] ' . $row_img->id);
					}

					// Delete working table
					$q = "DELETE FROM `$this->_table_img_optming` WHERE id = %d ";
					$wpdb->query($wpdb->prepare($q, $row_img->id));

					// Save server_list to notify taken
					if (empty($server_list[$server_info['server']])) {
						$server_list[$server_info['server']] = array();
					}

					$server_info_id = !empty($server_info['file_id']) ? $server_info['file_id'] : $server_info['id'];
					$server_list[$server_info['server']][] = $server_info_id;

					$total_pulled_ori++;
				};

				if (class_exists('\WpOrg\Requests\Requests') && class_exists('\WpOrg\Requests\Autoload') && version_compare(PHP_VERSION, '5.6.0', '>=')) {
					// Make sure Requests can load internal classes.
					Autoload::register();

					// Run pull requests in parallel
					Requests::request_multiple($requests, array(
						'timeout' => 60,
						'connect_timeout' => 60,
						'complete' => $complete_action,
					));
				} else {
					foreach ($requests as $cnt => $req) {
						$wp_response = wp_remote_get($req['url'], array('timeout' => 60));
						$request_response = array(
							'success' => false,
							'status_code' => 0,
							'body' => null,
						);
						if (is_wp_error($wp_response)) {
							$error_message = $wp_response->get_error_message();
							self::debug('❌ failed to pull image: ' . $error_message);
						} else {
							$request_response['success'] = true;
							$request_response['status_code'] = $wp_response['response']['code'];
							$request_response['body'] = $wp_response['body'];
						}
						self::debug('response code [code] ' . $wp_response['response']['code'] . ' [url] ' . $req['url']);

						$request_response = (object) $request_response;

						$complete_action($request_response, $cnt);
					}
				}
				self::debug('Current batch pull finished');
			}
		} catch (\Exception $e) {
			Admin_Display::error('Image pull process failure: ' . $e->getMessage());
		}

		// Notify IAPI images taken
		foreach ($server_list as $server => $img_list) {
			$data = array(
				'action' => self::CLOUD_ACTION_TAKEN,
				'list' => $img_list,
				'server' => $server,
			);
			// TODO: improve this so we do not call once per server, but just once and then filter on the server side
			Cloud::post(Cloud::SVC_IMG_OPTM, $data);
		}

		if (empty($this->_summary['img_taken'])) {
			$this->_summary['img_taken'] = 0;
		}
		$this->_summary['img_taken'] += $total_pulled_ori + $total_pulled_webp;
		self::save_summary();

		// Manually running needs to roll back timestamp for next running
		if ($manual) {
			$this->_update_cron_running(true);
		}

		// $msg = sprintf(__('Pulled %d image(s)', 'litespeed-cache'), $total_pulled_ori + $total_pulled_webp);
		// Admin_Display::succeed($msg);

		// Check if there is still task in queue
		$q = "SELECT * FROM `$this->_table_img_optming` WHERE optm_status = %d LIMIT 1";
		$to_be_continued = $wpdb->get_row($wpdb->prepare($q, self::STATUS_NOTIFIED));
		if ($to_be_continued) {
			self::debug('Task in queue, to be continued...');
			return;
			// return Router::self_redirect(Router::ACTION_IMG_OPTM, self::TYPE_PULL);
		}

		// If all pulled, update tag to done
		self::debug('Marked pull status to all pulled');
		self::update_option(self::DB_NEED_PULL, self::STATUS_PULLED);
	}

	/**
	 * Push image back to previous status
	 *
	 * @since  3.0
	 * @access private
	 */
	private function _step_back_image($id)
	{
		global $wpdb;

		self::debug('Push image back to new status [id] ' . $id);

		// Reset the image to gathered status
		$q = "UPDATE `$this->_table_img_optming` SET optm_status = %d WHERE id = %d ";
		$wpdb->query($wpdb->prepare($q, array(self::STATUS_RAW, $id)));
	}

	/**
	 * Parse wp's meta value
	 *
	 * @since 1.6.7
	 * @access private
	 */
	private function _parse_wp_meta_value($v)
	{
		if (empty($v)) {
			self::debug('bypassed parsing meta due to null value');
			return false;
		}

		if (!$v->meta_value) {
			self::debug('bypassed parsing meta due to no meta_value: pid ' . $v->post_id);
			return false;
		}

		$meta_value = @maybe_unserialize($v->meta_value);
		if (!is_array($meta_value)) {
			self::debug('bypassed parsing meta due to meta_value not json: pid ' . $v->post_id);
			return false;
		}

		if (empty($meta_value['file'])) {
			self::debug('bypassed parsing meta due to no ori file: pid ' . $v->post_id);
			return false;
		}

		return $meta_value;
	}

	/**
	 * Clean up all unfinished queue locally and to Cloud server
	 *
	 * @since 2.1.2
	 * @access public
	 */
	public function clean()
	{
		global $wpdb;

		// Reset img_optm table's queue
		if ($this->__data->tb_exist('img_optming')) {
			// Get min post id to mark
			$q = "SELECT MIN(post_id) FROM `$this->_table_img_optming`";
			$min_pid = $wpdb->get_var($q) - 1;
			if ($this->_summary['next_post_id'] > $min_pid) {
				$this->_summary['next_post_id'] = $min_pid;
				self::save_summary();
			}

			$q = "DELETE FROM `$this->_table_img_optming`";
			$wpdb->query($q);
		}

		$msg = __('Cleaned up unfinished data successfully.', 'litespeed-cache');
		Admin_Display::succeed($msg);
	}

	/**
	 * Destroy all optimized images
	 *
	 * @since 3.0
	 * @access private
	 */
	private function _destroy()
	{
		global $wpdb;

		self::debug('executing DESTROY process');

		$offset = !empty($_GET['litespeed_i']) ? $_GET['litespeed_i'] : 0;
		/**
		 * Limit images each time before redirection to fix Out of memory issue. #665465
		 * @since  2.9.8
		 */
		// Start deleting files
		$limit = apply_filters('litespeed_imgoptm_destroy_max_rows', 500);

		$img_q = "SELECT b.post_id, b.meta_value
			FROM `$wpdb->posts` a
			LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID
			WHERE b.meta_key = '_wp_attachment_metadata'
				AND a.post_type = 'attachment'
				AND a.post_status = 'inherit'
				AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif')
			ORDER BY a.ID
			LIMIT %d,%d
			";
		$q = $wpdb->prepare($img_q, array($offset * $limit, $limit));
		$list = $wpdb->get_results($q);
		$i = 0;
		foreach ($list as $v) {
			if (!$v->post_id) {
				continue;
			}

			$meta_value = $this->_parse_wp_meta_value($v);
			if (!$meta_value) {
				continue;
			}

			$i++;

			$this->tmp_pid = $v->post_id;
			$this->tmp_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/';
			$this->_destroy_optm_file($meta_value, true);
			if (!empty($meta_value['sizes'])) {
				array_map(array($this, '_destroy_optm_file'), $meta_value['sizes']);
			}
		}

		self::debug('batch switched images total: ' . $i);

		$offset++;
		$to_be_continued = $wpdb->get_row($wpdb->prepare($img_q, array($offset * $limit, 1)));
		if ($to_be_continued) {
			# Check if post_id is beyond next_post_id
			self::debug('[next_post_id] ' . $this->_summary['next_post_id'] . ' [cursor post id] ' . $to_be_continued->post_id);
			if ($to_be_continued->post_id <= $this->_summary['next_post_id']) {
				self::debug('redirecting to next');
				return Router::self_redirect(Router::ACTION_IMG_OPTM, self::TYPE_DESTROY);
			}
			self::debug('🎊 Finished destroying');
		}

		// Delete postmeta info
		$q = "DELETE FROM `$wpdb->postmeta` WHERE meta_key = %s";
		$wpdb->query($wpdb->prepare($q, self::DB_SIZE));
		$wpdb->query($wpdb->prepare($q, self::DB_SET));

		// Delete img_optm table
		$this->__data->tb_del('img_optm');
		$this->__data->tb_del('img_optming');

		// Clear options table summary info
		self::delete_option('_summary');
		self::delete_option(self::DB_NEED_PULL);

		$msg = __('Destroy all optimization data successfully.', 'litespeed-cache');
		Admin_Display::succeed($msg);
	}

	/**
	 * Destroy optm file
	 */
	private function _destroy_optm_file($meta_value, $is_ori_file = false)
	{
		$short_file_path = $meta_value['file'];
		if (!$is_ori_file) {
			$short_file_path = $this->tmp_path . $short_file_path;
		}
		self::debug('deleting ' . $short_file_path);

		// del webp
		$this->__media->info($short_file_path . '.webp', $this->tmp_pid) && $this->__media->del($short_file_path . '.webp', $this->tmp_pid);
		$this->__media->info($short_file_path . '.optm.webp', $this->tmp_pid) && $this->__media->del($short_file_path . '.optm.webp', $this->tmp_pid);

		$extension = pathinfo($short_file_path, PATHINFO_EXTENSION);
		$local_filename = substr($short_file_path, 0, -strlen($extension) - 1);
		$bk_file = $local_filename . '.bk.' . $extension;
		$bk_optm_file = $local_filename . '.bk.optm.' . $extension;

		// del optimized ori
		if ($this->__media->info($bk_file, $this->tmp_pid)) {
			self::debug('deleting optim ori');
			$this->__media->del($short_file_path, $this->tmp_pid);
			$this->__media->rename($bk_file, $short_file_path, $this->tmp_pid);
		}
		$this->__media->info($bk_optm_file, $this->tmp_pid) && $this->__media->del($bk_optm_file, $this->tmp_pid);
	}

	/**
	 * Rescan to find new generated images
	 *
	 * @since 1.6.7
	 * @access private
	 */
	private function _rescan()
	{
		global $wpdb;
		exit('tobedone');

		$offset = !empty($_GET['litespeed_i']) ? $_GET['litespeed_i'] : 0;
		$limit = 500;

		self::debug('rescan images');

		// Get images
		$q = "SELECT b.post_id, b.meta_value
			FROM `$wpdb->posts` a, `$wpdb->postmeta` b
			WHERE a.post_type = 'attachment'
				AND a.post_status = 'inherit'
				AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif')
				AND a.ID = b.post_id
				AND b.meta_key = '_wp_attachment_metadata'
			ORDER BY a.ID
			LIMIT %d, %d
			";
		$list = $wpdb->get_results($wpdb->prepare($q, $offset * $limit, $limit + 1)); // last one is the seed for next batch

		if (!$list) {
			$msg = __('Rescanned successfully.', 'litespeed-cache');
			Admin_Display::succeed($msg);

			self::debug('rescan bypass: no gathered image found');
			return;
		}

		if (count($list) == $limit + 1) {
			$to_be_continued = true;
			array_pop($list); // last one is the seed for next round, discard here.
		} else {
			$to_be_continued = false;
		}

		// Prepare post_ids to inquery gathered images
		$pid_set = array();
		$scanned_list = array();
		foreach ($list as $v) {
			$meta_value = $this->_parse_wp_meta_value($v);
			if (!$meta_value) {
				continue;
			}

			$scanned_list[] = array(
				'pid' => $v->post_id,
				'meta' => $meta_value,
			);

			$pid_set[] = $v->post_id;
		}

		// Build gathered images
		$q = "SELECT src, post_id FROM `$this->_table_img_optm` WHERE post_id IN (" . implode(',', array_fill(0, count($pid_set), '%d')) . ')';
		$list = $wpdb->get_results($wpdb->prepare($q, $pid_set));
		foreach ($list as $v) {
			$this->_existed_src_list[] = $v->post_id . '.' . $v->src;
		}

		// Find new images
		foreach ($scanned_list as $v) {
			$meta_value = $v['meta'];
			// Parse all child src and put them into $this->_img_in_queue, missing ones to $this->_img_in_queue_missed
			$this->tmp_pid = $v['pid'];
			$this->tmp_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/';
			$this->_append_img_queue($meta_value, true);
			if (!empty($meta_value['sizes'])) {
				array_map(array($this, '_append_img_queue'), $meta_value['sizes']);
			}
		}

		self::debug('rescanned [img] ' . count($this->_img_in_queue));

		$count = count($this->_img_in_queue);
		if ($count > 0) {
			// Save to DB
			$this->_save_raw();
		}

		if ($to_be_continued) {
			return Router::self_redirect(Router::ACTION_IMG_OPTM, self::TYPE_RESCAN);
		}

		$msg = $count ? sprintf(__('Rescanned %d images successfully.', 'litespeed-cache'), $count) : __('Rescanned successfully.', 'litespeed-cache');
		Admin_Display::succeed($msg);
	}

	/**
	 * Calculate bkup original images storage
	 *
	 * @since 2.2.6
	 * @access private
	 */
	private function _calc_bkup()
	{
		global $wpdb;

		$offset = !empty($_GET['litespeed_i']) ? $_GET['litespeed_i'] : 0;
		$limit = 500;

		if (!$offset) {
			$this->_summary['bk_summary'] = array(
				'date' => time(),
				'count' => 0,
				'sum' => 0,
			);
		}

		$img_q = "SELECT b.post_id, b.meta_value
			FROM `$wpdb->posts` a
			LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID
			WHERE b.meta_key = '_wp_attachment_metadata'
				AND a.post_type = 'attachment'
				AND a.post_status = 'inherit'
				AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif')
			ORDER BY a.ID
			LIMIT %d,%d
			";
		$q = $wpdb->prepare($img_q, array($offset * $limit, $limit));
		$list = $wpdb->get_results($q);
		foreach ($list as $v) {
			if (!$v->post_id) {
				continue;
			}

			$meta_value = $this->_parse_wp_meta_value($v);
			if (!$meta_value) {
				continue;
			}

			$this->tmp_pid = $v->post_id;
			$this->tmp_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/';
			$this->_get_bk_size($meta_value, true);
			if (!empty($meta_value['sizes'])) {
				array_map(array($this, '_get_bk_size'), $meta_value['sizes']);
			}
		}

		$this->_summary['bk_summary']['date'] = time();
		self::save_summary();

		self::debug('_calc_bkup total: ' . $this->_summary['bk_summary']['count'] . ' [size] ' . $this->_summary['bk_summary']['sum']);

		$offset++;
		$to_be_continued = $wpdb->get_row($wpdb->prepare($img_q, array($offset * $limit, 1)));

		if ($to_be_continued) {
			return Router::self_redirect(Router::ACTION_IMG_OPTM, self::TYPE_CALC_BKUP);
		}

		$msg = __('Calculated backups successfully.', 'litespeed-cache');
		Admin_Display::succeed($msg);
	}

	/**
	 * Calculate single size
	 */
	private function _get_bk_size($meta_value, $is_ori_file = false)
	{
		$short_file_path = $meta_value['file'];
		if (!$is_ori_file) {
			$short_file_path = $this->tmp_path . $short_file_path;
		}

		$extension = pathinfo($short_file_path, PATHINFO_EXTENSION);
		$local_filename = substr($short_file_path, 0, -strlen($extension) - 1);
		$bk_file = $local_filename . '.bk.' . $extension;

		$img_info = $this->__media->info($bk_file, $this->tmp_pid);
		if (!$img_info) {
			return;
		}

		$this->_summary['bk_summary']['count']++;
		$this->_summary['bk_summary']['sum'] += $img_info['size'];
	}

	/**
	 * Delete bkup original images storage
	 *
	 * @since  2.5
	 * @access public
	 */
	public function rm_bkup()
	{
		global $wpdb;

		if (!$this->__data->tb_exist('img_optming')) {
			return;
		}

		$offset = !empty($_GET['litespeed_i']) ? $_GET['litespeed_i'] : 0;
		$limit = 500;

		if (empty($this->_summary['rmbk_summary'])) {
			$this->_summary['rmbk_summary'] = array(
				'date' => time(),
				'count' => 0,
				'sum' => 0,
			);
		}

		$img_q = "SELECT b.post_id, b.meta_value
			FROM `$wpdb->posts` a
			LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID
			WHERE b.meta_key = '_wp_attachment_metadata'
				AND a.post_type = 'attachment'
				AND a.post_status = 'inherit'
				AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif')
			ORDER BY a.ID
			LIMIT %d,%d
			";
		$q = $wpdb->prepare($img_q, array($offset * $limit, $limit));
		$list = $wpdb->get_results($q);
		foreach ($list as $v) {
			if (!$v->post_id) {
				continue;
			}

			$meta_value = $this->_parse_wp_meta_value($v);
			if (!$meta_value) {
				continue;
			}

			$this->tmp_pid = $v->post_id;
			$this->tmp_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/';
			$this->_del_bk_file($meta_value, true);
			if (!empty($meta_value['sizes'])) {
				array_map(array($this, '_del_bk_file'), $meta_value['sizes']);
			}
		}

		$this->_summary['rmbk_summary']['date'] = time();
		self::save_summary();

		self::debug('rm_bkup total: ' . $this->_summary['rmbk_summary']['count'] . ' [size] ' . $this->_summary['rmbk_summary']['sum']);

		$offset++;
		$to_be_continued = $wpdb->get_row($wpdb->prepare($img_q, array($offset * $limit, 1)));

		if ($to_be_continued) {
			return Router::self_redirect(Router::ACTION_IMG_OPTM, self::TYPE_RM_BKUP);
		}

		$msg = __('Removed backups successfully.', 'litespeed-cache');
		Admin_Display::succeed($msg);
	}

	/**
	 * Delete single file
	 */
	private function _del_bk_file($meta_value, $is_ori_file = false)
	{
		$short_file_path = $meta_value['file'];
		if (!$is_ori_file) {
			$short_file_path = $this->tmp_path . $short_file_path;
		}

		$extension = pathinfo($short_file_path, PATHINFO_EXTENSION);
		$local_filename = substr($short_file_path, 0, -strlen($extension) - 1);
		$bk_file = $local_filename . '.bk.' . $extension;

		$img_info = $this->__media->info($bk_file, $this->tmp_pid);
		if (!$img_info) {
			return;
		}

		$this->_summary['rmbk_summary']['count']++;
		$this->_summary['rmbk_summary']['sum'] += $img_info['size'];

		$this->__media->del($bk_file, $this->tmp_pid);
	}

	/**
	 * Count images
	 *
	 * @since 1.6
	 * @access public
	 */
	public function img_count()
	{
		global $wpdb;

		$q = "SELECT count(*)
			FROM `$wpdb->posts` a
			LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID
			WHERE b.meta_key = '_wp_attachment_metadata'
				AND a.post_type = 'attachment'
				AND a.post_status = 'inherit'
				AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif')
			";
		$groups_all = $wpdb->get_var($q);
		$groups_new = $wpdb->get_var($q . ' AND ID>' . (int) $this->_summary['next_post_id'] . ' ORDER BY ID');
		$groups_done = $wpdb->get_var($q . ' AND ID<=' . (int) $this->_summary['next_post_id'] . ' ORDER BY ID');

		$q = "SELECT b.post_id
			FROM `$wpdb->posts` a
			LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID
			WHERE b.meta_key = '_wp_attachment_metadata'
				AND a.post_type = 'attachment'
				AND a.post_status = 'inherit'
				AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif')
			ORDER BY a.ID DESC
			LIMIT 1
			";
		$max_id = $wpdb->get_var($q);

		$count_list = array(
			'max_id' => $max_id,
			'groups_all' => $groups_all,
			'groups_new' => $groups_new,
			'groups_done' => $groups_done,
		);

		// images count from work table
		if ($this->__data->tb_exist('img_optming')) {
			$q = "SELECT COUNT(DISTINCT post_id),COUNT(*) FROM `$this->_table_img_optming` WHERE optm_status = %d";
			$groups_to_check = array(self::STATUS_RAW, self::STATUS_REQUESTED, self::STATUS_NOTIFIED, self::STATUS_ERR_FETCH);
			foreach ($groups_to_check as $v) {
				$count_list['img.' . $v] = $count_list['group.' . $v] = 0;
				list($count_list['group.' . $v], $count_list['img.' . $v]) = $wpdb->get_row($wpdb->prepare($q, $v), ARRAY_N);
			}
		}

		return $count_list;
	}

	/**
	 * Check if fetch cron is running
	 *
	 * @since  1.6.2
	 * @access public
	 */
	public function cron_running($bool_res = true)
	{
		$last_run = !empty($this->_summary['last_pull']) ? $this->_summary['last_pull'] : 0;

		$is_running = $last_run && time() - $last_run < 120;

		if ($bool_res) {
			return $is_running;
		}

		return array($last_run, $is_running);
	}

	/**
	 * Update fetch cron timestamp tag
	 *
	 * @since  1.6.2
	 * @access private
	 */
	private function _update_cron_running($done = false)
	{
		$this->_summary['last_pull'] = time();

		if ($done) {
			// Only update cron tag when its from the active running cron
			if ($this->_cron_ran) {
				// Rollback for next running
				$this->_summary['last_pull'] -= 120;
			} else {
				return;
			}
		}

		self::save_summary();

		$this->_cron_ran = true;
	}

	/**
	 * Batch switch images to ori/optm version
	 *
	 * @since  1.6.2
	 * @access public
	 */
	public function batch_switch($type)
	{
		global $wpdb;

		if (defined('LITESPEED_CLI') || defined('DOING_CRON')) {
			$offset = 0;
			while ($offset !== 'done') {
				Admin_Display::info("Starting switch to $type [offset] $offset");
				$offset = $this->_batch_switch($type, $offset);
			}
		} else {
			$offset = !empty($_GET['litespeed_i']) ? $_GET['litespeed_i'] : 0;

			$newOffset = $this->_batch_switch($type, $offset);
			if ($newOffset !== 'done') {
				return Router::self_redirect(Router::ACTION_IMG_OPTM, $type);
			}
		}

		$msg = __('Switched images successfully.', 'litespeed-cache');
		Admin_Display::succeed($msg);
	}

	/**
	 * Switch images per offset
	 */
	private function _batch_switch($type, $offset)
	{
		global $wpdb;
		$limit = 500;
		$this->tmp_type = $type;

		$img_q = "SELECT b.post_id, b.meta_value
			FROM `$wpdb->posts` a
			LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID
			WHERE b.meta_key = '_wp_attachment_metadata'
				AND a.post_type = 'attachment'
				AND a.post_status = 'inherit'
				AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif')
			ORDER BY a.ID
			LIMIT %d,%d
			";
		$q = $wpdb->prepare($img_q, array($offset * $limit, $limit));
		$list = $wpdb->get_results($q);
		$i = 0;
		foreach ($list as $v) {
			if (!$v->post_id) {
				continue;
			}

			$meta_value = $this->_parse_wp_meta_value($v);
			if (!$meta_value) {
				continue;
			}

			$i++;

			$this->tmp_pid = $v->post_id;
			$this->tmp_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/';
			$this->_switch_bk_file($meta_value, true);
			if (!empty($meta_value['sizes'])) {
				array_map(array($this, '_switch_bk_file'), $meta_value['sizes']);
			}
		}

		self::debug('batch switched images total: ' . $i . ' [type] ' . $type);

		$offset++;
		$to_be_continued = $wpdb->get_row($wpdb->prepare($img_q, array($offset * $limit, 1)));
		if ($to_be_continued) {
			return $offset;
		}
		return 'done';
	}

	/**
	 * Delete single file
	 */
	private function _switch_bk_file($meta_value, $is_ori_file = false)
	{
		$short_file_path = $meta_value['file'];
		if (!$is_ori_file) {
			$short_file_path = $this->tmp_path . $short_file_path;
		}

		$extension = pathinfo($short_file_path, PATHINFO_EXTENSION);
		$local_filename = substr($short_file_path, 0, -strlen($extension) - 1);
		$bk_file = $local_filename . '.bk.' . $extension;
		$bk_optm_file = $local_filename . '.bk.optm.' . $extension;

		// self::debug('_switch_bk_file ' . $bk_file . ' [type] ' . $this->tmp_type);
		// switch to ori
		if ($this->tmp_type === self::TYPE_BATCH_SWITCH_ORI || $this->tmp_type == 'orig') {
			// self::debug('switch to orig ' . $bk_file);
			if (!$this->__media->info($bk_file, $this->tmp_pid)) {
				return;
			}
			$this->__media->rename($local_filename . '.' . $extension, $bk_optm_file, $this->tmp_pid);
			$this->__media->rename($bk_file, $local_filename . '.' . $extension, $this->tmp_pid);
		}
		// switch to optm
		elseif ($this->tmp_type === self::TYPE_BATCH_SWITCH_OPTM || $this->tmp_type == 'optm') {
			// self::debug('switch to optm ' . $bk_file);
			if (!$this->__media->info($bk_optm_file, $this->tmp_pid)) {
				return;
			}
			$this->__media->rename($local_filename . '.' . $extension, $bk_file, $this->tmp_pid);
			$this->__media->rename($bk_optm_file, $local_filename . '.' . $extension, $this->tmp_pid);
		}
	}

	/**
	 * Switch image between original one and optimized one
	 *
	 * @since 1.6.2
	 * @access private
	 */
	private function _switch_optm_file($type)
	{
		Admin_Display::succeed(__('Switched to optimized file successfully.', 'litespeed-cache'));
		return;
		global $wpdb;

		$pid = substr($type, 4);
		$switch_type = substr($type, 0, 4);

		$q = "SELECT src,post_id FROM `$this->_table_img_optm` WHERE post_id = %d AND optm_status = %d";
		$list = $wpdb->get_results($wpdb->prepare($q, array($pid, self::STATUS_PULLED)));

		$msg = 'Unknown Msg';

		foreach ($list as $v) {
			// to switch webp file
			if ($switch_type === 'webp') {
				if ($this->__media->info($v->src . '.webp', $v->post_id)) {
					$this->__media->rename($v->src . '.webp', $v->src . '.optm.webp', $v->post_id);
					self::debug('Disabled WebP: ' . $v->src);

					$msg = __('Disabled WebP file successfully.', 'litespeed-cache');
				} elseif ($this->__media->info($v->src . '.optm.webp', $v->post_id)) {
					$this->__media->rename($v->src . '.optm.webp', $v->src . '.webp', $v->post_id);
					self::debug('Enable WebP: ' . $v->src);

					$msg = __('Enabled WebP file successfully.', 'litespeed-cache');
				}
			}
			// to switch original file
			else {
				$extension = pathinfo($v->src, PATHINFO_EXTENSION);
				$local_filename = substr($v->src, 0, -strlen($extension) - 1);
				$bk_file = $local_filename . '.bk.' . $extension;
				$bk_optm_file = $local_filename . '.bk.optm.' . $extension;

				// revert ori back
				if ($this->__media->info($bk_file, $v->post_id)) {
					$this->__media->rename($v->src, $bk_optm_file, $v->post_id);
					$this->__media->rename($bk_file, $v->src, $v->post_id);
					self::debug('Restore original img: ' . $bk_file);

					$msg = __('Restored original file successfully.', 'litespeed-cache');
				} elseif ($this->__media->info($bk_optm_file, $v->post_id)) {
					$this->__media->rename($v->src, $bk_file, $v->post_id);
					$this->__media->rename($bk_optm_file, $v->src, $v->post_id);
					self::debug('Switch to optm img: ' . $v->src);

					$msg = __('Switched to optimized file successfully.', 'litespeed-cache');
				}
			}
		}

		Admin_Display::succeed($msg);
	}

	/**
	 * Delete one optm data and recover original file
	 *
	 * @since 2.4.2
	 * @access public
	 */
	public function reset_row($post_id)
	{
		global $wpdb;

		if (!$post_id) {
			return;
		}

		// Gathered image don't have DB_SIZE info yet
		// $size_meta = get_post_meta( $post_id, self::DB_SIZE, true );

		// if ( ! $size_meta ) {
		// 	return;
		// }

		self::debug('_reset_row [pid] ' . $post_id);

		# TODO: Load image sub files
		$img_q = "SELECT b.post_id, b.meta_value
			FROM `$wpdb->postmeta` b
			WHERE b.post_id =%d  AND b.meta_key = '_wp_attachment_metadata'";
		$q = $wpdb->prepare($img_q, array($post_id));
		$v = $wpdb->get_row($q);

		$meta_value = $this->_parse_wp_meta_value($v);
		if ($meta_value) {
			$this->tmp_pid = $v->post_id;
			$this->tmp_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/';
			$this->_destroy_optm_file($meta_value, true);
			if (!empty($meta_value['sizes'])) {
				array_map(array($this, '_destroy_optm_file'), $meta_value['sizes']);
			}
		}

		delete_post_meta($post_id, self::DB_SIZE);
		delete_post_meta($post_id, self::DB_SET);

		$msg = __('Reset the optimized data successfully.', 'litespeed-cache');
		Admin_Display::succeed($msg);
	}

	/**
	 * Show an image's optm status
	 *
	 * @since  1.6.5
	 * @access public
	 */
	public function check_img()
	{
		global $wpdb;

		$pid = $_POST['data'];

		self::debug('Check image [ID] ' . $pid);

		$data = array();

		$data['img_count'] = $this->img_count();
		$data['optm_summary'] = self::get_summary();

		$data['_wp_attached_file'] = get_post_meta($pid, '_wp_attached_file', true);
		$data['_wp_attachment_metadata'] = get_post_meta($pid, '_wp_attachment_metadata', true);

		// Get img_optm data
		$q = "SELECT * FROM `$this->_table_img_optm` WHERE post_id = %d";
		$list = $wpdb->get_results($wpdb->prepare($q, $pid));
		$img_data = array();
		if ($list) {
			foreach ($list as $v) {
				$img_data[] = array(
					'id' => $v->id,
					'optm_status' => $v->optm_status,
					'src' => $v->src,
					'srcpath_md5' => $v->srcpath_md5,
					'src_md5' => $v->src_md5,
					'server_info' => $v->server_info,
				);
			}
		}
		$data['img_data'] = $img_data;

		return array('_res' => 'ok', 'data' => $data);
	}

	/**
	 * Handle all request actions from main cls
	 *
	 * @since  2.0
	 * @access public
	 */
	public function handler()
	{
		$type = Router::verify_type();

		switch ($type) {
			case self::TYPE_RESET_ROW:
				$this->reset_row(!empty($_GET['id']) ? $_GET['id'] : false);
				break;

			case self::TYPE_CALC_BKUP:
				$this->_calc_bkup();
				break;

			case self::TYPE_RM_BKUP:
				$this->rm_bkup();
				break;

			case self::TYPE_NEW_REQ:
				$this->new_req();
				break;

			case self::TYPE_RESCAN:
				$this->_rescan();
				break;

			case self::TYPE_DESTROY:
				$this->_destroy();
				break;

			case self::TYPE_CLEAN:
				$this->clean();
				break;

			case self::TYPE_PULL:
				self::start_async();
				break;

			case self::TYPE_BATCH_SWITCH_ORI:
			case self::TYPE_BATCH_SWITCH_OPTM:
				$this->batch_switch($type);
				break;

			case substr($type, 0, 4) === 'webp':
			case substr($type, 0, 4) === 'orig':
				$this->_switch_optm_file($type);
				break;

			default:
				break;
		}

		Admin::redirect();
	}
}

Youez - 2016 - github.com/yon3zu
LinuXploit