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. 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; } }