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