settings(); $this->apiEndPoint = $settings->httpProto . '://' . SHORTPIXEL_API . '/v2/reducer.php'; $this->apiDumpEndPoint = $settings->httpProto . '://' . SHORTPIXEL_API . '/v2/cleanup.php'; } public static function getInstance() { if (is_null(self::$instance)) self::$instance = new ApiController(); return self::$instance; } /* * @param Object $item Item of stdClass * @return Returns same Item with Result of request */ public function processMediaItem($item, $imageObj) { if (! is_object($imageObj)) { $item->result = $this->returnFailure(self::STATUS_FAIL, __('Item seems invalid, removed or corrupted.', 'shortpixel-image-optimiser')); return $item; } elseif (false === $imageObj->isProcessable() || $imageObj->isOptimizePrevented() == true) { if ($imageObj->isOptimized()) // This only looks at main item { $item->result = $this->returnFailure(self::STATUS_FAIL, __('Item is already optimized', 'shortpixel-image-optimiser')); return $item; } else { $item->result = $this->returnFailure(self::STATUS_FAIL, __('Item is not processable and not optimized', 'shortpixel-image-optimiser')); return $item; } } if (! is_array($item->urls) || count($item->urls) == 0) { $item->result = $this->returnFailure(self::STATUS_FAIL, __('No Urls given for this Item', 'shortpixel-image-optimiser')); return $item; } else { // if ok, urlencode them. $list = array(); foreach($item->urls as $url) { $parsed_url = parse_url($url); if (false !== $parsed_url) { //$url = $this->encodeURL($parsed_url, $url); } $list[] = $url; } $item->urls = $list; } $requestArgs = array('urls' => $item->urls); // obligatory if (property_exists($item, 'compressionType')) $requestArgs['compressionType'] = $item->compressionType; $requestArgs['blocking'] = ($item->tries == 0) ? false : true; $requestArgs['item_id'] = $item->item_id; $requestArgs['refresh'] = (property_exists($item, 'refresh') && $item->refresh) || $item->tries == 0 ? true : false; $requestArgs['flags'] = (property_exists($item, 'flags')) ? $item->flags : array(); $requestArgs['paramlist'] = property_exists($item, 'paramlist') ? $item->paramlist : null; $requestArgs['returndatalist'] = property_exists($item, 'returndatalist') ? $item->returndatalist : null; $request = $this->getRequest($requestArgs); $item = $this->doRequest($item, $request); ResponseController::addData($item->item_id, 'images_total', count($item->urls)); // If error has occured, but it's not related to connection. if ($item->result->is_error === true && $item->result->is_done === true) { $this->dumpMediaItem($item); // item failed, directly dump anything from server. } return $item; } /* Ask to remove the items from the remote cache. @param $item Must be object, with URLS set as array of urllist. - Secretly not a mediaItem - shame */ public function dumpMediaItem($item) { $settings = \wpSPIO()->settings(); $keyControl = ApiKeyController::getInstance(); if (property_exists($item, 'urls') === false || ! is_array($item->urls) || count($item->urls) == 0) { Log::addWarn('Media Item without URLS cannnot be dumped ', $item); return false; } $request = $this->getRequest(); $request['body'] = json_encode( array( 'plugin_version' => SHORTPIXEL_IMAGE_OPTIMISER_VERSION, 'key' => $keyControl->forceGetApiKey(), 'urllist' => $item->urls ) , JSON_UNESCAPED_UNICODE); Log::addDebug('Dumping Media Item ', $item->urls); $ret = wp_remote_post($this->apiDumpEndPoint, $request); return $ret; } /** Former, prepare Request in API */ private function getRequest($args = array()) { $settings = \wpSPIO()->settings(); $keyControl = ApiKeyController::getInstance(); $defaults = array( 'urls' => null, 'paramlist' => null, 'returndatalist' => null, 'compressionType' => $settings->compressionType, 'blocking' => true, 'item_id' => null, 'refresh' => false, 'flags' => array(), ); $args = wp_parse_args($args, $defaults); $convertTo = implode("|", $args['flags']); $requestParameters = array( 'plugin_version' => SHORTPIXEL_IMAGE_OPTIMISER_VERSION, 'key' => $keyControl->forceGetApiKey(), 'lossy' => $args['compressionType'], 'cmyk2rgb' => $settings->CMYKtoRGBconversion, 'keep_exif' => ($settings->keepExif ? "1" : "0"), 'convertto' => $convertTo, 'resize' => $settings->resizeImages ? 1 + 2 * ($settings->resizeType == 'inner' ? 1 : 0) : 0, 'resize_width' => $settings->resizeWidth, 'resize_height' => $settings->resizeHeight, 'urllist' => $args['urls'], ); if (! is_null($args['paramlist'])) { $requestParameters['paramlist'] = $args['paramlist']; } if (! is_null($args['returndatalist'])) { $requestParameters['returndatalist'] = $args['returndatalist']; } if($args['refresh']) { // @todo if previous status was ShortPixelAPI::ERR_INCORRECT_FILE_SIZE; then refresh. $requestParameters['refresh'] = 1; } $requestParameters = apply_filters('shortpixel/api/request', $requestParameters, $args['item_id']); $arguments = array( 'method' => 'POST', 'timeout' => 15, 'redirection' => 3, 'sslverify' => apply_filters('shortpixel/system/sslverify', true), 'httpversion' => '1.0', 'blocking' => $args['blocking'], 'headers' => array(), 'body' => json_encode($requestParameters, JSON_UNESCAPED_UNICODE), 'cookies' => array() ); //add this explicitely only for https, otherwise (for http) it slows down the request if($settings->httpProto !== 'https') { unset($arguments['sslverify']); } return $arguments; } /** DoRequest : Does a remote_post to the API * * @param Object $item The QueueItemObject * @param Array $requestParameters The HTTP parameters for the remote post (arguments in getRequest) */ protected function doRequest($item, $requestParameters ) { $response = wp_remote_post($this->apiEndPoint, $requestParameters ); Log::addDebug('ShortPixel API Request sent', $requestParameters['body']); //only if $Blocking is true analyze the response if ( $requestParameters['blocking'] ) { if ( is_object($response) && get_class($response) == 'WP_Error' ) { $errorMessage = $response->errors['http_request_failed'][0]; $errorCode = self::STATUS_CONNECTION_ERROR; $item->result = $this->returnRetry($errorCode, $errorMessage); } elseif ( isset($response['response']['code']) && $response['response']['code'] <> 200 ) { $errorMessage = $response['response']['code'] . " - " . $response['response']['message']; $errorCode = $response['response']['code']; $item->result = $this->returnFailure($errorCode, $errorMessage); } else { $item->result = $this->handleResponse($item, $response); } } else // This should be only non-blocking the FIRST time it's send off. { if ($item->tries > 0) { Log::addWarn('DOREQUEST sent item non-blocking with multiple tries!', $item); } $urls = count($item->urls); $flags = property_exists($item, 'flags') ? $item->flags : array(); $flags = implode("|", $flags); $text = sprintf(__('New item #%d sent for processing ( %d URLS %s) ', 'shortpixel-image-optimiser'), $item->item_id, $urls, $flags ); $item->result = $this->returnOK(self::STATUS_ENQUEUED, $text ); } return $item; } /** * @param $parsed_url Array Result of parse_url */ private function encodeURL($parsed_url, $url) { //str_replace($parsed_url['path'], urlencode($parsed_url['path']), $url); $path = $parsed_url['path']; //echo strrpos($parsed_url, ','); $filename = substr($path, strrpos($path, '/') + 1); //strrpos($path, '/'); $path = str_replace($filename, urlencode($filename), $url); return $path; } private function parseResponse($response) { $data = $response['body']; $data = json_decode($data); return (array)$data; } /** * **/ private function handleResponse($item, $response) { $APIresponse = $this->parseResponse($response);//get the actual response from API, its an array $settings = \wpSPIO()->settings(); // Don't know if it's this or that. $status = false; if (isset($APIresponse['Status'])) { $status = $APIresponse['Status']; } elseif(is_array($APIresponse) && isset($APIresponse[0]) && property_exists($APIresponse[0], 'Status')) { $status = $APIresponse[0]->Status; } elseif ( is_array($APIresponse)) // This is a workaround for some obscure PHP 5.6 bug. @todo Remove when dropping support PHP < 7. { foreach($APIresponse as $key => $data) { // Running the whole array, because handleSuccess enums on key index as well :/ // we are not just looking for status here, but also replacing the whole array, because of obscure bug. if (property_exists($data, 'Status')) { if ($status === false) { $status = $data->Status; } $APIresponse[$key] = $data; // reset it, so it can read the index. This should be 0. } } } if (isset($APIresponse['returndatalist'])) { $returnDataList = (array) $APIresponse['returndatalist']; if (isset($returnDataList['sizes']) && is_object($returnDataList['sizes'])) $returnDataList['sizes'] = (array) $returnDataList['sizes']; if (isset($returnDataList['doubles']) && is_object($returnDataList['doubles'])) $returnDataList['doubles'] = (array) $returnDataList['doubles']; if (isset($returnDataList['duplicates']) && is_object($returnDataList['duplicates'])) $returnDataList['duplicates'] = (array) $returnDataList['duplicates']; if (isset($returnDataList['fileSizes']) && is_object($returnDataList['fileSizes'])) $returnDataList['fileSizes'] = (array) $returnDataList['fileSizes']; unset($APIresponse['returndatalist']); } else { $returnDataList = array(); } // This is only set if something is up, otherwise, ApiResponse returns array if (is_object($status)) { // Check for known errors. : https://shortpixel.com/api-docs Log::addDebug('Api Response Status :' . $status->Code ); switch($status->Code) { case -102: // Invalid URL case -105: // URL missing case -106: // Url is inaccessible case -113: // Too many inaccessible URLs case -201: // Invalid image format case -202: // Invalid image or unsupported format case -203: // Could not download file return $this->returnFailure( self::STATUS_ERROR, $status->Message); break; case -403: // Quota Exceeded case -301: // The file is larger than remaining quota // legacy @delete_option('bulkProcessingStatus'); QuotaController::getInstance()->setQuotaExceeded(); return $this->returnRetry( self::STATUS_QUOTA_EXCEEDED, __('Quota exceeded.','shortpixel-image-optimiser')); break; case -306: return $this->returnFailure( self::STATUS_FAIL, __('Files need to be from a single domain per request.', 'shortpixel-image-optimiser')); break; case -401: // Invalid Api Key case -402: // Wrong API key return $this->returnFailure( self::STATUS_NO_KEY, $status->Message); break; case -404: // Maximum number in optimization queue (remote) //return array("Status" => self::STATUS_QUEUE_FULL, "Message" => $APIresponse['Status']->Message); return $this->returnRetry( self::STATUS_QUEUE_FULL, $status->Message); case -500: // API in maintenance. //return array("Status" => self::STATUS_MAINTENANCE, "Message" => $APIresponse['Status']->Message); return $this->returnRetry( self::STATUS_MAINTENANCE, $status->Message); } } $neededURLS = $item->urls; // URLS we are waiting for. if ( is_array($APIresponse) && isset($APIresponse[0]) ) //API returned image details { if (! isset($returnDataList['sizes'])) { return $this->returnFailure(self::STATUS_FAIL, __('Item did not return image size information. This might be a failed queue item. Reset the queue if this persists or contact support','shortpixel-image-optimiser')); } // return $this->returnFailure(self::STATUS_FAIL, __('Unrecognized API response. Please contact support.','shortpixel-image-optimiser')); $analyze = array('total' => count($item->urls), 'ready' => 0, 'waiting' => 0); $waitingDebug = array(); $imageList = array(); $partialSuccess = false; $imageNames = array_keys($returnDataList['sizes']); $fileNames = array_values($returnDataList['sizes']); foreach($APIresponse as $index => $imageObject) { if (! property_exists($imageObject, 'Status')) { Log::addWarn('Result without Status', $imageObject); continue; // can't do nothing with that, probably not an image. } elseif ($imageObject->Status->Code == self::STATUS_UNCHANGED || $imageObject->Status->Code == self::STATUS_WAITING) { $analyze['waiting']++; $partialSuccess = true; // Not the whole job has been done. } elseif ($imageObject->Status->Code == self::STATUS_SUCCESS) { $analyze['ready']++; $imageName = $imageNames[$index]; $fileName = $fileNames[$index]; $data = array( 'fileName' => $fileName, 'imageName' => $imageName, ); // Filesize might not be present, but also imageName ( only if smartcrop is done, might differ per image) if (isset($returnDataList['fileSizes']) && isset($returnDataList['fileSizes'][$imageName])) { $data['fileSize'] = $returnDataList['fileSizes'][$imageName]; } if (! isset($item->files[$imageName])) { $imageList[$imageName] = $this->handleNewSuccess($item, $imageObject, $data); } else { } } } $imageData = array( 'images_done' => $analyze['ready'], 'images_waiting' => $analyze['waiting'], 'images_total' => $analyze['total'] ); ResponseController::addData($item->item_id, $imageData); if (count($imageList) > 0) { $data = array( 'files' => $imageList, 'data' => $returnDataList, ); if (false === $partialSuccess) { return $this->returnSuccess($data, self::STATUS_SUCCESS, false); } else { return $this->returnSuccess($data, self::STATUS_PARTIAL_SUCCESS, false); } } elseif ($analyze['waiting'] > 0) { return $this->returnOK(self::STATUS_UNCHANGED, sprintf(__('Item is waiting', 'shortpixel-image-optimiser'))); } else { // Theoretically this should not be needed. Log::addWarn('ApiController Response not handled before default case'); if ( isset($APIresponse[0]->Status->Message) ) { $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 . ")"); return $this->returnRetry($err['Code'], $err['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)); return $this->returnRetry($err['Code'], $err['Message']); } } } // ApiResponse[0] // If this code reaches here, something is wrong. if(!isset($APIresponse['Status'])) { Log::addError('API returned Unknown Status/Response ', $response); return $this->returnFailure(self::STATUS_FAIL, __('Unrecognized API response. Please contact support.','shortpixel-image-optimiser')); } else { //sometimes the response array can be different if (is_numeric($APIresponse['Status']->Code)) { $message = $APIresponse['Status']->Message; } else { $message = $APIresponse[0]->Status->Message; } if (! isset($message) || is_null($message) || $message == '') { $message = __('Unrecognized API message. Please contact support.','shortpixel-image-optimiser'); } return $this->returnRetry(self::STATUS_FAIL, $message); } // else } // handleResponse function private function handleNewSuccess($item, $fileData, $data) { $compressionType = property_exists($item, 'compressionType') ? $item->compressionType : $settings->compressionType; //$savedSpace = $originalSpace = $optimizedSpace = $fileCount = 0; $defaults = array( 'fileName' => false, 'imageName' => false, 'fileSize' => false, ); $data = wp_parse_args($data, $defaults); if (false === $data['fileName'] || false === $data['imageName']) { Log::addError('Failure! HandleSuccess did not receive filename or imagename! ', $data); Log::addError('Error Item:', $item); return $this->returnFailure(self::STATUS_FAIL, __('Internal error, missing variables')); } $originalFileSize = (false === $data['fileSize']) ? intval($fileData->OriginalSize) : $data['fileSize']; $image = array( 'image' => array( 'url' => false, 'originalSize' => $originalFileSize, 'optimizedSize' => false, 'status' => self::STATUS_SUCCESS, ), 'webp' => array( 'url' => false, 'size' => false, 'status' => self::STATUS_SKIP, ), 'avif' => array( 'url' => false, 'size' => false, 'status' => self::STATUS_SKIP, ), ); $fileType = ($compressionType > 0) ? 'LossyURL' : 'LosslessURL'; $fileSize = ($compressionType > 0) ? 'LossySize' : 'LosslessSize'; // if originalURL and OptimizedURL is the same, API is returning it as the same item, aka not optimized. if ($fileData->$fileType === $fileData->OriginalURL) { $image['image']['status'] = self::STATUS_UNCHANGED; } else { $image['image']['url'] = $fileData->$fileType; $image['image']['optimizedSize'] = intval($fileData->$fileSize); } // Don't download if the originalSize / OptimizedSize is the same ( same image ) . This can be non-opt result or it was not asked to be optimized( webp/avif only job i.e. ) if ($image['image']['originalSize'] == $image['image']['optimizedSize']) { $image['image']['status'] = self::STATUS_UNCHANGED; } $checkFileSize = intval($fileData->$fileSize); // Size of optimized image to check against Avif/Webp if (false === $this->checkFileSizeMargin($originalFileSize, $checkFileSize)) { $image['image']['status'] = self::STATUS_OPTIMIZED_BIGGER; $checkFileSize = $originalFileSize; } if (property_exists($fileData, "WebP" . $fileType)) { $type = "WebP" . $fileType; $size = "WebP" . $fileSize; if ($fileData->$type != 'NA') { $image['webp']['url'] = $fileData->$type; $image['webp']['size'] = $fileData->$size; if (false === $this->checkFileSizeMargin($checkFileSize, $fileData->$size)) { $image['webp']['status'] = self::STATUS_OPTIMIZED_BIGGER; } else { $image['webp']['status'] = self::STATUS_SUCCESS; } } } if (property_exists($fileData, "AVIF" . $fileType)) { $type = "AVIF" . $fileType; $size = "AVIF" . $fileSize; if ($fileData->$type != 'NA') { $image['avif']['url'] = $fileData->$type; $image['avif']['size'] = $fileData->$size; if (false === $this->checkFileSizeMargin($checkFileSize, $fileData->$size)) { $image['avif']['status'] = self::STATUS_OPTIMIZED_BIGGER; } else { $image['avif']['status'] = self::STATUS_SUCCESS; } } } return $image; } private function getResultObject() { $result = new \stdClass; $result->apiStatus = null; $result->message = ''; $result->is_error = false; $result->is_done = false; //$result->errors = array(); return $result; } private function returnFailure($status, $message) { $result = $this->getResultObject(); $result->apiStatus = $status; $result->message = $message; $result->is_error = true; $result->is_done = true; return $result; // fatal. } // Temporary Error, retry. private function returnRetry($status, $message) { $result = $this->getResultObject(); $result->apiStatus = $status; $result->message = $message; //$result->errors[] = array('status' => $status, 'message' => $message); $result->is_error = true; return $result; } private function returnOK($status = self::STATUS_UNCHANGED, $message = false) { $result = $this->getResultObject(); $result->apiStatus = $status; $result->is_error = false; $result->message = $message; return $result; } /** Returns a success status. This is succeseption, each file gives it's own status, bundled. */ private function returnSuccess($file, $status = self::STATUS_SUCCESS, $message = false) { $result = $this->getResultObject(); $result->apiStatus = $status; $result->message = $message; if (self::STATUS_SUCCESS === $status) $result->is_done = true; if (is_array($file)) $result->files = $file; else $result->file = $file; // this file is being used in imageModel return $result; } // If this returns false, the resultSize is bigger, thus should be oversize. private function checkFileSizeMargin($fileSize, $resultSize) { // This is ok. if ($fileSize >= $resultSize) return true; // Fine suppose, but crashes the increase if ($fileSize == 0) return true; $percentage = apply_filters('shortpixel/api/filesizeMargin', 5); $increase = (($resultSize - $fileSize) / $fileSize) * 100; if ($increase <= $percentage) return true; return false; } } // class