This commit is contained in:
2024-05-20 15:37:46 +03:00
commit 00b7dbd0b7
10404 changed files with 3285853 additions and 0 deletions

View File

@ -0,0 +1,2 @@
#make sure the plugin.json file is loaded for auto-loading the classes, in case json files are excluded via gitignore
!plugin.json

View File

@ -0,0 +1,67 @@
<?php
namespace ShortPixel;
class BuildAutoLoader
{
public static function buildJSON()
{
echo 'Building Plugin.JSON';
$plugin = array(
'name' => 'ShortPixel/Plugin',
'description' => 'ShortPixel AutoLoader',
'type' => 'function',
'autoload' => array('psr-4' => array('ShortPixel' => 'class'),
'files' => self::getFiles(),
),
);
$f = fopen('class/plugin.json', 'w');
$result = fwrite($f, json_encode($plugin));
if ($result === false)
echo "!!! Error !!! Could not write Plugin.json";
fclose($f);
}
public static function getFiles()
{
$main = array(
// 'shortpixel_api.php',
// 'class/wp-short-pixel.php',
'class/wp-shortpixel-settings.php',
// 'class/view/shortpixel_view.php',
'class/front/img-to-picture-webp.php',
);
$models = array(
);
$externals = array(
'class/external/cloudflare.php',
//'class/external/gravityforms.php',
'class/external/nextgen/nextGenController.php',
'class/external/nextgen/nextGenViewController.php',
'class/external/visualcomposer.php',
'class/external/offload/Offloader.php',
'class/external/offload/wp-offload-media.php',
'class/external/offload/virtual-filesystem.php',
'class/external/wp-cli/wp-cli-base.php',
'class/external/wp-cli/wp-cli-single.php',
'class/external/wp-cli/wp-cli-bulk.php',
'class/external/image-galleries.php',
'class/external/pantheon.php',
'class/external/spai.php',
'class/external/cache.php',
'class/external/uncode.php',
'class/external/query-monitor.php',
'class/external/Woocommerce.php',
'class/external/themes/total-theme.php',
);
echo "Build Plugin.JSON ";
return array_merge($main,$models,$externals);
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\Helper\UiHelper as UiHelper;
/** Proto parent class for all controllers.
*
* So far none of the controller need or implement similar enough functions for a parent to make sense. * Perhaps this will change of time, so most are extending this parent.
**/
// @todo Think how to do this better.
class Controller
{
protected $model;
protected $userIsAllowed = false;
public function __construct()
{
$this->userIsAllowed = $this->checkUserPrivileges();
}
protected function checkUserPrivileges()
{
if ((current_user_can( 'manage_options' ) || current_user_can( 'upload_files' ) || current_user_can( 'edit_posts' )))
return true;
return false;
}
// helper for a helper.
protected function formatNumber($number, $precision = 2)
{
return UIHelper::formatNumber($number, $precision);
}
} // class

View File

@ -0,0 +1,509 @@
<?php
namespace ShortPixel\Controller;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Notices\NoticeController as Notices;
use ShortPixel\Controller\Queue\Queue as Queue;
use ShortPixel\Model\Converter\Converter as Converter;
use ShortPixel\Model\Converter\ApiConverter as ApiConverter;
use ShortPixel\Model\Image\MediaLibraryModel as MediaLibraryModel;
use ShortPixel\Model\Image\ImageModel as ImageModel;
use ShortPixel\Model\AccessModel as AccessModel;
use ShortPixel\Helper\UtilHelper as UtilHelper;
/* AdminController is meant for handling events, hooks, filters in WordPress where there is *NO* specific or more precise ShortPixel Page active.
*
* This should be a delegation class connection global hooks and such to the best shortpixel handler.
*/
class AdminController extends \ShortPixel\Controller
{
protected static $instance;
private static $preventUploadHook = array();
public static function getInstance()
{
if (is_null(self::$instance))
self::$instance = new AdminController();
return self::$instance;
}
/** Handling upload actions
* @hook wp_generate_attachment_metadata
*/
public function handleImageUploadHook($meta, $id)
{
// Media only hook
if ( in_array($id, self::$preventUploadHook))
{
return $meta;
}
// todo add check here for mediaitem
$fs = \wpSPIO()->filesystem();
$fs->flushImageCache(); // it's possible file just changed by external plugin.
$mediaItem = $fs->getImage($id, 'media');
if ($mediaItem === false)
{
Log::addError('Handle Image Upload Hook triggered, by error in image :' . $id );
return $meta;
}
if ($mediaItem->getExtension() == 'pdf')
{
$settings = \wpSPIO()->settings();
if (! $settings->optimizePdfs)
{
Log::addDebug('Image Upload Hook detected PDF, which is turned off - not optimizing');
return $meta;
}
}
if ($mediaItem->isProcessable())
{
$converter = Converter::getConverter($mediaItem, true);
if (is_object($converter) && $converter->isConvertable())
{
$args = array('runReplacer' => false);
$converter->convert($args);
$mediaItem = $fs->getImage($id, 'media');
$meta = $converter->getUpdatedMeta();
}
$control = new OptimizeController();
$control->addItemToQueue($mediaItem);
}
else {
Log::addWarn('Passed mediaItem is not processable', $mediaItem);
}
return $meta; // It's a filter, otherwise no thumbs
}
public function preventImageHook($id)
{
self::$preventUploadHook[] = $id;
}
// Placeholder function for heic and such, return placeholder URL in image to help w/ database replacements after conversion.
public function checkPlaceHolder($url, $post_id)
{
if (false === strpos($url, 'heic'))
return $url;
$extension = pathinfo($url, PATHINFO_EXTENSION);
if (false === in_array($extension, ApiConverter::CONVERTABLE_EXTENSIONS))
{
return $url;
}
$fs = \wpSPIO()->filesystem();
$mediaImage = $fs->getImage($post_id, 'media');
if (false === $mediaImage)
{
return $url;
}
if (false === $mediaImage->getMeta()->convertMeta()->hasPlaceholder())
{
return $url;
}
$url = str_replace($extension, 'jpg', $url);
return $url;
}
public function processQueueHook($args = array())
{
$defaults = array(
'wait' => 3, // amount of time to wait for next round. Prevents high loads
'run_once' => false, // If true queue must be run at least every few minutes. If false, it tries to complete all.
'queues' => array('media','custom'),
'bulk' => false,
);
if (wp_doing_cron())
{
$this->loadCronCompat();
}
$args = wp_parse_args($args, $defaults);
$control = new OptimizeController();
if ($args['bulk'] === true)
{
$control->setBulk(true);
}
if ($args['run_once'] === true)
{
return $control->processQueue($args['queues']);
}
$running = true;
$i = 0;
while($running)
{
$results = $control->processQueue($args['queues']);
$running = false;
foreach($args['queues'] as $qname)
{
if (property_exists($results, $qname))
{
$result = $results->$qname;
// If Queue is not completely empty, there should be something to do.
if ($result->qstatus != QUEUE::RESULT_QUEUE_EMPTY)
{
$running = true;
continue;
}
}
}
sleep($args['wait']);
}
}
public function scanCustomFoldersHook($args = array() )
{
$defaults = array(
'force' => false,
'wait' => 3,
);
$args = wp_parse_args($args, $defaults);
$otherMediaController = OtherMediaController::getInstance();
$running = true;
while (true === $running)
{
$result = $otherMediaController->doNextRefreshableFolder($args);
if (false === $result) // stop on false return.
{
$running = false;
}
sleep($args['wait']);
}
}
// WP functions that are not loaded during Cron Time.
protected function loadCronCompat()
{
if (! function_exists('download_url'))
{
include(ABSPATH . "wp-admin/includes/admin.php");
}
}
/** Filter for Medialibrary items in list and grid view. Because grid uses ajax needs to be caught more general.
* @handles pre_get_posts
* @param WP_Query $query
*
* @return WP_Query
*/
public function filter_listener($query)
{
global $pagenow;
if ( empty( $query->query_vars["post_type"] ) || 'attachment' !== $query->query_vars["post_type"] ) {
return $query;
}
if ( ! in_array( $pagenow, array( 'upload.php', 'admin-ajax.php' ) ) ) {
return $query;
}
$filter = $this->selected_filter_value( 'shortpixel_status', 'all' );
// No filter
if ($filter == 'all')
{
return $query;
}
// add_filter( 'posts_join', array( $this, 'filter_join' ), 10, 2 );
add_filter( 'posts_where', array( $this, 'filter_add_where' ), 10, 2 );
// add_filter( 'posts_orderby', array( $this, 'query_add_orderby' ), 10, 2 );
return $query;
}
public function filter_add_where ($where, $query)
{
global $wpdb;
$filter = $this->selected_filter_value( 'shortpixel_status', 'all' );
$tableName = UtilHelper::getPostMetaTable();
switch($filter)
{
case 'all':
break;
case 'unoptimized':
// The parent <> %d exclusion is meant to also deselect duplicate items ( translations ) since they don't have a status, but shouldn't be in a list like this.
$sql = " AND " . $wpdb->posts . '.ID not in ( SELECT attach_id FROM ' . $tableName . " WHERE (parent = %d and status = %d) OR parent <> %d ) ";
$where .= $wpdb->prepare($sql, MediaLibraryModel::IMAGE_TYPE_MAIN, ImageModel::FILE_STATUS_SUCCESS, MediaLibraryModel::IMAGE_TYPE_MAIN);
break;
case 'optimized':
$sql = ' AND ' . $wpdb->posts . '.ID in ( SELECT attach_id FROM ' . $tableName . ' WHERE parent = %d and status = %d) ';
$where .= $wpdb->prepare($sql, MediaLibraryModel::IMAGE_TYPE_MAIN, ImageModel::FILE_STATUS_SUCCESS);
break;
case 'prevented':
$sql = sprintf('AND %s.ID in (SELECT post_id FROM %s WHERE meta_key = %%s)', $wpdb->posts, $wpdb->postmeta);
$sql .= sprintf(' AND %s.ID not in ( SELECT attach_id FROM %s WHERE parent = 0 and status = %s)', $wpdb->posts, $tableName, ImageModel::FILE_STATUS_MARKED_DONE);
$where = $wpdb->prepare($sql, '_shortpixel_prevent_optimize');
break;
}
return $where;
}
/**
* Safely retrieve the selected filter value from a dropdown.
*
* @param string $key
* @param string $default
*
* @return string
*/
private function selected_filter_value( $key, $default ) {
if ( wp_doing_ajax() ) {
if ( isset( $_REQUEST['query'][ $key ] ) ) {
$value = sanitize_text_field( $_REQUEST['query'][ $key ] );
}
} else {
if ( ! isset( $_REQUEST['filter_action'] ) ) {
return $default;
}
if ( ! isset( $_REQUEST[ $key ] ) ) {
return $default;
}
$value = sanitize_text_field( $_REQUEST[ $key ] );
}
return ! empty( $value ) ? $value : $default;
}
/**
* When replacing happens.
* @hook wp_handle_replace
* @integration Enable Media Replace
*/
public function handleReplaceHook($params)
{
if(isset($params['post_id'])) { //integration with EnableMediaReplace - that's an upload for replacing an existing ID
$post_id = intval($params['post_id']);
$fs = \wpSPIO()->filesystem();
$imageObj = $fs->getImage($post_id, 'media');
// In case entry is corrupted data, this might fail.
if (is_object($imageObj))
{
$imageObj->onDelete();
}
}
}
/** This function is bound to enable-media-replace hook and fire when a file was replaced
*
*
*/
public function handleReplaceEnqueue($target, $source, $post_id)
{
// Delegate this to the hook, so all checks are done there.
$this->handleImageUploadHook(array(), $post_id);
}
public function generatePluginLinks($links) {
$in = '<a href="options-general.php?page=wp-shortpixel-settings">Settings</a>';
array_unshift($links, $in);
return $links;
}
/** Allow certain mime-types if we will be using those.
*
*/
public function addMimes($mimes)
{
$settings = \wpSPIO()->settings();
if ($settings->createWebp)
{
if (! isset($mimes['webp']))
$mimes['webp'] = 'image/webp';
}
if ($settings->createAvif)
{
if (! isset($mimes['avif']))
$mimes['avif'] = 'image/avif';
}
if (! isset($mimes['heic']))
{
$mimes['heic'] = 'image/heic';
}
if (! isset($mimes['heif']))
{
$mimes['heif'] = 'image/heif';
}
return $mimes;
}
/** Media library gallery view, attempt to add fields that looks like the SPIO status */
public function editAttachmentScreen($fields, $post)
{
return;
// Prevent this thing running on edit media screen. The media library grid is before the screen is set, so just check if we are not on the attachment window.
$screen_id = \wpSPIO()->env()->screen_id;
if ($screen_id == 'attachment')
{
return $fields;
}
$fields["shortpixel-image-optimiser"] = array(
"label" => esc_html__("ShortPixel", "shortpixel-image-optimiser"),
"input" => "html",
"html" => '<div id="sp-msg-' . $post->ID . '">--</div>',
);
return $fields;
}
public function printComparer()
{
$screen_id = \wpSPIO()->env()->screen_id;
if ($screen_id !== 'upload')
{
return false;
}
$view = \ShortPixel\Controller\View\ListMediaViewController::getInstance();
$view->loadComparer();
}
/** When an image is deleted
* @hook delete_attachment
* @param int $post_id ID of Post
* @return itemHandler ItemHandler object.
*/
public function onDeleteAttachment($post_id) {
Log::addDebug('onDeleteImage - Image Removal Detected ' . $post_id);
$result = null;
$fs = \wpSPIO()->filesystem();
try
{
$imageObj = $fs->getImage($post_id, 'media');
//Log::addDebug('OnDelete ImageObj', $imageObj);
if ($imageObj !== false)
$result = $imageObj->onDelete();
}
catch(\Exception $e)
{
Log::addError('OndeleteImage triggered an error. ' . $e->getMessage(), $e);
}
return $result;
}
/** Displays an icon in the toolbar when processing images
* hook - admin_bar_menu
* @param Obj $wp_admin_bar
*/
public function toolbar_shortpixel_processing( $wp_admin_bar ) {
if (! \wpSPIO()->env()->is_screen_to_use )
return; // not ours, don't load JS and such.
$settings = \wpSPIO()->settings();
$access = AccessModel::getInstance();
$quotaController = QuotaController::getInstance();
$extraClasses = " shortpixel-hide";
/*translators: toolbar icon tooltip*/
$id = 'short-pixel-notice-toolbar';
$tooltip = __('ShortPixel optimizing...','shortpixel-image-optimiser');
$icon = "shortpixel.png";
$successLink = $link = admin_url(current_user_can( 'edit_others_posts')? 'upload.php?page=wp-short-pixel-bulk' : 'upload.php');
$blank = "";
if($quotaController->hasQuota() === false)
{
$extraClasses = " shortpixel-alert shortpixel-quota-exceeded";
/*translators: toolbar icon tooltip*/
$id = 'short-pixel-notice-exceed';
$tooltip = '';
if ($access->userIsAllowed('quota-warning'))
{
$exceedTooltip = __('ShortPixel quota exceeded. Click for details.','shortpixel-image-optimiser');
//$link = "http://shortpixel.com/login/" . $this->_settings->apiKey;
$link = "options-general.php?page=wp-shortpixel-settings";
}
else {
$exceedTooltip = __('ShortPixel quota exceeded. Click for details.','shortpixel-image-optimiser');
//$link = "http://shortpixel.com/login/" . $this->_settings->apiKey;
$link = false;
}
}
$args = array(
'id' => 'shortpixel_processing',
'title' => '<div id="' . $id . '" title="' . $tooltip . '"><span class="stats hidden">0</span><img alt="' . __('ShortPixel icon','shortpixel-image-optimiser') . '" src="'
. plugins_url( 'res/img/'.$icon, SHORTPIXEL_PLUGIN_FILE ) . '" success-url="' . $successLink . '"><span class="shp-alert">!</span>'
. '<div class="controls">
<span class="dashicons dashicons-controls-pause pause" title="' . __('Pause', 'shortpixel-image-optimiser') . '">&nbsp;</span>
<span class="dashicons dashicons-controls-play play" title="' . __('Resume', 'shortpixel-image-optimiser') . '">&nbsp;</span>
</div>'
.'<div class="cssload-container"><div class="cssload-speeding-wheel"></div></div></div>',
// 'href' => 'javascript:void(0)', // $link,
'meta' => array('target'=> $blank, 'class' => 'shortpixel-toolbar-processing' . $extraClasses)
);
$wp_admin_bar->add_node( $args );
if($quotaController->hasQuota() === false)
{
$wp_admin_bar->add_node( array(
'id' => 'shortpixel_processing-title',
'parent' => 'shortpixel_processing',
'title' => $exceedTooltip,
'href' => $link
));
}
}
} // class

View File

@ -0,0 +1,553 @@
<?php
namespace ShortPixel\Controller;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\Notices\NoticeController as Notices;
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\ViewController as ViewController;
use ShortPixel\Model\AccessModel as AccessModel;
// Use ShortPixel\Model\ApiKeyModel as ApiKeyModel
/**
* Controller for automatic Notices about status of the plugin.
* This controller is bound for automatic fire. Regular procedural notices should just be queued using the Notices modules.
* Called in admin_notices.
*/
class AdminNoticesController extends \ShortPixel\Controller
{
protected static $instance;
protected $definedNotices = array( // NoticeModels by Class. This is not optimal but until solution found, workable.
'CompatNotice',
'UnlistedNotice',
'AvifNotice',
'QuotaNoticeMonth',
'QuotaNoticeReached',
'ApiNotice',
'ApiNoticeRepeat',
'ApiNoticeRepeatLong',
'NextgenNotice',
// 'SmartcropNotice',
'LegacyNotice',
'ListviewNotice',
// 'HeicFeatureNotice',
'NewExclusionFormat',
);
protected $adminNotices; // Models
private $remote_message_endpoint = 'https://api.shortpixel.com/v2/notices.php';
private $remote_readme_endpoint = 'https://plugins.svn.wordpress.org/shortpixel-image-optimiser/trunk/readme.txt';
public function __construct()
{
add_action('admin_notices', array($this, 'displayNotices'), 50); // notices occured before page load
add_action('admin_footer', array($this, 'displayNotices')); // called in views.
add_action('in_plugin_update_message-' . plugin_basename(SHORTPIXEL_PLUGIN_FILE), array($this, 'pluginUpdateMessage') , 50, 2 );
// no persistent notifications with this flag set.
if (defined('SHORTPIXEL_SILENT_MODE') && SHORTPIXEL_SILENT_MODE === true)
return;
add_action('admin_notices', array($this, 'check_admin_notices'), 5); // run before the plugin admin notices
$this->initNotices();
}
public static function getInstance()
{
if (is_null(self::$instance))
self::$instance = new AdminNoticesController();
return self::$instance;
}
public static function resetAllNotices()
{
Notices::resetNotices();
}
// Notices no longer in use.
public static function resetOldNotices()
{
Notices::removeNoticeByID('MSG_FEATURE_SMARTCROP');
Notices::removeNoticeByID('MSG_FEATURE_HEIC');
}
/** Triggered when plugin is activated */
public static function resetCompatNotice()
{
Notices::removeNoticeByID('MSG_COMPAT');
}
public static function resetAPINotices()
{
Notices::removeNoticeByID('MSG_NO_APIKEY');
Notices::removeNoticeByID('MSG_NO_APIKEY_REPEAT');
Notices::removeNoticeByID('MSG_NO_APIKEY_REPEAT_LONG');
}
public static function resetQuotaNotices()
{
Notices::removeNoticeByID('MSG_UPGRADE_MONTH');
Notices::removeNoticeByID('MSG_UPGRADE_BULK');
Notices::removeNoticeBYID('MSG_QUOTA_REACHED');
}
public static function resetIntegrationNotices()
{
Notices::removeNoticeByID('MSG_INTEGRATION_NGGALLERY');
}
public static function resetLegacyNotice()
{
Notices::removeNoticeByID('MSG_CONVERT_LEGACY');
}
public function displayNotices()
{
if (! \wpSPIO()->env()->is_screen_to_use)
{
if(get_current_screen()->base !== 'dashboard') // ugly exception for dashboard.
return; // suppress all when not our screen.
}
$access = AccessModel::getInstance();
$screen = get_current_screen();
$screen_id = \wpSPIO()->env()->screen_id;
$noticeControl = Notices::getInstance();
$noticeControl->loadIcons(array(
'normal' => '<img class="short-pixel-notice-icon" src="' . plugins_url('res/img/slider.png', SHORTPIXEL_PLUGIN_FILE) . '">',
'success' => '<img class="short-pixel-notice-icon" src="' . plugins_url('res/img/robo-cool.png', SHORTPIXEL_PLUGIN_FILE) . '">',
'warning' => '<img class="short-pixel-notice-icon" src="' . plugins_url('res/img/robo-scared.png', SHORTPIXEL_PLUGIN_FILE) . '">',
'error' => '<img class="short-pixel-notice-icon" src="' . plugins_url('res/img/robo-scared.png', SHORTPIXEL_PLUGIN_FILE) . '">',
));
if ($noticeControl->countNotices() > 0)
{
$notices = $noticeControl->getNoticesForDisplay();
if (count($notices) > 0)
{
\wpSPIO()->load_style('shortpixel-notices');
\wpSPIO()->load_style('notices-module');
foreach($notices as $notice)
{
if ($notice->checkScreen($screen_id) === false)
{
continue;
}
elseif ($access->noticeIsAllowed($notice))
{
echo $notice->getForDisplay();
}
else
{
continue;
}
// @Todo change this to new keys
if ($notice->getID() == 'MSG_QUOTA_REACHED' || $notice->getID() == 'MSG_UPGRADE_MONTH') //|| $notice->getID() == AdminNoticesController::MSG_UPGRADE_BULK
{
// @todo check if this is still needed.
wp_enqueue_script('jquery.knob.min.js');
wp_enqueue_script('shortpixel');
}
}
}
}
$noticeControl->update(); // puts views, and updates
}
/* General function to check on Hook for admin notices if there is something to show globally */
public function check_admin_notices()
{
if (! \wpSPIO()->env()->is_screen_to_use)
{
if(get_current_screen()->base !== 'dashboard') // ugly exception for dashboard.
return; // suppress all when not our screen.
}
$this->loadNotices();
}
protected function initNotices()
{
foreach($this->definedNotices as $className)
{
$ns = '\ShortPixel\Model\AdminNotices\\' . $className;
$class = new $ns();
$this->adminNotices[$class->getKey()] = $class;
}
}
protected function loadNotices()
{
foreach($this->adminNotices as $key => $class)
{
$class->load();
$this->doRemoteNotices();
}
}
public function getNoticeByKey($key)
{
if (isset($this->adminNotices[$key]))
{
return $this->adminNotices[$key];
}
else {
return false;
}
}
public function getAllNotices()
{
return $this->adminNotices;
}
// Called by MediaLibraryModel
public function invokeLegacyNotice()
{
$noticeModel = $this->getNoticeByKey('MSG_CONVERT_LEGACY');
if (! $noticeModel->isDismissed())
{
$noticeModel->addManual();
}
}
protected function doRemoteNotices()
{
$notices = $this->get_remote_notices();
if ($notices == false)
return;
foreach($notices as $remoteNotice)
{
if (! isset($remoteNotice->id) && ! isset($remoteNotice->message))
return;
if (! isset($remoteNotice->type))
$remoteNotice->type = 'notice';
$message = esc_html($remoteNotice->message);
$id = sanitize_text_field($remoteNotice->id);
$noticeController = Notices::getInstance();
$noticeObj = $noticeController->getNoticeByID($id);
// not added to system yet
if ($noticeObj === false)
{
switch ($remoteNotice->type)
{
case 'warning':
$new_notice = Notices::addWarning($message);
break;
case 'error':
$new_notice = Notices::addError($message);
break;
case 'notice':
default:
$new_notice = Notices::addNormal($message);
break;
}
Notices::makePersistent($new_notice, $id, MONTH_IN_SECONDS);
}
}
}
public function proposeUpgradePopup() {
$view = new ViewController();
$view->loadView('snippets/part-upgrade-options');
}
public function proposeUpgradeRemote()
{
//$stats = $this->countAllIfNeeded($this->_settings->currentStats, 300);
$statsController = StatsController::getInstance();
$apiKeyController = ApiKeyController::getInstance();
$settings = \wpSPIO()->settings();
$webpActive = ($settings->createWebp) ? true : false;
$avifActive = ($settings->createAvif) ? true : false;
$args = array(
'method' => 'POST',
'timeout' => 10,
'redirection' => 5,
'httpversion' => '1.0',
'blocking' => true,
'headers' => array(),
'body' => array("params" => json_encode(array(
'plugin_version' => SHORTPIXEL_IMAGE_OPTIMISER_VERSION,
'key' => $apiKeyController->forceGetApiKey(),
'm1' => $statsController->find('period', 'months', '1'),
'm2' => $statsController->find('period', 'months', '2'),
'm3' => $statsController->find('period', 'months', '3'),
'm4' => $statsController->find('period', 'months', '4'),
'filesTodo' => $statsController->totalImagesToOptimize(),
'estimated' => $settings->optimizeUnlisted || $settings->optimizeRetina ? 'true' : 'false',
'webp' => $webpActive,
'avif' => $avifActive,
/* */
'iconsUrl' => base64_encode(wpSPIO()->plugin_url('res/img'))
))),
'cookies' => array()
);
$proposal = wp_remote_post("https://shortpixel.com/propose-upgrade-frag", $args);
if(is_wp_error( $proposal )) {
$proposal = array('body' => __('Error. Could not contact ShortPixel server for proposal', 'shortpixel-image-optimiser'));
}
die( $proposal['body'] );
}
private function get_remote_notices()
{
$transient_name = 'shortpixel_remote_notice';
$transient_duration = DAY_IN_SECONDS;
if (\wpSPIO()->env()->is_debug)
$transient_duration = 30;
$keyControl = new apiKeyController();
//$keyControl->loadKey();
$notices = get_transient($transient_name);
$url = $this->remote_message_endpoint;
$url = add_query_arg(array( // has url
'key' => $keyControl->forceGetApiKey(),
'version' => SHORTPIXEL_IMAGE_OPTIMISER_VERSION,
'target' => 3,
), $url);
if ( $notices === false ) {
$notices_response = wp_safe_remote_request( $url );
$content = false;
if (! is_wp_error( $notices_response ) )
{
$notices = json_decode($notices_response['body']);
if (! is_array($notices))
$notices = false;
// Save transient anywhere to prevent over-asking when nothing good is there.
set_transient( $transient_name, $notices, $transient_duration );
}
else
{
set_transient( $transient_name, false, $transient_duration );
}
}
return $notices;
}
public function pluginUpdateMessage($data, $response)
{
// $message = $this->getPluginUpdateMessage($plugin['new_version']);
$message = $this->get_update_notice($data, $response);
if( $message !== false && strlen(trim($message)) > 0) {
$wp_list_table = _get_list_table( 'WP_Plugins_List_Table' );
printf(
'<tr class="plugin-update-tr active"><td colspan="%s" class="plugin-update colspanchange"><div class="notice inline notice-warning notice-alt">%s</div></td></tr>',
$wp_list_table->get_column_count(),
wpautop( $message )
);
}
}
/**
* Stolen from SPAI, Thanks.
*/
private function get_update_notice($data, $response) {
$transient_name = 'shortpixel_update_notice_' . $response->new_version;
$transient_duration = DAY_IN_SECONDS;
if (\wpSPIO()->env()->is_debug)
$transient_duration = 30;
$update_notice = get_transient( $transient_name );
$url = $this->remote_readme_endpoint;
if ( $update_notice === false || strlen( $update_notice ) == 0 ) {
$readme_response = wp_safe_remote_request( $url );
$content = false;
if (! is_wp_error( $readme_response ) )
{
$content = $readme_response['body'];
}
if ( !empty( $readme_response ) ) {
$update_notice = $this->parse_update_notice( $content, $response );
set_transient( $transient_name, $update_notice, $transient_duration );
}
}
return $update_notice;
}
/**
* Parse update notice from readme file.
*
* @param string $content ShortPixel AI readme file content
* @param object $response WordPress response
*
* @return string
*/
private function parse_update_notice( $content, $response ) {
$new_version = $response->new_version;
$update_notice = '';
// foreach ( $check_for_notices as $id => $check_version ) {
if ( version_compare( SHORTPIXEL_IMAGE_OPTIMISER_VERSION, $new_version, '>' ) ) {
return '';
}
$result = $this->parse_readme_content( $content, $new_version, $response );
if ( !empty( $result ) ) {
$update_notice = $result;
}
// }
return wp_kses_post( $update_notice );
}
/*
*
* Parses readme file's content to find notice related to passed version
*
* @param string $content Readme file content
* @param string $version Checked version
* @param object $response WordPress response
*
* @return string
*/
private function parse_readme_content( $content, $new_version, $response ) {
$notices_pattern = '/==.*Upgrade Notice.*==(.*)$|==/Uis';
$notice = '';
$matches = null;
if ( preg_match( $notices_pattern, $content, $matches ) ) {
if (! isset($matches[1]))
return ''; // no update texts.
$match = str_replace('\n', '', $matches[1]);
$lines = str_split(trim($match));
$versions = array();
$inv = false;
foreach($lines as $char)
{
//if (count($versions) == 0)
if ($char == '=' && ! $inv) // = and not recording version, start one.
{
$curver = '';
$inv = true;
}
elseif ($char == ' ' && $inv) // don't record spaces in version
continue;
elseif ($char == '=' && $inv) // end of version line
{ $versions[trim($curver)] = '';
$inv = false;
}
elseif($inv) // record the version string
{
$curver .= $char;
}
elseif(! $inv) // record the message
{
$versions[trim($curver)] .= $char;
}
}
foreach($versions as $version => $line)
{
if (version_compare(SHORTPIXEL_IMAGE_OPTIMISER_VERSION, $version, '<') && version_compare($version, $new_version, '<='))
{
$notice .= '<span>';
$notice .= $this->markdown2html( $line );
$notice .= '</span>';
}
}
}
return $notice;
}
/*private function replace_readme_constants( $content, $response ) {
$constants = [ '{{ NEW VERSION }}', '{{ CURRENT VERSION }}', '{{ PHP VERSION }}', '{{ REQUIRED PHP VERSION }}' ];
$replacements = [ $response->new_version, SHORTPIXEL_IMAGE_OPTIMISER_VERSION, PHP_VERSION, $response->requires_php ];
return str_replace( $constants, $replacements, $content );
} */
private function markdown2html( $content ) {
$patterns = [
'/\*\*(.+)\*\*/U', // bold
'/__(.+)__/U', // italic
'/\[([^\]]*)\]\(([^\)]*)\)/U', // link
];
$replacements = [
'<strong>${1}</strong>',
'<em>${1}</em>',
'<a href="${2}" target="_blank">${1}</a>',
];
$prepared_content = preg_replace( $patterns, $replacements, $content );
return isset( $prepared_content ) ? $prepared_content : $content;
}
} // class

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,729 @@
<?php
namespace ShortPixel\Controller;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
class ApiController
{
const STATUS_ENQUEUED = 10;
const STATUS_PARTIAL_SUCCESS = 3;
const STATUS_SUCCESS = 2;
const STATUS_WAITING = 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_OPTIMIZED_BIGGER = -9;
const STATUS_CONVERTED = -10;
const STATUS_QUEUE_FULL = -404;
const STATUS_MAINTENANCE = -500;
const STATUS_CONNECTION_ERROR = -503; // Not official, error connection in WP.
const STATUS_NOT_API = -1000; // Not an API process, i.e restore / migrate. Don't handle as optimized
// Moved these numbers higher to prevent conflict with STATUS
const ERR_FILE_NOT_FOUND = -902;
const ERR_TIMEOUT = -903;
const ERR_SAVE = -904;
const ERR_SAVE_BKP = -905;
const ERR_INCORRECT_FILE_SIZE = -906;
const ERR_DOWNLOAD = -907;
const ERR_PNG2JPG_MEMORY = -908;
const ERR_POSTMETA_CORRUPT = -909;
const ERR_UNKNOWN = -999;
const DOWNLOAD_ARCHIVE = 7;
private static $instance;
private $apiEndPoint;
private $apiDumpEndPoint;
protected static $temporaryFiles = array();
protected static $temporaryDirs = array();
public function __construct()
{
$settings = \wpSPIO()->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

View File

@ -0,0 +1,75 @@
<?php
namespace ShortPixel\Controller;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Model\ApiKeyModel as ApiKeyModel;
/* Main function of this controller is to load key on runtime
This should probably in future incorporate some apikey checking functions that shouldn't be in model.
*/
class ApiKeyController extends \ShortPixel\Controller
{
private static $instance;
public function __construct()
{
$this->model = new ApiKeyModel();
$this->load();
}
public static function getInstance()
{
if (is_null(self::$instance))
self::$instance = new ApiKeyController();
return self::$instance;
}
public function load()
{
$this->model->loadKey();
}
public function getKeyModel()
{
return $this->model;
}
public function getKeyForDisplay()
{
if (! $this->model->is_hidden())
{
return $this->model->getKey();
}
else
return false;
}
/** Warning: NEVER use this for displaying API keys. Only for internal functions */
public function forceGetApiKey()
{
return $this->model->getKey();
}
public function keyIsVerified()
{
return $this->model->is_verified();
}
public function uninstall()
{
$this->model->uninstall();
}
public static function uninstallPlugin()
{
$controller = self::getInstance();
$controller->uninstall();
}
}

View File

@ -0,0 +1,254 @@
<?php
namespace ShortPixel\Controller;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\Controller\Queue\MediaLibraryQueue as MediaLibraryQueue;
use ShortPixel\Controller\Queue\CustomQueue as CustomQueue;
use ShortPixel\Controller\Queue\Queue as Queue;
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
// Class for controlling bulk and reporting.
class BulkController
{
protected static $instance;
protected static $logName = 'shortpixel-bulk-logs';
public function __construct()
{
}
public static function getInstance()
{
if ( is_null(self::$instance))
self::$instance = new BulkController();
return self::$instance;
}
/** Create a new bulk, enqueue items for bulking
* @param $type String media or custom is supported.
* @param $customOp String Not a usual optimize queue, but something else. options:
* 'bulk-restore', or 'migrate'.
*/
public function createNewBulk($type = 'media', $customOp = null)
{
$optimizeController = new OptimizeController();
$optimizeController->setBulk(true);
$fs = \wpSPIO()->filesystem();
$backupDir = $fs->getDirectory(SHORTPIXEL_BACKUP_FOLDER);
$current_log = $fs->getFile($backupDir->getPath() . 'current_bulk_' . $type . '.log');
// When starting new bulk remove any open 'current logs';
if ($current_log->exists() && $current_log->is_writable())
{
$current_log->delete();
}
$Q = $optimizeController->getQueue($type);
$Q->createNewBulk();
$Q = $optimizeController->getQueue($type);
if (! is_null($customOp))
{
$options = array();
if ($customOp == 'bulk-restore')
{
$options['numitems'] = 5;
$options['retry_limit'] = 5;
$options['process_timeout'] = 3000;
}
if ($customOp == 'migrate' || $customOp == 'removeLegacy')
{
$options['numitems'] = 200;
}
$options = apply_filters('shortpixel/bulk/custom_options', $options, $customOp);
$Q->setCustomBulk($customOp, $options);
}
return $Q->getStats();
}
public function isBulkRunning($type = 'media')
{
$optimizeControl = new OptimizeController();
$optimizeControl->setBulk(true);
$queue = $optimizeControl->getQueue($type);
$stats = $queue->getStats();
if ( $stats->is_finished === false && $stats->total > 0)
{
return true;
}
else
{
return false;
}
}
public function isAnyBulkRunning()
{
$bool = $this->isBulkRunning('media');
if ($bool === false)
{
$bool = $this->isBulkRunning('custom');
}
return $bool;
}
/*** Start the bulk run. Must deliver all queues at once due to processQueue bundling */
public function startBulk($types = 'media')
{
$optimizeControl = new OptimizeController();
$optimizeControl->setBulk(true);
if (! is_array($types))
$types = array($types);
foreach($types as $type)
{
$q = $optimizeControl->getQueue($type);
$q->startBulk();
}
return $optimizeControl->processQueue($types);
}
public function finishBulk($type = 'media')
{
$optimizeControl = new OptimizeController();
$optimizeControl->setBulk(true);
$q = $optimizeControl->getQueue($type);
$this->addLog($q);
$op = $q->getCustomDataItem('customOperation');
// When finishing, remove the Legacy Notice
if ($op == 'migrate')
{
AdminNoticesController::resetLegacyNotice();
}
$q->resetQueue();
}
public function getLogs()
{
$logs = get_option(self::$logName, array());
return $logs;
}
public function getLog($logName)
{
$fs = \wpSPIO()->filesystem();
$backupDir = $fs->getDirectory(SHORTPIXEL_BACKUP_FOLDER);
$log = $fs->getFile($backupDir->getPath() . $logName);
if ($log->exists())
return $log;
else
return false;
}
public function getLogData($fileName)
{
$logs = $this->getLogs();
foreach($logs as $log)
{
if (isset($log['logfile']) && $log['logfile'] == $fileName)
return $log;
}
return false;
}
protected function addLog($q)
{
//$data = (array) $stats;
$stats = $q->getStats(); // for the log
$type = $q->getType();
// $customData = $q->getCustomDataItem('');
if ($stats->done == 0 && $stats->fatal_errors == 0)
{
return; // nothing done, don't log
}
$data['processed'] = $stats->done;
$data['not_processed'] = $stats->in_queue;
$data['errors'] = $stats->errors;
$data['fatal_errors'] = $stats->fatal_errors;
$webpcount = $q->getCustomDataItem('webpcount');
$avifcount = $q->getCustomDataItem('avifcount');
$basecount = $q->getCustomDataItem('basecount');
if (property_exists($stats, 'images'))
$data['total_images'] = $stats->images->images_done;
$data['type'] = $type;
if ($q->getCustomDataItem('customOperation'))
{
$data['operation'] = $q->getCustomDataItem('customOperation');
}
$data['date'] = time();
$logs = $this->getLogs();
$fs = \wpSPIO()->filesystem();
$backupDir = $fs->getDirectory(SHORTPIXEL_BACKUP_FOLDER);
if (count($logs) == 10) // remove logs if more than 10.
{
$log = array_shift($logs);
//$log_date = $log['date'];
//$log_type = $log['type'];
if (isset($data['logfile']))
{
$logfile = $data['logfile'];
$fileLog = $fs->getFile($backupDir->getPath() . $logfile);
if ($fileLog->exists())
$fileLog->delete();
}
}
$fileLog = $fs->getFile($backupDir->getPath() . 'current_bulk_' . $type . '.log');
$moveLog = $fs->getFile($backupDir->getPath() . 'bulk_' . $type. '_' . $data['date'] . '.log');
if ($fileLog->exists())
$fileLog->move($moveLog);
$data['logfile'] = 'bulk_' . $type . '_' . $data['date'] . '.log';
$logs[] = $data;
$this->saveLogs($logs);
}
protected function saveLogs($logs)
{
if (is_array($logs) && count($logs) > 0)
update_option(self::$logName, $logs, false);
else
delete_option(self::$logName);
}
// Removes Bulk Log .
public static function uninstallPlugin()
{
delete_option(self::$logName);
}
} // class

View File

@ -0,0 +1,74 @@
<?php
namespace ShortPixel\Controller;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Model\CacheModel as CacheModel;
// Future replacement for everything that needs temporary storage
// Storage agnostic -> called function should not need to know what is stored where, this is job of controller.
// Works with cache-model, which handles the data representation and storage.
//
class CacheController extends \ShortPixel\Controller
{
protected static $cached_items = array();
public function __construct()
{
}
public function storeItem($name, $value, $expires = HOUR_IN_SECONDS)
{
$cache = $this->getItem($name);
$cache->setValue($value);
$cache->setExpires($expires);
$cache->save();
$cache = apply_filters('shortpixel/cache/save', $cache, $name);
self::$cached_items[$name] = $cache;
return $cache;
}
/** Store a cacheModel Object.
* This can be used after requesting a cache item for instance.
* @param CacheModel $cache The Cache Model Item.
*/
public function storeItemObject(CacheModel $cache)
{
self::$cached_items[$cache->getName()] = $cache;
$cache->save();
}
public function getItem($name)
{
if (isset(self::$cached_items[$name]))
return self::$cached_items[$name];
$cache = new CacheModel($name);
$cache = apply_filters('shortpixel/cache/get', $cache, $name);
self::$cached_items[$name] = $cache;
return $cache;
}
public function deleteItem($name)
{
$cache = $this->getItem($name);
if ($cache->exists())
{
$cache->delete();
}
}
public function deleteItemObject(CacheModel $cache)
{
$cache->delete();
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace ShortPixel\Controller;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
class ErrorController
{
public function __construct()
{
}
public static function start()
{
if (true === \wpSPIO()->env()->is_debug)
{
register_shutdown_function(array(self::class, 'checkErrors'));
}
}
public static function checkErrors()
{
$error = error_get_last();
// Nothing, happy us.
if (is_null($error))
{
return;
}
elseif (1 !== $error['type']) // Nothing fatal.
{
return;
}
else {
ob_clean(); // try to scrub other stuff
echo '<PRE>' . $error['message'] . ' in ' . $error['file'] . ' on line ' . $error['line'] . '<br> Last Item ID: ' . OptimizeController::getLastId() . '</PRE>';
exit(' <small><br> -ShortPixel Error Handler- </small>');
}
}
}

View File

@ -0,0 +1,599 @@
<?php
namespace ShortPixel\Controller;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Model\File\DirectoryModel as DirectoryModel;
use ShortPixel\Model\File\FileModel as FileModel;
use ShortPixel\Model\Image\MediaLibraryModel as MediaLibraryModel;
use ShortPixel\Model\Image\MediaLibraryThumbnailModel as MediaLibraryThumbnailModel;
use ShortPixel\Model\Image\CustomImageModel as CustomImageModel;
/** Controller for FileSystem operations
*
* This controller is used for -compound- ( complex ) FS operations, using the provided models File en Directory.
* USE via \wpSPIO()->filesystem();
*/
Class FileSystemController extends \ShortPixel\Controller
{
protected $env;
static $mediaItems = array();
static $customItems = array();
public function __construct()
{
$this->env = wpSPIO()->env();
}
/** Get FileModel for a certain path. This can exist or not
*
* @param String Path Full Path to the file
* @return FileModel FileModel Object. If file does not exist, not all values are set.
*/
public function getFile($path)
{
return new FileModel($path);
}
/** Get MediaLibraryModel for a Post_id
* @param int $id
* @param bool $useCache If false then it will require a fresh copt from database. Use when meta has changed / saved
* @param bool $cacheOnly Prevent fetching from Database. Used for checkLegacy and other places where conflicts with mainFile arise, checking for backups.
*/
public function getMediaImage($id, $useCache = true, $cacheOnly = false)
{
if ($useCache === true && isset(self::$mediaItems[$id]))
{
return self::$mediaItems[$id];
}
if (true === $cacheOnly)
return false;
$filepath = get_attached_file($id);
$filepath = apply_filters('shortpixel_get_attached_file', $filepath, $id);
// Somehow get_attached_file can return other random stuff.
if ($filepath === false || strlen($filepath) == 0)
return false;
$imageObj = new MediaLibraryModel($id, $filepath);
if (is_object($imageObj))
{
self::$mediaItems[$id] = $imageObj;
}
return $imageObj;
}
/**
* @param int $id
*/
public function getCustomImage($id, $useCache = true)
{
if ($useCache === true && isset(self::$customItems[$id]))
{
return self::$customItems[$id];
}
$imageObj = new CustomImageModel($id);
if (is_object($imageObj))
{
self::$customItems[$id] = $imageObj;
}
return $imageObj;
}
// Use sporadically, every time an angel o performance dies.
// Required for files that change i.e. enable media replace or other filesystem changing operation.
public function flushImageCache()
{
self::$mediaItems = array();
self::$customItems = array();
MediaLibraryModel::onFlushImageCache();
}
public function flushImage($imageObj)
{
$id = $imageObj->get('id');
$type = $imageObj->get('type');
if ('media' == $type && isset(self::$mediaItems[$id]))
{
unset(self::$mediaItems[$id]);
MediaLibraryModel::onFlushImageCache();
}
if ('custom' == $type && isset(self::$customItems[$id]))
{
unset(self::$customItems[$id]);
}
}
/** Gets a custom Image Model without being in the database. This is used to check if path is a proper customModel path ( not mediaLibrary ) and see if the file should be included per excusion rules */
public function getCustomStub( $path, $load = true)
{
$imageObj = new CustomImageModel(0);
$imageObj->setStub($path, $load);
return $imageObj;
}
/** Generic function to get the correct image Object, to prevent many switches everywhere
* int $id
* string $type
*/
public function getImage( $id, $type, $useCache = true)
{
// False, OptimizeController does a hard check for false.
$imageObj = false;
if ($type == 'media')
{
$imageObj = $this->getMediaImage($id, $useCache);
}
elseif($type == 'custom')
{
$imageObj = $this->getCustomImage($id, $useCache);
}
else
{
Log::addError('FileSystemController GetImage - no correct type given: ' . $type);
}
return $imageObj;
}
/* wp_get_original_image_path with specific ShortPixel filter
* @param int $id
*/
public function getOriginalImage($id)
{
$filepath = \wp_get_original_image_path($id);
$filepath = apply_filters('shortpixel_get_original_image_path', $filepath, $id);
return new MediaLibraryThumbnailModel($filepath, $id, 'original');
}
/** Get DirectoryModel for a certain path. This can exist or not
*
* @param String $path Full Path to the Directory.
* @return DirectoryModel Object with status set on current directory.
*/
public function getDirectory($path)
{
return new DirectoryModel($path);
}
/** Get the BackupLocation for a FileModel. FileModel should be not a backup itself or it will recurse
*
* For now this function should only be used for *new* backup files. Retrieving backup files via this method
* doesn't take into account legacy ways of storage.
*
* @param FileModel $file FileModel with file that needs a backup.
* @return DirectoryModel | Boolean DirectoryModel pointing to the backup directory. Returns false if the directory could not be created, or initialized.
*/
public function getBackupDirectory(FileModel $file, $create = false)
{
if (! function_exists('get_home_path'))
{
require_once(ABSPATH . 'wp-admin/includes/file.php');
}
$wp_home = \get_home_path();
$filepath = $file->getFullPath();
if ($file->is_virtual())
{
$filepath = apply_filters('shortpixel/file/virtual/translate', $filepath, $file);
}
// translate can return false if not properly offloaded / not found there.
if ($filepath !== $file->getFullPath() && $filepath !== false)
{
$file = $this->getFile($filepath);
}
$fileDir = $file->getFileDir();
$backup_subdir = $fileDir->getRelativePath();
if ($backup_subdir === false)
{
$backup_subdir = $this->returnOldSubDir($filepath);
}
$backup_fulldir = SHORTPIXEL_BACKUP_FOLDER . '/' . $backup_subdir;
$directory = $this->getDirectory($backup_fulldir);
$directory = apply_filters("shortpixel/file/backup_folder", $directory, $file);
if ($create === false && $directory->exists())
return $directory;
elseif ($create === true && $directory->check()) // creates directory if needed.
return $directory;
else {
return false;
}
}
/** Get the base folder from where custom paths are possible (from WP-base / sitebase)
*/
public function getWPFileBase()
{
if(\wpSPIO()->env()->is_mainsite) {
$path = (string) $this->getWPAbsPath();
} else {
$up = wp_upload_dir();
$path = realpath($up['basedir']);
}
$dir = $this->getDirectory($path);
if (! $dir->exists())
Log::addWarn('getWPFileBase - Base path doesnt exist');
return $dir;
}
/** This function returns the WordPress Basedir for uploads ( without date and such )
* Normally this would point to /wp-content/uploads.
* @returns DirectoryModel
*/
public function getWPUploadBase()
{
$upload_dir = wp_upload_dir(null, false);
return $this->getDirectory($upload_dir['basedir']);
}
/** This function returns the Absolute Path of the WordPress installation where the **CONTENT** directory is located.
* Normally this would be the same as ABSPATH, but there are installations out there with -cough- alternative approaches
* The Abspath is uses to replace against the domain URL ( home_url ).
* @returns DirectoryModel Either the ABSPATH or where the WP_CONTENT_DIR is located
*/
public function getWPAbsPath()
{
$wpContentPos = strpos(WP_CONTENT_DIR, 'wp-content');
// Check if Content DIR actually has wp-content in it.
if (false !== $wpContentPos)
{
$wpContentAbs = substr(WP_CONTENT_DIR, 0, $wpContentPos); //str_replace( 'wp-content', '', WP_CONTENT_DIR);
}
else {
$wpContentAbs = WP_CONTENT_DIR;
}
if (ABSPATH == $wpContentAbs)
$abspath = ABSPATH;
else
$abspath = $wpContentAbs;
// If constants UPLOADS is defined -AND- there is a blogs.dir in it, add it like this. UPLOAD constant alone is not enough since it can cause ugly doublures in the path if there is another style config.
if (defined('UPLOADS') && strpos(UPLOADS, 'blogs.dir') !== false)
{
$abspath = trailingslashit(ABSPATH) . UPLOADS;
}
$abspath = apply_filters('shortpixel/filesystem/abspath', $abspath );
return $this->getDirectory($abspath);
}
/** Not in use yet, do not use. Future replacement. */
public function checkBackUpFolder($folder = SHORTPIXEL_BACKUP_FOLDER)
{
$dirObj = $this->getDirectory($folder);
$result = $dirObj->check(true); // check creates the whole structure if needed.
return $result;
}
/** Utility function that tries to convert a file-path to a webURL.
*
* If possible, rely on other better methods to find URL ( e.g. via WP functions ).
*/
public function pathToUrl(FileModel $file)
{
$filepath = $file->getFullPath();
$directory = $file->getFileDir();
$is_multi_site = $this->env->is_multisite;
$is_main_site = $this->env->is_mainsite;
// stolen from wp_get_attachment_url
if ( ( $uploads = wp_get_upload_dir() ) && (false === $uploads['error'] || strlen(trim($uploads['error'])) == 0 ) ) {
// Check that the upload base exists in the file location.
if ( 0 === strpos( $filepath, $uploads['basedir'] ) ) { // Simple as it should, filepath and basedir share.
// Replace file location with url location.
$url = str_replace( $uploads['basedir'], $uploads['baseurl'], $filepath );
}
// Multisite backups are stored under uploads/ShortpixelBackups/etc , but basedir would include uploads/sites/2 etc, not matching above
// If this is case, test if removing the last two directories will result in a 'clean' uploads reference.
// This is used by getting preview path ( backup pathToUrl) in bulk and for comparer..
elseif ($is_multi_site && ! $is_main_site && 0 === strpos($filepath, dirname(dirname($uploads['basedir']))) )
{
$url = str_replace( dirname(dirname($uploads['basedir'])), dirname(dirname($uploads['baseurl'])), $filepath );
$homeUrl = home_url();
// The result didn't end in a full URL because URL might have less subdirs ( dirname dirname) .
// This happens when site has blogs.dir (sigh) on a subdomain . Try to substitue the ABSPATH root with the home_url
if (strpos($url, $homeUrl) === false)
{
$url = str_replace( trailingslashit(ABSPATH), trailingslashit($homeUrl), $filepath);
}
} elseif ( false !== strpos( $filepath, 'wp-content/uploads' ) ) {
// Get the directory name relative to the basedir (back compat for pre-2.7 uploads)
$url = trailingslashit( $uploads['baseurl'] . '/' . _wp_get_attachment_relative_path( $filepath ) ) . wp_basename( $filepath );
} else {
// It's a newly-uploaded file, therefore $file is relative to the basedir.
$url = $uploads['baseurl'] . "/$filepath";
}
}
$wp_home_path = (string) $this->getWPAbsPath();
// If the whole WP homepath is still in URL, assume the replace when wrong ( not replaced w/ URL)
// This happens when file is outside of wp_uploads_dir
if (strpos($url, $wp_home_path) !== false)
{
// This is SITE URL, for the same reason it should be home_url in FILEMODEL. The difference is when the site is running on a subdirectory
// (1) ** This is a fix for a real-life issue, do not change if this causes issues, another fix is needed then.
// (2) ** Also a real life fix when a path is /wwwroot/assets/sites/2/ etc, in get site url, the home URL is the site URL, without appending the sites stuff. Fails on original image.
if ($is_multi_site && ! $is_main_site)
{
$wp_home_path = trailingslashit($uploads['basedir']);
$home_url = trailingslashit($uploads['baseurl']);
}
else
$home_url = trailingslashit(get_site_url()); // (1)
$url = str_replace($wp_home_path, $home_url, $filepath);
}
// can happen if there are WP path errors.
if (is_null($url))
return false;
$parsed = parse_url($url); // returns array, null, or false.
// Some hosts set the content dir to a relative path instead of a full URL. Api can't handle that, so add domain and such if this is the case.
if ( !isset($parsed['scheme']) ) {//no absolute URLs used -> we implement a hack
if (isset($parsed['host'])) // This is for URL's for // without http or https. hackhack.
{
$scheme = is_ssl() ? 'https:' : 'http:';
return $scheme. $url;
}
else
{
// From Metafacade. Multiple solutions /hacks.
$home_url = trailingslashit((function_exists("is_multisite") && is_multisite()) ? trim(network_site_url("/")) : trim(home_url()));
return $home_url . ltrim($url,'/');//get the file URL
}
}
if (! is_null($parsed) && $parsed !== false)
return $url;
return false;
}
public function checkURL($url)
{
if (! $this->pathIsURL($url))
{
//$siteurl = get_option('siteurl');
if (strpos($url, get_site_url()) == false)
{
$url = get_site_url(null, $url);
}
}
return apply_filters('shortpixel/filesystem/url', $url);
}
/** Utility function to check if a path is an URL
* Checks if this path looks like an URL.
* @param $path String Path to check
* @return Boolean If path seems domain.
*/
public function pathIsUrl($path)
{
$is_http = (substr($path, 0, 4) == 'http') ? true : false;
$is_https = (substr($path, 0, 5) == 'https') ? true : false;
$is_neutralscheme = (substr($path, 0, 2) == '//') ? true : false; // when URL is relative like //wp-content/etc
$has_urldots = (strpos($path, '://') !== false) ? true : false; // Like S3 offloads
if ($is_http || $is_https || $is_neutralscheme || $has_urldots)
return true;
else
return false;
}
/** Sort files / directories in a certain way.
* Future dev to include options via arg.
*/
public function sortFiles($array, $args = array() )
{
if (count($array) == 0)
return $array;
// what are we sorting.
$class = get_class($array[0]);
$is_files = ($class == 'ShortPixel\FileModel') ? true : false; // if not files, then dirs.
usort($array, function ($a, $b) use ($is_files)
{
if ($is_files)
return strcmp($a->getFileName(), $b->getFileName());
else {
return strcmp($a->getName(), $b->getName());
}
}
);
return $array;
}
// @todo Deprecate this, move some functs perhaps to DownloadHelper.
// @todo Should not be in use anymore. Remove on next update / annoyance
public function downloadFile($url, $destinationPath)
{
Log::addWarn('Deprecated DownloadFile function invoked (FileSystemController)');
$downloadTimeout = max(SHORTPIXEL_MAX_EXECUTION_TIME - 10, 15);
$fs = \wpSPIO()->filesystem(); // @todo change this all to $this
// $fs = \wpSPIO()->fileSystem();
$destinationFile = $fs->getFile($destinationPath);
$args_for_get = array(
'stream' => true,
'filename' => $destinationPath,
'timeout' => $downloadTimeout,
);
$response = wp_remote_get( $url, $args_for_get );
if(is_wp_error( $response )) {
Log::addError('Download file failed', array($url, $response->get_error_messages(), $response->get_error_codes() ));
// Try to get it then via this way.
$response = download_url($url, $downloadTimeout);
if (!is_wp_error($response)) // response when alright is a tmp filepath. But given path can't be trusted since that can be reason for fail.
{
$tmpFile = $fs->getFile($response);
$result = $tmpFile->move($destinationFile);
} // download_url ..
else {
Log::addError('Secondary download failed', array($url, $response->get_error_messages(), $response->get_error_codes() ));
}
}
else { // success, at least the download.
$destinationFile = $fs->getFile($response['filename']);
}
Log::addDebug('Remote Download attempt result', array($url, $destinationPath));
if ($destinationFile->exists())
return true;
else
return false;
}
/** Get all files from a directory tree, starting at given dir.
* @param DirectoryModel $dir to recursive into
* @param Array $filters Collection of optional filters as accepted by FileFilter in directoryModel
* @return Array Array of FileModel Objects
**/
public function getFilesRecursive(DirectoryModel $dir, $filters = array() )
{
$fileArray = array();
if (! $dir->exists())
return $fileArray;
$files = $dir->getFiles($filters);
$fileArray = array_merge($fileArray, $files);
$subdirs = $dir->getSubDirectories();
foreach($subdirs as $subdir)
{
$fileArray = array_merge($fileArray, $this->getFilesRecursive($subdir, $filters));
}
return $fileArray;
}
// Url very sparingly.
public function url_exists($url)
{
if (! \wpSPIO()->env()->is_function_usable('curl_init'))
{
return null;
}
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_NOBODY, true);
curl_exec($ch);
$responseCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($responseCode == 200)
{
return true;
}
else {
return false;
}
}
/** Any files / directories loaded while this is active will not check for exists or other filesystem operations
*
*/
public function startTrustedMode()
{
if (\wpSPIO()->env()->useTrustedMode())
{
FileModel::$TRUSTED_MODE = true;
DirectoryModel::$TRUSTED_MODE = true;
}
}
public function endTrustedMode()
{
if (\wpSPIO()->env()->useTrustedMode())
{
FileModel::$TRUSTED_MODE = false;
DirectoryModel::$TRUSTED_MODE = false;
}
}
/** Old method of getting a subDir. This is messy and hopefully should not be used anymore. It's added here for backward compat in case of exceptions */
private function returnOldSubDir($file)
{
// Experimental FS handling for relativePath. Should be able to cope with more exceptions. See Unit Tests
Log::addWarn('Return Old Subdir was called, everything else failed');
$homePath = get_home_path();
if($homePath == '/') {
$homePath = $this->getWPAbsPath();
}
$hp = wp_normalize_path($homePath);
$file = wp_normalize_path($file);
// $sp__uploads = wp_upload_dir();
if(strstr($file, $hp)) {
$path = str_replace( $hp, "", $file);
} elseif( strstr($file, dirname( WP_CONTENT_DIR ))) { //in some situations the content dir is not inside the root, check this also (ex. single.shortpixel.com)
$path = str_replace( trailingslashit(dirname( WP_CONTENT_DIR )), "", $file);
} elseif( (strstr(realpath($file), realpath($hp)))) {
$path = str_replace( realpath($hp), "", realpath($file));
} elseif( strstr($file, trailingslashit(dirname(dirname( SHORTPIXEL_UPLOADS_BASE )))) ) {
$path = str_replace( trailingslashit(dirname(dirname( SHORTPIXEL_UPLOADS_BASE ))), "", $file);
} else {
$path = (substr($file, 1));
}
$pathArr = explode('/', $path);
unset($pathArr[count($pathArr) - 1]);
return implode('/', $pathArr) . '/';
}
}

View File

@ -0,0 +1,422 @@
<?php
namespace ShortPixel\Controller;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Notices\NoticeController as Notices;
use ShortPixel\Helper\UtilHelper as UtilHelper;
use ShortPixel\Model\FrontImage as FrontImage;
use ShortPixel\ShortPixelImgToPictureWebp as ShortPixelImgToPictureWebp;
/** Handle everything that SP is doing front-wise */
class FrontController extends \ShortPixel\Controller
{
// DeliverWebp option settings for front-end delivery of webp
const WEBP_GLOBAL = 1;
const WEBP_WP = 2;
const WEBP_NOCHANGE = 3;
public function __construct()
{
if (\wpSPIO()->env()->is_front) // if is front.
{
$this->initWebpHooks();
}
}
protected function initWebpHooks()
{
$webp_option = \wpSPIO()->settings()->deliverWebp;
if ( $webp_option ) { // @tood Replace this function with the one in ENV.
if(UtilHelper::shortPixelIsPluginActive('shortpixel-adaptive-images/short-pixel-ai.php')) {
Notices::addWarning(__('Please deactivate the ShortPixel Image Optimizer\'s
<a href="options-general.php?page=wp-shortpixel-settings&part=adv-settings">Deliver the next generation versions of the images in the front-end</a>
option when the ShortPixel Adaptive Images plugin is active.','shortpixel-image-optimiser'), true);
}
elseif( $webp_option == self::WEBP_GLOBAL ){
//add_action( 'wp_head', array($this, 'addPictureJs') ); // adds polyfill JS to the header || Removed. Browsers without picture support?
add_action( 'init', array($this, 'startOutputBuffer'), 1 ); // start output buffer to capture content
} elseif ($webp_option == self::WEBP_WP){
add_filter( 'the_content', array($this, 'convertImgToPictureAddWebp'), 10000 ); // priority big, so it will be executed last
add_filter( 'the_excerpt', array($this, 'convertImgToPictureAddWebp'), 10000 );
add_filter( 'post_thumbnail_html', array($this,'convertImgToPictureAddWebp') );
}
}
}
/* Picture generation, hooked on the_content filter
* @param $content String The content to check and convert
* @return String Converted content
*/
public function convertImgToPictureAddWebp($content) {
if(function_exists('is_amp_endpoint') && is_amp_endpoint()) {
//for AMP pages the <picture> tag is not allowed
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
return $content . (isset($_GET['SHORTPIXEL_DEBUG']) ? '<!-- SPDBG is AMP -->' : '');
}
$content = $this->convert($content);
return $content;
}
public function startOutputBuffer() {
$env = wpSPIO()->env();
if ($env->is_admin || $env->is_ajaxcall)
return;
$call = array($this, 'convertImgToPictureAddWebp');
ob_start( $call );
}
protected function convert($content)
{
// Don't do anything with the RSS feed.
if (is_feed() || is_admin()) {
Log::addInfo('SPDBG convert is_feed or is_admin');
return $content; // . (isset($_GET['SHORTPIXEL_DEBUG']) ? '<!-- -->' : '');
}
$new_content = $this->testPictures($content);
if ($new_content !== false)
{
$content = $new_content;
}
else
{
Log::addDebug('Test Pictures returned empty.');
}
if (! class_exists('DOMDocument'))
{
Log::addWarn('Webp Active, but DomDocument class not found ( missing xmldom library )');
return false;
}
// preg_match_all
$content = preg_replace_callback('/<img[^>]*>/i', array($this, 'convertImage'), $content);
// [BS] No callback because we need preg_match_all
$content = $this->testInlineStyle($content);
return $content;
}
/** If lazy loading is happening, get source (src) from those values
* Otherwise pass back image data in a regular way.
*/
private function lazyGet($img, $type)
{
$value = false;
$prefix = false;
if (isset($img['data-lazy-' . $type]) && strlen($img['data-lazy-' . $type]) > 0)
{
$value = $img['data-lazy-' . $type];
$prefix = 'data-lazy-';
}
elseif( isset($img['data-' . $type]) && strlen($img['data-' . $type]) > 0)
{
$value = $img['data-' . $type];
$prefix = 'data-';
}
elseif(isset($img[$type]) && strlen($img[$type]) > 0)
{
$value = $img[$type];
$prefix = '';
}
return array(
'value' => $value,
'prefix' => $prefix,
);
}
/* Find image tags within picture definitions and make sure they are converted only by block, */
private function testPictures($content)
{
// [BS] Escape when DOM Module not installed
//if (! class_exists('DOMDocument'))
// return false;
//$pattern =''
//$pattern ='/(?<=(<picture>))(.*)(?=(<\/picture>))/mi';
$pattern = '/<picture.*?>.*?(<img.*?>).*?<\/picture>/is';
$count = preg_match_all($pattern, $content, $matches);
if ($matches === false)
return false;
if ( is_array($matches) && count($matches) > 0)
{
foreach($matches[1] as $match)
{
$imgtag = $match;
if (strpos($imgtag, 'class=') !== false) // test for class, if there, insert ours in there.
{
$pos = strpos($imgtag, 'class=');
$pos = $pos + 7;
$newimg = substr($imgtag, 0, $pos) . 'sp-no-webp ' . substr($imgtag, $pos);
}
else {
$pos = 4;
$newimg = substr($imgtag, 0, $pos) . ' class="sp-no-webp" ' . substr($imgtag, $pos);
}
$content = str_replace($imgtag, $newimg, $content);
}
}
return $content;
}
/* This might be a future solution for regex callbacks.
public static function processImageNode($node, $type)
{
$srcsets = $node->getElementsByTagName('srcset');
$srcs = $node->getElementsByTagName('src');
$imgs = $node->getElementsByTagName('img');
} */
/** Callback function with received an <img> tag match
* @param $match Image declaration block
* @return String Replacement image declaration block
*/
protected function convertImage($match)
{
$fs = \wpSPIO()->filesystem();
$raw_image = $match[0];
// Raw Image HTML
$image = new FrontImage($raw_image);
if (false === $image->isParseable())
{
return $raw_image;
}
$srcsetWebP = array();
$srcsetAvif = array();
// Count real instances of either of them, without fillers.
$webpCount = $avifCount = 0;
$imagePaths = array();
$definitions = $image->getImageData();
$imageBase = $image->getImageBase();
foreach ($definitions as $definition) {
// Split the URL from the size definition ( eg 800w )
$parts = preg_split('/\s+/', trim($definition));
$image_url = $parts[0];
// The space if not set is required, otherwise it will not work.
$image_condition = isset($parts[1]) ? ' ' . $parts[1] : ' ';
// A source that starts with data:, will not need processing.
if (strpos($image_url, 'data:') === 0)
{
continue;
}
$fsFile = $fs->getFile($image_url);
$extension = $fsFile->getExtension(); // trigger setFileinfo, which will resolve URL -> Path
$mime = $fsFile->getMime();
// Can happen when file is virtual, or other cases. Just assume this type.
if ($mime === false)
{
$mime = 'image/' . $extension;
}
$fileWebp = $fs->getFile($imageBase . $fsFile->getFileBase() . '.webp');
$fileWebpCompat = $fs->getFile($imageBase . $fsFile->getFileName() . '.webp');
// The URL of the image without the filename
$image_url_base = str_replace($fsFile->getFileName(), '', $image_url);
$files = array($fileWebp, $fileWebpCompat);
$fileAvif = $fs->getFile($imageBase . $fsFile->getFileBase() . '.avif');
$lastwebp = false;
foreach($files as $index => $thisfile)
{
if (! $thisfile->exists())
{
// FILTER: boolean, object, string, filedir
$thisfile = $fileWebp_exists = apply_filters('shortpixel/front/webp_notfound', false, $thisfile, $image_url, $imageBase);
}
if ($thisfile !== false)
{
// base url + found filename + optional condition ( in case of sourceset, as in 1400w or similar)
$webpCount++;
$lastwebp = $image_url_base . $thisfile->getFileName() . $image_condition;
$srcsetWebP[] = $lastwebp;
break;
}
elseif ($index+1 !== count($files)) // Don't write the else on the first file, because then the srcset will be written twice ( if file exists on the first fails)
{
continue;
}
else {
$lastwebp = $definition;
$srcsetWebP[] = $lastwebp;
}
}
if (false === $fileAvif->exists())
{
$fileAvif = apply_filters('shortpixel/front/webp_notfound', false, $fileAvif, $image_url, $imageBase);
}
if ($fileAvif !== false)
{
$srcsetAvif[] = $image_url_base . $fileAvif->getFileName() . $image_condition;
$avifCount++;
}
else { //fallback to jpg
if (false !== $lastwebp) // fallback to webp if there is a variant in this run. or jpg if none
{
$srcsetAvif[] = $lastwebp;
}
else {
$srcsetAvif[] = $definition;
}
}
}
if ($webpCount == 0 && $avifCount == 0) {
return $raw_image;
}
$args = array();
if ($webpCount > 0)
$args['webp'] = $srcsetWebP;
if ($avifCount > 0)
$args['avif'] = $srcsetAvif;
$output = $image->parseReplacement($args);
return $output;
}
protected function testInlineStyle($content)
{
//preg_match_all('/background.*[^:](url\(.*\))[;]/isU', $content, $matches);
preg_match_all('/url\(.*\)/isU', $content, $matches);
if (count($matches) == 0)
return $content;
$content = $this->convertInlineStyle($matches, $content);
return $content;
}
/** Function to convert inline CSS backgrounds to webp
* @param $match Regex match for inline style
* @return String Replaced (or not) content for webp.
* @author Bas Schuiling
*/
protected function convertInlineStyle($matches, $content)
{
$fs = \wpSPIO()->filesystem();
$allowed_exts = array('jpg', 'jpeg', 'gif', 'png');
$converted = array();
for($i = 0; $i < count($matches[0]); $i++)
{
$item = $matches[0][$i];
preg_match('/url\(\'(.*)\'\)/imU', $item, $match);
if (! isset($match[1]))
continue;
$url = $match[1];
//$parsed_url = parse_url($url);
$filename = basename($url);
$fileonly = pathinfo($url, PATHINFO_FILENAME);
$ext = pathinfo($url, PATHINFO_EXTENSION);
if (! in_array($ext, $allowed_exts))
continue;
$image_base_url = str_replace($filename, '', $url);
$fsFile = $fs->getFile($url);
$dir = $fsFile->getFileDir();
$imageBase = is_object($dir) ? $dir->getPath() : false;
if (false === $imageBase) // returns false if URL is external, do nothing with that.
continue;
$checkedFile = false;
$fileWebp = $fs->getFile($imageBase . $fsFile->getFileBase() . '.webp');
$fileWebpCompat = $fs->getFile($imageBase . $fsFile->getFileName() . '.webp');
if (true === $fileWebp->exists())
{
$checkedFile = $image_base_url . $fsFile->getFileBase() . '.webp';
}
elseif (true === $fileWebpCompat->exists())
{
$checkedFile = $image_base_url . $fsFile->getFileName() . '.webp';
}
else
{
$fileWebp_exists = apply_filters('shortpixel/front/webp_notfound', false, $fileWebp, $url, $imageBase);
if (false !== $fileWebp_exists)
{
$checkedFile = $image_base_url . $fsFile->getFileBase() . '.webp';
}
else {
$fileWebp_exists = apply_filters('shortpixel/front/webp_notfound', false, $fileWebpCompat, $url, $imageBase);
if (false !== $fileWebp_exists)
{
$checkedFile = $image_base_url . $fsFile->getFileName() . '.webp';
}
}
}
if ($checkedFile)
{
// if webp, then add another URL() def after the targeted one. (str_replace old full URL def, with new one on main match?
$target_urldef = $matches[0][$i];
if (! isset($converted[$target_urldef])) // if the same image is on multiple elements, this replace might go double. prevent.
{
$converted[] = $target_urldef;
$new_urldef = "url('" . $checkedFile . "'), " . $target_urldef;
$content = str_replace($target_urldef, $new_urldef, $content);
}
}
}
return $content;
}
} // class

View File

@ -0,0 +1,103 @@
<?php
namespace ShortPixel\Controller;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
/** Class for handling changes done by WP in the Image Edit section. **/
class ImageEditorController
{
protected static $instance;
public function __construct()
{
}
public static function getInstance()
{
if (is_null(self::$instance))
self::$instance = new ImageEditorController();
return self::$instance;
}
public static function localizeScript()
{
$local = array(
);
$fs = \wpSPIO()->filesystem();
// $local['is_restorable'] = ($mediaImage->isRestorable() ) ? 'true' : 'false';
// $local['is_optimized'] = ($mediaImage->isOptimized()) ? 'true' : 'false';
// $local['post_id'] = $post_id;
$local['optimized_text'] = sprintf(__('This image has been optimized by ShortPixel. It is strongly %s recommended %s to restore the image from the backup (if any) before editing it, because after saving the image all optimization data will be lost. If the image is not restored and ShortPixel re-optimizes the new image, this may result in a loss of quality. After you have finished editing, please optimize the image again by clicking "Optimize Now" as this will not happen automatically.', 'shortpixel-image-optimiser'), '<strong>', '</strong>');
$local['restore_link'] = 'javascript:window.ShortPixelProcessor.screen.RestoreItem(#post_id#)';
$local['restore_link_text'] = __('Restore the backup now.', 'shortpixel-image-optimiser');
$local['restore_link_text_unrestorable'] = __(' (This item is not restorable) ', 'shortpixel-image-optimiser');
return $local;
}
/*
* If SPIO has a backup of this image, load the backup file for editing instead of the (optimized) image
*/
public function getImageForEditor( $filepath, $attachment_id, $size)
{
$fs = \wpSPIO()->filesystem();
$mediaImage = $fs->getImage($attachment_id, 'media');
// Not an image, let's not get into this.
if (false === $mediaImage)
return $filepath;
$imagepath = false;
if ($size == 'full')
{
$optimized_and_backup = ($mediaImage->isOptimized() && $mediaImage->hasBackup());
if ( true === $optimized_and_backup)
$imagepath = $mediaImage->getBackupFile()->getFullPath();
}
elseif (false !== $mediaImage->getThumbNail($size)) {
$thumbObj = $mediaImage->getThumbNail($size);
$optimized_and_backup = ($thumbObj->isOptimized() && $thumbObj->hasBackup());
if (true === $optimized_and_backup)
$imagepath = $thumbObj->getBackupFile()->getFullPath();
}
if (true === $optimized_and_backup)
{
return $imagepath;
}
return $filepath;
}
public function saveImageFile( $null, $filename, $image, $mime_type, $post_id )
{
// Check image and if needed, delete backups.
$fs = \wpSPIO()->filesystem();
$mediaImage = $fs->getImage($post_id, 'media');
if (is_object($mediaImage))
{
$mediaImage->onDelete();
}
return $null;
}
} //class

View File

@ -0,0 +1,596 @@
<?php
namespace ShortPixel\Controller;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Notices\NoticeController as Notices;
use ShortPixel\Model\File\DirectoryOtherMediaModel as DirectoryOtherMediaModel;
use ShortPixel\Model\File\DirectoryModel as DirectoryModel;
use ShortPixel\Controller\OptimizeController as OptimizeController;
use ShortPixel\Helper\InstallHelper as InstallHelper;
use ShortPixel\Helper\UtilHelper as UtilHelper;
// Future contoller for the edit media metabox view.
class OtherMediaController extends \ShortPixel\Controller
{
private $folderIDCache;
private static $hasFoldersTable;
private static $hasCustomImages;
protected static $instance;
public function __construct()
{
parent::__construct();
}
public static function getInstance()
{
if (is_null(self::$instance))
self::$instance = new OtherMediaController();
return self::$instance;
}
public function getFolderTable()
{
global $wpdb;
return $wpdb->prefix . 'shortpixel_folders';
}
public function getMetaTable()
{
global $wpdb;
return $wpdb->prefix . 'shortpixel_meta';
}
// Get CustomFolder for usage.
public function getAllFolders()
{
$folders = $this->getFolders();
return $this->loadFoldersFromResult($folders);
//return $folders;
}
public function getActiveFolders()
{
$folders = $this->getFolders(array('remove_hidden' => true));
return $this->loadFoldersFromResult($folders);
}
private function loadFoldersFromResult($folders)
{
$dirFolders = array();
foreach($folders as $result)
{
$dirObj = new DirectoryOtherMediaModel($result);
$dirFolders[] = $dirObj;
}
return $dirFolders;
}
public function getActiveDirectoryIDS()
{
if (! is_null($this->folderIDCache))
return $this->folderIDCache;
global $wpdb;
$sql = 'SELECT id from ' . $wpdb->prefix .'shortpixel_folders where status <> -1';
$results = $wpdb->get_col($sql);
$this->folderIDCache = $results;
return $this->folderIDCache;
}
public function getHiddenDirectoryIDS()
{
global $wpdb;
$sql = 'SELECT id from ' . $wpdb->prefix .'shortpixel_folders where status = -1';
$results = $wpdb->get_col($sql);
return $results;
}
public function getFolderByID($id)
{
$folders = $this->getFolders(array('id' => $id));
if (count($folders) > 0)
{
$folders = $this->loadFoldersFromResult($folders);
return array_pop($folders);
}
return false;
}
public function getFolderByPath($path)
{
$folder = new DirectoryOtherMediaModel($path);
return $folder;
}
public function getCustomImageByPath($path)
{
global $wpdb;
$sql = 'SELECT id FROM ' . $this->getMetaTable() . ' WHERE path = %s';
$sql = $wpdb->prepare($sql, $path);
$custom_id = $wpdb->get_var($sql);
$fs = \wpSPIO()->filesystem();
if (! is_null($custom_id))
{
return $fs->getImage($custom_id, 'custom');
}
else
return $fs->getCustomStub($path); // stub
}
/* Check if installation has custom image, or anything. To show interface */
public function hasCustomImages()
{
if (! is_null(self::$hasCustomImages)) // prevent repeat
return self::$hasCustomImages;
if (InstallHelper::checkTableExists('shortpixel_meta') === false)
$count = 0;
else
{
global $wpdb;
$sql = 'SELECT count(id) as count from ' . $wpdb->prefix . 'shortpixel_meta';
$count = $wpdb->get_var($sql); //$this->getFolders(['only_count' => true, 'remove_hidden' => true]);
}
if ($count == 0)
$result = false;
else
$result = true;
self::$hasCustomImages = $result;
return $result;
}
public function showMenuItem()
{
$settings = \wpSPIO()->settings();
if ( $settings->showCustomMedia)
{
return true;
}
return false;
}
public function addDirectory($path)
{
$fs = \wpSPIO()->filesystem();
$directory = new DirectoryOtherMediaModel($path);
// Check if this directory is allowed.
if ($this->checkDirectoryRecursive($directory) === false)
{
Log::addDebug('Check Recursive Directory not allowed');
return false;
}
if (! $directory->get('in_db'))
{
if ($directory->save())
{
$this->folderIDCache = null;
$directory->refreshFolder(true);
$directory->updateFileContentChange();
}
}
else // if directory is already added, fail silently, but still refresh it.
{
if ($directory->isRemoved())
{
$this->folderIDCache = null;
$directory->set('status', DirectoryOtherMediaModel::DIRECTORY_STATUS_NORMAL);
$directory->refreshFolder(true);
$directory->updateFileContentChange(); // does a save. Dunno if that's wise.
}
else
$directory->refreshFolder(false);
}
if ($directory->exists() && $directory->get('id') > 0)
return $directory;
else
return false;
}
// Recursive check if any of the directories is not addable. If so cancel the whole thing.
public function checkDirectoryRecursive($directory)
{
if ($directory->checkDirectory() === false)
{
return false;
}
$subDirs = $directory->getSubDirectories();
foreach($subDirs as $subDir)
{
if ($subDir->checkDirectory(true) === false)
{
return false;
}
else
{
$result = $this->checkDirectoryRecursive($subDir);
if ($result === false)
{
return $result;
}
}
}
return true;
}
// Main function to add a path to the Custom Media.
public function addImage($path_or_file, $args = array())
{
$defaults = array(
'is_nextgen' => false,
);
$args = wp_parse_args($args, $defaults);
$fs = \wpSPIO()->filesystem();
if (is_object($path_or_file)) // assume fileObject
{
$file = $path_or_file;
}
else
{
$file = $fs->getFile($path_or_file);
}
$folder = $this->getFolderByPath( (string) $file->getFileDir());
if ($folder->get('in_db') === false)
{
if ($args['is_nextgen'] == true)
{
$folder->set('status', DirectoryOtherMediaModel::DIRECTORY_STATUS_NEXTGEN );
}
$folder->save();
}
$folder->addImages(array($file));
}
/* New structure for folder refresing based on checked value in database + interval. Via Scan interface
*
* @param $args Array ( force true / false )
* @return Array - Should return folder_id, folder_path, amount of new files / result / warning
*/
public function doNextRefreshableFolder($args = array())
{
$defaults = array(
'force' => false,
'interval' => apply_filters('shortpixel/othermedia/refreshfolder_interval', HOUR_IN_SECONDS),
);
$args = wp_parse_args($args, $defaults);
global $wpdb;
$folderTable = $this->getFolderTable();
$tsInterval = UtilHelper::timestampToDB(time() - $args['interval']);
$sql = ' SELECT id FROM ' . $folderTable . ' WHERE status >= 0 AND (ts_checked <= %s OR ts_checked IS NULL)';
$sql = $wpdb->prepare($sql, $tsInterval);
$folder_id = $wpdb->get_var($sql);
if (is_null($folder_id))
{
return false;
}
$directoryObj = $this->getFolderByID($folder_id);
$old_count = $directoryObj->get('fileCount');
$return = array(
'folder_id' => $folder_id,
'old_count' => $old_count,
'new_count' => null,
'path' => $directoryObj->getPath(),
'message' => '',
);
// Always force here since last updated / interval is decided by interal on the above query
$result = $directoryObj->refreshFolder($args['force']);
if (false === $result)
{
$directoryObj->set('checked', time()); // preventing loops here in case some wrong
$directoryObj->save();
// Probably should catch some notice here to return @todo
}
$new_count = $directoryObj->get('fileCount');
$return['new_count'] = $new_count;
if ($old_count == $new_count)
{
$message = __('No new files added', 'shortpixel-image-optimiser');
}
elseif ($old_count < $new_count)
{
$message = print_f(__(' %s files added', 'shortpixel-image-optimiser'), ($new_count-$old_count));
}
else {
$message = print_f(__(' %s files removed', 'shortpixel-image-optimiser'), ($old_count-$new_count));
}
$return['message'] = $message;
return $return;
}
public function resetCheckedTimestamps()
{
global $wpdb;
$folderTable = $this->getFolderTable();
$sql = 'UPDATE ' . $folderTable . ' set ts_checked = NULL ';
$wpdb->query($sql);
}
/**
* Function to clean the folders and meta from unused stuff
*/
protected function cleanUp()
{
global $wpdb;
$folderTable = $this->getFolderTable();
$metaTable = $this->getMetaTable();
// Remove folders that are removed, and have no images in MetaTable.
$sql = " DELETE FROM $folderTable WHERE status < 0 AND id NOT IN ( SELECT DISTINCT folder_id FROM $metaTable)";
$result = $wpdb->query($sql);
}
/* Check if this directory is part of the MediaLibrary */
public function checkifMediaLibrary(DirectoryModel $directory)
{
$fs = \wpSPIO()->filesystem();
$uploadDir = $fs->getWPUploadBase();
$wpUploadDir = wp_upload_dir(null, false);
$is_year_based = (isset($wpUploadDir['subdir']) && strlen(trim($wpUploadDir['subdir'])) > 0) ? true : false;
// if it's the uploads base dir, check if the library is year-based, then allow. If all files are in uploads root, don't allow.
if ($directory->getPath() == $uploadDir->getPath() )
{
if ($is_year_based)
{
return false;
}
return true;
}
elseif (! $directory->isSubFolderOf($uploadDir))// The easy check. No subdir of uploads, no problem.
{
return false;
}
elseif ($directory->isSubFolderOf($uploadDir)) // upload subdirs come in variation of year or month, both numeric. Exclude the WP-based years
{
// Only check if direct subdir of /uploads/ is a number-based directory name. Don't bother with deeply nested dirs with accidental year.
if ($directory->getParent()->getPath() !== $uploadDir->getPath())
{
return false;
}
$name = $directory->getName();
if (is_numeric($name) && strlen($name) == 4) // exclude year based stuff.
{
return true;
}
else {
return false;
}
}
}
public function browseFolder($postDir)
{
$error = array('is_error' => true, 'message' => '');
if ( ! $this->userIsAllowed ) {
$error['message'] = __('You do not have sufficient permissions to access this page.','shortpixel-image-optimiser');
return $error;
}
$fs = \wpSPIO()->filesystem();
$rootDirObj = $fs->getWPFileBase();
$path = $rootDirObj->getPath();
$folders = array();
if (! is_null($postDir) && strlen($postDir) > 0)
{
$postDir = rawurldecode($postDir);
$children = explode('/', $postDir );
foreach($children as $child)
{
if ($child == '.' || $child == '..')
continue;
$path .= '/' . $child;
}
}
$dirObj = $fs->getDirectory($path);
if ($dirObj->getPath() !== $rootDirObj->getPath() && ! $dirObj->isSubFolderOf($rootDirObj))
{
$error['message'] = __('This directory seems not part of WordPress', 'shortpixel-image-optimiser');
return $error;
}
if( $dirObj->exists() ) {
//$dir = $fs->getDirectory($postDir);
// $files = $dirObj->getFiles();
$subdirs = $fs->sortFiles($dirObj->getSubDirectories()); // runs through FS sort.
foreach($subdirs as $index => $dir) // weed out the media library subdirectories.
{
$dirname = $dir->getName();
// @todo This should probably be checked via getBackupDirectory or so, not hardcoded ShortipxelBackups
if($dirname == 'ShortpixelBackups' || $this->checkifMediaLibrary($dir) )
{
unset($subdirs[$index]);
}
}
if( count($subdirs) > 0 ) {
// echo "<ul class='jqueryFileTree'>";
foreach($subdirs as $dir ) {
$returnDir = substr($dir->getPath(), strlen($rootDirObj->getPath())); // relative to root.
$dirpath = $dir->getPath();
$dirname = $dir->getName();
$folderObj = $this->getFolderByPath($dirpath);
$htmlRel = str_replace("'", "&apos;", $returnDir );
$htmlName = htmlentities($dirname);
//$ext = preg_replace('/^.*\./', '', $file);
if( $dir->exists() ) {
//KEEP the spaces in front of the rel values - it's a trick to make WP Hide not replace the wp-content path
// echo "<li class='directory collapsed'><a rel=' " .esc_attr($htmlRel) . "'>" . esc_html($htmlName) . "</a></li>";
$htmlRel = esc_attr($htmlRel);
$folders[] = array(
'relpath' => $htmlRel,
'name' => esc_html($htmlName),
'type' => 'folder',
'is_active' => (true === $folderObj->get('in_db') && false === $folderObj->isRemoved()),
);
}
}
// echo "</ul>";
}
elseif ($_POST['dir'] == '/')
{
$error['message'] = __('No Directories found that can be added to Custom Folders', 'shortpixel-image-optimiser');
return $error;
/* echo "<ul class='jqueryFileTree'>";
esc_html_e('No Directories found that can be added to Custom Folders', 'shortpixel-image-optimiser');
echo "</ul>"; */
}
else {
$error['message'] = 'Nothing found';
return $error;
}
}
else {
$error['message'] = 'Dir not existing';
return $error;
}
return $folders;
}
/* Get the custom Folders from DB, put them in model
@return Array Array database result
@todo Has been replaced by getItems in FoldersViewController
*/
private function getFolders($args = array())
{
global $wpdb;
$defaults = array(
'id' => false, // Get folder by Id
'remove_hidden' => true, // Query only active folders
'path' => false,
'only_count' => false,
'limit' => false,
'offset' => false,
);
$args = wp_parse_args($args, $defaults);
if (! $this->hasFoldersTable())
{
if ($args['only_count'])
return 0;
else
return array();
}
$fs = \wpSPIO()->fileSystem();
if ($args['only_count'])
$selector = 'count(id) as id';
else
$selector = '*';
$sql = "SELECT " . $selector . " FROM " . $wpdb->prefix . "shortpixel_folders WHERE 1=1 ";
$prepare = array();
// $mask = array();
if ($args['id'] !== false && $args['id'] > 0)
{
$sql .= ' AND id = %d';
$prepare[] = $args['id'];
}
elseif($args['path'] !== false && strlen($args['path']) > 0)
{
$sql .= ' AND path = %s';
$prepare[] = $args['path'];
}
if ($args['remove_hidden'])
{
$sql .= " AND status <> -1";
}
if (count($prepare) > 0)
$sql = $wpdb->prepare($sql, $prepare);
if ($args['only_count'])
$results = intval($wpdb->get_var($sql));
else
$results = $wpdb->get_results($sql);
return $results;
}
private function hasFoldersTable()
{
return InstallHelper::checkTableExists('shortpixel_folders');
}
} // Class

View File

@ -0,0 +1,101 @@
<?php
namespace ShortPixel\Controller\Queue;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortQ\ShortQ as ShortQ;
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
class CustomQueue extends Queue
{
protected $queueName = '';
protected $cacheName = 'CustomCache'; // When preparing, write needed data to cache.
protected static $instance;
public function __construct($queueName = 'Custom')
{
$shortQ = new ShortQ(static::PLUGIN_SLUG);
$this->q = $shortQ->getQueue($queueName);
$this->queueName = $queueName;
$options = array(
'numitems' => 5,
'mode' => 'wait',
'process_timeout' => 7000,
'retry_limit' => 20,
'enqueue_limit' => 120,
);
$options = apply_filters('shortpixel/customqueue/options', $options);
$this->q->setOptions($options);
}
public function getType()
{
return 'custom';
}
public function prepare()
{
$items = $this->queryItems();
return $this->prepareItems($items);
}
public function queryItems()
{
$last_id = $this->getStatus('last_item_id');
$limit = $this->q->getOption('enqueue_limit');
$prepare = array();
$items = array();
global $wpdb;
$folderSQL = ' SELECT id FROM ' . $wpdb->prefix . 'shortpixel_folders where status >= 0';
$folderRow = $wpdb->get_col($folderSQL);
// No Active Folders, No Items.
if (count($folderRow) == 0)
return $items;
// List of prepared (%d) for the folders.
$query_arr = join( ',', array_fill( 0, count( $folderRow ), '%d' ) );
$sql = 'SELECT id FROM ' . $wpdb->prefix . 'shortpixel_meta WHERE folder_id in ( ';
$sql .= $query_arr . ') ';
$prepare = $folderRow;
if ($last_id > 0)
{
$sql .= " AND id < %d ";
$prepare [] = intval($last_id);
}
$sql .= ' order by id DESC LIMIT %d ';
$prepare[] = $limit;
$sql = $wpdb->prepare($sql, $prepare);
$results = $wpdb->get_col($sql);
$fs = \wpSPIO()->filesystem();
foreach($results as $item_id)
{
$items[] = $item_id; //$fs->getImage($item_id, 'custom');
}
return array_filter($items);
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace ShortPixel\Controller\Queue;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortQ\ShortQ as ShortQ;
use ShortPixel\Controller\CacheController as CacheController;
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
class MediaLibraryQueue extends Queue
{
protected $queueName = '';
protected $cacheName = 'MediaCache'; // When preparing, write needed data to cache.
protected static $instance;
/* MediaLibraryQueue Instance */
public function __construct($queueName = 'Media')
{
$shortQ = new ShortQ(self::PLUGIN_SLUG);
$this->q = $shortQ->getQueue($queueName);
$this->queueName = $queueName;
$options = array(
'numitems' => 2, // amount of items to pull per tick when optimizing
'mode' => 'wait',
'process_timeout' => 7000, // time between request for the image. (in milisecs)
'retry_limit' => 30, // amount of times it will retry without errors before giving up
'enqueue_limit' => 200, // amount of items added to the queue when preparing.
);
$options = apply_filters('shortpixel/medialibraryqueue/options', $options);
$this->q->setOptions($options);
}
public function getType()
{
return 'media';
}
protected function prepare()
{
$items = $this->queryPostMeta();
return $this->prepareItems($items);
}
private function queryPostMeta()
{
$last_id = $this->getStatus('last_item_id');
$limit = $this->q->getOption('enqueue_limit');
$prepare = array();
global $wpdb;
$sqlmeta = "SELECT DISTINCT post_id FROM " . $wpdb->prefix . "postmeta where (meta_key = %s or meta_key = %s)";
$prepare[] = '_wp_attached_file';
$prepare[] = '_wp_attachment_metadata';
if ($last_id > 0)
{
$sqlmeta .= " and post_id < %d ";
$prepare [] = intval($last_id);
}
$sqlmeta .= ' order by post_id DESC LIMIT %d ';
$prepare[] = $limit;
$sqlmeta = $wpdb->prepare($sqlmeta, $prepare);
$results = $wpdb->get_col($sqlmeta);
$fs = \wpSPIO()->filesystem();
$items = array();
foreach($results as $item_id)
{
$items[] = $item_id; //$fs->getImage($item_id, 'media');
}
// Remove failed object, ie if getImage returned false.
return array_filter($items);
}
/* public function queueToMediaItem($queueItem)
{
$id = $queueItem->id;
return $fs->getMediaImage($id);
} */
}

View File

@ -0,0 +1,761 @@
<?php
namespace ShortPixel\Controller\Queue;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\Model\Image\ImageModel as ImageModel;
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Controller\CacheController as CacheController;
use ShortPixel\Controller\ResponseController as ResponseController;
use ShortPixel\Model\Converter\Converter as Converter;
use ShortPixel\Helper\UiHelper as UiHelper;
use ShortPixel\ShortQ\ShortQ as ShortQ;
abstract class Queue
{
protected $q;
// protected static $instance;
protected static $results;
const PLUGIN_SLUG = 'SPIO';
// Result status for Run function
const RESULT_ITEMS = 1;
const RESULT_PREPARING = 2;
const RESULT_PREPARING_DONE = 3;
const RESULT_EMPTY = 4;
const RESULT_QUEUE_EMPTY = 10;
const RESULT_RECOUNT = 11;
const RESULT_ERROR = -1;
const RESULT_UNKNOWN = -10;
abstract protected function prepare();
abstract public function getType();
public function createNewBulk()
{
$this->resetQueue();
$this->q->setStatus('preparing', true, false);
$this->q->setStatus('finished', false, false);
$this->q->setStatus('bulk_running', true, true);
$cache = new CacheController();
$cache->deleteItem($this->cacheName);
}
public function startBulk()
{
$this->q->setStatus('preparing', false, false);
$this->q->setStatus('running', true, true);
}
public function cleanQueue()
{
$this->q->cleanQueue();
}
public function resetQueue()
{
$this->q->resetQueue();
}
// gateway to set custom options for queue.
public function setOptions($options)
{
return $this->q->setOptions($options);
}
/** Enqueues a single items into the urgent queue list
* - Should not be used for bulk images
* @param ImageModel $mediaItem An ImageModel (CustomImageModel or MediaLibraryModel) object
* @return mixed
*/
public function addSingleItem(ImageModel $imageModel, $args = array())
{
$defaults = array(
'forceExclusion' => false,
);
$args = wp_parse_args($args, $defaults);
$qItem = $this->imageModelToQueue($imageModel);
$counts = $qItem->counts;
$media_id = $imageModel->get('id');
// Check if this is a duplicate existing.
if ($imageModel->getParent() !== false)
{
$media_id = $imageModel->getParent();
}
if (count($args) > 0)
{
$qItem->options = $args;
}
$result = new \stdClass;
$item = array('id' => $media_id, 'value' => $qItem, 'item_count' => $counts->creditCount);
$this->q->addItems(array($item), false);
$numitems = $this->q->withRemoveDuplicates()->enqueue(); // enqueue returns numitems
// $this->q->setStatus('preparing', $preparing, true); // add single should not influence preparing status.
$result = $this->getQStatus($result, $numitems);
$result->numitems = $numitems;
do_action('shortpixel_start_image_optimisation', $imageModel->get('id'), $imageModel);
return $result;
}
/** Drop Item if it needs dropping. This can be needed in case of image alteration and it's in the queue */
public function dropItem($item_id)
{
$this->q->removeItems(array(
'item_id' => $item_id,
));
}
public function run()
{
$result = new \stdClass();
$result->qstatus = self::RESULT_UNKNOWN;
$result->items = null;
if ( $this->getStatus('preparing') === true) // When preparing a queue for bulk
{
$prepared = $this->prepare();
$result->qstatus = self::RESULT_PREPARING;
$result->items = $prepared['items']; // number of items.
$result->images = $prepared['images'];
if ($prepared['items'] == 0)
{
Log::addDebug( $this->queueName . ' Queue, prepared came back as zero ', array($prepared, $result->items));
if ($prepared['results'] == 0) /// This means no results, empty query.
{
$result->qstatus = self::RESULT_PREPARING_DONE;
}
}
}
elseif ($this->getStatus('bulk_running') == true) // this is a bulk queue, don't start automatically.
{
if ($this->getStatus('running') == true)
{
$items = $this->deQueue();
}
elseif ($this->getStatus('preparing') == false && $this->getStatus('finished') == false)
{
$result->qstatus = self::RESULT_PREPARING_DONE;
}
elseif ($this->getStatus('finished') == true)
{
$result->qstatus = self::RESULT_QUEUE_EMPTY;
}
}
else // regular queue can run whenever.
{
$items = $this->deQueue();
}
if (isset($items)) // did a dequeue.
{
$result = $this->getQStatus($result, count($items));
$result->items = $items;
}
return $result;
}
protected function prepareItems($items)
{
$return = array('items' => 0, 'images' => 0, 'results' => 0);
$settings = \wpSPIO()->settings();
if (count($items) == 0)
{
$this->q->setStatus('preparing', false);
Log::addDebug('PrepareItems: Items can back as empty array. Nothing to prepare');
return $return;
}
$fs = \wpSPIO()->filesystem();
$queue = array();
$imageCount = $webpCount = $avifCount = $baseCount = 0;
$operation = $this->getCustomDataItem('customOperation'); // false or value (or null)
if (is_null($operation))
$operation = false;
// maybe while on the whole function, until certain time has elapsed?
foreach($items as $item_id)
{
// Migrate shouldn't load image object at all since that would trigger the conversion.
if ($operation == 'migrate' || $operation == 'removeLegacy')
{
$qObject = new \stdClass; //$this->imageModelToQueue($mediaItem);
$qObject->action = $operation;
$queue[] = array('id' => $item_id, 'value' => $qObject, 'item_count' => 1);
continue;
}
$mediaItem = $fs->getImage($item_id, $this->getType() );
//checking if the $mediaItem actually exists
if ( $mediaItem ) {
if ($mediaItem->isProcessable() && $mediaItem->isOptimizePrevented() == false && ! $operation) // Checking will be done when processing queue.
{
// If PDF and not enabled, not processing.
if ($mediaItem->getExtension() == 'pdf' && ! $settings->optimizePdfs)
{
continue;
}
if ($this->isDuplicateActive($mediaItem, $queue))
{
continue;
}
$qObject = $this->imageModelToQueue($mediaItem);
$counts = $qObject->counts;
$media_id = $mediaItem->get('id');
if ($mediaItem->getParent() !== false)
{
$media_id = $mediaItem->getParent();
}
$queue[] = array('id' => $media_id, 'value' => $qObject, 'item_count' => $counts->creditCount);
$imageCount += $counts->creditCount;
$webpCount += $counts->webpCount;
$avifCount += $counts->avifCount;
$baseCount += $counts->baseCount; // base images (all minus webp/avif)
do_action('shortpixel_start_image_optimisation', $media_id, $mediaItem);
}
else
{
if($operation !== false)
{
if ($operation == 'bulk-restore')
{
if ($mediaItem->isRestorable())
{
$qObject = new \stdClass; //$this->imageModelToQueue($mediaItem);
$qObject->action = 'restore';
$queue[] = array('id' => $mediaItem->get('id'), 'value' => $qObject);
}
}
}
elseif($mediaItem->isOptimized())
{
}
else
{
$response = array(
'is_error' => true,
'item_type' => ResponseController::ISSUE_QUEUE_FAILED,
'message ' => ' Item failed: ' . $mediaItem->getProcessableReason(),
);
ResponseController::addData($item_id, $response);
}
}
}
else
{
$response = array(
'is_error' => true,
'item_type' => ResponseController::ISSUE_QUEUE_FAILED,
'message ' => ' Enqueing of item failed : invalid post content or post type',
);
ResponseController::addData($item_id, $response);
Log::addWarn('The item with id ' . $item_id . ' cannot be processed because it is either corrupted or an invalid post type');
}
}
$this->q->additems($queue);
$numitems = $this->q->enqueue();
$customData = $this->getStatus('custom_data');
$customData->webpCount += $webpCount;
$customData->avifCount += $avifCount;
$customData->baseCount += $baseCount;
$this->q->setStatus('custom_data', $customData, false);
// mediaItem should be last_item_id, save this one.
$this->q->setStatus('last_item_id', $item_id); // enum status to prevent a hang when no items are enqueued, thus last_item_id is not raised. save to DB.
$qCount = count($queue);
$return['items'] = $qCount;
$return['images'] = $imageCount;
/** NOTE! The count items is the amount of items queried and checked. It might be they never enqueued, just that the check process is running.
*/
$return['results'] = count($items); // This is the return of the query. Preparing should not be 'done' before the query ends, but it can return 0 on the qcount if all results are already optimized.
return $return; // only return real amount.
}
// Used by Optimizecontroller on handlesuccess.
public function getQueueName()
{
return $this->queueName;
}
public function getQStatus($result, $numitems)
{
if ($numitems == 0)
{
if ($this->getStatus('items') == 0 && $this->getStatus('errors') == 0 && $this->getStatus('in_process') == 0) // no items, nothing waiting in retry. Signal finished.
{
$result->qstatus = self::RESULT_QUEUE_EMPTY;
}
else
{
$result->qstatus = self::RESULT_EMPTY;
}
}
else
{
$result->qstatus = self::RESULT_ITEMS;
}
return $result;
}
public function getStats()
{
$stats = new \stdClass; // For frontend reporting back.
$stats->is_preparing = (bool) $this->getStatus('preparing');
$stats->is_running = (bool) $this->getStatus('running');
$stats->is_finished = (bool) $this->getStatus('finished');
$stats->in_queue = (int) $this->getStatus('items');
$stats->in_process = (int) $this->getStatus('in_process');
$stats->awaiting = $stats->in_queue + $stats->in_process; // calculation used for WP-CLI.
$stats->errors = (int) $this->getStatus('errors');
$stats->fatal_errors = (int) $this->getStatus('fatal_errors');
$stats->done = (int) $this->getStatus('done');
$stats->bulk_running = (bool) $this->getStatus('bulk_running');
$customData = $this->getStatus('custom_data');
if ($this->isCustomOperation())
{
$stats->customOperation = $this->getCustomDataItem('customOperation');
$stats->isCustomOperation = '10'; // numeric value for the bulk JS
}
$stats->total = $stats->in_queue + $stats->fatal_errors + $stats->errors + $stats->done + $stats->in_process;
if ($stats->total > 0)
{
$stats->percentage_done = round((100 / $stats->total) * ($stats->done + $stats->fatal_errors), 0, PHP_ROUND_HALF_DOWN);
}
else
$stats->percentage_done = 100; // no items means all done.
if (! $stats->is_running)
{
$stats->images = $this->countQueue();
}
return $stats;
}
/** Recounts the ItemSum for the Queue
*
* Note that this is not the same number as preparing adds to the cache, which counts across the installation how much images were already optimized. However, we don't want to stop and reset cache just for a few lost numbers so we should accept a flawed outcome here perhaps.
*/
protected function countQueue()
{
$recount = $this->q->itemSum('countbystatus');
$customData = $this->getStatus('custom_data');
$count = (object) [
'images' => $recount[ShortQ::QSTATUS_WAITING],
'images_done' => $recount[ShortQ::QSTATUS_DONE],
'images_inprocess' => $recount[ShortQ::QSTATUS_INPROCESS],
];
$count->images_webp = 0;
$count->images_avif = 0;
if (is_object($customData))
{
$count->images_webp = (int) $customData->webpCount;
$count->images_avif = (int) $customData->avifCount;
$count->images_basecount = (int) $customData->baseCount;
}
return $count;
}
protected function getStatus($name = false)
{
if ($name == 'items')
return $this->q->itemCount(); // This one also recounts once queue returns 0
elseif ($name == 'custom_data')
{
$customData = $this->q->getStatus('custom_data');
if (! is_object($customData))
{
$customData = $this->createCustomData();
}
return $customData;
}
return $this->q->getStatus($name);
}
public function setCustomBulk($type = null, $options = array() )
{
if (is_null($type))
return false;
$customData = $this->getStatus('custom_data');
$customData->customOperation = $type;
if (is_array($options) && count($options) > 0)
$customData->queueOptions = $options;
$this->getShortQ()->setStatus('custom_data', $customData);
}
// Return if this queue has any special operation outside of normal optimizing.
// Use to give the go processing when out of credits (ie)
public function isCustomOperation()
{
if ($this->getCustomDataItem('customOperation'))
{
return true;
}
return false;
}
public function getCustomDataItem($name)
{
$customData = $this->getStatus('custom_data');
if (is_object($customData) && property_exists($customData, $name))
{
return $customData->$name;
}
return false;
}
protected function deQueue()
{
$items = $this->q->deQueue(); // Items, can be multiple different according to throttle.
$items = array_map(array($this, 'queueToMediaItem'), $items);
return $items;
}
protected function queueToMediaItem($qItem)
{
$item = new \stdClass;
$item = $qItem->value;
$item->_queueItem = $qItem;
$item->item_id = $qItem->item_id;
$item->tries = $qItem->tries;
if (property_exists($item, 'files'))
{ // This must be array & shite.
$item->files = json_decode(json_encode($item->files), true);
}
return $item;
}
protected function mediaItemToQueue($item)
{
$mediaItem = clone $item; // clone here, not to loose referenced data.
unset($mediaItem->item_id);
unset($mediaItem->tries);
$qItem = $mediaItem->_queueItem;
unset($mediaItem->_queueItem);
$qItem->value = $mediaItem;
return $qItem;
}
// This is a general implementation - This should be done only once!
// The 'avif / webp left imp. is commented out since both API / and OptimizeController don't play well with this.
protected function imageModelToQueue(ImageModel $imageModel)
{
$item = new \stdClass;
$item->compressionType = \wpSPIO()->settings()->compressionType;
$data = $imageModel->getOptimizeData();
$urls = $data['urls'];
$params = $data['params'];
list($u, $baseCount) = $imageModel->getCountOptimizeData('thumbnails');
list($u, $webpCount) = $imageModel->getCountOptimizeData('webp');
list($u, $avifCount) = $imageModel->getCountOptimizeData('avif');
$counts = new \stdClass;
$counts->creditCount = $baseCount + $webpCount + $avifCount; // count the used credits for this item.
$counts->baseCount = $baseCount; // count the base images.
$counts->avifCount = $avifCount;
$counts->webpCount = $webpCount;
$removeKeys = array('image', 'webp', 'avif'); // keys not native to API / need to be removed.
// Is UI info, not for processing.
if (isset($data['params']['paths']))
{
unset($data['params']['paths']);
}
foreach($data['params'] as $sizeName => $param)
{
$plus = false;
$convertTo = array();
if ($param['image'] === true)
{
$plus = true;
}
if ($param['webp'] === true)
{
$convertTo[] = ($plus === true) ? '+webp' : 'webp';
}
if ($param['avif'] === true)
{
$convertTo[] = ($plus === true) ? '+avif' : 'avif';
}
foreach($removeKeys as $key)
{
if (isset($param[$key]))
{
unset($data['params'][$sizeName][$key]);
}
}
if (count($convertTo) > 0)
{
$convertTo = implode('|', $convertTo);
$data['params'][$sizeName]['convertto'] = $convertTo;
}
}
$converter = Converter::getConverter($imageModel, true);
if ($baseCount > 0 && is_object($converter) && $converter->isConvertable())
{
if ($converter->isConverterFor('png')) // Flag is set in Is_Processable in mediaLibraryModel, when settings are on, image is png.
{
$item->action = 'png2jpg';
}
elseif($converter->isConverterFor('heic'))
{
foreach($data['params'] as $sizeName => $sizeData)
{
if (isset($sizeData['convertto']))
{
$data['params'][$sizeName]['convertto'] = 'jpg';
}
}
// Run converter to create backup and make placeholder to block similar heics from overwriting.
$args = array('runReplacer' => false);
$converter->convert($args);
//Lossless because thumbnails will otherwise be derived of compressed image, leaving to double compr..
if (property_exists($item, 'compressionType'))
{
$item->compressionTypeRequested = $item->compressionType;
}
// Process Heic as Lossless so we don't have double opts.
$item->compressionType = ImageModel::COMPRESSION_LOSSLESS;
// Reset counts
$counts->baseCount = 1; // count the base images.
$counts->avifCount = 0;
$counts->webpCount = 0;
$counts->creditCount = 1;
}
}
// CompressionType can be integer, but not empty string. In cases empty string might happen, causing lossless optimization, which is not correct.
if (! is_null($imageModel->getMeta('compressionType')) && is_numeric($imageModel->getMeta('compressionType')))
{
$item->compressionType = $imageModel->getMeta('compressionType');
}
// Former securi function, add timestamp to all URLS, for cache busting.
$urls = $this->timestampURLS( array_values($urls), $imageModel->get('id'));
$item->urls = apply_filters('shortpixel_image_urls', $urls, $imageModel->get('id'));
if (count($data['params']) > 0)
{
$item->paramlist= array_values($data['params']);
}
if (count($data['returnParams']) > 0)
{
$item->returndatalist = $data['returnParams'];
}
// $item->preview = $imagePreviewURL;
$item->counts = $counts;
return $item;
}
// @internal
public function _debug_imageModelToQueue($imageModel)
{
return $this->imageModelToQueue($imageModel);
}
protected function timestampURLS($urls, $id)
{
// https://developer.wordpress.org/reference/functions/get_post_modified_time/
$time = get_post_modified_time('U', false, $id );
foreach($urls as $index => $url)
{
$urls[$index] = add_query_arg('ver', $time, $url); //has url
}
return $urls;
}
private function countQueueItem()
{
}
// Check if item is in queue. Considered not in queue if status is done.
public function isItemInQueue($item_id)
{
$itemObj = $this->q->getItem($item_id);
$notQ = array(ShortQ::QSTATUS_DONE, ShortQ::QSTATUS_FATAL);
if (is_object($itemObj) && in_array(floor($itemObj->status), $notQ) === false )
{
return true;
}
return false;
}
public function itemFailed($item, $fatal = false)
{
if ($fatal)
{
Log::addError('Item failed while optimizing', $item);
}
$qItem = $this->mediaItemToQueue($item); // convert again
$this->q->itemFailed($qItem, $fatal);
$this->q->updateItemValue($qItem);
}
public function updateItem($item)
{
$qItem = $this->mediaItemToQueue($item); // convert again
$this->q->updateItemValue($qItem);
}
public function isDuplicateActive($mediaItem, $queue = array() )
{
if ($mediaItem->get('type') === 'custom')
return false;
$WPMLduplicates = $mediaItem->getWPMLDuplicates();
$qitems = array();
if (count($queue) > 0)
{
foreach($queue as $qitem)
{
$qitems[] = $qitem['id'];
}
}
if (is_array($WPMLduplicates) && count($WPMLduplicates) > 0)
{
$duplicateActive = false;
foreach($WPMLduplicates as $duplicate_id)
{
if (in_array($duplicate_id, $qitems))
{
Log::addDebug('Duplicate Item is in queue already, skipping (ar). Duplicate:' . $duplicate_id);
$duplicateActive = true;
break;
}
elseif ($this->isItemInQueue($duplicate_id))
{
Log::addDebug('Duplicate Item is in queue already, skipping (db). Duplicate:' . $duplicate_id);
$duplicateActive = true;
break;
}
}
if (true === $duplicateActive)
{
return $duplicateActive;
}
}
return false;
}
public function itemDone ($item)
{
$qItem = $this->mediaItemToQueue($item); // convert again
$this->q->itemDone($qItem);
}
public function uninstall()
{
$this->q->uninstall();
}
public function activatePlugin()
{
$this->q->resetQueue();
}
public function getShortQ()
{
return $this->q;
}
// All custom Data in the App should be created here.
private function createCustomData()
{
$data = new \stdClass;
$data->webpCount = 0;
$data->avifCount = 0;
$data->baseCount = 0;
$data->customOperation = false;
return $data;
}
} // class

View File

@ -0,0 +1,363 @@
<?php
namespace ShortPixel\Controller;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
class QuotaController
{
protected static $instance;
const CACHE_NAME = 'quotaData';
protected $quotaData;
/** Singleton instance
* @return Object QuotaController object
*/
public static function getInstance()
{
if (is_null(self::$instance))
self::$instance = new QuotaController();
return self::$instance;
}
/**
* Return if account has any quota left
* @return boolean Has quota left
*/
public function hasQuota()
{
$settings = \wpSPIO()->settings();
if ($settings->quotaExceeded)
{
return false;
}
return true;
}
/**
* Retrieves QuotaData object from cache or from remote source
* @return array The quotadata array (remote format)
*/
protected function getQuotaData()
{
if (! is_null($this->quotaData))
return $this->quotaData;
$cache = new CacheController();
$cacheData = $cache->getItem(self::CACHE_NAME);
if (! $cacheData->exists() )
{
$quotaData = $this->getRemoteQuota();
if (! $this->hasQuota())
$timeout = MINUTE_IN_SECONDS;
else {
$timeout = HOUR_IN_SECONDS;
}
$cache->storeItem(self::CACHE_NAME, $quotaData, $timeout);
}
else
{
$quotaData = $cacheData->getValue();
}
return $quotaData;
}
/**
* Retrieve quota information for this account
* @return object quotadata SPIO format
*/
public function getQuota()
{
$quotaData = $this->getQuotaData();
$DateNow = time();
$DateSubscription = strtotime($quotaData['APILastRenewalDate']);
$DaysToReset = 30 - ( (int) ( ( $DateNow - $DateSubscription) / 84600) % 30);
$quota = (object) [
'unlimited' => isset($quotaData['Unlimited']) ? $quotaData['Unlimited'] : false,
'monthly' => (object) [
'text' => sprintf(__('%s/month', 'shortpixel-image-optimiser'), $quotaData['APICallsQuota']),
'total' => $quotaData['APICallsQuotaNumeric'],
'consumed' => $quotaData['APICallsMadeNumeric'],
'remaining' => max($quotaData['APICallsQuotaNumeric'] - $quotaData['APICallsMadeNumeric'], 0),
'renew' => $DaysToReset,
],
'onetime' => (object) [
'text' => $quotaData['APICallsQuotaOneTime'],
'total' => $quotaData['APICallsQuotaOneTimeNumeric'],
'consumed' => $quotaData['APICallsMadeOneTimeNumeric'],
'remaining' => $quotaData['APICallsQuotaOneTimeNumeric'] - $quotaData['APICallsMadeOneTimeNumeric'],
],
];
$quota->total = (object) [
'total' => $quota->monthly->total + $quota->onetime->total,
'consumed' => $quota->monthly->consumed + $quota->onetime->consumed,
'remaining' =>$quota->monthly->remaining + $quota->onetime->remaining,
];
return $quota;
}
/**
* Get available remaining - total - credits
* @return int Total remaining credits
*/
public function getAvailableQuota()
{
$quota = $this->getQuota();
return $quota->total->remaining;
}
/**
* Force to get quotadata from API, even if cache is still active ( use very sparingly )
* Does not actively fetches it, but invalidates cache, so next call will trigger a remote call
* @return void
*/
public function forceCheckRemoteQuota()
{
$cache = new CacheController();
$cacheData = $cache->getItem(self::CACHE_NAME);
$cacheData->delete();
$this->quotaData = null;
}
/**
* Validate account key in the API via quota check
* @param string $key User account key
* @return array Quotadata array (remote format) with validated key
*/
public function remoteValidateKey($key)
{
// Remove the cache before checking.
$this->forceCheckRemoteQuota();
return $this->getRemoteQuota($key, true);
}
/**
* Called when plugin detects the remote quota has been exceeded.
* Triggers various call to actions to customer
*/
public function setQuotaExceeded()
{
$settings = \wpSPIO()->settings();
$settings->quotaExceeded = 1;
$this->forceCheckRemoteQuota(); // remove the previous cache.
}
/**
* When quota is detected again via remote check, reset all call to actions
*/
private function resetQuotaExceeded()
{
$settings = \wpSPIO()->settings();
AdminNoticesController::resetAPINotices();
// Only reset after a quotaExceeded situation, otherwise it keeps popping.
if ($settings->quotaExceeded == 1)
{
AdminNoticesController::resetQuotaNotices();
}
// Log::addDebug('Reset Quota Exceeded and reset Notices');
$settings->quotaExceeded = 0;
}
/**
* [getRemoteQuota description]
* @param string $apiKey User account key
* @param boolean $validate Api should also validate key or not
* @return array Quotadata array (remote format) [with validated key]
*/
private function getRemoteQuota($apiKey = false, $validate = false)
{
if (! $apiKey && ! $validate) // validation is done by apikeymodel, might result in a loop.
{
$keyControl = ApiKeyController::getInstance();
$apiKey = $keyControl->forceGetApiKey();
}
$settings = \wpSPIO()->settings();
if($settings->httpProto != 'https' && $settings->httpProto != 'http') {
$settings->httpProto = 'https';
}
$requestURL = $settings->httpProto . '://' . SHORTPIXEL_API . '/v2/api-status.php';
$args = array(
'timeout'=> 15, // wait for 15 secs.
'body' => array('key' => $apiKey)
);
$argsStr = "?key=".$apiKey;
$serverAgent = isset($_SERVER['HTTP_USER_AGENT']) ? urlencode(sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT']))) : '';
$args['body']['useragent'] = "Agent" . $serverAgent;
$argsStr .= "&useragent=Agent".$args['body']['useragent'];
// Only used for keyValidation
if($validate) {
$statsController = StatsController::getInstance();
$imageCount = $statsController->find('media', 'itemsTotal');
$thumbsCount = $statsController->find('media', 'thumbsTotal');
$args['body']['DomainCheck'] = get_site_url();
$args['body']['Info'] = get_bloginfo('version') . '|' . phpversion();
$args['body']['ImagesCount'] = $imageCount; //$imageCount['mainFiles'];
$args['body']['ThumbsCount'] = $thumbsCount; // $imageCount['totalFiles'] - $imageCount['mainFiles'];
$argsStr .= "&DomainCheck={$args['body']['DomainCheck']}&Info={$args['body']['Info']}&ImagesCount=$imageCount&ThumbsCount=$thumbsCount";
}
$args['body']['host'] = parse_url(get_site_url(),PHP_URL_HOST);
$argsStr .= "&host={$args['body']['host']}";
if (defined('SHORTPIXEL_HTTP_AUTH_USER') && defined('SHORTPIXEL_HTTP_AUTH_PASSWORD'))
{
$args['body']['user'] = stripslashes(SHORTPIXEL_HTTP_AUTH_USER);
$args['body']['pass'] = stripslashes(SHORTPIXEL_HTTP_AUTH_PASSWORD);
$argsStr .= '&user=' . urlencode($args['body']['user']) . '&pass=' . urlencode($args['body']['pass']);
}
elseif(! is_null($settings->siteAuthUser) && strlen($settings->siteAuthUser)) {
$args['body']['user'] = stripslashes($settings->siteAuthUser);
$args['body']['pass'] = stripslashes($settings->siteAuthPass);
$argsStr .= '&user=' . urlencode($args['body']['user']) . '&pass=' . urlencode($args['body']['pass']);
}
if($settings !== false) {
$args['body']['Settings'] = $settings;
}
$time = microtime(true);
$comm = array();
//Try first HTTPS post. add the sslverify = false if https
if($settings->httpProto === 'https') {
$args['sslverify'] = apply_filters('shortpixel/system/sslverify', true);
}
$response = wp_remote_post($requestURL, $args);
$comm['A: ' . (number_format(microtime(true) - $time, 2))] = array("sent" => "POST: " . $requestURL, "args" => $args, "received" => $response);
//some hosting providers won't allow https:// POST connections so we try http:// as well
if(is_wp_error( $response )) {
$requestURL = $settings->httpProto == 'https' ?
str_replace('https://', 'http://', $requestURL) :
str_replace('http://', 'https://', $requestURL);
// add or remove the sslverify
if($settings->httpProto === 'https') {
$args['sslverify'] = apply_filters('shortpixel/system/sslverify', true);
} else {
unset($args['sslverify']);
}
$response = wp_remote_post($requestURL, $args);
$comm['B: ' . (number_format(microtime(true) - $time, 2))] = array("sent" => "POST: " . $requestURL, "args" => $args, "received" => $response);
if(!is_wp_error( $response )){
$settings->httpProto = ($settings->httpProto == 'https' ? 'http' : 'https');
} else {
}
}
//Second fallback to HTTP get
if(is_wp_error( $response )){
$args['body'] = null;
$requestURL .= $argsStr;
$response = wp_remote_get($requestURL, $args);
$comm['C: ' . (number_format(microtime(true) - $time, 2))] = array("sent" => "POST: " . $requestURL, "args" => $args, "received" => $response);
}
Log::addInfo("API STATUS COMM: " . json_encode($comm));
$defaultData = array(
"APIKeyValid" => false,
"Message" => __('API Key could not be validated due to a connectivity error.<BR>Your firewall may be blocking us. Please contact your hosting provider and ask them to allow connections from your site to api.shortpixel.com (IP 176.9.21.94).<BR> If you still cannot validate your API Key after this, please <a href="https://shortpixel.com/contact" target="_blank">contact us</a> and we will try to help. ','shortpixel-image-optimiser'),
"APICallsMade" => __('Information unavailable. Please check your API key.','shortpixel-image-optimiser'),
"APICallsQuota" => __('Information unavailable. Please check your API key.','shortpixel-image-optimiser'),
"APICallsMadeOneTime" => 0,
"APICallsQuotaOneTime" => 0,
"APICallsMadeNumeric" => 0,
"APICallsQuotaNumeric" => 0,
"APICallsMadeOneTimeNumeric" => 0,
"APICallsQuotaOneTimeNumeric" => 0,
"APICallsRemaining" => 0,
"APILastRenewalDate" => 0,
"DomainCheck" => 'NOT Accessible');
$defaultData = is_array($settings->currentStats) ? array_merge( $settings->currentStats, $defaultData) : $defaultData;
if(is_object($response) && get_class($response) == 'WP_Error') {
$urlElements = parse_url($requestURL);
$portConnect = @fsockopen($urlElements['host'],8,$errno,$errstr,15);
if(!$portConnect) {
$defaultData['Message'] .= "<BR>Debug info: <i>$errstr</i>";
}
return $defaultData;
}
if($response['response']['code'] != 200) {
return $defaultData;
}
$data = $response['body'];
$data = json_decode($data);
if(empty($data)) { return $defaultData; }
if($data->Status->Code != 2) {
$defaultData['Message'] = $data->Status->Message;
return $defaultData;
}
$dataArray = array(
"APIKeyValid" => true,
"APICallsMade" => number_format($data->APICallsMade) . __(' credits','shortpixel-image-optimiser'),
"APICallsQuota" => number_format($data->APICallsQuota) . __(' credits','shortpixel-image-optimiser'),
"APICallsMadeOneTime" => number_format($data->APICallsMadeOneTime) . __(' credits','shortpixel-image-optimiser'),
"APICallsQuotaOneTime" => number_format($data->APICallsQuotaOneTime) . __(' credits','shortpixel-image-optimiser'),
"APICallsMadeNumeric" => (int) max($data->APICallsMade, 0),
"APICallsQuotaNumeric" => (int) max($data->APICallsQuota, 0),
"APICallsMadeOneTimeNumeric" => (int) max($data->APICallsMadeOneTime, 0),
"APICallsQuotaOneTimeNumeric" => (int) max($data->APICallsQuotaOneTime, 0),
"Unlimited" => (property_exists($data, 'Unlimited') && $data->Unlimited == 'true') ? true : false,
"APILastRenewalDate" => $data->DateSubscription,
"DomainCheck" => (isset($data->DomainCheck) ? $data->DomainCheck : null)
);
// My Eyes! Basically : ApiCalls - ApiCalls used, both for monthly and onetime. Max of each is 0. Negative quota seems possible, but should not be substracted from one or the other.
$dataArray["APICallsRemaining"] = max($dataArray['APICallsQuotaNumeric'] - $dataArray['APICallsMadeNumeric'], 0) + max($dataArray['APICallsQuotaOneTimeNumeric'] - $dataArray['APICallsMadeOneTimeNumeric'],0);
//reset quota exceeded flag -> user is allowed to process more images.
if ( $dataArray['APICallsRemaining'] > 0 || $dataArray['Unlimited'])
{
$this->resetQuotaExceeded();
}
else
{
//activate quota limiting
$this->setQuotaExceeded();
}
// Log::addDebug('GetQuotaInformation Result ', $dataArray);
return $dataArray;
}
}

View File

@ -0,0 +1,224 @@
<?php
namespace ShortPixel\Controller;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Model\ResponseModel as ResponseModel;
use ShortPixel\Model\Image\ImageModel as ImageModel;
class ResponseController
{
protected static $items = array();
protected static $queueName; // the current queueName.
protected static $queueType; // the currrent queueType.
protected static $queueMaxTries;
protected static $screenOutput = 1; // see consts down
// Some form of issue keeping
const ISSUE_BACKUP_CREATE = 10; // Issues with backups in ImageModel
const ISSUE_BACKUP_EXISTS = 11;
const ISSUE_OPTIMIZED_NOFILE = 12; // Issues with missing files
const ISSUE_QUEUE_FAILED = 13; // Issues with enqueueing items ( Queue )
const ISSUE_FILE_NOTWRITABLE = 20; // Issues with file writing
const ISSUE_DIRECTORY_NOTWRITABLE = 30; // Issues with directory writing
const ISSUE_API = 50; // Issues with API - general
const ISSUE_QUOTA = 100; // Issues with Quota.
const OUTPUT_MEDIA = 1; // Has context of image, needs simple language
const OUTPUT_BULK = 2;
const OUTPUT_CLI = 3; // Has no context, needs more information
/** Correlates type of item with the queue being used. Be aware that usage *outside* the queue system needs to manually set type
* @param Object QueueObject being used.
*
*/
public static function setQ($q)
{
$queueType = $q->getType();
self::$queueName = $q->getQueueName();
self::$queueType = $queueType;
self::$queueMaxTries = $q->getShortQ()->getOption('retry_limit');
if (! isset(self::$items[$queueType]))
{
self::$items[self::$queueType] = array();
}
}
public static function setOutput($output)
{
self::$screenOutput = $output;
}
public static function getResponseItem($item_id)
{
if (is_null(self::$queueType)) // fail-safe
{
$itemType = "Unknown";
}
else {
$itemType = self::$queueType;
}
if (isset(self::$items[$itemType][$item_id]))
{
$item = self::$items[$itemType][$item_id];
}
else {
$item = new ResponseModel($item_id, $itemType);
}
return $item;
}
protected static function updateResponseItem($item)
{
$itemType = $item->item_type;
self::$items[$itemType][$item->item_id] = $item;
}
// ?
//
public static function addData($item_id, $name, $value = null)
{
if (! is_array($name) && ! is_object($name) )
{
$data = array($name => $value);
}
else {
$data = $name;
}
$item_type = (array_key_exists('item_type', $data)) ? $data['item_type'] : false;
// If no queue / queue type is set, set it if item type is passed to ResponseController. For items outside the queue system.
if ($item_type && is_null(self::$queueType))
{
self::$queueType = $item_type;
}
$resp = self::getResponseItem($item_id); // responseModel
foreach($data as $prop => $val)
{
if (property_exists($resp, $prop))
{
$resp->$prop = $val;
}
else {
}
}
self::updateResponseItem($resp);
}
public static function formatItem($item_id)
{
$item = self::getResponseItem($item_id); // ResponseMOdel
$text = $item->message;
if ($item->is_error)
$text = self::formatErrorItem($item, $text);
else {
$text = self::formatRegularItem($item, $text);
}
return $text;
}
private static function formatErrorItem($item, $text)
{
switch($item->issue_type)
{
case self::ISSUE_BACKUP_CREATE:
if (self::$screenOutput < self::OUTPUT_CLI) // all but cli .
$text .= sprintf(__(' - file %s', 'shortpixel-image-optimiser'), $item->fileName);
break;
}
switch($item->fileStatus)
{
case ImageModel::FILE_STATUS_ERROR:
$text .= sprintf(__('( %s %d ) ', 'shortpixel-image-optimizer'), (strtolower($item->item_type) == 'media') ? __('Attachment ID ') : __('Custom Type '), $item->item_id);
break;
}
switch($item->apiStatus)
{
case ApiController::STATUS_FAIL:
$text .= sprintf(__('( %s %d ) ', 'shortpixel-image-optimizer'), (strtolower($item->item_type) == 'media') ? __('Attachment ID ') : __('Custom Type '), $item->item_id);
break;
}
if (self::$screenOutput == self::OUTPUT_CLI)
{
$text = '(' . self::$queueName . ' : ' . $item->fileName . ') ' . $text . ' ';
}
return $text;
}
private static function formatRegularItem($item, $text)
{
if (! $item->is_done && $item->apiStatus == ApiController::STATUS_UNCHANGED)
{
$text = sprintf(__('Optimizing - waiting for results (%d/%d)','shortpixel-image-optimiser'), $item->images_done, $item->images_total);
}
if (! $item->is_done && $item->apiStatus == ApiController::STATUS_ENQUEUED)
{
$text = sprintf(__('Optimizing - Item has been sent to ShortPixel (%d/%d)','shortpixel-image-optimiser'), $item->images_done, $item->images_total);
}
switch($item->apiStatus)
{
case ApiController::STATUS_SUCCESS:
$text = __('Item successfully optimized', 'shortpixel-image-optimiser');
break;
case ApiController::STATUS_FAIL:
case ApiController::ERR_TIMEOUT:
if (self::$screenOutput < self::OUTPUT_CLI)
{
}
break;
case ApiController::STATUS_NOT_API:
$action = (property_exists($item, 'action')) ? ucfirst($item->action) : __('Action', 'shortpixel-image-optimiser');
$filename = (property_exists($item, 'fileName')) ? $item->fileName : '';
$text = sprintf(__('%s completed for %s'), $action, $item->fileName);
break;
}
if (self::$screenOutput == self::OUTPUT_CLI)
{
$text = '(' . self::$queueName . ' : ' . $item->fileName . ') ' . $text . ' ';
if ($item->tries > 0)
$text .= sprintf(__('(cycle %d)', 'shortpixel-image-optimiser'), intval($item->tries) );
}
return $text;
}
private function responseStrings()
{
}
} // Class

View File

@ -0,0 +1,813 @@
<?php
namespace ShortPixel\Controller;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Notices\NoticeController as Notice;
use ShortPixel\Helper\UiHelper as UiHelper;
use ShortPixel\Helper\UtilHelper as UtilHelper;
use ShortPixel\Helper\InstallHelper as InstallHelper;
use ShortPixel\Model\ApiKeyModel as ApiKeyModel;
use ShortPixel\Model\AccessModel as AccessModel;
use ShortPixel\NextGenController as NextGenController;
class SettingsController extends \ShortPixel\ViewController
{
//env
protected $is_nginx;
protected $is_verifiedkey;
protected $is_htaccess_writable;
protected $is_gd_installed;
protected $is_curl_installed;
protected $is_multisite;
protected $is_mainsite;
protected $is_constant_key;
protected $hide_api_key;
protected $has_nextgen;
protected $do_redirect = false;
protected $disable_heavy_features = false; // if virtual and stateless, might disable heavy file ops.
protected $quotaData = null;
protected $keyModel;
protected $mapper = array(
'key' => 'apiKey',
'cmyk2rgb' => 'CMYKtoRGBconversion',
);
protected $display_part = 'settings';
protected $all_display_parts = array('settings', 'adv-settings', 'cloudflare', 'debug', 'tools');
protected $form_action = 'save-settings';
protected static $instance;
public function __construct()
{
$this->model = \wpSPIO()->settings();
//@todo Streamline this mess. Should run through controller mostly. Risk of desync otherwise.
$keyControl = ApiKeyController::getInstance();
$this->keyModel = $keyControl->getKeyModel(); //new ApiKeyModel();
// $this->keyModel->loadKey();
$this->is_verifiedkey = $this->keyModel->is_verified();
$this->is_constant_key = $this->keyModel->is_constant();
$this->hide_api_key = $this->keyModel->is_hidden();
parent::__construct();
}
// default action of controller
public function load()
{
$this->loadEnv();
$this->checkPost(); // sets up post data
$this->model->redirectedSettings = 2; // Prevents any redirects after loading settings
if ($this->is_form_submit)
{
$this->processSave();
}
$this->load_settings();
}
// this is the nokey form, submitting api key
public function action_addkey()
{
$this->loadEnv();
$this->checkPost();
Log::addDebug('Settings Action - addkey ', array($this->is_form_submit, $this->postData) );
if ($this->is_form_submit && isset($this->postData['apiKey']))
{
$apiKey = $this->postData['apiKey'];
if (strlen(trim($apiKey)) == 0) // display notice when submitting empty API key
{
Notice::addError(sprintf(__("The key you provided has %s characters. The API key should have 20 characters, letters and numbers only.",'shortpixel-image-optimiser'), strlen($apiKey) ));
}
else
{
$this->keyModel->resetTried();
$this->keyModel->checkKey($this->postData['apiKey']);
}
}
$this->doRedirect();
}
public function action_request_new_key()
{
$this->loadEnv();
$this->checkPost();
$email = isset($_POST['pluginemail']) ? trim(sanitize_text_field($_POST['pluginemail'])) : null;
// Not a proper form post.
if (is_null($email))
{
$this->load();
return;
}
// Old code starts here.
if( $this->keyModel->is_verified() === true) {
$this->load(); // already verified?
return;
}
$bodyArgs = array(
'plugin_version' => SHORTPIXEL_IMAGE_OPTIMISER_VERSION,
'email' => $email,
'ip' => isset($_SERVER["HTTP_X_FORWARDED_FOR"]) ? sanitize_text_field($_SERVER["HTTP_X_FORWARDED_FOR"]) : sanitize_text_field($_SERVER['REMOTE_ADDR']),
);
$affl_id = false;
$affl_id = (defined('SHORTPIXEL_AFFILIATE_ID')) ? SHORTPIXEL_AFFILIATE_ID : false;
$affl_id = apply_filters('shortpixel/settings/affiliate', $affl_id); // /af/bla35
if ($affl_id !== false)
{
$bodyArgs['affiliate'] = $affl_id;
}
$params = array(
'method' => 'POST',
'timeout' => 10,
'redirection' => 5,
'httpversion' => '1.0',
'blocking' => true,
'sslverify' => false,
'headers' => array(),
'body' => $bodyArgs,
);
$newKeyResponse = wp_remote_post("https://shortpixel.com/free-sign-up-plugin", $params);
$errorText = __("There was problem requesting a new code. Server response: ", 'shortpixel-image-optimiser');
if ( is_object($newKeyResponse) && get_class($newKeyResponse) == 'WP_Error' ) {
//die(json_encode((object)array('Status' => 'fail', 'Details' => '503')));
Notice::addError($errorText . $newKeyResponse->get_error_message() );
$this->doRedirect(); // directly redirect because other data / array is not set.
}
elseif ( isset($newKeyResponse['response']['code']) && $newKeyResponse['response']['code'] <> 200 ) {
//die(json_encode((object)array('Status' => 'fail', 'Details' =>
Notice::addError($errorText . $newKeyResponse['response']['code']);
$this->doRedirect(); // strange http status, redirect with error.
}
$body = $newKeyResponse['body'];
$body = json_decode($body);
if($body->Status == 'success') {
$key = trim($body->Details);
$valid = $this->keyModel->checkKey($key);
//$validityData = $this->getQuotaInformation($key, true, true);
if($valid === true) {
\ShortPixel\Controller\AdminNoticesController::resetAPINotices();
/* Notice::addSuccess(__('Great, you successfully claimed your API Key! Please take a few moments to review the plugin settings below before starting to optimize your images.','shortpixel-image-optimiser')); */
}
}
elseif($body->Status == 'existing')
{
Notice::addWarning( sprintf(__('This email address is already in use. Please use your API-key in the "Already have an API key" field. You can obtain your license key via %s your account %s ', 'shortpixel-image-optimiser'), '<a href="https://shortpixel.com/login/">', '</a>') );
}
else
{
Notice::addError( __('Unexpected error obtaining the ShortPixel key. Please contact support about this:', 'shortpixel-image-optimiser') . ' ' . json_encode($body) );
}
$this->doRedirect();
}
public function action_debug_redirectBulk()
{
$this->checkPost();
OptimizeController::resetQueues();
$action = isset($_REQUEST['bulk']) ? sanitize_text_field($_REQUEST['bulk']) : null;
if ($action == 'migrate')
{
$this->doRedirect('bulk-migrate');
}
if ($action == 'restore')
{
$this->doRedirect('bulk-restore');
}
if ($action == 'removeLegacy')
{
$this->doRedirect('bulk-removeLegacy');
}
}
/** Button in part-debug, routed via custom Action */
public function action_debug_resetStats()
{
$this->loadEnv();
$this->checkPost();
$statsController = StatsController::getInstance();
$statsController->reset();
$this->doRedirect();
}
public function action_debug_resetquota()
{
$this->loadEnv();
$this->checkPost();
$quotaController = QuotaController::getInstance();
$quotaController->forceCheckRemoteQuota();
$this->doRedirect();
}
public function action_debug_resetNotices()
{
$this->loadEnv();
$this->checkPost();
Notice::resetNotices();
$nControl = new Notice(); // trigger reload.
$this->doRedirect();
}
public function action_debug_triggerNotice()
{
$this->checkPost();
$key = isset($_REQUEST['notice_constant']) ? sanitize_text_field($_REQUEST['notice_constant']) : false;
if ($key !== false)
{
$adminNoticesController = AdminNoticesController::getInstance();
if ($key == 'trigger-all')
{
$notices = $adminNoticesController->getAllNotices();
foreach($notices as $noticeObj)
{
$noticeObj->addManual();
}
}
else
{
$model = $adminNoticesController->getNoticeByKey($key);
if ($model)
$model->addManual();
}
}
$this->doRedirect();
}
public function action_debug_resetQueue()
{
$queue = isset($_REQUEST['queue']) ? sanitize_text_field($_REQUEST['queue']) : null;
$this->loadEnv();
$this->checkPost();
if (! is_null($queue))
{
$opt = new OptimizeController();
$statsMedia = $opt->getQueue('media');
$statsCustom = $opt->getQueue('custom');
$opt->setBulk(true);
$bulkMedia = $opt->getQueue('media');
$bulkCustom = $opt->getQueue('custom');
$queues = array('media' => $statsMedia, 'custom' => $statsCustom, 'mediaBulk' => $bulkMedia, 'customBulk' => $bulkCustom);
if ( strtolower($queue) == 'all')
{
foreach($queues as $q)
{
$q->resetQueue();
}
}
else
{
$queues[$queue]->resetQueue();
}
if ($queue == 'all')
{
$message = sprintf(__('All items in the queues have been removed and the process is stopped', 'shortpixel-image-optimiser'));
}
else
{
$message = sprintf(__('All items in the %s queue have been removed and the process is stopped', 'shortpixel-image-optimiser'), $queue);
}
Notice::addSuccess($message);
}
$this->doRedirect();
}
public function action_debug_removePrevented()
{
$this->loadEnv();
$this->checkPost();
global $wpdb;
$sql = 'delete from ' . $wpdb->postmeta . ' where meta_key = %s';
$sql = $wpdb->prepare($sql, '_shortpixel_prevent_optimize');
$wpdb->query($sql);
$message = __('Item blocks have been removed. It is recommended to create a backup before trying to optimize image.', 'shortpixel-image-optimiser');
Notice::addSuccess($message);
$this->doRedirect();
}
public function action_debug_removeProcessorKey()
{
//$this->loadEnv();
$this->checkPost();
$cacheControl = new CacheController();
$cacheControl->deleteItem('bulk-secret');
exit('reloading settings would cause processorKey to be set again');
}
public function processSave()
{
// Split this in the several screens. I.e. settings, advanced, Key Request IF etc.
if (isset($this->postData['includeNextGen']) && $this->postData['includeNextGen'] == 1)
{
$nextgen = NextGenController::getInstance();
$previous = $this->model->includeNextGen;
$nextgen->enableNextGen(true);
// Reset any integration notices when updating settings.
AdminNoticesController::resetIntegrationNotices();
}
$check_key = false;
if (isset($this->postData['apiKey']))
{
$check_key = $this->postData['apiKey'];
unset($this->postData['apiKey']); // unset, since keyModel does the saving.
}
// If the compression type setting changes, remove all queued items to prevent further optimizing with a wrong type.
if (intval($this->postData['compressionType']) !== intval($this->model->compressionType))
{
OptimizeController::resetQueues();
}
// write checked and verified post data to model. With normal models, this should just be call to update() function
foreach($this->postData as $name => $value)
{
$this->model->{$name} = $value;
}
// first save all other settings ( like http credentials etc ), then check
if (! $this->keyModel->is_constant() && $check_key !== false) // don't allow settings key if there is a constant
{
$this->keyModel->resetTried(); // reset the tried api keys on a specific post request.
$this->keyModel->checkKey($check_key);
}
// Every save, force load the quota. One reason, because of the HTTP Auth settings refresh.
$this->loadQuotaData(true);
// end
if ($this->do_redirect)
$this->doRedirect('bulk');
else {
$noticeController = Notice::getInstance();
$notice = Notice::addSuccess(__('Settings Saved', 'shortpixel-image-optimiser'));
$notice->is_removable = false;
$noticeController->update();
$this->doRedirect();
}
}
/* Loads the view data and the view */
public function load_settings()
{
if ($this->is_verifiedkey) // supress quotaData alerts when handing unset API's.
$this->loadQuotaData();
else
InstallHelper::checkTables();
$keyController = ApiKeyController::getInstance();
$this->view->data = (Object) $this->model->getData();
$this->view->data->apiKey = $keyController->getKeyForDisplay();
$this->loadStatistics();
$this->checkCloudFlare();
$statsControl = StatsController::getInstance();
$this->view->minSizes = $this->getMaxIntermediateImageSize();
$excludeOptions = UtilHelper::getWordPressImageSizes();
$mainOptions = array(
'shortpixel_main_donotuse' => array('nice-name' => __('Main Image', 'shortpixel-image-optimiser')),
'shortpixel_original_donotuse' => array('nice-name' => __('Original Image', 'shortpixel-image-optimiser')),
);
$excludeOptions = array_merge($mainOptions, $excludeOptions);
$this->view->allThumbSizes = $excludeOptions;
$this->view->averageCompression = $statsControl->getAverageCompression();
$this->view->savedBandwidth = UiHelper::formatBytes( intval($this->view->data->savedSpace) * 10000,2);
$this->view->cloudflare_constant = defined('SHORTPIXEL_CFTOKEN') ? true : false;
$settings = \wpSPIO()->settings();
if ($this->view->data->createAvif == 1)
$this->avifServerCheck();
$this->loadView('view-settings');
}
protected function avifServerCheck()
{
$noticeControl = AdminNoticesController::getInstance();
$notice = $noticeControl->getNoticeByKey('MSG_AVIF_ERROR');
$notice->check();
}
protected function loadStatistics()
{
/*
$statsControl = StatsController::getInstance();
$stats = new \stdClass;
$stats->totalOptimized = $statsControl->find('totalOptimized');
$stats->totalOriginal = $statsControl->find('totalOriginal');
$stats->mainOptimized = $statsControl->find('media', 'images');
// used in part-g eneral
$stats->thumbnailsToProcess = $statsControl->thumbNailsToOptimize(); // $totalImages - $totalOptimized;
// $stats->totalFiles = $statsControl->find('media', '')
$this->view->stats = $stats;
*/
}
/** @todo Remove this check in Version 5.1 including all data on the old CF token */
protected function checkCloudFlare()
{
$settings = \wpSPIO()->settings();
$authkey = $settings->cloudflareAuthKey;
$this->view->hide_cf_global = true;
if (strlen($authkey) > 0)
{
$message = '<h3> ' . __('Cloudflare', 'shortpixel-image-optimiser') . '</h3>';
$message .= '<p>' . __('It appears that you are using the Cloudflare Global API key. As it is not as safe as the Cloudflare Token, it will be removed in the next version. Please, switch to the token.', 'shortpixel-image-optimiser') . '</p>';
$message .= '<p>' . sprintf(__('%s How to set up the Cloudflare Token %s', 'shortpixel-image-optimiser'), '<a href="https://shortpixel.com/knowledge-base/article/325-using-shortpixel-image-optimizer-with-cloudflare-api-token" target="_blank">', '</a>') . '</p>';
Notice::addNormal($message);
$this->view->hide_cf_global = false;
}
}
/** Checks on things and set them for information. */
protected function loadEnv()
{
$env = wpSPIO()->env();
$this->is_nginx = $env->is_nginx;
$this->is_gd_installed = $env->is_gd_installed;
$this->is_curl_installed = $env->is_curl_installed;
$this->is_htaccess_writable = $this->HTisWritable();
$this->is_multisite = $env->is_multisite;
$this->is_mainsite = $env->is_mainsite;
$this->has_nextgen = $env->has_nextgen;
$this->disable_heavy_features = (\wpSPIO()->env()->hasOffload() && false === \wpSPIO()->env()->useVirtualHeavyFunctions()) ? true : false;
$this->display_part = (isset($_GET['part']) && in_array($_GET['part'], $this->all_display_parts) ) ? sanitize_text_field($_GET['part']) : 'settings';
}
/* Temporary function to check if HTaccess is writable.
* HTaccess is writable if it exists *and* is_writable, or can be written if directory is writable.
*/
private function HTisWritable()
{
if ($this->is_nginx)
return false;
$file = \wpSPIO()->filesystem()->getFile(get_home_path() . '.htaccess');
if ($file->is_writable())
{
return true;
}
return false;
}
protected function getMaxIntermediateImageSize() {
global $_wp_additional_image_sizes;
$width = 0;
$height = 0;
$get_intermediate_image_sizes = get_intermediate_image_sizes();
// Create the full array with sizes and crop info
if(is_array($get_intermediate_image_sizes)) foreach( $get_intermediate_image_sizes as $_size ) {
if ( in_array( $_size, array( 'thumbnail', 'medium', 'large' ) ) ) {
$width = max($width, get_option( $_size . '_size_w' ));
$height = max($height, get_option( $_size . '_size_h' ));
//$sizes[ $_size ]['crop'] = (bool) get_option( $_size . '_crop' );
} elseif ( isset( $_wp_additional_image_sizes[ $_size ] ) ) {
$width = max($width, $_wp_additional_image_sizes[ $_size ]['width']);
$height = max($height, $_wp_additional_image_sizes[ $_size ]['height']);
//'crop' => $_wp_additional_image_sizes[ $_size ]['crop']
}
}
return array('width' => max(100, $width), 'height' => max(100, $height));
}
// @param Force. needed on settings save because it sends off the HTTP Auth
protected function loadQuotaData($force = false)
{
$quotaController = QuotaController::getInstance();
if ($force === true)
{
$quotaController->forceCheckRemoteQuota();
$this->quotaData = null;
}
if (is_null($this->quotaData))
$this->quotaData = $quotaController->getQuota(); //$this->shortPixel->checkQuotaAndAlert();
$quotaData = $this->quotaData;
$remainingImages = $quotaData->total->remaining; // $quotaData['APICallsRemaining'];
$remainingImages = ( $remainingImages < 0 ) ? 0 : $this->formatNumber($remainingImages, 0);
$this->view->remainingImages = $remainingImages;
}
// This is done before handing it off to the parent controller, to sanitize and check against model.
protected function processPostData($post)
{
if (isset($post['display_part']) && strlen($post['display_part']) > 0)
{
$this->display_part = sanitize_text_field($post['display_part']);
}
unset($post['display_part']);
// analyse the save button
if (isset($post['save_bulk']))
{
$this->do_redirect = true;
}
unset($post['save_bulk']);
unset($post['save']);
// handle 'reverse' checkbox.
$keepExif = isset($post['removeExif']) ? 0 : 1;
$post['keepExif'] = $keepExif;
unset($post['removeExif']);
// checkbox overloading
$png2jpg = (isset($post['png2jpg']) ? (isset($post['png2jpgForce']) ? 2 : 1): 0);
$post['png2jpg'] = $png2jpg;
unset($post['png2jpgForce']);
// must be an array
$post['excludeSizes'] = (isset($post['excludeSizes']) && is_array($post['excludeSizes']) ? $post['excludeSizes']: array());
$post = $this->processWebp($post);
$post = $this->processExcludeFolders($post);
$post = $this->processCloudFlare($post);
parent::processPostData($post);
}
/** Function for the WebP settings overload
*
*/
protected function processWebP($post)
{
$deliverwebp = 0;
if (! $this->is_nginx)
UtilHelper::alterHtaccess(false, false); // always remove the statements.
$webpOn = isset($post['createWebp']) && $post['createWebp'] == 1;
$avifOn = isset($post['createAvif']) && $post['createAvif'] == 1;
if (isset($post['deliverWebp']) && $post['deliverWebp'] == 1)
{
$type = isset($post['deliverWebpType']) ? $post['deliverWebpType'] : '';
$altering = isset($post['deliverWebpAlteringType']) ? $post['deliverWebpAlteringType'] : '';
if ($type == 'deliverWebpAltered')
{
if ($altering == 'deliverWebpAlteredWP')
{
$deliverwebp = 2;
}
elseif($altering = 'deliverWebpAlteredGlobal')
{
$deliverwebp = 1;
}
}
elseif ($type == 'deliverWebpUnaltered') {
$deliverwebp = 3;
}
}
if (! $this->is_nginx && $deliverwebp == 3) // deliver webp/avif via htaccess, write rules
{
UtilHelper::alterHtaccess(true, true);
}
$post['deliverWebp'] = $deliverwebp;
unset($post['deliverWebpAlteringType']);
unset($post['deliverWebpType']);
return $post;
}
protected function processExcludeFolders($post)
{
$patterns = array();
if (false === isset($post['exclusions']))
{
return $post;
}
$exclusions = $post['exclusions'];
$accepted = array();
foreach($exclusions as $index => $exclusions)
{
$accepted[] = json_decode(html_entity_decode( stripslashes($exclusions)), true);
}
foreach($accepted as $index => $pair)
{
$pattern = $pair['value'];
$type = $pair['type'];
//$first = substr($pattern, 0,1);
if ($type == 'regex-name' || $type == 'regex-path')
{
if ( @preg_match($pattern, false) === false)
{
$accepted[$index]['has-error'] = true;
Notice::addWarning(sprintf(__('Regular Expression Pattern %s returned an error. Please check if the expression is correct. %s * Special characters should be escaped. %s * A regular expression must be contained between two slashes ', 'shortpixel-image-optimiser'), $pattern, "<br>", "<br>" ));
}
}
}
$post['excludePatterns'] = $accepted;
return $post; // @todo The switch to check regex patterns or not.
if(isset($post['excludePatterns']) && strlen($post['excludePatterns'])) {
$items = explode(',', $post['excludePatterns']);
foreach($items as $pat) {
$parts = explode(':', $pat);
if (count($parts) == 1)
{
$type = 'name';
$value = str_replace('\\\\','\\', trim($parts[0]));
}
else
{
$type = trim($parts[0]);
$value = str_replace('\\\\','\\',trim($parts[1]));
}
if (strlen($value) > 0) // omit faulty empty statements.
$patterns[] = array('type' => $type, 'value' => $value);
}
}
foreach($patterns as $pair)
{
$pattern = $pair['value'];
//$first = substr($pattern, 0,1);
if ($type == 'regex-name' || $type == 'regex-path')
{
if ( @preg_match($pattern, false) === false)
{
Notice::addWarning(sprintf(__('Regular Expression Pattern %s returned an error. Please check if the expression is correct. %s * Special characters should be escaped. %s * A regular expression must be contained between two slashes ', 'shortpixel-image-optimiser'), $pattern, "<br>", "<br>" ));
}
}
}
$post['excludePatterns'] = $patterns;
return $post;
}
protected function processCloudFlare($post)
{
if (isset($post['cf_auth_switch']) && $post['cf_auth_switch'] == 'token')
{
if (isset($post['cloudflareAuthKey']))
unset($post['cloudflareAuthKey']);
if (isset($post['cloudflareEmail']))
unset($post['cloudflareEmail']);
}
elseif (isset($post['cloudflareAuthKey']) && $post['cf_auth_switch'] == 'global')
{
if (isset($post['cloudflareToken']))
unset($post['cloudflareToken']);
}
return $post;
}
protected function doRedirect($redirect = 'self')
{
if ($redirect == 'self')
{
$url = esc_url_raw(add_query_arg('part', $this->display_part));
$url = remove_query_arg('noheader', $url); // has url
$url = remove_query_arg('sp-action', $url); // has url
}
elseif($redirect == 'bulk')
{
$url = admin_url("upload.php?page=wp-short-pixel-bulk");
}
elseif($redirect == 'bulk-migrate')
{
$url = admin_url('upload.php?page=wp-short-pixel-bulk&panel=bulk-migrate');
}
elseif ($redirect == 'bulk-restore')
{
$url = admin_url('upload.php?page=wp-short-pixel-bulk&panel=bulk-restore');
}
elseif ($redirect == 'bulk-removeLegacy')
{
$url = admin_url('upload.php?page=wp-short-pixel-bulk&panel=bulk-removeLegacy');
}
wp_redirect($url);
exit();
}
}

View File

@ -0,0 +1,131 @@
<?php
namespace ShortPixel\Controller;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Model\StatsModel as StatsModel;
use ShortPixel\Controller\Queue\StatsQueue as StatsQueue;
class StatsController extends \ShortPixel\Controller
{
protected $model;
protected $queue;
protected static $instance;
protected $stats = array(
// 'processed'
);
public function __construct()
{
$this->model = new StatsModel();
}
public static function getInstance()
{
if (is_null(self::$instance))
self::$instance = new StatsController();
return self::$instance;
}
public function find(... $params)
{
if (count($params) == 1)
{
$stat = $this->model->get($params[0]); // check if stat is simple property
if (! is_null($stat) )
{
return $stat;
}
}
$stat = $this->model->getStat(array_shift($params));
for($i = 0; $i < count($params); $i++)
{
$stat = $stat->grab($params[$i]);
}
if (is_object($stat)) // failed to get statistic.
{
Log::addWarn('Statistics for this path failed', $params );
return 0;
}
else
return $stat;
}
public function reset()
{
$this->model->reset();
}
public function getAverageCompression()
{
$totalOptimized = $this->model->get('totalOptimized');
$totalOriginal = $this->model->get('totalOriginal');
$average = 0;
if ($totalOptimized > 0 && $totalOriginal > 0)
{
$average = round(( 1 - ( $totalOptimized / $totalOriginal ) ) * 100, 2);
}
return $average;
}
// This is not functional @todo
public function addImage($stats)
{
$stats->type = 'media';
$stats->compression = 'lossy';
$stats->images = 6;
$stats->items = 1;
$stats->timestamp = 0;
$this->model->add($stats);
}
/** This is a different calculation since the thumbs and totals are products of a database query without taking into account optimizable, excluded thumbs etc. This is a performance thing */
public function thumbNailsToOptimize()
{
$totalThumbs = $this->find('media',
'thumbsTotal'); // according to database.
$totalThumbsOptimized = $this->find('media', 'thumbs');
$excludedThumbnails = \wpSPIO()->settings()->excludeSizes;
$excludeCount = (is_array($excludedThumbnails)) ? count($excludedThumbnails) : 0;
// Totalthumbs - thumbsOptimized - minus amount of excluded (guess)
$toOptimize = $totalThumbs - $totalThumbsOptimized - ($this->find('media', 'items') * $excludeCount);
return $toOptimize;
}
/** This count all possible optimizable images (approx). Not checking settings like excludesizes / webp / original images etc. More fine-grained approx in BulkViewController */
public function totalImagesToOptimize()
{
$totalImagesOptimized = $this->find('total', 'images');
$totalImages = $this->find('total', 'itemsTotal') + $this->find('total', 'thumbsTotal');
$toOpt = $totalImages - $totalImagesOptimized;
return $toOpt;
}
} // class

View File

@ -0,0 +1,260 @@
<?php
namespace ShortPixel\Controller\View;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Notices\NoticeController as Notices;
use ShortPixel\Controller\AdminNoticesController as AdminNoticesController;
use ShortPixel\Controller\ApiKeyController as ApiKeyController;
use ShortPixel\Controller\QuotaController as QuotaController;
use ShortPixel\Controller\OptimizeController as OptimizeController;
use ShortPixel\Controller\BulkController as BulkController;
use ShortPixel\Controller\StatsController as StatsController;
use ShortPixel\Controller\OtherMediaController as OtherMediaController;
use ShortPixel\Helper\UiHelper as UiHelper;
use ShortPixel\Model\AccessModel as AccessModel;
class BulkViewController extends \ShortPixel\ViewController
{
protected $form_action = 'sp-bulk';
protected $template = 'view-bulk';
protected $quotaData;
protected $pendingMeta;
protected $selected_folders = array();
protected static $instance;
public function load()
{
$quota = QuotaController::getInstance();
$optimizeController = new OptimizeController();
$this->view->quotaData = $quota->getQuota();
$this->view->stats = $optimizeController->getStartupData();
$this->view->approx = $this->getApproxData();
$this->view->logHeaders = array(__('Images', 'shortpixel_image_optimiser'), __('Errors', 'shortpixel_image_optimizer'), __('Date', 'shortpixel_image_optimizer'));
$this->view->logs = $this->getLogs();
$keyControl = ApiKeyController::getInstance();
$this->view->error = false;
if ( ! $keyControl->keyIsVerified() )
{
$adminNoticesController = AdminNoticesController::getInstance();
$this->view->error = true;
$this->view->errorTitle = __('Missing API Key', 'shortpixel_image_optimiser');
$this->view->errorContent = $this->getActivationNotice();
$this->view->showError = 'key';
}
elseif ( ! $quota->hasQuota())
{
$this->view->error = true;
$this->view->errorTitle = __('Quota Exceeded','shortpixel-image-optimiser');
$this->view->errorContent = __('Can\'t start the Bulk Process due to lack of credits.', 'shortpixel-image-optimiser');
$this->view->errorText = __('Please check or add quota and refresh the page', 'shortpixel-image-optimiser');
$this->view->showError = 'quota';
}
$this->view->mediaErrorLog = $this->loadCurrentLog('media');
$this->view->customErrorLog = $this->loadCurrentLog('custom');
$this->view->buyMoreHref = 'https://shortpixel.com/' . ($keyControl->getKeyForDisplay() ? 'login/' . $keyControl->getKeyForDisplay() . '/spio-unlimited' : 'pricing');
$this->loadView();
}
// Double with ApiNotice . @todo Fix.
protected function getActivationNotice()
{
$message = "<p>" . __('In order to start the optimization process, you need to validate your API Key in the '
. '<a href="options-general.php?page=wp-shortpixel-settings">ShortPixel Settings</a> page in your WordPress Admin.','shortpixel-image-optimiser') . "
</p>
<p>" . __('If you dont have an API Key, just fill out the form and a key will be created.','shortpixel-image-optimiser') . "</p>";
return $message;
}
protected function getApproxData()
{
$otherMediaController = OtherMediaController::getInstance();
$approx = new \stdClass; // guesses on basis of the statsController SQL.
$approx->media = new \stdClass;
$approx->custom = new \stdClass;
$approx->total = new \stdClass;
$sc = StatsController::getInstance();
$sc->reset(); // Get a fresh stat.
$excludeSizes = \wpSPIO()->settings()->excludeSizes;
$approx->media->items = $sc->find('media', 'itemsTotal') - $sc->find('media', 'items');
// ThumbsTotal - Approx thumbs in installation - Approx optimized thumbs (same query)
$approx->media->thumbs = $sc->find('media', 'thumbsTotal') - $sc->find('media', 'thumbs');
// If sizes are excluded, remove this count from the approx.
if (is_array($excludeSizes) && count($excludeSizes) > 0)
$approx->media->thumbs = $approx->media->thumbs - ($approx->media->items * count($excludeSizes));
// Total optimized items + Total optimized (approx) thumbnails
$approx->media->total = $approx->media->items + $approx->media->thumbs;
$approx->custom->images = $sc->find('custom', 'itemsTotal') - $sc->find('custom', 'items');
$approx->custom->has_custom = $otherMediaController->hasCustomImages();
$approx->total->images = $approx->media->total + $approx->custom->images; // $sc->totalImagesToOptimize();
$approx->media->isLimited = $sc->find('media', 'isLimited');
// Prevent any guesses to go below zero.
foreach($approx->media as $item => $value)
{
if (is_numeric($value))
$approx->media->$item = max($value, 0);
}
foreach($approx->total as $item => $value)
{
if (is_numeric($value))
$approx->total->$item = max($value, 0);
}
return $approx;
}
/* Function to check for and load the current Log. This can be present on load time when the bulk page is refreshed during operations.
* Reload the past error and display them in the error box.
* @param String $type media or custom
*/
protected function loadCurrentLog($type = 'media')
{
$bulkController = BulkController::getInstance();
$log = $bulkController->getLog('current_bulk_' . $type . '.log');
if ($log == false)
return false;
$content = $log->getContents();
$lines = array_filter(explode(';', $content));
$output = '';
foreach ($lines as $line)
{
$cells = array_filter(explode('|', $line));
if (count($cells) == 1)
continue; // empty line.
$date = $filename = $message = $item_id = false;
$date = $cells[0];
$filename = isset($cells[1]) ? $cells[1] : false;
$item_id = isset($cells[2]) ? $cells[2] : false;
$message = isset($cells[3]) ? $cells[3] : false;
$kblink = UIHelper::getKBSearchLink($message);
$kbinfo = '<span class="kbinfo"><a href="' . $kblink . '" target="_blank" ><span class="dashicons dashicons-editor-help">&nbsp;</span></a></span>';
$output .= '<div class="fatal">';
$output .= $date . ': ';
if ($message)
$output .= $message;
if ($filename)
$output .= ' ( '. __('in file ','shortpixel-image-optimiser') . ' ' . $filename . ' ) ' . $kbinfo;
$output .= '</div>';
}
return $output;
}
public function getLogs()
{
$bulkController = BulkController::getInstance();
$logs = $bulkController->getLogs();
$fs = \wpSPIO()->filesystem();
$backupDir = $fs->getDirectory(SHORTPIXEL_BACKUP_FOLDER);
$view = array();
foreach($logs as $logData)
{
$logFile = $fs->getFile($backupDir->getPath() . 'bulk_' . $logData['type'] . '_' . $logData['date'] . '.log');
$errors = $logData['fatal_errors'];
if ($logFile->exists())
{
$errors = '<a data-action="OpenLog" data-file="' . $logFile->getFileName() . '" href="' . $fs->pathToUrl($logFile) . '">' . $errors . '</a>';
}
$op = (isset($logData['operation'])) ? $logData['operation'] : false;
// BulkName is just to compile a user-friendly name for the operation log.
$bulkName = '';
switch($logData['type'])
{
case 'custom':
$bulkName = __('Custom Media Bulk', 'shortpixel-image-optimiser');
break;
case 'media':
$bulkName = __('Media Library Bulk', 'shortpixel-image-optimiser');
break;
}
$bulkName .= ' '; // add a space.
switch($op)
{
case 'bulk-restore':
$bulkName .= __('Restore', 'shortpixel-image-optimiser');
break;
case 'migrate':
$bulkName .= __('Migrate old Metadata', 'shortpixel-image-optimiser');
break;
case 'removeLegacy':
$bulkName = __('Remove Legacy Data', 'shortpixel-image-optimiser');
break;
default:
$bulkName .= __('Optimization', 'shortpixel-image-optimiser');
break;
}
$images = isset($logData['total_images']) ? $logData['total_images'] : $logData['processed'];
$view[] = array('type' => $logData['type'], 'images' => $images, 'errors' => $errors, 'date' => UiHelper::formatTS($logData['date']), 'operation' => $op, 'bulkName' => $bulkName);
}
krsort($view);
return $view;
}
} // class

View File

@ -0,0 +1,352 @@
<?php
namespace ShortPixel\Controller\View;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Helper\UiHelper as UiHelper;
use ShortPixel\Helper\UtilHelper as UtilHelper;
use ShortPixel\Controller\OptimizeController as OptimizeController;
use ShortPixel\Controller\ErrorController as ErrorController;
use ShortPixel\Model\File\FileModel as FileModel;
use ShortPixel\Helper\DownloadHelper as DownloadHelper;
// Future contoller for the edit media metabox view.
class EditMediaViewController extends \ShortPixel\ViewController
{
protected $template = 'view-edit-media';
// protected $model = 'image';
protected $post_id;
protected $legacyViewObj;
protected $imageModel;
protected $hooked;
protected static $instance;
public function __construct()
{
parent::__construct();
}
protected function loadHooks()
{
add_action( 'add_meta_boxes_attachment', array( $this, 'addMetaBox') );
$this->hooked = true;
}
public function load()
{
if (! $this->hooked)
$this->loadHooks();
$fs = \wpSPIO()->filesystem();
$fs->startTrustedMode();
}
public function addMetaBox()
{
add_meta_box(
'shortpixel_info_box', // this is HTML id of the box on edit screen
__('ShortPixel Info', 'shortpixel-image-optimiser'), // title of the box
array( $this, 'doMetaBox'), // function to be called to display the info
null,//, // on which edit screen the box should appear
'side'//'normal', // part of page where the box should appear
//'default' // priority of the box
);
}
public function dometaBox($post)
{
$this->post_id = $post->ID;
$this->view->debugInfo = array();
$this->view->id = $this->post_id;
$this->view->list_actions = '';
$fs = \wpSPIO()->filesystem();
$this->imageModel = $fs->getMediaImage($this->post_id);
// Asking for something non-existing.
if ($this->imageModel === false)
{
$this->view->status_message = __('File Error. This could be not an image or the file is missing', 'shortpixel-image-optimiser');
$this->loadView();
return false;
}
$this->view->status_message = null;
$this->view->text = UiHelper::getStatusText($this->imageModel);
$this->view->list_actions = UiHelper::getListActions($this->imageModel);
if ( count($this->view->list_actions) > 0)
$this->view->list_actions = UiHelper::renderBurgerList($this->view->list_actions, $this->imageModel);
else
$this->view->list_actions = '';
//$this->imageModel->cancelUserExclusions();
$this->view->actions = UiHelper::getActions($this->imageModel);
$this->view->stats = $this->getStatistics();
if (! $this->userIsAllowed)
{
$this->view->actions = array();
$this->view->list_actions = '';
}
if(true === \wpSPIO()->env()->is_debug )
{
$this->view->debugInfo = $this->getDebugInfo();
}
$this->loadView();
}
protected function getStatusMessage()
{
return UIHelper::renderSuccessText($this->imageModel);
}
protected function getStatistics()
{
//$data = $this->data;
$stats = array();
$imageObj = $this->imageModel;
$did_keepExif = $imageObj->getMeta('did_keepExif');
$did_convert = $imageObj->getMeta()->convertMeta()->isConverted();
$resize = $imageObj->getMeta('resize');
// Not optimized, not data.
if (! $imageObj->isOptimized())
return array();
if ($did_keepExif)
$stats[] = array(__('EXIF kept', 'shortpixel-image-optimiser'), '');
elseif ( $did_keepExif === false) {
$stats[] = array(__('EXIF removed', 'shortpixel-image-optimiser'), '');
}
if (true === $did_convert )
{
$ext = $imageObj->getMeta()->convertMeta()->getFileFormat();
$stats[] = array( sprintf(__('Converted from %s','shortpixel-image-optimiser'), $ext), '');
}
elseif (false !== $imageObj->getMeta()->convertMeta()->didTry()) {
$ext = $imageObj->getMeta()->convertMeta()->getFileFormat();
$error = $imageObj->getMeta()->convertMeta()->getError(); // error code.
$stats[] = array(UiHelper::getConvertErrorReason($error, $ext), '');
}
if ($resize == true)
{
$from = $imageObj->getMeta('originalWidth') . 'x' . $imageObj->getMeta('originalHeight');
$to = $imageObj->getMeta('resizeWidth') . 'x' . $imageObj->getMeta('resizeHeight');
$type = ($imageObj->getMeta('resizeType') !== null) ? '(' . $imageObj->getMeta('resizeType') . ')' : '';
$stats[] = array(sprintf(__('Resized %s %s to %s'), $type, $from, $to), '');
}
$tsOptimized = $imageObj->getMeta('tsOptimized');
if ($tsOptimized !== null)
$stats[] = array(__("Optimized on :", 'shortpixel-image-optimiser') . "<br /> ", UiHelper::formatTS($tsOptimized) );
if ($imageObj->isOptimized())
{
$stats[] = array( sprintf(__('%s %s Read more about theses stats %s ', 'shortpixel-image-optimiser'), '
<p><img alt=' . esc_html('Info Icon', 'shortpixel-image-optimiser') . ' src=' . esc_url( wpSPIO()->plugin_url('res/img/info-icon.png' )) . ' style="margin-bottom: -4px;"/>', '<a href="https://shortpixel.com/knowledge-base/article/553-the-stats-from-the-shortpixel-column-in-the-media-library-explained" target="_blank">', '</a></p>'), '');
}
return $stats;
}
protected function getDebugInfo()
{
if(! \wpSPIO()->env()->is_debug )
{
return array();
}
$meta = \wp_get_attachment_metadata($this->post_id);
$fs = \wpSPIO()->filesystem();
$imageObj = $this->imageModel;
if ($imageObj->isProcessable())
{
$optimizeData = $imageObj->getOptimizeData();
$urls = $optimizeData['urls'];
}
$thumbnails = $imageObj->get('thumbnails');
$processable = ($imageObj->isProcessable()) ? '<span class="green">Yes</span>' : '<span class="red">No</span> (' . $imageObj->getReason('processable') . ')';
$anyFileType = ($imageObj->isProcessableAnyFileType()) ? '<span class="green">Yes</span>' : '<span class="red">No</span>';
$restorable = ($imageObj->isRestorable()) ? '<span class="green">Yes</span>' : '<span class="red">No</span> (' . $imageObj->getReason('restorable') . ')';
$hasrecord = ($imageObj->hasDBRecord()) ? '<span class="green">Yes</span>' : '<span class="red">No</span> ';
$debugInfo = array();
$debugInfo[] = array(__('URL (get attachment URL)', 'shortpixel_image_optiser'), wp_get_attachment_url($this->post_id));
$debugInfo[] = array(__('File (get attached)'), get_attached_file($this->post_id));
if ($imageObj->is_virtual())
{
$virtual = $imageObj->get('virtual_status');
if($virtual == FileModel::$VIRTUAL_REMOTE)
$vtext = 'Remote';
elseif($virtual == FileModel::$VIRTUAL_STATELESS)
$vtext = 'Stateless';
else
$vtext = 'Not set';
$debugInfo[] = array(__('Is Virtual: ') . $vtext, $imageObj->getFullPath() );
}
$debugInfo[] = array(__('Size and Mime (ImageObj)'), $imageObj->get('width') . 'x' . $imageObj->get('height'). ' (' . $imageObj->get('mime') . ')');
$debugInfo[] = array(__('Status (ShortPixel)'), $imageObj->getMeta('status') . ' ' );
$debugInfo[] = array(__('Processable'), $processable);
$debugInfo[] = array(__('Avif/Webp needed'), $anyFileType);
$debugInfo[] = array(__('Restorable'), $restorable);
$debugInfo[] = array(__('Record'), $hasrecord);
if ($imageObj->getMeta()->convertMeta()->didTry())
{
$debugInfo[] = array(__('Converted'), ($imageObj->getMeta()->convertMeta()->isConverted() ?'<span class="green">Yes</span>' : '<span class="red">No</span> '));
$debugInfo[] = array(__('Checksum'), $imageObj->getMeta()->convertMeta()->didTry());
$debugInfo[] = array(__('Error'), $imageObj->getMeta()->convertMeta()->getError());
}
$debugInfo[] = array(__('WPML Duplicates'), json_encode($imageObj->getWPMLDuplicates()) );
if ($imageObj->getParent() !== false)
{
$debugInfo[] = array(__('WPML duplicate - Parent: '), $imageObj->getParent());
}
if (isset($urls))
{
$debugInfo[] = array(__('To Optimize URLS'), $urls);
}
if (isset($optimizeData))
{
$debugInfo[] = array(__('Optimize Data'), $optimizeData);
$optControl = new optimizeController();
$q = $optControl->getQueue($imageObj->get('type'));
$debugInfo[] = array(__('Image to Queue'), $q->_debug_imageModelToQueue($imageObj) );
}
$debugInfo['imagemetadata'] = array(__('ImageModel Metadata (ShortPixel)'), $imageObj);
$debugInfo[] = array('', '<hr>');
$debugInfo['wpmetadata'] = array(__('WordPress Get Attachment Metadata'), $meta );
$debugInfo[] = array('', '<hr>');
if ($imageObj->hasBackup())
$backupFile = $imageObj->getBackupFile();
else {
$backupFile = $fs->getFile($fs->getBackupDirectory($imageObj) . $imageObj->getBackupFileName());
}
$debugInfo[] = array(__('Backup Folder'), (string) $backupFile->getFileDir() );
if ($imageObj->hasBackup())
$backupText = __('Backup File :');
else {
$backupText = __('Target Backup File after optimization (no backup) ');
}
$debugInfo[] = array( $backupText, (string) $backupFile . '(' . UiHelper::formatBytes($backupFile->getFileSize()) . ')' );
$debugInfo[] = array(__("No Main File Backup Available"), '');
if ($imageObj->getMeta()->convertMeta()->isConverted())
{
$convertedBackup = ($imageObj->hasBackup(array('forceConverted' => true))) ? '<span class="green">Yes</span>' : '<span class="red">No</span>';
$backup = $imageObj->getBackupFile(array('forceConverted' => true));
$debugInfo[] = array('Has converted backup', $convertedBackup);
if (is_object($backup))
$debugInfo[] = array('Backup: ', $backup->getFullPath() );
}
if ($or = $imageObj->hasOriginal())
{
$original = $imageObj->getOriginalFile();
$debugInfo[] = array(__('Has Original File: '), $original->getFullPath() . '(' . UiHelper::formatBytes($original->getFileSize()) . ')');
$orbackup = $original->getBackupFile();
if ($orbackup)
$debugInfo[] = array(__('Has Backup Original Image'), $orbackup->getFullPath() . '(' . UiHelper::formatBytes($orbackup->getFileSize()) . ')');
$debugInfo[] = array('', '<hr>');
}
if (! isset($meta['sizes']) )
{
$debugInfo[] = array('', __('Thumbnails were not generated', 'shortpixel-image-optimiser'));
}
else
{
foreach($thumbnails as $thumbObj)
{
$size = $thumbObj->get('size');
$display_size = ucfirst(str_replace("_", " ", $size));
//$thumbObj = $imageObj->getThumbnail($size);
if ($thumbObj === false)
{
$debugInfo[] = array(__('Thumbnail not found / loaded: ', 'shortpixel-image-optimiser'), $size );
continue;
}
$url = $thumbObj->getURL(); //$fs->pathToURL($thumbObj); //wp_get_attachment_image_src($this->post_id, $size);
$filename = $thumbObj->getFullPath();
$backupFile = $thumbObj->getBackupFile();
if ($thumbObj->hasBackup())
{
$backup = $backupFile->getFullPath();
$backupText = __('Backup File :');
}
else {
$backupFile = $fs->getFile($fs->getBackupDirectory($thumbObj) . $thumbObj->getBackupFileName());
$backup = $backupFile->getFullPath();
$backupText = __('Target Backup File after optimization (no backup) ');
}
$width = $thumbObj->get('width');
$height = $thumbObj->get('height');
$processable = ($thumbObj->isProcessable()) ? '<span class="green">Yes</span>' : '<span class="red">No</span> (' . $thumbObj->getReason('processable') . ')';
$restorable = ($thumbObj->isRestorable()) ? '<span class="green">Yes</span>' : '<span class="red">No</span> (' . $thumbObj->getReason('restorable') . ')';
$hasrecord = ($thumbObj->hasDBRecord()) ? '<span class="green">Yes</span>' : '<span class="red">No</span> ';
$dbid = $thumbObj->getMeta('databaseID');
$debugInfo[] = array('', "<div class='$size previewwrapper'><img src='" . $url . "'><p class='label'>
<b>URL:</b> $url ( $display_size - $width X $height ) <br><b>FileName:</b> $filename <br> <b> $backupText </b> $backup </p>
<p><b>Processable: </b> $processable <br> <b>Restorable:</b> $restorable <br> <b>Record:</b> $hasrecord ($dbid) </p>
<hr></div>");
}
}
return $debugInfo;
}
} // controller .

View File

@ -0,0 +1,259 @@
<?php
namespace ShortPixel\Controller\View;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Helper\UiHelper as UiHelper;
use ShortPixel\Helper\UtilHelper as UtilHelper;
use ShortPixel\Controller\ApiKeyController as ApiKeyController;
use ShortPixel\Controller\QuotaController as QuotaController;
use ShortPixel\Controller\OptimizeController as OptimizeController;
use ShortPixel\Notices\NoticeController as Notice;
use ShortPixel\Model\Image\ImageModel as ImageModel;
use ShortPixel\Model\Image\MediaLibraryModel as MediaLibraryModel;
// Controller for the MediaLibraryView
class ListMediaViewController extends \ShortPixel\ViewController
{
protected static $instance;
protected $template = 'view-list-media';
// protected $model = 'image';
public function __construct()
{
parent::__construct();
}
public function load()
{
$fs = \wpSPIO()->filesystem();
$fs->startTrustedMode();
$this->checkAction(); // bulk action checkboxes, y'all
$this->loadHooks();
}
/** Check if a bulk action (checkboxes) was requested
*/
protected function checkAction()
{
$wp_list_table = _get_list_table('WP_Media_List_Table');
$action = $wp_list_table->current_action();
if (! $action)
return;
if(strpos($action, 'shortpixel') === 0 ) {
check_admin_referer('bulk-media');
}
// Nothing selected, nothing doin'
if (! isset($_GET['media']) || ! is_array($_GET['media']))
return;
$fs = \wpSPIO()->filesystem();
$optimizeController = new OptimizeController();
$items = array_filter($_GET['media'], 'intval');
$numItems = count($items);
$plugin_action = str_replace('shortpixel-', '', $action);
$targetCompressionType = $targetCrop = null;
switch ($plugin_action)
{
case "glossy":
$targetCompressionType = ImageModel::COMPRESSION_GLOSSY;
break;
case "lossy":
$targetCompressionType = ImageModel::COMPRESSION_LOSSY;
break;
case "lossless":
$targetCompressionType = ImageModel::COMPRESSION_LOSSLESS;
break;
case 'smartcrop':
$targetCrop = ImageModel::ACTION_SMARTCROP;
break;
case 'smartcropless':
$targetCrop = ImageModel::ACTION_SMARTCROPLESS;
break;
}
foreach($items as $item_id)
{
$mediaItem = $fs->getMediaImage($item_id);
switch($plugin_action)
{
case 'optimize':
if ($mediaItem->isProcessable())
$res = $optimizeController->addItemToQueue($mediaItem);
break;
case 'smartcrop':
case 'smartcropless':
if ($mediaItem->isOptimized())
{
$targetCompressionType = $mediaItem->getMeta('compressionType');
}
else {
$targetCompressionType = \wpSPIO()->settings()->compressionType;
}
case 'glossy':
case 'lossy':
case 'lossless':
if ($mediaItem->isOptimized() && $mediaItem->getMeta('compressionType') == $targetCompressionType && is_null($targetCrop) )
{
// do nothing if already done w/ this compression.
}
elseif(! $mediaItem->isOptimized())
{
$mediaItem->setMeta('compressionType', $targetCompressionType);
if (! is_null($targetCrop))
{
$mediaItem->doSetting('smartcrop', $targetCrop);
}
$res = $optimizeController->addItemToQueue($mediaItem);
}
else
{
$args = array();
if (! is_null($targetCrop))
{
$args = array('smartcrop' => $targetCrop);
}
$res = $optimizeController->reOptimizeItem($mediaItem, $targetCompressionType, $args);
}
break;
case 'restore';
if ($mediaItem->isOptimized())
$res = $optimizeController->restoreItem($mediaItem);
break;
case 'mark-completed':
if ($mediaItem->isProcessable())
{
$mediaItem->markCompleted(__('This item has been manually marked as completed', 'shortpixel-image-optimiser'), ImageModel::FILE_STATUS_MARKED_DONE);
}
break;
}
}
}
/** Hooks for the MediaLibrary View */
protected function loadHooks()
{
add_filter( 'manage_media_columns', array( $this, 'headerColumns' ) );//add media library column header
add_action( 'manage_media_custom_column', array( $this, 'doColumn' ), 10, 2 );//generate the media library column
//Sort and filter on ShortPixel Compression column
//add_filter( 'manage_upload_sortable_columns', array( $this, 'registerSortable') );
add_action('restrict_manage_posts', array( $this, 'mediaAddFilterDropdown'));
add_action('loop_end', array($this, 'loadComparer'));
}
public function headerColumns($defaults)
{
$defaults['wp-shortPixel'] = __('ShortPixel Compression', 'shortpixel-image-optimiser');
return $defaults;
}
public function doColumn($column_name, $id)
{
if($column_name == 'wp-shortPixel')
{
$this->view = new \stdClass; // reset every row
$this->view->id = $id;
$this->loadItem($id);
$this->loadView(null, false);
}
}
public function loadItem($id)
{
$fs = \wpSPIO()->filesystem();
$mediaItem = $fs->getMediaImage($id);
$keyControl = ApiKeyController::getInstance();
$quotaControl = QuotaController::getInstance();
// Asking for something non-existing.
if ($mediaItem === false)
{
$this->view->text = __('File Error. This could be not an image or the file is missing', 'shortpixel-image-optimiser');
return;
}
$this->view->mediaItem = $mediaItem;
$actions = array();
$list_actions = array();
$this->view->text = UiHelper::getStatusText($mediaItem);
$this->view->list_actions = UiHelper::getListActions($mediaItem);
if ( count($this->view->list_actions) > 0)
{
$this->view->list_actions = UiHelper::renderBurgerList($this->view->list_actions, $mediaItem);
}
else
{
$this->view->list_actions = '';
}
$this->view->actions = UiHelper::getActions($mediaItem);
//$this->view->actions = $actions;
if (! $this->userIsAllowed)
{
$this->view->actions = array();
$this->view->list_actions = '';
}
}
public function loadComparer()
{
$this->loadView('snippets/part-comparer');
}
/*
* @hook restrict_manage_posts
*/
public function mediaAddFilterDropdown() {
$scr = get_current_screen();
if ( $scr->base !== 'upload' ) return;
$status = filter_input(INPUT_GET, 'shortpixel_status', FILTER_UNSAFE_RAW );
$options = array(
'all' => __('Any ShortPixel State', 'shortpixel-image-optimiser'),
'optimized' => __('Optimized', 'shortpixel-image-optimiser'),
'unoptimized' => __('Unoptimized', 'shortpixel-image-optimiser'),
'prevented' => __('Optimization Error', 'shortpixer-image-optimiser'),
);
echo "<select name='shortpixel_status' id='shortpixel_status'>\n";
foreach($options as $optname => $optval)
{
$selected = ($status == $optname) ? esc_attr('selected') : '';
echo "<option value='". esc_attr($optname) . "' $selected >" . esc_html($optval) . "</option>\n";
}
echo "</select>";
}
}

View File

@ -0,0 +1,487 @@
<?php
namespace ShortPixel\Controller\View;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Notices\NoticeController as Notices;
use ShortPixel\Helper\InstallHelper as InstallHelper;
use ShortPixel\Controller\OtherMediaController as OtherMediaController;
class OtherMediaFolderViewController extends \ShortPixel\ViewController
{
protected $template = 'view-other-media-folder';
protected static $instance;
// Pagination .
protected $items_per_page = 20;
protected $currentPage = 1;
protected $total_items = 0;
protected $order;
protected $orderby;
protected $search;
protected $show_hidden = false;
protected $has_hidden_items = false;
protected $customFolderBase;
private $controller;
public function __construct()
{
parent::__construct();
$fs = \wpSPIO()->filesystem();
$this->controller = OtherMediaController::getInstance();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
$this->currentPage = isset($_GET['paged']) ? intval($_GET['paged']) : 1;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
$this->orderby = ( ! empty( $_GET['orderby'] ) ) ? $this->filterAllowedOrderBy(sanitize_text_field(wp_unslash($_GET['orderby']))) : 'id';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
$this->order = ( ! empty($_GET['order'] ) ) ? sanitize_text_field( wp_unslash($_GET['order'])) : 'desc'; // If no order, default to asc
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
$this->search = (isset($_GET["s"]) && strlen($_GET["s"]) > 0) ? sanitize_text_field( wp_unslash($_GET['s'])) : false;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
$this->show_hidden = isset($_GET['show_hidden']) ? sanitize_text_field(wp_unslash($_GET['show_hidden'])) : false;
$customFolderBase = $fs->getWPFileBase();
$this->customFolderBase = $customFolderBase->getPath();
$this->loadSettings();
}
/** Controller default action - overview */
public function load()
{
// $this->process_actions();
//
$this->view->items = $this->getItems();
// $this->view->folders = $this->getItemFolders($this->view->items);
$this->view->headings = $this->getHeadings();
$this->view->pagination = $this->getPagination();
$this->view->filter = $this->getFilter();
// $this->checkQueue();
$this->loadView();
}
public function singleItemView($folderObj)
{
ob_start();
$this->view->current_item = $folderObj;
$this->loadView('custom/part-single-folder', false);
$result = ob_get_contents();
ob_end_clean();
return $result;
}
protected function loadSettings()
{
$settings = \wpSPIO()->settings();
$this->view->settings = new \stdclass;
$this->view->settings->includeNextGen = $settings->includeNextGen;
$this->view->title = __('Shortpixel Custom Folders', 'shortpixel-image-optimiser');
$this->view->show_search = true;
$this->view->has_filters = true;
}
protected function getRowActions($item)
{
$actions = array();
$removeAction = array('remove' => array(
'function' => 'window.ShortPixelProcessor.screen.StopMonitoringFolder(' . intval($item->get('id')) . ')',
'type' => 'js',
'text' => __('Stop Monitoring', 'shortpixel-image-optimiser'),
'display' => 'inline',
));
$refreshAction = array('refresh' => array(
'function' => 'window.ShortPixelProcessor.screen.RefreshFolder(' . intval($item->get('id')) . ')',
'type' => 'js',
'text' => __('Refresh Folder', 'shortpixel-image-optimiser'),
'display' => 'inline',
));
// @todo Get path of last one/two subdirectories and link to files page (?) or add a query for folder_id options.
$url = add_query_arg('part', 'files', $this->url);
$url = add_query_arg('folder_id', $item->get('id'), $url);
$showFilesAction = array('showfiles' => array(
'function' => esc_url($url),
'type' => 'link',
'text' => __('Show all Files', 'shortpixel-image-optimiser'),
'display' => 'inline',
));
$actions = array_merge($actions, $refreshAction, $removeAction, $showFilesAction);
// $actions = array_merge($actions, );
return $actions;
}
private function getItems($args = array())
{
$results = $this->queryItems($args);
$items = array();
foreach($results as $index => $databaseObj)
{
$db_id = $databaseObj->id;
$folderObj = $this->controller->getFolderByID($db_id);
$items[$db_id] = $folderObj;
}
$this->total_items = $this->queryItems(array('limit' => -1, 'only_count' => true));
return $items;
}
private function queryItems($args = array())
{
global $wpdb;
$page = $this->currentPage;
if ($page <= 0)
$page = 1;
$defaults = array(
'id' => false, // Get folder by Id
'remove_hidden' => true, // Query only active folders
'path' => false,
'only_count' => false,
'limit' => $this->items_per_page,
'offset' => ($page - 1) * $this->items_per_page,
);
$filters = $this->getFilter();
$args = wp_parse_args($args, $defaults);
if (! $this->hasFoldersTable())
{
if ($args['only_count'])
return 0;
else
return array();
}
$fs = \wpSPIO()->fileSystem();
if ($args['only_count'])
$selector = 'count(id) as id';
else
$selector = '*';
$sql = "SELECT " . $selector . " FROM " . $wpdb->prefix . "shortpixel_folders WHERE 1=1 ";
$prepare = array();
// $mask = array();
if ($args['id'] !== false && $args['id'] > 0)
{
$sql .= ' AND id = %d';
$prepare[] = $args['id'];
}
elseif($args['path'] !== false && strlen($args['path']) > 0)
{
$sql .= ' AND path = %s';
$prepare[] = $args['path'];
}
if ($args['remove_hidden'])
{
$sql .= " AND status <> -1";
}
$sql .= ($this->orderby ? " ORDER BY " . $this->orderby . " " . $this->order . " " : "");
if ($args['limit'] > 0)
{
$sql .= " LIMIT " . intval($args['limit']) . " OFFSET " . intval($args['offset']);
}
if (count($prepare) > 0)
$sql = $wpdb->prepare($sql, $prepare);
if ($args['only_count'])
$results = intval($wpdb->get_var($sql));
else
$results = $wpdb->get_results($sql);
return $results;
}
protected function getHeadings()
{
$headings = array(
'checkbox' => array('title' => '<input type="checkbox" name="select-all">',
'sortable' => false,
'orderby' => 'id', // placeholder to allow sort on this.
),
'name' => array('title' => __('Folder Name', 'shortpixel-image-optimiser'),
'sortable' => true,
'orderby' => 'name',
),
'type' => array('title' => __('Type', 'shortpixel-image-optimiser'),
'sortable' => false,
'orderby' => 'path',
),
'files' => array('title' => __('Files', 'shortpixel-image-optimiser'),
'sortable' => false,
'orderby' => 'files',
'title_context' => __('Images in folder - optimized / unoptimized ','shortpixel-image-optimiser'),
),
'date' => array('title' => __('Last change', 'shortpixel-image-optimiser'),
'sortable' => true,
'orderby' => 'ts_updated',
),
/* Status is only yes, or nextgen. Already in the Type string. Status use for messages */
'status' => array('title' => __('Message', 'shortpixel-image-optimiser'),
'sortable' => false,
'orderby' => 'status',
),
/* 'actions' => array('title' => __('Actions', 'shortpixel-image-optimiser'),
'sortable' => false,
), */
);
return $headings;
}
private function getPageArgs($args = array())
{
$defaults = array(
'orderby' => $this->orderby,
'order' => $this->order,
's' => $this->search,
'paged' => $this->currentPage,
'part' => 'folders',
);
$page_args = array_filter(wp_parse_args($args, $defaults));
return $page_args; // has url
}
// @todo duplicate of OtherMediaViewController which is not nice.
protected function getDisplayHeading($heading)
{
$output = '';
$defaults = array('title' => '', 'sortable' => false);
$heading = wp_parse_args($heading, $defaults);
$title = $heading['title'];
if ($heading['sortable'])
{
//$current_order = isset($_GET['order']) ? $current_order : false;
//$current_orderby = isset($_GET['orderby']) ? $current_orderby : false;
$sorturl = add_query_arg('orderby', $heading['orderby'] );
$sorted = '';
if ($this->orderby == $heading['orderby'])
{
if ($this->order == 'desc')
{
$sorturl = add_query_arg('order', 'asc', $sorturl);
$sorted = 'sorted desc';
}
else
{
$sorturl = add_query_arg('order', 'desc', $sorturl);
$sorted = 'sorted asc';
}
}
else
{
$sorturl = add_query_arg('order', 'asc', $sorturl);
}
$output = '<a href="' . esc_url($sorturl) . '"><span>' . esc_html($title) . '</span><span class="sorting-indicator '. esc_attr($sorted) . '">&nbsp;</span></a>';
}
else
{
$output = $title;
}
return $output;
}
protected function filterAllowedOrderBy($orderby)
{
$headings = $this->getHeadings() ;
$filters = array();
foreach ($headings as $heading)
{
if (isset($heading['orderby']))
{
$filters[]= $heading['orderby'];
}
}
if (! in_array($orderby, $filters))
return '';
return $orderby;
}
protected function getPagination()
{
$parray = array();
$current = $this->currentPage;
$total = $this->total_items;
$per_page = $this->items_per_page;
$pages = ceil($total / $per_page);
if ($pages <= 1)
return false; // no pages.
$disable_first = $disable_last = $disable_prev = $disable_next = false;
$page_links = array();
if ( $current == 1 ) {
$disable_first = true;
$disable_prev = true;
}
if ( $current == 2 ) {
$disable_first = true;
}
if ( $current == $pages ) {
$disable_last = true;
$disable_next = true;
}
if ( $current == $pages - 1 ) {
$disable_last = true;
}
$total_pages_before = '<span class="paging-input">';
$total_pages_after = '</span></span>';
$page_args =$this->getPageArgs(); // has url
if (isset($page_args['paged']))
unset($page_args['paged']);
// Try with controller URL, if not present, try with upload URL and page param.
$admin_url = admin_url('upload.php');
$url = (is_null($this->url)) ? add_query_arg('page','wp-short-pixel-custom', $admin_url) : $this->url; // has url
$current_url = add_query_arg($page_args, $url);
$url = remove_query_arg('page', $url);
$page_args['page'] = 'wp-short-pixel-custom';
$output = '<form method="GET" action="'. esc_attr($url) . '">';
foreach($page_args as $arg => $val)
{
$output .= sprintf('<input type="hidden" name="%s" value="%s">', $arg, $val);
}
$output .= '<span class="displaying-num">'. sprintf(esc_html__('%d Images', 'shortpixel-image-optimiser'), $this->total_items) . '</span>';
if ( $disable_first ) {
$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&laquo;</span>';
} else {
$page_links[] = sprintf(
"<a class='first-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
esc_url( $current_url ),
esc_html__( 'First page' ),
'&laquo;'
);
}
if ( $disable_prev ) {
$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&lsaquo;</span>';
} else {
$page_links[] = sprintf(
"<a class='prev-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
esc_url( add_query_arg( 'paged', max( 1, $current - 1 ), $current_url ) ),
esc_html__( 'Previous page' ),
'&lsaquo;'
);
}
$html_current_page = sprintf(
"%s<input class='current-page' id='current-page-selector' type='text' name='paged' value='%s' size='%d' aria-describedby='table-paging' /><span class='tablenav-paging-text'>",
'<label for="current-page-selector" class="screen-reader-text">' . esc_html__( 'Current Page' ) . '</label>',
$current,
strlen( $pages )
);
$html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $pages ) );
$page_links[] = $total_pages_before . sprintf(
/* translators: 1: Current page, 2: Total pages. */
_x( '%1$s of %2$s', 'paging' ),
$html_current_page,
$html_total_pages
) . $total_pages_after;
if ( $disable_next ) {
$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&rsaquo;</span>';
} else {
$page_links[] = sprintf(
"<a class='next-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
esc_url( add_query_arg( 'paged', min( $pages, $current + 1 ), $current_url ) ),
__( 'Next page' ),
'&rsaquo;'
);
}
if ( $disable_last ) {
$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&raquo;</span>';
} else {
$page_links[] = sprintf(
"<a class='last-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
esc_url( add_query_arg( 'paged', $pages, $current_url ) ),
__( 'Last page' ),
'&raquo;'
);
}
$output .= "\n<span class='pagination-links'>" . join( "\n", $page_links ) . '</span>';
$output .= "</form>";
return $output;
}
protected function getFilter() {
$filter = array();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
$search = (isset($_GET['s'])) ? sanitize_text_field(wp_unslash($_GET['s'])) : '';
if(strlen($search) > 0) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
$filter['path'] = (object)array("operator" => "like", "value" =>"'%" . esc_sql($search) . "%'");
}
return $filter;
}
private function hasFoldersTable()
{
return InstallHelper::checkTableExists('shortpixel_folders');
}
} // class

View File

@ -0,0 +1,45 @@
<?php
namespace ShortPixel\Controller\View;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Notices\NoticeController as Notices;
use ShortPixel\Helper\InstallHelper as InstallHelper;
use ShortPixel\Controller\OtherMediaController as OtherMediaController;
class OtherMediaScanViewController extends \ShortPixel\ViewController
{
protected $template = 'view-other-media-scan';
protected static $instance;
protected static $allFolders;
private $controller;
public function __construct()
{
parent::__construct();
$this->controller = OtherMediaController::getInstance();
}
public function load()
{
$this->view->title = __('Scan for new files', 'shortpixel-image-optimiser');
$this->view->pagination = false;
$this->view->show_search = false;
$this->view->has_filters = false;
$this->view->totalFolders = count($this->controller->getActiveDirectoryIDS());
$this->loadView();
}
} // class

View File

@ -0,0 +1,591 @@
<?php
namespace ShortPixel\Controller\View;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Notices\NoticeController as Notices;
use ShortPixel\Controller\ApiKeyController as ApiKeyController;
use ShortPixel\Controller\OtherMediaController as OtherMediaController;
use ShortPixel\Model\File\DirectoryOtherMediaModel as DirectoryOtherMediaModel;
use ShortPixel\Model\Image\ImageModel as ImageModel;
use ShortPixel\Controller\Queue\CustomQueue as CustomQueue;
use ShortPixel\Helper\UiHelper as UiHelper;
// Future contoller for the edit media metabox view.
class OtherMediaViewController extends \ShortPixel\ViewController
{
//$this->model = new
protected $template = 'view-other-media';
protected static $instance;
// Pagination .
protected $items_per_page = 20;
protected $currentPage = 1;
protected $total_items = 0;
protected $order;
protected $orderby;
protected $search;
protected $show_hidden = false;
protected $has_hidden_items = false;
public function __construct()
{
parent::__construct();
// 2015: https://github.com/WordPress/WordPress-Coding-Standards/issues/426 !
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
$this->currentPage = isset($_GET['paged']) ? intval($_GET['paged']) : 1;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
$this->orderby = ( ! empty( $_GET['orderby'] ) ) ? $this->filterAllowedOrderBy(sanitize_text_field(wp_unslash($_GET['orderby']))) : 'id';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
$this->order = ( ! empty($_GET['order'] ) ) ? sanitize_text_field( wp_unslash($_GET['order'])) : 'desc'; // If no order, default to asc
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
$this->search = (isset($_GET["s"]) && strlen($_GET["s"]) > 0) ? sanitize_text_field( wp_unslash($_GET['s'])) : false;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
$this->show_hidden = isset($_GET['show_hidden']) ? sanitize_text_field(wp_unslash($_GET['show_hidden'])) : false;
}
/** Controller default action - overview */
public function load()
{
// $this->process_actions();
$this->view->items = $this->getItems();
$this->view->folders = $this->getItemFolders($this->view->items);
$this->view->headings = $this->getHeadings();
$this->view->pagination = $this->getPagination();
$this->view->filter = $this->getFilter();
$this->view->title = __('Custom Media optimized by ShortPixel', 'shortpixel-image-optimiser');
$this->view->show_search = true;
// $this->checkQueue();
$this->loadView();
}
protected function getHeadings()
{
$headings = array(
'checkbox' => array('title' => '<input type="checkbox" name="select-all">', 'sortable' => false),
'thumbnails' => array('title' => __('Thumbnail', 'shortpixel-image-optimiser'),
'sortable' => false,
'orderby' => 'id', // placeholder to allow sort on this.
),
'name' => array('title' => __('Name', 'shortpixel-image-optimiser'),
'sortable' => true,
'orderby' => 'name',
),
'folder' => array('title' => __('Folder', 'shortpixel-image-optimiser'),
'sortable' => true,
'orderby' => 'path',
),
'type' => array('title' => __('Type', 'shortpixel-image-optimiser'),
'sortable' => false,
),
'date' => array('title' => __('Date', 'shortpixel-image-optimiser'),
'sortable' => true,
'orderby' => 'ts_optimized',
),
'status' => array('title' => __('Status', 'shortpixel-image-optimiser'),
'sortable' => true,
'orderby' => 'status',
),
/* 'actions' => array('title' => __('Actions', 'shortpixel-image-optimiser'),
'sortable' => false,
), */
);
$keyControl = ApiKeyController::getInstance();
if (! $keyControl->keyIsVerified())
{
$headings['actions']['title'] = '';
}
return $headings;
}
protected function getItems()
{
$fs = \wpSPIO()->filesystem();
// [BS] Moving this from ts_added since often images get added at the same time, resulting in unpredictable sorting
$items = $this->queryItems();
$removed = array();
foreach($items as $index => $item)
{
$mediaItem = $fs->getImage($item->id, 'custom');
if (! $mediaItem->exists()) // remove image if it doesn't exist.
{
$mediaItem->onDelete();
$removed[] = $item->path;
unset($items[$index]);
}
$items[$index] = $mediaItem;
}
if (count($removed) > 0)
{
Notices::addWarning(sprintf(__('Some images were missing. They have been removed from the Custom Media overview : %s %s'),
'<BR>', implode('<BR>', $removed)));
}
return $items;
}
protected function getItemFolders($items)
{
$folderArray = array();
$otherMedia = OtherMediaController::getInstance();
foreach ($items as $item) // mediaItem;
{
$folder_id = $item->get('folder_id');
if (! isset($folderArray[$folder_id]))
{
$folderArray[$folder_id] = $otherMedia->getFolderByID($folder_id);
}
}
return $folderArray;
}
/* Check which folders are in result, and load them. */
protected function loadFolders($items)
{
$folderArray = array();
$otherMedia = OtherMediaController::getInstance();
foreach($items as $item)
{
$folder_id = $item->get('folder_id');
if (! isset($folderArray[$folder_id]))
{
$folderArray[$folder_id] = $otherMedia->getFolderByID($folder_id);
}
}
return $folderArray;
}
protected function getFilter() {
$filter = array();
$this->view->hasFilter = false;
$this->view->hasSearch = false;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
$search = (isset($_GET['s'])) ? sanitize_text_field(wp_unslash($_GET['s'])) : '';
if(strlen($search) > 0) {
$this->view->hasSearch = true;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
$filter['path'] = (object)array("operator" => "like", "value" =>"'%" . esc_sql($search) . "%'");
}
$folderFilter = (isset($_GET['folder_id'])) ? intval($_GET['folder_id']) : false;
if (false !== $folderFilter)
{
$this->view->hasFilter = true;
$filter['folder_id'] = (object)array("operator" => "=", "value" =>"'" . esc_sql($folderFilter) . "'");
}
$statusFilter = isset($_GET['custom-status']) ? sanitize_text_field($_GET['custom-status']) : false;
if (false !== $statusFilter)
{
$operator = '=';
$value = false;
$this->view->hasFilter = true;
switch($statusFilter)
{
case 'optimized':
$value = ImageModel::FILE_STATUS_SUCCESS;
break;
case 'unoptimized':
$value = ImageModel::FILE_STATUS_UNPROCESSED;
break;
case 'prevented':
// $value = 0;
// $operator = '<';
$filter['status'] = (object) array('field' => 'status',
'operator' => "<", 'value' => "0");
$filter['status2'] = (object) array('field' => 'status',
'operator' => '<>', 'value' => ImageModel::FILE_STATUS_MARKED_DONE
);
break;
}
if (false !== $value)
{
$filter['status'] = (object)array("operator" => $operator, "value" =>"'" . esc_sql($value) . "'");
}
}
return $filter;
}
public function queryItems() {
$filters = $this->getFilter();
global $wpdb;
$page = $this->currentPage;
if ($page <= 0)
$page = 1;
$controller = OtherMediaController::getInstance();
$hidden_ids = $controller->getHiddenDirectoryIDS();
if (count($hidden_ids) > 0)
$this->has_hidden_items = true;
if ($this->show_hidden == true)
$dirs = implode(',', $hidden_ids );
else
$dirs = implode(',', $controller->getActiveDirectoryIDS() );
if (strlen($dirs) == 0)
return array();
$sql = "SELECT COUNT(id) as count FROM " . $wpdb->prefix . "shortpixel_meta where folder_id in ( " . $dirs . ") ";
foreach($filters as $field => $value) {
$field = property_exists($value, 'field') ? $value->field : $field;
$sql .= " AND $field " . $value->operator . " ". $value->value . " ";
}
$this->total_items = $wpdb->get_var($sql);
$sql = "SELECT * FROM " . $wpdb->prefix . "shortpixel_meta where folder_id in ( " . $dirs . ") ";
foreach($filters as $field => $value) {
$field = property_exists($value, 'field') ? $value->field : $field;
$sql .= " AND $field " . $value->operator . " ". $value->value . " ";
}
$sql .= ($this->orderby ? " ORDER BY " . $this->orderby . " " . $this->order . " " : "")
. " LIMIT " . $this->items_per_page . " OFFSET " . ($page - 1) * $this->items_per_page;
$results = $wpdb->get_results($sql);
return $results;
}
private function getPageArgs($args = array())
{
$defaults = array(
'orderby' => $this->orderby,
'order' => $this->order,
's' => $this->search,
'paged' => $this->currentPage
);
$page_args = array_filter(wp_parse_args($args, $defaults));
return $page_args; // has url
}
protected function filterAllowedOrderBy($orderby)
{
$headings = $this->getHeadings() ;
$filters = array();
foreach ($headings as $heading)
{
if (isset($heading['orderby']))
{
$filters[]= $heading['orderby'];
}
}
if (! in_array($orderby, $filters))
return '';
return $orderby;
}
protected function getPagination()
{
$parray = array();
$current = $this->currentPage;
$total = $this->total_items;
$per_page = $this->items_per_page;
$pages = ceil($total / $per_page);
if ($pages <= 1)
return false; // no pages.
$disable_first = $disable_last = $disable_prev = $disable_next = false;
$page_links = array();
if ( $current == 1 ) {
$disable_first = true;
$disable_prev = true;
}
if ( $current == 2 ) {
$disable_first = true;
}
if ( $current == $pages ) {
$disable_last = true;
$disable_next = true;
}
if ( $current == $pages - 1 ) {
$disable_last = true;
}
$total_pages_before = '<span class="paging-input">';
$total_pages_after = '</span></span>';
$page_args =$this->getPageArgs(); // has url
if (isset($page_args['paged']))
unset($page_args['paged']);
// Try with controller URL, if not present, try with upload URL and page param.
$admin_url = admin_url('upload.php');
$url = (is_null($this->url)) ? add_query_arg('page','wp-short-pixel-custom', $admin_url) : $this->url; // has url
$current_url = add_query_arg($page_args, $url);
$url = remove_query_arg('page', $url);
$page_args['page'] = 'wp-short-pixel-custom';
$output = '<form method="GET" action="'. esc_attr($url) . '">';
foreach($page_args as $arg => $val)
{
$output .= sprintf('<input type="hidden" name="%s" value="%s">', $arg, $val);
}
$output .= '<span class="displaying-num">'. sprintf(esc_html__('%d Images', 'shortpixel-image-optimiser'), $this->total_items) . '</span>';
if ( $disable_first ) {
$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&laquo;</span>';
} else {
$page_links[] = sprintf(
"<a class='first-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
esc_url( $current_url ),
esc_html__( 'First page' ),
'&laquo;'
);
}
if ( $disable_prev ) {
$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&lsaquo;</span>';
} else {
$page_links[] = sprintf(
"<a class='prev-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
esc_url( add_query_arg( 'paged', max( 1, $current - 1 ), $current_url ) ),
esc_html__( 'Previous page' ),
'&lsaquo;'
);
}
$html_current_page = sprintf(
"%s<input class='current-page' id='current-page-selector' type='text' name='paged' value='%s' size='%d' aria-describedby='table-paging' /><span class='tablenav-paging-text'>",
'<label for="current-page-selector" class="screen-reader-text">' . esc_html__( 'Current Page' ) . '</label>',
$current,
strlen( $pages )
);
$html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $pages ) );
$page_links[] = $total_pages_before . sprintf(
/* translators: 1: Current page, 2: Total pages. */
_x( '%1$s of %2$s', 'paging' ),
$html_current_page,
$html_total_pages
) . $total_pages_after;
if ( $disable_next ) {
$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&rsaquo;</span>';
} else {
$page_links[] = sprintf(
"<a class='next-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
esc_url( add_query_arg( 'paged', min( $pages, $current + 1 ), $current_url ) ),
__( 'Next page' ),
'&rsaquo;'
);
}
if ( $disable_last ) {
$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&raquo;</span>';
} else {
$page_links[] = sprintf(
"<a class='last-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
esc_url( add_query_arg( 'paged', $pages, $current_url ) ),
__( 'Last page' ),
'&raquo;'
);
}
$output .= "\n<span class='pagination-links'>" . join( "\n", $page_links ) . '</span>';
$output .= "</form>";
return $output;
}
/** Actions to list under the Image row
* @param $item CustomImageModel
*/
protected function getRowActions($item)
{
$settings = \wpSPIO()->settings();
$keyControl = ApiKeyController::getInstance();
$actions = UIHelper::getActions($item);
$viewAction = array('view' => array(
'function' => $item->getUrl(),
'type' => 'link',
'text' => __('View', 'shortpixel-image-optimiser'),
'display' => 'inline',
));
$rowActions = array();
$rowActions = array_merge($rowActions, $viewAction);
if (false === $settings->quotaExceeded || true === $keyControl->keyIsVerified() )
$rowActions = array_merge($rowActions,$actions);
return $rowActions;
}
// Function to sync output exactly with Media Library functions for consistency
public function doActionColumn($item)
{
?>
<div id='sp-msg-<?php echo esc_attr($item->get('id')) ?>' class='sp-column-info'><?php
$this->printItemActions($item);
echo "<div>" . UiHelper::getStatusText($item) . "</div>";
?>
</div> <!-- sp-column-info -->
<?php
}
// Use for view, also for renderItemView
public function printItemActions($item)
{
$this->view->actions = UiHelper::getActions($item); // $this->getActions($item, $itemFile);
$list_actions = UiHelper::getListActions($item);
if (count($list_actions) > 0)
$list_actions = UiHelper::renderBurgerList($list_actions, $item);
else
$list_actions = '';
if (count($this->view->actions) > 0)
{
$this->loadView('snippets/part-single-actions', false);
}
echo $list_actions;
}
public function printFilter()
{
$status = filter_input(INPUT_GET, 'custom-status', FILTER_UNSAFE_RAW );
$options = array(
'all' => __('Any ShortPixel State', 'shortpixel-image-optimiser'),
'optimized' => __('Optimized', 'shortpixel-image-optimiser'),
'unoptimized' => __('Unoptimized', 'shortpixel-image-optimiser'),
'prevented' => __('Optimization Error', 'shortpixer-image-optimiser'),
);
echo "<select name='custom-status' id='status'>\n";
foreach($options as $optname => $optval)
{
$selected = ($status == $optname) ? esc_attr('selected') : '';
echo "<option value='". esc_attr($optname) . "' $selected >" . esc_html($optval) . "</option>\n";
}
echo "</select>";
}
protected function getDisplayHeading($heading)
{
$output = '';
$defaults = array('title' => '', 'sortable' => false);
$heading = wp_parse_args($heading, $defaults);
$title = $heading['title'];
if ($heading['sortable'])
{
//$current_order = isset($_GET['order']) ? $current_order : false;
//$current_orderby = isset($_GET['orderby']) ? $current_orderby : false;
$sorturl = add_query_arg('orderby', $heading['orderby'] );
$sorted = '';
if ($this->orderby == $heading['orderby'])
{
if ($this->order == 'desc')
{
$sorturl = add_query_arg('order', 'asc', $sorturl);
$sorted = 'sorted desc';
}
else
{
$sorturl = add_query_arg('order', 'desc', $sorturl);
$sorted = 'sorted asc';
}
}
else
{
$sorturl = add_query_arg('order', 'asc', $sorturl);
}
$output = '<a href="' . esc_url($sorturl) . '"><span>' . esc_html($title) . '</span><span class="sorting-indicator '. esc_attr($sorted) . '">&nbsp;</span></a>';
}
else
{
$output = $title;
}
return $output;
}
protected function getDisplayDate($item)
{
if ($item->getMeta('tsOptimized') > 0)
$timestamp = $item->getMeta('tsOptimized');
else
$timestamp = $item->getMeta('tsAdded');
$date = new \DateTime();
$date->setTimestamp($timestamp);
$display_date = UiHelper::formatDate($date);
return $display_date;
}
}

View File

@ -0,0 +1,183 @@
<?php
namespace ShortPixel\Helper;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Controller\ResponseController as ResponseController;
class DownloadHelper
{
private static $instance;
public function __construct()
{
$this->checkEnv();
}
public static function getInstance()
{
if (is_null(self::$instance))
{
self::$instance = new DownloadHelper();
}
return self::$instance;
}
protected function checkEnv()
{
if ( ! function_exists( 'download_url' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
}
public function downloadFile($url, $args = array())
{
$defaults = array(
'expectedSize' => null,
'destinationPath' => null,
);
$args = wp_parse_args($args, $defaults);
$success = false;
Log::addDebug('Downloading file :' . $url, $args);
$methods = array(
"download_url" => array(array($this, 'downloadURLMethod'), $url, false),
"download_url_force" => array(array($this, 'downloadURLMethod'), true),
"remote_get" => array(array($this, 'remoteGetMethod'), $url)
);
foreach($methods as $name => $data)
{
$function = $data[0];
if (is_callable($function))
{
$result = call_user_func_array($function, array_slice($data, 1) );
if (false !== $result)
{
$tempFile = $result;
$success = true;
break;
}
}
}
if (false === $success)
{
Log::addError('Failed to download File', $result);
ResponseController::addData('is_error', true);
//Responsecontroller::addData('message', $tempFile->get_error_message());
return false;
}
$fs = \wpSPIO()->filesystem();
$file = $fs->getFile($tempFile);
if (! is_null($args['destinationPath']))
{
$result = $this->moveDownload($file, $args['destinationPath']);
if (false === $result)
{
Log::addError('Failed to move Download', $args);
ResponseController::addData('is_error', true);
Responsecontroller::addData('message', __('Failed to move download to destination!', 'shortpixel-image-optimiser'));
return false;
}
else {
$file = $result;
}
}
return $file;
}
protected function moveDownload($fileObj, $destinationPath)
{
$fs = \wpSPIO()->filesystem();
$destinationFile = $fs->getFile($destinationPath);
// If file is non-existing, check directory and write-permissions.
if (false == $destinationFile->exists())
{
$dirObj = $destinationFile->getFileDir();
$dirObj->check(true);
}
$result = $fileObj->copy($destinationFile);
if ($result === false)
return false;
return $destinationFile;
}
private function downloadURLMethod($url, $force = false)
{
$executionTime = ini_get('max_execution_time');
if (! is_numeric($executionTime)) // edge case
{
$executionTime = 0;
}
$downloadTimeout = max($executionTime - 10, 15);
$url = $this->setPreferredProtocol(urldecode($url), $force);
$tempFile = \download_url($url, $downloadTimeout);
if (is_wp_error($tempFile))
{
Log::addError('Failed to Download File ', $tempFile);
Responsecontroller::addData('message', $tempFile->get_error_message());
return false;
}
return $tempFile;
}
private function remoteGetMethod($url)
{
//get_temp_dir
$tmpfname = tempnam(get_temp_dir(), 'spiotmp');
$downloadTimeout = max(ini_get('max_execution_time') - 10, 15);
$args_for_get = array(
'stream' => true,
'filename' => $tmpfname,
'timeout' => $downloadTimeout,
);
$response = wp_remote_get( $url, $args_for_get );
if (wp_remote_retrieve_response_code($response) == 200 && isset($response['filename']))
{
$filepath = $response['filename'];
return $filepath; // body is the full image is all went well.
}
else {
Log::addError('Wp Remote Get failed', $response);
}
return false;
}
private function setPreferredProtocol($url, $reset = false) {
//switch protocol based on the formerly detected working protocol
$settings = \wpSPIO()->settings();
if($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);
$settings->downloadProto = is_wp_error( $result ) ? 'https' : 'http';
}
return $settings->downloadProto == 'http' ?
str_replace('https://', 'http://', $url) :
str_replace('http://', 'https://', $url);
}
}

View File

@ -0,0 +1,315 @@
<?php
namespace ShortPixel\Helper;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Controller\OptimizeController as OptimizeController;
use ShortPixel\Controller\BulkController as BulkController;
use ShortPixel\Controller\FileSystemController as FileSystemController;
use ShortPixel\Controller\AdminNoticesController as AdminNoticesController;
use ShortPixel\Controller\StatsController as StatsController;
use ShortPixel\Controller\ApiKeyController as ApiKeyController;
use ShortPixel\Helper\UtilHelper as UtilHelper;
class InstallHelper
{
public static function activatePlugin()
{
self::deactivatePlugin();
$settings = \wpSPIO()->settings();
$env = wpSPIO()->env();
if(\WPShortPixelSettings::getOpt('deliverWebp') == 3 && ! $env->is_nginx) {
UtilHelper::alterHtaccess(true,true); //add the htaccess lines. Both are true because even if one option is now off in the past both fileformats could have been generated.
}
self::checkTables();
AdminNoticesController::resetOldNotices();
\WPShortPixelSettings::onActivate();
$optimizeController = new OptimizeController();
$q = $optimizeController->getQueue('media');
$q->getShortQ()->install(); // create table.
$settings->currentVersion = SHORTPIXEL_IMAGE_OPTIMISER_VERSION;
}
public static function deactivatePlugin()
{
$settings = \wpSPIO()->settings();
$settings::onDeactivate();
$env = wpSPIO()->env();
if (! $env->is_nginx)
{
UtilHelper::alterHtaccess(false, false);
}
// save remove.
$fs = new FileSystemController();
$log = $fs->getFile(SHORTPIXEL_BACKUP_FOLDER . "/shortpixel_log");
if ($log->exists())
$log->delete();
global $wpdb;
$sql = "delete from " . $wpdb->options . " where option_name like '%_transient_shortpixel%'";
$wpdb->query($sql); // remove transients.
// saved in settings object, reset all stats.
StatsController::getInstance()->reset();
}
public static function uninstallPlugin()
{
OptimizeController::uninstallPlugin();
ApiKeyController::uninstallPlugin();
delete_transient('bulk-secret');
delete_transient('othermedia_refresh_folder_delay');
delete_transient('avif_server_check');
delete_transient('quotaData');
}
// Removes everything of SPIO 5.x . Not recommended.
public static function hardUninstall()
{
$env = \wpSPIO()->env();
$settings = \wpSPIO()->settings();
$nonce = (isset($_POST['tools-nonce'])) ? sanitize_key($_POST['tools-nonce']) : null;
if ( ! wp_verify_nonce( $nonce, 'remove-all' ) ) {
wp_nonce_ays( '' );
}
self::deactivatePlugin(); // deactivate
self::uninstallPlugin(); // uninstall
// Bulk Log
BulkController::uninstallPlugin();
$settings::resetOptions();
if (! $env->is_nginx)
{
insert_with_markers( get_home_path() . '.htaccess', 'ShortPixelWebp', '');
}
self::removeTables();
// Remove Backups
$dir = \wpSPIO()->filesystem()->getDirectory(SHORTPIXEL_BACKUP_FOLDER);
$dir->recursiveDelete();
$plugin = basename(SHORTPIXEL_PLUGIN_DIR) . '/' . basename(SHORTPIXEL_PLUGIN_FILE);
deactivate_plugins($plugin);
}
public static function deactivateConflictingPlugin()
{
if ( ! isset($_GET['_wpnonce']) || ! wp_verify_nonce( sanitize_key($_GET['_wpnonce']), 'sp_deactivate_plugin_nonce' ) ) {
wp_nonce_ays( 'Nononce' );
}
$referrer_url = wp_get_referer();
$url = wp_get_referer();
$plugin = (isset($_GET['plugin'])) ? sanitize_text_field(wp_unslash($_GET['plugin'])) : null; // our target.
if (! is_null($plugin))
deactivate_plugins($plugin);
wp_safe_redirect($url);
die();
}
/**
* Check if TableName exists
* @param $tableName The Name of the Table without Prefix.
*/
public static function checkTableExists($tableName)
{
global $wpdb;
$tableName = $wpdb->prefix . $tableName;
$sql = $wpdb->prepare("
SHOW TABLES LIKE %s
", $tableName);
$result = intval($wpdb->query($sql));
if ($result == 0)
return false;
else {
return true;
}
}
public static function checkTables()
{
global $wpdb;
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
dbDelta(self::getFolderTableSQL());
dbDelta(self::getMetaTableSQL());
dbDelta(self::getPostMetaSQL());
self::checkIndexes();
}
private static function checkIndexes()
{
global $wpdb;
$definitions = array(
'shortpixel_meta' => array(
'path' => 'path'
),
'shortpixel_folders' => array(
'path' => 'path'
),
'shortpixel_postmeta' => array(
'attach_id' => 'attach_id',
'parent' => 'parent',
'size' => 'size',
'status' => 'status',
'compression_type' => 'compression_type'
)
);
foreach($definitions as $raw_tableName => $indexes)
{
$tableName = $wpdb->prefix . $raw_tableName;
foreach($indexes as $indexName => $fieldName)
{
// Check exists
$sql = 'SHOW INDEX FROM ' . $tableName . ' WHERE Key_name = %s';
$sql = $wpdb->prepare($sql, $indexName);
$res = $wpdb->get_row($sql);
if (is_null($res))
{
// can't prepare for those, also not any user data here.
$sql = 'CREATE INDEX ' . $indexName . ' ON ' . $tableName . ' ( ' . $fieldName . ')';
$res = $wpdb->query($sql);
}
}
}
}
private static function removeTables()
{
global $wpdb;
if (self::checkTableExists('shortpixel_folders') === true)
{
$sql = 'DROP TABLE ' . $wpdb->prefix . 'shortpixel_folders';
$wpdb->query($sql);
}
if (self::checkTableExists('shortpixel_meta') === true)
{
$sql = 'DROP TABLE ' . $wpdb->prefix . 'shortpixel_meta';
$wpdb->query($sql);
}
if (self::checkTableExists('shortpixel_postmeta') === true)
{
$sql = 'DROP TABLE ' . $wpdb->prefix . 'shortpixel_postmeta';
error_log('Dropping postmeta' . $sql);
$wpdb->query($sql);
}
}
public static function getFolderTableSQL() {
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$prefix = $wpdb->prefix;
return "CREATE TABLE {$prefix}shortpixel_folders (
id mediumint(9) NOT NULL AUTO_INCREMENT,
path varchar(512),
name varchar(150),
path_md5 char(32),
file_count int,
status SMALLINT NOT NULL DEFAULT 0,
parent SMALLINT DEFAULT 0,
ts_checked timestamp,
ts_updated timestamp,
ts_created timestamp,
PRIMARY KEY id (id)
) $charsetCollate;";
}
public static function getMetaTableSQL() {
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$prefix = $wpdb->prefix;
return "CREATE TABLE {$prefix}shortpixel_meta (
id mediumint(10) NOT NULL AUTO_INCREMENT,
folder_id mediumint(9) NOT NULL,
ext_meta_id int(10),
path varchar(512),
name varchar(150),
path_md5 char(32),
compressed_size int(10) NOT NULL DEFAULT 0,
compression_type tinyint,
keep_exif tinyint DEFAULT 0,
cmyk2rgb tinyint DEFAULT 0,
resize tinyint,
resize_width smallint,
resize_height smallint,
backup tinyint DEFAULT 0,
status SMALLINT NOT NULL DEFAULT 0,
retries tinyint NOT NULL DEFAULT 0,
message varchar(255),
ts_added timestamp,
ts_optimized timestamp,
extra_info LONGTEXT,
PRIMARY KEY sp_id (id)
) $charsetCollate;";
}
public static function getPostMetaSQL()
{
global $wpdb;
$charsetCollate = $wpdb->get_charset_collate();
$prefix = $wpdb->prefix;
$sql = "CREATE TABLE {$prefix}shortpixel_postmeta (
id bigint unsigned NOT NULL AUTO_INCREMENT ,
attach_id bigint unsigned NOT NULL,
parent bigint unsigned NOT NULL,
image_type tinyint default 0,
size varchar(200),
status tinyint default 0,
compression_type tinyint,
compressed_size int,
original_size int,
tsAdded timestamp,
tsOptimized timestamp,
extra_info LONGTEXT,
PRIMARY KEY id (id)
) $charsetCollate;";
return $sql;
}
} // InstallHelper

View File

@ -0,0 +1,885 @@
<?php
namespace ShortPixel\Helper;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\Model\Image\ImageModel as ImageModel;
use ShortPixel\Controller\ApiKeyController as ApiKeyController;
use ShortPixel\Controller\QuotaController as QuotaController;
use ShortPixel\Controller\OptimizeController as OptimizeController;
use ShortPixel\Model\AccessModel as AccessModel;
class UiHelper
{
private static $outputMode = 'admin';
private static $knowledge_url = 'https://shortpixel.com/knowledge-base/search?query='; // the URL of all knowledge.
public static function setOutputHandler($name)
{
self::$outputMode = $name;
}
public static function renderBurgerList($actions, $imageObj)
{
$output = "";
$id = $imageObj->get('id');
$primary = isset($actions['optimizethumbs']) ? 'button-primary' : '';
$output .= "<div class='sp-column-actions '>
<div class='sp-dropdown'>
<button onclick='ShortPixel.openImageMenu(event);' class='sp-dropbtn button dashicons dashicons-menu $primary' title='ShortPixel Actions'></button>";
$output .= "<div id='sp-dd-$id' class='sp-dropdown-content'>";
foreach($actions as $actionName => $actionData)
{
$link = ($actionData['type'] == 'js') ? 'javascript:' . $actionData['function'] : $actionData['function'];
$output .= "<a href='" . $link . "' class='" . esc_attr($actionName) . "' >" . esc_html($actionData['text']) . "</a>";
}
$output .= "</div> <!--sp-dropdown-content--> </div> <!--sp-dropdown--> </div> <!--sp-column-actions--> ";
return $output;
}
public static function renderSuccessText($imageObj)
{
$output = '';
//$percent = $imageObj->getMeta('improvement');
$percent = $imageObj->getImprovement();
if($percent == 999) return ;
if ($percent == 999 )
$output .= __("Reduced by X%(unknown)", 'shortpixel-image-optimiser');
if ($percent && $percent > 0)
{
$output .= __('Reduced by','shortpixel-image-optimiser') . ' <strong>' . self::formatNumber($percent,2) . '%</strong> ';
}
if (intval($percent) < 5)
$output .= __('Bonus processing','shortpixel-image-optimiser');
$type = $imageObj->getMeta('compressionType');
$output .= ' ('. self::compressionTypeToText($type) .')';
$thumbs = $imageObj->get('thumbnails');
$thumbsDone = $retinasDone = 0;
$thumbsTotal = ($thumbs) ? count($thumbs) : 0;
$retinas = $imageObj->get('retinas');
$webpsTotal = $imageObj->count('webps');
$avifsTotal = $imageObj->count('avifs');
if($retinas)
{
foreach($retinas as $retinaObj)
{
if ($retinaObj->isOptimized())
{
$retinasDone++;
}
}
}
$improvements = $imageObj->getImprovements();
$thumbTotal = $thumbsDone = 0;
if ($imageObj->get('thumbnails'))
{
$thumbsTotal = count($imageObj->get('thumbnails')); //
//$thumbsDone = (isset($improvements['thumbnails'])) ? count($improvements['thumbnails']) : 0;
$thumbsDone = $imageObj->count('optimized', array('thumbs_only' => true));
$excludedThumbs = $imageObj->count('user_excluded', array('thumbs_only' => true));
}
if (isset($improvements['thumbnails']))
{
$excluded = ($excludedThumbs > 0) ? sprintf(__('(%s excluded)', 'shortpixel-image-optimiser'), $excludedThumbs) : '';
$output .= '<div class="thumbnails optimized">';
if ($thumbsTotal > $thumbsDone)
$output .= '<div class="totals">' . sprintf(__('+%s of %s thumbnails optimized','shortpixel-image-optimiser'), self::formatNumber($thumbsDone,0), self::formatNumber($thumbsTotal,0)) . ' ' . $excluded . '</div>';
elseif ($thumbsDone > 0)
$output .= '<div class="totals">' . sprintf(__('+%s thumbnails optimized','shortpixel-image-optimiser'), self::formatNumber($thumbsDone, 0)) . ' ' . $excluded . '</div>';
$improvs = array();
uasort($improvements['thumbnails'], function ($a, $b) {
//return $b[0] <=> $a[0]; // @todo Efficient code to use once PHP 5 support is done.
if ($a == $b) {
return 0;
}
return ($b < $a) ? -1 : 1;
});
$cutoff = false;
$thumbCount = count($improvements['thumbnails']);
if ($thumbCount > 20)
{
$improvements['thumbnails'] = array_slice($improvements['thumbnails'], 0, 15, true);
$cutoff = true;
}
// Quality Check
foreach($improvements['thumbnails'] as $thumbName => $thumbStat)
{
$stat = $thumbStat[0];
if (is_numeric($stat) && $stat >= 0)
{
$improvs[$thumbName] = $stat; //self::formatNumber($stat,2);
}
}
if (count($improvs) > 0)
{
$output .= "<div class='thumb-wrapper'>";
$lowrating = 0;
foreach($improvs as $thumbName => $stat)
{
$statText = self::formatNumber($stat, 2);
$title = sprintf(__('%s : %s', 'shortpixel-image-optimiser'), $thumbName, $statText . '%');
$rating = ceil( round($stat) / 10);
if (0 == $rating)
{
$lowrating++;
continue;
}
$blocks_on = str_repeat('<span class="point checked">&nbsp;</span>', $rating);
$blocks_off = str_repeat('<span class="point">&nbsp;</span>', (10- $rating));
$output .= "<div class='thumb " . $thumbName . "' title='" . $title . "'>"
. "<span class='thumb-name'>" . $thumbName . '</span>' .
"<span class='optimize-bar'>" . $blocks_on . $blocks_off . "</span>
</div>";
}
if ($lowrating > 0)
{
$blocks_off = str_repeat('<span class="point">&nbsp;</span>', 10);
$output .= "<div class='thumb'>"
. "<span class='thumb-name'>" . sprintf(__('+ %d thumbnails ', 'shortpixel-image-optimiser'), $lowrating) . '</span>' .
"<span class='optimize-bar'>" . $blocks_off . "</span>
</div>";
}
if (true === $cutoff)
{
$output .= '<div class="thumb"><span class="cutoff">' . sprintf(__('+ %d more', 'shortpixel-image-optimiser'), ($thumbCount - 15)) . '</span></div>';
}
$output .= "</div> <!-- /thumb-wrapper -->";
}
$output .= "</div> <!-- /thumb optimized -->";
}
if ($retinasDone > 0)
{
$output .= '<div class="filetype retina">' . sprintf(__('+%s Retina images optimized','shortpixel-image-optimiser') , $retinasDone) . '</div>';
}
if ($webpsTotal > 0)
{
$output .= '<div class="filetype webp">' . sprintf(__('+%s Webp images ','shortpixel-image-optimiser') , $webpsTotal) . '</div>';
}
if ($avifsTotal > 0)
{
$output .= '<div class="filetype avif">' . sprintf(__('+%s Avif images ','shortpixel-image-optimiser') , $avifsTotal) . '</div>';
}
if ($imageObj->isSomethingOptimized() && $imageObj->isProcessable())
{
list($urls, $optimizable) = $imageObj->getCountOptimizeData('thumbnails');
list($webpUrls, $webpCount) = $imageObj->getCountOptimizeData('webp');
list($avifUrls, $avifCount) = $imageObj->getCountOptimizeData('avif');
$maxList = 10;
if (count($urls) > $maxList)
{
$urls = array_slice($urls, 0, $maxList, true);
$urls[] = '...';
}
if (count($webpUrls) > $maxList)
{
$webpUrls = array_slice($webpUrls, 0, $maxList, true);
$webpUrls[] = '...';
}
if (count($avifUrls) > $maxList)
{
$avifUrls = array_slice($avifUrls, 0, $maxList, true);
$avifUrls[] = '...';
}
if ($optimizable > 0)
{
$output .= '<div class="thumbs-todo"><h4>' . sprintf(__('%d images to optimize', 'shortpixel-image-optimiser'), $optimizable) . '</h4>';
$output .= "<span>";
foreach($urls as $optObj)
{
if ($optObj === '...')
$output .= $optObj;
else
$output .= substr($optObj, strrpos($optObj, '/')+1) . '<br>';
}
$output .= "</span>";
$output .= '</div>';
}
if ($webpCount > 0 )
{
$output .= '<div class="thumbs-todo"><h4>' . sprintf(__('%d Webp files to create', 'shortpixel-image-optimiser'), $webpCount) . '</h4>';
$output .= "<span>";
foreach($webpUrls as $optObj)
{
if ($optObj === '...')
$output .= $optObj;
else
$output .= self::convertImageTypeName(substr($optObj, strrpos($optObj, '/')+1), 'webp') . '<br>';
}
$output .= "</span>";
$output .= '</div>';
}
if ($avifCount > 0)
{
$output .= '<div class="thumbs-todo"><h4>' . sprintf(__('%d Avif files to create', 'shortpixel-image-optimiser'), $avifCount) . '</h4>';
$output .= "<span>";
foreach($avifUrls as $optObj)
{
$output .= self::convertImageTypeName(substr($optObj, strrpos($optObj, '/')+1), 'avif') . '<br>';
}
$output .= "</span>";
$output .= '</div>';
}
}
return $output;
}
public static function compressionTypeToText($type)
{
if ($type == ImageModel::COMPRESSION_LOSSLESS )
return __('Lossless', 'shortpixel-image-optimiser');
if ($type == ImageModel::COMPRESSION_LOSSY )
return __('Lossy', 'shortpixel-image-optimiser');
if ($type == ImageModel::COMPRESSION_GLOSSY )
return __('Glossy', 'shortpixel-image-optimiser');
return $type;
}
public static function getListActions($mediaItem)
{
$list_actions = array();
$id = $mediaItem->get('id');
$keyControl = ApiKeyController::getInstance();
if (! $keyControl->keyIsVerified())
{
return array(); // nothing
}
$quotaControl = QuotaController::getInstance();
$access = AccessModel::getInstance();
if (! $access->imageIsEditable($mediaItem))
{
return array();
}
if ($id === 0)
return array();
if ($mediaItem->isSomethingOptimized() )
{
list($u, $optimizable) = $mediaItem->getCountOptimizeData('thumbnails');
list($u, $optimizableWebp) = $mediaItem->getCountOptimizeData('webp');
list($u, $optimizableAvif) = $mediaItem->getCountOptimizeData('avif');
if ($mediaItem->isProcessable() && ! $mediaItem->isOptimizePrevented())
{
$action = self::getAction('optimizethumbs', $id);
if ($optimizable > 0)
{
$total = $optimizable + $optimizableWebp + $optimizableAvif;
if ($optimizableWebp > 0 || $optimizableAvif > 0)
$itemText = __('items', 'shortpixel-image-optimiser');
else {
$itemText = __('thumbnails', 'shortpixel-image-optimiser');
}
$action['text'] = sprintf(__('Optimize %s %s','shortpixel-image-optimiser'),$total, $itemText);
}
else
{
if ($optimizableWebp > 0 && $optimizableAvif > 0)
$text = sprintf(__('Optimize %s webps and %s avif','shortpixel-image-optimiser'),$optimizableWebp, $optimizableAvif);
elseif ($optimizableWebp > 0)
$text = sprintf(__('Optimize %s webps','shortpixel-image-optimiser'),$optimizableWebp);
else
$text = sprintf(__('Optimize %s avifs','shortpixel-image-optimiser'),$optimizableAvif);
$action['text'] = $text;
}
$list_actions['optimizethumbs'] = $action;
}
if ($mediaItem->isRestorable())
{
if ($mediaItem->get('type') == 'custom')
{
if ($mediaItem->getExtension() !== 'pdf') // no support for this
$list_actions['comparer'] = self::getAction('compare-custom', $id);
}
else
{
// PDF without thumbnail can't be compared.
$showCompare = true;
if ($mediaItem->getExtension() == 'pdf')
{
if (! $mediaItem->getThumbnail('full'))
$showCompare = false;
elseif(! $mediaItem->getThumbnail('full')->hasBackup())
$showCompare = false;
}
if ($showCompare)
$list_actions['comparer'] = self::getAction('compare', $id);
}
if ($mediaItem->isRestorable())
{
$compressionType = $mediaItem->getMeta('compressionType');
switch($compressionType)
{
case ImageModel::COMPRESSION_LOSSLESS:
$list_actions['reoptimize-lossy'] = self::getAction('reoptimize-lossy', $id);
$list_actions['reoptimize-glossy'] = self::getAction('reoptimize-glossy', $id);
break;
case ImageModel::COMPRESSION_LOSSY:
$list_actions['reoptimize-lossless'] = self::getAction('reoptimize-lossless', $id);
$list_actions['reoptimize-glossy'] = self::getAction('reoptimize-glossy', $id);
break;
case ImageModel::COMPRESSION_GLOSSY:
$list_actions['reoptimize-lossy'] = self::getAction('reoptimize-lossy', $id);
$list_actions['reoptimize-lossless'] = self::getAction('reoptimize-lossless', $id);
break;
}
if ($mediaItem->get('type') === 'media')
{
$list_actions['reoptimize-smartcrop'] = self::getAction('reoptimize-smartcrop', $id, array('compressionType' => $compressionType));
$list_actions['reoptimize-smartcropless'] = self::getAction('reoptimize-smartcropless', $id, array('compressionType' => $compressionType));
}
$list_actions['restore'] = self::getAction('restore', $id);
} // isRestorable
else
{
}
} // hasBackup
if (\wpSPIO()->env()->is_debug && $mediaItem->get('type') == 'media')
{
$list_actions['redo_legacy'] = self::getAction('redo_legacy', $id);
}
} //isOptimized
if(! $quotaControl->hasQuota())
{
$remove = array('reoptimize-lossy' => '', 'reoptimize-glossy' => '', 'reoptimize-lossless' => '', 'optimizethumbs' => '');
$list_actions = array_diff_key($list_actions, $remove);
}
return $list_actions;
}
public static function getActions($mediaItem)
{
$actions = array();
$id = $mediaItem->get('id');
$quotaControl = QuotaController::getInstance();
$optimizeController = new OptimizeController();
$keyControl = ApiKeyController::getInstance();
if (! $keyControl->keyIsVerified())
{
return array(); // nothing
}
$access = AccessModel::getInstance();
if (! $access->imageIsEditable($mediaItem))
{
return array();
}
if ($id === 0)
return array();
if(! $quotaControl->hasQuota())
{
$actions['extendquota'] = self::getAction('extendquota', $id);
$actions['checkquota'] = self::getAction('checkquota', $id);
}
elseif($mediaItem->isProcessable() && ! $mediaItem->isSomethingOptimized() && ! $mediaItem->isOptimizePrevented() && ! $optimizeController->isItemInQueue($mediaItem))
{
$actions['optimize'] = self::getAction('optimize', $id);
$actions['markCompleted'] = self::getAction('markCompleted', $id);
}
elseif ($mediaItem->isUserExcluded() && false === $mediaItem->isSomethingOptimized())
{
$actions['optimize'] = self::getAction('forceOptimize', $id);
}
return $actions;
}
public static function getStatusText($mediaItem)
{
$keyControl = ApiKeyController::getInstance();
$quotaControl = QuotaController::getInstance();
$optimizeController = new OptimizeController();
$settings = \wpSPIO()->settings();
$text = '';
if (! $keyControl->keyIsVerified())
{
$text = __('Invalid API Key. <a href="options-general.php?page=wp-shortpixel-settings">Check your Settings</a>','shortpixel-image-optimiser');
}
// This basically happens when a NextGen gallery is not added to Custom Media.
elseif ($mediaItem->get('id') === 0)
{
if ($mediaItem->isProcessable(true) === false)
{
$text = __('Not Processable: ','shortpixel_image_optimiser');
$text .= $mediaItem->getProcessableReason();
}
else {
if (\wpSPIO()->env()->has_nextgen && false == $settings->includeNextGen)
{
$text = __('This image was not found in our database. Enable "Optimize nextgen galleries" in the settings, or add this folder manually. ', 'shortpixel-image-optimiser');
}
else {
$text = __('This image was not found in our database. Refresh folders, or add this gallery', 'shortpixel-image-optimiser');
}
}
}
elseif ($mediaItem->isSomethingOptimized())
{
$text = UiHelper::renderSuccessText($mediaItem);
}
elseif (false === $mediaItem->isProcessable() )
{
$text = __('Not Processable: ','shortpixel_image_optimiser');
$text .= $mediaItem->getProcessableReason();
}
elseif (! $mediaItem->exists())
{
$text = __('File does not exist.','shortpixel-image-optimiser');
}
elseif ($mediaItem->getMeta('status') < 0)
{
$text = $mediaItem->getMeta('errorMessage');
}
elseif( $optimizeController->isItemInQueue($mediaItem) === true)
{
$text = '<p>' . __('This item is waiting to be processed', 'shortpixel-image-optimiser') . '</p>';
$action = self::getAction('cancelOptimize', $mediaItem->get('id'));
$text .= '<p><a href="javascript:' . $action['function'] . '">' . $action['text'] . '</a></p>';
}
if ($mediaItem->isOptimizePrevented() !== false)
{
$retry = self::getAction('retry', $mediaItem->get('id'));
$unmark = self::getAction('unMarkCompleted', $mediaItem->get('id'));
$redo_legacy = false;
if ($mediaItem->get('type') == 'media')
{
$was_converted = get_post_meta($mediaItem->get('id'), '_shortpixel_was_converted', true);
$updateTs = 1656892800; // July 4th 2022 - 00:00 GMT
if ($was_converted < $updateTs)
{
$meta = $mediaItem->getWPMetaData();
if (is_array($meta) && isset($meta['ShortPixel']))
{
$redo_legacy = self::getAction('redo_legacy', $mediaItem->get('id'));
}
}
}
$status = $mediaItem->getMeta('status');
$text = ''; // reset text
if (ImageModel::FILE_STATUS_MARKED_DONE == $status)
{
$text .= "<div class='shortpixel-image-notice'>" . esc_html($mediaItem->getReason('processable'));
$text .= "<p class='shortpixel-error-reset'>" . sprintf(__('%s Click to unmark as completed %s', 'shortpixel-image-optimiser'), '<a href="javascript:' . $unmark['function'] . '">', '</a>') . '</p>';
$text .= '</div>';
}
else {
$text .= "<div class='shortpixel-image-error'>" . esc_html($mediaItem->getReason('processable'));
$text .= "<span class='shortpixel-error-reset'>" . sprintf(__('After you have fixed this issue, you can %s click here to retry %s', 'shortpixel-image-optimiser'), '<a href="javascript:' . $retry['function'] . '">', '</a>') . '</span>';
$text .= '</div>';
}
if ($redo_legacy !== false)
{
$text .= "<div class='shortpixel-image-error'><span class='shortpixel-error-reset'>";
$text .= sprintf(esc_html__('It seems you have older converted legacy data, which might cause this issue. You can try to %s %s %s . If nothing changes, this is not the cause. ','shortpixel-image-optimiser'), '<a href="javascript:' . $redo_legacy['function'] . '">', $redo_legacy['text'], '</a>');
$text .= "</span></div>";
}
}
return $text;
}
// Defines all possible actions in the Ui
public static function getAction($name, $id, $args = array())
{
$action = array('function' => '', 'type' => '', 'text' => '', 'display' => '');
$keyControl = ApiKeyController::getInstance();
$compressionType = isset($args['compressionType']) ? $args['compressionType'] : null;
switch($name)
{
case 'optimize':
$action['function'] = 'window.ShortPixelProcessor.screen.Optimize(' . $id . ')';
$action['type'] = 'js';
$action['text'] = __('Optimize Now', 'shortpixel-image-optimiser');
$action['display'] = 'button';
break;
case 'forceOptimize':
$action['function'] = 'window.ShortPixelProcessor.screen.Optimize(' . $id . ', true)';
$action['type'] = 'js';
$action['text'] = __('Override exclusions and optimize now', 'shortpixel-image-optimiser');
$action['display'] = 'button';
break;
case 'cancelOptimize':
$action['function'] = 'window.ShortPixelProcessor.screen.CancelOptimizeItem(' . $id . ')';
$action['type'] = 'js';
$action['text'] = __('Cancel item optimization', 'shortpixel-image-optimiser');
$action['display'] = 'button';
break;
case 'markCompleted':
$action['function'] = 'window.ShortPixelProcessor.screen.MarkCompleted(' . $id . ')';
$action['type'] = 'js';
$action['text'] = __('Mark as Completed', 'shortpixel-image-optimiser');
$action['display'] = 'button-secondary';
$action['layout'] = 'paragraph';
$action['title'] = __('This will cause the plugin to skip this image for optimization', 'shortpixel-image-optimiser');
break;
case 'unMarkCompleted':
$action['function'] = 'window.ShortPixelProcessor.screen.UnMarkCompleted(' . $id . ')';
$action['type'] = 'js';
$action['text'] = __('Click to unmark this item as done', 'shortpixel-image-optimiser');
$action['display'] = 'js';
break;
case 'optimizethumbs':
$action['function'] = 'window.ShortPixelProcessor.screen.Optimize(' . $id . ');';
$action['type'] = 'js';
$action['text'] = '';
$action['display'] = 'inline';
break;
case 'retry':
$action['function'] = 'window.ShortPixelProcessor.screen.Optimize(' . $id . ');';
$action['type'] = 'js';
$action['text'] = __('Retry', 'shortpixel-image-optimiser') ;
$action['display'] = 'button';
break;
case 'redo_legacy':
$action['function'] = 'window.ShortPixelProcessor.screen.RedoLegacy(' . $id . ');';
$action['type'] = 'js';
$action['text'] = __('Redo Conversion', 'shortpixel-image-optimiser') ;
$action['display'] = 'button';
break;
case 'restore':
$action['function'] = 'window.ShortPixelProcessor.screen.RestoreItem(' . $id . ');';
$action['type'] = 'js';
$action['text'] = __('Restore backup','shortpixel-image-optimiser');
$action['display'] = 'inline';
break;
case 'compare':
$action['function'] = 'ShortPixel.loadComparer(' . $id . ')';
$action['type'] = 'js';
$action['text'] = __('Compare', 'shortpixel-image-optimiser');
$action['display'] = 'inline';
break;
case 'compare-custom':
$action['function'] = 'ShortPixel.loadComparer(' . $id . ',"custom")';
$action['type'] = 'js';
$action['text'] = __('Compare', 'shortpixel-image-optimiser');
$action['display'] = 'inline';
break;
case 'reoptimize-glossy':
$action['function'] = 'window.ShortPixelProcessor.screen.ReOptimize(' . $id . ',' . ImageModel::COMPRESSION_GLOSSY . ')';
$action['type'] = 'js';
$action['text'] = __('Re-optimize Glossy','shortpixel-image-optimiser') ;
$action['display'] = 'inline';
break;
case 'reoptimize-lossy':
$action['function'] = 'window.ShortPixelProcessor.screen.ReOptimize(' . $id . ',' . ImageModel::COMPRESSION_LOSSY . ')';
$action['type'] = 'js';
$action['text'] = __('Re-optimize Lossy','shortpixel-image-optimiser');
$action['display'] = 'inline';
break;
case 'reoptimize-lossless':
$action['function'] = 'window.ShortPixelProcessor.screen.ReOptimize(' . $id . ',' . ImageModel::COMPRESSION_LOSSLESS . ')';
$action['type'] = 'js';
$action['text'] = __('Re-optimize Lossless','shortpixel-image-optimiser');
$action['display'] = 'inline';
break;
case 'reoptimize-smartcrop':
$action['function'] = 'window.ShortPixelProcessor.screen.ReOptimize(' . $id . ',' . $compressionType . ',' . ImageModel::ACTION_SMARTCROP . ')';
$action['type'] = 'js';
$action['text'] = __('Re-optimize with SmartCrop','shortpixel-image-optimiser');
$action['display'] = 'inline';
break;
case 'reoptimize-smartcropless':
$action['function'] = 'window.ShortPixelProcessor.screen.ReOptimize(' . $id . ',' . $compressionType . ',' . ImageModel::ACTION_SMARTCROPLESS . ')';
$action['type'] = 'js';
$action['text'] = __('Re-optimize without SmartCrop','shortpixel-image-optimiser');
$action['display'] = 'inline';
break;
case 'extendquota':
$action['function'] = 'https://shortpixel.com/login/'. $keyControl->getKeyForDisplay();
$action['type'] = 'button';
$action['text'] = __('Extend Quota','shortpixel-image-optimiser');
$action['display'] = 'button';
break;
case 'checkquota':
$action['function'] = 'ShortPixel.checkQuota()';
$action['type'] = 'js';
$action['display'] = 'button';
$action['text'] = __('Check&nbsp;&nbsp;Quota','shortpixel-image-optimiser');
break;
}
return $action;
}
public static function getConvertErrorReason($error)
{
switch($error)
{
case -1: //ERROR_LIBRARY:
$reason = __('PNG Library is not present or not working', 'shortpixel-image-optimiser');
break;
case -2: //ERROR_PATHFAIL:
$reason = __('Could not create path', 'shortpixel-image-optimiser');
break;
case -3: //ERROR_RESULTLARGER:
$reason = __('Result file is larger','shortpixel-image-optimiser');
break;
case -4: // ERROR_WRITEERROR
$reason = __('Could not write result file', 'shortpixel-image-optimiser');
break;
case -5: // ERROR_BACKUPERROR
$reason = __('Could not create backup', 'shortpixel-image-optimiser');
break;
case -6: // ERROR_TRANSPARENT
$reason = __('Image is transparent', 'shortpixel-image-optimiser');
break;
default:
$reason = sprintf(__('Unknown error %s', 'shortpixel-image-optimiser'), $error);
break;
}
$message = sprintf(__('Not converted: %s ', 'shortpixel-image-optimiser'), $reason);
return $message;
}
public static function getKBSearchLink($subject)
{
return esc_url(self::$knowledge_url . sanitize_text_field($subject));
}
// @param MediaLibraryModel Object $imageItem
// @param String $size Preferred size
// @param String Preload The preloader tries to guess what the preview might be for a more smooth process. Ignore optimize / backup
public static function findBestPreview($imageItem, $size = 800, $preload = false)
{
$closestObj = $imageItem;
// set the standard.
if ($imageItem->getExtension() == 'pdf') // try not to select non-showable extensions.
$bestdiff = 0;
else
$bestdiff = abs($imageItem->get('width') - $size);
$thumbnails = $imageItem->get('thumbnails');
if (! is_array($thumbnails))
{
return $closestObj; // nothing more to do.
}
foreach($thumbnails as $thumbnail)
{
if (! $preload && (! $thumbnail->isOptimized() || ! $thumbnail->hasBackup()))
continue;
$diff = abs($thumbnail->get('width') - $size);
if ($diff < $bestdiff)
{
$closestObj = $thumbnail;
$bestdiff = $diff;
}
}
return $closestObj;
}
public static function formatTS($ts)
{
//$format = get_option('date_format') .' @ ' . date_i18n(get_option('time_format');
if (function_exists('wp_date'))
{
$date = wp_date(get_option('date_format'), $ts);
$date .= ' @ ' . wp_date(get_option('time_format'), $ts);
}
else
{
$date = date_i18n(get_option('date_format'), $ts);
$date .= ' @ ' . date_i18n(get_option('time_format'), $ts);
}
return $date;
}
public static function formatBytes($bytes, $precision = 2) {
$units = array('B', 'KB', 'MB', 'GB', 'TB');
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return number_format_i18n(round($bytes, $precision), $precision) . ' ' . $units[$pow];
}
public static function formatNumber($number, $precision = 2)
{
global $wp_locale;
$decimalpoint = isset($wp_locale->number_format['decimal_point']) ? $wp_locale->number_format['decimal_point'] : false;
$number = number_format_i18n( (float) $number, $precision);
$hasDecimal = (strpos($number, $decimalpoint) === false) ? false : true;
// Don't show trailing zeroes if number is a whole unbroken number. -> string comparison because number_format_i18n returns string.
if ($decimalpoint !== false && $hasDecimal && substr($number, strpos($number, $decimalpoint) + 1) === '00')
{
$number = substr($number, 0, strpos($number, $decimalpoint));
}
// Some locale's have no-breaking-space as thousands separator. This doesn't work well in JS / Cron Shell so replace with space.
$number = str_replace('&nbsp;', ' ', $number);
return $number;
}
public static function formatDate( $date ) {
if ( '0000-00-00 00:00:00' === $date->format('Y-m-d ') ) {
$h_time = '';
} else {
$time = $date->format('U'); //get_post_time( 'G', true, $post, false );
if ( ( abs( $t_diff = time() - $time ) ) < DAY_IN_SECONDS ) {
if ( $t_diff < 0 ) {
$h_time = sprintf( __( '%s from now' ), human_time_diff( $time ) );
} else {
$h_time = sprintf( __( '%s ago' ), human_time_diff( $time ) );
}
} else {
$h_time = $date->format( 'Y/m/d' );
}
}
return $h_time;
}
protected static function convertImageTypeName($name, $type)
{
if ($type == 'webp')
{
$is_double = \wpSPIO()->env()->useDoubleWebpExtension();
}
if ($type == 'avif')
{
$is_double = \wpSPIO()->env()->useDoubleAvifExtension();
}
if ($is_double)
{
return $name . '.' . $type;
}
else
{
return substr($name, 0, strrpos($name, '.')) . '.' . $type;
}
}
/* Strings on settings page that need to be available for both JS and PHP */
public static function getSettingsStrings($name = false)
{
$strings = array(
);
$exclusion_types = array(
'name' => __('Name', 'shortpixel-image-optimiser'),
'path' => __('Path', 'shortpixel-image-optimiser'),
'size' => __('Size', 'shortpixel-image-optimiser'),
);
$exclusion_apply = array(
'all' => __('All', 'shortpixel-image-optimiser'),
'only-thumbs' => __('Only Thumbnails', 'shortpixel-image-optimiser'),
'only-custom' => __('Only Custom Media Images', 'shortpixel-image-optimiser'),
'selected-thumbs' => __('Selected Images', 'shortpixel-image-optimiser'),
);
$strings['exclusion_types'] = $exclusion_types;
$strings['exclusion_apply'] = $exclusion_apply;
if ($name !== false && isset($strings[$name]))
{
return $strings[$name];
}
return $strings;
}
} // class

View File

@ -0,0 +1,277 @@
<?php
namespace ShortPixel\Helper;
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// Our newest Tools class
class UtilHelper
{
public static function getPostMetaTable()
{
global $wpdb;
return $wpdb->prefix . 'shortpixel_postmeta';
}
public static function shortPixelIsPluginActive($plugin) {
$activePlugins = apply_filters( 'active_plugins', get_option( 'active_plugins', array()));
if ( is_multisite() ) {
$activePlugins = array_merge($activePlugins, get_site_option( 'active_sitewide_plugins'));
}
return in_array( $plugin, $activePlugins);
}
static public function timestampToDB($timestamp)
{
return date("Y-m-d H:i:s", $timestamp);
}
static public function DBtoTimestamp($date)
{
return strtotime($date);
}
public static function getWordPressImageSizes()
{
global $_wp_additional_image_sizes;
$sizes_names = get_intermediate_image_sizes();
$sizes = array();
foreach ( $sizes_names as $size ) {
$sizes[ $size ][ 'width' ] = intval( get_option( "{$size}_size_w" ) );
$sizes[ $size ][ 'height' ] = intval( get_option( "{$size}_size_h" ) );
$sizes[ $size ][ 'crop' ] = get_option( "{$size}_crop" ) ? get_option( "{$size}_crop" ) : false;
$sizes[ $size ][ 'nice-name'] = ucfirst($size);
}
if(function_exists('wp_get_additional_image_sizes')) {
$sizes = array_merge($sizes, wp_get_additional_image_sizes());
} elseif(is_array($_wp_additional_image_sizes)) {
$sizes = array_merge($sizes, $_wp_additional_image_sizes);
}
$sizes = apply_filters('shortpixel/settings/image_sizes', $sizes);
return $sizes;
}
// wp_normalize_path doesn't work for windows installs in some situations, so we can use it, but we still want some of the functions.
public static function spNormalizePath($path)
{
$path = preg_replace( '|(?<=.)/+|', '/', $path );
return $path;
}
// Copy of private https://developer.wordpress.org/reference/functions/_wp_relative_upload_path/
public static function getRelativeUploadPath($path)
{
$new_path = $path;
$uploads = wp_get_upload_dir();
if ( 0 === strpos( $new_path, $uploads['basedir'] ) ) {
$new_path = str_replace( $uploads['basedir'], '', $new_path );
$new_path = ltrim( $new_path, '/' );
}
return $new_path;
}
public static function getExclusions($args = array())
{
$defaults = array(
'filter' => false,
'thumbname' => null,
'is_thumbnail' => false,
'is_custom' => false,
);
$args = wp_parse_args($args, $defaults);
$patterns = \wpSPIO()->settings()->excludePatterns;
$matches = array();
if (false === is_array($patterns))
{
return array();
}
foreach($patterns as $index => $pattern)
{
if (! isset($pattern['apply']))
{
$patterns[$index]['apply'] = 'all';
}
if (true === $args['filter'])
{
if (true === self::matchExclusion($patterns[$index], $args))
{
$matches[] = $pattern;
}
}
}
if (true === $args['filter'])
{
return $matches;
}
else
return $patterns;
}
protected static function matchExclusion($pattern, $options)
{
$apply = $pattern['apply'];
$thumblist = isset($pattern['thumblist']) ? $pattern['thumblist'] : array();
$bool = false;
if ($apply === 'all')
{
$bool = true;
}
elseif ($apply == 'only-thumbs' && true === $options['is_thumbnail'])
{
$bool = true;
}
elseif ($apply == 'only-custom' && true === $options['is_custom'])
{
$bool = true;
}
elseif (count($thumblist) > 0 && ! is_null($options['thumbname']))
{
$thumbname = $options['thumbname'];
if (in_array($thumbname, $thumblist))
{
$bool = true;
}
}
return $bool;
}
public static function alterHtaccess($webp = false, $avif = false)
{
// [BS] Backward compat. 11/03/2019 - remove possible settings from root .htaccess
/* Plugin init is before loading these admin scripts. So it can happen misc.php is not yet loaded */
if (! function_exists('insert_with_markers'))
{
Log::addWarn('AlterHtaccess Called before WP init');
return;
//require_once( ABSPATH . 'wp-admin/includes/misc.php' );
}
$upload_dir = wp_upload_dir();
$upload_base = trailingslashit($upload_dir['basedir']);
if (false === $webp && false === $avif ) {
insert_with_markers( get_home_path() . '.htaccess', 'ShortPixelWebp', '');
// Only empty these tags if the file actually exist, they are created by SPIO.
if (file_exists($upload_base . '.htaccess'))
{
insert_with_markers( $upload_base . '.htaccess', 'ShortPixelWebp', '');
}
if (file_exists(trailingslashit(WP_CONTENT_DIR) . '.htaccess'))
{
insert_with_markers( trailingslashit(WP_CONTENT_DIR) . '.htaccess', 'ShortPixelWebp', '');
}
} else {
$avif_rules = '
<IfModule mod_rewrite.c>
RewriteEngine On
##### Directives for delivering AVIF files, if they exist #####
# Does the browser support avif?
RewriteCond %{HTTP_ACCEPT} image/avif
# AND is the request a jpg or png? (also grab the basepath %1 to match in the next rule)
RewriteCond %{REQUEST_URI} ^(.+)\.(?:jpe?g|png|gif)$
# AND does a .avif image exist?
RewriteCond %{DOCUMENT_ROOT}/%1.avif -f
# THEN send the avif image and set the env var avif
RewriteRule (.+)\.(?:jpe?g|png)$ $1.avif [NC,T=image/avif,E=avif,L]
# Does the browser support avif?
RewriteCond %{HTTP_ACCEPT} image/avif
# AND is the request a jpg or png? (also grab the basepath %1 to match in the next rule)
RewriteCond %{REQUEST_URI} ^(.+)\.(?:jpe?g|png|gif)$
# AND does a .jpg.avif image exist?
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI}.avif -f
# THEN send the avif image and set the env var avif
RewriteRule ^(.+)$ $1.avif [NC,T=image/avif,E=avif,L]
</IfModule>
<IfModule mod_headers.c>
# If REDIRECT_avif env var exists, append Accept to the Vary header
Header append Vary Accept env=REDIRECT_avif
</IfModule>
<IfModule mod_mime.c>
AddType image/avif .avif
</IfModule>
';
$webp_rules = '
<IfModule mod_rewrite.c>
RewriteEngine On
##### TRY FIRST the file appended with .webp (ex. test.jpg.webp) #####
# Is the browser Chrome?
RewriteCond %{HTTP_USER_AGENT} Chrome [OR]
# OR Is request from Page Speed
RewriteCond %{HTTP_USER_AGENT} "Google Page Speed Insights" [OR]
# OR does this browser explicitly support webp
RewriteCond %{HTTP_ACCEPT} image/webp
# AND NOT MS EDGE 42/17 - doesnt work.
RewriteCond %{HTTP_USER_AGENT} !Edge/17
# AND is the request a jpg, png or gif?
RewriteCond %{REQUEST_URI} ^(.+)\.(?:jpe?g|png|gif)$
# AND does a .ext.webp image exist?
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI}.webp -f
# THEN send the webp image and set the env var webp
RewriteRule ^(.+)$ $1.webp [NC,T=image/webp,E=webp,L]
##### IF NOT, try the file with replaced extension (test.webp) #####
RewriteCond %{HTTP_USER_AGENT} Chrome [OR]
RewriteCond %{HTTP_USER_AGENT} "Google Page Speed Insights" [OR]
RewriteCond %{HTTP_ACCEPT} image/webp
RewriteCond %{HTTP_USER_AGENT} !Edge/17
# AND is the request a jpg, png or gif? (also grab the basepath %1 to match in the next rule)
RewriteCond %{REQUEST_URI} ^(.+)\.(?:jpe?g|png|gif)$
# AND does a .webp image exist?
RewriteCond %{DOCUMENT_ROOT}/%1.webp -f
# THEN send the webp image and set the env var webp
RewriteRule (.+)\.(?:jpe?g|png|gif)$ $1.webp [NC,T=image/webp,E=webp,L]
</IfModule>
<IfModule mod_headers.c>
# If REDIRECT_webp env var exists, append Accept to the Vary header
Header append Vary Accept env=REDIRECT_webp
</IfModule>
<IfModule mod_mime.c>
AddType image/webp .webp
</IfModule>
' ;
$rules = '';
// if ($avif)
$rules .= $avif_rules;
// if ($webp)
$rules .= $webp_rules;
insert_with_markers( get_home_path() . '.htaccess', 'ShortPixelWebp', $rules);
/** In uploads and on, it needs Inherit. Otherwise things such as the 404 error page will not be loaded properly
* since the WP rewrite will not be active at that point (overruled) **/
$rules = str_replace('RewriteEngine On', 'RewriteEngine On' . PHP_EOL . 'RewriteOptions Inherit', $rules);
// Can shortcircuit (return false) the creation of subdirectory Htaccess files if this causes issues and is not needed.
$bool = apply_filters('shortpixel/install/write_deep_htaccess', true);
if (true === $bool)
{
insert_with_markers( $upload_base . '.htaccess', 'ShortPixelWebp', $rules);
insert_with_markers( trailingslashit(WP_CONTENT_DIR) . '.htaccess', 'ShortPixelWebp', $rules);
}
}
}
} // class

View File

@ -0,0 +1,153 @@
<?php
namespace ShortPixel;
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Model
{
protected $model = array();
public function getData()
{
$data = array();
foreach($this->model as $item => $options)
{
$data[$item] = $this->{$item};
}
return $data;
}
public function getModel()
{
return array_keys($this->model); // only the variable names are public.
}
protected function sanitize($name, $value)
{
if (! isset($this->model[$name]))
return null;
// if no sanitize method is set, default to strictest string.
$sanitize = isset($this->model[$name]['s']) ? $this->model[$name]['s'] : 'string';
switch($sanitize)
{
case "string":
$value = $this->sanitizeString($value);
break;
case "int":
$value = $this->sanitizeInteger($value);
break;
case "boolean":
$value = $this->sanitizeBoolean($value);
break;
case 'array':
case 'Array':
$value = $this->sanitizeArray($value);
break;
case 'exception': // for exceptional situations. The data will not be sanitized! Need to do this elsewhere.
return $value;
break;
case 'skip': // skips should not be in any save candidate and not be sanitized.
return null;
break;
}
return $value;
}
/** Sanitize the passed post data against the model attribute formats.
*
* @param Array $post The Post data
* @param boolean $missing If fields are missing, include them empty in the output
* @return Array Sanitized Post Data
*/
public function getSanitizedData($post, $missing = true)
{
$postData = array();
foreach($post as $name => $value)
{
$name = sanitize_text_field($name);
$value = $this->sanitize($name, $value);
if ($value !== null)
$postData[$name] = $value;
else {
Log::addWarn("Provided field $name not part of model " . get_class() );
}
}
if ($missing)
{
$model_fields = $this->getModel();
$post_fields = array_keys($postData);
$missing_fields = array_diff($model_fields, $post_fields);
foreach($missing_fields as $index => $field_name)
{
$field_name = sanitize_text_field($field_name);
$type = $this->getType($field_name);
if ($type === 'boolean')
{
$postData[$field_name] = 0;
}
elseif ($type !== false && $type !== 'skip')
{
$postData[$field_name] = '';
}
}
}
return $postData;
}
public function getType($name)
{
if (! isset($this->model[$name]))
{
return null;
Log::addWarn("Provided field $name not part of model " . get_class() );
}
$type = isset($this->model[$name]['s']) ? $this->model[$name]['s'] : false;
return $type;
}
public function sanitizeString($string)
{
return (string) sanitize_text_field($string);
}
public function sanitizeInteger($int)
{
return intval($int);
}
public function sanitizeBoolean($bool)
{
return ($bool) ? true : false;
}
public function sanitizeArray($array)
{
if (! is_array($array))
{
Log::addWarn('Field is of type Array, but Array not provided');
return null;
}
$new_array = array();
foreach($array as $key => $value)
{
$newkey = $this->sanitizeString($key);
$newval = $this->sanitizeString($value);
$new_array[$newkey] = $newval ;
}
return $new_array;
}
}

View File

@ -0,0 +1,140 @@
<?php
namespace ShortPixel\Model;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Controller\QuotaController as QuotaController;
// Central place for user / access checking, roles etc.
class AccessModel
{
// Instance of class
private static $instance;
// Array of known SPIO Capabilities mapped to WordPress variants
private $caps;
// int . The current user id
private $current_user_id;
public function __construct()
{
$this->setDefaultPermissions();
}
protected function setDefaultPermissions()
{
$spioCaps = array(
'notices' => 'activate_plugins', // used in AdminNoticesController
'quota-warning' => 'manage_options', // used in AdminController
'image_all' => 'edit_others_posts',
'image_user' => 'edit_post',
'custom_all' => 'edit_others_posts',
'is_admin_user' => 'manage_options',
'actions' => array(),
);
$spioCaps = apply_filters('shortpixel/init/permissions', $spioCaps);
// $this->cap_actions = bla.
$this->caps = $spioCaps;
}
public static function getInstance()
{
if (is_null(self::$instance))
{
self::$instance = new AccessModel();
}
return self::$instance;
}
/** Check for allowing a notice
* @param $notice Object of type notice.
*/
public function noticeIsAllowed($notice)
{
$cap = $this->getCap('notices');
return $this->user()->has_cap($cap);
}
/*
@param SPIO capability to check again the user WordPress permissions.
*/
public function userIsAllowed($cap)
{
$cap = $this->getCap($cap);
return $this->user()->has_cap($cap);
}
public function imageIsEditable($mediaItem)
{
$type = $mediaItem->get('type');
if ($type == 'custom' )
{
return $this->user()->has_cap($this->getCap('custom_all'), $mediaItem->get('id'));
}
elseif ($type == 'media') // media
{
if ($this->user()->has_cap($this->getCap('image_all'), $mediaItem->get('id')) || $this->user()->has_cap($this->getCap('image_user'), $mediaItem->get('id')) )
{
return true;
}
}
return false;
}
public function isFeatureAvailable($name)
{
$available = true;
switch($name)
{
case 'avif':
$quotaControl = QuotaController::getInstance();
$quota = $quotaControl->getQuota();
if (property_exists($quota, 'unlimited') && $quota->unlimited === true)
{
$available = false;
}
break;
case 'webp':
default:
break;
}
return $available;
}
protected function user()
{
return wp_get_current_user();
}
/** Find the needed capability
*
* This translates a SPIO capability into the associated cap that is registered within WordPress.
*
* @param $cap The required Capability
* @param $default The default value if not found. This is defaults to an admin cap to prevent access leaking.
*/
protected function getCap($cap, $default = 'manage_options')
{
if (isset($this->caps[$cap]))
return $this->caps[$cap];
else
return $default;
}
} // CLASS

View File

@ -0,0 +1,155 @@
<?php
namespace ShortPixel\Model;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Notices\NoticeController as Notice;
use ShortPixel\Notices\NoticeController as Notices;
abstract class AdminNoticeModel
{
protected $key; // abstract
protected $notice;
protected $errorLevel = 'normal';
protected $suppress_delay = YEAR_IN_SECONDS;
protected $callback;
protected $include_screens = array();
protected $exclude_screens = array();
protected $data;
// No stuff loading here, low init
public function __construct()
{
}
// The main init, ty.
public function load()
{
$noticeController = Notices::getInstance();
$notice = $noticeController->getNoticeByID($this->key);
if (is_object($notice))
{
$this->notice = $notice;
}
if (is_object($notice) && $notice->isDismissed())
{
return false;
}
if (is_null($this->notice) && $this->checkTrigger() === true)
{
$this->add();
}
elseif ( is_object($this->notice) && $this->checkReset() === true)
{
$this->reset();
}
}
public function getKey()
{
return $this->key;
}
public function reset($key = null)
{
$key = (is_null($key)) ? $this->key : $key;
Notices::removeNoticeByID($key);
}
protected function checkReset()
{
return false;
}
// For when trigger condition is not applicable.
public function addManual($args = array())
{
foreach($args as $key => $val)
{
$this->addData($key, $val);
}
$this->add();
}
public function getNoticeObj()
{
return $this->notice; // can be null!
}
// Proxy for noticeModel dismissed
public function isDismissed()
{
$notice = $this->getNoticeObj();
if (is_null($notice) || $notice->isDismissed() === false)
return false;
return true;
}
protected function add()
{
switch ($this->errorLevel)
{
case 'warning':
$notice = Notices::addWarning($this->getMessage());
break;
case 'normal';
default:
$notice = Notices::addNormal($this->getMessage());
break;
}
/// Todo implement include / exclude screens here.
if (count($this->exclude_screens) > 0)
{
$notice->limitScreens('exclude', $this->exclude_screens);
}
if (count($this->include_screens) > 0)
{
$notice->limitScreens('include', $this->include_screens);
}
if (! is_null($this->callback))
Notices::makePersistent($notice, $this->key, $this->suppress_delay, $this->callback);
else
Notices::makePersistent($notice, $this->key, $this->suppress_delay);
$this->notice = $notice;
}
protected function addData($name, $value)
{
$this->data[$name] = $value;
}
protected function getData($name)
{
if (isset($this->data[$name]))
{
return $this->data[$name];
}
return false;
}
// ?? abstract proteced removeCondition();
abstract protected function checkTrigger();
abstract protected function getMessage();
}

View File

@ -0,0 +1,44 @@
<?php
namespace ShortPixel\Model\AdminNotices;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class ApiNotice extends \ShortPixel\Model\AdminNoticeModel
{
protected $key = 'MSG_NO_APIKEY';
public function load()
{
$activationDate = \wpSPIO()->settings()->activationDate;
if (! $activationDate)
{
$activationDate = time();
\wpSPIO()->settings()->activationDate = $activationDate;
}
parent::load();
}
protected function checkTrigger()
{
if (\wpSPIO()->settings()->verifiedKey)
{
return false;
}
// If not key is verified.
return true;
}
protected function getMessage()
{
$message = "<p>" . __('To start the optimization process, you need to validate your API key on the '
. '<a href="options-general.php?page=wp-shortpixel-settings">ShortPixel Settings</a> page in your WordPress admin.','shortpixel-image-optimiser') . "
</p>
<p>" . __('If you do not have an API key yet, just fill out the form and a key will be created.','shortpixel-image-optimiser') . "</p>";
return $message;
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace ShortPixel\Model\AdminNotices;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\Controller\AdminNoticesController as AdminNoticesController;
class ApiNoticeRepeat extends \ShortPixel\Model\AdminNoticeModel
{
protected $key = 'MSG_NO_APIKEY_REPEAT';
protected $errorLevel = 'warning';
protected function checkTrigger()
{
if (\wpSPIO()->settings()->verifiedKey)
{
return false;
}
// Is set by general ApiNotice. If not set, don't bother with the repeat.
$activationDate = \wpSPIO()->settings()->activationDate;
if (! $activationDate)
{
return false;
}
$controller = AdminNoticesController::getInstance();
$firstNotice = $controller->getNoticeByKey('MSG_NO_APIKEY');
// Check if first notice is there, and not dismissed, then don't repeat.
if ($firstNotice->isDismissed() === false)
{
return false;
}
// After 6 hours
if (time() < $activationDate + (6 * HOUR_IN_SECONDS))
{
return false;
}
// If not key is verified and first one is dismissed, and not this one.
return true;
}
protected function getMessage()
{
$message = __("Action required! Please <a href='https://shortpixel.com/wp-apikey' target='_blank'>get your API key</a> to activate your ShortPixel plugin.",'shortpixel-image-optimiser');
return $message;
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace ShortPixel\Model\AdminNotices;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\Controller\AdminNoticesController as AdminNoticesController;
class ApiNoticeRepeatLong extends \ShortPixel\Model\AdminNoticeModel
{
protected $key = 'MSG_NO_APIKEY_REPEAT_LONG';
protected $errorLevel = 'warning';
protected function checkTrigger()
{
if (\wpSPIO()->settings()->verifiedKey)
{
return false;
}
// Is set by general ApiNotice. If not set, don't bother with the repeat.
$activationDate = \wpSPIO()->settings()->activationDate;
if (! $activationDate)
{
return false;
}
$controller = AdminNoticesController::getInstance();
// Check the original
$firstNotice = $controller->getNoticeByKey('MSG_NO_APIKEY');
if ($firstNotice->isDismissed() === false)
{
return false;
}
// Check the Repeat.
$secondNotice = $controller->getNoticeByKey('MSG_NO_APIKEY_REPEAT');
if ($secondNotice->isDismissed() === false)
{
return false;
}
// After 3 days.
if (time() < $activationDate + (3 * DAY_IN_SECONDS))
{
return false;
}
// If not key is verified and first one is dismissed, and not this one.
return true;
}
protected function getMessage()
{
$message = __("Your image gallery is not optimized. It takes 2 minutes to <a href='https://shortpixel.com/wp-apikey' target='_blank'>get your API key</a> and activate your ShortPixel plugin.",'shortpixel-image-optimiser') . "<BR><BR>";
return $message;
}
}

View File

@ -0,0 +1,118 @@
<?php
namespace ShortPixel\Model\AdminNotices;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use \ShortPixel\Controller\CacheController as CacheController;
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
class AvifNotice extends \ShortPixel\Model\AdminNoticeModel
{
protected $key = 'MSG_AVIF_ERROR';
protected $errorLevel = 'error';
protected $error_message;
protected $error_detail;
// Remove this.
public function __construct()
{
$this->callback = array($this, 'function_crash');
parent::__construct();
}
/*
public function function_crash()
{
echo 'Yall';
return false;
}
*/
protected function checkTrigger()
{
// No Automatic Trigger.
return false;
}
public function check()
{
$cache = new CacheController();
if (apply_filters('shortpixel/avifcheck/override', false) === true)
{ return; }
if ($cache->getItem('avif_server_check')->exists() === false)
{
$url = \WPSPIO()->plugin_url('res/img/test.avif');
$headers = get_headers($url);
$is_error = true;
$this->addData('headers', $headers);
// Defaults.
$this->error_message = __('AVIF server test failed. Your server may not be configured to display AVIF files correctly. Serving AVIF might cause your images not to load. Check your images, disable the AVIF option, or update your web server configuration.', 'shortpixel-image-optimiser');
$this->error_detail = __('The request did not return valid HTTP headers. Check if the plugin is allowed to access ' . $url, 'shortpixel-image-optimiser');
$contentType = null;
$response = $headers[0];
if (is_array($headers) )
{
foreach($headers as $index => $header)
{
if ( strpos(strtolower($header), 'content-type') !== false )
{
// This is another header that can interrupt.
if (strpos(strtolower($header), 'x-content-type-options') === false)
{
$contentType = $header;
}
}
}
// http not ok, redirect etc. Shouldn't happen.
if (is_null($response) || strpos($response, '200') === false)
{
$this->error_detail = sprintf(__('AVIF check could not be completed because the plugin could not retrieve %s %s %s. %s Please check the security/firewall settings and try again', 'shortpixel-image-optimiser'), '<a href="' . $url . '">', $url, '</a>', '<br>');
}
elseif(is_null($contentType) || strpos($contentType, 'avif') === false)
{
$this->error_detail = sprintf(__('The required Content-type header for AVIF files was not found. Please check this with your hosting and/or CDN provider. For more details on how to fix this issue, %s see this article %s', 'shortpixel_image_optimiser'), '<a href="https://shortpixel.com/blog/avif-mime-type-delivery-apache-nginx/" target="_blank"> ', '</a>');
}
else
{
$is_error = false;
}
}
if ($is_error)
{
if (is_null($this->notice) || $this->notice->isDismissed() === false)
{
$this->addManual();
}
}
else
{
$this->reset();
$item = $cache->getItem('avif_server_check');
$item->setValue(time());
$item->setExpires(MONTH_IN_SECONDS);
$cache->storeItemObject($item );
}
}
}
protected function getMessage()
{
$headers = $this->getData('headers');
$message = '<h4>' . $this->error_message . '</h4><p>' . $this->error_detail . '</p><p class="small">' . __('Returned headers for:<br>', 'shortpixel-image-optimiser') . print_r($headers, true) . '</p>';
return $message;
}
}

View File

@ -0,0 +1,219 @@
<?php
namespace ShortPixel\Model\AdminNotices;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class CompatNotice extends \ShortPixel\Model\AdminNoticeModel
{
protected $key = 'MSG_COMPAT';
protected $errorLevel = 'warning';
protected function checkTrigger()
{
$conflictPlugins = $this->getConflictingPlugins();
if (count($conflictPlugins) > 0)
{
$this->addData('conflicts', $conflictPlugins);
return true;
}
else {
return false;
}
}
protected function getMessage()
{
$conflicts = $this->getData('conflicts');
if (! is_array($conflicts))
$conflicts = array();
$message = __("The following plugins are not compatible with ShortPixel and may cause unexpected results: ",'shortpixel-image-optimiser');
$message .= '<ul class="sp-conflict-plugins">';
foreach($conflicts as $plugin) {
//ShortPixelVDD($plugin);
$action = $plugin['action'];
$link = ( $action == 'Deactivate' )
? wp_nonce_url( admin_url( 'admin-post.php?action=shortpixel_deactivate_conflict_plugin&plugin=' . urlencode( $plugin['path'] ) ), 'sp_deactivate_plugin_nonce' )
: $plugin['href'];
$message .= '<li class="sp-conflict-plugins-list"><strong>' . $plugin['name'] . '</strong>';
$message .= '<a href="' . $link . '" class="button button-primary">' . $action . '</a>';
if($plugin['details']) $message .= '<br>';
if($plugin['details']) $message .= '<span>' . $plugin['details'] . '</span>';
}
$message .= "</ul>";
return $message;
}
protected function getConflictingPlugins() {
$settings = \wpSPIO()->settings();
$conflictPlugins = array(
'WP Smush - Image Optimization'
=> array(
'action'=>'Deactivate',
'data'=>'wp-smushit/wp-smush.php',
'page'=>'wp-smush-bulk'
),
'Imagify Image Optimizer'
=> array(
'action'=>'Deactivate',
'data'=>'imagify/imagify.php',
'page'=>'imagify'
),
'Compress JPEG & PNG images (TinyPNG)'
=> array(
'action'=>'Deactivate',
'data'=>'tiny-compress-images/tiny-compress-images.php',
'page'=>'tinify'
),
'Kraken.io Image Optimizer'
=> array(
'action'=>'Deactivate',
'data'=>'kraken-image-optimizer/kraken.php',
'page'=>'wp-krakenio'
),
'Optimus - WordPress Image Optimizer'
=> array(
'action'=>'Deactivate',
'data'=>'optimus/optimus.php',
'page'=>'optimus'
),
'Phoenix Media Rename' => array(
'action' => 'Deactivate',
'data' => 'phoenix-media-rename/phoenix-media-rename.php',
),
'EWWW Image Optimizer'
=> array(
'action'=>'Deactivate',
'data'=>'ewww-image-optimizer/ewww-image-optimizer.php',
'page'=>'ewww-image-optimizer%2F'
),
'EWWW Image Optimizer Cloud'
=> array(
'action'=>'Deactivate',
'data'=>'ewww-image-optimizer-cloud/ewww-image-optimizer-cloud.php',
'page'=>'ewww-image-optimizer-cloud%2F'
),
'ImageRecycle pdf & image compression'
=> array(
'action'=>'Deactivate',
'data'=>'imagerecycle-pdf-image-compression/wp-image-recycle.php',
'page'=>'option-image-recycle'
),
'CheetahO Image Optimizer'
=> array(
'action'=>'Deactivate',
'data'=>'cheetaho-image-optimizer/cheetaho.php',
'page'=>'cheetaho'
),
'Zara 4 Image Compression'
=> array(
'action'=>'Deactivate',
'data'=>'zara-4/zara-4.php',
'page'=>'zara-4'
),
'CW Image Optimizer'
=> array(
'action'=>'Deactivate',
'data'=>'cw-image-optimizer/cw-image-optimizer.php',
'page'=>'cw-image-optimizer'
),
'Simple Image Sizes'
=> array(
'action'=>'Deactivate',
'data'=>'simple-image-sizes/simple_image_sizes.php'
),
'Regenerate Thumbnails and Delete Unused'
=> array(
'action' => 'Deactivate',
'data' => 'regenerate-thumbnails-and-delete-unused/regenerate_wpregenerate.php',
),
'Swift Performance'
=> array(
'action' => 'Deactivate',
'data' => 'swift-performance/performance.php',
),
'Swift AI'
=> array(
'action' => 'Deactivate',
'data' => 'swift-ai/main.php',
),
'Swift Performance Lite'
=> array(
'action' => 'Deactivate',
'data' => 'swift-performance-lite/performance.php',
),
//DEACTIVATED TEMPORARILY - it seems that the customers get scared.
/* 'Jetpack by WordPress.com - The Speed up image load times Option'
=> array(
'action'=>'Change Setting',
'data'=>'jetpack/jetpack.php',
'href'=>'admin.php?page=jetpack#/settings'
)
*/
);
if($settings->processThumbnails) {
$details = __('Details: recreating image files may require re-optimization of the resulting thumbnails, even if they were previously optimized. Please use <a href="https://wordpress.org/plugins/regenerate-thumbnails-advanced/" target="_blank">reGenerate Thumbnails Advanced</a> instead.','shortpixel-image-optimiser');
$conflictPlugins = array_merge($conflictPlugins, array(
'Regenerate Thumbnails'
=> array(
'action'=>'Deactivate',
'data'=>'regenerate-thumbnails/regenerate-thumbnails.php',
'page'=>'regenerate-thumbnails',
'details' => $details
),
'Force Regenerate Thumbnails'
=> array(
'action'=>'Deactivate',
'data'=>'force-regenerate-thumbnails/force-regenerate-thumbnails.php',
'page'=>'force-regenerate-thumbnails',
'details' => $details
)
));
}
$found = array();
foreach($conflictPlugins as $name => $path) {
$action = ( isset($path['action']) ) ? $path['action'] : null;
$data = ( isset($path['data']) ) ? $path['data'] : null;
$href = ( isset($path['href']) ) ? $path['href'] : null;
$page = ( isset($path['page']) ) ? $path['page'] : null;
$details = ( isset($path['details']) ) ? $path['details'] : null;
if(is_plugin_active($data)) {
// Local checks for things. If too much this needs some other impl.
if( $data == 'jetpack/jetpack.php' ){
$jetPackPhoton = get_option('jetpack_active_modules') ? in_array('photon', get_option('jetpack_active_modules')) : false;
if( !$jetPackPhoton ){ continue; }
}
if ($data == 'swift-performance/performance.php' || $data == 'swift-ai/main.php')
{
if (false === $this->checkSwiftActive())
{
continue;
}
}
$found[] = array( 'name' => $name, 'action'=> $action, 'path' => $data, 'href' => $href , 'page' => $page, 'details' => $details);
}
}
return $found;
}
private function checkSwiftActive()
{
if ( function_exists('swift3_check_option') && true == swift3_check_option('optimize-images', 'on'))
{
return true;
}
return false;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace ShortPixel\Model\AdminNotices;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class LegacyNotice extends \ShortPixel\Model\AdminNoticeModel
{
protected $key = 'MSG_CONVERT_LEGACY';
protected function checkTrigger()
{
return false;
}
protected function getMessage()
{
$message = '<p><strong>' . __('ShortPixel has found items in the media library with an outdated optimization format!', 'shortpixel-image-optimiser') . '</strong></p>';
$message .= '<p>' . __('Prior to version 5.0, a different format was used to store ShortPixel optimization information. ShortPixel automatically migrates the media library items to the new format when they are opened. %s Please check if your images contain the optimization information after migration. %s Read more %s', 'shortpixel-image-optimiser') . '</p>';
$message .= '<p>' . __('It is recommended to migrate all items to the modern format by clicking the button below.', 'shortpixel-image-optimser') . '</p>';
$message .= '<p><a href="%s" class="button button-primary">%s</a></p>';
$read_link = esc_url('https://shortpixel.com/knowledge-base/article/539-spio-5-tells-me-to-convert-legacy-data-what-is-this');
$action_link = esc_url(admin_url('upload.php?page=wp-short-pixel-bulk&panel=bulk-migrate'));
$action_name = __('Migrate optimization data', 'shortpixel-image-optimiser');
$message = sprintf($message, '<br>', '<a href="' . $read_link . '" target="_blank">', '</a>', $action_link, $action_name);
return $message;
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace ShortPixel\Model\AdminNotices;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
class ListviewNotice extends \ShortPixel\Model\AdminNoticeModel
{
protected $key = 'MSG_LISTVIEW_ACTIVE';
public function __construct()
{
$this->include_screens[] = 'upload';
parent::__construct();
}
/*public function load()
{
parent::load();
// Reset this notice even when dismissed when condition changed.
if ($this->isDismissed() && $this->checkReset() === true)
{
$this->reset();
}
} */
protected function checkTrigger()
{
// Don't check for this, when not on this screen.
$screen_id = \wpSPIO()->env()->screen_id;
if ($screen_id !== 'upload')
{
return false;
}
if (! function_exists('wp_get_current_user') )
{
return false;
}
$viewMode = get_user_option('media_library_mode', get_current_user_id() );
if ($viewMode === "" || strlen($viewMode) == 0)
{
// If nothing is set, set it for them.
update_user_option( get_current_user_id(), 'media_library_mode', 'list' );
return false;
}
elseif ($viewMode !== "list")
{
return true;
}
else
{
if (is_object($this->getNoticeObj()))
$this->reset();
}
return false;
}
protected function checkReset()
{
if (! function_exists('wp_get_current_user') )
{
return false;
}
$current_user = wp_get_current_user();
$currentUserID = $current_user->ID;
$viewMode = get_user_meta($currentUserID, "wp_media_library_mode", true);
if ($viewMode == 'list')
{
return true;
}
return false;
}
protected function getMessage()
{
$message = sprintf(__('Now you can see ShortPixel Image Optimizer\'s actions and optimization data in Grid view too! Click on any image below and you can see the ShortPixel actions and menus in the popup that opens. However, the list view provides a better experience. Click now to %sswitch to the list view%s. ', 'shortpixel-image-optimiser'), '<a href="' . admin_url('upload.php?mode=list') . '">','</a>');
return $message;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace ShortPixel\Model\AdminNotices;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class NewExclusionFormat extends \ShortPixel\Model\AdminNoticeModel
{
protected $key = 'MSG_EXCLUSION_WARNING';
protected function checkTrigger()
{
$patterns = \wpSPIO()->settings()->excludePatterns;
if (! is_array($patterns))
{
return false;
}
foreach($patterns as $index => $pattern)
{
if (! isset($pattern['apply']))
{
return true;
}
}
return false;
}
protected function getMessage()
{
$message = "<p>" . __('As of version 5.5.0, ShortPixel Image Optimiser also checks thumbnails for exclusions. This can change which images are optimized and which are excluded. Please check your exclusion rules on the '
. '<a href="options-general.php?page=wp-shortpixel-settings&part=adv-settings">ShortPixel Settings</a> page.','shortpixel-image-optimiser') . "
</p>";
return $message;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace ShortPixel\Model\AdminNotices;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class NextgenNotice extends \ShortPixel\Model\AdminNoticeModel
{
protected $key = 'MSG_INTEGRATION_NGGALLERY';
protected function checkTrigger()
{
$settings = \wpSPIO()->settings();
if (! $settings->verifiedKey)
{
return false; // no key, no integrations.
}
if (\wpSPIO()->env()->has_nextgen && ! $settings->includeNextGen)
{
return true;
}
return false;
}
protected function getMessage()
{
$url = esc_url(admin_url('options-general.php?page=wp-shortpixel-settings&part=adv-settings'));
$message = sprintf(__('You seem to be using NextGen Gallery. You can optimize your galleries with ShortPixel, but this is not currently enabled. To enable it, %sgo to settings and enable%s it!', 'shortpixel_image_optimiser'), '<a href="' . $url . '">', '</a>');
return $message;
}
}

View File

@ -0,0 +1,104 @@
<?php
namespace ShortPixel\Model\AdminNotices;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\Controller\StatsController as StatsController;
use ShortPixel\Controller\AdminNoticesController as AdminNoticesController;
use ShortPixel\Controller\QuotaController as QuotaController;
class QuotaNoticeMonth extends \ShortPixel\Model\AdminNoticeModel
{
protected $key = 'MSG_UPGRADE_MONTH';
public function load()
{
$this->callback = array(AdminNoticesController::getInstance(), 'proposeUpgradePopup');
parent::load();
}
protected function checkTrigger()
{
$quotaController = QuotaController::getInstance();
if ($quotaController->hasQuota() === false)
return false;
$quotaData = $quotaController->getQuota();
if ($this->monthlyUpgradeNeeded($quotaData) === false)
return false;
$this->addData('average', $this->getMonthAverage());
$this->addData('month_total', $quotaData->monthly->total);
$this->addData('onetime_remaining', $quotaData->onetime->remaining);
}
protected function getMessage()
{
$quotaController = QuotaController::getInstance();
$quotaData = $quotaController->getQuota();
$average = $this->getMonthAverage(); // $this->getData('average');
$month_total = $quotaData->monthly->total;// $this->getData('month_total');
$onetime_remaining = $quotaData->onetime->remaining; //$this->getData('onetime_remaining'); */
$message = '<p>' . sprintf(__("You add an average of <strong>%d images and thumbnails</strong> to your Media Library every month and you have <strong>a plan of %d images/month (and %d one-time images)</strong>.%s"
. " You may need to upgrade your plan to have all your images optimized.", 'shortpixel-image-optimiser'), $average, $month_total, $onetime_remaining, '<br>') . '</p>';
$message .= ' <button class="button button-primary" id="shortpixel-upgrade-advice" onclick="ShortPixel.proposeUpgrade()" style="margin-right:10px;"><strong>' . __('Show me the best available options', 'shortpixel-image-optimiser') . '</strong></button>';
return $message;
}
protected function CheckUpgradeNeeded($quotaData)
{
if (isset($quotaData->monthly->total) && !$quotaData->unlimited)
{
$monthAvg = $this->getMonthAvg($quotaData);
// +20 I suspect to not trigger on very low values of monthly use(?)
$threshold = $quotaData->monthly->total + ($quotaData->onetime->remaining / 6 ) +20;
if ($monthAvg > $threshold)
{
return true;
}
}
return false;
}
protected function getMonthAverage() {
$stats = StatsController::getInstance();
// Count how many months have some optimized images.
for($i = 4, $count = 0; $i>=1; $i--) {
if($count == 0 && $stats->find('period', 'months', $i) == 0)
{
continue;
}
$count++;
}
// Sum last 4 months, and divide by number of active months to get number of avg per active month.
return ($stats->find('period', 'months', 1) + $stats->find('period', 'months', 2) + $stats->find('period', 'months', 3) + $stats->find('period', 'months', 4) / max(1,$count));
}
protected function monthlyUpgradeNeeded($quotaData)
{
if (isset($quotaData->monthly->total))
{
$monthAvg = $this->getMonthAverage($quotaData);
// +20 I suspect to not trigger on very low values of monthly use(?)
$threshold = $quotaData->monthly->total + ($quotaData->onetime->remaining / 6 ) +20;
if ($monthAvg > $threshold)
{
return true;
}
}
return false;
}
} // class

View File

@ -0,0 +1,111 @@
<?php
namespace ShortPixel\Model\AdminNotices;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\Controller\StatsController as StatsController;
use ShortPixel\Controller\ApiKeyController as ApiKeyController;
use ShortPixel\Controller\AdminNoticesController as AdminNoticesController;
use ShortPixel\Controller\QuotaController as QuotaController;
class QuotaNoticeReached extends \ShortPixel\Model\AdminNoticeModel
{
protected $key = 'MSG_QUOTA_REACHED';
protected $errorLevel = 'error';
public function load()
{
$this->callback = array(AdminNoticesController::getInstance(), 'proposeUpgradePopup');
parent::load();
}
protected function checkTrigger()
{
$quotaController = QuotaController::getInstance();
if ($quotaController->hasQuota() === true)
return false;
// $quotaData = $quotaController->getQuota();
$this->reset('MSG_UPGRADE_MONTH');
$this->reset('MSG_UPGRADE_BULK');
return true;
}
protected function getMessage()
{
$statsControl = StatsController::getInstance();
$averageCompression = $statsControl->getAverageCompression();
$quotaController = QuotaController::getInstance();
$keyControl = ApiKeyController::getInstance();
//$keyModel->loadKey();
$login_url = 'https://shortpixel.com/login/';
$friend_url = $login_url;
if ($keyControl->getKeyForDisplay())
{
$login_url .= $keyControl->getKeyForDisplay() . '/spio-unlimited';
$friend_url = $login_url . 'tell-a-friend';
}
$message = '<div class="sp-quota-exceeded-alert" id="short-pixel-notice-exceed">';
if($averageCompression) {
$message .= '<div style="float:right;">
<div class="bulk-progress-indicator" style="height: 110px">
<div style="margin-bottom:5px">' . __('Average image<br>reduction until now:','shortpixel-image-optimiser') . '</div>
<div id="sp-avg-optimization"><input type="text" id="sp-avg-optimization-dial" value="' . round($averageCompression) . '" class="dial percentDial" data-dialsize="60"></div>
<script>
jQuery(function() {
if (ShortPixel)
{
ShortPixel.percentDial("#sp-avg-optimization-dial", 60);
}
});
</script>
</div>
</div>';
}
$message .= '<h3>' . __('Quota Exceeded','shortpixel-image-optimiser') . '</h3>';
$quota = $quotaController->getQuota();
$creditsUsed = number_format($quota->monthly->consumed + $quota->onetime->consumed);
$totalOptimized = $statsControl->find('total', 'images');
$totalImagesToOptimize = number_format($statsControl->totalImagesToOptimize());
$message .= '<p>' . sprintf(__('The plugin has optimized <strong>%s images</strong> and has been stopped because it has reached the available quota limit.','shortpixel-image-optimiser'),
$creditsUsed);
if($totalImagesToOptimize > 0) {
$message .= sprintf(__('<strong> %s images and thumbnails</strong> have not been optimized by ShortPixel yet.','shortpixel-image-optimiser'), $totalImagesToOptimize );
}
$message .= sprintf('</p>
<div>
<button class="button button-primary" type="button" id="shortpixel-upgrade-advice" onclick="ShortPixel.proposeUpgrade()" style="margin-right:10px;"><strong>' . __('Show me the best available options', 'shortpixel-image-optimiser') . '</strong></button>
<a class="button button-primary" href="%s"
title="' . __('Go to My Account and choose a plan','shortpixel-image-optimiser') . '" target="_blank" style="margin-right:10px;">
<strong>' . __('Upgrade','shortpixel-image-optimiser') . '</strong>
</a>
<button type="button" name="checkQuota" class="button" onclick="ShortPixel.checkQuota()">'. __('Confirm new credits','shortpixel-image-optimiser') . '</button>
</div>', $login_url);
$message .= '</div>'; /// closing div
return $message;
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace ShortPixel\Model\AdminNotices;
class SmartcropNotice extends \ShortPixel\Model\AdminNoticeModel
{
protected $key = 'MSG_FEATURE_SMARTCROP';
public function __construct()
{
$this->exclude_screens[] = 'settings_page_wp-shortpixel-settings';
parent::__construct();
}
protected function checkTrigger()
{
$settings = \wpSPIO()->settings();
if (! $settings->verifiedKey)
{
return false; // no key, no integrations.
}
if (! $settings->useSmartcrop)
{
return true;
}
return false;
}
protected function checkReset()
{
$settings = \wpSPIO()->settings();
if ($settings->useSmartcrop == true)
{
return true;
}
return false;
}
protected function getMessage()
{
$link = 'https://shortpixel.com/knowledge-base/article/182-what-is-smart-cropping';
$link2 = 'https://shortpixel.com/blog/how-to-smart-crop-wordpress-images/#how-to-crop-wordpress-images-automatically-smart-solution';
$link3 = esc_url(admin_url('options-general.php?page=wp-shortpixel-settings'));
$message = sprintf(__('%s With ShortPixel you can now %ssmartly crop%s thumbnails on your website. This is especially useful for eCommerce websites %s(read more)%s. %s %s Enable the option on the %sShortPixel Settings%s page. %s', 'shortpixel-image-optimiser'),
'<p>' ,
'<a href="' . $link . '" target="_blank">', '</a>',
'<a href="' . $link2 . '" target="_blank">', '</a>',
'</p>', '<p>',
'<a href="' . $link3 . '" >', '</a>',
'</p>'
);
return $message;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace ShortPixel\Model\AdminNotices;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class UnlistedNotice extends \ShortPixel\Model\AdminNoticeModel
{
protected $key = 'MSG_UNLISTED_FOUND';
protected function checkTrigger()
{
return false;
}
// @todo This message is not properly stringF'ed.
protected function getMessage()
{
$settings = \wpSPIO()->settings();
//$unlisted = isset($settings->currentStats['foundUnlistedThumbs']) ? $settings->currentStats['foundUnlistedThumbs'] : null;
$unlisted_id = $this->getData('id');
$unlisted_name = $this->getData('name');
$unlistedFiles = (is_array($this->getData('filelist'))) ? $this->getData('filelist') : array();
$admin_url = esc_url(admin_url('options-general.php?page=wp-shortpixel-settings&part=adv-settings'));
$message = __("<p>ShortPixel has found thumbnails that are not registered in the metadata, but are present alongside the other thumbnails. These thumbnails could be created and needed by a plugin or the theme. Let ShortPixel optimize them too?</p>", 'shortpixel-image-optimiser');
$message .= '<p>' . __("For example, the image", 'shortpixel-image-optimiser') . '
<a href="post.php?post=' . $unlisted_id . '&action=edit" target="_blank">
' . $unlisted_name . '
</a> also has these thumbnails that are not listed in the metadata: ' . (implode(', ', $unlistedFiles)) . '
</p>';
$message .= '<p>' . sprintf(__('You can enable optimizing %s Unlisted Images %s in the %s settings %s', 'shortpixel-image-optimiser'), '<b>', '</b>', '<a href="'. $admin_url . '">','</a>') . '</p>';
return $message;
}
}

View File

@ -0,0 +1,311 @@
<?php
namespace ShortPixel\Model;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Notices\NoticeController as Notice;
use ShortPixel\Controller\AdminNoticesController as AdminNoticesController;
use ShortPixel\Controller\QuotaController as QuotaController;
class ApiKeyModel extends \ShortPixel\Model
{
// variables
protected $apiKey;
protected $apiKeyTried; // stop retrying the same key over and over if not valid.
protected $verifiedKey;
protected $redirectedSettings;
// states
// key is verified is set by checkKey *after* checks and validation
protected $key_is_verified = false; // this state doesn't have to be the same as the verifiedKey field in DB.
protected $key_is_empty = false;
protected $key_is_constant = false;
protected $key_is_hidden = false;
protected static $notified = array();
protected $model = array(
'apiKey' => array('s' => 'string',
'key' => 'wp-short-pixel-apiKey',
),
'apiKeyTried' => array('s' => 'string',
'key' => 'wp-short-pixel-apiKeyTried'
),
'verifiedKey' => array('s' => 'boolean',
'key' => 'wp-short-pixel-verifiedKey',
),
'redirectedSettings' => array('s' => 'int',
'key' => 'wp-short-pixel-redirected-settings',
),
);
public $shortPixel;
/** Constructor. Check for constants, load the key */
public function __construct()
{
$this->key_is_constant = (defined("SHORTPIXEL_API_KEY")) ? true : false;
$this->key_is_hidden = (defined("SHORTPIXEL_HIDE_API_KEY")) ? SHORTPIXEL_HIDE_API_KEY : false;
}
/** Load the key from storage. This can be a constant, or the database. Check if key is valid.
*
*/
public function loadKey()
{
$this->apiKey = get_option($this->model['apiKey']['key'], false);
$this->verifiedKey = get_option($this->model['verifiedKey']['key'], false);
$this->redirectedSettings = get_option($this->model['redirectedSettings']['key'], false);
$this->apiKeyTried = get_option($this->model['apiKeyTried']['key'], false);
if ($this->key_is_constant)
{
$key = SHORTPIXEL_API_KEY;
}
else
{
$key = $this->apiKey;
}
$valid = $this->checkKey($key);
return $valid;
}
protected function update()
{
update_option($this->model['apiKey']['key'], trim($this->apiKey));
update_option($this->model['verifiedKey']['key'], $this->verifiedKey);
update_option($this->model['redirectedSettings']['key'], $this->redirectedSettings);
update_option($this->model['apiKeyTried']['key'], $this->apiKeyTried);
}
/** Resets the last APIkey that was attempted with validation
*
* The last apikey tried is saved to prevent server and notice spamming when using a constant key, or a wrong key in the database without updating.
*/
public function resetTried()
{
if (is_null($this->apiKeyTried))
{
return; // if already null, no need for additional activity
}
$this->apiKeyTried = null;
$this->update();
Log::addDebug('Reset Tried', $this->apiKeyTried);
}
/** Checks the API key to see if we have a validated situation
* @param $key String The 20-character ShortPixel API Key or empty string
* @return boolean Returns a boolean indicating valid key or not
*
* An Api key can be removed from the system by passing an empty string when the key is not hidden.
* If the key has changed from stored key, the function will pass a validation request to the server
* Failing to key a 20char string, or passing an empty key will result in notices.
*/
public function checkKey($key)
{
$valid = false;
if (strlen($key) == 0)
{
// first-timers, redirect to nokey screen
$this->checkRedirect(); // this should be a one-time thing.
if($this->key_is_hidden) // hidden empty keys shouldn't be checked
{
$this->key_is_verified = $this->verifiedKey;
return $this->key_is_verified;
}
elseif ($key != $this->apiKey)
{
Notice::addWarning(__('Your API Key has been removed', 'shortpixel-image-optimiser'));
$this->clearApiKey(); // notice and remove;
return false;
}
$valid = false;
}
elseif (strlen($key) <> 20 && $key != $this->apiKeyTried)
{
$this->NoticeApiKeyLength($key);
Log::addDebug('Key Wrong Length');
$valid = $this->verifiedKey; // if we already had a verified key, and a wrong new one is giving keep status.
}
elseif( ($key != $this->apiKey || ! $this->verifiedKey) && $key != $this->apiKeyTried)
{
Log::addDebug('Validate Key' . $key);
$valid = $this->validateKey($key);
}
elseif($key == $this->apiKey) // all is fine!
{
$valid = $this->verifiedKey;
}
// if key is not valid on load, means not valid at all
if (! $valid)
{
$this->verifiedKey = false;
$this->key_is_verified = false;
$this->apiKeyTried = $key;
$this->update();
}
else {
$this->key_is_verified = true;
}
return $this->key_is_verified; // first time this is set! *after* this function
}
public function is_verified()
{
return $this->key_is_verified;
}
public function is_constant()
{
return $this->key_is_constant;
}
public function is_hidden()
{
return $this->key_is_hidden;
}
public function getKey()
{
return $this->apiKey;
}
public function uninstall()
{
$this->clearApiKey();
}
protected function clearApiKey()
{
$this->key_is_empty = true;
$this->apiKey = '';
$this->verifiedKey = false;
$this->apiKeyTried = '';
$this->key_is_verified = false;
AdminNoticesController::resetAPINotices();
AdminNoticesController::resetQuotaNotices();
AdminNoticesController::resetIntegrationNotices();
// Remove them all
delete_option($this->model['apiKey']['key']);
delete_option($this->model['verifiedKey']['key']);
delete_option($this->model['redirectedSettings']['key']);
delete_option($this->model['apiKeyTried']['key']);
// $this->update();
}
protected function validateKey($key)
{
Log::addDebug('Validating Key ' . $key);
// first, save Auth to satisfy getquotainformation
$quotaData = $this->remoteValidate($key);
$checked_key = ($quotaData['APIKeyValid']) ? true : false;
if (! $checked_key)
{
Log::addError('Key is not validated', $quotaData['Message']);
Notice::addError(sprintf(__('Error during verifying API key: %s','shortpixel-image-optimiser'), $quotaData['Message'] ));
}
elseif ($checked_key) {
$this->apiKey = $key;
$this->verifiedKey = $checked_key;
$this->processNewKey($quotaData);
$this->update();
}
return $this->verifiedKey;
}
/** Process some things when key has been added. This is from original wp-short-pixel.php */
protected function processNewKey($quotaData)
{
//display notification
$urlParts = explode("/", get_site_url());
if( $quotaData['DomainCheck'] == 'NOT Accessible'){
$notice = array("status" => "warn", "msg" => __("API Key is valid but your site is not accessible from our servers. Please make sure that your server is accessible from the Internet before using the API or otherwise we won't be able to optimize them.",'shortpixel-image-optimiser'));
Notice::addWarning($notice);
} else {
if ( function_exists("is_multisite") && is_multisite() && !defined("SHORTPIXEL_API_KEY"))
$notice = __("Great, your API Key is valid! <br>You seem to be running a multisite, please note that API Key can also be configured in wp-config.php like this:",'shortpixel-image-optimiser')
. "<BR> <b>define('SHORTPIXEL_API_KEY', '". $this->apiKey ."');</b>";
else
$notice = __('Great, your API Key is valid. Please take a few moments to review the plugin settings before starting to optimize your images.','shortpixel-image-optimiser');
Notice::addSuccess($notice);
}
//test that the "uploads" have the right rights and also we can create the backup dir for ShortPixel
if ( \wpSPIO()->filesystem()->checkBackupFolder() === false)
{
$notice = sprintf(__("There is something preventing us to create a new folder for backing up your original files.<BR>Please make sure that folder <b>%s</b> has the necessary write and read rights.",'shortpixel-image-optimiser'), WP_CONTENT_DIR . '/' . SHORTPIXEL_UPLOADS_NAME );
Notice::addError($notice);
}
AdminNoticesController::resetAPINotices();
}
protected function NoticeApiKeyLength($key)
{
// repress double warning.
if (isset(self::$notified['apilength']) && self::$notified['apilength'])
return;
$KeyLength = strlen($key);
$notice = sprintf(__("The key you provided has %s characters. The API key should have 20 characters, letters and numbers only.",'shortpixel-image-optimiser'), $KeyLength)
. "<BR> <b>"
. __('Please check that the API key is the same as the one you received in your confirmation email.','shortpixel-image-optimiser')
. "</b><BR> "
. __('If this problem persists, please contact us at ','shortpixel-image-optimiser')
. "<a href='mailto:help@shortpixel.com?Subject=API Key issues' target='_top'>help@shortpixel.com</a>"
. __(' or ','shortpixel-image-optimiser')
. "<a href='https://shortpixel.com/contact' target='_blank'>" . __('here','shortpixel-image-optimiser') . "</a>.";
self::$notified['apilength'] = true;
Notice::addError($notice);
}
// Does remote Validation of key. In due time should be replaced with something more lean.
private function remoteValidate($key)
{
$qControl = QuotaController::getInstance();
$quotaData = $qControl->remoteValidateKey($key);
return $quotaData;
}
protected function checkRedirect()
{
if(! \wpSPIO()->env()->is_ajaxcall && !$this->redirectedSettings && !$this->verifiedKey && (!function_exists("is_multisite") || ! is_multisite())) {
$this->redirectedSettings = 1;
$this->update();
wp_safe_redirect(admin_url("options-general.php?page=wp-shortpixel-settings"));
exit();
}
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace ShortPixel\Model;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
/* Model for storing cached data
*
* Use this in conjunction with cache controller, don't call it stand-alone.
*/
class CacheModel
{
protected $name;
protected $value;
protected $expires = HOUR_IN_SECONDS; // This is the expires, when saved without SetExpires! This value is not a representation of any expire time when loading something cache!
protected $exists = false;
public function __construct($name)
{
$this->name = $name;
$this->load();
}
/** Set the expiration of this item. In seconds
* @param $time Expiration in Seconds
*/
public function setExpires($time)
{
$this->expires = $time;
}
public function setValue($value)
{
$this->value = $value;
}
public function exists()
{
return $this->exists;
}
public function getValue()
{
return $this->value;
}
public function getName()
{
return $this->name;
}
public function save()
{
if ($this->expires <= 0)
{
return; // don't save transients without expiration
}
$this->exists = set_transient($this->name, $this->value, $this->expires);
}
public function delete()
{
delete_transient($this->name);
$this->exists = false;
}
protected function load()
{
$item = get_transient($this->name);
if ($item !== false)
{
$this->value = $item;
$this->exists = true;
$this->checkExpiration($this->name);
}
}
/** It has been shown that sometimes the expire of the transient is lost, creating a persistent transient. This can be harmful, especially in the case of bulk-secret which can create a situation were no client will optimize due to the hanging transient. */
private function checkExpiration($name)
{
$option = get_option('_transient_timeout_' . $name);
if (false !== $option && is_numeric($option))
{
return true; // ok
}
else {
// Via object cache the expire info can't be retrieved. Customer is on it's own with this.
if (wp_using_ext_object_cache())
{
return true;
}
$this->value = '';
$this->delete();
Log::addError('Found hanging transient with no expiration! ' . $name, $option);
}
}
}

View File

@ -0,0 +1,253 @@
<?php
namespace ShortPixel\Model\Converter;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Helper\UtilHelper as UtilHelper;
class ApiConverter extends MediaLibraryConverter
{
const CONVERTABLE_EXTENSIONS = array( 'heic');
protected $requestAPIthumbnails = true;
public function isConvertable()
{
$fs = \wpSPIO()->filesystem();
$extension = $this->imageModel->getExtension();
// If extension is in list of allowed Api Converts.
if (in_array($extension, static::CONVERTABLE_EXTENSIONS) && $extension !== 'png')
{
return true;
}
// If file has not been converted in terms of file, but has a placeholder - process ongoing, so continue;
if (false === $this->imageModel->getMeta()->convertMeta()->isConverted() && true === $this->imageModel->getMeta()->convertMeta()->hasPlaceHolder())
{
return true;
}
// File has been converted, not converting again.
if (true === $this->imageModel->getMeta()->convertMeta()->isConverted())
{
return false;
}
}
// Create placeholder here.
public function convert($args = array())
{
$defaults = array(
'runReplacer' => true, // The replacer doesn't need running when the file is just uploaded and doing in handle upload hook.
);
$args = wp_parse_args($args, $defaults);
$this->setupReplacer();
$fs = \wpSPIO()->filesystem();
$placeholderFile = $fs->getFile(\wpSPIO()->plugin_path('res/img/fileformat-heic-placeholder.jpg'));
// Convert runs when putting imageModel to queue format in the Queue classs. This could run without optimization (before) taking place and when accidentally running it more than once results in duplicate files / backups (img-1, img-2 etc). Check placeholder and baseName to prevent this. Assume already done when it has it .
if ($this->imageModel->getMeta()->convertMeta()->hasPlaceHolder() && $this->imageModel->getMeta()->convertMeta()->getReplacementImageBase() !== false)
{
return true;
}
// @todo Check replacementpath here. Rename main file - and backup - if numeration is needed.
// @todo Also placeholder probably needs to be done each time to block current job in progress.
$replacementPath = $this->getReplacementPath();
if (false === $replacementPath)
{
Log::addWarn('ApiConverter replacement path failed');
$this->imageModel->getMeta()->convertMeta()->setError(self::ERROR_PATHFAIL);
return false; // @todo Add ResponseController something here.
}
$replaceFile = $fs->getFile($replacementPath);
// If filebase (filename without extension) is not the same, this indicates that a double is there and it's enumerated. Move backup accordingly.
$destinationFile = $fs->getFile($replacementPath);
$copyok = $placeholderFile->copy($destinationFile);
if ($copyok)
{
$this->imageModel->getMeta()->convertMeta()->setFileFormat('heic');
$this->imageModel->getMeta()->convertMeta()->setPlaceHolder(true);
$this->imageModel->getMeta()->convertMeta()->setReplacementImageBase($destinationFile->getFileBase());
$this->imageModel->saveMeta();
// @todo Wip . Moved from handleConverted.
// Backup basically. Do this first.
$conversion_args = array('replacementPath' => $replacementPath);
$prepared = $this->imageModel->conversionPrepare($conversion_args);
if (false === $prepared)
{
return false;
}
$this->setTarget($destinationFile);
// $params = array('success' => true, 'generate_metadata' => false);
// $this->updateMetaData($params);
$fs->flushImage($this->imageModel);
if (true === $args['runReplacer'])
{
$result = $this->replacer->replace();
}
}
else {
Log::addError('Failed to copy placeholder');
return false;
}
return true;
}
// Restore from original file. Search and replace everything else to death.
public function restore()
{
/*$params = array('restore' => true);
$fs = \wpSPIO()->filesystem();
$this->setupReplacer();
$newExtension = $this->imageModel->getMeta()->convertMeta()->getFileFormat();
$oldFileName = $this->imageModel->getFileName(); // Old File Name, Still .jpg
$newFileName = $this->imageModel->getFileBase() . '.' . $newExtension;
if ($this->imageModel->isScaled())
{
$oldFileName = $this->imageModel->getOriginalFile()->getFileName();
$newFileName = $this->imageModel->getOriginalFile()->getFileBase() . '.' . $newExtension;
}
$fsNewFile = $fs->getFile($this->imageModel->getFileDir() . $newFileName);
$this->newFile = $fsNewFile;
$this->setTarget($fsNewFile);
$this->updateMetaData($params);
// $result = $this->replacer->replace();
$fs->flushImageCache(); */
}
public function getCheckSum()
{
return 1; // done or not.
}
public function handleConverted($optimizeData)
{
$this->setupReplacer();
$fs = \wpSPIO()->filesystem();
/* $replacementPath = $this->getReplacementPath();
if (false === $replacementPath)
{
Log::addWarn('ApiConverter replacement path failed');
$this->imageModel->getMeta()->convertMeta()->setError(self::ERROR_PATHFAIL);
return false; // @todo Add ResponseController something here.
}
$replacementFile = $fs->getFile($replacementPath);
*/
$replacementBase = $this->imageModel->getMeta()->convertMeta()->getReplacementImageBase();
if (false === $replacementBase)
{
$replacementPath = $this->getReplacementPath();
$replacementFile = $fs->getFile($replacementPath);
}
else {
$replacementPath = $replacementBase . '.jpg';
$replacementFile = $fs->getFile($this->imageModel->getFileDir() . $replacementPath);
}
// If -sigh- file has a placeholder, then do something with that.
if (true === $this->imageModel->getMeta()->convertMeta()->hasPlaceHolder())
{
$this->imageModel->getMeta()->convertMeta()->setPlaceHolder(false);
// $this->imageModel->getMeta()->convertMeta()->setReplacementImageBase(false);
// $attach_id = $this->imageModel->get('id');
// ReplacementFile as source should not point to the placeholder file
$this->source_url = $fs->pathToUrl($replacementFile);
$this->replacer->setSource($this->source_url);
$replacementFile->delete();
}
if (isset($optimizeData['files']) && isset($optimizeData['data']))
{
$files = $optimizeData['files'];
$data = $optimizeData['data'];
}
else {
Log::addError('Something went wrong with handleOptimized', $optimizeData);
return false;
}
$mainImageKey = $this->imageModel->get('mainImageKey');
$mainFile = (isset($files) && isset($files[$mainImageKey])) ? $files[$mainImageKey] : false;
if (false === $mainFile)
{
Log::addError('MainFile not set during success Api Conversion');
return false;
}
if (! isset($mainFile['image']) || ! isset($mainFile['image']['file']))
{
Log::addError('Optimizer didn\'t return file', $mainFile);
return false;
}
$tempFile = $fs->getFile($mainFile['image']['file']);
$res = $tempFile->copy($replacementFile);
if (true === $res)
{
$this->newFile = $replacementFile;
$tempFile->delete();
$params = array('success' => true);
$this->updateMetaData($params);
$result = true;
// if (true === $args['runReplacer'])
// {
$result = $this->replacer->replace();
// }
// Conversion done, but backup results.
$this->imageModel->conversionSuccess(array('omit_backup' => false));
return true;
}
else {
return false;
}
}
} // class

View File

@ -0,0 +1,172 @@
<?php
namespace ShortPixel\Model\Converter;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\Replacer\Replacer as Replacer;
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Model\File\DirectoryModel as DirectoryModel;
use ShortPixel\Model\File\FileModel as FileModel;
use ShortPixel\Controller\ResponseController as ResponseController;
/* ShortPixel Image Optimiser Converters. Unified interface for handling conversion between file types */
abstract class Converter
{
const CONVERTABLE_EXTENSIONS = array('png', 'heic');
const ERROR_LIBRARY = -1; /// PNG Library error
const ERROR_PATHFAIL = -2; // Couldn't create replacement path
const ERROR_RESULTLARGER = -3; // Converted file is bigger than the original
const ERROR_WRITEERROR = -4; // Result file could not be written
const ERROR_BACKUPERROR = -5; // Backup didn't work.
const ERROR_TRANSPARENT = -6; // Transparency when it is not allowed.
protected $imageModel; // The current ImageModel from SPIO
// Method specific
abstract public function convert($args = array());
abstract public function isConvertable();
abstract public function restore();
abstract public function getCheckSum();
// Media Library specific
abstract protected function updateMetaData($params);
abstract public function getUpdatedMeta();
abstract protected function setupReplacer();
abstract protected function setTarget($file);
public function __construct($imageModel)
{
$this->imageModel = $imageModel;
$this->imageModel->getMeta()->convertMeta()->setFileFormat($imageModel->getExtension());
}
public function isConverterFor($extension)
{
if ($extension === $this->imageModel->getMeta()->convertMeta()->getFileFormat())
{
return true;
}
return false;
}
// ForConversion: Return empty if file can't be converted or is already converrted
public static function getConverter($imageModel, $forConversion = false)
{
$extension = $imageModel->getExtension();
$converter = false;
$converter = self::getConverterByExt($extension, $imageModel);
// No Support (yet)
if ($imageModel->get('type') == 'custom')
{
return false;
}
// Second option for conversion is image who have been placeholdered.
if (true === $imageModel->getMeta()->convertMeta()->hasPlaceHolder() && false === $imageModel->getMeta()->convertMeta()->isConverted() && ! is_null($imageModel->getMeta()->convertMeta()->getFileFormat()))
{
$converter = self::getConverterByExt($imageModel->getMeta()->convertMeta()->getFileFormat(), $imageModel);
}
if (true === $forConversion) // don't check more.
{
return $converter;
}
if (false === $converter)
{
if ($imageModel->getMeta()->convertMeta()->isConverted() && ! is_null($imageModel->getMeta()->convertMeta()->getFileFormat()) )
{
$converter = self::getConverterByExt($imageModel->getMeta()->convertMeta()->getFileFormat(), $imageModel);
}
else
{
return false;
}
}
return $converter;
}
/** Own function to get a unique filename since the WordPress wp_unique_filename seems to not function properly w/ thumbnails */
protected function unique_file(DirectoryModel $dir, FileModel $file, $number = 0)
{
if (! $file->exists())
return $file;
if ($file->is_virtual())
{
return $file;
}
$number = 0;
$fs = \wpSPIO()->filesystem();
$base = $file->getFileBase();
$ext = $file->getExtension();
while($file->exists())
{
$number++;
$numberbase = $base . '-' . $number;
Log::addDebug('check for unique file -- ' . $dir->getPath() . $numberbase . '.' . $ext);
$file = $fs->getFile($dir->getPath() . $numberbase . '.' . $ext);
}
return $file;
}
protected function getReplacementPath()
{
$fs = \wpSPIO()->filesystem();
$filename = $this->imageModel->getFileName();
$newFileName = $this->imageModel->getFileBase() . '.jpg'; // convert extension to .jpg
$fsNewFile = $fs->getFile($this->imageModel->getFileDir() . $newFileName);
$uniqueFile = $this->unique_file( $this->imageModel->getFileDir(), $fsNewFile);
$newPath = $uniqueFile->getFullPath(); //(string) $fsFile->getFileDir() . $uniquepath;
if (! $this->imageModel->getFileDir()->is_writable())
{
Log::addWarn('Replacement path for PNG not writable ' . $this->imageModel->getFileDir()->getPath());
$msg = __('Replacement path for PNG not writable', 'shortpixel-image-optimiser');
ResponseController::addData($this->imageModel->get('id'), 'message', $msg);
return false;
}
$this->setTarget($uniqueFile);
return $newPath;
}
private static function getConverterByExt($ext, $imageModel)
{
$converter = false;
switch($ext)
{
case 'png':
$converter = new PNGConverter($imageModel);
break;
case 'heic':
$converter = new ApiConverter($imageModel);
break;
}
return $converter;
}
}

View File

@ -0,0 +1,173 @@
<?php
namespace ShortPixel\Model\Converter;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\Replacer\Replacer as Replacer;
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
/* Abstract base to use for image converters. Handles media library related functions ( replacing ) */
abstract class MediaLibraryConverter extends Converter
{
protected $source_url;
protected $replacer; // Replacer class Object.
protected $newFile; // The newFile Object.
public function getUpdatedMeta()
{
$id = $this->imageModel->get('id');
$meta = wp_get_attachment_metadata($id); // reset the metadata because we are on the hook.
return $meta;
}
protected function setupReplacer()
{
$this->replacer = new Replacer();
$fs = \wpSPIO()->filesystem();
$url = $fs->pathToUrl($this->imageModel);
if ($this->imageModel->isScaled()) // @todo Test these assumptions
{
$url = $fs->pathToUrl($this->imageModel->getOriginalFile());
}
$this->source_url = $url;
$this->replacer->setSource($url);
$this->replacer->setSourceMeta($this->imageModel->getWPMetaData());
}
protected function setTarget($newFile)
{
$fs = \wpSPIO()->filesystem();
$this->newFile = $newFile; // set target newFile.
$url = $fs->pathToUrl($this->imageModel);
$newUrl = str_replace($this->imageModel->getFileName(), $newFile->getFileName(), $url);
$this->replacer->setTarget($newUrl);
}
protected function updateMetaData($params)
{
$defaults = array(
'success' => false,
'restore' => false,
'generate_metadata' => true,
);
$params = wp_parse_args($params, $defaults);
$newFile = $this->newFile;
$fullPath = $newFile->getFullPath();
if (! is_object($newFile))
{
Log::addError('Update metadata failed. NewFile not properly set', $newFile);
return false;
}
$attach_id = $this->imageModel->get('id');
$WPMLduplicates = $this->imageModel->getWPMLDuplicates();
$attachment = get_post($attach_id);
$guid = $attachment->guid;
// This action prevents images from being regenerated on the thumbnail hook.
do_action('shortpixel-thumbnails-before-regenerate', $attach_id );
do_action('shortpixel/converter/prevent-offload', $attach_id);
// Update attached_file
$bool = update_attached_file($attach_id, $newFile->getFullPath() );
if (false === $bool)
return false;
// Update post mime on attachment
if (isset($params['success']) && true === $params['success'])
{
$newExt = $this->imageModel->getMeta()->convertMeta()->getFileFormat();
$newGuid = str_replace($guid, $newExt, 'jpg'); // This probable doesn't work bcause doesn't update Guid with this function.
$post_ar = array('ID' => $attach_id, 'post_mime_type' => 'image/jpeg', 'guid' => $newGuid);
}
elseif ( isset($params['restore']) && true === $params['restore'] )
{
$oldExt = $this->imageModel->getMeta()->convertMeta()->getFileFormat();
$newGuid = str_replace($guid, 'jpg', $oldExt);
$post_ar = array('ID' => $attach_id, 'post_mime_type' => 'image/png', 'guid' => $newGuid);
}
$result = wp_update_post($post_ar);
if ($result === 0 || is_wp_error($result))
{
Log::addError('Issue updating WP Post converter - ' . $attach_id);
return false;
}
$metadata = wp_get_attachment_metadata($attach_id);
if (true === $params['generate_metadata'])
{
$attachment = get_post( $attach_id );
$new_metadata = wp_generate_attachment_metadata($attach_id, $newFile->getFullPath());
}
else {
$new_metadata = array();
}
// Metadata might not be array when add_attachment is calling this hook via AdminController ( PNG2JPG)
if (is_array($metadata))
{
// Original Image in the new situation can not be there. Don't preserve it.
if (isset($metadata['original_image']) && ! isset($new_metadata['original_image']) )
{
unset($metadata['original_image']);
}
$new_metadata = array_merge($metadata, $new_metadata); // merge to preserve other custom metadata
}
if (isset($params['success']) && true === $params['success'])
{
do_action('shortpixel/converter/prevent-offload-off', $attach_id);
}
if (is_array($new_metadata) && count($new_metadata) > 0)
{
$bool = wp_update_attachment_metadata($attach_id, $new_metadata);
}
// Restore -sigh- fires off a later signal, because on the succesHandler in MediaLIbraryModel it may copy back backups.
if (isset($params['restore']) && true === $params['restore'])
{
do_action('shortpixel/converter/prevent-offload-off', $attach_id);
}
if (is_array($WPMLduplicates) && count($WPMLduplicates) > 0)
{
foreach ($WPMLduplicates as $duplicate_id)
{
update_attached_file($duplicate_id, $newFile->getFullPath() );
wp_update_attachment_metadata($duplicate_id, $new_metadata);
$post_ar["ID"] = $duplicate_id;
wp_update_post($post_ar);
}
}
$this->replacer->setTargetMeta($new_metadata);
}
} // class

View File

@ -0,0 +1,427 @@
<?php
namespace ShortPixel\Model\Converter;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Model\Image\ImageModel as ImageModel;
use ShortPixel\Model\File\DirectoryModel as DirectoryModel;
use ShortPixel\Model\File\FileModel as FileModel;
use ShortPixel\Notices\NoticeController as Notices;
use ShortPixel\Controller\ResponseController as ResponseController;
use ShortPixel\Helper\DownloadHelper as DownloadHelper;
class PNGConverter extends MediaLibraryConverter
{
protected $instance;
protected $current_image; // The current PHP image resource in memory
protected $virtual_filesize;
protected $replacer; // Replacer class Object.
protected $converterActive = false;
protected $forceConvertTransparent = false;
protected $lastError;
protected $settingCheckSum;
public function __construct($imageModel)
{
parent::__construct($imageModel);
$settings = \wpSPIO()->settings();
$env = \wpSPIO()->env();
$this->converterActive = (intval($settings->png2jpg) > 0) ? true : false;
if ($env->is_gd_installed === false)
{
$this->converterActive = false;
$this->lastError = __('GD library is not active on this installation. Can\'t convert images to PNG', 'shortpixel-image-optimiser');
}
$this->forceConvertTransparent = ($settings->png2jpg == 2) ? true : false;
// If conversion is tried, but failed somehow, it will never try it again, even after changing settings. This should prevent that switch.
$this->settingCheckSum = intval($settings->png2jpg) + intval($settings->backupImages);
}
public function isConvertable()
{
$imageModel = $this->imageModel;
// Settings
if ($this->converterActive === false)
{
return false;
}
// Extension
if ($imageModel->getExtension() !== 'png') // not a png ext. fail silently.
{
return false;
}
// Existence
if (! $imageModel->exists())
{
return false;
}
if (true === $imageModel->getMeta()->convertMeta()->isConverted() || true === $this->hasTried($imageModel->getMeta()->convertMeta()->didTry()) )
{
return false;
}
return true;
}
protected function hasTried($checksum)
{
if ( intval($checksum) === $this->getCheckSum())
{
return true;
}
return false;
}
public function convert($args = array())
{
if (! $this->isConvertable($this->imageModel))
{
return false;
}
$fs = \wpSPIO()->filesystem();
$defaults = array(
'runReplacer' => true, // The replacer doesn't need running when the file is just uploaded and doing in handle upload hook.
);
$conversionArgs = array('checksum' => $this->getCheckSum());
$this->setupReplacer();
$this->raiseMemoryLimit();
$replacementPath = $this->getReplacementPath();
if (false === $replacementPath)
{
Log::addWarn('ApiConverter replacement path failed');
$this->imageModel->getMeta()->convertMeta()->setError(self::ERROR_PATHFAIL);
return false; // @todo Add ResponseController something here.
}
$replaceFile = $fs->getFile($replacementPath);
Log::addDebug('Image replacement base : ' . $replaceFile->getFileBase());
$this->imageModel->getMeta()->convertMeta()->setReplacementImageBase($replaceFile->getFileBase());
$prepared = $this->imageModel->conversionPrepare($conversionArgs);
if (false === $prepared)
{
return false;
}
$args = wp_parse_args($args, $defaults);
if ($this->forceConvertTransparent === false && $this->isTransparent())
{
$this->imageModel->getMeta()->convertMeta()->setError(self::ERROR_TRANSPARENT);
$this->imageModel->conversionFailed($conversionArgs);
return false;
}
Log::addDebug('Starting PNG conversion of #' . $this->imageModel->get('id'));
$bool = $this->run();
if (true === $bool)
{
$params = array('success' => true);
$this->updateMetaData($params);
$result = true;
if (true === $args['runReplacer'])
{
$result = $this->replacer->replace();
}
if (is_array($result))
{
foreach($result as $error)
Notices::addError($error);
}
$this->imageModel->conversionSuccess($conversionArgs);
// new hook.
do_action('shortpixel/image/convertpng2jpg_success', $this->imageModel);
return true;
}
$this->imageModel->conversionFailed($conversionArgs);
//legacy. Note at this point metadata has not been updated.
do_action('shortpixel/image/convertpng2jpg_after', $this->imageModel, $args);
return false;
}
public function getCheckSum()
{
return intval($this->settingCheckSum);
}
protected function run()
{
do_action('shortpixel/image/convertpng2jpg_before', $this->imageModel);
$img = $this->getPNGImage();
$fs = \wpSPIO()->filesystem();
$width = $this->imageModel->get('width');
$height = $this->imageModel->get('height');
// If imageModel doesn't have proper width / height set. This can happen with remote files.
if (! is_int($width) && ! $width > 0)
{
$width = imagesx($img);
}
if (! is_int($height) && ! $height > 0)
{
$height = imagesy($img);
}
Log::addDebug("PNG2JPG doConvert width $width height $height", memory_get_usage());
$bg = imagecreatetruecolor($width, $height);
if(false === $bg || false === $img)
{
Log::addError('ImageCreateTrueColor failed');
if (false === $bg)
{
$msg = __('Creating an TrueColor Image failed - Possible library error', 'shortpixel-image-optimiser');
}
elseif (false === $img)
{
$msg = __('Image source failed - Check if source image is PNG and library is working', 'shortpixel-image-optimiser');
}
$this->imageModel->getMeta()->convertMeta()->setError(self::ERROR_LIBRARY);
ResponseController::addData($this->imageModel->get('id'), 'message', $msg);
return false;
}
imagefill($bg, 0, 0, imagecolorallocate($bg, 255, 255, 255));
imagealphablending($bg, 1);
imagecopy($bg, $img, 0, 0, 0, 0, $width, $height);
// $fsFile = $fs->getFile($image); // the original png file
// @todo Add ResponseController support to here and getReplacementPath.
$replacementPath = $this->getReplacementPath();
if (false === $replacementPath)
{
Log::addWarn('Png2Jpg replacement path failed');
$this->imageModel->getMeta()->convertMeta()->setError(self::ERROR_PATHFAIL);
return false; // @todo Add ResponseController something here.
}
// check old filename, replace with uniqued filename.
/** Quality is set to 90 and not using WP defaults (or filter) for good reason. Lower settings very quickly degrade the libraries output quality. Better to leave this hardcoded at 90 and let the ShortPixel API handle the optimization **/
if ($bool = imagejpeg($bg, $replacementPath, 90)) {
Log::addDebug("PNG2JPG doConvert created JPEG at $replacementPath");
$newSize = filesize($replacementPath); // This might invoke wrapper but ok
if (! is_null($this->virtual_filesize))
{
$origSize = $this->virtual_filesize;
}
else {
$origSize = $this->imageModel->getFileSize();
}
// Reload the file we just wrote.
$newFile = $fs->getFile($replacementPath);
if($newSize > $origSize * 0.95 || $newSize == 0) {
//if the image is not 5% smaller, don't bother.
//if the size is 0, a conversion (or disk write) problem happened, go on with the PNG
Log::addDebug("PNG2JPG converted image is larger ($newSize vs. $origSize), keeping the PNG");
$msg = __('Converted file is larger. Keeping original file', 'shortpixel-image-optimiser');
ResponseController::addData($this->imageModel->get('id'), 'message', $msg);
$newFile->delete();
$this->imageModel->getMeta()->convertMeta()->setError(self::ERROR_RESULTLARGER);
return false;
}
elseif (! $newFile->exists())
{
Log::addWarn('PNG imagejpeg file not written!', $uniqueFile->getFileName() );
$msg = __('Error - PNG file not written', 'shortpixel-image-optimiser');
ResponseController::addData($this->imageModel->get('id'), 'message', $msg);
$this->imageModel->getMeta()->convertMeta()->setError(self::ERROR_WRITEERROR);
return false;
}
else {
$this->newFile = $newFile;
}
Log::addDebug('PNG2jPG Converted');
}
$fs->flushImageCache();
return true;
}
public function restore()
{
$params = array(
'restore' => true,
);
$fs = \wpSPIO()->filesystem();
$this->setupReplacer();
$oldFileName = $this->imageModel->getFileName(); // Old File Name, Still .jpg
$newFileName = $this->imageModel->getFileBase() . '.png';
if ($this->imageModel->isScaled())
{
$oldFileName = $this->imageModel->getOriginalFile()->getFileName();
$newFileName = $this->imageModel->getOriginalFile()->getFileBase() . '.png';
}
$fsNewFile = $fs->getFile($this->imageModel->getFileDir() . $newFileName);
$this->newFile = $fsNewFile;
$this->setTarget($fsNewFile);
$this->updateMetaData($params);
$result = $this->replacer->replace();
$fs->flushImageCache();
}
protected function isTransparent() {
$isTransparent = false;
$transparent_pixel = $bg = false;
$imagePath = $this->imageModel->getFullPath();
// Check for transparency at the bit path.
if(ord(file_get_contents($imagePath, false, null, 25, 1)) & 4) {
Log::addDebug("PNG2JPG: 25th byte has third bit 1 - transparency");
$isTransparent = true;
// return true;
} else {
$contents = file_get_contents($imagePath);
if (stripos($contents, 'PLTE') !== false && stripos($contents, 'tRNS') !== false) {
$isTransparent = true;
}
if (false === $isTransparent) {
$width = $this->imageModel->get('width');
$height = $this->imageModel->get('height');
Log::addDebug("PNG2JPG Image width: " . $width . " height: " . $height . " aprox. size: " . round($width*$height*5/1024/1024) . "M memory limit: " . ini_get('memory_limit') . " USED: " . memory_get_usage());
$image = $this->getPNGImage();
if (false === $image)
{
return false;
}
Log::addDebug("PNG2JPG width $width height $height. Now checking pixels.");
//run through pixels until transparent pixel is found:
for ($i = 0; $i < $width; $i++) {
for ($j = 0; $j < $height; $j++) {
$rgba = imagecolorat($image, $i, $j);
if (($rgba & 0x7F000000) >> 24) {
$isTransparent = true;
break;
}
}
}
}
} // non-transparant.
Log::addDebug("PNG2JPG is " . (false === $isTransparent ? " not" : "") . " transparent");
return $isTransparent;
}
/** Try to load resource and an PNG via library */
protected function getPNGImage()
{
if (is_object($this->current_image))
{
return $this->current_image;
}
if ($this->imageModel->isScaled())
{
$imagePath = $this->imageModel->getOriginalFile()->getFullPath();
}
else {
$imagePath = $this->imageModel->getFullPath();
}
if (true === $this->imageModel->is_virtual())
{
$downloadHelper = DownloadHelper::getInstance();
Log::addDebug('PNG converter: Item is remote, attempting to download');
$tempFile = $downloadHelper->downloadFile($this->imageModel->getURL());
if (is_object($tempFile))
{
$imagePath = $tempFile->getFullPath();
$this->virtual_filesize = $tempFile->getFileSize();
}
}
$image = @imagecreatefrompng($imagePath);
if (! $image)
{
$this->current_image = false;
}
else
{
$this->current_image = $image;
}
return $this->current_image;
}
// Try to increase limits when doing heavy processing
private function raiseMemoryLimit()
{
if(function_exists('wp_raise_memory_limit')) {
wp_raise_memory_limit( 'image' );
}
}
} // class

View File

@ -0,0 +1,328 @@
<?php
namespace ShortPixel\Model;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
/** Loads a few environment variables handy to have nearby
*
* Notice - This is meant to be loaded via the plugin class. Easy access with wpSPIO()->getEnv().
*/
class EnvironmentModel extends \ShortPixel\Model
{
// Server and PHP
public $is_nginx;
public $is_apache;
public $is_gd_installed;
public $is_curl_installed;
private $disabled_functions = array();
// MultiSite
public $is_multisite;
public $is_mainsite;
// Integrations
public $has_nextgen;
// WordPress
public $is_front = false;
public $is_admin = false;
public $is_ajaxcall = false;
private $screen_is_set = false;
public $is_screen_to_use = false; // where shortpixel optimizer loads
public $is_our_screen = false; // where shortpixel hooks in more complicated functions.
public $is_gutenberg_editor = false;
public $is_bulk_page = false; // ShortPixel bulk screen.
public $screen_id = false;
// Debug flag
public $is_debug = false;
// Is the plugin configured to automatically optimize on upload hook?
public $is_autoprocess = false;
protected static $instance;
public function __construct()
{
$this->setServer();
$this->setWordPress();
add_action('plugins_loaded', array($this, 'setIntegrations') ); // not set on construct.
add_action('current_screen', array($this, 'setScreen') ); // Not set on construct
}
public static function getInstance()
{
if (is_null(self::$instance))
self::$instance = new EnvironmentModel();
/*if (! self::$instance->screen_is_set)
self::$instance->setScreen(); */
return self::$instance;
}
/** Check ENV is a specific function is allowed. Use this with functions that might be turned off on configurations
* @param $function String The name of the function being tested
* Note: In future this function can be extended with other function edge cases.
*/
public function is_function_usable($function)
{
if (count($this->disabled_functions) == 0)
{
$disabled = ini_get('disable_functions');
$this->disabled_functions = explode(',', $disabled);
}
if (isset($this->disabled_functions[$function]))
return false;
if (function_exists($function))
return true;
return false;
}
public function checkPHPVersion($needed)
{
if (version_compare(PHP_VERSION, $needed) >= 0 )
{
return true;
}
return false;
}
public function plugin_active($name)
{
switch($name)
{
case 'wpml':
$plugin = 'sitepress-multilingual-cms/sitepress.php';
break;
case 'polylang':
$plugin = 'polylang/polylang.php';
break;
case 'spai':
$plugin = 'shortpixel-adaptive-images/short-pixel-ai.php';
break;
case 's3-offload':
$plugin = 'amazon-s3-and-cloudfront/wordpress-s3.php';
break;
case 'woocommerce':
$plugin = 'woocommerce/woocommerce.php';
break;
default:
$plugin = 'none';
break;
}
if (!function_exists('is_plugin_active')) {
include_once(ABSPATH . 'wp-admin/includes/plugin.php');
}
return \is_plugin_active($plugin);
}
//https://www.php.net/manual/en/function.sys-getloadavg.php
public function getSystemLoad()
{
$load = sys_getloadavg();
}
/* https://github.com/WordPress/WordPress/blob/master/wp-includes/class-wp-image-editor-imagick.php */
public function hasImagick()
{
$editor = wp_get_image_editor(\wpSPIO()->plugin_path('res/img/test.jpg'));
$className = get_class($editor);
if ($className == 'WP_Image_Editor_Imagick')
return true;
else
return false;
}
public function hasOffload()
{
$off = \ShortPixel\External\Offload\Offloader::getInstance();
$name = $off->getOffloadName();
if (is_null($name))
return false;
else
return true;
}
public function getOffloadName()
{
$off = \ShortPixel\External\Offload\Offloader::getInstance();
$name = $off->getOffloadName();
return $name;
}
public function useVirtualHeavyFunctions()
{
$bool = apply_filters('shortpixel/file/virtual/heavy_features', true);
return $bool;
}
private function setServer()
{
$this->is_nginx = ! empty($_SERVER["SERVER_SOFTWARE"]) && strpos(strtolower(wp_unslash($_SERVER["SERVER_SOFTWARE"])), 'nginx') !== false ? true : false;
$this->is_apache = ! empty($_SERVER["SERVER_SOFTWARE"]) && strpos(strtolower(wp_unslash($_SERVER["SERVER_SOFTWARE"])), 'apache') !== false ? true : false;
$this->is_gd_installed = function_exists('imagecreatefrompng') && function_exists('imagejpeg');
$this->is_curl_installed = function_exists('curl_init');
}
private function setWordPress()
{
$this->is_multisite = (function_exists("is_multisite") && is_multisite()) ? true : false;
$this->is_mainsite = (function_exists('is_main_site') && true === is_main_site()) ? true : false;
$this->determineFrontBack();
if (wp_doing_ajax())
{
$this->is_ajaxcall = true;
}
$this->is_debug = Log::debugIsActive();
if (\wpSPIO()->settings()->autoMediaLibrary == 1)
$this->is_autoprocess = true;
}
// check if this request is front or back.
protected function determineFrontBack()
{
if ( is_admin() || wp_doing_ajax() )
$this->is_admin = true;
else
$this->is_front = true;
}
public function setScreen($screen)
{
// WordPress pages where we'll be active on.
// https://codex.wordpress.org/Plugin_API/Admin_Screen_Reference
$use_screens = array(
'edit-post_tag', // edit tags
'upload', // media library
'attachment', // edit media
'post', // post screen
'page', // page editor
'edit-post', // edit post
'new-post', // new post
'edit-page', // all pages
'media', // add new item screen
);
$use_screens = apply_filters('shortpixel/init/optimize_on_screens', $use_screens);
$this->screen_id = $screen->id;
if(is_array($use_screens) && in_array($screen->id, $use_screens)) {
$this->is_screen_to_use = true;
}
// Our pages.
$pages = \wpSPIO()->get_admin_pages();
// the main WP pages where SPIO hooks a lot of functions into, our operating area.
$wp_pages = array('upload', 'attachment');
$pages = array_merge($pages, $wp_pages);
/* pages can be null in certain cases i.e. plugin activation.
* treat those cases as improper screen set.
*/
if (is_null($pages))
{
return false;
}
if ( in_array($screen->id, $pages))
{
$this->is_screen_to_use = true;
$this->is_our_screen = true;
// Strpos instead of full screen id, because the first page (media_page) is not reliable and can change.
if ( strpos($screen->id, 'wp-short-pixel-bulk') !== false)
$this->is_bulk_page = true;
}
elseif (is_object($screen) && method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() ) {
$this->is_screen_to_use = true;
$this->is_gutenberg_editor = true;
}
$this->screen_is_set = true;
}
public function setIntegrations()
{
$ng = \ShortPixel\NextGenController::getInstance();
$this->has_nextgen = $ng->has_nextgen();
}
//set default move as "list". only set once, it won't try to set the default mode again.
public function setDefaultViewModeList()
{
$settings = \wpSPIO()->settings();
if( $settings->mediaLibraryViewMode == false)
{
$settings->mediaLibraryViewMode = 1;
$currentUserID = false;
if ( function_exists('wp_get_current_user') ) {
$current_user = wp_get_current_user();
$currentUserID = $current_user->ID;
update_user_meta($currentUserID, "wp_media_library_mode", "list");
}
}
}
public function getRelativePluginSlug()
{
$dir = SHORTPIXEL_PLUGIN_DIR;
$file = SHORTPIXEL_PLUGIN_FILE;
$fs = \wpSPIO()->filesystem();
$plugins_dir = $fs->getDirectory($dir)->getParent();
$slug = str_replace($plugins_dir->getPath(), '', $file);
return $slug;
}
public function useDoubleWebpExtension()
{
if (defined('SHORTPIXEL_USE_DOUBLE_WEBP_EXTENSION') && SHORTPIXEL_USE_DOUBLE_WEBP_EXTENSION)
return true;
return false;
}
public function useDoubleAvifExtension()
{
if (defined('SHORTPIXEL_USE_DOUBLE_AVIF_EXTENSION') && SHORTPIXEL_USE_DOUBLE_AVIF_EXTENSION)
return true;
return false;
}
public function useTrustedMode()
{
if (defined('SHORTPIXEL_TRUSTED_MODE') && true === SHORTPIXEL_TRUSTED_MODE)
{
return true;
}
return false;
}
}

View File

@ -0,0 +1,557 @@
<?php
namespace ShortPixel\Model\File;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Helper\UtilHelper as UtilHelper;
/* Model for Directories
*
* For all low-level operations on directories
* Private model of FileSystemController. Please get your directories via there.
*
*/
class DirectoryModel extends \ShortPixel\Model
{
// Directory info
protected $path;
protected $name;
// Directory status
protected $exists = null;
protected $is_writable = null;
protected $is_readable = null;
protected $is_virtual = null;
protected $fields = array();
protected $new_directory_permission = 0755;
public static $TRUSTED_MODE = false;
/** Creates a directory model object. DirectoryModel directories don't need to exist on FileSystem
*
* When a filepath is given, it will remove the file part.
* @param $path String The Path
*/
public function __construct($path)
{
$fs = \wpSPIO()->filesystem();
// Test if path actually has someting, otherwise just bail.
if (strlen(trim($path)) == 0)
{
return false;
}
if ($fs->pathIsUrl($path))
{
$pathinfo = pathinfo($path);
if (isset($pathinfo['extension'])) // check if this is a file, remove the file information.
{
$path = $pathinfo['dirname'];
}
$this->is_virtual = true;
$this->is_readable = true; // assume
$this->exists = true;
}
// When in trusted mode prevent filesystem checks as much as possible.
if (true === self::$TRUSTED_MODE)
{
$this->exists = true;
$this->is_writable = true;
$this->is_readable = true;
}
// On virtual situation this would remove the slashes on :// , causing issues with offload et al.
if (false === $this->is_virtual)
$path = UtilHelper::spNormalizePath($path);
if (! $this->is_virtual() && ! is_dir($path) ) // path is wrong, *or* simply doesn't exist.
{
/* Test for file input.
* If pathinfo is fed a fullpath, it rips of last entry without setting extension, don't further trust.
* If it's a file extension is set, then trust.
*/
$pathinfo = pathinfo($path);
if (isset($pathinfo['extension']))
{
$path = $pathinfo['dirname'];
}
elseif (is_file($path))
$path = dirname($path);
}
if (! $this->is_virtual() && ! is_dir($path))
{
/* Check if realpath improves things. We support non-existing paths, which realpath fails on, so only apply on result.
Moved realpath to check after main pathinfo is set. Reason is that symlinked directories which don't include the WordPress upload dir will start to fail in file_model on processpath ( doesn't see it as a wp path, starts to try relative path). Not sure if realpath should be used anyhow in this model /BS
*/
$testpath = realpath($path);
if ($testpath)
$path = $testpath;
}
$this->path = trailingslashit($path);
// Basename doesn't work properly on non-latin ( cyrillic, greek etc ) directory names, returning the parent path instead.
$dir = new \SplFileInfo($path);
//basename($this->path);
$this->name = $dir->getFileName();
}
public function __toString()
{
return (string) $this->path;
}
/** Returns path *with* trailing slash
*
* @return String Path with trailing slash
*/
public function getPath()
{
return $this->path;
}
public function getModified()
{
return filemtime($this->path);
}
/**
* Get basename of the directory. Without path
*/
public function getName()
{
return $this->name;
}
public function exists()
{
if (is_null($this->exists))
{
$this->exists = file_exists($this->path) && is_dir($this->path);
}
return $this->exists;
}
public function is_writable()
{
if (is_null($this->is_writable))
{
$this->is_writable = is_writable($this->path);
}
return $this->is_writable;
}
public function is_readable()
{
if (is_null($this->is_readable))
{
$this->is_readable = is_readable($this->path);
}
return $this->is_readable;
}
public function is_virtual()
{
return $this->is_virtual;
}
/** Try to obtain the path, minus the installation directory.
* @return Mixed False if this didn't work, Path as string without basedir if it did. With trailing slash, without starting slash.
*/
public function getRelativePath()
{
// not used anywhere in directory.
// $upload_dir = wp_upload_dir(null, false);
$install_dir = get_home_path();
if($install_dir == '/') {
$install_dir = \wpSPIO()->filesystem()->getWPAbsPath();
}
$install_dir = trailingslashit($install_dir);
$path = $this->getPath();
// try to build relativePath without first slash.
$relativePath = str_replace($install_dir, '', $path);
if (is_dir( $install_dir . $relativePath) === false)
{
$test_path = $this->reverseConstructPath($path, $install_dir);
if ($test_path !== false)
{
$relativePath = $test_path;
}
else {
if($test_path = $this->constructUsualDirectories($path))
{
$relativePath = $test_path;
}
}
}
// If relativePath has less amount of characters, changes are this worked.
if (strlen($path) > strlen($relativePath))
{
return ltrim(trailingslashit($relativePath), '/');
}
return false;
}
private function reverseConstructPath($path, $install_path)
{
// Array value to reset index
$pathar = array_values(array_filter(explode('/', $path)));
$parts = array();
if (is_array($pathar))
{
// Reverse loop the structure until solid ground is found.
for ($i = (count($pathar)); $i > 0; $i--)
{
$parts[] = $pathar[$i - 1];
$testpath = implode('/', array_reverse($parts));
// if the whole thing exists
if (is_dir($install_path . $testpath) === true)
{
return $testpath;
}
}
}
return false;
}
/* Last Resort function to just reduce path to various known WorPress paths. */
private function constructUsualDirectories($path)
{
$pathar = array_values(array_filter(explode('/', $path))); // array value to reset index
$testpath = false;
if ( ($key = array_search('wp-content', $pathar)) !== false)
{
$testpath = implode('/', array_slice($pathar, $key));
}
elseif ( ($key = array_search('uploads', $pathar)) !== false)
{
$testpath = implode('/', array_slice($pathar, $key));
}
return $testpath;
}
/** Checks the directory into working order
* Tries to create directory if it doesn't exist
* Tries to fix file permission if writable is needed
* @param $check_writable Boolean Directory should be writable
*/
public function check($check_writable = false)
{
$permission = $this->getPermissionRecursive();
if ($permission === false) // if something wrong, return to default.
{
$permission = $this->new_directory_permission;
}
if (! $this->exists())
{
Log::addInfo('Directory does not exists. Try to create recursive ' . $this->path . ' with ' . $permission);
$result = @mkdir($this->path, $permission , true);
chmod ($this->path, $permission );
if (! $result)
{
$error = error_get_last();
Log::addWarn('MkDir failed: ' . $error['message'], array($error));
}
// reset.
$this->exists = null;
$this->is_readable = null;
$this->is_writable = null;
}
if ($this->exists() && $check_writable && ! $this->is_writable())
{
chmod($this->path, $permission);
if (! $this->is_writable()) // perhaps parent permission is no good.
{
chmod($this->path, $this->new_directory_permission);
}
}
if (! $this->exists())
{
Log::addInfo('Directory does not exist :' . $this->path);
return false;
}
if ($check_writable && !$this->is_writable())
{
Log::addInfo('Directory not writable :' . $this->path);
return false;
}
return true;
}
public function getPermissionRecursive()
{
$parent = $this->getParent();
if (! $parent->exists())
{
return $parent->getPermissionRecursive();
}
else
{
return $parent->getPermissions();
}
}
/* Get files from directory
* @returns Array|boolean Returns false if something wrong w/ directory, otherwise a files array of FileModel Object.
*/
public function getFiles($args = array())
{
$defaults = array(
'date_newer' => null,
'exclude_files' => null,
'include_files' => null,
);
$args = wp_parse_args($args, $defaults);
// if all filters are set to null, so point in checking those.
$has_filters = (count(array_filter($args)) > 0) ? true : false;
if ( ! $this->exists() || ! $this->is_readable() )
return false;
$fileArray = array();
if ($handle = opendir($this->path)) {
while (false !== ($entry = readdir($handle))) {
if ( ($entry != "." && $entry != "..") && ! is_dir($this->path . $entry) ) {
$fileObj = new FileModel($this->path . $entry);
if ($has_filters)
{
if ($this->fileFilter($fileObj,$args) === false)
{
$fileObj = null;
}
}
if (! is_null($fileObj))
$fileArray[] = $fileObj;
}
}
closedir($handle);
}
/*
if ($has_filters)
{
$fileArray = array_filter($fileArray, function ($file) use ($args) {
return $this->fileFilter($file, $args);
} );
} */
return $fileArray;
}
// @return boolean true if it should be kept in array, false if not.
private function fileFilter(FileModel $file, $args)
{
$filter = true;
if (! is_null($args['include_files']))
{
foreach($args['include_files'] as $inc)
{
// If any in included is true, filter is good for us.
$filter = false;
if (strpos( strtolower($file->getRawFullPath()), strtolower($inc) ) !== false)
{
$filter = true;
break;
}
}
}
if (! is_null($args['date_newer']))
{
$modified = $file->getModified();
if ($modified < $args['date_newer'] )
$filter = false;
}
if (! is_null($args['exclude_files']))
{
foreach($args['exclude_files'] as $ex)
{
if (strpos( strtolower($file->getRawFullPath()), strtolower($ex) ) !== false)
$filter = false;
}
}
return $filter;
}
/** Get subdirectories from directory
* * @returns Array|boolean Returns false if something wrong w/ directory, otherwise a files array of DirectoryModel Object.
*/
public function getSubDirectories()
{
if (! $this->exists() || ! $this->is_readable())
{
return false;
}
$dirIt = new \DirectoryIterator($this->path);
$dirArray = array();
foreach ($dirIt as $fileInfo)
{ // IsDot must go first here, or there is possiblity to run into openbasedir restrictions.
if (! $fileInfo->isDot() && $fileInfo->isDir() && $fileInfo->isReadable())
{
if ('ShortPixel\Model\File\DirectoryOtherMediaModel' == get_called_class())
{
$dir = new DirectoryOtherMediaModel($fileInfo->getRealPath());
}
else
{
$dir = new DirectoryModel($fileInfo->getRealPath());
}
if ($dir->exists())
$dirArray[] = $dir;
}
}
return $dirArray;
}
/** Check if this dir is a subfolder
* @param DirectoryModel The directoryObject that is tested as the parent */
public function isSubFolderOf(DirectoryModel $dir)
{
// the same path, is not a subdir of.
if ($this->getPath() === $dir->getPath())
return false;
// the main path must be followed from the beginning to be a subfolder.
if (strpos($this->getPath(), $dir->getPath() ) === 0)
{
return true;
}
return false;
}
//** Note, use sparingly, recursive function
public function getFolderSize()
{
// \wpSPIO()->filesystem()->getFilesRecursive($this)
$size = 0;
$files = $this->getFiles();
// GetFiles can return Boolean false on missing directory.
if (! is_array($files))
{
return $size;
}
foreach($files as $fileObj)
{
$size += $fileObj->getFileSize();
}
unset($files); //attempt at performance.
$subdirs = $this->getSubDirectories();
foreach($subdirs as $subdir)
{
$size += $subdir->getFolderSize();
}
return $size;
}
/** Get this paths parent */
public function getParent()
{
$path = $this->getPath();
$parentPath = dirname($path);
$parentDir = new DirectoryModel($parentPath);
return $parentDir;
}
public function getPermissions()
{
if (! $this->exists())
{
Log::addWarning('Directory not existing (fileperms): '. $this->getPath() );
return false;
}
$perms = fileperms($this->getPath());
if ($perms !== false)
{
return $perms;
}
else
return false;
}
public function delete()
{
return rmdir($this->getPath());
}
/** This will try to remove the whole structure. Use with care.
* This is mostly used to clear the backups.
*/
public function recursiveDelete()
{
if (! $this->exists() || ! $this->is_writable())
return false;
// This is a security measure to prevent unintended wipes.
$wpdir = \wpSPIO()->filesystem()->getWPUploadBase();
if (! $this->isSubFolderOf($wpdir))
return false;
$files = $this->getFiles();
$subdirs = $this->getSubDirectories();
foreach($files as $file)
$file->delete();
foreach($subdirs as $subdir)
$subdir->recursiveDelete();
$this->delete();
}
}

View File

@ -0,0 +1,637 @@
<?php
namespace ShortPixel\Model\File;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Notices\NoticeController as Notice;
use \ShortPixel\Model\File\DirectoryModel as DirectoryModel;
use \ShortPixel\Model\Image\ImageModel as ImageModel;
use ShortPixel\Controller\OptimizeController as OptimizeController;
use ShortPixel\Controller\OtherMediaController as OtherMediaController;
// extends DirectoryModel. Handles ShortPixel_meta database table
// Replacing main parts of shortpixel-folder
class DirectoryOtherMediaModel extends DirectoryModel
{
protected $id = -1; // if -1, this might not exist yet in Dbase. Null is not used, because that messes with isset
protected $name;
protected $status = 0;
protected $fileCount = 0; // inherent onreliable statistic in dbase. When insert / batch insert the folder count could not be updated, only on refreshFolder which is a relative heavy function to use on every file upload. Totals are better gotten from a stat-query, on request.
protected $updated = 0;
protected $created = 0;
protected $checked = 0;
protected $path_md5;
protected $is_nextgen = false;
protected $in_db = false;
protected $is_removed = false;
protected $last_message;
//protected $stats;
protected static $stats;
const DIRECTORY_STATUS_REMOVED = -1;
const DIRECTORY_STATUS_NORMAL = 0;
const DIRECTORY_STATUS_NEXTGEN = 1;
/** Path or Folder Object, from SpMetaDao
*
*/
public function __construct($path)
{
if (is_object($path)) // Load directly via Database object, this saves a query.
{
$folder = $path;
$path = $folder->path;
parent::__construct($path);
$this->loadFolder($folder);
}
else
{
parent::__construct($path);
$this->loadFolderbyPath($path);
}
}
public function get($name)
{
if (property_exists($this, $name))
return $this->$name;
return null;
}
public function set($name, $value)
{
if (property_exists($this, $name))
{
$this->$name = $value;
return true;
}
return null;
}
public static function getAllStats()
{
if (is_null(self::$stats))
{
global $wpdb;
$sql = 'SELECT SUM(CASE WHEN status = 2 OR status = -11 THEN 1 ELSE 0 END) optimized, SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) waiting, count(*) total, folder_id FROM ' . $wpdb->prefix . 'shortpixel_meta GROUP BY folder_id';
$result = $wpdb->get_results($sql, ARRAY_A);
$stats = array();
foreach($result as $rawdata)
{
$folder_id = $rawdata['folder_id'];
$data = array(
'optimized' => (int) $rawdata['optimized'],
'waiting' => (int) $rawdata['waiting'],
'total' => (int) $rawdata['total'],
);
$stats[$folder_id] = $data;
}
self::$stats = $stats;
}
return self::$stats;
}
public function getStats()
{
$stats = self::getAllStats(); // Querying all stats is more efficient than one-by-one
if (isset($stats[$this->id]))
{
return $stats[$this->id];
}
else {
global $wpdb;
$sql = "SELECT SUM(CASE WHEN status = 2 OR status = -11 THEN 1 ELSE 0 END) optimized, "
. "SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) waiting, count(*) total "
. "FROM " . $wpdb->prefix . "shortpixel_meta "
. "WHERE folder_id = %d";
$sql = $wpdb->prepare($sql, $this->id);
$res = $wpdb->get_row($sql, ARRAY_A);
if (is_array($res))
{
$result = array(
'optimized' => (int) $res['optimized'],
'waiting' => (int) $res['waiting'],
'total' => (int) $res['total'],
);
return $result;
}
else {
return false;
}
}
}
public function save()
{
// Simple Update
global $wpdb;
$data = array(
// 'id' => $this->id,
'status' => $this->status,
'file_count' => $this->fileCount,
'ts_updated' => $this->timestampToDB($this->updated),
'ts_checked' => $this->timestampToDB($this->checked),
'name' => $this->name,
'path' => $this->getPath(),
);
$format = array('%d', '%d', '%s', '%s', '%s', '%s');
$table = $wpdb->prefix . 'shortpixel_folders';
$is_new = false;
$result = false;
if ($this->in_db) // Update
{
$result = $wpdb->update($table, $data, array('id' => $this->id), $format);
}
else // Add new
{
// Fallback. In case some other process adds it. This happens with Nextgen.
if (true === $this->loadFolderByPath($this->getPath()))
{
$result = $wpdb->update($table, $data, array('id' => $this->id), $format);
}
else
{
$data['ts_created'] = $this->timestampToDB(time());
$this->id = $wpdb->insert($table, $data);
if ($this->id !== false)
{
$is_new = true;
$result = $this->id;
}
}
}
// reloading because action can create a new DB-entry, which will not be reflected (in id )
if ($is_new)
{
$this->loadFolderByPath($this->getPath());
}
return $result;
}
public function delete()
{
$id = $this->id;
if (! $this->in_db)
{
Log::addError('Trying to remove Folder without being in the database (in_db false) ' . $id, $this->getPath());
}
global $wpdb;
$otherMedia = OtherMediaController::getInstance();
// Remove all files from this folder that are not optimized.
$sql = "DELETE FROM " . $otherMedia->getMetaTable() . ' WHERE status <> 2 and folder_id = %d';
$sql = $wpdb->prepare($sql, $this->id);
$wpdb->query($sql);
// Check if there are any images left.
$sql = 'SELECT count(id) FROM ' . $otherMedia->getMetaTable() . ' WHERE folder_id = %d';
$sql = $wpdb->prepare($sql, $this->id);
$numImages = $wpdb->get_var($sql);
if ($numImages > 0)
{
$sql = 'UPDATE ' . $otherMedia->getFolderTable() . ' SET status = -1 where id = %d';
$sql = $wpdb->prepare($sql, $this->id);
$result = $wpdb->query($sql);
}
else
{
$sql = 'DELETE FROM ' . $otherMedia->getFolderTable() . ' where id = %d';
$sql = $wpdb->prepare($sql, $this->id);
$result = $wpdb->query($sql);
}
return $result;
}
public function isRemoved()
{
if ($this->is_removed)
return true;
else
return false;
}
/** Updates the updated variable on folder to indicating when the last file change was made
* @return boolean True if file were changed since last update, false if not
*/
public function updateFileContentChange()
{
if (! $this->exists() )
return false;
$old_time = $this->updated;
$time = $this->recurseLastChangeFile();
$this->updated = $time;
$this->save();
if ($old_time !== $time)
return true;
else
return false;
}
/** Crawls the folder and check for files that are newer than param time, or folder updated
* Note - last update timestamp is not updated here, needs to be done separately.
*/
public function refreshFolder($force = false)
{
if ($force === false)
{
$time = $this->updated;
}
else
{
$time = 0; //force refresh of the whole.
}
$stats = $this->getStats();
$total_before = $stats['total'];
if (! $this->checkDirectory(true))
{
Log::addWarn('Refreshing directory, something wrong in checkDirectory ' . $this->getPath());
return false;
}
if ($this->id <= 0)
{
Log::addWarn('FolderObj from database is not there, while folder seems ok ' . $this->getPath() );
return false;
}
elseif (! $this->exists())
{
$message = sprintf(__('Folder %s does not exist! ', 'shortpixel-image-optimiser'), $this->getPath());
$this->last_message = $message;
Notice::addError( $message );
return false;
}
elseif (! $this->is_writable())
{
$message = sprintf(__('Folder %s is not writeable. Please check permissions and try again.','shortpixel-image-optimiser'),$this->getPath());
$this->last_message = $message;
Notice::addWarning( $message );
return false;
}
$fs = \wpSPIO()->filesystem();
$filter = ($time > 0) ? array('date_newer' => $time) : array();
$filter['exclude_files'] = array('.webp', '.avif');
$filter['include_files'] = ImageModel::PROCESSABLE_EXTENSIONS;
$files = $fs->getFilesRecursive($this, $filter);
\wpSPIO()->settings()->hasCustomFolders = time(); // note, check this against bulk when removing. Custom Media Bulk depends on having a setting.
$result = $this->addImages($files);
// Reset stat.
unset(self::$stats[$this->id]);
$stats = $this->getStats();
$this->fileCount = $stats['total'];
$this->checked = time();
$this->save();
$stats['new'] = $stats['total'] - $total_before;
return $stats;
}
/** Check if a directory is allowed. Directory can't be media library, outside of root, or already existing in the database
* @param $silent If not allowed, don't generate notices.
*
*/
public function checkDirectory($silent = false)
{
$fs = \wpSPIO()->filesystem();
$rootDir = $fs->getWPFileBase();
$backupDir = $fs->getDirectory(SHORTPIXEL_BACKUP_FOLDER);
$otherMediaController = OtherMediaController::getInstance();
if (! $this->exists())
{
$message = __('Could not be added, directory not found: ' . $path ,'shortpixel-image-optimiser');
$this->last_message = $message;
if (false === $silent)
{
Notice::addError($message);
}
return false;
}
elseif (! $this->isSubFolderOf($rootDir) && $this->getPath() != $rootDir->getPath() )
{
$message = sprintf(__('The %s folder cannot be processed as it\'s not inside the root path of your website (%s).','shortpixel-image-optimiser'),$this->getPath(), $rootDir->getPath());
$this->last_message = $message;
if (false === $silent)
{
Notice::addError( $message );
}
return false;
}
elseif($this->isSubFolderOf($backupDir) || $this->getPath() == $backupDir->getPath() )
{
$message = __('This folder contains the ShortPixel Backups. Please select a different folder.','shortpixel-image-optimiser');
$this->last_message = $message;
if (false === $silent)
{
Notice::addError( $message );
}
return false;
}
elseif( $otherMediaController->checkIfMediaLibrary($this) )
{
$message = __('This folder contains Media Library images. To optimize Media Library images please go to <a href="upload.php?mode=list">Media Library list view</a> or to <a href="upload.php?page=wp-short-pixel-bulk">ShortPixel Bulk page</a>.','shortpixel-image-optimiser');
$this->last_message = $message;
if (false === $silent)
{
Notice::addError($message);
}
return false;
}
elseif (! $this->is_writable())
{
$message = sprintf(__('Folder %s is not writeable. Please check permissions and try again.','shortpixel-image-optimiser'),$this->getPath());
$this->last_message = $message;
if (false === $silent)
{
Notice::addError( $message );
}
return false;
}
else
{
$folders = $otherMediaController->getAllFolders();
foreach($folders as $folder)
{
if ($this->isSubFolderOf($folder))
{
if (false === $silent)
{
Notice::addError(sprintf(__('This folder is a subfolder of an already existing Other Media folder. Folder %s can not be added', 'shortpixel-image-optimiser'), $this->getPath() ));
}
return false;
}
}
}
return true;
}
/*
public function getFiles($args = array())
{
// Check if this directory if not forbidden.
if (! $this->checkDirectory(true))
{
return array();
}
return parent::getFiles($args);
}
*/
/* public function getSubDirectories()
{
$dirs = parent::getSubDirectories();
$checked = array();
foreach($dirs as $dir)
{
if ($dir->checkDirectory(false))
{
$checked[] = $dir;
}
else
{
Log::addDebug('Illegal directory' . $dir->getPath());
}
}
return $checked;
}
*/
private function recurseLastChangeFile($mtime = 0)
{
$ignore = array('.','..');
// Directories without read rights should not be checked at all.
if (! $this->is_readable())
return $mtime;
$path = $this->getPath();
$files = scandir($path);
// no files, nothing to update.
if (! is_array($files))
{
return $mtime;
}
$files = array_diff($files, $ignore);
$mtime = max($mtime, filemtime($path));
foreach($files as $file) {
$filepath = $path . $file;
if (is_dir($filepath)) {
$mtime = max($mtime, filemtime($filepath));
$subDirObj = new DirectoryOtherMediaModel($filepath);
$subdirtime = $subDirObj->recurseLastChangeFile($mtime);
if ($subdirtime > $mtime)
$mtime = $subdirtime;
}
}
return $mtime;
}
private function timestampToDB($timestamp)
{
if ($timestamp == 0) // when adding / or empty.
$timestamp = time();
return date("Y-m-d H:i:s", $timestamp);
}
private function DBtoTimestamp($date)
{
if (is_null($date))
{
$timestamp = time();
}
else {
$timestamp =strtotime($date);
}
return $timestamp;
}
/** This function is called by OtherMediaController / RefreshFolders. Other scripts should not call it
* @public
* @param Array of CustomMediaImageModel stubs.
*/
public function addImages($files) {
global $wpdb;
if ( apply_filters('shortpixel/othermedia/addfiles', true, $files, $this) === false)
{
return false;
}
$values = array();
$optimizeControl = new OptimizeController();
$otherMediaControl = OtherMediaController::getInstance();
$activeFolders = $otherMediaControl->getActiveDirectoryIDS();
$fs = \wpSPIO()->filesystem();
foreach($files as $fileObj)
{
// Note that load is set to false here.
$imageObj = $fs->getCustomStub($fileObj->getFullPath(), false);
// image already exists
if ($imageObj->get('in_db') == true)
{
// Load meta to make it check the folder_id.
$imageObj->loadMeta();
// Check if folder id is something else. This might indicate removed or inactive folders.
// If in inactive folder, move to current active.
if ($imageObj->get('folder_id') !== $this->id)
{
if (! in_array($imageObj->get('folder_id'), $activeFolders) )
{
$imageObj->setFolderId($this->id);
$imageObj->saveMeta();
}
}
// If in Db, but not optimized and autoprocess is on; add to queue for optimizing
if (\wpSPIO()->env()->is_autoprocess && $imageObj->isProcessable())
{
$optimizeControl->addItemToQueue($imageObj);
}
continue;
}
elseif ($imageObj->isProcessable()) // Check strict on Processable here.
{
$imageObj->setFolderId($this->id);
$imageObj->saveMeta();
if (\wpSPIO()->env()->is_autoprocess)
{
$optimizeControl->addItemToQueue($imageObj);
}
}
else {
}
}
}
private function loadFolderByPath($path)
{
//$folders = self::getFolders(array('path' => $path));
global $wpdb;
$sql = 'SELECT * FROM ' . $wpdb->prefix . 'shortpixel_folders where path = %s ';
$sql = $wpdb->prepare($sql, $path);
$folder = $wpdb->get_row($sql);
if (! is_object($folder))
return false;
else
{
$this->loadFolder($folder);
$this->in_db = true; // exists in database
return true;
}
}
/** Loads from database into model, the extra data of this model. */
private function loadFolder($folder)
{
// $class = get_class($folder);
// Setters before action
$this->id = $folder->id;
if ($this->id > 0)
$this->in_db = true;
$this->updated = property_exists($folder,'ts_updated') ? $this->DBtoTimestamp($folder->ts_updated) : time();
$this->created = property_exists($folder,'ts_created') ? $this->DBtoTimestamp($folder->ts_created) : time();
$this->checked = property_exists($folder,'ts_checked') ? $this->DBtoTimestamp($folder->ts_checked) : time();
$this->fileCount = property_exists($folder,'file_count') ? $folder->file_count : 0; // deprecated, do not rely on.
$this->status = $folder->status;
if (strlen($folder->name) == 0)
$this->name = basename($folder->path);
else
$this->name = $folder->name;
do_action('shortpixel/othermedia/folder/load', $this->id, $this);
// Making conclusions after action.
if ($this->status == -1)
$this->is_removed = true;
if ($this->status == self::DIRECTORY_STATUS_NEXTGEN)
{
$this->is_nextgen = true;
}
}
}

View File

@ -0,0 +1,889 @@
<?php
namespace ShortPixel\Model\File;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Helper\UtilHelper as UtilHelper;
/* FileModel class.
*
*
* - Represents a -single- file.
* - Can handle any type
* - Usually controllers would use a collection of files
* - Meant for all low-level file operations and checks.
* - Every file can have a backup counterpart.
*
*/
class FileModel extends \ShortPixel\Model
{
// File info
protected $fullpath = null;
protected $rawfullpath = null;
protected $filename = null; // filename + extension
protected $filebase = null; // filename without extension
protected $directory = null;
protected $extension = null;
protected $mime = null;
protected $permissions = null;
protected $filesize = null;
// File Status
protected $exists = null;
protected $is_writable = null;
protected $is_directory_writable = null;
protected $is_readable = null;
protected $is_file = null;
protected $is_virtual = false;
protected $virtual_status = null;
protected $status;
protected $backupDirectory;
const FILE_OK = 1;
const FILE_UNKNOWN_ERROR = 2;
public static $TRUSTED_MODE = false;
// Constants for is_virtual . Virtual Remote is truly a remote file, not writable from machine. Stateless means it looks remote, but it's a protocol-based filesystem remote or not - that will accept writes / is_writable. Stateless also mean performance issue since it can't be 'translated' to a local path. All communication happens over http wrapper, so check should be very limited.
public static $VIRTUAL_REMOTE = 1;
public static $VIRTUAL_STATELESS = 2;
/** Creates a file model object. FileModel files don't need to exist on FileSystem */
public function __construct($path)
{
$this->rawfullpath = $path;
if (is_null($path))
{
Log::addWarn('FileModel: Loading null path! ');
return false;
}
if (strlen($path) > 0)
$path = trim($path);
$this->fullpath = $path;
$this->checkTrustedMode();
$fs = \wpSPIO()->filesystem();
if ($fs->pathIsUrl($path)) // Asap check for URL's to prevent remote wrappers from running.
{
$this->UrlToPath($path);
}
}
/* Get a string representation of file, the fullpath
* Note - this might be risky, without processedpath, in cases.
* @return String Full path processed or unprocessed.
*/
public function __toString()
{
return (string) $this->fullpath;
}
protected function setFileInfo()
{
$processed_path = $this->processPath($this->fullpath);
if ($processed_path !== false)
$this->fullpath = $processed_path; // set processed path if that went alright
$info = $this->mb_pathinfo($this->fullpath);
// Todo, maybe replace this with splFileINfo.
if ($this->is_file()) // only set fileinfo when it's an actual file.
{
$this->filename = isset($info['basename']) ? $info['basename'] : null; // filename + extension
$this->filebase = isset($info['filename']) ? $info['filename'] : null; // only filename
$this->extension = isset($info['extension']) ? strtolower($info['extension']) : null; // only (last) extension
}
}
/** Call when file status changed, so writable / readable / exists are not reliable anymore */
public function resetStatus()
{
$this->is_writable = null;
$this->is_directory_writable = null;
$this->is_readable = null;
$this->is_file = null;
$this->exists = null;
$this->is_virtual = null;
$this->filesize = null;
$this->permissions = null;
}
/**
* @param $forceCheck Forces a filesystem check instead of using cached. Use very sparingly. Implemented for retina on trusted mode.
*/
public function exists($forceCheck = false)
{
if (true === $forceCheck || is_null($this->exists))
{
if (true === $this->fileIsRestricted($this->fullpath))
{
$this->exists = false;
}
else {
$this->exists = (@file_exists($this->fullpath) && is_file($this->fullpath));
}
}
$this->exists = apply_filters('shortpixel_image_exists', $this->exists, $this->fullpath, $this); //legacy
$this->exists = apply_filters('shortpixel/file/exists', $this->exists, $this->fullpath, $this);
return $this->exists;
}
public function is_writable()
{
// Return when already asked / Stateless might set this
if (! is_null($this->is_writable))
{
return $this->is_writable;
}
elseif ($this->is_virtual())
{
$this->is_writable = false; // can't write to remote files
}
elseif (is_null($this->is_writable))
{
if ($this->exists())
{
$this->is_writable = @is_writable($this->fullpath);
}
else // quite expensive check to see if file is writable.
{
$res = $this->create();
$this->delete();
$this->is_writable = $res;
}
}
return $this->is_writable;
}
public function is_directory_writable()
{
// Return when already asked / Stateless might set this
if (! is_null($this->is_directory_writable))
{
return $this->is_directory_writable;
}
elseif ($this->is_virtual())
{
$this->is_directory_writable = false; // can't write to remote files
}
elseif (is_null($this->is_directory_writable))
{
$directory = $this->getFileDir();
if (is_object($directory) && $directory->exists())
{
$this->is_directory_writable = $directory->is_writable();
}
else {
$this->is_directory_writable = false;
}
}
return $this->is_directory_writable;
}
public function is_readable()
{
if (is_null($this->is_readable))
$this->is_readable = @is_readable($this->fullpath);
return $this->is_readable;
}
// A file is virtual when the file is remote with URL and no local alternative is present.
public function is_virtual()
{
if ( is_null($this->is_virtual))
$this->is_virtual = false; // return bool
return $this->is_virtual;
}
/* Function checks if path is actually a file. This can be used to check possible confusion if a directory path is given to filemodel */
public function is_file()
{
if ($this->is_virtual()) // don't look further when virtual
{
$this->is_file = true;
return $this->is_file;
}
elseif (is_null($this->is_file))
{
if ($this->exists())
{
if (basename($this->fullpath) == '..' || basename($this->fullpath) == '.')
$this->is_file = false;
else
$this->is_file = is_file($this->fullpath);
}
else // file can not exist, but still have a valid filepath format. In that case, if file should return true.
{
/* if file does not exist on disk, anything can become a file ( with/ without extension, etc). Meaning everything non-existing is a potential file ( or directory ) until created. */
if (basename($this->fullpath) == '..' || basename($this->fullpath) == '.') // don't see this as file.
{
$this->is_file = false;
}
else if (! file_exists($this->fullpath) && ! is_dir($this->fullpath))
{
$this->is_file = true;
}
else //if (! is_file($this->fullpath)) // can be a non-existing directory. /
{
$this->is_file = false;
}
}
}
return $this->is_file;
}
public function getModified()
{
return filemtime($this->fullpath);
}
public function hasBackup()
{
$directory = $this->getBackupDirectory();
if (! $directory)
return false;
$backupFile = $directory . $this->getBackupFileName();
if (file_exists($backupFile) && ! is_dir($backupFile) )
return true;
else {
return false;
}
}
/** Tries to retrieve an *existing* BackupFile. Returns false if not present.
* This file might not be writable.
* To get writable directory reference to backup, use FileSystemController
*/
public function getBackupFile()
{
if ($this->hasBackup())
return new FileModel($this->getBackupDirectory() . $this->getBackupFileName() );
else
return false;
}
/** Function returns the filename for the backup. This is an own function so it's possible to manipulate backup file name if needed, i.e. conversion or enumeration */
public function getBackupFileName()
{
return $this->getFileName();
}
/** Returns the Directory Model this file resides in
*
* @return DirectoryModel Directorymodel Object
*/
public function getFileDir()
{
$fullpath = $this->getFullPath(); // triggers a file lookup if needed.
// create this only when needed.
if (is_null($this->directory) && strlen($fullpath) > 0)
{
// Feed to full path to DirectoryModel since it checks if input is file, or dir. Using dirname here would cause errors when fullpath is already just a dirpath ( faulty input )
$this->directory = new DirectoryModel($fullpath);
}
return $this->directory;
}
public function getFileSize()
{
if (! is_null($this->filesize))
{
return $this->filesize;
}
elseif ($this->exists() && false === $this->is_virtual() )
{
$this->filesize = filesize($this->fullpath);
return $this->filesize;
}
elseif (true === $this->is_virtual())
{
return -1;
}
else
return 0;
}
// Creates an empty file
public function create()
{
if (! $this->exists() )
{
$fileDir = $this->getFileDir();
if (! is_null($fileDir) && $fileDir->exists())
{
$res = @touch($this->fullpath);
$this->exists = $res;
return $res;
}
}
else
Log::addWarn('Could not create/write file: ' . $this->fullpath);
return false;
}
public function append($message)
{
if (! $this->exists() )
$this->create();
if (! $this->is_writable() )
{
Log::addWarn('File append failed on ' . $this->getFullPath() . ' - not writable');
return false;
}
$handle = fopen($this->getFullPath(), 'a');
fwrite($handle, $message);
fclose($handle);
return true;
}
/** Copy a file to somewhere
*
* @param $destination String Full Path to new file.
*/
public function copy(FileModel $destination)
{
$sourcePath = $this->getFullPath();
$destinationPath = $destination->getFullPath();
Log::addDebug("Copy from $sourcePath to $destinationPath ");
if (! strlen($sourcePath) > 0 || ! strlen($destinationPath) > 0)
{
Log::addWarn('Attempted Copy on Empty Path', array($sourcePath, $destinationPath));
return false;
}
if (! $this->exists())
{
Log::addWarn('Tried to copy non-existing file - ' . $sourcePath);
return false;
}
$is_new = ($destination->exists()) ? false : true;
$status = @copy($sourcePath, $destinationPath);
if (! $status)
{
Log::addWarn('Could not copy file ' . $sourcePath . ' to' . $destinationPath);
}
else
{
$destination->resetStatus();
$destination->setFileInfo(); // refresh info.
}
//
do_action('shortpixel/filesystem/addfile', array($destinationPath, $destination, $this, $is_new));
return $status;
}
/** Move a file to somewhere
* This uses copy and delete functions and will fail if any of those fail.
* @param $destination String Full Path to new file.
*/
public function move(FileModel $destination)
{
$result = false;
if ($this->copy($destination))
{
$result = $this->delete();
if ($result == false)
{
Log::addError('Move can\'t remove file ' . $this->getFullPath());
}
$this->resetStatus();
$destination->resetStatus();
}
return $result;
}
/** Deletes current file
* This uses the WP function since it has a filter that might be useful
*/
public function delete()
{
if ($this->exists())
{
\wp_delete_file($this->fullpath); // delete file hook via wp_delete_file
}
else
{
Log::addWarn('Trying to remove non-existing file: ' . $this->getFullPath());
}
if (! file_exists($this->fullpath))
{
$this->resetStatus();
return true;
}
else {
$writable = ($this->is_writable()) ? 'true' : 'false';
Log::addWarn('File seems not removed - ' . $this->getFullPath() . ' (writable:' . $writable . ')');
return false;
}
}
public function getContents()
{
return file_get_contents($this->getFullPath());
}
public function getFullPath()
{
// filename here since fullpath is set unchecked in constructor, but might be a different take
if (is_null($this->filename))
{
$this->setFileInfo();
}
return $this->fullpath;
}
// Testing this. Principle is that when the plugin is absolutely sure this is a file, not something remote, not something non-existing, get the fullpath without any check.
// This function should *only* be used when processing mega amounts of files while not doing optimization or any processing.
// So far, testing use for file Filter */
public function getRawFullPath()
{
return $this->rawfullpath;
}
public function getFileName()
{
if (is_null($this->filename))
$this->setFileInfo();
return $this->filename;
}
public function getFileBase()
{
if (is_null($this->filebase))
$this->setFileInfo();
return $this->filebase;
}
public function getExtension()
{
if (is_null($this->extension))
$this->setFileInfo();
return $this->extension;
}
public function getMime()
{
if (is_null($this->mime))
$this->setFileInfo();
if ($this->exists() && ! $this->is_virtual() )
{
$this->mime = wp_get_image_mime($this->fullpath);
if (false === $this->mime)
{
$image_data = wp_check_filetype_and_ext($this->getFullPath(), $this->getFileName());
if (is_array($image_data) && isset($image_data['type']) && strlen($image_data['type']) > 0)
{
$this->mime = $image_data['type'];
}
}
}
else
$this->mime = false;
return $this->mime;
}
/* Util function to get location of backup Directory.
* @param Create - If true will try to create directory if it doesn't exist.
* @return Boolean | DirectModel Returns false if directory is not properly set, otherwhise with a new directoryModel
*/
protected function getBackupDirectory($create = false)
{
if (is_null($this->getFileDir()))
{
Log::addWarn('Could not establish FileDir ' . $this->fullpath);
return false;
}
$fs = \wpSPIO()->filesystem();
if (is_null($this->backupDirectory))
{
$directory = $fs->getBackupDirectory($this, $create);
if ($directory === false || ! $directory->exists()) // check if exists. FileModel should not attempt to create.
{
//Log::addWarn('Backup Directory not existing ' . $directory-);
return false;
}
elseif ($directory !== false)
{
$this->backupDirectory = $directory;
}
else
{
return false;
}
}
return $this->backupDirectory;
}
/* Internal function to check if path is a real path
* - Test for URL's based on http / https
* - Test if given path is absolute, from the filesystem root.
* @param $path String The file path
* @param String The Fixed filepath.
*/
protected function processPath($path)
{
$original_path = $path;
$fs = \wpSPIO()->filesystem();
if ($fs->pathIsUrl($path))
{
$path = $this->UrlToPath($path);
}
if ($path === false) // don't process further
return false;
$path = UtilHelper::spNormalizePath($path);
$abspath = $fs->getWPAbsPath();
// Prevent file operation below if trusted.
if (true === self::$TRUSTED_MODE)
{
return $path;
}
// Check if some openbasedir is active.
if (true === $this->fileIsRestricted($path))
{
$path = $this->relativeToFullPath($path);
}
if ( is_file($path) && ! is_dir($path) ) // if path and file exist, all should be okish.
{
return $path;
}
// If attempted file does not exist, but the file is in a dir that exists, that is good enough.
elseif ( ! is_dir($path) && is_dir(dirname($path)) )
{
return $path;
}
// If path is not in the abspath, it might be relative.
elseif (strpos($path, $abspath->getPath()) === false)
{
// if path does not contain basepath.
//$uploadDir = $fs->getWPUploadBase();
//$abspath = $fs->getWPAbsPath();
$path = $this->relativeToFullPath($path);
}
$path = apply_filters('shortpixel/filesystem/processFilePath', $path, $original_path);
/* This needs some check here on malformed path's, but can't be test for existing since that's not a requirement.
if (file_exists($path) === false) // failed to process path to something workable.
{
// Log::addInfo('Failed to process path', array($path));
$path = false;
} */
return $path;
}
protected function checkTrustedMode()
{
// When in trusted mode prevent filesystem checks as much as possible.
if (true === self::$TRUSTED_MODE)
{
// At this point file info might not be loaded, because it goes w/ construct -> processpath -> urlToPath etc on virtual files. And called via getFileInfo. Using any of the file info functions can trigger a loop.
if (is_null($this->extension))
{
$extension = pathinfo($this->fullpath, PATHINFO_EXTENSION);
}
else {
$extension = $this->getExtension();
}
$this->exists = true;
$this->is_writable = true;
$this->is_directory_writable = true;
$this->is_readable = true;
$this->is_file = true;
// Set mime to prevent lookup in IsImage
$this->mime = 'image/' . $extension;
if (is_null($this->filesize))
{
$this->filesize = 0;
}
}
}
/** Check if path is allowed within openbasedir restrictions. This is an attempt to limit notices in file funtions if so. Most likely the path will be relative in that case.
* @param String Path as String
*/
private function fileIsRestricted($path)
{
$basedir = ini_get('open_basedir');
if (false === $basedir || strlen($basedir) == 0)
{
return false;
}
$restricted = true;
$basedirs = preg_split('/:|;/i', $basedir);
foreach($basedirs as $basepath)
{
if (strpos($path, $basepath) !== false)
{
$restricted = false;
break;
}
}
return $restricted;
}
/** Resolve an URL to a local path
* This partially comes from WordPress functions attempting the same
* @param String $url The URL to resolve
* @return String/Boolean - False is this seems an external domain, otherwise resolved path.
*/
private function UrlToPath($url)
{
// If files is present, high chance that it's WPMU old style, which doesn't have in home_url the /files/ needed to properly replace and get the filepath . It would result in a /files/files path which is incorrect.
if (strpos($url, '/files/') !== false)
{
$uploadDir = wp_upload_dir();
$site_url = str_replace(array('http:', 'https:'), '', $uploadDir['baseurl']);
}
else {
$site_url = str_replace('http:', '', home_url('', 'http'));
}
$url = str_replace(array('http:', 'https:'), '', $url);
$fs = \wpSPIO()->filesystem();
// The site URL domain is included in the URL string
if (strpos($url, $site_url) !== false)
{
// try to replace URL for Path
$abspath = \wpSPIO()->filesystem()->getWPAbsPath();
$path = str_replace($site_url, rtrim($abspath->getPath(),'/'), $url);
if (! $fs->pathIsUrl($path)) // test again.
{
return $path;
}
}
$this->is_virtual = true;
/* This filter checks if some supplier will be able to handle the file when needed.
* Use translate filter to correct filepath when needed.
* Return could be true, or fileModel virtual constant
*/
$result = apply_filters('shortpixel/image/urltopath', false, $url);
if ($result === false)
{
$this->exists = false;
$this->is_readable = false;
$this->is_file = false;
}
else {
$this->exists = true;
$this->is_readable = true;
$this->is_file = true;
}
// If return is a stateless server, assume that it's writable and all that.
if ($result === self::$VIRTUAL_STATELESS)
{
$this->is_writable = true;
$this->is_directory_writable = true;
$this->virtual_status = self::$VIRTUAL_STATELESS;
}
elseif ($result === self::$VIRTUAL_REMOTE)
{
$this->virtual_status = self::$VIRTUAL_REMOTE;
}
return false; // seems URL from other server, use virtual mode.
}
/** Tries to find the full path for a perceived relative path.
*
* Relative path is detected on basis of WordPress ABSPATH. If this doesn't appear in the file path, it might be a relative path.
* Function checks for expections on this rule ( tmp path ) and returns modified - or not - path.
* @param $path The path for the file_exists
* @returns String The updated path, if that was possible.
*/
private function relativeToFullPath($path)
{
$originalPath = $path; // for safe-keeping
// A file with no path, can never be created to a fullpath.
if (strlen($path) == 0)
return $path;
// if the file plainly exists, it's usable /**
if (false === $this->fileIsRestricted($path) && file_exists($path))
{
return $path;
}
// Test if our 'relative' path is not a path to /tmp directory.
// This ini value might not exist.
$tempdirini = ini_get('upload_tmp_dir');
if ( (strlen($tempdirini) > 0) && strpos($path, $tempdirini) !== false)
return $path;
$tempdir = sys_get_temp_dir();
if ( (strlen($tempdir) > 0) && strpos($path, $tempdir) !== false)
return $path;
// Path contains upload basedir. This happens when upload dir is outside of usual WP.
$fs = \wpSPIO()->filesystem();
$uploadDir = $fs->getWPUploadBase();
$abspath = $fs->getWPAbsPath();
if (strpos($path, $uploadDir->getPath()) !== false) // If upload Dir is feature in path, consider it ok.
{
return $path;
}
elseif (file_exists($abspath->getPath() . $path)) // If upload dir is abspath plus return path. Exceptions.
{
return $abspath->getPath() . $path;
}
elseif(file_exists($uploadDir->getPath() . $path)) // This happens when upload_dir is not properly prepended in get_attachment_file due to WP errors
{
return $uploadDir->getPath() . $path;
}
// this is probably a bit of a sharp corner to take.
// if path starts with / remove it due to trailingslashing ABSPATH
$path = ltrim($path, '/');
$fullpath = $abspath->getPath() . $path;
// We can't test for file_exists here, since file_model allows non-existing files.
// Test if directory exists, perhaps. Otherwise we are in for a failure anyhow.
//if (is_dir(dirname($fullpath)))
return $fullpath;
//else
// return $originalPath;
}
public function getPermissions()
{
if (is_null($this->permissions))
$this->permissions = fileperms($this->getFullPath()) & 0777;
return $this->permissions;
}
// @tozo Lazy IMplementation / copy, should be brought in line w/ other attributes.
public function setPermissions($permissions)
{
@chmod($this->fullpath, $permissions);
}
/** Fix for multibyte pathnames and pathinfo which doesn't take into regard the locale.
* This snippet taken from PHPMailer.
*/
private function mb_pathinfo($path, $options = null)
{
$ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''];
$pathinfo = [];
if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^.\\\\/]+?)|))[\\\\/.]*$#m', $path, $pathinfo)) {
if (array_key_exists(1, $pathinfo)) {
$ret['dirname'] = $pathinfo[1];
}
if (array_key_exists(2, $pathinfo)) {
$ret['basename'] = $pathinfo[2];
}
if (array_key_exists(5, $pathinfo)) {
$ret['extension'] = $pathinfo[5];
}
if (array_key_exists(3, $pathinfo)) {
$ret['filename'] = $pathinfo[3];
}
}
switch ($options) {
case PATHINFO_DIRNAME:
case 'dirname':
return $ret['dirname'];
case PATHINFO_BASENAME:
case 'basename':
return $ret['basename'];
case PATHINFO_EXTENSION:
case 'extension':
return $ret['extension'];
case PATHINFO_FILENAME:
case 'filename':
return $ret['filename'];
default:
return $ret;
}
}
public function __debuginfo()
{
return [
'fullpath' => $this->fullpath,
'filename' => $this->filename,
'filebase' => $this->filebase,
'exists' => $this->exists,
'is_writable' => $this->is_writable,
'is_readable' => $this->is_readable,
'is_virtual' => $this->is_virtual,
];
}
} // FileModel Class

View File

@ -0,0 +1,349 @@
<?php
namespace ShortPixel\Model;
use ShortPixel\Model\Image\ImageModel as ImageModel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
class FrontImage
{
protected $raw;
protected $image_loaded = false;
protected $is_parsable = false;
protected $imageBase; // directory path of this image.
protected $id; // HTML ID of image
protected $alt;
protected $src; // original src of image
protected $srcset; // orginal srcset of image
protected $class;
protected $width;
protected $height;
protected $style;
protected $sizes;
// Array of all other attributes.
protected $attributes;
// Parsed items of src /srcset / sizes
protected $dataTags = array();
public function __construct($raw_html)
{
$this->raw = $raw_html;
$this->loadImageDom();
}
public function loadImageDom()
{
if (function_exists("mb_convert_encoding")) {
$this->raw = mb_encode_numericentity($this->raw, [0x80, 0x10FFFF, 0, ~0], 'UTF-8');
}
$dom = new \DOMDocument();
libxml_use_internal_errors(true); // disable error emit from libxml
$result = $dom->loadHTML($this->raw, LIBXML_NOWARNING);
// HTML failed loading
if (false === $result)
{
return false;
}
$image = $dom->getElementsByTagName('img')->item(0);
$attributes = array();
/* This can happen with mismatches, or extremely malformed HTML.
In customer case, a javascript that did for (i<imgDefer) --- </script> */
if (! is_object($image))
{
$this->is_parsable = false;
return false;
}
foreach ($image->attributes as $attr) {
// Skip is no value
if (strlen($attr->nodeValue) == 0)
continue;
if (property_exists($this, $attr->nodeName))
{
$this->{$attr->nodeName} = $attr->nodeValue;
}
$this->attributes[$attr->nodeName] = $attr->nodeValue;
}
// Parse the directory path and other sources
$result = $this->setupSources();
if (true === $result)
$this->image_loaded = true;
}
public function hasBackground()
{
if (! is_null($this->style) && strpos($this->style, 'background') !== false)
{
return true;
}
return false;
}
public function hasPreventClasses()
{
// no class, no prevent.
if (is_null($this->class))
{
return false;
}
$preventArray = apply_filters('shortpixel/front/preventclasses', array('sp-no-webp', 'rev-sildebg') );
foreach($preventArray as $classname)
{
if (false !== strpos($this->class, $classname) )
{
return true;
}
}
return false;
}
public function hasSource()
{
if (is_null($this->src) && is_null($this->srcset))
{
return false;
}
return true;
}
public function isParseable()
{
if (
false === $this->hasPreventClasses() &&
false === $this->hasBackground() &&
true === $this->hasSource() &&
true === $this->image_loaded
)
{
return true;
}
return false;
}
public function getImageData()
{
if (! is_null($this->srcset))
{
$data = $this->getLazyData('srcset');
$data = explode(',', $data); // srcset is multiple images, split.
}
else {
$data = $this->getLazyData('src');
$data = array($data); // single item, wrap in array
}
$this->getLazyData('sizes'); // sets the sizes.
return $data;
}
public function getImageBase()
{
if (! is_null($this->imageBase))
return $this->imageBase->getPath();
return null;
}
public function parseReplacement($args)
{
if (is_null($this->class))
{
$this->class = '';
}
$this->class .= ' sp-no-webp';
$output = "<picture>";
if (isset($args['avif']) && count($args['avif']) > 0)
{
$output .= $this->buildSource($args['avif'], 'avif');
}
if (isset($args['webp']) && count($args['webp']) > 0)
{
$output .= $this->buildSource($args['webp'], 'webp');
}
$output .= $this->buildImage();
$output .= "</picture>";
return $output;
}
protected function setupSources()
{
$src = null;
if (! is_null($this->src))
{
$src = $this->src;
}
elseif (! is_null($this->srcset))
{
$parts = preg_split('/\s+/', trim($this->srcset));
$image_url = $parts[0];
$src = $image_url;
}
if (is_null($src))
{
return false;
}
// Filter out extension that are not for us.
if (false === $this->checkExtensionConvertable($src))
{
return false;
}
$fs = \wpSPIO()->filesystem();
$fileObj = $fs->getFile($src);
$fileDir = $fileObj->getFileDir();
$this->imageBase = $fileObj->getFileDir();
return true;
// If (! is_hnull $srcset)
// Get first item from srcset ( remove the size ? , then feed it to FS, get directory from it.
}
/*** Check if the extension is something we want to check
* @param String The URL source of the image.
**/
private function checkExtensionConvertable($source)
{
$extension = substr($source, strrpos($source, '.') + 1);
if (in_array($extension, ImageModel::PROCESSABLE_EXTENSIONS))
{
return true;
}
return false;
}
protected function buildSource($sources, $fileFormat)
{
$prefix = (isset($this->dataTags['srcset'])) ? $this->dataTags['srcset'] : $this->dataTags['src'];
$srcset = implode(',', $sources);
$sizeOutput = '';
if (! is_null($this->sizes))
{
$sizeOutput = $this->dataTags['sizes'] . 'sizes="' . $this->sizes . '"';
}
$output = '<source ' . $prefix . 'srcset="' . $srcset . '" ' . $sizeOutput . ' type="image/' . $fileFormat . '">';
return $output;
}
protected function buildImage()
{
$src = $this->src;
$output = '<img src="' . $src . '" ';
// Get this from set attributes on class.
$attrs = array('id', 'height', 'width', 'srcset', 'sizes', 'class');
foreach($attrs as $attr)
{
if (! is_null($this->{$attr}))
{
$output .= $attr . '="' . $this->{$attr} . '" ';
}
}
// Always output alt tag, because it's important to screen readers and otherwise.
$output .= 'alt="' . $this->alt . '" ';
// Left over attributes that should be harmless, ie extra image data or other custom tags.
$leftAttrs = $this->getImageAttributes();
foreach($leftAttrs as $name => $value)
{
$output .= $name . '="' . $value . '" ';
}
$output .= ' > '; // ending image.
return $output;
}
protected function getImageAttributes()
{
$dontuse = array(
'src', 'data-src', 'data-lazy-src', 'srcset', 'sizes'
);
$dontuse = array_merge($dontuse, array('id', 'alt', 'height', 'width', 'srcset', 'sizes', 'class'));
$attributes = $this->attributes;
$leftAttrs = array();
foreach($attributes as $name => $value)
{
if (! in_array($name, $dontuse ))
{
$leftAttrs[$name] = $value;
}
}
return $leftAttrs;
}
protected function getLazyData($type)
{
$attributes = $this->attributes;
$value = $prefix = false;
if (isset($attributes['data-lazy-' . $type]) && strlen($attributes['data-lazy-' . $type]) > 0)
{
$value = $attributes['data-lazy-' . $type];
$prefix = 'data-lazy-';
}
elseif( isset($attributes['data-' . $type]) && strlen($attributes['data-' . $type]) > 0)
{
$value = $attributes['data-' . $type];
$prefix = 'data-';
}
elseif(isset($attributes[$type]) && strlen($attributes[$type]) > 0)
{
$value = $attributes[$type];
$prefix = '';
}
$this->dataTags[$type] = $prefix;
return $value;
}
} // class FrontImage

View File

@ -0,0 +1,684 @@
<?php
namespace ShortPixel\Model\Image;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Controller\OptimizeController as OptimizeController;
use ShortPixel\Helper\UtilHelper as UtilHelper;
use ShortPixel\Controller\ApiController as API;
// @todo Custom Model for adding files, instead of meta DAO.
class CustomImageModel extends \ShortPixel\Model\Image\ImageModel
{
protected $folder_id;
protected $path_md5;
protected $type = 'custom';
protected $thumbnails = array(); // placeholder, should return empty.
protected $retinas = array(); // placeholder, should return empty.
protected $in_db = false;
protected $is_stub = false;
protected $is_main_file = true;
/** @var array */
protected $forceSettings = array(); // option derives from setting or otherwise, request to be forced upon via UI to use specific value.
// @param int $id
public function __construct($id)
{
$this->id = $id;
if ($id > 0)
{
$bool = $this->loadMeta();
/*if ($bool)
{
$this->setWebp();
$this->setAvif();
} */
}
else
{
$this->fullpath = ''; // stub
$this->image_meta = new ImageMeta();
$this->is_stub = true;
}
parent::__construct($this->fullpath);
}
/**
* @param int $folder_id;
*/
public function setFolderId($folder_id)
{
$this->folder_id = $folder_id;
}
public function getOptimizeUrls()
{
$data = $this->getOptimizeData();
return array_values($data['urls']);
}
protected function getExcludePatterns()
{
$args = array(
'filter' => true,
'is_custom' => true,
);
$patterns = UtilHelper::getExclusions($args);
return $patterns;
}
public function getOptimizeData()
{
$parameters = array(
'urls' => array(),
'params' => array(),
'returnParams' => array(),
);
$fs = \wpSPIO()->filesystem();
if ($this->is_virtual())
$url = $this->getFullPath();
else
$url = $this->getURL();
$settings = \wpSPIO()->settings();
$isSmartCrop = ($settings->useSmartcrop == true && $this->getExtension() !== 'pdf') ? true : false;
$paramListArgs = array(); // args for the params, yes.
if (isset($this->forceSettings['smartcrop']) && $this->getExtension() !== 'pdf')
{
$isSmartCrop = ($this->forceSettings['smartcrop'] == ImageModel::ACTION_SMARTCROP) ? true : false;
}
$paramListArgs['smartcrop'] = $isSmartCrop;
$paramListArgs['main_url'] = $url;
$paramListArgs['url'] = $url;
if ($this->isProcessable(true) || $this->isProcessableAnyFileType())
{
$parameters['urls'][0] = $url;
$parameters['paths'][0] = $this->getFullPath();
$parameters['params'][0] = $this->createParamList($paramListArgs);
$parameters['returnParams']['sizes'][0] = $this->getFileName();
if ($isSmartCrop )
{
$parameters['returnParams']['fileSizes'][0] = $this->getFileSize();
}
}
return $parameters;
}
public function doSetting($setting, $value)
{
$this->forceSettings[$setting] = $value;
}
public function getURL()
{
return \wpSPIO()->filesystem()->pathToUrl($this);
}
public function count($type)
{
// everything is 1 on 1 in the customModel
switch($type)
{
case 'thumbnails':
return 0;
break;
case 'webps':
$count = count($this->getWebps());
break;
case 'avifs':
$count = count($this->getAvifs());
break;
case 'retinas':
$count = count($this->getRetinas());
break;
}
return $count; // 0 or 1
}
/* Check if an image in theory could be processed. Check only exclusions, don't check status etc */
public function isProcessable($strict = false)
{
$bool = parent::isProcessable();
if($strict)
{
return $bool;
}
// The exclude size on the image - via regex - if fails, prevents the whole thing from optimization.
if ($this->processable_status == ImageModel::P_EXCLUDE_SIZE || $this->processable_status == ImageModel::P_EXCLUDE_PATH)
{
return $bool;
}
/* if ($bool === false && $strict === false)
{
// Todo check if Webp / Acif is active, check for unoptimized items
if ($this->isProcessableFileType('webp'))
{
$bool = true;
}
if ($this->isProcessableFileType('avif'))
{
$bool = true;
}
} */
// From above to below was implemented because it could not detect file not writable / directory not writable issues if there was any option to generate webp in the settings. Should check for all those file issues first.
// First test if this file isn't unprocessable for any other reason, then check.
if (($this->isProcessable(true) || $this->isOptimized() ) && $this->isProcessableAnyFileType() === true)
{
if (false === $this->is_directory_writable())
{
$bool = false;
}
else {
$bool = true;
}
}
return $bool;
}
public function isRestorable()
{
$bool = parent::isRestorable();
// If fine, all fine.
if ($bool == true)
{
return $bool;
}
// If not, check this..
if ($this->hasBackup() && $this->getMeta('status') == self::FILE_STATUS_PREVENT)
{
return true;
}
else
{
return $bool;
}
}
protected function getWebps()
{
$webp = array($this->getWebp());
return array_filter($webp);
}
protected function getAvifs()
{
$avif = array($this->getAvif());
return array_filter($avif);
}
/** Get FileTypes that might be optimized. Checking for setting should go via isProcessableFileType! */
public function getOptimizeFileType($type = 'webp')
{
// Pdf files can't have special images.
if ($this->getExtension() == 'pdf')
return array();
if ($type == 'webp')
{
$types = $this->getWebps();
}
elseif ($type == 'avif')
{
$types = $this->getAvifs();
}
$toOptimize = array();
$fs = \WPSPIO()->filesystem();
// The file must not exist yet.
if (count($types) == 0 && ($this->isProcessable(true) || $this->isOptimized()) )
return array($fs->pathToUrl($this));
else
return array();
}
public function restore($args = array())
{
do_action('shortpixel_before_restore_image', $this->get('id'));
do_action('shortpixel/image/before_restore', $this);
$defaults = array(
'keep_in_queue' => false, // used for bulk restore.
);
$args = wp_parse_args($args, $defaults);
$bool = parent::restore();
$return = true;
if ($bool)
{
$this->setMeta('status', ImageModel::FILE_STATUS_UNPROCESSED);
$this->setMeta('compressedSize', 0);
$this->setMeta('compressionType', null);
$this->saveMeta();
$webps = $this->getWebps();
foreach($webps as $webpFile)
$webpFile->delete();
$avifs = $this->getAvifs();
foreach($avifs as $avifFile)
$avifFile->delete();
}
else
{
$return = false;
}
if ($args['keep_in_queue'] === false)
{
$this->dropFromQueue();
}
do_action('shortpixel/image/after_restore', $this, $this->id, $bool);
return $return;
}
public function handleOptimized($optimizeData, $args = array())
{
$bool = true;
if (isset($optimizeData['files']) && isset($optimizeData['data']))
{
$files = $optimizeData['files'];
$data = $optimizeData['data'];
}
else {
Log::addError('Something went wrong with handleOptimized', $optimizeData);
}
if (! $this->isOptimized() ) // main file might not be contained in results
{
$bool = parent::handleOptimized($files[0]);
}
$this->handleOptimizedFileType($files[0]);
if ($bool)
{
$this->setMeta('customImprovement', parent::getImprovement());
$this->saveMeta();
}
// $this->deleteTempFiles($files);
return $bool;
}
public function loadMeta()
{
global $wpdb;
$sql = 'SELECT * FROM ' . $wpdb->prefix . 'shortpixel_meta where id = %d';
$sql = $wpdb->prepare($sql, $this->id);
$imagerow = $wpdb->get_row($sql);
$metaObj = new ImageMeta();
$this->image_meta = $metaObj; // even if not found, load an empty imageMeta.
if (! is_object($imagerow))
return false;
$this->in_db = true; // record found.
$this->fullpath = $imagerow->path;
$this->folder_id = $imagerow->folder_id;
$this->path_md5 = $imagerow->path_md5;
$status = intval($imagerow->status);
$metaObj->status = $status;
if ($status == ImageModel::FILE_STATUS_SUCCESS)
{
$metaObj->customImprovement = $imagerow->message;
}
$metaObj->compressedSize = intval($imagerow->compressed_size);
// The null check is important, otherwise it will always optimize wrongly.
$metaObj->compressionType = (is_null($imagerow->compression_type)) ? null : intval($imagerow->compression_type);
if (! is_numeric($imagerow->message) && ! is_null($imagerow->message))
$metaObj->errorMessage = $imagerow->message;
$metaObj->did_keepExif = (intval($imagerow->keep_exif) == 1) ? true : false;
$metaObj->did_cmyk2rgb = (intval($imagerow->cmyk2rgb) == 1) ? true : false;
$metaObj->resize = (intval($imagerow->resize) > 1) ? true : false;
if (intval($imagerow->resize_width) > 0)
$metaObj->resizeWidth = intval($imagerow->resize_width);
if (intval($imagerow->resize_height) > 0)
$metaObj->resizeHeight = intval($imagerow->resize_height);
//$metaObj->has_backup = (intval($imagerow->backup) == 1) ? true : false;
$addedDate = UtilHelper::DBtoTimestamp($imagerow->ts_added);
$metaObj->tsAdded = $addedDate;
$optimizedDate = UtilHelper::DBtoTimestamp($imagerow->ts_optimized);
$metaObj->tsOptimized = $optimizedDate;
$extraInfo = property_exists($imagerow, 'extra_info') ? $imagerow->extra_info : null;
if (! is_null($extraInfo))
{
$data = json_decode($extraInfo, true);
if (isset($data['webpStatus']))
{
$this->setMeta('webp', $data['webpStatus']);
}
if (isset($data['avifStatus']))
{
$this->setMeta('avif', $data['avifStatus']);
}
}
$this->image_meta = $metaObj;
}
public function getParent()
{
return false; // no parents here
}
/** Load a CustomImageModel as Stub ( to be added ) . Checks if the image is already added as well
*
* @param String $path
* @param Boolean $load
*/
public function setStub($path, $load = true)
{
$this->fullpath = $path;
$this->path_md5 = md5($this->fullpath);
global $wpdb;
$sql = 'SELECT id from ' . $wpdb->prefix . 'shortpixel_meta where path = %s';
$sql = $wpdb->prepare($sql, $path);
$result = $wpdb->get_var($sql);
if ( ! is_null($result) )
{
$this->in_db = true;
$this->id = $result;
if ($load)
$this->loadMeta();
}
else
{
$this->image_meta = new ImageMeta();
$this->image_meta->compressedSize = 0;
$this->image_meta->tsOptimized = 0;
$this->image_meta->tsAdded = time();
}
}
protected function preventNextTry($reason = '', $status = self::FILE_STATUS_PREVENT)
{
$this->setMeta('errorMessage', $reason);
$this->setMeta('status', $status);
$this->saveMeta();
}
public function markCompleted($reason, $status)
{
return $this->preventNextTry($reason, $status);
}
public function isOptimizePrevented()
{
$status = $this->getMeta('status');
if ($status == self::FILE_STATUS_PREVENT || $status == self::FILE_STATUS_MARKED_DONE )
{
$this->processable_status = self::P_OPTIMIZE_PREVENTED;
$this->optimizePreventedReason = $this->getMeta('errorMessage');
return $this->getMeta('errorMessage');
}
return false;
}
// Only one item for now, so it's equal
public function isSomethingOptimized()
{
return $this->isOptimized();
}
public function resetPrevent()
{
if ($this->hasBackup())
$this->setMeta('status', self::FILE_STATUS_SUCCESS);
else
$this->setMeta('status', self::FILE_STATUS_UNPROCESSED);
$this->setMeta('errorMessage', '');
$this->saveMeta();
}
public function saveMeta()
{
global $wpdb;
$table = $wpdb->prefix . 'shortpixel_meta';
$where = array('id' => $this->id);
$metaObj = $this->image_meta;
if (! is_null($metaObj->customImprovement) && is_numeric($metaObj->customImprovement))
$message = $metaObj->customImprovement;
elseif (! is_null($metaObj->errorMessage))
$message = $metaObj->errorMessage;
else
$message = null;
$optimized = new \DateTime();
$optimized->setTimestamp($metaObj->tsOptimized);
$added = new \DateTime();
$added->setTimeStamp($metaObj->tsAdded);
$extra_info = array();
if ($this->getMeta('webp') === self::FILETYPE_BIGGER)
{
$extra_info['webpStatus'] = self::FILETYPE_BIGGER;
}
if ($this->getMeta('avif') === self::FILETYPE_BIGGER)
{
$extra_info['avifStatus'] = self::FILETYPE_BIGGER;
}
if (count($extra_info) > 0)
{
$extra_info = json_encode($extra_info);
}
else {
$extra_info = null;
}
$data = array(
'folder_id' => $this->folder_id,
'compressed_size' => $metaObj->compressedSize,
'compression_type' => $metaObj->compressionType,
'keep_exif' => ($metaObj->did_keepExif) ? 1 : 0,
'cmyk2rgb' => ($metaObj->did_cmyk2rgb) ? 1 : 0,
'resize' => ($metaObj->resize) ? 1 : 0,
'resize_width' => $metaObj->resizeWidth,
'resize_height' => $metaObj->resizeHeight,
'backup' => ($this->hasBackup()) ? 1 : 0,
'status' => $metaObj->status,
'retries' => 0, // this is unused / legacy
'message' => $message, // this is used for improvement line.
'ts_added' => UtilHelper::timestampToDB($metaObj->tsAdded),
'ts_optimized' => UtilHelper::timestampToDB($metaObj->tsOptimized),
'path' => $this->getFullPath(),
'name' => $this->getFileName(),
'path_md5' => md5($this->getFullPath()), // this is legacy
'extra_info' => $extra_info,
);
// The keys are just for readability.
$format = array(
'folder_id' => '%d',
'compressed_size' => '%d',
'compression_type' => '%d' ,
'keep_exif' => '%d' ,
'cmyk2rgb' => '%d' ,
'resize' => '%d' ,
'resize_width' => '%d',
'resize_height' => '%d',
'backup' => '%d',
'status' => '%d',
'retries' => '%d', // this is unused / legacy
'message' => '%s', // this is used for improvement line.
'ts_added' => '%s',
'ts_optimized' => '%s' ,
'path' => '%s',
'name' => '%s',
'path_md5' => '%s' , // this is legacy
'extra_info' => '%s',
);
$is_new = false;
if ($this->in_db)
{
$res = $wpdb->update($table, $data, $where, $format); // result is amount rows updated.
}
else
{
$is_new = true;
$res = $wpdb->insert($table, $data, $format); // result is new inserted id
}
if ($is_new)
{
$this->id = $wpdb->insert_id;
}
if ($res !== false)
return true;
else
return false;
}
public function deleteMeta()
{
global $wpdb;
$table = $wpdb->prefix . 'shortpixel_meta';
$where = array('id' => $this->id);
$result = $wpdb->delete($table, $where, array('%d'));
return $result;
}
public function onDelete()
{
parent::onDelete();
$this->deleteMeta();
$this->dropfromQueue();
}
public function dropFromQueue()
{
$optimizeController = new OptimizeController();
$q = $optimizeController->getQueue($this->type);
$q->dropItem($this->get('id'));
// Drop also from bulk if there.
$optimizeController->setBulk(true);
$q = $optimizeController->getQueue($this->type);
$q->dropItem($this->get('id'));
}
public function getImprovement($int = false)
{
return $this->getMeta('customImprovement');
}
public function getImprovements()
{
$improvements = array();
/*$totalsize = $totalperc = $count = 0;
if ($this->isOptimized())
{
$perc = $this->getImprovement();
$size = $this->getImprovement(true);
$totalsize += $size;
$totalperc += $perc;
$improvements['main'] = array($perc, $size);
$count++;
} */
$improvement = $this->getImprovement();
if (is_null($improvement)) // getImprovement can return null.
{
$improvement = 0;
}
$improvements['main'] = array($improvement, 0);
$improvements['totalpercentage'] = round($improvement); // the same.
return $improvements;
// return $improvements; // we have no thumbnails.
}
}

View File

@ -0,0 +1,121 @@
<?php
namespace ShortPixel\Model\Image;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
class ImageConvertMeta
{
protected $fileFormat; // png / heic etc
protected $isConverted = false;
protected $placeholder = false;
protected $replacementImageBase = false;
// protected $doConversion = false;
protected $triedConversion = false;
protected $errorReason = false;
protected $omitBackup = true; // Don't backup the converted image (again), keeping only the original format. if not, make a backup of the converted file and treat that as the default backup/restore
public function __construct()
{
}
public function isConverted()
{
return $this->isConverted;
}
public function didTry()
{
return $this->triedConversion;
}
public function setTried($value)
{
$this->triedConversion = $value;
}
public function setConversionDone($omitBackup = true)
{
$this->isConverted = true;
$this->omitBackup = $omitBackup;
}
public function setError($code)
{
$this->errorReason = $code;
}
public function getError()
{
return $this->errorReason;
}
public function setFileFormat($ext)
{
if (is_null($this->fileFormat))
$this->fileFormat = $ext;
}
public function getFileFormat()
{
return $this->fileFormat;
}
public function omitBackup()
{
return $this->omitBackup;
}
// bool for now, otherwise if needed.
public function setPlaceHolder($placeholder = true)
{
$this->placeholder = $placeholder;
}
public function hasPlaceHolder()
{
return $this->placeholder;
}
public function setReplacementImageBase($name)
{
$this->replacementImageBase = $name;
}
public function getReplacementImageBase()
{
return $this->replacementImageBase;
}
public function fromClass($object)
{
foreach($object as $property => $value)
{
if (property_exists($this, $property))
{
$this->$property = $value;
}
}
}
public function toClass()
{
$class = new \stdClass;
$vars = get_object_vars($this);
foreach($vars as $property => $value) // only used by media library.
{
$class->$property = $this->$property;
}
return $class;
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace ShortPixel\Model\Image;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// Base Class for ImageMeta
class ImageMeta extends ImageThumbnailMeta
{
public $errorMessage;
public $wasConverted = false; // Was converted from legacy format
protected $convertMeta;
public function __construct()
{
parent::__construct();
$this->convertMeta = new ImageConvertMeta();
}
public function fromClass($object)
{
if (property_exists($object, 'convertMeta'))
{
$this->convertMeta->fromClass($object->convertMeta);
unset($object->convertMeta);
}
// legacy.
if (property_exists($object, 'tried_png2jpg') && $object->tried_png2jpg)
{
$this->convertMeta()->setTried($object->tried_png2jpg);
}
elseif (property_exists($object, 'did_png2jpg') && $object->did_png2jpg)
{
$this->convertMeta()->setFileFormat('png');
$this->convertMeta()->setConversionDone();
}
parent::fromClass($object);
}
public function convertMeta()
{
return $this->convertMeta;
}
} // class

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,106 @@
<?php
namespace ShortPixel\Model\Image;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
class ImageThumbnailMeta
{
/** @var int */
public $databaseID = null;
/** @var int */
public $status = 0;
/** @var int */
public $compressionType;
/** @var int */
public $compressedSize;
/** @var int */
public $originalSize;
// public $improvement;
/** @var boolean */
public $did_keepExif = false;
/** @var boolean */
public $did_cmyk2rgb = false;
/** @var int */
public $resize;
/** @var int */
public $resizeWidth;
/** @var int */
public $resizeHeight;
/** @var int */
public $resizeType;
/** @var int */
public $originalWidth;
/** @var int */
public $originalHeight;
public $tsAdded;
public $tsOptimized;
public $webp;
public $avif;
public $file; // **Only for unlisted images. This defines an unlisted image */
// Only for customImageModel! Exception to prevent having to create a whole class. Second var here, warrants a subclass.
public $customImprovement;
public function __construct()
{
$this->tsAdded = time(); // default
}
/** Load data from basic class to prevent issues when class definitions changes over time */
public function fromClass($object)
{
foreach($object as $property => $value)
{
if ($property == 'customImprovement')
{ continue; }
if (property_exists($this, $property))
{
$this->$property = $value;
}
}
}
/** Save data as basic class to prevent issues when class definitions changes over time */
public function toClass()
{
$class = new \stdClass;
$vars = get_object_vars($this);
foreach($vars as $property => $value) // only used by media library.
{
if ($property == 'customImprovement')
{ continue; }
if ($property == 'convertMeta' && is_null($this->convertMeta))
{
continue;
}
elseif ($property == 'convertMeta') {
$class->$property = $this->$property->toClass();
continue;
}
// if (is_null($value)) // don't save default / values without init.
// continue;
$class->$property = $this->$property;
}
return $class;
}
}

View File

@ -0,0 +1,591 @@
<?php
namespace ShortPixel\Model\Image;
use ShortPixel\Helper\DownloadHelper as DownloadHelper;
use ShortPixel\Helper\UtilHelper as UtilHelper;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use \ShortPixel\Model\File\FileModel as FileModel;
// Represent a thumbnail image / limited image in mediaLibrary.
class MediaLibraryThumbnailModel extends \ShortPixel\Model\Image\ImageModel
{
public $name;
/* public $width;
public $height;
public $mime; */
protected $prevent_next_try = false;
protected $is_main_file = false;
protected $is_retina = false; // diffentiate from thumbnail / retina.
protected $id; // this is the parent attachment id
protected $size; // size name of image in WP, if applicable.
protected $sizeDefinition; // size width / height / crop according to WordPress
public function __construct($path, $id, $size)
{
parent::__construct($path);
$this->image_meta = new ImageThumbnailMeta();
$this->id = $id;
$this->imageType = self::IMAGE_TYPE_THUMB;
$this->size = $size;
}
protected function loadMeta()
{
}
protected function saveMeta()
{
}
public function __debugInfo() {
return array(
'image_meta' => $this->image_meta,
'name' => $this->name,
'path' => $this->getFullPath(),
'size' => $this->size,
'width' => $this->get('width'),
'height' => $this->get('height'),
'exists' => ($this->exists()) ? 'yes' : 'no',
'is_virtual' => ($this->is_virtual()) ? 'yes' : 'no',
'wordpress_size' => $this->sizeDefinition,
);
}
/** Set the meta name of thumbnail. */
public function setName($name)
{
$this->name = $name;
}
public function setSizeDefinition($sizedef)
{
$this->sizeDefinition = $sizedef;
}
public function setImageType($type)
{
$this->imageType = $type;
}
public function getRetina()
{
if ($this->is_virtual())
{
$fs = \wpSPIO()->filesystem();
$filepath = apply_filters('shortpixel/file/virtual/translate', $this->getFullPath(), $this);
$virtualFile = $fs->getFile($filepath);
$filebase = $virtualFile->getFileBase();
$filepath = (string) $virtualFile->getFileDir();
$extension = $virtualFile->getExtension();
// This function needs an hard check on file exists, which might not be wanted.
if (false === \wpSPIO()->env()->useVirtualHeavyFunctions())
{
return false;
}
}
else {
$filebase = $this->getFileBase();
$filepath = (string) $this->getFileDir();
$extension = $this->getExtension();
}
$retina = new MediaLibraryThumbnailModel($filepath . $filebase . '@2x.' . $extension, $this->id, $this->size); // mind the dot in after 2x
$retina->setName($this->size);
$retina->setImageType(self::IMAGE_TYPE_RETINA);
$retina->is_retina = true;
$forceCheck = true;
if ($retina->exists($forceCheck))
return $retina;
return false;
}
public function isFileTypeNeeded($type = 'webp')
{
// pdf extension can be optimized, but don't come with these filetypes
if ($this->getExtension() == 'pdf')
{
return false;
}
if ($type == 'webp')
$file = $this->getWebp();
elseif ($type == 'avif')
$file = $this->getAvif();
if ( ($this->isThumbnailProcessable() || $this->isOptimized()) && $file === false) // if no file, it can be optimized.
return true;
else
return false;
}
// @param FileDelete can be false. I.e. multilang duplicates might need removal of metadata, but not images.
public function onDelete($fileDelete = true)
{
if ($fileDelete == true)
$bool = parent::onDelete();
else {
$bool = true;
}
// minimally reset all the metadata.
if ($this->is_main_file)
{
$this->image_meta = new ImageMeta();
}
else {
$this->image_meta = new ImageThumbnailMeta();
}
return $bool;
}
protected function setMetaObj($metaObj)
{
$this->image_meta = clone $metaObj;
}
protected function getMetaObj()
{
return $this->image_meta;
}
// get_path param see MediaLibraryModel
// This should be unused at the moment!
public function getOptimizeUrls()
{
if (! $this->isProcessable() )
return false;
$url = $this->getURL();
if (! $url)
{
return false; //nothing
}
return $url;
}
public function getURL()
{
$fs = \wpSPIO()->filesystem();
if ($this->size == 'original' && ! $this->get('is_retina'))
{
$url = wp_get_original_image_url($this->id);
}
elseif ($this->isUnlisted())
{
$url = $fs->pathToUrl($this);
}
else
{
// We can't trust higher lever function, or any WP functions. I.e. Woocommerce messes with the URL's if they like so.
// So get it from intermediate and if that doesn't work, default to pathToUrl - better than nothing.
// https://app.asana.com/0/1200110778640816/1202589533659780
$size_array = image_get_intermediate_size($this->id, $this->size);
if ($size_array === false || ! isset($size_array['url']))
{
$url = $fs->pathToUrl($this);
}
elseif (isset($size_array['url']))
{
$url = $size_array['url'];
// Even this can go wrong :/
if (strpos($url, $this->getFileName() ) === false)
{
// Taken from image_get_intermediate_size if somebody still messes with the filters.
$mainurl = wp_get_attachment_url( $this->id);
$url = path_join( dirname( $mainurl ), $this->getFileName() );
}
}
else {
return false;
}
}
return $this->fs()->checkURL($url);
}
// Just a placeholder for abstract, shouldn't do anything.
public function getImprovements()
{
return parent::getImprovements();
}
public function getBackupFileName()
{
$mainFile = ($this->is_main_file) ? $this : $this->getMainFile();
if (false == $mainFile)
{
return parent::getBackupFileName();
}
if ($mainFile->getMeta()->convertMeta()->getReplacementImageBase() !== false)
{
if ($this->is_main_file)
return $mainFile->getMeta()->convertMeta()->getReplacementImageBase() . '.' . $this->getExtension();
else {
// $fileBaseNoSize =
$name = str_replace($mainFile->getFileBase(), $mainFile->getMeta()->convertMeta()->getReplacementImageBase(), $this->getFileName());
return $name;
}
}
return parent::getBackupFileName();
}
protected function preventNextTry($reason = '')
{
$this->prevent_next_try = $reason;
}
// Don't ask thumbnails this, only the main image
public function isOptimizePrevented()
{
return false;
}
// Don't ask thumbnails this, only the main image
public function resetPrevent()
{
return null;
}
protected function isThumbnailProcessable()
{
// if thumbnail processing is off, thumbs are never processable.
// This is also used by main file, so check for that!
if ( $this->excludeThumbnails() && $this->is_main_file === false && $this->get('imageType') !== self::IMAGE_TYPE_ORIGINAL)
{
$this->processable_status = self::P_EXCLUDE_SIZE;
return false;
}
else
{
$bool = parent::isProcessable();
return $bool;
}
}
/** Function to check if said thumbnail is a WP-native or something SPIO added as unlisted
*
*
*/
protected function isUnlisted()
{
if (! is_null($this->getMeta('file')))
return true;
else
return false;
}
// !Important . This doubles as checking excluded image sizes.
protected function isSizeExcluded()
{
$excludeSizes = \wpSPIO()->settings()->excludeSizes;
if (is_array($excludeSizes) && in_array($this->name, $excludeSizes))
{
$this->processable_status = self::P_EXCLUDE_SIZE;
return true;
}
$bool = parent::isSizeExcluded();
return $bool;
}
public function isProcessableFileType($type = 'webp')
{
// Prevent webp / avif processing for thumbnails if this is off. Exclude main file
if ($this->excludeThumbnails() === true && $this->is_main_file === false )
return false;
return parent::isProcessableFileType($type);
}
protected function getExcludePatterns()
{
$args = array(
'filter' => true,
'thumbname' => $this->name,
'is_thumbnail' => (true === $this->is_main_file) ? false : true,
);
// @todo Find a way to cache IsProcessable perhaps due to amount of checks being done. Should be release in flushOptimizeCache or elsewhere (?)
// $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
$patterns = UtilHelper::getExclusions($args);
// echo "<PRE>"; print_r($args); print_r($patterns); echo "</PRE>";
return $patterns;
}
protected function excludeThumbnails()
{
return (! \wpSPIO()->settings()->processThumbnails);
}
public function hasBackup($args = array())
{
$defaults = array(
'forceConverted' => false,
'noConversionCheck' => false, // do not check on mainfile, this loops when used in loadMeta / legacyConversion
);
$args = wp_parse_args($args, $defaults);
// @todo This can probably go.
if (true === $args['noConversionCheck'])
{
return parent::hasBackup();
}
$mainFile = ($this->is_main_file) ? $this : $this->getMainFile();
if (false == $mainFile)
{
return parent::hasBackup();
}
// When main file and converted and omitBackup is true ( only original backup ) and not forced.
$loadRegular= (false === $mainFile->getMeta()->convertMeta()->isConverted() ||
false === $mainFile->getMeta()->convertMeta()->omitBackup()) && false === $args['forceConverted'];
if (true === $loadRegular)
{
return parent::hasBackup();
}
else
{
$directory = $this->getBackupDirectory();
$converted_ext = $mainFile->getMeta()->convertMeta()->getFileFormat();
if (! $directory)
return false;
$backupFile = $directory . $this->getFileBase() . '.' . $converted_ext;
// Issue with PNG not being scaled on the main file.
if (! file_exists($backupFile) && $mainFile->isScaled())
{
$backupFile = $directory . $mainFile->getOriginalFile()->getFileBase() . '.' . $converted_ext;
}
if (file_exists($backupFile) && ! is_dir($backupFile) )
return true;
else {
return false;
}
}
}
public function hasDBRecord()
{
global $wpdb;
$sql = 'SELECT id FROM ' . $wpdb->prefix . 'shortpixel_postmeta WHERE attach_id = %d AND size = %s';
$sql = $wpdb->prepare($sql, $this->id, $this->size);
$id = $wpdb->get_var($sql);
if (is_null($id))
{
return false;
}
elseif (is_numeric($id)) {
return true;
}
}
public function restore()
{
if ($this->is_virtual())
{
$fs = \wpSPIO()->filesystem();
$filepath = apply_filters('shortpixel/file/virtual/translate', $this->getFullPath(), $this);
$this->setVirtualToReal($filepath);
}
$bool = parent::restore();
if ($bool === true)
{
if ($this->is_main_file)
{
// If item is converted and will not be moved back to original format ( but converted ) , keep the convert metadata
if (true === $this->getMeta()->convertMeta()->isConverted() && false === $this->getMeta()->convertMeta()->omitBackup() )
{
$convertMeta = clone $this->getMeta()->convertMeta();
$imageMeta = new ImageMeta();
$imageMeta->convertMeta()->fromClass($convertMeta);
$bool = false; // Prevent cleanRestore from deleting the metadata.
}
else {
$imageMeta = new ImageMeta();
}
$this->image_meta = $imageMeta;
}
else
{
$this->image_meta = new ImageThumbNailMeta();
}
}
return $bool;
}
/** Tries to retrieve an *existing* BackupFile. Returns false if not present.
* This file might not be writable.
* To get writable directory reference to backup, use FileSystemController
*/
public function getBackupFile($args = array())
{
$defaults = array(
'forceConverted' => false,
'noConversionCheck' => false, // do not check on mainfile, this loops when used in loadMeta / legacyConversion
);
$args = wp_parse_args($args, $defaults);
if (true === $args['noConversionCheck'])
{
return parent::getBackupFile();
}
$mainFile = ($this->is_main_file) ? $this : $this->getMainFile();
if (false == $mainFile)
{
return parent::getBackupFile();
}
// When main file and converted and omitBackup is true ( only original backup ) and not forced.
$loadRegular= (false === $mainFile->getMeta()->convertMeta()->isConverted() ||
false === $mainFile->getMeta()->convertMeta()->omitBackup()) && false === $args['forceConverted'];
if (true === $loadRegular )
{
return parent::getBackupFile();
}
else
{
if ($this->hasBackup($args))
{
$directory = $this->getBackupDirectory();
$converted_ext = $mainFile->getMeta()->convertMeta()->getFileFormat();
$backupFile = $directory . $this->getFileBase() . '.' . $converted_ext;
/* Because WP doesn't support big PNG with scaled for some reason, it's possible it doesn't create them. Which means we end up with a scaled images without backup */
if (! file_exists($backupFile) && $mainFile->isScaled())
{
$backupFile = $directory . $mainFile->getOriginalFile()->getFileBase() . '.' . $converted_ext;
}
return new FileModel($backupFile);
}
else
return false;
}
}
protected function createBackup()
{
if ($this->is_virtual()) // download remote file to backup.
{
$fs = \wpSPIO()->filesystem();
$filepath = apply_filters('shortpixel/file/virtual/translate', $this->getFullPath(), $this);
$result = false;
if ($this->virtual_status == self::$VIRTUAL_REMOTE)
{
// filepath is translated. Check if this exists as a local copy, if not remotely download.
if ($filepath !== $this->getFullPath())
{
$fileObj = $fs->getFile($filepath);
$fileExists = $fileObj->exists();
}
else {
$fileExists = false;
}
if (false === $fileExists)
{
$downloadHelper = DownloadHelper::getInstance();
$url = $this->getURL();
$result = $downloadHelper->downloadFile($url, array('destinationPath' => $filepath));
}
}
elseif ($this->virtual_status == self::$VIRTUAL_STATELESS)
{
$result = $filepath;
}
else {
Log::addWarning('Virtual Status not set. Trying to blindly download vv DownloadHelper');
$downloadHelper = DownloadHelper::getInstance();
$url = $this->getURL();
$result = $downloadHelper->downloadFile($url, array('destinationPath' => $filepath));
}
if ($result == false)
{
$this->preventNextTry(__('Fatal Issue: Remote virtual file could not be downloaded for backup', 'shortpixel-image-optimiser'));
Log::addError('Remote file download failed from : ' . $url . ' to: ' . $filepath, $this->getURL());
$this->error_message = __('Remote file could not be downloaded' . $this->getFullPath(), 'shortpixel-image-optimiser');
return false;
}
$this->setVirtualToReal($filepath);
}
return parent::createBackup();
}
// @todo This is a breach of pattern to realize checking for changes to the main image path on conversion / duplicates.
private function getMainFile()
{
$fs = \wpSPIO()->filesystem();
return $fs->getMediaImage($this->id, true, true);
}
} // class

View File

@ -0,0 +1,54 @@
<?php
namespace ShortPixel\Model;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\Controller\ResponseController as ResponseController;
class ResponseModel
{
// Identification for Item.
public $item_id;
public $item_type; // set by queue
// General item variables
public $fileName;
public $is_error;
public $is_done;
public $apiStatus;
public $fileStatus;
// Images being processed variables. From APIController
public $tries;
public $images_done;
public $images_waiting;
public $images_total;
public $issue_type; // Optional - if there is any issue to report.
public $message; // This can be base text, but decision textually is within responsecontroller.
public $action; // Custom Operations use this ( i.e. migrate )
// public $queueName;
/**
*
* @param $item_id int The attachment_id of the item in process
* @param $item_type string item type: media or custom.
*
**/
public function __construct($item_id, $item_type)
{
$this->item_id = $item_id;
$this->item_type = $item_type; // media or custum
}
}

View File

@ -0,0 +1,216 @@
<?php
namespace ShortPixel\Model;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
class SettingsModel
{
protected static $instance;
private $option_name = 'spio_settings';
private $state_name = 'spio_states';
protected $model = array(
'apiKey' => array('s' => 'string'), // string
'verifiedKey' => array('s' => 'int'), // string
'compressionType' => array('s' => 'int'), // int
'resizeWidth' => array('s' => 'int'), // int
'resizeHeight' => array('s' => 'int'), // int
'processThumbnails' => array('s' => 'boolean'), // checkbox
'useSmartcrop' => array('s' => 'boolean'),
'backupImages' => array('s' => 'boolean'), // checkbox
'keepExif' => array('s' => 'int'), // checkbox
'resizeImages' => array('s' => 'boolean'),
'resizeType' => array('s' => 'string'),
'includeNextGen' => array('s' => 'boolean'), // checkbox
'png2jpg' => array('s' => 'int'), // checkbox
'CMYKtoRGBconversion' => array('s' => 'boolean'), //checkbox
'createWebp' => array('s' => 'boolean'), // checkbox
'createAvif' => array('s' => 'boolean'), // checkbox
'deliverWebp' => array('s' => 'int'), // checkbox
'optimizeRetina' => array('s' => 'boolean'), // checkbox
'optimizeUnlisted' => array('s' => 'boolean'), // $checkbox
'optimizePdfs' => array('s' => 'boolean'), //checkbox
'excludePatterns' => array('s' => 'exception'), // - processed, multi-layer, so skip
'siteAuthUser' => array('s' => 'string'), // string
'siteAuthPass' => array('s' => 'string'), // string
'frontBootstrap' => array('s' =>'boolean'), // checkbox
'autoMediaLibrary' => array('s' => 'boolean'), // checkbox
'excludeSizes' => array('s' => 'array'), // Array
'cloudflareEmail' => array('s' => 'string'), // string
'cloudflareAuthKey' => array('s' => 'string'), // string
'cloudflareZoneID' => array('s' => 'string'), // string
'cloudflareToken' => array('s' => 'string'),
'savedSpace' => array('s' => 'skip'),
'fileCount' => array('s' => 'skip'), // int
'under5Percent' => array('s' => 'skip'), // int
);
protected $state = array(
);
protected $settings;
protected $states;
public function __construct()
{
$this->checkLegacy();
$this->load();
}
public static function getInstance()
{
if (is_null(self::$instance))
{
self::$instance = new SettingsModel;
}
return self::$instance;
}
protected function load()
{
$settings = get_option($this->option_name);
}
protected function save()
{
update_option($this->option_name, $this->settings);
}
public function __get($name)
{
if (isset($this->settings[$name]))
{
return $this->sanitize($name, $this->settings[$name]);
}
}
protected function checkLegacy()
{
$this->deleteLegacy(); // very legacy, unused
// $this->convertLegacy(); // legacy, move to new format.
}
public function convertLegacy()
{
$options = array(
//optimization options
'apiKey' => array('key' => 'wp-short-pixel-apiKey'),
'verifiedKey' => array('key' => 'wp-short-pixel-verifiedKey'),
'compressionType' => array('key' => 'wp-short-pixel-compression'),
'processThumbnails' => array('key' => 'wp-short-process_thumbnails'),
'useSmartcrop' => array('key' => 'wpspio-usesmartcrop'),
'keepExif' => array('key' => 'wp-short-pixel-keep-exif'),
'CMYKtoRGBconversion' => array('key' => 'wp-short-pixel_cmyk2rgb'),
'createWebp' => array('key' => 'wp-short-create-webp'),
'createAvif' => array('key' => 'wp-short-create-avif'),
'deliverWebp' => array('key' => 'wp-short-pixel-create-webp-markup'),
'optimizeRetina' => array('key' => 'wp-short-pixel-optimize-retina'),
'optimizeUnlisted' => array('key' => 'wp-short-pixel-optimize-unlisted'),
'backupImages' => array('key' => 'wp-short-backup_images'),
'resizeImages' => array('key' => 'wp-short-pixel-resize-images'),
'resizeType' => array('key' => 'wp-short-pixel-resize-type'),
'resizeWidth' => array('key' => 'wp-short-pixel-resize-width'),
'resizeHeight' => array('key' => 'wp-short-pixel-resize-height'),
'siteAuthUser' => array('key' => 'wp-short-pixel-site-auth-user'),
'siteAuthPass' => array('key' => 'wp-short-pixel-site-auth-pass'),
'autoMediaLibrary' => array('key' => 'wp-short-pixel-auto-media-library'),
'optimizePdfs' => array('key' => 'wp-short-pixel-optimize-pdfs'),
'excludePatterns' => array('key' => 'wp-short-pixel-exclude-patterns'),
'png2jpg' => array('key' => 'wp-short-pixel-png2jpg'),
'excludeSizes' => array('key' => 'wp-short-pixel-excludeSizes'),
'currentVersion' => array('key' => 'wp-short-pixel-currentVersion'),
//CloudFlare
'cloudflareEmail' => array( 'key' => 'wp-short-pixel-cloudflareAPIEmail'),
'cloudflareAuthKey' => array( 'key' => 'wp-short-pixel-cloudflareAuthKey'),
'cloudflareZoneID' => array( 'key' => 'wp-short-pixel-cloudflareAPIZoneID'),
'cloudflareToken' => array( 'key' => 'wp-short-pixel-cloudflareToken'),
//optimize other images than the ones in Media Library
'includeNextGen' => array('key' => 'wp-short-pixel-include-next-gen'),
'hasCustomFolders' => array('key' => 'wp-short-pixel-has-custom-folders'),
'customBulkPaused' => array('key' => 'wp-short-pixel-custom-bulk-paused'),
//stats, notices, etc.
// @todo Most of this can go. See state machine comment.
'currentStats' => array('key' => 'wp-short-pixel-current-total-files'),
'fileCount' => array('key' => 'wp-short-pixel-fileCount'),
'thumbsCount' => array('key' => 'wp-short-pixel-thumbnail-count'),
'under5Percent' => array('key' => 'wp-short-pixel-files-under-5-percent'),
'savedSpace' => array('key' => 'wp-short-pixel-savedSpace'),
'apiRetries' => array('key' => 'wp-short-pixel-api-retries'),
'totalOptimized' => array('key' => 'wp-short-pixel-total-optimized'),
'totalOriginal' => array('key' => 'wp-short-pixel-total-original'),
'quotaExceeded' => array('key' => 'wp-short-pixel-quota-exceeded'),
'httpProto' => array('key' => 'wp-short-pixel-protocol'),
'downloadProto' => array('key' => 'wp-short-pixel-download-protocol'),
'downloadArchive' => array('key' => 'wp-short-pixel-download-archive'),
'activationDate' => array('key' => 'wp-short-pixel-activation-date'),
'mediaLibraryViewMode' => array('key' => 'wp-short-pixel-view-mode'),
'redirectedSettings' => array('key' => 'wp-short-pixel-redirected-settings'),
'convertedPng2Jpg' => array('key' => 'wp-short-pixel-converted-png2jpg'),
);
}
private function deleteLegacy()
{
delete_option('wp-short-pixel-activation-notice');
delete_option('wp-short-pixel-bulk-last-status'); // legacy shizzle
delete_option('wp-short-pixel-current-total-files');
delete_option('wp-short-pixel-remove-settings-on-delete-plugin');
// Bulk State machine legacy
$bulkLegacyOptions = array(
'wp-short-pixel-bulk-type',
'wp-short-pixel-bulk-last-status',
'wp-short-pixel-query-id-start',
'wp-short-pixel-query-id-stop',
'wp-short-pixel-bulk-count',
'wp-short-pixel-bulk-previous-percent',
'wp-short-pixel-bulk-processed-items',
'wp-short-pixel-bulk-done-count',
'wp-short-pixel-last-bulk-start-time',
'wp-short-pixel-last-bulk-success-time',
'wp-short-pixel-bulk-running-time',
'wp-short-pixel-cancel-pointer',
'wp-short-pixel-skip-to-custom',
'wp-short-pixel-bulk-ever-ran',
'wp-short-pixel-flag-id',
'wp-short-pixel-failed-imgs',
'bulkProcessingStatus',
'wp-short-pixel-prioritySkip',
);
$removedStats = array(
'wp-short-pixel-helpscout-optin',
'wp-short-pixel-activation-notice',
'wp-short-pixel-dismissed-notices',
'wp-short-pixel-media-alert',
);
$removedOptions = array(
'wp-short-pixel-remove-settings-on-delete-plugin',
'wp-short-pixel-custom-bulk-paused',
'wp-short-pixel-last-back-action',
'wp-short-pixel-front-bootstrap',
);
$toRemove = array_merge($bulkLegacyOptions, $removedStats, $removedOptions);
foreach($toRemove as $option)
{
delete_option($option);
}
}
} // class

View File

@ -0,0 +1,467 @@
<?php
namespace ShortPixel\Model;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Controller\OtherMediaController as OtherMediaController;
use ShortPixel\Model\Image\ImageModel as ImageModel;
use ShortPixel\Model\Image\MediaLibraryModel as MediaLibraryModel;
use ShortPixel\Helper\UtilHelper as UtilHelper;
use ShortPixel\Helper\InstallHelper as InstallHelper;
class StatsModel
{
// Below are counted and saved in settings
protected $totalOptimized; // combined filesize of optimized images
protected $totalOriginal; // combined filesize of original images
// There are gotten via SQL and saved in stats
//protected $totalImages;
//protected $totalThumbnails
protected $lastUpdate;
protected $path = array();
protected $currentStat; // used for chaining it.
protected $refreshStatTime;
// Commented out stats were dropped.
// Note: the difference in items / images including thumbs and the counts don't . This is due to technical difference in acquiring the data.
protected $defaults = array(
'media' => array('items' => -1, // total optimized media items found
'images' => -1, // total optimized images (+thumbs) found
'thumbs' => -1, // Optimized thumbs - SQL does thumbs, but queue doesn't. (imprecise query)
'itemsTotal' => -1, // Total items in media ( sql )
'thumbsTotal' => -1, // Total thumbs in media ( sql ) - imprecise query
'isLimited' => false,
/* 'lossy' => 0, // processed x compression
'lossy_thumbs' => 0, // main / thumbs
'lossless' => 0, // main /thumbs
'lossless_thumbs' => 0,
'glossy' => 0,
'glossy_thumbs' => 0, */
),
'custom' => array('items' => -1, // total optimized custom items
'images' => -1, // total optimized custom images
'itemsTotal' => -1,
/* 'lossy' => 0, // process x compression
'lossless' => 0,
'glossy' => 0, */
),
'period' => array('months' => // amount of images compressed in x month
array('1' => -1, /// count x months ago what was done.
'2' => -1,
'3' => -1,
'4' => -1,
),
),
'total' => array('items' => -1,
'images' => -1,
'thumbs' => -1,
'itemsTotal' => -1,
'thumbsTotal' => -1,
),
/* 'total' => array('items' => 0, // total items found
'images' => 0, // total images found
), */
);
protected $stats; // loaded as defaults, or from dbase.
public function __construct()
{
$this->refreshStatTime = apply_filters('shortpixel/statistics/refresh', WEEK_IN_SECONDS);
$this->load();
}
public function load()
{
$settings = \wpSPIO()->settings();
$this->totalOptimized = $settings->totalOptimized;
$this->totalOriginal = $settings->totalOriginal;
$stats = $settings->currentStats;
// Legacy. Stats from < 5.0 are loaded somehow. Don't load them.
if (isset($stats['APIKeyValid']))
$stats = $this->defaults;
$this->lastUpdate = (isset($stats['time'])) ? $stats['time'] : 0;
if ( ($this->lastUpdate + $this->refreshStatTime) >= time())
{
$this->stats = $stats;
}
else
$this->stats = $this->defaults;
}
public function save()
{
$settings = \wpSPIO()->settings();
$stats = $this->stats;
$stats['time'] = time();
$settings->currentStats = $stats;
}
public function reset()
{
$this->stats = $this->defaults;
\wpSPIO()->settings()->deleteOption('currentStats');
// $this->save();
}
// @todo This is not functional
public function add($stat)
{
if (property_exists($stat, 'images'))
$this->stats[$stat->type][$images] += $stats->images;
if (property_exists($stat, 'items'))
$this->stats[$stat->type][$items] += $stats->items;
}
public function get($name)
{
if (property_exists($this, $name))
return $this->$name;
else
return null;
}
public function getStat($type)
{
$this->currentStat = null;
if (isset($this->stats[$type]))
{
$this->currentStat = $this->stats[$type];
$this->path = [$type];
}
return $this;
}
public function grab($data)
{
if (is_null($this->currentStat))
return null;
if (array_key_exists($data, $this->currentStat))
{
$this->currentStat = $this->currentStat[$data];
$this->path[] = $data;
}
if (! is_array($this->currentStat))
{
if ($this->currentStat === -1)
{
$this->currentStat = $this->fetchStatdata(); // if -1 stat might not be loaded, load.
}
return $this->currentStat;
}
else
{
return $this;
}
}
private function fetchStatData()
{
$path = $this->path;
$data = -1;
if ($path[0] == 'period' && $path[1] == 'months' && isset($path[2]))
{
$month = $path[2];
$data = $this->countMonthlyOptimized(intval($month));
if ($data >= 0)
{
$this->stats['period']['months'][$month] = $data;
$this->save();
}
}
if ($path[0] == 'media')
{
switch($path[1])
{
case 'items':
$data = $this->countMediaItems(['optimizedOnly' => true]);
break;
case 'thumbs': // unrealiable if certain thumbs are not optimized, but the main image is.
$data = $this->countMediaThumbnails(['optimizedOnly' => true]);
break;
case 'images':
$data = $this->getStat('media')->grab('items') + $this->getStat('media')->grab('thumbs');
break;
case 'itemsTotal':
$data = $this->countMediaItems();
break;
case 'thumbsTotal':
$data = $this->countMediaThumbnails();
break;
case 'isLimited':
$data = $this->stats['media']['isLimited'];
break;
}
if ($data >= 0)
{
if (is_numeric($data))
{
$data = max($data, 0);
}
$this->stats['media'][$path[1]] = $data; // never allow any data below zero.
$this->save();
}
}
if ($path[0] == 'custom')
{
switch($path[1])
{
case 'items':
$data = $this->customItems(['optimizedOnly' => true]);
break;
case 'itemsTotal':
$data = $this->customItems();
break;
}
if ($data >= 0)
{
$this->stats['custom'][$path[1]] = $data;
$this->save();
}
}
if ($path[0] == 'total')
{
switch($path[1])
{
case 'items':
$media = $this->getStat('media')->grab('items');
$custom = $this->getStat('custom')->grab('items');
$data = $media + $custom;
break;
case 'images':
$media = $this->getStat('media')->grab('images');
$custom = $this->getStat('custom')->grab('items'); // items == images
$data = $media + $custom;
break;
case 'thumbs':
$data = $this->getStat('media')->grab('thumbs');
break;
case 'itemsTotal':
$media = $this->getStat('media')->grab('itemsTotal');
$custom = $this->getStat('custom')->grab('itemsTotal');
$data = $media + $custom;
break;
case 'thumbsTotal':
$data = $this->getStat('media')->grab('thumbsTotal');
break;
}
if ($data >= 0)
{
$this->stats['total'][$path[1]] = $data;
$this->save();
}
}
return $data;
}
// suboptimal over full stats implementation, but faster.
private function countMediaThumbnails($args = array())
{
global $wpdb;
$defaults = array(
'optimizedOnly' => false,
'limit' => 50000,
);
$args = wp_parse_args($args,$defaults);
$prepare = array();
if ($args['optimizedOnly'] == true)
{
$sql = ' SELECT count(id) as thumbcount FROM ' . UtilHelper::getPostMetaTable() . ' WHERE status = %d AND (image_type = %d or image_type = %d)';
$prepare = array(ImageModel::FILE_STATUS_SUCCESS, MediaLibraryModel::IMAGE_TYPE_THUMB, MediaLibraryModel::IMAGE_TYPE_ORIGINAL);
}
else {
// This query will return 2 positions after the thumbnail array declaration. Value can be up to two positions ( 0-100 thumbnails) . If positions is 1-10 intval will filter out the string part.
$sql = "SELECT meta_id, post_id, substr(meta_value, instr(meta_value,'sizes')+9,2) as thumbcount, LOCATE('original_image', meta_value) as originalImage FROM " . $wpdb->postmeta . " WHERE meta_key = '_wp_attachment_metadata' ";
$sql .= " AND post_id NOT IN ( SELECT post_id FROM " . $wpdb->postmeta . " where meta_key = '_shortpixel_prevent_optimize' )"; // exclude 'crashed items'
$sql .= " limit 0," . $args['limit'];
}
if (count($prepare) > 0)
{
$sql = $wpdb->prepare($sql, $prepare);
}
$results = $wpdb->get_results($sql);
//og::addDebug('Limit and count results' . $args['limit'] . ' ' . count($results));
if ($args['limit'] <= count($results))
{
$this->stats['media']['isLimited']= true;
}
$thumbCount = 0;
foreach($results as $row)
{
$count = intval($row->thumbcount);
if ($count > 0)
$thumbCount += $count;
if (property_exists($row, 'originalImage') && $row->originalImage > 0) // add to count, return value is string pos
$thumbCount++;
}
return intval($thumbCount);
}
private function countMediaItems($args = array())
{
global $wpdb;
$defaults = array(
'optimizedOnly' => false,
);
$args = wp_parse_args($args,$defaults);
$prepare = array();
if ($args['optimizedOnly'] == true)
{
//$sql .= ' AND post_id IN ( SELECT post_id FROM ' . $wpdb->postmeta . ' WHERE meta_key = "_shortpixel_optimized")';
$sql = ' SELECT count(id) as count FROM ' . UtilHelper::getPostMetaTable() . ' WHERE status = %d AND parent = %d';
$prepare = array(ImageModel::FILE_STATUS_SUCCESS, MediaLibraryModel::IMAGE_TYPE_MAIN);
}
else {
$sql = 'SELECT count(meta_id) FROM ' . $wpdb->postmeta . ' WHERE meta_key = "_wp_attached_file"';
$sql .= " AND post_id NOT IN ( SELECT post_id FROM " . $wpdb->postmeta . " where meta_key = '_shortpixel_prevent_optimize' )"; // exclude 'crashed items'
}
if (count($prepare) > 0)
$sql = $wpdb->prepare($sql, $prepare);
$count = $wpdb->get_var($sql);
if (is_null($count) && strpos($wpdb->last_error, 'exist') !== false)
{
InstallHelper::checkTables();
return 0;
}
return intval($count);
}
private function countMonthlyOptimized($monthsAgo = 1)
{
global $wpdb;
//$monthsAgo = 0 - $monthsAgo; // minus it for the sub.
/*$sql = "select meta_id from wp_postmeta where meta_key = '_shortpixel_meta' HAVING substr(meta_value, instr(meta_value, 'tsOptimized')+15,10) as stamp >= %d and stamp <= %d"; */
$date = new \DateTime();
$date->sub( new \DateInterval('P' . $monthsAgo . 'M'));
$dateUntil = new \DateTime();
$dateUntil->sub( new \DateInterval('P' . ($monthsAgo-1). 'M'));
$sql = 'SELECT count(id) FROM ' . $wpdb->prefix . 'shortpixel_postmeta WHERE tsOptimized >= %s and tsOptimized <= %s';
$sql = $wpdb->prepare($sql, $date->format('Y-m-d H:i:s'), $dateUntil->format('Y-m-d H:i:s') );
$count_media = $wpdb->get_var($sql);
// Custom
$sql = 'SELECT count(id) FROM ' . $wpdb->prefix . 'shortpixel_meta WHERE ts_optimized >= %s and ts_optimized <= %s';
$sql = $wpdb->prepare($sql, $date->format('Y-m-d H:i:s'), $dateUntil->format('Y-m-d H:i:s') );
$count_custom = $wpdb->get_var($sql);
$count = 0;
if (! is_null($count_media) && is_numeric($count_media))
$count += $count_media;
if (! is_null($count_custom) && is_numeric($count_custom))
$count += $count_custom;
return $count;
}
private function customItems($args = array())
{
global $wpdb;
$defaults = array(
'optimizedOnly' => false,
);
$args = wp_parse_args($args,$defaults);
$otherMediaController = OtherMediaController::getInstance();
if (! $otherMediaController->hasCustomImages() )
{
return 0;
}
$activeDirectories = $otherMediaController->getActiveDirectoryIDS();
// $foldersids = implode(',', $activeDirectories );
if (count($activeDirectories) == 0)
return 0; // no active folders
$in_str_arr = array_fill( 0, count( $activeDirectories ), '%s' );
$in_str = join( ',', $in_str_arr );
$sql = 'SELECT COUNT(id) as count FROM ' . $wpdb->prefix . 'shortpixel_meta WHERE folder_id in (' . $in_str . ')';
$sql = $wpdb->prepare($sql, $activeDirectories);
if ($args['optimizedOnly'] == true)
{
$sql .= ' AND status = %d';
$sql = $wpdb->prepare($sql, ImageModel::FILE_STATUS_SUCCESS);
}
$count = $wpdb->get_var($sql);
return $count;
}
} // class

View File

@ -0,0 +1,173 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Model\AccessModel as AccessModel;
class ViewController extends Controller
{
protected static $controllers = array();
protected static $viewsLoaded = array();
protected $model; // connected model to load.
protected $template = null; // template name to include when loading.
protected $data = array(); // data array for usage with databases data and such
protected $postData = array(); // data coming from form posts.
protected $mapper; // Mapper is array of View Name => Model Name. Convert between the two
protected $is_form_submit = false; // Was the form submitted?
protected $view; // object to use in the view.
protected $url; // if controller is home to a page, sets the URL here. For redirects and what not.
protected $form_action = 'sp-action';
public static function init()
{
foreach (get_declared_classes() as $class) {
if (is_subclass_of($class, 'ShortPixel\Controller') )
self::$controllers[] = $class;
}
}
public function __construct()
{
parent::__construct();
$this->view = new \stdClass;
// Basic View Construct
$this->view->notices = null; // Notices of class notice, for everything noticable
$this->view->data = null; // Data(base), to separate from regular view data
}
public static function getInstance() {
if (is_null(static::$instance)) {
static::$instance = new static;
}
return static::$instance;
}
/* Check if postData has been submitted.
* This function should always be called at any ACTION function ( load, load_$action etc ).
*/
protected function checkPost()
{
if(count($_POST) === 0) // no post, nothing to check, return silent.
{
return true;
}
elseif (! isset($_POST['sp-nonce']) || ! wp_verify_nonce( sanitize_key($_POST['sp-nonce']), $this->form_action))
{
Log::addInfo('Check Post fails nonce check, action : ' . $this->form_action, array($_POST) );
exit('Nonce Failed');
return false;
}
elseif (isset($_POST) && count($_POST) > 0)
{
check_admin_referer( $this->form_action, 'sp-nonce' ); // extra check, when we are wrong here, it dies.
// unset($_POST['sp-nonce']);
// unset($_POST['_wp_http_referer']);
$this->is_form_submit = true;
$this->processPostData($_POST);
}
return true;
}
public function access()
{
return AccessModel::getInstance();
}
/** Loads a view
*
* @param String View Template in view directory to load. When empty will search for class attribute
*/
public function loadView($template = null, $unique = true)
{
// load either param or class template.
$template = (is_null($template)) ? $this->template : $template;
if (is_null($template) )
{
return false;
}
elseif (strlen(trim($template)) == 0)
{
return false;
}
$view = $this->view;
$controller = $this;
$template_path = \wpSPIO()->plugin_path('class/view/' . $template . '.php');
if (file_exists($template_path) === false)
{
Log::addError("View $template could not be found in " . $template_path,
array('class' => get_class($this)));
}
elseif ($unique === false || ! in_array($template, self::$viewsLoaded))
{
include($template_path);
self::$viewsLoaded[] = $template;
}
}
/** Accepts POST data, maps, checks missing fields, and applies sanitization to it.
* @param array $post POST data
*/
protected function processPostData($post)
{
// If there is something to map, map.
if ($this->mapper && is_array($this->mapper) && count($this->mapper) > 0)
{
foreach($this->mapper as $item => $replace)
{
if ( isset($post[$item]))
{
$post[$replace] = $post[$item];
unset($post[$item]);
}
}
}
if (is_null($this->model))
{
foreach($post as $name => $value )
{
$this->postData[sanitize_text_field($name)] = sanitize_text_field($value);
return true;
}
}
else
{
$model = $this->model;
$this->postData = $model->getSanitizedData($post);
}
return $this->postData;
}
/** Sets the URL of the admin page */
public function setControllerURL($url)
{
$this->url = $url;
}
} // controller

View File

@ -0,0 +1,107 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
class Woocommerce
{
// public function $new_sizes = array();
protected static $SIGNAL = false;
public function __construct()
{
add_action('plugins_loaded', array($this, 'hooks'));
}
public function hooks()
{
if (\wpSPIO()->env()->plugin_active('woocommerce'))
{
add_filter('woocommerce_regenerate_images_intermediate_image_sizes', array($this, 'signalWC'));
add_filter('woocommerce_debug_tools', array($this, 'addWarning'));
// If new images are created, drop the optimize data of them . Late as possible, this is a hook often used by plugins to refine.
add_filter('intermediate_image_sizes_advanced', array($this, 'handleCreateImages'), 99, 3);
}
}
// This hook is ran just before create new images / regenerating them. Only then signal to check for optimized thumbs et al.
public function signalWC()
{
self::$SIGNAL = true;
}
/** Hook to run when Wordpress is about to generate new thumbnails. Remove backup and optimize data if that happens */
public function handleCreateImages($new_sizes, $image_meta, $attach_id)
{
// No signal, no run.
if (false === self::$SIGNAL)
{
return $new_sizes;
}
if (count($new_sizes) === 0)
{
self::$SIGNAL = false;
return $new_sizes;
}
$fs = \wpSPIO()->filesystem();
$mediaImage = $fs->getMediaImage($attach_id);
$changes = false;
if (is_object($mediaImage))
{
// Performance; This item is not in database, hence not optimized in any way.
if (! is_null($mediaImage->getMeta('databaseID')))
{
foreach($new_sizes as $new_size => $data)
{
$thumbnailObj = $mediaImage->getThumbNail($new_size);
if (is_object($thumbnailObj) && $thumbnailObj->isOptimized())
{
$thumbnailObj->onDelete();
$changes = true;
}
}
}
else {
}
}
if (true === $changes)
{
$mediaImage->saveMeta();
}
self::$SIGNAL = false;
return $new_sizes;
}
public function addWarning($tools)
{
if (isset($tools['regenerate_thumbnails']) && \wpSPIO()->env()->is_autoprocess)
{
$text = $tools['regenerate_thumbnails']['desc'];
$text .= sprintf(
'<br><br><strong class="red">%1$s</strong> %2$s',
__( 'ShortPixel Image Optimizer Note:', 'shortpixel-image-optimiser' ),
__( 'The ShortPixel Image Optimizer plugin is set to automatically optimize images on upload. When running the thumbnails tools, each image that is not optimized will be added to the queue. It is recommend to disable this option while running these tools', 'shortpixel-image-optimiser')
);
$tools['regenerate_thumbnails']['desc'] = $text;
}
return $tools;
}
} // class
$w = new Woocommerce();

View File

@ -0,0 +1,176 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
class cacheRemover
{
protected $has_supercache = false; // supercache seems to replace quite fine, without our help. @todo Test if this is needed
protected $has_w3tc = false;
protected $has_wpengine = false;
protected $has_fastestcache = false;
protected $has_siteground = false;
protected $has_litespeed = false;
private static $instance;
public function __construct()
{
$this->addHooks();
$this->checkCaches();
}
public static function getInstance()
{
if (is_null(self::$instance))
self::$instance = new cacheRemover();
return self::$instance;
}
public function addHooks()
{
add_action('shortpixel/image/optimised', array($this, 'flushCache'));
}
/** Checks which cache plugins are active on the moment a flush is needed */
public function checkCaches()
{
if ( function_exists( 'w3tc_pgcache_flush' ) )
$this->has_w3tc = true;
if ( function_exists('wp_cache_clean_cache') )
$this->has_supercache = true;
if ( class_exists( 'WpeCommon' ) )
$this->has_wpengine = true;
global $wp_fastest_cache;
if ( method_exists( 'WpFastestCache', 'deleteCache' ) && !empty( $wp_fastest_cache ) )
$this->has_fastestcache = true;
// SG SuperCacher
if (function_exists('sg_cachepress_purge_cache')) {
$this->has_siteground = true;
}
if (defined( 'LSCWP_DIR' ))
{
$this->has_litespeed = true;
}
// @todo WpRocket?
// @todo BlueHost Caching?
}
/* Tries to flush cache there were we have issues
*
* @param Array $args Argument Array to provide data.
*/
public function flushCache($imageItem)
{
if ($imageItem->get('type') == 'custom')
{
$post_id = 0;
}
else {
$post_id = $imageItem->get('id');
}
// important - first check the available cache plugins
$this->checkCaches();
$bool = apply_filters('shortpixel/external/flush_cache', true, $post_id, $imageItem);
if (false === $bool)
{
return false;
}
// general WP
if ($post_id > 0)
clean_post_cache($post_id);
else
wp_cache_flush();
/* Verified working without.
if ($this->has_supercache)
$this->removeSuperCache();
*/
if ($this->has_w3tc)
$this->removeW3tcCache();
if ($this->has_wpengine)
$this->removeWpeCache();
if ($this->has_siteground)
$this->removeSiteGround();
if ($this->has_fastestcache)
$this->removeFastestCache();
if ($this->has_litespeed)
$this->litespeedReset($imageItem);
}
protected function removeSuperCache()
{
global $file_prefix, $supercachedir;
if ( empty( $supercachedir ) && function_exists( 'get_supercache_dir' ) ) {
$supercachedir = get_supercache_dir();
}
wp_cache_clean_cache( $file_prefix );
}
protected function removeW3tcCache()
{
w3tc_pgcache_flush();
}
protected function removeWpeCache()
{
if ( method_exists( 'WpeCommon', 'purge_memcached' ) ) {
\WpeCommon::purge_memcached();
}
if ( method_exists( 'WpeCommon', 'clear_maxcdn_cache' ) ) {
\WpeCommon::clear_maxcdn_cache();
}
if ( method_exists( 'WpeCommon', 'purge_varnish_cache' ) ) {
\WpeCommon::purge_varnish_cache();
}
}
protected function removeFastestCache()
{
global $wp_fastest_cache;
$wp_fastest_cache->deleteCache();
}
protected function removeSiteGround()
{
sg_cachepress_purge_cache();
}
protected function litespeedReset($imageItem)
{
// Suppress the notices on purge.
if (! defined( 'LITESPEED_PURGE_SILENT' ))
{
define('LITESPEED_PURGE_SILENT', true);
}
$urls = $imageItem->getAllUrls();
foreach($urls as $url)
{
do_action('litespeed_purge_url', $url, false, true);
}
// do_action('litespeed_media_reset', $post_id);
}
}
cacheRemover::getInstance();

View File

@ -0,0 +1,259 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Notices\NoticeController as Notices;
// @todo Clean up unused lines in this file. (cloudflare)
class CloudFlareAPI {
private $email; // $_cloudflareEmail
private $authkey; // $_cloudflareAuthKey
private $zone_id; // $_cloudflareZoneID
private $token;
private $setup_done = false;
private $config_ok = false;
private $use_token = false;
private $cf_exists = true;
private $api_url = 'https://api.cloudflare.com/client/v4/zones/';
/*public function __construct($cloudflareEmail, $cloudflareAuthKey, $cloudflareZoneID) {
$this->set_up($cloudflareEmail, $cloudflareAuthKey, $cloudflareZoneID);
$this->set_up_required_hooks();
} */
public function __construct()
{
add_action('shortpixel/image/optimised', array( $this, 'check_cloudflare' ), 10 );
add_action('shortpixel/image/before_restore', array($this, 'check_cloudflare'), 10);
}
public function setup()
{
$this->email = \wpSPIO()->settings()->cloudflareEmail;
$this->authkey = \wpSPIO()->settings()->cloudflareAuthKey;
$this->zone_id = (defined('SHORTPIXEL_CFZONE') ) ? SHORTPIXEL_CFZONE : \wpSPIO()->settings()->cloudflareZoneID;
$this->token = (defined('SHORTPIXEL_CFTOKEN') ) ? SHORTPIXEL_CFTOKEN : \wpSPIO()->settings()->cloudflareToken;
if (! empty($this->token) && ! empty($this->zone_id))
{
$this->use_token = true;
$this->config_ok = true;
}
elseif(! empty($this->email) && ! empty($this->authkey) && ! empty($this->zone_id))
{
$this->config_ok = true;
}
if (empty($this->zone_id))
{
//Log::addWarn('CF - ZoneID setting is obligatory');
}
$this->setup_done = true;
}
public function check_cloudflare($imageObj)
{
if (! $this->setup_done)
$this->setup();
if ($this->config_ok)
{
if (! function_exists('curl_init'))
{
Log::addWarn('Cloudflare Config OK, but no CURL to request');
}
else
$this->start_cloudflare_cache_purge_process($imageObj);
}
}
/**
* @desc Start the process of purging all cache for image URL (includes all the image sizes/thumbnails)f1
*
* @param $image_id - WordPress image media ID
*/
private function start_cloudflare_cache_purge_process( $imageItem ) {
// Fetch CloudFlare API credentials
/*$cloudflare_auth_email = $this->_cloudflareEmail;
$cloudflare_auth_key = $this->_cloudflareAuthKey;
$cloudflare_zone_id = $this->_cloudflareZoneID; */
// Fetch all WordPress install possible thumbnail sizes ( this will not return the full size option )
$fetch_images_sizes = get_intermediate_image_sizes();
$purge_array = array();
$prepare_request_info = array();
// if full image size tag is missing, we need to add it
if ( ! in_array( 'full', $fetch_images_sizes ) ) {
$fetch_images_sizes[] = 'full';
}
/*
if ( $imageItem->getType() == 'media' ) {
// The item is a Media Library item, fetch the URL for each image size
foreach ( $fetch_images_sizes as $size ) {
// 0 - url; 1 - width; 2 - height
$image_attributes = wp_get_attachment_image_src( $image_id, $size );
// Append to the list
array_push( $purge_array, $image_attributes[0] );
}
} else {
// The item is a Custom Media item
$fs = \wpSPIO()->filesystem();
$item = $fs->getImage( $image_id, 'custom' );
$item_url = $fs->pathToUrl( $item );
array_push( $purge_array, $item_url );
}
*/
$fs = \wpSPIO()->filesystem();
$image_paths[] = $imageItem->getURL();
if ($imageItem->getWebp() !== false)
$image_paths[] = $fs->pathToUrl($imageItem->getWebp());
if ($imageItem->getAvif() !== false)
$image_paths[] = $fs->pathToUrl($imageItem->getAvif());
if ($imageItem->get('type') == 'media')
{
if ($imageItem->hasOriginal())
{
$originalFile = $imageItem->getOriginalFile();
$image_paths[] = $originalFile->getURL();
if ($originalFile->getWebp() !== false)
$image_paths[] = $fs->pathToUrl($originalFile->getWebp());
if ($originalFile->getAvif() !== false)
$image_paths[] = $fs->pathToUrl($originalFile->getAvif());
}
if (count($imageItem->get('thumbnails')) > 0)
{
foreach($imageItem->get('thumbnails') as $thumbObj)
{
$image_paths[] = $thumbObj->getURL();
if ($thumbObj->getWebp() !== false)
$image_paths[] = $fs->pathToUrl($thumbObj->getWebp());
if ($thumbObj->getAvif() !== false)
$image_paths[] = $fs->pathToUrl($thumbObj->getAvif());
}
}
}
if ( ! empty( $image_paths ) ) {
//$prepare_request_info['files'] = $image_url_for_purge;
// Encode the data into JSON before send
$dispatch_purge_info = function_exists('wp_json_encode') ? wp_json_encode( $prepare_request_info ) : json_encode( $prepare_request_info );
// Set headers for remote API to authenticate for the request
/* $dispatch_header = array(
'X-Auth-Email: ' . $cloudflare_auth_email,
'X-Auth-Key: ' . $cloudflare_auth_key,
'Content-Type: application/json'
); */
$response = $this->delete_url_cache_request_action($image_paths);
// Start the process of cache purge
/* $request_response = $this->delete_url_cache_request_action( "https://api.cloudflare.com/client/v4/zones/" . $cloudflare_zone_id . "/purge_cache", $dispatch_purge_info, $dispatch_header ); */
} else {
// No use in running the process
}
}
/**
* @desc Send a delete cache request to CloudFlare for specified URL(s)
* Implements -> https://api.cloudflare.com/#zone-purge-files-by-url
* @return array|mixed|object - Request response as decoded JSON
*/
private function delete_url_cache_request_action( $files ) {
$request_url = $this->api_url . $this->zone_id . '/purge_cache';
$postfields = array('files' => $files);
return $this->doRequest($request_url, $postfields);
}
private function addAuth($headers)
{
if ($this->use_token)
{
$headers['authorization'] = 'Authorization: Bearer ' . $this->token;
}
else
{
$headers['x-auth-email'] = 'X-Auth-Email: ' . $this->email;
$headers['x-auth-key'] = 'X-Auth-Key: ' . $this->authkey;
}
return $headers;
}
/**
* @param $url String . Api Url to target with zone_id and acton
* @param $postfields Array . Fields for POST
* @param $headers Valid HTTP headers to add.
*/
private function doRequest($url, $postfields, $headers = array())
{
if(!function_exists('curl_init'))
{ return false; }
$curl_connection = curl_init();
$default_headers =
array('content_type' => 'Content-Type: application/json');
$default_headers = $this->addAuth($default_headers);
$headers = wp_parse_args($headers, $default_headers);
$headers = array_filter(array_values($headers));
$postfields = wp_json_encode($postfields);
curl_setopt( $curl_connection, CURLOPT_URL, $url );
curl_setopt( $curl_connection, CURLOPT_CUSTOMREQUEST, "POST" );
curl_setopt( $curl_connection, CURLOPT_POSTFIELDS, $postfields);
curl_setopt( $curl_connection, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $curl_connection, CURLOPT_HTTPHEADER, $headers );
curl_setopt( $curl_connection, CURLOPT_CONNECTTIMEOUT, 5); // in seconds!
curl_setopt( $curl_connection, CURLOPT_TIMEOUT, 10); // in seconds!
curl_setopt( $curl_connection, CURLOPT_USERAGENT, '"User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.87 Safari/537.36"' );
$request_response = curl_exec( $curl_connection );
$result = json_decode( $request_response, true );
curl_close( $curl_connection );
if ( ! is_array( $result ) ) {
Log::addWarn( 'ShortPixel - CloudFlare: The CloudFlare API is not responding correctly', $result);
} elseif ( isset( $result['success'] ) && isset( $result['errors'] ) && false === (bool) $result['success'] ) {
Log::addWarn( 'ShortPixel - CloudFlare, Error messages: '
. (isset($result['errors']['message']) ? $result['errors']['message'] : json_encode($result['errors'])) );
} else {
Log::addInfo('ShortPixel - CloudFlare successfully requested clear cache for: ', array($postfields));
}
return $result;
}
}
$c = new CloudFlareAPI(); // monitor hook.

View File

@ -0,0 +1,60 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\Controller\OtherMediaController as OtherMediaController;
// Gravity Forms integrations.
class gravityForms
{
public function __construct()
{
// @todo All this off, because it can only fatal error.
// add_filter( 'gform_save_field_value', array($this,'shortPixelGravityForms'), 10, 5 );
}
function shortPixelGravityForms( $value, $lead, $field, $form ) {
if($field->type == 'post_image') {
$this->handleGravityFormsImageField($value);
}
return $value;
}
public function handleGravityFormsImageField($value) {
$fs = \wpSPIO()->filesystem();
$otherMediaController = OtherMediaController::getInstance();
$uploadBase = $fs->getWPUploadBase();
$gravFolder = $otherMediaController->getFolderByPath($uploadBase->getPath() . 'gravity_forms');
if (! $gravFolder->exists())
return false;
/* no clue what this legacy is suppposed to be.
if(strpos($value , '|:|')) {
$cleanup = explode('|:|', $value);
$value = $cleanup[0];
}
*/
if (! $gravFolder->get('in_db'))
{
$otherMediaController->addDirectory($gravFolder->getPath());
}
//ShortPixel is monitoring the gravity forms folder, add the image to queue
// $uploadDir = wp_upload_dir();
//$localPath = str_replace($uploadDir['baseurl'], SHORTPIXEL_UPLOADS_BASE, $value);
//return $shortPixelObj->addPathToCustomFolder($localPath, $folder->getId(), 0);
}
} // class
$g = new gravityForms();

View File

@ -0,0 +1,70 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Notices\NoticeController as Notices;
// Image gallery plugins that require a few small extra's
class ImageGalleries
{
public function __construct()
{
add_action('admin_init', array($this, 'addConstants'));
add_filter('shortpixel/init/optimize_on_screens', array($this, 'add_screen_loads'));
}
// This adds constants for mentioned plugins checking for specific suffixes on addUnlistedImages.
// @integration Envira Gallery
// @integration Soliloquy
public function addConstants()
{
//if( !defined('SHORTPIXEL_CUSTOM_THUMB_SUFFIXES')) {
if(\is_plugin_active('envira-gallery/envira-gallery.php') ||
\is_plugin_active('soliloquy-lite/soliloquy-lite.php') ||
\is_plugin_active('soliloquy/soliloquy.php') ||
\is_plugin_active('envira-gallery-lite/envira-gallery-lite.php')
)
{
add_filter('shortpixel/image/unlisted_suffixes', array($this, 'envira_suffixes'));
//define('SHORTPIXEL_CUSTOM_THUMB_SUFFIXES', '_c,_tl,_tr,_br,_bl');
// }
// not in use?
// elseif(defined('SHORTPIXEL_CUSTOM_THUMB_SUFFIX')) {
// define('SHORTPIXEL_CUSTOM_THUMB_SUFFIXES', SHORTPIXEL_CUSTOM_THUMB_SUFFIX);
// }
}
}
public function add_screen_loads($screens)
{
// Envira Gallery Lite
$screens[] = 'edit-envira';
$screens[] = 'envira';
// Solo Cuy
$screens[] = 'edit-soliloquy';
$screens[] = 'soliloquy';
return $screens;
}
public function envira_suffixes($suffixes)
{
$envira_suffixes = array('_c','_tl','_tr','_br','_bl', '-\d+x\d+');
$suffixes = array_merge($suffixes, $envira_suffixes);
return $suffixes;
}
} // class
$c = new ImageGalleries();

View File

@ -0,0 +1,376 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\Notices\NoticeController as Notice;
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Model\File\DirectoryOtherMediaModel as DirectoryOtherMediaModel;
use ShortPixel\Controller\OtherMediaController as OtherMediaController;
use ShortPixel\Controller\AdminNoticesController as AdminNoticesController;
use ShortPixel\NextGenViewController as NextGenViewController;
// @integration NextGen Gallery
class NextGenController
{
protected static $instance;
// protected $view;
private $enableOverride = false; // when activating NG will not report active yet, but try to refresh folders. Do so.
// ngg_created_new_gallery
public function __construct()
{
add_filter('shortpixel/init/optimize_on_screens', array($this, 'add_screen_loads'));
add_action('plugins_loaded', array($this, 'hooks'));
add_action('deactivate_nextgen-gallery/nggallery.php', array($this, 'resetNotification'));
}
public function hooks()
{
$controller = new NextGenViewController();
if ($this->optimizeNextGen()) // if optimization is on, hook.
{
add_action('ngg_update_addgallery_page', array( $this, 'addNextGenGalleriesToCustom'));
add_action('ngg_added_new_image', array($this,'handleImageUpload'));
}
if ($this->has_nextgen())
{
add_action('ngg_delete_image', array($this, 'OnDeleteImage'),10, 2); // this works only on single images!
add_action('shortpixel/othermedia/folder/load', array($this, 'loadFolder'), 10, 2);
// Off because this causes bad UX ( refresh folder but no images added)
//add_action('shortpixel/othermedia/addfiles', array($this, 'checkAddFiles'), 10, 3);
add_filter( 'ngg_manage_images_columns', array( $controller, 'nggColumns' ) );
add_filter( 'ngg_manage_images_number_of_columns', array( $controller, 'nggCountColumns' ) );
add_filter( 'ngg_manage_images_column_7_header', array( $controller, 'nggColumnHeader' ) );
add_filter( 'ngg_manage_images_column_7_content', array( $this, 'loadNextGenItem' ), 10,2 );
add_filter('ngg_manage_gallery_fields', array($this, 'refreshFolderOnLoad'), 10, 2);
}
}
// Use GetInstance, don't use the construct.
public static function getInstance()
{
if (is_null(self::$instance))
self::$instance = new NextGenController();
return self::$instance;
}
public function has_nextgen()
{
if (defined('NGG_PLUGIN'))
return true;
else
return false;
}
public function optimizeNextGen()
{
if (true === $this->enableOverride || \wpSPIO()->settings()->includeNextGen == 1)
{
return true;
}
return false;
}
public function isNextGenScreen()
{
$screens = $this->add_screen_loads(array());
if (! is_admin())
{
return false;
}
if (! function_exists('get_current_screen'))
{
return false;
}
$screen_id = \wpSPIO()->env()->screen_id;
if (in_array($screen_id, $screens))
return true;
else
return false;
}
/** called from settingController when enabling the nextGen settings */
public function enableNextGen($silent)
{
$this->enableOverride = true;
$this->addNextGenGalleriesToCustom($silent);
}
public function add_screen_loads($use_screens)
{
$use_screens[] = 'toplevel_page_nextgen-gallery'; // toplevel
$use_screens[] = 'nextgen-gallery_page_ngg_addgallery'; // add gallery
$use_screens[] = 'nextgen-gallery_page_nggallery-manage-album'; // manage album
$use_screens[] = 'nextgen-gallery_page_nggallery-manage-gallery'; // manage toplevel gallery
$use_screens[] = 'nggallery-manage-images'; // images in gallery overview
return $use_screens;
}
public function loadNextGenItem($unknown, $picture)
{
$viewController = new NextGenViewController();
$viewController->loadItem($picture);
}
public function refreshFolderOnLoad($array, $gallery)
{
$galleries = $this->getGalleries($gallery->gid);
if (isset($galleries[0]))
{
$otherMedia = OtherMediaController::getInstance();
$galleryFolder = $galleries[0];
$folder = $otherMedia->getFolderByPath($galleryFolder->getPath());
$folder->refreshFolder(true);
}
return $array;
}
/** Enables nextGen, add galleries to custom folders
* @param boolean $silent Throw a notice or not. This seems to be based if nextgen was already activated previously or not.
*/
/*
public function nextGenEnabled($silent)
{
$this->addNextGenGalleriesToCustom($silent);
} */
/** Tries to find a nextgen gallery for a shortpixel folder.
* Purpose is to test if this folder is a nextgen gallery
* Problem is that NG stores folders in a short format, not from root while SPIO stores whole path
* Assumption: The last two directory names should lead to an unique gallery and if so, it's nextgen
* @param $id int Folder ID
* @param $directory DirectoryOtherMediaModel Directory Object
*/
public function loadFolder($id, $directory)
{
$path = $directory->getPath();
// No reason to check this?
if ($directory->get('status') == DirectoryOtherMediaModel::DIRECTORY_STATUS_NEXTGEN)
{ return; }
$path_split = array_filter(explode('/', $path));
$searchPath = trailingslashit(implode('/', array_slice($path_split, -2, 2)));
global $wpdb;
$sql = "SELECT gid FROM {$wpdb->prefix}ngg_gallery WHERE path LIKE %s";
$sql = $wpdb->prepare($sql, '%' . $searchPath . '');
$gid = $wpdb->get_var($sql);
if (! is_null($gid) && is_numeric($gid))
{
$res = $directory->set('status', DirectoryOtherMediaModel::DIRECTORY_STATUS_NEXTGEN);
$directory->save();
//echo $gid;
}
}
/** Hook. If there is a refreshFolder action on a nextGen Directory, but the optimize Nextgen setting is off, it should not add those files to the custom Media */
public function checkAddFiles($bool, $files, $dirObj)
{
// Nothing nextgen.
if ($dirObj->get('is_nextgen') === false)
{
return $bool;
}
// If it's nextgen, but the setting is not on, reject those files.
if ($this->optimizeNextGen() === false)
{
return false;
}
return $bool;
}
/* @return DirectoryModel */
public function getGalleries($id = null)
{
global $wpdb;
$fs = \wpSPIO()->filesystem();
$homepath = $fs->getWPFileBase();
$sql = "SELECT path FROM {$wpdb->prefix}ngg_gallery";
if (! is_null($id))
{
$sql .= ' WHERE gid = %d';
$sql = $wpdb->prepare($sql, $id);
}
$result = $wpdb->get_results($sql);
$galleries = array();
foreach($result as $row)
{
$directory = $fs->getDirectory($homepath->getPath() . $row->path);
if ($directory->exists())
$galleries[] = $directory;
}
return $galleries;
}
/** Adds nextGen galleries to custom table
* Note - this function does *Not* check if nextgen is enabled, not if checks custom Tables. Use nextgenEnabled for this.
* Enabled checks are not an external class issue, so must be done before calling.
*/
public function addNextGenGalleriesToCustom($silent = true) {
$fs = \wpSPIO()->filesystem();
$homepath = $fs->getWPFileBase();
//add the NextGen galleries to custom folders
$ngGalleries = $this->getGalleries(); // DirectoryModel return.
$otherMedia = OtherMediaController::getInstance();
foreach($ngGalleries as $gallery)
{
$folder = $otherMedia->getFolderByPath($gallery->getPath());
if ($folder->get('in_db') === true)
{
if ($folder->get('status') !== 1)
{
$folder->set('status', DirectoryOtherMediaModel::DIRECTORY_STATUS_NEXTGEN);
$folder->save();
}
continue;
}
else
{
// Try to silently fail this if directory is not allowed.
if (false === $folder->checkDirectory(true))
{
continue;
}
$directory = $otherMedia->addDirectory($gallery->getPath());
if (! $directory)
{
Log::addWarn('Could not add this directory' . $gallery->getPath() );
}
else
{
$directory->set('status', DirectoryOtherMediaModel::DIRECTORY_STATUS_NEXTGEN);
$directory->save();
}
}
}
if (count($ngGalleries) > 0)
{
// put timestamp to this setting.
$settings = \wpSPIO()->settings();
$settings->hasCustomFolders = time();
}
}
public function handleImageUpload($image)
{
$otherMedia = OtherMediaController::getInstance();
//$fs = \wpSPIO()->filesystem();
if ($this->optimizeNextGen() === true) {
$imageFsPath = $this->getImageAbspath($image);
$otherMedia->addImage($imageFsPath, array('is_nextgen' => true));
}
}
public function resetNotification()
{
Notice::removeNoticeByID('MSG_INTEGRATION_NGGALLERY');
}
public function onDeleteImage($nggId, $size)
{
$image = $this->getNGImageByID($nggId);
$paths = array();
if ($size === false)
{
$imageSizes = $this->getImageSizes($image);
foreach($imageSizes as $size)
{
$paths[] = $this->getImageAbspath($image, $size);
}
}
else {
$paths = array_merge($paths, $this->getImageAbspath($image, $size));
}
foreach($paths as $path)
{
$otherMediaController = OtherMediaController::getInstance();
$mediaItem = $otherMediaController->getCustomImageByPath($path);
$mediaItem->onDelete();
}
}
public function updateImageSize($nggId, $path) {
$image = $this->getNGImageByID($nggId);
$dimensions = getimagesize($this->getImageAbspath($image));
$size_meta = array('width' => $dimensions[0], 'height' => $dimensions[1]);
$image->meta_data = array_merge($image->meta_data, $size_meta);
$image->meta_data['full'] = $size_meta;
$this->saveToNextGen($image);
}
protected function getNGImageByID($nggId)
{
$mapper = \C_Image_Mapper::get_instance();
$image = $mapper->find($nggId);
return $image;
}
/* @param NextGen Image */
protected function saveToNextGen($image)
{
$mapper = \C_Image_Mapper::get_instance();
$mapper->save($image);
}
protected function getImageAbspath($image, $size = 'full') {
$storage = \C_Gallery_Storage::get_instance();
return $storage->get_image_abspath($image, $size);
}
protected function getImageSizes($image)
{
$storage = \C_Gallery_Storage::get_instance();
return $storage->get_image_sizes($image);
}
} // class.
$ng = NextGenController::getInstance();

View File

@ -0,0 +1,78 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\Notices\NoticeController as Notice;
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Helper\UiHelper as UiHelper;
use ShortPixel\Controller\OtherMediaController as OtherMediaController;
/* Class for View integration in the Nextgen gallery */
class NextGenViewController extends \ShortPixel\ViewController
{
protected static $nggColumnIndex = 0;
protected $template = 'view-list-media';
protected function hooks()
{
}
public function nggColumns( $defaults ) {
self::$nggColumnIndex = count($defaults) + 1;
/* add_filter( 'ngg_manage_images_column_' . self::$nggColumnIndex . '_header', array( '\ShortPixel\nextGenViewController', 'nggColumnHeader' ) );
add_filter( 'ngg_manage_images_column_' . self::$nggColumnIndex . '_content', array( '\ShortPixel\nextGenViewController', 'nggColumnContent' ), 10, 2 );
$defaults['wp-shortPixelNgg'] = 'ShortPixel Compression'; */
return $defaults;
}
public function nggCountColumns( $count ) {
return $count + 1;
}
public function nggColumnHeader( $default ) {
wp_enqueue_style('dashicons');
$this->loadView('snippets/part-comparer');
return __('ShortPixel Compression','shortpixel-image-optimiser');
}
public function loadItem( $nextGenObj ) {
$this->view = new \stdClass; // reset every row
$otherMediaController = OtherMediaController::getInstance();
$mediaItem = $otherMediaController->getCustomImageByPath($nextGenObj->imagePath);
$this->view->mediaItem = $mediaItem;
$this->view->id = $mediaItem->get('id');
$this->view->text = UiHelper::getStatusText($mediaItem);
$this->view->list_actions = UiHelper::getListActions($mediaItem);
if ( count($this->view->list_actions) > 0)
$this->view->list_actions = UiHelper::renderBurgerList($this->view->list_actions, $mediaItem);
else
$this->view->list_actions = '';
$this->view->actions = UiHelper::getActions($mediaItem);
//$this->view->actions = $actions;
if (! $this->userIsAllowed)
{
$this->view->actions = array();
$this->view->list_actions = '';
}
$this->loadView($this->template, false);
}
} // class

View File

@ -0,0 +1,88 @@
<?php
namespace ShortPixel\External\Offload;
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// Class to check what offloader to use and load it. To offload.
class Offloader
{
private static $instance;
private static $offload_instance;
private $offloadName;
public static function getInstance()
{
if (is_null(self::$instance))
{
self::$instance = new Offloader();
}
return self::$instance;
}
public function __construct()
{
add_action('plugins_loaded', array($this, 'load'));
add_action('as3cf_init', array($this, 'initS3Offload'));
}
public function load()
{
$bool = $this->checkVirtualLoaders();
if (true === $bool)
{
self::$offload_instance = new VirtualFileSystem($this->offloadName);
}
}
protected function checkVirtualLoaders()
{
if ( class_exists('\Stack\Config') ) // Bitpoke Stack MU
{
$this->offloadName = 'stack';
return true;
}
elseif (defined('STACK_MEDIA_BUCKET'))
{
$this->offloadName = 'stack';
return true;
}
elseif (class_exists('\S3_Uploads\Plugin'))
{
$this->offloadName = 's3-uploads-human';
return true;
}
/* (Doesn't work)
elseif (function_exists('ud_check_stateless_media'))
{
$this->offloadName = 'wp-stateless';
return true;
} */
return false;
}
// If As3cfInit is called check WpOffload runtime. This is later in order than plugins_loaded!
public function initS3Offload($as3cf)
{
if (is_null(self::$offload_instance))
{
$this->offloadName = 'wp-offload';
self::$offload_instance = new wpOffload($as3cf);
}
else {
Log::addError('Instance is not null - other virtual component has loaded! (' . $this->offloadName . ')');
}
}
public function getOffloadName()
{
return $this->offloadName;
}
}
Offloader::getInstance(); // init

View File

@ -0,0 +1,57 @@
<?php
namespace ShortPixel\External\Offload;
use ShortPixel\Model\File\FileModel as FileModel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class VirtualFileSystem
{
protected $offloadName;
public function __construct($name)
{
$this->offloadName = $name;
$this->listen();
}
public function listen()
{
// $fs = \wpSPIO()->fileSystem()->startTrustedMode(); // @todo check if this works trusted mode forever.
add_filter('shortpixel/image/urltopath', array($this, 'checkIfOffloaded'), 10,2);
add_filter('shortpixel/file/virtual/translate', array($this, 'getLocalPathByURL'));
add_filter('shortpixel/file/virtual/heavy_features', array($this, 'extraFeatures'), 10);
}
public function checkIfOffloaded($bool, $url)
{
// Slow as it is, check nothing.
if ($offloadName = 's3-uploads-human')
{
return FileModel::$VIRTUAL_STATELESS;
}
if (file_exists($url))
{
return FileModel::$VIRTUAL_STATELESS;
}
return false;
}
public function getLocalPathByURL($path)
{
return $path;
}
// Features like addUNlisted and retina's ( check outside the WP metadata realm ) add a lot of extra time to stateless / remote filesystems. Disable by default to prevent pages from not loading.
public function extraFeatures()
{
return false;
}
} // class

View File

@ -0,0 +1,690 @@
<?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;
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Pantheon {
public static $is_pantheon = false;
public function __construct()
{
add_action( 'shortpixel/image/optimised', array( $this, 'flush_image_caches' ), 10 );
if (! defined('SHORTPIXEL_TRUSTED_MODE'))
{
define('SHORTPIXEL_TRUSTED_MODE', true);
}
self::$is_pantheon = true;
}
public static function IsActive()
{
return self::$is_pantheon;
}
public function flush_image_caches( $imageItem )
{
$image_paths[] = $imageItem->getURL();
if ($imageItem->hasOriginal())
{
$image_paths[] = $imageItem->getOriginalFile()->getURL();
}
if (count($imageItem->get('thumbnails')) > 0)
{
foreach($imageItem->get('thumbnails') as $thumbObj)
{
$image_paths[] = $thumbObj->getURL();
}
}
$domain = get_site_url();
$image_paths = array_map(function($path) use ($domain)
{
return str_replace( $domain, '', $path);
},$image_paths);
if ( ! empty( $image_paths ) ) {
$image_paths = array_unique( $image_paths );
if ( function_exists( 'pantheon_wp_clear_edge_paths' ) ) {
// Do the flush
pantheon_wp_clear_edge_paths( $image_paths );
}
}
}
} // class
if ( ! empty( $_ENV['PANTHEON_ENVIRONMENT'] ) ) {
$p = new Pantheon(); // monitor hook.
}

View File

@ -0,0 +1,33 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class QueryMonitor
{
public function __construct()
{
if (false === \wpSPIO()->env()->is_debug)
return;
$this->hooks();
}
public function hooks()
{
add_action('qm/output/after', array($this, 'panelEnd'), 10, 2);
}
public function panelEnd($qmObj, $outputters)
{
}
}
$qm = new QueryMonitor();

View File

@ -0,0 +1,44 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
class Spai
{
public function __construct()
{
add_action('plugins_loaded', array($this, 'addHooks'));
}
public function addHooks()
{
if (\wpSPIO()->env()->plugin_active('spai'))
{
// Prevent SPAI doing its stuff to our JSON returns.
$hook_upon = array('shortpixel_image_processing', 'shortpixel_ajaxRequest');
if (wp_doing_ajax() &&
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
isset($_REQUEST['action']) &&
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
in_array($_REQUEST['action'], $hook_upon) )
{
$this->preventCache();
}
}
}
public function preventCache()
{
if (! defined('DONOTCDN'))
{
define('DONOTCDN', true);
}
}
}
$s = new Spai();

View File

@ -0,0 +1,47 @@
<?php
namespace ShortPixel\External\Themes;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
class TotalTheme
{
public function __construct()
{
// do_action( 'totaltheme/resize-image/after_save_image', $attachment, $intermediate_size );
add_action( 'totaltheme/resize-image/after_save_image', array($this, 'resizeImage'), 10, 2);
}
public function resizeImage($attachment_id, $size)
{
$image = \wpSPIO()->filesystem()->getMediaImage($attachment_id);
if (! is_object($image))
{
return;
}
$changes = false;
$thumbObj = $image->getThumbnail($size);
if (is_object($thumbObj))
{
$thumbObj->onDelete(true);
$changes = true;
}
else {
}
if ( true === $changes)
{
$image->saveMeta();
}
}
} // class
$t = new TotalTheme();

View File

@ -0,0 +1,48 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
class UncodeController
{
function __construct()
{
$this->addHooks();
}
protected function addHooks()
{
add_action('uncode_delete_crop_image', array($this, 'removedMetaData'), 10, 2);
}
public function removedMetaData($attach_id, $filePath)
{
$fs = \wpSPIO()->filesystem();
$imageObj = $fs->getImage($attach_id, 'media', false);
$imageObj->saveMeta();
$fileObj = $fs->getFile($filePath);
if ($fileObj->hasBackup())
{
$backupObj = $fileObj->getBackupFile();
$backupObj->delete();
}
// Check Webp
$webpObj = $fs->getFile( (string) $fileObj->getFileDir() . $fileObj->getFileBase() . '.webp');
if ($webpObj->exists())
$webpObj->delete();
// Check Avif
$avifObj = $fs->getFile( (string) $fileObj->getFileDir() . $fileObj->getFileBase() . '.avif');
if ($avifObj->exists())
$avifObj->delete();
}
}
$u = new UncodeController();

View File

@ -0,0 +1,27 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// Visual Composer and compat class.
class visualComp
{
public function __construct()
{
add_filter('shortpixel/init/automedialibrary', array($this, 'check_vcinline'));
}
// autolibrary should not do things when VC is being inline somewhere.
public function check_vcinline($bool)
{
if ( function_exists( 'vc_action' ) && vc_action() == 'vc_inline' )
return false;
else
return $bool;
}
} // Class
$vc = new visualComp();

View File

@ -0,0 +1,633 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Controller\OptimizeController as OptimizeController;
use ShortPixel\Controller\BulkController as BulkController;
use ShortPixel\Controller\Queue\Queue as Queue;
use ShortPixel\Controller\ApiController as ApiController;
use ShortPixel\Controller\ResponseController as ResponseController;
use ShortPixel\Helper\UiHelper as UiHelper;
class WpCliController
{
public static $instance;
protected static $ticks = 0;
protected static $emptyq = 0;
public function __construct()
{
$log = \ShortPixel\ShortPixelLogger\ShortPixelLogger::getInstance();
if (\ShortPixel\ShortPixelLogger\ShortPixelLogger::debugIsActive())
$log->setLogPath(SHORTPIXEL_BACKUP_FOLDER . "/shortpixel_log_wpcli");
$this->initCommands();
}
public static function getInstance()
{
if (is_null(self::$instance))
self::$instance = new WpCliController();
return self::$instance;
}
protected function initCommands()
{
\WP_CLI::add_command('spio', '\ShortPixel\SpioSingle');
\WP_CLI::add_command('spio bulk', '\ShortPixel\SpioBulk');
}
} // class WpCliController
/**
* ShortPixel Image Optimizer
*
*
*/
class SpioCommandBase
{
protected static $runs = 0;
protected $last_combinedStatus;
/**
* Adds a single item to the queue(s), then processes the queue(s).
*
* ## OPTIONS
*
* <id>
* : Media Library ID or Custom Media ID
*
*
* [--type=<type>]
* : media or custom
* ---
* default: media
* options:
* - media
* - custom
* ---
*
* [--halt]
* : Stops (does not process the queues) after the item is added.
*
*
* ## EXAMPLES
*
* wp spio [bulk] add 123
* wp spio [bulk] add 21 --type=custom --halt
*
* @when after_wp_load
*/
public function add($args, $assoc)
{
$controller = $this->getOptimizeController();
$type = isset($assoc['type']) ? sanitize_text_field($assoc['type']) : 'media';
if (! isset($args[0]))
{
\WP_CLI::Error(__('Specify an Media Library Item ID', 'shortpixel-image-optimiser'));
return;
}
$id = intval($args[0]);
$fs = \wpSPIO()->filesystem();
$imageObj = $fs->getImage($id, $type);
if ($imageObj === false)
{
\WP_CLI::Error(__('Image object not found / non-existing in database by this ID', 'shortpixel-image-optimiser'));
}
$result = $controller->addItemtoQueue($imageObj);
// $complete = isset($assoc['complete']) ? true : false;
if ($result->status == 1)
{
\WP_CLI::Success($result->result->message);
if (! isset($assoc['halt']))
{
$this->run($args, $assoc);
}
else {
\WP_CLI::Line (__('You can optimize images via the run command', 'shortpixel-image-optimiser'));
}
}
elseif ($result->status == 0)
{
\WP_CLI::Error(sprintf(__("while adding item: %s", 'shortpixel_image_optimiser'), $result->result->message) );
}
$this->status($args, $assoc);
}
/**
* Starts processing what has been added to the processing queue(s), optionally stopping after a specified number of "ticks".
*
* A tick (or cycle) means a request sent to the API, either to send an image to be processed or to check if the API has completed processing. Use the ticks (cycles) if you want to run the script regularly (every few minutes) want to run the script.
*
* If you do not define ticks, the queue will run until everything has been processed.
*
* ## OPTIONS
*
* [--ticks=<number>]
* : How often the queue runs (how many ticks/cycles)
* ---
*
* [--wait=<seconds>]
* : How many seconds the system waits for next tick (cycle).
* ---
* default: 3
* ---
*
* [--queue=<name>]
* : Either 'media' or 'custom'. Omit the parameter to run both queues.
* ---
* default: media,custom
* ---
* options:
* - media
* - custom
* ---
*
* ## EXAMPLES
*
* wp spio [bulk] run | Complete all processes
* wp spio [bulk] run --ticks=20 --wait=3 | Ticks and wait time.
* wp spio [bulk] run --queue=media | Only run a specific queue.
*
*
* @when after_wp_load
*/
public function run($args, $assoc)
{
if ( isset($assoc['ticks']))
$ticks = intval($assoc['ticks']);
if (isset($assoc['wait']))
$wait = intval($assoc['wait']);
else
$wait = 3;
// Prepare limit
if (isset($assoc['limit']))
{
$limit = intval($assoc['limit']);
}
else {
$limit = false;
}
$complete = false;
if (! isset($assoc['ticks']))
{
$ticks = -1;
$complete = true; // run until all is done.
}
$queue = $this->getQueueArgument($assoc);
while($ticks > 0 || $complete == true)
{
$bool = $this->runClick($queue);
if ($bool === false)
{
$this->status($args, $assoc);
break;
}
if (false !== $limit)
{
$status = $this->getStatus();
$total = $this->unFormatNumber($status->total->stats->total);
$is_preparing = $status->total->stats->is_preparing;
if ($total >= $limit && $is_preparing)
{
\WP_CLI::log(sprintf('Bulk Preparing is done. Limit reached of %s items (%s items). Use start command to signal ready. Use run to process after starting.', $limit, $status->total->stats->total));
$this->status($args, $assoc);
$bool = false;
break;
}
}
$ticks--;
if (ob_get_length() !== false)
{
ob_flush();
}
sleep($wait);
}
// Done.
$this->showResponses();
}
protected function runClick($queueTypes)
{
ResponseController::setOutput(ResponseController::OUTPUT_CLI);
$controller = $this->getOptimizeController();
$results = $controller->processQueue($queueTypes);
$totalStats = (property_exists($results, 'total') && property_exists($results->total, 'stats')) ? $results->total->stats : null;
// Trouble
if (is_object($results) && property_exists($results, 'status') && $results->status === false)
{
\WP_CLI::error($results->message);
}
foreach($queueTypes as $qname)
{
$qresult = $results->$qname; // qname really is type.
if (! is_null($qresult->message))
{
// Queue Empty not interesting for CLI.
if ($qresult->qstatus == Queue::RESULT_QUEUE_EMPTY || $qresult->qstatus == Queue::RESULT_EMPTY)
{
}
// Result / Results have more interesting information than how much was fetched here probably.
elseif (! property_exists($qresult, 'result') && ! property_exists($qresult, 'results'))
{
\WP_CLI::log( ucfirst($qname) . ' : ' . $qresult->message); // Single Response ( ie prepared, enqueued etc )
}
}
// Result after optimizing items and such.
if (property_exists($qresult, 'results') && is_array($qresult->results))
{
foreach($qresult->results as $item)
{
// Non-result results can happen ( ie. with PNG conversion ). Probably just ignore.
if (false === property_exists($item, 'result') || ! is_object($item->result))
{
continue;
}
$result = (true === property_exists($item, 'result')) ? $item->result : null;
$counts = (true === property_exists($item, 'counts')) ? $item->counts : null;
$apiStatus = property_exists($result, 'apiStatus') ? $result->apiStatus : null;
$this->displayResult($result, $qname, $counts);
// prevent spamming.
if (! is_null($totalStats) && $apiStatus == ApiController::STATUS_SUCCESS )
{
$this->displayStatsLine('Total', $totalStats);
}
}
}
if (property_exists($qresult, 'result') && is_object($qresult->result))
{
$this->displayResult($qresult->result);
}
}
// Combined Status. Implememented from shortpixel-processor.js
$mediaStatus = $customStatus = 100;
if (property_exists($results, 'media') && property_exists($results->media, 'qstatus') )
{
$mediaStatus = $results->media->qstatus;
}
if (property_exists($results, 'custom') && property_exists($results->custom, 'qstatus') )
{
$customStatus = $results->custom->qstatus;
}
// The lowest queue status (for now) equals earlier in process. Don't halt until both are done.
if ($mediaStatus <= $customStatus)
$combinedStatus = $mediaStatus;
else
$combinedStatus = $customStatus;
if ($combinedStatus == Queue::RESULT_QUEUE_EMPTY)
{
\WP_CLI::log('All Queues report processing has finished');
return false;
}
elseif($combinedStatus == Queue::RESULT_PREPARING_DONE)
{
\WP_CLI::log(sprintf('Bulk Preparing is done. %s items. Use start command to signal ready. Use run to process after starting.', $results->total->stats->total));
return false;
}
$this->last_combinedStatus = $combinedStatus;
return true;
}
// Function for Showing JSON output of Optimizer regarding the process.
protected function displayResult($result, $type, $counts = null)
{
$apiStatus = property_exists($result, 'apiStatus') ? $result->apiStatus : null;
if ($apiStatus === ApiController::STATUS_SUCCESS)
{
\WP_CLI::line(' ');
\WP_CLI::line('---------------------------------------');
\WP_CLI::line(' ');
\WP_CLI::line(' ' . $result->message); // testing
if (property_exists($result, 'improvements'))
{
$outputTable = array();
$improvements = $result->improvements;
if (isset($improvements['main']))
{
$outputTable[] = array('name' => 'main', 'improvement' => $improvements['main'][0] .'%');
}
// \WP_CLI::Success( sprintf(__('Image optimized by %d %% ', 'shortpixel-image-optimiser'), $improvements['main'][0]));
if (isset($improvements['thumbnails']))
{
foreach($improvements['thumbnails'] as $thumbName => $optData)
{
$outputTable[] = array('name' => $thumbName, 'improvement' => $optData[0] . '%');
}
}
$outputTable[] = array('name' => ' ', 'improvement' => ' ');
$outputTable[] = array('name' => __('Total', 'shortpixel-image-optimiser'), 'improvement' => $improvements['totalpercentage']. '%');
\WP_CLI\Utils\format_items('table', $outputTable, array('name', 'improvement'));
if (! is_null($counts))
{
$baseMsg = sprintf(' This job, %d credit(s) were used. %d for images ', $counts->creditCount,
$counts->baseCount);
if ($counts->webpCount > 0)
$baseMsg .= sprintf(', %d for webps ', $counts->webpCount);
if ( $counts->avifCount > 0)
$baseMsg .= sprintf(', %d for avifs ', $counts->avifCount);
\WP_CLI::line($baseMsg);
}
\WP_CLI::line(' ');
\WP_CLI::line('---------------------------------------');
\WP_CLI::line(' ');
}
} // success
elseif ($apiStatus === ApiController::STATUS_NOT_API)
{
$message = property_exists($result, 'message') ? $result->message : '';
\WP_CLI::line($message);
}
else
{
if ($result->is_error)
{
\WP_CLI::error($result->message, false);
}
else {
\WP_CLI::line($result->message);
}
}
}
protected function displayStatsLine($name, $stats)
{
$line = sprintf('Current Status for %s : (%s\%s) Done (%s%%), %s awaiting %s errors --', $name, ($stats->done + $stats->fatal_errors), $stats->total, $stats->percentage_done, ( $stats->awaiting ), $stats->fatal_errors);
\WP_CLI::line($line);
}
/**
* Displays the current status of the processing queue(s)
*
* [--show-debug]
* : Dumps more information for debugging purposes
* ---
*
* ## EXAMPLES
*
* wp spio [bulk] status [--show-debug]
*
*/
public function status($args, $assoc)
{
$queue = $this->getQueueArgument($assoc);
$startupData = $this->getStatus();
$items = array();
$fields = array('queue name', 'in queue', 'in process', 'fatal errors', 'done', 'total', 'preparing', 'running', 'finished');
foreach($queue as $queue_name)
{
//$Q = $optimizeController->getQueue($queue_name);
$stats = $startupData->$queue_name->stats;
$item = array(
'queue name' => $queue_name,
'in queue' => $stats->in_queue,
'in process' => $stats->in_process,
'fatal errors' => $stats->fatal_errors,
'done' => $stats->done,
'total' => $stats->total,
'preparing' => ($stats->is_preparing) ? __('Yes', 'shortpixel-image-optimiser') : __('No', 'shortpixel-image-optimiser'),
'running' => ($stats->is_running) ? __('Yes', 'shortpixel-image-optimiser') : __('No', 'shortpixel-image-optimiser'),
'finished' => ($stats->is_finished) ? __('Yes', 'shortpixel-image-optimiser') : __('No', 'shortpixel-image-optimiser'),
);
$items[] = $item;
if (isset($assoc['show-debug']))
{
print_r($stats);
}
}
\WP_CLI::Line("--- Current Status ---");
\WP_CLI\Utils\format_items('table', $items, $fields);
\WP_CLI::Line($this->displayStatsLine('Total', $startupData->total->stats));
}
/**
* Displays the key settings that are applied when executing commands with WP-CLI.
*
*
* ---
*
* ## EXAMPLES
*
* wp spio [bulk] settings
*
*/
public function settings()
{
$settings = \WPspio()->settings();
$items = array();
$fields = array('setting', 'value');
$items[] = array('setting' => 'Compression', 'value' => UiHelper::compressionTypeToText($settings->compressionType));
$items[] = array('setting' => 'Image Backup', 'value' => $this->textBoolean($settings->backupImages, true));
$items[] = array('setting' => 'Processed Thumbnails', 'value' => $this->textBoolean($settings->processThumbnails, true));
$items[] = array('setting' => ' ', 'value' => ' ');
$items[] = array('setting' => 'Creates Webp', 'value' => $this->textBoolean($settings->createWebp));
$items[] = array('setting' => 'Creates Avif', 'value' => $this->textBoolean($settings->createAvif));
\WP_CLI\Utils\format_items('table', $items, $fields);
}
/**
* Clears the Queue(s)
*
*
* [--queue=<name>]
* : Either 'media' or 'custom'. Omit the parameter to clear both queues.
* ---
* default: media,custom
* options:
* - media
* - custom
*
* ## EXAMPLES
*
* wp spio [bulk] clear
*/
public function clear($args, $assoc)
{
$queues = $this->getQueueArgument($assoc);
$optimizeController = $this->getOptimizeController();
foreach($queues as $type)
{
$queue = $optimizeController->getQueue($type);
$queue->resetQueue();
}
\WP_CLI::Success(__('Queue(s) cleared', 'shortpixel-image-optimiser'));
}
// Colored is buggy, so off for now -> https://github.com/wp-cli/php-cli-tools/issues/134
private function textBoolean($bool, $colored = false)
{
$colored = false;
$values = array('','');
if ($bool)
{
if ($colored)
{
$values = array('%g', '%n');
}
$res = vsprintf(__('%sYes%s', 'shortpixel-image-optimiser'), $values);
if ($colored)
$res = \WP_CLI::colorize($res);
}
else
{
if ($colored)
{
$values = array('%r', '');
}
$res = vsprintf(__('%sNo%s', 'shortpixel-image-optimiser'), $values);
if ($colored)
$res = \WP_CLI::colorize($res);
}
return $res;
}
protected function getStatus()
{
$optimizeController = $this->getOptimizeController();
$startupData = $optimizeController->getStartupData();
return $startupData;
}
protected function showResponses()
{
return false; // @todo Pending responseControl, offf.
/*$responses = ResponseController::getAll();
foreach ($responses as $response)
{
if ($response->is('error'))
\WP_CLI::Error($response->message, false);
elseif ($response->is('success'))
\WP_CLI::Success($response->message);
else
\WP_CLI::line($response->message);
} */
}
protected function getQueueArgument($assoc)
{
if (isset($assoc['queue']))
{
if (strpos($assoc['queue'], ',') !== false)
{
$queue = explode(',', $assoc['queue']);
$queue = array_map('sanitize_text_field', $queue);
}
else
$queue = array(sanitize_text_field($assoc['queue']));
}
else
$queue = array('media', 'custom');
return $queue;
}
// To ensure the bulk switch is ok.
protected function getOptimizeController()
{
$optimizeController = new OptimizeController();
return $optimizeController;
}
private function unFormatNumber($string)
{
$string = str_replace(',', '', $string);
$string = str_replace('.', '', $string);
return $string;
}
} // Class SpioCommandBase

View File

@ -0,0 +1,296 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Controller\OptimizeController as OptimizeController;
use ShortPixel\Controller\BulkController as BulkController;
use ShortPixel\Controller\Queue\Queue as Queue;
use ShortPixel\Controller\ApiController as ApiController;
use ShortPixel\Controller\ResponseController as ResponseController;
/**
* Actions for running bulk operations from WP-CLI
*/
class SpioBulk extends SpioCommandBase
{
/**
* Starts the prepared queue(s). The bulk needs an express command to start processing.
* Once started, the queue(s) can be processed and finished with the run command.
*
* ## OPTIONS
* [--queue=<name>]
* : Either 'media' or 'custom'. Omit the parameter to start both queues.
* ---
* default: media,custom
* options:
* - media
* - custom
* ---
*
* ## EXAMPLES
*
* wp spio bulk start
*
*
* @when after_wp_load
*/
public function start($args, $assoc)
{
$bulkControl = BulkController::getInstance();
$queue = $this->getQueueArgument($assoc);
foreach($queue as $qname)
{
$result = $bulkControl->startBulk($qname);
}
\WP_CLI::Line('Start signal for Bulk Processing given.');
// $this->run($args, $assoc);
//$controller = new OptimizeController();
//$result = $controller->startBulk();
}
/**
* Automatically Bulk Processes everything that needs to be done.
*
* [--queue=<name>]
* : Either 'media' or 'custom'. Omit the parameter to process both queues.
* ---
* default: media,custom
* options:
* - media
* - custom
* ---
*
* [--limit=<num>]
* : Limit the amount of items being prepared.
*
* [--special=<migrate>]
* : Run the migration
*
* ## EXAMPLES
*
* wp spio bulk auto
*
*
*/
public function auto($args, $assoc)
{
$queue = $this->getQueueArgument($assoc);
$optimizeController = $this->getOptimizeController();
$bulkControl = BulkController::getInstance();
$running = true;
$created = false;
$this->settings();
sleep(2); // user can digest settings
while($running)
{
$data = $optimizeController->getStartupData();
$combined = $data->total->stats;
// Is_finished is no queue running.
if ($combined->is_preparing)
{
\WP_CLI::line('[Auto Bulk] Preparing .. ');
$this->prepare($args, $assoc);
$this->start($args, $assoc);
\WP_CLI::line('Preparing Run done');
}
elseif ($combined->is_running)
{
\WP_CLI::line('Bulk Running ...');
$this->run($args, $assoc); // Run All
}
elseif ($combined->total > 0 && $combined->done == 0 && $combined->is_running == false && $combined->is_preparing == false && $combined->is_finished == false)
{
\WP_CLI::line('[Auto Bulk] Starting to process');
$this->status($args, $assoc);
$this->start($args, $assoc);
}
elseif ($combined->is_finished)
{
if ($combined->done > 0 || $created == true) // means we already ran the whole thing once.
{
\WP_CLI::Line('[Auto Bulk] Seems finished and done running');
$running = false;
$this->finishBulk($args, $assoc);
break;
}
\WP_CLI::Line('[Auto Bulk] Creating New Queue');
$this->create($args, $assoc);
$created = true;
}
else
{
\WP_CLI::error("[Auto Bulk] : Encountered nothing to do", true);
$running = false; // extra fallback
}
}
\WP_CLI::log('Automatic Bulk ended');
}
/**
* Creates the queue(s) for bulk optimization of media library and/or custom media items.
*
* ## OPTIONS
*
* [--queue=<name>]
* : Either 'media' or 'custom'. Omit the parameter to create both queues.
* ---
* default: media,custom
* options:
* - media
* - custom
* [--special=<migrate>]
* : Run the migration
* ---
*
* ## EXAMPLES
*
* wp spio bulk create
*
*
* @when after_wp_load
*/
public function create($args, $assoc)
{
$bulkControl = BulkController::getInstance();
$json = new \stdClass;
$json->media = new \stdClass;
$json->custom = new \stdClass;
$queues = $this->getQueueArgument($assoc);
$operation = null;
if (isset($assoc['special']))
{
switch ($assoc['special'])
{
case 'migrate':
$operation = 'migrate';
$queues = array('media'); // can only have one bulk, this.
break;
}
}
foreach($queues as $qname)
{
$stats = $bulkControl->createNewBulk($qname, $operation);
$json->$qname->stats = $stats;
\WP_CLI::Line("Bulk $qname created. Ready to prepare");
}
$this->showResponses();
return $stats;
}
/**
* ## OPTIONS
*
* <start-id>
* : ID to start restore
*
* <end-id>
* : ID to stop restore
*
* [--type=<type>]
* : media or custom
* ---
* default: media
* options:
* - media
* - custom
* ---
*
* ## EXAMPLES
*
* wp spio bulk restore 0 100
*
*
* @when after_wp_load
*/
/*public function restore($args, $assoc)
{
\WP_CLI::Line('Not yet implemented');
} */
protected function finishBulk($args, $assoc)
{
$bulkControl = BulkController::getInstance();
$queues = $this->getQueueArgument($assoc);
foreach($queues as $queue_name)
{
$bulkControl->finishBulk($queue_name);
}
}
// To ensure the bulk switch is ok.
protected function getOptimizeController()
{
$optimizeController = new OptimizeController();
$optimizeController->setBulk(true);
return $optimizeController;
}
/**
* Prepares the items by adding them to the queue(s). It runs only when the queue is in the preparing phase and finishes when everything is prepared.
*
*
* [--queue=<name>]
* : Either 'media' or 'custom'. Omit the parameter to run both queues.
* ---
* default: media,custom
* options:
* - media
* - custom
* ---
*
* [--limit=<num>]
* : Limit the amount of items being prepared.
*
* ## EXAMPLES
*
* wp spio bulk prepare
*
*/
public function prepare($args, $assoc)
{
$queues = $this->getQueueArgument($assoc);
$optimizeController = $this->getOptimizeController();
$data = $optimizeController->getStartupData();
if (! $data->total->stats->is_preparing)
{
\WP_CLI::Error("No queues have status preparing, aborting");
}
else
{
$assoc['wait'] = 0.5;
$bool = $this->run($args, $assoc);
}
}
} // CLASS

View File

@ -0,0 +1,95 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Controller\OptimizeController as OptimizeController;
use ShortPixel\Controller\BulkController as BulkController;
use ShortPixel\Controller\Queue\Queue as Queue;
use ShortPixel\Controller\ApiController as ApiController;
use ShortPixel\Controller\ResponseController as ResponseController;
/**
* Actions and operations for the ShortPixel Image Optimizer plugin
*/
class SpioSingle extends SpioCommandBase
{
/**
* Restores the optimized item to its original state (if backups are active).
*
* ## OPTIONS
*
* <id>
* : Media Library ID or Custom Media ID
*
* [--type=<type>]
* : media | custom
* ---
* default: media
* options:
* - media
* - custom
* ---
*
* ## EXAMPLES
*
* wp spio restore 123
* wp spio restore 21 --type=custom
*
* @when after_wp_load
*/
public function restore($args, $assoc_args)
{
$controller = new OptimizeController();
$fs = \wpSPIO()->filesystem();
if (! isset($args[0]))
{
\WP_CLI::Error(__('Specify an (Media Library) Item ID', 'shortpixel_image_optimiser'));
return;
}
if (! is_numeric($args[0]))
{
\WP_CLI::Error(__('Item ID needs to be a number', 'shortpixel-image-optimiser'));
return;
}
$id = intval($args[0]);
$type = $assoc_args['type'];
$image = $fs->getImage($id, $type);
if ($image === false)
{
\WP_CLI::Error(__('No Image returned. Please check if the number and type are correct and the image exists', 'shortpixel-image-optimiser'));
return;
}
$result = $controller->restoreItem($image);
$this->showResponses();
if (property_exists($result,'message') && ! is_null($result->message) && strlen($result->message) > 0)
$message = $result->message;
elseif (property_exists($result, 'result') && property_exists($result->result, 'message'))
$message = $result->result->message;
if ($result->status == 1)
{
\WP_CLI::Success($message);
}
elseif ($result->status == 0)
{
\WP_CLI::Error(sprintf(__("Restoring Item: %s", 'shortpixel_image_optimiser'), $message) );
}
}
} // CLASS

View File

@ -0,0 +1 @@
{"name":"ShortPixel\/Plugin","description":"ShortPixel AutoLoader","type":"function","autoload":{"psr-4":{"ShortPixel":"class"},"files":["class\/wp-shortpixel-settings.php","class\/front\/img-to-picture-webp.php","class\/external\/cloudflare.php","class\/external\/nextgen\/nextGenController.php","class\/external\/nextgen\/nextGenViewController.php","class\/external\/visualcomposer.php","class\/external\/offload\/Offloader.php","class\/external\/offload\/wp-offload-media.php","class\/external\/offload\/virtual-filesystem.php","class\/external\/wp-cli\/wp-cli-base.php","class\/external\/wp-cli\/wp-cli-single.php","class\/external\/wp-cli\/wp-cli-bulk.php","class\/external\/image-galleries.php","class\/external\/pantheon.php","class\/external\/spai.php","class\/external\/cache.php","class\/external\/uncode.php","class\/external\/query-monitor.php","class\/external\/Woocommerce.php","class\/external\/themes\/total-theme.php"]}}

View File

@ -0,0 +1,120 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use \ShortPixel\Controller\BulkController as BulkController;
$bulk = BulkController::getInstance();
$queueRunning = $bulk->isAnyBulkRunning();
?>
<section class='panel bulk-restore' data-panel="bulk-restore" >
<h3 class='heading'>
<?php esc_html_e("Bulk Restore", 'shortpixel-image-optimiser'); ?>
</h3>
<div class='bulk-special-wrapper'>
<h4 class='warning'><?php esc_html_e('Warning', 'shortpixel-image-optimiser'); ?></h4>
<p><?php printf(esc_html__('By starting the %s bulk restore %s process, the plugin will try to restore %s all images %s to the original state. All images will become unoptimized.', 'shortpixel-image-optimiser'), '<b>', '</b>', '<b>', '</b>'); ?></p>
<p><?php printf(esc_html__('We recommend users to %s contact us %s before restoring the images - many times the restoring is not necessary and we can help. But if you choose to continue then we strongly recommend to create a full backup before starting the process.', 'shortpixel-image-optimiser'), '<b><a href="https://shortpixel.com/contact" target="_blank">', '</a></b>'); ?>
</p>
<p class='warning'><?php esc_html_e('It is strongly advised to create a full backup before starting this process.', 'shortpixel-image-optimiser'); ?></p>
<?php if ($this->view->approx->custom->has_custom === true) : ?>
<div class='optiongroup' data-check-visibility data-control="data-check-custom-hascustom">
<div class='switch_button'>
<label>
<input type="checkbox" class="switch" id="restore_media_checkbox" >
<div class="the_switch">&nbsp; </div>
</label>
</div>
<h4><label for="restore_media_checkbox"><?php esc_html_e('Restore media library','shortpixel-image-optimiser'); ?></label></h4>
</div>
<div class='optiongroup' data-check-visibility data-control="data-check-custom-hascustom">
<div class='switch_button'>
<label>
<input type="checkbox" class="switch" id="restore_custom_checkbox" value='1' >
<div class="the_switch">&nbsp; </div>
</label>
</div>
<h4><label for="restore_custom_checkbox"><?php esc_html_e('Restore custom media','shortpixel-image-optimiser'); ?></label></h4>
</div>
<?php endif ?>
<p class='optiongroup warning hidden' id="restore_media_warn"><?php esc_html_e('Please select one of the options', 'shortpixel-image-optimiser'); ?></p>
<p class='optiongroup' ><input type="checkbox" id="bulk-restore-agree" value="agree" data-action="ToggleButton" data-target="bulk-restore-button"> <?php esc_html_e('I want to restore all selected images. I understand this action is permanent and nonreversible', 'shortpixel-image-optimiser'); ?></p>
<nav>
<button type="button" class="button" data-action="open-panel" data-panel="dashboard"><?php esc_html_e('Back','shortpixel-image-optimiser'); ?></button>
<button type="button" class="button button-primary disabled" id='bulk-restore-button' data-action="BulkRestoreAll" disabled><?php esc_html_e('Bulk Restore All Images', 'shortpixel-image-optimiser') ?></button>
</nav>
</div>
</section>
<section class='panel bulk-migrate' data-panel="bulk-migrate" >
<h3 class='heading'>
<?php esc_html_e("Bulk Migrate", 'shortpixel-image-optimiser'); ?>
</h3>
<div class='bulk-special-wrapper'>
<h4 class='warning'><?php esc_html_e('Warning', 'shortpixel-image-optimiser'); ?></h4>
<p><?php printf(esc_html__('By starting the %s bulk metadata migration %s process, the plugin will try to migrate the old format of optimization information (used by the plugin for versions prior to 5.0) to the new format used from version 5.0 onward for %s all the images. %s It is possible to have exceptions and some of the image information migration may fail. You should get all the details for these cases at the end of the process, in the Errors section.', 'shortpixel-image-optimiser'), '<b>', '</b>', '<b>', '</b>'); ?></p>
<p class='warning optiongroup'><?php esc_html_e('It is strongly advised to create a full backup before starting this process.', 'shortpixel-image-optimiser'); ?></p>
<p class='optiongroup'><input type="checkbox" id="bulk-migrate-agree" value="agree" data-action="ToggleButton" data-target="bulk-migrate-button"> <?php esc_html_e('I want to migrate the metadata for all images. I understand this action is permanent. I made a backup of my site including images and database.', 'shortpixel-image-optimiser'); ?></p>
<nav>
<button class="button" type="button" data-action="open-panel" data-panel="dashboard"><?php esc_html_e('Back','shortpixel-image-optimiser'); ?></button>
<button type="button" type="button" class="button disabled button-primary" disabled id='bulk-migrate-button' data-action="BulkMigrateAll" ><?php esc_html_e('Search and migrate All Images', 'shortpixel-image-optimiser') ?>
</button>
</nav>
</div>
</section>
<section class='panel bulk-removeLegacy' data-panel="bulk-removeLegacy" >
<h3 class='heading'>
<?php esc_html_e("Bulk remove legacy data", 'shortpixel-image-optimiser'); ?>
</h3>
<div class='bulk-special-wrapper'>
<h4 class='warning'><?php esc_html_e('Warning', 'shortpixel-image-optimiser'); ?></h4>
<p><?php printf(esc_html__('By starting the %s remove legacy metadata %s process, the plugin will try to remove all the %s legacy data %s (that was used by the plugin to store the optimization information in versions earlier than 5.0). If this legacy metadata isn\'t properly migrated or some of the migration failed for any reason, it will be impossible to undo or redo the process. In these cases, the optimization information for images processed with versions earlier than 5.0 could be lost.', 'shortpixel-image-optimiser'), '<b>', '</b>', '<b>', '</b>'); ?></p>
<p class='warning optiongroup'><?php esc_html_e('It is strongly advised to create a full backup before starting this process.', 'shortpixel-image-optimiser'); ?></p>
<p class='optiongroup'><input type="checkbox" id="bulk-migrate-agree" value="agree" data-action="ToggleButton" data-target="bulk-removelegacy-button"> <?php esc_html_e('I want to remove all legacy data. I understand this action is permanent. I made a backup of my site including images and database.', 'shortpixel-image-optimiser'); ?></p>
<nav>
<button type="button" class="button" data-action="open-panel" data-panel="dashboard"><?php esc_html_e('Back','shortpixel-image-optimiser'); ?></button>
<button type="button" class="button disabled button-primary" disabled id='bulk-removelegacy-button' data-action="BulkRemoveLegacy" ><?php esc_html_e('Remove all legacy metadata', 'shortpixel-image-optimiser') ?></button>
</nav>
</div>
</section>

View File

@ -0,0 +1,120 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<section class='dashboard panel active' data-panel="dashboard" style='display: block' >
<div class="panel-container">
<h3 class="heading"><span><img src="<?php echo esc_url(\wpSPIO()->plugin_url('res/img/robo-slider.png')); ?>"></span>
<?php esc_html_e('Welcome to the Bulk Processing page!', 'shortpixel-image-optimiser'); ?>
</h3>
<div class='interface wrapper'>
<div class='bulk-wrapper'>
<button type="button" class="button-primary button" id="start-optimize" data-action="open-panel" data-panel="selection" <?php echo ($this->view->error) ? "disabled" : ''; ?> >
<span class='dashicons dashicons-controls-play'>&nbsp;</span>
<p><?php esc_html_e('Start optimizing','shortpixel-image-optimiser'); ?></p>
</button>
</div>
<div class='dashboard-text'>
<p class='description'><?php esc_html_e('Here you can (re)optimize your Media Library or Custom Media folders from your website.', 'shortpixel-image-optimiser'); ?></p>
<p class='description'><?php
printf(__('If you have any question don\'t hesitate to %s contact us %s %s, we are friendly and helpful, 24/7. %s
Also, if you have a minute please leave a %s review %s for us, it always brings joy to our team! %s','shortpixel-image-optimiser'),
'<a href="https://shortpixel.com/contact" target="_blank">',
'</a>',
'&#x1F4AC;',
'<br>',
'<a href="https://wordpress.org/support/plugin/shortpixel-image-optimiser/reviews/?filter=5" target="_blank">',
'</a>',
'&#x1F913');
?>
</p>
</div>
</div>
<?php if ($this->view->error): ?>
<div class='bulk error'>
<h3><?php echo esc_html($this->view->errorTitle); ?></h3>
<p><?php echo $this->view->errorContent; ?></p>
<?php if (property_exists($this->view, 'errorText')): ?>
<p class='text'><?php echo esc_html($this->view->errorText) ?></p>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (count($this->view->logs) > 0): ?>
<div id="LogModal" class="shortpixel-modal shortpixel-hide bulk-modal">
<span class="close" data-action="CloseModal" data-id="LogModal">X</span>
<div class='title'>
</div>
<div class="content sptw-modal-spinner">
<div class='table-wrapper'>
</div>
</div>
</div>
<div id="LogModal-Shade" class='sp-modal-shade'></div>
<div class='dashboard-log'>
<h3><?php esc_html_e('Previous Bulks', 'shortpixel_image_optimizer'); ?></h3>
<?php
echo "<div class='head'>";
foreach($this->view->logHeaders as $header)
{
echo "<span>" . esc_attr($header) . "</span>";
}
echo "</div>";
foreach ($this->view->logs as $logItem):
{
echo "<div class='data " . esc_attr($logItem['type']) . "'>";
echo "<span>" . esc_html($logItem['images']) . '</span>';
echo "<span>" . $logItem['errors'] . '</span>';
echo '<span class="checkmark_green date">' . sprintf(esc_html__('%sCompleted%s on %s','shortpixel-image-optimiser'), '<b>','</b>', esc_html($logItem['date'])) . '</span>';
echo "<span>" . esc_html($logItem['bulkName']) . '</span>';
echo "</div>";
}
?>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (! $this->view->error): ?>
<div class='shortpixel-bulk-loader' id="bulk-loading" data-status='loading'>
<div class='loader'>
<span class="svg-spinner"><?php $this->loadView('snippets/part-svgloader', false); ?></span>
<span>
<h2><?php esc_html_e('Please wait, ShortPixel is loading'); ?></h2>
</span>
</div>
</div>
<?php endif; ?>
</div> <!-- panel-container -->
</section> <!-- section -->

View File

@ -0,0 +1,140 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<section class="panel finished" data-panel="finished">
<div class="panel-container">
<h3 class="heading"><span><img src="<?php echo \wpSPIO()->plugin_url('res/img/robo-slider.png'); ?>"></span>
<?php esc_html_e('The ShortPixel Bulk Processing is finished' ,'shortpixel-image-optimiser'); ?>
<div class='average-optimization'>
<p><?php esc_html_e('Average Optimization','shortpixel-image-optimiser'); ?></p>
<svg class="opt-circle-average" viewBox="-10 0 150 140">
<path class="trail" d="
M 50,50
m 0,-46
a 46,46 0 1 1 0,92
a 46,46 0 1 1 0,-92
" stroke-width="16" fill-opacity="0">
</path>
<path class="path" d="
M 50,50
m 0,-46
a 46,46 0 1 1 0,92
a 46,46 0 1 1 0,-92
" stroke-width="16" fill-opacity="0" style="stroke-dasharray: 289.027px, 289.027px; stroke-dashoffset: 180px;">
</path>
<text class="text" x="50" y="50"><?php esc_html_e('N/A', 'shortpixel-image-optimiser'); ?></text>
</svg>
</div>
</h3>
<?php $this->loadView('bulk/part-progressbar', false); ?>
<span class='hidden' data-check-media-total data-stats-media="total">0</span>
<span class='hidden' data-check-media-customOperation data-stats-media="isCustomOperation">-1</span>
<div class='bulk-summary' data-check-visibility="false" data-control='data-check-media-customOperation'>
<p class='finished-paragraph'>
<?php printf(__('Congratulations, ShortPixel has optimized %s %s images and thumbs %s for your website! Yay to faster loading websites! %s', 'shortpixel-image-optimiser'), '<b>', '<span data-stats-total="total"></span>','</b>', '&#x1F389;');
?>
<br>
<?php
printf(__('ShortPixel plugins are installed on hundreds of thousands of websites and we save our users over 500 GB by optimizing over 15 million images. Each and every day! %s', 'shortpixel-image-optimiser'), '&#x1F4AA;');
?>
<br>
<?php
printf(__('We have been working on improving ShortPixel every day for over 7 years. It is very motivating for us when customers take a minute to leave us a %sreview%s. We thank you for that! %s', 'shortpixel-image-optimiser'), '<a href="https://wordpress.org/support/plugin/shortpixel-image-optimiser/reviews/?filter=5" target="_blank">','</a>', '&#x1F64C;');
?>
</p>
</div>
<div class='bulk-summary' data-check-visibility data-control="data-check-media-total">
<div class='heading'>
<span><i class='dashicons dashicons-images-alt2'>&nbsp;</i> <?php esc_html_e('Media Library','shortpixel-image-optimiser'); ?></span>
<span>
<span class='line-progressbar'>
<span class='done-text'><i data-stats-media="percentage_done"></i> %</span>
<span class='done' data-stats-media="percentage_done" data-presentation="css.width.percentage"></span>
</span>
</span>
<span><?php esc_html_e('Processing','shortpixel-image-optimiser') ?>: <i data-stats-media="in_process">0</i></span>
</div>
<div>
<span><?php esc_html_e('Processed','shortpixel-image-optimiser'); ?>: <i data-stats-media="done">0</i></span>
<span><?php esc_html_e('Images Left','shortpixel-image-optimiser'); ?>: <i data-stats-media="in_queue">0</i></span>
<span><?php esc_html_e('Errors','shortpixel-image-optimiser'); ?>: <i data-check-media-fatalerrors data-stats-media="fatal_errors" class='error'>0 </i>
<span class="display-error-box" data-check-visibility data-control="data-check-media-fatalerrors" ><label title="<?php esc_html_e('Show Errors', 'shortpixel-image-optimiser'); ?>">
<input type="checkbox" name="show-errors" value="show" data-action='ToggleErrorBox' data-errorbox='media' data-event='change'><?php esc_html_e('Show Errors','shortpixel-image-optimiser'); ?></label>
</span>
</span>
</div>
</div>
<div data-error-media="message" data-presentation="append" class='errorbox media'>
<?php if(property_exists($this->view, 'mediaErrorLog') && $this->view->mediaErrorLog !== false)
{
echo $this->view->mediaErrorLog;
}
?>
</div>
<!-- ****** CUSTOM ******** --->
<span class='hidden' data-check-custom-total data-stats-custom="total">0</span>
<div class='bulk-summary' data-check-visibility data-control="data-check-custom-total">
<div class='heading'>
<span><i class='dashicons dashicons-open-folder'>&nbsp;</i> <?php esc_html_e('Custom Media','shortpixel-image-optimiser'); ?></span>
<span>
<span class='line-progressbar'>
<span class='done-text'><i data-stats-custom="percentage_done"></i> %</span>
<span class='done' data-stats-custom="percentage_done" data-presentation="css.width.percentage"></span>
</span>
</span>
<span><?php esc_html_e('Processing','shortpixel-image-optimiser') ?>: <i data-stats-custom="in_process">-</i></span>
</div>
<div>
<span><?php esc_html_e('Processed','shortpixel-image-optimiser'); ?>: <i data-stats-custom="done">-</i></span>
<span><?php esc_html_e('Images Left', 'shortpixel-image-optimiser') ?>: <i data-stats-custom="in_queue">-</i></span>
<span><?php esc_html_e('Errors','shortpixel-image-optimiser') ?>: <i data-check-custom-fatalerrors data-stats-custom="fatal_errors" class='error'>-</i>
<span class="display-error-box" data-check-visibility data-control="data-check-custom-fatalerrors" ><label title="<?php esc_html_e('Show Errors', 'shortpixel-image-optimiser'); ?>">
<input type="checkbox" name="show-errors" value="show" data-action='ToggleErrorBox' data-errorbox='custom' data-event='change'>Show Errors</label>
</span>
</span>
</div>
</div>
<div data-error-custom="message" data-presentation="append" class='errorbox custom'>
<?php if(property_exists($this->view, 'customErrorLog') && $this->view->customErrorLog !== false)
{
echo $this->view->customErrorLog;
}
?>
</div>
<nav>
<button class='button finish' type="button" data-action="FinishBulk" id="FinishBulkButton"><?php esc_html_e('Finish Bulk Process','shortpixel-image-optimiser'); ?></button>
</nav>
</div>
</section>

View File

@ -0,0 +1,207 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<section class="panel process" data-panel="process" >
<div class="panel-container">
<h3 class="heading"><span><img src="<?php echo esc_url(\wpSPIO()->plugin_url('res/img/robo-slider.png')); ?>"></span>
<?php esc_html_e('ShortPixel Bulk Process is in progress','shortpixel-image-optimiser'); ?>
<div class='average-optimization'>
<p><?php esc_html_e('Average this run','shortpixel-image-optimiser'); ?></p>
<svg class="opt-circle-average" viewBox="-10 0 150 140">
<path class="trail" d="
M 50,50
m 0,-46
a 46,46 0 1 1 0,92
a 46,46 0 1 1 0,-92
" stroke-width="16" fill-opacity="0">
</path>
<path class="path" d="
M 50,50
m 0,-46
a 46,46 0 1 1 0,92
a 46,46 0 1 1 0,-92
" stroke-width="16" fill-opacity="0" style="stroke-dasharray: 289.027px, 289.027px; stroke-dashoffset: 180px;">
</path>
<text class="text" x="50" y="50"><?php esc_html_e('N/A', 'shortpixel-image-optimiser'); ?></text>
</svg>
</div>
</h3>
<p class='description'><?php esc_html_e('ShortPixel is processing your images. Please keep this window open to complete the process.', 'shortpixel-image-optimiser'); ?> </p>
<?php $this->loadView('bulk/part-progressbar', false); ?>
<!--- ###### MEDIA ###### -->
<span class='hidden' data-check-media-total data-stats-media="total">0</span>
<div class='bulk-summary' data-check-visibility data-control="data-check-media-total">
<div class='heading'>
<span><i class='dashicons dashicons-images-alt2'>&nbsp;</i> <?php esc_html_e('Media Library' ,'shortpixel-image-optimiser'); ?></span>
<span>
<span class='line-progressbar'>
<span class='done-text'><i data-stats-media="percentage_done"></i> %</span>
<span class='done' data-stats-media="percentage_done" data-presentation="css.width.percentage"></span>
</span>
<span class='dashicons spin dashicons-update line-progressbar-spinner' data-check-visibility data-control="data-check-media-in_process">&nbsp;</span>
</span>
<span><?php esc_html_e('Processing', 'shortpixel-image-optimiser') ?>: <i data-stats-media="in_process" data-check-media-in_process >0</i></span>
</div>
<div>
<span><?php esc_html_e('Processed', 'shortpixel-image-optimiser'); ?>: <i data-stats-media="done">0</i></span>
<span><?php esc_html_e('Waiting','shortpixel-image-optimiser'); ?>: <i data-stats-media="in_queue">0</i></span>
<span><?php esc_html_e('Errors','shortpixel-image-optimiser') ?>: <i data-check-media-fatalerrors data-stats-media="fatal_errors" class='error'>0 </i>
<span class="display-error-box" data-check-visibility data-control="data-check-media-fatalerrors" ><label title="<?php esc_html_e('Show Errors', 'shortpixel-image-optimiser'); ?>">
<input type="checkbox" name="show-errors" value="show" data-action='ToggleErrorBox' data-errorbox='media' data-event='change'>
<?php esc_html_e('Show Errors','shortpixel-image-optimiser'); ?></label>
</span>
</span>
</div>
</div>
<div data-error-media="message" data-presentation="append" class='errorbox media'>
<?php if(property_exists($this->view, 'mediaErrorLog') && $this->view->mediaErrorLog !== false)
{
echo $this->view->mediaErrorLog;
}
?>
</div>
<!-- ****** CUSTOM ******** --->
<span class='hidden' data-check-custom-total data-stats-custom="total">0</span>
<div class='bulk-summary' data-check-visibility data-control="data-check-custom-total">
<div class='heading'>
<span><i class='dashicons dashicons-open-folder'>&nbsp;</i> <?php esc_html_e('Custom Media', 'shortpixel-image-optimiser'); ?> </span>
<span>
<span class='line-progressbar'>
<span class='done-text'><i data-stats-custom="percentage_done"></i> %</span>
<span class='done' data-stats-custom="percentage_done" data-presentation="css.width.percentage"></span>
</span>
<span class='dashicons spin dashicons-update line-progressbar-spinner' data-check-visibility data-control="data-check-custom-in_process">&nbsp;</span>
</span>
<span><?php esc_html_e('Processing', 'shortpixel-image-optimiser') ?>: <i data-stats-custom="in_process" data-check-custom-in_process>-</i></span>
</div>
<div>
<span><?php esc_html_e('Processed','shortpixel-image-optimiser'); ?>: <i data-stats-custom="done">-</i></span>
<span><?php esc_html_e('Waiting','shortpixel-image-optimiser'); ?>: <i data-stats-custom="in_queue">-</i></span>
<span><?php esc_html_e('Errors') ?>: <i data-check-custom-fatalerrors data-stats-custom="fatal_errors" class='error'>-</i>
<span class="display-error-box" data-check-visibility data-control="data-check-custom-fatalerrors" ><label title="<?php esc_html_e('Show Errors', 'shortpixel-image-optimiser'); ?>">
<input type="checkbox" name="show-errors" value="show" data-action='ToggleErrorBox' data-errorbox='custom' data-event='change'><?php esc_html_e('Show Errors','shortpixel-image-optimiser'); ?></label>
</span>
</span>
</div>
</div>
<div data-error-custom="message" data-presentation="append" class='errorbox custom'>
<?php if(property_exists($this->view, 'customErrorLog') && $this->view->customErrorLog !== false)
{
echo $this->view->customErrorLog;
}
?>
</div>
<nav>
<button class='button stop' type='button' data-action="StopBulk" >
<?php esc_html_e('Stop Bulk Processing' ,'shortpixel-image-optimiser'); ?>
</button>
<button class='button pause' type='button' data-action="PauseBulk" id="PauseBulkButton">
<?php esc_html_e('Pause Bulk Processing' ,'shortpixel-image-optimiser') ?>
</button>
<button class='button button-primary resume' type='button' data-action='ResumeBulk' id="ResumeBulkButton">
<?php esc_html_e('Resume Bulk Processing','shortpixel-image-optimiser'); ?>
</button>
</nav>
<div class='image-preview-section hidden'> <!-- /hidden -->
<div class='title'><?php esc_html_e('Just Optimized', 'shortpixel-image-optimiser'); ?></div>
<div class="image-preview-line">
<!-- <strong data-result="queuetype"></strong> -->
<span>&nbsp;</span> <!-- Spacer for flex -->
<span data-result="filename">&nbsp;</span>
<svg class="opt-circle-image" viewBox="0 0 100 100">
<path class="trail" d="
M 50,50
m 0,-46
a 46,46 0 1 1 0,92
a 46,46 0 1 1 0,-92
" stroke-width="8" fill-opacity="0">
</path>
<path class="path" d="
M 50,50
m 0,-46
a 46,46 0 1 1 0,92
a 46,46 0 1 1 0,-92
" stroke-width="8" fill-opacity="0" style="stroke-dasharray: 289.027px, 289.027px; stroke-dashoffset: 180px;">
</path>
<text class="text" x="50" y="50">-- %</text>
</svg>
</div>
<div class="preview-wrapper">
<div class="slide-mask" id="preview-structure" data-placeholder="<?php echo esc_url(\wpSPIO()->plugin_url('res/img/bulk/placeholder.svg')); ?>">
<div class='current preview-image'>
<div class="image source">
<img src="<?php echo esc_url(\wpSPIO()->plugin_url('res/img/bulk/placeholder.svg')); ?>" >
<p><?php esc_html_e('Original Image', 'shortpixel-image-optimiser'); ?></p>
<?php $this->loadView('snippets/part-svgloader', false); ?>
</div>
<div class="image result">
<img src="<?php echo esc_url(\wpSPIO()->plugin_url('res/img/bulk/placeholder.svg')); ?>" >
<p><?php esc_html_e('Optimized Image', 'shortpixel-image-optimiser'); ?>
- <span data-result="improvements-totalpercentage"></span>% <?php _e('smaller', 'shortpixel-image-optimiser'); ?>
</p>
<?php $this->loadView('snippets/part-svgloader', false); ?>
</div>
</div>
<div class='new preview-image'>
<div class="image source">
<img src="<?php echo esc_url(\wpSPIO()->plugin_url('res/img/bulk/placeholder.svg')); ?>" >
<?php $this->loadView('snippets/part-svgloader', false); ?>
<p><?php esc_html_e('Original Image','shortpixel-image-optimiser'); ?></p>
</div>
<div class="image result">
<img src="<?php echo esc_url(\wpSPIO()->plugin_url('res/img/bulk/placeholder.svg')); ?>" >
<?php $this->loadView('snippets/part-svgloader', false); ?>
<p><?php esc_html_e('Optimized Image','shortpixel-image-optimiser'); ?>
- <span data-result="improvements-totalpercentage"></span>% <?php _e('smaller', 'shortpixel-image-optimiser'); ?>
</p>
</div>
</div>
</div> <!-- slidemask -->
</div> <!-- preview wrapper -->
</div>
<div id="preloader" class="hidden">
</div>
</section>

View File

@ -0,0 +1,33 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<section class='spio-progressbar'>
<div class="flex">
<div class="select">
<span class='line'></span>
<span class="step">1</span>
<span class="text"><?php esc_html_e('Select Images','shortpixel-image-optimiser'); ?></span>
</div>
<div class="summary">
<span class='line'></span>
<span class="step">2</span>
<span class="text"><?php esc_html_e('Summary','shortpixel-image-optimiser'); ?></span>
</div>
<div class="process">
<span class='line'></span>
<span class="step">3</span>
<span class="text"><?php esc_html_e('Bulk Process','shortpixel-image-optimiser'); ?></span>
</div>
<div class="result">
<span class='line'></span>
<span class="step">4</span>
<span class="text"><?php esc_html_e('Results','shortpixel-image-optimiser'); ?></span>
</div>
</div>
</section>

View File

@ -0,0 +1,190 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
$approx = $this->view->approx;
?>
<section class='panel selection' data-panel="selection" data-status="loaded" >
<div class="panel-container">
<span class='hidden' data-check-custom-hascustom >
<?php echo ($this->view->approx->custom->has_custom === true) ? 1 : 0; ?>
</span>
<h3 class="heading"><span><img src="<?php echo esc_url(\wpSPIO()->plugin_url('res/img/robo-slider.png')); ?>"></span>
<?php esc_html_e('ShortPixel Bulk Optimization - Select Images', 'shortpixel-image-optimiser'); ?>
</h3>
<p class='description'><?php esc_html_e('Select the type of images that ShortPixel should optimize for you.','shortpixel-image-optimiser'); ?></p>
<?php $this->loadView('bulk/part-progressbar', false); ?>
<div class='load wrapper' >
<div class='loading'>
<span><img src="<?php echo esc_url(\wpSPIO()->plugin_url('res/img/bulk/loading-hourglass.svg')); ?>" /></span>
<span>
<p><?php esc_html_e('Please wait, ShortPixel is checking the images to be processed...','shortpixel-image-optimiser'); ?><br>
<span class="number" data-stats-total="total">x</span> <?php esc_html_e('items found', 'shortpixel-image-optimiser'); ?></p>
</span>
</div>
<div class='loading skip'>
<span><p><button class='button' data-action="SkipPreparing"><?php _e('Start now', 'shortpixel-image-optimiser'); ?></button></p>
</span>
<span>
<p><?php _e("Clicking this button will start optimization of the items added to the queue. The remaining items can be processed in a new bulk. After completion, you can start bulk and the system will continue with the unprocessed images.",'shortpixel-image-optimiser'); ?></p>
</span>
</div>
</div>
<div class="interface wrapper">
<div class="option-block">
<h2><?php esc_html_e('Optimize:','shortpixel-image-optimiser'); ?> </h2>
<p><?php printf(esc_html__('ShortPixel has %sestimated%s the number of images that can still be optimized. %sAfter you select the options, the plugin will calculate exactly how many images to optimize.','shortpixel-image-optimiser'), '<b>','</b>', '<br />'); ?></p>
<?php if ($approx->media->isLimited): ?>
<h4 class='count_limited'><?php esc_html_e('ShortPixel has detected a high number of images. This estimates are limited for performance reasons. On the next step an accurate count will be produced', 'shortpixel-image-optimiser'); ?></h4>
<?php endif; ?>
<div class="media-library optiongroup">
<div class='switch_button'>
<label>
<input type="checkbox" class="switch" id="media_checkbox" checked>
<div class="the_switch">&nbsp; </div>
</label>
</div>
<h4><label for="media_checkbox"><?php esc_html_e('Media Library','shortpixel-image-optimiser'); ?></label></h4>
<div class='option'>
<label><?php esc_html_e('Images (estimate)', 'shortpixel-image-optimiser'); ?></label>
<span class="number" ><?php echo esc_html($approx->media->items) ?></span>
</div>
<?php if (\wpSPIO()->settings()->processThumbnails == 1): ?>
<div class='option'>
<label><?php esc_html_e('Thumbnails (estimate)','shortpixel-image-optimiser'); ?></label> <span class="number" ><?php echo esc_html($approx->media->thumbs) ?> </span>
</div>
<?php endif; ?>
</div>
<?php if (! \wpSPIO()->settings()->processThumbnails): ?>
<div class='thumbnails optiongroup'>
<div class='switch_button'>
<label>
<input type="checkbox" class="switch" id="thumbnails_checkbox" <?php checked(\wpSPIO()->settings()->processThumbnails); ?>>
<div class="the_switch">&nbsp; </div>
</label>
</div>
<h4><label for="thumbnails_checkbox"><?php esc_html_e('Process Image Thumbnails','shortpixel-image-optimiser'); ?></label></h4>
<div class='option'>
<label><?php esc_html_e('Thumbnails (estimate)','shortpixel-image-optimiser'); ?></label>
<span class="number" ><?php echo esc_html($approx->media->total) ?> </span>
</div>
<p><?php esc_html_e('It is recommended to process the WordPress thumbnails. These are the small images that are most often used in posts and pages.This option changes the global ShortPixel settings of your site.','shortpixel-image-optimiser'); ?></p>
</div>
<?php endif; ?>
<div class="custom-images optiongroup" data-check-visibility data-control="data-check-custom-hascustom" >
<div class='switch_button'>
<label>
<input type="checkbox" class="switch" id="custom_checkbox" checked>
<div class="the_switch">&nbsp; </div>
</label>
</div>
<h4><label for="custom_checkbox"><?php esc_html_e('Custom Media images','shortpixel-image-optimiser') ?></label></h4>
<div class='option'>
<label><?php esc_html_e('Images (estimate)','shortpixel-image-optimiser'); ?></label>
<span class="number" ><?php echo esc_html($approx->custom->images) ?></span>
</div>
</div>
</div> <!-- block -->
<div class="option-block selection-settings">
<h2><?php esc_html_e('Options','shortpixel-image-optimiser') ?>: </h2>
<p><?php esc_html_e('Enable these options if you also want to create WebP/AVIF files. These options change the global ShortPixel settings of your site.','shortpixel-image-optimiser'); ?></p>
<div class='optiongroup' >
<div class='switch_button'>
<label>
<input type="checkbox" class="switch" id="webp_checkbox" name="webp_checkbox"
<?php checked(\wpSPIO()->settings()->createWebp); ?> />
<div class="the_switch">&nbsp; </div>
</label>
</div>
<h4><label for="webp_checkbox">
<?php printf(esc_html__('Also create WebP versions of the images' ,'shortpixel-image-optimiser') ); ?>
</label></h4>
<div class="option"><?php esc_html_e('The total number of WebP images will be calculated in the next step.','shortpixel-image-optimiser'); ?></div>
</div>
<?php
$avifEnabled = $this->access()->isFeatureAvailable('avif');
$createAvifChecked = (\wpSPIO()->settings()->createAvif == 1 && $avifEnabled === true) ? true : false;
$disabled = ($avifEnabled === false) ? 'disabled' : '';
?>
<div class='optiongroup'>
<div class='switch_button'>
<label>
<input type="checkbox" class="switch" id="avif_checkbox" name="avif_checkbox" <?php echo $disabled ?>
<?php checked($createAvifChecked); ?> />
<div class="the_switch">&nbsp; </div>
</label>
</div>
<h4><label for="avif_checkbox"><?php esc_html_e('Also create AVIF versions of the images','shortpixel-image-optimiser'); ?></label></h4>
<?php if ($avifEnabled == true): ?>
<div class="option"><?php esc_html_e('The total number of AVIF images will be calculated in the next step.','shortpixel-image-optimiser'); ?></div>
</div>
<?php else : ?>
<div class="option warning"><?php printf(esc_html__('The creation of AVIF files is not possible with this license type. %s Read more %s ','shortpixel-image-optimiser'), '<a href="https://shortpixel.com/knowledge-base/article/555-how-does-the-unlimited-plan-work" target="_blank">', '</a>'); ?>
</div>
</div>
<?php endif; ?>
</div>
<div class="option-block">
<div class='optiongroup' data-check-visibility="false" data-control="data-check-approx-total">
<h3><?php esc_html_e('No images found', 'shortpixel-image-optimiser'); ?></h3>
<p><?php esc_html_e('ShortPixel Bulk couldn\'t find any optimizable images.','shortpixel-image-optimiser'); ?></p>
</div>
<h4 class='approx'><?php esc_html_e('An estimate of unoptimized images in this installation', 'shortpixel-image-optimiser'); ?> :
<span data-check-approx-total><?php echo esc_html($approx->total->images) ?></span> </h4>
<div><p><?php printf(__('In the next step, the plugin will calculate the total number of images to be optimized, and your bulk process will be prepared. The processing %s will not start yet %s, but a summary of the images to be optimized will be displayed.', 'shortpixel-image-optimiser'),'<b>','</b>'); ?></p></div>
</div>
<nav>
<button class="button" type="button" data-action="FinishBulk">
<span class='dashicons dashicons-arrow-left'></span>
<p><?php esc_html_e('Back', 'shortpixel-image-optimiser'); ?></p>
</button>
<button class="button-primary button" type="button" data-action="CreateBulk" data-panel="summary" data-check-disable data-control="data-check-total-total">
<span class='dashicons dashicons-arrow-right'></span>
<p><?php esc_html_e('Calculate', 'shortpixel-image-optimiser'); ?></p>
</button>
</nav>
</div> <!-- interface wrapper -->
</div><!-- container -->
</section>

View File

@ -0,0 +1,140 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<section class="panel summary" data-panel="summary">
<div class="panel-container">
<h3 class="heading"><span><img src="<?php echo esc_url(\wpSPIO()->plugin_url('res/img/robo-slider.png')); ?>"></span>
<?php esc_html_e('ShortPixel Bulk Optimization - Summary','shortpixel-image-optimiser'); ?>
</h3>
<p class='description'><?php esc_html_e('Welcome to the bulk optimization wizard, where you can select the images that ShortPixel will optimize in the background for you.','shortpixel-image-optimiser'); ?></p>
<?php $this->loadView('bulk/part-progressbar', false); ?>
<div class='summary-list'>
<h3><?php esc_html_e('Review and start the Bulk Process', 'shortpixel-image-optimiser'); ?>
<span>
<img src="<?php echo esc_url(wpSPIO()->plugin_url('res/img/robo-notes.png')); ?>" style="transform: scale(-1, 1);height: 50px;"/>
</span>
</h3>
<div class="section-wrapper" data-check-visibility data-control="data-check-media-total">
<h4><span class='dashicons dashicons-images-alt2'>&nbsp;</span>
<?php esc_html_e('Media Library','shortpixel-image-optimiser'); ?> (<span data-stats-media="in_queue">0</span> <?php esc_html_e('items','shortpixel-image-optimiser'); ?>)</h4>
<div class="list-table">
<div><span><?php esc_html_e('Images','shortpixel-image-optimiser'); ?></span>
<span data-stats-media="images-images_basecount">n/a</span>
</div>
<div class='filetypes' data-check-visibility data-control="data-check-has-webp">
<span>&nbsp; <?php esc_html_e('+ WebP images','shortpixel-image-optimiser'); ?> </span><span data-stats-media="images-images_webp" data-check-has-webp>&nbsp;</span>
</div>
<div class='filetypes' data-check-visibility data-control="data-check-has-avif">
<span>&nbsp; <?php esc_html_e('+ AVIF images','shortpixel-image-optimiser'); ?> </span><span data-stats-media="images-images_avif" data-check-has-avif>&nbsp;</span>
</div>
<div><span><?php esc_html_e('Total from Media Library','shortpixel-image-optimiser'); ?></span><span data-stats-media="images-images">0</span></div>
</div>
</div>
<div class="section-wrapper" data-check-visibility data-control="data-check-custom-total">
<h4><span class='dashicons dashicons-open-folder'>&nbsp;</span><?php esc_html_e('Custom Media', 'shortpixel-image-optimiser') ?> (<span data-stats-custom="in_queue">0</span> <?php esc_html_e('items','shortpixel-image-optimiser'); ?>)</h4>
<div class="list-table">
<div><span><?php esc_html_e('Images','shortpixel-image-optimiser'); ?></span>
<span data-stats-custom="images-images_basecount">n/a</span>
</div>
<div class='filetypes' data-check-visibility data-control="data-check-has-custom-webp" ><span>&nbsp; <?php esc_html_e('+ WebP images','shortpixel-image-optimiser'); ?></span>
<span data-stats-custom="images-images_webp" data-check-has-custom-webp>&nbsp;</span>
</div>
<div class='filetypes' data-check-visibility data-control="data-check-has-custom-avif">
<span>&nbsp; <?php esc_html_e('+ AVIF images','shortpixel-image-optimiser'); ?></span><span data-stats-custom="images-images_avif" data-check-has-custom-avif>&nbsp;</span>
</div>
<div><span><?php esc_html_e('Total from Custom Media','shortpixel-image-optimiser'); ?></span><span data-stats-custom="images-images">0</span></div>
</div>
</div>
<?php
$quotaData = $this->view->quotaData;
?>
<div class="totals">
<?php
$quotaData->unlimited ? esc_html_e('Total','shortpixel-image-optimiser') : esc_html_e('Total credits needed','shortpixel-image-optimiser');
?>: <span class="number" data-stats-total="images-images" data-check-total-total >0</span>
<span class='number'></span>
</div>
</div>
<?php
if(true === $quotaData->unlimited): ?>
<div class='credits'>
<p><span><?php _e('This site is currently on the ShortPixel Unlimited plan, so you do not have to worry about credits. Enjoy!', 'shortpixel-image-optimiser'); ?></span></p>
</div>
<?php else: ?>
<div class="credits">
<p class='heading'><span><?php esc_html_e('Your ShortPixel Credits Available', 'shortpixel-image-optimiser'); ?></span>
<span><?php echo esc_html($this->formatNumber($quotaData->total->remaining, 0)) ?></span>
<span><a href="<?php echo esc_url($this->view->buyMoreHref) ?>" target="_new" class='button button-primary'><?php esc_html_e('Buy unlimited credits','shortpixel-image-optimiser'); ?></a></span>
</p>
<p><span><?php esc_html_e('Your monthly plan','shortpixel-image-optimiser'); ?></span>
<span><?php echo esc_html($quotaData->monthly->text) ?> <br>
<?php esc_html_e('Used:', 'shortpixel-image-optimiser'); ?> <?php echo esc_html($this->formatNumber($quotaData->monthly->consumed, 0)); ?>
<?php esc_html_e('; Remaining:', 'shortpixel-image-optimiser'); ?> <?php echo esc_html($this->formatNumber($quotaData->monthly->remaining, 0)); ?>
</span>
</p>
<p>
<span><?php esc_html_e('Your one-time credits') ?></span>
<span><?php echo esc_html($quotaData->onetime->text) ?> <br>
<?php esc_html_e('Used:', 'shortpixel-image-optimiser'); ?> <?php echo esc_html($this->formatNumber($quotaData->onetime->consumed, 0)); ?>
<?php esc_html_e('; Remaining:', 'shortpixel-image-optimiser'); ?> <?php echo esc_html($this->formatNumber($quotaData->onetime->remaining, 0)) ?>
</span>
</p>
</div>
<div class="over-quota" data-check-visibility="false" data-control="data-quota-remaining" data-control-check="data-check-total-total">
<span><img src="<?php echo esc_url(wpSPIO()->plugin_url('res/img/bulk/over-quota.svg')) ?>" /></span>
<p><?php printf(esc_html('In your ShortPixel account you %shave only %s credits available %s, but you have chosen %s images to be optimized in this bulk process. You can either go back and select less images, or you can upgrade to a higher plan or buy one-time credits.','shortpixel-image-optimiser'), '<span class="red">', esc_html($this->formatNumber($quotaData->total->remaining, 0)), '</span>', '<b data-stats-total="images-images">0</b>'); ?>
<button type="button" class="button" onClick="ShortPixel.proposeUpgrade();"><?php esc_html_e('Show me the best options') ?></button>
</p>
<span class='hidden' data-quota-remaining><?php
// This is hidden check, no number format.
echo esc_html($quotaData->total->remaining);
?></span>
</div>
<?php $this->loadView('snippets/part-upgrade-options'); ?>
<?php endif;
?>
<div class='no-images' data-check-visibility="false" data-control="data-check-total-total">
<?php esc_html_e('The current selection contains no images. The bulk process cannot start.', 'shortpixel-image-optimiser'); ?>
</div>
<nav>
<button class="button" type="button" data-action="open-panel" data-panel="selection">
<span class='dashicons dashicons-arrow-left' ></span>
<p><?php esc_html_e('Back','shortpixel-image-optimiser'); ?></p>
</button>
<button class="button-primary button" type="button" data-action="StartBulk" data-control="data-check-total-total" data-check-presentation="disable">
<span class='dashicons dashicons-arrow-right'></span>
<p><?php esc_html_e('Start Bulk Optimization', 'shortpixel-image-optimiser'); ?></p>
</button>
</nav>
</div>
</section>

View File

@ -0,0 +1,14 @@
<?php
namespace ShortPixel;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<div class='pagination tablenav bottom'>
<div class='tablenav-pages'>
<?php echo $this->view->pagination; ?>
</div>
</div>

View File

@ -0,0 +1,73 @@
<?php
namespace ShortPixel;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<div class="wrap shortpixel-other-media">
<h2>
<?php esc_html_e($view->title);?>
</h2>
<div class='toolbar'>
<hr class='wp-header-end' />
<?php if (property_exists($view, 'show_search') && true === $view->show_search): ?>
<div class="searchbox">
<form method="get">
<input type="hidden" name="page" value="wp-short-pixel-custom" />
<input type='hidden' name='order' value="<?php echo esc_attr($this->order) ?>" />
<input type="hidden" name="orderby" value="<?php echo esc_attr($this->orderby) ?>" />
<p class="search-form">
<label><?php esc_html_e('Search', 'shortpixel-image-optimiser'); ?></label>
<input type="text" name="s" value="<?php echo esc_attr($this->search) ?>" />
</p>
</form>
</div>
</div>
<?php endif; ?>
<div class='pagination tablenav'>
<?php if ($this->view->pagination !== false): ?>
<div class='tablenav-pages'>
<?php echo $this->view->pagination; ?>
</div>
<?php endif; ?>
</div>
<?php
$file_url = esc_url(add_query_arg('part', 'files', $this->url));
$folder_url = esc_url(add_query_arg('part', 'folders', $this->url));
$scan_url = esc_url(add_query_arg('part', 'scan', $this->url));
$current_part = isset($_GET['part']) ? sanitize_text_field($_GET['part']) : 'files';
$tabs = array(
'files' => array('link' => $file_url,
'text' => __('Files', 'shortpixel-image-optimiser'),
),
'folders' => array('link' => $folder_url,
'text' => __('Folders', 'shortpixel-image-optimiser'),
),
'scan' => array('link' => $scan_url,
'text' => __('Scan', 'shortpixel-image-optimiser'),
),
);
?>
<div class="custom-media-tabs">
<?php foreach($tabs as $tabName => $tab)
{
$class = ($current_part == $tabName) ? ' class="selected" ' : '';
echo '<a href="' . $tab['link'] . '" ' . $class . '>' . $tab['text'] . '</a>';
} ?>
</div>

Some files were not shown because too many files have changed in this diff Show More