wp_back/wp-content/plugins/shortpixel-image-optimiser/class/external/offload/wp-offload-media.php
2024-05-20 15:37:46 +03:00

691 lines
21 KiB
PHP

<?php
namespace ShortPixel\External\Offload;
use ShortPixel\Model\File\FileModel as FileModel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Notices\NoticeController as Notice;
use ShortPixel\Controller\QuotaController as QuotaController;
use ShortPixel\Controller\ResponseController as ResponseController;
// @integration WP Offload Media Lite
class wpOffload
{
protected $as3cf;
protected $active = false;
protected $offloading = true;
private $itemClassName;
private $useHandlers = false; // Check for newer ItemHandlers or Compat mode.
protected $shouldPrevent = true; // if offload should be prevented. This is turned off when SPIO want to tell S3 to offload. Better than removing filter.
protected $settings;
protected $is_cname = false;
protected $cname;
private static $sources; // cache for url > source_id lookup, to prevent duplicate queries.
private static $offloadPrevented = array();
// if might have to do these checks many times for each thumbnails, keep it fastish.
//protected $retrievedCache = array();
public function __construct($as3cf)
{
// This must be called before WordPress' init.
$this->init($as3cf);
}
public function init($as3cf)
{
if (! class_exists('\DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item'))
{
Notice::addWarning(__('Your S3-Offload plugin version doesn\'t seem to be compatible. Please upgrade the S3-Offload plugin', 'shortpixel-image-optimiser'), true);
return false;
}
$this->itemClassName = '\DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item';
if (method_exists($as3cf, 'get_item_handler'))
{
$this->useHandlers = true; // we have a new version
}
else {
Notice::addWarning(__('Your S3-Offload plugin version doesn\'t seem to be compatible. Please upgrade the S3-Offload plugin', 'shortpixel-image-optimiser'), true);
return false;
}
$this->as3cf = $as3cf;
$this->active = true;
// if setting to upload to bucket is off, don't hook or do anything really.
if (! $this->as3cf->get_setting( 'copy-to-s3' ))
{
$this->offloading = false;
}
/* // Lets see if this can be without
if ('cloudfront' === $this->as3cf->get_setting( 'domain' ))
{
$this->is_cname = true;
$this->cname = $this->as3cf->get_setting( 'cloudfront' );
} */
// $provider = $this->as3cf->get_provider();
add_action('shortpixel/image/optimised', array($this, 'image_upload'), 10);
add_action('shortpixel/image/after_restore', array($this, 'image_restore'), 10, 3); // hit this when restoring.
add_action('shortpixel-thumbnails-before-regenerate', array($this, 'remove_remote'), 10);
add_action('shortpixel/converter/prevent-offload', array($this, 'preventOffload'), 10);
add_action('shortpixel/converter/prevent-offload-off', array($this, 'preventOffloadOff'), 10);
// add_action('shortpixel_restore_after_pathget', array($this, 'remove_remote')); // not optimal -> has to do w/ doRestore and when URL/PATH is available when not on server .
// Seems this better served by _after? If it fails, it's removed from remote w/o filechange.
// add_action('shortpixel/image/convertpng2jpg_before', array($this, 'remove_remote'));
add_filter('as3cf_attachment_file_paths', array($this, 'add_webp_paths'));
// add_filter('as3cf_remove_source_files_from_provider', array($this, 'remove_webp_paths'), 10);
// add_action('shortpixel/image/convertpng2jpg_success', array($this, 'image_converted'), 10);
add_filter('as3cf_remove_source_files_from_provider', array($this, 'remove_webp_paths'));
// add_filter('shortpixel/restore/targetfile', array($this, 'returnOriginalFile'),10,2);
add_filter('as3cf_pre_update_attachment_metadata', array($this, 'preventUpdateMetaData'), 10,4);
add_filter('as3cf_pre_handle_item_upload', array($this, 'preventInitialUploadHandler'), 10,3);
//add_filter('as3cf_get_attached_file', array($this, 'fixScaledUrl'), 10, 4);
add_filter('shortpixel_get_original_image_path', array($this, 'checkScaledUrl'), 10,2);
// add_filter('as3cf_get_attached_file_noop', array($this, 'fixScaledUrl'), 10,4);
//add_filter('shortpixel_get_attached_file', array($this, 'get_raw_attached_file'),10, 2);
// add_filter('shortpixel_get_original_image_path', array($this, 'get_raw_original_path'), 10, 2);
add_filter('shortpixel/image/urltopath', array($this, 'checkIfOffloaded'), 10,2);
add_filter('shortpixel/file/virtual/translate', array($this, 'getLocalPathByURL'));
// for webp picture paths rendered via output
// add_filter('shortpixel_webp_image_base', array($this, 'checkWebpRemotePath'), 10, 2);
add_filter('shortpixel/front/webp_notfound', array($this, 'fixWebpRemotePath'), 10, 4);
// Fix for updating source paths when converting
add_action('shortpixel/image/convertpng2jpg_success', array($this, 'updateOriginalPath'));
}
public function returnOriginalFile($file, $attach_id)
{
$file = get_attached_file($attach_id, true);
return $file;
}
private function getMediaClass()
{
if ($this->useHandlers)
{
$class = $this->as3cf->get_source_type_class('media-library');
}
else
{
$class = $this->itemClassName; //backward compat.
}
return $class;
}
// This is used in the converted. Might be deployed elsewhere for better control.
public function preventOffload($attach_id)
{
self::$offloadPrevented[$attach_id] = true;
}
public function preventOffloadOff($attach_id)
{
unset(self::$offloadPrevented[$attach_id]);
}
// When Offload is not offloaded but is created during the process of generate metadata in WP, wp_create_image_subsizes fires an update metadata after just moving the upload, before making any thumbnails. If this is the case and the file has an -scaled / original image setup, the original_source_path becomes the same as the source_path which creates issue later on when dealing with optimizing it, if the file is deleted on local server. Prevent this, and lean on later update metadata.
public function preventUpdateMetaData($bool, $data, $post_id, $old_provider_object)
{
if (isset(self::$offloadPrevented[$post_id]))
{
return true ; // return true to cancel.
}
return $bool;
}
/**
* @param $id attachment id (WP)
* @param $mediaItem MediaLibraryModel SPIO
* @param $clean - boolean - if restore did all files (clean) or partial (not clean)
*/
public function image_restore($mediaItem, $id, $clean)
{
$settings = \wpSPIO()->settings();
// Only medialibrary offloading supported.
if ('media' !== $mediaItem->get('type') )
{
return false;
}
// If there are excluded sizes, there are not in backups. might not be left on remote, or ( if delete ) on server, so just generate the images and move them.
$mediaItem->wpCreateImageSizes();
$result = $this->remove_remote($id);
$this->image_upload($mediaItem);
}
public function remove_remote($id)
{
$a3cfItem = $this->getItemById($id); // MediaItem is AS3CF Object
if ($a3cfItem === false)
{
Log::addDebug('S3-Offload MediaItem not remote - ' . $id);
return false;
}
$remove = \DeliciousBrains\WP_Offload_Media\Items\Remove_Provider_Handler::get_item_handler_key_name();
$itemHandler = $this->as3cf->get_item_handler($remove);
$result = $itemHandler->handle($a3cfItem, array( 'verify_exists_on_local' => false)); //handle it then.
return $result;
}
/** @return Returns S3Ofload MediaItem, or false when this does not exist */
protected function getItemById($id, $create = false)
{
$class = $this->getMediaClass();
$mediaItem = $class::get_by_source_id($id);
if (true === $create && $mediaItem === false)
{
$mediaItem = $class::create_from_source_id($id);
}
return $mediaItem;
}
/** Cache source requests to improve performance
* @param $url string The URL that is being checked
* @param $source_id int Source ID of the item URL to be cached
* @return int|boolean|null Returns source_if or false ( not offloaded ) if found, returns null if not sourcecached.
*/
private function sourceCache($url, $source_id = null)
{
if ($source_id === null && isset(static::$sources[$url]))
{
$source_id = static::$sources[$url];
return $source_id;
}
elseif ($source_id !== null)
{
if (! isset(static::$sources[$url]))
{
static::$sources[$url] = $source_id;
}
return $source_id;
}
return null;
}
public function checkIfOffloaded($bool, $url)
{
$source_id = $this->sourceCache($url);
$orig_url = $url;
if (is_null($source_id))
{
$extension = substr($url, strrpos($url, '.') + 1);
// If these filetypes are not in the cache, they cannot be found via geSourceyIDByUrl method ( not in path DB ), so it's pointless to try. If they are offloaded, at some point the extra-info might load.
if ($extension == 'webp' || $extension == 'avif')
{
return false;
}
$source_id = $this->getSourceIDByURL($url);
}
else {
}
if ($source_id !== false)
{
return FileModel::$VIRTUAL_REMOTE;
}
else
{
return false;
}
}
protected function getSourceIDByURL($url)
{
$source_id = $this->sourceCache($url); // check cache first.
$cacheHit = false; // prevent a cache hit to be cached again.
$raw_url = $url; // keep raw. If resolved, add the raw url to the cache.
// If in cache, we are done.
if (! is_null($source_id))
{
return $source_id;
}
if (is_null($source_id)) // check on the raw url.
{
$class = $this->getMediaClass();
$parsedUrl = parse_url($url);
if (! isset($parsedUrl['scheme']) || ! in_array($parsedUrl['scheme'], array('http','https')))
{
$url = 'http://' . $url; //str_replace($parsedUrl['scheme'], 'https', $url);
}
$source_id = $this->sourceCache($url);
if(is_null($source_id))
{
$source = $class::get_item_source_by_remote_url($url);
$source2 = $class::get_item_source_by_remote_url($raw_url);
$source_id = isset($source['id']) ? intval($source['id']) : null;
}
else {
$cacheHit = true; // hit the cache. Yeah.
$this->sourceCache($raw_url, $source_id);
}
}
if (is_null($source_id)) // check now via the thumbnail hocus.
{
$pattern = '/(.*)-\d+[xX]\d+(\.\w+)/m';
$url = preg_replace($pattern, '$1$2', $url);
$source_id = $this->sourceCache($url); // check cache first.
if (is_null($source_id))
{
$source = $class::get_item_source_by_remote_url($url);
$source_id = isset($source['id']) ? intval($source['id']) : null;
}
else {
$cacheHit = true;
$this->sourceCache($raw_url , $source_id);
}
}
// Check issue with double extensions. If say double webp/avif is on, the double extension causes the URL not to be found (ie .jpg)
if (is_null($source_id))
{
if (substr_count($parsedUrl['path'], '.') > 1)
{
// Get extension
$ext = substr(strrchr($url, '.'), 1);
// Remove all extensions from the URL
$checkurl = substr($url, 0, strpos($url,'.')) ;
// Add back the last one.
$checkurl .= '.' . $ext;
// Retry
$source_id = $this->sourceCache($checkurl); // check cache first.
if (is_null($source_id))
{
$source = $class::get_item_source_by_remote_url($url);
$source_id = isset($source['id']) ? intval($source['id']) : null;
}
else {
$cacheHit = true;
$this->sourceCache($raw_url , $source_id);
}
}
}
if(is_null($source_id))
{
$source_id = false;
}
if (false === $cacheHit)
{
$this->sourceCache($url, $source_id); // cache it.
}
if ($source_id !== false && false === $cacheHit)
{
// get item
$item = $this->getItemById($source_id);
if (is_object($item) && method_exists($item, 'extra_info'))
{
$baseUrl = str_replace(basename($url),'', $url);
//$rawBaseUrl =
$extra_info = $item->extra_info();
if (isset($extra_info['objects']))
{
foreach($extra_info['objects'] as $extraItem)
{
if (is_array($extraItem) && isset($extraItem['source_file']))
{
// Add source stuff into cache.
$this->sourceCache($baseUrl . $extraItem['source_file'], $source_id);
}
}
}
}
}
return $source_id;
}
// @param s3 based URL that which is needed for finding local path
// @return String Filepath. Translated file path
public function getLocalPathByURL($url)
{
$source_id = $this->getSourceIDByURL($url);
if ($source_id === false)
{
return false;
}
$item = $this->getItemById($source_id);
$original_path = $item->original_source_path(); // $values['original_source_path'];
if (wp_basename($url) !== wp_basename($original_path)) // thumbnails translate to main file.
{
$original_path = str_replace(wp_basename($original_path), wp_basename($url), $original_path);
}
$fs = \wpSPIO()->filesystem();
$base = $fs->getWPUploadBase();
$file = $base . $original_path;
return $file;
}
/** Converted after png2jpg
*
* @param MediaItem Object SPIO
*/
public function image_converted($mediaItem)
{
$fs = \wpSPIO()->fileSystem();
$id = $mediaItem->get('id');
//$this->remove_remote($id);
$this->image_upload($mediaItem);
}
public function image_upload($mediaLibraryObject)
{
$id = $mediaLibraryObject->get('id');
$a3cfItem = $this->getItemById($id);
// Only medialibrary offloading supported.
if ('media' !== $mediaLibraryObject->get('type') )
{
return false;
}
if ( false === $a3cfItem)
{
return false;
}
$item = $this->getItemById($id, true);
if ( $item === false && ! $this->as3cf->get_setting( 'copy-to-s3' ) ) {
// abort if not already uploaded to provider and the copy setting is off
Log::addDebug('As3cf image upload is off and object not previously uploaded');
return false;
}
// Add Web/Avifs back under new method.
$this->shouldPrevent = false;
// The Handler doesn't work properly /w local removal if not the exact correct files are passed (?) . Offload does this probably via update metadata function, so let them sort it out with this . (until it breaks)
$meta = wp_get_attachment_metadata($id);
wp_update_attachment_metadata($id, $meta);
$this->shouldPrevent = true;
}
// WP Offload -for some reason - returns the same result of get_attached_file and wp_get_original_image_path , which are different files (one scaled) which then causes a wrong copy action after optimizing the image ( wrong destination download of the remote file ). This happens if offload with delete is on. Attempt to fix the URL to reflect the differences between -scaled and not.
public function checkScaledUrl($filepath, $id)
{
// Original filepath can never have a scaled in there.
// @todo This should probably check -scaled.<extension> as string end preventing issues.
if (strpos($filepath, '-scaled') !== false)
{
$filepath = str_replace('-scaled', '', $filepath);
}
return $filepath;
}
/** This function will cut out the initial upload to S3Offload . This cuts it off in the new handle area, leaving other updating in tact.
*/
public function preventInitialUploadHandler($bool, $as3cf_item, $options)
{
$fs = \wpSPIO()->filesystem();
$settings = \WPSPIO()->settings();
$post_id = $as3cf_item->source_id();
$quotaController = quotaController::getInstance();
if ($quotaController->hasQuota() === false)
{
return false;
}
if (! $this->offloading)
{
return false;
}
if ($this->shouldPrevent === false) // if false is returned, it's NOT prevented, so on-going.
{
return false;
}
if (isset(self::$offloadPrevented[$post_id]))
{
Log::addDebug('Offload Prevented via static for '. $post_id);
$error = new \WP_Error( 'upload-prevented', 'No offloading at this time, thanks' );
return $error;
}
Log::addDebug('Not preventing S3 Offload');
return $bool;
}
public function updateOriginalPath($imageModel)
{
$post_id = $imageModel->get('id');
$item = $this->getItemById($post_id);
if (false === $item) // item not offloaded.
{
return false;
}
$original_path = $item->original_path(); // Original path (non-scaled-)
$original_source_path = $item->original_source_path();
$path = $item->path();
$source_path = $item->source_path();
$wp_original = wp_get_original_image_path($post_id, apply_filters( 'emr_unfiltered_get_attached_file', true ));
$wp_original = apply_filters('emr/replace/original_image_path', $wp_original, $post_id);
$wp_source = trim(get_attached_file($post_id, apply_filters( 'emr_unfiltered_get_attached_file', true )));
$updated = false;
// If image is replaced with another name, the original soruce path will not match. This could also happen when an image is with -scaled as main is replaced by an image that doesn't have it. In all cases update the table to reflect proper changes.
if (wp_basename($wp_original) !== wp_basename($original_path))
{
$newpath = str_replace( wp_basename( $original_path ), wp_basename($wp_original), $original_path );
$item->set_original_path($newpath);
$newpath = str_replace( wp_basename( $original_source_path ), wp_basename($wp_original), $original_source_path );
$updated = true;
$item->set_original_source_path($newpath);
$item->save();
}
}
private function getWebpPaths($paths, $check_exists = true)
{
$newPaths = array();
$fs = \wpSPIO()->fileSystem();
foreach($paths as $size => $path)
{
$file = $fs->getFile($path);
$basedir = $file->getFileDir();
if (is_null($basedir)) // This could only happen if path is completely empty.
{
continue;
}
$basepath = $basedir->getPath();
$newPaths[$size] = $path;
$webpformat1 = $basepath . $file->getFileName() . '.webp';
$webpformat2 = $basepath . $file->getFileBase() . '.webp';
$avifformat = $basepath . $file->getFileName() . '.avif';
$avifformat2 = $basepath . $file->getFileBase() . '.avif';
if ($check_exists)
{
if (file_exists($webpformat1))
$newPaths[$size . '_webp'] = $webpformat1;
}
else {
$newPaths[$size . '_webp'] = $webpformat1;
}
if ($check_exists)
{
if(file_exists($webpformat2))
$newPaths[$size . '_webp2'] = $webpformat2;
}
else {
$newPaths[$size . '_webp2'] = $webpformat2;
}
if ($check_exists)
{
if (file_exists($avifformat))
{
$newPaths[$size . '_avif'] = $avifformat;
}
}
else {
$newPaths[$size . '_avif'] = $avifformat;
}
if ($check_exists)
{
if (file_exists($avifformat2))
{
$newPaths[$size . '_avif2'] = $avifformat2;
}
}
else {
$newPaths[$size . '_avif2'] = $avifformat2;
}
}
return $newPaths;
}
/** Get Webp Paths that might be generated and offload them as well.
* Paths - size : path values
*/
public function add_webp_paths($paths)
{
$paths = $this->getWebpPaths($paths, true);
//Log::addDebug('Add S3 Paths - ', array($paths));
return $paths;
}
public function remove_webp_paths($paths)
{
$paths = $this->getWebpPaths($paths, false);
//Log::addDebug('Remove S3 Paths', array($paths));
return $paths;
}
// GetbyURL can't find thumbnails, only the main image. Check via extrainfo method if we can find needed filetype
// @param $bool Boolean
// @param $fileObj FileModel The webp file we are searching for
// @param $url string The URL of the main file ( aka .jpg )
// @param $imagebaseDir DirectoryModel The remote path / path this all takes place at.
public function fixWebpRemotePath($bool, $fileObj, $url, $imagebaseDir)
{
$source_id = $this->getSourceIDByURL($url);
if (false === $source_id)
return false;
$item = $this->getItemById($source_id);
$extra_info = $item->extra_info();
if (! isset( $extra_info['objects'] ) || ! is_array( $extra_info['objects'] ) )
return false;
$bool = false;
foreach($extra_info['objects'] as $data)
{
$sourceFile = $data['source_file'];
if ($sourceFile == $fileObj->getFileName())
{
$bool = true;
return $fileObj;
break;
}
}
return $bool;
}
}