File: /home/muoapartman/www/wp-content/plugins/shortpixel-image-optimiser/shortpixel_api.php
<?php
if ( !function_exists( 'download_url' ) ) {
require_once( ABSPATH . 'wp-admin/includes/file.php' );
}
use ShortPixel\ShortpixelLogger\ShortPixelLogger as Log;
class ShortPixelAPI {
const STATUS_SUCCESS = 1;
const STATUS_UNCHANGED = 0;
const STATUS_ERROR = -1;
const STATUS_FAIL = -2;
const STATUS_QUOTA_EXCEEDED = -3;
const STATUS_SKIP = -4;
const STATUS_NOT_FOUND = -5;
const STATUS_NO_KEY = -6;
const STATUS_RETRY = -7;
const STATUS_SEARCHING = -8; // when the Queue is looping over images, but in batch none were found.
const STATUS_QUEUE_FULL = -404;
const STATUS_MAINTENANCE = -500;
const ERR_FILE_NOT_FOUND = -2;
const ERR_TIMEOUT = -3;
const ERR_SAVE = -4;
const ERR_SAVE_BKP = -5;
const ERR_INCORRECT_FILE_SIZE = -6;
const ERR_DOWNLOAD = -7;
const ERR_PNG2JPG_MEMORY = -8;
const ERR_POSTMETA_CORRUPT = -9;
const ERR_UNKNOWN = -999;
private $_settings;
private $_apiEndPoint;
private $_apiDumpEndPoint;
public function __construct($settings) {
$this->_settings = $settings;
$this->_apiEndPoint = $this->_settings->httpProto . '://' . SHORTPIXEL_API . '/v2/reducer.php';
$this->_apiDumpEndPoint = $this->_settings->httpProto . '://' . SHORTPIXEL_API . '/v2/cleanup.php';
}
protected function prepareRequest($requestParameters, $Blocking = false) {
$arguments = array(
'method' => 'POST',
'timeout' => 15,
'redirection' => 3,
'sslverify' => apply_filters('shortpixel/system/sslverify', true),
'httpversion' => '1.0',
'blocking' => $Blocking,
'headers' => array(),
'body' => json_encode($requestParameters),
'cookies' => array()
);
//die(var_dump($requestParameters));
//add this explicitely only for https, otherwise (for http) it slows down the request
if($this->_settings->httpProto !== 'https') {
unset($arguments['sslverify']);
}
return $arguments;
}
public function doDumpRequests($URLs) {
if(!count($URLs)) {
return false;
}
$ret = wp_remote_post($this->_apiDumpEndPoint, $this->prepareRequest(array(
'plugin_version' => SHORTPIXEL_IMAGE_OPTIMISER_VERSION,
'key' => $this->_settings->apiKey,
'urllist' => $URLs
) ) );
return $ret;
}
/**
* sends a compression request to the API
* @param array $URLs - list of urls to send to API
* @param Boolean $Blocking - true means it will wait for an answer
* @param ShortPixelMetaFacade $itemHandler - the Facade that manages different types of image metadatas: MediaLibrary (postmeta table), ShortPixel custom (shortpixel_meta table)
* @param bool|int $compressionType 1 - lossy, 2 - glossy, 0 - lossless
* @param bool $refresh
* @return Array response from wp_remote_post or error
* @throws Exception
*/
public function doRequests($URLs, $Blocking, $itemHandler, $compressionType = false, $refresh = false) {
if(!count($URLs)) {
$meta = $itemHandler->getMeta();
$thumbsMissing = $meta->getThumbsMissing();
if(is_array($thumbsMissing) && count($thumbsMissing)) {
$added = array();
$files = " (";
foreach ($meta->getThumbsMissing() as $miss) {
if(isset($added[$miss])) continue;
$files .= $miss . ", ";
$added[$miss] = true;
}
if(strrpos($files, ', ')) {
$files = substr_replace($files , ')', strrpos($files , ', '));
}
throw new Exception(__('Image files are missing.', 'shortpixel-image-optimiser') . (strlen($files) > 1 ? $files : ''));
}
else throw new Exception(__('Image files are missing.', 'shortpixel-image-optimiser'));
}
$apiKey = $this->_settings->apiKey;
if(strlen($apiKey) < 20) { //found in the logs many cases when the API Key is '', probably deleted from the DB but the verifiedKey setting is not changed
$this->_settings->verifiedKey = false;
Log::addWarn('Invalid API Key');
throw new Exception(__('Invalid API Key', 'shortpixel-image-optimiser'));
}
// This filter, moved to facade GetuRls/Paths
// $URLs = apply_filters('shortpixel_image_urls', $URLs, $itemHandler->getId()) ;
$convertTo = array();
if ($this->_settings->createWebp)
$convertTo[]= urlencode("+webp");
if ($this->_settings->createAvif)
$convertTo[] = urlencode('+avif');
if (count($convertTo) > 0)
$convertTo = implode('|', $convertTo);
else
$convertTo = '';
$requestParameters = array(
'plugin_version' => SHORTPIXEL_IMAGE_OPTIMISER_VERSION,
'key' => $apiKey,
'lossy' => $compressionType === false ? $this->_settings->compressionType : $compressionType,
'cmyk2rgb' => $this->_settings->CMYKtoRGBconversion,
'keep_exif' => ($this->_settings->keepExif ? "1" : "0"),
'convertto' => $convertTo,
'resize' => $this->_settings->resizeImages ? 1 + 2 * ($this->_settings->resizeType == 'inner' ? 1 : 0) : 0,
'resize_width' => $this->_settings->resizeWidth,
'resize_height' => $this->_settings->resizeHeight,
'urllist' => $URLs
);
if(/*false &&*/ $this->_settings->downloadArchive == 7 && class_exists('PharData')) {
$requestParameters['group'] = $itemHandler->getId();
}
if($refresh) {
$requestParameters['refresh'] = 1;
}
//WpShortPixel::log("ShortPixel API Request Settings: " . json_encode($requestParameters));
$response = wp_remote_post($this->_apiEndPoint, $this->prepareRequest($requestParameters, $Blocking) );
Log::addDebug('ShortPixel API Request sent', $requestParameters);
//WpShortPixel::log('RESPONSE: ' . json_encode($response));
//only if $Blocking is true analyze the response
if ( $Blocking )
{
if ( is_object($response) && get_class($response) == 'WP_Error' )
{
$errorMessage = $response->errors['http_request_failed'][0];
$errorCode = 503;
}
elseif ( isset($response['response']['code']) && $response['response']['code'] <> 200 )
{
$errorMessage = $response['response']['code'] . " - " . $response['response']['message'];
$errorCode = $response['response']['code'];
}
if ( isset($errorMessage) )
{//set details inside file so user can know what happened
$itemHandler->incrementRetries(1, $errorCode, $errorMessage);
return array("response" => array("code" => $errorCode, "message" => $errorMessage ));
}
return $response;//this can be an error or a good response
}
return $response;
}
/**
* parse the JSON response
* @param $response
* @return array parsed
*/
public function parseResponse($response) {
$data = $response['body'];
$data = json_decode($data);
return (array)$data;
}
/**
* handles the processing of the image using the ShortPixel API
* @param array $URLs - list of urls to send to API
* @param array $PATHs - list of local paths for the images
* @param ShortPixelMetaFacade $itemHandler - the Facade that manages different types of image metadatas: MediaLibrary (postmeta table), ShortPixel custom (shortpixel_meta table)
* @return array status/message array
*/
public function processImage($URLs, $PATHs, $itemHandler = null) {
return $this->processImageRecursive($URLs, $PATHs, $itemHandler, 0);
}
/**
* handles the processing of the image using the ShortPixel API - cals itself recursively until success
* @param array $URLs - list of urls to send to API
* @param array $PATHs - list of local paths for the images
* @param ShortPixelMetaFacade $itemHandler - the Facade that manages different types of image metadatas: MediaLibrary (postmeta table), ShortPixel custom (shortpixel_meta table)
* @param int $startTime - time of the first call
* @return array status/message array
*/
private function processImageRecursive($URLs, $PATHs, $itemHandler = null, $startTime = 0)
{
//WPShortPixel::log("processImageRecursive ID: " . $itemHandler->getId() . " PATHs: " . json_encode($PATHs));
$PATHs = self::CheckAndFixImagePaths($PATHs);//check for images to make sure they exist on disk
if ( $PATHs === false || isset($PATHs['error'])) {
$missingFiles = '';
if(isset($PATHs['error'])) {
foreach($PATHs['error'] as $errPath) {
$missingFiles .= (strlen($missingFiles) ? ', ':'') . basename(stripslashes($errPath));
}
}
$msg = __('The file(s) do not exist on disk: ','shortpixel-image-optimiser') . $missingFiles;
$itemHandler->setError(self::ERR_FILE_NOT_FOUND, $msg );
return array("Status" => self::STATUS_SKIP, "Message" => $msg, "Silent" => $itemHandler->getType() == ShortPixelMetaFacade::CUSTOM_TYPE ? 1 : 0);
}
//tries multiple times (till timeout almost reached) to fetch images.
if($startTime == 0) {
$startTime = time();
}
$apiRetries = $this->_settings->apiRetries;
if( time() - $startTime > SHORTPIXEL_MAX_EXECUTION_TIME2)
{//keeps track of time
if ( $apiRetries > SHORTPIXEL_MAX_API_RETRIES )//we tried to process this time too many times, giving up...
{
$itemHandler->incrementRetries(1, self::ERR_TIMEOUT, __('Timed out while processing.','shortpixel-image-optimiser'));
$this->_settings->apiRetries = 0; //fai added to solve a bug?
return array("Status" => self::STATUS_SKIP,
"Message" => ($itemHandler->getType() == ShortPixelMetaFacade::CUSTOM_TYPE ? __('Image ID','shortpixel-image-optimiser') : __('Media ID','shortpixel-image-optimiser'))
. ": " . $itemHandler->getId() .' ' . __('Skip this image, try the next one.','shortpixel-image-optimiser'));
}
else
{//we'll try again next time user visits a page on admin panel
$apiRetries++;
$this->_settings->apiRetries = $apiRetries;
return array("Status" => self::STATUS_RETRY, "Message" => __('Timed out while processing.','shortpixel-image-optimiser') . ' (pass '.$apiRetries.')',
"Count" => $apiRetries);
}
}
//#$compressionType = isset($meta['ShortPixel']['type']) ? ($meta['ShortPixel']['type'] == 'lossy' ? 1 : 0) : $this->_settings->compressionType;
$meta = $itemHandler->getMeta();
$compressionType = $meta->getCompressionType() !== null ? $meta->getCompressionType() : $this->_settings->compressionType;
try {
$response = $this->doRequests($URLs, true, $itemHandler, $compressionType);//send requests to API
}
catch(Exception $e) {
Log::addError('Api DoRequest Thrown ' . $e->getMessage());
$response = array(); // otherwise not set.
}
//die($response['body']);
if($response['response']['code'] != 200) {//response <> 200 -> there was an error apparently?
return array("Status" => self::STATUS_FAIL, "Message" => __('There was an error and your request was not processed.', 'shortpixel-image-optimiser')
. (isset($response['response']['message']) ? ' (' . $response['response']['message'] . ')' : ''), "Code" => $response['response']['code']);
}
$APIresponse = $this->parseResponse($response);//get the actual response from API, its an array
if ( isset($APIresponse[0]) ) //API returned image details
{
foreach ( $APIresponse as $imageObject ) {//this part makes sure that all the sizes were processed and ready to be downloaded
if ( isset($imageObject->Status) && ( $imageObject->Status->Code == 0 || $imageObject->Status->Code == 1 ) ) {
sleep(1);
return $this->processImageRecursive($URLs, $PATHs, $itemHandler, $startTime);
}
}
$firstImage = $APIresponse[0];//extract as object first image
switch($firstImage->Status->Code)
{
case 2: //self::STATUS_SUCCESS: <- @todo Success in this constant is 1 ,but appears to be 2? // success
//handle image has been processed
if(!isset($firstImage->Status->QuotaExceeded)) {
$this->_settings->quotaExceeded = 0;//reset the quota exceeded flag
}
return $this->handleSuccess($APIresponse, $PATHs, $itemHandler, $compressionType);
default:
//handle error
$incR = 1;
if ( !file_exists($PATHs[0]) ) {
$err = array("Status" => self::STATUS_NOT_FOUND, "Message" => "File not found on disk. "
. ($itemHandler->getType() == ShortPixelMetaFacade::CUSTOM_TYPE ? "Image" : "Media")
. " ID: " . $itemHandler->getId(), "Code" => self::ERR_FILE_NOT_FOUND);
$incR = 3;
}
elseif ( isset($APIresponse[0]->Status->Message) ) {
//return array("Status" => self::STATUS_FAIL, "Message" => "There was an error and your request was not processed (" . $APIresponse[0]->Status->Message . "). REQ: " . json_encode($URLs));
$err = array("Status" => self::STATUS_FAIL, "Code" => (isset($APIresponse[0]->Status->Code) ? $APIresponse[0]->Status->Code : self::ERR_UNKNOWN),
"Message" => __('There was an error and your request was not processed.','shortpixel-image-optimiser')
. " (" . wp_basename($APIresponse[0]->OriginalURL) . ": " . $APIresponse[0]->Status->Message . ")");
} else {
$err = array("Status" => self::STATUS_FAIL, "Message" => __('There was an error and your request was not processed.','shortpixel-image-optimiser'),
"Code" => (isset($APIresponse[0]->Status->Code) ? $APIresponse[0]->Status->Code : self::ERR_UNKNOWN));
}
$itemHandler->incrementRetries($incR, $err["Code"], $err["Message"]);
$meta = $itemHandler->getMeta();
if($meta->getRetries() >= SHORTPIXEL_MAX_FAIL_RETRIES) {
$meta->setStatus($APIresponse[0]->Status->Code);
$meta->setMessage($APIresponse[0]->Status->Message);
$itemHandler->updateMeta($meta);
}
return $err;
}
}
if(!isset($APIresponse['Status'])) {
WpShortPixel::log("API Response Status unfound : " . json_encode($APIresponse));
return array("Status" => self::STATUS_FAIL, "Message" => __('Unrecognized API response. Please contact support.','shortpixel-image-optimiser'),
"Code" => self::ERR_UNKNOWN, "Debug" => ' (SERVER RESPONSE: ' . json_encode($response) . ')');
} else {
switch($APIresponse['Status']->Code)
{
case -403:
@delete_option('bulkProcessingStatus');
$this->_settings->quotaExceeded = 1;
return array("Status" => self::STATUS_QUOTA_EXCEEDED, "Message" => __('Quota exceeded.','shortpixel-image-optimiser'));
break;
case -404:
return array("Status" => self::STATUS_QUEUE_FULL, "Message" => $APIresponse['Status']->Message);
case -500:
return array("Status" => self::STATUS_MAINTENANCE, "Message" => $APIresponse['Status']->Message);
}
//sometimes the response array can be different
if (is_numeric($APIresponse['Status']->Code)) {
return array("Status" => self::STATUS_FAIL, "Message" => $APIresponse['Status']->Message);
} else {
return array("Status" => self::STATUS_FAIL, "Message" => $APIresponse[0]->Status->Message);
}
}
}
/**
* sets the preferred protocol of URL using the globally set preferred protocol.
* If global protocol not set, sets it by testing the download of a http test image from ShortPixel site.
* If http works then it's http, otherwise sets https
* @param string $url
* @param bool $reset - forces recheck even if preferred protocol is already set
* @return string url with the preferred protocol
*/
public function setPreferredProtocol($url, $reset = false) {
//switch protocol based on the formerly detected working protocol
if($this->_settings->downloadProto == '' || $reset) {
//make a test to see if the http is working
$testURL = 'http://' . SHORTPIXEL_API . '/img/connection-test-image.png';
$result = download_url($testURL, 10);
$this->_settings->downloadProto = is_wp_error( $result ) ? 'https' : 'http';
}
return $this->_settings->downloadProto == 'http' ?
str_replace('https://', 'http://', $url) :
str_replace('http://', 'https://', $url);
}
function fromArchive($path, $optimizedUrl, $optimizedSize, $originalSize, $webpUrl, $avifUrl) {
$webpTempFile = "NA";
if($webpUrl !== "NA") {
$webpTempFile = $path . '/' . wp_basename($webpUrl);
$webpTempFile = file_exists($webpTempFile) ? $webpTempFile : 'NA';
}
$avifTempFile = false;
if ($avifUrl !== "NA")
{
$avifTempFile = $path . '/' . wp_basename($avifUrl);
$avifTempFile = file_exists($avifTempFile) ? $avifTempFile : 'NA';
}
//if there is no improvement in size then we do not download this file
if ( $originalSize == $optimizedSize )
return array("Status" => self::STATUS_UNCHANGED, "Message" => "File wasn't optimized so we do not download it.", "WebP" => $webpTempFile, 'Avif' => $avifTempFile);
$correctFileSize = $optimizedSize;
$tempFile = $path . '/' . wp_basename($optimizedUrl);
if(file_exists($tempFile)) {
//on success we return this
if( filesize($tempFile) != $correctFileSize) {
$size = filesize($tempFile);
@unlink($tempFile);
@unlink($webpTempFile);
@unlink($avifTempFile);
$returnMessage = array(
"Status" => self::STATUS_ERROR,
"Code" => self::ERR_INCORRECT_FILE_SIZE,
"Message" => sprintf(__('Error in archive - incorrect file size (downloaded: %s, correct: %s )','shortpixel-image-optimiser'),$size, $correctFileSize));
} else {
$returnMessage = array("Status" => self::STATUS_SUCCESS, "Message" => $tempFile, "WebP" => $webpTempFile, 'Avif' => $avifTempFile);
}
} else {
$returnMessage = array("Status" => self::STATUS_ERROR,
"Code" => self::ERR_FILE_NOT_FOUND,
"Message" => __('Unable to locate downloaded file','shortpixel-image-optimiser') . " " . $tempFile);
}
return $returnMessage;
}
/**
* handles the download of an optimized image from ShortPixel API
* @param string $optimizedUrl
* @param int $optimizedSize
* @param int $originalSize
* @param string $webpUrl
* @return array status /message array
*/
private function handleDownload($optimizedUrl, $optimizedSize, $originalSize, $webpUrl, $avifUrl){
$downloadTimeout = max(ini_get('max_execution_time') - 10, 15);
$webpTempFile = "NA";
if($webpUrl !== "NA") {
$webpURL = $this->setPreferredProtocol(urldecode($webpUrl));
$webpTempFile = download_url($webpURL, $downloadTimeout);
$webpTempFile = is_wp_error( $webpTempFile ) ? "NA" : $webpTempFile;
}
$avifTempFile = "NA";
if($avifUrl !== "NA") {
$avifUrl = $this->setPreferredProtocol(urldecode($avifUrl));
$avifTempFile = download_url($avifUrl, $downloadTimeout);
$avifTempFile = is_wp_error( $avifTempFile ) ? "NA" : $avifTempFile;
}
//if there is no improvement in size then we do not download this file
if ( $originalSize == $optimizedSize )
return array("Status" => self::STATUS_UNCHANGED, "Message" => "File wasn't optimized so we do not download it.", "WebP" => $webpTempFile, 'Avif' => $avifTempFile);
$correctFileSize = $optimizedSize;
$fileURL = $this->setPreferredProtocol(urldecode($optimizedUrl));
$tempFile = download_url($fileURL, $downloadTimeout);
Log::addInfo('Downloading file: '.json_encode($tempFile));
if(is_wp_error( $tempFile ))
{ //try to switch the default protocol
$fileURL = $this->setPreferredProtocol(urldecode($optimizedUrl), true); //force recheck of the protocol
$tempFile = download_url($fileURL, $downloadTimeout);
}
//on success we return this
$returnMessage = array("Status" => self::STATUS_SUCCESS, "Message" => $tempFile, "WebP" => $webpTempFile, 'Avif' => $avifTempFile);
if ( is_wp_error( $tempFile ) ) {
@unlink($tempFile);
@unlink($webpTempFile);
@unlink($avifTempFile);
$returnMessage = array(
"Status" => self::STATUS_ERROR,
"Code" => self::ERR_DOWNLOAD,
"Message" => __('Error downloading file','shortpixel-image-optimiser') . " ({$optimizedUrl}) " . $tempFile->get_error_message());
}
//check response so that download is OK
elseif (!file_exists($tempFile)) {
$returnMessage = array("Status" => self::STATUS_ERROR,
"Code" => self::ERR_FILE_NOT_FOUND,
"Message" => __('Unable to locate downloaded file','shortpixel-image-optimiser') . " " . $tempFile);
}
elseif( filesize($tempFile) != $correctFileSize) {
$size = filesize($tempFile);
@unlink($tempFile);
@unlink($webpTempFile);
@unlink($avifTempFile);
$returnMessage = array(
"Status" => self::STATUS_ERROR,
"Code" => self::ERR_INCORRECT_FILE_SIZE,
"Message" => sprintf(__('Error downloading file - incorrect file size (downloaded: %s, correct: %s )','shortpixel-image-optimiser'),$size, $correctFileSize));
}
Log::addDebug('HandleDownload ReturnMSG ', $returnMessage);
return $returnMessage;
}
/** Tries to create backup
*
* @param $mainPath The path of the main image?
* @param $PATHs MUST be included. If just one image is for backup, add array($mainPath)
* @return Array Array with Status and optional Message */
public static function backupImage($mainPath, $PATHs) {
/**
* Passing a truthy value to the filter will effectively short-circuit this function.
* So third party plugins can handle Backup by there own.
*/
if(apply_filters('shortpixel_skip_backup', false, $mainPath, $PATHs)){
return array("Status" => self::STATUS_SUCCESS);
}
$PATHs = apply_filters('shortpixel/backup/paths', $PATHs, $mainPath);
//$fullSubDir = str_replace(wp_normalize_path(get_home_path()), "", wp_normalize_path(dirname($itemHandler->getMeta()->getPath()))) . '/';
//$SubDir = ShortPixelMetaFacade::returnSubDir($itemHandler->getMeta()->getPath(), $itemHandler->getType());
$fullSubDir = ShortPixelMetaFacade::returnSubDir($mainPath);
$source = $PATHs; //array with final paths for these files
$fs = \wpSPIO()->filesystem();
if( !file_exists(SHORTPIXEL_BACKUP_FOLDER) && ! ShortPixelFolder::createBackUpFolder() ) {//creates backup folder if it doesn't exist
Log::addWarn('Backup folder does not exist and it cannot be created');
return array("Status" => self::STATUS_FAIL, "Message" => __('Backup folder does not exist and it cannot be created','shortpixel-image-optimiser'));
}
//create subdir in backup folder if needed
ShortPixelFolder::createBackUpFolder(SHORTPIXEL_BACKUP_FOLDER . '/' . $fullSubDir);
foreach ( $source as $fileID => $filePATH )//create destination files array
{
$file = $fs->getFile($filePATH);
$bkFilePath = $fs->getBackupDirectory($file);
$bkFile = $fs->getFile($bkFilePath . $file->getFileName() );
$destination[$fileID] = $bkFile; //SHORTPIXEL_BACKUP_FOLDER . '/' . $fullSubDir . self::MB_basename($source[$fileID]);
}
//now that we have original files and where we should back them up we attempt to do just that
if(is_writable(SHORTPIXEL_BACKUP_FOLDER))
{
foreach ( $destination as $fileID => $destination_file )
{
//$destination_file = $fs->getFile($filePATH);
if ( ! $destination_file->exists() )
{
$source_file = $fs->getFile($source[$fileID]);
$result = $source_file->copy($destination_file);
if ( ! $result )
{//file couldn't be saved in backup folder
$msg = sprintf(__('Cannot save file <i>%s</i> in backup directory','shortpixel-image-optimiser'),$destination_file->getFullPath() );
return array("Status" => self::STATUS_FAIL, "Message" => $msg);
}
}
}
return array("Status" => self::STATUS_SUCCESS);
}
else {//cannot write to the backup dir, return with an error
$msg = __('Cannot save file in backup directory','shortpixel-image-optimiser');
Log::addWarn('Backup directory not writable');
return array("Status" => self::STATUS_FAIL, "Message" => $msg);
}
}
private function createArchiveTempFolder($archiveBasename) {
$archiveTempDir = get_temp_dir() . '/' . $archiveBasename;
$fs = \wpSPIO()->filesystem();
$tempDir = $fs->getDirectory($archiveTempDir);
if( $tempDir->exists() && (time() - filemtime($archiveTempDir) < max(30, SHORTPIXEL_MAX_EXECUTION_TIME) + 10)) {
Log::addWarn("CONFLICT. Folder already exists and is modified in the last minute. Current IP:" . $_SERVER['REMOTE_ADDR']);
return array("Status" => self::STATUS_RETRY, "Code" => 1, "Message" => "Pending");
}
// try to create temporary folder
$tempDir->check();
if( ! $tempDir->exists() ) {
return array("Status" => self::STATUS_ERROR, "Code" => self::ERR_SAVE, "Message" => "Could not create temporary folder.");
}
return array("Status" => self::STATUS_SUCCESS, "Dir" => $tempDir);
}
private function downloadArchive($archive, $compressionType, $first = true) {
if($archive->ArchiveStatus->Code == 1 || $archive->ArchiveStatus->Code == 0) {
return array("Status" => self::STATUS_RETRY, "Code" => 1, "Message" => "Pending");
} elseif($archive->ArchiveStatus->Code == 2) {
$suffix = ($compressionType == 0 ? "-lossless" : "");
$archiveURL = "Archive" . ($compressionType == 0 ? "Lossless" : "") . "URL";
$archiveSize = "Archive" . ($compressionType == 0 ? "Lossless" : "") . "Size";
$archiveTemp = $this->createArchiveTempFolder(wp_basename($archive->$archiveURL, '.tar'));
if($archiveTemp["Status"] == self::STATUS_SUCCESS) { $archiveTempDir = $archiveTemp["Dir"]; }
else { return $archiveTemp; }
$downloadResult = $this->handleDownload($archive->$archiveURL, $archive->$archiveSize, 0, 'NA');
if ( $downloadResult['Status'] == self::STATUS_SUCCESS ) {
$archiveFile = $downloadResult['Message'];
if(filesize($archiveFile) !== $archive->$archiveSize) {
@unlink($archiveFile);
ShortpixelFolder::deleteFolder($archiveTempDir);
return array("Status" => self::STATUS_RETRY, "Code" => 1, "Message" => "Pending");
}
$pharData = new PharData($archiveFile);
try {
if (SHORTPIXEL_DEBUG === true) {
$info = "Current IP:" . $_SERVER['REMOTE_ADDR'] . "ARCHIVE CONTENTS: COUNT " . $pharData->count() . ", ";
foreach($pharData as $file) {
$info .= $file . ", ";
}
WPShortPixel::log($info);
}
$pharData->extractTo($archiveTempDir, null, true);
WPShortPixel::log("ARCHIVE EXTRACTED " . json_encode(scandir($archiveTempDir)));
@unlink($archiveFile);
} catch (Exception $ex) {
@unlink($archiveFile);
ShortpixelFolder::deleteFolder($archiveTempDir);
return array("Status" => self::STATUS_ERROR, "Code" => $ex->getCode(), "Message" => $ex->getMessage());
}
return array("Status" => self::STATUS_SUCCESS, "Code" => 2, "Message" => "Success", "Path" => $archiveTempDir);
} else {
WPShortPixel::log("ARCHIVE ERROR (" . $archive->$archiveURL . "): " . json_encode($downloadResult));
if($first && $downloadResult['Code'] == self::ERR_INCORRECT_FILE_SIZE) {
WPShortPixel::log("RETRYING AFTER ARCHIVE ERROR");
return $this->downloadArchive($archive, $compressionType, false); // try again, maybe the archive was flushing...
}
@rmdir($archiveTempDir); //in the case it was just created and it's empty...
return array("Status" => $downloadResult['Status'], "Code" => $downloadResult['Code'], "Message" => $downloadResult['Message']);
}
}
return false;
}
/**
* handles a successful optimization, setting metadata and handling download for each file in the set
* @param array $APIresponse - the response from the API - contains the optimized images URLs to download
* @param array $PATHs - list of local paths for the files
* @param ShortPixelMetaFacade $itemHandler - the Facade that manages different types of image metadatas: MediaLibrary (postmeta table), ShortPixel custom (shortpixel_meta table)
* @param int $compressionType - 1 - lossy, 2 - glossy, 0 - lossless
* @return array status/message
*/
private function handleSuccess($APIresponse, $PATHs, $itemHandler, $compressionType) {
Log::addDebug('Shortpixel API : Handling Success!');
$counter = $savedSpace = $originalSpace = $optimizedSpace /* = $averageCompression */ = 0;
$NoBackup = true;
if($compressionType) {
$fileType = "LossyURL";
$fileSize = "LossySize";
} else {
$fileType = "LosslessURL";
$fileSize = "LosslessSize";
}
$webpType = "WebP" . $fileType;
$avifType = 'AVIF' . $fileType;
$archive = /*false &&*/
($this->_settings->downloadArchive == 7 && class_exists('PharData') && isset($APIresponse[count($APIresponse) - 1]->ArchiveStatus))
? $this->downloadArchive($APIresponse[count($APIresponse) - 1], $compressionType) : false;
if($archive !== false && $archive['Status'] !== self::STATUS_SUCCESS) {
return $archive;
}
//download each file from array and process it
foreach ( $APIresponse as $fileData )
{
if(!isset($fileData->Status)) continue; //if optimized images archive is activated, last entry of APIResponse if the Archive data.
if ( $fileData->Status->Code == 2 ) //file was processed OK
{
if ( $counter == 0 ) { //save percent improvement for main file
$percentImprovement = $fileData->PercentImprovement;
} else { //count thumbnails only
$this->_settings->thumbsCount = $this->_settings->thumbsCount + 1;
}
$webpUrl = isset($fileData->$webpType) ? $fileData->$webpType : 'NA';
$avifUrl = isset($fileData->$avifType) ? $fileData->$avifType : 'NA';
//TODO la sfarsit sa faca fallback la handleDownload
if($archive) {
$downloadResult = $this->fromArchive($archive['Path'], $fileData->$fileType, $fileData->$fileSize, $fileData->OriginalSize, $webpUrl, $avifUrl );
} else {
$downloadResult = $this->handleDownload($fileData->$fileType, $fileData->$fileSize, $fileData->OriginalSize, $webpUrl, $avifUrl);
}
$tempFiles[$counter] = $downloadResult;
if ( $downloadResult['Status'] == self::STATUS_SUCCESS ) {
//nothing to do
}
//when the status is STATUS_UNCHANGED we just skip the array line for that one
elseif( $downloadResult['Status'] == self::STATUS_UNCHANGED ) {
//this image is unchanged so won't be copied below, only the optimization stats need to be computed
$originalSpace += $fileData->OriginalSize;
$optimizedSpace += $fileData->$fileSize;
}
else {
self::cleanupTemporaryFiles($archive, $tempFiles);
return array("Status" => $downloadResult['Status'], "Code" => $downloadResult['Code'], "Message" => $downloadResult['Message']);
}
}
else { //there was an error while trying to download a file
$tempFiles[$counter] = "";
}
$counter++;
}
//figure out in what SubDir files should land
$mainPath = $itemHandler->getMeta()->getPath();
//if backup is enabled - we try to save the images
if( $this->_settings->backupImages )
{
$backupStatus = self::backupImage($mainPath, $PATHs);
if($backupStatus['Status'] == self::STATUS_FAIL) {
$itemHandler->incrementRetries(1, self::ERR_SAVE_BKP, $backupStatus["Message"]);
self::cleanupTemporaryFiles($archive, empty($tempFiles) ? array() : $tempFiles);
Log::addError('Failed to create image backup!', array('status' => $backupStatus));
return array("Status" => self::STATUS_FAIL, "Code" =>"backup-fail", "Message" => "Failed to back the image up.");
}
$NoBackup = false;
}//end backup section
$writeFailed = 0;
$width = $height = null;
$do_resize = $this->_settings->resizeImages;
$retinas = 0;
$thumbsOpt = 0;
$thumbsOptList = array();
// The settings model.
$settings = \wpSPIO()->settings();
$fs = \wpSPIO()->fileSystem();
//Log::addDebug($tempFiles);
// Check and Run all tempfiles. Move it to appropiate places.
if ( !empty($tempFiles) )
{
//overwrite the original files with the optimized ones
foreach ( $tempFiles as $tempFileID => $tempFile )
{
if(!is_array($tempFile)) continue;
$targetFile = $fs->getFile($PATHs[$tempFileID]);
$isRetina = ShortPixelMetaFacade::isRetina($targetFile->getFullPath());
if( ($tempFile['Status'] == self::STATUS_UNCHANGED || $tempFile['Status'] == self::STATUS_SUCCESS) && !$isRetina
&& $targetFile->getFullPath() !== $mainPath) {
$thumbsOpt++;
$thumbsOptList[] = self::MB_basename($targetFile->getFullPath());
}
if($tempFile['Status'] == self::STATUS_SUCCESS) { //if it's unchanged it will still be in the array but only for WebP (handled below)
$tempFilePATH = $fs->getFile($tempFile["Message"]);
//@todo Move file logic to use FS controller / fileModel.
if ( $tempFilePATH->exists() && (! $targetFile->exists() || $targetFile->is_writable()) ) {
// copy($tempFilePATH, $targetFile);
$tempFilePATH->move($targetFile);
if(ShortPixelMetaFacade::isRetina($targetFile->getFullPath())) {
$retinas ++;
}
if($do_resize && $itemHandler->getMeta()->getPath() == $targetFile->getFullPath() ) { //this is the main image
$size = getimagesize($PATHs[$tempFileID]);
$width = $size[0];
$height = $size[1];
}
//Calculate the saved space
$fileData = $APIresponse[$tempFileID];
$savedSpace += $fileData->OriginalSize - $fileData->$fileSize;
$originalSpace += $fileData->OriginalSize;
$optimizedSpace += $fileData->$fileSize;
//$averageCompression += $fileData->PercentImprovement;
Log::addInfo("HANDLE SUCCESS: Image " . $PATHs[$tempFileID] . " original size: ".$fileData->OriginalSize . " optimized: " . $fileData->$fileSize);
//add the number of files with < 5% optimization
if ( ( ( 1 - $APIresponse[$tempFileID]->$fileSize/$APIresponse[$tempFileID]->OriginalSize ) * 100 ) < 5 ) {
$this->_settings->under5Percent++;
}
}
else {
if($archive && SHORTPIXEL_DEBUG === true) {
if(! $tempFilePATH->exists()) {
Log::addWarn("MISSING FROM ARCHIVE. tempFilePath: " . $tempFilePATH->getFullPath() . " with ID: $tempFileID");
} elseif(! $targetFile->is_writable() ){
Log::addWarn("TARGET NOT WRITABLE: " . $targetFile->getFullPath() );
}
}
$writeFailed++;
}
//@unlink($tempFilePATH); // @todo Unlink is risky due to lack of checks.
// $tempFilePath->delete();
}
$tempWebpFilePATH = $fs->getFile($tempFile["WebP"]);
if( $tempWebpFilePATH->exists() ) {
$targetWebPFileCompat = $fs->getFile($targetFile->getFileDir() . $targetFile->getFileName() . '.webp');
/*$targetWebPFileCompat = dirname($targetFile) . '/'. self::MB_basename($targetFile, '.' . pathinfo($targetFile, PATHINFO_EXTENSION)) . ".webp"; */
$targetWebPFile = $fs->getFile($targetFile->getFileDir() . $targetFile->getFileBase() . '.webp');
//if the Targetfile already exists, it means that there is another file with the same basename but different extension which has its .webP counterpart save it with double extension
if(SHORTPIXEL_USE_DOUBLE_WEBP_EXTENSION || $targetWebPFile->exists()) {
$tempWebpFilePATH->move($targetWebPFileCompat);
} else {
$tempWebpFilePATH->move($targetWebPFile);
}
}
$tempAvifFilePATH = $fs->getFile($tempFile["Avif"]);
if( $tempAvifFilePATH->exists() ) {
/*$targetAvifFileCompat = $fs->getFile($targetFile->getFileDir() . $targetFile->getFileName() . '.avif');*/
$targetWebPFile = $fs->getFile($targetFile->getFileDir() . $targetFile->getFileBase() . '.avif');
$tempAvifFilePATH->move($targetWebPFile);
}
} // / For each tempFile
self::cleanupTemporaryFiles($archive, $tempFiles);
if ( $writeFailed > 0 )//there was an error
{
/* Log::addDebug("ARCHIVE HAS MISSING FILES. EXPECTED: " . json_encode($PATHs)
. " AND: " . json_encode($APIresponse)
. " GOT ARCHIVE: " . $APIresponse[count($APIresponse) - 1]->ArchiveURL . " LOSSLESS: " . $APIresponse[count($APIresponse) - 1]->ArchiveLosslessURL
. " CONTAINING: " . json_encode(scandir($archive['Path']))); */
Log::addDebug('Archive files missing (expected paths, response)', array($PATHs, $APIresponse));
$msg = sprintf(__('Optimized version of %s file(s) couldn\'t be updated.','shortpixel-image-optimiser'),$writeFailed);
$itemHandler->incrementRetries(1, self::ERR_SAVE, $msg);
$this->_settings->bulkProcessingStatus = "error";
return array("Status" => self::STATUS_FAIL, "Code" =>"write-fail", "Message" => $msg);
}
} elseif( 0 + $fileData->PercentImprovement < 5) {
$this->_settings->under5Percent++;
}
//old average counting
$this->_settings->savedSpace += $savedSpace;
//$averageCompression = $this->_settings->averageCompression * $this->_settings->fileCount / ($this->_settings->fileCount + count($APIresponse));
//$this->_settings->averageCompression = $averageCompression;
$this->_settings->fileCount += count($APIresponse);
//new average counting
$this->_settings->totalOriginal += $originalSpace;
$this->_settings->totalOptimized += $optimizedSpace;
//update metadata for this file
$meta = $itemHandler->getMeta();
// die(var_dump($percentImprovement));
if($meta->getThumbsTodo()) {
$percentImprovement = $meta->getImprovementPercent();
}
$png2jpg = $meta->getPng2Jpg();
$png2jpg = is_array($png2jpg) ? $png2jpg['optimizationPercent'] : 0;
$meta->setMessage($originalSpace
? number_format(100.0 * (1.0 - $optimizedSpace / $originalSpace), 2)
: "Couldn't compute thumbs optimization percent. Main image: " . $percentImprovement);
WPShortPixel::log("HANDLE SUCCESS: Image optimization: ".$meta->getMessage());
$meta->setCompressionType($compressionType);
$meta->setCompressedSize(@filesize($meta->getPath()));
$meta->setKeepExif($this->_settings->keepExif);
$meta->setCmyk2rgb($this->_settings->CMYKtoRGBconversion);
$meta->setTsOptimized(date("Y-m-d H:i:s"));
$meta->setThumbsOptList(is_array($meta->getThumbsOptList()) ? array_unique(array_merge($meta->getThumbsOptList(), $thumbsOptList)) : $thumbsOptList);
$meta->setThumbsOpt(($meta->getThumbsTodo() || $this->_settings->processThumbnails) ? count($meta->getThumbsOptList()) : 0);
$meta->setRetinasOpt($retinas);
if(null !== $this->_settings->excludeSizes) {
$meta->setExcludeSizes($this->_settings->excludeSizes);
}
$meta->setThumbsTodo(false);
//* Not yet as it doesn't seem to work... */$meta->addThumbs($webpSizes);
if($width && $height) {
$meta->setActualWidth($width);
$meta->setActualHeight($height);
}
$meta->setRetries($meta->getRetries() + 1);
$meta->setBackup(!$NoBackup);
$meta->setStatus(2);
if ($do_resize)
{
$resizeWidth = $settings->resizeWidth;
$resizeHeight = $settings->resizeHeight;
if ($resizeWidth == $width || $resizeHeight == $height) // resized.
{
$meta->setResizeWidth($width);
$meta->setResizeHeight($height);
$meta->setResize(true);
}
else
$meta->setResize(false);
}
$itemHandler->updateMeta($meta);
$itemHandler->optimizationSucceeded();
$itemHandler->deleteItemCache(); // remove cache when done, to prevent re-optimizing things.
Log::addDebug("HANDLE SUCCESS: Metadata saved.");
if(!$originalSpace) { //das kann nicht sein, alles klar?!
throw new Exception("OriginalSpace = 0. APIResponse" . json_encode($APIresponse));
}
//we reset the retry counter in case of success
$this->_settings->apiRetries = 0;
return array("Status" => self::STATUS_SUCCESS, "Message" => 'Success: No pixels remained unsqueezed :-)',
"PercentImprovement" => $originalSpace
? number_format(100.0 * (1.0 - (1.0 - $png2jpg / 100.0) * $optimizedSpace / $originalSpace), 2)
: "Couldn't compute thumbs optimization percent. Main image: " . $percentImprovement);
}//end handleSuccess
/**
* @param $archive
* @param $tempFiles
* @todo Move to FS-controller
*/
protected static function cleanupTemporaryFiles($archive, $tempFiles)
{
if ($archive) {
ShortpixelFolder::deleteFolder($archive['Path']);
} else {
if (!empty($tempFiles) && is_array($tempFiles)) {
foreach ($tempFiles as $tmpFile) {
$filepath = isset($tmpFile['Message']) ? $tmpFile['Message'] : false;
if ($filepath)
{
$file = \wpSPIO()->filesystem()->getFile($filepath);
if ($file->exists())
$file->delete();
//@unlink($tmpFile["Message"]);
}
}
}
}
}
/**
* a basename alternative that deals OK with multibyte charsets (e.g. Arabic)
* @param string $Path
* @return string
*/
static public function MB_basename($Path, $suffix = false){
$Separator = " qq ";
$qqPath = preg_replace("/[^ ]/u", $Separator."\$0".$Separator, $Path);
if(!$qqPath) { //this is not an UTF8 string!! Don't rely on basename either, since if filename starts with a non-ASCII character it strips it off
// This line is separated because of 'passed by reference' errors otherwise.
$pathAr = explode(DIRECTORY_SEPARATOR, $Path);
$fileName = end($pathAr);
$pos = strpos($fileName, $suffix);
if($pos !== false) {
return substr($fileName, 0, $pos);
}
return $fileName;
}
$suffix = preg_replace("/[^ ]/u", $Separator."\$0".$Separator, $suffix);
$Base = basename($qqPath, $suffix);
$Base = str_replace($Separator, "", $Base);
return $Base;
}
/**
* sometimes, the paths to the files as defined in metadata are wrong, we try to automatically correct them
* @param array $PATHs
* @return boolean|string
*/
static public function CheckAndFixImagePaths($PATHs){
$ErrorCount = 0;
$Tmp = explode("/", SHORTPIXEL_UPLOADS_BASE);
$TmpCount = count($Tmp);
$StichString = $Tmp[$TmpCount-2] . "/" . $Tmp[$TmpCount-1];
//files exist on disk?
$missingFiles = array();
foreach ( $PATHs as $Id => $File )
{
//we try again with a different path
if ( !apply_filters( 'shortpixel_image_exists', file_exists($File), $File, null ) ){
//$NewFile = $uploadDir['basedir'] . "/" . substr($File,strpos($File, $StichString));//+strlen($StichString));
$NewFile = SHORTPIXEL_UPLOADS_BASE . substr($File,strpos($File, $StichString)+strlen($StichString));
if (file_exists($NewFile)) {
$PATHs[$Id] = $NewFile;
} else {
$NewFile = SHORTPIXEL_UPLOADS_BASE . "/" . $File;
if (file_exists($NewFile)) {
$PATHs[$Id] = $NewFile;
} else {
$missingFiles[] = $File;
$ErrorCount++;
}
}
}
}
if ( $ErrorCount > 0 ) {
return array("error" => $missingFiles);//false;
} else {
return $PATHs;
}
}
static public function getCompressionTypeName($compressionType) {
if(is_array($compressionType)) {
return array_map(array('ShortPixelAPI', 'getCompressionTypeName'), $compressionType);
}
return 0 + $compressionType == 2 ? 'glossy' : (0 + $compressionType == 1 ? 'lossy' : 'lossless');
}
static public function getCompressionTypeCode($compressionName) {
return $compressionName == 'glossy' ? 2 : ($compressionName == 'lossy' ? 1 : 0);
}
}